Skip to content

Commit

Permalink
[feat/#28] 거북목, 어깨 틀어짐 자세 유지 시 서버에 데이터 전송
Browse files Browse the repository at this point in the history
  • Loading branch information
lkhoony committed Aug 26, 2024
1 parent 805fea3 commit 078a563
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 50 deletions.
17 changes: 15 additions & 2 deletions src/api/axiosInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import axios from "axios"

const API_BASE_URL = import.meta.env.VITE_API_BASE_URL
const EXCEPT_HEADER_API = ["/token", "/user/me", "/oauth"]

const axiosInstance = axios.create({
baseURL: API_BASE_URL,
Expand All @@ -10,6 +11,18 @@ const axiosInstance = axios.create({
},
})

// 요청 인터셉터 설정
axiosInstance.interceptors.request.use((config) => {
// 특정 API 경로에 대해 토큰을 제거
if (config.url) {
// 요청 URL이 EXCEPT_HEADER_API에 포함되어 있는지 확인
if (EXCEPT_HEADER_API.some((api) => config.url?.includes(api))) {
delete config.headers["X-HERO-AUTH-TOKEN"]
}
}
return config
})

// localStorage에서 토큰 가져오기
const token = localStorage.getItem("accessToken")
if (token) {
Expand All @@ -19,13 +32,13 @@ if (token) {
// 엑세스 토큰 설정 함수
export const setAccessToken = (_token: string): void => {
axiosInstance.defaults.headers.common["X-HERO-AUTH-TOKEN"] = _token
// localStorage.setItem("accessToken", token)
localStorage.setItem("accessToken", _token)
}

// 엑세스 토큰 제거 함수
export const clearAccessToken = (): void => {
delete axiosInstance.defaults.headers.common["X-HERO-AUTH-TOKEN"]
// localStorage.removeItem("accessToken")
localStorage.removeItem("accessToken")
}

export default axiosInstance
26 changes: 26 additions & 0 deletions src/api/pose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { pose } from "@/utils"
import axiosInstance from "./axiosInstance"

export type poseType = "GOOD" | "TURTLE_NECK" | "SHOULDER_TWIST" | "CHIN_UTP" | "TAILBONE_SIT"

export interface poseReq {
snapshot: pose
type: poseType
imageUrl?: string
}

export interface poseRes {
id: number
uid: number
type: poseType
createdAt: string
}
export const sendPose = async (poseReq: poseReq): Promise<poseRes> => {
try {
const res = await axiosInstance.post(`/pose-snapshots`, { ...poseReq })
const { id, uid, type, createdAt } = res.data.data
return { id, uid, type, createdAt }
} catch (e) {
throw e
}
}
2 changes: 1 addition & 1 deletion src/api/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export interface createSnapshotRes {

export const createSnapshot = async (snapshot: snapshot): Promise<createSnapshotRes> => {
try {
const res = await axiosInstance.post(`/pose-layouts`, { points : snapshot.points })
const res = await axiosInstance.post(`/pose-layouts`, { points: snapshot.points })
const { id } = res.data.data

return { id }
Expand Down
81 changes: 43 additions & 38 deletions src/components/PoseDetector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,36 @@ import { useCreateSnaphot } from "@/hooks/useSnapshotMutation"
import { position } from "@/api"
import PostureCheckIcon from "@assets/icons/good-posture-check-button-icon.svg?react"
import GuideIcon from "@assets/icons/posture-guide-button-icon.svg?react"
import { useSendPose } from "@/hooks/usePoseMutation"
import { poseType } from "@/api/pose"

const PoseDetector: React.FC = () => {
const [isScriptLoaded, setIsScriptLoaded] = useState<boolean>(false)
const [isScriptError, setIsScriptError] = useState<boolean>(false)
const [slope, setSlope] = useState<string | null>(null)
const [isTextNeck, setIsTextNeck] = useState<boolean | null>(null)
const [isShoulderTwist, setIsShoulderTwist] = useState<boolean | null>(null)
const [isModelLoaded, setIsModelLoaded] = useState<boolean>(false)
const [isSnapSaved, setIsSnapSaved] = useState<boolean>(false)
const [isPopupVisible, setIsPopupVisible] = useState<boolean>(false)
const modelRef = useRef<any>(null)
const snapRef = useRef<pose[] | null>(null)
const resultRef = useRef<pose[] | null>(null)
const textNeckStartTime = useRef<number | null>(null)
const timer = useRef<any>(null)

const turtleNeckTimer = useRef<any>(null)
const shoulderTwistTimer = useRef<any>(null)
// const chinUtpTimer = useRef<any>(null)
// const tailboneSit = useRef<any>(null)

const canvasRef = useRef<HTMLCanvasElement>(null)

const snapshot = useSnapshotStore((state) => state.snapshot)
const createSnapMutation = useCreateSnaphot()
const sendPoseMutation = useSendPose()

const setSnap = useSnapshotStore((state) => state.setSnapshot)

const { requestNotificationPermission, showNotification } = usePushNotification()

const requestApi = (delay: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, delay))

// webgl설정
const initializeBackend = async (): Promise<void> => {
await window.ml5.setBackend("webgl")
Expand Down Expand Up @@ -81,31 +87,14 @@ const PoseDetector: React.FC = () => {
drawPose(results, canvasRef.current)
}
if (snapRef.current) {
const _slope = detectSlope(snapRef.current, results, false)
const _isShoulderTwist = detectSlope(snapRef.current, results, false)
const _isTextNeck = detectTextNeck(snapRef.current, results, true)
if (_slope !== null) setSlope(_slope)
if (_isTextNeck !== null) setIsTextNeck(_isTextNeck)

if (_isTextNeck) {
if (!textNeckStartTime || !textNeckStartTime.current) {
textNeckStartTime.current = Date.now()
// 거북목 자세 3초 유지 시, api 요청을 보내게 (콘솔 로그에서 확인)
} else if (Date.now() - textNeckStartTime.current >= 3000) {
if (!timer.current) {
timer.current = setInterval(() => {
requestApi(1000).then(() => console.log("api request"))
showNotification()
}, 2000)
}
}
} else {
clearInterval(timer.current)
timer.current = null
textNeckStartTime.current = null
}
if (_isShoulderTwist !== null) setIsShoulderTwist(_isShoulderTwist)
if (_isTextNeck !== null) setIsTextNeck(_isTextNeck)
}
},
[setSlope, setIsTextNeck, showNotification]
[setIsShoulderTwist, setIsTextNeck, showNotification]
)

const detectStart = useCallback(
Expand Down Expand Up @@ -150,11 +139,34 @@ const PoseDetector: React.FC = () => {
}
}

const getIsRight = (_slope: string | null, _isTextNeck: boolean | null): boolean => {
if (_slope === "적절한 자세입니다" && !_isTextNeck) return true
const getIsRight = (_isShoulderTwist: boolean | null, _isTextNeck: boolean | null): boolean => {
if (!_isShoulderTwist && !_isTextNeck) 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)

useEffect(() => {
requestNotificationPermission()
getScript()
Expand All @@ -173,20 +185,13 @@ const PoseDetector: React.FC = () => {
getUserSnap()
}, [snapshot])

// const initializePoseMonitoring = () => {
// setIsTextNeck(null)
// setSlope(null)
// snapRef.current = null
// setIsSnapSaved(false)
// }

// 팝업 열기
const handleShowPopup = () : void => {
const handleShowPopup = (): void => {
setIsPopupVisible(true)
}

// 팝업 닫기
const handleClosePopup = () : void => {
const handleClosePopup = (): void => {
setIsPopupVisible(false)
}

Expand All @@ -204,7 +209,7 @@ 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(slope, isTextNeck)
: getIsRight(isShoulderTwist, isTextNeck)
? "올바른 자세입니다."
: "올바르지 않은 자세입니다."}
</div>
Expand Down
13 changes: 13 additions & 0 deletions src/hooks/usePoseMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { poseReq, poseRes, sendPose } from "@/api/pose"
import { useMutation, UseMutationResult } from "@tanstack/react-query"

export const useSendPose = (): UseMutationResult<poseRes, unknown, poseReq, unknown> => {
return useMutation({
mutationFn: (poseReq: poseReq) => {
return sendPose(poseReq)
},
onSuccess: (data) => {
console.log(data)
},
})
}
2 changes: 1 addition & 1 deletion src/pages/MonitoringPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import React, { useState } from "react"
const MonitoringPage: React.FC = () => {
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(false)

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

Expand Down
16 changes: 8 additions & 8 deletions src/utils/detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,25 +143,25 @@ export const detectTextNeck = (refer: pose[], comp: pose[], isSnapShotMode = tru
* @returns 기울기가 왼쪽으로 치우쳤으면 "left", 오른쪽으로 치우쳤으면 "right"를 반환하며,
* 기울기를 계산할 수 없는 경우 null을 반환
*/
export const detectSlope = (refer: pose[], comp: pose[], isSnapShotMode = true): string | null => {
export const detectSlope = (refer: pose[], comp: pose[], isSnapShotMode = true): boolean | null => {
if (!comp) return null

const referLeftSoulder = getXYfromPose(refer, "left_shoulder")
const referRightSoulder = getXYfromPose(refer, "right_shoulder")
const compLeftShoulder = getXYfromPose(comp, "left_shoulder")
const compRightShoulder = getXYfromPose(comp, "right_shoulder")

const SHOULDER_DIFF_THRESHOLD = 60
const SHOULDER_DIFF_THRESHOLD = 80

if (!isSnapShotMode && compLeftShoulder && compRightShoulder) {
const shoulderSlope = compLeftShoulder.y - compRightShoulder.y

if (Math.abs(shoulderSlope) < SHOULDER_DIFF_THRESHOLD) {
return "적절한 자세입니다"
return false
} else if (shoulderSlope > 0) {
return "오른쪽 어깨가 올라갔습니다"
return true
} else {
return "왼쪽 어깨가 올라갔습니다"
return true
}
}

Expand All @@ -177,10 +177,10 @@ export const detectSlope = (refer: pose[], comp: pose[], isSnapShotMode = true):
const slopeDifference = Math.abs(referSlope - compSlope)

if (slopeDifference <= tenPercentOfReferSlope) {
return "적절한 자세입니다"
return false
} else if (referSlope < compSlope) {
return "왼쪽으로 치우쳐져 있습니다"
return true
} else {
return "오른쪽으로 치우쳐져 있습니다"
return true
}
}

0 comments on commit 078a563

Please sign in to comment.