diff --git a/src/main/src/widgetConfig.ts b/src/main/src/widgetConfig.ts index de9ed20..5cc32d7 100644 --- a/src/main/src/widgetConfig.ts +++ b/src/main/src/widgetConfig.ts @@ -8,8 +8,8 @@ export const WIDGET_CONFIG = { defaultHeight: 320, // 최소 / 최대 크기 (사용자가 조절 가능한 범위) - minWidth: 150, - minHeight: 32, + minWidth: 160, + minHeight: 45, maxWidth: 260, maxHeight: 348, diff --git a/src/renderer/src/components/WidgetTitleBar/WidgetTitleBar.tsx b/src/renderer/src/components/WidgetTitleBar/WidgetTitleBar.tsx index aa75989..685cedb 100644 --- a/src/renderer/src/components/WidgetTitleBar/WidgetTitleBar.tsx +++ b/src/renderer/src/components/WidgetTitleBar/WidgetTitleBar.tsx @@ -40,8 +40,8 @@ export function WidgetTitleBar({
, sendMetrics: () => void, ) => { + // sessionId를 state로 관리하여 변경 감지 + const [sessionId, setSessionId] = useState(() => + localStorage.getItem('sessionId'), + ); + + // sendMetrics 함수를 ref로 저장 (dependency 문제 방지) + const sendMetricsRef = useRef(sendMetrics); + + // sendMetrics가 변경되면 ref 업데이트 + useEffect(() => { + sendMetricsRef.current = sendMetrics; + }, [sendMetrics]); + + // sessionId 변경 감지 (1초마다 체크) useEffect(() => { + const checkSessionId = () => { + const currentSessionId = localStorage.getItem('sessionId'); + if (currentSessionId !== sessionId) { + console.log( + `[세션 변경 감지] 타이머 초기화 - 이전: ${sessionId}, 현재: ${currentSessionId}`, + ); + setSessionId(currentSessionId); + } + }; + + const checkInterval = setInterval(checkSessionId, 1000); + + return () => { + clearInterval(checkInterval); + }; + }, [sessionId]); + + // 5분마다 자동 전송 (sessionId가 변경되면 타이머 재시작) + useEffect(() => { + // 세션이 없으면 interval을 시작하지 않음 + if (!sessionId) { + console.log('[자동 전송] 세션이 없어 타이머를 시작하지 않습니다.'); + return; + } + + console.log('[자동 전송] 5분 타이머 시작'); const FIVE_MINUTES = 5 * 60 * 1000; // 5분 = 300,000ms const intervalId = setInterval(() => { - const sessionId = localStorage.getItem('sessionId'); + const currentSessionId = localStorage.getItem('sessionId'); // 세션이 활성화되어 있고, 전송할 데이터가 있을 때만 전송 - if (sessionId && metricsRef.current && metricsRef.current.length > 0) { + if ( + currentSessionId && + metricsRef.current && + metricsRef.current.length > 0 + ) { console.log( `[자동 전송] ${metricsRef.current.length}개 메트릭 데이터 전송`, ); - sendMetrics(); + sendMetricsRef.current(); // ref를 통해 최신 함수 호출 } }, FIVE_MINUTES); - // 클린업: 컴포넌트 언마운트 시 interval 정리 + // 클린업: 컴포넌트 언마운트 시 또는 세션 변경 시 interval 정리 return () => { + console.log('[자동 전송] 타이머 정리'); clearInterval(intervalId); }; - }, [metricsRef, sendMetrics]); + }, [sessionId, metricsRef]); // sendMetrics 제거! }; diff --git a/src/renderer/src/pages/Calibration/components/WelcomePanel.tsx b/src/renderer/src/pages/Calibration/components/WelcomePanel.tsx index cb09f84..db34ccf 100644 --- a/src/renderer/src/pages/Calibration/components/WelcomePanel.tsx +++ b/src/renderer/src/pages/Calibration/components/WelcomePanel.tsx @@ -9,6 +9,9 @@ const WelcomePanel = ({ isPoseDetected, onStartMeasurement, }: WelcomePanelProps) => { + // localStorage에서 사용자 이름 가져오기 + const username = localStorage.getItem('userName') || '사용자'; + return (
@@ -16,7 +19,7 @@ const WelcomePanel = ({ 바른자세 기준점 등록

- 거부기온앤온님의 바른 자세를 등록할 준비가 되셨다면 + {username}님의 바른 자세를 등록할 준비가 되셨다면
측정하기 버튼을 눌러주세요.

diff --git a/src/renderer/src/pages/Main/MainPage.tsx b/src/renderer/src/pages/Main/MainPage.tsx index 83df5a2..9d23677 100644 --- a/src/renderer/src/pages/Main/MainPage.tsx +++ b/src/renderer/src/pages/Main/MainPage.tsx @@ -43,17 +43,34 @@ const MainPage = () => { const classifierRef = useRef(new PostureClassifier()); - // 메트릭을 서버로 전송하는 함수 - const sendMetricsToServer = () => { - const sessionId = localStorage.getItem('sessionId'); - if (sessionId && metricsRef.current.length > 0) { - saveMetrics({ - sessionId, - metrics: metricsRef.current, - }); - // 전송 후 메트릭 초기화 - metricsRef.current = []; - } + // 메트릭을 서버로 전송하는 함수 (Promise 반환) + const sendMetricsToServer = (): Promise => { + return new Promise((resolve) => { + const sessionId = localStorage.getItem('sessionId'); + if (sessionId && metricsRef.current.length > 0) { + saveMetrics( + { + sessionId, + metrics: metricsRef.current, + }, + { + onSuccess: () => { + // 전송 완료 후 메트릭 초기화 + metricsRef.current = []; + resolve(); + }, + onError: () => { + // 에러가 발생해도 resolve (계속 진행) + metricsRef.current = []; + resolve(); + }, + }, + ); + } else { + // 전송할 데이터가 없으면 즉시 완료 + resolve(); + } + }); }; /* 창 닫기 시 세션 정리 (메트릭 전송, 세션 종료, 카메라 종료, 위젯 닫기) */ diff --git a/src/renderer/src/pages/Main/components/AverageGraph/AverageGraphPannel.tsx b/src/renderer/src/pages/Main/components/AverageGraph/AverageGraphPannel.tsx index 57c2c64..daa080f 100644 --- a/src/renderer/src/pages/Main/components/AverageGraph/AverageGraphPannel.tsx +++ b/src/renderer/src/pages/Main/components/AverageGraph/AverageGraphPannel.tsx @@ -91,10 +91,10 @@ const AverageGraphPannel = () => { ''} itemStyle={{ fontSize: 12 }} diff --git a/src/renderer/src/pages/Main/components/ExitPanel.tsx b/src/renderer/src/pages/Main/components/ExitPanel.tsx index b8e0a79..419ad88 100644 --- a/src/renderer/src/pages/Main/components/ExitPanel.tsx +++ b/src/renderer/src/pages/Main/components/ExitPanel.tsx @@ -49,10 +49,16 @@ const ExitPanel = () => { }; // 세션 조회 API 데이터 사용 - const totalTime = Math.round((data?.data.totalSeconds || 0) / 60); // 초를 분으로 변환 - const correctPostureTime = Math.round((data?.data.goodSeconds || 0) / 60); // 바른 자세 시간 (분) + const totalSeconds = data?.data.totalSeconds || 0; + const goodSeconds = data?.data.goodSeconds || 0; + + const totalTime = Math.round(totalSeconds / 60); // 초를 분으로 변환 + const correctPostureTime = Math.round(goodSeconds / 60); // 바른 자세 시간 (분) + + // 비율은 초 단위로 먼저 계산 후 반올림 (정확도 향상) const correctPosturePercentage = - totalTime > 0 ? Math.round((correctPostureTime / totalTime) * 100) : 75; + totalSeconds > 0 ? Math.round((goodSeconds / totalSeconds) * 100) : 0; + const score = data?.data.score || 0; // 바른 자세 점수 /* 이번 세션에서 이동한 거리 계산 */ @@ -82,10 +88,16 @@ const ExitPanel = () => { [colors.background], ); - // 안쪽 링 프로그레스 데이터 (노란색) - 바른 자세 점수만큼 노란색 + // 안쪽 링 프로그레스 데이터 (노란색) - 바른 자세 비율만큼 노란색 const ScoreProgressData = useMemo( - () => [{ name: '바른 자세 점수', value: score, color: colors.score }], - [score, colors.score], + () => [ + { + name: '바른 자세 비율', + value: correctPosturePercentage, + color: colors.score, + }, + ], + [correctPosturePercentage, colors.score], ); const formatTime = (minutes: number) => { @@ -179,7 +191,7 @@ const ExitPanel = () => { innerRadius={77.75} outerRadius={92} startAngle={450} - endAngle={450 - (72 / 100) * 360} + endAngle={450 - (correctPosturePercentage / 100) * 360} dataKey="value" stroke="none" paddingAngle={0} @@ -211,7 +223,7 @@ const ExitPanel = () => { 바른 자세 시간 - {72}% + {correctPosturePercentage}%

diff --git a/src/renderer/src/pages/Main/components/WebcamPanel.tsx b/src/renderer/src/pages/Main/components/WebcamPanel.tsx index 462f80c..fa855c2 100644 --- a/src/renderer/src/pages/Main/components/WebcamPanel.tsx +++ b/src/renderer/src/pages/Main/components/WebcamPanel.tsx @@ -22,7 +22,7 @@ interface Props { worldLandmarks?: WorldLandmark[], ) => void; onToggleWebcam: () => void; - onSendMetrics: () => void; + onSendMetrics: () => Promise; } const WebcamPanel = ({ @@ -45,7 +45,7 @@ const WebcamPanel = ({ useResumeSessionMutation(); const { data: levelData } = useLevelQuery(); - const handleStartStop = () => { + const handleStartStop = async () => { if (isExit) { // 시작하기: 세션 생성 후 카메라 시작 createSession(undefined, { @@ -61,13 +61,13 @@ const WebcamPanel = ({ }, }); } else { - // 종료하기: 메트릭 전송 → 세션 중단 → 카메라 종료 → 위젯 닫기 + // 종료하기: 메트릭 전송 완료 → 세션 중단 → 카메라 종료 → 위젯 닫기 const sessionId = localStorage.getItem('sessionId'); if (sessionId) { - // 1. 수집된 메트릭을 서버로 전송 - onSendMetrics(); + // 1. 수집된 메트릭을 서버로 전송 (완료 대기) + await onSendMetrics(); - // 2. 세션 종료 + // 2. 세션 종료 (메트릭 전송 완료 후 실행) stopSession(sessionId, { onSuccess: () => { setExit(); diff --git a/src/renderer/src/pages/Widget/WidgetPage.tsx b/src/renderer/src/pages/Widget/WidgetPage.tsx index fcfd664..12f694f 100644 --- a/src/renderer/src/pages/Widget/WidgetPage.tsx +++ b/src/renderer/src/pages/Widget/WidgetPage.tsx @@ -9,7 +9,6 @@ import { usePostureSyncWithLocalStorage } from './hooks/usePostureSyncWithLocalS import { useThemeSync } from './hooks/useThemeSync'; type WidgetSize = 'mini' | 'medium'; -type PostureState = 'turtle' | 'giraffe'; /* 레이아웃 전환 기준점 */ const BREAKPOINT = { @@ -75,7 +74,7 @@ export function WidgetPage() { const isMini = widgetSize === 'mini'; return ( -
+
{/* 커스텀 타이틀바 */} diff --git a/src/renderer/src/pages/Widget/components/MediumWidgetContent.tsx b/src/renderer/src/pages/Widget/components/MediumWidgetContent.tsx index c5e9957..8b05d24 100644 --- a/src/renderer/src/pages/Widget/components/MediumWidgetContent.tsx +++ b/src/renderer/src/pages/Widget/components/MediumWidgetContent.tsx @@ -64,10 +64,10 @@ export function MediumWidgetContent({ posture }: MediumWidgetContentProps) { } return ( -
+
{/* 캐릭터 영역 */}
{isGiraffe ? ( @@ -78,7 +78,7 @@ export function MediumWidgetContent({ posture }: MediumWidgetContentProps) {
{/* 상세 정보 영역 */} -
+
{/* 진행 바 */}
diff --git a/src/renderer/src/styles/breakpoint.css b/src/renderer/src/styles/breakpoint.css index aa7940f..268e863 100644 --- a/src/renderer/src/styles/breakpoint.css +++ b/src/renderer/src/styles/breakpoint.css @@ -20,6 +20,7 @@ } /* 미디어 쿼리 헬퍼 */ +@custom-variant mini (@media(max-height: 62px)); @custom-media --mobile (max-width: 30rem); @custom-media --tablet (min-width: 30rem) and (max-width: 56rem); @custom-media --large (min-width: 80rem); diff --git a/src/renderer/src/styles/colors.css b/src/renderer/src/styles/colors.css index 976c60e..7e86381 100644 --- a/src/renderer/src/styles/colors.css +++ b/src/renderer/src/styles/colors.css @@ -89,6 +89,9 @@ --color-modal-button: #f9f8f7; --color-modal-disabled: #e3e1df; --color-global-yellow-100: #ffebb0; + + /* 평균 자세 그래프 색깔 */ + --color-dashboard-score: #ffe28a; } @theme inline { @@ -166,6 +169,9 @@ --color-modal-disabled: var(--color-modal-disabled); --color-sementic-brand-primary: var(--color-sementic-brand-primary); --color-global-yellow-100: var(--color-global-yellow-100); + + /* 평균 자세 그래프 색깔*/ + --color-dashboard-score: var(--color-dashboard-score); } /* Dark mode colors */ @@ -246,4 +252,7 @@ --color-modal-button: #232323; --color-modal-disabled: #2c2c2c; --color-global-yellow-100: rgba(73, 55, 4, 0.5); + + /* 평균 자세 그래프 색깔*/ + --color-dashboard-score: #2c2c2c; }