Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat/#28] 거북목, 어깨 틀어짐 자세 유지 시 서버에 데이터 전송 #36

Merged
merged 1 commit into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}
Loading