From 83b01e5fda8bfdf84a45402a72f22035151ab111 Mon Sep 17 00:00:00 2001 From: G-hoon Date: Tue, 24 Sep 2024 02:00:03 +0900 Subject: [PATCH] =?UTF-8?q?[feat/#58]=20=EC=82=AC=EC=9D=B4=EB=93=9C=20?= =?UTF-8?q?=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9E=AC=EC=B4=AC=EC=98=81=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95,=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../icons/posture-snapshot-retake-icon.svg | 12 ++ .../Modal/GoodPostureGuideModal.tsx | 55 +++++++++ src/components/Modal/Modals.tsx | 6 +- src/components/PoseDetector.tsx | 110 ++++++++---------- src/components/Posture/Controls.tsx | 57 ++++----- .../{GuidePopup.tsx => GuidePopupModal.tsx} | 4 +- src/components/Posture/PostrueCrew.tsx | 76 +++++++++--- src/components/Posture/PostureMessage.tsx | 6 +- src/components/SideNav.tsx | 2 +- src/hooks/useGuidePopup.tsx | 24 ++++ src/hooks/useSnapShot.ts | 31 +++++ src/pages/AuthPage.tsx | 4 +- src/pages/MonitoringPage.tsx | 22 ++-- src/store/GuidePopupStore.ts | 27 +++++ src/store/SnapshotStore.ts | 8 +- 15 files changed, 309 insertions(+), 135 deletions(-) create mode 100644 src/assets/icons/posture-snapshot-retake-icon.svg create mode 100644 src/components/Modal/GoodPostureGuideModal.tsx rename src/components/Posture/GuidePopup/{GuidePopup.tsx => GuidePopupModal.tsx} (90%) create mode 100644 src/hooks/useGuidePopup.tsx create mode 100644 src/hooks/useSnapShot.ts create mode 100644 src/store/GuidePopupStore.ts diff --git a/src/assets/icons/posture-snapshot-retake-icon.svg b/src/assets/icons/posture-snapshot-retake-icon.svg new file mode 100644 index 0000000..a643b44 --- /dev/null +++ b/src/assets/icons/posture-snapshot-retake-icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/components/Modal/GoodPostureGuideModal.tsx b/src/components/Modal/GoodPostureGuideModal.tsx new file mode 100644 index 0000000..2a3e7ce --- /dev/null +++ b/src/components/Modal/GoodPostureGuideModal.tsx @@ -0,0 +1,55 @@ +import CloseCrewPanelIcon from "@assets/images/posture-snapshot-guide.png" +import { ReactElement } from "react" +import ModalContainer from "../ModalContainer" + +const GoodPostureGuidePopupModal = ({ onClose }: { onClose: () => void }): ReactElement => { + return ( + +
+ {/* blur 처리 */} +
+
+
바른 자세 가이드
+
+ {/* content */} +
+
+ 스냅샷 가이드 +
+
+
+ + 1 + + 머리와 목을 일직선으로 곧게 펴기 +
+
+ + 2 + + 양쪽 어깨 일직선 유지하기 +
+
+ + 3 + + 팔은 책상 위에 수평으로 두기 +
+
+ + 4 + + 등과 허리는 등받이에 지지하기 +
+
+
+ +
+
+
+ ) +} + +export default GoodPostureGuidePopupModal diff --git a/src/components/Modal/Modals.tsx b/src/components/Modal/Modals.tsx index 497e838..0765d63 100644 --- a/src/components/Modal/Modals.tsx +++ b/src/components/Modal/Modals.tsx @@ -1,11 +1,12 @@ import { ModalsDispatchContext, ModalsStateContext } from "@/contexts/ModalsContext" import { useContext, useEffect } from "react" +import { useLocation } from "react-router-dom" import CreateCrewModal from "./CreateCrewModal" +import GoodPostureGuidePopupModal from "./GoodPostureGuideModal" import InviteCrewModal from "./InviteCrewModal" import JoinCrewModal from "./JoinCrewModal" -import WithdrawCrewModal from "./WithdrawCrewModal" import ToWithdrawModal from "./ToWithdrawModal" -import { useLocation } from "react-router-dom" +import WithdrawCrewModal from "./WithdrawCrewModal" export const modals = { createCrewModal: CreateCrewModal, @@ -13,6 +14,7 @@ export const modals = { joinCrewModal: JoinCrewModal, withdrawCrewModal: WithdrawCrewModal, ToWithdrawModal: ToWithdrawModal, + postureGuideModal: GoodPostureGuidePopupModal, } const Modals = (): React.ReactNode => { diff --git a/src/components/PoseDetector.tsx b/src/components/PoseDetector.tsx index d6b6a92..1e11c8c 100644 --- a/src/components/PoseDetector.tsx +++ b/src/components/PoseDetector.tsx @@ -1,22 +1,23 @@ +import { position } from "@/api" +import { duration } from "@/api/notification" +import { poseType } from "@/api/pose" +import { useCameraPermission } from "@/hooks/useCameraPermission" +import { useGuidePopup } from "@/hooks/useGuidePopup" +import { useSendPose } from "@/hooks/usePoseMutation" import usePushNotification from "@/hooks/usePushNotification" +import { useCreateSnaphot } from "@/hooks/useSnapshotMutation" +import { useNotificationStore } from "@/store/NotificationStore" +import { useSnapshotStore } from "@/store/SnapShotStore" import type { pose } from "@/utils/detector" -import { detectHandOnChin, detectSlope, detectTextNeck, detectTailboneSit } from "@/utils/detector" +import { detectHandOnChin, detectSlope, detectTailboneSit, detectTextNeck } from "@/utils/detector" import { drawPose } from "@/utils/drawer" import { worker } from "@/utils/worker" import { useCallback, useEffect, useRef, useState } from "react" +import { useLocation } from "react-router-dom" import Camera from "./Camera" -import GuidePopup from "./Posture/GuidePopup/GuidePopup" -import { useSnapshotStore } from "@/store/SnapshotStore" -import { useCreateSnaphot } from "@/hooks/useSnapshotMutation" -import { position } from "@/api" -import { useSendPose } from "@/hooks/usePoseMutation" -import { poseType } from "@/api/pose" -import PostureMessage from "./Posture/PostureMessage" import Controls from "./Posture/Controls" -import { useNotificationStore } from "@/store/NotificationStore" -import { duration } from "@/api/notification" -import { useCameraPermission } from "@/hooks/useCameraPermission" -import { useLocation } from "react-router-dom" +import GuidePopupModal from "./Posture/GuidePopup/GuidePopupModal" +import PostureMessage from "./Posture/PostureMessage" const PoseDetector: React.FC = () => { const [isScriptLoaded, setIsScriptLoaded] = useState(false) @@ -26,10 +27,12 @@ const PoseDetector: React.FC = () => { const [isTailboneSit, setIsTailboneSit] = useState(null) const [isHandOnChin, setIsHandOnChin] = useState(null) const [isModelLoaded, setIsModelLoaded] = useState(false) - const [isSnapSaved, setIsSnapSaved] = useState(false) - const [isPopupVisible, setIsPopupVisible] = useState(true) + // const [isSnapShotSaved, setIsSnapSaved] = useState(false) const { showNotification } = usePushNotification() + + const { isPopupOpen, handleClosePopup, openPopup } = useGuidePopup() + const modelRef = useRef(null) const snapRef = useRef(null) const resultRef = useRef(null) @@ -46,11 +49,10 @@ const PoseDetector: React.FC = () => { const canvasRef = useRef(null) - const snapshot = useSnapshotStore((state) => state.snapshot) + const { isSnapShotSaved, snapshot, setSnapShot } = useSnapshotStore() const createSnapMutation = useCreateSnaphot() const sendPoseMutation = useSendPose() - const setSnap = useSnapshotStore((state) => state.setSnapshot) const userNoti = useNotificationStore((state) => state.notification) const { requestNotificationPermission } = usePushNotification() @@ -115,11 +117,11 @@ const PoseDetector: React.FC = () => { condition: boolean | null, timerRef: React.MutableRefObject, poseType: poseType, - isSnapSaved: boolean, + isSnapShotSaved: boolean, cntRef: React.MutableRefObject, isShowNoti: boolean | undefined ): void => { - if (condition && isSnapSaved) { + if (condition && isSnapShotSaved) { if (!timerRef.current) { console.log(poseType, "start") timerRef.current = setInterval(() => { @@ -158,24 +160,24 @@ const PoseDetector: React.FC = () => { if (_isTailboneSit !== null) setIsTailboneSit(_isTailboneSit) // 공통 타이머 관리 함수 호출 - managePoseTimer(_isTextNeck, turtleNeckTimer, "TURTLE_NECK", isSnapSaved, turtleNeckCnt, _isShowNoti) + managePoseTimer(_isTextNeck, turtleNeckTimer, "TURTLE_NECK", isSnapShotSaved, turtleNeckCnt, _isShowNoti) managePoseTimer( _isShoulderTwist, shoulderTwistTimer, "SHOULDER_TWIST", - isSnapSaved, + isSnapShotSaved, shoulderTwistCnt, _isShowNoti ) - managePoseTimer(_isTailboneSit, tailboneSitTimer, "TAILBONE_SIT", isSnapSaved, tailboneSitCnt, _isShowNoti) - managePoseTimer(_isHandOnChin, chinUtpTimer, "CHIN_UTP", isSnapSaved, chinUtpCnt, _isShowNoti) + managePoseTimer(_isTailboneSit, tailboneSitTimer, "TAILBONE_SIT", isSnapShotSaved, tailboneSitCnt, _isShowNoti) + managePoseTimer(_isHandOnChin, chinUtpTimer, "CHIN_UTP", isSnapShotSaved, chinUtpCnt, _isShowNoti) const isRight = !_isTextNeck && !_isHandOnChin && !_isShoulderTwist && !_isTailboneSit if (canvasRef.current) drawPose(results, canvasRef.current, isRight) } else { if (canvasRef.current) drawPose(results, canvasRef.current) } }, - [setIsShoulderTwist, setIsTextNeck, setIsHandOnChin, setIsTailboneSit, isSnapSaved, managePoseTimer, userNoti] + [setIsShoulderTwist, setIsTextNeck, setIsHandOnChin, setIsTailboneSit, isSnapShotSaved, managePoseTimer, userNoti] ) const detectStart = useCallback( @@ -204,8 +206,8 @@ const PoseDetector: React.FC = () => { { onSuccess: () => { if (snapRef.current) { - setSnap(snapRef.current[0].keypoints) - setIsSnapSaved(true) + setSnapShot(snapRef.current[0].keypoints) + // setIsSnapSaved(true) } }, } @@ -213,12 +215,12 @@ const PoseDetector: React.FC = () => { } } } - }, [createSnapMutation, snapshot, setSnap]) + }, [createSnapMutation, snapshot, setSnapShot]) const getUserSnap = (): void => { if (snapshot) { snapRef.current = [{ keypoints: snapshot }] - setIsSnapSaved(true) + // setIsSnapSaved(true) } } @@ -235,15 +237,6 @@ const PoseDetector: React.FC = () => { notificationTimer.current = null } - const clearSnap = (): void => { - if (snapshot) { - snapRef.current = null - setIsSnapSaved(false) - setSnap(null) - clearTimers() // 타이머들을 초기화 - } - } - const clearCnt = (): void => { turtleNeckCnt.current = 0 shoulderTwistCnt.current = 0 @@ -293,11 +286,11 @@ const PoseDetector: React.FC = () => { }, []) useEffect(() => { - if (!isSnapSaved || !hasPermission) { + if (!isSnapShotSaved || !hasPermission) { clearTimers() // 스냅샷이 저장되지 않았을 때 타이머들을 초기화 clearCnt() // 횟수도 초기화 } - }, [isSnapSaved, hasPermission]) + }, [isSnapShotSaved, hasPermission]) useEffect(() => { if (isModelLoaded && hasPermission) { @@ -313,7 +306,7 @@ const PoseDetector: React.FC = () => { }, [snapshot]) useEffect(() => { - if (!isSnapSaved || !userNoti) return + if (!isSnapShotSaved || !userNoti) return clearCnt() clearInterval(notificationTimer.current) @@ -328,16 +321,11 @@ const PoseDetector: React.FC = () => { } }, 1000 * 60 * t) } - }, [userNoti, isSnapSaved]) + }, [userNoti, isSnapShotSaved]) // 팝업 열기 const handleShowPopup = (): void => { - setIsPopupVisible(true) - } - - // 팝업 닫기 - const handleClosePopup = (): void => { - setIsPopupVisible(false) + openPopup() } return ( @@ -351,24 +339,22 @@ const PoseDetector: React.FC = () => { {isModelLoaded && ( <> - - + {!isPopupOpen && ( + + )} + {!isSnapShotSaved && hasPermission && ( + + )} )} - {isPopupVisible && } + {isPopupOpen && } )} diff --git a/src/components/Posture/Controls.tsx b/src/components/Posture/Controls.tsx index 5c6ceb4..29fde2f 100644 --- a/src/components/Posture/Controls.tsx +++ b/src/components/Posture/Controls.tsx @@ -2,46 +2,29 @@ import PostureCheckIcon from "@assets/icons/good-posture-check-button-icon.svg?r import GuideIcon from "@assets/icons/posture-guide-button-icon.svg?react" const Controls: React.FC<{ - isSnapSaved: boolean - hasPermission: boolean getInitSnap: () => void - clearSnap: () => void handleShowPopup: () => void -}> = ({ isSnapSaved, getInitSnap, clearSnap, handleShowPopup, hasPermission }) => { - return !hasPermission ? null : ( +}> = ({ getInitSnap, handleShowPopup }) => { + return (
- {!isSnapSaved ? ( - <> - - - - ) : ( - - )} + +
) } diff --git a/src/components/Posture/GuidePopup/GuidePopup.tsx b/src/components/Posture/GuidePopup/GuidePopupModal.tsx similarity index 90% rename from src/components/Posture/GuidePopup/GuidePopup.tsx rename to src/components/Posture/GuidePopup/GuidePopupModal.tsx index 60526a2..875f069 100644 --- a/src/components/Posture/GuidePopup/GuidePopup.tsx +++ b/src/components/Posture/GuidePopup/GuidePopupModal.tsx @@ -2,7 +2,7 @@ import { ReactElement, useState } from "react" import ServiceIntroduction from "./ServiceIntroduction" import SnapshotGuide from "./SnapshotGuide" -const GuidePopup = ({ onClose }: { onClose: () => void }): ReactElement => { +const GuidePopupModal = ({ onClose }: { onClose: () => void }): ReactElement => { const [step, setStep] = useState(0) const onClickNext = () => { setStep(1) @@ -35,4 +35,4 @@ const GuidePopup = ({ onClose }: { onClose: () => void }): ReactElement => { ) } -export default GuidePopup +export default GuidePopupModal diff --git a/src/components/Posture/PostrueCrew.tsx b/src/components/Posture/PostrueCrew.tsx index b96b231..e0e364c 100644 --- a/src/components/Posture/PostrueCrew.tsx +++ b/src/components/Posture/PostrueCrew.tsx @@ -1,14 +1,19 @@ +import { duration, notification } from "@/api/notification" +import { useModals } from "@/hooks/useModals" +import { usePatchNoti } from "@/hooks/useNotiMutation" +import usePushNotification from "@/hooks/usePushNotification" +import { useAuthStore } from "@/store" +import { useNotificationStore } from "@/store/NotificationStore" import CloseCrewPanelIcon from "@assets/icons/crew-panel-close-button.svg?react" -import QuestionIcon from "@assets/icons/question-info-icon.svg?react" import PostureGuide from "@assets/icons/posture-guide-button-icon.svg?react" +import PostureRetakeIcon from "@assets/icons/posture-snapshot-retake-icon.svg?react" +import QuestionIcon from "@assets/icons/question-info-icon.svg?react" import RankingGuideToolTip from "@assets/images/ranking-guide.png" -import { ReactElement, useCallback, useEffect, useState } from "react" import SelectBox from "@components/SelectBox" -import { useAuthStore } from "@/store" -import { duration, notification } from "@/api/notification" -import { useNotificationStore } from "@/store/NotificationStore" -import { usePatchNoti } from "@/hooks/useNotiMutation" -import usePushNotification from "@/hooks/usePushNotification" +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 @@ -36,15 +41,30 @@ const NOTI_OPTIONS: NotiOption[] = [ ] const MAX_RECONNECT_ATTEMPTS = 5 -const INITIAL_RECONNECT_DELAY = 1000 // +const INITIAL_RECONNECT_DELAY = 1000 +const UPDATE_INTERVAL = 1000 // 1초마다 상태 업데이트 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 [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 throttledUpdateCrews = useCallback(() => { + if (!updateTimeoutRef.current) { + updateTimeoutRef.current = setTimeout(() => { + setCrews(latestCrewsRef.current) + updateTimeoutRef.current = null + }, UPDATE_INTERVAL) + } + }, []) const userNoti = useNotificationStore((state) => state.notification) const setUserNoti = useNotificationStore((state) => state.setNotification) @@ -65,7 +85,11 @@ export default function PostrueCrew(props: PostureCrewProps): ReactElement { newSocket.onmessage = (event) => { const data = JSON.parse(event.data) - setCrews(data.groupUsers || []) + console.log("Received message:", data) + if (data.groupUsers) { + latestCrewsRef.current = data.groupUsers + throttledUpdateCrews() + } } newSocket.onerror = (error) => { @@ -89,7 +113,7 @@ export default function PostrueCrew(props: PostureCrewProps): ReactElement { } setSocket(newSocket) - }, [accessToken, reconnectAttempts]) + }, [accessToken, reconnectAttempts, throttledUpdateCrews]) useEffect(() => { connectWebSocket() @@ -98,6 +122,9 @@ export default function PostrueCrew(props: PostureCrewProps): ReactElement { if (socket) { socket.close() } + if (updateTimeoutRef.current) { + clearTimeout(updateTimeoutRef.current) + } } }, [connectWebSocket]) @@ -130,6 +157,17 @@ export default function PostrueCrew(props: PostureCrewProps): ReactElement { ) } + const onClickPostureGuide = () => { + openModal(modals.postureGuideModal, {}) + } + + const onClickReTakeSnapShot = () => { + resetSnapShot() + createSnapMutation.mutate({ + points: [], + }) + } + return (
-
-
- - 바른자세 가이드 -
+
+ +
) diff --git a/src/components/Posture/PostureMessage.tsx b/src/components/Posture/PostureMessage.tsx index 04aa47c..6a8e73a 100644 --- a/src/components/Posture/PostureMessage.tsx +++ b/src/components/Posture/PostureMessage.tsx @@ -1,11 +1,11 @@ const PostureMessage: React.FC<{ - isSnapSaved: boolean + isSnapShotSaved: boolean isShoulderTwist: boolean | null isTextNeck: boolean | null isHandOnChin: boolean | null isTailboneSit: boolean | null hasPermission: boolean -}> = ({ isSnapSaved, isShoulderTwist, isTextNeck, isHandOnChin, isTailboneSit, hasPermission }) => { +}> = ({ isSnapShotSaved, isShoulderTwist, isTextNeck, isHandOnChin, isTailboneSit, hasPermission }) => { const getIsRight = ( _isShoulderTwist: boolean | null, _isTextNeck: boolean | null, @@ -37,7 +37,7 @@ const PostureMessage: React.FC<{ > {!hasPermission ? "브라우저의 카메라 권한을 허용해주세요." - : !isSnapSaved + : !isSnapShotSaved ? "바른 자세를 취한 후, 하단의 버튼을 눌러주세요." : getIsRight(isShoulderTwist, isTextNeck, isTailboneSit, isHandOnChin) ? "올바른 자세입니다." diff --git a/src/components/SideNav.tsx b/src/components/SideNav.tsx index 451bb1f..cbf9d98 100644 --- a/src/components/SideNav.tsx +++ b/src/components/SideNav.tsx @@ -4,7 +4,7 @@ import AnalysisIcon from "@assets/icons/side-nav-analysis-icon.svg?react" import CrewIcon from "@assets/icons/side-nav-crew-icon.svg?react" import MonitoringIcon from "@assets/icons/side-nav-monitor-icon.svg?react" import { Link, useLocation, useNavigate } from "react-router-dom" -import { useSnapshotStore } from "@/store/SnapshotStore" +import { useSnapshotStore } from "@/store/SnapShotStore" import { useMemo } from "react" import { clearAccessToken } from "@/api/axiosInstance" import { useNotificationStore } from "@/store/NotificationStore" diff --git a/src/hooks/useGuidePopup.tsx b/src/hooks/useGuidePopup.tsx new file mode 100644 index 0000000..0785ee7 --- /dev/null +++ b/src/hooks/useGuidePopup.tsx @@ -0,0 +1,24 @@ +import { useGuidePopupStore } from "@/store/GuidePopupStore" +import { useEffect } from "react" + +export const useGuidePopup = () => { + const { isPopupOpen, lastClosedDate, openPopup, closePopup, setLastClosedDate } = useGuidePopupStore() + + useEffect(() => { + const checkPopupDate = () => { + const currentDate = new Date().toDateString() + if (!lastClosedDate || new Date(lastClosedDate) < new Date(currentDate)) { + openPopup() + } + } + + checkPopupDate() + }, [lastClosedDate, openPopup]) + + const handleClosePopup = () => { + closePopup() + setLastClosedDate(new Date().toDateString()) + } + + return { isPopupOpen, handleClosePopup, openPopup } +} diff --git a/src/hooks/useSnapShot.ts b/src/hooks/useSnapShot.ts new file mode 100644 index 0000000..8b403fe --- /dev/null +++ b/src/hooks/useSnapShot.ts @@ -0,0 +1,31 @@ +import { createSnapshot, createSnapshotRes, getRecentSnapshot, getSnapshots, snapshot } from "@/api" +import { useMutation, UseMutationResult, useQuery, UseQueryResult } from "@tanstack/react-query" + +export const useCreateSnaphot = (): UseMutationResult => { + return useMutation({ + mutationFn: (_snapshot: snapshot) => { + return createSnapshot(_snapshot) + }, + onSuccess: (data) => { + console.log(data) + }, + }) +} + +export const useGetSnapshot = (): UseMutationResult => { + return useMutation({ + mutationFn: (id: string) => { + return getSnapshots(id) + }, + onSuccess: (data) => { + console.log(data) + }, + }) +} + +export const useGetRecentSnapshot = (): UseQueryResult => { + return useQuery({ + queryKey: ["recent-snapshot"], + queryFn: getRecentSnapshot, + }) +} diff --git a/src/pages/AuthPage.tsx b/src/pages/AuthPage.tsx index 8e2925a..2b8e0b0 100644 --- a/src/pages/AuthPage.tsx +++ b/src/pages/AuthPage.tsx @@ -4,7 +4,7 @@ import Login from "@/components/Login" import { useOauth, useSignUp, useSignIn, useGetIsSignUp } from "@/hooks/useAuthMutation" import RoutePath from "@/constants/routes.json" import { useAuthStore } from "@/store/AuthStore" -import { useSnapshotStore } from "@/store/SnapshotStore" +import { useSnapshotStore } from "@/store/SnapShotStore" import { useGetRecentSnapshot } from "@/hooks/useSnapshotMutation" import { useGetNoti } from "@/hooks/useNotiMutation" import { useNotificationStore } from "@/store/NotificationStore" @@ -22,7 +22,7 @@ const AuthPage: React.FC = () => { const [isError, setIsError] = useState(false) const setUser = useAuthStore((state) => state.setUser) - const setSnap = useSnapshotStore((state) => state.setSnapshot) + const setSnap = useSnapshotStore((state) => state.setSnapShot) const setNoti = useNotificationStore((state) => state.setNotification) useEffect(() => { diff --git a/src/pages/MonitoringPage.tsx b/src/pages/MonitoringPage.tsx index 659e557..4265992 100644 --- a/src/pages/MonitoringPage.tsx +++ b/src/pages/MonitoringPage.tsx @@ -3,16 +3,18 @@ import PostrueCrew from "@/components/Posture/PostrueCrew" import GroupSideIcon from "@assets/icons/group-side-nav-button.svg?react" import React, { useEffect, useState } from "react" import { useGetRecentSnapshot } from "@/hooks/useSnapshotMutation" -import { useSnapshotStore } from "@/store/SnapshotStore" +import { useSnapshotStore } from "@/store/SnapShotStore" import usePushNotification from "@/hooks/usePushNotification" +import { useGuidePopup } from "@/hooks/useGuidePopup" const MonitoringPage: React.FC = () => { const { hasPermission } = usePushNotification() const getRecentSnapMutation = useGetRecentSnapshot() - const setSnap = useSnapshotStore((state) => state.setSnapshot) - const snapshot = useSnapshotStore((state) => state.snapshot) - const [isSidebarOpen, setIsSidebarOpen] = useState(false) + const { isPopupOpen } = useGuidePopup() + const { snapshot, setSnapShot } = useSnapshotStore() + + const [isSidebarOpen, setIsSidebarOpen] = useState(true) const toggleSidebar = (): void => { setIsSidebarOpen((prev) => !prev) @@ -25,7 +27,9 @@ const MonitoringPage: React.FC = () => { // 스냅샷이 있으면 store에 저장 if (userSnap.id !== -1) { - setSnap(userSnap.points.map((p) => ({ name: p.position.toLocaleLowerCase(), x: p.x, y: p.y, confidence: 1 }))) + setSnapShot( + userSnap.points.map((p) => ({ name: p.position.toLocaleLowerCase(), x: p.x, y: p.y, confidence: 1 })) + ) } } } @@ -37,7 +41,7 @@ const MonitoringPage: React.FC = () => { return (
{/* Main content area */} -
+
@@ -53,14 +57,14 @@ const MonitoringPage: React.FC = () => { {/* 사이드바 */}
- {isSidebarOpen && } + {isSidebarOpen && !isPopupOpen && }
{/* 토글 버튼 */} - {!isSidebarOpen && ( + {!isSidebarOpen && !isPopupOpen && ( diff --git a/src/store/GuidePopupStore.ts b/src/store/GuidePopupStore.ts new file mode 100644 index 0000000..d20c7bf --- /dev/null +++ b/src/store/GuidePopupStore.ts @@ -0,0 +1,27 @@ +import { create } from "zustand" +import { createJSONStorage, persist } from "zustand/middleware" + +interface PopupStore { + isPopupOpen: boolean + lastClosedDate: string | null + openPopup: () => void + closePopup: () => void + setLastClosedDate: (date: string) => void +} + +// Zustand store 생성 +export const useGuidePopupStore = create( + persist( + (set: any) => ({ + isPopupOpen: false, + lastClosedDate: null, + openPopup: () => set({ isPopupOpen: true }), + closePopup: () => set({ isPopupOpen: false }), + setLastClosedDate: (date) => set({ lastClosedDate: date }), + }), + { + name: "popup-storage", + storage: createJSONStorage(() => localStorage), + } + ) +) diff --git a/src/store/SnapshotStore.ts b/src/store/SnapshotStore.ts index 07fcbc6..acee90d 100644 --- a/src/store/SnapshotStore.ts +++ b/src/store/SnapshotStore.ts @@ -3,15 +3,19 @@ import { create } from "zustand" import { persist } from "zustand/middleware" interface SnapshotState { + isSnapShotSaved: boolean snapshot: keypoint[] | null - setSnapshot: (snapshot: keypoint[] | null) => void + setSnapShot: (snapshot: keypoint[] | null) => void + resetSnapShot: () => void } export const useSnapshotStore = create( persist( (set) => ({ + isSnapShotSaved: false, snapshot: null, - setSnapshot: (snapshot: keypoint[] | null) => set({ snapshot }), + setSnapShot: (snapshot: keypoint[] | null) => set({ snapshot, isSnapShotSaved: true }), + resetSnapShot: () => set({ snapshot: null, isSnapShotSaved: false }), }), { name: "snapshotStorage" } )