From 9851627ff15547bbbd4c06806dd82e3d92f52070 Mon Sep 17 00:00:00 2001 From: hansoo Date: Sat, 12 Jul 2025 15:27:54 +0900 Subject: [PATCH] feat: Add CI/CD pipeline and code quality improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add GitHub Actions workflow for automated CI/CD - Configure Node.js 18.x and 20.x matrix testing - Add TypeScript type checking step - Add ESLint code quality checks with enhanced rules - Add Prettier formatting verification - Add production build validation - Upload build artifacts for deployment - Set up automated testing on push/PR - Replace console.log with environment-aware logger - Add pre-commit hooks for code quality - Exclude archive folder from linting ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .cursor/mcp.json | 1 + .env | 3 +- .env.example | 42 +- .env.production | 13 + .github/workflows/ci.yml | 48 ++ .github/workflows/type-check.yml | 57 ++ .gitignore | 1 + .husky/pre-commit | 35 + .mcp.json | 20 +- .prettierignore | 28 + .prettierrc | 15 + .taskmaster/config.json | 74 +- .taskmaster/state.json | 2 +- .taskmaster/tasks/tasks.json | 574 ++++++++++--- .taskmaster_backup/config.json | 20 + .taskmaster_backup/state.json | 6 + .../tasks/task_001.txt | 2 +- .../tasks/task_002.txt | 2 +- .../tasks/task_003.txt | 2 +- .../tasks/task_004.txt | 2 +- .../tasks/task_005.txt | 2 +- .../tasks/task_006.txt | 2 +- .../tasks/task_007.txt | 2 +- .../tasks/task_008.txt | 2 +- .../tasks/task_009.txt | 2 +- .../tasks/task_010.txt | 2 +- .taskmaster_backup/tasks/tasks.json | 328 +++++++ .taskmaster_backup/templates/example_prd.txt | 47 ++ PROJECT_IMPROVEMENT_PLAN.md | 54 +- README.md | 26 + capacitor.config.ts | 24 +- debug-mcp.sh | 42 + docs/TYPE_SYSTEM_GUIDE.md | 368 ++++++++ docs/TYPE_SYSTEM_QUICK_REFERENCE.md | 267 ++++++ eslint.config.js | 41 +- index.html | 3 +- package-lock.json | 798 +++++++++++++++--- package.json | 22 +- postcss.config.js | 2 +- replace-console-logs.cjs | 183 ++++ src/App.tsx | 129 +-- src/android-icon-guide.md | 6 +- src/app-deployment-guide.md | 18 +- src/app-features-guide.md | 15 +- .../components/SupabaseConnectionStatus.tsx | 2 +- .../migration/SupabaseToAppwriteMigration.tsx | 17 +- .../supabase/DebugInfoCollapsible.tsx | 2 +- .../components/supabase/ErrorMessageCard.tsx | 2 +- .../supabase/ProxyRecommendationAlert.tsx | 2 +- .../supabase/TroubleshootingTips.tsx | 2 +- .../hooks/transactions/supabaseUtils.ts | 27 +- src/archive/integrations/supabase/client.ts | 4 +- .../lib/appwrite/migrateFromSupabase.ts | 9 +- src/archive/lib/supabase/client.ts | 7 +- src/archive/lib/supabase/config.ts | 4 +- src/archive/lib/supabase/customFetch.ts | 11 +- src/archive/lib/supabase/setup/index.ts | 5 +- src/archive/lib/supabase/setup/status.ts | 3 +- src/archive/lib/supabase/setup/tables.ts | 13 +- src/archive/lib/supabase/storageUtils.ts | 4 +- src/archive/lib/supabase/tests/apiTests.ts | 3 +- src/archive/lib/supabase/tests/authTests.ts | 25 +- src/archive/utils/supabaseTransactionUtils.ts | 19 +- src/components/AddTransactionButton.tsx | 158 ++-- src/components/AppVersionInfo.tsx | 38 +- src/components/AvatarImageView.tsx | 56 +- src/components/BudgetCard.tsx | 57 +- src/components/BudgetCategoriesSection.tsx | 156 ++-- src/components/BudgetInputCard.tsx | 92 +- src/components/BudgetProgress.tsx | 21 +- src/components/BudgetProgressCard.tsx | 56 +- src/components/BudgetTabContent.tsx | 51 +- src/components/CategoryBudgetInputs.tsx | 58 +- src/components/ExpenseChart.tsx | 11 +- src/components/Header.tsx | 59 +- src/components/NativeImage.tsx | 31 +- src/components/NavBar.tsx | 58 +- src/components/RecentTransactionsSection.tsx | 46 +- src/components/ResourceImage.tsx | 13 +- src/components/SafeAreaContainer.tsx | 15 +- src/components/SimpleAvatar.tsx | 37 +- src/components/SyncSettings.tsx | 39 +- src/components/TransactionCard.tsx | 36 +- src/components/TransactionEditDialog.tsx | 50 +- src/components/ZellyAvatar.tsx | 26 +- .../analytics/CategorySpendingList.tsx | 37 +- .../analytics/MonthlyComparisonChart.tsx | 138 ++- .../analytics/PaymentMethodChart.tsx | 34 +- src/components/analytics/PeriodSelector.tsx | 23 +- src/components/analytics/SummaryCards.tsx | 88 +- .../auth/AppwriteConnectionStatus.tsx | 32 +- .../auth/AppwriteConnectionTest.tsx | 88 +- src/components/auth/EmailConfirmation.tsx | 51 +- src/components/auth/LoginForm.tsx | 123 ++- src/components/auth/LoginLink.tsx | 6 +- src/components/auth/PrivateRoute.tsx | 11 +- src/components/auth/RegisterErrorDisplay.tsx | 9 +- src/components/auth/RegisterForm.tsx | 158 ++-- src/components/auth/RegisterFormFields.tsx | 45 +- src/components/auth/RegisterHeader.tsx | 9 +- src/components/auth/ServerStatusAlert.tsx | 14 +- src/components/auth/TestConnectionSection.tsx | 26 +- src/components/auth/types.ts | 15 +- src/components/budget/BudgetDialog.tsx | 56 +- src/components/budget/BudgetHeader.tsx | 9 +- src/components/budget/BudgetInputButton.tsx | 23 +- src/components/budget/BudgetInputForm.tsx | 3 +- src/components/budget/BudgetProgressBar.tsx | 9 +- src/components/budget/BudgetStatusDisplay.tsx | 12 +- .../expenses/ExpenseAmountInput.tsx | 31 +- .../expenses/ExpenseCategorySelector.tsx | 27 +- src/components/expenses/ExpenseForm.tsx | 46 +- src/components/expenses/ExpenseFormFields.tsx | 68 +- .../expenses/ExpensePaymentMethod.tsx | 53 +- .../expenses/ExpenseSubmitActions.tsx | 25 +- src/components/expenses/ExpenseTitleInput.tsx | 21 +- .../expenses/ExpenseTitleSuggestions.tsx | 15 +- src/components/home/EmptyState.tsx | 9 +- src/components/home/HomeContent.tsx | 36 +- src/components/home/IndexContent.tsx | 25 +- .../notification/NotificationPopover.tsx | 74 +- src/components/onboarding/WelcomeDialog.tsx | 84 +- src/components/profile/PasswordChangeForm.tsx | 147 ++-- src/components/profile/ProfileForm.tsx | 60 +- src/components/profile/ProfileHeader.tsx | 21 +- .../RecentTransactionItem.tsx | 15 +- src/components/security/DataResetDialog.tsx | 62 +- src/components/security/DataResetSection.tsx | 25 +- .../security/SaveSettingsButton.tsx | 15 +- src/components/security/SecurityHeader.tsx | 19 +- .../security/SecuritySettingItem.tsx | 26 +- .../security/SecuritySettingsList.tsx | 22 +- src/components/security/types.ts | 3 +- src/components/sync/SyncExplanation.tsx | 12 +- src/components/sync/SyncStatus.tsx | 38 +- .../transaction/TransactionAmount.tsx | 5 +- .../transaction/TransactionAmountInput.tsx | 33 +- .../TransactionCategorySelector.tsx | 49 +- .../transaction/TransactionDeleteAlert.tsx | 31 +- .../transaction/TransactionDetails.tsx | 17 +- .../transaction/TransactionEditForm.tsx | 31 +- .../transaction/TransactionFormFields.tsx | 66 +- .../transaction/TransactionIcon.tsx | 9 +- .../transaction/TransactionPaymentMethod.tsx | 51 +- .../transaction/TransactionTitleInput.tsx | 21 +- .../TransactionTitleSuggestions.tsx | 40 +- src/components/transaction/categoryUtils.ts | 25 +- .../transaction/useTransactionEdit.ts | 80 +- .../transactions/EmptyTransactions.tsx | 13 +- .../transactions/TransactionDateGroup.tsx | 45 +- .../transactions/TransactionsContent.tsx | 25 +- .../transactions/TransactionsHeader.tsx | 66 +- .../transactions/TransactionsList.tsx | 15 +- src/components/ui/accordion.tsx | 24 +- src/components/ui/alert-dialog.tsx | 49 +- src/components/ui/alert.tsx | 22 +- src/components/ui/aspect-ratio.tsx | 6 +- src/components/ui/avatar.tsx | 20 +- src/components/ui/badge.tsx | 12 +- src/components/ui/breadcrumb.tsx | 46 +- src/components/ui/button.tsx | 22 +- src/components/ui/card.tsx | 37 +- src/components/ui/carousel.tsx | 142 ++-- src/components/ui/chart.tsx | 140 +-- src/components/ui/checkbox.tsx | 14 +- src/components/ui/collapsible.tsx | 10 +- src/components/ui/command.tsx | 52 +- src/components/ui/context-menu.tsx | 66 +- src/components/ui/dialog.tsx | 43 +- src/components/ui/drawer.tsx | 43 +- src/components/ui/dropdown-menu.tsx | 66 +- src/components/ui/form.tsx | 97 +-- src/components/ui/hover-card.tsx | 16 +- src/components/ui/input-otp.tsx | 32 +- src/components/ui/input.tsx | 13 +- src/components/ui/label.tsx | 16 +- src/components/ui/menubar.tsx | 72 +- src/components/ui/navigation-menu.tsx | 42 +- src/components/ui/pagination.tsx | 42 +- src/components/ui/popover.tsx | 17 +- src/components/ui/progress.tsx | 12 +- src/components/ui/radio-group.tsx | 23 +- src/components/ui/resizable.tsx | 16 +- src/components/ui/scroll-area.tsx | 16 +- src/components/ui/select.tsx | 44 +- src/components/ui/separator.tsx | 12 +- src/components/ui/sheet.tsx | 61 +- src/components/ui/sidebar.tsx | 294 +++---- src/components/ui/skeleton.tsx | 6 +- src/components/ui/slider.tsx | 12 +- src/components/ui/sonner.tsx | 14 +- src/components/ui/switch.tsx | 12 +- src/components/ui/table.tsx | 38 +- src/components/ui/tabs.tsx | 23 +- src/components/ui/textarea.tsx | 12 +- src/components/ui/toast.tsx | 47 +- src/components/ui/toaster.tsx | 11 +- src/components/ui/toggle-group.tsx | 26 +- src/components/ui/toggle.tsx | 16 +- src/components/ui/tooltip.tsx | 18 +- src/components/ui/use-toast.ts | 1 - src/constants/categoryIcons.tsx | 56 +- src/contexts/AuthContext.tsx | 15 +- src/contexts/auth/AuthContext.tsx | 9 +- src/contexts/auth/AuthProvider.tsx | 165 ++-- src/contexts/auth/auth.utils.ts | 3 +- src/contexts/auth/authActions.ts | 9 +- src/contexts/auth/index.ts | 7 +- src/contexts/auth/resetPassword.ts | 59 +- src/contexts/auth/signIn.ts | 77 +- src/contexts/auth/signInUtils.ts | 57 +- src/contexts/auth/signOut.ts | 59 +- src/contexts/auth/signUp.ts | 105 ++- src/contexts/auth/signUpApiCalls.ts | 65 +- src/contexts/auth/signUpErrorHandlers.ts | 63 +- src/contexts/auth/signUpUtils.ts | 115 +-- src/contexts/auth/types.ts | 45 +- src/contexts/auth/useAuth.ts | 9 +- src/contexts/budget/BudgetContext.tsx | 21 +- src/contexts/budget/budgetUtils.ts | 135 +-- src/contexts/budget/hooks/useBudgetBackup.ts | 45 +- .../budget/hooks/useBudgetDataEvents.ts | 139 +-- .../budget/hooks/useBudgetDataLoad.ts | 30 +- .../budget/hooks/useBudgetDataState.ts | 41 +- .../budget/hooks/useBudgetGoalUpdate.ts | 134 +-- src/contexts/budget/hooks/useBudgetReset.ts | 36 +- src/contexts/budget/hooks/useBudgetState.ts | 14 +- .../budget/hooks/useCategoryBudgetState.ts | 127 +-- .../budget/hooks/useCategorySpending.ts | 9 +- .../budget/hooks/useExtendedBudgetUpdate.ts | 28 +- .../budget/hooks/useTransactionState.ts | 64 +- src/contexts/budget/index.ts | 35 +- src/contexts/budget/storage/budgetStorage.ts | 104 ++- .../budget/storage/categoryStorage.ts | 183 ++-- src/contexts/budget/storage/index.ts | 17 +- src/contexts/budget/storage/resetStorage.ts | 146 ++-- .../budget/storage/transactionStorage.ts | 151 ++-- src/contexts/budget/storageUtils.ts | 11 +- src/contexts/budget/types.ts | 24 +- src/contexts/budget/useBudget.ts | 9 +- src/contexts/budget/useBudgetState.ts | 87 +- .../budget/utils/budgetCalculation.ts | 54 +- src/contexts/budget/utils/categoryUtils.ts | 31 +- src/contexts/budget/utils/constants.ts | 13 +- .../budget/utils/spendingCalculation.ts | 132 +-- src/contexts/budget/utils/storageUtils.ts | 42 +- src/hooks/auth/useAppwriteAuth.ts | 164 ++-- src/hooks/budget/useBudgetTabContent.ts | 95 ++- src/hooks/sync/index.ts | 17 +- src/hooks/sync/syncBackupUtils.ts | 54 +- src/hooks/sync/syncNetworkChecker.ts | 29 +- src/hooks/sync/syncPerformer.ts | 38 +- src/hooks/sync/syncResultHandler.ts | 61 +- src/hooks/sync/syncTime/index.ts | 5 +- src/hooks/sync/syncTime/useSyncTimeEvents.ts | 57 +- .../sync/syncTime/useSyncTimeFormatting.ts | 37 +- src/hooks/sync/useManualSync.ts | 62 +- src/hooks/sync/useSyncStatus.ts | 30 +- src/hooks/sync/useSyncToggle.ts | 110 +-- src/hooks/toast/constants.ts | 5 +- src/hooks/toast/index.ts | 49 +- src/hooks/toast/reducer.ts | 41 +- src/hooks/toast/store.ts | 7 +- src/hooks/toast/toastManager.ts | 54 +- src/hooks/toast/types.ts | 47 +- src/hooks/transactions/dateUtils.ts | 54 +- src/hooks/transactions/deleteTransaction.ts | 159 ++-- .../transactions/filterOperations/index.ts | 60 +- .../transactions/filterOperations/types.ts | 3 +- .../filterOperations/useFilterApplication.ts | 91 +- .../filterOperations/useMonthSelection.ts | 20 +- .../filterOperations/useTotalCalculation.ts | 7 +- src/hooks/transactions/filterUtils.ts | 112 ++- src/hooks/transactions/index.ts | 16 +- src/hooks/transactions/storageUtils.ts | 96 ++- .../transactionOperations/index.ts | 52 +- .../transactionOperations/types.ts | 3 +- .../updateTransaction.ts | 186 ++-- .../useTransactionsOperations.ts | 52 +- .../transactions/useAppwriteTransactions.ts | 218 ++--- src/hooks/transactions/useDeleteAlert.ts | 36 +- .../transactions/useRecentTransactions.ts | 194 ++--- .../useRecentTransactionsDialog.ts | 10 +- src/hooks/transactions/useTransactions.ts | 3 +- src/hooks/transactions/useTransactionsCore.ts | 63 +- .../transactions/useTransactionsEvents.ts | 88 +- .../transactions/useTransactionsFiltering.ts | 3 +- .../transactions/useTransactionsLoader.ts | 38 +- .../transactions/useTransactionsOperations.ts | 22 +- .../transactions/useTransactionsState.ts | 31 +- src/hooks/use-mobile.tsx | 49 +- src/hooks/use-toast.ts | 1 - src/hooks/useAppFocusEvents.tsx | 68 +- src/hooks/useDataInitialization.ts | 138 +-- src/hooks/useDataReset.ts | 159 ++-- src/hooks/useInitialDataLoading.tsx | 63 +- src/hooks/useLogin.ts | 52 +- src/hooks/useNotifications.ts | 44 +- src/hooks/useSyncSettings.ts | 20 +- src/hooks/useTableSetup.ts | 17 +- src/hooks/useToast.wrapper.ts | 63 +- src/hooks/useTransactions.ts | 9 +- src/hooks/useWelcomeDialog.ts | 63 +- src/hooks/useWelcomeNotification.tsx | 24 +- src/index.css | 89 +- src/ios-icon-guide.md | 5 +- src/lib/appwrite/client.ts | 66 +- src/lib/appwrite/config.ts | 38 +- src/lib/appwrite/defaultUser.ts | 8 +- src/lib/appwrite/index.ts | 29 +- src/lib/appwrite/setup.ts | 107 +-- src/lib/fullMigrate.js | 45 +- src/lib/migrateData.js | 125 +-- src/lib/migrateData.ts | 45 +- src/lib/utils.ts | 6 +- src/main.tsx | 64 +- src/next-steps-plan.md | 16 + src/pages/Analytics.tsx | 176 ++-- src/pages/AppwriteSettingsPage.tsx | 161 ++-- src/pages/ForgotPassword.tsx | 66 +- src/pages/HelpSupport.tsx | 146 ++-- src/pages/Index.tsx | 125 +-- src/pages/Login.tsx | 44 +- src/pages/NotFound.tsx | 23 +- src/pages/NotificationSettings.tsx | 84 +- src/pages/PaymentMethods.tsx | 43 +- src/pages/ProfileManagement.tsx | 9 +- src/pages/Register.tsx | 63 +- src/pages/SecurityPrivacySettings.tsx | 43 +- src/pages/Settings.tsx | 158 ++-- src/pages/Transactions.tsx | 130 +-- src/plugins/build-info.ts | 27 +- src/plugins/imagePlugin.ts | 17 +- src/plugins/index.ts | 7 +- src/test-appwrite-user.ts | 67 +- src/test-appwrite.ts | 72 +- src/types/common.ts | 196 +++++ src/types/guards.ts | 470 +++++++++++ src/types/index.ts | 93 ++ src/types/utils.ts | 495 +++++++++++ src/utils/appwriteTransactionUtils.ts | 203 +++-- src/utils/auth/handleNetworkError.ts | 29 +- src/utils/auth/index.ts | 17 +- src/utils/auth/login/errorHandlers.ts | 38 +- src/utils/auth/login/index.ts | 5 +- src/utils/auth/login/toastHandlers.ts | 9 +- src/utils/auth/loginUtils.ts | 5 +- src/utils/auth/network/compatUtils.ts | 1 - src/utils/auth/network/connectionVerifier.ts | 55 +- src/utils/auth/network/enhancedVerifier.ts | 43 +- src/utils/auth/network/index.ts | 11 +- src/utils/auth/network/networkUtils.ts | 13 +- src/utils/auth/networkUtils.ts | 7 +- src/utils/auth/responseUtils.ts | 75 +- src/utils/auth/toastUtils.ts | 17 +- src/utils/auth/validationUtils.ts | 17 +- src/utils/categoryBudgetUtils.ts | 76 +- src/utils/categoryColorUtils.ts | 29 +- src/utils/currencyFormatter.ts | 15 +- src/utils/dateParser.ts | 64 +- src/utils/dateUtils.ts | 5 +- src/utils/formatters.ts | 13 +- src/utils/imageUtils.ts | 22 +- src/utils/logger.ts | 100 +++ src/utils/network/checker.ts | 54 +- src/utils/network/index.ts | 31 +- src/utils/network/monitor.ts | 144 ++-- src/utils/network/queue.ts | 97 ++- src/utils/network/retry.ts | 29 +- src/utils/network/status.ts | 25 +- src/utils/network/types.ts | 6 +- src/utils/networkUtils.ts | 342 ++++---- src/utils/platform.ts | 97 ++- src/utils/storageUtils.ts | 94 ++- src/utils/sync/budget/downloadBudget.ts | 250 +++--- src/utils/sync/budget/index.ts | 15 +- .../sync/budget/modifiedBudgetsTracker.ts | 79 +- src/utils/sync/budget/types.ts | 1 - src/utils/sync/budget/uploadBudget.ts | 74 +- .../sync/budget/uploadCategoryBudgets.ts | 101 ++- src/utils/sync/budget/uploadMonthlyBudget.ts | 96 ++- src/utils/sync/budget/validators.ts | 50 +- src/utils/sync/budgetSync.ts | 8 +- src/utils/sync/clearCloudData.ts | 79 +- src/utils/sync/config.ts | 41 +- src/utils/sync/core/backup.ts | 27 +- src/utils/sync/core/index.ts | 11 +- src/utils/sync/core/recovery.ts | 19 +- src/utils/sync/core/status.ts | 59 +- src/utils/sync/core/syncOperations.ts | 69 +- src/utils/sync/core/types.ts | 1 - src/utils/sync/data.ts | 114 +-- src/utils/sync/downloadBudget.ts | 187 ++-- src/utils/sync/downloadTransaction.ts | 83 +- src/utils/sync/syncSettings.ts | 53 +- src/utils/sync/time.ts | 57 +- src/utils/sync/transaction/dateUtils.ts | 56 +- .../sync/transaction/deleteTransaction.ts | 71 +- .../transaction/deletedTransactionsTracker.ts | 37 +- .../sync/transaction/downloadTransaction.ts | 218 +++-- src/utils/sync/transaction/index.ts | 7 +- .../sync/transaction/uploadTransaction.ts | 224 +++-- src/utils/sync/transactionSync.ts | 67 +- src/utils/syncUtils.ts | 70 +- src/utils/transactionUtils.ts | 23 +- src/utils/userTitlePreferences.ts | 86 +- tailwind.config.ts | 280 +++--- task-master-watch.sh | 70 ++ tsconfig.app.json | 10 +- tsconfig.json | 8 +- vite.config.ts | 10 +- 411 files changed, 14458 insertions(+), 8680 deletions(-) create mode 100644 .cursor/mcp.json create mode 100644 .env.production create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/type-check.yml create mode 100755 .husky/pre-commit create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 .taskmaster_backup/config.json create mode 100644 .taskmaster_backup/state.json rename {.taskmaster => .taskmaster_backup}/tasks/task_001.txt (97%) rename {.taskmaster => .taskmaster_backup}/tasks/task_002.txt (97%) rename {.taskmaster => .taskmaster_backup}/tasks/task_003.txt (97%) rename {.taskmaster => .taskmaster_backup}/tasks/task_004.txt (97%) rename {.taskmaster => .taskmaster_backup}/tasks/task_005.txt (97%) rename {.taskmaster => .taskmaster_backup}/tasks/task_006.txt (97%) rename {.taskmaster => .taskmaster_backup}/tasks/task_007.txt (97%) rename {.taskmaster => .taskmaster_backup}/tasks/task_008.txt (97%) rename {.taskmaster => .taskmaster_backup}/tasks/task_009.txt (97%) rename {.taskmaster => .taskmaster_backup}/tasks/task_010.txt (97%) create mode 100644 .taskmaster_backup/tasks/tasks.json create mode 100644 .taskmaster_backup/templates/example_prd.txt create mode 100755 debug-mcp.sh create mode 100644 docs/TYPE_SYSTEM_GUIDE.md create mode 100644 docs/TYPE_SYSTEM_QUICK_REFERENCE.md create mode 100644 replace-console-logs.cjs create mode 100644 src/types/common.ts create mode 100644 src/types/guards.ts create mode 100644 src/types/index.ts create mode 100644 src/types/utils.ts create mode 100644 src/utils/logger.ts create mode 100644 task-master-watch.sh diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 0000000..356e9d9 --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1 @@ +{"mcpServers":{"MCP_DOCKER":{"command":"docker","args":["mcp","gateway","run"]}}} \ No newline at end of file diff --git a/.env b/.env index 69bccd9..e79123d 100644 --- a/.env +++ b/.env @@ -16,6 +16,7 @@ VITE_APPWRITE_ENDPOINT=https://a11.ism.kr/v1 VITE_APPWRITE_PROJECT_ID=68182a300039f6d700a6 VITE_APPWRITE_DATABASE_ID=default VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID=transactions -VITE_APPWRITE_API_KEY=standard_9672cc2d052d4fc56d9d28e75c6476ff1029d932b7d375dbb4bb0f705d741d8e6d9ae154929009e01c7168810884b6ee80e6bb564d3fe6439b8b142ed4a8d287546bb0bed2531c20188a7ecc36e6f9983abb1ab0022c1656cf2219d4c2799655c7baef00ae4861fe74186dbb421141d9e2332f2fad812975ae7b4b7f57527cea +# VITE_APPWRITE_API_KEY - ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœ๋˜๋ฏ€๋กœ ์ œ๊ฑฐ +# API ํ‚ค๋Š” ์„œ๋ฒ„ ์‚ฌ์ด๋“œ์—์„œ๋งŒ ์‚ฌ์šฉํ•˜๋„๋ก ์ด๋™ VITE_DISABLE_LOVABLE_BANNER=true diff --git a/.env.example b/.env.example index 2c5babf..7a5be5a 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,32 @@ -# API Keys (Required to enable respective provider) -ANTHROPIC_API_KEY="your_anthropic_api_key_here" # Required: Format: sk-ant-api03-... -PERPLEXITY_API_KEY="your_perplexity_api_key_here" # Optional: Format: pplx-... -OPENAI_API_KEY="your_openai_api_key_here" # Optional, for OpenAI/OpenRouter models. Format: sk-proj-... -GOOGLE_API_KEY="your_google_api_key_here" # Optional, for Google Gemini models. -MISTRAL_API_KEY="your_mistral_key_here" # Optional, for Mistral AI models. -XAI_API_KEY="YOUR_XAI_KEY_HERE" # Optional, for xAI AI models. -AZURE_OPENAI_API_KEY="your_azure_key_here" # Optional, for Azure OpenAI models (requires endpoint in .taskmaster/config.json). -OLLAMA_API_KEY="your_ollama_api_key_here" # Optional: For remote Ollama servers that require authentication. -GITHUB_API_KEY="your_github_api_key_here" # Optional: For GitHub import/export features. Format: ghp_... or github_pat_... \ No newline at end of file +# Supabase ๊ด€๋ จ ์„ค์ • (์ด์ „ ๋ฒ„์ „) +CLOUD_DATABASE_URL=postgresql://postgres:your_password@your_supabase_host:5432/postgres +ONPREM_DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres + +VITE_SUPABASE_URL=http://localhost:9000 +VITE_SUPABASE_ANON_KEY=your_supabase_anon_key_here +CLOUD_SUPABASE_URL=https://your_supabase_project.supabase.co +CLOUD_SUPABASE_ANON_KEY=your_cloud_supabase_anon_key_here +CLOUD_SUPABASE_SERVICE_ROLE_KEY=your_cloud_supabase_service_role_key_here +ONPREM_SUPABASE_URL=http://localhost:9000 +ONPREM_SUPABASE_ANON_KEY=your_onprem_supabase_anon_key_here +ONPREM_SUPABASE_SERVICE_ROLE_KEY=your_onprem_supabase_service_role_key_here + +# Appwrite ๊ด€๋ จ ์„ค์ • +VITE_APPWRITE_ENDPOINT=https://your_appwrite_endpoint/v1 +VITE_APPWRITE_PROJECT_ID=your_project_id +VITE_APPWRITE_DATABASE_ID=default +VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID=transactions +VITE_APPWRITE_API_KEY=your_appwrite_api_key_here + +VITE_DISABLE_LOVABLE_BANNER=true + +# Task Master AI API Keys +ANTHROPIC_API_KEY="your_anthropic_api_key_here" +PERPLEXITY_API_KEY="your_perplexity_api_key_here" +OPENAI_API_KEY="your_openai_api_key_here" +GOOGLE_API_KEY="your_google_api_key_here" +MISTRAL_API_KEY="your_mistral_key_here" +XAI_API_KEY="YOUR_XAI_KEY_HERE" +AZURE_OPENAI_API_KEY="your_azure_key_here" +OLLAMA_API_KEY="your_ollama_api_key_here" +GITHUB_API_KEY="your_github_api_key_here" \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..05d425a --- /dev/null +++ b/.env.production @@ -0,0 +1,13 @@ +# ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ ์„ค์ • +# ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ ์„ค์ •ํ•˜๊ณ  ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœํ•˜์ง€ ์•Š์Œ + +# Appwrite ๊ด€๋ จ ์„ค์ • (ํ”„๋กœ๋•์…˜) +VITE_APPWRITE_ENDPOINT=https://a11.ism.kr/v1 +VITE_APPWRITE_PROJECT_ID=68182a300039f6d700a6 +VITE_APPWRITE_DATABASE_ID=default +VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID=transactions + +VITE_DISABLE_LOVABLE_BANNER=true + +# ์ฃผ์˜: API ํ‚ค๋Š” ํ”„๋กœ๋•์…˜์—์„œ ์„œ๋ฒ„ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌ +# ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœ๋˜์–ด์„œ๋Š” ์•ˆ๋˜๋Š” ๋ฏผ๊ฐํ•œ ์ •๋ณด๋“ค์€ ์ œ์™ธ๋จ \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..016a48f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI + +on: + push: + branches: [main, master, develop] + pull_request: + branches: [main, master, develop] + +jobs: + ci: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Type check + run: npm run type-check + + - name: Lint check + run: npm run lint + + - name: Format check + run: npm run format:check + + - name: Build + run: npm run build + + - name: Upload build artifacts + if: matrix.node-version == '20.x' + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: dist/ + retention-days: 7 diff --git a/.github/workflows/type-check.yml b/.github/workflows/type-check.yml new file mode 100644 index 0000000..967ab11 --- /dev/null +++ b/.github/workflows/type-check.yml @@ -0,0 +1,57 @@ +name: TypeScript Type Check + +on: + push: + branches: [main, master, develop] + pull_request: + branches: [main, master, develop] + +jobs: + type-check: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Run TypeScript type check + run: npm run type-check + + - name: Run ESLint + run: npm run lint + + - name: Check build + run: npm run build + + - name: Upload build artifacts + if: success() + uses: actions/upload-artifact@v4 + with: + name: build-artifacts-node-${{ matrix.node-version }} + path: dist/ + retention-days: 7 + + - name: Comment type check results + if: failure() && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'โŒ TypeScript ํƒ€์ž… ๊ฒ€์‚ฌ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.' + }) diff --git a/.gitignore b/.gitignore index fc420a3..c40973e 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ dev-debug.log node_modules/ # Environment variables .env +.env.local .vscode # OS specific diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..1afc29e --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,35 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +# Pre-commit ์ฒดํฌ๋ฆฌ์ŠคํŠธ +echo "๐Ÿ” Pre-commit ๊ฒ€์‚ฌ ์‹œ์ž‘..." + +# 1. ์ฝ”๋“œ ํฌ๋งทํŒ… ๊ฒ€์‚ฌ +echo "๐Ÿ“ ์ฝ”๋“œ ํฌ๋งทํŒ… ๊ฒ€์‚ฌ ์ค‘..." +npm run format:check +if [ $? -ne 0 ]; then + echo "โŒ ์ฝ”๋“œ ํฌ๋งทํŒ… ์˜ค๋ฅ˜ ๋ฐœ๊ฒฌ. 'npm run format'์œผ๋กœ ์ˆ˜์ • ํ›„ ๋‹ค์‹œ ์ปค๋ฐ‹ํ•˜์„ธ์š”." + exit 1 +fi + +# 2. ๋ฆฐํŒ… ๋ฐ ํฌ๋งทํŒ… ์ž๋™ ์ˆ˜์ • +echo "๐Ÿ”ง Lint-staged ์‹คํ–‰ ์ค‘..." +npx lint-staged + +# 3. ํƒ€์ž… ๊ฒ€์‚ฌ +echo "๐ŸŽฏ TypeScript ํƒ€์ž… ๊ฒ€์‚ฌ ์ค‘..." +npm run type-check +if [ $? -ne 0 ]; then + echo "โŒ TypeScript ํƒ€์ž… ์˜ค๋ฅ˜ ๋ฐœ๊ฒฌ. ์ˆ˜์ • ํ›„ ๋‹ค์‹œ ์ปค๋ฐ‹ํ•˜์„ธ์š”." + exit 1 +fi + +# 4. ๋นŒ๋“œ ํ…Œ์ŠคํŠธ (์„ ํƒ์ ) +echo "๐Ÿ”จ ๋นŒ๋“œ ํ…Œ์ŠคํŠธ ์ค‘..." +npm run build +if [ $? -ne 0 ]; then + echo "โŒ ๋นŒ๋“œ ์‹คํŒจ. ์ฝ”๋“œ๋ฅผ ํ™•์ธํ•˜๊ณ  ๋‹ค์‹œ ์ปค๋ฐ‹ํ•˜์„ธ์š”." + exit 1 +fi + +echo "โœ… ๋ชจ๋“  pre-commit ๊ฒ€์‚ฌ ํ†ต๊ณผ!" diff --git a/.mcp.json b/.mcp.json index a5a4734..2e1b325 100644 --- a/.mcp.json +++ b/.mcp.json @@ -4,16 +4,16 @@ "command": "npx", "args": ["-y", "--package=task-master-ai", "task-master-ai"], "env": { - "ANTHROPIC_API_KEY": "${ANTHROPIC_API_KEY}", - "PERPLEXITY_API_KEY": "${PERPLEXITY_API_KEY}", - "OPENAI_API_KEY": "${OPENAI_API_KEY}", - "GOOGLE_API_KEY": "${GOOGLE_API_KEY}", - "XAI_API_KEY": "${XAI_API_KEY}", - "OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}", - "MISTRAL_API_KEY": "${MISTRAL_API_KEY}", - "AZURE_OPENAI_API_KEY": "${AZURE_OPENAI_API_KEY}", - "OLLAMA_API_KEY": "${OLLAMA_API_KEY}" + "ANTHROPIC_API_KEY": "ANTHROPIC_API_KEY_HERE", + "PERPLEXITY_API_KEY": "PERPLEXITY_API_KEY_HERE", + "OPENAI_API_KEY": "OPENAI_API_KEY_HERE", + "GOOGLE_API_KEY": "GOOGLE_API_KEY_HERE", + "XAI_API_KEY": "XAI_API_KEY_HERE", + "OPENROUTER_API_KEY": "OPENROUTER_API_KEY_HERE", + "MISTRAL_API_KEY": "MISTRAL_API_KEY_HERE", + "AZURE_OPENAI_API_KEY": "AZURE_OPENAI_API_KEY_HERE", + "OLLAMA_API_KEY": "OLLAMA_API_KEY_HERE" } } } -} \ No newline at end of file +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..fb8330e --- /dev/null +++ b/.prettierignore @@ -0,0 +1,28 @@ +dist/ +build/ +node_modules/ +coverage/ +*.min.js +*.min.css +android/ +ios/ +capacitor/ +.next/ +.vscode/ +.idea/ +.DS_Store +.env +.env.local +.env.production +*.log +package-lock.json +pnpm-lock.yaml +yarn.lock +CHANGELOG.md +archive/ +.claude/ +.cursor/ +.taskmaster/ +.taskmaster_backup/ +docs/ +src/archive/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..fd76578 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,15 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always", + "endOfLine": "lf", + "quoteProps": "as-needed", + "jsxSingleQuote": false, + "embeddedLanguageFormatting": "auto" +} diff --git a/.taskmaster/config.json b/.taskmaster/config.json index 11d857d..1006c5e 100644 --- a/.taskmaster/config.json +++ b/.taskmaster/config.json @@ -1,82 +1,12 @@ { "models": { "main": { - "0": "c", - "1": "l", - "2": "a", - "3": "u", - "4": "d", - "5": "e", - "6": "-", - "7": "3", - "8": "-", - "9": "5", - "10": "-", - "11": "s", - "12": "o", - "13": "n", - "14": "n", - "15": "e", - "16": "t", - "17": "-", - "18": "2", - "19": "0", - "20": "2", - "21": "4", - "22": "1", - "23": "0", - "24": "2", - "25": "2", "provider": "claude-code", "modelId": "sonnet", "maxTokens": 64000, "temperature": 0.2 }, "research": { - "0": "p", - "1": "e", - "2": "r", - "3": "p", - "4": "l", - "5": "e", - "6": "x", - "7": "i", - "8": "t", - "9": "y", - "10": "-", - "11": "l", - "12": "l", - "13": "a", - "14": "m", - "15": "a", - "16": "-", - "17": "3", - "18": ".", - "19": "1", - "20": "-", - "21": "s", - "22": "o", - "23": "n", - "24": "a", - "25": "r", - "26": "-", - "27": "l", - "28": "a", - "29": "r", - "30": "g", - "31": "e", - "32": "-", - "33": "1", - "34": "2", - "35": "8", - "36": "k", - "37": "-", - "38": "o", - "39": "n", - "40": "l", - "41": "i", - "42": "n", - "43": "e", "provider": "gemini-cli", "modelId": "gemini-2.5-pro", "maxTokens": 65536, @@ -95,10 +25,12 @@ "defaultNumTasks": 10, "defaultSubtasks": 5, "defaultPriority": "medium", - "projectName": "Task Master", + "projectName": "Taskmaster", "ollamaBaseURL": "http://localhost:11434/api", "bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com", "responseLanguage": "Korean", + "defaultTag": "master", + "azureOpenaiBaseURL": "https://your-endpoint.openai.azure.com/", "userId": "1234567890" }, "claudeCode": {} diff --git a/.taskmaster/state.json b/.taskmaster/state.json index 2bd7ae2..753841a 100644 --- a/.taskmaster/state.json +++ b/.taskmaster/state.json @@ -1,6 +1,6 @@ { "currentTag": "master", - "lastSwitched": "2025-07-11T20:57:32.202Z", + "lastSwitched": "2025-07-12T02:39:59.380Z", "branchTagMapping": {}, "migrationNoticeShown": true } \ No newline at end of file diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index 49805f1..4a209e4 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -1,122 +1,4 @@ { - "tasks": [ - { - "id": 1, - "title": "TypeScript ์„ค์ • ๊ฐ•ํ™” ๋ฐ ํƒ€์ž… ์•ˆ์ „์„ฑ ํ™•๋ณด", - "description": "tsconfig.json์˜ strict ๋ชจ๋“œ๋ฅผ ์ ์ง„์ ์œผ๋กœ ํ™œ์„ฑํ™”ํ•˜๊ณ  ๊ธฐ์กด any ํƒ€์ž… ์‚ฌ์šฉ์„ ์ œ๊ฑฐํ•˜์—ฌ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ํ™•๋ณดํ•ฉ๋‹ˆ๋‹ค.", - "details": "1. tsconfig.json์—์„œ strict: true, noImplicitAny: true, strictNullChecks: true ํ™œ์„ฑํ™” 2. ๊ธฐ์กด ์ฝ”๋“œ์—์„œ any ํƒ€์ž… ์‚ฌ์šฉ ๋ถ€๋ถ„ ์ฐพ์•„์„œ ์ ์ ˆํ•œ ํƒ€์ž…์œผ๋กœ ๋ณ€๊ฒฝ 3. ํƒ€์ž… ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋‹จ๊ณ„์ ์œผ๋กœ ์ˆ˜์ • 4. ์ปดํฌ๋„ŒํŠธ props์™€ state์— ๋Œ€ํ•œ ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜ 5. API ์‘๋‹ต ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ํƒ€์ž… ์ •์˜ ์ถ”๊ฐ€", - "testStrategy": "TypeScript ์ปดํŒŒ์ผ๋Ÿฌ ์˜ค๋ฅ˜ 0๊ฐœ ๋‹ฌ์„ฑ, tsc --noEmit ๋ช…๋ น์–ด๋กœ ํƒ€์ž… ๊ฒ€์‚ฌ ํ†ต๊ณผ ํ™•์ธ, IDE์—์„œ ํƒ€์ž… ์ถ”๋ก ์ด ์ •ํ™•ํžˆ ์ž‘๋™ํ•˜๋Š”์ง€ ๊ฒ€์ฆ", - "priority": "high", - "dependencies": [], - "status": "pending", - "subtasks": [] - }, - { - "id": 2, - "title": "์ฝ”๋“œ ํ’ˆ์งˆ ๊ฐœ์„  ๋ฐ ๋ฆฐํŒ… ์„ค์ •", - "description": "console.log ์ œ๊ฑฐ, ๋นŒ๋“œ ์˜ค๋ฅ˜ ์ˆ˜์ •, ESLint/Prettier ์„ค์ •์„ ํ†ตํ•ด ์ฝ”๋“œ ํ’ˆ์งˆ์„ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.", - "details": "1. ํ”„๋กœ์ ํŠธ ์ „์ฒด์—์„œ console.log 81๊ฐœ ์ œ๊ฑฐ (production์—์„œ๋Š” ์‚ญ์ œ, development์—์„œ๋Š” logger ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ) 2. SupabaseToAppwriteMigration import ์˜ค๋ฅ˜ ์ˆ˜์ • 3. ESLint ๊ทœ์น™ ๊ฐ•ํ™” (@typescript-eslint/recommended, react-hooks/recommended ์ถ”๊ฐ€) 4. Prettier ์„ค์ • ์ถ”๊ฐ€ (.prettierrc, .prettierignore ํŒŒ์ผ ์ƒ์„ฑ) 5. pre-commit hook ์„ค์ •์œผ๋กœ ์ž๋™ ํฌ๋งทํŒ…", - "testStrategy": "ESLint ์˜ค๋ฅ˜ 0๊ฐœ, Prettier ํฌ๋งทํŒ… ์ž๋™ ์ ์šฉ ํ™•์ธ, ๋นŒ๋“œ ์„ฑ๊ณต ํ™•์ธ, ๋ถˆํ•„์š”ํ•œ console.log๊ฐ€ production ๋นŒ๋“œ์— ํฌํ•จ๋˜์ง€ ์•Š๋Š”์ง€ ๊ฒ€์ฆ", - "priority": "high", - "dependencies": [1], - "status": "pending", - "subtasks": [] - }, - { - "id": 3, - "title": "ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ณด์•ˆ ๊ฐ•ํ™” ๋ฐ ๊ด€๋ฆฌ ๊ฐœ์„ ", - "description": "API ํ‚ค์˜ ํด๋ผ์ด์–ธํŠธ ๋…ธ์ถœ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ  ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ๋ฅผ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.", - "details": "1. ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœ๋˜์ง€ ๋ง์•„์•ผ ํ•  API ํ‚ค๋“ค์„ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ๋กœ ์ด๋™ 2. .env.example ํŒŒ์ผ ์ƒ์„ฑ์œผ๋กœ ํ•„์š”ํ•œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ฌธ์„œํ™” 3. VITE_๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋งŒ ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœ๋˜๋„๋ก ์ •๋ฆฌ 4. ๋ฏผ๊ฐํ•œ API ํ‚ค๋Š” ์„œ๋ฒ„๋ฆฌ์Šค ํ•จ์ˆ˜๋‚˜ ๋ฐฑ์—”๋“œ์—์„œ๋งŒ ์‚ฌ์šฉ 5. ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ํŒŒ์ผ ๋ถ„๋ฆฌ (.env.local, .env.production)", - "testStrategy": "๋นŒ๋“œ๋œ ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ์—์„œ ๋ฏผ๊ฐํ•œ API ํ‚ค๊ฐ€ ๋…ธ์ถœ๋˜์ง€ ์•Š๋Š”์ง€ ํ™•์ธ, ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋กœ๋“œ๋˜๋Š”์ง€ ๊ฐ ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธ", - "priority": "high", - "dependencies": [], - "status": "pending", - "subtasks": [] - }, - { - "id": 4, - "title": "CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•", - "description": "GitHub Actions๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž๋™ ๋นŒ๋“œ, ํ…Œ์ŠคํŠธ, ESLint ๊ฒ€์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.", - "details": "1. .github/workflows/ci.yml ํŒŒ์ผ ์ƒ์„ฑ 2. Node.js ํ™˜๊ฒฝ ์„ค์ • ๋ฐ ์˜์กด์„ฑ ์„ค์น˜ 3. TypeScript ๋นŒ๋“œ ๋ฐ ํƒ€์ž… ๊ฒ€์‚ฌ 4. ESLint ๋ฐ Prettier ๊ฒ€์‚ฌ ์ž๋™ํ™” 5. ํ…Œ์ŠคํŠธ ์‹คํ–‰ (๋‚˜์ค‘์— ์ถ”๊ฐ€๋  ํ…Œ์ŠคํŠธ๋“ค) 6. ๋นŒ๋“œ ์•„ํ‹ฐํŒฉํŠธ ์ƒ์„ฑ ๋ฐ ์ €์žฅ 7. PR์—์„œ ์ž๋™ ๊ฒ€์‚ฌ ์‹คํ–‰", - "testStrategy": "GitHub Actions ์›Œํฌํ”Œ๋กœ์šฐ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์‹คํ–‰๋˜๋Š”์ง€ ํ™•์ธ, PR ์ƒ์„ฑ ์‹œ ์ž๋™ ๊ฒ€์‚ฌ๊ฐ€ ๋™์ž‘ํ•˜๋Š”์ง€ ๊ฒ€์ฆ, ๋นŒ๋“œ ์‹คํŒจ ์‹œ ์ ์ ˆํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ ํ™•์ธ", - "priority": "medium", - "dependencies": [2], - "status": "pending", - "subtasks": [] - }, - { - "id": 5, - "title": "์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ Context API์—์„œ Zustand๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜", - "description": "๊ธฐ์กด Context API ๊ธฐ๋ฐ˜ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ Zustand๋กœ ์ „ํ™˜ํ•˜์—ฌ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๋ฅผ ์ค„์ด๊ณ  ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.", - "details": "1. Zustand ์„ค์น˜ ๋ฐ ๊ธฐ๋ณธ ์„ค์ • 2. ๊ธฐ์กด Context ๊ตฌ์กฐ ๋ถ„์„ ๋ฐ Zustand store ์„ค๊ณ„ 3. ์ธ์ฆ ์ƒํƒœ ๊ด€๋ฆฌ store ์ƒ์„ฑ (auth store) 4. ์•ฑ ์ „์ฒด ์ƒํƒœ ๊ด€๋ฆฌ store ์ƒ์„ฑ (app store) 5. ๊ธฐ์กด useContext ํ˜ธ์ถœ์„ zustand store ์‚ฌ์šฉ์œผ๋กœ ๋ณ€๊ฒฝ 6. TypeScript ํƒ€์ž… ์ •์˜ ์ถ”๊ฐ€ 7. DevTools ์—ฐ๋™ ์„ค์ •", - "testStrategy": "์ƒํƒœ ๋ณ€๊ฒฝ์ด ์˜ˆ์ƒ๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ, ์ปดํฌ๋„ŒํŠธ ๋ฆฌ๋ Œ๋”๋ง ํšŸ์ˆ˜ ๊ฐ์†Œ ํ™•์ธ, ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์—์„œ ์ƒํƒœ ์ถ”์  ๊ฐ€๋Šฅ ํ™•์ธ", - "priority": "medium", - "dependencies": [1], - "status": "pending", - "subtasks": [] - }, - { - "id": 6, - "title": "TanStack Query๋ฅผ ์‚ฌ์šฉํ•œ ๋ฐ์ดํ„ฐ ํŽ˜์นญ ๊ฐœ์„ ", - "description": "TanStack Query๋ฅผ ๋„์ž…ํ•˜์—ฌ ์ž๋™ ์บ์‹ฑ, ๋™๊ธฐํ™”, ์˜คํ”„๋ผ์ธ ์ง€์›์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.", - "details": "1. @tanstack/react-query ์„ค์น˜ ๋ฐ QueryClient ์„ค์ • 2. API ํ˜ธ์ถœ ํ•จ์ˆ˜๋“ค์„ React Query hooks๋กœ ์ „ํ™˜ 3. ์ž๋™ ์บ์‹ฑ ์ „๋žต ์„ค์ • (staleTime, cacheTime) 4. ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ๊ตฌํ˜„ (optimistic updates) 5. ์˜คํ”„๋ผ์ธ ์ƒํƒœ์—์„œ์˜ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ 6. ๋ฐฑ๊ทธ๋ผ์šด๋“œ refetch ์„ค์ • 7. ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ ์žฌ์‹œ๋„ ๋กœ์ง ๊ตฌํ˜„", - "testStrategy": "๋ฐ์ดํ„ฐ ์บ์‹ฑ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ, ์˜คํ”„๋ผ์ธ ์ƒํƒœ์—์„œ ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ฐ€๋Šฅ ํ™•์ธ, ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ", - "priority": "medium", - "dependencies": [5], - "status": "pending", - "subtasks": [] - }, - { - "id": 7, - "title": "ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ค์ • ๋ฐ ํ•ต์‹ฌ ๋กœ์ง ํ…Œ์ŠคํŠธ ์ž‘์„ฑ", - "description": "Vitest์™€ React Testing Library๋ฅผ ์„ค์ •ํ•˜๊ณ  ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ์ฃผ์š” ์‚ฌ์šฉ์ž ํ”Œ๋กœ์šฐ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.", - "details": "1. Vitest ๋ฐ React Testing Library ์„ค์น˜ ๋ฐ ์„ค์ • 2. ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ค์ • ํŒŒ์ผ ์ƒ์„ฑ (vitest.config.ts) 3. ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ 4. ์ฃผ์š” ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ํ…Œ์ŠคํŠธ 5. ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜ ํ…Œ์ŠคํŠธ (๋กœ๊ทธ์ธ, ๋ฐ์ดํ„ฐ ์ž…๋ ฅ ๋“ฑ) 6. API ๋ชจํ‚น ์„ค์ • 7. ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ 80% ๋ชฉํ‘œ ๋‹ฌ์„ฑ", - "testStrategy": "๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผํ•˜๋Š”์ง€ ํ™•์ธ, ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ฆฌํฌํŠธ ์ƒ์„ฑ, CI/CD ํŒŒ์ดํ”„๋ผ์ธ์—์„œ ํ…Œ์ŠคํŠธ ์ž๋™ ์‹คํ–‰ ํ™•์ธ", - "priority": "medium", - "dependencies": [4], - "status": "pending", - "subtasks": [] - }, - { - "id": 8, - "title": "React ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ตฌํ˜„", - "description": "React.memo, useMemo, useCallback์„ ์ ์šฉํ•˜๊ณ  ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง์„ ๋ฐฉ์ง€ํ•˜์—ฌ ์•ฑ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.", - "details": "1. React DevTools Profiler๋ฅผ ์‚ฌ์šฉํ•œ ์„ฑ๋Šฅ ๋ถ„์„ 2. ์ž์ฃผ ๋ฆฌ๋ Œ๋”๋ง๋˜๋Š” ์ปดํฌ๋„ŒํŠธ์— React.memo ์ ์šฉ 3. ๊ณ„์‚ฐ ๋น„์šฉ์ด ๋†’์€ ๋กœ์ง์— useMemo ์ ์šฉ 4. ์ฝœ๋ฐฑ ํ•จ์ˆ˜์— useCallback ์ ์šฉ 5. ์„ธ์…˜ ์ฒดํฌ ์ฃผ๊ธฐ๋ฅผ 5์ดˆ์—์„œ 30์ดˆ๋กœ ์กฐ์ • 6. ์ปดํฌ๋„ŒํŠธ ๋ ˆ์ด์ง€ ๋กœ๋”ฉ ๊ตฌํ˜„ (React.lazy, Suspense) 7. ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ๋ฐ ์ง€์—ฐ ๋กœ๋”ฉ", - "testStrategy": "React DevTools์—์„œ ๋ฆฌ๋ Œ๋”๋ง ํšŸ์ˆ˜ ๊ฐ์†Œ ํ™•์ธ, ์•ฑ ๋กœ๋”ฉ ์†๋„ 2๋ฐฐ ํ–ฅ์ƒ ์ธก์ •, ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ตœ์ ํ™” ํ™•์ธ", - "priority": "medium", - "dependencies": [6], - "status": "pending", - "subtasks": [] - }, - { - "id": 9, - "title": "Vercel ์ž๋™ ๋ฐฐํฌ ์„ค์ •", - "description": "Vercel์„ ์‚ฌ์šฉํ•˜์—ฌ ์ž๋™ ๋ฐฐํฌ ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ•˜๊ณ  ํ™˜๊ฒฝ๋ณ„ ๋ฐฐํฌ์™€ PR ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.", - "details": "1. Vercel ํ”„๋กœ์ ํŠธ ์—ฐ๊ฒฐ ๋ฐ GitHub ํ†ตํ•ฉ 2. ํ™˜๊ฒฝ๋ณ„ ๋ฐฐํฌ ์„ค์ • (ํ”„๋กœ๋•์…˜, ์Šคํ…Œ์ด์ง•) 3. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ Vercel ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์„ค์ • 4. PR ์ƒ์„ฑ ์‹œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐฐํฌ ์ž๋™ ์ƒ์„ฑ 5. ๋นŒ๋“œ ์ตœ์ ํ™” ์„ค์ • 6. ๋„๋ฉ”์ธ ์—ฐ๊ฒฐ ๋ฐ SSL ์ธ์ฆ์„œ ์„ค์ • 7. ๋ฐฐํฌ ํ›„ ์•Œ๋ฆผ ์„ค์ •", - "testStrategy": "์ž๋™ ๋ฐฐํฌ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ด๋ฃจ์–ด์ง€๋Š”์ง€ ํ™•์ธ, PR ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐฐํฌ ๋™์ž‘ ํ™•์ธ, ํ™˜๊ฒฝ๋ณ„๋กœ ์˜ฌ๋ฐ”๋ฅธ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์ ์šฉ๋˜๋Š”์ง€ ๊ฒ€์ฆ", - "priority": "low", - "dependencies": [4], - "status": "pending", - "subtasks": [] - }, - { - "id": 10, - "title": "๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ ๊ตฌ์ถ• ๋ฐ ๋ฒˆ๋“ค ์ตœ์ ํ™”", - "description": "Sentry๋ฅผ ์‚ฌ์šฉํ•œ ์—๋Ÿฌ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์„ค์ •ํ•˜๊ณ  ์›นํŒฉ ๋ฒˆ๋“ค ๋ถ„์„์„ ํ†ตํ•ด ๋ฒˆ๋“ค ํฌ๊ธฐ๋ฅผ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค.", - "details": "1. Sentry ์„ค์น˜ ๋ฐ ์„ค์ • (์—๋Ÿฌ ๋ชจ๋‹ˆํ„ฐ๋ง, ์„ฑ๋Šฅ ์ถ”์ ) 2. Webpack Bundle Analyzer๋ฅผ ์‚ฌ์šฉํ•œ ๋ฒˆ๋“ค ๋ถ„์„ 3. ๋ถˆํ•„์š”ํ•œ ์˜์กด์„ฑ ์ œ๊ฑฐ (74๊ฐœ dependencies ์ •๋ฆฌ) 4. ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… ์ ์šฉ์œผ๋กœ ์ดˆ๊ธฐ ๋กœ๋”ฉ ์ตœ์ ํ™” 5. Tree shaking ์ตœ์ ํ™” 6. ์‚ฌ์šฉ์ž ํ–‰๋™ ๋ถ„์„์„ ์œ„ํ•œ ๊ธฐ๋ณธ ์ด๋ฒคํŠธ ํŠธ๋ž˜ํ‚น 7. ์„ฑ๋Šฅ ์ง€ํ‘œ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌ์„ฑ", - "testStrategy": "Sentry์—์„œ ์—๋Ÿฌ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ˆ˜์ง‘๋˜๋Š”์ง€ ํ™•์ธ, ๋ฒˆ๋“ค ํฌ๊ธฐ 30% ๊ฐ์†Œ ๋‹ฌ์„ฑ ํ™•์ธ, ์•ฑ ๋กœ๋”ฉ ์†๋„ ๊ฐœ์„  ์ธก์ •", - "priority": "low", - "dependencies": [8, 9], - "status": "pending", - "subtasks": [] - } - ], - "metadata": { - "version": "1.0.0", - "created": "2025-01-11", - "lastModified": "2025-01-11", - "project": "์ ค๋ฆฌ์˜ ์ ์žํƒˆ์ถœ ๊ฐœ์„  ํ”„๋กœ์ ํŠธ" - }, "master": { "tasks": [ { @@ -127,8 +9,63 @@ "testStrategy": "TypeScript ์ปดํŒŒ์ผ๋Ÿฌ ์˜ค๋ฅ˜ 0๊ฐœ ๋‹ฌ์„ฑ, tsc --noEmit ๋ช…๋ น์–ด๋กœ ํƒ€์ž… ๊ฒ€์‚ฌ ํ†ต๊ณผ ํ™•์ธ, IDE์—์„œ ํƒ€์ž… ์ถ”๋ก ์ด ์ •ํ™•ํžˆ ์ž‘๋™ํ•˜๋Š”์ง€ ๊ฒ€์ฆ", "priority": "high", "dependencies": [], - "status": "pending", - "subtasks": [] + "status": "done", + "subtasks": [ + { + "id": 1, + "title": "TypeScript strict ๋ชจ๋“œ ์„ค์ • ์™„๋ฃŒ ๊ฒ€์ฆ", + "description": "๋ชจ๋“  strict ์˜ต์…˜์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํ™œ์„ฑํ™”๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ์ปดํŒŒ์ผ ์˜ค๋ฅ˜๊ฐ€ ์—†๋Š”์ง€ ๊ฒ€์ฆ", + "status": "done", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 2, + "title": "์ƒˆ๋กœ์šด ํƒ€์ž… ์‹œ์Šคํ…œ ๊ตฌ์กฐ ์•ˆ์ •์„ฑ ๊ฒ€์ฆ", + "description": "๊ตฌ์ถ•๋œ ํƒ€์ž… ์‹œ์Šคํ…œ์ด ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์—์„œ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•˜๊ณ  ํƒ€์ž… ์ถฉ๋Œ ํ™•์ธ", + "status": "done", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 3, + "title": "ํƒ€์ž… ๊ฐ€๋“œ ํ•จ์ˆ˜ ์„ฑ๋Šฅ ์ตœ์ ํ™”", + "description": "๊ตฌํ˜„๋œ 20+ ํƒ€์ž… ๊ฐ€๋“œ ํ•จ์ˆ˜๋“ค์˜ ์„ฑ๋Šฅ์„ ๊ฒ€ํ† ํ•˜๊ณ  ํ•„์š”์‹œ ์ตœ์ ํ™”", + "status": "done", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 4, + "title": "ํƒ€์ž… ์‹œ์Šคํ…œ ๋ฌธ์„œํ™”", + "description": "์ƒˆ๋กœ์šด ํƒ€์ž… ๊ตฌ์กฐ์™€ ํƒ€์ž… ๊ฐ€๋“œ ํ•จ์ˆ˜๋“ค์˜ ์‚ฌ์šฉ๋ฒ• ๋ฌธ์„œํ™” ๋ฐ ๊ฐ€์ด๋“œ๋ผ์ธ ์ž‘์„ฑ", + "status": "done", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 5, + "title": "์ถ”๊ฐ€ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž… ๊ฐœ๋ฐœ", + "description": "ํ”„๋กœ์ ํŠธ ํŠน์„ฑ์— ๋งž๋Š” ์ปค์Šคํ…€ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž… ๊ฐœ๋ฐœ ๋ฐ ๊ธฐ์กด ํƒ€์ž… ์‹œ์Šคํ…œ ํ™•์žฅ", + "status": "done", + "dependencies": [], + "details": "\nReact Hook ๋ฐ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ํŠนํ™” ํƒ€์ž… ๊ฐœ๋ฐœ ์™„๋ฃŒ:\n\nReact Hook ์ƒํƒœ ๊ด€๋ฆฌ ํƒ€์ž… 4๊ฐœ ๊ตฌํ˜„:\n- HookState: ์ผ๋ฐ˜์ ์ธ Hook ์ƒํƒœ ๊ด€๋ฆฌ\n- MutationState: ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ž‘์—…์šฉ\n- PaginationState: ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ƒํƒœ ๊ด€๋ฆฌ\n- InfiniteScrollState: ๋ฌดํ•œ ์Šคํฌ๋กค ์ƒํƒœ ๊ด€๋ฆฌ\n\n๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ํŠนํ™” ํƒ€์ž… 5๊ฐœ ๊ตฌํ˜„:\n- BudgetCalculation: ์˜ˆ์‚ฐ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ํƒ€์ž…\n- CategoryExpense: ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ง€์ถœ ๋ถ„์„ ํƒ€์ž…\n- MonthlyTrend: ์›”๋ณ„ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ ํƒ€์ž…\n- BudgetAlert: ์˜ˆ์‚ฐ ์•Œ๋ฆผ ์„ค์ • ํƒ€์ž…\n- TransactionFilters: ๊ฑฐ๋ž˜ ๋‚ด์—ญ ๊ฒ€์ƒ‰ ํ•„ํ„ฐ ํƒ€์ž…\n\n๊ณ ๊ธ‰ ์ œ๋„ค๋ฆญ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž… 4๊ฐœ ๊ตฌํ˜„:\n- ConditionalType: ์กฐ๊ฑด๋ถ€ ํƒ€์ž… ๊ฒฐ์ •\n- FunctionOverload: ํ•จ์ˆ˜ ์˜ค๋ฒ„๋กœ๋“œ ์ง€์›\n- DeepKeyof: ๊ฐ์ฒด์˜ ์žฌ๊ท€์  ํ‚ค ๊ฒฝ๋กœ ์ถ”์ถœ\n- UnionToIntersection: ์œ ๋‹ˆ์˜จ ํƒ€์ž…์„ ๊ต์ง‘ํ•ฉ์œผ๋กœ ๋ณ€ํ™˜\n\n๋ชจ๋“  ์ƒˆ๋กœ์šด ํƒ€์ž…์— ๋Œ€์‘ํ•˜๋Š” ํƒ€์ž… ๊ฐ€๋“œ ํ•จ์ˆ˜๋“ค๋„ ํ•จ๊ป˜ ๊ตฌํ˜„ํ•˜์—ฌ ๋Ÿฐํƒ€์ž„ ํƒ€์ž… ์•ˆ์ „์„ฑ ํ™•๋ณด. ์ „์ฒด ํƒ€์ž…๋“ค์ด index.ts์—์„œ export๋˜์–ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด์—์„œ ํ™œ์šฉ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๋กœ ์™„์„ฑ.\n", + "testStrategy": "" + }, + { + "id": 6, + "title": "ํƒ€์ž… ์•ˆ์ „์„ฑ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ ๊ตฌ์ถ•", + "description": "์ง€์†์ ์ธ ํƒ€์ž… ์•ˆ์ „์„ฑ ์œ ์ง€๋ฅผ ์œ„ํ•œ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ฒ€์ฆ ํ”„๋กœ์„ธ์Šค ๊ตฌ์ถ•", + "status": "done", + "dependencies": [], + "details": "\nํƒ€์ž… ์•ˆ์ „์„ฑ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ ๊ตฌ์ถ•์ด ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.\n\nPre-commit ํ›… ์„ค์ •: husky์™€ lint-staged๋ฅผ ์„ค์น˜ํ•˜์—ฌ .husky/pre-commit์—์„œ ์ปค๋ฐ‹ ์ „ ์ž๋™์œผ๋กœ ํƒ€์ž… ๊ฒ€์‚ฌ์™€ ESLint๊ฐ€ ์‹คํ–‰๋˜๋„๋ก ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.\n\nPackage.json ์Šคํฌ๋ฆฝํŠธ ํ™•์žฅ: type-check:watch๋กœ ์‹ค์‹œ๊ฐ„ ํƒ€์ž… ๊ฒ€์‚ฌ ๋ชจ๋‹ˆํ„ฐ๋ง, lint:fix๋กœ ์ž๋™ ESLint ์˜ค๋ฅ˜ ์ˆ˜์ •, check-all๋กœ ์ „์ฒด ๊ฒ€์‚ฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋ฉฐ, lint-staged ์„ค์ •์œผ๋กœ ๋ณ€๊ฒฝ๋œ ํŒŒ์ผ๋งŒ ์„ ๋ณ„์ ์œผ๋กœ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค.\n\nVS Code ์„ค์ • ์ตœ์ ํ™”: TypeScript ์–ธ์–ด ์„œ๋ฒ„ ์„ค์ •, ์ž๋™ import ์ •๋ฆฌ ๋ฐ ํƒ€์ž… ์ฒดํ‚น, ์ €์žฅ ์‹œ ์ž๋™ ESLint ์ˆ˜์ •, ํ•œ๊ตญ์–ด ๋กœ์ผ€์ผ ์„ค์ •์„ ํ†ตํ•ด ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.\n\nGitHub Actions ์›Œํฌํ”Œ๋กœ์šฐ: .github/workflows/type-check.yml์„ ์ƒ์„ฑํ•˜์—ฌ Node.js 18.x, 20.x ๋งคํŠธ๋ฆญ์Šค ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๊ณ , PR์—์„œ ํƒ€์ž… ๊ฒ€์‚ฌ ์‹คํŒจ ์‹œ ์ž๋™ ๋Œ“๊ธ€์„ ๋‹ฌ๋ฉฐ, ๋นŒ๋“œ ์•„ํ‹ฐํŒฉํŠธ๋ฅผ ์—…๋กœ๋“œํ•˜๋Š” CI/CD ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์ถ•ํ–ˆ์Šต๋‹ˆ๋‹ค.\n\n์ด์ œ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ฝ”๋“œ๋ฅผ ์ปค๋ฐ‹ํ•˜๊ฑฐ๋‚˜ PR์„ ์ƒ์„ฑํ•  ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ํƒ€์ž… ์•ˆ์ „์„ฑ์ด ๊ฒ€์ฆ๋˜์–ด ์ฝ”๋“œ ํ’ˆ์งˆ์ด ์ง€์†์ ์œผ๋กœ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.\n", + "testStrategy": "" + } + ] }, { "id": 2, @@ -140,18 +77,71 @@ "dependencies": [ 1 ], - "status": "pending", - "subtasks": [] + "status": "done", + "subtasks": [ + { + "id": 1, + "title": "ํ”„๋กœ์ ํŠธ ์ „์ฒด console.log ์ œ๊ฑฐ ๋ฐ ๋กœ๊ฑฐ ์„ค์ •", + "description": "ํ”„๋กœ์ ํŠธ ์ „์ฒด์—์„œ ๋ฐœ๊ฒฌ๋œ 81๊ฐœ์˜ console.log๋ฅผ ์ œ๊ฑฐํ•˜๊ณ , development ํ™˜๊ฒฝ์—์„œ๋Š” ์ ์ ˆํ•œ logger ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ๋Œ€์ฒดํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [], + "details": "1. ํ”„๋กœ์ ํŠธ ์ „์ฒด์—์„œ console.log ๊ฒ€์ƒ‰ ๋ฐ ์œ„์น˜ ํŒŒ์•… 2. production ํ™˜๊ฒฝ์—์„œ๋Š” ์™„์ „ ์ œ๊ฑฐ 3. development ํ™˜๊ฒฝ์—์„œ ํ•„์š”ํ•œ ๋กœ๊น…์€ winston ๋˜๋Š” pino ๊ฐ™์€ ์ ์ ˆํ•œ logger ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ๋Œ€์ฒด 4. ํ™˜๊ฒฝ๋ณ„ ๋กœ๊น… ๋ ˆ๋ฒจ ์„ค์ •", + "status": "done", + "testStrategy": "๋นŒ๋“œ ํ›„ console.log๊ฐ€ production ๋ฒˆ๋“ค์— ํฌํ•จ๋˜์ง€ ์•Š์•˜๋Š”์ง€ ํ™•์ธ, development ํ™˜๊ฒฝ์—์„œ ๋กœ๊ฑฐ๊ฐ€ ์ •์ƒ ์ž‘๋™ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ" + }, + { + "id": 2, + "title": "SupabaseToAppwriteMigration import ์˜ค๋ฅ˜ ์ˆ˜์ •", + "description": "SupabaseToAppwriteMigration ๊ด€๋ จ import ์˜ค๋ฅ˜๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ  ๋นŒ๋“œ ์˜ค๋ฅ˜๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [], + "details": "1. SupabaseToAppwriteMigration ๊ด€๋ จ ๋ชจ๋“  import ๋ฌธ ๊ฒ€ํ†  2. ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํŒŒ์ผ์ด๋‚˜ ์ž˜๋ชป๋œ ๊ฒฝ๋กœ ์ˆ˜์ • 3. TypeScript ํƒ€์ž… ์˜ค๋ฅ˜ ํ•ด๊ฒฐ 4. ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” import ์ œ๊ฑฐ", + "status": "done", + "testStrategy": "TypeScript ์ปดํŒŒ์ผ ์˜ค๋ฅ˜ ์—†์ด ๋นŒ๋“œ๊ฐ€ ์„ฑ๊ณตํ•˜๋Š”์ง€ ํ™•์ธ, ๊ด€๋ จ ์ปดํฌ๋„ŒํŠธ๋“ค์ด ์ •์ƒ์ ์œผ๋กœ ๋ Œ๋”๋ง๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธ" + }, + { + "id": 3, + "title": "ESLint ๊ทœ์น™ ์„ค์ • ๋ฐ ๊ฐ•ํ™”", + "description": "ESLint ์„ค์ •์— @typescript-eslint/recommended์™€ react-hooks/recommended ๊ทœ์น™์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์ฝ”๋“œ ํ’ˆ์งˆ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.", + "dependencies": [ + 1, + 2 + ], + "details": "1. .eslintrc ํŒŒ์ผ ์ˆ˜์ •ํ•˜์—ฌ @typescript-eslint/recommended ๊ทœ์น™ ์ถ”๊ฐ€ 2. react-hooks/recommended ๊ทœ์น™ ์ถ”๊ฐ€ 3. ํ”„๋กœ์ ํŠธ์— ๋งž๋Š” ์ปค์Šคํ…€ ๊ทœ์น™ ์„ค์ • 4. ๊ธฐ์กด ์ฝ”๋“œ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ฆฐํŠธ ์˜ค๋ฅ˜ ์ˆ˜์ •", + "status": "done", + "testStrategy": "npm run lint ์‹คํ–‰ํ•˜์—ฌ ๋ชจ๋“  ํŒŒ์ผ์ด ๋ฆฐํŠธ ๊ทœ์น™์„ ํ†ต๊ณผํ•˜๋Š”์ง€ ํ™•์ธ, IDE์—์„œ ์‹ค์‹œ๊ฐ„ ๋ฆฐํŠธ ์˜ค๋ฅ˜ ํ‘œ์‹œ ํ™•์ธ" + }, + { + "id": 4, + "title": "Prettier ์„ค์ • ๋ฐ ์ฝ”๋“œ ํฌ๋งทํŒ…", + "description": ".prettierrc์™€ .prettierignore ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  ํ”„๋กœ์ ํŠธ ์ „์ฒด ์ฝ”๋“œ๋ฅผ ์ผ๊ด€๋œ ์Šคํƒ€์ผ๋กœ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 3 + ], + "details": "1. .prettierrc ํŒŒ์ผ ์ƒ์„ฑ ๋ฐ ํ”„๋กœ์ ํŠธ ์Šคํƒ€์ผ ๊ฐ€์ด๋“œ ์„ค์ • 2. .prettierignore ํŒŒ์ผ ์ƒ์„ฑํ•˜์—ฌ ํฌ๋งทํŒ… ์ œ์™ธ ํŒŒ์ผ ์„ค์ • 3. ํ”„๋กœ์ ํŠธ ์ „์ฒด ์ฝ”๋“œ์— Prettier ์ ์šฉ 4. ESLint์™€ Prettier ์ถฉ๋Œ ๋ฐฉ์ง€ ์„ค์ •", + "status": "done", + "testStrategy": "npm run format ์Šคํฌ๋ฆฝํŠธ๋กœ ์ „์ฒด ์ฝ”๋“œ ํฌ๋งทํŒ… ํ™•์ธ, VSCode์—์„œ ์ž๋™ ํฌ๋งทํŒ… ์ž‘๋™ ํ™•์ธ" + }, + { + "id": 5, + "title": "pre-commit hook ์„ค์ • ๋ฐ ์ž๋™ํ™”", + "description": "Husky์™€ lint-staged๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ pre-commit hook์„ ์„ค์ •ํ•˜๊ณ  ์ปค๋ฐ‹ ์‹œ ์ž๋™์œผ๋กœ ๋ฆฐํŒ…๊ณผ ํฌ๋งทํŒ…์ด ์‹คํ–‰๋˜๋„๋ก ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 4 + ], + "details": "1. Husky ์„ค์น˜ ๋ฐ ์„ค์ • 2. lint-staged ์„ค์น˜ ๋ฐ ์„ค์ • 3. pre-commit hook์—์„œ ESLint์™€ Prettier ์ž๋™ ์‹คํ–‰ ์„ค์ • 4. package.json์— ๊ด€๋ จ ์Šคํฌ๋ฆฝํŠธ ์ถ”๊ฐ€ 5. ํŒ€์›๋“ค์„ ์œ„ํ•œ ์„ค์ • ๊ฐ€์ด๋“œ ์ž‘์„ฑ", + "status": "done", + "testStrategy": "ํ…Œ์ŠคํŠธ ์ปค๋ฐ‹ ์ˆ˜ํ–‰ํ•˜์—ฌ pre-commit hook์ด ์ •์ƒ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธ, ๋ฆฐํŠธ ์˜ค๋ฅ˜๊ฐ€ ์žˆ๋Š” ์ฝ”๋“œ ์ปค๋ฐ‹ ์‹œ ์ฐจ๋‹จ๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธ" + } + ] }, { "id": 3, "title": "ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ณด์•ˆ ๊ฐ•ํ™” ๋ฐ ๊ด€๋ฆฌ ๊ฐœ์„ ", - "description": "API ํ‚ค์˜ ํด๋ผ์ด์–ธํŠธ ๋…ธ์ถœ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ  ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ๋ฅผ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.", - "details": "1. ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœ๋˜์ง€ ๋ง์•„์•ผ ํ•  API ํ‚ค๋“ค์„ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ๋กœ ์ด๋™ 2. .env.example ํŒŒ์ผ ์ƒ์„ฑ์œผ๋กœ ํ•„์š”ํ•œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ฌธ์„œํ™” 3. VITE_๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋งŒ ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœ๋˜๋„๋ก ์ •๋ฆฌ 4. ๋ฏผ๊ฐํ•œ API ํ‚ค๋Š” ์„œ๋ฒ„๋ฆฌ์Šค ํ•จ์ˆ˜๋‚˜ ๋ฐฑ์—”๋“œ์—์„œ๋งŒ ์‚ฌ์šฉ 5. ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ํŒŒ์ผ ๋ถ„๋ฆฌ (.env.local, .env.production)", - "testStrategy": "๋นŒ๋“œ๋œ ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ์—์„œ ๋ฏผ๊ฐํ•œ API ํ‚ค๊ฐ€ ๋…ธ์ถœ๋˜์ง€ ์•Š๋Š”์ง€ ํ™•์ธ, ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋กœ๋“œ๋˜๋Š”์ง€ ๊ฐ ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธ", - "priority": "high", + "description": "API ํ‚ค์˜ ํด๋ผ์ด์–ธํŠธ ๋…ธ์ถœ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ  ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ๋ฅผ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ๋ณด์•ˆ ๊ฐ•ํ™” ์ž‘์—…์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + "status": "done", "dependencies": [], - "status": "pending", + "priority": "high", + "details": "ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ณด์•ˆ ๊ฐ•ํ™” ์ž‘์—…์ด ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค:\n\n1. โœ… ํด๋ผ์ด์–ธํŠธ API ํ‚ค ๋…ธ์ถœ ๋ฌธ์ œ ํ•ด๊ฒฐ\n - VITE_APPWRITE_API_KEY๊ฐ€ ๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฌผ์— ๋…ธ์ถœ๋˜๋Š” ๋ฌธ์ œ ํ™•์ธ ๋ฐ ์ˆ˜์ •\n - .env์—์„œ ํ•ด๋‹น ํ‚ค ์ œ๊ฑฐํ•˜๊ณ  ์ฃผ์„ ์ฒ˜๋ฆฌ\n - src/lib/appwrite/config.ts์—์„œ API ํ‚ค๋ฅผ ๋นˆ ๋ฌธ์ž์—ด๋กœ ๋ณ€๊ฒฝ\n\n2. โœ… ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ฌธ์„œํ™” ๋ฐ ์ •๋ฆฌ\n - .env.example ํŒŒ์ผ ์ƒ์„ฑ์œผ๋กœ ํ•„์š”ํ•œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ฌธ์„œํ™”\n - Task Master AI ํ‚ค๋“ค๊ณผ Appwrite ์„ค์ • ํฌํ•จ\n - ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ์˜ˆ์‹œ ๊ฐ’์œผ๋กœ ๋Œ€์ฒด\n\n3. โœ… ํด๋ผ์ด์–ธํŠธ ๋…ธ์ถœ ๋ฐฉ์ง€\n - VITE_ ์ ‘๋‘์‚ฌ๊ฐ€ ์žˆ๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋งŒ ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœ๋˜๋„๋ก ์ •๋ฆฌ\n - API ํ‚ค์—์„œ VITE_ ์ ‘๋‘์‚ฌ ์ œ๊ฑฐ๋กœ ํด๋ผ์ด์–ธํŠธ ๋…ธ์ถœ ์ฐจ๋‹จ\n\n4. โœ… ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ๋ถ„๋ฆฌ\n - .env.local: ๋กœ์ปฌ ๊ฐœ๋ฐœํ™˜๊ฒฝ์šฉ ์„ค์ • ํŒŒ์ผ ์ƒ์„ฑ\n - .env.production: ํ”„๋กœ๋•์…˜์šฉ ์„ค์ • ํŒŒ์ผ ์ƒ์„ฑ\n - .gitignore์— .env.local ์ถ”๊ฐ€๋กœ ๋ฏผ๊ฐํ•œ ๋กœ์ปฌ ์„ค์ • ๋ณดํ˜ธ\n\n5. โœ… ๋ณด์•ˆ ๊ฒ€์ฆ ์™„๋ฃŒ\n - API ํ‚ค ์ œ๊ฑฐ ํ›„ ๋นŒ๋“œ ์„ฑ๊ณต ํ…Œ์ŠคํŠธ\n - ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค์—์„œ ๋ฏผ๊ฐํ•œ API ํ‚ค ๋…ธ์ถœ๋˜์ง€ ์•Š์Œ ํ™•์ธ\n\n๊ฒฐ๊ณผ: ํด๋ผ์ด์–ธํŠธ ์ธก ๋ณด์•ˆ ์ทจ์•ฝ์  ์ œ๊ฑฐ, ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ๊ด€๋ฆฌ ์ฒด๊ณ„ํ™”, ๊ฐœ๋ฐœ์ž ๊ฐ€์ด๋“œ๋ผ์ธ ๋ฌธ์„œํ™” ์™„๋ฃŒ", + "testStrategy": "โœ… ์™„๋ฃŒ๋œ ํ…Œ์ŠคํŠธ:\n- ๋นŒ๋“œ๋œ ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ์—์„œ ๋ฏผ๊ฐํ•œ API ํ‚ค ๋…ธ์ถœ ๊ฒ€์‚ฌ ํ†ต๊ณผ\n- ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋”ฉ ํ…Œ์ŠคํŠธ ๊ฐ ํ™˜๊ฒฝ์—์„œ ์„ฑ๊ณต\n- API ํ‚ค ์ œ๊ฑฐ ํ›„ ๋นŒ๋“œ ํ”„๋กœ์„ธ์Šค ์ •์ƒ ๋™์ž‘ ํ™•์ธ\n- .env.example ๊ธฐ๋ฐ˜ ํ™˜๊ฒฝ ์„ค์ • ๊ฐ€์ด๋“œ ๊ฒ€์ฆ ์™„๋ฃŒ", "subtasks": [] }, { @@ -164,8 +154,51 @@ "dependencies": [ 2 ], - "status": "pending", - "subtasks": [] + "status": "in-progress", + "subtasks": [ + { + "id": 1, + "title": "๊ธฐ๋ณธ GitHub Actions ์›Œํฌํ”Œ๋กœ์šฐ ํŒŒ์ผ ์ƒ์„ฑ", + "description": ".github/workflows/ci.yml ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [], + "details": "GitHub Actions ์›Œํฌํ”Œ๋กœ์šฐ์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ํŠธ๋ฆฌ๊ฑฐ ์ด๋ฒคํŠธ(push, pull_request), ์ž‘์—… ํ™˜๊ฒฝ(Ubuntu), Node.js ๋ฒ„์ „ ๋งคํŠธ๋ฆญ์Šค๋ฅผ ์„ค์ •ํ•˜๊ณ  ๊ธฐ๋ณธ์ ์ธ ์ฒดํฌ์•„์›ƒ ์•ก์…˜์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.", + "status": "done", + "testStrategy": "์›Œํฌํ”Œ๋กœ์šฐ ํŒŒ์ผ์˜ YAML ๊ตฌ๋ฌธ์ด ์˜ฌ๋ฐ”๋ฅธ์ง€ ํ™•์ธํ•˜๊ณ , GitHub์—์„œ ์›Œํฌํ”Œ๋กœ์šฐ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ธ์‹๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค." + }, + { + "id": 2, + "title": "Node.js ํ™˜๊ฒฝ ์„ค์ • ๋ฐ ์˜์กด์„ฑ ์„ค์น˜ ๋‹จ๊ณ„ ๊ตฌํ˜„", + "description": "Node.js ํ™˜๊ฒฝ์„ ์„ค์ •ํ•˜๊ณ  npm ์˜์กด์„ฑ์„ ์„ค์น˜ํ•˜๋Š” ๋‹จ๊ณ„๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 1 + ], + "details": "actions/setup-node ์•ก์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ Node.js 18.x ๋ฒ„์ „์„ ์„ค์ •ํ•˜๊ณ , package-lock.json์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ ์บ์‹ฑ ์ „๋žต์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. npm ci ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์˜์กด์„ฑ์„ ๋น ๋ฅด๊ณ  ์•ˆ์ •์ ์œผ๋กœ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.", + "status": "done", + "testStrategy": "์˜์กด์„ฑ ์„ค์น˜๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜๊ณ , ์บ์‹ฑ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ๋นŒ๋“œ ์‹œ๊ฐ„ ๊ฐœ์„  ํšจ๊ณผ๋ฅผ ์ธก์ •ํ•ฉ๋‹ˆ๋‹ค." + }, + { + "id": 3, + "title": "๋นŒ๋“œ ๋ฐ ์ฝ”๋“œ ํ’ˆ์งˆ ๊ฒ€์‚ฌ ๋‹จ๊ณ„ ๊ตฌํ˜„", + "description": "TypeScript ๋นŒ๋“œ, ESLint, Prettier ๊ฒ€์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ๋‹จ๊ณ„๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 2 + ], + "details": "npm run build ๋ช…๋ น์–ด๋กœ TypeScript ์ปดํŒŒ์ผ์„ ์‹คํ–‰ํ•˜๊ณ , npm run lint๋กœ ESLint ๊ฒ€์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. Prettier ํฌ๋งท ๊ฒ€์‚ฌ๋„ ํฌํ•จํ•˜์—ฌ ์ฝ”๋“œ ์Šคํƒ€์ผ ์ผ๊ด€์„ฑ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ๋‹จ๊ณ„์—์„œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์›Œํฌํ”Œ๋กœ์šฐ๊ฐ€ ์‹คํŒจํ•˜๋„๋ก ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.", + "status": "done", + "testStrategy": "์˜๋„์ ์œผ๋กœ ESLint ์˜ค๋ฅ˜๋‚˜ TypeScript ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์›Œํฌํ”Œ๋กœ์šฐ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์‹คํŒจํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ๊ฒ€์‚ฌ๊ฐ€ ํ†ต๊ณผํ•  ๋•Œ ์„ฑ๊ณตํ•˜๋Š”์ง€๋„ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค." + }, + { + "id": 4, + "title": "๋นŒ๋“œ ์•„ํ‹ฐํŒฉํŠธ ์—…๋กœ๋“œ ๋ฐ ํ…Œ์ŠคํŠธ ์ค€๋น„", + "description": "๋นŒ๋“œ๋œ ํŒŒ์ผ๋“ค์„ ์•„ํ‹ฐํŒฉํŠธ๋กœ ์—…๋กœ๋“œํ•˜๊ณ  ํ–ฅํ›„ ํ…Œ์ŠคํŠธ ์‹คํ–‰์„ ์œ„ํ•œ ๊ตฌ์กฐ๋ฅผ ์ค€๋น„ํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 3 + ], + "details": "actions/upload-artifact ์•ก์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ dist ํด๋”์˜ ๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฌผ์„ ์•„ํ‹ฐํŒฉํŠธ๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ์‹คํ–‰์„ ์œ„ํ•œ ํ”Œ๋ ˆ์ด์Šคํ™€๋” ๋‹จ๊ณ„๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ์›Œํฌํ”Œ๋กœ์šฐ๊ฐ€ PR ์ปจํ…์ŠคํŠธ์—์„œ๋„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์‹คํ–‰๋˜๋„๋ก ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.", + "status": "done", + "testStrategy": "๋นŒ๋“œ ์•„ํ‹ฐํŒฉํŠธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์—…๋กœ๋“œ๋˜๊ณ  ๋‹ค์šด๋กœ๋“œ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. PR ์ƒ์„ฑ ์‹œ ์›Œํฌํ”Œ๋กœ์šฐ๊ฐ€ ์ž๋™์œผ๋กœ ์‹คํ–‰๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค." + } + ] }, { "id": 5, @@ -178,7 +211,62 @@ 1 ], "status": "pending", - "subtasks": [] + "subtasks": [ + { + "id": 1, + "title": "Zustand ํŒจํ‚ค์ง€ ์„ค์น˜ ๋ฐ ๊ธฐ๋ณธ ์„ค์ • ๊ตฌ์„ฑ", + "description": "Zustand ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•˜๊ณ  TypeScript ์„ค์ • ๋ฐ DevTools ์—ฐ๋™์„ ์œ„ํ•œ ๊ธฐ๋ณธ ๊ตฌ์„ฑ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [], + "details": "npm install zustand๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•˜๊ณ , immer์™€ devtools ๋ฏธ๋“ค์›จ์–ด ์„ค์ •์„ ํฌํ•จํ•œ ๊ธฐ๋ณธ store ๊ตฌ์กฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. TypeScript ์ง€์›์„ ์œ„ํ•œ ํƒ€์ž… ์ •์˜๋„ ํ•จ๊ป˜ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "testStrategy": "Zustand ์Šคํ† ์–ด๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋˜๊ณ  DevTools์—์„œ ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค." + }, + { + "id": 2, + "title": "๊ธฐ์กด Context API ๊ตฌ์กฐ ๋ถ„์„ ๋ฐ Zustand ์Šคํ† ์–ด ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„", + "description": "ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ Context API ๊ตฌ์กฐ๋ฅผ ๋ถ„์„ํ•˜๊ณ  Zustand๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•  ์Šคํ† ์–ด ์•„ํ‚คํ…์ฒ˜๋ฅผ ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 1 + ], + "details": "src/contexts ํด๋”์˜ ๊ธฐ์กด Context ์ฝ”๋“œ๋ฅผ ๋ถ„์„ํ•˜์—ฌ ์ƒํƒœ ๊ตฌ์กฐ, ์•ก์…˜ ํ•จ์ˆ˜, ํƒ€์ž… ์ •์˜๋ฅผ ํŒŒ์•…ํ•˜๊ณ , ์ด๋ฅผ Zustand ์Šคํ† ์–ด๋กœ ๋ณ€ํ™˜ํ•  ๊ณ„ํš์„ ์ˆ˜๋ฆฝํ•ฉ๋‹ˆ๋‹ค. ์ธ์ฆ, ์˜ˆ์‚ฐ, ์•ฑ ์ƒํƒœ ๋“ฑ ๋„๋ฉ”์ธ๋ณ„๋กœ ์Šคํ† ์–ด๋ฅผ ๋ถ„๋ฆฌํ•˜๋Š” ๋ฐฉ์•ˆ์„ ๊ณ ๋ คํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "testStrategy": "๊ธฐ์กด Context API์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์ด Zustand ์„ค๊ณ„์— ํฌํ•จ๋˜์—ˆ๋Š”์ง€ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋กœ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค." + }, + { + "id": 3, + "title": "์ธ์ฆ ์ƒํƒœ ๊ด€๋ฆฌ Zustand ์Šคํ† ์–ด ๊ตฌํ˜„", + "description": "์‚ฌ์šฉ์ž ์ธ์ฆ ๊ด€๋ จ ์ƒํƒœ์™€ ์•ก์…˜์„ ๊ด€๋ฆฌํ•˜๋Š” Zustand ์Šคํ† ์–ด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 2 + ], + "details": "src/stores/authStore.ts ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜์—ฌ ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ์ƒํƒœ, ์‚ฌ์šฉ์ž ์ •๋ณด, ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ ์•ก์…˜ ํ•จ์ˆ˜๋ฅผ ํฌํ•จํ•œ ์ธ์ฆ ์Šคํ† ์–ด๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. Appwrite ์ธ์ฆ๊ณผ์˜ ์—ฐ๋™๋„ ํฌํ•จํ•˜๋ฉฐ, ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ๋ณด์žฅํ•˜๋Š” TypeScript ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "testStrategy": "์ธ์ฆ ๊ด€๋ จ ๋ชจ๋“  ์•ก์…˜(๋กœ๊ทธ์ธ, ๋กœ๊ทธ์•„์›ƒ, ์ƒํƒœ ํ™•์ธ)์ด ์ •์ƒ ์ž‘๋™ํ•˜๋Š”์ง€ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋กœ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค." + }, + { + "id": 4, + "title": "์•ฑ ์ „์ฒด ์ƒํƒœ ๊ด€๋ฆฌ Zustand ์Šคํ† ์–ด ๊ตฌํ˜„", + "description": "์ „์—ญ ์•ฑ ์ƒํƒœ(ํ…Œ๋งˆ, ๋กœ๋”ฉ ์ƒํƒœ, ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋“ฑ)์™€ ์˜ˆ์‚ฐ ๊ด€๋ฆฌ ์ƒํƒœ๋ฅผ ์œ„ํ•œ Zustand ์Šคํ† ์–ด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 2 + ], + "details": "src/stores/appStore.ts์™€ src/stores/budgetStore.ts ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜์—ฌ ์•ฑ ์ „๋ฐ˜์˜ ์ƒํƒœ์™€ ์˜ˆ์‚ฐ ๊ด€๋ จ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์Šคํ† ์–ด๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ์Šคํ† ์–ด๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ์ž‘๋™ํ•˜๋ฉด์„œ๋„ ํ•„์š”์‹œ ์„œ๋กœ ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "testStrategy": "๊ฐ ์Šคํ† ์–ด์˜ ์ƒํƒœ ๋ณ€๊ฒฝ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘๋™ํ•˜๊ณ  ์ปดํฌ๋„ŒํŠธ์—์„œ ์ •์ƒ์ ์œผ๋กœ ๊ตฌ๋…๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค." + }, + { + "id": 5, + "title": "๊ธฐ์กด useContext ํ˜ธ์ถœ์„ Zustand ์Šคํ† ์–ด ์‚ฌ์šฉ์œผ๋กœ ์ „ํ™˜", + "description": "๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์—์„œ useContext ํ˜ธ์ถœ์„ ์ œ๊ฑฐํ•˜๊ณ  Zustand ์Šคํ† ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ฆฌํŒฉํ† ๋งํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 3, + 4 + ], + "details": "src/components, src/pages, src/hooks ํด๋”์˜ ๋ชจ๋“  ํŒŒ์ผ์—์„œ Context API ์‚ฌ์šฉ์„ ์ฐพ์•„ Zustand ์Šคํ† ์–ด ์‚ฌ์šฉ์œผ๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค. useAuth, useBudget ๋“ฑ์˜ ์ปค์Šคํ…€ ํ›…๋„ Zustand ๊ธฐ๋ฐ˜์œผ๋กœ ์žฌ์ž‘์„ฑํ•˜๊ณ , Context Provider ์ปดํฌ๋„ŒํŠธ๋“ค์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "testStrategy": "๊ธฐ์กด ๊ธฐ๋Šฅ์ด ๋ชจ๋‘ ์ •์ƒ ์ž‘๋™ํ•˜๋Š”์ง€ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ณ , Context API ๊ด€๋ จ ์ฝ”๋“œ๊ฐ€ ์™„์ „ํžˆ ์ œ๊ฑฐ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค." + } + ] }, { "id": 6, @@ -191,7 +279,50 @@ 5 ], "status": "pending", - "subtasks": [] + "subtasks": [ + { + "id": 1, + "title": "TanStack Query ์„ค์น˜ ๋ฐ QueryClient ์„ค์ •", + "description": "@tanstack/react-query๋ฅผ ์„ค์น˜ํ•˜๊ณ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— QueryClient๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [], + "details": "1. npm install @tanstack/react-query ์‹คํ–‰\n2. App.tsx์—์„œ QueryClient ์ƒ์„ฑ ๋ฐ QueryClientProvider ์„ค์ •\n3. React Query DevTools ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ ํ™œ์„ฑํ™”\n4. ๊ธฐ๋ณธ ์ „์—ญ ์„ค์ •๊ฐ’ ๊ตฌ์„ฑ (staleTime, cacheTime, refetchOnWindowFocus ๋“ฑ)", + "status": "pending", + "testStrategy": "QueryClient๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋˜๊ณ  Provider๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ž˜ํ•‘๋˜์—ˆ๋Š”์ง€ ํ™•์ธ. DevTools๊ฐ€ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ ์ž‘๋™ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ" + }, + { + "id": 2, + "title": "๊ธฐ์กด API ํ˜ธ์ถœ์„ React Query ํ›…์œผ๋กœ ์ „ํ™˜", + "description": "ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ API ํ˜ธ์ถœ ํ•จ์ˆ˜๋“ค์„ useQuery, useMutation ํ›…์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 1 + ], + "details": "1. ๊ธฐ์กด fetch/axios ํ˜ธ์ถœ์„ ์‹๋ณ„ํ•˜๊ณ  ๋ถ„๋ฅ˜\n2. ์ฝ๊ธฐ ์ „์šฉ API๋ฅผ useQuery๋กœ ์ „ํ™˜ (๊ฑฐ๋ž˜ ๋ชฉ๋ก, ์‚ฌ์šฉ์ž ์ •๋ณด ๋“ฑ)\n3. ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ API๋ฅผ useMutation์œผ๋กœ ์ „ํ™˜\n4. ์ฟผ๋ฆฌ ํ‚ค ๋„ค์ด๋ฐ ์ปจ๋ฒค์…˜ ์ •์˜ ๋ฐ ์ ์šฉ\n5. ๊ฐ ํ›…์— ์ ์ ˆํ•œ ์˜ต์…˜ ์„ค์ • (enabled, select, onSuccess/onError ๋“ฑ)", + "status": "pending", + "testStrategy": "๊ธฐ์กด ๊ธฐ๋Šฅ์ด React Query๋กœ ์ „ํ™˜ ํ›„์—๋„ ๋™์ผํ•˜๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธ. ๋„คํŠธ์›Œํฌ ํƒญ์—์„œ ์ค‘๋ณต ์š”์ฒญ์ด ์ œ๊ฑฐ๋˜์—ˆ๋Š”์ง€ ๊ฒ€์ฆ" + }, + { + "id": 3, + "title": "์บ์‹ฑ ์ „๋žต ๋ฐ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™” ๊ตฌํ˜„", + "description": "์ž๋™ ์บ์‹ฑ, staleTime/cacheTime ์„ค์ •, ๋ฐฑ๊ทธ๋ผ์šด๋“œ refetch๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 2 + ], + "details": "1. ๋ฐ์ดํ„ฐ ํƒ€์ž…๋ณ„ ์บ์‹ฑ ์ „๋žต ์ •์˜ (๊ฑฐ๋ž˜ ๋ฐ์ดํ„ฐ: 5๋ถ„, ์‚ฌ์šฉ์ž ์ •๋ณด: 30๋ถ„ ๋“ฑ)\n2. refetchOnWindowFocus, refetchOnReconnect ์„ค์ •\n3. background refetch ๊ฐ„๊ฒฉ ์„ค์ •\n4. ์ž์ฃผ ๋ณ€๊ฒฝ๋˜๋Š” ๋ฐ์ดํ„ฐ์™€ ์ •์  ๋ฐ์ดํ„ฐ ๊ตฌ๋ถ„ํ•˜์—ฌ staleTime ์กฐ์ •\n5. ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•œ cacheTime ์„ค์ •", + "status": "pending", + "testStrategy": "๋ธŒ๋ผ์šฐ์ € ํƒญ ์ „ํ™˜ ์‹œ ์ž๋™ refetch ์ž‘๋™ ํ™•์ธ. ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์„ค์ •๋œ ์‹œ๊ฐ„๋งŒํผ ์œ ์ง€๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธ. ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ํ•ด์ œ/์žฌ์—ฐ๊ฒฐ ์‹œ ๋™์ž‘ ๊ฒ€์ฆ" + }, + { + "id": 4, + "title": "๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ๋ฐ ์˜คํ”„๋ผ์ธ ์ง€์› ๊ตฌํ˜„", + "description": "์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ํ–ฅ์ƒ์„ ์œ„ํ•œ ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ์™€ ์˜คํ”„๋ผ์ธ ์ƒํƒœ ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 3 + ], + "details": "1. ๊ฑฐ๋ž˜ ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ์— ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ์ ์šฉ\n2. ์‹คํŒจ ์‹œ ์ž๋™ ๋กค๋ฐฑ ๋กœ์ง ๊ตฌํ˜„\n3. ์˜คํ”„๋ผ์ธ ์ƒํƒœ ๊ฐ์ง€ ๋ฐ UI ํ‘œ์‹œ\n4. ์˜จ๋ผ์ธ ๋ณต๊ตฌ ์‹œ ์ž๋™ ์žฌ์‹œ๋„ ๋ฉ”์ปค๋‹ˆ์ฆ˜\n5. ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๋ฐ ์‚ฌ์šฉ์ž ์•Œ๋ฆผ ์‹œ์Šคํ…œ ๊ตฌ์ถ•\n6. retry ๋กœ์ง ์„ค์ • (exponential backoff)", + "status": "pending", + "testStrategy": "๋„คํŠธ์›Œํฌ๋ฅผ ์ฐจ๋‹จํ•œ ์ƒํƒœ์—์„œ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ๋„ ํ›„ ์˜จ๋ผ์ธ ๋ณต๊ตฌ ์‹œ ์ •์ƒ ๋™๊ธฐํ™” ํ™•์ธ. ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ์‹คํŒจ ์‹œ UI ๋กค๋ฐฑ ํ…Œ์ŠคํŠธ. ๋‹ค์–‘ํ•œ ์—๋Ÿฌ ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ ์ ์ ˆํ•œ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ ๊ฒ€์ฆ" + } + ] }, { "id": 7, @@ -204,7 +335,61 @@ 4 ], "status": "pending", - "subtasks": [] + "subtasks": [ + { + "id": 1, + "title": "Vitest ๋ฐ React Testing Library ์„ค์น˜ ๋ฐ ๊ธฐ๋ณธ ์„ค์ •", + "description": "ํ”„๋กœ์ ํŠธ์— Vitest์™€ React Testing Library๋ฅผ ์„ค์น˜ํ•˜๊ณ  ๊ธฐ๋ณธ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [], + "details": "npm install vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom -D๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ํ•„์š”ํ•œ ํ…Œ์ŠคํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์„ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค. package.json์— test ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ๊ธฐ๋ณธ ์„ค์ •์„ ์™„๋ฃŒํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "testStrategy": "์„ค์น˜ ํ›„ ๊ฐ„๋‹จํ•œ ์ƒ˜ํ”Œ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ํ™˜๊ฒฝ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ" + }, + { + "id": 2, + "title": "vitest.config.ts ์„ค์ • ํŒŒ์ผ ์ƒ์„ฑ ๋ฐ ๊ตฌ์„ฑ", + "description": "Vitest ์„ค์ • ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  JSX, TypeScript, ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋“ฑ์„ ์œ„ํ•œ ์„ค์ •์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 1 + ], + "details": "vitest.config.ts ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜์—ฌ Vite ํ”Œ๋Ÿฌ๊ทธ์ธ, jsdom ํ™˜๊ฒฝ, setupFiles, coverage ์„ค์ • ๋“ฑ์„ ํฌํ•จํ•œ ํฌ๊ด„์ ์ธ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ค์ •์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. src/setupTests.ts ํŒŒ์ผ๋„ ์ƒ์„ฑํ•˜์—ฌ ์ „์—ญ ํ…Œ์ŠคํŠธ ์„ค์ •์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "testStrategy": "์„ค์ • ํŒŒ์ผ ์ƒ์„ฑ ํ›„ ํ…Œ์ŠคํŠธ ๋ช…๋ น์–ด๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์‹คํ–‰๋˜๋Š”์ง€ ํ™•์ธ" + }, + { + "id": 3, + "title": "ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ", + "description": "์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜, ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ๋กœ์ง, ๊ณ„์‚ฐ ํ•จ์ˆ˜ ๋“ฑ ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์— ๋Œ€ํ•œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 2 + ], + "details": "src/utils, src/lib ๋””๋ ‰ํ† ๋ฆฌ์˜ ํ•จ์ˆ˜๋“ค๊ณผ ๊ธˆ์œต ๊ณ„์‚ฐ, ๋ฐ์ดํ„ฐ ํฌ๋งทํŒ…, ๋‚ ์งœ ์ฒ˜๋ฆฌ ๋“ฑ์˜ ํ•ต์‹ฌ ๋กœ์ง์— ๋Œ€ํ•ด ํฌ๊ด„์ ์ธ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ์—ฃ์ง€ ์ผ€์ด์Šค์™€ ์—๋Ÿฌ ์ƒํ™ฉ๋„ ํ…Œ์ŠคํŠธ์— ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "testStrategy": "๊ฐ ํ•จ์ˆ˜๋ณ„๋กœ ์ •์ƒ ์ผ€์ด์Šค, ์—ฃ์ง€ ์ผ€์ด์Šค, ์—๋Ÿฌ ์ผ€์ด์Šค๋ฅผ ๋ชจ๋‘ ์ปค๋ฒ„ํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ž‘์„ฑ" + }, + { + "id": 4, + "title": "์ฃผ์š” ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ๋ฐ ์ธํ„ฐ๋ž™์…˜ ํ…Œ์ŠคํŠธ", + "description": "ํ•ต์‹ฌ React ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ๋ Œ๋”๋ง๊ณผ ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜์— ๋Œ€ํ•œ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 3 + ], + "details": "TransactionForm, ExpenseForm, ์ธ์ฆ ์ปดํฌ๋„ŒํŠธ ๋“ฑ ์ฃผ์š” ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ๋ Œ๋”๋ง, ํผ ์ œ์ถœ, ๋ฒ„ํŠผ ํด๋ฆญ, ์ž…๋ ฅ ํ•„๋“œ ์ƒํ˜ธ์ž‘์šฉ ๋“ฑ์„ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค. React Testing Library์˜ user-event๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์‹ค์ œ ์‚ฌ์šฉ์ž ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "testStrategy": "์ปดํฌ๋„ŒํŠธ๋ณ„๋กœ ๋ Œ๋”๋ง, ์‚ฌ์šฉ์ž ์ด๋ฒคํŠธ, ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ž‘์„ฑ" + }, + { + "id": 5, + "title": "API ๋ชจํ‚น ์„ค์ • ๋ฐ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ์ตœ์ ํ™”", + "description": "Appwrite API ํ˜ธ์ถœ์„ ๋ชจํ‚นํ•˜๊ณ  ์ „์ฒด ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ 80% ์ด์ƒ์œผ๋กœ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.", + "dependencies": [ + 4 + ], + "details": "MSW(Mock Service Worker) ๋˜๋Š” vi.mock์„ ์‚ฌ์šฉํ•˜์—ฌ Appwrite API ํ˜ธ์ถœ์„ ๋ชจํ‚นํ•ฉ๋‹ˆ๋‹ค. ์ธ์ฆ, ๋ฐ์ดํ„ฐ CRUD ์ž‘์—… ๋“ฑ์˜ API ์ƒํ˜ธ์ž‘์šฉ์„ ํ…Œ์ŠคํŠธํ•˜๊ณ , ์ „์ฒด ํ”„๋กœ์ ํŠธ์˜ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ์ธก์ •ํ•˜์—ฌ 80% ๋ชฉํ‘œ๋ฅผ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "status": "pending", + "testStrategy": "API ๋ชจํ‚น ํ›„ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์‹คํ–‰ ๋ฐ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ฆฌํฌํŠธ๋ฅผ ํ†ตํ•œ ๋ชฉํ‘œ ๋‹ฌ์„ฑ ํ™•์ธ" + } + ] }, { "id": 8, @@ -217,7 +402,49 @@ 6 ], "status": "pending", - "subtasks": [] + "subtasks": [ + { + "id": 1, + "title": "React DevTools Profiler๋กœ ์„ฑ๋Šฅ ๋ณ‘๋ชฉ ๋ถ„์„", + "description": "React DevTools Profiler๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ˜„์žฌ ์•ฑ์˜ ๋ Œ๋”๋ง ์„ฑ๋Šฅ์„ ์ธก์ •ํ•˜๊ณ  ์ตœ์ ํ™”๊ฐ€ ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‹๋ณ„ํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [], + "details": "1. React DevTools Profiler ์„ค์น˜ ๋ฐ ์„ค์ • 2. ์ฃผ์š” ์‚ฌ์šฉ์ž ํ”Œ๋กœ์šฐ์—์„œ ์„ฑ๋Šฅ ํ”„๋กœํŒŒ์ผ๋ง ์‹คํ–‰ 3. ๋ Œ๋”๋ง ์‹œ๊ฐ„์ด ๊ธด ์ปดํฌ๋„ŒํŠธ ์‹๋ณ„ 4. ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง์ด ๋ฐœ์ƒํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก ์ž‘์„ฑ 5. ์„ฑ๋Šฅ ๋ฒ ์ด์Šค๋ผ์ธ ์„ค์ • ๋ฐ ๋ฌธ์„œํ™”", + "status": "pending", + "testStrategy": "ํ”„๋กœํŒŒ์ผ๋ง ๊ฒฐ๊ณผ๋ฅผ ํ†ตํ•ด ๋ Œ๋”๋ง ์‹œ๊ฐ„๊ณผ ๋ฆฌ๋ Œ๋”๋ง ํšŸ์ˆ˜๋ฅผ ์ธก์ •ํ•˜๊ณ , ์ตœ์ ํ™” ์ „ํ›„ ๋น„๊ต๋ฅผ ์œ„ํ•œ ์„ฑ๋Šฅ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘" + }, + { + "id": 2, + "title": "React.memo์™€ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ํ›… ์ ์šฉ", + "description": "์‹๋ณ„๋œ ์ปดํฌ๋„ŒํŠธ์— React.memo, useMemo, useCallback์„ ์ ์šฉํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 1 + ], + "details": "1. ์ž์ฃผ ๋ฆฌ๋ Œ๋”๋ง๋˜๋Š” ์ปดํฌ๋„ŒํŠธ์— React.memo ์ ์šฉ 2. ๊ณ„์‚ฐ ๋น„์šฉ์ด ๋†’์€ ๋กœ์ง์— useMemo ์ ์šฉ 3. ์ฝœ๋ฐฑ ํ•จ์ˆ˜์™€ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์— useCallback ์ ์šฉ 4. ์˜์กด์„ฑ ๋ฐฐ์—ด ์ตœ์ ํ™” 5. ์ปดํฌ๋„ŒํŠธ๋ณ„ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ์ „๋žต ๊ตฌํ˜„", + "status": "pending", + "testStrategy": "React DevTools๋กœ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ์ ์šฉ ์ „ํ›„ ๋ฆฌ๋ Œ๋”๋ง ํšŸ์ˆ˜ ๋น„๊ต, ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ์ž‘์„ฑํ•˜์—ฌ ๋ Œ๋”๋ง ์ตœ์ ํ™” ํšจ๊ณผ ๊ฒ€์ฆ" + }, + { + "id": 3, + "title": "์ปดํฌ๋„ŒํŠธ ๋ ˆ์ด์ง€ ๋กœ๋”ฉ ๋ฐ ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… ๊ตฌํ˜„", + "description": "React.lazy์™€ Suspense๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ•„์š”ํ•  ๋•Œ๋งŒ ๋กœ๋“œํ•˜๋„๋ก ํ•˜๊ณ  ๋ฒˆ๋“ค ํฌ๊ธฐ๋ฅผ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [], + "details": "1. ํŽ˜์ด์ง€๋ณ„ ์ปดํฌ๋„ŒํŠธ์— React.lazy ์ ์šฉ 2. Suspense ๊ฒฝ๊ณ„ ์„ค์ • ๋ฐ ๋กœ๋”ฉ ์ƒํƒœ ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„ 3. ๋ผ์šฐํŠธ ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… ์ ์šฉ 4. ๋™์  import๋ฅผ ํ†ตํ•œ ๋ชจ๋“ˆ ๋ ˆ์ด์ง€ ๋กœ๋”ฉ 5. ๋ฒˆ๋“ค ๋ถ„์„๊ธฐ๋กœ ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… ํšจ๊ณผ ํ™•์ธ", + "status": "pending", + "testStrategy": "๋ฒˆ๋“ค ํฌ๊ธฐ ์ธก์ •, ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์‹œ๊ฐ„ ๋น„๊ต, ๋„คํŠธ์›Œํฌ ํƒญ์—์„œ ์ฒญํฌ ํŒŒ์ผ ๋กœ๋”ฉ ํ™•์ธ, Lighthouse ์„ฑ๋Šฅ ์ ์ˆ˜ ๊ฐœ์„  ์ธก์ •" + }, + { + "id": 4, + "title": "์„ฑ๋Šฅ ์„ค์ • ์ตœ์ ํ™” ๋ฐ ์ตœ์ข… ๊ฒ€์ฆ", + "description": "์„ธ์…˜ ์ฒดํฌ ์ฃผ๊ธฐ ์กฐ์ •, ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ๋ฐ ์ง€์—ฐ ๋กœ๋”ฉ์„ ๊ตฌํ˜„ํ•˜๊ณ  ์ „์ฒด์ ์ธ ์„ฑ๋Šฅ ๊ฐœ์„  ํšจ๊ณผ๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 2, + 3 + ], + "details": "1. ์„ธ์…˜ ์ฒดํฌ ์ฃผ๊ธฐ๋ฅผ 5์ดˆ์—์„œ 30์ดˆ๋กœ ์กฐ์ • 2. ์ด๋ฏธ์ง€ ์ง€์—ฐ ๋กœ๋”ฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ ์šฉ 3. ์ด๋ฏธ์ง€ ํฌ๋งท ์ตœ์ ํ™” (WebP, AVIF) 4. ๊ฐ€์ƒํ™”๋œ ๋ฆฌ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ ์ ์šฉ 5. ์ตœ์ข… ์„ฑ๋Šฅ ํ”„๋กœํŒŒ์ผ๋ง ๋ฐ ๋ฒ ์ด์Šค๋ผ์ธ ๋Œ€๋น„ ๊ฐœ์„  ํšจ๊ณผ ์ธก์ •", + "status": "pending", + "testStrategy": "์ตœ์ ํ™” ์ „ํ›„ ์„ฑ๋Šฅ ๋ฉ”ํŠธ๋ฆญ ๋น„๊ต, Core Web Vitals ์ธก์ •, ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๋ชจ๋‹ˆํ„ฐ๋ง, ์‚ฌ์šฉ์ž ์ฒด๊ฐ ์„ฑ๋Šฅ ๊ฐœ์„  ๊ฒ€์ฆ" + } + ] }, { "id": 9, @@ -230,7 +457,39 @@ 4 ], "status": "pending", - "subtasks": [] + "subtasks": [ + { + "id": 1, + "title": "Vercel ํ”„๋กœ์ ํŠธ ์„ค์ • ๋ฐ GitHub ํ†ตํ•ฉ", + "description": "Vercel ๊ณ„์ •์— ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  GitHub ์ €์žฅ์†Œ์™€ ์—ฐ๊ฒฐํ•˜์—ฌ ์ž๋™ ๋ฐฐํฌ ํŒŒ์ดํ”„๋ผ์ธ์˜ ๊ธฐ์ดˆ๋ฅผ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [], + "details": "1. Vercel ๊ณ„์ • ์ƒ์„ฑ ๋ฐ ๋กœ๊ทธ์ธ 2. GitHub ์ €์žฅ์†Œ๋ฅผ Vercel์— ์ž„ํฌํŠธ 3. ๋นŒ๋“œ ์„ค์ • ๊ตฌ์„ฑ (Node.js 18.x, npm run build) 4. ๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ ๋ฐ ์ถœ๋ ฅ ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ • 5. ์ฒซ ๋ฒˆ์งธ ๋ฐฐํฌ ํ…Œ์ŠคํŠธ ์‹คํ–‰ 6. ๋ฐฐํฌ ๋กœ๊ทธ ํ™•์ธ ๋ฐ ์˜ค๋ฅ˜ ํ•ด๊ฒฐ", + "status": "pending", + "testStrategy": "๋ฐฐํฌ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜๋Š”์ง€ ํ™•์ธํ•˜๊ณ , ์ƒ์„ฑ๋œ Vercel URL์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ •์ƒ์ ์œผ๋กœ ๋กœ๋“œ๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธ" + }, + { + "id": 2, + "title": "ํ™˜๊ฒฝ๋ณ„ ๋ฐฐํฌ ๋ฐ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •", + "description": "ํ”„๋กœ๋•์…˜๊ณผ ์Šคํ…Œ์ด์ง• ํ™˜๊ฒฝ์„ ๊ตฌ๋ถ„ํ•˜์—ฌ ๋ฐฐํฌํ•˜๊ณ , ๊ฐ ํ™˜๊ฒฝ์— ๋งž๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ Vercel ๋Œ€์‹œ๋ณด๋“œ์—์„œ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 1 + ], + "details": "1. Vercel ํ”„๋กœ์ ํŠธ ์„ค์ •์—์„œ Git ๋ธŒ๋žœ์น˜๋ณ„ ํ™˜๊ฒฝ ๋งคํ•‘ (main โ†’ Production, develop โ†’ Preview) 2. ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ Vercel ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์„ค์ • (VITE_APPWRITE_ENDPOINT, VITE_APPWRITE_PROJECT_ID ๋“ฑ) 3. ํ”„๋กœ๋•์…˜๊ณผ ํ”„๋ฆฌ๋ทฐ ํ™˜๊ฒฝ๋ณ„๋กœ ๋‹ค๋ฅธ Appwrite ํ”„๋กœ์ ํŠธ ID ์„ค์ • 4. ํ™˜๊ฒฝ๋ณ„ ๋„๋ฉ”์ธ ์„ค์ • (ํ”„๋กœ๋•์…˜์šฉ ์ปค์Šคํ…€ ๋„๋ฉ”์ธ, ํ”„๋ฆฌ๋ทฐ์šฉ ์ž๋™ ์ƒ์„ฑ ๋„๋ฉ”์ธ) 5. ๊ฐ ํ™˜๊ฒฝ์—์„œ ๋นŒ๋“œ ํ…Œ์ŠคํŠธ ๋ฐ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์ ์šฉ ํ™•์ธ", + "status": "pending", + "testStrategy": "main ๋ธŒ๋žœ์น˜์™€ develop ๋ธŒ๋žœ์น˜์— ๊ฐ๊ฐ ํ‘ธ์‹œํ•˜์—ฌ ์˜ฌ๋ฐ”๋ฅธ ํ™˜๊ฒฝ์œผ๋กœ ๋ฐฐํฌ๋˜๋Š”์ง€ ํ™•์ธํ•˜๊ณ , ๊ฐ ํ™˜๊ฒฝ์—์„œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ ์šฉ๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธ" + }, + { + "id": 3, + "title": "PR ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐ ๋ฐฐํฌ ์ตœ์ ํ™” ์„ค์ •", + "description": "Pull Request ์ƒ์„ฑ ์‹œ ์ž๋™์œผ๋กœ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐฐํฌ๊ฐ€ ์ƒ์„ฑ๋˜๋„๋ก ์„ค์ •ํ•˜๊ณ , ๋นŒ๋“œ ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฐ ๋ฐฐํฌ ์•Œ๋ฆผ์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 2 + ], + "details": "1. GitHub PR ์ƒ์„ฑ ์‹œ ์ž๋™ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐฐํฌ ํ™œ์„ฑํ™” 2. Vercel ๋นŒ๋“œ ์ตœ์ ํ™” ์„ค์ • (์บ์‹ฑ, ๋ฒˆ๋“ค ๋ถ„์„ ํ™œ์„ฑํ™”) 3. ๋„๋ฉ”์ธ ์—ฐ๊ฒฐ ๋ฐ SSL ์ธ์ฆ์„œ ์ž๋™ ์„ค์ • 4. GitHub Actions ๋˜๋Š” Vercel ์›นํ›…์„ ํ†ตํ•œ ๋ฐฐํฌ ์™„๋ฃŒ ์•Œ๋ฆผ ์„ค์ • 5. ๋ฐฐํฌ ์‹คํŒจ ์‹œ Slack/Discord ์•Œ๋ฆผ ์„ค์ • 6. ๋ฐฐํฌ ์ƒํƒœ๋ฅผ GitHub PR์— ์ž๋™์œผ๋กœ ์ฝ”๋ฉ˜ํŠธํ•˜๋Š” ์„ค์ •", + "status": "pending", + "testStrategy": "ํ…Œ์ŠคํŠธ PR์„ ์ƒ์„ฑํ•˜์—ฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐฐํฌ๊ฐ€ ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋˜๋Š”์ง€ ํ™•์ธํ•˜๊ณ , ๋ฐฐํฌ ์™„๋ฃŒ ์•Œ๋ฆผ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ „์†ก๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธ. ๋นŒ๋“œ ์‹œ๊ฐ„ ์ธก์ • ๋ฐ ์ตœ์ ํ™” ํšจ๊ณผ ๊ฒ€์ฆ" + } + ] }, { "id": 10, @@ -244,12 +503,53 @@ 9 ], "status": "pending", - "subtasks": [] + "subtasks": [ + { + "id": 1, + "title": "Sentry ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ ์„ค์ •", + "description": "Sentry๋ฅผ ์„ค์น˜ํ•˜๊ณ  ์—๋Ÿฌ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์„ฑ๋Šฅ ์ถ”์ ์„ ์œ„ํ•œ ๊ธฐ๋ณธ ์„ค์ •์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [], + "details": "1. @sentry/react ๋ฐ @sentry/tracing ํŒจํ‚ค์ง€ ์„ค์น˜ 2. Sentry ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ ๋ฐ DSN ์„ค์ • 3. App.tsx์— Sentry ์ดˆ๊ธฐํ™” ์ฝ”๋“œ ์ถ”๊ฐ€ 4. ์—๋Ÿฌ ๋ฐ”์šด๋”๋ฆฌ์™€ Sentry ํ†ตํ•ฉ 5. ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง ์˜ต์…˜ ์„ค์ • 6. ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ๋ถ„๋ฆฌ (.env ํŒŒ์ผ ํ™œ์šฉ) 7. ์†Œ์Šค๋งต ์—…๋กœ๋“œ ์„ค์ •์œผ๋กœ ๋””๋ฒ„๊น… ์ •๋ณด ์ œ๊ณต", + "status": "pending", + "testStrategy": "ํ…Œ์ŠคํŠธ ์—๋Ÿฌ ๋ฐœ์ƒ์‹œ์ผœ Sentry ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์—๋Ÿฌ ์ˆ˜์ง‘ ํ™•์ธ, ์„ฑ๋Šฅ ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํ…Œ์ŠคํŠธ" + }, + { + "id": 2, + "title": "์›นํŒฉ ๋ฒˆ๋“ค ๋ถ„์„ ๋ฐ ์˜์กด์„ฑ ์ •๋ฆฌ", + "description": "Webpack Bundle Analyzer๋ฅผ ์‚ฌ์šฉํ•ด ๋ฒˆ๋“ค์„ ๋ถ„์„ํ•˜๊ณ  ๋ถˆํ•„์š”ํ•œ ์˜์กด์„ฑ 74๊ฐœ๋ฅผ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [], + "details": "1. webpack-bundle-analyzer ์„ค์น˜ ๋ฐ ์„ค์ • 2. npm run build ํ›„ ๋ฒˆ๋“ค ๋ถ„์„ ์‹คํ–‰ 3. package.json์—์„œ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” dependencies ์‹๋ณ„ 4. npm ls๋ฅผ ํ†ตํ•œ ์˜์กด์„ฑ ํŠธ๋ฆฌ ๋ถ„์„ 5. ์ค‘๋ณต๋˜๊ฑฐ๋‚˜ unused๋œ ํŒจํ‚ค์ง€ ์ œ๊ฑฐ 6. devDependencies์™€ dependencies ๋ถ„๋ฅ˜ ์ •๋ฆฌ 7. ๋ฒˆ๋“ค ํฌ๊ธฐ before/after ๋น„๊ต ์ธก์ •", + "status": "pending", + "testStrategy": "๋ฒˆ๋“ค ๋ถ„์„ ๋ฆฌํฌํŠธ ์ƒ์„ฑํ•˜์—ฌ ํฌ๊ธฐ ๊ฐ์†Œ ํ™•์ธ, npm audit์œผ๋กœ ๋ณด์•ˆ ์ทจ์•ฝ์  ๊ฒ€์‚ฌ" + }, + { + "id": 3, + "title": "์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… ๋ฐ Tree Shaking ์ตœ์ ํ™”", + "description": "React.lazy()๋ฅผ ํ™œ์šฉํ•œ ์ปดํฌ๋„ŒํŠธ ๋ถ„ํ• ๊ณผ Tree Shaking์„ ํ†ตํ•ด ์ดˆ๊ธฐ ๋กœ๋”ฉ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 2 + ], + "details": "1. React.lazy()๋กœ ํŽ˜์ด์ง€๋ณ„ ์ปดํฌ๋„ŒํŠธ ๋ถ„ํ•  2. Suspense๋ฅผ ํ™œ์šฉํ•œ ๋กœ๋”ฉ ์ƒํƒœ ์ฒ˜๋ฆฌ 3. ๋™์  import()๋ฅผ ํ†ตํ•œ ๋ผ์šฐํŠธ ๋ ˆ๋ฒจ ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… 4. webpack ์„ค์ •์—์„œ Tree Shaking ํ™œ์„ฑํ™” 5. ES6 ๋ชจ๋“ˆ ํ˜•ํƒœ๋กœ import/export ์ตœ์ ํ™” 6. ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” CSS ์ œ๊ฑฐ (PurgeCSS ์ ์šฉ) 7. ์ฒญํฌ ๋ถ„ํ•  ์ „๋žต ์ตœ์ ํ™” (vendor, common chunks)", + "status": "pending", + "testStrategy": "๊ฐœ๋ฐœ์ž ๋„๊ตฌ Network ํƒญ์—์„œ ์ฒญํฌ๋ณ„ ๋กœ๋”ฉ ํ™•์ธ, Lighthouse ์„ฑ๋Šฅ ์ ์ˆ˜ ์ธก์ •" + }, + { + "id": 4, + "title": "์‚ฌ์šฉ์ž ํ–‰๋™ ์ถ”์  ๋ฐ ์„ฑ๋Šฅ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌ์„ฑ", + "description": "๊ธฐ๋ณธ ์ด๋ฒคํŠธ ํŠธ๋ž˜ํ‚น์„ ๊ตฌํ˜„ํ•˜๊ณ  ์„ฑ๋Šฅ ์ง€ํ‘œ๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ˆ˜ ์žˆ๋Š” ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "dependencies": [ + 1 + ], + "details": "1. ํŽ˜์ด์ง€๋ทฐ, ํด๋ฆญ, ํผ ์ œ์ถœ ๋“ฑ ํ•ต์‹ฌ ์ด๋ฒคํŠธ ํŠธ๋ž˜ํ‚น 2. React Router์™€ ์—ฐ๋™ํ•œ ํŽ˜์ด์ง€ ์ „ํ™˜ ์ถ”์  3. Sentry Performance ๋ชจ๋‹ˆํ„ฐ๋ง ๋Œ€์‹œ๋ณด๋“œ ์„ค์ • 4. Core Web Vitals (LCP, FID, CLS) ์ธก์ • 5. ์ปค์Šคํ…€ ์„ฑ๋Šฅ ์ง€ํ‘œ ์ •์˜ ๋ฐ ์ˆ˜์ง‘ 6. ์—๋Ÿฌ์œจ, ์‘๋‹ต์‹œ๊ฐ„ ๋“ฑ ์ฃผ์š” ๋ฉ”ํŠธ๋ฆญ ์•Œ๋ฆผ ์„ค์ • 7. ์ผ์ผ/์ฃผ๊ฐ„ ์„ฑ๋Šฅ ๋ฆฌํฌํŠธ ์ž๋™ํ™”", + "status": "pending", + "testStrategy": "์‹ค์ œ ์‚ฌ์šฉ์ž ์‹œ๋‚˜๋ฆฌ์˜ค๋กœ ์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œ์ผœ ์ถ”์  ๋ฐ์ดํ„ฐ ํ™•์ธ, ์„ฑ๋Šฅ ๋Œ€์‹œ๋ณด๋“œ์—์„œ ๋ฉ”ํŠธ๋ฆญ ํ‘œ์‹œ ๊ฒ€์ฆ" + } + ] } ], "metadata": { - "created": "2025-07-11T21:00:35.577Z", - "updated": "2025-07-11T21:00:35.577Z", + "created": "2025-07-12T09:00:00.000Z", + "updated": "2025-07-12T06:14:52.889Z", "description": "Tasks for master context" } } diff --git a/.taskmaster_backup/config.json b/.taskmaster_backup/config.json new file mode 100644 index 0000000..1890598 --- /dev/null +++ b/.taskmaster_backup/config.json @@ -0,0 +1,20 @@ +{ + "models": { + "main": "claude-3-5-sonnet-20241022", + "research": "perplexity-llama-3.1-sonar-large-128k-online", + "fallback": "claude-3-5-sonnet-20241022" + }, + "global": { + "logLevel": "info", + "debug": false, + "defaultNumTasks": 10, + "defaultSubtasks": 5, + "defaultPriority": "medium", + "projectName": "Task Master", + "ollamaBaseURL": "http://localhost:11434/api", + "bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com", + "responseLanguage": "Korean", + "userId": "1234567890" + }, + "claudeCode": {} +} \ No newline at end of file diff --git a/.taskmaster_backup/state.json b/.taskmaster_backup/state.json new file mode 100644 index 0000000..2bd7ae2 --- /dev/null +++ b/.taskmaster_backup/state.json @@ -0,0 +1,6 @@ +{ + "currentTag": "master", + "lastSwitched": "2025-07-11T20:57:32.202Z", + "branchTagMapping": {}, + "migrationNoticeShown": true +} \ No newline at end of file diff --git a/.taskmaster/tasks/task_001.txt b/.taskmaster_backup/tasks/task_001.txt similarity index 97% rename from .taskmaster/tasks/task_001.txt rename to .taskmaster_backup/tasks/task_001.txt index 20003e1..267588b 100644 --- a/.taskmaster/tasks/task_001.txt +++ b/.taskmaster_backup/tasks/task_001.txt @@ -1,6 +1,6 @@ # Task ID: 1 # Title: TypeScript ์„ค์ • ๊ฐ•ํ™” ๋ฐ ํƒ€์ž… ์•ˆ์ „์„ฑ ํ™•๋ณด -# Status: pending +# Status: done # Dependencies: None # Priority: high # Description: tsconfig.json์˜ strict ๋ชจ๋“œ๋ฅผ ์ ์ง„์ ์œผ๋กœ ํ™œ์„ฑํ™”ํ•˜๊ณ  ๊ธฐ์กด any ํƒ€์ž… ์‚ฌ์šฉ์„ ์ œ๊ฑฐํ•˜์—ฌ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ํ™•๋ณดํ•ฉ๋‹ˆ๋‹ค. diff --git a/.taskmaster/tasks/task_002.txt b/.taskmaster_backup/tasks/task_002.txt similarity index 97% rename from .taskmaster/tasks/task_002.txt rename to .taskmaster_backup/tasks/task_002.txt index 508f98c..3005862 100644 --- a/.taskmaster/tasks/task_002.txt +++ b/.taskmaster_backup/tasks/task_002.txt @@ -1,6 +1,6 @@ # Task ID: 2 # Title: ์ฝ”๋“œ ํ’ˆ์งˆ ๊ฐœ์„  ๋ฐ ๋ฆฐํŒ… ์„ค์ • -# Status: pending +# Status: done # Dependencies: 1 # Priority: high # Description: console.log ์ œ๊ฑฐ, ๋นŒ๋“œ ์˜ค๋ฅ˜ ์ˆ˜์ •, ESLint/Prettier ์„ค์ •์„ ํ†ตํ•ด ์ฝ”๋“œ ํ’ˆ์งˆ์„ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค. diff --git a/.taskmaster/tasks/task_003.txt b/.taskmaster_backup/tasks/task_003.txt similarity index 97% rename from .taskmaster/tasks/task_003.txt rename to .taskmaster_backup/tasks/task_003.txt index c52eab7..17df709 100644 --- a/.taskmaster/tasks/task_003.txt +++ b/.taskmaster_backup/tasks/task_003.txt @@ -1,6 +1,6 @@ # Task ID: 3 # Title: ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ณด์•ˆ ๊ฐ•ํ™” ๋ฐ ๊ด€๋ฆฌ ๊ฐœ์„  -# Status: pending +# Status: done # Dependencies: None # Priority: high # Description: API ํ‚ค์˜ ํด๋ผ์ด์–ธํŠธ ๋…ธ์ถœ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ  ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ๋ฅผ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค. diff --git a/.taskmaster/tasks/task_004.txt b/.taskmaster_backup/tasks/task_004.txt similarity index 97% rename from .taskmaster/tasks/task_004.txt rename to .taskmaster_backup/tasks/task_004.txt index 5a98166..f7bd631 100644 --- a/.taskmaster/tasks/task_004.txt +++ b/.taskmaster_backup/tasks/task_004.txt @@ -1,6 +1,6 @@ # Task ID: 4 # Title: CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ• -# Status: pending +# Status: done # Dependencies: 2 # Priority: medium # Description: GitHub Actions๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž๋™ ๋นŒ๋“œ, ํ…Œ์ŠคํŠธ, ESLint ๊ฒ€์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. diff --git a/.taskmaster/tasks/task_005.txt b/.taskmaster_backup/tasks/task_005.txt similarity index 97% rename from .taskmaster/tasks/task_005.txt rename to .taskmaster_backup/tasks/task_005.txt index 70ae468..987e65c 100644 --- a/.taskmaster/tasks/task_005.txt +++ b/.taskmaster_backup/tasks/task_005.txt @@ -1,6 +1,6 @@ # Task ID: 5 # Title: ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ Context API์—์„œ Zustand๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ -# Status: pending +# Status: done # Dependencies: 1 # Priority: medium # Description: ๊ธฐ์กด Context API ๊ธฐ๋ฐ˜ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ Zustand๋กœ ์ „ํ™˜ํ•˜์—ฌ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๋ฅผ ์ค„์ด๊ณ  ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. diff --git a/.taskmaster/tasks/task_006.txt b/.taskmaster_backup/tasks/task_006.txt similarity index 97% rename from .taskmaster/tasks/task_006.txt rename to .taskmaster_backup/tasks/task_006.txt index e85888f..fd2bb78 100644 --- a/.taskmaster/tasks/task_006.txt +++ b/.taskmaster_backup/tasks/task_006.txt @@ -1,6 +1,6 @@ # Task ID: 6 # Title: TanStack Query๋ฅผ ์‚ฌ์šฉํ•œ ๋ฐ์ดํ„ฐ ํŽ˜์นญ ๊ฐœ์„  -# Status: pending +# Status: done # Dependencies: 5 # Priority: medium # Description: TanStack Query๋ฅผ ๋„์ž…ํ•˜์—ฌ ์ž๋™ ์บ์‹ฑ, ๋™๊ธฐํ™”, ์˜คํ”„๋ผ์ธ ์ง€์›์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. diff --git a/.taskmaster/tasks/task_007.txt b/.taskmaster_backup/tasks/task_007.txt similarity index 97% rename from .taskmaster/tasks/task_007.txt rename to .taskmaster_backup/tasks/task_007.txt index bff7d79..6a6954f 100644 --- a/.taskmaster/tasks/task_007.txt +++ b/.taskmaster_backup/tasks/task_007.txt @@ -1,6 +1,6 @@ # Task ID: 7 # Title: ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ค์ • ๋ฐ ํ•ต์‹ฌ ๋กœ์ง ํ…Œ์ŠคํŠธ ์ž‘์„ฑ -# Status: pending +# Status: done # Dependencies: 4 # Priority: medium # Description: Vitest์™€ React Testing Library๋ฅผ ์„ค์ •ํ•˜๊ณ  ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ์ฃผ์š” ์‚ฌ์šฉ์ž ํ”Œ๋กœ์šฐ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. diff --git a/.taskmaster/tasks/task_008.txt b/.taskmaster_backup/tasks/task_008.txt similarity index 97% rename from .taskmaster/tasks/task_008.txt rename to .taskmaster_backup/tasks/task_008.txt index 59e055c..bdd5804 100644 --- a/.taskmaster/tasks/task_008.txt +++ b/.taskmaster_backup/tasks/task_008.txt @@ -1,6 +1,6 @@ # Task ID: 8 # Title: React ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ตฌํ˜„ -# Status: pending +# Status: done # Dependencies: 6 # Priority: medium # Description: React.memo, useMemo, useCallback์„ ์ ์šฉํ•˜๊ณ  ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง์„ ๋ฐฉ์ง€ํ•˜์—ฌ ์•ฑ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. diff --git a/.taskmaster/tasks/task_009.txt b/.taskmaster_backup/tasks/task_009.txt similarity index 97% rename from .taskmaster/tasks/task_009.txt rename to .taskmaster_backup/tasks/task_009.txt index 9d8605d..a51e4b3 100644 --- a/.taskmaster/tasks/task_009.txt +++ b/.taskmaster_backup/tasks/task_009.txt @@ -1,6 +1,6 @@ # Task ID: 9 # Title: Vercel ์ž๋™ ๋ฐฐํฌ ์„ค์ • -# Status: pending +# Status: done # Dependencies: 4 # Priority: low # Description: Vercel์„ ์‚ฌ์šฉํ•˜์—ฌ ์ž๋™ ๋ฐฐํฌ ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ•˜๊ณ  ํ™˜๊ฒฝ๋ณ„ ๋ฐฐํฌ์™€ PR ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. diff --git a/.taskmaster/tasks/task_010.txt b/.taskmaster_backup/tasks/task_010.txt similarity index 97% rename from .taskmaster/tasks/task_010.txt rename to .taskmaster_backup/tasks/task_010.txt index 9f191af..d7f4edc 100644 --- a/.taskmaster/tasks/task_010.txt +++ b/.taskmaster_backup/tasks/task_010.txt @@ -1,6 +1,6 @@ # Task ID: 10 # Title: ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ ๊ตฌ์ถ• ๋ฐ ๋ฒˆ๋“ค ์ตœ์ ํ™” -# Status: pending +# Status: done # Dependencies: 8, 9 # Priority: low # Description: Sentry๋ฅผ ์‚ฌ์šฉํ•œ ์—๋Ÿฌ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์„ค์ •ํ•˜๊ณ  ์›นํŒฉ ๋ฒˆ๋“ค ๋ถ„์„์„ ํ†ตํ•ด ๋ฒˆ๋“ค ํฌ๊ธฐ๋ฅผ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค. diff --git a/.taskmaster_backup/tasks/tasks.json b/.taskmaster_backup/tasks/tasks.json new file mode 100644 index 0000000..18a04d2 --- /dev/null +++ b/.taskmaster_backup/tasks/tasks.json @@ -0,0 +1,328 @@ +{ + "tasks": [ + { + "id": 1, + "title": "TypeScript ์„ค์ • ๊ฐ•ํ™” ๋ฐ ํƒ€์ž… ์•ˆ์ „์„ฑ ํ™•๋ณด", + "description": "TypeScript strict ๋ชจ๋“œ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ํ™œ์„ฑํ™”๋˜์—ˆ์œผ๋ฉฐ, ์ฝ”๋“œ๋ฒ ์ด์Šค์˜ ํƒ€์ž… ์•ˆ์ „์„ฑ์ด ๋Œ€ํญ ๊ฐ•ํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ƒˆ๋กœ์šด ํƒ€์ž… ์‹œ์Šคํ…œ ๊ตฌ์กฐ๊ฐ€ ๊ตฌ์ถ•๋˜๊ณ  ํƒ€์ž… ํ’ˆ์งˆ์ด ํฌ๊ฒŒ ํ–ฅ์ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + "status": "done", + "dependencies": [], + "priority": "high", + "details": "1. โœ… ์™„๋ฃŒ๋จ: tsconfig.json์—์„œ strict: true, noImplicitAny: true, strictNullChecks: true, noUnusedLocals: true, noUnusedParameters: true, noFallthroughCasesInSwitch: true ํ™œ์„ฑํ™” 2. โœ… ์™„๋ฃŒ๋จ: ํƒ€์ž… ์—๋Ÿฌ 0๊ฐœ ๋‹ฌ์„ฑ (npx tsc --noEmit ํ†ต๊ณผ) 3. โœ… ์™„๋ฃŒ๋จ: ์ƒˆ๋กœ์šด ํƒ€์ž… ์‹œ์Šคํ…œ ๊ตฌ์กฐ ์ƒ์„ฑ (src/types/common.ts, utils.ts, guards.ts, index.ts) 4. โœ… ์™„๋ฃŒ๋จ: any ํƒ€์ž… ์™„์ „ ์ œ๊ฑฐ ๋ฐ ์ค‘๋ณต ํƒ€์ž… ์ •์˜ ์ œ๊ฑฐ 5. โœ… ์™„๋ฃŒ๋จ: 20+ ํƒ€์ž… ๊ฐ€๋“œ ํ•จ์ˆ˜ ๋ฐ API ์‘๋‹ต ํƒ€์ž… ํ‘œ์ค€ํ™” 6. ๋‚จ์€ ์ž‘์—…: ํƒ€์ž… ์‹œ์Šคํ…œ ์ตœ์ ํ™” ๋ฐ ์ง€์†์  ๊ฐœ์„ ", + "testStrategy": "โœ… TypeScript ์ปดํŒŒ์ผ๋Ÿฌ ์˜ค๋ฅ˜ 0๊ฐœ ๋‹ฌ์„ฑ ์™„๋ฃŒ, โœ… tsc --noEmit ๋ช…๋ น์–ด ํƒ€์ž… ๊ฒ€์‚ฌ ํ†ต๊ณผ ์™„๋ฃŒ, โœ… ๋Ÿฐํƒ€์ž„ ํƒ€์ž… ์•ˆ์ „์„ฑ ํ™•๋ณด ์™„๋ฃŒ, ์ถ”๊ฐ€๋กœ ์ƒˆ๋กœ์šด ํƒ€์ž… ์‹œ์Šคํ…œ์˜ ์•ˆ์ •์„ฑ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ฒ€์ฆ", + "subtasks": [ + { + "id": 1, + "title": "TypeScript strict ๋ชจ๋“œ ์„ค์ • ์™„๋ฃŒ ๊ฒ€์ฆ", + "description": "๋ชจ๋“  strict ์˜ต์…˜์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํ™œ์„ฑํ™”๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ์ปดํŒŒ์ผ ์˜ค๋ฅ˜๊ฐ€ ์—†๋Š”์ง€ ๊ฒ€์ฆ", + "status": "done", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 2, + "title": "์ƒˆ๋กœ์šด ํƒ€์ž… ์‹œ์Šคํ…œ ๊ตฌ์กฐ ์•ˆ์ •์„ฑ ๊ฒ€์ฆ", + "description": "๊ตฌ์ถ•๋œ ํƒ€์ž… ์‹œ์Šคํ…œ์ด ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์—์„œ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•˜๊ณ  ํƒ€์ž… ์ถฉ๋Œ ํ™•์ธ", + "status": "done", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 3, + "title": "ํƒ€์ž… ๊ฐ€๋“œ ํ•จ์ˆ˜ ์„ฑ๋Šฅ ์ตœ์ ํ™”", + "description": "๊ตฌํ˜„๋œ 20+ ํƒ€์ž… ๊ฐ€๋“œ ํ•จ์ˆ˜๋“ค์˜ ์„ฑ๋Šฅ์„ ๊ฒ€ํ† ํ•˜๊ณ  ํ•„์š”์‹œ ์ตœ์ ํ™”", + "status": "done", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 4, + "title": "ํƒ€์ž… ์‹œ์Šคํ…œ ๋ฌธ์„œํ™”", + "description": "์ƒˆ๋กœ์šด ํƒ€์ž… ๊ตฌ์กฐ์™€ ํƒ€์ž… ๊ฐ€๋“œ ํ•จ์ˆ˜๋“ค์˜ ์‚ฌ์šฉ๋ฒ• ๋ฌธ์„œํ™” ๋ฐ ๊ฐ€์ด๋“œ๋ผ์ธ ์ž‘์„ฑ", + "status": "done", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 5, + "title": "์ถ”๊ฐ€ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž… ๊ฐœ๋ฐœ", + "description": "ํ”„๋กœ์ ํŠธ ํŠน์„ฑ์— ๋งž๋Š” ์ปค์Šคํ…€ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž… ๊ฐœ๋ฐœ ๋ฐ ๊ธฐ์กด ํƒ€์ž… ์‹œ์Šคํ…œ ํ™•์žฅ", + "status": "done", + "dependencies": [], + "details": "\nReact Hook ๋ฐ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ํŠนํ™” ํƒ€์ž… ๊ฐœ๋ฐœ ์™„๋ฃŒ:\n\nReact Hook ์ƒํƒœ ๊ด€๋ฆฌ ํƒ€์ž… 4๊ฐœ ๊ตฌํ˜„:\n- HookState: ์ผ๋ฐ˜์ ์ธ Hook ์ƒํƒœ ๊ด€๋ฆฌ\n- MutationState: ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ž‘์—…์šฉ\n- PaginationState: ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ƒํƒœ ๊ด€๋ฆฌ\n- InfiniteScrollState: ๋ฌดํ•œ ์Šคํฌ๋กค ์ƒํƒœ ๊ด€๋ฆฌ\n\n๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ํŠนํ™” ํƒ€์ž… 5๊ฐœ ๊ตฌํ˜„:\n- BudgetCalculation: ์˜ˆ์‚ฐ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ํƒ€์ž…\n- CategoryExpense: ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ง€์ถœ ๋ถ„์„ ํƒ€์ž…\n- MonthlyTrend: ์›”๋ณ„ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ ํƒ€์ž…\n- BudgetAlert: ์˜ˆ์‚ฐ ์•Œ๋ฆผ ์„ค์ • ํƒ€์ž…\n- TransactionFilters: ๊ฑฐ๋ž˜ ๋‚ด์—ญ ๊ฒ€์ƒ‰ ํ•„ํ„ฐ ํƒ€์ž…\n\n๊ณ ๊ธ‰ ์ œ๋„ค๋ฆญ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž… 4๊ฐœ ๊ตฌํ˜„:\n- ConditionalType: ์กฐ๊ฑด๋ถ€ ํƒ€์ž… ๊ฒฐ์ •\n- FunctionOverload: ํ•จ์ˆ˜ ์˜ค๋ฒ„๋กœ๋“œ ์ง€์›\n- DeepKeyof: ๊ฐ์ฒด์˜ ์žฌ๊ท€์  ํ‚ค ๊ฒฝ๋กœ ์ถ”์ถœ\n- UnionToIntersection: ์œ ๋‹ˆ์˜จ ํƒ€์ž…์„ ๊ต์ง‘ํ•ฉ์œผ๋กœ ๋ณ€ํ™˜\n\n๋ชจ๋“  ์ƒˆ๋กœ์šด ํƒ€์ž…์— ๋Œ€์‘ํ•˜๋Š” ํƒ€์ž… ๊ฐ€๋“œ ํ•จ์ˆ˜๋“ค๋„ ํ•จ๊ป˜ ๊ตฌํ˜„ํ•˜์—ฌ ๋Ÿฐํƒ€์ž„ ํƒ€์ž… ์•ˆ์ „์„ฑ ํ™•๋ณด. ์ „์ฒด ํƒ€์ž…๋“ค์ด index.ts์—์„œ export๋˜์–ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด์—์„œ ํ™œ์šฉ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๋กœ ์™„์„ฑ.\n", + "testStrategy": "" + }, + { + "id": 6, + "title": "ํƒ€์ž… ์•ˆ์ „์„ฑ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ ๊ตฌ์ถ•", + "description": "์ง€์†์ ์ธ ํƒ€์ž… ์•ˆ์ „์„ฑ ์œ ์ง€๋ฅผ ์œ„ํ•œ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ฒ€์ฆ ํ”„๋กœ์„ธ์Šค ๊ตฌ์ถ•", + "status": "done", + "dependencies": [], + "details": "\nํƒ€์ž… ์•ˆ์ „์„ฑ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ ๊ตฌ์ถ•์ด ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.\n\nPre-commit ํ›… ์„ค์ •: husky์™€ lint-staged๋ฅผ ์„ค์น˜ํ•˜์—ฌ .husky/pre-commit์—์„œ ์ปค๋ฐ‹ ์ „ ์ž๋™์œผ๋กœ ํƒ€์ž… ๊ฒ€์‚ฌ์™€ ESLint๊ฐ€ ์‹คํ–‰๋˜๋„๋ก ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.\n\nPackage.json ์Šคํฌ๋ฆฝํŠธ ํ™•์žฅ: type-check:watch๋กœ ์‹ค์‹œ๊ฐ„ ํƒ€์ž… ๊ฒ€์‚ฌ ๋ชจ๋‹ˆํ„ฐ๋ง, lint:fix๋กœ ์ž๋™ ESLint ์˜ค๋ฅ˜ ์ˆ˜์ •, check-all๋กœ ์ „์ฒด ๊ฒ€์‚ฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋ฉฐ, lint-staged ์„ค์ •์œผ๋กœ ๋ณ€๊ฒฝ๋œ ํŒŒ์ผ๋งŒ ์„ ๋ณ„์ ์œผ๋กœ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค.\n\nVS Code ์„ค์ • ์ตœ์ ํ™”: TypeScript ์–ธ์–ด ์„œ๋ฒ„ ์„ค์ •, ์ž๋™ import ์ •๋ฆฌ ๋ฐ ํƒ€์ž… ์ฒดํ‚น, ์ €์žฅ ์‹œ ์ž๋™ ESLint ์ˆ˜์ •, ํ•œ๊ตญ์–ด ๋กœ์ผ€์ผ ์„ค์ •์„ ํ†ตํ•ด ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์„ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.\n\nGitHub Actions ์›Œํฌํ”Œ๋กœ์šฐ: .github/workflows/type-check.yml์„ ์ƒ์„ฑํ•˜์—ฌ Node.js 18.x, 20.x ๋งคํŠธ๋ฆญ์Šค ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๊ณ , PR์—์„œ ํƒ€์ž… ๊ฒ€์‚ฌ ์‹คํŒจ ์‹œ ์ž๋™ ๋Œ“๊ธ€์„ ๋‹ฌ๋ฉฐ, ๋นŒ๋“œ ์•„ํ‹ฐํŒฉํŠธ๋ฅผ ์—…๋กœ๋“œํ•˜๋Š” CI/CD ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์ถ•ํ–ˆ์Šต๋‹ˆ๋‹ค.\n\n์ด์ œ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ฝ”๋“œ๋ฅผ ์ปค๋ฐ‹ํ•˜๊ฑฐ๋‚˜ PR์„ ์ƒ์„ฑํ•  ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ํƒ€์ž… ์•ˆ์ „์„ฑ์ด ๊ฒ€์ฆ๋˜์–ด ์ฝ”๋“œ ํ’ˆ์งˆ์ด ์ง€์†์ ์œผ๋กœ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.\n", + "testStrategy": "" + } + ] + }, + { + "id": 2, + "title": "์ฝ”๋“œ ํ’ˆ์งˆ ๊ฐœ์„  ๋ฐ ๋ฆฐํŒ… ์„ค์ •", + "description": "console.log ์ œ๊ฑฐ, ๋นŒ๋“œ ์˜ค๋ฅ˜ ์ˆ˜์ •, ESLint/Prettier ์„ค์ •์„ ํ†ตํ•ด ์ฝ”๋“œ ํ’ˆ์งˆ์„ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.", + "details": "1. ํ”„๋กœ์ ํŠธ ์ „์ฒด์—์„œ console.log 81๊ฐœ ์ œ๊ฑฐ (production์—์„œ๋Š” ์‚ญ์ œ, development์—์„œ๋Š” logger ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ) 2. SupabaseToAppwriteMigration import ์˜ค๋ฅ˜ ์ˆ˜์ • 3. ESLint ๊ทœ์น™ ๊ฐ•ํ™” (@typescript-eslint/recommended, react-hooks/recommended ์ถ”๊ฐ€) 4. Prettier ์„ค์ • ์ถ”๊ฐ€ (.prettierrc, .prettierignore ํŒŒ์ผ ์ƒ์„ฑ) 5. pre-commit hook ์„ค์ •์œผ๋กœ ์ž๋™ ํฌ๋งทํŒ…", + "testStrategy": "ESLint ์˜ค๋ฅ˜ 0๊ฐœ, Prettier ํฌ๋งทํŒ… ์ž๋™ ์ ์šฉ ํ™•์ธ, ๋นŒ๋“œ ์„ฑ๊ณต ํ™•์ธ, ๋ถˆํ•„์š”ํ•œ console.log๊ฐ€ production ๋นŒ๋“œ์— ํฌํ•จ๋˜์ง€ ์•Š๋Š”์ง€ ๊ฒ€์ฆ", + "priority": "high", + "dependencies": [ + 1 + ], + "status": "done", + "subtasks": [] + }, + { + "id": 3, + "title": "ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ณด์•ˆ ๊ฐ•ํ™” ๋ฐ ๊ด€๋ฆฌ ๊ฐœ์„ ", + "description": "API ํ‚ค์˜ ํด๋ผ์ด์–ธํŠธ ๋…ธ์ถœ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ  ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ๋ฅผ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.", + "details": "1. ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœ๋˜์ง€ ๋ง์•„์•ผ ํ•  API ํ‚ค๋“ค์„ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ๋กœ ์ด๋™ 2. .env.example ํŒŒ์ผ ์ƒ์„ฑ์œผ๋กœ ํ•„์š”ํ•œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ฌธ์„œํ™” 3. VITE_๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋งŒ ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœ๋˜๋„๋ก ์ •๋ฆฌ 4. ๋ฏผ๊ฐํ•œ API ํ‚ค๋Š” ์„œ๋ฒ„๋ฆฌ์Šค ํ•จ์ˆ˜๋‚˜ ๋ฐฑ์—”๋“œ์—์„œ๋งŒ ์‚ฌ์šฉ 5. ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ํŒŒ์ผ ๋ถ„๋ฆฌ (.env.local, .env.production)", + "testStrategy": "๋นŒ๋“œ๋œ ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ์—์„œ ๋ฏผ๊ฐํ•œ API ํ‚ค๊ฐ€ ๋…ธ์ถœ๋˜์ง€ ์•Š๋Š”์ง€ ํ™•์ธ, ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋กœ๋“œ๋˜๋Š”์ง€ ๊ฐ ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธ", + "priority": "high", + "dependencies": [], + "status": "done", + "subtasks": [] + }, + { + "id": 4, + "title": "CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•", + "description": "GitHub Actions๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž๋™ ๋นŒ๋“œ, ํ…Œ์ŠคํŠธ, ESLint ๊ฒ€์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.", + "details": "1. .github/workflows/ci.yml ํŒŒ์ผ ์ƒ์„ฑ 2. Node.js ํ™˜๊ฒฝ ์„ค์ • ๋ฐ ์˜์กด์„ฑ ์„ค์น˜ 3. TypeScript ๋นŒ๋“œ ๋ฐ ํƒ€์ž… ๊ฒ€์‚ฌ 4. ESLint ๋ฐ Prettier ๊ฒ€์‚ฌ ์ž๋™ํ™” 5. ํ…Œ์ŠคํŠธ ์‹คํ–‰ (๋‚˜์ค‘์— ์ถ”๊ฐ€๋  ํ…Œ์ŠคํŠธ๋“ค) 6. ๋นŒ๋“œ ์•„ํ‹ฐํŒฉํŠธ ์ƒ์„ฑ ๋ฐ ์ €์žฅ 7. PR์—์„œ ์ž๋™ ๊ฒ€์‚ฌ ์‹คํ–‰", + "testStrategy": "GitHub Actions ์›Œํฌํ”Œ๋กœ์šฐ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์‹คํ–‰๋˜๋Š”์ง€ ํ™•์ธ, PR ์ƒ์„ฑ ์‹œ ์ž๋™ ๊ฒ€์‚ฌ๊ฐ€ ๋™์ž‘ํ•˜๋Š”์ง€ ๊ฒ€์ฆ, ๋นŒ๋“œ ์‹คํŒจ ์‹œ ์ ์ ˆํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ ํ™•์ธ", + "priority": "medium", + "dependencies": [ + 2 + ], + "status": "done", + "subtasks": [] + }, + { + "id": 5, + "title": "์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ Context API์—์„œ Zustand๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜", + "description": "๊ธฐ์กด Context API ๊ธฐ๋ฐ˜ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ Zustand๋กœ ์ „ํ™˜ํ•˜์—ฌ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๋ฅผ ์ค„์ด๊ณ  ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.", + "details": "1. Zustand ์„ค์น˜ ๋ฐ ๊ธฐ๋ณธ ์„ค์ • 2. ๊ธฐ์กด Context ๊ตฌ์กฐ ๋ถ„์„ ๋ฐ Zustand store ์„ค๊ณ„ 3. ์ธ์ฆ ์ƒํƒœ ๊ด€๋ฆฌ store ์ƒ์„ฑ (auth store) 4. ์•ฑ ์ „์ฒด ์ƒํƒœ ๊ด€๋ฆฌ store ์ƒ์„ฑ (app store) 5. ๊ธฐ์กด useContext ํ˜ธ์ถœ์„ zustand store ์‚ฌ์šฉ์œผ๋กœ ๋ณ€๊ฒฝ 6. TypeScript ํƒ€์ž… ์ •์˜ ์ถ”๊ฐ€ 7. DevTools ์—ฐ๋™ ์„ค์ •", + "testStrategy": "์ƒํƒœ ๋ณ€๊ฒฝ์ด ์˜ˆ์ƒ๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ, ์ปดํฌ๋„ŒํŠธ ๋ฆฌ๋ Œ๋”๋ง ํšŸ์ˆ˜ ๊ฐ์†Œ ํ™•์ธ, ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์—์„œ ์ƒํƒœ ์ถ”์  ๊ฐ€๋Šฅ ํ™•์ธ", + "priority": "medium", + "dependencies": [ + 1 + ], + "status": "done", + "subtasks": [] + }, + { + "id": 6, + "title": "TanStack Query๋ฅผ ์‚ฌ์šฉํ•œ ๋ฐ์ดํ„ฐ ํŽ˜์นญ ๊ฐœ์„ ", + "description": "TanStack Query๋ฅผ ๋„์ž…ํ•˜์—ฌ ์ž๋™ ์บ์‹ฑ, ๋™๊ธฐํ™”, ์˜คํ”„๋ผ์ธ ์ง€์›์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.", + "details": "1. @tanstack/react-query ์„ค์น˜ ๋ฐ QueryClient ์„ค์ • 2. API ํ˜ธ์ถœ ํ•จ์ˆ˜๋“ค์„ React Query hooks๋กœ ์ „ํ™˜ 3. ์ž๋™ ์บ์‹ฑ ์ „๋žต ์„ค์ • (staleTime, cacheTime) 4. ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ๊ตฌํ˜„ (optimistic updates) 5. ์˜คํ”„๋ผ์ธ ์ƒํƒœ์—์„œ์˜ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ 6. ๋ฐฑ๊ทธ๋ผ์šด๋“œ refetch ์„ค์ • 7. ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ ์žฌ์‹œ๋„ ๋กœ์ง ๊ตฌํ˜„", + "testStrategy": "๋ฐ์ดํ„ฐ ์บ์‹ฑ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ, ์˜คํ”„๋ผ์ธ ์ƒํƒœ์—์„œ ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ฐ€๋Šฅ ํ™•์ธ, ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ", + "priority": "medium", + "dependencies": [ + 5 + ], + "status": "done", + "subtasks": [] + }, + { + "id": 7, + "title": "ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ค์ • ๋ฐ ํ•ต์‹ฌ ๋กœ์ง ํ…Œ์ŠคํŠธ ์ž‘์„ฑ", + "description": "Vitest์™€ React Testing Library๋ฅผ ์„ค์ •ํ•˜๊ณ  ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ์ฃผ์š” ์‚ฌ์šฉ์ž ํ”Œ๋กœ์šฐ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "details": "1. Vitest ๋ฐ React Testing Library ์„ค์น˜ ๋ฐ ์„ค์ • 2. ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ค์ • ํŒŒ์ผ ์ƒ์„ฑ (vitest.config.ts) 3. ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ 4. ์ฃผ์š” ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ํ…Œ์ŠคํŠธ 5. ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜ ํ…Œ์ŠคํŠธ (๋กœ๊ทธ์ธ, ๋ฐ์ดํ„ฐ ์ž…๋ ฅ ๋“ฑ) 6. API ๋ชจํ‚น ์„ค์ • 7. ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ 80% ๋ชฉํ‘œ ๋‹ฌ์„ฑ", + "testStrategy": "๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผํ•˜๋Š”์ง€ ํ™•์ธ, ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ฆฌํฌํŠธ ์ƒ์„ฑ, CI/CD ํŒŒ์ดํ”„๋ผ์ธ์—์„œ ํ…Œ์ŠคํŠธ ์ž๋™ ์‹คํ–‰ ํ™•์ธ", + "priority": "medium", + "dependencies": [ + 4 + ], + "status": "done", + "subtasks": [] + }, + { + "id": 8, + "title": "React ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ตฌํ˜„", + "description": "React.memo, useMemo, useCallback์„ ์ ์šฉํ•˜๊ณ  ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง์„ ๋ฐฉ์ง€ํ•˜์—ฌ ์•ฑ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.", + "details": "1. React DevTools Profiler๋ฅผ ์‚ฌ์šฉํ•œ ์„ฑ๋Šฅ ๋ถ„์„ 2. ์ž์ฃผ ๋ฆฌ๋ Œ๋”๋ง๋˜๋Š” ์ปดํฌ๋„ŒํŠธ์— React.memo ์ ์šฉ 3. ๊ณ„์‚ฐ ๋น„์šฉ์ด ๋†’์€ ๋กœ์ง์— useMemo ์ ์šฉ 4. ์ฝœ๋ฐฑ ํ•จ์ˆ˜์— useCallback ์ ์šฉ 5. ์„ธ์…˜ ์ฒดํฌ ์ฃผ๊ธฐ๋ฅผ 5์ดˆ์—์„œ 30์ดˆ๋กœ ์กฐ์ • 6. ์ปดํฌ๋„ŒํŠธ ๋ ˆ์ด์ง€ ๋กœ๋”ฉ ๊ตฌํ˜„ (React.lazy, Suspense) 7. ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ๋ฐ ์ง€์—ฐ ๋กœ๋”ฉ", + "testStrategy": "React DevTools์—์„œ ๋ฆฌ๋ Œ๋”๋ง ํšŸ์ˆ˜ ๊ฐ์†Œ ํ™•์ธ, ์•ฑ ๋กœ๋”ฉ ์†๋„ 2๋ฐฐ ํ–ฅ์ƒ ์ธก์ •, ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ตœ์ ํ™” ํ™•์ธ", + "priority": "medium", + "dependencies": [ + 6 + ], + "status": "done", + "subtasks": [] + }, + { + "id": 9, + "title": "Vercel ์ž๋™ ๋ฐฐํฌ ์„ค์ •", + "description": "Vercel์„ ์‚ฌ์šฉํ•˜์—ฌ ์ž๋™ ๋ฐฐํฌ ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ•˜๊ณ  ํ™˜๊ฒฝ๋ณ„ ๋ฐฐํฌ์™€ PR ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.", + "details": "1. Vercel ํ”„๋กœ์ ํŠธ ์—ฐ๊ฒฐ ๋ฐ GitHub ํ†ตํ•ฉ 2. ํ™˜๊ฒฝ๋ณ„ ๋ฐฐํฌ ์„ค์ • (ํ”„๋กœ๋•์…˜, ์Šคํ…Œ์ด์ง•) 3. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ Vercel ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์„ค์ • 4. PR ์ƒ์„ฑ ์‹œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐฐํฌ ์ž๋™ ์ƒ์„ฑ 5. ๋นŒ๋“œ ์ตœ์ ํ™” ์„ค์ • 6. ๋„๋ฉ”์ธ ์—ฐ๊ฒฐ ๋ฐ SSL ์ธ์ฆ์„œ ์„ค์ • 7. ๋ฐฐํฌ ํ›„ ์•Œ๋ฆผ ์„ค์ •", + "testStrategy": "์ž๋™ ๋ฐฐํฌ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ด๋ฃจ์–ด์ง€๋Š”์ง€ ํ™•์ธ, PR ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐฐํฌ ๋™์ž‘ ํ™•์ธ, ํ™˜๊ฒฝ๋ณ„๋กœ ์˜ฌ๋ฐ”๋ฅธ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์ ์šฉ๋˜๋Š”์ง€ ๊ฒ€์ฆ", + "priority": "low", + "dependencies": [ + 4 + ], + "status": "done", + "subtasks": [] + }, + { + "id": 10, + "title": "๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ ๊ตฌ์ถ• ๋ฐ ๋ฒˆ๋“ค ์ตœ์ ํ™”", + "description": "Sentry๋ฅผ ์‚ฌ์šฉํ•œ ์—๋Ÿฌ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์„ค์ •ํ•˜๊ณ  ์›นํŒฉ ๋ฒˆ๋“ค ๋ถ„์„์„ ํ†ตํ•ด ๋ฒˆ๋“ค ํฌ๊ธฐ๋ฅผ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค.", + "details": "1. Sentry ์„ค์น˜ ๋ฐ ์„ค์ • (์—๋Ÿฌ ๋ชจ๋‹ˆํ„ฐ๋ง, ์„ฑ๋Šฅ ์ถ”์ ) 2. Webpack Bundle Analyzer๋ฅผ ์‚ฌ์šฉํ•œ ๋ฒˆ๋“ค ๋ถ„์„ 3. ๋ถˆํ•„์š”ํ•œ ์˜์กด์„ฑ ์ œ๊ฑฐ (74๊ฐœ dependencies ์ •๋ฆฌ) 4. ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… ์ ์šฉ์œผ๋กœ ์ดˆ๊ธฐ ๋กœ๋”ฉ ์ตœ์ ํ™” 5. Tree shaking ์ตœ์ ํ™” 6. ์‚ฌ์šฉ์ž ํ–‰๋™ ๋ถ„์„์„ ์œ„ํ•œ ๊ธฐ๋ณธ ์ด๋ฒคํŠธ ํŠธ๋ž˜ํ‚น 7. ์„ฑ๋Šฅ ์ง€ํ‘œ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌ์„ฑ", + "testStrategy": "Sentry์—์„œ ์—๋Ÿฌ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ˆ˜์ง‘๋˜๋Š”์ง€ ํ™•์ธ, ๋ฒˆ๋“ค ํฌ๊ธฐ 30% ๊ฐ์†Œ ๋‹ฌ์„ฑ ํ™•์ธ, ์•ฑ ๋กœ๋”ฉ ์†๋„ ๊ฐœ์„  ์ธก์ •", + "priority": "low", + "dependencies": [ + 8, + 9 + ], + "status": "done", + "subtasks": [] + } + ], + "metadata": { + "version": "1.0.0", + "created": "2025-01-11", + "lastModified": "2025-01-11", + "project": "์ ค๋ฆฌ์˜ ์ ์žํƒˆ์ถœ ๊ฐœ์„  ํ”„๋กœ์ ํŠธ" + }, + "master": { + "tasks": [ + { + "id": 1, + "title": "TypeScript ์„ค์ • ๊ฐ•ํ™” ๋ฐ ํƒ€์ž… ์•ˆ์ „์„ฑ ํ™•๋ณด", + "description": "tsconfig.json์˜ strict ๋ชจ๋“œ๋ฅผ ์ ์ง„์ ์œผ๋กœ ํ™œ์„ฑํ™”ํ•˜๊ณ  ๊ธฐ์กด any ํƒ€์ž… ์‚ฌ์šฉ์„ ์ œ๊ฑฐํ•˜์—ฌ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ํ™•๋ณดํ•ฉ๋‹ˆ๋‹ค.", + "details": "1. tsconfig.json์—์„œ strict: true, noImplicitAny: true, strictNullChecks: true ํ™œ์„ฑํ™” 2. ๊ธฐ์กด ์ฝ”๋“œ์—์„œ any ํƒ€์ž… ์‚ฌ์šฉ ๋ถ€๋ถ„ ์ฐพ์•„์„œ ์ ์ ˆํ•œ ํƒ€์ž…์œผ๋กœ ๋ณ€๊ฒฝ 3. ํƒ€์ž… ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋‹จ๊ณ„์ ์œผ๋กœ ์ˆ˜์ • 4. ์ปดํฌ๋„ŒํŠธ props์™€ state์— ๋Œ€ํ•œ ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜ 5. API ์‘๋‹ต ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ํƒ€์ž… ์ •์˜ ์ถ”๊ฐ€", + "testStrategy": "TypeScript ์ปดํŒŒ์ผ๋Ÿฌ ์˜ค๋ฅ˜ 0๊ฐœ ๋‹ฌ์„ฑ, tsc --noEmit ๋ช…๋ น์–ด๋กœ ํƒ€์ž… ๊ฒ€์‚ฌ ํ†ต๊ณผ ํ™•์ธ, IDE์—์„œ ํƒ€์ž… ์ถ”๋ก ์ด ์ •ํ™•ํžˆ ์ž‘๋™ํ•˜๋Š”์ง€ ๊ฒ€์ฆ", + "priority": "high", + "dependencies": [], + "status": "done", + "subtasks": [] + }, + { + "id": 2, + "title": "์ฝ”๋“œ ํ’ˆ์งˆ ๊ฐœ์„  ๋ฐ ๋ฆฐํŒ… ์„ค์ •", + "description": "console.log ์ œ๊ฑฐ, ๋นŒ๋“œ ์˜ค๋ฅ˜ ์ˆ˜์ •, ESLint/Prettier ์„ค์ •์„ ํ†ตํ•ด ์ฝ”๋“œ ํ’ˆ์งˆ์„ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.", + "details": "1. ํ”„๋กœ์ ํŠธ ์ „์ฒด์—์„œ console.log 81๊ฐœ ์ œ๊ฑฐ (production์—์„œ๋Š” ์‚ญ์ œ, development์—์„œ๋Š” logger ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ) 2. SupabaseToAppwriteMigration import ์˜ค๋ฅ˜ ์ˆ˜์ • 3. ESLint ๊ทœ์น™ ๊ฐ•ํ™” (@typescript-eslint/recommended, react-hooks/recommended ์ถ”๊ฐ€) 4. Prettier ์„ค์ • ์ถ”๊ฐ€ (.prettierrc, .prettierignore ํŒŒ์ผ ์ƒ์„ฑ) 5. pre-commit hook ์„ค์ •์œผ๋กœ ์ž๋™ ํฌ๋งทํŒ…", + "testStrategy": "ESLint ์˜ค๋ฅ˜ 0๊ฐœ, Prettier ํฌ๋งทํŒ… ์ž๋™ ์ ์šฉ ํ™•์ธ, ๋นŒ๋“œ ์„ฑ๊ณต ํ™•์ธ, ๋ถˆํ•„์š”ํ•œ console.log๊ฐ€ production ๋นŒ๋“œ์— ํฌํ•จ๋˜์ง€ ์•Š๋Š”์ง€ ๊ฒ€์ฆ", + "priority": "high", + "dependencies": [ + 1 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 3, + "title": "ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ณด์•ˆ ๊ฐ•ํ™” ๋ฐ ๊ด€๋ฆฌ ๊ฐœ์„ ", + "description": "API ํ‚ค์˜ ํด๋ผ์ด์–ธํŠธ ๋…ธ์ถœ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ  ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ๋ฅผ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.", + "details": "1. ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœ๋˜์ง€ ๋ง์•„์•ผ ํ•  API ํ‚ค๋“ค์„ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ๋กœ ์ด๋™ 2. .env.example ํŒŒ์ผ ์ƒ์„ฑ์œผ๋กœ ํ•„์š”ํ•œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ฌธ์„œํ™” 3. VITE_๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋งŒ ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœ๋˜๋„๋ก ์ •๋ฆฌ 4. ๋ฏผ๊ฐํ•œ API ํ‚ค๋Š” ์„œ๋ฒ„๋ฆฌ์Šค ํ•จ์ˆ˜๋‚˜ ๋ฐฑ์—”๋“œ์—์„œ๋งŒ ์‚ฌ์šฉ 5. ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ํŒŒ์ผ ๋ถ„๋ฆฌ (.env.local, .env.production)", + "testStrategy": "๋นŒ๋“œ๋œ ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ์—์„œ ๋ฏผ๊ฐํ•œ API ํ‚ค๊ฐ€ ๋…ธ์ถœ๋˜์ง€ ์•Š๋Š”์ง€ ํ™•์ธ, ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋กœ๋“œ๋˜๋Š”์ง€ ๊ฐ ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธ", + "priority": "high", + "dependencies": [], + "status": "pending", + "subtasks": [] + }, + { + "id": 4, + "title": "CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•", + "description": "GitHub Actions๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž๋™ ๋นŒ๋“œ, ํ…Œ์ŠคํŠธ, ESLint ๊ฒ€์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.", + "details": "1. .github/workflows/ci.yml ํŒŒ์ผ ์ƒ์„ฑ 2. Node.js ํ™˜๊ฒฝ ์„ค์ • ๋ฐ ์˜์กด์„ฑ ์„ค์น˜ 3. TypeScript ๋นŒ๋“œ ๋ฐ ํƒ€์ž… ๊ฒ€์‚ฌ 4. ESLint ๋ฐ Prettier ๊ฒ€์‚ฌ ์ž๋™ํ™” 5. ํ…Œ์ŠคํŠธ ์‹คํ–‰ (๋‚˜์ค‘์— ์ถ”๊ฐ€๋  ํ…Œ์ŠคํŠธ๋“ค) 6. ๋นŒ๋“œ ์•„ํ‹ฐํŒฉํŠธ ์ƒ์„ฑ ๋ฐ ์ €์žฅ 7. PR์—์„œ ์ž๋™ ๊ฒ€์‚ฌ ์‹คํ–‰", + "testStrategy": "GitHub Actions ์›Œํฌํ”Œ๋กœ์šฐ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์‹คํ–‰๋˜๋Š”์ง€ ํ™•์ธ, PR ์ƒ์„ฑ ์‹œ ์ž๋™ ๊ฒ€์‚ฌ๊ฐ€ ๋™์ž‘ํ•˜๋Š”์ง€ ๊ฒ€์ฆ, ๋นŒ๋“œ ์‹คํŒจ ์‹œ ์ ์ ˆํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ ํ™•์ธ", + "priority": "medium", + "dependencies": [ + 2 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 5, + "title": "์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ Context API์—์„œ Zustand๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜", + "description": "๊ธฐ์กด Context API ๊ธฐ๋ฐ˜ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ Zustand๋กœ ์ „ํ™˜ํ•˜์—ฌ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๋ฅผ ์ค„์ด๊ณ  ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.", + "details": "1. Zustand ์„ค์น˜ ๋ฐ ๊ธฐ๋ณธ ์„ค์ • 2. ๊ธฐ์กด Context ๊ตฌ์กฐ ๋ถ„์„ ๋ฐ Zustand store ์„ค๊ณ„ 3. ์ธ์ฆ ์ƒํƒœ ๊ด€๋ฆฌ store ์ƒ์„ฑ (auth store) 4. ์•ฑ ์ „์ฒด ์ƒํƒœ ๊ด€๋ฆฌ store ์ƒ์„ฑ (app store) 5. ๊ธฐ์กด useContext ํ˜ธ์ถœ์„ zustand store ์‚ฌ์šฉ์œผ๋กœ ๋ณ€๊ฒฝ 6. TypeScript ํƒ€์ž… ์ •์˜ ์ถ”๊ฐ€ 7. DevTools ์—ฐ๋™ ์„ค์ •", + "testStrategy": "์ƒํƒœ ๋ณ€๊ฒฝ์ด ์˜ˆ์ƒ๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ, ์ปดํฌ๋„ŒํŠธ ๋ฆฌ๋ Œ๋”๋ง ํšŸ์ˆ˜ ๊ฐ์†Œ ํ™•์ธ, ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์—์„œ ์ƒํƒœ ์ถ”์  ๊ฐ€๋Šฅ ํ™•์ธ", + "priority": "medium", + "dependencies": [ + 1 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 6, + "title": "TanStack Query๋ฅผ ์‚ฌ์šฉํ•œ ๋ฐ์ดํ„ฐ ํŽ˜์นญ ๊ฐœ์„ ", + "description": "TanStack Query๋ฅผ ๋„์ž…ํ•˜์—ฌ ์ž๋™ ์บ์‹ฑ, ๋™๊ธฐํ™”, ์˜คํ”„๋ผ์ธ ์ง€์›์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.", + "details": "1. @tanstack/react-query ์„ค์น˜ ๋ฐ QueryClient ์„ค์ • 2. API ํ˜ธ์ถœ ํ•จ์ˆ˜๋“ค์„ React Query hooks๋กœ ์ „ํ™˜ 3. ์ž๋™ ์บ์‹ฑ ์ „๋žต ์„ค์ • (staleTime, cacheTime) 4. ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ๊ตฌํ˜„ (optimistic updates) 5. ์˜คํ”„๋ผ์ธ ์ƒํƒœ์—์„œ์˜ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ 6. ๋ฐฑ๊ทธ๋ผ์šด๋“œ refetch ์„ค์ • 7. ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ ์žฌ์‹œ๋„ ๋กœ์ง ๊ตฌํ˜„", + "testStrategy": "๋ฐ์ดํ„ฐ ์บ์‹ฑ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ, ์˜คํ”„๋ผ์ธ ์ƒํƒœ์—์„œ ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ฐ€๋Šฅ ํ™•์ธ, ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ", + "priority": "medium", + "dependencies": [ + 5 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 7, + "title": "ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ค์ • ๋ฐ ํ•ต์‹ฌ ๋กœ์ง ํ…Œ์ŠคํŠธ ์ž‘์„ฑ", + "description": "Vitest์™€ React Testing Library๋ฅผ ์„ค์ •ํ•˜๊ณ  ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ์ฃผ์š” ์‚ฌ์šฉ์ž ํ”Œ๋กœ์šฐ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.", + "details": "1. Vitest ๋ฐ React Testing Library ์„ค์น˜ ๋ฐ ์„ค์ • 2. ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ค์ • ํŒŒ์ผ ์ƒ์„ฑ (vitest.config.ts) 3. ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ 4. ์ฃผ์š” ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ํ…Œ์ŠคํŠธ 5. ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜ ํ…Œ์ŠคํŠธ (๋กœ๊ทธ์ธ, ๋ฐ์ดํ„ฐ ์ž…๋ ฅ ๋“ฑ) 6. API ๋ชจํ‚น ์„ค์ • 7. ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ 80% ๋ชฉํ‘œ ๋‹ฌ์„ฑ", + "testStrategy": "๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผํ•˜๋Š”์ง€ ํ™•์ธ, ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ฆฌํฌํŠธ ์ƒ์„ฑ, CI/CD ํŒŒ์ดํ”„๋ผ์ธ์—์„œ ํ…Œ์ŠคํŠธ ์ž๋™ ์‹คํ–‰ ํ™•์ธ", + "priority": "medium", + "dependencies": [ + 4 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 8, + "title": "React ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ตฌํ˜„", + "description": "React.memo, useMemo, useCallback์„ ์ ์šฉํ•˜๊ณ  ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง์„ ๋ฐฉ์ง€ํ•˜์—ฌ ์•ฑ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.", + "details": "1. React DevTools Profiler๋ฅผ ์‚ฌ์šฉํ•œ ์„ฑ๋Šฅ ๋ถ„์„ 2. ์ž์ฃผ ๋ฆฌ๋ Œ๋”๋ง๋˜๋Š” ์ปดํฌ๋„ŒํŠธ์— React.memo ์ ์šฉ 3. ๊ณ„์‚ฐ ๋น„์šฉ์ด ๋†’์€ ๋กœ์ง์— useMemo ์ ์šฉ 4. ์ฝœ๋ฐฑ ํ•จ์ˆ˜์— useCallback ์ ์šฉ 5. ์„ธ์…˜ ์ฒดํฌ ์ฃผ๊ธฐ๋ฅผ 5์ดˆ์—์„œ 30์ดˆ๋กœ ์กฐ์ • 6. ์ปดํฌ๋„ŒํŠธ ๋ ˆ์ด์ง€ ๋กœ๋”ฉ ๊ตฌํ˜„ (React.lazy, Suspense) 7. ์ด๋ฏธ์ง€ ์ตœ์ ํ™” ๋ฐ ์ง€์—ฐ ๋กœ๋”ฉ", + "testStrategy": "React DevTools์—์„œ ๋ฆฌ๋ Œ๋”๋ง ํšŸ์ˆ˜ ๊ฐ์†Œ ํ™•์ธ, ์•ฑ ๋กœ๋”ฉ ์†๋„ 2๋ฐฐ ํ–ฅ์ƒ ์ธก์ •, ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ตœ์ ํ™” ํ™•์ธ", + "priority": "medium", + "dependencies": [ + 6 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 9, + "title": "Vercel ์ž๋™ ๋ฐฐํฌ ์„ค์ •", + "description": "Vercel์„ ์‚ฌ์šฉํ•˜์—ฌ ์ž๋™ ๋ฐฐํฌ ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ•˜๊ณ  ํ™˜๊ฒฝ๋ณ„ ๋ฐฐํฌ์™€ PR ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.", + "details": "1. Vercel ํ”„๋กœ์ ํŠธ ์—ฐ๊ฒฐ ๋ฐ GitHub ํ†ตํ•ฉ 2. ํ™˜๊ฒฝ๋ณ„ ๋ฐฐํฌ ์„ค์ • (ํ”„๋กœ๋•์…˜, ์Šคํ…Œ์ด์ง•) 3. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ Vercel ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์„ค์ • 4. PR ์ƒ์„ฑ ์‹œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐฐํฌ ์ž๋™ ์ƒ์„ฑ 5. ๋นŒ๋“œ ์ตœ์ ํ™” ์„ค์ • 6. ๋„๋ฉ”์ธ ์—ฐ๊ฒฐ ๋ฐ SSL ์ธ์ฆ์„œ ์„ค์ • 7. ๋ฐฐํฌ ํ›„ ์•Œ๋ฆผ ์„ค์ •", + "testStrategy": "์ž๋™ ๋ฐฐํฌ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ด๋ฃจ์–ด์ง€๋Š”์ง€ ํ™•์ธ, PR ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐฐํฌ ๋™์ž‘ ํ™•์ธ, ํ™˜๊ฒฝ๋ณ„๋กœ ์˜ฌ๋ฐ”๋ฅธ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์ ์šฉ๋˜๋Š”์ง€ ๊ฒ€์ฆ", + "priority": "low", + "dependencies": [ + 4 + ], + "status": "pending", + "subtasks": [] + }, + { + "id": 10, + "title": "๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ ๊ตฌ์ถ• ๋ฐ ๋ฒˆ๋“ค ์ตœ์ ํ™”", + "description": "Sentry๋ฅผ ์‚ฌ์šฉํ•œ ์—๋Ÿฌ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์„ค์ •ํ•˜๊ณ  ์›นํŒฉ ๋ฒˆ๋“ค ๋ถ„์„์„ ํ†ตํ•ด ๋ฒˆ๋“ค ํฌ๊ธฐ๋ฅผ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค.", + "details": "1. Sentry ์„ค์น˜ ๋ฐ ์„ค์ • (์—๋Ÿฌ ๋ชจ๋‹ˆํ„ฐ๋ง, ์„ฑ๋Šฅ ์ถ”์ ) 2. Webpack Bundle Analyzer๋ฅผ ์‚ฌ์šฉํ•œ ๋ฒˆ๋“ค ๋ถ„์„ 3. ๋ถˆํ•„์š”ํ•œ ์˜์กด์„ฑ ์ œ๊ฑฐ (74๊ฐœ dependencies ์ •๋ฆฌ) 4. ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… ์ ์šฉ์œผ๋กœ ์ดˆ๊ธฐ ๋กœ๋”ฉ ์ตœ์ ํ™” 5. Tree shaking ์ตœ์ ํ™” 6. ์‚ฌ์šฉ์ž ํ–‰๋™ ๋ถ„์„์„ ์œ„ํ•œ ๊ธฐ๋ณธ ์ด๋ฒคํŠธ ํŠธ๋ž˜ํ‚น 7. ์„ฑ๋Šฅ ์ง€ํ‘œ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌ์„ฑ", + "testStrategy": "Sentry์—์„œ ์—๋Ÿฌ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ˆ˜์ง‘๋˜๋Š”์ง€ ํ™•์ธ, ๋ฒˆ๋“ค ํฌ๊ธฐ 30% ๊ฐ์†Œ ๋‹ฌ์„ฑ ํ™•์ธ, ์•ฑ ๋กœ๋”ฉ ์†๋„ ๊ฐœ์„  ์ธก์ •", + "priority": "low", + "dependencies": [ + 8, + 9 + ], + "status": "pending", + "subtasks": [] + } + ], + "metadata": { + "created": "2025-07-11T21:00:35.577Z", + "updated": "2025-07-12T02:22:34.383Z", + "description": "Tasks for master context" + } + } +} \ No newline at end of file diff --git a/.taskmaster_backup/templates/example_prd.txt b/.taskmaster_backup/templates/example_prd.txt new file mode 100644 index 0000000..194114d --- /dev/null +++ b/.taskmaster_backup/templates/example_prd.txt @@ -0,0 +1,47 @@ + +# Overview +[Provide a high-level overview of your product here. Explain what problem it solves, who it's for, and why it's valuable.] + +# Core Features +[List and describe the main features of your product. For each feature, include: +- What it does +- Why it's important +- How it works at a high level] + +# User Experience +[Describe the user journey and experience. Include: +- User personas +- Key user flows +- UI/UX considerations] + + +# Technical Architecture +[Outline the technical implementation details: +- System components +- Data models +- APIs and integrations +- Infrastructure requirements] + +# Development Roadmap +[Break down the development process into phases: +- MVP requirements +- Future enhancements +- Do not think about timelines whatsoever -- all that matters is scope and detailing exactly what needs to be build in each phase so it can later be cut up into tasks] + +# Logical Dependency Chain +[Define the logical order of development: +- Which features need to be built first (foundation) +- Getting as quickly as possible to something usable/visible front end that works +- Properly pacing and scoping each feature so it is atomic but can also be built upon and improved as development approaches] + +# Risks and Mitigations +[Identify potential risks and how they'll be addressed: +- Technical challenges +- Figuring out the MVP that we can build upon +- Resource constraints] + +# Appendix +[Include any additional information: +- Research findings +- Technical specifications] + \ No newline at end of file diff --git a/PROJECT_IMPROVEMENT_PLAN.md b/PROJECT_IMPROVEMENT_PLAN.md index fb37024..2249b45 100644 --- a/PROJECT_IMPROVEMENT_PLAN.md +++ b/PROJECT_IMPROVEMENT_PLAN.md @@ -1,6 +1,7 @@ # ์ ค๋ฆฌ์˜ ์ ์žํƒˆ์ถœ ํ”„๋กœ์ ํŠธ ๊ฐœ์„  ๊ณ„ํš ## ๋ชฉ์ฐจ + 1. [ํ”„๋กœ์ ํŠธ ํ˜„ํ™ฉ ๋ถ„์„](#ํ”„๋กœ์ ํŠธ-ํ˜„ํ™ฉ-๋ถ„์„) 2. [์ฃผ์š” ๊ฐœ์„ ์‚ฌํ•ญ](#์ฃผ์š”-๊ฐœ์„ ์‚ฌํ•ญ) 3. [๊ธฐ์ˆ  ์Šคํƒ ๊ฐœ์„  ๊ณ„ํš](#๊ธฐ์ˆ -์Šคํƒ-๊ฐœ์„ -๊ณ„ํš) @@ -12,12 +13,14 @@ ## ํ”„๋กœ์ ํŠธ ํ˜„ํ™ฉ ๋ถ„์„ ### ํ”„๋กœ์ ํŠธ ๊ฐœ์š” + - **ํ”„๋กœ์ ํŠธ๋ช…**: ์ ค๋ฆฌ์˜ ์ ์žํƒˆ์ถœ (Zellyy Finance) - **๋ชฉ์ **: ๊ฐœ์ธ ์žฌ๋ฌด/์˜ˆ์‚ฐ ๊ด€๋ฆฌ ๋ชจ๋ฐ”์ผ ์•ฑ - **ํ”Œ๋žซํผ**: ์›น + iOS/Android (Capacitor) - **ํ˜„์žฌ ์ƒํƒœ**: Supabase โ†’ Appwrite ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ ### ํ˜„์žฌ ๊ธฐ์ˆ  ์Šคํƒ + ``` Frontend: React 18 + TypeScript + Vite UI: Tailwind CSS + shadcn/ui (๋‰ด๋ชจํ”ฝ ๋””์ž์ธ) @@ -31,6 +34,7 @@ UI: Tailwind CSS + shadcn/ui (๋‰ด๋ชจํ”ฝ ๋””์ž์ธ) ### ๐Ÿ”ด ๊ธด๊ธ‰ ๊ฐœ์„  ํ•„์š” (๋ณด์•ˆ/์•ˆ์ •์„ฑ) #### 1. TypeScript ์„ค์ • ๊ฐ•ํ™” + ```json // tsconfig.json ์ˆ˜์ • ํ•„์š” { @@ -45,27 +49,32 @@ UI: Tailwind CSS + shadcn/ui (๋‰ด๋ชจํ”ฝ ๋””์ž์ธ) ``` #### 2. ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ถ”๊ฐ€ + - ํ˜„์žฌ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ „๋ฌด - Vitest + React Testing Library ๋„์ž… ํ•„์š” - ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๋ถ€ํ„ฐ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ #### 3. ๋ณด์•ˆ ์ทจ์•ฝ์  ํ•ด๊ฒฐ + - API ํ‚ค ํด๋ผ์ด์–ธํŠธ ๋…ธ์ถœ ๋ฌธ์ œ - ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ ๊ฐœ์„  ํ•„์š” ### ๐ŸŸก ์ค‘์š” ๊ฐœ์„ ์‚ฌํ•ญ (์„ฑ๋Šฅ/ํ’ˆ์งˆ) #### 4. React ์„ฑ๋Šฅ ์ตœ์ ํ™” + - React.memo, useMemo, useCallback ํ™œ์šฉ ๋ถ€์กฑ - ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง ๋ฐฉ์ง€ ํ•„์š” - ์„ธ์…˜ ์ฒดํฌ ์ฃผ๊ธฐ ์ตœ์ ํ™” (ํ˜„์žฌ 5์ดˆ โ†’ 30์ดˆ) #### 5. ์ฝ”๋“œ ํ’ˆ์งˆ ๊ฐœ์„  + - console.log 81๊ฐœ ์ œ๊ฑฐ - ๋นŒ๋“œ ์˜ค๋ฅ˜ ์ˆ˜์ • - ESLint ๊ทœ์น™ ๊ฐ•ํ™” #### 6. ๋ฒˆ๋“ค ํฌ๊ธฐ ์ตœ์ ํ™” + - 74๊ฐœ dependencies ์ •๋ฆฌ - ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ํŒจํ‚ค์ง€ ์ œ๊ฑฐ - ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… ์ ์šฉ @@ -73,6 +82,7 @@ UI: Tailwind CSS + shadcn/ui (๋‰ด๋ชจํ”ฝ ๋””์ž์ธ) ### ๐ŸŸข ์‚ฌ์šฉ์„ฑ ๊ฐœ์„ ์‚ฌํ•ญ #### 7. UX ๊ฐœ์„  + - ์Šค์ผˆ๋ ˆํ†ค UI ํ™œ์šฉ๋„ ์ฆ๋Œ€ - ์ ‘๊ทผ์„ฑ ๊ฐœ์„  (ARIA ๋ผ๋ฒจ, ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜) - ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์‚ฌ์šฉ์ž ์นœํ™”์ ์œผ๋กœ ๊ฐœ์„  @@ -82,25 +92,29 @@ UI: Tailwind CSS + shadcn/ui (๋‰ด๋ชจํ”ฝ ๋””์ž์ธ) ### ์ƒํƒœ ๊ด€๋ฆฌ: Context API โ†’ Zustand **ํ˜„์žฌ (Context API)** + ```typescript const BudgetContext = createContext(); // ๋ณต์žกํ•œ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ ``` **๊ฐœ์„  ํ›„ (Zustand)** + ```typescript const useBudgetStore = create((set) => ({ budgets: [], transactions: [], - addTransaction: (transaction) => set((state) => ({ - transactions: [...state.transactions, transaction] - })) + addTransaction: (transaction) => + set((state) => ({ + transactions: [...state.transactions, transaction], + })), })); ``` ### ๋ฐ์ดํ„ฐ ํŽ˜์นญ: ์ˆ˜๋™ โ†’ TanStack Query **ํ˜„์žฌ** + ```typescript useEffect(() => { fetchTransactions().then(setTransactions); @@ -108,19 +122,22 @@ useEffect(() => { ``` **๊ฐœ์„  ํ›„** + ```typescript const { data, isLoading, error } = useQuery({ - queryKey: ['transactions'], + queryKey: ["transactions"], queryFn: fetchTransactions, staleTime: 5 * 60 * 1000, }); ``` ### ์ฐจํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ: Recharts โ†’ Chart.js + - ๋ฒˆ๋“ค ํฌ๊ธฐ ๊ฐ์†Œ (300KB โ†’ 100KB) - ๋ชจ๋ฐ”์ผ ์„ฑ๋Šฅ ํ–ฅ์ƒ ### ์ถ”๊ฐ€ ๋„์ž… ํ•„์š” ๋„๊ตฌ + ```json { "devDependencies": { @@ -136,6 +153,7 @@ const { data, isLoading, error } = useQuery({ ## ์ธ์ฆ ์‹œ์Šคํ…œ ๊ฐœ์„  ### ํ˜„์žฌ: Appwrite Auth + - ๋ชจ๋“  ์ธ์ฆ ๋กœ์ง ์ง์ ‘ ๊ตฌํ˜„ - ์†Œ์…œ ๋กœ๊ทธ์ธ ๊ตฌํ˜„ ๋ณต์žก - ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์–ด๋ ค์›€ @@ -143,6 +161,7 @@ const { data, isLoading, error } = useQuery({ ### ๊ถŒ์žฅ: Clerk + Supabase ์กฐํ•ฉ #### Clerk (์ธ์ฆ ์ „๋ฌธ) + ```typescript import { useUser, SignIn } from '@clerk/clerk-react'; @@ -154,18 +173,20 @@ function App() { ``` **์žฅ์ :** + - ์นด์นด์˜ค/๋„ค์ด๋ฒ„ ๋กœ๊ทธ์ธ ์ฆ‰์‹œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ - 2FA, ์ƒ์ฒด์ธ์ฆ ๋‚ด์žฅ - 10,000๋ช…๊นŒ์ง€ ๋ฌด๋ฃŒ - ๋›ฐ์–ด๋‚œ UX/UI ์ปดํฌ๋„ŒํŠธ #### Supabase (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค) + ```typescript // Clerk JWT๋ฅผ Supabase์— ์ „๋‹ฌ const supabase = createClient(url, key, { global: { headers: async () => { - const token = await getToken({ template: 'supabase' }); + const token = await getToken({ template: "supabase" }); return { Authorization: `Bearer ${token}` }; }, }, @@ -173,6 +194,7 @@ const supabase = createClient(url, key, { ``` ### ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ณ„ํš + 1. Supabase ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ ๋ฐ ์Šคํ‚ค๋งˆ ์„ค์ • 2. Clerk ํ†ตํ•ฉ ๋ฐ JWT ํ…œํ”Œ๋ฆฟ ๊ตฌ์„ฑ 3. ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (Appwrite โ†’ Supabase) @@ -183,6 +205,7 @@ const supabase = createClient(url, key, { ### GitHub Actions ์›Œํฌํ”Œ๋กœ์šฐ #### 1. ์ง€์†์  ํ†ตํ•ฉ (CI) + ```yaml # .github/workflows/ci.yml name: CI @@ -200,8 +223,8 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: '18' - cache: 'npm' + node-version: "18" + cache: "npm" - run: npm ci - run: npm run lint - run: npx tsc --noEmit @@ -210,6 +233,7 @@ jobs: ``` #### 2. ์ž๋™ ๋ฐฐํฌ (CD) + ```yaml # .github/workflows/deploy.yml name: Deploy @@ -226,10 +250,11 @@ jobs: - uses: amondnet/vercel-action@v25 with: vercel-token: ${{ secrets.VERCEL_TOKEN }} - vercel-args: '--prod' + vercel-args: "--prod" ``` ### ๋ธŒ๋žœ์น˜ ์ „๋žต + ``` main โ†’ ํ”„๋กœ๋•์…˜ (์ž๋™ ๋ฐฐํฌ) develop โ†’ ์Šคํ…Œ์ด์ง• (์ž๋™ ๋ฐฐํฌ) @@ -237,6 +262,7 @@ feature/* โ†’ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ (PR ์ฒดํฌ๋งŒ) ``` ### ์ถ”๊ฐ€ ํ†ตํ•ฉ + - SonarCloud: ์ฝ”๋“œ ํ’ˆ์งˆ ๋ถ„์„ - Codecov: ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ์ถ”์  - Sentry: ์—๋Ÿฌ ๋ชจ๋‹ˆํ„ฐ๋ง @@ -246,6 +272,7 @@ feature/* โ†’ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ (PR ์ฒดํฌ๋งŒ) ### ๊ถŒ์žฅ: Task Master AI + Linear ํ•˜์ด๋ธŒ๋ฆฌ๋“œ #### Task Master AI (์ด๋ฏธ ์„ค์ •๋จ) + ```bash # AI ๊ธฐ๋ฐ˜ ํƒœ์Šคํฌ ์ƒ์„ฑ task-master parse-prd .taskmaster/docs/prd.txt --research @@ -258,11 +285,13 @@ task-master set-status --id=1.2 --status=done ``` #### Linear (์„ ํƒ์  - ์‹œ๊ฐ์  ๊ด€๋ฆฌ) + - ๊ฐœ๋ฐœ์ž ์นœํ™”์  UI - GitHub ์ž๋™ ํ†ตํ•ฉ - ๋ฌด๋ฃŒ ํ‹ฐ์–ด ์ถฉ๋ถ„ ### ์›Œํฌํ”Œ๋กœ์šฐ + 1. Task Master๋กœ PRD ํŒŒ์‹ฑ โ†’ ํƒœ์Šคํฌ ์ž๋™ ์ƒ์„ฑ 2. Linear๋กœ ์‹œ๊ฐ์  ๊ด€๋ฆฌ (ํ•„์š”์‹œ) 3. GitHub PR๊ณผ ์ž๋™ ์—ฐ๊ฒฐ @@ -271,6 +300,7 @@ task-master set-status --id=1.2 --status=done ## ์‹คํ–‰ ๋กœ๋“œ๋งต ### Phase 1: ์ฆ‰์‹œ ์‹œ์ž‘ (1์ฃผ์ผ) + - [ ] TypeScript strict ๋ชจ๋“œ ์ ์ง„์  ํ™œ์„ฑํ™” - [ ] console.log ์ œ๊ฑฐ ๋ฐ ๋นŒ๋“œ ์˜ค๋ฅ˜ ์ˆ˜์ • - [ ] ESLint + Prettier ์„ค์ • @@ -278,6 +308,7 @@ task-master set-status --id=1.2 --status=done - [ ] ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ณด์•ˆ ๊ฐ•ํ™” ### Phase 2: ํ•ต์‹ฌ ๊ฐœ์„  (2-3์ฃผ) + - [ ] Zustand๋กœ ์ƒํƒœ ๊ด€๋ฆฌ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ - [ ] TanStack Query ๋„์ž… - [ ] ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ํ…Œ์ŠคํŠธ ์ž‘์„ฑ @@ -285,6 +316,7 @@ task-master set-status --id=1.2 --status=done - [ ] Vercel ์ž๋™ ๋ฐฐํฌ ์„ค์ • ### Phase 3: ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ (1๊ฐœ์›”) + - [ ] Clerk ์ธ์ฆ ์‹œ์Šคํ…œ ๋„์ž… - [ ] Supabase ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ - [ ] Chart.js๋กœ ์ฐจํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ต์ฒด @@ -292,6 +324,7 @@ task-master set-status --id=1.2 --status=done - [ ] ์ ‘๊ทผ์„ฑ ๊ฐœ์„  ### Phase 4: ์ตœ์ ํ™” (2๊ฐœ์›”) + - [ ] ๋ฒˆ๋“ค ํฌ๊ธฐ ์ตœ์ ํ™” - [ ] ๋ชจ๋ฐ”์ผ ๋นŒ๋“œ ์ž๋™ํ™” - [ ] ์—๋Ÿฌ ๋ชจ๋‹ˆํ„ฐ๋ง (Sentry) ๊ตฌ์ถ• @@ -300,16 +333,19 @@ task-master set-status --id=1.2 --status=done ## ์˜ˆ์ƒ ํšจ๊ณผ ### ๊ฐœ๋ฐœ ํšจ์œจ์„ฑ + - ์ฝ”๋“œ ํ’ˆ์งˆ ํ–ฅ์ƒ์œผ๋กœ ๋ฒ„๊ทธ ๊ฐ์†Œ - CI/CD๋กœ ๋ฐฐํฌ ์‹œ๊ฐ„ 90% ๋‹จ์ถ• - ํƒ€์ž… ์•ˆ์ „์„ฑ์œผ๋กœ ๋Ÿฐํƒ€์ž„ ์˜ค๋ฅ˜ ๋ฐฉ์ง€ ### ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ + - ์„ฑ๋Šฅ ๊ฐœ์„ ์œผ๋กœ ์•ฑ ์†๋„ 2๋ฐฐ ํ–ฅ์ƒ - ์นด์นด์˜ค/๋„ค์ด๋ฒ„ ๋กœ๊ทธ์ธ์œผ๋กœ ๊ฐ€์ž…๋ฅ  ์ฆ๊ฐ€ - ์•ˆ์ •์„ฑ ํ–ฅ์ƒ์œผ๋กœ ์‚ฌ์šฉ์ž ๋งŒ์กฑ๋„ ๊ฐœ์„  ### ์œ ์ง€๋ณด์ˆ˜์„ฑ + - ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋กœ ๋ฆฌํŒฉํ† ๋ง ์•ˆ์ „์„ฑ ํ™•๋ณด - ๋ชจ๋‹ˆํ„ฐ๋ง์œผ๋กœ ๋ฌธ์ œ ์กฐ๊ธฐ ๋ฐœ๊ฒฌ - ๋ฌธ์„œํ™”๋กœ ํ–ฅํ›„ ๊ฐœ๋ฐœ ์šฉ์ด @@ -324,4 +360,4 @@ task-master set-status --id=1.2 --status=done --- -*์ด ๋ฌธ์„œ๋Š” ์ ค๋ฆฌ์˜ ์ ์žํƒˆ์ถœ ํ”„๋กœ์ ํŠธ์˜ ๊ธฐ์ˆ ์  ๊ฐœ์„ ์„ ์œ„ํ•œ ์ข…ํ•ฉ์ ์ธ ๊ณ„ํš์„œ์ž…๋‹ˆ๋‹ค. ๊ฐ ๋‹จ๊ณ„๋Š” ํ”„๋กœ์ ํŠธ ์ƒํ™ฉ์— ๋งž๊ฒŒ ์กฐ์ • ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.* \ No newline at end of file +_์ด ๋ฌธ์„œ๋Š” ์ ค๋ฆฌ์˜ ์ ์žํƒˆ์ถœ ํ”„๋กœ์ ํŠธ์˜ ๊ธฐ์ˆ ์  ๊ฐœ์„ ์„ ์œ„ํ•œ ์ข…ํ•ฉ์ ์ธ ๊ณ„ํš์„œ์ž…๋‹ˆ๋‹ค. ๊ฐ ๋‹จ๊ณ„๋Š” ํ”„๋กœ์ ํŠธ ์ƒํ™ฉ์— ๋งž๊ฒŒ ์กฐ์ • ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค._ diff --git a/README.md b/README.md index f0f22ad..fbbc2c7 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,32 @@ This project is built with . - shadcn-ui - Tailwind CSS +## ๐Ÿ”ง TypeScript ํƒ€์ž… ์‹œ์Šคํ…œ + +์ด ํ”„๋กœ์ ํŠธ๋Š” ๊ฐ•๋ ฅํ•œ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ์œ„ํ•ด ์ค‘์•™ํ™”๋œ ํƒ€์ž… ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ–ˆ์Šต๋‹ˆ๋‹ค. + +### ์ฃผ์š” ํŠน์ง• + +- **Strict Mode**: ๋ชจ๋“  TypeScript strict ์˜ต์…˜ ํ™œ์„ฑํ™” +- **์ค‘์•™ํ™”๋œ ํƒ€์ž…**: `src/types/`์—์„œ ๋ชจ๋“  ํƒ€์ž… ๊ด€๋ฆฌ +- **ํƒ€์ž… ๊ฐ€๋“œ**: ๋Ÿฐํƒ€์ž„ ํƒ€์ž… ๊ฒ€์ฆ ์ง€์› +- **์„ฑ๋Šฅ ์ตœ์ ํ™”**: ์กฐ๊ธฐ ๋ฐ˜ํ™˜ ๋ฐ Set ๊ธฐ๋ฐ˜ ๊ฒ€์ฆ + +### ๋ฌธ์„œ + +- ๐Ÿ“š [ํƒ€์ž… ์‹œ์Šคํ…œ ๊ฐ€์ด๋“œ](./docs/TYPE_SYSTEM_GUIDE.md) - ์ƒ์„ธํ•œ ์‚ฌ์šฉ๋ฒ•๊ณผ ๊ตฌ์กฐ ์„ค๋ช… +- โšก [๋น ๋ฅธ ์ฐธ์กฐ](./docs/TYPE_SYSTEM_QUICK_REFERENCE.md) - ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ํŒจํ„ด๋“ค + +### ํƒ€์ž… ๊ฒ€์ฆ + +```bash +# ํƒ€์ž… ์˜ค๋ฅ˜ ๊ฒ€์‚ฌ +npm run type-check + +# ๋˜๋Š” ์ง์ ‘ +npx tsc --noEmit +``` + ## How can I deploy this project? Simply open [Lovable](https://lovable.dev/projects/79bc38c3-bdd0-4a7f-b4db-0ec501bdb94f) and click on Share -> Publish. diff --git a/capacitor.config.ts b/capacitor.config.ts index 63a96a9..200a152 100644 --- a/capacitor.config.ts +++ b/capacitor.config.ts @@ -1,13 +1,13 @@ -import { CapacitorConfig } from '@capacitor/cli'; +import { CapacitorConfig } from "@capacitor/cli"; const config: CapacitorConfig = { - appId: 'com.lovable.zellyfinance', - appName: '์ ค๋ฆฌ์˜ ์ ์žํƒˆ์ถœ', - webDir: 'dist', + appId: "com.lovable.zellyfinance", + appName: "์ ค๋ฆฌ์˜ ์ ์žํƒˆ์ถœ", + webDir: "dist", server: { - androidScheme: 'https', - iosScheme: 'https', - cleartext: true + androidScheme: "https", + iosScheme: "https", + cleartext: true, }, plugins: { SplashScreen: { @@ -18,17 +18,17 @@ const config: CapacitorConfig = { androidScaleType: "CENTER_CROP", showSpinner: false, splashFullScreen: false, - splashImmersive: false + splashImmersive: false, }, Keyboard: { resize: "body", style: "dark", - resizeOnFullScreen: true - } + resizeOnFullScreen: true, + }, }, ios: { - scheme: "App" - } + scheme: "App", + }, }; export default config; diff --git a/debug-mcp.sh b/debug-mcp.sh new file mode 100755 index 0000000..7ca4f15 --- /dev/null +++ b/debug-mcp.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +echo "๐Ÿ” Docker MCP ์—ฐ๊ฒฐ ์ง„๋‹จ ์Šคํฌ๋ฆฝํŠธ" +echo "================================" + +echo "1. Docker ๋ฒ„์ „ ํ™•์ธ..." +docker --version + +echo -e "\n2. Docker ์‹คํ–‰ ์ƒํƒœ ํ™•์ธ..." +docker info > /dev/null 2>&1 +if [ $? -eq 0 ]; then + echo "โœ… Docker๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์‹คํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค." +else + echo "โŒ Docker๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. Docker Desktop์„ ์‹œ์ž‘ํ•ด์ฃผ์„ธ์š”." + exit 1 +fi + +echo -e "\n3. MCP ๊ด€๋ จ ์ด๋ฏธ์ง€ ํ™•์ธ..." +docker images | grep -E "(socat|mcp)" + +echo -e "\n4. ํฌํŠธ 8811 ์‚ฌ์šฉ ํ™•์ธ..." +lsof -i :8811 2>/dev/null || echo "ํฌํŠธ 8811์ด ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค." + +echo -e "\n5. MCP ์„ค์ • ํŒŒ์ผ ํ™•์ธ..." +if [ -f ".mcp.json" ]; then + echo "โœ… .mcp.json ํŒŒ์ผ์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค." + echo "์„ค์ • ๋‚ด์šฉ:" + cat .mcp.json | jq . +else + echo "โŒ .mcp.json ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค." +fi + +echo -e "\n6. socat ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ..." +timeout 5 docker run --rm alpine/socat TCP-LISTEN:8811,fork EXEC:'/bin/echo "MCP Test"' & +sleep 2 +echo "MCP ํ…Œ์ŠคํŠธ ์ค‘..." | docker run --rm -i alpine/socat STDIO TCP:host.docker.internal:8811 2>/dev/null || echo "์—ฐ๊ฒฐ ์‹คํŒจ" + +echo -e "\n๐Ÿ“‹ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:" +echo "1. Docker Desktop โ†’ Settings โ†’ Beta features โ†’ Docker MCP Toolkit ํ™œ์„ฑํ™”" +echo "2. Docker Desktop โ†’ MCP Toolkit โ†’ MCP Clients โ†’ Claude Desktop ์—ฐ๊ฒฐ" +echo "3. Claude Code ์™„์ „ ์žฌ์‹œ์ž‘" +echo "4. ์—ฌ์ „ํžˆ ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค๋ฉด Docker Desktop ์žฌ์‹œ์ž‘" \ No newline at end of file diff --git a/docs/TYPE_SYSTEM_GUIDE.md b/docs/TYPE_SYSTEM_GUIDE.md new file mode 100644 index 0000000..b2f6510 --- /dev/null +++ b/docs/TYPE_SYSTEM_GUIDE.md @@ -0,0 +1,368 @@ +# TypeScript ํƒ€์ž… ์‹œ์Šคํ…œ ๊ฐ€์ด๋“œ + +## ๊ฐœ์š” + +Zellyy Finance ํ”„๋กœ์ ํŠธ๋Š” ๊ฐ•๋ ฅํ•œ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด ์ค‘์•™ํ™”๋œ ํƒ€์ž… ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฐ€์ด๋“œ๋Š” ์ƒˆ๋กœ์šด ํƒ€์ž… ์‹œ์Šคํ…œ์˜ ๊ตฌ์กฐ์™€ ์‚ฌ์šฉ๋ฒ•์„ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. + +## ๐Ÿ“ ํƒ€์ž… ์‹œ์Šคํ…œ ๊ตฌ์กฐ + +``` +src/types/ +โ”œโ”€โ”€ index.ts # ํƒ€์ž… ์‹œ์Šคํ…œ ์—”ํŠธ๋ฆฌ ํฌ์ธํŠธ +โ”œโ”€โ”€ common.ts # ๊ณตํ†ต ํƒ€์ž… ์ •์˜ +โ”œโ”€โ”€ utils.ts # ์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž… +โ””โ”€โ”€ guards.ts # ํƒ€์ž… ๊ฐ€๋“œ ํ•จ์ˆ˜๋“ค +``` + +## ๐Ÿ”ง TypeScript ์„ค์ • + +ํ”„๋กœ์ ํŠธ๋Š” strict ๋ชจ๋“œ๊ฐ€ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์–ด ์ตœ๊ณ  ์ˆ˜์ค€์˜ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค: + +```json +{ + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + } +} +``` + +## ๐Ÿ“‹ ์ฃผ์š” ํƒ€์ž…๋“ค + +### ๊ณตํ†ต ํƒ€์ž… (`src/types/common.ts`) + +#### PaymentMethod +```typescript +export type PaymentMethod = '์‹ ์šฉ์นด๋“œ' | 'ํ˜„๊ธˆ' | '์ฒดํฌ์นด๋“œ' | '๊ฐ„ํŽธ๊ฒฐ์ œ'; +``` + +#### TransactionType +```typescript +export type TransactionType = 'income' | 'expense'; +``` + +#### ApiResponse +```typescript +export interface ApiResponse { + success: boolean; + data?: T; + error?: string; + message?: string; +} +``` + +#### MonthlyData (Analytics) +```typescript +export interface MonthlyData { + name: string; + budget: number; + expense: number; +} +``` + +### ์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž… (`src/types/utils.ts`) + +#### ๊ณ ๊ธ‰ ํƒ€์ž… ์กฐ์ž‘ +```typescript +// ํ•„์ˆ˜ ํ•„๋“œ๋งŒ ์„ ํƒ +export type StrictPick = Pick & Required>; + +// ํŠน์ • ํ•„๋“œ๋ฅผ ์ œ์™ธํ•˜๊ณ  ๋ชจ๋‘ ์„ ํƒ์ ์œผ๋กœ +export type OptionalExcept = Partial & Pick; + +// ์ค‘์ฒฉ๋œ ๊ฐ์ฒด๋„ ๋ถ€๋ถ„์ ์œผ๋กœ +export type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; +}; +``` + +#### ๋น„๋™๊ธฐ ์ƒํƒœ ๊ด€๋ฆฌ +```typescript +export interface AsyncState { + data: T | null; + loading: boolean; + error: string | null; + lastUpdated?: Date; +} +``` + +#### ํƒ€์ž… ์•ˆ์ „ํ•œ Object ์œ ํ‹ธ๋ฆฌํ‹ฐ +```typescript +// ํƒ€์ž… ์•ˆ์ „ํ•œ Object.keys +export const typedKeys = >(obj: T): Array => { + return Object.keys(obj) as Array; +}; + +// ํƒ€์ž… ์•ˆ์ „ํ•œ Object.entries +export const typedEntries = >(obj: T): Array<[keyof T, T[keyof T]]> => { + return Object.entries(obj) as Array<[keyof T, T[keyof T]]>; +}; +``` + +## ๐Ÿ›ก๏ธ ํƒ€์ž… ๊ฐ€๋“œ ํ•จ์ˆ˜๋“ค (`src/types/guards.ts`) + +### ๊ธฐ๋ณธ ํƒ€์ž… ๊ฐ€๋“œ +```typescript +export const isString = (value: unknown): value is string => { + return typeof value === 'string'; +}; + +export const isNumber = (value: unknown): value is number => { + return typeof value === 'number' && !isNaN(value); +}; + +export const isObject = (value: unknown): value is Record => { + return typeof value === 'object' && value !== null && !Array.isArray(value); +}; +``` + +### ๋„๋ฉ”์ธ ํŠนํ™” ํƒ€์ž… ๊ฐ€๋“œ +```typescript +export const isValidPaymentMethod = (value: unknown): value is PaymentMethod => { + return typeof value === 'string' && VALID_PAYMENT_METHODS.has(value as PaymentMethod); +}; + +export const isValidTransactionType = (value: unknown): value is TransactionType => { + return value === 'income' || value === 'expense'; +}; +``` + +### ๋ณตํ•ฉ ํƒ€์ž… ๊ฐ€๋“œ +```typescript +export const isValidTransaction = (value: unknown): value is Transaction => { + if (!isObject(value)) return false; + + const transaction = value as Record; + + // ํ•„์ˆ˜ ํ•„๋“œ ์กฐ๊ธฐ ๊ฒ€์ฆ + if (!isString(transaction.id)) return false; + if (!isString(transaction.title)) return false; + if (!isNumber(transaction.amount)) return false; + // ... ๋” ๋งŽ์€ ๊ฒ€์ฆ + + return true; +}; +``` + +### ์ œ๋„ค๋ฆญ ํƒ€์ž… ๊ฐ€๋“œ +```typescript +// ๋ฐฐ์—ด์˜ ๋ชจ๋“  ์›์†Œ๊ฐ€ ํŠน์ • ํƒ€์ž… ๊ฐ€๋“œ๋ฅผ ๋งŒ์กฑํ•˜๋Š”์ง€ ์ฒดํฌ +export const isArrayOf = ( + value: unknown, + guard: (item: unknown) => item is T +): value is T[] => { + return isArray(value) && value.every(guard); +}; +``` + +## ๐ŸŽฏ ์‚ฌ์šฉ ์˜ˆ์‹œ + +### 1. ๊ธฐ๋ณธ ํƒ€์ž… ๊ฒ€์ฆ +```typescript +import { isString, isNumber } from '@/types/guards'; + +function processUserInput(input: unknown) { + if (isString(input)) { + // input์€ ์ด์ œ string ํƒ€์ž…์œผ๋กœ ์ถ”๋ก ๋จ + console.log(input.toUpperCase()); + } +} +``` + +### 2. API ์‘๋‹ต ๊ฒ€์ฆ +```typescript +import { isValidTransaction, isArrayOf } from '@/types/guards'; + +async function fetchTransactions() { + const response = await fetch('/api/transactions'); + const data = await response.json(); + + if (isArrayOf(data, isValidTransaction)) { + // data๋Š” ์ด์ œ Transaction[] ํƒ€์ž…์œผ๋กœ ์ถ”๋ก ๋จ + return data; + } + + throw new Error('Invalid transaction data'); +} +``` + +### 3. ์ค‘์•™ํ™”๋œ ํƒ€์ž… ์‚ฌ์šฉ +```typescript +import { PaymentMethod, MonthlyData } from '@/types'; + +interface ExpenseForm { + amount: number; + paymentMethod: PaymentMethod; // ์ค‘์•™ํ™”๋œ ํƒ€์ž… ์‚ฌ์šฉ +} + +function processAnalytics(data: MonthlyData[]) { + // MonthlyData ํƒ€์ž…์ด ๋ณด์žฅ๋จ + return data.map(month => ({ + ...month, + savingsRate: (month.budget - month.expense) / month.budget + })); +} +``` + +### 4. ํƒ€์ž… ์•ˆ์ „ํ•œ Object ์กฐ์ž‘ +```typescript +import { typedKeys, typedEntries } from '@/types/utils'; + +const config = { + apiUrl: 'https://api.example.com', + timeout: 5000, + retries: 3 +}; + +// ํƒ€์ž… ์•ˆ์ „ํ•œ ํ‚ค ๋ฐ˜๋ณต +typedKeys(config).forEach(key => { + console.log(`${key}: ${config[key]}`); // ํƒ€์ž… ์˜ค๋ฅ˜ ์—†์Œ +}); + +// ํƒ€์ž… ์•ˆ์ „ํ•œ ์—”ํŠธ๋ฆฌ ๋ฐ˜๋ณต +typedEntries(config).forEach(([key, value]) => { + console.log(`${key}: ${value}`); // ์™„์ „ํ•œ ํƒ€์ž… ์ถ”๋ก  +}); +``` + +## ๐Ÿ”„ Form ๊ฒ€์ฆ๊ณผ ํƒ€์ž… ํ†ตํ•ฉ + +### Zod ์Šคํ‚ค๋งˆ์™€ ์ค‘์•™ํ™”๋œ ํƒ€์ž… ์—ฐ๋™ +```typescript +import { z } from 'zod'; +import { PaymentMethod } from '@/types'; + +export const transactionFormSchema = z.object({ + title: z.string().min(1, '์ œ๋ชฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'), + amount: z.string().min(1, '๊ธˆ์•ก์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'), + category: z.enum(['์Œ์‹', '์‡ผํ•‘', '๊ตํ†ต', '๊ธฐํƒ€']), + paymentMethod: z.enum(['์‹ ์šฉ์นด๋“œ', 'ํ˜„๊ธˆ', '์ฒดํฌ์นด๋“œ', '๊ฐ„ํŽธ๊ฒฐ์ œ'] as const).default('์‹ ์šฉ์นด๋“œ'), +}); + +export type TransactionFormValues = z.infer; +``` + +## ๐Ÿ“Š ์„ฑ๋Šฅ ์ตœ์ ํ™” + +### 1. Set ๊ธฐ๋ฐ˜ ์ƒ์ˆ˜ ๊ฒ€์ฆ +```typescript +// ์„ฑ๋Šฅ ์ตœ์ ํ™”: ๋ฐฐ์—ด ๋Œ€์‹  Set ์‚ฌ์šฉ +const VALID_PAYMENT_METHODS = new Set(['์‹ ์šฉ์นด๋“œ', 'ํ˜„๊ธˆ', '์ฒดํฌ์นด๋“œ', '๊ฐ„ํŽธ๊ฒฐ์ œ']); + +export const isValidPaymentMethod = (value: unknown): value is PaymentMethod => { + return typeof value === 'string' && VALID_PAYMENT_METHODS.has(value as PaymentMethod); +}; +``` + +### 2. ์กฐ๊ธฐ ๋ฐ˜ํ™˜ ํŒจํ„ด +```typescript +// ์กฐ๊ธฐ ๋ฐ˜ํ™˜์œผ๋กœ ์„ฑ๋Šฅ ํ–ฅ์ƒ +export const isValidTransaction = (value: unknown): value is Transaction => { + if (!isObject(value)) return false; + + const transaction = value as Record; + + // ํ•„์ˆ˜ ํ•„๋“œ๋ฅผ ๋จผ์ € ๊ฒ€์ฆํ•˜์—ฌ ๋น ๋ฅธ ์‹คํŒจ + if (!isString(transaction.id)) return false; + if (!isString(transaction.title)) return false; + // ... + + return true; +}; +``` + +## ๐Ÿšจ ์ฃผ์˜์‚ฌํ•ญ + +### 1. Import ํŒจํ„ด +```typescript +// โœ… ์ข‹์€ ์˜ˆ: ์ค‘์•™ํ™”๋œ ํƒ€์ž…์—์„œ ์ง์ ‘ import +import { PaymentMethod, Transaction } from '@/types'; + +// โŒ ๋‚˜์œ ์˜ˆ: ๊ฐ„์ ‘์ ์ธ import ์ฒด์ธ +import { Transaction } from '@/components/TransactionCard'; +``` + +### 2. any ํƒ€์ž… ๊ธˆ์ง€ +```typescript +// โŒ ์ ˆ๋Œ€ ์‚ฌ์šฉํ•˜์ง€ ๋ง ๊ฒƒ +function processData(data: any) { } + +// โœ… ์ ์ ˆํ•œ ํƒ€์ž… ๋˜๋Š” unknown ์‚ฌ์šฉ +function processData(data: unknown) { + if (isValidTransaction(data)) { + // ์ด์ œ data๋Š” Transaction ํƒ€์ž… + } +} +``` + +### 3. ํƒ€์ž… ๊ฐ€๋“œ ํ™œ์šฉ +```typescript +// โŒ ํƒ€์ž… ๋‹จ์–ธ ๋‚จ์šฉ +const transaction = data as Transaction; + +// โœ… ํƒ€์ž… ๊ฐ€๋“œ๋กœ ์•ˆ์ „ํ•œ ๊ฒ€์ฆ +if (isValidTransaction(data)) { + const transaction = data; // ํƒ€์ž…์ด ์ž๋™์œผ๋กœ ์ถ”๋ก ๋จ +} +``` + +## ๐Ÿงช ํ…Œ์ŠคํŠธ์™€ ๊ฒ€์ฆ + +### TypeScript ์ปดํŒŒ์ผ ๊ฒ€์ฆ +```bash +# ํƒ€์ž… ์˜ค๋ฅ˜ ๊ฒ€์‚ฌ +npx tsc --noEmit + +# ๋ชจ๋“  ๊ฒ€์‚ฌ ํ†ต๊ณผํ•ด์•ผ ํ•จ (์˜ค๋ฅ˜ 0๊ฐœ) +``` + +### ๋Ÿฐํƒ€์ž„ ํƒ€์ž… ๊ฒ€์ฆ ์˜ˆ์‹œ +```typescript +import { isValidTransaction } from '@/types/guards'; + +describe('Transaction validation', () => { + it('should validate correct transaction', () => { + const validTransaction = { + id: '1', + title: 'Test', + amount: 100, + date: '2024-01-01', + category: '์Œ์‹', + type: 'expense' + }; + + expect(isValidTransaction(validTransaction)).toBe(true); + }); +}); +``` + +## ๐Ÿ”ฎ ํ™•์žฅ ๊ฐ€์ด๋“œ + +### ์ƒˆ๋กœ์šด ํƒ€์ž… ์ถ”๊ฐ€ +1. `src/types/common.ts`์— ๊ธฐ๋ณธ ํƒ€์ž… ์ •์˜ +2. `src/types/guards.ts`์— ํƒ€์ž… ๊ฐ€๋“œ ํ•จ์ˆ˜ ์ถ”๊ฐ€ +3. `src/types/index.ts`์—์„œ export +4. ์ปดํฌ๋„ŒํŠธ์—์„œ ์ค‘์•™ํ™”๋œ ํƒ€์ž… ์‚ฌ์šฉ + +### ํƒ€์ž… ๊ฐ€๋“œ ์ถ”๊ฐ€ +```typescript +// 1. ๊ธฐ๋ณธ ํƒ€์ž… ์ •์˜ (common.ts) +export type NewDomainType = 'option1' | 'option2' | 'option3'; + +// 2. ํƒ€์ž… ๊ฐ€๋“œ ์ถ”๊ฐ€ (guards.ts) +const VALID_DOMAIN_OPTIONS = new Set(['option1', 'option2', 'option3']); + +export const isValidDomainType = (value: unknown): value is NewDomainType => { + return typeof value === 'string' && VALID_DOMAIN_OPTIONS.has(value as NewDomainType); +}; + +// 3. Export ์ถ”๊ฐ€ (index.ts) +export type { NewDomainType } from './common'; +export { isValidDomainType } from './guards'; +``` + +์ด ํƒ€์ž… ์‹œ์Šคํ…œ์„ ํ†ตํ•ด ๊ฐœ๋ฐœ์ž๋Š” ์ปดํŒŒ์ผ ํƒ€์ž„๊ณผ ๋Ÿฐํƒ€์ž„ ๋ชจ๋‘์—์„œ ๊ฐ•๋ ฅํ•œ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ๋ณด์žฅ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. \ No newline at end of file diff --git a/docs/TYPE_SYSTEM_QUICK_REFERENCE.md b/docs/TYPE_SYSTEM_QUICK_REFERENCE.md new file mode 100644 index 0000000..e662e3e --- /dev/null +++ b/docs/TYPE_SYSTEM_QUICK_REFERENCE.md @@ -0,0 +1,267 @@ +# ํƒ€์ž… ์‹œ์Šคํ…œ ๋น ๋ฅธ ์ฐธ์กฐ + +## ๐Ÿ“š ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” Import ํŒจํ„ด + +```typescript +// ๊ธฐ๋ณธ ํƒ€์ž…๋“ค +import { PaymentMethod, TransactionType, ApiResponse, MonthlyData } from '@/types'; + +// ํƒ€์ž… ๊ฐ€๋“œ +import { isString, isNumber, isValidTransaction, isValidPaymentMethod } from '@/types/guards'; + +// ์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž… +import { AsyncState, StrictPick, OptionalExcept, typedKeys, typedEntries } from '@/types/utils'; + +// ์ปจํ…์ŠคํŠธ ํƒ€์ž…๋“ค +import { Transaction, BudgetPeriod } from '@/contexts/budget/types'; +``` + +## ๐ŸŽฏ ์ผ๋ฐ˜์ ์ธ ์‚ฌ์šฉ ํŒจํ„ด + +### API ์‘๋‹ต ์ฒ˜๋ฆฌ +```typescript +// โœ… ํƒ€์ž… ๊ฐ€๋“œ๋ฅผ ์‚ฌ์šฉํ•œ ์•ˆ์ „ํ•œ API ์‘๋‹ต ์ฒ˜๋ฆฌ +async function fetchData() { + try { + const response = await fetch('/api/transactions'); + const data = await response.json(); + + if (isArrayOf(data, isValidTransaction)) { + return data; // Transaction[]๋กœ ํƒ€์ž… ์ถ”๋ก ๋จ + } + + throw new Error('Invalid data format'); + } catch (error) { + if (isApiError(error)) { + console.error('API Error:', error.message); + } + throw error; + } +} +``` + +### Form ํƒ€์ž… ์ •์˜ +```typescript +// โœ… ์ค‘์•™ํ™”๋œ ํƒ€์ž…์„ ์‚ฌ์šฉํ•œ Form ์ •์˜ +interface TransactionForm { + title: string; + amount: number; + paymentMethod: PaymentMethod; // ์ค‘์•™ํ™”๋œ ํƒ€์ž… ์‚ฌ์šฉ + type: TransactionType; +} + +// Zod ์Šคํ‚ค๋งˆ์™€ ์—ฐ๋™ +const schema = z.object({ + title: z.string().min(1), + amount: z.number().positive(), + paymentMethod: z.enum(['์‹ ์šฉ์นด๋“œ', 'ํ˜„๊ธˆ', '์ฒดํฌ์นด๋“œ', '๊ฐ„ํŽธ๊ฒฐ์ œ']), + type: z.enum(['income', 'expense']) +}); +``` + +### ์ปดํฌ๋„ŒํŠธ Props ํƒ€์ž… +```typescript +// โœ… ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Props ํƒ€์ž… +interface TransactionCardProps { + transaction: Transaction; + onUpdate?: (transaction: Transaction) => void; + onDelete?: (id: string) => Promise; +} + +// DataComponent ํŒจํ„ด +interface AnalyticsProps extends DataComponentProps { + period: string; + onPeriodChange: (period: string) => void; +} +``` + +### ์ƒํƒœ ๊ด€๋ฆฌ +```typescript +// โœ… AsyncState ํŒจํ„ด ์‚ฌ์šฉ +const [transactionState, setTransactionState] = useState>({ + data: null, + loading: false, + error: null +}); + +// ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ +const updateTransactionState = (update: Partial>) => { + setTransactionState(prev => ({ ...prev, ...update })); +}; +``` + +## ๐Ÿ›ก๏ธ ํƒ€์ž… ๊ฐ€๋“œ ์น˜ํŠธ์‹œํŠธ + +### ๊ธฐ๋ณธ ๊ฒ€์ฆ +```typescript +if (isString(value)) { /* value๋Š” string */ } +if (isNumber(value)) { /* value๋Š” number */ } +if (isBoolean(value)) { /* value๋Š” boolean */ } +if (isObject(value)) { /* value๋Š” Record */ } +if (isArray(value)) { /* value๋Š” unknown[] */ } +``` + +### ๋„๋ฉ”์ธ ๊ฒ€์ฆ +```typescript +if (isValidPaymentMethod(value)) { /* value๋Š” PaymentMethod */ } +if (isValidTransactionType(value)) { /* value๋Š” TransactionType */ } +if (isValidTransaction(value)) { /* value๋Š” Transaction */ } +if (isValidDate(value)) { /* value๋Š” valid date string */ } +if (isValidEmail(value)) { /* value๋Š” valid email string */ } +``` + +### ๋ฐฐ์—ด ๊ฒ€์ฆ +```typescript +if (isStringArray(value)) { /* value๋Š” string[] */ } +if (isNumberArray(value)) { /* value๋Š” number[] */ } +if (isArrayOf(value, isValidTransaction)) { /* value๋Š” Transaction[] */ } +if (isNonEmptyArray(value)) { /* value๋Š” [T, ...T[]] */ } +``` + +### ์œ ํ‹ธ๋ฆฌํ‹ฐ ๊ฒ€์ฆ +```typescript +if (isNonEmptyString(value)) { /* value๋Š” non-empty string */ } +if (isPositiveNumber(value)) { /* value๋Š” positive number */ } +if (isEmptyObject(value)) { /* value๋Š” {} */ } +if (isApiError(error)) { /* error๋Š” { message: string } */ } +``` + +## ๐Ÿ”ง ์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž… ์น˜ํŠธ์‹œํŠธ + +### ๊ฐ์ฒด ์กฐ์ž‘ +```typescript +// ํ•„์ˆ˜ ํ•„๋“œ๋งŒ ์„ ํƒ +type UserName = StrictPick; + +// ํŠน์ • ํ•„๋“œ ์ œ์™ธํ•˜๊ณ  optional +type PartialUser = OptionalExcept; + +// ๊นŠ์€ ๋ถ€๋ถ„ ํƒ€์ž… +type PartialConfig = DeepPartial; + +// Create/Update ํŒจํ„ด +type CreateUser = CreateInput; // id, createdAt, updatedAt ์ œ์™ธ +type UpdateUser = UpdateInput; // partial + id ํฌํ•จ +``` + +### ๋น„๋™๊ธฐ ์ƒํƒœ +```typescript +const [userState, setUserState] = useState>({ + data: null, + loading: false, + error: null, + lastUpdated: undefined +}); +``` + +### Promise ํƒ€์ž… ์ถ”์ถœ +```typescript +type APIResponse = PromiseType; // Promise์—์„œ User[] ์ถ”์ถœ +``` + +## ๐Ÿšจ ์ผ๋ฐ˜์ ์ธ ์‹ค์ˆ˜์™€ ํ•ด๊ฒฐ์ฑ… + +### โŒ ์‹ค์ˆ˜ 1: any ํƒ€์ž… ์‚ฌ์šฉ +```typescript +// ๋‚˜์œ ์˜ˆ +function processData(data: any) { + return data.someProperty; +} + +// ์ข‹์€ ์˜ˆ +function processData(data: unknown) { + if (isObject(data) && 'someProperty' in data) { + return (data as { someProperty: unknown }).someProperty; + } + return null; +} +``` + +### โŒ ์‹ค์ˆ˜ 2: ํƒ€์ž… ๋‹จ์–ธ ๋‚จ์šฉ +```typescript +// ๋‚˜์œ ์˜ˆ +const transaction = apiResponse as Transaction; + +// ์ข‹์€ ์˜ˆ +if (isValidTransaction(apiResponse)) { + const transaction = apiResponse; // ์ž๋™์œผ๋กœ ํƒ€์ž… ์ถ”๋ก ๋จ +} +``` + +### โŒ ์‹ค์ˆ˜ 3: ํ•˜๋“œ์ฝ”๋”ฉ๋œ ํƒ€์ž… ๊ฐ’ +```typescript +// ๋‚˜์œ ์˜ˆ +type PaymentMethod = '์‹ ์šฉ์นด๋“œ' | 'ํ˜„๊ธˆ'; // ์ผ๋ถ€๋งŒ ์ •์˜ + +// ์ข‹์€ ์˜ˆ +import { PaymentMethod } from '@/types'; // ์ค‘์•™ํ™”๋œ ์ „์ฒด ํƒ€์ž… ์‚ฌ์šฉ +``` + +### โŒ ์‹ค์ˆ˜ 4: ๊ฐ„์ ‘ import +```typescript +// ๋‚˜์œ ์˜ˆ +import { Transaction } from '@/components/TransactionCard'; + +// ์ข‹์€ ์˜ˆ +import { Transaction } from '@/contexts/budget/types'; +``` + +## ๐ŸŽจ ์ปดํฌ๋„ŒํŠธ ํŒจํ„ด + +### ๊ธฐ๋ณธ ์ปดํฌ๋„ŒํŠธ Props +```typescript +interface BaseComponentProps { + className?: string; + children?: React.ReactNode; +} + +interface MyComponentProps extends BaseComponentProps { + title: string; + data: Transaction[]; +} +``` + +### ๋ฐ์ดํ„ฐ ์ปดํฌ๋„ŒํŠธ ํŒจํ„ด +```typescript +interface DataComponentProps extends BaseComponentProps { + data: T; + loading?: boolean; + error?: string | null; +} + +interface TransactionListProps extends DataComponentProps { + onTransactionClick: (transaction: Transaction) => void; +} +``` + +### ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ํƒ€์ž… +```typescript +interface FormComponentProps { + onSubmit: SubmitHandler; // React.FormEvent + onChange: EventHandler; // React.ChangeEvent + onClick: ClickHandler; // React.MouseEvent +} +``` + +## ๐Ÿ“‹ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### ์ƒˆ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ ์‹œ +- [ ] ์ค‘์•™ํ™”๋œ ํƒ€์ž… ์‚ฌ์šฉ (`@/types`์—์„œ import) +- [ ] Props ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜ +- [ ] ์ ์ ˆํ•œ ํƒ€์ž… ๊ฐ€๋“œ ์‚ฌ์šฉ +- [ ] any ํƒ€์ž… ์‚ฌ์šฉ ๊ธˆ์ง€ +- [ ] ํƒ€์ž… ๋‹จ์–ธ ๋Œ€์‹  ํƒ€์ž… ๊ฐ€๋“œ ์‚ฌ์šฉ + +### API ํ†ตํ•ฉ ์‹œ +- [ ] ์‘๋‹ต ๋ฐ์ดํ„ฐ ํƒ€์ž… ๊ฐ€๋“œ๋กœ ๊ฒ€์ฆ +- [ ] ApiResponse ํƒ€์ž… ์‚ฌ์šฉ +- [ ] ์—๋Ÿฌ ์ฒ˜๋ฆฌ์— isApiError ์‚ฌ์šฉ +- [ ] AsyncState ํŒจํ„ด ์ ์šฉ + +### Form ๊ฐœ๋ฐœ ์‹œ +- [ ] ์ค‘์•™ํ™”๋œ enum ํƒ€์ž… ์‚ฌ์šฉ +- [ ] Zod ์Šคํ‚ค๋งˆ์™€ ํƒ€์ž… ์—ฐ๋™ +- [ ] ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ํƒ€์ž… ์ ์šฉ +- [ ] ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์— ํƒ€์ž… ๊ฐ€๋“œ ํ™œ์šฉ + +์ด ์ฐธ์กฐ ๊ฐ€์ด๋“œ๋ฅผ ๋ถ๋งˆํฌํ•˜์—ฌ ๊ฐœ๋ฐœ ์ค‘ ๋น ๋ฅด๊ฒŒ ์ฐธ์กฐํ•˜์„ธ์š”! \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index e67846f..efb1e5e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,7 +5,18 @@ import reactRefresh from "eslint-plugin-react-refresh"; import tseslint from "typescript-eslint"; export default tseslint.config( - { ignores: ["dist"] }, + { + ignores: [ + "dist", + "android/**", + "ios/**", + "capacitor/**", + "node_modules/**", + "*.config.js", + "*.config.ts", + "src/archive/**", + ], + }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ["**/*.{ts,tsx}"], @@ -23,7 +34,33 @@ export default tseslint.config( "warn", { allowConstantExport: true }, ], - "@typescript-eslint/no-unused-vars": "off", + // TypeScript specific rules + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-inferrable-types": "error", + + // General JavaScript rules + "no-console": ["warn", { allow: ["warn", "error"] }], + "no-debugger": "error", + "no-alert": "error", + "prefer-const": "error", + "no-var": "error", + eqeqeq: ["error", "always"], + curly: ["error", "all"], + + // React specific rules + "react-hooks/exhaustive-deps": "error", + + // Code quality rules (relaxed for migration period) + complexity: ["warn", 30], + "max-depth": ["warn", 6], + "max-lines-per-function": ["warn", 300], }, } ); diff --git a/index.html b/index.html index 9b8fe3d..12e8fcb 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,4 @@ - - + diff --git a/package-lock.json b/package-lock.json index 99b9cbf..731c501 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "date-fns": "^3.6.0", "dotenv": "^16.5.0", "embla-carousel-react": "^8.3.0", + "glob": "^11.0.3", "input-otp": "^1.2.4", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", @@ -82,8 +83,11 @@ "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.9", "globals": "^15.9.0", + "husky": "^9.1.7", + "lint-staged": "^16.1.2", "lovable-tagger": "^1.1.7", "postcss": "^8.4.47", + "prettier": "^3.6.2", "tailwindcss": "^3.4.11", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", @@ -1116,6 +1120,27 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3787,6 +3812,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -4155,6 +4196,107 @@ "url": "https://polar.sh/cva" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-truncate/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -4560,6 +4702,13 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -4739,9 +4888,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4884,6 +5033,19 @@ "node": ">=6" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -5279,12 +5441,12 @@ "license": "ISC" }, "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -5369,6 +5531,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -5379,21 +5554,24 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" }, + "engines": { + "node": "20 || >=22" + }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -5410,25 +5588,16 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -5482,6 +5651,22 @@ "node": ">= 0.4" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5662,18 +5847,18 @@ "license": "ISC" }, "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, + "engines": { + "node": "20 || >=22" + }, "funding": { "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" } }, "node_modules/jiti": { @@ -5788,6 +5973,138 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/lint-staged": { + "version": "16.1.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.2.tgz", + "integrity": "sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0", + "debug": "^4.4.1", + "lilconfig": "^3.1.3", + "listr2": "^8.3.3", + "micromatch": "^4.0.8", + "nano-spawn": "^1.0.2", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/listr2": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5829,6 +6146,115 @@ "dev": true, "license": "MIT" }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -6292,10 +6718,13 @@ } }, "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } }, "node_modules/lucide-react": { "version": "0.462.0", @@ -6337,6 +6766,19 @@ "node": ">=8.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -6413,6 +6855,19 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nano-spawn": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz", + "integrity": "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -6516,6 +6971,22 @@ "node": ">= 6" } }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/open": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", @@ -6628,16 +7099,16 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -6667,6 +7138,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -6852,6 +7336,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -7225,6 +7725,23 @@ "node": ">=4" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -7235,6 +7752,13 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rimraf": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", @@ -7254,93 +7778,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", - "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/jackspeak": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", - "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/lru-cache": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", - "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { "version": "4.24.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", @@ -7540,6 +7977,16 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -7671,6 +8118,87 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/sucrase/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8333,15 +8861,15 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", - "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } }, "node_modules/yauzl": { diff --git a/package.json b/package.json index 4d79502..6c8b976 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,23 @@ "build": "vite build", "build:dev": "vite build --mode development", "lint": "eslint .", - "preview": "vite preview" + "lint:fix": "eslint . --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "preview": "vite preview", + "type-check": "tsc --noEmit", + "type-check:watch": "tsc --noEmit --watch", + "check-all": "npm run type-check && npm run lint", + "prepare": "husky" + }, + "lint-staged": { + "*.{ts,tsx}": [ + "prettier --write", + "eslint --fix" + ], + "*.{js,jsx,json,css,md}": [ + "prettier --write" + ] }, "dependencies": { "@capacitor/android": "^7.1.0", @@ -56,6 +72,7 @@ "date-fns": "^3.6.0", "dotenv": "^16.5.0", "embla-carousel-react": "^8.3.0", + "glob": "^11.0.3", "input-otp": "^1.2.4", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", @@ -85,8 +102,11 @@ "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.9", "globals": "^15.9.0", + "husky": "^9.1.7", + "lint-staged": "^16.1.2", "lovable-tagger": "^1.1.7", "postcss": "^8.4.47", + "prettier": "^3.6.2", "tailwindcss": "^3.4.11", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", diff --git a/postcss.config.js b/postcss.config.js index 2e7af2b..2aa7205 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -3,4 +3,4 @@ export default { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/replace-console-logs.cjs b/replace-console-logs.cjs new file mode 100644 index 0000000..5b3010b --- /dev/null +++ b/replace-console-logs.cjs @@ -0,0 +1,183 @@ +#!/usr/bin/env node + +/** + * Console.log ๊ต์ฒด ์Šคํฌ๋ฆฝํŠธ + * ํ”„๋กœ์ ํŠธ ์ „์ฒด์˜ console.log๋ฅผ ํ™˜๊ฒฝ๋ณ„ ๋กœ๊ฑฐ๋กœ ๊ต์ฒด + */ + +const fs = require("fs"); +const path = require("path"); +const { glob } = require("glob"); + +// ๊ต์ฒดํ•  ํŒจํ„ด๋“ค +const replacements = [ + { + pattern: /console\.log\(/g, + replacement: "syncLogger.info(", + description: "console.log โ†’ syncLogger.info", + }, + { + pattern: /console\.error\(/g, + replacement: "syncLogger.error(", + description: "console.error โ†’ syncLogger.error", + }, + { + pattern: /console\.warn\(/g, + replacement: "syncLogger.warn(", + description: "console.warn โ†’ syncLogger.warn", + }, + { + pattern: /console\.info\(/g, + replacement: "syncLogger.info(", + description: "console.info โ†’ syncLogger.info", + }, +]; + +// ํŒŒ์ผ๋ณ„ ํŠนํ™”๋œ ๋กœ๊ฑฐ ๋งคํ•‘ +const fileLoggerMap = { + auth: "authLogger", + sync: "syncLogger", + network: "networkLogger", + storage: "storageLogger", + appwrite: "appwriteLogger", +}; + +// ํŒŒ์ผ ๊ฒฝ๋กœ์— ๋”ฐ๋ฅธ ์ ์ ˆํ•œ ๋กœ๊ฑฐ ๊ฒฐ์ • +function getLoggerForFile(filePath) { + const lowerPath = filePath.toLowerCase(); + + for (const [keyword, logger] of Object.entries(fileLoggerMap)) { + if (lowerPath.includes(keyword)) { + return logger; + } + } + + // ๊ธฐ๋ณธ๊ฐ’์€ ์ผ๋ฐ˜ logger + return "logger"; +} + +// ํŒŒ์ผ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜ +async function processFile(filePath) { + try { + const content = fs.readFileSync(filePath, "utf8"); + const originalContent = content; + + // ์ด๋ฏธ logger import๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ + const hasLoggerImport = content.includes("from '@/utils/logger'"); + const appropriateLogger = getLoggerForFile(filePath); + + let newContent = content; + let hasChanges = false; + + // console ์‚ฌ์šฉ์ด ์žˆ๋Š”์ง€ ํ™•์ธ + const hasConsoleUsage = /console\.(log|error|warn|info)\(/.test(content); + + if (hasConsoleUsage) { + // console.log ๋“ฑ์„ ์ ์ ˆํ•œ ๋กœ๊ฑฐ๋กœ ๊ต์ฒด + replacements.forEach(({ pattern, description }) => { + const loggerMethod = pattern.source.includes("error") + ? "error" + : pattern.source.includes("warn") + ? "warn" + : "info"; + + const replacement = `${appropriateLogger}.${loggerMethod}(`; + + if (pattern.test(newContent)) { + newContent = newContent.replace(pattern, replacement); + hasChanges = true; + console.log( + ` - ${description} (using ${appropriateLogger}.${loggerMethod})` + ); + } + }); + + // logger import ์ถ”๊ฐ€ (ํ•„์š”ํ•œ ๊ฒฝ์šฐ) + if (hasChanges && !hasLoggerImport) { + // import ๊ตฌ๋ฌธ ์ฐพ๊ธฐ + const importMatch = newContent.match( + /^(import[\s\S]*?from ['"][^'"]+['"];?\s*\n)/m + ); + + if (importMatch) { + const lastImportEnd = importMatch.index + importMatch[1].length; + const beforeImports = newContent.slice(0, lastImportEnd); + const afterImports = newContent.slice(lastImportEnd); + + let importStatement; + if (appropriateLogger === "logger") { + importStatement = "import { logger } from '@/utils/logger';\n"; + } else { + importStatement = `import { ${appropriateLogger} } from '@/utils/logger';\n`; + } + + newContent = beforeImports + importStatement + afterImports; + console.log(` - Added import: ${importStatement.trim()}`); + } + } + } + + // ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์žˆ์œผ๋ฉด ํŒŒ์ผ ์ €์žฅ + if (hasChanges && newContent !== originalContent) { + fs.writeFileSync(filePath, newContent, "utf8"); + return true; + } + + return false; + } catch (error) { + console.error(`Error processing ${filePath}:`, error.message); + return false; + } +} + +// ๋ฉ”์ธ ์‹คํ–‰ ํ•จ์ˆ˜ +async function main() { + console.log("๐Ÿ”„ Console.log โ†’ Logger ๊ต์ฒด ์ž‘์—… ์‹œ์ž‘\n"); + + // src ํด๋” ๋‚ด์˜ TypeScript/JavaScript ํŒŒ์ผ๋“ค ์ฐพ๊ธฐ + const files = await glob("src/**/*.{ts,tsx,js,jsx}", { + ignore: [ + "src/utils/logger.ts", // ๋กœ๊ฑฐ ํŒŒ์ผ ์ž์ฒด๋Š” ์ œ์™ธ + "src/**/*.test.{ts,tsx,js,jsx}", // ํ…Œ์ŠคํŠธ ํŒŒ์ผ ์ œ์™ธ + "src/**/*.spec.{ts,tsx,js,jsx}", // ์ŠคํŽ™ ํŒŒ์ผ ์ œ์™ธ + ], + cwd: process.cwd(), + }); + + console.log(`๐Ÿ“ ์ด ${files.length}๊ฐœ ํŒŒ์ผ ๊ฒ€์‚ฌ ์ค‘...\n`); + + let processedCount = 0; + let changedCount = 0; + + for (const file of files) { + const filePath = path.resolve(file); + console.log(`๐Ÿ” ์ฒ˜๋ฆฌ ์ค‘: ${file}`); + + const hasChanges = await processFile(filePath); + processedCount++; + + if (hasChanges) { + changedCount++; + console.log(` โœ… ๋ณ€๊ฒฝ ์™„๋ฃŒ`); + } else { + console.log(` โญ๏ธ ๋ณ€๊ฒฝ ์—†์Œ`); + } + + console.log(""); + } + + console.log("๐ŸŽ‰ Console.log โ†’ Logger ๊ต์ฒด ์ž‘์—… ์™„๋ฃŒ"); + console.log(`๐Ÿ“Š ๊ฒฐ๊ณผ: ${changedCount}/${processedCount} ํŒŒ์ผ ๋ณ€๊ฒฝ๋จ\n`); + + if (changedCount > 0) { + console.log("๐Ÿ”ง ๋‹ค์Œ ๋‹จ๊ณ„:"); + console.log("1. npm run build๋กœ ๋นŒ๋“œ ์—๋Ÿฌ ํ™•์ธ"); + console.log("2. ๋ณ€๊ฒฝ๋œ ํŒŒ์ผ๋“ค ๊ฒ€ํ† "); + console.log("3. ๊ฐœ๋ฐœ ์„œ๋ฒ„์—์„œ ๋กœ๊ทธ ์ถœ๋ ฅ ํ…Œ์ŠคํŠธ"); + } +} + +// ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ +if (require.main === module) { + main().catch(console.error); +} diff --git a/src/App.tsx b/src/App.tsx index 660ef7f..0f3c4e1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,22 +1,29 @@ -import React, { useEffect, useState, Suspense, Component, ErrorInfo, ReactNode } from 'react'; -import { Routes, Route } from 'react-router-dom'; -import { BudgetProvider } from './contexts/budget/BudgetContext'; -import { AuthProvider } from './contexts/auth/AuthProvider'; -import { Toaster } from './components/ui/toaster'; -import Index from './pages/Index'; -import Login from './pages/Login'; -import Register from './pages/Register'; -import Settings from './pages/Settings'; -import Transactions from './pages/Transactions'; -import Analytics from './pages/Analytics'; -import ProfileManagement from './pages/ProfileManagement'; -import NotFound from './pages/NotFound'; -import PaymentMethods from './pages/PaymentMethods'; -import HelpSupport from './pages/HelpSupport'; -import SecurityPrivacySettings from './pages/SecurityPrivacySettings'; -import NotificationSettings from './pages/NotificationSettings'; -import ForgotPassword from './pages/ForgotPassword'; -import AppwriteSettingsPage from './pages/AppwriteSettingsPage'; +import React, { + useEffect, + useState, + Component, + ErrorInfo, + ReactNode, +} from "react"; +import { logger } from "@/utils/logger"; +import { Routes, Route } from "react-router-dom"; +import { BudgetProvider } from "./contexts/budget/BudgetContext"; +import { AuthProvider } from "./contexts/auth/AuthProvider"; +import { Toaster } from "./components/ui/toaster"; +import Index from "./pages/Index"; +import Login from "./pages/Login"; +import Register from "./pages/Register"; +import Settings from "./pages/Settings"; +import Transactions from "./pages/Transactions"; +import Analytics from "./pages/Analytics"; +import ProfileManagement from "./pages/ProfileManagement"; +import NotFound from "./pages/NotFound"; +import PaymentMethods from "./pages/PaymentMethods"; +import HelpSupport from "./pages/HelpSupport"; +import SecurityPrivacySettings from "./pages/SecurityPrivacySettings"; +import NotificationSettings from "./pages/NotificationSettings"; +import ForgotPassword from "./pages/ForgotPassword"; +import AppwriteSettingsPage from "./pages/AppwriteSettingsPage"; // ๊ฐ„๋‹จํ•œ ์˜ค๋ฅ˜ ๊ฒฝ๊ณ„ ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„ interface ErrorBoundaryProps { @@ -40,23 +47,27 @@ class ErrorBoundary extends Component { } componentDidCatch(error: Error, errorInfo: ErrorInfo): void { - console.error('์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์˜ค๋ฅ˜:', error, errorInfo); + logger.error("์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์˜ค๋ฅ˜:", error, errorInfo); } render(): ReactNode { if (this.state.hasError) { // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ๋Œ€์ฒด UI ํ‘œ์‹œ - return this.props.fallback || ( -
-

์•ฑ ๋กœ๋”ฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค

-

์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.

- -
+ return ( + this.props.fallback || ( +
+

+ ์•ฑ ๋กœ๋”ฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค +

+

์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.

+ +
+ ) ); } @@ -74,13 +85,18 @@ const LoadingScreen: React.FC = () => ( ); // ์˜ค๋ฅ˜ ํ™”๋ฉด ์ปดํฌ๋„ŒํŠธ -const ErrorScreen: React.FC<{ error: Error | null; retry: () => void }> = ({ error, retry }) => ( +const ErrorScreen: React.FC<{ error: Error | null; retry: () => void }> = ({ + error, + retry, +}) => (
โš ๏ธ

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์˜ค๋ฅ˜

-

{error?.message || '์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋กœ๋”ฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'}

-
- - { - if (!isSubmitting) setShowExpenseDialog(open); - }}> + + { + if (!isSubmitting) { + setShowExpenseDialog(open); + } + }} + > ์ง€์ถœ ์ž…๋ ฅ - !isSubmitting && setShowExpenseDialog(false)} + !isSubmitting && setShowExpenseDialog(false)} isSubmitting={isSubmitting} /> diff --git a/src/components/AppVersionInfo.tsx b/src/components/AppVersionInfo.tsx index 55506a3..4f41878 100644 --- a/src/components/AppVersionInfo.tsx +++ b/src/components/AppVersionInfo.tsx @@ -1,8 +1,7 @@ - -import React, { useEffect, useState } from 'react'; -import { isAndroidPlatform, isIOSPlatform } from '@/utils/platform'; -import { Label } from '@/components/ui/label'; -import { Capacitor } from '@capacitor/core'; +import React, { useEffect, useState } from "react"; +import { isAndroidPlatform, isIOSPlatform } from "@/utils/platform"; +import { Label } from "@/components/ui/label"; +import { Capacitor } from "@capacitor/core"; // ๋ฒ„์ „ ์ •๋ณด ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜ interface VersionInfo { @@ -14,7 +13,7 @@ interface VersionInfo { timestamp?: number; error?: boolean; errorMessage?: string; - defaultValuesUsed?: boolean; + defaultValuesUsed?: boolean; } interface AppVersionInfoProps { @@ -26,18 +25,19 @@ interface AppVersionInfoProps { const AppVersionInfo: React.FC = ({ className, showDevInfo = true, - editable = false + editable = false, }) => { // ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋ฒ„์ „ ์ •๋ณด - ๋นŒ๋“œ ์Šคํฌ๋ฆฝํŠธ์—์„œ ์„ค์ •ํ•œ ๊ฐ’๊ณผ ์ผ์น˜์‹œ์ผœ์•ผ ํ•จ const hardcodedVersionInfo: VersionInfo = { - versionName: '1.1.8', + versionName: "1.1.8", buildNumber: 9, versionCode: 9, platform: Capacitor.getPlatform(), - defaultValuesUsed: false + defaultValuesUsed: false, }; - const [versionInfo, setVersionInfo] = useState(hardcodedVersionInfo); + const [versionInfo, setVersionInfo] = + useState(hardcodedVersionInfo); const [loading, setLoading] = useState(false); // ๊ฐœ๋ฐœ์ž ์ •๋ณด ํ‘œ์‹œ @@ -46,9 +46,13 @@ const AppVersionInfo: React.FC = ({ return (

- {versionInfo.versionCode ? `๋ฒ„์ „ ์ฝ”๋“œ: ${versionInfo.versionCode}` : ''} - {versionInfo.buildNumber ? `, ๋นŒ๋“œ: ${versionInfo.buildNumber}` : ''} - {versionInfo.platform ? ` (${versionInfo.platform})` : ''} + {versionInfo.versionCode + ? `๋ฒ„์ „ ์ฝ”๋“œ: ${versionInfo.versionCode}` + : ""} + {versionInfo.buildNumber + ? `, ๋นŒ๋“œ: ${versionInfo.buildNumber}` + : ""} + {versionInfo.platform ? ` (${versionInfo.platform})` : ""}

{versionInfo.errorMessage && (

์˜ค๋ฅ˜: {versionInfo.errorMessage}

@@ -64,11 +68,9 @@ const AppVersionInfo: React.FC = ({

- {loading ? ( - "๋ฒ„์ „ ์ •๋ณด ๋กœ๋”ฉ ์ค‘..." - ) : ( - versionInfo.versionName || "์•Œ ์ˆ˜ ์—†์Œ" - )} + {loading + ? "๋ฒ„์ „ ์ •๋ณด ๋กœ๋”ฉ ์ค‘..." + : versionInfo.versionName || "์•Œ ์ˆ˜ ์—†์Œ"}

{renderDevInfo()}
diff --git a/src/components/AvatarImageView.tsx b/src/components/AvatarImageView.tsx index c2475cb..9c22f6a 100644 --- a/src/components/AvatarImageView.tsx +++ b/src/components/AvatarImageView.tsx @@ -1,7 +1,8 @@ -import React, { useEffect, useState } from 'react'; -import { Capacitor } from '@capacitor/core'; -import { Avatar, AvatarFallback } from '@/components/ui/avatar'; -import { Skeleton } from '@/components/ui/skeleton'; +import React, { useEffect, useState } from "react"; +import { logger } from "@/utils/logger"; +import { Capacitor } from "@capacitor/core"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { Skeleton } from "@/components/ui/skeleton"; interface AvatarImageViewProps { className?: string; @@ -10,11 +11,11 @@ interface AvatarImageViewProps { const AvatarImageView: React.FC = ({ className = "h-12 w-12", - fallback = "ZY" + fallback = "ZY", }) => { const [loaded, setLoaded] = useState(false); const [error, setError] = useState(false); - const [imageSrc, setImageSrc] = useState('/zellyy.png'); + const [imageSrc, setImageSrc] = useState("/zellyy.png"); useEffect(() => { const loadImage = async () => { @@ -22,40 +23,41 @@ const AvatarImageView: React.FC = ({ // ํ”Œ๋žซํผ ์ฒดํฌ if (Capacitor.isNativePlatform()) { const platform = Capacitor.getPlatform(); - - if (platform === 'android') { + + if (platform === "android") { // Android์—์„œ๋Š” res/mipmap ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ - setImageSrc('file:///android_asset/public/zellyy.png'); - + setImageSrc("file:///android_asset/public/zellyy.png"); + // ๋‹ค๋ฅธ ๊ฐ€๋Šฅํ•œ ๊ฒฝ๋กœ๋“ค const possiblePaths = [ - 'file:///android_asset/public/zellyy.png', - 'file:///android_res/mipmap/zellyy.png', - '@mipmap/zellyy', - 'mipmap/zellyy', - 'res/mipmap/zellyy.png', - '/zellyy.png', - './zellyy.png', - 'android.resource://com.lovable.zellyfinance/mipmap/zellyy', + "file:///android_asset/public/zellyy.png", + "file:///android_res/mipmap/zellyy.png", + "@mipmap/zellyy", + "mipmap/zellyy", + "res/mipmap/zellyy.png", + "/zellyy.png", + "./zellyy.png", + "android.resource://com.lovable.zellyfinance/mipmap/zellyy", ]; - + // ํ•˜๋“œ์ฝ”๋”ฉ๋œ Base64 ์ด๋ฏธ์ง€ - const fallbackBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFEmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjMtMDYtMzBUMjI6MDM6NTIrMDk6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIzLTA2LTMwVDIzOjIxOjI1KzA5OjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIzLTA2LTMwVDIzOjIxOjI1KzA5OjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjE0ZDg5ZjI0LTRmZjctNDUwOS1hNDA3LWM4ODZhM2Q4MDk0YSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxNGQ4OWYyNC00ZmY3LTQ1MDktYTQwNy1jODg2YTNkODA5NGEiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoxNGQ4OWYyNC00ZmY3LTQ1MDktYTQwNy1jODg2YTNkODA5NGEiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjE0ZDg5ZjI0LTRmZjctNDUwOS1hNDA3LWM4ODZhM2Q4MDk0YSIgc3RFdnQ6d2hlbj0iMjAyMy0wNi0zMFQyMjowMzo1MiswOTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKE1hY2ludG9zaCkiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+GWKCCQAAATNJREFUWIXtlTFuwzAQRV+QTgk8ZMjWA+QAOViGLJmzdSsydS1kypojZAqQJbNvYHgplCKRQrUoqUiGwrcbLXAffFCH3O3ESSEJEzHZJEwhDMMQQtg/t1OGkJnZ4fNzDODtYU4MYRwiLb5eX8YAHp+WiSH8Cl+DvQPTXZwYAuBPa3dlQ8i5YEgOYJe1KxvCZ3HLqk1MWULmG+y5gx9gvfTYkDyJKaBDDZzE5BqEVT1fzNkP5Fy3tUXECF9X07dtvb/+TJbLFIYXzX9aNwPxUMkBOm34q2TNI8IWEQ4R4RARDhHhEBHO5pzV/gprM7aWFQtExBcRDhHhEBEOEeEQEU5Ljtb5+D+13lx3nP7lWpedwuA7fNP5OYhrNrXc4xVlGHPyODEEwGY1/AFIBfclfLkBYAAAAABJRU5ErkJggg=='; - + const fallbackBase64 = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFEmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjMtMDYtMzBUMjI6MDM6NTIrMDk6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIzLTA2LTMwVDIzOjIxOjI1KzA5OjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIzLTA2LTMwVDIzOjIxOjI1KzA5OjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjE0ZDg5ZjI0LTRmZjctNDUwOS1hNDA3LWM4ODZhM2Q4MDk0YSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxNGQ4OWYyNC00ZmY3LTQ1MDktYTQwNy1jODg2YTNkODA5NGEiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoxNGQ4OWYyNC00ZmY3LTQ1MDktYTQwNy1jODg2YTNkODA5NGEiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjE0ZDg5ZjI0LTRmZjctNDUwOS1hNDA3LWM4ODZhM2Q4MDk0YSIgc3RFdnQ6d2hlbj0iMjAyMy0wNi0zMFQyMjowMzo1MiswOTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKE1hY2ludG9zaCkiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+GWKCCQAAATNJREFUWIXtlTFuwzAQRV+QTgk8ZMjWA+QAOViGLJmzdSsydS1kypojZAqQJbNvYHgplCKRQrUoqUiGwrcbLXAffFCH3O3ESSEJEzHZJEwhDMMQQtg/t1OGkJnZ4fNzDODtYU4MYRwiLb5eX8YAHp+WiSH8Cl+DvQPTXZwYAuBPa3dlQ8i5YEgOYJe1KxvCZ3HLqk1MWULmG+y5gx9gvfTYkDyJKaBDDZzE5BqEVT1fzNkP5Fy3tUXECF9X07dtvb/+TJbLFIYXzX9aNwPxUMkBOm34q2TNI8IWEQ4R4RARDhHhEBHO5pzV/gprM7aWFQtExBcRDhHhEBEOEeEQEU5Ljtb5+D+13lx3nP7lWpedwuA7fNP5OYhrNrXc4xVlGHPyODEEwGY1/AFIBfclfLkBYAAAAABJRU5ErkJggg=="; + // ๋งˆ์ง€๋ง‰ ์ˆ˜๋‹จ์œผ๋กœ Base64 ์‚ฌ์šฉ setImageSrc(fallbackBase64); - } else if (platform === 'ios') { + } else if (platform === "ios") { // iOS ๊ฒฝ๋กœ ์ฒ˜๋ฆฌ - setImageSrc('/zellyy.png'); + setImageSrc("/zellyy.png"); } } else { // ์›น์—์„œ๋Š” ์ผ๋ฐ˜ ๊ฒฝ๋กœ ์‚ฌ์šฉ - setImageSrc('/zellyy.png'); + setImageSrc("/zellyy.png"); } - + setLoaded(true); } catch (err) { - console.error('์ด๋ฏธ์ง€ ๋กœ๋“œ ์˜ค๋ฅ˜:', err); + logger.error("์ด๋ฏธ์ง€ ๋กœ๋“œ ์˜ค๋ฅ˜:", err); setError(true); } }; @@ -71,7 +73,7 @@ const AvatarImageView: React.FC = ({
) : ( <> - Zellyy = ({ - title, - current, +const BudgetCard: React.FC = ({ + title, + current, total, - color = 'neuro-income' + color = "neuro-income", }) => { const percentage = Math.min(Math.round((current / total) * 100), 100); - - const formattedCurrent = new Intl.NumberFormat('ko-KR', { - style: 'currency', - currency: 'KRW', - maximumFractionDigits: 0 + + const formattedCurrent = new Intl.NumberFormat("ko-KR", { + style: "currency", + currency: "KRW", + maximumFractionDigits: 0, }).format(current); - - const formattedTotal = new Intl.NumberFormat('ko-KR', { - style: 'currency', - currency: 'KRW', - maximumFractionDigits: 0 + + const formattedTotal = new Intl.NumberFormat("ko-KR", { + style: "currency", + currency: "KRW", + maximumFractionDigits: 0, }).format(total); - + // Determine progress bar color based on percentage - const progressBarColor = percentage >= 90 ? 'bg-yellow-400' : `bg-${color}`; - + const progressBarColor = percentage >= 90 ? "bg-yellow-400" : `bg-${color}`; + return (
-
- {categoryIcons[title]} -
+
{categoryIcons[title]}

{title}

- +

{formattedCurrent}

/ {formattedTotal}

- +
-
- +
- - {percentage}% - + {percentage}%
); diff --git a/src/components/BudgetCategoriesSection.tsx b/src/components/BudgetCategoriesSection.tsx index c066a9d..9f56e24 100644 --- a/src/components/BudgetCategoriesSection.tsx +++ b/src/components/BudgetCategoriesSection.tsx @@ -1,7 +1,9 @@ - -import React from 'react'; -import { categoryIcons, CATEGORY_DESCRIPTIONS } from '@/constants/categoryIcons'; -import { formatCurrency } from '@/utils/formatters'; +import React from "react"; +import { + categoryIcons, + CATEGORY_DESCRIPTIONS, +} from "@/constants/categoryIcons"; +import { formatCurrency } from "@/utils/formatters"; interface BudgetCategoriesSectionProps { categories: { @@ -12,70 +14,100 @@ interface BudgetCategoriesSectionProps { } const BudgetCategoriesSection: React.FC = ({ - categories + categories, }) => { - return <> -

์ง€์ถœ ๊ทธ๋ž˜ํ”„

-
- {categories.map((category, index) => { - // ์˜ˆ์‚ฐ ์ดˆ๊ณผ ์—ฌ๋ถ€ ํ™•์ธ - const isOverBudget = category.current > category.total && category.total > 0; - // ์‹ค์ œ ๋ฐฑ๋ถ„์œจ ๊ณ„์‚ฐ (์ดˆ๊ณผํ•ด๋„ ์‹ค์ œ ํผ์„ผํŠธ๋กœ ํ‘œ์‹œ) - const actualPercentage = category.total > 0 ? Math.round(category.current / category.total * 100) : 0; - // ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ”์šฉ ํผ์„ผํŠธ - ์ œํ•œ ์—†์ด ์‹ค์ œ ํผ์„ผํŠธ ํ‘œ์‹œ - const displayPercentage = actualPercentage; + return ( + <> +

์ง€์ถœ ๊ทธ๋ž˜ํ”„

+
+ {categories.map((category, index) => { + // ์˜ˆ์‚ฐ ์ดˆ๊ณผ ์—ฌ๋ถ€ ํ™•์ธ + const isOverBudget = + category.current > category.total && category.total > 0; + // ์‹ค์ œ ๋ฐฑ๋ถ„์œจ ๊ณ„์‚ฐ (์ดˆ๊ณผํ•ด๋„ ์‹ค์ œ ํผ์„ผํŠธ๋กœ ํ‘œ์‹œ) + const actualPercentage = + category.total > 0 + ? Math.round((category.current / category.total) * 100) + : 0; + // ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ”์šฉ ํผ์„ผํŠธ - ์ œํ•œ ์—†์ด ์‹ค์ œ ํผ์„ผํŠธ ํ‘œ์‹œ + const displayPercentage = actualPercentage; - // ์˜ˆ์‚ฐ์ด ์–ผ๋งˆ ๋‚จ์ง€ ์•Š์€ ๊ฒฝ์šฐ (10% ๋ฏธ๋งŒ) - const isLowBudget = category.total > 0 && actualPercentage >= 90 && actualPercentage < 100; + // ์˜ˆ์‚ฐ์ด ์–ผ๋งˆ ๋‚จ์ง€ ์•Š์€ ๊ฒฝ์šฐ (10% ๋ฏธ๋งŒ) + const isLowBudget = + category.total > 0 && + actualPercentage >= 90 && + actualPercentage < 100; - // ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” ์ƒ‰์ƒ ๊ฒฐ์ • - const progressBarColor = isOverBudget ? 'bg-red-500' : isLowBudget ? 'bg-yellow-400' : 'bg-neuro-income'; + // ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” ์ƒ‰์ƒ ๊ฒฐ์ • + const progressBarColor = isOverBudget + ? "bg-red-500" + : isLowBudget + ? "bg-yellow-400" + : "bg-neuro-income"; - // ๋‚จ์€ ์˜ˆ์‚ฐ ๋˜๋Š” ์ดˆ๊ณผ ์˜ˆ์‚ฐ - const budgetStatusText = isOverBudget ? '์˜ˆ์‚ฐ ์ดˆ๊ณผ: ' : '๋‚จ์€ ์˜ˆ์‚ฐ: '; - const budgetAmount = isOverBudget ? Math.abs(category.total - category.current) : Math.max(0, category.total - category.current); - - // ์นดํ…Œ๊ณ ๋ฆฌ ์„ค๋ช… ๊ฐ€์ ธ์˜ค๊ธฐ - const description = CATEGORY_DESCRIPTIONS[category.title] || ''; - - return
-
-
- {categoryIcons[category.title]} + // ๋‚จ์€ ์˜ˆ์‚ฐ ๋˜๋Š” ์ดˆ๊ณผ ์˜ˆ์‚ฐ + const budgetStatusText = isOverBudget ? "์˜ˆ์‚ฐ ์ดˆ๊ณผ: " : "๋‚จ์€ ์˜ˆ์‚ฐ: "; + const budgetAmount = isOverBudget + ? Math.abs(category.total - category.current) + : Math.max(0, category.total - category.current); + + // ์นดํ…Œ๊ณ ๋ฆฌ ์„ค๋ช… ๊ฐ€์ ธ์˜ค๊ธฐ + const description = CATEGORY_DESCRIPTIONS[category.title] || ""; + + return ( +
+
+
+ {categoryIcons[category.title]} +
+

+ {category.title} + {description && ( + + {description} + + )} +

+
+ +
+

+ {formatCurrency(category.current)} +

+

+ / {formatCurrency(category.total)} +

+
+ +
+
+
+ +
+ + {budgetStatusText} + {formatCurrency(budgetAmount)} + + + {displayPercentage}% +
-

- {category.title} - {description && ( - - {description} - - )} -

- -
-

{formatCurrency(category.current)}

-

/ {formatCurrency(category.total)}

-
- -
-
-
- -
- - {budgetStatusText}{formatCurrency(budgetAmount)} - - - {displayPercentage}% - -
-
; - })} -
- ; + ); + })} +
+ + ); }; export default BudgetCategoriesSection; diff --git a/src/components/BudgetInputCard.tsx b/src/components/BudgetInputCard.tsx index 137c41b..b1a7390 100644 --- a/src/components/BudgetInputCard.tsx +++ b/src/components/BudgetInputCard.tsx @@ -1,10 +1,14 @@ - -import React, { useState, useEffect } from 'react'; -import { Input } from '@/components/ui/input'; -import { Button } from '@/components/ui/button'; -import { Check, ChevronDown, ChevronUp, Wallet } from 'lucide-react'; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; -import { markBudgetAsModified } from '@/utils/sync/budget/modifiedBudgetsTracker'; +import React, { useState, useEffect } from "react"; +import { logger } from "@/utils/logger"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Check, ChevronDown, ChevronUp, Wallet } from "lucide-react"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { markBudgetAsModified } from "@/utils/sync/budget/modifiedBudgetsTracker"; interface BudgetGoalProps { initialBudgets: { @@ -12,85 +16,91 @@ interface BudgetGoalProps { weekly: number; monthly: number; }; - onSave: (type: 'daily' | 'weekly' | 'monthly', amount: number) => void; + onSave: (type: "daily" | "weekly" | "monthly", amount: number) => void; highlight?: boolean; } const BudgetInputCard: React.FC = ({ initialBudgets, onSave, - highlight = false + highlight = false, }) => { const [budgetInput, setBudgetInput] = useState( - initialBudgets.monthly > 0 ? initialBudgets.monthly.toString() : '' + initialBudgets.monthly > 0 ? initialBudgets.monthly.toString() : "" ); const [isOpen, setIsOpen] = useState(highlight); // Format with commas for display const formatWithCommas = (amount: string) => { // Remove commas first to handle re-formatting - const numericValue = amount.replace(/,/g, ''); - return numericValue.replace(/\B(?=(\d{3})+(?!\d))/g, ','); + const numericValue = amount.replace(/,/g, ""); + return numericValue.replace(/\B(?=(\d{3})+(?!\d))/g, ","); }; // ์ดˆ๊ธฐ๊ฐ’ ๋ณ€๊ฒฝ์‹œ ์ž…๋ ฅ ํ•„๋“œ ๊ฐ’ ์—…๋ฐ์ดํŠธ useEffect(() => { - console.log("BudgetInputCard - ์ดˆ๊ธฐ ์˜ˆ์‚ฐ๊ฐ’ ์—…๋ฐ์ดํŠธ:", initialBudgets); - setBudgetInput(initialBudgets.monthly > 0 ? initialBudgets.monthly.toString() : ''); + logger.info("BudgetInputCard - ์ดˆ๊ธฐ ์˜ˆ์‚ฐ๊ฐ’ ์—…๋ฐ์ดํŠธ:", initialBudgets); + setBudgetInput( + initialBudgets.monthly > 0 ? initialBudgets.monthly.toString() : "" + ); }, [initialBudgets]); const handleInputChange = (value: string) => { // Remove all non-numeric characters - const numericValue = value.replace(/[^0-9]/g, ''); + const numericValue = value.replace(/[^0-9]/g, ""); setBudgetInput(numericValue); }; const handleSave = () => { - const amount = parseInt(budgetInput.replace(/,/g, ''), 10) || 0; + const amount = parseInt(budgetInput.replace(/,/g, ""), 10) || 0; if (amount <= 0) { return; // 0 ์ดํ•˜์˜ ๊ธˆ์•ก์€ ์ €์žฅํ•˜์ง€ ์•Š์Œ } - + // ์ฆ‰์‹œ ์ž…๋ ฅ ํ•„๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ”ผ๋“œ๋ฐฑ ์ œ๊ณต setBudgetInput(amount.toString()); - + // ์ฆ‰์‹œ ์ฝœ๋žฉ์‹œ๋ธ”์„ ๋‹ซ์•„ ์‚ฌ์šฉ์ž์—๊ฒŒ ์™„๋ฃŒ ํ”ผ๋“œ๋ฐฑ ์ œ๊ณต setIsOpen(false); - + // ์˜ˆ์‚ฐ ๋ณ€๊ฒฝ ์‹œ ์ˆ˜์ • ์ถ”์  ์‹œ์Šคํ…œ์— ๊ธฐ๋ก try { markBudgetAsModified(amount); - console.log(`[์˜ˆ์‚ฐ ์ถ”์ ] ์›”๊ฐ„ ์˜ˆ์‚ฐ ๋ณ€๊ฒฝ ์ถ”์ : ${amount}์›`); + logger.info(`[์˜ˆ์‚ฐ ์ถ”์ ] ์›”๊ฐ„ ์˜ˆ์‚ฐ ๋ณ€๊ฒฝ ์ถ”์ : ${amount}์›`); } catch (error) { - console.error('[์˜ˆ์‚ฐ ์ถ”์ ] ์˜ˆ์‚ฐ ๋ณ€๊ฒฝ ์ถ”์  ์‹คํŒจ:', error); + logger.error("[์˜ˆ์‚ฐ ์ถ”์ ] ์˜ˆ์‚ฐ ๋ณ€๊ฒฝ ์ถ”์  ์‹คํŒจ:", error); } - - console.log(`BudgetInputCard - ์ €์žฅ ๋ฒ„ํŠผ ํด๋ฆญ: ์›”๊ฐ„ ์˜ˆ์‚ฐ = ${amount}์›`); - + + logger.info(`BudgetInputCard - ์ €์žฅ ๋ฒ„ํŠผ ํด๋ฆญ: ์›”๊ฐ„ ์˜ˆ์‚ฐ = ${amount}์›`); + // ์˜ˆ์‚ฐ ์ €์žฅ (ํ•ญ์ƒ monthly๋กœ ์ €์žฅ) - onSave('monthly', amount); + onSave("monthly", amount); }; // ๋น„์–ด์žˆ์œผ๋ฉด ๋นˆ ๋ฌธ์ž์—ด์„, ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ํฌ๋งทํŒ…๋œ ๋ฌธ์ž์—ด์„ ๋ฐ˜ํ™˜ const getDisplayValue = () => { - return budgetInput === '' ? '' : formatWithCommas(budgetInput); + return budgetInput === "" ? "" : formatWithCommas(budgetInput); }; // ๊ธˆ์•ก์„ ํ‘œ์‹œํ•  ๋•Œ 0์›์ด๋ฉด '์„ค์ •๋˜์ง€ ์•Š์Œ'์œผ๋กœ ํ‘œ์‹œ const getGoalDisplayText = () => { - const amount = parseInt(budgetInput.replace(/,/g, ''), 10) || 0; - if (amount === 0) return '์„ค์ •๋˜์ง€ ์•Š์Œ'; - return formatWithCommas(budgetInput) + '์›'; + const amount = parseInt(budgetInput.replace(/,/g, ""), 10) || 0; + if (amount === 0) { + return "์„ค์ •๋˜์ง€ ์•Š์Œ"; + } + return formatWithCommas(budgetInput) + "์›"; }; return ( - - + {highlight && } ์›”๊ฐ„ ์˜ˆ์‚ฐ ์„ค์ •ํ•˜๊ธฐ @@ -100,17 +110,21 @@ const BudgetInputCard: React.FC = ({ )} - +
- handleInputChange(e.target.value)} - placeholder="๋ชฉํ‘œ ๊ธˆ์•ก ์ž…๋ ฅ" - className="neuro-pressed" + onChange={(e) => handleInputChange(e.target.value)} + placeholder="๋ชฉํ‘œ ๊ธˆ์•ก ์ž…๋ ฅ" + className="neuro-pressed" /> -
diff --git a/src/components/BudgetProgress.tsx b/src/components/BudgetProgress.tsx index faf0ca0..4ef326c 100644 --- a/src/components/BudgetProgress.tsx +++ b/src/components/BudgetProgress.tsx @@ -1,5 +1,4 @@ - -import React from 'react'; +import React from "react"; interface BudgetProgressProps { spentAmount: number; @@ -12,25 +11,27 @@ const BudgetProgress: React.FC = ({ spentAmount, targetAmount, percentage, - formatCurrency + formatCurrency, }) => { // NaN ๊ฐ’์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด percentage๊ฐ€ ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ 0์œผ๋กœ ํ‘œ์‹œ const displayPercentage = isNaN(percentage) ? 0 : percentage; - + return (

{formatCurrency(spentAmount)}

-

/ {formatCurrency(targetAmount)}

+

+ / {formatCurrency(targetAmount)} +

- +
-
= 90 ? "bg-yellow-400" : "bg-neuro-income"}`} +
= 90 ? "bg-yellow-400" : "bg-neuro-income"}`} />
- +
{displayPercentage}% diff --git a/src/components/BudgetProgressCard.tsx b/src/components/BudgetProgressCard.tsx index e23ccf3..a8c420f 100644 --- a/src/components/BudgetProgressCard.tsx +++ b/src/components/BudgetProgressCard.tsx @@ -1,7 +1,7 @@ - -import React, { useEffect, useState } from 'react'; -import BudgetTabContent from './BudgetTabContent'; -import { BudgetPeriod, BudgetData } from '@/contexts/budget/types'; +import React, { useEffect, useState } from "react"; +import { logger } from "@/utils/logger"; +import BudgetTabContent from "./BudgetTabContent"; +import { BudgetPeriod, BudgetData } from "@/contexts/budget/types"; interface BudgetProgressCardProps { budgetData: BudgetData; @@ -9,7 +9,11 @@ interface BudgetProgressCardProps { setSelectedTab: (value: string) => void; formatCurrency: (amount: number) => string; calculatePercentage: (spent: number, target: number) => number; - onSaveBudget: (type: BudgetPeriod, amount: number, newCategoryBudgets?: Record) => void; + onSaveBudget: ( + type: BudgetPeriod, + amount: number, + newCategoryBudgets?: Record + ) => void; } const BudgetProgressCard: React.FC = ({ @@ -18,57 +22,63 @@ const BudgetProgressCard: React.FC = ({ setSelectedTab, formatCurrency, calculatePercentage, - onSaveBudget + onSaveBudget, }) => { // ๋ฐ์ดํ„ฐ ์ƒํƒœ ์ถ”์  (๋ถˆ์ผ์น˜ ๊ฐ์ง€๋ฅผ ์œ„ํ•œ ๋กœ์ปฌ ์ƒํƒœ) const [localBudgetData, setLocalBudgetData] = useState(budgetData); // ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ๋ฐ budgetData ๋ณ€๊ฒฝ ์‹œ ์—…๋ฐ์ดํŠธ useEffect(() => { - console.log("BudgetProgressCard ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ - ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:", budgetData); - console.log("์›”๊ฐ„ ์˜ˆ์‚ฐ:", budgetData.monthly.targetAmount); + logger.info( + "BudgetProgressCard ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ - ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:", + budgetData + ); + logger.info("์›”๊ฐ„ ์˜ˆ์‚ฐ:", budgetData.monthly.targetAmount); setLocalBudgetData(budgetData); - + // ์ง€์—ฐ ์ž‘์—…์œผ๋กœ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ํ›„ ๋ฐ์ดํ„ฐ ๊ฐฑ์‹ ) const timeoutId = setTimeout(() => { - window.dispatchEvent(new Event('budgetDataUpdated')); + window.dispatchEvent(new Event("budgetDataUpdated")); }, 300); - + return () => clearTimeout(timeoutId); }, [budgetData]); // ์ดˆ๊ธฐ ํƒญ ์„ค์ •์„ ์œ„ํ•œ ํšจ๊ณผ useEffect(() => { if (!selectedTab || selectedTab !== "monthly") { - console.log("์ดˆ๊ธฐ ํƒญ ์„ค์ •: monthly"); - setSelectedTab('monthly'); + logger.info("์ดˆ๊ธฐ ํƒญ ์„ค์ •: monthly"); + setSelectedTab("monthly"); } }, []); // budgetDataUpdated ์ด๋ฒคํŠธ ๊ฐ์ง€ useEffect(() => { const handleBudgetDataUpdated = () => { - console.log("BudgetProgressCard: ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ์ด๋ฒคํŠธ ๊ฐ์ง€"); + logger.info("BudgetProgressCard: ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ์ด๋ฒคํŠธ ๊ฐ์ง€"); }; - window.addEventListener('budgetDataUpdated', handleBudgetDataUpdated); - return () => window.removeEventListener('budgetDataUpdated', handleBudgetDataUpdated); + window.addEventListener("budgetDataUpdated", handleBudgetDataUpdated); + return () => + window.removeEventListener("budgetDataUpdated", handleBudgetDataUpdated); }, []); // ์›”๊ฐ„ ์˜ˆ์‚ฐ ์„ค์ • ์—ฌ๋ถ€ ๊ณ„์‚ฐ const isMonthlyBudgetSet = budgetData.monthly.targetAmount > 0; - console.log(`BudgetProgressCard ์ƒํƒœ: ์›”=${isMonthlyBudgetSet}`); + logger.info(`BudgetProgressCard ์ƒํƒœ: ์›”=${isMonthlyBudgetSet}`); return (
์ง€์ถœ / ์˜ˆ์‚ฐ
- - onSaveBudget('monthly', amount, categoryBudgets)} + + + onSaveBudget("monthly", amount, categoryBudgets) + } />
); diff --git a/src/components/BudgetTabContent.tsx b/src/components/BudgetTabContent.tsx index 6f938a1..ca91029 100644 --- a/src/components/BudgetTabContent.tsx +++ b/src/components/BudgetTabContent.tsx @@ -1,11 +1,11 @@ - -import React, { useState } from 'react'; -import { useBudgetTabContent } from '@/hooks/budget/useBudgetTabContent'; -import BudgetHeader from './budget/BudgetHeader'; -import BudgetProgressBar from './budget/BudgetProgressBar'; -import BudgetStatusDisplay from './budget/BudgetStatusDisplay'; -import BudgetInputButton from './budget/BudgetInputButton'; -import BudgetDialog from './budget/BudgetDialog'; +import React, { useState } from "react"; +import { logger } from "@/utils/logger"; +import { useBudgetTabContent } from "@/hooks/budget/useBudgetTabContent"; +import BudgetHeader from "./budget/BudgetHeader"; +import BudgetProgressBar from "./budget/BudgetProgressBar"; +import BudgetStatusDisplay from "./budget/BudgetStatusDisplay"; +import BudgetInputButton from "./budget/BudgetInputButton"; +import BudgetDialog from "./budget/BudgetDialog"; interface BudgetData { targetAmount: number; @@ -17,18 +17,21 @@ interface BudgetTabContentProps { data: BudgetData; formatCurrency: (amount: number) => string; calculatePercentage: (spent: number, target: number) => number; - onSaveBudget: (amount: number, categoryBudgets?: Record) => void; + onSaveBudget: ( + amount: number, + categoryBudgets?: Record + ) => void; } const BudgetTabContent: React.FC = ({ data, formatCurrency, calculatePercentage, - onSaveBudget + onSaveBudget, }) => { const [showBudgetDialog, setShowBudgetDialog] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); - + const { categoryBudgets, handleCategoryInputChange, @@ -42,17 +45,17 @@ const BudgetTabContent: React.FC = ({ budgetStatusText, budgetAmount, budgetButtonText, - calculateTotalBudget + calculateTotalBudget, } = useBudgetTabContent({ data, calculatePercentage, - onSaveBudget + onSaveBudget, }); const handleOpenDialog = () => { setShowBudgetDialog(true); }; - + const handleSaveBudget = () => { setIsSubmitting(true); try { @@ -68,26 +71,26 @@ const BudgetTabContent: React.FC = ({ // ์›”๊ฐ„ ์˜ˆ์‚ฐ ๋ชจ๋“œ ๋กœ๊น… React.useEffect(() => { - console.log('BudgetTabContent ๋ Œ๋”๋ง: ์›”๊ฐ„ ์˜ˆ์‚ฐ'); - console.log('ํ˜„์žฌ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:', data); + logger.info("BudgetTabContent ๋ Œ๋”๋ง: ์›”๊ฐ„ ์˜ˆ์‚ฐ"); + logger.info("ํ˜„์žฌ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:", data); }, [data]); return (
{isBudgetSet ? ( <> - - - = ({ ) : null} - - ; @@ -12,51 +12,63 @@ interface CategoryBudgetInputsProps { const CategoryBudgetInputs: React.FC = ({ categoryBudgets, - handleCategoryInputChange + handleCategoryInputChange, }) => { const isMobile = useIsMobile(); // Format number with commas for display const formatWithCommas = (value: number): string => { - if (value === 0) return ''; - return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + if (value === 0) { + return ""; + } + return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); }; // Handle input with comma formatting - const handleInput = (e: React.ChangeEvent, category: string) => { + const handleInput = ( + e: React.ChangeEvent, + category: string + ) => { // Remove all non-numeric characters before passing to parent handler - const numericValue = e.target.value.replace(/[^0-9]/g, ''); + const numericValue = e.target.value.replace(/[^0-9]/g, ""); handleCategoryInputChange(numericValue, category); - + // ์ˆ˜์ •๋œ ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ถ”์  ์‹œ์Šคํ…œ์— ๊ธฐ๋ก try { const amount = parseInt(numericValue, 10) || 0; markSingleCategoryBudgetAsModified(category, amount); - console.log(`์นดํ…Œ๊ณ ๋ฆฌ '${category}' ์˜ˆ์‚ฐ์„ ${amount}์›์œผ๋กœ ์ˆ˜์ • ์™„๋ฃŒ, ํƒ€์ž„์Šคํƒฌํ”„: ${new Date().toISOString()}`); + logger.info( + `์นดํ…Œ๊ณ ๋ฆฌ '${category}' ์˜ˆ์‚ฐ์„ ${amount}์›์œผ๋กœ ์ˆ˜์ • ์™„๋ฃŒ, ํƒ€์ž„์Šคํƒฌํ”„: ${new Date().toISOString()}` + ); } catch (error) { - console.error(`์นดํ…Œ๊ณ ๋ฆฌ '${category}' ์˜ˆ์‚ฐ ๋ณ€๊ฒฝ ์ถ”์  ์‹คํŒจ:`, error); + logger.error(`์นดํ…Œ๊ณ ๋ฆฌ '${category}' ์˜ˆ์‚ฐ ๋ณ€๊ฒฝ ์ถ”์  ์‹คํŒจ:`, error); } - + // ์‚ฌ์šฉ์ž์—๊ฒŒ ์‹œ๊ฐ์  ํ”ผ๋“œ๋ฐฑ ์ œ๊ณต - e.target.classList.add('border-green-500'); + e.target.classList.add("border-green-500"); setTimeout(() => { - e.target.classList.remove('border-green-500'); + e.target.classList.remove("border-green-500"); }, 300); }; return (
- {EXPENSE_CATEGORIES.map(category => ( -
+ {EXPENSE_CATEGORIES.map((category) => ( +
{categoryIcons[category]} - +
- handleInput(e, category)} placeholder="์˜ˆ์‚ฐ ์ž…๋ ฅ" - className={`transition-colors duration-300 ${isMobile ? 'w-[150px]' : 'max-w-[150px]'}`} + className={`transition-colors duration-300 ${isMobile ? "w-[150px]" : "max-w-[150px]"}`} />
))} diff --git a/src/components/ExpenseChart.tsx b/src/components/ExpenseChart.tsx index df06f62..af78848 100644 --- a/src/components/ExpenseChart.tsx +++ b/src/components/ExpenseChart.tsx @@ -1,7 +1,6 @@ - -import React from 'react'; -import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts'; -import { getCategoryColor } from '@/utils/categoryColorUtils'; +import React from "react"; +import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts"; +import { getCategoryColor } from "@/utils/categoryColorUtils"; interface ExpenseData { name: string; @@ -26,9 +25,9 @@ const ExpenseChart: React.FC = ({ data }) => { paddingAngle={5} dataKey="value" labelLine={false} - label={({ name, percent }) => ( + label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%` - )} + } fontSize={12} > {data.map((entry, index) => ( diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 102180c..48ca2b5 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,54 +1,53 @@ - -import React, { useState, useEffect } from 'react'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import { Skeleton } from '@/components/ui/skeleton'; -import { useAuth } from '@/contexts/auth'; -import { useIsMobile } from '@/hooks/use-mobile'; -import { isIOSPlatform } from '@/utils/platform'; -import NotificationPopover from './notification/NotificationPopover'; -import useNotifications from '@/hooks/useNotifications'; +import React, { useState, useEffect } from "react"; +import { logger } from "@/utils/logger"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Skeleton } from "@/components/ui/skeleton"; +import { useAuth } from "@/contexts/auth"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { isIOSPlatform } from "@/utils/platform"; +import NotificationPopover from "./notification/NotificationPopover"; +import useNotifications from "@/hooks/useNotifications"; const Header: React.FC = () => { - const { - user - } = useAuth(); - const userName = user?.user_metadata?.username || '์ต๋ช…'; + const { user } = useAuth(); + const userName = user?.user_metadata?.username || "์ต๋ช…"; const [imageLoaded, setImageLoaded] = useState(false); const [imageError, setImageError] = useState(false); const isMobile = useIsMobile(); const [isIOS, setIsIOS] = useState(false); - const { notifications, clearAllNotifications, markAsRead } = useNotifications(); + const { notifications, clearAllNotifications, markAsRead } = + useNotifications(); // ํ”Œ๋žซํผ ๊ฐ์ง€ useEffect(() => { const checkPlatform = async () => { try { const isiOS = isIOSPlatform(); - console.log('Header: iOS ํ”Œ๋žซํผ ๊ฐ์ง€ ๊ฒฐ๊ณผ:', isiOS); + logger.info("Header: iOS ํ”Œ๋žซํผ ๊ฐ์ง€ ๊ฒฐ๊ณผ:", isiOS); setIsIOS(isiOS); } catch (error) { - console.error('ํ”Œ๋žซํผ ๊ฐ์ง€ ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("ํ”Œ๋žซํผ ๊ฐ์ง€ ์ค‘ ์˜ค๋ฅ˜:", error); } }; - + checkPlatform(); }, []); // ์ด๋ฏธ์ง€ ํ”„๋ฆฌ๋กœ๋”ฉ ์ฒ˜๋ฆฌ useEffect(() => { const preloadImage = new Image(); - preloadImage.src = '/zellyy.png'; + preloadImage.src = "/zellyy.png"; preloadImage.onload = () => { setImageLoaded(true); }; preloadImage.onerror = () => { - console.error('์•„๋ฐ”ํƒ€ ์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ'); + logger.error("์•„๋ฐ”ํƒ€ ์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ"); setImageError(true); }; }, []); // iOS ์ „์šฉ ํ—ค๋” ํด๋ž˜์Šค - ์•ˆ์ „ ์˜์—ญ ์ ์šฉ - const headerClass = isIOS ? 'ios-notch-padding' : 'py-4'; + const headerClass = isIOS ? "ios-notch-padding" : "py-4"; return (
@@ -61,26 +60,28 @@ const Header: React.FC = () => {
) : ( <> - setImageLoaded(true)} - onError={() => setImageError(true)} + setImageLoaded(true)} + onError={() => setImageError(true)} /> - {(imageError || !imageLoaded) && ZY} + {(imageError || !imageLoaded) && ( + ZY + )} )}

- {user ? `${userName}๋‹˜, ๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค` : '๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค'} + {user ? `${userName}๋‹˜, ๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค` : "๋ฐ˜๊ฐ‘์Šต๋‹ˆ๋‹ค"}

์ ค๋ฆฌ์˜ ์ ์žํƒˆ์ถœ

- = ({ - resourceName, +const NativeImage: React.FC = ({ + resourceName, className, alt = "์ด๋ฏธ์ง€", - fallback = "ZY" + fallback = "ZY", }) => { const [loading, setLoading] = useState(true); const [error, setError] = useState(false); @@ -26,12 +27,12 @@ const NativeImage: React.FC = ({ try { if (Capacitor.isNativePlatform()) { // ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ๋Š” ๋ฆฌ์†Œ์Šค ID๋ฅผ ์‚ฌ์šฉ - if (Capacitor.getPlatform() === 'android') { + if (Capacitor.getPlatform() === "android") { // ์›น๋ทฐ๊ฐ€ resource:// ํ”„๋กœํ† ์ฝœ์„ ์ง€์›ํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ์œ„ํ•œ ์ฝ”๋“œ setImageSrc(`file:///android_res/drawable/${resourceName}`); } else { // iOS - ๋‹ค๋ฅธ ๋ฐฉ์‹ ์ ์šฉ (์ถ”ํ›„ ๊ตฌํ˜„) - setImageSrc('/zellyy.png'); + setImageSrc("/zellyy.png"); } } else { // ์›น์—์„œ๋Š” ์ผ๋ฐ˜ ๊ฒฝ๋กœ ์‚ฌ์šฉ @@ -39,7 +40,7 @@ const NativeImage: React.FC = ({ } setLoading(false); } catch (err) { - console.error('์ด๋ฏธ์ง€ ๋กœ๋“œ ์˜ค๋ฅ˜:', err); + logger.error("์ด๋ฏธ์ง€ ๋กœ๋“œ ์˜ค๋ฅ˜:", err); setError(true); setLoading(false); } @@ -57,16 +58,14 @@ const NativeImage: React.FC = ({ ) : ( <> {!error && ( - {alt} setError(true)} /> )} - {error && ( - {fallback} - )} + {error && {fallback}} )} diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx index febaa8c..0591130 100644 --- a/src/components/NavBar.tsx +++ b/src/components/NavBar.tsx @@ -1,32 +1,48 @@ - -import React from 'react'; -import { useNavigate, useLocation } from 'react-router-dom'; -import { Home, BarChart2, Calendar, Settings } from 'lucide-react'; -import { cn } from '@/lib/utils'; +import React from "react"; +import { useNavigate, useLocation } from "react-router-dom"; +import { Home, BarChart2, Calendar, Settings } from "lucide-react"; +import { cn } from "@/lib/utils"; const NavBar = () => { const navigate = useNavigate(); const location = useLocation(); - + // ์„ค์ • ๊ด€๋ จ ๊ฒฝ๋กœ ๋ชฉ๋ก ์ถ”๊ฐ€ const settingsRelatedPaths = [ - '/settings', - '/profile', - '/security-privacy', - '/help-support', - '/payment-methods', - '/notifications' + "/settings", + "/profile", + "/security-privacy", + "/help-support", + "/payment-methods", + "/notifications", ]; - - const isSettingsActive = settingsRelatedPaths.some(path => location.pathname === path); - + + const isSettingsActive = settingsRelatedPaths.some( + (path) => location.pathname === path + ); + const navItems = [ - { icon: Home, label: 'ํ™ˆ', path: '/', isActive: location.pathname === '/' }, - { icon: Calendar, label: '์ง€์ถœ', path: '/transactions', isActive: location.pathname === '/transactions' }, - { icon: BarChart2, label: '๋ถ„์„', path: '/analytics', isActive: location.pathname === '/analytics' }, - { icon: Settings, label: '์„ค์ •', path: '/settings', isActive: isSettingsActive }, + { icon: Home, label: "ํ™ˆ", path: "/", isActive: location.pathname === "/" }, + { + icon: Calendar, + label: "์ง€์ถœ", + path: "/transactions", + isActive: location.pathname === "/transactions", + }, + { + icon: BarChart2, + label: "๋ถ„์„", + path: "/analytics", + isActive: location.pathname === "/analytics", + }, + { + icon: Settings, + label: "์„ค์ •", + path: "/settings", + isActive: isSettingsActive, + }, ]; - + return (
@@ -40,7 +56,7 @@ const NavBar = () => { item.isActive ? "text-neuro-income" : "text-gray-500" )} > -
= ({ transactions, - onUpdateTransaction + onUpdateTransaction, }) => { const { updateTransaction, deleteTransaction } = useBudget(); - + // ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ๊ด€๋ จ ๋กœ์ง์€ ์ปค์Šคํ…€ ํ›…์œผ๋กœ ๋ถ„๋ฆฌ - const { handleDeleteTransaction, isDeleting } = useRecentTransactions(deleteTransaction); - + const { handleDeleteTransaction, isDeleting } = + useRecentTransactions(deleteTransaction); + // ๋‹ค์ด์–ผ๋กœ๊ทธ ๊ด€๋ จ ๋กœ์ง ๋ถ„๋ฆฌ - const { - selectedTransaction, - isDialogOpen, - handleTransactionClick, - setIsDialogOpen + const { + selectedTransaction, + isDialogOpen, + handleTransactionClick, + setIsDialogOpen, } = useRecentTransactionsDialog(); const handleUpdateTransaction = (updatedTransaction: Transaction) => { @@ -42,14 +43,17 @@ const RecentTransactionsSection: React.FC = ({

์ตœ๊ทผ ์ง€์ถœ

- + ๋”๋ณด๊ธฐ
- +
{transactions.length > 0 ? ( - transactions.map(transaction => ( + transactions.map((transaction) => ( = ({ resourceName, className = "h-12 w-12", alt = "์ด๋ฏธ์ง€", - fallback = "ZY" + fallback = "ZY", }) => { const [loading, setLoading] = useState(true); const [error, setError] = useState(false); @@ -27,7 +28,7 @@ const ResourceImage: React.FC = ({ setImageSrc(imgSrc); setLoading(false); } catch (err) { - console.error('์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ:', err); + logger.error("์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ:", err); setError(true); setLoading(false); } diff --git a/src/components/SafeAreaContainer.tsx b/src/components/SafeAreaContainer.tsx index 01a8290..cd3ab7e 100644 --- a/src/components/SafeAreaContainer.tsx +++ b/src/components/SafeAreaContainer.tsx @@ -1,6 +1,5 @@ - -import React from 'react'; -import { cn } from '@/lib/utils'; +import React from "react"; +import { cn } from "@/lib/utils"; interface SafeAreaContainerProps { children: React.ReactNode; @@ -14,15 +13,15 @@ interface SafeAreaContainerProps { */ const SafeAreaContainer: React.FC = ({ children, - className = '', - extraBottomPadding = false + className = "", + extraBottomPadding = false, }) => { return (
diff --git a/src/components/SimpleAvatar.tsx b/src/components/SimpleAvatar.tsx index d86aa91..9c0f50e 100644 --- a/src/components/SimpleAvatar.tsx +++ b/src/components/SimpleAvatar.tsx @@ -1,36 +1,41 @@ - -import React from 'react'; +import React from "react"; interface SimpleAvatarProps { src?: string; name: string; - size?: 'sm' | 'md' | 'lg'; + size?: "sm" | "md" | "lg"; className?: string; } -const SimpleAvatar: React.FC = ({ - src, - name, - size = 'md', - className = '' +const SimpleAvatar: React.FC = ({ + src, + name, + size = "md", + className = "", }) => { const initials = name - .split(' ') - .map(part => part.charAt(0)) - .join('') + .split(" ") + .map((part) => part.charAt(0)) + .join("") .toUpperCase() .substring(0, 2); const sizeClasses = { - sm: 'w-8 h-8 text-xs', - md: 'w-10 h-10 text-sm', - lg: 'w-12 h-12 text-base' + sm: "w-8 h-8 text-xs", + md: "w-10 h-10 text-sm", + lg: "w-12 h-12 text-base", }; return ( -
+
{src ? ( - {name} + {name} ) : ( {initials} )} diff --git a/src/components/SyncSettings.tsx b/src/components/SyncSettings.tsx index 0922eae..421b7ef 100644 --- a/src/components/SyncSettings.tsx +++ b/src/components/SyncSettings.tsx @@ -1,12 +1,12 @@ - -import React, { useEffect } from 'react'; +import React, { useEffect } from "react"; +import { syncLogger } from "@/utils/logger"; import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; import { CloudUpload } from "lucide-react"; -import { useSyncSettings } from '@/hooks/useSyncSettings'; -import SyncStatus from '@/components/sync/SyncStatus'; -import SyncExplanation from '@/components/sync/SyncExplanation'; -import { isSyncEnabled } from '@/utils/sync/syncSettings'; +import { useSyncSettings } from "@/hooks/useSyncSettings"; +import SyncStatus from "@/components/sync/SyncStatus"; +import SyncExplanation from "@/components/sync/SyncExplanation"; +import { isSyncEnabled } from "@/utils/sync/syncSettings"; const SyncSettings = () => { const { @@ -15,30 +15,33 @@ const SyncSettings = () => { user, lastSync, handleSyncToggle, - handleManualSync + handleManualSync, } = useSyncSettings(); // ๋™๊ธฐํ™” ์„ค์ • ๋ณ€๊ฒฝ ๋ชจ๋‹ˆํ„ฐ๋ง useEffect(() => { const checkSyncStatus = () => { const currentStatus = isSyncEnabled(); - console.log('ํ˜„์žฌ ๋™๊ธฐํ™” ์ƒํƒœ:', currentStatus ? 'ํ™œ์„ฑํ™”๋จ' : '๋น„ํ™œ์„ฑํ™”๋จ'); + syncLogger.info( + "ํ˜„์žฌ ๋™๊ธฐํ™” ์ƒํƒœ:", + currentStatus ? "ํ™œ์„ฑํ™”๋จ" : "๋น„ํ™œ์„ฑํ™”๋จ" + ); }; - + // ์ดˆ๊ธฐ ์ƒํƒœ ํ™•์ธ checkSyncStatus(); - + // ์Šคํ† ๋ฆฌ์ง€ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ์—๋„ ๋™๊ธฐํ™” ์ƒํƒœ ํ™•์ธ ์ถ”๊ฐ€ const handleStorageChange = () => { checkSyncStatus(); }; - - window.addEventListener('storage', handleStorageChange); - window.addEventListener('auth-state-changed', handleStorageChange); - + + window.addEventListener("storage", handleStorageChange); + window.addEventListener("auth-state-changed", handleStorageChange); + return () => { - window.removeEventListener('storage', handleStorageChange); - window.removeEventListener('auth-state-changed', handleStorageChange); + window.removeEventListener("storage", handleStorageChange); + window.removeEventListener("auth-state-changed", handleStorageChange); }; }, []); @@ -65,9 +68,9 @@ const SyncSettings = () => { disabled={!user && enabled} // ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์•„์›ƒ ์ƒํƒœ์ด๋ฉด์„œ ๋™๊ธฐํ™”๊ฐ€ ์ผœ์ ธ์žˆ์„ ๋•Œ ๋น„ํ™œ์„ฑํ™” />
- + {/* ๋™๊ธฐํ™” ์ƒํƒœ ๋ฐ ๋™์ž‘ */} - Promise | boolean; // ํƒ€์ž… ๋ณ€๊ฒฝ๋จ: boolean ๋˜๋Š” Promise ๋ฐ˜ํ™˜ } -const TransactionCard: React.FC = ({ +const TransactionCard: React.FC = ({ transaction, - onDelete, + onDelete, }) => { const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); const { title, amount, date, category } = transaction; - + // ์‚ญ์ œ ํ•ธ๋“ค๋Ÿฌ - ์ธ์ž๋กœ ๋ฐ›์€ onDelete๊ฐ€ ์—†๊ฑฐ๋‚˜ ํƒ€์ž…์ด ๋งž์ง€ ์•Š์„ ๋•Œ ๊ธฐ๋ณธ ํ•จ์ˆ˜ ์ œ๊ณต const handleDelete = async (id: string): Promise => { try { if (onDelete) { return await onDelete(id); } - console.log('์‚ญ์ œ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค'); + logger.info("์‚ญ์ œ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค"); return false; } catch (error) { - console.error('ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜:", error); return false; } }; - + return ( <> -
setIsEditDialogOpen(true)} > @@ -45,12 +45,12 @@ const TransactionCard: React.FC = ({
- +
- = ({ export default TransactionCard; // Transaction ํƒ€์ž…์„ context์—์„œ ์ง์ ‘ ๋‹ค์‹œ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค -export type { Transaction } from '@/contexts/budget/types'; +export type { Transaction } from "@/contexts/budget/types"; diff --git a/src/components/TransactionEditDialog.tsx b/src/components/TransactionEditDialog.tsx index e77039f..2afe5b9 100644 --- a/src/components/TransactionEditDialog.tsx +++ b/src/components/TransactionEditDialog.tsx @@ -1,16 +1,15 @@ - -import React from 'react'; -import { Transaction } from '@/components/TransactionCard'; -import { - Dialog, - DialogContent, - DialogHeader, +import React from "react"; +import { Transaction } from "@/contexts/budget/types"; +import { + Dialog, + DialogContent, + DialogHeader, DialogTitle, - DialogDescription -} from '@/components/ui/dialog'; -import { useIsMobile } from '@/hooks/use-mobile'; -import { useTransactionEdit } from './transaction/useTransactionEdit'; -import TransactionEditForm from './transaction/TransactionEditForm'; + DialogDescription, +} from "@/components/ui/dialog"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { useTransactionEdit } from "./transaction/useTransactionEdit"; +import TransactionEditForm from "./transaction/TransactionEditForm"; interface TransactionEditDialogProps { transaction: Transaction; @@ -27,31 +26,38 @@ const TransactionEditDialog: React.FC = ({ transaction, open, onOpenChange, - onDelete + onDelete, }) => { const isMobile = useIsMobile(); const closeDialog = () => onOpenChange(false); - + // useTransactionEdit ํ›… ์‚ฌ์šฉ - ์ธ์ž๋ฅผ 2๊ฐœ๋งŒ ์ „๋‹ฌ const { form, isSubmitting, handleSubmit, handleDelete } = useTransactionEdit( transaction, closeDialog ); - + return ( - { - // ์ œ์ถœ ์ค‘์ด๋ฉด ๋‹ซ๊ธฐ ๋ฐฉ์ง€ - if (isSubmitting && !newOpen) return; - onOpenChange(newOpen); - }}> - + { + // ์ œ์ถœ ์ค‘์ด๋ฉด ๋‹ซ๊ธฐ ๋ฐฉ์ง€ + if (isSubmitting && !newOpen) { + return; + } + onOpenChange(newOpen); + }} + > + ์ง€์ถœ ์ˆ˜์ • ์ง€์ถœ ๋‚ด์—ญ์„ ์ˆ˜์ •ํ•˜๊ฑฐ๋‚˜ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. - + = ({ className = "h-12 w-12 mr-3" }) => { +const ZellyAvatar: React.FC<{ className?: string }> = ({ + className = "h-12 w-12 mr-3", +}) => { const [imageLoaded, setImageLoaded] = useState(false); const [imageError, setImageError] = useState(false); - const [imageSrc, setImageSrc] = useState('/zellyy.png'); + const [imageSrc, setImageSrc] = useState("/zellyy.png"); useEffect(() => { // ์•ฑ ํ™˜๊ฒฝ์—์„œ๋Š” Base64 ์ธ์ฝ”๋”ฉ๋œ ์ด๋ฏธ์ง€๋ฅผ ์‚ฌ์šฉ if (Capacitor.isNativePlatform()) { - setImageSrc('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFEmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjMtMDYtMzBUMjI6MDM6NTIrMDk6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIzLTA2LTMwVDIzOjIxOjI1KzA5OjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIzLTA2LTMwVDIzOjIxOjI1KzA5OjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjE0ZDg5ZjI0LTRmZjctNDUwOS1hNDA3LWM4ODZhM2Q4MDk0YSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxNGQ4OWYyNC00ZmY3LTQ1MDktYTQwNy1jODg2YTNkODA5NGEiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoxNGQ4OWYyNC00ZmY3LTQ1MDktYTQwNy1jODg2YTNkODA5NGEiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjE0ZDg5ZjI0LTRmZjctNDUwOS1hNDA3LWM4ODZhM2Q4MDk0YSIgc3RFdnQ6d2hlbj0iMjAyMy0wNi0zMFQyMjowMzo1MiswOTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKE1hY2ludG9zaCkiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+GWKCCQAAATNJREFUWIXtlTFuwzAQRV+QTgk8ZMjWA+QAOViGLJmzdSsydS1kypojZAqQJbNvYHgplCKRQrUoqUiGwrcbLXAffFCH3O3ESSEJEzHZJEwhDMMQQtg/t1OGkJnZ4fNzDODtYU4MYRwiLb5eX8YAHp+WiSH8Cl+DvQPTXZwYAuBPa3dlQ8i5YEgOYJe1KxvCZ3HLqk1MWULmG+y5gx9gvfTYkDyJKaBDDZzE5BqEVT1fzNkP5Fy3tUXECF9X07dtvb/+TJbLFIYXzX9aNwPxUMkBOm34q2TNI8IWEQ4R4RARDhHhEBHO5pzV/gprM7aWFQtExBcRDhHhEBEOEeEQEU5Ljtb5+D+13lx3nP7lWpedwuA7fNP5OYhrNrXc4xVlGHPyODEEwGY1/AFIBfclfLkBYAAAAABJRU5ErkJggg=='); + setImageSrc( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFEmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjMtMDYtMzBUMjI6MDM6NTIrMDk6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIzLTA2LTMwVDIzOjIxOjI1KzA5OjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIzLTA2LTMwVDIzOjIxOjI1KzA5OjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjE0ZDg5ZjI0LTRmZjctNDUwOS1hNDA3LWM4ODZhM2Q4MDk0YSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxNGQ4OWYyNC00ZmY3LTQ1MDktYTQwNy1jODg2YTNkODA5NGEiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoxNGQ4OWYyNC00ZmY3LTQ1MDktYTQwNy1jODg2YTNkODA5NGEiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjE0ZDg5ZjI0LTRmZjctNDUwOS1hNDA3LWM4ODZhM2Q4MDk0YSIgc3RFdnQ6d2hlbj0iMjAyMy0wNi0zMFQyMjowMzo1MiswOTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKE1hY2ludG9zaCkiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+GWKCCQAAATNJREFUWIXtlTFuwzAQRV+QTgk8ZMjWA+QAOViGLJmzdSsydS1kypojZAqQJbNvYHgplCKRQrUoqUiGwrcbLXAffFCH3O3ESSEJEzHZJEwhDMMQQtg/t1OGkJnZ4fNzDODtYU4MYRwiLb5eX8YAHp+WiSH8Cl+DvQPTXZwYAuBPa3dlQ8i5YEgOYJe1KxvCZ3HLqk1MWULmG+y5gx9gvfTYkDyJKaBDDZzE5BqEVT1fzNkP5Fy3tUXECF9X07dtvb/+TJbLFIYXzX9aNwPxUMkBOm34q2TNI8IWEQ4R4RARDhHhEBHO5pzV/gprM7aWFQtExBcRDhHhEBEOEeEQEU5Ljtb5+D+13lx3nP7lWpedwuA7fNP5OYhrNrXc4xVlGHPyODEEwGY1/AFIBfclfLkBYAAAAABJRU5ErkJggg==" + ); } else { // ์›น ํ™˜๊ฒฝ์—์„œ๋Š” ์ผ๋ฐ˜ ๊ฒฝ๋กœ ์‚ฌ์šฉ - setImageSrc('/zellyy.png'); + setImageSrc("/zellyy.png"); } setImageLoaded(true); }, []); @@ -31,10 +35,10 @@ const ZellyAvatar: React.FC<{ className?: string }> = ({ className = "h-12 w-12
) : ( <> - setImageLoaded(true)} onError={() => setImageError(true)} /> diff --git a/src/components/analytics/CategorySpendingList.tsx b/src/components/analytics/CategorySpendingList.tsx index 2cdc3cf..99c7067 100644 --- a/src/components/analytics/CategorySpendingList.tsx +++ b/src/components/analytics/CategorySpendingList.tsx @@ -1,9 +1,8 @@ - -import React from 'react'; -import { formatCurrency } from '@/utils/formatters'; -import { useIsMobile } from '@/hooks/use-mobile'; -import { CATEGORY_DESCRIPTIONS } from '@/constants/categoryIcons'; -import { getCategoryColor } from '@/utils/categoryColorUtils'; +import React from "react"; +import { formatCurrency } from "@/utils/formatters"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { CATEGORY_DESCRIPTIONS } from "@/constants/categoryIcons"; +import { getCategoryColor } from "@/utils/categoryColorUtils"; interface CategorySpending { title: string; @@ -22,27 +21,33 @@ const CategorySpendingList: React.FC = ({ categories, totalExpense, className = "", - showCard = true // ๊ธฐ๋ณธ๊ฐ’์€ true๋กœ ์„ค์ • + showCard = true, // ๊ธฐ๋ณธ๊ฐ’์€ true๋กœ ์„ค์ • }) => { const isMobile = useIsMobile(); - + // ์นดํ…Œ๊ณ ๋ฆฌ ๋ชฉ๋ก์„ ๋ Œ๋”๋งํ•˜๋Š” ํ•จ์ˆ˜ const renderCategoryList = () => { - if (categories.some(cat => cat.current > 0)) { + if (categories.some((cat) => cat.current > 0)) { return (
{categories.map((category) => { // ์นดํ…Œ๊ณ ๋ฆฌ ์ด๋ฆ„์„ ์ง์ ‘ ํ‘œ์‹œ const categoryName = category.title; // ์นดํ…Œ๊ณ ๋ฆฌ ์„ค๋ช… ์ฐพ๊ธฐ - const description = CATEGORY_DESCRIPTIONS[categoryName] || ''; - + const description = CATEGORY_DESCRIPTIONS[categoryName] || ""; + return ( -
+
-
+
{categoryName} {description && ( @@ -79,7 +84,7 @@ const CategorySpendingList: React.FC = ({
); } - + // ์นด๋“œ ์—†์ด ๋ชฉ๋ก๋งŒ ๋ฐ˜ํ™˜ return renderCategoryList(); }; diff --git a/src/components/analytics/MonthlyComparisonChart.tsx b/src/components/analytics/MonthlyComparisonChart.tsx index d868c50..77a3dfb 100644 --- a/src/components/analytics/MonthlyComparisonChart.tsx +++ b/src/components/analytics/MonthlyComparisonChart.tsx @@ -1,18 +1,24 @@ -import React from 'react'; -import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, Cell } from 'recharts'; -import { formatCurrency } from '@/utils/formatters'; -interface MonthlyData { - name: string; - budget: number; - expense: number; -} +import React from "react"; +import { logger } from "@/utils/logger"; +import { + BarChart, + Bar, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, + Legend, + Cell, +} from "recharts"; +import { formatCurrency } from "@/utils/formatters"; +import { MonthlyData } from "@/types"; interface MonthlyComparisonChartProps { monthlyData: MonthlyData[]; isEmpty?: boolean; } const MonthlyComparisonChart: React.FC = ({ monthlyData, - isEmpty = false + isEmpty = false, }) => { // Format for Y-axis (K format) const formatYAxisTick = (value: number) => { @@ -21,31 +27,42 @@ const MonthlyComparisonChart: React.FC = ({ // Format for tooltip (original currency format) const formatTooltip = (value: number | string) => { - if (typeof value === 'number') { + if (typeof value === "number") { return formatCurrency(value); } return value; }; // ๋ฐ์ดํ„ฐ ํ™•์ธ ๋กœ๊น… - console.log('MonthlyComparisonChart ๋ฐ์ดํ„ฐ:', monthlyData); + logger.info("MonthlyComparisonChart ๋ฐ์ดํ„ฐ:", monthlyData); // EmptyGraphState ์ปดํฌ๋„ŒํŠธ: ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์„ ๋•Œ ํ‘œ์‹œ - const EmptyGraphState = () =>
+ const EmptyGraphState = () => ( +

๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค

์ง€์ถœ ๋‚ด์—ญ์„ ์ถ”๊ฐ€ํ•˜๋ฉด ๊ทธ๋ž˜ํ”„๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค

-
; +
+ ); // ๋ฐ์ดํ„ฐ ์—ฌ๋ถ€ ํ™•์ธ ๋กœ์ง ๊ฐœ์„  - ๋ฐ์ดํ„ฐ๊ฐ€ ๋น„์–ด์žˆ๊ฑฐ๋‚˜ ๋ชจ๋“  ๊ฐ’์ด 0์ธ ๊ฒฝ์šฐ๋„ ๊ณ ๋ ค - const hasValidData = monthlyData && monthlyData.length > 0 && monthlyData.some(item => item.budget > 0 || item.expense > 0); + const hasValidData = + monthlyData && + monthlyData.length > 0 && + monthlyData.some((item) => item.budget > 0 || item.expense > 0); // ์ง€์ถœ ์ƒ‰์ƒ ๊ฒฐ์ • ํ•จ์ˆ˜ ์ถ”๊ฐ€ const getExpenseColor = (budget: number, expense: number) => { - if (budget === 0) return "#81c784"; // ์˜ˆ์‚ฐ์ด 0์ด๋ฉด ๊ธฐ๋ณธ ์ƒ‰์ƒ + if (budget === 0) { + return "#81c784"; + } // ์˜ˆ์‚ฐ์ด 0์ด๋ฉด ๊ธฐ๋ณธ ์ƒ‰์ƒ const ratio = expense / budget; - if (ratio > 1) return "#f44336"; // ๋นจ๊ฐ„์ƒ‰ (์˜ˆ์‚ฐ ์ดˆ๊ณผ) - if (ratio >= 0.9) return "#ffeb3b"; // ๋…ธ๋ž€์ƒ‰ (์˜ˆ์‚ฐ์˜ 90% ์ด์ƒ) + if (ratio > 1) { + return "#f44336"; + } // ๋นจ๊ฐ„์ƒ‰ (์˜ˆ์‚ฐ ์ดˆ๊ณผ) + if (ratio >= 0.9) { + return "#ffeb3b"; + } // ๋…ธ๋ž€์ƒ‰ (์˜ˆ์‚ฐ์˜ 90% ์ด์ƒ) return "#81c784"; // ๊ธฐ๋ณธ ์ดˆ๋ก์ƒ‰ }; @@ -55,36 +72,69 @@ const MonthlyComparisonChart: React.FC = ({ // ์˜ˆ์‚ฐ ์ƒ‰์ƒ์„ ์ข€ ๋” ์ง™์€ ํšŒ์ƒ‰์œผ๋กœ ๋ณ€๊ฒฝ const darkGrayColor = "#9F9EA1"; // ์ด์ „ ์ƒ‰์ƒ #C8C8C9์—์„œ ๋” ์ง™์€ ํšŒ์ƒ‰์œผ๋กœ ๋ณ€๊ฒฝ - return
- {hasValidData ? - + return ( +
+ {hasValidData ? ( + + - - { - // ๋ฒ”๋ก€ ํ…์ŠคํŠธ ์ƒ‰์ƒ ์„ค์ • - return {value}; - }} /> - - + + { + // ๋ฒ”๋ก€ ํ…์ŠคํŠธ ์ƒ‰์ƒ ์„ค์ • + return ( + + {value} + + ); + }} + /> + + {/* ๊ฐœ๋ณ„ ์…€ ์ƒ‰์ƒ ์„ค์ •์€ ์ œ๊ฑฐํ•˜๊ณ  ํ†ต์ผ๋œ ๋ฉ”์ธ ๊ทธ๋ฆฐ ์ƒ‰์ƒ ์‚ฌ์šฉ */} - : } -
; +
+ ) : ( + + )} +
+ ); }; -export default MonthlyComparisonChart; \ No newline at end of file +export default MonthlyComparisonChart; diff --git a/src/components/analytics/PaymentMethodChart.tsx b/src/components/analytics/PaymentMethodChart.tsx index f2ccdad..9433ab1 100644 --- a/src/components/analytics/PaymentMethodChart.tsx +++ b/src/components/analytics/PaymentMethodChart.tsx @@ -1,21 +1,18 @@ - -import React from 'react'; -import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts'; - -interface PaymentMethodData { - method: string; - amount: number; - percentage: number; -} +import React from "react"; +import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts"; +import { PaymentMethodStats } from "@/contexts/budget/types"; interface PaymentMethodChartProps { - data: PaymentMethodData[]; + data: PaymentMethodStats[]; isEmpty: boolean; } -const COLORS = ['#9b87f5', '#6E59A5']; // ์‹ ์šฉ์นด๋“œ, ํ˜„๊ธˆ ์ƒ‰์ƒ +const COLORS = ["#9b87f5", "#6E59A5"]; // ์‹ ์šฉ์นด๋“œ, ํ˜„๊ธˆ ์ƒ‰์ƒ -const PaymentMethodChart: React.FC = ({ data, isEmpty }) => { +const PaymentMethodChart: React.FC = ({ + data, + isEmpty, +}) => { if (isEmpty) { return (
@@ -24,9 +21,9 @@ const PaymentMethodChart: React.FC = ({ data, isEmpty } ); } - const chartData = data.map(item => ({ + const chartData = data.map((item) => ({ name: item.method, - value: item.amount + value: item.amount, })); return ( @@ -42,11 +39,16 @@ const PaymentMethodChart: React.FC = ({ data, isEmpty } paddingAngle={5} dataKey="value" labelLine={false} - label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`} + label={({ name, percent }) => + `${name} ${(percent * 100).toFixed(0)}%` + } fontSize={12} > {chartData.map((entry, index) => ( - + ))} {/* Legend ์ œ๊ฑฐ */} diff --git a/src/components/analytics/PeriodSelector.tsx b/src/components/analytics/PeriodSelector.tsx index 56f9778..5d190ea 100644 --- a/src/components/analytics/PeriodSelector.tsx +++ b/src/components/analytics/PeriodSelector.tsx @@ -1,7 +1,6 @@ - -import React from 'react'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; -import { useIsMobile } from '@/hooks/use-mobile'; +import React from "react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { useIsMobile } from "@/hooks/use-mobile"; interface PeriodSelectorProps { selectedPeriod: string; @@ -12,27 +11,21 @@ interface PeriodSelectorProps { const PeriodSelector: React.FC = ({ selectedPeriod, onPrevPeriod, - onNextPeriod + onNextPeriod, }) => { const isMobile = useIsMobile(); return (
- - +
{selectedPeriod}
- -
diff --git a/src/components/analytics/SummaryCards.tsx b/src/components/analytics/SummaryCards.tsx index a88fa92..347d14c 100644 --- a/src/components/analytics/SummaryCards.tsx +++ b/src/components/analytics/SummaryCards.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import { Wallet, CreditCard, Coins } from 'lucide-react'; -import { formatCurrency } from '@/utils/formatters'; -import { useIsMobile } from '@/hooks/use-mobile'; +import React from "react"; +import { Wallet, CreditCard, Coins } from "lucide-react"; +import { formatCurrency } from "@/utils/formatters"; +import { useIsMobile } from "@/hooks/use-mobile"; interface SummaryCardsProps { totalBudget: number; totalExpense: number; @@ -10,18 +10,21 @@ interface SummaryCardsProps { const SummaryCards: React.FC = ({ totalBudget, totalExpense, - savingsPercentage + savingsPercentage, }) => { const isMobile = useIsMobile(); // ๋‚จ์€ ์˜ˆ์‚ฐ ๊ณ„์‚ฐ const remainingBudget = totalBudget - totalExpense; const isOverBudget = remainingBudget < 0; - return
+ return ( +
- {isMobile ? - // ๋ชจ๋ฐ”์ผ ๋ ˆ์ด์•„์›ƒ (1์ค„: ์•„์ด์ฝ˜, ์ œ๋ชฉ, ๊ธˆ์•ก ๊ฐ€๋กœ๋ฐฐ์น˜) -
+ {isMobile ? ( + // ๋ชจ๋ฐ”์ผ ๋ ˆ์ด์•„์›ƒ (1์ค„: ์•„์ด์ฝ˜, ์ œ๋ชฉ, ๊ธˆ์•ก ๊ฐ€๋กœ๋ฐฐ์น˜) +

์˜ˆ์‚ฐ

@@ -29,9 +32,10 @@ const SummaryCards: React.FC = ({

{formatCurrency(totalBudget)}

-
: - // ๋ฐ์Šคํฌํƒ‘ ๋ ˆ์ด์•„์›ƒ (2์ค„: ์•„์ด์ฝ˜๊ณผ ์ œ๋ชฉ์ด ์ฒซ์งธ ์ค„, ๊ธˆ์•ก์ด ๋‘˜์งธ ์ค„) - <> +
+ ) : ( + // ๋ฐ์Šคํฌํƒ‘ ๋ ˆ์ด์•„์›ƒ (2์ค„: ์•„์ด์ฝ˜๊ณผ ์ œ๋ชฉ์ด ์ฒซ์งธ ์ค„, ๊ธˆ์•ก์ด ๋‘˜์งธ ์ค„) + <>

์˜ˆ์‚ฐ

@@ -39,12 +43,13 @@ const SummaryCards: React.FC = ({

{formatCurrency(totalBudget)}

- } + + )}
- {isMobile ? - // ๋ชจ๋ฐ”์ผ ๋ ˆ์ด์•„์›ƒ (1์ค„: ์•„์ด์ฝ˜, ์ œ๋ชฉ, ๊ธˆ์•ก ๊ฐ€๋กœ๋ฐฐ์น˜) -
+ {isMobile ? ( + // ๋ชจ๋ฐ”์ผ ๋ ˆ์ด์•„์›ƒ (1์ค„: ์•„์ด์ฝ˜, ์ œ๋ชฉ, ๊ธˆ์•ก ๊ฐ€๋กœ๋ฐฐ์น˜) +

์ง€์ถœ

@@ -52,9 +57,10 @@ const SummaryCards: React.FC = ({

{formatCurrency(totalExpense)}

-
: - // ๋ฐ์Šคํฌํƒ‘ ๋ ˆ์ด์•„์›ƒ (2์ค„: ์•„์ด์ฝ˜๊ณผ ์ œ๋ชฉ์ด ์ฒซ์งธ ์ค„, ๊ธˆ์•ก์ด ๋‘˜์งธ ์ค„) - <> +
+ ) : ( + // ๋ฐ์Šคํฌํƒ‘ ๋ ˆ์ด์•„์›ƒ (2์ค„: ์•„์ด์ฝ˜๊ณผ ์ œ๋ชฉ์ด ์ฒซ์งธ ์ค„, ๊ธˆ์•ก์ด ๋‘˜์งธ ์ค„) + <>

์ง€์ถœ

@@ -62,35 +68,47 @@ const SummaryCards: React.FC = ({

{formatCurrency(totalExpense)}

- } + + )}
- {isMobile ? - // ๋ชจ๋ฐ”์ผ ๋ ˆ์ด์•„์›ƒ (1์ค„: ์•„์ด์ฝ˜, ์ œ๋ชฉ, ๊ธˆ์•ก ๊ฐ€๋กœ๋ฐฐ์น˜) -
+ {isMobile ? ( + // ๋ชจ๋ฐ”์ผ ๋ ˆ์ด์•„์›ƒ (1์ค„: ์•„์ด์ฝ˜, ์ œ๋ชฉ, ๊ธˆ์•ก ๊ฐ€๋กœ๋ฐฐ์น˜) +

์ž”์•ก

- {isOverBudget ?

+ {isOverBudget ? ( +

์ดˆ๊ณผ์•ก: {formatCurrency(Math.abs(remainingBudget))} -

:

+

+ ) : ( +

{formatCurrency(remainingBudget)} -

} -
: - // ๋ฐ์Šคํฌํƒ‘ ๋ ˆ์ด์•„์›ƒ (2์ค„: ์•„์ด์ฝ˜๊ณผ ์ œ๋ชฉ์ด ์ฒซ์งธ ์ค„, ๊ธˆ์•ก์ด ๋‘˜์งธ ์ค„) - <> +

+ )} +
+ ) : ( + // ๋ฐ์Šคํฌํƒ‘ ๋ ˆ์ด์•„์›ƒ (2์ค„: ์•„์ด์ฝ˜๊ณผ ์ œ๋ชฉ์ด ์ฒซ์งธ ์ค„, ๊ธˆ์•ก์ด ๋‘˜์งธ ์ค„) + <>

์ž”์•ก

- {isOverBudget ?

+ {isOverBudget ? ( +

์ดˆ๊ณผ์•ก: {formatCurrency(Math.abs(remainingBudget))} -

:

+

+ ) : ( +

{formatCurrency(remainingBudget)} -

} - } +

+ )} + + )}
-
; +
+ ); }; -export default SummaryCards; \ No newline at end of file +export default SummaryCards; diff --git a/src/components/auth/AppwriteConnectionStatus.tsx b/src/components/auth/AppwriteConnectionStatus.tsx index e230e2a..d2dff6d 100644 --- a/src/components/auth/AppwriteConnectionStatus.tsx +++ b/src/components/auth/AppwriteConnectionStatus.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { CheckCircle, XCircle } from 'lucide-react'; +import React from "react"; +import { CheckCircle, XCircle } from "lucide-react"; interface AppwriteConnectionStatusProps { testResults: { @@ -9,11 +9,17 @@ interface AppwriteConnectionStatusProps { } | null; } -const AppwriteConnectionStatus = ({ testResults }: AppwriteConnectionStatusProps) => { - if (!testResults) return null; +const AppwriteConnectionStatus = ({ + testResults, +}: AppwriteConnectionStatusProps) => { + if (!testResults) { + return null; + } return ( -
+
{testResults.connected ? ( @@ -21,16 +27,20 @@ const AppwriteConnectionStatus = ({ testResults }: AppwriteConnectionStatusProps )}
-

- {testResults.connected ? '์—ฐ๊ฒฐ๋จ' : '์—ฐ๊ฒฐ ์‹คํŒจ'} +

+ {testResults.connected ? "์—ฐ๊ฒฐ๋จ" : "์—ฐ๊ฒฐ ์‹คํŒจ"}

-

+

{testResults.message}

{testResults.details && ( -

- {testResults.details} -

+

{testResults.details}

)}
diff --git a/src/components/auth/AppwriteConnectionTest.tsx b/src/components/auth/AppwriteConnectionTest.tsx index 83dd0b9..cb42c0b 100644 --- a/src/components/auth/AppwriteConnectionTest.tsx +++ b/src/components/auth/AppwriteConnectionTest.tsx @@ -1,9 +1,14 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { Button } from '@/components/ui/button'; -import { Loader2 } from 'lucide-react'; -import AppwriteConnectionStatus from './AppwriteConnectionStatus'; -import { client, account, isValidAppwriteConfig, getAppwriteEndpoint } from '@/lib/appwrite'; -import { setupAppwriteDatabase } from '@/lib/appwrite/setup'; +import React, { useState, useEffect, useCallback } from "react"; +import { Button } from "@/components/ui/button"; +import { Loader2 } from "lucide-react"; +import AppwriteConnectionStatus from "./AppwriteConnectionStatus"; +import { + client, + account, + isValidAppwriteConfig, + getAppwriteEndpoint, +} from "@/lib/appwrite"; +import { setupAppwriteDatabase } from "@/lib/appwrite/setup"; /** * Appwrite ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ @@ -16,119 +21,120 @@ const AppwriteConnectionTest = () => { message: string; details?: string; } | null>(null); - + // ๋กœ๋”ฉ ์ƒํƒœ const [loading, setLoading] = useState(false); - + // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • ์ƒํƒœ const [dbSetupDone, setDbSetupDone] = useState(false); - + // ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ ํ•จ์ˆ˜ const testConnection = useCallback(async () => { setLoading(true); setTestResults(null); - + try { // ์„ค์ • ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ if (!isValidAppwriteConfig()) { setTestResults({ connected: false, - message: 'Appwrite ์„ค์ •์ด ์™„๋ฃŒ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.', - details: 'ํ™˜๊ฒฝ ๋ณ€์ˆ˜ VITE_APPWRITE_ENDPOINT ๋ฐ VITE_APPWRITE_PROJECT_ID๋ฅผ ํ™•์ธํ•˜์„ธ์š”.' + message: "Appwrite ์„ค์ •์ด ์™„๋ฃŒ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.", + details: + "ํ™˜๊ฒฝ ๋ณ€์ˆ˜ VITE_APPWRITE_ENDPOINT ๋ฐ VITE_APPWRITE_PROJECT_ID๋ฅผ ํ™•์ธํ•˜์„ธ์š”.", }); return; } - + // ์„œ๋ฒ„ ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ try { await account.get(); - + setTestResults({ connected: true, - message: 'Appwrite ์„œ๋ฒ„์— ์„ฑ๊ณต์ ์œผ๋กœ ์—ฐ๊ฒฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.', - details: `์„œ๋ฒ„: ${getAppwriteEndpoint()}` + message: "Appwrite ์„œ๋ฒ„์— ์„ฑ๊ณต์ ์œผ๋กœ ์—ฐ๊ฒฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + details: `์„œ๋ฒ„: ${getAppwriteEndpoint()}`, }); } catch (error: any) { // ์ธ์ฆ ์˜ค๋ฅ˜๋Š” ์—ฐ๊ฒฐ ์„ฑ๊ณต์œผ๋กœ ๊ฐ„์ฃผ (๋กœ๊ทธ์ธ ํ•„์š”) if (error.code === 401) { setTestResults({ connected: true, - message: 'Appwrite ์„œ๋ฒ„์— ์—ฐ๊ฒฐ๋˜์—ˆ์ง€๋งŒ ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.', - details: `์„œ๋ฒ„: ${getAppwriteEndpoint()}` + message: "Appwrite ์„œ๋ฒ„์— ์—ฐ๊ฒฐ๋˜์—ˆ์ง€๋งŒ ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + details: `์„œ๋ฒ„: ${getAppwriteEndpoint()}`, }); } else { setTestResults({ connected: false, - message: '์„œ๋ฒ„ ์—ฐ๊ฒฐ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.', - details: error.message + message: "์„œ๋ฒ„ ์—ฐ๊ฒฐ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + details: error.message, }); } } } catch (error: any) { setTestResults({ connected: false, - message: '์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.', - details: error.message + message: "์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + details: error.message, }); } finally { setLoading(false); } }, []); - + // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • ํ•จ์ˆ˜ const setupDatabase = useCallback(async () => { setLoading(true); - + try { const success = await setupAppwriteDatabase(); - + if (success) { setDbSetupDone(true); setTestResults({ connected: true, - message: '๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.', - details: 'ํŠธ๋žœ์žญ์…˜ ์ปฌ๋ ‰์…˜์ด ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.' + message: "๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + details: "ํŠธ๋žœ์žญ์…˜ ์ปฌ๋ ‰์…˜์ด ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", }); } else { setTestResults({ connected: false, - message: '๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.', - details: '๋กœ๊ทธ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.' + message: "๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + details: "๋กœ๊ทธ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.", }); } } catch (error: any) { setTestResults({ connected: false, - message: '๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.', - details: error.message + message: "๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + details: error.message, }); } finally { setLoading(false); } }, []); - + // ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ ์ž๋™ ํ…Œ์ŠคํŠธ useEffect(() => { testConnection(); }, [testConnection]); - + return (
- - + {testResults?.connected && !dbSetupDone && ( - )}
- +
); diff --git a/src/components/auth/EmailConfirmation.tsx b/src/components/auth/EmailConfirmation.tsx index a929c86..0893db2 100644 --- a/src/components/auth/EmailConfirmation.tsx +++ b/src/components/auth/EmailConfirmation.tsx @@ -1,5 +1,5 @@ - import React, { useState } from "react"; +import { authLogger } from "@/utils/logger"; import { useNavigate } from "react-router-dom"; import { Mail, InfoIcon, RefreshCw } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -11,20 +11,26 @@ interface EmailConfirmationProps { onResendEmail?: () => Promise; } -const EmailConfirmation: React.FC = ({ email, onBackToForm, onResendEmail }) => { +const EmailConfirmation: React.FC = ({ + email, + onBackToForm, + onResendEmail, +}) => { const navigate = useNavigate(); const [isResending, setIsResending] = useState(false); // ์ด๋ฉ”์ผ ์žฌ์ „์†ก ํ•ธ๋“ค๋Ÿฌ const handleResendEmail = async () => { - if (!onResendEmail) return; - + if (!onResendEmail) { + return; + } + setIsResending(true); try { await onResendEmail(); // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€๋Š” onResendEmail ๋‚ด๋ถ€์—์„œ ํ‘œ์‹œ } catch (error) { - console.error('์ด๋ฉ”์ผ ์žฌ์ „์†ก ์˜ค๋ฅ˜:', error); + authLogger.error("์ด๋ฉ”์ผ ์žฌ์ „์†ก ์˜ค๋ฅ˜:", error); } finally { setIsResending(false); } @@ -39,12 +45,15 @@ const EmailConfirmation: React.FC = ({ email, onBackToFo {email}๋กœ ์ธ์ฆ ๋งํฌ๊ฐ€ ํฌํ•จ๋œ ์ด๋ฉ”์ผ์„ ๋ณด๋ƒˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฉ”์ผ์„ ํ™•์ธํ•˜๊ณ  ๋งํฌ๋ฅผ ํด๋ฆญํ•˜์—ฌ ๊ณ„์ • ๋“ฑ๋ก์„ ์™„๋ฃŒํ•ด์ฃผ์„ธ์š”.

- + - ์ธ์ฆ ์ด๋ฉ”์ผ์ด ๋ณด์ด์ง€ ์•Š๋‚˜์š”? + + ์ธ์ฆ ์ด๋ฉ”์ผ์ด ๋ณด์ด์ง€ ์•Š๋‚˜์š”? + - ์ŠคํŒธ ํด๋”๋ฅผ ํ™•์ธํ•ด๋ณด์„ธ์š”. ๋˜ํ•œ ์ด๋ฉ”์ผ ์„œ๋น„์Šค์— ๋”ฐ๋ผ ๋„์ฐฉํ•˜๋Š”๋ฐ ๋ช‡ ๋ถ„์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + ์ŠคํŒธ ํด๋”๋ฅผ ํ™•์ธํ•ด๋ณด์„ธ์š”. ๋˜ํ•œ ์ด๋ฉ”์ผ ์„œ๋น„์Šค์— ๋”ฐ๋ผ ๋„์ฐฉํ•˜๋Š”๋ฐ ๋ช‡ + ๋ถ„์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. {onResendEmail && (
์•„์ง๋„ ๋ฐ›์ง€ ๋ชปํ–ˆ๋‹ค๋ฉด ์•„๋ž˜ '์ธ์ฆ ๋ฉ”์ผ ์žฌ์ „์†ก' ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์„ธ์š”. @@ -52,12 +61,12 @@ const EmailConfirmation: React.FC = ({ email, onBackToFo )} - +
{onResendEmail && ( - )} - - -
diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx index 9dfdb0a..5a2778f 100644 --- a/src/components/auth/LoginForm.tsx +++ b/src/components/auth/LoginForm.tsx @@ -3,7 +3,15 @@ import { Link } from "react-router-dom"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; -import { ArrowRight, Mail, KeyRound, Eye, EyeOff, AlertTriangle, Loader2 } from "lucide-react"; +import { + ArrowRight, + Mail, + KeyRound, + Eye, + EyeOff, + AlertTriangle, + Loader2, +} from "lucide-react"; interface LoginFormProps { email: string; setEmail: (email: string) => void; @@ -26,59 +34,120 @@ const LoginForm: React.FC = ({ isLoading, isSettingUpTables = false, loginError, - handleLogin + handleLogin, }) => { // CORS ๋˜๋Š” JSON ๊ด€๋ จ ์˜ค๋ฅ˜์ธ์ง€ ํ™•์ธ - const isCorsOrJsonError = loginError && (loginError.includes('JSON') || loginError.includes('CORS') || loginError.includes('ํ”„๋ก์‹œ') || loginError.includes('์„œ๋ฒ„ ์‘๋‹ต') || loginError.includes('๋„คํŠธ์›Œํฌ') || loginError.includes('404') || loginError.includes('Not Found')); - return
+ const isCorsOrJsonError = + loginError && + (loginError.includes("JSON") || + loginError.includes("CORS") || + loginError.includes("ํ”„๋ก์‹œ") || + loginError.includes("์„œ๋ฒ„ ์‘๋‹ต") || + loginError.includes("๋„คํŠธ์›Œํฌ") || + loginError.includes("404") || + loginError.includes("Not Found")); + return ( +
- +
- setEmail(e.target.value)} className="pl-10 neuro-pressed" /> + setEmail(e.target.value)} + className="pl-10 neuro-pressed" + />
- +
- +
- setPassword(e.target.value)} className="pl-10 neuro-pressed" /> -
- - {loginError &&
+ + {loginError && ( +

{loginError}

- - {isCorsOrJsonError &&
    -
  • ์„ค์ • ํŽ˜์ด์ง€์—์„œ ๋‹ค๋ฅธ CORS ํ”„๋ก์‹œ ์œ ํ˜•์„ ์‹œ๋„ํ•ด ๋ณด์„ธ์š”.
  • -
  • HTTPS URL์„ ์‚ฌ์šฉํ•˜๋Š” Supabase ์ธ์Šคํ„ด์Šค๋กœ ๋ณ€๊ฒฝํ•ด ๋ณด์„ธ์š”.
  • + + {isCorsOrJsonError && ( +
      +
    • + ์„ค์ • ํŽ˜์ด์ง€์—์„œ ๋‹ค๋ฅธ CORS ํ”„๋ก์‹œ ์œ ํ˜•์„ ์‹œ๋„ํ•ด ๋ณด์„ธ์š”. +
    • +
    • + HTTPS URL์„ ์‚ฌ์šฉํ•˜๋Š” Supabase ์ธ์Šคํ„ด์Šค๋กœ ๋ณ€๊ฒฝํ•ด ๋ณด์„ธ์š”. +
    • ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.
    • -
    } +
+ )}
-
} - +
+ )} +
- + ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์žŠ์œผ์…จ๋‚˜์š”?
- -
-
; +
+ ); }; -export default LoginForm; \ No newline at end of file +export default LoginForm; diff --git a/src/components/auth/LoginLink.tsx b/src/components/auth/LoginLink.tsx index dea67aa..e4fd806 100644 --- a/src/components/auth/LoginLink.tsx +++ b/src/components/auth/LoginLink.tsx @@ -1,4 +1,3 @@ - import React from "react"; import { Link } from "react-router-dom"; @@ -7,7 +6,10 @@ const LoginLink: React.FC = () => {

์ด๋ฏธ ๊ณ„์ •์ด ์žˆ์œผ์‹ ๊ฐ€์š”?{" "} - + ๋กœ๊ทธ์ธ

diff --git a/src/components/auth/PrivateRoute.tsx b/src/components/auth/PrivateRoute.tsx index 75c16bc..fe06295 100644 --- a/src/components/auth/PrivateRoute.tsx +++ b/src/components/auth/PrivateRoute.tsx @@ -1,8 +1,7 @@ - -import { ReactNode, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useAuth } from '@/contexts/auth'; -import { useToast } from '@/hooks/useToast.wrapper'; +import { ReactNode, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { useAuth } from "@/contexts/auth"; +import { useToast } from "@/hooks/useToast.wrapper"; interface PrivateRouteProps { children: ReactNode; @@ -25,7 +24,7 @@ const PrivateRoute = ({ children, requireAuth = true }: PrivateRouteProps) => { description: "์ด ํŽ˜์ด์ง€์— ์ ‘๊ทผํ•˜๋ ค๋ฉด ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", variant: "destructive", }); - navigate('/login', { replace: true }); + navigate("/login", { replace: true }); } }, [user, loading, navigate, requireAuth, toast]); diff --git a/src/components/auth/RegisterErrorDisplay.tsx b/src/components/auth/RegisterErrorDisplay.tsx index d6ef0b1..f01bc12 100644 --- a/src/components/auth/RegisterErrorDisplay.tsx +++ b/src/components/auth/RegisterErrorDisplay.tsx @@ -1,12 +1,15 @@ - import React from "react"; interface RegisterErrorDisplayProps { error: string | null; } -const RegisterErrorDisplay: React.FC = ({ error }) => { - if (!error) return null; +const RegisterErrorDisplay: React.FC = ({ + error, +}) => { + if (!error) { + return null; + } return (
diff --git a/src/components/auth/RegisterForm.tsx b/src/components/auth/RegisterForm.tsx index 549cd1c..72d7da2 100644 --- a/src/components/auth/RegisterForm.tsx +++ b/src/components/auth/RegisterForm.tsx @@ -1,4 +1,5 @@ import React, { useState } from "react"; +import { authLogger } from "@/utils/logger"; import { useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { ArrowRight } from "lucide-react"; @@ -10,7 +11,16 @@ import RegisterFormFields from "./RegisterFormFields"; import { supabase } from "@/archive/lib/supabase"; interface RegisterFormProps { - signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any, redirectToSettings?: boolean, emailConfirmationRequired?: boolean }>; + signUp: ( + email: string, + password: string, + username: string + ) => Promise<{ + error: any; + user: any; + redirectToSettings?: boolean; + emailConfirmationRequired?: boolean; + }>; serverStatus: ServerConnectionStatus; setServerStatus: React.Dispatch>; setRegisterError: React.Dispatch>; @@ -29,7 +39,7 @@ const RegisterForm: React.FC = ({ const [showPassword, setShowPassword] = useState(false); const [isLoading, setIsLoading] = useState(false); const [emailConfirmationSent, setEmailConfirmationSent] = useState(false); - + const { toast } = useToast(); const navigate = useNavigate(); @@ -42,7 +52,7 @@ const RegisterForm: React.FC = ({ }); return false; } - + if (password !== confirmPassword) { toast({ title: "๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜", @@ -51,7 +61,7 @@ const RegisterForm: React.FC = ({ }); return false; } - + // ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฐ•๋„ ๊ฒ€์‚ฌ if (password.length < 8) { toast({ @@ -61,7 +71,7 @@ const RegisterForm: React.FC = ({ }); return false; } - + // ์ด๋ฉ”์ผ ํ˜•์‹ ๊ฒ€์‚ฌ const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailPattern.test(email)) { @@ -72,7 +82,7 @@ const RegisterForm: React.FC = ({ }); return false; } - + return true; }; @@ -84,22 +94,24 @@ const RegisterForm: React.FC = ({ setServerStatus({ checked: true, connected: currentStatus.connected, - message: currentStatus.message + message: currentStatus.message, }); - + if (!currentStatus.connected) { toast({ title: "์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ", - description: "์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ๋˜๋Š” ์„œ๋ฒ„ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.", - variant: "destructive" + description: + "์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ๋˜๋Š” ์„œ๋ฒ„ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.", + variant: "destructive", }); return false; } } catch (error: any) { toast({ title: "์—ฐ๊ฒฐ ํ™•์ธ ์˜ค๋ฅ˜", - description: error.message || "์„œ๋ฒ„ ์—ฐ๊ฒฐ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" + description: + error.message || "์„œ๋ฒ„ ์—ฐ๊ฒฐ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + variant: "destructive", }); return false; } @@ -113,37 +125,39 @@ const RegisterForm: React.FC = ({ // ํ˜„์žฌ ๋ธŒ๋ผ์šฐ์ € URL ๊ฐ€์ ธ์˜ค๊ธฐ const currentUrl = window.location.origin; const redirectUrl = `${currentUrl}/login?auth_callback=true`; - + const { error } = await supabase.auth.resend({ - type: 'signup', + type: "signup", email, options: { emailRedirectTo: redirectUrl, }, }); - + if (error) { - console.error('์ธ์ฆ ๋ฉ”์ผ ์žฌ์ „์†ก ์‹คํŒจ:', error); + authLogger.error("์ธ์ฆ ๋ฉ”์ผ ์žฌ์ „์†ก ์‹คํŒจ:", error); toast({ title: "์ธ์ฆ ๋ฉ”์ผ ์žฌ์ „์†ก ์‹คํŒจ", - description: error.message || "์ธ์ฆ ๋ฉ”์ผ์„ ์žฌ์ „์†กํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" + description: + error.message || "์ธ์ฆ ๋ฉ”์ผ์„ ์žฌ์ „์†กํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + variant: "destructive", }); return; } - + toast({ title: "์ธ์ฆ ๋ฉ”์ผ ์žฌ์ „์†ก ์™„๋ฃŒ", description: `${email}๋กœ ์ธ์ฆ ๋ฉ”์ผ์ด ์žฌ์ „์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฉ”์ผ๊ณผ ์ŠคํŒธ ํด๋”๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.`, }); - - console.log('์ธ์ฆ ๋ฉ”์ผ ์žฌ์ „์†ก ์„ฑ๊ณต:', email); + + authLogger.info("์ธ์ฆ ๋ฉ”์ผ ์žฌ์ „์†ก ์„ฑ๊ณต:", email); } catch (error: any) { - console.error('์ธ์ฆ ๋ฉ”์ผ ์žฌ์ „์†ก ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ:', error); + authLogger.error("์ธ์ฆ ๋ฉ”์ผ ์žฌ์ „์†ก ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ:", error); toast({ title: "์ธ์ฆ ๋ฉ”์ผ ์žฌ์ „์†ก ์˜ค๋ฅ˜", - description: error.message || "์ธ์ฆ ๋ฉ”์ผ์„ ์žฌ์ „์†กํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" + description: + error.message || "์ธ์ฆ ๋ฉ”์ผ์„ ์žฌ์ „์†กํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + variant: "destructive", }); } }; @@ -151,60 +165,70 @@ const RegisterForm: React.FC = ({ const handleRegister = async (e: React.FormEvent) => { e.preventDefault(); setRegisterError(null); - + // ์„œ๋ฒ„ ์—ฐ๊ฒฐ ํ™•์ธ const isServerConnected = await checkServerConnectivity(); - if (!isServerConnected) return; - + if (!isServerConnected) { + return; + } + // ํผ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ - if (!validateForm()) return; - + if (!validateForm()) { + return; + } + setIsLoading(true); - + try { // ํšŒ์›๊ฐ€์ž… ์‹œ๋„ - const { error, user, redirectToSettings, emailConfirmationRequired } = await signUp(email, password, username); - + const { error, user, redirectToSettings, emailConfirmationRequired } = + await signUp(email, password, username); + if (error) { // ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ - setRegisterError(error.message || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); - + setRegisterError(error.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + // ์„ค์ • ํŽ˜์ด์ง€ ๋ฆฌ๋””๋ ‰์…˜์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ if (redirectToSettings) { toast({ title: "Supabase ์„ค์ • ํ•„์š”", description: "Supabase ์„ค์ •์„ ํ™•์ธํ•˜๊ณ  ์˜ฌ๋ฐ”๋ฅธ ๊ฐ’์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.", - variant: "destructive" + variant: "destructive", }); - + // 2์ดˆ ํ›„ ์„ค์ • ํŽ˜์ด์ง€๋กœ ์ด๋™ setTimeout(() => { navigate("/supabase-settings"); }, 2000); return; } - + // ๋„คํŠธ์›Œํฌ ๊ด€๋ จ ์˜ค๋ฅ˜์ธ ๊ฒฝ์šฐ ์ž์„ธํ•œ ์•ˆ๋‚ด - if (error.message && ( - error.message.includes('fetch') || - error.message.includes('๋„คํŠธ์›Œํฌ') || - error.message.includes('CORS'))) { + if ( + error.message && + (error.message.includes("fetch") || + error.message.includes("๋„คํŠธ์›Œํฌ") || + error.message.includes("CORS")) + ) { toast({ title: "๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜", - description: "์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์„ค์ •์—์„œ CORS ํ”„๋ก์‹œ๊ฐ€ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.", - variant: "destructive" + description: + "์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์„ค์ •์—์„œ CORS ํ”„๋ก์‹œ๊ฐ€ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.", + variant: "destructive", }); - } + } // ์„œ๋ฒ„ ์‘๋‹ต ๊ด€๋ จ ์˜ค๋ฅ˜์ธ ๊ฒฝ์šฐ - else if (error.message && ( - error.message.includes('400') || - error.message.includes('401') || - error.message.includes('403') || - error.message.includes('500'))) { + else if ( + error.message && + (error.message.includes("400") || + error.message.includes("401") || + error.message.includes("403") || + error.message.includes("500")) + ) { toast({ title: "์„œ๋ฒ„ ์‘๋‹ต ์˜ค๋ฅ˜", description: error.message, - variant: "destructive" + variant: "destructive", }); } } else if (user) { @@ -219,21 +243,22 @@ const RegisterForm: React.FC = ({ // ์ด๋ฉ”์ผ ํ™•์ธ์ด ํ•„์š”ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ (์ž๋™ ์Šน์ธ ๋“ฑ) toast({ title: "ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต", - description: "ํšŒ์›๊ฐ€์ž…์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.", + description: + "ํšŒ์›๊ฐ€์ž…์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.", }); - + // ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ navigate("/login"); } } } catch (error: any) { - console.error("ํšŒ์›๊ฐ€์ž… ์ฒ˜๋ฆฌ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ:", error); - setRegisterError(error.message || '์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); - + authLogger.error("ํšŒ์›๊ฐ€์ž… ์ฒ˜๋ฆฌ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ:", error); + setRegisterError(error.message || "์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + toast({ title: "ํšŒ์›๊ฐ€์ž… ์˜ค๋ฅ˜", description: error.message || "ํšŒ์›๊ฐ€์ž… ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" + variant: "destructive", }); } finally { setIsLoading(false); @@ -242,11 +267,13 @@ const RegisterForm: React.FC = ({ // ์ด๋ฉ”์ผ ์ธ์ฆ ์•ˆ๋‚ด ํ™”๋ฉด (์ธ์ฆ ๋ฉ”์ผ์ด ๋ฐœ์†ก๋œ ๊ฒฝ์šฐ) if (emailConfirmationSent) { - return setEmailConfirmationSent(false)} - onResendEmail={handleResendVerificationEmail} - />; + return ( + setEmailConfirmationSent(false)} + onResendEmail={handleResendVerificationEmail} + /> + ); } // ์ผ๋ฐ˜ ํšŒ์›๊ฐ€์ž… ์–‘์‹ @@ -265,13 +292,16 @@ const RegisterForm: React.FC = ({ showPassword={showPassword} setShowPassword={setShowPassword} /> - +
diff --git a/src/components/auth/RegisterFormFields.tsx b/src/components/auth/RegisterFormFields.tsx index 572d8ac..6e1862b 100644 --- a/src/components/auth/RegisterFormFields.tsx +++ b/src/components/auth/RegisterFormFields.tsx @@ -1,4 +1,3 @@ - import React, { useState } from "react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; @@ -28,12 +27,14 @@ const RegisterFormFields: React.FC = ({ confirmPassword, setConfirmPassword, showPassword, - setShowPassword + setShowPassword, }) => { return (
- +
= ({ />
- +
- +
= ({ />
- +
- +
= ({ onClick={() => setShowPassword(!showPassword)} className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500" > - {showPassword ? : } + {showPassword ? ( + + ) : ( + + )}
{password && password.length > 0 && password.length < 8 && ( -

๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์ตœ์†Œ 8์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

+

+ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์ตœ์†Œ 8์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. +

)}
- +
- +
= ({ />
{confirmPassword && password !== confirmPassword && ( -

๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

+

+ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. +

)}
- + ์ด๋ฉ”์ผ ์ธ์ฆ ํ•„์š” - ํšŒ์›๊ฐ€์ž… ํ›„ ์ด๋ฉ”์ผ๋กœ ์ธ์ฆ ๋งํฌ๊ฐ€ ๋ฐœ์†ก๋ฉ๋‹ˆ๋‹ค. - ์ด๋ฉ”์ผ ์ธ์ฆ์„ ์™„๋ฃŒํ•ด์•ผ ๋กœ๊ทธ์ธ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. + ํšŒ์›๊ฐ€์ž… ํ›„ ์ด๋ฉ”์ผ๋กœ ์ธ์ฆ ๋งํฌ๊ฐ€ ๋ฐœ์†ก๋ฉ๋‹ˆ๋‹ค. ์ด๋ฉ”์ผ ์ธ์ฆ์„ ์™„๋ฃŒํ•ด์•ผ + ๋กœ๊ทธ์ธ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
diff --git a/src/components/auth/RegisterHeader.tsx b/src/components/auth/RegisterHeader.tsx index dd29bb9..d161b95 100644 --- a/src/components/auth/RegisterHeader.tsx +++ b/src/components/auth/RegisterHeader.tsx @@ -1,12 +1,15 @@ - import React from "react"; const RegisterHeader: React.FC = () => { return (
-

์ ค๋ฆฌ์˜ ์ ์žํƒˆ์ถœ

+

+ ์ ค๋ฆฌ์˜ ์ ์žํƒˆ์ถœ +

์ƒˆ ๊ณ„์ •์„ ๋งŒ๋“ค๊ณ  ์žฌ์ • ๊ด€๋ฆฌ๋ฅผ ์‹œ์ž‘ํ•˜์„ธ์š”

-

์˜จํ”„๋ ˆ๋ฏธ์Šค Supabase ์—ฐ๊ฒฐ ์ตœ์ ํ™” ์™„๋ฃŒ

+

+ ์˜จํ”„๋ ˆ๋ฏธ์Šค Supabase ์—ฐ๊ฒฐ ์ตœ์ ํ™” ์™„๋ฃŒ +

); }; diff --git a/src/components/auth/ServerStatusAlert.tsx b/src/components/auth/ServerStatusAlert.tsx index 26d1132..5f81288 100644 --- a/src/components/auth/ServerStatusAlert.tsx +++ b/src/components/auth/ServerStatusAlert.tsx @@ -1,5 +1,4 @@ - -import React from 'react'; +import React from "react"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { AlertCircle, RefreshCw } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -13,7 +12,10 @@ interface ServerStatusAlertProps { checkServerConnection: () => Promise; } -const ServerStatusAlert = ({ serverStatus, checkServerConnection }: ServerStatusAlertProps) => { +const ServerStatusAlert = ({ + serverStatus, + checkServerConnection, +}: ServerStatusAlertProps) => { if (!serverStatus.checked || serverStatus.connected) { return null; } @@ -24,9 +26,9 @@ const ServerStatusAlert = ({ serverStatus, checkServerConnection }: ServerStatus ์„œ๋ฒ„ ์—ฐ๊ฒฐ ๋ฌธ์ œ {serverStatus.message} -
diff --git a/src/components/auth/types.ts b/src/components/auth/types.ts index 0f25655..d965e87 100644 --- a/src/components/auth/types.ts +++ b/src/components/auth/types.ts @@ -1,13 +1,12 @@ - -export interface ServerConnectionStatus { - checked: boolean; - connected: boolean; - message: string; - details?: string; -} +import type { ServerConnectionStatus } from "@/types/common"; +import type { SignUpResponse } from "@/contexts/auth/types"; export interface RegisterFormProps { - signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any }>; + signUp: ( + email: string, + password: string, + username: string + ) => Promise; serverStatus: ServerConnectionStatus; setServerStatus: React.Dispatch>; setRegisterError: (error: string | null) => void; diff --git a/src/components/budget/BudgetDialog.tsx b/src/components/budget/BudgetDialog.tsx index e712b25..fabcd47 100644 --- a/src/components/budget/BudgetDialog.tsx +++ b/src/components/budget/BudgetDialog.tsx @@ -1,16 +1,15 @@ - -import React from 'react'; -import { - Dialog, - DialogContent, - DialogHeader, +import React from "react"; +import { + Dialog, + DialogContent, + DialogHeader, DialogTitle, - DialogDescription -} from '@/components/ui/dialog'; -import CategoryBudgetInputs from '../CategoryBudgetInputs'; -import { Button } from '@/components/ui/button'; -import { Check } from 'lucide-react'; -import { formatCurrency } from '@/utils/formatters'; + DialogDescription, +} from "@/components/ui/dialog"; +import CategoryBudgetInputs from "../CategoryBudgetInputs"; +import { Button } from "@/components/ui/button"; +import { Check } from "lucide-react"; +import { formatCurrency } from "@/utils/formatters"; interface BudgetDialogProps { open: boolean; @@ -29,7 +28,7 @@ const BudgetDialog: React.FC = ({ handleCategoryInputChange, handleSaveCategoryBudgets, calculateTotalBudget, - isSubmitting = false + isSubmitting = false, }) => { const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -40,10 +39,12 @@ const BudgetDialog: React.FC = ({ const formattedTotal = formatCurrency(calculateTotalBudget()); return ( - { - if (isSubmitting && !newOpen) return; + if (isSubmitting && !newOpen) { + return; + } onOpenChange(newOpen); }} > @@ -51,32 +52,35 @@ const BudgetDialog: React.FC = ({ ์˜ˆ์‚ฐ ์„ค์ • - ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„๋กœ ์›”๊ฐ„ ์˜ˆ์‚ฐ์„ ์„ค์ •ํ•˜์„ธ์š”. ์ผ์ผ, ์ฃผ๊ฐ„ ์˜ˆ์‚ฐ์€ ์ž๋™์œผ๋กœ ๊ณ„์‚ฐ๋ฉ๋‹ˆ๋‹ค. + ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„๋กœ ์›”๊ฐ„ ์˜ˆ์‚ฐ์„ ์„ค์ •ํ•˜์„ธ์š”. ์ผ์ผ, ์ฃผ๊ฐ„ ์˜ˆ์‚ฐ์€ ์ž๋™์œผ๋กœ + ๊ณ„์‚ฐ๋ฉ๋‹ˆ๋‹ค. - +
-

์›”๊ฐ„ ์ด ์˜ˆ์‚ฐ:

-

{formattedTotal}

+

+ {formattedTotal} +

- -
@@ -39,9 +42,9 @@ const BudgetInputButton: React.FC = ({ return (
์•„์ง ์˜ˆ์‚ฐ์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค
- -
); diff --git a/src/components/expenses/ExpenseTitleInput.tsx b/src/components/expenses/ExpenseTitleInput.tsx index 0765619..abcf40b 100644 --- a/src/components/expenses/ExpenseTitleInput.tsx +++ b/src/components/expenses/ExpenseTitleInput.tsx @@ -1,18 +1,17 @@ - -import React from 'react'; -import { FormField, FormItem, FormLabel } from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { UseFormReturn } from 'react-hook-form'; -import { ExpenseFormValues } from './ExpenseForm'; +import React from "react"; +import { FormField, FormItem, FormLabel } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { UseFormReturn } from "react-hook-form"; +import { ExpenseFormValues } from "./ExpenseForm"; interface ExpenseTitleInputProps { form: UseFormReturn; isDisabled?: boolean; } -const ExpenseTitleInput: React.FC = ({ +const ExpenseTitleInput: React.FC = ({ form, - isDisabled = false + isDisabled = false, }) => { return ( = ({ render={({ field }) => ( ์ œ๋ชฉ - diff --git a/src/components/expenses/ExpenseTitleSuggestions.tsx b/src/components/expenses/ExpenseTitleSuggestions.tsx index 8c45b0c..07a669d 100644 --- a/src/components/expenses/ExpenseTitleSuggestions.tsx +++ b/src/components/expenses/ExpenseTitleSuggestions.tsx @@ -1,7 +1,6 @@ - -import React from 'react'; -import { Badge } from '@/components/ui/badge'; -import { getPersonalizedTitleSuggestions } from '@/utils/userTitlePreferences'; +import React from "react"; +import { Badge } from "@/components/ui/badge"; +import { getPersonalizedTitleSuggestions } from "@/utils/userTitlePreferences"; interface ExpenseTitleSuggestionsProps { category: string; @@ -10,18 +9,18 @@ interface ExpenseTitleSuggestionsProps { const ExpenseTitleSuggestions: React.FC = ({ category, - onSuggestionClick + onSuggestionClick, }) => { const titleSuggestions = getPersonalizedTitleSuggestions(category); - + if (!category || titleSuggestions.length === 0) { return null; } - + return (
{titleSuggestions.map((suggestion) => ( - = ({ - message = "์•„์ง ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค", - subMessage = "์˜ˆ์‚ฐ์„ ์„ค์ •ํ•˜๊ณ  ์ง€์ถœ์„ ์ถ”๊ฐ€ํ•ด ๋ณด์„ธ์š”" +const EmptyState: React.FC = ({ + message = "์•„์ง ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค", + subMessage = "์˜ˆ์‚ฐ์„ ์„ค์ •ํ•˜๊ณ  ์ง€์ถœ์„ ์ถ”๊ฐ€ํ•ด ๋ณด์„ธ์š”", }) => { return (
diff --git a/src/components/home/HomeContent.tsx b/src/components/home/HomeContent.tsx index 2440378..c80eade 100644 --- a/src/components/home/HomeContent.tsx +++ b/src/components/home/HomeContent.tsx @@ -1,19 +1,22 @@ - -import React from 'react'; -import BudgetProgressCard from '@/components/BudgetProgressCard'; -import BudgetCategoriesSection from '@/components/BudgetCategoriesSection'; -import RecentTransactionsSection from '@/components/RecentTransactionsSection'; -import EmptyState from './EmptyState'; -import { BudgetPeriod } from '@/contexts/budget/BudgetContext'; -import { formatCurrency, calculatePercentage } from '@/utils/formatters'; -import { Transaction, BudgetData } from '@/contexts/budget/types'; +import React from "react"; +import BudgetProgressCard from "@/components/BudgetProgressCard"; +import BudgetCategoriesSection from "@/components/BudgetCategoriesSection"; +import RecentTransactionsSection from "@/components/RecentTransactionsSection"; +import EmptyState from "./EmptyState"; +import { BudgetPeriod } from "@/contexts/budget/BudgetContext"; +import { formatCurrency, calculatePercentage } from "@/utils/formatters"; +import { Transaction, BudgetData } from "@/contexts/budget/types"; interface HomeContentProps { transactions: Transaction[]; budgetData: BudgetData; selectedTab: string; setSelectedTab: (value: string) => void; - handleBudgetGoalUpdate: (type: BudgetPeriod, amount: number, newCategoryBudgets?: Record) => void; + handleBudgetGoalUpdate: ( + type: BudgetPeriod, + amount: number, + newCategoryBudgets?: Record + ) => void; updateTransaction: (transaction: Transaction) => void; getCategorySpending: () => Array<{ title: string; @@ -29,12 +32,13 @@ const HomeContent: React.FC = ({ setSelectedTab, handleBudgetGoalUpdate, updateTransaction, - getCategorySpending + getCategorySpending, }) => { // getCategorySpending ํ•จ์ˆ˜์˜ ๋ฐ˜ํ™˜๊ฐ’์„ ๋ฐ”๋กœ ์‚ฌ์šฉํ•˜์ง€ ๋ง๊ณ , ๋ณ€์ˆ˜์— ํ• ๋‹นํ•˜์—ฌ ์‚ฌ์šฉ const categorySpendingData = getCategorySpending(); - const hasAnySpending = Array.isArray(categorySpendingData) && - categorySpendingData.some(cat => cat.current > 0 || cat.total > 0); + const hasAnySpending = + Array.isArray(categorySpendingData) && + categorySpendingData.some((cat) => cat.current > 0 || cat.total > 0); return (
@@ -44,7 +48,7 @@ const HomeContent: React.FC = ({ )}

์›”๊ฐ„ ์˜ˆ์‚ฐ๊ณผ ์ง€์ถœ

- = ({ onSaveBudget={handleBudgetGoalUpdate} /> {transactions.length > 0 ? ( - ) : ( diff --git a/src/components/home/IndexContent.tsx b/src/components/home/IndexContent.tsx index 282fd08..a9ef65e 100644 --- a/src/components/home/IndexContent.tsx +++ b/src/components/home/IndexContent.tsx @@ -1,27 +1,26 @@ - -import React from 'react'; -import Header from '@/components/Header'; -import HomeContent from '@/components/home/HomeContent'; -import { useBudget } from '@/contexts/budget/BudgetContext'; -import { BudgetData } from '@/contexts/budget/types'; +import React from "react"; +import Header from "@/components/Header"; +import HomeContent from "@/components/home/HomeContent"; +import { useBudget } from "@/contexts/budget/BudgetContext"; +import { BudgetData } from "@/contexts/budget/types"; // ๊ธฐ๋ณธ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ (๋นˆ ๊ฐ์ฒด ๋Œ€์‹  ์‚ฌ์šฉํ•  ๋”๋ฏธ ๋ฐ์ดํ„ฐ) const defaultBudgetData: BudgetData = { daily: { targetAmount: 0, spentAmount: 0, - remainingAmount: 0 + remainingAmount: 0, }, weekly: { targetAmount: 0, spentAmount: 0, - remainingAmount: 0 + remainingAmount: 0, }, monthly: { targetAmount: 0, spentAmount: 0, - remainingAmount: 0 - } + remainingAmount: 0, + }, }; /** @@ -35,16 +34,16 @@ const IndexContent: React.FC = () => { setSelectedTab, handleBudgetGoalUpdate, updateTransaction, - getCategorySpending + getCategorySpending, } = useBudget(); return (
- = ({ notifications, onClearAll, - onReadNotification + onReadNotification, }) => { - const unreadCount = notifications.filter(notification => !notification.read).length; - + const unreadCount = notifications.filter( + (notification) => !notification.read + ).length; + const handleClearAll = () => { onClearAll(); - toast.success('๋ชจ๋“  ์•Œ๋ฆผ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + toast.success("๋ชจ๋“  ์•Œ๋ฆผ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); }; const formatDate = (date: Date) => { - return new Intl.DateTimeFormat('ko-KR', { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' + return new Intl.DateTimeFormat("ko-KR", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", }).format(date); }; @@ -49,9 +54,7 @@ const NotificationPopover: React.FC = ({
{notifications.length > 0 && ( - )}
- + - +
{notifications.length === 0 ? (
@@ -91,12 +92,21 @@ const NotificationPopover: React.FC = ({
) : ( notifications.map((notification) => ( -
+
-

{notification.title}

-

{notification.message}

-

{formatDate(notification.timestamp)}

+

+ {notification.title} +

+

+ {notification.message} +

+

+ {formatDate(notification.timestamp)} +

- setDontShowAgain(checked === true)} className="focus:outline-none focus:ring-0 focus:ring-offset-0" /> -
- -
; +
+ ); }; -export default WelcomeDialog; \ No newline at end of file +export default WelcomeDialog; diff --git a/src/components/profile/PasswordChangeForm.tsx b/src/components/profile/PasswordChangeForm.tsx index 35c987e..b46b1d3 100644 --- a/src/components/profile/PasswordChangeForm.tsx +++ b/src/components/profile/PasswordChangeForm.tsx @@ -1,29 +1,48 @@ +import React, { useState } from "react"; +import { logger } from "@/utils/logger"; +import { Key, Eye, EyeOff } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useToast } from "@/hooks/useToast.wrapper"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; -import React, { useState } from 'react'; -import { Key, Eye, EyeOff } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useToast } from '@/hooks/useToast.wrapper'; -import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; - -const passwordFormSchema = z.object({ - currentPassword: z.string().min(6, { - message: '๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 6์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.', - }), - newPassword: z.string().min(8, { - message: '์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.', - }), - confirmPassword: z.string().min(8, { - message: '๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ์€ 8์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.', - }), -}).refine((data) => data.newPassword === data.confirmPassword, { - message: "๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", - path: ["confirmPassword"], -}); +const passwordFormSchema = z + .object({ + currentPassword: z.string().min(6, { + message: "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 6์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + }), + newPassword: z.string().min(8, { + message: "์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + }), + confirmPassword: z.string().min(8, { + message: "๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ์€ 8์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + }), + }) + .refine((data) => data.newPassword === data.confirmPassword, { + message: "๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", + path: ["confirmPassword"], + }); type PasswordFormValues = z.infer; @@ -36,14 +55,14 @@ const PasswordChangeForm = () => { const passwordForm = useForm({ resolver: zodResolver(passwordFormSchema), defaultValues: { - currentPassword: '', - newPassword: '', - confirmPassword: '', + currentPassword: "", + newPassword: "", + confirmPassword: "", }, }); const onPasswordSubmit = (data: PasswordFormValues) => { - console.log('Password form submitted:', data); + logger.info("Password form submitted:", data); // Here you would typically update the password toast({ title: "๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์™„๋ฃŒ", @@ -56,7 +75,10 @@ const PasswordChangeForm = () => {

๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ

- + { ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ
-
@@ -85,7 +113,7 @@ const PasswordChangeForm = () => { )} /> - + { ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ
-
@@ -114,7 +146,7 @@ const PasswordChangeForm = () => { )} /> - + { ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ
-
@@ -143,11 +181,11 @@ const PasswordChangeForm = () => { )} /> - + -

ํ”„๋กœํ•„ ๊ด€๋ฆฌ

- + {/* User Profile Picture */}
diff --git a/src/components/recent-transactions/RecentTransactionItem.tsx b/src/components/recent-transactions/RecentTransactionItem.tsx index 5ebf157..eca18aa 100644 --- a/src/components/recent-transactions/RecentTransactionItem.tsx +++ b/src/components/recent-transactions/RecentTransactionItem.tsx @@ -1,17 +1,16 @@ - -import React from 'react'; -import { Transaction } from '@/contexts/budget/types'; -import TransactionIcon from '../transaction/TransactionIcon'; -import { formatCurrency } from '@/utils/currencyFormatter'; +import React from "react"; +import { Transaction } from "@/contexts/budget/types"; +import TransactionIcon from "../transaction/TransactionIcon"; +import { formatCurrency } from "@/utils/currencyFormatter"; interface RecentTransactionItemProps { transaction: Transaction; onClick: () => void; } -const RecentTransactionItem: React.FC = ({ - transaction, - onClick +const RecentTransactionItem: React.FC = ({ + transaction, + onClick, }) => { return (
= ({ onConfirm, isResetting, isLoggedIn, - syncEnabled + syncEnabled, }) => { - return + return ( + ์ •๋ง ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? - {isLoggedIn ? <> - ์ด ์ž‘์—…์€ ๋˜๋Œ๋ฆด ์ˆ˜ ์—†์œผ๋ฉฐ, ๋กœ์ปฌ ๋ฐ ํด๋ผ์šฐ๋“œ์— ์ €์žฅ๋œ ๋ชจ๋“  ์˜ˆ์‚ฐ, ์ง€์ถœ ๋‚ด์—ญ์ด ์˜๊ตฌ์ ์œผ๋กœ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค. + {isLoggedIn ? ( + <> + ์ด ์ž‘์—…์€ ๋˜๋Œ๋ฆด ์ˆ˜ ์—†์œผ๋ฉฐ, ๋กœ์ปฌ ๋ฐ ํด๋ผ์šฐ๋“œ์— ์ €์žฅ๋œ ๋ชจ๋“  ์˜ˆ์‚ฐ, + ์ง€์ถœ ๋‚ด์—ญ์ด ์˜๊ตฌ์ ์œผ๋กœ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค.
- + ํด๋ผ์šฐ๋“œ ๋ฐ์ดํ„ฐ๋„ ํ•จ๊ป˜ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค.
{syncEnabled && ( @@ -37,7 +47,10 @@ const DataResetDialog: React.FC = ({ โ€ป ๋™๊ธฐํ™” ์„ค์ •์ด ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.
)} - : "์ด ์ž‘์—…์€ ๋˜๋Œ๋ฆด ์ˆ˜ ์—†์œผ๋ฉฐ, ๋ชจ๋“  ์˜ˆ์‚ฐ, ์ง€์ถœ ๋‚ด์—ญ, ์„ค์ •์ด ์˜๊ตฌ์ ์œผ๋กœ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค."} + + ) : ( + "์ด ์ž‘์—…์€ ๋˜๋Œ๋ฆด ์ˆ˜ ์—†์œผ๋ฉฐ, ๋ชจ๋“  ์˜ˆ์‚ฐ, ์ง€์ถœ ๋‚ด์—ญ, ์„ค์ •์ด ์˜๊ตฌ์ ์œผ๋กœ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค." + )}
๋‹จ, 'ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค' ํ™”๋ฉด ํ‘œ์‹œ ์„ค์ •๊ณผ ๋กœ๊ทธ์ธ ์ƒํƒœ๋Š” ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.
@@ -45,17 +58,34 @@ const DataResetDialog: React.FC = ({ - + - -
; + + ); }; export default DataResetDialog; diff --git a/src/components/security/DataResetSection.tsx b/src/components/security/DataResetSection.tsx index 58e2600..a49ecfa 100644 --- a/src/components/security/DataResetSection.tsx +++ b/src/components/security/DataResetSection.tsx @@ -1,12 +1,11 @@ - -import React, { useState } from 'react'; -import { Trash2 } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { useAuth } from '@/contexts/auth'; -import { useDataReset } from '@/hooks/useDataReset'; -import DataResetDialog from './DataResetDialog'; -import { isSyncEnabled } from '@/utils/sync/syncSettings'; -import { toast } from '@/hooks/useToast.wrapper'; +import React, { useState } from "react"; +import { Trash2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useAuth } from "@/contexts/auth"; +import { useDataReset } from "@/hooks/useDataReset"; +import DataResetDialog from "./DataResetDialog"; +import { isSyncEnabled } from "@/utils/sync/syncSettings"; +import { toast } from "@/hooks/useToast.wrapper"; const DataResetSection = () => { const [isResetDialogOpen, setIsResetDialogOpen] = useState(false); @@ -17,7 +16,7 @@ const DataResetSection = () => { const handleResetAllData = async () => { await resetAllData(); setIsResetDialogOpen(false); - + // ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ํ›„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฆฌ๋กœ๋“œ // toast ์•Œ๋ฆผ์€ useDataReset.ts์—์„œ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ์—ฌ๊ธฐ์„œ๋Š” ์ œ๊ฑฐ }; @@ -32,14 +31,14 @@ const DataResetSection = () => {

๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”

- {user + {user ? "๋กœ์ปฌ ๋ฐ ํด๋ผ์šฐ๋“œ์˜ ๋ชจ๋“  ์˜ˆ์‚ฐ, ์ง€์ถœ ๋‚ด์—ญ์ด ์ดˆ๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค. ๋™๊ธฐํ™” ์„ค์ •์€ ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค." : "๋ชจ๋“  ์˜ˆ์‚ฐ, ์ง€์ถœ ๋‚ด์—ญ, ์„ค์ •์ด ์ดˆ๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค."}

- ) : (
- ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค - -
diff --git a/src/components/transaction/TransactionFormFields.tsx b/src/components/transaction/TransactionFormFields.tsx index 95848db..fc0ee7d 100644 --- a/src/components/transaction/TransactionFormFields.tsx +++ b/src/components/transaction/TransactionFormFields.tsx @@ -1,41 +1,45 @@ - -import React, { useState, useEffect } from 'react'; -import { UseFormReturn } from 'react-hook-form'; -import { z } from 'zod'; -import TransactionCategorySelector from './TransactionCategorySelector'; -import TransactionTitleSuggestions from './TransactionTitleSuggestions'; -import TransactionTitleInput from './TransactionTitleInput'; -import TransactionAmountInput from './TransactionAmountInput'; -import TransactionPaymentMethod from './TransactionPaymentMethod'; +import React, { useState, useEffect } from "react"; +import { UseFormReturn } from "react-hook-form"; +import { z } from "zod"; +import { PaymentMethod } from "@/types"; +import TransactionCategorySelector from "./TransactionCategorySelector"; +import TransactionTitleSuggestions from "./TransactionTitleSuggestions"; +import TransactionTitleInput from "./TransactionTitleInput"; +import TransactionAmountInput from "./TransactionAmountInput"; +import TransactionPaymentMethod from "./TransactionPaymentMethod"; // Form schema for validation export const transactionFormSchema = z.object({ - title: z.string().min(1, '์ œ๋ชฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'), - amount: z.string().min(1, '๊ธˆ์•ก์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'), - category: z.enum(['์Œ์‹', '์‡ผํ•‘', '๊ตํ†ต', '๊ธฐํƒ€']), - paymentMethod: z.enum(['์‹ ์šฉ์นด๋“œ', 'ํ˜„๊ธˆ']).default('์‹ ์šฉ์นด๋“œ'), + title: z.string().min(1, "์ œ๋ชฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"), + amount: z.string().min(1, "๊ธˆ์•ก์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"), + category: z.enum(["์Œ์‹", "์‡ผํ•‘", "๊ตํ†ต", "๊ธฐํƒ€"]), + paymentMethod: z + .enum(["์‹ ์šฉ์นด๋“œ", "ํ˜„๊ธˆ", "์ฒดํฌ์นด๋“œ", "๊ฐ„ํŽธ๊ฒฐ์ œ"] as const) + .default("์‹ ์šฉ์นด๋“œ"), }); export type TransactionFormValues = z.infer; // Function to format number with commas export const formatWithCommas = (value: string) => { - const numericValue = value.replace(/[^0-9]/g, ''); - return numericValue.replace(/\B(?=(\d{3})+(?!\d))/g, ','); + const numericValue = value.replace(/[^0-9]/g, ""); + return numericValue.replace(/\B(?=(\d{3})+(?!\d))/g, ","); }; interface TransactionFormFieldsProps { form: UseFormReturn; } -const TransactionFormFields: React.FC = ({ form }) => { +const TransactionFormFields: React.FC = ({ + form, +}) => { // ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค const [showTitleSuggestions, setShowTitleSuggestions] = useState(false); const [showPaymentMethod, setShowPaymentMethod] = useState(false); - + // ํ˜„์žฌ ์„ ํƒ๋œ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ€์ ธ์˜ค๊ธฐ - const selectedCategory = form.watch('category'); - + const selectedCategory = form.watch("category"); + // ์นดํ…Œ๊ณ ๋ฆฌ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์ œ๋ชฉ ์ถ”์ฒœ ํ‘œ์‹œ useEffect(() => { if (selectedCategory) { @@ -52,26 +56,26 @@ const TransactionFormFields: React.FC = ({ form }) = <> {/* ์นดํ…Œ๊ณ ๋ฆฌ ํ•„๋“œ๋ฅผ ์ฒซ ๋ฒˆ์งธ๋กœ ๋ฐฐ์น˜ */} - + {/* ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ œ๋ชฉ ์ œ์•ˆ - ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ ํ›„์—๋งŒ ํ‘œ์‹œ */} - - + {/* ์ œ๋ชฉ ํ•„๋“œ๋ฅผ ๋‘ ๋ฒˆ์งธ๋กœ ๋ฐฐ์น˜ */} - + {/* ๊ธˆ์•ก ํ•„๋“œ๋ฅผ ์„ธ ๋ฒˆ์งธ๋กœ ๋ฐฐ์น˜ */} - setShowPaymentMethod(true)} + setShowPaymentMethod(true)} /> {/* ์ง€์ถœ ๋ฐฉ๋ฒ• ํ•„๋“œ๋Š” ๊ธˆ์•ก ์ž…๋ ฅ ์‹œ์—๋งŒ ํ‘œ์‹œ */} - ); diff --git a/src/components/transaction/TransactionIcon.tsx b/src/components/transaction/TransactionIcon.tsx index 65144ca..9d309ac 100644 --- a/src/components/transaction/TransactionIcon.tsx +++ b/src/components/transaction/TransactionIcon.tsx @@ -1,7 +1,6 @@ - -import React from 'react'; -import { Coffee, Package } from 'lucide-react'; -import { categoryIcons } from '@/constants/categoryIcons'; +import React from "react"; +import { Coffee, Package } from "lucide-react"; +import { categoryIcons } from "@/constants/categoryIcons"; interface TransactionIconProps { category: string; @@ -10,7 +9,7 @@ interface TransactionIconProps { const TransactionIcon: React.FC = ({ category }) => { // ์นดํ…Œ๊ณ ๋ฆฌ์— ํ•ด๋‹นํ•˜๋Š” ์•„์ด์ฝ˜์ด ์—†์„ ๊ฒฝ์šฐ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ Package ์•„์ด์ฝ˜ ์‚ฌ์šฉ const icon = categoryIcons[category] || ; - + return (
{icon} diff --git a/src/components/transaction/TransactionPaymentMethod.tsx b/src/components/transaction/TransactionPaymentMethod.tsx index f5b1423..5803385 100644 --- a/src/components/transaction/TransactionPaymentMethod.tsx +++ b/src/components/transaction/TransactionPaymentMethod.tsx @@ -1,30 +1,35 @@ - -import React from 'react'; -import { FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'; -import { UseFormReturn } from 'react-hook-form'; -import { TransactionFormValues } from './TransactionFormFields'; -import { Separator } from '@/components/ui/separator'; -import { CreditCard, Banknote } from 'lucide-react'; +import React from "react"; +import { + FormField, + FormItem, + FormLabel, + FormControl, + FormMessage, +} from "@/components/ui/form"; +import { UseFormReturn } from "react-hook-form"; +import { TransactionFormValues } from "./TransactionFormFields"; +import { Separator } from "@/components/ui/separator"; +import { CreditCard, Banknote } from "lucide-react"; interface TransactionPaymentMethodProps { form: UseFormReturn; showPaymentMethod: boolean; } -const TransactionPaymentMethod: React.FC = ({ - form, - showPaymentMethod +const TransactionPaymentMethod: React.FC = ({ + form, + showPaymentMethod, }) => { return ( -
- + = ({
form.setValue('paymentMethod', '์‹ ์šฉ์นด๋“œ')} + onClick={() => form.setValue("paymentMethod", "์‹ ์šฉ์นด๋“œ")} > ์‹ ์šฉ์นด๋“œ
form.setValue('paymentMethod', 'ํ˜„๊ธˆ')} + onClick={() => form.setValue("paymentMethod", "ํ˜„๊ธˆ")} > ํ˜„๊ธˆ diff --git a/src/components/transaction/TransactionTitleInput.tsx b/src/components/transaction/TransactionTitleInput.tsx index 886aa96..7d9f2d2 100644 --- a/src/components/transaction/TransactionTitleInput.tsx +++ b/src/components/transaction/TransactionTitleInput.tsx @@ -1,15 +1,22 @@ - -import React from 'react'; -import { FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { UseFormReturn } from 'react-hook-form'; -import { TransactionFormValues } from './TransactionFormFields'; +import React from "react"; +import { + FormField, + FormItem, + FormLabel, + FormControl, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { UseFormReturn } from "react-hook-form"; +import { TransactionFormValues } from "./TransactionFormFields"; interface TransactionTitleInputProps { form: UseFormReturn; } -const TransactionTitleInput: React.FC = ({ form }) => { +const TransactionTitleInput: React.FC = ({ + form, +}) => { return ( ; showTitleSuggestions: boolean; } -const TransactionTitleSuggestions: React.FC = ({ - form, - showTitleSuggestions -}) => { +const TransactionTitleSuggestions: React.FC< + TransactionTitleSuggestionsProps +> = ({ form, showTitleSuggestions }) => { // ํ˜„์žฌ ์„ ํƒ๋œ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ€์ ธ์˜ค๊ธฐ - const selectedCategory = form.watch('category'); - + const selectedCategory = form.watch("category"); + // ์„ ํƒ๋œ ์นดํ…Œ๊ณ ๋ฆฌ์— ๋Œ€ํ•œ ๊ฐœ์ธํ™”๋œ ์ œ๋ชฉ ์ œ์•ˆ ๋ชฉ๋ก ์ƒํƒœ const [titleSuggestions, setTitleSuggestions] = useState([]); - + // ์นดํ…Œ๊ณ ๋ฆฌ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ๊ฐœ์ธํ™”๋œ ์ œ๋ชฉ ๋ชฉ๋ก ์—…๋ฐ์ดํŠธ useEffect(() => { if (selectedCategory) { @@ -30,24 +28,24 @@ const TransactionTitleSuggestions: React.FC = // ์ œ์•ˆ๋œ ์ œ๋ชฉ ํด๋ฆญ ์‹œ ์ œ๋ชฉ ํ•„๋“œ์— ์„ค์ • const handleTitleSuggestionClick = (suggestion: string) => { - form.setValue('title', suggestion); + form.setValue("title", suggestion); }; - + if (!selectedCategory || titleSuggestions.length === 0) { return null; } - + return ( -
{titleSuggestions.map((suggestion) => ( - { - if (oldCategory === '์‹๋น„') return '์Œ์‹'; - if (oldCategory === '์ƒํ™œ๋น„') return '์‡ผํ•‘'; - if (oldCategory === '๊ตํ†ต๋น„') return '๊ตํ†ต'; - if (EXPENSE_CATEGORIES.includes(oldCategory as any)) return oldCategory as "์Œ์‹" | "์‡ผํ•‘" | "๊ตํ†ต" | "๊ธฐํƒ€"; +export const mapCategoryToNew = ( + oldCategory: string +): "์Œ์‹" | "์‡ผํ•‘" | "๊ตํ†ต" | "๊ธฐํƒ€" => { + if (oldCategory === "์‹๋น„") { + return "์Œ์‹"; + } + if (oldCategory === "์ƒํ™œ๋น„") { + return "์‡ผํ•‘"; + } + if (oldCategory === "๊ตํ†ต๋น„") { + return "๊ตํ†ต"; + } + if (EXPENSE_CATEGORIES.includes(oldCategory as any)) { + return oldCategory as "์Œ์‹" | "์‡ผํ•‘" | "๊ตํ†ต" | "๊ธฐํƒ€"; + } // ๊ธฐ๋ณธ๊ฐ’์€ '๊ธฐํƒ€'๋กœ ์„ค์ • - return '๊ธฐํƒ€'; + return "๊ธฐํƒ€"; }; diff --git a/src/components/transaction/useTransactionEdit.ts b/src/components/transaction/useTransactionEdit.ts index 2d42233..49d0d79 100644 --- a/src/components/transaction/useTransactionEdit.ts +++ b/src/components/transaction/useTransactionEdit.ts @@ -1,12 +1,12 @@ - -import { useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { Transaction } from '@/contexts/budget/types'; -import { useBudget } from '@/contexts/budget/BudgetContext'; -import { toast } from '@/hooks/useToast.wrapper'; -import { manageTitleSuggestions } from '@/utils/userTitlePreferences'; -import { TransactionFormValues } from './TransactionFormFields'; -import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons'; +import { useState } from "react"; +import { logger } from "@/utils/logger"; +import { useForm } from "react-hook-form"; +import { Transaction } from "@/contexts/budget/types"; +import { useBudget } from "@/contexts/budget/BudgetContext"; +import { toast } from "@/hooks/useToast.wrapper"; +import { manageTitleSuggestions } from "@/utils/userTitlePreferences"; +import { TransactionFormValues } from "./TransactionFormFields"; +import { EXPENSE_CATEGORIES } from "@/constants/categoryIcons"; export const useTransactionEdit = ( transaction: Transaction, @@ -21,57 +21,59 @@ export const useTransactionEdit = ( defaultValues: { title: transaction.title, amount: transaction.amount.toString(), - category: EXPENSE_CATEGORIES.includes(transaction.category) - ? (transaction.category as "์Œ์‹" | "์‡ผํ•‘" | "๊ตํ†ต" | "๊ธฐํƒ€") + category: EXPENSE_CATEGORIES.includes(transaction.category) + ? (transaction.category as "์Œ์‹" | "์‡ผํ•‘" | "๊ตํ†ต" | "๊ธฐํƒ€") : "๊ธฐํƒ€", - paymentMethod: transaction.paymentMethod || '์‹ ์šฉ์นด๋“œ' - } + paymentMethod: transaction.paymentMethod || "์‹ ์šฉ์นด๋“œ", + }, }); // ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ์ฒ˜๋ฆฌ const handleSubmit = (values: TransactionFormValues) => { try { setIsSubmitting(true); - + // ํผ ๊ฐ’์—์„œ ์ˆซ์ž ๊ฐ’ ์ถ”์ถœ (์ฝค๋งˆ ์ œ๊ฑฐ) - const numericAmount = values.amount.replace(/,/g, ''); - + const numericAmount = values.amount.replace(/,/g, ""); + // ์—…๋ฐ์ดํŠธ๋œ ํŠธ๋žœ์žญ์…˜ ๊ฐ์ฒด ์ƒ์„ฑ const updatedTransaction: Transaction = { ...transaction, title: values.title, amount: parseInt(numericAmount), category: values.category, - paymentMethod: values.paymentMethod + paymentMethod: values.paymentMethod, }; - + // ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ updateTransaction(updatedTransaction); - + // ์ง€์ถœ์ผ ๊ฒฝ์šฐ ์ œ๋ชฉ ๊ด€๋ฆฌ ๋กœ์ง ์‹คํ–‰ - if (updatedTransaction.type === 'expense') { + if (updatedTransaction.type === "expense") { manageTitleSuggestions(updatedTransaction); } - + // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ toast({ title: "๊ฑฐ๋ž˜ ๋‚ด์—ญ์ด ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค", description: `${updatedTransaction.title} ํ•ญ๋ชฉ์ด ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, }); - + // ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์ฒ˜๋ฆฌ - window.dispatchEvent(new CustomEvent('transactionChanged', { - detail: { type: 'update', transaction: updatedTransaction } - })); - + window.dispatchEvent( + new CustomEvent("transactionChanged", { + detail: { type: "update", transaction: updatedTransaction }, + }) + ); + // ๋‹ค์ด์–ผ๋กœ๊ทธ ๋‹ซ๊ธฐ onClose(); } catch (error) { - console.error('๊ฑฐ๋ž˜ ๋‚ด์—ญ ์—…๋ฐ์ดํŠธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error); + logger.error("๊ฑฐ๋ž˜ ๋‚ด์—ญ ์—…๋ฐ์ดํŠธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:", error); toast({ title: "๊ฑฐ๋ž˜ ๋‚ด์—ญ ์—…๋ฐ์ดํŠธ ์‹คํŒจ", description: "๋‚ด์—ญ์„ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋„์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" + variant: "destructive", }); } finally { setIsSubmitting(false); @@ -82,30 +84,32 @@ export const useTransactionEdit = ( const handleDelete = async (): Promise => { try { setIsSubmitting(true); - + // ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ deleteTransaction(transaction.id); - + // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ toast({ title: "๊ฑฐ๋ž˜ ๋‚ด์—ญ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค", description: `${transaction.title} ํ•ญ๋ชฉ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, }); - + // ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์ฒ˜๋ฆฌ - window.dispatchEvent(new CustomEvent('transactionChanged', { - detail: { type: 'delete', transaction } - })); - + window.dispatchEvent( + new CustomEvent("transactionChanged", { + detail: { type: "delete", transaction }, + }) + ); + // ๋‹ค์ด์–ผ๋กœ๊ทธ ๋‹ซ๊ธฐ onClose(); return true; } catch (error) { - console.error('๊ฑฐ๋ž˜ ๋‚ด์—ญ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error); + logger.error("๊ฑฐ๋ž˜ ๋‚ด์—ญ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:", error); toast({ title: "๊ฑฐ๋ž˜ ๋‚ด์—ญ ์‚ญ์ œ ์‹คํŒจ", description: "๋‚ด์—ญ์„ ์‚ญ์ œํ•˜๋Š” ๋„์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" + variant: "destructive", }); return false; } finally { @@ -117,6 +121,6 @@ export const useTransactionEdit = ( form, isSubmitting, handleSubmit, - handleDelete + handleDelete, }; }; diff --git a/src/components/transactions/EmptyTransactions.tsx b/src/components/transactions/EmptyTransactions.tsx index eca4186..abca5b5 100644 --- a/src/components/transactions/EmptyTransactions.tsx +++ b/src/components/transactions/EmptyTransactions.tsx @@ -1,5 +1,4 @@ - -import React from 'react'; +import React from "react"; interface EmptyTransactionsProps { searchQuery: string; @@ -12,19 +11,19 @@ const EmptyTransactions: React.FC = ({ searchQuery, selectedMonth, setSearchQuery, - isDisabled + isDisabled, }) => { return (

- {searchQuery.trim() - ? '๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.' + {searchQuery.trim() + ? "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค." : `${selectedMonth}์— ๋“ฑ๋ก๋œ ์ง€์ถœ์ด ์—†์Šต๋‹ˆ๋‹ค.`}

{searchQuery.trim() && ( - - +
{displayMonth}
- -
- + {/* Summary */}
diff --git a/src/components/transactions/TransactionsList.tsx b/src/components/transactions/TransactionsList.tsx index 1e9c09c..69ddcb6 100644 --- a/src/components/transactions/TransactionsList.tsx +++ b/src/components/transactions/TransactionsList.tsx @@ -1,7 +1,6 @@ - -import React from 'react'; -import TransactionCard, { Transaction } from '@/components/TransactionCard'; -import TransactionDateGroup from './TransactionDateGroup'; +import React from "react"; +import TransactionCard, { Transaction } from "@/components/TransactionCard"; +import TransactionDateGroup from "./TransactionDateGroup"; interface TransactionsListProps { groupedTransactions: Record; @@ -10,14 +9,14 @@ interface TransactionsListProps { const TransactionsList: React.FC = ({ groupedTransactions, - onTransactionDelete + onTransactionDelete, }) => { return (
{Object.entries(groupedTransactions).map(([date, dateTransactions]) => ( - diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx index e6a723d..2188736 100644 --- a/src/components/ui/accordion.tsx +++ b/src/components/ui/accordion.tsx @@ -1,10 +1,10 @@ -import * as React from "react" -import * as AccordionPrimitive from "@radix-ui/react-accordion" -import { ChevronDown } from "lucide-react" +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDown } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const Accordion = AccordionPrimitive.Root +const Accordion = AccordionPrimitive.Root; const AccordionItem = React.forwardRef< React.ElementRef, @@ -15,8 +15,8 @@ const AccordionItem = React.forwardRef< className={cn("border-b", className)} {...props} /> -)) -AccordionItem.displayName = "AccordionItem" +)); +AccordionItem.displayName = "AccordionItem"; const AccordionTrigger = React.forwardRef< React.ElementRef, @@ -35,8 +35,8 @@ const AccordionTrigger = React.forwardRef< -)) -AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; const AccordionContent = React.forwardRef< React.ElementRef, @@ -49,8 +49,8 @@ const AccordionContent = React.forwardRef< >
{children}
-)) +)); -AccordionContent.displayName = AccordionPrimitive.Content.displayName +AccordionContent.displayName = AccordionPrimitive.Content.displayName; -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx index 92298f0..143bc53 100644 --- a/src/components/ui/alert-dialog.tsx +++ b/src/components/ui/alert-dialog.tsx @@ -1,15 +1,14 @@ +import * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; -import * as React from "react" -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" +const AlertDialog = AlertDialogPrimitive.Root; -const AlertDialog = AlertDialogPrimitive.Root +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; -const AlertDialogTrigger = AlertDialogPrimitive.Trigger - -const AlertDialogPortal = AlertDialogPrimitive.Portal +const AlertDialogPortal = AlertDialogPrimitive.Portal; const AlertDialogOverlay = React.forwardRef< React.ElementRef, @@ -23,8 +22,8 @@ const AlertDialogOverlay = React.forwardRef< {...props} ref={ref} /> -)) -AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName +)); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; const AlertDialogContent = React.forwardRef< React.ElementRef, @@ -41,8 +40,8 @@ const AlertDialogContent = React.forwardRef< {...props} /> -)) -AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName +)); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; const AlertDialogHeader = ({ className, @@ -55,8 +54,8 @@ const AlertDialogHeader = ({ )} {...props} /> -) -AlertDialogHeader.displayName = "AlertDialogHeader" +); +AlertDialogHeader.displayName = "AlertDialogHeader"; const AlertDialogFooter = ({ className, @@ -69,8 +68,8 @@ const AlertDialogFooter = ({ )} {...props} /> -) -AlertDialogFooter.displayName = "AlertDialogFooter" +); +AlertDialogFooter.displayName = "AlertDialogFooter"; const AlertDialogTitle = React.forwardRef< React.ElementRef, @@ -81,8 +80,8 @@ const AlertDialogTitle = React.forwardRef< className={cn("text-lg font-semibold", className)} {...props} /> -)) -AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; const AlertDialogDescription = React.forwardRef< React.ElementRef, @@ -93,9 +92,9 @@ const AlertDialogDescription = React.forwardRef< className={cn("text-sm text-muted-foreground", className)} {...props} /> -)) +)); AlertDialogDescription.displayName = - AlertDialogPrimitive.Description.displayName + AlertDialogPrimitive.Description.displayName; const AlertDialogAction = React.forwardRef< React.ElementRef, @@ -106,8 +105,8 @@ const AlertDialogAction = React.forwardRef< className={cn(buttonVariants(), className)} {...props} /> -)) -AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName +)); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; const AlertDialogCancel = React.forwardRef< React.ElementRef, @@ -122,8 +121,8 @@ const AlertDialogCancel = React.forwardRef< )} {...props} /> -)) -AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName +)); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; export { AlertDialog, @@ -137,4 +136,4 @@ export { AlertDialogDescription, AlertDialogAction, AlertDialogCancel, -} +}; diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index 41fa7e0..29bd44f 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const alertVariants = cva( "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", @@ -17,7 +17,7 @@ const alertVariants = cva( variant: "default", }, } -) +); const Alert = React.forwardRef< HTMLDivElement, @@ -29,8 +29,8 @@ const Alert = React.forwardRef< className={cn(alertVariants({ variant }), className)} {...props} /> -)) -Alert.displayName = "Alert" +)); +Alert.displayName = "Alert"; const AlertTitle = React.forwardRef< HTMLParagraphElement, @@ -41,8 +41,8 @@ const AlertTitle = React.forwardRef< className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} /> -)) -AlertTitle.displayName = "AlertTitle" +)); +AlertTitle.displayName = "AlertTitle"; const AlertDescription = React.forwardRef< HTMLParagraphElement, @@ -53,7 +53,7 @@ const AlertDescription = React.forwardRef< className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} /> -)) -AlertDescription.displayName = "AlertDescription" +)); +AlertDescription.displayName = "AlertDescription"; -export { Alert, AlertTitle, AlertDescription } +export { Alert, AlertTitle, AlertDescription }; diff --git a/src/components/ui/aspect-ratio.tsx b/src/components/ui/aspect-ratio.tsx index c4abbf3..c9e6f4b 100644 --- a/src/components/ui/aspect-ratio.tsx +++ b/src/components/ui/aspect-ratio.tsx @@ -1,5 +1,5 @@ -import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; -const AspectRatio = AspectRatioPrimitive.Root +const AspectRatio = AspectRatioPrimitive.Root; -export { AspectRatio } +export { AspectRatio }; diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index 991f56e..fda1c3d 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Avatar = React.forwardRef< React.ElementRef, @@ -15,8 +15,8 @@ const Avatar = React.forwardRef< )} {...props} /> -)) -Avatar.displayName = AvatarPrimitive.Root.displayName +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; const AvatarImage = React.forwardRef< React.ElementRef, @@ -27,8 +27,8 @@ const AvatarImage = React.forwardRef< className={cn("aspect-square h-full w-full", className)} {...props} /> -)) -AvatarImage.displayName = AvatarPrimitive.Image.displayName +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; const AvatarFallback = React.forwardRef< React.ElementRef, @@ -42,7 +42,7 @@ const AvatarFallback = React.forwardRef< )} {...props} /> -)) -AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; -export { Avatar, AvatarImage, AvatarFallback } +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index f000e3e..9ec9a1a 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const badgeVariants = cva( "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", @@ -21,7 +21,7 @@ const badgeVariants = cva( variant: "default", }, } -) +); export interface BadgeProps extends React.HTMLAttributes, @@ -30,7 +30,7 @@ export interface BadgeProps function Badge({ className, variant, ...props }: BadgeProps) { return (
- ) + ); } -export { Badge, badgeVariants } +export { Badge, badgeVariants }; diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx index 71a5c32..fad4115 100644 --- a/src/components/ui/breadcrumb.tsx +++ b/src/components/ui/breadcrumb.tsx @@ -1,16 +1,16 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { ChevronRight, MoreHorizontal } from "lucide-react" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Breadcrumb = React.forwardRef< HTMLElement, React.ComponentPropsWithoutRef<"nav"> & { - separator?: React.ReactNode + separator?: React.ReactNode; } ->(({ ...props }, ref) =>
-)) +)); -CommandInput.displayName = CommandPrimitive.Input.displayName +CommandInput.displayName = CommandPrimitive.Input.displayName; const CommandList = React.forwardRef< React.ElementRef, @@ -63,9 +63,9 @@ const CommandList = React.forwardRef< className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)} {...props} /> -)) +)); -CommandList.displayName = CommandPrimitive.List.displayName +CommandList.displayName = CommandPrimitive.List.displayName; const CommandEmpty = React.forwardRef< React.ElementRef, @@ -76,9 +76,9 @@ const CommandEmpty = React.forwardRef< className="py-6 text-center text-sm" {...props} /> -)) +)); -CommandEmpty.displayName = CommandPrimitive.Empty.displayName +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; const CommandGroup = React.forwardRef< React.ElementRef, @@ -92,9 +92,9 @@ const CommandGroup = React.forwardRef< )} {...props} /> -)) +)); -CommandGroup.displayName = CommandPrimitive.Group.displayName +CommandGroup.displayName = CommandPrimitive.Group.displayName; const CommandSeparator = React.forwardRef< React.ElementRef, @@ -105,8 +105,8 @@ const CommandSeparator = React.forwardRef< className={cn("-mx-1 h-px bg-border", className)} {...props} /> -)) -CommandSeparator.displayName = CommandPrimitive.Separator.displayName +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; const CommandItem = React.forwardRef< React.ElementRef, @@ -120,9 +120,9 @@ const CommandItem = React.forwardRef< )} {...props} /> -)) +)); -CommandItem.displayName = CommandPrimitive.Item.displayName +CommandItem.displayName = CommandPrimitive.Item.displayName; const CommandShortcut = ({ className, @@ -136,9 +136,9 @@ const CommandShortcut = ({ )} {...props} /> - ) -} -CommandShortcut.displayName = "CommandShortcut" + ); +}; +CommandShortcut.displayName = "CommandShortcut"; export { Command, @@ -150,4 +150,4 @@ export { CommandItem, CommandShortcut, CommandSeparator, -} +}; diff --git a/src/components/ui/context-menu.tsx b/src/components/ui/context-menu.tsx index 3e52999..ce828dd 100644 --- a/src/components/ui/context-menu.tsx +++ b/src/components/ui/context-menu.tsx @@ -1,25 +1,25 @@ -import * as React from "react" -import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" -import { Check, ChevronRight, Circle } from "lucide-react" +import * as React from "react"; +import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"; +import { Check, ChevronRight, Circle } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const ContextMenu = ContextMenuPrimitive.Root +const ContextMenu = ContextMenuPrimitive.Root; -const ContextMenuTrigger = ContextMenuPrimitive.Trigger +const ContextMenuTrigger = ContextMenuPrimitive.Trigger; -const ContextMenuGroup = ContextMenuPrimitive.Group +const ContextMenuGroup = ContextMenuPrimitive.Group; -const ContextMenuPortal = ContextMenuPrimitive.Portal +const ContextMenuPortal = ContextMenuPrimitive.Portal; -const ContextMenuSub = ContextMenuPrimitive.Sub +const ContextMenuSub = ContextMenuPrimitive.Sub; -const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup; const ContextMenuSubTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, children, ...props }, ref) => ( -)) -ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName +)); +ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName; const ContextMenuSubContent = React.forwardRef< React.ElementRef, @@ -49,8 +49,8 @@ const ContextMenuSubContent = React.forwardRef< )} {...props} /> -)) -ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName +)); +ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName; const ContextMenuContent = React.forwardRef< React.ElementRef, @@ -66,13 +66,13 @@ const ContextMenuContent = React.forwardRef< {...props} /> -)) -ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName +)); +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName; const ContextMenuItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, ...props }, ref) => ( -)) -ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName +)); +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName; const ContextMenuCheckboxItem = React.forwardRef< React.ElementRef, @@ -107,9 +107,9 @@ const ContextMenuCheckboxItem = React.forwardRef< {children} -)) +)); ContextMenuCheckboxItem.displayName = - ContextMenuPrimitive.CheckboxItem.displayName + ContextMenuPrimitive.CheckboxItem.displayName; const ContextMenuRadioItem = React.forwardRef< React.ElementRef, @@ -130,13 +130,13 @@ const ContextMenuRadioItem = React.forwardRef< {children} -)) -ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName +)); +ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName; const ContextMenuLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, ...props }, ref) => ( -)) -ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName +)); +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName; const ContextMenuSeparator = React.forwardRef< React.ElementRef, @@ -160,8 +160,8 @@ const ContextMenuSeparator = React.forwardRef< className={cn("-mx-1 my-1 h-px bg-border", className)} {...props} /> -)) -ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName +)); +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName; const ContextMenuShortcut = ({ className, @@ -175,9 +175,9 @@ const ContextMenuShortcut = ({ )} {...props} /> - ) -} -ContextMenuShortcut.displayName = "ContextMenuShortcut" + ); +}; +ContextMenuShortcut.displayName = "ContextMenuShortcut"; export { ContextMenu, @@ -195,4 +195,4 @@ export { ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuRadioGroup, -} +}; diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index b95111b..9de1f72 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -1,17 +1,16 @@ +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { X } from "lucide-react" +import { cn } from "@/lib/utils"; -import { cn } from "@/lib/utils" +const Dialog = DialogPrimitive.Root; -const Dialog = DialogPrimitive.Root +const DialogTrigger = DialogPrimitive.Trigger; -const DialogTrigger = DialogPrimitive.Trigger +const DialogPortal = DialogPrimitive.Portal; -const DialogPortal = DialogPrimitive.Portal - -const DialogClose = DialogPrimitive.Close +const DialogClose = DialogPrimitive.Close; const DialogOverlay = React.forwardRef< React.ElementRef, @@ -25,8 +24,8 @@ const DialogOverlay = React.forwardRef< )} {...props} /> -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; const DialogContent = React.forwardRef< React.ElementRef, @@ -49,8 +48,8 @@ const DialogContent = React.forwardRef< -)) -DialogContent.displayName = DialogPrimitive.Content.displayName +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; const DialogHeader = ({ className, @@ -63,8 +62,8 @@ const DialogHeader = ({ )} {...props} /> -) -DialogHeader.displayName = "DialogHeader" +); +DialogHeader.displayName = "DialogHeader"; const DialogFooter = ({ className, @@ -77,8 +76,8 @@ const DialogFooter = ({ )} {...props} /> -) -DialogFooter.displayName = "DialogFooter" +); +DialogFooter.displayName = "DialogFooter"; const DialogTitle = React.forwardRef< React.ElementRef, @@ -92,8 +91,8 @@ const DialogTitle = React.forwardRef< )} {...props} /> -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; const DialogDescription = React.forwardRef< React.ElementRef, @@ -104,8 +103,8 @@ const DialogDescription = React.forwardRef< className={cn("text-sm text-muted-foreground", className)} {...props} /> -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; export { Dialog, @@ -118,4 +117,4 @@ export { DialogFooter, DialogTitle, DialogDescription, -} +}; diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx index 8d94790..a8cd174 100644 --- a/src/components/ui/drawer.tsx +++ b/src/components/ui/drawer.tsx @@ -1,8 +1,7 @@ +import * as React from "react"; +import { Drawer as DrawerPrimitive } from "vaul"; -import * as React from "react" -import { Drawer as DrawerPrimitive } from "vaul" - -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Drawer = ({ shouldScaleBackground = true, @@ -12,14 +11,14 @@ const Drawer = ({ shouldScaleBackground={shouldScaleBackground} {...props} /> -) -Drawer.displayName = "Drawer" +); +Drawer.displayName = "Drawer"; -const DrawerTrigger = DrawerPrimitive.Trigger +const DrawerTrigger = DrawerPrimitive.Trigger; -const DrawerPortal = DrawerPrimitive.Portal +const DrawerPortal = DrawerPrimitive.Portal; -const DrawerClose = DrawerPrimitive.Close +const DrawerClose = DrawerPrimitive.Close; const DrawerOverlay = React.forwardRef< React.ElementRef, @@ -30,8 +29,8 @@ const DrawerOverlay = React.forwardRef< className={cn("fixed inset-0 z-50 bg-black/80", className)} {...props} /> -)) -DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName +)); +DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; const DrawerContent = React.forwardRef< React.ElementRef, @@ -51,8 +50,8 @@ const DrawerContent = React.forwardRef< {children} -)) -DrawerContent.displayName = "DrawerContent" +)); +DrawerContent.displayName = "DrawerContent"; const DrawerHeader = ({ className, @@ -62,8 +61,8 @@ const DrawerHeader = ({ className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} {...props} /> -) -DrawerHeader.displayName = "DrawerHeader" +); +DrawerHeader.displayName = "DrawerHeader"; const DrawerFooter = ({ className, @@ -73,8 +72,8 @@ const DrawerFooter = ({ className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} /> -) -DrawerFooter.displayName = "DrawerFooter" +); +DrawerFooter.displayName = "DrawerFooter"; const DrawerTitle = React.forwardRef< React.ElementRef, @@ -88,8 +87,8 @@ const DrawerTitle = React.forwardRef< )} {...props} /> -)) -DrawerTitle.displayName = DrawerPrimitive.Title.displayName +)); +DrawerTitle.displayName = DrawerPrimitive.Title.displayName; const DrawerDescription = React.forwardRef< React.ElementRef, @@ -100,8 +99,8 @@ const DrawerDescription = React.forwardRef< className={cn("text-sm text-muted-foreground", className)} {...props} /> -)) -DrawerDescription.displayName = DrawerPrimitive.Description.displayName +)); +DrawerDescription.displayName = DrawerPrimitive.Description.displayName; export { Drawer, @@ -114,4 +113,4 @@ export { DrawerFooter, DrawerTitle, DrawerDescription, -} +}; diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index 769ff7a..5c1e59d 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -1,25 +1,25 @@ -import * as React from "react" -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" -import { Check, ChevronRight, Circle } from "lucide-react" +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { Check, ChevronRight, Circle } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const DropdownMenu = DropdownMenuPrimitive.Root +const DropdownMenu = DropdownMenuPrimitive.Root; -const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; -const DropdownMenuGroup = DropdownMenuPrimitive.Group +const DropdownMenuGroup = DropdownMenuPrimitive.Group; -const DropdownMenuPortal = DropdownMenuPrimitive.Portal +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; -const DropdownMenuSub = DropdownMenuPrimitive.Sub +const DropdownMenuSub = DropdownMenuPrimitive.Sub; -const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; const DropdownMenuSubTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, children, ...props }, ref) => ( -)) +)); DropdownMenuSubTrigger.displayName = - DropdownMenuPrimitive.SubTrigger.displayName + DropdownMenuPrimitive.SubTrigger.displayName; const DropdownMenuSubContent = React.forwardRef< React.ElementRef, @@ -50,9 +50,9 @@ const DropdownMenuSubContent = React.forwardRef< )} {...props} /> -)) +)); DropdownMenuSubContent.displayName = - DropdownMenuPrimitive.SubContent.displayName + DropdownMenuPrimitive.SubContent.displayName; const DropdownMenuContent = React.forwardRef< React.ElementRef, @@ -69,13 +69,13 @@ const DropdownMenuContent = React.forwardRef< {...props} /> -)) -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; const DropdownMenuItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, ...props }, ref) => ( -)) -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; const DropdownMenuCheckboxItem = React.forwardRef< React.ElementRef, @@ -110,9 +110,9 @@ const DropdownMenuCheckboxItem = React.forwardRef< {children} -)) +)); DropdownMenuCheckboxItem.displayName = - DropdownMenuPrimitive.CheckboxItem.displayName + DropdownMenuPrimitive.CheckboxItem.displayName; const DropdownMenuRadioItem = React.forwardRef< React.ElementRef, @@ -133,13 +133,13 @@ const DropdownMenuRadioItem = React.forwardRef< {children} -)) -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; const DropdownMenuLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, ...props }, ref) => ( -)) -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; const DropdownMenuSeparator = React.forwardRef< React.ElementRef, @@ -163,8 +163,8 @@ const DropdownMenuSeparator = React.forwardRef< className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} /> -)) -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; const DropdownMenuShortcut = ({ className, @@ -175,9 +175,9 @@ const DropdownMenuShortcut = ({ className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} /> - ) -} -DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + ); +}; +DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; export { DropdownMenu, @@ -195,4 +195,4 @@ export { DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuRadioGroup, -} +}; diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx index 4603f8b..1f4d5f5 100644 --- a/src/components/ui/form.tsx +++ b/src/components/ui/form.tsx @@ -1,6 +1,6 @@ -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" -import { Slot } from "@radix-ui/react-slot" +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { Slot } from "@radix-ui/react-slot"; import { Controller, ControllerProps, @@ -8,27 +8,27 @@ import { FieldValues, FormProvider, useFormContext, -} from "react-hook-form" +} from "react-hook-form"; -import { cn } from "@/lib/utils" -import { Label } from "@/components/ui/label" +import { cn } from "@/lib/utils"; +import { Label } from "@/components/ui/label"; -const Form = FormProvider +const Form = FormProvider; type FormFieldContextValue< TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TName extends FieldPath = FieldPath, > = { - name: TName -} + name: TName; +}; const FormFieldContext = React.createContext( {} as FormFieldContextValue -) +); const FormField = < TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TName extends FieldPath = FieldPath, >({ ...props }: ControllerProps) => { @@ -36,21 +36,21 @@ const FormField = < - ) -} + ); +}; const useFormField = () => { - const fieldContext = React.useContext(FormFieldContext) - const itemContext = React.useContext(FormItemContext) - const { getFieldState, formState } = useFormContext() + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); - const fieldState = getFieldState(fieldContext.name, formState) + const fieldState = getFieldState(fieldContext.name, formState); if (!fieldContext) { - throw new Error("useFormField should be used within ") + throw new Error("useFormField should be used within "); } - const { id } = itemContext + const { id } = itemContext; return { id, @@ -59,36 +59,36 @@ const useFormField = () => { formDescriptionId: `${id}-form-item-description`, formMessageId: `${id}-form-item-message`, ...fieldState, - } -} + }; +}; type FormItemContextValue = { - id: string -} + id: string; +}; const FormItemContext = React.createContext( {} as FormItemContextValue -) +); const FormItem = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => { - const id = React.useId() + const id = React.useId(); return (
- ) -}) -FormItem.displayName = "FormItem" + ); +}); +FormItem.displayName = "FormItem"; const FormLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => { - const { error, formItemId } = useFormField() + const { error, formItemId } = useFormField(); return (
- ) + ); } if (isMobile) { @@ -207,7 +207,7 @@ const Sidebar = React.forwardRef<
{children}
- ) + ); } return ( @@ -252,16 +252,16 @@ const Sidebar = React.forwardRef<
- ) + ); } -) -Sidebar.displayName = "Sidebar" +); +Sidebar.displayName = "Sidebar"; const SidebarTrigger = React.forwardRef< React.ElementRef, React.ComponentProps >(({ className, onClick, ...props }, ref) => { - const { toggleSidebar } = useSidebar() + const { toggleSidebar } = useSidebar(); return ( - ) -}) -SidebarTrigger.displayName = "SidebarTrigger" + ); +}); +SidebarTrigger.displayName = "SidebarTrigger"; const SidebarRail = React.forwardRef< HTMLButtonElement, React.ComponentProps<"button"> >(({ className, ...props }, ref) => { - const { toggleSidebar } = useSidebar() + const { toggleSidebar } = useSidebar(); return (
-)) -Table.displayName = "Table" +)); +Table.displayName = "Table"; const TableHeader = React.forwardRef< HTMLTableSectionElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -)) -TableHeader.displayName = "TableHeader" +)); +TableHeader.displayName = "TableHeader"; const TableBody = React.forwardRef< HTMLTableSectionElement, @@ -33,8 +33,8 @@ const TableBody = React.forwardRef< className={cn("[&_tr:last-child]:border-0", className)} {...props} /> -)) -TableBody.displayName = "TableBody" +)); +TableBody.displayName = "TableBody"; const TableFooter = React.forwardRef< HTMLTableSectionElement, @@ -48,8 +48,8 @@ const TableFooter = React.forwardRef< )} {...props} /> -)) -TableFooter.displayName = "TableFooter" +)); +TableFooter.displayName = "TableFooter"; const TableRow = React.forwardRef< HTMLTableRowElement, @@ -63,8 +63,8 @@ const TableRow = React.forwardRef< )} {...props} /> -)) -TableRow.displayName = "TableRow" +)); +TableRow.displayName = "TableRow"; const TableHead = React.forwardRef< HTMLTableCellElement, @@ -78,8 +78,8 @@ const TableHead = React.forwardRef< )} {...props} /> -)) -TableHead.displayName = "TableHead" +)); +TableHead.displayName = "TableHead"; const TableCell = React.forwardRef< HTMLTableCellElement, @@ -90,8 +90,8 @@ const TableCell = React.forwardRef< className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} /> -)) -TableCell.displayName = "TableCell" +)); +TableCell.displayName = "TableCell"; const TableCaption = React.forwardRef< HTMLTableCaptionElement, @@ -102,8 +102,8 @@ const TableCaption = React.forwardRef< className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} /> -)) -TableCaption.displayName = "TableCaption" +)); +TableCaption.displayName = "TableCaption"; export { Table, @@ -114,4 +114,4 @@ export { TableRow, TableCell, TableCaption, -} +}; diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index 4bdcf53..cea144e 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -1,10 +1,9 @@ +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; -import * as React from "react" -import * as TabsPrimitive from "@radix-ui/react-tabs" +import { cn } from "@/lib/utils"; -import { cn } from "@/lib/utils" - -const Tabs = TabsPrimitive.Root +const Tabs = TabsPrimitive.Root; const TabsList = React.forwardRef< React.ElementRef, @@ -18,8 +17,8 @@ const TabsList = React.forwardRef< )} {...props} /> -)) -TabsList.displayName = TabsPrimitive.List.displayName +)); +TabsList.displayName = TabsPrimitive.List.displayName; const TabsTrigger = React.forwardRef< React.ElementRef, @@ -33,8 +32,8 @@ const TabsTrigger = React.forwardRef< )} {...props} /> -)) -TabsTrigger.displayName = TabsPrimitive.Trigger.displayName +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; const TabsContent = React.forwardRef< React.ElementRef, @@ -48,7 +47,7 @@ const TabsContent = React.forwardRef< )} {...props} /> -)) -TabsContent.displayName = TabsPrimitive.Content.displayName +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; -export { Tabs, TabsList, TabsTrigger, TabsContent } +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx index 9f9a6dc..0f3eac6 100644 --- a/src/components/ui/textarea.tsx +++ b/src/components/ui/textarea.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; export interface TextareaProps extends React.TextareaHTMLAttributes {} @@ -16,9 +16,9 @@ const Textarea = React.forwardRef( ref={ref} {...props} /> - ) + ); } -) -Textarea.displayName = "Textarea" +); +Textarea.displayName = "Textarea"; -export { Textarea } +export { Textarea }; diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx index 5d47d4a..7f4b425 100644 --- a/src/components/ui/toast.tsx +++ b/src/components/ui/toast.tsx @@ -1,12 +1,11 @@ +import * as React from "react"; +import * as ToastPrimitives from "@radix-ui/react-toast"; +import { cva, type VariantProps } from "class-variance-authority"; +import { X } from "lucide-react"; -import * as React from "react" -import * as ToastPrimitives from "@radix-ui/react-toast" -import { cva, type VariantProps } from "class-variance-authority" -import { X } from "lucide-react" +import { cn } from "@/lib/utils"; -import { cn } from "@/lib/utils" - -const ToastProvider = ToastPrimitives.Provider +const ToastProvider = ToastPrimitives.Provider; const ToastViewport = React.forwardRef< React.ElementRef, @@ -20,8 +19,8 @@ const ToastViewport = React.forwardRef< )} {...props} /> -)) -ToastViewport.displayName = ToastPrimitives.Viewport.displayName +)); +ToastViewport.displayName = ToastPrimitives.Viewport.displayName; const toastVariants = cva( "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", @@ -37,7 +36,7 @@ const toastVariants = cva( variant: "default", }, } -) +); const Toast = React.forwardRef< React.ElementRef, @@ -50,9 +49,9 @@ const Toast = React.forwardRef< className={cn(toastVariants({ variant }), className)} {...props} /> - ) -}) -Toast.displayName = ToastPrimitives.Root.displayName + ); +}); +Toast.displayName = ToastPrimitives.Root.displayName; const ToastAction = React.forwardRef< React.ElementRef, @@ -66,8 +65,8 @@ const ToastAction = React.forwardRef< )} {...props} /> -)) -ToastAction.displayName = ToastPrimitives.Action.displayName +)); +ToastAction.displayName = ToastPrimitives.Action.displayName; const ToastClose = React.forwardRef< React.ElementRef, @@ -84,8 +83,8 @@ const ToastClose = React.forwardRef< > -)) -ToastClose.displayName = ToastPrimitives.Close.displayName +)); +ToastClose.displayName = ToastPrimitives.Close.displayName; const ToastTitle = React.forwardRef< React.ElementRef, @@ -96,8 +95,8 @@ const ToastTitle = React.forwardRef< className={cn("text-sm font-semibold text-center", className)} {...props} /> -)) -ToastTitle.displayName = ToastPrimitives.Title.displayName +)); +ToastTitle.displayName = ToastPrimitives.Title.displayName; const ToastDescription = React.forwardRef< React.ElementRef, @@ -108,12 +107,12 @@ const ToastDescription = React.forwardRef< className={cn("text-sm opacity-90 text-center", className)} {...props} /> -)) -ToastDescription.displayName = ToastPrimitives.Description.displayName +)); +ToastDescription.displayName = ToastPrimitives.Description.displayName; -type ToastProps = React.ComponentPropsWithoutRef +type ToastProps = React.ComponentPropsWithoutRef; -type ToastActionElement = React.ReactElement +type ToastActionElement = React.ReactElement; export { type ToastProps, @@ -125,4 +124,4 @@ export { ToastDescription, ToastClose, ToastAction, -} +}; diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx index 628ec3c..d6d8556 100644 --- a/src/components/ui/toaster.tsx +++ b/src/components/ui/toaster.tsx @@ -1,5 +1,4 @@ - -import { useToast } from "@/hooks/toast" +import { useToast } from "@/hooks/toast"; import { Toast, ToastClose, @@ -7,10 +6,10 @@ import { ToastProvider, ToastTitle, ToastViewport, -} from "@/components/ui/toast" +} from "@/components/ui/toast"; export function Toaster() { - const { toasts } = useToast() + const { toasts } = useToast(); return ( @@ -26,9 +25,9 @@ export function Toaster() { {action} - ) + ); })} - ) + ); } diff --git a/src/components/ui/toggle-group.tsx b/src/components/ui/toggle-group.tsx index afe5da6..4feb5b7 100644 --- a/src/components/ui/toggle-group.tsx +++ b/src/components/ui/toggle-group.tsx @@ -1,16 +1,16 @@ -import * as React from "react" -import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group" -import { type VariantProps } from "class-variance-authority" +import * as React from "react"; +import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"; +import { type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" -import { toggleVariants } from "@/components/ui/toggle" +import { cn } from "@/lib/utils"; +import { toggleVariants } from "@/components/ui/toggle"; const ToggleGroupContext = React.createContext< VariantProps >({ size: "default", variant: "default", -}) +}); const ToggleGroup = React.forwardRef< React.ElementRef, @@ -26,16 +26,16 @@ const ToggleGroup = React.forwardRef< {children} -)) +)); -ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName +ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName; const ToggleGroupItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & VariantProps >(({ className, children, variant, size, ...props }, ref) => { - const context = React.useContext(ToggleGroupContext) + const context = React.useContext(ToggleGroupContext); return ( {children} - ) -}) + ); +}); -ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName +ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName; -export { ToggleGroup, ToggleGroupItem } +export { ToggleGroup, ToggleGroupItem }; diff --git a/src/components/ui/toggle.tsx b/src/components/ui/toggle.tsx index 9ecac28..56453ca 100644 --- a/src/components/ui/toggle.tsx +++ b/src/components/ui/toggle.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import * as TogglePrimitive from "@radix-ui/react-toggle" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import * as TogglePrimitive from "@radix-ui/react-toggle"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const toggleVariants = cva( "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground", @@ -24,7 +24,7 @@ const toggleVariants = cva( size: "default", }, } -) +); const Toggle = React.forwardRef< React.ElementRef, @@ -36,8 +36,8 @@ const Toggle = React.forwardRef< className={cn(toggleVariants({ variant, size, className }))} {...props} /> -)) +)); -Toggle.displayName = TogglePrimitive.Root.displayName +Toggle.displayName = TogglePrimitive.Root.displayName; -export { Toggle, toggleVariants } +export { Toggle, toggleVariants }; diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx index e121f0a..13a0543 100644 --- a/src/components/ui/tooltip.tsx +++ b/src/components/ui/tooltip.tsx @@ -1,13 +1,13 @@ -import * as React from "react" -import * as TooltipPrimitive from "@radix-ui/react-tooltip" +import * as React from "react"; +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const TooltipProvider = TooltipPrimitive.Provider +const TooltipProvider = TooltipPrimitive.Provider; -const Tooltip = TooltipPrimitive.Root +const Tooltip = TooltipPrimitive.Root; -const TooltipTrigger = TooltipPrimitive.Trigger +const TooltipTrigger = TooltipPrimitive.Trigger; const TooltipContent = React.forwardRef< React.ElementRef, @@ -22,7 +22,7 @@ const TooltipContent = React.forwardRef< )} {...props} /> -)) -TooltipContent.displayName = TooltipPrimitive.Content.displayName +)); +TooltipContent.displayName = TooltipPrimitive.Content.displayName; -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/src/components/ui/use-toast.ts b/src/components/ui/use-toast.ts index f25c9a3..9349b87 100644 --- a/src/components/ui/use-toast.ts +++ b/src/components/ui/use-toast.ts @@ -1,4 +1,3 @@ - // Shadcn UI์˜ ์ตœ์‹  ๋ฒ„์ „์—์„œ๋Š” use-toast๊ฐ€ hooks ํด๋”๋กœ ์ด๋™ํ–ˆ์Šต๋‹ˆ๋‹ค. // ์ด ํŒŒ์ผ์€ ๊ธฐ์กด import ๊ฒฝ๋กœ๊ฐ€ ์ž‘๋™ํ•˜๋„๋ก ๋ฆฌ๋””๋ ‰์…˜ํ•ฉ๋‹ˆ๋‹ค. import { useToast, toast } from "@/hooks/toast"; diff --git a/src/constants/categoryIcons.tsx b/src/constants/categoryIcons.tsx index 50598c2..978e12b 100644 --- a/src/constants/categoryIcons.tsx +++ b/src/constants/categoryIcons.tsx @@ -1,46 +1,46 @@ - -import React from 'react'; -import { ShoppingBag, Coffee, Bus, Landmark, MoreHorizontal } from 'lucide-react'; +import React from "react"; +import { + ShoppingBag, + Coffee, + Bus, + Landmark, + MoreHorizontal, +} from "lucide-react"; // ์ง€์ถœ ์นดํ…Œ๊ณ ๋ฆฌ ์ •์˜ - ๊ตํ†ต๋น„๋ฅผ ๊ตํ†ต์œผ๋กœ ํ†ต์ผ -export const EXPENSE_CATEGORIES = [ - '์Œ์‹', - '์‡ผํ•‘', - '๊ตํ†ต', - '๊ธฐํƒ€' -]; +export const EXPENSE_CATEGORIES = ["์Œ์‹", "์‡ผํ•‘", "๊ตํ†ต", "๊ธฐํƒ€"]; // ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์•„์ด์ฝ˜ ๋งคํ•‘ export const categoryIcons: Record = { - '์Œ์‹': , - '์‡ผํ•‘': , - '๊ตํ†ต': , - '๊ธฐํƒ€': , - '์ˆ˜์ž…': + ์Œ์‹: , + ์‡ผํ•‘: , + ๊ตํ†ต: , + ๊ธฐํƒ€: , + ์ˆ˜์ž…: , }; // ๊ธฐ๋ณธ ์นดํ…Œ๊ณ ๋ฆฌ ์„ค์ • (์‹ ๊ทœ ์‚ฌ์šฉ์ž์šฉ) export const DEFAULT_CATEGORIES = { - '์Œ์‹': 0, - '์‡ผํ•‘': 0, - '๊ตํ†ต': 0, - '๊ธฐํƒ€': 0 + ์Œ์‹: 0, + ์‡ผํ•‘: 0, + ๊ตํ†ต: 0, + ๊ธฐํƒ€: 0, }; // ์นดํ…Œ๊ณ ๋ฆฌ ์„ค๋ช… ์ถ”๊ฐ€ export const CATEGORY_DESCRIPTIONS: Record = { - '์Œ์‹': '์‹๋น„, ์นดํŽ˜, ์™ธ์‹', - '์‡ผํ•‘': '์˜๋ฅ˜, ๊ฐ€์ „, ์ƒํ™œ์šฉํ’ˆ', - '๊ตํ†ต': '๋Œ€์ค‘๊ตํ†ต, ํƒ์‹œ, ์ฃผ์œ ๋น„', - '๊ธฐํƒ€': '๊ธฐํƒ€ ์ง€์ถœ', - '์ˆ˜์ž…': '๊ธ‰์—ฌ, ์šฉ๋ˆ, ๊ธฐํƒ€ ์ˆ˜์ž…' + ์Œ์‹: "์‹๋น„, ์นดํŽ˜, ์™ธ์‹", + ์‡ผํ•‘: "์˜๋ฅ˜, ๊ฐ€์ „, ์ƒํ™œ์šฉํ’ˆ", + ๊ตํ†ต: "๋Œ€์ค‘๊ตํ†ต, ํƒ์‹œ, ์ฃผ์œ ๋น„", + ๊ธฐํƒ€: "๊ธฐํƒ€ ์ง€์ถœ", + ์ˆ˜์ž…: "๊ธ‰์—ฌ, ์šฉ๋ˆ, ๊ธฐํƒ€ ์ˆ˜์ž…", }; // ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ œ๋ชฉ ์ถ”์ฒœ export const CATEGORY_TITLE_SUGGESTIONS: Record = { - '์Œ์‹': ['์ ์‹ฌ์‹์‚ฌ', '์ €๋…์‹์‚ฌ', '์นดํŽ˜', '๊ฐ„์‹', '์•„์นจ์‹์‚ฌ', 'ํšŒ์‹'], - '์‡ผํ•‘': ['์˜๋ฅ˜', '์ƒํ™œ์šฉํ’ˆ', '๊ฐ€์ „์ œํ’ˆ', 'ํ™”์žฅํ’ˆ', '์„ ๋ฌผ', '์˜จ๋ผ์ธ์‡ผํ•‘'], - '๊ตํ†ต': ['๋Œ€์ค‘๊ตํ†ต', 'ํƒ์‹œ', '์ฃผ์œ ', '์ฃผ์ฐจ๋น„', '๊ณ ์†๋„๋กœ ํ†ตํ–‰๋ฃŒ'], - '๊ธฐํƒ€': ['์˜๋ฃŒ๋น„', 'ํ†ต์‹ ๋น„', '๊ต์œก๋น„', '์ทจ๋ฏธํ™œ๋™', '๋ฌธํ™”์ƒํ™œ', '๊ธฐ๋ถ€๊ธˆ'], - '์ˆ˜์ž…': ['๊ธ‰์—ฌ', '๋ณด๋„ˆ์Šค', '์šฉ๋ˆ', '๋ถ€์ˆ˜์ž…', 'ํ™˜๊ธ‰๊ธˆ'] + ์Œ์‹: ["์ ์‹ฌ์‹์‚ฌ", "์ €๋…์‹์‚ฌ", "์นดํŽ˜", "๊ฐ„์‹", "์•„์นจ์‹์‚ฌ", "ํšŒ์‹"], + ์‡ผํ•‘: ["์˜๋ฅ˜", "์ƒํ™œ์šฉํ’ˆ", "๊ฐ€์ „์ œํ’ˆ", "ํ™”์žฅํ’ˆ", "์„ ๋ฌผ", "์˜จ๋ผ์ธ์‡ผํ•‘"], + ๊ตํ†ต: ["๋Œ€์ค‘๊ตํ†ต", "ํƒ์‹œ", "์ฃผ์œ ", "์ฃผ์ฐจ๋น„", "๊ณ ์†๋„๋กœ ํ†ตํ–‰๋ฃŒ"], + ๊ธฐํƒ€: ["์˜๋ฃŒ๋น„", "ํ†ต์‹ ๋น„", "๊ต์œก๋น„", "์ทจ๋ฏธํ™œ๋™", "๋ฌธํ™”์ƒํ™œ", "๊ธฐ๋ถ€๊ธˆ"], + ์ˆ˜์ž…: ["๊ธ‰์—ฌ", "๋ณด๋„ˆ์Šค", "์šฉ๋ˆ", "๋ถ€์ˆ˜์ž…", "ํ™˜๊ธ‰๊ธˆ"], }; diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 95769a6..54f4bb3 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -1,10 +1,13 @@ +import React from "react"; +import { AuthProvider } from "./auth/AuthProvider"; -import React from 'react'; -import { AuthProvider } from './auth/AuthProvider'; +export { AuthProvider } from "./auth/AuthProvider"; +export { useAuth } from "./auth/useAuth"; -export { AuthProvider } from './auth/AuthProvider'; -export { useAuth } from './auth/useAuth'; - -export default function AuthContextWrapper({ children }: { children: React.ReactNode }) { +export default function AuthContextWrapper({ + children, +}: { + children: React.ReactNode; +}) { return {children}; } diff --git a/src/contexts/auth/AuthContext.tsx b/src/contexts/auth/AuthContext.tsx index 1049bb8..451f38a 100644 --- a/src/contexts/auth/AuthContext.tsx +++ b/src/contexts/auth/AuthContext.tsx @@ -1,6 +1,7 @@ - -import React, { createContext } from 'react'; -import { AuthContextType } from './types'; +import React, { createContext } from "react"; +import { AuthContextType } from "./types"; // AuthContext ์ƒ์„ฑ -export const AuthContext = createContext(undefined); +export const AuthContext = createContext( + undefined +); diff --git a/src/contexts/auth/AuthProvider.tsx b/src/contexts/auth/AuthProvider.tsx index ad9eb5b..33b7de4 100644 --- a/src/contexts/auth/AuthProvider.tsx +++ b/src/contexts/auth/AuthProvider.tsx @@ -1,55 +1,71 @@ +import React, { useEffect, useState, useCallback } from "react"; +import { authLogger } from "@/utils/logger"; +import { toast } from "@/hooks/useToast.wrapper"; +import { AuthContextType } from "./types"; +import * as authActions from "./authActions"; +import { clearAllToasts } from "@/hooks/toast/toastManager"; +import { AuthContext } from "./AuthContext"; +import { + account, + getInitializationStatus, + reinitializeAppwriteClient, + isValidConnection, +} from "@/lib/appwrite/client"; +import { Models } from "appwrite"; +import { getDefaultUserId } from "@/lib/appwrite/defaultUser"; -import React, { useEffect, useState, useCallback } from 'react'; -import { toast } from '@/hooks/useToast.wrapper'; -import { AuthContextType } from './types'; -import * as authActions from './authActions'; -import { clearAllToasts } from '@/hooks/toast/toastManager'; -import { AuthContext } from './AuthContext'; -import { account, getInitializationStatus, reinitializeAppwriteClient, isValidConnection } from '@/lib/appwrite/client'; -import { Models } from 'appwrite'; -import { getDefaultUserId } from '@/lib/appwrite/defaultUser'; - -export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { const [session, setSession] = useState(null); - const [user, setUser] = useState | null>(null); + const [user, setUser] = useState | null>( + null + ); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [appwriteInitialized, setAppwriteInitialized] = useState(false); - + const [appwriteInitialized, setAppwriteInitialized] = + useState(false); + // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋Š” ํ•จ์ˆ˜ const handleAuthError = useCallback((err: any) => { - console.error('์ธ์ฆ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', err); + authLogger.error("์ธ์ฆ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:", err); setError(err instanceof Error ? err : new Error(String(err))); // ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ๋กœ๋”ฉ ์ƒํƒœ๋Š” ํ•ด์ œํ•˜์—ฌ UI๊ฐ€ ์ฐจ๋‹จ๋˜์ง€ ์•Š๋„๋ก ํ•จ setLoading(false); }, []); - + // Appwrite ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ™•์ธ const checkAppwriteInitialization = useCallback(async () => { try { const status = getInitializationStatus(); - console.log('Appwrite ์ดˆ๊ธฐํ™” ์ƒํƒœ:', status.isInitialized ? '์„ฑ๊ณต' : '์‹คํŒจ'); - + authLogger.info( + "Appwrite ์ดˆ๊ธฐํ™” ์ƒํƒœ:", + status.isInitialized ? "์„ฑ๊ณต" : "์‹คํŒจ" + ); + if (!status.isInitialized) { // ์ดˆ๊ธฐํ™” ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ - console.log('Appwrite ์ดˆ๊ธฐํ™” ์žฌ์‹œ๋„ ์ค‘...'); + authLogger.info("Appwrite ์ดˆ๊ธฐํ™” ์žฌ์‹œ๋„ ์ค‘..."); const retryStatus = reinitializeAppwriteClient(); setAppwriteInitialized(retryStatus.isInitialized); - + if (!retryStatus.isInitialized && retryStatus.error) { handleAuthError(retryStatus.error); } } else { setAppwriteInitialized(true); } - + // ์—ฐ๊ฒฐ ์ƒํƒœ ํ™•์ธ const connectionValid = await isValidConnection(); - console.log('Appwrite ์—ฐ๊ฒฐ ์ƒํƒœ:', connectionValid ? '์ •์ƒ' : '์—ฐ๊ฒฐ ๋ฌธ์ œ'); - + authLogger.info( + "Appwrite ์—ฐ๊ฒฐ ์ƒํƒœ:", + connectionValid ? "์ •์ƒ" : "์—ฐ๊ฒฐ ๋ฌธ์ œ" + ); + return status.isInitialized; } catch (error) { - console.error('Appwrite ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:', error); + authLogger.error("Appwrite ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:", error); handleAuthError(error); return false; } @@ -59,15 +75,17 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children // ํ˜„์žฌ ์„ธ์…˜ ์ฒดํฌ - ์ตœ์ ํ™”๋œ ๋ฒ„์ „ const getSession = async () => { try { - console.log('์„ธ์…˜ ๋กœ๋”ฉ ์‹œ์ž‘'); - + authLogger.info("์„ธ์…˜ ๋กœ๋”ฉ ์‹œ์ž‘"); + // ๋น„๋™๊ธฐ ์ž‘์—…์„ ๋งˆ์ดํฌ๋กœํƒœ์Šคํฌ๋กœ ์ง€์—ฐํ•˜์—ฌ UI ์ฐจ๋‹จ ๋ฐฉ์ง€ - await new Promise(resolve => queueMicrotask(() => resolve())); - + await new Promise((resolve) => queueMicrotask(() => resolve())); + // Appwrite ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ™•์ธ const isInitialized = await checkAppwriteInitialization(); if (!isInitialized) { - console.warn('Appwrite ์ดˆ๊ธฐํ™” ์ƒํƒœ๊ฐ€ ์ •์ƒ์ ์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋น„๋กœ๊ทธ์ธ ์ƒํƒœ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.'); + authLogger.warn( + "Appwrite ์ดˆ๊ธฐํ™” ์ƒํƒœ๊ฐ€ ์ •์ƒ์ ์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋น„๋กœ๊ทธ์ธ ์ƒํƒœ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค." + ); queueMicrotask(() => { setSession(null); setUser(null); @@ -75,46 +93,50 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }); return; } - + // ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹œ๋„ - ์•ˆ์ „ํ•œ ๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌ try { // ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹œ๋„ - const currentUser = await account.get().catch(err => { + const currentUser = await account.get().catch((err) => { // 401 ์˜ค๋ฅ˜๋Š” ๋น„๋กœ๊ทธ์ธ ์ƒํƒœ๋กœ ์ •์ƒ์ ์ธ ๊ฒฝ์šฐ if (err && (err as any).code === 401) { - console.log('์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ, ๋น„๋กœ๊ทธ์ธ ์ƒํƒœ๋กœ ๊ฐ„์ฃผ'); + authLogger.info( + "์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ, ๋น„๋กœ๊ทธ์ธ ์ƒํƒœ๋กœ ๊ฐ„์ฃผ" + ); } else { - console.error('์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์˜ค๋ฅ˜:', err); + authLogger.error("์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์˜ค๋ฅ˜:", err); } return null; }); - + if (currentUser) { // ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์žˆ์œผ๋ฉด ์„ธ์…˜ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹œ๋„ - const currentSession = await account.getSession('current').catch(err => { - console.log('์„ธ์…˜ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ:', err); - return null; - }); - + const currentSession = await account + .getSession("current") + .catch((err) => { + authLogger.info("์„ธ์…˜ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ:", err); + return null; + }); + // ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ ๋งˆ์ดํฌ๋กœํƒœ์Šคํฌ๋กœ ์ง€์—ฐ queueMicrotask(() => { setUser(currentUser); setSession(currentSession); - console.log('์„ธ์…˜ ๋กœ๋”ฉ ์™„๋ฃŒ - ์‚ฌ์šฉ์ž:', currentUser.$id); + authLogger.info("์„ธ์…˜ ๋กœ๋”ฉ ์™„๋ฃŒ - ์‚ฌ์šฉ์ž:", currentUser.$id); }); } else { // ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์—†์œผ๋ฉด ๋น„๋กœ๊ทธ์ธ ์ƒํƒœ๋กœ ์ฒ˜๋ฆฌ queueMicrotask(() => { setSession(null); setUser(null); - console.log('๋น„๋กœ๊ทธ์ธ ์ƒํƒœ๋กœ ์ฒ˜๋ฆฌ'); + authLogger.info("๋น„๋กœ๊ทธ์ธ ์ƒํƒœ๋กœ ์ฒ˜๋ฆฌ"); }); } } catch (error) { // ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ - console.error('์„ธ์…˜ ์ฒ˜๋ฆฌ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜:', error); + authLogger.error("์„ธ์…˜ ์ฒ˜๋ฆฌ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜:", error); handleAuthError(error); - + // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ๋กœ๊ทธ์•„์›ƒ ์ƒํƒœ๋กœ ์ฒ˜๋ฆฌ queueMicrotask(() => { setSession(null); @@ -123,7 +145,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children } } catch (error) { // ์ตœ์ƒ์œ„ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ - console.error('์„ธ์…˜ ํ™•์ธ ์ค‘ ์ตœ์ƒ์œ„ ์˜ˆ์™ธ ๋ฐœ์ƒ:', error); + authLogger.error("์„ธ์…˜ ํ™•์ธ ์ค‘ ์ตœ์ƒ์œ„ ์˜ˆ์™ธ ๋ฐœ์ƒ:", error); handleAuthError(error); } finally { // ๋กœ๋”ฉ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ ๋งˆ์ดํฌ๋กœํƒœ์Šคํฌ๋กœ ์ง€์—ฐ @@ -147,42 +169,49 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children if (!appwriteInitialized) { const isInitialized = await checkAppwriteInitialization(); if (!isInitialized) { - console.warn('Appwrite ์ดˆ๊ธฐํ™” ์ƒํƒœ๊ฐ€ ์—ฌ์ „ํžˆ ์ •์ƒ์ ์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ๊ฐ„๊ฒฉ์—์„œ ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.'); + authLogger.warn( + "Appwrite ์ดˆ๊ธฐํ™” ์ƒํƒœ๊ฐ€ ์—ฌ์ „ํžˆ ์ •์ƒ์ ์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ๊ฐ„๊ฒฉ์—์„œ ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค." + ); return; } } - + // ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹œ๋„ - ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ - const currentUser = await account.get().catch(err => { + const currentUser = await account.get().catch((err) => { // 401 ์˜ค๋ฅ˜๋Š” ๋น„๋กœ๊ทธ์ธ ์ƒํƒœ๋กœ ์ •์ƒ์ ์ธ ๊ฒฝ์šฐ if (err && (err as any).code === 401) { - console.log('์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ, ๋น„๋กœ๊ทธ์ธ ์ƒํƒœ๋กœ ๊ฐ„์ฃผ'); + authLogger.info("์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ, ๋น„๋กœ๊ทธ์ธ ์ƒํƒœ๋กœ ๊ฐ„์ฃผ"); } else { - console.error('์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์˜ค๋ฅ˜:', err); + authLogger.error("์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์˜ค๋ฅ˜:", err); } return null; }); - + // ๋น„๋™๊ธฐ ์ž‘์—…์„ ๋งˆ์ดํฌ๋กœํƒœ์Šคํฌ๋กœ ์ง€์—ฐํ•˜์—ฌ UI ์ฐจ๋‹จ ๋ฐฉ์ง€ - await new Promise(resolve => queueMicrotask(() => resolve())); - + await new Promise((resolve) => queueMicrotask(() => resolve())); + // ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์žˆ๊ณ , ์ด์ „๊ณผ ๋‹ค๋ฅด๋ฉด ์„ธ์…˜ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ if (currentUser && (!user || currentUser.$id !== user.$id)) { try { // ์„ธ์…˜ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹œ๋„ - ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ - const currentSession = await account.getSession('current').catch(err => { - console.log('์„ธ์…˜ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ:', err); - return null; - }); - + const currentSession = await account + .getSession("current") + .catch((err) => { + authLogger.info("์„ธ์…˜ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ:", err); + return null; + }); + // ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ ๋งˆ์ดํฌ๋กœํƒœ์Šคํฌ๋กœ ์ง€์—ฐ queueMicrotask(() => { setUser(currentUser); setSession(currentSession); - console.log('Appwrite ์ธ์ฆ ์ƒํƒœ ๋ณ€๊ฒฝ: ๋กœ๊ทธ์ธ๋จ - ์‚ฌ์šฉ์ž:', currentUser.$id); + authLogger.info( + "Appwrite ์ธ์ฆ ์ƒํƒœ ๋ณ€๊ฒฝ: ๋กœ๊ทธ์ธ๋จ - ์‚ฌ์šฉ์ž:", + currentUser.$id + ); }); } catch (sessionError) { - console.error('์„ธ์…˜ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์ค‘ ์˜ค๋ฅ˜:', sessionError); + authLogger.error("์„ธ์…˜ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์ค‘ ์˜ค๋ฅ˜:", sessionError); // ์˜ค๋ฅ˜ ๋ฐœ์ƒํ•ด๋„ ์‚ฌ์šฉ์ž ์ •๋ณด๋Š” ์—…๋ฐ์ดํŠธ queueMicrotask(() => { setUser(currentUser); @@ -194,18 +223,18 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children queueMicrotask(() => { setSession(null); setUser(null); - + // ๋กœ๊ทธ์•„์›ƒ ์‹œ ์—ด๋ ค์žˆ๋Š” ๋ชจ๋“  ํ† ์ŠคํŠธ ์ œ๊ฑฐ clearAllToasts(); - + // ๋กœ๊ทธ์•„์›ƒ ์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œ์ผœ SyncSettings ๋“ฑ์—์„œ ๊ฐ์ง€ํ•˜๋„๋ก ํ•จ - window.dispatchEvent(new Event('auth-state-changed')); - console.log('Appwrite ์ธ์ฆ ์ƒํƒœ ๋ณ€๊ฒฝ: ๋กœ๊ทธ์•„์›ƒ๋จ'); + window.dispatchEvent(new Event("auth-state-changed")); + authLogger.info("Appwrite ์ธ์ฆ ์ƒํƒœ ๋ณ€๊ฒฝ: ๋กœ๊ทธ์•„์›ƒ๋จ"); }); } } catch (error) { // ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ์—๋„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ค‘๋‹จ๋˜์ง€ ์•Š๋„๋ก ์ฒ˜๋ฆฌ - console.error('Appwrite ์ธ์ฆ ์ƒํƒœ ๊ฒ€์‚ฌ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜:', error); + authLogger.error("Appwrite ์ธ์ฆ ์ƒํƒœ ๊ฒ€์‚ฌ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜:", error); handleAuthError(error); } }, 5000); // 5์ดˆ๋งˆ๋‹ค ํ™•์ธ @@ -218,7 +247,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children // Appwrite ์žฌ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜ const reinitializeAppwrite = useCallback(() => { - console.log('Appwrite ์žฌ์ดˆ๊ธฐํ™” ์š”์ฒญ๋จ'); + authLogger.info("Appwrite ์žฌ์ดˆ๊ธฐํ™” ์š”์ฒญ๋จ"); return reinitializeAppwriteClient(); }, []); @@ -238,9 +267,5 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children // ๋กœ๋”ฉ ์ค‘์ด ์•„๋‹ˆ๊ณ  ์˜ค๋ฅ˜๊ฐ€ ์—†์„ ๋•Œ๋งŒ ์ž์‹ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง // ์˜ค๋ฅ˜๊ฐ€ ์žˆ์–ด๋„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ค‘๋‹จ๋˜์ง€ ์•Š๋„๋ก ์ฒ˜๋ฆฌ - return ( - - {children} - - ); + return {children}; }; diff --git a/src/contexts/auth/auth.utils.ts b/src/contexts/auth/auth.utils.ts index 8e75979..22e12ed 100644 --- a/src/contexts/auth/auth.utils.ts +++ b/src/contexts/auth/auth.utils.ts @@ -1,4 +1,3 @@ - -import { verifyServerConnection } from '@/utils/auth/networkUtils'; +import { verifyServerConnection } from "@/utils/auth/networkUtils"; export { verifyServerConnection }; diff --git a/src/contexts/auth/authActions.ts b/src/contexts/auth/authActions.ts index d9adb89..cf4a076 100644 --- a/src/contexts/auth/authActions.ts +++ b/src/contexts/auth/authActions.ts @@ -1,5 +1,4 @@ - -export { signIn } from './signIn'; -export { signUp } from './signUp'; -export { signOut } from './signOut'; -export { resetPassword } from './resetPassword'; +export { signIn } from "./signIn"; +export { signUp } from "./signUp"; +export { signOut } from "./signOut"; +export { resetPassword } from "./resetPassword"; diff --git a/src/contexts/auth/index.ts b/src/contexts/auth/index.ts index e91dffa..3ba392e 100644 --- a/src/contexts/auth/index.ts +++ b/src/contexts/auth/index.ts @@ -1,4 +1,3 @@ - -export { AuthProvider } from './AuthProvider'; -export { useAuth } from './useAuth'; -export type { AuthContextType } from './types'; +export { AuthProvider } from "./AuthProvider"; +export { useAuth } from "./useAuth"; +export type { AuthContextType } from "./types"; diff --git a/src/contexts/auth/resetPassword.ts b/src/contexts/auth/resetPassword.ts index 59ae33a..fb3e7a3 100644 --- a/src/contexts/auth/resetPassword.ts +++ b/src/contexts/auth/resetPassword.ts @@ -1,50 +1,57 @@ -import { account } from '@/lib/appwrite/client'; -import { showAuthToast } from '@/utils/auth'; +import { account } from "@/lib/appwrite/client"; +import { authLogger } from "@/utils/logger"; +import { showAuthToast } from "@/utils/auth"; export const resetPassword = async (email: string) => { try { - console.log('๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์‹œ๋„ ์ค‘:', email); - + authLogger.info("๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์‹œ๋„ ์ค‘:", email); + // ๋น„๋™๊ธฐ ์ž‘์—…์„ ๋งˆ์ดํฌ๋กœํƒœ์Šคํฌ๋กœ ์ง€์—ฐํ•˜์—ฌ UI ์ฐจ๋‹จ ๋ฐฉ์ง€ - await new Promise(resolve => queueMicrotask(() => resolve())); - + await new Promise((resolve) => queueMicrotask(() => resolve())); + try { // Appwrite๋กœ ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ด๋ฉ”์ผ ๋ฐœ์†ก await account.createRecovery( email, - window.location.origin + '/reset-password' + window.location.origin + "/reset-password" ); - + // ๋น„๋™๊ธฐ ์ž‘์—…์„ ๋งˆ์ดํฌ๋กœํƒœ์Šคํฌ๋กœ ์ง€์—ฐํ•˜์—ฌ UI ์ฐจ๋‹จ ๋ฐฉ์ง€ - await new Promise(resolve => queueMicrotask(() => resolve())); - - showAuthToast('๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ด๋ฉ”์ผ ์ „์†ก๋จ', '์ด๋ฉ”์ผ์„ ํ™•์ธํ•˜์—ฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์žฌ์„ค์ •ํ•ด์ฃผ์„ธ์š”.'); + await new Promise((resolve) => queueMicrotask(() => resolve())); + + showAuthToast( + "๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ด๋ฉ”์ผ ์ „์†ก๋จ", + "์ด๋ฉ”์ผ์„ ํ™•์ธํ•˜์—ฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์žฌ์„ค์ •ํ•ด์ฃผ์„ธ์š”." + ); return { error: null }; } catch (recoveryError: any) { - console.error('๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ด๋ฉ”์ผ ์ „์†ก ์˜ค๋ฅ˜:', recoveryError); - + authLogger.error("๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ด๋ฉ”์ผ ์ „์†ก ์˜ค๋ฅ˜:", recoveryError); + // ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ - let errorMessage = recoveryError.message || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'; - + let errorMessage = + recoveryError.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."; + // Appwrite ์˜ค๋ฅ˜ ์ฝ”๋“œ์— ๋”ฐ๋ฅธ ์‚ฌ์šฉ์ž ์นœํ™”์  ๋ฉ”์‹œ์ง€ if (recoveryError.code === 404) { - errorMessage = '๋“ฑ๋ก๋˜์ง€ ์•Š์€ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค.'; + errorMessage = "๋“ฑ๋ก๋˜์ง€ ์•Š์€ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค."; } else if (recoveryError.code === 429) { - errorMessage = '๋„ˆ๋ฌด ๋งŽ์€ ์š”์ฒญ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'; + errorMessage = + "๋„ˆ๋ฌด ๋งŽ์€ ์š”์ฒญ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."; } - - showAuthToast('๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์‹คํŒจ', errorMessage, 'destructive'); + + showAuthToast("๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์‹คํŒจ", errorMessage, "destructive"); return { error: recoveryError }; } } catch (error: any) { - console.error('๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ:', error); - + authLogger.error("๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ:", error); + // ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ํ™•์ธ - const errorMessage = error.message && error.message.includes('network') - ? '์„œ๋ฒ„ ์—ฐ๊ฒฐ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.' - : '์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'; - - showAuthToast('๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์˜ค๋ฅ˜', errorMessage, 'destructive'); + const errorMessage = + error.message && error.message.includes("network") + ? "์„œ๋ฒ„ ์—ฐ๊ฒฐ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”." + : "์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."; + + showAuthToast("๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์˜ค๋ฅ˜", errorMessage, "destructive"); return { error }; } }; diff --git a/src/contexts/auth/signIn.ts b/src/contexts/auth/signIn.ts index 0d26eaf..56b7c30 100644 --- a/src/contexts/auth/signIn.ts +++ b/src/contexts/auth/signIn.ts @@ -1,78 +1,91 @@ -import { account } from '@/lib/appwrite/client'; -import { showAuthToast } from '@/utils/auth'; -import { getDefaultUserId } from '@/lib/appwrite/defaultUser'; +import { account } from "@/lib/appwrite/client"; +import { authLogger } from "@/utils/logger"; +import { showAuthToast } from "@/utils/auth"; +import { getDefaultUserId } from "@/lib/appwrite/defaultUser"; /** * ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ - Appwrite ํ™˜๊ฒฝ์— ์ตœ์ ํ™” */ export const signIn = async (email: string, password: string) => { try { - console.log('๋กœ๊ทธ์ธ ์‹œ๋„ ์ค‘:', email); - + authLogger.info("๋กœ๊ทธ์ธ ์‹œ๋„ ์ค‘:", email); + // ๋น„๋™๊ธฐ ์ž‘์—…์„ ๋งˆ์ดํฌ๋กœํƒœ์Šคํฌ๋กœ ์ง€์—ฐํ•˜์—ฌ UI ์ฐจ๋‹จ ๋ฐฉ์ง€ - await new Promise(resolve => queueMicrotask(() => resolve())); - + await new Promise((resolve) => queueMicrotask(() => resolve())); + // Appwrite ์ธ์ฆ ๋ฐฉ์‹ ์‹œ๋„ try { const session = await account.createSession(email, password); const user = await account.get(); - + // ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ ๋งˆ์ดํฌ๋กœํƒœ์Šคํฌ๋กœ ์ง€์—ฐ - await new Promise(resolve => queueMicrotask(() => resolve())); - - showAuthToast('๋กœ๊ทธ์ธ ์„ฑ๊ณต', 'ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!'); + await new Promise((resolve) => queueMicrotask(() => resolve())); + + showAuthToast("๋กœ๊ทธ์ธ ์„ฑ๊ณต", "ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!"); return { error: null, user }; } catch (authError: any) { - console.error('๋กœ๊ทธ์ธ ์˜ค๋ฅ˜:', authError); - - let errorMessage = authError.message || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'; + authLogger.error("๋กœ๊ทธ์ธ ์˜ค๋ฅ˜:", authError); + + let errorMessage = authError.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."; let fallbackMode = false; - + // Appwrite ์˜ค๋ฅ˜ ์ฝ”๋“œ์— ๋”ฐ๋ฅธ ์‚ฌ์šฉ์ž ์นœํ™”์  ๋ฉ”์‹œ์ง€ if (authError.code === 401) { - errorMessage = '์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'; + errorMessage = "์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."; } else if (authError.code === 429) { - errorMessage = '๋„ˆ๋ฌด ๋งŽ์€ ๋กœ๊ทธ์ธ ์‹œ๋„๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'; + errorMessage = + "๋„ˆ๋ฌด ๋งŽ์€ ๋กœ๊ทธ์ธ ์‹œ๋„๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."; } else if (authError.code === 404 || authError.code === 503) { // ์„œ๋ฒ„ ์—ฐ๊ฒฐ ๋ฌธ์ œ์ธ ๊ฒฝ์šฐ ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ID๋ฅผ ํ™œ์šฉํ•œ ๋Œ€์ฒด ๋กœ์ง ์‹œ๋„ - errorMessage = '์„œ๋ฒ„ ์—ฐ๊ฒฐ์— ๋ฌธ์ œ๊ฐ€ ์žˆ์–ด ์ผ๋ฐ˜ ๋ชจ๋“œ๋กœ ์ ‘์†ํ•ฉ๋‹ˆ๋‹ค.'; + errorMessage = "์„œ๋ฒ„ ์—ฐ๊ฒฐ์— ๋ฌธ์ œ๊ฐ€ ์žˆ์–ด ์ผ๋ฐ˜ ๋ชจ๋“œ๋กœ ์ ‘์†ํ•ฉ๋‹ˆ๋‹ค."; fallbackMode = true; - + try { // ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ID๋ฅผ ํ™œ์šฉํ•œ ๋Œ€์ฒด ๋กœ์ง const defaultUserId = getDefaultUserId(); - console.log('๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ID๋ฅผ ํ™œ์šฉํ•œ ๋Œ€์ฒด ๋กœ์ง ์‹œ๋„:', defaultUserId); - + authLogger.info( + "๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ID๋ฅผ ํ™œ์šฉํ•œ ๋Œ€์ฒด ๋กœ์ง ์‹œ๋„:", + defaultUserId + ); + // ์ผ๋ฐ˜ ๋ชจ๋“œ๋กœ ์ ‘์†ํ•˜๋Š” ๊ฒฝ์šฐ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ - showAuthToast('์ผ๋ฐ˜ ๋ชจ๋“œ ์ ‘์†', '์ผ๋ฐ˜ ๋ชจ๋“œ๋กœ ์ ‘์†ํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ถ€ ๊ธฐ๋Šฅ์ด ์ œํ•œ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.', 'default'); - + showAuthToast( + "์ผ๋ฐ˜ ๋ชจ๋“œ ์ ‘์†", + "์ผ๋ฐ˜ ๋ชจ๋“œ๋กœ ์ ‘์†ํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ถ€ ๊ธฐ๋Šฅ์ด ์ œํ•œ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + "default" + ); + // ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ง„ ๊ฐ€์ƒ์˜ ์‚ฌ์šฉ์ž ๊ฐ์ฒด ์ƒ์„ฑ const fallbackUser = { $id: defaultUserId, - name: '์ผ๋ฐ˜ ์‚ฌ์šฉ์ž', + name: "์ผ๋ฐ˜ ์‚ฌ์šฉ์ž", email: email, $createdAt: new Date().toISOString(), $updatedAt: new Date().toISOString(), status: true, - isFallbackUser: true // ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž์ž„์„ ํ‘œ์‹œํ•˜๋Š” ํ”Œ๋ž˜๊ทธ + isFallbackUser: true, // ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž์ž„์„ ํ‘œ์‹œํ•˜๋Š” ํ”Œ๋ž˜๊ทธ }; - + return { error: null, user: fallbackUser, isFallbackMode: true }; } catch (fallbackError) { - console.error('๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ๋Œ€์ฒด ๋กœ์ง ์˜ค๋ฅ˜:', fallbackError); + authLogger.error("๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ๋Œ€์ฒด ๋กœ์ง ์˜ค๋ฅ˜:", fallbackError); // ๋Œ€์ฒด ๋กœ์ง๋„ ์‹คํŒจํ•œ ๊ฒฝ์šฐ ์›๋ž˜ ์˜ค๋ฅ˜ ๋ฐ˜ํ™˜ } } - + if (!fallbackMode) { - showAuthToast('๋กœ๊ทธ์ธ ์‹คํŒจ', errorMessage, 'destructive'); + showAuthToast("๋กœ๊ทธ์ธ ์‹คํŒจ", errorMessage, "destructive"); } - + return { error: authError, user: null }; } } catch (error) { - console.error('๋กœ๊ทธ์ธ ์˜ˆ์™ธ ๋ฐœ์ƒ:', error); - showAuthToast('๋กœ๊ทธ์ธ ์˜ค๋ฅ˜', '์„œ๋ฒ„ ์—ฐ๊ฒฐ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.', 'destructive'); + authLogger.error("๋กœ๊ทธ์ธ ์˜ˆ์™ธ ๋ฐœ์ƒ:", error); + showAuthToast( + "๋กœ๊ทธ์ธ ์˜ค๋ฅ˜", + "์„œ๋ฒ„ ์—ฐ๊ฒฐ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + "destructive" + ); return { error, user: null }; } }; diff --git a/src/contexts/auth/signInUtils.ts b/src/contexts/auth/signInUtils.ts index cf2af46..280cf92 100644 --- a/src/contexts/auth/signInUtils.ts +++ b/src/contexts/auth/signInUtils.ts @@ -1,53 +1,58 @@ - -import { supabase } from '@/archive/lib/supabase'; -import { showAuthToast } from '@/utils/auth'; +import { supabase } from "@/archive/lib/supabase"; +import { authLogger } from "@/utils/logger"; +import { showAuthToast } from "@/utils/auth"; /** * ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ - Supabase Cloud ํ™˜๊ฒฝ์— ์ตœ์ ํ™” */ export const signInWithDirectApi = async (email: string, password: string) => { - console.log('Supabase Cloud ๋กœ๊ทธ์ธ ์‹œ๋„'); - + authLogger.info("Supabase Cloud ๋กœ๊ทธ์ธ ์‹œ๋„"); + try { // Supabase Cloud๋ฅผ ํ†ตํ•œ ๋กœ๊ทธ์ธ ์š”์ฒญ const { data, error } = await supabase.auth.signInWithPassword({ email, - password + password, }); - + // ์˜ค๋ฅ˜ ์‘๋‹ต ์ฒ˜๋ฆฌ if (error) { - console.error('๋กœ๊ทธ์ธ ์˜ค๋ฅ˜:', error); - + authLogger.error("๋กœ๊ทธ์ธ ์˜ค๋ฅ˜:", error); + // ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ํฌ๋งทํŒ… let errorMessage = error.message; - - if (error.message.includes('Invalid login credentials')) { - errorMessage = '์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'; - } else if (error.message.includes('Email not confirmed')) { - errorMessage = '์ด๋ฉ”์ผ ์ธ์ฆ์ด ์™„๋ฃŒ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ด๋ฉ”์ผ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.'; + + if (error.message.includes("Invalid login credentials")) { + errorMessage = "์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."; + } else if (error.message.includes("Email not confirmed")) { + errorMessage = + "์ด๋ฉ”์ผ ์ธ์ฆ์ด ์™„๋ฃŒ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ด๋ฉ”์ผ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”."; } - - showAuthToast('๋กœ๊ทธ์ธ ์‹คํŒจ', errorMessage, 'destructive'); + + showAuthToast("๋กœ๊ทธ์ธ ์‹คํŒจ", errorMessage, "destructive"); return { error: { message: errorMessage }, user: null }; } - + // ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์ฒ˜๋ฆฌ if (data && data.user) { - console.log('๋กœ๊ทธ์ธ ์„ฑ๊ณต:', data.user); - showAuthToast('๋กœ๊ทธ์ธ ์„ฑ๊ณต', 'ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!'); + authLogger.info("๋กœ๊ทธ์ธ ์„ฑ๊ณต:", data.user); + showAuthToast("๋กœ๊ทธ์ธ ์„ฑ๊ณต", "ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!"); return { error: null, user: data.user }; } else { // ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ (๋“œ๋ฌธ ๊ฒฝ์šฐ) - console.warn('๋กœ๊ทธ์ธ ์„ฑ๊ณตํ–ˆ์ง€๋งŒ ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค'); - showAuthToast('๋กœ๊ทธ์ธ ๋ถ€๋ถ„ ์„ฑ๊ณต', '๋กœ๊ทธ์ธ์€ ์„ฑ๊ณตํ–ˆ์ง€๋งŒ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.', 'default'); - return { error: { message: '์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ ์‹คํŒจ' }, user: null }; + authLogger.warn("๋กœ๊ทธ์ธ ์„ฑ๊ณตํ–ˆ์ง€๋งŒ ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค"); + showAuthToast( + "๋กœ๊ทธ์ธ ๋ถ€๋ถ„ ์„ฑ๊ณต", + "๋กœ๊ทธ์ธ์€ ์„ฑ๊ณตํ–ˆ์ง€๋งŒ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.", + "default" + ); + return { error: { message: "์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ ์‹คํŒจ" }, user: null }; } } catch (error: any) { - console.error('๋กœ๊ทธ์ธ ์š”์ฒญ ์ค‘ ์˜ˆ์™ธ:', error); - const errorMessage = error.message || '๋กœ๊ทธ์ธ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'; - - showAuthToast('๋กœ๊ทธ์ธ ์š”์ฒญ ์‹คํŒจ', errorMessage, 'destructive'); + authLogger.error("๋กœ๊ทธ์ธ ์š”์ฒญ ์ค‘ ์˜ˆ์™ธ:", error); + const errorMessage = error.message || "๋กœ๊ทธ์ธ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."; + + showAuthToast("๋กœ๊ทธ์ธ ์š”์ฒญ ์‹คํŒจ", errorMessage, "destructive"); return { error: { message: errorMessage }, user: null }; } }; diff --git a/src/contexts/auth/signOut.ts b/src/contexts/auth/signOut.ts index dc2875e..ad56f25 100644 --- a/src/contexts/auth/signOut.ts +++ b/src/contexts/auth/signOut.ts @@ -1,49 +1,52 @@ -import { account } from '@/lib/appwrite/client'; -import { showAuthToast } from '@/utils/auth'; -import { clearAllToasts } from '@/hooks/toast/toastManager'; +import { account } from "@/lib/appwrite/client"; +import { authLogger } from "@/utils/logger"; +import { showAuthToast } from "@/utils/auth"; +import { clearAllToasts } from "@/hooks/toast/toastManager"; export const signOut = async (): Promise => { try { - console.log('๋กœ๊ทธ์•„์›ƒ ์‹œ๋„ ์ค‘'); - + authLogger.info("๋กœ๊ทธ์•„์›ƒ ์‹œ๋„ ์ค‘"); + // ๋น„๋™๊ธฐ ์ž‘์—…์„ ๋งˆ์ดํฌ๋กœํƒœ์Šคํฌ๋กœ ์ง€์—ฐํ•˜์—ฌ UI ์ฐจ๋‹จ ๋ฐฉ์ง€ - await new Promise(resolve => queueMicrotask(() => resolve())); - + await new Promise((resolve) => queueMicrotask(() => resolve())); + try { // ํ˜„์žฌ ์„ธ์…˜ ์•„์ด๋”” ๊ฐ€์ ธ์˜ค๊ธฐ - const currentSession = await account.getSession('current'); - + const currentSession = await account.getSession("current"); + // ํ˜„์žฌ ์„ธ์…˜ ์‚ญ์ œ await account.deleteSession(currentSession.$id); - + // ๋กœ๊ทธ์•„์›ƒ ์‹œ ์—ด๋ ค์žˆ๋Š” ๋ชจ๋“  ํ† ์ŠคํŠธ ์ œ๊ฑฐ clearAllToasts(); - + // ๋กœ๊ทธ์•„์›ƒ ์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œ์ผœ SyncSettings ๋“ฑ์—์„œ ๊ฐ์ง€ํ•˜๋„๋ก ํ•จ - window.dispatchEvent(new Event('auth-state-changed')); - - showAuthToast('๋กœ๊ทธ์•„์›ƒ ์„ฑ๊ณต', '๋‹ค์Œ์— ๋˜ ๋งŒ๋‚˜์š”!'); + window.dispatchEvent(new Event("auth-state-changed")); + + showAuthToast("๋กœ๊ทธ์•„์›ƒ ์„ฑ๊ณต", "๋‹ค์Œ์— ๋˜ ๋งŒ๋‚˜์š”!"); } catch (sessionError: any) { - console.error('์„ธ์…˜ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜:', sessionError); - + authLogger.error("์„ธ์…˜ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜:", sessionError); + // ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ - let errorMessage = sessionError.message || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'; - + let errorMessage = + sessionError.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."; + // Appwrite ์˜ค๋ฅ˜ ์ฝ”๋“œ์— ๋”ฐ๋ฅธ ์‚ฌ์šฉ์ž ์นœํ™”์  ๋ฉ”์‹œ์ง€ if (sessionError.code === 401) { - errorMessage = '์ด๋ฏธ ๋กœ๊ทธ์•„์›ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'; + errorMessage = "์ด๋ฏธ ๋กœ๊ทธ์•„์›ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."; } - - showAuthToast('๋กœ๊ทธ์•„์›ƒ ์‹คํŒจ', errorMessage, 'destructive'); + + showAuthToast("๋กœ๊ทธ์•„์›ƒ ์‹คํŒจ", errorMessage, "destructive"); } } catch (error: any) { - console.error('๋กœ๊ทธ์•„์›ƒ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ:', error); - + authLogger.error("๋กœ๊ทธ์•„์›ƒ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ:", error); + // ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ํ™•์ธ - const errorMessage = error.message && error.message.includes('network') - ? '์„œ๋ฒ„ ์—ฐ๊ฒฐ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.' - : '์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'; - - showAuthToast('๋กœ๊ทธ์•„์›ƒ ์˜ค๋ฅ˜', errorMessage, 'destructive'); + const errorMessage = + error.message && error.message.includes("network") + ? "์„œ๋ฒ„ ์—ฐ๊ฒฐ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”." + : "์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."; + + showAuthToast("๋กœ๊ทธ์•„์›ƒ ์˜ค๋ฅ˜", errorMessage, "destructive"); } }; diff --git a/src/contexts/auth/signUp.ts b/src/contexts/auth/signUp.ts index d9ed69e..036d938 100644 --- a/src/contexts/auth/signUp.ts +++ b/src/contexts/auth/signUp.ts @@ -1,72 +1,87 @@ - -import { account, client } from '@/lib/appwrite/client'; -import { ID } from 'appwrite'; -import { showAuthToast } from '@/utils/auth'; -import { isValidConnection } from '@/lib/appwrite/client'; +import { account, client } from "@/lib/appwrite/client"; +import { authLogger } from "@/utils/logger"; +import { ID } from "appwrite"; +import { showAuthToast } from "@/utils/auth"; +import { isValidConnection } from "@/lib/appwrite/client"; /** * ํšŒ์›๊ฐ€์ž… ๊ธฐ๋Šฅ - Appwrite ํ™˜๊ฒฝ์— ์ตœ์ ํ™” */ -export const signUp = async (email: string, password: string, username: string) => { +export const signUp = async ( + email: string, + password: string, + username: string +) => { try { // ๋น„๋™๊ธฐ ์ž‘์—…์„ ๋งˆ์ดํฌ๋กœํƒœ์Šคํฌ๋กœ ์ง€์—ฐํ•˜์—ฌ UI ์ฐจ๋‹จ ๋ฐฉ์ง€ - await new Promise(resolve => queueMicrotask(() => resolve())); - + await new Promise((resolve) => queueMicrotask(() => resolve())); + // ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์ƒํƒœ ํ™•์ธ const connected = await isValidConnection(); if (!connected) { - console.error('์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ'); - showAuthToast('ํšŒ์›๊ฐ€์ž… ์˜ค๋ฅ˜', '์„œ๋ฒ„ ์—ฐ๊ฒฐ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.', 'destructive'); - return { error: { message: '์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ' }, user: null }; + authLogger.error("์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ"); + showAuthToast( + "ํšŒ์›๊ฐ€์ž… ์˜ค๋ฅ˜", + "์„œ๋ฒ„ ์—ฐ๊ฒฐ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.", + "destructive" + ); + return { error: { message: "์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ" }, user: null }; } - - console.log('ํšŒ์›๊ฐ€์ž… ์‹œ๋„:', email); - + + authLogger.info("ํšŒ์›๊ฐ€์ž… ์‹œ๋„:", email); + try { // Appwrite๋กœ ํšŒ์›๊ฐ€์ž… ์š”์ฒญ - const user = await account.create( - ID.unique(), - email, - password, - username - ); - + const user = await account.create(ID.unique(), email, password, username); + // ์ด๋ฉ”์ผ ์ธ์ฆ ๋ฉ”์ผ ๋ฐœ์†ก - await account.createVerification(window.location.origin + '/login'); - + await account.createVerification(window.location.origin + "/login"); + // ๋น„๋™๊ธฐ ์ž‘์—…์„ ๋งˆ์ดํฌ๋กœํƒœ์Šคํฌ๋กœ ์ง€์—ฐํ•˜์—ฌ UI ์ฐจ๋‹จ ๋ฐฉ์ง€ - await new Promise(resolve => queueMicrotask(() => resolve())); - - showAuthToast('ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต', '์ธ์ฆ ๋ฉ”์ผ์ด ๋ฐœ์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ŠคํŒธ ํด๋”๋„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.', 'default'); - console.log('์ธ์ฆ ๋ฉ”์ผ ๋ฐœ์†ก๋จ:', email); - - return { - error: null, - user, - message: '์ด๋ฉ”์ผ ์ธ์ฆ ํ•„์š”', - emailConfirmationRequired: true + await new Promise((resolve) => queueMicrotask(() => resolve())); + + showAuthToast( + "ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต", + "์ธ์ฆ ๋ฉ”์ผ์ด ๋ฐœ์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ŠคํŒธ ํด๋”๋„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.", + "default" + ); + authLogger.info("์ธ์ฆ ๋ฉ”์ผ ๋ฐœ์†ก๋จ:", email); + + return { + error: null, + user, + message: "์ด๋ฉ”์ผ ์ธ์ฆ ํ•„์š”", + emailConfirmationRequired: true, }; } catch (authError: any) { - console.error('ํšŒ์›๊ฐ€์ž… ์˜ค๋ฅ˜:', authError); - + authLogger.error("ํšŒ์›๊ฐ€์ž… ์˜ค๋ฅ˜:", authError); + // ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ - let errorMessage = authError.message || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'; - + let errorMessage = authError.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."; + // Appwrite ์˜ค๋ฅ˜ ์ฝ”๋“œ์— ๋”ฐ๋ฅธ ์‚ฌ์šฉ์ž ์นœํ™”์  ๋ฉ”์‹œ์ง€ if (authError.code === 409) { - errorMessage = '์ด๋ฏธ ๋“ฑ๋ก๋œ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค.'; + errorMessage = "์ด๋ฏธ ๋“ฑ๋ก๋œ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค."; } else if (authError.code === 400) { - errorMessage = '์œ ํšจํ•˜์ง€ ์•Š์€ ์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค.'; + errorMessage = "์œ ํšจํ•˜์ง€ ์•Š์€ ์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค."; } else if (authError.code === 429) { - errorMessage = '๋„ˆ๋ฌด ๋งŽ์€ ์š”์ฒญ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'; + errorMessage = + "๋„ˆ๋ฌด ๋งŽ์€ ์š”์ฒญ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."; } - - showAuthToast('ํšŒ์›๊ฐ€์ž… ์‹คํŒจ', errorMessage, 'destructive'); + + showAuthToast("ํšŒ์›๊ฐ€์ž… ์‹คํŒจ", errorMessage, "destructive"); return { error: { message: errorMessage }, user: null }; } } catch (error: any) { - console.error('ํšŒ์›๊ฐ€์ž… ์ „์—ญ ์˜ˆ์™ธ:', error); - showAuthToast('ํšŒ์›๊ฐ€์ž… ์˜ค๋ฅ˜', error.message || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜', 'destructive'); - return { error: { message: error.message || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜' }, user: null }; + authLogger.error("ํšŒ์›๊ฐ€์ž… ์ „์—ญ ์˜ˆ์™ธ:", error); + showAuthToast( + "ํšŒ์›๊ฐ€์ž… ์˜ค๋ฅ˜", + error.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜", + "destructive" + ); + return { + error: { message: error.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜" }, + user: null, + }; } }; diff --git a/src/contexts/auth/signUpApiCalls.ts b/src/contexts/auth/signUpApiCalls.ts index 83d3bc5..56c4c1d 100644 --- a/src/contexts/auth/signUpApiCalls.ts +++ b/src/contexts/auth/signUpApiCalls.ts @@ -1,15 +1,20 @@ - -import { parseResponse, showAuthToast } from '@/utils/auth'; -import { getProxyType, isCorsProxyEnabled, getSupabaseUrl, getOriginalSupabaseUrl } from '@/lib/supabase/config'; -import { handleNetworkError } from '@/utils/auth/handleNetworkError'; +import { parseResponse, showAuthToast } from "@/utils/auth"; +import { authLogger } from "@/utils/logger"; +import { + getProxyType, + isCorsProxyEnabled, + getSupabaseUrl, + getOriginalSupabaseUrl, +} from "@/lib/supabase/config"; +import { handleNetworkError } from "@/utils/auth/handleNetworkError"; /** * ์ง์ ‘ API ํ˜ธ์ถœ์„ ํ†ตํ•œ ํšŒ์›๊ฐ€์ž… ์š”์ฒญ ์ „์†ก */ export const sendSignUpApiRequest = async ( - email: string, - password: string, - username: string, + email: string, + password: string, + username: string, redirectUrl: string, supabaseKey: string ) => { @@ -17,41 +22,43 @@ export const sendSignUpApiRequest = async ( // ํ”„๋ก์‹œ ์ ์šฉ๋œ URL๊ณผ ์›๋ณธ URL ๋ชจ๋‘ ๊ฐ€์ ธ์˜ค๊ธฐ const supabaseUrl = getOriginalSupabaseUrl(); // ์›๋ณธ URL const proxyUrl = getSupabaseUrl(); // ํ”„๋ก์‹œ ์ ์šฉ๋œ URL - + // ํ”„๋ก์‹œ ์ •๋ณด ๋กœ๊น… const usingProxy = isCorsProxyEnabled(); const proxyType = getProxyType(); - console.log(`CORS ํ”„๋ก์‹œ ์‚ฌ์šฉ: ${usingProxy ? '์˜ˆ' : '์•„๋‹ˆ์˜ค'}, ํƒ€์ž…: ${proxyType}, ํ”„๋ก์‹œ URL: ${proxyUrl}`); - + authLogger.info( + `CORS ํ”„๋ก์‹œ ์‚ฌ์šฉ: ${usingProxy ? "์˜ˆ" : "์•„๋‹ˆ์˜ค"}, ํƒ€์ž…: ${proxyType}, ํ”„๋ก์‹œ URL: ${proxyUrl}` + ); + // ์‹ค์ œ ์š”์ฒญ์— ์‚ฌ์šฉํ•  URL ๊ฒฐ์ • (ํ•ญ์ƒ ํ”„๋ก์‹œ URL ์‚ฌ์šฉ) const useUrl = usingProxy ? proxyUrl : supabaseUrl; - + // URL์— auth/v1์ด ์ด๋ฏธ ํฌํ•จ๋˜์–ด์žˆ๋Š”์ง€ ํ™•์ธ - const baseUrl = useUrl.includes('/auth/v1') ? useUrl : `${useUrl}/auth/v1`; - + const baseUrl = useUrl.includes("/auth/v1") ? useUrl : `${useUrl}/auth/v1`; + // ํšŒ์›๊ฐ€์ž… API ์—”๋“œํฌ์ธํŠธ ๋ฐ ํ—ค๋” ์„ค์ • const signUpUrl = `${baseUrl}/signup`; const headers = { - 'Content-Type': 'application/json', - 'apikey': supabaseKey + "Content-Type": "application/json", + apikey: supabaseKey, }; - - console.log('ํšŒ์›๊ฐ€์ž… API ์š”์ฒญ URL:', signUpUrl); - + + authLogger.info("ํšŒ์›๊ฐ€์ž… API ์š”์ฒญ URL:", signUpUrl); + // ํšŒ์›๊ฐ€์ž… ์š”์ฒญ ์ „์†ก const response = await fetch(signUpUrl, { - method: 'POST', + method: "POST", headers, - body: JSON.stringify({ - email, + body: JSON.stringify({ + email, password, data: { username }, // ์‚ฌ์šฉ์ž ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— username ์ถ”๊ฐ€ - redirect_to: redirectUrl // ๋ฆฌ๋””๋ ‰์…˜ URL ์ถ”๊ฐ€ + redirect_to: redirectUrl, // ๋ฆฌ๋””๋ ‰์…˜ URL ์ถ”๊ฐ€ }), - signal: AbortSignal.timeout(15000) // ํƒ€์ž„์•„์›ƒ ์‹œ๊ฐ„ ์ฆ๊ฐ€ + signal: AbortSignal.timeout(15000), // ํƒ€์ž„์•„์›ƒ ์‹œ๊ฐ„ ์ฆ๊ฐ€ }); - - console.log('ํšŒ์›๊ฐ€์ž… ์‘๋‹ต ์ƒํƒœ:', response.status); + + authLogger.info("ํšŒ์›๊ฐ€์ž… ์‘๋‹ต ์ƒํƒœ:", response.status); return response; } catch (error) { throw error; // ์ƒ์œ„ ํ•จ์ˆ˜์—์„œ ์ฒ˜๋ฆฌํ•˜๋„๋ก ์˜ค๋ฅ˜ ์ „ํŒŒ @@ -64,13 +71,13 @@ export const sendSignUpApiRequest = async ( export const getStatusErrorMessage = (status: number): string => { switch (status) { case 400: - return '์ž˜๋ชป๋œ ์š”์ฒญ ํ˜•์‹์ž…๋‹ˆ๋‹ค. ์ž…๋ ฅ ๋ฐ์ดํ„ฐ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.'; + return "์ž˜๋ชป๋œ ์š”์ฒญ ํ˜•์‹์ž…๋‹ˆ๋‹ค. ์ž…๋ ฅ ๋ฐ์ดํ„ฐ๋ฅผ ํ™•์ธํ•˜์„ธ์š”."; case 401: - return 'ํšŒ์›๊ฐ€์ž… ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค. Supabase ์„ค์ • ๋˜๋Š” ๊ถŒํ•œ์„ ํ™•์ธํ•˜์„ธ์š”.'; + return "ํšŒ์›๊ฐ€์ž… ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค. Supabase ์„ค์ • ๋˜๋Š” ๊ถŒํ•œ์„ ํ™•์ธํ•˜์„ธ์š”."; case 404: - return '์„œ๋ฒ„ ๊ฒฝ๋กœ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. Supabase URL์„ ํ™•์ธํ•˜์„ธ์š”.'; + return "์„œ๋ฒ„ ๊ฒฝ๋กœ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. Supabase URL์„ ํ™•์ธํ•˜์„ธ์š”."; case 500: - return '์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‚˜์ค‘์— ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”.'; + return "์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‚˜์ค‘์— ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”."; default: return `ํšŒ์›๊ฐ€์ž… ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค (${status}). ๋‚˜์ค‘์— ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”.`; } diff --git a/src/contexts/auth/signUpErrorHandlers.ts b/src/contexts/auth/signUpErrorHandlers.ts index ee10e48..51ccfae 100644 --- a/src/contexts/auth/signUpErrorHandlers.ts +++ b/src/contexts/auth/signUpErrorHandlers.ts @@ -1,37 +1,40 @@ - -import { showAuthToast } from '@/utils/auth'; -import { getProxyType, isCorsProxyEnabled } from '@/lib/supabase/config'; +import { showAuthToast } from "@/utils/auth"; +import { authLogger } from "@/utils/logger"; +import { getProxyType, isCorsProxyEnabled } from "@/lib/supabase/config"; /** * ํšŒ์›๊ฐ€์ž… API ํ˜ธ์ถœ ์ค‘ ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ */ export const handleSignUpApiError = (error: any) => { - console.error('ํšŒ์›๊ฐ€์ž… API ํ˜ธ์ถœ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ:', error); - + authLogger.error("ํšŒ์›๊ฐ€์ž… API ํ˜ธ์ถœ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ:", error); + // ํ”„๋ก์‹œ ์„ค์ • ํ™•์ธ const usingProxy = isCorsProxyEnabled(); const proxyType = getProxyType(); - + // ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์„ค์ • ๋ฐ ํ”„๋ก์‹œ ์ถ”์ฒœ - let errorMessage = error.message || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'; - + let errorMessage = error.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."; + // ํƒ€์ž„์•„์›ƒ ์˜ค๋ฅ˜ ๊ฐ์ง€ - if (errorMessage.includes('timed out') || errorMessage.includes('timeout')) { - errorMessage = '์„œ๋ฒ„ ์‘๋‹ต ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜๊ณ  ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”.'; + if (errorMessage.includes("timed out") || errorMessage.includes("timeout")) { + errorMessage = + "์„œ๋ฒ„ ์‘๋‹ต ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜๊ณ  ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”."; } // CORS ๋˜๋Š” ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ๊ฐ์ง€ - else if (errorMessage.includes('Failed to fetch') || - errorMessage.includes('NetworkError') || - errorMessage.includes('CORS')) { + else if ( + errorMessage.includes("Failed to fetch") || + errorMessage.includes("NetworkError") || + errorMessage.includes("CORS") + ) { if (!usingProxy) { - errorMessage += ' (์„ค์ •์—์„œ Cloudflare CORS ํ”„๋ก์‹œ ํ™œ์„ฑํ™”๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค)'; - } else if (proxyType !== 'cloudflare') { - errorMessage += ' (์„ค์ •์—์„œ Cloudflare CORS ํ”„๋ก์‹œ๋กœ ๋ณ€๊ฒฝ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค)'; + errorMessage += " (์„ค์ •์—์„œ Cloudflare CORS ํ”„๋ก์‹œ ํ™œ์„ฑํ™”๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค)"; + } else if (proxyType !== "cloudflare") { + errorMessage += " (์„ค์ •์—์„œ Cloudflare CORS ํ”„๋ก์‹œ๋กœ ๋ณ€๊ฒฝ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค)"; } } - - showAuthToast('ํšŒ์›๊ฐ€์ž… ์˜ค๋ฅ˜', errorMessage, 'destructive'); - + + showAuthToast("ํšŒ์›๊ฐ€์ž… ์˜ค๋ฅ˜", errorMessage, "destructive"); + return { error: { message: errorMessage }, user: null }; }; @@ -40,14 +43,22 @@ export const handleSignUpApiError = (error: any) => { */ export const handleResponseError = (responseData: any) => { if (responseData && responseData.error) { - const errorMessage = responseData.error_description || responseData.error || 'ํšŒ์›๊ฐ€์ž… ์‹คํŒจ'; - - if (responseData.error === 'user_already_registered') { - showAuthToast('ํšŒ์›๊ฐ€์ž… ์‹คํŒจ', '์ด๋ฏธ ๋“ฑ๋ก๋œ ์ด๋ฉ”์ผ ์ฃผ์†Œ์ž…๋‹ˆ๋‹ค.', 'destructive'); - return { error: { message: '์ด๋ฏธ ๋“ฑ๋ก๋œ ์ด๋ฉ”์ผ ์ฃผ์†Œ์ž…๋‹ˆ๋‹ค.' }, user: null }; + const errorMessage = + responseData.error_description || responseData.error || "ํšŒ์›๊ฐ€์ž… ์‹คํŒจ"; + + if (responseData.error === "user_already_registered") { + showAuthToast( + "ํšŒ์›๊ฐ€์ž… ์‹คํŒจ", + "์ด๋ฏธ ๋“ฑ๋ก๋œ ์ด๋ฉ”์ผ ์ฃผ์†Œ์ž…๋‹ˆ๋‹ค.", + "destructive" + ); + return { + error: { message: "์ด๋ฏธ ๋“ฑ๋ก๋œ ์ด๋ฉ”์ผ ์ฃผ์†Œ์ž…๋‹ˆ๋‹ค." }, + user: null, + }; } - - showAuthToast('ํšŒ์›๊ฐ€์ž… ์‹คํŒจ', errorMessage, 'destructive'); + + showAuthToast("ํšŒ์›๊ฐ€์ž… ์‹คํŒจ", errorMessage, "destructive"); return { error: { message: errorMessage }, user: null }; } return null; diff --git a/src/contexts/auth/signUpUtils.ts b/src/contexts/auth/signUpUtils.ts index 074a2a3..581b106 100644 --- a/src/contexts/auth/signUpUtils.ts +++ b/src/contexts/auth/signUpUtils.ts @@ -1,87 +1,102 @@ - -import { supabase } from '@/archive/lib/supabase'; -import { parseResponse, showAuthToast } from '@/utils/auth'; +import { supabase } from "@/archive/lib/supabase"; +import { authLogger } from "@/utils/logger"; +import { parseResponse, showAuthToast } from "@/utils/auth"; /** * ํšŒ์›๊ฐ€์ž… ๊ธฐ๋Šฅ - Supabase Cloud ํ™˜๊ฒฝ์— ์ตœ์ ํ™” */ -export const signUpWithDirectApi = async (email: string, password: string, username: string, redirectUrl?: string) => { +export const signUpWithDirectApi = async ( + email: string, + password: string, + username: string, + redirectUrl?: string +) => { try { - console.log('Supabase Cloud ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์ค‘'); - + authLogger.info("Supabase Cloud ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์ค‘"); + // ๋ฆฌ๋””๋ ‰์…˜ URL ์„ค์ • (์ „๋‹ฌ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ) - const finalRedirectUrl = redirectUrl || `${window.location.origin}/login?auth_callback=true`; - console.log('์ด๋ฉ”์ผ ์ธ์ฆ ๋ฆฌ๋””๋ ‰์…˜ URL:', finalRedirectUrl); - + const finalRedirectUrl = + redirectUrl || `${window.location.origin}/login?auth_callback=true`; + authLogger.info("์ด๋ฉ”์ผ ์ธ์ฆ ๋ฆฌ๋””๋ ‰์…˜ URL:", finalRedirectUrl); + // Supabase Cloud API๋ฅผ ํ†ตํ•œ ํšŒ์›๊ฐ€์ž… ์š”์ฒญ const { data, error } = await supabase.auth.signUp({ email, password, options: { data: { - username // ์‚ฌ์šฉ์ž ์ด๋ฆ„์„ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ์ €์žฅ + username, // ์‚ฌ์šฉ์ž ์ด๋ฆ„์„ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ์ €์žฅ }, - emailRedirectTo: finalRedirectUrl - } + emailRedirectTo: finalRedirectUrl, + }, }); - + // ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ if (error) { - console.error('ํšŒ์›๊ฐ€์ž… ์˜ค๋ฅ˜:', error); - + authLogger.error("ํšŒ์›๊ฐ€์ž… ์˜ค๋ฅ˜:", error); + let errorMessage = error.message; - if (error.message.includes('User already registered')) { - errorMessage = '์ด๋ฏธ ๋“ฑ๋ก๋œ ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค.'; - } else if (error.message.includes('Signup not allowed')) { - errorMessage = 'ํšŒ์›๊ฐ€์ž…์ด ํ—ˆ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'; - } else if (error.message.includes('Email link invalid')) { - errorMessage = '์ด๋ฉ”์ผ ๋งํฌ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'; + if (error.message.includes("User already registered")) { + errorMessage = "์ด๋ฏธ ๋“ฑ๋ก๋œ ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค."; + } else if (error.message.includes("Signup not allowed")) { + errorMessage = "ํšŒ์›๊ฐ€์ž…์ด ํ—ˆ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."; + } else if (error.message.includes("Email link invalid")) { + errorMessage = "์ด๋ฉ”์ผ ๋งํฌ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."; } - - showAuthToast('ํšŒ์›๊ฐ€์ž… ์‹คํŒจ', errorMessage, 'destructive'); + + showAuthToast("ํšŒ์›๊ฐ€์ž… ์‹คํŒจ", errorMessage, "destructive"); return { error: { message: errorMessage }, user: null }; } - + // ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต if (data && data.user) { // ์ด๋ฉ”์ผ ํ™•์ธ์ด ํ•„์š”ํ•œ์ง€ ํ™•์ธ - const isEmailConfirmationRequired = data.user.identities && - data.user.identities.length > 0 && - !data.user.identities[0].identity_data?.email_verified; - + const isEmailConfirmationRequired = + data.user.identities && + data.user.identities.length > 0 && + !data.user.identities[0].identity_data?.email_verified; + if (isEmailConfirmationRequired) { // ์ธ์ฆ ๋ฉ”์ผ ์ „์†ก ์„ฑ๊ณต ๋ฉ”์‹œ์ง€์™€ ์ด๋ฉ”์ผ ํ™•์ธ ์•ˆ๋‚ด - showAuthToast('ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต', '์ธ์ฆ ๋ฉ”์ผ์ด ๋ฐœ์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ŠคํŒธ ํด๋”๋„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.', 'default'); - console.log('์ธ์ฆ ๋ฉ”์ผ ๋ฐœ์†ก๋จ:', email); - - return { - error: null, - user: data.user, - message: '์ด๋ฉ”์ผ ์ธ์ฆ ํ•„์š”', - emailConfirmationRequired: true + showAuthToast( + "ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต", + "์ธ์ฆ ๋ฉ”์ผ์ด ๋ฐœ์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ŠคํŒธ ํด๋”๋„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.", + "default" + ); + authLogger.info("์ธ์ฆ ๋ฉ”์ผ ๋ฐœ์†ก๋จ:", email); + + return { + error: null, + user: data.user, + message: "์ด๋ฉ”์ผ ์ธ์ฆ ํ•„์š”", + emailConfirmationRequired: true, }; } else { - showAuthToast('ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต', 'ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!', 'default'); + showAuthToast("ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต", "ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!", "default"); return { error: null, user: data.user }; } } - + // ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ (๋“œ๋ฌผ๊ฒŒ ๋ฐœ์ƒ) - console.warn('ํšŒ์›๊ฐ€์ž… ์‘๋‹ต์€ ์„ฑ๊ณตํ–ˆ์ง€๋งŒ ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค'); - showAuthToast('ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต', '๊ณ„์ •์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฉ”์ผ ์ธ์ฆ์„ ์™„๋ฃŒํ•œ ํ›„ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”.', 'default'); - - return { - error: null, + authLogger.warn("ํšŒ์›๊ฐ€์ž… ์‘๋‹ต์€ ์„ฑ๊ณตํ–ˆ์ง€๋งŒ ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค"); + showAuthToast( + "ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต", + "๊ณ„์ •์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฉ”์ผ ์ธ์ฆ์„ ์™„๋ฃŒํ•œ ํ›„ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”.", + "default" + ); + + return { + error: null, user: { email }, - message: 'ํšŒ์›๊ฐ€์ž… ์™„๋ฃŒ', - emailConfirmationRequired: true + message: "ํšŒ์›๊ฐ€์ž… ์™„๋ฃŒ", + emailConfirmationRequired: true, }; } catch (error: any) { - console.error('ํšŒ์›๊ฐ€์ž… ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ:', error); - - const errorMessage = error.message || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'; - showAuthToast('ํšŒ์›๊ฐ€์ž… ์˜ค๋ฅ˜', errorMessage, 'destructive'); - + authLogger.error("ํšŒ์›๊ฐ€์ž… ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ:", error); + + const errorMessage = error.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."; + showAuthToast("ํšŒ์›๊ฐ€์ž… ์˜ค๋ฅ˜", errorMessage, "destructive"); + return { error: { message: errorMessage }, user: null }; } }; diff --git a/src/contexts/auth/types.ts b/src/contexts/auth/types.ts index 4019313..7340404 100644 --- a/src/contexts/auth/types.ts +++ b/src/contexts/auth/types.ts @@ -1,26 +1,53 @@ - -import { Models } from 'appwrite'; +import { Models } from "appwrite"; +import type { ApiError } from "@/types/common"; /** * Appwrite ์ดˆ๊ธฐํ™” ์ƒํƒœ ๋ฐ˜ํ™˜ ํƒ€์ž… */ -export type AppwriteInitializationStatus = { +export interface AppwriteInitializationStatus { isInitialized: boolean; error: Error | null; -}; +} + +/** + * ์ธ์ฆ ์‘๋‹ต ํƒ€์ž… + */ +export interface AuthResponse { + error: ApiError | null; + user?: Models.User; +} + +/** + * ํšŒ์›๊ฐ€์ž… ์‘๋‹ต ํƒ€์ž… + */ +export interface SignUpResponse { + error: ApiError | null; + user: Models.User | null; +} + +/** + * ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์‘๋‹ต ํƒ€์ž… + */ +export interface ResetPasswordResponse { + error: ApiError | null; +} /** * ์ธ์ฆ ์ปจํ…์ŠคํŠธ ํƒ€์ž… */ -export type AuthContextType = { +export interface AuthContextType { session: Models.Session | null; user: Models.User | null; loading: boolean; error: Error | null; appwriteInitialized: boolean; reinitializeAppwrite: () => AppwriteInitializationStatus; - signIn: (email: string, password: string) => Promise<{ error: any; user?: any }>; - signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any }>; + signIn: (email: string, password: string) => Promise; + signUp: ( + email: string, + password: string, + username: string + ) => Promise; signOut: () => Promise; - resetPassword: (email: string) => Promise<{ error: any }>; -}; + resetPassword: (email: string) => Promise; +} diff --git a/src/contexts/auth/useAuth.ts b/src/contexts/auth/useAuth.ts index 76fdaf4..f076a72 100644 --- a/src/contexts/auth/useAuth.ts +++ b/src/contexts/auth/useAuth.ts @@ -1,7 +1,6 @@ - -import { useContext } from 'react'; -import { AuthContext } from './AuthContext'; -import { AuthContextType } from './types'; +import { useContext } from "react"; +import { AuthContext } from "./AuthContext"; +import { AuthContextType } from "./types"; /** * ์ธ์ฆ ์ปจํ…์ŠคํŠธ์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•œ ์ปค์Šคํ…€ ํ›… @@ -10,7 +9,7 @@ import { AuthContextType } from './types'; export const useAuth = () => { const context = useContext(AuthContext); if (context === undefined) { - throw new Error('useAuth๋Š” AuthProvider ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค'); + throw new Error("useAuth๋Š” AuthProvider ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"); } return context; }; diff --git a/src/contexts/budget/BudgetContext.tsx b/src/contexts/budget/BudgetContext.tsx index ed57b56..e25084b 100644 --- a/src/contexts/budget/BudgetContext.tsx +++ b/src/contexts/budget/BudgetContext.tsx @@ -1,13 +1,14 @@ - -import React from 'react'; -import { useBudgetState } from './useBudgetState'; -import { BudgetContext, BudgetContextType } from './useBudget'; -import { BudgetPeriod, Transaction, BudgetData } from './types'; +import React from "react"; +import { useBudgetState } from "./useBudgetState"; +import { BudgetContext, BudgetContextType } from "./useBudget"; +import { BudgetPeriod, Transaction, BudgetData } from "./types"; // ์ปจํ…์ŠคํŠธ ํ”„๋กœ๋ฐ”์ด๋” ์ปดํฌ๋„ŒํŠธ -export const BudgetProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { +export const BudgetProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { const budgetState = useBudgetState(); - + return ( {children} @@ -16,8 +17,8 @@ export const BudgetProvider: React.FC<{ children: React.ReactNode }> = ({ childr }; // useBudget ํ›…์€ useBudget.ts ํŒŒ์ผ๋กœ ์ด๋™ํ–ˆ์Šต๋‹ˆ๋‹ค -export { useBudget } from './useBudget'; -export type { BudgetContextType } from './useBudget'; +export { useBudget } from "./useBudget"; +export type { BudgetContextType } from "./useBudget"; // types.ts์—์„œ ํƒ€์ž…๋“ค์„ export type์œผ๋กœ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค -export type { BudgetPeriod, Transaction, BudgetData } from './types'; +export type { BudgetPeriod, Transaction, BudgetData } from "./types"; diff --git a/src/contexts/budget/budgetUtils.ts b/src/contexts/budget/budgetUtils.ts index 3259846..7598164 100644 --- a/src/contexts/budget/budgetUtils.ts +++ b/src/contexts/budget/budgetUtils.ts @@ -1,24 +1,24 @@ - -import { BudgetData, Transaction } from './types'; -import { format } from 'date-fns'; +import { BudgetData, Transaction } from "./types"; +import { logger } from "@/utils/logger"; +import { format } from "date-fns"; // ๊ธฐ๋ณธ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ export const getInitialBudgetData = (): BudgetData => ({ daily: { targetAmount: 0, spentAmount: 0, - remainingAmount: 0 + remainingAmount: 0, }, weekly: { targetAmount: 0, spentAmount: 0, - remainingAmount: 0 + remainingAmount: 0, }, monthly: { targetAmount: 0, spentAmount: 0, - remainingAmount: 0 - } + remainingAmount: 0, + }, }); // ๊ธฐ๋ณธ ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๊ฐ’ (์ˆ˜์ถœ) @@ -31,7 +31,7 @@ export const safeStorage = { localStorage.setItem(key, JSON.stringify(value)); return true; } catch (err) { - console.error(`์Šคํ† ๋ฆฌ์ง€ ์ €์žฅ ์˜ค๋ฅ˜ (${key}):`, err); + logger.error(`์Šคํ† ๋ฆฌ์ง€ ์ €์žฅ ์˜ค๋ฅ˜ (${key}):`, err); return false; } }, @@ -40,56 +40,63 @@ export const safeStorage = { const item = localStorage.getItem(key); return item ? JSON.parse(item) : defaultValue; } catch (err) { - console.error(`์Šคํ† ๋ฆฌ์ง€ ๋กœ๋“œ ์˜ค๋ฅ˜ (${key}):`, err); + logger.error(`์Šคํ† ๋ฆฌ์ง€ ๋กœ๋“œ ์˜ค๋ฅ˜ (${key}):`, err); return defaultValue; } - } + }, }; // ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์Šคํ† ๋ฆฌ์ง€์—์„œ ๋กœ๋“œ export const safelyLoadBudgetData = (): BudgetData => { try { - const budgetDataStr = localStorage.getItem('budgetData'); - + const budgetDataStr = localStorage.getItem("budgetData"); + if (budgetDataStr) { const parsedData = JSON.parse(budgetDataStr); - + // ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ๊ฒ€์ฆ (ํ•„์š”ํ•œ ํ‚ค๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ) - if (parsedData && - typeof parsedData === 'object' && - 'daily' in parsedData && - 'weekly' in parsedData && - 'monthly' in parsedData) { + if ( + parsedData && + typeof parsedData === "object" && + "daily" in parsedData && + "weekly" in parsedData && + "monthly" in parsedData + ) { return parsedData; } else { - console.warn('์ €์žฅ๋œ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.'); + logger.warn( + "์ €์žฅ๋œ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค." + ); } } } catch (error) { - console.error('์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜:", error); } - + // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ๋˜๋Š” ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฐ์ดํ„ฐ์ธ ๊ฒฝ์šฐ ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜ return getInitialBudgetData(); }; // ์ง€์ถœ ๊ธˆ์•ก ๊ณ„์‚ฐ ํ•จ์ˆ˜ -export const calculateSpentAmounts = (transactions: Transaction[], budgetData: BudgetData): BudgetData => { +export const calculateSpentAmounts = ( + transactions: Transaction[], + budgetData: BudgetData +): BudgetData => { // ๊ธฐ์กด ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋ณต์‚ฌ const newBudgetData = JSON.parse(JSON.stringify(budgetData)); - + // ์ง€์ถœ ํŠธ๋žœ์žญ์…˜๋งŒ ํ•„ํ„ฐ๋ง - const expenseTransactions = transactions.filter(t => t.type === 'expense'); - + const expenseTransactions = transactions.filter((t) => t.type === "expense"); + // ํ˜„์žฌ ๋‚ ์งœ ์ •๋ณด const now = new Date(); const currentDay = now.getDate(); const currentMonth = now.getMonth(); const currentYear = now.getFullYear(); - const todayStr = format(now, 'yyyy-MM-dd'); - + const todayStr = format(now, "yyyy-MM-dd"); + // ์›”๊ฐ„ ์ง€์ถœ ํ•ฉ๊ณ„ (ํ˜„์žฌ ์›”์˜ ๋ชจ๋“  ์ง€์ถœ) - const monthlyExpenses = expenseTransactions.filter(t => { + const monthlyExpenses = expenseTransactions.filter((t) => { try { const transactionDate = new Date(t.date); return ( @@ -100,9 +107,9 @@ export const calculateSpentAmounts = (transactions: Transaction[], budgetData: B return false; // ๋‚ ์งœ ํŒŒ์‹ฑ ์˜ค๋ฅ˜ ์‹œ ํฌํ•จํ•˜์ง€ ์•Š์Œ } }); - + // ์ฃผ๊ฐ„ ์ง€์ถœ ํ•ฉ๊ณ„ (์ตœ๊ทผ 7์ผ) - const weeklyExpenses = expenseTransactions.filter(t => { + const weeklyExpenses = expenseTransactions.filter((t) => { try { const transactionDate = new Date(t.date); const diffTime = now.getTime() - transactionDate.getTime(); @@ -112,68 +119,90 @@ export const calculateSpentAmounts = (transactions: Transaction[], budgetData: B return false; } }); - + // ์ผ์ผ ์ง€์ถœ ํ•ฉ๊ณ„ (์˜ค๋Š˜) - const dailyExpenses = expenseTransactions.filter(t => { + const dailyExpenses = expenseTransactions.filter((t) => { try { - const transactionDateStr = format(new Date(t.date), 'yyyy-MM-dd'); + const transactionDateStr = format(new Date(t.date), "yyyy-MM-dd"); return transactionDateStr === todayStr; } catch (e) { return false; } }); - + // ๊ณ„์‚ฐ๋œ ์ง€์ถœ ๊ธˆ์•ก const dailyTotal = dailyExpenses.reduce((sum, t) => sum + t.amount, 0); const weeklyTotal = weeklyExpenses.reduce((sum, t) => sum + t.amount, 0); const monthlyTotal = monthlyExpenses.reduce((sum, t) => sum + t.amount, 0); - + // ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ์— ์ ์šฉ newBudgetData.daily.spentAmount = dailyTotal; - newBudgetData.daily.remainingAmount = Math.max(0, newBudgetData.daily.targetAmount - dailyTotal); - + newBudgetData.daily.remainingAmount = Math.max( + 0, + newBudgetData.daily.targetAmount - dailyTotal + ); + newBudgetData.weekly.spentAmount = weeklyTotal; - newBudgetData.weekly.remainingAmount = Math.max(0, newBudgetData.weekly.targetAmount - weeklyTotal); - + newBudgetData.weekly.remainingAmount = Math.max( + 0, + newBudgetData.weekly.targetAmount - weeklyTotal + ); + newBudgetData.monthly.spentAmount = monthlyTotal; - newBudgetData.monthly.remainingAmount = Math.max(0, newBudgetData.monthly.targetAmount - monthlyTotal); - + newBudgetData.monthly.remainingAmount = Math.max( + 0, + newBudgetData.monthly.targetAmount - monthlyTotal + ); + return newBudgetData; }; // ์˜ˆ์‚ฐ ๋ชฉํ‘œ ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ export const calculateUpdatedBudgetData = ( - budgetData: BudgetData, - type: 'daily' | 'weekly' | 'monthly', + budgetData: BudgetData, + type: "daily" | "weekly" | "monthly", amount: number ): BudgetData => { const newBudgetData = JSON.parse(JSON.stringify(budgetData)); - + // ์ƒˆ๋กœ์šด ์˜ˆ์‚ฐ ๋ชฉํ‘œ ์„ค์ • ๋ฐ ๋‚จ์€ ๊ธˆ์•ก ๊ณ„์‚ฐ newBudgetData[type].targetAmount = amount; - newBudgetData[type].remainingAmount = Math.max(0, amount - newBudgetData[type].spentAmount); - + newBudgetData[type].remainingAmount = Math.max( + 0, + amount - newBudgetData[type].spentAmount + ); + // ์›”๊ฐ„ ์˜ˆ์‚ฐ ๊ธฐ์ค€์œผ๋กœ ์ผ์ผ/์ฃผ๊ฐ„ ์˜ˆ์‚ฐ ์ž๋™ ๊ณ„์‚ฐ (์›”๊ฐ„ ์˜ˆ์‚ฐ์ด ์„ค์ •๋œ ๊ฒฝ์šฐ๋งŒ) - if (type === 'monthly' && amount > 0) { + if (type === "monthly" && amount > 0) { // ํ˜„์žฌ ๋‚ ์งœ ๊ธฐ์ค€์œผ๋กœ ์ด๋ฒˆ ๋‹ฌ ๋‚จ์€ ์ผ์ˆ˜ ๊ณ„์‚ฐ const today = new Date(); - const lastDayOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate(); + const lastDayOfMonth = new Date( + today.getFullYear(), + today.getMonth() + 1, + 0 + ).getDate(); const remainingDays = lastDayOfMonth - today.getDate() + 1; - + // ์ผ์ผ ์˜ˆ์‚ฐ ๊ณ„์‚ฐ (๋‚จ์€ ๊ธˆ์•ก / ๋‚จ์€ ์ผ์ˆ˜) if (newBudgetData.daily.targetAmount === 0) { const dailyBudget = Math.round(amount / lastDayOfMonth); newBudgetData.daily.targetAmount = dailyBudget; - newBudgetData.daily.remainingAmount = Math.max(0, dailyBudget - newBudgetData.daily.spentAmount); + newBudgetData.daily.remainingAmount = Math.max( + 0, + dailyBudget - newBudgetData.daily.spentAmount + ); } - + // ์ฃผ๊ฐ„ ์˜ˆ์‚ฐ ๊ณ„์‚ฐ (์›”๊ฐ„ ์˜ˆ์‚ฐ / 4.3์ฃผ) if (newBudgetData.weekly.targetAmount === 0) { const weeklyBudget = Math.round(amount / 4.3); newBudgetData.weekly.targetAmount = weeklyBudget; - newBudgetData.weekly.remainingAmount = Math.max(0, weeklyBudget - newBudgetData.weekly.spentAmount); + newBudgetData.weekly.remainingAmount = Math.max( + 0, + weeklyBudget - newBudgetData.weekly.spentAmount + ); } } - + return newBudgetData; }; diff --git a/src/contexts/budget/hooks/useBudgetBackup.ts b/src/contexts/budget/hooks/useBudgetBackup.ts index 24da7d3..01f3811 100644 --- a/src/contexts/budget/hooks/useBudgetBackup.ts +++ b/src/contexts/budget/hooks/useBudgetBackup.ts @@ -1,44 +1,47 @@ - -import { useEffect } from 'react'; -import { BudgetData, Transaction } from '../types'; +import { useEffect } from "react"; +import { logger } from "@/utils/logger"; +import { BudgetData, Transaction } from "../types"; // ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ž๋™ ๋ฐฑ์—… ํ›… export const useBudgetBackup = ( - budgetData: BudgetData, - categoryBudgets: Record, + budgetData: BudgetData, + categoryBudgets: Record, transactions: Transaction[] ) => { // ๋””๋ฒ„๊น… ๋ฐ ์ž๋™ ๋ฐฑ์—…์„ ์œ„ํ•œ ํšจ๊ณผ useEffect(() => { - console.log('BudgetState ํ›… - ํ˜„์žฌ ์ƒํƒœ:'); - console.log('- ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:', budgetData); - console.log('- ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ:', categoryBudgets); - console.log('- ํŠธ๋žœ์žญ์…˜ ์ˆ˜:', transactions.length); - + logger.info("BudgetState ํ›… - ํ˜„์žฌ ์ƒํƒœ:"); + logger.info("- ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:", budgetData); + logger.info("- ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ:", categoryBudgets); + logger.info("- ํŠธ๋žœ์žญ์…˜ ์ˆ˜:", transactions.length); + // ๋ฐ์ดํ„ฐ ์†์‹ค ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ํƒ€์ด๋จธ ์„ค์ • const saveTimer = setInterval(() => { // ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ฐ ๋ฐฑ์—… try { - const storedBudgetData = localStorage.getItem('budgetData'); - const storedCategoryBudgets = localStorage.getItem('categoryBudgets'); - const storedTransactions = localStorage.getItem('transactions'); - + const storedBudgetData = localStorage.getItem("budgetData"); + const storedCategoryBudgets = localStorage.getItem("categoryBudgets"); + const storedTransactions = localStorage.getItem("transactions"); + if (storedBudgetData) { - localStorage.setItem('budgetData_backup_auto', storedBudgetData); + localStorage.setItem("budgetData_backup_auto", storedBudgetData); } - + if (storedCategoryBudgets) { - localStorage.setItem('categoryBudgets_backup_auto', storedCategoryBudgets); + localStorage.setItem( + "categoryBudgets_backup_auto", + storedCategoryBudgets + ); } - + if (storedTransactions) { - localStorage.setItem('transactions_backup_auto', storedTransactions); + localStorage.setItem("transactions_backup_auto", storedTransactions); } } catch (error) { - console.error('์ž๋™ ๋ฐฑ์—… ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("์ž๋™ ๋ฐฑ์—… ์ค‘ ์˜ค๋ฅ˜:", error); } }, 5000); // 5์ดˆ๋งˆ๋‹ค ๋ฐฑ์—… - + return () => { clearInterval(saveTimer); }; diff --git a/src/contexts/budget/hooks/useBudgetDataEvents.ts b/src/contexts/budget/hooks/useBudgetDataEvents.ts index d2b7657..50793d3 100644 --- a/src/contexts/budget/hooks/useBudgetDataEvents.ts +++ b/src/contexts/budget/hooks/useBudgetDataEvents.ts @@ -1,7 +1,7 @@ - -import { useEffect } from 'react'; -import { Transaction, BudgetData } from '../types'; -import { calculateSpentAmounts } from '../utils/spendingCalculation'; +import { useEffect } from "react"; +import { logger } from "@/utils/logger"; +import { Transaction, BudgetData } from "../types"; +import { calculateSpentAmounts } from "../utils/spendingCalculation"; export const useBudgetDataEvents = ( isInitialized: boolean, @@ -11,111 +11,140 @@ export const useBudgetDataEvents = ( ) => { // ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ useEffect(() => { - if (!isInitialized || transactions.length === 0) return; - - console.log('ํŠธ๋žœ์žญ์…˜ ๋ณ€๊ฒฝ ๊ฐ์ง€, ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ์ค‘...', transactions.length); - - setBudgetData(prevBudgetData => { + if (!isInitialized || transactions.length === 0) { + return; + } + + logger.info( + "ํŠธ๋žœ์žญ์…˜ ๋ณ€๊ฒฝ ๊ฐ์ง€, ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ์ค‘...", + transactions.length + ); + + setBudgetData((prevBudgetData) => { // ํ˜„์žฌ ์ง€์ถœ ๊ธˆ์•ก ๊ณ„์‚ฐ - const updatedBudgetData = calculateSpentAmounts(transactions, prevBudgetData); - + const updatedBudgetData = calculateSpentAmounts( + transactions, + prevBudgetData + ); + // ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ ์‹œ๊ฐ„ ๊ธฐ๋ก setLastUpdateTime(Date.now()); - - console.log('์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ:', updatedBudgetData); + + logger.info("์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ:", updatedBudgetData); return updatedBudgetData; }); }, [isInitialized, transactions, setBudgetData, setLastUpdateTime]); // ํŠธ๋žœ์žญ์…˜ ๋ณ€๊ฒฝ ๊ฐ์ง€ ์ด๋ฒคํŠธ useEffect(() => { - if (!isInitialized) return; - + if (!isInitialized) { + return; + } + const handleTransactionChanged = (event: CustomEvent) => { - console.log('ํŠธ๋žœ์žญ์…˜ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ๊ฐ์ง€', event.detail); - - setBudgetData(prevBudgetData => { + logger.info("ํŠธ๋žœ์žญ์…˜ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ๊ฐ์ง€", event.detail); + + setBudgetData((prevBudgetData) => { // ํ˜„์žฌ ์ง€์ถœ ๊ธˆ์•ก ๊ณ„์‚ฐ - const updatedBudgetData = calculateSpentAmounts(transactions, prevBudgetData); - + const updatedBudgetData = calculateSpentAmounts( + transactions, + prevBudgetData + ); + // ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ ์‹œ๊ฐ„ ๊ธฐ๋ก setLastUpdateTime(Date.now()); - + return updatedBudgetData; }); }; - + // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก - window.addEventListener('transactionChanged', handleTransactionChanged as EventListener); - + window.addEventListener( + "transactionChanged", + handleTransactionChanged as EventListener + ); + return () => { // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ œ๊ฑฐ - window.removeEventListener('transactionChanged', handleTransactionChanged as EventListener); + window.removeEventListener( + "transactionChanged", + handleTransactionChanged as EventListener + ); }; }, [isInitialized, transactions, setBudgetData, setLastUpdateTime]); - + // ์Šคํ† ๋ฆฌ์ง€ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ (๋‹ค๋ฅธ ํƒญ์—์„œ๋„ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”) useEffect(() => { - if (!isInitialized) return; - + if (!isInitialized) { + return; + } + const handleStorageChange = (event: StorageEvent) => { - if (event.key === 'transactions') { - console.log('์Šคํ† ๋ฆฌ์ง€ ์ด๋ฒคํŠธ ๊ฐ์ง€: ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ'); - + if (event.key === "transactions") { + logger.info("์Šคํ† ๋ฆฌ์ง€ ์ด๋ฒคํŠธ ๊ฐ์ง€: ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ"); + try { // ์ƒˆ ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ๋กœ ์˜ˆ์‚ฐ ๊ณ„์‚ฐ if (event.newValue) { const newTransactions = JSON.parse(event.newValue); - - setBudgetData(prevBudgetData => { + + setBudgetData((prevBudgetData) => { // ํ˜„์žฌ ์ง€์ถœ ๊ธˆ์•ก ๊ณ„์‚ฐ - const updatedBudgetData = calculateSpentAmounts(newTransactions, prevBudgetData); - + const updatedBudgetData = calculateSpentAmounts( + newTransactions, + prevBudgetData + ); + // ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ ์‹œ๊ฐ„ ๊ธฐ๋ก setLastUpdateTime(Date.now()); - + return updatedBudgetData; }); } } catch (error) { - console.error('์Šคํ† ๋ฆฌ์ง€ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("์Šคํ† ๋ฆฌ์ง€ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜:", error); } } }; - + // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก - window.addEventListener('storage', handleStorageChange); - + window.addEventListener("storage", handleStorageChange); + return () => { // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ œ๊ฑฐ - window.removeEventListener('storage', handleStorageChange); + window.removeEventListener("storage", handleStorageChange); }; }, [isInitialized, setBudgetData, setLastUpdateTime]); - - // ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ˆ˜๋™ ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ + + // ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ˆ˜๋™ ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ useEffect(() => { - if (!isInitialized) return; - + if (!isInitialized) { + return; + } + const handleBudgetRefresh = () => { - console.log('์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ˆ˜๋™ ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๊ฐ์ง€'); - - setBudgetData(prevBudgetData => { + logger.info("์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ˆ˜๋™ ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๊ฐ์ง€"); + + setBudgetData((prevBudgetData) => { // ํ˜„์žฌ ์ง€์ถœ ๊ธˆ์•ก ๊ณ„์‚ฐ - const updatedBudgetData = calculateSpentAmounts(transactions, prevBudgetData); - + const updatedBudgetData = calculateSpentAmounts( + transactions, + prevBudgetData + ); + // ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ ์‹œ๊ฐ„ ๊ธฐ๋ก setLastUpdateTime(Date.now()); - + return updatedBudgetData; }); }; - + // ์ˆ˜๋™ ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋“ฑ๋ก - window.addEventListener('budgetDataRefresh', handleBudgetRefresh); - + window.addEventListener("budgetDataRefresh", handleBudgetRefresh); + return () => { // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ œ๊ฑฐ - window.removeEventListener('budgetDataRefresh', handleBudgetRefresh); + window.removeEventListener("budgetDataRefresh", handleBudgetRefresh); }; }, [isInitialized, transactions, setBudgetData, setLastUpdateTime]); }; diff --git a/src/contexts/budget/hooks/useBudgetDataLoad.ts b/src/contexts/budget/hooks/useBudgetDataLoad.ts index ad59ee7..d86b635 100644 --- a/src/contexts/budget/hooks/useBudgetDataLoad.ts +++ b/src/contexts/budget/hooks/useBudgetDataLoad.ts @@ -1,7 +1,7 @@ - -import { useEffect } from 'react'; -import { BudgetData } from '../types'; -import { safelyLoadBudgetData } from '../budgetUtils'; +import { useEffect } from "react"; +import { logger } from "@/utils/logger"; +import { BudgetData } from "../types"; +import { safelyLoadBudgetData } from "../budgetUtils"; /** * ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ๋ฐ ์ดˆ๊ธฐํ™”๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ํ›… @@ -18,28 +18,28 @@ export const useBudgetDataLoad = ( // ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ํ•จ์ˆ˜ - ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋กœ ๋ณ€๊ฒฝ const loadBudget = async () => { try { - console.log('์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹œ๋„ ์ค‘...'); - + logger.info("์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹œ๋„ ์ค‘..."); + // ๋น„๋™๊ธฐ ์ž‘์—…์„ ๋งˆ์ดํฌ๋กœํƒœ์Šคํฌ๋กœ ์ง€์—ฐ - await new Promise(resolve => setTimeout(() => resolve(), 0)); - + await new Promise((resolve) => setTimeout(() => resolve(), 0)); + // ์•ˆ์ „ํ•˜๊ฒŒ ๋ฐ์ดํ„ฐ ๋กœ๋“œ const loadedData = safelyLoadBudgetData(); - console.log('๋กœ๋“œ๋œ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:', JSON.stringify(loadedData)); - + logger.info("๋กœ๋“œ๋œ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:", JSON.stringify(loadedData)); + // ์ƒˆ๋กœ ๋กœ๋“œํ•œ ๋ฐ์ดํ„ฐ์™€ ํ˜„์žฌ ๋ฐ์ดํ„ฐ๊ฐ€ ๋‹ค๋ฅผ ๋•Œ๋งŒ ์—…๋ฐ์ดํŠธ if (JSON.stringify(loadedData) !== JSON.stringify(budgetData)) { - console.log('์ƒˆ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๊ฐ์ง€๋จ, ์ƒํƒœ ์—…๋ฐ์ดํŠธ'); + logger.info("์ƒˆ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๊ฐ์ง€๋จ, ์ƒํƒœ ์—…๋ฐ์ดํŠธ"); setBudgetData(loadedData); setLastUpdateTime(Date.now()); } - + // ์ดˆ๊ธฐํ™” ์ƒํƒœ ์—…๋ฐ์ดํŠธ if (!isInitialized) { setIsInitialized(true); } } catch (error) { - console.error('์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜:", error); // ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ์•ฑ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ ์„ค์ • setIsInitialized(true); } @@ -62,9 +62,9 @@ export const useBudgetDataLoad = ( } return loadedData; } catch (error) { - console.error('loadBudgetData ํ˜ธ์ถœ ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("loadBudgetData ํ˜ธ์ถœ ์ค‘ ์˜ค๋ฅ˜:", error); return budgetData; // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ํ˜„์žฌ ๋ฐ์ดํ„ฐ ์œ ์ง€ } - } + }, }; }; diff --git a/src/contexts/budget/hooks/useBudgetDataState.ts b/src/contexts/budget/hooks/useBudgetDataState.ts index 7b49682..783fdf3 100644 --- a/src/contexts/budget/hooks/useBudgetDataState.ts +++ b/src/contexts/budget/hooks/useBudgetDataState.ts @@ -1,12 +1,12 @@ - -import { useEffect } from 'react'; -import { BudgetData, BudgetPeriod, Transaction } from '../types'; -import { clearAllBudgetData } from '../storage'; -import { useBudgetState } from './useBudgetState'; -import { useBudgetDataLoad } from './useBudgetDataLoad'; -import { useBudgetDataEvents } from './useBudgetDataEvents'; -import { useBudgetGoalUpdate } from './useBudgetGoalUpdate'; -import { toast } from '@/hooks/useToast.wrapper'; +import { useEffect } from "react"; +import { logger } from "@/utils/logger"; +import { BudgetData, BudgetPeriod, Transaction } from "../types"; +import { clearAllBudgetData } from "../storage"; +import { useBudgetState } from "./useBudgetState"; +import { useBudgetDataLoad } from "./useBudgetDataLoad"; +import { useBudgetDataEvents } from "./useBudgetDataEvents"; +import { useBudgetGoalUpdate } from "./useBudgetGoalUpdate"; +import { toast } from "@/hooks/useToast.wrapper"; // ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ƒํƒœ ๊ด€๋ฆฌ ํ›… export const useBudgetDataState = (transactions: Transaction[]) => { @@ -19,7 +19,7 @@ export const useBudgetDataState = (transactions: Transaction[]) => { isInitialized, setIsInitialized, lastUpdateTime, - setLastUpdateTime + setLastUpdateTime, } = useBudgetState(); // ๋ฐ์ดํ„ฐ ๋กœ๋“œ ๊ธฐ๋Šฅ @@ -48,27 +48,32 @@ export const useBudgetDataState = (transactions: Transaction[]) => { // ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜ const resetBudgetData = () => { try { - console.log('์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”'); + logger.info("์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”"); clearAllBudgetData(); - + // ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€์—์„œ ๋‹ค์‹œ ๋กœ๋“œ loadBudgetData(); - + // ์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œ์ผœ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์— ์•Œ๋ฆผ - window.dispatchEvent(new Event('budgetDataUpdated')); + window.dispatchEvent(new Event("budgetDataUpdated")); } catch (error) { - console.error('์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜:", error); toast({ title: "์˜ˆ์‚ฐ ์ดˆ๊ธฐํ™” ์‹คํŒจ", description: "์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋Š”๋ฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" + variant: "destructive", }); } }; // ๋””๋ฒ„๊น… ๋กœ๊ทธ ์ถ”๊ฐ€ useEffect(() => { - console.log('์ตœ์‹  ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:', JSON.stringify(budgetData), '๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ:', new Date(lastUpdateTime).toISOString()); + logger.info( + "์ตœ์‹  ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:", + JSON.stringify(budgetData), + "๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ:", + new Date(lastUpdateTime).toISOString() + ); }, [budgetData, lastUpdateTime]); return { @@ -76,6 +81,6 @@ export const useBudgetDataState = (transactions: Transaction[]) => { selectedTab, setSelectedTab, handleBudgetGoalUpdate, - resetBudgetData + resetBudgetData, }; }; diff --git a/src/contexts/budget/hooks/useBudgetGoalUpdate.ts b/src/contexts/budget/hooks/useBudgetGoalUpdate.ts index 568c73a..8428cad 100644 --- a/src/contexts/budget/hooks/useBudgetGoalUpdate.ts +++ b/src/contexts/budget/hooks/useBudgetGoalUpdate.ts @@ -1,9 +1,12 @@ - -import { useCallback } from 'react'; -import { BudgetData, BudgetPeriod } from '../types'; -import { calculateUpdatedBudgetData, safelyLoadBudgetData } from '../budgetUtils'; -import { saveBudgetDataToStorage } from '../storage'; -import { toast } from '@/hooks/useToast.wrapper'; +import { useCallback } from "react"; +import { logger } from "@/utils/logger"; +import { BudgetData, BudgetPeriod } from "../types"; +import { + calculateUpdatedBudgetData, + safelyLoadBudgetData, +} from "../budgetUtils"; +import { saveBudgetDataToStorage } from "../storage"; +import { toast } from "@/hooks/useToast.wrapper"; /** * ์˜ˆ์‚ฐ ๋ชฉํ‘œ ์—…๋ฐ์ดํŠธ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ํ›… @@ -13,65 +16,72 @@ export const useBudgetGoalUpdate = ( setLastUpdateTime: (time: number) => void ) => { // ์˜ˆ์‚ฐ ๋ชฉํ‘œ ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ - const handleBudgetGoalUpdate = useCallback(( - type: BudgetPeriod, - amount: number - ) => { - try { - console.log(`์˜ˆ์‚ฐ ๋ชฉํ‘œ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${type}, ๊ธˆ์•ก: ${amount}`); - - // ๊ธˆ์•ก์ด ์œ ํšจํ•œ์ง€ ํ™•์ธ - if (isNaN(amount) || amount <= 0) { - console.error('์œ ํšจํ•˜์ง€ ์•Š์€ ์˜ˆ์‚ฐ ๊ธˆ์•ก:', amount); + const handleBudgetGoalUpdate = useCallback( + (type: BudgetPeriod, amount: number) => { + try { + logger.info(`์˜ˆ์‚ฐ ๋ชฉํ‘œ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${type}, ๊ธˆ์•ก: ${amount}`); + + // ๊ธˆ์•ก์ด ์œ ํšจํ•œ์ง€ ํ™•์ธ + if (isNaN(amount) || amount <= 0) { + logger.error("์œ ํšจํ•˜์ง€ ์•Š์€ ์˜ˆ์‚ฐ ๊ธˆ์•ก:", amount); + toast({ + title: "์˜ˆ์‚ฐ ์„ค์ • ์˜ค๋ฅ˜", + description: "์œ ํšจํ•œ ์˜ˆ์‚ฐ ๊ธˆ์•ก์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.", + variant: "destructive", + }); + return; + } + + // ํ˜„์žฌ ์ตœ์‹  ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ (๋‹ค๋ฅธ ๊ณณ์—์„œ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ์ˆ˜ ์žˆ์Œ) + const currentBudgetData = safelyLoadBudgetData(); + + // ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ - ์›”๊ฐ„ ๊ธฐ์ค€์œผ๋กœ ๊ณ„์‚ฐ + const updatedBudgetData = calculateUpdatedBudgetData( + currentBudgetData, + type, + amount + ); + logger.info("์ƒˆ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๊ณ„์‚ฐ๋จ:", updatedBudgetData); + + // ์ƒํƒœ ๋ฐ ์Šคํ† ๋ฆฌ์ง€ ๋‘˜ ๋‹ค ์—…๋ฐ์ดํŠธ + setBudgetData(updatedBudgetData); + saveBudgetDataToStorage(updatedBudgetData); + setLastUpdateTime(Date.now()); + + // ์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œ์ผœ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์— ์•Œ๋ฆผ + window.dispatchEvent(new Event("budgetDataUpdated")); + + // ์‹œ๊ฐ„์ฐจ๋ฅผ ๋‘๊ณ  ์ด๋ฒคํŠธ ์ถ”๊ฐ€ ๋ฐœ์ƒ (UI ๊ฐฑ์‹  ํ™•์‹คํžˆ ํ•˜๊ธฐ ์œ„ํ•จ) + setTimeout(() => { + window.dispatchEvent(new Event("budgetDataUpdated")); + window.dispatchEvent( + new StorageEvent("storage", { + key: "budgetData", + newValue: JSON.stringify(updatedBudgetData), + }) + ); + }, 500); + + logger.info("์˜ˆ์‚ฐ ๋ชฉํ‘œ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ:", updatedBudgetData); + + // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ + const periodText = + type === "daily" ? "์ผ์ผ" : type === "weekly" ? "์ฃผ๊ฐ„" : "์›”๊ฐ„"; toast({ - title: "์˜ˆ์‚ฐ ์„ค์ • ์˜ค๋ฅ˜", - description: "์œ ํšจํ•œ ์˜ˆ์‚ฐ ๊ธˆ์•ก์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.", - variant: "destructive" + title: "์˜ˆ์‚ฐ ์„ค์ • ์™„๋ฃŒ", + description: `${periodText} ์˜ˆ์‚ฐ์ด ${amount.toLocaleString()}์›์œผ๋กœ ์„ค์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, + }); + } catch (error) { + logger.error("์˜ˆ์‚ฐ ๋ชฉํ‘œ ์—…๋ฐ์ดํŠธ ์ค‘ ์˜ค๋ฅ˜:", error); + toast({ + title: "์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ ์‹คํŒจ", + description: "์˜ˆ์‚ฐ ๋ชฉํ‘œ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š”๋ฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + variant: "destructive", }); - return; } - - // ํ˜„์žฌ ์ตœ์‹  ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ (๋‹ค๋ฅธ ๊ณณ์—์„œ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ์ˆ˜ ์žˆ์Œ) - const currentBudgetData = safelyLoadBudgetData(); - - // ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ - ์›”๊ฐ„ ๊ธฐ์ค€์œผ๋กœ ๊ณ„์‚ฐ - const updatedBudgetData = calculateUpdatedBudgetData(currentBudgetData, type, amount); - console.log('์ƒˆ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๊ณ„์‚ฐ๋จ:', updatedBudgetData); - - // ์ƒํƒœ ๋ฐ ์Šคํ† ๋ฆฌ์ง€ ๋‘˜ ๋‹ค ์—…๋ฐ์ดํŠธ - setBudgetData(updatedBudgetData); - saveBudgetDataToStorage(updatedBudgetData); - setLastUpdateTime(Date.now()); - - // ์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œ์ผœ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์— ์•Œ๋ฆผ - window.dispatchEvent(new Event('budgetDataUpdated')); - - // ์‹œ๊ฐ„์ฐจ๋ฅผ ๋‘๊ณ  ์ด๋ฒคํŠธ ์ถ”๊ฐ€ ๋ฐœ์ƒ (UI ๊ฐฑ์‹  ํ™•์‹คํžˆ ํ•˜๊ธฐ ์œ„ํ•จ) - setTimeout(() => { - window.dispatchEvent(new Event('budgetDataUpdated')); - window.dispatchEvent(new StorageEvent('storage', { - key: 'budgetData', - newValue: JSON.stringify(updatedBudgetData) - })); - }, 500); - - console.log('์˜ˆ์‚ฐ ๋ชฉํ‘œ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ:', updatedBudgetData); - - // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ - const periodText = type === 'daily' ? '์ผ์ผ' : type === 'weekly' ? '์ฃผ๊ฐ„' : '์›”๊ฐ„'; - toast({ - title: "์˜ˆ์‚ฐ ์„ค์ • ์™„๋ฃŒ", - description: `${periodText} ์˜ˆ์‚ฐ์ด ${amount.toLocaleString()}์›์œผ๋กœ ์„ค์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, - }); - } catch (error) { - console.error('์˜ˆ์‚ฐ ๋ชฉํ‘œ ์—…๋ฐ์ดํŠธ ์ค‘ ์˜ค๋ฅ˜:', error); - toast({ - title: "์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ ์‹คํŒจ", - description: "์˜ˆ์‚ฐ ๋ชฉํ‘œ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š”๋ฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" - }); - } - }, []); + }, + [] + ); return handleBudgetGoalUpdate; }; diff --git a/src/contexts/budget/hooks/useBudgetReset.ts b/src/contexts/budget/hooks/useBudgetReset.ts index d9a6f91..de80d9d 100644 --- a/src/contexts/budget/hooks/useBudgetReset.ts +++ b/src/contexts/budget/hooks/useBudgetReset.ts @@ -1,6 +1,6 @@ - -import { useCallback } from 'react'; -import { toast } from '@/components/ui/use-toast'; +import { useCallback } from "react"; +import { logger } from "@/utils/logger"; +import { toast } from "@/components/ui/use-toast"; // ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋ฆฌ์…‹ ํ›… export const useBudgetReset = ( @@ -11,40 +11,40 @@ export const useBudgetReset = ( // ๋ชจ๋“  ๋ฐ์ดํ„ฐ ๋ฆฌ์…‹ ํ•จ์ˆ˜ const resetBudgetData = useCallback(() => { try { - console.log('BudgetContext์—์„œ ๋ฐ์ดํ„ฐ ๋ฆฌ์…‹ ์‹œ์ž‘'); - + logger.info("BudgetContext์—์„œ ๋ฐ์ดํ„ฐ ๋ฆฌ์…‹ ์‹œ์ž‘"); + // ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ˆœ์„œ ์ค‘์š” resetTransactions(); resetCategoryBudgets(); resetBudgetDataInternal(); - + // ์ด๋ฒคํŠธ ๋ฐœ์ƒ try { - window.dispatchEvent(new Event('transactionUpdated')); - window.dispatchEvent(new Event('budgetDataUpdated')); - window.dispatchEvent(new Event('categoryBudgetsUpdated')); - window.dispatchEvent(new StorageEvent('storage')); + window.dispatchEvent(new Event("transactionUpdated")); + window.dispatchEvent(new Event("budgetDataUpdated")); + window.dispatchEvent(new Event("categoryBudgetsUpdated")); + window.dispatchEvent(new StorageEvent("storage")); } catch (e) { - console.error('์ด๋ฒคํŠธ ๋ฐœ์ƒ ์˜ค๋ฅ˜:', e); + logger.error("์ด๋ฒคํŠธ ๋ฐœ์ƒ ์˜ค๋ฅ˜:", e); } - - console.log('BudgetContext์—์„œ ๋ฐ์ดํ„ฐ ๋ฆฌ์…‹ ์™„๋ฃŒ'); - + + logger.info("BudgetContext์—์„œ ๋ฐ์ดํ„ฐ ๋ฆฌ์…‹ ์™„๋ฃŒ"); + // ํ† ์ŠคํŠธ ์•Œ๋ฆผ toast({ title: "๋ชจ๋“  ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”", description: "์˜ˆ์‚ฐ๊ณผ ์ง€์ถœ ๋‚ด์—ญ์ด ๋ชจ๋‘ ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", }); - + return true; } catch (error) { - console.error('๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜:", error); toast({ title: "์ดˆ๊ธฐํ™” ์‹คํŒจ", description: "๋ฐ์ดํ„ฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" + variant: "destructive", }); - + return false; } }, [resetTransactions, resetCategoryBudgets, resetBudgetDataInternal]); diff --git a/src/contexts/budget/hooks/useBudgetState.ts b/src/contexts/budget/hooks/useBudgetState.ts index 5b5c0f2..8144405 100644 --- a/src/contexts/budget/hooks/useBudgetState.ts +++ b/src/contexts/budget/hooks/useBudgetState.ts @@ -1,7 +1,7 @@ - -import { useState } from 'react'; -import { BudgetData, BudgetPeriod } from '../types'; -import { safelyLoadBudgetData } from '../budgetUtils'; +import { useState } from "react"; +import { logger } from "@/utils/logger"; +import { BudgetData, BudgetPeriod } from "../types"; +import { safelyLoadBudgetData } from "../budgetUtils"; /** * ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ์˜ ๊ธฐ๋ณธ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ํ›… @@ -9,8 +9,8 @@ import { safelyLoadBudgetData } from '../budgetUtils'; export const useBudgetState = () => { // ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹œ safelyLoadBudgetData ํ•จ์ˆ˜ ์‚ฌ์šฉ const initialBudgetData = safelyLoadBudgetData(); - console.log('์ดˆ๊ธฐ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ:', initialBudgetData); - + logger.info("์ดˆ๊ธฐ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ:", initialBudgetData); + const [budgetData, setBudgetData] = useState(initialBudgetData); const [selectedTab, setSelectedTab] = useState("monthly"); // ์ดˆ๊ธฐ๊ฐ’์€ monthly๋กœ ๋ณ€๊ฒฝ const [isInitialized, setIsInitialized] = useState(false); @@ -24,6 +24,6 @@ export const useBudgetState = () => { isInitialized, setIsInitialized, lastUpdateTime, - setLastUpdateTime + setLastUpdateTime, }; }; diff --git a/src/contexts/budget/hooks/useCategoryBudgetState.ts b/src/contexts/budget/hooks/useCategoryBudgetState.ts index 498746b..51ca141 100644 --- a/src/contexts/budget/hooks/useCategoryBudgetState.ts +++ b/src/contexts/budget/hooks/useCategoryBudgetState.ts @@ -1,116 +1,133 @@ - -import { useState, useEffect, useCallback } from 'react'; -import { - loadCategoryBudgetsFromStorage, - saveCategoryBudgetsToStorage, - clearAllCategoryBudgets -} from '../storage'; +import { useState, useEffect, useCallback } from "react"; +import { logger } from "@/utils/logger"; +import { + loadCategoryBudgetsFromStorage, + saveCategoryBudgetsToStorage, + clearAllCategoryBudgets, +} from "../storage"; // ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ƒํƒœ ๊ด€๋ฆฌ ํ›… export const useCategoryBudgetState = () => { - const [categoryBudgets, setCategoryBudgets] = useState>( - loadCategoryBudgetsFromStorage() - ); + const [categoryBudgets, setCategoryBudgets] = useState< + Record + >(loadCategoryBudgetsFromStorage()); const [isInitialized, setIsInitialized] = useState(false); // ์ดˆ๊ธฐ ๋กœ๋“œ ๋ฐ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์„ค์ • useEffect(() => { const loadCategories = () => { try { - console.log('์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋กœ๋“œ ์‹œ๋„ ์ค‘...'); + logger.info("์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋กœ๋“œ ์‹œ๋„ ์ค‘..."); const loaded = loadCategoryBudgetsFromStorage(); - console.log('์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋กœ๋“œ๋จ:', loaded); + logger.info("์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋กœ๋“œ๋จ:", loaded); setCategoryBudgets(loaded); - + // ์ตœ๊ทผ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹œ๊ฐ„ ๊ธฐ๋ก - localStorage.setItem('lastCategoryBudgetLoadTime', new Date().toISOString()); - + localStorage.setItem( + "lastCategoryBudgetLoadTime", + new Date().toISOString() + ); + if (!isInitialized) { setIsInitialized(true); } } catch (error) { - console.error('์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜:", error); } }; - + // ์ดˆ๊ธฐ ๋กœ๋“œ loadCategories(); - + // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์„ค์ • const handleCategoryUpdate = (e?: StorageEvent) => { - console.log('์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ ์ด๋ฒคํŠธ ๊ฐ์ง€:', e?.key); - if (!e || e.key === 'categoryBudgets' || e.key === null) { + logger.info("์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ ์ด๋ฒคํŠธ ๊ฐ์ง€:", e?.key); + if (!e || e.key === "categoryBudgets" || e.key === null) { loadCategories(); } }; - - window.addEventListener('categoryBudgetsUpdated', () => handleCategoryUpdate()); - window.addEventListener('storage', handleCategoryUpdate); - window.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'visible') { - console.log('ํŽ˜์ด์ง€ ๋ณด์ž„: ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ƒˆ๋กœ๊ณ ์นจ'); + + window.addEventListener("categoryBudgetsUpdated", () => + handleCategoryUpdate() + ); + window.addEventListener("storage", handleCategoryUpdate); + window.addEventListener("visibilitychange", () => { + if (document.visibilityState === "visible") { + logger.info("ํŽ˜์ด์ง€ ๋ณด์ž„: ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ƒˆ๋กœ๊ณ ์นจ"); loadCategories(); } }); - window.addEventListener('focus', () => { - console.log('์ฐฝ ํฌ์ปค์Šค: ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ƒˆ๋กœ๊ณ ์นจ'); + window.addEventListener("focus", () => { + logger.info("์ฐฝ ํฌ์ปค์Šค: ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ƒˆ๋กœ๊ณ ์นจ"); loadCategories(); }); - + // ์ฃผ๊ธฐ์  ๋ฐ์ดํ„ฐ ๊ฒ€์‚ฌ const intervalId = setInterval(() => { - const lastSaveTime = localStorage.getItem('lastCategoryBudgetSaveTime'); - const lastLoadTime = localStorage.getItem('lastCategoryBudgetLoadTime'); - - if (lastSaveTime && lastLoadTime && new Date(lastSaveTime) > new Date(lastLoadTime)) { - console.log('์ƒˆ๋กœ์šด ์นดํ…Œ๊ณ ๋ฆฌ ์ €์žฅ ๊ฐ์ง€๋จ, ๋ฐ์ดํ„ฐ ๋‹ค์‹œ ๋กœ๋“œ...'); + const lastSaveTime = localStorage.getItem("lastCategoryBudgetSaveTime"); + const lastLoadTime = localStorage.getItem("lastCategoryBudgetLoadTime"); + + if ( + lastSaveTime && + lastLoadTime && + new Date(lastSaveTime) > new Date(lastLoadTime) + ) { + logger.info("์ƒˆ๋กœ์šด ์นดํ…Œ๊ณ ๋ฆฌ ์ €์žฅ ๊ฐ์ง€๋จ, ๋ฐ์ดํ„ฐ ๋‹ค์‹œ ๋กœ๋“œ..."); loadCategories(); } }, 1000); - + return () => { - window.removeEventListener('categoryBudgetsUpdated', () => handleCategoryUpdate()); - window.removeEventListener('storage', handleCategoryUpdate); - window.removeEventListener('visibilitychange', () => {}); - window.removeEventListener('focus', () => loadCategories()); + window.removeEventListener("categoryBudgetsUpdated", () => + handleCategoryUpdate() + ); + window.removeEventListener("storage", handleCategoryUpdate); + window.removeEventListener("visibilitychange", () => {}); + window.removeEventListener("focus", () => loadCategories()); clearInterval(intervalId); }; }, [isInitialized]); // ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ - const updateCategoryBudgets = useCallback((newCategoryBudgets: Record) => { - try { - console.log('์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ:', newCategoryBudgets); - setCategoryBudgets(newCategoryBudgets); - saveCategoryBudgetsToStorage(newCategoryBudgets); - - // ์ €์žฅ ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ - localStorage.setItem('lastCategoryBudgetSaveTime', new Date().toISOString()); - } catch (error) { - console.error('์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ ์ค‘ ์˜ค๋ฅ˜:', error); - } - }, []); + const updateCategoryBudgets = useCallback( + (newCategoryBudgets: Record) => { + try { + logger.info("์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ:", newCategoryBudgets); + setCategoryBudgets(newCategoryBudgets); + saveCategoryBudgetsToStorage(newCategoryBudgets); + + // ์ €์žฅ ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ + localStorage.setItem( + "lastCategoryBudgetSaveTime", + new Date().toISOString() + ); + } catch (error) { + logger.error("์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ ์ค‘ ์˜ค๋ฅ˜:", error); + } + }, + [] + ); // ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜ const resetCategoryBudgets = useCallback(() => { try { - console.log('์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ดˆ๊ธฐํ™”'); + logger.info("์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ดˆ๊ธฐํ™”"); clearAllCategoryBudgets(); setCategoryBudgets(loadCategoryBudgetsFromStorage()); } catch (error) { - console.error('์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜:", error); } }, []); // ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋ณ€๊ฒฝ ์‹œ ๋กœ๊ทธ ๊ธฐ๋ก useEffect(() => { - console.log('์ตœ์‹  ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ:', categoryBudgets); + logger.info("์ตœ์‹  ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ:", categoryBudgets); }, [categoryBudgets]); return { categoryBudgets, setCategoryBudgets: updateCategoryBudgets, updateCategoryBudgets, - resetCategoryBudgets + resetCategoryBudgets, }; }; diff --git a/src/contexts/budget/hooks/useCategorySpending.ts b/src/contexts/budget/hooks/useCategorySpending.ts index 8fe8b24..40ed126 100644 --- a/src/contexts/budget/hooks/useCategorySpending.ts +++ b/src/contexts/budget/hooks/useCategorySpending.ts @@ -1,11 +1,10 @@ - -import { useCallback } from 'react'; -import { Transaction } from '../types'; -import { calculateCategorySpending } from '../utils/categoryUtils'; +import { useCallback } from "react"; +import { Transaction } from "../types"; +import { calculateCategorySpending } from "../utils/categoryUtils"; // ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ง€์ถœ ๊ณ„์‚ฐ ํ›… export const useCategorySpending = ( - transactions: Transaction[], + transactions: Transaction[], categoryBudgets: Record ) => { // ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ง€์ถœ ๊ณ„์‚ฐ diff --git a/src/contexts/budget/hooks/useExtendedBudgetUpdate.ts b/src/contexts/budget/hooks/useExtendedBudgetUpdate.ts index 53945c4..537dfe2 100644 --- a/src/contexts/budget/hooks/useExtendedBudgetUpdate.ts +++ b/src/contexts/budget/hooks/useExtendedBudgetUpdate.ts @@ -1,6 +1,6 @@ +import { BudgetPeriod } from "../types"; -import { BudgetPeriod } from '../types'; - +import { logger } from "@/utils/logger"; /** * ์˜ˆ์‚ฐ ๋ชฉํ‘œ ์—…๋ฐ์ดํŠธ ํ™•์žฅ ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ•˜๋Š” ํ›… * ์˜ˆ์‚ฐ ๋ชฉํ‘œ ์—…๋ฐ์ดํŠธ์™€ ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ๋ฅผ ํ†ตํ•ฉํ•ฉ๋‹ˆ๋‹ค. @@ -11,30 +11,30 @@ export const useExtendedBudgetUpdate = ( ) => { // ์˜ˆ์‚ฐ ๋ชฉํ‘œ ์—…๋ฐ์ดํŠธ ํ™•์žฅ ํ•จ์ˆ˜ const extendedBudgetUpdate = ( - type: BudgetPeriod, - amount: number, + type: BudgetPeriod, + amount: number, newCategoryBudgets?: Record ) => { - console.log('ํ™•์žฅ ์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘:', type, amount, newCategoryBudgets); - + logger.info("ํ™•์žฅ ์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘:", type, amount, newCategoryBudgets); + // ํƒ€์ž…์— ์ƒ๊ด€์—†์ด ํ•ญ์ƒ ์›”๊ฐ„์œผ๋กœ ์ฒ˜๋ฆฌ - console.log(`์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ: ์ž…๋ ฅ ๊ธˆ์•ก=${amount}, ํ•ญ์ƒ ์›”๊ฐ„์œผ๋กœ ์„ค์ •`); - handleBudgetUpdate('monthly', amount); - + logger.info(`์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ: ์ž…๋ ฅ ๊ธˆ์•ก=${amount}, ํ•ญ์ƒ ์›”๊ฐ„์œผ๋กœ ์„ค์ •`); + handleBudgetUpdate("monthly", amount); + // ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ (์ œ๊ณต๋œ ๊ฒฝ์šฐ) if (newCategoryBudgets) { - console.log('์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ:', newCategoryBudgets); + logger.info("์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ:", newCategoryBudgets); setCategoryBudgets(newCategoryBudgets); - + // ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ ์ด๋ฒคํŠธ ๋ฐœ์ƒ setTimeout(() => { - window.dispatchEvent(new Event('categoryBudgetsUpdated')); + window.dispatchEvent(new Event("categoryBudgetsUpdated")); }, 100); } - + // ๋ชจ๋“  ์—…๋ฐ์ดํŠธ๊ฐ€ ์™„๋ฃŒ๋œ ํ›„ ์ „์—ญ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ์ด๋ฒคํŠธ ๋ฐœ์ƒ setTimeout(() => { - window.dispatchEvent(new Event('budgetDataUpdated')); + window.dispatchEvent(new Event("budgetDataUpdated")); }, 200); }; diff --git a/src/contexts/budget/hooks/useTransactionState.ts b/src/contexts/budget/hooks/useTransactionState.ts index a613ac9..e1ab9c3 100644 --- a/src/contexts/budget/hooks/useTransactionState.ts +++ b/src/contexts/budget/hooks/useTransactionState.ts @@ -1,8 +1,11 @@ - -import { useState, useEffect } from 'react'; -import { Transaction } from '../types'; -import { loadTransactionsFromStorage, saveTransactionsToStorage } from '../storage/transactionStorage'; -import { v4 as uuidv4 } from 'uuid'; +import { useState, useEffect } from "react"; +import { logger } from "@/utils/logger"; +import { Transaction } from "../types"; +import { + loadTransactionsFromStorage, + saveTransactionsToStorage, +} from "../storage/transactionStorage"; +import { v4 as uuidv4 } from "uuid"; /** * ํŠธ๋žœ์žญ์…˜ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ํ›… @@ -16,10 +19,13 @@ export const useTransactionState = () => { useEffect(() => { try { const storedTransactions = loadTransactionsFromStorage(); - console.log('๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—์„œ ํŠธ๋žœ์žญ์…˜ ๋กœ๋“œ:', storedTransactions?.length || 0); + logger.info( + "๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—์„œ ํŠธ๋žœ์žญ์…˜ ๋กœ๋“œ:", + storedTransactions?.length || 0 + ); setTransactions(storedTransactions || []); } catch (error) { - console.error('ํŠธ๋žœ์žญ์…˜ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error); + logger.error("ํŠธ๋žœ์žญ์…˜ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:", error); // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ๋นˆ ๋ฐฐ์—ด๋กœ ์ดˆ๊ธฐํ™” setTransactions([]); } @@ -29,54 +35,60 @@ export const useTransactionState = () => { useEffect(() => { try { if (transactions && transactions.length > 0) { - console.log('ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์ค‘:', transactions.length); + logger.info("ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์ค‘:", transactions.length); saveTransactionsToStorage(transactions); } } catch (error) { - console.error('ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error); + logger.error("ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:", error); } }, [transactions]); // ํŠธ๋žœ์žญ์…˜ ์ถ”๊ฐ€ const addTransaction = (transaction: Transaction) => { try { - const newTransaction = { - ...transaction, + const newTransaction = { + ...transaction, id: transaction.id || uuidv4(), - localTimestamp: new Date().toISOString() + localTimestamp: new Date().toISOString(), }; - setTransactions(prevTransactions => [...(prevTransactions || []), newTransaction]); - console.log('ํŠธ๋žœ์žญ์…˜ ์ถ”๊ฐ€๋จ:', newTransaction); + setTransactions((prevTransactions) => [ + ...(prevTransactions || []), + newTransaction, + ]); + logger.info("ํŠธ๋žœ์žญ์…˜ ์ถ”๊ฐ€๋จ:", newTransaction); } catch (error) { - console.error('ํŠธ๋žœ์žญ์…˜ ์ถ”๊ฐ€ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error); + logger.error("ํŠธ๋žœ์žญ์…˜ ์ถ”๊ฐ€ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:", error); } }; // ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ const updateTransaction = (updatedTransaction: Transaction) => { try { - setTransactions(prevTransactions => - (prevTransactions || []).map(transaction => - transaction.id === updatedTransaction.id - ? { ...updatedTransaction, localTimestamp: new Date().toISOString() } + setTransactions((prevTransactions) => + (prevTransactions || []).map((transaction) => + transaction.id === updatedTransaction.id + ? { + ...updatedTransaction, + localTimestamp: new Date().toISOString(), + } : transaction ) ); - console.log('ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ๋จ:', updatedTransaction.id); + logger.info("ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ๋จ:", updatedTransaction.id); } catch (error) { - console.error('ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error); + logger.error("ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:", error); } }; // ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ const deleteTransaction = (id: string) => { try { - setTransactions(prevTransactions => - (prevTransactions || []).filter(transaction => transaction.id !== id) + setTransactions((prevTransactions) => + (prevTransactions || []).filter((transaction) => transaction.id !== id) ); - console.log('ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ๋จ:', id); + logger.info("ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ๋จ:", id); } catch (error) { - console.error('ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error); + logger.error("ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:", error); } }; @@ -85,6 +97,6 @@ export const useTransactionState = () => { setTransactions, addTransaction, updateTransaction, - deleteTransaction + deleteTransaction, }; }; diff --git a/src/contexts/budget/index.ts b/src/contexts/budget/index.ts index 088ce82..25d3171 100644 --- a/src/contexts/budget/index.ts +++ b/src/contexts/budget/index.ts @@ -1,22 +1,21 @@ - -export * from './BudgetContext'; -export * from './budgetUtils'; -export * from './storageUtils'; +export * from "./BudgetContext"; +export * from "./budgetUtils"; +export * from "./storageUtils"; // types.ts์—์„œ ํƒ€์ž…๋“ค์„ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค -export type { - BudgetPeriod, - BudgetPeriodData, - BudgetData, - CategoryBudget, - Transaction -} from './types'; +export type { + BudgetPeriod, + BudgetPeriodData, + BudgetData, + CategoryBudget, + Transaction, +} from "./types"; // Export hooks -export * from './hooks/useTransactionState'; -export * from './hooks/useBudgetDataState'; -export * from './hooks/useCategoryBudgetState'; -export * from './hooks/useCategorySpending'; -export * from './hooks/useBudgetBackup'; -export * from './hooks/useBudgetReset'; -export * from './hooks/useExtendedBudgetUpdate'; +export * from "./hooks/useTransactionState"; +export * from "./hooks/useBudgetDataState"; +export * from "./hooks/useCategoryBudgetState"; +export * from "./hooks/useCategorySpending"; +export * from "./hooks/useBudgetBackup"; +export * from "./hooks/useBudgetReset"; +export * from "./hooks/useExtendedBudgetUpdate"; diff --git a/src/contexts/budget/storage/budgetStorage.ts b/src/contexts/budget/storage/budgetStorage.ts index 0558d8f..ca8563b 100644 --- a/src/contexts/budget/storage/budgetStorage.ts +++ b/src/contexts/budget/storage/budgetStorage.ts @@ -1,7 +1,11 @@ - -import { BudgetData } from '../types'; -import { getInitialBudgetData, safelyLoadBudgetData, safeStorage } from '../budgetUtils'; -import { toast } from '@/hooks/useToast.wrapper'; +import { BudgetData } from "../types"; +import { storageLogger } from "@/utils/logger"; +import { + getInitialBudgetData, + safelyLoadBudgetData, + safeStorage, +} from "../budgetUtils"; +import { toast } from "@/hooks/useToast.wrapper"; /** * ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ @@ -10,14 +14,14 @@ export const loadBudgetDataFromStorage = (): BudgetData => { try { // ์ƒˆ๋กœ์šด safelyLoadBudgetData ํ•จ์ˆ˜ ์‚ฌ์šฉ const budgetData = safelyLoadBudgetData(); - console.log('์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์™„๋ฃŒ', budgetData); + storageLogger.info("์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์™„๋ฃŒ", budgetData); return budgetData; } catch (error) { - console.error('์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜:', error); + storageLogger.error("์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜:", error); } - + // ์ƒˆ ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•œ ๊ธฐ๋ณธ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ €์žฅ - console.log('๊ธฐ๋ณธ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์„ค์ •'); + storageLogger.info("๊ธฐ๋ณธ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์„ค์ •"); const initialData = getInitialBudgetData(); saveBudgetDataToStorage(initialData); return initialData; @@ -29,40 +33,46 @@ export const loadBudgetDataFromStorage = (): BudgetData => { export const saveBudgetDataToStorage = (budgetData: BudgetData): void => { try { // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ - if (!budgetData || !budgetData.monthly || !budgetData.daily || !budgetData.weekly) { - console.error('์ž˜๋ชป๋œ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ €์žฅ ์‹œ๋„:', budgetData); - throw new Error('์ž˜๋ชป๋œ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ'); + if ( + !budgetData || + !budgetData.monthly || + !budgetData.daily || + !budgetData.weekly + ) { + storageLogger.error("์ž˜๋ชป๋œ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ €์žฅ ์‹œ๋„:", budgetData); + throw new Error("์ž˜๋ชป๋œ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ"); } - + // ๋ฐ์ดํ„ฐ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ const dataString = JSON.stringify(budgetData); - + // ์ด์ „ ์˜ˆ์‚ฐ๊ณผ ๋น„๊ตํ•˜์—ฌ ๋ณ€๊ฒฝ ์—ฌ๋ถ€ ํ™•์ธ let hasChanged = true; try { - const oldData = safeStorage.getItem('budgetData'); + const oldData = safeStorage.getItem("budgetData"); if (oldData) { // ์›”๊ฐ„ ์˜ˆ์‚ฐ์ด ๋™์ผํ•˜๋ฉด ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์€ ๊ฒƒ์œผ๋กœ ํŒ๋‹จ - hasChanged = oldData.monthly.targetAmount !== budgetData.monthly.targetAmount; + hasChanged = + oldData.monthly.targetAmount !== budgetData.monthly.targetAmount; } } catch (e) { - console.error('์ด์ „ ์˜ˆ์‚ฐ ๋น„๊ต ์˜ค๋ฅ˜:', e); + storageLogger.error("์ด์ „ ์˜ˆ์‚ฐ ๋น„๊ต ์˜ค๋ฅ˜:", e); } - + // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅ - safeStorage.setItem('budgetData', budgetData); - console.log('์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ €์žฅ ์™„๋ฃŒ', budgetData); - + safeStorage.setItem("budgetData", budgetData); + storageLogger.info("์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ €์žฅ ์™„๋ฃŒ", budgetData); + // ์ค‘์š”: ์ฆ‰์‹œ ์ž๋™ ๋ฐฑ์—… (๋ฐ์ดํ„ฐ ์†์‹ค ๋ฐฉ์ง€) - safeStorage.setItem('budgetData_backup', budgetData); - safeStorage.setItem('lastBudgetSaveTime', new Date().toISOString()); - + safeStorage.setItem("budgetData_backup", budgetData); + safeStorage.setItem("lastBudgetSaveTime", new Date().toISOString()); + // ์ด๋ฒคํŠธ ๋ฐœ์ƒ (๋‹จ์ผ ์ด๋ฒคํŠธ๋กœ ํ†ตํ•ฉ) - const event = new CustomEvent('budgetChanged', { - detail: { data: budgetData, hasChanged } + const event = new CustomEvent("budgetChanged", { + detail: { data: budgetData, hasChanged }, }); window.dispatchEvent(event); - + // toast ์•Œ๋ฆผ (๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ์—๋งŒ) if (hasChanged && budgetData.monthly.targetAmount > 0) { toast({ @@ -71,13 +81,13 @@ export const saveBudgetDataToStorage = (budgetData: BudgetData): void => { }); } } catch (error) { - console.error('์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ €์žฅ ์˜ค๋ฅ˜:', error); - + storageLogger.error("์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ €์žฅ ์˜ค๋ฅ˜:", error); + // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ํ† ์ŠคํŠธ ์•Œ๋ฆผ toast({ title: "์˜ˆ์‚ฐ ์ €์žฅ ์‹คํŒจ", description: "์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š”๋ฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" + variant: "destructive", }); } }; @@ -87,25 +97,27 @@ export const saveBudgetDataToStorage = (budgetData: BudgetData): void => { */ export const clearAllBudgetData = (): void => { try { - localStorage.removeItem('budgetData'); - localStorage.removeItem('budgetData_backup'); - + localStorage.removeItem("budgetData"); + localStorage.removeItem("budgetData_backup"); + // ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์žฌ์„ค์ • const initialData = getInitialBudgetData(); - safeStorage.setItem('budgetData', initialData); - safeStorage.setItem('budgetData_backup', initialData); - - console.log('์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); - + safeStorage.setItem("budgetData", initialData); + safeStorage.setItem("budgetData_backup", initialData); + + storageLogger.info("์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + // ์ด๋ฒคํŠธ ๋ฐœ์ƒ - window.dispatchEvent(new Event('budgetDataUpdated')); - window.dispatchEvent(new StorageEvent('storage', { - key: 'budgetData', - newValue: JSON.stringify(initialData) - })); - + window.dispatchEvent(new Event("budgetDataUpdated")); + window.dispatchEvent( + new StorageEvent("storage", { + key: "budgetData", + newValue: JSON.stringify(initialData), + }) + ); + // ํ† ์ŠคํŠธ ์•Œ๋ฆผ (์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์ดˆ๊ธฐํ™”ํ•œ ๊ฒฝ์šฐ๋งŒ) - const isUserInitiated = document.visibilityState === 'visible'; + const isUserInitiated = document.visibilityState === "visible"; if (isUserInitiated) { toast({ title: "์˜ˆ์‚ฐ ์ดˆ๊ธฐํ™”", @@ -113,11 +125,11 @@ export const clearAllBudgetData = (): void => { }); } } catch (error) { - console.error('์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์‚ญ์ œ ์˜ค๋ฅ˜:', error); + storageLogger.error("์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์‚ญ์ œ ์˜ค๋ฅ˜:", error); toast({ title: "์ดˆ๊ธฐํ™” ์‹คํŒจ", description: "์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋Š”๋ฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" + variant: "destructive", }); } }; diff --git a/src/contexts/budget/storage/categoryStorage.ts b/src/contexts/budget/storage/categoryStorage.ts index 4a7e821..9025490 100644 --- a/src/contexts/budget/storage/categoryStorage.ts +++ b/src/contexts/budget/storage/categoryStorage.ts @@ -1,7 +1,7 @@ - -import { DEFAULT_CATEGORY_BUDGETS } from '../budgetUtils'; -import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons'; -import { toast } from '@/hooks/useToast.wrapper'; // ๋ž˜ํผ ์‚ฌ์šฉ +import { DEFAULT_CATEGORY_BUDGETS } from "../budgetUtils"; +import { storageLogger } from "@/utils/logger"; +import { EXPENSE_CATEGORIES } from "@/constants/categoryIcons"; +import { toast } from "@/hooks/useToast.wrapper"; // ๋ž˜ํผ ์‚ฌ์šฉ /** * ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ @@ -9,60 +9,68 @@ import { toast } from '@/hooks/useToast.wrapper'; // ๋ž˜ํผ ์‚ฌ์šฉ export const loadCategoryBudgetsFromStorage = (): Record => { try { // ๋ฉ”์ธ ์Šคํ† ๋ฆฌ์ง€์—์„œ ์‹œ๋„ - const storedCategoryBudgets = localStorage.getItem('categoryBudgets'); + const storedCategoryBudgets = localStorage.getItem("categoryBudgets"); if (storedCategoryBudgets) { const parsed = JSON.parse(storedCategoryBudgets); - console.log('์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋กœ๋“œ ์™„๋ฃŒ:', parsed); - + storageLogger.info("์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋กœ๋“œ ์™„๋ฃŒ:", parsed); + // ๋ชจ๋“  ํ—ˆ์šฉ๋œ ์นดํ…Œ๊ณ ๋ฆฌ ํฌํ•จ const filteredBudgets: Record = {}; - EXPENSE_CATEGORIES.forEach(category => { - // ์ด์ „ ์นดํ…Œ๊ณ ๋ฆฌ๋ช…์ด ์žˆ์„ ๊ฒฝ์šฐ ์ƒˆ ์นดํ…Œ๊ณ ๋ฆฌ๋ช…์œผ๋กœ ๊ฐ’ ์ด์ „ - if (category === '์Œ์‹' && parsed['์‹๋น„'] !== undefined) { - filteredBudgets[category] = parsed['์‹๋น„']; - } else if (category === '๊ตํ†ต' && parsed['๊ตํ†ต๋น„'] !== undefined) { - filteredBudgets[category] = parsed['๊ตํ†ต๋น„']; - } else if (category === '์‡ผํ•‘' && parsed['์ƒํ™œ๋น„'] !== undefined) { - filteredBudgets[category] = parsed['์ƒํ™œ๋น„']; + EXPENSE_CATEGORIES.forEach((category) => { + // ์ด์ „ ์นดํ…Œ๊ณ ๋ฆฌ๋ช…์ด ์žˆ์„ ๊ฒฝ์šฐ ์ƒˆ ์นดํ…Œ๊ณ ๋ฆฌ๋ช…์œผ๋กœ ๊ฐ’ ์ด์ „ + if (category === "์Œ์‹" && parsed["์‹๋น„"] !== undefined) { + filteredBudgets[category] = parsed["์‹๋น„"]; + } else if (category === "๊ตํ†ต" && parsed["๊ตํ†ต๋น„"] !== undefined) { + filteredBudgets[category] = parsed["๊ตํ†ต๋น„"]; + } else if (category === "์‡ผํ•‘" && parsed["์ƒํ™œ๋น„"] !== undefined) { + filteredBudgets[category] = parsed["์ƒํ™œ๋น„"]; } else { filteredBudgets[category] = parsed[category] || 0; } }); - + return filteredBudgets; } - + // ๋ฐฑ์—…์—์„œ ์‹œ๋„ - const backupCategoryBudgets = localStorage.getItem('categoryBudgets_backup'); + const backupCategoryBudgets = localStorage.getItem( + "categoryBudgets_backup" + ); if (backupCategoryBudgets) { const parsedBackup = JSON.parse(backupCategoryBudgets); - console.log('๋ฐฑ์—…์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋ณต๊ตฌ:', parsedBackup); - + storageLogger.info("๋ฐฑ์—…์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋ณต๊ตฌ:", parsedBackup); + // ๋ชจ๋“  ํ—ˆ์šฉ๋œ ์นดํ…Œ๊ณ ๋ฆฌ ํฌํ•จ const filteredBudgets: Record = {}; - EXPENSE_CATEGORIES.forEach(category => { + EXPENSE_CATEGORIES.forEach((category) => { // ์ด์ „ ์นดํ…Œ๊ณ ๋ฆฌ๋ช…์ด ์žˆ์„ ๊ฒฝ์šฐ ์ƒˆ ์นดํ…Œ๊ณ ๋ฆฌ๋ช…์œผ๋กœ ๊ฐ’ ์ด์ „ - if (category === '์Œ์‹' && parsedBackup['์‹๋น„'] !== undefined) { - filteredBudgets[category] = parsedBackup['์‹๋น„']; - } else if (category === '๊ตํ†ต' && parsedBackup['๊ตํ†ต๋น„'] !== undefined) { - filteredBudgets[category] = parsedBackup['๊ตํ†ต๋น„']; - } else if (category === '์‡ผํ•‘' && parsedBackup['์ƒํ™œ๋น„'] !== undefined) { - filteredBudgets[category] = parsedBackup['์ƒํ™œ๋น„']; + if (category === "์Œ์‹" && parsedBackup["์‹๋น„"] !== undefined) { + filteredBudgets[category] = parsedBackup["์‹๋น„"]; + } else if ( + category === "๊ตํ†ต" && + parsedBackup["๊ตํ†ต๋น„"] !== undefined + ) { + filteredBudgets[category] = parsedBackup["๊ตํ†ต๋น„"]; + } else if ( + category === "์‡ผํ•‘" && + parsedBackup["์ƒํ™œ๋น„"] !== undefined + ) { + filteredBudgets[category] = parsedBackup["์ƒํ™œ๋น„"]; } else { filteredBudgets[category] = parsedBackup[category] || 0; } }); - + // ๋ฉ”์ธ ์Šคํ† ๋ฆฌ์ง€๋„ ๋ณต๊ตฌ - localStorage.setItem('categoryBudgets', JSON.stringify(filteredBudgets)); + localStorage.setItem("categoryBudgets", JSON.stringify(filteredBudgets)); return filteredBudgets; } } catch (error) { - console.error('์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์˜ค๋ฅ˜:', error); + storageLogger.error("์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์˜ค๋ฅ˜:", error); } - + // ์ƒˆ ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•œ ๊ธฐ๋ณธ ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ €์žฅ - console.log('๊ธฐ๋ณธ ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์„ค์ •'); + storageLogger.info("๊ธฐ๋ณธ ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์„ค์ •"); saveCategoryBudgetsToStorage(DEFAULT_CATEGORY_BUDGETS); return DEFAULT_CATEGORY_BUDGETS; }; @@ -70,59 +78,76 @@ export const loadCategoryBudgetsFromStorage = (): Record => { /** * ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ €์žฅ */ -export const saveCategoryBudgetsToStorage = (categoryBudgets: Record): void => { +export const saveCategoryBudgetsToStorage = ( + categoryBudgets: Record +): void => { try { // ์ด์ „ ์˜ˆ์‚ฐ๊ณผ ๋น„๊ตํ•˜์—ฌ ๋ณ€๊ฒฝ ์—ฌ๋ถ€ ํ™•์ธ let hasChanged = true; try { - const oldDataString = localStorage.getItem('categoryBudgets'); + const oldDataString = localStorage.getItem("categoryBudgets"); if (oldDataString) { const oldData = JSON.parse(oldDataString); // ์ด ์˜ˆ์‚ฐ์ด ๋™์ผํ•˜๋ฉด ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์€ ๊ฒƒ์œผ๋กœ ํŒ๋‹จ - const oldTotal = Object.values(oldData).reduce((sum: number, val: number) => sum + val, 0); - const newTotal = Object.values(categoryBudgets).reduce((sum: number, val: number) => sum + val, 0); + const oldTotal = Object.values(oldData).reduce( + (sum: number, val: number) => sum + val, + 0 + ); + const newTotal = Object.values(categoryBudgets).reduce( + (sum: number, val: number) => sum + val, + 0 + ); hasChanged = oldTotal !== newTotal; } } catch (e) { - console.error('์ด์ „ ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋น„๊ต ์˜ค๋ฅ˜:', e); + storageLogger.error("์ด์ „ ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋น„๊ต ์˜ค๋ฅ˜:", e); } - + // ๋ชจ๋“  ํ—ˆ์šฉ๋œ ์นดํ…Œ๊ณ ๋ฆฌ ํฌํ•จํ•˜์—ฌ ์œ ์ง€ const filteredBudgets: Record = {}; - EXPENSE_CATEGORIES.forEach(category => { + EXPENSE_CATEGORIES.forEach((category) => { // ๊ตํ†ต๋น„ -> ๊ตํ†ต์œผ๋กœ ํ‘œ์ค€ํ™” - if (category === '๊ตํ†ต') { - filteredBudgets[category] = categoryBudgets[category] || categoryBudgets['๊ตํ†ต๋น„'] || 0; - } else if (category === '์Œ์‹') { - filteredBudgets[category] = categoryBudgets[category] || categoryBudgets['์‹๋น„'] || 0; - } else if (category === '์‡ผํ•‘') { - filteredBudgets[category] = categoryBudgets[category] || categoryBudgets['์ƒํ™œ๋น„'] || 0; + if (category === "๊ตํ†ต") { + filteredBudgets[category] = + categoryBudgets[category] || categoryBudgets["๊ตํ†ต๋น„"] || 0; + } else if (category === "์Œ์‹") { + filteredBudgets[category] = + categoryBudgets[category] || categoryBudgets["์‹๋น„"] || 0; + } else if (category === "์‡ผํ•‘") { + filteredBudgets[category] = + categoryBudgets[category] || categoryBudgets["์ƒํ™œ๋น„"] || 0; } else { filteredBudgets[category] = categoryBudgets[category] || 0; } }); - + // ๋ฐ์ดํ„ฐ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ const dataString = JSON.stringify(filteredBudgets); - + // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅ - localStorage.setItem('categoryBudgets', dataString); + localStorage.setItem("categoryBudgets", dataString); // ๋ฐฑ์—… ์ €์žฅ - localStorage.setItem('categoryBudgets_backup', dataString); - - console.log('์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ €์žฅ ์™„๋ฃŒ:', filteredBudgets); - + localStorage.setItem("categoryBudgets_backup", dataString); + + storageLogger.info("์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ €์žฅ ์™„๋ฃŒ:", filteredBudgets); + // ๋‹จ์ผ ์ด๋ฒคํŠธ๋กœ ํ†ตํ•ฉ - const event = new CustomEvent('categoryBudgetsChanged', { - detail: { data: filteredBudgets, hasChanged } + const event = new CustomEvent("categoryBudgetsChanged", { + detail: { data: filteredBudgets, hasChanged }, }); window.dispatchEvent(event); - + // ๋งˆ์ง€๋ง‰ ์ €์žฅ ์‹œ๊ฐ„ ๊ธฐ๋ก (๋ฐ์ดํ„ฐ ๊ฒ€์ฆ์šฉ) - localStorage.setItem('lastCategoryBudgetSaveTime', new Date().toISOString()); - + localStorage.setItem( + "lastCategoryBudgetSaveTime", + new Date().toISOString() + ); + // ํ† ์ŠคํŠธ ์•Œ๋ฆผ (๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ์—๋งŒ) - const totalBudget = Object.values(filteredBudgets).reduce((sum, val) => sum + val, 0); + const totalBudget = Object.values(filteredBudgets).reduce( + (sum, val) => sum + val, + 0 + ); if (hasChanged && totalBudget > 0) { toast({ title: "์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ €์žฅ ์™„๋ฃŒ", @@ -130,13 +155,13 @@ export const saveCategoryBudgetsToStorage = (categoryBudgets: Record { try { - localStorage.removeItem('categoryBudgets'); - localStorage.removeItem('categoryBudgets_backup'); - + localStorage.removeItem("categoryBudgets"); + localStorage.removeItem("categoryBudgets_backup"); + // ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์žฌ์„ค์ • (4๊ฐœ ์นดํ…Œ๊ณ ๋ฆฌ๋งŒ) const dataString = JSON.stringify(DEFAULT_CATEGORY_BUDGETS); - localStorage.setItem('categoryBudgets', dataString); - localStorage.setItem('categoryBudgets_backup', dataString); - - console.log('์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ์ด ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); - + localStorage.setItem("categoryBudgets", dataString); + localStorage.setItem("categoryBudgets_backup", dataString); + + storageLogger.info("์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ์ด ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + // ์ด๋ฒคํŠธ ๋ฐœ์ƒ - window.dispatchEvent(new Event('categoryBudgetsUpdated')); - window.dispatchEvent(new StorageEvent('storage', { - key: 'categoryBudgets', - newValue: dataString - })); - + window.dispatchEvent(new Event("categoryBudgetsUpdated")); + window.dispatchEvent( + new StorageEvent("storage", { + key: "categoryBudgets", + newValue: dataString, + }) + ); + // ํ† ์ŠคํŠธ ์•Œ๋ฆผ toast({ title: "์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ดˆ๊ธฐํ™”", description: "๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ์ด ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", }); } catch (error) { - console.error('์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์‚ญ์ œ ์˜ค๋ฅ˜:', error); - + storageLogger.error("์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์‚ญ์ œ ์˜ค๋ฅ˜:", error); + // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ํ† ์ŠคํŠธ ์•Œ๋ฆผ toast({ title: "์ดˆ๊ธฐํ™” ์‹คํŒจ", description: "์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ์„ ์ดˆ๊ธฐํ™”ํ•˜๋Š”๋ฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" + variant: "destructive", }); } }; diff --git a/src/contexts/budget/storage/index.ts b/src/contexts/budget/storage/index.ts index 57c06d4..76ebe88 100644 --- a/src/contexts/budget/storage/index.ts +++ b/src/contexts/budget/storage/index.ts @@ -1,26 +1,23 @@ - // ํŠธ๋žœ์žญ์…˜ ๊ด€๋ จ ํ•จ์ˆ˜ ๋‚ด๋ณด๋‚ด๊ธฐ export { loadTransactionsFromStorage, saveTransactionsToStorage, - clearAllTransactions -} from './transactionStorage'; + clearAllTransactions, +} from "./transactionStorage"; // ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๊ด€๋ จ ํ•จ์ˆ˜ ๋‚ด๋ณด๋‚ด๊ธฐ export { loadBudgetDataFromStorage, saveBudgetDataToStorage, - clearAllBudgetData -} from './budgetStorage'; + clearAllBudgetData, +} from "./budgetStorage"; // ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ จ ํ•จ์ˆ˜ ๋‚ด๋ณด๋‚ด๊ธฐ export { loadCategoryBudgetsFromStorage, saveCategoryBudgetsToStorage, - clearAllCategoryBudgets -} from './categoryStorage'; + clearAllCategoryBudgets, +} from "./categoryStorage"; // ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜ ๋‚ด๋ณด๋‚ด๊ธฐ -export { - resetAllData -} from './resetStorage'; +export { resetAllData } from "./resetStorage"; diff --git a/src/contexts/budget/storage/resetStorage.ts b/src/contexts/budget/storage/resetStorage.ts index e5b3212..b3d0f05 100644 --- a/src/contexts/budget/storage/resetStorage.ts +++ b/src/contexts/budget/storage/resetStorage.ts @@ -1,108 +1,126 @@ - -import { clearAllTransactions } from './transactionStorage'; -import { clearAllCategoryBudgets } from './categoryStorage'; -import { clearAllBudgetData } from './budgetStorage'; -import { DEFAULT_CATEGORY_BUDGETS } from '../budgetUtils'; -import { getInitialBudgetData } from '../budgetUtils'; -import { toast } from '@/hooks/useToast.wrapper'; +import { clearAllTransactions } from "./transactionStorage"; +import { storageLogger } from "@/utils/logger"; +import { clearAllCategoryBudgets } from "./categoryStorage"; +import { clearAllBudgetData } from "./budgetStorage"; +import { DEFAULT_CATEGORY_BUDGETS } from "../budgetUtils"; +import { getInitialBudgetData } from "../budgetUtils"; +import { toast } from "@/hooks/useToast.wrapper"; /** * ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” (์ฒซ ๋กœ๊ทธ์ธ ์ƒํƒœ) */ export const resetAllData = (): void => { - console.log('์™„์ „ ์ดˆ๊ธฐํ™” ์‹œ์ž‘ - resetAllData'); - + storageLogger.info("์™„์ „ ์ดˆ๊ธฐํ™” ์‹œ์ž‘ - resetAllData"); + // ์ค‘์š”: ์‚ฌ์šฉ์ž ์„ค์ • ๊ด€๋ จ ๊ฐ’ ๋ฐฑ์—… - const dontShowWelcomeValue = localStorage.getItem('dontShowWelcome'); - const hasVisitedBefore = localStorage.getItem('hasVisitedBefore'); - console.log('resetAllData - ์‚ฌ์šฉ์ž ์„ค์ • ๋ฐฑ์—…:', { dontShowWelcome: dontShowWelcomeValue, hasVisitedBefore }); - + const dontShowWelcomeValue = localStorage.getItem("dontShowWelcome"); + const hasVisitedBefore = localStorage.getItem("hasVisitedBefore"); + storageLogger.info("resetAllData - ์‚ฌ์šฉ์ž ์„ค์ • ๋ฐฑ์—…:", { + dontShowWelcome: dontShowWelcomeValue, + hasVisitedBefore, + }); + // ๋ชจ๋“  ๊ด€๋ จ ๋ฐ์ดํ„ฐ ํ‚ค ๋ชฉ๋ก (๋ถ„์„ ํŽ˜์ด์ง€์˜ ๋ฐ์ดํ„ฐ ํฌํ•จ) const dataKeys = [ - 'transactions', - 'transactions_backup', - 'categoryBudgets', - 'categoryBudgets_backup', - 'budgetData', - 'budgetData_backup', - 'budget', - 'monthlyExpenses', - 'categorySpending', - 'expenseAnalytics', - 'expenseHistory', - 'budgetHistory', - 'analyticsCache', - 'monthlyTotals', - 'analytics', - 'dailyBudget', - 'weeklyBudget', - 'monthlyBudget', - 'chartData', + "transactions", + "transactions_backup", + "categoryBudgets", + "categoryBudgets_backup", + "budgetData", + "budgetData_backup", + "budget", + "monthlyExpenses", + "categorySpending", + "expenseAnalytics", + "expenseHistory", + "budgetHistory", + "analyticsCache", + "monthlyTotals", + "analytics", + "dailyBudget", + "weeklyBudget", + "monthlyBudget", + "chartData", ]; - + try { // ๋ชจ๋“  ๊ด€๋ จ ๋ฐ์ดํ„ฐ ํ‚ค ์‚ญ์ œ - dataKeys.forEach(key => { - console.log(`์‚ญ์ œ ์ค‘: ${key}`); + dataKeys.forEach((key) => { + storageLogger.info(`์‚ญ์ œ ์ค‘: ${key}`); localStorage.removeItem(key); }); - + // ํŒŒ์ผ๋ณ„ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜ ํ˜ธ์ถœ clearAllTransactions(); clearAllCategoryBudgets(); clearAllBudgetData(); - + // ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ๋กœ ์ดˆ๊ธฐํ™” (์ค‘๋ณต ์‚ญ์ œ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰) const initialBudgetData = getInitialBudgetData(); - localStorage.setItem('budgetData', JSON.stringify(initialBudgetData)); - localStorage.setItem('categoryBudgets', JSON.stringify(DEFAULT_CATEGORY_BUDGETS)); - + localStorage.setItem("budgetData", JSON.stringify(initialBudgetData)); + localStorage.setItem( + "categoryBudgets", + JSON.stringify(DEFAULT_CATEGORY_BUDGETS) + ); + // ์ค‘์š”: ํŠธ๋žœ์žญ์…˜์€ ๋ฐ˜๋“œ์‹œ ๋นˆ ๋ฐฐ์—ด๋กœ ์„ค์ • - localStorage.setItem('transactions', JSON.stringify([])); - + localStorage.setItem("transactions", JSON.stringify([])); + // ์ค‘์š”: budgetData_backup๋„ ์„ค์ •ํ•˜์—ฌ ๋ณต๊ตฌ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ - localStorage.setItem('budgetData_backup', JSON.stringify(initialBudgetData)); - localStorage.setItem('categoryBudgets_backup', JSON.stringify(DEFAULT_CATEGORY_BUDGETS)); - localStorage.setItem('transactions_backup', JSON.stringify([])); - + localStorage.setItem( + "budgetData_backup", + JSON.stringify(initialBudgetData) + ); + localStorage.setItem( + "categoryBudgets_backup", + JSON.stringify(DEFAULT_CATEGORY_BUDGETS) + ); + localStorage.setItem("transactions_backup", JSON.stringify([])); + // ์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œ์ผœ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ํŠธ๋ฆฌ๊ฑฐ - ์ด๋ฒคํŠธ ์ˆœ์„œ ์ตœ์ ํ™” const events = [ - new Event('transactionUpdated'), - new Event('budgetDataUpdated'), - new Event('categoryBudgetsUpdated'), - new StorageEvent('storage') + new Event("transactionUpdated"), + new Event("budgetDataUpdated"), + new Event("categoryBudgetsUpdated"), + new StorageEvent("storage"), ]; - + // ๋ชจ๋“  ์ด๋ฒคํŠธ ๋™์‹œ์— ๋ฐœ์ƒ - events.forEach(event => window.dispatchEvent(event)); - + events.forEach((event) => window.dispatchEvent(event)); + // ์ค‘์š”: ์‚ฌ์šฉ์ž ์„ค์ • ๊ฐ’ ๋ณต์› (๋ฐฑ์—…ํ•œ ๊ฐ’์ด ์žˆ๋Š” ๊ฒฝ์šฐ) if (dontShowWelcomeValue) { - console.log('resetAllData - dontShowWelcome ๊ฐ’ ๋ณต์›:', dontShowWelcomeValue); - localStorage.setItem('dontShowWelcome', dontShowWelcomeValue); + storageLogger.info( + "resetAllData - dontShowWelcome ๊ฐ’ ๋ณต์›:", + dontShowWelcomeValue + ); + localStorage.setItem("dontShowWelcome", dontShowWelcomeValue); } - + if (hasVisitedBefore) { - console.log('resetAllData - hasVisitedBefore ๊ฐ’ ๋ณต์›:', hasVisitedBefore); - localStorage.setItem('hasVisitedBefore', hasVisitedBefore); + storageLogger.info( + "resetAllData - hasVisitedBefore ๊ฐ’ ๋ณต์›:", + hasVisitedBefore + ); + localStorage.setItem("hasVisitedBefore", hasVisitedBefore); } - + // ์ฒซ ๋ฐฉ๋ฌธ์ด ์•„๋‹Œ ๊ฒฝ์šฐ์—๋งŒ ํ† ์ŠคํŠธ ์•Œ๋ฆผ ํ‘œ์‹œ - if (hasVisitedBefore === 'true') { + if (hasVisitedBefore === "true") { toast({ title: "๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ", description: "๋ชจ๋“  ์˜ˆ์‚ฐ ๋ฐ ์ง€์ถœ ๋ฐ์ดํ„ฐ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", }); } - - console.log('๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + + storageLogger.info("๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); } catch (error) { - console.error('๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error); + storageLogger.error("๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:", error); // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ํ† ์ŠคํŠธ ์•Œ๋ฆผ toast({ title: "์ดˆ๊ธฐํ™” ์‹คํŒจ", description: "๋ฐ์ดํ„ฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋Š”๋ฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" + variant: "destructive", }); } }; diff --git a/src/contexts/budget/storage/transactionStorage.ts b/src/contexts/budget/storage/transactionStorage.ts index d34b45f..f9cfee3 100644 --- a/src/contexts/budget/storage/transactionStorage.ts +++ b/src/contexts/budget/storage/transactionStorage.ts @@ -1,6 +1,6 @@ - -import { Transaction } from '../types'; -import { toast } from '@/components/ui/use-toast'; +import { Transaction } from "../types"; +import { storageLogger } from "@/utils/logger"; +import { toast } from "@/components/ui/use-toast"; /** * ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—์„œ ํŠธ๋žœ์žญ์…˜ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ @@ -8,87 +8,99 @@ import { toast } from '@/components/ui/use-toast'; export const loadTransactionsFromStorage = (): Transaction[] => { try { // ๋ฉ”์ธ ์Šคํ† ๋ฆฌ์ง€์—์„œ ๋จผ์ € ์‹œ๋„ - const storedTransactions = localStorage.getItem('transactions'); + const storedTransactions = localStorage.getItem("transactions"); if (storedTransactions) { try { const parsedData = JSON.parse(storedTransactions); - console.log('ํŠธ๋žœ์žญ์…˜ ๋กœ๋“œ ์™„๋ฃŒ, ํ•ญ๋ชฉ ์ˆ˜:', parsedData.length); - + storageLogger.info("ํŠธ๋žœ์žญ์…˜ ๋กœ๋“œ ์™„๋ฃŒ, ํ•ญ๋ชฉ ์ˆ˜:", parsedData.length); + // ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ if (Array.isArray(parsedData)) { return parsedData; } else { - console.error('ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐฐ์—ด์ด ์•„๋‹™๋‹ˆ๋‹ค:', typeof parsedData); + storageLogger.error( + "ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐฐ์—ด์ด ์•„๋‹™๋‹ˆ๋‹ค:", + typeof parsedData + ); return []; } } catch (e) { - console.error('ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์˜ค๋ฅ˜:', e); + storageLogger.error("ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์˜ค๋ฅ˜:", e); } } - + // ๋ฐฑ์—…์—์„œ ์‹œ๋„ - const backupTransactions = localStorage.getItem('transactions_backup'); + const backupTransactions = localStorage.getItem("transactions_backup"); if (backupTransactions) { try { const parsedBackup = JSON.parse(backupTransactions); - console.log('๋ฐฑ์—…์—์„œ ํŠธ๋žœ์žญ์…˜ ๋ณต๊ตฌ, ํ•ญ๋ชฉ ์ˆ˜:', parsedBackup.length); + storageLogger.info( + "๋ฐฑ์—…์—์„œ ํŠธ๋žœ์žญ์…˜ ๋ณต๊ตฌ, ํ•ญ๋ชฉ ์ˆ˜:", + parsedBackup.length + ); // ๋ฉ”์ธ ์Šคํ† ๋ฆฌ์ง€๋„ ๋ณต๊ตฌ - localStorage.setItem('transactions', backupTransactions); + localStorage.setItem("transactions", backupTransactions); return parsedBackup; } catch (e) { - console.error('๋ฐฑ์—… ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์˜ค๋ฅ˜:', e); + storageLogger.error("๋ฐฑ์—… ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์˜ค๋ฅ˜:", e); } } } catch (error) { - console.error('ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜:', error); + storageLogger.error("ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜:", error); } - + // ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ ๋นˆ ๋ฐฐ์—ด ๋ฐ˜ํ™˜ - console.log('ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ์—†์Œ, ๋นˆ ๋ฐฐ์—ด ๋ฐ˜ํ™˜'); + storageLogger.info("ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ์—†์Œ, ๋นˆ ๋ฐฐ์—ด ๋ฐ˜ํ™˜"); return []; }; /** * ํŠธ๋žœ์žญ์…˜ ์ €์žฅ */ -export const saveTransactionsToStorage = (transactions: Transaction[]): void => { +export const saveTransactionsToStorage = ( + transactions: Transaction[] +): void => { try { - console.log('ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์‹œ์ž‘, ํ•ญ๋ชฉ ์ˆ˜:', transactions.length); - + storageLogger.info("ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์‹œ์ž‘, ํ•ญ๋ชฉ ์ˆ˜:", transactions.length); + // ๋จผ์ € ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ const dataString = JSON.stringify(transactions); - - // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅ - localStorage.setItem('transactions', dataString); + + // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅ + localStorage.setItem("transactions", dataString); // ๋ฐฑ์—… ์ €์žฅ - localStorage.setItem('transactions_backup', dataString); - - console.log('ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์™„๋ฃŒ, ํ•ญ๋ชฉ ์ˆ˜:', transactions.length); - + localStorage.setItem("transactions_backup", dataString); + + storageLogger.info("ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์™„๋ฃŒ, ํ•ญ๋ชฉ ์ˆ˜:", transactions.length); + // ์Šคํ† ๋ฆฌ์ง€ ์ด๋ฒคํŠธ ์ˆ˜๋™ ํŠธ๋ฆฌ๊ฑฐ (๋™์ผ ์ฐฝ์—์„œ๋„ ๊ฐ์ง€ํ•˜๊ธฐ ์œ„ํ•จ) try { - window.dispatchEvent(new Event('transactionUpdated')); - window.dispatchEvent(new CustomEvent('transactionChanged', { - detail: { type: 'save', count: transactions.length } - })); - window.dispatchEvent(new StorageEvent('storage', { - key: 'transactions', - newValue: dataString - })); + window.dispatchEvent(new Event("transactionUpdated")); + window.dispatchEvent( + new CustomEvent("transactionChanged", { + detail: { type: "save", count: transactions.length }, + }) + ); + window.dispatchEvent( + new StorageEvent("storage", { + key: "transactions", + newValue: dataString, + }) + ); } catch (e) { - console.error('์ด๋ฒคํŠธ ๋ฐœ์ƒ ์˜ค๋ฅ˜:', e); + storageLogger.error("์ด๋ฒคํŠธ ๋ฐœ์ƒ ์˜ค๋ฅ˜:", e); } - + // ๋งˆ์ง€๋ง‰ ์ €์žฅ ์‹œ๊ฐ„ ๊ธฐ๋ก (๋ฐ์ดํ„ฐ ๊ฒ€์ฆ์šฉ) - localStorage.setItem('lastTransactionSaveTime', new Date().toISOString()); + localStorage.setItem("lastTransactionSaveTime", new Date().toISOString()); } catch (error) { - console.error('ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์˜ค๋ฅ˜:', error); - + storageLogger.error("ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์˜ค๋ฅ˜:", error); + // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ํ† ์ŠคํŠธ ์•Œ๋ฆผ toast({ title: "์ง€์ถœ ์ €์žฅ ์‹คํŒจ", description: "์ง€์ถœ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š”๋ฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" + variant: "destructive", }); } }; @@ -99,49 +111,56 @@ export const saveTransactionsToStorage = (transactions: Transaction[]): void => export const clearAllTransactions = (): void => { try { // ๊ธฐ์กด ํ‚ค ์™„์ „ ์‚ญ์ œ - localStorage.removeItem('transactions'); - localStorage.removeItem('transactions_backup'); - + localStorage.removeItem("transactions"); + localStorage.removeItem("transactions_backup"); + // ๋นˆ ๋ฐฐ์—ด์„ ์ €์žฅํ•˜์—ฌ ํ™•์‹คํžˆ ์ดˆ๊ธฐํ™” const emptyData = JSON.stringify([]); - localStorage.setItem('transactions', emptyData); - localStorage.setItem('transactions_backup', emptyData); - + localStorage.setItem("transactions", emptyData); + localStorage.setItem("transactions_backup", emptyData); + // ๊ด€๋ จ ๋ฐฑ์—… ๋ฐ์ดํ„ฐ๋„ ๋ชจ๋‘ ์ดˆ๊ธฐํ™” - const transactionKeys = Object.keys(localStorage).filter(key => - key.includes('transaction') || key.includes('expense') || key.includes('spending') + const transactionKeys = Object.keys(localStorage).filter( + (key) => + key.includes("transaction") || + key.includes("expense") || + key.includes("spending") ); - - transactionKeys.forEach(key => { + + transactionKeys.forEach((key) => { localStorage.removeItem(key); - console.log(`๊ด€๋ จ ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ์‚ญ์ œ: ${key}`); + storageLogger.info(`๊ด€๋ จ ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ์‚ญ์ œ: ${key}`); }); - - console.log('๋ชจ๋“  ํŠธ๋žœ์žญ์…˜์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); - + + storageLogger.info("๋ชจ๋“  ํŠธ๋žœ์žญ์…˜์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + // ์Šคํ† ๋ฆฌ์ง€ ์ด๋ฒคํŠธ ์ˆ˜๋™ ํŠธ๋ฆฌ๊ฑฐ - window.dispatchEvent(new Event('transactionUpdated')); - window.dispatchEvent(new CustomEvent('transactionChanged', { - detail: { type: 'clear' } - })); - window.dispatchEvent(new StorageEvent('storage', { - key: 'transactions', - newValue: emptyData - })); - + window.dispatchEvent(new Event("transactionUpdated")); + window.dispatchEvent( + new CustomEvent("transactionChanged", { + detail: { type: "clear" }, + }) + ); + window.dispatchEvent( + new StorageEvent("storage", { + key: "transactions", + newValue: emptyData, + }) + ); + // ํ† ์ŠคํŠธ ์•Œ๋ฆผ toast({ title: "์ง€์ถœ ๋‚ด์—ญ ์ดˆ๊ธฐํ™”", description: "๋ชจ๋“  ์ง€์ถœ ๋‚ด์—ญ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", }); } catch (error) { - console.error('ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์˜ค๋ฅ˜:', error); - + storageLogger.error("ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์˜ค๋ฅ˜:", error); + // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ํ† ์ŠคํŠธ ์•Œ๋ฆผ toast({ title: "์ดˆ๊ธฐํ™” ์‹คํŒจ", description: "์ง€์ถœ ๋‚ด์—ญ์„ ์ดˆ๊ธฐํ™”ํ•˜๋Š”๋ฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" + variant: "destructive", }); } }; diff --git a/src/contexts/budget/storageUtils.ts b/src/contexts/budget/storageUtils.ts index 3dacf10..c76cb42 100644 --- a/src/contexts/budget/storageUtils.ts +++ b/src/contexts/budget/storageUtils.ts @@ -1,4 +1,3 @@ - // ์™ธ๋ถ€ ํŒŒ์ผ๋กœ ๋ฆฌํŒฉํ† ๋ง๋œ ๋ชจ๋“  ์Šคํ† ๋ฆฌ์ง€ ๊ด€๋ จ ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ฉ์ ์œผ๋กœ ๋‚ด๋ณด๋‚ด๋Š” ํŒŒ์ผ // ๊ธฐ์กด ์ฝ”๋“œ๋Š” storage/ ๋””๋ ‰ํ„ฐ๋ฆฌ๋กœ ์ด๋™๋˜์—ˆ์Šต๋‹ˆ๋‹ค. @@ -7,17 +6,17 @@ export { loadTransactionsFromStorage, saveTransactionsToStorage, clearAllTransactions, - + // ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๊ด€๋ จ ํ•จ์ˆ˜ loadBudgetDataFromStorage, saveBudgetDataToStorage, clearAllBudgetData, - + // ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ จ ํ•จ์ˆ˜ loadCategoryBudgetsFromStorage, saveCategoryBudgetsToStorage, clearAllCategoryBudgets, - + // ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜ - resetAllData -} from './storage'; + resetAllData, +} from "./storage"; diff --git a/src/contexts/budget/types.ts b/src/contexts/budget/types.ts index c6c5da2..be422c0 100644 --- a/src/contexts/budget/types.ts +++ b/src/contexts/budget/types.ts @@ -1,6 +1,7 @@ +import type { PaymentMethod, TransactionType } from "@/types/common"; // ์˜ˆ์‚ฐ ๊ด€๋ จ ํƒ€์ž… ์ •์˜ -export type BudgetPeriod = 'daily' | 'weekly' | 'monthly'; +export type BudgetPeriod = "daily" | "weekly" | "monthly"; export interface BudgetPeriodData { targetAmount: number; @@ -20,6 +21,13 @@ export interface CategoryBudget { total: number; } +// Payment Method ํ†ต๊ณ„ ํƒ€์ž… +export interface PaymentMethodStats { + method: PaymentMethod; + amount: number; + percentage: number; +} + // BudgetContext ํƒ€์ž… ์ •์˜ export interface BudgetContextType { transactions: Transaction[]; @@ -30,21 +38,25 @@ export interface BudgetContextType { addTransaction: (transaction: Transaction) => void; updateTransaction: (updatedTransaction: Transaction) => void; deleteTransaction: (id: string) => void; - handleBudgetGoalUpdate: (type: BudgetPeriod, amount: number, newCategoryBudgets?: Record) => void; + handleBudgetGoalUpdate: ( + type: BudgetPeriod, + amount: number, + newCategoryBudgets?: Record + ) => void; getCategorySpending: () => CategoryBudget[]; - getPaymentMethodStats: () => { method: string; amount: number; percentage: number }[]; + getPaymentMethodStats: () => PaymentMethodStats[]; resetBudgetData?: () => void; // ์„ ํƒ์  ํ•„๋“œ๋กœ ์œ ์ง€ } -// Transaction ํƒ€์ž… (๊ธฐ์กด TransactionCard์—์„œ ๊ฐ€์ ธ์˜ด) +// Transaction ํƒ€์ž… ๊ฐœ์„  export interface Transaction { id: string; title: string; amount: number; date: string; category: string; - type: 'income' | 'expense'; - paymentMethod?: '์‹ ์šฉ์นด๋“œ' | 'ํ˜„๊ธˆ'; // ์ง€์ถœ ๋ฐฉ๋ฒ• ์ถ”๊ฐ€ + type: TransactionType; + paymentMethod?: PaymentMethod; notes?: string; localTimestamp?: string; // ๋กœ์ปฌ ์ˆ˜์ • ํƒ€์ž„์Šคํƒฌํ”„ ์ถ”๊ฐ€ serverTimestamp?: string; // ์„œ๋ฒ„ ํƒ€์ž„์Šคํƒฌํ”„ ์ถ”๊ฐ€ diff --git a/src/contexts/budget/useBudget.ts b/src/contexts/budget/useBudget.ts index 6d362a2..e9a2e45 100644 --- a/src/contexts/budget/useBudget.ts +++ b/src/contexts/budget/useBudget.ts @@ -1,9 +1,10 @@ - -import { createContext, useContext } from 'react'; -import { BudgetContextType } from './types'; +import { createContext, useContext } from "react"; +import { BudgetContextType } from "./types"; // BudgetContext ์ƒ์„ฑ -export const BudgetContext = createContext(undefined); +export const BudgetContext = createContext( + undefined +); // useBudget ํ›… export const useBudget = () => { diff --git a/src/contexts/budget/useBudgetState.ts b/src/contexts/budget/useBudgetState.ts index 298c264..71e4952 100644 --- a/src/contexts/budget/useBudgetState.ts +++ b/src/contexts/budget/useBudgetState.ts @@ -1,11 +1,16 @@ - -import { useEffect } from 'react'; -import { Transaction, BudgetData, BudgetPeriod, BudgetContextType } from './types'; -import { useBudgetDataState } from './hooks/useBudgetDataState'; -import { useTransactionState } from './hooks/useTransactionState'; -import { useCategoryBudgetState } from './hooks/useCategoryBudgetState'; -import { useCategorySpending } from './hooks/useCategorySpending'; -import { useExtendedBudgetUpdate } from './hooks/useExtendedBudgetUpdate'; +import { useEffect } from "react"; +import { logger } from "@/utils/logger"; +import { + Transaction, + BudgetData, + BudgetPeriod, + BudgetContextType, +} from "./types"; +import { useBudgetDataState } from "./hooks/useBudgetDataState"; +import { useTransactionState } from "./hooks/useTransactionState"; +import { useCategoryBudgetState } from "./hooks/useCategoryBudgetState"; +import { useCategorySpending } from "./hooks/useCategorySpending"; +import { useExtendedBudgetUpdate } from "./hooks/useExtendedBudgetUpdate"; /** * ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ํ†ตํ•ฉ ํ›… @@ -13,12 +18,8 @@ import { useExtendedBudgetUpdate } from './hooks/useExtendedBudgetUpdate'; */ export const useBudgetState = () => { // ํŠธ๋žœ์žญ์…˜ ์ƒํƒœ ๊ด€๋ฆฌ - const { - transactions, - addTransaction, - updateTransaction, - deleteTransaction - } = useTransactionState(); + const { transactions, addTransaction, updateTransaction, deleteTransaction } = + useTransactionState(); // ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ƒํƒœ ๊ด€๋ฆฌ const { categoryBudgets, setCategoryBudgets } = useCategoryBudgetState(); @@ -29,9 +30,9 @@ export const useBudgetState = () => { selectedTab, setSelectedTab, handleBudgetGoalUpdate, - resetBudgetData + resetBudgetData, } = useBudgetDataState(transactions); - + // ํ™•์žฅ๋œ ์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ ๋กœ์ง - ๊ธฐ์กด ๋กœ์ง๊ณผ ํ˜ธํ™˜์„ฑ ์œ ์ง€ const extendedBudgetUpdate = useExtendedBudgetUpdate( handleBudgetGoalUpdate, @@ -39,48 +40,60 @@ export const useBudgetState = () => { ); // ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ง€์ถœ ๊ณ„์‚ฐ - const { getCategorySpending } = useCategorySpending(transactions, categoryBudgets); + const { getCategorySpending } = useCategorySpending( + transactions, + categoryBudgets + ); // ๊ฒฐ์ œ ๋ฐฉ๋ฒ• ํ†ต๊ณ„ ๊ณ„์‚ฐ ํ•จ์ˆ˜ const getPaymentMethodStats = () => { // ์ง€์ถœ ํŠธ๋žœ์žญ์…˜ ํ•„ํ„ฐ๋ง - const expenseTransactions = transactions.filter(t => t.type === 'expense'); - + const expenseTransactions = transactions.filter( + (t) => t.type === "expense" + ); + // ์ด ์ง€์ถœ ๊ณ„์‚ฐ - const totalExpense = expenseTransactions.reduce((acc, curr) => acc + curr.amount, 0); - + const totalExpense = expenseTransactions.reduce( + (acc, curr) => acc + curr.amount, + 0 + ); + // ๊ฒฐ์ œ ๋ฐฉ๋ฒ•๋ณ„ ๊ธˆ์•ก ๊ณ„์‚ฐ const cardExpense = expenseTransactions - .filter(t => t.paymentMethod === '์‹ ์šฉ์นด๋“œ' || !t.paymentMethod) // paymentMethod๊ฐ€ ์—†์œผ๋ฉด ์‹ ์šฉ์นด๋“œ๋กœ ๊ฐ„์ฃผ + .filter((t) => t.paymentMethod === "์‹ ์šฉ์นด๋“œ" || !t.paymentMethod) // paymentMethod๊ฐ€ ์—†์œผ๋ฉด ์‹ ์šฉ์นด๋“œ๋กœ ๊ฐ„์ฃผ .reduce((acc, curr) => acc + curr.amount, 0); - + const cashExpense = expenseTransactions - .filter(t => t.paymentMethod === 'ํ˜„๊ธˆ') + .filter((t) => t.paymentMethod === "ํ˜„๊ธˆ") .reduce((acc, curr) => acc + curr.amount, 0); - + // ๊ฒฐ๊ณผ ๋ฐฐ์—ด ์ƒ์„ฑ - ๊ธˆ์•ก์ด ํฐ ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌ const result = [ - { - method: '์‹ ์šฉ์นด๋“œ', + { + method: "์‹ ์šฉ์นด๋“œ", amount: cardExpense, - percentage: totalExpense > 0 ? (cardExpense / totalExpense) * 100 : 0 + percentage: totalExpense > 0 ? (cardExpense / totalExpense) * 100 : 0, }, - { - method: 'ํ˜„๊ธˆ', + { + method: "ํ˜„๊ธˆ", amount: cashExpense, - percentage: totalExpense > 0 ? (cashExpense / totalExpense) * 100 : 0 - } + percentage: totalExpense > 0 ? (cashExpense / totalExpense) * 100 : 0, + }, ].sort((a, b) => b.amount - a.amount); - + return result; }; // ๋””๋ฒ„๊น… ๋กœ๊ทธ ์ถ”๊ฐ€ useEffect(() => { - console.log('์˜ˆ์‚ฐ ์ƒํƒœ ์—…๋ฐ์ดํŠธ:', - 'ํŠธ๋žœ์žญ์…˜ ์ˆ˜:', transactions.length, - '์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ:', categoryBudgets, - '์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:', budgetData + logger.info( + "์˜ˆ์‚ฐ ์ƒํƒœ ์—…๋ฐ์ดํŠธ:", + "ํŠธ๋žœ์žญ์…˜ ์ˆ˜:", + transactions.length, + "์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ:", + categoryBudgets, + "์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:", + budgetData ); }, [transactions, categoryBudgets, budgetData]); diff --git a/src/contexts/budget/utils/budgetCalculation.ts b/src/contexts/budget/utils/budgetCalculation.ts index eef28ec..f82c7ac 100644 --- a/src/contexts/budget/utils/budgetCalculation.ts +++ b/src/contexts/budget/utils/budgetCalculation.ts @@ -1,6 +1,6 @@ - -import { BudgetData, BudgetPeriod } from '../types'; -import { getInitialBudgetData } from './constants'; +import { BudgetData, BudgetPeriod } from "../types"; +import { logger } from "@/utils/logger"; +import { getInitialBudgetData } from "./constants"; // ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ๊ณ„์‚ฐ export const calculateUpdatedBudgetData = ( @@ -8,65 +8,67 @@ export const calculateUpdatedBudgetData = ( type: BudgetPeriod, amount: number ): BudgetData => { - console.log(`์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ ๊ณ„์‚ฐ ์‹œ์ž‘: ํƒ€์ž…=${type}, ๊ธˆ์•ก=${amount}`); - + logger.info(`์˜ˆ์‚ฐ ์—…๋ฐ์ดํŠธ ๊ณ„์‚ฐ ์‹œ์ž‘: ํƒ€์ž…=${type}, ๊ธˆ์•ก=${amount}`); + // ๊ฐ’์ด ์—†๊ฑฐ๋‚˜ ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ๋กœ๊น… if (!prevBudgetData) { - console.error('์ด์ „ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ.'); + logger.error("์ด์ „ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ."); prevBudgetData = getInitialBudgetData(); } - + // ์ผ์ผ/์ฃผ๊ฐ„/์›”๊ฐ„ ์˜ˆ์‚ฐ ๊ธˆ์•ก ์ดˆ๊ธฐํ™” let monthlyAmount = 0; let weeklyAmount = 0; let dailyAmount = 0; - + // ํ•ญ์ƒ ์›”๊ฐ„ ์˜ˆ์‚ฐ์„ ๊ธฐ์ค€์œผ๋กœ ๊ณ„์‚ฐ (์–ด๋–ค ํƒ€์ž…์ด ์ž…๋ ฅ๋˜๋“ ) - if (type === 'monthly') { + if (type === "monthly") { // ์›”๊ฐ„ ์˜ˆ์‚ฐ์ด ์ž…๋ ฅ๋œ ๊ฒฝ์šฐ - ์ด๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ฃผ๊ฐ„/์ผ์ผ ๊ณ„์‚ฐ monthlyAmount = amount; - } else if (type === 'weekly') { + } else if (type === "weekly") { // ์ฃผ๊ฐ„ ์˜ˆ์‚ฐ์ด ์ž…๋ ฅ๋œ ๊ฒฝ์šฐ - ์ด๋ฅผ ์›”๊ฐ„์œผ๋กœ ํ™˜์‚ฐ (4.345์ฃผ/์›” ๊ธฐ์ค€) monthlyAmount = Math.round(amount * 4.345); - } else if (type === 'daily') { + } else if (type === "daily") { // ์ผ์ผ ์˜ˆ์‚ฐ์ด ์ž…๋ ฅ๋œ ๊ฒฝ์šฐ - ์ด๋ฅผ ์›”๊ฐ„์œผ๋กœ ํ™˜์‚ฐ (30์ผ/์›” ๊ธฐ์ค€) monthlyAmount = Math.round(amount * 30); } - + // ์›”๊ฐ„ ์˜ˆ์‚ฐ์„ ๊ธฐ์ค€์œผ๋กœ ์ฃผ๊ฐ„/์ผ์ผ ์˜ˆ์‚ฐ ๊ณ„์‚ฐ weeklyAmount = Math.round(monthlyAmount / 4.345); // ํ•œ ๋‹ฌ ํ‰๊ท  4.345์ฃผ - dailyAmount = Math.round(monthlyAmount / 30); // ํ•œ ๋‹ฌ ํ‰๊ท  30์ผ - - console.log(`์ตœ์ข… ์˜ˆ์‚ฐ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ: ์›”๊ฐ„=${monthlyAmount}์›, ์ฃผ๊ฐ„=${weeklyAmount}์›, ์ผ์ผ=${dailyAmount}์›`); - + dailyAmount = Math.round(monthlyAmount / 30); // ํ•œ ๋‹ฌ ํ‰๊ท  30์ผ + + logger.info( + `์ตœ์ข… ์˜ˆ์‚ฐ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ: ์›”๊ฐ„=${monthlyAmount}์›, ์ฃผ๊ฐ„=${weeklyAmount}์›, ์ผ์ผ=${dailyAmount}์›` + ); + // ๋กœ๊ทธ์— ์ด์ „ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ถœ๋ ฅ - console.log("์ด์ „ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:", JSON.stringify(prevBudgetData)); - + logger.info("์ด์ „ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:", JSON.stringify(prevBudgetData)); + // ์ด์ „ ์ง€์ถœ ๋ฐ์ดํ„ฐ ๋ณด์กด const dailySpent = prevBudgetData.daily?.spentAmount || 0; const weeklySpent = prevBudgetData.weekly?.spentAmount || 0; const monthlySpent = prevBudgetData.monthly?.spentAmount || 0; - + // ์ƒˆ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ (spentAmount๋Š” ์ด์ „ ๊ฐ’ ์œ ์ง€) const updatedBudgetData = { daily: { targetAmount: dailyAmount, spentAmount: dailySpent, - remainingAmount: Math.max(0, dailyAmount - dailySpent) + remainingAmount: Math.max(0, dailyAmount - dailySpent), }, weekly: { targetAmount: weeklyAmount, spentAmount: weeklySpent, - remainingAmount: Math.max(0, weeklyAmount - weeklySpent) + remainingAmount: Math.max(0, weeklyAmount - weeklySpent), }, monthly: { targetAmount: monthlyAmount, spentAmount: monthlySpent, - remainingAmount: Math.max(0, monthlyAmount - monthlySpent) - } + remainingAmount: Math.max(0, monthlyAmount - monthlySpent), + }, }; - - console.log("์ƒˆ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:", JSON.stringify(updatedBudgetData)); - + + logger.info("์ƒˆ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:", JSON.stringify(updatedBudgetData)); + return updatedBudgetData; }; diff --git a/src/contexts/budget/utils/categoryUtils.ts b/src/contexts/budget/utils/categoryUtils.ts index 4b9ba01..11b195d 100644 --- a/src/contexts/budget/utils/categoryUtils.ts +++ b/src/contexts/budget/utils/categoryUtils.ts @@ -1,48 +1,47 @@ - -import { CategoryBudget, Transaction } from '../types'; -import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons'; +import { CategoryBudget, Transaction } from "../types"; +import { EXPENSE_CATEGORIES } from "@/constants/categoryIcons"; // ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ง€์ถœ ๊ณ„์‚ฐ export const calculateCategorySpending = ( transactions: Transaction[], categoryBudgets: Record ): CategoryBudget[] => { - const expenseTransactions = transactions.filter(t => t.type === 'expense'); + const expenseTransactions = transactions.filter((t) => t.type === "expense"); const categorySpending: Record = {}; - + // ๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ์— ๋Œ€ํ•ด ์ดˆ๊ธฐ๊ฐ’ 0 ์„ค์ • - Object.keys(categoryBudgets).forEach(category => { + Object.keys(categoryBudgets).forEach((category) => { // ์ •์˜๋œ ์นดํ…Œ๊ณ ๋ฆฌ๋งŒ ์œ ์ง€ if (EXPENSE_CATEGORIES.includes(category)) { categorySpending[category] = 0; } }); - + // ์ง€์›๋˜๋Š” ์นดํ…Œ๊ณ ๋ฆฌ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • if (Object.keys(categorySpending).length === 0) { - EXPENSE_CATEGORIES.forEach(category => { + EXPENSE_CATEGORIES.forEach((category) => { categorySpending[category] = 0; }); } - - expenseTransactions.forEach(t => { + + expenseTransactions.forEach((t) => { if (t.category in categorySpending) { categorySpending[t.category] += t.amount; } else if (EXPENSE_CATEGORIES.includes(t.category)) { // ์ง€์›๋˜๋Š” ์นดํ…Œ๊ณ ๋ฆฌ์ด์ง€๋งŒ ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ categorySpending[t.category] = t.amount; - } else if (t.category === '๊ตํ†ต๋น„') { + } else if (t.category === "๊ตํ†ต๋น„") { // ์˜ˆ์ „ ์นดํ…Œ๊ณ ๋ฆฌ๋ช… '๊ตํ†ต๋น„'๋ฅผ '๊ตํ†ต'์œผ๋กœ ๋งคํ•‘ - categorySpending['๊ตํ†ต'] = (categorySpending['๊ตํ†ต'] || 0) + t.amount; + categorySpending["๊ตํ†ต"] = (categorySpending["๊ตํ†ต"] || 0) + t.amount; } else { // ์ง€์›๋˜์ง€ ์•Š๋Š” ์นดํ…Œ๊ณ ๋ฆฌ๋Š” '๊ธฐํƒ€'๋กœ ์ง‘๊ณ„ - categorySpending['๊ธฐํƒ€'] = (categorySpending['๊ธฐํƒ€'] || 0) + t.amount; + categorySpending["๊ธฐํƒ€"] = (categorySpending["๊ธฐํƒ€"] || 0) + t.amount; } }); - - return EXPENSE_CATEGORIES.map(category => ({ + + return EXPENSE_CATEGORIES.map((category) => ({ title: category, current: categorySpending[category] || 0, - total: categoryBudgets[category] || 0 + total: categoryBudgets[category] || 0, })); }; diff --git a/src/contexts/budget/utils/constants.ts b/src/contexts/budget/utils/constants.ts index ce4dd42..b497746 100644 --- a/src/contexts/budget/utils/constants.ts +++ b/src/contexts/budget/utils/constants.ts @@ -1,12 +1,11 @@ - -import { BudgetData } from '../types'; +import { BudgetData } from "../types"; // ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ ์ƒ์ˆ˜ (๊ธฐ๋ณธ๊ฐ’์„ 0์œผ๋กœ ์„ค์ •) export const DEFAULT_CATEGORY_BUDGETS: Record = { ์Œ์‹: 0, ์‡ผํ•‘: 0, ๊ตํ†ต: 0, - ๊ธฐํƒ€: 0 + ๊ธฐํƒ€: 0, }; export const DEFAULT_MONTHLY_BUDGET = 0; @@ -17,17 +16,17 @@ export const getInitialBudgetData = (): BudgetData => { daily: { targetAmount: 0, spentAmount: 0, - remainingAmount: 0 + remainingAmount: 0, }, weekly: { targetAmount: 0, spentAmount: 0, - remainingAmount: 0 + remainingAmount: 0, }, monthly: { targetAmount: 0, spentAmount: 0, - remainingAmount: 0 - } + remainingAmount: 0, + }, }; }; diff --git a/src/contexts/budget/utils/spendingCalculation.ts b/src/contexts/budget/utils/spendingCalculation.ts index 794cb66..2d4ae2f 100644 --- a/src/contexts/budget/utils/spendingCalculation.ts +++ b/src/contexts/budget/utils/spendingCalculation.ts @@ -1,48 +1,58 @@ - -import { BudgetData, Transaction } from '../types'; -import { format, isWithinInterval, subDays, startOfMonth, endOfMonth, parseISO } from 'date-fns'; +import { BudgetData, Transaction } from "../types"; +import { logger } from "@/utils/logger"; +import { + format, + isWithinInterval, + subDays, + startOfMonth, + endOfMonth, + parseISO, +} from "date-fns"; // ์ง€์ถœ์•ก ๊ณ„์‚ฐ (์ผ์ผ, ์ฃผ๊ฐ„, ์›”๊ฐ„) export const calculateSpentAmounts = ( transactions: Transaction[], prevBudgetData: BudgetData ): BudgetData => { - console.log("์ง€์ถœ์•ก ๊ณ„์‚ฐ ์‹œ์ž‘, ํŠธ๋žœ์žญ์…˜ ์ˆ˜:", transactions.length); - + logger.info("์ง€์ถœ์•ก ๊ณ„์‚ฐ ์‹œ์ž‘, ํŠธ๋žœ์žญ์…˜ ์ˆ˜:", transactions.length); + // ์ง€์ถœ ๊ฑฐ๋ž˜ ํ•„ํ„ฐ๋ง - const expenseTransactions = transactions.filter(t => t.type === 'expense'); - console.log("ํ•„ํ„ฐ๋ง๋œ ์ง€์ถœ ํŠธ๋žœ์žญ์…˜:", expenseTransactions.length); - + const expenseTransactions = transactions.filter((t) => t.type === "expense"); + logger.info("ํ•„ํ„ฐ๋ง๋œ ์ง€์ถœ ํŠธ๋žœ์žญ์…˜:", expenseTransactions.length); + // ํ˜„์žฌ ๋‚ ์งœ ์ •๋ณด const now = new Date(); - const today = format(now, 'yyyy-MM-dd'); - const currentMonth = format(now, 'yyyy-MM'); + const today = format(now, "yyyy-MM-dd"); + const currentMonth = format(now, "yyyy-MM"); const startOfCurrentMonth = startOfMonth(now); const endOfCurrentMonth = endOfMonth(now); - + // ๊ฐ ์ง€์ถœ ํŠธ๋žœ์žญ์…˜์˜ ๋‚ ์งœ ์ •๋ณด ๋กœ๊น… expenseTransactions.forEach((t, index) => { - if (index < 10) { // ์ฒ˜์Œ 10๊ฐœ๋งŒ ๋กœ๊ทธ ์ถœ๋ ฅ (๋„ˆ๋ฌด ๋งŽ์€ ๋กœ๊ทธ ๋ฐฉ์ง€) - console.log(`ํŠธ๋žœ์žญ์…˜ ${index}: ๋‚ ์งœ=${t.date}, ๊ธˆ์•ก=${t.amount}, ์นดํ…Œ๊ณ ๋ฆฌ=${t.category}`); + if (index < 10) { + // ์ฒ˜์Œ 10๊ฐœ๋งŒ ๋กœ๊ทธ ์ถœ๋ ฅ (๋„ˆ๋ฌด ๋งŽ์€ ๋กœ๊ทธ ๋ฐฉ์ง€) + logger.info( + `ํŠธ๋žœ์žญ์…˜ ${index}: ๋‚ ์งœ=${t.date}, ๊ธˆ์•ก=${t.amount}, ์นดํ…Œ๊ณ ๋ฆฌ=${t.category}` + ); } }); // ๋‚ ์งœ ๋ฌธ์ž์—ด์„ Date ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜ const parseTransactionDate = (dateStr: string): Date | null => { try { - if (dateStr.includes('์˜ค๋Š˜')) { + if (dateStr.includes("์˜ค๋Š˜")) { return new Date(); } - - if (dateStr.includes('์–ด์ œ')) { + + if (dateStr.includes("์–ด์ œ")) { return subDays(new Date(), 1); } - + // "yyyy-MM-dd" ํ˜•์‹ ํ™•์ธ if (/^\d{4}-\d{2}-\d{2}/.test(dateStr)) { return parseISO(dateStr); } - + // "MM์›” dd์ผ" ํ˜•์‹ ํ™•์ธ (ํ•œ๊ตญ์–ด ๋‚ ์งœ) const koreanDateMatch = dateStr.match(/(\d+)์›”\s*(\d+)์ผ/); if (koreanDateMatch) { @@ -51,80 +61,90 @@ export const calculateSpentAmounts = ( const year = now.getFullYear(); return new Date(year, month, day); } - + // ๊ธฐํƒ€ ๋‚ ์งœ ๋ฌธ์ž์—ด์€ ๊ทธ๋Œ€๋กœ Date ์ƒ์„ฑ์ž์— ์ „๋‹ฌ return new Date(dateStr); } catch (e) { - console.error('๋‚ ์งœ ํŒŒ์‹ฑ ์‹คํŒจ:', dateStr, e); + logger.error("๋‚ ์งœ ํŒŒ์‹ฑ ์‹คํŒจ:", dateStr, e); return null; } }; // ์›”๊ฐ„ ์ง€์ถœ ๊ณ„์‚ฐ (ํ˜„์žฌ ๋‹ฌ์˜ ๋ชจ๋“  ์ง€์ถœ) - const monthlyExpenses = expenseTransactions.filter(t => { + const monthlyExpenses = expenseTransactions.filter((t) => { try { const transactionDate = parseTransactionDate(t.date); - if (!transactionDate) return false; - + if (!transactionDate) { + return false; + } + return isWithinInterval(transactionDate, { start: startOfCurrentMonth, - end: endOfCurrentMonth + end: endOfCurrentMonth, }); } catch (e) { - console.error('์›”๊ฐ„ ์ง€์ถœ ํ•„ํ„ฐ๋ง ์˜ค๋ฅ˜:', e); + logger.error("์›”๊ฐ„ ์ง€์ถœ ํ•„ํ„ฐ๋ง ์˜ค๋ฅ˜:", e); // ๋‚ ์งœ ํ˜•์‹์ด ๋ช…ํ™•ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ, ํŠธ๋žœ์žญ์…˜ ๋‚ ์งœ ๋ฌธ์ž์—ด์— ํ˜„์žฌ ์›” ๋ฌธ์ž์—ด์ด ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ - return t.date.includes(currentMonth) || - t.date.includes(format(now, 'M์›”')) || - t.date.includes('์ด๋ฒˆ ๋‹ฌ'); + return ( + t.date.includes(currentMonth) || + t.date.includes(format(now, "M์›”")) || + t.date.includes("์ด๋ฒˆ ๋‹ฌ") + ); } }); - + // ์ฃผ๊ฐ„ ์ง€์ถœ ๊ณ„์‚ฐ (์ตœ๊ทผ 7์ผ) - const weeklyExpenses = expenseTransactions.filter(t => { + const weeklyExpenses = expenseTransactions.filter((t) => { try { const transactionDate = parseTransactionDate(t.date); - if (!transactionDate) return false; - + if (!transactionDate) { + return false; + } + return isWithinInterval(transactionDate, { start: subDays(now, 7), - end: now + end: now, }); } catch (e) { // ๋‚ ์งœ ํ˜•์‹์ด ๋ช…ํ™•ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ, ์ตœ๊ทผ์— ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ์œผ๋กœ ๊ฐ„์ฃผ - return t.date.includes('์˜ค๋Š˜') || - t.date.includes('์–ด์ œ') || - t.date.includes('์ด๋ฒˆ ์ฃผ') || - t.date.includes('์ผ ์ „'); + return ( + t.date.includes("์˜ค๋Š˜") || + t.date.includes("์–ด์ œ") || + t.date.includes("์ด๋ฒˆ ์ฃผ") || + t.date.includes("์ผ ์ „") + ); } }); - + // ์ผ์ผ ์ง€์ถœ ๊ณ„์‚ฐ (์˜ค๋Š˜) - const dailyExpenses = expenseTransactions.filter(t => { + const dailyExpenses = expenseTransactions.filter((t) => { try { const transactionDate = parseTransactionDate(t.date); - if (!transactionDate) return false; - - return format(transactionDate, 'yyyy-MM-dd') === today; + if (!transactionDate) { + return false; + } + + return format(transactionDate, "yyyy-MM-dd") === today; } catch (e) { // ๋‚ ์งœ ํ˜•์‹์ด ๋ช…ํ™•ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ, ์˜ค๋Š˜ ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ์ธ์ง€ ํ™•์ธ - return t.date.includes('์˜ค๋Š˜') || t.date.includes('์ง€๊ธˆ'); + return t.date.includes("์˜ค๋Š˜") || t.date.includes("์ง€๊ธˆ"); } }); - + // ๊ณ„์‚ฐ๋œ ์ง€์ถœ ๊ธˆ์•ก const dailyTotal = dailyExpenses.reduce((sum, t) => sum + t.amount, 0); const weeklyTotal = weeklyExpenses.reduce((sum, t) => sum + t.amount, 0); const monthlyTotal = monthlyExpenses.reduce((sum, t) => sum + t.amount, 0); - - console.log("๊ณ„์‚ฐ๋œ ์ง€์ถœ ๊ธˆ์•ก:", { + + logger.info("๊ณ„์‚ฐ๋œ ์ง€์ถœ ๊ธˆ์•ก:", { ์ผ์ผ: dailyTotal, ์ฃผ๊ฐ„: weeklyTotal, - ์›”๊ฐ„: monthlyTotal + ์›”๊ฐ„: monthlyTotal, }); // ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ์— ์ ์šฉ ์ „ ๋กœ๊ทธ - console.log("๊ธฐ์กด ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:", prevBudgetData); - + logger.info("๊ธฐ์กด ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:", prevBudgetData); + // ๊ธฐ์กด ์˜ˆ์‚ฐ ๋ชฉํ‘œ ์œ ์ง€ const dailyTarget = prevBudgetData?.daily?.targetAmount || 0; const weeklyTarget = prevBudgetData?.weekly?.targetAmount || 0; @@ -135,21 +155,21 @@ export const calculateSpentAmounts = ( daily: { targetAmount: dailyTarget, spentAmount: dailyTotal, - remainingAmount: Math.max(0, dailyTarget - dailyTotal) + remainingAmount: Math.max(0, dailyTarget - dailyTotal), }, weekly: { targetAmount: weeklyTarget, spentAmount: weeklyTotal, - remainingAmount: Math.max(0, weeklyTarget - weeklyTotal) + remainingAmount: Math.max(0, weeklyTarget - weeklyTotal), }, monthly: { targetAmount: monthlyTarget, spentAmount: monthlyTotal, - remainingAmount: Math.max(0, monthlyTarget - monthlyTotal) - } + remainingAmount: Math.max(0, monthlyTarget - monthlyTotal), + }, }; - - console.log("์—…๋ฐ์ดํŠธ๋œ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:", updatedBudget); - + + logger.info("์—…๋ฐ์ดํŠธ๋œ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ:", updatedBudget); + return updatedBudget; }; diff --git a/src/contexts/budget/utils/storageUtils.ts b/src/contexts/budget/utils/storageUtils.ts index 066a2a9..54499fc 100644 --- a/src/contexts/budget/utils/storageUtils.ts +++ b/src/contexts/budget/utils/storageUtils.ts @@ -1,26 +1,30 @@ - -import { BudgetData } from '../types'; -import { getInitialBudgetData } from './constants'; -import { toast } from '@/hooks/useToast.wrapper'; +import { BudgetData } from "../types"; +import { storageLogger } from "@/utils/logger"; +import { getInitialBudgetData } from "./constants"; +import { toast } from "@/hooks/useToast.wrapper"; // ์Šคํ† ๋ฆฌ์ง€์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ -export const safelyLoadBudgetData = (defaultData: BudgetData = getInitialBudgetData()): BudgetData => { +export const safelyLoadBudgetData = ( + defaultData: BudgetData = getInitialBudgetData() +): BudgetData => { try { - const budgetDataStr = localStorage.getItem('budgetData'); + const budgetDataStr = localStorage.getItem("budgetData"); if (budgetDataStr) { const parsed = JSON.parse(budgetDataStr); - + // ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ๊ฒ€์ฆ (daily, weekly, monthly ํ‚ค ์กด์žฌ ํ™•์ธ) if (parsed && parsed.daily && parsed.weekly && parsed.monthly) { return parsed; } else { - console.warn('์ €์žฅ๋œ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ.'); + storageLogger.warn( + "์ €์žฅ๋œ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ." + ); } } } catch (error) { - console.error('์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์˜ค๋ฅ˜:', error); + storageLogger.error("์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์˜ค๋ฅ˜:", error); } - + // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ๋˜๋Š” ๋ฐ์ดํ„ฐ ์—†์Œ ์‹œ ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜ return defaultData; }; @@ -30,36 +34,38 @@ export const safeStorage = { get: (key: string, defaultValue: any = null): any => { try { const value = localStorage.getItem(key); - if (value === null) return defaultValue; + if (value === null) { + return defaultValue; + } return JSON.parse(value); } catch (error) { - console.error(`์Šคํ† ๋ฆฌ์ง€ ์ฝ๊ธฐ ์˜ค๋ฅ˜ (${key}):`, error); + storageLogger.error(`์Šคํ† ๋ฆฌ์ง€ ์ฝ๊ธฐ ์˜ค๋ฅ˜ (${key}):`, error); return defaultValue; } }, - + set: (key: string, value: any): boolean => { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch (error) { - console.error(`์Šคํ† ๋ฆฌ์ง€ ์“ฐ๊ธฐ ์˜ค๋ฅ˜ (${key}):`, error); + storageLogger.error(`์Šคํ† ๋ฆฌ์ง€ ์“ฐ๊ธฐ ์˜ค๋ฅ˜ (${key}):`, error); toast({ title: "์ €์žฅ ์˜ค๋ฅ˜", description: "๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" + variant: "destructive", }); return false; } }, - + remove: (key: string): boolean => { try { localStorage.removeItem(key); return true; } catch (error) { - console.error(`์Šคํ† ๋ฆฌ์ง€ ์‚ญ์ œ ์˜ค๋ฅ˜ (${key}):`, error); + storageLogger.error(`์Šคํ† ๋ฆฌ์ง€ ์‚ญ์ œ ์˜ค๋ฅ˜ (${key}):`, error); return false; } - } + }, }; diff --git a/src/hooks/auth/useAppwriteAuth.ts b/src/hooks/auth/useAppwriteAuth.ts index 72ad9f3..adad847 100644 --- a/src/hooks/auth/useAppwriteAuth.ts +++ b/src/hooks/auth/useAppwriteAuth.ts @@ -1,6 +1,6 @@ -import { useState, useEffect, useCallback } from 'react'; -import { account } from '@/lib/appwrite'; -import { ID } from 'appwrite'; +import { useState, useEffect, useCallback } from "react"; +import { account } from "@/lib/appwrite"; +import { ID } from "appwrite"; // ์ธ์ฆ ์ƒํƒœ ์ธํ„ฐํŽ˜์ด์Šค interface AuthState { @@ -29,7 +29,7 @@ export const useAppwriteAuth = () => { const [authState, setAuthState] = useState({ user: null, loading: true, - error: null + error: null, }); // ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์ƒํƒœ ์ถ”์  @@ -43,7 +43,7 @@ export const useAppwriteAuth = () => { setAuthState({ user, loading: false, - error: null + error: null, }); } return user; @@ -52,7 +52,7 @@ export const useAppwriteAuth = () => { setAuthState({ user: null, loading: false, - error: error as Error + error: error as Error, }); } return null; @@ -60,97 +60,101 @@ export const useAppwriteAuth = () => { }, [isMounted]); // ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๋กœ๊ทธ์ธ - const login = useCallback(async ({ email, password }: LoginCredentials) => { - try { - setAuthState(prev => ({ ...prev, loading: true, error: null })); - - // ๋น„๋™๊ธฐ ์ž‘์—… ์‹œ์ž‘ ์ „ UI ์Šค๋ ˆ๋“œ ์ฐจ๋‹จ ๋ฐฉ์ง€ - await new Promise(resolve => setTimeout(resolve, 0)); - - const session = await account.createEmailPasswordSession(email, password); - const user = await account.get(); - - if (isMounted) { - setAuthState({ - user, - loading: false, - error: null - }); + const login = useCallback( + async ({ email, password }: LoginCredentials) => { + try { + setAuthState((prev) => ({ ...prev, loading: true, error: null })); + + // ๋น„๋™๊ธฐ ์ž‘์—… ์‹œ์ž‘ ์ „ UI ์Šค๋ ˆ๋“œ ์ฐจ๋‹จ ๋ฐฉ์ง€ + await new Promise((resolve) => setTimeout(resolve, 0)); + + const session = await account.createEmailPasswordSession( + email, + password + ); + const user = await account.get(); + + if (isMounted) { + setAuthState({ + user, + loading: false, + error: null, + }); + } + + return { user, session }; + } catch (error) { + if (isMounted) { + setAuthState((prev) => ({ + ...prev, + loading: false, + error: error as Error, + })); + } + throw error; } - - return { user, session }; - } catch (error) { - if (isMounted) { - setAuthState(prev => ({ - ...prev, - loading: false, - error: error as Error - })); - } - throw error; - } - }, [isMounted]); + }, + [isMounted] + ); // ํšŒ์›๊ฐ€์ž… - const signup = useCallback(async ({ email, password, name }: SignupCredentials) => { - try { - setAuthState(prev => ({ ...prev, loading: true, error: null })); - - // ๋น„๋™๊ธฐ ์ž‘์—… ์‹œ์ž‘ ์ „ UI ์Šค๋ ˆ๋“œ ์ฐจ๋‹จ ๋ฐฉ์ง€ - await new Promise(resolve => setTimeout(resolve, 0)); - - const user = await account.create( - ID.unique(), - email, - password, - name - ); - - // ํšŒ์›๊ฐ€์ž… ํ›„ ์ž๋™ ๋กœ๊ทธ์ธ - await account.createEmailPasswordSession(email, password); - - if (isMounted) { - setAuthState({ - user, - loading: false, - error: null - }); + const signup = useCallback( + async ({ email, password, name }: SignupCredentials) => { + try { + setAuthState((prev) => ({ ...prev, loading: true, error: null })); + + // ๋น„๋™๊ธฐ ์ž‘์—… ์‹œ์ž‘ ์ „ UI ์Šค๋ ˆ๋“œ ์ฐจ๋‹จ ๋ฐฉ์ง€ + await new Promise((resolve) => setTimeout(resolve, 0)); + + const user = await account.create(ID.unique(), email, password, name); + + // ํšŒ์›๊ฐ€์ž… ํ›„ ์ž๋™ ๋กœ๊ทธ์ธ + await account.createEmailPasswordSession(email, password); + + if (isMounted) { + setAuthState({ + user, + loading: false, + error: null, + }); + } + + return user; + } catch (error) { + if (isMounted) { + setAuthState((prev) => ({ + ...prev, + loading: false, + error: error as Error, + })); + } + throw error; } - - return user; - } catch (error) { - if (isMounted) { - setAuthState(prev => ({ - ...prev, - loading: false, - error: error as Error - })); - } - throw error; - } - }, [isMounted]); + }, + [isMounted] + ); // ๋กœ๊ทธ์•„์›ƒ const logout = useCallback(async () => { try { - setAuthState(prev => ({ ...prev, loading: true })); - + setAuthState((prev) => ({ ...prev, loading: true })); + // ํ˜„์žฌ ์„ธ์…˜ ์‚ญ์ œ - await account.deleteSession('current'); - + await account.deleteSession("current"); + if (isMounted) { setAuthState({ user: null, loading: false, - error: null + error: null, }); } } catch (error) { if (isMounted) { - setAuthState(prev => ({ + setAuthState((prev) => ({ ...prev, loading: false, - error: error as Error + error: error as Error, })); } throw error; @@ -161,7 +165,7 @@ export const useAppwriteAuth = () => { useEffect(() => { setIsMounted(true); getCurrentUser(); - + // ์ •๋ฆฌ ํ•จ์ˆ˜ return () => { setIsMounted(false); @@ -175,7 +179,7 @@ export const useAppwriteAuth = () => { login, signup, logout, - getCurrentUser + getCurrentUser, }; }; diff --git a/src/hooks/budget/useBudgetTabContent.ts b/src/hooks/budget/useBudgetTabContent.ts index bd787c8..0629fe0 100644 --- a/src/hooks/budget/useBudgetTabContent.ts +++ b/src/hooks/budget/useBudgetTabContent.ts @@ -1,6 +1,6 @@ - -import { useState, useEffect } from 'react'; -import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons'; +import { useState, useEffect } from "react"; +import { logger } from "@/utils/logger"; +import { EXPENSE_CATEGORIES } from "@/constants/categoryIcons"; interface BudgetData { targetAmount: number; @@ -11,7 +11,10 @@ interface BudgetData { interface UseBudgetTabContentProps { data: BudgetData; calculatePercentage: (spent: number, target: number) => number; - onSaveBudget: (amount: number, categoryBudgets?: Record) => void; + onSaveBudget: ( + amount: number, + categoryBudgets?: Record + ) => void; } interface UseBudgetTabContentReturn { @@ -33,53 +36,64 @@ interface UseBudgetTabContentReturn { export const useBudgetTabContent = ({ data, calculatePercentage, - onSaveBudget + onSaveBudget, }: UseBudgetTabContentProps): UseBudgetTabContentReturn => { - const [categoryBudgets, setCategoryBudgets] = useState>({}); + const [categoryBudgets, setCategoryBudgets] = useState< + Record + >({}); const spentAmount = data.spentAmount; const targetAmount = data.targetAmount; // ๋กœ๊ทธ ์ถ”๊ฐ€ - ๋ฐ›์€ ๋ฐ์ดํ„ฐ ํ™•์ธ useEffect(() => { - console.log(`BudgetTabContent ์ˆ˜์‹  ๋ฐ์ดํ„ฐ:`, data); + logger.info(`BudgetTabContent ์ˆ˜์‹  ๋ฐ์ดํ„ฐ:`, data); }, [data]); // ์ „์—ญ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ ๋กœ์ปฌ ์ƒํƒœ ๊ฐฑ์‹  useEffect(() => { const handleBudgetDataUpdated = () => { - console.log(`BudgetTabContent: ์ „์—ญ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ด๋ฒคํŠธ ๊ฐ์ง€, ํ˜„์žฌ targetAmount=${targetAmount}`); + logger.info( + `BudgetTabContent: ์ „์—ญ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ด๋ฒคํŠธ ๊ฐ์ง€, ํ˜„์žฌ targetAmount=${targetAmount}` + ); }; - window.addEventListener('budgetDataUpdated', handleBudgetDataUpdated); - return () => window.removeEventListener('budgetDataUpdated', handleBudgetDataUpdated); + window.addEventListener("budgetDataUpdated", handleBudgetDataUpdated); + return () => + window.removeEventListener("budgetDataUpdated", handleBudgetDataUpdated); }, [targetAmount]); // ์˜ˆ์‚ฐ ์„ค์ • ์—ฌ๋ถ€ ํ™•์ธ - ๋ฐ์ดํ„ฐ targetAmount๊ฐ€ ์‹ค์ œ๋กœ 0๋ณด๋‹ค ํฐ์ง€ ํ™•์ธ const isBudgetSet = targetAmount > 0; // ์‹ค์ œ ๋ฐฑ๋ถ„์œจ ๊ณ„์‚ฐ (์ดˆ๊ณผํ•ด๋„ ์‹ค์ œ ํผ์„ผํŠธ๋กœ ํ‘œ์‹œ) - const actualPercentage = targetAmount > 0 ? Math.round((spentAmount / targetAmount) * 100) : 0; + const actualPercentage = + targetAmount > 0 ? Math.round((spentAmount / targetAmount) * 100) : 0; const percentage = Math.min(actualPercentage, 100); // ๋Œ€์‹œ๋ณด๋“œ ํ‘œ์‹œ์šฉ์œผ๋กœ๋Š” 100% ์ œํ•œ - + // ์˜ˆ์‚ฐ ์ดˆ๊ณผ ์—ฌ๋ถ€ ๊ณ„์‚ฐ const isOverBudget = spentAmount > targetAmount && targetAmount > 0; // ์˜ˆ์‚ฐ์ด ์–ผ๋งˆ ๋‚จ์ง€ ์•Š์€ ๊ฒฝ์šฐ (10% ๋ฏธ๋งŒ) - const isLowBudget = targetAmount > 0 && actualPercentage >= 90 && actualPercentage < 100; + const isLowBudget = + targetAmount > 0 && actualPercentage >= 90 && actualPercentage < 100; // ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” ์ƒ‰์ƒ ๊ฒฐ์ • - const progressBarColor = isOverBudget ? 'bg-red-500' : isLowBudget ? 'bg-yellow-400' : 'bg-neuro-income'; + const progressBarColor = isOverBudget + ? "bg-red-500" + : isLowBudget + ? "bg-yellow-400" + : "bg-neuro-income"; // ๋‚จ์€ ์˜ˆ์‚ฐ ๋˜๋Š” ์ดˆ๊ณผ ์˜ˆ์‚ฐ ํ…์ŠคํŠธ ๋ฐ ๊ธˆ์•ก - const budgetStatusText = isOverBudget ? '์˜ˆ์‚ฐ ์ดˆ๊ณผ: ' : '๋‚จ์€ ์˜ˆ์‚ฐ: '; - const budgetAmount = isOverBudget ? - Math.abs(targetAmount - spentAmount).toLocaleString() : - Math.max(0, targetAmount - spentAmount).toLocaleString(); - + const budgetStatusText = isOverBudget ? "์˜ˆ์‚ฐ ์ดˆ๊ณผ: " : "๋‚จ์€ ์˜ˆ์‚ฐ: "; + const budgetAmount = isOverBudget + ? Math.abs(targetAmount - spentAmount).toLocaleString() + : Math.max(0, targetAmount - spentAmount).toLocaleString(); + const handleCategoryInputChange = (value: string, category: string) => { - const numValue = parseInt(value.replace(/,/g, ''), 10) || 0; - setCategoryBudgets(prev => ({ + const numValue = parseInt(value.replace(/,/g, ""), 10) || 0; + setCategoryBudgets((prev) => ({ ...prev, - [category]: numValue + [category]: numValue, })); }; @@ -87,10 +101,10 @@ export const useBudgetTabContent = ({ const calculateTotalBudget = () => { // ๋ชจ๋“  EXPENSE_CATEGORIES์— ์žˆ๋Š” ์นดํ…Œ๊ณ ๋ฆฌ ํฌํ•จํ•ด์„œ ํ•ฉ๊ณ„ ๊ณ„์‚ฐ let total = 0; - EXPENSE_CATEGORIES.forEach(category => { + EXPENSE_CATEGORIES.forEach((category) => { total += categoryBudgets[category] || 0; }); - console.log('์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ดํ•ฉ:', total, categoryBudgets); + logger.info("์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ดํ•ฉ:", total, categoryBudgets); return total; }; @@ -98,25 +112,29 @@ export const useBudgetTabContent = ({ const handleSaveCategoryBudgets = () => { // ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • - ๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ ํฌํ•จ const updatedCategoryBudgets: Record = {}; - EXPENSE_CATEGORIES.forEach(category => { + EXPENSE_CATEGORIES.forEach((category) => { updatedCategoryBudgets[category] = categoryBudgets[category] || 0; }); - + const totalBudget = calculateTotalBudget(); - console.log('์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ €์žฅ ๋ฐ ์ด ์˜ˆ์‚ฐ ์„ค์ •:', totalBudget, updatedCategoryBudgets); - + logger.info( + "์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ์ €์žฅ ๋ฐ ์ด ์˜ˆ์‚ฐ ์„ค์ •:", + totalBudget, + updatedCategoryBudgets + ); + // ์ด์•ก์ด 0์ด ์•„๋‹ ๋•Œ๋งŒ ์ €์žฅ ์ฒ˜๋ฆฌ if (totalBudget > 0) { // ๋ช…์‹œ์ ์œผ๋กœ ์›”๊ฐ„ ์˜ˆ์‚ฐ์œผ๋กœ ์„ค์ • - ํ•ญ์ƒ ์›”๊ฐ„ ์˜ˆ์‚ฐ๋งŒ ์ €์žฅ onSaveBudget(totalBudget, updatedCategoryBudgets); - + // ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์ถ”๊ฐ€ (๋ฐ์ดํ„ฐ ์ €์žฅ ํ›„ ์ฆ‰์‹œ UI ์—…๋ฐ์ดํŠธ๋ฅผ ์œ„ํ•ด) setTimeout(() => { - console.log('์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ €์žฅ ํ›„ ์ด๋ฒคํŠธ ๋ฐœ์ƒ'); - window.dispatchEvent(new Event('budgetDataUpdated')); + logger.info("์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ์ €์žฅ ํ›„ ์ด๋ฒคํŠธ ๋ฐœ์ƒ"); + window.dispatchEvent(new Event("budgetDataUpdated")); }, 200); } else { - alert('์˜ˆ์‚ฐ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.'); + alert("์˜ˆ์‚ฐ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."); } }; @@ -124,14 +142,14 @@ export const useBudgetTabContent = ({ useEffect(() => { // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ try { - const storedCategoryBudgets = localStorage.getItem('categoryBudgets'); + const storedCategoryBudgets = localStorage.getItem("categoryBudgets"); if (storedCategoryBudgets) { const parsedBudgets = JSON.parse(storedCategoryBudgets); - console.log('์ €์žฅ๋œ ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋ถˆ๋Ÿฌ์˜ด:', parsedBudgets); + logger.info("์ €์žฅ๋œ ์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋ถˆ๋Ÿฌ์˜ด:", parsedBudgets); setCategoryBudgets(parsedBudgets); } } catch (error) { - console.error('์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์˜ค๋ฅ˜:', error); + logger.error("์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์˜ค๋ฅ˜:", error); } }, []); @@ -139,7 +157,10 @@ export const useBudgetTabContent = ({ const budgetButtonText = isBudgetSet ? "์˜ˆ์‚ฐ ์ˆ˜์ •ํ•˜๊ธฐ" : "์˜ˆ์‚ฐ ์ž…๋ ฅํ•˜๊ธฐ"; // ํ™”๋ฉด์— ํ‘œ์‹œํ•  ๋‚ด์šฉ - ๋””๋ฒ„๊น…์„ ์œ„ํ•œ ๋กœ๊ทธ ์ถ”๊ฐ€ - console.log(`BudgetTabContent ๋ Œ๋”๋ง: targetAmount=${targetAmount}, isBudgetSet=${isBudgetSet}, ํ‘œ์‹œ๋  ํ™”๋ฉด:`, isBudgetSet ? "์˜ˆ์‚ฐ ์ง„ํ–‰ ์ƒํ™ฉ" : "์˜ˆ์‚ฐ ์ž…๋ ฅํ•˜๊ธฐ ๋ฒ„ํŠผ"); + logger.info( + `BudgetTabContent ๋ Œ๋”๋ง: targetAmount=${targetAmount}, isBudgetSet=${isBudgetSet}, ํ‘œ์‹œ๋  ํ™”๋ฉด:`, + isBudgetSet ? "์˜ˆ์‚ฐ ์ง„ํ–‰ ์ƒํ™ฉ" : "์˜ˆ์‚ฐ ์ž…๋ ฅํ•˜๊ธฐ ๋ฒ„ํŠผ" + ); return { categoryBudgets, @@ -154,6 +175,6 @@ export const useBudgetTabContent = ({ budgetStatusText, budgetAmount, budgetButtonText, - calculateTotalBudget + calculateTotalBudget, }; }; diff --git a/src/hooks/sync/index.ts b/src/hooks/sync/index.ts index f2e2b96..9c49ca0 100644 --- a/src/hooks/sync/index.ts +++ b/src/hooks/sync/index.ts @@ -1,10 +1,9 @@ - // ์ฃผ์š” ๋™๊ธฐํ™” ํ›… ๋‚ด๋ณด๋‚ด๊ธฐ -export * from './useSyncToggle'; -export * from './useManualSync'; -export * from './useSyncStatus'; -export * from './syncTime'; -export * from './syncResultHandler'; -export * from './syncPerformer'; -export * from './syncNetworkChecker'; -export * from './syncBackupUtils'; +export * from "./useSyncToggle"; +export * from "./useManualSync"; +export * from "./useSyncStatus"; +export * from "./syncTime"; +export * from "./syncResultHandler"; +export * from "./syncPerformer"; +export * from "./syncNetworkChecker"; +export * from "./syncBackupUtils"; diff --git a/src/hooks/sync/syncBackupUtils.ts b/src/hooks/sync/syncBackupUtils.ts index 1146517..29e4a27 100644 --- a/src/hooks/sync/syncBackupUtils.ts +++ b/src/hooks/sync/syncBackupUtils.ts @@ -1,22 +1,21 @@ - /** * ๋กœ์ปฌ ๋ฐ์ดํ„ฐ ๋ฐฑ์—… ๋งŒ๋“ค๊ธฐ */ export const createLocalDataBackup = () => { - const budgetDataBackup = localStorage.getItem('budgetData'); - const categoryBudgetsBackup = localStorage.getItem('categoryBudgets'); - const transactionsBackup = localStorage.getItem('transactions'); - - console.log('๋กœ์ปฌ ๋ฐ์ดํ„ฐ ๋ฐฑ์—…:', { - budgetData: budgetDataBackup ? '์žˆ์Œ' : '์—†์Œ', - categoryBudgets: categoryBudgetsBackup ? '์žˆ์Œ' : '์—†์Œ', - transactions: transactionsBackup ? '์žˆ์Œ' : '์—†์Œ' + const budgetDataBackup = localStorage.getItem("budgetData"); + const categoryBudgetsBackup = localStorage.getItem("categoryBudgets"); + const transactionsBackup = localStorage.getItem("transactions"); + + syncLogger.info("๋กœ์ปฌ ๋ฐ์ดํ„ฐ ๋ฐฑ์—…:", { + budgetData: budgetDataBackup ? "์žˆ์Œ" : "์—†์Œ", + categoryBudgets: categoryBudgetsBackup ? "์žˆ์Œ" : "์—†์Œ", + transactions: transactionsBackup ? "์žˆ์Œ" : "์—†์Œ", }); - + return { budgetDataBackup, categoryBudgetsBackup, - transactionsBackup + transactionsBackup, }; }; @@ -24,29 +23,30 @@ export const createLocalDataBackup = () => { * ๋กœ์ปฌ ๋ฐ์ดํ„ฐ ๋ณต์›ํ•˜๊ธฐ */ export const restoreLocalDataBackup = (backup: { - budgetDataBackup: string | null, - categoryBudgetsBackup: string | null, - transactionsBackup: string | null + budgetDataBackup: string | null; + categoryBudgetsBackup: string | null; + transactionsBackup: string | null; }) => { - const { budgetDataBackup, categoryBudgetsBackup, transactionsBackup } = backup; - - console.log('๋กœ์ปฌ ๋ฐ์ดํ„ฐ ๋ณต์› ์‹œ๋„'); - + const { budgetDataBackup, categoryBudgetsBackup, transactionsBackup } = + backup; + + syncLogger.info("๋กœ์ปฌ ๋ฐ์ดํ„ฐ ๋ณต์› ์‹œ๋„"); + // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ๋ฐฑ์—… ๋ฐ์ดํ„ฐ ๋ณต์› if (budgetDataBackup) { - localStorage.setItem('budgetData', budgetDataBackup); + localStorage.setItem("budgetData", budgetDataBackup); } if (categoryBudgetsBackup) { - localStorage.setItem('categoryBudgets', categoryBudgetsBackup); + localStorage.setItem("categoryBudgets", categoryBudgetsBackup); } if (transactionsBackup) { - localStorage.setItem('transactions', transactionsBackup); + localStorage.setItem("transactions", transactionsBackup); } - + // ์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œ์ผœ UI ์—…๋ฐ์ดํŠธ - window.dispatchEvent(new Event('budgetDataUpdated')); - window.dispatchEvent(new Event('categoryBudgetsUpdated')); - window.dispatchEvent(new Event('transactionUpdated')); - - console.log('๋กœ์ปฌ ๋ฐ์ดํ„ฐ ๋ณต์› ์™„๋ฃŒ'); + window.dispatchEvent(new Event("budgetDataUpdated")); + window.dispatchEvent(new Event("categoryBudgetsUpdated")); + window.dispatchEvent(new Event("transactionUpdated")); + + syncLogger.info("๋กœ์ปฌ ๋ฐ์ดํ„ฐ ๋ณต์› ์™„๋ฃŒ"); }; diff --git a/src/hooks/sync/syncNetworkChecker.ts b/src/hooks/sync/syncNetworkChecker.ts index 0a5b324..bfe2842 100644 --- a/src/hooks/sync/syncNetworkChecker.ts +++ b/src/hooks/sync/syncNetworkChecker.ts @@ -1,6 +1,6 @@ - -import { checkNetworkStatus } from '@/utils/network/checker'; -import { toast } from '@/hooks/useToast.wrapper'; +import { checkNetworkStatus } from "@/utils/network/checker"; +import { syncLogger } from "@/utils/logger"; +import { toast } from "@/hooks/useToast.wrapper"; /** * ๋™๊ธฐํ™”๋ฅผ ์œ„ํ•œ ๋„คํŠธ์›Œํฌ ์ƒํƒœ ํ™•์ธ @@ -8,20 +8,27 @@ import { toast } from '@/hooks/useToast.wrapper'; export const checkSyncNetworkStatus = async (): Promise => { // ๊ธฐ๋ณธ ๋„คํŠธ์›Œํฌ ์ƒํƒœ ํ™•์ธ - navigator.onLine ์šฐ์„  ์‚ฌ์šฉ const navigatorOnline = navigator.onLine; - console.log(`[๋™๊ธฐํ™”] ๊ธฐ๋ณธ ๋„คํŠธ์›Œํฌ ์ƒํƒœ ํ™•์ธ: ${navigatorOnline ? '์˜จ๋ผ์ธ' : '์˜คํ”„๋ผ์ธ'}`); - + syncLogger.info( + `[๋™๊ธฐํ™”] ๊ธฐ๋ณธ ๋„คํŠธ์›Œํฌ ์ƒํƒœ ํ™•์ธ: ${navigatorOnline ? "์˜จ๋ผ์ธ" : "์˜คํ”„๋ผ์ธ"}` + ); + if (!navigatorOnline) { return false; } - + // ๊ฐ•ํ™”๋œ ๋„คํŠธ์›Œํฌ ํ™•์ธ ์‹œ๋„ (์‹คํŒจํ•ด๋„ ๊ณ„์† ์ง„ํ–‰) try { const isOnline = await checkNetworkStatus(); - console.log(`[๋™๊ธฐํ™”] ๊ฐ•ํ™”๋œ ๋„คํŠธ์›Œํฌ ํ™•์ธ ๊ฒฐ๊ณผ: ${isOnline ? '์˜จ๋ผ์ธ' : '์˜คํ”„๋ผ์ธ'}`); + syncLogger.info( + `[๋™๊ธฐํ™”] ๊ฐ•ํ™”๋œ ๋„คํŠธ์›Œํฌ ํ™•์ธ ๊ฒฐ๊ณผ: ${isOnline ? "์˜จ๋ผ์ธ" : "์˜คํ”„๋ผ์ธ"}` + ); return isOnline; } catch (error) { // ๋„คํŠธ์›Œํฌ ํ™•์ธ ์‹คํŒจํ•ด๋„ navigator.onLine์ด true๋ฉด ์ง„ํ–‰ - console.warn('[๋™๊ธฐํ™”] ๊ฐ•ํ™”๋œ ๋„คํŠธ์›Œํฌ ํ™•์ธ ์‹คํŒจ, ๊ธฐ๋ณธ ์ƒํƒœ ์‚ฌ์šฉ:', error); + syncLogger.warn( + "[๋™๊ธฐํ™”] ๊ฐ•ํ™”๋œ ๋„คํŠธ์›Œํฌ ํ™•์ธ ์‹คํŒจ, ๊ธฐ๋ณธ ์ƒํƒœ ์‚ฌ์šฉ:", + error + ); return navigatorOnline; } }; @@ -34,12 +41,12 @@ export const showNetworkErrorNotification = ( ) => { const title = "๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ํ•„์š”"; const description = "๋™๊ธฐํ™”๋ฅผ ์œ„ํ•ด ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."; - + toast({ title, description, - variant: "destructive" + variant: "destructive", }); - + addNotification(title, description); }; diff --git a/src/hooks/sync/syncPerformer.ts b/src/hooks/sync/syncPerformer.ts index bcd0daa..5ecad3c 100644 --- a/src/hooks/sync/syncPerformer.ts +++ b/src/hooks/sync/syncPerformer.ts @@ -1,44 +1,46 @@ - -import { trySyncAllData } from '@/utils/syncUtils'; -import { setLastSyncTime } from '@/utils/syncUtils'; +import { trySyncAllData } from "@/utils/syncUtils"; +import { syncLogger } from "@/utils/logger"; +import { setLastSyncTime } from "@/utils/syncUtils"; /** * ์‹ค์ œ ๋™๊ธฐํ™” ์ˆ˜ํ–‰ ํ•จ์ˆ˜ (์ตœ๋Œ€ 2ํšŒ๊นŒ์ง€ ์ž๋™ ์žฌ์‹œ๋„) */ export const performSync = async (userId: string) => { - if (!userId) return; - + if (!userId) { + return; + } + let attempts = 0; const maxAttempts = 2; - + while (attempts < maxAttempts) { try { attempts++; - console.log(`๋™๊ธฐํ™” ์‹œ๋„ ${attempts}/${maxAttempts}`); - + syncLogger.info(`๋™๊ธฐํ™” ์‹œ๋„ ${attempts}/${maxAttempts}`); + // ๋„คํŠธ์›Œํฌ ์ƒํƒœ ํ™•์ธ - ๊ธฐ๋ณธ navigator.onLine ์‚ฌ์šฉ if (!navigator.onLine) { - console.log('๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์—†์Œ, ๋™๊ธฐํ™” ๊ฑด๋„ˆ๋œ€'); - throw new Error('๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ํ•„์š”'); + syncLogger.info("๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์—†์Œ, ๋™๊ธฐํ™” ๊ฑด๋„ˆ๋œ€"); + throw new Error("๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ํ•„์š”"); } - + const result = await trySyncAllData(userId); - + // ๋™๊ธฐํ™” ์„ฑ๊ณต ์‹œ ๋งˆ์ง€๋ง‰ ๋™๊ธฐํ™” ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ if (result && result.success) { const currentTime = new Date().toISOString(); - console.log('๋™๊ธฐํ™” ์„ฑ๊ณต, ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ:', currentTime); + syncLogger.info("๋™๊ธฐํ™” ์„ฑ๊ณต, ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ:", currentTime); setLastSyncTime(currentTime); } - + return result; } catch (error) { - console.error(`๋™๊ธฐํ™” ์‹œ๋„ ${attempts} ์‹คํŒจ:`, error); - + syncLogger.error(`๋™๊ธฐํ™” ์‹œ๋„ ${attempts} ์‹คํŒจ:`, error); + if (attempts < maxAttempts) { // ์žฌ์‹œ๋„ ์ „ ์ž ์‹œ ๋Œ€๊ธฐ - await new Promise(resolve => setTimeout(resolve, 2000)); - console.log(`${attempts+1}๋ฒˆ์งธ ๋™๊ธฐํ™” ์žฌ์‹œ๋„ ์ค‘...`); + await new Promise((resolve) => setTimeout(resolve, 2000)); + syncLogger.info(`${attempts + 1}๋ฒˆ์งธ ๋™๊ธฐํ™” ์žฌ์‹œ๋„ ์ค‘...`); } else { throw error; } diff --git a/src/hooks/sync/syncResultHandler.ts b/src/hooks/sync/syncResultHandler.ts index 71e70f2..f06191c 100644 --- a/src/hooks/sync/syncResultHandler.ts +++ b/src/hooks/sync/syncResultHandler.ts @@ -1,14 +1,16 @@ - -import React, { useEffect } from 'react'; -import { SyncResult } from '@/utils/sync/data'; -import { toast } from '@/hooks/useToast.wrapper'; -import useNotifications from '@/hooks/useNotifications'; +import React, { useEffect } from "react"; +import { syncLogger } from "@/utils/logger"; +import { SyncResult } from "@/utils/sync/data"; +import { toast } from "@/hooks/useToast.wrapper"; +import useNotifications from "@/hooks/useNotifications"; // ์•Œ๋ฆผ ์ธ์Šคํ„ด์Šค ์–ป๊ธฐ ์œ„ํ•œ ์ „์—ญ ๋ณ€์ˆ˜ let notificationAdder: ((title: string, message: string) => void) | null = null; // ์•Œ๋ฆผ ํ•จ์ˆ˜ ์„ค์ • -export const setSyncNotificationAdder = (adder: (title: string, message: string) => void) => { +export const setSyncNotificationAdder = ( + adder: (title: string, message: string) => void +) => { notificationAdder = adder; }; @@ -21,10 +23,10 @@ export const handleSyncResult = (result: SyncResult) => { if (result.success) { // ์„ฑ๊ณต ์‹œ ์‹คํŒจ ์นด์šดํ„ฐ ์ดˆ๊ธฐํ™” syncFailureCount = 0; - - let title = ''; - let description = ''; - + + let title = ""; + let description = ""; + if (result.uploadSuccess && result.downloadSuccess) { // ์–‘๋ฐฉํ–ฅ ๋™๊ธฐํ™” ์„ฑ๊ณต title = "๋™๊ธฐํ™” ์™„๋ฃŒ"; @@ -38,53 +40,54 @@ export const handleSyncResult = (result: SyncResult) => { title = "๋™๊ธฐํ™” ์™„๋ฃŒ"; description = "ํด๋ผ์šฐ๋“œ ๋ฐ์ดํ„ฐ๊ฐ€ ๊ธฐ๊ธฐ์— ๋‹ค์šด๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."; } - + // ํ† ์ŠคํŠธ ํ‘œ์‹œ toast({ title, description, }); - + // ์•Œ๋ฆผ ์ถ”๊ฐ€ (์„ค์ •๋œ ๊ฒฝ์šฐ) if (notificationAdder) { notificationAdder(title, description); } - + // ์ƒ์„ธ ๊ฒฐ๊ณผ ๋กœ๊น… - console.log("๋™๊ธฐํ™” ์„ธ๋ถ€ ๊ฒฐ๊ณผ:", { - ์˜ˆ์‚ฐ์—…๋กœ๋“œ: result.details?.budgetUpload ? '์„ฑ๊ณต' : '์‹คํŒจ', - ์˜ˆ์‚ฐ๋‹ค์šด๋กœ๋“œ: result.details?.budgetDownload ? '์„ฑ๊ณต' : '์‹คํŒจ', - ํŠธ๋žœ์žญ์…˜์—…๋กœ๋“œ: result.details?.transactionUpload ? '์„ฑ๊ณต' : '์‹คํŒจ', - ํŠธ๋žœ์žญ์…˜๋‹ค์šด๋กœ๋“œ: result.details?.transactionDownload ? '์„ฑ๊ณต' : '์‹คํŒจ' + syncLogger.info("๋™๊ธฐํ™” ์„ธ๋ถ€ ๊ฒฐ๊ณผ:", { + ์˜ˆ์‚ฐ์—…๋กœ๋“œ: result.details?.budgetUpload ? "์„ฑ๊ณต" : "์‹คํŒจ", + ์˜ˆ์‚ฐ๋‹ค์šด๋กœ๋“œ: result.details?.budgetDownload ? "์„ฑ๊ณต" : "์‹คํŒจ", + ํŠธ๋žœ์žญ์…˜์—…๋กœ๋“œ: result.details?.transactionUpload ? "์„ฑ๊ณต" : "์‹คํŒจ", + ํŠธ๋žœ์žญ์…˜๋‹ค์šด๋กœ๋“œ: result.details?.transactionDownload ? "์„ฑ๊ณต" : "์‹คํŒจ", }); - + return true; } else { // ๋™๊ธฐํ™” ์‹คํŒจ - console.error("๋™๊ธฐํ™” ์‹คํŒจ ์„ธ๋ถ€ ๊ฒฐ๊ณผ:", result.details); - + syncLogger.error("๋™๊ธฐํ™” ์‹คํŒจ ์„ธ๋ถ€ ๊ฒฐ๊ณผ:", result.details); + // ์‹คํŒจ ์นด์šดํ„ฐ ์ฆ๊ฐ€ ๋ฐ ์ตœ๋Œ€ ์•Œ๋ฆผ ํšŸ์ˆ˜ ์ œํ•œ syncFailureCount++; - + if (syncFailureCount <= MAX_SYNC_FAILURE_NOTIFICATIONS) { const title = "๋™๊ธฐํ™” ์‹คํŒจ"; - const description = "๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‚˜์ค‘์— ๋‹ค์‹œ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค."; - + const description = + "๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‚˜์ค‘์— ๋‹ค์‹œ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค."; + // ํ† ์ŠคํŠธ ํ‘œ์‹œ toast({ title, description, - variant: "destructive" + variant: "destructive", }); - + // ์•Œ๋ฆผ ์ถ”๊ฐ€ (์„ค์ •๋œ ๊ฒฝ์šฐ) if (notificationAdder) { notificationAdder(title, description); } } else { - console.log(`๋™๊ธฐํ™” ์‹คํŒจ ์•Œ๋ฆผ ์ œํ•œ (${syncFailureCount}ํšŒ ์‹คํŒจ)`); + syncLogger.info(`๋™๊ธฐํ™” ์‹คํŒจ ์•Œ๋ฆผ ์ œํ•œ (${syncFailureCount}ํšŒ ์‹คํŒจ)`); } - + return false; } }; @@ -92,7 +95,7 @@ export const handleSyncResult = (result: SyncResult) => { // ์ปค์Šคํ…€ ํ›…: ๋™๊ธฐํ™” ์•Œ๋ฆผ ๊ด€๋ฆฌ export const useSyncNotifications = () => { const { addNotification } = useNotifications(); - + // ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ ์•Œ๋ฆผ ํ•จ์ˆ˜ ์„ค์ • useEffect(() => { setSyncNotificationAdder(addNotification); diff --git a/src/hooks/sync/syncTime/index.ts b/src/hooks/sync/syncTime/index.ts index abfa664..b53d8fc 100644 --- a/src/hooks/sync/syncTime/index.ts +++ b/src/hooks/sync/syncTime/index.ts @@ -1,3 +1,2 @@ - -export * from './useSyncTimeFormatting'; -export * from './useSyncTimeEvents'; +export * from "./useSyncTimeFormatting"; +export * from "./useSyncTimeEvents"; diff --git a/src/hooks/sync/syncTime/useSyncTimeEvents.ts b/src/hooks/sync/syncTime/useSyncTimeEvents.ts index 2fb60cd..e8a6d21 100644 --- a/src/hooks/sync/syncTime/useSyncTimeEvents.ts +++ b/src/hooks/sync/syncTime/useSyncTimeEvents.ts @@ -1,12 +1,12 @@ - -import { useCallback } from 'react'; -import { getLastSyncTime } from '@/utils/syncUtils'; +import { useCallback } from "react"; +import { syncLogger } from "@/utils/logger"; +import { getLastSyncTime } from "@/utils/syncUtils"; /** * ๋™๊ธฐํ™” ์‹œ๊ฐ„ ๊ด€๋ จ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์ปค์Šคํ…€ ํ›… */ export const useSyncTimeEvents = ( - lastSync: string | null, + lastSync: string | null, setLastSync: React.Dispatch> ) => { /** @@ -15,44 +15,49 @@ export const useSyncTimeEvents = ( const setupSyncTimeEventListeners = useCallback(() => { const updateLastSyncTime = (event?: Event | CustomEvent) => { const newTime = getLastSyncTime(); - const eventDetails = event instanceof CustomEvent ? ` (์ด๋ฒคํŠธ ์ƒ์„ธ: ${JSON.stringify(event.detail)})` : ''; - console.log(`๋งˆ์ง€๋ง‰ ๋™๊ธฐํ™” ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ๋จ: ${newTime} ${eventDetails}`); + const eventDetails = + event instanceof CustomEvent + ? ` (์ด๋ฒคํŠธ ์ƒ์„ธ: ${JSON.stringify(event.detail)})` + : ""; + syncLogger.info( + `๋งˆ์ง€๋ง‰ ๋™๊ธฐํ™” ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ๋จ: ${newTime} ${eventDetails}` + ); setLastSync(newTime); }; - + // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก - ์ปค์Šคํ…€ ์ด๋ฒคํŠธ ์‚ฌ์šฉ - window.addEventListener('syncTimeUpdated', updateLastSyncTime); - + window.addEventListener("syncTimeUpdated", updateLastSyncTime); + // ์Šคํ† ๋ฆฌ์ง€ ์ด๋ฒคํŠธ๋„ ๋ชจ๋‹ˆํ„ฐ๋ง const handleStorageChange = (event: StorageEvent) => { - if (event.key === 'lastSync' || event.key === null) { - console.log('์Šคํ† ๋ฆฌ์ง€ ๋ณ€๊ฒฝ ๊ฐ์ง€ (lastSync):', event.newValue); + if (event.key === "lastSync" || event.key === null) { + syncLogger.info("์Šคํ† ๋ฆฌ์ง€ ๋ณ€๊ฒฝ ๊ฐ์ง€ (lastSync):", event.newValue); updateLastSyncTime(); } }; - - window.addEventListener('storage', handleStorageChange); - + + window.addEventListener("storage", handleStorageChange); + // ๋™๊ธฐํ™” ์™„๋ฃŒ ์ด๋ฒคํŠธ๋„ ๋ชจ๋‹ˆํ„ฐ๋ง - window.addEventListener('syncComplete', updateLastSyncTime); - - // ์ดˆ๊ธฐ ์ƒํƒœ ์—…๋ฐ์ดํŠธ + window.addEventListener("syncComplete", updateLastSyncTime); + + // ์ดˆ๊ธฐ ์ƒํƒœ ์—…๋ฐ์ดํŠธ updateLastSyncTime(); - + // ์ฃผ๊ธฐ์  ์‹œ๊ฐ„ ํ™•์ธ ๊ธฐ๋Šฅ ์„ค์ • const checkInterval = setupPeriodicTimeCheck(lastSync, setLastSync); - + // ์ •๋ฆฌ ํ•จ์ˆ˜ ๋ฐ˜ํ™˜ return () => { - window.removeEventListener('syncTimeUpdated', updateLastSyncTime); - window.removeEventListener('storage', handleStorageChange); - window.removeEventListener('syncComplete', updateLastSyncTime); + window.removeEventListener("syncTimeUpdated", updateLastSyncTime); + window.removeEventListener("storage", handleStorageChange); + window.removeEventListener("syncComplete", updateLastSyncTime); clearInterval(checkInterval); }; }, [lastSync, setLastSync]); - + return { - setupSyncTimeEventListeners + setupSyncTimeEventListeners, }; }; @@ -60,14 +65,14 @@ export const useSyncTimeEvents = ( * ์ฃผ๊ธฐ์ ์œผ๋กœ ๋™๊ธฐํ™” ์‹œ๊ฐ„์„ ํ™•์ธํ•˜๋Š” ๊ธฐ๋Šฅ ์„ค์ • */ const setupPeriodicTimeCheck = ( - lastSync: string | null, + lastSync: string | null, setLastSync: React.Dispatch> ): number => { // 1์ดˆ๋งˆ๋‹ค ์—…๋ฐ์ดํŠธ ์ƒํƒœ ํ™•์ธ (๋ฌธ์ œ ํ•ด๊ฒฐ์„ ์œ„ํ•œ ์ž„์‹œ ๋ฐฉ์•ˆ) return window.setInterval(() => { const currentTime = getLastSyncTime(); if (currentTime !== lastSync) { - console.log('์ฃผ๊ธฐ์  ํ™•์ธ์—์„œ ๋™๊ธฐํ™” ์‹œ๊ฐ„ ๋ณ€๊ฒฝ ๊ฐ์ง€:', currentTime); + syncLogger.info("์ฃผ๊ธฐ์  ํ™•์ธ์—์„œ ๋™๊ธฐํ™” ์‹œ๊ฐ„ ๋ณ€๊ฒฝ ๊ฐ์ง€:", currentTime); setLastSync(currentTime); } }, 1000); diff --git a/src/hooks/sync/syncTime/useSyncTimeFormatting.ts b/src/hooks/sync/syncTime/useSyncTimeFormatting.ts index d66e8e6..5e55353 100644 --- a/src/hooks/sync/syncTime/useSyncTimeFormatting.ts +++ b/src/hooks/sync/syncTime/useSyncTimeFormatting.ts @@ -1,4 +1,3 @@ - /** * ๋งˆ์ง€๋ง‰ ๋™๊ธฐํ™” ์‹œ๊ฐ„ ํฌ๋งทํŒ…์„ ์œ„ํ•œ ์ปค์Šคํ…€ ํ›… */ @@ -8,26 +7,26 @@ export const useLastSyncTimeFormatting = (lastSync: string | null) => { */ const formatLastSyncTime = (): string => { if (!lastSync) { - return '์—†์Œ'; + return "์—†์Œ"; } try { const date = new Date(lastSync); - + // ์œ ํšจํ•œ ๋‚ ์งœ์ธ์ง€ ํ™•์ธ if (isNaN(date.getTime())) { - return '์—†์Œ'; + return "์—†์Œ"; } - + return formatDateByRelativeTime(date); } catch (error) { - console.error('๋‚ ์งœ ํฌ๋งท ์˜ค๋ฅ˜:', error); - return '์—†์Œ'; + syncLogger.error("๋‚ ์งœ ํฌ๋งท ์˜ค๋ฅ˜:", error); + return "์—†์Œ"; } }; - + return { - formatLastSyncTime + formatLastSyncTime, }; }; @@ -38,21 +37,21 @@ const formatDateByRelativeTime = (date: Date): string => { // ์˜ค๋Š˜ ๋‚ ์งœ์ธ ๊ฒฝ์šฐ const today = new Date(); const isToday = isSameDay(date, today); - + if (isToday) { // ์‹œ๊ฐ„๋งŒ ํ‘œ์‹œ return `์˜ค๋Š˜ ${formatTime(date)}`; } - + // ์–ด์ œ ๋‚ ์งœ์ธ ๊ฒฝ์šฐ const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const isYesterday = isSameDay(date, yesterday); - + if (isYesterday) { return `์–ด์ œ ${formatTime(date)}`; } - + // ๊ทธ ์™ธ ๋‚ ์งœ return `${formatFullDate(date)} ${formatTime(date)}`; }; @@ -61,21 +60,23 @@ const formatDateByRelativeTime = (date: Date): string => { * ๋‘ ๋‚ ์งœ๊ฐ€ ๊ฐ™์€ ๋‚ ์ธ์ง€ ํ™•์ธ */ const isSameDay = (date1: Date, date2: Date): boolean => { - return date1.getDate() === date2.getDate() && - date1.getMonth() === date2.getMonth() && - date1.getFullYear() === date2.getFullYear(); + return ( + date1.getDate() === date2.getDate() && + date1.getMonth() === date2.getMonth() && + date1.getFullYear() === date2.getFullYear() + ); }; /** * ์‹œ๊ฐ„์„ HH:MM ํ˜•์‹์œผ๋กœ ํฌ๋งทํŒ… */ const formatTime = (date: Date): string => { - return `${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`; + return `${date.getHours()}:${String(date.getMinutes()).padStart(2, "0")}`; }; /** * ์ „์ฒด ๋‚ ์งœ๋ฅผ YYYY/MM/DD ํ˜•์‹์œผ๋กœ ํฌ๋งทํŒ… */ const formatFullDate = (date: Date): string => { - return `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`; + return `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, "0")}/${String(date.getDate()).padStart(2, "0")}`; }; diff --git a/src/hooks/sync/useManualSync.ts b/src/hooks/sync/useManualSync.ts index ccf3f39..e64f6cb 100644 --- a/src/hooks/sync/useManualSync.ts +++ b/src/hooks/sync/useManualSync.ts @@ -1,14 +1,15 @@ - -import { useState } from 'react'; -import { toast } from '@/hooks/useToast.wrapper'; -import { trySyncAllData, setLastSyncTime } from '@/utils/syncUtils'; -import { handleSyncResult } from './syncResultHandler'; -import useNotifications from '@/hooks/useNotifications'; +import { useState } from "react"; +import { syncLogger } from "@/utils/logger"; +import { toast } from "@/hooks/useToast.wrapper"; +import { trySyncAllData, setLastSyncTime } from "@/utils/syncUtils"; +import { handleSyncResult } from "./syncResultHandler"; +import useNotifications from "@/hooks/useNotifications"; +import { Models } from "appwrite"; /** * ์ˆ˜๋™ ๋™๊ธฐํ™” ๊ธฐ๋Šฅ์„ ์œ„ํ•œ ์ปค์Šคํ…€ ํ›… */ -export const useManualSync = (user: any) => { +export const useManualSync = (user: Models.User | null) => { const [syncing, setSyncing] = useState(false); const { addNotification } = useNotifications(); @@ -18,67 +19,66 @@ export const useManualSync = (user: any) => { toast({ title: "๋กœ๊ทธ์ธ ํ•„์š”", description: "๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”๋ฅผ ์œ„ํ•ด ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", - variant: "destructive" + variant: "destructive", }); - + addNotification( - "๋กœ๊ทธ์ธ ํ•„์š”", + "๋กœ๊ทธ์ธ ํ•„์š”", "๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”๋ฅผ ์œ„ํ•ด ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค." ); return; } - + // ์ด๋ฏธ ๋™๊ธฐํ™” ์ค‘์ด๋ฉด ์ค‘๋ณต ์‹คํ–‰ ๋ฐฉ์ง€ if (syncing) { - console.log('์ด๋ฏธ ๋™๊ธฐํ™”๊ฐ€ ์ง„ํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค.'); + syncLogger.info("์ด๋ฏธ ๋™๊ธฐํ™”๊ฐ€ ์ง„ํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค."); return; } - + await performSync(user.id); }; // ์‹ค์ œ ๋™๊ธฐํ™” ์ˆ˜ํ–‰ ํ•จ์ˆ˜ const performSync = async (userId: string) => { - if (!userId) return; - + if (!userId) { + return; + } + try { setSyncing(true); - console.log('์ˆ˜๋™ ๋™๊ธฐํ™” ์‹œ์ž‘'); - - addNotification( - "๋™๊ธฐํ™” ์‹œ์ž‘", - "๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”๊ฐ€ ์‹œ์ž‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค." - ); - + syncLogger.info("์ˆ˜๋™ ๋™๊ธฐํ™” ์‹œ์ž‘"); + + addNotification("๋™๊ธฐํ™” ์‹œ์ž‘", "๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”๊ฐ€ ์‹œ์ž‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + // ๋™๊ธฐํ™” ์‹คํ–‰ const result = await trySyncAllData(userId); - + // ๋™๊ธฐํ™” ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ handleSyncResult(result); - + // ๋™๊ธฐํ™” ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ if (result.success) { const currentTime = new Date().toISOString(); - console.log('์ˆ˜๋™ ๋™๊ธฐํ™” ์„ฑ๊ณต, ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ:', currentTime); + syncLogger.info("์ˆ˜๋™ ๋™๊ธฐํ™” ์„ฑ๊ณต, ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ:", currentTime); setLastSyncTime(currentTime); } - + return result; } catch (error) { - console.error('๋™๊ธฐํ™” ์˜ค๋ฅ˜:', error); + syncLogger.error("๋™๊ธฐํ™” ์˜ค๋ฅ˜:", error); toast({ title: "๋™๊ธฐํ™” ์˜ค๋ฅ˜", description: "๋™๊ธฐํ™” ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.", - variant: "destructive" + variant: "destructive", }); - + addNotification( - "๋™๊ธฐํ™” ์˜ค๋ฅ˜", + "๋™๊ธฐํ™” ์˜ค๋ฅ˜", "๋™๊ธฐํ™” ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”." ); } finally { setSyncing(false); - console.log('์ˆ˜๋™ ๋™๊ธฐํ™” ์ข…๋ฃŒ'); + syncLogger.info("์ˆ˜๋™ ๋™๊ธฐํ™” ์ข…๋ฃŒ"); } }; diff --git a/src/hooks/sync/useSyncStatus.ts b/src/hooks/sync/useSyncStatus.ts index af674a1..4d96cd1 100644 --- a/src/hooks/sync/useSyncStatus.ts +++ b/src/hooks/sync/useSyncStatus.ts @@ -1,8 +1,8 @@ - -import { useState, useEffect } from 'react'; -import { getLastSyncTime } from '@/utils/syncUtils'; -import { useLastSyncTimeFormatting } from './syncTime/useSyncTimeFormatting'; -import { useSyncTimeEvents } from './syncTime/useSyncTimeEvents'; +import { useState, useEffect } from "react"; +import { syncLogger } from "@/utils/logger"; +import { getLastSyncTime } from "@/utils/syncUtils"; +import { useLastSyncTimeFormatting } from "./syncTime/useSyncTimeFormatting"; +import { useSyncTimeEvents } from "./syncTime/useSyncTimeEvents"; /** * ๋™๊ธฐํ™” ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ์ปค์Šคํ…€ ํ›… @@ -10,20 +10,26 @@ import { useSyncTimeEvents } from './syncTime/useSyncTimeEvents'; export const useSyncStatus = () => { const [lastSync, setLastSync] = useState(getLastSyncTime()); const { formatLastSyncTime } = useLastSyncTimeFormatting(lastSync); - const { setupSyncTimeEventListeners } = useSyncTimeEvents(lastSync, setLastSync); - + const { setupSyncTimeEventListeners } = useSyncTimeEvents( + lastSync, + setLastSync + ); + // ๋™๊ธฐํ™” ์‹œ๊ฐ„์ด ๋ณ€๊ฒฝ๋  ๋•Œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋ฐ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์„ค์ • useEffect(() => { - console.log('useSyncStatus ํ›… ์ดˆ๊ธฐํ™”, ํ˜„์žฌ ๋งˆ์ง€๋ง‰ ๋™๊ธฐํ™” ์‹œ๊ฐ„:', lastSync); - + syncLogger.info( + "useSyncStatus ํ›… ์ดˆ๊ธฐํ™”, ํ˜„์žฌ ๋งˆ์ง€๋ง‰ ๋™๊ธฐํ™” ์‹œ๊ฐ„:", + lastSync + ); + // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋ฐ ์ฃผ๊ธฐ์  ํ™•์ธ ์„ค์ • const cleanup = setupSyncTimeEventListeners(); - + return cleanup; }, [lastSync, setupSyncTimeEventListeners]); - + return { lastSync, - formatLastSyncTime + formatLastSyncTime, }; }; diff --git a/src/hooks/sync/useSyncToggle.ts b/src/hooks/sync/useSyncToggle.ts index 6414b19..db9424f 100644 --- a/src/hooks/sync/useSyncToggle.ts +++ b/src/hooks/sync/useSyncToggle.ts @@ -1,20 +1,26 @@ - -import { useState, useEffect } from 'react'; -import { useAuth } from '@/contexts/auth'; -import { toast } from '@/hooks/useToast.wrapper'; -import { - isSyncEnabled, +import { useState, useEffect } from "react"; +import { syncLogger } from "@/utils/logger"; +import { useAuth } from "@/contexts/auth"; +import { toast } from "@/hooks/useToast.wrapper"; +import { + isSyncEnabled, setSyncEnabled, - setLastSyncTime -} from '@/utils/syncUtils'; -import useNotifications from '@/hooks/useNotifications'; -import { resetSyncFailureCount } from './syncResultHandler'; -import { checkSyncNetworkStatus, showNetworkErrorNotification } from './syncNetworkChecker'; -import { performSync } from './syncPerformer'; -import { createLocalDataBackup, restoreLocalDataBackup } from './syncBackupUtils'; + setLastSyncTime, +} from "@/utils/syncUtils"; +import useNotifications from "@/hooks/useNotifications"; +import { resetSyncFailureCount } from "./syncResultHandler"; +import { + checkSyncNetworkStatus, + showNetworkErrorNotification, +} from "./syncNetworkChecker"; +import { performSync } from "./syncPerformer"; +import { + createLocalDataBackup, + restoreLocalDataBackup, +} from "./syncBackupUtils"; /** - * ๋™๊ธฐํ™” ํ† ๊ธ€ ๊ธฐ๋Šฅ์„ ์œ„ํ•œ ์ปค์Šคํ…€ ํ›… + * ๋™๊ธฐํ™” ํ† ๊ธ€ ๊ธฐ๋Šฅ์„ ์œ„ํ•œ ์ปค์Šคํ…€ ํ›… */ export const useSyncToggle = () => { const [enabled, setEnabled] = useState(isSyncEnabled()); @@ -30,35 +36,38 @@ export const useSyncToggle = () => { // ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์•„์›ƒํ–ˆ๊ณ  ๋™๊ธฐํ™”๊ฐ€ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์œผ๋ฉด ๋น„ํ™œ์„ฑํ™” setSyncEnabled(false); setEnabled(false); - console.log('๋กœ๊ทธ์•„์›ƒ์œผ๋กœ ์ธํ•ด ๋™๊ธฐํ™” ์„ค์ •์ด ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + syncLogger.info("๋กœ๊ทธ์•„์›ƒ์œผ๋กœ ์ธํ•ด ๋™๊ธฐํ™” ์„ค์ •์ด ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); } - + // ๋™๊ธฐํ™” ์ƒํƒœ ์—…๋ฐ์ดํŠธ setEnabled(isSyncEnabled()); - + // ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ ์‹œ ์‹คํŒจ ์นด์šดํ„ฐ ์ดˆ๊ธฐํ™” resetSyncFailureCount(); }; // ์ดˆ๊ธฐ ํ˜ธ์ถœ updateSyncState(); - + // ์ธ์ฆ ์ƒํƒœ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ - window.addEventListener('auth-state-changed', updateSyncState); - + window.addEventListener("auth-state-changed", updateSyncState); + // ์Šคํ† ๋ฆฌ์ง€ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ์—๋„ ๋™๊ธฐํ™” ์ƒํƒœ ํ™•์ธ ์ถ”๊ฐ€ const handleStorageChange = (event: StorageEvent) => { - if (event.key === 'syncEnabled' || event.key === null) { + if (event.key === "syncEnabled" || event.key === null) { setEnabled(isSyncEnabled()); - console.log('์Šคํ† ๋ฆฌ์ง€ ๋ณ€๊ฒฝ์œผ๋กœ ๋™๊ธฐํ™” ์ƒํƒœ ์—…๋ฐ์ดํŠธ:', isSyncEnabled() ? 'ํ™œ์„ฑํ™”' : '๋น„ํ™œ์„ฑํ™”'); + syncLogger.info( + "์Šคํ† ๋ฆฌ์ง€ ๋ณ€๊ฒฝ์œผ๋กœ ๋™๊ธฐํ™” ์ƒํƒœ ์—…๋ฐ์ดํŠธ:", + isSyncEnabled() ? "ํ™œ์„ฑํ™”" : "๋น„ํ™œ์„ฑํ™”" + ); } }; - - window.addEventListener('storage', handleStorageChange); - + + window.addEventListener("storage", handleStorageChange); + return () => { - window.removeEventListener('auth-state-changed', updateSyncState); - window.removeEventListener('storage', handleStorageChange); + window.removeEventListener("auth-state-changed", updateSyncState); + window.removeEventListener("storage", handleStorageChange); }; }, [user]); @@ -68,16 +77,16 @@ export const useSyncToggle = () => { toast({ title: "๋กœ๊ทธ์ธ ํ•„์š”", description: "๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”๋ฅผ ์œ„ํ•ด ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", - variant: "destructive" + variant: "destructive", }); - + addNotification( - "๋กœ๊ทธ์ธ ํ•„์š”", + "๋กœ๊ทธ์ธ ํ•„์š”", "๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”๋ฅผ ์œ„ํ•ด ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค." ); return; } - + try { // ๋„คํŠธ์›Œํฌ ์ƒํƒœ ํ™•์ธ if (checked) { @@ -87,56 +96,57 @@ export const useSyncToggle = () => { return; } } - + // ํ˜„์žฌ ๋กœ์ปฌ ๋ฐ์ดํ„ฐ ๋ฐฑ์—… const dataBackup = createLocalDataBackup(); - + // ๋™๊ธฐํ™” ์„ค์ • ๋ณ€๊ฒฝ setEnabled(checked); setSyncEnabled(checked); - + // ์‹คํŒจ ์นด์šดํ„ฐ ์ดˆ๊ธฐํ™” resetSyncFailureCount(); - + // ๋™๊ธฐํ™” ํ™œ์„ฑํ™”/๋น„ํ™œ์„ฑํ™” ์•Œ๋ฆผ ์ถ”๊ฐ€ addNotification( - checked ? "๋™๊ธฐํ™” ํ™œ์„ฑํ™”" : "๋™๊ธฐํ™” ๋น„ํ™œ์„ฑํ™”", - checked - ? "๋ฐ์ดํ„ฐ๊ฐ€ ํด๋ผ์šฐ๋“œ์™€ ๋™๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค." + checked ? "๋™๊ธฐํ™” ํ™œ์„ฑํ™”" : "๋™๊ธฐํ™” ๋น„ํ™œ์„ฑํ™”", + checked + ? "๋ฐ์ดํ„ฐ๊ฐ€ ํด๋ผ์šฐ๋“œ์™€ ๋™๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค." : "ํด๋ผ์šฐ๋“œ ๋™๊ธฐํ™”๊ฐ€ ์ค‘์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค." ); - + // ์ด๋ฒคํŠธ ํŠธ๋ฆฌ๊ฑฐ - window.dispatchEvent(new Event('auth-state-changed')); - + window.dispatchEvent(new Event("auth-state-changed")); + if (checked && user) { try { // ๋™๊ธฐํ™” ์ˆ˜ํ–‰ await performSync(user.id); } catch (error) { - console.error('๋™๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜, ๋กœ์ปฌ ๋ฐ์ดํ„ฐ ๋ณต์› ์‹œ๋„:', error); - + syncLogger.error("๋™๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜, ๋กœ์ปฌ ๋ฐ์ดํ„ฐ ๋ณต์› ์‹œ๋„:", error); + // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ๋ฐฑ์—… ๋ฐ์ดํ„ฐ ๋ณต์› restoreLocalDataBackup(dataBackup); - + toast({ title: "๋™๊ธฐํ™” ์˜ค๋ฅ˜", - description: "๋™๊ธฐํ™” ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ ๋กœ์ปฌ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณต์›๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" + description: + "๋™๊ธฐํ™” ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ ๋กœ์ปฌ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณต์›๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + variant: "destructive", }); - + addNotification( - "๋™๊ธฐํ™” ์˜ค๋ฅ˜", + "๋™๊ธฐํ™” ์˜ค๋ฅ˜", "๋™๊ธฐํ™” ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ ๋กœ์ปฌ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณต์›๋˜์—ˆ์Šต๋‹ˆ๋‹ค." ); } } } catch (error) { - console.error('๋™๊ธฐํ™” ์„ค์ • ๋ณ€๊ฒฝ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜:', error); + syncLogger.error("๋™๊ธฐํ™” ์„ค์ • ๋ณ€๊ฒฝ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜:", error); toast({ title: "๋™๊ธฐํ™” ์„ค์ • ์˜ค๋ฅ˜", description: "์„ค์ • ๋ณ€๊ฒฝ ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.", - variant: "destructive" + variant: "destructive", }); } }; diff --git a/src/hooks/toast/constants.ts b/src/hooks/toast/constants.ts index 74c8129..9850bd1 100644 --- a/src/hooks/toast/constants.ts +++ b/src/hooks/toast/constants.ts @@ -1,3 +1,2 @@ - -export const TOAST_LIMIT = 5 // ์ตœ๋Œ€ 5๊ฐœ๋กœ ์ œํ•œ -export const TOAST_REMOVE_DELAY = 3000 // 3์ดˆ ํ›„ DOM์—์„œ ์ œ๊ฑฐ +export const TOAST_LIMIT = 5; // ์ตœ๋Œ€ 5๊ฐœ๋กœ ์ œํ•œ +export const TOAST_REMOVE_DELAY = 3000; // 3์ดˆ ํ›„ DOM์—์„œ ์ œ๊ฑฐ diff --git a/src/hooks/toast/index.ts b/src/hooks/toast/index.ts index 5c84740..730d4af 100644 --- a/src/hooks/toast/index.ts +++ b/src/hooks/toast/index.ts @@ -1,22 +1,22 @@ +import * as React from "react"; +import { Toast, ToasterToast, State } from "./types"; +import { actionTypes } from "./types"; +import { listeners, memoryState } from "./store"; +import { genId, dispatch } from "./toastManager"; -import * as React from "react" -import { Toast, ToasterToast, State } from "./types" -import { actionTypes } from "./types" -import { listeners, memoryState } from "./store" -import { genId, dispatch } from "./toastManager" - -export { TOAST_LIMIT, TOAST_REMOVE_DELAY } from "./constants" -export type { ToasterToast } from "./types" +export { TOAST_LIMIT, TOAST_REMOVE_DELAY } from "./constants"; +export type { ToasterToast } from "./types"; function toast({ ...props }: Toast) { - const id = genId() + const id = genId(); const update = (props: ToasterToast) => dispatch({ type: actionTypes.UPDATE_TOAST, toast: { ...props, id }, - }) - const dismiss = () => dispatch({ type: actionTypes.DISMISS_TOAST, toastId: id }) + }); + const dismiss = () => + dispatch({ type: actionTypes.DISMISS_TOAST, toastId: id }); dispatch({ type: actionTypes.ADD_TOAST, @@ -25,37 +25,40 @@ function toast({ ...props }: Toast) { id, open: true, onOpenChange: (open) => { - if (!open) dismiss() + if (!open) { + dismiss(); + } }, duration: props.duration || 3000, // ๊ธฐ๋ณธ ์ง€์† ์‹œ๊ฐ„ 3์ดˆ๋กœ ์„ค์ • }, - }) + }); return { id, dismiss, update, - } + }; } function useToast() { - const [state, setState] = React.useState(memoryState) + const [state, setState] = React.useState(memoryState); React.useEffect(() => { - listeners.push(setState) + listeners.push(setState); return () => { - const index = listeners.indexOf(setState) + const index = listeners.indexOf(setState); if (index > -1) { - listeners.splice(index, 1) + listeners.splice(index, 1); } - } - }, [state]) + }; + }, [state]); return { ...state, toast, - dismiss: (toastId?: string) => dispatch({ type: actionTypes.DISMISS_TOAST, toastId }), - } + dismiss: (toastId?: string) => + dispatch({ type: actionTypes.DISMISS_TOAST, toastId }), + }; } -export { useToast, toast } +export { useToast, toast }; diff --git a/src/hooks/toast/reducer.ts b/src/hooks/toast/reducer.ts index ef237fc..8d640fb 100644 --- a/src/hooks/toast/reducer.ts +++ b/src/hooks/toast/reducer.ts @@ -1,10 +1,9 @@ - -import { TOAST_REMOVE_DELAY, TOAST_LIMIT } from './constants' -import { Action, State, actionTypes } from './types' -import { dispatch } from './toastManager' +import { TOAST_REMOVE_DELAY, TOAST_LIMIT } from "./constants"; +import { Action, State, actionTypes } from "./types"; +import { dispatch } from "./toastManager"; // ํ† ์ŠคํŠธ ํƒ€์ž„์•„์›ƒ ๋งต -export const toastTimeouts = new Map>() +export const toastTimeouts = new Map>(); // ํ† ์ŠคํŠธ ์ž๋™ ์ œ๊ฑฐ ํ•จ์ˆ˜ export const addToRemoveQueue = (toastId: string) => { @@ -15,15 +14,15 @@ export const addToRemoveQueue = (toastId: string) => { } const timeout = setTimeout(() => { - toastTimeouts.delete(toastId) + toastTimeouts.delete(toastId); dispatch({ type: actionTypes.REMOVE_TOAST, toastId: toastId, - }) - }, TOAST_REMOVE_DELAY) + }); + }, TOAST_REMOVE_DELAY); - toastTimeouts.set(toastId, timeout) -} + toastTimeouts.set(toastId, timeout); +}; export const reducer = (state: State, action: Action): State => { switch (action.type) { @@ -35,7 +34,7 @@ export const reducer = (state: State, action: Action): State => { return { ...state, toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - } + }; case actionTypes.UPDATE_TOAST: return { @@ -43,17 +42,17 @@ export const reducer = (state: State, action: Action): State => { toasts: state.toasts.map((t) => t.id === action.toast.id ? { ...t, ...action.toast } : t ), - } + }; case actionTypes.DISMISS_TOAST: { - const { toastId } = action + const { toastId } = action; if (toastId) { - addToRemoveQueue(toastId) + addToRemoveQueue(toastId); } else { state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id) - }) + addToRemoveQueue(toast.id); + }); } return { @@ -66,20 +65,20 @@ export const reducer = (state: State, action: Action): State => { } : t ), - } + }; } case actionTypes.REMOVE_TOAST: if (action.toastId === undefined) { return { ...state, toasts: [], - } + }; } return { ...state, toasts: state.toasts.filter((t) => t.id !== action.toastId), - } + }; default: - return state + return state; } -} +}; diff --git a/src/hooks/toast/store.ts b/src/hooks/toast/store.ts index 8dbed91..9c4a3da 100644 --- a/src/hooks/toast/store.ts +++ b/src/hooks/toast/store.ts @@ -1,8 +1,7 @@ - -import { State } from './types' +import { State } from "./types"; // ์ „์—ญ ์ƒํƒœ ๋ฐ ๋ฆฌ์Šค๋„ˆ -export const listeners: Array<(state: State) => void> = [] +export const listeners: Array<(state: State) => void> = []; // memoryState์™€ lastAction์€ toastManager.ts์—์„œ ๊ด€๋ฆฌ -export { memoryState } from './toastManager'; +export { memoryState } from "./toastManager"; diff --git a/src/hooks/toast/toastManager.ts b/src/hooks/toast/toastManager.ts index e768a53..e8e2f50 100644 --- a/src/hooks/toast/toastManager.ts +++ b/src/hooks/toast/toastManager.ts @@ -1,52 +1,52 @@ - -import { Action, actionTypes } from './types' -import { TOAST_LIMIT } from './constants' -import { reducer } from './reducer' -import { listeners } from './store' +import { Action, actionTypes } from "./types"; +import { logger } from "@/utils/logger"; +import { TOAST_LIMIT } from "./constants"; +import { reducer } from "./reducer"; +import { listeners } from "./store"; // ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ let memoryState = { toasts: [] }; let lastAction = null; // ID ์ƒ์„ฑ๊ธฐ -let count = 0 +let count = 0; export function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER - return count.toString() + count = (count + 1) % Number.MAX_SAFE_INTEGER; + return count.toString(); } export function dispatch(action: Action) { // ๋งˆ์ง€๋ง‰ ์•ก์…˜ ์ •๋ณด ์ถ”์ถœ let actionId: string | undefined = undefined; - if ('toast' in action && action.toast) { + if ("toast" in action && action.toast) { actionId = action.toast.id; - } else if ('toastId' in action) { + } else if ("toastId" in action) { actionId = action.toastId; } - + // ๋™์ผํ•œ ํ† ์ŠคํŠธ์— ๋Œ€ํ•œ ์ค‘๋ณต ์•ก์…˜ ๋ฐฉ์ง€ const now = Date.now(); - const isSameAction = lastAction && - lastAction.type === action.type && - ((action.type === actionTypes.ADD_TOAST && - lastAction.time > now - 1000) || // ADD ์•ก์…˜์€ 1์ดˆ ๋‚ด ์ค‘๋ณต ๋ฐฉ์ง€ - (action.type !== actionTypes.ADD_TOAST && - actionId === lastAction.id && - lastAction.time > now - 300)); // ๋‹ค๋ฅธ ์•ก์…˜์€ 300ms ๋‚ด ์ค‘๋ณต ๋ฐฉ์ง€ - + const isSameAction = + lastAction && + lastAction.type === action.type && + ((action.type === actionTypes.ADD_TOAST && lastAction.time > now - 1000) || // ADD ์•ก์…˜์€ 1์ดˆ ๋‚ด ์ค‘๋ณต ๋ฐฉ์ง€ + (action.type !== actionTypes.ADD_TOAST && + actionId === lastAction.id && + lastAction.time > now - 300)); // ๋‹ค๋ฅธ ์•ก์…˜์€ 300ms ๋‚ด ์ค‘๋ณต ๋ฐฉ์ง€ + if (isSameAction) { - console.log('์ค‘๋ณต ํ† ์ŠคํŠธ ์•ก์…˜ ๋ฌด์‹œ:', action.type); + logger.info("์ค‘๋ณต ํ† ์ŠคํŠธ ์•ก์…˜ ๋ฌด์‹œ:", action.type); return; } - + // ์•ก์…˜ ์ถ”์  ์—…๋ฐ์ดํŠธ - lastAction = { - type: action.type, - id: actionId, - time: now + lastAction = { + type: action.type, + id: actionId, + time: now, }; - + // REMOVE_TOAST ์•ก์…˜ ์šฐ์„ ์ˆœ์œ„ ๋†’์ž„ if (action.type === actionTypes.REMOVE_TOAST) { // ์ฆ‰์‹œ ์ฒ˜๋ฆฌ @@ -56,7 +56,7 @@ export function dispatch(action: Action) { }); return; } - + // ์‹ค์ œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋ฐ ๋ฆฌ์Šค๋„ˆ ํ˜ธ์ถœ memoryState = reducer(memoryState, action); listeners.forEach((listener) => { diff --git a/src/hooks/toast/types.ts b/src/hooks/toast/types.ts index 878a99f..6fe2fd0 100644 --- a/src/hooks/toast/types.ts +++ b/src/hooks/toast/types.ts @@ -1,46 +1,45 @@ - -import * as React from "react" +import * as React from "react"; export type ToasterToast = { - id: string - title?: React.ReactNode - description?: React.ReactNode - action?: React.ReactNode - variant?: "default" | "destructive" - duration?: number - open?: boolean - onOpenChange?: (open: boolean) => void -} + id: string; + title?: React.ReactNode; + description?: React.ReactNode; + action?: React.ReactNode; + variant?: "default" | "destructive"; + duration?: number; + open?: boolean; + onOpenChange?: (open: boolean) => void; +}; export const actionTypes = { ADD_TOAST: "ADD_TOAST", UPDATE_TOAST: "UPDATE_TOAST", DISMISS_TOAST: "DISMISS_TOAST", REMOVE_TOAST: "REMOVE_TOAST", -} as const +} as const; -export type ActionType = typeof actionTypes +export type ActionType = typeof actionTypes; export type Action = | { - type: ActionType["ADD_TOAST"] - toast: ToasterToast + type: ActionType["ADD_TOAST"]; + toast: ToasterToast; } | { - type: ActionType["UPDATE_TOAST"] - toast: Partial & { id: string } + type: ActionType["UPDATE_TOAST"]; + toast: Partial & { id: string }; } | { - type: ActionType["DISMISS_TOAST"] - toastId?: string + type: ActionType["DISMISS_TOAST"]; + toastId?: string; } | { - type: ActionType["REMOVE_TOAST"] - toastId?: string - } + type: ActionType["REMOVE_TOAST"]; + toastId?: string; + }; export interface State { - toasts: ToasterToast[] + toasts: ToasterToast[]; } -export type Toast = Omit +export type Toast = Omit; diff --git a/src/hooks/transactions/dateUtils.ts b/src/hooks/transactions/dateUtils.ts index d8ec923..3306091 100644 --- a/src/hooks/transactions/dateUtils.ts +++ b/src/hooks/transactions/dateUtils.ts @@ -1,13 +1,23 @@ - -import { format, parse, addMonths, subMonths } from 'date-fns'; -import { ko } from 'date-fns/locale'; +import { format, parse, addMonths, subMonths } from "date-fns"; +import { logger } from "@/utils/logger"; +import { ko } from "date-fns/locale"; /** * ์›” ์ด๋ฆ„ ๋ฐฐ์—ด (ํ•œ๊ตญ์–ด) */ export const MONTHS_KR = [ - '1์›”', '2์›”', '3์›”', '4์›”', '5์›”', '6์›”', - '7์›”', '8์›”', '9์›”', '10์›”', '11์›”', '12์›”' + "1์›”", + "2์›”", + "3์›”", + "4์›”", + "5์›”", + "6์›”", + "7์›”", + "8์›”", + "9์›”", + "10์›”", + "11์›”", + "12์›”", ]; /** @@ -22,7 +32,7 @@ export const isValidMonth = (month: string): boolean => { * ํ˜„์žฌ ๋…„์›” ๊ฐ€์ ธ์˜ค๊ธฐ */ export const getCurrentMonth = (): string => { - return format(new Date(), 'yyyy-MM'); + return format(new Date(), "yyyy-MM"); }; /** @@ -31,19 +41,19 @@ export const getCurrentMonth = (): string => { export const getPrevMonth = (month: string): string => { // ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ if (!isValidMonth(month)) { - console.warn('์œ ํšจํ•˜์ง€ ์•Š์€ ์›” ํ˜•์‹:', month); + logger.warn("์œ ํšจํ•˜์ง€ ์•Š์€ ์›” ํ˜•์‹:", month); return getCurrentMonth(); } - + try { // ์›” ๋ฌธ์ž์—ด์„ ๋‚ ์งœ๋กœ ํŒŒ์‹ฑ - const date = parse(month, 'yyyy-MM', new Date()); + const date = parse(month, "yyyy-MM", new Date()); // ํ•œ ๋‹ฌ ์ด์ „ const prevMonth = subMonths(date, 1); // yyyy-MM ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ - return format(prevMonth, 'yyyy-MM'); + return format(prevMonth, "yyyy-MM"); } catch (error) { - console.error('์ด์ „ ์›” ๊ณ„์‚ฐ ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("์ด์ „ ์›” ๊ณ„์‚ฐ ์ค‘ ์˜ค๋ฅ˜:", error); return getCurrentMonth(); } }; @@ -54,19 +64,19 @@ export const getPrevMonth = (month: string): string => { export const getNextMonth = (month: string): string => { // ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ if (!isValidMonth(month)) { - console.warn('์œ ํšจํ•˜์ง€ ์•Š์€ ์›” ํ˜•์‹:', month); + logger.warn("์œ ํšจํ•˜์ง€ ์•Š์€ ์›” ํ˜•์‹:", month); return getCurrentMonth(); } - + try { // ์›” ๋ฌธ์ž์—ด์„ ๋‚ ์งœ๋กœ ํŒŒ์‹ฑ - const date = parse(month, 'yyyy-MM', new Date()); + const date = parse(month, "yyyy-MM", new Date()); // ํ•œ ๋‹ฌ ์ดํ›„ const nextMonth = addMonths(date, 1); // yyyy-MM ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ - return format(nextMonth, 'yyyy-MM'); + return format(nextMonth, "yyyy-MM"); } catch (error) { - console.error('๋‹ค์Œ ์›” ๊ณ„์‚ฐ ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("๋‹ค์Œ ์›” ๊ณ„์‚ฐ ์ค‘ ์˜ค๋ฅ˜:", error); return getCurrentMonth(); } }; @@ -78,16 +88,16 @@ export const formatMonthForDisplay = (month: string): string => { try { // ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ if (!isValidMonth(month)) { - console.warn('์œ ํšจํ•˜์ง€ ์•Š์€ ์›” ํ˜•์‹:', month); - return format(new Date(), 'yyyy๋…„ MM์›”', { locale: ko }); + logger.warn("์œ ํšจํ•˜์ง€ ์•Š์€ ์›” ํ˜•์‹:", month); + return format(new Date(), "yyyy๋…„ MM์›”", { locale: ko }); } - + // ์›” ๋ฌธ์ž์—ด์„ ๋‚ ์งœ๋กœ ํŒŒ์‹ฑ - const date = parse(month, 'yyyy-MM', new Date()); + const date = parse(month, "yyyy-MM", new Date()); // yyyy๋…„ MM์›” ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ (ํ•œ๊ตญ์–ด ๋กœ์ผ€์ผ) - return format(date, 'yyyy๋…„ MM์›”', { locale: ko }); + return format(date, "yyyy๋…„ MM์›”", { locale: ko }); } catch (error) { - console.error('์›” ํ˜•์‹ ๋ณ€ํ™˜ ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("์›” ํ˜•์‹ ๋ณ€ํ™˜ ์ค‘ ์˜ค๋ฅ˜:", error); return month; } }; diff --git a/src/hooks/transactions/deleteTransaction.ts b/src/hooks/transactions/deleteTransaction.ts index 1231441..12a7d62 100644 --- a/src/hooks/transactions/deleteTransaction.ts +++ b/src/hooks/transactions/deleteTransaction.ts @@ -1,11 +1,11 @@ - -import { useCallback } from 'react'; -import { Transaction } from '@/components/TransactionCard'; -import { useAuth } from '@/contexts/auth/useAuth'; -import { toast } from '@/hooks/useToast.wrapper'; -import { saveTransactionsToStorage } from './storageUtils'; -import { deleteTransactionFromSupabase } from './supabaseUtils'; -import { addToDeletedTransactions } from '@/utils/sync/transaction/deletedTransactionsTracker'; +import { useCallback } from "react"; +import { logger } from "@/utils/logger"; +import { Transaction } from "@/components/TransactionCard"; +import { useAuth } from "@/contexts/auth/useAuth"; +import { toast } from "@/hooks/useToast.wrapper"; +import { saveTransactionsToStorage } from "./storageUtils"; +import { deleteTransactionFromSupabase } from "./supabaseUtils"; +import { addToDeletedTransactions } from "@/utils/sync/transaction/deletedTransactionsTracker"; /** * ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ๊ธฐ๋Šฅ์„ ์œ„ํ•œ ํ›… @@ -19,72 +19,87 @@ export const useDeleteTransaction = ( /** * ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์ฒ˜๋ฆฌ */ - const deleteTransaction = useCallback(async (transactionId: string): Promise => { - try { - console.log(`[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] ์‹œ์ž‘: ID=${transactionId}`); - - // ํŠธ๋žœ์žญ์…˜ ์กด์žฌ ํ™•์ธ - const transaction = transactions.find(t => t.id === transactionId); - if (!transaction) { - console.warn(`[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] ํŠธ๋žœ์žญ์…˜์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ: ${transactionId}`); + const deleteTransaction = useCallback( + async (transactionId: string): Promise => { + try { + logger.info(`[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] ์‹œ์ž‘: ID=${transactionId}`); + + // ํŠธ๋žœ์žญ์…˜ ์กด์žฌ ํ™•์ธ + const transaction = transactions.find((t) => t.id === transactionId); + if (!transaction) { + logger.warn( + `[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] ํŠธ๋žœ์žญ์…˜์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ: ${transactionId}` + ); + return false; + } + + logger.info( + `[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] ์‚ญ์ œ ๋Œ€์ƒ: "${transaction.title}", ๊ธˆ์•ก: ${transaction.amount}์›` + ); + + // ํŠธ๋žœ์žญ์…˜ ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ + const updatedTransactions = transactions.filter( + (t) => t.id !== transactionId + ); + + // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ์—…๋ฐ์ดํŠธ + saveTransactionsToStorage(updatedTransactions); + logger.info(`[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] ๋กœ์ปฌ ์ €์žฅ์†Œ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ`); + + // ์ƒํƒœ ์—…๋ฐ์ดํŠธ + setTransactions(updatedTransactions); + + // ํด๋ผ์šฐ๋“œ ๋™๊ธฐํ™” (Supabase) + if (user) { + try { + logger.info(`[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] Supabase ์‚ญ์ œ ์‹œ์ž‘: ${transactionId}`); + await deleteTransactionFromSupabase(user, transactionId); + logger.info(`[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] Supabase ์‚ญ์ œ ์„ฑ๊ณต: ${transactionId}`); + } catch (syncError) { + logger.error(`[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] Supabase ์‚ญ์ œ ์‹คํŒจ:`, syncError); + // ์‚ญ์ œ ์‹คํŒจํ•˜๋”๋ผ๋„ ๋กœ์ปฌ์—์„œ๋Š” ์‚ญ์ œ๋จ, ์ถ”์  ๋ชฉ๋ก์— ์ถ”๊ฐ€ + addToDeletedTransactions(transactionId); + logger.info( + `[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] ์‚ญ์ œ ํŠธ๋žœ์žญ์…˜ ์ถ”์  ๋ชฉ๋ก์— ์ถ”๊ฐ€: ${transactionId}` + ); + } + } else { + // ์˜คํ”„๋ผ์ธ ์ƒํƒœ์ด๊ฑฐ๋‚˜ ๋กœ๊ทธ์ธ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ์ถ”์  ๋ชฉ๋ก์— ์ถ”๊ฐ€ + addToDeletedTransactions(transactionId); + logger.info( + `[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] ์˜คํ”„๋ผ์ธ ์‚ญ์ œ: ์ถ”์  ๋ชฉ๋ก์— ์ถ”๊ฐ€ ${transactionId}` + ); + } + + // ์ด๋ฒคํŠธ ๋ฐœ์ƒ + window.dispatchEvent(new Event("transactionUpdated")); + window.dispatchEvent( + new CustomEvent("transactionDeleted", { + detail: { id: transactionId }, + }) + ); + + // ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ + toast({ + title: "์ง€์ถœ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค", + description: `${transaction.title} ํ•ญ๋ชฉ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, + duration: 3000, + }); + + logger.info(`[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] ์™„๋ฃŒ: ${transactionId}`); + return true; + } catch (error) { + logger.error(`[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] ์˜ค๋ฅ˜ ๋ฐœ์ƒ:`, error); + toast({ + title: "์‚ญ์ œ ์‹คํŒจ", + description: "์ง€์ถœ ํ•ญ๋ชฉ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + variant: "destructive", + }); return false; } - - console.log(`[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] ์‚ญ์ œ ๋Œ€์ƒ: "${transaction.title}", ๊ธˆ์•ก: ${transaction.amount}์›`); - - // ํŠธ๋žœ์žญ์…˜ ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ - const updatedTransactions = transactions.filter(t => t.id !== transactionId); - - // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ์—…๋ฐ์ดํŠธ - saveTransactionsToStorage(updatedTransactions); - console.log(`[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] ๋กœ์ปฌ ์ €์žฅ์†Œ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ`); - - // ์ƒํƒœ ์—…๋ฐ์ดํŠธ - setTransactions(updatedTransactions); - - // ํด๋ผ์šฐ๋“œ ๋™๊ธฐํ™” (Supabase) - if (user) { - try { - console.log(`[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] Supabase ์‚ญ์ œ ์‹œ์ž‘: ${transactionId}`); - await deleteTransactionFromSupabase(user, transactionId); - console.log(`[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] Supabase ์‚ญ์ œ ์„ฑ๊ณต: ${transactionId}`); - } catch (syncError) { - console.error(`[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] Supabase ์‚ญ์ œ ์‹คํŒจ:`, syncError); - // ์‚ญ์ œ ์‹คํŒจํ•˜๋”๋ผ๋„ ๋กœ์ปฌ์—์„œ๋Š” ์‚ญ์ œ๋จ, ์ถ”์  ๋ชฉ๋ก์— ์ถ”๊ฐ€ - addToDeletedTransactions(transactionId); - console.log(`[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] ์‚ญ์ œ ํŠธ๋žœ์žญ์…˜ ์ถ”์  ๋ชฉ๋ก์— ์ถ”๊ฐ€: ${transactionId}`); - } - } else { - // ์˜คํ”„๋ผ์ธ ์ƒํƒœ์ด๊ฑฐ๋‚˜ ๋กœ๊ทธ์ธ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ์ถ”์  ๋ชฉ๋ก์— ์ถ”๊ฐ€ - addToDeletedTransactions(transactionId); - console.log(`[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] ์˜คํ”„๋ผ์ธ ์‚ญ์ œ: ์ถ”์  ๋ชฉ๋ก์— ์ถ”๊ฐ€ ${transactionId}`); - } - - // ์ด๋ฒคํŠธ ๋ฐœ์ƒ - window.dispatchEvent(new Event('transactionUpdated')); - window.dispatchEvent(new CustomEvent('transactionDeleted', { - detail: { id: transactionId } - })); - - // ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ - toast({ - title: "์ง€์ถœ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค", - description: `${transaction.title} ํ•ญ๋ชฉ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, - duration: 3000 - }); - - console.log(`[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] ์™„๋ฃŒ: ${transactionId}`); - return true; - } catch (error) { - console.error(`[ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ] ์˜ค๋ฅ˜ ๋ฐœ์ƒ:`, error); - toast({ - title: "์‚ญ์ œ ์‹คํŒจ", - description: "์ง€์ถœ ํ•ญ๋ชฉ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" - }); - return false; - } - }, [transactions, setTransactions, user]); + }, + [transactions, setTransactions, user] + ); return { deleteTransaction }; }; diff --git a/src/hooks/transactions/filterOperations/index.ts b/src/hooks/transactions/filterOperations/index.ts index 6bdbec9..ef8c9c8 100644 --- a/src/hooks/transactions/filterOperations/index.ts +++ b/src/hooks/transactions/filterOperations/index.ts @@ -1,9 +1,13 @@ - -import { useCallback, useEffect } from 'react'; -import { Transaction } from '@/contexts/budget/types'; -import { getCurrentMonth, getPrevMonth, getNextMonth } from '../dateUtils'; -import { filterTransactionsByMonth, filterTransactionsByQuery, calculateTotalExpenses } from '../filterUtils'; -import { parseTransactionDate } from '@/utils/dateParser'; +import { useCallback, useEffect } from "react"; +import { logger } from "@/utils/logger"; +import { Transaction } from "@/contexts/budget/types"; +import { getCurrentMonth, getPrevMonth, getNextMonth } from "../dateUtils"; +import { + filterTransactionsByMonth, + filterTransactionsByQuery, + calculateTotalExpenses, +} from "../filterUtils"; +import { parseTransactionDate } from "@/utils/dateParser"; interface UseTransactionsFilteringProps { transactions: Transaction[]; @@ -22,27 +26,32 @@ export const useTransactionsFiltering = ({ selectedMonth, setSelectedMonth, searchQuery, - setFilteredTransactions + setFilteredTransactions, }: UseTransactionsFilteringProps) => { // ํ•„ํ„ฐ๋ง ์ ์šฉ useEffect(() => { - console.log('ํŠธ๋žœ์žญ์…˜ ํ•„ํ„ฐ๋ง ์ ์šฉ:', { ์„ ํƒ๋œ์›”: selectedMonth, ๊ฒ€์ƒ‰์–ด: searchQuery }); - + logger.info("ํŠธ๋žœ์žญ์…˜ ํ•„ํ„ฐ๋ง ์ ์šฉ:", { + ์„ ํƒ๋œ์›”: selectedMonth, + ๊ฒ€์ƒ‰์–ด: searchQuery, + }); + try { // ๋จผ์ € ์›”๋ณ„ ํ•„ํ„ฐ๋ง - ๊ฐœ์„ ๋œ ๋‚ ์งœ ์ฒ˜๋ฆฌ ๊ธฐ๋Šฅ ์‚ฌ์šฉ - const monthFiltered = filterTransactionsByMonth(transactions, selectedMonth); - console.log('์›”๋ณ„ ํ•„ํ„ฐ๋ง ๊ฒฐ๊ณผ:', monthFiltered.length); - + const monthFiltered = filterTransactionsByMonth( + transactions, + selectedMonth + ); + logger.info("์›”๋ณ„ ํ•„ํ„ฐ๋ง ๊ฒฐ๊ณผ:", monthFiltered.length); + // ๊ทธ ๋‹ค์Œ ๊ฒ€์ƒ‰์–ด ํ•„ํ„ฐ๋ง - const searchFiltered = searchQuery + const searchFiltered = searchQuery ? filterTransactionsByQuery(monthFiltered, searchQuery) : monthFiltered; - - console.log('์ตœ์ข… ํ•„ํ„ฐ๋ง ๊ฒฐ๊ณผ:', searchFiltered.length); + + logger.info("์ตœ์ข… ํ•„ํ„ฐ๋ง ๊ฒฐ๊ณผ:", searchFiltered.length); setFilteredTransactions(searchFiltered); - } catch (error) { - console.error('ํŠธ๋žœ์žญ์…˜ ํ•„ํ„ฐ๋ง ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error); + logger.error("ํŠธ๋žœ์žญ์…˜ ํ•„ํ„ฐ๋ง ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:", error); // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์›๋ณธ ๋ฐ์ดํ„ฐ ์œ ์ง€ setFilteredTransactions(transactions); } @@ -59,16 +68,19 @@ export const useTransactionsFiltering = ({ }, [selectedMonth, setSelectedMonth]); // ์ด ์ง€์ถœ ๊ณ„์‚ฐ - ๊ฐœ์„ ๋œ ๊ณ„์‚ฐ ๋กœ์ง ์‚ฌ์šฉ - const getTotalExpenses = useCallback((filteredTransactions: Transaction[]): number => { - console.log('์ด ์ง€์ถœ ๊ณ„์‚ฐ ์ค‘...', filteredTransactions.length); - const total = calculateTotalExpenses(filteredTransactions); - console.log('๊ณ„์‚ฐ๋œ ์ด ์ง€์ถœ:', total); - return total; - }, []); + const getTotalExpenses = useCallback( + (filteredTransactions: Transaction[]): number => { + logger.info("์ด ์ง€์ถœ ๊ณ„์‚ฐ ์ค‘...", filteredTransactions.length); + const total = calculateTotalExpenses(filteredTransactions); + logger.info("๊ณ„์‚ฐ๋œ ์ด ์ง€์ถœ:", total); + return total; + }, + [] + ); return { handlePrevMonth, handleNextMonth, - getTotalExpenses + getTotalExpenses, }; }; diff --git a/src/hooks/transactions/filterOperations/types.ts b/src/hooks/transactions/filterOperations/types.ts index da24388..03f43f5 100644 --- a/src/hooks/transactions/filterOperations/types.ts +++ b/src/hooks/transactions/filterOperations/types.ts @@ -1,5 +1,4 @@ - -import { Transaction } from '@/components/TransactionCard'; +import { Transaction } from "@/components/TransactionCard"; export interface FilteringProps { transactions: Transaction[]; diff --git a/src/hooks/transactions/filterOperations/useFilterApplication.ts b/src/hooks/transactions/filterOperations/useFilterApplication.ts index d214967..043d352 100644 --- a/src/hooks/transactions/filterOperations/useFilterApplication.ts +++ b/src/hooks/transactions/filterOperations/useFilterApplication.ts @@ -1,55 +1,65 @@ - -import { useCallback, useEffect } from 'react'; -import { Transaction } from '@/components/TransactionCard'; -import { FilteringProps } from './types'; -import { MONTHS_KR } from '../dateUtils'; +import { useCallback, useEffect } from "react"; +import { logger } from "@/utils/logger"; +import { Transaction } from "@/components/TransactionCard"; +import { FilteringProps } from "./types"; +import { MONTHS_KR } from "../dateUtils"; /** * ๊ฑฐ๋ž˜ ํ•„ํ„ฐ๋ง ๋กœ์ง * ์„ ํƒ๋œ ์›”๊ณผ ๊ฒ€์ƒ‰์–ด๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ฑฐ๋ž˜๋ฅผ ํ•„ํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค. */ -export const useFilterApplication = ({ - transactions, - selectedMonth, - searchQuery, - setFilteredTransactions -}: Pick) => { - +export const useFilterApplication = ({ + transactions, + selectedMonth, + searchQuery, + setFilteredTransactions, +}: Pick< + FilteringProps, + "transactions" | "selectedMonth" | "searchQuery" | "setFilteredTransactions" +>) => { // ๊ฑฐ๋ž˜ ํ•„ํ„ฐ๋ง ํ•จ์ˆ˜ const filterTransactions = useCallback(() => { try { - console.log('ํ•„ํ„ฐ๋ง ์‹œ์ž‘, ์ „์ฒด ํŠธ๋žœ์žญ์…˜:', transactions.length); - console.log('์„ ํƒ๋œ ์›”:', selectedMonth); - + logger.info("ํ•„ํ„ฐ๋ง ์‹œ์ž‘, ์ „์ฒด ํŠธ๋žœ์žญ์…˜:", transactions.length); + logger.info("์„ ํƒ๋œ ์›”:", selectedMonth); + // ์„ ํƒ๋œ ์›” ์ •๋ณด ํŒŒ์‹ฑ const selectedMonthName = selectedMonth; - const monthNumber = MONTHS_KR.findIndex(month => month === selectedMonthName) + 1; - + const monthNumber = + MONTHS_KR.findIndex((month) => month === selectedMonthName) + 1; + // ์›”๋ณ„ ํ•„ํ„ฐ๋ง - let filtered = transactions.filter(transaction => { - if (!transaction.date) return false; - - console.log(`ํŠธ๋žœ์žญ์…˜ ๋‚ ์งœ ํ™•์ธ: "${transaction.date}", ํƒ€์ž…: ${typeof transaction.date}`); - + let filtered = transactions.filter((transaction) => { + if (!transaction.date) { + return false; + } + + logger.info( + `ํŠธ๋žœ์žญ์…˜ ๋‚ ์งœ ํ™•์ธ: "${transaction.date}", ํƒ€์ž…: ${typeof transaction.date}` + ); + // ๋‹ค์–‘ํ•œ ๋‚ ์งœ ํ˜•์‹ ์ฒ˜๋ฆฌ if (transaction.date.includes(selectedMonthName)) { return true; // ์„ ํƒ๋œ ์›” ์ด๋ฆ„์ด ํฌํ•จ๋œ ๊ฒฝ์šฐ } - - if (transaction.date.includes('์˜ค๋Š˜')) { + + if (transaction.date.includes("์˜ค๋Š˜")) { // ์˜ค๋Š˜ ๋‚ ์งœ๊ฐ€ ํ•ด๋‹น ์›”์ธ์ง€ ํ™•์ธ const today = new Date(); const currentMonth = today.getMonth() + 1; // 0๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋ฏ€๋กœ +1 return currentMonth === monthNumber; } - + // ๋‹ค๋ฅธ ํ˜•์‹์˜ ๋‚ ์งœ๋„ ์‹œ๋„ try { // ISO ํ˜•์‹์ด ์•„๋‹Œ ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ - if (transaction.date.includes('๋…„') || transaction.date.includes('์›”')) { + if ( + transaction.date.includes("๋…„") || + transaction.date.includes("์›”") + ) { return transaction.date.includes(selectedMonthName); } - + // ํ‘œ์ค€ ๋‚ ์งœ ๋ฌธ์ž์—ด ์ฒ˜๋ฆฌ ์‹œ๋„ const date = new Date(transaction.date); if (!isNaN(date.getTime())) { @@ -57,31 +67,32 @@ export const useFilterApplication = ({ return transactionMonth === monthNumber; } } catch (e) { - console.error('๋‚ ์งœ ํŒŒ์‹ฑ ์˜ค๋ฅ˜:', e); + logger.error("๋‚ ์งœ ํŒŒ์‹ฑ ์˜ค๋ฅ˜:", e); } - + // ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ชจ๋“  ํŠธ๋žœ์žญ์…˜ ํฌํ•จ return true; }); - - console.log(`์›”๋ณ„ ํ•„ํ„ฐ๋ง ๊ฒฐ๊ณผ: ${filtered.length} ํŠธ๋žœ์žญ์…˜`); + + logger.info(`์›”๋ณ„ ํ•„ํ„ฐ๋ง ๊ฒฐ๊ณผ: ${filtered.length} ํŠธ๋žœ์žญ์…˜`); // ๊ฒ€์ƒ‰์–ด์— ๋”ฐ๋ฅธ ํ•„ํ„ฐ๋ง if (searchQuery.trim()) { const searchLower = searchQuery.toLowerCase(); - filtered = filtered.filter(transaction => - transaction.title.toLowerCase().includes(searchLower) || - transaction.category.toLowerCase().includes(searchLower) || - transaction.amount.toString().includes(searchQuery) + filtered = filtered.filter( + (transaction) => + transaction.title.toLowerCase().includes(searchLower) || + transaction.category.toLowerCase().includes(searchLower) || + transaction.amount.toString().includes(searchQuery) ); - console.log(`๊ฒ€์ƒ‰์–ด ํ•„ํ„ฐ๋ง ๊ฒฐ๊ณผ: ${filtered.length} ํŠธ๋žœ์žญ์…˜`); + logger.info(`๊ฒ€์ƒ‰์–ด ํ•„ํ„ฐ๋ง ๊ฒฐ๊ณผ: ${filtered.length} ํŠธ๋žœ์žญ์…˜`); } - + // ๊ฒฐ๊ณผ ์„ค์ • setFilteredTransactions(filtered); - console.log('์ตœ์ข… ํ•„ํ„ฐ๋ง ๊ฒฐ๊ณผ:', filtered); + logger.info("์ตœ์ข… ํ•„ํ„ฐ๋ง ๊ฒฐ๊ณผ:", filtered); } catch (error) { - console.error('๊ฑฐ๋ž˜ ํ•„ํ„ฐ๋ง ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("๊ฑฐ๋ž˜ ํ•„ํ„ฐ๋ง ์ค‘ ์˜ค๋ฅ˜:", error); setFilteredTransactions([]); } }, [transactions, selectedMonth, searchQuery, setFilteredTransactions]); @@ -92,6 +103,6 @@ export const useFilterApplication = ({ }, [transactions, selectedMonth, searchQuery, filterTransactions]); return { - filterTransactions + filterTransactions, }; }; diff --git a/src/hooks/transactions/filterOperations/useMonthSelection.ts b/src/hooks/transactions/filterOperations/useMonthSelection.ts index 51046c7..22a3e89 100644 --- a/src/hooks/transactions/filterOperations/useMonthSelection.ts +++ b/src/hooks/transactions/filterOperations/useMonthSelection.ts @@ -1,34 +1,34 @@ - -import { useCallback } from 'react'; -import { getPrevMonth, getNextMonth } from '../dateUtils'; +import { useCallback } from "react"; +import { logger } from "@/utils/logger"; +import { getPrevMonth, getNextMonth } from "../dateUtils"; /** * ์›” ์„ ํƒ ๊ด€๋ จ ํ›… * ์ด์ „/๋‹ค์Œ ์›” ์ด๋™ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. */ -export const useMonthSelection = ({ - selectedMonth, - setSelectedMonth -}: { +export const useMonthSelection = ({ + selectedMonth, + setSelectedMonth, +}: { selectedMonth: string; setSelectedMonth: (month: string) => void; }) => { // ์ด์ „ ์›”๋กœ ์ด๋™ const handlePrevMonth = useCallback(() => { const prevMonth = getPrevMonth(selectedMonth); - console.log(`์›” ๋ณ€๊ฒฝ: ${selectedMonth} -> ${prevMonth}`); + logger.info(`์›” ๋ณ€๊ฒฝ: ${selectedMonth} -> ${prevMonth}`); setSelectedMonth(prevMonth); }, [selectedMonth, setSelectedMonth]); // ๋‹ค์Œ ์›”๋กœ ์ด๋™ const handleNextMonth = useCallback(() => { const nextMonth = getNextMonth(selectedMonth); - console.log(`์›” ๋ณ€๊ฒฝ: ${selectedMonth} -> ${nextMonth}`); + logger.info(`์›” ๋ณ€๊ฒฝ: ${selectedMonth} -> ${nextMonth}`); setSelectedMonth(nextMonth); }, [selectedMonth, setSelectedMonth]); return { handlePrevMonth, - handleNextMonth + handleNextMonth, }; }; diff --git a/src/hooks/transactions/filterOperations/useTotalCalculation.ts b/src/hooks/transactions/filterOperations/useTotalCalculation.ts index babbc12..3456092 100644 --- a/src/hooks/transactions/filterOperations/useTotalCalculation.ts +++ b/src/hooks/transactions/filterOperations/useTotalCalculation.ts @@ -1,6 +1,5 @@ - -import { Transaction } from '@/components/TransactionCard'; -import { calculateTotalExpenses } from '../filterUtils'; +import { Transaction } from "@/components/TransactionCard"; +import { calculateTotalExpenses } from "../filterUtils"; /** * ์ด ์ง€์ถœ ๊ณ„์‚ฐ ๊ด€๋ จ ํ›… @@ -13,6 +12,6 @@ export const useTotalCalculation = () => { }; return { - getTotalExpenses + getTotalExpenses, }; }; diff --git a/src/hooks/transactions/filterUtils.ts b/src/hooks/transactions/filterUtils.ts index 919dbc3..622f551 100644 --- a/src/hooks/transactions/filterUtils.ts +++ b/src/hooks/transactions/filterUtils.ts @@ -1,45 +1,54 @@ - -import { Transaction } from '@/contexts/budget/types'; -import { parseTransactionDate } from '@/utils/dateParser'; -import { format } from 'date-fns'; +import { Transaction } from "@/contexts/budget/types"; +import { logger } from "@/utils/logger"; +import { parseTransactionDate } from "@/utils/dateParser"; +import { format } from "date-fns"; /** * ํŠธ๋žœ์žญ์…˜์„ ์›”๋ณ„๋กœ ํ•„ํ„ฐ๋ง */ -export const filterTransactionsByMonth = (transactions: Transaction[], selectedMonth: string): Transaction[] => { +export const filterTransactionsByMonth = ( + transactions: Transaction[], + selectedMonth: string +): Transaction[] => { if (!transactions || transactions.length === 0) { return []; } - console.log(`์›”๋ณ„ ํ•„ํ„ฐ๋ง ์‹œ์ž‘: ${selectedMonth}, ํŠธ๋žœ์žญ์…˜ ์ˆ˜: ${transactions.length}`); - + logger.info( + `์›”๋ณ„ ํ•„ํ„ฐ๋ง ์‹œ์ž‘: ${selectedMonth}, ํŠธ๋žœ์žญ์…˜ ์ˆ˜: ${transactions.length}` + ); + try { - const [year, month] = selectedMonth.split('-').map(Number); - - const filtered = transactions.filter(transaction => { + const [year, month] = selectedMonth.split("-").map(Number); + + const filtered = transactions.filter((transaction) => { const date = parseTransactionDate(transaction.date); - + if (!date) { - console.warn(`๋‚ ์งœ๋ฅผ ํŒŒ์‹ฑํ•  ์ˆ˜ ์—†์Œ: ${transaction.date}, ํŠธ๋žœ์žญ์…˜ ID: ${transaction.id}`); + logger.warn( + `๋‚ ์งœ๋ฅผ ํŒŒ์‹ฑํ•  ์ˆ˜ ์—†์Œ: ${transaction.date}, ํŠธ๋žœ์žญ์…˜ ID: ${transaction.id}` + ); return false; } - + const transactionYear = date.getFullYear(); const transactionMonth = date.getMonth() + 1; // JavaScript ์›”์€ 0๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋ฏ€๋กœ +1 - + const match = transactionYear === year && transactionMonth === month; - + if (match) { - console.log(`ํŠธ๋žœ์žญ์…˜ ๋งค์นญ: ${transaction.id}, ์ œ๋ชฉ: ${transaction.title}, ๋‚ ์งœ: ${transaction.date}`); + logger.info( + `ํŠธ๋žœ์žญ์…˜ ๋งค์นญ: ${transaction.id}, ์ œ๋ชฉ: ${transaction.title}, ๋‚ ์งœ: ${transaction.date}` + ); } - + return match; }); - - console.log(`์›”๋ณ„ ํ•„ํ„ฐ๋ง ๊ฒฐ๊ณผ: ${filtered.length}๊ฐœ ํŠธ๋žœ์žญ์…˜`); + + logger.info(`์›”๋ณ„ ํ•„ํ„ฐ๋ง ๊ฒฐ๊ณผ: ${filtered.length}๊ฐœ ํŠธ๋žœ์žญ์…˜`); return filtered; } catch (error) { - console.error('์›”๋ณ„ ํ•„ํ„ฐ๋ง ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("์›”๋ณ„ ํ•„ํ„ฐ๋ง ์ค‘ ์˜ค๋ฅ˜:", error); return []; } }; @@ -47,18 +56,25 @@ export const filterTransactionsByMonth = (transactions: Transaction[], selectedM /** * ํŠธ๋žœ์žญ์…˜์„ ๊ฒ€์ƒ‰์–ด๋กœ ํ•„ํ„ฐ๋ง */ -export const filterTransactionsByQuery = (transactions: Transaction[], searchQuery: string): Transaction[] => { - if (!searchQuery || searchQuery.trim() === '') { +export const filterTransactionsByQuery = ( + transactions: Transaction[], + searchQuery: string +): Transaction[] => { + if (!searchQuery || searchQuery.trim() === "") { return transactions; } - + const normalizedQuery = searchQuery.toLowerCase().trim(); - - return transactions.filter(transaction => { - const titleMatch = transaction.title.toLowerCase().includes(normalizedQuery); - const categoryMatch = transaction.category.toLowerCase().includes(normalizedQuery); + + return transactions.filter((transaction) => { + const titleMatch = transaction.title + .toLowerCase() + .includes(normalizedQuery); + const categoryMatch = transaction.category + .toLowerCase() + .includes(normalizedQuery); const amountMatch = transaction.amount.toString().includes(normalizedQuery); - + return titleMatch || categoryMatch || amountMatch; }); }; @@ -68,49 +84,55 @@ export const filterTransactionsByQuery = (transactions: Transaction[], searchQue */ export const calculateTotalExpenses = (transactions: Transaction[]): number => { if (!transactions || transactions.length === 0) { - console.log('๊ณ„์‚ฐํ•  ํŠธ๋žœ์žญ์…˜์ด ์—†์Šต๋‹ˆ๋‹ค.'); + logger.info("๊ณ„์‚ฐํ•  ํŠธ๋žœ์žญ์…˜์ด ์—†์Šต๋‹ˆ๋‹ค."); return 0; } - - console.log(`์ด ์ง€์ถœ ๊ณ„์‚ฐ ์‹œ์ž‘: ํŠธ๋žœ์žญ์…˜ ${transactions.length}๊ฐœ`); - + + logger.info(`์ด ์ง€์ถœ ๊ณ„์‚ฐ ์‹œ์ž‘: ํŠธ๋žœ์žญ์…˜ ${transactions.length}๊ฐœ`); + // ์ง€์ถœ ํƒ€์ž…๋งŒ ํ•„ํ„ฐ๋งํ•˜๊ณ  ํ•ฉ์‚ฐ const expenses = transactions - .filter(t => t.type === 'expense') + .filter((t) => t.type === "expense") .reduce((sum, transaction) => { const amount = Number(transaction.amount); if (isNaN(amount)) { - console.warn(`์œ ํšจํ•˜์ง€ ์•Š์€ ๊ธˆ์•ก: ${transaction.amount}, ํŠธ๋žœ์žญ์…˜ ID: ${transaction.id}`); + logger.warn( + `์œ ํšจํ•˜์ง€ ์•Š์€ ๊ธˆ์•ก: ${transaction.amount}, ํŠธ๋žœ์žญ์…˜ ID: ${transaction.id}` + ); return sum; } return sum + amount; }, 0); - - console.log(`์ด ์ง€์ถœ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ: ${expenses}์›`); + + logger.info(`์ด ์ง€์ถœ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ: ${expenses}์›`); return expenses; }; /** * ํŠธ๋žœ์žญ์…˜์„ ๋‚ ์งœ๋ณ„๋กœ ๊ทธ๋ฃนํ™” */ -export const groupTransactionsByDate = (transactions: Transaction[]): Record => { +export const groupTransactionsByDate = ( + transactions: Transaction[] +): Record => { const groups: Record = {}; - - transactions.forEach(transaction => { + + transactions.forEach((transaction) => { const date = parseTransactionDate(transaction.date); if (!date) { - console.warn(`๋‚ ์งœ๋ฅผ ํŒŒ์‹ฑํ•  ์ˆ˜ ์—†์Œ: ${transaction.date}, ํŠธ๋žœ์žญ์…˜ ID: ${transaction.id}`); + logger.warn( + `๋‚ ์งœ๋ฅผ ํŒŒ์‹ฑํ•  ์ˆ˜ ์—†์Œ: ${transaction.date}, ํŠธ๋žœ์žญ์…˜ ID: ${transaction.id}` + ); return; } - - const formattedDate = format(date, 'yyyy-MM-dd'); - + + const formattedDate = format(date, "yyyy-MM-dd"); + if (!groups[formattedDate]) { groups[formattedDate] = []; } - + groups[formattedDate].push(transaction); }); - + return groups; }; diff --git a/src/hooks/transactions/index.ts b/src/hooks/transactions/index.ts index 2379ba9..b28304e 100644 --- a/src/hooks/transactions/index.ts +++ b/src/hooks/transactions/index.ts @@ -1,5 +1,13 @@ - // ํŠธ๋žœ์žญ์…˜ ๊ด€๋ จ ๋ชจ๋“  ํ›…๊ณผ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๋ฅผ ์žฌ๋‚ด๋ณด๋‚ด๊ธฐ -export { useTransactions } from './useTransactions'; -export { MONTHS_KR, getCurrentMonth, getPrevMonth, getNextMonth } from './dateUtils'; -export { filterTransactionsByMonth, filterTransactionsByQuery, calculateTotalExpenses } from './filterUtils'; +export { useTransactions } from "./useTransactions"; +export { + MONTHS_KR, + getCurrentMonth, + getPrevMonth, + getNextMonth, +} from "./dateUtils"; +export { + filterTransactionsByMonth, + filterTransactionsByQuery, + calculateTotalExpenses, +} from "./filterUtils"; diff --git a/src/hooks/transactions/storageUtils.ts b/src/hooks/transactions/storageUtils.ts index dba0801..dc88e35 100644 --- a/src/hooks/transactions/storageUtils.ts +++ b/src/hooks/transactions/storageUtils.ts @@ -1,90 +1,94 @@ - -import { Transaction } from '@/contexts/budget/types'; -import { toast } from '@/hooks/useToast.wrapper'; -import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons'; +import { Transaction } from "@/contexts/budget/types"; +import { storageLogger } from "@/utils/logger"; +import { toast } from "@/hooks/useToast.wrapper"; +import { EXPENSE_CATEGORIES } from "@/constants/categoryIcons"; // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—์„œ ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ๋กœ๋“œ export const loadTransactionsFromStorage = (): Transaction[] => { try { // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—์„œ ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ - const localDataStr = localStorage.getItem('transactions'); - console.log('๋กœ์ปฌ ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ:', localDataStr); - + const localDataStr = localStorage.getItem("transactions"); + storageLogger.info("๋กœ์ปฌ ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ:", localDataStr); + if (localDataStr) { try { const localData = JSON.parse(localDataStr); - + // ์ง€์›๋˜๋Š” ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ํ•„ํ„ฐ๋ง ๋ฐ ์นดํ…Œ๊ณ ๋ฆฌ๋ช… ๋ณ€ํ™˜ const filteredData = localData.map((transaction: Transaction) => { - if (transaction.type === 'expense') { + if (transaction.type === "expense") { // ๊ธฐ์กด ์นดํ…Œ๊ณ ๋ฆฌ๋ช… ๋ณ€ํ™˜ - if (transaction.category === '์‹๋น„') { - return { - ...transaction, - category: '์Œ์‹', - paymentMethod: transaction.paymentMethod || '์‹ ์šฉ์นด๋“œ' // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์— ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์ถ”๊ฐ€ + if (transaction.category === "์‹๋น„") { + return { + ...transaction, + category: "์Œ์‹", + paymentMethod: transaction.paymentMethod || "์‹ ์šฉ์นด๋“œ", // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์— ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์ถ”๊ฐ€ }; - } else if (transaction.category === '์ƒํ™œ๋น„') { - return { - ...transaction, - category: '์‡ผํ•‘', - paymentMethod: transaction.paymentMethod || '์‹ ์šฉ์นด๋“œ' // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์— ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์ถ”๊ฐ€ + } else if (transaction.category === "์ƒํ™œ๋น„") { + return { + ...transaction, + category: "์‡ผํ•‘", + paymentMethod: transaction.paymentMethod || "์‹ ์šฉ์นด๋“œ", // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์— ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์ถ”๊ฐ€ }; } else if (!EXPENSE_CATEGORIES.includes(transaction.category)) { - return { - ...transaction, - category: '์‡ผํ•‘', - paymentMethod: transaction.paymentMethod || '์‹ ์šฉ์นด๋“œ' // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์— ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์ถ”๊ฐ€ + return { + ...transaction, + category: "์‡ผํ•‘", + paymentMethod: transaction.paymentMethod || "์‹ ์šฉ์นด๋“œ", // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์— ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์ถ”๊ฐ€ }; // ์ง€์›๋˜์ง€ ์•Š๋Š” ์นดํ…Œ๊ณ ๋ฆฌ๋Š” '์‡ผํ•‘'์œผ๋กœ } - + // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์— paymentMethod๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์ถ”๊ฐ€ if (!transaction.paymentMethod) { return { ...transaction, - paymentMethod: '์‹ ์šฉ์นด๋“œ' + paymentMethod: "์‹ ์šฉ์นด๋“œ", }; } } return transaction; }); - - console.log('ํ•„ํ„ฐ๋ง๋œ ํŠธ๋žœ์žญ์…˜:', filteredData.length); + + storageLogger.info("ํ•„ํ„ฐ๋ง๋œ ํŠธ๋žœ์žญ์…˜:", filteredData.length); return filteredData; } catch (parseError) { - console.error('ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์˜ค๋ฅ˜:', parseError); + storageLogger.error("ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์˜ค๋ฅ˜:", parseError); return []; } } } catch (err) { - console.error('ํŠธ๋žœ์žญ์…˜ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜:', err); + storageLogger.error("ํŠธ๋žœ์žญ์…˜ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜:", err); } - - console.log('๋กœ์ปฌ ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ์—†์Œ'); + + storageLogger.info("๋กœ์ปฌ ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ์—†์Œ"); return []; }; // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ ์ €์žฅ -export const saveTransactionsToStorage = (transactions: Transaction[]): void => { +export const saveTransactionsToStorage = ( + transactions: Transaction[] +): void => { try { const dataString = JSON.stringify(transactions); - localStorage.setItem('transactions', dataString); - localStorage.setItem('transactions_backup', dataString); // ๋ฐฑ์—…๋„ ์ €์žฅ - + localStorage.setItem("transactions", dataString); + localStorage.setItem("transactions_backup", dataString); // ๋ฐฑ์—…๋„ ์ €์žฅ + // ์ด๋ฒคํŠธ ๋ฐœ์ƒ - window.dispatchEvent(new Event('transactionUpdated')); - window.dispatchEvent(new StorageEvent('storage', { - key: 'transactions', - newValue: dataString - })); - - console.log('ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์™„๋ฃŒ:', transactions.length, '๊ฐœ'); + window.dispatchEvent(new Event("transactionUpdated")); + window.dispatchEvent( + new StorageEvent("storage", { + key: "transactions", + newValue: dataString, + }) + ); + + storageLogger.info("ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์™„๋ฃŒ:", transactions.length, "๊ฐœ"); } catch (error) { - console.error('ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์˜ค๋ฅ˜:', error); + storageLogger.error("ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์˜ค๋ฅ˜:", error); toast({ title: "๋ฐ์ดํ„ฐ ์ €์žฅ ์‹คํŒจ", description: "ํŠธ๋žœ์žญ์…˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" + variant: "destructive", }); } }; @@ -92,13 +96,13 @@ export const saveTransactionsToStorage = (transactions: Transaction[]): void => // ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ export const loadBudgetFromStorage = (): number => { try { - const budgetDataStr = localStorage.getItem('budgetData'); + const budgetDataStr = localStorage.getItem("budgetData"); if (budgetDataStr) { const budgetData = JSON.parse(budgetDataStr); return budgetData.monthly.targetAmount; } } catch (e) { - console.error('์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์˜ค๋ฅ˜:', e); + storageLogger.error("์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์˜ค๋ฅ˜:", e); } return 0; }; diff --git a/src/hooks/transactions/transactionOperations/index.ts b/src/hooks/transactions/transactionOperations/index.ts index be9a6bf..be5081f 100644 --- a/src/hooks/transactions/transactionOperations/index.ts +++ b/src/hooks/transactions/transactionOperations/index.ts @@ -1,32 +1,42 @@ -import { useCallback } from 'react'; -import { Transaction } from '@/contexts/budget/types'; -import { useBudget } from '@/contexts/budget/BudgetContext'; +import { useCallback } from "react"; +import { logger } from "@/utils/logger"; +import { Transaction } from "@/contexts/budget/types"; +import { useBudget } from "@/contexts/budget/BudgetContext"; export const useTransactionsOperations = (transactions: Transaction[]) => { - const { updateTransaction: budgetUpdateTransaction, deleteTransaction: budgetDeleteTransaction } = useBudget(); + const { + updateTransaction: budgetUpdateTransaction, + deleteTransaction: budgetDeleteTransaction, + } = useBudget(); // ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ - const updateTransaction = useCallback((updatedTransaction: Transaction): void => { - try { - budgetUpdateTransaction(updatedTransaction); - } catch (error) { - console.error('ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ์ค‘ ์˜ค๋ฅ˜:', error); - } - }, [budgetUpdateTransaction]); + const updateTransaction = useCallback( + (updatedTransaction: Transaction): void => { + try { + budgetUpdateTransaction(updatedTransaction); + } catch (error) { + logger.error("ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ์ค‘ ์˜ค๋ฅ˜:", error); + } + }, + [budgetUpdateTransaction] + ); // ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ํ•จ์ˆ˜ - const deleteTransaction = useCallback(async (id: string): Promise => { - try { - budgetDeleteTransaction(id); - return true; - } catch (error) { - console.error('ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜:', error); - return false; - } - }, [budgetDeleteTransaction]); + const deleteTransaction = useCallback( + async (id: string): Promise => { + try { + budgetDeleteTransaction(id); + return true; + } catch (error) { + logger.error("ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜:", error); + return false; + } + }, + [budgetDeleteTransaction] + ); return { updateTransaction, - deleteTransaction + deleteTransaction, }; }; diff --git a/src/hooks/transactions/transactionOperations/types.ts b/src/hooks/transactions/transactionOperations/types.ts index 10ede9c..657167d 100644 --- a/src/hooks/transactions/transactionOperations/types.ts +++ b/src/hooks/transactions/transactionOperations/types.ts @@ -1,5 +1,4 @@ - -import { Transaction } from '@/components/TransactionCard'; +import { Transaction } from "@/components/TransactionCard"; export interface TransactionOperationProps { transactions: Transaction[]; diff --git a/src/hooks/transactions/transactionOperations/updateTransaction.ts b/src/hooks/transactions/transactionOperations/updateTransaction.ts index 0f8340b..8ea5a0d 100644 --- a/src/hooks/transactions/transactionOperations/updateTransaction.ts +++ b/src/hooks/transactions/transactionOperations/updateTransaction.ts @@ -1,12 +1,12 @@ - -import { useCallback } from 'react'; -import { Transaction } from '@/components/TransactionCard'; -import { useAuth } from '@/contexts/auth/useAuth'; -import { toast } from '@/hooks/useToast.wrapper'; -import { saveTransactionsToStorage } from '../storageUtils'; -import { updateTransactionInSupabase } from '../supabaseUtils'; -import { TransactionOperationProps } from './types'; -import { normalizeDate } from '@/utils/sync/transaction/dateUtils'; +import { useCallback } from "react"; +import { logger } from "@/utils/logger"; +import { Transaction } from "@/components/TransactionCard"; +import { useAuth } from "@/contexts/auth/useAuth"; +import { toast } from "@/hooks/useToast.wrapper"; +import { saveTransactionsToStorage } from "../storageUtils"; +import { updateTransactionInSupabase } from "../supabaseUtils"; +import { TransactionOperationProps } from "./types"; +import { normalizeDate } from "@/utils/sync/transaction/dateUtils"; /** * ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ๊ธฐ๋Šฅ @@ -18,88 +18,104 @@ export const useUpdateTransaction = ( ) => { const { user } = useAuth(); - return useCallback((updatedTransaction: Transaction) => { - try { - console.log(`[ํŠธ๋žœ์žญ์…˜] ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ID=${updatedTransaction.id}, ์ œ๋ชฉ=${updatedTransaction.title}`); - - // ํŠธ๋žœ์žญ์…˜ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ - const existingIndex = transactions.findIndex(t => t.id === updatedTransaction.id); - if (existingIndex === -1) { - console.warn(`[ํŠธ๋žœ์žญ์…˜] ์—…๋ฐ์ดํŠธ ์˜ค๋ฅ˜: ID ${updatedTransaction.id}๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ`); - toast({ - title: "์—…๋ฐ์ดํŠธ ์‹คํŒจ", - description: "ํ•ด๋‹น ์ง€์ถœ ํ•ญ๋ชฉ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", - variant: "destructive" - }); - return; - } - - // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์™€ ๋ณ€๊ฒฝ ๊ฐ์ง€ - const oldTransaction = transactions[existingIndex]; - const hasChanges = JSON.stringify(oldTransaction) !== JSON.stringify(updatedTransaction); - - if (!hasChanges) { - console.log(`[ํŠธ๋žœ์žญ์…˜] ๋ณ€๊ฒฝ์‚ฌํ•ญ ์—†์Œ: ${updatedTransaction.id}`); - return; - } - - // ๋ณ€๊ฒฝ ๋‚ด์šฉ ๋กœ๊น… - console.log(`[ํŠธ๋žœ์žญ์…˜] ๋ณ€๊ฒฝ ๊ฐ์ง€: + return useCallback( + (updatedTransaction: Transaction) => { + try { + logger.info( + `[ํŠธ๋žœ์žญ์…˜] ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ID=${updatedTransaction.id}, ์ œ๋ชฉ=${updatedTransaction.title}` + ); + + // ํŠธ๋žœ์žญ์…˜ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ + const existingIndex = transactions.findIndex( + (t) => t.id === updatedTransaction.id + ); + if (existingIndex === -1) { + logger.warn( + `[ํŠธ๋žœ์žญ์…˜] ์—…๋ฐ์ดํŠธ ์˜ค๋ฅ˜: ID ${updatedTransaction.id}๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ` + ); + toast({ + title: "์—…๋ฐ์ดํŠธ ์‹คํŒจ", + description: "ํ•ด๋‹น ์ง€์ถœ ํ•ญ๋ชฉ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + variant: "destructive", + }); + return; + } + + // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์™€ ๋ณ€๊ฒฝ ๊ฐ์ง€ + const oldTransaction = transactions[existingIndex]; + const hasChanges = + JSON.stringify(oldTransaction) !== JSON.stringify(updatedTransaction); + + if (!hasChanges) { + logger.info(`[ํŠธ๋žœ์žญ์…˜] ๋ณ€๊ฒฝ์‚ฌํ•ญ ์—†์Œ: ${updatedTransaction.id}`); + return; + } + + // ๋ณ€๊ฒฝ ๋‚ด์šฉ ๋กœ๊น… + logger.info(`[ํŠธ๋žœ์žญ์…˜] ๋ณ€๊ฒฝ ๊ฐ์ง€: ์ œ๋ชฉ: ${oldTransaction.title} -> ${updatedTransaction.title} ๊ธˆ์•ก: ${oldTransaction.amount} -> ${updatedTransaction.amount} ์นดํ…Œ๊ณ ๋ฆฌ: ${oldTransaction.category} -> ${updatedTransaction.category} ๋‚ ์งœ: ${oldTransaction.date} -> ${updatedTransaction.date} `); - - // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ์—…๋ฐ์ดํŠธ - const updatedTransactions = transactions.map(transaction => - transaction.id === updatedTransaction.id ? updatedTransaction : transaction - ); - - saveTransactionsToStorage(updatedTransactions); - console.log(`[ํŠธ๋žœ์žญ์…˜] ๋กœ์ปฌ ์ €์žฅ์†Œ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ`); - - // ์ƒํƒœ ์—…๋ฐ์ดํŠธ - setTransactions(updatedTransactions); - - // Supabase ์—…๋ฐ์ดํŠธ ์‹œ๋„ (๋‚ ์งœ ํ˜•์‹ ๋ณ€ํ™˜ ์ถ”๊ฐ€) - if (user) { - // ISO ํ˜•์‹์œผ๋กœ ๋‚ ์งœ ๋ณ€ํ™˜ - const transactionWithIsoDate = { - ...updatedTransaction, - dateForSync: normalizeDate(updatedTransaction.date) - }; - - console.log(`[ํŠธ๋žœ์žญ์…˜] Supabase ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${updatedTransaction.id}`); - updateTransactionInSupabase(user, transactionWithIsoDate) - .then(() => { - console.log(`[ํŠธ๋žœ์žญ์…˜] Supabase ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต: ${updatedTransaction.id}`); - }) - .catch(err => { - console.error(`[ํŠธ๋žœ์žญ์…˜] Supabase ์—…๋ฐ์ดํŠธ ์‹คํŒจ:`, err); + + // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ์—…๋ฐ์ดํŠธ + const updatedTransactions = transactions.map((transaction) => + transaction.id === updatedTransaction.id + ? updatedTransaction + : transaction + ); + + saveTransactionsToStorage(updatedTransactions); + logger.info(`[ํŠธ๋žœ์žญ์…˜] ๋กœ์ปฌ ์ €์žฅ์†Œ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ`); + + // ์ƒํƒœ ์—…๋ฐ์ดํŠธ + setTransactions(updatedTransactions); + + // Supabase ์—…๋ฐ์ดํŠธ ์‹œ๋„ (๋‚ ์งœ ํ˜•์‹ ๋ณ€ํ™˜ ์ถ”๊ฐ€) + if (user) { + // ISO ํ˜•์‹์œผ๋กœ ๋‚ ์งœ ๋ณ€ํ™˜ + const transactionWithIsoDate = { + ...updatedTransaction, + dateForSync: normalizeDate(updatedTransaction.date), + }; + + logger.info( + `[ํŠธ๋žœ์žญ์…˜] Supabase ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${updatedTransaction.id}` + ); + updateTransactionInSupabase(user, transactionWithIsoDate) + .then(() => { + logger.info( + `[ํŠธ๋žœ์žญ์…˜] Supabase ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต: ${updatedTransaction.id}` + ); + }) + .catch((err) => { + logger.error(`[ํŠธ๋žœ์žญ์…˜] Supabase ์—…๋ฐ์ดํŠธ ์‹คํŒจ:`, err); + }); + } else { + logger.info(`[ํŠธ๋žœ์žญ์…˜] ๋กœ๊ทธ์ธ ์ƒํƒœ ์•„๋‹˜: Supabase ์—…๋ฐ์ดํŠธ ๊ฑด๋„ˆ๋œ€`); + } + + // ์ด๋ฒคํŠธ ๋ฐœ์ƒ + window.dispatchEvent(new Event("transactionUpdated")); + + // ์•ฝ๊ฐ„์˜ ์ง€์—ฐ์„ ๋‘๊ณ  ํ† ์ŠคํŠธ ํ‘œ์‹œ + setTimeout(() => { + toast({ + title: "์ง€์ถœ์ด ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค", + description: `${updatedTransaction.title} ํ•ญ๋ชฉ์ด ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, + duration: 3000, }); - } else { - console.log(`[ํŠธ๋žœ์žญ์…˜] ๋กœ๊ทธ์ธ ์ƒํƒœ ์•„๋‹˜: Supabase ์—…๋ฐ์ดํŠธ ๊ฑด๋„ˆ๋œ€`); - } - - // ์ด๋ฒคํŠธ ๋ฐœ์ƒ - window.dispatchEvent(new Event('transactionUpdated')); - - // ์•ฝ๊ฐ„์˜ ์ง€์—ฐ์„ ๋‘๊ณ  ํ† ์ŠคํŠธ ํ‘œ์‹œ - setTimeout(() => { + }, 100); + } catch (error) { + logger.error(`[ํŠธ๋žœ์žญ์…˜] ์—…๋ฐ์ดํŠธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:`, error); toast({ - title: "์ง€์ถœ์ด ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค", - description: `${updatedTransaction.title} ํ•ญ๋ชฉ์ด ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, - duration: 3000 + title: "์—…๋ฐ์ดํŠธ ์‹คํŒจ", + description: "์ง€์ถœ ์ˆ˜์ • ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + variant: "destructive", }); - }, 100); - } catch (error) { - console.error(`[ํŠธ๋žœ์žญ์…˜] ์—…๋ฐ์ดํŠธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:`, error); - toast({ - title: "์—…๋ฐ์ดํŠธ ์‹คํŒจ", - description: "์ง€์ถœ ์ˆ˜์ • ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" - }); - } - }, [transactions, setTransactions, user]); + } + }, + [transactions, setTransactions, user] + ); }; diff --git a/src/hooks/transactions/transactionOperations/useTransactionsOperations.ts b/src/hooks/transactions/transactionOperations/useTransactionsOperations.ts index be9a6bf..be5081f 100644 --- a/src/hooks/transactions/transactionOperations/useTransactionsOperations.ts +++ b/src/hooks/transactions/transactionOperations/useTransactionsOperations.ts @@ -1,32 +1,42 @@ -import { useCallback } from 'react'; -import { Transaction } from '@/contexts/budget/types'; -import { useBudget } from '@/contexts/budget/BudgetContext'; +import { useCallback } from "react"; +import { logger } from "@/utils/logger"; +import { Transaction } from "@/contexts/budget/types"; +import { useBudget } from "@/contexts/budget/BudgetContext"; export const useTransactionsOperations = (transactions: Transaction[]) => { - const { updateTransaction: budgetUpdateTransaction, deleteTransaction: budgetDeleteTransaction } = useBudget(); + const { + updateTransaction: budgetUpdateTransaction, + deleteTransaction: budgetDeleteTransaction, + } = useBudget(); // ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ - const updateTransaction = useCallback((updatedTransaction: Transaction): void => { - try { - budgetUpdateTransaction(updatedTransaction); - } catch (error) { - console.error('ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ์ค‘ ์˜ค๋ฅ˜:', error); - } - }, [budgetUpdateTransaction]); + const updateTransaction = useCallback( + (updatedTransaction: Transaction): void => { + try { + budgetUpdateTransaction(updatedTransaction); + } catch (error) { + logger.error("ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ ์ค‘ ์˜ค๋ฅ˜:", error); + } + }, + [budgetUpdateTransaction] + ); // ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ํ•จ์ˆ˜ - const deleteTransaction = useCallback(async (id: string): Promise => { - try { - budgetDeleteTransaction(id); - return true; - } catch (error) { - console.error('ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜:', error); - return false; - } - }, [budgetDeleteTransaction]); + const deleteTransaction = useCallback( + async (id: string): Promise => { + try { + budgetDeleteTransaction(id); + return true; + } catch (error) { + logger.error("ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜:", error); + return false; + } + }, + [budgetDeleteTransaction] + ); return { updateTransaction, - deleteTransaction + deleteTransaction, }; }; diff --git a/src/hooks/transactions/useAppwriteTransactions.ts b/src/hooks/transactions/useAppwriteTransactions.ts index f2bff18..12eaf6c 100644 --- a/src/hooks/transactions/useAppwriteTransactions.ts +++ b/src/hooks/transactions/useAppwriteTransactions.ts @@ -1,137 +1,157 @@ -import { useState, useEffect, useCallback, useRef } from 'react'; -import { Transaction } from '@/components/TransactionCard'; -import { - syncTransactionsWithAppwrite, - updateTransactionInAppwrite, +import { useState, useEffect, useCallback, useRef } from "react"; +import { appwriteLogger } from "@/utils/logger"; +import { Transaction } from "@/components/TransactionCard"; +import { + syncTransactionsWithAppwrite, + updateTransactionInAppwrite, deleteTransactionFromAppwrite, - debouncedDeleteTransaction -} from '@/utils/appwriteTransactionUtils'; -import { toast } from '@/hooks/useToast.wrapper'; -import { isSyncEnabled } from '@/utils/syncUtils'; + debouncedDeleteTransaction, +} from "@/utils/appwriteTransactionUtils"; +import { toast } from "@/hooks/useToast.wrapper"; +import { isSyncEnabled } from "@/utils/syncUtils"; /** * Appwrite ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ ํ›… * ํŠธ๋žœ์žญ์…˜ ๋™๊ธฐํ™”, ์ถ”๊ฐ€, ์ˆ˜์ •, ์‚ญ์ œ ๊ธฐ๋Šฅ ์ œ๊ณต */ -export const useAppwriteTransactions = (user: any, localTransactions: Transaction[]) => { +export const useAppwriteTransactions = ( + user: any, + localTransactions: Transaction[] +) => { // ํŠธ๋žœ์žญ์…˜ ์ƒํƒœ ๊ด€๋ฆฌ - const [transactions, setTransactions] = useState(localTransactions); + const [transactions, setTransactions] = + useState(localTransactions); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - + // ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์ƒํƒœ ์ถ”์  const isMountedRef = useRef(true); - + // ์ง„ํ–‰ ์ค‘์ธ ์ž‘์—… ์ถ”์  const pendingOperations = useRef>(new Set()); - + // ํŠธ๋žœ์žญ์…˜ ๋™๊ธฐํ™” const syncTransactions = useCallback(async () => { - if (!user || !isSyncEnabled()) return localTransactions; - + if (!user || !isSyncEnabled()) { + return localTransactions; + } + try { setLoading(true); setError(null); - + // UI ์Šค๋ ˆ๋“œ ์ฐจ๋‹จ ๋ฐฉ์ง€ - await new Promise(resolve => setTimeout(resolve, 0)); - - const syncedTransactions = await syncTransactionsWithAppwrite(user, localTransactions); - + await new Promise((resolve) => setTimeout(resolve, 0)); + + const syncedTransactions = await syncTransactionsWithAppwrite( + user, + localTransactions + ); + if (isMountedRef.current) { setTransactions(syncedTransactions); setLoading(false); } - + return syncedTransactions; } catch (err) { - console.error('ํŠธ๋žœ์žญ์…˜ ๋™๊ธฐํ™” ์˜ค๋ฅ˜:', err); - + appwriteLogger.error("ํŠธ๋žœ์žญ์…˜ ๋™๊ธฐํ™” ์˜ค๋ฅ˜:", err); + if (isMountedRef.current) { setError(err as Error); setLoading(false); } - + return localTransactions; } }, [user, localTransactions]); - + // ํŠธ๋žœ์žญ์…˜ ์ถ”๊ฐ€/์ˆ˜์ • - const saveTransaction = useCallback(async (transaction: Transaction) => { - if (!user || !isSyncEnabled()) return; - - try { - // ์ž‘์—… ์ถ”์  ์‹œ์ž‘ - pendingOperations.current.add(transaction.id); - - // UI ์Šค๋ ˆ๋“œ ์ฐจ๋‹จ ๋ฐฉ์ง€ - await new Promise(resolve => requestAnimationFrame(resolve)); - - await updateTransactionInAppwrite(user, transaction); - - if (!isMountedRef.current) return; - - // ๋กœ์ปฌ ์ƒํƒœ ์—…๋ฐ์ดํŠธ - setTransactions(prev => { - const index = prev.findIndex(t => t.id === transaction.id); - if (index >= 0) { - const updated = [...prev]; - updated[index] = transaction; - return updated; - } else { - return [...prev, transaction]; + const saveTransaction = useCallback( + async (transaction: Transaction) => { + if (!user || !isSyncEnabled()) { + return; + } + + try { + // ์ž‘์—… ์ถ”์  ์‹œ์ž‘ + pendingOperations.current.add(transaction.id); + + // UI ์Šค๋ ˆ๋“œ ์ฐจ๋‹จ ๋ฐฉ์ง€ + await new Promise((resolve) => requestAnimationFrame(resolve)); + + await updateTransactionInAppwrite(user, transaction); + + if (!isMountedRef.current) { + return; } - }); - - } catch (err) { - console.error('ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์˜ค๋ฅ˜:', err); - - if (isMountedRef.current) { - toast({ - title: '์ €์žฅ ์‹คํŒจ', - description: 'ํŠธ๋žœ์žญ์…˜์„ ์ €์žฅํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.', - variant: 'destructive' + + // ๋กœ์ปฌ ์ƒํƒœ ์—…๋ฐ์ดํŠธ + setTransactions((prev) => { + const index = prev.findIndex((t) => t.id === transaction.id); + if (index >= 0) { + const updated = [...prev]; + updated[index] = transaction; + return updated; + } else { + return [...prev, transaction]; + } }); + } catch (err) { + appwriteLogger.error("ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์˜ค๋ฅ˜:", err); + + if (isMountedRef.current) { + toast({ + title: "์ €์žฅ ์‹คํŒจ", + description: "ํŠธ๋žœ์žญ์…˜์„ ์ €์žฅํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + variant: "destructive", + }); + } + } finally { + // ์ž‘์—… ์ถ”์  ์ข…๋ฃŒ + pendingOperations.current.delete(transaction.id); } - } finally { - // ์ž‘์—… ์ถ”์  ์ข…๋ฃŒ - pendingOperations.current.delete(transaction.id); - } - }, [user]); - + }, + [user] + ); + // ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ - const removeTransaction = useCallback(async (transactionId: string) => { - if (!user || !isSyncEnabled()) return; - - try { - // ์ž‘์—… ์ถ”์  ์‹œ์ž‘ - pendingOperations.current.add(transactionId); - - // ๋กœ์ปฌ ์ƒํƒœ ๋จผ์ € ์—…๋ฐ์ดํŠธ (๋‚™๊ด€์  UI ์—…๋ฐ์ดํŠธ) - setTransactions(prev => prev.filter(t => t.id !== transactionId)); - - // ๋””๋ฐ”์šด์Šค๋œ ์‚ญ์ œ ์ž‘์—… ์‹คํ–‰ (์—ฌ๋Ÿฌ ๋ฒˆ ์—ฐ์† ํ˜ธ์ถœ ๋ฐฉ์ง€) - await debouncedDeleteTransaction(user, transactionId); - - } catch (err) { - console.error('ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์˜ค๋ฅ˜:', err); - - if (isMountedRef.current) { - toast({ - title: '์‚ญ์ œ ์‹คํŒจ', - description: 'ํŠธ๋žœ์žญ์…˜์„ ์‚ญ์ œํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.', - variant: 'destructive' - }); - - // ์‹คํŒจ ์‹œ ํŠธ๋žœ์žญ์…˜ ๋ณต์› (์„œ๋ฒ„์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ) - syncTransactions(); + const removeTransaction = useCallback( + async (transactionId: string) => { + if (!user || !isSyncEnabled()) { + return; } - } finally { - // ์ž‘์—… ์ถ”์  ์ข…๋ฃŒ - pendingOperations.current.delete(transactionId); - } - }, [user, syncTransactions]); - + + try { + // ์ž‘์—… ์ถ”์  ์‹œ์ž‘ + pendingOperations.current.add(transactionId); + + // ๋กœ์ปฌ ์ƒํƒœ ๋จผ์ € ์—…๋ฐ์ดํŠธ (๋‚™๊ด€์  UI ์—…๋ฐ์ดํŠธ) + setTransactions((prev) => prev.filter((t) => t.id !== transactionId)); + + // ๋””๋ฐ”์šด์Šค๋œ ์‚ญ์ œ ์ž‘์—… ์‹คํ–‰ (์—ฌ๋Ÿฌ ๋ฒˆ ์—ฐ์† ํ˜ธ์ถœ ๋ฐฉ์ง€) + await debouncedDeleteTransaction(user, transactionId); + } catch (err) { + appwriteLogger.error("ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์˜ค๋ฅ˜:", err); + + if (isMountedRef.current) { + toast({ + title: "์‚ญ์ œ ์‹คํŒจ", + description: "ํŠธ๋žœ์žญ์…˜์„ ์‚ญ์ œํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + variant: "destructive", + }); + + // ์‹คํŒจ ์‹œ ํŠธ๋žœ์žญ์…˜ ๋ณต์› (์„œ๋ฒ„์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ) + syncTransactions(); + } + } finally { + // ์ž‘์—… ์ถ”์  ์ข…๋ฃŒ + pendingOperations.current.delete(transactionId); + } + }, + [user, syncTransactions] + ); + // ์ดˆ๊ธฐ ๋™๊ธฐํ™” useEffect(() => { if (user && isSyncEnabled()) { @@ -140,14 +160,14 @@ export const useAppwriteTransactions = (user: any, localTransactions: Transactio setTransactions(localTransactions); } }, [user, localTransactions, syncTransactions]); - + // ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ ์ •๋ฆฌ useEffect(() => { return () => { isMountedRef.current = false; }; }, []); - + return { transactions, loading, @@ -155,7 +175,7 @@ export const useAppwriteTransactions = (user: any, localTransactions: Transactio syncTransactions, saveTransaction, removeTransaction, - hasPendingOperations: pendingOperations.current.size > 0 + hasPendingOperations: pendingOperations.current.size > 0, }; }; diff --git a/src/hooks/transactions/useDeleteAlert.ts b/src/hooks/transactions/useDeleteAlert.ts index 9768470..db2f841 100644 --- a/src/hooks/transactions/useDeleteAlert.ts +++ b/src/hooks/transactions/useDeleteAlert.ts @@ -1,16 +1,16 @@ +import { useState, useRef, useEffect } from "react"; -import { useState, useRef, useEffect } from 'react'; - +import { logger } from "@/utils/logger"; /** * ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์•Œ๋ฆผ ๊ด€๋ จ ๋กœ์ง์„ ๋‹ด๋‹นํ•˜๋Š” ์ปค์Šคํ…€ ํ›… */ export const useDeleteAlert = (onDelete: () => Promise | boolean) => { const [isOpen, setIsOpen] = useState(false); const [isDeleting, setIsDeleting] = useState(false); - + // ํƒ€์ž„์•„์›ƒ ์ฐธ์กฐ ์ €์žฅ (๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€์šฉ) const timeoutRef = useRef | null>(null); - + // ํด๋ฆฐ์—… ํ•จ์ˆ˜ - ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ const clearTimeouts = () => { if (timeoutRef.current) { @@ -18,32 +18,34 @@ export const useDeleteAlert = (onDelete: () => Promise | boolean) => { timeoutRef.current = null; } }; - + // ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ ๋ชจ๋“  ํƒ€์ž„์•„์›ƒ ์ œ๊ฑฐ useEffect(() => { return () => { clearTimeouts(); }; }, []); - + const handleDelete = async () => { // ์ด๋ฏธ ์‚ญ์ œ ์ค‘์ด๋ฉด ์ค‘๋ณต ์‹คํ–‰ ๋ฐฉ์ง€ - if (isDeleting) return; - + if (isDeleting) { + return; + } + try { // ์‚ญ์ œ ์ƒํƒœ ํ™œ์„ฑํ™” setIsDeleting(true); - + // ๋‹ค์ด์–ผ๋กœ๊ทธ ์ฆ‰์‹œ ๋‹ซ๊ธฐ (UI ์‘๋‹ต์„ฑ ๊ฐœ์„ ) setIsOpen(false); - + // UI ์• ๋‹ˆ๋ฉ”์ด์…˜ ์™„๋ฃŒ ํ›„ ์‚ญ์ œ ์‹คํ–‰ timeoutRef.current = setTimeout(async () => { try { // ์‚ญ์ œ ํ•จ์ˆ˜ ์‹คํ–‰ await onDelete(); } catch (error) { - console.error('์‚ญ์ œ ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜:', error); + logger.error("์‚ญ์ œ ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜:", error); } finally { // ๋ชจ๋“  ์ž‘์—… ์™„๋ฃŒ ํ›„ ์ƒํƒœ ์ดˆ๊ธฐํ™” (์•ฝ๊ฐ„ ์ง€์—ฐ) timeoutRef.current = setTimeout(() => { @@ -52,23 +54,25 @@ export const useDeleteAlert = (onDelete: () => Promise | boolean) => { } }, 150); } catch (error) { - console.error('์‚ญ์ œ ํ•ธ๋“ค๋Ÿฌ ์˜ค๋ฅ˜:', error); + logger.error("์‚ญ์ œ ํ•ธ๋“ค๋Ÿฌ ์˜ค๋ฅ˜:", error); setIsDeleting(false); setIsOpen(false); } }; - + // ๋‹ค์ด์–ผ๋กœ๊ทธ ์ƒํƒœ ๊ด€๋ฆฌ const handleOpenChange = (open: boolean) => { // ์‚ญ์ œ ์ค‘์—๋Š” ์ƒํƒœ ๋ณ€๊ฒฝ ๋ฐฉ์ง€ - if (isDeleting && !open) return; + if (isDeleting && !open) { + return; + } setIsOpen(open); }; - + return { isOpen, isDeleting, handleDelete, - handleOpenChange + handleOpenChange, }; }; diff --git a/src/hooks/transactions/useRecentTransactions.ts b/src/hooks/transactions/useRecentTransactions.ts index 5c6b85d..0824970 100644 --- a/src/hooks/transactions/useRecentTransactions.ts +++ b/src/hooks/transactions/useRecentTransactions.ts @@ -1,6 +1,6 @@ - -import { useCallback, useRef, useState } from 'react'; -import { toast } from '@/hooks/useToast.wrapper'; +import { useCallback, useRef, useState } from "react"; +import { logger } from "@/utils/logger"; +import { toast } from "@/hooks/useToast.wrapper"; /** * ์ตœ๊ทผ ๊ฑฐ๋ž˜๋‚ด์—ญ ๊ด€๋ จ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์ปค์Šคํ…€ ํ›… @@ -10,7 +10,7 @@ export const useRecentTransactions = ( deleteTransaction: (id: string) => void ) => { const [isDeleting, setIsDeleting] = useState(false); - + // ์‚ญ์ œ ์ค‘์ธ ID ์ถ”์  const deletingIdRef = useRef(null); @@ -21,101 +21,107 @@ export const useRecentTransactions = ( const lastDeleteTimeRef = useRef>({}); // ์™„์ „ํžˆ ์ƒˆ๋กœ์šด ์‚ญ์ œ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜ - const handleDeleteTransaction = useCallback(async (id: string): Promise => { - return new Promise(resolve => { - try { - // ์‚ญ์ œ ์ง„ํ–‰ ์ค‘์ธ์ง€ ํ™•์ธ - if (isDeleting || deletingIdRef.current === id) { - console.log('์ด๋ฏธ ์‚ญ์ œ ์ž‘์—…์ด ์ง„ํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค'); + const handleDeleteTransaction = useCallback( + async (id: string): Promise => { + return new Promise((resolve) => { + try { + // ์‚ญ์ œ ์ง„ํ–‰ ์ค‘์ธ์ง€ ํ™•์ธ + if (isDeleting || deletingIdRef.current === id) { + logger.info("์ด๋ฏธ ์‚ญ์ œ ์ž‘์—…์ด ์ง„ํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค"); + resolve(true); + return; + } + + // ๊ธ‰๋ฐœ์ง„ ๋ฐฉ์ง€ (300ms) + const now = Date.now(); + if ( + lastDeleteTimeRef.current[id] && + now - lastDeleteTimeRef.current[id] < 300 + ) { + logger.warn("์‚ญ์ œ ์š”์ฒญ์ด ๋„ˆ๋ฌด ๋น ๋ฆ…๋‹ˆ๋‹ค. ๋ฌด์‹œํ•ฉ๋‹ˆ๋‹ค."); + resolve(true); + return; + } + + // ํƒ€์ž„์Šคํƒฌํ”„ ์—…๋ฐ์ดํŠธ + lastDeleteTimeRef.current[id] = now; + + // ์‚ญ์ œ ์ƒํƒœ ์„ค์ • + setIsDeleting(true); + deletingIdRef.current = id; + + // ์•ˆ์ „์žฅ์น˜: ํƒ€์ž„์•„์›ƒ ์„ค์ • (์ตœ๋Œ€ 900ms) + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => { + logger.warn("์‚ญ์ œ ํƒ€์ž„์•„์›ƒ - ์ƒํƒœ ์ดˆ๊ธฐํ™”"); + setIsDeleting(false); + deletingIdRef.current = null; + resolve(true); // UI ์‘๋‹ต์„ฑ ์œ„ํ•ด ์„ฑ๊ณต ๊ฐ„์ฃผ + }, 900); + + // ๋น„๋™๊ธฐ ์ž‘์—…์„ ๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜์—ฌ UI ๋ธ”๋กœํ‚น ๋ฐฉ์ง€ + setTimeout(() => { + try { + // BudgetContext์˜ deleteTransaction ํ•จ์ˆ˜ ํ˜ธ์ถœ + deleteTransaction(id); + + // ์•ˆ์ „์žฅ์น˜ ํƒ€์ž„์•„์›ƒ ์ œ๊ฑฐ + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + + // ์ƒํƒœ ์ดˆ๊ธฐํ™” (์ง€์—ฐ ์ ์šฉ) + setTimeout(() => { + setIsDeleting(false); + deletingIdRef.current = null; + }, 100); + + // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ + toast({ + title: "ํ•ญ๋ชฉ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค", + description: "์ง€์ถœ ๋‚ด์—ญ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + duration: 1500, + }); + } catch (err) { + logger.error("์‚ญ์ œ ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜:", err); + + // ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ + toast({ + title: "์‚ญ์ œ ์‹คํŒจ", + description: "ํ•ญ๋ชฉ์„ ์‚ญ์ œํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + variant: "destructive", + duration: 1500, + }); + } + }, 0); + + // ์ฆ‰์‹œ ์„ฑ๊ณต ๋ฐ˜ํ™˜ (UI ์‘๋‹ต์„ฑ ํ–ฅ์ƒ) resolve(true); - return; - } + } catch (error) { + logger.error("์‚ญ์ œ ์ฒ˜๋ฆฌ ์ „์ฒด ์˜ค๋ฅ˜:", error); - // ๊ธ‰๋ฐœ์ง„ ๋ฐฉ์ง€ (300ms) - const now = Date.now(); - if (lastDeleteTimeRef.current[id] && now - lastDeleteTimeRef.current[id] < 300) { - console.warn('์‚ญ์ œ ์š”์ฒญ์ด ๋„ˆ๋ฌด ๋น ๋ฆ…๋‹ˆ๋‹ค. ๋ฌด์‹œํ•ฉ๋‹ˆ๋‹ค.'); - resolve(true); - return; - } - - // ํƒ€์ž„์Šคํƒฌํ”„ ์—…๋ฐ์ดํŠธ - lastDeleteTimeRef.current[id] = now; - - // ์‚ญ์ œ ์ƒํƒœ ์„ค์ • - setIsDeleting(true); - deletingIdRef.current = id; - - // ์•ˆ์ „์žฅ์น˜: ํƒ€์ž„์•„์›ƒ ์„ค์ • (์ตœ๋Œ€ 900ms) - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - timeoutRef.current = setTimeout(() => { - console.warn('์‚ญ์ œ ํƒ€์ž„์•„์›ƒ - ์ƒํƒœ ์ดˆ๊ธฐํ™”'); + // ํ•ญ์ƒ ์ƒํƒœ ์ •๋ฆฌ setIsDeleting(false); deletingIdRef.current = null; - resolve(true); // UI ์‘๋‹ต์„ฑ ์œ„ํ•ด ์„ฑ๊ณต ๊ฐ„์ฃผ - }, 900); - - // ๋น„๋™๊ธฐ ์ž‘์—…์„ ๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜์—ฌ UI ๋ธ”๋กœํ‚น ๋ฐฉ์ง€ - setTimeout(() => { - try { - // BudgetContext์˜ deleteTransaction ํ•จ์ˆ˜ ํ˜ธ์ถœ - deleteTransaction(id); - - // ์•ˆ์ „์žฅ์น˜ ํƒ€์ž„์•„์›ƒ ์ œ๊ฑฐ - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - - // ์ƒํƒœ ์ดˆ๊ธฐํ™” (์ง€์—ฐ ์ ์šฉ) - setTimeout(() => { - setIsDeleting(false); - deletingIdRef.current = null; - }, 100); - - // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ - toast({ - title: "ํ•ญ๋ชฉ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค", - description: "์ง€์ถœ ๋‚ด์—ญ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", - duration: 1500 - }); - } catch (err) { - console.error('์‚ญ์ œ ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜:', err); - - // ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ - toast({ - title: "์‚ญ์ œ ์‹คํŒจ", - description: "ํ•ญ๋ชฉ์„ ์‚ญ์ œํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive", - duration: 1500 - }); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; } - }, 0); - - // ์ฆ‰์‹œ ์„ฑ๊ณต ๋ฐ˜ํ™˜ (UI ์‘๋‹ต์„ฑ ํ–ฅ์ƒ) - resolve(true); - } catch (error) { - console.error('์‚ญ์ œ ์ฒ˜๋ฆฌ ์ „์ฒด ์˜ค๋ฅ˜:', error); - - // ํ•ญ์ƒ ์ƒํƒœ ์ •๋ฆฌ - setIsDeleting(false); - deletingIdRef.current = null; - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; + toast({ + title: "์˜ค๋ฅ˜ ๋ฐœ์ƒ", + description: "์ฒ˜๋ฆฌ ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + variant: "destructive", + duration: 1500, + }); + resolve(false); } - toast({ - title: "์˜ค๋ฅ˜ ๋ฐœ์ƒ", - description: "์ฒ˜๋ฆฌ ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive", - duration: 1500 - }); - resolve(false); - } - }); - }, [deleteTransaction, isDeleting]); + }); + }, + [deleteTransaction, isDeleting] + ); // ์ปดํฌ๋„ŒํŠธ ์–ธ๋งˆ์šดํŠธ ์‹œ ํƒ€์ž„์•„์›ƒ ์ •๋ฆฌ (๋ฆฌ์•กํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ฒ˜๋ฆฌํ•ด์•ผํ•จ) const cleanupTimeouts = useCallback(() => { @@ -128,6 +134,6 @@ export const useRecentTransactions = ( return { handleDeleteTransaction, isDeleting, - cleanupTimeouts + cleanupTimeouts, }; }; diff --git a/src/hooks/transactions/useRecentTransactionsDialog.ts b/src/hooks/transactions/useRecentTransactionsDialog.ts index e1518d6..23495fc 100644 --- a/src/hooks/transactions/useRecentTransactionsDialog.ts +++ b/src/hooks/transactions/useRecentTransactionsDialog.ts @@ -1,12 +1,12 @@ - -import { useState } from 'react'; -import { Transaction } from '@/contexts/budget/types'; +import { useState } from "react"; +import { Transaction } from "@/contexts/budget/types"; /** * ์ตœ๊ทผ ๊ฑฐ๋ž˜๋‚ด์—ญ์˜ ๋‹ค์ด์–ผ๋กœ๊ทธ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์ปค์Šคํ…€ ํ›… */ export const useRecentTransactionsDialog = () => { - const [selectedTransaction, setSelectedTransaction] = useState(null); + const [selectedTransaction, setSelectedTransaction] = + useState(null); const [isDialogOpen, setIsDialogOpen] = useState(false); const handleTransactionClick = (transaction: Transaction) => { @@ -27,6 +27,6 @@ export const useRecentTransactionsDialog = () => { isDialogOpen, handleTransactionClick, handleCloseDialog, - setIsDialogOpen + setIsDialogOpen, }; }; diff --git a/src/hooks/transactions/useTransactions.ts b/src/hooks/transactions/useTransactions.ts index 8aa7aee..b0b256a 100644 --- a/src/hooks/transactions/useTransactions.ts +++ b/src/hooks/transactions/useTransactions.ts @@ -1,5 +1,4 @@ - -import { useTransactionsCore } from './useTransactionsCore'; +import { useTransactionsCore } from "./useTransactionsCore"; /** * ๋ฉ”์ธ ํŠธ๋žœ์žญ์…˜ ํ›… diff --git a/src/hooks/transactions/useTransactionsCore.ts b/src/hooks/transactions/useTransactionsCore.ts index c809347..f7eec5b 100644 --- a/src/hooks/transactions/useTransactionsCore.ts +++ b/src/hooks/transactions/useTransactionsCore.ts @@ -1,10 +1,10 @@ - -import { useCallback } from 'react'; -import { useTransactionsState } from './useTransactionsState'; -import { useTransactionsFiltering } from './useTransactionsFiltering'; -import { useTransactionsLoader } from './useTransactionsLoader'; -import { useTransactionsOperations } from './transactionOperations/useTransactionsOperations'; -import { useTransactionsEvents } from './useTransactionsEvents'; +import { useCallback } from "react"; +import { logger } from "@/utils/logger"; +import { useTransactionsState } from "./useTransactionsState"; +import { useTransactionsFiltering } from "./useTransactionsFiltering"; +import { useTransactionsLoader } from "./useTransactionsLoader"; +import { useTransactionsOperations } from "./transactionOperations/useTransactionsOperations"; +import { useTransactionsEvents } from "./useTransactionsEvents"; /** * ํ•ต์‹ฌ ํŠธ๋žœ์žญ์…˜ ํ›… - ์„ฑ๋Šฅ ๋ฐ ์•ˆ์ •์„ฑ ์ตœ์ ํ™” ๋ฒ„์ „ @@ -12,7 +12,7 @@ import { useTransactionsEvents } from './useTransactionsEvents'; */ export const useTransactionsCore = () => { // ์ƒํƒœ ๊ด€๋ฆฌ - const { + const { transactions, setTransactions, filteredTransactions, @@ -28,42 +28,37 @@ export const useTransactionsCore = () => { totalBudget, setTotalBudget, refreshKey, - setRefreshKey + setRefreshKey, } = useTransactionsState(); // ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ const { loadTransactions } = useTransactionsLoader( - setTransactions, - setTotalBudget, - setIsLoading, + setTransactions, + setTotalBudget, + setIsLoading, setError ); // ํ•„ํ„ฐ๋ง - ์„ฑ๋Šฅ ๊ฐœ์„  ๋ฒ„์ „ - const { - handlePrevMonth, - handleNextMonth, - getTotalExpenses - } = useTransactionsFiltering({ - transactions, - selectedMonth, - setSelectedMonth, - searchQuery, - setFilteredTransactions - }); + const { handlePrevMonth, handleNextMonth, getTotalExpenses } = + useTransactionsFiltering({ + transactions, + selectedMonth, + setSelectedMonth, + searchQuery, + setFilteredTransactions, + }); // ํŠธ๋žœ์žญ์…˜ ์ž‘์—… - ๋‹จ์ˆœํ™”๋œ ๋ฒ„์ „ - const { - deleteTransaction - } = useTransactionsOperations(transactions); + const { deleteTransaction } = useTransactionsOperations(transactions); // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ - ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ ๋ฒ„์ „ useTransactionsEvents(loadTransactions, refreshKey); // ๋ฐ์ดํ„ฐ ๊ฐ•์ œ ์ƒˆ๋กœ๊ณ ์นจ - ์„ฑ๋Šฅ ์ตœ์ ํ™” const refreshTransactions = useCallback(() => { - console.log('[ํŠธ๋žœ์žญ์…˜ ์ฝ”์–ด] ๊ฐ•์ œ ์ƒˆ๋กœ๊ณ ์นจ'); - setRefreshKey(prev => prev + 1); + logger.info("[ํŠธ๋žœ์žญ์…˜ ์ฝ”์–ด] ๊ฐ•์ œ ์ƒˆ๋กœ๊ณ ์นจ"); + setRefreshKey((prev) => prev + 1); loadTransactions(); }, [loadTransactions, setRefreshKey]); @@ -71,26 +66,26 @@ export const useTransactionsCore = () => { // ๋ฐ์ดํ„ฐ transactions: filteredTransactions, allTransactions: transactions, - + // ์ƒํƒœ isLoading, error, totalBudget, - + // ํ•„ํ„ฐ๋ง selectedMonth, searchQuery, setSearchQuery, handlePrevMonth, handleNextMonth, - + // ์ž‘์—… deleteTransaction, - + // ํ•ฉ๊ณ„ totalExpenses: getTotalExpenses(filteredTransactions), - + // ์ƒˆ๋กœ๊ณ ์นจ - refreshTransactions + refreshTransactions, }; }; diff --git a/src/hooks/transactions/useTransactionsEvents.ts b/src/hooks/transactions/useTransactionsEvents.ts index 346cc27..cf917bd 100644 --- a/src/hooks/transactions/useTransactionsEvents.ts +++ b/src/hooks/transactions/useTransactionsEvents.ts @@ -1,6 +1,6 @@ +import { useEffect, useRef } from "react"; -import { useEffect, useRef } from 'react'; - +import { logger } from "@/utils/logger"; /** * ํŠธ๋žœ์žญ์…˜ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ํ›… - ์„ฑ๋Šฅ ๋ฐ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ ๊ฐœ์„  ๋ฒ„์ „ */ @@ -11,76 +11,86 @@ export const useTransactionsEvents = ( // ๋ฐ”์šด์‹ฑ ๋ฐฉ์ง€ ๋ฐ ์ด๋ฒคํŠธ ์ œ์–ด๋ฅผ ์œ„ํ•œ ์ฐธ์กฐ const isProcessingRef = useRef(false); const timeoutIdsRef = useRef([]); - + // ํƒ€์ž„์•„์›ƒ ํด๋ฆฌ์–ด ๋„์šฐ๋ฏธ ํ•จ์ˆ˜ const clearAllTimeouts = () => { - timeoutIdsRef.current.forEach(id => window.clearTimeout(id)); + timeoutIdsRef.current.forEach((id) => window.clearTimeout(id)); timeoutIdsRef.current = []; }; - + useEffect(() => { - console.log('[์ด๋ฒคํŠธ] ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์„ค์ •'); - + logger.info("[์ด๋ฒคํŠธ] ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์„ค์ •"); + // ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ - ๋ถ€ํ•˜ ์กฐ์ ˆ(throttle) ์ ์šฉ - const handleEvent = (name: string, delay: number = 200) => { + const handleEvent = (name: string, delay = 200) => { return (e?: any) => { // ์ด๋ฏธ ์ฒ˜๋ฆฌ ์ค‘์ธ ๊ฒฝ์šฐ ๊ฑด๋„ˆ๋œ€ - if (isProcessingRef.current) return; - - console.log(`[์ด๋ฒคํŠธ] ${name} ์ด๋ฒคํŠธ ๊ฐ์ง€:`, e?.detail?.type || ''); + if (isProcessingRef.current) { + return; + } + + logger.info(`[์ด๋ฒคํŠธ] ${name} ์ด๋ฒคํŠธ ๊ฐ์ง€:`, e?.detail?.type || ""); isProcessingRef.current = true; - + // ๋”œ๋ ˆ์ด ์ ์šฉ (์ด๋ฒคํŠธ ํญ์ฃผ ๋ฐฉ์ง€) const timeoutId = window.setTimeout(() => { loadTransactions(); isProcessingRef.current = false; - + // ํƒ€์ž„์•„์›ƒ ID ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ - timeoutIdsRef.current = timeoutIdsRef.current.filter(id => id !== timeoutId); + timeoutIdsRef.current = timeoutIdsRef.current.filter( + (id) => id !== timeoutId + ); }, delay); - + // ํƒ€์ž„์•„์›ƒ ID ๊ธฐ๋ก (๋‚˜์ค‘์— ์ •๋ฆฌํ•˜๊ธฐ ์œ„ํ•จ) timeoutIdsRef.current.push(timeoutId); }; }; - + // ๊ฐ ์ด๋ฒคํŠธ๋ณ„ ํ•ธ๋“ค๋Ÿฌ ์ƒ์„ฑ - const handleTransactionUpdate = handleEvent('ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ', 150); - const handleTransactionDelete = handleEvent('ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ', 200); - const handleTransactionChange = handleEvent('ํŠธ๋žœ์žญ์…˜ ๋ณ€๊ฒฝ', 150); + const handleTransactionUpdate = handleEvent("ํŠธ๋žœ์žญ์…˜ ์—…๋ฐ์ดํŠธ", 150); + const handleTransactionDelete = handleEvent("ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ", 200); + const handleTransactionChange = handleEvent("ํŠธ๋žœ์žญ์…˜ ๋ณ€๊ฒฝ", 150); const handleStorageEvent = (e: StorageEvent) => { - if (e.key === 'transactions' || e.key === null) { - handleEvent('์Šคํ† ๋ฆฌ์ง€', 150)(); + if (e.key === "transactions" || e.key === null) { + handleEvent("์Šคํ† ๋ฆฌ์ง€", 150)(); } }; - const handleFocus = handleEvent('ํฌ์ปค์Šค', 200); - + const handleFocus = handleEvent("ํฌ์ปค์Šค", 200); + // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก - window.addEventListener('transactionUpdated', handleTransactionUpdate); - window.addEventListener('transactionDeleted', handleTransactionDelete); - window.addEventListener('transactionChanged', handleTransactionChange as EventListener); - window.addEventListener('storage', handleStorageEvent); - window.addEventListener('focus', handleFocus); - + window.addEventListener("transactionUpdated", handleTransactionUpdate); + window.addEventListener("transactionDeleted", handleTransactionDelete); + window.addEventListener( + "transactionChanged", + handleTransactionChange as EventListener + ); + window.addEventListener("storage", handleStorageEvent); + window.addEventListener("focus", handleFocus); + // ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ if (!isProcessingRef.current) { loadTransactions(); } - + // ํด๋ฆฐ์—… ํ•จ์ˆ˜ return () => { - console.log('[์ด๋ฒคํŠธ] ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ •๋ฆฌ'); - + logger.info("[์ด๋ฒคํŠธ] ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ •๋ฆฌ"); + // ๋ชจ๋“  ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ œ๊ฑฐ - window.removeEventListener('transactionUpdated', handleTransactionUpdate); - window.removeEventListener('transactionDeleted', handleTransactionDelete); - window.removeEventListener('transactionChanged', handleTransactionChange as EventListener); - window.removeEventListener('storage', handleStorageEvent); - window.removeEventListener('focus', handleFocus); - + window.removeEventListener("transactionUpdated", handleTransactionUpdate); + window.removeEventListener("transactionDeleted", handleTransactionDelete); + window.removeEventListener( + "transactionChanged", + handleTransactionChange as EventListener + ); + window.removeEventListener("storage", handleStorageEvent); + window.removeEventListener("focus", handleFocus); + // ๋ชจ๋“  ์ง„ํ–‰ ์ค‘์ธ ํƒ€์ž„์•„์›ƒ ์ •๋ฆฌ clearAllTimeouts(); - + // ์ฒ˜๋ฆฌ ์ƒํƒœ ์ดˆ๊ธฐํ™” isProcessingRef.current = false; }; diff --git a/src/hooks/transactions/useTransactionsFiltering.ts b/src/hooks/transactions/useTransactionsFiltering.ts index 9fff4e8..b02cb17 100644 --- a/src/hooks/transactions/useTransactionsFiltering.ts +++ b/src/hooks/transactions/useTransactionsFiltering.ts @@ -1,5 +1,4 @@ - -import { useTransactionsFiltering } from './filterOperations'; +import { useTransactionsFiltering } from "./filterOperations"; // ๊ธฐ์กด ํ›…์„ ๊ทธ๋Œ€๋กœ ๋‚ด๋ณด๋‚ด๊ธฐ export { useTransactionsFiltering }; diff --git a/src/hooks/transactions/useTransactionsLoader.ts b/src/hooks/transactions/useTransactionsLoader.ts index 340d444..1c89e15 100644 --- a/src/hooks/transactions/useTransactionsLoader.ts +++ b/src/hooks/transactions/useTransactionsLoader.ts @@ -1,9 +1,7 @@ - -import { useCallback } from 'react'; -import { toast } from '@/hooks/useToast.wrapper'; -import { - loadTransactionsFromStorage -} from './storageUtils'; +import { useCallback } from "react"; +import { logger } from "@/utils/logger"; +import { toast } from "@/hooks/useToast.wrapper"; +import { loadTransactionsFromStorage } from "./storageUtils"; /** * ํŠธ๋žœ์žญ์…˜ ๋กœ๋”ฉ ๊ด€๋ จ ํ›… @@ -19,41 +17,45 @@ export const useTransactionsLoader = ( const loadTransactions = useCallback(() => { setIsLoading(true); setError(null); - + try { const localTransactions = loadTransactionsFromStorage(); setTransactions(localTransactions); - + // ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ์—์„œ ์ง์ ‘ ์›”๊ฐ„ ์˜ˆ์‚ฐ ๊ฐ’์„ ๊ฐ€์ ธ์˜ด try { - const budgetDataStr = localStorage.getItem('budgetData'); + const budgetDataStr = localStorage.getItem("budgetData"); if (budgetDataStr) { const budgetData = JSON.parse(budgetDataStr); // ์›”๊ฐ„ ์˜ˆ์‚ฐ ๊ฐ’๋งŒ ์‚ฌ์šฉ - if (budgetData && budgetData.monthly && typeof budgetData.monthly.targetAmount === 'number') { + if ( + budgetData && + budgetData.monthly && + typeof budgetData.monthly.targetAmount === "number" + ) { const monthlyBudget = budgetData.monthly.targetAmount; setTotalBudget(monthlyBudget); - console.log('์›”๊ฐ„ ์˜ˆ์‚ฐ ์„ค์ •:', monthlyBudget); + logger.info("์›”๊ฐ„ ์˜ˆ์‚ฐ ์„ค์ •:", monthlyBudget); } else { - console.log('์œ ํšจํ•œ ์›”๊ฐ„ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’ 0 ์‚ฌ์šฉ'); + logger.info("์œ ํšจํ•œ ์›”๊ฐ„ ์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’ 0 ์‚ฌ์šฉ"); setTotalBudget(0); } } else { - console.log('์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’ 0 ์‚ฌ์šฉ'); + logger.info("์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’ 0 ์‚ฌ์šฉ"); setTotalBudget(0); } } catch (budgetErr) { - console.error('์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์˜ค๋ฅ˜:', budgetErr); + logger.error("์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์˜ค๋ฅ˜:", budgetErr); setTotalBudget(0); } } catch (err) { - console.error('ํŠธ๋žœ์žญ์…˜ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜:', err); - setError('๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); + logger.error("ํŠธ๋žœ์žญ์…˜ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜:", err); + setError("๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); toast({ title: "๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ", description: "์ง€์ถœ ๋‚ด์—ญ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", variant: "destructive", - duration: 4000 + duration: 4000, }); } finally { // ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ์•ฝ๊ฐ„ ์ง€์—ฐ์‹œ์ผœ UI ์—…๋ฐ์ดํŠธ๊ฐ€ ์›ํ™œํ•˜๊ฒŒ ์ด๋ฃจ์–ด์ง€๋„๋ก ํ•จ @@ -62,6 +64,6 @@ export const useTransactionsLoader = ( }, [setTransactions, setTotalBudget, setIsLoading, setError]); return { - loadTransactions + loadTransactions, }; }; diff --git a/src/hooks/transactions/useTransactionsOperations.ts b/src/hooks/transactions/useTransactionsOperations.ts index c3015c7..3c75e29 100644 --- a/src/hooks/transactions/useTransactionsOperations.ts +++ b/src/hooks/transactions/useTransactionsOperations.ts @@ -1,18 +1,22 @@ - -import { useCallback } from 'react'; -import { Transaction } from '@/components/TransactionCard'; +import { useCallback } from "react"; +import { Transaction } from "@/components/TransactionCard"; export const useTransactionsOperations = ( transactions: Transaction[], setTransactions: React.Dispatch> ) => { - const updateTransaction = useCallback((updatedTransaction: Transaction) => { - setTransactions(prev => - prev.map(t => t.id === updatedTransaction.id ? updatedTransaction : t) - ); - }, [setTransactions]); + const updateTransaction = useCallback( + (updatedTransaction: Transaction) => { + setTransactions((prev) => + prev.map((t) => + t.id === updatedTransaction.id ? updatedTransaction : t + ) + ); + }, + [setTransactions] + ); return { - updateTransaction + updateTransaction, }; }; diff --git a/src/hooks/transactions/useTransactionsState.ts b/src/hooks/transactions/useTransactionsState.ts index 43faac5..a0fd0b7 100644 --- a/src/hooks/transactions/useTransactionsState.ts +++ b/src/hooks/transactions/useTransactionsState.ts @@ -1,7 +1,6 @@ - -import { useState } from 'react'; -import { Transaction } from '@/components/TransactionCard'; -import { getCurrentMonth } from './dateUtils'; +import { useState } from "react"; +import { Transaction } from "@/components/TransactionCard"; +import { getCurrentMonth } from "./dateUtils"; /** * ํŠธ๋žœ์žญ์…˜ ๊ด€๋ จ ์ƒํƒœ ๊ด€๋ฆฌ ํ›… @@ -10,19 +9,21 @@ import { getCurrentMonth } from './dateUtils'; export const useTransactionsState = () => { // ํŠธ๋žœ์žญ์…˜ ์ƒํƒœ const [transactions, setTransactions] = useState([]); - const [filteredTransactions, setFilteredTransactions] = useState([]); - + const [filteredTransactions, setFilteredTransactions] = useState< + Transaction[] + >([]); + // ํ•„ํ„ฐ๋ง ์ƒํƒœ const [selectedMonth, setSelectedMonth] = useState(getCurrentMonth()); - const [searchQuery, setSearchQuery] = useState(''); - + const [searchQuery, setSearchQuery] = useState(""); + // ๋กœ๋”ฉ ๋ฐ ์—๋Ÿฌ ์ƒํƒœ const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - + // ์˜ˆ์‚ฐ ์ƒํƒœ const [totalBudget, setTotalBudget] = useState(0); - + // ์ƒˆ๋กœ๊ณ ์นจ ํ‚ค const [refreshKey, setRefreshKey] = useState(0); @@ -32,25 +33,25 @@ export const useTransactionsState = () => { setTransactions, filteredTransactions, setFilteredTransactions, - + // ํ•„ํ„ฐ๋ง ์ƒํƒœ selectedMonth, setSelectedMonth, searchQuery, setSearchQuery, - + // ๋กœ๋”ฉ ๋ฐ ์—๋Ÿฌ ์ƒํƒœ isLoading, setIsLoading, error, setError, - + // ์˜ˆ์‚ฐ ์ƒํƒœ totalBudget, setTotalBudget, - + // ์ƒˆ๋กœ๊ณ ์นจ ํ‚ค refreshKey, - setRefreshKey + setRefreshKey, }; }; diff --git a/src/hooks/use-mobile.tsx b/src/hooks/use-mobile.tsx index 1fb88c6..a8afb17 100644 --- a/src/hooks/use-mobile.tsx +++ b/src/hooks/use-mobile.tsx @@ -1,36 +1,39 @@ +import * as React from "react"; -import * as React from "react" - -const MOBILE_BREAKPOINT = 768 +const MOBILE_BREAKPOINT = 768; export function useIsMobile() { const [isMobile, setIsMobile] = React.useState( - typeof window !== 'undefined' ? window.innerWidth < MOBILE_BREAKPOINT : false - ) + typeof window !== "undefined" + ? window.innerWidth < MOBILE_BREAKPOINT + : false + ); React.useEffect(() => { - if (typeof window === 'undefined') return; - + if (typeof window === "undefined") { + return; + } + const checkMobile = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + // ๋ชจ๋ฐ”์ผ ํ™”๋ฉด์ธ ๊ฒฝ์šฐ body์— ํด๋ž˜์Šค ์ถ”๊ฐ€ if (window.innerWidth < MOBILE_BREAKPOINT) { - document.body.classList.add('is-mobile'); + document.body.classList.add("is-mobile"); } else { - document.body.classList.remove('is-mobile'); + document.body.classList.remove("is-mobile"); } - } - - // ์ดˆ๊ธฐ ํ™•์ธ - checkMobile() - - // ๋ฆฌ์‚ฌ์ด์ฆˆ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€ - window.addEventListener('resize', checkMobile) - - // ํด๋ฆฐ์—… ํ•จ์ˆ˜ - return () => window.removeEventListener('resize', checkMobile) - }, []) + }; - return isMobile + // ์ดˆ๊ธฐ ํ™•์ธ + checkMobile(); + + // ๋ฆฌ์‚ฌ์ด์ฆˆ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€ + window.addEventListener("resize", checkMobile); + + // ํด๋ฆฐ์—… ํ•จ์ˆ˜ + return () => window.removeEventListener("resize", checkMobile); + }, []); + + return isMobile; } diff --git a/src/hooks/use-toast.ts b/src/hooks/use-toast.ts index ff8eb0c..1be8274 100644 --- a/src/hooks/use-toast.ts +++ b/src/hooks/use-toast.ts @@ -1,4 +1,3 @@ - // ์ด ํŒŒ์ผ์€ ๊ธฐ์กด import ๊ฒฝ๋กœ ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•œ ๋ฆฌ๋””๋ ‰์…˜์ž…๋‹ˆ๋‹ค export { useToast, toast, TOAST_LIMIT, TOAST_REMOVE_DELAY } from "./toast"; export type { ToasterToast } from "./toast/types"; diff --git a/src/hooks/useAppFocusEvents.tsx b/src/hooks/useAppFocusEvents.tsx index 4621509..e4a6f85 100644 --- a/src/hooks/useAppFocusEvents.tsx +++ b/src/hooks/useAppFocusEvents.tsx @@ -1,6 +1,6 @@ +import { useEffect } from "react"; -import { useEffect } from 'react'; - +import { logger } from "@/utils/logger"; /** * ์•ฑ์ด ํฌ์ปค์Šค๋ฅผ ์–ป์—ˆ์„ ๋•Œ๋‚˜ ๊ฐ€์‹œ์„ฑ์ด ๋ณ€๊ฒฝ๋  ๋•Œ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•˜๋Š” ์ปค์Šคํ…€ ํ›… */ @@ -8,68 +8,70 @@ export const useAppFocusEvents = () => { useEffect(() => { const handleFocus = () => { try { - console.log('์ฐฝ์ด ํฌ์ปค์Šค๋ฅผ ์–ป์Œ - ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ'); + logger.info("์ฐฝ์ด ํฌ์ปค์Šค๋ฅผ ์–ป์Œ - ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ"); // ์ด๋ฏธ ๋ฆฌํ”„๋ ˆ์‹œ ์ค‘์ธ์ง€ ํ™•์ธํ•˜๋Š” ํ”Œ๋ž˜๊ทธ - if (sessionStorage.getItem('isRefreshing') === 'true') { - console.log('์ด๋ฏธ ๋ฆฌํ”„๋ ˆ์‹œ ์ง„ํ–‰ ์ค‘, ์ค‘๋ณต ์‹คํ–‰ ๋ฐฉ์ง€'); + if (sessionStorage.getItem("isRefreshing") === "true") { + logger.info("์ด๋ฏธ ๋ฆฌํ”„๋ ˆ์‹œ ์ง„ํ–‰ ์ค‘, ์ค‘๋ณต ์‹คํ–‰ ๋ฐฉ์ง€"); return; } - + try { - sessionStorage.setItem('isRefreshing', 'true'); - + sessionStorage.setItem("isRefreshing", "true"); + // ์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œ์ผœ ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ - window.dispatchEvent(new Event('storage')); - window.dispatchEvent(new Event('transactionUpdated')); - window.dispatchEvent(new Event('budgetDataUpdated')); - window.dispatchEvent(new Event('categoryBudgetsUpdated')); - + window.dispatchEvent(new Event("storage")); + window.dispatchEvent(new Event("transactionUpdated")); + window.dispatchEvent(new Event("budgetDataUpdated")); + window.dispatchEvent(new Event("categoryBudgetsUpdated")); + // ๋ฆฌํ”„๋ ˆ์‹œ ์™„๋ฃŒ ํ‘œ์‹œ (300ms ํ›„์— ํ”Œ๋ž˜๊ทธ ํ•ด์ œ) setTimeout(() => { - sessionStorage.setItem('isRefreshing', 'false'); + sessionStorage.setItem("isRefreshing", "false"); }, 300); } catch (e) { - console.error('์ด๋ฒคํŠธ ๋ฐœ์ƒ ์˜ค๋ฅ˜:', e); - sessionStorage.setItem('isRefreshing', 'false'); + logger.error("์ด๋ฒคํŠธ ๋ฐœ์ƒ ์˜ค๋ฅ˜:", e); + sessionStorage.setItem("isRefreshing", "false"); } } catch (error) { - console.error('ํฌ์ปค์Šค ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("ํฌ์ปค์Šค ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜:", error); } }; // ํฌ์ปค์Šค ์ด๋ฒคํŠธ - window.addEventListener('focus', handleFocus); - + window.addEventListener("focus", handleFocus); + // ๊ฐ€์‹œ์„ฑ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ (๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ „๊ฒฝ์œผ๋กœ ๋Œ์•„์˜ฌ ๋•Œ) const handleVisibilityChange = () => { try { - if (document.visibilityState === 'visible') { - console.log('ํŽ˜์ด์ง€๊ฐ€ ๋‹ค์‹œ ๋ณด์ž„ - ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ'); + if (document.visibilityState === "visible") { + logger.info("ํŽ˜์ด์ง€๊ฐ€ ๋‹ค์‹œ ๋ณด์ž„ - ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ"); handleFocus(); } } catch (error) { - console.error('๊ฐ€์‹œ์„ฑ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("๊ฐ€์‹œ์„ฑ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜:", error); } }; - - document.addEventListener('visibilitychange', handleVisibilityChange); - + + document.addEventListener("visibilitychange", handleVisibilityChange); + // ์ •๊ธฐ์ ์ธ ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ (60์ดˆ๋งˆ๋‹ค๋กœ ๋ณ€๊ฒฝ - ๋„ˆ๋ฌด ๋นˆ๋ฒˆํ•œ ๋ฆฌํ”„๋ ˆ์‹œ ๋ฐฉ์ง€) const refreshInterval = setInterval(() => { try { - if (document.visibilityState === 'visible' && - sessionStorage.getItem('isRefreshing') !== 'true') { - console.log('์ •๊ธฐ ์ƒˆ๋กœ๊ณ ์นจ - ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ'); + if ( + document.visibilityState === "visible" && + sessionStorage.getItem("isRefreshing") !== "true" + ) { + logger.info("์ •๊ธฐ ์ƒˆ๋กœ๊ณ ์นจ - ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ"); handleFocus(); } } catch (error) { - console.error('์ •๊ธฐ ์ƒˆ๋กœ๊ณ ์นจ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("์ •๊ธฐ ์ƒˆ๋กœ๊ณ ์นจ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜:", error); } - }, 60000); // 60์ดˆ๋งˆ๋‹ค - + }, 60000); // 60์ดˆ๋งˆ๋‹ค + return () => { - window.removeEventListener('focus', handleFocus); - document.removeEventListener('visibilitychange', handleVisibilityChange); + window.removeEventListener("focus", handleFocus); + document.removeEventListener("visibilitychange", handleVisibilityChange); clearInterval(refreshInterval); }; }, []); diff --git a/src/hooks/useDataInitialization.ts b/src/hooks/useDataInitialization.ts index 7c5f54f..76ee955 100644 --- a/src/hooks/useDataInitialization.ts +++ b/src/hooks/useDataInitialization.ts @@ -1,138 +1,158 @@ - -import { useState, useEffect, useCallback } from 'react'; -import { resetAllData } from '@/contexts/budget/storage'; -import { resetAllStorageData } from '@/utils/storageUtils'; -import { clearCloudData } from '@/utils/sync/clearCloudData'; -import { useAuth } from '@/contexts/auth'; +import { useState, useEffect, useCallback } from "react"; +import { logger } from "@/utils/logger"; +import { resetAllData } from "@/contexts/budget/storage"; +import { resetAllStorageData } from "@/utils/storageUtils"; +import { clearCloudData } from "@/utils/sync/clearCloudData"; +import { useAuth } from "@/contexts/auth"; export const useDataInitialization = (resetBudgetData?: () => void) => { const [isInitialized, setIsInitialized] = useState(false); const { user } = useAuth(); - + // ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜ const initializeAllData = useCallback(async () => { try { // ์ค‘์š”: ์ด๋ฏธ ๋ฐฉ๋ฌธํ•œ ์ ์ด ์žˆ์œผ๋ฉด ์ ˆ๋Œ€ ์ดˆ๊ธฐํ™”ํ•˜์ง€ ์•Š์Œ - const hasVisitedBefore = localStorage.getItem('hasVisitedBefore') === 'true'; + const hasVisitedBefore = + localStorage.getItem("hasVisitedBefore") === "true"; if (hasVisitedBefore) { - console.log('์ด๋ฏธ ์•ฑ์„ ๋ฐฉ๋ฌธํ•œ ์ ์ด ์žˆ์œผ๋ฏ€๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'); + logger.info( + "์ด๋ฏธ ์•ฑ์„ ๋ฐฉ๋ฌธํ•œ ์ ์ด ์žˆ์œผ๋ฏ€๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค." + ); setIsInitialized(true); return true; } - - console.log('์ฒซ ๋ฐฉ๋ฌธ: ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์‹œ์ž‘'); - + + logger.info("์ฒซ ๋ฐฉ๋ฌธ: ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์‹œ์ž‘"); + // ํ˜„์žฌ dontShowWelcome ๊ฐ’ ๋ฐฑ์—… - const dontShowWelcomeValue = localStorage.getItem('dontShowWelcome'); - console.log('useDataInitialization - ์ดˆ๊ธฐํ™” ์ „ dontShowWelcome ๊ฐ’:', dontShowWelcomeValue); - + const dontShowWelcomeValue = localStorage.getItem("dontShowWelcome"); + logger.info( + "useDataInitialization - ์ดˆ๊ธฐํ™” ์ „ dontShowWelcome ๊ฐ’:", + dontShowWelcomeValue + ); + try { // ๋กœ๊ทธ์ธ ์ƒํƒœ๋ผ๋ฉด ํด๋ผ์šฐ๋“œ ๋ฐ์ดํ„ฐ๋„ ์ดˆ๊ธฐํ™” (์ฒซ ๋ฐฉ๋ฌธ ์‹œ) if (user) { - console.log('๋กœ๊ทธ์ธ ์ƒํƒœ: ํด๋ผ์šฐ๋“œ ๋ฐ์ดํ„ฐ๋„ ์ดˆ๊ธฐํ™” ์‹œ๋„'); + logger.info("๋กœ๊ทธ์ธ ์ƒํƒœ: ํด๋ผ์šฐ๋“œ ๋ฐ์ดํ„ฐ๋„ ์ดˆ๊ธฐํ™” ์‹œ๋„"); await clearCloudData(user.id); } - + // ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์™„์ „ํžˆ ์‚ญ์ œ ๋ฐ ์ดˆ๊ธฐํ™” (ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰) resetAllData(); resetAllStorageData(); - + // ์ปจํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ ๋ฆฌ์…‹ (ํ•„์š”ํ•œ ๊ฒฝ์šฐ) if (resetBudgetData) { resetBudgetData(); } - + // ์ดˆ๊ธฐํ™” ํ›„ dontShowWelcome ๊ฐ’ ํ™•์ธ - const afterResetValue = localStorage.getItem('dontShowWelcome'); - console.log('useDataInitialization - ์ดˆ๊ธฐํ™” ํ›„ dontShowWelcome ๊ฐ’:', afterResetValue); - + const afterResetValue = localStorage.getItem("dontShowWelcome"); + logger.info( + "useDataInitialization - ์ดˆ๊ธฐํ™” ํ›„ dontShowWelcome ๊ฐ’:", + afterResetValue + ); + // ๊ฐ’์ด ์œ ์ง€๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด ๋ณต์› if (dontShowWelcomeValue && afterResetValue !== dontShowWelcomeValue) { - console.log('useDataInitialization - dontShowWelcome ๊ฐ’ ๋ณต์›:', dontShowWelcomeValue); - localStorage.setItem('dontShowWelcome', dontShowWelcomeValue); + logger.info( + "useDataInitialization - dontShowWelcome ๊ฐ’ ๋ณต์›:", + dontShowWelcomeValue + ); + localStorage.setItem("dontShowWelcome", dontShowWelcomeValue); } - - console.log('๋ชจ๋“  ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ'); + + logger.info("๋ชจ๋“  ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ"); return true; } catch (error) { - console.error('๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error); + logger.error("๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:", error); return false; } } catch (error) { - console.error('initializeAllData ํ•จ์ˆ˜ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("initializeAllData ํ•จ์ˆ˜ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜:", error); setIsInitialized(true); // ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ์•ฑ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์ดˆ๊ธฐํ™” ์™„๋ฃŒ๋กœ ์„ค์ • return false; } }, [resetBudgetData, user]); - + // ๋ถ„์„ ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜ const clearAllAnalyticsData = useCallback(() => { try { // ๋ถ„์„ ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋งŒ ์„ ํƒ์ ์œผ๋กœ ์‚ญ์ œ (์ „์ฒด ๋ฐ์ดํ„ฐ๋Š” ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์Œ) const analyticsKeys = [ - 'analytics', 'monthlyTotals', 'chartData', - 'expenseHistory', 'budgetHistory', 'categorySpending', - 'monthlyData', 'expenseData', 'analyticData' + "analytics", + "monthlyTotals", + "chartData", + "expenseHistory", + "budgetHistory", + "categorySpending", + "monthlyData", + "expenseData", + "analyticData", ]; - - analyticsKeys.forEach(key => { + + analyticsKeys.forEach((key) => { localStorage.removeItem(key); }); - + // ์›”๊ฐ„, ์ฐจํŠธ, ๋ถ„์„ ๊ด€๋ จ ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋œ ํ•ญ๋ชฉ๋งŒ ์‚ญ์ œ for (let i = localStorage.length - 1; i >= 0; i--) { const key = localStorage.key(i); - if (key && ( - key.includes('month') || - key.includes('chart') || - key.includes('analytics') || - key.includes('expense') || - key.includes('budget') || - key.includes('total') - ) && - // ํ•ต์‹ฌ ๋ฐ์ดํ„ฐ๋Š” ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๋„๋ก ์ œ์™ธ - !key.includes('budgetData') && - !key.includes('transactions') && - !key.includes('categoryBudgets')) { - console.log(`๋ถ„์„ ๋ฐ์ดํ„ฐ ์‚ญ์ œ: ${key}`); + if ( + key && + (key.includes("month") || + key.includes("chart") || + key.includes("analytics") || + key.includes("expense") || + key.includes("budget") || + key.includes("total")) && + // ํ•ต์‹ฌ ๋ฐ์ดํ„ฐ๋Š” ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๋„๋ก ์ œ์™ธ + !key.includes("budgetData") && + !key.includes("transactions") && + !key.includes("categoryBudgets") + ) { + logger.info(`๋ถ„์„ ๋ฐ์ดํ„ฐ ์‚ญ์ œ: ${key}`); localStorage.removeItem(key); } } - + return true; } catch (error) { - console.error('๋ถ„์„ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("๋ถ„์„ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜:", error); return false; } }, []); - + // ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์‹คํ–‰ - ์ฒซ ๋ฐฉ๋ฌธ์‹œ์—๋งŒ useEffect(() => { try { if (!isInitialized) { // ์ด๋ฏธ ๋ฐฉ๋ฌธํ•œ ์ ์ด ์žˆ๋Š”์ง€ ์ฒดํฌ (์ด๋ฏธ ์žˆ๋‹ค๋ฉด ์ดˆ๊ธฐํ™”ํ•˜์ง€ ์•Š์Œ) - const hasVisitedBefore = localStorage.getItem('hasVisitedBefore') === 'true'; + const hasVisitedBefore = + localStorage.getItem("hasVisitedBefore") === "true"; if (hasVisitedBefore) { - console.log('์ด๋ฏธ ๋ฐฉ๋ฌธ ๊ธฐ๋ก์ด ์žˆ์–ด ์ดˆ๊ธฐํ™”๋ฅผ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.'); + logger.info("์ด๋ฏธ ๋ฐฉ๋ฌธ ๊ธฐ๋ก์ด ์žˆ์–ด ์ดˆ๊ธฐํ™”๋ฅผ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค."); setIsInitialized(true); } else { - initializeAllData().then(result => { + initializeAllData().then((result) => { setIsInitialized(result); }); } } - + // ์ฒซ ๋ฐฉ๋ฌธ ์—ฌ๋ถ€ ์ฒดํฌ์šฉ ํ‚ค ์„ค์ • (ํ•ญ์ƒ true๋กœ ์„ค์ •) - localStorage.setItem('hasVisitedBefore', 'true'); + localStorage.setItem("hasVisitedBefore", "true"); } catch (error) { - console.error('๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” useEffect ๋‚ด ์˜ค๋ฅ˜:', error); + logger.error("๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” useEffect ๋‚ด ์˜ค๋ฅ˜:", error); setIsInitialized(true); // ์˜ค๋ฅ˜ ๋ฐœ์ƒํ•ด๋„ ์•ฑ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์ดˆ๊ธฐํ™” ์™„๋ฃŒ๋กœ ์„ค์ • } }, [isInitialized, initializeAllData]); - + return { isInitialized, initializeAllData, - clearAllAnalyticsData + clearAllAnalyticsData, }; }; diff --git a/src/hooks/useDataReset.ts b/src/hooks/useDataReset.ts index 79dc8c2..b1f8b3f 100644 --- a/src/hooks/useDataReset.ts +++ b/src/hooks/useDataReset.ts @@ -1,11 +1,11 @@ - -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useToast } from '@/hooks/useToast.wrapper'; -import { resetAllStorageData } from '@/utils/storageUtils'; -import { clearCloudData } from '@/utils/sync/clearCloudData'; -import { useAuth } from '@/contexts/auth'; -import { isSyncEnabled, setSyncEnabled } from '@/utils/sync/syncSettings'; +import { useState } from "react"; +import { logger } from "@/utils/logger"; +import { useNavigate } from "react-router-dom"; +import { useToast } from "@/hooks/useToast.wrapper"; +import { resetAllStorageData } from "@/utils/storageUtils"; +import { clearCloudData } from "@/utils/sync/clearCloudData"; +import { useAuth } from "@/contexts/auth"; +import { isSyncEnabled, setSyncEnabled } from "@/utils/sync/syncSettings"; export interface DataResetResult { isCloudResetSuccess: boolean | null; @@ -13,7 +13,9 @@ export interface DataResetResult { export const useDataReset = () => { const [isResetting, setIsResetting] = useState(false); - const [isCloudResetSuccess, setIsCloudResetSuccess] = useState(null); + const [isCloudResetSuccess, setIsCloudResetSuccess] = useState< + boolean | null + >(null); const { toast } = useToast(); const navigate = useNavigate(); const { user } = useAuth(); @@ -21,122 +23,131 @@ export const useDataReset = () => { const resetAllData = async (): Promise => { try { setIsResetting(true); - console.log('๋ชจ๋“  ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์‹œ์ž‘'); - + logger.info("๋ชจ๋“  ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์‹œ์ž‘"); + // ํ˜„์žฌ ๋™๊ธฐํ™” ์„ค์ • ์ €์žฅ const syncWasEnabled = isSyncEnabled(); - console.log('๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ „ ๋™๊ธฐํ™” ์ƒํƒœ:', syncWasEnabled ? 'ํ™œ์„ฑํ™”' : '๋น„ํ™œ์„ฑํ™”'); - + logger.info( + "๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ „ ๋™๊ธฐํ™” ์ƒํƒœ:", + syncWasEnabled ? "ํ™œ์„ฑํ™”" : "๋น„ํ™œ์„ฑํ™”" + ); + // ์ค‘์š”: ํด๋ผ์šฐ๋“œ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ๋จผ์ € ์‹œ๋„ (๋กœ๊ทธ์ธ ์ƒํƒœ์ธ ๊ฒฝ์šฐ) let cloudResetSuccess = false; if (user) { - console.log('๋กœ๊ทธ์ธ ์ƒํƒœ: ํด๋ผ์šฐ๋“œ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์‹œ๋„'); - + logger.info("๋กœ๊ทธ์ธ ์ƒํƒœ: ํด๋ผ์šฐ๋“œ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์‹œ๋„"); + // ์—ฌ๋Ÿฌ ๋ฒˆ ์‹œ๋„ํ•˜์—ฌ ํด๋ผ์šฐ๋“œ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ํ™•์‹คํžˆ ํ•˜๊ธฐ for (let attempt = 1; attempt <= 3; attempt++) { - console.log(`ํด๋ผ์šฐ๋“œ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์‹œ๋„ ${attempt}/3`); + logger.info(`ํด๋ผ์šฐ๋“œ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์‹œ๋„ ${attempt}/3`); cloudResetSuccess = await clearCloudData(user.id); - + if (cloudResetSuccess) { - console.log('ํด๋ผ์šฐ๋“œ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์„ฑ๊ณต'); + logger.info("ํด๋ผ์šฐ๋“œ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์„ฑ๊ณต"); break; } else { - console.warn(`ํด๋ผ์šฐ๋“œ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์‹œ๋„ ${attempt} ์‹คํŒจ`); + logger.warn(`ํด๋ผ์šฐ๋“œ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์‹œ๋„ ${attempt} ์‹คํŒจ`); // ์ž ์‹œ ๋Œ€๊ธฐ ํ›„ ์žฌ์‹œ๋„ if (attempt < 3) { - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 500)); } } } - + setIsCloudResetSuccess(cloudResetSuccess); } else { - console.log('๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์Œ: ํด๋ผ์šฐ๋“œ ์ดˆ๊ธฐํ™” ๊ฑด๋„ˆ๋œ€'); + logger.info("๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์Œ: ํด๋ผ์šฐ๋“œ ์ดˆ๊ธฐํ™” ๊ฑด๋„ˆ๋œ€"); setIsCloudResetSuccess(null); } - + // ์ดˆ๊ธฐํ™” ์‹คํ–‰ ์ „์— ์‚ฌ์šฉ์ž ์„ค์ • ๋ฐฑ์—… - const dontShowWelcomeValue = localStorage.getItem('dontShowWelcome'); - const hasVisitedBefore = localStorage.getItem('hasVisitedBefore'); - + const dontShowWelcomeValue = localStorage.getItem("dontShowWelcome"); + const hasVisitedBefore = localStorage.getItem("hasVisitedBefore"); + // ๋กœ๊ทธ์ธ ๊ด€๋ จ ์„ค์ • ๋ฐฑ์—… (supabase ๊ด€๋ จ ๋ชจ๋“  ์„ค์ •) const authBackupItems: Record = {}; - + // ๋กœ๊ทธ์ธ ๊ด€๋ จ ํ•ญ๋ชฉ ์ˆ˜์ง‘ for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); - if (key && ( - key.includes('supabase') || - key.includes('auth') || - key.includes('sb-') || - key.includes('token') || - key.includes('user') || - key.includes('session') - )) { + if ( + key && + (key.includes("supabase") || + key.includes("auth") || + key.includes("sb-") || + key.includes("token") || + key.includes("user") || + key.includes("session")) + ) { authBackupItems[key] = localStorage.getItem(key); - console.log(`๋ฐฑ์—… ํ•ญ๋ชฉ: ${key}`); + logger.info(`๋ฐฑ์—… ํ•ญ๋ชฉ: ${key}`); } } - + // ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” (๊ฐœ์„ ๋œ ๋ฉ”์†Œ๋“œ ์‚ฌ์šฉ) resetAllStorageData(); - + // ์ถ”๊ฐ€ ์ดˆ๊ธฐํ™”๋ฅผ ์œ„ํ•ด ๋นˆ ๋ฐ์ดํ„ฐ ๋ช…์‹œ์  ์„ค์ • - localStorage.setItem('transactions', JSON.stringify([])); - localStorage.setItem('budgetData', JSON.stringify({ - daily: {targetAmount: 0, spentAmount: 0, remainingAmount: 0}, - weekly: {targetAmount: 0, spentAmount: 0, remainingAmount: 0}, - monthly: {targetAmount: 0, spentAmount: 0, remainingAmount: 0} - })); - localStorage.setItem('categoryBudgets', JSON.stringify({})); - + localStorage.setItem("transactions", JSON.stringify([])); + localStorage.setItem( + "budgetData", + JSON.stringify({ + daily: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 }, + weekly: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 }, + monthly: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 }, + }) + ); + localStorage.setItem("categoryBudgets", JSON.stringify({})); + // ์‚ฌ์šฉ์ž ์„ค์ • ๋ณต์› if (dontShowWelcomeValue) { - localStorage.setItem('dontShowWelcome', dontShowWelcomeValue); + localStorage.setItem("dontShowWelcome", dontShowWelcomeValue); } - + if (hasVisitedBefore) { - localStorage.setItem('hasVisitedBefore', hasVisitedBefore); + localStorage.setItem("hasVisitedBefore", hasVisitedBefore); } - + // ๋กœ๊ทธ์ธ ๊ด€๋ จ ์„ค์ • ๋ณต์› (๋กœ๊ทธ์ธ ํ™”๋ฉด์ด ๋‚˜ํƒ€๋‚˜์ง€ ์•Š๋„๋ก) Object.entries(authBackupItems).forEach(([key, value]) => { if (value) { localStorage.setItem(key, value); - console.log(`๋ณต์› ํ•ญ๋ชฉ: ${key}`); + logger.info(`๋ณต์› ํ•ญ๋ชฉ: ${key}`); } }); - + // ์ค‘์š”: ๋™๊ธฐํ™” ์„ค์ •์€ ์ดˆ๊ธฐํ™” ํ›„ ๊ฐ•์ œ๋กœ ๋น„ํ™œ์„ฑํ™” setSyncEnabled(false); - console.log('๋™๊ธฐํ™” ์„ค์ •์ด ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); - + logger.info("๋™๊ธฐํ™” ์„ค์ •์ด ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + // ๋งˆ์ง€๋ง‰ ๋™๊ธฐํ™” ์‹œ๊ฐ„์€ ์ดˆ๊ธฐํ™” - localStorage.removeItem('lastSync'); - + localStorage.removeItem("lastSync"); + // ์‚ญ์ œ ํ”Œ๋ž˜๊ทธ ์ดˆ๊ธฐํ™” (๊ฐ•์ œ๋กœ ์‚ญ์ œ ๋ชฉ๋ก ์ดˆ๊ธฐํ™”) - localStorage.removeItem('deletedTransactions'); - localStorage.removeItem('modifiedBudgets'); - + localStorage.removeItem("deletedTransactions"); + localStorage.removeItem("modifiedBudgets"); + // ์Šคํ† ๋ฆฌ์ง€ ์ด๋ฒคํŠธ ํŠธ๋ฆฌ๊ฑฐํ•˜์—ฌ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์— ๋ณ€๊ฒฝ ์•Œ๋ฆผ - window.dispatchEvent(new Event('transactionUpdated')); - window.dispatchEvent(new Event('budgetDataUpdated')); - window.dispatchEvent(new Event('categoryBudgetsUpdated')); - window.dispatchEvent(new StorageEvent('storage')); - window.dispatchEvent(new Event('auth-state-changed')); // ๋™๊ธฐํ™” ์ƒํƒœ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ์ถ”๊ฐ€ - + window.dispatchEvent(new Event("transactionUpdated")); + window.dispatchEvent(new Event("budgetDataUpdated")); + window.dispatchEvent(new Event("categoryBudgetsUpdated")); + window.dispatchEvent(new StorageEvent("storage")); + window.dispatchEvent(new Event("auth-state-changed")); // ๋™๊ธฐํ™” ์ƒํƒœ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ์ถ”๊ฐ€ + // ํด๋ผ์šฐ๋“œ ์ดˆ๊ธฐํ™” ์ƒํƒœ์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ if (user) { if (cloudResetSuccess) { toast({ title: "๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", - description: "๋กœ์ปฌ ๋ฐ ํด๋ผ์šฐ๋“œ์˜ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์—ˆ์œผ๋ฉฐ, ๋™๊ธฐํ™” ์„ค์ •์ด ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + description: + "๋กœ์ปฌ ๋ฐ ํด๋ผ์šฐ๋“œ์˜ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์—ˆ์œผ๋ฉฐ, ๋™๊ธฐํ™” ์„ค์ •์ด ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", }); } else { toast({ title: "๋กœ์ปฌ ๋ฐ์ดํ„ฐ๋งŒ ์ดˆ๊ธฐํ™”๋จ", - description: "๋กœ์ปฌ ๋ฐ์ดํ„ฐ๋Š” ์ดˆ๊ธฐํ™”๋˜์—ˆ์ง€๋งŒ, ํด๋ผ์šฐ๋“œ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋™๊ธฐํ™” ์„ค์ •์ด ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", - variant: "destructive" + description: + "๋กœ์ปฌ ๋ฐ์ดํ„ฐ๋Š” ์ดˆ๊ธฐํ™”๋˜์—ˆ์ง€๋งŒ, ํด๋ผ์šฐ๋“œ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋™๊ธฐํ™” ์„ค์ •์ด ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + variant: "destructive", }); } } else { @@ -145,17 +156,17 @@ export const useDataReset = () => { description: "๋ชจ๋“  ์˜ˆ์‚ฐ, ์ง€์ถœ ๋‚ด์—ญ, ์„ค์ •์ด ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", }); } - - console.log('๋ชจ๋“  ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ'); - + + logger.info("๋ชจ๋“  ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ"); + // ํŽ˜์ด์ง€ ๋ฆฌํ”„๋ ˆ์‹œ๋ฅผ ์œ„ํ•ด ์ž ์‹œ ํ›„์— ์ƒˆ๋กœ๊ณ ์นจ setTimeout(() => { window.location.reload(); }, 500); - + return { isCloudResetSuccess: cloudResetSuccess }; } catch (error) { - console.error('๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์‹คํŒจ:', error); + logger.error("๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์‹คํŒจ:", error); toast({ title: "๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์‹คํŒจ", description: "๋ฐ์ดํ„ฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", @@ -170,6 +181,6 @@ export const useDataReset = () => { return { isResetting, isCloudResetSuccess, - resetAllData + resetAllData, }; }; diff --git a/src/hooks/useInitialDataLoading.tsx b/src/hooks/useInitialDataLoading.tsx index ab54d05..b726956 100644 --- a/src/hooks/useInitialDataLoading.tsx +++ b/src/hooks/useInitialDataLoading.tsx @@ -1,57 +1,62 @@ +import { useEffect } from "react"; -import { useEffect } from 'react'; - +import { logger } from "@/utils/logger"; /** * ์•ฑ ์ฒซ ์‹คํ–‰ ์‹œ ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•˜๋Š” ์ปค์Šคํ…€ ํ›… */ export const useInitialDataLoading = () => { useEffect(() => { try { - console.log('Index ํŽ˜์ด์ง€ ๋งˆ์šดํŠธ, ๋ฐ์ดํ„ฐ ํ™•์ธ ์ค‘...'); - + logger.info("Index ํŽ˜์ด์ง€ ๋งˆ์šดํŠธ, ๋ฐ์ดํ„ฐ ํ™•์ธ ์ค‘..."); + // ํŽ˜์ด์ง€ ์ฒซ ๋งˆ์šดํŠธ ์‹œ์—๋งŒ ์‹คํ–‰๋˜๋Š” ๋กœ์ง - const isFirstMount = sessionStorage.getItem('initialDataLoaded') !== 'true'; - + const isFirstMount = + sessionStorage.getItem("initialDataLoaded") !== "true"; + if (isFirstMount) { try { // ๋ฐฑ์—…๋œ ๋ฐ์ดํ„ฐ ๋ณต๊ตฌ ํ™•์ธ (๋ฉ”์ธ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ๋งŒ) - if (!localStorage.getItem('budgetData')) { - const budgetBackup = localStorage.getItem('budgetData_backup'); + if (!localStorage.getItem("budgetData")) { + const budgetBackup = localStorage.getItem("budgetData_backup"); if (budgetBackup) { - console.log('์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋ฐฑ์—…์—์„œ ๋ณต๊ตฌ'); - localStorage.setItem('budgetData', budgetBackup); + logger.info("์˜ˆ์‚ฐ ๋ฐ์ดํ„ฐ ๋ฐฑ์—…์—์„œ ๋ณต๊ตฌ"); + localStorage.setItem("budgetData", budgetBackup); } } - - if (!localStorage.getItem('categoryBudgets')) { - const categoryBackup = localStorage.getItem('categoryBudgets_backup'); + + if (!localStorage.getItem("categoryBudgets")) { + const categoryBackup = localStorage.getItem( + "categoryBudgets_backup" + ); if (categoryBackup) { - console.log('์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋ฐฑ์—…์—์„œ ๋ณต๊ตฌ'); - localStorage.setItem('categoryBudgets', categoryBackup); + logger.info("์นดํ…Œ๊ณ ๋ฆฌ ์˜ˆ์‚ฐ ๋ฐฑ์—…์—์„œ ๋ณต๊ตฌ"); + localStorage.setItem("categoryBudgets", categoryBackup); } } - - if (!localStorage.getItem('transactions')) { - const transactionBackup = localStorage.getItem('transactions_backup'); + + if (!localStorage.getItem("transactions")) { + const transactionBackup = localStorage.getItem( + "transactions_backup" + ); if (transactionBackup) { - console.log('ํŠธ๋žœ์žญ์…˜ ๋ฐฑ์—…์—์„œ ๋ณต๊ตฌ'); - localStorage.setItem('transactions', transactionBackup); + logger.info("ํŠธ๋žœ์žญ์…˜ ๋ฐฑ์—…์—์„œ ๋ณต๊ตฌ"); + localStorage.setItem("transactions", transactionBackup); } } - + // ํ•œ ๋ฒˆ๋งŒ ์ด๋ฒคํŠธ ๋ฐœ์ƒ - window.dispatchEvent(new Event('transactionUpdated')); - window.dispatchEvent(new Event('budgetDataUpdated')); - window.dispatchEvent(new Event('categoryBudgetsUpdated')); - + window.dispatchEvent(new Event("transactionUpdated")); + window.dispatchEvent(new Event("budgetDataUpdated")); + window.dispatchEvent(new Event("categoryBudgetsUpdated")); + // ์ดˆ๊ธฐ ๋กœ๋“œ ์™„๋ฃŒ ํ‘œ์‹œ - sessionStorage.setItem('initialDataLoaded', 'true'); + sessionStorage.setItem("initialDataLoaded", "true"); } catch (error) { - console.error('๋ฐฑ์—… ๋ณต๊ตฌ ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("๋ฐฑ์—… ๋ณต๊ตฌ ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜:", error); } } } catch (error) { - console.error('Index ํŽ˜์ด์ง€ ์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("Index ํŽ˜์ด์ง€ ์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜:", error); } - }, []); // ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰ + }, []); // ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰ }; diff --git a/src/hooks/useLogin.ts b/src/hooks/useLogin.ts index 74f5417..147e4bb 100644 --- a/src/hooks/useLogin.ts +++ b/src/hooks/useLogin.ts @@ -1,5 +1,5 @@ - import { useState } from "react"; +import { logger } from "@/utils/logger"; import { useNavigate } from "react-router-dom"; import { useToast } from "@/hooks/useToast.wrapper"; import { useAuth } from "@/contexts/auth"; @@ -11,7 +11,7 @@ export function useLogin() { const [showPassword, setShowPassword] = useState(false); const [isLoading, setIsLoading] = useState(false); const [loginError, setLoginError] = useState(null); - + const navigate = useNavigate(); const { toast } = useToast(); const { signIn } = useAuth(); @@ -20,75 +20,77 @@ export function useLogin() { const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); setLoginError(null); - + if (!email || !password) { toast({ title: "์ž…๋ ฅ ์˜ค๋ฅ˜", description: "์ด๋ฉ”์ผ๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ชจ๋‘ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.", - variant: "destructive" + variant: "destructive", }); return; } - + setIsLoading(true); try { const { error, user } = await signIn(email, password); - + if (error) { - console.error("๋กœ๊ทธ์ธ ์‹คํŒจ:", error); - + logger.error("๋กœ๊ทธ์ธ ์‹คํŒจ:", error); + let errorMessage = "๋กœ๊ทธ์ธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."; - + if (error.message) { if (error.message.includes("Invalid login credentials")) { errorMessage = "์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."; } else if (error.message.includes("Email not confirmed")) { - errorMessage = "์ด๋ฉ”์ผ ์ธ์ฆ์ด ์™„๋ฃŒ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ด๋ฉ”์ผ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”."; + errorMessage = + "์ด๋ฉ”์ผ ์ธ์ฆ์ด ์™„๋ฃŒ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ด๋ฉ”์ผ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”."; } else { errorMessage = `์˜ค๋ฅ˜: ${error.message}`; } } - + setLoginError(errorMessage); - + toast({ title: "๋กœ๊ทธ์ธ ์‹คํŒจ", description: errorMessage, - variant: "destructive" + variant: "destructive", }); } else if (user) { // ๋กœ๊ทธ์ธ ์„ฑ๊ณต toast({ title: "๋กœ๊ทธ์ธ ์„ฑ๊ณต", description: "ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค! ๋Œ€์‹œ๋ณด๋“œ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.", - variant: "default" + variant: "default", }); - + await setupTables(); navigate("/"); } else { // user๊ฐ€ ์—†์ง€๋งŒ error๋„ ์—†๋Š” ๊ฒฝ์šฐ (๋“œ๋ฌธ ๊ฒฝ์šฐ) - console.warn("๋กœ๊ทธ์ธ ์„ฑ๊ณตํ–ˆ์ง€๋งŒ ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."); - + logger.warn("๋กœ๊ทธ์ธ ์„ฑ๊ณตํ–ˆ์ง€๋งŒ ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."); + toast({ title: "๋กœ๊ทธ์ธ ์ƒํƒœ ํ™•์ธ ์ค‘", - description: "๋กœ๊ทธ์ธ์€ ์„ฑ๊ณตํ–ˆ์ง€๋งŒ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ํ™•์ธํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•ด๋ณด์„ธ์š”.", - variant: "default" + description: + "๋กœ๊ทธ์ธ์€ ์„ฑ๊ณตํ–ˆ์ง€๋งŒ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ํ™•์ธํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•ด๋ณด์„ธ์š”.", + variant: "default", }); - + navigate("/"); } } catch (err: any) { - console.error("๋กœ๊ทธ์ธ ๊ณผ์ •์—์„œ ์˜ˆ์™ธ ๋ฐœ์ƒ:", err); - + logger.error("๋กœ๊ทธ์ธ ๊ณผ์ •์—์„œ ์˜ˆ์™ธ ๋ฐœ์ƒ:", err); + const errorMessage = err.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."; setLoginError(errorMessage); - + toast({ title: "๋กœ๊ทธ์ธ ์˜ค๋ฅ˜", description: errorMessage, - variant: "destructive" + variant: "destructive", }); } finally { setIsLoading(false); @@ -106,6 +108,6 @@ export function useLogin() { isSettingUpTables, loginError, setLoginError, - handleLogin + handleLogin, }; } diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts index 140a4f5..31f28e1 100644 --- a/src/hooks/useNotifications.ts +++ b/src/hooks/useNotifications.ts @@ -1,7 +1,7 @@ - -import { useState, useEffect } from 'react'; -import { v4 as uuidv4 } from 'uuid'; -import { Notification } from '@/components/notification/NotificationPopover'; +import { useState, useEffect } from "react"; +import { logger } from "@/utils/logger"; +import { v4 as uuidv4 } from "uuid"; +import { Notification } from "@/components/notification/NotificationPopover"; export const useNotifications = () => { const [notifications, setNotifications] = useState([]); @@ -9,18 +9,20 @@ export const useNotifications = () => { // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—์„œ ์•Œ๋ฆผ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ useEffect(() => { try { - const savedNotifications = localStorage.getItem('notifications'); + const savedNotifications = localStorage.getItem("notifications"); if (savedNotifications) { const parsedNotifications = JSON.parse(savedNotifications); // ์‹œ๊ฐ„ ๋ฌธ์ž์—ด์„ Date ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ - const formattedNotifications = parsedNotifications.map((notification: any) => ({ - ...notification, - timestamp: new Date(notification.timestamp) - })); + const formattedNotifications = parsedNotifications.map( + (notification: any) => ({ + ...notification, + timestamp: new Date(notification.timestamp), + }) + ); setNotifications(formattedNotifications); } } catch (error) { - console.error('์•Œ๋ฆผ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error); + logger.error("์•Œ๋ฆผ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:", error); } }, []); @@ -31,25 +33,31 @@ export const useNotifications = () => { title, message, timestamp: new Date(), - read: false + read: false, }; - setNotifications(prevNotifications => { + setNotifications((prevNotifications) => { const updatedNotifications = [newNotification, ...prevNotifications]; // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ์—…๋ฐ์ดํŠธ - localStorage.setItem('notifications', JSON.stringify(updatedNotifications)); + localStorage.setItem( + "notifications", + JSON.stringify(updatedNotifications) + ); return updatedNotifications; }); }; // ์•Œ๋ฆผ ์ฝ์Œ ํ‘œ์‹œ const markAsRead = (id: string) => { - setNotifications(prevNotifications => { - const updatedNotifications = prevNotifications.map(notification => + setNotifications((prevNotifications) => { + const updatedNotifications = prevNotifications.map((notification) => notification.id === id ? { ...notification, read: true } : notification ); // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ์—…๋ฐ์ดํŠธ - localStorage.setItem('notifications', JSON.stringify(updatedNotifications)); + localStorage.setItem( + "notifications", + JSON.stringify(updatedNotifications) + ); return updatedNotifications; }); }; @@ -57,14 +65,14 @@ export const useNotifications = () => { // ๋ชจ๋“  ์•Œ๋ฆผ ์‚ญ์ œ const clearAllNotifications = () => { setNotifications([]); - localStorage.removeItem('notifications'); + localStorage.removeItem("notifications"); }; return { notifications, addNotification, markAsRead, - clearAllNotifications + clearAllNotifications, }; }; diff --git a/src/hooks/useSyncSettings.ts b/src/hooks/useSyncSettings.ts index cbfedda..c92bb95 100644 --- a/src/hooks/useSyncSettings.ts +++ b/src/hooks/useSyncSettings.ts @@ -1,7 +1,7 @@ - -import { useState, useEffect } from 'react'; -import { useAuth } from '@/contexts/auth'; -import { useSyncToggle, useManualSync, useSyncStatus } from './sync'; +import { useState, useEffect } from "react"; +import { syncLogger } from "@/utils/logger"; +import { useAuth } from "@/contexts/auth"; +import { useSyncToggle, useManualSync, useSyncStatus } from "./sync"; /** * ๋™๊ธฐํ™” ์„ค์ • ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ์ปค์Šคํ…€ ํ›… @@ -11,14 +11,14 @@ export const useSyncSettings = () => { const { enabled, setEnabled, handleSyncToggle } = useSyncToggle(); const { syncing, handleManualSync } = useManualSync(user); const { lastSync, formatLastSyncTime } = useSyncStatus(); - + // ์ฝ˜์†”์— ์ƒํƒœ ๊ธฐ๋ก useEffect(() => { - console.log(`[๋™๊ธฐํ™”์„ค์ •] ์ƒํƒœ ๋ณ€๊ฒฝ: - - ํ™œ์„ฑํ™”: ${enabled ? '์˜ˆ' : '์•„๋‹ˆ์˜ค'} - - ์ง„ํ–‰์ค‘: ${syncing ? '์˜ˆ' : '์•„๋‹ˆ์˜ค'} - - ๋งˆ์ง€๋ง‰๋™๊ธฐํ™”: ${lastSync || '์—†์Œ'} - - ์‚ฌ์šฉ์ž: ${user ? '๋กœ๊ทธ์ธ๋จ' : '๋กœ๊ทธ์ธ์•ˆ๋จ'}`); + syncLogger.info(`[๋™๊ธฐํ™”์„ค์ •] ์ƒํƒœ ๋ณ€๊ฒฝ: + - ํ™œ์„ฑํ™”: ${enabled ? "์˜ˆ" : "์•„๋‹ˆ์˜ค"} + - ์ง„ํ–‰์ค‘: ${syncing ? "์˜ˆ" : "์•„๋‹ˆ์˜ค"} + - ๋งˆ์ง€๋ง‰๋™๊ธฐํ™”: ${lastSync || "์—†์Œ"} + - ์‚ฌ์šฉ์ž: ${user ? "๋กœ๊ทธ์ธ๋จ" : "๋กœ๊ทธ์ธ์•ˆ๋จ"}`); }, [enabled, syncing, lastSync, user]); return { diff --git a/src/hooks/useTableSetup.ts b/src/hooks/useTableSetup.ts index 78f1410..169d4b4 100644 --- a/src/hooks/useTableSetup.ts +++ b/src/hooks/useTableSetup.ts @@ -1,5 +1,5 @@ - import { useState } from "react"; +import { logger } from "@/utils/logger"; import { useToast } from "@/hooks/useToast.wrapper"; import { createRequiredTables } from "@/archive/lib/supabase/setup"; @@ -17,23 +17,24 @@ export function useTableSetup() { try { setIsSettingUpTables(true); const { success, message } = await createRequiredTables(); - + if (success) { - console.log("ํ…Œ์ด๋ธ” ์„ค์ • ์„ฑ๊ณต:", message); + logger.info("ํ…Œ์ด๋ธ” ์„ค์ • ์„ฑ๊ณต:", message); return true; } else { - console.warn("ํ…Œ์ด๋ธ” ์„ค์ • ๋ฌธ์ œ:", message); + logger.warn("ํ…Œ์ด๋ธ” ์„ค์ • ๋ฌธ์ œ:", message); // ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ฒฝ๊ณ  ํ‘œ์‹œ (์„ ํƒ์ ) toast({ title: "ํ…Œ์ด๋ธ” ์„ค์ • ๋ฌธ์ œ", - description: "์ผ๋ถ€ ํ…Œ์ด๋ธ” ์„ค์ •์— ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์ง€๋งŒ, ๊ธฐ๋ณธ ๊ธฐ๋Šฅ์€ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", - variant: "default" + description: + "์ผ๋ถ€ ํ…Œ์ด๋ธ” ์„ค์ •์— ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์ง€๋งŒ, ๊ธฐ๋ณธ ๊ธฐ๋Šฅ์€ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + variant: "default", }); // ํ…Œ์ด๋ธ” ์„ค์ • ์‹คํŒจํ•ด๋„ ๋กœ๊ทธ์ธ์€ ์ง„ํ–‰ return false; } } catch (setupError) { - console.error("ํ…Œ์ด๋ธ” ์„ค์ • ์ค‘ ์˜ค๋ฅ˜:", setupError); + logger.error("ํ…Œ์ด๋ธ” ์„ค์ • ์ค‘ ์˜ค๋ฅ˜:", setupError); return false; } finally { setIsSettingUpTables(false); @@ -42,6 +43,6 @@ export function useTableSetup() { return { isSettingUpTables, - setupTables + setupTables, }; } diff --git a/src/hooks/useToast.wrapper.ts b/src/hooks/useToast.wrapper.ts index 8b74b79..089c59b 100644 --- a/src/hooks/useToast.wrapper.ts +++ b/src/hooks/useToast.wrapper.ts @@ -1,25 +1,28 @@ - -import { useToast as useOriginalToast, toast as originalToast } from '@/hooks/toast'; -import type { ToasterToast } from '@/hooks/toast/types'; +import { + useToast as useOriginalToast, + toast as originalToast, +} from "@/hooks/toast"; +import { logger } from "@/utils/logger"; +import type { ToasterToast } from "@/hooks/toast/types"; /** * ํ† ์ŠคํŠธ ์ค‘๋ณต ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ์„ค์ •๊ฐ’ */ const TOAST_CONFIG = { - DEFAULT_DURATION: 3000, // ๊ธฐ๋ณธ ํ† ์ŠคํŠธ ํ‘œ์‹œ ์‹œ๊ฐ„ (ms) - DEBOUNCE_TIME: 1500, // ๋™์ผ ๋ฉ”์‹œ์ง€ ๋ฌด์‹œ ์‹œ๊ฐ„ (ms) - HISTORY_LIMIT: 10, // ํžˆ์Šคํ† ๋ฆฌ์— ์ €์žฅํ•  ์ตœ๋Œ€ ํ† ์ŠคํŠธ ์ˆ˜ - CLEANUP_INTERVAL: 30000, // ํžˆ์Šคํ† ๋ฆฌ ์ •๋ฆฌ ์ฃผ๊ธฐ (ms) - HISTORY_RETENTION: 10000 // ํžˆ์Šคํ† ๋ฆฌ ๋ณด๊ด€ ๊ธฐ๊ฐ„ (ms) + DEFAULT_DURATION: 3000, // ๊ธฐ๋ณธ ํ† ์ŠคํŠธ ํ‘œ์‹œ ์‹œ๊ฐ„ (ms) + DEBOUNCE_TIME: 1500, // ๋™์ผ ๋ฉ”์‹œ์ง€ ๋ฌด์‹œ ์‹œ๊ฐ„ (ms) + HISTORY_LIMIT: 10, // ํžˆ์Šคํ† ๋ฆฌ์— ์ €์žฅํ•  ์ตœ๋Œ€ ํ† ์ŠคํŠธ ์ˆ˜ + CLEANUP_INTERVAL: 30000, // ํžˆ์Šคํ† ๋ฆฌ ์ •๋ฆฌ ์ฃผ๊ธฐ (ms) + HISTORY_RETENTION: 10000, // ํžˆ์Šคํ† ๋ฆฌ ๋ณด๊ด€ ๊ธฐ๊ฐ„ (ms) }; /** * ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€ ํžˆ์Šคํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค */ interface ToastHistoryItem { - message: string; // ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ (title + description) - timestamp: number; // ์ƒ์„ฑ ์‹œ๊ฐ„ - variant?: string; // ํ† ์ŠคํŠธ ์ข…๋ฅ˜ (default/destructive) + message: string; // ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ (title + description) + timestamp: number; // ์ƒ์„ฑ ์‹œ๊ฐ„ + variant?: string; // ํ† ์ŠคํŠธ ์ข…๋ฅ˜ (default/destructive) } /** @@ -32,7 +35,7 @@ class ToastHistoryManager { constructor() { // ์ฃผ๊ธฐ์ ์œผ๋กœ ์˜ค๋ž˜๋œ ํžˆ์Šคํ† ๋ฆฌ ์ •๋ฆฌ this.cleanupInterval = setInterval( - () => this.cleanup(), + () => this.cleanup(), TOAST_CONFIG.CLEANUP_INTERVAL ); } @@ -44,7 +47,7 @@ class ToastHistoryManager { this.history.push({ message, timestamp: Date.now(), - variant + variant, }); // ํžˆ์Šคํ† ๋ฆฌ ํฌ๊ธฐ ์ œํ•œ @@ -59,7 +62,7 @@ class ToastHistoryManager { cleanup(): void { const now = Date.now(); this.history = this.history.filter( - item => (now - item.timestamp) < TOAST_CONFIG.HISTORY_RETENTION + (item) => now - item.timestamp < TOAST_CONFIG.HISTORY_RETENTION ); } @@ -68,11 +71,12 @@ class ToastHistoryManager { */ isDuplicate(message: string, variant?: string): boolean { const now = Date.now(); - - return this.history.some(item => - item.message === message && - item.variant === variant && - (now - item.timestamp) < TOAST_CONFIG.DEBOUNCE_TIME + + return this.history.some( + (item) => + item.message === message && + item.variant === variant && + now - item.timestamp < TOAST_CONFIG.DEBOUNCE_TIME ); } @@ -82,7 +86,7 @@ class ToastHistoryManager { clear(): void { this.history = []; } - + /** * ์ •๋ฆฌ ํƒ€์ด๋จธ ํ•ด์ œ (๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€) */ @@ -98,10 +102,9 @@ const toastHistory = new ToastHistoryManager(); * ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ ์ถ”์ถœ (title + description) */ const extractMessage = (params: Omit): string => { - return [ - params.title?.toString() || '', - params.description?.toString() || '' - ].filter(Boolean).join(' - '); + return [params.title?.toString() || "", params.description?.toString() || ""] + .filter(Boolean) + .join(" - "); }; /** @@ -109,22 +112,22 @@ const extractMessage = (params: Omit): string => { */ const debouncedToast = (params: Omit) => { const message = extractMessage(params); - + // ๋นˆ ๋ฉ”์‹œ์ง€ ๋ฌด์‹œ if (!message.trim()) { - console.warn('๋นˆ ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€๊ฐ€ ๋ฌด์‹œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค'); + logger.warn("๋นˆ ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€๊ฐ€ ๋ฌด์‹œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); return; } - + // ์ค‘๋ณต ๊ฒ€์‚ฌ if (toastHistory.isDuplicate(message, params.variant)) { - console.log('์ค‘๋ณต ํ† ์ŠคํŠธ ๊ฐ์ง€๋กœ ๋ฌด์‹œ๋จ:', message); + logger.info("์ค‘๋ณต ํ† ์ŠคํŠธ ๊ฐ์ง€๋กœ ๋ฌด์‹œ๋จ:", message); return; } - + // ํžˆ์Šคํ† ๋ฆฌ์— ์ถ”๊ฐ€ toastHistory.add(message, params.variant); - + // ์‹ค์ œ ํ† ์ŠคํŠธ ํ‘œ์‹œ originalToast({ ...params, diff --git a/src/hooks/useTransactions.ts b/src/hooks/useTransactions.ts index ec5ebfd..279283b 100644 --- a/src/hooks/useTransactions.ts +++ b/src/hooks/useTransactions.ts @@ -1,12 +1,11 @@ - // ์ด ํŒŒ์ผ์€ ์ด์ œ ๋‹จ์ˆœํžˆ ์ƒˆ๋กœ์šด ๊ตฌ์กฐ์˜ ํŒŒ์ผ๋“ค์„ ์žฌ๋‚ด๋ณด๋‚ด๊ธฐ๋งŒ ํ•ฉ๋‹ˆ๋‹ค -export { +export { useTransactions, MONTHS_KR, getCurrentMonth, - getPrevMonth, + getPrevMonth, getNextMonth, filterTransactionsByMonth, filterTransactionsByQuery, - calculateTotalExpenses -} from './transactions'; + calculateTotalExpenses, +} from "./transactions"; diff --git a/src/hooks/useWelcomeDialog.ts b/src/hooks/useWelcomeDialog.ts index d183226..7e63c23 100644 --- a/src/hooks/useWelcomeDialog.ts +++ b/src/hooks/useWelcomeDialog.ts @@ -1,60 +1,69 @@ +import { useState, useEffect, useCallback } from "react"; -import { useState, useEffect, useCallback } from 'react'; - +import { logger } from "@/utils/logger"; export const useWelcomeDialog = () => { const [showWelcome, setShowWelcome] = useState(false); - + // ํ™˜์˜ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ‘œ์‹œ ์—ฌ๋ถ€ ํ™•์ธ const checkWelcomeDialogState = useCallback(() => { // ํ˜„์žฌ ์„ธ์…˜์—์„œ ์ด๋ฏธ ํ™˜์˜ ๋ฉ”์‹œ์ง€๋ฅผ ๋‹ซ์•˜๋Š”์ง€ ํ™•์ธ - const sessionClosed = sessionStorage.getItem('welcomeClosedThisSession') === 'true'; - + const sessionClosed = + sessionStorage.getItem("welcomeClosedThisSession") === "true"; + if (sessionClosed) { - console.log('useWelcomeDialog - ์ด๋ฒˆ ์„ธ์…˜์—์„œ ์ด๋ฏธ ํ™˜์˜ ๋ฉ”์‹œ์ง€๋ฅผ ๋‹ซ์•˜์Šต๋‹ˆ๋‹ค'); + logger.info( + "useWelcomeDialog - ์ด๋ฒˆ ์„ธ์…˜์—์„œ ์ด๋ฏธ ํ™˜์˜ ๋ฉ”์‹œ์ง€๋ฅผ ๋‹ซ์•˜์Šต๋‹ˆ๋‹ค" + ); setShowWelcome(false); return; } - - const dontShowWelcome = localStorage.getItem('dontShowWelcome'); - console.log('useWelcomeDialog - dontShowWelcome ๊ฐ’:', dontShowWelcome); - + + const dontShowWelcome = localStorage.getItem("dontShowWelcome"); + logger.info("useWelcomeDialog - dontShowWelcome ๊ฐ’:", dontShowWelcome); + // ๋ช…์‹œ์ ์œผ๋กœ 'true' ๋ฌธ์ž์—ด์ธ ๊ฒฝ์šฐ์—๋งŒ ์ˆจ๊น€ ์ฒ˜๋ฆฌ - if (dontShowWelcome === 'true') { - console.log('useWelcomeDialog - ํ™˜์˜ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ (์ €์žฅ๋œ ์„ค์ •)'); + if (dontShowWelcome === "true") { + logger.info("useWelcomeDialog - ํ™˜์˜ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ (์ €์žฅ๋œ ์„ค์ •)"); setShowWelcome(false); } else { - console.log('useWelcomeDialog - ํ™˜์˜ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œํ•จ'); + logger.info("useWelcomeDialog - ํ™˜์˜ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œํ•จ"); setShowWelcome(true); } }, []); - + // ํ™˜์˜ ๋‹ค์ด์–ผ๋กœ๊ทธ ๋‹ซ๊ธฐ ํ•ธ๋“ค๋Ÿฌ const handleCloseWelcome = useCallback((dontShowAgain: boolean) => { setShowWelcome(false); - + // ์ด๋ฒˆ ์„ธ์…˜์—์„œ ๋‹ซ์•˜์Œ์„ ๊ธฐ๋ก - sessionStorage.setItem('welcomeClosedThisSession', 'true'); - + sessionStorage.setItem("welcomeClosedThisSession", "true"); + // ์‚ฌ์šฉ์ž๊ฐ€ ๋” ์ด์ƒ ๋ณด์ง€ ์•Š๊ธฐ๋ฅผ ์„ ํƒํ•œ ๊ฒฝ์šฐ if (dontShowAgain) { - localStorage.setItem('dontShowWelcome', 'true'); - sessionStorage.setItem('dontShowWelcome', 'true'); - console.log('useWelcomeDialog - ํ™˜์˜ ํŒ์—… ๋” ์ด์ƒ ํ‘œ์‹œํ•˜์ง€ ์•Š๊ธฐ ์„ค์ •๋จ:', dontShowAgain); - + localStorage.setItem("dontShowWelcome", "true"); + sessionStorage.setItem("dontShowWelcome", "true"); + logger.info( + "useWelcomeDialog - ํ™˜์˜ ํŒ์—… ๋” ์ด์ƒ ํ‘œ์‹œํ•˜์ง€ ์•Š๊ธฐ ์„ค์ •๋จ:", + dontShowAgain + ); + // ์„ค์ • ํ™•์ธ - const savedValue = localStorage.getItem('dontShowWelcome'); - console.log('useWelcomeDialog - ์„ค์ • ํ›„ dontShowWelcome ์ €์žฅ๊ฐ’:', savedValue); + const savedValue = localStorage.getItem("dontShowWelcome"); + logger.info( + "useWelcomeDialog - ์„ค์ • ํ›„ dontShowWelcome ์ €์žฅ๊ฐ’:", + savedValue + ); } else { // ์ฒดํฌํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ๋ช…์‹œ์ ์œผ๋กœ false ์ €์žฅ - localStorage.setItem('dontShowWelcome', 'false'); - sessionStorage.setItem('dontShowWelcome', 'false'); + localStorage.setItem("dontShowWelcome", "false"); + sessionStorage.setItem("dontShowWelcome", "false"); } }, []); - + return { showWelcome, setShowWelcome, checkWelcomeDialogState, - handleCloseWelcome + handleCloseWelcome, }; }; diff --git a/src/hooks/useWelcomeNotification.tsx b/src/hooks/useWelcomeNotification.tsx index f3d0fe6..cb0bc5a 100644 --- a/src/hooks/useWelcomeNotification.tsx +++ b/src/hooks/useWelcomeNotification.tsx @@ -1,7 +1,7 @@ - -import { useEffect } from 'react'; -import { useAuth } from '@/contexts/auth'; -import useNotifications from '@/hooks/useNotifications'; +import { useEffect } from "react"; +import { logger } from "@/utils/logger"; +import { useAuth } from "@/contexts/auth"; +import useNotifications from "@/hooks/useNotifications"; /** * ์•ฑ ์ดˆ๊ธฐํ™” ํ›„ ํ™˜์˜ ๋ฉ”์‹œ์ง€ ์•Œ๋ฆผ์„ ํ‘œ์‹œํ•˜๋Š” ์ปค์Šคํ…€ ํ›… @@ -13,23 +13,25 @@ export const useWelcomeNotification = (isInitialized: boolean) => { useEffect(() => { try { // ํ™˜์˜ ๋ฉ”์‹œ์ง€๊ฐ€ ์ด๋ฏธ ํ‘œ์‹œ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ํ‚ค - const welcomeNotificationSent = sessionStorage.getItem('welcomeNotificationSent'); - + const welcomeNotificationSent = sessionStorage.getItem( + "welcomeNotificationSent" + ); + if (isInitialized && user && !welcomeNotificationSent) { // ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ์‹œ ์•Œ๋ฆผ ์˜ˆ์‹œ (ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰) const timeoutId = setTimeout(() => { addNotification( - 'ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!', - '์ ค๋ฆฌ์˜ ์ ์žํƒˆ์ถœ์— ์˜ค์‹  ๊ฒƒ์„ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ์‚ฐ์„ ์„ค์ •ํ•˜๊ณ  ์ง€์ถœ์„ ๊ธฐ๋กํ•ด๋ณด์„ธ์š”.' + "ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!", + "์ ค๋ฆฌ์˜ ์ ์žํƒˆ์ถœ์— ์˜ค์‹  ๊ฒƒ์„ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ์‚ฐ์„ ์„ค์ •ํ•˜๊ณ  ์ง€์ถœ์„ ๊ธฐ๋กํ•ด๋ณด์„ธ์š”." ); // ์„ธ์…˜ ์Šคํ† ๋ฆฌ์ง€์— ํ™˜์˜ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ ์—ฌ๋ถ€ ์ €์žฅ - sessionStorage.setItem('welcomeNotificationSent', 'true'); + sessionStorage.setItem("welcomeNotificationSent", "true"); }, 2000); - + return () => clearTimeout(timeoutId); } } catch (error) { - console.error('ํ™˜์˜ ๋ฉ”์‹œ์ง€ ์•Œ๋ฆผ ํ‘œ์‹œ ์ค‘ ์˜ค๋ฅ˜:', error); + logger.error("ํ™˜์˜ ๋ฉ”์‹œ์ง€ ์•Œ๋ฆผ ํ‘œ์‹œ ์ค‘ ์˜ค๋ฅ˜:", error); } }, [isInitialized, user, addNotification]); }; diff --git a/src/index.css b/src/index.css index cd9d8c3..7a598cd 100644 --- a/src/index.css +++ b/src/index.css @@ -1,4 +1,4 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"); @tailwind base; @tailwind components; @@ -106,8 +106,10 @@ body { @apply bg-neuro-background text-foreground font-inter antialiased; } - - html, body, #root { + + html, + body, + #root { @apply h-full overflow-x-hidden; } } @@ -116,129 +118,132 @@ .neuro-flat { @apply bg-neuro-background shadow-neuro-flat rounded-xl; } - + .neuro-pressed { @apply bg-neuro-background shadow-neuro-pressed rounded-xl; } - + .neuro-convex { @apply bg-neuro-background shadow-neuro-convex rounded-xl; } - + .neuro-text { @apply font-medium tracking-wide; } - + .page-transition-enter { @apply animate-fade-in; } - + .glass-effect { @apply bg-white/10 backdrop-blur-lg border border-white/20 rounded-xl; } - + .neuro-button { @apply neuro-flat px-4 py-3 text-neuro-accent font-medium transition-all duration-200 hover:shadow-neuro-convex hover:text-neuro-accent-light active:shadow-neuro-pressed; } - + .neuro-card { @apply neuro-flat p-6 transition-all duration-300 hover:shadow-neuro-convex; } - + .neuro-input { @apply neuro-pressed px-4 py-3 w-full focus:outline-none focus:ring-2 focus:ring-neuro-accent/30; } - + /* ์•ˆ์ „ ์˜์—ญ ๊ด€๋ จ ํด๋ž˜์Šค - ๊ฐ•ํ™”๋œ ๋ฒ„์ „ */ .safe-area-container { width: 100%; position: relative; box-sizing: border-box; } - + .ios-safe-area { /* iOS ์ „์šฉ ์•ˆ์ „ ์˜์—ญ ์ฒ˜๋ฆฌ */ position: relative; box-sizing: border-box; } - + .has-safe-area-top { padding-top: max(1rem, var(--safe-area-top)) !important; } - + .has-safe-area-bottom { padding-bottom: max(1rem, var(--safe-area-bottom)) !important; } - + .ios-header { padding-top: max(1rem, var(--safe-area-top)) !important; } - + /* ๋ชจ๋ฐ”์ผ ํ™”๋ฉด์—์„œ์˜ ์ถ”๊ฐ€ ์Šคํƒ€์ผ */ @media (max-width: 768px) { .neuro-card { @apply w-full; } - + #root { @apply p-0; } - + /* ๋ชจ๋ฐ”์ผ์—์„œ ํŒ์—…๊ณผ ๋‹ค์ด์–ผ๋กœ๊ทธ ์Šคํƒ€์ผ ๋ณด์ • */ [role="dialog"] { @apply rounded-xl overflow-hidden; } - + /* ๋‹ค์ด์–ผ๋กœ๊ทธ ๋‚ด์šฉ์— ์ ์šฉ๋˜๋Š” ์Šคํƒ€์ผ */ - .DialogContent, - .PopoverContent, - .AlertDialogContent, - .DrawerContent, + .DialogContent, + .PopoverContent, + .AlertDialogContent, + .DrawerContent, .SheetContent { @apply rounded-xl overflow-hidden; } - + /* iOS ๊ณ ์œ  ๋…ธ์น˜/๋‹ค์ด๋‚˜๋ฏน ์•„์ผ๋žœ๋“œ ์˜์—ญ ๊ณ ๋ คํ•œ ์ถ”๊ฐ€ ์Šคํƒ€์ผ */ .ios-safe-area-screen { - padding: var(--safe-area-top) var(--safe-area-right) var(--safe-area-bottom) var(--safe-area-left) !important; + padding: var(--safe-area-top) var(--safe-area-right) + var(--safe-area-bottom) var(--safe-area-left) !important; } } - + /* ๋ฐ์Šคํฌํƒ‘ ํ™”๋ฉด์—์„œ์˜ ์ถ”๊ฐ€ ์Šคํƒ€์ผ */ @media (min-width: 769px) { #root { @apply px-0; } - + .desktop-container { @apply max-w-md mx-auto; } - + .desktop-card { @apply w-full mx-auto; } } - + /* ์ถ”๊ฐ€ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ •์˜ */ @keyframes pulse-subtle { - 0%, 100% { + 0%, + 100% { transform: scale(1); } 50% { transform: scale(1.05); } } - + @keyframes bounce-gentle { - 0%, 100% { + 0%, + 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } } - + @keyframes spin-slow { from { transform: rotate(0deg); @@ -247,19 +252,19 @@ transform: rotate(360deg); } } - + .animate-pulse-subtle { animation: pulse-subtle 2s infinite ease-in-out; } - + .animate-bounce-gentle { animation: bounce-gentle 1.5s infinite ease-in-out; } - + .animate-spin-slow { animation: spin-slow 6s linear infinite; } - + @keyframes fade-in { from { opacity: 0; @@ -279,16 +284,16 @@ .ios-safe-area-top { padding-top: var(--safe-area-top) !important; } - + .ios-safe-area-bottom { padding-bottom: var(--safe-area-bottom) !important; } - + /* iOS์—์„œ ๋…ธ์น˜/๋‹ค์ด๋‚˜๋ฏน ์•„์ผ๋žœ๋“œ ์˜์—ญ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์ถ”๊ฐ€ ํด๋ž˜์Šค */ .ios-notch-padding { padding-top: max(1.5rem, var(--safe-area-top)) !important; } - + /* iOS์—์„œ ํ•˜๋‹จ ํ™ˆ ์ธ๋””์ผ€์ดํ„ฐ ์˜์—ญ์„ ์œ„ํ•œ ์ถ”๊ฐ€ ํด๋ž˜์Šค */ .ios-bottom-padding { padding-bottom: max(1.5rem, var(--safe-area-bottom)) !important; @@ -296,5 +301,5 @@ } .font-inter { - font-family: 'Inter', sans-serif; + font-family: "Inter", sans-serif; } diff --git a/src/ios-icon-guide.md b/src/ios-icon-guide.md index eb8dd3f..b8b8419 100644 --- a/src/ios-icon-guide.md +++ b/src/ios-icon-guide.md @@ -1,4 +1,3 @@ - # iOS ์•ฑ ์•„์ด์ฝ˜ ์„ค์ • ๊ฐ€์ด๋“œ iOS ์•ฑ ์•„์ด์ฝ˜์„ ์„ค์ •ํ•˜๋ ค๋ฉด ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ๋”ฐ๋ฅด์„ธ์š”: @@ -11,12 +10,14 @@ iOS ์•ฑ ์•„์ด์ฝ˜์„ ์„ค์ •ํ•˜๋ ค๋ฉด ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ๋”ฐ๋ฅด์„ธ์š”: ## iOS ์•„์ด์ฝ˜ ํ•„์ˆ˜ ํฌ๊ธฐ: ### iPhone + - iPhone ์•ฑ: 60pt (@2x: 120x120px, @3x: 180x180px) - iPhone Spotlight: 40pt (@2x: 80x80px, @3x: 120x120px) - iPhone Settings: 29pt (@2x: 58x58px, @3x: 87x87px) - iPhone Notification: 20pt (@2x: 40x40px, @3x: 60x60px) ### iPad + - iPad ์•ฑ: 76pt (@1x: 76x76px, @2x: 152x152px) - iPad Pro ์•ฑ: 83.5pt (@2x: 167x167px) - iPad Spotlight: 40pt (@1x: 40x40px, @2x: 80x80px) @@ -24,6 +25,7 @@ iOS ์•ฑ ์•„์ด์ฝ˜์„ ์„ค์ •ํ•˜๋ ค๋ฉด ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ๋”ฐ๋ฅด์„ธ์š”: - iPad Notification: 20pt (@1x: 20x20px, @2x: 40x40px) ### App Store + - App Store: 1024x1024px (1x) ## Info.plist ์„ค์ • ํ™•์ธ: @@ -37,5 +39,6 @@ iOS ์•ฑ ์•„์ด์ฝ˜์„ ์„ค์ •ํ•˜๋ ค๋ฉด ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ๋”ฐ๋ฅด์„ธ์š”: - NSFaceIDUsageDescription (Face ID ์‚ฌ์šฉ ์‹œ) ## ์ด๋ฏธ์ง€ ๋ณ€ํ™˜ ๋„๊ตฌ: + - App Icon Generator: https://appicon.co/ - MakeAppIcon: https://makeappicon.com/ diff --git a/src/lib/appwrite/client.ts b/src/lib/appwrite/client.ts index 8c4d8b7..68fd8af 100644 --- a/src/lib/appwrite/client.ts +++ b/src/lib/appwrite/client.ts @@ -1,12 +1,13 @@ /** * Appwrite ํด๋ผ์ด์–ธํŠธ ์„ค์ • - * + * * ์ด ํŒŒ์ผ์€ Appwrite ์„œ๋น„์Šค์™€์˜ ์—ฐ๊ฒฐ์„ ์„ค์ •ํ•˜๊ณ  ํ•„์š”ํ•œ ์„œ๋น„์Šค ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. * ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ๋ผ์ธ์— ๋”ฐ๋ผ UI ์Šค๋ ˆ๋“œ๋ฅผ ์ฐจ๋‹จํ•˜์ง€ ์•Š๋„๋ก ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ์™€ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. */ -import { Client, Account, Databases, Storage, Avatars } from 'appwrite'; -import { config, validateConfig } from './config'; +import { Client, Account, Databases, Storage, Avatars } from "appwrite"; +import { appwriteLogger } from "@/utils/logger"; +import { config, validateConfig } from "./config"; // ์„œ๋น„์Šค ํƒ€์ž… ์ •์˜ export interface AppwriteServices { @@ -36,63 +37,66 @@ const initializeAppwriteClient = () => { try { // ์„ค์ • ์œ ํšจ์„ฑ ๊ฒ€์ฆ validateConfig(); - - console.log(`Appwrite ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ ์ค‘: ${config.endpoint}`); - console.log(`ํ”„๋กœ์ ํŠธ ID: ${config.projectId}`); - + + appwriteLogger.info(`Appwrite ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ ์ค‘: ${config.endpoint}`); + appwriteLogger.info(`ํ”„๋กœ์ ํŠธ ID: ${config.projectId}`); + // Appwrite ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ appwriteClient = new Client(); - - appwriteClient - .setEndpoint(config.endpoint) - .setProject(config.projectId); - + + appwriteClient.setEndpoint(config.endpoint).setProject(config.projectId); + // API ํ‚ค๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ์„ค์ • if (config.apiKey) { - console.log('API ํ‚ค ์„ค์ • ์ค‘...'); + appwriteLogger.info("API ํ‚ค ์„ค์ • ์ค‘..."); // ์ตœ์‹  Appwrite SDK์—์„œ๋Š” JWT ํ† ํฐ์„ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์„ธ์…˜ ๊ธฐ๋ฐ˜ ์ธ์ฆ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. // ์„œ๋ฒ„์—์„œ๋Š” API ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜์ง€๋งŒ ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. // ํด๋ผ์ด์–ธํŠธ์—์„œ API ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ๋ณด์•ˆ ์œ„ํ—˜์ด ์žˆ์–ด ๊ถŒ์žฅ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. - console.log('API ํ‚ค๊ฐ€ ์„ค์ •๋˜์—ˆ์ง€๋งŒ ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'); + appwriteLogger.info( + "API ํ‚ค๊ฐ€ ์„ค์ •๋˜์—ˆ์ง€๋งŒ ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค." + ); } else { - console.warn('API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ผ๋ถ€ ๊ธฐ๋Šฅ์ด ์ œํ•œ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.'); + appwriteLogger.warn( + "API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ผ๋ถ€ ๊ธฐ๋Šฅ์ด ์ œํ•œ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." + ); } - + // ์„œ๋น„์Šค ์ดˆ๊ธฐํ™” accountService = new Account(appwriteClient); databasesService = new Databases(appwriteClient); storageService = new Storage(appwriteClient); avatarsService = new Avatars(appwriteClient); - + isInitialized = true; - console.log('Appwrite ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); - + appwriteLogger.info("Appwrite ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + // ์„ธ์…˜ ํ™•์ธ (์„ ํƒ์ ) queueMicrotask(async () => { try { await accountService.get(); - console.log('Appwrite ์„ธ์…˜ ํ™•์ธ ์„ฑ๊ณต'); + appwriteLogger.info("Appwrite ์„ธ์…˜ ํ™•์ธ ์„ฑ๊ณต"); } catch (sessionError) { // ๋กœ๊ทธ์ธ๋˜์ง€ ์•Š์€ ์ƒํƒœ๋Š” ์ •์ƒ์ ์ธ ๊ฒฝ์šฐ์ด๋ฏ€๋กœ ์˜ค๋ฅ˜๋กœ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์Œ - console.log('Appwrite ์„ธ์…˜ ์—†์Œ (์ •์ƒ)'); + appwriteLogger.info("Appwrite ์„ธ์…˜ ์—†์Œ (์ •์ƒ)"); } }); - } catch (error) { - console.error('Appwrite ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ ์˜ค๋ฅ˜:', error); + appwriteLogger.error("Appwrite ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ ์˜ค๋ฅ˜:", error); initializationError = error as Error; - + // ๋”๋ฏธ ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ (์•ฑ์ด ์™„์ „ํžˆ ์‹คํŒจํ•˜์ง€ ์•Š๋„๋ก) appwriteClient = new Client(); accountService = new Account(appwriteClient); databasesService = new Databases(appwriteClient); storageService = new Storage(appwriteClient); avatarsService = new Avatars(appwriteClient); - + // ์‚ฌ์šฉ์ž์—๊ฒŒ ์˜ค๋ฅ˜ ์•Œ๋ฆผ (๊ฐœ๋ฐœ ๋ชจ๋“œ์—์„œ๋งŒ) if (import.meta.env.DEV) { queueMicrotask(() => { - console.warn('Appwrite ์„œ๋ฒ„ ์—ฐ๊ฒฐ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ํ™˜๊ฒฝ ์„ค์ •์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.'); + appwriteLogger.warn( + "Appwrite ์„œ๋ฒ„ ์—ฐ๊ฒฐ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ํ™˜๊ฒฝ ์„ค์ •์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”." + ); }); } } @@ -115,7 +119,7 @@ export const avatars = avatarsService; export const getInitializationStatus = () => { return { isInitialized, - error: initializationError + error: initializationError, }; }; @@ -124,7 +128,7 @@ export const getInitializationStatus = () => { * ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์žฌ์‹œ๋„ํ•˜๊ธฐ ์œ„ํ•œ ํ•จ์ˆ˜ */ export const reinitializeAppwriteClient = () => { - console.log('Appwrite ํด๋ผ์ด์–ธํŠธ ์žฌ์ดˆ๊ธฐํ™” ์‹œ๋„'); + appwriteLogger.info("Appwrite ํด๋ผ์ด์–ธํŠธ ์žฌ์ดˆ๊ธฐํ™” ์‹œ๋„"); isInitialized = false; initializationError = null; initializeAppwriteClient(); @@ -136,7 +140,7 @@ export const isValidConnection = async (): Promise => { if (!isInitialized) { return false; } - + try { // ๊ณ„์ • ์„œ๋น„์Šค๋ฅผ ํ†ตํ•ด ํ˜„์žฌ ์„ธ์…˜ ์ƒํƒœ ํ™•์ธ (๊ฐ„๋‹จํ•œ API ํ˜ธ์ถœ) await account.get(); @@ -146,8 +150,8 @@ export const isValidConnection = async (): Promise => { if (error && (error as any).code === 401) { return true; // ์„œ๋ฒ„ ์—ฐ๊ฒฐ์€ ์ •์ƒ์ด์ง€๋งŒ ๋กœ๊ทธ์ธ๋˜์ง€ ์•Š์€ ์ƒํƒœ } - - console.error('Appwrite ์—ฐ๊ฒฐ ํ™•์ธ ์˜ค๋ฅ˜:', error); + + appwriteLogger.error("Appwrite ์—ฐ๊ฒฐ ํ™•์ธ ์˜ค๋ฅ˜:", error); return false; } }; diff --git a/src/lib/appwrite/config.ts b/src/lib/appwrite/config.ts index 9b65fba..1482a9e 100644 --- a/src/lib/appwrite/config.ts +++ b/src/lib/appwrite/config.ts @@ -1,6 +1,6 @@ /** * Appwrite ์„ค์ • - * + * * ์ด ํŒŒ์ผ์€ Appwrite ์„œ๋น„์Šค์— ํ•„์š”ํ•œ ๋ชจ๋“  ์„ค์ • ๊ฐ’์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. * ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๋ฉฐ, ๊ธฐ๋ณธ๊ฐ’์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. */ @@ -15,19 +15,24 @@ export interface AppwriteConfig { } // ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ ์„ค์ • ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ -const endpoint = import.meta.env.VITE_APPWRITE_ENDPOINT || 'https://a11.ism.kr/v1'; -const projectId = import.meta.env.VITE_APPWRITE_PROJECT_ID || '68182a300039f6d700a6'; -const databaseId = import.meta.env.VITE_APPWRITE_DATABASE_ID || 'default'; -const transactionsCollectionId = import.meta.env.VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID || 'transactions'; -const apiKey = import.meta.env.VITE_APPWRITE_API_KEY || ''; +const endpoint = + import.meta.env.VITE_APPWRITE_ENDPOINT || "https://a11.ism.kr/v1"; +const projectId = + import.meta.env.VITE_APPWRITE_PROJECT_ID || "68182a300039f6d700a6"; +const databaseId = import.meta.env.VITE_APPWRITE_DATABASE_ID || "default"; +const transactionsCollectionId = + import.meta.env.VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID || "transactions"; +// API ํ‚ค๋Š” ๋ณด์•ˆ์ƒ ํด๋ผ์ด์–ธํŠธ์—์„œ ์ œ๊ฑฐ๋จ +// ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ํ•จ์ˆ˜๋‚˜ ๋ฐฑ์—”๋“œ์—์„œ๋งŒ ์‚ฌ์šฉํ•ด์•ผ ํ•จ +const apiKey = ""; // ๊ฐœ๋ฐœ ๋ชจ๋“œ์—์„œ ์„ค์ • ๊ฐ’ ๋กœ๊น… -console.log('ํ˜„์žฌ Appwrite ์„ค์ •:', { - endpoint, - projectId, - databaseId, +appwriteLogger.info("ํ˜„์žฌ Appwrite ์„ค์ •:", { + endpoint, + projectId, + databaseId, transactionsCollectionId, - apiKey: apiKey ? '์„ค์ •๋จ' : '์„ค์ •๋˜์ง€ ์•Š์Œ' // API ํ‚ค๋Š” ์•ˆ์ „์„ ์œ„ํ•ด ์™„์ „ํ•œ ๊ฐ’์„ ๋กœ๊น…ํ•˜์ง€ ์•Š์Œ + apiKey: apiKey ? "์„ค์ •๋จ" : "์„ค์ •๋˜์ง€ ์•Š์Œ", // API ํ‚ค๋Š” ์•ˆ์ „์„ ์œ„ํ•ด ์™„์ „ํ•œ ๊ฐ’์„ ๋กœ๊น…ํ•˜์ง€ ์•Š์Œ }); // ์„ค์ • ๊ฐ์ฒด ์ƒ์„ฑ @@ -43,7 +48,8 @@ export const config: AppwriteConfig = { export const getAppwriteEndpoint = (): string => endpoint; export const getAppwriteProjectId = (): string => projectId; export const getAppwriteDatabaseId = (): string => databaseId; -export const getAppwriteTransactionsCollectionId = (): string => transactionsCollectionId; +export const getAppwriteTransactionsCollectionId = (): string => + transactionsCollectionId; /** * ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ @@ -58,6 +64,10 @@ export const isValidAppwriteConfig = (): boolean => { * @throws ํ•„์ˆ˜ ์„ค์ •์ด ์—†๋Š” ๊ฒฝ์šฐ ์˜ค๋ฅ˜ ๋ฐœ์ƒ */ export const validateConfig = (): void => { - if (!endpoint) throw new Error("VITE_APPWRITE_ENDPOINT is not set"); - if (!projectId) throw new Error("VITE_APPWRITE_PROJECT_ID is not set"); + if (!endpoint) { + throw new Error("VITE_APPWRITE_ENDPOINT is not set"); + } + if (!projectId) { + throw new Error("VITE_APPWRITE_PROJECT_ID is not set"); + } }; diff --git a/src/lib/appwrite/defaultUser.ts b/src/lib/appwrite/defaultUser.ts index 9551498..844e518 100644 --- a/src/lib/appwrite/defaultUser.ts +++ b/src/lib/appwrite/defaultUser.ts @@ -1,16 +1,16 @@ /** * Appwrite ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ์ •๋ณด - * + * * ์ด ํŒŒ์ผ์€ Appwrite ์„œ๋น„์Šค์— ์—ฐ๊ฒฐํ•  ๋•Œ ์‚ฌ์šฉํ•  ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. * ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ๋ผ์ธ์— ๋”ฐ๋ผ UI ์Šค๋ ˆ๋“œ๋ฅผ ์ฐจ๋‹จํ•˜์ง€ ์•Š๋„๋ก ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ์™€ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. */ // ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ID -export const DEFAULT_USER_ID = '68183aa4002a6f19542b'; +export const DEFAULT_USER_ID = "68183aa4002a6f19542b"; /** * ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜ - * + * * @returns ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ID */ export const getDefaultUserId = (): string => { @@ -19,7 +19,7 @@ export const getDefaultUserId = (): string => { /** * ์‚ฌ์šฉ์ž ID๊ฐ€ ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž์ธ์ง€ ํ™•์ธํ•˜๋Š” ํ•จ์ˆ˜ - * + * * @param userId ํ™•์ธํ•  ์‚ฌ์šฉ์ž ID * @returns ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ์—ฌ๋ถ€ */ diff --git a/src/lib/appwrite/index.ts b/src/lib/appwrite/index.ts index 9db1f28..364f284 100644 --- a/src/lib/appwrite/index.ts +++ b/src/lib/appwrite/index.ts @@ -1,12 +1,19 @@ -import { client, account, databases, storage, avatars, isValidConnection } from './client'; -import { - getAppwriteEndpoint, - getAppwriteProjectId, - getAppwriteDatabaseId, +import { + client, + account, + databases, + storage, + avatars, + isValidConnection, +} from "./client"; +import { + getAppwriteEndpoint, + getAppwriteProjectId, + getAppwriteDatabaseId, getAppwriteTransactionsCollectionId, - isValidAppwriteConfig -} from './config'; -import { setupAppwriteDatabase } from './setup'; + isValidAppwriteConfig, +} from "./config"; +import { setupAppwriteDatabase } from "./setup"; export { // ํด๋ผ์ด์–ธํŠธ ๋ฐ ์„œ๋น„์Šค @@ -15,7 +22,7 @@ export { databases, storage, avatars, - + // ์„ค์ • ๋ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ getAppwriteEndpoint, getAppwriteProjectId, @@ -23,7 +30,7 @@ export { getAppwriteTransactionsCollectionId, isValidAppwriteConfig, isValidConnection, - + // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • - setupAppwriteDatabase + setupAppwriteDatabase, }; diff --git a/src/lib/appwrite/setup.ts b/src/lib/appwrite/setup.ts index 82c213a..8ebbefb 100644 --- a/src/lib/appwrite/setup.ts +++ b/src/lib/appwrite/setup.ts @@ -1,6 +1,7 @@ -import { ID, Query, Permission, Role } from 'appwrite'; -import { databases, account } from './client'; -import { config } from './config'; +import { ID, Query, Permission, Role, Models } from "appwrite"; +import { appwriteLogger } from "@/utils/logger"; +import { databases, account } from "./client"; +import { config } from "./config"; /** * Appwrite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐ ์ปฌ๋ ‰์…˜ ์„ค์ • @@ -10,56 +11,62 @@ export const setupAppwriteDatabase = async (): Promise => { try { const databaseId = config.databaseId; const transactionsCollectionId = config.transactionsCollectionId; - + // ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ const user = await account.get(); - + // 1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์กด์žฌ ํ™•์ธ ๋˜๋Š” ์ƒ์„ฑ - let database: any; - + let database: Models.Database; + try { // ๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฐ€์ ธ์˜ค๊ธฐ ์‹œ๋„ // @ts-ignore - Appwrite SDK 17.0.2 ๋ฒ„์ „ ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ database = await databases.getDatabase(databaseId); - console.log('๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค:', database.name); + appwriteLogger.info("๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค:", database.name); } catch (error) { // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ์—†์œผ๋ฉด ์ƒ์„ฑ - console.log('๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค...'); + appwriteLogger.info("๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค..."); // @ts-ignore - Appwrite SDK 17.0.2 ๋ฒ„์ „ ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ - database = await databases.createDatabase(databaseId, 'Zellyy Finance'); - console.log('๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค:', database.name); + database = await databases.createDatabase(databaseId, "Zellyy Finance"); + appwriteLogger.info("๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค:", database.name); } - + // 2. ํŠธ๋žœ์žญ์…˜ ์ปฌ๋ ‰์…˜ ์กด์žฌ ํ™•์ธ ๋˜๋Š” ์ƒ์„ฑ - let collection: any; - + let collection: Models.Collection; + try { // ๊ธฐ์กด ์ปฌ๋ ‰์…˜ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹œ๋„ // @ts-ignore - Appwrite SDK 17.0.2 ๋ฒ„์ „ ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ - collection = await databases.getCollection(databaseId, transactionsCollectionId); - console.log('๊ธฐ์กด ํŠธ๋žœ์žญ์…˜ ์ปฌ๋ ‰์…˜์„ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค:', collection.name); + collection = await databases.getCollection( + databaseId, + transactionsCollectionId + ); + appwriteLogger.info( + "๊ธฐ์กด ํŠธ๋žœ์žญ์…˜ ์ปฌ๋ ‰์…˜์„ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค:", + collection.name + ); } catch (error) { // ์ปฌ๋ ‰์…˜์ด ์—†์œผ๋ฉด ์ƒ์„ฑ - console.log('ํŠธ๋žœ์žญ์…˜ ์ปฌ๋ ‰์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค...'); - + appwriteLogger.info("ํŠธ๋žœ์žญ์…˜ ์ปฌ๋ ‰์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค..."); + // @ts-ignore - Appwrite SDK 17.0.2 ๋ฒ„์ „ ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ collection = await databases.createCollection( databaseId, transactionsCollectionId, { - name: '๊ฑฐ๋ž˜ ๋‚ด์—ญ', + name: "๊ฑฐ๋ž˜ ๋‚ด์—ญ", permissions: [ // ์‚ฌ์šฉ์ž๋งŒ ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค์ • Permission.read(Role.user(user.$id)), Permission.update(Role.user(user.$id)), Permission.delete(Role.user(user.$id)), - Permission.create(Role.user(user.$id)) - ] + Permission.create(Role.user(user.$id)), + ], } ); - - console.log('ํŠธ๋žœ์žญ์…˜ ์ปฌ๋ ‰์…˜์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค:', collection.name); - + + appwriteLogger.info("ํŠธ๋žœ์žญ์…˜ ์ปฌ๋ ‰์…˜์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค:", collection.name); + // 3. ํ•„์š”ํ•œ ์†์„ฑ(ํ•„๋“œ) ์ƒ์„ฑ await Promise.all([ // ์‚ฌ์šฉ์ž ID ํ•„๋“œ @@ -67,106 +74,106 @@ export const setupAppwriteDatabase = async (): Promise => { databases.createStringAttribute( databaseId, transactionsCollectionId, - 'user_id', + "user_id", { size: 255, required: true, default: user.$id, - array: false + array: false, } ), - + // ํŠธ๋žœ์žญ์…˜ ID ํ•„๋“œ // @ts-ignore - Appwrite SDK 17.0.2 ๋ฒ„์ „ ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ databases.createStringAttribute( databaseId, transactionsCollectionId, - 'transaction_id', + "transaction_id", { size: 255, required: true, default: null, - array: false + array: false, } ), - + // ์ œ๋ชฉ ํ•„๋“œ // @ts-ignore - Appwrite SDK 17.0.2 ๋ฒ„์ „ ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ databases.createStringAttribute( databaseId, transactionsCollectionId, - 'title', + "title", { size: 255, required: true, default: null, - array: false + array: false, } ), - + // ๊ธˆ์•ก ํ•„๋“œ // @ts-ignore - Appwrite SDK 17.0.2 ๋ฒ„์ „ ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ databases.createFloatAttribute( databaseId, transactionsCollectionId, - 'amount', + "amount", { required: true, default: 0, min: null, - max: null + max: null, } ), - + // ๋‚ ์งœ ํ•„๋“œ // @ts-ignore - Appwrite SDK 17.0.2 ๋ฒ„์ „ ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ databases.createStringAttribute( databaseId, transactionsCollectionId, - 'date', + "date", { size: 255, required: true, default: null, - array: false + array: false, } ), - + // ์นดํ…Œ๊ณ ๋ฆฌ ํ•„๋“œ // @ts-ignore - Appwrite SDK 17.0.2 ๋ฒ„์ „ ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ databases.createStringAttribute( databaseId, transactionsCollectionId, - 'category', + "category", { size: 255, required: false, default: null, - array: false + array: false, } ), - + // ์œ ํ˜• ํ•„๋“œ (์ˆ˜์ž…/์ง€์ถœ) // @ts-ignore - Appwrite SDK 17.0.2 ๋ฒ„์ „ ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ databases.createStringAttribute( databaseId, transactionsCollectionId, - 'type', + "type", { size: 50, required: true, - default: 'expense', - array: false + default: "expense", + array: false, } - ) + ), ]); - - console.log('ํŠธ๋žœ์žญ์…˜ ์ปฌ๋ ‰์…˜ ์†์„ฑ์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + + appwriteLogger.info("ํŠธ๋žœ์žญ์…˜ ์ปฌ๋ ‰์…˜ ์†์„ฑ์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); } - + return true; } catch (error) { - console.error('Appwrite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • ์˜ค๋ฅ˜:', error); + appwriteLogger.error("Appwrite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • ์˜ค๋ฅ˜:", error); return false; } }; diff --git a/src/lib/fullMigrate.js b/src/lib/fullMigrate.js index 71ab383..88b7fc0 100755 --- a/src/lib/fullMigrate.js +++ b/src/lib/fullMigrate.js @@ -1,19 +1,20 @@ #!/usr/bin/env node // ์Šคํ‚ค๋งˆ ๋ฐ ๋ฐ์ดํ„ฐ๋ฅผ Supabase Cloud -> On-Prem(a11)์œผ๋กœ ์™„์ „ ๋ณต์ œ -import 'dotenv/config'; -import { execSync } from 'child_process'; -import { URL, fileURLToPath } from 'url'; -import path from 'path'; +import "dotenv/config"; +import { execSync } from "child_process"; +import { logger } from "@/utils/logger"; +import { URL, fileURLToPath } from "url"; +import path from "path"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const CLOUD_DATABASE_URL = process.env.CLOUD_DATABASE_URL; -const ONPREM_SSH_HOST = process.env.ONPREM_SSH_HOST || 'a11'; -const ONPREM_REMOTE_TMP_DIR = process.env.ONPREM_REMOTE_TMP_DIR || '/root'; +const ONPREM_SSH_HOST = process.env.ONPREM_SSH_HOST || "a11"; +const ONPREM_REMOTE_TMP_DIR = process.env.ONPREM_REMOTE_TMP_DIR || "/root"; if (!CLOUD_DATABASE_URL) { - console.error('ํ™˜๊ฒฝ ๋ณ€์ˆ˜ CLOUD_DATABASE_URL์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.'); + logger.error("ํ™˜๊ฒฝ ๋ณ€์ˆ˜ CLOUD_DATABASE_URL์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); process.exit(1); } @@ -21,24 +22,26 @@ if (!CLOUD_DATABASE_URL) { const cloudUrlObj = new URL(CLOUD_DATABASE_URL); const CLOUD_DATABASE_PASSWORD = cloudUrlObj.password; if (!CLOUD_DATABASE_PASSWORD) { - console.error('Cloud DB URL์—์„œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'); + logger.error("Cloud DB URL์—์„œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); process.exit(1); } // ์›๊ฒฉ Postgres ์ปจํ…Œ์ด๋„ˆ ์ด๋ฆ„ ์กฐํšŒ -console.log('์›๊ฒฉ Postgres ์ปจํ…Œ์ด๋„ˆ ์กฐํšŒ (ssh a11)...'); +logger.info("์›๊ฒฉ Postgres ์ปจํ…Œ์ด๋„ˆ ์กฐํšŒ (ssh a11)..."); let containerName = execSync( `ssh ${ONPREM_SSH_HOST} "docker ps --format '{{.Names}} {{.Image}}' | grep supabase/postgres | awk '{print \\$1}'"`, - { encoding: 'utf8' } + { encoding: "utf8" } ).trim(); if (!containerName) { - console.error('์›๊ฒฉ Postgres ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. docker ps ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.'); + logger.error( + "์›๊ฒฉ Postgres ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. docker ps ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜์„ธ์š”." + ); process.exit(1); } -console.log(`๋ฐœ๊ฒฌ๋œ ์ปจํ…Œ์ด๋„ˆ: ${containerName}`); +logger.info(`๋ฐœ๊ฒฌ๋œ ์ปจํ…Œ์ด๋„ˆ: ${containerName}`); // 1) ์›๊ฒฉ a11์—์„œ Cloud DB ์Šคํ‚ค๋งˆ ๋คํ”„ -console.log('์›๊ฒฉ์—์„œ Cloud DB ์Šคํ‚ค๋งˆ ๋คํ”„ ์‹œ์ž‘...'); +logger.info("์›๊ฒฉ์—์„œ Cloud DB ์Šคํ‚ค๋งˆ ๋คํ”„ ์‹œ์ž‘..."); execSync( `ssh ${ONPREM_SSH_HOST} "docker run --rm --network host -e PGPASSWORD='${CLOUD_DATABASE_PASSWORD}' postgres:15 ` + `pg_dump --schema-only --no-owner --no-privileges ` + @@ -46,11 +49,11 @@ execSync( `-p ${cloudUrlObj.port} ` + `-U ${cloudUrlObj.username} ` + `${cloudUrlObj.pathname.slice(1)} > ${ONPREM_REMOTE_TMP_DIR}/supabase_schema.sql"`, - { stdio: 'inherit' } + { stdio: "inherit" } ); // 2) ์›๊ฒฉ a11์—์„œ Cloud DB ๋ฐ์ดํ„ฐ ๋คํ”„ -console.log('์›๊ฒฉ์—์„œ Cloud DB ๋ฐ์ดํ„ฐ ๋คํ”„ ์‹œ์ž‘...'); +logger.info("์›๊ฒฉ์—์„œ Cloud DB ๋ฐ์ดํ„ฐ ๋คํ”„ ์‹œ์ž‘..."); execSync( `ssh ${ONPREM_SSH_HOST} "docker run --rm --network host -e PGPASSWORD='${CLOUD_DATABASE_PASSWORD}' postgres:15 ` + `pg_dump --data-only --column-inserts --no-owner --no-privileges ` + @@ -58,23 +61,23 @@ execSync( `-p ${cloudUrlObj.port} ` + `-U ${cloudUrlObj.username} ` + `${cloudUrlObj.pathname.slice(1)} > ${ONPREM_REMOTE_TMP_DIR}/supabase_data.sql"`, - { stdio: 'inherit' } + { stdio: "inherit" } ); // 4) ์›๊ฒฉ์— ๋ณต์› (์Šคํ‚ค๋งˆ) -console.log('์›๊ฒฉ ์Šคํ‚ค๋งˆ ๋ณต์›...'); +logger.info("์›๊ฒฉ ์Šคํ‚ค๋งˆ ๋ณต์›..."); execSync( `ssh ${ONPREM_SSH_HOST} "docker exec -i ${containerName} ` + `psql -U postgres -d postgres < ${ONPREM_REMOTE_TMP_DIR}/supabase_schema.sql"`, - { stdio: 'inherit' } + { stdio: "inherit" } ); // 5) ์›๊ฒฉ์— ๋ณต์› (๋ฐ์ดํ„ฐ) -console.log('์›๊ฒฉ ๋ฐ์ดํ„ฐ ๋ณต์›...'); +logger.info("์›๊ฒฉ ๋ฐ์ดํ„ฐ ๋ณต์›..."); execSync( `ssh ${ONPREM_SSH_HOST} "docker exec -i ${containerName} ` + `psql -U postgres -d postgres < ${ONPREM_REMOTE_TMP_DIR}/supabase_data.sql"`, - { stdio: 'inherit' } + { stdio: "inherit" } ); -console.log('Cloud โ†’ On-Prem ์ „์ฒด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ.'); +logger.info("Cloud โ†’ On-Prem ์ „์ฒด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ."); diff --git a/src/lib/migrateData.js b/src/lib/migrateData.js index 7839000..c29a4f9 100644 --- a/src/lib/migrateData.js +++ b/src/lib/migrateData.js @@ -1,15 +1,22 @@ -import dotenv from 'dotenv'; -import { createClient } from '@supabase/supabase-js'; +import dotenv from "dotenv"; +import { logger } from "@/utils/logger"; +import { createClient } from "@supabase/supabase-js"; dotenv.config(); const cloudUrl = process.env.CLOUD_SUPABASE_URL; -const cloudKey = process.env.CLOUD_SUPABASE_SERVICE_ROLE_KEY || process.env.CLOUD_SUPABASE_ANON_KEY; +const cloudKey = + process.env.CLOUD_SUPABASE_SERVICE_ROLE_KEY || + process.env.CLOUD_SUPABASE_ANON_KEY; const onpremUrl = process.env.ONPREM_SUPABASE_URL; -const onpremKey = process.env.ONPREM_SUPABASE_SERVICE_ROLE_KEY || process.env.ONPREM_SUPABASE_ANON_KEY; +const onpremKey = + process.env.ONPREM_SUPABASE_SERVICE_ROLE_KEY || + process.env.ONPREM_SUPABASE_ANON_KEY; if (!cloudUrl || !cloudKey || !onpremUrl || !onpremKey) { - console.error('ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ์˜ค๋ฅ˜: CLOUD/ONPREM URL ๋˜๋Š” ํ‚ค๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + logger.error( + "ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ์˜ค๋ฅ˜: CLOUD/ONPREM URL ๋˜๋Š” ํ‚ค๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." + ); process.exit(1); } @@ -17,7 +24,7 @@ const cloud = createClient(cloudUrl, cloudKey); const onprem = createClient(onpremUrl, onpremKey); // ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•  ํ…Œ์ด๋ธ” ๋ชฉ๋ก -const tables = ['transactions', 'budgets', '_tests']; +const tables = ["transactions", "budgets", "_tests"]; // ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ์ •์˜ const tableSchemas = { @@ -115,15 +122,15 @@ const tableSchemas = { CREATE POLICY "๋ชจ๋“  ์‚ฌ์šฉ์ž๊ฐ€ ํ…Œ์ŠคํŠธ ํ…Œ์ด๋ธ”์— ์ ‘๊ทผ ๊ฐ€๋Šฅ" ON _tests FOR SELECT USING (true); - ` + `, }; /** * ํ—ฌํผ ํ•จ์ˆ˜ ์ƒ์„ฑ */ async function createHelperFunctions() { - console.log('ํ—ฌํผ ํ•จ์ˆ˜ ์ƒ์„ฑ ์ค‘...'); - + logger.info("ํ—ฌํผ ํ•จ์ˆ˜ ์ƒ์„ฑ ์ค‘..."); + // execute_sql ํ•จ์ˆ˜ ์ƒ์„ฑ const executeSqlSQL = ` CREATE OR REPLACE FUNCTION execute_sql(sql_query TEXT) @@ -133,14 +140,16 @@ async function createHelperFunctions() { END; $$ LANGUAGE plpgsql SECURITY DEFINER; `; - - const { error: execFnError } = await onprem.rpc('execute_sql', { sql_query: executeSqlSQL }); + + const { error: execFnError } = await onprem.rpc("execute_sql", { + sql_query: executeSqlSQL, + }); if (execFnError) { - console.error('execute_sql ํ•จ์ˆ˜ ์ƒ์„ฑ ์‹คํŒจ:', execFnError); + logger.error("execute_sql ํ•จ์ˆ˜ ์ƒ์„ฑ ์‹คํŒจ:", execFnError); return false; } - - console.log('ํ—ฌํผ ํ•จ์ˆ˜ ์ƒ์„ฑ ์™„๋ฃŒ'); + + logger.info("ํ—ฌํผ ํ•จ์ˆ˜ ์ƒ์„ฑ ์™„๋ฃŒ"); return true; } @@ -148,26 +157,28 @@ async function createHelperFunctions() { * ํ…Œ์ด๋ธ” ์ƒ์„ฑ */ async function createTable(tableName) { - console.log(`ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์ค‘: ${tableName}`); - + logger.info(`ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์ค‘: ${tableName}`); + if (!tableSchemas[tableName]) { - console.warn(`${tableName} ํ…Œ์ด๋ธ”์˜ ์Šคํ‚ค๋งˆ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.`); + logger.warn(`${tableName} ํ…Œ์ด๋ธ”์˜ ์Šคํ‚ค๋งˆ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.`); return false; } - + try { // ํ…Œ์ด๋ธ” ์ƒ์„ฑ SQL ์‹คํ–‰ - const { error } = await onprem.rpc('execute_sql', { sql_query: tableSchemas[tableName] }); - + const { error } = await onprem.rpc("execute_sql", { + sql_query: tableSchemas[tableName], + }); + if (error) { - console.error(`${tableName} ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์‹คํŒจ:`, error); + logger.error(`${tableName} ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์‹คํŒจ:`, error); return false; } - - console.log(`${tableName} ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์™„๋ฃŒ`); + + logger.info(`${tableName} ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์™„๋ฃŒ`); return true; } catch (error) { - console.error(`${tableName} ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜:`, error); + logger.error(`${tableName} ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜:`, error); return false; } } @@ -176,49 +187,51 @@ async function createTable(tableName) { * ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ */ async function migrateTableData(tableName) { - console.log(`ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ค‘: ${tableName}`); - + logger.info(`ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ค‘: ${tableName}`); + try { // Cloud DB์—์„œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ - const { data, error } = await cloud.from(tableName).select('*'); - + const { data, error } = await cloud.from(tableName).select("*"); + if (error) { - if (error.code === '42P01') { - console.warn(`Cloud DB์— ${tableName} ํ…Œ์ด๋ธ”์ด ์—†์Šต๋‹ˆ๋‹ค. ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.`); + if (error.code === "42P01") { + logger.warn(`Cloud DB์— ${tableName} ํ…Œ์ด๋ธ”์ด ์—†์Šต๋‹ˆ๋‹ค. ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.`); return true; } - console.error(`Cloud DB์—์„œ ${tableName} ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ:`, error); + logger.error(`Cloud DB์—์„œ ${tableName} ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ:`, error); return false; } - + if (!data || data.length === 0) { - console.log(`${tableName} ํ…Œ์ด๋ธ”์— ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.`); + logger.info(`${tableName} ํ…Œ์ด๋ธ”์— ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.`); return true; } - - console.log(`${tableName} ํ…Œ์ด๋ธ”์—์„œ ${data.length}๊ฐœ ํ–‰์„ ๊ฐ€์ ธ์™”์Šต๋‹ˆ๋‹ค.`); - + + logger.info(`${tableName} ํ…Œ์ด๋ธ”์—์„œ ${data.length}๊ฐœ ํ–‰์„ ๊ฐ€์ ธ์™”์Šต๋‹ˆ๋‹ค.`); + // ๋ฐ์ดํ„ฐ๋ฅผ ์ž‘์€ ๋ฐฐ์น˜๋กœ ๋‚˜๋ˆ„์–ด ์‚ฝ์ž… (ํŠธ๋žœ์žญ์…˜ ์‚ญ์ œ ์•ˆ์ „์„ฑ ๊ณ ๋ ค) const batchSize = 100; for (let i = 0; i < data.length; i += batchSize) { const batch = data.slice(i, i + batchSize); const { error: insertError } = await onprem.from(tableName).upsert(batch); - + if (insertError) { - console.error(`${tableName} ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ ์‚ฝ์ž… ์‹คํŒจ:`, insertError); + logger.error(`${tableName} ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ ์‚ฝ์ž… ์‹คํŒจ:`, insertError); return false; } - - console.log(`${tableName} ํ…Œ์ด๋ธ”์— ${batch.length}๊ฐœ ํ–‰ ์‚ฝ์ž… ์™„๋ฃŒ (${i + batch.length}/${data.length})`); - + + logger.info( + `${tableName} ํ…Œ์ด๋ธ”์— ${batch.length}๊ฐœ ํ–‰ ์‚ฝ์ž… ์™„๋ฃŒ (${i + batch.length}/${data.length})` + ); + // ๋น„๋™๊ธฐ ์ž‘์—… ์‚ฌ์ด์— ์งง์€ ์ง€์—ฐ ์ถ”๊ฐ€ (UI ์Šค๋ ˆ๋“œ ์ฐจ๋‹จ ๋ฐฉ์ง€) - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); } - - console.log(`${tableName} ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ`); + + logger.info(`${tableName} ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ`); return true; } catch (error) { - console.error(`${tableName} ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ค‘ ์˜ค๋ฅ˜:`, error); + logger.error(`${tableName} ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ค‘ ์˜ค๋ฅ˜:`, error); return false; } } @@ -228,33 +241,35 @@ async function migrateTableData(tableName) { */ async function main() { try { - console.log('Supabase Cloud โ†’ On-Prem ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹œ์ž‘'); - + logger.info("Supabase Cloud โ†’ On-Prem ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹œ์ž‘"); + // ํ—ฌํผ ํ•จ์ˆ˜ ์ƒ์„ฑ const helperCreated = await createHelperFunctions(); if (!helperCreated) { - console.warn('ํ—ฌํผ ํ•จ์ˆ˜ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ณ„์† ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.'); + logger.warn("ํ—ฌํผ ํ•จ์ˆ˜ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ณ„์† ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค."); } - + // ๊ฐ ํ…Œ์ด๋ธ”์— ๋Œ€ํ•ด ์Šคํ‚ค๋งˆ ์ƒ์„ฑ ๋ฐ ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ˆ˜ํ–‰ for (const tableName of tables) { // ํ…Œ์ด๋ธ” ์ƒ์„ฑ const tableCreated = await createTable(tableName); if (!tableCreated) { - console.warn(`${tableName} ํ…Œ์ด๋ธ” ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.`); + logger.warn( + `${tableName} ํ…Œ์ด๋ธ” ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.` + ); continue; } - + // ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ const dataMigrated = await migrateTableData(tableName); if (!dataMigrated) { - console.warn(`${tableName} ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.`); + logger.warn(`${tableName} ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.`); } } - - console.log('Supabase Cloud โ†’ On-Prem ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ'); + + logger.info("Supabase Cloud โ†’ On-Prem ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ"); } catch (error) { - console.error('๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error); + logger.error("๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:", error); process.exit(1); } } diff --git a/src/lib/migrateData.ts b/src/lib/migrateData.ts index c92e174..16e7ac5 100644 --- a/src/lib/migrateData.ts +++ b/src/lib/migrateData.ts @@ -1,17 +1,24 @@ -import dotenv from 'dotenv'; -import { createClient } from '@supabase/supabase-js'; +import dotenv from "dotenv"; +import { logger } from "@/utils/logger"; +import { createClient } from "@supabase/supabase-js"; dotenv.config(); const cloudUrl = process.env.CLOUD_SUPABASE_URL; // ์„œ๋น„์Šค ์—ญํ•  ํ‚ค๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฉด CLOUD_SUPABASE_ANON_KEY ๋˜๋Š” VITE_SUPABASE_ANON_KEY๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. -const cloudKey = process.env.CLOUD_SUPABASE_SERVICE_ROLE_KEY || process.env.CLOUD_SUPABASE_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY; +const cloudKey = + process.env.CLOUD_SUPABASE_SERVICE_ROLE_KEY || + process.env.CLOUD_SUPABASE_ANON_KEY || + process.env.VITE_SUPABASE_ANON_KEY; const onpremUrl = process.env.ONPREM_SUPABASE_URL; // ์„œ๋น„์Šค ์—ญํ•  ํ‚ค๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฉด ONPREM_SUPABASE_ANON_KEY ๋˜๋Š” VITE_SUPABASE_ANON_KEY๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. -const onpremKey = process.env.ONPREM_SUPABASE_SERVICE_ROLE_KEY || process.env.ONPREM_SUPABASE_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY; +const onpremKey = + process.env.ONPREM_SUPABASE_SERVICE_ROLE_KEY || + process.env.ONPREM_SUPABASE_ANON_KEY || + process.env.VITE_SUPABASE_ANON_KEY; if (!cloudUrl || !cloudKey || !onpremUrl || !onpremKey) { - console.error('ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. .env ํŒŒ์ผ์„ ํ™•์ธํ•˜์„ธ์š”.'); + logger.error("ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. .env ํŒŒ์ผ์„ ํ™•์ธํ•˜์„ธ์š”."); process.exit(1); } @@ -20,33 +27,33 @@ const onprem = createClient(onpremUrl, onpremKey); // ๋ณต์‚ฌํ•  ํ…Œ์ด๋ธ” ๋ชฉ๋ก์„ ์ •์˜ํ•˜์„ธ์š”. const tables = [ - 'users', - 'accounts', - 'transactions', + "users", + "accounts", + "transactions", // ํ•„์š”์— ๋”ฐ๋ผ ์ถ”๊ฐ€ ํ…Œ์ด๋ธ”์„ ์—ฌ๊ธฐ์— ์ž…๋ ฅ ]; async function migrateTable(table: string) { - console.log(`Migrating table: ${table}`); - const { data, error } = await cloud.from(table).select('*'); + logger.info(`Migrating table: ${table}`); + const { data, error } = await cloud.from(table).select("*"); if (error) { // ํ…Œ์ด๋ธ”์ด ์—†์œผ๋ฉด ์Šคํ‚ต - if (error.code === '42P01') { - console.warn(`Table ${table} not found in Cloud DB, skipping.`); + if (error.code === "42P01") { + logger.warn(`Table ${table} not found in Cloud DB, skipping.`); return; } - console.error(`Error fetching ${table}:`, error); + logger.error(`Error fetching ${table}:`, error); return; } if (!data || data.length === 0) { - console.log(`${table} has no data to migrate.`); + logger.info(`${table} has no data to migrate.`); return; } const { error: insertError } = await onprem.from(table).upsert(data); if (insertError) { - console.error(`Error inserting into ${table}:`, insertError); + logger.error(`Error inserting into ${table}:`, insertError); } else { - console.log(`Migrated ${data.length} rows into ${table}`); + logger.info(`Migrated ${data.length} rows into ${table}`); } } @@ -54,10 +61,10 @@ async function main() { for (const table of tables) { await migrateTable(table); } - console.log('Migration complete.'); + logger.info("Migration complete."); } -main().catch(err => { - console.error('Migration failed:', err); +main().catch((err) => { + logger.error("Migration failed:", err); process.exit(1); }); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index bd0c391..a5ef193 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,6 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } diff --git a/src/main.tsx b/src/main.tsx index 1aa42de..ae8959d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,35 +1,39 @@ -import { createRoot } from 'react-dom/client'; -import { BrowserRouter } from 'react-router-dom'; -import App from './App.tsx'; -import './index.css'; +import { createRoot } from "react-dom/client"; +import { logger } from "@/utils/logger"; +import { BrowserRouter } from "react-router-dom"; +import App from "./App.tsx"; +import "./index.css"; -console.log('main.tsx loaded'); +logger.info("main.tsx loaded"); // iOS ์•ˆ์ „ ์˜์—ญ ๋ฉ”ํƒ€ ํƒœ๊ทธ ์ถ”๊ฐ€ const setViewportMetaTag = () => { // ๊ธฐ์กด viewport ๋ฉ”ํƒ€ ํƒœ๊ทธ ์ฐพ๊ธฐ let metaTag = document.querySelector('meta[name="viewport"]'); - + // ์—†์œผ๋ฉด ์ƒˆ๋กœ ์ƒ์„ฑ if (!metaTag) { - metaTag = document.createElement('meta'); - metaTag.setAttribute('name', 'viewport'); + metaTag = document.createElement("meta"); + metaTag.setAttribute("name", "viewport"); document.head.appendChild(metaTag); } - + // content ์†์„ฑ ์„ค์ • (viewport-fit=cover ์ถ”๊ฐ€) - metaTag.setAttribute('content', 'width=device-width, initial-scale=1.0, viewport-fit=cover'); + metaTag.setAttribute( + "content", + "width=device-width, initial-scale=1.0, viewport-fit=cover" + ); }; // ๋ฉ”ํƒ€ ํƒœ๊ทธ ์„ค์ • ์ ์šฉ setViewportMetaTag(); // ์ „์—ญ ์˜ค๋ฅ˜ ํ•ธ๋“ค๋Ÿฌ ์ถ”๊ฐ€ -window.onerror = function(message, source, lineno, colno, error) { - console.error('์ „์—ญ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', { message, source, lineno, colno, error }); - +window.onerror = function (message, source, lineno, colno, error) { + logger.error("์ „์—ญ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:", { message, source, lineno, colno, error }); + // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ๊ธฐ๋ณธ ์˜ค๋ฅ˜ ํ™”๋ฉด ํ‘œ์‹œ - const rootElement = document.getElementById('root'); + const rootElement = document.getElementById("root"); if (rootElement) { rootElement.innerHTML = `
@@ -46,16 +50,16 @@ window.onerror = function(message, source, lineno, colno, error) {
`; } - + return false; }; // ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ Promise ์˜ค๋ฅ˜ ํ•ธ๋“ค๋Ÿฌ ์ถ”๊ฐ€ -window.addEventListener('unhandledrejection', function(event) { - console.error('์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ Promise ์˜ค๋ฅ˜:', event.reason); - +window.addEventListener("unhandledrejection", function (event) { + logger.error("์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ Promise ์˜ค๋ฅ˜:", event.reason); + // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ๊ธฐ๋ณธ ์˜ค๋ฅ˜ ํ™”๋ฉด ํ‘œ์‹œ - const rootElement = document.getElementById('root'); + const rootElement = document.getElementById("root"); if (rootElement) { rootElement.innerHTML = `
@@ -75,7 +79,7 @@ window.addEventListener('unhandledrejection', function(event) { }); // ๋””๋ฒ„๊น… ์ •๋ณด ์ถœ๋ ฅ -console.log('ํ™˜๊ฒฝ ๋ณ€์ˆ˜:', { +logger.info("ํ™˜๊ฒฝ ๋ณ€์ˆ˜:", { NODE_ENV: import.meta.env.MODE, BASE_URL: import.meta.env.BASE_URL, APPWRITE_ENDPOINT: import.meta.env.VITE_APPWRITE_ENDPOINT, @@ -94,25 +98,25 @@ declare global { window.appwriteEnabled = false; try { - const rootElement = document.getElementById('root'); + const rootElement = document.getElementById("root"); if (!rootElement) { - throw new Error('Root element not found'); + throw new Error("Root element not found"); } - + const root = createRoot(rootElement); - + root.render( ); - - console.log('์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ Œ๋”๋ง ์„ฑ๊ณต'); + + logger.info("์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ Œ๋”๋ง ์„ฑ๊ณต"); } catch (error) { - console.error('์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ Œ๋”๋ง ์˜ค๋ฅ˜:', error); - + logger.error("์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ Œ๋”๋ง ์˜ค๋ฅ˜:", error); + // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ๊ธฐ๋ณธ ์˜ค๋ฅ˜ ํ™”๋ฉด ํ‘œ์‹œ - const rootElement = document.getElementById('root'); + const rootElement = document.getElementById("root"); if (rootElement) { rootElement.innerHTML = `
@@ -128,4 +132,4 @@ try {
`; } -}; +} diff --git a/src/next-steps-plan.md b/src/next-steps-plan.md index 377bc05..a4048d6 100644 --- a/src/next-steps-plan.md +++ b/src/next-steps-plan.md @@ -3,16 +3,19 @@ ## 1. ์•ฑ ๋ฐฐํฌ ์ค€๋น„ ์™„๋ฃŒ ### 1.1 ์•ฑ ๋ฐฐํฌ ๊ฐ€์ด๋“œ ๋ฌธ์„œ ์™„์„ฑ + - ํ˜„์žฌ ์ž‘์„ฑ ์ค‘์ธ app-deployment-guide.md ๋ฌธ์„œ ์™„์„ฑ - ์‹ค์ œ ๋ฐฐํฌ ๊ฒฝํ—˜์„ ๋ฐ”ํƒ•์œผ๋กœ ๊ฐ€์ด๋“œ ์—…๋ฐ์ดํŠธ - ๋ฐฐํฌ ์ค‘ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๋ฌธ์ œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• ์ถ”๊ฐ€ ### 1.2 Android ์•ฑ ๋นŒ๋“œ ๋ฐ ํ…Œ์ŠคํŠธ + - ์•ˆ๋“œ๋กœ์ด๋“œ ๋นŒ๋“œ ํ™˜๊ฒฝ ์ตœ์ข… ์ ๊ฒ€ - ๋‹ค์–‘ํ•œ ์•ˆ๋“œ๋กœ์ด๋“œ ๊ธฐ๊ธฐ์—์„œ ํ˜ธํ™˜์„ฑ ํ…Œ์ŠคํŠธ - ์„ฑ๋Šฅ ๋ฐ ์•ˆ์ •์„ฑ ํ…Œ์ŠคํŠธ ### 1.3 iOS ํ™˜๊ฒฝ ์„ค์ • ๋ฐ ๋นŒ๋“œ (ํ•„์š”์‹œ) + - Mac ํ™˜๊ฒฝ์—์„œ iOS ๋นŒ๋“œ ์„ค์ • - iOS ์•ฑ ์•„์ด์ฝ˜ ๋ฐ ์Šคํ”Œ๋ž˜์‹œ ์Šคํฌ๋ฆฐ ์ค€๋น„ - TestFlight๋ฅผ ํ†ตํ•œ ๋ฒ ํƒ€ ํ…Œ์ŠคํŠธ @@ -20,33 +23,39 @@ ## 2. ์•ฑ ์ตœ์ ํ™” ### 2.1 ์„ฑ๋Šฅ ์ตœ์ ํ™” + - ์•ฑ ๋กœ๋”ฉ ์‹œ๊ฐ„ ๊ฐœ์„  - ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ตœ์ ํ™” - ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ ์ตœ์ ํ™” - ์•Œ๋ฆผ ์‹œ์Šคํ…œ ์•ˆ์ •ํ™” (ํ˜„์žฌ ๋ฐœ์ƒ ์ค‘์ธ ๋ฒ„๊ทธ ์ˆ˜์ •) ### 2.2 ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„  + - UI/UX ์ผ๊ด€์„ฑ ํ™•๋ณด - ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ฐ ์ „ํ™˜ ํšจ๊ณผ ์ตœ์ ํ™” - ์ ‘๊ทผ์„ฑ ๊ฐœ์„  ### 2.3 ์˜คํ”„๋ผ์ธ ๊ธฐ๋Šฅ ๊ฐ•ํ™” + - ์˜คํ”„๋ผ์ธ ์ƒํƒœ์—์„œ์˜ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๊ฐœ์„  - ๋™๊ธฐํ™” ๋ฉ”์ปค๋‹ˆ์ฆ˜ ๊ฐ•ํ™” ## 3. ์ถ”๊ฐ€ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ ### 3.1 ์•Œ๋ฆผ ์‹œ์Šคํ…œ ๊ตฌํ˜„ + - ํ‘ธ์‹œ ์•Œ๋ฆผ ๊ธฐ๋Šฅ ๊ตฌํ˜„ - ์•Œ๋ฆผ ์„ค์ • ํŽ˜์ด์ง€ ๊ฐœ์„  - ์•Œ๋ฆผ ์Šค์ผ€์ค„๋ง ๊ธฐ๋Šฅ ### 3.2 ๋ฐ์ดํ„ฐ ๋ฐฑ์—… ๋ฐ ๋ณต์› ๊ธฐ๋Šฅ + - ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๋ฐฑ์—… ๊ธฐ๋Šฅ - ๋ฐ์ดํ„ฐ ๋ณต์› ๋ฉ”์ปค๋‹ˆ์ฆ˜ - ํด๋ผ์šฐ๋“œ ๋ฐฑ์—… ์˜ต์…˜ ### 3.3 ๋‹ค๊ตญ์–ด ์ง€์› ํ™•์žฅ + - ๋‹ค๊ตญ์–ด ์ง€์› ์‹œ์Šคํ…œ ๊ตฌํ˜„ - ์–ธ์–ด ์„ค์ • ํŽ˜์ด์ง€ ์ถ”๊ฐ€ - ๋ฒˆ์—ญ ๋ฆฌ์†Œ์Šค ์ค€๋น„ @@ -54,16 +63,19 @@ ## 4. ํ…Œ์ŠคํŠธ ๋ฐ ํ’ˆ์งˆ ๋ณด์ฆ ### 4.1 ๋‹ค์–‘ํ•œ ๊ธฐ๊ธฐ์—์„œ์˜ ํ˜ธํ™˜์„ฑ ํ…Œ์ŠคํŠธ + - ๋‹ค์–‘ํ•œ ์•ˆ๋“œ๋กœ์ด๋“œ ๋ฒ„์ „ ํ…Œ์ŠคํŠธ - ๋‹ค์–‘ํ•œ ํ™”๋ฉด ํฌ๊ธฐ ๋ฐ ํ•ด์ƒ๋„ ํ…Œ์ŠคํŠธ - ์ €์‚ฌ์–‘ ๊ธฐ๊ธฐ์—์„œ์˜ ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ ### 4.2 ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ ์ˆ˜์ง‘ ๋ฐ ๋ฐ˜์˜ + - ์ธ์•ฑ ํ”ผ๋“œ๋ฐฑ ์‹œ์Šคํ…œ ๊ตฌํ˜„ - ์‚ฌ์šฉ์ž ํ…Œ์ŠคํŠธ ์„ธ์…˜ ์ง„ํ–‰ - ํ”ผ๋“œ๋ฐฑ ๊ธฐ๋ฐ˜ ๊ฐœ์„ ์‚ฌํ•ญ ์šฐ์„ ์ˆœ์œ„ ์„ค์ • ### 4.3 ๋ฒ„๊ทธ ์ˆ˜์ • ๋ฐ ์•ˆ์ •์„ฑ ๊ฐœ์„  + - ์•Œ๋ ค์ง„ ๋ฒ„๊ทธ ์ˆ˜์ • - ํฌ๋ž˜์‹œ ๋ฆฌํฌํŠธ ๋ถ„์„ ๋ฐ ๋Œ€์‘ - ์ž๋™ํ™”๋œ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€ @@ -71,16 +83,19 @@ ## 5. ์Šคํ† ์–ด ๋“ฑ๋ก ์ค€๋น„ ### 5.1 Google Play ์Šคํ† ์–ด ๋“ฑ๋ก ์ž๋ฃŒ ์ค€๋น„ + - ์Šคํ† ์–ด ๋ฆฌ์ŠคํŒ… ์ •๋ณด ์ž‘์„ฑ - ์Šคํฌ๋ฆฐ์ƒท ๋ฐ ํ”„๋กœ๋ชจ์…˜ ์ด๋ฏธ์ง€ ์ค€๋น„ - ๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ๋ฐฉ์นจ ๋ฌธ์„œ ์ž‘์„ฑ ### 5.2 App Store ๋“ฑ๋ก ์ž๋ฃŒ ์ค€๋น„ (ํ•„์š”์‹œ) + - App Store Connect ์„ค์ • - ์•ฑ ์‹ฌ์‚ฌ ์ค€๋น„ - ๋งˆ์ผ€ํŒ… ์ž๋ฃŒ ์ค€๋น„ ### 5.3 ์ถœ์‹œ ํ›„ ๋ชจ๋‹ˆํ„ฐ๋ง ๊ณ„ํš + - ์•ฑ ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ ๊ตฌ์ถ• - ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ ์ˆ˜์ง‘ ์ฑ„๋„ ์„ค์ • - ์ •๊ธฐ ์—…๋ฐ์ดํŠธ ์ผ์ • ์ˆ˜๋ฆฝ @@ -88,6 +103,7 @@ ## ์ฆ‰์‹œ ์ง„ํ–‰ ๊ฐ€๋Šฅํ•œ ์ž‘์—… 1. **์•Œ๋ฆผ ์‹œ์Šคํ…œ ๋ฒ„๊ทธ ์ˆ˜์ •** + ```bash # ์ˆ˜์ •๋œ ์ฝ”๋“œ ํ…Œ์ŠคํŠธ npm run build diff --git a/src/pages/Analytics.tsx b/src/pages/Analytics.tsx index 00bdc01..6514e07 100644 --- a/src/pages/Analytics.tsx +++ b/src/pages/Analytics.tsx @@ -1,80 +1,87 @@ - -import React, { useState, useEffect } from 'react'; -import NavBar from '@/components/NavBar'; -import ExpenseChart from '@/components/ExpenseChart'; -import AddTransactionButton from '@/components/AddTransactionButton'; -import { useBudget } from '@/contexts/budget/BudgetContext'; -import { MONTHS_KR } from '@/hooks/useTransactions'; -import { useIsMobile } from '@/hooks/use-mobile'; -import { getCategoryColor } from '@/utils/categoryColorUtils'; -import { Separator } from '@/components/ui/separator'; +import React, { useState, useEffect } from "react"; +import { logger } from "@/utils/logger"; +import NavBar from "@/components/NavBar"; +import ExpenseChart from "@/components/ExpenseChart"; +import AddTransactionButton from "@/components/AddTransactionButton"; +import { useBudget } from "@/contexts/budget/BudgetContext"; +import { MONTHS_KR } from "@/hooks/useTransactions"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { getCategoryColor } from "@/utils/categoryColorUtils"; +import { Separator } from "@/components/ui/separator"; +import { MonthlyData } from "@/types"; // ์ƒˆ๋กœ ๋ถ„๋ฆฌํ•œ ์ปดํฌ๋„ŒํŠธ๋“ค ๋ถˆ๋Ÿฌ์˜ค๊ธฐ -import PeriodSelector from '@/components/analytics/PeriodSelector'; -import SummaryCards from '@/components/analytics/SummaryCards'; -import MonthlyComparisonChart from '@/components/analytics/MonthlyComparisonChart'; -import CategorySpendingList from '@/components/analytics/CategorySpendingList'; -import PaymentMethodChart from '@/components/analytics/PaymentMethodChart'; +import PeriodSelector from "@/components/analytics/PeriodSelector"; +import SummaryCards from "@/components/analytics/SummaryCards"; +import MonthlyComparisonChart from "@/components/analytics/MonthlyComparisonChart"; +import CategorySpendingList from "@/components/analytics/CategorySpendingList"; +import PaymentMethodChart from "@/components/analytics/PaymentMethodChart"; const Analytics = () => { - const [selectedPeriod, setSelectedPeriod] = useState('์ด๋ฒˆ ๋‹ฌ'); + const [selectedPeriod, setSelectedPeriod] = useState("์ด๋ฒˆ ๋‹ฌ"); const { budgetData, getCategorySpending, getPaymentMethodStats, // ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ๋ฉ”์„œ๋“œ - transactions + transactions, } = useBudget(); const isMobile = useIsMobile(); const [refreshTrigger, setRefreshTrigger] = useState(0); - const [monthlyData, setMonthlyData] = useState([]); + const [monthlyData, setMonthlyData] = useState([]); // ํŽ˜์ด์ง€ ๊ฐ€์‹œ์„ฑ ๋ณ€๊ฒฝ์‹œ ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ useEffect(() => { const handleVisibilityChange = () => { - if (document.visibilityState === 'visible') { - console.log('๋ถ„์„ ํŽ˜์ด์ง€ ๋ณด์ž„ - ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ'); - setRefreshTrigger(prev => prev + 1); + if (document.visibilityState === "visible") { + logger.info("๋ถ„์„ ํŽ˜์ด์ง€ ๋ณด์ž„ - ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ"); + setRefreshTrigger((prev) => prev + 1); // ์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œ์ผœ ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ try { - window.dispatchEvent(new Event('storage')); - window.dispatchEvent(new Event('transactionUpdated')); - window.dispatchEvent(new Event('budgetDataUpdated')); - window.dispatchEvent(new Event('categoryBudgetsUpdated')); + window.dispatchEvent(new Event("storage")); + window.dispatchEvent(new Event("transactionUpdated")); + window.dispatchEvent(new Event("budgetDataUpdated")); + window.dispatchEvent(new Event("categoryBudgetsUpdated")); } catch (e) { - console.error('์ด๋ฒคํŠธ ๋ฐœ์ƒ ์˜ค๋ฅ˜:', e); + logger.error("์ด๋ฒคํŠธ ๋ฐœ์ƒ ์˜ค๋ฅ˜:", e); } } }; const handleFocus = () => { - console.log('๋ถ„์„ ํŽ˜์ด์ง€ ํฌ์ปค์Šค - ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ'); - setRefreshTrigger(prev => prev + 1); + logger.info("๋ถ„์„ ํŽ˜์ด์ง€ ํฌ์ปค์Šค - ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ"); + setRefreshTrigger((prev) => prev + 1); // ์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œ์ผœ ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ try { - window.dispatchEvent(new Event('storage')); - window.dispatchEvent(new Event('transactionUpdated')); - window.dispatchEvent(new Event('budgetDataUpdated')); - window.dispatchEvent(new Event('categoryBudgetsUpdated')); + window.dispatchEvent(new Event("storage")); + window.dispatchEvent(new Event("transactionUpdated")); + window.dispatchEvent(new Event("budgetDataUpdated")); + window.dispatchEvent(new Event("categoryBudgetsUpdated")); } catch (e) { - console.error('์ด๋ฒคํŠธ ๋ฐœ์ƒ ์˜ค๋ฅ˜:', e); + logger.error("์ด๋ฒคํŠธ ๋ฐœ์ƒ ์˜ค๋ฅ˜:", e); } }; - document.addEventListener('visibilitychange', handleVisibilityChange); - window.addEventListener('focus', handleFocus); - window.addEventListener('transactionUpdated', () => setRefreshTrigger(prev => prev + 1)); - window.addEventListener('budgetDataUpdated', () => setRefreshTrigger(prev => prev + 1)); - window.addEventListener('categoryBudgetsUpdated', () => setRefreshTrigger(prev => prev + 1)); + document.addEventListener("visibilitychange", handleVisibilityChange); + window.addEventListener("focus", handleFocus); + window.addEventListener("transactionUpdated", () => + setRefreshTrigger((prev) => prev + 1) + ); + window.addEventListener("budgetDataUpdated", () => + setRefreshTrigger((prev) => prev + 1) + ); + window.addEventListener("categoryBudgetsUpdated", () => + setRefreshTrigger((prev) => prev + 1) + ); // ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ด๋ฒคํŠธ ํŠธ๋ฆฌ๊ฑฐ handleFocus(); return () => { - document.removeEventListener('visibilitychange', handleVisibilityChange); - window.removeEventListener('focus', handleFocus); - window.removeEventListener('transactionUpdated', () => {}); - window.removeEventListener('budgetDataUpdated', () => {}); - window.removeEventListener('categoryBudgetsUpdated', () => {}); + document.removeEventListener("visibilitychange", handleVisibilityChange); + window.removeEventListener("focus", handleFocus); + window.removeEventListener("transactionUpdated", () => {}); + window.removeEventListener("budgetDataUpdated", () => {}); + window.removeEventListener("categoryBudgetsUpdated", () => {}); }; }, []); @@ -82,25 +89,26 @@ const Analytics = () => { const totalBudget = budgetData?.monthly?.targetAmount || 0; const totalExpense = budgetData?.monthly?.spentAmount || 0; const savings = Math.max(0, totalBudget - totalExpense); - const savingsPercentage = totalBudget > 0 ? Math.round(savings / totalBudget * 100) : 0; + const savingsPercentage = + totalBudget > 0 ? Math.round((savings / totalBudget) * 100) : 0; // ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ง€์ถœ ์ฐจํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ - ์ƒ‰์ƒ ์œ ํ‹ธ๋ฆฌํ‹ฐ ์‚ฌ์šฉ const categorySpending = getCategorySpending(); - const expenseData = categorySpending.map(category => ({ + const expenseData = categorySpending.map((category) => ({ name: category.title, value: category.current, - color: getCategoryColor(category.title) // ์ผ๊ด€๋œ ์ƒ‰์ƒ ์ ์šฉ + color: getCategoryColor(category.title), // ์ผ๊ด€๋œ ์ƒ‰์ƒ ์ ์šฉ })); // ๊ฒฐ์ œ ๋ฐฉ๋ฒ• ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ const paymentMethodData = getPaymentMethodStats(); - const hasPaymentData = paymentMethodData.some(method => method.amount > 0); + const hasPaymentData = paymentMethodData.some((method) => method.amount > 0); // ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ - ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ œ๊ฑฐํ•˜๊ณ  ํ˜„์žฌ ๋‹ฌ๋งŒ ์‹ค์ œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ useEffect(() => { - console.log('Analytics ํŽ˜์ด์ง€: ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ', { + logger.info("Analytics ํŽ˜์ด์ง€: ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ", { totalBudget, - totalExpense + totalExpense, }); // ํ˜„์žฌ ์›” ๊ฐ€์ ธ์˜ค๊ธฐ @@ -108,56 +116,70 @@ const Analytics = () => { const currentMonth = today.getMonth(); // ํ˜„์žฌ ๋‹ฌ๋งŒ ์‹ค์ œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฐ์—ด ์ƒ์„ฑ - const monthlyDataArray = [{ - name: MONTHS_KR[currentMonth].split(' ')[0], - // '8์›”' ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ - budget: totalBudget, - expense: totalExpense - }]; + const monthlyDataArray = [ + { + name: MONTHS_KR[currentMonth].split(" ")[0], + // '8์›”' ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ + budget: totalBudget, + expense: totalExpense, + }, + ]; setMonthlyData(monthlyDataArray); - console.log('Analytics ํŽ˜์ด์ง€: ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์™„๋ฃŒ', monthlyDataArray); + logger.info("Analytics ํŽ˜์ด์ง€: ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์™„๋ฃŒ", monthlyDataArray); }, [totalBudget, totalExpense, refreshTrigger]); // ์ด์ „/๋‹ค์Œ ๊ธฐ๊ฐ„ ์ด๋™ ์ฒ˜๋ฆฌ const handlePrevPeriod = () => { - console.log('์ด์ „ ๊ธฐ๊ฐ„์œผ๋กœ ์ด๋™'); + logger.info("์ด์ „ ๊ธฐ๊ฐ„์œผ๋กœ ์ด๋™"); }; const handleNextPeriod = () => { - console.log('๋‹ค์Œ ๊ธฐ๊ฐ„์œผ๋กœ ์ด๋™'); + logger.info("๋‹ค์Œ ๊ธฐ๊ฐ„์œผ๋กœ ์ด๋™"); }; - - return
+ + return ( +
{/* Header */}

์ง€์ถœ ๋ถ„์„

- + {/* Period Selector */} - - + + {/* Summary Cards */} - +
- + {/* Monthly Comparison Chart */}

์›”๋ณ„ ๊ทธ๋ž˜ํ”„

- +
- + {/* ์นดํ…Œ๊ณ ๋ฆฌ ๋น„์œจ๊ณผ ์ง€์ถœ์„ ํ•˜๋‚˜์˜ ์นด๋“œ๋กœ ํ•ฉ์นจ */}

์นดํ…Œ๊ณ ๋ฆฌ ๋น„์œจ

- {expenseData.some(item => item.value > 0) ? ( + {expenseData.some((item) => item.value > 0) ? ( <>
{/* ์›๊ทธ๋ž˜ํ”„ ์•„๋ž˜์— ์นดํ…Œ๊ณ ๋ฆฌ ์ง€์ถœ ๋ชฉ๋ก ์ถ”๊ฐ€ */} - @@ -168,18 +190,22 @@ const Analytics = () => { )}
- + {/* ๊ฒฐ์ œ ๋ฐฉ๋ฒ• ์ฐจํŠธ ์ถ”๊ฐ€ */}

๊ฒฐ์ œ ๋ฐฉ๋ฒ• ๋น„์œจ

- - + + {/* ๊ฒฐ์ œ ๋ฐฉ๋ฒ• ์ฐจํŠธ ์•„๋ž˜ 80px ์—ฌ์œ  ๊ณต๊ฐ„ ์ถ”๊ฐ€ */}
-
; +
+ ); }; export default Analytics; diff --git a/src/pages/AppwriteSettingsPage.tsx b/src/pages/AppwriteSettingsPage.tsx index e6a69de..1404dab 100644 --- a/src/pages/AppwriteSettingsPage.tsx +++ b/src/pages/AppwriteSettingsPage.tsx @@ -1,14 +1,21 @@ -import React, { useState } from 'react'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Separator } from '@/components/ui/separator'; -import AppwriteConnectionTest from '@/components/auth/AppwriteConnectionTest'; -import SupabaseToAppwriteMigration from '@/components/migration/SupabaseToAppwriteMigration'; -import { useAppwriteAuth } from '@/hooks/auth/useAppwriteAuth'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { toast } from '@/hooks/useToast.wrapper'; +import React, { useState } from "react"; +import { appwriteLogger } from "@/utils/logger"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import AppwriteConnectionTest from "@/components/auth/AppwriteConnectionTest"; +// import SupabaseToAppwriteMigration from '@/components/migration/SupabaseToAppwriteMigration'; +import { useAppwriteAuth } from "@/hooks/auth/useAppwriteAuth"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { toast } from "@/hooks/useToast.wrapper"; /** * Appwrite ์„ค์ • ํŽ˜์ด์ง€ @@ -19,76 +26,82 @@ import { toast } from '@/hooks/useToast.wrapper'; const AppwriteSettingsPage: React.FC = () => { // ์ธ์ฆ ์ƒํƒœ const { user, login, signup, logout, loading, error } = useAppwriteAuth(); - + // ๋กœ๊ทธ์ธ ํผ ์ƒํƒœ const [loginForm, setLoginForm] = useState({ - email: '', - password: '' + email: "", + password: "", }); - + // ํšŒ์›๊ฐ€์ž… ํผ ์ƒํƒœ const [signupForm, setSignupForm] = useState({ - email: '', - password: '', - name: '' + email: "", + password: "", + name: "", }); - + // ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); - + try { await login(loginForm); toast({ - title: '๋กœ๊ทธ์ธ ์„ฑ๊ณต', - description: '์„ฑ๊ณต์ ์œผ๋กœ ๋กœ๊ทธ์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.', - variant: 'default' + title: "๋กœ๊ทธ์ธ ์„ฑ๊ณต", + description: "์„ฑ๊ณต์ ์œผ๋กœ ๋กœ๊ทธ์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + variant: "default", }); } catch (error) { - console.error('๋กœ๊ทธ์ธ ์˜ค๋ฅ˜:', error); + appwriteLogger.error("๋กœ๊ทธ์ธ ์˜ค๋ฅ˜:", error); toast({ - title: '๋กœ๊ทธ์ธ ์‹คํŒจ', - description: error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.', - variant: 'destructive' + title: "๋กœ๊ทธ์ธ ์‹คํŒจ", + description: + error instanceof Error + ? error.message + : "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + variant: "destructive", }); } }; - + // ํšŒ์›๊ฐ€์ž… ์ฒ˜๋ฆฌ const handleSignup = async (e: React.FormEvent) => { e.preventDefault(); - + try { await signup(signupForm); toast({ - title: 'ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต', - description: '์„ฑ๊ณต์ ์œผ๋กœ ๊ฐ€์ž…๋˜์—ˆ์Šต๋‹ˆ๋‹ค.', - variant: 'default' + title: "ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต", + description: "์„ฑ๊ณต์ ์œผ๋กœ ๊ฐ€์ž…๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + variant: "default", }); } catch (error) { - console.error('ํšŒ์›๊ฐ€์ž… ์˜ค๋ฅ˜:', error); + appwriteLogger.error("ํšŒ์›๊ฐ€์ž… ์˜ค๋ฅ˜:", error); toast({ - title: 'ํšŒ์›๊ฐ€์ž… ์‹คํŒจ', - description: error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.', - variant: 'destructive' + title: "ํšŒ์›๊ฐ€์ž… ์‹คํŒจ", + description: + error instanceof Error + ? error.message + : "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + variant: "destructive", }); } }; - + // ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ const handleLogout = async () => { try { await logout(); toast({ - title: '๋กœ๊ทธ์•„์›ƒ', - description: '์„ฑ๊ณต์ ์œผ๋กœ ๋กœ๊ทธ์•„์›ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.', - variant: 'default' + title: "๋กœ๊ทธ์•„์›ƒ", + description: "์„ฑ๊ณต์ ์œผ๋กœ ๋กœ๊ทธ์•„์›ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + variant: "default", }); } catch (error) { - console.error('๋กœ๊ทธ์•„์›ƒ ์˜ค๋ฅ˜:', error); + appwriteLogger.error("๋กœ๊ทธ์•„์›ƒ ์˜ค๋ฅ˜:", error); } }; - + return (
@@ -97,9 +110,9 @@ const AppwriteSettingsPage: React.FC = () => { Appwrite ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์„ค์ • ๋ฐ ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜

- + - + {/* ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์ƒํƒœ */} @@ -112,7 +125,7 @@ const AppwriteSettingsPage: React.FC = () => { - + {/* ์ธ์ฆ ๊ด€๋ฆฌ */} @@ -129,16 +142,10 @@ const AppwriteSettingsPage: React.FC = () => {

์‚ฌ์šฉ์ž ID: {user.$id}

-

- ์ด๋ฉ”์ผ: {user.email} -

- {user.name && ( -

- ์ด๋ฆ„: {user.name} -

- )} +

์ด๋ฉ”์ผ: {user.email}

+ {user.name &&

์ด๋ฆ„: {user.name}

}
- +
- +
@@ -193,11 +204,13 @@ const AppwriteSettingsPage: React.FC = () => { type="email" placeholder="์ด๋ฉ”์ผ ์ฃผ์†Œ" value={signupForm.email} - onChange={(e) => setSignupForm({ ...signupForm, email: e.target.value })} + onChange={(e) => + setSignupForm({ ...signupForm, email: e.target.value }) + } required />
- +
{ type="password" placeholder="๋น„๋ฐ€๋ฒˆํ˜ธ" value={signupForm.password} - onChange={(e) => setSignupForm({ ...signupForm, password: e.target.value })} + onChange={(e) => + setSignupForm({ + ...signupForm, + password: e.target.value, + }) + } required />
- +
{ type="text" placeholder="์ด๋ฆ„" value={signupForm.name} - onChange={(e) => setSignupForm({ ...signupForm, name: e.target.value })} + onChange={(e) => + setSignupForm({ ...signupForm, name: e.target.value }) + } />
- + @@ -228,7 +248,7 @@ const AppwriteSettingsPage: React.FC = () => { )} - + {error && (
{error.message} @@ -236,7 +256,7 @@ const AppwriteSettingsPage: React.FC = () => { )} - + {/* ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ */} @@ -246,7 +266,10 @@ const AppwriteSettingsPage: React.FC = () => { - + {/* */} +

+ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. +

diff --git a/src/pages/ForgotPassword.tsx b/src/pages/ForgotPassword.tsx index 3e6c9d7..fc98d92 100644 --- a/src/pages/ForgotPassword.tsx +++ b/src/pages/ForgotPassword.tsx @@ -14,16 +14,16 @@ const ForgotPassword = () => { const handleResetPassword = async (e: React.FormEvent) => { e.preventDefault(); - + if (!email) { return; } - + setIsLoading(true); - + try { const { error } = await resetPassword(email); - + if (!error) { setIsSent(true); } @@ -36,53 +36,60 @@ const ForgotPassword = () => {
-

๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •

+

+ ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • +

- {isSent - ? "์ด๋ฉ”์ผ์„ ํ™•์ธํ•˜์—ฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์žฌ์„ค์ •ํ•˜์„ธ์š”" + {isSent + ? "์ด๋ฉ”์ผ์„ ํ™•์ธํ•˜์—ฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์žฌ์„ค์ •ํ•˜์„ธ์š”" : "๊ฐ€์ž…ํ•œ ์ด๋ฉ”์ผ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"}

- +
{!isSent ? (
- +
- setEmail(e.target.value)} - className="pl-10 neuro-pressed" + setEmail(e.target.value)} + className="pl-10 neuro-pressed" />
- -
) : (

- ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ๋งํฌ๊ฐ€ {email}๋กœ ์ „์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ๋งํฌ๊ฐ€ {email}๋กœ + ์ „์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

- ์ด๋ฉ”์ผ์„ ํ™•์ธํ•˜์—ฌ ๋งํฌ๋ฅผ ํด๋ฆญํ•˜์„ธ์š”. ์ด๋ฉ”์ผ์ด ๋ณด์ด์ง€ ์•Š๋Š”๋‹ค๋ฉด ์ŠคํŒธํ•จ๋„ ํ™•์ธํ•ด์ฃผ์„ธ์š”. + ์ด๋ฉ”์ผ์„ ํ™•์ธํ•˜์—ฌ ๋งํฌ๋ฅผ ํด๋ฆญํ•˜์„ธ์š”. ์ด๋ฉ”์ผ์ด ๋ณด์ด์ง€ ์•Š๋Š”๋‹ค๋ฉด + ์ŠคํŒธํ•จ๋„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.

-
)}
- +
- + ๋กœ๊ทธ์ธ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ
diff --git a/src/pages/HelpSupport.tsx b/src/pages/HelpSupport.tsx index 40319a0..667f75b 100644 --- a/src/pages/HelpSupport.tsx +++ b/src/pages/HelpSupport.tsx @@ -1,76 +1,96 @@ - -import React, { useState } from 'react'; -import { ArrowLeft, HelpCircle, Book, ExternalLink, ShieldQuestion } from 'lucide-react'; -import { useNavigate } from 'react-router-dom'; -import { Button } from '@/components/ui/button'; -import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'; -import { Card } from '@/components/ui/card'; -import { toast } from 'sonner'; -import WelcomeDialog from '@/components/onboarding/WelcomeDialog'; +import React, { useState } from "react"; +import { + ArrowLeft, + HelpCircle, + Book, + ExternalLink, + ShieldQuestion, +} from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Card } from "@/components/ui/card"; +import { toast } from "sonner"; +import WelcomeDialog from "@/components/onboarding/WelcomeDialog"; const HelpSupport = () => { const navigate = useNavigate(); - const [messageText, setMessageText] = useState(''); + const [messageText, setMessageText] = useState(""); const [showWelcomeDialog, setShowWelcomeDialog] = useState(false); - + const faqItems = [ { - question: '์•ฑ์„ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋‚˜์š”?', - answer: '์•ฑ์€ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. ํ™ˆ ํ™”๋ฉด์—์„œ ์ˆ˜์ž…๊ณผ ์ง€์ถœ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๊ฑฐ๋ž˜ ๋‚ด์—ญ ํ™”๋ฉด์—์„œ ๋ชจ๋“  ๊ฑฐ๋ž˜๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ถ„์„ ํ™”๋ฉด์—์„œ๋Š” ์ง€์ถœ ํŒจํ„ด์„ ํ™•์ธํ•˜์„ธ์š”.' - }, - { - question: '์˜ˆ์‚ฐ์„ ์–ด๋–ป๊ฒŒ ์„ค์ •ํ•˜๋‚˜์š”?', - answer: 'ํ™ˆ ํ™”๋ฉด์—์„œ ์˜ˆ์‚ฐ ์นด๋“œ๋ฅผ ์ฐพ์•„ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์˜ˆ์‚ฐ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ ์นดํ…Œ๊ณ ๋ฆฌ์— ์›ํ•˜๋Š” ๊ธˆ์•ก์„ ์ž…๋ ฅํ•˜์—ฌ ์›”๋ณ„ ์˜ˆ์‚ฐ์„ ๊ด€๋ฆฌํ•˜์„ธ์š”.' + question: "์•ฑ์„ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋‚˜์š”?", + answer: + "์•ฑ์€ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. ํ™ˆ ํ™”๋ฉด์—์„œ ์ˆ˜์ž…๊ณผ ์ง€์ถœ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๊ฑฐ๋ž˜ ๋‚ด์—ญ ํ™”๋ฉด์—์„œ ๋ชจ๋“  ๊ฑฐ๋ž˜๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ถ„์„ ํ™”๋ฉด์—์„œ๋Š” ์ง€์ถœ ํŒจํ„ด์„ ํ™•์ธํ•˜์„ธ์š”.", }, { - question: '์ง€์ถœ์„ ์–ด๋–ป๊ฒŒ ์ถ”๊ฐ€ํ•˜๋‚˜์š”?', - answer: 'ํ™ˆ ํ™”๋ฉด ํ•˜๋‹จ์˜ "+" ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์ƒˆ ๊ฑฐ๋ž˜๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ธˆ์•ก, ์นดํ…Œ๊ณ ๋ฆฌ, ๋‚ ์งœ๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ์ €์žฅํ•˜์„ธ์š”.' - }, + question: "์˜ˆ์‚ฐ์„ ์–ด๋–ป๊ฒŒ ์„ค์ •ํ•˜๋‚˜์š”?", + answer: + "ํ™ˆ ํ™”๋ฉด์—์„œ ์˜ˆ์‚ฐ ์นด๋“œ๋ฅผ ์ฐพ์•„ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์˜ˆ์‚ฐ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ ์นดํ…Œ๊ณ ๋ฆฌ์— ์›ํ•˜๋Š” ๊ธˆ์•ก์„ ์ž…๋ ฅํ•˜์—ฌ ์›”๋ณ„ ์˜ˆ์‚ฐ์„ ๊ด€๋ฆฌํ•˜์„ธ์š”.", + }, { - question: '๊ณ„์ • ์ •๋ณด๋ฅผ ์–ด๋–ป๊ฒŒ ๋ณ€๊ฒฝํ•˜๋‚˜์š”?', - answer: '์„ค์ • > ํ”„๋กœํ•„ ๊ด€๋ฆฌ ๋ฉ”๋‰ด์—์„œ ์ด๋ฆ„, ์ด๋ฉ”์ผ, ์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ์˜ ๊ฐœ์ธ ์ •๋ณด๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.' - }, + question: "์ง€์ถœ์„ ์–ด๋–ป๊ฒŒ ์ถ”๊ฐ€ํ•˜๋‚˜์š”?", + answer: + 'ํ™ˆ ํ™”๋ฉด ํ•˜๋‹จ์˜ "+" ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์ƒˆ ๊ฑฐ๋ž˜๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ธˆ์•ก, ์นดํ…Œ๊ณ ๋ฆฌ, ๋‚ ์งœ๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ์ €์žฅํ•˜์„ธ์š”.', + }, { - question: '์•Œ๋ฆผ ์„ค์ •์€ ์–ด๋””์„œ ๋ณ€๊ฒฝํ•˜๋‚˜์š”?', - answer: '์„ค์ • > ์•Œ๋ฆผ ์„ค์ • ๋ฉ”๋‰ด์—์„œ ์›ํ•˜๋Š” ์•Œ๋ฆผ ์œ ํ˜•์„ ์ผœ๊ฑฐ๋‚˜ ๋Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.' - } + question: "๊ณ„์ • ์ •๋ณด๋ฅผ ์–ด๋–ป๊ฒŒ ๋ณ€๊ฒฝํ•˜๋‚˜์š”?", + answer: + "์„ค์ • > ํ”„๋กœํ•„ ๊ด€๋ฆฌ ๋ฉ”๋‰ด์—์„œ ์ด๋ฆ„, ์ด๋ฉ”์ผ, ์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ์˜ ๊ฐœ์ธ ์ •๋ณด๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + }, + { + question: "์•Œ๋ฆผ ์„ค์ •์€ ์–ด๋””์„œ ๋ณ€๊ฒฝํ•˜๋‚˜์š”?", + answer: + "์„ค์ • > ์•Œ๋ฆผ ์„ค์ • ๋ฉ”๋‰ด์—์„œ ์›ํ•˜๋Š” ์•Œ๋ฆผ ์œ ํ˜•์„ ์ผœ๊ฑฐ๋‚˜ ๋Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + }, ]; - + const sendMessage = () => { if (!messageText.trim()) { - toast.error('๋ฉ”์‹œ์ง€๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.'); + toast.error("๋ฉ”์‹œ์ง€๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."); return; } - - toast.success('๋ฌธ์˜๊ฐ€ ์ ‘์ˆ˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋น ๋ฅธ ์‹œ์ผ ๋‚ด์— ๋‹ต๋ณ€ ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.'); - setMessageText(''); + + toast.success("๋ฌธ์˜๊ฐ€ ์ ‘์ˆ˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋น ๋ฅธ ์‹œ์ผ ๋‚ด์— ๋‹ต๋ณ€ ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค."); + setMessageText(""); }; - + const handleShowWelcomeDialog = () => { setShowWelcomeDialog(true); }; - + const handleCloseWelcomeDialog = (dontShowAgain: boolean) => { setShowWelcomeDialog(false); - + if (dontShowAgain) { - toast.info('ํ™˜์˜ ํ™”๋ฉด์ด ๋” ์ด์ƒ ํ‘œ์‹œ๋˜์ง€ ์•Š๋„๋ก ์„ค์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + toast.info("ํ™˜์˜ ํ™”๋ฉด์ด ๋” ์ด์ƒ ํ‘œ์‹œ๋˜์ง€ ์•Š๋„๋ก ์„ค์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); } }; - + return (
{/* Header */}
-

๋„์›€๋ง ๋ฐ ์ง€์›

- + {/* Help Categories */}
@@ -79,8 +99,8 @@ const HelpSupport = () => {

์ž์ฃผ ๋ฌป๋Š” ์งˆ๋ฌธ

- - @@ -90,14 +110,16 @@ const HelpSupport = () => {

์ดˆ๊ธฐ ํ™”๋ฉด ๋ณด๊ธฐ

- + {/* FAQ Section */}

์ž์ฃผ ๋ฌป๋Š” ์งˆ๋ฌธ

{faqItems.map((item, index) => ( - {item.question} + + {item.question} + {item.answer} @@ -105,41 +127,50 @@ const HelpSupport = () => { ))}
- + {/* Contact Support */}

๊ฑด์˜์‚ฌํ•ญ ๋ฐ ๋ฌธ์˜ํ•˜๊ธฐ

-