From c6b605200d7b8ce6991d6ec0ecd3733dd517ddb0 Mon Sep 17 00:00:00 2001 From: byeongcheolryu Date: Fri, 19 Dec 2025 19:12:34 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=8B=A0=EA=B7=9C=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20HR/=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 신규 페이지: - 회계관리: 거래처, 예상비용, 청구서, 발주서 - 게시판: 공지사항, 자료실, 커뮤니티 - 고객센터: 문의/FAQ - 설정: 계정, 알림, 출퇴근, 팝업, 구독, 결제내역 - 리포트 (차트 시각화) - 개발자 테스트 URL 페이지 기능 개선: - HR 직원관리/휴가관리/카드관리 강화 - IntegratedListTemplateV2 확장 - AuthenticatedLayout 패딩 표준화 - 로그인 페이지 UI 개선 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- claudedocs/[REF] all-pages-test-urls.md | 336 +++++-- claudedocs/_index.md | 33 +- .../[IMPL-2025-12-18] bill-management.md | 65 ++ ...-2025-12-18] expected-expense-checklist.md | 38 + .../[IMPL-2025-12-18] purchase-management.md | 111 ++ .../[IMPL-2025-12-18] receivables-status.md | 73 ++ .../[IMPL-2025-12-18] vendor-ledger.md | 129 +++ ...2025-12-18] vendor-management-checklist.md | 287 ++++++ ...-12-18] withdrawal-management-checklist.md | 142 +++ ...5-12-19] bad-debt-collection-management.md | 230 +++++ ...PL-2025-12-19] card-transaction-inquiry.md | 89 ++ .../[PLAN-2025-12-18] sales-management.md | 270 +++++ ...12-19] bank-account-transaction-inquiry.md | 204 ++++ ...-12-19] board-management-implementation.md | 313 ++++++ .../[IMPL-2025-12-19] inquiry-management.md | 89 ++ ...E-2025-12-16] options-vs-flattened-data.md | 222 ++++ ...2025-12-19] page-layout-standardization.md | 169 ++++ .../hr/[IMPL-2025-12-16] mobile-attendance.md | 206 ++++ .../hr/[IMPL-2025-12-19] card-management.md | 86 ++ ...-12-16] dynamicitemform-hook-extraction.md | 287 +++--- .../[IMPL-2025-12-19] account-info.md | 76 ++ ...025-12-19] account-management-checklist.md | 125 +++ .../[IMPL-2025-12-19] company-info.md | 83 ++ .../[IMPL-2025-12-19] popup-management.md | 71 ++ ...MPL-2025-12-19] subscription-management.md | 71 ++ package-lock.json | 815 ++++++++++++++- package.json | 9 + .../bad-debt-collection/[id]/edit/page.tsx | 11 + .../bad-debt-collection/[id]/page.tsx | 11 + .../bad-debt-collection/new/page.tsx | 7 + .../accounting/bad-debt-collection/page.tsx | 7 + .../accounting/bank-transactions/page.tsx | 7 + .../accounting/bills/[id]/page.tsx | 13 + .../(protected)/accounting/bills/new/page.tsx | 7 + .../(protected)/accounting/bills/page.tsx | 12 + .../accounting/card-transactions/page.tsx | 5 + .../accounting/daily-report/page.tsx | 7 + .../accounting/deposits/[id]/page.tsx | 13 + .../(protected)/accounting/deposits/page.tsx | 5 + .../accounting/expected-expenses/page.tsx | 5 + .../accounting/purchase/[id]/page.tsx | 13 + .../(protected)/accounting/purchase/page.tsx | 5 + .../accounting/receivables-status/page.tsx | 11 + .../accounting/sales/[id]/page.tsx | 13 + .../(protected)/accounting/sales/new/page.tsx | 5 + .../(protected)/accounting/sales/page.tsx | 5 + .../accounting/vendor-ledger/[id]/page.tsx | 11 + .../accounting/vendor-ledger/page.tsx | 7 + .../accounting/vendors/[id]/page.tsx | 13 + .../accounting/vendors/new/page.tsx | 7 + .../(protected)/accounting/vendors/page.tsx | 7 + .../accounting/withdrawals/[id]/page.tsx | 13 + .../accounting/withdrawals/page.tsx | 5 + .../(protected)/board/[id]/edit/page.tsx | 57 ++ .../[locale]/(protected)/board/[id]/page.tsx | 94 ++ .../board/board-management/[id]/edit/page.tsx | 55 + .../board/board-management/[id]/page.tsx | 100 ++ .../board/board-management/new/page.tsx | 22 + .../board/board-management/page.tsx | 7 + .../(protected)/board/create/page.tsx | 14 + src/app/[locale]/(protected)/board/page.tsx | 14 + .../(protected)/company-info/page.tsx | 5 + .../customer-center/events/[id]/page.tsx | 22 + .../customer-center/events/page.tsx | 7 + .../(protected)/customer-center/faq/page.tsx | 7 + .../inquiries/[id]/edit/page.tsx | 27 + .../customer-center/inquiries/[id]/page.tsx | 43 + .../customer-center/inquiries/create/page.tsx | 14 + .../customer-center/inquiries/page.tsx | 14 + .../customer-center/notices/[id]/page.tsx | 22 + .../customer-center/notices/page.tsx | 7 + .../dev/test-urls/TestUrlsClient.tsx | 267 +++++ .../(protected)/dev/test-urls/page.tsx | 167 ++++ .../(protected)/hr/attendance/page.tsx | 295 ++++++ .../hr/card-management/[id]/edit/page.tsx | 65 ++ .../hr/card-management/[id]/page.tsx | 110 ++ .../hr/card-management/new/page.tsx | 22 + .../(protected)/hr/card-management/page.tsx | 7 + .../hr/employee-management/[id]/edit/page.tsx | 4 + .../hr/employee-management/[id]/page.tsx | 4 + .../(protected)/items/[id]/edit/page.tsx | 2 +- .../[locale]/(protected)/items/[id]/page.tsx | 2 +- .../(protected)/items/create/page.tsx | 2 +- src/app/[locale]/(protected)/items/page.tsx | 6 +- .../(protected)/payment-history/page.tsx | 5 + .../screen-production/[id]/edit/page.tsx | 4 +- .../screen-production/[id]/page.tsx | 2 +- .../screen-production/create/page.tsx | 2 +- .../production/screen-production/page.tsx | 8 +- .../reports/comprehensive-analysis/page.tsx | 5 + src/app/[locale]/(protected)/reports/page.tsx | 6 + .../sales/pricing-management/page.tsx | 6 + .../settings/account-info/page.tsx | 5 + .../settings/accounts/[id]/page.tsx | 129 +++ .../settings/accounts/new/page.tsx | 7 + .../(protected)/settings/accounts/page.tsx | 7 + .../settings/attendance-settings/page.tsx | 5 + .../settings/notification-settings/page.tsx | 5 + .../popup-management/[id]/edit/page.tsx | 23 + .../settings/popup-management/[id]/page.tsx | 89 ++ .../settings/popup-management/new/page.tsx | 5 + .../settings/popup-management/page.tsx | 5 + .../(protected)/subscription/page.tsx | 7 + .../BadDebtCollection/BadDebtDetail.tsx | 944 ++++++++++++++++++ .../accounting/BadDebtCollection/index.tsx | 495 +++++++++ .../accounting/BadDebtCollection/types.ts | 121 +++ .../BankTransactionInquiry/index.tsx | 508 ++++++++++ .../BankTransactionInquiry/types.ts | 130 +++ .../accounting/BillManagement/BillDetail.tsx | 472 +++++++++ .../accounting/BillManagement/index.tsx | 527 ++++++++++ .../accounting/BillManagement/types.ts | 171 ++++ .../CardTransactionInquiry/index.tsx | 332 ++++++ .../CardTransactionInquiry/types.ts | 26 + .../accounting/DailyReport/index.tsx | 295 ++++++ .../accounting/DailyReport/types.ts | 66 ++ .../DepositManagement/DepositDetail.tsx | 313 ++++++ .../accounting/DepositManagement/index.tsx | 613 ++++++++++++ .../accounting/DepositManagement/types.ts | 137 +++ .../ExpectedExpenseManagement/index.tsx | 886 ++++++++++++++++ .../ExpectedExpenseManagement/types.ts | 120 +++ .../PurchaseManagement/PurchaseDetail.tsx | 731 ++++++++++++++ .../PurchaseDetailModal.tsx | 381 +++++++ .../accounting/PurchaseManagement/index.tsx | 656 ++++++++++++ .../accounting/PurchaseManagement/types.ts | 179 ++++ .../accounting/ReceivablesStatus/index.tsx | 378 +++++++ .../accounting/ReceivablesStatus/types.ts | 81 ++ .../SalesManagement/SalesDetail.tsx | 595 +++++++++++ .../accounting/SalesManagement/index.tsx | 687 +++++++++++++ .../accounting/SalesManagement/types.ts | 164 +++ .../VendorLedger/VendorLedgerDetail.tsx | 346 +++++++ .../accounting/VendorLedger/index.tsx | 365 +++++++ .../accounting/VendorLedger/types.ts | 61 ++ .../VendorManagement/VendorDetail.tsx | 620 ++++++++++++ .../accounting/VendorManagement/index.tsx | 538 ++++++++++ .../accounting/VendorManagement/types.ts | 218 ++++ .../WithdrawalManagement/WithdrawalDetail.tsx | 319 ++++++ .../accounting/WithdrawalManagement/index.tsx | 605 +++++++++++ .../accounting/WithdrawalManagement/types.ts | 116 +++ .../attendance/AttendanceComplete.tsx | 83 ++ src/components/attendance/GoogleMap.tsx | 316 ++++++ src/components/auth/LoginPage.tsx | 40 +- src/components/board/BoardDetail/index.tsx | 228 +++++ src/components/board/BoardForm/index.tsx | 447 +++++++++ src/components/board/BoardList/index.tsx | 471 +++++++++ .../board/BoardManagement/BoardDetail.tsx | 116 +++ .../board/BoardManagement/BoardForm.tsx | 221 ++++ .../board/BoardManagement/index.tsx | 467 +++++++++ src/components/board/BoardManagement/types.ts | 58 ++ .../board/CommentSection/CommentItem.tsx | 182 ++++ src/components/board/CommentSection/index.tsx | 90 ++ .../board/RichTextEditor/MenuBar.tsx | 287 ++++++ .../board/RichTextEditor/extensions.ts | 48 + src/components/board/RichTextEditor/index.tsx | 90 ++ src/components/board/types.ts | 120 +++ .../EventManagement/EventDetail.tsx | 116 +++ .../EventManagement/EventList.tsx | 303 ++++++ .../customer-center/EventManagement/index.tsx | 3 + .../customer-center/EventManagement/types.ts | 182 ++++ .../customer-center/FAQManagement/FAQList.tsx | 148 +++ .../customer-center/FAQManagement/index.tsx | 2 + .../customer-center/FAQManagement/types.ts | 90 ++ .../InquiryManagement/InquiryDetail.tsx | 373 +++++++ .../InquiryManagement/InquiryForm.tsx | 302 ++++++ .../InquiryManagement/InquiryList.tsx | 336 +++++++ .../InquiryManagement/index.tsx | 4 + .../InquiryManagement/types.ts | 211 ++++ .../NoticeManagement/NoticeDetail.tsx | 116 +++ .../NoticeManagement/NoticeList.tsx | 260 +++++ .../NoticeManagement/index.tsx | 3 + .../customer-center/NoticeManagement/types.ts | 184 ++++ .../hr/AttendanceManagement/index.tsx | 166 +-- .../hr/AttendanceManagement/types.ts | 28 +- .../hr/CardManagement/CardDetail.tsx | 132 +++ src/components/hr/CardManagement/CardForm.tsx | 234 +++++ src/components/hr/CardManagement/index.tsx | 461 +++++++++ src/components/hr/CardManagement/types.ts | 72 ++ .../hr/EmployeeManagement/EmployeeDetail.tsx | 24 + .../hr/EmployeeManagement/EmployeeForm.tsx | 527 +++++----- .../FieldSettingsDialog.tsx | 38 +- .../hr/EmployeeManagement/index.tsx | 189 +++- src/components/hr/EmployeeManagement/types.ts | 16 + .../hr/VacationManagement/index.tsx | 267 +++-- src/components/hr/VacationManagement/types.ts | 30 +- .../molecules/DateRangeSelector.tsx | 176 ++++ src/components/pricing/actions.ts | 52 +- .../reports/ComprehensiveAnalysis/index.tsx | 346 +++++++ src/components/reports/mockData.ts | 238 +++++ src/components/reports/types.ts | 85 ++ .../settings/AccountInfoManagement/index.tsx | 435 ++++++++ .../settings/AccountInfoManagement/types.ts | 44 + .../AccountManagement/AccountDetail.tsx | 343 +++++++ .../settings/AccountManagement/index.tsx | 383 +++++++ .../settings/AccountManagement/types.ts | 80 ++ .../AttendanceSettingsManagement/index.tsx | 226 +++++ .../AttendanceSettingsManagement/types.ts | 55 + .../AddCompanyDialog.tsx | 149 +++ .../settings/CompanyInfoManagement/index.tsx | 488 +++++++++ .../settings/CompanyInfoManagement/types.ts | 84 ++ .../settings/NotificationSettings/index.tsx | 452 +++++++++ .../settings/NotificationSettings/types.ts | 121 +++ .../PaymentHistoryManagement/index.tsx | 281 ++++++ .../PaymentHistoryManagement/types.ts | 25 + .../settings/PopupManagement/PopupDetail.tsx | 124 +++ .../settings/PopupManagement/PopupForm.tsx | 291 ++++++ .../settings/PopupManagement/PopupList.tsx | 329 ++++++ .../settings/PopupManagement/index.tsx | 4 + .../settings/PopupManagement/types.ts | 136 +++ .../settings/SubscriptionManagement/index.tsx | 216 ++++ .../settings/SubscriptionManagement/types.ts | 31 + .../templates/IntegratedListTemplateV2.tsx | 138 ++- src/components/ui/multi-select-combobox.tsx | 126 +++ src/layouts/AuthenticatedLayout.tsx | 19 +- tsconfig.tsbuildinfo | 2 +- 213 files changed, 32644 insertions(+), 775 deletions(-) create mode 100644 claudedocs/accounting/[IMPL-2025-12-18] bill-management.md create mode 100644 claudedocs/accounting/[IMPL-2025-12-18] expected-expense-checklist.md create mode 100644 claudedocs/accounting/[IMPL-2025-12-18] purchase-management.md create mode 100644 claudedocs/accounting/[IMPL-2025-12-18] receivables-status.md create mode 100644 claudedocs/accounting/[IMPL-2025-12-18] vendor-ledger.md create mode 100644 claudedocs/accounting/[IMPL-2025-12-18] vendor-management-checklist.md create mode 100644 claudedocs/accounting/[IMPL-2025-12-18] withdrawal-management-checklist.md create mode 100644 claudedocs/accounting/[IMPL-2025-12-19] bad-debt-collection-management.md create mode 100644 claudedocs/accounting/[IMPL-2025-12-19] card-transaction-inquiry.md create mode 100644 claudedocs/accounting/[PLAN-2025-12-18] sales-management.md create mode 100644 claudedocs/accounting/[PLAN-2025-12-19] bank-account-transaction-inquiry.md create mode 100644 claudedocs/board/[PLAN-2025-12-19] board-management-implementation.md create mode 100644 claudedocs/customer-center/[IMPL-2025-12-19] inquiry-management.md create mode 100644 claudedocs/guides/[GUIDE-2025-12-16] options-vs-flattened-data.md create mode 100644 claudedocs/guides/[PLAN-2025-12-19] page-layout-standardization.md create mode 100644 claudedocs/hr/[IMPL-2025-12-16] mobile-attendance.md create mode 100644 claudedocs/hr/[IMPL-2025-12-19] card-management.md create mode 100644 claudedocs/settings/[IMPL-2025-12-19] account-info.md create mode 100644 claudedocs/settings/[IMPL-2025-12-19] account-management-checklist.md create mode 100644 claudedocs/settings/[IMPL-2025-12-19] company-info.md create mode 100644 claudedocs/settings/[IMPL-2025-12-19] popup-management.md create mode 100644 claudedocs/settings/[IMPL-2025-12-19] subscription-management.md create mode 100644 src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/edit/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/bad-debt-collection/new/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/bank-transactions/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/bills/[id]/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/bills/new/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/bills/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/card-transactions/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/daily-report/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/deposits/[id]/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/deposits/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/expected-expenses/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/purchase/[id]/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/purchase/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/receivables-status/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/sales/[id]/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/sales/new/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/sales/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/vendor-ledger/[id]/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/vendor-ledger/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/vendors/[id]/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/vendors/new/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/vendors/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/withdrawals/[id]/page.tsx create mode 100644 src/app/[locale]/(protected)/accounting/withdrawals/page.tsx create mode 100644 src/app/[locale]/(protected)/board/[id]/edit/page.tsx create mode 100644 src/app/[locale]/(protected)/board/[id]/page.tsx create mode 100644 src/app/[locale]/(protected)/board/board-management/[id]/edit/page.tsx create mode 100644 src/app/[locale]/(protected)/board/board-management/[id]/page.tsx create mode 100644 src/app/[locale]/(protected)/board/board-management/new/page.tsx create mode 100644 src/app/[locale]/(protected)/board/board-management/page.tsx create mode 100644 src/app/[locale]/(protected)/board/create/page.tsx create mode 100644 src/app/[locale]/(protected)/board/page.tsx create mode 100644 src/app/[locale]/(protected)/company-info/page.tsx create mode 100644 src/app/[locale]/(protected)/customer-center/events/[id]/page.tsx create mode 100644 src/app/[locale]/(protected)/customer-center/events/page.tsx create mode 100644 src/app/[locale]/(protected)/customer-center/faq/page.tsx create mode 100644 src/app/[locale]/(protected)/customer-center/inquiries/[id]/edit/page.tsx create mode 100644 src/app/[locale]/(protected)/customer-center/inquiries/[id]/page.tsx create mode 100644 src/app/[locale]/(protected)/customer-center/inquiries/create/page.tsx create mode 100644 src/app/[locale]/(protected)/customer-center/inquiries/page.tsx create mode 100644 src/app/[locale]/(protected)/customer-center/notices/[id]/page.tsx create mode 100644 src/app/[locale]/(protected)/customer-center/notices/page.tsx create mode 100644 src/app/[locale]/(protected)/dev/test-urls/TestUrlsClient.tsx create mode 100644 src/app/[locale]/(protected)/dev/test-urls/page.tsx create mode 100644 src/app/[locale]/(protected)/hr/attendance/page.tsx create mode 100644 src/app/[locale]/(protected)/hr/card-management/[id]/edit/page.tsx create mode 100644 src/app/[locale]/(protected)/hr/card-management/[id]/page.tsx create mode 100644 src/app/[locale]/(protected)/hr/card-management/new/page.tsx create mode 100644 src/app/[locale]/(protected)/hr/card-management/page.tsx create mode 100644 src/app/[locale]/(protected)/payment-history/page.tsx create mode 100644 src/app/[locale]/(protected)/reports/comprehensive-analysis/page.tsx create mode 100644 src/app/[locale]/(protected)/reports/page.tsx create mode 100644 src/app/[locale]/(protected)/settings/account-info/page.tsx create mode 100644 src/app/[locale]/(protected)/settings/accounts/[id]/page.tsx create mode 100644 src/app/[locale]/(protected)/settings/accounts/new/page.tsx create mode 100644 src/app/[locale]/(protected)/settings/accounts/page.tsx create mode 100644 src/app/[locale]/(protected)/settings/attendance-settings/page.tsx create mode 100644 src/app/[locale]/(protected)/settings/notification-settings/page.tsx create mode 100644 src/app/[locale]/(protected)/settings/popup-management/[id]/edit/page.tsx create mode 100644 src/app/[locale]/(protected)/settings/popup-management/[id]/page.tsx create mode 100644 src/app/[locale]/(protected)/settings/popup-management/new/page.tsx create mode 100644 src/app/[locale]/(protected)/settings/popup-management/page.tsx create mode 100644 src/app/[locale]/(protected)/subscription/page.tsx create mode 100644 src/components/accounting/BadDebtCollection/BadDebtDetail.tsx create mode 100644 src/components/accounting/BadDebtCollection/index.tsx create mode 100644 src/components/accounting/BadDebtCollection/types.ts create mode 100644 src/components/accounting/BankTransactionInquiry/index.tsx create mode 100644 src/components/accounting/BankTransactionInquiry/types.ts create mode 100644 src/components/accounting/BillManagement/BillDetail.tsx create mode 100644 src/components/accounting/BillManagement/index.tsx create mode 100644 src/components/accounting/BillManagement/types.ts create mode 100644 src/components/accounting/CardTransactionInquiry/index.tsx create mode 100644 src/components/accounting/CardTransactionInquiry/types.ts create mode 100644 src/components/accounting/DailyReport/index.tsx create mode 100644 src/components/accounting/DailyReport/types.ts create mode 100644 src/components/accounting/DepositManagement/DepositDetail.tsx create mode 100644 src/components/accounting/DepositManagement/index.tsx create mode 100644 src/components/accounting/DepositManagement/types.ts create mode 100644 src/components/accounting/ExpectedExpenseManagement/index.tsx create mode 100644 src/components/accounting/ExpectedExpenseManagement/types.ts create mode 100644 src/components/accounting/PurchaseManagement/PurchaseDetail.tsx create mode 100644 src/components/accounting/PurchaseManagement/PurchaseDetailModal.tsx create mode 100644 src/components/accounting/PurchaseManagement/index.tsx create mode 100644 src/components/accounting/PurchaseManagement/types.ts create mode 100644 src/components/accounting/ReceivablesStatus/index.tsx create mode 100644 src/components/accounting/ReceivablesStatus/types.ts create mode 100644 src/components/accounting/SalesManagement/SalesDetail.tsx create mode 100644 src/components/accounting/SalesManagement/index.tsx create mode 100644 src/components/accounting/SalesManagement/types.ts create mode 100644 src/components/accounting/VendorLedger/VendorLedgerDetail.tsx create mode 100644 src/components/accounting/VendorLedger/index.tsx create mode 100644 src/components/accounting/VendorLedger/types.ts create mode 100644 src/components/accounting/VendorManagement/VendorDetail.tsx create mode 100644 src/components/accounting/VendorManagement/index.tsx create mode 100644 src/components/accounting/VendorManagement/types.ts create mode 100644 src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx create mode 100644 src/components/accounting/WithdrawalManagement/index.tsx create mode 100644 src/components/accounting/WithdrawalManagement/types.ts create mode 100644 src/components/attendance/AttendanceComplete.tsx create mode 100644 src/components/attendance/GoogleMap.tsx create mode 100644 src/components/board/BoardDetail/index.tsx create mode 100644 src/components/board/BoardForm/index.tsx create mode 100644 src/components/board/BoardList/index.tsx create mode 100644 src/components/board/BoardManagement/BoardDetail.tsx create mode 100644 src/components/board/BoardManagement/BoardForm.tsx create mode 100644 src/components/board/BoardManagement/index.tsx create mode 100644 src/components/board/BoardManagement/types.ts create mode 100644 src/components/board/CommentSection/CommentItem.tsx create mode 100644 src/components/board/CommentSection/index.tsx create mode 100644 src/components/board/RichTextEditor/MenuBar.tsx create mode 100644 src/components/board/RichTextEditor/extensions.ts create mode 100644 src/components/board/RichTextEditor/index.tsx create mode 100644 src/components/board/types.ts create mode 100644 src/components/customer-center/EventManagement/EventDetail.tsx create mode 100644 src/components/customer-center/EventManagement/EventList.tsx create mode 100644 src/components/customer-center/EventManagement/index.tsx create mode 100644 src/components/customer-center/EventManagement/types.ts create mode 100644 src/components/customer-center/FAQManagement/FAQList.tsx create mode 100644 src/components/customer-center/FAQManagement/index.tsx create mode 100644 src/components/customer-center/FAQManagement/types.ts create mode 100644 src/components/customer-center/InquiryManagement/InquiryDetail.tsx create mode 100644 src/components/customer-center/InquiryManagement/InquiryForm.tsx create mode 100644 src/components/customer-center/InquiryManagement/InquiryList.tsx create mode 100644 src/components/customer-center/InquiryManagement/index.tsx create mode 100644 src/components/customer-center/InquiryManagement/types.ts create mode 100644 src/components/customer-center/NoticeManagement/NoticeDetail.tsx create mode 100644 src/components/customer-center/NoticeManagement/NoticeList.tsx create mode 100644 src/components/customer-center/NoticeManagement/index.tsx create mode 100644 src/components/customer-center/NoticeManagement/types.ts create mode 100644 src/components/hr/CardManagement/CardDetail.tsx create mode 100644 src/components/hr/CardManagement/CardForm.tsx create mode 100644 src/components/hr/CardManagement/index.tsx create mode 100644 src/components/hr/CardManagement/types.ts create mode 100644 src/components/molecules/DateRangeSelector.tsx create mode 100644 src/components/reports/ComprehensiveAnalysis/index.tsx create mode 100644 src/components/reports/mockData.ts create mode 100644 src/components/reports/types.ts create mode 100644 src/components/settings/AccountInfoManagement/index.tsx create mode 100644 src/components/settings/AccountInfoManagement/types.ts create mode 100644 src/components/settings/AccountManagement/AccountDetail.tsx create mode 100644 src/components/settings/AccountManagement/index.tsx create mode 100644 src/components/settings/AccountManagement/types.ts create mode 100644 src/components/settings/AttendanceSettingsManagement/index.tsx create mode 100644 src/components/settings/AttendanceSettingsManagement/types.ts create mode 100644 src/components/settings/CompanyInfoManagement/AddCompanyDialog.tsx create mode 100644 src/components/settings/CompanyInfoManagement/index.tsx create mode 100644 src/components/settings/CompanyInfoManagement/types.ts create mode 100644 src/components/settings/NotificationSettings/index.tsx create mode 100644 src/components/settings/NotificationSettings/types.ts create mode 100644 src/components/settings/PaymentHistoryManagement/index.tsx create mode 100644 src/components/settings/PaymentHistoryManagement/types.ts create mode 100644 src/components/settings/PopupManagement/PopupDetail.tsx create mode 100644 src/components/settings/PopupManagement/PopupForm.tsx create mode 100644 src/components/settings/PopupManagement/PopupList.tsx create mode 100644 src/components/settings/PopupManagement/index.tsx create mode 100644 src/components/settings/PopupManagement/types.ts create mode 100644 src/components/settings/SubscriptionManagement/index.tsx create mode 100644 src/components/settings/SubscriptionManagement/types.ts create mode 100644 src/components/ui/multi-select-combobox.tsx diff --git a/claudedocs/[REF] all-pages-test-urls.md b/claudedocs/[REF] all-pages-test-urls.md index 9bc05fa0..6f3513c6 100644 --- a/claudedocs/[REF] all-pages-test-urls.md +++ b/claudedocs/[REF] all-pages-test-urls.md @@ -1,6 +1,12 @@ # 전체 페이지 테스트 URL 목록 -> 백엔드 메뉴 연동 전 테스트용 직접 접근 URL (Last Updated: 2025-12-08) +> 백엔드 메뉴 연동 전 테스트용 직접 접근 URL (Last Updated: 2025-12-19) + +## 🚀 클릭 가능한 웹 페이지 + +👉 **[테스트 URL 페이지 열기](http://localhost:3000/ko/dev/test-urls)** + +위 링크에서 모든 URL을 클릭하여 새 탭으로 열 수 있습니다! --- @@ -24,8 +30,6 @@ http://localhost:3000/ko/dashboard ## 👥 인사관리 (HR) -### 메인 페이지 - | 페이지 | URL | 상태 | |--------|-----|------| | 부서관리 | `/ko/hr/department-management` | ✅ | @@ -33,6 +37,7 @@ http://localhost:3000/ko/dashboard | 근태관리 | `/ko/hr/attendance-management` | ✅ | | 휴가관리 | `/ko/hr/vacation-management` | ✅ | | 급여관리 | `/ko/hr/salary-management` | ✅ | +| **모바일 출퇴근** | `/ko/hr/attendance` | 🧪 테스트중 | ``` http://localhost:3000/ko/hr/department-management @@ -40,30 +45,13 @@ http://localhost:3000/ko/hr/employee-management http://localhost:3000/ko/hr/attendance-management http://localhost:3000/ko/hr/vacation-management http://localhost:3000/ko/hr/salary-management -``` - -### 사원관리 하위 페이지 - -| 페이지 | URL | -|--------|-----| -| 사원 등록 | `/ko/hr/employee-management/new` | -| 사원 상세 | `/ko/hr/employee-management/[id]` | -| 사원 수정 | `/ko/hr/employee-management/[id]/edit` | -| CSV 업로드 | `/ko/hr/employee-management/csv-upload` | - -``` -http://localhost:3000/ko/hr/employee-management/new -http://localhost:3000/ko/hr/employee-management/1 -http://localhost:3000/ko/hr/employee-management/1/edit -http://localhost:3000/ko/hr/employee-management/csv-upload +http://localhost:3000/ko/hr/attendance # 🧪 모바일 출퇴근 (테스트) ``` --- ## 💰 판매관리 (Sales) -### 메인 페이지 - | 페이지 | URL | 상태 | |--------|-----|------| | 거래처관리 | `/ko/sales/client-management-sales-admin` | ✅ | @@ -76,94 +64,28 @@ http://localhost:3000/ko/sales/quote-management http://localhost:3000/ko/sales/pricing-management ``` -### 거래처관리 하위 페이지 - -| 페이지 | URL | -|--------|-----| -| 거래처 등록 | `/ko/sales/client-management-sales-admin/new` | -| 거래처 상세 | `/ko/sales/client-management-sales-admin/[id]` | -| 거래처 수정 | `/ko/sales/client-management-sales-admin/[id]/edit` | - -``` -http://localhost:3000/ko/sales/client-management-sales-admin/new -http://localhost:3000/ko/sales/client-management-sales-admin/1 -http://localhost:3000/ko/sales/client-management-sales-admin/1/edit -``` - -### 견적관리 하위 페이지 - -| 페이지 | URL | -|--------|-----| -| 견적 등록 | `/ko/sales/quote-management/new` | -| 견적 상세 | `/ko/sales/quote-management/[id]` | -| 견적 수정 | `/ko/sales/quote-management/[id]/edit` | - -``` -http://localhost:3000/ko/sales/quote-management/new -http://localhost:3000/ko/sales/quote-management/1 -http://localhost:3000/ko/sales/quote-management/1/edit -``` - -### 단가관리 하위 페이지 - -| 페이지 | URL | -|--------|-----| -| 단가 등록 | `/ko/sales/pricing-management/create` | -| 단가 수정 | `/ko/sales/pricing-management/[id]/edit` | - -``` -http://localhost:3000/ko/sales/pricing-management/create -http://localhost:3000/ko/sales/pricing-management/1/edit -``` - --- ## 📦 기준정보관리 (Master Data) -### 품목기준관리 - | 페이지 | URL | 상태 | |--------|-----|------| -| 품목 목록 | `/ko/master-data/item-master-data-management` | ✅ | +| 품목기준관리 | `/ko/master-data/item-master-data-management` | ✅ | ``` http://localhost:3000/ko/master-data/item-master-data-management ``` -### 품목관리 (Items) - 구버전 - -| 페이지 | URL | -|--------|-----| -| 품목 목록 | `/ko/items` | -| 품목 등록 | `/ko/items/create` | -| 품목 상세 | `/ko/items/[id]` | -| 품목 수정 | `/ko/items/[id]/edit` | - -``` -http://localhost:3000/ko/items -http://localhost:3000/ko/items/create -http://localhost:3000/ko/items/1 -http://localhost:3000/ko/items/1/edit -``` - --- ## 🏭 생산관리 (Production) -### 스크린 생산 - -| 페이지 | URL | -|--------|-----| -| 생산 목록 | `/ko/production/screen-production` | -| 생산 등록 | `/ko/production/screen-production/create` | -| 생산 상세 | `/ko/production/screen-production/[id]` | -| 생산 수정 | `/ko/production/screen-production/[id]/edit` | +| 페이지 | URL | 상태 | +|--------|-----|------| +| 스크린 생산 | `/ko/production/screen-production` | ✅ | ``` http://localhost:3000/ko/production/screen-production -http://localhost:3000/ko/production/screen-production/create -http://localhost:3000/ko/production/screen-production/1 -http://localhost:3000/ko/production/screen-production/1/edit ``` --- @@ -177,6 +99,12 @@ http://localhost:3000/ko/production/screen-production/1/edit | 직급관리 | `/ko/settings/ranks` | ✅ | | 직책관리 | `/ko/settings/titles` | ✅ | | 근무일정 | `/ko/settings/work-schedule` | ✅ | +| **출퇴근관리** | `/ko/settings/attendance-settings` | ✅ | +| **계좌관리** | `/ko/settings/accounts` | ✅ | +| **카드관리** | `/ko/hr/card-management` | 🆕 NEW | +| **게시판관리** | `/ko/board/board-management` | 🆕 NEW | +| **팝업관리** | `/ko/settings/popup-management` | 🆕 NEW | +| **알림설정** | `/ko/settings/notification-settings` | 🆕 NEW | ``` http://localhost:3000/ko/settings/leave-policy @@ -184,10 +112,140 @@ http://localhost:3000/ko/settings/permissions http://localhost:3000/ko/settings/ranks http://localhost:3000/ko/settings/titles http://localhost:3000/ko/settings/work-schedule +http://localhost:3000/ko/settings/attendance-settings # 출퇴근관리 +http://localhost:3000/ko/settings/accounts # 계좌관리 +http://localhost:3000/ko/settings/notification-settings # 🆕 알림설정 +http://localhost:3000/ko/hr/card-management # 🆕 카드관리 +http://localhost:3000/ko/board/board-management # 🆕 게시판관리 +http://localhost:3000/ko/settings/popup-management # 🆕 팝업관리 ``` --- +## 📝 전자결재 (Approval) + +| 페이지 | URL | 상태 | +|--------|-----|------| +| 기안함 | `/ko/approval/draft` | 🧪 테스트중 | +| **결재함** | `/ko/approval/inbox` | ✅ | +| **참조함** | `/ko/approval/reference` | ✅ | + +``` +http://localhost:3000/ko/approval/draft +http://localhost:3000/ko/approval/inbox # ✅ 결재함 +http://localhost:3000/ko/approval/reference # ✅ 참조함 +``` + +--- + +## 💵 회계관리 (Accounting) + +| 페이지 | URL | 상태 | +|--------|-----|------| +| **거래처 목록** | `/ko/accounting/vendors` | 🆕 NEW | +| **매입 목록** | `/ko/accounting/purchase` | ✅ | +| **매출 목록** | `/ko/accounting/sales` | ✅ | +| **입금 목록** | `/ko/accounting/deposits` | ✅ | +| **출금 목록** | `/ko/accounting/withdrawals` | 🆕 NEW | +| **어음 목록** | `/ko/accounting/bills` | ✅ | +| **거래처원장** | `/ko/accounting/vendor-ledger` | ✅ | +| **일일 일보** | `/ko/accounting/daily-report` | ✅ | +| **지출 예상 내역서** | `/ko/accounting/expected-expenses` | ✅ | +| **미수금 현황** | `/ko/accounting/receivables-status` | ✅ | +| **입출금 계좌조회** | `/ko/accounting/bank-transactions` | ✅ | +| **카드 내역 조회** | `/ko/accounting/card-transactions` | 🆕 NEW | +| **악성채권 추심관리** | `/ko/accounting/bad-debt-collection` | 🆕 NEW | + +``` +http://localhost:3000/ko/accounting/vendors # 거래처관리 +http://localhost:3000/ko/accounting/purchase # 매입관리 +http://localhost:3000/ko/accounting/sales # 매출관리 +http://localhost:3000/ko/accounting/deposits # 입금관리 +http://localhost:3000/ko/accounting/withdrawals # 출금관리 +http://localhost:3000/ko/accounting/bills # 어음관리 +http://localhost:3000/ko/accounting/vendor-ledger # 거래처원장 +http://localhost:3000/ko/accounting/daily-report # 일일 일보 +http://localhost:3000/ko/accounting/expected-expenses # 지출 예상 내역서 +http://localhost:3000/ko/accounting/receivables-status # 미수금 현황 +http://localhost:3000/ko/accounting/bank-transactions # 입출금 계좌조회 +http://localhost:3000/ko/accounting/card-transactions # 카드 내역 조회 +http://localhost:3000/ko/accounting/bad-debt-collection # 악성채권 추심관리 +``` + +--- + +## 📝 게시판 (Board) + +| 페이지 | URL | 상태 | +|--------|-----|------| +| **게시판 목록** | `/ko/board` | ✅ | + +``` +http://localhost:3000/ko/board # 게시판 목록 +``` + +> ⚠️ **참고**: 게시판관리는 설정(Settings)에서 관리합니다 + +--- + +## 📊 보고서 및 분석 (Reports) + +| 페이지 | URL | 상태 | +|--------|-----|------| +| **종합 경영 분석** | `/ko/reports/comprehensive-analysis` | 🆕 NEW | + +``` +http://localhost:3000/ko/reports/comprehensive-analysis # 종합 경영 분석 +``` + +> 📋 **사이드바 메뉴**: 종합 경영 분석, 매출현황, 거래현황, 시계열, 거래처분석, 대금회수, 미수금현황, 재고현황, 생산현황, 손익현황, 판관비현황, 고객현황 +> ℹ️ **참고**: "거래처별 미수금 현황" 버튼 클릭 시 `/ko/accounting/receivables-status`로 이동 + +--- + +## 👤 계정/회사/구독 (사이드바 별도 메뉴) + +> ⚠️ **참고**: 아래 항목들은 Settings 안이 아닌 **사이드바 루트 레벨**에 별도 메뉴로 존재 + +| 페이지 | URL | 상태 | +|--------|-----|------| +| **계정정보** | `/ko/account-info` | 🆕 NEW | +| **회사정보** | `/ko/company-info` | 🆕 NEW | +| **구독관리** | `/ko/subscription` | 🆕 NEW | +| **결제내역** | `/ko/payment-history` | 🆕 NEW | + +``` +http://localhost:3000/ko/account-info # 계정정보 +http://localhost:3000/ko/company-info # 회사정보 +http://localhost:3000/ko/subscription # 구독관리 +http://localhost:3000/ko/payment-history # 결제내역 +``` + +> ℹ️ **계정정보**: 탈퇴 버튼은 테넌트 마스터가 아닌 경우에만 활성화, 사용중지 버튼은 테넌트 마스터인 경우에만 활성화 +> ℹ️ **회사정보**: 테넌트 마스터에게만 표시 + +--- + +## 📢 고객센터 (Customer Center) + +| 페이지 | URL | 상태 | +|--------|-----|------| +| **공지사항** | `/ko/customer-center/notices` | ✅ | +| **이벤트** | `/ko/customer-center/events` | ✅ | +| **FAQ** | `/ko/customer-center/faq` | 🆕 NEW | +| **1:1 문의** | `/ko/customer-center/inquiries` | 🆕 NEW | + +``` +http://localhost:3000/ko/customer-center/notices # 공지사항 +http://localhost:3000/ko/customer-center/events # 이벤트 +http://localhost:3000/ko/customer-center/faq # FAQ +http://localhost:3000/ko/customer-center/inquiries # 1:1 문의 +``` + +> ℹ️ **고객센터 메뉴**: 공지사항, 이벤트, FAQ, 1:1 문의 + +--- + ## 📋 전체 URL 한눈에 보기 ### 기본 @@ -215,7 +273,6 @@ http://localhost:3000/ko/sales/pricing-management ### Master Data ``` http://localhost:3000/ko/master-data/item-master-data-management -http://localhost:3000/ko/items ``` ### Production @@ -230,6 +287,54 @@ http://localhost:3000/ko/settings/permissions http://localhost:3000/ko/settings/ranks http://localhost:3000/ko/settings/titles http://localhost:3000/ko/settings/work-schedule +http://localhost:3000/ko/settings/attendance-settings # 출퇴근관리 +http://localhost:3000/ko/settings/accounts # 계좌관리 +http://localhost:3000/ko/settings/notification-settings # 🆕 알림설정 +http://localhost:3000/ko/hr/card-management # 🆕 카드관리 +http://localhost:3000/ko/board/board-management # 🆕 게시판관리 +http://localhost:3000/ko/settings/popup-management # 🆕 팝업관리 +``` + +### Approval +``` +http://localhost:3000/ko/approval/draft +http://localhost:3000/ko/approval/inbox +http://localhost:3000/ko/approval/reference +``` + +### Accounting +``` +http://localhost:3000/ko/accounting/vendors # 거래처관리 +http://localhost:3000/ko/accounting/purchase # 매입관리 +http://localhost:3000/ko/accounting/sales # 매출관리 +http://localhost:3000/ko/accounting/deposits # 입금관리 +http://localhost:3000/ko/accounting/withdrawals # 출금관리 +http://localhost:3000/ko/accounting/bills # 어음관리 +http://localhost:3000/ko/accounting/vendor-ledger # 거래처원장 +http://localhost:3000/ko/accounting/daily-report # 일일 일보 +http://localhost:3000/ko/accounting/expected-expenses # 지출 예상 내역서 +http://localhost:3000/ko/accounting/receivables-status # 미수금 현황 +http://localhost:3000/ko/accounting/bank-transactions # 입출금 계좌조회 +http://localhost:3000/ko/accounting/card-transactions # 🆕 카드 내역 조회 +http://localhost:3000/ko/accounting/bad-debt-collection # 악성채권 추심관리 +``` + +### Board +``` +http://localhost:3000/ko/board # 게시판 목록 +``` + +### Reports +``` +http://localhost:3000/ko/reports/comprehensive-analysis # 종합 경영 분석 +``` + +### Customer Center +``` +http://localhost:3000/ko/customer-center/notices # 공지사항 +http://localhost:3000/ko/customer-center/events # 이벤트 +http://localhost:3000/ko/customer-center/faq # FAQ +http://localhost:3000/ko/customer-center/inquiries # 1:1 문의 ``` --- @@ -251,7 +356,6 @@ http://localhost:3000/ko/settings/work-schedule // Master Data '/master-data/item-master-data-management' -'/items' // Production '/production/screen-production' @@ -262,6 +366,50 @@ http://localhost:3000/ko/settings/work-schedule '/settings/ranks' '/settings/titles' '/settings/work-schedule' +'/settings/attendance-settings' // 출퇴근관리 +'/settings/accounts' // 계좌관리 +'/settings/notification-settings' // 알림설정 (🆕 NEW) +'/hr/card-management' // 카드관리 (🆕 NEW) +'/board/board-management' // 게시판관리 (🆕 NEW) +'/settings/popup-management' // 팝업관리 (🆕 NEW) + +// 계정/회사/구독 (사이드바 루트 레벨 별도 메뉴) +'/account-info' // 계정정보 (🆕 NEW) +'/company-info' // 회사정보 (🆕 NEW) +'/subscription' // 구독관리 (🆕 NEW) +'/payment-history' // 결제내역 (🆕 NEW) + +// Approval (전자결재) +'/approval/draft' // 기안함 +'/approval/inbox' // 결재함 +'/approval/reference' // 참조함 + +// Accounting (회계관리) +'/accounting/vendors' // 거래처관리 +'/accounting/purchase' // 매입관리 +'/accounting/sales' // 매출관리 +'/accounting/deposits' // 입금관리 +'/accounting/withdrawals' // 출금관리 +'/accounting/bills' // 어음관리 +'/accounting/vendor-ledger' // 거래처원장 +'/accounting/daily-report' // 일일 일보 +'/accounting/expected-expenses' // 지출 예상 내역서 +'/accounting/receivables-status' // 미수금 현황 +'/accounting/bank-transactions' // 입출금 계좌조회 +'/accounting/card-transactions' // 카드 내역 조회 +'/accounting/bad-debt-collection' // 악성채권 추심관리 + +// Board (게시판) +'/board' // 게시판 목록 + +// Reports (보고서 및 분석) +'/reports/comprehensive-analysis' // 종합 경영 분석 + +// Customer Center (고객센터) +'/customer-center/notices' // 공지사항 +'/customer-center/events' // 이벤트 +'/customer-center/faq' // FAQ (🆕 NEW) +'/customer-center/inquiries' // 1:1 문의 (🆕 NEW) ``` --- @@ -269,4 +417,4 @@ http://localhost:3000/ko/settings/work-schedule ## 작성일 - 최초 작성: 2025-12-06 -- 최종 업데이트: 2025-12-08 (전체 페이지 통합) \ No newline at end of file +- 최종 업데이트: 2025-12-19 (하위 페이지 정리, 리스트 페이지만 유지) diff --git a/claudedocs/_index.md b/claudedocs/_index.md index 128222dd..b8f710c7 100644 --- a/claudedocs/_index.md +++ b/claudedocs/_index.md @@ -19,6 +19,9 @@ claudedocs/ ├── hr/ # 👥 인사관리 (부서/사원) ├── item-master/ # 📦 품목기준관리 ├── sales/ # 💰 판매관리 (견적/거래처) +├── accounting/ # 💳 회계관리 (매입/매출/출금) +├── board/ # 📝 게시판 관리 +├── settings/ # ⚙️ 설정 관리 (NEW) ├── dashboard/ # 📊 대시보드 & 사이드바 ├── api/ # 🔌 API 통합 ├── guides/ # 📚 범용 가이드 @@ -50,6 +53,7 @@ claudedocs/ | 파일 | 설명 | |------|------| +| `[IMPL-2025-12-16] mobile-attendance.md` | 🔴 **NEW** - 모바일 출퇴근 시스템 (카카오맵 GPS 기반, MVP) | | `[IMPL-2025-12-05] department-management-checklist.md` | ✅ **완료** - 부서관리 구현 체크리스트 (무제한 트리구조) | | `[IMPL-2025-12-05] employee-management-checklist.md` | ✅ **완료** - 사원관리 구현 체크리스트 | | `[IMPL-2025-12-06] vacation-management-checklist.md` | ✅ **완료** - 휴가관리 구현 체크리스트 | @@ -130,7 +134,8 @@ claudedocs/ | 파일 | 설명 | |------|------| -| `[GUIDE] large-file-handling-strategy.md` | 🔴 **NEW** - 대용량 파일 처리 전략 (100MB+ CAD 도면, 청크 업로드, 스트리밍 다운로드) | +| `[GUIDE-2025-12-16] options-vs-flattened-data.md` | 🔴 **NEW** - options vs 평탄화 데이터 패턴 (API 응답 매핑 시 options 직접 파싱 금지) | +| `[GUIDE] large-file-handling-strategy.md` | 대용량 파일 처리 전략 (100MB+ CAD 도면, 청크 업로드, 스트리밍 다운로드) | | `[FIX-2025-12-05] radix-ui-select-controlled-mode-bug.md` | ⭐ **핵심** - Radix UI Select 버그 해결 (Edit 모드 값 표시 안됨 → key prop 강제 리마운트) | | `i18n-usage-guide.md` | 다국어 사용 가이드 | | `form-validation-guide.md` | 폼 유효성 검사 | @@ -153,6 +158,32 @@ claudedocs/ --- +## 💳 accounting/ - 회계관리 (거래처/매입/매출/출금) + +| 파일 | 설명 | +|------|------| +| `[IMPL-2025-12-18] vendor-management-checklist.md` | 🔴 **NEW** - 거래처관리 구현 체크리스트 (리스트 + 상세 페이지) | +| `[IMPL-2025-12-18] purchase-management.md` | 매입관리 페이지 구현 (리스트 + 상세 모달) | + +--- + +## 📝 board/ - 게시판 관리 + +| 파일 | 설명 | +|------|------| +| `[PLAN-2025-12-19] board-management-implementation.md` | 🔴 **NEW** - 게시판 구현 계획서 (리스트/등록/상세/댓글, TipTap 에디터) | + +--- + +## ⚙️ settings/ - 설정 관리 + +| 파일 | 설명 | +|------|------| +| `[IMPL-2025-12-19] company-info.md` | 🔴 **NEW** - 회사정보 구현 (폼 기반, 회사 추가 팝업) | +| `[IMPL-2025-12-19] popup-management.md` | 팝업관리 구현 (리스트/등록/상세/수정, RichTextEditor) | + +--- + ## 📁 archive/ - 레거시/완료된 문서 완료되거나 더 이상 활성화되지 않은 문서들. 참조용으로 보관. diff --git a/claudedocs/accounting/[IMPL-2025-12-18] bill-management.md b/claudedocs/accounting/[IMPL-2025-12-18] bill-management.md new file mode 100644 index 00000000..1f6bad99 --- /dev/null +++ b/claudedocs/accounting/[IMPL-2025-12-18] bill-management.md @@ -0,0 +1,65 @@ +# 어음관리 (Bill Management) 구현 + +## 스크린샷 분석 + +### 리스트 화면 +- 상단: 일괄등록 버튼, 날짜범위 선택, 상태 탭 (전체, 전일, 오늘, 미결, 수취, 우등록) +- 통계 카드: 4개 (건수 표시) +- 테이블 컬럼: No, 어음번호, 구분, 거래처, 금액, 만기일, 이유, 추수, 메모, 상태 +- 필터: 거래처, 상태 + +### 상세/수정 화면 +- 타이틀: "어음 상세" +- 버튼: view 모드 [목록, 삭제, 수정] / edit 모드 [취소, 저장] +- 기본 정보: + - 어음번호 (Input) + - 구분 (Select: 수취/발행) + - 거래처 (Select) + - 금액 (Input) + - 발행일 (Date) + - 만기일 (Date) + - 상태 (Select - 구분에 따라 옵션 변경) + - 비고 (Input) +- 차수 관리 섹션: + - 테이블: 일자, 금액, 비고 + - [+ 추가] 버튼 + +### 타입 정의 +- **구분**: 수취, 발행 +- **상태 (수취)**: 보관중, 만기입금(7일전), 만기결과, 결제완료, 부도 +- **상태 (발행)**: 보관중, 만기입금(7일전), 추심의뢰, 추심완료, 추소중, 부도 + +--- + +## 체크리스트 + +### Phase 1: 기본 구조 +- [x] types.ts 생성 (타입, 상수 정의) +- [x] 폴더 구조 생성 (BillManagement/) + +### Phase 2: 리스트 화면 +- [x] index.tsx 생성 (리스트 컴포넌트) +- [x] 통계 카드 구현 +- [x] 테이블 렌더링 구현 +- [x] 필터 및 정렬 구현 + +### Phase 3: 상세/수정 화면 +- [x] BillDetail.tsx 생성 +- [x] 기본 정보 폼 구현 +- [x] 차수 관리 섹션 구현 +- [x] view/edit 모드 분기 처리 + +### Phase 4: 라우팅 +- [x] page.tsx 파일 생성 (리스트, 상세, 등록) +- [x] 네비게이션 패턴 적용 (?mode=edit) + +### Phase 5: 검증 +- [x] 빌드 테스트 ✅ +- [ ] 기능 확인 (사용자 확인 필요) + +--- + +## 참고 패턴 +- 입금관리 (DepositManagement) 구조 참고 +- IntegratedListTemplateV2 사용 +- PageLayout + PageHeader 패턴 \ No newline at end of file diff --git a/claudedocs/accounting/[IMPL-2025-12-18] expected-expense-checklist.md b/claudedocs/accounting/[IMPL-2025-12-18] expected-expense-checklist.md new file mode 100644 index 00000000..d6ea836d --- /dev/null +++ b/claudedocs/accounting/[IMPL-2025-12-18] expected-expense-checklist.md @@ -0,0 +1,38 @@ +# 지출 예상 내역서 구현 체크리스트 + +## 현재 세션 작업 (2025-12-18) + +### 1. 테이블 필터 수정 +- [x] 1.1 첫번째 필터: 전체 → 거래처 목록으로 변경 ✅ + - 현재: 거래유형 필터 (매입, 선급금, 가지급금 등) + - 변경: 거래처 필터 (전체, 거래처1, 거래처2...) +- [x] 1.2 두번째 필터: 정렬 옵션 축소 ✅ + - 현재: 최신순, 등록순, 지급일 빠른순, 지급일 느린순, 금액 높은순, 금액 낮은순 + - 변경: 최신순, 등록순 (2개만) + +### 2. 예상 지급일 변경 팝업 +- [x] 2.1 팝업 다이얼로그 생성 ✅ + - 헤더: "예상 지급일 변경" + X 닫기 버튼 +- [x] 2.2 선택 항목 요약 영역 ✅ + - 항목명 외 N (선택된 항목 수) + - 총 금액 표시 (합계) +- [x] 2.3 예상 지급일 선택 ✅ + - 라벨: "예상 지급일" + - 공용 달력 컴포넌트 사용 (Calendar + Popover) +- [x] 2.4 버튼 영역 ✅ + - 취소 버튼 + - 저장 버튼 (날짜 미선택 시 비활성화) + +### 3. 전자결재 버튼 기능 +- [x] 3.1 버튼 클릭 시 페이지 이동 ✅ + - 이동 경로: `/ko/approval/document-write/expected-expense` (문서 작성_지출 예상 내역서) + - 항목 선택 필수 (미선택 시 버튼 비활성화) + +--- + +## 참고 스크린샷 +- 예상 지급일 변경 팝업: `스크린샷 2025-12-18 오후 4.39.35.png` +- 필터 위치 참고: `스크린샷 2025-12-18 오후 4.19.33.png`, `4.20.20.png` + +## 테스트 URL +- http://localhost:3000/ko/accounting/expected-expenses \ No newline at end of file diff --git a/claudedocs/accounting/[IMPL-2025-12-18] purchase-management.md b/claudedocs/accounting/[IMPL-2025-12-18] purchase-management.md new file mode 100644 index 00000000..a38ac185 --- /dev/null +++ b/claudedocs/accounting/[IMPL-2025-12-18] purchase-management.md @@ -0,0 +1,111 @@ +# [IMPL-2025-12-18] 매입관리 페이지 구현 + +## 개요 +회계관리 > 매입관리 페이지 구현 (리스트 + 상세) + +## 참고자료 +- 기안함 리스트 페이지: `src/components/approval/DraftBox/index.tsx` +- 공통 템플릿: `IntegratedListTemplateV2` +- 공통 컴포넌트: `DateRangeSelector`, `ListMobileCard` + +--- + +## Phase 1: 리스트 페이지 + +### 1.1 페이지 구조 +- [ ] 라우트 생성: `/accounting/purchase` +- [ ] 컴포넌트 생성: `src/components/accounting/PurchaseManagement/index.tsx` +- [ ] 타입 정의: `src/components/accounting/PurchaseManagement/types.ts` + +### 1.2 상단 영역 +- [ ] 날짜 범위 선택 (DateRangeSelector) +- [ ] 탭 버튼: 담대조건, 진행중, 당일, 이달, 이번, 미결 + +### 1.3 통계 카드 (4개) +- [ ] 매입금액 합계 (원) +- [ ] 출금액 합계 (원) +- [ ] 매입 건수 +- [ ] 미결 건수 + +### 1.4 필터 셀렉트 박스 +- [ ] 부가세여부 필터 (다중 선택): 부가세여부, 상품매입, 오주경비, 소모품비, 수선비, 원재료비, 사무용품비 등 +- [ ] 거래처 필터 (검색 + 다중 선택) +- [ ] 증빙 필터 (다중 선택): 증빙유형 목록 + +### 1.5 테이블 컬럼 +| 컬럼 | 설명 | +|------|------| +| No | 순번 | +| 매입일자 | 매입 등록일 | +| 매입금액 | 금액 | +| 거래처 | 거래처명 | +| 출금액 | 실제 출금액 | +| 부가세 | 부가세 금액 | +| 매입유형 | 유형 분류 | +| 증빙유형 | 세금계산서 등 | + +### 1.6 기능 +- [ ] 매입 자동 등록: 지출예상내역서 승인 완료 시 자동 등록 +- [ ] 매입/매출등록 Alert 표시 (API 연동 예정) +- [ ] 일람표/거래처원장 연계 출력 + +--- + +## Phase 2: 상세 페이지 (모달) + +### 2.1 기본 정보 섹션 +- [ ] 근거 문서명: 품의서 또는 지출결의서 표시 +- [ ] 결재 버튼: 클릭 시 매입 문서 상세 팝업 +- [ ] 예상 비용 표시: 품의서/지출결의서 예상/총 비용 + +### 2.2 매입 정보 섹션 +- [ ] 매입일자: 문서번호 + 상세조회 아이콘 +- [ ] 출금계좌 셀렉트 박스: 등록된 계좌 목록 (계좌번호 마지막 4자리 + 별명) +- [ ] 거래처 셀렉트 박스: 검색 기능 +- [ ] 매입 유형 셀렉트 박스: 부자재매입, 상품매입, 오주경비, 소모품비, 수선비, 원재료비, 사무용품비, 임차료, 수도광열비, 통신비, 차량유지비, 잡비, 보험료, 기타경비, 미상담 + +### 2.3 품목 정보 섹션 +- [ ] 테이블: 품목명, 수량, 단가, 공급가액, 부가세, 적요 +- [ ] 행 추가/삭제 기능 +- [ ] 합계 표시 + +### 2.4 세금계산서 섹션 +- [ ] 세금계산서 수취 토글 버튼 +- [ ] 토글 시 미수취/수취완료 상태 변경 +- [ ] 수취 완료 후 완료 상태로 변경 + +--- + +## Phase 3: 연동 및 마무리 +- [ ] 전자결재 시스템 연동 (지출예상내역서 승인 → 매입 자동 등록) +- [ ] API 연동 준비 (비포템 API 자동 등록 예정) +- [ ] 일람표/거래처원장 출력 기능 + +--- + +## 파일 구조 +``` +src/ +├── app/[locale]/(protected)/accounting/ +│ └── purchase/ +│ └── page.tsx +├── components/accounting/ +│ └── PurchaseManagement/ +│ ├── index.tsx # 리스트 페이지 +│ ├── types.ts # 타입 정의 +│ └── PurchaseDetailModal.tsx # 상세 모달 +``` + +--- + +## 진행 상태 +- [x] 요구사항 분석 완료 +- [x] 계획서 작성 완료 +- [x] Phase 1 완료 (리스트 페이지) +- [x] Phase 2 완료 (상세 모달) +- [ ] Phase 3 대기 (API 연동) + +## 테스트 URL +``` +http://localhost:3000/ko/accounting/purchase +``` \ No newline at end of file diff --git a/claudedocs/accounting/[IMPL-2025-12-18] receivables-status.md b/claudedocs/accounting/[IMPL-2025-12-18] receivables-status.md new file mode 100644 index 00000000..427657dd --- /dev/null +++ b/claudedocs/accounting/[IMPL-2025-12-18] receivables-status.md @@ -0,0 +1,73 @@ +# 미수금 현황 페이지 구현 체크리스트 + +## 기본 정보 +- **생성일**: 2025-12-18 +- **경로**: `/ko/accounting/receivables-status` +- **참고 페이지**: 매출관리, 거래처원장 + +--- + +## Phase 1: 기본 구조 설정 + +- [x] 페이지 라우트 생성 (`src/app/[locale]/(protected)/accounting/receivables-status/page.tsx`) +- [x] 컴포넌트 디렉토리 생성 (`src/components/accounting/ReceivablesStatus/`) +- [x] 메인 컴포넌트 생성 (`index.tsx`) +- [x] 타입 정의 파일 생성 (`types.ts`) + +--- + +## Phase 2: 레이아웃 구현 + +- [x] DateRangeSelector 공통 달력 적용 +- [x] 프리셋 버튼 (당해년도, 전전월, 전월, 당월, 어제, 오늘) +- [x] 엑셀 다운로드 버튼 +- [x] 저장 버튼 (엑셀 다운로드 아래) +- [x] 검색창 (거래처 검색) + +--- + +## Phase 3: 테이블 구현 (특수 구조) + +테이블 구조 (스크린샷 기준): +- 컬럼: 거래처/연체, 구분, 1월~12월, 합계 +- 그룹핑: 거래처별로 묶이고 각 거래처 아래 구분 (5개: 매출, 입금, 어음, 미수금, 메모) + +- [x] 거래처별 그룹핑 테이블 구조 (rowSpan=5 사용) +- [x] 월별 컬럼 (1월~12월 + 합계) +- [x] 구분 행 (매출, 입금, 어음, 미수금, 메모) - 5개 카테고리 +- [x] 연체 토글 (거래처/연체 컬럼 내) +- [x] 연체 영역 전체 하이라이트 (토글 ON 시 해당 월 전체 빨간 배경) + +--- + +## Phase 4: 토글 기능 + +스크린샷 Description 기준: +- ON: 연체 상태로 표시, 연체일수 시작 +- OFF: 정상 상태 +- 거래처 상세에서 연체 설정과 연동 + +- [x] Switch 컴포넌트로 연체 토글 구현 +- [x] 토글 상태에 따른 UI 변화 +- [x] 연체 상태 표시 로직 + +--- + +## Phase 5: 추가 기능 + +- [x] Mock 데이터 생성 +- [x] 합계 행 계산 +- [ ] 모바일 카드 뷰 (추후 필요시) +- [x] 반응형 레이아웃 (overflow-x-auto) + +--- + +## 진행 상태 +- **현재 단계**: 완료 +- **마지막 업데이트**: 2025-12-18 + +--- + +## 참고 사항 +- Description 영역 (수취 어음 등록 시 표시, 메모 입력박스)은 현재 스코프에서 제외 +- 기본 기능 먼저 구현 후 추가 기능 논의 \ No newline at end of file diff --git a/claudedocs/accounting/[IMPL-2025-12-18] vendor-ledger.md b/claudedocs/accounting/[IMPL-2025-12-18] vendor-ledger.md new file mode 100644 index 00000000..60eaa4af --- /dev/null +++ b/claudedocs/accounting/[IMPL-2025-12-18] vendor-ledger.md @@ -0,0 +1,129 @@ +# [IMPL-2025-12-18] 거래처원장 (Vendor Ledger) 구현 + +## 개요 +- **화면명**: 거래처원장 +- **경로**: 회계관리 > 거래처원장 +- **구성**: 리스트 페이지 + 상세 페이지 + +--- + +## Phase 1: 리스트 페이지 (VendorLedger/index.tsx) + +### 1.1 헤더 영역 +- [ ] 제목: "거래처원장" +- [ ] 설명: "거래처별 기간 내역을 조회합니다." +- [ ] 기간 선택기: 2025-09-01 ~ 2025-09-03 형식 +- [ ] 기간 버튼: 당해년도, 전년월, 전월, 당월, 어제, 오늘 +- [ ] 엑셀 다운로드 버튼 + +### 1.2 요약 카드 (4개) +- [ ] 전기 이월: 3,123,000원 +- [ ] 매출: 3,123,000원 +- [ ] 수금: 3,123,000원 +- [ ] 잔액: 3,123,000원 + +### 1.3 테이블 영역 +- [ ] 검색 필드 +- [ ] 총 N건 표시 +- [ ] 테이블 컬럼: + - No. + - 거래처명 + - 이월잔액 + - 매출 + - 수금 + - 잔액 + - 결제일 +- [ ] 합계 행 (테이블 하단) +- [ ] 행 클릭 시 상세 페이지 이동 + +--- + +## Phase 2: 상세 페이지 (VendorLedger/VendorLedgerDetail.tsx) + +### 2.1 헤더 영역 +- [ ] 제목: "거래처원장 상세 (거래명세서별)" +- [ ] 설명: "거래처 상세 내역을 조회합니다." +- [ ] 기간 선택기: 2025-09-01 ~ 2025-09-03 +- [ ] 기간 버튼: 당해년도, 전년월, 전월, 당월, 어제, 오늘 +- [ ] PDF 다운로드 버튼 + +### 2.2 거래처 정보 섹션 (2열 레이아웃) +- [ ] 좌측 열: + - 회사명: [값] + - 사업자등록번호: 123-12-12345 + - 전화번호: 02-1234-1234 + - 팩스: 02-1234-1236 + - 주소: 주소영 +- [ ] 우측 열: + - 기간: 2025-01-01 ~ 2025-12-31 + - 대표자: 홍길동 + - 모바일: 02-1234-1235 + - 이메일: abc@email.com + +### 2.3 판매/수금 내역 테이블 +- [ ] 컬럼: 일자, 적요, 판매, 수금, 잔액, 작업 +- [ ] 이월잔액 행 (첫 행, 적요에 "이월잔액") +- [ ] 거래 행 (◆ 아이콘 + 날짜) + - 클릭 시 어음 상세 화면 이동 +- [ ] 거래명세서 행 (클릭 시 문서 상세 팝업) +- [ ] 품목명 행 (세금계산서 미발행 시 노란색 하이라이트) +- [ ] 누계 행 (※ 123건 누계 (VAT 포함) 형식) +- [ ] 월별 계 행 (회색 배경, 예: "2025/01 계", "2025/09 계") +- [ ] 작업 컬럼: 수정 아이콘 (✏️) + +--- + +## Phase 3: 타입 및 라우트 + +### 3.1 types.ts +- [ ] VendorLedgerItem 인터페이스 +- [ ] VendorLedgerDetail 인터페이스 +- [ ] TransactionEntry 인터페이스 + +### 3.2 라우트 설정 +- [ ] `/ko/accounting/vendor-ledger` - 리스트 페이지 +- [ ] `/ko/accounting/vendor-ledger/[id]` - 상세 페이지 + +--- + +## 스크린샷 상세 분석 + +### 리스트 페이지 테이블 데이터 예시 +| No. | 거래처명 | 이월잔액 | 매출 | 수금 | 잔액 | 결제일 | +|-----|---------|---------|------|------|------|--------| +| 7 | 회사명 | -100,000 | | | | | +| 6 | 회사명 | 10,000,000 | 10,000,000 | 10,000,000 | | | +| 5 | 회사명 | 10,000,000 | | | | | +| ... | | | | | | | +| **합계** | | 10,000,000 | 10,000,000 | 10,000,000 | 10,000,000 | | + +### 상세 페이지 판매/수금 내역 예시 +| 일자 | 적요 | 판매 | 수금 | 잔액 | 작업 | +|------|------|------|------|------|------| +| | 이월잔액 | 10,000,000 | | | | +| ◆ 2025-01-01 | 수취 어음 (만기 1/5) | | 3,000,000 | 10,000,000 | ✏️ | +| ◆ 2025-01-05 | 어음 회수 | | 3,000,000 | | | +| **2025/01 계** | | | | | | +| ◆ 2025-09-25 | 매출 입력 | 1,000,000 | | | | +| | 품목명 | **🟡 500,000** | | | | +| | ※ 123건 누계 (VAT 포함) | 1,000,000 | | 1,000,000 | | +| **2025/09 계** | | 1,000,000 | 8,000,000 | | | + +--- + +## 작업 진행 상황 +- [x] Phase 1: 리스트 페이지 구현 +- [x] Phase 2: 상세 페이지 구현 +- [x] Phase 3: 타입 및 라우트 설정 +- [x] Phase 4: 테스트 및 검증 + +## 생성된 파일 +1. `src/components/accounting/VendorLedger/types.ts` - 타입 정의 +2. `src/components/accounting/VendorLedger/index.tsx` - 리스트 페이지 +3. `src/components/accounting/VendorLedger/VendorLedgerDetail.tsx` - 상세 페이지 +4. `src/app/[locale]/(protected)/accounting/vendor-ledger/page.tsx` - 리스트 라우트 +5. `src/app/[locale]/(protected)/accounting/vendor-ledger/[id]/page.tsx` - 상세 라우트 + +## 접속 URL +- 리스트: `/ko/accounting/vendor-ledger` +- 상세: `/ko/accounting/vendor-ledger/[id]` \ No newline at end of file diff --git a/claudedocs/accounting/[IMPL-2025-12-18] vendor-management-checklist.md b/claudedocs/accounting/[IMPL-2025-12-18] vendor-management-checklist.md new file mode 100644 index 00000000..5ab96a44 --- /dev/null +++ b/claudedocs/accounting/[IMPL-2025-12-18] vendor-management-checklist.md @@ -0,0 +1,287 @@ +# 거래처 관리 (Vendor Management) 구현 체크리스트 + +> **상태**: ✅ 완료 (2025-12-18) +> **리스트 수정**: ✅ 완료 (2025-12-18) - 필터/액션버튼/신규등록 수정 + +## 개요 +- **경로**: `/accounting/vendors` (리스트), `/accounting/vendors/[id]` (상세), `/accounting/vendors/new` (신규등록) +- **기능**: 거래처 등록, 조회, 수정, 삭제 +- **참고**: 스크린샷 4장 (리스트 1장, 상세 3장) + +--- + +## Phase 1: 타입 및 상수 정의 ✅ + +### 1.1 types.ts 생성 +- [x] 거래처 구분 타입 (VendorCategory): `sales` | `purchase` | `both` +- [x] 신용등급 타입 (CreditRating): `AAA` | `AA` | `A` | `BBB` | `BB` | `B` | `CCC` | `CC` | `C` | `D` +- [x] 거래등급 타입 (TransactionGrade): `A` | `B` | `C` | `D` | `E` +- [x] 악성채권 상태 타입 (BadDebtStatus): `none` | `normal` | `warning` +- [x] 정렬 옵션 타입 (SortOption) +- [x] 거래처 인터페이스 (Vendor) + - id, vendorCode, businessNumber + - vendorName, representativeName + - category (sales/purchase/both) + - businessType, businessCategory + - address (zipCode, address1, address2) + - phone, mobile, fax, email + - managerName, managerPhone, systemManager + - logoUrl + - purchasePaymentDay, salesPaymentDay + - creditRating, transactionGrade + - taxInvoiceEmail, bankName, accountNumber, accountHolder + - outstandingAmount, overdueAmount, overdueDays + - unpaidAmount, badDebtStatus + - overdueToggle, badDebtToggle + - memos: Memo[] + - createdAt, updatedAt + +### 1.2 상수 정의 +- [x] VENDOR_CATEGORY_OPTIONS (구분 필터) +- [x] CREDIT_RATING_OPTIONS (신용등급 필터) +- [x] TRANSACTION_GRADE_OPTIONS (거래등급 필터) +- [x] BAD_DEBT_STATUS_OPTIONS (악성채권 필터) +- [x] SORT_OPTIONS (정렬 옵션) +- [x] PAYMENT_DAY_OPTIONS (결제일 1~31일) +- [x] BANK_OPTIONS (은행 목록) + +--- + +## Phase 2: 리스트 페이지 구현 ✅ + +### 2.1 페이지 라우트 생성 +- [x] `/src/app/[locale]/(protected)/accounting/vendors/page.tsx` 생성 + +### 2.2 VendorManagement 컴포넌트 +- [x] `/src/components/accounting/VendorManagement/index.tsx` 생성 + +#### 2.2.1 상단 통계 카드 (3개) +| 카드 | 값 | 아이콘 색상 | +|------|-----|------------| +| 전체 거래처 | {count}개 | blue | +| 매출 거래처 | {count}개 | green | +| 매입 거래처 | {count}개 | orange | + +#### 2.2.2 필터 조건 (5개 셀렉트박스) +| 필터 | 옵션 | 기본값 | +|------|------|--------| +| 구분 | 전체, 매출, 매입, 매입매출 | 전체 | +| 신용등급 | 전체, AAA, AA, A, BBB, BB, B, CCC, CC, C, D | 전체 | +| 거래등급 | 전체, A(우수), B(양호), C(보통), D(주의), E(위험) | 전체 | +| 악성채권 | 전체, 미설정, 정상 | 전체 | +| 정렬 | 최신순, 등록순, 거래처명 오름차순, 거래처명 내림차순, 미수금 높은순, 미수금 낮은순 | 최신순 | + +#### 2.2.3 테이블 컬럼 (체크박스 + 9개) +| 순서 | 컬럼명 | 정렬 | 비고 | +|------|--------|------|------| +| 0 | 체크박스 | center | - | +| 1 | 번호 | center | globalIndex + 1 | +| 2 | 구분 | center | Badge (매출/매입/매입매출) | +| 3 | 거래처명 | left | - | +| 4 | 매입 결제일 | center | {n}일 | +| 5 | 매출 결제일 | center | {n}일 | +| 6 | 신용등급 | center | Badge | +| 7 | 거래등급 | center | Badge | +| 8 | 미수금 | right | 금액 포맷 | +| 9 | 악성채권 | center | Badge or - | + +#### 2.2.4 행 선택 시 버튼 +- [x] 상세 버튼 → `/accounting/vendors/{id}` 이동 +- [x] 수정 버튼 → `/accounting/vendors/{id}?mode=edit` 이동 +- [x] 삭제 버튼 → 삭제 확인 AlertDialog +- [x] 취소 버튼 → 선택 해제 + +#### 2.2.5 헤더 액션 +- [x] 신규등록 버튼 → `/accounting/vendors/new` 이동 + +--- + +## Phase 3: 상세/수정/등록 페이지 구현 ✅ + +### 3.1 페이지 라우트 생성 +- [x] `/src/app/[locale]/(protected)/accounting/vendors/[id]/page.tsx` (상세/수정) +- [x] `/src/app/[locale]/(protected)/accounting/vendors/new/page.tsx` (신규등록) + +### 3.2 VendorDetail 컴포넌트 +- [x] `/src/components/accounting/VendorManagement/VendorDetail.tsx` 생성 +- [x] mode prop: `view` | `edit` | `new` + +#### 3.2.1 상단 버튼 +| 모드 | 버튼 | +|------|------| +| view | 삭제, 수정 | +| edit | 취소, 저장 | +| new | 취소, 등록 | + +#### 3.2.2 기본 정보 섹션 +| 필드명 | 타입 | 필수 | 비고 | +|--------|------|------|------| +| 사업자등록번호 | text (마스크: 000-00-00000) | * | - | +| 거래처코드 | text | - | 자동생성 또는 수동입력 | +| 거래처명 | text | * | - | +| 대표자명 | text | - | - | +| 거래처 유형 | select | * | 매출매입, 매출, 매입 | +| 업태 | text | - | - | +| 업종 | text | - | - | + +#### 3.2.3 연락처 정보 섹션 +| 필드명 | 타입 | 필수 | 비고 | +|--------|------|------|------| +| 주소 | address | - | 우편번호 찾기 + 기본주소 + 상세주소 | +| 전화번호 | tel | - | 02-0000-0000 | +| 모바일 | tel | - | 010-0000-0000 | +| 팩스 | tel | - | 02-0000-0000 | +| 이메일 | email | - | - | + +#### 3.2.4 담당자 정보 섹션 +| 필드명 | 타입 | 필수 | 비고 | +|--------|------|------|------| +| 담당자명 | text | - | - | +| 담당자 전화 | tel | - | - | +| 시스템 관리자 | text | - | - | + +#### 3.2.5 회사 정보 섹션 +| 필드명 | 타입 | 필수 | 비고 | +|--------|------|------|------| +| 회사 로고 | file | - | 750x250px, 10MB 이하, PNG/JPEG/GIF | +| 매입 결제일 | select | - | 1~31일 | +| 매출 결제일 | select | - | 1~31일 | + +#### 3.2.6 신용/거래 정보 섹션 +| 필드명 | 타입 | 필수 | 비고 | +|--------|------|------|------| +| 신용등급 | select | - | AAA~D | +| 거래등급 | select | - | A(우수)~E(위험) | +| 세금계산서 이메일 | email | - | - | +| 입금계좌 은행 | select | - | 은행 목록 | +| 계좌 | text | - | 숫자만 | +| 예금주 | text | - | - | + +#### 3.2.7 추가 정보 섹션 +| 필드명 | 타입 | 필수 | 비고 | +|--------|------|------|------| +| 미수금 | number | - | 금액 입력 | +| 연체 | number + toggle | - | 일수 + ON/OFF | +| 미지급 | number | - | 금액 입력 | +| 악성채권 | select + toggle | - | 선택 + ON/OFF (악성채권 추심관리 연동) | + +#### 3.2.8 메모 섹션 +| 필드명 | 타입 | 비고 | +|--------|------|------| +| 메모 | textarea + list | 추가 버튼으로 메모 리스트에 추가 | +| 메모 리스트 | table | 날짜, 내용, 삭제버튼 | + +--- + +## Phase 4: 다이얼로그 ✅ + +### 4.1 삭제 확인 다이얼로그 +- [x] AlertDialog 사용 +- [x] 제목: "거래처(명)을 삭제하시겠습니까?" +- [x] 설명: 확인 클릭 시 거래처관리 목록으로 이동 +- [x] 버튼: 취소, 삭제 + +### 4.2 수정 확인 다이얼로그 +- [x] AlertDialog 사용 +- [x] 제목: "정말 수정하시겠습니까?" +- [x] 설명: 확인 클릭 시 "수정이 완료되었습니다." 알림 +- [x] 버튼: 취소, 확인 + +--- + +## Phase 5: 파일 구조 ✅ + +``` +src/ +├── app/[locale]/(protected)/accounting/vendors/ +│ ├── page.tsx # 리스트 페이지 ✅ +│ ├── [id]/ +│ │ └── page.tsx # 상세/수정 페이지 ✅ +│ └── new/ +│ └── page.tsx # 신규등록 페이지 ✅ +└── components/accounting/VendorManagement/ + ├── index.tsx # 리스트 컴포넌트 ✅ + ├── VendorDetail.tsx # 상세/수정/등록 컴포넌트 ✅ + └── types.ts # 타입 및 상수 정의 ✅ +``` + +--- + +## 구현 완료 요약 + +### Step 1: 기본 구조 ✅ +- [x] types.ts 생성 및 타입/상수 정의 +- [x] 페이지 라우트 파일 생성 + +### Step 2: 리스트 페이지 ✅ +- [x] index.tsx 생성 +- [x] IntegratedListTemplateV2 활용 +- [x] 통계 카드 (3개) +- [x] 필터 (5개 셀렉트박스) +- [x] 테이블 (체크박스 + 번호 + 9개 컬럼) +- [x] 행 선택 시 버튼 (상세/수정/삭제/취소) +- [x] 삭제 확인 다이얼로그 + +### Step 3: 상세 페이지 ✅ +- [x] VendorDetail.tsx 생성 +- [x] 기본 정보 섹션 +- [x] 연락처 정보 섹션 +- [x] 담당자 정보 섹션 +- [x] 회사 정보 섹션 (로고 업로드 영역 포함) +- [x] 신용/거래 정보 섹션 +- [x] 추가 정보 섹션 (토글 포함) +- [x] 메모 섹션 + +### Step 4: 수정/등록 기능 ✅ +- [x] mode별 버튼 및 동작 구현 +- [x] 수정/삭제 확인 다이얼로그 + +--- + +## 테스트 URL + +| 페이지 | URL | +|--------|-----| +| 리스트 | `/ko/accounting/vendors` | +| 상세 | `/ko/accounting/vendors/vendor-1` | +| 수정 | `/ko/accounting/vendors/vendor-1?mode=edit` | +| 신규등록 | `/ko/accounting/vendors/new` | + +--- + +## 참고 사항 + +### 공통 컴포넌트 사용 +- IntegratedListTemplateV2 (리스트 템플릿) +- AlertDialog (삭제/수정 확인) +- Card, CardHeader, CardContent (섹션 구분) +- Select, Input, Textarea (폼 요소) +- Switch (토글) +- Badge (상태 표시) + +### 스타일 가이드 +- 주요 색상: orange (강조), blue/green (통계 카드) +- Badge 색상: + - 구분: 매출(green), 매입(orange), 매입매출(blue) + - 신용등급: AAA~A(green), BBB~B(yellow), CCC~D(red) + - 거래등급: A(green), B(blue), C(yellow), D(orange), E(red) + - 악성채권: 정상(green), 미설정(-), 경고(red) + +### 주의사항 +- 상세 페이지는 **페이지**로 구현 (모달 X) ✅ +- 테이블에 번호 컬럼 필수 (체크박스 바로 뒤) ✅ +- 금액은 toLocaleString() 포맷 사용 ✅ +- 행 클릭 시 상세 페이지로 이동 ✅ + +--- + +## 리스트 페이지 수정 (2025-12-18) + +### 수정 사항 +- [x] 악성채권 필터 옵션: "미설정" → "악성채권" 변경 +- [x] BadDebtStatus 타입: 'warning' → 'badDebt' 변경 +- [x] 작업 버튼: 상단 액션바 → 테이블 맨 끝 컬럼으로 이동 +- [x] 체크박스 선택 시에만 수정/삭제 버튼 표시 +- [x] headerActions (신규등록 버튼) 제거 +- [x] beforeTableContent (상단 액션 바) 제거 +- [x] 미사용 핸들러 정리 (handleNewVendor, handleCancelSelection) \ No newline at end of file diff --git a/claudedocs/accounting/[IMPL-2025-12-18] withdrawal-management-checklist.md b/claudedocs/accounting/[IMPL-2025-12-18] withdrawal-management-checklist.md new file mode 100644 index 00000000..a6c39020 --- /dev/null +++ b/claudedocs/accounting/[IMPL-2025-12-18] withdrawal-management-checklist.md @@ -0,0 +1,142 @@ +# 출금관리 (Withdrawal Management) 구현 체크리스트 + +> **상태**: ✅ 완료 (2025-12-18) +> **참고**: 입금관리(DepositManagement)와 동일한 구조 + +## 개요 +- **경로**: `/accounting/withdrawals` (리스트), `/accounting/withdrawals/[id]` (상세) +- **기능**: 출금 내역 조회, 수정 (계좌 관리에 등록된 계좌의 자동 출금 내역 수집) +- **참고**: 스크린샷 3장 + +--- + +## Phase 1: 타입 및 상수 정의 + +### 1.1 types.ts 생성 +- [ ] 출금 유형 타입 (WithdrawalType) + - `unset` (미설정) + - `purchasePayment` (매입대금) + - `advance` (선급금) + - `suspense` (가지급금) + - `rent` (임대료) + - `interestExpense` (이자비용) + - `depositPayment` (보증금 지급) + - `loanRepayment` (차입금 상환) + - `dividendPayment` (배당금 지급) + - `vatPayment` (부가세 납부) + - `salary` (급여) + - `insurance` (4대보험) + - `tax` (세금) + - `utilities` (공과금) + - `expenses` (경비) + - `other` (기타) + +- [ ] 정렬 옵션 (SortOption): 최신순, 등록순, 금액 높은순, 금액 낮은순 +- [ ] 출금 레코드 인터페이스 (WithdrawalRecord) + - id, withdrawalDate, withdrawalAmount + - accountName, recipientName (수취인명) + - note (적요), withdrawalType + - vendorId, vendorName + - createdAt, updatedAt + +--- + +## Phase 2: 리스트 페이지 구현 + +### 2.1 페이지 라우트 +- [ ] `/src/app/[locale]/(protected)/accounting/withdrawals/page.tsx` + +### 2.2 WithdrawalManagement 컴포넌트 +- [ ] `/src/components/accounting/WithdrawalManagement/index.tsx` + +#### 2.2.1 상단 통계 카드 (4개) +| 카드 | 값 | 아이콘 색상 | +|------|-----|------------| +| 출금 총액 | {amount}원 | blue | +| 당월 출금 | {amount}원 | green | +| 거래처 미설정 | {count}건 | orange | +| 출금유형 미설정 | {count}건 | red | + +#### 2.2.2 헤더 액션 +- [ ] DateRangeSelector (날짜 범위) +- [ ] 빠른 필터: 당해연도, 전년도, 전월, 당월, 어제, 오늘, 새로고침 + +#### 2.2.3 계정과목명 셀렉트 + 저장 버튼 +- [ ] 계정과목명 Select (출금 유형 옵션) +- [ ] 저장 버튼 → "N개의 출금 유형을 {계정과목명}으로 모두 변경하시겠습니까?" Alert + +#### 2.2.4 필터 (3개) +| 필터 | 옵션 | +|------|------| +| 거래처 | 전체, 거래처 목록 | +| 출금유형 | 전체, 매입대금, 선급금, 가지급금, 임대료, 이자비용, 보증금 지급, 차입금 상환, 배당금 지급, 부가세 납부, 급여, 4대보험, 세금, 공과금, 경비, 기타, 미설정 | +| 정렬 | 최신순, 등록순, 금액 높은순, 금액 낮은순 | + +#### 2.2.5 테이블 컬럼 (체크박스 + 7개 + 작업) +| 순서 | 컬럼명 | 정렬 | 비고 | +|------|--------|------|------| +| 0 | 체크박스 | center | - | +| 1 | 출금일 | center | - | +| 2 | 출금계좌 | left | - | +| 3 | 수취인명 | left | - | +| 4 | 출금금액 | right | 금액 포맷 | +| 5 | 거래처 | left | 미설정시 빨간색 | +| 6 | 출금유형 | center | Badge | +| 7 | 작업 | center | 선택 시 수정/삭제 버튼 | + +#### 2.2.6 테이블 합계 행 +- [ ] 출금금액 합계 + +--- + +## Phase 3: 상세 페이지 구현 + +### 3.1 페이지 라우트 +- [ ] `/src/app/[locale]/(protected)/accounting/withdrawals/[id]/page.tsx` + +### 3.2 WithdrawalDetail 컴포넌트 +- [ ] `/src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx` +- [ ] mode prop: `view` | `edit` + +#### 3.2.1 상단 버튼 +| 모드 | 버튼 | +|------|------| +| view | 목록, 삭제, 수정 | +| edit | 취소, 저장 | + +#### 3.2.2 기본 정보 섹션 +| 필드명 | 타입 | 편집 가능 | 비고 | +|--------|------|----------|------| +| 출금일 | date | ❌ | 읽기 전용 | +| 출금계좌 | text | ❌ | 읽기 전용 | +| 수취인명 | text | ❌ | 읽기 전용 | +| 출금금액 | number | ❌ | 읽기 전용 | +| 적요 | select | ✅ | 선택 가능 | +| 거래처 | select | ✅ | 필수 | +| 출금 유형 | select | ✅ | 필수 | + +--- + +## Phase 4: 파일 구조 + +``` +src/ +├── app/[locale]/(protected)/accounting/withdrawals/ +│ ├── page.tsx # 리스트 페이지 +│ └── [id]/ +│ └── page.tsx # 상세/수정 페이지 +└── components/accounting/WithdrawalManagement/ + ├── index.tsx # 리스트 컴포넌트 + ├── WithdrawalDetail.tsx # 상세/수정 컴포넌트 + └── types.ts # 타입 및 상수 정의 +``` + +--- + +## 테스트 URL + +| 페이지 | URL | +|--------|-----| +| 리스트 | `/ko/accounting/withdrawals` | +| 상세 | `/ko/accounting/withdrawals/withdrawal-1` | +| 수정 | `/ko/accounting/withdrawals/withdrawal-1?mode=edit` | \ No newline at end of file diff --git a/claudedocs/accounting/[IMPL-2025-12-19] bad-debt-collection-management.md b/claudedocs/accounting/[IMPL-2025-12-19] bad-debt-collection-management.md new file mode 100644 index 00000000..4f3222a0 --- /dev/null +++ b/claudedocs/accounting/[IMPL-2025-12-19] bad-debt-collection-management.md @@ -0,0 +1,230 @@ +# 악성채권 추심관리 구현 계획서 + +> 작성일: 2025-12-19 +> URL: `/ko/accounting/bad-debt-collection` + +--- + +## 📋 스크린샷 분석 요약 + +### 리스트 화면 +- **제목**: 악성채권 추심관리 +- **통계 카드 4개**: 총 악성채권, 추심중, 법적조치, 회수완료 +- **필터 3개**: + 1. 거래처 필터 (검색 + 다중선택, 디폴트: 전체) + 2. 상태 필터 (전체, 추심중, 법적조치, 회수완료, 대손처리) + 3. 정렬 (최신순, 등록순, 디폴트: 최신순) +- **테이블 컬럼**: No., 거래처, 채권금액, 발생일, 연체일수, 담당자, 상태, 설정, 작업 +- **설정 컬럼**: ON/OFF 토글 +- **작업 컬럼**: 수정/삭제 아이콘 + +### 상세 화면 (view/edit/new 모드) +1. **기본 정보**: 사업자등록번호, 거래처코드, 거래처명, 대표자명, 거래처유형(토글), 악성채권등록(업태/업종) +2. **연락처 정보**: 주소(우편번호 찾기), 전화번호, 모바일, 팩스, 이메일 +3. **담당자 정보**: 담당자명, 담당자전화, 시스템관리자 +4. **필요 서류**: 사업자등록증, 세금계산서, 추가서류 (파일 첨부) +5. **악성 채권 정보**: + - 미수금 + 상태 셀렉트 (추심중, 법적조치, 회수완료, 대손처리) + - 연체일수 + 본사 담당자 셀렉트 (부서명 이름 직급명 연락처) + - 악성채권 발생일 / 종료일 + - **수취 어음 현황 버튼** → 어음관리 화면 (해당 거래처 필터) + - **거래처 미수금 현황 버튼** → 미수금 현황 화면 (해당 거래처 하이라이트) +6. **메모**: 타임스탬프 형식 메모 목록 + +--- + +## ✅ 구현 체크리스트 + +### Phase 1: 파일 구조 및 타입 정의 +- [x] 1.1 `src/components/accounting/BadDebtCollection/types.ts` 생성 + - BadDebtRecord 인터페이스 + - CollectionStatus 타입 (추심중, 법적조치, 회수완료, 대손처리) + - SortOption 타입 + - 상태 라벨/컬러 상수 +- [x] 1.2 `src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx` 생성 +- [x] 1.3 `src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/page.tsx` 생성 +- [x] 1.4 `src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/edit/page.tsx` 생성 +- [x] 1.5 `src/app/[locale]/(protected)/accounting/bad-debt-collection/new/page.tsx` 생성 + +### Phase 2: 리스트 컴포넌트 구현 +- [x] 2.1 `src/components/accounting/BadDebtCollection/index.tsx` 생성 + - IntegratedListTemplateV2 사용 +- [x] 2.2 통계 카드 4개 구현 (총 악성채권, 추심중, 법적조치, 회수완료) +- [x] 2.3 필터 3개 구현 + - 거래처 필터 (검색 + 다중선택) + - 상태 필터 (전체, 추심중, 법적조치, 회수완료, 대손처리) + - 정렬 (최신순, 등록순) +- [x] 2.4 테이블 컬럼 구현 (체크박스 + No. + 거래처 + 채권금액 + 발생일 + 연체일수 + 담당자 + 상태 + 설정 + 작업) + - No.: 순번 (1부터) + - 거래처: 거래처명 + - 채권금액: 금액 (원) + - 발생일: YYYY-MM-DD + - 연체일수: 숫자 + "일" + - 담당자: 담당자명 + - 상태: Badge (추심중/법적조치/회수완료/대손처리) + - 설정: ON/OFF 토글 (Switch) + - 작업: 수정/삭제 아이콘 (row 선택 시 표시) +- [x] 2.5 Mock 데이터 생성 함수 +- [x] 2.6 행 클릭 → 상세 페이지 이동 기능 +- [x] 2.7 모바일 카드 렌더링 + +### Phase 3: 상세/수정/등록 컴포넌트 구현 +- [x] 3.1 `src/components/accounting/BadDebtCollection/BadDebtDetail.tsx` 생성 + - mode prop (view/edit/new) +- [x] 3.2 기본 정보 섹션 + - 사업자등록번호 (Input, readonly) + - 거래처 코드 (Input, readonly) + - 거래처명 (Input) + - 대표자명 (Input) + - 거래처 유형 (Switch - 매출매입) + - 악성채권 등록 - 업태 (Input) + - 악성채권 등록 - 업종 (Input) +- [x] 3.3 연락처 정보 섹션 + - 주소 (우편번호 찾기 버튼 + Input 3개) + - 전화번호 (Input) + - 모바일 (Input) + - 팩스 (Input) + - 이메일 (Input) +- [x] 3.4 담당자 정보 섹션 + - 담당자명 (Input) + - 담당자 전화 (Input) + - 시스템 관리자 (Input, readonly) +- [x] 3.5 필요 서류 섹션 + - 사업자등록증 (파일 찾기) + - 세금계산서 (파일 찾기) + - 추가 서류 (파일 찾기 + 추가 버튼 + 삭제 X) +- [x] 3.6 악성 채권 정보 섹션 + - 미수금 (Input, readonly) + - 상태 (Select: 추심중, 법적조치, 회수완료, 대손처리) + - 연체일수 (Switch + Input) + - 본사 담당자 (Select: 부서명 이름 직급명 연락처) + - 악성채권 발생일 (DatePicker) + - 악성채권 종료일 (DatePicker) + - **수취 어음 현황 버튼** → `/ko/accounting/bills?vendorId={id}&type=received` + - **거래처 미수금 현황 버튼** → `/ko/accounting/receivables-status?highlight={id}` +- [x] 3.7 메모 섹션 + - 메모 목록 (Textarea, readonly) + - 메모 추가 기능 (타임스탬프 자동 생성) +- [x] 3.8 삭제/수정 버튼 및 다이얼로그 +- [x] 3.9 저장 기능 (mock) + +### Phase 4: 연동 기능 구현 +- [x] 4.1 어음관리 페이지 수정 - vendorId 쿼리 파라미터 지원 +- [x] 4.2 미수금 현황 페이지 수정 - highlight 쿼리 파라미터 지원 (해당 거래처 행 하이라이트) + +### Phase 5: 최종 검증 및 문서화 +- [x] 5.1 리스트 페이지 테스트 +- [x] 5.2 상세/수정/등록 페이지 테스트 +- [x] 5.3 어음관리 연동 테스트 +- [x] 5.4 미수금 현황 연동 테스트 +- [x] 5.5 모바일 반응형 테스트 +- [x] 5.6 `[REF] all-pages-test-urls.md` 업데이트 + +--- + +## 📁 파일 구조 + +``` +src/ +├── app/[locale]/(protected)/accounting/ +│ └── bad-debt-collection/ +│ ├── page.tsx # 리스트 페이지 +│ ├── new/ +│ │ └── page.tsx # 등록 페이지 +│ └── [id]/ +│ ├── page.tsx # 상세 페이지 +│ └── edit/ +│ └── page.tsx # 수정 페이지 +└── components/accounting/ + └── BadDebtCollection/ + ├── index.tsx # 리스트 컴포넌트 + ├── BadDebtDetail.tsx # 상세/수정/등록 컴포넌트 + └── types.ts # 타입 정의 +``` + +--- + +## 🔗 URL 구조 + +| 페이지 | URL | 비고 | +|--------|-----|------| +| 리스트 | `/ko/accounting/bad-debt-collection` | 메인 | +| 등록 | `/ko/accounting/bad-debt-collection/new` | | +| 상세 | `/ko/accounting/bad-debt-collection/[id]` | | +| 수정 | `/ko/accounting/bad-debt-collection/[id]/edit` | | + +--- + +## 📊 테이블 컬럼 상세 + +| 컬럼 | key | label | 정렬 | 비고 | +|------|-----|-------|------|------| +| 체크박스 | checkbox | - | center | Checkbox | +| 번호 | no | No. | center | 1부터 시작 | +| 거래처 | vendorName | 거래처 | left | | +| 채권금액 | debtAmount | 채권금액 | right | 1,000,000원 | +| 발생일 | occurrenceDate | 발생일 | center | YYYY-MM-DD | +| 연체일수 | overdueDays | 연체일수 | center | 100일 | +| 담당자 | managerName | 담당자 | left | | +| 상태 | status | 상태 | center | Badge | +| 설정 | settingToggle | 설정 | center | Switch | +| 작업 | actions | 작업 | center | 수정/삭제 | + +--- + +## 🎨 상태 Badge 스타일 + +| 상태 | 라벨 | 스타일 | +|------|------|--------| +| collecting | 추심중 | `border-orange-300 text-orange-600 bg-orange-50` | +| legalAction | 법적조치 | `border-red-300 text-red-600 bg-red-50` | +| recovered | 회수완료 | `border-green-300 text-green-600 bg-green-50` | +| badDebt | 대손처리 | `border-gray-300 text-gray-600 bg-gray-50` | + +--- + +## 🔍 필터 상세 + +### 1. 거래처 필터 +- **타입**: 검색 + 다중선택 (Combobox/MultiSelect) +- **옵션**: 전체, 거래처 목록 (API) +- **디폴트**: 전체 + +### 2. 상태 필터 +- **타입**: Select +- **옵션**: 전체, 추심중, 법적조치, 회수완료, 대손처리 +- **디폴트**: 악성채권 설정 시 디폴트 상태 + +### 3. 정렬 +- **타입**: Select +- **옵션**: 최신순, 등록순 +- **디폴트**: 최신순 + +--- + +## 🔗 연동 기능 + +### 수취 어음 현황 버튼 +- **대상**: 어음관리 페이지 (`/ko/accounting/bills`) +- **쿼리**: `?vendorId={id}&type=received` +- **동작**: 해당 거래처의 수취 어음만 필터링하여 표시 + +### 거래처 미수금 현황 버튼 +- **대상**: 미수금 현황 페이지 (`/ko/accounting/receivables-status`) +- **쿼리**: `?highlight={id}` +- **동작**: 해당 거래처 행을 하이라이트 표시 + +--- + +## 📝 작업 완료 후 체크 + +- [x] `claudedocs/[REF] all-pages-test-urls.md` 업데이트 +- [x] 빌드 오류 없음 확인 +- [x] 리스트/상세/수정/등록 페이지 정상 작동 +- [x] 연동 페이지 정상 작동 + +--- + +## ✅ 구현 완료 + +> 완료일: 2025-12-19 \ No newline at end of file diff --git a/claudedocs/accounting/[IMPL-2025-12-19] card-transaction-inquiry.md b/claudedocs/accounting/[IMPL-2025-12-19] card-transaction-inquiry.md new file mode 100644 index 00000000..572f81b5 --- /dev/null +++ b/claudedocs/accounting/[IMPL-2025-12-19] card-transaction-inquiry.md @@ -0,0 +1,89 @@ +# 카드 내역 조회 구현 체크리스트 + +## 개요 +- **페이지 경로**: `/ko/accounting/card-transactions` +- **참고**: 입출금 계좌조회 페이지와 유사한 구조 + +## 화면 구성 + +### 1. 상단 영역 +- 제목: "카드 내역 조회" +- 부제: "법인카드 사용 내역을 조회합니다" + +### 2. 날짜 선택 + 빠른 버튼 +- 날짜 범위 선택 +- 빠른 버튼: 당해년도, 전전월, 전월, 당월, 어제, 오늘 +- 새로고침 버튼 (출금관리 스타일, 오른쪽 위치) + +### 3. 요약 카드 (2개) +- 전월 사용액: 금액 표시 +- 당월 사용액: 금액 표시 + +### 4. 필터 영역 +- 검색 입력창 (좌측) +- 총 N건 표시 +- 필터 2개: + - 카드명 필터 (전체, 카드명 목록) - 디폴트: 전체 + - 정렬 필터 (최신순, 등록순, 금액 높은순, 금액 낮은순) - 디폴트: 최신순 + +### 5. 테이블 +- **체크박스 없음** +- **번호 컬럼 없음** +- 컬럼 순서: + 1. 카드 + 2. 카드명 + 3. 사용자 + 4. 사용일시 + 5. 가맹점명 + 6. 사용금액 + +### 6. 합계 행 +- 테이블 마지막에 합계 행 +- 사용금액 열에만 합계 표시 + +--- + +## 구현 체크리스트 + +### Phase 1: 파일 구조 생성 +- [x] types.ts 생성 (타입 정의) +- [x] index.tsx 생성 (메인 컴포넌트) +- [x] page.tsx 생성 (라우트) + +### Phase 2: 타입 정의 +- [x] CardTransaction 인터페이스 +- [x] SortOption 타입 + +### Phase 3: 컴포넌트 구현 +- [x] 헤더 영역 (제목, 부제) +- [x] 날짜 선택 + 빠른 버튼 +- [x] 새로고침 버튼 +- [x] 요약 카드 2개 (전월/당월 사용액) +- [x] 검색 + 필터 영역 (카드명, 정렬) +- [x] 테이블 (체크박스/번호 없음) +- [x] 합계 행 + +### Phase 4: 테스트 및 문서화 +- [x] all-pages-test-urls.md 업데이트 + +### Phase 5: 템플릿 기능 추가 +- [x] IntegratedListTemplateV2에 showCheckbox props 추가 +- [x] 조건부 체크박스 렌더링 구현 + +--- + +## Mock 데이터 예시 +```typescript +{ + card: '신한 1234', + cardName: '법인카드1', + user: '홍길동', + usedAt: '2025-12-12 12:12', + merchantName: '가맹점명', + amount: 100000 +} +``` + +## 참고 파일 +- `src/components/accounting/BankTransactionInquiry/index.tsx` +- `src/components/accounting/BankTransactionInquiry/types.ts` \ No newline at end of file diff --git a/claudedocs/accounting/[PLAN-2025-12-18] sales-management.md b/claudedocs/accounting/[PLAN-2025-12-18] sales-management.md new file mode 100644 index 00000000..86032a66 --- /dev/null +++ b/claudedocs/accounting/[PLAN-2025-12-18] sales-management.md @@ -0,0 +1,270 @@ +# [PLAN-2025-12-18] 매출관리 페이지 구현 계획서 + +## 개요 +- **목표**: 회계관리 > 매출관리 페이지 구현 +- **참조**: 기안함(DraftBox) 구조 기반 +- **페이지 수**: 2개 (리스트 + 상세/등록) + +--- + +## 1. 리스트 페이지 (매출관리) + +### 1.1 페이지 구조 +- [ ] 경로: `/accounting/sales` +- [ ] 컴포넌트: `src/components/accounting/SalesManagement/index.tsx` +- [ ] IntegratedListTemplateV2 사용 + +### 1.2 헤더 영역 +- [ ] DateRangeSelector (공통 달력) +- [ ] 상태 필터 버튼들 + - [ ] 당월마감 + - [ ] 전월 + - [ ] 합의 + - [ ] 미수 + - [ ] 전체 +- [ ] 매출 등록 버튼 (클릭 시 등록 페이지로 이동) + +### 1.3 통계 카드 (4개) +- [ ] 매출금액 합계 (예: 3,123,000원) +- [ ] 입금금액 합계 (예: 3,123,000원) +- [ ] 미수건수 (예: 3건) +- [ ] 전체건수 (예: 4건) + +### 1.4 필터 셀렉트 박스들 +| 필터명 | 설명 | 옵션 | 기본값 | +|--------|------|------|--------| +| 계정과목별 | 계정과목명으로 분류 | 전체, 외상 매출, 상품 매출, 부품 매출, 공사 매출, 임대 수익, 기타 매출 | 제품 매출 | +| 거래처 필터 | 거래처별 검색/필터 | 거래처 검색 (검색 가능) | - | +| 매출유형 필터 | 다중 선택 가능 | 외상 매출, 상품 매출, 부품 매출, 공사 매출, 임대 수익, 기타 매출 | 전체 | +| 정렬 | 단독 선택 | 최신순, 금액 높은 순, 금액 낮은 순 | 최신순 | + +### 1.5 테이블 컬럼 +| 컬럼 | 설명 | +|------|------| +| 체크박스 | 다중 선택 | +| 번호 | 순번 | +| 매출번호 | 자동 채번 (포맷: 조합+자동생성) | +| 매출일 | 날짜 | +| 거래처명 | 거래처 | +| 결제대면(?) | 결제 방식 | +| 매출금액 | 금액 | +| 미수금액 | 미수 금액 | +| 비고 | 적요 | +| 작업 | 수정/삭제 아이콘 | + +### 1.6 기능 +- [ ] 검색 기능 (매출번호, 거래처명, 적요) +- [ ] 페이지네이션 (20건씩) +- [ ] 체크박스 전체/개별 선택 +- [ ] 행 클릭 → 상세 페이지 이동 + +--- + +## 2. 상세/등록 페이지 (매출 상세) + +### 2.1 페이지 구조 +- [ ] 경로: `/accounting/sales/[id]` (상세/수정) +- [ ] 경로: `/accounting/sales/new` (신규 등록) +- [ ] 컴포넌트: `src/components/accounting/SalesManagement/SalesDetail.tsx` + +### 2.2 헤더 영역 +- [ ] 페이지 제목: "매출 상세" 또는 "매출 상세_직접 등록" +- [ ] 버튼 + - [ ] 삭제 버튼 (신규 등록 시) + - [ ] 수정 버튼 + +### 2.3 기본 정보 섹션 +| 필드 | 타입 | 설명 | +|------|------|------| +| 매출번호 | 텍스트 (읽기전용/자동채번) | 수정 시 표시, 신규 시 자동 채번 | +| 매출일 | DatePicker | 날짜 선택 | +| 거래처명 | Select (검색 가능) | 거래처 선택 | +| 매출 유형 | Select | 외상 매출, 상품 매출, 부품 매출, 공사 매출, 임대 수익, 기타 매출 (기본: 제품 매출) | + +### 2.4 품목 정보 섹션 +- [ ] 테이블 형태의 품목 입력 +| 컬럼 | 타입 | 설명 | +|------|------|------| +| 번호 | 자동 | 순번 | +| 품목명 | Select/Input | 품목 선택 또는 입력 | +| 수량 | Number | 수량 입력 | +| 단가 | Number | 단가 입력 | +| 공급가액 | Number (계산) | 수량 × 단가 자동 계산 | +| 부가세 | Number (계산) | 공급가액 × 10% 자동 계산 | +| 적요 | Text | 메모 | +| 삭제 | Button | 행 삭제 (X 버튼) | + +- [ ] 합계 행: 공급가액 합계, 부가세 합계 +- [ ] 추가 버튼: 품목 행 추가 + +### 2.5 세금계산서 섹션 +- [ ] 세금계산서 발행 토글 (Switch) + - 클릭 시: 미발행 ↔ 발행완료 토글 + - 세금계산서 수동 발행 후 발행 상태로 변경 + +### 2.6 거래명세서 섹션 +- [ ] 거래명세서 발행 토글 (Switch) + - 클릭 시: 미발행 ↔ 발행완료 토글 +- [ ] 거래명세서 발행하기 버튼 + - 클릭 시: 거래처 이메일로 자동 발송 + - Alert: "거래명세서가 'abc@email.com'으로 발송되었습니다" + - 발행 후 자동으로 발행 상태 변경 + +--- + +## 3. 파일 구조 + +``` +src/ +├── app/[locale]/(protected)/accounting/ +│ └── sales/ +│ ├── page.tsx # 리스트 페이지 +│ ├── [id]/ +│ │ └── page.tsx # 상세/수정 페이지 +│ └── new/ +│ └── page.tsx # 신규 등록 페이지 +│ +└── components/accounting/ + └── SalesManagement/ + ├── index.tsx # 리스트 컴포넌트 + ├── SalesDetail.tsx # 상세/등록 컴포넌트 + ├── SalesItemTable.tsx # 품목 테이블 컴포넌트 + ├── TaxInvoiceSection.tsx # 세금계산서 섹션 + ├── TransactionStatementSection.tsx # 거래명세서 섹션 + └── types.ts # 타입 정의 +``` + +--- + +## 4. 타입 정의 (types.ts) + +```typescript +// 매출 레코드 +interface SalesRecord { + id: string; + salesNo: string; // 매출번호 + salesDate: string; // 매출일 + vendorId: string; // 거래처 ID + vendorName: string; // 거래처명 + salesType: SalesType; // 매출 유형 + items: SalesItem[]; // 품목 목록 + totalSupplyAmount: number; // 공급가액 합계 + totalVat: number; // 부가세 합계 + totalAmount: number; // 총 금액 + receivedAmount: number; // 입금액 + outstandingAmount: number; // 미수금액 + taxInvoiceIssued: boolean; // 세금계산서 발행 여부 + transactionStatementIssued: boolean; // 거래명세서 발행 여부 + note: string; // 비고 + status: SalesStatus; // 상태 + createdAt: string; + updatedAt: string; +} + +// 매출 유형 +type SalesType = + | 'credit' // 외상 매출 + | 'product' // 제품 매출 + | 'goods' // 상품 매출 + | 'parts' // 부품 매출 + | 'construction'// 공사 매출 + | 'rental' // 임대 수익 + | 'other'; // 기타 매출 + +// 매출 품목 +interface SalesItem { + id: string; + itemName: string; // 품목명 + quantity: number; // 수량 + unitPrice: number; // 단가 + supplyAmount: number; // 공급가액 + vat: number; // 부가세 + note: string; // 적요 +} + +// 매출 상태 +type SalesStatus = + | 'monthlyClose' // 당월마감 + | 'lastMonth' // 전월 + | 'agreed' // 합의 + | 'outstanding' // 미수 + | 'all'; // 전체 + +// 필터 옵션 +type FilterOption = 'all' | 'monthlyClose' | 'lastMonth' | 'agreed' | 'outstanding'; + +// 정렬 옵션 +type SortOption = 'latest' | 'amountHigh' | 'amountLow'; +``` + +--- + +## 5. 구현 체크리스트 + +### Phase 1: 기본 구조 설정 +- [ ] 폴더 구조 생성 +- [ ] 라우트 페이지 생성 (page.tsx들) +- [ ] types.ts 작성 + +### Phase 2: 리스트 페이지 구현 +- [ ] SalesManagement/index.tsx 생성 +- [ ] IntegratedListTemplateV2 연동 +- [ ] DateRangeSelector 연동 +- [ ] 상태 필터 버튼 구현 +- [ ] 통계 카드 구현 +- [ ] 필터 셀렉트 박스들 구현 +- [ ] 테이블 렌더링 구현 +- [ ] 모바일 카드 렌더링 구현 +- [ ] 페이지네이션 구현 +- [ ] 검색 기능 구현 +- [ ] Mock 데이터 생성 + +### Phase 3: 상세/등록 페이지 구현 +- [ ] SalesDetail.tsx 생성 +- [ ] 기본 정보 섹션 구현 +- [ ] SalesItemTable.tsx 구현 (품목 테이블) + - [ ] 품목 행 추가/삭제 + - [ ] 자동 계산 (공급가액, 부가세) + - [ ] 합계 행 +- [ ] TaxInvoiceSection.tsx 구현 +- [ ] TransactionStatementSection.tsx 구현 +- [ ] 폼 유효성 검사 +- [ ] 저장/수정 로직 + +### Phase 4: 연동 및 테스트 +- [ ] 리스트 ↔ 상세 페이지 연결 +- [ ] 신규 등록 플로우 확인 +- [ ] 수정 플로우 확인 +- [ ] 반응형 레이아웃 확인 + +--- + +## 6. 참고 사항 + +### 기안함(DraftBox)에서 참고할 패턴 +- IntegratedListTemplateV2 사용법 +- DateRangeSelector 연동 +- 필터/정렬 셀렉트 박스 패턴 +- 테이블/모바일 카드 렌더링 +- 체크박스 선택 관리 +- 페이지네이션 + +### 주의 사항 +1. 매출번호 자동 채번 로직 확인 필요 (API 연동 시) +2. 거래처 목록 API 연동 필요 +3. 품목 목록 API 연동 필요 +4. 세금계산서/거래명세서 발행 API 연동 필요 + +--- + +## 7. 예상 작업 시간 +- Phase 1: 기본 구조 설정 +- Phase 2: 리스트 페이지 구현 +- Phase 3: 상세/등록 페이지 구현 +- Phase 4: 연동 및 테스트 + +--- + +**작성일**: 2025-12-18 +**작성자**: Claude +**상태**: 계획 검토 대기 \ No newline at end of file diff --git a/claudedocs/accounting/[PLAN-2025-12-19] bank-account-transaction-inquiry.md b/claudedocs/accounting/[PLAN-2025-12-19] bank-account-transaction-inquiry.md new file mode 100644 index 00000000..b76a92ac --- /dev/null +++ b/claudedocs/accounting/[PLAN-2025-12-19] bank-account-transaction-inquiry.md @@ -0,0 +1,204 @@ +# [PLAN-2025-12-19] 입출금 계좌조회 페이지 구현 계획서 + +> **작성일**: 2025-12-19 +> **상태**: 📋 대기 (사용자 확인 필요) +> **참조**: DepositManagement, IntegratedListTemplateV2 + +--- + +## 1. 페이지 개요 + +| 항목 | 내용 | +|------|------| +| **페이지명** | 입출금 계좌조회 | +| **설명** | 은행 계좌 정보와 입출금 내역을 조회할 수 있습니다 | +| **URL** | `/ko/accounting/bank-transactions` | +| **아이콘** | `Building2` (은행) 또는 `Wallet` | + +--- + +## 2. UI 구성 분석 (스크린샷 기준) + +### 2.1 헤더 영역 +- [x] 페이지 타이틀: "입출금 계좌조회" +- [x] 설명: "은행 계좌 정보와 입출금 내역을 조회할 수 있습니다" + +### 2.2 필터 영역 (DateRangeSelector 확장) +- [ ] 기간 선택: 시작일 ~ 종료일 (DateRangeSelector 컴포넌트) +- [ ] 탭 버튼 그룹: + - `전체(선택)` - 전체 입출금 내역 + - `입금/수입` - 입금만 필터 + - `출금` - 출금만 필터 + - `입금` - (중복인지 확인 필요, 스크린샷 확인) + - `어제` - 어제 날짜 빠른 선택 + - `오늘` - 오늘 날짜 빠른 선택 + - `새로고침` - 데이터 새로고침 버튼 + +### 2.3 통계 카드 (4개) +| 순서 | 라벨 | 값 예시 | 아이콘 색상 | +|------|-----------|---------|-------------| +| 1 | 입금 | 3,123,000원 | 🔵 blue | +| 2 | 출금 | 3,123,000원 | 🔴 red | +| 3 | 입금 유형 미설정 | 4건 | 🟢 green | +| 4 | 출금 유형 미설정 | 4건 | 🟠 orange | + +### 2.4 검색 영역 +- [ ] 검색창 (은행명, 계좌번호, 거래처, 비고 검색) + +### 2.5 테이블 컬럼 (14개) +| 순서 | 컬럼명 | key | 정렬 | 비고 | +|------|--------|-----|------|------| +| 1 | 체크박스 | checkbox | center | 선택용 | +| 2 | 번호 | no | center | globalIndex | +| 3 | 은행명 | bankName | left | | +| 4 | 계좌명 | accountName | left | | +| 5 | 거래일시 | transactionDate | center | | +| 6 | 구분 | type | center | 입금/출금 Badge | +| 7 | 적요 | note | left | | +| 8 | 거래처 | vendorName | left | | +| 9 | 입금자/수취인 | depositorName | left | | +| 10 | 입금 | depositAmount | right | 숫자 포맷 | +| 11 | 출금 | withdrawalAmount | right | 숫자 포맷 | +| 12 | 잔액 | balance | right | 숫자 포맷 | +| 13 | 입출금 유형 | transactionType | center | Badge | +| 14 | 작업 | actions | center | 수정 버튼 (체크 시 표시) | + +### 2.6 테이블 합계 행 +- [ ] 합계 행 표시 (입금액 합계, 출금액 합계) + +### 2.7 수정 버튼 동작 +- [ ] 수정 버튼 클릭 시: + - 입금 데이터 → `/ko/accounting/deposits/{id}?mode=edit` 이동 + - 출금 데이터 → `/ko/accounting/withdrawals/{id}?mode=edit` 이동 + +--- + +## 3. 구현 체크리스트 + +### Phase 1: 기본 구조 설정 +- [ ] 1.1 페이지 라우트 생성 (`/accounting/bank-transactions/page.tsx`) +- [ ] 1.2 컴포넌트 폴더 생성 (`/components/accounting/BankTransactionInquiry/`) +- [ ] 1.3 types.ts 작성 (타입 정의) +- [ ] 1.4 index.tsx 기본 구조 작성 + +### Phase 2: 타입 정의 +- [ ] 2.1 `BankTransaction` 인터페이스 정의 + ```typescript + interface BankTransaction { + id: string; + bankName: string; // 은행명 + accountName: string; // 계좌명 + transactionDate: string; // 거래일시 + type: 'deposit' | 'withdrawal'; // 구분 (입금/출금) + note?: string; // 적요 + vendorId?: string; // 거래처 ID + vendorName?: string; // 거래처명 + depositorName?: string; // 입금자/수취인 + depositAmount: number; // 입금 + withdrawalAmount: number; // 출금 + balance: number; // 잔액 + transactionType?: string; // 입출금 유형 + sourceId: string; // 원본 입금/출금 ID (상세 이동용) + } + ``` +- [ ] 2.2 필터 타입 정의 +- [ ] 2.3 정렬 옵션 정의 + +### Phase 3: 통계 카드 구현 +- [ ] 3.1 총 입금 카드 +- [ ] 3.2 총 출금 카드 +- [ ] 3.3 입금 건 카드 +- [ ] 3.4 출금 건 카드 + +### Phase 4: 필터 영역 구현 +- [ ] 4.1 DateRangeSelector 연동 +- [ ] 4.2 탭 버튼 그룹 구현 (전체/입금/수입/출금/어제/오늘) +- [ ] 4.3 새로고침 버튼 +- [ ] 4.4 빠른 날짜 선택 (어제/오늘) 로직 + +### Phase 5: 테이블 구현 +- [ ] 5.1 IntegratedListTemplateV2 연동 +- [ ] 5.2 테이블 컬럼 정의 (14개 컬럼) +- [ ] 5.3 체크박스 기능 +- [ ] 5.4 번호 컬럼 (globalIndex 사용) +- [ ] 5.5 구분 컬럼 Badge (입금: 파랑, 출금: 빨강) +- [ ] 5.6 금액 컬럼 숫자 포맷팅 +- [ ] 5.7 합계 행 구현 (tableFooter) +- [ ] 5.8 작업 컬럼 (체크 시 수정 버튼 표시) + +### Phase 6: 상세 이동 로직 +- [ ] 6.1 수정 버튼 클릭 핸들러 +- [ ] 6.2 type에 따른 분기 처리 + - deposit → `/ko/accounting/deposits/{sourceId}?mode=edit` + - withdrawal → `/ko/accounting/withdrawals/{sourceId}?mode=edit` + +### Phase 7: Mock 데이터 및 테스트 +- [ ] 7.1 Mock 데이터 생성 함수 +- [ ] 7.2 필터링 로직 테스트 +- [ ] 7.3 정렬 로직 테스트 +- [ ] 7.4 페이지네이션 테스트 + +### Phase 8: 마무리 +- [ ] 8.1 모바일 카드 뷰 구현 +- [ ] 8.2 코드 정리 및 최적화 +- [ ] 8.3 테스트 URL 문서 업데이트 (`[REF] all-pages-test-urls.md`) + +--- + +## 4. 파일 구조 + +``` +src/ +├── app/[locale]/(protected)/accounting/ +│ └── bank-transactions/ +│ └── page.tsx # 페이지 라우트 +└── components/accounting/ + └── BankTransactionInquiry/ + ├── index.tsx # 메인 컴포넌트 + └── types.ts # 타입 정의 +``` + +--- + +## 5. 참고 사항 + +### 5.1 기존 컴포넌트 재사용 +- `IntegratedListTemplateV2` - 리스트 템플릿 +- `DateRangeSelector` - 날짜 범위 선택 +- `StatCard` - 통계 카드 +- `ListMobileCard` - 모바일 카드 + +### 5.2 스크린샷 Description 정보 참고 +- 필터 탭: 전체(선택), 입금/수입, 출금, 입금, 어제, 오늘 +- 수정 버튼 클릭 시 입금/출금 상세 화면으로 이동 +- 종류(정렬): 취입선, 등록순, 입력순 + +### 5.3 레이아웃 일치 확인 사항 +- DepositManagement와 동일한 레이아웃 구조 사용 +- 카드 4개 가로 배치 +- 테이블 합계 행 스타일 일치 + +--- + +## 6. 완료 후 작업 + +- [ ] `claudedocs/[REF] all-pages-test-urls.md` 업데이트 + ```markdown + | 입출금 계좌조회 | `/ko/accounting/bank-transactions` | 🆕 NEW | + ``` + +--- + +## 7. 예상 소요 시간 + +| Phase | 작업 | 예상 | +|-------|------|------| +| 1-2 | 기본 구조 + 타입 | - | +| 3-4 | 카드 + 필터 | - | +| 5 | 테이블 | - | +| 6-7 | 상세 이동 + Mock | - | +| 8 | 마무리 | - | + +--- + +**확인 후 작업 시작하겠습니다!** \ No newline at end of file diff --git a/claudedocs/board/[PLAN-2025-12-19] board-management-implementation.md b/claudedocs/board/[PLAN-2025-12-19] board-management-implementation.md new file mode 100644 index 00000000..7e690b5b --- /dev/null +++ b/claudedocs/board/[PLAN-2025-12-19] board-management-implementation.md @@ -0,0 +1,313 @@ +ㅓ# 게시판 관리 기능 구현 계획서 + +> 작성일: 2025-12-19 +> 상태: 🔴 **계획 검토 대기** + +--- + +## 1. 개요 + +### 1.1 기능 요약 +게시판 리스트/등록/상세/댓글 기능 구현 + +### 1.2 참고 페이지 +- **탭 네비게이션**: `src/components/items/ItemListClient.tsx` (품목관리) +- **테이블 필터 위치**: `src/components/accounting/SalesManagement/index.tsx` (매출관리) +- **공통 레이아웃**: `IntegratedListTemplateV2` 템플릿 + +### 1.3 디자인 스펙 (스크린샷 기준) + +#### 리스트 페이지 +| 항목 | 내용 | +|------|------| +| 페이지 타이틀 | 게시판 | +| 페이지 설명 | 게시판의 게시글을 등록하고 관리합니다. | +| 날짜 범위 | 2025-09-01 ~ 2025-09-03 | +| 탭 | 전체보드, 전자결재, 인쇄, 미역, 우울 + **게시글 등록** 버튼 | +| 게시판 필터 | 공지사항 (게시판명, 게시판명2, 나의 게시글) | +| 검색 | 검색 입력창 | +| 정렬 | 최신순 ▼ | +| 테이블 컬럼 | No., 제목, 작성자, 등록일, 조회수 | +| 행 클릭 | 게시글 상세 화면으로 이동 | + +#### 등록/수정 페이지 +| 항목 | 내용 | +|------|------| +| 페이지 타이틀 | 게시글 상세 | +| 페이지 설명 | 게시글을 등록하고 관리합니다. | +| **게시글 정보** (필수 섹션) | | +| - 게시판 | Select: "게시판을 선택해주세요" | +| - 상단 노출 | Radio: 사용안함 / **사용함** | +| - 제목 | Input: "제목을 입력해주세요" | +| - 내용 | **WYSIWYG 에디터** (B, I, U, S, 정렬, 목록, 링크, 이미지 등) | +| - 첨부파일 | 파일 찾기 버튼 | +| - 작성자 | 읽기 전용 (예: 홍길동) | +| - 댓글 | Radio: **사용안함** / 사용함 | +| - 등록일시 | 읽기 전용 (예: 2025-09-09 12:20) | +| 상단 노출 제한 | 최대 5개까지 설정 가능, 초과 시 Alert 표시 | + +#### 상세 페이지 +| 항목 | 내용 | +|------|------| +| 페이지 타이틀 | 게시글 상세 | +| 페이지 설명 | 게시글을 조회합니다. | +| 버튼 (본인 글만) | **삭제**, **수정** | +| 게시판명 라벨 | 게시판명 | +| 제목 | 제목 | +| 메타 정보 | 작성자 \| 날짜 \| 조회수 (예: 홍길동 \| 2025-09-03 12:23 \| 조회수 123) | +| 내용 | HTML 콘텐츠 (이미지 포함) | +| 첨부파일 | 다운로드 링크 (예: abc.pdf) | +| 댓글 등록 | Textarea + 등록 버튼 (댓글 사용함 설정 시만 표시) | + +#### 댓글 섹션 +| 항목 | 내용 | +|------|------| +| 댓글 수 | 댓글 N | +| 댓글 정보 | 프로필 이미지, 부서명 이름 직책, 등록일시, 댓글 내용 | +| 수정 버튼 (본인만) | 클릭 시 인풋박스에 기존 댓글 내용 입력 상태로 변경 | +| 삭제 버튼 (본인만) | 클릭 시 **"정말 삭제하시겠습니까?"** 확인 Alert 표시 | + +--- + +## 2. WYSIWYG 에디터 추천 + +### 2.1 옵션 비교 + +| 라이브러리 | 장점 | 단점 | 추천도 | +|------------|------|------|--------| +| **TipTap** | 최신, Headless (커스텀 자유), React 네이티브, shadcn/ui 호환 | 학습 곡선 | ⭐⭐⭐⭐⭐ | +| **CKEditor 5** | 기능 풍부, 엔터프라이즈급, 이미지 업로드 내장 | 무거움, 스타일 충돌, 라이선스 | ⭐⭐⭐⭐ | +| **Quill** | 간단, 가벼움 | 구식 스타일, 유지보수 부족 | ⭐⭐⭐ | +| **Editor.js** | 블록 기반, 노션 스타일 | JSON 출력 (HTML 아님), 변환 필요 | ⭐⭐⭐ | +| **React-Quill** | Quill + React 래퍼 | Next.js SSR 이슈 | ⭐⭐ | + +### 2.2 최종 추천: **TipTap** + +**이유**: +1. **Headless 아키텍처**: shadcn/ui와 Tailwind CSS 완벽 호환 +2. **모던 React**: useState/useEffect 패턴, TypeScript 완벽 지원 +3. **확장성**: 필요한 기능만 설치 (경량화) +4. **커뮤니티**: 활발한 개발, 문서화 우수 +5. **이미지 업로드**: 커스텀 핸들러로 S3/백엔드 연동 용이 + +### 2.3 필요 패키지 (TipTap) + +```bash +npm install @tiptap/react @tiptap/pm @tiptap/starter-kit \ + @tiptap/extension-image @tiptap/extension-link \ + @tiptap/extension-underline @tiptap/extension-text-align \ + @tiptap/extension-placeholder +``` + +--- + +## 3. 파일 구조 + +``` +src/ +├── app/[locale]/(protected)/board/ +│ ├── page.tsx # 게시판 리스트 페이지 +│ ├── create/ +│ │ └── page.tsx # 게시글 등록 페이지 +│ └── [id]/ +│ ├── page.tsx # 게시글 상세 페이지 +│ └── edit/ +│ └── page.tsx # 게시글 수정 페이지 +│ +├── components/board/ +│ ├── BoardList/ +│ │ ├── index.tsx # 리스트 메인 컴포넌트 +│ │ └── types.ts # 타입 정의 +│ ├── BoardForm/ +│ │ ├── index.tsx # 등록/수정 폼 +│ │ └── types.ts +│ ├── BoardDetail/ +│ │ ├── index.tsx # 상세 보기 +│ │ └── types.ts +│ ├── CommentSection/ +│ │ ├── index.tsx # 댓글 섹션 +│ │ ├── CommentItem.tsx # 개별 댓글 컴포넌트 +│ │ └── types.ts +│ └── RichTextEditor/ +│ ├── index.tsx # TipTap 에디터 래퍼 +│ ├── MenuBar.tsx # 에디터 툴바 +│ └── extensions.ts # TipTap 확장 설정 +│ +└── hooks/ + └── useBoardList.ts # 게시판 목록 API 훅 +``` + +--- + +## 4. 구현 체크리스트 + +### Phase 1: 기반 작업 (에디터 + 타입) + +- [ ] **1.1** TipTap 패키지 설치 +- [ ] **1.2** `RichTextEditor` 컴포넌트 구현 + - [ ] 1.2.1 기본 에디터 (Bold, Italic, Underline, Strike) + - [ ] 1.2.2 텍스트 정렬 (좌/중/우) + - [ ] 1.2.3 목록 (Bullet, Ordered) + - [ ] 1.2.4 링크 삽입 + - [ ] 1.2.5 이미지 업로드 (파일 선택 → 백엔드 업로드 → URL 삽입) +- [ ] **1.3** `types.ts` 정의 + - [ ] 1.3.1 Board (게시판 타입) + - [ ] 1.3.2 Post (게시글 타입) + - [ ] 1.3.3 Comment (댓글 타입) + +### Phase 2: 리스트 페이지 + +- [ ] **2.1** `BoardList/index.tsx` 구현 + - [ ] 2.1.1 IntegratedListTemplateV2 적용 + - [ ] 2.1.2 DateRangeSelector (날짜 범위) + - [ ] 2.1.3 탭 네비게이션 (전체보드, 전자결재, 인쇄, 미역, 우울) + - [ ] 2.1.4 게시판 필터 (공지사항 드롭다운) + - [ ] 2.1.5 검색 입력창 + - [ ] 2.1.6 정렬 드롭다운 (최신순) + - [ ] 2.1.7 총 N건 표시 +- [ ] **2.2** 테이블 구현 + - [ ] 2.2.1 컬럼: No., 제목, 작성자, 등록일, 조회수 + - [ ] 2.2.2 체크박스 선택 + - [ ] 2.2.3 행 클릭 → 상세 이동 +- [ ] **2.3** 모바일 카드 뷰 구현 +- [ ] **2.4** 페이지네이션 구현 +- [ ] **2.5** `page.tsx` 라우트 생성 + +### Phase 3: 등록/수정 페이지 + +- [ ] **3.1** `BoardForm/index.tsx` 구현 + - [ ] 3.1.1 게시판 Select (게시판 목록) + - [ ] 3.1.2 상단 노출 Radio (사용안함/사용함) + - [ ] 3.1.2.1 최대 5개 제한 Alert + - [ ] 3.1.3 제목 Input + - [ ] 3.1.4 내용 RichTextEditor + - [ ] 3.1.5 첨부파일 업로드 (다중 파일) + - [ ] 3.1.6 작성자 표시 (읽기 전용) + - [ ] 3.1.7 댓글 Radio (사용안함/사용함) + - [ ] 3.1.8 등록일시 표시 (읽기 전용) + - [ ] 3.1.9 등록 버튼 +- [ ] **3.2** 수정 모드 구현 (기존 데이터 로드) +- [ ] **3.3** 유효성 검사 + - [ ] 3.3.1 필수 필드: 게시판, 제목, 내용 +- [ ] **3.4** `create/page.tsx` 라우트 생성 +- [ ] **3.5** `[id]/edit/page.tsx` 라우트 생성 + +### Phase 4: 상세 페이지 + +- [ ] **4.1** `BoardDetail/index.tsx` 구현 + - [ ] 4.1.1 게시판명 라벨 + - [ ] 4.1.2 제목 + - [ ] 4.1.3 메타 정보 (작성자 | 날짜 | 조회수) + - [ ] 4.1.4 내용 (HTML 렌더링) + - [ ] 4.1.5 첨부파일 다운로드 링크 +- [ ] **4.2** 삭제/수정 버튼 (본인 글만 표시) + - [ ] 4.2.1 삭제 버튼 → **AlertDialog** ("정말 삭제하시겠습니까?") + - [ ] 4.2.2 수정 버튼 → 수정 페이지 이동 +- [ ] **4.3** `[id]/page.tsx` 라우트 생성 + +### Phase 5: 댓글 기능 + +- [ ] **5.1** `CommentSection/index.tsx` 구현 + - [ ] 5.1.1 댓글 등록 Textarea + 버튼 + - [ ] 5.1.2 댓글 수 표시 ("댓글 N") +- [ ] **5.2** `CommentItem.tsx` 구현 + - [ ] 5.2.1 프로필 이미지 + - [ ] 5.2.2 부서명 이름 직책 + - [ ] 5.2.3 등록일시 + - [ ] 5.2.4 댓글 내용 + - [ ] 5.2.5 수정 버튼 (본인만) → 인라인 수정 모드 + - [ ] 5.2.6 삭제 버튼 (본인만) → **AlertDialog** +- [ ] **5.3** 댓글 CRUD API 연동 + +### Phase 6: API 연동 + 마무리 + +- [ ] **6.1** Mock 데이터 → 실제 API 연동 +- [ ] **6.2** 에러 핸들링 (toast 알림) +- [ ] **6.3** 로딩 상태 UI +- [ ] **6.4** 반응형 테스트 (모바일/태블릿/데스크톱) +- [ ] **6.5** 접근 권한 테스트 (본인 글/타인 글) + +### Phase 7: 문서화 + +- [ ] **7.1** 테스트 URL 문서 업데이트 (`[REF] all-pages-test-urls.md`) + - [ ] 7.1.1 게시판 섹션 신규 추가 (기존 구역과 별도) + - [ ] 7.1.2 메인 리스트 URL만 등록 +- [ ] **7.2** 이 문서 완료 처리 + +--- + +## 5. 주의사항 (버디가 자주 틀리는 것들) + +### 5.1 디스크립션 확인 필수 +- [ ] 리스트: "게시판의 게시글을 등록하고 관리합니다." +- [ ] 등록: "게시글을 등록하고 관리합니다." +- [ ] 상세: "게시글을 조회합니다." + +### 5.2 테이블 컬럼 타이틀 정확히 +- [ ] No. (번호 아님) +- [ ] 제목 +- [ ] 작성자 +- [ ] 등록일 +- [ ] 조회수 + +### 5.3 카드/라벨 텍스트 정확히 +- [ ] "게시판" (게시판명 아님, Select label) +- [ ] "상단 노출" (상단고정 아님) +- [ ] "댓글" (댓글허용 아님) +- [ ] "댓글 N" (댓글 수 표시) + +### 5.4 팝업 메시지 정확히 +- [ ] 삭제 확인: **"정말 삭제하시겠습니까?"** +- [ ] 상단 노출 초과: **"상단 노출은 5개까지 설정 가능합니다."** + +### 5.5 본인 글/댓글 체크 +- [ ] 게시글 삭제/수정 버튼 → 본인 글만 +- [ ] 댓글 수정/삭제 버튼 → 본인 댓글만 + +--- + +## 6. 기술 스택 + +| 항목 | 기술 | +|------|------| +| 프레임워크 | Next.js 14 App Router | +| UI 컴포넌트 | shadcn/ui | +| 스타일링 | Tailwind CSS | +| 에디터 | **TipTap** | +| 폼 | React Hook Form (권장) | +| 상태 관리 | React useState/useCallback | +| API | Next.js API Routes (Proxy) | +| 팝업 | AlertDialog, Dialog (Radix UI) | + +--- + +## 7. 작업 완료 후 필수 조치 + +### 7.1 테스트 URL 문서 업데이트 + +`claudedocs/[REF] all-pages-test-urls.md` 파일에 다음 내용 추가: + +```markdown +## 게시판 (Board) - 🆕 NEW SECTION + +| 페이지 | URL | 비고 | +|--------|-----|------| +| 게시판 목록 | `/ko/board` | 🆕 NEW | +``` + +> ⚠️ **참고**: 상세/수정/등록 페이지는 메인 리스트에서 접근 가능하므로 별도 등록하지 않음 + +### 7.2 _index.md 업데이트 + +`claudedocs/_index.md`에 board/ 폴더 섹션 추가 + +--- + +## 8. 승인 대기 + +- [ ] 사용자 확인 완료 +- [ ] 작업 시작 + +--- + +*작성: Claude Code* \ No newline at end of file diff --git a/claudedocs/customer-center/[IMPL-2025-12-19] inquiry-management.md b/claudedocs/customer-center/[IMPL-2025-12-19] inquiry-management.md new file mode 100644 index 00000000..be57588e --- /dev/null +++ b/claudedocs/customer-center/[IMPL-2025-12-19] inquiry-management.md @@ -0,0 +1,89 @@ +# [IMPL-2025-12-19] 1:1 문의 관리 구현 + +## 개요 +- **페이지**: 1:1 문의 (고객센터) +- **URL**: `/ko/customer-center/inquiries` +- **참조**: 공지사항, 이벤트, 게시판 구조 + +## 체크리스트 + +### Phase 1: 기본 구조 +- [ ] types.ts 생성 (Inquiry 타입, 필터 옵션 등) +- [ ] Mock 데이터 생성 + +### Phase 2: 목록 페이지 +- [ ] InquiryList.tsx 생성 + - [ ] IntegratedListTemplateV2 사용 + - [ ] 날짜 범위 선택 (DateRangeSelector) + - [ ] 문의 등록 버튼 + - [ ] 검색창 + - [ ] 테이블 필터 3개 (상담분류, 상태, 정렬) + - [ ] 테이블 컬럼: No., 상담분류, 제목, 상태, 등록일 +- [ ] page.tsx (목록) + +### Phase 3: 상세 페이지 +- [ ] InquiryDetail.tsx 생성 + - [ ] 문의 영역 (제목, 작성자, 날짜, 내용, 첨부파일) + - [ ] 답변 영역 (작성자, 날짜, 내용, 첨부파일) + - [ ] 댓글 등록 입력창 + - [ ] 댓글 목록 (프로필, 이름, 내용, 날짜, 수정/삭제) + - [ ] 삭제/수정 버튼 +- [ ] [id]/page.tsx (상세) + +### Phase 4: 등록/수정 페이지 +- [ ] InquiryForm.tsx 생성 + - [ ] 상담분류 선택 + - [ ] 제목 입력 + - [ ] 내용 에디터 (게시판 에디터 사용) + - [ ] 파일 첨부 +- [ ] create/page.tsx (등록) +- [ ] [id]/edit/page.tsx (수정) + +### Phase 5: 마무리 +- [ ] index.tsx export +- [ ] 테스트 URL 문서 업데이트 + +## 스펙 상세 + +### 목록 페이지 +| 필드 | 타입 | 설명 | +|------|------|------| +| 상담분류 필터 | Select | 전체, 문의하기, 신고하기, 건의사항, 서비스오류 | +| 상태 필터 | Select | 전체, 답변대기, 답변완료 | +| 정렬 | Select | 최신순, 오래된순 | + +### 테이블 컬럼 +| 컬럼 | 설명 | +|------|------| +| No. | 번호 | +| 상담분류 | 문의하기, 신고하기, 건의사항, 서비스오류 | +| 제목 | 문의 제목 | +| 상태 | 답변대기, 답변완료 | +| 등록일 | YYYY-MM-DD | + +### 상세 페이지 구조 +1. **문의 영역** + - 제목 + - 작성자 | 등록일시 + - 내용 (에디터 콘텐츠) + - 첨부파일 + +2. **답변 영역** + - 작성자 | 답변일시 + - 내용 + - 첨부파일 + +3. **댓글 영역** + - 댓글 등록 입력창 + 등록 버튼 + - 댓글 목록 + - 프로필 이미지 + - 이름 + - 댓글 내용 + - 등록일시 + - 수정/삭제 버튼 + +## 테스트 URL +- 목록: `/ko/customer-center/inquiries` +- 상세: `/ko/customer-center/inquiries/[id]` +- 등록: `/ko/customer-center/inquiries/create` +- 수정: `/ko/customer-center/inquiries/[id]/edit` \ No newline at end of file diff --git a/claudedocs/guides/[GUIDE-2025-12-16] options-vs-flattened-data.md b/claudedocs/guides/[GUIDE-2025-12-16] options-vs-flattened-data.md new file mode 100644 index 00000000..e4f9daf6 --- /dev/null +++ b/claudedocs/guides/[GUIDE-2025-12-16] options-vs-flattened-data.md @@ -0,0 +1,222 @@ +# [GUIDE-2025-12-16] options vs 평탄화 데이터 패턴 + +## 개요 + +품목관리 시스템에서 백엔드 API 응답의 `options` 배열과 평탄화된 필드 데이터를 처리하는 패턴에 대한 가이드. + +**핵심 원칙**: `options` 배열을 직접 파싱하지 말고, 백엔드가 정제해서 주는 평탄화된 필드만 사용한다. + +## 배경: 백엔드 데이터 저장 구조 + +### 두 가지 저장 방식 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 백엔드 저장 구조 │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ │ +│ │ 동적 필드 │ ────────▶ options (JSON 컬럼) │ +│ │ (품목기준관리에서 │ [{label, value}, ...] │ +│ │ 동적으로 생성) │ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ 고정 필드 │ ────────▶ item_details 테이블 컬럼 │ +│ │ (하드코딩된 │ bending_details, │ +│ │ 시스템 필드) │ specification_file 등 │ +│ └─────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### options 배열 구조 + +```json +{ + "options": [ + { "label": "custom_field_1", "value": "사용자 입력값1" }, + { "label": "custom_field_2", "value": "사용자 입력값2" } + ] +} +``` + +### 평탄화된 응답 구조 + +백엔드가 조회 시 정제해서 내려주는 형태: + +```json +{ + "id": 123, + "code": "FG-001", + "name": "제품명", + "unit": "EA", + "specification": "100x200", + "details": { + "bending_details": "1110", + "specification_file": "https://..." + }, + "options": [...] // ← 이건 무시해야 함! +} +``` + +## 문제: options 직접 파싱의 위험성 + +### 발생 원인 + +백엔드 `ItemService.php`의 `getKnownFields()` 함수가 `item_details` 테이블 컬럼을 인식하지 못해서, 고정 필드도 `options`에 중복 저장되는 버그가 있었음. + +### 데이터 흐름 (버그 상황) + +``` +[1차 저장] bending_details = 111 +┌─────────────────────────────────────────────────────┐ +│ item_details.bending_details = 111 ✅ 정상 │ +│ options = [{label: "bending_details", value: "111"}] ❌ 중복! │ +└─────────────────────────────────────────────────────┘ + +[수정] bending_details = 1110 +┌─────────────────────────────────────────────────────┐ +│ item_details.bending_details = 1110 ✅ 최신값 │ +│ options = [{label: "bending_details", value: "111"}] ⚠️ 이전값! │ +└─────────────────────────────────────────────────────┘ + +[조회 시 프론트엔드] +1. data.bending_details = 1110 가져옴 ✅ +2. options 순회하며 덮어쓰기... +3. formData.bending_details = "111" ❌ 이전값으로 덮어씀! +``` + +### 증상 + +- 품목 수정 후 다시 조회하면 이전 값이 표시됨 +- 입력한 최신값이 저장은 되지만 화면에 반영 안됨 +- 특히 `bending_details`, `specification_file`, `certification_file` 등에서 발생 + +## 해결: 평탄화 데이터만 사용 + +### 올바른 패턴 (현재 코드) + +**파일**: `src/app/[locale]/(protected)/items/[id]/edit/page.tsx` + +```typescript +function mapApiResponseToFormData(data: ItemApiResponse): DynamicFormData { + const formData: DynamicFormData = {}; + + // 1. 백엔드 응답의 최상위 필드를 그대로 복사 + Object.entries(data).forEach(([key, value]) => { + if (!excludeKeys.includes(key) && value !== null) { + formData[key] = value; + } + }); + + // 2. details 객체 펼치기 (item_details 테이블 필드) + const details = data.details; + if (details && typeof details === 'object') { + Object.entries(details).forEach(([key, value]) => { + if (value !== null && value !== undefined) { + formData[key] = value; // 백엔드가 정제한 최신값 + } + }); + } + + // 3. attributes 객체 펼치기 (동적 필드) + const attributes = data.attributes || {}; + Object.entries(attributes).forEach(([key, value]) => { + if (!(key in formData)) { // 기존 값 덮어쓰지 않음 + formData[key] = value; + } + }); + + // ❌ options 파싱 로직 제거! + // options는 백엔드 내부 매핑용이므로 프론트엔드에서 사용하지 않음 + + return formData; +} +``` + +### 잘못된 패턴 (이전 코드) + +```typescript +// ❌ 이렇게 하면 안됨! +if (data.options && Array.isArray(data.options)) { + data.options.forEach((opt) => { + if (opt.label && opt.value) { + formData[opt.label] = opt.value; // stale 데이터로 덮어쓸 수 있음! + } + }); +} +``` + +## 데이터 소스 우선순위 + +프론트엔드에서 폼 데이터 매핑 시 사용해야 할 데이터 소스 우선순위: + +| 우선순위 | 데이터 소스 | 설명 | +|---------|------------|------| +| 1 | `data.details.*` | item_details 테이블의 고정 필드 (최신값 보장) | +| 2 | `data.*` (최상위) | items/products 테이블 필드 | +| 3 | `data.attributes.*` | 동적 필드 (품목기준관리에서 생성) | +| ❌ | `data.options[]` | 사용하지 않음 (내부 매핑용) | + +## 영향받는 필드들 + +`options`에 잘못 저장될 수 있는 `item_details` 고정 필드들: + +| 필드명 | 설명 | 저장 위치 | +|--------|------|----------| +| `bending_details` | 전개도 상세 정보 | item_details | +| `bending_diagram` | 전개도 이미지 URL | item_details | +| `specification_file` | 시방서 파일 URL | item_details | +| `certification_file` | 인정서 파일 URL | item_details | +| `files` | 첨부파일 목록 | item_details | + +## DropdownField의 options 정규화 + +드롭다운 필드에서 사용하는 `options`는 **필드 정의의 선택지 목록**으로, 위에서 말하는 **품목 데이터의 options**와 다름. + +**파일**: `src/components/items/DynamicItemForm/fields/DropdownField.tsx` + +```typescript +// 필드 정의의 options (선택지 목록)를 정규화 +function normalizeOptions(rawOptions: unknown): Array<{ label: string; value: string }> { + // 문자열: "옵션1, 옵션2" → [{label, value}, ...] + // 배열: ["옵션1", "옵션2"] → [{label, value}, ...] + // 객체 배열: [{label, value}, ...] → 그대로 사용 +} +``` + +이것은 필드 메타데이터의 선택지를 처리하는 것으로, 품목 데이터의 `options` 배열과 무관함. + +## 관련 파일 + +| 파일 | 역할 | +|------|------| +| `src/app/[locale]/(protected)/items/[id]/edit/page.tsx` | mapApiResponseToFormData() | +| `src/app/[locale]/(protected)/items/create/page.tsx` | 생성 페이지 (options 미사용) | +| `src/components/items/DynamicItemForm/fields/DropdownField.tsx` | 필드 정의 options 정규화 | +| `claudedocs/item-master/[FIX-2025-12-16] options-details-duplicate-bug.md` | 버그 상세 분석 | + +## 향후 계획 + +1. **백엔드 근본 수정**: `getKnownFields()`에 `item_details` 컬럼 추가 + - 이렇게 되면 고정 필드가 `options`에 중복 저장되지 않음 + +2. **고정 필드 동적화**: 전개도 상세 등을 품목기준관리에서 동적 필드로 등록 + - 이 경우 `options`에 저장되는 것이 정상 + +3. **프론트엔드 유지**: 현재 패턴 (options 무시)은 백엔드 수정 후에도 안전함 + +## 체크리스트 + +새로운 API 응답 매핑 코드 작성 시: + +- [ ] `options` 배열을 직접 파싱하지 않았는가? +- [ ] `details` 객체를 펼쳐서 최신값을 가져왔는가? +- [ ] `attributes` 객체 처리 시 기존 값을 덮어쓰지 않았는가? +- [ ] 고정 필드 (`bending_*`, `*_file`)가 올바른 소스에서 오는가? + +## 참고 + +- `[FIX-2025-12-16] options-details-duplicate-bug.md` - 버그 발생 원인 상세 분석 +- `[GUIDE] radix-ui-select-controlled-mode-bug.md` - Radix UI Select 관련 유사 이슈 \ No newline at end of file diff --git a/claudedocs/guides/[PLAN-2025-12-19] page-layout-standardization.md b/claudedocs/guides/[PLAN-2025-12-19] page-layout-standardization.md new file mode 100644 index 00000000..d7a911c7 --- /dev/null +++ b/claudedocs/guides/[PLAN-2025-12-19] page-layout-standardization.md @@ -0,0 +1,169 @@ +# 페이지 레이아웃 표준화 계획 + +## 📋 개요 + +**목표**: 품목관리(`/items`) 페이지를 기준으로 모든 페이지의 헤더/레프트 사이드바 간격을 통일 + +**기준 페이지**: `/items/page.tsx` +```tsx +
+ +
+``` + +## 🔍 현재 상황 분석 + +### 레이아웃 구조 +``` +AuthenticatedLayout +├── Header (top, mx-3 mt-3) +└── Content Area (flex, gap-3, px-3 pb-3) + ├── Sidebar (sticky, w-64) + └── Main (flex-1, overflow-auto) + └── 각 페이지 컴포넌트 +``` + +### 패딩 레이어 +1. **AuthenticatedLayout**: `
` - 패딩 없음 +2. **page.tsx wrapper**: `
` - 품목관리 패턴 +3. **PageLayout**: `p-3 md:p-6 pb-0` - 내부 패딩 + +### 문제점 +| 구분 | 품목관리 (기준) | 다른 페이지들 | +|------|-----------------|--------------| +| page.tsx | `
` | `` 또는 `
` | +| 결과 | 헤더/레프트와 적절한 간격 | 헤더/레프트에 붙어있음 | + +## ✅ 수정 대상 페이지 체크리스트 + +### 1️⃣ 리스트 페이지 (IntegratedListTemplateV2 사용) +- [ ] `/board/page.tsx` - 게시판 목록 +- [ ] `/hr/employee-management/page.tsx` - 사원관리 목록 +- [ ] `/hr/attendance-management/page.tsx` - 근태관리 목록 +- [ ] `/hr/vacation-management/page.tsx` - 휴가관리 목록 +- [ ] `/accounting/purchase/page.tsx` - 매입관리 목록 +- [ ] `/accounting/sales/page.tsx` - 매출관리 목록 +- [ ] `/accounting/vendors/page.tsx` - 거래처관리 목록 +- [ ] `/accounting/deposits/page.tsx` - 입금관리 목록 +- [ ] `/accounting/withdrawals/page.tsx` - 출금관리 목록 +- [ ] `/accounting/bills/page.tsx` - 어음관리 목록 +- [ ] `/accounting/bank-transactions/page.tsx` - 통장거래내역 +- [ ] `/accounting/vendor-ledger/page.tsx` - 거래처원장 +- [ ] `/accounting/daily-report/page.tsx` - 일일마감 +- [ ] `/accounting/expected-expenses/page.tsx` - 예상지출 +- [ ] `/accounting/bad-debt-collection/page.tsx` - 악성채권추심 +- [ ] `/accounting/receivables-status/page.tsx` - 매출채권현황 +- [ ] `/approval/draft/page.tsx` - 기안함 +- [ ] `/approval/inbox/page.tsx` - 결재함 +- [ ] `/approval/reference/page.tsx` - 참조함 + +### 2️⃣ 상세/수정/등록 페이지 (PageLayout 직접 사용) +- [ ] `/board/create/page.tsx` - 게시글 등록 +- [ ] `/board/[id]/page.tsx` - 게시글 상세 +- [ ] `/board/[id]/edit/page.tsx` - 게시글 수정 +- [ ] `/hr/employee-management/new/page.tsx` - 사원 등록 +- [ ] `/hr/employee-management/[id]/page.tsx` - 사원 상세 +- [ ] `/hr/employee-management/[id]/edit/page.tsx` - 사원 수정 +- [ ] `/accounting/sales/new/page.tsx` - 매출 등록 +- [ ] `/accounting/sales/[id]/page.tsx` - 매출 상세 +- [ ] `/accounting/vendors/new/page.tsx` - 거래처 등록 +- [ ] `/accounting/vendors/[id]/page.tsx` - 거래처 상세 +- [ ] `/accounting/purchase/[id]/page.tsx` - 매입 상세 +- [ ] `/accounting/deposits/[id]/page.tsx` - 입금 상세 +- [ ] `/accounting/withdrawals/[id]/page.tsx` - 출금 상세 +- [ ] `/accounting/bills/[id]/page.tsx` - 어음 상세 +- [ ] `/accounting/bills/new/page.tsx` - 어음 등록 +- [ ] `/accounting/vendor-ledger/[id]/page.tsx` - 거래처원장 상세 +- [ ] `/accounting/bad-debt-collection/new/page.tsx` - 악성채권 등록 +- [ ] `/accounting/bad-debt-collection/[id]/page.tsx` - 악성채권 상세 +- [ ] `/accounting/bad-debt-collection/[id]/edit/page.tsx` - 악성채권 수정 +- [ ] `/approval/draft/new/page.tsx` - 기안 작성 + +### 3️⃣ 기타 페이지 +- [ ] `/hr/attendance/page.tsx` - 모바일 근태 +- [ ] `/hr/salary-management/page.tsx` - 급여관리 +- [ ] `/settings/ranks/page.tsx` - 직급관리 +- [ ] `/settings/titles/page.tsx` - 직책관리 +- [ ] `/settings/permissions/page.tsx` - 권한관리 +- [ ] `/settings/work-schedule/page.tsx` - 근무일정 +- [ ] `/settings/leave-policy/page.tsx` - 휴가정책 +- [ ] `/dashboard/page.tsx` - 대시보드 + +## 🔧 수정 방법 + +### 표준 패턴 (품목관리 기준) + +**page.tsx에서 wrapper 추가:** +```tsx +// Before +export default function SomePage() { + return ; +} + +// After +export default function SomePage() { + return ( +
+ +
+ ); +} +``` + +## 📌 공통 레이아웃 래퍼 제안 + +향후 관리 편의를 위해 공통 래퍼 컴포넌트 생성: + +```tsx +// src/components/organisms/ContentWrapper.tsx +export function ContentWrapper({ children }: { children: React.ReactNode }) { + return
{children}
; +} +``` + +## 🎯 실행 계획 + +### Phase 1: 게시판 페이지 (대표 수정) +1. `/board/page.tsx` 수정 +2. 브라우저에서 확인 +3. 품목관리와 비교 검증 + +### Phase 2: 나머지 페이지 일괄 수정 +- 체크리스트 기반으로 순차 수정 +- 각 수정 후 체크 표시 + +### Phase 3: RULES.md 업데이트 +- 페이지 레이아웃 표준 규칙 추가 + +## 📝 RULES.md 추가 내용 (예정) + +```markdown +## Page Layout Standards +**Priority**: 🟡 **Triggers**: 새 페이지 생성, 기존 페이지 레이아웃 수정 + +### 표준 패턴 +- **page.tsx wrapper**: 모든 페이지는 `
` wrapper 필수 +- **기준**: 품목관리(`/items/page.tsx`) 페이지 + +### 예시 +\`\`\`tsx +// ✅ 올바른 패턴 +export default function SomePage() { + return ( +
+ +
+ ); +} + +// ❌ 잘못된 패턴 +export default function SomePage() { + return ; +} +\`\`\` + +### 패딩 구조 +- AuthenticatedLayout: 메인 영역 패딩 없음 +- page.tsx: `p-6` wrapper (24px) +- 컴포넌트 내부: 추가 패딩 선택적 +``` \ No newline at end of file diff --git a/claudedocs/hr/[IMPL-2025-12-16] mobile-attendance.md b/claudedocs/hr/[IMPL-2025-12-16] mobile-attendance.md new file mode 100644 index 00000000..00d4e438 --- /dev/null +++ b/claudedocs/hr/[IMPL-2025-12-16] mobile-attendance.md @@ -0,0 +1,206 @@ +# 모바일 출퇴근 시스템 구현 체크리스트 + +> Last Updated: 2025-12-18 + +## 개요 +- **목적**: 모바일 기기에서 GPS 기반 출퇴근 기록 +- **대상**: 특정 사용자 (하드코딩) +- **조건**: 지정된 현장 좌표 100m 반경 내에서만 출퇴근 가능 + +## 기술 스택 +- **지도 API**: Google Maps JavaScript API (카카오맵에서 변경) +- **API 키**: `.env.local`에 `NEXT_PUBLIC_GOOGLE_MAPS_API_KEY` 저장 +- **현장 좌표**: `37.557358, 126.864414` (본사) +- **반경**: 100m + +--- + +## Phase 1: 환경 설정 ✅ + +- [x] Google Maps API 키 .env.local에 추가 +- [x] @types/google.maps 패키지 설치 + +## Phase 2: 페이지 구조 ✅ + +- [x] `/hr/attendance` 라우트 생성 (기존 protected 레이아웃 활용) +- [x] 모바일 전용 레이아웃 (AuthenticatedLayout 모바일 모드 활용) +- [x] 페이지 컴포넌트 기본 구조 + +## Phase 3: 지도 컴포넌트 ✅ + +- [x] GoogleMap 컴포넌트 생성 (`src/components/attendance/GoogleMap.tsx`) +- [x] 현장 좌표에 100m 파란 원(Circle) 표시 +- [x] 현재 위치 마커 표시 (빨간색) +- [x] GPS watchPosition으로 실시간 위치 추적 +- [x] 개발 환경 GPS 시뮬레이션 (localhost에서 본사 근처 50m로 자동 설정) + +## Phase 4: 출퇴근 로직 ✅ + +- [x] GPS 거리 계산 함수 (Haversine formula) +- [x] 100m 반경 체크 → 버튼 활성화/비활성화 +- [x] 출근 상태 관리 (출근전/출근중/퇴근완료) +- [x] 현재 시간 실시간 표시 + +## Phase 5: 완료 화면 ✅ + +- [x] 출근 완료 화면 구현 (`src/components/attendance/AttendanceComplete.tsx`) + - [x] ✓ 체크 아이콘 + - [x] "출근 완료" 텍스트 (빨간색) + - [x] 시간 표시 (HH:MM:SS) + - [x] 날짜 표시 (YYYY년 MM월 DD일 요일) + - [x] 위치 표시 (본사) + - [x] 확인 버튼 +- [x] 퇴근 완료 화면 구현 (동일 컴포넌트 재사용) + +## Phase 6: 모바일 감지 & 리다이렉트 ⏳ + +- [ ] User-Agent 기반 모바일 감지 +- [ ] 특정 사용자 하드코딩 체크 +- [ ] 로그인 후 자동 리다이렉트 로직 +- [ ] 별도 모바일 레이아웃 (헤더/사이드바 제거) +- [ ] 웹에서 접근 차단 (모바일 전용) + +## Phase 7: API 연동 ⏳ + +- [ ] 출퇴근 기록 API 설계 +- [ ] 출근 API (`POST /api/attendance/check-in`) +- [ ] 퇴근 API (`POST /api/attendance/check-out`) +- [ ] 오늘 출퇴근 상태 조회 API (`GET /api/attendance/today`) +- [ ] 현장 좌표 API에서 가져오기 (하드코딩 제거) + +## Phase 8: 사용자 정보 연동 ⏳ + +- [ ] 로그인 사용자 정보 연동 (TEST_USER 제거) +- [ ] 출퇴근 가능 사용자 권한 체크 +- [ ] 현장별 사용자 배정 로직 + +--- + +## 생성된 파일 + +| 파일 | 설명 | +|------|------| +| `src/components/attendance/GoogleMap.tsx` | Google Maps 컴포넌트 (원, 마커, GPS 추적) | +| `src/components/attendance/AttendanceComplete.tsx` | 출퇴근 완료 화면 | +| `src/app/[locale]/(protected)/hr/attendance/page.tsx` | 출퇴근 메인 페이지 | + +--- + +## 테스트 URL + +``` +http://localhost:3000/ko/hr/attendance +``` + +모바일 테스트: Chrome DevTools → Toggle Device Toolbar (Ctrl+Shift+M) + +--- + +## 하드코딩 설정값 (추후 API로 대체 필요) + +```typescript +// 현장 좌표 (본사) - page.tsx:12-17 +const SITE_LOCATION = { + name: '본사', + lat: 37.557358, + lng: 126.864414, + radius: 100, // meters +}; + +// 테스트용 사용자 정보 - page.tsx:19-23 +const TEST_USER = { + name: '홍길동', + department: '부서명', + position: '직급명', +}; +``` + +--- + +## 개발 중 해결한 이슈 + +### 1. Hydration 에러 +- **원인**: 서버/클라이언트 HTML 불일치 (Date, localStorage 등) +- **해결**: `mounted` 상태 체크 + `suppressHydrationWarning` 속성 + +### 2. Google Maps API 중복 로드 +- **원인**: React 컴포넌트 리렌더링 시 스크립트 중복 추가 +- **해결**: `window.googleMapsLoading` 플래그 + 기존 스크립트 체크 + +### 3. GPS 권한 거부 (localhost) +- **원인**: HTTPS가 아닌 환경에서 GPS 권한 제한 +- **해결**: 개발 환경 감지 후 테스트 좌표로 시뮬레이션 +```typescript +const isDevelopment = + hostname === 'localhost' || + hostname === '127.0.0.1' || + hostname.startsWith('192.168.') || + process.env.NODE_ENV === 'development'; +``` + +--- + +## UI 스펙 (스크린샷 기반) + +### 출퇴근 메인 화면 +``` +┌─────────────────────────────┐ +│ < 🏠 ⚙️⚙️⚙️ ☰ │ ← 헤더 (AuthenticatedLayout) +├─────────────────────────────┤ +│ 출퇴근하기 │ ← 타이틀 +├─────────────────────────────┤ +│ │ +│ ┌───────────────────┐ │ +│ │ │ │ +│ │ 🔵 (100m 원) │ │ ← Google Maps +│ │ 📍 │ │ +│ │ │ │ +│ └───────────────────┘ │ +│ │ +├─────────────────────────────┤ +│ 👤 홍길동 │ +│ 부서명 직급명 │ +│ │ +│ 08:43:15 (빨간색) │ ← 실시간 시간 +├─────────────────────────────┤ +│ [ 출근하기 ] [ 퇴근하기 ]│ ← 버튼 +└─────────────────────────────┘ +``` + +### 완료 화면 +``` +┌─────────────────────────────┐ +│ < 🏠 ⚙️⚙️⚙️ ☰ │ +├─────────────────────────────┤ +│ 출근하기 │ +├─────────────────────────────┤ +│ │ +│ ✓ │ ← 체크 아이콘 (원형) +│ │ +│ 출근 완료 │ ← 빨간색 +│ 08:43:15 │ +│ │ +│ 2025년 12월 15일 (월) │ +│ │ +│ 📍 본사 │ +│ │ +├─────────────────────────────┤ +│ [ 확인 ] │ +└─────────────────────────────┘ +``` + +--- + +## 다음 작업 TODO + +1. **Phase 6 진행**: 모바일 전용 레이아웃 + User-Agent 감지 +2. **API 설계**: 백엔드 팀과 출퇴근 API 협의 +3. **테스트**: 실제 모바일 기기에서 GPS 테스트 (HTTPS 환경) + +--- + +## 참고 사항 + +- MVP 버전: API 연동 없이 하드코딩으로 동작 확인 (현재 상태) +- 추후 개선: 출퇴근 기록 API, 현장 좌표 DB 저장, 사용자 권한 체크 +- Phase 6 (모바일 감지 & 리다이렉트)는 MVP 테스트 후 진행 \ No newline at end of file diff --git a/claudedocs/hr/[IMPL-2025-12-19] card-management.md b/claudedocs/hr/[IMPL-2025-12-19] card-management.md new file mode 100644 index 00000000..42a995af --- /dev/null +++ b/claudedocs/hr/[IMPL-2025-12-19] card-management.md @@ -0,0 +1,86 @@ +# [IMPL-2025-12-19] 카드관리 기능 구현 + +## 개요 +- 위치: 기준정보 > 카드관리 +- 경로: `/hr/card-management` + +## 구현 체크리스트 + +### 1. Types 정의 +- [x] `types.ts` - Card 타입, 상태, 카드사 옵션 정의 + +### 2. 컴포넌트 구현 +- [x] `src/components/hr/CardManagement/index.tsx` - 리스트 컴포넌트 (IntegratedListTemplateV2 사용) +- [x] `src/components/hr/CardManagement/CardDetail.tsx` - 상세 컴포넌트 +- [x] `src/components/hr/CardManagement/CardForm.tsx` - 등록/수정 폼 컴포넌트 +- [x] `src/components/hr/CardManagement/types.ts` - 타입 정의 + +### 3. 페이지 라우팅 +- [x] `src/app/[locale]/(protected)/hr/card-management/page.tsx` - 리스트 페이지 +- [x] `src/app/[locale]/(protected)/hr/card-management/[id]/page.tsx` - 상세 페이지 +- [x] `src/app/[locale]/(protected)/hr/card-management/[id]/edit/page.tsx` - 수정 페이지 +- [x] `src/app/[locale]/(protected)/hr/card-management/new/page.tsx` - 등록 페이지 + +### 4. 문서 업데이트 +- [x] `claudedocs/[REF] all-pages-test-urls.md` 업데이트 + +## 스크린샷 기반 필드 정의 + +### 리스트 테이블 컬럼 +| 컬럼 | 설명 | +|------|------| +| No. | 번호 | +| 카드사 | 카드사 이름 | +| 카드번호 | 1234-****-****-1234 형식 | +| 카드명 | 카드 명칭 | +| 상태 | 사용/정지 | +| 부서 | 사용자 부서 | +| 사용자 | 사용자 이름 | +| 직책 | 사용자 직책 | +| 작업 | 선택 시 수정/삭제 버튼 | + +### 상세 페이지 필드 + +#### 기본 정보 +| 필드 | 타입 | 설명 | +|------|------|------| +| 카드사 | Select | 카드사 선택 | +| 카드번호 | Input | 1234-1234-1234-1234 | +| 유효기간 | Input | MMYY 형식 | +| 카드 비밀번호 앞 2자리 | Input | ** | +| 카드명 | Input | 카드 명칭 | +| 상태 | Select | 사용/정지 | + +#### 사용자 정보 +| 필드 | 타입 | 설명 | +|------|------|------| +| 부서/이름/직책 | Select | 사용자 선택 셀렉트박스 | + +## 진행 상황 +- 시작일: 2025-12-19 +- 완료일: 2025-12-19 +- 현재 상태: 완료 + +## 생성된 파일 목록 +``` +src/components/hr/CardManagement/ +├── types.ts # 카드 타입 정의 +├── index.tsx # 리스트 컴포넌트 +├── CardDetail.tsx # 상세 컴포넌트 +└── CardForm.tsx # 등록/수정 폼 컴포넌트 + +src/app/[locale]/(protected)/hr/card-management/ +├── page.tsx # 리스트 페이지 +├── new/ +│ └── page.tsx # 등록 페이지 +└── [id]/ + ├── page.tsx # 상세 페이지 + └── edit/ + └── page.tsx # 수정 페이지 +``` + +## 테스트 URL +- 리스트: http://localhost:3000/ko/hr/card-management +- 등록: http://localhost:3000/ko/hr/card-management/new +- 상세: http://localhost:3000/ko/hr/card-management/1 +- 수정: http://localhost:3000/ko/hr/card-management/1/edit diff --git a/claudedocs/item-master/[PLAN-2025-12-16] dynamicitemform-hook-extraction.md b/claudedocs/item-master/[PLAN-2025-12-16] dynamicitemform-hook-extraction.md index 5b21a94c..3c8f4b36 100644 --- a/claudedocs/item-master/[PLAN-2025-12-16] dynamicitemform-hook-extraction.md +++ b/claudedocs/item-master/[PLAN-2025-12-16] dynamicitemform-hook-extraction.md @@ -14,168 +14,184 @@ ## Phase 1: 컴포넌트 분리 (~386줄 감소) -### 1.1 FormHeader 컴포넌트 분리 -- [ ] `components/FormHeader.tsx` 파일 생성 -- [ ] FormHeader 함수 이동 (56-107줄, ~51줄) -- [ ] Props 타입 정의 -- [ ] index.tsx에서 import 및 사용 -- [ ] 빌드 확인 +### 1.1 FormHeader 컴포넌트 분리 ✅ +- [x] `components/FormHeader.tsx` 파일 생성 +- [x] FormHeader 함수 이동 (56-107줄, ~51줄) +- [x] Props 타입 정의 +- [x] index.tsx에서 import 및 사용 +- [x] 빌드 확인 (Phase 1 완료 후 일괄) -### 1.2 ValidationAlert 컴포넌트 분리 -- [ ] `components/ValidationAlert.tsx` 파일 생성 -- [ ] ValidationAlert 함수 이동 (112-141줄, ~30줄) -- [ ] Props 타입 정의 -- [ ] index.tsx에서 import 및 사용 -- [ ] 빌드 확인 +### 1.2 ValidationAlert 컴포넌트 분리 ✅ +- [x] `components/ValidationAlert.tsx` 파일 생성 +- [x] ValidationAlert 함수 이동 (112-141줄, ~30줄) +- [x] Props 타입 정의 +- [x] index.tsx에서 import 및 사용 +- [x] 빌드 확인 (Phase 1 완료 후 일괄) -### 1.3 DynamicSectionRenderer 삭제 -- [ ] 현재 사용 여부 최종 확인 -- [ ] 미사용 확인 시 코드 삭제 (146-227줄, ~82줄) -- [ ] 빌드 확인 +### 1.3 DynamicSectionRenderer 삭제 ✅ +- [x] 현재 사용 여부 최종 확인 → 미사용 확인됨 +- [x] 미사용 확인 시 코드 삭제 (~82줄) +- [x] types.ts의 DynamicSectionRendererProps도 삭제 +- [x] 빌드 확인 (Phase 1 완료 후 일괄) -### 1.4 FileUploadFields 컴포넌트 분리 -- [ ] `components/FileUploadFields.tsx` 파일 생성 -- [ ] 시방서/인정서 업로드 JSX 이동 (1771-1963줄, ~193줄) -- [ ] Props 타입 정의 (파일 상태, 핸들러 등) -- [ ] index.tsx에서 import 및 사용 -- [ ] 빌드 확인 +### 1.4 FileUploadFields 컴포넌트 분리 ✅ +- [x] `components/FileUploadFields.tsx` 파일 생성 (~220줄) +- [x] 시방서/인정서 업로드 JSX 이동 (~173줄 감소) +- [x] Props 타입 정의 (FileUploadFieldsProps) +- [x] index.tsx에서 import 및 사용 +- [x] lucide-react 불필요 import 정리 +- [x] 빌드 확인 (Phase 1 완료 후 일괄) -### 1.5 DuplicateCodeDialog 컴포넌트 분리 -- [ ] `components/DuplicateCodeDialog.tsx` 파일 생성 -- [ ] AlertDialog JSX 이동 (2137-2158줄, ~30줄) -- [ ] Props 타입 정의 -- [ ] index.tsx에서 import 및 사용 -- [ ] 빌드 확인 +### 1.5 DuplicateCodeDialog 컴포넌트 분리 ✅ +- [x] `components/DuplicateCodeDialog.tsx` 파일 생성 (~53줄) +- [x] AlertDialog JSX 이동 +- [x] Props 타입 정의 (DuplicateCodeDialogProps) +- [x] index.tsx에서 import 및 사용 +- [x] 빌드 확인 (Phase 1 완료 후 일괄) **Phase 1 완료 후 예상:** ~1,775줄 +**Phase 1 실제 결과:** 1,842줄 (2,161줄 → 1,842줄, 319줄 감소) ✅ --- -## Phase 2: 품목코드 생성 훅 분리 (~300줄 감소) +## Phase 2: 품목코드 생성 훅 분리 (~300줄 감소) ✅ -### 2.1 useItemCodeGeneration 훅 생성 -- [ ] `hooks/useItemCodeGeneration.ts` 파일 생성 -- [ ] 타입 정의 (입력/출력) +### 2.1 useItemCodeGeneration 훅 생성 ✅ +- [x] `hooks/useItemCodeGeneration.ts` 파일 생성 (~420줄) +- [x] 타입 정의 (입력/출력) - UseItemCodeGenerationParams, ItemCodeGenerationResult +- [x] BendingFieldKeys, AssemblyFieldKeys, PurchasedFieldKeys, CategoryKeyWithId 타입 export -### 2.2 품목코드 관련 useMemo 이동 -- [ ] `hasAutoItemCode`, `itemNameKey`, `allSpecificationKeys`, `statusFieldKey` useMemo 이동 (622-674줄) -- [ ] `activeSpecificationKey` useMemo 이동 (678-708줄) -- [ ] `autoGeneratedItemCode` useMemo 이동 (1234-1254줄) -- [ ] 빌드 확인 +### 2.2 품목코드 관련 useMemo 이동 ✅ +- [x] `hasAutoItemCode`, `itemNameKey`, `allSpecificationKeys`, `statusFieldKey` useMemo 이동 +- [x] `activeSpecificationKey` useMemo 이동 +- [x] `autoGeneratedItemCode` useMemo 이동 +- [x] 빌드 확인 ✅ -### 2.3 절곡부품 품목코드 로직 이동 -- [ ] `bendingFieldKeys`, `autoBendingItemCode`, `allCategoryKeysWithIds` useMemo 이동 (837-967줄) -- [ ] 빌드 확인 +### 2.3 절곡부품 품목코드 로직 이동 ✅ +- [x] `bendingFieldKeys`, `autoBendingItemCode`, `allCategoryKeysWithIds` useMemo 이동 +- [x] `generateBendingItemCodeSimple` 함수 훅 내부로 이동 +- [x] 빌드 확인 ✅ -### 2.4 조립부품 품목코드 로직 이동 -- [ ] `hasAssemblyFields`, `assemblyFieldKeys`, `autoAssemblyItemName`, `autoAssemblySpec` useMemo 이동 (1051-1136줄) -- [ ] 빌드 확인 +### 2.4 조립부품 품목코드 로직 이동 ✅ +- [x] `hasAssemblyFields`, `assemblyFieldKeys`, `autoAssemblyItemName`, `autoAssemblySpec` useMemo 이동 +- [x] `generateAssemblyItemNameSimple`, `generateAssemblySpecification` 함수 훅 내부로 이동 +- [x] 빌드 확인 ✅ -### 2.5 구매부품 품목코드 로직 이동 -- [ ] `purchasedFieldKeys`, `autoPurchasedItemCode` useMemo 이동 (1140-1227줄) -- [ ] 빌드 확인 +### 2.5 구매부품 품목코드 로직 이동 ✅ +- [x] `purchasedFieldKeys`, `autoPurchasedItemCode` useMemo 이동 +- [x] 빌드 확인 ✅ -### 2.6 index.tsx 연결 -- [ ] useItemCodeGeneration 훅 import -- [ ] 기존 useMemo 코드 제거 -- [ ] 훅 반환값으로 대체 -- [ ] 빌드 확인 -- [ ] 기능 테스트 (품목코드 자동생성 동작 확인) +### 2.6 index.tsx 연결 ✅ +- [x] useItemCodeGeneration 훅 import +- [x] 기존 useMemo 코드 제거 (4개 블록, ~305줄) +- [x] 훅 반환값으로 대체 +- [x] 빌드 확인 ✅ +- [ ] 기능 테스트 (품목코드 자동생성 동작 확인) - 수동 테스트 필요 **Phase 2 완료 후 예상:** ~1,475줄 +**Phase 2 실제 결과:** 1,432줄 (1,842줄 → 1,432줄, 410줄 감소) ✅ --- -## Phase 3: 필드 탐지 훅 분리 (~200줄 감소) +## Phase 3: 필드 탐지 훅 분리 (~200줄 감소) ✅ -### 3.1 useFieldDetection 훅 생성 -- [ ] `hooks/useFieldDetection.ts` 파일 생성 -- [ ] 타입 정의 +### 3.1 useFieldDetection 훅 생성 ✅ +- [x] `hooks/useFieldDetection.ts` 파일 생성 (~174줄) +- [x] 타입 정의 (PartTypeDetectionResult, UseFieldDetectionParams, FieldDetectionResult) -### 3.2 부품 유형 필드 탐지 로직 이동 -- [ ] `partTypeFieldKey`, `selectedPartType`, `isBendingPart`, `isAssemblyPart`, `isPurchasedPart` useMemo 이동 (711-759줄) -- [ ] 빌드 확인 +### 3.2 부품 유형 필드 탐지 로직 이동 ✅ +- [x] `partTypeFieldKey`, `selectedPartType`, `isBendingPart`, `isAssemblyPart`, `isPurchasedPart` useMemo 이동 +- [x] 빌드 확인 ✅ -### 3.3 BOM 체크박스 필드 탐지 로직 이동 -- [ ] `bomRequiredFieldKey` useMemo 이동 (998-1047줄) -- [ ] 빌드 확인 +### 3.3 BOM 체크박스 필드 탐지 로직 이동 ✅ +- [x] `bomRequiredFieldKey` useMemo 이동 +- [x] 빌드 확인 ✅ -### 3.4 index.tsx 연결 -- [ ] useFieldDetection 훅 import -- [ ] 기존 useMemo 코드 제거 -- [ ] 훅 반환값으로 대체 -- [ ] 빌드 확인 -- [ ] 기능 테스트 (조건부 필드 표시 확인) +### 3.4 index.tsx 연결 ✅ +- [x] useFieldDetection 훅 import +- [x] 기존 useMemo 코드 제거 (2개 블록, ~88줄) +- [x] 훅 반환값으로 대체 +- [x] 빌드 확인 ✅ +- [ ] 기능 테스트 (조건부 필드 표시 확인) - 수동 테스트 필요 **Phase 3 완료 후 예상:** ~1,275줄 +**Phase 3 실제 결과:** 1,344줄 (1,432줄 → 1,344줄, 88줄 감소) ✅ --- -## Phase 4: 부품 유형 처리 훅 분리 (~150줄 감소) +## Phase 4: 부품 유형 처리 훅 분리 (~150줄 감소) ✅ -### 4.1 usePartTypeHandling 훅 생성 -- [ ] `hooks/usePartTypeHandling.ts` 파일 생성 -- [ ] 타입 정의 +### 4.1 usePartTypeHandling 훅 생성 ✅ +- [x] `hooks/usePartTypeHandling.ts` 파일 생성 (~192줄) +- [x] 타입 정의 (UsePartTypeHandlingParams) -### 4.2 부품 유형 변경 useEffect 이동 -- [ ] `prevPartTypeRef` 및 부품 유형 변경 감지 useEffect 이동 (762-833줄) -- [ ] 빌드 확인 +### 4.2 부품 유형 변경 useEffect 이동 ✅ +- [x] `prevPartTypeRef` 및 부품 유형 변경 감지 useEffect 이동 +- [x] `bendingWidthSumSyncedRef` 및 폭 합계 동기화 useEffect 이동 +- [x] 빌드 확인 ✅ -### 4.3 품목명 변경 시 종류 초기화 useEffect 이동 -- [ ] `prevItemNameValueRef` 및 품목명 변경 감지 useEffect 이동 (972-996줄) -- [ ] 빌드 확인 +### 4.3 품목명 변경 시 종류 초기화 useEffect 이동 ✅ +- [x] `prevItemNameValueRef` 및 품목명 변경 감지 useEffect 이동 +- [x] 빌드 확인 ✅ -### 4.4 index.tsx 연결 -- [ ] usePartTypeHandling 훅 import -- [ ] 기존 useEffect 코드 제거 -- [ ] 훅 호출로 대체 -- [ ] 빌드 확인 -- [ ] 기능 테스트 (부품 유형 변경 시 필드 초기화 확인) +### 4.4 index.tsx 연결 ✅ +- [x] usePartTypeHandling 훅 import +- [x] 기존 useEffect 코드 제거 (~116줄) +- [x] 훅 호출로 대체 +- [x] 빌드 확인 ✅ +- [ ] 기능 테스트 (부품 유형 변경 시 필드 초기화 확인) - 수동 테스트 필요 **Phase 4 완료 후 예상:** ~1,125줄 +**Phase 4 실제 결과:** 1,228줄 (1,344줄 → 1,228줄, 116줄 감소) ✅ --- -## Phase 5: 파일 처리 훅 분리 (~150줄 감소) +## Phase 5: 파일 처리 훅 분리 (~150줄 감소) ✅ -### 5.1 useFileHandling 훅 생성 -- [ ] `hooks/useFileHandling.ts` 파일 생성 -- [ ] 타입 정의 +### 5.1 useFileHandling 훅 생성 ✅ +- [x] `hooks/useFileHandling.ts` 파일 생성 (~328줄) +- [x] 타입 정의 (UseFileHandlingParams, FileHandlingResult) -### 5.2 파일 상태 및 useEffect 이동 -- [ ] 파일 관련 state 선언 이동 (274-286줄) -- [ ] 파일 정보 로드 useEffect 이동 (294-406줄 중 파일 관련 부분) -- [ ] `getDownloadUrl` 함수 이동 (418-422줄) -- [ ] `handleDeleteFile` 함수 이동 (425-488줄) -- [ ] 빌드 확인 +### 5.2 파일 상태 및 useEffect 이동 ✅ +- [x] 파일 관련 state 선언 이동 (existingBendingDiagram, existingSpecificationFile 등) +- [x] 파일 정보 로드 useEffect 이동 (initialData에서 files 추출) +- [x] bendingDetails 로드 로직 이동 +- [x] handleFileDownload 함수 이동 +- [x] handleDeleteFile 함수 이동 (콜백 패턴 적용) +- [x] 빌드 확인 ✅ -### 5.3 index.tsx 연결 -- [ ] useFileHandling 훅 import -- [ ] 기존 코드 제거 -- [ ] 훅 반환값으로 대체 -- [ ] 빌드 확인 -- [ ] 기능 테스트 (파일 업로드/삭제/다운로드 확인) +### 5.3 index.tsx 연결 ✅ +- [x] useFileHandling 훅 import +- [x] 기존 코드 제거 (~178줄) +- [x] 훅 반환값으로 대체 +- [x] loadedBendingDetails/loadedWidthSum 동기화 useEffect 추가 +- [x] handleDeleteFile wrapper 함수 추가 (콜백 전달용) +- [x] 빌드 확인 ✅ +- [ ] 기능 테스트 (파일 업로드/삭제/다운로드 확인) - 수동 테스트 필요 **Phase 5 완료 후 예상:** ~975줄 +**Phase 5 실제 결과:** 1,050줄 (1,228줄 → 1,050줄, 178줄 감소) ✅ --- -## Phase 6: 최종 정리 및 검증 +## Phase 6: 최종 정리 및 검증 ✅ -### 6.1 코드 정리 -- [ ] 불필요한 import 제거 -- [ ] 타입 정리 (중복 제거) -- [ ] 주석 정리 +### 6.1 코드 정리 ✅ +- [x] 불필요한 import 제거 (Button, DynamicSection, DynamicFieldValue, ItemSaveResult) +- [x] 미사용 변수 `_` 접두사 처리 (ESLint 경고 해결) +- [x] 불필요한 eslint-disable 주석 제거 +- [x] 브라우저 API (atob, Blob) ESLint 예외 처리 -### 6.2 hooks/index.ts 업데이트 -- [ ] 새로운 훅들 export 추가 +### 6.2 hooks/index.ts 업데이트 ✅ +- [x] useFileHandling export 추가 +- [x] FileHandlingResult, UseFileHandlingParams 타입 export 추가 -### 6.3 최종 검증 -- [ ] 빌드 성공 확인 -- [ ] 타입 에러 없음 확인 -- [ ] ESLint 경고 확인 +### 6.3 최종 검증 ✅ +- [x] 빌드 성공 확인 (`npm run build` 통과) +- [x] 타입 에러 없음 확인 +- [x] ESLint 경고 확인 (0 errors, 2 warnings - 기존 경고) -### 6.4 기능 테스트 체크리스트 +### 6.4 기능 테스트 체크리스트 (수동 테스트 필요) - [ ] FG(제품) 등록/수정 테스트 - [ ] PT(부품) - 절곡부품 등록/수정 테스트 - [ ] PT(부품) - 조립부품 등록/수정 테스트 @@ -188,35 +204,62 @@ - [ ] 품목코드 자동생성 테스트 - [ ] 조건부 필드 표시 테스트 +**Phase 6 완료:** 2025-12-16 ✅ + --- -## 최종 파일 구조 +## 최종 파일 구조 (실제 결과) ``` src/components/items/DynamicItemForm/ -├── index.tsx (~900줄, 메인 컴포넌트) +├── index.tsx (1,050줄, 메인 컴포넌트) ✅ ├── components/ +│ ├── index.ts (배럴 export) │ ├── FormHeader.tsx (~60줄) │ ├── ValidationAlert.tsx (~40줄) │ ├── FileUploadFields.tsx (~200줄) │ └── DuplicateCodeDialog.tsx (~40줄) ├── hooks/ -│ ├── index.ts (기존 + 새 훅 export) -│ ├── useFormStructure.ts (기존) -│ ├── useDynamicFormState.ts (기존) -│ ├── useConditionalDisplay.ts (기존) -│ ├── useItemCodeGeneration.ts (~300줄, 신규) -│ ├── useFieldDetection.ts (~200줄, 신규) -│ ├── usePartTypeHandling.ts (~150줄, 신규) -│ └── useFileHandling.ts (~150줄, 신규) +│ ├── index.ts (22줄, 배럴 export) +│ ├── useFormStructure.ts (95줄, 기존) +│ ├── useDynamicFormState.ts (199줄, 기존) +│ ├── useConditionalDisplay.ts (182줄, 기존) +│ ├── useItemCodeGeneration.ts (523줄, 신규) ✅ +│ ├── useFieldDetection.ts (174줄, 신규) ✅ +│ ├── usePartTypeHandling.ts (192줄, 신규) ✅ +│ └── useFileHandling.ts (328줄, 신규) ✅ ├── fields/ (기존) ├── sections/ (기존) ├── types/ (기존) └── utils/ (기존) + +hooks 디렉토리 총: 1,715줄 ``` --- +## 리팩토링 결과 요약 + +| Phase | 시작 | 종료 | 감소량 | 상태 | +|-------|------|------|--------|------| +| Phase 1: 컴포넌트 분리 | 2,161줄 | 1,842줄 | -319줄 | ✅ | +| Phase 2: useItemCodeGeneration | 1,842줄 | 1,432줄 | -410줄 | ✅ | +| Phase 3: useFieldDetection | 1,432줄 | 1,344줄 | -88줄 | ✅ | +| Phase 4: usePartTypeHandling | 1,344줄 | 1,228줄 | -116줄 | ✅ | +| Phase 5: useFileHandling | 1,228줄 | 1,050줄 | -178줄 | ✅ | +| Phase 6: 최종 정리 | 1,050줄 | 1,050줄 | 0줄 | ✅ | +| **총계** | **2,161줄** | **1,050줄** | **-1,111줄 (51% 감소)** | ✅ | + +**최종 결과:** +- index.tsx: 2,161줄 → 1,050줄 (51% 감소) +- 신규 훅 4개 생성 (1,217줄) +- 기존 훅 4개 재사용 (498줄) +- 컴포넌트 4개 분리 + +**완료일:** 2025-12-16 + +--- + ## 리스크 및 롤백 계획 ### 리스크 평가 diff --git a/claudedocs/settings/[IMPL-2025-12-19] account-info.md b/claudedocs/settings/[IMPL-2025-12-19] account-info.md new file mode 100644 index 00000000..3bfbeace --- /dev/null +++ b/claudedocs/settings/[IMPL-2025-12-19] account-info.md @@ -0,0 +1,76 @@ +# 계정정보 페이지 구현 + +> 생성일: 2025-12-19 +> URL: `/ko/settings/account-info` + +## 📋 체크리스트 + +### Phase 1: 기본 구조 +- [x] page.tsx 생성 (`/settings/account-info/page.tsx`) +- [x] AccountInfoClient 컴포넌트 생성 + +### Phase 2: 계정 정보 섹션 +- [x] 프로필 사진 영역 (1250x250px, 10MB 이하 PNG/JPEG/GIF) +- [x] 아이디 표시 (읽기 전용) +- [x] 비밀번호 영역 + "변경" 버튼 +- [x] 권한 표시 +- [x] 상태 표시 + +### Phase 3: 약관 동의 정보 섹션 +- [x] [필수] 서비스 이용약관 동의 + 동의일시 +- [x] [필수] 개인정보 취급방침 + 동의일시 +- [x] [선택] 마케팅 정보 수신 동의 + - [x] 이메일 수신 동의 + 동의일시 + - [x] SMS 수신 동의 + 동의철회일시 + +### Phase 4: 액션 버튼 +- [x] 탈퇴 버튼 (테넌트 마스터 아닌 경우만 활성화) + - [x] 확인 Alert: "정말 탈퇴하시겠습니까?" + - [ ] 탈퇴 처리 API 연동 (Mock 구현) +- [x] 사용중지 버튼 (테넌트 마스터인 경우만 활성화) + - [x] 확인 Alert: "정말 사용중지하시겠습니까?" + - [ ] 사용중지 처리 API 연동 (Mock 구현) +- [x] 수정 버튼 +- [x] 비밀번호 변경 버튼 → 비밀번호 설정 화면 이동 + +### Phase 5: 마무리 +- [x] URL 목록 문서 업데이트 +- [ ] 테스트 및 확인 + +--- + +## 📝 Description (스크린샷 기준) + +### 탈퇴 버튼 +- 테넌트 마스터가 아닌 경우에만 버튼 활성화 +- 클릭: "정말 탈퇴하시겠습니까?" 확인 Alert 표시 +- 확인 버튼 클릭 시 탈퇴 처리 (모든 테넌트에서 탈퇴 처리, SAM 탈퇴 처리) + +### 사용중지 버튼 +- 테넌트 마스터인 이면서 경우에만 버튼 활성화 +- 클릭: "정말 사용중지하시겠습니까?" 확인 Alert 표시 +- 확인 버튼 클릭 시 사용중지 처리 (해당 테넌트의 사용중지 처리) + +### 변경 버튼 +- 클릭: 비밀번호 설정 화면으로 이동 + +--- + +## 🎨 UI 참고 + +### 계정 정보 섹션 +| 필드 | 타입 | 비고 | +|------|------|------| +| 프로필 사진 | 이미지 업로드 | 1250x250px, 10MB 이하, PNG/JPEG/GIF | +| 아이디 | 텍스트 (읽기전용) | abc@email.com | +| 비밀번호 | 버튼 | "변경" + 숨김 아이콘 | +| 권한 | 텍스트 (읽기전용) | 권한명 | +| 상태 | 텍스트 (읽기전용) | 정상 | + +### 약관 동의 정보 섹션 +| 항목 | 타입 | 동의일시/철회일시 | +|------|------|------------------| +| [필수] 서비스 이용약관 동의 | 텍스트 | 동의일시 표시 | +| [필수] 개인정보 취급방침 | 텍스트 | 동의일시 표시 | +| [선택] 이메일 수신 동의 | 체크박스 | 동의일시 표시 | +| [선택] SMS 수신 동의 | 체크박스 | 동의철회일시 표시 | \ No newline at end of file diff --git a/claudedocs/settings/[IMPL-2025-12-19] account-management-checklist.md b/claudedocs/settings/[IMPL-2025-12-19] account-management-checklist.md new file mode 100644 index 00000000..0aeaa649 --- /dev/null +++ b/claudedocs/settings/[IMPL-2025-12-19] account-management-checklist.md @@ -0,0 +1,125 @@ +# 계좌관리 구현 체크리스트 + +> 작성일: 2025-12-19 +> 경로: 기준정보 > 계좌관리 + +--- + +## 스크린샷 분석 + +### 리스트 페이지 (계좌관리) +- **경로**: `/ko/settings/accounts` +- **화면명**: 계좌관리 +- **제목**: 계좌관리 +- **부제목**: 계좌 목록을 관리합니다 + +**테이블 컬럼**: +| No. | 컬럼명 | 설명 | +|-----|--------|------| +| 1 | No. | 번호 (1부터 시작) | +| 2 | 은행 | 신한은행, 국민은행, 우리은행 등 | +| 3 | 계좌번호 | 1234-****-****-1234 (마스킹) | +| 4 | 계좌명 | 계좌명 | +| 5 | 예금주 | 예금주명 | +| 6 | 작업 | 수정/삭제 버튼 (체크박스 선택 시) | + +**상단 UI**: +- 검색창 +- 페이지 개수 선택 (12개 선택) +- 삭제 버튼 (다중 선택) +- 계좌 등록 버튼 + +**버튼 동작**: +- 계좌 등록: 계좌 상세 화면(등록 모드)으로 이동 +- 삭제 (상단): "선택하신 N개의 계좌를 정말 삭제하시겠습니까?" 확인 팝업 +- 수정 (행): 계좌 상세 화면으로 이동 +- 삭제 (행): "계좌를 정말 삭제하시겠습니까?" 확인 팝업 + +--- + +### 상세 페이지 (계좌 상세) +- **경로**: `/ko/settings/accounts/[id]` (상세), `/ko/settings/accounts/new` (등록) +- **화면명**: 계좌 상세 +- **제목**: 계좌 상세 +- **부제목**: 계좌 정보를 관리합니다 + +**상단 버튼**: 삭제, 수정 + +**기본 정보 섹션**: +| 필드 | 타입 | 설명 | +|------|------|------| +| 은행 | Dropdown | 은행명 선택 | +| 계좌번호 | Text (readonly) | 1234-1234-1234-1234 | +| 예금주 | Text | 예금주명 | +| 계좌 비밀번호 (빠른 조회 서비스) | Password | 마스킹 처리 | +| 계좌명 | Text | 계좌명을 입력해주세요 | +| 상태 | Dropdown | 사용/정지 | + +**상태 옵션**: +- 사용: 계좌 활성화 +- 정지: 해당 계좌의 자동 조회 중지 + +--- + +## 구현 체크리스트 + +### Phase 1: 파일 구조 생성 +- [ ] `src/app/[locale]/(protected)/settings/accounts/page.tsx` 생성 +- [ ] `src/app/[locale]/(protected)/settings/accounts/[id]/page.tsx` 생성 +- [ ] `src/app/[locale]/(protected)/settings/accounts/new/page.tsx` 생성 +- [ ] `src/components/settings/AccountManagement/index.tsx` 생성 +- [ ] `src/components/settings/AccountManagement/types.ts` 생성 +- [ ] `src/components/settings/AccountManagement/AccountDetail.tsx` 생성 +- [ ] `src/components/settings/AccountManagement/AccountForm.tsx` 생성 + +### Phase 2: 타입 정의 +- [ ] Account 인터페이스 정의 +- [ ] AccountFormData 타입 정의 +- [ ] 은행 목록 상수 정의 +- [ ] 상태 옵션 상수 정의 + +### Phase 3: 리스트 페이지 구현 +- [ ] IntegratedListTemplateV2 사용 +- [ ] 테이블 컬럼 정의 (No., 은행, 계좌번호, 계좌명, 예금주, 작업) +- [ ] 계좌번호 마스킹 처리 +- [ ] 검색 기능 +- [ ] 페이지네이션 +- [ ] 다중 선택 삭제 기능 +- [ ] 계좌 등록 버튼 → /settings/accounts/new 이동 +- [ ] 수정 버튼 → /settings/accounts/[id] 이동 +- [ ] 삭제 확인 AlertDialog + +### Phase 4: 상세/등록 페이지 구현 +- [ ] 등록 페이지 (/new) - AccountForm mode="create" +- [ ] 상세 페이지 (/[id]) - AccountDetail + AccountForm mode="view/edit" +- [ ] 기본 정보 섹션 레이아웃 +- [ ] 은행 선택 드롭다운 +- [ ] 계좌번호 필드 (상세에서 readonly) +- [ ] 예금주 필드 +- [ ] 계좌 비밀번호 필드 (마스킹) +- [ ] 계좌명 필드 +- [ ] 상태 드롭다운 (사용/정지) +- [ ] 삭제/수정 버튼 + +### Phase 5: Mock 데이터 & 테스트 +- [ ] Mock 계좌 데이터 생성 +- [ ] 리스트 페이지 테스트 +- [ ] 상세 페이지 테스트 +- [ ] 등록 페이지 테스트 +- [ ] 삭제 기능 테스트 + +### Phase 6: 마무리 +- [ ] URL 테스트 문서 업데이트 +- [ ] 체크리스트 완료 표시 + +--- + +## 참고 사항 + +### Description (스크린샷 오른쪽 패널) +- 계좌 인증 정보, 비밀번호(빠른 조회 서비스)를 바로결제 API에 전달하여 계좌 내역 자동 수신 +- 연동 성공 시 해당 계좌의 사용 내역이 자동으로 시스템에 반영됨 +- 해당 타인에는 은행에서 빠른 조회 서비스 사전 등록 필요 + +### 삭제 시 주의사항 +- 삭제된 계좌의 과거 사용 내역은 보존 \ No newline at end of file diff --git a/claudedocs/settings/[IMPL-2025-12-19] company-info.md b/claudedocs/settings/[IMPL-2025-12-19] company-info.md new file mode 100644 index 00000000..115d909c --- /dev/null +++ b/claudedocs/settings/[IMPL-2025-12-19] company-info.md @@ -0,0 +1,83 @@ +# [IMPL-2025-12-19] 회사정보 페이지 구현 + +## 개요 +- **위치**: 보고서 및 분석 > 계정정보 다음 (사이드바 루트 레벨 별도 메뉴) +- **경로**: `/ko/company-info` + +## 스크린샷 분석 + +### 1. 회사 정보 섹션 +- [x] 회사 로고 업로드 (750x250px, 10MB 이하, PNG/JPEG/GIF) +- [x] 회사명 입력 +- [x] 대표자명 입력 +- [x] 업태 입력 +- [x] 업종 입력 +- [x] 주소 (우편번호 찾기 버튼, 주소명, 상세주소) +- [x] 이메일 (아이디) 입력 +- [x] 세금계산서 이메일 입력 +- [x] 담당자명 입력 +- [x] 담당자 연락처 입력 +- [x] 사업자등록증 파일 업로드 +- [x] 사업자등록번호 입력 + +### 2. 결제 계좌 정보 섹션 +- [x] 결제 은행 입력 +- [x] 계좌 입력 +- [x] 예금주 입력 +- [x] 결제일 입력 + +### 3. 버튼/기능 +- [x] 회사 추가 버튼 → 회사 추가 팝업 표시 +- [x] 수정 버튼 + +### 4. 회사 추가 팝업 +- [x] 사업자등록번호 입력 필드 (숫자만 가능, 10자리) +- [x] 취소 버튼 +- [x] 다음 버튼 (바로빌 API 조회) + - 사용 불가 경우: "휴폐업 상태인 사업자입니다." Alert + - 등록된 번호: "등록된 사업자등록번호 입니다." Alert + - 미등록 번호: "매니저에게 회사 추가 신청 알림을 발송했습니다." Alert + +## 구현 체크리스트 + +### Phase 1: 기본 구조 +- [x] 폴더/파일 생성 + - [x] `src/app/[locale]/(protected)/company-info/page.tsx` + - [x] `src/components/settings/CompanyInfoManagement/index.tsx` + - [x] `src/components/settings/CompanyInfoManagement/types.ts` + - [x] `src/components/settings/CompanyInfoManagement/AddCompanyDialog.tsx` + +### Phase 2: 컴포넌트 구현 +- [x] types.ts - 타입 정의 +- [x] index.tsx - 메인 폼 컴포넌트 +- [x] AddCompanyDialog.tsx - 회사 추가 팝업 + +### Phase 3: 페이지 연결 +- [x] page.tsx 생성 +- [ ] API 연동 (TODO) + +## 생성된 파일 목록 + +``` +src/ +├── app/[locale]/(protected)/company-info/ +│ └── page.tsx +└── components/settings/CompanyInfoManagement/ + ├── index.tsx + ├── types.ts + └── AddCompanyDialog.tsx +``` + +## 테스트 URL +- `/ko/company-info` + +## 참조 +- 기존 스타일: `AccountDetail.tsx` +- 레이아웃: `PageLayout`, `Card` 컴포넌트 사용 + +## TODO (API 연동) +- [ ] 회사 정보 조회 API +- [ ] 회사 정보 수정 API +- [ ] 회사 추가 (바로빌 사업자등록번호 조회) API +- [ ] 다음 주소 API 연동 +- [ ] 파일 업로드 API (로고, 사업자등록증) diff --git a/claudedocs/settings/[IMPL-2025-12-19] popup-management.md b/claudedocs/settings/[IMPL-2025-12-19] popup-management.md new file mode 100644 index 00000000..868e70a9 --- /dev/null +++ b/claudedocs/settings/[IMPL-2025-12-19] popup-management.md @@ -0,0 +1,71 @@ +# [IMPL-2025-12-19] 팝업관리 페이지 구현 + +> 버디 셋팅 > 팝업관리 페이지 구현 + +## 스크린샷 분석 + +### 리스트 페이지 (팝업관리) +- **테이블 컬럼**: 체크박스, No, 대상, 제목, 상태, 작성자, 등록일, 기간, 작업 +- **헤더**: "팝업 등록" 버튼 +- **검색**: 검색창 +- **선택 삭제**: 2개 이상 선택 시 활성화 + +### 상세/등록 페이지 (팝업 상세) +- **대상**: Select (전사/부서별) +- **기간**: DateRangePicker (시작일~종료일) +- **제목**: Input +- **내용**: RichTextEditor (게시판과 동일) +- **상태**: Radio (사용안함/사용함) +- **작성자**: 읽기전용 +- **등록일시**: 읽기전용 + +--- + +## 체크리스트 + +### Phase 1: 컴포넌트 구조 설정 +- [x] types.ts 생성 (Popup 타입 정의) +- [x] PopupList 컴포넌트 생성 +- [x] PopupForm 컴포넌트 생성 +- [x] PopupDetail 컴포넌트 생성 + +### Phase 2: 페이지 라우트 생성 +- [x] /settings/popup-management/page.tsx (리스트) +- [x] /settings/popup-management/new/page.tsx (등록) +- [x] /settings/popup-management/[id]/page.tsx (상세) +- [x] /settings/popup-management/[id]/edit/page.tsx (수정) + +### Phase 3: 마무리 +- [x] 테스트 URL 문서 업데이트 (all-pages-test-urls.md) +- [x] 구현 완료 + +--- + +## 구현 세부사항 + +### 경로 구조 +``` +/ko/settings/popup-management → 리스트 +/ko/settings/popup-management/new → 등록 +/ko/settings/popup-management/[id] → 상세 +/ko/settings/popup-management/[id]/edit → 수정 +``` + +### 참고 컴포넌트 +- 리스트: IntegratedListTemplateV2 +- 에디터: /components/board/RichTextEditor +- 폼 패턴: /components/board/BoardForm + +--- + +## 작업 로그 + +| 시간 | 작업 | 상태 | +|------|------|------| +| 시작 | 체크리스트 문서 생성 | ✅ | +| | types.ts 생성 | ✅ | +| | PopupList 컴포넌트 | ✅ | +| | PopupForm 컴포넌트 | ✅ | +| | PopupDetail 컴포넌트 | ✅ | +| | page.tsx 라우트 생성 | ✅ | +| | 테스트 URL 업데이트 | ✅ | \ No newline at end of file diff --git a/claudedocs/settings/[IMPL-2025-12-19] subscription-management.md b/claudedocs/settings/[IMPL-2025-12-19] subscription-management.md new file mode 100644 index 00000000..dadcab08 --- /dev/null +++ b/claudedocs/settings/[IMPL-2025-12-19] subscription-management.md @@ -0,0 +1,71 @@ +# 구독관리 페이지 구현 + +> 작성일: 2025-12-19 +> URL: `/ko/settings/subscription` + +## 스크린샷 분석 + +### 페이지 구조 +- **제목**: 구독관리 +- **부제목**: 구독 정보를 관리합니다 +- **테넌트 마스터에게만 표시** + +### 상단 버튼 +1. **자료 내보내기** (01) - 클릭: 자료 다운로드 처리 +2. **서비스 해지** (02) - 클릭: "모든 데이터가 삭제되며 복구할 수 없습니다. 정말 서비스를 해지하시겠습니까?" 확인 Alert 표시, 확인 버튼 클릭 시 서비스 해지 처리 + +### 구독 정보 카드 (3개 가로 배열) +| 항목 | 값 | +|------|-----| +| 최근 결제일시 | 2025년 12월 1일 | +| 다음 결제일시 | 2025년 12월 1일 | +| 구독금액 | 500,000원 | + +### 구독 정보 영역 (03) +| 항목 | 진행률 | 값 | +|------|--------|-----| +| 플랜 | - | 프리미엄 | +| 사용자 수 | Progress Bar | 100명 / 무제한 | +| 저장 공간 | Progress Bar | 5.5 TB / 10 TB | +| AI API 호출 | Progress Bar | 8,500 / 10,000 | + +--- + +## 체크리스트 + +### Phase 1: 기본 구조 +- [x] page.tsx 생성 (`/settings/subscription`) +- [x] SubscriptionManagement 컴포넌트 생성 +- [x] types.ts 정의 + +### Phase 2: UI 구현 +- [x] PageLayout 적용 +- [x] 헤더 영역 (제목 + 버튼) +- [x] 구독 정보 카드 3개 (최근결제, 다음결제, 금액) +- [x] 구독 정보 영역 (플랜 + Progress Bar) + +### Phase 3: 기능 구현 +- [x] 자료 내보내기 버튼 핸들러 +- [x] 서비스 해지 AlertDialog 구현 +- [x] Mock 데이터 연결 + +### Phase 4: 마무리 +- [x] URL 문서 업데이트 +- [ ] 테스트 + +--- + +## 생성된 파일 + +``` +src/ +├── app/[locale]/(protected)/settings/subscription/ +│ └── page.tsx +└── components/settings/SubscriptionManagement/ + ├── index.tsx + └── types.ts +``` + +## 테스트 URL + +http://localhost:3000/ko/settings/subscription \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index de4d0081..d9c01ebc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,14 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "@tiptap/extension-image": "^3.13.0", + "@tiptap/extension-link": "^3.13.0", + "@tiptap/extension-placeholder": "^3.13.0", + "@tiptap/extension-text-align": "^3.13.0", + "@tiptap/extension-underline": "^3.13.0", + "@tiptap/pm": "^3.13.0", + "@tiptap/react": "^3.13.0", + "@tiptap/starter-kit": "^3.13.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -45,6 +53,7 @@ "devDependencies": { "@playwright/test": "^1.57.0", "@tailwindcss/postcss": "^4", + "@types/google.maps": "^3.58.1", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -3332,6 +3341,12 @@ } } }, + "node_modules/@remirror/core-constants": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", + "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==", + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -3644,6 +3659,479 @@ "tailwindcss": "4.1.16" } }, + "node_modules/@tiptap/core": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.13.0.tgz", + "integrity": "sha512-iUelgiTMgPVMpY5ZqASUpk8mC8HuR9FWKaDzK27w9oWip9tuB54Z8mePTxNcQaSPb6ErzEaC8x8egrRt7OsdGQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.13.0.tgz", + "integrity": "sha512-K1z/PAIIwEmiWbzrP//4cC7iG1TZknDlF1yb42G7qkx2S2X4P0NiqX7sKOej3yqrPjKjGwPujLMSuDnCF87QkQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.13.0.tgz", + "integrity": "sha512-VYiDN9EEwR6ShaDLclG8mphkb/wlIzqfk7hxaKboq1G+NSDj8PcaSI9hldKKtTCLeaSNu6UR5nkdu/YHdzYWTw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.13.0.tgz", + "integrity": "sha512-qZ3j2DBsqP9DjG2UlExQ+tHMRhAnWlCKNreKddKocb/nAFrPdBCtvkqIEu+68zPlbLD4ukpoyjUklRJg+NipFg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0", + "@tiptap/pm": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.13.0.tgz", + "integrity": "sha512-fFQmmEUoPzRGiQJ/KKutG35ZX21GE+1UCDo8Q6PoWH7Al9lex47nvyeU1BiDYOhcTKgIaJRtEH5lInsOsRJcSA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.13.0.tgz", + "integrity": "sha512-sF5raBni6iSVpXWvwJCAcOXw5/kZ+djDHx1YSGWhopm4+fsj0xW7GvVO+VTwiFjZGKSw+K5NeAxzcQTJZd3Vhw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.13.0.tgz", + "integrity": "sha512-kIwfQ4iqootsWg9e74iYJK54/YMIj6ahUxEltjZRML5z/h4gTDcQt2eTpnEC8yjDjHeUVOR94zH9auCySyk9CQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0", + "@tiptap/pm": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.13.0.tgz", + "integrity": "sha512-RjU7hTJwjKXIdY57o/Pc+Yr8swLkrwT7PBQ/m+LCX5oO/V2wYoWCjoBYnK5KSHrWlNy/aLzC33BvLeqZZ9nzlQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.13.0.tgz", + "integrity": "sha512-m7GPT3c/83ni+bbU8c+3dpNa8ug+aQ4phNB1Q52VQG3oTonDJnZS7WCtn3lB/Hi1LqoqMtEHwhepU2eD+JeXqQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.13.0.tgz", + "integrity": "sha512-OsezV2cMofZM4c13gvgi93IEYBUzZgnu8BXTYZQiQYekz4bX4uulBmLa1KOA9EN71FzS+SoLkXHU0YzlbLjlxA==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "^3.13.0", + "@tiptap/pm": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.13.0.tgz", + "integrity": "sha512-KVxjQKkd964nin+1IdM2Dvej/Jy4JTMcMgq5seusUhJ9T9P8F9s2D5Iefwgkps3OCzub/aF+eAsZe+1P5KSIgA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.13.0.tgz", + "integrity": "sha512-nH1OBaO+/pakhu+P1jF208mPgB70IKlrR/9d46RMYoYbqJTNf4KVLx5lHAOHytIhjcNg+MjyTfJWfkK+dyCCyg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.13.0.tgz", + "integrity": "sha512-8VKWX8waYPtUWN97J89em9fOtxNteh6pvUEd0htcOAtoxjt2uZjbW5N4lKyWhNKifZBrVhH2Cc2NUPuftCVgxw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.13.0.tgz", + "integrity": "sha512-ZUFyORtjj22ib8ykbxRhWFQOTZjNKqOsMQjaAGof30cuD2DN5J5pMz7Haj2fFRtLpugWYH+f0Mi+WumQXC3hCw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0", + "@tiptap/pm": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-image": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.13.0.tgz", + "integrity": "sha512-223uzLUkIa1rkK7aQK3AcIXe6LbCtmnpVb7sY5OEp+LpSaSPyXwyrZ4A0EO1o98qXG68/0B2OqMntFtA9c5Fbw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.13.0.tgz", + "integrity": "sha512-XbVTgmzk1kgUMTirA6AGdLTcKHUvEJoh3R4qMdPtwwygEOe7sBuvKuLtF6AwUtpnOM+Y3tfWUTNEDWv9AcEdww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.13.0.tgz", + "integrity": "sha512-LuFPJ5GoL12GHW4A+USsj60O90pLcwUPdvEUSWewl9USyG6gnLnY/j5ZOXPYH7LiwYW8+lhq7ABwrDF2PKyBbA==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0", + "@tiptap/pm": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-list": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.13.0.tgz", + "integrity": "sha512-MMFH0jQ4LeCPkJJFyZ77kt6eM/vcKujvTbMzW1xSHCIEA6s4lEcx9QdZMPpfmnOvTzeoVKR4nsu2t2qT9ZXzAw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0", + "@tiptap/pm": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.13.0.tgz", + "integrity": "sha512-63NbcS/XeQP2jcdDEnEAE3rjJICDj8y1SN1h/MsJmSt1LusnEo8WQ2ub86QELO6XnD3M04V03cY6Knf6I5mTkw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.13.0.tgz", + "integrity": "sha512-P+HtIa1iwosb1feFc8B/9MN5EAwzS+/dZ0UH0CTF2E4wnp5Z9OMxKl1IYjfiCwHzZrU5Let+S/maOvJR/EmV0g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.13.0.tgz", + "integrity": "sha512-QuDyLzuK/3vCvx9GeKhgvHWrGECBzmJyAx6gli2HY+Iil7XicbfltV4nvhIxgxzpx3LDHLKzJN9pBi+2MzX60g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.13.0.tgz", + "integrity": "sha512-9csQde1i0yeZI5oQQ9e1GYNtGL2JcC2d8Fwtw9FsGC8yz2W0h+Fmk+3bc2kobbtO5LGqupSc1fKM8fAg5rSRDg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-placeholder": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.13.0.tgz", + "integrity": "sha512-Au4ktRBraQktX9gjSzGWyJV6kPof7+kOhzE8ej+rOMjIrHbx3DCHy1CJWftSO9BbqIyonjsFmm4nE+vjzZ3Z5Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.13.0.tgz", + "integrity": "sha512-VHhWNqTAMOfrC48m2FcPIZB0nhl6XHQviAV16SBc+EFznKNv9tQUsqQrnuQ2y6ZVfqq5UxvZ3hKF/JlN/Ff7xw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.13.0.tgz", + "integrity": "sha512-VcZIna93rixw7hRkHGCxDbL3kvJWi80vIT25a2pXg0WP1e7Pi3nBYvZIL4SQtkbBCji9EHrbZx3p8nNPzfazYw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-text-align": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.13.0.tgz", + "integrity": "sha512-hebIus9tdXWb+AmhO+LTeUxZLdb0tqwdeaL/0wYxJQR5DeCTlJe6huXacMD/BkmnlEpRhxzQH0FrmXAd0d4Wgg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.13.0.tgz", + "integrity": "sha512-VDQi+UYw0tFnfghpthJTFmtJ3yx90kXeDwFvhmT8G+O+si5VmP05xYDBYBmYCix5jqKigJxEASiBL0gYOgMDEg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.13.0.tgz", + "integrity": "sha512-i7O0ptSibEtTy+2PIPsNKEvhTvMaFJg1W4Oxfnbuxvaigs7cJV9Q0lwDUcc7CPsNw2T1+44wcxg431CzTvdYoA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0", + "@tiptap/pm": "^3.13.0" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.13.0.tgz", + "integrity": "sha512-WKR4ucALq+lwx0WJZW17CspeTpXorbIOpvKv5mulZica6QxqfMhn8n1IXCkDws/mCoLRx4Drk5d377tIjFNsvQ==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.3.0", + "prosemirror-collab": "^1.3.1", + "prosemirror-commands": "^1.6.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-inputrules": "^1.4.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.13.1", + "prosemirror-menu": "^1.2.4", + "prosemirror-model": "^1.24.1", + "prosemirror-schema-basic": "^1.2.3", + "prosemirror-schema-list": "^1.5.0", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.6.4", + "prosemirror-trailing-node": "^3.0.0", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.38.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.13.0.tgz", + "integrity": "sha512-VqpqNZ9qtPr3pWK4NsZYxXgLSEiAnzl6oS7tEGmkkvJbcGSC+F7R13Xc9twv/zT5QCLxaHdEbmxHbuAIkrMgJQ==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "fast-equals": "^5.3.3", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.13.0", + "@tiptap/extension-floating-menu": "^3.13.0" + }, + "peerDependencies": { + "@tiptap/core": "^3.13.0", + "@tiptap/pm": "^3.13.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.13.0.tgz", + "integrity": "sha512-Ojn6sRub04CRuyQ+9wqN62JUOMv+rG1vXhc2s6DCBCpu28lkCMMW+vTe7kXJcEdbot82+5swPbERw9vohswFzg==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^3.13.0", + "@tiptap/extension-blockquote": "^3.13.0", + "@tiptap/extension-bold": "^3.13.0", + "@tiptap/extension-bullet-list": "^3.13.0", + "@tiptap/extension-code": "^3.13.0", + "@tiptap/extension-code-block": "^3.13.0", + "@tiptap/extension-document": "^3.13.0", + "@tiptap/extension-dropcursor": "^3.13.0", + "@tiptap/extension-gapcursor": "^3.13.0", + "@tiptap/extension-hard-break": "^3.13.0", + "@tiptap/extension-heading": "^3.13.0", + "@tiptap/extension-horizontal-rule": "^3.13.0", + "@tiptap/extension-italic": "^3.13.0", + "@tiptap/extension-link": "^3.13.0", + "@tiptap/extension-list": "^3.13.0", + "@tiptap/extension-list-item": "^3.13.0", + "@tiptap/extension-list-keymap": "^3.13.0", + "@tiptap/extension-ordered-list": "^3.13.0", + "@tiptap/extension-paragraph": "^3.13.0", + "@tiptap/extension-strike": "^3.13.0", + "@tiptap/extension-text": "^3.13.0", + "@tiptap/extension-underline": "^3.13.0", + "@tiptap/extensions": "^3.13.0", + "@tiptap/pm": "^3.13.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -3725,6 +4213,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/google.maps": { + "version": "3.58.1", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz", + "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3739,6 +4234,28 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.24", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz", @@ -3753,7 +4270,6 @@ "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -3763,7 +4279,6 @@ "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", - "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -4405,7 +4920,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-hidden": { @@ -4841,6 +5355,12 @@ "dev": true, "license": "MIT" }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4860,7 +5380,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, "license": "MIT" }, "node_modules/d3-array": { @@ -5199,6 +5718,18 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -5390,7 +5921,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -5817,6 +6347,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -7162,6 +7701,21 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/linkifyjs": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -7217,6 +7771,23 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -7227,6 +7798,12 @@ "node": ">= 0.4" } }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -7579,6 +8156,12 @@ "node": ">= 0.8.0" } }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -7781,6 +8364,201 @@ "react-is": "^16.13.1" } }, + "node_modules/prosemirror-changeset": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz", + "integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-collab": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz", + "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.0.tgz", + "integrity": "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-markdown": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz", + "integrity": "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==", + "license": "MIT", + "dependencies": { + "@types/markdown-it": "^14.0.0", + "markdown-it": "^14.0.0", + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-menu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz", + "integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==", + "license": "MIT", + "dependencies": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.4", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", + "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-basic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz", + "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.3.tgz", + "integrity": "sha512-wbqCR/RlRPRe41a4LFtmhKElzBEfBTdtAYWNIGHM6X2e24NN/MTNUKyXjjphfAfdQce37Kh/5yf765mLPYDe7Q==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-trailing-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", + "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", + "license": "MIT", + "dependencies": { + "@remirror/core-constants": "3.0.0", + "escape-string-regexp": "^4.0.0" + }, + "peerDependencies": { + "prosemirror-model": "^1.22.1", + "prosemirror-state": "^1.4.2", + "prosemirror-view": "^1.33.8" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.5.tgz", + "integrity": "sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.41.4", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.4.tgz", + "integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.20.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7791,6 +8569,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8115,6 +8902,12 @@ "node": ">=0.10.0" } }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8887,6 +9680,12 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -9059,6 +9858,12 @@ "d3-timer": "^3.0.1" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index eb3ed1b5..0736f23a 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,14 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "@tiptap/extension-image": "^3.13.0", + "@tiptap/extension-link": "^3.13.0", + "@tiptap/extension-placeholder": "^3.13.0", + "@tiptap/extension-text-align": "^3.13.0", + "@tiptap/extension-underline": "^3.13.0", + "@tiptap/pm": "^3.13.0", + "@tiptap/react": "^3.13.0", + "@tiptap/starter-kit": "^3.13.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -49,6 +57,7 @@ "devDependencies": { "@playwright/test": "^1.57.0", "@tailwindcss/postcss": "^4", + "@types/google.maps": "^3.58.1", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/edit/page.tsx b/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/edit/page.tsx new file mode 100644 index 00000000..a49973fc --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/edit/page.tsx @@ -0,0 +1,11 @@ +'use client'; + +import { useParams } from 'next/navigation'; +import { BadDebtDetail } from '@/components/accounting/BadDebtCollection/BadDebtDetail'; + +export default function EditBadDebtPage() { + const params = useParams(); + const recordId = params.id as string; + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/page.tsx b/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/page.tsx new file mode 100644 index 00000000..0a7e9bf1 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/page.tsx @@ -0,0 +1,11 @@ +'use client'; + +import { useParams } from 'next/navigation'; +import { BadDebtDetail } from '@/components/accounting/BadDebtCollection/BadDebtDetail'; + +export default function BadDebtDetailPage() { + const params = useParams(); + const recordId = params.id as string; + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/bad-debt-collection/new/page.tsx b/src/app/[locale]/(protected)/accounting/bad-debt-collection/new/page.tsx new file mode 100644 index 00000000..342d5bb7 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/bad-debt-collection/new/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { BadDebtDetail } from '@/components/accounting/BadDebtCollection/BadDebtDetail'; + +export default function NewBadDebtPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx b/src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx new file mode 100644 index 00000000..729b940c --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { BadDebtCollection } from '@/components/accounting/BadDebtCollection'; + +export default function BadDebtCollectionPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/bank-transactions/page.tsx b/src/app/[locale]/(protected)/accounting/bank-transactions/page.tsx new file mode 100644 index 00000000..5f0ce7e2 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/bank-transactions/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { BankTransactionInquiry } from '@/components/accounting/BankTransactionInquiry'; + +export default function BankTransactionsPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/bills/[id]/page.tsx b/src/app/[locale]/(protected)/accounting/bills/[id]/page.tsx new file mode 100644 index 00000000..7775d30a --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/bills/[id]/page.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { useParams, useSearchParams } from 'next/navigation'; +import { BillDetail } from '@/components/accounting/BillManagement/BillDetail'; + +export default function BillDetailPage() { + const params = useParams(); + const searchParams = useSearchParams(); + const billId = params.id as string; + const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view'; + + return ; +} diff --git a/src/app/[locale]/(protected)/accounting/bills/new/page.tsx b/src/app/[locale]/(protected)/accounting/bills/new/page.tsx new file mode 100644 index 00000000..e6159c62 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/bills/new/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { BillDetail } from '@/components/accounting/BillManagement/BillDetail'; + +export default function BillNewPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/accounting/bills/page.tsx b/src/app/[locale]/(protected)/accounting/bills/page.tsx new file mode 100644 index 00000000..e20303e4 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/bills/page.tsx @@ -0,0 +1,12 @@ +'use client'; + +import { useSearchParams } from 'next/navigation'; +import { BillManagement } from '@/components/accounting/BillManagement'; + +export default function BillsPage() { + const searchParams = useSearchParams(); + const vendorId = searchParams.get('vendorId') || undefined; + const billType = searchParams.get('type') || undefined; + + return ; +} diff --git a/src/app/[locale]/(protected)/accounting/card-transactions/page.tsx b/src/app/[locale]/(protected)/accounting/card-transactions/page.tsx new file mode 100644 index 00000000..e7f209e0 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/card-transactions/page.tsx @@ -0,0 +1,5 @@ +import { CardTransactionInquiry } from '@/components/accounting/CardTransactionInquiry'; + +export default function CardTransactionsPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/accounting/daily-report/page.tsx b/src/app/[locale]/(protected)/accounting/daily-report/page.tsx new file mode 100644 index 00000000..9e661a63 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/daily-report/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { DailyReport } from '@/components/accounting/DailyReport'; + +export default function DailyReportPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/accounting/deposits/[id]/page.tsx b/src/app/[locale]/(protected)/accounting/deposits/[id]/page.tsx new file mode 100644 index 00000000..56fd4747 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/deposits/[id]/page.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { useParams, useSearchParams } from 'next/navigation'; +import { DepositDetail } from '@/components/accounting/DepositManagement/DepositDetail'; + +export default function DepositDetailPage() { + const params = useParams(); + const searchParams = useSearchParams(); + const depositId = params.id as string; + const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view'; + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/deposits/page.tsx b/src/app/[locale]/(protected)/accounting/deposits/page.tsx new file mode 100644 index 00000000..97aed0b7 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/deposits/page.tsx @@ -0,0 +1,5 @@ +import { DepositManagement } from '@/components/accounting/DepositManagement'; + +export default function DepositsPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/expected-expenses/page.tsx b/src/app/[locale]/(protected)/accounting/expected-expenses/page.tsx new file mode 100644 index 00000000..69c02942 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/expected-expenses/page.tsx @@ -0,0 +1,5 @@ +import { ExpectedExpenseManagement } from '@/components/accounting/ExpectedExpenseManagement'; + +export default function ExpectedExpensesPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/accounting/purchase/[id]/page.tsx b/src/app/[locale]/(protected)/accounting/purchase/[id]/page.tsx new file mode 100644 index 00000000..f1d5760b --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/purchase/[id]/page.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { useParams, useSearchParams } from 'next/navigation'; +import { PurchaseDetail } from '@/components/accounting/PurchaseManagement/PurchaseDetail'; + +export default function PurchaseDetailPage() { + const params = useParams(); + const searchParams = useSearchParams(); + const purchaseId = params.id as string; + const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view'; + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/purchase/page.tsx b/src/app/[locale]/(protected)/accounting/purchase/page.tsx new file mode 100644 index 00000000..dc183a86 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/purchase/page.tsx @@ -0,0 +1,5 @@ +import { PurchaseManagement } from '@/components/accounting/PurchaseManagement'; + +export default function PurchasePage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/receivables-status/page.tsx b/src/app/[locale]/(protected)/accounting/receivables-status/page.tsx new file mode 100644 index 00000000..638d1bcf --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/receivables-status/page.tsx @@ -0,0 +1,11 @@ +'use client'; + +import { useSearchParams } from 'next/navigation'; +import { ReceivablesStatus } from '@/components/accounting/ReceivablesStatus'; + +export default function ReceivablesStatusPage() { + const searchParams = useSearchParams(); + const highlightVendorId = searchParams.get('highlight') || undefined; + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/sales/[id]/page.tsx b/src/app/[locale]/(protected)/accounting/sales/[id]/page.tsx new file mode 100644 index 00000000..fb4395fa --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/sales/[id]/page.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { useParams, useSearchParams } from 'next/navigation'; +import { SalesDetail } from '@/components/accounting/SalesManagement/SalesDetail'; + +export default function SalesDetailPage() { + const params = useParams(); + const searchParams = useSearchParams(); + const salesId = params.id as string; + const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view'; + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/sales/new/page.tsx b/src/app/[locale]/(protected)/accounting/sales/new/page.tsx new file mode 100644 index 00000000..d81bf89c --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/sales/new/page.tsx @@ -0,0 +1,5 @@ +import { SalesDetail } from '@/components/accounting/SalesManagement/SalesDetail'; + +export default function NewSalesPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/sales/page.tsx b/src/app/[locale]/(protected)/accounting/sales/page.tsx new file mode 100644 index 00000000..2607a8f8 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/sales/page.tsx @@ -0,0 +1,5 @@ +import { SalesManagement } from '@/components/accounting/SalesManagement'; + +export default function SalesPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/vendor-ledger/[id]/page.tsx b/src/app/[locale]/(protected)/accounting/vendor-ledger/[id]/page.tsx new file mode 100644 index 00000000..d40923a5 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/vendor-ledger/[id]/page.tsx @@ -0,0 +1,11 @@ +'use client'; + +import { useParams } from 'next/navigation'; +import { VendorLedgerDetail } from '@/components/accounting/VendorLedger/VendorLedgerDetail'; + +export default function VendorLedgerDetailPage() { + const params = useParams(); + const vendorId = params.id as string; + + return ; +} diff --git a/src/app/[locale]/(protected)/accounting/vendor-ledger/page.tsx b/src/app/[locale]/(protected)/accounting/vendor-ledger/page.tsx new file mode 100644 index 00000000..ae8595a1 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/vendor-ledger/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { VendorLedger } from '@/components/accounting/VendorLedger'; + +export default function VendorLedgerPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/accounting/vendors/[id]/page.tsx b/src/app/[locale]/(protected)/accounting/vendors/[id]/page.tsx new file mode 100644 index 00000000..4d4591a7 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/vendors/[id]/page.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { useParams, useSearchParams } from 'next/navigation'; +import { VendorDetail } from '@/components/accounting/VendorManagement/VendorDetail'; + +export default function VendorDetailPage() { + const params = useParams(); + const searchParams = useSearchParams(); + const vendorId = params.id as string; + const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view'; + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/vendors/new/page.tsx b/src/app/[locale]/(protected)/accounting/vendors/new/page.tsx new file mode 100644 index 00000000..3fa23088 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/vendors/new/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { VendorDetail } from '@/components/accounting/VendorManagement/VendorDetail'; + +export default function NewVendorPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/vendors/page.tsx b/src/app/[locale]/(protected)/accounting/vendors/page.tsx new file mode 100644 index 00000000..5ec37d9c --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/vendors/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { VendorManagement } from '@/components/accounting/VendorManagement'; + +export default function VendorsPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/accounting/withdrawals/[id]/page.tsx b/src/app/[locale]/(protected)/accounting/withdrawals/[id]/page.tsx new file mode 100644 index 00000000..a27b057d --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/withdrawals/[id]/page.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { useParams, useSearchParams } from 'next/navigation'; +import { WithdrawalDetail } from '@/components/accounting/WithdrawalManagement/WithdrawalDetail'; + +export default function WithdrawalDetailPage() { + const params = useParams(); + const searchParams = useSearchParams(); + const withdrawalId = params.id as string; + const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view'; + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/withdrawals/page.tsx b/src/app/[locale]/(protected)/accounting/withdrawals/page.tsx new file mode 100644 index 00000000..42509ef1 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/withdrawals/page.tsx @@ -0,0 +1,5 @@ +import { WithdrawalManagement } from '@/components/accounting/WithdrawalManagement'; + +export default function WithdrawalsPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/board/[id]/edit/page.tsx b/src/app/[locale]/(protected)/board/[id]/edit/page.tsx new file mode 100644 index 00000000..61bfc55f --- /dev/null +++ b/src/app/[locale]/(protected)/board/[id]/edit/page.tsx @@ -0,0 +1,57 @@ +'use client'; + +/** + * 게시글 수정 페이지 + */ + +import { useParams } from 'next/navigation'; +import { useMemo } from 'react'; +import { format } from 'date-fns'; +import { BoardForm } from '@/components/board/BoardForm'; +import type { Post } from '@/components/board/types'; +import { MOCK_BOARDS } from '@/components/board/types'; + +// Mock 데이터 생성 (실제로는 API에서 가져옴) +const generateMockPost = (id: string): Post => { + const boards = MOCK_BOARDS.filter((b) => b.id !== 'all'); + const board = boards[0]; + + return { + id, + boardId: board.id, + boardName: board.name, + title: '제목', + content: ` +

게시글 내용입니다.

+

이것은 테스트용 콘텐츠입니다.

+ `, + authorId: 'user1', + authorName: '홍길동', + authorDepartment: '개발팀', + authorPosition: '과장', + isPinned: false, + allowComments: true, + viewCount: 123, + attachments: [ + { + id: 'file-1', + fileName: 'abc.pdf', + fileSize: 1024000, + fileUrl: '/files/abc.pdf', + mimeType: 'application/pdf', + }, + ], + createdAt: format(new Date(2025, 8, 9, 12, 20), "yyyy-MM-dd'T'HH:mm:ss"), + updatedAt: format(new Date(2025, 8, 9, 12, 20), "yyyy-MM-dd'T'HH:mm:ss"), + }; +}; + +export default function BoardEditPage() { + const params = useParams(); + const postId = params.id as string; + + // Mock 데이터 (실제로는 API에서 가져옴) + const post = useMemo(() => generateMockPost(postId), [postId]); + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/board/[id]/page.tsx b/src/app/[locale]/(protected)/board/[id]/page.tsx new file mode 100644 index 00000000..76d0b2d0 --- /dev/null +++ b/src/app/[locale]/(protected)/board/[id]/page.tsx @@ -0,0 +1,94 @@ +'use client'; + +/** + * 게시글 상세 페이지 + */ + +import { useParams } from 'next/navigation'; +import { useMemo } from 'react'; +import { format } from 'date-fns'; +import { BoardDetail } from '@/components/board/BoardDetail'; +import type { Post, Comment } from '@/components/board/types'; +import { MOCK_BOARDS } from '@/components/board/types'; + +// 현재 로그인 사용자 ID (실제로는 auth context에서 가져옴) +const CURRENT_USER_ID = 'user1'; + +// Mock 데이터 생성 (실제로는 API에서 가져옴) +const generateMockPost = (id: string): Post => { + const boards = MOCK_BOARDS.filter((b) => b.id !== 'all'); + const board = boards[0]; + + return { + id, + boardId: board.id, + boardName: board.name, + title: '제목', + content: ` +

게시글 내용입니다.

+

이것은 테스트용 콘텐츠입니다.

+

IMG

+

내용

+ `, + authorId: 'user1', + authorName: '홍길동', + authorDepartment: '개발팀', + authorPosition: '과장', + isPinned: false, + allowComments: true, + viewCount: 123, + attachments: [ + { + id: 'file-1', + fileName: 'abc.pdf', + fileSize: 1024000, + fileUrl: '/files/abc.pdf', + mimeType: 'application/pdf', + }, + ], + createdAt: format(new Date(2025, 8, 3, 12, 23), "yyyy-MM-dd'T'HH:mm:ss"), + updatedAt: format(new Date(2025, 8, 3, 12, 23), "yyyy-MM-dd'T'HH:mm:ss"), + }; +}; + +const generateMockComments = (postId: string): Comment[] => [ + { + id: 'comment-1', + postId, + authorId: 'user2', + authorName: '이름 직책', + authorDepartment: '부서명', + authorPosition: '', + content: '댓글 내용', + createdAt: format(new Date(2025, 8, 3, 15, 33), "yyyy-MM-dd'T'HH:mm:ss"), + updatedAt: format(new Date(2025, 8, 3, 15, 33), "yyyy-MM-dd'T'HH:mm:ss"), + }, + { + id: 'comment-2', + postId, + authorId: 'user1', // 본인 댓글 + authorName: '이름 직책', + authorDepartment: '부서명', + authorPosition: '', + content: '댓글 내용', + createdAt: format(new Date(2025, 8, 3, 15, 33), "yyyy-MM-dd'T'HH:mm:ss"), + updatedAt: format(new Date(2025, 8, 3, 15, 33), "yyyy-MM-dd'T'HH:mm:ss"), + }, +]; + +export default function BoardDetailPage() { + const params = useParams(); + const postId = params.id as string; + + // Mock 데이터 (실제로는 API에서 가져옴) + const post = useMemo(() => generateMockPost(postId), [postId]); + const comments = useMemo(() => generateMockComments(postId), [postId]); + + return ( + + ); +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/board/board-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/board/board-management/[id]/edit/page.tsx new file mode 100644 index 00000000..4e8d5cf6 --- /dev/null +++ b/src/app/[locale]/(protected)/board/board-management/[id]/edit/page.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { useRouter, useParams } from 'next/navigation'; +import { useState, useEffect } from 'react'; +import { BoardForm } from '@/components/board/BoardManagement/BoardForm'; +import type { Board, BoardFormData } from '@/components/board/BoardManagement/types'; + +// TODO: 실제 API에서 데이터 가져오기 +const mockBoard: Board = { + id: '1', + target: 'all', + boardName: '공지사항', + status: 'active', + authorId: 'u1', + authorName: '홍길동', + createdAt: '2025-09-09T12:20:00Z', + updatedAt: '2025-09-09T12:20:00Z', +}; + +export default function BoardEditPage() { + const router = useRouter(); + const params = useParams(); + const [board, setBoard] = useState(null); + + useEffect(() => { + // TODO: API 연동 + // const id = params.id; + setBoard(mockBoard); + }, [params.id]); + + const handleSubmit = (data: BoardFormData) => { + // TODO: API 연동 + console.log('Update board:', params.id, data); + router.push(`/ko/board/board-management/${params.id}`); + }; + + if (!board) { + return ( +
+
+
+

로딩 중...

+
+
+ ); + } + + return ( + + ); +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/board/board-management/[id]/page.tsx b/src/app/[locale]/(protected)/board/board-management/[id]/page.tsx new file mode 100644 index 00000000..4072c0f0 --- /dev/null +++ b/src/app/[locale]/(protected)/board/board-management/[id]/page.tsx @@ -0,0 +1,100 @@ +'use client'; + +import { useRouter, useParams } from 'next/navigation'; +import { useState, useEffect } from 'react'; +import { BoardDetail } from '@/components/board/BoardManagement/BoardDetail'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import type { Board } from '@/components/board/BoardManagement/types'; + +// TODO: 실제 API에서 데이터 가져오기 +const mockBoard: Board = { + id: '1', + target: 'all', + boardName: '공지사항', + status: 'active', + authorId: 'u1', + authorName: '홍길동', + createdAt: '2025-09-09T12:20:00Z', + updatedAt: '2025-09-09T12:20:00Z', +}; + +export default function BoardDetailPage() { + const router = useRouter(); + const params = useParams(); + const [board, setBoard] = useState(null); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + + useEffect(() => { + // TODO: API 연동 + // const id = params.id; + setBoard(mockBoard); + }, [params.id]); + + const handleEdit = () => { + router.push(`/ko/board/board-management/${params.id}/edit`); + }; + + const handleDelete = () => { + setDeleteDialogOpen(true); + }; + + const confirmDelete = () => { + // TODO: API 연동 + console.log('Delete board:', params.id); + router.push('/ko/board/board-management'); + }; + + if (!board) { + return ( +
+
+
+

로딩 중...

+
+
+ ); + } + + return ( + <> + + + + + + 게시판 삭제 + + "{board.boardName}" 게시판을 삭제하시겠습니까? +
+ + 삭제된 게시판 정보는 복구할 수 없습니다. + +
+
+ + 취소 + + 삭제 + + +
+
+ + ); +} diff --git a/src/app/[locale]/(protected)/board/board-management/new/page.tsx b/src/app/[locale]/(protected)/board/board-management/new/page.tsx new file mode 100644 index 00000000..a6f40c20 --- /dev/null +++ b/src/app/[locale]/(protected)/board/board-management/new/page.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { BoardForm } from '@/components/board/BoardManagement/BoardForm'; +import type { BoardFormData } from '@/components/board/BoardManagement/types'; + +export default function BoardNewPage() { + const router = useRouter(); + + const handleSubmit = (data: BoardFormData) => { + // TODO: API 연동 + console.log('Create board:', data); + router.push('/ko/board/board-management'); + }; + + return ( + + ); +} diff --git a/src/app/[locale]/(protected)/board/board-management/page.tsx b/src/app/[locale]/(protected)/board/board-management/page.tsx new file mode 100644 index 00000000..3162bc54 --- /dev/null +++ b/src/app/[locale]/(protected)/board/board-management/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { BoardManagement } from '@/components/board/BoardManagement'; + +export default function BoardManagementPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/board/create/page.tsx b/src/app/[locale]/(protected)/board/create/page.tsx new file mode 100644 index 00000000..69e9affd --- /dev/null +++ b/src/app/[locale]/(protected)/board/create/page.tsx @@ -0,0 +1,14 @@ +/** + * 게시글 등록 페이지 + */ + +import { BoardForm } from '@/components/board/BoardForm'; + +export default function BoardCreatePage() { + return ; +} + +export const metadata = { + title: '게시글 등록', + description: '게시글을 등록하고 관리합니다.', +}; \ No newline at end of file diff --git a/src/app/[locale]/(protected)/board/page.tsx b/src/app/[locale]/(protected)/board/page.tsx new file mode 100644 index 00000000..7051dc92 --- /dev/null +++ b/src/app/[locale]/(protected)/board/page.tsx @@ -0,0 +1,14 @@ +/** + * 게시판 목록 페이지 + */ + +import { BoardList } from '@/components/board/BoardList'; + +export default function BoardPage() { + return ; +} + +export const metadata = { + title: '게시판', + description: '게시판의 게시글을 등록하고 관리합니다.', +}; \ No newline at end of file diff --git a/src/app/[locale]/(protected)/company-info/page.tsx b/src/app/[locale]/(protected)/company-info/page.tsx new file mode 100644 index 00000000..7561ae69 --- /dev/null +++ b/src/app/[locale]/(protected)/company-info/page.tsx @@ -0,0 +1,5 @@ +import { CompanyInfoManagement } from '@/components/settings/CompanyInfoManagement'; + +export default function CompanyInfoPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/customer-center/events/[id]/page.tsx b/src/app/[locale]/(protected)/customer-center/events/[id]/page.tsx new file mode 100644 index 00000000..2be37a5d --- /dev/null +++ b/src/app/[locale]/(protected)/customer-center/events/[id]/page.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { useParams } from 'next/navigation'; +import { EventDetail, MOCK_EVENTS } from '@/components/customer-center/EventManagement'; + +export default function EventDetailPage() { + const params = useParams(); + const eventId = params.id as string; + + // Mock 데이터에서 이벤트 찾기 + const event = MOCK_EVENTS.find((e) => e.id === eventId); + + if (!event) { + return ( +
+

이벤트를 찾을 수 없습니다.

+
+ ); + } + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/customer-center/events/page.tsx b/src/app/[locale]/(protected)/customer-center/events/page.tsx new file mode 100644 index 00000000..6f9c48fb --- /dev/null +++ b/src/app/[locale]/(protected)/customer-center/events/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { EventList } from '@/components/customer-center/EventManagement'; + +export default function EventsPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/customer-center/faq/page.tsx b/src/app/[locale]/(protected)/customer-center/faq/page.tsx new file mode 100644 index 00000000..f153a923 --- /dev/null +++ b/src/app/[locale]/(protected)/customer-center/faq/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { FAQList } from '@/components/customer-center/FAQManagement'; + +export default function FAQPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/customer-center/inquiries/[id]/edit/page.tsx b/src/app/[locale]/(protected)/customer-center/inquiries/[id]/edit/page.tsx new file mode 100644 index 00000000..486ba781 --- /dev/null +++ b/src/app/[locale]/(protected)/customer-center/inquiries/[id]/edit/page.tsx @@ -0,0 +1,27 @@ +'use client'; + +/** + * 1:1 문의 수정 페이지 + */ + +import { useParams } from 'next/navigation'; +import { InquiryForm } from '@/components/customer-center/InquiryManagement'; +import { MOCK_INQUIRIES } from '@/components/customer-center/InquiryManagement/types'; + +export default function InquiryEditPage() { + const params = useParams(); + const inquiryId = params.id as string; + + // Mock: 문의 데이터 조회 + const inquiry = MOCK_INQUIRIES.find((i) => i.id === inquiryId); + + if (!inquiry) { + return ( +
+

문의를 찾을 수 없습니다.

+
+ ); + } + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/customer-center/inquiries/[id]/page.tsx b/src/app/[locale]/(protected)/customer-center/inquiries/[id]/page.tsx new file mode 100644 index 00000000..e37ec55b --- /dev/null +++ b/src/app/[locale]/(protected)/customer-center/inquiries/[id]/page.tsx @@ -0,0 +1,43 @@ +'use client'; + +/** + * 1:1 문의 상세 페이지 + */ + +import { useParams } from 'next/navigation'; +import { InquiryDetail } from '@/components/customer-center/InquiryManagement'; +import { MOCK_INQUIRIES, MOCK_REPLY, MOCK_COMMENTS } from '@/components/customer-center/InquiryManagement/types'; + +export default function InquiryDetailPage() { + const params = useParams(); + const inquiryId = params.id as string; + + // Mock: 문의 데이터 조회 + const inquiry = MOCK_INQUIRIES.find((i) => i.id === inquiryId); + + if (!inquiry) { + return ( +
+

문의를 찾을 수 없습니다.

+
+ ); + } + + // Mock: 답변 데이터 (답변완료 상태일 때만) + const reply = inquiry.status === 'completed' ? MOCK_REPLY : undefined; + + // Mock: 댓글 데이터 + const comments = MOCK_COMMENTS.filter((c) => c.inquiryId === inquiryId); + + // Mock: 현재 사용자 ID + const currentUserId = 'user1'; + + return ( + + ); +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/customer-center/inquiries/create/page.tsx b/src/app/[locale]/(protected)/customer-center/inquiries/create/page.tsx new file mode 100644 index 00000000..60a1e738 --- /dev/null +++ b/src/app/[locale]/(protected)/customer-center/inquiries/create/page.tsx @@ -0,0 +1,14 @@ +/** + * 1:1 문의 등록 페이지 + */ + +import { InquiryForm } from '@/components/customer-center/InquiryManagement'; + +export default function InquiryCreatePage() { + return ; +} + +export const metadata = { + title: '1:1 문의 등록', + description: '1:1 문의를 등록합니다.', +}; \ No newline at end of file diff --git a/src/app/[locale]/(protected)/customer-center/inquiries/page.tsx b/src/app/[locale]/(protected)/customer-center/inquiries/page.tsx new file mode 100644 index 00000000..9aa356c5 --- /dev/null +++ b/src/app/[locale]/(protected)/customer-center/inquiries/page.tsx @@ -0,0 +1,14 @@ +/** + * 1:1 문의 목록 페이지 + */ + +import { InquiryList } from '@/components/customer-center/InquiryManagement'; + +export default function InquiriesPage() { + return ; +} + +export const metadata = { + title: '1:1 문의', + description: '1:1 문의를 등록하고 답변을 확인합니다.', +}; \ No newline at end of file diff --git a/src/app/[locale]/(protected)/customer-center/notices/[id]/page.tsx b/src/app/[locale]/(protected)/customer-center/notices/[id]/page.tsx new file mode 100644 index 00000000..f1cfd80b --- /dev/null +++ b/src/app/[locale]/(protected)/customer-center/notices/[id]/page.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { useParams } from 'next/navigation'; +import { NoticeDetail, MOCK_NOTICES } from '@/components/customer-center/NoticeManagement'; + +export default function NoticeDetailPage() { + const params = useParams(); + const id = params.id as string; + + // Mock 데이터에서 해당 공지사항 찾기 + const notice = MOCK_NOTICES.find((n) => n.id === id); + + if (!notice) { + return ( +
+

공지사항을 찾을 수 없습니다.

+
+ ); + } + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/customer-center/notices/page.tsx b/src/app/[locale]/(protected)/customer-center/notices/page.tsx new file mode 100644 index 00000000..3f61f5b9 --- /dev/null +++ b/src/app/[locale]/(protected)/customer-center/notices/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { NoticeList } from '@/components/customer-center/NoticeManagement'; + +export default function NoticesPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/dev/test-urls/TestUrlsClient.tsx b/src/app/[locale]/(protected)/dev/test-urls/TestUrlsClient.tsx new file mode 100644 index 00000000..b2980d27 --- /dev/null +++ b/src/app/[locale]/(protected)/dev/test-urls/TestUrlsClient.tsx @@ -0,0 +1,267 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { ExternalLink, Copy, Check, ChevronDown, ChevronRight, RefreshCw } from 'lucide-react'; + +export interface UrlItem { + name: string; + url: string; + status?: string; +} + +export interface UrlCategory { + title: string; + icon: string; + items: UrlItem[]; + subCategories?: { + title: string; + items: UrlItem[]; + }[]; +} + +interface TestUrlsClientProps { + initialData: UrlCategory[]; + lastUpdated: string; +} + +function UrlCard({ item, baseUrl }: { item: UrlItem; baseUrl: string }) { + const [copied, setCopied] = useState(false); + const fullUrl = `${baseUrl}${item.url}`; + + const handleCopy = async (e: React.MouseEvent) => { + e.stopPropagation(); + await navigator.clipboard.writeText(fullUrl); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const handleOpen = () => { + window.open(fullUrl, '_blank'); + }; + + return ( +
+
+
+ + {item.name} + + {item.status && ( + + {item.status} + + )} +
+

+ {item.url} +

+
+
+ + +
+
+ ); +} + +function CategorySection({ category, baseUrl }: { category: UrlCategory; baseUrl: string }) { + const [expanded, setExpanded] = useState(true); + const [subExpanded, setSubExpanded] = useState>({}); + + const toggleSub = (title: string) => { + setSubExpanded((prev) => ({ ...prev, [title]: !prev[title] })); + }; + + const totalItems = category.items.length + + (category.subCategories?.reduce((acc, sub) => acc + sub.items.length, 0) || 0); + + if (totalItems === 0) return null; + + return ( +
+ + + {expanded && ( +
+ {category.items.length > 0 && ( +
+ {category.items.map((item) => ( + + ))} +
+ )} + + {category.subCategories?.map((sub) => ( +
+ + {subExpanded[sub.title] !== false && ( +
+ {sub.items.map((item) => ( + + ))} +
+ )} +
+ ))} +
+ )} +
+ ); +} + +export default function TestUrlsClient({ initialData, lastUpdated }: TestUrlsClientProps) { + const [baseUrl, setBaseUrl] = useState('http://localhost:3000'); + const [searchTerm, setSearchTerm] = useState(''); + + useEffect(() => { + if (typeof window !== 'undefined') { + setBaseUrl(window.location.origin); + } + }, []); + + // 검색 필터링 + const filteredData = initialData + .map((category) => ({ + ...category, + items: category.items.filter( + (item) => + item.name.toLowerCase().includes(searchTerm.toLowerCase()) || + item.url.toLowerCase().includes(searchTerm.toLowerCase()) + ), + subCategories: category.subCategories?.map((sub) => ({ + ...sub, + items: sub.items.filter( + (item) => + item.name.toLowerCase().includes(searchTerm.toLowerCase()) || + item.url.toLowerCase().includes(searchTerm.toLowerCase()) + ), + })).filter((sub) => sub.items.length > 0), + })) + .filter( + (category) => + category.items.length > 0 || (category.subCategories && category.subCategories.length > 0) + ); + + const totalLinks = initialData.reduce( + (acc, cat) => + acc + + cat.items.length + + (cat.subCategories?.reduce((subAcc, sub) => subAcc + sub.items.length, 0) || 0), + 0 + ); + + const handleRefresh = () => { + window.location.reload(); + }; + + return ( +
+
+ {/* Header */} +
+
+

+ 🔗 테스트 URL 목록 +

+ +
+

+ 백엔드 메뉴 연동 전 테스트용 직접 접근 URL ({totalLinks}개) +

+

+ 클릭하면 새 탭에서 열립니다 • 최종 업데이트: {lastUpdated} +

+

+ ✨ md 파일 수정 시 자동 반영됩니다 +

+
+ + {/* Search & Base URL */} +
+ setSearchTerm(e.target.value)} + className="flex-1 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+ Base: + setBaseUrl(e.target.value)} + className="w-48 text-sm bg-transparent text-gray-900 dark:text-white focus:outline-none" + /> +
+
+ + {/* Categories */} +
+ {filteredData.map((category) => ( + + ))} +
+ + {filteredData.length === 0 && ( +
+ 검색 결과가 없습니다. +
+ )} + + {/* Footer */} +
+

+ 📁 데이터 소스: claudedocs/[REF] all-pages-test-urls.md +

+

+ md 파일 수정 후 새로고침하면 자동 반영! +

+
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/dev/test-urls/page.tsx b/src/app/[locale]/(protected)/dev/test-urls/page.tsx new file mode 100644 index 00000000..fbfcc7f8 --- /dev/null +++ b/src/app/[locale]/(protected)/dev/test-urls/page.tsx @@ -0,0 +1,167 @@ +import { promises as fs } from 'fs'; +import path from 'path'; +import TestUrlsClient, { UrlCategory, UrlItem } from './TestUrlsClient'; + +// 아이콘 매핑 +const iconMap: Record = { + '기본': '🏠', + '인사관리': '👥', + 'HR': '👥', + '판매관리': '💰', + 'Sales': '💰', + '기준정보관리': '📦', + 'Master Data': '📦', + '생산관리': '🏭', + 'Production': '🏭', + '설정': '⚙️', + 'Settings': '⚙️', + '전자결재': '📝', + 'Approval': '📝', + '회계관리': '💵', + 'Accounting': '💵', + '게시판': '📋', + 'Board': '📋', + '보고서': '📊', + 'Reports': '📊', +}; + +function getIcon(title: string): string { + for (const [key, icon] of Object.entries(iconMap)) { + if (title.includes(key)) return icon; + } + return '📄'; +} + +function parseTableRow(line: string): UrlItem | null { + // | 페이지 | URL | 상태 | 형식 파싱 + const parts = line.split('|').map(p => p.trim()).filter(p => p); + + if (parts.length < 2) return null; + if (parts[0] === '페이지' || parts[0].startsWith('---')) return null; + + const name = parts[0].replace(/\*\*/g, ''); // **bold** 제거 + const url = parts[1].replace(/`/g, ''); // backtick 제거 + const status = parts[2] || undefined; + + // URL이 /ko로 시작하는지 확인 + if (!url.startsWith('/ko')) return null; + + return { name, url, status }; +} + +function parseMdFile(content: string): { categories: UrlCategory[]; lastUpdated: string } { + const lines = content.split('\n'); + const categories: UrlCategory[] = []; + let currentCategory: UrlCategory | null = null; + let currentSubCategory: { title: string; items: UrlItem[] } | null = null; + let lastUpdated = 'N/A'; + + // Last Updated 추출 + const updateMatch = content.match(/Last Updated:\s*(\d{4}-\d{2}-\d{2})/); + if (updateMatch) { + lastUpdated = updateMatch[1]; + } + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + // ## 카테고리 (메인 섹션) + if (line.startsWith('## ') && !line.includes('클릭 가능한') && !line.includes('전체 URL') && !line.includes('백엔드 메뉴')) { + // 이전 카테고리 저장 + if (currentCategory) { + if (currentSubCategory) { + currentCategory.subCategories = currentCategory.subCategories || []; + currentCategory.subCategories.push(currentSubCategory); + currentSubCategory = null; + } + categories.push(currentCategory); + } + + const title = line.replace('## ', '').replace(/[🏠👥💰📦🏭⚙️📝📋💵]/g, '').trim(); + currentCategory = { + title, + icon: getIcon(title), + items: [], + subCategories: [], + }; + currentSubCategory = null; + } + + // ### 서브 카테고리 + else if (line.startsWith('### ') && currentCategory) { + // 이전 서브카테고리 저장 + if (currentSubCategory) { + currentCategory.subCategories = currentCategory.subCategories || []; + currentCategory.subCategories.push(currentSubCategory); + } + + const subTitle = line.replace('### ', '').trim(); + // "메인 페이지"는 서브카테고리가 아니라 메인 아이템으로 + if (subTitle === '메인 페이지') { + currentSubCategory = null; + } else { + currentSubCategory = { + title: subTitle, + items: [], + }; + } + } + + // 테이블 행 파싱 + else if (line.startsWith('|') && currentCategory) { + const item = parseTableRow(line); + if (item) { + if (currentSubCategory) { + currentSubCategory.items.push(item); + } else { + currentCategory.items.push(item); + } + } + } + } + + // 마지막 카테고리 저장 + if (currentCategory) { + if (currentSubCategory) { + currentCategory.subCategories = currentCategory.subCategories || []; + currentCategory.subCategories.push(currentSubCategory); + } + categories.push(currentCategory); + } + + // 빈 서브카테고리 제거 + categories.forEach(cat => { + cat.subCategories = cat.subCategories?.filter(sub => sub.items.length > 0); + }); + + return { categories, lastUpdated }; +} + +export default async function TestUrlsPage() { + // md 파일 경로 + const mdFilePath = path.join( + process.cwd(), + 'claudedocs', + '[REF] all-pages-test-urls.md' + ); + + let urlData: UrlCategory[] = []; + let lastUpdated = 'N/A'; + + try { + const fileContent = await fs.readFile(mdFilePath, 'utf-8'); + const parsed = parseMdFile(fileContent); + urlData = parsed.categories; + lastUpdated = parsed.lastUpdated; + } catch (error) { + console.error('Failed to read md file:', error); + // 파일 읽기 실패 시 빈 데이터 + urlData = []; + } + + return ; +} + +// 캐싱 비활성화 - 항상 최신 md 파일 읽기 +export const dynamic = 'force-dynamic'; +export const revalidate = 0; \ No newline at end of file diff --git a/src/app/[locale]/(protected)/hr/attendance/page.tsx b/src/app/[locale]/(protected)/hr/attendance/page.tsx new file mode 100644 index 00000000..df406ad2 --- /dev/null +++ b/src/app/[locale]/(protected)/hr/attendance/page.tsx @@ -0,0 +1,295 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { MapPin } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import GoogleMap from '@/components/attendance/GoogleMap'; +import AttendanceComplete from '@/components/attendance/AttendanceComplete'; + +// ======================================== +// 하드코딩 설정값 (MVP - 추후 API로 대체) +// ======================================== +// TODO: 이 값들은 출퇴근관리 설정 페이지에서 관리됨 +// 설정 페이지 경로: /settings/attendance-settings +// API 연동 시: GET /api/settings/attendance 에서 조회 +// ──────────────────────────────────────── +// - radius: 출퇴근관리 설정의 allowedRadius 값 사용 +// - gpsDepartments: 로그인 사용자의 부서가 포함되어 있는지 체크 +// - gpsEnabled: false면 GPS 출퇴근 기능 비활성화 +// ──────────────────────────────────────── +const SITE_LOCATION = { + name: '본사', + lat: 37.557358, + lng: 126.864414, + radius: 100, // meters → 출퇴근관리 설정의 allowedRadius 값으로 대체 예정 +}; + +const TEST_USER = { + name: '홍길동', + department: '부서명', + position: '직급명', +}; + +// 출퇴근 상태 타입 +type AttendanceStatus = 'not-checked-in' | 'checked-in' | 'checked-out'; +type ViewMode = 'main' | 'check-in-complete' | 'check-out-complete'; + +export default function MobileAttendancePage() { + // Hydration 에러 방지: 클라이언트 마운트 상태 + const [mounted, setMounted] = useState(false); + + // 상태 관리 + const [currentTime, setCurrentTime] = useState('--:--:--'); + const [currentDate, setCurrentDate] = useState(''); + const [distance, setDistance] = useState(null); + const [isInRange, setIsInRange] = useState(false); + const [attendanceStatus, setAttendanceStatus] = useState('not-checked-in'); + const [viewMode, setViewMode] = useState('main'); + const [checkInTime, setCheckInTime] = useState(''); + const [checkOutTime, setCheckOutTime] = useState(''); + const [userName, setUserName] = useState(TEST_USER.name); + const [userDepartment, setUserDepartment] = useState(TEST_USER.department); + const [userPosition, setUserPosition] = useState(TEST_USER.position); + + // 클라이언트 마운트 확인 + useEffect(() => { + setMounted(true); + }, []); + + // 현재 시간 업데이트 (마운트 후에만 실행) + useEffect(() => { + if (!mounted) return; + + const updateTime = () => { + const now = new Date(); + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + const seconds = String(now.getSeconds()).padStart(2, '0'); + setCurrentTime(`${hours}:${minutes}:${seconds}`); + + // 날짜 포맷 + const year = now.getFullYear(); + const month = now.getMonth() + 1; + const date = now.getDate(); + const dayNames = ['일', '월', '화', '수', '목', '금', '토']; + const day = dayNames[now.getDay()]; + setCurrentDate(`${year}년 ${month}월 ${date}일 (${day})`); + }; + + updateTime(); + const interval = setInterval(updateTime, 1000); + return () => clearInterval(interval); + }, [mounted]); + + // localStorage에서 사용자 정보 가져오기 (마운트 후에만 실행) + useEffect(() => { + if (!mounted) return; + + const userDataStr = localStorage.getItem('user'); + if (userDataStr) { + try { + const userData = JSON.parse(userDataStr); + if (userData.name) setUserName(userData.name); + if (userData.department) setUserDepartment(userData.department); + if (userData.position) setUserPosition(userData.position); + } catch (e) { + console.error('사용자 정보 파싱 실패:', e); + } + } + }, [mounted]); + + // 거리 변경 콜백 + const handleDistanceChange = useCallback((dist: number, inRange: boolean) => { + setDistance(dist); + setIsInRange(inRange); + }, []); + + // 출근하기 + const handleCheckIn = () => { + if (!isInRange) return; + + const now = new Date(); + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + const seconds = String(now.getSeconds()).padStart(2, '0'); + const timeStr = `${hours}:${minutes}:${seconds}`; + + setCheckInTime(timeStr); + setAttendanceStatus('checked-in'); + setViewMode('check-in-complete'); + + // TODO: API 호출로 출근 기록 저장 + console.log('[출근 기록]', { + time: timeStr, + location: SITE_LOCATION.name, + coordinates: { lat: SITE_LOCATION.lat, lng: SITE_LOCATION.lng }, + }); + }; + + // 퇴근하기 + const handleCheckOut = () => { + if (!isInRange) return; + + const now = new Date(); + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + const seconds = String(now.getSeconds()).padStart(2, '0'); + const timeStr = `${hours}:${minutes}:${seconds}`; + + setCheckOutTime(timeStr); + setAttendanceStatus('checked-out'); + setViewMode('check-out-complete'); + + // TODO: API 호출로 퇴근 기록 저장 + console.log('[퇴근 기록]', { + time: timeStr, + location: SITE_LOCATION.name, + coordinates: { lat: SITE_LOCATION.lat, lng: SITE_LOCATION.lng }, + }); + }; + + // 완료 화면에서 확인 클릭 + const handleConfirm = () => { + setViewMode('main'); + }; + + // 마운트 전 로딩 UI (Hydration 에러 방지) + if (!mounted) { + return ( +
+
+

출퇴근하기

+
+
+
+
+

로딩 중...

+
+
+
+ ); + } + + // 완료 화면 렌더링 + if (viewMode === 'check-in-complete') { + return ( +
+ +
+ ); + } + + if (viewMode === 'check-out-complete') { + return ( +
+ +
+ ); + } + + // 버튼 활성화 상태 + const canCheckIn = isInRange && attendanceStatus === 'not-checked-in'; + const canCheckOut = isInRange && attendanceStatus === 'checked-in'; + + return ( +
+ {/* 타이틀 */} +
+

출퇴근하기

+
+ + {/* 지도 영역 */} +
+ + + {/* 거리 표시 오버레이 */} + {distance !== null && ( +
+
+ + + {distance < 1000 + ? `${Math.round(distance)}m` + : `${(distance / 1000).toFixed(1)}km`} + {isInRange && ' (범위 내)'} + +
+
+ )} +
+ + {/* 사용자 정보 + 시간 + 버튼 */} +
+ {/* 사용자 정보 */} +
+
+ {userName.charAt(0)} +
+
+

{userName}

+

+ {userDepartment} {userPosition} +

+
+
+ + {/* 현재 시간 */} +
+

{currentTime}

+

{currentDate}

+ {attendanceStatus === 'checked-in' && ( +

출근중

+ )} +
+ + {/* 출근/퇴근 버튼 */} +
+ + +
+ + {/* 범위 밖 경고 */} + {!isInRange && distance !== null && ( +

+ 출퇴근 가능 범위({SITE_LOCATION.radius}m) 밖에 있습니다. +

+ )} +
+
+ ); +} diff --git a/src/app/[locale]/(protected)/hr/card-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/hr/card-management/[id]/edit/page.tsx new file mode 100644 index 00000000..a3e871c1 --- /dev/null +++ b/src/app/[locale]/(protected)/hr/card-management/[id]/edit/page.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { useRouter, useParams } from 'next/navigation'; +import { useState, useEffect } from 'react'; +import { CardForm } from '@/components/hr/CardManagement/CardForm'; +import type { Card, CardFormData } from '@/components/hr/CardManagement/types'; + +// TODO: 실제 API에서 데이터 가져오기 +const mockCard: Card = { + id: '1', + cardCompany: 'shinhan', + cardNumber: '1234-1234-1234-1234', + cardName: '법인카드1', + expiryDate: '0327', + pinPrefix: '12', + status: 'active', + user: { + id: 'u1', + departmentId: 'd1', + departmentName: '부서명', + employeeId: 'e1', + employeeName: '홍길동', + positionId: 'p1', + positionName: '팀장', + }, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', +}; + +export default function CardEditPage() { + const router = useRouter(); + const params = useParams(); + const [card, setCard] = useState(null); + + useEffect(() => { + // TODO: API 연동 + // const id = params.id; + setCard(mockCard); + }, [params.id]); + + const handleSubmit = (data: CardFormData) => { + // TODO: API 연동 + console.log('Update card:', params.id, data); + router.push(`/ko/hr/card-management/${params.id}`); + }; + + if (!card) { + return ( +
+
+
+

로딩 중...

+
+
+ ); + } + + return ( + + ); +} diff --git a/src/app/[locale]/(protected)/hr/card-management/[id]/page.tsx b/src/app/[locale]/(protected)/hr/card-management/[id]/page.tsx new file mode 100644 index 00000000..018d9be6 --- /dev/null +++ b/src/app/[locale]/(protected)/hr/card-management/[id]/page.tsx @@ -0,0 +1,110 @@ +'use client'; + +import { useRouter, useParams } from 'next/navigation'; +import { useState, useEffect } from 'react'; +import { CardDetail } from '@/components/hr/CardManagement/CardDetail'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import type { Card } from '@/components/hr/CardManagement/types'; + +// TODO: 실제 API에서 데이터 가져오기 +const mockCard: Card = { + id: '1', + cardCompany: 'shinhan', + cardNumber: '1234-1234-1234-1234', + cardName: '법인카드1', + expiryDate: '0327', + pinPrefix: '12', + status: 'active', + user: { + id: 'u1', + departmentId: 'd1', + departmentName: '부서명', + employeeId: 'e1', + employeeName: '홍길동', + positionId: 'p1', + positionName: '팀장', + }, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', +}; + +export default function CardDetailPage() { + const router = useRouter(); + const params = useParams(); + const [card, setCard] = useState(null); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + + useEffect(() => { + // TODO: API 연동 + // const id = params.id; + setCard(mockCard); + }, [params.id]); + + const handleEdit = () => { + router.push(`/ko/hr/card-management/${params.id}/edit`); + }; + + const handleDelete = () => { + setDeleteDialogOpen(true); + }; + + const confirmDelete = () => { + // TODO: API 연동 + console.log('Delete card:', params.id); + router.push('/ko/hr/card-management'); + }; + + if (!card) { + return ( +
+
+
+

로딩 중...

+
+
+ ); + } + + return ( + <> + + + + + + 카드 삭제 + + "{card.cardName}" 카드를 삭제하시겠습니까? +
+ + 삭제된 카드 정보는 복구할 수 없습니다. + +
+
+ + 취소 + + 삭제 + + +
+
+ + ); +} diff --git a/src/app/[locale]/(protected)/hr/card-management/new/page.tsx b/src/app/[locale]/(protected)/hr/card-management/new/page.tsx new file mode 100644 index 00000000..b4a034e6 --- /dev/null +++ b/src/app/[locale]/(protected)/hr/card-management/new/page.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { CardForm } from '@/components/hr/CardManagement/CardForm'; +import type { CardFormData } from '@/components/hr/CardManagement/types'; + +export default function CardNewPage() { + const router = useRouter(); + + const handleSubmit = (data: CardFormData) => { + // TODO: API 연동 + console.log('Create card:', data); + router.push('/ko/hr/card-management'); + }; + + return ( + + ); +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/hr/card-management/page.tsx b/src/app/[locale]/(protected)/hr/card-management/page.tsx new file mode 100644 index 00000000..288bf67d --- /dev/null +++ b/src/app/[locale]/(protected)/hr/card-management/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { CardManagement } from '@/components/hr/CardManagement'; + +export default function CardManagementPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/hr/employee-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/hr/employee-management/[id]/edit/page.tsx index 9309802a..e85aa486 100644 --- a/src/app/[locale]/(protected)/hr/employee-management/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/hr/employee-management/[id]/edit/page.tsx @@ -31,6 +31,10 @@ const mockEmployee: Employee = { departmentPositions: [ { id: '1', departmentId: 'd1', departmentName: '개발본부', positionId: 'p1', positionName: '팀장' } ], + clockInLocation: 'headquarters', + clockOutLocation: 'headquarters', + concurrentPosition: '', + concurrentReason: '', userInfo: { userId: 'kimcs', role: 'manager', accountStatus: 'active' }, createdAt: '2020-03-15T00:00:00Z', updatedAt: '2024-01-15T00:00:00Z', diff --git a/src/app/[locale]/(protected)/hr/employee-management/[id]/page.tsx b/src/app/[locale]/(protected)/hr/employee-management/[id]/page.tsx index 6c5ec002..6262ba9a 100644 --- a/src/app/[locale]/(protected)/hr/employee-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/hr/employee-management/[id]/page.tsx @@ -41,6 +41,10 @@ const mockEmployee: Employee = { departmentPositions: [ { id: '1', departmentId: 'd1', departmentName: '개발본부', positionId: 'p1', positionName: '팀장' } ], + clockInLocation: 'headquarters', + clockOutLocation: 'headquarters', + concurrentPosition: '', + concurrentReason: '', userInfo: { userId: 'kimcs', role: 'manager', accountStatus: 'active' }, createdAt: '2020-03-15T00:00:00Z', updatedAt: '2024-01-15T00:00:00Z', diff --git a/src/app/[locale]/(protected)/items/[id]/edit/page.tsx b/src/app/[locale]/(protected)/items/[id]/edit/page.tsx index 4ce9748c..0dfbbdb2 100644 --- a/src/app/[locale]/(protected)/items/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/items/[id]/edit/page.tsx @@ -430,7 +430,7 @@ export default function EditItemPage() { } return ( -
+
+
); diff --git a/src/app/[locale]/(protected)/items/create/page.tsx b/src/app/[locale]/(protected)/items/create/page.tsx index 50a9db3e..a14c0810 100644 --- a/src/app/[locale]/(protected)/items/create/page.tsx +++ b/src/app/[locale]/(protected)/items/create/page.tsx @@ -86,7 +86,7 @@ export default function CreateItemPage() { }; return ( -
+
{submitError && (
⚠️ {submitError} diff --git a/src/app/[locale]/(protected)/items/page.tsx b/src/app/[locale]/(protected)/items/page.tsx index 6cc29fe5..d0ffef41 100644 --- a/src/app/[locale]/(protected)/items/page.tsx +++ b/src/app/[locale]/(protected)/items/page.tsx @@ -12,11 +12,7 @@ import ItemListClient from '@/components/items/ItemListClient'; * 품목 목록 페이지 */ export default function ItemsPage() { - return ( -
- -
- ); + return ; } /** diff --git a/src/app/[locale]/(protected)/payment-history/page.tsx b/src/app/[locale]/(protected)/payment-history/page.tsx new file mode 100644 index 00000000..e5149a55 --- /dev/null +++ b/src/app/[locale]/(protected)/payment-history/page.tsx @@ -0,0 +1,5 @@ +import { PaymentHistoryManagement } from '@/components/settings/PaymentHistoryManagement'; + +export default function PaymentHistoryPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/production/screen-production/[id]/edit/page.tsx b/src/app/[locale]/(protected)/production/screen-production/[id]/edit/page.tsx index a9508292..6d654e8f 100644 --- a/src/app/[locale]/(protected)/production/screen-production/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/production/screen-production/[id]/edit/page.tsx @@ -190,7 +190,7 @@ export default function EditItemPage() { if (isLoading) { return ( -
+
로딩 중...
); @@ -201,7 +201,7 @@ export default function EditItemPage() { } return ( -
+
); diff --git a/src/app/[locale]/(protected)/production/screen-production/[id]/page.tsx b/src/app/[locale]/(protected)/production/screen-production/[id]/page.tsx index 58ad4e3e..74929ac1 100644 --- a/src/app/[locale]/(protected)/production/screen-production/[id]/page.tsx +++ b/src/app/[locale]/(protected)/production/screen-production/[id]/page.tsx @@ -163,7 +163,7 @@ export default async function ItemDetailPage({ } return ( -
+
로딩 중...
}> diff --git a/src/app/[locale]/(protected)/production/screen-production/create/page.tsx b/src/app/[locale]/(protected)/production/screen-production/create/page.tsx index 021444de..96919eb1 100644 --- a/src/app/[locale]/(protected)/production/screen-production/create/page.tsx +++ b/src/app/[locale]/(protected)/production/screen-production/create/page.tsx @@ -21,7 +21,7 @@ export default function CreateItemPage() { }; return ( -
+
); diff --git a/src/app/[locale]/(protected)/production/screen-production/page.tsx b/src/app/[locale]/(protected)/production/screen-production/page.tsx index 79ac01ab..5dcf0340 100644 --- a/src/app/[locale]/(protected)/production/screen-production/page.tsx +++ b/src/app/[locale]/(protected)/production/screen-production/page.tsx @@ -145,11 +145,9 @@ export default async function ItemsPage() { const items = await getItems(); return ( -
- 로딩 중...
}> - - -
+ 로딩 중...
}> + + ); } diff --git a/src/app/[locale]/(protected)/reports/comprehensive-analysis/page.tsx b/src/app/[locale]/(protected)/reports/comprehensive-analysis/page.tsx new file mode 100644 index 00000000..6b4339f1 --- /dev/null +++ b/src/app/[locale]/(protected)/reports/comprehensive-analysis/page.tsx @@ -0,0 +1,5 @@ +import ComprehensiveAnalysis from '@/components/reports/ComprehensiveAnalysis'; + +export default function ComprehensiveAnalysisPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/reports/page.tsx b/src/app/[locale]/(protected)/reports/page.tsx new file mode 100644 index 00000000..1efad11f --- /dev/null +++ b/src/app/[locale]/(protected)/reports/page.tsx @@ -0,0 +1,6 @@ +import { redirect } from 'next/navigation'; + +export default function ReportsPage() { + // 보고서 및 분석 메인 → 종합 경영 분석으로 리다이렉트 + redirect('/ko/reports/comprehensive-analysis'); +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/sales/pricing-management/page.tsx b/src/app/[locale]/(protected)/sales/pricing-management/page.tsx index 749f3544..edecd877 100644 --- a/src/app/[locale]/(protected)/sales/pricing-management/page.tsx +++ b/src/app/[locale]/(protected)/sales/pricing-management/page.tsx @@ -140,6 +140,12 @@ function mapStatus(apiStatus: string, isFinal: boolean): PricingStatus | 'not_re } } +// 품목 유형 → API item_type_code 매핑 (백엔드 pricing API용) +function mapItemTypeCode(itemType?: string): 'PRODUCT' | 'MATERIAL' { + // FG(제품)만 PRODUCT, 나머지는 모두 MATERIAL + return itemType === 'FG' ? 'PRODUCT' : 'MATERIAL'; +} + // ============================================ // API 호출 함수 // ============================================ diff --git a/src/app/[locale]/(protected)/settings/account-info/page.tsx b/src/app/[locale]/(protected)/settings/account-info/page.tsx new file mode 100644 index 00000000..ae8d8611 --- /dev/null +++ b/src/app/[locale]/(protected)/settings/account-info/page.tsx @@ -0,0 +1,5 @@ +import { AccountInfoClient } from '@/components/settings/AccountInfoManagement'; + +export default function AccountInfoPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/settings/accounts/[id]/page.tsx b/src/app/[locale]/(protected)/settings/accounts/[id]/page.tsx new file mode 100644 index 00000000..f5964262 --- /dev/null +++ b/src/app/[locale]/(protected)/settings/accounts/[id]/page.tsx @@ -0,0 +1,129 @@ +'use client'; + +import { useParams } from 'next/navigation'; +import { AccountDetail } from '@/components/settings/AccountManagement/AccountDetail'; +import type { Account } from '@/components/settings/AccountManagement/types'; + +// Mock 데이터 (API 연동 전 임시) +const mockAccounts: Account[] = [ + { + id: 'account-1', + bankCode: 'shinhan', + accountNumber: '1234-1234-1234-1234', + accountName: '운영계좌 1', + accountHolder: '예금주1', + status: 'active', + createdAt: '2025-12-19T00:00:00.000Z', + updatedAt: '2025-12-19T00:00:00.000Z', + }, + { + id: 'account-2', + bankCode: 'kb', + accountNumber: '1234-1234-1234-1235', + accountName: '운영계좌 2', + accountHolder: '예금주2', + status: 'inactive', + createdAt: '2025-12-19T00:00:00.000Z', + updatedAt: '2025-12-19T00:00:00.000Z', + }, + { + id: 'account-3', + bankCode: 'woori', + accountNumber: '1234-1234-1234-1236', + accountName: '운영계좌 3', + accountHolder: '예금주3', + status: 'active', + createdAt: '2025-12-19T00:00:00.000Z', + updatedAt: '2025-12-19T00:00:00.000Z', + }, + { + id: 'account-4', + bankCode: 'hana', + accountNumber: '1234-1234-1234-1237', + accountName: '운영계좌 4', + accountHolder: '예금주4', + status: 'inactive', + createdAt: '2025-12-19T00:00:00.000Z', + updatedAt: '2025-12-19T00:00:00.000Z', + }, + { + id: 'account-5', + bankCode: 'nh', + accountNumber: '1234-1234-1234-1238', + accountName: '운영계좌 5', + accountHolder: '예금주5', + status: 'active', + createdAt: '2025-12-19T00:00:00.000Z', + updatedAt: '2025-12-19T00:00:00.000Z', + }, + { + id: 'account-6', + bankCode: 'ibk', + accountNumber: '1234-1234-1234-1239', + accountName: '운영계좌 6', + accountHolder: '예금주6', + status: 'inactive', + createdAt: '2025-12-19T00:00:00.000Z', + updatedAt: '2025-12-19T00:00:00.000Z', + }, + { + id: 'account-7', + bankCode: 'shinhan', + accountNumber: '1234-1234-1234-1240', + accountName: '운영계좌 7', + accountHolder: '예금주7', + status: 'active', + createdAt: '2025-12-19T00:00:00.000Z', + updatedAt: '2025-12-19T00:00:00.000Z', + }, + { + id: 'account-8', + bankCode: 'kb', + accountNumber: '1234-1234-1234-1241', + accountName: '운영계좌 8', + accountHolder: '예금주8', + status: 'inactive', + createdAt: '2025-12-19T00:00:00.000Z', + updatedAt: '2025-12-19T00:00:00.000Z', + }, + { + id: 'account-9', + bankCode: 'woori', + accountNumber: '1234-1234-1234-1242', + accountName: '운영계좌 9', + accountHolder: '예금주9', + status: 'active', + createdAt: '2025-12-19T00:00:00.000Z', + updatedAt: '2025-12-19T00:00:00.000Z', + }, + { + id: 'account-10', + bankCode: 'hana', + accountNumber: '1234-1234-1234-1243', + accountName: '운영계좌 10', + accountHolder: '예금주10', + status: 'inactive', + createdAt: '2025-12-19T00:00:00.000Z', + updatedAt: '2025-12-19T00:00:00.000Z', + }, +]; + +export default function AccountDetailPage() { + const params = useParams(); + const accountId = params.id as string; + + // Mock: 계좌 조회 + const account = mockAccounts.find(a => a.id === accountId); + + if (!account) { + return ( +
+
+ 계좌를 찾을 수 없습니다. +
+
+ ); + } + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/settings/accounts/new/page.tsx b/src/app/[locale]/(protected)/settings/accounts/new/page.tsx new file mode 100644 index 00000000..c04efd18 --- /dev/null +++ b/src/app/[locale]/(protected)/settings/accounts/new/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { AccountDetail } from '@/components/settings/AccountManagement/AccountDetail'; + +export default function NewAccountPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/settings/accounts/page.tsx b/src/app/[locale]/(protected)/settings/accounts/page.tsx new file mode 100644 index 00000000..561bf564 --- /dev/null +++ b/src/app/[locale]/(protected)/settings/accounts/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { AccountManagement } from '@/components/settings/AccountManagement'; + +export default function AccountsPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/settings/attendance-settings/page.tsx b/src/app/[locale]/(protected)/settings/attendance-settings/page.tsx new file mode 100644 index 00000000..699fb874 --- /dev/null +++ b/src/app/[locale]/(protected)/settings/attendance-settings/page.tsx @@ -0,0 +1,5 @@ +import { AttendanceSettingsManagement } from '@/components/settings/AttendanceSettingsManagement'; + +export default function AttendanceSettingsPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/settings/notification-settings/page.tsx b/src/app/[locale]/(protected)/settings/notification-settings/page.tsx new file mode 100644 index 00000000..30c04e40 --- /dev/null +++ b/src/app/[locale]/(protected)/settings/notification-settings/page.tsx @@ -0,0 +1,5 @@ +import { NotificationSettingsManagement } from '@/components/settings/NotificationSettings'; + +export default function NotificationSettingsPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/settings/popup-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/settings/popup-management/[id]/edit/page.tsx new file mode 100644 index 00000000..93e0fc74 --- /dev/null +++ b/src/app/[locale]/(protected)/settings/popup-management/[id]/edit/page.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { useParams } from 'next/navigation'; +import { PopupForm } from '@/components/settings/PopupManagement'; +import { MOCK_POPUPS } from '@/components/settings/PopupManagement/types'; + +export default function PopupEditPage() { + const params = useParams(); + const id = params.id as string; + + // Mock: ID로 팝업 데이터 조회 + const popup = MOCK_POPUPS.find((p) => p.id === id); + + if (!popup) { + return ( +
+

팝업을 찾을 수 없습니다.

+
+ ); + } + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/settings/popup-management/[id]/page.tsx b/src/app/[locale]/(protected)/settings/popup-management/[id]/page.tsx new file mode 100644 index 00000000..682c414c --- /dev/null +++ b/src/app/[locale]/(protected)/settings/popup-management/[id]/page.tsx @@ -0,0 +1,89 @@ +'use client'; + +import { useRouter, useParams } from 'next/navigation'; +import { useState, useEffect } from 'react'; +import { PopupDetail } from '@/components/settings/PopupManagement'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { MOCK_POPUPS, type Popup } from '@/components/settings/PopupManagement/types'; + +export default function PopupDetailPage() { + const router = useRouter(); + const params = useParams(); + const [popup, setPopup] = useState(null); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + + useEffect(() => { + // TODO: API 연동 + const id = params.id as string; + const found = MOCK_POPUPS.find((p) => p.id === id); + setPopup(found || null); + }, [params.id]); + + const handleEdit = () => { + router.push(`/ko/settings/popup-management/${params.id}/edit`); + }; + + const handleDelete = () => { + setDeleteDialogOpen(true); + }; + + const confirmDelete = () => { + // TODO: API 연동 + console.log('Delete popup:', params.id); + router.push('/ko/settings/popup-management'); + }; + + if (!popup) { + return ( +
+
+
+

로딩 중...

+
+
+ ); + } + + return ( + <> + + + + + + 팝업 삭제 + + "{popup.title}" 팝업을 삭제하시겠습니까? +
+ + 삭제된 팝업 정보는 복구할 수 없습니다. + +
+
+ + 취소 + + 삭제 + + +
+
+ + ); +} diff --git a/src/app/[locale]/(protected)/settings/popup-management/new/page.tsx b/src/app/[locale]/(protected)/settings/popup-management/new/page.tsx new file mode 100644 index 00000000..ab40d806 --- /dev/null +++ b/src/app/[locale]/(protected)/settings/popup-management/new/page.tsx @@ -0,0 +1,5 @@ +import { PopupForm } from '@/components/settings/PopupManagement'; + +export default function PopupCreatePage() { + return ; +} diff --git a/src/app/[locale]/(protected)/settings/popup-management/page.tsx b/src/app/[locale]/(protected)/settings/popup-management/page.tsx new file mode 100644 index 00000000..a8759985 --- /dev/null +++ b/src/app/[locale]/(protected)/settings/popup-management/page.tsx @@ -0,0 +1,5 @@ +import { PopupList } from '@/components/settings/PopupManagement'; + +export default function PopupManagementPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/subscription/page.tsx b/src/app/[locale]/(protected)/subscription/page.tsx new file mode 100644 index 00000000..d7d4009c --- /dev/null +++ b/src/app/[locale]/(protected)/subscription/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { SubscriptionManagement } from '@/components/settings/SubscriptionManagement'; + +export default function SubscriptionPage() { + return ; +} \ No newline at end of file diff --git a/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx b/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx new file mode 100644 index 00000000..fcd134d4 --- /dev/null +++ b/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx @@ -0,0 +1,944 @@ +'use client'; + +import { useState, useCallback, useMemo } from 'react'; +import { useRouter } from 'next/navigation'; +import { format } from 'date-fns'; +import { AlertTriangle, Plus, X, FileText, Receipt, CreditCard, Upload, Download, Trash2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Switch } from '@/components/ui/switch'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { PageLayout } from '@/components/organisms/PageLayout'; +import { PageHeader } from '@/components/organisms/PageHeader'; +import type { + BadDebtRecord, + BadDebtMemo, + Manager, + AttachedFile, + CollectionStatus, +} from './types'; +import { + STATUS_SELECT_OPTIONS, + VENDOR_TYPE_LABELS, +} from './types'; + +interface BadDebtDetailProps { + mode: 'view' | 'edit' | 'new'; + recordId?: string; +} + +// Mock 담당자 목록 +const MANAGER_OPTIONS: Manager[] = [ + { id: 'm1', departmentName: '경영지원팀', name: '홍길동', position: '과장', phone: '010-1234-1234' }, + { id: 'm2', departmentName: '재무팀', name: '김철수', position: '대리', phone: '010-2345-2345' }, + { id: 'm3', departmentName: '영업팀', name: '이영희', position: '차장', phone: '010-3456-3456' }, + { id: 'm4', departmentName: '관리팀', name: '박민수', position: '사원', phone: '010-4567-4567' }, +]; + +// Mock 데이터 가져오기 +const getMockRecord = (id: string): BadDebtRecord => ({ + id, + vendorId: 'v1', + vendorCode: '1234', + vendorName: '회사명', + businessNumber: '123-12-12345', + representativeName: '대표자명', + vendorType: 'both', + businessType: '업태명', + businessCategory: '업종명', + zipCode: '', + address1: '123 서울특별시 서초구 서초대로 123', + address2: '대한건물 12층 1201호', + phone: '02-1234-1234', + mobile: '010-1234-1234', + fax: '02-1234-1235', + email: 'abc@email.com', + contactName: '담당자명', + contactPhone: '010-1234-1234', + systemManager: '관리자명', + debtAmount: 11000000, + status: 'collecting', + overdueDays: 100, + overdueToggle: true, + occurrenceDate: '2025-12-12', + endDate: null, + assignedManagerId: 'm1', + assignedManager: MANAGER_OPTIONS[0], + settingToggle: true, + files: [ + { id: 'f1', name: 'abc.pdf', url: '#', type: 'businessRegistration' }, + { id: 'f2', name: 'abc.pdf', url: '#', type: 'taxInvoice' }, + ], + memos: [ + { + id: 'memo-1', + content: '2025-12-12 12:21 [홍길동] 메모 내용', + createdAt: '2025-12-12T12:21:00.000Z', + createdBy: '홍길동', + }, + ], + createdAt: '2025-12-01T00:00:00.000Z', + updatedAt: '2025-12-18T00:00:00.000Z', +}); + +const getEmptyRecord = (): Omit => ({ + vendorId: '', + vendorCode: '', + vendorName: '', + businessNumber: '', + representativeName: '', + vendorType: 'both', + businessType: '', + businessCategory: '', + zipCode: '', + address1: '', + address2: '', + phone: '', + mobile: '', + fax: '', + email: '', + contactName: '', + contactPhone: '', + systemManager: '', + debtAmount: 0, + status: 'collecting', + overdueDays: 0, + overdueToggle: false, + occurrenceDate: format(new Date(), 'yyyy-MM-dd'), + endDate: null, + assignedManagerId: null, + assignedManager: null, + settingToggle: true, + files: [], + memos: [], +}); + +export function BadDebtDetail({ mode, recordId }: BadDebtDetailProps) { + const router = useRouter(); + const isViewMode = mode === 'view'; + const isNewMode = mode === 'new'; + + // 폼 데이터 + const initialData = recordId ? getMockRecord(recordId) : getEmptyRecord(); + const [formData, setFormData] = useState(initialData); + + // 다이얼로그 상태 + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [showSaveDialog, setShowSaveDialog] = useState(false); + + // 새 메모 입력 + const [newMemo, setNewMemo] = useState(''); + + // 파일 업로드 상태 + const [newBusinessRegistrationFile, setNewBusinessRegistrationFile] = useState(null); + const [newTaxInvoiceFile, setNewTaxInvoiceFile] = useState(null); + const [newAdditionalFiles, setNewAdditionalFiles] = useState([]); + + // 필드 변경 핸들러 + const handleChange = useCallback((field: string, value: string | number | boolean | null) => { + setFormData(prev => ({ ...prev, [field]: value })); + }, []); + + // 네비게이션 핸들러 + const handleBack = useCallback(() => { + router.push('/ko/accounting/bad-debt-collection'); + }, [router]); + + const handleEdit = useCallback(() => { + router.push(`/ko/accounting/bad-debt-collection/${recordId}/edit`); + }, [router, recordId]); + + const handleCancel = useCallback(() => { + if (isNewMode) { + router.push('/ko/accounting/bad-debt-collection'); + } else { + router.push(`/ko/accounting/bad-debt-collection/${recordId}`); + } + }, [router, recordId, isNewMode]); + + // 저장 핸들러 + const handleSave = useCallback(() => { + setShowSaveDialog(true); + }, []); + + const handleConfirmSave = useCallback(() => { + console.log('저장:', formData); + setShowSaveDialog(false); + if (isNewMode) { + router.push('/ko/accounting/bad-debt-collection'); + } else { + router.push(`/ko/accounting/bad-debt-collection/${recordId}`); + } + }, [formData, router, recordId, isNewMode]); + + // 삭제 핸들러 + const handleDelete = useCallback(() => { + setShowDeleteDialog(true); + }, []); + + const handleConfirmDelete = useCallback(() => { + console.log('삭제:', recordId); + setShowDeleteDialog(false); + router.push('/ko/accounting/bad-debt-collection'); + }, [router, recordId]); + + // 메모 추가 핸들러 + const handleAddMemo = useCallback(() => { + if (!newMemo.trim()) return; + const now = new Date(); + const dateStr = format(now, 'yyyy-MM-dd'); + const timeStr = format(now, 'HH:mm'); + const memo: BadDebtMemo = { + id: String(Date.now()), + content: `${dateStr} ${timeStr} [사용자] ${newMemo}`, + createdAt: now.toISOString(), + createdBy: '사용자', + }; + setFormData(prev => ({ + ...prev, + memos: [...prev.memos, memo], + })); + setNewMemo(''); + }, [newMemo]); + + // 메모 삭제 핸들러 + const handleDeleteMemo = useCallback((memoId: string) => { + setFormData(prev => ({ + ...prev, + memos: prev.memos.filter(m => m.id !== memoId), + })); + }, []); + + // 담당자 변경 핸들러 + const handleManagerChange = useCallback((managerId: string) => { + const manager = MANAGER_OPTIONS.find(m => m.id === managerId) || null; + setFormData(prev => ({ + ...prev, + assignedManagerId: managerId, + assignedManager: manager, + })); + }, []); + + // 수취 어음 현황 버튼 + const handleBillStatus = useCallback(() => { + router.push(`/ko/accounting/bills?vendorId=${formData.vendorId}&type=received`); + }, [router, formData.vendorId]); + + // 거래처 미수금 현황 버튼 + const handleReceivablesStatus = useCallback(() => { + router.push(`/ko/accounting/receivables-status?highlight=${formData.vendorId}`); + }, [router, formData.vendorId]); + + // 파일 다운로드 핸들러 + const handleFileDownload = useCallback((fileName: string) => { + console.log('파일 다운로드:', fileName); + // TODO: 실제 다운로드 로직 + }, []); + + // 기존 파일 삭제 핸들러 + const handleDeleteExistingFile = useCallback((fileId: string) => { + setFormData(prev => ({ + ...prev, + files: prev.files.filter(f => f.id !== fileId), + })); + }, []); + + // 추가 서류 추가 핸들러 + const handleAddAdditionalFile = useCallback((file: File) => { + setNewAdditionalFiles(prev => [...prev, file]); + }, []); + + // 추가 서류 (새 파일) 삭제 핸들러 + const handleRemoveNewAdditionalFile = useCallback((index: number) => { + setNewAdditionalFiles(prev => prev.filter((_, i) => i !== index)); + }, []); + + // 헤더 버튼 + const headerActions = useMemo(() => { + if (isViewMode) { + return ( +
+ + +
+ ); + } + return ( +
+ + +
+ ); + }, [isViewMode, isNewMode, handleDelete, handleEdit, handleCancel, handleSave]); + + // 입력 필드 렌더링 헬퍼 + const renderField = ( + label: string, + field: string, + value: string | number, + options?: { + required?: boolean; + type?: 'text' | 'tel' | 'email' | 'number'; + placeholder?: string; + disabled?: boolean; + } + ) => { + const { required, type = 'text', placeholder, disabled } = options || {}; + return ( +
+ + handleChange(field, type === 'number' ? Number(e.target.value) : e.target.value)} + placeholder={placeholder} + disabled={isViewMode || disabled} + className="bg-white" + /> +
+ ); + }; + + return ( + + + +
+ {/* 기본 정보 */} + + + 기본 정보 + + + {renderField('사업자등록번호', 'businessNumber', formData.businessNumber, { required: true, placeholder: '000-00-00000', disabled: true })} + {renderField('거래처 코드', 'vendorCode', formData.vendorCode, { disabled: true })} + {renderField('거래처명', 'vendorName', formData.vendorName, { required: true })} + {renderField('대표자명', 'representativeName', formData.representativeName)} + {/* 거래처 유형 */} +
+
+ + handleChange('vendorType', checked ? 'both' : 'sales')} + disabled={isViewMode} + className="data-[state=checked]:bg-orange-500" + /> +
+ +
+ {/* 악성채권 등록 */} +
+ +
+ handleChange('businessType', e.target.value)} + placeholder="업태" + disabled={isViewMode} + className="bg-white" + /> + handleChange('businessCategory', e.target.value)} + placeholder="업종" + disabled={isViewMode} + className="bg-white" + /> +
+
+
+
+ + {/* 연락처 정보 */} + + + 연락처 정보 + + + {/* 주소 */} +
+ +
+ handleChange('zipCode', e.target.value)} + placeholder="우편번호" + disabled={isViewMode} + className="w-[120px] bg-white" + /> + +
+ handleChange('address1', e.target.value)} + placeholder="기본주소" + disabled={isViewMode} + className="bg-white" + /> + handleChange('address2', e.target.value)} + placeholder="상세주소" + disabled={isViewMode} + className="bg-white" + /> +
+
+ {renderField('전화번호', 'phone', formData.phone, { type: 'tel', placeholder: '02-0000-0000' })} + {renderField('모바일', 'mobile', formData.mobile, { type: 'tel', placeholder: '010-0000-0000' })} + {renderField('팩스', 'fax', formData.fax, { type: 'tel', placeholder: '02-0000-0000' })} + {renderField('이메일', 'email', formData.email, { type: 'email' })} +
+
+
+ + {/* 담당자 정보 */} + + + 담당자 정보 + + + {renderField('담당자명', 'contactName', formData.contactName)} + {renderField('담당자 전화', 'contactPhone', formData.contactPhone, { type: 'tel' })} + {renderField('시스템 관리자', 'systemManager', formData.systemManager, { disabled: true })} + + + + {/* 필요 서류 */} + + + 필요 서류 + + + {/* 사업자등록증 */} +
+ +
+ {/* 기존 파일 있는 경우 */} + {formData.files.find(f => f.type === 'businessRegistration') && !newBusinessRegistrationFile ? ( +
+
+ + {formData.files.find(f => f.type === 'businessRegistration')?.name} +
+ + {!isViewMode && ( + <> + + + + )} +
+ ) : newBusinessRegistrationFile ? ( + /* 새 파일 선택된 경우 */ +
+
+ + {newBusinessRegistrationFile.name} + (새 파일) +
+ +
+ ) : ( + /* 파일 없는 경우 */ +
+ + {!isViewMode && ( + setNewBusinessRegistrationFile(e.target.files?.[0] || null)} + className="hidden" + /> + )} +
+ )} +
+
+ + {/* 세금계산서 */} +
+ +
+ {/* 기존 파일 있는 경우 */} + {formData.files.find(f => f.type === 'taxInvoice') && !newTaxInvoiceFile ? ( +
+
+ + {formData.files.find(f => f.type === 'taxInvoice')?.name} +
+ + {!isViewMode && ( + <> + + + + )} +
+ ) : newTaxInvoiceFile ? ( + /* 새 파일 선택된 경우 */ +
+
+ + {newTaxInvoiceFile.name} + (새 파일) +
+ +
+ ) : ( + /* 파일 없는 경우 */ +
+ + {!isViewMode && ( + setNewTaxInvoiceFile(e.target.files?.[0] || null)} + className="hidden" + /> + )} +
+ )} +
+
+ + {/* 추가 서류 */} +
+
+ + {!isViewMode && ( + + )} +
+
+ {/* 기존 추가 서류 */} + {formData.files.filter(f => f.type === 'additional').map((file) => ( +
+
+ + {file.name} +
+ + {!isViewMode && ( + + )} +
+ ))} + {/* 새로 추가된 파일 */} + {newAdditionalFiles.map((file, index) => ( +
+
+ + {file.name} + (새 파일) +
+ +
+ ))} + {/* 파일 없는 경우 안내 */} + {formData.files.filter(f => f.type === 'additional').length === 0 && newAdditionalFiles.length === 0 && ( +
+ + 추가 서류가 없습니다 +
+ )} +
+
+
+
+ + {/* 악성 채권 정보 */} + + + 악성 채권 정보 + + +
+ {/* 미수금 */} +
+ + handleChange('debtAmount', Number(e.target.value))} + disabled={isViewMode} + className="bg-white" + /> +
+ {/* 상태 */} +
+ + +
+ {/* 연체일수 */} +
+ +
+ handleChange('overdueDays', Number(e.target.value))} + disabled={isViewMode} + className="bg-white w-[100px]" + /> + +
+
+ {/* 본사 담당자 */} +
+ + +
+ {/* 악성채권 발생일 */} +
+ + handleChange('occurrenceDate', e.target.value)} + disabled={isViewMode} + className="bg-white" + /> +
+ {/* 악성채권 종료일 */} +
+ + handleChange('endDate', e.target.value || null)} + disabled={isViewMode} + className="bg-white" + placeholder="-" + /> +
+
+ {/* 연동 버튼 */} +
+ + +
+
+
+ + {/* 메모 */} + + + 메모 + + + {/* 메모 입력 */} + {!isViewMode && ( +
+