Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into feat/#46
Browse files Browse the repository at this point in the history
  • Loading branch information
lkhoony committed Sep 27, 2024
2 parents f5030f2 + 1cf0d33 commit ea28c39
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 76 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/assets/animation/check-lottie.json
Original file line number Diff line number Diff line change
@@ -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":[]}
3 changes: 3 additions & 0 deletions src/assets/icons/crew-side-nav-down-arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 21 additions & 26 deletions src/components/Crew/CrewRanking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,34 +25,29 @@ const RankPillar = ({ rank, name, score, height }: any) => {
const style = rankStyleMap[rank - 1]

return (
<div className="flex flex-col items-center text-zinc-800">
<div
className={`flex w-[180px] flex-col items-center rounded-[12px] py-6`}
style={{
backgroundColor: style.bgColor,
gap: style.gap,
height,
}}
>
<div className="flex flex-col items-center gap-2">
{rank === 1 && (
<div className="">
<Crew1stCrownIcon className="h-6 w-6" />
</div>
)}
<div
className={`font-medium`}
style={{
fontSize: style.fontSize,
fontWeight: style.fontWeight,
}}
>
{rank}
</div>
<div
className={`flex w-[180px] flex-col items-center rounded-[12px] py-6 text-zinc-800`}
style={{
backgroundColor: style.bgColor,
gap: style.gap,
height,
}}
>
<div className="flex flex-col items-center gap-2">
{rank === 1 && <Crew1stCrownIcon className="h-6 w-6" />}
<div
className={`font-medium`}
style={{
fontSize: style.fontSize,
fontWeight: style.fontWeight,
lineHeight: rank === 1 ? "40px" : "30px",
}}
>
{rank}
</div>
<div className="text-[20px] font-semibold">{name}</div>
<div className="text-[15px] font-normal ">자세 경고 {score}</div>
</div>
<div className="text-[20px] font-semibold leading-[28px]">{name}</div>
<div className="text-[15px] font-normal leading-6">자세 경고 {score}</div>
</div>
)
}
Expand Down
51 changes: 48 additions & 3 deletions src/components/PoseDetector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -29,6 +32,7 @@ const PoseDetector: React.FC = () => {
const [isHandOnChin, setIsHandOnChin] = useState<boolean | null>(null)
const [isModelLoaded, setIsModelLoaded] = useState<boolean>(false)
const [isClosedInitialGuidePopup, setIsClosedInitialGuidePopup] = useState(false)
const [isSuccessSnapShotSaved, setIsSuccessSnapShotSaved] = useState(false)
// const [isSnapShotSaved, setIsSnapSaved] = useState<boolean>(false)

const { showNotification, hasPermission: hasNotiPermisson } = usePushNotification()
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -175,6 +184,8 @@ const PoseDetector: React.FC = () => {
isSnapShotSaved,
managePoseTimer,
notification,
isInitialSnapShotExist,
isSnapShotSaved,
]
)

Expand Down Expand Up @@ -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)
}
},
}
Expand Down Expand Up @@ -284,6 +298,14 @@ const PoseDetector: React.FC = () => {
getScript()
}, [])

useEffect(() => {
if (isPopupOpen) {
openModal(modals.postureGuideModal, {
onClose: () => [handleClosePopup()],
})
}
}, [isPopupOpen])

useEffect(() => {
if (!isSnapShotSaved || !hasPermission) {
clearTimers() // 스냅샷이 저장되지 않았을 때 타이머들을 초기화
Expand All @@ -305,7 +327,7 @@ const PoseDetector: React.FC = () => {
}, [snapshot])

useEffect(() => {
if (!isSnapShotSaved || !notification) return
if (!isSnapShotSaved || !notification || !isInitialSnapShotExist || isModalOpen) return

clearCnt()
clearInterval(notificationTimer.current)
Expand All @@ -318,7 +340,7 @@ const PoseDetector: React.FC = () => {
clearCnt()
}, 1000 * 60 * t)
}
}, [notification, isSnapShotSaved])
}, [notification, isSnapShotSaved, isInitialSnapShotExist])

// 즉시 알림을 사용 하는 경우, 푸시를 보낼지 여부를 저장
useEffect(() => {
Expand Down Expand Up @@ -364,8 +386,31 @@ const PoseDetector: React.FC = () => {
{!isSnapShotSaved && hasPermission && (
<Controls getInitSnap={getInitSnap} handleShowPopup={handleShowPopup} />
)}
<div
className={`
absolute bottom-2 flex h-[50px] items-center justify-center
rounded-[20px] bg-[#1A1B1D] bg-opacity-60 pl-4 pr-6
transition-all duration-500 ease-in-out
${isSuccessSnapShotSaved ? "translate-y-0 opacity-100" : "translate-y-full opacity-0"}
`}
>
<Lottie
options={{
// loop: false,
autoplay: true,
animationData: CheckLottie,
rendererSettings: {
preserveAspectRatio: "xMidYMid slice",
},
}}
height={50}
width={50}
/>
<span className="text-[14px] font-semibold text-white ">스냅샷이 성공적으로 저장되었습니다.</span>
</div>
</>
)}

{!isClosedInitialGuidePopup && !isInitialSnapShotExist && (
<GuidePopupModal onClose={handleCloseInitialGuidePopup} />
)}
Expand Down
4 changes: 2 additions & 2 deletions src/components/Posture/PostrueCrew.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export default function PostrueCrew(props: PostureCrewProps): ReactElement {
<div className="flex-grow">
<div className="flex items-center justify-between p-2 pb-3">
<div className="flex items-center">
<span className="font-medium">자세 알림</span>
<span className="text-[15px] font-medium">자세 알림</span>
</div>

<label className="relative inline-flex cursor-pointer items-center">
Expand Down Expand Up @@ -217,7 +217,7 @@ export default function PostrueCrew(props: PostureCrewProps): ReactElement {

<div className="group relative">
<div className="flex items-center gap-2 p-2">
<span>자세 랭킹</span>
<span className="text-[15px] font-medium">자세 랭킹</span>
<QuestionIcon />
</div>

Expand Down
13 changes: 2 additions & 11 deletions src/components/SelectBox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from "react"
import DownArrowIcon from "@assets/icons/crew-side-nav-down-arrow.svg?react"

interface SelectBoxOption {
label: string
Expand Down Expand Up @@ -35,17 +36,7 @@ export default function SelectBox(props: SelectBoxProps): React.ReactElement {
onClick={toggleDropdown}
>
<span className={isDisabled ? "text-[#D4D4D8]" : ""}>{value}</span>
<svg
className={`h-4 w-4 transform transition-transform ${isOpen ? "rotate-180" : ""} ${
isDisabled ? "text-[#D4D4D8]" : ""
}`}
fill={isDisabled ? "#D4D4D8" : ""}
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
<DownArrowIcon />
</div>
{isOpen && (
<div className="absolute z-10 mt-1 w-full flex-col rounded-md bg-white py-1 shadow-[0px_2px_16px_0px_rgba(0,0,0,0.13)]">
Expand Down
46 changes: 29 additions & 17 deletions src/hooks/useCameraPermission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,51 @@ export const useCameraPermission = () => {
// 카메라 권한 확인 함수
const checkCameraPermission = async () => {
try {
const permissionStatus = await navigator.permissions.query({
name: "camera" as PermissionName,
})
// 먼저 navigator.permissions.query()를 시도합니다.
if ("permissions" in navigator && "query" in navigator.permissions) {
const permissionStatus = await navigator.permissions.query({
name: "camera" as PermissionName,
})

// 권한 상태 설정
if (permissionStatus.state === "granted") {
setHasPermission(true)
setIsPermissionDenied(false)
} else if (permissionStatus.state === "denied") {
setHasPermission(false)
setIsPermissionDenied(true)
}

// 권한 변경 감지
permissionStatus.onchange = () => {
// 권한 상태 설정
if (permissionStatus.state === "granted") {
setHasPermission(true)
setIsPermissionDenied(false)
return
} else if (permissionStatus.state === "denied") {
setHasPermission(false)
setIsPermissionDenied(true)
return
}

// 권한 변경 감지 (크롬에서 작동)
permissionStatus.onchange = () => {
if (permissionStatus.state === "granted") {
setHasPermission(true)
setIsPermissionDenied(false)
} else if (permissionStatus.state === "denied") {
setHasPermission(false)
setIsPermissionDenied(true)
}
}
}

// navigator.permissions.query()가 지원되지 않거나 "prompt" 상태인 경우
// 실제 카메라 접근을 시도합니다.
const stream = await navigator.mediaDevices.getUserMedia({ video: true })
stream.getTracks().forEach((track) => track.stop()) // 스트림 정리
setHasPermission(true)
setIsPermissionDenied(false)
} catch (error) {
console.error("Permission API error:", error)
console.error("Camera access error:", error)
setHasPermission(false)
setIsPermissionDenied(true)
}
}

useEffect(() => {
checkCameraPermission() // 컴포넌트 마운트 시 권한 확인
checkCameraPermission()
}, [])

return { hasPermission, isPermissionDenied }
return { hasPermission, isPermissionDenied, checkCameraPermission }
}
Loading

0 comments on commit ea28c39

Please sign in to comment.