diff --git a/package.json b/package.json index fba7525..8a1d560 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@tanstack/react-query": "^5.51.23", + "@types/react-lottie": "^1.2.10", "axios": "1.7.3", "core-js": "^3.28.0", "dayjs": "^1.11.13", @@ -23,6 +24,7 @@ "p5": "^1.9.4", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-lottie": "^1.2.4", "react-router-dom": "^6.8.1", "react-tailwindcss-datepicker": "^1.7.2", "socket.io-client": "^4.7.5", diff --git a/src/assets/animation/check-lottie.json b/src/assets/animation/check-lottie.json new file mode 100644 index 0000000..f8229c4 --- /dev/null +++ b/src/assets/animation/check-lottie.json @@ -0,0 +1 @@ +{"v":"5.7.12","fr":24,"ip":0,"op":63,"w":520,"h":520,"nm":"Checklist 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Checklist","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":50,"s":[100]},{"t":62,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[259.587,260.119,0],"ix":2,"l":2},"a":{"a":0,"k":[297.587,298.119,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[54.754,-36.121],[-17.487,36.12],[-54.754,-1.147]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":23,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[294.971,298.679],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":24,"s":[100]},{"t":36,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":720,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Cricle","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":50,"s":[100]},{"t":62,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[260,260,0],"ix":2,"l":2},"a":{"a":0,"k":[-7.627,-7.691,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":18,"s":[124.222,124.222,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[134.222,134.222,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":22,"s":[114.222,114.222,100]},{"t":24,"s":[124.222,124.222,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[178.46,178.46],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.239],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":24,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.074509803922,0.596078431373,0.349019607843,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.074509803922,0.596078431373,0.349019607843,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":22,"s":[0]},{"t":24,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-7.627,-7.691],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":720,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Line","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":28,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":39,"s":[100]},{"t":50,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[260,260,0],"ix":2,"l":2},"a":{"a":0,"k":[298,298,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":28,"s":[46,46,100]},{"t":50,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-120.208,0],[0,-120.208],[120.207,0],[0,120.208]],"o":[[120.207,0],[0,120.208],[-120.208,0],[0,-120.208]],"v":[[0,-217.655],[217.655,0],[0,217.655],[-217.655,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.65023354923,0.829386991613,0.709956449621,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[297.587,298.119],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":28,"op":720,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shadow","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":22,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":33,"s":[100]},{"t":44,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[260,260,0],"ix":2,"l":2},"a":{"a":0,"k":[298,298,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":22,"s":[60,60,100]},{"t":44,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-94.797,0],[0,-94.797],[94.797,0],[0,94.798]],"o":[[94.797,0],[0,94.798],[-94.797,0],[0,-94.797]],"v":[[0,-171.646],[171.646,0],[0,171.646],[-171.646,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.888151161343,0.944075939702,0.895734480316,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[297.587,298.119],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":22,"op":720,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/src/assets/icons/crew-side-nav-down-arrow.svg b/src/assets/icons/crew-side-nav-down-arrow.svg new file mode 100644 index 0000000..07c2ed7 --- /dev/null +++ b/src/assets/icons/crew-side-nav-down-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Crew/CrewRanking.tsx b/src/components/Crew/CrewRanking.tsx index 4a0ddca..0240565 100644 --- a/src/components/Crew/CrewRanking.tsx +++ b/src/components/Crew/CrewRanking.tsx @@ -25,34 +25,29 @@ const RankPillar = ({ rank, name, score, height }: any) => { const style = rankStyleMap[rank - 1] return ( -
-
-
- {rank === 1 && ( -
- -
- )} -
- {rank}등 -
+
+
+ {rank === 1 && } +
+ {rank}등
-
{name}
-
자세 경고 {score}회
+
{name}
+
자세 경고 {score}회
) } diff --git a/src/components/PoseDetector.tsx b/src/components/PoseDetector.tsx index f43c7be..a304f70 100644 --- a/src/components/PoseDetector.tsx +++ b/src/components/PoseDetector.tsx @@ -2,6 +2,7 @@ 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 { useModals } from "@/hooks/useModals" import useNotification from "@/hooks/useNotification" import { useSendPose } from "@/hooks/usePoseMutation" @@ -12,7 +13,9 @@ import type { pose } from "@/utils/detector" import { detectHandOnChin, detectSlope, detectTailboneSit, detectTextNeck } from "@/utils/detector" import { drawPose } from "@/utils/drawer" import { worker } from "@/utils/worker" +import CheckLottie from "@assets/animation/check-lottie.json" import { useCallback, useEffect, useRef, useState } from "react" +import Lottie from "react-lottie" import { useLocation } from "react-router-dom" import Camera from "./Camera" import { modals } from "./Modal/Modals" @@ -29,6 +32,7 @@ const PoseDetector: React.FC = () => { const [isHandOnChin, setIsHandOnChin] = useState(null) const [isModelLoaded, setIsModelLoaded] = useState(false) const [isClosedInitialGuidePopup, setIsClosedInitialGuidePopup] = useState(false) + const [isSuccessSnapShotSaved, setIsSuccessSnapShotSaved] = useState(false) // const [isSnapShotSaved, setIsSnapSaved] = useState(false) const { showNotification, hasPermission: hasNotiPermisson } = usePushNotification() @@ -54,6 +58,7 @@ const PoseDetector: React.FC = () => { const { isSnapShotSaved, snapshot, setSnapShot, isInitialSnapShotExist } = useSnapShotStore() const createSnapMutation = useCreateSnaphot() const sendPoseMutation = useSendPose() + const { isPopupOpen, handleClosePopup } = useGuidePopup() // const userNoti = useNotificationStore((state) => state.notification) const { notification } = useNotification() @@ -144,6 +149,10 @@ const PoseDetector: React.FC = () => { const detect = useCallback( (results: pose[]): void => { + if (!isSnapShotSaved || !isInitialSnapShotExist || isModalOpen) { + if (canvasRef.current) drawPose(results, canvasRef.current) + return + } resultRef.current = results if (snapRef.current) { const _isShoulderTwist = detectSlope(snapRef.current, results, false) @@ -175,6 +184,8 @@ const PoseDetector: React.FC = () => { isSnapShotSaved, managePoseTimer, notification, + isInitialSnapShotExist, + isSnapShotSaved, ] ) @@ -203,9 +214,12 @@ const PoseDetector: React.FC = () => { { points: req }, { onSuccess: () => { + setIsSuccessSnapShotSaved(true) + setTimeout(() => { + setIsSuccessSnapShotSaved(false) + }, 3000) if (snapRef.current) { setSnapShot(snapRef.current[0].keypoints) - // setIsSnapSaved(true) } }, } @@ -284,6 +298,14 @@ const PoseDetector: React.FC = () => { getScript() }, []) + useEffect(() => { + if (isPopupOpen) { + openModal(modals.postureGuideModal, { + onClose: () => [handleClosePopup()], + }) + } + }, [isPopupOpen]) + useEffect(() => { if (!isSnapShotSaved || !hasPermission) { clearTimers() // 스냅샷이 저장되지 않았을 때 타이머들을 초기화 @@ -305,7 +327,7 @@ const PoseDetector: React.FC = () => { }, [snapshot]) useEffect(() => { - if (!isSnapShotSaved || !notification) return + if (!isSnapShotSaved || !notification || !isInitialSnapShotExist || isModalOpen) return clearCnt() clearInterval(notificationTimer.current) @@ -318,7 +340,7 @@ const PoseDetector: React.FC = () => { clearCnt() }, 1000 * 60 * t) } - }, [notification, isSnapShotSaved]) + }, [notification, isSnapShotSaved, isInitialSnapShotExist]) // 즉시 알림을 사용 하는 경우, 푸시를 보낼지 여부를 저장 useEffect(() => { @@ -364,8 +386,31 @@ const PoseDetector: React.FC = () => { {!isSnapShotSaved && hasPermission && ( )} +
+ + 스냅샷이 성공적으로 저장되었습니다. +
)} + {!isClosedInitialGuidePopup && !isInitialSnapShotExist && ( )} diff --git a/src/components/Posture/PostrueCrew.tsx b/src/components/Posture/PostrueCrew.tsx index 8c75853..29253e5 100644 --- a/src/components/Posture/PostrueCrew.tsx +++ b/src/components/Posture/PostrueCrew.tsx @@ -189,7 +189,7 @@ export default function PostrueCrew(props: PostureCrewProps): ReactElement {
- 자세 알림 + 자세 알림