diff --git a/src/api/notification.ts b/src/api/notification.ts index ab4cff9..d1cd322 100644 --- a/src/api/notification.ts +++ b/src/api/notification.ts @@ -8,14 +8,10 @@ export interface notification { duration?: duration } -export const getNotification = async (): Promise => { +export const getNotification = async (): Promise<{ data: notification }> => { try { const res = await axiosInstance.get(`/pose-notifications`) - - if (!res.data?.data) return null - - const { id, duration } = res.data.data - return { id, duration } + return res.data } catch (e) { throw e } @@ -24,19 +20,18 @@ export const getNotification = async (): Promise => { export const registerNotification = async (notification: notification): Promise => { try { const res = await axiosInstance.post(`/pose-notifications`, { ...notification }) - const { id, duration } = res.data.data - return { id, duration } + return res.data.data } catch (e) { throw e } } -export const updateNotification = async (notification: notification): Promise => { - try { - const res = await axiosInstance.patch(`/pose-notifications/${notification.id}`, { ...notification }) - const { id, isActive, duration } = res.data.data - return { id, isActive, duration } - } catch (e) { - throw e - } -} +// export const updateNotification = async (notification: notification): Promise => { +// try { +// const res = await axiosInstance.patch(`/pose-notifications/${notification.id}`, { ...notification }) +// const { id, isActive, duration } = res.data.data +// return { id, isActive, duration } +// } catch (e) { +// throw e +// } +// } diff --git a/src/components/PoseDetector.tsx b/src/components/PoseDetector.tsx index 44987c7..9e62912 100644 --- a/src/components/PoseDetector.tsx +++ b/src/components/PoseDetector.tsx @@ -18,6 +18,7 @@ import Camera from "./Camera" import Controls from "./Posture/Controls" import GuidePopupModal from "./Posture/GuidePopup/GuidePopupModal" import PostureMessage from "./Posture/PostureMessage" +import useNotification from "@/hooks/useNotification" const PoseDetector: React.FC = () => { const [isScriptLoaded, setIsScriptLoaded] = useState(false) @@ -53,7 +54,8 @@ const PoseDetector: React.FC = () => { const createSnapMutation = useCreateSnaphot() const sendPoseMutation = useSendPose() - const userNoti = useNotificationStore((state) => state.notification) + // const userNoti = useNotificationStore((state) => state.notification) + const { notification } = useNotification() const { requestNotificationPermission } = usePushNotification() const { hasPermission } = useCameraPermission() @@ -152,7 +154,7 @@ const PoseDetector: React.FC = () => { const _isTextNeck = detectTextNeck(snapRef.current, results, true, 0.88) const _isHandOnChin = detectHandOnChin(snapRef.current, results) const _isTailboneSit = detectTailboneSit(snapRef.current, results) - const _isShowNoti = userNoti?.duration === "IMMEDIATELY" && userNoti?.isActive + const _isShowNoti = notification?.duration === "IMMEDIATELY" && notification?.isActive if (_isShoulderTwist !== null) setIsShoulderTwist(_isShoulderTwist) if (_isTextNeck !== null) setIsTextNeck(_isTextNeck) @@ -177,7 +179,15 @@ const PoseDetector: React.FC = () => { if (canvasRef.current) drawPose(results, canvasRef.current) } }, - [setIsShoulderTwist, setIsTextNeck, setIsHandOnChin, setIsTailboneSit, isSnapShotSaved, managePoseTimer, userNoti] + [ + setIsShoulderTwist, + setIsTextNeck, + setIsHandOnChin, + setIsTailboneSit, + isSnapShotSaved, + managePoseTimer, + notification, + ] ) const detectStart = useCallback( @@ -306,22 +316,20 @@ const PoseDetector: React.FC = () => { }, [snapshot]) useEffect(() => { - if (!isSnapShotSaved || !userNoti) return + if (!isSnapShotSaved || !notification) return clearCnt() clearInterval(notificationTimer.current) notificationTimer.current = null - if (userNoti.isActive && userNoti.duration && userNoti.duration !== "IMMEDIATELY") { - const t = getDurationInMinutes(userNoti?.duration) + if (notification.isActive && notification.duration && notification.duration !== "IMMEDIATELY") { + const t = getDurationInMinutes(notification.duration) notificationTimer.current = setInterval(() => { - if (userNoti.duration) { - sendNotification() - clearCnt() - } + sendNotification() + clearCnt() }, 1000 * 60 * t) } - }, [userNoti, isSnapShotSaved]) + }, [notification, isSnapShotSaved]) // 팝업 열기 const handleShowPopup = (): void => { diff --git a/src/components/Posture/PostrueCrew.tsx b/src/components/Posture/PostrueCrew.tsx index eded8ad..5bd9c74 100644 --- a/src/components/Posture/PostrueCrew.tsx +++ b/src/components/Posture/PostrueCrew.tsx @@ -1,9 +1,11 @@ import { duration, notification } from "@/api/notification" import { useModals } from "@/hooks/useModals" -import { usePatchNoti } from "@/hooks/useNotiMutation" +import useNotification from "@/hooks/useNotification" +import { useModifyNoti } from "@/hooks/useNotiMutation" import usePushNotification from "@/hooks/usePushNotification" +import { useCreateSnaphot } from "@/hooks/useSnapshotMutation" import { useAuthStore } from "@/store" -import { useNotificationStore } from "@/store/NotificationStore" +import { useSnapShotStore } from "@/store/SnapshotStore" import CloseCrewPanelIcon from "@assets/icons/crew-panel-close-button.svg?react" import PostureGuide from "@assets/icons/posture-guide-button-icon.svg?react" import PostureRetakeIcon from "@assets/icons/posture-snapshot-retake-icon.svg?react" @@ -12,8 +14,6 @@ import RankingGuideToolTip from "@assets/images/ranking-guide.png" import SelectBox from "@components/SelectBox" import { ReactElement, useCallback, useEffect, useRef, useState } from "react" import { modals } from "../Modal/Modals" -import { useSnapShotStore } from "@/store/SnapshotStore" -import { useCreateSnaphot } from "@/hooks/useSnapshotMutation" interface IPostureCrew { groupUserId: number @@ -40,118 +40,120 @@ const NOTI_OPTIONS: NotiOption[] = [ { value: "MIN_60", label: "1시간 간격" }, ] -const MAX_RECONNECT_ATTEMPTS = 5 -const INITIAL_RECONNECT_DELAY = 1000 -const UPDATE_INTERVAL = 1000 // 1초마다 상태 업데이트 +const NOTI_VALUE_MAP = (value: string | undefined) => { + switch (value) { + case "IMMEDIATELY": + return "틀어진 즉시" + case "MIN_15": + return "15분 간격" + case "MIN_30": + return "30분 간격" + case "MIN_45": + return "45분 간격" + case "MIN_60": + return "1시간 간격" + } + return "틀어진 즉시" +} -export default function PostrueCrew(props: PostureCrewProps): ReactElement { - const { toggleSidebar } = props - const accessToken = useAuthStore((state) => state.accessToken) - const { resetSnapShot } = useSnapShotStore() - const { openModal } = useModals() - const createSnapMutation = useCreateSnaphot() - const [crews, setCrews] = useState([]) +const useWebSocket = (url: string) => { const [isConnected, setIsConnected] = useState<"loading" | "success" | "disconnected">("loading") - const [socket, setSocket] = useState(null) - const [reconnectAttempts, setReconnectAttempts] = useState(0) - const latestCrewsRef = useRef([]) - const updateTimeoutRef = useRef(null) + const [crews, setCrews] = useState([]) + const socketRef = useRef(null) + const reconnectTimeoutRef = useRef(null) - const throttledUpdateCrews = useCallback(() => { - if (!updateTimeoutRef.current) { - updateTimeoutRef.current = setTimeout(() => { - setCrews(latestCrewsRef.current) - updateTimeoutRef.current = null - }, UPDATE_INTERVAL) + const connect = useCallback(() => { + if (socketRef.current?.readyState === WebSocket.OPEN) { + return } - }, []) - - const userNoti = useNotificationStore((state) => state.notification) - const setUserNoti = useNotificationStore((state) => state.setNotification) - const patchNotiMutation = usePatchNoti() - const { hasPermission } = usePushNotification() - const [isEnabled, setIsEnabled] = useState(userNoti?.isActive) - const [notiAlarmTime, setNotiAlarmTime] = useState(NOTI_OPTIONS.find((n) => n.value === userNoti?.duration)?.label) + socketRef.current = new WebSocket(url) - const connectWebSocket = useCallback(() => { - const newSocket = new WebSocket(`wss://api.alignlab.site/ws/v1/groups/1/users?X-HERO-AUTH-TOKEN=${accessToken}`) - - newSocket.onopen = () => { + socketRef.current.onopen = () => { console.log("WebSocket connected") setIsConnected("success") - setReconnectAttempts(0) } - newSocket.onmessage = (event) => { + socketRef.current.onmessage = (event) => { const data = JSON.parse(event.data) console.log("Received message:", data) if (data.groupUsers) { - latestCrewsRef.current = data.groupUsers - throttledUpdateCrews() + setCrews(data.groupUsers) } } - newSocket.onerror = (error) => { + socketRef.current.onerror = (error) => { console.error("WebSocket error:", error) } - newSocket.onclose = (event) => { + socketRef.current.onclose = (event) => { console.log("WebSocket disconnected. Code:", event.code, "Reason:", event.reason) setIsConnected("disconnected") - - if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { - const delay = INITIAL_RECONNECT_DELAY * Math.pow(2, reconnectAttempts) - console.log(`Attempting to reconnect in ${delay}ms...`) - setTimeout(() => { - setReconnectAttempts((prev) => prev + 1) - connectWebSocket() - }, delay) - } else { - console.log("Max reconnection attempts reached. Please try again later.") - } + reconnect() } + }, [url]) - setSocket(newSocket) - }, [accessToken, reconnectAttempts, throttledUpdateCrews]) + const reconnect = useCallback(() => { + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current) + } + reconnectTimeoutRef.current = setTimeout(() => { + console.log("Attempting to reconnect...") + connect() + }, 5000) // 5초 후 재연결 시도 + }, [connect]) useEffect(() => { - connectWebSocket() + connect() return () => { - if (socket) { - socket.close() + if (socketRef.current) { + socketRef.current.close() } - if (updateTimeoutRef.current) { - clearTimeout(updateTimeoutRef.current) + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current) } } - }, [connectWebSocket]) + }, [connect]) + + return { isConnected, crews } +} + +export default function PostrueCrew(props: PostureCrewProps): ReactElement { + const { toggleSidebar } = props + const accessToken = useAuthStore((state) => state.accessToken) + const { resetSnapShot } = useSnapShotStore() + const { openModal } = useModals() + const createSnapMutation = useCreateSnaphot() + const wsUrl = `wss://api.alignlab.site/ws/v1/groups/1/users?X-HERO-AUTH-TOKEN=${accessToken}` + const { isConnected, crews } = useWebSocket(wsUrl) + + const { notification, setNotification } = useNotification() + const updateNotiMutation = useModifyNoti() + const { hasPermission } = usePushNotification() const onClickCloseSideNavButton = (): void => { toggleSidebar() } const onClickNotiAlarmTime = (option: NotiOption): void => { - setNotiAlarmTime(option.label) - patchNotiMutation.mutate( - { id: userNoti?.id, duration: option.value }, + updateNotiMutation.mutate( + { isActive: notification?.isActive, duration: option.value }, { onSuccess: (data: notification) => { - setNotiAlarmTime(option.label) - setUserNoti(data) + setNotification(data) }, } ) } const onClickNotiAlarm = (): void => { - patchNotiMutation.mutate( - { id: userNoti?.id, isActive: !userNoti?.isActive }, + updateNotiMutation.mutate( + { isActive: !notification?.isActive, duration: notification?.duration }, { onSuccess: (data: notification) => { - setIsEnabled(data.isActive) - setUserNoti(data) + console.log("#### : ", data) + setNotification(data) }, } ) @@ -168,6 +170,8 @@ export default function PostrueCrew(props: PostureCrewProps): ReactElement { }) } + console.log("notification: ", notification) + return (