From 23fcc8bd26b266ac549bcd2cae6e53cc27189847 Mon Sep 17 00:00:00 2001 From: choihooo Date: Fri, 28 Nov 2025 11:12:02 +0900 Subject: [PATCH 01/12] =?UTF-8?q?refactor(shared):=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=9C=A0=ED=8B=B8=20=ED=95=A8=EC=88=98=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/components/Button/Button.tsx | 2 +- .../src/components/IntensitySlider/IntensitySlider.tsx | 2 +- .../src/components/PageMoveButton/PageMoveButton.tsx | 2 +- .../src/components/ThemeToggleSwitch/ThemeToggleSwitch.tsx | 2 +- .../src/components/ToggleSwitch/NotificationToggleSwitch.tsx | 2 +- src/renderer/src/components/ToggleSwitch/ToggleSwitch.tsx | 2 +- src/renderer/src/components/Typography/Typography.tsx | 2 +- .../src/components/pose-detection/PostureClassifier.ts | 2 +- .../components/AverageGraph/hooks/useAverageGraphChart.ts | 2 +- .../components/HighlightsPanel/hooks/useHighlightsChart.ts | 2 +- src/renderer/src/pages/Main/components/MainHeader.tsx | 2 +- src/renderer/src/pages/Main/components/RunningPanel.tsx | 4 ++-- src/renderer/src/{utils => shared/lib}/cn.ts | 0 .../src/{utils/getColor.ts => shared/lib/get-color.ts} | 0 .../getScoreLevel.ts => shared/lib/get-score-level.ts} | 3 --- src/renderer/src/shared/lib/index.ts | 5 +++++ src/renderer/tsconfig.json | 3 ++- vite.config.mts | 2 ++ 18 files changed, 22 insertions(+), 17 deletions(-) rename src/renderer/src/{utils => shared/lib}/cn.ts (100%) rename src/renderer/src/{utils/getColor.ts => shared/lib/get-color.ts} (100%) rename src/renderer/src/{utils/getScoreLevel.ts => shared/lib/get-score-level.ts} (99%) create mode 100644 src/renderer/src/shared/lib/index.ts diff --git a/src/renderer/src/components/Button/Button.tsx b/src/renderer/src/components/Button/Button.tsx index 2e1ea7a..9e5a47a 100644 --- a/src/renderer/src/components/Button/Button.tsx +++ b/src/renderer/src/components/Button/Button.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { cva, type VariantProps } from 'class-variance-authority'; -import { cn } from '../../utils/cn'; +import { cn } from '@shared/lib/cn'; const buttonVariants = cva( 'inline-flex items-center justify-center rounded-full transition-colors focus-visible:outline-none disabled:cursor-not-allowed active:scale-95', diff --git a/src/renderer/src/components/IntensitySlider/IntensitySlider.tsx b/src/renderer/src/components/IntensitySlider/IntensitySlider.tsx index 034286f..4b2256a 100644 --- a/src/renderer/src/components/IntensitySlider/IntensitySlider.tsx +++ b/src/renderer/src/components/IntensitySlider/IntensitySlider.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { cn } from '../../utils/cn'; +import { cn } from '@shared/lib/cn'; interface IntensitySliderProps { leftLabel?: string; diff --git a/src/renderer/src/components/PageMoveButton/PageMoveButton.tsx b/src/renderer/src/components/PageMoveButton/PageMoveButton.tsx index 0951a10..9178e3f 100644 --- a/src/renderer/src/components/PageMoveButton/PageMoveButton.tsx +++ b/src/renderer/src/components/PageMoveButton/PageMoveButton.tsx @@ -1,6 +1,6 @@ import PageMoveIcon from '@assets/page-move-button.svg?react'; import * as React from 'react'; -import { cn } from '../../utils/cn'; +import { cn } from '@shared/lib/cn'; interface PageMoveButtonProps { direction?: 'prev' | 'next'; diff --git a/src/renderer/src/components/ThemeToggleSwitch/ThemeToggleSwitch.tsx b/src/renderer/src/components/ThemeToggleSwitch/ThemeToggleSwitch.tsx index 9924445..374fb57 100644 --- a/src/renderer/src/components/ThemeToggleSwitch/ThemeToggleSwitch.tsx +++ b/src/renderer/src/components/ThemeToggleSwitch/ThemeToggleSwitch.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import MoonIcon from '../../assets/moon_icon.svg?react'; import SunIcon from '../../assets/sun_icon.svg?react'; -import { cn } from '../../utils/cn'; +import { cn } from '@shared/lib/cn'; interface ThemeToggleSwitchProps { checked: boolean; diff --git a/src/renderer/src/components/ToggleSwitch/NotificationToggleSwitch.tsx b/src/renderer/src/components/ToggleSwitch/NotificationToggleSwitch.tsx index c0b61bf..ac7796d 100644 --- a/src/renderer/src/components/ToggleSwitch/NotificationToggleSwitch.tsx +++ b/src/renderer/src/components/ToggleSwitch/NotificationToggleSwitch.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { cn } from '../../utils/cn'; +import { cn } from '@shared/lib/cn'; interface NotificationToggleSwitchProps { checked: boolean; diff --git a/src/renderer/src/components/ToggleSwitch/ToggleSwitch.tsx b/src/renderer/src/components/ToggleSwitch/ToggleSwitch.tsx index fb80a72..32db2d5 100644 --- a/src/renderer/src/components/ToggleSwitch/ToggleSwitch.tsx +++ b/src/renderer/src/components/ToggleSwitch/ToggleSwitch.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { cn } from '../../utils/cn'; +import { cn } from '@shared/lib/cn'; interface ToggleSwitchProps { checked: boolean; diff --git a/src/renderer/src/components/Typography/Typography.tsx b/src/renderer/src/components/Typography/Typography.tsx index 1ec53fc..a14f9be 100644 --- a/src/renderer/src/components/Typography/Typography.tsx +++ b/src/renderer/src/components/Typography/Typography.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { cn } from '../../utils/cn'; +import { cn } from '@shared/lib/cn'; interface TypographyProps { variant?: diff --git a/src/renderer/src/components/pose-detection/PostureClassifier.ts b/src/renderer/src/components/pose-detection/PostureClassifier.ts index 51c68e3..8186bfe 100644 --- a/src/renderer/src/components/pose-detection/PostureClassifier.ts +++ b/src/renderer/src/components/pose-detection/PostureClassifier.ts @@ -1,4 +1,4 @@ -import { getScoreLevel } from '../../utils/getScoreLevel'; +import { getScoreLevel } from '@shared/lib/get-score-level'; import { EmaSmoother } from './calculations'; import { PostureStabilizer } from './PostureStabilizer'; import { ScoreProcessor } from './ScoreProcessor'; diff --git a/src/renderer/src/pages/Main/components/AverageGraph/hooks/useAverageGraphChart.ts b/src/renderer/src/pages/Main/components/AverageGraph/hooks/useAverageGraphChart.ts index 6214748..74ee8e1 100644 --- a/src/renderer/src/pages/Main/components/AverageGraph/hooks/useAverageGraphChart.ts +++ b/src/renderer/src/pages/Main/components/AverageGraph/hooks/useAverageGraphChart.ts @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; -import { getColor } from '@utils/getColor'; +import { getColor } from '@shared/lib/get-color'; import { usePostureGraphQuery } from '@api/dashboard/usePostureGraphQuery'; type AverageGraphDatum = { diff --git a/src/renderer/src/pages/Main/components/HighlightsPanel/hooks/useHighlightsChart.ts b/src/renderer/src/pages/Main/components/HighlightsPanel/hooks/useHighlightsChart.ts index 7479c37..c400714 100644 --- a/src/renderer/src/pages/Main/components/HighlightsPanel/hooks/useHighlightsChart.ts +++ b/src/renderer/src/pages/Main/components/HighlightsPanel/hooks/useHighlightsChart.ts @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from 'react'; import { useHighlightQuery } from '../../../../../api/dashboard/useHighlightQuery'; -import { getColor } from '../../../../../utils/getColor'; +import { getColor } from '@shared/lib/get-color'; import type { HighlightDatum } from '../data'; export type HighlightPeriod = 'weekly' | 'monthly'; diff --git a/src/renderer/src/pages/Main/components/MainHeader.tsx b/src/renderer/src/pages/Main/components/MainHeader.tsx index c748a97..c736a63 100644 --- a/src/renderer/src/pages/Main/components/MainHeader.tsx +++ b/src/renderer/src/pages/Main/components/MainHeader.tsx @@ -9,7 +9,7 @@ import { Button } from '../../../components/Button/Button'; import { ThemeToggleSwitch } from '../../../components/ThemeToggleSwitch/ThemeToggleSwitch'; import { useThemePreference } from '../../../hooks/useThemePreference'; -import { cn } from '../../../utils/cn'; +import { cn } from '@shared/lib/cn'; type TabType = 'dashboard' | 'plan' | 'settings'; diff --git a/src/renderer/src/pages/Main/components/RunningPanel.tsx b/src/renderer/src/pages/Main/components/RunningPanel.tsx index de84687..33aeb69 100644 --- a/src/renderer/src/pages/Main/components/RunningPanel.tsx +++ b/src/renderer/src/pages/Main/components/RunningPanel.tsx @@ -16,8 +16,8 @@ import TireBugiRestSvg from '@assets/video/tire-bugi-rest.svg'; import { useEffect, useMemo, useRef } from 'react'; import { useCameraStore } from '../../../store/useCameraStore'; import { usePostureStore } from '../../../store/usePostureStore'; -import { cn } from '../../../utils/cn'; -import { getScoreLevel } from '../../../utils/getScoreLevel'; +import { cn } from '@shared/lib/cn'; +import { getScoreLevel } from '@shared/lib/get-score-level'; const RunningPanel = () => { const score = usePostureStore((state) => state.score); diff --git a/src/renderer/src/utils/cn.ts b/src/renderer/src/shared/lib/cn.ts similarity index 100% rename from src/renderer/src/utils/cn.ts rename to src/renderer/src/shared/lib/cn.ts diff --git a/src/renderer/src/utils/getColor.ts b/src/renderer/src/shared/lib/get-color.ts similarity index 100% rename from src/renderer/src/utils/getColor.ts rename to src/renderer/src/shared/lib/get-color.ts diff --git a/src/renderer/src/utils/getScoreLevel.ts b/src/renderer/src/shared/lib/get-score-level.ts similarity index 99% rename from src/renderer/src/utils/getScoreLevel.ts rename to src/renderer/src/shared/lib/get-score-level.ts index 306a3a9..15bca49 100644 --- a/src/renderer/src/utils/getScoreLevel.ts +++ b/src/renderer/src/shared/lib/get-score-level.ts @@ -108,6 +108,3 @@ export function getScoreLevel(score: number): ScoreLevelInfo { export function getAllLevelDefinitions(): ScoreLevelInfo[] { return Object.values(LEVEL_DEFINITIONS); } - - - diff --git a/src/renderer/src/shared/lib/index.ts b/src/renderer/src/shared/lib/index.ts new file mode 100644 index 0000000..84fdce4 --- /dev/null +++ b/src/renderer/src/shared/lib/index.ts @@ -0,0 +1,5 @@ +export { cn } from './cn'; +export { getColor } from './get-color'; +export { getScoreLevel, getAllLevelDefinitions } from './get-score-level'; +export type { ScoreLevel, ScoreLevelInfo } from './get-score-level'; + diff --git a/src/renderer/tsconfig.json b/src/renderer/tsconfig.json index 6d8b6fe..8c137b7 100644 --- a/src/renderer/tsconfig.json +++ b/src/renderer/tsconfig.json @@ -25,7 +25,8 @@ "@ui/*": ["./src/components/*"], "@assets/*": ["./src/assets/*"], "@api/*": ["./src/api/*"], - "@utils/*": ["./src/utils/*"] + "@utils/*": ["./src/utils/*"], + "@shared/*": ["./src/shared/*"] } }, "include": [ diff --git a/vite.config.mts b/vite.config.mts index 39b3464..effd245 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -23,6 +23,8 @@ export default defineConfig({ '@api': path.resolve(__dirname, 'src/renderer/src/api'), '@utils/': path.resolve(__dirname, 'src/renderer/src/utils') + '/', '@utils': path.resolve(__dirname, 'src/renderer/src/utils'), + '@shared/': path.resolve(__dirname, 'src/renderer/src/shared') + '/', + '@shared': path.resolve(__dirname, 'src/renderer/src/shared'), ui: path.resolve(__dirname, 'src/renderer/src/components'), }, }, From 266a62a3bd82be43d14e036e976f4144fb23c11a Mon Sep 17 00:00:00 2001 From: choihooo Date: Fri, 28 Nov 2025 11:20:54 +0900 Subject: [PATCH 02/12] =?UTF-8?q?refactor(shared):=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/api/dashboard/useAttendanceQuery.ts | 2 +- .../src/api/dashboard/useAverageScoreQuery.ts | 2 +- .../src/api/dashboard/useHighlightQuery.ts | 2 +- .../src/api/dashboard/useLevelQuery.ts | 2 +- .../src/api/dashboard/usePostureGraphQuery.ts | 2 +- .../api/dashboard/usePosturePatternQuery.ts | 2 +- .../src/api/login/useLoginMutation.ts | 2 +- .../api/session/useCreateSessionMutation.ts | 2 +- .../api/session/usePauseSessionMutation.ts | 2 +- .../api/session/useResumeSessionMutation.ts | 2 +- .../src/api/session/useSaveMetricsMutation.ts | 2 +- .../src/api/session/useSessionReportQuery.ts | 2 +- .../src/api/session/useStopSessionMutation.ts | 2 +- src/renderer/src/api/signup/signup.ts | 2 +- src/renderer/src/api/signup/verifyEmail.ts | 2 +- src/renderer/src/components/Button/Button.tsx | 53 ------------------ .../components/Modal/NotificationModal.tsx | 4 +- .../Modal/components/TimeControlSection.tsx | 2 +- src/renderer/src/components/index.ts | 10 ++-- src/renderer/src/layout/Header/Header.tsx | 2 +- .../Calibration/components/MeasuringPanel.tsx | 2 +- .../Calibration/components/WebcamView.tsx | 2 +- .../Calibration/components/WelcomePanel.tsx | 2 +- .../pages/Login/components/LoginButton.tsx | 2 +- .../src/pages/Login/components/Loginforrm.tsx | 2 +- .../pages/Login/components/PasswordField.tsx | 2 +- src/renderer/src/pages/Main/MainPage.tsx | 2 +- .../pages/Main/components/AttendacePanel.tsx | 8 +-- .../AverageGraph/AverageGraphPannel.tsx | 4 +- .../pages/Main/components/HighlightsPanel.tsx | 4 +- .../src/pages/Main/components/MainHeader.tsx | 4 +- .../Main/components/PosePatternPanel.tsx | 2 +- .../Main/components/TotalDistanceModal.tsx | 4 +- .../Main/components/TotalDistancePanel.tsx | 2 +- .../Onboarding/OnboardingCompletionPage.tsx | 2 +- .../components/CameraPermissionButton.tsx | 2 +- .../pages/Onboarding/components/InfoPanel.tsx | 2 +- .../pages/SignUp/EmailVerificationPage.tsx | 2 +- .../pages/SignUp/components/SignUpform.tsx | 4 +- .../pages/SignUp/components/VerifyAction.tsx | 4 +- src/renderer/src/routers/index.tsx | 2 +- src/renderer/src/shared/api/index.ts | 2 + .../{api/api.ts => shared/api/instance.ts} | 0 src/renderer/src/shared/ui/button/Button.tsx | 54 +++++++++++++++++++ src/renderer/src/shared/ui/button/index.ts | 3 ++ .../ui/input-field}/TextField.tsx | 1 + .../src/shared/ui/input-field/index.ts | 3 ++ .../ui/intensity-slider}/IntensitySlider.tsx | 1 + .../src/shared/ui/intensity-slider/index.ts | 2 + .../Modal => shared/ui/modal}/ModalPortal.ts | 1 + src/renderer/src/shared/ui/modal/index.ts | 2 + .../NotificateMessage.tsx | 1 + .../ui/notification-message}/icons.tsx | 1 + .../shared/ui/notification-message/index.ts | 3 ++ .../ui/page-move-button}/PageMoveButton.tsx | 1 + .../src/shared/ui/page-move-button/index.ts | 2 + .../ui/panel-header}/PannelHeader.tsx | 1 + .../src/shared/ui/panel-header/index.ts | 2 + .../ThemeToggleSwitch.tsx | 5 +- .../shared/ui/theme-toggle-switch/index.ts | 2 + .../Timer => shared/ui/timer}/Timer.tsx | 1 + src/renderer/src/shared/ui/timer/index.ts | 3 ++ .../NotificationToggleSwitch.tsx | 1 + .../ui/toggle-switch}/ToggleSwitch.tsx | 1 + .../src/shared/ui/toggle-switch/index.ts | 3 ++ .../ui/typography}/Typography.tsx | 1 + .../src/shared/ui/typography/index.ts | 2 + 67 files changed, 151 insertions(+), 109 deletions(-) delete mode 100644 src/renderer/src/components/Button/Button.tsx create mode 100644 src/renderer/src/shared/api/index.ts rename src/renderer/src/{api/api.ts => shared/api/instance.ts} (100%) create mode 100644 src/renderer/src/shared/ui/button/Button.tsx create mode 100644 src/renderer/src/shared/ui/button/index.ts rename src/renderer/src/{components/InputField => shared/ui/input-field}/TextField.tsx (99%) create mode 100644 src/renderer/src/shared/ui/input-field/index.ts rename src/renderer/src/{components/IntensitySlider => shared/ui/intensity-slider}/IntensitySlider.tsx (99%) create mode 100644 src/renderer/src/shared/ui/intensity-slider/index.ts rename src/renderer/src/{components/Modal => shared/ui/modal}/ModalPortal.ts (99%) create mode 100644 src/renderer/src/shared/ui/modal/index.ts rename src/renderer/src/{components/NotificateMessage => shared/ui/notification-message}/NotificateMessage.tsx (99%) rename src/renderer/src/{components/NotificateMessage => shared/ui/notification-message}/icons.tsx (99%) create mode 100644 src/renderer/src/shared/ui/notification-message/index.ts rename src/renderer/src/{components/PageMoveButton => shared/ui/page-move-button}/PageMoveButton.tsx (99%) create mode 100644 src/renderer/src/shared/ui/page-move-button/index.ts rename src/renderer/src/{components/PannelHeader => shared/ui/panel-header}/PannelHeader.tsx (99%) create mode 100644 src/renderer/src/shared/ui/panel-header/index.ts rename src/renderer/src/{components/ThemeToggleSwitch => shared/ui/theme-toggle-switch}/ThemeToggleSwitch.tsx (92%) create mode 100644 src/renderer/src/shared/ui/theme-toggle-switch/index.ts rename src/renderer/src/{components/Timer => shared/ui/timer}/Timer.tsx (99%) create mode 100644 src/renderer/src/shared/ui/timer/index.ts rename src/renderer/src/{components/ToggleSwitch => shared/ui/toggle-switch}/NotificationToggleSwitch.tsx (99%) rename src/renderer/src/{components/ToggleSwitch => shared/ui/toggle-switch}/ToggleSwitch.tsx (99%) create mode 100644 src/renderer/src/shared/ui/toggle-switch/index.ts rename src/renderer/src/{components/Typography => shared/ui/typography}/Typography.tsx (99%) create mode 100644 src/renderer/src/shared/ui/typography/index.ts diff --git a/src/renderer/src/api/dashboard/useAttendanceQuery.ts b/src/renderer/src/api/dashboard/useAttendanceQuery.ts index 1557c8a..aabefc8 100644 --- a/src/renderer/src/api/dashboard/useAttendanceQuery.ts +++ b/src/renderer/src/api/dashboard/useAttendanceQuery.ts @@ -3,7 +3,7 @@ import { AttendanceQueryParams, AttendanceResponse, } from '../../types/dashboard/attendance'; -import api from '../api'; +import api from '@shared/api'; /** * 출석 현황 조회 API diff --git a/src/renderer/src/api/dashboard/useAverageScoreQuery.ts b/src/renderer/src/api/dashboard/useAverageScoreQuery.ts index d86f643..70d53d0 100644 --- a/src/renderer/src/api/dashboard/useAverageScoreQuery.ts +++ b/src/renderer/src/api/dashboard/useAverageScoreQuery.ts @@ -1,5 +1,5 @@ import { useQuery } from '@tanstack/react-query'; -import api from '../api'; +import api from '@shared/api'; import { AverageScoreResponse } from '../../types/dashboard/averageScore'; /** diff --git a/src/renderer/src/api/dashboard/useHighlightQuery.ts b/src/renderer/src/api/dashboard/useHighlightQuery.ts index 344d884..faae8c2 100644 --- a/src/renderer/src/api/dashboard/useHighlightQuery.ts +++ b/src/renderer/src/api/dashboard/useHighlightQuery.ts @@ -3,7 +3,7 @@ import { HighlightQueryParams, HighlightResponse, } from '../../types/dashboard/highlight'; -import api from '../api'; +import api from '@shared/api'; /** * 하이라이트 조회 API diff --git a/src/renderer/src/api/dashboard/useLevelQuery.ts b/src/renderer/src/api/dashboard/useLevelQuery.ts index 333b15a..d17f243 100644 --- a/src/renderer/src/api/dashboard/useLevelQuery.ts +++ b/src/renderer/src/api/dashboard/useLevelQuery.ts @@ -1,5 +1,5 @@ import { useQuery } from '@tanstack/react-query'; -import api from '../api'; +import api from '@shared/api'; import { LevelResponse } from '../../types/dashboard/level'; /** diff --git a/src/renderer/src/api/dashboard/usePostureGraphQuery.ts b/src/renderer/src/api/dashboard/usePostureGraphQuery.ts index 0949937..a5db4c7 100644 --- a/src/renderer/src/api/dashboard/usePostureGraphQuery.ts +++ b/src/renderer/src/api/dashboard/usePostureGraphQuery.ts @@ -1,5 +1,5 @@ import { useQuery } from '@tanstack/react-query'; -import api from '../api'; +import api from '@shared/api'; import { PostureGraphResponse } from '../../types/dashboard/postureGraph'; /** diff --git a/src/renderer/src/api/dashboard/usePosturePatternQuery.ts b/src/renderer/src/api/dashboard/usePosturePatternQuery.ts index ced9c56..2c291cb 100644 --- a/src/renderer/src/api/dashboard/usePosturePatternQuery.ts +++ b/src/renderer/src/api/dashboard/usePosturePatternQuery.ts @@ -1,5 +1,5 @@ import { useQuery } from '@tanstack/react-query'; -import api from '../api'; +import api from '@shared/api'; import { PosturePatternResponse } from '../../types/dashboard/posturePattern'; /** diff --git a/src/renderer/src/api/login/useLoginMutation.ts b/src/renderer/src/api/login/useLoginMutation.ts index 7b1a498..a944a05 100644 --- a/src/renderer/src/api/login/useLoginMutation.ts +++ b/src/renderer/src/api/login/useLoginMutation.ts @@ -1,6 +1,6 @@ import { useMutation } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; -import api from '../api'; +import api from '@shared/api'; import { LoginInput, LoginResponse } from '../../types/login/mutation'; /*로그인 api */ diff --git a/src/renderer/src/api/session/useCreateSessionMutation.ts b/src/renderer/src/api/session/useCreateSessionMutation.ts index 16868c7..59e3a98 100644 --- a/src/renderer/src/api/session/useCreateSessionMutation.ts +++ b/src/renderer/src/api/session/useCreateSessionMutation.ts @@ -1,5 +1,5 @@ import { useMutation } from '@tanstack/react-query'; -import api from '../api'; +import api from '@shared/api'; import { CreateSessionResponse } from '../../types/main/session'; /** diff --git a/src/renderer/src/api/session/usePauseSessionMutation.ts b/src/renderer/src/api/session/usePauseSessionMutation.ts index 7ced5bf..4836905 100644 --- a/src/renderer/src/api/session/usePauseSessionMutation.ts +++ b/src/renderer/src/api/session/usePauseSessionMutation.ts @@ -1,5 +1,5 @@ import { useMutation } from '@tanstack/react-query'; -import api from '../api'; +import api from '@shared/api'; import { SessionActionResponse } from '../../types/main/session'; /** diff --git a/src/renderer/src/api/session/useResumeSessionMutation.ts b/src/renderer/src/api/session/useResumeSessionMutation.ts index 638cea0..6395c2b 100644 --- a/src/renderer/src/api/session/useResumeSessionMutation.ts +++ b/src/renderer/src/api/session/useResumeSessionMutation.ts @@ -1,5 +1,5 @@ import { useMutation } from '@tanstack/react-query'; -import api from '../api'; +import api from '@shared/api'; import { SessionActionResponse } from '../../types/main/session'; /** diff --git a/src/renderer/src/api/session/useSaveMetricsMutation.ts b/src/renderer/src/api/session/useSaveMetricsMutation.ts index eff26a4..a8c1bdb 100644 --- a/src/renderer/src/api/session/useSaveMetricsMutation.ts +++ b/src/renderer/src/api/session/useSaveMetricsMutation.ts @@ -1,5 +1,5 @@ import { useMutation } from '@tanstack/react-query'; -import api from '../api'; +import api from '@shared/api'; import { SaveMetricsRequest, SaveMetricsResponse, diff --git a/src/renderer/src/api/session/useSessionReportQuery.ts b/src/renderer/src/api/session/useSessionReportQuery.ts index 46d2d36..333a404 100644 --- a/src/renderer/src/api/session/useSessionReportQuery.ts +++ b/src/renderer/src/api/session/useSessionReportQuery.ts @@ -1,5 +1,5 @@ import { useQuery } from '@tanstack/react-query'; -import api from '../api'; +import api from '@shared/api'; import { SessionReportResponse } from '../../types/main/session'; /** diff --git a/src/renderer/src/api/session/useStopSessionMutation.ts b/src/renderer/src/api/session/useStopSessionMutation.ts index 7b3819b..df79dfe 100644 --- a/src/renderer/src/api/session/useStopSessionMutation.ts +++ b/src/renderer/src/api/session/useStopSessionMutation.ts @@ -1,5 +1,5 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import api from '../api'; +import api from '@shared/api'; import { SessionActionResponse } from '../../types/main/session'; /** diff --git a/src/renderer/src/api/signup/signup.ts b/src/renderer/src/api/signup/signup.ts index ea3b2db..43eb797 100644 --- a/src/renderer/src/api/signup/signup.ts +++ b/src/renderer/src/api/signup/signup.ts @@ -1,6 +1,6 @@ import { useMutation } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; -import api from '../api'; +import api from '@shared/api'; export interface SignupRequest { email: string; diff --git a/src/renderer/src/api/signup/verifyEmail.ts b/src/renderer/src/api/signup/verifyEmail.ts index d3a50be..9d309a8 100644 --- a/src/renderer/src/api/signup/verifyEmail.ts +++ b/src/renderer/src/api/signup/verifyEmail.ts @@ -1,6 +1,6 @@ import { useMutation } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; -import api from '../api'; +import api from '@shared/api'; export interface ResendVerifyEmailRequest { email: string; diff --git a/src/renderer/src/components/Button/Button.tsx b/src/renderer/src/components/Button/Button.tsx deleted file mode 100644 index 9e5a47a..0000000 --- a/src/renderer/src/components/Button/Button.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import * as React from 'react'; -import { cva, type VariantProps } from 'class-variance-authority'; - -import { cn } from '@shared/lib/cn'; - -const buttonVariants = cva( - 'inline-flex items-center justify-center rounded-full transition-colors focus-visible:outline-none disabled:cursor-not-allowed active:scale-95', - { - variants: { - variant: { - primary: - 'bg-yellow-400 text-grey-1000 hover:bg-yellow-500 active:bg-yellow-600 disabled:bg-yellow-100 disabled:text-grey-0 cursor-pointer', - sub: 'bg-yellow-50 text-yellow-500 hover:bg-yellow-100 active:bg-yellow-200 active:text-yellow-600 cursor-pointer', - grey: 'bg-grey-25 text-grey-500 hover:bg-grey-50 active:bg-grey-100 active:text-grey-300 cursor-pointer disabled:bg-grey-25 disabled:text-grey-100', - }, - - size: { - xs: 'h-[33px] px-3 text-caption-sm-medium', - sm: 'h-10 px-4 text-body-md-medium', - md: 'h-10 px-5 text-body-md-medium', - lg: 'h-[51px] px-6 text-body-lg-medium', - xl: 'h-[59px] px-7 text-body-lg-medium ', - }, - }, - defaultVariants: { - variant: 'primary', - size: 'md', - }, - }, -); - -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - text?: React.ReactNode; -} - -const Button = React.forwardRef( - ({ className, variant, size, text, ...props }, ref) => { - return ( - - ); - }, -); -Button.displayName = 'Button'; - -export { Button, buttonVariants }; diff --git a/src/renderer/src/components/Modal/NotificationModal.tsx b/src/renderer/src/components/Modal/NotificationModal.tsx index f024c14..88e66e5 100644 --- a/src/renderer/src/components/Modal/NotificationModal.tsx +++ b/src/renderer/src/components/Modal/NotificationModal.tsx @@ -1,8 +1,8 @@ -import { NotificationToggleSwitch } from '@ui/ToggleSwitch/NotificationToggleSwitch'; +import { NotificationToggleSwitch } from '@shared/ui/toggle-switch'; import { useState } from 'react'; import { useTimeEditor } from './hooks/useTimeEditor'; import { TimeControlSection } from './components/TimeControlSection'; -import { Button } from '@ui/Button/Button'; +import { Button } from '@shared/ui/button'; import { useNotificationStore } from '../../store/useNotificationStore'; interface NotificationModalProps { diff --git a/src/renderer/src/components/Modal/components/TimeControlSection.tsx b/src/renderer/src/components/Modal/components/TimeControlSection.tsx index 94ddba3..0a8e8b6 100644 --- a/src/renderer/src/components/Modal/components/TimeControlSection.tsx +++ b/src/renderer/src/components/Modal/components/TimeControlSection.tsx @@ -1,4 +1,4 @@ -import { NotificationToggleSwitch } from '@ui/ToggleSwitch/NotificationToggleSwitch'; +import { NotificationToggleSwitch } from '@shared/ui/toggle-switch'; import { useTimeEditor } from '../hooks/useTimeEditor'; import MinusIcon from '../../../assets/main/minus_icon.svg?react'; import PlusIcon from '../../../assets/main/plus_icon.svg?react'; diff --git a/src/renderer/src/components/index.ts b/src/renderer/src/components/index.ts index 1ba01fd..3156150 100644 --- a/src/renderer/src/components/index.ts +++ b/src/renderer/src/components/index.ts @@ -1,6 +1,6 @@ // UI 컴포넌트 export -export { Button } from './Button/Button'; -export { default as TextField } from './InputField/TextField'; -export * from './NotificateMessage'; -export { Timer } from './Timer/Timer'; -export { Typography } from './Typography/Typography'; +export { Button } from '@shared/ui/button'; +export { TextField } from '@shared/ui/input-field'; +export * from '@shared/ui/notification-message'; +export { Timer } from '@shared/ui/timer'; +export { Typography } from '@shared/ui/typography'; diff --git a/src/renderer/src/layout/Header/Header.tsx b/src/renderer/src/layout/Header/Header.tsx index 1760b47..4c03d7e 100644 --- a/src/renderer/src/layout/Header/Header.tsx +++ b/src/renderer/src/layout/Header/Header.tsx @@ -1,6 +1,6 @@ import Logo from '../../assets/logo.svg?react'; import Symbol from '../../assets/symbol.svg?react'; -import { ThemeToggleSwitch } from '../../components/ThemeToggleSwitch/ThemeToggleSwitch'; +import { ThemeToggleSwitch } from '@shared/ui/theme-toggle-switch'; import { useThemePreference } from '../../hooks/useThemePreference'; const Header = () => { diff --git a/src/renderer/src/pages/Calibration/components/MeasuringPanel.tsx b/src/renderer/src/pages/Calibration/components/MeasuringPanel.tsx index 7c89e9a..160176f 100644 --- a/src/renderer/src/pages/Calibration/components/MeasuringPanel.tsx +++ b/src/renderer/src/pages/Calibration/components/MeasuringPanel.tsx @@ -1,4 +1,4 @@ -import NotificateMessage from '../../../components/NotificateMessage/NotificateMessage'; +import { NotificateMessage } from '@shared/ui/notification-message'; interface MeasuringPanelProps { step1Error?: string | null; diff --git a/src/renderer/src/pages/Calibration/components/WebcamView.tsx b/src/renderer/src/pages/Calibration/components/WebcamView.tsx index 96c1b2a..9e691a4 100644 --- a/src/renderer/src/pages/Calibration/components/WebcamView.tsx +++ b/src/renderer/src/pages/Calibration/components/WebcamView.tsx @@ -1,7 +1,7 @@ import SleepIcon from '@assets/sleep.svg?react'; import { useEffect, useRef, useState, type RefObject } from 'react'; import Webcam from 'react-webcam'; -import { Timer } from '../../../components/Timer/Timer'; +import { Timer } from '@shared/ui/timer'; import { PoseLandmark, WorldLandmark, diff --git a/src/renderer/src/pages/Calibration/components/WelcomePanel.tsx b/src/renderer/src/pages/Calibration/components/WelcomePanel.tsx index db34ccf..3684524 100644 --- a/src/renderer/src/pages/Calibration/components/WelcomePanel.tsx +++ b/src/renderer/src/pages/Calibration/components/WelcomePanel.tsx @@ -1,4 +1,4 @@ -import { Button } from '../../../components/Button/Button'; +import { Button } from '@shared/ui/button'; interface WelcomePanelProps { isPoseDetected: boolean; diff --git a/src/renderer/src/pages/Login/components/LoginButton.tsx b/src/renderer/src/pages/Login/components/LoginButton.tsx index 42104ea..c51a1d6 100644 --- a/src/renderer/src/pages/Login/components/LoginButton.tsx +++ b/src/renderer/src/pages/Login/components/LoginButton.tsx @@ -1,4 +1,4 @@ -import { Button } from '../../../components/Button/Button'; +import { Button } from '@shared/ui/button'; interface LoginButtonProps { text?: string; diff --git a/src/renderer/src/pages/Login/components/Loginforrm.tsx b/src/renderer/src/pages/Login/components/Loginforrm.tsx index 1d076cf..063d122 100644 --- a/src/renderer/src/pages/Login/components/Loginforrm.tsx +++ b/src/renderer/src/pages/Login/components/Loginforrm.tsx @@ -1,6 +1,6 @@ import { useForm } from 'react-hook-form'; import { useEffect } from 'react'; -import TextInput from '../../../components/InputField/TextField'; +import { TextField as TextInput } from '@shared/ui/input-field'; import SaveIdIcon from '../../../assets/auth/saveid_icon.svg?react'; import LoginButton from './LoginButton'; import PasswordField from './PasswordField'; diff --git a/src/renderer/src/pages/Login/components/PasswordField.tsx b/src/renderer/src/pages/Login/components/PasswordField.tsx index bcc9176..531992d 100644 --- a/src/renderer/src/pages/Login/components/PasswordField.tsx +++ b/src/renderer/src/pages/Login/components/PasswordField.tsx @@ -1,5 +1,5 @@ import { useState, forwardRef, type ChangeEvent } from 'react'; -import TextInput from '../../../components/InputField/TextField'; +import { TextField as TextInput } from '@shared/ui/input-field'; import VisibleIcon from '../../../assets/auth/visible_icon.svg?react'; import InvisibleIcon from '../../../assets/auth/invisible_icon.svg?react'; diff --git a/src/renderer/src/pages/Main/MainPage.tsx b/src/renderer/src/pages/Main/MainPage.tsx index 9d23677..6ecb080 100644 --- a/src/renderer/src/pages/Main/MainPage.tsx +++ b/src/renderer/src/pages/Main/MainPage.tsx @@ -19,7 +19,7 @@ import PosePatternPanel from './components/PosePatternPanel'; import WebcamPanel from './components/WebcamPanel'; import TotalDistancePanel from './components/TotalDistancePanel'; import NotificationModal from '../../components/Modal/NotificationModal'; -import { ModalPortal } from '@ui/Modal/ModalPortal'; +import { ModalPortal } from '@shared/ui/modal'; import AverageGraphPannel from './components/AverageGraph/AverageGraphPannel'; import { useModal } from '../../hooks/useModal'; import { useNotificationScheduler } from '../../hooks/useNotificationScheduler'; diff --git a/src/renderer/src/pages/Main/components/AttendacePanel.tsx b/src/renderer/src/pages/Main/components/AttendacePanel.tsx index e1b3c03..096e078 100644 --- a/src/renderer/src/pages/Main/components/AttendacePanel.tsx +++ b/src/renderer/src/pages/Main/components/AttendacePanel.tsx @@ -2,10 +2,10 @@ import DownIcon from '@assets/arrow-narrow-down.svg?react'; import UpIcon from '@assets/arrow-narrow-up.svg?react'; import { useState } from 'react'; import { useAttendanceQuery } from '../../../api/dashboard/useAttendanceQuery'; -import { IntensitySlider } from '../../../components/IntensitySlider/IntensitySlider'; -import { PageMoveButton } from '../../../components/PageMoveButton/PageMoveButton'; -import { PannelHeader } from '../../../components/PannelHeader/PannelHeader'; -import { ToggleSwitch } from '../../../components/ToggleSwitch/ToggleSwitch'; +import { IntensitySlider } from '@shared/ui/intensity-slider'; +import { PageMoveButton } from '@shared/ui/page-move-button'; +import { PannelHeader } from '@shared/ui/panel-header'; +import { ToggleSwitch } from '@shared/ui/toggle-switch'; type CalendarProps = { year: number; diff --git a/src/renderer/src/pages/Main/components/AverageGraph/AverageGraphPannel.tsx b/src/renderer/src/pages/Main/components/AverageGraph/AverageGraphPannel.tsx index daa080f..ee35bd4 100644 --- a/src/renderer/src/pages/Main/components/AverageGraph/AverageGraphPannel.tsx +++ b/src/renderer/src/pages/Main/components/AverageGraph/AverageGraphPannel.tsx @@ -8,8 +8,8 @@ import { XAxis, YAxis, } from 'recharts'; -import { PannelHeader } from '@ui/PannelHeader/PannelHeader'; -import { ToggleSwitch } from '@ui/ToggleSwitch/ToggleSwitch'; +import { PannelHeader } from '@shared/ui/panel-header'; +import { ToggleSwitch } from '@shared/ui/toggle-switch'; import { useAverageGraphChart, type AverageGraphPeriod, diff --git a/src/renderer/src/pages/Main/components/HighlightsPanel.tsx b/src/renderer/src/pages/Main/components/HighlightsPanel.tsx index 50035c1..aa81472 100644 --- a/src/renderer/src/pages/Main/components/HighlightsPanel.tsx +++ b/src/renderer/src/pages/Main/components/HighlightsPanel.tsx @@ -10,8 +10,8 @@ import { YAxis, } from 'recharts'; -import { PannelHeader } from '../../../components/PannelHeader/PannelHeader'; -import { ToggleSwitch } from '../../../components/ToggleSwitch/ToggleSwitch'; +import { PannelHeader } from '@shared/ui/panel-header'; +import { ToggleSwitch } from '@shared/ui/toggle-switch'; import type { HighlightDatum } from './HighlightsPanel/data'; import { useHighlightsChart, diff --git a/src/renderer/src/pages/Main/components/MainHeader.tsx b/src/renderer/src/pages/Main/components/MainHeader.tsx index c736a63..d8cdc97 100644 --- a/src/renderer/src/pages/Main/components/MainHeader.tsx +++ b/src/renderer/src/pages/Main/components/MainHeader.tsx @@ -5,9 +5,9 @@ import { useNavigate } from 'react-router-dom'; import Logo from '../../../assets/logo.svg?react'; import NotificationIcon from '../../../assets/main/bell_icon.svg?react'; import Symbol from '../../../assets/symbol.svg?react'; -import { Button } from '../../../components/Button/Button'; +import { Button } from '@shared/ui/button'; -import { ThemeToggleSwitch } from '../../../components/ThemeToggleSwitch/ThemeToggleSwitch'; +import { ThemeToggleSwitch } from '@shared/ui/theme-toggle-switch'; import { useThemePreference } from '../../../hooks/useThemePreference'; import { cn } from '@shared/lib/cn'; diff --git a/src/renderer/src/pages/Main/components/PosePatternPanel.tsx b/src/renderer/src/pages/Main/components/PosePatternPanel.tsx index e5bec2a..8c97022 100644 --- a/src/renderer/src/pages/Main/components/PosePatternPanel.tsx +++ b/src/renderer/src/pages/Main/components/PosePatternPanel.tsx @@ -6,7 +6,7 @@ import ClockIcon from '@assets/clock.svg?react'; import GlassHourIcon from '@assets/hourglass.svg?react'; import TumbupIcon from '@assets/thumbup.svg?react'; import { usePosturePatternQuery } from '../../../api/dashboard/usePosturePatternQuery'; -import { PannelHeader } from '../../../components/PannelHeader/PannelHeader'; +import { PannelHeader } from '@shared/ui/panel-header'; // 시간 형식 변환: "14:00:00" -> "오후 2시" const formatTime = (timeStr: string): string => { diff --git a/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx b/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx index 1313976..2aa7e9f 100644 --- a/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx +++ b/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx @@ -1,5 +1,5 @@ -import { Button } from '@ui/Button/Button'; -import { ModalPortal } from '@ui/Modal/ModalPortal'; +import { Button } from '@shared/ui/button'; +import { ModalPortal } from '@shared/ui/modal'; import CharacterSpeedRow, { CHARACTER_SPEED_DATA } from './CharacterSpeedRow'; interface TotalDistanceModalProps { diff --git a/src/renderer/src/pages/Main/components/TotalDistancePanel.tsx b/src/renderer/src/pages/Main/components/TotalDistancePanel.tsx index dadf6c1..c84454c 100644 --- a/src/renderer/src/pages/Main/components/TotalDistancePanel.tsx +++ b/src/renderer/src/pages/Main/components/TotalDistancePanel.tsx @@ -1,4 +1,4 @@ -import { PannelHeader } from '@ui/PannelHeader/PannelHeader'; +import { PannelHeader } from '@shared/ui/panel-header'; import { useLevelQuery } from '../../../api/dashboard/useLevelQuery'; import AchivementMedal from '../../../assets/main/achivement_meadl.svg?react'; import { useModal } from '../../../hooks/useModal'; diff --git a/src/renderer/src/pages/Onboarding/OnboardingCompletionPage.tsx b/src/renderer/src/pages/Onboarding/OnboardingCompletionPage.tsx index afea2d6..568a71f 100644 --- a/src/renderer/src/pages/Onboarding/OnboardingCompletionPage.tsx +++ b/src/renderer/src/pages/Onboarding/OnboardingCompletionPage.tsx @@ -1,6 +1,6 @@ import { useNavigate } from 'react-router-dom'; import CompletionCharacter from '../../assets/completion.svg?react'; -import { Button } from '../../components/Button/Button'; +import { Button } from '@shared/ui/button'; import { useCreateSessionMutation } from '../../api/session/useCreateSessionMutation'; import { useCameraStore } from '../../store/useCameraStore'; import { useLevelQuery } from '../../api/dashboard/useLevelQuery'; diff --git a/src/renderer/src/pages/Onboarding/components/CameraPermissionButton.tsx b/src/renderer/src/pages/Onboarding/components/CameraPermissionButton.tsx index 4bafa19..006e86e 100644 --- a/src/renderer/src/pages/Onboarding/components/CameraPermissionButton.tsx +++ b/src/renderer/src/pages/Onboarding/components/CameraPermissionButton.tsx @@ -1,5 +1,5 @@ import { useNavigate } from 'react-router-dom'; -import { Button } from '../../../components/Button/Button'; +import { Button } from '@shared/ui/button'; import { useCameraStore } from '../../../store/useCameraStore'; const CameraPermissionButton = () => { diff --git a/src/renderer/src/pages/Onboarding/components/InfoPanel.tsx b/src/renderer/src/pages/Onboarding/components/InfoPanel.tsx index 945c1de..ccc6205 100644 --- a/src/renderer/src/pages/Onboarding/components/InfoPanel.tsx +++ b/src/renderer/src/pages/Onboarding/components/InfoPanel.tsx @@ -3,7 +3,7 @@ import FirstIcon from '@assets/onboarding/first_progress_icon.svg?react'; import FourthIcon from '@assets/onboarding/fourth_progress_icon.svg?react'; import SecondIcon from '@assets/onboarding/second_progress_icon.svg?react'; import ThirdIcon from '@assets/onboarding/third_progress_icon.svg?react'; -import { Button } from '@ui/index'; +import { Button } from '@shared/ui/button'; // 단계별 아이콘 const STEP_ICONS = [FirstIcon, SecondIcon, ThirdIcon, FourthIcon, FifthIcon]; diff --git a/src/renderer/src/pages/SignUp/EmailVerificationPage.tsx b/src/renderer/src/pages/SignUp/EmailVerificationPage.tsx index afabce2..9c695aa 100644 --- a/src/renderer/src/pages/SignUp/EmailVerificationPage.tsx +++ b/src/renderer/src/pages/SignUp/EmailVerificationPage.tsx @@ -2,7 +2,7 @@ import { useNavigate } from 'react-router-dom'; import { useResendVerifyEmailMuation } from '../../api/signup/verifyEmail'; -import { Button } from '../../components/Button/Button'; +import { Button } from '@shared/ui/button'; import { useEmailStore } from '../../store/useSignUpStore'; import EmailHeroSection from './components/EmailHeroSection'; import ResendSection from './components/ResendSection'; diff --git a/src/renderer/src/pages/SignUp/components/SignUpform.tsx b/src/renderer/src/pages/SignUp/components/SignUpform.tsx index 20b979e..4a76a8d 100644 --- a/src/renderer/src/pages/SignUp/components/SignUpform.tsx +++ b/src/renderer/src/pages/SignUp/components/SignUpform.tsx @@ -1,7 +1,7 @@ import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { Button } from '../../../components/Button/Button'; -import TextField from '../../../components/InputField/TextField'; +import { Button } from '@shared/ui/button'; +import { TextField } from '@shared/ui/input-field'; import PasswordField from '../../Login/components/PasswordField'; import SuccessIcon from '../../../assets/auth/success_icon.svg?react'; import FailIcon from '../../../assets/auth/error_icon.svg?react'; diff --git a/src/renderer/src/pages/SignUp/components/VerifyAction.tsx b/src/renderer/src/pages/SignUp/components/VerifyAction.tsx index 7a99815..4906b80 100644 --- a/src/renderer/src/pages/SignUp/components/VerifyAction.tsx +++ b/src/renderer/src/pages/SignUp/components/VerifyAction.tsx @@ -1,5 +1,5 @@ -import { Button } from '../../../components/Button/Button'; -import TextField from '../../../components/InputField/TextField'; +import { Button } from '@shared/ui/button'; +import { TextField } from '@shared/ui/input-field'; import { useNavigate } from 'react-router-dom'; export default function VerifyAction({ email }: { email: string }) { diff --git a/src/renderer/src/routers/index.tsx b/src/renderer/src/routers/index.tsx index 7a47ac8..802ddd1 100644 --- a/src/renderer/src/routers/index.tsx +++ b/src/renderer/src/routers/index.tsx @@ -1,5 +1,5 @@ import { createBrowserRouter, redirect } from 'react-router-dom'; -import api from '../api/api'; +import api from '@shared/api'; import Layout from '../layout/Layout'; import CalibrationPage from '../pages/Calibration/CalibrationPage'; import LoginPage from '../pages/Login/LoginPage'; diff --git a/src/renderer/src/shared/api/index.ts b/src/renderer/src/shared/api/index.ts new file mode 100644 index 0000000..f25205d --- /dev/null +++ b/src/renderer/src/shared/api/index.ts @@ -0,0 +1,2 @@ +export { default } from './instance'; + diff --git a/src/renderer/src/api/api.ts b/src/renderer/src/shared/api/instance.ts similarity index 100% rename from src/renderer/src/api/api.ts rename to src/renderer/src/shared/api/instance.ts diff --git a/src/renderer/src/shared/ui/button/Button.tsx b/src/renderer/src/shared/ui/button/Button.tsx new file mode 100644 index 0000000..ce7001b --- /dev/null +++ b/src/renderer/src/shared/ui/button/Button.tsx @@ -0,0 +1,54 @@ +import { cva, type VariantProps } from 'class-variance-authority'; +import * as React from 'react'; + +import { cn } from '@shared/lib/cn'; + +const buttonVariants = cva( + 'inline-flex items-center justify-center rounded-full transition-colors focus-visible:outline-none disabled:cursor-not-allowed active:scale-95', + { + variants: { + variant: { + primary: + 'bg-yellow-400 text-grey-1000 hover:bg-yellow-500 active:bg-yellow-600 disabled:bg-yellow-100 disabled:text-grey-0 cursor-pointer', + sub: 'bg-yellow-50 text-yellow-500 hover:bg-yellow-100 active:bg-yellow-200 active:text-yellow-600 cursor-pointer', + grey: 'bg-grey-25 text-grey-500 hover:bg-grey-50 active:bg-grey-100 active:text-grey-300 cursor-pointer disabled:bg-grey-25 disabled:text-grey-100', + }, + + size: { + xs: 'h-[33px] px-3 text-caption-sm-medium', + sm: 'h-10 px-4 text-body-md-medium', + md: 'h-10 px-5 text-body-md-medium', + lg: 'h-[51px] px-6 text-body-lg-medium', + xl: 'h-[59px] px-7 text-body-lg-medium ', + }, + }, + defaultVariants: { + variant: 'primary', + size: 'md', + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + text?: React.ReactNode; +} + +const Button = React.forwardRef( + ({ className, variant, size, text, ...props }, ref) => { + return ( + + ); + }, +); +Button.displayName = 'Button'; + +export { Button, buttonVariants }; + diff --git a/src/renderer/src/shared/ui/button/index.ts b/src/renderer/src/shared/ui/button/index.ts new file mode 100644 index 0000000..83c53d5 --- /dev/null +++ b/src/renderer/src/shared/ui/button/index.ts @@ -0,0 +1,3 @@ +export { Button, buttonVariants } from './Button'; +export type { ButtonProps } from './Button'; + diff --git a/src/renderer/src/components/InputField/TextField.tsx b/src/renderer/src/shared/ui/input-field/TextField.tsx similarity index 99% rename from src/renderer/src/components/InputField/TextField.tsx rename to src/renderer/src/shared/ui/input-field/TextField.tsx index 672762d..7018731 100644 --- a/src/renderer/src/components/InputField/TextField.tsx +++ b/src/renderer/src/shared/ui/input-field/TextField.tsx @@ -53,3 +53,4 @@ const TextField = forwardRef( TextField.displayName = 'TextField'; export default TextField; + diff --git a/src/renderer/src/shared/ui/input-field/index.ts b/src/renderer/src/shared/ui/input-field/index.ts new file mode 100644 index 0000000..f931a66 --- /dev/null +++ b/src/renderer/src/shared/ui/input-field/index.ts @@ -0,0 +1,3 @@ +export { default as TextField } from './TextField'; +export { default } from './TextField'; + diff --git a/src/renderer/src/components/IntensitySlider/IntensitySlider.tsx b/src/renderer/src/shared/ui/intensity-slider/IntensitySlider.tsx similarity index 99% rename from src/renderer/src/components/IntensitySlider/IntensitySlider.tsx rename to src/renderer/src/shared/ui/intensity-slider/IntensitySlider.tsx index 4b2256a..8304aca 100644 --- a/src/renderer/src/components/IntensitySlider/IntensitySlider.tsx +++ b/src/renderer/src/shared/ui/intensity-slider/IntensitySlider.tsx @@ -38,3 +38,4 @@ const IntensitySlider = React.forwardRef( IntensitySlider.displayName = 'IntensitySlider'; export { IntensitySlider }; + diff --git a/src/renderer/src/shared/ui/intensity-slider/index.ts b/src/renderer/src/shared/ui/intensity-slider/index.ts new file mode 100644 index 0000000..10f6fc0 --- /dev/null +++ b/src/renderer/src/shared/ui/intensity-slider/index.ts @@ -0,0 +1,2 @@ +export { IntensitySlider } from './IntensitySlider'; + diff --git a/src/renderer/src/components/Modal/ModalPortal.ts b/src/renderer/src/shared/ui/modal/ModalPortal.ts similarity index 99% rename from src/renderer/src/components/Modal/ModalPortal.ts rename to src/renderer/src/shared/ui/modal/ModalPortal.ts index 71a2858..000c972 100644 --- a/src/renderer/src/components/Modal/ModalPortal.ts +++ b/src/renderer/src/shared/ui/modal/ModalPortal.ts @@ -13,3 +13,4 @@ export const ModalPortal = ({ children }: ModalPortalProps) => { } return ReactDOM.createPortal(children, el); }; + diff --git a/src/renderer/src/shared/ui/modal/index.ts b/src/renderer/src/shared/ui/modal/index.ts new file mode 100644 index 0000000..fcdf064 --- /dev/null +++ b/src/renderer/src/shared/ui/modal/index.ts @@ -0,0 +1,2 @@ +export { ModalPortal } from './ModalPortal'; + diff --git a/src/renderer/src/components/NotificateMessage/NotificateMessage.tsx b/src/renderer/src/shared/ui/notification-message/NotificateMessage.tsx similarity index 99% rename from src/renderer/src/components/NotificateMessage/NotificateMessage.tsx rename to src/renderer/src/shared/ui/notification-message/NotificateMessage.tsx index 44e9022..6fc2ff0 100644 --- a/src/renderer/src/components/NotificateMessage/NotificateMessage.tsx +++ b/src/renderer/src/shared/ui/notification-message/NotificateMessage.tsx @@ -91,3 +91,4 @@ export function NotificateMessage({ } export default NotificateMessage; + diff --git a/src/renderer/src/components/NotificateMessage/icons.tsx b/src/renderer/src/shared/ui/notification-message/icons.tsx similarity index 99% rename from src/renderer/src/components/NotificateMessage/icons.tsx rename to src/renderer/src/shared/ui/notification-message/icons.tsx index 99a51cf..8ada8a9 100644 --- a/src/renderer/src/components/NotificateMessage/icons.tsx +++ b/src/renderer/src/shared/ui/notification-message/icons.tsx @@ -53,3 +53,4 @@ export function ErrorIcon({ className }: { className?: string }) { ); } + diff --git a/src/renderer/src/shared/ui/notification-message/index.ts b/src/renderer/src/shared/ui/notification-message/index.ts new file mode 100644 index 0000000..7931c67 --- /dev/null +++ b/src/renderer/src/shared/ui/notification-message/index.ts @@ -0,0 +1,3 @@ +export { ErrorIcon, SuccessIcon } from './icons'; +export { default, NotificateMessage } from './NotificateMessage'; + diff --git a/src/renderer/src/components/PageMoveButton/PageMoveButton.tsx b/src/renderer/src/shared/ui/page-move-button/PageMoveButton.tsx similarity index 99% rename from src/renderer/src/components/PageMoveButton/PageMoveButton.tsx rename to src/renderer/src/shared/ui/page-move-button/PageMoveButton.tsx index 9178e3f..17ac0a5 100644 --- a/src/renderer/src/components/PageMoveButton/PageMoveButton.tsx +++ b/src/renderer/src/shared/ui/page-move-button/PageMoveButton.tsx @@ -42,3 +42,4 @@ const PageMoveButton = React.forwardRef( PageMoveButton.displayName = 'PageMoveButton'; export { PageMoveButton }; + diff --git a/src/renderer/src/shared/ui/page-move-button/index.ts b/src/renderer/src/shared/ui/page-move-button/index.ts new file mode 100644 index 0000000..ebb05bb --- /dev/null +++ b/src/renderer/src/shared/ui/page-move-button/index.ts @@ -0,0 +1,2 @@ +export { PageMoveButton } from './PageMoveButton'; + diff --git a/src/renderer/src/components/PannelHeader/PannelHeader.tsx b/src/renderer/src/shared/ui/panel-header/PannelHeader.tsx similarity index 99% rename from src/renderer/src/components/PannelHeader/PannelHeader.tsx rename to src/renderer/src/shared/ui/panel-header/PannelHeader.tsx index 84917bd..e2333c5 100644 --- a/src/renderer/src/components/PannelHeader/PannelHeader.tsx +++ b/src/renderer/src/shared/ui/panel-header/PannelHeader.tsx @@ -21,3 +21,4 @@ const PannelHeader = React.forwardRef( PannelHeader.displayName = 'PannelHeader'; export { PannelHeader }; + diff --git a/src/renderer/src/shared/ui/panel-header/index.ts b/src/renderer/src/shared/ui/panel-header/index.ts new file mode 100644 index 0000000..ad68825 --- /dev/null +++ b/src/renderer/src/shared/ui/panel-header/index.ts @@ -0,0 +1,2 @@ +export { PannelHeader } from './PannelHeader'; + diff --git a/src/renderer/src/components/ThemeToggleSwitch/ThemeToggleSwitch.tsx b/src/renderer/src/shared/ui/theme-toggle-switch/ThemeToggleSwitch.tsx similarity index 92% rename from src/renderer/src/components/ThemeToggleSwitch/ThemeToggleSwitch.tsx rename to src/renderer/src/shared/ui/theme-toggle-switch/ThemeToggleSwitch.tsx index 374fb57..e48a8b9 100644 --- a/src/renderer/src/components/ThemeToggleSwitch/ThemeToggleSwitch.tsx +++ b/src/renderer/src/shared/ui/theme-toggle-switch/ThemeToggleSwitch.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import MoonIcon from '../../assets/moon_icon.svg?react'; -import SunIcon from '../../assets/sun_icon.svg?react'; +import MoonIcon from '@assets/moon_icon.svg?react'; +import SunIcon from '@assets/sun_icon.svg?react'; import { cn } from '@shared/lib/cn'; interface ThemeToggleSwitchProps { @@ -43,3 +43,4 @@ const ThemeToggleSwitch = React.forwardRef< ThemeToggleSwitch.displayName = 'ThemeToggleSwitch'; export { ThemeToggleSwitch }; + diff --git a/src/renderer/src/shared/ui/theme-toggle-switch/index.ts b/src/renderer/src/shared/ui/theme-toggle-switch/index.ts new file mode 100644 index 0000000..03e98fc --- /dev/null +++ b/src/renderer/src/shared/ui/theme-toggle-switch/index.ts @@ -0,0 +1,2 @@ +export { ThemeToggleSwitch } from './ThemeToggleSwitch'; + diff --git a/src/renderer/src/components/Timer/Timer.tsx b/src/renderer/src/shared/ui/timer/Timer.tsx similarity index 99% rename from src/renderer/src/components/Timer/Timer.tsx rename to src/renderer/src/shared/ui/timer/Timer.tsx index 0d18904..c26c14e 100644 --- a/src/renderer/src/components/Timer/Timer.tsx +++ b/src/renderer/src/shared/ui/timer/Timer.tsx @@ -116,3 +116,4 @@ const Timer = function Timer({ export { Timer }; export default Timer; + diff --git a/src/renderer/src/shared/ui/timer/index.ts b/src/renderer/src/shared/ui/timer/index.ts new file mode 100644 index 0000000..d7e5d43 --- /dev/null +++ b/src/renderer/src/shared/ui/timer/index.ts @@ -0,0 +1,3 @@ +export { Timer } from './Timer'; +export { default } from './Timer'; + diff --git a/src/renderer/src/components/ToggleSwitch/NotificationToggleSwitch.tsx b/src/renderer/src/shared/ui/toggle-switch/NotificationToggleSwitch.tsx similarity index 99% rename from src/renderer/src/components/ToggleSwitch/NotificationToggleSwitch.tsx rename to src/renderer/src/shared/ui/toggle-switch/NotificationToggleSwitch.tsx index ac7796d..4124e3d 100644 --- a/src/renderer/src/components/ToggleSwitch/NotificationToggleSwitch.tsx +++ b/src/renderer/src/shared/ui/toggle-switch/NotificationToggleSwitch.tsx @@ -39,3 +39,4 @@ const NotificationToggleSwitch = React.forwardRef< NotificationToggleSwitch.displayName = 'NotificationToggleSwitch'; export { NotificationToggleSwitch }; + diff --git a/src/renderer/src/components/ToggleSwitch/ToggleSwitch.tsx b/src/renderer/src/shared/ui/toggle-switch/ToggleSwitch.tsx similarity index 99% rename from src/renderer/src/components/ToggleSwitch/ToggleSwitch.tsx rename to src/renderer/src/shared/ui/toggle-switch/ToggleSwitch.tsx index 32db2d5..8828a31 100644 --- a/src/renderer/src/components/ToggleSwitch/ToggleSwitch.tsx +++ b/src/renderer/src/shared/ui/toggle-switch/ToggleSwitch.tsx @@ -88,3 +88,4 @@ const ToggleSwitch = React.forwardRef( ToggleSwitch.displayName = 'ToggleSwitch'; export { ToggleSwitch }; + diff --git a/src/renderer/src/shared/ui/toggle-switch/index.ts b/src/renderer/src/shared/ui/toggle-switch/index.ts new file mode 100644 index 0000000..292ed0c --- /dev/null +++ b/src/renderer/src/shared/ui/toggle-switch/index.ts @@ -0,0 +1,3 @@ +export { ToggleSwitch } from './ToggleSwitch'; +export { NotificationToggleSwitch } from './NotificationToggleSwitch'; + diff --git a/src/renderer/src/components/Typography/Typography.tsx b/src/renderer/src/shared/ui/typography/Typography.tsx similarity index 99% rename from src/renderer/src/components/Typography/Typography.tsx rename to src/renderer/src/shared/ui/typography/Typography.tsx index a14f9be..b5d71b1 100644 --- a/src/renderer/src/components/Typography/Typography.tsx +++ b/src/renderer/src/shared/ui/typography/Typography.tsx @@ -63,3 +63,4 @@ export function Typography({ ); } + diff --git a/src/renderer/src/shared/ui/typography/index.ts b/src/renderer/src/shared/ui/typography/index.ts new file mode 100644 index 0000000..41c5bc3 --- /dev/null +++ b/src/renderer/src/shared/ui/typography/index.ts @@ -0,0 +1,2 @@ +export { Typography } from './Typography'; + From 475cbc6614e8a35276e09e3cb64ff455d93884ba Mon Sep 17 00:00:00 2001 From: choihooo Date: Fri, 28 Nov 2025 11:35:18 +0900 Subject: [PATCH 03/12] =?UTF-8?q?refactor(entities):=20=EC=9E=90=EC=84=B8?= =?UTF-8?q?=20=ED=83=90=EC=A7=80=20=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/App.tsx | 2 +- .../pose-detection/PoseDetection.tsx | 186 ------------------ .../src/components/pose-detection/index.ts | 8 - src/renderer/src/entities/posture/index.ts | 14 ++ .../entities/posture/lib/PoseDetection.tsx | 181 +++++++++++++++++ .../posture/lib}/PoseVisualizer.tsx | 11 +- .../posture/lib}/PostureClassifier.ts | 1 + .../posture/lib}/PostureStabilizer.ts | 1 + .../posture/lib}/ScoreProcessor.ts | 38 +--- .../posture/lib}/calculations.ts | 4 +- .../posture/lib}/calibration.ts | 3 +- .../posture/lib}/errorChecks.ts | 1 + .../src/entities/posture/lib/index.ts | 22 +++ .../posture/lib}/types.ts | 0 .../src/entities/posture/model/index.ts | 2 + .../posture/model/use-posture-store.ts} | 1 + .../src/hooks/useNotificationScheduler.ts | 2 +- src/renderer/src/layout/Header/Header.tsx | 2 +- .../src/pages/Calibration/CalibrationPage.tsx | 2 +- .../Calibration/components/WebcamView.tsx | 6 +- src/renderer/src/pages/Main/MainPage.tsx | 24 +-- .../src/pages/Main/components/MainHeader.tsx | 2 +- .../pages/Main/components/RunningPanel.tsx | 2 +- .../Main/components/TotalDistancePanel.tsx | 2 +- .../src/pages/Main/components/WebcamPanel.tsx | 2 +- src/renderer/src/pages/Widget/WidgetPage.tsx | 2 +- .../hooks/usePostureSyncWithLocalStorage.ts | 2 +- src/renderer/src/routers/index.tsx | 94 --------- src/renderer/src/shared/config/router.tsx | 95 +++++++++ src/renderer/src/shared/hooks/index.ts | 2 + .../useModal.ts => shared/hooks/use-modal.ts} | 0 .../hooks/use-theme-preference.ts} | 1 - src/renderer/src/shared/types/svg.d.ts | 8 + .../src/{ => shared}/types/vite-env.d.ts | 1 + src/renderer/src/types/svg.d.ts | 5 - src/renderer/tsconfig.json | 4 +- vite.config.mts | 2 + 37 files changed, 366 insertions(+), 369 deletions(-) delete mode 100644 src/renderer/src/components/pose-detection/PoseDetection.tsx delete mode 100644 src/renderer/src/components/pose-detection/index.ts create mode 100644 src/renderer/src/entities/posture/index.ts create mode 100644 src/renderer/src/entities/posture/lib/PoseDetection.tsx rename src/renderer/src/{components/pose-detection => entities/posture/lib}/PoseVisualizer.tsx (98%) rename src/renderer/src/{components/pose-detection => entities/posture/lib}/PostureClassifier.ts (99%) rename src/renderer/src/{components/pose-detection => entities/posture/lib}/PostureStabilizer.ts (99%) rename src/renderer/src/{components/pose-detection => entities/posture/lib}/ScoreProcessor.ts (73%) rename src/renderer/src/{components/pose-detection => entities/posture/lib}/calculations.ts (100%) rename src/renderer/src/{components/pose-detection => entities/posture/lib}/calibration.ts (93%) rename src/renderer/src/{components/pose-detection => entities/posture/lib}/errorChecks.ts (99%) create mode 100644 src/renderer/src/entities/posture/lib/index.ts rename src/renderer/src/{components/pose-detection => entities/posture/lib}/types.ts (100%) create mode 100644 src/renderer/src/entities/posture/model/index.ts rename src/renderer/src/{store/usePostureStore.ts => entities/posture/model/use-posture-store.ts} (99%) delete mode 100644 src/renderer/src/routers/index.tsx create mode 100644 src/renderer/src/shared/config/router.tsx create mode 100644 src/renderer/src/shared/hooks/index.ts rename src/renderer/src/{hooks/useModal.ts => shared/hooks/use-modal.ts} (100%) rename src/renderer/src/{hooks/useThemePreference.ts => shared/hooks/use-theme-preference.ts} (99%) create mode 100644 src/renderer/src/shared/types/svg.d.ts rename src/renderer/src/{ => shared}/types/vite-env.d.ts (98%) delete mode 100644 src/renderer/src/types/svg.d.ts diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 6284de4..7b8d74d 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,6 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { RouterProvider } from 'react-router-dom'; -import { router } from './routers'; +import { router } from '@shared/config/router'; function App() { const queryClient = new QueryClient(); diff --git a/src/renderer/src/components/pose-detection/PoseDetection.tsx b/src/renderer/src/components/pose-detection/PoseDetection.tsx deleted file mode 100644 index d3ad5b1..0000000 --- a/src/renderer/src/components/pose-detection/PoseDetection.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import { FilesetResolver, PoseLandmarker } from '@mediapipe/tasks-vision'; -import { - useCallback, - useEffect, - useRef, - useState, - type RefObject, -} from 'react'; -import Webcam from 'react-webcam'; - -interface WebcamRef { - video?: HTMLVideoElement | null; -} - -interface PoseDetectionProps { - videoRef: RefObject; // Webcam 컴포넌트 ref - onPoseDetected?: ( - landmarks: PoseLandmark[], - worldLandmarks?: PoseLandmark[], - ) => void; - isEnabled?: boolean; -} - -interface PoseLandmark { - x: number; - y: number; - z: number; - visibility?: number; -} - -const PoseDetection = ({ - videoRef, - onPoseDetected, - isEnabled = true, -}: PoseDetectionProps) => { - const [isInitialized, setIsInitialized] = useState(false); - const [isDetecting, setIsDetecting] = useState(false); - const poseLandmarkerRef = useRef(null); - const lastVideoTimeRef = useRef(-1); - - // MediaPipe 초기화 - useEffect(() => { - const initializePoseLandmarker = async () => { - try { - const vision = await FilesetResolver.forVisionTasks( - 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm', - ); - - poseLandmarkerRef.current = await PoseLandmarker.createFromOptions( - vision, - { - baseOptions: { - modelAssetPath: - 'https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_heavy/float16/1/pose_landmarker_heavy.task', - delegate: 'GPU', - }, - runningMode: 'VIDEO', - numPoses: 1, - minPoseDetectionConfidence: 0.2, // 더 낮은 감지 임계값 - minPosePresenceConfidence: 0.2, // 더 낮은 존재 임계값 - minTrackingConfidence: 0.2, // 더 낮은 추적 임계값 - }, - ); - - setIsInitialized(true); - } catch (error) { - console.error('Failed to initialize pose landmarker:', error); - } - }; - - if (isEnabled) { - initializePoseLandmarker(); - } - }, [isEnabled]); - - // 포즈 감지 실행 - const detectPose = useCallback( - async (videoElement: HTMLVideoElement) => { - if (!poseLandmarkerRef.current || !isEnabled || isDetecting) return; - - const currentTime = videoElement.currentTime; - if (currentTime === lastVideoTimeRef.current) return; - - lastVideoTimeRef.current = currentTime; - setIsDetecting(true); - - try { - const results = poseLandmarkerRef.current.detectForVideo( - videoElement, - performance.now(), - ); - - if (results.landmarks && results.landmarks.length > 0) { - // 13개 주요 포즈 랜드마크 추출 (MediaPipe Pose는 33개 랜드마크를 제공) - const keyLandmarks = extractKeyLandmarks(results.landmarks[0]); - - // World landmarks도 추출 (PI 계산에 필요) - let worldLandmarks: PoseLandmark[] = []; - if (results.worldLandmarks && results.worldLandmarks.length > 0) { - worldLandmarks = extractKeyLandmarks(results.worldLandmarks[0]); - } else { - // World landmarks가 없으면 2D 랜드마크를 3D로 변환해서 사용 - worldLandmarks = keyLandmarks.map((landmark) => ({ - ...landmark, - z: landmark.z || 0, // z값이 없으면 0으로 설정 - })); - } - - onPoseDetected?.(keyLandmarks, worldLandmarks); - } - } catch (error) { - console.error('Pose detection error:', error); - } finally { - setIsDetecting(false); - } - }, - [isEnabled, isDetecting, onPoseDetected], - ); - - // 주요 랜드마크 추출 (얼굴 + 상체 중심) - const extractKeyLandmarks = (landmarks: PoseLandmark[]): PoseLandmark[] => { - // MediaPipe Pose 33개 랜드마크에서 주요 포인트들 선택 - const keyIndices = [ - // 얼굴 영역 (11개) - 0, // NOSE - 1, // LEFT_EYE_INNER - 2, // LEFT_EYE - 3, // LEFT_EYE_OUTER - 4, // RIGHT_EYE_INNER - 5, // RIGHT_EYE - 6, // RIGHT_EYE_OUTER - 7, // LEFT_EAR - 8, // RIGHT_EAR - 9, // MOUTH_LEFT - 10, // MOUTH_RIGHT - // 어깨 영역 (2개) - 11, // LEFT_SHOULDER - 12, // RIGHT_SHOULDER - ]; - - return keyIndices.map((index) => { - const landmark = landmarks[index]; - if (landmark) { - // 측면 각도에서도 최소한의 가시성 보장 - return { - ...landmark, - visibility: Math.max(landmark.visibility || 0, 0.1), - }; - } - return { x: 0, y: 0, z: 0, visibility: 0 }; - }); - }; - - // 비디오 프레임마다 포즈 감지 실행 - useEffect(() => { - if (!isInitialized || !videoRef.current || !isEnabled) return; - - const getVideoElement = () => { - // Webcam 컴포넌트에서 video 요소 가져오기 - const ref = videoRef.current; - if (!ref) return null; - // WebcamRef 인터페이스인 경우 - if ('video' in ref) { - return ref.video || null; - } - // Webcam 컴포넌트인 경우 - video 속성이 있을 수 있음 - return ( - (ref as unknown as { video?: HTMLVideoElement | null })?.video || null - ); - }; - - const interval = setInterval(() => { - const videoElement = getVideoElement(); - if (videoElement && videoElement.readyState >= 2) { - // HAVE_CURRENT_DATA - detectPose(videoElement); - } - }, 50); // 20fps로 감지 (더 빠른 반응) - - return () => clearInterval(interval); - }, [isInitialized, videoRef, isEnabled, detectPose]); - - return null; // 이 컴포넌트는 UI를 렌더링하지 않음 -}; - -export default PoseDetection; diff --git a/src/renderer/src/components/pose-detection/index.ts b/src/renderer/src/components/pose-detection/index.ts deleted file mode 100644 index 6769577..0000000 --- a/src/renderer/src/components/pose-detection/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Barrel export - 모든 것을 재수출하여 기존 import 경로 유지 -export * from './types'; -export * from './calculations'; -export * from './PostureClassifier'; -export * from './calibration'; -export * from './errorChecks'; -export * from './ScoreProcessor'; -export * from './PostureStabilizer'; diff --git a/src/renderer/src/entities/posture/index.ts b/src/renderer/src/entities/posture/index.ts new file mode 100644 index 0000000..60c0f51 --- /dev/null +++ b/src/renderer/src/entities/posture/index.ts @@ -0,0 +1,14 @@ +export * from './lib'; +export * from './model'; + +// 타입 명시적 export +export type { + PoseLandmark, + WorldLandmark, + PIResult, + FrontalityResult, + PostureClassification, + CalibrationState, + CalibrationFrame, +} from './lib/types'; + diff --git a/src/renderer/src/entities/posture/lib/PoseDetection.tsx b/src/renderer/src/entities/posture/lib/PoseDetection.tsx new file mode 100644 index 0000000..dc5b5e3 --- /dev/null +++ b/src/renderer/src/entities/posture/lib/PoseDetection.tsx @@ -0,0 +1,181 @@ +import { FilesetResolver, PoseLandmarker } from '@mediapipe/tasks-vision'; +import { + useCallback, + useEffect, + useRef, + useState, + type RefObject, +} from 'react'; +import Webcam from 'react-webcam'; +import { PoseLandmark } from './types'; + +interface WebcamRef { + video?: HTMLVideoElement | null; +} + +interface PoseDetectionProps { + videoRef: RefObject; // Webcam 컴포넌트 ref + onPoseDetected?: ( + landmarks: PoseLandmark[], + worldLandmarks?: PoseLandmark[], + ) => void; + isEnabled?: boolean; +} + +const PoseDetection = ({ + videoRef, + onPoseDetected, + isEnabled = true, +}: PoseDetectionProps) => { + const [isInitialized, setIsInitialized] = useState(false); + const [isDetecting, setIsDetecting] = useState(false); + const poseLandmarkerRef = useRef(null); + const lastVideoTimeRef = useRef(-1); + + // MediaPipe 초기화 + useEffect(() => { + const initializePoseLandmarker = async () => { + try { + const vision = await FilesetResolver.forVisionTasks( + 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm', + ); + + poseLandmarkerRef.current = await PoseLandmarker.createFromOptions( + vision, + { + baseOptions: { + modelAssetPath: + 'https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_heavy/float16/1/pose_landmarker_heavy.task', + delegate: 'GPU', + }, + runningMode: 'VIDEO', + numPoses: 1, + minPoseDetectionConfidence: 0.2, // 더 낮은 감지 임계값 + minPosePresenceConfidence: 0.2, // 더 낮은 존재 임계값 + minTrackingConfidence: 0.2, // 더 낮은 추적 임계값 + }, + ); + + setIsInitialized(true); + } catch (error) { + console.error('Failed to initialize pose landmarker:', error); + } + }; + + if (isEnabled) { + initializePoseLandmarker(); + } + }, [isEnabled]); + + // 포즈 감지 실행 + const detectPose = useCallback( + async (videoElement: HTMLVideoElement) => { + if (!poseLandmarkerRef.current || !isEnabled || isDetecting) return; + + const currentTime = videoElement.currentTime; + if (currentTime === lastVideoTimeRef.current) return; + + lastVideoTimeRef.current = currentTime; + setIsDetecting(true); + + try { + const results = poseLandmarkerRef.current.detectForVideo( + videoElement, + performance.now(), + ); + + if (results.landmarks && results.landmarks.length > 0) { + // 13개 주요 포즈 랜드마크 추출 (MediaPipe Pose는 33개 랜드마크를 제공) + const keyLandmarks = extractKeyLandmarks(results.landmarks[0]); + + // World landmarks도 추출 (PI 계산에 필요) + let worldLandmarks: PoseLandmark[] = []; + if (results.worldLandmarks && results.worldLandmarks.length > 0) { + worldLandmarks = extractKeyLandmarks(results.worldLandmarks[0]); + } else { + // World landmarks가 없으면 2D 랜드마크를 3D로 변환해서 사용 + worldLandmarks = keyLandmarks.map((landmark) => ({ + ...landmark, + z: landmark.z || 0, // z값이 없으면 0으로 설정 + })); + } + + onPoseDetected?.(keyLandmarks, worldLandmarks); + } + } catch (error) { + console.error('Pose detection error:', error); + } finally { + setIsDetecting(false); + } + }, + [isEnabled, isDetecting, onPoseDetected], + ); + + // 주요 랜드마크 추출 (얼굴 + 상체 중심) + const extractKeyLandmarks = (landmarks: PoseLandmark[]): PoseLandmark[] => { + // MediaPipe Pose 33개 랜드마크에서 주요 포인트들 선택 + const keyIndices = [ + // 얼굴 영역 (11개) + 0, // NOSE + 1, // LEFT_EYE_INNER + 2, // LEFT_EYE + 3, // LEFT_EYE_OUTER + 4, // RIGHT_EYE_INNER + 5, // RIGHT_EYE + 6, // RIGHT_EYE_OUTER + 7, // LEFT_EAR + 8, // RIGHT_EAR + 9, // MOUTH_LEFT + 10, // MOUTH_RIGHT + // 어깨 영역 (2개) + 11, // LEFT_SHOULDER + 12, // RIGHT_SHOULDER + ]; + + return keyIndices.map((index) => { + const landmark = landmarks[index]; + if (landmark) { + // 측면 각도에서도 최소한의 가시성 보장 + return { + ...landmark, + visibility: Math.max(landmark.visibility || 0, 0.1), + }; + } + return { x: 0, y: 0, z: 0, visibility: 0 }; + }); + }; + + // 비디오 프레임마다 포즈 감지 실행 + useEffect(() => { + if (!isInitialized || !videoRef.current || !isEnabled) return; + + const getVideoElement = () => { + // Webcam 컴포넌트에서 video 요소 가져오기 + const ref = videoRef.current; + if (!ref) return null; + // WebcamRef 인터페이스인 경우 + if ('video' in ref) { + return ref.video || null; + } + // Webcam 컴포넌트인 경우 - video 속성이 있을 수 있음 + return ( + (ref as unknown as { video?: HTMLVideoElement | null })?.video || null + ); + }; + + const interval = setInterval(() => { + const videoElement = getVideoElement(); + if (videoElement && videoElement.readyState >= 2) { + // HAVE_CURRENT_DATA + detectPose(videoElement); + } + }, 50); // 20fps로 감지 (더 빠른 반응) + + return () => clearInterval(interval); + }, [isInitialized, videoRef, isEnabled, detectPose]); + + return null; // 이 컴포넌트는 UI를 렌더링하지 않음 +}; + +export default PoseDetection; + diff --git a/src/renderer/src/components/pose-detection/PoseVisualizer.tsx b/src/renderer/src/entities/posture/lib/PoseVisualizer.tsx similarity index 98% rename from src/renderer/src/components/pose-detection/PoseVisualizer.tsx rename to src/renderer/src/entities/posture/lib/PoseVisualizer.tsx index 0e4fe85..844ad24 100644 --- a/src/renderer/src/components/pose-detection/PoseVisualizer.tsx +++ b/src/renderer/src/entities/posture/lib/PoseVisualizer.tsx @@ -1,12 +1,6 @@ import { useEffect, useRef } from 'react'; -import { usePostureStore } from '../../store/usePostureStore'; - -interface PoseLandmark { - x: number; - y: number; - z: number; - visibility?: number; -} +import { usePostureStore } from '@entities/posture'; +import { PoseLandmark } from './types'; interface PoseVisualizerProps { landmarks: PoseLandmark[]; @@ -274,3 +268,4 @@ const PoseVisualizer = ({ }; export default PoseVisualizer; + diff --git a/src/renderer/src/components/pose-detection/PostureClassifier.ts b/src/renderer/src/entities/posture/lib/PostureClassifier.ts similarity index 99% rename from src/renderer/src/components/pose-detection/PostureClassifier.ts rename to src/renderer/src/entities/posture/lib/PostureClassifier.ts index 8186bfe..4845619 100644 --- a/src/renderer/src/components/pose-detection/PostureClassifier.ts +++ b/src/renderer/src/entities/posture/lib/PostureClassifier.ts @@ -173,3 +173,4 @@ export class PostureClassifier { this.lastScoreUpdateTime = 0; } } + diff --git a/src/renderer/src/components/pose-detection/PostureStabilizer.ts b/src/renderer/src/entities/posture/lib/PostureStabilizer.ts similarity index 99% rename from src/renderer/src/components/pose-detection/PostureStabilizer.ts rename to src/renderer/src/entities/posture/lib/PostureStabilizer.ts index 79c0835..9a94432 100644 --- a/src/renderer/src/components/pose-detection/PostureStabilizer.ts +++ b/src/renderer/src/entities/posture/lib/PostureStabilizer.ts @@ -146,3 +146,4 @@ export class PostureStabilizer { return weightedSum / totalWeight; } } + diff --git a/src/renderer/src/components/pose-detection/ScoreProcessor.ts b/src/renderer/src/entities/posture/lib/ScoreProcessor.ts similarity index 73% rename from src/renderer/src/components/pose-detection/ScoreProcessor.ts rename to src/renderer/src/entities/posture/lib/ScoreProcessor.ts index 126686c..1937aba 100644 --- a/src/renderer/src/components/pose-detection/ScoreProcessor.ts +++ b/src/renderer/src/entities/posture/lib/ScoreProcessor.ts @@ -14,43 +14,6 @@ function getPercentile(data: number[], percentile: number): number { ); } -// function getMedian(data: number[]): number { -// if (data.length === 0) return 0; -// const sortedData = [...data].sort((a, b) => a - b); -// const mid = Math.floor(sortedData.length / 2); -// if (sortedData.length % 2 === 0) { -// return (sortedData[mid - 1] + sortedData[mid]) / 2; -// } -// return sortedData[mid]; -// } - -// function removeOutliersIqr(scores: number[], multiplier: number): number[] { -// if (scores.length === 0) return []; - -// const q1 = getPercentile(scores, 25); -// const q3 = getPercentile(scores, 75); -// const iqr = q3 - q1; -// const lowerBound = q1 - multiplier * iqr; -// const upperBound = q3 + multiplier * iqr; - -// const cleanedScores: number[] = []; -// const median = getMedian(scores); - -// for (let i = 0; i < scores.length; i++) { -// const score = scores[i]; -// if (score < lowerBound || score > upperBound) { -// if (i > 0) { -// cleanedScores.push(cleanedScores[cleanedScores.length - 1]); -// } else { -// cleanedScores.push(median); -// } -// } else { -// cleanedScores.push(score); -// } -// } -// return cleanedScores; -// } - function applyMovingAverage(scores: number[], window: number): number[] { if (scores.length === 0) return []; @@ -135,3 +98,4 @@ export class ScoreProcessor { this.scoreBuffer = []; } } + diff --git a/src/renderer/src/components/pose-detection/calculations.ts b/src/renderer/src/entities/posture/lib/calculations.ts similarity index 100% rename from src/renderer/src/components/pose-detection/calculations.ts rename to src/renderer/src/entities/posture/lib/calculations.ts index b2c93f1..1c023d3 100644 --- a/src/renderer/src/components/pose-detection/calculations.ts +++ b/src/renderer/src/entities/posture/lib/calculations.ts @@ -1,8 +1,8 @@ import { + FrontalityResult, + PIResult, PoseLandmark, WorldLandmark, - PIResult, - FrontalityResult, } from './types'; // EMA 스무딩 클래스 diff --git a/src/renderer/src/components/pose-detection/calibration.ts b/src/renderer/src/entities/posture/lib/calibration.ts similarity index 93% rename from src/renderer/src/components/pose-detection/calibration.ts rename to src/renderer/src/entities/posture/lib/calibration.ts index 86234e9..4bbacc3 100644 --- a/src/renderer/src/components/pose-detection/calibration.ts +++ b/src/renderer/src/entities/posture/lib/calibration.ts @@ -1,6 +1,5 @@ +import { checkFrontality, trimmedStats } from './calculations'; import { CalibrationState } from './types'; -import { checkFrontality } from './calculations'; -import { trimmedStats } from './calculations'; // 캘리브레이션 데이터 처리 export function processCalibrationData( diff --git a/src/renderer/src/components/pose-detection/errorChecks.ts b/src/renderer/src/entities/posture/lib/errorChecks.ts similarity index 99% rename from src/renderer/src/components/pose-detection/errorChecks.ts rename to src/renderer/src/entities/posture/lib/errorChecks.ts index 122e3fa..e9a073a 100644 --- a/src/renderer/src/components/pose-detection/errorChecks.ts +++ b/src/renderer/src/entities/posture/lib/errorChecks.ts @@ -218,3 +218,4 @@ export function getStep2Error(frames: CalibrationFrame[]): string | null { checkPostureStability(frames) ); } + diff --git a/src/renderer/src/entities/posture/lib/index.ts b/src/renderer/src/entities/posture/lib/index.ts new file mode 100644 index 0000000..f5fc18c --- /dev/null +++ b/src/renderer/src/entities/posture/lib/index.ts @@ -0,0 +1,22 @@ +// Barrel export - 모든 것을 재수출하여 기존 import 경로 유지 +export * from './types'; +export * from './calculations'; +export * from './PostureClassifier'; +export * from './calibration'; +export * from './errorChecks'; +export * from './ScoreProcessor'; +export * from './PostureStabilizer'; +export { default as PoseDetection } from './PoseDetection'; +export { default as PoseVisualizer } from './PoseVisualizer'; + +// 타입 명시적 export +export type { + PoseLandmark, + WorldLandmark, + PIResult, + FrontalityResult, + PostureClassification, + CalibrationState, + CalibrationFrame, +} from './types'; + diff --git a/src/renderer/src/components/pose-detection/types.ts b/src/renderer/src/entities/posture/lib/types.ts similarity index 100% rename from src/renderer/src/components/pose-detection/types.ts rename to src/renderer/src/entities/posture/lib/types.ts diff --git a/src/renderer/src/entities/posture/model/index.ts b/src/renderer/src/entities/posture/model/index.ts new file mode 100644 index 0000000..6d96948 --- /dev/null +++ b/src/renderer/src/entities/posture/model/index.ts @@ -0,0 +1,2 @@ +export { usePostureStore } from './use-posture-store'; + diff --git a/src/renderer/src/store/usePostureStore.ts b/src/renderer/src/entities/posture/model/use-posture-store.ts similarity index 99% rename from src/renderer/src/store/usePostureStore.ts rename to src/renderer/src/entities/posture/model/use-posture-store.ts index 643f7a6..121f8a8 100644 --- a/src/renderer/src/store/usePostureStore.ts +++ b/src/renderer/src/entities/posture/model/use-posture-store.ts @@ -21,3 +21,4 @@ export const usePostureStore = create()( }, ), ); + diff --git a/src/renderer/src/hooks/useNotificationScheduler.ts b/src/renderer/src/hooks/useNotificationScheduler.ts index d6ec139..5a5a26a 100644 --- a/src/renderer/src/hooks/useNotificationScheduler.ts +++ b/src/renderer/src/hooks/useNotificationScheduler.ts @@ -1,6 +1,6 @@ import { useEffect, useRef } from 'react'; import { useNotificationStore } from '../store/useNotificationStore'; -import { usePostureStore } from '../store/usePostureStore'; +import { usePostureStore } from '@entities/posture'; /* 알림 스케줄러 훅 , 설정된 시간에 따라 시스템 알림을 자동으로 표시 */ export const useNotificationScheduler = () => { diff --git a/src/renderer/src/layout/Header/Header.tsx b/src/renderer/src/layout/Header/Header.tsx index 4c03d7e..33ff94b 100644 --- a/src/renderer/src/layout/Header/Header.tsx +++ b/src/renderer/src/layout/Header/Header.tsx @@ -1,7 +1,7 @@ import Logo from '../../assets/logo.svg?react'; import Symbol from '../../assets/symbol.svg?react'; import { ThemeToggleSwitch } from '@shared/ui/theme-toggle-switch'; -import { useThemePreference } from '../../hooks/useThemePreference'; +import { useThemePreference } from '@shared/hooks/use-theme-preference'; const Header = () => { const [isDark, setIsDark] = useThemePreference(); diff --git a/src/renderer/src/pages/Calibration/CalibrationPage.tsx b/src/renderer/src/pages/Calibration/CalibrationPage.tsx index b0c4a10..c250a16 100644 --- a/src/renderer/src/pages/Calibration/CalibrationPage.tsx +++ b/src/renderer/src/pages/Calibration/CalibrationPage.tsx @@ -18,7 +18,7 @@ import { PIResult, processCalibrationData, WorldLandmark, -} from '../../components/pose-detection'; +} from '@entities/posture'; import MeasuringPanel from './components/MeasuringPanel'; import WebcamView from './components/WebcamView'; import WelcomePanel from './components/WelcomePanel'; diff --git a/src/renderer/src/pages/Calibration/components/WebcamView.tsx b/src/renderer/src/pages/Calibration/components/WebcamView.tsx index 9e691a4..35dc49c 100644 --- a/src/renderer/src/pages/Calibration/components/WebcamView.tsx +++ b/src/renderer/src/pages/Calibration/components/WebcamView.tsx @@ -5,9 +5,9 @@ import { Timer } from '@shared/ui/timer'; import { PoseLandmark, WorldLandmark, -} from '../../../components/pose-detection'; -import PoseDetection from '../../../components/pose-detection/PoseDetection'; -import PoseVisualizer from '../../../components/pose-detection/PoseVisualizer'; + PoseDetection, + PoseVisualizer, +} from '@entities/posture'; import { useCameraStore } from '../../../store/useCameraStore'; interface WebcamViewProps { diff --git a/src/renderer/src/pages/Main/MainPage.tsx b/src/renderer/src/pages/Main/MainPage.tsx index 6ecb080..a6590cc 100644 --- a/src/renderer/src/pages/Main/MainPage.tsx +++ b/src/renderer/src/pages/Main/MainPage.tsx @@ -1,30 +1,30 @@ -import { useEffect, useRef, useState } from 'react'; -import { useSaveMetricsMutation } from '../../api/session/useSaveMetricsMutation'; import { PoseLandmark as AnalyzerPoseLandmark, calculatePI, checkFrontality, PostureClassifier, + usePostureStore, WorldLandmark, -} from '../../components/pose-detection'; +} from '@entities/posture'; +import { useModal } from '@shared/hooks/use-modal'; +import { ModalPortal } from '@shared/ui/modal'; +import { useEffect, useRef } from 'react'; +import { useSaveMetricsMutation } from '../../api/session/useSaveMetricsMutation'; +import NotificationModal from '../../components/Modal/NotificationModal'; +import { useAutoMetricsSender } from '../../hooks/useAutoMetricsSender'; +import { useNotificationScheduler } from '../../hooks/useNotificationScheduler'; +import { useSessionCleanup } from '../../hooks/useSessionCleanup'; import { useCameraStore } from '../../store/useCameraStore'; -import { usePostureStore } from '../../store/usePostureStore'; import { MetricData } from '../../types/main/session'; import AttendacePanel from './components/AttendacePanel'; +import AverageGraphPannel from './components/AverageGraph/AverageGraphPannel'; import AveragePosturePanel from './components/AveragePosture/AveragePosturePanel'; import HighlightsPanel from './components/HighlightsPanel'; import MainHeader from './components/MainHeader'; import MiniRunningPanel from './components/MiniRunningPanel'; import PosePatternPanel from './components/PosePatternPanel'; -import WebcamPanel from './components/WebcamPanel'; import TotalDistancePanel from './components/TotalDistancePanel'; -import NotificationModal from '../../components/Modal/NotificationModal'; -import { ModalPortal } from '@shared/ui/modal'; -import AverageGraphPannel from './components/AverageGraph/AverageGraphPannel'; -import { useModal } from '../../hooks/useModal'; -import { useNotificationScheduler } from '../../hooks/useNotificationScheduler'; -import { useSessionCleanup } from '../../hooks/useSessionCleanup'; -import { useAutoMetricsSender } from '../../hooks/useAutoMetricsSender'; +import WebcamPanel from './components/WebcamPanel'; const LOCAL_STORAGE_KEY = 'calibration_result_v1'; diff --git a/src/renderer/src/pages/Main/components/MainHeader.tsx b/src/renderer/src/pages/Main/components/MainHeader.tsx index d8cdc97..283ae11 100644 --- a/src/renderer/src/pages/Main/components/MainHeader.tsx +++ b/src/renderer/src/pages/Main/components/MainHeader.tsx @@ -8,7 +8,7 @@ import Symbol from '../../../assets/symbol.svg?react'; import { Button } from '@shared/ui/button'; import { ThemeToggleSwitch } from '@shared/ui/theme-toggle-switch'; -import { useThemePreference } from '../../../hooks/useThemePreference'; +import { useThemePreference } from '@shared/hooks/use-theme-preference'; import { cn } from '@shared/lib/cn'; type TabType = 'dashboard' | 'plan' | 'settings'; diff --git a/src/renderer/src/pages/Main/components/RunningPanel.tsx b/src/renderer/src/pages/Main/components/RunningPanel.tsx index 33aeb69..e0f4dc0 100644 --- a/src/renderer/src/pages/Main/components/RunningPanel.tsx +++ b/src/renderer/src/pages/Main/components/RunningPanel.tsx @@ -15,7 +15,7 @@ import TireBugiRestSvg from '@assets/video/tire-bugi-rest.svg'; import { useEffect, useMemo, useRef } from 'react'; import { useCameraStore } from '../../../store/useCameraStore'; -import { usePostureStore } from '../../../store/usePostureStore'; +import { usePostureStore } from '@entities/posture'; import { cn } from '@shared/lib/cn'; import { getScoreLevel } from '@shared/lib/get-score-level'; diff --git a/src/renderer/src/pages/Main/components/TotalDistancePanel.tsx b/src/renderer/src/pages/Main/components/TotalDistancePanel.tsx index c84454c..95f588c 100644 --- a/src/renderer/src/pages/Main/components/TotalDistancePanel.tsx +++ b/src/renderer/src/pages/Main/components/TotalDistancePanel.tsx @@ -1,7 +1,7 @@ import { PannelHeader } from '@shared/ui/panel-header'; import { useLevelQuery } from '../../../api/dashboard/useLevelQuery'; import AchivementMedal from '../../../assets/main/achivement_meadl.svg?react'; -import { useModal } from '../../../hooks/useModal'; +import { useModal } from '@shared/hooks/use-modal'; import TotalDistanceModal from './TotalDistanceModal'; const TotalDistance = () => { diff --git a/src/renderer/src/pages/Main/components/WebcamPanel.tsx b/src/renderer/src/pages/Main/components/WebcamPanel.tsx index fa855c2..0774b1e 100644 --- a/src/renderer/src/pages/Main/components/WebcamPanel.tsx +++ b/src/renderer/src/pages/Main/components/WebcamPanel.tsx @@ -9,7 +9,7 @@ import { Button } from '../../../components'; import { PoseLandmark, WorldLandmark, -} from '../../../components/pose-detection'; +} from '@entities/posture'; import { useWidget } from '../../../hooks/useWidget'; import { useCameraStore } from '../../../store/useCameraStore'; import WebcamView from '../../Calibration/components/WebcamView'; diff --git a/src/renderer/src/pages/Widget/WidgetPage.tsx b/src/renderer/src/pages/Widget/WidgetPage.tsx index 12f694f..b6e469f 100644 --- a/src/renderer/src/pages/Widget/WidgetPage.tsx +++ b/src/renderer/src/pages/Widget/WidgetPage.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { WidgetTitleBar } from '../../components/WidgetTitleBar/WidgetTitleBar'; -import { usePostureStore } from '../../store/usePostureStore'; +import { usePostureStore } from '@entities/posture'; import { MediumWidgetContent } from './components/MediumWidgetContent'; import { MiniWidgetContent } from './components/MiniWidgetContent'; import { usePostureSyncWithLocalStorage } from './hooks/usePostureSyncWithLocalStorage'; diff --git a/src/renderer/src/pages/Widget/hooks/usePostureSyncWithLocalStorage.ts b/src/renderer/src/pages/Widget/hooks/usePostureSyncWithLocalStorage.ts index a8609a5..60729de 100644 --- a/src/renderer/src/pages/Widget/hooks/usePostureSyncWithLocalStorage.ts +++ b/src/renderer/src/pages/Widget/hooks/usePostureSyncWithLocalStorage.ts @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { usePostureStore } from '../../../store/usePostureStore'; +import { usePostureStore } from '@entities/posture'; /* 메인 창의 실시간 자세 상태 위젯 창에 실시간 동기화 localStorage의 storage 이벤트를 통해 창 간 통신 */ diff --git a/src/renderer/src/routers/index.tsx b/src/renderer/src/routers/index.tsx deleted file mode 100644 index 802ddd1..0000000 --- a/src/renderer/src/routers/index.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { createBrowserRouter, redirect } from 'react-router-dom'; -import api from '@shared/api'; -import Layout from '../layout/Layout'; -import CalibrationPage from '../pages/Calibration/CalibrationPage'; -import LoginPage from '../pages/Login/LoginPage'; -import MainPage from '../pages/Main/MainPage'; -import OnboardingCompletionPage from '../pages/Onboarding/OnboardingCompletionPage'; -import OnboardingPage from '../pages/Onboarding/OnboardingPage'; -import EmailVerificationPage from '../pages/SignUp/EmailVerificationPage'; -import EmailVerificationCallbackPage from '../pages/SignUp/EmailVerificationCallbackPage'; -import ResendVerificationPage from '../pages/SignUp/ResendVerificationPage'; -import SignUpPage from '../pages/SignUp/SignUpPage'; -import { WidgetPage } from '../pages/Widget/WidgetPage'; -import OnboardinInitPage from '../pages/Onboarding/OnboardingInitPage'; - -// 인증이 필요한 페이지용 loader -const requireAuthLoader = async () => { - const accessToken = localStorage.getItem('accessToken'); - if (!accessToken) { - return redirect('/'); - } - - try { - await api.get('/users/me'); - return null; - } catch (error) { - localStorage.clear(); - return redirect('/'); - } -}; - -// 로그인 페이지용 loader (토큰이 있으면 메인으로 리다이렉트) -const loginPageLoader = async () => { - const accessToken = localStorage.getItem('accessToken'); - if (!accessToken) { - return null; - } - - try { - await api.get('/users/me'); - return redirect('/main'); - } catch (error) { - localStorage.clear(); - return null; - } -}; - -export const router = createBrowserRouter([ - { - path: '/main', - loader: requireAuthLoader, - element: , - }, - { - element: , - path: '/auth', - children: [ - { - path: 'login', - loader: loginPageLoader, - element: , - }, - { path: 'signup', element: }, - { path: 'verify', element: }, - { path: 'verify-callback', element: }, - { path: 'resend', element: }, - ], - }, - { - element: , - path: '/', - children: [ - { - path: '', - loader: loginPageLoader, - element: , - }, - ], - }, - { - element: , - path: '/onboarding', - children: [ - { path: '', element: }, - { path: 'calibration', element: }, - { path: 'completion', element: }, - { path: 'init', element: }, - ], - }, - { - path: '/widget', - children: [{ path: '', element: }], - }, -]); diff --git a/src/renderer/src/shared/config/router.tsx b/src/renderer/src/shared/config/router.tsx new file mode 100644 index 0000000..69499b0 --- /dev/null +++ b/src/renderer/src/shared/config/router.tsx @@ -0,0 +1,95 @@ +import api from '@shared/api'; +import { createBrowserRouter, redirect } from 'react-router-dom'; +import Layout from '../../layout/Layout'; +import CalibrationPage from '../../pages/Calibration/CalibrationPage'; +import LoginPage from '../../pages/Login/LoginPage'; +import MainPage from '../../pages/Main/MainPage'; +import OnboardingCompletionPage from '../../pages/Onboarding/OnboardingCompletionPage'; +import OnboardinInitPage from '../../pages/Onboarding/OnboardingInitPage'; +import OnboardingPage from '../../pages/Onboarding/OnboardingPage'; +import EmailVerificationCallbackPage from '../../pages/SignUp/EmailVerificationCallbackPage'; +import EmailVerificationPage from '../../pages/SignUp/EmailVerificationPage'; +import ResendVerificationPage from '../../pages/SignUp/ResendVerificationPage'; +import SignUpPage from '../../pages/SignUp/SignUpPage'; +import { WidgetPage } from '../../pages/Widget/WidgetPage'; + +// 인증이 필요한 페이지용 loader +const requireAuthLoader = async () => { + const accessToken = localStorage.getItem('accessToken'); + if (!accessToken) { + return redirect('/'); + } + + try { + await api.get('/users/me'); + return null; + } catch (error) { + localStorage.clear(); + return redirect('/'); + } +}; + +// 로그인 페이지용 loader (토큰이 있으면 메인으로 리다이렉트) +const loginPageLoader = async () => { + const accessToken = localStorage.getItem('accessToken'); + if (!accessToken) { + return null; + } + + try { + await api.get('/users/me'); + return redirect('/main'); + } catch (error) { + localStorage.clear(); + return null; + } +}; + +export const router = createBrowserRouter([ + { + path: '/main', + loader: requireAuthLoader, + element: , + }, + { + element: , + path: '/auth', + children: [ + { + path: 'login', + loader: loginPageLoader, + element: , + }, + { path: 'signup', element: }, + { path: 'verify', element: }, + { path: 'verify-callback', element: }, + { path: 'resend', element: }, + ], + }, + { + element: , + path: '/', + children: [ + { + path: '', + loader: loginPageLoader, + element: , + }, + ], + }, + { + element: , + path: '/onboarding', + children: [ + { path: '', element: }, + { path: 'calibration', element: }, + { path: 'completion', element: }, + { path: 'init', element: }, + ], + }, + { + path: '/widget', + children: [{ path: '', element: }], + }, +]); + diff --git a/src/renderer/src/shared/hooks/index.ts b/src/renderer/src/shared/hooks/index.ts new file mode 100644 index 0000000..16f7ff0 --- /dev/null +++ b/src/renderer/src/shared/hooks/index.ts @@ -0,0 +1,2 @@ +export { useModal } from './use-modal'; +export { useThemePreference } from './use-theme-preference'; diff --git a/src/renderer/src/hooks/useModal.ts b/src/renderer/src/shared/hooks/use-modal.ts similarity index 100% rename from src/renderer/src/hooks/useModal.ts rename to src/renderer/src/shared/hooks/use-modal.ts diff --git a/src/renderer/src/hooks/useThemePreference.ts b/src/renderer/src/shared/hooks/use-theme-preference.ts similarity index 99% rename from src/renderer/src/hooks/useThemePreference.ts rename to src/renderer/src/shared/hooks/use-theme-preference.ts index b508e1b..586b767 100644 --- a/src/renderer/src/hooks/useThemePreference.ts +++ b/src/renderer/src/shared/hooks/use-theme-preference.ts @@ -72,4 +72,3 @@ export function useThemePreference(): UseThemePreferenceReturn { return [isDark, setIsDark]; } - diff --git a/src/renderer/src/shared/types/svg.d.ts b/src/renderer/src/shared/types/svg.d.ts new file mode 100644 index 0000000..1b1baf4 --- /dev/null +++ b/src/renderer/src/shared/types/svg.d.ts @@ -0,0 +1,8 @@ +declare module '*.svg?react' { + import * as React from 'react'; + const ReactComponent: React.FunctionComponent< + React.SVGProps & { title?: string } + >; + export default ReactComponent; +} + diff --git a/src/renderer/src/types/vite-env.d.ts b/src/renderer/src/shared/types/vite-env.d.ts similarity index 98% rename from src/renderer/src/types/vite-env.d.ts rename to src/renderer/src/shared/types/vite-env.d.ts index b1f45c7..8827bbe 100644 --- a/src/renderer/src/types/vite-env.d.ts +++ b/src/renderer/src/shared/types/vite-env.d.ts @@ -1,2 +1,3 @@ /// /// + diff --git a/src/renderer/src/types/svg.d.ts b/src/renderer/src/types/svg.d.ts deleted file mode 100644 index b39749e..0000000 --- a/src/renderer/src/types/svg.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare module '*.svg?react' { - import React from 'react'; - const ReactComponent: React.FC>; - export default ReactComponent; -} diff --git a/src/renderer/tsconfig.json b/src/renderer/tsconfig.json index 8c137b7..892ffc5 100644 --- a/src/renderer/tsconfig.json +++ b/src/renderer/tsconfig.json @@ -26,13 +26,15 @@ "@assets/*": ["./src/assets/*"], "@api/*": ["./src/api/*"], "@utils/*": ["./src/utils/*"], - "@shared/*": ["./src/shared/*"] + "@shared/*": ["./src/shared/*"], + "@entities/*": ["./src/entities/*"] } }, "include": [ "src/**/*.ts", "src/**/*.tsx", "../preload/exposedInMainWorld.d.ts", + "src/shared/types/*.d.ts", "src/types/*.d.ts" ], "exclude": ["node_modules", "dist"] diff --git a/vite.config.mts b/vite.config.mts index effd245..fa9ba97 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -25,6 +25,8 @@ export default defineConfig({ '@utils': path.resolve(__dirname, 'src/renderer/src/utils'), '@shared/': path.resolve(__dirname, 'src/renderer/src/shared') + '/', '@shared': path.resolve(__dirname, 'src/renderer/src/shared'), + '@entities/': path.resolve(__dirname, 'src/renderer/src/entities') + '/', + '@entities': path.resolve(__dirname, 'src/renderer/src/entities'), ui: path.resolve(__dirname, 'src/renderer/src/components'), }, }, From 17d99a5f0e4551b0f03e52f56b1945bc21bc360e Mon Sep 17 00:00:00 2001 From: choihooo Date: Fri, 28 Nov 2025 11:37:45 +0900 Subject: [PATCH 04/12] =?UTF-8?q?refactor(entities):=20=EC=84=B8=EC=85=98?= =?UTF-8?q?=20=ED=83=90=EC=A7=80=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/entities/session/api/index.ts | 7 +++++++ .../session/api/use-create-session-mutation.ts} | 3 ++- .../session/api/use-pause-session-mutation.ts} | 3 ++- .../session/api/use-resume-session-mutation.ts} | 4 ++-- .../session/api/use-save-metrics-mutation.ts} | 3 ++- .../session/api/use-session-report-query.ts} | 3 ++- .../session/api/use-stop-session-mutation.ts} | 3 ++- src/renderer/src/entities/session/index.ts | 14 ++++++++++++++ .../session.ts => entities/session/types/index.ts} | 1 + src/renderer/src/hooks/useAutoMetricsSender.ts | 2 +- src/renderer/src/hooks/useSessionCleanup.ts | 8 +++++--- src/renderer/src/pages/Main/MainPage.tsx | 4 ++-- .../src/pages/Main/components/ExitPanel.tsx | 2 +- .../src/pages/Main/components/WebcamPanel.tsx | 10 ++++++---- .../pages/Onboarding/OnboardingCompletionPage.tsx | 2 +- 15 files changed, 50 insertions(+), 19 deletions(-) create mode 100644 src/renderer/src/entities/session/api/index.ts rename src/renderer/src/{api/session/useCreateSessionMutation.ts => entities/session/api/use-create-session-mutation.ts} (94%) rename src/renderer/src/{api/session/usePauseSessionMutation.ts => entities/session/api/use-pause-session-mutation.ts} (93%) rename src/renderer/src/{api/session/useResumeSessionMutation.ts => entities/session/api/use-resume-session-mutation.ts} (93%) rename src/renderer/src/{api/session/useSaveMetricsMutation.ts => entities/session/api/use-save-metrics-mutation.ts} (97%) rename src/renderer/src/{api/session/useSessionReportQuery.ts => entities/session/api/use-session-report-query.ts} (95%) rename src/renderer/src/{api/session/useStopSessionMutation.ts => entities/session/api/use-stop-session-mutation.ts} (96%) create mode 100644 src/renderer/src/entities/session/index.ts rename src/renderer/src/{types/main/session.ts => entities/session/types/index.ts} (99%) diff --git a/src/renderer/src/entities/session/api/index.ts b/src/renderer/src/entities/session/api/index.ts new file mode 100644 index 0000000..15b1abe --- /dev/null +++ b/src/renderer/src/entities/session/api/index.ts @@ -0,0 +1,7 @@ +export { useCreateSessionMutation } from './use-create-session-mutation'; +export { usePauseSessionMutation } from './use-pause-session-mutation'; +export { useResumeSessionMutation } from './use-resume-session-mutation'; +export { useStopSessionMutation } from './use-stop-session-mutation'; +export { useSaveMetricsMutation } from './use-save-metrics-mutation'; +export { useSessionReportQuery } from './use-session-report-query'; + diff --git a/src/renderer/src/api/session/useCreateSessionMutation.ts b/src/renderer/src/entities/session/api/use-create-session-mutation.ts similarity index 94% rename from src/renderer/src/api/session/useCreateSessionMutation.ts rename to src/renderer/src/entities/session/api/use-create-session-mutation.ts index 59e3a98..77b6a4a 100644 --- a/src/renderer/src/api/session/useCreateSessionMutation.ts +++ b/src/renderer/src/entities/session/api/use-create-session-mutation.ts @@ -1,6 +1,6 @@ import { useMutation } from '@tanstack/react-query'; import api from '@shared/api'; -import { CreateSessionResponse } from '../../types/main/session'; +import { CreateSessionResponse } from '../types'; /** * 세션 생성 API @@ -40,3 +40,4 @@ export const useCreateSessionMutation = () => { }, }); }; + diff --git a/src/renderer/src/api/session/usePauseSessionMutation.ts b/src/renderer/src/entities/session/api/use-pause-session-mutation.ts similarity index 93% rename from src/renderer/src/api/session/usePauseSessionMutation.ts rename to src/renderer/src/entities/session/api/use-pause-session-mutation.ts index 4836905..9da1fca 100644 --- a/src/renderer/src/api/session/usePauseSessionMutation.ts +++ b/src/renderer/src/entities/session/api/use-pause-session-mutation.ts @@ -1,6 +1,6 @@ import { useMutation } from '@tanstack/react-query'; import api from '@shared/api'; -import { SessionActionResponse } from '../../types/main/session'; +import { SessionActionResponse } from '../types'; /** * 세션 일시정지 API @@ -39,3 +39,4 @@ export const usePauseSessionMutation = () => { }, }); }; + diff --git a/src/renderer/src/api/session/useResumeSessionMutation.ts b/src/renderer/src/entities/session/api/use-resume-session-mutation.ts similarity index 93% rename from src/renderer/src/api/session/useResumeSessionMutation.ts rename to src/renderer/src/entities/session/api/use-resume-session-mutation.ts index 6395c2b..e17f685 100644 --- a/src/renderer/src/api/session/useResumeSessionMutation.ts +++ b/src/renderer/src/entities/session/api/use-resume-session-mutation.ts @@ -1,6 +1,6 @@ -import { useMutation } from '@tanstack/react-query'; import api from '@shared/api'; -import { SessionActionResponse } from '../../types/main/session'; +import { useMutation } from '@tanstack/react-query'; +import { SessionActionResponse } from '../types'; /** * 세션 재개 API diff --git a/src/renderer/src/api/session/useSaveMetricsMutation.ts b/src/renderer/src/entities/session/api/use-save-metrics-mutation.ts similarity index 97% rename from src/renderer/src/api/session/useSaveMetricsMutation.ts rename to src/renderer/src/entities/session/api/use-save-metrics-mutation.ts index a8c1bdb..6c27f1b 100644 --- a/src/renderer/src/api/session/useSaveMetricsMutation.ts +++ b/src/renderer/src/entities/session/api/use-save-metrics-mutation.ts @@ -3,7 +3,7 @@ import api from '@shared/api'; import { SaveMetricsRequest, SaveMetricsResponse, -} from '../../types/main/session'; +} from '../types'; /** * 세션 메트릭 저장 API @@ -50,3 +50,4 @@ export const useSaveMetricsMutation = () => { }, }); }; + diff --git a/src/renderer/src/api/session/useSessionReportQuery.ts b/src/renderer/src/entities/session/api/use-session-report-query.ts similarity index 95% rename from src/renderer/src/api/session/useSessionReportQuery.ts rename to src/renderer/src/entities/session/api/use-session-report-query.ts index 333a404..14971a0 100644 --- a/src/renderer/src/api/session/useSessionReportQuery.ts +++ b/src/renderer/src/entities/session/api/use-session-report-query.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import api from '@shared/api'; -import { SessionReportResponse } from '../../types/main/session'; +import { SessionReportResponse } from '../types'; /** * 세션 리포트 조회 API @@ -43,3 +43,4 @@ export const useSessionReportQuery = ( retry: 1, // 실패 시 1번만 재시도 }); }; + diff --git a/src/renderer/src/api/session/useStopSessionMutation.ts b/src/renderer/src/entities/session/api/use-stop-session-mutation.ts similarity index 96% rename from src/renderer/src/api/session/useStopSessionMutation.ts rename to src/renderer/src/entities/session/api/use-stop-session-mutation.ts index df79dfe..a7a92a8 100644 --- a/src/renderer/src/api/session/useStopSessionMutation.ts +++ b/src/renderer/src/entities/session/api/use-stop-session-mutation.ts @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import api from '@shared/api'; -import { SessionActionResponse } from '../../types/main/session'; +import { SessionActionResponse } from '../types'; /** * 세션 중단 API @@ -59,3 +59,4 @@ export const useStopSessionMutation = () => { }, }); }; + diff --git a/src/renderer/src/entities/session/index.ts b/src/renderer/src/entities/session/index.ts new file mode 100644 index 0000000..3bbc8a3 --- /dev/null +++ b/src/renderer/src/entities/session/index.ts @@ -0,0 +1,14 @@ +export * from './api'; +export * from './types'; + +// 타입 명시적 export +export type { + CreateSessionResponse, + SessionActionResponse, + MetricData, + SaveMetricsRequest, + SaveMetricsResponse, + SessionReportData, + SessionReportResponse, +} from './types'; + diff --git a/src/renderer/src/types/main/session.ts b/src/renderer/src/entities/session/types/index.ts similarity index 99% rename from src/renderer/src/types/main/session.ts rename to src/renderer/src/entities/session/types/index.ts index 7fb13d5..f10049b 100644 --- a/src/renderer/src/types/main/session.ts +++ b/src/renderer/src/entities/session/types/index.ts @@ -52,3 +52,4 @@ export interface SessionReportResponse { code: string; message: string; } + diff --git a/src/renderer/src/hooks/useAutoMetricsSender.ts b/src/renderer/src/hooks/useAutoMetricsSender.ts index 1112cb3..12ebb9d 100644 --- a/src/renderer/src/hooks/useAutoMetricsSender.ts +++ b/src/renderer/src/hooks/useAutoMetricsSender.ts @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import { MetricData } from '../types/main/session'; +import type { MetricData } from '@entities/session'; /** * 5분마다 자동으로 메트릭 데이터를 전송하는 훅 diff --git a/src/renderer/src/hooks/useSessionCleanup.ts b/src/renderer/src/hooks/useSessionCleanup.ts index ec42f0c..9231745 100644 --- a/src/renderer/src/hooks/useSessionCleanup.ts +++ b/src/renderer/src/hooks/useSessionCleanup.ts @@ -1,7 +1,9 @@ import { useEffect } from 'react'; -import { useSaveMetricsMutation } from '../api/session/useSaveMetricsMutation'; -import { useStopSessionMutation } from '../api/session/useStopSessionMutation'; -import { MetricData } from '../types/main/session'; +import { + useSaveMetricsMutation, + useStopSessionMutation, +} from '@entities/session'; +import type { MetricData } from '@entities/session'; /** * 창 닫기 시 세션 정리 훅 diff --git a/src/renderer/src/pages/Main/MainPage.tsx b/src/renderer/src/pages/Main/MainPage.tsx index a6590cc..e06e885 100644 --- a/src/renderer/src/pages/Main/MainPage.tsx +++ b/src/renderer/src/pages/Main/MainPage.tsx @@ -6,16 +6,16 @@ import { usePostureStore, WorldLandmark, } from '@entities/posture'; +import type { MetricData } from '@entities/session'; +import { useSaveMetricsMutation } from '@entities/session'; import { useModal } from '@shared/hooks/use-modal'; import { ModalPortal } from '@shared/ui/modal'; import { useEffect, useRef } from 'react'; -import { useSaveMetricsMutation } from '../../api/session/useSaveMetricsMutation'; import NotificationModal from '../../components/Modal/NotificationModal'; import { useAutoMetricsSender } from '../../hooks/useAutoMetricsSender'; import { useNotificationScheduler } from '../../hooks/useNotificationScheduler'; import { useSessionCleanup } from '../../hooks/useSessionCleanup'; import { useCameraStore } from '../../store/useCameraStore'; -import { MetricData } from '../../types/main/session'; import AttendacePanel from './components/AttendacePanel'; import AverageGraphPannel from './components/AverageGraph/AverageGraphPannel'; import AveragePosturePanel from './components/AveragePosture/AveragePosturePanel'; diff --git a/src/renderer/src/pages/Main/components/ExitPanel.tsx b/src/renderer/src/pages/Main/components/ExitPanel.tsx index 419ad88..d31a387 100644 --- a/src/renderer/src/pages/Main/components/ExitPanel.tsx +++ b/src/renderer/src/pages/Main/components/ExitPanel.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from 'react'; import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts'; -import { useSessionReportQuery } from '@api/session/useSessionReportQuery'; +import { useSessionReportQuery } from '@entities/session'; import { useLevelQuery } from '@api/dashboard/useLevelQuery'; const ExitPanel = () => { diff --git a/src/renderer/src/pages/Main/components/WebcamPanel.tsx b/src/renderer/src/pages/Main/components/WebcamPanel.tsx index 0774b1e..9c7779f 100644 --- a/src/renderer/src/pages/Main/components/WebcamPanel.tsx +++ b/src/renderer/src/pages/Main/components/WebcamPanel.tsx @@ -1,10 +1,12 @@ import HideIcon from '@assets/hide.svg?react'; import ShowIcon from '@assets/show.svg?react'; import WidgetIcon from '@assets/widget.svg?react'; -import { useCreateSessionMutation } from '../../../api/session/useCreateSessionMutation'; -import { usePauseSessionMutation } from '../../../api/session/usePauseSessionMutation'; -import { useResumeSessionMutation } from '../../../api/session/useResumeSessionMutation'; -import { useStopSessionMutation } from '../../../api/session/useStopSessionMutation'; +import { + useCreateSessionMutation, + usePauseSessionMutation, + useResumeSessionMutation, + useStopSessionMutation, +} from '@entities/session'; import { Button } from '../../../components'; import { PoseLandmark, diff --git a/src/renderer/src/pages/Onboarding/OnboardingCompletionPage.tsx b/src/renderer/src/pages/Onboarding/OnboardingCompletionPage.tsx index 568a71f..eb04311 100644 --- a/src/renderer/src/pages/Onboarding/OnboardingCompletionPage.tsx +++ b/src/renderer/src/pages/Onboarding/OnboardingCompletionPage.tsx @@ -1,7 +1,7 @@ import { useNavigate } from 'react-router-dom'; import CompletionCharacter from '../../assets/completion.svg?react'; import { Button } from '@shared/ui/button'; -import { useCreateSessionMutation } from '../../api/session/useCreateSessionMutation'; +import { useCreateSessionMutation } from '@entities/session'; import { useCameraStore } from '../../store/useCameraStore'; import { useLevelQuery } from '../../api/dashboard/useLevelQuery'; From d292b41109c56fd4c01d37355b9f24fdd0dd9779 Mon Sep 17 00:00:00 2001 From: choihooo Date: Fri, 28 Nov 2025 11:39:50 +0900 Subject: [PATCH 05/12] =?UTF-8?q?refactor(entities):=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/entities/user/api/index.ts | 10 ++++++++++ .../user/api/use-login-mutation.ts} | 3 ++- .../user/api/use-signup-mutation.ts} | 11 ++--------- .../user/api/use-verify-email-mutation.ts} | 7 ++----- src/renderer/src/entities/user/index.ts | 12 ++++++++++++ src/renderer/src/entities/user/model/index.ts | 2 ++ .../user/model/use-email-store.ts} | 1 + .../mutation.ts => entities/user/types/index.ts} | 13 +++++++++++++ .../src/pages/Login/components/Loginforrm.tsx | 2 +- .../pages/SignUp/EmailVerificationCallbackPage.tsx | 2 +- .../src/pages/SignUp/EmailVerificationPage.tsx | 8 ++++---- .../src/pages/SignUp/ResendVerificationPage.tsx | 4 ++-- .../pages/SignUp/components/EmailHeroSection.tsx | 2 +- .../src/pages/SignUp/components/SignUpform.tsx | 6 +++--- 14 files changed, 56 insertions(+), 27 deletions(-) create mode 100644 src/renderer/src/entities/user/api/index.ts rename src/renderer/src/{api/login/useLoginMutation.ts => entities/user/api/use-login-mutation.ts} (94%) rename src/renderer/src/{api/signup/signup.ts => entities/user/api/use-signup-mutation.ts} (91%) rename src/renderer/src/{api/signup/verifyEmail.ts => entities/user/api/use-verify-email-mutation.ts} (94%) create mode 100644 src/renderer/src/entities/user/index.ts create mode 100644 src/renderer/src/entities/user/model/index.ts rename src/renderer/src/{store/useSignUpStore.ts => entities/user/model/use-email-store.ts} (99%) rename src/renderer/src/{types/login/mutation.ts => entities/user/types/index.ts} (53%) diff --git a/src/renderer/src/entities/user/api/index.ts b/src/renderer/src/entities/user/api/index.ts new file mode 100644 index 0000000..2569c48 --- /dev/null +++ b/src/renderer/src/entities/user/api/index.ts @@ -0,0 +1,10 @@ +export { useLoginMutation } from './use-login-mutation'; +export { + useSignupMutation, + useDuplicatedEmailMutation, +} from './use-signup-mutation'; +export { + useVerifyEmailMutation, + useResendVerifyEmailMuation, +} from './use-verify-email-mutation'; + diff --git a/src/renderer/src/api/login/useLoginMutation.ts b/src/renderer/src/entities/user/api/use-login-mutation.ts similarity index 94% rename from src/renderer/src/api/login/useLoginMutation.ts rename to src/renderer/src/entities/user/api/use-login-mutation.ts index a944a05..2804ba2 100644 --- a/src/renderer/src/api/login/useLoginMutation.ts +++ b/src/renderer/src/entities/user/api/use-login-mutation.ts @@ -1,7 +1,7 @@ import { useMutation } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import api from '@shared/api'; -import { LoginInput, LoginResponse } from '../../types/login/mutation'; +import { LoginInput, LoginResponse } from '../types'; /*로그인 api */ const login = async (data: LoginInput): Promise => { @@ -45,3 +45,4 @@ export const useLoginMutation = () => { }, }); }; + diff --git a/src/renderer/src/api/signup/signup.ts b/src/renderer/src/entities/user/api/use-signup-mutation.ts similarity index 91% rename from src/renderer/src/api/signup/signup.ts rename to src/renderer/src/entities/user/api/use-signup-mutation.ts index 43eb797..01ab38d 100644 --- a/src/renderer/src/api/signup/signup.ts +++ b/src/renderer/src/entities/user/api/use-signup-mutation.ts @@ -1,14 +1,7 @@ +import api from '@shared/api'; import { useMutation } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; -import api from '@shared/api'; - -export interface SignupRequest { - email: string; - password: string; - name: string; - callbackUrl: string; - avatar?: string; -} +import { SignupRequest } from '../types'; /* 이메일 중복 확인 api */ const duplicatedEmail = async (email: string) => { diff --git a/src/renderer/src/api/signup/verifyEmail.ts b/src/renderer/src/entities/user/api/use-verify-email-mutation.ts similarity index 94% rename from src/renderer/src/api/signup/verifyEmail.ts rename to src/renderer/src/entities/user/api/use-verify-email-mutation.ts index 9d309a8..ef65101 100644 --- a/src/renderer/src/api/signup/verifyEmail.ts +++ b/src/renderer/src/entities/user/api/use-verify-email-mutation.ts @@ -1,11 +1,7 @@ import { useMutation } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import api from '@shared/api'; - -export interface ResendVerifyEmailRequest { - email: string; - callbackUrl: string; -} +import { ResendVerifyEmailRequest } from '../types'; /*이메일 인증 api*/ const verifyEmail = async (token: string) => { @@ -67,3 +63,4 @@ export const useResendVerifyEmailMuation = () => { }, }); }; + diff --git a/src/renderer/src/entities/user/index.ts b/src/renderer/src/entities/user/index.ts new file mode 100644 index 0000000..15a3039 --- /dev/null +++ b/src/renderer/src/entities/user/index.ts @@ -0,0 +1,12 @@ +export * from './api'; +export * from './model'; +export * from './types'; + +// 타입 명시적 export +export type { + LoginInput, + LoginResponse, + SignupRequest, + ResendVerifyEmailRequest, +} from './types'; + diff --git a/src/renderer/src/entities/user/model/index.ts b/src/renderer/src/entities/user/model/index.ts new file mode 100644 index 0000000..54fa135 --- /dev/null +++ b/src/renderer/src/entities/user/model/index.ts @@ -0,0 +1,2 @@ +export { useEmailStore } from './use-email-store'; + diff --git a/src/renderer/src/store/useSignUpStore.ts b/src/renderer/src/entities/user/model/use-email-store.ts similarity index 99% rename from src/renderer/src/store/useSignUpStore.ts rename to src/renderer/src/entities/user/model/use-email-store.ts index 44bdb6f..7e97df0 100644 --- a/src/renderer/src/store/useSignUpStore.ts +++ b/src/renderer/src/entities/user/model/use-email-store.ts @@ -18,3 +18,4 @@ export const useEmailStore = create()( }, ), ); + diff --git a/src/renderer/src/types/login/mutation.ts b/src/renderer/src/entities/user/types/index.ts similarity index 53% rename from src/renderer/src/types/login/mutation.ts rename to src/renderer/src/entities/user/types/index.ts index b8c1f79..489de81 100644 --- a/src/renderer/src/types/login/mutation.ts +++ b/src/renderer/src/entities/user/types/index.ts @@ -13,3 +13,16 @@ export interface LoginResponse { code: string; message: string; } + +export interface SignupRequest { + email: string; + password: string; + name: string; + callbackUrl: string; + avatar?: string; +} + +export interface ResendVerifyEmailRequest { + email: string; + callbackUrl: string; +} diff --git a/src/renderer/src/pages/Login/components/Loginforrm.tsx b/src/renderer/src/pages/Login/components/Loginforrm.tsx index 063d122..266b814 100644 --- a/src/renderer/src/pages/Login/components/Loginforrm.tsx +++ b/src/renderer/src/pages/Login/components/Loginforrm.tsx @@ -5,7 +5,7 @@ import SaveIdIcon from '../../../assets/auth/saveid_icon.svg?react'; import LoginButton from './LoginButton'; import PasswordField from './PasswordField'; import { useNavigate } from 'react-router-dom'; -import { useLoginMutation } from '../../../api/login/useLoginMutation'; +import { useLoginMutation } from '@entities/user'; import FailIcon from '../../../assets/auth/error_icon.svg?react'; interface LoginFormData { diff --git a/src/renderer/src/pages/SignUp/EmailVerificationCallbackPage.tsx b/src/renderer/src/pages/SignUp/EmailVerificationCallbackPage.tsx index c6facbb..d4f6603 100644 --- a/src/renderer/src/pages/SignUp/EmailVerificationCallbackPage.tsx +++ b/src/renderer/src/pages/SignUp/EmailVerificationCallbackPage.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { useVerifyEmailMutation } from '../../api/signup/verifyEmail'; +import { useVerifyEmailMutation } from '@entities/user'; import CompletionCharacter from '../../assets/completion.svg?react'; const EmailVerificationCallbackPage = () => { diff --git a/src/renderer/src/pages/SignUp/EmailVerificationPage.tsx b/src/renderer/src/pages/SignUp/EmailVerificationPage.tsx index 9c695aa..85b6899 100644 --- a/src/renderer/src/pages/SignUp/EmailVerificationPage.tsx +++ b/src/renderer/src/pages/SignUp/EmailVerificationPage.tsx @@ -1,9 +1,9 @@ import { useNavigate } from 'react-router-dom'; -import { - useResendVerifyEmailMuation -} from '../../api/signup/verifyEmail'; import { Button } from '@shared/ui/button'; -import { useEmailStore } from '../../store/useSignUpStore'; +import { + useResendVerifyEmailMuation, + useEmailStore, +} from '@entities/user'; import EmailHeroSection from './components/EmailHeroSection'; import ResendSection from './components/ResendSection'; diff --git a/src/renderer/src/pages/SignUp/ResendVerificationPage.tsx b/src/renderer/src/pages/SignUp/ResendVerificationPage.tsx index 5165796..96835b5 100644 --- a/src/renderer/src/pages/SignUp/ResendVerificationPage.tsx +++ b/src/renderer/src/pages/SignUp/ResendVerificationPage.tsx @@ -6,8 +6,8 @@ import { useSearchParams } from 'react-router-dom'; import { useResendVerifyEmailMuation, useVerifyEmailMutation, -} from '../../api/signup/verifyEmail'; -import { useEmailStore } from '../../store/useSignUpStore'; +} from '@entities/user'; +import { useEmailStore } from '@entities/user'; const ResendVerificationPage = () => { const [searchParams] = useSearchParams(); diff --git a/src/renderer/src/pages/SignUp/components/EmailHeroSection.tsx b/src/renderer/src/pages/SignUp/components/EmailHeroSection.tsx index 9499ea2..ac1d0e5 100644 --- a/src/renderer/src/pages/SignUp/components/EmailHeroSection.tsx +++ b/src/renderer/src/pages/SignUp/components/EmailHeroSection.tsx @@ -1,5 +1,5 @@ +import { useEmailStore } from '@entities/user'; import EmailIcon from '../../../assets/auth/email_icon.svg?react'; -import { useEmailStore } from '../../../store/useSignUpStore'; export default function EmailHeroSection() { const email = useEmailStore((state) => state.email); diff --git a/src/renderer/src/pages/SignUp/components/SignUpform.tsx b/src/renderer/src/pages/SignUp/components/SignUpform.tsx index 4a76a8d..90c1a72 100644 --- a/src/renderer/src/pages/SignUp/components/SignUpform.tsx +++ b/src/renderer/src/pages/SignUp/components/SignUpform.tsx @@ -6,12 +6,12 @@ import PasswordField from '../../Login/components/PasswordField'; import SuccessIcon from '../../../assets/auth/success_icon.svg?react'; import FailIcon from '../../../assets/auth/error_icon.svg?react'; import { signUpSchema, SignUpFormData } from '../utils/SignupSchemas'; +import { useState } from 'react'; import { useDuplicatedEmailMutation, useSignupMutation, -} from '../../../api/signup/signup'; -import { useState } from 'react'; -import { useEmailStore } from '../../../store/useSignUpStore'; + useEmailStore, +} from '@entities/user'; const SignUpForm = () => { const { mutate: checkDuplicateEmail } = useDuplicatedEmailMutation(); From 57cda320d7787515c302f46723cf966a63035225 Mon Sep 17 00:00:00 2001 From: choihooo Date: Fri, 28 Nov 2025 11:42:08 +0900 Subject: [PATCH 06/12] =?UTF-8?q?refactor(entities):=20=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/entities/dashboard/api/index.ts | 7 ++ .../dashboard/api/use-attendance-query.ts} | 5 +- .../dashboard/api/use-average-score-query.ts} | 3 +- .../dashboard/api/use-highlight-query.ts} | 5 +- .../dashboard/api/use-level-query.ts} | 3 +- .../dashboard/api/use-posture-graph-query.ts} | 3 +- .../api/use-posture-pattern-query.ts} | 3 +- src/renderer/src/entities/dashboard/index.ts | 23 +++++ .../src/entities/dashboard/types/index.ts | 97 +++++++++++++++++++ .../pages/Main/components/AttendacePanel.tsx | 2 +- .../hooks/useAverageGraphChart.ts | 2 +- .../AveragePosture/AveragePosturePanel.tsx | 2 +- .../src/pages/Main/components/ExitPanel.tsx | 2 +- .../hooks/useHighlightsChart.ts | 2 +- .../Main/components/PosePatternPanel.tsx | 2 +- .../Main/components/TotalDistancePanel.tsx | 2 +- .../src/pages/Main/components/WebcamPanel.tsx | 2 +- .../Onboarding/OnboardingCompletionPage.tsx | 2 +- .../src/types/dashboard/attendance.ts | 23 ----- .../src/types/dashboard/averageScore.ts | 11 --- src/renderer/src/types/dashboard/highlight.ts | 20 ---- src/renderer/src/types/dashboard/level.ts | 13 --- .../src/types/dashboard/postureGraph.ts | 11 --- .../src/types/dashboard/posturePattern.ts | 16 --- 24 files changed, 149 insertions(+), 112 deletions(-) create mode 100644 src/renderer/src/entities/dashboard/api/index.ts rename src/renderer/src/{api/dashboard/useAttendanceQuery.ts => entities/dashboard/api/use-attendance-query.ts} (97%) rename src/renderer/src/{api/dashboard/useAverageScoreQuery.ts => entities/dashboard/api/use-average-score-query.ts} (91%) rename src/renderer/src/{api/dashboard/useHighlightQuery.ts => entities/dashboard/api/use-highlight-query.ts} (97%) rename src/renderer/src/{api/dashboard/useLevelQuery.ts => entities/dashboard/api/use-level-query.ts} (92%) rename src/renderer/src/{api/dashboard/usePostureGraphQuery.ts => entities/dashboard/api/use-posture-graph-query.ts} (92%) rename src/renderer/src/{api/dashboard/usePosturePatternQuery.ts => entities/dashboard/api/use-posture-pattern-query.ts} (92%) create mode 100644 src/renderer/src/entities/dashboard/index.ts create mode 100644 src/renderer/src/entities/dashboard/types/index.ts delete mode 100644 src/renderer/src/types/dashboard/attendance.ts delete mode 100644 src/renderer/src/types/dashboard/averageScore.ts delete mode 100644 src/renderer/src/types/dashboard/highlight.ts delete mode 100644 src/renderer/src/types/dashboard/level.ts delete mode 100644 src/renderer/src/types/dashboard/postureGraph.ts delete mode 100644 src/renderer/src/types/dashboard/posturePattern.ts diff --git a/src/renderer/src/entities/dashboard/api/index.ts b/src/renderer/src/entities/dashboard/api/index.ts new file mode 100644 index 0000000..f8599ad --- /dev/null +++ b/src/renderer/src/entities/dashboard/api/index.ts @@ -0,0 +1,7 @@ +export { useAttendanceQuery } from './use-attendance-query'; +export { useAverageScoreQuery } from './use-average-score-query'; +export { useHighlightQuery } from './use-highlight-query'; +export { useLevelQuery } from './use-level-query'; +export { usePostureGraphQuery } from './use-posture-graph-query'; +export { usePosturePatternQuery } from './use-posture-pattern-query'; + diff --git a/src/renderer/src/api/dashboard/useAttendanceQuery.ts b/src/renderer/src/entities/dashboard/api/use-attendance-query.ts similarity index 97% rename from src/renderer/src/api/dashboard/useAttendanceQuery.ts rename to src/renderer/src/entities/dashboard/api/use-attendance-query.ts index aabefc8..1ae2a20 100644 --- a/src/renderer/src/api/dashboard/useAttendanceQuery.ts +++ b/src/renderer/src/entities/dashboard/api/use-attendance-query.ts @@ -1,9 +1,9 @@ import { useQuery } from '@tanstack/react-query'; +import api from '@shared/api'; import { AttendanceQueryParams, AttendanceResponse, -} from '../../types/dashboard/attendance'; -import api from '@shared/api'; +} from '../types'; /** * 출석 현황 조회 API @@ -58,3 +58,4 @@ export const useAttendanceQuery = ( retry: 1, // 실패 시 1번만 재시도 }); }; + diff --git a/src/renderer/src/api/dashboard/useAverageScoreQuery.ts b/src/renderer/src/entities/dashboard/api/use-average-score-query.ts similarity index 91% rename from src/renderer/src/api/dashboard/useAverageScoreQuery.ts rename to src/renderer/src/entities/dashboard/api/use-average-score-query.ts index 70d53d0..69d7853 100644 --- a/src/renderer/src/api/dashboard/useAverageScoreQuery.ts +++ b/src/renderer/src/entities/dashboard/api/use-average-score-query.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import api from '@shared/api'; -import { AverageScoreResponse } from '../../types/dashboard/averageScore'; +import { AverageScoreResponse } from '../types'; /** * 평균 자세 점수 조회 API @@ -31,3 +31,4 @@ export const useAverageScoreQuery = () => { queryFn: getAverageScore, }); }; + diff --git a/src/renderer/src/api/dashboard/useHighlightQuery.ts b/src/renderer/src/entities/dashboard/api/use-highlight-query.ts similarity index 97% rename from src/renderer/src/api/dashboard/useHighlightQuery.ts rename to src/renderer/src/entities/dashboard/api/use-highlight-query.ts index faae8c2..c5dd01d 100644 --- a/src/renderer/src/api/dashboard/useHighlightQuery.ts +++ b/src/renderer/src/entities/dashboard/api/use-highlight-query.ts @@ -1,9 +1,9 @@ import { useQuery } from '@tanstack/react-query'; +import api from '@shared/api'; import { HighlightQueryParams, HighlightResponse, -} from '../../types/dashboard/highlight'; -import api from '@shared/api'; +} from '../types'; /** * 하이라이트 조회 API @@ -58,3 +58,4 @@ export const useHighlightQuery = ( retry: 1, // 실패 시 1번만 재시도 }); }; + diff --git a/src/renderer/src/api/dashboard/useLevelQuery.ts b/src/renderer/src/entities/dashboard/api/use-level-query.ts similarity index 92% rename from src/renderer/src/api/dashboard/useLevelQuery.ts rename to src/renderer/src/entities/dashboard/api/use-level-query.ts index d17f243..6003e1c 100644 --- a/src/renderer/src/api/dashboard/useLevelQuery.ts +++ b/src/renderer/src/entities/dashboard/api/use-level-query.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import api from '@shared/api'; -import { LevelResponse } from '../../types/dashboard/level'; +import { LevelResponse } from '../types'; /** * 레벨 도달 현황 조회 API @@ -31,3 +31,4 @@ export const useLevelQuery = () => { queryFn: getLevel, }); }; + diff --git a/src/renderer/src/api/dashboard/usePostureGraphQuery.ts b/src/renderer/src/entities/dashboard/api/use-posture-graph-query.ts similarity index 92% rename from src/renderer/src/api/dashboard/usePostureGraphQuery.ts rename to src/renderer/src/entities/dashboard/api/use-posture-graph-query.ts index a5db4c7..afd2098 100644 --- a/src/renderer/src/api/dashboard/usePostureGraphQuery.ts +++ b/src/renderer/src/entities/dashboard/api/use-posture-graph-query.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import api from '@shared/api'; -import { PostureGraphResponse } from '../../types/dashboard/postureGraph'; +import { PostureGraphResponse } from '../types'; /** * 바른 자세 점수 그래프 조회 API (최근 31일) @@ -32,3 +32,4 @@ export const usePostureGraphQuery = () => { queryFn: getPostureGraph, }); }; + diff --git a/src/renderer/src/api/dashboard/usePosturePatternQuery.ts b/src/renderer/src/entities/dashboard/api/use-posture-pattern-query.ts similarity index 92% rename from src/renderer/src/api/dashboard/usePosturePatternQuery.ts rename to src/renderer/src/entities/dashboard/api/use-posture-pattern-query.ts index 2c291cb..f837ee6 100644 --- a/src/renderer/src/api/dashboard/usePosturePatternQuery.ts +++ b/src/renderer/src/entities/dashboard/api/use-posture-pattern-query.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import api from '@shared/api'; -import { PosturePatternResponse } from '../../types/dashboard/posturePattern'; +import { PosturePatternResponse } from '../types'; /** * 자세 패턴 분석 조회 API @@ -35,4 +35,3 @@ export const usePosturePatternQuery = () => { }); }; - diff --git a/src/renderer/src/entities/dashboard/index.ts b/src/renderer/src/entities/dashboard/index.ts new file mode 100644 index 0000000..8c5b070 --- /dev/null +++ b/src/renderer/src/entities/dashboard/index.ts @@ -0,0 +1,23 @@ +export * from './api'; +export * from './types'; + +// 타입 명시적 export +export type { + AttendancePeriod, + AttendanceData, + AttendanceResponse, + AttendanceQueryParams, + AverageScoreData, + AverageScoreResponse, + HighlightPeriod, + HighlightData, + HighlightResponse, + HighlightQueryParams, + LevelData, + LevelResponse, + PostureGraphData, + PostureGraphResponse, + PosturePatternData, + PosturePatternResponse, +} from './types'; + diff --git a/src/renderer/src/entities/dashboard/types/index.ts b/src/renderer/src/entities/dashboard/types/index.ts new file mode 100644 index 0000000..b15df45 --- /dev/null +++ b/src/renderer/src/entities/dashboard/types/index.ts @@ -0,0 +1,97 @@ +export type AttendancePeriod = 'WEEKLY' | 'MONTHLY' | 'YEARLY'; + +export interface AttendanceData { + attendances: Record; // 날짜별 레벨 값 (예: "2025-01-01": 3) + title: string; + content1: string; + content2: string; + subContent: string; +} + +export interface AttendanceResponse { + timestamp: string; + success: boolean; + data: AttendanceData; + code: string; + message: string; +} + +export interface AttendanceQueryParams { + period: AttendancePeriod; + year: number; + month?: number; +} + +export interface AverageScoreData { + score: number; +} + +export interface AverageScoreResponse { + timestamp: string; + success: boolean; + data: AverageScoreData; + code: string; + message: string | null; +} + +export type HighlightPeriod = 'WEEKLY' | 'MONTHLY' | 'YEARLY'; + +export interface HighlightData { + current: number; + previous: number; +} + +export interface HighlightResponse { + timestamp: string; + success: boolean; + data: HighlightData; + code: string; + message: string; +} + +export interface HighlightQueryParams { + period: HighlightPeriod; + year: number; + month?: number; +} + +export interface LevelData { + level: number; + current: number; + required: number; +} + +export interface LevelResponse { + timestamp: string; + success: boolean; + data: LevelData; + code: string; + message: string | null; +} + +export interface PostureGraphData { + points: Record; +} + +export interface PostureGraphResponse { + timestamp: string; + success: boolean; + data: PostureGraphData; + code: string; + message: string | null; +} + +export interface PosturePatternData { + worstTime: string; // "14:00:00" 형식 + worstDay: string; // "FRIDAY" 형식 + recovery: number; // 회복까지 평균 시간 (분) + stretching: string; // 스트레칭 추천 +} + +export interface PosturePatternResponse { + timestamp: string; + success: boolean; + data: PosturePatternData; + code: string; + message: string; +} diff --git a/src/renderer/src/pages/Main/components/AttendacePanel.tsx b/src/renderer/src/pages/Main/components/AttendacePanel.tsx index 096e078..acfd814 100644 --- a/src/renderer/src/pages/Main/components/AttendacePanel.tsx +++ b/src/renderer/src/pages/Main/components/AttendacePanel.tsx @@ -1,7 +1,7 @@ import DownIcon from '@assets/arrow-narrow-down.svg?react'; import UpIcon from '@assets/arrow-narrow-up.svg?react'; import { useState } from 'react'; -import { useAttendanceQuery } from '../../../api/dashboard/useAttendanceQuery'; +import { useAttendanceQuery } from '@entities/dashboard'; import { IntensitySlider } from '@shared/ui/intensity-slider'; import { PageMoveButton } from '@shared/ui/page-move-button'; import { PannelHeader } from '@shared/ui/panel-header'; diff --git a/src/renderer/src/pages/Main/components/AverageGraph/hooks/useAverageGraphChart.ts b/src/renderer/src/pages/Main/components/AverageGraph/hooks/useAverageGraphChart.ts index 74ee8e1..9bfac26 100644 --- a/src/renderer/src/pages/Main/components/AverageGraph/hooks/useAverageGraphChart.ts +++ b/src/renderer/src/pages/Main/components/AverageGraph/hooks/useAverageGraphChart.ts @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from 'react'; import { getColor } from '@shared/lib/get-color'; -import { usePostureGraphQuery } from '@api/dashboard/usePostureGraphQuery'; +import { usePostureGraphQuery } from '@entities/dashboard'; type AverageGraphDatum = { periodLabel: string; diff --git a/src/renderer/src/pages/Main/components/AveragePosture/AveragePosturePanel.tsx b/src/renderer/src/pages/Main/components/AveragePosture/AveragePosturePanel.tsx index d8eb6b6..764b9d0 100644 --- a/src/renderer/src/pages/Main/components/AveragePosture/AveragePosturePanel.tsx +++ b/src/renderer/src/pages/Main/components/AveragePosture/AveragePosturePanel.tsx @@ -1,4 +1,4 @@ -import { useAverageScoreQuery } from '../../../../api/dashboard/useAverageScoreQuery'; +import { useAverageScoreQuery } from '@entities/dashboard'; import { LEVEL_INFO, getLevel } from './levelConfig'; const AveragePosturePanel = () => { diff --git a/src/renderer/src/pages/Main/components/ExitPanel.tsx b/src/renderer/src/pages/Main/components/ExitPanel.tsx index d31a387..9634d42 100644 --- a/src/renderer/src/pages/Main/components/ExitPanel.tsx +++ b/src/renderer/src/pages/Main/components/ExitPanel.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from 'react'; import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts'; import { useSessionReportQuery } from '@entities/session'; -import { useLevelQuery } from '@api/dashboard/useLevelQuery'; +import { useLevelQuery } from '@entities/dashboard'; const ExitPanel = () => { const [sessionId, setSessionId] = useState(null); diff --git a/src/renderer/src/pages/Main/components/HighlightsPanel/hooks/useHighlightsChart.ts b/src/renderer/src/pages/Main/components/HighlightsPanel/hooks/useHighlightsChart.ts index c400714..8f5a019 100644 --- a/src/renderer/src/pages/Main/components/HighlightsPanel/hooks/useHighlightsChart.ts +++ b/src/renderer/src/pages/Main/components/HighlightsPanel/hooks/useHighlightsChart.ts @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; -import { useHighlightQuery } from '../../../../../api/dashboard/useHighlightQuery'; +import { useHighlightQuery } from '@entities/dashboard'; import { getColor } from '@shared/lib/get-color'; import type { HighlightDatum } from '../data'; diff --git a/src/renderer/src/pages/Main/components/PosePatternPanel.tsx b/src/renderer/src/pages/Main/components/PosePatternPanel.tsx index 8c97022..91c91f0 100644 --- a/src/renderer/src/pages/Main/components/PosePatternPanel.tsx +++ b/src/renderer/src/pages/Main/components/PosePatternPanel.tsx @@ -5,7 +5,7 @@ import ChevronRigthIcon from '@assets/chevron-right.svg?react'; import ClockIcon from '@assets/clock.svg?react'; import GlassHourIcon from '@assets/hourglass.svg?react'; import TumbupIcon from '@assets/thumbup.svg?react'; -import { usePosturePatternQuery } from '../../../api/dashboard/usePosturePatternQuery'; +import { usePosturePatternQuery } from '@entities/dashboard'; import { PannelHeader } from '@shared/ui/panel-header'; // 시간 형식 변환: "14:00:00" -> "오후 2시" diff --git a/src/renderer/src/pages/Main/components/TotalDistancePanel.tsx b/src/renderer/src/pages/Main/components/TotalDistancePanel.tsx index 95f588c..a81981e 100644 --- a/src/renderer/src/pages/Main/components/TotalDistancePanel.tsx +++ b/src/renderer/src/pages/Main/components/TotalDistancePanel.tsx @@ -1,5 +1,5 @@ import { PannelHeader } from '@shared/ui/panel-header'; -import { useLevelQuery } from '../../../api/dashboard/useLevelQuery'; +import { useLevelQuery } from '@entities/dashboard'; import AchivementMedal from '../../../assets/main/achivement_meadl.svg?react'; import { useModal } from '@shared/hooks/use-modal'; import TotalDistanceModal from './TotalDistanceModal'; diff --git a/src/renderer/src/pages/Main/components/WebcamPanel.tsx b/src/renderer/src/pages/Main/components/WebcamPanel.tsx index 9c7779f..00b9c8f 100644 --- a/src/renderer/src/pages/Main/components/WebcamPanel.tsx +++ b/src/renderer/src/pages/Main/components/WebcamPanel.tsx @@ -15,7 +15,7 @@ import { import { useWidget } from '../../../hooks/useWidget'; import { useCameraStore } from '../../../store/useCameraStore'; import WebcamView from '../../Calibration/components/WebcamView'; -import { useLevelQuery } from '../../../api/dashboard/useLevelQuery'; +import { useLevelQuery } from '@entities/dashboard'; interface Props { onUserMediaError: (e: string | DOMException) => void; diff --git a/src/renderer/src/pages/Onboarding/OnboardingCompletionPage.tsx b/src/renderer/src/pages/Onboarding/OnboardingCompletionPage.tsx index eb04311..2f76ca5 100644 --- a/src/renderer/src/pages/Onboarding/OnboardingCompletionPage.tsx +++ b/src/renderer/src/pages/Onboarding/OnboardingCompletionPage.tsx @@ -3,7 +3,7 @@ import CompletionCharacter from '../../assets/completion.svg?react'; import { Button } from '@shared/ui/button'; import { useCreateSessionMutation } from '@entities/session'; import { useCameraStore } from '../../store/useCameraStore'; -import { useLevelQuery } from '../../api/dashboard/useLevelQuery'; +import { useLevelQuery } from '@entities/dashboard'; const OnboardingCompletionPage = () => { const navigate = useNavigate(); diff --git a/src/renderer/src/types/dashboard/attendance.ts b/src/renderer/src/types/dashboard/attendance.ts deleted file mode 100644 index 89b486b..0000000 --- a/src/renderer/src/types/dashboard/attendance.ts +++ /dev/null @@ -1,23 +0,0 @@ -export type AttendancePeriod = 'WEEKLY' | 'MONTHLY' | 'YEARLY'; - -export interface AttendanceData { - attendances: Record; // 날짜별 레벨 값 (예: "2025-01-01": 3) - title: string; - content1: string; - content2: string; - subContent: string; -} - -export interface AttendanceResponse { - timestamp: string; - success: boolean; - data: AttendanceData; - code: string; - message: string; -} - -export interface AttendanceQueryParams { - period: AttendancePeriod; - year: number; - month?: number; -} diff --git a/src/renderer/src/types/dashboard/averageScore.ts b/src/renderer/src/types/dashboard/averageScore.ts deleted file mode 100644 index 4a0c67c..0000000 --- a/src/renderer/src/types/dashboard/averageScore.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface AverageScoreData { - score: number; -} - -export interface AverageScoreResponse { - timestamp: string; - success: boolean; - data: AverageScoreData; - code: string; - message: string | null; -} diff --git a/src/renderer/src/types/dashboard/highlight.ts b/src/renderer/src/types/dashboard/highlight.ts deleted file mode 100644 index 3b81488..0000000 --- a/src/renderer/src/types/dashboard/highlight.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type HighlightPeriod = 'WEEKLY' | 'MONTHLY' | 'YEARLY'; - -export interface HighlightData { - current: number; - previous: number; -} - -export interface HighlightResponse { - timestamp: string; - success: boolean; - data: HighlightData; - code: string; - message: string; -} - -export interface HighlightQueryParams { - period: HighlightPeriod; - year: number; - month?: number; -} diff --git a/src/renderer/src/types/dashboard/level.ts b/src/renderer/src/types/dashboard/level.ts deleted file mode 100644 index 9c4e91e..0000000 --- a/src/renderer/src/types/dashboard/level.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface LevelData { - level: number; - current: number; - required: number; -} - -export interface LevelResponse { - timestamp: string; - success: boolean; - data: LevelData; - code: string; - message: string | null; -} diff --git a/src/renderer/src/types/dashboard/postureGraph.ts b/src/renderer/src/types/dashboard/postureGraph.ts deleted file mode 100644 index b793259..0000000 --- a/src/renderer/src/types/dashboard/postureGraph.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface PostureGraphData { - points: Record; -} - -export interface PostureGraphResponse { - timestamp: string; - success: boolean; - data: PostureGraphData; - code: string; - message: string | null; -} diff --git a/src/renderer/src/types/dashboard/posturePattern.ts b/src/renderer/src/types/dashboard/posturePattern.ts deleted file mode 100644 index 30d1583..0000000 --- a/src/renderer/src/types/dashboard/posturePattern.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface PosturePatternData { - worstTime: string; // "14:00:00" 형식 - worstDay: string; // "FRIDAY" 형식 - recovery: number; // 회복까지 평균 시간 (분) - stretching: string; // 스트레칭 추천 -} - -export interface PosturePatternResponse { - timestamp: string; - success: boolean; - data: PosturePatternData; - code: string; - message: string; -} - - From f1bc9592cf0bdec86bf712f51501a5925049e5b8 Mon Sep 17 00:00:00 2001 From: choihooo Date: Fri, 28 Nov 2025 11:45:22 +0900 Subject: [PATCH 07/12] =?UTF-8?q?refactor(feature):=20auth=20=ED=94=BC?= =?UTF-8?q?=EC=B3=90=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/features/auth/index.ts | 2 ++ .../Login => features/auth/ui/login}/LoginPage.tsx | 0 .../auth/ui/login}/components/HeroSection.tsx | 4 ++-- .../auth/ui/login}/components/LoginButton.tsx | 0 .../auth/ui/login}/components/Loginforrm.tsx | 12 ++++++------ .../auth/ui/login}/components/PasswordField.tsx | 6 +++--- src/renderer/src/features/auth/ui/login/index.ts | 2 ++ .../ui/signup}/EmailVerificationCallbackPage.tsx | 2 +- .../auth/ui/signup}/EmailVerificationPage.tsx | 0 .../auth/ui/signup}/ResendVerificationPage.tsx | 0 .../auth/ui/signup}/SignUpPage.tsx | 0 .../auth/ui/signup}/components/EmailHeroSection.tsx | 2 +- .../ui/signup}/components/ResendEmailHeroSection.tsx | 0 .../auth/ui/signup}/components/ResendSection.tsx | 0 .../auth/ui/signup}/components/SignUpform.tsx | 6 +++--- .../auth/ui/signup}/components/VerifyAction.tsx | 0 src/renderer/src/features/auth/ui/signup/index.ts | 4 ++++ .../auth/ui/signup}/utils/SignupSchemas.ts | 0 src/renderer/src/shared/config/router.tsx | 6 +----- src/renderer/tsconfig.json | 3 ++- vite.config.mts | 2 ++ 21 files changed, 29 insertions(+), 22 deletions(-) create mode 100644 src/renderer/src/features/auth/index.ts rename src/renderer/src/{pages/Login => features/auth/ui/login}/LoginPage.tsx (100%) rename src/renderer/src/{pages/Login => features/auth/ui/login}/components/HeroSection.tsx (82%) rename src/renderer/src/{pages/Login => features/auth/ui/login}/components/LoginButton.tsx (100%) rename src/renderer/src/{pages/Login => features/auth/ui/login}/components/Loginforrm.tsx (96%) rename src/renderer/src/{pages/Login => features/auth/ui/login}/components/PasswordField.tsx (88%) create mode 100644 src/renderer/src/features/auth/ui/login/index.ts rename src/renderer/src/{pages/SignUp => features/auth/ui/signup}/EmailVerificationCallbackPage.tsx (96%) rename src/renderer/src/{pages/SignUp => features/auth/ui/signup}/EmailVerificationPage.tsx (100%) rename src/renderer/src/{pages/SignUp => features/auth/ui/signup}/ResendVerificationPage.tsx (100%) rename src/renderer/src/{pages/SignUp => features/auth/ui/signup}/SignUpPage.tsx (100%) rename src/renderer/src/{pages/SignUp => features/auth/ui/signup}/components/EmailHeroSection.tsx (93%) rename src/renderer/src/{pages/SignUp => features/auth/ui/signup}/components/ResendEmailHeroSection.tsx (100%) rename src/renderer/src/{pages/SignUp => features/auth/ui/signup}/components/ResendSection.tsx (100%) rename src/renderer/src/{pages/SignUp => features/auth/ui/signup}/components/SignUpform.tsx (97%) rename src/renderer/src/{pages/SignUp => features/auth/ui/signup}/components/VerifyAction.tsx (100%) create mode 100644 src/renderer/src/features/auth/ui/signup/index.ts rename src/renderer/src/{pages/SignUp => features/auth/ui/signup}/utils/SignupSchemas.ts (100%) diff --git a/src/renderer/src/features/auth/index.ts b/src/renderer/src/features/auth/index.ts new file mode 100644 index 0000000..d07f8de --- /dev/null +++ b/src/renderer/src/features/auth/index.ts @@ -0,0 +1,2 @@ +export * from './ui/login'; +export * from './ui/signup'; diff --git a/src/renderer/src/pages/Login/LoginPage.tsx b/src/renderer/src/features/auth/ui/login/LoginPage.tsx similarity index 100% rename from src/renderer/src/pages/Login/LoginPage.tsx rename to src/renderer/src/features/auth/ui/login/LoginPage.tsx diff --git a/src/renderer/src/pages/Login/components/HeroSection.tsx b/src/renderer/src/features/auth/ui/login/components/HeroSection.tsx similarity index 82% rename from src/renderer/src/pages/Login/components/HeroSection.tsx rename to src/renderer/src/features/auth/ui/login/components/HeroSection.tsx index 145c4c6..82feec4 100644 --- a/src/renderer/src/pages/Login/components/HeroSection.tsx +++ b/src/renderer/src/features/auth/ui/login/components/HeroSection.tsx @@ -1,5 +1,5 @@ -import Logo from '../../../assets/logo.svg?react'; -import Symbol from "../../../assets/symbol.svg?react"; +import Logo from '../../../../assets/logo.svg?react'; +import Symbol from '../../../../assets/symbol.svg?react'; export default function HeroSection() { return ( diff --git a/src/renderer/src/pages/Login/components/LoginButton.tsx b/src/renderer/src/features/auth/ui/login/components/LoginButton.tsx similarity index 100% rename from src/renderer/src/pages/Login/components/LoginButton.tsx rename to src/renderer/src/features/auth/ui/login/components/LoginButton.tsx diff --git a/src/renderer/src/pages/Login/components/Loginforrm.tsx b/src/renderer/src/features/auth/ui/login/components/Loginforrm.tsx similarity index 96% rename from src/renderer/src/pages/Login/components/Loginforrm.tsx rename to src/renderer/src/features/auth/ui/login/components/Loginforrm.tsx index 266b814..1bb1b70 100644 --- a/src/renderer/src/pages/Login/components/Loginforrm.tsx +++ b/src/renderer/src/features/auth/ui/login/components/Loginforrm.tsx @@ -1,12 +1,12 @@ -import { useForm } from 'react-hook-form'; -import { useEffect } from 'react'; +import { useLoginMutation } from '@entities/user'; import { TextField as TextInput } from '@shared/ui/input-field'; -import SaveIdIcon from '../../../assets/auth/saveid_icon.svg?react'; +import { useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { useNavigate } from 'react-router-dom'; +import FailIcon from '../../../../assets/auth/error_icon.svg?react'; +import SaveIdIcon from '../../../../assets/auth/saveid_icon.svg?react'; import LoginButton from './LoginButton'; import PasswordField from './PasswordField'; -import { useNavigate } from 'react-router-dom'; -import { useLoginMutation } from '@entities/user'; -import FailIcon from '../../../assets/auth/error_icon.svg?react'; interface LoginFormData { email: string; diff --git a/src/renderer/src/pages/Login/components/PasswordField.tsx b/src/renderer/src/features/auth/ui/login/components/PasswordField.tsx similarity index 88% rename from src/renderer/src/pages/Login/components/PasswordField.tsx rename to src/renderer/src/features/auth/ui/login/components/PasswordField.tsx index 531992d..12f4a9b 100644 --- a/src/renderer/src/pages/Login/components/PasswordField.tsx +++ b/src/renderer/src/features/auth/ui/login/components/PasswordField.tsx @@ -1,7 +1,7 @@ -import { useState, forwardRef, type ChangeEvent } from 'react'; import { TextField as TextInput } from '@shared/ui/input-field'; -import VisibleIcon from '../../../assets/auth/visible_icon.svg?react'; -import InvisibleIcon from '../../../assets/auth/invisible_icon.svg?react'; +import { forwardRef, useState, type ChangeEvent } from 'react'; +import InvisibleIcon from '../../../../assets/auth/invisible_icon.svg?react'; +import VisibleIcon from '../../../../assets/auth/visible_icon.svg?react'; interface PasswordFieldProps { hasValue?: boolean; diff --git a/src/renderer/src/features/auth/ui/login/index.ts b/src/renderer/src/features/auth/ui/login/index.ts new file mode 100644 index 0000000..ed7ee8d --- /dev/null +++ b/src/renderer/src/features/auth/ui/login/index.ts @@ -0,0 +1,2 @@ +export { default as LoginPage } from './LoginPage'; + diff --git a/src/renderer/src/pages/SignUp/EmailVerificationCallbackPage.tsx b/src/renderer/src/features/auth/ui/signup/EmailVerificationCallbackPage.tsx similarity index 96% rename from src/renderer/src/pages/SignUp/EmailVerificationCallbackPage.tsx rename to src/renderer/src/features/auth/ui/signup/EmailVerificationCallbackPage.tsx index d4f6603..16ca4fb 100644 --- a/src/renderer/src/pages/SignUp/EmailVerificationCallbackPage.tsx +++ b/src/renderer/src/features/auth/ui/signup/EmailVerificationCallbackPage.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useVerifyEmailMutation } from '@entities/user'; -import CompletionCharacter from '../../assets/completion.svg?react'; +import CompletionCharacter from '../../../assets/completion.svg?react'; const EmailVerificationCallbackPage = () => { const [searchParams] = useSearchParams() diff --git a/src/renderer/src/pages/SignUp/EmailVerificationPage.tsx b/src/renderer/src/features/auth/ui/signup/EmailVerificationPage.tsx similarity index 100% rename from src/renderer/src/pages/SignUp/EmailVerificationPage.tsx rename to src/renderer/src/features/auth/ui/signup/EmailVerificationPage.tsx diff --git a/src/renderer/src/pages/SignUp/ResendVerificationPage.tsx b/src/renderer/src/features/auth/ui/signup/ResendVerificationPage.tsx similarity index 100% rename from src/renderer/src/pages/SignUp/ResendVerificationPage.tsx rename to src/renderer/src/features/auth/ui/signup/ResendVerificationPage.tsx diff --git a/src/renderer/src/pages/SignUp/SignUpPage.tsx b/src/renderer/src/features/auth/ui/signup/SignUpPage.tsx similarity index 100% rename from src/renderer/src/pages/SignUp/SignUpPage.tsx rename to src/renderer/src/features/auth/ui/signup/SignUpPage.tsx diff --git a/src/renderer/src/pages/SignUp/components/EmailHeroSection.tsx b/src/renderer/src/features/auth/ui/signup/components/EmailHeroSection.tsx similarity index 93% rename from src/renderer/src/pages/SignUp/components/EmailHeroSection.tsx rename to src/renderer/src/features/auth/ui/signup/components/EmailHeroSection.tsx index ac1d0e5..6bf65e0 100644 --- a/src/renderer/src/pages/SignUp/components/EmailHeroSection.tsx +++ b/src/renderer/src/features/auth/ui/signup/components/EmailHeroSection.tsx @@ -1,5 +1,5 @@ import { useEmailStore } from '@entities/user'; -import EmailIcon from '../../../assets/auth/email_icon.svg?react'; +import EmailIcon from '../../../../assets/auth/email_icon.svg?react'; export default function EmailHeroSection() { const email = useEmailStore((state) => state.email); diff --git a/src/renderer/src/pages/SignUp/components/ResendEmailHeroSection.tsx b/src/renderer/src/features/auth/ui/signup/components/ResendEmailHeroSection.tsx similarity index 100% rename from src/renderer/src/pages/SignUp/components/ResendEmailHeroSection.tsx rename to src/renderer/src/features/auth/ui/signup/components/ResendEmailHeroSection.tsx diff --git a/src/renderer/src/pages/SignUp/components/ResendSection.tsx b/src/renderer/src/features/auth/ui/signup/components/ResendSection.tsx similarity index 100% rename from src/renderer/src/pages/SignUp/components/ResendSection.tsx rename to src/renderer/src/features/auth/ui/signup/components/ResendSection.tsx diff --git a/src/renderer/src/pages/SignUp/components/SignUpform.tsx b/src/renderer/src/features/auth/ui/signup/components/SignUpform.tsx similarity index 97% rename from src/renderer/src/pages/SignUp/components/SignUpform.tsx rename to src/renderer/src/features/auth/ui/signup/components/SignUpform.tsx index 90c1a72..65ffe5a 100644 --- a/src/renderer/src/pages/SignUp/components/SignUpform.tsx +++ b/src/renderer/src/features/auth/ui/signup/components/SignUpform.tsx @@ -2,9 +2,9 @@ import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { Button } from '@shared/ui/button'; import { TextField } from '@shared/ui/input-field'; -import PasswordField from '../../Login/components/PasswordField'; -import SuccessIcon from '../../../assets/auth/success_icon.svg?react'; -import FailIcon from '../../../assets/auth/error_icon.svg?react'; +import PasswordField from '../../login/components/PasswordField'; +import SuccessIcon from '../../../../assets/auth/success_icon.svg?react'; +import FailIcon from '../../../../assets/auth/error_icon.svg?react'; import { signUpSchema, SignUpFormData } from '../utils/SignupSchemas'; import { useState } from 'react'; import { diff --git a/src/renderer/src/pages/SignUp/components/VerifyAction.tsx b/src/renderer/src/features/auth/ui/signup/components/VerifyAction.tsx similarity index 100% rename from src/renderer/src/pages/SignUp/components/VerifyAction.tsx rename to src/renderer/src/features/auth/ui/signup/components/VerifyAction.tsx diff --git a/src/renderer/src/features/auth/ui/signup/index.ts b/src/renderer/src/features/auth/ui/signup/index.ts new file mode 100644 index 0000000..dc69aec --- /dev/null +++ b/src/renderer/src/features/auth/ui/signup/index.ts @@ -0,0 +1,4 @@ +export { default as EmailVerificationCallbackPage } from './EmailVerificationCallbackPage'; +export { default as EmailVerificationPage } from './EmailVerificationPage'; +export { default as ResendVerificationPage } from './ResendVerificationPage'; +export { default as SignUpPage } from './SignUpPage'; diff --git a/src/renderer/src/pages/SignUp/utils/SignupSchemas.ts b/src/renderer/src/features/auth/ui/signup/utils/SignupSchemas.ts similarity index 100% rename from src/renderer/src/pages/SignUp/utils/SignupSchemas.ts rename to src/renderer/src/features/auth/ui/signup/utils/SignupSchemas.ts diff --git a/src/renderer/src/shared/config/router.tsx b/src/renderer/src/shared/config/router.tsx index 69499b0..73d74da 100644 --- a/src/renderer/src/shared/config/router.tsx +++ b/src/renderer/src/shared/config/router.tsx @@ -1,16 +1,12 @@ +import { EmailVerificationCallbackPage, EmailVerificationPage, LoginPage, ResendVerificationPage, SignUpPage } from '@features/auth'; import api from '@shared/api'; import { createBrowserRouter, redirect } from 'react-router-dom'; import Layout from '../../layout/Layout'; import CalibrationPage from '../../pages/Calibration/CalibrationPage'; -import LoginPage from '../../pages/Login/LoginPage'; import MainPage from '../../pages/Main/MainPage'; import OnboardingCompletionPage from '../../pages/Onboarding/OnboardingCompletionPage'; import OnboardinInitPage from '../../pages/Onboarding/OnboardingInitPage'; import OnboardingPage from '../../pages/Onboarding/OnboardingPage'; -import EmailVerificationCallbackPage from '../../pages/SignUp/EmailVerificationCallbackPage'; -import EmailVerificationPage from '../../pages/SignUp/EmailVerificationPage'; -import ResendVerificationPage from '../../pages/SignUp/ResendVerificationPage'; -import SignUpPage from '../../pages/SignUp/SignUpPage'; import { WidgetPage } from '../../pages/Widget/WidgetPage'; // 인증이 필요한 페이지용 loader diff --git a/src/renderer/tsconfig.json b/src/renderer/tsconfig.json index 892ffc5..11aab98 100644 --- a/src/renderer/tsconfig.json +++ b/src/renderer/tsconfig.json @@ -27,7 +27,8 @@ "@api/*": ["./src/api/*"], "@utils/*": ["./src/utils/*"], "@shared/*": ["./src/shared/*"], - "@entities/*": ["./src/entities/*"] + "@entities/*": ["./src/entities/*"], + "@features/*": ["./src/features/*"] } }, "include": [ diff --git a/vite.config.mts b/vite.config.mts index fa9ba97..1482221 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -27,6 +27,8 @@ export default defineConfig({ '@shared': path.resolve(__dirname, 'src/renderer/src/shared'), '@entities/': path.resolve(__dirname, 'src/renderer/src/entities') + '/', '@entities': path.resolve(__dirname, 'src/renderer/src/entities'), + '@features/': path.resolve(__dirname, 'src/renderer/src/features') + '/', + '@features': path.resolve(__dirname, 'src/renderer/src/features'), ui: path.resolve(__dirname, 'src/renderer/src/components'), }, }, From 479aa14f7944c751639cdcf90812f2e8d67ba36c Mon Sep 17 00:00:00 2001 From: choihooo Date: Fri, 28 Nov 2025 11:47:04 +0900 Subject: [PATCH 08/12] =?UTF-8?q?refactor(feature):=20=EC=BA=98=EB=A6=AC?= =?UTF-8?q?=EB=B8=8C=EB=A0=88=EC=9D=B4=EC=85=98=20=ED=94=BC=EC=B3=90=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/features/calibration/index.ts | 3 ++ .../src/features/calibration/lib/index.ts | 2 + .../lib}/useNotificationScheduler.ts | 2 +- .../calibration/ui}/CalibrationPage.tsx | 20 +++++----- .../ui}/components/MeasuringPanel.tsx | 0 .../calibration/ui}/components/WebcamView.tsx | 40 +++++++++---------- .../ui}/components/WelcomePanel.tsx | 0 .../src/features/calibration/ui/index.ts | 2 + src/renderer/src/pages/Main/MainPage.tsx | 2 +- src/renderer/src/shared/config/router.tsx | 2 +- 10 files changed, 40 insertions(+), 33 deletions(-) create mode 100644 src/renderer/src/features/calibration/index.ts create mode 100644 src/renderer/src/features/calibration/lib/index.ts rename src/renderer/src/{hooks => features/calibration/lib}/useNotificationScheduler.ts (98%) rename src/renderer/src/{pages/Calibration => features/calibration/ui}/CalibrationPage.tsx (99%) rename src/renderer/src/{pages/Calibration => features/calibration/ui}/components/MeasuringPanel.tsx (100%) rename src/renderer/src/{pages/Calibration => features/calibration/ui}/components/WebcamView.tsx (95%) rename src/renderer/src/{pages/Calibration => features/calibration/ui}/components/WelcomePanel.tsx (100%) create mode 100644 src/renderer/src/features/calibration/ui/index.ts diff --git a/src/renderer/src/features/calibration/index.ts b/src/renderer/src/features/calibration/index.ts new file mode 100644 index 0000000..6a63040 --- /dev/null +++ b/src/renderer/src/features/calibration/index.ts @@ -0,0 +1,3 @@ +export * from './ui'; +export * from './lib'; + diff --git a/src/renderer/src/features/calibration/lib/index.ts b/src/renderer/src/features/calibration/lib/index.ts new file mode 100644 index 0000000..60bf772 --- /dev/null +++ b/src/renderer/src/features/calibration/lib/index.ts @@ -0,0 +1,2 @@ +export { useNotificationScheduler } from './useNotificationScheduler'; + diff --git a/src/renderer/src/hooks/useNotificationScheduler.ts b/src/renderer/src/features/calibration/lib/useNotificationScheduler.ts similarity index 98% rename from src/renderer/src/hooks/useNotificationScheduler.ts rename to src/renderer/src/features/calibration/lib/useNotificationScheduler.ts index 5a5a26a..0587c50 100644 --- a/src/renderer/src/hooks/useNotificationScheduler.ts +++ b/src/renderer/src/features/calibration/lib/useNotificationScheduler.ts @@ -1,5 +1,5 @@ import { useEffect, useRef } from 'react'; -import { useNotificationStore } from '../store/useNotificationStore'; +import { useNotificationStore } from '../../../store/useNotificationStore'; import { usePostureStore } from '@entities/posture'; /* 알림 스케줄러 훅 , 설정된 시간에 따라 시스템 알림을 자동으로 표시 */ diff --git a/src/renderer/src/pages/Calibration/CalibrationPage.tsx b/src/renderer/src/features/calibration/ui/CalibrationPage.tsx similarity index 99% rename from src/renderer/src/pages/Calibration/CalibrationPage.tsx rename to src/renderer/src/features/calibration/ui/CalibrationPage.tsx index c250a16..a3869f9 100644 --- a/src/renderer/src/pages/Calibration/CalibrationPage.tsx +++ b/src/renderer/src/features/calibration/ui/CalibrationPage.tsx @@ -1,13 +1,3 @@ -import { - useCallback, - useEffect, - useRef, - useState, - type RefObject, -} from 'react'; -import { useNavigate } from 'react-router-dom'; -import Webcam from 'react-webcam'; -import CalibrationGuide from '../../assets/calibration_guide.svg?react'; import { PoseLandmark as AnalyzerPoseLandmark, calculateFrameBrightness, @@ -19,6 +9,16 @@ import { processCalibrationData, WorldLandmark, } from '@entities/posture'; +import { + useCallback, + useEffect, + useRef, + useState, + type RefObject, +} from 'react'; +import { useNavigate } from 'react-router-dom'; +import Webcam from 'react-webcam'; +import CalibrationGuide from '../../../assets/calibration_guide.svg?react'; import MeasuringPanel from './components/MeasuringPanel'; import WebcamView from './components/WebcamView'; import WelcomePanel from './components/WelcomePanel'; diff --git a/src/renderer/src/pages/Calibration/components/MeasuringPanel.tsx b/src/renderer/src/features/calibration/ui/components/MeasuringPanel.tsx similarity index 100% rename from src/renderer/src/pages/Calibration/components/MeasuringPanel.tsx rename to src/renderer/src/features/calibration/ui/components/MeasuringPanel.tsx diff --git a/src/renderer/src/pages/Calibration/components/WebcamView.tsx b/src/renderer/src/features/calibration/ui/components/WebcamView.tsx similarity index 95% rename from src/renderer/src/pages/Calibration/components/WebcamView.tsx rename to src/renderer/src/features/calibration/ui/components/WebcamView.tsx index 35dc49c..36d293e 100644 --- a/src/renderer/src/pages/Calibration/components/WebcamView.tsx +++ b/src/renderer/src/features/calibration/ui/components/WebcamView.tsx @@ -1,14 +1,14 @@ import SleepIcon from '@assets/sleep.svg?react'; -import { useEffect, useRef, useState, type RefObject } from 'react'; -import Webcam from 'react-webcam'; -import { Timer } from '@shared/ui/timer'; import { - PoseLandmark, - WorldLandmark, PoseDetection, + PoseLandmark, PoseVisualizer, + WorldLandmark, } from '@entities/posture'; -import { useCameraStore } from '../../../store/useCameraStore'; +import { Timer } from '@shared/ui/timer'; +import { useEffect, useRef, useState, type RefObject } from 'react'; +import Webcam from 'react-webcam'; +import { useCameraStore } from '../../../../store/useCameraStore'; interface WebcamViewProps { onPoseDetected?: ( @@ -67,15 +67,15 @@ const WebcamView = ({ const videoConstraints = preferredDeviceId ? { - deviceId: { exact: preferredDeviceId }, - width: 1000, - height: 563, - } + deviceId: { exact: preferredDeviceId }, + width: 1000, + height: 563, + } : { - facingMode: 'user', - width: 1000, - height: 563, - }; + facingMode: 'user', + width: 1000, + height: 563, + }; const handlePoseDetected = ( landmarks: PoseLandmark[], @@ -210,12 +210,12 @@ const WebcamView = ({ diff --git a/src/renderer/src/pages/Calibration/components/WelcomePanel.tsx b/src/renderer/src/features/calibration/ui/components/WelcomePanel.tsx similarity index 100% rename from src/renderer/src/pages/Calibration/components/WelcomePanel.tsx rename to src/renderer/src/features/calibration/ui/components/WelcomePanel.tsx diff --git a/src/renderer/src/features/calibration/ui/index.ts b/src/renderer/src/features/calibration/ui/index.ts new file mode 100644 index 0000000..0467d0c --- /dev/null +++ b/src/renderer/src/features/calibration/ui/index.ts @@ -0,0 +1,2 @@ +export { default as CalibrationPage } from './CalibrationPage'; + diff --git a/src/renderer/src/pages/Main/MainPage.tsx b/src/renderer/src/pages/Main/MainPage.tsx index e06e885..eb28f6f 100644 --- a/src/renderer/src/pages/Main/MainPage.tsx +++ b/src/renderer/src/pages/Main/MainPage.tsx @@ -13,7 +13,7 @@ import { ModalPortal } from '@shared/ui/modal'; import { useEffect, useRef } from 'react'; import NotificationModal from '../../components/Modal/NotificationModal'; import { useAutoMetricsSender } from '../../hooks/useAutoMetricsSender'; -import { useNotificationScheduler } from '../../hooks/useNotificationScheduler'; +import { useNotificationScheduler } from '@features/calibration'; import { useSessionCleanup } from '../../hooks/useSessionCleanup'; import { useCameraStore } from '../../store/useCameraStore'; import AttendacePanel from './components/AttendacePanel'; diff --git a/src/renderer/src/shared/config/router.tsx b/src/renderer/src/shared/config/router.tsx index 73d74da..e49e951 100644 --- a/src/renderer/src/shared/config/router.tsx +++ b/src/renderer/src/shared/config/router.tsx @@ -2,7 +2,7 @@ import { EmailVerificationCallbackPage, EmailVerificationPage, LoginPage, Resend import api from '@shared/api'; import { createBrowserRouter, redirect } from 'react-router-dom'; import Layout from '../../layout/Layout'; -import CalibrationPage from '../../pages/Calibration/CalibrationPage'; +import { CalibrationPage } from '@features/calibration'; import MainPage from '../../pages/Main/MainPage'; import OnboardingCompletionPage from '../../pages/Onboarding/OnboardingCompletionPage'; import OnboardinInitPage from '../../pages/Onboarding/OnboardingInitPage'; From a1563eab9f46ec533bda56c9b1837e0b9afbaabd Mon Sep 17 00:00:00 2001 From: choihooo Date: Fri, 28 Nov 2025 11:49:33 +0900 Subject: [PATCH 09/12] =?UTF-8?q?refactor(feature):=20=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=ED=94=BC=EC=B3=90=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/dashboard/ui/index.ts | 16 +++++++++++++ .../src/features/calibration/ui/index.ts | 1 + src/renderer/src/features/dashboard/index.ts | 3 +++ .../src/features/dashboard/lib/index.ts | 3 +++ .../dashboard/lib}/useAutoMetricsSender.ts | 0 .../dashboard/lib}/useSessionCleanup.ts | 0 .../dashboard/ui}/AttendacePanel.tsx | 0 .../ui}/AverageGraph/AverageGraphPannel.tsx | 0 .../hooks/useAverageGraphChart.ts | 0 .../AveragePosture/AveragePosturePanel.tsx | 0 .../ui}/AveragePosture/levelConfig.ts | 0 .../dashboard/ui}/CharacterPanel.tsx | 0 .../dashboard/ui}/CharacterSpeedRow.tsx | 0 .../dashboard/ui}/ExitPanel.tsx | 0 .../dashboard/ui}/HighlightsPanel.tsx | 0 .../dashboard/ui}/HighlightsPanel/data.ts | 0 .../hooks/useHighlightsChart.ts | 0 .../dashboard/ui}/LevelProgressPanel.tsx | 0 .../dashboard/ui}/MainHeader.tsx | 0 .../dashboard/ui}/MiniRunningPanel.tsx | 0 .../dashboard/ui}/PosePatternPanel.tsx | 0 .../dashboard/ui}/RunningPanel.tsx | 0 .../dashboard/ui}/TotalDistanceModal.tsx | 0 .../dashboard/ui}/TotalDistancePanel.tsx | 0 .../dashboard/ui}/TrendPanel.tsx | 0 .../dashboard/ui}/WebcamPanel.tsx | 4 ++-- .../src/features/dashboard/ui/index.ts | 18 ++++++++++++++ src/renderer/src/pages/Main/MainPage.tsx | 24 ++++++++++--------- 28 files changed, 56 insertions(+), 13 deletions(-) create mode 100644 src/features/dashboard/ui/index.ts create mode 100644 src/renderer/src/features/dashboard/index.ts create mode 100644 src/renderer/src/features/dashboard/lib/index.ts rename src/renderer/src/{hooks => features/dashboard/lib}/useAutoMetricsSender.ts (100%) rename src/renderer/src/{hooks => features/dashboard/lib}/useSessionCleanup.ts (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/AttendacePanel.tsx (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/AverageGraph/AverageGraphPannel.tsx (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/AverageGraph/hooks/useAverageGraphChart.ts (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/AveragePosture/AveragePosturePanel.tsx (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/AveragePosture/levelConfig.ts (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/CharacterPanel.tsx (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/CharacterSpeedRow.tsx (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/ExitPanel.tsx (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/HighlightsPanel.tsx (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/HighlightsPanel/data.ts (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/HighlightsPanel/hooks/useHighlightsChart.ts (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/LevelProgressPanel.tsx (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/MainHeader.tsx (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/MiniRunningPanel.tsx (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/PosePatternPanel.tsx (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/RunningPanel.tsx (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/TotalDistanceModal.tsx (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/TotalDistancePanel.tsx (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/TrendPanel.tsx (100%) rename src/renderer/src/{pages/Main/components => features/dashboard/ui}/WebcamPanel.tsx (97%) create mode 100644 src/renderer/src/features/dashboard/ui/index.ts diff --git a/src/features/dashboard/ui/index.ts b/src/features/dashboard/ui/index.ts new file mode 100644 index 0000000..f5c793e --- /dev/null +++ b/src/features/dashboard/ui/index.ts @@ -0,0 +1,16 @@ +// Dashboard UI components barrel export +export { default as AttendacePanel } from './AttendacePanel'; +export { default as CharacterPanel } from './CharacterPanel'; +export { default as CharacterSpeedRow } from './CharacterSpeedRow'; +export { default as ExitPanel } from './ExitPanel'; +export { default as HighlightsPanel } from './HighlightsPanel'; +export { default as LevelProgressPanel } from './LevelProgressPanel'; +export { default as MainHeader } from './MainHeader'; +export { default as MiniRunningPanel } from './MiniRunningPanel'; +export { default as PosePatternPanel } from './PosePatternPanel'; +export { default as RunningPanel } from './RunningPanel'; +export { default as TotalDistanceModal } from './TotalDistanceModal'; +export { default as TotalDistancePanel } from './TotalDistancePanel'; +export { default as TrendPanel } from './TrendPanel'; +export { default as WebcamPanel } from './WebcamPanel'; + diff --git a/src/renderer/src/features/calibration/ui/index.ts b/src/renderer/src/features/calibration/ui/index.ts index 0467d0c..c660040 100644 --- a/src/renderer/src/features/calibration/ui/index.ts +++ b/src/renderer/src/features/calibration/ui/index.ts @@ -1,2 +1,3 @@ export { default as CalibrationPage } from './CalibrationPage'; +export { default as WebcamView } from './components/WebcamView'; diff --git a/src/renderer/src/features/dashboard/index.ts b/src/renderer/src/features/dashboard/index.ts new file mode 100644 index 0000000..6a63040 --- /dev/null +++ b/src/renderer/src/features/dashboard/index.ts @@ -0,0 +1,3 @@ +export * from './ui'; +export * from './lib'; + diff --git a/src/renderer/src/features/dashboard/lib/index.ts b/src/renderer/src/features/dashboard/lib/index.ts new file mode 100644 index 0000000..4555386 --- /dev/null +++ b/src/renderer/src/features/dashboard/lib/index.ts @@ -0,0 +1,3 @@ +export { useAutoMetricsSender } from './useAutoMetricsSender'; +export { useSessionCleanup } from './useSessionCleanup'; + diff --git a/src/renderer/src/hooks/useAutoMetricsSender.ts b/src/renderer/src/features/dashboard/lib/useAutoMetricsSender.ts similarity index 100% rename from src/renderer/src/hooks/useAutoMetricsSender.ts rename to src/renderer/src/features/dashboard/lib/useAutoMetricsSender.ts diff --git a/src/renderer/src/hooks/useSessionCleanup.ts b/src/renderer/src/features/dashboard/lib/useSessionCleanup.ts similarity index 100% rename from src/renderer/src/hooks/useSessionCleanup.ts rename to src/renderer/src/features/dashboard/lib/useSessionCleanup.ts diff --git a/src/renderer/src/pages/Main/components/AttendacePanel.tsx b/src/renderer/src/features/dashboard/ui/AttendacePanel.tsx similarity index 100% rename from src/renderer/src/pages/Main/components/AttendacePanel.tsx rename to src/renderer/src/features/dashboard/ui/AttendacePanel.tsx diff --git a/src/renderer/src/pages/Main/components/AverageGraph/AverageGraphPannel.tsx b/src/renderer/src/features/dashboard/ui/AverageGraph/AverageGraphPannel.tsx similarity index 100% rename from src/renderer/src/pages/Main/components/AverageGraph/AverageGraphPannel.tsx rename to src/renderer/src/features/dashboard/ui/AverageGraph/AverageGraphPannel.tsx diff --git a/src/renderer/src/pages/Main/components/AverageGraph/hooks/useAverageGraphChart.ts b/src/renderer/src/features/dashboard/ui/AverageGraph/hooks/useAverageGraphChart.ts similarity index 100% rename from src/renderer/src/pages/Main/components/AverageGraph/hooks/useAverageGraphChart.ts rename to src/renderer/src/features/dashboard/ui/AverageGraph/hooks/useAverageGraphChart.ts diff --git a/src/renderer/src/pages/Main/components/AveragePosture/AveragePosturePanel.tsx b/src/renderer/src/features/dashboard/ui/AveragePosture/AveragePosturePanel.tsx similarity index 100% rename from src/renderer/src/pages/Main/components/AveragePosture/AveragePosturePanel.tsx rename to src/renderer/src/features/dashboard/ui/AveragePosture/AveragePosturePanel.tsx diff --git a/src/renderer/src/pages/Main/components/AveragePosture/levelConfig.ts b/src/renderer/src/features/dashboard/ui/AveragePosture/levelConfig.ts similarity index 100% rename from src/renderer/src/pages/Main/components/AveragePosture/levelConfig.ts rename to src/renderer/src/features/dashboard/ui/AveragePosture/levelConfig.ts diff --git a/src/renderer/src/pages/Main/components/CharacterPanel.tsx b/src/renderer/src/features/dashboard/ui/CharacterPanel.tsx similarity index 100% rename from src/renderer/src/pages/Main/components/CharacterPanel.tsx rename to src/renderer/src/features/dashboard/ui/CharacterPanel.tsx diff --git a/src/renderer/src/pages/Main/components/CharacterSpeedRow.tsx b/src/renderer/src/features/dashboard/ui/CharacterSpeedRow.tsx similarity index 100% rename from src/renderer/src/pages/Main/components/CharacterSpeedRow.tsx rename to src/renderer/src/features/dashboard/ui/CharacterSpeedRow.tsx diff --git a/src/renderer/src/pages/Main/components/ExitPanel.tsx b/src/renderer/src/features/dashboard/ui/ExitPanel.tsx similarity index 100% rename from src/renderer/src/pages/Main/components/ExitPanel.tsx rename to src/renderer/src/features/dashboard/ui/ExitPanel.tsx diff --git a/src/renderer/src/pages/Main/components/HighlightsPanel.tsx b/src/renderer/src/features/dashboard/ui/HighlightsPanel.tsx similarity index 100% rename from src/renderer/src/pages/Main/components/HighlightsPanel.tsx rename to src/renderer/src/features/dashboard/ui/HighlightsPanel.tsx diff --git a/src/renderer/src/pages/Main/components/HighlightsPanel/data.ts b/src/renderer/src/features/dashboard/ui/HighlightsPanel/data.ts similarity index 100% rename from src/renderer/src/pages/Main/components/HighlightsPanel/data.ts rename to src/renderer/src/features/dashboard/ui/HighlightsPanel/data.ts diff --git a/src/renderer/src/pages/Main/components/HighlightsPanel/hooks/useHighlightsChart.ts b/src/renderer/src/features/dashboard/ui/HighlightsPanel/hooks/useHighlightsChart.ts similarity index 100% rename from src/renderer/src/pages/Main/components/HighlightsPanel/hooks/useHighlightsChart.ts rename to src/renderer/src/features/dashboard/ui/HighlightsPanel/hooks/useHighlightsChart.ts diff --git a/src/renderer/src/pages/Main/components/LevelProgressPanel.tsx b/src/renderer/src/features/dashboard/ui/LevelProgressPanel.tsx similarity index 100% rename from src/renderer/src/pages/Main/components/LevelProgressPanel.tsx rename to src/renderer/src/features/dashboard/ui/LevelProgressPanel.tsx diff --git a/src/renderer/src/pages/Main/components/MainHeader.tsx b/src/renderer/src/features/dashboard/ui/MainHeader.tsx similarity index 100% rename from src/renderer/src/pages/Main/components/MainHeader.tsx rename to src/renderer/src/features/dashboard/ui/MainHeader.tsx diff --git a/src/renderer/src/pages/Main/components/MiniRunningPanel.tsx b/src/renderer/src/features/dashboard/ui/MiniRunningPanel.tsx similarity index 100% rename from src/renderer/src/pages/Main/components/MiniRunningPanel.tsx rename to src/renderer/src/features/dashboard/ui/MiniRunningPanel.tsx diff --git a/src/renderer/src/pages/Main/components/PosePatternPanel.tsx b/src/renderer/src/features/dashboard/ui/PosePatternPanel.tsx similarity index 100% rename from src/renderer/src/pages/Main/components/PosePatternPanel.tsx rename to src/renderer/src/features/dashboard/ui/PosePatternPanel.tsx diff --git a/src/renderer/src/pages/Main/components/RunningPanel.tsx b/src/renderer/src/features/dashboard/ui/RunningPanel.tsx similarity index 100% rename from src/renderer/src/pages/Main/components/RunningPanel.tsx rename to src/renderer/src/features/dashboard/ui/RunningPanel.tsx diff --git a/src/renderer/src/pages/Main/components/TotalDistanceModal.tsx b/src/renderer/src/features/dashboard/ui/TotalDistanceModal.tsx similarity index 100% rename from src/renderer/src/pages/Main/components/TotalDistanceModal.tsx rename to src/renderer/src/features/dashboard/ui/TotalDistanceModal.tsx diff --git a/src/renderer/src/pages/Main/components/TotalDistancePanel.tsx b/src/renderer/src/features/dashboard/ui/TotalDistancePanel.tsx similarity index 100% rename from src/renderer/src/pages/Main/components/TotalDistancePanel.tsx rename to src/renderer/src/features/dashboard/ui/TotalDistancePanel.tsx diff --git a/src/renderer/src/pages/Main/components/TrendPanel.tsx b/src/renderer/src/features/dashboard/ui/TrendPanel.tsx similarity index 100% rename from src/renderer/src/pages/Main/components/TrendPanel.tsx rename to src/renderer/src/features/dashboard/ui/TrendPanel.tsx diff --git a/src/renderer/src/pages/Main/components/WebcamPanel.tsx b/src/renderer/src/features/dashboard/ui/WebcamPanel.tsx similarity index 97% rename from src/renderer/src/pages/Main/components/WebcamPanel.tsx rename to src/renderer/src/features/dashboard/ui/WebcamPanel.tsx index 00b9c8f..dc816f0 100644 --- a/src/renderer/src/pages/Main/components/WebcamPanel.tsx +++ b/src/renderer/src/features/dashboard/ui/WebcamPanel.tsx @@ -7,14 +7,14 @@ import { useResumeSessionMutation, useStopSessionMutation, } from '@entities/session'; -import { Button } from '../../../components'; +import { Button } from '@shared/ui/button'; import { PoseLandmark, WorldLandmark, } from '@entities/posture'; import { useWidget } from '../../../hooks/useWidget'; import { useCameraStore } from '../../../store/useCameraStore'; -import WebcamView from '../../Calibration/components/WebcamView'; +import { WebcamView } from '@features/calibration/ui'; import { useLevelQuery } from '@entities/dashboard'; interface Props { diff --git a/src/renderer/src/features/dashboard/ui/index.ts b/src/renderer/src/features/dashboard/ui/index.ts new file mode 100644 index 0000000..033c541 --- /dev/null +++ b/src/renderer/src/features/dashboard/ui/index.ts @@ -0,0 +1,18 @@ +// Dashboard UI components barrel export +export { default as AttendacePanel } from './AttendacePanel'; +export { default as AverageGraphPannel } from './AverageGraph/AverageGraphPannel'; +export { default as AveragePosturePanel } from './AveragePosture/AveragePosturePanel'; +export { default as CharacterPanel } from './CharacterPanel'; +export { default as CharacterSpeedRow } from './CharacterSpeedRow'; +export { default as ExitPanel } from './ExitPanel'; +export { default as HighlightsPanel } from './HighlightsPanel'; +export { default as LevelProgressPanel } from './LevelProgressPanel'; +export { default as MainHeader } from './MainHeader'; +export { default as MiniRunningPanel } from './MiniRunningPanel'; +export { default as PosePatternPanel } from './PosePatternPanel'; +export { default as RunningPanel } from './RunningPanel'; +export { default as TotalDistanceModal } from './TotalDistanceModal'; +export { default as TotalDistancePanel } from './TotalDistancePanel'; +export { default as TrendPanel } from './TrendPanel'; +export { default as WebcamPanel } from './WebcamPanel'; + diff --git a/src/renderer/src/pages/Main/MainPage.tsx b/src/renderer/src/pages/Main/MainPage.tsx index eb28f6f..a71ff5c 100644 --- a/src/renderer/src/pages/Main/MainPage.tsx +++ b/src/renderer/src/pages/Main/MainPage.tsx @@ -12,19 +12,21 @@ import { useModal } from '@shared/hooks/use-modal'; import { ModalPortal } from '@shared/ui/modal'; import { useEffect, useRef } from 'react'; import NotificationModal from '../../components/Modal/NotificationModal'; -import { useAutoMetricsSender } from '../../hooks/useAutoMetricsSender'; +import { + useAutoMetricsSender, + useSessionCleanup, + AttendacePanel, + AverageGraphPannel, + AveragePosturePanel, + HighlightsPanel, + MainHeader, + MiniRunningPanel, + PosePatternPanel, + TotalDistancePanel, + WebcamPanel, +} from '@features/dashboard'; import { useNotificationScheduler } from '@features/calibration'; -import { useSessionCleanup } from '../../hooks/useSessionCleanup'; import { useCameraStore } from '../../store/useCameraStore'; -import AttendacePanel from './components/AttendacePanel'; -import AverageGraphPannel from './components/AverageGraph/AverageGraphPannel'; -import AveragePosturePanel from './components/AveragePosture/AveragePosturePanel'; -import HighlightsPanel from './components/HighlightsPanel'; -import MainHeader from './components/MainHeader'; -import MiniRunningPanel from './components/MiniRunningPanel'; -import PosePatternPanel from './components/PosePatternPanel'; -import TotalDistancePanel from './components/TotalDistancePanel'; -import WebcamPanel from './components/WebcamPanel'; const LOCAL_STORAGE_KEY = 'calibration_result_v1'; From 4819b36b806105dfa0fdf93a285ef465baa8a66f Mon Sep 17 00:00:00 2001 From: choihooo Date: Fri, 28 Nov 2025 13:00:12 +0900 Subject: [PATCH 10/12] =?UTF-8?q?refactor(feature):=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=ED=94=BC=EC=B3=90=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/features/calibration/lib/useNotificationScheduler.ts | 2 +- src/renderer/src/features/notification/index.ts | 3 +++ src/renderer/src/features/notification/model/index.ts | 3 +++ .../notification/model/use-notification-store.ts} | 0 .../Modal => features/notification/ui}/NotificationModal.tsx | 2 +- .../notification/ui}/components/TimeControlSection.tsx | 4 ++-- .../Modal => features/notification/ui}/hooks/useTimeEditor.ts | 0 src/renderer/src/features/notification/ui/index.ts | 2 ++ src/renderer/src/pages/Main/MainPage.tsx | 2 +- 9 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 src/renderer/src/features/notification/index.ts create mode 100644 src/renderer/src/features/notification/model/index.ts rename src/renderer/src/{store/useNotificationStore.ts => features/notification/model/use-notification-store.ts} (100%) rename src/renderer/src/{components/Modal => features/notification/ui}/NotificationModal.tsx (98%) rename src/renderer/src/{components/Modal => features/notification/ui}/components/TimeControlSection.tsx (95%) rename src/renderer/src/{components/Modal => features/notification/ui}/hooks/useTimeEditor.ts (100%) create mode 100644 src/renderer/src/features/notification/ui/index.ts diff --git a/src/renderer/src/features/calibration/lib/useNotificationScheduler.ts b/src/renderer/src/features/calibration/lib/useNotificationScheduler.ts index 0587c50..dfcf1ee 100644 --- a/src/renderer/src/features/calibration/lib/useNotificationScheduler.ts +++ b/src/renderer/src/features/calibration/lib/useNotificationScheduler.ts @@ -1,5 +1,5 @@ import { useEffect, useRef } from 'react'; -import { useNotificationStore } from '../../../store/useNotificationStore'; +import { useNotificationStore } from '@features/notification'; import { usePostureStore } from '@entities/posture'; /* 알림 스케줄러 훅 , 설정된 시간에 따라 시스템 알림을 자동으로 표시 */ diff --git a/src/renderer/src/features/notification/index.ts b/src/renderer/src/features/notification/index.ts new file mode 100644 index 0000000..c18b7f4 --- /dev/null +++ b/src/renderer/src/features/notification/index.ts @@ -0,0 +1,3 @@ +export * from './ui'; +export * from './model'; + diff --git a/src/renderer/src/features/notification/model/index.ts b/src/renderer/src/features/notification/model/index.ts new file mode 100644 index 0000000..af58b41 --- /dev/null +++ b/src/renderer/src/features/notification/model/index.ts @@ -0,0 +1,3 @@ +export { useNotificationStore } from './use-notification-store'; +export type { NotificationSettings } from './use-notification-store'; + diff --git a/src/renderer/src/store/useNotificationStore.ts b/src/renderer/src/features/notification/model/use-notification-store.ts similarity index 100% rename from src/renderer/src/store/useNotificationStore.ts rename to src/renderer/src/features/notification/model/use-notification-store.ts diff --git a/src/renderer/src/components/Modal/NotificationModal.tsx b/src/renderer/src/features/notification/ui/NotificationModal.tsx similarity index 98% rename from src/renderer/src/components/Modal/NotificationModal.tsx rename to src/renderer/src/features/notification/ui/NotificationModal.tsx index 88e66e5..68db1fb 100644 --- a/src/renderer/src/components/Modal/NotificationModal.tsx +++ b/src/renderer/src/features/notification/ui/NotificationModal.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import { useTimeEditor } from './hooks/useTimeEditor'; import { TimeControlSection } from './components/TimeControlSection'; import { Button } from '@shared/ui/button'; -import { useNotificationStore } from '../../store/useNotificationStore'; +import { useNotificationStore } from '../model/use-notification-store'; interface NotificationModalProps { onClose: () => void; diff --git a/src/renderer/src/components/Modal/components/TimeControlSection.tsx b/src/renderer/src/features/notification/ui/components/TimeControlSection.tsx similarity index 95% rename from src/renderer/src/components/Modal/components/TimeControlSection.tsx rename to src/renderer/src/features/notification/ui/components/TimeControlSection.tsx index 0a8e8b6..40fb93a 100644 --- a/src/renderer/src/components/Modal/components/TimeControlSection.tsx +++ b/src/renderer/src/features/notification/ui/components/TimeControlSection.tsx @@ -1,7 +1,7 @@ import { NotificationToggleSwitch } from '@shared/ui/toggle-switch'; import { useTimeEditor } from '../hooks/useTimeEditor'; -import MinusIcon from '../../../assets/main/minus_icon.svg?react'; -import PlusIcon from '../../../assets/main/plus_icon.svg?react'; +import MinusIcon from '../../../../assets/main/minus_icon.svg?react'; +import PlusIcon from '../../../../assets/main/plus_icon.svg?react'; interface TimeControlSectionProps { title: string; diff --git a/src/renderer/src/components/Modal/hooks/useTimeEditor.ts b/src/renderer/src/features/notification/ui/hooks/useTimeEditor.ts similarity index 100% rename from src/renderer/src/components/Modal/hooks/useTimeEditor.ts rename to src/renderer/src/features/notification/ui/hooks/useTimeEditor.ts diff --git a/src/renderer/src/features/notification/ui/index.ts b/src/renderer/src/features/notification/ui/index.ts new file mode 100644 index 0000000..c16ae80 --- /dev/null +++ b/src/renderer/src/features/notification/ui/index.ts @@ -0,0 +1,2 @@ +export { default as NotificationModal } from './NotificationModal'; + diff --git a/src/renderer/src/pages/Main/MainPage.tsx b/src/renderer/src/pages/Main/MainPage.tsx index a71ff5c..36c9702 100644 --- a/src/renderer/src/pages/Main/MainPage.tsx +++ b/src/renderer/src/pages/Main/MainPage.tsx @@ -11,7 +11,7 @@ import { useSaveMetricsMutation } from '@entities/session'; import { useModal } from '@shared/hooks/use-modal'; import { ModalPortal } from '@shared/ui/modal'; import { useEffect, useRef } from 'react'; -import NotificationModal from '../../components/Modal/NotificationModal'; +import { NotificationModal } from '@features/notification'; import { useAutoMetricsSender, useSessionCleanup, From 6f93b1be91d7225addd2a8606648453532e6b5ec Mon Sep 17 00:00:00 2001 From: choihooo Date: Fri, 28 Nov 2025 13:09:30 +0900 Subject: [PATCH 11/12] =?UTF-8?q?refactor(widgets):=20=EC=9C=84=EC=A0=AF?= =?UTF-8?q?=20=ED=8F=B4=EB=8D=94=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/ui/login/components/HeroSection.tsx | 4 ++-- .../features/auth/ui/login/components/Loginforrm.tsx | 4 ++-- .../auth/ui/login/components/PasswordField.tsx | 4 ++-- .../auth/ui/signup/EmailVerificationCallbackPage.tsx | 2 +- .../auth/ui/signup/components/EmailHeroSection.tsx | 2 +- .../auth/ui/signup/components/SignUpform.tsx | 4 ++-- .../src/features/calibration/ui/CalibrationPage.tsx | 2 +- .../calibration/ui/components/WebcamView.tsx | 2 +- .../dashboard/ui/AveragePosture/levelConfig.ts | 10 +++++----- .../src/features/dashboard/ui/MainHeader.tsx | 6 +++--- .../src/features/dashboard/ui/MiniRunningPanel.tsx | 2 +- .../src/features/dashboard/ui/RunningPanel.tsx | 2 +- .../src/features/dashboard/ui/TotalDistancePanel.tsx | 2 +- .../src/features/dashboard/ui/WebcamPanel.tsx | 4 ++-- .../ui/components/TimeControlSection.tsx | 4 ++-- src/renderer/src/features/onboarding/index.ts | 2 ++ .../onboarding/ui}/OnboardingCompletionPage.tsx | 4 ++-- .../onboarding/ui}/OnboardingInitPage.tsx | 0 .../onboarding/ui}/OnboardingPage.tsx | 2 +- .../ui}/components/CameraPermissionButton.tsx | 2 +- .../ui}/components/FirstImageDescription.tsx | 4 ++-- .../ui}/components/ImageDescriptionPanel.tsx | 0 .../onboarding/ui}/components/InfoPanel.tsx | 0 src/renderer/src/features/onboarding/ui/index.ts | 4 ++++ src/renderer/src/layout/Header/Header.tsx | 4 ++-- src/renderer/src/pages/Main/MainPage.tsx | 2 +- src/renderer/src/shared/config/router.tsx | 12 +++++++----- src/renderer/src/widgets/camera/index.ts | 2 ++ src/renderer/src/widgets/camera/model/index.ts | 1 + .../camera/model/use-camera-store.ts} | 0 src/renderer/src/widgets/widget/index.ts | 3 +++ src/renderer/src/widgets/widget/lib/index.ts | 2 ++ .../src/{hooks => widgets/widget/lib}/useWidget.ts | 0 .../Widget => widgets/widget/ui}/WidgetPage.tsx | 4 ++-- .../widget/ui}/WidgetTitleBar/WidgetTitleBar.tsx | 4 ++-- .../widget/ui}/components/MediumWidgetContent.tsx | 4 ++-- .../widget/ui}/components/MiniWidgetContent.tsx | 4 ++-- .../{pages/Widget => widgets/widget/ui}/data.json | 0 .../ui}/hooks/usePostureSyncWithLocalStorage.ts | 0 .../widget/ui}/hooks/useThemeSync.ts | 0 src/renderer/src/widgets/widget/ui/index.ts | 2 ++ src/renderer/tsconfig.json | 3 ++- vite.config.mts | 2 ++ 43 files changed, 71 insertions(+), 50 deletions(-) create mode 100644 src/renderer/src/features/onboarding/index.ts rename src/renderer/src/{pages/Onboarding => features/onboarding/ui}/OnboardingCompletionPage.tsx (93%) rename src/renderer/src/{pages/Onboarding => features/onboarding/ui}/OnboardingInitPage.tsx (100%) rename src/renderer/src/{pages/Onboarding => features/onboarding/ui}/OnboardingPage.tsx (94%) rename src/renderer/src/{pages/Onboarding => features/onboarding/ui}/components/CameraPermissionButton.tsx (97%) rename src/renderer/src/{pages/Onboarding => features/onboarding/ui}/components/FirstImageDescription.tsx (81%) rename src/renderer/src/{pages/Onboarding => features/onboarding/ui}/components/ImageDescriptionPanel.tsx (100%) rename src/renderer/src/{pages/Onboarding => features/onboarding/ui}/components/InfoPanel.tsx (100%) create mode 100644 src/renderer/src/features/onboarding/ui/index.ts create mode 100644 src/renderer/src/widgets/camera/index.ts create mode 100644 src/renderer/src/widgets/camera/model/index.ts rename src/renderer/src/{store/useCameraStore.ts => widgets/camera/model/use-camera-store.ts} (100%) create mode 100644 src/renderer/src/widgets/widget/index.ts create mode 100644 src/renderer/src/widgets/widget/lib/index.ts rename src/renderer/src/{hooks => widgets/widget/lib}/useWidget.ts (100%) rename src/renderer/src/{pages/Widget => widgets/widget/ui}/WidgetPage.tsx (97%) rename src/renderer/src/{components => widgets/widget/ui}/WidgetTitleBar/WidgetTitleBar.tsx (93%) rename src/renderer/src/{pages/Widget => widgets/widget/ui}/components/MediumWidgetContent.tsx (95%) rename src/renderer/src/{pages/Widget => widgets/widget/ui}/components/MiniWidgetContent.tsx (91%) rename src/renderer/src/{pages/Widget => widgets/widget/ui}/data.json (100%) rename src/renderer/src/{pages/Widget => widgets/widget/ui}/hooks/usePostureSyncWithLocalStorage.ts (100%) rename src/renderer/src/{pages/Widget => widgets/widget/ui}/hooks/useThemeSync.ts (100%) create mode 100644 src/renderer/src/widgets/widget/ui/index.ts diff --git a/src/renderer/src/features/auth/ui/login/components/HeroSection.tsx b/src/renderer/src/features/auth/ui/login/components/HeroSection.tsx index 82feec4..081a96b 100644 --- a/src/renderer/src/features/auth/ui/login/components/HeroSection.tsx +++ b/src/renderer/src/features/auth/ui/login/components/HeroSection.tsx @@ -1,5 +1,5 @@ -import Logo from '../../../../assets/logo.svg?react'; -import Symbol from '../../../../assets/symbol.svg?react'; +import Logo from '@assets/logo.svg?react'; +import Symbol from '@assets/symbol.svg?react'; export default function HeroSection() { return ( diff --git a/src/renderer/src/features/auth/ui/login/components/Loginforrm.tsx b/src/renderer/src/features/auth/ui/login/components/Loginforrm.tsx index 1bb1b70..d310553 100644 --- a/src/renderer/src/features/auth/ui/login/components/Loginforrm.tsx +++ b/src/renderer/src/features/auth/ui/login/components/Loginforrm.tsx @@ -3,8 +3,8 @@ import { TextField as TextInput } from '@shared/ui/input-field'; import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; -import FailIcon from '../../../../assets/auth/error_icon.svg?react'; -import SaveIdIcon from '../../../../assets/auth/saveid_icon.svg?react'; +import FailIcon from '@assets/auth/error_icon.svg?react'; +import SaveIdIcon from '@assets/auth/saveid_icon.svg?react'; import LoginButton from './LoginButton'; import PasswordField from './PasswordField'; diff --git a/src/renderer/src/features/auth/ui/login/components/PasswordField.tsx b/src/renderer/src/features/auth/ui/login/components/PasswordField.tsx index 12f4a9b..77a3f20 100644 --- a/src/renderer/src/features/auth/ui/login/components/PasswordField.tsx +++ b/src/renderer/src/features/auth/ui/login/components/PasswordField.tsx @@ -1,7 +1,7 @@ import { TextField as TextInput } from '@shared/ui/input-field'; import { forwardRef, useState, type ChangeEvent } from 'react'; -import InvisibleIcon from '../../../../assets/auth/invisible_icon.svg?react'; -import VisibleIcon from '../../../../assets/auth/visible_icon.svg?react'; +import InvisibleIcon from '@assets/auth/invisible_icon.svg?react'; +import VisibleIcon from '@assets/auth/visible_icon.svg?react'; interface PasswordFieldProps { hasValue?: boolean; diff --git a/src/renderer/src/features/auth/ui/signup/EmailVerificationCallbackPage.tsx b/src/renderer/src/features/auth/ui/signup/EmailVerificationCallbackPage.tsx index 16ca4fb..718f9c0 100644 --- a/src/renderer/src/features/auth/ui/signup/EmailVerificationCallbackPage.tsx +++ b/src/renderer/src/features/auth/ui/signup/EmailVerificationCallbackPage.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useVerifyEmailMutation } from '@entities/user'; -import CompletionCharacter from '../../../assets/completion.svg?react'; +import CompletionCharacter from '@assets/completion.svg?react'; const EmailVerificationCallbackPage = () => { const [searchParams] = useSearchParams() diff --git a/src/renderer/src/features/auth/ui/signup/components/EmailHeroSection.tsx b/src/renderer/src/features/auth/ui/signup/components/EmailHeroSection.tsx index 6bf65e0..eddb915 100644 --- a/src/renderer/src/features/auth/ui/signup/components/EmailHeroSection.tsx +++ b/src/renderer/src/features/auth/ui/signup/components/EmailHeroSection.tsx @@ -1,5 +1,5 @@ import { useEmailStore } from '@entities/user'; -import EmailIcon from '../../../../assets/auth/email_icon.svg?react'; +import EmailIcon from '@assets/auth/email_icon.svg?react'; export default function EmailHeroSection() { const email = useEmailStore((state) => state.email); diff --git a/src/renderer/src/features/auth/ui/signup/components/SignUpform.tsx b/src/renderer/src/features/auth/ui/signup/components/SignUpform.tsx index 65ffe5a..1c53cdd 100644 --- a/src/renderer/src/features/auth/ui/signup/components/SignUpform.tsx +++ b/src/renderer/src/features/auth/ui/signup/components/SignUpform.tsx @@ -3,8 +3,8 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { Button } from '@shared/ui/button'; import { TextField } from '@shared/ui/input-field'; import PasswordField from '../../login/components/PasswordField'; -import SuccessIcon from '../../../../assets/auth/success_icon.svg?react'; -import FailIcon from '../../../../assets/auth/error_icon.svg?react'; +import SuccessIcon from '@assets/auth/success_icon.svg?react'; +import FailIcon from '@assets/auth/error_icon.svg?react'; import { signUpSchema, SignUpFormData } from '../utils/SignupSchemas'; import { useState } from 'react'; import { diff --git a/src/renderer/src/features/calibration/ui/CalibrationPage.tsx b/src/renderer/src/features/calibration/ui/CalibrationPage.tsx index a3869f9..909aaa9 100644 --- a/src/renderer/src/features/calibration/ui/CalibrationPage.tsx +++ b/src/renderer/src/features/calibration/ui/CalibrationPage.tsx @@ -1,3 +1,4 @@ +import CalibrationGuide from '@assets/calibration_guide.svg?react'; import { PoseLandmark as AnalyzerPoseLandmark, calculateFrameBrightness, @@ -18,7 +19,6 @@ import { } from 'react'; import { useNavigate } from 'react-router-dom'; import Webcam from 'react-webcam'; -import CalibrationGuide from '../../../assets/calibration_guide.svg?react'; import MeasuringPanel from './components/MeasuringPanel'; import WebcamView from './components/WebcamView'; import WelcomePanel from './components/WelcomePanel'; diff --git a/src/renderer/src/features/calibration/ui/components/WebcamView.tsx b/src/renderer/src/features/calibration/ui/components/WebcamView.tsx index 36d293e..7167422 100644 --- a/src/renderer/src/features/calibration/ui/components/WebcamView.tsx +++ b/src/renderer/src/features/calibration/ui/components/WebcamView.tsx @@ -6,9 +6,9 @@ import { WorldLandmark, } from '@entities/posture'; import { Timer } from '@shared/ui/timer'; +import { useCameraStore } from '@widgets/camera'; import { useEffect, useRef, useState, type RefObject } from 'react'; import Webcam from 'react-webcam'; -import { useCameraStore } from '../../../../store/useCameraStore'; interface WebcamViewProps { onPoseDetected?: ( diff --git a/src/renderer/src/features/dashboard/ui/AveragePosture/levelConfig.ts b/src/renderer/src/features/dashboard/ui/AveragePosture/levelConfig.ts index c386833..e3691da 100644 --- a/src/renderer/src/features/dashboard/ui/AveragePosture/levelConfig.ts +++ b/src/renderer/src/features/dashboard/ui/AveragePosture/levelConfig.ts @@ -1,8 +1,8 @@ -import stepOneCharacter from '../../../../assets/main/averagePosture/step_one_character.png'; -import stepTwoCharacter from '../../../../assets/main/averagePosture/step_two_character.png'; -import stepThreeCharacter from '../../../../assets/main/averagePosture/step_three_character.png'; -import stepFourCharacter from '../../../../assets/main/averagePosture/step_four_character.png'; -import stepFiveCharacter from '../../../../assets/main/averagePosture/step_five_character.png'; +import stepOneCharacter from '@assets/main/averagePosture/step_one_character.png'; +import stepTwoCharacter from '@assets/main/averagePosture/step_two_character.png'; +import stepThreeCharacter from '@assets/main/averagePosture/step_three_character.png'; +import stepFourCharacter from '@assets/main/averagePosture/step_four_character.png'; +import stepFiveCharacter from '@assets/main/averagePosture/step_five_character.png'; export interface LevelInfo { level: number; diff --git a/src/renderer/src/features/dashboard/ui/MainHeader.tsx b/src/renderer/src/features/dashboard/ui/MainHeader.tsx index 283ae11..b65c27d 100644 --- a/src/renderer/src/features/dashboard/ui/MainHeader.tsx +++ b/src/renderer/src/features/dashboard/ui/MainHeader.tsx @@ -2,9 +2,9 @@ import DashboardIcon from '@assets/dashboard.svg?react'; import SettingIcon from '@assets/setting.svg?react'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import Logo from '../../../assets/logo.svg?react'; -import NotificationIcon from '../../../assets/main/bell_icon.svg?react'; -import Symbol from '../../../assets/symbol.svg?react'; +import Logo from '@assets/logo.svg?react'; +import NotificationIcon from '@assets/main/bell_icon.svg?react'; +import Symbol from '@assets/symbol.svg?react'; import { Button } from '@shared/ui/button'; import { ThemeToggleSwitch } from '@shared/ui/theme-toggle-switch'; diff --git a/src/renderer/src/features/dashboard/ui/MiniRunningPanel.tsx b/src/renderer/src/features/dashboard/ui/MiniRunningPanel.tsx index 7956f3e..d122966 100644 --- a/src/renderer/src/features/dashboard/ui/MiniRunningPanel.tsx +++ b/src/renderer/src/features/dashboard/ui/MiniRunningPanel.tsx @@ -1,4 +1,4 @@ -import { useCameraStore } from '../../../store/useCameraStore'; +import { useCameraStore } from '@widgets/camera'; import ExitPanel from './ExitPanel'; import RunningPanel from './RunningPanel'; diff --git a/src/renderer/src/features/dashboard/ui/RunningPanel.tsx b/src/renderer/src/features/dashboard/ui/RunningPanel.tsx index e0f4dc0..008540d 100644 --- a/src/renderer/src/features/dashboard/ui/RunningPanel.tsx +++ b/src/renderer/src/features/dashboard/ui/RunningPanel.tsx @@ -14,7 +14,7 @@ import StoneBugiRestSvg from '@assets/video/stone-bugi-rest.svg'; import TireBugiRestSvg from '@assets/video/tire-bugi-rest.svg'; import { useEffect, useMemo, useRef } from 'react'; -import { useCameraStore } from '../../../store/useCameraStore'; +import { useCameraStore } from '@widgets/camera'; import { usePostureStore } from '@entities/posture'; import { cn } from '@shared/lib/cn'; import { getScoreLevel } from '@shared/lib/get-score-level'; diff --git a/src/renderer/src/features/dashboard/ui/TotalDistancePanel.tsx b/src/renderer/src/features/dashboard/ui/TotalDistancePanel.tsx index a81981e..07eeb58 100644 --- a/src/renderer/src/features/dashboard/ui/TotalDistancePanel.tsx +++ b/src/renderer/src/features/dashboard/ui/TotalDistancePanel.tsx @@ -1,6 +1,6 @@ import { PannelHeader } from '@shared/ui/panel-header'; import { useLevelQuery } from '@entities/dashboard'; -import AchivementMedal from '../../../assets/main/achivement_meadl.svg?react'; +import AchivementMedal from '@assets/main/achivement_meadl.svg?react'; import { useModal } from '@shared/hooks/use-modal'; import TotalDistanceModal from './TotalDistanceModal'; diff --git a/src/renderer/src/features/dashboard/ui/WebcamPanel.tsx b/src/renderer/src/features/dashboard/ui/WebcamPanel.tsx index dc816f0..239d4a6 100644 --- a/src/renderer/src/features/dashboard/ui/WebcamPanel.tsx +++ b/src/renderer/src/features/dashboard/ui/WebcamPanel.tsx @@ -12,8 +12,8 @@ import { PoseLandmark, WorldLandmark, } from '@entities/posture'; -import { useWidget } from '../../../hooks/useWidget'; -import { useCameraStore } from '../../../store/useCameraStore'; +import { useWidget } from '@widgets/widget'; +import { useCameraStore } from '@widgets/camera'; import { WebcamView } from '@features/calibration/ui'; import { useLevelQuery } from '@entities/dashboard'; diff --git a/src/renderer/src/features/notification/ui/components/TimeControlSection.tsx b/src/renderer/src/features/notification/ui/components/TimeControlSection.tsx index 40fb93a..ba37951 100644 --- a/src/renderer/src/features/notification/ui/components/TimeControlSection.tsx +++ b/src/renderer/src/features/notification/ui/components/TimeControlSection.tsx @@ -1,7 +1,7 @@ import { NotificationToggleSwitch } from '@shared/ui/toggle-switch'; import { useTimeEditor } from '../hooks/useTimeEditor'; -import MinusIcon from '../../../../assets/main/minus_icon.svg?react'; -import PlusIcon from '../../../../assets/main/plus_icon.svg?react'; +import MinusIcon from '@assets/main/minus_icon.svg?react'; +import PlusIcon from '@assets/main/plus_icon.svg?react'; interface TimeControlSectionProps { title: string; diff --git a/src/renderer/src/features/onboarding/index.ts b/src/renderer/src/features/onboarding/index.ts new file mode 100644 index 0000000..ccf1f66 --- /dev/null +++ b/src/renderer/src/features/onboarding/index.ts @@ -0,0 +1,2 @@ +export * from './ui'; + diff --git a/src/renderer/src/pages/Onboarding/OnboardingCompletionPage.tsx b/src/renderer/src/features/onboarding/ui/OnboardingCompletionPage.tsx similarity index 93% rename from src/renderer/src/pages/Onboarding/OnboardingCompletionPage.tsx rename to src/renderer/src/features/onboarding/ui/OnboardingCompletionPage.tsx index 2f76ca5..e976bba 100644 --- a/src/renderer/src/pages/Onboarding/OnboardingCompletionPage.tsx +++ b/src/renderer/src/features/onboarding/ui/OnboardingCompletionPage.tsx @@ -1,8 +1,8 @@ import { useNavigate } from 'react-router-dom'; -import CompletionCharacter from '../../assets/completion.svg?react'; +import CompletionCharacter from '@assets/completion.svg?react'; import { Button } from '@shared/ui/button'; import { useCreateSessionMutation } from '@entities/session'; -import { useCameraStore } from '../../store/useCameraStore'; +import { useCameraStore } from '@widgets/camera'; import { useLevelQuery } from '@entities/dashboard'; const OnboardingCompletionPage = () => { diff --git a/src/renderer/src/pages/Onboarding/OnboardingInitPage.tsx b/src/renderer/src/features/onboarding/ui/OnboardingInitPage.tsx similarity index 100% rename from src/renderer/src/pages/Onboarding/OnboardingInitPage.tsx rename to src/renderer/src/features/onboarding/ui/OnboardingInitPage.tsx diff --git a/src/renderer/src/pages/Onboarding/OnboardingPage.tsx b/src/renderer/src/features/onboarding/ui/OnboardingPage.tsx similarity index 94% rename from src/renderer/src/pages/Onboarding/OnboardingPage.tsx rename to src/renderer/src/features/onboarding/ui/OnboardingPage.tsx index 99f1a1a..821b4e8 100644 --- a/src/renderer/src/pages/Onboarding/OnboardingPage.tsx +++ b/src/renderer/src/features/onboarding/ui/OnboardingPage.tsx @@ -1,4 +1,4 @@ -import CameraIcon from '../../assets/camera.svg?react'; +import CameraIcon from '@assets/camera.svg?react'; import CameraPermissionButton from './components/CameraPermissionButton'; const OnboardingPage = () => { diff --git a/src/renderer/src/pages/Onboarding/components/CameraPermissionButton.tsx b/src/renderer/src/features/onboarding/ui/components/CameraPermissionButton.tsx similarity index 97% rename from src/renderer/src/pages/Onboarding/components/CameraPermissionButton.tsx rename to src/renderer/src/features/onboarding/ui/components/CameraPermissionButton.tsx index 006e86e..d479b1b 100644 --- a/src/renderer/src/pages/Onboarding/components/CameraPermissionButton.tsx +++ b/src/renderer/src/features/onboarding/ui/components/CameraPermissionButton.tsx @@ -1,6 +1,6 @@ import { useNavigate } from 'react-router-dom'; import { Button } from '@shared/ui/button'; -import { useCameraStore } from '../../../store/useCameraStore'; +import { useCameraStore } from '@widgets/camera'; const CameraPermissionButton = () => { const navigate = useNavigate(); diff --git a/src/renderer/src/pages/Onboarding/components/FirstImageDescription.tsx b/src/renderer/src/features/onboarding/ui/components/FirstImageDescription.tsx similarity index 81% rename from src/renderer/src/pages/Onboarding/components/FirstImageDescription.tsx rename to src/renderer/src/features/onboarding/ui/components/FirstImageDescription.tsx index 61e9cd2..caeb518 100644 --- a/src/renderer/src/pages/Onboarding/components/FirstImageDescription.tsx +++ b/src/renderer/src/features/onboarding/ui/components/FirstImageDescription.tsx @@ -1,5 +1,5 @@ -import GiraffeIcon from '../../../assets/onboarding/giraffe.svg?react'; -import TurtleIcon from '../../../assets/onboarding/turtle.svg?react'; +import GiraffeIcon from '@assets/onboarding/giraffe.svg?react'; +import TurtleIcon from '@assets/onboarding/turtle.svg?react'; const FirstImageDescription = () => { const userName = localStorage.getItem('userName') || '사용자'; diff --git a/src/renderer/src/pages/Onboarding/components/ImageDescriptionPanel.tsx b/src/renderer/src/features/onboarding/ui/components/ImageDescriptionPanel.tsx similarity index 100% rename from src/renderer/src/pages/Onboarding/components/ImageDescriptionPanel.tsx rename to src/renderer/src/features/onboarding/ui/components/ImageDescriptionPanel.tsx diff --git a/src/renderer/src/pages/Onboarding/components/InfoPanel.tsx b/src/renderer/src/features/onboarding/ui/components/InfoPanel.tsx similarity index 100% rename from src/renderer/src/pages/Onboarding/components/InfoPanel.tsx rename to src/renderer/src/features/onboarding/ui/components/InfoPanel.tsx diff --git a/src/renderer/src/features/onboarding/ui/index.ts b/src/renderer/src/features/onboarding/ui/index.ts new file mode 100644 index 0000000..bf9a24b --- /dev/null +++ b/src/renderer/src/features/onboarding/ui/index.ts @@ -0,0 +1,4 @@ +export { default as OnboardingPage } from './OnboardingPage'; +export { default as OnboardingInitPage } from './OnboardingInitPage'; +export { default as OnboardingCompletionPage } from './OnboardingCompletionPage'; + diff --git a/src/renderer/src/layout/Header/Header.tsx b/src/renderer/src/layout/Header/Header.tsx index 33ff94b..139109b 100644 --- a/src/renderer/src/layout/Header/Header.tsx +++ b/src/renderer/src/layout/Header/Header.tsx @@ -1,5 +1,5 @@ -import Logo from '../../assets/logo.svg?react'; -import Symbol from '../../assets/symbol.svg?react'; +import Logo from '@assets/logo.svg?react'; +import Symbol from '@assets/symbol.svg?react'; import { ThemeToggleSwitch } from '@shared/ui/theme-toggle-switch'; import { useThemePreference } from '@shared/hooks/use-theme-preference'; diff --git a/src/renderer/src/pages/Main/MainPage.tsx b/src/renderer/src/pages/Main/MainPage.tsx index 36c9702..0f891ae 100644 --- a/src/renderer/src/pages/Main/MainPage.tsx +++ b/src/renderer/src/pages/Main/MainPage.tsx @@ -26,7 +26,7 @@ import { WebcamPanel, } from '@features/dashboard'; import { useNotificationScheduler } from '@features/calibration'; -import { useCameraStore } from '../../store/useCameraStore'; +import { useCameraStore } from '@widgets/camera'; const LOCAL_STORAGE_KEY = 'calibration_result_v1'; diff --git a/src/renderer/src/shared/config/router.tsx b/src/renderer/src/shared/config/router.tsx index e49e951..76dc037 100644 --- a/src/renderer/src/shared/config/router.tsx +++ b/src/renderer/src/shared/config/router.tsx @@ -4,10 +4,12 @@ import { createBrowserRouter, redirect } from 'react-router-dom'; import Layout from '../../layout/Layout'; import { CalibrationPage } from '@features/calibration'; import MainPage from '../../pages/Main/MainPage'; -import OnboardingCompletionPage from '../../pages/Onboarding/OnboardingCompletionPage'; -import OnboardinInitPage from '../../pages/Onboarding/OnboardingInitPage'; -import OnboardingPage from '../../pages/Onboarding/OnboardingPage'; -import { WidgetPage } from '../../pages/Widget/WidgetPage'; +import { + OnboardingPage, + OnboardingInitPage, + OnboardingCompletionPage, +} from '@features/onboarding'; +import { WidgetPage } from '@widgets/widget'; // 인증이 필요한 페이지용 loader const requireAuthLoader = async () => { @@ -80,7 +82,7 @@ export const router = createBrowserRouter([ { path: '', element: }, { path: 'calibration', element: }, { path: 'completion', element: }, - { path: 'init', element: }, + { path: 'init', element: }, ], }, { diff --git a/src/renderer/src/widgets/camera/index.ts b/src/renderer/src/widgets/camera/index.ts new file mode 100644 index 0000000..450f009 --- /dev/null +++ b/src/renderer/src/widgets/camera/index.ts @@ -0,0 +1,2 @@ +export * from './model'; + diff --git a/src/renderer/src/widgets/camera/model/index.ts b/src/renderer/src/widgets/camera/model/index.ts new file mode 100644 index 0000000..4d233b2 --- /dev/null +++ b/src/renderer/src/widgets/camera/model/index.ts @@ -0,0 +1 @@ +export { useCameraStore } from './use-camera-store'; diff --git a/src/renderer/src/store/useCameraStore.ts b/src/renderer/src/widgets/camera/model/use-camera-store.ts similarity index 100% rename from src/renderer/src/store/useCameraStore.ts rename to src/renderer/src/widgets/camera/model/use-camera-store.ts diff --git a/src/renderer/src/widgets/widget/index.ts b/src/renderer/src/widgets/widget/index.ts new file mode 100644 index 0000000..6a63040 --- /dev/null +++ b/src/renderer/src/widgets/widget/index.ts @@ -0,0 +1,3 @@ +export * from './ui'; +export * from './lib'; + diff --git a/src/renderer/src/widgets/widget/lib/index.ts b/src/renderer/src/widgets/widget/lib/index.ts new file mode 100644 index 0000000..946de13 --- /dev/null +++ b/src/renderer/src/widgets/widget/lib/index.ts @@ -0,0 +1,2 @@ +export { useWidget } from './useWidget'; + diff --git a/src/renderer/src/hooks/useWidget.ts b/src/renderer/src/widgets/widget/lib/useWidget.ts similarity index 100% rename from src/renderer/src/hooks/useWidget.ts rename to src/renderer/src/widgets/widget/lib/useWidget.ts diff --git a/src/renderer/src/pages/Widget/WidgetPage.tsx b/src/renderer/src/widgets/widget/ui/WidgetPage.tsx similarity index 97% rename from src/renderer/src/pages/Widget/WidgetPage.tsx rename to src/renderer/src/widgets/widget/ui/WidgetPage.tsx index b6e469f..d8cab89 100644 --- a/src/renderer/src/pages/Widget/WidgetPage.tsx +++ b/src/renderer/src/widgets/widget/ui/WidgetPage.tsx @@ -1,12 +1,12 @@ /* 위젯 창에 표시될 페이지 - 반응형 */ -import { useEffect, useState } from 'react'; -import { WidgetTitleBar } from '../../components/WidgetTitleBar/WidgetTitleBar'; import { usePostureStore } from '@entities/posture'; +import { useEffect, useState } from 'react'; import { MediumWidgetContent } from './components/MediumWidgetContent'; import { MiniWidgetContent } from './components/MiniWidgetContent'; import { usePostureSyncWithLocalStorage } from './hooks/usePostureSyncWithLocalStorage'; import { useThemeSync } from './hooks/useThemeSync'; +import { WidgetTitleBar } from './WidgetTitleBar/WidgetTitleBar'; type WidgetSize = 'mini' | 'medium'; diff --git a/src/renderer/src/components/WidgetTitleBar/WidgetTitleBar.tsx b/src/renderer/src/widgets/widget/ui/WidgetTitleBar/WidgetTitleBar.tsx similarity index 93% rename from src/renderer/src/components/WidgetTitleBar/WidgetTitleBar.tsx rename to src/renderer/src/widgets/widget/ui/WidgetTitleBar/WidgetTitleBar.tsx index 685cedb..651b90c 100644 --- a/src/renderer/src/components/WidgetTitleBar/WidgetTitleBar.tsx +++ b/src/renderer/src/widgets/widget/ui/WidgetTitleBar/WidgetTitleBar.tsx @@ -1,5 +1,5 @@ -import MediumDragIcon from '../../assets/widget/drag_icon.svg?react'; -import MiniDragIcon from '../../assets/widget/mini_drag_icon.svg?react'; +import MediumDragIcon from '@assets/widget/drag_icon.svg?react'; +import MiniDragIcon from '@assets/widget/mini_drag_icon.svg?react'; interface WidgetTitleBarProps { onClose?: () => void; diff --git a/src/renderer/src/pages/Widget/components/MediumWidgetContent.tsx b/src/renderer/src/widgets/widget/ui/components/MediumWidgetContent.tsx similarity index 95% rename from src/renderer/src/pages/Widget/components/MediumWidgetContent.tsx rename to src/renderer/src/widgets/widget/ui/components/MediumWidgetContent.tsx index 8b05d24..c2ec495 100644 --- a/src/renderer/src/pages/Widget/components/MediumWidgetContent.tsx +++ b/src/renderer/src/widgets/widget/ui/components/MediumWidgetContent.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; -import MediumGiraffe from '../../../assets/widget/medium_giraffe.svg?react'; -import MediumTurtle from '../../../assets/widget/medium_turtle.svg?react'; +import MediumGiraffe from '@assets/widget/medium_giraffe.svg?react'; +import MediumTurtle from '@assets/widget/medium_turtle.svg?react'; import messages from '../data.json'; /* 실시간 자세 판별 */ diff --git a/src/renderer/src/pages/Widget/components/MiniWidgetContent.tsx b/src/renderer/src/widgets/widget/ui/components/MiniWidgetContent.tsx similarity index 91% rename from src/renderer/src/pages/Widget/components/MiniWidgetContent.tsx rename to src/renderer/src/widgets/widget/ui/components/MiniWidgetContent.tsx index 77cf641..bddbb50 100644 --- a/src/renderer/src/pages/Widget/components/MiniWidgetContent.tsx +++ b/src/renderer/src/widgets/widget/ui/components/MiniWidgetContent.tsx @@ -1,5 +1,5 @@ -import MiniGiraffe from '../../../assets/widget/mini_giraffe.svg?react'; -import MiniTurtle from '../../../assets/widget/mini_turtle.svg?react'; +import MiniGiraffe from '@assets/widget/mini_giraffe.svg?react'; +import MiniTurtle from '@assets/widget/mini_turtle.svg?react'; /* 실시간 자세 판별 */ type PostureState = 0 | 1 | 2 | 3 | 4 | 5 | 6; diff --git a/src/renderer/src/pages/Widget/data.json b/src/renderer/src/widgets/widget/ui/data.json similarity index 100% rename from src/renderer/src/pages/Widget/data.json rename to src/renderer/src/widgets/widget/ui/data.json diff --git a/src/renderer/src/pages/Widget/hooks/usePostureSyncWithLocalStorage.ts b/src/renderer/src/widgets/widget/ui/hooks/usePostureSyncWithLocalStorage.ts similarity index 100% rename from src/renderer/src/pages/Widget/hooks/usePostureSyncWithLocalStorage.ts rename to src/renderer/src/widgets/widget/ui/hooks/usePostureSyncWithLocalStorage.ts diff --git a/src/renderer/src/pages/Widget/hooks/useThemeSync.ts b/src/renderer/src/widgets/widget/ui/hooks/useThemeSync.ts similarity index 100% rename from src/renderer/src/pages/Widget/hooks/useThemeSync.ts rename to src/renderer/src/widgets/widget/ui/hooks/useThemeSync.ts diff --git a/src/renderer/src/widgets/widget/ui/index.ts b/src/renderer/src/widgets/widget/ui/index.ts new file mode 100644 index 0000000..5184557 --- /dev/null +++ b/src/renderer/src/widgets/widget/ui/index.ts @@ -0,0 +1,2 @@ +export { WidgetPage } from './WidgetPage'; +export { WidgetTitleBar } from './WidgetTitleBar/WidgetTitleBar'; diff --git a/src/renderer/tsconfig.json b/src/renderer/tsconfig.json index 11aab98..e224f15 100644 --- a/src/renderer/tsconfig.json +++ b/src/renderer/tsconfig.json @@ -28,7 +28,8 @@ "@utils/*": ["./src/utils/*"], "@shared/*": ["./src/shared/*"], "@entities/*": ["./src/entities/*"], - "@features/*": ["./src/features/*"] + "@features/*": ["./src/features/*"], + "@widgets/*": ["./src/widgets/*"] } }, "include": [ diff --git a/vite.config.mts b/vite.config.mts index 1482221..c49c15b 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -29,6 +29,8 @@ export default defineConfig({ '@entities': path.resolve(__dirname, 'src/renderer/src/entities'), '@features/': path.resolve(__dirname, 'src/renderer/src/features') + '/', '@features': path.resolve(__dirname, 'src/renderer/src/features'), + '@widgets/': path.resolve(__dirname, 'src/renderer/src/widgets') + '/', + '@widgets': path.resolve(__dirname, 'src/renderer/src/widgets'), ui: path.resolve(__dirname, 'src/renderer/src/components'), }, }, From 5fc350af2b06f673e2be9e51c90847cd37ea6446 Mon Sep 17 00:00:00 2001 From: choihooo Date: Fri, 28 Nov 2025 17:08:19 +0900 Subject: [PATCH 12/12] =?UTF-8?q?refactor:=20=ED=8F=B4=EB=8D=94=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ARCHITECTURE.md | 723 ++++++++++++++++++ src/features/dashboard/ui/index.ts | 1 - src/main/src/security-restrictions.ts | 1 - src/preload/exposedInMainWorld.d.ts | 6 +- src/renderer/index.html | 2 +- .../dev}/DevNavbar/DevNavbar.tsx | 0 src/renderer/src/app/dev/index.ts | 1 + src/renderer/src/app/index.ts | 2 + .../src/{layout => app/layouts}/Layout.tsx | 3 +- .../Header => app/layouts/header}/Header.tsx | 4 +- src/renderer/src/app/layouts/index.ts | 2 + src/renderer/src/{ => app}/main.tsx | 4 +- src/renderer/src/{ => app/providers}/App.tsx | 0 src/renderer/src/app/providers/index.ts | 1 + .../{ => common/icons}/arrow-narrow-down.svg | 0 .../{ => common/icons}/arrow-narrow-up.svg | 0 .../assets/{ => common/icons}/calendar.svg | 0 .../src/assets/{ => common/icons}/camera.svg | 0 .../{ => common/icons}/chevron-right.svg | 0 .../src/assets/{ => common/icons}/clock.svg | 0 .../assets/{ => common/icons}/completion.svg | 0 .../assets/{ => common/icons}/dashboard.svg | 0 .../src/assets/{ => common/icons}/hide.svg | 0 .../assets/{ => common/icons}/hourglass.svg | 0 .../assets/{ => common/icons}/info-circle.svg | 0 .../src/assets/{ => common/icons}/logo.svg | 0 .../assets/{ => common/icons}/moon_icon.svg | 0 .../{ => common/icons}/page-move-button.svg | 0 .../src/assets/{ => common/icons}/plan.svg | 0 .../src/assets/{ => common/icons}/profile.svg | 0 .../src/assets/{ => common/icons}/setting.svg | 0 .../src/assets/{ => common/icons}/show.svg | 0 .../src/assets/{ => common/icons}/sleep.svg | 0 .../assets/{ => common/icons}/sun_icon.svg | 0 .../src/assets/{ => common/icons}/symbol.svg | 0 .../src/assets/{ => common/icons}/thumbup.svg | 0 .../src/assets/{ => common/icons}/widget.svg | 0 .../{ => common/images}/calibration_guide.svg | 0 .../assets/{ => modal}/angel-rini-modal.svg | 0 .../src/assets/{ => modal}/bugi-modal.svg | 0 .../src/assets/{ => modal}/pm-rini-modal.svg | 0 .../src/assets/{ => modal}/rini-modal.svg | 0 .../assets/{ => modal}/stone-bugi-modal.svg | 0 .../assets/{ => modal}/tire-bugi-modal.svg | 0 .../components/NotificateMessage/index.tsx | 2 - .../WidgetController/WidgetController.tsx | 77 -- src/renderer/src/components/index.ts | 6 - .../src/entities/dashboard/api/index.ts | 1 - .../dashboard/api/use-attendance-query.ts | 6 +- .../dashboard/api/use-average-score-query.ts | 1 - .../dashboard/api/use-highlight-query.ts | 6 +- .../entities/dashboard/api/use-level-query.ts | 1 - .../dashboard/api/use-posture-graph-query.ts | 1 - .../api/use-posture-pattern-query.ts | 1 - src/renderer/src/entities/dashboard/index.ts | 1 - src/renderer/src/entities/posture/index.ts | 1 - .../entities/posture/lib/PoseDetection.tsx | 315 ++++---- .../entities/posture/lib/PoseVisualizer.tsx | 1 - .../entities/posture/lib/PostureClassifier.ts | 3 +- .../entities/posture/lib/PostureStabilizer.ts | 1 - .../entities/posture/lib/ScoreProcessor.ts | 4 +- .../src/entities/posture/lib/errorChecks.ts | 1 - .../src/entities/posture/lib/index.ts | 1 - .../src/entities/posture/model/index.ts | 1 - .../posture/model/use-posture-store.ts | 1 - .../src/entities/session/api/index.ts | 1 - .../api/use-create-session-mutation.ts | 1 - .../session/api/use-pause-session-mutation.ts | 1 - .../session/api/use-save-metrics-mutation.ts | 6 +- .../session/api/use-session-report-query.ts | 1 - .../session/api/use-stop-session-mutation.ts | 1 - src/renderer/src/entities/session/index.ts | 1 - .../src/entities/session/types/index.ts | 1 - src/renderer/src/entities/user/api/index.ts | 1 - .../entities/user/api/use-login-mutation.ts | 1 - .../user/api/use-verify-email-mutation.ts | 1 - src/renderer/src/entities/user/index.ts | 1 - src/renderer/src/entities/user/model/index.ts | 1 - .../entities/user/model/use-email-store.ts | 1 - src/renderer/src/features/auth/index.ts | 2 - .../auth/ui/login/components/HeroSection.tsx | 6 +- .../auth/ui/login/components/LoginButton.tsx | 2 +- .../auth/ui/login/components/Loginforrm.tsx | 6 +- .../src/features/auth/ui/login/index.ts | 2 - .../signup/EmailVerificationCallbackPage.tsx | 40 - .../src/features/auth/ui/signup/index.ts | 4 - .../src/features/calibration/index.ts | 1 - .../src/features/calibration/lib/index.ts | 1 - .../lib/useNotificationScheduler.ts | 14 +- .../calibration/ui/components/WebcamView.tsx | 4 +- .../src/features/calibration/ui/index.ts | 2 - src/renderer/src/features/dashboard/index.ts | 1 - .../src/features/dashboard/lib/index.ts | 1 - .../features/dashboard/ui/AttendacePanel.tsx | 16 +- .../hooks/useAverageGraphChart.ts | 22 +- .../dashboard/ui/CharacterSpeedRow.tsx | 45 +- .../ui/CharacterSpeedRow/constants.ts | 43 ++ .../src/features/dashboard/ui/ExitPanel.tsx | 25 +- .../features/dashboard/ui/HighlightsPanel.tsx | 2 +- .../hooks/useHighlightsChart.ts | 30 +- .../src/features/dashboard/ui/MainHeader.tsx | 8 +- .../dashboard/ui/PosePatternPanel.tsx | 14 +- .../features/dashboard/ui/RunningPanel.tsx | 21 +- .../dashboard/ui/TotalDistanceModal.tsx | 91 +-- .../dashboard/ui/TotalDistancePanel.tsx | 4 +- .../src/features/dashboard/ui/WebcamPanel.tsx | 11 +- .../src/features/dashboard/ui/index.ts | 1 - .../src/features/notification/index.ts | 1 - .../src/features/notification/model/index.ts | 1 - .../src/features/notification/ui/index.ts | 1 - src/renderer/src/features/onboarding/index.ts | 2 - .../onboarding/ui/components/InfoPanel.tsx | 5 +- .../src/features/onboarding/ui/index.ts | 4 - src/renderer/src/index.css | 2 +- .../components/MeasuringPanel.tsx | 34 + .../components/WebcamView.tsx | 270 +++++++ .../components/WelcomePanel.tsx | 38 + .../src/pages/calibration-page/index.ts | 1 + .../calibration-page/index.tsx} | 31 +- .../email-verification-callback-page/index.ts | 1 + .../index.tsx | 40 + .../pages/email-verification-page/index.ts | 1 + .../email-verification-page/index.tsx} | 11 +- src/renderer/src/pages/index.ts | 11 + .../login-page/components/HeroSection.tsx | 16 + .../login-page/components/LoginButton.tsx | 31 + .../login-page/components/Loginforrm.tsx | 137 ++++ .../login-page/components/PasswordField.tsx | 59 ++ src/renderer/src/pages/login-page/index.ts | 1 + .../login-page/index.tsx} | 0 src/renderer/src/pages/main-page/index.ts | 1 + .../MainPage.tsx => main-page/index.tsx} | 0 .../pages/onboarding-completion-page/index.ts | 1 + .../onboarding-completion-page/index.tsx} | 2 +- .../src/pages/onboarding-init-page/index.ts | 1 + .../onboarding-init-page/index.tsx} | 4 +- .../components/CameraPermissionButton.tsx | 84 ++ .../components/FirstImageDescription.tsx | 20 + .../components/ImageDescriptionPanel.tsx | 118 +++ .../onboarding-page/components/InfoPanel.tsx | 114 +++ .../src/pages/onboarding-page/index.ts | 1 + .../onboarding-page/index.tsx} | 2 +- .../pages/resend-verification-page/index.ts | 1 + .../resend-verification-page/index.tsx} | 8 +- .../components/EmailHeroSection.tsx | 26 + .../components/ResendEmailHeroSection.tsx | 14 + .../signup-page/components/ResendSection.tsx | 17 + .../signup-page/components/SignUpform.tsx | 244 ++++++ .../signup-page/components/VerifyAction.tsx | 22 + src/renderer/src/pages/signup-page/index.ts | 1 + .../signup-page/index.tsx} | 0 .../pages/signup-page/utils/SignupSchemas.ts | 28 + .../WidgetTitleBar/WidgetTitleBar.tsx | 75 ++ .../components/MediumWidgetContent.tsx | 102 +++ .../components/MiniWidgetContent.tsx | 54 ++ src/renderer/src/pages/widget-page/data.json | 63 ++ .../hooks/usePostureSyncWithLocalStorage.ts | 43 ++ .../pages/widget-page/hooks/useThemeSync.ts | 28 + src/renderer/src/pages/widget-page/index.ts | 1 + .../widget-page/index.tsx} | 6 +- src/renderer/src/shared/api/index.ts | 1 - src/renderer/src/shared/api/instance.ts | 2 +- src/renderer/src/shared/config/router.tsx | 155 ++-- src/renderer/src/shared/lib/index.ts | 1 - src/renderer/src/{ => shared}/styles/base.css | 0 .../src/{ => shared}/styles/breakpoint.css | 0 .../src/{ => shared}/styles/colors.css | 0 .../src/{ => shared}/styles/fonts.css | 0 .../src/{ => shared}/styles/globals.css | 0 .../src/{ => shared}/styles/typography.css | 0 src/renderer/src/shared/types/svg.d.ts | 1 - src/renderer/src/shared/types/vite-env.d.ts | 2 +- src/renderer/src/shared/ui/button/Button.tsx | 58 +- .../src/shared/ui/button/buttonVariants.ts | 28 + src/renderer/src/shared/ui/button/index.ts | 4 +- .../src/shared/ui/input-field/TextField.tsx | 1 - .../src/shared/ui/input-field/index.ts | 1 - .../ui/intensity-slider/IntensitySlider.tsx | 1 - .../src/shared/ui/intensity-slider/index.ts | 1 - .../src/shared/ui/modal/ModalPortal.ts | 1 - src/renderer/src/shared/ui/modal/index.ts | 1 - .../NotificateMessage.tsx | 6 - .../shared/ui/notification-message/icons.tsx | 1 - .../shared/ui/notification-message/index.ts | 1 - .../ui/page-move-button/PageMoveButton.tsx | 3 +- .../src/shared/ui/page-move-button/index.ts | 1 - .../shared/ui/panel-header/PannelHeader.tsx | 3 +- .../src/shared/ui/panel-header/index.ts | 1 - .../theme-toggle-switch/ThemeToggleSwitch.tsx | 5 +- .../shared/ui/theme-toggle-switch/index.ts | 1 - src/renderer/src/shared/ui/timer/Timer.tsx | 4 +- src/renderer/src/shared/ui/timer/index.ts | 1 - .../NotificationToggleSwitch.tsx | 1 - .../shared/ui/toggle-switch/ToggleSwitch.tsx | 1 - .../src/shared/ui/toggle-switch/index.ts | 1 - .../src/shared/ui/typography/Typography.tsx | 1 - .../src/shared/ui/typography/index.ts | 1 - src/renderer/src/widgets/camera/index.ts | 1 - src/renderer/src/widgets/widget/index.ts | 2 - src/renderer/src/widgets/widget/lib/index.ts | 1 - src/renderer/src/widgets/widget/ui/index.ts | 2 - src/renderer/tsconfig.json | 18 +- 202 files changed, 2923 insertions(+), 790 deletions(-) create mode 100644 docs/ARCHITECTURE.md rename src/renderer/src/{components => app/dev}/DevNavbar/DevNavbar.tsx (100%) create mode 100644 src/renderer/src/app/dev/index.ts create mode 100644 src/renderer/src/app/index.ts rename src/renderer/src/{layout => app/layouts}/Layout.tsx (75%) rename src/renderer/src/{layout/Header => app/layouts/header}/Header.tsx (86%) create mode 100644 src/renderer/src/app/layouts/index.ts rename src/renderer/src/{ => app}/main.tsx (89%) rename src/renderer/src/{ => app/providers}/App.tsx (100%) create mode 100644 src/renderer/src/app/providers/index.ts rename src/renderer/src/assets/{ => common/icons}/arrow-narrow-down.svg (100%) rename src/renderer/src/assets/{ => common/icons}/arrow-narrow-up.svg (100%) rename src/renderer/src/assets/{ => common/icons}/calendar.svg (100%) rename src/renderer/src/assets/{ => common/icons}/camera.svg (100%) rename src/renderer/src/assets/{ => common/icons}/chevron-right.svg (100%) rename src/renderer/src/assets/{ => common/icons}/clock.svg (100%) rename src/renderer/src/assets/{ => common/icons}/completion.svg (100%) rename src/renderer/src/assets/{ => common/icons}/dashboard.svg (100%) rename src/renderer/src/assets/{ => common/icons}/hide.svg (100%) rename src/renderer/src/assets/{ => common/icons}/hourglass.svg (100%) rename src/renderer/src/assets/{ => common/icons}/info-circle.svg (100%) rename src/renderer/src/assets/{ => common/icons}/logo.svg (100%) rename src/renderer/src/assets/{ => common/icons}/moon_icon.svg (100%) rename src/renderer/src/assets/{ => common/icons}/page-move-button.svg (100%) rename src/renderer/src/assets/{ => common/icons}/plan.svg (100%) rename src/renderer/src/assets/{ => common/icons}/profile.svg (100%) rename src/renderer/src/assets/{ => common/icons}/setting.svg (100%) rename src/renderer/src/assets/{ => common/icons}/show.svg (100%) rename src/renderer/src/assets/{ => common/icons}/sleep.svg (100%) rename src/renderer/src/assets/{ => common/icons}/sun_icon.svg (100%) rename src/renderer/src/assets/{ => common/icons}/symbol.svg (100%) rename src/renderer/src/assets/{ => common/icons}/thumbup.svg (100%) rename src/renderer/src/assets/{ => common/icons}/widget.svg (100%) rename src/renderer/src/assets/{ => common/images}/calibration_guide.svg (100%) rename src/renderer/src/assets/{ => modal}/angel-rini-modal.svg (100%) rename src/renderer/src/assets/{ => modal}/bugi-modal.svg (100%) rename src/renderer/src/assets/{ => modal}/pm-rini-modal.svg (100%) rename src/renderer/src/assets/{ => modal}/rini-modal.svg (100%) rename src/renderer/src/assets/{ => modal}/stone-bugi-modal.svg (100%) rename src/renderer/src/assets/{ => modal}/tire-bugi-modal.svg (100%) delete mode 100644 src/renderer/src/components/NotificateMessage/index.tsx delete mode 100644 src/renderer/src/components/WidgetController/WidgetController.tsx delete mode 100644 src/renderer/src/components/index.ts delete mode 100644 src/renderer/src/features/auth/index.ts delete mode 100644 src/renderer/src/features/auth/ui/login/index.ts delete mode 100644 src/renderer/src/features/auth/ui/signup/EmailVerificationCallbackPage.tsx delete mode 100644 src/renderer/src/features/auth/ui/signup/index.ts create mode 100644 src/renderer/src/features/dashboard/ui/CharacterSpeedRow/constants.ts delete mode 100644 src/renderer/src/features/onboarding/index.ts delete mode 100644 src/renderer/src/features/onboarding/ui/index.ts create mode 100644 src/renderer/src/pages/calibration-page/components/MeasuringPanel.tsx create mode 100644 src/renderer/src/pages/calibration-page/components/WebcamView.tsx create mode 100644 src/renderer/src/pages/calibration-page/components/WelcomePanel.tsx create mode 100644 src/renderer/src/pages/calibration-page/index.ts rename src/renderer/src/{features/calibration/ui/CalibrationPage.tsx => pages/calibration-page/index.tsx} (91%) create mode 100644 src/renderer/src/pages/email-verification-callback-page/index.ts create mode 100644 src/renderer/src/pages/email-verification-callback-page/index.tsx create mode 100644 src/renderer/src/pages/email-verification-page/index.ts rename src/renderer/src/{features/auth/ui/signup/EmailVerificationPage.tsx => pages/email-verification-page/index.tsx} (83%) create mode 100644 src/renderer/src/pages/index.ts create mode 100644 src/renderer/src/pages/login-page/components/HeroSection.tsx create mode 100644 src/renderer/src/pages/login-page/components/LoginButton.tsx create mode 100644 src/renderer/src/pages/login-page/components/Loginforrm.tsx create mode 100644 src/renderer/src/pages/login-page/components/PasswordField.tsx create mode 100644 src/renderer/src/pages/login-page/index.ts rename src/renderer/src/{features/auth/ui/login/LoginPage.tsx => pages/login-page/index.tsx} (100%) create mode 100644 src/renderer/src/pages/main-page/index.ts rename src/renderer/src/pages/{Main/MainPage.tsx => main-page/index.tsx} (100%) create mode 100644 src/renderer/src/pages/onboarding-completion-page/index.ts rename src/renderer/src/{features/onboarding/ui/OnboardingCompletionPage.tsx => pages/onboarding-completion-page/index.tsx} (96%) create mode 100644 src/renderer/src/pages/onboarding-init-page/index.ts rename src/renderer/src/{features/onboarding/ui/OnboardingInitPage.tsx => pages/onboarding-init-page/index.tsx} (89%) create mode 100644 src/renderer/src/pages/onboarding-page/components/CameraPermissionButton.tsx create mode 100644 src/renderer/src/pages/onboarding-page/components/FirstImageDescription.tsx create mode 100644 src/renderer/src/pages/onboarding-page/components/ImageDescriptionPanel.tsx create mode 100644 src/renderer/src/pages/onboarding-page/components/InfoPanel.tsx create mode 100644 src/renderer/src/pages/onboarding-page/index.ts rename src/renderer/src/{features/onboarding/ui/OnboardingPage.tsx => pages/onboarding-page/index.tsx} (94%) create mode 100644 src/renderer/src/pages/resend-verification-page/index.ts rename src/renderer/src/{features/auth/ui/signup/ResendVerificationPage.tsx => pages/resend-verification-page/index.tsx} (83%) create mode 100644 src/renderer/src/pages/signup-page/components/EmailHeroSection.tsx create mode 100644 src/renderer/src/pages/signup-page/components/ResendEmailHeroSection.tsx create mode 100644 src/renderer/src/pages/signup-page/components/ResendSection.tsx create mode 100644 src/renderer/src/pages/signup-page/components/SignUpform.tsx create mode 100644 src/renderer/src/pages/signup-page/components/VerifyAction.tsx create mode 100644 src/renderer/src/pages/signup-page/index.ts rename src/renderer/src/{features/auth/ui/signup/SignUpPage.tsx => pages/signup-page/index.tsx} (100%) create mode 100644 src/renderer/src/pages/signup-page/utils/SignupSchemas.ts create mode 100644 src/renderer/src/pages/widget-page/WidgetTitleBar/WidgetTitleBar.tsx create mode 100644 src/renderer/src/pages/widget-page/components/MediumWidgetContent.tsx create mode 100644 src/renderer/src/pages/widget-page/components/MiniWidgetContent.tsx create mode 100644 src/renderer/src/pages/widget-page/data.json create mode 100644 src/renderer/src/pages/widget-page/hooks/usePostureSyncWithLocalStorage.ts create mode 100644 src/renderer/src/pages/widget-page/hooks/useThemeSync.ts create mode 100644 src/renderer/src/pages/widget-page/index.ts rename src/renderer/src/{widgets/widget/ui/WidgetPage.tsx => pages/widget-page/index.tsx} (98%) rename src/renderer/src/{ => shared}/styles/base.css (100%) rename src/renderer/src/{ => shared}/styles/breakpoint.css (100%) rename src/renderer/src/{ => shared}/styles/colors.css (100%) rename src/renderer/src/{ => shared}/styles/fonts.css (100%) rename src/renderer/src/{ => shared}/styles/globals.css (100%) rename src/renderer/src/{ => shared}/styles/typography.css (100%) create mode 100644 src/renderer/src/shared/ui/button/buttonVariants.ts delete mode 100644 src/renderer/src/widgets/widget/ui/index.ts diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..b0a1660 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,723 @@ +# 프로젝트 아키텍처 문서 + +이 문서는 프로젝트의 전체 구조와 아키텍처를 설명합니다. + +## 📋 목차 + +1. [Electron 프로세스 구조](#electron-프로세스-구조) +2. [FSD 아키텍처 (Renderer Process)](#fsd-아키텍처-renderer-process) +3. [프로젝트 전체 구조](#프로젝트-전체-구조) +4. [레이어별 설명](#레이어별-설명) +5. [의존성 규칙](#의존성-규칙) +6. [Path Alias 설정](#path-alias-설정) +7. [프로세스 간 통신 (IPC)](#프로세스-간-통신-ipc) +8. [Assets 폴더 구조](#assets-폴더-구조) + +--- + +## Electron 프로세스 구조 + +이 프로젝트는 **Electron** 데스크톱 애플리케이션으로, 다음과 같은 프로세스 구조를 가집니다: + +### 프로세스 분리 + +``` +src/ +├── main/ # Main Process (Node.js 환경) +├── preload/ # Preload Scripts (보안 브리지) +└── renderer/ # Renderer Process (React 앱) +``` + +**프로세스 구조 다이어그램**: + +```mermaid +graph TB + Main[Main Process
Node.js 환경] + Preload[Preload Scripts
Context Bridge] + Renderer[Renderer Process
React 앱] + OS[운영체제] + + Main -->|IPC 통신| Preload + Preload -->|Context Bridge| Renderer + Renderer -->|window.electronAPI| Preload + Main -.->|창 관리/시스템 API| OS +``` + +**참고**: 이 구조는 **Electron의 표준 아키텍처 패턴**입니다. + +- **Main Process**: 애플리케이션의 진입점, 창 관리, 시스템 API 접근 +- **Preload Scripts**: 보안을 위한 Context Bridge, Main과 Renderer 간 안전한 통신 +- **Renderer Process**: React 앱이 실행되는 브라우저 환경 + +**관련 문서**: + +- [Electron 프로세스 모델](https://www.electronjs.org/docs/latest/tutorial/process-model) - **표준 프로세스 구조 설명** +- [Context Isolation](https://www.electronjs.org/docs/latest/tutorial/context-isolation) - **Preload 스크립트 사용 이유** +- [IPC 통신](https://www.electronjs.org/docs/latest/tutorial/ipc) - **프로세스 간 통신 방법** + +--- + +## FSD 아키텍처 (Renderer Process) + +Renderer Process는 **Feature-Sliced Design (FSD)** 방법론을 따릅니다. + +### 레이어 구조 (하위 → 상위) + +``` +app → pages → widgets → features → entities → shared +``` + +**FSD 레이어 계층 구조**: + +```mermaid +graph TD + App[app
애플리케이션 초기화] + Pages[pages
페이지 컴포넌트] + Widgets[widgets
독립적인 UI 블록] + Features[features
비즈니스 기능] + Entities[entities
비즈니스 엔티티] + Shared[shared
공유 코드] + + App --> Pages + Pages --> Widgets + Pages --> Features + Widgets --> Features + Features --> Entities + Features --> Shared + Entities --> Shared +``` + +**의존성 규칙**: 하위 레이어는 상위 레이어에 의존할 수 없습니다. + +### 핵심 원칙 + +1. **계층화 (Layering)**: 코드를 명확한 계층으로 분리 +2. **슬라이싱 (Slicing)**: 각 계층을 기능별로 세분화 +3. **세그멘테이션 (Segmentation)**: 각 슬라이스를 세그먼트로 분리 + +--- + +## 프로젝트 전체 구조 + +``` +src/ +├── main/ # Main Process (Electron) +│ ├── src/ +│ │ ├── index.ts # 메인 프로세스 진입점 +│ │ ├── mainWindow.ts # 메인 윈도우 관리 +│ │ ├── widgetWindow.ts # 위젯 윈도우 관리 +│ │ ├── notificationHandlers.ts # 알림 핸들러 +│ │ ├── security-restrictions.ts # 보안 제한 설정 +│ │ ├── widgetConfig.ts # 위젯 설정 +│ │ └── utils/ # 유틸리티 함수 +│ ├── tsconfig.json +│ └── vite.config.js +│ +├── preload/ # Preload Scripts (Context Bridge) +│ ├── src/ +│ │ ├── index.ts # Preload 스크립트 진입점 +│ │ └── sha256sum.ts # 해시 유틸리티 +│ ├── exposedInMainWorld.d.ts # 타입 정의 (자동 생성) +│ ├── tsconfig.json +│ └── vite.config.js +│ +└── renderer/ # Renderer Process (React 앱) + ├── src/ + │ ├── app/ # 애플리케이션 초기화 및 설정 + │ │ ├── dev/ # 개발용 컴포넌트 + │ │ │ └── DevNavbar/ + │ │ ├── layouts/ # 레이아웃 컴포넌트 + │ │ │ ├── header/ # 헤더 컴포넌트 + │ │ │ └── Layout.tsx + │ │ ├── providers/ # 전역 프로바이더 + │ │ │ └── App.tsx # QueryClientProvider, RouterProvider + │ │ └── main.tsx # 애플리케이션 진입점 + │ │ + │ ├── pages/ # 페이지 컴포넌트 + │ │ ├── calibration-page/ + │ │ ├── email-verification-callback-page/ + │ │ ├── email-verification-page/ + │ │ ├── login-page/ + │ │ ├── main-page/ + │ │ ├── onboarding-completion-page/ + │ │ ├── onboarding-init-page/ + │ │ ├── onboarding-page/ + │ │ ├── resend-verification-page/ + │ │ ├── signup-page/ + │ │ └── widget-page/ + │ │ + │ ├── widgets/ # 위젯 레이어 (독립적인 UI 블록) + │ │ ├── camera/ # 카메라 위젯 + │ │ │ └── model/ # 카메라 상태 관리 + │ │ └── widget/ # 위젯 창 + │ │ ├── lib/ # 위젯 로직 + │ │ └── ui/ # 위젯 UI 컴포넌트 + │ │ + │ ├── features/ # 기능 레이어 (비즈니스 기능) + │ │ ├── auth/ # 인증 기능 + │ │ │ └── ui/ # 로그인, 회원가입 UI + │ │ ├── calibration/ # 캘리브레이션 기능 + │ │ │ ├── lib/ # 캘리브레이션 로직 + │ │ │ └── ui/ # 캘리브레이션 UI + │ │ ├── dashboard/ # 대시보드 기능 + │ │ │ ├── lib/ # 대시보드 로직 + │ │ │ └── ui/ # 대시보드 UI 컴포넌트 + │ │ ├── notification/ # 알림 기능 + │ │ │ ├── model/ # 알림 상태 관리 + │ │ │ └── ui/ # 알림 UI + │ │ └── onboarding/ # 온보딩 기능 + │ │ └── ui/ # 온보딩 UI + │ │ + │ ├── entities/ # 엔티티 레이어 (비즈니스 엔티티) + │ │ ├── dashboard/ # 대시보드 엔티티 + │ │ │ ├── api/ # 대시보드 API 훅 + │ │ │ └── types/ # 대시보드 타입 정의 + │ │ ├── posture/ # 자세 분석 엔티티 + │ │ │ ├── lib/ # 자세 분석 로직 + │ │ │ └── model/ # 자세 상태 관리 + │ │ ├── session/ # 세션 엔티티 + │ │ │ ├── api/ # 세션 API 훅 + │ │ │ └── types/ # 세션 타입 정의 + │ │ └── user/ # 사용자 엔티티 + │ │ ├── api/ # 사용자 API 훅 + │ │ ├── model/ # 사용자 상태 관리 + │ │ └── types/ # 사용자 타입 정의 + │ │ + │ ├── shared/ # 공유 레이어 (공통 코드) + │ │ ├── api/ # API 인스턴스 + │ │ │ └── instance.ts # Axios 인스턴스 및 인터셉터 + │ │ ├── config/ # 설정 + │ │ │ └── router.tsx # React Router 설정 + │ │ ├── hooks/ # 공유 훅 + │ │ │ ├── use-modal.ts + │ │ │ └── use-theme-preference.ts + │ │ ├── lib/ # 유틸리티 함수 + │ │ │ ├── cn.ts + │ │ │ ├── get-color.ts + │ │ │ └── get-score-level.ts + │ │ ├── styles/ # 전역 스타일 + │ │ │ ├── base.css + │ │ │ ├── breakpoint.css + │ │ │ ├── colors.css + │ │ │ ├── fonts.css + │ │ │ ├── globals.css + │ │ │ └── typography.css + │ │ ├── types/ # 타입 정의 + │ │ │ ├── svg.d.ts + │ │ │ └── vite-env.d.ts + │ │ └── ui/ # 공유 UI 컴포넌트 + │ │ ├── button/ + │ │ │ ├── Button.tsx + │ │ │ ├── buttonVariants.ts # 상수 분리 (Fast refresh 경고 방지) + │ │ │ └── index.ts + │ │ ├── input-field/ + │ │ ├── intensity-slider/ + │ │ ├── modal/ + │ │ ├── notification-message/ + │ │ ├── page-move-button/ + │ │ ├── panel-header/ + │ │ ├── theme-toggle-switch/ + │ │ ├── timer/ + │ │ ├── toggle-switch/ + │ │ └── typography/ + │ │ + │ └── assets/ # 정적 자산 + │ ├── auth/ # 인증 관련 아이콘 + │ ├── common/ # 공통 자산 + │ │ ├── icons/ # 공통 아이콘 + │ │ └── images/ # 공통 이미지 + │ ├── main/ # 메인 페이지 이미지 + │ ├── modal/ # 모달 캐릭터 이미지 + │ ├── onboarding/ # 온보딩 이미지 + │ ├── video/ # 비디오 파일 + │ └── widget/ # 위젯 아이콘 + │ + ├── index.html + └── tsconfig.json +``` + +--- + +## 레이어별 설명 + +### Main Process (`src/main/`) + +**역할**: 애플리케이션의 생명주기 관리, 창 생성 및 관리, IPC 핸들러 설정 + +**주요 파일**: + +- `index.ts`: 메인 프로세스 진입점, IPC 핸들러 설정 +- `mainWindow.ts`: 메인 윈도우 관리 +- `widgetWindow.ts`: 위젯 윈도우 관리 +- `notificationHandlers.ts`: 알림 핸들러 +- `security-restrictions.ts`: 보안 제한 설정 + +**예시**: + +```typescript +// src/main/src/index.ts +import { app, ipcMain } from 'electron'; + +ipcMain.handle('widget:open', async () => { + // 위젯 창 열기 로직 +}); +``` + +**참고**: + +- Main Process는 Node.js 환경에서 실행됩니다. +- 모든 Electron API에 접근 가능합니다. +- 이 구조는 **Electron의 표준 패턴**입니다. + +**관련 문서**: + +- [Electron Main Process](https://www.electronjs.org/docs/latest/tutorial/process-model#the-main-process) +- [BrowserWindow API](https://www.electronjs.org/docs/latest/api/browser-window) + +--- + +### Preload Scripts (`src/preload/`) + +**역할**: Context Bridge를 통한 안전한 API 노출, Renderer에서 Main Process로의 IPC 통신 브리지 + +**주요 파일**: + +- `src/index.ts`: Preload 스크립트 진입점 +- `exposedInMainWorld.d.ts`: 타입 정의 (자동 생성) + +**예시**: + +```typescript +// src/preload/src/index.ts +import { contextBridge, ipcRenderer } from 'electron'; + +const electronAPI = { + widget: { + open: () => ipcRenderer.invoke('widget:open'), + close: () => ipcRenderer.invoke('widget:close'), + isOpen: () => ipcRenderer.invoke('widget:isOpen'), + }, + notification: { + show: (title: string, body: string) => + ipcRenderer.invoke('notification:show', title, body), + }, +}; + +contextBridge.exposeInMainWorld('electronAPI', electronAPI); +``` + +**Renderer에서 사용**: + +```typescript +// src/renderer/src/*/*.tsx +window.electronAPI.widget.open(); +``` + +**참고**: + +- Preload 스크립트는 `contextIsolation: true` 환경에서 실행됩니다. +- `exposedInMainWorld.d.ts`는 `dts-for-context-bridge`로 자동 생성됩니다. +- 이 구조는 **Electron의 보안 모범 사례**를 따릅니다. + +**관련 문서**: + +- [Context Isolation 가이드](https://www.electronjs.org/docs/latest/tutorial/context-isolation) +- [IPC 통신 패턴](https://www.electronjs.org/docs/latest/tutorial/ipc) +- [보안 체크리스트](https://www.electronjs.org/docs/latest/tutorial/security) + +--- + +### Renderer Process - `app` 레이어 + +**역할**: 애플리케이션의 초기화, 설정, 전역 프로바이더 + +**구조**: + +- `main.tsx`: React 앱 진입점 (ReactDOM.createRoot) +- `providers/App.tsx`: QueryClientProvider, RouterProvider +- `layouts/`: 레이아웃 컴포넌트 (Header, Layout) +- `dev/`: 개발용 컴포넌트 (DevNavbar) + +**특징**: + +- 다른 레이어에 의존하지 않음 +- 애플리케이션의 최상위 레벨 설정만 포함 + +--- + +### Renderer Process - `pages` 레이어 + +**역할**: 라우트와 직접 연결되는 페이지 컴포넌트 + +**구조**: + +- `main-page/`: 메인 대시보드 페이지 +- `login-page/`: 로그인 페이지 +- `signup-page/`: 회원가입 페이지 +- `calibration-page/`: 캘리브레이션 페이지 +- `widget-page/`: 위젯 페이지 +- `onboarding-page/`: 온보딩 페이지 +- 기타 인증 관련 페이지들 + +**특징**: + +- 여러 `features`를 조합하여 페이지 구성 +- 라우터 설정과 직접 연결됨 + +--- + +### Renderer Process - `widgets` 레이어 + +**역할**: 독립적으로 동작하는 재사용 가능한 UI 블록 + +**구조**: + +- `camera/`: 카메라 상태 관리 위젯 +- `widget/`: 위젯 창 UI 및 로직 + +**특징**: + +- 독립적으로 동작 가능 +- 여러 페이지에서 재사용 가능 +- 자체 상태 관리 포함 가능 + +--- + +### Renderer Process - `features` 레이어 + +**역할**: 완전한 비즈니스 기능을 구현 + +**구조**: + +- `auth/`: 인증 기능 (로그인, 회원가입, 이메일 인증) +- `calibration/`: 캘리브레이션 기능 +- `dashboard/`: 대시보드 기능 (통계, 그래프, 패널 등) +- `notification/`: 알림 기능 +- `onboarding/`: 온보딩 기능 + +**Feature 구조 다이어그램**: + +```mermaid +graph TD + UI[ui/ UI 컴포넌트] + Lib[lib/ 비즈니스 로직 선택] + Model[model/ 상태 관리 선택] + Index[index.ts Barrel export] + Entities[entities/] + Shared[shared/] + + UI --> Entities + UI --> Shared + Lib --> Entities + Lib --> Shared + Model --> Entities + Model --> Shared +``` + +**각 feature의 구조**: + +``` +feature-name/ +├── ui/ # UI 컴포넌트 +├── lib/ # 비즈니스 로직 (선택) +├── model/ # 상태 관리 (선택) +└── index.ts # Barrel export +``` + +**특징**: + +- 하나의 완전한 비즈니스 기능을 포함 +- `entities`와 `shared`를 사용하여 구현 +- 다른 `features`에 직접 의존하지 않음 + +--- + +### Renderer Process - `entities` 레이어 + +**역할**: 비즈니스 엔티티를 정의하고 관리 + +**구조**: + +- `dashboard/`: 대시보드 데이터 및 API +- `posture/`: 자세 분석 엔티티 (분류, 계산, 시각화) +- `session/`: 세션 관리 엔티티 +- `user/`: 사용자 엔티티 (인증, 회원가입) + +**Entity 구조 다이어그램**: + +```mermaid +graph LR + API[api/ TanStack Query] + Model[model/ Zustand] + Types[types/ 타입 정의] + Index[index.ts Barrel export] + Shared[shared/] + + API --> Shared + Model --> Shared + Types --> Shared +``` + +**각 entity의 구조**: + +``` +entity-name/ +├── api/ # API 훅 (TanStack Query) +├── model/ # 상태 관리 (Zustand) +├── types/ # 타입 정의 +└── index.ts # Barrel export +``` + +**특징**: + +- 비즈니스 로직의 핵심 +- 여러 `features`에서 재사용됨 +- `shared`만 의존 가능 + +--- + +### Renderer Process - `shared` 레이어 + +**역할**: 프로젝트 전반에서 공유되는 코드 + +**구조**: + +- `api/`: Axios 인스턴스 및 인터셉터 +- `config/`: 설정 파일 (라우터 등) +- `hooks/`: 공유 훅 +- `lib/`: 유틸리티 함수 +- `styles/`: 전역 스타일 +- `types/`: 공유 타입 정의 +- `ui/`: 공유 UI 컴포넌트 + +**특징**: + +- 프로젝트 전반에서 사용됨 +- 다른 레이어에 의존하지 않음 +- 프레임워크나 라이브러리 래퍼 포함 가능 + +**참고**: Fast Refresh 경고를 방지하기 위해 상수는 별도 파일로 분리합니다. + +- 예: `shared/ui/button/buttonVariants.ts` + +--- + +## 의존성 규칙 + +### 허용되는 의존성 + +``` +app → pages → widgets → features → entities → shared +``` + +**의존성 규칙 다이어그램**: + +```mermaid +graph LR + App1[app] --> Pages1[pages] + Pages1 --> Widgets1[widgets] + Pages1 --> Features1[features] + Widgets1 --> Features1 + Features1 --> Entities1[entities] + Features1 --> Shared1[shared] + Entities1 --> Shared1 +``` + +- **하위 레이어는 상위 레이어에 의존 가능** +- **상위 레이어는 하위 레이어에 의존 불가** + +### 예시 + +✅ **허용**: + +- `features/dashboard` → `entities/posture` ✅ +- `features/dashboard` → `shared/ui/button` ✅ +- `pages/main-page` → `features/dashboard` ✅ + +❌ **금지**: + +- `entities/posture` → `features/dashboard` ❌ +- `shared/lib` → `entities/user` ❌ +- `features/auth` → `features/dashboard` ❌ + +--- + +## Path Alias 설정 + +### TypeScript (`tsconfig.json`) + +```json +{ + "compilerOptions": { + "baseUrl": "./src", + "paths": { + "@shared/*": ["./shared/*"], + "@entities/*": ["./entities/*"], + "@features/*": ["./features/*"], + "@widgets/*": ["./widgets/*"], + "@assets/*": ["./assets/*"] + } + } +} +``` + +### Vite (`vite.config.mts`) + +```typescript +resolve: { + alias: { + '@shared/': path.resolve(__dirname, 'src/renderer/src/shared') + '/', + '@entities/': path.resolve(__dirname, 'src/renderer/src/entities') + '/', + '@features/': path.resolve(__dirname, 'src/renderer/src/features') + '/', + '@widgets/': path.resolve(__dirname, 'src/renderer/src/widgets') + '/', + '@assets/': path.resolve(__dirname, 'src/renderer/src/assets') + '/', + } +} +``` + +### 사용 예시 + +```typescript +// ✅ 좋은 예시 +import { Button } from '@shared/ui/button'; +import { usePostureStore } from '@entities/posture'; +import { LoginPage } from '@features/auth'; +import { useWidget } from '@widgets/widget'; +import Logo from '@assets/common/icons/logo.svg?react'; + +// ❌ 나쁜 예시 (상대 경로) +import { Button } from '../../../shared/ui/button'; +``` + +--- + +## 프로세스 간 통신 (IPC) + +### Main ↔ Renderer 통신 패턴 + +이 프로젝트는 Electron의 표준 IPC 패턴을 사용합니다: + +1. **Renderer → Main**: `ipcRenderer.invoke()` 사용 +2. **Main → Renderer**: `webContents.send()` 사용 (필요시) +3. **Preload**: Context Bridge를 통한 안전한 API 노출 + +**IPC 통신 흐름도**: + +```mermaid +sequenceDiagram + participant R as Renderer Process + participant P as Preload Script + participant M as Main Process + + R->>P: window.electronAPI.widget.open() + P->>M: ipcRenderer.invoke('widget:open') + Note over M: ipcMain.handle 처리 + M-->>P: Promise result + P-->>R: Promise result +``` + +**예시 흐름**: + +``` +Renderer Process Preload Script Main Process + │ │ │ + │ window.electronAPI │ │ + │ .widget.open() │ │ + ├────────────────────────>│ │ + │ │ ipcRenderer.invoke │ + │ │ ('widget:open') │ + │ ├───────────────────────>│ + │ │ │ ipcMain.handle + │ │ │ ('widget:open') + │ │ │ 처리 + │ │<───────────────────────┤ + │ │ Promise │ + │<────────────────────────┤ │ + │ Promise │ │ +``` + +**보안 고려사항**: + +- ✅ Context Isolation 활성화 +- ✅ Node.js 통합 비활성화 (Renderer) +- ✅ Preload를 통한 제한된 API만 노출 +- ✅ 모든 IPC 핸들러에서 입력 검증 + +**관련 문서**: + +- [IPC 통신 가이드](https://www.electronjs.org/docs/latest/tutorial/ipc) +- [보안 가이드](https://www.electronjs.org/docs/latest/tutorial/security) + +--- + +## 세그먼트 (Segment) + +각 레이어의 슬라이스는 다음 세그먼트로 구성될 수 있습니다: + +- **`ui/`**: UI 컴포넌트 +- **`lib/`**: 비즈니스 로직 및 유틸리티 +- **`model/`**: 상태 관리 (Zustand store 등) +- **`api/`**: API 호출 (TanStack Query hooks) +- **`types/`**: 타입 정의 +- **`config/`**: 설정 파일 + +### Barrel Export (`index.ts`) + +각 슬라이스와 레이어는 `index.ts`를 통해 public API를 제공합니다: + +```typescript +// entities/posture/index.ts +export * from './lib'; +export * from './model'; +export type { PoseLandmark, WorldLandmark } from './lib/types'; +``` + +이를 통해 깔끔한 import가 가능합니다: + +```typescript +import { usePostureStore, PostureClassifier } from '@entities/posture'; +``` + +--- + +## Import Best Practices + +### 1. Path Alias 사용 + +```typescript +// ✅ 좋은 예시 +import { Button } from '@shared/ui/button'; +import { usePostureStore } from '@entities/posture'; +import { LoginPage } from '@features/auth'; + +// ❌ 나쁜 예시 +import { Button } from '../../../shared/ui/button'; +``` + +### 2. Barrel Export 활용 + +```typescript +// ✅ 좋은 예시: Barrel export 사용 +import { + usePostureStore, + PostureClassifier, + type PoseLandmark, +} from '@entities/posture'; +``` + +### 3. 타입 Import 분리 + +```typescript +// ✅ 좋은 예시: 타입과 값 분리 +import { usePostureStore } from '@entities/posture'; +import type { PoseLandmark, WorldLandmark } from '@entities/posture'; +``` + +--- + +## 참고 자료 + +### FSD 관련 + +- [Feature-Sliced Design 공식 문서](https://feature-sliced.design/) +- [FSD Best Practices](https://feature-sliced.design/docs/get-started/overview) diff --git a/src/features/dashboard/ui/index.ts b/src/features/dashboard/ui/index.ts index f5c793e..7ee8bb5 100644 --- a/src/features/dashboard/ui/index.ts +++ b/src/features/dashboard/ui/index.ts @@ -13,4 +13,3 @@ export { default as TotalDistanceModal } from './TotalDistanceModal'; export { default as TotalDistancePanel } from './TotalDistancePanel'; export { default as TrendPanel } from './TrendPanel'; export { default as WebcamPanel } from './WebcamPanel'; - diff --git a/src/main/src/security-restrictions.ts b/src/main/src/security-restrictions.ts index eebfe6d..d9ca2f8 100644 --- a/src/main/src/security-restrictions.ts +++ b/src/main/src/security-restrictions.ts @@ -184,7 +184,6 @@ app.on('web-contents-created', (_, contents) => { * 개발 모드에서는 허용하고 프로덕션에서만 막기 */ if (import.meta.env.PROD) { - const originalReload = contents.reload.bind(contents); contents.reload = () => { console.warn('Page reload is disabled in production mode'); // 새로고침 실행하지 않음 diff --git a/src/preload/exposedInMainWorld.d.ts b/src/preload/exposedInMainWorld.d.ts index a23ce86..3d4697d 100644 --- a/src/preload/exposedInMainWorld.d.ts +++ b/src/preload/exposedInMainWorld.d.ts @@ -1,5 +1,5 @@ interface Window { - readonly bugi: BugiAPI; - readonly nodeCrypto: NodeCryptoAPI; - readonly electronAPI: ElectronAPI; + readonly bugi: BugiAPI; + readonly nodeCrypto: NodeCryptoAPI; + readonly electronAPI: ElectronAPI; } diff --git a/src/renderer/index.html b/src/renderer/index.html index e94ab21..1e23d93 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -9,6 +9,6 @@
- + diff --git a/src/renderer/src/components/DevNavbar/DevNavbar.tsx b/src/renderer/src/app/dev/DevNavbar/DevNavbar.tsx similarity index 100% rename from src/renderer/src/components/DevNavbar/DevNavbar.tsx rename to src/renderer/src/app/dev/DevNavbar/DevNavbar.tsx diff --git a/src/renderer/src/app/dev/index.ts b/src/renderer/src/app/dev/index.ts new file mode 100644 index 0000000..6041d8f --- /dev/null +++ b/src/renderer/src/app/dev/index.ts @@ -0,0 +1 @@ +export { default as DevNavbar } from './DevNavbar/DevNavbar'; diff --git a/src/renderer/src/app/index.ts b/src/renderer/src/app/index.ts new file mode 100644 index 0000000..522e67c --- /dev/null +++ b/src/renderer/src/app/index.ts @@ -0,0 +1,2 @@ +export * from './providers'; +export * from './layouts'; diff --git a/src/renderer/src/layout/Layout.tsx b/src/renderer/src/app/layouts/Layout.tsx similarity index 75% rename from src/renderer/src/layout/Layout.tsx rename to src/renderer/src/app/layouts/Layout.tsx index e8c990c..46fce8b 100644 --- a/src/renderer/src/layout/Layout.tsx +++ b/src/renderer/src/app/layouts/Layout.tsx @@ -1,5 +1,6 @@ import { Outlet } from 'react-router-dom'; -import Header from './Header/Header'; +import Header from './header/Header'; +// import { DevNavbar } from '../dev'; const Layout = () => { return ( diff --git a/src/renderer/src/layout/Header/Header.tsx b/src/renderer/src/app/layouts/header/Header.tsx similarity index 86% rename from src/renderer/src/layout/Header/Header.tsx rename to src/renderer/src/app/layouts/header/Header.tsx index 139109b..d3b64a6 100644 --- a/src/renderer/src/layout/Header/Header.tsx +++ b/src/renderer/src/app/layouts/header/Header.tsx @@ -1,5 +1,5 @@ -import Logo from '@assets/logo.svg?react'; -import Symbol from '@assets/symbol.svg?react'; +import Logo from '@assets/common/icons/logo.svg?react'; +import Symbol from '@assets/common/icons/symbol.svg?react'; import { ThemeToggleSwitch } from '@shared/ui/theme-toggle-switch'; import { useThemePreference } from '@shared/hooks/use-theme-preference'; diff --git a/src/renderer/src/app/layouts/index.ts b/src/renderer/src/app/layouts/index.ts new file mode 100644 index 0000000..07d44f6 --- /dev/null +++ b/src/renderer/src/app/layouts/index.ts @@ -0,0 +1,2 @@ +export { default as Layout } from './Layout'; +export { default as Header } from './header/Header'; diff --git a/src/renderer/src/main.tsx b/src/renderer/src/app/main.tsx similarity index 89% rename from src/renderer/src/main.tsx rename to src/renderer/src/app/main.tsx index d0c514d..fd82390 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/app/main.tsx @@ -1,8 +1,8 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import App from './App'; -import './index.css'; +import App from './providers/App'; +import '../index.css'; // React Query 클라이언트 생성 const queryClient = new QueryClient({ diff --git a/src/renderer/src/App.tsx b/src/renderer/src/app/providers/App.tsx similarity index 100% rename from src/renderer/src/App.tsx rename to src/renderer/src/app/providers/App.tsx diff --git a/src/renderer/src/app/providers/index.ts b/src/renderer/src/app/providers/index.ts new file mode 100644 index 0000000..c866729 --- /dev/null +++ b/src/renderer/src/app/providers/index.ts @@ -0,0 +1 @@ +export { default as App } from './App'; diff --git a/src/renderer/src/assets/arrow-narrow-down.svg b/src/renderer/src/assets/common/icons/arrow-narrow-down.svg similarity index 100% rename from src/renderer/src/assets/arrow-narrow-down.svg rename to src/renderer/src/assets/common/icons/arrow-narrow-down.svg diff --git a/src/renderer/src/assets/arrow-narrow-up.svg b/src/renderer/src/assets/common/icons/arrow-narrow-up.svg similarity index 100% rename from src/renderer/src/assets/arrow-narrow-up.svg rename to src/renderer/src/assets/common/icons/arrow-narrow-up.svg diff --git a/src/renderer/src/assets/calendar.svg b/src/renderer/src/assets/common/icons/calendar.svg similarity index 100% rename from src/renderer/src/assets/calendar.svg rename to src/renderer/src/assets/common/icons/calendar.svg diff --git a/src/renderer/src/assets/camera.svg b/src/renderer/src/assets/common/icons/camera.svg similarity index 100% rename from src/renderer/src/assets/camera.svg rename to src/renderer/src/assets/common/icons/camera.svg diff --git a/src/renderer/src/assets/chevron-right.svg b/src/renderer/src/assets/common/icons/chevron-right.svg similarity index 100% rename from src/renderer/src/assets/chevron-right.svg rename to src/renderer/src/assets/common/icons/chevron-right.svg diff --git a/src/renderer/src/assets/clock.svg b/src/renderer/src/assets/common/icons/clock.svg similarity index 100% rename from src/renderer/src/assets/clock.svg rename to src/renderer/src/assets/common/icons/clock.svg diff --git a/src/renderer/src/assets/completion.svg b/src/renderer/src/assets/common/icons/completion.svg similarity index 100% rename from src/renderer/src/assets/completion.svg rename to src/renderer/src/assets/common/icons/completion.svg diff --git a/src/renderer/src/assets/dashboard.svg b/src/renderer/src/assets/common/icons/dashboard.svg similarity index 100% rename from src/renderer/src/assets/dashboard.svg rename to src/renderer/src/assets/common/icons/dashboard.svg diff --git a/src/renderer/src/assets/hide.svg b/src/renderer/src/assets/common/icons/hide.svg similarity index 100% rename from src/renderer/src/assets/hide.svg rename to src/renderer/src/assets/common/icons/hide.svg diff --git a/src/renderer/src/assets/hourglass.svg b/src/renderer/src/assets/common/icons/hourglass.svg similarity index 100% rename from src/renderer/src/assets/hourglass.svg rename to src/renderer/src/assets/common/icons/hourglass.svg diff --git a/src/renderer/src/assets/info-circle.svg b/src/renderer/src/assets/common/icons/info-circle.svg similarity index 100% rename from src/renderer/src/assets/info-circle.svg rename to src/renderer/src/assets/common/icons/info-circle.svg diff --git a/src/renderer/src/assets/logo.svg b/src/renderer/src/assets/common/icons/logo.svg similarity index 100% rename from src/renderer/src/assets/logo.svg rename to src/renderer/src/assets/common/icons/logo.svg diff --git a/src/renderer/src/assets/moon_icon.svg b/src/renderer/src/assets/common/icons/moon_icon.svg similarity index 100% rename from src/renderer/src/assets/moon_icon.svg rename to src/renderer/src/assets/common/icons/moon_icon.svg diff --git a/src/renderer/src/assets/page-move-button.svg b/src/renderer/src/assets/common/icons/page-move-button.svg similarity index 100% rename from src/renderer/src/assets/page-move-button.svg rename to src/renderer/src/assets/common/icons/page-move-button.svg diff --git a/src/renderer/src/assets/plan.svg b/src/renderer/src/assets/common/icons/plan.svg similarity index 100% rename from src/renderer/src/assets/plan.svg rename to src/renderer/src/assets/common/icons/plan.svg diff --git a/src/renderer/src/assets/profile.svg b/src/renderer/src/assets/common/icons/profile.svg similarity index 100% rename from src/renderer/src/assets/profile.svg rename to src/renderer/src/assets/common/icons/profile.svg diff --git a/src/renderer/src/assets/setting.svg b/src/renderer/src/assets/common/icons/setting.svg similarity index 100% rename from src/renderer/src/assets/setting.svg rename to src/renderer/src/assets/common/icons/setting.svg diff --git a/src/renderer/src/assets/show.svg b/src/renderer/src/assets/common/icons/show.svg similarity index 100% rename from src/renderer/src/assets/show.svg rename to src/renderer/src/assets/common/icons/show.svg diff --git a/src/renderer/src/assets/sleep.svg b/src/renderer/src/assets/common/icons/sleep.svg similarity index 100% rename from src/renderer/src/assets/sleep.svg rename to src/renderer/src/assets/common/icons/sleep.svg diff --git a/src/renderer/src/assets/sun_icon.svg b/src/renderer/src/assets/common/icons/sun_icon.svg similarity index 100% rename from src/renderer/src/assets/sun_icon.svg rename to src/renderer/src/assets/common/icons/sun_icon.svg diff --git a/src/renderer/src/assets/symbol.svg b/src/renderer/src/assets/common/icons/symbol.svg similarity index 100% rename from src/renderer/src/assets/symbol.svg rename to src/renderer/src/assets/common/icons/symbol.svg diff --git a/src/renderer/src/assets/thumbup.svg b/src/renderer/src/assets/common/icons/thumbup.svg similarity index 100% rename from src/renderer/src/assets/thumbup.svg rename to src/renderer/src/assets/common/icons/thumbup.svg diff --git a/src/renderer/src/assets/widget.svg b/src/renderer/src/assets/common/icons/widget.svg similarity index 100% rename from src/renderer/src/assets/widget.svg rename to src/renderer/src/assets/common/icons/widget.svg diff --git a/src/renderer/src/assets/calibration_guide.svg b/src/renderer/src/assets/common/images/calibration_guide.svg similarity index 100% rename from src/renderer/src/assets/calibration_guide.svg rename to src/renderer/src/assets/common/images/calibration_guide.svg diff --git a/src/renderer/src/assets/angel-rini-modal.svg b/src/renderer/src/assets/modal/angel-rini-modal.svg similarity index 100% rename from src/renderer/src/assets/angel-rini-modal.svg rename to src/renderer/src/assets/modal/angel-rini-modal.svg diff --git a/src/renderer/src/assets/bugi-modal.svg b/src/renderer/src/assets/modal/bugi-modal.svg similarity index 100% rename from src/renderer/src/assets/bugi-modal.svg rename to src/renderer/src/assets/modal/bugi-modal.svg diff --git a/src/renderer/src/assets/pm-rini-modal.svg b/src/renderer/src/assets/modal/pm-rini-modal.svg similarity index 100% rename from src/renderer/src/assets/pm-rini-modal.svg rename to src/renderer/src/assets/modal/pm-rini-modal.svg diff --git a/src/renderer/src/assets/rini-modal.svg b/src/renderer/src/assets/modal/rini-modal.svg similarity index 100% rename from src/renderer/src/assets/rini-modal.svg rename to src/renderer/src/assets/modal/rini-modal.svg diff --git a/src/renderer/src/assets/stone-bugi-modal.svg b/src/renderer/src/assets/modal/stone-bugi-modal.svg similarity index 100% rename from src/renderer/src/assets/stone-bugi-modal.svg rename to src/renderer/src/assets/modal/stone-bugi-modal.svg diff --git a/src/renderer/src/assets/tire-bugi-modal.svg b/src/renderer/src/assets/modal/tire-bugi-modal.svg similarity index 100% rename from src/renderer/src/assets/tire-bugi-modal.svg rename to src/renderer/src/assets/modal/tire-bugi-modal.svg diff --git a/src/renderer/src/components/NotificateMessage/index.tsx b/src/renderer/src/components/NotificateMessage/index.tsx deleted file mode 100644 index 122341b..0000000 --- a/src/renderer/src/components/NotificateMessage/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export { ErrorIcon, SuccessIcon } from './icons'; -export { default, NotificateMessage } from './NotificateMessage'; diff --git a/src/renderer/src/components/WidgetController/WidgetController.tsx b/src/renderer/src/components/WidgetController/WidgetController.tsx deleted file mode 100644 index e06bf79..0000000 --- a/src/renderer/src/components/WidgetController/WidgetController.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useState, useEffect } from 'react'; - -/* 위젯 창을 제어하는 컴포넌트 메인 창에서 위젯 창을 열고 닫을 수 있습니다 */ -export function WidgetController() { - const [isWidgetOpen, setIsWidgetOpen] = useState(false); - - /* 위젯 창이 열려있는지 확인 */ - useEffect(() => { - const checkWidgetStatus = async () => { - if (window.electronAPI?.widget) { - const isOpen = await window.electronAPI.widget.isOpen(); - setIsWidgetOpen(isOpen); - } - }; - - checkWidgetStatus(); - }, []); - - /*위젯 창 열기*/ - const handleOpenWidget = async () => { - try { - if (window.electronAPI?.widget) { - await window.electronAPI.widget.open(); - setIsWidgetOpen(true); - console.log('위젯 창이 열렸습니다'); - } - } catch (error) { - console.error('위젯 창 열기 실패:', error); - } - }; - - /* 위젯 창 닫기 */ - const handleCloseWidget = async () => { - try { - if (window.electronAPI?.widget) { - await window.electronAPI.widget.close(); - setIsWidgetOpen(false); - console.log('위젯 창이 닫혔습니다'); - } - } catch (error) { - console.error('위젯 창 닫기 실패:', error); - } - }; - - /* Electron 환경이 아닌 경우 */ - if (!window.electronAPI?.widget) { - return ( -
-

Electron 환경에서만 사용 가능합니다.

-
- ); - } - - return ( -
-
- - -
-
- 상태: {isWidgetOpen ? '열림' : '닫힘'} -
-
- ); -} diff --git a/src/renderer/src/components/index.ts b/src/renderer/src/components/index.ts deleted file mode 100644 index 3156150..0000000 --- a/src/renderer/src/components/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// UI 컴포넌트 export -export { Button } from '@shared/ui/button'; -export { TextField } from '@shared/ui/input-field'; -export * from '@shared/ui/notification-message'; -export { Timer } from '@shared/ui/timer'; -export { Typography } from '@shared/ui/typography'; diff --git a/src/renderer/src/entities/dashboard/api/index.ts b/src/renderer/src/entities/dashboard/api/index.ts index f8599ad..a327233 100644 --- a/src/renderer/src/entities/dashboard/api/index.ts +++ b/src/renderer/src/entities/dashboard/api/index.ts @@ -4,4 +4,3 @@ export { useHighlightQuery } from './use-highlight-query'; export { useLevelQuery } from './use-level-query'; export { usePostureGraphQuery } from './use-posture-graph-query'; export { usePosturePatternQuery } from './use-posture-pattern-query'; - diff --git a/src/renderer/src/entities/dashboard/api/use-attendance-query.ts b/src/renderer/src/entities/dashboard/api/use-attendance-query.ts index 1ae2a20..4aa2723 100644 --- a/src/renderer/src/entities/dashboard/api/use-attendance-query.ts +++ b/src/renderer/src/entities/dashboard/api/use-attendance-query.ts @@ -1,9 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import api from '@shared/api'; -import { - AttendanceQueryParams, - AttendanceResponse, -} from '../types'; +import { AttendanceQueryParams, AttendanceResponse } from '../types'; /** * 출석 현황 조회 API @@ -58,4 +55,3 @@ export const useAttendanceQuery = ( retry: 1, // 실패 시 1번만 재시도 }); }; - diff --git a/src/renderer/src/entities/dashboard/api/use-average-score-query.ts b/src/renderer/src/entities/dashboard/api/use-average-score-query.ts index 69d7853..c3b8c28 100644 --- a/src/renderer/src/entities/dashboard/api/use-average-score-query.ts +++ b/src/renderer/src/entities/dashboard/api/use-average-score-query.ts @@ -31,4 +31,3 @@ export const useAverageScoreQuery = () => { queryFn: getAverageScore, }); }; - diff --git a/src/renderer/src/entities/dashboard/api/use-highlight-query.ts b/src/renderer/src/entities/dashboard/api/use-highlight-query.ts index c5dd01d..4e76215 100644 --- a/src/renderer/src/entities/dashboard/api/use-highlight-query.ts +++ b/src/renderer/src/entities/dashboard/api/use-highlight-query.ts @@ -1,9 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import api from '@shared/api'; -import { - HighlightQueryParams, - HighlightResponse, -} from '../types'; +import { HighlightQueryParams, HighlightResponse } from '../types'; /** * 하이라이트 조회 API @@ -58,4 +55,3 @@ export const useHighlightQuery = ( retry: 1, // 실패 시 1번만 재시도 }); }; - diff --git a/src/renderer/src/entities/dashboard/api/use-level-query.ts b/src/renderer/src/entities/dashboard/api/use-level-query.ts index 6003e1c..1abf62e 100644 --- a/src/renderer/src/entities/dashboard/api/use-level-query.ts +++ b/src/renderer/src/entities/dashboard/api/use-level-query.ts @@ -31,4 +31,3 @@ export const useLevelQuery = () => { queryFn: getLevel, }); }; - diff --git a/src/renderer/src/entities/dashboard/api/use-posture-graph-query.ts b/src/renderer/src/entities/dashboard/api/use-posture-graph-query.ts index afd2098..e9e7bbb 100644 --- a/src/renderer/src/entities/dashboard/api/use-posture-graph-query.ts +++ b/src/renderer/src/entities/dashboard/api/use-posture-graph-query.ts @@ -32,4 +32,3 @@ export const usePostureGraphQuery = () => { queryFn: getPostureGraph, }); }; - diff --git a/src/renderer/src/entities/dashboard/api/use-posture-pattern-query.ts b/src/renderer/src/entities/dashboard/api/use-posture-pattern-query.ts index f837ee6..42495d0 100644 --- a/src/renderer/src/entities/dashboard/api/use-posture-pattern-query.ts +++ b/src/renderer/src/entities/dashboard/api/use-posture-pattern-query.ts @@ -34,4 +34,3 @@ export const usePosturePatternQuery = () => { retry: 1, // 실패 시 1번만 재시도 }); }; - diff --git a/src/renderer/src/entities/dashboard/index.ts b/src/renderer/src/entities/dashboard/index.ts index 8c5b070..408fce6 100644 --- a/src/renderer/src/entities/dashboard/index.ts +++ b/src/renderer/src/entities/dashboard/index.ts @@ -20,4 +20,3 @@ export type { PosturePatternData, PosturePatternResponse, } from './types'; - diff --git a/src/renderer/src/entities/posture/index.ts b/src/renderer/src/entities/posture/index.ts index 60c0f51..85e5181 100644 --- a/src/renderer/src/entities/posture/index.ts +++ b/src/renderer/src/entities/posture/index.ts @@ -11,4 +11,3 @@ export type { CalibrationState, CalibrationFrame, } from './lib/types'; - diff --git a/src/renderer/src/entities/posture/lib/PoseDetection.tsx b/src/renderer/src/entities/posture/lib/PoseDetection.tsx index dc5b5e3..450ecc3 100644 --- a/src/renderer/src/entities/posture/lib/PoseDetection.tsx +++ b/src/renderer/src/entities/posture/lib/PoseDetection.tsx @@ -1,181 +1,180 @@ import { FilesetResolver, PoseLandmarker } from '@mediapipe/tasks-vision'; import { - useCallback, - useEffect, - useRef, - useState, - type RefObject, + useCallback, + useEffect, + useRef, + useState, + type RefObject, } from 'react'; import Webcam from 'react-webcam'; import { PoseLandmark } from './types'; interface WebcamRef { - video?: HTMLVideoElement | null; + video?: HTMLVideoElement | null; } interface PoseDetectionProps { - videoRef: RefObject; // Webcam 컴포넌트 ref - onPoseDetected?: ( - landmarks: PoseLandmark[], - worldLandmarks?: PoseLandmark[], - ) => void; - isEnabled?: boolean; + videoRef: RefObject; // Webcam 컴포넌트 ref + onPoseDetected?: ( + landmarks: PoseLandmark[], + worldLandmarks?: PoseLandmark[], + ) => void; + isEnabled?: boolean; } const PoseDetection = ({ - videoRef, - onPoseDetected, - isEnabled = true, + videoRef, + onPoseDetected, + isEnabled = true, }: PoseDetectionProps) => { - const [isInitialized, setIsInitialized] = useState(false); - const [isDetecting, setIsDetecting] = useState(false); - const poseLandmarkerRef = useRef(null); - const lastVideoTimeRef = useRef(-1); - - // MediaPipe 초기화 - useEffect(() => { - const initializePoseLandmarker = async () => { - try { - const vision = await FilesetResolver.forVisionTasks( - 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm', - ); - - poseLandmarkerRef.current = await PoseLandmarker.createFromOptions( - vision, - { - baseOptions: { - modelAssetPath: - 'https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_heavy/float16/1/pose_landmarker_heavy.task', - delegate: 'GPU', - }, - runningMode: 'VIDEO', - numPoses: 1, - minPoseDetectionConfidence: 0.2, // 더 낮은 감지 임계값 - minPosePresenceConfidence: 0.2, // 더 낮은 존재 임계값 - minTrackingConfidence: 0.2, // 더 낮은 추적 임계값 - }, - ); - - setIsInitialized(true); - } catch (error) { - console.error('Failed to initialize pose landmarker:', error); - } - }; - - if (isEnabled) { - initializePoseLandmarker(); - } - }, [isEnabled]); - - // 포즈 감지 실행 - const detectPose = useCallback( - async (videoElement: HTMLVideoElement) => { - if (!poseLandmarkerRef.current || !isEnabled || isDetecting) return; - - const currentTime = videoElement.currentTime; - if (currentTime === lastVideoTimeRef.current) return; - - lastVideoTimeRef.current = currentTime; - setIsDetecting(true); - - try { - const results = poseLandmarkerRef.current.detectForVideo( - videoElement, - performance.now(), - ); - - if (results.landmarks && results.landmarks.length > 0) { - // 13개 주요 포즈 랜드마크 추출 (MediaPipe Pose는 33개 랜드마크를 제공) - const keyLandmarks = extractKeyLandmarks(results.landmarks[0]); - - // World landmarks도 추출 (PI 계산에 필요) - let worldLandmarks: PoseLandmark[] = []; - if (results.worldLandmarks && results.worldLandmarks.length > 0) { - worldLandmarks = extractKeyLandmarks(results.worldLandmarks[0]); - } else { - // World landmarks가 없으면 2D 랜드마크를 3D로 변환해서 사용 - worldLandmarks = keyLandmarks.map((landmark) => ({ - ...landmark, - z: landmark.z || 0, // z값이 없으면 0으로 설정 - })); - } - - onPoseDetected?.(keyLandmarks, worldLandmarks); - } - } catch (error) { - console.error('Pose detection error:', error); - } finally { - setIsDetecting(false); - } - }, - [isEnabled, isDetecting, onPoseDetected], - ); - - // 주요 랜드마크 추출 (얼굴 + 상체 중심) - const extractKeyLandmarks = (landmarks: PoseLandmark[]): PoseLandmark[] => { - // MediaPipe Pose 33개 랜드마크에서 주요 포인트들 선택 - const keyIndices = [ - // 얼굴 영역 (11개) - 0, // NOSE - 1, // LEFT_EYE_INNER - 2, // LEFT_EYE - 3, // LEFT_EYE_OUTER - 4, // RIGHT_EYE_INNER - 5, // RIGHT_EYE - 6, // RIGHT_EYE_OUTER - 7, // LEFT_EAR - 8, // RIGHT_EAR - 9, // MOUTH_LEFT - 10, // MOUTH_RIGHT - // 어깨 영역 (2개) - 11, // LEFT_SHOULDER - 12, // RIGHT_SHOULDER - ]; - - return keyIndices.map((index) => { - const landmark = landmarks[index]; - if (landmark) { - // 측면 각도에서도 최소한의 가시성 보장 - return { - ...landmark, - visibility: Math.max(landmark.visibility || 0, 0.1), - }; - } - return { x: 0, y: 0, z: 0, visibility: 0 }; - }); + const [isInitialized, setIsInitialized] = useState(false); + const [isDetecting, setIsDetecting] = useState(false); + const poseLandmarkerRef = useRef(null); + const lastVideoTimeRef = useRef(-1); + + // MediaPipe 초기화 + useEffect(() => { + const initializePoseLandmarker = async () => { + try { + const vision = await FilesetResolver.forVisionTasks( + 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm', + ); + + poseLandmarkerRef.current = await PoseLandmarker.createFromOptions( + vision, + { + baseOptions: { + modelAssetPath: + 'https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_heavy/float16/1/pose_landmarker_heavy.task', + delegate: 'GPU', + }, + runningMode: 'VIDEO', + numPoses: 1, + minPoseDetectionConfidence: 0.2, // 더 낮은 감지 임계값 + minPosePresenceConfidence: 0.2, // 더 낮은 존재 임계값 + minTrackingConfidence: 0.2, // 더 낮은 추적 임계값 + }, + ); + + setIsInitialized(true); + } catch (error) { + console.error('Failed to initialize pose landmarker:', error); + } }; - // 비디오 프레임마다 포즈 감지 실행 - useEffect(() => { - if (!isInitialized || !videoRef.current || !isEnabled) return; - - const getVideoElement = () => { - // Webcam 컴포넌트에서 video 요소 가져오기 - const ref = videoRef.current; - if (!ref) return null; - // WebcamRef 인터페이스인 경우 - if ('video' in ref) { - return ref.video || null; - } - // Webcam 컴포넌트인 경우 - video 속성이 있을 수 있음 - return ( - (ref as unknown as { video?: HTMLVideoElement | null })?.video || null - ); + if (isEnabled) { + initializePoseLandmarker(); + } + }, [isEnabled]); + + // 포즈 감지 실행 + const detectPose = useCallback( + async (videoElement: HTMLVideoElement) => { + if (!poseLandmarkerRef.current || !isEnabled || isDetecting) return; + + const currentTime = videoElement.currentTime; + if (currentTime === lastVideoTimeRef.current) return; + + lastVideoTimeRef.current = currentTime; + setIsDetecting(true); + + try { + const results = poseLandmarkerRef.current.detectForVideo( + videoElement, + performance.now(), + ); + + if (results.landmarks && results.landmarks.length > 0) { + // 13개 주요 포즈 랜드마크 추출 (MediaPipe Pose는 33개 랜드마크를 제공) + const keyLandmarks = extractKeyLandmarks(results.landmarks[0]); + + // World landmarks도 추출 (PI 계산에 필요) + let worldLandmarks: PoseLandmark[] = []; + if (results.worldLandmarks && results.worldLandmarks.length > 0) { + worldLandmarks = extractKeyLandmarks(results.worldLandmarks[0]); + } else { + // World landmarks가 없으면 2D 랜드마크를 3D로 변환해서 사용 + worldLandmarks = keyLandmarks.map((landmark) => ({ + ...landmark, + z: landmark.z || 0, // z값이 없으면 0으로 설정 + })); + } + + onPoseDetected?.(keyLandmarks, worldLandmarks); + } + } catch (error) { + console.error('Pose detection error:', error); + } finally { + setIsDetecting(false); + } + }, + [isEnabled, isDetecting, onPoseDetected], + ); + + // 주요 랜드마크 추출 (얼굴 + 상체 중심) + const extractKeyLandmarks = (landmarks: PoseLandmark[]): PoseLandmark[] => { + // MediaPipe Pose 33개 랜드마크에서 주요 포인트들 선택 + const keyIndices = [ + // 얼굴 영역 (11개) + 0, // NOSE + 1, // LEFT_EYE_INNER + 2, // LEFT_EYE + 3, // LEFT_EYE_OUTER + 4, // RIGHT_EYE_INNER + 5, // RIGHT_EYE + 6, // RIGHT_EYE_OUTER + 7, // LEFT_EAR + 8, // RIGHT_EAR + 9, // MOUTH_LEFT + 10, // MOUTH_RIGHT + // 어깨 영역 (2개) + 11, // LEFT_SHOULDER + 12, // RIGHT_SHOULDER + ]; + + return keyIndices.map((index) => { + const landmark = landmarks[index]; + if (landmark) { + // 측면 각도에서도 최소한의 가시성 보장 + return { + ...landmark, + visibility: Math.max(landmark.visibility || 0, 0.1), }; + } + return { x: 0, y: 0, z: 0, visibility: 0 }; + }); + }; + + // 비디오 프레임마다 포즈 감지 실행 + useEffect(() => { + if (!isInitialized || !videoRef.current || !isEnabled) return; + + const getVideoElement = () => { + // Webcam 컴포넌트에서 video 요소 가져오기 + const ref = videoRef.current; + if (!ref) return null; + // WebcamRef 인터페이스인 경우 + if ('video' in ref) { + return ref.video || null; + } + // Webcam 컴포넌트인 경우 - video 속성이 있을 수 있음 + return ( + (ref as unknown as { video?: HTMLVideoElement | null })?.video || null + ); + }; - const interval = setInterval(() => { - const videoElement = getVideoElement(); - if (videoElement && videoElement.readyState >= 2) { - // HAVE_CURRENT_DATA - detectPose(videoElement); - } - }, 50); // 20fps로 감지 (더 빠른 반응) + const interval = setInterval(() => { + const videoElement = getVideoElement(); + if (videoElement && videoElement.readyState >= 2) { + // HAVE_CURRENT_DATA + detectPose(videoElement); + } + }, 50); // 20fps로 감지 (더 빠른 반응) - return () => clearInterval(interval); - }, [isInitialized, videoRef, isEnabled, detectPose]); + return () => clearInterval(interval); + }, [isInitialized, videoRef, isEnabled, detectPose]); - return null; // 이 컴포넌트는 UI를 렌더링하지 않음 + return null; // 이 컴포넌트는 UI를 렌더링하지 않음 }; export default PoseDetection; - diff --git a/src/renderer/src/entities/posture/lib/PoseVisualizer.tsx b/src/renderer/src/entities/posture/lib/PoseVisualizer.tsx index 844ad24..b33cb7b 100644 --- a/src/renderer/src/entities/posture/lib/PoseVisualizer.tsx +++ b/src/renderer/src/entities/posture/lib/PoseVisualizer.tsx @@ -268,4 +268,3 @@ const PoseVisualizer = ({ }; export default PoseVisualizer; - diff --git a/src/renderer/src/entities/posture/lib/PostureClassifier.ts b/src/renderer/src/entities/posture/lib/PostureClassifier.ts index 4845619..0f202af 100644 --- a/src/renderer/src/entities/posture/lib/PostureClassifier.ts +++ b/src/renderer/src/entities/posture/lib/PostureClassifier.ts @@ -33,7 +33,7 @@ export class PostureClassifier { piData: PIResult, mu: number, sigma: number, - frontality: FrontalityResult, + _frontality: FrontalityResult, ): PostureClassification { const currentTime = Date.now(); @@ -173,4 +173,3 @@ export class PostureClassifier { this.lastScoreUpdateTime = 0; } } - diff --git a/src/renderer/src/entities/posture/lib/PostureStabilizer.ts b/src/renderer/src/entities/posture/lib/PostureStabilizer.ts index 9a94432..79c0835 100644 --- a/src/renderer/src/entities/posture/lib/PostureStabilizer.ts +++ b/src/renderer/src/entities/posture/lib/PostureStabilizer.ts @@ -146,4 +146,3 @@ export class PostureStabilizer { return weightedSum / totalWeight; } } - diff --git a/src/renderer/src/entities/posture/lib/ScoreProcessor.ts b/src/renderer/src/entities/posture/lib/ScoreProcessor.ts index 1937aba..64967dc 100644 --- a/src/renderer/src/entities/posture/lib/ScoreProcessor.ts +++ b/src/renderer/src/entities/posture/lib/ScoreProcessor.ts @@ -1,5 +1,6 @@ // Helper functions from the python script, translated to JS -function getPercentile(data: number[], percentile: number): number { +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function _getPercentile(data: number[], percentile: number): number { if (data.length === 0) return 0; const sortedData = [...data].sort((a, b) => a - b); const index = (percentile / 100) * (sortedData.length - 1); @@ -98,4 +99,3 @@ export class ScoreProcessor { this.scoreBuffer = []; } } - diff --git a/src/renderer/src/entities/posture/lib/errorChecks.ts b/src/renderer/src/entities/posture/lib/errorChecks.ts index e9a073a..122e3fa 100644 --- a/src/renderer/src/entities/posture/lib/errorChecks.ts +++ b/src/renderer/src/entities/posture/lib/errorChecks.ts @@ -218,4 +218,3 @@ export function getStep2Error(frames: CalibrationFrame[]): string | null { checkPostureStability(frames) ); } - diff --git a/src/renderer/src/entities/posture/lib/index.ts b/src/renderer/src/entities/posture/lib/index.ts index f5fc18c..7fc1dca 100644 --- a/src/renderer/src/entities/posture/lib/index.ts +++ b/src/renderer/src/entities/posture/lib/index.ts @@ -19,4 +19,3 @@ export type { CalibrationState, CalibrationFrame, } from './types'; - diff --git a/src/renderer/src/entities/posture/model/index.ts b/src/renderer/src/entities/posture/model/index.ts index 6d96948..6dea6b7 100644 --- a/src/renderer/src/entities/posture/model/index.ts +++ b/src/renderer/src/entities/posture/model/index.ts @@ -1,2 +1 @@ export { usePostureStore } from './use-posture-store'; - diff --git a/src/renderer/src/entities/posture/model/use-posture-store.ts b/src/renderer/src/entities/posture/model/use-posture-store.ts index 121f8a8..643f7a6 100644 --- a/src/renderer/src/entities/posture/model/use-posture-store.ts +++ b/src/renderer/src/entities/posture/model/use-posture-store.ts @@ -21,4 +21,3 @@ export const usePostureStore = create()( }, ), ); - diff --git a/src/renderer/src/entities/session/api/index.ts b/src/renderer/src/entities/session/api/index.ts index 15b1abe..b24fb24 100644 --- a/src/renderer/src/entities/session/api/index.ts +++ b/src/renderer/src/entities/session/api/index.ts @@ -4,4 +4,3 @@ export { useResumeSessionMutation } from './use-resume-session-mutation'; export { useStopSessionMutation } from './use-stop-session-mutation'; export { useSaveMetricsMutation } from './use-save-metrics-mutation'; export { useSessionReportQuery } from './use-session-report-query'; - diff --git a/src/renderer/src/entities/session/api/use-create-session-mutation.ts b/src/renderer/src/entities/session/api/use-create-session-mutation.ts index 77b6a4a..86375d6 100644 --- a/src/renderer/src/entities/session/api/use-create-session-mutation.ts +++ b/src/renderer/src/entities/session/api/use-create-session-mutation.ts @@ -40,4 +40,3 @@ export const useCreateSessionMutation = () => { }, }); }; - diff --git a/src/renderer/src/entities/session/api/use-pause-session-mutation.ts b/src/renderer/src/entities/session/api/use-pause-session-mutation.ts index 9da1fca..34a44a2 100644 --- a/src/renderer/src/entities/session/api/use-pause-session-mutation.ts +++ b/src/renderer/src/entities/session/api/use-pause-session-mutation.ts @@ -39,4 +39,3 @@ export const usePauseSessionMutation = () => { }, }); }; - diff --git a/src/renderer/src/entities/session/api/use-save-metrics-mutation.ts b/src/renderer/src/entities/session/api/use-save-metrics-mutation.ts index 6c27f1b..abeb6e1 100644 --- a/src/renderer/src/entities/session/api/use-save-metrics-mutation.ts +++ b/src/renderer/src/entities/session/api/use-save-metrics-mutation.ts @@ -1,9 +1,6 @@ import { useMutation } from '@tanstack/react-query'; import api from '@shared/api'; -import { - SaveMetricsRequest, - SaveMetricsResponse, -} from '../types'; +import { SaveMetricsRequest, SaveMetricsResponse } from '../types'; /** * 세션 메트릭 저장 API @@ -50,4 +47,3 @@ export const useSaveMetricsMutation = () => { }, }); }; - diff --git a/src/renderer/src/entities/session/api/use-session-report-query.ts b/src/renderer/src/entities/session/api/use-session-report-query.ts index 14971a0..22b82d2 100644 --- a/src/renderer/src/entities/session/api/use-session-report-query.ts +++ b/src/renderer/src/entities/session/api/use-session-report-query.ts @@ -43,4 +43,3 @@ export const useSessionReportQuery = ( retry: 1, // 실패 시 1번만 재시도 }); }; - diff --git a/src/renderer/src/entities/session/api/use-stop-session-mutation.ts b/src/renderer/src/entities/session/api/use-stop-session-mutation.ts index a7a92a8..211c4db 100644 --- a/src/renderer/src/entities/session/api/use-stop-session-mutation.ts +++ b/src/renderer/src/entities/session/api/use-stop-session-mutation.ts @@ -59,4 +59,3 @@ export const useStopSessionMutation = () => { }, }); }; - diff --git a/src/renderer/src/entities/session/index.ts b/src/renderer/src/entities/session/index.ts index 3bbc8a3..6496874 100644 --- a/src/renderer/src/entities/session/index.ts +++ b/src/renderer/src/entities/session/index.ts @@ -11,4 +11,3 @@ export type { SessionReportData, SessionReportResponse, } from './types'; - diff --git a/src/renderer/src/entities/session/types/index.ts b/src/renderer/src/entities/session/types/index.ts index f10049b..7fb13d5 100644 --- a/src/renderer/src/entities/session/types/index.ts +++ b/src/renderer/src/entities/session/types/index.ts @@ -52,4 +52,3 @@ export interface SessionReportResponse { code: string; message: string; } - diff --git a/src/renderer/src/entities/user/api/index.ts b/src/renderer/src/entities/user/api/index.ts index 2569c48..9a466e0 100644 --- a/src/renderer/src/entities/user/api/index.ts +++ b/src/renderer/src/entities/user/api/index.ts @@ -7,4 +7,3 @@ export { useVerifyEmailMutation, useResendVerifyEmailMuation, } from './use-verify-email-mutation'; - diff --git a/src/renderer/src/entities/user/api/use-login-mutation.ts b/src/renderer/src/entities/user/api/use-login-mutation.ts index 2804ba2..4bc3a45 100644 --- a/src/renderer/src/entities/user/api/use-login-mutation.ts +++ b/src/renderer/src/entities/user/api/use-login-mutation.ts @@ -45,4 +45,3 @@ export const useLoginMutation = () => { }, }); }; - diff --git a/src/renderer/src/entities/user/api/use-verify-email-mutation.ts b/src/renderer/src/entities/user/api/use-verify-email-mutation.ts index ef65101..675afc0 100644 --- a/src/renderer/src/entities/user/api/use-verify-email-mutation.ts +++ b/src/renderer/src/entities/user/api/use-verify-email-mutation.ts @@ -63,4 +63,3 @@ export const useResendVerifyEmailMuation = () => { }, }); }; - diff --git a/src/renderer/src/entities/user/index.ts b/src/renderer/src/entities/user/index.ts index 15a3039..2a25be4 100644 --- a/src/renderer/src/entities/user/index.ts +++ b/src/renderer/src/entities/user/index.ts @@ -9,4 +9,3 @@ export type { SignupRequest, ResendVerifyEmailRequest, } from './types'; - diff --git a/src/renderer/src/entities/user/model/index.ts b/src/renderer/src/entities/user/model/index.ts index 54fa135..bae8082 100644 --- a/src/renderer/src/entities/user/model/index.ts +++ b/src/renderer/src/entities/user/model/index.ts @@ -1,2 +1 @@ export { useEmailStore } from './use-email-store'; - diff --git a/src/renderer/src/entities/user/model/use-email-store.ts b/src/renderer/src/entities/user/model/use-email-store.ts index 7e97df0..44bdb6f 100644 --- a/src/renderer/src/entities/user/model/use-email-store.ts +++ b/src/renderer/src/entities/user/model/use-email-store.ts @@ -18,4 +18,3 @@ export const useEmailStore = create()( }, ), ); - diff --git a/src/renderer/src/features/auth/index.ts b/src/renderer/src/features/auth/index.ts deleted file mode 100644 index d07f8de..0000000 --- a/src/renderer/src/features/auth/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './ui/login'; -export * from './ui/signup'; diff --git a/src/renderer/src/features/auth/ui/login/components/HeroSection.tsx b/src/renderer/src/features/auth/ui/login/components/HeroSection.tsx index 081a96b..24f49c3 100644 --- a/src/renderer/src/features/auth/ui/login/components/HeroSection.tsx +++ b/src/renderer/src/features/auth/ui/login/components/HeroSection.tsx @@ -1,11 +1,11 @@ -import Logo from '@assets/logo.svg?react'; -import Symbol from '@assets/symbol.svg?react'; +import Logo from '@assets/common/icons/logo.svg?react'; +import Symbol from '@assets/common/icons/symbol.svg?react'; export default function HeroSection() { return (
- +

diff --git a/src/renderer/src/features/auth/ui/login/components/LoginButton.tsx b/src/renderer/src/features/auth/ui/login/components/LoginButton.tsx index c51a1d6..adb05b1 100644 --- a/src/renderer/src/features/auth/ui/login/components/LoginButton.tsx +++ b/src/renderer/src/features/auth/ui/login/components/LoginButton.tsx @@ -9,7 +9,7 @@ interface LoginButtonProps { } export default function LoginButton({ - text = '', + text: _text = '', type = 'button', onClick, disabled = true, diff --git a/src/renderer/src/features/auth/ui/login/components/Loginforrm.tsx b/src/renderer/src/features/auth/ui/login/components/Loginforrm.tsx index d310553..e2a2eaa 100644 --- a/src/renderer/src/features/auth/ui/login/components/Loginforrm.tsx +++ b/src/renderer/src/features/auth/ui/login/components/Loginforrm.tsx @@ -1,10 +1,10 @@ +import FailIcon from '@assets/auth/error_icon.svg?react'; +import SaveIdIcon from '@assets/auth/saveid_icon.svg?react'; import { useLoginMutation } from '@entities/user'; import { TextField as TextInput } from '@shared/ui/input-field'; import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; -import FailIcon from '@assets/auth/error_icon.svg?react'; -import SaveIdIcon from '@assets/auth/saveid_icon.svg?react'; import LoginButton from './LoginButton'; import PasswordField from './PasswordField'; @@ -22,7 +22,7 @@ const LoginForm = () => { handleSubmit, watch, setValue, - formState: { isValid }, + } = useForm({ mode: 'onChange', defaultValues: { diff --git a/src/renderer/src/features/auth/ui/login/index.ts b/src/renderer/src/features/auth/ui/login/index.ts deleted file mode 100644 index ed7ee8d..0000000 --- a/src/renderer/src/features/auth/ui/login/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as LoginPage } from './LoginPage'; - diff --git a/src/renderer/src/features/auth/ui/signup/EmailVerificationCallbackPage.tsx b/src/renderer/src/features/auth/ui/signup/EmailVerificationCallbackPage.tsx deleted file mode 100644 index 718f9c0..0000000 --- a/src/renderer/src/features/auth/ui/signup/EmailVerificationCallbackPage.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useEffect } from 'react'; -import { useSearchParams } from 'react-router-dom'; -import { useVerifyEmailMutation } from '@entities/user'; -import CompletionCharacter from '@assets/completion.svg?react'; - -const EmailVerificationCallbackPage = () => { - const [searchParams] = useSearchParams() - - const verifyEmailMutation = useVerifyEmailMutation(); - useEffect(() => { - const token = searchParams.get('token'); - if (token) { - verifyEmailMutation.mutate(token); - } - }, [searchParams]); - - return ( -

-
-
-
-
-

환영합니다

-

- 이메일 인증이 완료되었습니다. -
- 거부기린 앱으로 돌아가서 -
- 로그인하여 서비스를 이용해주세요. -

-
-
-
-
-
- ); -}; - -export default EmailVerificationCallbackPage; - diff --git a/src/renderer/src/features/auth/ui/signup/index.ts b/src/renderer/src/features/auth/ui/signup/index.ts deleted file mode 100644 index dc69aec..0000000 --- a/src/renderer/src/features/auth/ui/signup/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { default as EmailVerificationCallbackPage } from './EmailVerificationCallbackPage'; -export { default as EmailVerificationPage } from './EmailVerificationPage'; -export { default as ResendVerificationPage } from './ResendVerificationPage'; -export { default as SignUpPage } from './SignUpPage'; diff --git a/src/renderer/src/features/calibration/index.ts b/src/renderer/src/features/calibration/index.ts index 6a63040..2d99edf 100644 --- a/src/renderer/src/features/calibration/index.ts +++ b/src/renderer/src/features/calibration/index.ts @@ -1,3 +1,2 @@ export * from './ui'; export * from './lib'; - diff --git a/src/renderer/src/features/calibration/lib/index.ts b/src/renderer/src/features/calibration/lib/index.ts index 60bf772..194c2d4 100644 --- a/src/renderer/src/features/calibration/lib/index.ts +++ b/src/renderer/src/features/calibration/lib/index.ts @@ -1,2 +1 @@ export { useNotificationScheduler } from './useNotificationScheduler'; - diff --git a/src/renderer/src/features/calibration/lib/useNotificationScheduler.ts b/src/renderer/src/features/calibration/lib/useNotificationScheduler.ts index dfcf1ee..2771fe4 100644 --- a/src/renderer/src/features/calibration/lib/useNotificationScheduler.ts +++ b/src/renderer/src/features/calibration/lib/useNotificationScheduler.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { useNotificationStore } from '@features/notification'; import { usePostureStore } from '@entities/posture'; @@ -18,7 +18,7 @@ export const useNotificationScheduler = () => { const badPostureStartTime = useRef(null); /* 스트레칭 알림 표시 */ - const showStretchingNotification = async () => { + const showStretchingNotification = useCallback(async () => { try { await window.electronAPI.notification.show( '스트레칭 시간이에요! 🧘', @@ -27,10 +27,10 @@ export const useNotificationScheduler = () => { } catch (error) { console.error('Failed to show stretching notification:', error); } - }; + }, [stretching.interval]); /* 거북목 알림 표시 */ - const showTurtleNeckNotification = async () => { + const showTurtleNeckNotification = useCallback(async () => { try { await window.electronAPI.notification.show( '자세를 확인해주세요! 🐢', @@ -39,7 +39,7 @@ export const useNotificationScheduler = () => { } catch (error) { console.error('Failed to show turtle neck notification:', error); } - }; + }, [turtleNeck.interval]); /* 스트레칭 타이머 설정 */ useEffect(() => { @@ -69,7 +69,7 @@ export const useNotificationScheduler = () => { stretchingTimerRef.current = null; } }; - }, [isAllow, stretching.isEnabled, stretching.interval]); + }, [isAllow, stretching.isEnabled, stretching.interval, showStretchingNotification]); /* 거북목 상태 추적 - postureClass가 4, 5, 6 (bugi 계열)일 때 시작 시간 기록 */ useEffect(() => { @@ -124,7 +124,7 @@ export const useNotificationScheduler = () => { turtleNeckCheckRef.current = null; } }; - }, [isAllow, turtleNeck.isEnabled, turtleNeck.interval]); + }, [isAllow, turtleNeck.isEnabled, turtleNeck.interval, showTurtleNeckNotification]); /* 수동으로 알림을 트리거하는 함수들 (테스트용) */ return { diff --git a/src/renderer/src/features/calibration/ui/components/WebcamView.tsx b/src/renderer/src/features/calibration/ui/components/WebcamView.tsx index 7167422..7884fd0 100644 --- a/src/renderer/src/features/calibration/ui/components/WebcamView.tsx +++ b/src/renderer/src/features/calibration/ui/components/WebcamView.tsx @@ -1,4 +1,4 @@ -import SleepIcon from '@assets/sleep.svg?react'; +import SleepIcon from '@assets/common/icons/sleep.svg?react'; import { PoseDetection, PoseLandmark, @@ -133,7 +133,7 @@ const WebcamView = ({ for (const entry of entries) { const { width, height } = entry.contentRect; if (width > 0 && height > 0) { - setVideoDimensions((prev) => { + setVideoDimensions((_prev) => { // 카메라가 켜져있을 때는 실제 비디오 크기를 우선 사용 if (cameraState === 'show' && webcamRef.current?.video) { const video = webcamRef.current.video; diff --git a/src/renderer/src/features/calibration/ui/index.ts b/src/renderer/src/features/calibration/ui/index.ts index c660040..72f6e7b 100644 --- a/src/renderer/src/features/calibration/ui/index.ts +++ b/src/renderer/src/features/calibration/ui/index.ts @@ -1,3 +1 @@ -export { default as CalibrationPage } from './CalibrationPage'; export { default as WebcamView } from './components/WebcamView'; - diff --git a/src/renderer/src/features/dashboard/index.ts b/src/renderer/src/features/dashboard/index.ts index 6a63040..2d99edf 100644 --- a/src/renderer/src/features/dashboard/index.ts +++ b/src/renderer/src/features/dashboard/index.ts @@ -1,3 +1,2 @@ export * from './ui'; export * from './lib'; - diff --git a/src/renderer/src/features/dashboard/lib/index.ts b/src/renderer/src/features/dashboard/lib/index.ts index 4555386..9ad91c8 100644 --- a/src/renderer/src/features/dashboard/lib/index.ts +++ b/src/renderer/src/features/dashboard/lib/index.ts @@ -1,3 +1,2 @@ export { useAutoMetricsSender } from './useAutoMetricsSender'; export { useSessionCleanup } from './useSessionCleanup'; - diff --git a/src/renderer/src/features/dashboard/ui/AttendacePanel.tsx b/src/renderer/src/features/dashboard/ui/AttendacePanel.tsx index acfd814..1dc3704 100644 --- a/src/renderer/src/features/dashboard/ui/AttendacePanel.tsx +++ b/src/renderer/src/features/dashboard/ui/AttendacePanel.tsx @@ -1,11 +1,11 @@ -import DownIcon from '@assets/arrow-narrow-down.svg?react'; -import UpIcon from '@assets/arrow-narrow-up.svg?react'; -import { useState } from 'react'; +import DownIcon from '@assets/common/icons/arrow-narrow-down.svg?react'; +import UpIcon from '@assets/common/icons/arrow-narrow-up.svg?react'; import { useAttendanceQuery } from '@entities/dashboard'; import { IntensitySlider } from '@shared/ui/intensity-slider'; import { PageMoveButton } from '@shared/ui/page-move-button'; import { PannelHeader } from '@shared/ui/panel-header'; import { ToggleSwitch } from '@shared/ui/toggle-switch'; +import { useState } from 'react'; type CalendarProps = { year: number; @@ -104,7 +104,7 @@ const Calendar = ({ year, month, attendances = {} }: CalendarProps) => { const todayMonth = today.getMonth(); const todayDate = today.getDate(); - const isSameMonth = todayYear === year && todayMonth === month; + // API 데이터에서 날짜별 레벨 가져오기 const getLevelForDay = (day: number): number | null => { @@ -113,7 +113,11 @@ const Calendar = ({ year, month, attendances = {} }: CalendarProps) => { const usageMinutes = attendances[dateStr]; // 사용 시간이 없거나 0이면 null (안 사용한 날 - 그레이로 표시) - if (usageMinutes === undefined || usageMinutes === null || usageMinutes === 0) { + if ( + usageMinutes === undefined || + usageMinutes === null || + usageMinutes === 0 + ) { return null; } @@ -245,7 +249,7 @@ const AttendacePanel = () => { uncheckedLabel="월간" checkedLabel="연간" checked={false} - onChange={() => {}} + onChange={() => { }} />
diff --git a/src/renderer/src/features/dashboard/ui/AverageGraph/hooks/useAverageGraphChart.ts b/src/renderer/src/features/dashboard/ui/AverageGraph/hooks/useAverageGraphChart.ts index 9bfac26..0310c10 100644 --- a/src/renderer/src/features/dashboard/ui/AverageGraph/hooks/useAverageGraphChart.ts +++ b/src/renderer/src/features/dashboard/ui/AverageGraph/hooks/useAverageGraphChart.ts @@ -1,6 +1,6 @@ -import { useEffect, useMemo, useState } from 'react'; -import { getColor } from '@shared/lib/get-color'; import { usePostureGraphQuery } from '@entities/dashboard'; +import { getColor } from '@shared/lib/get-color'; +import { useMemo } from 'react'; type AverageGraphDatum = { periodLabel: string; @@ -19,24 +19,8 @@ type ChartConfig = { }; export function useAverageGraphChart(activePeriod: AverageGraphPeriod) { - const [isDark, setIsDark] = useState(() => - document.documentElement.classList.contains('dark'), - ); - const { data: apiData } = usePostureGraphQuery(); - /* html의 class 속성 변경될 때마다 콜백 실행(다크모드 감지) */ - useEffect(() => { - const observer = new MutationObserver(() => { - setIsDark(document.documentElement.classList.contains('dark')); - }); - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ['class'], - }); - return () => observer.disconnect(); - }, []); - /* 그래프 색상 */ const chartConfig = useMemo(() => { const gridColorValue = getColor('--color-grey-50', '#efeeed'); @@ -88,7 +72,7 @@ export function useAverageGraphChart(activePeriod: AverageGraphPeriod) { gridColor: gridColorValue, yAxisTicks: ticks, }; - }, [activePeriod, isDark, apiData]); + }, [activePeriod, apiData]); return chartConfig; } diff --git a/src/renderer/src/features/dashboard/ui/CharacterSpeedRow.tsx b/src/renderer/src/features/dashboard/ui/CharacterSpeedRow.tsx index f4855d8..a88d057 100644 --- a/src/renderer/src/features/dashboard/ui/CharacterSpeedRow.tsx +++ b/src/renderer/src/features/dashboard/ui/CharacterSpeedRow.tsx @@ -1,9 +1,6 @@ -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'; +import { + CHARACTER_COMPONENTS, +} from './CharacterSpeedRow/constants'; interface CharacterSpeedRowProps { level: number; @@ -11,36 +8,18 @@ interface CharacterSpeedRowProps { 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 && (
@@ -52,14 +31,4 @@ const CharacterSpeedRow = ({ level, name, speed }: CharacterSpeedRowProps) => { ); }; -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/features/dashboard/ui/CharacterSpeedRow/constants.ts b/src/renderer/src/features/dashboard/ui/CharacterSpeedRow/constants.ts new file mode 100644 index 0000000..5d841b5 --- /dev/null +++ b/src/renderer/src/features/dashboard/ui/CharacterSpeedRow/constants.ts @@ -0,0 +1,43 @@ +import AngelRiniModal from '@assets/modal/angel-rini-modal.svg?react'; +import BugiModal from '@assets/modal/bugi-modal.svg?react'; +import PmRiniModal from '@assets/modal/pm-rini-modal.svg?react'; +import RiniModal from '@assets/modal/rini-modal.svg?react'; +import StoneBugiModal from '@assets/modal/stone-bugi-modal.svg?react'; +import TireBugiModal from '@assets/modal/tire-bugi-modal.svg?react'; +import * as React from 'react'; + +interface CharacterSpeedRowProps { + level: number; + name: string; + speed: string; +} + +export const CHARACTER_COMPONENTS: Record< + number, + React.ComponentType> +> = { + 1: TireBugiModal, + 2: StoneBugiModal, + 3: BugiModal, + 4: RiniModal, + 5: PmRiniModal, + 6: AngelRiniModal, +}; + +export const CHARACTER_NAMES: Record = { + 1: '타이어 맨 거부기', + 2: '돌덩이 거부기', + 3: '거부기', + 4: '기린', + 5: '씽씽이 기린', + 6: '천사기린', +}; + +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' }, +]; diff --git a/src/renderer/src/features/dashboard/ui/ExitPanel.tsx b/src/renderer/src/features/dashboard/ui/ExitPanel.tsx index 9634d42..2a48dde 100644 --- a/src/renderer/src/features/dashboard/ui/ExitPanel.tsx +++ b/src/renderer/src/features/dashboard/ui/ExitPanel.tsx @@ -1,7 +1,7 @@ +import { useLevelQuery } from '@entities/dashboard'; +import { useSessionReportQuery } from '@entities/session'; import { useEffect, useMemo, useState } from 'react'; import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts'; -import { useSessionReportQuery } from '@entities/session'; -import { useLevelQuery } from '@entities/dashboard'; const ExitPanel = () => { const [sessionId, setSessionId] = useState(null); @@ -15,7 +15,7 @@ const ExitPanel = () => { if (id && id !== sessionId) { setSessionId(id); } - }, []); + }, [sessionId]); // 세션 리포트 조회 const { data, isLoading, error } = useSessionReportQuery(sessionId); @@ -23,22 +23,6 @@ const ExitPanel = () => { // 현재 이동거리 조회 const { data: levelData } = useLevelQuery(); - // 다크모드 상태 (간단한 방법) - const [isDark, setIsDark] = useState(() => - document.documentElement.classList.contains('dark'), - ); - - useEffect(() => { - const observer = new MutationObserver(() => { - setIsDark(document.documentElement.classList.contains('dark')); - }); - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ['class'], - }); - return () => observer.disconnect(); - }, []); - // CSS 변수에서 색상 가져오기 const getColor = (cssVar: string, fallback: string) => { return ( @@ -53,7 +37,6 @@ const ExitPanel = () => { const goodSeconds = data?.data.goodSeconds || 0; const totalTime = Math.round(totalSeconds / 60); // 초를 분으로 변환 - const correctPostureTime = Math.round(goodSeconds / 60); // 바른 자세 시간 (분) // 비율은 초 단위로 먼저 계산 후 반올림 (정확도 향상) const correctPosturePercentage = @@ -79,7 +62,7 @@ const ExitPanel = () => { background: getColor('--color-grey-25', '#e5e7eb'), score: getColor('--color-yellow-400', '#fbbf24'), }), - [isDark], + [], ); // 안쪽 링 배경 데이터 (회색) - 전체를 회색으로 채움 diff --git a/src/renderer/src/features/dashboard/ui/HighlightsPanel.tsx b/src/renderer/src/features/dashboard/ui/HighlightsPanel.tsx index aa81472..7c0594e 100644 --- a/src/renderer/src/features/dashboard/ui/HighlightsPanel.tsx +++ b/src/renderer/src/features/dashboard/ui/HighlightsPanel.tsx @@ -142,7 +142,7 @@ const HighlightsPanel = () => { const datum = data[index] as HighlightDatum | undefined; if (!datum) return null; - + const isCurrent = datum.barKey === 'current'; // 막대 중앙 좌표 diff --git a/src/renderer/src/features/dashboard/ui/HighlightsPanel/hooks/useHighlightsChart.ts b/src/renderer/src/features/dashboard/ui/HighlightsPanel/hooks/useHighlightsChart.ts index 8f5a019..2190d3a 100644 --- a/src/renderer/src/features/dashboard/ui/HighlightsPanel/hooks/useHighlightsChart.ts +++ b/src/renderer/src/features/dashboard/ui/HighlightsPanel/hooks/useHighlightsChart.ts @@ -1,6 +1,6 @@ -import { useEffect, useMemo, useState } from 'react'; import { useHighlightQuery } from '@entities/dashboard'; import { getColor } from '@shared/lib/get-color'; +import { useMemo } from 'react'; import type { HighlightDatum } from '../data'; export type HighlightPeriod = 'weekly' | 'monthly'; @@ -33,22 +33,6 @@ type ChartConfig = { }; export function useHighlightsChart(activePeriod: HighlightPeriod): ChartConfig { - // 다크모드 상태 감지 - const [isDark, setIsDark] = useState(() => - document.documentElement.classList.contains('dark'), - ); - - useEffect(() => { - const observer = new MutationObserver(() => { - setIsDark(document.documentElement.classList.contains('dark')); - }); - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ['class'], - }); - return () => observer.disconnect(); - }, []); - // 현재 날짜 기준으로 year, month 계산 const now = new Date(); const currentYear = now.getFullYear(); @@ -84,15 +68,19 @@ export function useHighlightsChart(activePeriod: HighlightPeriod): ChartConfig { ]; } + // highlightData.data.current와 highlightData.data.previous를 직접 참조하여 React Compiler 경고 해결 + const previousValue = highlightData.data.previous; + const currentValue = highlightData.data.current; + return [ { periodLabel: periodLabel[0], - value: highlightData.data.previous, + value: previousValue, barKey: 'previous', }, { periodLabel: periodLabel[1], - value: highlightData.data.current, + value: currentValue, barKey: 'current', }, ]; @@ -104,7 +92,7 @@ export function useHighlightsChart(activePeriod: HighlightPeriod): ChartConfig { previous: getColor('--color-grey-100', '#e3e1df'), // 저번 주/달 바 색 current: getColor('--color-sementic-brand-primary', '#ffbf00'), // 이번 주/달 바 색 }), - [isDark], + [], ); const chartConfig = useMemo(() => { @@ -153,7 +141,7 @@ export function useHighlightsChart(activePeriod: HighlightPeriod): ChartConfig { maxDomain: maxValue, yAxisTicks: ticks, }; - }, [activePeriod, chartColors, chartData]); + }, [chartColors, chartData]); return chartConfig; } diff --git a/src/renderer/src/features/dashboard/ui/MainHeader.tsx b/src/renderer/src/features/dashboard/ui/MainHeader.tsx index b65c27d..ee23529 100644 --- a/src/renderer/src/features/dashboard/ui/MainHeader.tsx +++ b/src/renderer/src/features/dashboard/ui/MainHeader.tsx @@ -1,10 +1,10 @@ -import DashboardIcon from '@assets/dashboard.svg?react'; -import SettingIcon from '@assets/setting.svg?react'; +import DashboardIcon from '@assets/common/icons/dashboard.svg?react'; +import SettingIcon from '@assets/common/icons/setting.svg?react'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import Logo from '@assets/logo.svg?react'; +import Logo from '@assets/common/icons/logo.svg?react'; import NotificationIcon from '@assets/main/bell_icon.svg?react'; -import Symbol from '@assets/symbol.svg?react'; +import Symbol from '@assets/common/icons/symbol.svg?react'; import { Button } from '@shared/ui/button'; import { ThemeToggleSwitch } from '@shared/ui/theme-toggle-switch'; diff --git a/src/renderer/src/features/dashboard/ui/PosePatternPanel.tsx b/src/renderer/src/features/dashboard/ui/PosePatternPanel.tsx index 91c91f0..c004fbe 100644 --- a/src/renderer/src/features/dashboard/ui/PosePatternPanel.tsx +++ b/src/renderer/src/features/dashboard/ui/PosePatternPanel.tsx @@ -1,16 +1,16 @@ import * as React from 'react'; -import CalendarIcon from '@assets/calendar.svg?react'; -import ChevronRigthIcon from '@assets/chevron-right.svg?react'; -import ClockIcon from '@assets/clock.svg?react'; -import GlassHourIcon from '@assets/hourglass.svg?react'; -import TumbupIcon from '@assets/thumbup.svg?react'; +import CalendarIcon from '@assets/common/icons/calendar.svg?react'; +import ChevronRigthIcon from '@assets/common/icons/chevron-right.svg?react'; +import ClockIcon from '@assets/common/icons/clock.svg?react'; +import GlassHourIcon from '@assets/common/icons/hourglass.svg?react'; +import TumbupIcon from '@assets/common/icons/thumbup.svg?react'; import { usePosturePatternQuery } from '@entities/dashboard'; import { PannelHeader } from '@shared/ui/panel-header'; // 시간 형식 변환: "14:00:00" -> "오후 2시" const formatTime = (timeStr: string): string => { - const [hours, minutes] = timeStr.split(':').map(Number); + const [hours,] = timeStr.split(':').map(Number); const hour12 = hours % 12 || 12; const period = hours < 12 ? '오전' : '오후'; return `${period} ${hour12}시`; @@ -76,7 +76,7 @@ const PatternHeader = React.forwardRef( PatternHeader.displayName = 'PatternHeader'; const PosePatternPanel = () => { - const { data: patternData, isLoading } = usePosturePatternQuery(); + const { data: patternData } = usePosturePatternQuery(); const worstTime = patternData?.data.worstTime ? formatTime(patternData.data.worstTime) diff --git a/src/renderer/src/features/dashboard/ui/RunningPanel.tsx b/src/renderer/src/features/dashboard/ui/RunningPanel.tsx index 008540d..84df75e 100644 --- a/src/renderer/src/features/dashboard/ui/RunningPanel.tsx +++ b/src/renderer/src/features/dashboard/ui/RunningPanel.tsx @@ -13,11 +13,11 @@ import RiniSvg from '@assets/video/rini.svg'; import StoneBugiRestSvg from '@assets/video/stone-bugi-rest.svg'; import TireBugiRestSvg from '@assets/video/tire-bugi-rest.svg'; -import { useEffect, useMemo, useRef } from 'react'; -import { useCameraStore } from '@widgets/camera'; import { usePostureStore } from '@entities/posture'; import { cn } from '@shared/lib/cn'; import { getScoreLevel } from '@shared/lib/get-score-level'; +import { useCameraStore } from '@widgets/camera'; +import { useEffect, useMemo, useRef } from 'react'; const RunningPanel = () => { const score = usePostureStore((state) => state.score); @@ -119,23 +119,6 @@ const RunningPanel = () => { } }, [isCameraShow]); - // 위젯 창 상태 확인 - useEffect(() => { - const checkWidgetStatus = async () => { - if (window.electronAPI?.widget) { - const isOpen = await window.electronAPI.widget.isOpen(); - } - }; - - checkWidgetStatus(); - - // 주기적으로 위젯 상태 확인 (위젯이 외부에서 닫힐 수 있음) - const interval = setInterval(checkWidgetStatus, 1000); - return () => clearInterval(interval); - }, []); - - // 위젯 열기/닫기 핸들러 - return (
diff --git a/src/renderer/src/features/dashboard/ui/TotalDistanceModal.tsx b/src/renderer/src/features/dashboard/ui/TotalDistanceModal.tsx index 2aa7e9f..6ebe0f6 100644 --- a/src/renderer/src/features/dashboard/ui/TotalDistanceModal.tsx +++ b/src/renderer/src/features/dashboard/ui/TotalDistanceModal.tsx @@ -1,56 +1,57 @@ import { Button } from '@shared/ui/button'; import { ModalPortal } from '@shared/ui/modal'; -import CharacterSpeedRow, { CHARACTER_SPEED_DATA } from './CharacterSpeedRow'; +import CharacterSpeedRow from './CharacterSpeedRow'; +import { CHARACTER_SPEED_DATA } from './CharacterSpeedRow/constants'; 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; diff --git a/src/renderer/src/features/dashboard/ui/TotalDistancePanel.tsx b/src/renderer/src/features/dashboard/ui/TotalDistancePanel.tsx index 07eeb58..4408633 100644 --- a/src/renderer/src/features/dashboard/ui/TotalDistancePanel.tsx +++ b/src/renderer/src/features/dashboard/ui/TotalDistancePanel.tsx @@ -16,13 +16,13 @@ const TotalDistance = () => { return ( <>
-
+
{isLoading ? '로딩 중...' : `Level.${level + 1} `} diff --git a/src/renderer/src/features/dashboard/ui/WebcamPanel.tsx b/src/renderer/src/features/dashboard/ui/WebcamPanel.tsx index 239d4a6..cf9e0ff 100644 --- a/src/renderer/src/features/dashboard/ui/WebcamPanel.tsx +++ b/src/renderer/src/features/dashboard/ui/WebcamPanel.tsx @@ -1,6 +1,6 @@ -import HideIcon from '@assets/hide.svg?react'; -import ShowIcon from '@assets/show.svg?react'; -import WidgetIcon from '@assets/widget.svg?react'; +import HideIcon from '@assets/common/icons/hide.svg?react'; +import ShowIcon from '@assets/common/icons/show.svg?react'; +import WidgetIcon from '@assets/common/icons/widget.svg?react'; import { useCreateSessionMutation, usePauseSessionMutation, @@ -8,10 +8,7 @@ import { useStopSessionMutation, } from '@entities/session'; import { Button } from '@shared/ui/button'; -import { - PoseLandmark, - WorldLandmark, -} from '@entities/posture'; +import { PoseLandmark, WorldLandmark } from '@entities/posture'; import { useWidget } from '@widgets/widget'; import { useCameraStore } from '@widgets/camera'; import { WebcamView } from '@features/calibration/ui'; diff --git a/src/renderer/src/features/dashboard/ui/index.ts b/src/renderer/src/features/dashboard/ui/index.ts index 033c541..08d5e71 100644 --- a/src/renderer/src/features/dashboard/ui/index.ts +++ b/src/renderer/src/features/dashboard/ui/index.ts @@ -15,4 +15,3 @@ export { default as TotalDistanceModal } from './TotalDistanceModal'; export { default as TotalDistancePanel } from './TotalDistancePanel'; export { default as TrendPanel } from './TrendPanel'; export { default as WebcamPanel } from './WebcamPanel'; - diff --git a/src/renderer/src/features/notification/index.ts b/src/renderer/src/features/notification/index.ts index c18b7f4..c51f0c5 100644 --- a/src/renderer/src/features/notification/index.ts +++ b/src/renderer/src/features/notification/index.ts @@ -1,3 +1,2 @@ export * from './ui'; export * from './model'; - diff --git a/src/renderer/src/features/notification/model/index.ts b/src/renderer/src/features/notification/model/index.ts index af58b41..4f733d9 100644 --- a/src/renderer/src/features/notification/model/index.ts +++ b/src/renderer/src/features/notification/model/index.ts @@ -1,3 +1,2 @@ export { useNotificationStore } from './use-notification-store'; export type { NotificationSettings } from './use-notification-store'; - diff --git a/src/renderer/src/features/notification/ui/index.ts b/src/renderer/src/features/notification/ui/index.ts index c16ae80..8ada3ff 100644 --- a/src/renderer/src/features/notification/ui/index.ts +++ b/src/renderer/src/features/notification/ui/index.ts @@ -1,2 +1 @@ export { default as NotificationModal } from './NotificationModal'; - diff --git a/src/renderer/src/features/onboarding/index.ts b/src/renderer/src/features/onboarding/index.ts deleted file mode 100644 index ccf1f66..0000000 --- a/src/renderer/src/features/onboarding/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './ui'; - diff --git a/src/renderer/src/features/onboarding/ui/components/InfoPanel.tsx b/src/renderer/src/features/onboarding/ui/components/InfoPanel.tsx index ccc6205..089b04e 100644 --- a/src/renderer/src/features/onboarding/ui/components/InfoPanel.tsx +++ b/src/renderer/src/features/onboarding/ui/components/InfoPanel.tsx @@ -61,8 +61,9 @@ const InfoPanel = ({ currentStep, onNext, direction }: InfoPanelProps) => { {Array.from({ length: 5 }).map((_, index) => ( ))}
diff --git a/src/renderer/src/features/onboarding/ui/index.ts b/src/renderer/src/features/onboarding/ui/index.ts deleted file mode 100644 index bf9a24b..0000000 --- a/src/renderer/src/features/onboarding/ui/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { default as OnboardingPage } from './OnboardingPage'; -export { default as OnboardingInitPage } from './OnboardingInitPage'; -export { default as OnboardingCompletionPage } from './OnboardingCompletionPage'; - diff --git a/src/renderer/src/index.css b/src/renderer/src/index.css index 9677945..b9b6c85 100644 --- a/src/renderer/src/index.css +++ b/src/renderer/src/index.css @@ -1,5 +1,5 @@ @import 'tailwindcss'; -@import './styles/globals.css'; +@import './shared/styles/globals.css'; @font-face { font-family: 'Pretendard'; diff --git a/src/renderer/src/pages/calibration-page/components/MeasuringPanel.tsx b/src/renderer/src/pages/calibration-page/components/MeasuringPanel.tsx new file mode 100644 index 0000000..160176f --- /dev/null +++ b/src/renderer/src/pages/calibration-page/components/MeasuringPanel.tsx @@ -0,0 +1,34 @@ +import { NotificateMessage } from '@shared/ui/notification-message'; + +interface MeasuringPanelProps { + step1Error?: string | null; + step2Error?: string | null; +} + +const MeasuringPanel = ({ step1Error, step2Error }: MeasuringPanelProps) => { + return ( +
+
+

+ 바른자세 기준점 등록 +

+
+ + +
+
+
+ ); +}; + +export default MeasuringPanel; diff --git a/src/renderer/src/pages/calibration-page/components/WebcamView.tsx b/src/renderer/src/pages/calibration-page/components/WebcamView.tsx new file mode 100644 index 0000000..5787108 --- /dev/null +++ b/src/renderer/src/pages/calibration-page/components/WebcamView.tsx @@ -0,0 +1,270 @@ +import SleepIcon from '@assets/common/icons/sleep.svg?react'; +import { + PoseDetection, + PoseLandmark, + PoseVisualizer, + WorldLandmark, +} from '@entities/posture'; +import { Timer } from '@shared/ui/timer'; +import { useCameraStore } from '@widgets/camera'; +import { useEffect, useRef, useState, type RefObject } from 'react'; +import Webcam from 'react-webcam'; + +interface WebcamViewProps { + onPoseDetected?: ( + landmarks: PoseLandmark[], + worldLandmarks?: WorldLandmark[], + ) => void; + showPoseOverlay?: boolean; + showTimer?: boolean; + remainingTime?: number; + onVideoRefReady?: (videoRef: RefObject) => void; +} + +const WebcamView = ({ + onPoseDetected, + showPoseOverlay = false, + showTimer = false, + remainingTime = 0, + onVideoRefReady, +}: WebcamViewProps) => { + const webcamRef = useRef(null); + + // 비디오 ref를 부모 컴포넌트에 전달 + useEffect(() => { + if (onVideoRefReady) { + onVideoRefReady(webcamRef as RefObject); + } + }, [onVideoRefReady]); + const containerRef = useRef(null); + const [detectedLandmarks, setDetectedLandmarks] = useState( + [], + ); + const [videoDimensions, setVideoDimensions] = useState({ + width: 760, + height: 428, + }); + + // 초기 마운트 시 container 크기로 초기화 + useEffect(() => { + const container = containerRef.current; + if (container) { + const { clientWidth, clientHeight } = container; + if (clientWidth > 0 && clientHeight > 0) { + setVideoDimensions({ + width: clientWidth, + height: clientHeight, + }); + } + } + }, []); + + const { cameraState, setShow } = useCameraStore(); + const isWebcamOn = cameraState === 'show'; + + // 저장된 카메라 deviceId 사용 + const preferredDeviceId = localStorage.getItem('preferred-camera-device'); + + const videoConstraints = preferredDeviceId + ? { + deviceId: { exact: preferredDeviceId }, + width: 1000, + height: 563, + } + : { + facingMode: 'user', + width: 1000, + height: 563, + }; + + const handlePoseDetected = ( + landmarks: PoseLandmark[], + worldLandmarks?: WorldLandmark[], + ) => { + setDetectedLandmarks(landmarks); + onPoseDetected?.(landmarks, worldLandmarks); + }; + + const handleUserMedia = (stream: MediaStream | null) => { + if (stream) { + setShow(); + const videoTrack = stream.getVideoTracks()[0]; + if (videoTrack) { + const settings = videoTrack.getSettings(); + setVideoDimensions({ + width: settings.width || 760, + height: settings.height || 428, + }); + } + } else { + console.warn('[WebcamView] handleUserMedia called with null stream'); + } + }; + + const handleUserMediaError = (error: string | DOMException) => { + console.error('[WebcamView] handleUserMediaError:', error); + if (error instanceof DOMException) { + console.error('[WebcamView] Error name:', error.name); + console.error('[WebcamView] Error message:', error.message); + } + }; + + // 카메라 스트림 정리 + useEffect(() => { + if (cameraState === 'hide' || cameraState === 'exit') { + if ( + webcamRef.current && + webcamRef.current.video && + webcamRef.current.video.srcObject + ) { + const stream = webcamRef.current.video.srcObject as MediaStream; + const tracks = stream.getTracks(); + tracks.forEach((track) => track.stop()); + } + } + }, [cameraState]); + + // containerRef 크기 변경 감지 + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { width, height } = entry.contentRect; + if (width > 0 && height > 0) { + setVideoDimensions((_prev) => { + // 카메라가 켜져있을 때는 실제 비디오 크기를 우선 사용 + if (cameraState === 'show' && webcamRef.current?.video) { + const video = webcamRef.current.video; + return { + width: video.videoWidth || width, + height: video.videoHeight || height, + }; + } + // 카메라가 꺼져있을 때는 container 크기 사용 + return { + width, + height, + }; + }); + } + } + }); + + resizeObserver.observe(container); + + return () => { + resizeObserver.disconnect(); + }; + }, [cameraState]); + + // 비디오 요소 크기 변경 감지 (카메라가 켜져있을 때) + useEffect(() => { + if (cameraState !== 'show') return; + + const video = webcamRef.current?.video; + if (!video) return; + + const handleResize = () => { + if (video.videoWidth > 0 && video.videoHeight > 0) { + setVideoDimensions({ + width: video.videoWidth, + height: video.videoHeight, + }); + } + }; + + video.addEventListener('loadedmetadata', handleResize); + video.addEventListener('resize', handleResize); + + return () => { + video.removeEventListener('loadedmetadata', handleResize); + video.removeEventListener('resize', handleResize); + }; + }, [cameraState]); + + return ( +
+ {cameraState === 'show' ? ( +
+ + {showPoseOverlay && detectedLandmarks.length > 0 && ( + + )} + {showTimer && ( +
+ +
+ )} + + } + onPoseDetected={handlePoseDetected} + isEnabled={isWebcamOn} + /> +
+ ) : cameraState === 'hide' ? ( +
+
+ 측정을 멈췄어요!
+ 준비되면 카메라 버튼을 눌러주세요. +
+
+ ) : ( +
+
+
+ 오늘 한걸음 나아갔네요
+ 내일을 위해 쉬어요 + +
+
+
+ )} +
+ ); +}; + +export default WebcamView; diff --git a/src/renderer/src/pages/calibration-page/components/WelcomePanel.tsx b/src/renderer/src/pages/calibration-page/components/WelcomePanel.tsx new file mode 100644 index 0000000..3684524 --- /dev/null +++ b/src/renderer/src/pages/calibration-page/components/WelcomePanel.tsx @@ -0,0 +1,38 @@ +import { Button } from '@shared/ui/button'; + +interface WelcomePanelProps { + isPoseDetected: boolean; + onStartMeasurement: () => void; +} + +const WelcomePanel = ({ + isPoseDetected, + onStartMeasurement, +}: WelcomePanelProps) => { + // localStorage에서 사용자 이름 가져오기 + const username = localStorage.getItem('userName') || '사용자'; + + return ( +
+
+

+ 바른자세 기준점 등록 +

+

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

+
+
+ ); +}; + +export default WelcomePanel; diff --git a/src/renderer/src/pages/calibration-page/index.ts b/src/renderer/src/pages/calibration-page/index.ts new file mode 100644 index 0000000..1d109f4 --- /dev/null +++ b/src/renderer/src/pages/calibration-page/index.ts @@ -0,0 +1 @@ +export { default } from './index.tsx'; diff --git a/src/renderer/src/features/calibration/ui/CalibrationPage.tsx b/src/renderer/src/pages/calibration-page/index.tsx similarity index 91% rename from src/renderer/src/features/calibration/ui/CalibrationPage.tsx rename to src/renderer/src/pages/calibration-page/index.tsx index 909aaa9..45c6d1f 100644 --- a/src/renderer/src/features/calibration/ui/CalibrationPage.tsx +++ b/src/renderer/src/pages/calibration-page/index.tsx @@ -1,4 +1,4 @@ -import CalibrationGuide from '@assets/calibration_guide.svg?react'; +import CalibrationGuide from '@assets/common/images/calibration_guide.svg?react'; import { PoseLandmark as AnalyzerPoseLandmark, calculateFrameBrightness, @@ -61,24 +61,14 @@ const CalibrationPage = () => { // 캘리브레이션 상태 const [isCalibrating, setIsCalibrating] = useState(false); - const [calibrationProgress, setCalibrationProgress] = useState(0); const [remainingTime, setRemainingTime] = useState(5); const worldLandmarksRef = useRef([]); - const [calibrationFrames, setCalibrationFrames] = useState< - Array<{ - lms: PoseLandmark[]; - pi: PIResult; - worldLms: WorldLandmark[]; - brightness?: number; - }> - >([]); // 예외 케이스 에러 메시지 상태 const [step1Error, setStep1Error] = useState(null); // Step 1 에러 (WelcomePanel용) const [step2Error, setStep2Error] = useState(null); // Step 2 에러 (MeasuringPanel step={2}용) - // 스텝 1: 최근 PI 값들을 저장하여 평균 계산 - const recentPIsRef = useRef([]); + // 비디오 ref 저장 const videoRefRef = useRef | null>(null); @@ -103,8 +93,6 @@ const CalibrationPage = () => { } setIsCalibrating(true); - setCalibrationFrames([]); - setCalibrationProgress(0); setRemainingTime(5); setStep2Error(null); }, [step1Error]); @@ -115,11 +103,6 @@ const CalibrationPage = () => { }; // 캘리브레이션 취소 - const cancelCalibration = () => { - setIsCalibrating(false); - setCalibrationProgress(0); - setCalibrationFrames([]); - }; // 측정하기 버튼 클릭 const handleStartMeasurement = useCallback(() => { @@ -147,7 +130,7 @@ const CalibrationPage = () => { // 이전 에러 상태 추적용 ref const prevStep2ErrorRef = useRef(null); const errorResetTimeRef = useRef(0); - const ERROR_HOLD_DURATION = 500; // 에러 상태를 500ms 동안 유지 + useEffect(() => { if (!isCalibrating) return; @@ -156,7 +139,6 @@ const CalibrationPage = () => { startTimeRef.current = Date.now(); framesRef.current = []; emaSmootherRef.current.reset(); - setCalibrationProgress(0); setRemainingTime(5); setStep2Error(null); prevStep2ErrorRef.current = null; @@ -170,9 +152,7 @@ const CalibrationPage = () => { // 타이머 업데이트 (1초마다) const timerInterval = setInterval(() => { const elapsed = Date.now() - startTimeRef.current; - const progress = Math.min(100, (elapsed / 5000) * 100); const remaining = Math.max(0, Math.ceil((5000 - elapsed) / 1000)); - setCalibrationProgress(progress); setRemainingTime(remaining); if (elapsed >= 5000) { @@ -181,7 +161,6 @@ const CalibrationPage = () => { stopCalibration(); const result = processCalibrationData(framesRef.current, true); if (result.success) { - setCalibrationFrames(framesRef.current); // 측정 완료 수치를 한 번만 콘솔에 출력 console.log('측정 완료 수치', { mu_PI: (result.mu_PI || 0).toFixed(4), @@ -270,7 +249,6 @@ const CalibrationPage = () => { // 스텝 1 에러가 발생했거나 계속 발생 중이면 시간 리셋 if (step1Err) { startTimeRef.current = Date.now(); - setCalibrationProgress(0); setRemainingTime(5); errorResetTimeRef.current = Date.now(); setStep1Error(step1Err); @@ -288,7 +266,6 @@ const CalibrationPage = () => { // 에러가 발생했거나 계속 발생 중이면 시간 리셋 (frames는 유지) if (error) { startTimeRef.current = Date.now(); - setCalibrationProgress(0); setRemainingTime(5); errorResetTimeRef.current = Date.now(); } @@ -304,7 +281,7 @@ const CalibrationPage = () => { clearInterval(timerInterval); clearInterval(dataInterval); }; - }, [isCalibrating]); + }, [isCalibrating, navigate]); // 상태에 따른 패딩 클래스 const paddingClass = isCalibrating diff --git a/src/renderer/src/pages/email-verification-callback-page/index.ts b/src/renderer/src/pages/email-verification-callback-page/index.ts new file mode 100644 index 0000000..1d109f4 --- /dev/null +++ b/src/renderer/src/pages/email-verification-callback-page/index.ts @@ -0,0 +1 @@ +export { default } from './index.tsx'; diff --git a/src/renderer/src/pages/email-verification-callback-page/index.tsx b/src/renderer/src/pages/email-verification-callback-page/index.tsx new file mode 100644 index 0000000..ea1dfb9 --- /dev/null +++ b/src/renderer/src/pages/email-verification-callback-page/index.tsx @@ -0,0 +1,40 @@ +import { useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { useVerifyEmailMutation } from '@entities/user'; +import CompletionCharacter from '@assets/common/icons/completion.svg?react'; + +const EmailVerificationCallbackPage = () => { + const [searchParams] = useSearchParams(); + + const verifyEmailMutation = useVerifyEmailMutation(); + useEffect(() => { + const token = searchParams.get('token'); + if (token) { + verifyEmailMutation.mutate(token); + } + }, [searchParams, verifyEmailMutation]); + + return ( +
+
+
+
+
+ +

환영합니다

+

+ 이메일 인증이 완료되었습니다. +
+ 거부기린 앱으로 돌아가서 +
+ 로그인하여 서비스를 이용해주세요. +

+
+
+
+
+
+ ); +}; + +export default EmailVerificationCallbackPage; diff --git a/src/renderer/src/pages/email-verification-page/index.ts b/src/renderer/src/pages/email-verification-page/index.ts new file mode 100644 index 0000000..1d109f4 --- /dev/null +++ b/src/renderer/src/pages/email-verification-page/index.ts @@ -0,0 +1 @@ +export { default } from './index.tsx'; diff --git a/src/renderer/src/features/auth/ui/signup/EmailVerificationPage.tsx b/src/renderer/src/pages/email-verification-page/index.tsx similarity index 83% rename from src/renderer/src/features/auth/ui/signup/EmailVerificationPage.tsx rename to src/renderer/src/pages/email-verification-page/index.tsx index 85b6899..7e6122e 100644 --- a/src/renderer/src/features/auth/ui/signup/EmailVerificationPage.tsx +++ b/src/renderer/src/pages/email-verification-page/index.tsx @@ -1,21 +1,16 @@ import { useNavigate } from 'react-router-dom'; import { Button } from '@shared/ui/button'; -import { - useResendVerifyEmailMuation, - useEmailStore, -} from '@entities/user'; -import EmailHeroSection from './components/EmailHeroSection'; -import ResendSection from './components/ResendSection'; +import { useResendVerifyEmailMuation, useEmailStore } from '@entities/user'; +import EmailHeroSection from '../signup-page/components/EmailHeroSection'; +import ResendSection from '../signup-page/components/ResendSection'; const EmailVerificationPage = () => { - const resendverifyEmailMutation = useResendVerifyEmailMuation(); const email = useEmailStore((state) => state.email); const navigate = useNavigate(); /* 토큰 여부에 따른 이메일 인증 */ - /*이메일 다시 보내기 */ const onResendClick = () => { resendverifyEmailMutation.mutate({ email: email, callbackUrl: '' }); diff --git a/src/renderer/src/pages/index.ts b/src/renderer/src/pages/index.ts new file mode 100644 index 0000000..e23065b --- /dev/null +++ b/src/renderer/src/pages/index.ts @@ -0,0 +1,11 @@ +export { default as LoginPage } from './login-page'; +export { default as SignUpPage } from './signup-page'; +export { default as EmailVerificationPage } from './email-verification-page'; +export { default as EmailVerificationCallbackPage } from './email-verification-callback-page'; +export { default as ResendVerificationPage } from './resend-verification-page'; +export { default as OnboardingPage } from './onboarding-page'; +export { default as OnboardingInitPage } from './onboarding-init-page'; +export { default as OnboardingCompletionPage } from './onboarding-completion-page'; +export { default as CalibrationPage } from './calibration-page'; +export { default as WidgetPage } from './widget-page'; +export { default as MainPage } from './main-page'; diff --git a/src/renderer/src/pages/login-page/components/HeroSection.tsx b/src/renderer/src/pages/login-page/components/HeroSection.tsx new file mode 100644 index 0000000..24f49c3 --- /dev/null +++ b/src/renderer/src/pages/login-page/components/HeroSection.tsx @@ -0,0 +1,16 @@ +import Logo from '@assets/common/icons/logo.svg?react'; +import Symbol from '@assets/common/icons/symbol.svg?react'; + +export default function HeroSection() { + return ( +
+
+ + +
+

+ 세상 모든 거북목들이 기린이 될 때까지 +

+
+ ); +} diff --git a/src/renderer/src/pages/login-page/components/LoginButton.tsx b/src/renderer/src/pages/login-page/components/LoginButton.tsx new file mode 100644 index 0000000..adb05b1 --- /dev/null +++ b/src/renderer/src/pages/login-page/components/LoginButton.tsx @@ -0,0 +1,31 @@ +import { Button } from '@shared/ui/button'; + +interface LoginButtonProps { + text?: string; + type?: 'button' | 'submit' | 'reset'; + onClick?: () => void; + disabled?: boolean; + className?: string; +} + +export default function LoginButton({ + text: _text = '', + type = 'button', + onClick, + disabled = true, + className = '', +}: LoginButtonProps) { + return ( + + ); +} diff --git a/src/renderer/src/pages/login-page/components/Loginforrm.tsx b/src/renderer/src/pages/login-page/components/Loginforrm.tsx new file mode 100644 index 0000000..c5fb922 --- /dev/null +++ b/src/renderer/src/pages/login-page/components/Loginforrm.tsx @@ -0,0 +1,137 @@ +import FailIcon from '@assets/auth/error_icon.svg?react'; +import SaveIdIcon from '@assets/auth/saveid_icon.svg?react'; +import { useLoginMutation } from '@entities/user'; +import { TextField as TextInput } from '@shared/ui/input-field'; +import { useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { useNavigate } from 'react-router-dom'; +import LoginButton from './LoginButton'; +import PasswordField from './PasswordField'; + +interface LoginFormData { + email: string; + password: string; + saveId: boolean; +} + +const SAVED_EMAIL_KEY = 'savedEmail'; + +const LoginForm = () => { + const { + register, + handleSubmit, + watch, + setValue, + } = useForm({ + mode: 'onChange', + defaultValues: { + email: '', + password: '', + saveId: false, + }, + }); + + useEffect(() => { + const savedEmail = localStorage.getItem(SAVED_EMAIL_KEY); + if (savedEmail) { + setValue('email', savedEmail); + setValue('saveId', true); + } + }, [setValue]); + + const loginMutation = useLoginMutation(); + const navigate = useNavigate(); + + /* @react-refresh-ignore */ + const email = watch('email'); + /* @react-refresh-ignore */ + const password = watch('password'); + + const handleSaveIdChange = (e: React.ChangeEvent) => { + setValue('saveId', e.target.checked); + }; + + const onSubmit = (data: LoginFormData) => { + console.log('로그인 시도:', data); + + loginMutation.mutate( + { + email: data.email, + password: data.password, + }, + { + onSuccess: () => { + /* 로그인 성공 시 아이디 저장 처리 */ + if (data.saveId) { + localStorage.setItem(SAVED_EMAIL_KEY, data.email); + } else { + localStorage.removeItem(SAVED_EMAIL_KEY); + } + }, + }, + ); + }; + + return ( + <> +
+ {/* 이메일 */} + + + {/* 비밀번호 */} + + {loginMutation.isError && ( +
+ + 이메일 또는 비밀번호가 올바르지 않습니다. +
+ )} + + {/* 아이디 저장 */} +
+ +
+ + {/* 버튼 */} + + + + {/* 회원가입 / 비밀번호 찾기 */} +
+ navigate('/auth/signup')} + className="cursor-pointer" + > + 회원가입 + + | + 비밀번호 찾기 +
+ + ); +}; + +export default LoginForm; diff --git a/src/renderer/src/pages/login-page/components/PasswordField.tsx b/src/renderer/src/pages/login-page/components/PasswordField.tsx new file mode 100644 index 0000000..77a3f20 --- /dev/null +++ b/src/renderer/src/pages/login-page/components/PasswordField.tsx @@ -0,0 +1,59 @@ +import { TextField as TextInput } from '@shared/ui/input-field'; +import { forwardRef, useState, type ChangeEvent } from 'react'; +import InvisibleIcon from '@assets/auth/invisible_icon.svg?react'; +import VisibleIcon from '@assets/auth/visible_icon.svg?react'; + +interface PasswordFieldProps { + hasValue?: boolean; + onChange?: (e: ChangeEvent) => void; + placeholder?: string; + className?: string; + name?: string; +} + +const PasswordField = forwardRef( + ( + { hasValue, onChange, placeholder = '비밀번호', className = '', name }, + ref, + ) => { + /* 비밀번호 보이기/숨기기 */ + const [isVisible, setIsVisible] = useState(false); + + const toggleVisibility = () => setIsVisible((prev) => !prev); + + return ( +
+ + + {/*hasvalue 있을때만 invisible/visible 아이콘 보이기 */} + {hasValue && ( + + )} +
+ ); + }, +); + +PasswordField.displayName = 'PasswordField'; + +export default PasswordField; diff --git a/src/renderer/src/pages/login-page/index.ts b/src/renderer/src/pages/login-page/index.ts new file mode 100644 index 0000000..1d109f4 --- /dev/null +++ b/src/renderer/src/pages/login-page/index.ts @@ -0,0 +1 @@ +export { default } from './index.tsx'; diff --git a/src/renderer/src/features/auth/ui/login/LoginPage.tsx b/src/renderer/src/pages/login-page/index.tsx similarity index 100% rename from src/renderer/src/features/auth/ui/login/LoginPage.tsx rename to src/renderer/src/pages/login-page/index.tsx diff --git a/src/renderer/src/pages/main-page/index.ts b/src/renderer/src/pages/main-page/index.ts new file mode 100644 index 0000000..1d109f4 --- /dev/null +++ b/src/renderer/src/pages/main-page/index.ts @@ -0,0 +1 @@ +export { default } from './index.tsx'; diff --git a/src/renderer/src/pages/Main/MainPage.tsx b/src/renderer/src/pages/main-page/index.tsx similarity index 100% rename from src/renderer/src/pages/Main/MainPage.tsx rename to src/renderer/src/pages/main-page/index.tsx diff --git a/src/renderer/src/pages/onboarding-completion-page/index.ts b/src/renderer/src/pages/onboarding-completion-page/index.ts new file mode 100644 index 0000000..1d109f4 --- /dev/null +++ b/src/renderer/src/pages/onboarding-completion-page/index.ts @@ -0,0 +1 @@ +export { default } from './index.tsx'; diff --git a/src/renderer/src/features/onboarding/ui/OnboardingCompletionPage.tsx b/src/renderer/src/pages/onboarding-completion-page/index.tsx similarity index 96% rename from src/renderer/src/features/onboarding/ui/OnboardingCompletionPage.tsx rename to src/renderer/src/pages/onboarding-completion-page/index.tsx index e976bba..695f6c1 100644 --- a/src/renderer/src/features/onboarding/ui/OnboardingCompletionPage.tsx +++ b/src/renderer/src/pages/onboarding-completion-page/index.tsx @@ -1,5 +1,5 @@ import { useNavigate } from 'react-router-dom'; -import CompletionCharacter from '@assets/completion.svg?react'; +import CompletionCharacter from '@assets/common/icons/completion.svg?react'; import { Button } from '@shared/ui/button'; import { useCreateSessionMutation } from '@entities/session'; import { useCameraStore } from '@widgets/camera'; diff --git a/src/renderer/src/pages/onboarding-init-page/index.ts b/src/renderer/src/pages/onboarding-init-page/index.ts new file mode 100644 index 0000000..1d109f4 --- /dev/null +++ b/src/renderer/src/pages/onboarding-init-page/index.ts @@ -0,0 +1 @@ +export { default } from './index.tsx'; diff --git a/src/renderer/src/features/onboarding/ui/OnboardingInitPage.tsx b/src/renderer/src/pages/onboarding-init-page/index.tsx similarity index 89% rename from src/renderer/src/features/onboarding/ui/OnboardingInitPage.tsx rename to src/renderer/src/pages/onboarding-init-page/index.tsx index 5eaeb83..7bd0552 100644 --- a/src/renderer/src/features/onboarding/ui/OnboardingInitPage.tsx +++ b/src/renderer/src/pages/onboarding-init-page/index.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import ImageDescriptionPannel from './components/ImageDescriptionPanel'; -import InfoPanel from './components/InfoPanel'; +import ImageDescriptionPannel from '../onboarding-page/components/ImageDescriptionPanel'; +import InfoPanel from '../onboarding-page/components/InfoPanel'; const OnboardinInitPage = () => { const navigate = useNavigate(); diff --git a/src/renderer/src/pages/onboarding-page/components/CameraPermissionButton.tsx b/src/renderer/src/pages/onboarding-page/components/CameraPermissionButton.tsx new file mode 100644 index 0000000..d479b1b --- /dev/null +++ b/src/renderer/src/pages/onboarding-page/components/CameraPermissionButton.tsx @@ -0,0 +1,84 @@ +import { useNavigate } from 'react-router-dom'; +import { Button } from '@shared/ui/button'; +import { useCameraStore } from '@widgets/camera'; + +const CameraPermissionButton = () => { + const navigate = useNavigate(); + const { setShow } = useCameraStore(); + + const requestCameraPermission = async () => { + try { + const isWindows = navigator.platform.includes('Win'); + + let stream: MediaStream | null = null; + let selectedDeviceId: string | null = null; + + // Windows 환경이면 Device 1 (두 번째 카메라) 사용 + if (isWindows) { + const devices = await navigator.mediaDevices.enumerateDevices(); // 연결된 모든 미디어 장치 탐색 + const videoDevices = devices.filter((d) => d.kind === 'videoinput'); // 카메라 목록만 구함 + + const targetDevice = videoDevices[1]; // 카메라 목록중 두 번째 카메라 선택 + if (targetDevice) { + stream = await navigator.mediaDevices.getUserMedia({ + video: { deviceId: { exact: targetDevice.deviceId } }, //지정한 카메라에 스트림 요청 + audio: false, + }); + selectedDeviceId = targetDevice.deviceId; //사용하는 카메라id 저장 변수 + } else { + stream = await navigator.mediaDevices.getUserMedia({ + video: true, + audio: false, + }); // 카메라 1대이면 기존 방식 사용 + } + } else { + // macOS 등 다른 환경: 기존 로직 + + stream = await navigator.mediaDevices.getUserMedia({ + video: true, + audio: false, + }); + const track = stream.getVideoTracks()[0]; + selectedDeviceId = track.getSettings().deviceId || null; + } + + if (!stream) { + throw new Error('사용 가능한 카메라를 찾을 수 없습니다.'); + } + + stream.getTracks().forEach((track) => { + track.stop(); + }); + + // 스트림이 완전히 해제될 때까지 약간의 딜레이 + await new Promise((resolve) => setTimeout(resolve, 100)); + + // 성공한 카메라 저장 + if (selectedDeviceId) { + localStorage.setItem('preferred-camera-device', selectedDeviceId); + } + + setShow(); // Set camera state to 'show' after permission is granted + + navigate('/onboarding/calibration'); + } catch (error) { + console.error('[CameraPermission] 카메라 권한 요청 실패:', error); + if (error instanceof Error) { + console.error('[CameraPermission] Error name:', error.name); + console.error('[CameraPermission] Error message:', error.message); + } + } + }; + + return ( +
+ ); +}; + +export default InfoPanel; diff --git a/src/renderer/src/pages/onboarding-page/index.ts b/src/renderer/src/pages/onboarding-page/index.ts new file mode 100644 index 0000000..1d109f4 --- /dev/null +++ b/src/renderer/src/pages/onboarding-page/index.ts @@ -0,0 +1 @@ +export { default } from './index.tsx'; diff --git a/src/renderer/src/features/onboarding/ui/OnboardingPage.tsx b/src/renderer/src/pages/onboarding-page/index.tsx similarity index 94% rename from src/renderer/src/features/onboarding/ui/OnboardingPage.tsx rename to src/renderer/src/pages/onboarding-page/index.tsx index 821b4e8..27553c6 100644 --- a/src/renderer/src/features/onboarding/ui/OnboardingPage.tsx +++ b/src/renderer/src/pages/onboarding-page/index.tsx @@ -1,4 +1,4 @@ -import CameraIcon from '@assets/camera.svg?react'; +import CameraIcon from '@assets/common/icons/camera.svg?react'; import CameraPermissionButton from './components/CameraPermissionButton'; const OnboardingPage = () => { diff --git a/src/renderer/src/pages/resend-verification-page/index.ts b/src/renderer/src/pages/resend-verification-page/index.ts new file mode 100644 index 0000000..1d109f4 --- /dev/null +++ b/src/renderer/src/pages/resend-verification-page/index.ts @@ -0,0 +1 @@ +export { default } from './index.tsx'; diff --git a/src/renderer/src/features/auth/ui/signup/ResendVerificationPage.tsx b/src/renderer/src/pages/resend-verification-page/index.tsx similarity index 83% rename from src/renderer/src/features/auth/ui/signup/ResendVerificationPage.tsx rename to src/renderer/src/pages/resend-verification-page/index.tsx index 96835b5..00a0f97 100644 --- a/src/renderer/src/features/auth/ui/signup/ResendVerificationPage.tsx +++ b/src/renderer/src/pages/resend-verification-page/index.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; -import ResendEmailHerosection from './components/ResendEmailHeroSection'; -import ResendSection from './components/ResendSection'; -import VerifyAction from './components/VerifyAction'; +import ResendEmailHerosection from '../signup-page/components/ResendEmailHeroSection'; +import ResendSection from '../signup-page/components/ResendSection'; +import VerifyAction from '../signup-page/components/VerifyAction'; import { useSearchParams } from 'react-router-dom'; import { useResendVerifyEmailMuation, @@ -21,7 +21,7 @@ const ResendVerificationPage = () => { if (token) { verifyEmailMutation.mutate(token); } - }, [searchParams]); + }, [searchParams, verifyEmailMutation]); /* 이메일 다시 보내기 */ const onResendClick = () => { diff --git a/src/renderer/src/pages/signup-page/components/EmailHeroSection.tsx b/src/renderer/src/pages/signup-page/components/EmailHeroSection.tsx new file mode 100644 index 0000000..eddb915 --- /dev/null +++ b/src/renderer/src/pages/signup-page/components/EmailHeroSection.tsx @@ -0,0 +1,26 @@ +import { useEmailStore } from '@entities/user'; +import EmailIcon from '@assets/auth/email_icon.svg?react'; + +export default function EmailHeroSection() { + const email = useEmailStore((state) => state.email); + + return ( +
+ +
+

이메일 인증

+

+ 본인 인증 메일을 귀하의 + + {' ' + email} + + 로 보냈습니다. +
+ 받은 메일함에서 인증 메일을 열고{' '} + 본인인증을 + 클릭하면 회원가입이 완료됩니다. +

+
+
+ ); +} diff --git a/src/renderer/src/pages/signup-page/components/ResendEmailHeroSection.tsx b/src/renderer/src/pages/signup-page/components/ResendEmailHeroSection.tsx new file mode 100644 index 0000000..9b3ab51 --- /dev/null +++ b/src/renderer/src/pages/signup-page/components/ResendEmailHeroSection.tsx @@ -0,0 +1,14 @@ +export default function ResendEmailHerosection() { + return ( +
+

+ 인증 링크를 메일로 전송했습니다 +

+

+ 이메일로 전송 받은 인증 링크를 확인해주세요. +
+ 링크는 발송 시점으로부터 24시간 동안 유효합니다. +

+
+ ); +} diff --git a/src/renderer/src/pages/signup-page/components/ResendSection.tsx b/src/renderer/src/pages/signup-page/components/ResendSection.tsx new file mode 100644 index 0000000..e495632 --- /dev/null +++ b/src/renderer/src/pages/signup-page/components/ResendSection.tsx @@ -0,0 +1,17 @@ +interface ResendSectionProps { + onClick: () => void; +} + +export default function ResendSection({ onClick }: ResendSectionProps) { + return ( +

+ 이메일을 못받으셨나요? + + 이메일 다시 보내기 + +

+ ); +} diff --git a/src/renderer/src/pages/signup-page/components/SignUpform.tsx b/src/renderer/src/pages/signup-page/components/SignUpform.tsx new file mode 100644 index 0000000..e7c8451 --- /dev/null +++ b/src/renderer/src/pages/signup-page/components/SignUpform.tsx @@ -0,0 +1,244 @@ +import FailIcon from '@assets/auth/error_icon.svg?react'; +import SuccessIcon from '@assets/auth/success_icon.svg?react'; +import { + useDuplicatedEmailMutation, + useEmailStore, + useSignupMutation, +} from '@entities/user'; +import PasswordField from '@features/auth/ui/login/components/PasswordField'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Button } from '@shared/ui/button'; +import { TextField } from '@shared/ui/input-field'; +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { SignUpFormData, signUpSchema } from '../utils/SignupSchemas'; + +const SignUpForm = () => { + const { mutate: checkDuplicateEmail } = useDuplicatedEmailMutation(); + const signupMutation = useSignupMutation(); + const [duplicateMessage, setDuplicateMessage] = useState(null); + const [duplicateSuccess, setDuplicateSuccess] = useState( + null, + ); + const { setEmail } = useEmailStore(); + const { + register, + handleSubmit, + getValues, + watch, + formState: { errors, isValid }, + } = useForm({ + resolver: zodResolver(signUpSchema), + mode: 'onChange', + defaultValues: { + email: '', + password: '', + confirmPassword: '', + name: '', + }, + }); + + const formValues = watch(); + + /* 이메일 중복 확인 */ + const handleDuplicateCheck = () => { + const email = getValues('email'); + if (!email) return; + + checkDuplicateEmail(email, { + onSuccess: (data) => { + if (data?.data?.isDuplicate) { + console.log(data); + setDuplicateSuccess(false); + setDuplicateMessage('이미 가입된 이메일입니다.'); + } else { + console.log(data); + setDuplicateSuccess(true); + setDuplicateMessage('사용 가능한 이메일입니다'); + } + }, + onError: () => { + setDuplicateSuccess(false); + setDuplicateMessage('이미 가입된 이메일입니다.'); + }, + }); + }; + + /* 회원가입 */ + const onSubmit = (data: SignUpFormData) => { + if (duplicateSuccess !== true) { + setDuplicateMessage('이메일 중복확인을 완료해주세요'); + setDuplicateSuccess(false); + return; + } + + setEmail(data.email); + + signupMutation.mutate({ + email: data.email, + password: data.password, + name: data.name, + avatar: '', + callbackUrl: '', + }); + }; + + return ( +
+ {/* 이메일 섹션 */} +
+ +
+ { + // 이메일 값이 바뀌면 중복확인 상태 초기화 + if (duplicateSuccess !== null) { + setDuplicateSuccess(null); + setDuplicateMessage(null); + } + }, + })} + className={`hbp:text-body-xl-regular aspect-[338/60] flex-1 ${errors.email + ? '!border-error' + : duplicateSuccess === true + ? '!border-success' + : duplicateSuccess === false + ? '!border-error' + : '' + }`} + /> +
+ {(errors.email || duplicateMessage) && ( +
+ {errors.email || duplicateSuccess === false ? ( + + ) : ( + + )} +

+ {errors.email?.message || duplicateMessage || ''} +

+
+ )} +
+ + {/* 비밀번호 섹션 */} +
+ +

+ 영문, 숫자, 특수문자를 조합하여 8-16글자로 입력해주세요. +

+ + + {/* 비밀번호 재입력 섹션 */} + + + { + /* 비밀번호 일치 여부 및 유효성 메시지 표시 */ + formValues.confirmPassword && + (() => { + /*비밀번호 불일치 또는 유효성 조건 미충족*/ + const isError = + formValues.password !== formValues.confirmPassword || + !!errors.password; + + const colorClass = isError ? 'text-error' : 'text-success'; + const Icon = isError ? FailIcon : SuccessIcon; + const message = isError + ? errors.password?.message || '비밀번호가 일치하지 않습니다.' + : '비밀번호가 일치합니다.'; + + return ( +
+ +

{message}

+
+ ); + })() + } +
+ + {/* 이름 섹션 */} +
+ +

+ 최대 10글자 이내로 작성해주세요. +

+ + + {(formValues.name || !!errors.name) && ( +
+ {errors.name ? : } +

+ {errors.name ? errors.name.message : '사용 가능한 이름입니다.'} +

+
+ )} +
+ + {/* 완료 버튼 */} +
+ ); +} diff --git a/src/renderer/src/pages/signup-page/index.ts b/src/renderer/src/pages/signup-page/index.ts new file mode 100644 index 0000000..1d109f4 --- /dev/null +++ b/src/renderer/src/pages/signup-page/index.ts @@ -0,0 +1 @@ +export { default } from './index.tsx'; diff --git a/src/renderer/src/features/auth/ui/signup/SignUpPage.tsx b/src/renderer/src/pages/signup-page/index.tsx similarity index 100% rename from src/renderer/src/features/auth/ui/signup/SignUpPage.tsx rename to src/renderer/src/pages/signup-page/index.tsx diff --git a/src/renderer/src/pages/signup-page/utils/SignupSchemas.ts b/src/renderer/src/pages/signup-page/utils/SignupSchemas.ts new file mode 100644 index 0000000..7ac38bd --- /dev/null +++ b/src/renderer/src/pages/signup-page/utils/SignupSchemas.ts @@ -0,0 +1,28 @@ +import { z } from 'zod'; + +export const signUpSchema = z + .object({ + email: z.string().email('유효한 이메일을 입력해주세요.'), + password: z + .string() + .min(8, '비밀번호는 8자 이상이어야 합니다.') + .max(16, '비밀번호는 16자 이하여야 합니다.') + .regex( + /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/, + '영문, 숫자, 특수문자를 조합해주세요.', + ), + confirmPassword: z.string(), + name: z + .string() + .min(1, '이름을 입력해주세요.') + .max(10, '최대 글자수를 초과했습니다.') + .refine((val) => !/\s/.test(val), { + message: '띄어쓰기 없이 붙여 작성해주세요.', + }), + }) + .refine((data) => data.password === data.confirmPassword, { + message: '비밀번호가 일치하지 않습니다.', + path: ['confirmPassword'], + }); + +export type SignUpFormData = z.infer; diff --git a/src/renderer/src/pages/widget-page/WidgetTitleBar/WidgetTitleBar.tsx b/src/renderer/src/pages/widget-page/WidgetTitleBar/WidgetTitleBar.tsx new file mode 100644 index 0000000..651b90c --- /dev/null +++ b/src/renderer/src/pages/widget-page/WidgetTitleBar/WidgetTitleBar.tsx @@ -0,0 +1,75 @@ +import MediumDragIcon from '@assets/widget/drag_icon.svg?react'; +import MiniDragIcon from '@assets/widget/mini_drag_icon.svg?react'; + +interface WidgetTitleBarProps { + onClose?: () => void; + isMini?: boolean; // 미니 모드 여부 +} + +/* 커스텀 위젯 타이틀바 (frame: false 일 때 사용) */ +export function WidgetTitleBar({ + onClose, + isMini = true, +}: WidgetTitleBarProps) { + // 위젯 닫기 + const handleClose = async () => { + try { + if (window.electronAPI?.widget) { + await window.electronAPI.widget.close(); + + // 위젯 닫힘 로그 저장 + if (window.electronAPI?.writeLog) { + try { + const logData = JSON.stringify({ + event: 'widget_closed', + timestamp: new Date().toISOString(), + }); + await window.electronAPI.writeLog(logData); + } catch (error) { + console.error('위젯 닫힘 로그 저장 실패:', error); + } + } + } + onClose?.(); + } catch (error) { + console.error('위젯 닫기 실패:', error); + } + }; + + return ( +
+ {/* 빨간 닫기 버튼 */} +
+ ); +} diff --git a/src/renderer/src/pages/widget-page/components/MediumWidgetContent.tsx b/src/renderer/src/pages/widget-page/components/MediumWidgetContent.tsx new file mode 100644 index 0000000..c2ec495 --- /dev/null +++ b/src/renderer/src/pages/widget-page/components/MediumWidgetContent.tsx @@ -0,0 +1,102 @@ +import { useEffect, useState } from 'react'; +import MediumGiraffe from '@assets/widget/medium_giraffe.svg?react'; +import MediumTurtle from '@assets/widget/medium_turtle.svg?react'; +import messages from '../data.json'; + +/* 실시간 자세 판별 */ +type PostureState = 0 | 1 | 2 | 3 | 4 | 5 | 6; + +interface Message { + level: number; + mainTitle: string; + subTitles: string[]; +} + +interface MediumWidgetContentProps { + posture: PostureState; +} + +/* 미디엄 위젯 레이아웃 */ +export function MediumWidgetContent({ posture }: MediumWidgetContentProps) { + const [mainTitle, setMainTitle] = useState('자세를 측정하고 있어요'); + const [subTitle, setSubTitle] = useState('잠시만 기다려주세요...'); + + /* eslint-disable react-hooks/set-state-in-effect */ + useEffect(() => { + const messageData = messages.find((m: Message) => m.level === posture); + + if (messageData) { + setMainTitle(messageData.mainTitle); + const { subTitles } = messageData; + const randomIndex = Math.floor(Math.random() * subTitles.length); + setSubTitle(subTitles[randomIndex]); + } else { + // posture가 0이거나 유효하지 않은 경우 기본 메시지 설정 + setMainTitle('자세를 측정하고 있어요'); + setSubTitle('잠시만 기다려주세요...'); + } + }, [posture]); + /* eslint-enable react-hooks/set-state-in-effect */ + + const isGiraffe = [1, 2, 3].includes(posture); + const gradient = isGiraffe + ? 'linear-gradient(180deg, var(--color-olive-green) 0.18%, var(--color-success) 99.7%)' + : 'linear-gradient(180deg, var(--color-coral-red) 0%, var(--color-error) 100%)'; + + /* 게이지 비율: 등급별 차등 적용 */ + let gaugeWidth: string; + switch (posture) { + case 1: + case 6: + gaugeWidth = '100%'; + break; + case 2: + case 5: + gaugeWidth = '75%'; + break; + case 3: + case 4: + gaugeWidth = '50%'; + break; + default: // posture 0 + gaugeWidth = '25%'; + break; + } + + return ( +
+ {/* 캐릭터 영역 */} +
+ {isGiraffe ? ( + + ) : ( + + )} +
+ + {/* 상세 정보 영역 */} +
+ {/* 진행 바 */} +
+
+
+
+
+
+
+ + {/* 메시지 */} +
+
{mainTitle}
+
{subTitle}
+
+
+
+ ); +} diff --git a/src/renderer/src/pages/widget-page/components/MiniWidgetContent.tsx b/src/renderer/src/pages/widget-page/components/MiniWidgetContent.tsx new file mode 100644 index 0000000..bddbb50 --- /dev/null +++ b/src/renderer/src/pages/widget-page/components/MiniWidgetContent.tsx @@ -0,0 +1,54 @@ +import MiniGiraffe from '@assets/widget/mini_giraffe.svg?react'; +import MiniTurtle from '@assets/widget/mini_turtle.svg?react'; + +/* 실시간 자세 판별 */ +type PostureState = 0 | 1 | 2 | 3 | 4 | 5 | 6; + +interface MiniWidgetContentProps { + posture: PostureState; +} + +/* 미니 위젯 레이아웃 - 최대 50px 높이에 맞게 가로 배치 */ +export function MiniWidgetContent({ posture }: MiniWidgetContentProps) { + const isGiraffe = [1, 2, 3].includes(posture); + const gradient = isGiraffe + ? 'linear-gradient(180deg, var(--color-olive-green) 0.18%, var(--color-success) 99.7%)' + : 'linear-gradient(180deg, var(--color-coral-red) 0%, var(--color-error) 100%)'; + + /* 게이지 비율: 등급별 차등 적용 */ + let gaugeWidth: string; + switch (posture) { + case 1: + case 6: + gaugeWidth = '100%'; + break; + case 2: + case 5: + gaugeWidth = '75%'; + break; + case 3: + case 4: + gaugeWidth = '50%'; + break; + default: // posture 0 + gaugeWidth = '25%'; + break; + } + + return ( +
+ {/* 캐릭터 이미지 영역 - 작게 */} +
+
+ {isGiraffe ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/src/renderer/src/pages/widget-page/data.json b/src/renderer/src/pages/widget-page/data.json new file mode 100644 index 0000000..c27f307 --- /dev/null +++ b/src/renderer/src/pages/widget-page/data.json @@ -0,0 +1,63 @@ +[ + { + "level": 6, + "mainTitle": "앗! 지금은 거북이 상태예요", + "subTitles": [ + "초등학교 3학년 아이를 얹고 있어요", + "시멘트 한 포대를 얹고 있어요", + "대형견 셰퍼드를 얹고 있어요", + "미래 병원비 3,000만원을 적립 중입니다", + "생산성 최악! 지금 당장 일어나세요!" + ] + }, + { + "level": 5, + "mainTitle": "앗! 지금은 거북이 상태예요", + "subTitles": [ + "골든 리트리버 한 마리를 얹고 있어요", + "42인치 TV를 얹고 있어요", + "자동차 타이어 1개를 얹고 있어요", + "목 디스크가 다가오고있어요", + "지금 느끼는 어깨 통증, 자세 때문이에요" + ] + }, + { + "level": 4, + "mainTitle": "앗! 지금은 거북이 상태예요", + "subTitles": [ + "7살 아이 한 명을 얹고 있어요", + "2L 생수병 9개 묶음을 얹고 있어요", + "비행기 기내용 캐리어를 얹고 있어요", + "본격적인 거북목 증후군 시작!", + "뇌로 가는 산소가 부족해지고 있어요" + ] + }, + { + "level": 3, + "mainTitle": "좋아요, 기린 상태 유지중!", + "subTitles": [ + "아직은 목 근육 긴장 가능성이 있어요", + "턱을 살짝만 더 당겨볼까요?", + "이 정도면 잘하고 있어요!" + ] + }, + { + "level": 2, + "mainTitle": "좋아요, 기린 상태 유지중!", + "subTitles": [ + "집중력 최고 상태예요", + "목과 어깨가 편안한 상태예요", + "생산성이 상승하고있어요" + ] + }, + { + "level": 1, + "mainTitle": "좋아요, 기린 상태 유지중!", + "subTitles": [ + "완벽해요! 최고의 퍼포먼스를 내고있어요", + "최고의 자세! 이 상태를 유지하세요", + "척추가 교과서처럼 정렬되었어요", + "이 자세, 몸이 기억하게 해주세요" + ] + } +] diff --git a/src/renderer/src/pages/widget-page/hooks/usePostureSyncWithLocalStorage.ts b/src/renderer/src/pages/widget-page/hooks/usePostureSyncWithLocalStorage.ts new file mode 100644 index 0000000..60729de --- /dev/null +++ b/src/renderer/src/pages/widget-page/hooks/usePostureSyncWithLocalStorage.ts @@ -0,0 +1,43 @@ +import { useEffect } from 'react'; +import { usePostureStore } from '@entities/posture'; + +/* 메인 창의 실시간 자세 상태 위젯 창에 실시간 동기화 + localStorage의 storage 이벤트를 통해 창 간 통신 */ + +export function usePostureSyncWithLocalStorage() { + const postureClass = usePostureStore((state) => state.postureClass); + const setStatus = usePostureStore((state) => state.setStatus); + + useEffect(() => { + const handleStorageChange = (e: StorageEvent) => { + /* posture-state-storage만 처리 (다른 localStorage 변경은 무시) */ + if (e.key !== 'posture-state-storage' || !e.newValue) return; + + try { + const storageData = JSON.parse(e.newValue); + const { postureClass: newPostureClass, score: newScore } = + storageData.state; + + console.log('[위젯] 메인 창에서 자세 변경 감지:', { + from: { postureClass }, + to: { postureClass: newPostureClass, score: newScore }, + }); + + setStatus(newPostureClass, newScore); + } catch (error) { + console.error('[위젯] localStorage 파싱 오류:', error); + } + }; + + window.addEventListener('storage', handleStorageChange); + return () => window.removeEventListener('storage', handleStorageChange); + }, [postureClass, setStatus]); + + /* 디버깅용 로그 */ + useEffect(() => { + console.log('[위젯] 자세 상태 업데이트:', { + postureClass, + timestamp: new Date().toLocaleTimeString(), + }); + }, [postureClass]); +} diff --git a/src/renderer/src/pages/widget-page/hooks/useThemeSync.ts b/src/renderer/src/pages/widget-page/hooks/useThemeSync.ts new file mode 100644 index 0000000..6043db7 --- /dev/null +++ b/src/renderer/src/pages/widget-page/hooks/useThemeSync.ts @@ -0,0 +1,28 @@ +import { useEffect } from 'react'; + +/* 메인 창의 테마 위젯 창에 동시에 반영 */ +export function useThemeSync() { + useEffect(() => { + const applyTheme = () => { + const isDark = localStorage.getItem('theme') === 'dark'; + if (isDark) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + }; + + // 초기 테마 적용 + applyTheme(); + + /*메인 창에서 테마 변경 시 자동 반영 */ + const handleStorageChange = (e: StorageEvent) => { + if (e.key === 'theme') { + applyTheme(); + } + }; + + window.addEventListener('storage', handleStorageChange); + return () => window.removeEventListener('storage', handleStorageChange); + }, []); +} diff --git a/src/renderer/src/pages/widget-page/index.ts b/src/renderer/src/pages/widget-page/index.ts new file mode 100644 index 0000000..1d109f4 --- /dev/null +++ b/src/renderer/src/pages/widget-page/index.ts @@ -0,0 +1 @@ +export { default } from './index.tsx'; diff --git a/src/renderer/src/widgets/widget/ui/WidgetPage.tsx b/src/renderer/src/pages/widget-page/index.tsx similarity index 98% rename from src/renderer/src/widgets/widget/ui/WidgetPage.tsx rename to src/renderer/src/pages/widget-page/index.tsx index d8cab89..4e61221 100644 --- a/src/renderer/src/widgets/widget/ui/WidgetPage.tsx +++ b/src/renderer/src/pages/widget-page/index.tsx @@ -15,7 +15,7 @@ const BREAKPOINT = { height: 62, } as const; -export function WidgetPage() { +const WidgetPage = () => { const [widgetSize, setWidgetSize] = useState('medium'); const currentPostureClass = usePostureStore((state) => state.postureClass); @@ -88,4 +88,6 @@ export function WidgetPage() {
); -} +}; + +export default WidgetPage; diff --git a/src/renderer/src/shared/api/index.ts b/src/renderer/src/shared/api/index.ts index f25205d..6e3e4dd 100644 --- a/src/renderer/src/shared/api/index.ts +++ b/src/renderer/src/shared/api/index.ts @@ -1,2 +1 @@ export { default } from './instance'; - diff --git a/src/renderer/src/shared/api/instance.ts b/src/renderer/src/shared/api/instance.ts index 3d805a0..8c93402 100644 --- a/src/renderer/src/shared/api/instance.ts +++ b/src/renderer/src/shared/api/instance.ts @@ -108,7 +108,7 @@ api.interceptors.response.use( originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`; } return api(originalRequest); - } catch (refreshError) { + } catch { // 리프레시 토큰도 만료되거나 유효하지 않은 경우 로그아웃 처리 localStorage.clear(); window.location.href = '/'; diff --git a/src/renderer/src/shared/config/router.tsx b/src/renderer/src/shared/config/router.tsx index 76dc037..75533de 100644 --- a/src/renderer/src/shared/config/router.tsx +++ b/src/renderer/src/shared/config/router.tsx @@ -1,93 +1,96 @@ -import { EmailVerificationCallbackPage, EmailVerificationPage, LoginPage, ResendVerificationPage, SignUpPage } from '@features/auth'; import api from '@shared/api'; import { createBrowserRouter, redirect } from 'react-router-dom'; -import Layout from '../../layout/Layout'; -import { CalibrationPage } from '@features/calibration'; -import MainPage from '../../pages/Main/MainPage'; +import Layout from '../../app/layouts/Layout'; import { - OnboardingPage, - OnboardingInitPage, + CalibrationPage, + EmailVerificationCallbackPage, + EmailVerificationPage, + LoginPage, + MainPage, OnboardingCompletionPage, -} from '@features/onboarding'; -import { WidgetPage } from '@widgets/widget'; + OnboardingInitPage, + OnboardingPage, + ResendVerificationPage, + SignUpPage, + WidgetPage, +} from '../../pages'; // 인증이 필요한 페이지용 loader const requireAuthLoader = async () => { - const accessToken = localStorage.getItem('accessToken'); - if (!accessToken) { - return redirect('/'); - } + const accessToken = localStorage.getItem('accessToken'); + if (!accessToken) { + return redirect('/'); + } - try { - await api.get('/users/me'); - return null; - } catch (error) { - localStorage.clear(); - return redirect('/'); - } + try { + await api.get('/users/me'); + return null; + } catch { + localStorage.clear(); + return redirect('/'); + } }; // 로그인 페이지용 loader (토큰이 있으면 메인으로 리다이렉트) const loginPageLoader = async () => { - const accessToken = localStorage.getItem('accessToken'); - if (!accessToken) { - return null; - } + const accessToken = localStorage.getItem('accessToken'); + if (!accessToken) { + return null; + } - try { - await api.get('/users/me'); - return redirect('/main'); - } catch (error) { - localStorage.clear(); - return null; - } + try { + await api.get('/users/me'); + return redirect('/main'); + } catch { + localStorage.clear(); + return null; + } }; export const router = createBrowserRouter([ - { - path: '/main', - loader: requireAuthLoader, - element: , - }, - { - element: , - path: '/auth', - children: [ - { - path: 'login', - loader: loginPageLoader, - element: , - }, - { path: 'signup', element: }, - { path: 'verify', element: }, - { path: 'verify-callback', element: }, - { path: 'resend', element: }, - ], - }, - { - element: , - path: '/', - children: [ - { - path: '', - loader: loginPageLoader, - element: , - }, - ], - }, - { - element: , - path: '/onboarding', - children: [ - { path: '', element: }, - { path: 'calibration', element: }, - { path: 'completion', element: }, - { path: 'init', element: }, - ], - }, - { - path: '/widget', - children: [{ path: '', element: }], - }, + { + path: '/main', + loader: requireAuthLoader, + element: , + }, + { + element: , + path: '/auth', + children: [ + { + path: 'login', + loader: loginPageLoader, + element: , + }, + { path: 'signup', element: }, + { path: 'verify', element: }, + { path: 'verify-callback', element: }, + { path: 'resend', element: }, + ], + }, + { + element: , + path: '/', + children: [ + { + path: '', + loader: loginPageLoader, + element: , + }, + ], + }, + { + element: , + path: '/onboarding', + children: [ + { path: '', element: }, + { path: 'calibration', element: }, + { path: 'completion', element: }, + { path: 'init', element: }, + ], + }, + { + path: '/widget', + children: [{ path: '', element: }], + }, ]); - diff --git a/src/renderer/src/shared/lib/index.ts b/src/renderer/src/shared/lib/index.ts index 84fdce4..252ff51 100644 --- a/src/renderer/src/shared/lib/index.ts +++ b/src/renderer/src/shared/lib/index.ts @@ -2,4 +2,3 @@ export { cn } from './cn'; export { getColor } from './get-color'; export { getScoreLevel, getAllLevelDefinitions } from './get-score-level'; export type { ScoreLevel, ScoreLevelInfo } from './get-score-level'; - diff --git a/src/renderer/src/styles/base.css b/src/renderer/src/shared/styles/base.css similarity index 100% rename from src/renderer/src/styles/base.css rename to src/renderer/src/shared/styles/base.css diff --git a/src/renderer/src/styles/breakpoint.css b/src/renderer/src/shared/styles/breakpoint.css similarity index 100% rename from src/renderer/src/styles/breakpoint.css rename to src/renderer/src/shared/styles/breakpoint.css diff --git a/src/renderer/src/styles/colors.css b/src/renderer/src/shared/styles/colors.css similarity index 100% rename from src/renderer/src/styles/colors.css rename to src/renderer/src/shared/styles/colors.css diff --git a/src/renderer/src/styles/fonts.css b/src/renderer/src/shared/styles/fonts.css similarity index 100% rename from src/renderer/src/styles/fonts.css rename to src/renderer/src/shared/styles/fonts.css diff --git a/src/renderer/src/styles/globals.css b/src/renderer/src/shared/styles/globals.css similarity index 100% rename from src/renderer/src/styles/globals.css rename to src/renderer/src/shared/styles/globals.css diff --git a/src/renderer/src/styles/typography.css b/src/renderer/src/shared/styles/typography.css similarity index 100% rename from src/renderer/src/styles/typography.css rename to src/renderer/src/shared/styles/typography.css diff --git a/src/renderer/src/shared/types/svg.d.ts b/src/renderer/src/shared/types/svg.d.ts index 1b1baf4..28dd978 100644 --- a/src/renderer/src/shared/types/svg.d.ts +++ b/src/renderer/src/shared/types/svg.d.ts @@ -5,4 +5,3 @@ declare module '*.svg?react' { >; export default ReactComponent; } - diff --git a/src/renderer/src/shared/types/vite-env.d.ts b/src/renderer/src/shared/types/vite-env.d.ts index 8827bbe..2ecdea7 100644 --- a/src/renderer/src/shared/types/vite-env.d.ts +++ b/src/renderer/src/shared/types/vite-env.d.ts @@ -1,3 +1,3 @@ /// /// - +/// diff --git a/src/renderer/src/shared/ui/button/Button.tsx b/src/renderer/src/shared/ui/button/Button.tsx index ce7001b..e08b948 100644 --- a/src/renderer/src/shared/ui/button/Button.tsx +++ b/src/renderer/src/shared/ui/button/Button.tsx @@ -1,54 +1,28 @@ -import { cva, type VariantProps } from 'class-variance-authority'; +import { type VariantProps } from 'class-variance-authority'; import * as React from 'react'; import { cn } from '@shared/lib/cn'; - -const buttonVariants = cva( - 'inline-flex items-center justify-center rounded-full transition-colors focus-visible:outline-none disabled:cursor-not-allowed active:scale-95', - { - variants: { - variant: { - primary: - 'bg-yellow-400 text-grey-1000 hover:bg-yellow-500 active:bg-yellow-600 disabled:bg-yellow-100 disabled:text-grey-0 cursor-pointer', - sub: 'bg-yellow-50 text-yellow-500 hover:bg-yellow-100 active:bg-yellow-200 active:text-yellow-600 cursor-pointer', - grey: 'bg-grey-25 text-grey-500 hover:bg-grey-50 active:bg-grey-100 active:text-grey-300 cursor-pointer disabled:bg-grey-25 disabled:text-grey-100', - }, - - size: { - xs: 'h-[33px] px-3 text-caption-sm-medium', - sm: 'h-10 px-4 text-body-md-medium', - md: 'h-10 px-5 text-body-md-medium', - lg: 'h-[51px] px-6 text-body-lg-medium', - xl: 'h-[59px] px-7 text-body-lg-medium ', - }, - }, - defaultVariants: { - variant: 'primary', - size: 'md', - }, - }, -); +import { buttonVariants } from './buttonVariants'; export interface ButtonProps - extends React.ButtonHTMLAttributes, + extends React.ButtonHTMLAttributes, VariantProps { - text?: React.ReactNode; + text?: React.ReactNode; } const Button = React.forwardRef( - ({ className, variant, size, text, ...props }, ref) => { - return ( - - ); - }, + ({ className, variant, size, text, ...props }, ref) => { + return ( + + ); + }, ); Button.displayName = 'Button'; -export { Button, buttonVariants }; - +export { Button }; diff --git a/src/renderer/src/shared/ui/button/buttonVariants.ts b/src/renderer/src/shared/ui/button/buttonVariants.ts new file mode 100644 index 0000000..7ed0e02 --- /dev/null +++ b/src/renderer/src/shared/ui/button/buttonVariants.ts @@ -0,0 +1,28 @@ +import { cva } from 'class-variance-authority'; + +export const buttonVariants = cva( + 'inline-flex items-center justify-center rounded-full transition-colors focus-visible:outline-none disabled:cursor-not-allowed active:scale-95', + { + variants: { + variant: { + primary: + 'bg-yellow-400 text-grey-1000 hover:bg-yellow-500 active:bg-yellow-600 disabled:bg-yellow-100 disabled:text-grey-0 cursor-pointer', + sub: 'bg-yellow-50 text-yellow-500 hover:bg-yellow-100 active:bg-yellow-200 active:text-yellow-600 cursor-pointer', + grey: 'bg-grey-25 text-grey-500 hover:bg-grey-50 active:bg-grey-100 active:text-grey-300 cursor-pointer disabled:bg-grey-25 disabled:text-grey-100', + }, + + size: { + xs: 'h-[33px] px-3 text-caption-sm-medium', + sm: 'h-10 px-4 text-body-md-medium', + md: 'h-10 px-5 text-body-md-medium', + lg: 'h-[51px] px-6 text-body-lg-medium', + xl: 'h-[59px] px-7 text-body-lg-medium ', + }, + }, + defaultVariants: { + variant: 'primary', + size: 'md', + }, + }, +); + diff --git a/src/renderer/src/shared/ui/button/index.ts b/src/renderer/src/shared/ui/button/index.ts index 83c53d5..6045153 100644 --- a/src/renderer/src/shared/ui/button/index.ts +++ b/src/renderer/src/shared/ui/button/index.ts @@ -1,3 +1,3 @@ -export { Button, buttonVariants } from './Button'; +export { Button } from './Button'; +export { buttonVariants } from './buttonVariants'; export type { ButtonProps } from './Button'; - diff --git a/src/renderer/src/shared/ui/input-field/TextField.tsx b/src/renderer/src/shared/ui/input-field/TextField.tsx index 7018731..672762d 100644 --- a/src/renderer/src/shared/ui/input-field/TextField.tsx +++ b/src/renderer/src/shared/ui/input-field/TextField.tsx @@ -53,4 +53,3 @@ const TextField = forwardRef( TextField.displayName = 'TextField'; export default TextField; - diff --git a/src/renderer/src/shared/ui/input-field/index.ts b/src/renderer/src/shared/ui/input-field/index.ts index f931a66..09648fa 100644 --- a/src/renderer/src/shared/ui/input-field/index.ts +++ b/src/renderer/src/shared/ui/input-field/index.ts @@ -1,3 +1,2 @@ export { default as TextField } from './TextField'; export { default } from './TextField'; - diff --git a/src/renderer/src/shared/ui/intensity-slider/IntensitySlider.tsx b/src/renderer/src/shared/ui/intensity-slider/IntensitySlider.tsx index 8304aca..4b2256a 100644 --- a/src/renderer/src/shared/ui/intensity-slider/IntensitySlider.tsx +++ b/src/renderer/src/shared/ui/intensity-slider/IntensitySlider.tsx @@ -38,4 +38,3 @@ const IntensitySlider = React.forwardRef( IntensitySlider.displayName = 'IntensitySlider'; export { IntensitySlider }; - diff --git a/src/renderer/src/shared/ui/intensity-slider/index.ts b/src/renderer/src/shared/ui/intensity-slider/index.ts index 10f6fc0..a760c60 100644 --- a/src/renderer/src/shared/ui/intensity-slider/index.ts +++ b/src/renderer/src/shared/ui/intensity-slider/index.ts @@ -1,2 +1 @@ export { IntensitySlider } from './IntensitySlider'; - diff --git a/src/renderer/src/shared/ui/modal/ModalPortal.ts b/src/renderer/src/shared/ui/modal/ModalPortal.ts index 000c972..71a2858 100644 --- a/src/renderer/src/shared/ui/modal/ModalPortal.ts +++ b/src/renderer/src/shared/ui/modal/ModalPortal.ts @@ -13,4 +13,3 @@ export const ModalPortal = ({ children }: ModalPortalProps) => { } return ReactDOM.createPortal(children, el); }; - diff --git a/src/renderer/src/shared/ui/modal/index.ts b/src/renderer/src/shared/ui/modal/index.ts index fcdf064..1f69e7e 100644 --- a/src/renderer/src/shared/ui/modal/index.ts +++ b/src/renderer/src/shared/ui/modal/index.ts @@ -1,2 +1 @@ export { ModalPortal } from './ModalPortal'; - diff --git a/src/renderer/src/shared/ui/notification-message/NotificateMessage.tsx b/src/renderer/src/shared/ui/notification-message/NotificateMessage.tsx index 6fc2ff0..746c252 100644 --- a/src/renderer/src/shared/ui/notification-message/NotificateMessage.tsx +++ b/src/renderer/src/shared/ui/notification-message/NotificateMessage.tsx @@ -2,11 +2,6 @@ import { cva, type VariantProps } from 'class-variance-authority'; import * as React from 'react'; import { ErrorIcon, SuccessIcon } from './icons'; -/** 작은 clsx 유틸 */ -function cn(...a: Array) { - return a.filter(Boolean).join(' '); -} - const notification = cva( 'w-[544px] p-[18px] text-body-md-regular transition-all duration-200 ease-in-out rounded-full', { @@ -91,4 +86,3 @@ export function NotificateMessage({ } export default NotificateMessage; - diff --git a/src/renderer/src/shared/ui/notification-message/icons.tsx b/src/renderer/src/shared/ui/notification-message/icons.tsx index 8ada8a9..99a51cf 100644 --- a/src/renderer/src/shared/ui/notification-message/icons.tsx +++ b/src/renderer/src/shared/ui/notification-message/icons.tsx @@ -53,4 +53,3 @@ export function ErrorIcon({ className }: { className?: string }) { ); } - diff --git a/src/renderer/src/shared/ui/notification-message/index.ts b/src/renderer/src/shared/ui/notification-message/index.ts index 7931c67..122341b 100644 --- a/src/renderer/src/shared/ui/notification-message/index.ts +++ b/src/renderer/src/shared/ui/notification-message/index.ts @@ -1,3 +1,2 @@ export { ErrorIcon, SuccessIcon } from './icons'; export { default, NotificateMessage } from './NotificateMessage'; - diff --git a/src/renderer/src/shared/ui/page-move-button/PageMoveButton.tsx b/src/renderer/src/shared/ui/page-move-button/PageMoveButton.tsx index 17ac0a5..a40737c 100644 --- a/src/renderer/src/shared/ui/page-move-button/PageMoveButton.tsx +++ b/src/renderer/src/shared/ui/page-move-button/PageMoveButton.tsx @@ -1,4 +1,4 @@ -import PageMoveIcon from '@assets/page-move-button.svg?react'; +import PageMoveIcon from '@assets/common/icons/page-move-button.svg?react'; import * as React from 'react'; import { cn } from '@shared/lib/cn'; @@ -42,4 +42,3 @@ const PageMoveButton = React.forwardRef( PageMoveButton.displayName = 'PageMoveButton'; export { PageMoveButton }; - diff --git a/src/renderer/src/shared/ui/page-move-button/index.ts b/src/renderer/src/shared/ui/page-move-button/index.ts index ebb05bb..d81cf94 100644 --- a/src/renderer/src/shared/ui/page-move-button/index.ts +++ b/src/renderer/src/shared/ui/page-move-button/index.ts @@ -1,2 +1 @@ export { PageMoveButton } from './PageMoveButton'; - diff --git a/src/renderer/src/shared/ui/panel-header/PannelHeader.tsx b/src/renderer/src/shared/ui/panel-header/PannelHeader.tsx index e2333c5..8661467 100644 --- a/src/renderer/src/shared/ui/panel-header/PannelHeader.tsx +++ b/src/renderer/src/shared/ui/panel-header/PannelHeader.tsx @@ -1,4 +1,4 @@ -import InfoIcon from '@assets/info-circle.svg?react'; +import InfoIcon from '@assets/common/icons/info-circle.svg?react'; import * as React from 'react'; interface PannelHeaderProps { @@ -21,4 +21,3 @@ const PannelHeader = React.forwardRef( PannelHeader.displayName = 'PannelHeader'; export { PannelHeader }; - diff --git a/src/renderer/src/shared/ui/panel-header/index.ts b/src/renderer/src/shared/ui/panel-header/index.ts index ad68825..3524dbb 100644 --- a/src/renderer/src/shared/ui/panel-header/index.ts +++ b/src/renderer/src/shared/ui/panel-header/index.ts @@ -1,2 +1 @@ export { PannelHeader } from './PannelHeader'; - diff --git a/src/renderer/src/shared/ui/theme-toggle-switch/ThemeToggleSwitch.tsx b/src/renderer/src/shared/ui/theme-toggle-switch/ThemeToggleSwitch.tsx index e48a8b9..d3136d0 100644 --- a/src/renderer/src/shared/ui/theme-toggle-switch/ThemeToggleSwitch.tsx +++ b/src/renderer/src/shared/ui/theme-toggle-switch/ThemeToggleSwitch.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import MoonIcon from '@assets/moon_icon.svg?react'; -import SunIcon from '@assets/sun_icon.svg?react'; +import MoonIcon from '@assets/common/icons/moon_icon.svg?react'; +import SunIcon from '@assets/common/icons/sun_icon.svg?react'; import { cn } from '@shared/lib/cn'; interface ThemeToggleSwitchProps { @@ -43,4 +43,3 @@ const ThemeToggleSwitch = React.forwardRef< ThemeToggleSwitch.displayName = 'ThemeToggleSwitch'; export { ThemeToggleSwitch }; - diff --git a/src/renderer/src/shared/ui/theme-toggle-switch/index.ts b/src/renderer/src/shared/ui/theme-toggle-switch/index.ts index 03e98fc..f2f3d92 100644 --- a/src/renderer/src/shared/ui/theme-toggle-switch/index.ts +++ b/src/renderer/src/shared/ui/theme-toggle-switch/index.ts @@ -1,2 +1 @@ export { ThemeToggleSwitch } from './ThemeToggleSwitch'; - diff --git a/src/renderer/src/shared/ui/timer/Timer.tsx b/src/renderer/src/shared/ui/timer/Timer.tsx index c26c14e..70bab23 100644 --- a/src/renderer/src/shared/ui/timer/Timer.tsx +++ b/src/renderer/src/shared/ui/timer/Timer.tsx @@ -7,8 +7,7 @@ type Props = { off?: string; // 비활성 색 (기본 grey-50) }; -const SEG_KEYS = ['LT', 'TOP', 'LB', 'RT', 'RB'] as const; -type SegmentKey = (typeof SEG_KEYS)[number]; +type SegmentKey = 'LT' | 'TOP' | 'LB' | 'RT' | 'RB'; // Tailwind 기준 (CSS 변수 사용해 다크/라이트 테마 자동 대응) const DEFAULT_ON = 'var(--color-yellow-500)'; @@ -116,4 +115,3 @@ const Timer = function Timer({ export { Timer }; export default Timer; - diff --git a/src/renderer/src/shared/ui/timer/index.ts b/src/renderer/src/shared/ui/timer/index.ts index d7e5d43..de4a30c 100644 --- a/src/renderer/src/shared/ui/timer/index.ts +++ b/src/renderer/src/shared/ui/timer/index.ts @@ -1,3 +1,2 @@ export { Timer } from './Timer'; export { default } from './Timer'; - diff --git a/src/renderer/src/shared/ui/toggle-switch/NotificationToggleSwitch.tsx b/src/renderer/src/shared/ui/toggle-switch/NotificationToggleSwitch.tsx index 4124e3d..ac7796d 100644 --- a/src/renderer/src/shared/ui/toggle-switch/NotificationToggleSwitch.tsx +++ b/src/renderer/src/shared/ui/toggle-switch/NotificationToggleSwitch.tsx @@ -39,4 +39,3 @@ const NotificationToggleSwitch = React.forwardRef< NotificationToggleSwitch.displayName = 'NotificationToggleSwitch'; export { NotificationToggleSwitch }; - diff --git a/src/renderer/src/shared/ui/toggle-switch/ToggleSwitch.tsx b/src/renderer/src/shared/ui/toggle-switch/ToggleSwitch.tsx index 8828a31..32db2d5 100644 --- a/src/renderer/src/shared/ui/toggle-switch/ToggleSwitch.tsx +++ b/src/renderer/src/shared/ui/toggle-switch/ToggleSwitch.tsx @@ -88,4 +88,3 @@ const ToggleSwitch = React.forwardRef( ToggleSwitch.displayName = 'ToggleSwitch'; export { ToggleSwitch }; - diff --git a/src/renderer/src/shared/ui/toggle-switch/index.ts b/src/renderer/src/shared/ui/toggle-switch/index.ts index 292ed0c..476e1d4 100644 --- a/src/renderer/src/shared/ui/toggle-switch/index.ts +++ b/src/renderer/src/shared/ui/toggle-switch/index.ts @@ -1,3 +1,2 @@ export { ToggleSwitch } from './ToggleSwitch'; export { NotificationToggleSwitch } from './NotificationToggleSwitch'; - diff --git a/src/renderer/src/shared/ui/typography/Typography.tsx b/src/renderer/src/shared/ui/typography/Typography.tsx index b5d71b1..a14f9be 100644 --- a/src/renderer/src/shared/ui/typography/Typography.tsx +++ b/src/renderer/src/shared/ui/typography/Typography.tsx @@ -63,4 +63,3 @@ export function Typography({ ); } - diff --git a/src/renderer/src/shared/ui/typography/index.ts b/src/renderer/src/shared/ui/typography/index.ts index 41c5bc3..5cbb1cc 100644 --- a/src/renderer/src/shared/ui/typography/index.ts +++ b/src/renderer/src/shared/ui/typography/index.ts @@ -1,2 +1 @@ export { Typography } from './Typography'; - diff --git a/src/renderer/src/widgets/camera/index.ts b/src/renderer/src/widgets/camera/index.ts index 450f009..9f8ccad 100644 --- a/src/renderer/src/widgets/camera/index.ts +++ b/src/renderer/src/widgets/camera/index.ts @@ -1,2 +1 @@ export * from './model'; - diff --git a/src/renderer/src/widgets/widget/index.ts b/src/renderer/src/widgets/widget/index.ts index 6a63040..f41a696 100644 --- a/src/renderer/src/widgets/widget/index.ts +++ b/src/renderer/src/widgets/widget/index.ts @@ -1,3 +1 @@ -export * from './ui'; export * from './lib'; - diff --git a/src/renderer/src/widgets/widget/lib/index.ts b/src/renderer/src/widgets/widget/lib/index.ts index 946de13..0e86f1c 100644 --- a/src/renderer/src/widgets/widget/lib/index.ts +++ b/src/renderer/src/widgets/widget/lib/index.ts @@ -1,2 +1 @@ export { useWidget } from './useWidget'; - diff --git a/src/renderer/src/widgets/widget/ui/index.ts b/src/renderer/src/widgets/widget/ui/index.ts deleted file mode 100644 index 5184557..0000000 --- a/src/renderer/src/widgets/widget/ui/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { WidgetPage } from './WidgetPage'; -export { WidgetTitleBar } from './WidgetTitleBar/WidgetTitleBar'; diff --git a/src/renderer/tsconfig.json b/src/renderer/tsconfig.json index e224f15..889a2bc 100644 --- a/src/renderer/tsconfig.json +++ b/src/renderer/tsconfig.json @@ -20,16 +20,16 @@ "jsx": "react-jsx", "allowImportingTsExtensions": true, "noEmit": true, - "baseUrl": ".", + "baseUrl": "./src", "paths": { - "@ui/*": ["./src/components/*"], - "@assets/*": ["./src/assets/*"], - "@api/*": ["./src/api/*"], - "@utils/*": ["./src/utils/*"], - "@shared/*": ["./src/shared/*"], - "@entities/*": ["./src/entities/*"], - "@features/*": ["./src/features/*"], - "@widgets/*": ["./src/widgets/*"] + "@ui/*": ["./components/*"], + "@assets/*": ["./assets/*"], + "@api/*": ["./api/*"], + "@utils/*": ["./utils/*"], + "@shared/*": ["./shared/*"], + "@entities/*": ["./entities/*"], + "@features/*": ["./features/*"], + "@widgets/*": ["./widgets/*"] } }, "include": [