Skip to content

Commit

Permalink
Merge pull request #39 from DDD-Community/feat/#28
Browse files Browse the repository at this point in the history
[Feat/#28] 꼬리뼈 앉기 탐지 로직 추가, 사용자 정보 유지, 로그아웃 기능 추가
  • Loading branch information
lkhoony authored Aug 28, 2024
2 parents 109fd0c + f2a70a5 commit 39a782b
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 83 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
{ "ts": "never", "tsx": "never" }
],
"no-shadow": "off",
"@typescript-eslint/no-shadow": "error",
"@typescript-eslint/no-shadow": "off",
"@typescript-eslint/explicit-function-return-type": [
"error",
{ "allowExpressions": true }
Expand Down
3 changes: 3 additions & 0 deletions src/assets/icons/home-kakao-signup-button-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
149 changes: 100 additions & 49 deletions src/components/PoseDetector.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import usePushNotification from "@/hooks/usePushNotification"
import type { pose } from "@/utils/detector"
import { detectHandOnChin, detectSlope, detectTextNeck } from "@/utils/detector"
import { detectHandOnChin, detectSlope, detectTextNeck, detectTailboneSit } from "@/utils/detector"
import { drawPose } from "@/utils/drawer"
import { worker } from "@/utils/worker"
import { useCallback, useEffect, useRef, useState } from "react"
Expand All @@ -19,6 +19,7 @@ const PoseDetector: React.FC = () => {
const [isScriptError, setIsScriptError] = useState<boolean>(false)
const [isTextNeck, setIsTextNeck] = useState<boolean | null>(null)
const [isShoulderTwist, setIsShoulderTwist] = useState<boolean | null>(null)
const [isTailboneSit, setIsTailboneSit] = useState<boolean | null>(null)
const [isHandOnChin, setIsHandOnChin] = useState<boolean | null>(null)
const [isModelLoaded, setIsModelLoaded] = useState<boolean>(false)
const [isSnapSaved, setIsSnapSaved] = useState<boolean>(false)
Expand All @@ -30,7 +31,7 @@ const PoseDetector: React.FC = () => {
const turtleNeckTimer = useRef<any>(null)
const shoulderTwistTimer = useRef<any>(null)
const chinUtpTimer = useRef<any>(null)
// const tailboneSit = useRef<any>(null)
const tailboneSitTimer = useRef<any>(null)

const canvasRef = useRef<HTMLCanvasElement>(null)

Expand All @@ -42,7 +43,7 @@ const PoseDetector: React.FC = () => {

const { requestNotificationPermission, showNotification } = usePushNotification()

// webgl설정
// webgl 설정
const initializeBackend = async (): Promise<void> => {
await window.ml5.setBackend("webgl")
}
Expand Down Expand Up @@ -81,23 +82,55 @@ const PoseDetector: React.FC = () => {
document.body.appendChild(script)
}

const managePoseTimer = (
condition: boolean | null,
timerRef: React.MutableRefObject<any>,
poseType: poseType,
isSnapSaved: boolean
): void => {
if (condition && isSnapSaved) {
if (!timerRef.current) {
timerRef.current = setInterval(() => {
if (resultRef.current) {
const { keypoints, score } = resultRef.current[0]
const req = { snapshot: { keypoints, score }, type: poseType }
sendPoseMutation.mutate(req)
}
}, 5000)
}
} else {
clearInterval(timerRef.current)
timerRef.current = null
}
}

const detect = useCallback(
(results: pose[]): void => {
resultRef.current = results

if (canvasRef.current) {
drawPose(results, canvasRef.current)
}

if (snapRef.current) {
const _isShoulderTwist = detectSlope(snapRef.current, results, false)
const _isTextNeck = detectTextNeck(snapRef.current, results, true)
const _isHandOnChin = detectHandOnChin(results)
const _isTailboneSit = detectTailboneSit(snapRef.current, results)

if (_isShoulderTwist !== null) setIsShoulderTwist(_isShoulderTwist)
if (_isTextNeck !== null) setIsTextNeck(_isTextNeck)
if (_isHandOnChin !== null) setIsHandOnChin(_isHandOnChin)
if (_isTailboneSit !== null) setIsTailboneSit(_isTailboneSit)

// 공통 타이머 관리 함수 호출
managePoseTimer(_isTextNeck, turtleNeckTimer, "TURTLE_NECK", isSnapSaved)
managePoseTimer(_isShoulderTwist, shoulderTwistTimer, "SHOULDER_TWIST", isSnapSaved)
managePoseTimer(_isTailboneSit, tailboneSitTimer, "TAILBONE_SIT", isSnapSaved)
managePoseTimer(_isHandOnChin, chinUtpTimer, "CHIN_UTP", isSnapSaved)
}
},
[setIsShoulderTwist, setIsTextNeck, setIsHandOnChin, showNotification]
[setIsShoulderTwist, setIsTextNeck, setIsHandOnChin, setIsTailboneSit, isSnapSaved, showNotification]
)

const detectStart = useCallback(
Expand All @@ -124,14 +157,16 @@ const PoseDetector: React.FC = () => {
createSnapMutation.mutate(
{ points: req },
{
onSuccess: (data: any) => {
setSnap(data)
onSuccess: () => {
if (snapRef.current) {
setSnap(snapRef.current[0].keypoints)
setIsSnapSaved(true)
}
},
}
)
}
}
setIsSnapSaved(true)
}
}

Expand All @@ -142,44 +177,48 @@ const PoseDetector: React.FC = () => {
}
}

const clearTimers = () => {
clearInterval(turtleNeckTimer.current)
clearInterval(shoulderTwistTimer.current)
clearInterval(tailboneSitTimer.current)
clearInterval(chinUtpTimer.current)

turtleNeckTimer.current = null
shoulderTwistTimer.current = null
tailboneSitTimer.current = null
chinUtpTimer.current = null
}

const clearSnap = (): void => {
if (snapshot) {
snapRef.current = null
setIsSnapSaved(false)
setSnap(null)
clearTimers() // 타이머들을 초기화
}
}

const getIsRight = (
_isShoulderTwist: boolean | null,
_isTextNeck: boolean | null,
_isTailboneSit: boolean | null,
_isHandOnChin: boolean | null
): boolean => {
if (!_isShoulderTwist && !_isTextNeck && !_isHandOnChin) return true
if (!_isShoulderTwist && !_isTextNeck && !_isTailboneSit && !_isHandOnChin) return true
return false
}

// 공통 타이머 관리 함수
const usePoseTimer = (isActive: boolean | null, poseType: poseType, timerRef: React.MutableRefObject<any>) => {
useEffect(() => {
if (isActive) {
if (!timerRef.current) {
timerRef.current = setInterval(() => {
if (resultRef.current) {
const { keypoints, score } = resultRef.current[0]
const req = { snapshot: { keypoints, score }, type: poseType }
sendPoseMutation.mutate(req)
}
}, 5000)
}
} else {
clearInterval(timerRef.current)
timerRef.current = null
}
}, [isActive, poseType])
}

usePoseTimer(isTextNeck, "TURTLE_NECK", turtleNeckTimer)
usePoseTimer(isShoulderTwist, "SHOULDER_TWIST", shoulderTwistTimer)
usePoseTimer(isHandOnChin, "CHIN_UTP", chinUtpTimer)

useEffect(() => {
requestNotificationPermission()
getScript()
}, [])

useEffect(() => {
if (!isSnapSaved) {
clearTimers() // 스냅샷이 저장되지 않았을 때 타이머들을 초기화
}
}, [isSnapSaved])

useEffect(() => {
if (isModelLoaded) {
const video = document.querySelector("video")
Expand All @@ -190,7 +229,7 @@ const PoseDetector: React.FC = () => {
}, [isModelLoaded, detectStart])

useEffect(() => {
getUserSnap()
if (snapshot) getUserSnap()
}, [snapshot])

// 팝업 열기
Expand All @@ -217,32 +256,44 @@ const PoseDetector: React.FC = () => {
<div className="absolute top-0 flex w-full items-center justify-center rounded-t-lg bg-[#1A1B1D] bg-opacity-75 p-[20px] text-white">
{!isSnapSaved
? "바른 자세를 취한 후, 하단의 버튼을 눌러주세요."
: getIsRight(isShoulderTwist, isTextNeck, isHandOnChin)
: getIsRight(isShoulderTwist, isTextNeck, isHandOnChin, isTailboneSit)
? "올바른 자세입니다."
: "올바르지 않은 자세입니다."}
</div>
{!isSnapSaved && (
<div className="absolute bottom-0 flex w-full items-center justify-center gap-[16px] p-[50px] text-white">
<button
className="flex w-[260px] items-center justify-center rounded rounded-full bg-white bg-opacity-80 p-[20px] text-black"
onClick={handleShowPopup}
>
<div className="flex flex-row items-center gap-2">
<GuideIcon />
<span>가이드 다시 볼게요!</span>
</div>
</button>
<div className="absolute bottom-0 flex w-full items-center justify-center gap-[16px] p-[50px] text-white">
{!isSnapSaved ? (
<>
<button
className="flex w-[260px] items-center justify-center rounded rounded-full bg-white bg-opacity-80 p-[20px] text-black"
onClick={handleShowPopup}
>
<div className="flex flex-row items-center gap-2">
<GuideIcon />
<span>가이드 다시 볼게요!</span>
</div>
</button>
<button
className="flex w-[260px] items-center justify-center rounded rounded-full bg-[#1A75FF] bg-opacity-80 p-[20px] text-white"
onClick={getInitSnap}
>
<div className="flex flex-row items-center gap-2">
<PostureCheckIcon />
바른자세를 취했어요!
</div>
</button>
</>
) : (
<button
className="flex w-[260px] items-center justify-center rounded rounded-full bg-[#1A75FF] bg-opacity-80 p-[20px] text-white"
onClick={getInitSnap}
onClick={clearSnap}
>
<div className="flex flex-row items-center gap-2">
<PostureCheckIcon />
바른자세를 취했어요!
스냅샷 다시찍기
</div>
</button>
</div>
)}
)}
</div>
</>
)}
{isPopupVisible && <GuidePopup onClose={handleClosePopup} />} {/* 팝업 표시 */}
Expand Down
35 changes: 29 additions & 6 deletions src/components/SideNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import MainCraftIcon from "@assets/icons/posture-craft-side-nav-icon.svg?react"
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 } from "react-router-dom"
import { Link, useLocation, useNavigate } from "react-router-dom"
import { useSnapshotStore } from "@/store/SnapshotStore"
import { useMemo } from "react"

const navItems = [
{
Expand All @@ -23,11 +25,32 @@ const navItems = [
},
]

const footerLinks = ["이용약관", "의견보내기", "로그아웃"]

export default function SideNav() {
const nickname = useAuthStore((state) => state.user?.nickname)
const location = useLocation()
const logout = useAuthStore((state) => state.logout)
const navigate = useNavigate()

const logoutHandler = (): void => {
const clearUser = useAuthStore.persist.clearStorage
const clearSnapshot = useSnapshotStore.persist.clearStorage

clearUser()
clearSnapshot()

logout(() => {
navigate("/")
})
}

const footerLinks = useMemo(
() => [
{ label: "이용약관", link: "", onClick: () => {} },
{ label: "의견보내기", link: "", onClick: () => {} },
{ label: "로그아웃", link: "", onClick: logoutHandler },
],
[logoutHandler]
)

return (
<aside className="w-[224px] flex-none bg-[#1C1D20]">
Expand Down Expand Up @@ -75,9 +98,9 @@ export default function SideNav() {
{/* Footer Links */}
<div className="mb-12">
<ul>
{footerLinks.map((link, index) => (
<li key={index} className="mb-3 cursor-pointer text-sm">
{link}
{footerLinks.map(({ label, onClick }, index) => (
<li key={index} className="mb-3 cursor-pointer text-sm" onClick={onClick}>
{label}
</li>
))}
</ul>
Expand Down
24 changes: 23 additions & 1 deletion src/pages/MonitoringPage.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
import { PoseDetector } from "@/components"
import PostrueCrew from "@/components/Posture/PostrueCrew"
import GroupSideIcon from "@assets/icons/group-side-nav-button.svg?react"
import React, { useState } from "react"
import React, { useEffect, useState } from "react"
import { useGetRecentSnapshot } from "@/hooks/useSnapshotMutation"
import { useSnapshotStore } from "@/store/SnapshotStore"

const MonitoringPage: React.FC = () => {
const getRecentSnapMutation = useGetRecentSnapshot()
const setSnap = useSnapshotStore((state) => state.setSnapshot)
const snapshot = useSnapshotStore((state) => state.snapshot)

const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(false)

const toggleSidebar = (): void => {
setIsSidebarOpen((prev) => !prev)
}

const init = async (): Promise<void> => {
// 최근 스냅샷을 가져오기
if (!snapshot) {
const userSnap = await getRecentSnapMutation.mutateAsync()

// 스냅샷이 있으면 store에 저장
if (userSnap.id !== -1) {
setSnap(userSnap.points.map((p) => ({ name: p.position.toLocaleLowerCase(), x: p.x, y: p.y, confidence: 1 })))
}
}
}

useEffect(() => {
init()
}, [])

return (
<div className="relative flex h-full w-full overflow-hidden">
{/* Main content area */}
Expand Down
7 changes: 6 additions & 1 deletion src/routes/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ import MonitoringLayout from "@/layouts/MonitoringLayout"
import AnalysisLayout from "@/layouts/AnalysisLayout"
import RoutePath from "@/constants/routes.json"
import AuthRoute from "@/routes/AuthRoute"
import { useAuthStore } from "@/store/AuthStore"

const Router: React.FC = () => {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated)

return (
<BrowserRouter>
<Routes>
<Route path={RoutePath.AUTH} element={<AuthPage />} />
<Route path="/" element={<HomePage />} />

{/* 로그인 상태에 따라 홈 페이지로 접근 시 리다이렉트 */}
<Route path="/" element={isAuthenticated ? <Navigate to={RoutePath.MONITORING} replace /> : <HomePage />} />

<Route element={<AuthRoute />}>
<Route element={<BaseLayout />}>
Expand Down
Loading

0 comments on commit 39a782b

Please sign in to comment.