From 5e9502cb2f61b267b6bf4458f6ac62dee723522b Mon Sep 17 00:00:00 2001 From: choihooo Date: Wed, 26 Nov 2025 21:12:52 +0900 Subject: [PATCH 1/7] =?UTF-8?q?fix(main):=20=EC=B6=9C=EC=84=9D=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=20=ED=8C=A8=EB=84=90=20=ED=99=94=EC=82=B4=ED=91=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/assets/arrow-narrow-down.svg | 2 +- src/renderer/src/assets/arrow-narrow-up.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/assets/arrow-narrow-down.svg b/src/renderer/src/assets/arrow-narrow-down.svg index 8e9d0c6..560cc24 100644 --- a/src/renderer/src/assets/arrow-narrow-down.svg +++ b/src/renderer/src/assets/arrow-narrow-down.svg @@ -1,3 +1,3 @@ - + diff --git a/src/renderer/src/assets/arrow-narrow-up.svg b/src/renderer/src/assets/arrow-narrow-up.svg index 028e2d5..91016a6 100644 --- a/src/renderer/src/assets/arrow-narrow-up.svg +++ b/src/renderer/src/assets/arrow-narrow-up.svg @@ -1,3 +1,3 @@ - + From e618d3a40f21bfffded57ccc6a540f0a45f395bb Mon Sep 17 00:00:00 2001 From: choihooo Date: Wed, 26 Nov 2025 21:17:43 +0900 Subject: [PATCH 2/7] =?UTF-8?q?docs:=20=EB=A6=AC=EB=93=9C=EB=AF=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 636 +++++++++++++++++------------------------------------- 1 file changed, 198 insertions(+), 438 deletions(-) diff --git a/README.md b/README.md index 0ee1294..84b2781 100644 --- a/README.md +++ b/README.md @@ -1,482 +1,242 @@ -## ๐Ÿ“ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ +# GBGR (๊ฑฐ๋ถ€๊ธฐ๋ฆฐ) -``` -src/ -โ”œโ”€โ”€ main/ # Electron Main Process -โ”œโ”€โ”€ preload/ # Electron Preload Script -โ””โ”€โ”€ renderer/ # Electron Renderer Process (React) - โ””โ”€โ”€ src/ - โ”œโ”€โ”€ packages/ - โ”‚ โ”œโ”€โ”€ api/ # API ๊ด€๋ จ ๋กœ์ง ๋ฐ ํ›… - โ”‚ โ””โ”€โ”€ ui/ # UI ์ปดํฌ๋„ŒํŠธ ๋ฐ ์Šคํƒ€์ผ - โ”œโ”€โ”€ pages/ # ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ - โ”œโ”€โ”€ components/ # ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ - โ””โ”€โ”€ ... -``` +> ์‹ค์‹œ๊ฐ„ ์›น์บ  ์ž์„ธ ์ธ์‹์„ ํ†ตํ•ด ๊ฑฐ๋ถ๋ชฉ๊ณผ ๊ฐ™์€ ์ž˜๋ชป๋œ ์ž์„ธ๋ฅผ ๊ต์ •ํ•˜๊ณ  ์‚ฌ์šฉ์ž์—๊ฒŒ ํ†ต๊ณ„ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•˜๋Š” ๋ฐ์Šคํฌํƒ‘ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ -## ๐Ÿ“ Git Commit Convention - -## 1. Branch Naming Rule - -**Branch ์ด๋ฆ„**์€ **์ž‘์—… ๋ชฉ์ ๊ณผ ์—ฐ๊ด€๋œ ์ด์Šˆ ๋ฒˆํ˜ธ๋ฅผ ํฌํ•จํ•˜๋Š” ๋ฐฉ์‹** - -```php -<ํƒ€์ž…>/<์ด์Šˆ ๋ฒˆํ˜ธ>-<๊ฐ„๋‹จํ•œ ์„ค๋ช…> - -- feature/1234-add-user-login -- bugfix/5678-fix-login-error -- release/1.2.0 -``` - -### Branch Type - -- **feature/ - ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ ์‹œ** -- **bugfix/ -** **๋ฒ„๊ทธ ์ˆ˜์ •** ์‹œ -- **hotfix/ -** **๊ธด๊ธ‰ํ•œ ๋ฒ„๊ทธ ์ˆ˜์ •** ์‹œ (๋ณดํ†ต ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ๋ฐœ์ƒ) -- **release/ -** **๋ฆด๋ฆฌ์ฆˆ ์ค€๋น„ ์‹œ** -- **chore/ -** ๋นŒ๋“œ ๋ฐ ๊ธฐํƒ€ ์ž‘์—… ์ž๋™ํ™”, ๋ฌธ์„œ ์ž‘์—… ๋“ฑ **์ฝ”๋“œ์™€ ๊ด€๋ จ ์—†๋Š” ์ž‘์—…** +

+ ๊ฑฐ๋ถ€๊ธฐ๋ฆฐ ์•„์ด์ฝ˜ +

--- -## ๐Ÿ”ง API ํŒจํ‚ค์ง€์— ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ์ถ”๊ฐ€ํ•˜๊ธฐ - -### 1. ์ƒˆ๋กœ์šด API ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€ - -#### 1.1 ํƒ€์ž… ์ •์˜ ์ถ”๊ฐ€ - -```typescript -// packages/api/src/types/index.ts -export interface NewFeatureResponse { - id: string; - name: string; - status: 'active' | 'inactive'; - createdAt: string; -} -``` - -#### 1.2 API ํด๋ผ์ด์–ธํŠธ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ - -```typescript -// packages/api/src/client/api-client.ts -export class ElectronAPIClient { - // ๊ธฐ์กด ๋ฉ”์„œ๋“œ๋“ค... - - async getNewFeature(id: string): Promise { - if (typeof window !== 'undefined' && (window as any).electronAPI) { - return await (window as any).electronAPI.getNewFeature(id); - } - throw new Error('Electron API not available'); - } -} - -export class WebAPIClient { - // ๊ธฐ์กด ๋ฉ”์„œ๋“œ๋“ค... - - async getNewFeature(id: string): Promise { - const response = await fetch(`${this.baseURL}/new-feature/${id}`); - if (!response.ok) { - throw new Error('Failed to fetch new feature'); - } - return await response.json(); - } -} -``` - -#### 1.3 React Query ํ›… ์ถ”๊ฐ€ - -```typescript -// packages/api/src/hooks/use-new-feature.ts -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { createAPIClient } from '../../index'; -import { NewFeatureResponse } from '../types'; - -let apiClient: ReturnType | null = null; - -function getApiClient() { - if (!apiClient) { - apiClient = createAPIClient(); - } - return apiClient; -} - -// ์กฐํšŒ ํ›… -export function useNewFeature(id: string) { - return useQuery({ - queryKey: ['newFeature', id], - queryFn: () => getApiClient().getNewFeature(id), - enabled: !!id, - staleTime: 5 * 60 * 1000, // 5๋ถ„ - retry: 3, - }); -} - -// ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ ๋ฎคํ…Œ์ด์…˜ ํ›… -export function useNewFeatureMutation() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (data: Partial) => - getApiClient().createNewFeature(data), - onSuccess: () => { - // ๊ด€๋ จ ์ฟผ๋ฆฌ ๋ฌดํšจํ™” - queryClient.invalidateQueries({ queryKey: ['newFeature'] }); - }, - }); -} -``` - -#### 1.4 ํ›… ์ต์ŠคํฌํŠธ ์ถ”๊ฐ€ - -```typescript -// packages/api/src/hooks/index.ts -export * from './use-new-feature'; -// ๊ธฐ์กด ์ต์ŠคํฌํŠธ๋“ค... -``` - -#### 1.5 ๋ฉ”์ธ ์ต์ŠคํฌํŠธ ์—…๋ฐ์ดํŠธ - -```typescript -// packages/api/index.ts -// ๊ธฐ์กด ์ต์ŠคํฌํŠธ๋“ค... -export * from './src/hooks/use-new-feature'; -``` +## ๐Ÿ”— ๋ฐ๋ชจ & ์ž๋ฃŒ -### 2. ์ƒˆ๋กœ์šด ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ์ถ”๊ฐ€ +- ์„œ๋น„์Šค ํ™ˆํŽ˜์ด์ง€: [https://bugi.co.kr](https://bugi.co.kr) -#### 2.1 ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ์ž‘์„ฑ - -```typescript -// packages/api/src/utils/new-feature-utils.ts -export function formatNewFeatureName(name: string): string { - return name.trim().toLowerCase().replace(/\s+/g, '-'); -} - -export function validateNewFeatureData(data: any): boolean { - return data && typeof data.name === 'string' && data.name.length > 0; -} -``` - -#### 2.2 ์œ ํ‹ธ๋ฆฌํ‹ฐ ์ต์ŠคํฌํŠธ ์ถ”๊ฐ€ - -```typescript -// packages/api/src/utils/index.ts -export * from './new-feature-utils'; -// ๊ธฐ์กด ์ต์ŠคํฌํŠธ๋“ค... -``` - -## ๐ŸŽจ UI ํŒจํ‚ค์ง€์— ์ƒˆ๋กœ์šด ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ํ•˜๊ธฐ +--- -### 1. ์ƒˆ๋กœ์šด ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ +## โœจ ํ•ต์‹ฌ ์š”์•ฝ (TL;DR) -#### 1.1 ์ปดํฌ๋„ŒํŠธ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ +- **๋ฌธ์ œ ์ธ์‹**: ์žฅ์‹œ๊ฐ„ ์ปดํ“จํ„ฐ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฌด์˜์‹์ ์œผ๋กœ ์ž์„ธ๊ฐ€ ํํŠธ๋Ÿฌ์ ธ ๊ฑฐ๋ถ๋ชฉ, ์–ด๊นจ ๋น„๋Œ€์นญ ๋“ฑ์˜ ๋ฌธ์ œ๋ฅผ ๊ฒช๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค. +- **ํ•ต์‹ฌ ํ•ด๊ฒฐ ์ „๋žต**: + - ์›น์บ ์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž์˜ ์ž์„ธ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ถ„์„ํ•˜๊ณ  ์‹œ๊ฐ์ ์œผ๋กœ ํ”ผ๋“œ๋ฐฑํ•ฉ๋‹ˆ๋‹ค. + - ์ž˜๋ชป๋œ ์ž์„ธ๊ฐ€ ๊ฐ์ง€๋˜๋ฉด ์•Œ๋ฆผ์„ ๋ณด๋‚ด ์ฆ‰๊ฐ์ ์ธ ๊ต์ •์„ ์œ ๋„ํ•ฉ๋‹ˆ๋‹ค. + - ์„ธ์…˜๋ณ„ ์ž์„ธ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋กํ•˜๊ณ  ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ์ œ๊ณตํ•˜์—ฌ ์žฅ๊ธฐ์ ์ธ ์Šต๊ด€ ๊ฐœ์„ ์„ ๋•์Šต๋‹ˆ๋‹ค. +- **ํ”„๋ก ํŠธ์—”๋“œ ๊ธฐ์ˆ  ํฌ์ธํŠธ**: + - `@mediapipe/tasks-vision`์„ ํ™œ์šฉํ•˜์—ฌ ์›น์บ  ์˜์ƒ์—์„œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์‹ ์ฒด ํ‚คํฌ์ธํŠธ๋ฅผ ์ถ”์ถœํ•˜๊ณ  ์ž์„ธ๋ฅผ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. + - `Zustand`๋ฅผ ์‚ฌ์šฉํ•ด ์„ธ์…˜, ์•Œ๋ฆผ, ์นด๋ฉ”๋ผ ๋“ฑ ๋ณต์žกํ•œ ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + - `Recharts`๋ฅผ ์ด์šฉํ•ด ์‚ฌ์šฉ์ž์˜ ์ž์„ธ ์ ์ˆ˜, ํŒจํ„ด ๋“ฑ ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ๋ฅผ ์‹œ๊ฐํ™”ํ•˜์—ฌ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + - `Electron`์˜ IPC ํ†ต์‹ ์„ ํ™œ์šฉํ•˜์—ฌ ๋ฉ”์ธ ์œˆ๋„์šฐ์™€ ์œ„์ ฏ ๊ฐ„์˜ ์ƒํƒœ๋ฅผ ๋™๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. -``` -packages/ui/src/components/NewComponent/ -โ”œโ”€โ”€ NewComponent.tsx -โ”œโ”€โ”€ NewComponent.css (์„ ํƒ์‚ฌํ•ญ) -โ””โ”€โ”€ index.ts (์„ ํƒ์‚ฌํ•ญ) -``` +--- -#### 1.2 ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„ - -```typescript -// packages/ui/src/components/NewComponent/NewComponent.tsx -import React from 'react'; -import { cn } from '../../utils/cn'; - -interface NewComponentProps { - title: string; - description?: string; - variant?: 'default' | 'primary' | 'secondary'; - size?: 'sm' | 'md' | 'lg'; - className?: string; - children?: React.ReactNode; -} - -export function NewComponent({ - title, - description, - variant = 'default', - size = 'md', - className, - children, -}: NewComponentProps) { - return ( -
-

{title}

- {description && ( -

{description}

- )} - {children} -
- ); -} -``` +## ๐Ÿงฉ ์ฃผ์š” ๊ธฐ๋Šฅ + +1. **์‹ค์‹œ๊ฐ„ ์ž์„ธ ๋ถ„์„ ๋ฐ ์œ„์ ฏ** + - ์„ค๋ช…: ์›น์บ ์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž์˜ ์ž์„ธ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ถ„์„ํ•˜๊ณ , MediaPipe๋ฅผ ํ™œ์šฉํ•ด ์‹ ์ฒด ํ‚คํฌ์ธํŠธ๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. ๋ฉ”์ธ ํ™”๋ฉด๊ณผ ๋ฐ์Šคํฌํƒ‘ ์œ„์ ฏ์„ ํ†ตํ•ด ํ˜„์žฌ ์ž์„ธ ์ƒํƒœ(๊ฑฐ๋ถ๋ชฉ/๊ธฐ๋ฆฐ)๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ™•์ธํ•˜๊ณ  ์„ธ์…˜์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์œ„์ ฏ์€ ํ•ญ์ƒ ์œ„์— ํ‘œ์‹œ๋˜๋ฉฐ ํฌ๊ธฐ ์กฐ์ ˆ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. + - ๊ธฐ์ˆ  ํฌ์ธํŠธ: `react-webcam`, `@mediapipe/tasks-vision`, Electron `BrowserWindow`, `PostureClassifier` (์•ˆ์ •ํ™” ๋กœ์ง ํฌํ•จ) + - ์ฃผ์š” ํŠน์ง•: + - PI(Posture Index) ๊ณ„์‚ฐ์„ ํ†ตํ•œ ์ •๋Ÿ‰์  ์ž์„ธ ํ‰๊ฐ€ + - EMA(Exponential Moving Average) ์Šค๋ฌด๋”ฉ์œผ๋กœ ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ + - PostureStabilizer๋ฅผ ํ†ตํ•œ ์•ˆ์ •ํ™”๋œ ์ž์„ธ ํŒ์ • + - ์ •๋ฉด์„ฑ ๊ฒ€์‚ฌ(roll, centerRatio)๋กœ ์ธก์ • ์‹ ๋ขฐ๋„ ๋ณด์žฅ + +2. **์บ˜๋ฆฌ๋ธŒ๋ ˆ์ด์…˜ ์‹œ์Šคํ…œ** + - ์„ค๋ช…: ์‚ฌ์šฉ์ž ๊ฐœ์ธ์˜ ์ •์ƒ ์ž์„ธ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์„ค์ •ํ•˜๋Š” ์บ˜๋ฆฌ๋ธŒ๋ ˆ์ด์…˜ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. ์ธก์ • ์ค‘ ๋ฐ๊ธฐ, ์ž์„ธ ์•ˆ์ •์„ฑ ๋“ฑ์„ ๊ฒ€์‚ฌํ•˜์—ฌ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ์ค€๊ฐ’์„ ํ™•๋ณดํ•ฉ๋‹ˆ๋‹ค. + - ๊ธฐ์ˆ  ํฌ์ธํŠธ: `trimmedStats` (์ƒํ•˜ 5% ์ ˆ์‚ฌ ํ‰๊ท  ๋ฐ ํ‘œ์ค€ํŽธ์ฐจ), `errorChecks` (๋ฐ๊ธฐ/์•ˆ์ •์„ฑ ๊ฒ€์ฆ) + +3. **ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ** + - ์„ค๋ช…: ์ผ๋ณ„, ์ฃผ๋ณ„ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž์„ธ ์ ์ˆ˜ ๋ณ€ํ™”, ์ฃผ์š” ์ž์„ธ ํŒจํ„ด, ์ถœ์„๋ฅ , ํ‰๊ท  ์ž์„ธ ์ ์ˆ˜, ๋ ˆ๋ฒจ ์ง„ํ–‰๋„ ๋“ฑ์„ ์‹œ๊ฐ์ ์œผ๋กœ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. Recharts๋ฅผ ํ™œ์šฉํ•œ ๋‹ค์–‘ํ•œ ์ฐจํŠธ๋กœ ์‚ฌ์šฉ์ž์˜ ์žฅ๊ธฐ์ ์ธ ์ž์„ธ ๊ฐœ์„  ์ถ”์ด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + - ๊ธฐ์ˆ  ํฌ์ธํŠธ: `TanStack Query` (์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ์บ์‹ฑ ๋ฐ ๋™๊ธฐํ™”), `Recharts` (์ฐจํŠธ ์‹œ๊ฐํ™”) + - ์ฃผ์š” ํŒจ๋„: + - ํ‰๊ท  ์ž์„ธ ์ ์ˆ˜ ๊ทธ๋ž˜ํ”„ (์‹œ๊ฐ„๋Œ€๋ณ„ ์ถ”์ด) + - ์ž์„ธ ํŒจํ„ด ๋ถ„์„ (๊ฑฐ๋ถ๋ชฉ/๊ธฐ๋ฆฐ ๋น„์œจ) + - ์ถœ์„๋ฅ  ๋ฐ ์—ฐ์† ์ถœ์„ ์ผ์ˆ˜ + - ๋ ˆ๋ฒจ ์ง„ํ–‰๋„ ๋ฐ ๋‹ฌ์„ฑ๋ฅ  + +4. **์ž์„ธ ๊ต์ • ์•Œ๋ฆผ** + - ์„ค๋ช…: ๊ฑฐ๋ถ๋ชฉ์ด๋‚˜ ๋น„๋Œ€์นญ ์ž์„ธ๊ฐ€ ์ผ์ • ์‹œ๊ฐ„ ์ด์ƒ ์ง€์†๋˜๋ฉด ๋ฐ์Šคํฌํƒ‘ ์•Œ๋ฆผ์„ ๋ณด๋‚ด ์‚ฌ์šฉ์ž๊ฐ€ ์ธ์ง€ํ•˜๊ณ  ๋ฐ”๋กœ์žก์„ ์ˆ˜ ์žˆ๋„๋ก ๋•์Šต๋‹ˆ๋‹ค. ์•Œ๋ฆผ ๊ฐ•๋„์™€ ์ฃผ๊ธฐ๋ฅผ ์‚ฌ์šฉ์ž๊ฐ€ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + - ๊ธฐ์ˆ  ํฌ์ธํŠธ: Electron `Notification` API, `useNotificationScheduler` ํ›…, `Zustand` (์•Œ๋ฆผ ์ƒํƒœ ๊ด€๋ฆฌ) + +5. **์„ธ์…˜ ๊ด€๋ฆฌ** + - ์„ค๋ช…: ์ž์„ธ ์ธก์ • ์„ธ์…˜์„ ์‹œ์ž‘, ์ผ์‹œ์ •์ง€, ์žฌ๊ฐœ, ์ข…๋ฃŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„ธ์…˜ ์ค‘ ์ˆ˜์ง‘๋œ ๋ฉ”ํŠธ๋ฆญ ๋ฐ์ดํ„ฐ๋Š” ์ฃผ๊ธฐ์ ์œผ๋กœ ์„œ๋ฒ„์— ์ €์žฅ๋˜๋ฉฐ, ์„ธ์…˜ ์ข…๋ฃŒ ์‹œ ๋ฆฌํฌํŠธ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + - ๊ธฐ์ˆ  ํฌ์ธํŠธ: `useAutoMetricsSender` (์ž๋™ ๋ฉ”ํŠธ๋ฆญ ์ „์†ก), `useSessionCleanup` (์„ธ์…˜ ์ •๋ฆฌ) -#### 1.3 ์ปดํฌ๋„ŒํŠธ ์ต์ŠคํฌํŠธ ์ถ”๊ฐ€ +--- -```typescript -// packages/ui/src/components/NewComponent/index.ts -export { NewComponent } from './NewComponent'; -``` +## ๐Ÿ›  ๊ธฐ์ˆ  ์Šคํƒ -### 2. Storybook ์Šคํ† ๋ฆฌ ์ถ”๊ฐ€ - -#### 2.1 ์Šคํ† ๋ฆฌ ํŒŒ์ผ ์ƒ์„ฑ - -```typescript -// packages/ui/src/stories/NewComponent.stories.tsx -import type { Meta, StoryObj } from '@storybook/react'; -import { NewComponent } from '../components/NewComponent/NewComponent'; - -const meta: Meta = { - title: 'Components/NewComponent', - component: NewComponent, - parameters: { - layout: 'centered', - }, - tags: ['autodocs'], - argTypes: { - variant: { - control: { type: 'select' }, - options: ['default', 'primary', 'secondary'], - }, - size: { - control: { type: 'select' }, - options: ['sm', 'md', 'lg'], - }, - }, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - title: '๊ธฐ๋ณธ ์ปดํฌ๋„ŒํŠธ', - description: '์ด๊ฒƒ์€ ๊ธฐ๋ณธ ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.', - }, -}; - -export const Primary: Story = { - args: { - title: 'Primary ์ปดํฌ๋„ŒํŠธ', - description: '์ด๊ฒƒ์€ Primary ์Šคํƒ€์ผ์˜ ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.', - variant: 'primary', - }, -}; - -export const Large: Story = { - args: { - title: 'Large ์ปดํฌ๋„ŒํŠธ', - description: '์ด๊ฒƒ์€ Large ํฌ๊ธฐ์˜ ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.', - size: 'lg', - }, -}; -``` +### Frontend -### 3. ํŒจํ‚ค์ง€ ์ต์ŠคํฌํŠธ ์—…๋ฐ์ดํŠธ +- **Framework**: `Electron`, `React` +- **Language**: `TypeScript` +- **State Management**: `Zustand`, `TanStack Query` +- **Styling**: `Tailwind CSS` +- **Build/Bundle**: `Vite` +- **Pose Detection**: `@mediapipe/tasks-vision` -#### 3.1 ๋ฉ”์ธ ์ต์ŠคํฌํŠธ ์ถ”๊ฐ€ +### ํ˜‘์—… & ๊ธฐํƒ€ -```typescript -// packages/ui/index.tsx -import './src/styles/globals.css'; +- **Version Control**: `Git`, GitHub +- **CI**: `GitHub Actions` +- **Package Manager**: `pnpm` +- **Form Validation**: `react-hook-form`, `zod` +- **Animation**: `framer-motion` +- **Routing**: `react-router-dom` -export * from './src/components/Button/Button'; -export * from './src/components/Header/Header'; -export * from './src/components/Page/Page'; -export * from './src/components/Alert/Alert'; -export * from './src/components/AnimatedBox/AnimatedBox'; -export * from './src/components/NewComponent/NewComponent'; // ์ƒˆ๋กœ ์ถ”๊ฐ€ -``` +--- -#### 3.2 package.json exports ์—…๋ฐ์ดํŠธ - -```json -// packages/ui/package.json -{ - "exports": { - ".": "./index.tsx", - "./tokens.css": "./src/styles/tokens.css", - "./globals.css": "./src/styles/globals.css", - "./theme.css": "./src/styles/theme.css", - "./Button": "./src/components/Button/Button.tsx", - "./AnimatedBox": "./src/components/AnimatedBox/AnimatedBox.tsx", - "./Header": "./src/components/Header/Header.tsx", - "./Page": "./src/components/Page/Page.tsx", - "./Alert": "./src/components/Alert/Alert.tsx", - "./NewComponent": "./src/components/NewComponent/NewComponent.tsx" - } -} -``` +## ๐Ÿ“ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ -## ๐Ÿงช ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€ํ•˜๊ธฐ - -### API ํŒจํ‚ค์ง€ ํ…Œ์ŠคํŠธ - -```typescript -// packages/api/src/hooks/__tests__/use-new-feature.test.ts -import { renderHook, waitFor } from '@testing-library/react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { useNewFeature } from '../use-new-feature'; - -const createWrapper = () => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, - }); - return ({ children }: { children: React.ReactNode }) => ( - - {children} - - ); -}; - -describe('useNewFeature', () => { - it('should fetch new feature data', async () => { - const { result } = renderHook(() => useNewFeature('test-id'), { - wrapper: createWrapper(), - }); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - }); -}); -``` +```bash +src/ +โ”œโ”€โ”€ main/ # Electron ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค +โ”‚ โ””โ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ index.ts # IPC ํ•ธ๋“ค๋Ÿฌ ๋ฐ ์•ฑ ์ดˆ๊ธฐํ™” +โ”‚ โ”œโ”€โ”€ mainWindow.ts # ๋ฉ”์ธ ์œˆ๋„์šฐ ์ƒ์„ฑ ๋ฐ ๊ด€๋ฆฌ +โ”‚ โ”œโ”€โ”€ widgetWindow.ts # ์œ„์ ฏ ์œˆ๋„์šฐ ์ƒ์„ฑ ๋ฐ ๊ด€๋ฆฌ +โ”‚ โ””โ”€โ”€ notificationHandlers.ts # ์•Œ๋ฆผ ๊ด€๋ จ ํ•ธ๋“ค๋Ÿฌ +โ”œโ”€โ”€ preload/ # Electron ํ”„๋ฆฌ๋กœ๋“œ ์Šคํฌ๋ฆฝํŠธ +โ”‚ โ””โ”€โ”€ src/ +โ”‚ โ””โ”€โ”€ index.ts # Context Bridge API ๋…ธ์ถœ +โ””โ”€โ”€ renderer/ # React ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ (UI) + โ””โ”€โ”€ src/ + โ”œโ”€โ”€ api/ # API ์š”์ฒญ ๊ด€๋ จ ํ›… (TanStack Query) + โ”‚ โ”œโ”€โ”€ dashboard/ # ๋Œ€์‹œ๋ณด๋“œ ๋ฐ์ดํ„ฐ ์ฟผ๋ฆฌ + โ”‚ โ”œโ”€โ”€ session/ # ์„ธ์…˜ ๊ด€๋ฆฌ ๋ฎคํ…Œ์ด์…˜ + โ”‚ โ””โ”€โ”€ login/ # ์ธ์ฆ ๊ด€๋ จ + โ”œโ”€โ”€ assets/ # ์ด๋ฏธ์ง€, ํฐํŠธ ๋“ฑ ์ •์  ์—์…‹ + โ”œโ”€โ”€ components/ # ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ + โ”‚ โ”œโ”€โ”€ pose-detection/ # ์ž์„ธ ์ธ์‹ ๊ด€๋ จ ์ปดํฌ๋„ŒํŠธ + โ”‚ โ”‚ โ”œโ”€โ”€ PostureClassifier.ts # ์ž์„ธ ๋ถ„๋ฅ˜ ์—”์ง„ + โ”‚ โ”‚ โ”œโ”€โ”€ PostureStabilizer.ts # ์•ˆ์ •ํ™” ๋กœ์ง + โ”‚ โ”‚ โ””โ”€โ”€ calculations.ts # PI ๊ณ„์‚ฐ ๋“ฑ + โ”‚ โ”œโ”€โ”€ Button/ # ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ + โ”‚ โ”œโ”€โ”€ Modal/ # ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ + โ”‚ โ””โ”€โ”€ ... + โ”œโ”€โ”€ hooks/ # ์ปค์Šคํ…€ ํ›… + โ”‚ โ”œโ”€โ”€ useNotificationScheduler.ts # ์•Œ๋ฆผ ์Šค์ผ€์ค„๋ง + โ”‚ โ”œโ”€โ”€ useAutoMetricsSender.ts # ์ž๋™ ๋ฉ”ํŠธ๋ฆญ ์ „์†ก + โ”‚ โ””โ”€โ”€ useWidget.ts # ์œ„์ ฏ ์ƒํƒœ ๊ด€๋ฆฌ + โ”œโ”€โ”€ layout/ # ํŽ˜์ด์ง€ ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ + โ”œโ”€โ”€ pages/ # ๋ผ์šฐํŒ… ๋‹จ์œ„ ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ + โ”‚ โ”œโ”€โ”€ Main/ # ๋ฉ”์ธ ๋Œ€์‹œ๋ณด๋“œ ํŽ˜์ด์ง€ + โ”‚ โ”œโ”€โ”€ Widget/ # ์œ„์ ฏ ํŽ˜์ด์ง€ + โ”‚ โ”œโ”€โ”€ Calibration/ # ์บ˜๋ฆฌ๋ธŒ๋ ˆ์ด์…˜ ํŽ˜์ด์ง€ + โ”‚ โ””โ”€โ”€ ... + โ”œโ”€โ”€ store/ # Zustand ์ „์—ญ ์Šคํ† ์–ด + โ”‚ โ”œโ”€โ”€ usePostureStore.ts # ์ž์„ธ ์ƒํƒœ ๊ด€๋ฆฌ + โ”‚ โ”œโ”€โ”€ useCameraStore.ts # ์นด๋ฉ”๋ผ ์ƒํƒœ ๊ด€๋ฆฌ + โ”‚ โ””โ”€โ”€ useNotificationStore.ts # ์•Œ๋ฆผ ์„ค์ • ๊ด€๋ฆฌ + โ”œโ”€โ”€ styles/ # ์ „์—ญ ์Šคํƒ€์ผ ๋ฐ Tailwind ์„ค์ • + โ”œโ”€โ”€ types/ # ๊ณต์šฉ ํƒ€์ž… ์ •์˜ + โ””โ”€โ”€ utils/ # ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ +``` + +- **์ปดํฌ๋„ŒํŠธ ์„ค๊ณ„**: ํŽ˜์ด์ง€(Pages)๊ฐ€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ๋ฐ์ดํ„ฐ ํŽ˜์นญ์„ ๋‹ด๋‹นํ•˜๊ณ , ์ปดํฌ๋„ŒํŠธ(Components)๋Š” UI ํ‘œํ˜„์— ์ง‘์ค‘ํ•˜๋Š” ๊ตฌ์กฐ๋ฅผ ์ง€ํ–ฅํ•ฉ๋‹ˆ๋‹ค. `pose-detection` ํด๋”์—๋Š” ์ž์„ธ ์ธ์‹ ๊ด€๋ จ ํ•ต์‹ฌ ๋กœ์ง์ด ํด๋ž˜์Šค ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. +- **๋””์ž์ธ ์‹œ์Šคํ…œ**: `Button`, `Modal`, `TextField`, `ToggleSwitch` ๋“ฑ ํ•ต์‹ฌ UI๋ฅผ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ๋กœ ๋งŒ๋“ค์–ด ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•˜๊ณ  ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค. `clsx`์™€ `tailwind-merge`๋ฅผ ํ™œ์šฉํ•ด ์กฐ๊ฑด๋ถ€ ์Šคํƒ€์ผ๋ง์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. +- **์ƒํƒœ ๊ด€๋ฆฌ ์ „๋žต**: + - `Zustand`๋กœ ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ(์ž์„ธ, ์นด๋ฉ”๋ผ, ์•Œ๋ฆผ ์„ค์ •) ๊ด€๋ฆฌ + - `TanStack Query`๋กœ ์„œ๋ฒ„ ์ƒํƒœ ์บ์‹ฑ ๋ฐ ๋™๊ธฐํ™” + - ์œ„์ ฏ๊ณผ ๋ฉ”์ธ ์œˆ๋„์šฐ ๊ฐ„ ๋™๊ธฐํ™”๋Š” `localStorage`์˜ `storage` ์ด๋ฒคํŠธ ํ™œ์šฉ -### UI ํŒจํ‚ค์ง€ ํ…Œ์ŠคํŠธ - -```typescript -// packages/ui/src/components/NewComponent/__tests__/NewComponent.test.tsx -import { render, screen } from '@testing-library/react'; -import { NewComponent } from '../NewComponent'; - -describe('NewComponent', () => { - it('renders with title', () => { - render(); - expect(screen.getByText('Test Title')).toBeInTheDocument(); - }); - - it('renders with description', () => { - render( - - ); - expect(screen.getByText('Test Description')).toBeInTheDocument(); - }); -}); -``` +--- -## ๐Ÿ“ ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ +## ๐Ÿš€ ์‹คํ–‰ ๋ฐฉ๋ฒ• -### 1. ๊ฐœ๋ฐœ ์‹œ์ž‘ +`.env.example` ํŒŒ์ผ์„ ์ฐธ๊ณ ํ•˜์—ฌ ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— `.env` ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”. ```bash -# ํ”„๋กœ์ ํŠธ ์˜์กด์„ฑ ์„ค์น˜ -npm install - -# Electron ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹œ์ž‘ -npm run dev +# .env +VITE_BASE_URL=http://localhost:8080 ``` -### 2. ์ƒˆ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ - -1. **API ํŒจํ‚ค์ง€**: ํƒ€์ž… ์ •์˜ โ†’ API ํด๋ผ์ด์–ธํŠธ โ†’ React Query ํ›… โ†’ ์ต์ŠคํฌํŠธ -2. **UI ํŒจํ‚ค์ง€**: ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„ โ†’ Storybook ์Šคํ† ๋ฆฌ โ†’ ์ต์ŠคํฌํŠธ โ†’ ํ…Œ์ŠคํŠธ - -### 3. ์ฝ”๋“œ ํ’ˆ์งˆ ๊ด€๋ฆฌ - ```bash -# ํƒ€์ž… ์ฒดํฌ -npm run typecheck +# 1. ์˜์กด์„ฑ ์„ค์น˜ +pnpm install -# ๋ฆฐํŒ… -npm run lint +# 2. ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰ (๋ฉ”์ธ + ๋ Œ๋”๋Ÿฌ ๋™์‹œ ์‹คํ–‰) +pnpm dev -# ํฌ๋งทํŒ… -npm run format +# 3. ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ +pnpm build -# ํ…Œ์ŠคํŠธ -npm run test +# 4. ํ”„๋กœ๋•์…˜ ๋ชจ๋“œ๋กœ ์‹คํ–‰ +pnpm start:prod + +# 5. ํ”Œ๋žซํผ๋ณ„ ๋นŒ๋“œ +pnpm build:mac # macOS ๋นŒ๋“œ +pnpm build:win # Windows ๋นŒ๋“œ +pnpm build:all # ๋ชจ๋“  ํ”Œ๋žซํผ ๋นŒ๋“œ ``` -## ๐Ÿ”„ ํŒจํ‚ค์ง€ ๊ฐ„ ์˜์กด์„ฑ +## ๐Ÿงช ํ…Œ์ŠคํŠธ & ํ’ˆ์งˆ -### API ํŒจํ‚ค์ง€ ์‚ฌ์šฉ๋ฒ• +- **Lint & Format**: + - `pnpm lint`: ESLint๋ฅผ ํ†ตํ•ด ์ฝ”๋“œ ์ปจ๋ฒค์…˜์„ ๊ฒ€์‚ฌํ•˜๊ณ  ์ž๋™ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค. + - `pnpm lint:check`: ESLint ๊ฒ€์‚ฌ๋งŒ ์ˆ˜ํ–‰ (์ˆ˜์ • ์—†์Œ) + - `pnpm format`: Prettier๋ฅผ ํ†ตํ•ด ์ฝ”๋“œ ์Šคํƒ€์ผ์„ ํ†ต์ผํ•ฉ๋‹ˆ๋‹ค. + - `pnpm format:check`: Prettier ๊ฒ€์‚ฌ๋งŒ ์ˆ˜ํ–‰ (์ˆ˜์ • ์—†์Œ) + - `pnpm typecheck`: TypeScript ์ปดํŒŒ์ผ๋Ÿฌ๋กœ ํƒ€์ž… ์˜ค๋ฅ˜๋ฅผ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค (main, preload, renderer ๋ชจ๋‘) +- **Git Hooks**: `husky`์™€ `lint-staged`๋ฅผ ํ†ตํ•ด ์ปค๋ฐ‹ ์ „ ์ž๋™์œผ๋กœ lint์™€ format์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. +- **Unit/E2E Test**: ํ˜„์žฌ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ์—†์œผ๋‚˜, ํ–ฅํ›„ `Vitest`์™€ `Testing Library`๋ฅผ ๋„์ž…ํ•˜์—ฌ ํ›…, ์œ ํ‹ธ ๋ฐ ์ฃผ์š” ์ปดํฌ๋„ŒํŠธ์˜ ์•ˆ์ •์„ฑ์„ ๋†’์ผ ๊ณ„ํš์ž…๋‹ˆ๋‹ค. -```typescript -// src/renderer/src/App.tsx -import { useHealth, useVersion, createAPIClient } from 'api'; +--- -function App() { - const { data: health } = useHealth(); - const { data: version } = useVersion(); +## ๐ŸŽจ UI/UX & ์ ‘๊ทผ์„ฑ - return ( -
-

Health: {health?.status}

-

Version: {version?.version}

-
- ); -} -``` +- **๋””์ž์ธ ์›์น™**: + - **๋ช…ํ™•ํ•œ ํ”ผ๋“œ๋ฐฑ**: ์‚ฌ์šฉ์ž์˜ ์ž์„ธ ์ƒํƒœ(๊ฑฐ๋ถ๋ชฉ/๊ธฐ๋ฆฐ)๋ฅผ ์บ๋ฆญํ„ฐ ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ์ƒ‰์ƒ์œผ๋กœ ์ง๊ด€์ ์œผ๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. ์‹ค์‹œ๊ฐ„ ์ ์ˆ˜์™€ ๋ ˆ๋ฒจ์„ ํ†ตํ•ด ํ˜„์žฌ ์ž์„ธ๋ฅผ ์ฆ‰์‹œ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + - **์ตœ์†Œํ•œ์˜ ๋ฐฉํ•ด**: ์œ„์ ฏ ๋ชจ๋“œ๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž์˜ ์ž‘์—… ํ๋ฆ„์„ ๋ฐฉํ•ดํ•˜์ง€ ์•Š์œผ๋ฉด์„œ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์„ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค. ์œ„์ ฏ์€ ํ•ญ์ƒ ์œ„์— ํ‘œ์‹œ๋˜๋ฉฐ ํฌ๊ธฐ ์กฐ์ ˆ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. + - **๋‹คํฌ ๋ชจ๋“œ ์ง€์›**: ์‹œ์Šคํ…œ ํ…Œ๋งˆ๋ฅผ ๊ฐ์ง€ํ•˜์—ฌ ์ž๋™์œผ๋กœ ๋‹คํฌ/๋ผ์ดํŠธ ๋ชจ๋“œ๋ฅผ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค. +- **๋””์ž์ธ ์‹œ์Šคํ…œ**: + - **Component**: `Button`, `TextField`, `Modal`, `ToggleSwitch`, `Typography` ๋“ฑ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌ์ถ•ํ–ˆ์Šต๋‹ˆ๋‹ค. + - **Tokens**: `styles/colors.css`, `styles/typography.css`, `styles/breakpoint.css` ๋“ฑ์—์„œ ์ƒ‰์ƒ, ํฐํŠธ, ๊ฐ„๊ฒฉ, ๋ธŒ๋ ˆ์ดํฌํฌ์ธํŠธ ์‹œ์Šคํ…œ์„ ์ •์˜ํ•˜์—ฌ ์ผ๊ด€๋œ ๋””์ž์ธ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. + - **์• ๋‹ˆ๋ฉ”์ด์…˜**: `framer-motion`์„ ํ™œ์šฉํ•˜์—ฌ ๋ถ€๋“œ๋Ÿฌ์šด ์ „ํ™˜ ํšจ๊ณผ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. +- **์ ‘๊ทผ์„ฑ**: ํ–ฅํ›„ ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜, `aria-*` ์†์„ฑ ์ ์šฉ, ์ƒ‰์ƒ ๋Œ€๋น„ ์ค€์ˆ˜ ๋“ฑ ์›น ์ ‘๊ทผ์„ฑ ํ‘œ์ค€์„ ๊ณ ๋ คํ•˜์—ฌ ๊ฐœ์„ ํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค. -### UI ํŒจํ‚ค์ง€ ์‚ฌ์šฉ๋ฒ• - -```typescript -// src/renderer/src/App.tsx -import { Button, Header, Page, NewComponent } from 'ui'; -import 'ui/globals.css'; - -function App() { - return ( - -
- - - - ); -} -``` +--- -## ๐Ÿš€ ๋ฐฐํฌ ๋ฐ ๋นŒ๋“œ +## ๐Ÿ“Œ ๊ฐœ๋ฐœ ์˜๋„ & ๊ธฐ์ˆ ์  ๋„์ „ + +- **๊ธฐ์ˆ ์  ๋„์ „ ๊ณผ์ œ**: + 1. **์‹ค์‹œ๊ฐ„ ์˜์ƒ ์ฒ˜๋ฆฌ ์„ฑ๋Šฅ**: ์ €์‚ฌ์–‘ ์ปดํ“จํ„ฐ์—์„œ๋„ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์ž‘๋™ํ•˜๋„๋ก ์›น์บ  ์˜์ƒ ๋ถ„์„ ๊ณผ์ •์˜ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ–ˆ์Šต๋‹ˆ๋‹ค. + 2. **๋‹ค์ค‘ ์œˆ๋„์šฐ ์ƒํƒœ ๊ด€๋ฆฌ**: Electron์˜ ๋ฉ”์ธ ์œˆ๋„์šฐ์™€ ์œ„์ ฏ ์œˆ๋„์šฐ ๊ฐ„์˜ ์ƒํƒœ(์„ธ์…˜, ์‹œ๊ฐ„ ๋“ฑ)๋ฅผ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ๋™๊ธฐํ™”ํ•˜๋Š” ๊ฒƒ์ด ๋ณต์žกํ–ˆ์Šต๋‹ˆ๋‹ค. + 3. **ํฌ๋กœ์Šค ํ”Œ๋žซํผ ํ˜ธํ™˜์„ฑ**: macOS์™€ Windows ํ™˜๊ฒฝ ๋ชจ๋‘์—์„œ ์•ˆ์ •์ ์œผ๋กœ ๋นŒ๋“œ๋˜๊ณ  ์‹คํ–‰๋˜๋„๋ก `electron-builder` ์„ค์ •์„ ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. + +- **ํ•ด๊ฒฐ ๊ณผ์ •**: + 1. **์„ฑ๋Šฅ ์ตœ์ ํ™”**: + - MediaPipe์˜ `detectForVideo`๋ฅผ ํ”„๋ ˆ์ž„๋ณ„๋กœ ํ˜ธ์ถœํ•˜์—ฌ ์ค‘๋ณต ์ฒ˜๋ฆฌ ๋ฐฉ์ง€ + - `PostureStabilizer`๋ฅผ ํ†ตํ•ด ์ž์„ธ ํŒ์ •์˜ ์•ˆ์ •์„ฑ ํ™•๋ณด (300ms ์œˆ๋„์šฐ, 0.6 ์ž„๊ณ„๊ฐ’) + - `ScoreProcessor`์˜ EMA ์Šค๋ฌด๋”ฉ์œผ๋กœ ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ + - ๋ ˆ๋ฒจ ๋ณ€๊ฒฝ ์‹œ ์—…๋ฐ์ดํŠธ ์ฃผ๊ธฐ ์กฐ์ ˆ (๊ฒฝ๊ณ„ ์ „ํ™˜: 50ms, ๋ ˆ๋ฒจ ๋ณ€๊ฒฝ: 150ms, ์ผ๋ฐ˜: 200ms) + 2. **๋‹ค์ค‘ ์œˆ๋„์šฐ ๋™๊ธฐํ™”**: + - ๋ฉ”์ธ ํ”„๋กœ์„ธ์Šค๋ฅผ ์ค‘์•™ ํ—ˆ๋ธŒ๋กœ ์‚ผ์•„ `ipcMain`๊ณผ `ipcRenderer`๋ฅผ ํ†ตํ•ด ์œ„์ ฏ ์ฐฝ ์—ด๊ธฐ/๋‹ซ๊ธฐ ์ œ์–ด + - ์œ„์ ฏ๊ณผ ๋ฉ”์ธ ์œˆ๋„์šฐ ๊ฐ„ ์ž์„ธ ์ƒํƒœ ๋™๊ธฐํ™”๋Š” `localStorage`์˜ `storage` ์ด๋ฒคํŠธ ํ™œ์šฉ (IPC ๋Œ€์‹  ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ„๋‹จํ•˜๊ณ  ์•ˆ์ •์ ์œผ๋กœ ๊ตฌํ˜„) + - `usePostureSyncWithLocalStorage` ํ›…์œผ๋กœ ์œ„์ ฏ์—์„œ ๋ฉ”์ธ ์ฐฝ์˜ ์ƒํƒœ ๋ณ€๊ฒฝ ๊ฐ์ง€ + 3. **ํฌ๋กœ์Šค ํ”Œ๋žซํผ ํ˜ธํ™˜์„ฑ**: + - GitHub Actions๋ฅผ ์ด์šฉํ•ด ๊ฐ๊ธฐ ๋‹ค๋ฅธ OS ํ™˜๊ฒฝ์˜ ๋นŒ๋“œ๋ฅผ ์ž๋™ํ™” + - `entitlements.mac.plist`์— ์นด๋ฉ”๋ผ/๋งˆ์ดํฌ ๊ถŒํ•œ ๋ช…์‹œ + - `electron-builder` ์„ค์ •์œผ๋กœ macOS (Intel/Apple Silicon) ๋ฐ Windows ๋นŒ๋“œ ์ง€์› -### ํŒจํ‚ค์ง€ ๋นŒ๋“œ +--- -```bash -# ์ „์ฒด ํ”„๋กœ์ ํŠธ ๋นŒ๋“œ -npm run build +## ๐Ÿ‘ฅ ํŒ€ ๊ตฌ์„ฑ & ์—ญํ•  -# ํŠน์ • ํŒจํ‚ค์ง€๋งŒ ๋นŒ๋“œ -cd packages/api && npm run typecheck -cd packages/ui && npm run build-storybook -``` +- **Frontend**: + - ์ตœํ˜ธ ([@choihooo](https://github.com/choihooo)): ํ”„๋ก ํŠธ์—”๋“œ ๋ฆฌ๋“œ, ํŽ˜์ด์ง€ ์„ค๊ณ„, ์ƒํƒœ ๊ด€๋ฆฌ, ๋””์ž์ธ ์‹œ์Šคํ…œ + - ์ดํ™˜์„ ([@hwanseok1014](https://github.com/hwanseok1014)): ํ”„๋ก ํŠธ์—”๋“œ, API ์—ฐ๋™, ๋ฐ์ดํ„ฐ ์‹œ๊ฐํ™”, Electron ๊ธฐ๋Šฅ ๊ตฌํ˜„ +- **Backend**: + - ์†๋Œ€ํ˜„ ([@son-daehyeon](https://github.com/son-daehyeon)): ๋ฐฑ์—”๋“œ ๋ฆฌ๋“œ, API ์„ค๊ณ„, ์ธ์ฆ, ๋ฐฐํฌ -### Electron ์•ฑ ๋นŒ๋“œ +--- -```bash -npm run build -``` +## ๐Ÿ“š ํ–ฅํ›„ ๊ฐœ์„  ๊ณ„ํš + +- [ ] ๋ฐ˜์‘ํ˜• ๋ ˆ์ด์•„์›ƒ ๋” ์„ธ๋ถ„ํ™” (ํƒœ๋ธ”๋ฆฟ ์ตœ์ ํ™”) +- [ ] Skeleton UI / Loading ์ƒํƒœ ๊ณ ๋„ํ™” +- [ ] ์ถ”๊ฐ€ ์ ‘๊ทผ์„ฑ ๊ฒ€์‚ฌ ๋ฐ ๊ฐœ์„  (WAI-ARIA ํ‘œ์ค€ ์ ์šฉ) +- [ ] ๋‹ค๊ตญ์–ด(i18n) ์ง€์› +- [ ] Vitest๋ฅผ ์ด์šฉํ•œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ +- [ ] ์ž์„ธ ์ธ์‹ ์ •ํ™•๋„ ํ–ฅ์ƒ์„ ์œ„ํ•œ ์ถ”๊ฐ€ ์บ˜๋ฆฌ๋ธŒ๋ ˆ์ด์…˜ ์˜ต์…˜ +- [ ] ์˜คํ”„๋ผ์ธ ๋ชจ๋“œ ์ง€์› (๋กœ์ปฌ ์ €์žฅ ํ›„ ๋™๊ธฐํ™”) +- [ ] ์‚ฌ์šฉ์ž ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ์˜ต์…˜ ํ™•๋Œ€ (์•Œ๋ฆผ ์„ค์ •, ํ…Œ๋งˆ ๋“ฑ) From 8099538057105a898e3a25bd7a732eee08366842 Mon Sep 17 00:00:00 2001 From: choihooo Date: Wed, 26 Nov 2025 21:17:56 +0900 Subject: [PATCH 3/7] =?UTF-8?q?fix(auth):=20=EC=97=90=EB=9F=AC=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/api/api.ts | 113 ++++++++++++++++++++++++++---------- 1 file changed, 83 insertions(+), 30 deletions(-) diff --git a/src/renderer/src/api/api.ts b/src/renderer/src/api/api.ts index 5db3a9a..3d805a0 100644 --- a/src/renderer/src/api/api.ts +++ b/src/renderer/src/api/api.ts @@ -19,6 +19,51 @@ const api: AxiosInstance = axios.create({ }, }); +/** + * ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์œผ๋กœ ์•ก์„ธ์Šค ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ + */ +const refreshAccessToken = async (): Promise => { + const refreshToken = localStorage.getItem('refreshToken'); + if (!refreshToken) { + throw new Error('Refresh token not found'); + } + + try { + const { data: newToken } = await axios.post( + `${import.meta.env.VITE_BASE_URL}/auth/refresh`, + { refreshToken }, + { withCredentials: true }, + ); + + // AUTH-102 ์ฝ”๋“œ์ธ ๊ฒฝ์šฐ ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ + if (newToken.code?.toUpperCase() === 'AUTH-102') { + throw new Error('Invalid refresh token'); + } + + if (!newToken.success || !newToken.data) { + throw new Error('Refresh token expired'); + } + + // ์ƒˆ๋กœ ๋ฐœ๊ธ‰ ๋ฐ›์€ ํ† ํฐ ์ €์žฅ + const newAccessToken = newToken.data.accessToken; + const newRefreshToken = newToken.data.refreshToken; + + localStorage.setItem('accessToken', newAccessToken); + localStorage.setItem('refreshToken', newRefreshToken); + + api.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`; + } catch (error) { + // AUTH-102 ์—๋Ÿฌ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ฒ˜๋ฆฌ + if (axios.isAxiosError(error) && error.response?.data) { + const errorData = error.response.data as { code?: string }; + if (errorData.code?.toUpperCase() === 'AUTH-102') { + throw new Error('Invalid refresh token'); + } + } + throw error; + } +}; + api.interceptors.request.use( (config) => { const accessToken = localStorage.getItem('accessToken'); @@ -33,21 +78,42 @@ api.interceptors.request.use( ); api.interceptors.response.use( - (response) => { - // 200 OK ์‘๋‹ต์ด์ง€๋งŒ ์„ธ์…˜ ๋งŒ๋ฃŒ ์ฝ”๋“œ์ธ ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ + async (response) => { + // 200 OK ์‘๋‹ต์ด์ง€๋งŒ ํ† ํฐ ๊ด€๋ จ ์—๋Ÿฌ ์ฝ”๋“œ์ธ ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ const responseData = response.data as { code?: string; success?: boolean; message?: string; }; - // AUTH-101 ์ฝ”๋“œ์ธ ๊ฒฝ์šฐ ์„ธ์…˜ ๋งŒ๋ฃŒ๋กœ ์ฒ˜๋ฆฌ (๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„ ์—†์Œ) - if (responseData.code?.toUpperCase() === 'AUTH-101') { - // ์„ธ์…˜ ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ + const errorCode = responseData.code?.toUpperCase(); + + // AUTH-102 ์ฝ”๋“œ์ธ ๊ฒฝ์šฐ ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ โ†’ ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ + if (errorCode === 'AUTH-102') { localStorage.clear(); window.location.href = '/'; - // ์—๋Ÿฌ๋กœ ์ฒ˜๋ฆฌํ•˜์—ฌ ์ดํ›„ ๋กœ์ง ์‹คํ–‰ ๋ฐฉ์ง€ - throw new Error(responseData.message || 'Session expired'); + throw new Error(responseData.message || 'Invalid refresh token'); + } + + // AUTH-101 ์ฝ”๋“œ์ธ ๊ฒฝ์šฐ ํ† ํฐ ๋งŒ๋ฃŒ๋กœ ์ฒ˜๋ฆฌ (๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„ ์—†์Œ) + if (errorCode === 'AUTH-101') { + try { + // ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์œผ๋กœ ์žฌ๋ฐœ๊ธ‰ ์‹œ๋„ + await refreshAccessToken(); + + // ์žฌ๋ฐœ๊ธ‰ ์„ฑ๊ณต ์‹œ ์›๋ž˜ ์š”์ฒญ์„ ์ƒˆ ํ† ํฐ์œผ๋กœ ์žฌ์‹œ๋„ + const originalRequest = response.config; + const newAccessToken = localStorage.getItem('accessToken'); + if (newAccessToken && originalRequest.headers) { + originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`; + } + return api(originalRequest); + } catch (refreshError) { + // ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ๋„ ๋งŒ๋ฃŒ๋˜๊ฑฐ๋‚˜ ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ + localStorage.clear(); + window.location.href = '/'; + throw new Error(responseData.message || 'Session expired'); + } } return response; @@ -64,30 +130,17 @@ api.interceptors.response.use( ) { originalRequest._retry = true; try { - const refreshToken = localStorage.getItem('refreshToken'); + await refreshAccessToken(); - const { data: newToken } = await axios.post( - `${import.meta.env.VITE_BASE_URL}/auth/refresh`, - { refreshToken }, - { withCredentials: true }, - ); - - /* success๊ฐ€ false์ด๊ฑฐ๋‚˜ ์‘๋‹ต์œผ๋กœ ์˜จ ๋ฐ์ดํ„ฐ๊ฐ€ ๋น„์—ˆ์„ ๋•Œ */ - if (!newToken.success || !newToken.data) { - throw new Error('Refresh token expired'); - } - - /* ์ƒˆ๋กœ ๋ฐœ๊ธ‰ ๋ฐ›์€ ํ† ํฐ ์ €์žฅ */ - const newAccessToken = newToken.data.accessToken; - const newRefreshToken = newToken.data.refreshToken; - - localStorage.setItem('accessToken', newAccessToken); - localStorage.setItem('refreshToken', newRefreshToken); - - api.defaults.headers.common['Authorization'] = - `Bearer ${newAccessToken}`; - if (originalRequest.headers) { - originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`; + // ์žฌ๋ฐœ๊ธ‰ ์„ฑ๊ณต ์‹œ ์›๋ž˜ ์š”์ฒญ์„ ์ƒˆ ํ† ํฐ์œผ๋กœ ์žฌ์‹œ๋„ + const newAccessToken = localStorage.getItem('accessToken'); + if (newAccessToken) { + api.defaults.headers.common['Authorization'] = + `Bearer ${newAccessToken}`; + if (originalRequest.headers) { + originalRequest.headers['Authorization'] = + `Bearer ${newAccessToken}`; + } } return api(originalRequest); From abaf5b1b1774241e6ff5719e3a59a55b43c528cf Mon Sep 17 00:00:00 2001 From: choihooo Date: Thu, 27 Nov 2025 00:27:14 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat(main):=20=EB=A0=88=EB=B2=A8=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/assets/angel-rini-modal.svg | 287 ++++++++++++++++ src/renderer/src/assets/bugi-modal.svg | 170 +++++++++ src/renderer/src/assets/pm-rini-modal.svg | 322 ++++++++++++++++++ src/renderer/src/assets/rini-modal.svg | 264 ++++++++++++++ src/renderer/src/assets/stone-bugi-modal.svg | 213 ++++++++++++ src/renderer/src/assets/tire-bugi-modal.svg | 194 +++++++++++ .../Main/components/CharacterSpeedRow.tsx | 61 ++++ .../Main/components/TotalDistanceModal.tsx | 55 +++ .../Main/components/TotalDistancePanel.tsx | 78 +++-- 9 files changed, 1612 insertions(+), 32 deletions(-) create mode 100644 src/renderer/src/assets/angel-rini-modal.svg create mode 100644 src/renderer/src/assets/bugi-modal.svg create mode 100644 src/renderer/src/assets/pm-rini-modal.svg create mode 100644 src/renderer/src/assets/rini-modal.svg create mode 100644 src/renderer/src/assets/stone-bugi-modal.svg create mode 100644 src/renderer/src/assets/tire-bugi-modal.svg create mode 100644 src/renderer/src/pages/Main/components/CharacterSpeedRow.tsx create mode 100644 src/renderer/src/pages/Main/components/TotalDistanceModal.tsx diff --git a/src/renderer/src/assets/angel-rini-modal.svg b/src/renderer/src/assets/angel-rini-modal.svg new file mode 100644 index 0000000..94fc498 --- /dev/null +++ b/src/renderer/src/assets/angel-rini-modal.svg @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/renderer/src/assets/bugi-modal.svg b/src/renderer/src/assets/bugi-modal.svg new file mode 100644 index 0000000..efe533d --- /dev/null +++ b/src/renderer/src/assets/bugi-modal.svg @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/renderer/src/assets/pm-rini-modal.svg b/src/renderer/src/assets/pm-rini-modal.svg new file mode 100644 index 0000000..8660c38 --- /dev/null +++ b/src/renderer/src/assets/pm-rini-modal.svg @@ -0,0 +1,322 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/renderer/src/assets/rini-modal.svg b/src/renderer/src/assets/rini-modal.svg new file mode 100644 index 0000000..1b91269 --- /dev/null +++ b/src/renderer/src/assets/rini-modal.svg @@ -0,0 +1,264 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/renderer/src/assets/stone-bugi-modal.svg b/src/renderer/src/assets/stone-bugi-modal.svg new file mode 100644 index 0000000..deeaf8e --- /dev/null +++ b/src/renderer/src/assets/stone-bugi-modal.svg @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/renderer/src/assets/tire-bugi-modal.svg b/src/renderer/src/assets/tire-bugi-modal.svg new file mode 100644 index 0000000..c01a8a3 --- /dev/null +++ b/src/renderer/src/assets/tire-bugi-modal.svg @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/renderer/src/pages/Main/components/CharacterSpeedRow.tsx b/src/renderer/src/pages/Main/components/CharacterSpeedRow.tsx new file mode 100644 index 0000000..2f23e95 --- /dev/null +++ b/src/renderer/src/pages/Main/components/CharacterSpeedRow.tsx @@ -0,0 +1,61 @@ +import AngelRiniModal from '@assets/angel-rini-modal.svg?react'; +import BugiModal from '@assets/bugi-modal.svg?react'; +import PmRiniModal from '@assets/pm-rini-modal.svg?react'; +import RiniModal from '@assets/rini-modal.svg?react'; +import StoneBugiModal from '@assets/stone-bugi-modal.svg?react'; +import TireBugiModal from '@assets/tire-bugi-modal.svg?react'; + +interface CharacterSpeedRowProps { + level: number; + name: string; + speed: string; +} + +const CHARACTER_COMPONENTS: Record>> = { + 1: TireBugiModal, + 2: StoneBugiModal, + 3: BugiModal, + 4: RiniModal, + 5: PmRiniModal, + 6: AngelRiniModal, +}; + +const CHARACTER_NAMES: Record = { + 1: 'ํƒ€์ด์–ด ๋งจ ๊ฑฐ๋ถ€๊ธฐ', + 2: '๋Œ๋ฉ์ด ๊ฑฐ๋ถ€๊ธฐ', + 3: '๊ฑฐ๋ถ€๊ธฐ', + 4: '๊ธฐ๋ฆฐ', + 5: '์”ฝ์”ฝ์ด ๊ธฐ๋ฆฐ', + 6: '์ฒœ์‚ฌ๊ธฐ๋ฆฐ', +}; + +const CharacterSpeedRow = ({ level, name, speed }: CharacterSpeedRowProps) => { + const CharacterComponent = CHARACTER_COMPONENTS[level]; + + return ( +
+
+
+ {level} +
+ {name} +
+
+ {CharacterComponent && } +
+
{speed}
+
+ ); +}; + +export const CHARACTER_SPEED_DATA: CharacterSpeedRowProps[] = [ + { level: 1, name: CHARACTER_NAMES[1], speed: '0.1m/h' }, + { level: 2, name: CHARACTER_NAMES[2], speed: '50m/h' }, + { level: 3, name: CHARACTER_NAMES[3], speed: '200m/h' }, + { level: 4, name: CHARACTER_NAMES[4], speed: '500m/h' }, + { level: 5, name: CHARACTER_NAMES[5], speed: '1.5km/h' }, + { level: 6, name: CHARACTER_NAMES[6], speed: ' 3km/h' }, +]; + +export default CharacterSpeedRow; + diff --git a/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx b/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx new file mode 100644 index 0000000..bf1c4c6 --- /dev/null +++ b/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx @@ -0,0 +1,55 @@ +import { Button } from '@ui/Button/Button'; +import { ModalPortal } from '@ui/Modal/ModalPortal'; +import CharacterSpeedRow, { CHARACTER_SPEED_DATA } from './CharacterSpeedRow'; + +interface TotalDistanceModalProps { + onClose: () => void; +} + +const TotalDistanceModal = ({ onClose }: TotalDistanceModalProps) => { + return ( + +
+
e.stopPropagation()} + > +
+

์บ๋ฆญํ„ฐ๋ณ„ ์†๋„๋ฅผ ์†Œ๊ฐœํ•ด์š”

+
+
+
์ž์„ธ ์ƒํƒœ
+
์บ๋ฆญํ„ฐ
+
์‹œ๊ฐ„๋‹น ์†๋„
+
+
+
+ {CHARACTER_SPEED_DATA.map((character) => ( + + ))} +
+
+
+
+
+ + ); +}; + +export default TotalDistanceModal; + diff --git a/src/renderer/src/pages/Main/components/TotalDistancePanel.tsx b/src/renderer/src/pages/Main/components/TotalDistancePanel.tsx index 9aaa400..dadf6c1 100644 --- a/src/renderer/src/pages/Main/components/TotalDistancePanel.tsx +++ b/src/renderer/src/pages/Main/components/TotalDistancePanel.tsx @@ -1,9 +1,12 @@ import { PannelHeader } from '@ui/PannelHeader/PannelHeader'; -import AchivementMedal from '../../../assets/main/achivement_meadl.svg?react'; import { useLevelQuery } from '../../../api/dashboard/useLevelQuery'; +import AchivementMedal from '../../../assets/main/achivement_meadl.svg?react'; +import { useModal } from '../../../hooks/useModal'; +import TotalDistanceModal from './TotalDistanceModal'; const TotalDistance = () => { const { data, isLoading } = useLevelQuery(); + const { isOpen, open, close } = useModal(); const level = data?.data.level ?? 1; const current = data?.data.current ?? 0; @@ -11,39 +14,50 @@ const TotalDistance = () => { const progressPercentage = (current / required) * 100; return ( -
- - {isLoading ? '๋กœ๋”ฉ ์ค‘...' : `Level.${level + 1} `} - -

- - {isLoading ? '-' : current.toLocaleString()} - - - / {isLoading ? '-' : required.toLocaleString()}m - -

- {/*๊ฒŒ์ด์ง€ ๋ฐ”*/} -
-
-
- {/*์ง„ํ–‰๋ฐ” */} -
-
+ <> +
+
+ + {isLoading ? '๋กœ๋”ฉ ์ค‘...' : `Level.${level + 1} `} + + +
+

+ + {isLoading ? '-' : current.toLocaleString()} + + + / {isLoading ? '-' : required.toLocaleString()}m + +

+ {/*๊ฒŒ์ด์ง€ ๋ฐ”*/} +
+
+
+ {/*์ง„ํ–‰๋ฐ” */} +
+
+
+ +
+
+ {Array.from({ length: 4 }, (_, i) => i * (required / 3)).map( + (value) => ( + {Math.floor(value).toLocaleString()} + ), + )}
- -
-
- {Array.from({ length: 4 }, (_, i) => i * (required / 3)).map( - (value) => ( - {Math.floor(value).toLocaleString()} - ), - )}
-
+ {isOpen && } + ); }; From d0e91e590252b468ae4339dc063b14f241017c04 Mon Sep 17 00:00:00 2001 From: Lee hwan seuk Date: Thu, 27 Nov 2025 01:06:46 +0900 Subject: [PATCH 5/7] =?UTF-8?q?fix(main):=20=EB=A0=88=EB=B2=A8=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20top=EC=86=8D=EC=84=B1=2049=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Main/components/TotalDistanceModal.tsx | 87 ++++++++++--------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx b/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx index bf1c4c6..66e68ae 100644 --- a/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx +++ b/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx @@ -3,53 +3,54 @@ import { ModalPortal } from '@ui/Modal/ModalPortal'; import CharacterSpeedRow, { CHARACTER_SPEED_DATA } from './CharacterSpeedRow'; interface TotalDistanceModalProps { - onClose: () => void; + onClose: () => void; } const TotalDistanceModal = ({ onClose }: TotalDistanceModalProps) => { - return ( - -
-
e.stopPropagation()} - > -
-

์บ๋ฆญํ„ฐ๋ณ„ ์†๋„๋ฅผ ์†Œ๊ฐœํ•ด์š”

-
-
-
์ž์„ธ ์ƒํƒœ
-
์บ๋ฆญํ„ฐ
-
์‹œ๊ฐ„๋‹น ์†๋„
-
-
-
- {CHARACTER_SPEED_DATA.map((character) => ( - - ))} -
-
-
-
+ return ( + +
+
e.stopPropagation()} + > +
+

+ ์บ๋ฆญํ„ฐ๋ณ„ ์†๋„๋ฅผ ์†Œ๊ฐœํ•ด์š” +

+
+
+
์ž์„ธ ์ƒํƒœ
+
์บ๋ฆญํ„ฐ
+
์‹œ๊ฐ„๋‹น ์†๋„
+
+
+
+ {CHARACTER_SPEED_DATA.map((character) => ( + + ))} +
- - ); +
+
+
+ + ); }; export default TotalDistanceModal; - From e5b53ddde447a4b4cff7be8a5b0e503f82f653e7 Mon Sep 17 00:00:00 2001 From: choihooo Date: Thu, 27 Nov 2025 09:05:03 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat(main):=20=EB=A0=88=EB=B2=A8=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EB=B0=98=EC=9D=91=ED=98=95=202=EC=A4=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/Main/components/CharacterSpeedRow.tsx | 8 ++++---- .../src/pages/Main/components/TotalDistanceModal.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/renderer/src/pages/Main/components/CharacterSpeedRow.tsx b/src/renderer/src/pages/Main/components/CharacterSpeedRow.tsx index 2f23e95..7ea10ad 100644 --- a/src/renderer/src/pages/Main/components/CharacterSpeedRow.tsx +++ b/src/renderer/src/pages/Main/components/CharacterSpeedRow.tsx @@ -33,17 +33,17 @@ const CharacterSpeedRow = ({ level, name, speed }: CharacterSpeedRowProps) => { const CharacterComponent = CHARACTER_COMPONENTS[level]; return ( -
-
+
+
{level}
{name}
-
+
{CharacterComponent && }
-
{speed}
+
{speed}
); }; diff --git a/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx b/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx index bf1c4c6..4033ee6 100644 --- a/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx +++ b/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx @@ -17,16 +17,16 @@ const TotalDistanceModal = ({ onClose }: TotalDistanceModalProps) => { className="bg-surface-modal border-grey-0 fixed top-[45%] left-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col gap-4 rounded-[24px] border p-6" onClick={(e) => e.stopPropagation()} > -
+

์บ๋ฆญํ„ฐ๋ณ„ ์†๋„๋ฅผ ์†Œ๊ฐœํ•ด์š”

-
+
์ž์„ธ ์ƒํƒœ
์บ๋ฆญํ„ฐ
์‹œ๊ฐ„๋‹น ์†๋„
-
-
+
+
{CHARACTER_SPEED_DATA.map((character) => ( Date: Thu, 27 Nov 2025 09:21:26 +0900 Subject: [PATCH 7/7] =?UTF-8?q?feat(main):=20=EB=A0=88=EB=B2=A8=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EB=B0=98=EC=9D=91=ED=98=95=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/Main/components/CharacterSpeedRow.tsx | 14 +++++++++----- .../pages/Main/components/TotalDistanceModal.tsx | 16 ++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/renderer/src/pages/Main/components/CharacterSpeedRow.tsx b/src/renderer/src/pages/Main/components/CharacterSpeedRow.tsx index 7ea10ad..f4855d8 100644 --- a/src/renderer/src/pages/Main/components/CharacterSpeedRow.tsx +++ b/src/renderer/src/pages/Main/components/CharacterSpeedRow.tsx @@ -33,17 +33,21 @@ const CharacterSpeedRow = ({ level, name, speed }: CharacterSpeedRowProps) => { const CharacterComponent = CHARACTER_COMPONENTS[level]; return ( -
-
+
+
{level}
{name}
-
- {CharacterComponent && } +
+ {CharacterComponent && ( +
+ +
+ )}
-
{speed}
+
{speed}
); }; diff --git a/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx b/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx index 4033ee6..b61046e 100644 --- a/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx +++ b/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx @@ -10,23 +10,23 @@ const TotalDistanceModal = ({ onClose }: TotalDistanceModalProps) => { return (
e.stopPropagation()} > -
-

์บ๋ฆญํ„ฐ๋ณ„ ์†๋„๋ฅผ ์†Œ๊ฐœํ•ด์š”

+
+

์บ๋ฆญํ„ฐ๋ณ„ ์†๋„ ์†Œ๊ฐœ

-
+
์ž์„ธ ์ƒํƒœ
์บ๋ฆญํ„ฐ
์‹œ๊ฐ„๋‹น ์†๋„
-
-
+
+
{CHARACTER_SPEED_DATA.map((character) => ( { text="๋‹ซ๊ธฐ" variant="primary" size="md" - className="mt-2" + className="mt-2 max-[1439px]:mt-0" />