From a2c3e4c41e4b93c664c9ad274d6815d14e247618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Thu, 19 Feb 2026 16:30:07 +0900 Subject: [PATCH] =?UTF-8?q?refactor(WEB):=20=ED=94=84=EB=A1=A0=ED=8A=B8?= =?UTF-8?q?=EC=97=94=EB=93=9C=20=EB=8C=80=EA=B7=9C=EB=AA=A8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=95=EB=A6=AC=20=EB=B0=8F=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 미사용 코드 삭제: ThemeContext, itemStore, utils/date.ts, utils/formatAmount.ts - 유틸리티 이동: date, formatAmount → src/lib/utils/ (중앙 집중화) - 다수 page.tsx 클라이언트 컴포넌트 패턴 통일 - DateRangeSelector 리팩토링 및 date-range-picker UI 컴포넌트 추가 - ThemeSelect/themeStore Zustand 직접 연동으로 전환 - 건설/회계/영업/품목/출하 등 전반적 컴포넌트 개선 - UniversalListPage, IntegratedListTemplateV2 타입 확장 - 프론트엔드 종합 리뷰 문서 및 개선 체크리스트 추가 Co-Authored-By: Claude Opus 4.6 --- ...26-02-19] frontend-comprehensive-review.md | 165 +++++++ ...6-02-19] frontend-improvement-checklist.md | 465 ++++++++++++++++++ claudedocs/_index.md | 3 +- ...19] dynamic-rendering-platform-strategy.md | 224 +++++++++ .../[REF-2026-02-19] todo-issue-tracker.md | 342 +++++++++++++ package.json | 2 - .../accounting/bad-debt-collection/page.tsx | 9 +- .../(protected)/accounting/bills/page.tsx | 9 +- .../(protected)/accounting/deposits/page.tsx | 9 +- .../accounting/expected-expenses/page.tsx | 9 +- .../accounting/gift-certificates/page.tsx | 9 +- .../(protected)/accounting/sales/page.tsx | 10 +- .../accounting/tax-invoice-issuance/page.tsx | 9 +- .../(protected)/accounting/vendors/page.tsx | 9 +- .../accounting/withdrawals/page.tsx | 9 +- .../boards/[boardCode]/[postId]/page.tsx | 4 +- .../progress-billing-management/[id]/page.tsx | 9 +- .../progress-billing-management/page.tsx | 9 +- .../order/order-management/[id]/page.tsx | 9 +- .../project/bidding/[id]/page.tsx | 9 +- .../project/bidding/estimates/[id]/page.tsx | 9 +- .../project/bidding/partners/[id]/page.tsx | 9 +- .../bidding/site-briefings/[id]/page.tsx | 9 +- .../project/construction-management/page.tsx | 9 +- .../project/contract/[id]/page.tsx | 9 +- .../project/contract/create/page.tsx | 9 +- .../contract/handover-report/[id]/page.tsx | 9 +- .../project/issue-management/[id]/page.tsx | 9 +- .../project/issue-management/page.tsx | 9 +- .../customer-center/events/[id]/page.tsx | 9 +- .../customer-center/notices/[id]/page.tsx | 9 +- .../dev/construction-test-urls/page.tsx | 9 +- .../(protected)/dev/test-urls/page.tsx | 9 +- .../(protected)/payment-history/page.tsx | 9 +- .../order-management-sales/[id]/edit/page.tsx | 2 +- .../order-management-sales/[id]/page.tsx | 2 +- .../[id]/production-order/page.tsx | 2 +- .../sales/order-management-sales/page.tsx | 2 +- .../sales/pricing-management/[id]/page.tsx | 9 +- .../sales/pricing-management/create/page.tsx | 9 +- .../sales/pricing-management/page.tsx | 9 +- .../sales/quote-management/page.tsx | 9 +- .../settings/notification-settings/page.tsx | 9 +- .../settings/popup-management/page.tsx | 9 +- .../forklift/[id]/edit/page.tsx | 9 +- .../vehicle-management/forklift/[id]/page.tsx | 9 +- .../vehicle-management/forklift/page.tsx | 9 +- .../vehicle-log/[id]/edit/page.tsx | 9 +- .../vehicle-log/[id]/page.tsx | 9 +- .../vehicle-management/vehicle-log/page.tsx | 9 +- .../vehicle/[id]/edit/page.tsx | 9 +- .../vehicle-management/vehicle/[id]/page.tsx | 9 +- .../vehicle-management/vehicle/page.tsx | 9 +- src/app/[locale]/layout.tsx | 13 +- src/components/ThemeSelect.tsx | 4 +- .../accounting/SalesManagement/index.tsx | 5 +- .../approval/DocumentCreate/index.tsx | 22 +- src/components/attendance/actions.ts | 2 +- src/components/board/BoardForm/index.tsx | 4 +- .../board/BoardList/BoardListUnified.tsx | 48 +- .../business/CEODashboard/components.tsx | 2 +- .../sections/EnhancedSections.tsx | 2 +- src/components/business/MainDashboard.tsx | 2 +- .../bidding/BiddingDetailForm.tsx | 2 +- .../bidding/BiddingListClient.tsx | 4 +- .../business/construction/bidding/types.ts | 2 +- .../contract/ContractDetailForm.tsx | 2 +- .../contract/ContractListClient.tsx | 4 +- .../business/construction/contract/types.ts | 2 +- .../estimates/EstimateListClient.tsx | 2 +- .../modals/EstimateDocumentContent.tsx | 2 +- .../business/construction/estimates/types.ts | 2 +- .../estimates/utils/formatters.ts | 2 +- .../HandoverReportDetailForm.tsx | 2 +- .../HandoverReportListClient.tsx | 8 +- .../modals/HandoverReportDocumentModal.tsx | 2 +- .../issue-management/IssueDetailForm.tsx | 2 +- .../IssueManagementListClient.tsx | 6 +- .../management/ConstructionDetailClient.tsx | 2 +- .../ConstructionManagementListClient.tsx | 16 +- .../management/ProjectEndDialog.tsx | 2 +- .../OrderManagementListClient.tsx | 14 +- .../pricing-management/PricingListClient.tsx | 4 +- .../ProgressBillingManagementListClient.tsx | 4 +- .../construction/site-briefings/types.ts | 2 +- .../SiteManagementListClient.tsx | 4 +- .../StructureReviewListClient.tsx | 6 +- .../UtilityManagementListClient.tsx | 8 +- .../worker-status/WorkerStatusListClient.tsx | 4 +- .../InquiryManagement/InquiryForm.tsx | 4 +- src/components/dev/generators/index.ts | 2 +- .../VacationGrantDialog.tsx | 7 +- .../VacationRequestDialog.tsx | 7 +- .../hr/VacationManagement/index.tsx | 9 +- src/components/items/BOMManagementSection.tsx | 23 +- .../DynamicItemForm/hooks/useFileHandling.ts | 9 +- .../items/DynamicItemForm/index.tsx | 3 +- src/components/items/ItemDetailClient.tsx | 3 +- .../items/ItemForm/BendingDiagramSection.tsx | 5 +- src/components/items/ItemForm/index.tsx | 3 +- src/components/items/ItemListClient.tsx | 15 +- .../tabs/HierarchyTab/index.tsx | 75 ++- .../ReceivingManagement/InspectionCreate.tsx | 2 +- .../ReceivingReceiptContent.tsx | 2 +- .../material/StockStatus/mockData.ts | 2 +- .../molecules/DateRangeSelector.tsx | 86 ++-- src/components/orders/OrderRegistration.tsx | 2 +- .../orders/OrderSalesDetailEdit.tsx | 2 +- .../orders/OrderSalesDetailView.tsx | 2 +- .../orders/QuotationSelectDialog.tsx | 2 +- .../orders/documents/ContractDocument.tsx | 2 +- .../documents/PurchaseOrderDocument.tsx | 2 +- .../orders/documents/SalesOrderDocument.tsx | 2 +- .../orders/documents/TransactionDocument.tsx | 2 +- .../ShipmentManagement/ShipmentCreate.tsx | 2 +- .../ShipmentManagement/ShipmentDetail.tsx | 9 +- .../ShipmentManagement/ShipmentList.tsx | 13 +- src/components/pricing/PricingFormClient.tsx | 2 +- .../process-management/RuleModal.tsx | 3 +- .../production/WorkOrders/WorkOrderEdit.tsx | 22 +- .../InspectionManagement/InspectionList.tsx | 11 + .../quotes/QuoteManagementClient.tsx | 2 +- src/components/quotes/QuoteRegistration.tsx | 2 +- src/components/quotes/types.ts | 2 +- .../settings/PopupManagement/PopupForm.tsx | 4 +- .../templates/IntegratedListTemplateV2.tsx | 3 + .../templates/UniversalListPage/types.ts | 2 + src/components/ui/date-range-picker.tsx | 320 ++++++++++++ src/contexts/ThemeContext.tsx | 57 --- src/layouts/AuthenticatedLayout.tsx | 4 +- src/lib/auth/logout.ts | 10 +- src/lib/print-utils.ts | 3 +- .../formatAmount.ts => lib/utils/amount.ts} | 0 src/{ => lib}/utils/date.ts | 0 src/stores/itemStore.ts | 323 ------------ src/stores/themeStore.ts | 10 +- 136 files changed, 1987 insertions(+), 896 deletions(-) create mode 100644 claudedocs/[ANALYSIS-2026-02-19] frontend-comprehensive-review.md create mode 100644 claudedocs/[IMPL-2026-02-19] frontend-improvement-checklist.md create mode 100644 claudedocs/architecture/[VISION-2026-02-19] dynamic-rendering-platform-strategy.md create mode 100644 claudedocs/dev/[REF-2026-02-19] todo-issue-tracker.md create mode 100644 src/components/ui/date-range-picker.tsx delete mode 100644 src/contexts/ThemeContext.tsx rename src/{utils/formatAmount.ts => lib/utils/amount.ts} (100%) rename src/{ => lib}/utils/date.ts (100%) delete mode 100644 src/stores/itemStore.ts diff --git a/claudedocs/[ANALYSIS-2026-02-19] frontend-comprehensive-review.md b/claudedocs/[ANALYSIS-2026-02-19] frontend-comprehensive-review.md new file mode 100644 index 00000000..27178ed8 --- /dev/null +++ b/claudedocs/[ANALYSIS-2026-02-19] frontend-comprehensive-review.md @@ -0,0 +1,165 @@ +# SAM ERP 프론트엔드 종합 검수 보고서 + +> 작성일: 2026-02-19 +> 분석 범위: src/ 전체 (1,438개 TS/TSX 파일, ~314K줄) +> 분석 방법: 5개 에이전트 병렬 분석 (코드품질, 번들/성능, 에러/UX, 아키텍처, 모바일/보안) + +--- + +## 종합 스코어카드 + +| 영역 | 점수 | 등급 | 핵심 이슈 | +|------|------|------|-----------| +| **코드 품질** | 7.5/10 | 🟢 양호 | TS 규율 우수, any 133건/TODO 121건 잔존 | +| **번들/성능** | 8.5/10 | 🟢 우수 | 동적 로드 적용, tree-shaking 양호 | +| **에러/UX 일관성** | 5.5/10 | 🟡 보통 | 에러바운더리 우수, 로딩UI/접근성 미흡 | +| **아키텍처** | 6.5/10 | 🟡 보통 | 순환의존 없음, 상태관리 중복 | +| **모바일 대응** | 6/10 | 🟡 보통 | 57% 반응형, 터치영역 미달 | +| **보안** | 7/10 | 🟢 양호 | 인증 강함, CSP unsafe 허용 | + +**전체: 6.8/10** — 기능적으로 안정적이나, UX 일관성과 아키텍처 정리에 개선 여지 + +--- + +## 우선순위별 개선 항목 + +### P0: 보안 이슈 (즉시 조치) + +| # | 항목 | 심각도 | 현황 | 조치 | +|---|------|--------|------|------| +| S-1 | CSP `unsafe-inline`/`unsafe-eval` | 🔴 높음 | middleware.ts에서 허용 중 | nonce 기반으로 전환 | +| S-2 | `new Function()` 코드 주입 | 🔴 높음 | ComputedField.tsx에서 사용 | 사용자 입력 검증 추가 또는 safe-eval 대체 | +| S-3 | sanitizeHTML 함수 강도 | 🟡 중간 | 5개 파일에서 사용 중 | DOMPurify 사용 여부 확인 | + +### P1: 아키텍처 정리 (1~2주) + +| # | 항목 | 현황 | 개선안 | +|---|------|------|--------| +| A-1 | **상태관리 중복** | ItemMasterContext + itemStore + useItemMasterStore 3중 | Zustand 하나로 통합 | +| A-2 | **테마 중복** | ThemeContext + themeStore 병존 | Zustand로 완전 마이그레이션 | +| A-3 | **utils 폴더 중복** | `src/utils/` (2개) + `src/lib/utils/` (11개) 병존 | `src/utils/` → `src/lib/utils/`로 통합 | +| A-4 | **상수 산재** | constants/ 1개 파일만, 나머지 각 컴포넌트 내부 하드코딩 | 도메인별 `constants/` 정리 | + +### P2: 코드 품질 (2~3주) + +| # | 항목 | 건수 | 현황 | 조치 | +|---|------|------|------|------| +| Q-1 | `as any` 타입 캐스트 | 64건 | 주로 form errors 처리 | 제네릭 타입 정의 | +| Q-2 | `: any` 타입 선언 | 48건 | API 응답/props 타입 | 인터페이스 정의 | +| Q-3 | TODO/FIXME 누적 | 121건 (68파일) | useItemMasterStore 15건 등 | 이슈화 → 점진적 해소 | +| Q-4 | God 컴포넌트 | 5개 | ItemMasterContext 2,200줄, MainDashboard 1,400줄 | 단계적 분리 | +| Q-5 | 거대 훅 | 1개 | useCEODashboard 37.9KB | stats/charts/timeline 분리 | +| Q-6 | `alert()`/`confirm()` 잔존 | 32건 | 15개 alert + 17개 confirm | ConfirmDialog/toast로 교체 | + +### P3: UX 일관성 (3~4주) + +| # | 항목 | 현황 | 목표 | +|---|------|------|------| +| U-1 | **로딩 UI** | 40+ 페이지에서 `"로딩 중..."` 텍스트만 사용, Skeleton 2개만 | Skeleton 기반 로딩으로 통일 | +| U-2 | **접근성 (a11y)** | aria-label 3건, role 9건 | 주요 폼/테이블에 ARIA 추가 | +| U-3 | **i18n 사용률** | 인프라 완성(ko/en/ja), 실제 사용 ~5% | 점진적 적용 확대 | +| U-4 | **Zod 검증** | 2개 폼만 적용 | 신규 폼 필수, 기존은 유지 | +| U-5 | **EmptyState 활용** | 컴포넌트 존재하나 하드코딩 "데이터 없음" 다수 | EmptyState 컴포넌트 통일 | + +### P4: 모바일/성능 (선택) + +| # | 항목 | 현황 | 조치 | +|---|------|------|------| +| M-1 | **반응형 커버리지** | 57% 페이지 적용 | HR/대시보드 등 미적용 페이지 보강 | +| M-2 | **터치 영역** | Checkbox 20x20px (권장 44x44px) | 모바일 터치 타겟 확대 | +| M-3 | **html2canvas + dom-to-image** 중복 | 2개 라이브러리 공존 | 하나로 통합 (~50-80KB 절감) | +| M-4 | **Tiptap 동적 로딩** | 보드/팝업에서만 사용하나 번들 포함 | next/dynamic 적용 (~80-100KB 절감) | +| M-5 | **도메인별 actions.ts 표준화** | accounting만 page-level actions, 나머지는 컴포넌트 내부 | accounting 패턴으로 통일 | + +--- + +## 잘 되어있는 점 (유지 사항) + +### 코드 품질 +- ✅ **TypeScript 규율**: @ts-ignore 0건, @ts-nocheck 1건(레거시) +- ✅ **console.log 관리**: 23건만 (16건은 logger 유틸리티) +- ✅ **에러 바운더리**: 글로벌 + Protected 레벨 4개, Slack 연동 +- ✅ **Toast 시스템**: sonner 기반 1,277개 인스턴스 일관 사용 + +### 번들/성능 +- ✅ **XLSX 동적 로드**: 버튼 클릭 시에만 ~400KB 로드 +- ✅ **대시보드 코드 스플리팅**: ~850KB 초기 번들에서 제외 +- ✅ **tree-shaking**: `import *` 0건, lodash/moment 미사용 +- ✅ **Zustand 정규화**: 체계적 상태 + Immer + selector hooks +- ✅ **Tailwind v4**: 최신 버전, 효율적 트리셰이킹 + +### 아키텍처 +- ✅ **순환 의존성 없음**: pages→components→ui 단방향 +- ✅ **API 계층**: buildApiUrl 43개 actions.ts 전면 적용 +- ✅ **executePaginatedAction**: 14개 파일 표준화 + +### 보안 +- ✅ **Bot 차단**: 25개 패턴 필터링 +- ✅ **다층 인증**: Bearer Token + Authorization 헤더 + Sanctum + API Key +- ✅ **Open Redirect 방지**: 내부 경로 검증 +- ✅ **환경변수 분리**: NEXT_PUBLIC_ 적절히 사용 +- ✅ **민감 정보 노출 없음**: console.log에 토큰/비밀번호 출력 0건 + +--- + +## 주요 파일 참조 + +### God 컴포넌트 (분리 대상) +- `src/contexts/ItemMasterContext.tsx` (2,200줄) +- `src/components/business/MainDashboard.tsx` (1,400줄) +- `src/hooks/useCEODashboard.ts` (37.9KB) + +### any 타입 집중 지역 +- `src/components/items/ItemForm/forms/parts/` (22건) +- `src/components/items/ItemMasterDataManagement/` (18건) +- `src/components/quotes/LocationDetailPanel.tsx` (10건) + +### 보안 확인 대상 +- `src/middleware.ts` (CSP 설정) +- `src/components/**/ComputedField.tsx` (new Function) +- sanitizeHTML 사용 파일 5개 (게시판, 팝업, 고객센터) + +### 상태관리 중복 +- `src/contexts/ItemMasterContext.tsx` vs `src/stores/itemStore.ts` vs `src/stores/item-master/useItemMasterStore.ts` +- `src/contexts/ThemeContext.tsx` vs `src/stores/themeStore.ts` + +--- + +## 기존 로드맵과의 관계 + +| 기존 항목 | 상태 | 이번 분석 결과 | +|-----------|------|---------------| +| D-1 God 컴포넌트 분리 | ⏳ 대기 | → P2-Q4로 재확인, 여전히 필요 | +| D-2 `as` 타입 캐스트 | 보류 | → P2-Q1/Q2로 133건 확인 (기존 ~200건에서 감소) | +| D-6 TODO 102건 | ⏳ 대기 | → P2-Q3으로 121건 확인 (소폭 증가) | +| A-2 DataTable 최적화 | ⏳ 대기 | → 에이전트 분석 결과 re-render 위험 낮음 (우선순위 하향) | + +### 신규 발견 항목 (기존 로드맵에 없었던 것) +- **S-1~S-3**: 보안 이슈 (CSP, code injection, sanitization) +- **A-1~A-2**: 상태관리 3중 중복 +- **U-1~U-5**: UX 일관성 전반 (로딩/접근성/i18n/빈상태) +- **M-3~M-4**: 라이브러리 중복/동적 로딩 기회 + +--- + +## 실행 로드맵 요약 + +``` +Week 1-2: P0 보안 + P1 아키텍처 정리 + ├── CSP nonce 전환 + ├── ComputedField 보안 패치 + ├── 상태관리 중복 정리 (Context → Zustand) + └── utils 폴더 통합 + +Week 3-4: P2 코드 품질 + ├── any 타입 정리 (form errors 제네릭) + ├── alert/confirm → ConfirmDialog 교체 + └── TODO/FIXME 이슈 정리 + +Week 5-6: P3 UX 일관성 (선택) + ├── Skeleton 로딩 UI 통일 + ├── EmptyState 활용 확대 + └── 접근성 기본 적용 + +이후: P4 모바일/성능 (필요 시) +``` \ No newline at end of file diff --git a/claudedocs/[IMPL-2026-02-19] frontend-improvement-checklist.md b/claudedocs/[IMPL-2026-02-19] frontend-improvement-checklist.md new file mode 100644 index 00000000..30d2c113 --- /dev/null +++ b/claudedocs/[IMPL-2026-02-19] frontend-improvement-checklist.md @@ -0,0 +1,465 @@ +# SAM ERP 프론트엔드 개선 체크리스트 + +> 작성일: 2026-02-19 +> 기반: `[ANALYSIS-2026-02-19] frontend-comprehensive-review.md` 분석 결과 +> 구조: 8개 독립 작업 패키지 (WP) — 에이전트 병렬 작업 가능 + +--- + +## 작업 패키지 의존성 맵 + +``` +완료됨: + ✅ WP-1: alert/confirm 제거 + ✅ WP-2: utils 폴더 통합 + ✅ WP-3: 미사용 패키지 제거 + ✅ WP-4: ThemeContext → themeStore 통합 + ✅ WP-5: 로딩 UI Skeleton 통일 + ✅ WP-6: itemStore 제거 (미사용 store 정리) + ✅ WP-7: TODO 이슈 정리 (문서 작업) + +보류 (선택): + WP-8: any 타입 정리 +``` + +--- + +## WP-1: alert()/confirm() → Toast/Dialog 교체 ✅ 완료 (2026-02-19) + +**범위**: 42개 alert + 14개 confirm = 56건 +**난이도**: 낮음 | **영향**: 중간 + +### alert() → toast 교체 (42건) ✅ 전체 완료 + +#### 파일 1: `src/lib/print-utils.ts` +- [x] `alert('팝업이 차단되었습니다...')` → `toast.error(...)` + +#### 파일 2: `src/components/items/ItemListClient.tsx` +- [x] 삭제 결과 → `toast.success(...)` + 실패 시 `toast.error(...)` +- [x] 업로드 결과 → `toast.success(...)` / `toast.error('업로드 오류', { description })` +- [x] 기타 alert 4건 → toast 교체 + +#### 파일 3: `src/components/hr/VacationManagement/index.tsx` +- [x] 유효성 3건 → `toast.warning(...)` +- [x] 에러/성공 4건 → `toast.error(...)` / `toast.success(...)` + +#### 파일 4: `src/components/outbound/ShipmentManagement/ShipmentDetail.tsx` +- [x] 에러 4건 → `toast.error(...)` + +#### 파일 5: `src/components/items/ItemMasterDataManagement/tabs/HierarchyTab/index.tsx` +- [x] 클립보드 복사 4건 → `toast.success(...)` / `toast.error(...)` + +#### 파일 6: `src/components/items/DynamicItemForm/hooks/useFileHandling.ts` +- [x] 4건 → toast 교체 + +#### 파일 7: `src/components/items/ItemForm/BendingDiagramSection.tsx` +- [x] 2건 → `toast.error(...)` + +#### 파일 8: `src/components/items/DynamicItemForm/index.tsx` +- [x] 4건 → toast 교체 + +#### 파일 9: `src/components/items/ItemDetailClient.tsx` +- [x] 3건 → toast 교체 + +#### 파일 10: `src/components/hr/VacationManagement/VacationRequestDialog.tsx` +- [x] 2건 → toast 교체 + +#### 파일 11: `src/components/hr/VacationManagement/VacationGrantDialog.tsx` +- [x] 2건 → toast 교체 + +#### 파일 12: `src/components/process-management/RuleModal.tsx` +- [x] 1건 → toast 교체 + +#### 파일 13: `src/app/[locale]/(protected)/dev/page-builder/PageBuilderClient.tsx` +- [x] 5건 → toast 교체 (개발 도구) + +#### 파일 14: `src/app/[locale]/(protected)/dev/page-builder/components/PageSelector.tsx` +- [x] 2건 → toast 교체 (개발 도구) + +#### 파일 15: `src/components/board/BoardList/BoardListUnified.tsx` +- [x] 1건 → toast 교체 + +### confirm() → ConfirmDialog 교체 (14건) + +#### Component-level (9건) ✅ 완료 + +- [x] `src/components/production/WorkOrders/WorkOrderEdit.tsx` — `DeleteConfirmDialog` + state 패턴 +- [x] `src/components/approval/DocumentCreate/index.tsx` — `DeleteConfirmDialog` + state 패턴 +- [x] `src/components/board/BoardList/BoardListUnified.tsx` (2건) — `DeleteConfirmDialog` + `deleteTarget` state +- [x] `src/components/items/BOMManagementSection.tsx` — `DeleteConfirmDialog` + `deleteTargetId` state +- [x] `src/components/items/ItemMasterDataManagement/tabs/HierarchyTab/index.tsx` (3건) — `ConfirmDialog` + discriminated union `HierarchyConfirmAction` state + +#### Hook-level (5건) ⏸️ 후순위 — hooks에서 JSX 렌더링 불가, callback 패턴 리팩토링 필요 + +- [ ] `src/components/items/DynamicItemForm/hooks/useFileHandling.ts:258` +- [ ] `src/components/items/ItemMasterDataManagement/hooks/useDeleteManagement.ts:82` +- [ ] `src/components/items/ItemMasterDataManagement/hooks/useMasterFieldManagement.ts:220` +- [ ] `src/components/items/ItemMasterDataManagement/hooks/useTemplateManagement.ts:227` +- [ ] `src/components/items/ItemMasterDataManagement/hooks/useTemplateManagement.ts:402` + +#### Dev 도구 (1건) ⏸️ 낮은 우선순위 + +- [ ] `src/app/[locale]/(protected)/dev/page-builder/components/PageSelector.tsx:293` + +### 완료 검증 +- [x] 프로젝트 전체에서 `alert(` 검색 → 0건 (src/ 기준) +- [x] 프로젝트 전체에서 `confirm(` 검색 → 6건 잔여 (hook 5건 + dev 1건, 위 후순위 항목) + +--- + +## WP-2: utils 폴더 통합 (`src/utils/` → `src/lib/utils/`) ✅ 완료 (2026-02-19) + +**범위**: 2개 파일 이동 + 49개 import 경로 수정 +**난이도**: 낮음 | **영향**: 낮음 + +### Step 1: 파일 이동 + +- [x] `src/utils/date.ts` → `src/lib/utils/date.ts` (내용 변경 없음) +- [x] `src/utils/formatAmount.ts` → `src/lib/utils/amount.ts` (내용 변경 없음) + +### Step 2: import 경로 수정 + +#### `@/utils/date` → `@/lib/utils/date` (25파일) +- [x] 프로젝트 전체에서 `from '@/utils/date'` 검색 → 모두 교체 완료 + +#### `@/utils/formatAmount` → `@/lib/utils/amount` (24파일) +- [x] 프로젝트 전체에서 `from '@/utils/formatAmount'` 검색 → 모두 교체 완료 + +### Step 3: 정리 +- [x] `src/utils/` 디렉토리 삭제 +- [x] import 경로 검증: `@/utils/` 패턴 0건 확인 + +--- + +## WP-3: 미사용 패키지 제거 + Tiptap 동적 로딩 ✅ 완료 (2026-02-19) + +**범위**: package.json 정리 + 3개 파일 dynamic import +**난이도**: 낮음~중간 | **영향**: 번들 사이즈 감소 + +### Step 1: 미사용 패키지 확인 및 제거 + +- [x] `html2canvas` (^1.4.1) — 소스코드에서 import 0건 확인 → `npm uninstall` 완료 +- [x] `dom-to-image-more` (^3.7.2) — 소스코드에서 import 0건 확인 → `npm uninstall` 완료 + +### Step 2: Tiptap 동적 로딩 + +- [x] `src/components/board/BoardForm/index.tsx` — `dynamic(() => import('../RichTextEditor'), { ssr: false })` +- [x] `src/components/customer-center/InquiryManagement/InquiryForm.tsx` — 동일 패턴 +- [x] `src/components/settings/PopupManagement/PopupForm.tsx` — 동일 패턴 +- [x] `src/components/settings/PopupManagement/popupDetailConfig.ts` — ⏭️ 스킵 (createElement 패턴, dynamic 비호환) + +### 완료 검증 +- [x] `npm ls html2canvas` → not found +- [x] `npm ls dom-to-image-more` → not found + +--- + +## WP-4: ThemeContext → themeStore 통합 ✅ 완료 (2026-02-19) + +**범위**: Context 제거 + 3개 파일 import 수정 +**난이도**: 낮음 | **영향**: 낮음 (3파일) | **예상**: 30분 + +### 현황 +- `ThemeContext.tsx`: Provider 패턴, localStorage 직접 사용 +- `themeStore.ts`: Zustand + persist, 동일 기능 +- ThemeContext 사용처: 3개 파일만 + +### Step 1: 사용처 마이그레이션 + +- [x] `src/components/ThemeSelect.tsx`: + - `import { useTheme } from '@/contexts/ThemeContext'` → `import { useThemeStore } from '@/stores/themeStore'` + - `const { theme, setTheme } = useTheme()` → `const { theme, setTheme } = useThemeStore()` + +- [x] `src/layouts/AuthenticatedLayout.tsx`: + - 동일 패턴으로 교체 + +- [x] `src/app/[locale]/layout.tsx`: + - ThemeProvider import 및 래핑 제거 (Zustand는 Provider 불필요) + +### Step 2: ThemeContext 제거 + +- [x] `src/contexts/ThemeContext.tsx` 삭제 +- [x] `src/contexts/RootProvider.tsx` — ThemeProvider 미사용 확인 (이미 없었음) + +### Step 3: themeStore DOM 클래스 버그 수정 + +- [x] `themeStore.ts`의 `setTheme()`: `document.documentElement.className = ...` → `classList.remove/add` 방식으로 수정 (폰트 클래스 보존) +- [x] `themeStore.ts`의 `onRehydrateStorage`: 동일하게 `classList` 방식으로 수정 + +### 완료 검증 +- [x] `ThemeContext` import 0건 확인 (주석 1건만 잔존) +- [x] TypeScript 에러 없음 + +--- + +## WP-5: 로딩 UI Skeleton 통일 ✅ 완료 (2026-02-19) + +**범위**: GenericPageSkeleton 활용하여 44개 페이지 표준화 +**난이도**: 중간 | **영향**: UX 일관성 + +### 현황 +- 이미 존재: `GenericPageSkeleton` 컴포넌트 (protected/loading.tsx에서 사용) +- 44개 page.tsx에서 `
로딩 중...
` → `` 교체 완료 + +### 교체 완료 (43개 page.tsx → GenericPageSkeleton) + +- [x] `accounting/vendors/page.tsx` +- [x] `accounting/gift-certificates/page.tsx` +- [x] `accounting/sales/page.tsx` +- [x] `accounting/tax-invoice-issuance/page.tsx` +- [x] `accounting/deposits/page.tsx` +- [x] `accounting/bills/page.tsx` +- [x] `accounting/withdrawals/page.tsx` +- [x] `accounting/expected-expenses/page.tsx` +- [x] `accounting/bad-debt-collection/page.tsx` +- [x] `settings/notification-settings/page.tsx` +- [x] `settings/popup-management/page.tsx` +- [x] `vehicle-management/vehicle/page.tsx` +- [x] `vehicle-management/vehicle/[id]/page.tsx` +- [x] `vehicle-management/vehicle/[id]/edit/page.tsx` +- [x] `vehicle-management/forklift/page.tsx` +- [x] `vehicle-management/forklift/[id]/page.tsx` +- [x] `vehicle-management/forklift/[id]/edit/page.tsx` +- [x] `vehicle-management/vehicle-log/page.tsx` +- [x] `vehicle-management/vehicle-log/[id]/page.tsx` +- [x] `vehicle-management/vehicle-log/[id]/edit/page.tsx` +- [x] `sales/quote-management/page.tsx` +- [x] `sales/pricing-management/page.tsx` +- [x] `sales/pricing-management/[id]/page.tsx` +- [x] `sales/pricing-management/create/page.tsx` +- [x] `sales/order-management-sales/new/page.tsx` +- [x] `sales/client-management-sales-admin/page.tsx` +- [x] `construction/project/construction-management/page.tsx` +- [x] `construction/project/bidding/[id]/page.tsx` +- [x] `construction/project/bidding/partners/[id]/page.tsx` +- [x] `construction/project/bidding/estimates/[id]/page.tsx` +- [x] `construction/project/bidding/site-briefings/[id]/page.tsx` +- [x] `construction/project/contract/create/page.tsx` +- [x] `construction/project/contract/[id]/page.tsx` +- [x] `construction/project/contract/handover-report/[id]/page.tsx` +- [x] `construction/project/issue-management/page.tsx` +- [x] `construction/project/issue-management/[id]/page.tsx` +- [x] `construction/billing/progress-billing-management/page.tsx` +- [x] `construction/billing/progress-billing-management/[id]/page.tsx` +- [x] `construction/order/order-management/[id]/page.tsx` +- [x] `customer-center/notices/[id]/page.tsx` +- [x] `customer-center/events/[id]/page.tsx` +- [x] `payment-history/page.tsx` +- [x] `dev/test-urls/page.tsx` +- [x] `dev/construction-test-urls/page.tsx` +- [x] `[...slug]/page.tsx` + +### 특수 케이스 (1건 → DetailPageSkeleton) + +- [x] `boards/[boardCode]/[postId]/page.tsx` — PageLayout 래핑 유지, `DetailPageSkeleton` 사용 (이미 import 존재) + +### 미처리 (dev 도구, 컴포넌트 내부) + +- dev 도구 3건: `PageBuilderClient.tsx`, `PageSelector.tsx`, `ComponentRegistryClient.tsx` — page.tsx가 아닌 Client 컴포넌트 내부 로딩 +- 컴포넌트 내부 37건: 모달, 폼, 차트 등 내부 로딩 상태 — 페이지 레벨이 아니므로 GenericPageSkeleton 부적합 + +### 완료 검증 +- [x] `"로딩 중..."` 텍스트 검색 → page.tsx에서 0건 + +--- + +## WP-6: itemStore 제거 (미사용 Zustand store 정리) ✅ 완료 (2026-02-19) + +**범위**: 1개 store 삭제 + 1개 파일 수정 +**난이도**: 낮음 | **영향**: 최소 (1파일만 수정) +**의존성**: WP-4 완료 후 진행 권장 (stores/ 정리 순서) + +### 현황 +- `src/stores/itemStore.ts` — import 파일 2개만 (logout.ts, 자기자신) +- `src/stores/item-master/useItemMasterStore.ts` — 45개 파일에서 사용 (실질적 메인 store) +- `src/contexts/ItemMasterContext.tsx` — 42개 파일에서 사용 (레거시, 점진적 마이그레이션) + +### Step 1: itemStore 참조 제거 + +- [x] `src/lib/auth/logout.ts`에서 `useItemStore` → `useItemMasterStore` 교체 + - `resetZustandStores()`: `useItemMasterStore.getState().reset()` 으로 변경 + - `debugCacheStatus()`: 동일하게 변경 + - 개선: 기존에는 미사용 itemStore만 초기화하고 실제 사용 중인 useItemMasterStore는 초기화 안 됨 → 수정으로 로그아웃 시 실제 품목 데이터 정리됨 + +### Step 2: itemStore 삭제 + +- [x] `src/stores/itemStore.ts` 삭제 +- [x] `src/stores/` index.ts 없음 (개별 import 방식) — 추가 정리 불필요 + +### 완료 검증 +- [x] `itemStore` import 0건 확인 +- [x] `useItemStore` 검색 0건 확인 +- [x] TypeScript 에러 없음 + +--- + +## WP-7: TODO/FIXME 이슈 정리 (문서 작업) ✅ 완료 (2026-02-19) + +**범위**: 102건 TODO → 카테고리별 정리 문서 +**난이도**: 낮음 (문서 작업) + +### 산출물 +- [x] `claudedocs/dev/[REF-2026-02-19] todo-issue-tracker.md` 문서 작성 완료 + +### 분류 결과 (102건) + +| 카테고리 | 건수 | 비율 | +|----------|------|------| +| A. 백엔드 API 연동 대기 | 55건 | 53.9% | +| B. 백엔드 필드 추가 대기 | 10건 | 9.8% | +| C. UI/기능 구현 대기 | 16건 | 15.7% | +| D. Phase 2 / 장기 과제 | 10건 | 9.8% | +| E. CEO 대시보드 목업 데이터 | 4건 | 3.9% | +| F. 기타 | 7건 | 6.9% | + +### 핵심 인사이트 +- 백엔드 의존 항목이 63.7% (A+B = 65건) +- 완전 Mock 모듈 6개 (상품권, 세금계산서 관리/발행, 기성관리, 프로젝트관리, 품목기준관리) +- 품목기준관리 스토어 단일 최다 (16건) +- 백엔드 팀 전달 요약 섹션 포함 + +--- + +## WP-8: any 타입 정리 — ⏸️ 선별 보류 (2026-02-19 검토) + +**범위**: 112건 (as any 64 + : any 48) +**난이도**: 중간~높음 | **영향**: 타입 안전성 | **예상**: 3~4시간 +**참고**: 기존 로드맵 D-2에서 "보류 결정"된 항목 + +### 검토 결론: 전체 진행 비추, 선별 처리 권장 + +| 구분 | 건수 | 지금 가능? | 효과 | 판단 | +|------|------|-----------|------|------| +| form errors `(errors as any).field` | 23건 | 가능 | 중간 — 타입 안전성 향상, 현재 런타임 에러 없음 | 품목 폼 복잡하여 확인 범위 넓음, 투자 대비 효과 애매 | +| onValueChange `(v: any)` → `(v: string)` | 8건 | 가능 | 낮음 — 기계적 교체 5분 | **할 만함** (다른 작업과 묶어서) | +| API 응답/동적 접근 | 81건 | **불가** | 백엔드 API 타입 정의 선행 필요 | `any` → `unknown`은 의미 없음 | + +### A. onValueChange 콜백 (8건) — 다음 세션에서 빠르게 처리 가능 + +**문제**: `(v: any) => ...` 패턴이 Radix Select onValueChange에서 반복 +**해결**: `(v: string) => ...` 로 교체 (Radix Select는 항상 string 반환) + +#### 대상 파일: +- [ ] `src/components/items/ItemMasterDataManagement/dialogs/OptionDialog.tsx:164` +- [ ] `src/components/items/ItemMasterDataManagement/dialogs/MasterFieldDialog.tsx:174` +- [ ] `src/components/items/ItemMasterDataManagement/dialogs/PageDialog.tsx:87` +- [ ] `src/components/items/ItemMasterDataManagement/dialogs/TemplateFieldDialog.tsx:288` +- [ ] `src/components/items/ItemMasterDataManagement/dialogs/FieldDrawer.tsx:326` + +### B. form errors 패턴 (23건) — 보류, 필요 시 진행 + +**문제**: `(errors as any).fieldName` 패턴이 ItemForm 계열에서 반복 +**해결**: FormState 타입을 제네릭으로 정의 +**보류 사유**: 현재 런타임 에러 없음, ItemForm 계열 복잡도 높아 확인 범위 넓음 + +#### 대상 파일: +- [ ] `src/components/items/ItemForm/forms/PartForm.tsx` (4건) +- [ ] `src/components/items/ItemForm/forms/parts/PurchasedPartForm.tsx` (12건) +- [ ] `src/components/items/ItemForm/forms/parts/BendingPartForm.tsx` (7건) + +### C. API 응답/동적 접근 (81건) — 백엔드 타입 정의 후 진행 + +백엔드 API 타입 정의가 선행되어야 하므로 현 시점에서 처리 불가. + +--- + +## 화면 검수 결과 (Chrome DevTools MCP) ✅ 완료 (2026-02-19) + +**방법**: Chrome DevTools MCP 연동하여 localhost:3000 실시간 화면 검증 + +### ConfirmDialog 화면 검수 (5개 화면) + +| 화면 | 트리거 | 결과 | 다이얼로그 제목/설명 | +|------|--------|------|---------------------| +| 작업지시 수정 (`/production/work-orders/68?mode=edit`) | 품목 삭제 버튼 | **정상** | "품목 삭제 / 이 품목을 삭제하시겠습니까?" | +| 전자결재 문서작성 (`/approval/draft/new`) | 삭제 버튼 | **정상** | "문서 삭제 / 작성 중인 문서를 삭제하시겠습니까?" | +| 품목기준관리 계층구조 — 페이지 삭제 | 삭제 버튼 | **정상** | "섹션 삭제 / 이 섹션과 모든 하위섹션, 항목을 삭제하시겠습니까?" | +| 품목기준관리 계층구조 — 항목 연결 해제 | 섹션에서 연결 해제 | **정상** | "항목 연결 해제 / 이 항목을 섹션에서 연결 해제하시겠습니까?" | +| 품목기준관리 계층구조 — 섹션 연결 해제 | 페이지에서 연결 해제 | **정상** | "섹션 연결 해제 / 이 섹션을 페이지에서 연결 해제하시겠습니까?" | +| 게시판 목록 — 게시글 삭제 | — | **미테스트** | 게시글 0건으로 삭제 트리거 불가 | +| BOM 관리 — 품목 삭제 | — | **미테스트** | BOM 데이터 접근 경로 한정 (동일 `DeleteConfirmDialog` 컴포넌트) | + +### Tiptap 동적 로딩 화면 검수 (3개 화면) + +| 화면 | 결과 | 비고 | +|------|------|------| +| 팝업관리 수정 (`/settings/popup-management/10?mode=edit`) | **정상** | 전체 툴바 + 기존 콘텐츠 "QA 테스트용 팝업입니다." 정상 표시 | +| 고객센터 1:1 문의 등록 (`/customer-center/qna/create`) | **정상** | 전체 툴바 + placeholder "내용을 입력해주세요" 정상 표시 | +| 게시판 글쓰기 (`/board/{boardCode}?mode=new`) | **미테스트** | 기존 `authorId` 참조 버그로 페이지 크래시 (WP-1 변경과 무관한 기존 이슈) | + +### GenericPageSkeleton 화면 검수 (WP-5, 44개 페이지) + +**방법**: Slow 3G 네트워크 에뮬레이션으로 로딩 상태 캡처 + +| 화면 | 뷰 | 결과 | 비고 | +|------|-----|------|------| +| 거래처관리 (`/accounting/vendors`) | Desktop | **정상** | 제목+카드+테이블 스켈레톤 | +| 입금관리 (`/accounting/deposits`) | Desktop | **정상** | 동일 패턴 | +| 견적관리 (`/sales/quote-management`) | Desktop | **정상** | 동일 패턴 | +| 알림설정 (`/settings/notification-settings`) | Desktop | **정상** | 동일 패턴 | +| 공지사항 상세 (`/customer-center/notices/1`) | Desktop | **정상** | 동일 패턴 | +| 출금관리 (`/accounting/withdrawals`) | **Mobile (390x844)** | **정상** | 모바일 반응형 카드 스켈레톤 | +| 이슈관리 (`/construction/project/issue-management`) | Desktop | **정상** | import 버그 수정 후 정상 | + +### 화면 검수 중 발견/수정한 버그 (2건) + +| 파일 | 문제 | 원인 | 수정 | +|------|------|------|------| +| `construction/project/issue-management/page.tsx` | Build Error: `Expected ',', got '{'` | WP-5 스크립트가 `import type {` 블록 안에 `GenericPageSkeleton` import 삽입 | import 위치를 type import 블록 밖으로 이동 | +| `construction/project/construction-management/page.tsx` | 동일 에러 | 동일 원인 | 동일 수정 | + +### 검수 요약 +- **ConfirmDialog/Tiptap: 테스트 가능 항목 7건 모두 정상 통과** +- **GenericPageSkeleton: Desktop 5개 도메인 + Mobile 1건 정상 통과** +- 미테스트 3건은 데이터 부재/기존 버그로 인한 것이며, 동일 컴포넌트 패턴이므로 정상 동작 보장 +- `alertdialog` 역할(role)로 렌더링되어 접근성 표준 충족 +- **화면 검수 중 import 버그 2건 발견 → 즉시 수정 완료** + +--- + +## 보안 관련 참고사항 (조치 불필요) + +분석 결과 아래 항목들은 **이미 안전하게 처리되어 있음**: + +### ComputedField.tsx — `new Function()` ✅ 안전 +- 라인 37에서 정규식 검증: `if (!/^[\d\s+\-*/().]+$/.test(expression))` +- 숫자, 연산자, 괄호만 허용. 코드 주입 불가 + +### sanitizeHTML — DOMPurify ✅ 안전 +- `src/lib/sanitize.ts`에서 DOMPurify 사용 +- ALLOWED_TAGS/ATTR 화이트리스트 + FORBID_TAGS(script, iframe 등) 설정 +- 업계 표준 XSS 방지 + +### dangerouslySetInnerHTML — 전부 sanitizeHTML 통과 ✅ 안전 +- 6개 사용처 모두 `sanitizeHTML()` 적용 + +### CSP unsafe-inline/unsafe-eval — 현실적으로 유지 필요 +- Next.js 내부 인라인 스크립트 + Tailwind CSS 인라인 스타일 때문에 필요 +- nonce 기반 전환은 Next.js App Router에서 아직 실험적 기능 +- 현재 보안 수준: 폐쇄형 ERP + 인증 필수 → 리스크 수용 가능 + +--- + +## 실행 계획 요약 + +### 완료됨 ✅ (세션 1 + 이전 세션) + +| 작업 | 완료일 | 비고 | +|------|--------|------| +| WP-4: ThemeContext → themeStore 통합 | 2026-02-19 | themeStore DOM 클래스 버그도 함께 수정 | +| WP-6: itemStore 제거 | 2026-02-19 | logout 시 useItemMasterStore 초기화로 개선 | +| WP-2+3: utils 통합 + 패키지 제거 | 2026-02-19 | 49파일 import 변경, html2canvas/dom-to-image-more 삭제, Tiptap 동적 로딩 3건 | +| WP-1: alert/confirm 제거 | 2026-02-19 | alert 42건 전체 완료, confirm component-level 9건 완료 | +| WP-5: 로딩 UI Skeleton 통일 | 2026-02-19 | 44개 page.tsx → GenericPageSkeleton/DetailPageSkeleton | +| WP-7: TODO 정리 문서 | 2026-02-19 | 102건 분류, `claudedocs/dev/[REF-2026-02-19] todo-issue-tracker.md` | +| 화면 검수 (Chrome DevTools MCP) | 2026-02-19 | ConfirmDialog 5건 + Tiptap 2건 정상, 미테스트 3건은 데이터/기존버그 사유 | + +### 잔여 (후순위) + +| 작업 | 건수 | 비고 | +|------|------|------| +| WP-1 hook-level confirm | 5건 | hooks에서 JSX 렌더링 불가, callback 패턴 리팩토링 필요 | +| WP-1 dev 도구 confirm | 1건 | PageSelector, 낮은 우선순위 | +| WP-8-A: onValueChange `any` → `string` | 8건 | 5분 작업, 다른 작업과 묶어서 처리 가능 | +| WP-8-B: form errors `as any` 제거 | 23건 | 런타임 에러 없어 급하지 않음 | +| WP-8-C: API 응답 타입 | 81건 | 백엔드 API 타입 정의 선행 필요, 현 시점 불가 | diff --git a/claudedocs/_index.md b/claudedocs/_index.md index 7572266d..124f152a 100644 --- a/claudedocs/_index.md +++ b/claudedocs/_index.md @@ -531,7 +531,8 @@ claudedocs/ | `[IMPL-2026-02-05] formatter-commonization-plan.md` | formatter 공통화 계획 | | `[IMPL] IntegratedDetailTemplate-checklist.md` | 통합 상세 템플릿 체크리스트 | | `[REF] template-migration-status.md` | 템플릿 마이그레이션 현황 | -| **동적 필드 타입 확장** | | +| **동적 렌더링 플랫폼** | | +| `[VISION-2026-02-19] dynamic-rendering-platform-strategy.md` | 동적 렌더링 플랫폼 전략 (기준관리 기반 화면 자동 구성 비전) | | `[DESIGN-2026-02-11] dynamic-field-type-extension.md` | 동적 필드 타입 확장 설계서 (4-Level 구조) | | `[IMPL-2026-02-11] dynamic-field-components.md` | 동적 필드 컴포넌트 구현 기획서 (Phase 1~3 완료) | | **시스템 설계** | | diff --git a/claudedocs/architecture/[VISION-2026-02-19] dynamic-rendering-platform-strategy.md b/claudedocs/architecture/[VISION-2026-02-19] dynamic-rendering-platform-strategy.md new file mode 100644 index 00000000..69e29d3f --- /dev/null +++ b/claudedocs/architecture/[VISION-2026-02-19] dynamic-rendering-platform-strategy.md @@ -0,0 +1,224 @@ +# 동적 렌더링 플랫폼 전략 — 기준관리 기반 화면 자동 구성 + +> 작성일: 2026-02-19 +> 상태: 비전 정리 (논의 기반) +> 관련 기술 설계: `[DESIGN-2026-02-11] dynamic-field-type-extension.md` +> 관련 구현 현황: `[IMPL-2026-02-11] dynamic-field-components.md` +> 관련 로드맵: `item-master/[DESIGN-2025-12-12] item-master-form-builder-roadmap.md` + +--- + +## 1. 핵심 비전 + +``` +기준관리 페이지에서 설정 → API로 메타데이터 전달 → 프론트가 자동 렌더링 +``` + +**목표**: 개발자가 매번 화면을 코딩하는 것이 아니라, 기준관리 페이지에서 등록한 설정값에 따라 프론트엔드가 동적으로 화면을 구성하는 **ERP 커스터마이징 플랫폼**. + +--- + +## 2. 운영 워크플로우 비전 + +### 2.1 전체 흐름 + +``` +현장 방문 (영업자/매니저) + ├─ 녹음, 체크리스트, 문서 수집 + └─ → MD 파일 정리 (요구사항) + │ + ↓ +기준관리 페이지 (관리자/컨설턴트) + ├─ MD 보고 속성, 칼럼, 옵션 등록 + └─ → 메타데이터 저장 (DB) + │ + ↓ API + │ +프론트엔드 (자동 렌더링) + └─ 메타데이터 기반으로 동적 화면 구성 +``` + +### 2.2 역할 변화 + +| 역할 | 현재 | 비전 | +|------|------|------| +| 영업자/매니저 | 요구사항 전달 → 개발 대기 | 현장에서 MD 파일 작성 | +| 관리자/컨설턴트 | — | MD 보고 기준관리에 설정 입력 | +| **개발자** | **요구사항마다 화면 코딩** | **플랫폼 유지보수 + 새 블록 타입 추가 시에만 개입** | + +### 2.3 개발자 개입이 필요한 시점 + +- 기존 블록(Input, Select, DatePicker 등)으로 조합 가능 → **개발자 불필요** +- 새로운 입력 타입/계산 로직 필요 → **블록 1개 추가** → 이후 재사용 +- 기준관리 UI 자체 개선 → **설계/검증** +- page-builder 고도화 → **설계/구현** + +--- + +## 3. 현재 자산 현황 + +### 3.1 이미 있는 것 + +#### UI 블록 (공통 컴포넌트) +``` +src/components/ui/ + ├─ Input, NumberInput, QuantityInput, CurrencyInput + ├─ Select, Checkbox, DatePicker, Textarea + ├─ Button, Badge, Card, Dialog + └─ ... +``` +모든 도메인별 테이블이 이 공통 블록을 사용 중. + +#### 동적 필드 시스템 (14종 완성) +``` +DynamicItemForm/fields/ + ├─ 기존 6종: textbox, number, dropdown, checkbox, date, textarea + └─ 신규 8종: reference, multi-select, file, currency, unit-value, radio, toggle, computed +``` +Phase 1~3 프론트 구현 완료 (백엔드 작업 대기). + +#### 범용 테이블 섹션 +``` +DynamicTableSection — config 기반 칼럼 정의, 행 CRUD, 요약행 +TableCellRenderer — 테이블 셀 = DynamicFieldRenderer 재사용 +``` + +#### 속성 관리 시스템 (품목기준관리) +``` +useAttributeManagement — 속성 옵션 상태 관리 +AttributeTabContent — 동적 탭 렌더링 +OptionColumn[] + MasterOption[] — 메타데이터 구조 +``` + +#### page-builder 프로토타입 +``` +/dev/page-builder — 드래그앤드롭, 섹션/필드 구성, Undo/Redo, 반응형 뷰포트 +``` + +### 3.2 현재 구조: "기준관리 → 동적 렌더링" 패턴 + +``` +품목기준관리 (Admin) 품목 등록 (User) +ItemMasterDataManagement.tsx DynamicItemForm/index.tsx + ↓ 설정 (pages/sections/fields) ↓ 읽어서 렌더링 + DB에 메타데이터 저장 DynamicFieldRenderer (14종 switch) + DynamicTableSection (config 기반) +``` + +**이 패턴이 핵심이고, 다른 도메인에도 동일하게 확장하는 것이 비전.** + +--- + +## 4. 확장 대상 분석 + +### 4.1 도메인별 동적 렌더링 적합성 + +| 도메인 | 적합도 | 이유 | +|--------|:---:|------| +| 품목기준관리 | ✅ 이미 적용 | 테넌트/업종별 관리 항목이 다름 | +| 설비/자산 관리 | ✅ 높음 | 설비 종류별 관리 속성이 다름 | +| 거래처 관리 | ✅ 높음 | 업종별 추가 정보 다름 | +| 공정/라우팅 관리 | ✅ 높음 | 제조 방식별 공정 구성 다름 | +| 검사 항목 관리 | ✅ 높음 | 품목별 검사 항목/기준 다름 | +| 견적서/발주서 | 🟡 부분 | 테이블은 동적 가능, 비즈니스 로직은 고정 | +| 세금계산서 | ❌ 낮음 | 법정 양식, 테넌트별 차이 없음 | +| 대시보드 | ❌ 낮음 | 위젯 기반이 더 적합 | + +### 4.2 편집 가능 테이블 현황 + +| 컴포넌트 | 공통 컴포넌트 사용 | 자동 계산 | 합계 행 | +|---------|:---:|:---:|:---:| +| EditableTable (공통) | 본인이 공통 | ❌ | ❌ | +| TaxInvoiceItemTable | ❌ 개별 | ✅ | ✅ | +| OrderDetailItemTable | ❌ 개별 | ❌ | ✅ | +| EstimateDetailTableSection | ❌ 개별 | ✅ (복잡) | ✅ | +| DynamicTableSection | ❌ 개별 (config 기반) | ✅ (요약) | ✅ | + +**테이블 안의 부품(Input, Select 등)은 전부 공통 ui 컴포넌트 사용.** +껍데기(테이블 구조, 계산 로직)만 각자 구현. + +--- + +## 5. page-builder 갭 분석 + +### 5.1 현재 page-builder 상태 + +``` +/dev/page-builder (프로토타입) + ✅ 드래그앤드롭 (섹션/필드 → 캔버스) + ✅ 섹션 타입 (BASIC, BOM, CUSTOM) + ✅ 필드 타입 (기본 6종) + ✅ 조건부 표시 (DisplayCondition) + ✅ 검증 규칙 (ValidationRule) + ✅ BOM 테이블 + ✅ 마스터 필드 연동 + ✅ Undo/Redo 히스토리 + ✅ 반응형 뷰포트 (desktop/tablet/mobile) + ✅ API 변환 타입 정의 +``` + +### 5.2 비전 대비 부족한 점 + +| 항목 | 현재 | 필요 | +|------|------|------| +| 대상 도메인 | 품목 전용 (ItemType: FG/PT/SM/RM/CS) | 모든 기준관리 | +| 사용자 | 개발자용 프로토타입 | 비개발자(영업/매니저/관리자) | +| 테이블 섹션 | BOM만 (고정 칼럼) | 동적 칼럼 + 행 CRUD (DynamicTableSection 연결) | +| 신규 필드 타입 | 기본 6종만 | 14종 전체 반영 | +| API 연동 | 타입만 정의 | 실제 저장/조회 | +| 프리셋 | 하드코딩 | 산업별 섹션 프리셋 선택 | + +### 5.3 고도화 방향 + +``` +1단계: 도메인 범용화 + - ItemType 종속 제거 + - "기준관리 도메인" 선택 → 해당 도메인의 페이지 구성 + +2단계: 14종 필드 타입 반영 + - ComponentPalette에 신규 8종 필드 추가 + - PropertyPanel에 각 필드별 config 편집 UI + +3단계: DynamicTableSection 연결 + - BOM 외 범용 테이블 섹션 지원 + - 칼럼 정의 UI (타입/너비/필수 설정) + +4단계: 비개발자 UX + - 용어 단순화 (field_type → "입력 형태") + - 미리보기 강화 + - 저장/불러오기 +``` + +--- + +## 6. 4-Level 아키텍처 요약 + +기존 기술 설계서(`[DESIGN-2026-02-11]`)의 핵심: + +``` +Level 1: 필드 타입 컴포넌트 (14종) — 코드 레벨, 거의 안 바뀜 +Level 2: properties config (JSON) — 설정 레벨, 코드 변경 없음 +Level 3: 섹션 프리셋 (JSON) — 템플릿 레벨, 코드 변경 없음 +Level 4: reference sources (API URL) — 연결 레벨, 코드 변경 없음 +``` + +**새 산업 진출 시에도 프론트엔드 코드 변경 = 0줄.** +백엔드 API + config JSON만 추가. + +--- + +## 7. 관련 문서 + +| 문서 | 위치 | 내용 | +|------|------|------| +| 동적 필드 타입 확장 설계 | `architecture/[DESIGN-2026-02-11]` | 4-Level 구조, 14종 필드, 범용 테이블, 산업별 확장 | +| 동적 필드 컴포넌트 구현 | `architecture/[IMPL-2026-02-11]` | Phase 1~3 프론트 구현 완료 상태 | +| Form Builder 로드맵 | `item-master/[DESIGN-2025-12-12]` | Low-Code Form Builder 초기 로드맵 | +| 백엔드 API 스펙 | `item-master/[API-REQUEST-2026-02-12]` | 동적 필드 타입 백엔드 API 요청서 | +| page-builder 참조 | `dev/[REF] page-builder-implementation.md` | 페이지 빌더 구현 참조 | +| 멀티테넌시 최적화 | `architecture/[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md` | 테넌트별 격리/최적화 | + +--- + +**문서 버전**: 1.0 +**마지막 업데이트**: 2026-02-19 diff --git a/claudedocs/dev/[REF-2026-02-19] todo-issue-tracker.md b/claudedocs/dev/[REF-2026-02-19] todo-issue-tracker.md new file mode 100644 index 00000000..c1b87622 --- /dev/null +++ b/claudedocs/dev/[REF-2026-02-19] todo-issue-tracker.md @@ -0,0 +1,342 @@ +# SAM ERP TODO/FIXME 이슈 트래커 + +> 자동 생성: 2026-02-19 +> 총 건수: 102건 +> 분류: A~F 카테고리 +> 검색 범위: `src/` 디렉토리 내 `*.ts`, `*.tsx` 파일 + +--- + +## 요약 + +| 카테고리 | 건수 | 담당 | 비고 | +|----------|------|------|------| +| A. 백엔드 API 연동 대기 | 55 | 백엔드팀 | API 완성 시 해결 | +| B. 백엔드 필드 추가 대기 | 10 | 백엔드팀 | DB/API 스키마 추가 | +| C. UI/기능 구현 대기 | 16 | 프론트팀 | 백로그 | +| D. Phase 2 / 장기 과제 | 10 | 전체 | 로드맵 반영 | +| E. CEO 대시보드 목업 데이터 | 4 | 프론트팀 | 대시보드 API 연동 시 | +| F. 기타 | 7 | - | - | + +--- + +## A. 백엔드 API 연동 대기 (55건) + +### 모듈별 분류 + +#### 품목기준관리 (Item Master Store) - 16건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/stores/item-master/useItemMasterStore.ts` | 97 | `// TODO: API 호출` (createPage - 페이지 생성) | +| `src/stores/item-master/useItemMasterStore.ts` | 160 | `// TODO: API 호출` (updatePage - 페이지 수정) | +| `src/stores/item-master/useItemMasterStore.ts` | 214 | `// TODO: API 호출` (deletePage - 페이지 삭제) | +| `src/stores/item-master/useItemMasterStore.ts` | 289 | `// TODO: API 호출` (createSection - 섹션 생성) | +| `src/stores/item-master/useItemMasterStore.ts` | 335 | `// TODO: API 호출` (updateSection - 섹션 수정) | +| `src/stores/item-master/useItemMasterStore.ts` | 370 | `// TODO: API 호출` (deleteSection - 섹션 삭제) | +| `src/stores/item-master/useItemMasterStore.ts` | 423 | `// TODO: API 호출하여 서버에도 순서 저장` (reorderSections) | +| `src/stores/item-master/useItemMasterStore.ts` | 460 | `// TODO: API 호출` (createField - 필드 생성) | +| `src/stores/item-master/useItemMasterStore.ts` | 522 | `// TODO: API 호출` (updateField - 필드 수정) | +| `src/stores/item-master/useItemMasterStore.ts` | 552 | `// TODO: API 호출` (deleteField - 필드 삭제) | +| `src/stores/item-master/useItemMasterStore.ts` | 587 | `// TODO: API 호출` (updateFieldOptions - 필드 옵션 수정) | +| `src/stores/item-master/useItemMasterStore.ts` | 640 | `// TODO: API 호출하여 서버에도 순서 저장` (reorderFields) | +| `src/stores/item-master/useItemMasterStore.ts` | 677 | `// TODO: API 호출` (createTab - 탭 생성) | +| `src/stores/item-master/useItemMasterStore.ts` | 707 | `// TODO: API 호출` (updateTab - 탭 수정) | +| `src/stores/item-master/useItemMasterStore.ts` | 726 | `// TODO: API 호출` (deleteTab - 탭 삭제) | +| `src/components/items/ItemMasterDataManagement/hooks/usePageManagement.ts` | 157 | `// TODO: 원본 페이지의 섹션들도 복제 필요 (별도 API 호출)` | + +#### 회계 (Accounting) - 19건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/components/accounting/GiftCertificateManagement/actions.ts` | 32 | `// TODO: 실제 API 연동 시 교체` (getGiftCertificates) | +| `src/components/accounting/GiftCertificateManagement/actions.ts` | 45 | `// TODO: 실제 API 연동 시 교체` (getGiftCertificateDetail) | +| `src/components/accounting/GiftCertificateManagement/actions.ts` | 58 | `// TODO: 실제 API 연동 시 교체` (createGiftCertificate) | +| `src/components/accounting/GiftCertificateManagement/actions.ts` | 86 | `// TODO: 실제 API 연동 시 교체` (updateGiftCertificate) | +| `src/components/accounting/GiftCertificateManagement/actions.ts` | 113 | `// TODO: 실제 API 연동 시 교체` (deleteGiftCertificate) | +| `src/components/accounting/GiftCertificateManagement/actions.ts` | 136 | `// TODO: 실제 API 연동 시 교체` (useGiftCertificate) | +| `src/components/accounting/TaxInvoiceManagement/actions.ts` | 24 | `// TODO: 실제 API 연동 시 Mock 제거` (Mock 데이터) | +| `src/components/accounting/TaxInvoiceManagement/actions.ts` | 44 | `// TODO: 실제 API 연동 시 아래 코드로 교체` (getTaxInvoices) | +| `src/components/accounting/TaxInvoiceManagement/actions.ts` | 66 | `// TODO: 실제 API 연동 시 아래 코드로 교체` (getTaxInvoiceDetail) | +| `src/components/accounting/TaxInvoiceManagement/actions.ts` | 99 | `// TODO: 실제 API 연동 시 Mock 제거` (세금계산서 상세) | +| `src/components/accounting/TaxInvoiceManagement/actions.ts` | 115 | `// TODO: 실제 API 연동 시 아래 코드로 교체` (deleteTaxInvoice) | +| `src/components/accounting/TaxInvoiceIssuance/actions.ts` | 36 | `// TODO: 실제 API 연동 시 아래 코드로 교체` (getTaxInvoices) | +| `src/components/accounting/TaxInvoiceIssuance/actions.ts` | 59 | `// TODO: 실제 API 연동 시 아래 코드로 교체` (getTaxInvoiceDetail) | +| `src/components/accounting/TaxInvoiceIssuance/actions.ts` | 86 | `// TODO: 실제 API 연동 시 아래 코드로 교체` (issueTaxInvoice) | +| `src/components/accounting/TaxInvoiceIssuance/actions.ts` | 112 | `// TODO: 실제 API 연동 시 아래 코드로 교체` (sendEmail) | +| `src/components/accounting/TaxInvoiceIssuance/actions.ts` | 127 | `// TODO: 실제 API 연동 시 교체` (cancelTaxInvoice) | +| `src/components/accounting/TaxInvoiceIssuance/index.tsx` | 184 | `// TODO: 실제 API 연동 시 필터 조건으로 getTaxInvoices 호출` | +| `src/components/accounting/PurchaseManagement/index.tsx` | 232 | `// TODO: API 호출로 저장` | +| `src/components/accounting/SalesManagement/index.tsx` | 270 | `// TODO: API 호출로 저장` | + +#### 건설/사업 (Construction/Business) - 9건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/components/business/construction/progress-billing/actions.ts` | 196 | `// TODO: 실제 API 호출로 대체` (getProgressBillings) | +| `src/components/business/construction/progress-billing/actions.ts` | 230 | `// TODO: 실제 API 호출로 대체` (getProgressBillingDetail) | +| `src/components/business/construction/progress-billing/actions.ts` | 263 | `// TODO: 실제 API 호출로 대체` (saveProgressBilling) | +| `src/components/business/construction/progress-billing/actions.ts` | 292 | `// TODO: 실제 API 호출로 대체` (deleteProgressBilling) | +| `src/components/business/construction/progress-billing/actions.ts` | 300 | `// TODO: 매출 자동 등록 API 호출` | +| `src/components/business/construction/progress-billing/hooks/useProgressBillingDetailForm.ts` | 92 | `// TODO: API 호출` (handleSave) | +| `src/components/business/construction/progress-billing/hooks/useProgressBillingDetailForm.ts` | 111 | `// TODO: API 호출` (handleDelete) | +| `src/components/business/construction/progress-billing/ProgressBillingDetailForm.tsx` | 77 | `// TODO: API 호출` (handleDelete) | +| `src/components/business/construction/management/actions.ts` | 20 | `// TODO: 실제 API 연동 시 구현` (프로젝트 관리 전체) | + +#### 건설/현장관리 (Site Management) - 4건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/components/business/construction/structure-review/StructureReviewDetailForm.tsx` | 154 | `// TODO: API 연동` (handleSave) | +| `src/components/business/construction/structure-review/StructureReviewDetailClientV2.tsx` | 76 | `// TODO: API 연동` (handleSave) | +| `src/components/business/construction/site-management/SiteDetailForm.tsx` | 156 | `// TODO: API 연동` (handleSave) | +| `src/components/business/construction/site-management/SiteDetailClientV2.tsx` | 68 | `// TODO: API 연동` (handleSave) | + +#### HR (인사관리) - 2건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/app/[locale]/(protected)/hr/documents/page.tsx` | 73 | `// TODO: 백엔드 API 구현 필요` (인사서류함) | +| `src/app/[locale]/(protected)/hr/documents/new/page.tsx` | 91 | `// TODO: 백엔드 API 구현 필요` (인사서류 신규등록) | + +#### 생산 (Production) - 2건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/components/production/WorkOrders/WipProductionModal.tsx` | 106 | `// TODO: API 연동` (WIP 생산 모달) | +| `src/app/[locale]/(protected)/quality/qms/components/InspectionModal.tsx` | 340 | `// TODO: 실제 저장 API 연동` (품질검사 저장) | + +#### 게시판 (Board) - 2건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/components/board/BoardDetail/index.tsx` | 85 | `// TODO: 댓글 API 연동 (별도 작업)` | +| `src/app/[locale]/(protected)/board/[boardCode]/[postId]/page.tsx` | 45 | `// TODO: 댓글 API 호출 추가` | + +#### 설정 (Settings) - 1건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/components/settings/AttendanceSettingsManagement/index.tsx` | 18 | `// TODO: API 연동 시 작업 사항 - GET/PUT /api/settings/attendance` | + +--- + +## B. 백엔드 필드 추가 대기 (10건) + +### 모듈별 분류 + +#### 대시보드 트랜스포머 - 3건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/lib/api/dashboard/transformers.ts` | 261 | `// TODO: 백엔드 daily_change 필드 제공 시 더미값 제거` | +| `src/lib/api/dashboard/transformers.ts` | 427 | `// TODO: 백엔드 per-card sub_label/count 제공 시 더미값 제거` | +| `src/lib/api/dashboard/transformers.ts` | 654 | `// TODO: 백엔드 sub_label 필드 제공 시 더미값 제거` | + +#### 공정관리 (Process Management) - 3건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/components/process-management/actions.ts` | 486 | `// TODO: API 응답에 process_name, process_category 필드 추가 후 활성화` | +| `src/components/process-management/RuleModal.tsx` | 285 | `// TODO: API에서 process_name, process_category 필드 지원 후 실제 데이터 표시` | +| `src/components/process-management/RuleModal.tsx` | 322 | `// TODO: API 지원 후 item.processName / item.processCategory 표시` | + +#### 생산 (Production) - 2건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/components/production/WorkOrders/WorkOrderList.tsx` | 181 | `// TODO: API에서 긴급 건수 제공 시 연동` | +| `src/components/production/WorkOrders/WorkOrderList.tsx` | 187 | `// TODO: API에서 지연 건수 제공 시 연동` | + +#### 게시판 (Board) - 2건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/components/board/BoardManagement/BoardForm.tsx` | 24 | `// TODO: API에서 부서 목록 가져오기` | +| `src/components/board/BoardManagement/BoardForm.tsx` | 33 | `// TODO: API에서 권한 목록 가져오기` | + +--- + +## C. UI/기능 구현 대기 (16건) + +### 모듈별 분류 + +#### 다운로드/출력 기능 - 4건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/components/accounting/BadDebtCollection/BadDebtDetail.tsx` | 268 | `// TODO: 실제 다운로드 로직` | +| `src/app/[locale]/(protected)/quality/qms/components/Day1DocumentSection.tsx` | 149 | `// TODO: 다운로드 기능` | +| `src/app/[locale]/(protected)/quality/qms/components/InspectionModal.tsx` | 472 | `// 다운로드 핸들러 (TODO: 실제 구현)` | +| `src/components/material/ReceivingManagement/ReceivingReceiptDialog.tsx` | 23 | `// TODO: PDF 다운로드 기능` | + +#### 품목 (Items) - 4건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/components/items/ItemForm/BOMSection.tsx` | 115 | `// TODO: 실제 itemMasters 데이터로 교체 필요` | +| `src/components/items/ItemForm/BOMSection.tsx` | 196 | `// TODO: 품목 선택 시 데이터 채우기 로직` | +| `src/components/items/ItemForm/BOMSection.tsx` | 209 | `// TODO: pricing에서 가져오기` (unitPrice) | +| `src/components/items/ItemForm/hooks/useBOMManagement.ts` | 89 | `// TODO: pricing에서 가져오기` (unitPrice) | + +#### 생산 (Production) - 3건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/components/production/WorkOrders/WorkOrderEdit.tsx` | 277 | `// TODO: API 호출로 서버에 상태 저장` | +| `src/components/production/WorkOrders/WorkOrderEdit.tsx` | 310 | `// TODO: API 호출로 서버에 저장` | +| `src/components/production/WorkOrders/WorkOrderEdit.tsx` | 327 | `// TODO: API 호출로 서버에서 삭제` | + +#### 기타 기능 - 5건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/components/approval/DocumentDetail/index.tsx` | 161 | `// TODO: 공유 기능 추가 예정 - PDF 다운로드, 이메일, 팩스, 카카오톡 공유` | +| `src/components/pricing/PricingListClient.tsx` | 177 | `// TODO: 이력 다이얼로그 열기` | +| `src/components/pricing/PricingListClient.tsx` | 335 | `// TODO: API 연동 시 품목 마스터 동기화 로직 구현` | +| `src/components/hr/SalaryManagement/index.tsx` | 253 | `// TODO: 지급항목 추가 다이얼로그 또는 로직 구현` | +| `src/components/production/WorkResults/WorkResultList.tsx` | 70 | `// TODO: 상세 보기 기능 구현` | + +--- + +## D. Phase 2 / 장기 과제 (10건) + +### 모듈별 분류 + +#### 품목코드 생성 로직 개선 - 3건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/components/items/DynamicItemForm/utils/itemCodeGenerator.ts` | 11 | `// TODO: 추후 백엔드 API 또는 품목기준관리에서 설정 가능하도록 변경` | +| `src/components/items/DynamicItemForm/utils/itemCodeGenerator.ts` | 52 | `// TODO: 추후 품목기준관리에서 설정 가능하도록 변경` | +| `src/components/items/DynamicItemForm/utils/itemCodeGenerator.ts` | 98 | `// TODO: 추후 품목기준관리에서 설정 가능하도록 변경` | + +#### 품목기준관리 하드코딩 대체 - 2건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/components/items/ItemMasterDataManagement/hooks/useTabManagement.ts` | 104 | `// TODO: 나중에 백엔드에서 기준값 로드로 대체 예정` | +| `src/components/items/ItemMasterDataManagement/hooks/useAttributeManagement.ts` | 80 | `// TODO: 나중에 백엔드 API로 대체` (속성 옵션 하드코딩) | + +#### 프로덕션 배포 준비 - 2건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/lib/api/toast-utils.ts` | 14 | `// TODO: 프로덕션 배포 시 false로 변경하거나 환경변수 사용` (SHOW_ERROR_CODE) | +| `src/lib/api/error-handler.ts` | 112 | `// TODO: 프로덕션 배포 시 false로 변경하거나 환경변수 사용` (SHOW_ERROR_CODE) | + +#### Phase 2 명시 - 2건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/lib/api/item-master.ts` | 99 | `// TODO: Phase 2에서 구현` | +| `src/components/document-system/viewer/DocumentViewer.tsx` | 314 | `// TODO: BlockRenderer 구현 시 연결` (Phase 2 블록 렌더링) | + +#### 기타 장기 - 1건 + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/lib/api/items.ts` | 26 | `// TODO: 실제 인증 구현에 맞게 수정 필요` (getAuthToken) | + +--- + +## E. CEO 대시보드 목업 데이터 (4건) + +| 파일 | 라인 | TODO 내용 | +|------|------|----------| +| `src/components/business/CEODashboard/mockData.ts` | 5 | `// TODO: API 연동 시 이 파일을 API 호출로 대체` | +| `src/components/business/CEODashboard/CEODashboard.tsx` | 253 | `// TODO: API 호출하여 일정 저장` | +| `src/components/business/CEODashboard/CEODashboard.tsx` | 260 | `// TODO: API 호출하여 일정 삭제` | +| `src/components/business/CEODashboard/sections/TodayIssueSection.tsx` | 411 | `// TODO: 버튼 - API 구현 후 활성화` (승인/반려 버튼) | + +--- + +## F. 기타 (7건) + +| 파일 | 라인 | TODO 내용 | 비고 | +|------|------|----------|------| +| `src/contexts/ItemMasterContext.tsx` | 1894 | `// TODO: 전체 init 데이터 새로고침 기능 구현 필요` | 리팩토링 | +| `src/components/document-system/configs/index.ts` | 10 | `// TODO: Orders Configs` | 설정 추가 대기 | +| `src/components/quotes/types.ts` | 737 | `// TODO: 동적으로 결정` (productCategory) | 로직 개선 | +| `src/components/quotes/types.ts` | 875 | `// TODO: 동적으로 결정` (product_category) | 로직 개선 | +| `src/components/production/WorkOrders/types.ts` | 516 | `// TODO: 실제 단계 추적 필요` (in_progress 상태) | 로직 개선 | +| `src/components/items/ItemListClient.tsx` | 328 | `// TODO: 실제 API 호출로 데이터 저장` | API 연동 겸 로직 | +| `src/components/business/construction/management/ProjectListClient.tsx` | 127 | `// TODO: 실제 API 연동 시 new Date()로 변경` (목업 데이터 임시 설정) | 목업 제거 | +| `src/components/accounting/BadDebtCollection/BadDebtDetail.tsx` | 53 | `// TODO: API에서 조회` (담당자 목록 하드코딩) | API 연동 겸 데이터 | +| `src/components/settings/CompanyInfoManagement/AddCompanyDialog.tsx` | 60 | `// TODO: 바로빌 API 연동` (외부 API) | 외부 서비스 연동 | +| `src/components/accounting/SalesManagement/SalesDetail.tsx` | 500 | `// TODO: 거래명세서 조회 기능 연결` | 기능 연결 | +| `src/app/[locale]/(protected)/hr/employee-management/csv-upload/page.tsx` | 8 | `// TODO: API 연동` (CSV 업로드) | API 연동 | +| `src/app/[locale]/(protected)/hr/attendance/page.tsx` | 67 | `// TODO: 주소/좌표 설정 UI 추가 후 아래 주석 해제` | UI 추가 후 | +| `src/components/business/construction/estimates/sections/EstimateDetailTableSection.tsx` | 224 | `// TODO: 견적 상세 기획서 수정 후 초기화 버튼 및 테이블 항목/데이터 재작업 필요` | 기획 대기 | +| `src/components/material/ReceivingManagement/InspectionCreate.tsx` | 164 | `// TODO: API 호출` (검사 생성) | API 연동 | +| `src/components/process-management/actions.ts` | 503 | `// TODO: 백엔드 API 수정 요청 - process_name, process_category 필드 추가` | 백엔드 요청 문서 | + +> 참고: F 카테고리는 A~E에 명확히 분류되지 않는 항목을 포함합니다. 일부는 API 연동과 겹치지만 외부 서비스, 기획 대기, 로직 개선 등 복합적인 성격을 가집니다. + +--- + +## 백엔드 팀 전달 요약 (A + B = 65건) + +### 우선순위별 정리 + +#### 즉시 필요 (Critical) - 완전 Mock 상태 + +아래 모듈은 **전체 API가 Mock/로컬로 구현**되어 있어 백엔드 API 완성 즉시 교체 필요: + +| 모듈 | 건수 | actions.ts 파일 | +|------|------|-----------------| +| 상품권 관리 | 6 | `accounting/GiftCertificateManagement/actions.ts` | +| 세금계산서 관리 | 5 | `accounting/TaxInvoiceManagement/actions.ts` | +| 세금계산서 발행 | 6 | `accounting/TaxInvoiceIssuance/actions.ts` | +| 기성관리 | 5+3 | `construction/progress-billing/actions.ts` + hooks | +| 프로젝트관리 | 1 | `construction/management/actions.ts` (전체 Mock) | +| 품목기준관리 스토어 | 16 | `stores/item-master/useItemMasterStore.ts` | + +**소계: 42건** - 이 모듈들은 프론트엔드 UI는 완성되었으나 백엔드 API 미구현 상태 + +#### 필드 추가 요청 (Important) + +| 요청 대상 | 추가 필드 | 관련 파일 | +|-----------|----------|----------| +| CEO 대시보드 API | `daily_change`, `sub_label`, `count` | `lib/api/dashboard/transformers.ts` | +| 공정관리 품목 API | `process_name`, `process_category` | `process-management/actions.ts`, `RuleModal.tsx` | +| 작업지시 API | 긴급 건수, 지연 건수 | `WorkOrders/WorkOrderList.tsx` | +| 게시판 관리 API | 부서 목록, 권한 목록 | `board/BoardManagement/BoardForm.tsx` | + +**소계: 10건** + +#### 신규 API 필요 (Normal) + +| API | 용도 | 관련 파일 | +|-----|------|----------| +| 인사서류함 CRUD | HR 문서 관리 | `hr/documents/page.tsx`, `new/page.tsx` | +| 댓글 CRUD | 게시판 댓글 | `BoardDetail/index.tsx`, `[postId]/page.tsx` | +| 출퇴근 설정 | GET/PUT | `AttendanceSettingsManagement/index.tsx` | +| 건설 현장관리 저장 | 현장/구조검토 | `SiteDetailForm.tsx`, `StructureReviewDetailForm.tsx` | +| WIP 생산 | 생산 모달 | `WipProductionModal.tsx` | +| 품질검사 저장 | 검사 결과 | `InspectionModal.tsx` | + +**소계: 13건** + +--- + +## 통계 요약 + +``` +전체 TODO: 102건 +├── 백엔드 의존: 65건 (63.7%) ← A + B +├── 프론트 백로그: 16건 (15.7%) ← C +├── 장기 과제: 10건 (9.8%) ← D +├── 대시보드 목업: 4건 (3.9%) ← E +└── 기타: 7건 (6.9%) ← F + +프론트 독립 해결 가능: 26건 (C + D 일부) +백엔드 완성 필요: 69건 (A + B + E) +``` + +--- + +> **주의사항** +> - `src/app/[locale]/(protected)/dev/dashboard/_components/AIPoweredDashboard.tsx`의 `TODO_ITEMS`는 변수명이며 실제 TODO 주석이 아니므로 제외 +> - 일부 항목은 복합 성격(API 연동 + UI 구현)을 가지며, 주된 블로커 기준으로 분류 +> - 이 문서는 2026-02-19 기준 스냅샷이며, 코드 변경에 따라 갱신 필요 diff --git a/package.json b/package.json index ea32fa11..be5dfdd0 100644 --- a/package.json +++ b/package.json @@ -49,9 +49,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", - "dom-to-image-more": "^3.7.2", "dompurify": "^3.3.1", - "html2canvas": "^1.4.1", "immer": "^11.0.1", "jspdf": "^4.0.0", "lucide-react": "^0.552.0", diff --git a/src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx b/src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx index 9f148fbf..98707d18 100644 --- a/src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx +++ b/src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx @@ -12,6 +12,7 @@ import { useSearchParams } from 'next/navigation'; import { BadDebtCollection, BadDebtDetailClientV2 } from '@/components/accounting/BadDebtCollection'; import { getBadDebts, getBadDebtSummary } from '@/components/accounting/BadDebtCollection/actions'; import type { BadDebtSummary } from '@/components/accounting/BadDebtCollection/types'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; const DEFAULT_SUMMARY: BadDebtSummary = { totalCount: 0, @@ -49,13 +50,7 @@ export default function BadDebtCollectionPage() { return ; } - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; return ( ; } - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; return ( setIsLoading(false)); }, [mode]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; // mode=new일 때 등록 화면 표시 if (mode === 'new') { diff --git a/src/app/[locale]/(protected)/accounting/expected-expenses/page.tsx b/src/app/[locale]/(protected)/accounting/expected-expenses/page.tsx index a9ae932e..b2073dce 100644 --- a/src/app/[locale]/(protected)/accounting/expected-expenses/page.tsx +++ b/src/app/[locale]/(protected)/accounting/expected-expenses/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'; import { ExpectedExpenseManagement } from '@/components/accounting/ExpectedExpenseManagement'; import { getExpectedExpenses } from '@/components/accounting/ExpectedExpenseManagement/actions'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; const DEFAULT_PAGINATION = { currentPage: 1, @@ -30,13 +31,7 @@ export default function ExpectedExpensesPage() { .finally(() => setIsLoading(false)); }, []); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; return ( -
로딩 중...
- - ); - } + if (isLoading) return ; return ( { if (result.success) { @@ -43,13 +45,7 @@ export default function SalesPage() { return ; } - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; return ( setIsLoading(false)); }, [mode, id]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (mode === 'edit' && id) { return ( diff --git a/src/app/[locale]/(protected)/accounting/vendors/page.tsx b/src/app/[locale]/(protected)/accounting/vendors/page.tsx index 0f22a58d..32295ea0 100644 --- a/src/app/[locale]/(protected)/accounting/vendors/page.tsx +++ b/src/app/[locale]/(protected)/accounting/vendors/page.tsx @@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation'; import { VendorManagement } from '@/components/accounting/VendorManagement'; import { VendorDetail } from '@/components/accounting/VendorManagement/VendorDetail'; import { getClients } from '@/components/accounting/VendorManagement/actions'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function VendorsPage() { const searchParams = useSearchParams(); @@ -31,13 +32,7 @@ export default function VendorsPage() { return ; } - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; return ( setIsLoading(false)); }, [mode]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; // mode=new일 때 등록 화면 표시 if (mode === 'new') { diff --git a/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/page.tsx b/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/page.tsx index 5706161c..a10f1a67 100644 --- a/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/page.tsx +++ b/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/page.tsx @@ -232,9 +232,7 @@ function DynamicBoardDetailContent({ boardCode, postId }: { boardCode: string; p if (isLoading) { return ( -
-

로딩 중...

-
+
); } diff --git a/src/app/[locale]/(protected)/construction/billing/progress-billing-management/[id]/page.tsx b/src/app/[locale]/(protected)/construction/billing/progress-billing-management/[id]/page.tsx index 3be49edc..c448f6ea 100644 --- a/src/app/[locale]/(protected)/construction/billing/progress-billing-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/billing/progress-billing-management/[id]/page.tsx @@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation'; import { ProgressBillingDetailForm } from '@/components/business/construction/progress-billing'; import { getProgressBillingDetail } from '@/components/business/construction/progress-billing/actions'; import { ServerErrorPage } from '@/components/common/ServerErrorPage'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; interface ProgressBillingDetailPageProps { params: Promise<{ id: string }>; @@ -43,13 +44,7 @@ export default function ProgressBillingDetailPage({ params }: ProgressBillingDet fetchData(); }, [fetchData]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (error || !data) { return ( diff --git a/src/app/[locale]/(protected)/construction/billing/progress-billing-management/page.tsx b/src/app/[locale]/(protected)/construction/billing/progress-billing-management/page.tsx index 6c704ace..fdc46934 100644 --- a/src/app/[locale]/(protected)/construction/billing/progress-billing-management/page.tsx +++ b/src/app/[locale]/(protected)/construction/billing/progress-billing-management/page.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'; import ProgressBillingManagementListClient from '@/components/business/construction/progress-billing/ProgressBillingManagementListClient'; import { getProgressBillingList, getProgressBillingStats } from '@/components/business/construction/progress-billing/actions'; import type { ProgressBilling, ProgressBillingStats } from '@/components/business/construction/progress-billing/types'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function ProgressBillingManagementPage() { const [data, setData] = useState([]); @@ -26,13 +27,7 @@ export default function ProgressBillingManagementPage() { .finally(() => setIsLoading(false)); }, []); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/order/order-management/[id]/page.tsx b/src/app/[locale]/(protected)/construction/order/order-management/[id]/page.tsx index b28fd503..9673157c 100644 --- a/src/app/[locale]/(protected)/construction/order/order-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/order-management/[id]/page.tsx @@ -10,6 +10,7 @@ import { useRouter, useSearchParams } from 'next/navigation'; import { OrderDetailForm } from '@/components/business/construction/order-management'; import { getOrderDetailFull } from '@/components/business/construction/order-management/actions'; import { ServerErrorPage } from '@/components/common/ServerErrorPage'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; interface OrderDetailPageProps { params: Promise<{ id: string }>; @@ -39,13 +40,7 @@ export default function OrderDetailPage({ params }: OrderDetailPageProps) { .finally(() => setIsLoading(false)); }, [id]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (error || !data) { return ( diff --git a/src/app/[locale]/(protected)/construction/project/bidding/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/[id]/page.tsx index ccfa6c73..2758b094 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/[id]/page.tsx @@ -4,6 +4,7 @@ import { use, useEffect, useState, useCallback } from 'react'; import { useSearchParams } from 'next/navigation'; import { BiddingDetailForm, getBiddingDetail } from '@/components/business/construction/bidding'; import { ServerErrorPage } from '@/components/common/ServerErrorPage'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; interface BiddingDetailPageProps { params: Promise<{ id: string }>; @@ -42,13 +43,7 @@ export default function BiddingDetailPage({ params }: BiddingDetailPageProps) { fetchData(); }, [fetchData]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (error) { return ( diff --git a/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/page.tsx index a2593926..69a462d1 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/page.tsx @@ -6,6 +6,7 @@ import { EstimateDetailForm } from '@/components/business/construction/estimates import type { EstimateDetail } from '@/components/business/construction/estimates'; import { getEstimateDetail } from '@/components/business/construction/estimates/actions'; import { ServerErrorPage } from '@/components/common/ServerErrorPage'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; interface EstimateDetailPageProps { params: Promise<{ id: string }>; @@ -44,13 +45,7 @@ export default function EstimateDetailPage({ params }: EstimateDetailPageProps) fetchData(); }, [fetchData]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (error) { return ( diff --git a/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/page.tsx index e58a6845..569c2499 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/page.tsx @@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation'; import PartnerForm from '@/components/business/construction/partners/PartnerForm'; import { getPartner } from '@/components/business/construction/partners/actions'; import { ServerErrorPage } from '@/components/common/ServerErrorPage'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; interface PartnerDetailPageProps { params: Promise<{ id: string }>; @@ -43,13 +44,7 @@ export default function PartnerDetailPage({ params }: PartnerDetailPageProps) { fetchData(); }, [fetchData]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (error) { return ( diff --git a/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/page.tsx index ac188ba4..d0bac4e6 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/page.tsx @@ -4,6 +4,7 @@ import { use, useEffect, useState, useCallback } from 'react'; import { useSearchParams } from 'next/navigation'; import { SiteBriefingForm, getSiteBriefing } from '@/components/business/construction/site-briefings'; import { ServerErrorPage } from '@/components/common/ServerErrorPage'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; interface SiteBriefingDetailPageProps { params: Promise<{ id: string }>; @@ -42,13 +43,7 @@ export default function SiteBriefingDetailPage({ params }: SiteBriefingDetailPag fetchData(); }, [fetchData]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (error) { return ( diff --git a/src/app/[locale]/(protected)/construction/project/construction-management/page.tsx b/src/app/[locale]/(protected)/construction/project/construction-management/page.tsx index c8dd33b2..94514378 100644 --- a/src/app/[locale]/(protected)/construction/project/construction-management/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/construction-management/page.tsx @@ -10,6 +10,7 @@ import type { ConstructionManagement, ConstructionManagementStats, } from '@/components/business/construction/management/types'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function ConstructionManagementPage() { const [data, setData] = useState([]); @@ -40,13 +41,7 @@ export default function ConstructionManagementPage() { loadData(); }, []); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/project/contract/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/contract/[id]/page.tsx index 6e1a33d5..c25aa623 100644 --- a/src/app/[locale]/(protected)/construction/project/contract/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/contract/[id]/page.tsx @@ -5,6 +5,7 @@ import { useRouter, useSearchParams } from 'next/navigation'; import ContractDetailForm from '@/components/business/construction/contract/ContractDetailForm'; import { getContractDetail } from '@/components/business/construction/contract'; import { ServerErrorPage } from '@/components/common/ServerErrorPage'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; interface ContractDetailPageProps { params: Promise<{ id: string }>; @@ -34,13 +35,7 @@ export default function ContractDetailPage({ params }: ContractDetailPageProps) .finally(() => setIsLoading(false)); }, [id]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (error) { return ( diff --git a/src/app/[locale]/(protected)/construction/project/contract/create/page.tsx b/src/app/[locale]/(protected)/construction/project/contract/create/page.tsx index 1dcc7188..d69f8ac8 100644 --- a/src/app/[locale]/(protected)/construction/project/contract/create/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/contract/create/page.tsx @@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation'; import ContractDetailForm from '@/components/business/construction/contract/ContractDetailForm'; import { getContractDetail } from '@/components/business/construction/contract'; import type { ContractDetail } from '@/components/business/construction/contract/types'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function ContractCreatePage() { const searchParams = useSearchParams(); @@ -26,13 +27,7 @@ export default function ContractCreatePage() { } }, [baseContractId]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; return ( setIsLoading(false)); }, [id]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (error) { return ( diff --git a/src/app/[locale]/(protected)/construction/project/issue-management/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/issue-management/[id]/page.tsx index 7dbd2769..57e3b3a0 100644 --- a/src/app/[locale]/(protected)/construction/project/issue-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/issue-management/[id]/page.tsx @@ -6,6 +6,7 @@ import IssueDetailForm from '@/components/business/construction/issue-management import { getIssue } from '@/components/business/construction/issue-management/actions'; import type { Issue } from '@/components/business/construction/issue-management/types'; import { ServerErrorPage } from '@/components/common/ServerErrorPage'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function IssueDetailPage() { const params = useParams(); @@ -41,13 +42,7 @@ export default function IssueDetailPage() { fetchData(); }, [fetchData]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (error) { return ( diff --git a/src/app/[locale]/(protected)/construction/project/issue-management/page.tsx b/src/app/[locale]/(protected)/construction/project/issue-management/page.tsx index 0418e631..1dccc518 100644 --- a/src/app/[locale]/(protected)/construction/project/issue-management/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/issue-management/page.tsx @@ -12,6 +12,7 @@ import type { Issue, IssueStats, } from '@/components/business/construction/issue-management/types'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function IssueManagementPage() { const searchParams = useSearchParams(); @@ -53,13 +54,7 @@ export default function IssueManagementPage() { return ; } - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; return ; } diff --git a/src/app/[locale]/(protected)/customer-center/events/[id]/page.tsx b/src/app/[locale]/(protected)/customer-center/events/[id]/page.tsx index a3b8ef95..67b91750 100644 --- a/src/app/[locale]/(protected)/customer-center/events/[id]/page.tsx +++ b/src/app/[locale]/(protected)/customer-center/events/[id]/page.tsx @@ -6,6 +6,7 @@ import { EventDetail } from '@/components/customer-center/EventManagement'; import { transformPostToEvent, type Event } from '@/components/customer-center/EventManagement/types'; import { getPost } from '@/components/customer-center/shared/actions'; import { ServerErrorPage } from '@/components/common/ServerErrorPage'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function EventDetailPage() { const params = useParams(); @@ -34,13 +35,7 @@ export default function EventDetailPage() { fetchEvent(); }, [fetchEvent]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (error || !event) { return ( diff --git a/src/app/[locale]/(protected)/customer-center/notices/[id]/page.tsx b/src/app/[locale]/(protected)/customer-center/notices/[id]/page.tsx index 6deb1e99..4a84180c 100644 --- a/src/app/[locale]/(protected)/customer-center/notices/[id]/page.tsx +++ b/src/app/[locale]/(protected)/customer-center/notices/[id]/page.tsx @@ -6,6 +6,7 @@ import { NoticeDetail } from '@/components/customer-center/NoticeManagement'; import { transformPostToNotice, type Notice } from '@/components/customer-center/NoticeManagement/types'; import { getPost } from '@/components/customer-center/shared/actions'; import { ServerErrorPage } from '@/components/common/ServerErrorPage'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function NoticeDetailPage() { const params = useParams(); @@ -34,13 +35,7 @@ export default function NoticeDetailPage() { fetchNotice(); }, [fetchNotice]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (error || !notice) { return ( diff --git a/src/app/[locale]/(protected)/dev/construction-test-urls/page.tsx b/src/app/[locale]/(protected)/dev/construction-test-urls/page.tsx index 35942381..00f56afa 100644 --- a/src/app/[locale]/(protected)/dev/construction-test-urls/page.tsx +++ b/src/app/[locale]/(protected)/dev/construction-test-urls/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'; import ConstructionTestUrlsClient, { UrlCategory } from './ConstructionTestUrlsClient'; import { getConstructionTestUrlsData } from './actions'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function TestUrlsPage() { const [urlData, setUrlData] = useState([]); @@ -18,13 +19,7 @@ export default function TestUrlsPage() { .finally(() => setIsLoading(false)); }, []); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; return ; } \ 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 index 73402243..d4e9978b 100644 --- a/src/app/[locale]/(protected)/dev/test-urls/page.tsx +++ b/src/app/[locale]/(protected)/dev/test-urls/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'; import TestUrlsClient, { UrlCategory } from './TestUrlsClient'; import { getTestUrlsData } from './actions'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function TestUrlsPage() { const [urlData, setUrlData] = useState([]); @@ -18,13 +19,7 @@ export default function TestUrlsPage() { .finally(() => setIsLoading(false)); }, []); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/payment-history/page.tsx b/src/app/[locale]/(protected)/payment-history/page.tsx index 07075ed7..2f312262 100644 --- a/src/app/[locale]/(protected)/payment-history/page.tsx +++ b/src/app/[locale]/(protected)/payment-history/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'; import { PaymentHistoryManagement } from '@/components/settings/PaymentHistoryManagement'; import { getPayments } from '@/components/settings/PaymentHistoryManagement/actions'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function PaymentHistoryPage() { const [data, setData] = useState>['data']>(); @@ -18,13 +19,7 @@ export default function PaymentHistoryPage() { .finally(() => setIsLoading(false)); }, []); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (!data || !pagination) { return null; diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx index cd11ea39..518d836d 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx @@ -38,7 +38,7 @@ import { toast } from "sonner"; import { IntegratedDetailTemplate } from "@/components/templates/IntegratedDetailTemplate"; import { orderSalesConfig } from "@/components/orders/orderSalesConfig"; import { BadgeSm } from "@/components/atoms/BadgeSm"; -import { formatAmount } from "@/utils/formatAmount"; +import { formatAmount } from "@/lib/utils/amount"; import { OrderItem, getOrderById, diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx index ac8962dc..ad4c1f57 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx @@ -44,7 +44,7 @@ import { toast } from "sonner"; import { IntegratedDetailTemplate } from "@/components/templates/IntegratedDetailTemplate"; import { orderSalesConfig } from "@/components/orders/orderSalesConfig"; import { BadgeSm } from "@/components/atoms/BadgeSm"; -import { formatAmount } from "@/utils/formatAmount"; +import { formatAmount } from "@/lib/utils/amount"; import { Dialog, DialogContent, diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx index be815ae1..ddba66f0 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx @@ -51,7 +51,7 @@ import { } from "@/components/orders/actions"; import { getProcessList } from "@/components/process-management/actions"; import type { Process } from "@/types/process"; -import { formatAmount } from "@/utils/formatAmount"; +import { formatAmount } from "@/lib/utils/amount"; // 수주 정보 타입 interface OrderInfo { diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx index 3117eb6c..9d2a8a05 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx @@ -41,7 +41,7 @@ import { TableCell, } from "@/components/ui/table"; import { Checkbox } from "@/components/ui/checkbox"; -import { formatAmount, formatAmountManwon } from "@/utils/formatAmount"; +import { formatAmount, formatAmountManwon } from "@/lib/utils/amount"; import { ListMobileCard, InfoField } from "@/components/organisms/MobileCard"; import { ConfirmDialog, DeleteConfirmDialog } from "@/components/ui/confirm-dialog"; import { diff --git a/src/app/[locale]/(protected)/sales/pricing-management/[id]/page.tsx b/src/app/[locale]/(protected)/sales/pricing-management/[id]/page.tsx index e90ee79c..1f5d1537 100644 --- a/src/app/[locale]/(protected)/sales/pricing-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/sales/pricing-management/[id]/page.tsx @@ -14,6 +14,7 @@ import { PricingFormClient } from '@/components/pricing'; import { getPricingById, updatePricing, finalizePricing } from '@/components/pricing/actions'; import type { PricingData } from '@/components/pricing'; import { ServerErrorPage } from '@/components/common/ServerErrorPage'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; interface PricingDetailPageProps { params: Promise<{ @@ -61,13 +62,7 @@ export default function PricingDetailPage({ params }: PricingDetailPageProps) { } }; - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (error || !data) { return ( diff --git a/src/app/[locale]/(protected)/sales/pricing-management/create/page.tsx b/src/app/[locale]/(protected)/sales/pricing-management/create/page.tsx index 47162511..edf8e3ea 100644 --- a/src/app/[locale]/(protected)/sales/pricing-management/create/page.tsx +++ b/src/app/[locale]/(protected)/sales/pricing-management/create/page.tsx @@ -14,6 +14,7 @@ import { useSearchParams, useRouter } from 'next/navigation'; import { PricingFormClient } from '@/components/pricing'; import { getItemInfo, createPricing } from '@/components/pricing/actions'; import type { PricingData, ItemInfo } from '@/components/pricing'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function CreatePricingPage() { const searchParams = useSearchParams(); @@ -52,13 +53,7 @@ export default function CreatePricingPage() { } }; - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; // 품목 정보 없이 접근한 경우 if (!itemId) { diff --git a/src/app/[locale]/(protected)/sales/pricing-management/page.tsx b/src/app/[locale]/(protected)/sales/pricing-management/page.tsx index 3d254c89..3fc26923 100644 --- a/src/app/[locale]/(protected)/sales/pricing-management/page.tsx +++ b/src/app/[locale]/(protected)/sales/pricing-management/page.tsx @@ -16,6 +16,7 @@ import { useEffect, useState } from 'react'; import { useSearchParams, useRouter } from 'next/navigation'; import { PricingListClient } from '@/components/pricing'; import { getPricingListData, type PricingListItem } from '@/components/pricing/actions'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function PricingManagementPage() { const searchParams = useSearchParams(); @@ -53,13 +54,7 @@ export default function PricingManagementPage() { ); } - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/sales/quote-management/page.tsx b/src/app/[locale]/(protected)/sales/quote-management/page.tsx index a9cd884c..fcc268b8 100644 --- a/src/app/[locale]/(protected)/sales/quote-management/page.tsx +++ b/src/app/[locale]/(protected)/sales/quote-management/page.tsx @@ -14,6 +14,7 @@ import { useEffect, useState } from 'react'; import { useSearchParams, useRouter } from 'next/navigation'; import { QuoteManagementClient } from '@/components/quotes/QuoteManagementClient'; import { getQuotes } from '@/components/quotes'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; const DEFAULT_PAGINATION = { currentPage: 1, @@ -55,13 +56,7 @@ export default function QuoteManagementPage() { ); } - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; return ( (DEFAULT_NOTIFICATION_SETTINGS); @@ -20,13 +21,7 @@ export default function NotificationSettingsPage() { .finally(() => setIsLoading(false)); }, []); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/settings/popup-management/page.tsx b/src/app/[locale]/(protected)/settings/popup-management/page.tsx index cd8767df..9da479a8 100644 --- a/src/app/[locale]/(protected)/settings/popup-management/page.tsx +++ b/src/app/[locale]/(protected)/settings/popup-management/page.tsx @@ -6,6 +6,7 @@ import { PopupList } from '@/components/settings/PopupManagement'; import { PopupDetailClientV2 } from '@/components/settings/PopupManagement/PopupDetailClientV2'; import { getPopups } from '@/components/settings/PopupManagement/actions'; import type { Popup } from '@/components/settings/PopupManagement/types'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function PopupManagementPage() { const searchParams = useSearchParams(); @@ -30,13 +31,7 @@ export default function PopupManagementPage() { return ; } - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; return ; } diff --git a/src/app/[locale]/(protected)/vehicle-management/forklift/[id]/edit/page.tsx b/src/app/[locale]/(protected)/vehicle-management/forklift/[id]/edit/page.tsx index ef2f2c89..338e3be7 100644 --- a/src/app/[locale]/(protected)/vehicle-management/forklift/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/vehicle-management/forklift/[id]/edit/page.tsx @@ -9,6 +9,7 @@ import { useParams } from 'next/navigation'; import { ForkliftDetail } from '@/components/vehicle-management/ForkliftDetail'; import { getForkliftById } from '@/components/vehicle-management/ForkliftList/actions'; import type { Forklift } from '@/components/vehicle-management/types'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function ForkliftEditPage() { const params = useParams(); @@ -32,13 +33,7 @@ export default function ForkliftEditPage() { .finally(() => setIsLoading(false)); }, [id]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (error || !data) { return ( diff --git a/src/app/[locale]/(protected)/vehicle-management/forklift/[id]/page.tsx b/src/app/[locale]/(protected)/vehicle-management/forklift/[id]/page.tsx index 80af038f..b9af70d8 100644 --- a/src/app/[locale]/(protected)/vehicle-management/forklift/[id]/page.tsx +++ b/src/app/[locale]/(protected)/vehicle-management/forklift/[id]/page.tsx @@ -9,6 +9,7 @@ import { useParams } from 'next/navigation'; import { ForkliftDetail } from '@/components/vehicle-management/ForkliftDetail'; import { getForkliftById } from '@/components/vehicle-management/ForkliftList/actions'; import type { Forklift } from '@/components/vehicle-management/types'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function ForkliftDetailPage() { const params = useParams(); @@ -32,13 +33,7 @@ export default function ForkliftDetailPage() { .finally(() => setIsLoading(false)); }, [id]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (error || !data) { return ( diff --git a/src/app/[locale]/(protected)/vehicle-management/forklift/page.tsx b/src/app/[locale]/(protected)/vehicle-management/forklift/page.tsx index c247f8ad..73ab8848 100644 --- a/src/app/[locale]/(protected)/vehicle-management/forklift/page.tsx +++ b/src/app/[locale]/(protected)/vehicle-management/forklift/page.tsx @@ -8,6 +8,7 @@ import { useEffect, useState } from 'react'; import { ForkliftList } from '@/components/vehicle-management/ForkliftList'; import { getForklifts } from '@/components/vehicle-management/ForkliftList/actions'; import type { Forklift } from '@/components/vehicle-management/types'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function ForkliftPage() { const [data, setData] = useState([]); @@ -23,13 +24,7 @@ export default function ForkliftPage() { .finally(() => setIsLoading(false)); }, []); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; return ; } diff --git a/src/app/[locale]/(protected)/vehicle-management/vehicle-log/[id]/edit/page.tsx b/src/app/[locale]/(protected)/vehicle-management/vehicle-log/[id]/edit/page.tsx index 1fb26894..60367053 100644 --- a/src/app/[locale]/(protected)/vehicle-management/vehicle-log/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/vehicle-management/vehicle-log/[id]/edit/page.tsx @@ -9,6 +9,7 @@ import { useParams } from 'next/navigation'; import { VehicleLogDetail } from '@/components/vehicle-management/VehicleLogDetail'; import { getVehicleLogById } from '@/components/vehicle-management/VehicleLogList/actions'; import type { VehicleLog } from '@/components/vehicle-management/types'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function VehicleLogEditPage() { const params = useParams(); @@ -32,13 +33,7 @@ export default function VehicleLogEditPage() { .finally(() => setIsLoading(false)); }, [id]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (error || !data) { return ( diff --git a/src/app/[locale]/(protected)/vehicle-management/vehicle-log/[id]/page.tsx b/src/app/[locale]/(protected)/vehicle-management/vehicle-log/[id]/page.tsx index 227c68e7..8214a60d 100644 --- a/src/app/[locale]/(protected)/vehicle-management/vehicle-log/[id]/page.tsx +++ b/src/app/[locale]/(protected)/vehicle-management/vehicle-log/[id]/page.tsx @@ -9,6 +9,7 @@ import { useParams } from 'next/navigation'; import { VehicleLogDetail } from '@/components/vehicle-management/VehicleLogDetail'; import { getVehicleLogById } from '@/components/vehicle-management/VehicleLogList/actions'; import type { VehicleLog } from '@/components/vehicle-management/types'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function VehicleLogDetailPage() { const params = useParams(); @@ -32,13 +33,7 @@ export default function VehicleLogDetailPage() { .finally(() => setIsLoading(false)); }, [id]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (error || !data) { return ( diff --git a/src/app/[locale]/(protected)/vehicle-management/vehicle-log/page.tsx b/src/app/[locale]/(protected)/vehicle-management/vehicle-log/page.tsx index 5f820bd6..59ddf9b6 100644 --- a/src/app/[locale]/(protected)/vehicle-management/vehicle-log/page.tsx +++ b/src/app/[locale]/(protected)/vehicle-management/vehicle-log/page.tsx @@ -8,6 +8,7 @@ import { useEffect, useState } from 'react'; import { VehicleLogList } from '@/components/vehicle-management/VehicleLogList'; import { getVehicleLogs } from '@/components/vehicle-management/VehicleLogList/actions'; import type { VehicleLog } from '@/components/vehicle-management/types'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function VehicleLogPage() { const [data, setData] = useState([]); @@ -23,13 +24,7 @@ export default function VehicleLogPage() { .finally(() => setIsLoading(false)); }, []); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; return ; } diff --git a/src/app/[locale]/(protected)/vehicle-management/vehicle/[id]/edit/page.tsx b/src/app/[locale]/(protected)/vehicle-management/vehicle/[id]/edit/page.tsx index 7c447e9c..b6e831b3 100644 --- a/src/app/[locale]/(protected)/vehicle-management/vehicle/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/vehicle-management/vehicle/[id]/edit/page.tsx @@ -9,6 +9,7 @@ import { useParams } from 'next/navigation'; import { VehicleDetail } from '@/components/vehicle-management/VehicleDetail'; import { getVehicleById } from '@/components/vehicle-management/VehicleList/actions'; import type { Vehicle } from '@/components/vehicle-management/types'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function VehicleEditPage() { const params = useParams(); @@ -32,13 +33,7 @@ export default function VehicleEditPage() { .finally(() => setIsLoading(false)); }, [id]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (error || !data) { return ( diff --git a/src/app/[locale]/(protected)/vehicle-management/vehicle/[id]/page.tsx b/src/app/[locale]/(protected)/vehicle-management/vehicle/[id]/page.tsx index 5c8bef17..1c802d50 100644 --- a/src/app/[locale]/(protected)/vehicle-management/vehicle/[id]/page.tsx +++ b/src/app/[locale]/(protected)/vehicle-management/vehicle/[id]/page.tsx @@ -9,6 +9,7 @@ import { useParams } from 'next/navigation'; import { VehicleDetail } from '@/components/vehicle-management/VehicleDetail'; import { getVehicleById } from '@/components/vehicle-management/VehicleList/actions'; import type { Vehicle } from '@/components/vehicle-management/types'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function VehicleDetailPage() { const params = useParams(); @@ -32,13 +33,7 @@ export default function VehicleDetailPage() { .finally(() => setIsLoading(false)); }, [id]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; if (error || !data) { return ( diff --git a/src/app/[locale]/(protected)/vehicle-management/vehicle/page.tsx b/src/app/[locale]/(protected)/vehicle-management/vehicle/page.tsx index 265d238f..b0b1a38b 100644 --- a/src/app/[locale]/(protected)/vehicle-management/vehicle/page.tsx +++ b/src/app/[locale]/(protected)/vehicle-management/vehicle/page.tsx @@ -8,6 +8,7 @@ import { useEffect, useState } from 'react'; import { VehicleList } from '@/components/vehicle-management/VehicleList'; import { getVehicles } from '@/components/vehicle-management/VehicleList/actions'; import type { Vehicle } from '@/components/vehicle-management/types'; +import { GenericPageSkeleton } from '@/components/ui/skeleton'; export default function VehiclePage() { const [data, setData] = useState([]); @@ -23,13 +24,7 @@ export default function VehiclePage() { .finally(() => setIsLoading(false)); }, []); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + if (isLoading) return ; return ; } diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 353d9ab7..7ff631fe 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -4,7 +4,6 @@ import { NextIntlClientProvider } from 'next-intl'; import { getMessages } from 'next-intl/server'; import { notFound } from 'next/navigation'; import { locales, type Locale } from '@/i18n/config'; -import { ThemeProvider } from '@/contexts/ThemeContext'; import { Toaster } from 'sonner'; import { ChunkErrorHandler } from '@/components/providers/ChunkErrorHandler'; import "../globals.css"; @@ -98,13 +97,11 @@ export default async function RootLayout({ return ( - - - - {children} - - - + + + {children} + + ); diff --git a/src/components/ThemeSelect.tsx b/src/components/ThemeSelect.tsx index 59f1c43d..d1ab2e41 100644 --- a/src/components/ThemeSelect.tsx +++ b/src/components/ThemeSelect.tsx @@ -1,6 +1,6 @@ "use client"; -import { useTheme } from "@/contexts/ThemeContext"; +import { useThemeStore } from "@/stores/themeStore"; import { Select, SelectContent, @@ -21,7 +21,7 @@ interface ThemeSelectProps { } export function ThemeSelect({ native = true }: ThemeSelectProps) { - const { theme, setTheme } = useTheme(); + const { theme, setTheme } = useThemeStore(); const currentTheme = themes.find((t) => t.value === theme); const CurrentIcon = currentTheme?.icon || Sun; diff --git a/src/components/accounting/SalesManagement/index.tsx b/src/components/accounting/SalesManagement/index.tsx index f3176cba..d2b49238 100644 --- a/src/components/accounting/SalesManagement/index.tsx +++ b/src/components/accounting/SalesManagement/index.tsx @@ -93,8 +93,9 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem const router = useRouter(); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== - const [startDate, setStartDate] = useState('2025-01-01'); - const [endDate, setEndDate] = useState('2025-12-31'); + const currentYear = new Date().getFullYear(); + const [startDate, setStartDate] = useState(`${currentYear}-01-01`); + const [endDate, setEndDate] = useState(`${currentYear}-12-31`); const [salesData, setSalesData] = useState(initialData || []); const [pagination, setPagination] = useState(initialPagination); const [currentPage, setCurrentPage] = useState(initialPagination.currentPage); diff --git a/src/components/approval/DocumentCreate/index.tsx b/src/components/approval/DocumentCreate/index.tsx index d9a1bf21..4d92b93b 100644 --- a/src/components/approval/DocumentCreate/index.tsx +++ b/src/components/approval/DocumentCreate/index.tsx @@ -6,6 +6,7 @@ import { usePermission } from '@/hooks/usePermission'; import { format } from 'date-fns'; import { Trash2, Send, Save, Eye, Loader2 } from 'lucide-react'; import { toast } from 'sonner'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { documentCreateConfig, @@ -110,6 +111,9 @@ export function DocumentCreate() { // 복제 모드 toast 중복 호출 방지 const copyToastShownRef = useRef(false); + // 삭제 확인 다이얼로그 + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + // Hydration 불일치 방지: 클라이언트에서만 날짜 초기화 useEffect(() => { const today = format(new Date(), 'yyyy-MM-dd'); @@ -327,10 +331,12 @@ export function DocumentCreate() { router.back(); }, [router]); - const handleDelete = useCallback(async () => { - if (!confirm('작성 중인 문서를 삭제하시겠습니까?')) { - return; - } + const handleDelete = useCallback(() => { + setIsDeleteDialogOpen(true); + }, []); + + const handleDeleteConfirm = useCallback(async () => { + setIsDeleteDialogOpen(false); // 수정 모드: 실제 문서 삭제 if (isEditMode && documentId) { @@ -639,6 +645,14 @@ export function DocumentCreate() { handleSubmit(); }} /> + + ); } diff --git a/src/components/attendance/actions.ts b/src/components/attendance/actions.ts index 7b174016..9f7da182 100644 --- a/src/components/attendance/actions.ts +++ b/src/components/attendance/actions.ts @@ -3,7 +3,7 @@ import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action'; import type { PaginatedApiResponse } from '@/lib/api/types'; -import { getTodayString } from '@/utils/date'; +import { getTodayString } from '@/lib/utils/date'; const API_URL = process.env.NEXT_PUBLIC_API_URL; diff --git a/src/components/board/BoardForm/index.tsx b/src/components/board/BoardForm/index.tsx index 71ae3be3..9462d06c 100644 --- a/src/components/board/BoardForm/index.tsx +++ b/src/components/board/BoardForm/index.tsx @@ -47,7 +47,9 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; -import { RichTextEditor } from '../RichTextEditor'; +import dynamic from 'next/dynamic'; + +const RichTextEditor = dynamic(() => import('../RichTextEditor'), { ssr: false }); import type { Post, Attachment } from '../types'; import { createPost, updatePost } from '../actions'; import { getBoards } from '../BoardManagement/actions'; diff --git a/src/components/board/BoardList/BoardListUnified.tsx b/src/components/board/BoardList/BoardListUnified.tsx index d7c9120b..76decc00 100644 --- a/src/components/board/BoardList/BoardListUnified.tsx +++ b/src/components/board/BoardList/BoardListUnified.tsx @@ -29,6 +29,8 @@ import type { Post } from '../types'; import { getBoards } from '../BoardManagement/actions'; import { getPosts, getMyPosts, deletePost } from '../actions'; import type { Board } from '../BoardManagement/types'; +import { toast } from 'sonner'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; export function BoardListUnified() { const router = useRouter(); @@ -42,6 +44,9 @@ export function BoardListUnified() { const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); + // 삭제 확인 상태 + const [deleteTarget, setDeleteTarget] = useState<{ boardCode: string; id: string } | null>(null); + // 현재 사용자 ID 가져오기 useEffect(() => { const userId = localStorage.getItem('user_id') || ''; @@ -205,13 +210,8 @@ export function BoardListUnified() { router.push(`/ko/board/${item.boardCode}/${item.id}?mode=edit`); }; - const handleDelete = async () => { - if (confirm('정말 삭제하시겠습니까?')) { - const result = await deletePost(item.boardCode, item.id); - if (result.success) { - window.location.reload(); // 삭제 후 새로고침 - } - } + const handleDelete = () => { + setDeleteTarget({ boardCode: item.boardCode, id: item.id }); }; return ( @@ -296,13 +296,8 @@ export function BoardListUnified() { router.push(`/ko/board/${item.boardCode}/${item.id}?mode=edit`); }; - const handleDelete = async () => { - if (confirm('정말 삭제하시겠습니까?')) { - const result = await deletePost(item.boardCode, item.id); - if (result.success) { - window.location.reload(); - } - } + const handleDelete = () => { + setDeleteTarget({ boardCode: item.boardCode, id: item.id }); }; return ( @@ -361,10 +356,29 @@ export function BoardListUnified() { itemsPerPage: 20, }), [activeTab, boards, currentUserId, fetchTabs, router, startDate, endDate]); + const handleDeleteConfirm = useCallback(async () => { + if (!deleteTarget) return; + const result = await deletePost(deleteTarget.boardCode, deleteTarget.id); + if (result.success) { + window.location.reload(); + } + setDeleteTarget(null); + }, [deleteTarget]); + return ( - - config={config} - /> + <> + + config={config} + /> + + !open && setDeleteTarget(null)} + onConfirm={handleDeleteConfirm} + title="게시글 삭제" + description="정말 삭제하시겠습니까?" + /> + ); } diff --git a/src/components/business/CEODashboard/components.tsx b/src/components/business/CEODashboard/components.tsx index f3cdf130..924e9523 100644 --- a/src/components/business/CEODashboard/components.tsx +++ b/src/components/business/CEODashboard/components.tsx @@ -12,7 +12,7 @@ import { import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; -import { formatKoreanAmount } from '@/utils/formatAmount'; +import { formatKoreanAmount } from '@/lib/utils/amount'; import type { CheckPoint, CheckPointType, AmountCard, HighlightColor } from './types'; // 섹션별 컬러 테마 타입 diff --git a/src/components/business/CEODashboard/sections/EnhancedSections.tsx b/src/components/business/CEODashboard/sections/EnhancedSections.tsx index 83112176..84d9ef29 100644 --- a/src/components/business/CEODashboard/sections/EnhancedSections.tsx +++ b/src/components/business/CEODashboard/sections/EnhancedSections.tsx @@ -25,7 +25,7 @@ import { import { useRouter } from 'next/navigation'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; -import { formatKoreanAmount } from '@/utils/formatAmount'; +import { formatKoreanAmount } from '@/lib/utils/amount'; import type { DailyReportData, MonthlyExpenseData, TodayIssueItem, TodayIssueSettings } from '../types'; // ============================================================ diff --git a/src/components/business/MainDashboard.tsx b/src/components/business/MainDashboard.tsx index df9797d4..4dcd296d 100644 --- a/src/components/business/MainDashboard.tsx +++ b/src/components/business/MainDashboard.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from "react"; -import { getLocalDateString, getTodayString } from "@/utils/date"; +import { getLocalDateString, getTodayString } from "@/lib/utils/date"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; diff --git a/src/components/business/construction/bidding/BiddingDetailForm.tsx b/src/components/business/construction/bidding/BiddingDetailForm.tsx index 2e31bd57..ddd42fb6 100644 --- a/src/components/business/construction/bidding/BiddingDetailForm.tsx +++ b/src/components/business/construction/bidding/BiddingDetailForm.tsx @@ -35,7 +35,7 @@ import { biddingDetailToFormData, } from './types'; import { updateBidding } from './actions'; -import { formatNumber } from '@/utils/formatAmount'; +import { formatNumber } from '@/lib/utils/amount'; interface BiddingDetailFormProps { mode: 'view' | 'edit'; diff --git a/src/components/business/construction/bidding/BiddingListClient.tsx b/src/components/business/construction/bidding/BiddingListClient.tsx index 98e3f30c..8f6a336b 100644 --- a/src/components/business/construction/bidding/BiddingListClient.tsx +++ b/src/components/business/construction/bidding/BiddingListClient.tsx @@ -34,8 +34,8 @@ import { BIDDING_STATUS_LABELS, } from './types'; import { getBiddingList, getBiddingStats, deleteBidding, deleteBiddings } from './actions'; -import { formatNumber } from '@/utils/formatAmount'; -import { formatDate } from '@/utils/date'; +import { formatNumber } from '@/lib/utils/amount'; +import { formatDate } from '@/lib/utils/date'; // 테이블 컬럼 정의 const tableColumns = [ diff --git a/src/components/business/construction/bidding/types.ts b/src/components/business/construction/bidding/types.ts index 07771581..eb868f77 100644 --- a/src/components/business/construction/bidding/types.ts +++ b/src/components/business/construction/bidding/types.ts @@ -5,7 +5,7 @@ * (별도 등록 기능 없음, 상세/수정만 가능) */ -import { getTodayString } from '@/utils/date'; +import { getTodayString } from '@/lib/utils/date'; // 입찰 상태 export type BiddingStatus = diff --git a/src/components/business/construction/contract/ContractDetailForm.tsx b/src/components/business/construction/contract/ContractDetailForm.tsx index 3d884168..73ebaf10 100644 --- a/src/components/business/construction/contract/ContractDetailForm.tsx +++ b/src/components/business/construction/contract/ContractDetailForm.tsx @@ -41,7 +41,7 @@ import { getEmptyElectronicApproval, } from '../common'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; -import { formatNumber } from '@/utils/formatAmount'; +import { formatNumber } from '@/lib/utils/amount'; interface ContractDetailFormProps { mode: 'view' | 'edit' | 'create'; diff --git a/src/components/business/construction/contract/ContractListClient.tsx b/src/components/business/construction/contract/ContractListClient.tsx index 50016965..b88b90ad 100644 --- a/src/components/business/construction/contract/ContractListClient.tsx +++ b/src/components/business/construction/contract/ContractListClient.tsx @@ -33,8 +33,8 @@ import { CONTRACT_STATUS_LABELS, } from './types'; import { getContractList, getContractStats, deleteContract, deleteContracts } from './actions'; -import { formatNumber } from '@/utils/formatAmount'; -import { formatDate, formatDateRange } from '@/utils/date'; +import { formatNumber } from '@/lib/utils/amount'; +import { formatDate, formatDateRange } from '@/lib/utils/date'; // 테이블 컬럼 정의 const tableColumns = [ diff --git a/src/components/business/construction/contract/types.ts b/src/components/business/construction/contract/types.ts index 9c8bd0ce..4e985ed6 100644 --- a/src/components/business/construction/contract/types.ts +++ b/src/components/business/construction/contract/types.ts @@ -4,7 +4,7 @@ * 계약 데이터는 낙찰 후 자동 등록됨 */ -import { getTodayString } from '@/utils/date'; +import { getTodayString } from '@/lib/utils/date'; // 계약 상태 export type ContractStatus = diff --git a/src/components/business/construction/estimates/EstimateListClient.tsx b/src/components/business/construction/estimates/EstimateListClient.tsx index 67f9c765..e3ba0dd0 100644 --- a/src/components/business/construction/estimates/EstimateListClient.tsx +++ b/src/components/business/construction/estimates/EstimateListClient.tsx @@ -34,7 +34,7 @@ import { } from './types'; import { getEstimateList, getEstimateStats, deleteEstimate, deleteEstimates, getClientOptions, getUserOptions } from './actions'; import type { ClientOption, UserOption } from './actions'; -import { formatNumber } from '@/utils/formatAmount'; +import { formatNumber } from '@/lib/utils/amount'; // 테이블 컬럼 정의 const tableColumns = [ diff --git a/src/components/business/construction/estimates/modals/EstimateDocumentContent.tsx b/src/components/business/construction/estimates/modals/EstimateDocumentContent.tsx index 882810ef..1f91fc3e 100644 --- a/src/components/business/construction/estimates/modals/EstimateDocumentContent.tsx +++ b/src/components/business/construction/estimates/modals/EstimateDocumentContent.tsx @@ -12,7 +12,7 @@ import type { EstimateDetailFormData } from '../types'; import type { CompanyInfo } from '../actions'; import { DocumentHeader } from '@/components/document-system'; -import { formatNumber } from '@/utils/formatAmount'; +import { formatNumber } from '@/lib/utils/amount'; // 금액을 한글로 변환 function amountToKorean(amount: number): string { diff --git a/src/components/business/construction/estimates/types.ts b/src/components/business/construction/estimates/types.ts index 2d26570b..064d7d93 100644 --- a/src/components/business/construction/estimates/types.ts +++ b/src/components/business/construction/estimates/types.ts @@ -2,7 +2,7 @@ * 주일 기업 - 견적관리 타입 정의 */ -import { getTodayString } from '@/utils/date'; +import { getTodayString } from '@/lib/utils/date'; // 견적 상태 export type EstimateStatus = 'pending' | 'approval_waiting' | 'completed' | 'rejected' | 'hold'; diff --git a/src/components/business/construction/estimates/utils/formatters.ts b/src/components/business/construction/estimates/utils/formatters.ts index 104c8173..6c1cdeee 100644 --- a/src/components/business/construction/estimates/utils/formatters.ts +++ b/src/components/business/construction/estimates/utils/formatters.ts @@ -1,2 +1,2 @@ // 공통 유틸 re-export (backward compatibility) -export { formatNumber as formatAmount } from '@/utils/formatAmount'; +export { formatNumber as formatAmount } from '@/lib/utils/amount'; diff --git a/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx b/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx index bb881ddc..1ee41bc5 100644 --- a/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx +++ b/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx @@ -50,7 +50,7 @@ import { type ElectronicApproval, getEmptyElectronicApproval, } from '../common'; -import { formatNumber } from '@/utils/formatAmount'; +import { formatNumber } from '@/lib/utils/amount'; interface HandoverReportDetailFormProps { mode: 'view' | 'edit'; diff --git a/src/components/business/construction/handover-report/HandoverReportListClient.tsx b/src/components/business/construction/handover-report/HandoverReportListClient.tsx index 76a0c021..09232a74 100644 --- a/src/components/business/construction/handover-report/HandoverReportListClient.tsx +++ b/src/components/business/construction/handover-report/HandoverReportListClient.tsx @@ -33,8 +33,8 @@ import { HANDOVER_STATUS_STYLES, } from './types'; import { getHandoverReportList, getHandoverReportStats } from './actions'; -import { formatNumber } from '@/utils/formatAmount'; -import { formatDateRange } from '@/utils/date'; +import { formatNumber } from '@/lib/utils/amount'; +import { formatDateRange } from '@/lib/utils/date'; // 테이블 컬럼 정의 const tableColumns = [ @@ -89,8 +89,8 @@ export default function HandoverReportListClient({ // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [activeStatTab, setActiveStatTab] = useState<'all' | 'pending' | 'completed'>('all'); - const [startDate, setStartDate] = useState(''); - const [endDate, setEndDate] = useState(''); + const [startDate, setStartDate] = useState(() => new Date().toISOString().split('T')[0]); + const [endDate, setEndDate] = useState(() => new Date().toISOString().split('T')[0]); const [searchQuery, setSearchQuery] = useState(''); const [stats, setStats] = useState(initialStats || null); diff --git a/src/components/business/construction/handover-report/modals/HandoverReportDocumentModal.tsx b/src/components/business/construction/handover-report/modals/HandoverReportDocumentModal.tsx index 2ac52b6d..cdb28cda 100644 --- a/src/components/business/construction/handover-report/modals/HandoverReportDocumentModal.tsx +++ b/src/components/business/construction/handover-report/modals/HandoverReportDocumentModal.tsx @@ -11,7 +11,7 @@ import { useRouter } from 'next/navigation'; import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import type { HandoverReportDetail } from '../types'; import { deleteHandoverReport } from '../actions'; -import { formatNumber } from '@/utils/formatAmount'; +import { formatNumber } from '@/lib/utils/amount'; // 날짜 포맷팅 (년월) function formatYearMonth(dateStr: string | null): string { diff --git a/src/components/business/construction/issue-management/IssueDetailForm.tsx b/src/components/business/construction/issue-management/IssueDetailForm.tsx index bce5bc61..b8150881 100644 --- a/src/components/business/construction/issue-management/IssueDetailForm.tsx +++ b/src/components/business/construction/issue-management/IssueDetailForm.tsx @@ -2,7 +2,7 @@ import { useState, useCallback, useRef, useEffect } from 'react'; import { useRouter } from 'next/navigation'; -import { getTodayString } from '@/utils/date'; +import { getTodayString } from '@/lib/utils/date'; import { Mic, X, Upload } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; diff --git a/src/components/business/construction/issue-management/IssueManagementListClient.tsx b/src/components/business/construction/issue-management/IssueManagementListClient.tsx index 01b77ef4..c202902d 100644 --- a/src/components/business/construction/issue-management/IssueManagementListClient.tsx +++ b/src/components/business/construction/issue-management/IssueManagementListClient.tsx @@ -48,7 +48,7 @@ import { getIssueStats, withdrawIssues, } from './actions'; -import { formatDate } from '@/utils/date'; +import { formatDate } from '@/lib/utils/date'; // 테이블 컬럼 정의 const tableColumns = [ @@ -84,8 +84,8 @@ export default function IssueManagementListClient({ // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [activeStatTab, setActiveStatTab] = useState<'all' | 'received' | 'in_progress' | 'resolved' | 'unresolved'>('all'); - const [startDate, setStartDate] = useState(''); - const [endDate, setEndDate] = useState(''); + const [startDate, setStartDate] = useState(() => new Date().toISOString().split('T')[0]); + const [endDate, setEndDate] = useState(() => new Date().toISOString().split('T')[0]); const [stats, setStats] = useState(initialStats || null); const [searchQuery, setSearchQuery] = useState(''); const [withdrawDialogOpen, setWithdrawDialogOpen] = useState(false); diff --git a/src/components/business/construction/management/ConstructionDetailClient.tsx b/src/components/business/construction/management/ConstructionDetailClient.tsx index 6271b694..3b5ad154 100644 --- a/src/components/business/construction/management/ConstructionDetailClient.tsx +++ b/src/components/business/construction/management/ConstructionDetailClient.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { useRouter } from 'next/navigation'; -import { getTodayString, formatDate } from '@/utils/date'; +import { getTodayString, formatDate } from '@/lib/utils/date'; import { Plus, Trash2, FileText, Upload, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; diff --git a/src/components/business/construction/management/ConstructionManagementListClient.tsx b/src/components/business/construction/management/ConstructionManagementListClient.tsx index 5fc04149..5700cc5c 100644 --- a/src/components/business/construction/management/ConstructionManagementListClient.tsx +++ b/src/components/business/construction/management/ConstructionManagementListClient.tsx @@ -14,7 +14,7 @@ * - 삭제 기능 없음 (수정만 가능) */ -import { useState, useMemo, useCallback } from 'react'; +import { useState, useEffect, useMemo, useCallback } from 'react'; import { useStatsLoader } from '@/hooks/useStatsLoader'; import { HardHat, Pencil, Clock, CheckCircle } from 'lucide-react'; import { useListHandlers } from '@/hooks/useListHandlers'; @@ -83,8 +83,8 @@ export default function ConstructionManagementListClient({ // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [activeStatTab, setActiveStatTab] = useState<'all' | 'in_progress' | 'completed'>('all'); - const [startDate, setStartDate] = useState(''); - const [endDate, setEndDate] = useState(''); + const [startDate, setStartDate] = useState(() => new Date().toISOString().split('T')[0]); + const [endDate, setEndDate] = useState(() => new Date().toISOString().split('T')[0]); const { data: stats } = useStatsLoader(getConstructionManagementStats, initialStats); const [searchQuery, setSearchQuery] = useState(''); @@ -95,6 +95,16 @@ export default function ConstructionManagementListClient({ const [calendarSiteFilters, setCalendarSiteFilters] = useState([]); const [calendarWorkTeamFilters, setCalendarWorkTeamFilters] = useState([]); + // startDate 변경 시 캘린더 월 자동 이동 + useEffect(() => { + if (startDate) { + const parsed = parseISO(startDate); + if (!isNaN(parsed.getTime())) { + setCalendarDate(parsed); + } + } + }, [startDate]); + // 전체 데이터 (달력 이벤트용) const [allConstructions, setAllConstructions] = useState(initialData); diff --git a/src/components/business/construction/management/ProjectEndDialog.tsx b/src/components/business/construction/management/ProjectEndDialog.tsx index 347ec880..1e433b91 100644 --- a/src/components/business/construction/management/ProjectEndDialog.tsx +++ b/src/components/business/construction/management/ProjectEndDialog.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; -import { getTodayString } from '@/utils/date'; +import { getTodayString } from '@/lib/utils/date'; import { Dialog, DialogContent, diff --git a/src/components/business/construction/order-management/OrderManagementListClient.tsx b/src/components/business/construction/order-management/OrderManagementListClient.tsx index d4e3c493..8efe755c 100644 --- a/src/components/business/construction/order-management/OrderManagementListClient.tsx +++ b/src/components/business/construction/order-management/OrderManagementListClient.tsx @@ -90,8 +90,8 @@ export default function OrderManagementListClient({ ); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== - const [startDate, setStartDate] = useState(''); - const [endDate, setEndDate] = useState(''); + const [startDate, setStartDate] = useState(() => new Date().toISOString().split('T')[0]); + const [endDate, setEndDate] = useState(() => new Date().toISOString().split('T')[0]); const [searchQuery, setSearchQuery] = useState(''); // 달력 관련 상태 @@ -101,6 +101,16 @@ export default function OrderManagementListClient({ const [calendarSiteFilters, setCalendarSiteFilters] = useState([]); const [calendarWorkTeamFilters, setCalendarWorkTeamFilters] = useState([]); + // startDate 변경 시 캘린더 월 자동 이동 + useEffect(() => { + if (startDate) { + const parsed = parseISO(startDate); + if (!isNaN(parsed.getTime())) { + setCalendarDate(parsed); + } + } + }, [startDate]); + // 전체 데이터 (달력 이벤트용) const [allOrders, setAllOrders] = useState(initialData); diff --git a/src/components/business/construction/pricing-management/PricingListClient.tsx b/src/components/business/construction/pricing-management/PricingListClient.tsx index 408c2010..cec74cbb 100644 --- a/src/components/business/construction/pricing-management/PricingListClient.tsx +++ b/src/components/business/construction/pricing-management/PricingListClient.tsx @@ -61,8 +61,8 @@ export default function PricingListClient({ // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [activeStatTab, setActiveStatTab] = useState<'all' | 'in_use' | 'not_registered'>('all'); - const [startDate, setStartDate] = useState(''); - const [endDate, setEndDate] = useState(''); + const [startDate, setStartDate] = useState(() => new Date().toISOString().split('T')[0]); + const [endDate, setEndDate] = useState(() => new Date().toISOString().split('T')[0]); const [stats, setStats] = useState(initialStats || null); const [pricingData, setPricingData] = useState(initialData); diff --git a/src/components/business/construction/progress-billing/ProgressBillingManagementListClient.tsx b/src/components/business/construction/progress-billing/ProgressBillingManagementListClient.tsx index dc74ef9b..557cf144 100644 --- a/src/components/business/construction/progress-billing/ProgressBillingManagementListClient.tsx +++ b/src/components/business/construction/progress-billing/ProgressBillingManagementListClient.tsx @@ -76,8 +76,8 @@ export default function ProgressBillingManagementListClient({ // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [activeStatTab, setActiveStatTab] = useState<'all' | 'contractWaiting' | 'contractComplete'>('all'); - const [startDate, setStartDate] = useState(''); - const [endDate, setEndDate] = useState(''); + const [startDate, setStartDate] = useState(() => new Date().toISOString().split('T')[0]); + const [endDate, setEndDate] = useState(() => new Date().toISOString().split('T')[0]); const { data: stats } = useStatsLoader(getProgressBillingStats, initialStats); const [searchQuery, setSearchQuery] = useState(''); diff --git a/src/components/business/construction/site-briefings/types.ts b/src/components/business/construction/site-briefings/types.ts index 56cc2ac3..4ab66146 100644 --- a/src/components/business/construction/site-briefings/types.ts +++ b/src/components/business/construction/site-briefings/types.ts @@ -2,7 +2,7 @@ * 주일 기업 - 현장설명회 관리 타입 정의 */ -import { getTodayString } from '@/utils/date'; +import { getTodayString } from '@/lib/utils/date'; // 현장설명회 상태 export type SiteBriefingStatus = 'scheduled' | 'ongoing' | 'completed' | 'cancelled' | 'postponed'; diff --git a/src/components/business/construction/site-management/SiteManagementListClient.tsx b/src/components/business/construction/site-management/SiteManagementListClient.tsx index dc63e16f..94c07c74 100644 --- a/src/components/business/construction/site-management/SiteManagementListClient.tsx +++ b/src/components/business/construction/site-management/SiteManagementListClient.tsx @@ -66,8 +66,8 @@ export default function SiteManagementListClient({ // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [activeStatTab, setActiveStatTab] = useState<'all' | 'construction' | 'unregistered'>('all'); - const [startDate, setStartDate] = useState(''); - const [endDate, setEndDate] = useState(''); + const [startDate, setStartDate] = useState(() => new Date().toISOString().split('T')[0]); + const [endDate, setEndDate] = useState(() => new Date().toISOString().split('T')[0]); const [stats, setStats] = useState(initialStats || null); const [searchQuery, setSearchQuery] = useState(''); diff --git a/src/components/business/construction/structure-review/StructureReviewListClient.tsx b/src/components/business/construction/structure-review/StructureReviewListClient.tsx index 2a94bb5e..9aff6ecc 100644 --- a/src/components/business/construction/structure-review/StructureReviewListClient.tsx +++ b/src/components/business/construction/structure-review/StructureReviewListClient.tsx @@ -38,7 +38,7 @@ import { deleteStructureReview, deleteStructureReviews, } from './actions'; -import { formatDate } from '@/utils/date'; +import { formatDate } from '@/lib/utils/date'; // 테이블 컬럼 정의 const tableColumns = [ @@ -77,8 +77,8 @@ export default function StructureReviewListClient({ // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [activeStatTab, setActiveStatTab] = useState<'all' | 'pending' | 'completed'>('all'); - const [startDate, setStartDate] = useState(''); - const [endDate, setEndDate] = useState(''); + const [startDate, setStartDate] = useState(() => new Date().toISOString().split('T')[0]); + const [endDate, setEndDate] = useState(() => new Date().toISOString().split('T')[0]); const [stats, setStats] = useState(initialStats || null); const [searchQuery, setSearchQuery] = useState(''); diff --git a/src/components/business/construction/utility-management/UtilityManagementListClient.tsx b/src/components/business/construction/utility-management/UtilityManagementListClient.tsx index 86de4cad..2cde1d45 100644 --- a/src/components/business/construction/utility-management/UtilityManagementListClient.tsx +++ b/src/components/business/construction/utility-management/UtilityManagementListClient.tsx @@ -42,8 +42,8 @@ import { deleteUtility, deleteUtilities, } from './actions'; -import { formatNumber } from '@/utils/formatAmount'; -import { formatDate } from '@/utils/date'; +import { formatNumber } from '@/lib/utils/amount'; +import { formatDate } from '@/lib/utils/date'; // 테이블 컬럼 정의 const tableColumns = [ @@ -72,8 +72,8 @@ export default function UtilityManagementListClient({ }: UtilityManagementListClientProps) { // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [activeStatTab, setActiveStatTab] = useState<'all' | 'waiting' | 'complete'>('all'); - const [startDate, setStartDate] = useState(''); - const [endDate, setEndDate] = useState(''); + const [startDate, setStartDate] = useState(() => new Date().toISOString().split('T')[0]); + const [endDate, setEndDate] = useState(() => new Date().toISOString().split('T')[0]); const [stats, setStats] = useState(initialStats || null); const [searchQuery, setSearchQuery] = useState(''); diff --git a/src/components/business/construction/worker-status/WorkerStatusListClient.tsx b/src/components/business/construction/worker-status/WorkerStatusListClient.tsx index a638b99e..ceefa5d1 100644 --- a/src/components/business/construction/worker-status/WorkerStatusListClient.tsx +++ b/src/components/business/construction/worker-status/WorkerStatusListClient.tsx @@ -82,8 +82,8 @@ export default function WorkerStatusListClient({ // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [activeStatTab, setActiveStatTab] = useState<'all' | 'all_contract' | 'pending' | 'completed'>('all'); - const [startDate, setStartDate] = useState(''); - const [endDate, setEndDate] = useState(''); + const [startDate, setStartDate] = useState(() => new Date().toISOString().split('T')[0]); + const [endDate, setEndDate] = useState(() => new Date().toISOString().split('T')[0]); const [stats, setStats] = useState(initialStats || null); const [searchQuery, setSearchQuery] = useState(''); diff --git a/src/components/customer-center/InquiryManagement/InquiryForm.tsx b/src/components/customer-center/InquiryManagement/InquiryForm.tsx index 9ed6e3a4..b31ced98 100644 --- a/src/components/customer-center/InquiryManagement/InquiryForm.tsx +++ b/src/components/customer-center/InquiryManagement/InquiryForm.tsx @@ -32,7 +32,9 @@ import { CardHeader, CardTitle, } from '@/components/ui/card'; -import { RichTextEditor } from '@/components/board/RichTextEditor'; +import dynamic from 'next/dynamic'; + +const RichTextEditor = dynamic(() => import('@/components/board/RichTextEditor'), { ssr: false }); import type { Inquiry, InquiryCategory, Attachment } from './types'; import { INQUIRY_CATEGORIES } from './types'; import { createPost, updatePost } from '../shared/actions'; diff --git a/src/components/dev/generators/index.ts b/src/components/dev/generators/index.ts index f7d6b2a3..4be5643e 100644 --- a/src/components/dev/generators/index.ts +++ b/src/components/dev/generators/index.ts @@ -2,7 +2,7 @@ * 샘플 데이터 생성 공통 유틸리티 */ -import { getLocalDateString } from '@/utils/date'; +import { getLocalDateString } from '@/lib/utils/date'; // 랜덤 선택 export function randomPick(arr: readonly T[]): T { diff --git a/src/components/hr/VacationManagement/VacationGrantDialog.tsx b/src/components/hr/VacationManagement/VacationGrantDialog.tsx index d2fdd0aa..5c63009c 100644 --- a/src/components/hr/VacationManagement/VacationGrantDialog.tsx +++ b/src/components/hr/VacationManagement/VacationGrantDialog.tsx @@ -26,6 +26,7 @@ import { import type { VacationGrantFormData, VacationType } from './types'; import { VACATION_TYPE_LABELS } from './types'; import { getActiveEmployees, type EmployeeOption } from './actions'; +import { toast } from 'sonner'; interface VacationGrantDialogProps { open: boolean; @@ -77,15 +78,15 @@ export function VacationGrantDialog({ const handleSave = () => { if (!formData.employeeId) { - alert('사원을 선택해주세요.'); + toast.warning('사원을 선택해주세요.'); return; } if (!formData.grantDate) { - alert('부여일을 선택해주세요.'); + toast.warning('부여일을 선택해주세요.'); return; } if (formData.grantDays < 1) { - alert('부여 일수는 1 이상이어야 합니다.'); + toast.warning('부여 일수는 1 이상이어야 합니다.'); return; } onSave(formData); diff --git a/src/components/hr/VacationManagement/VacationRequestDialog.tsx b/src/components/hr/VacationManagement/VacationRequestDialog.tsx index 8d11f66c..8f2066c6 100644 --- a/src/components/hr/VacationManagement/VacationRequestDialog.tsx +++ b/src/components/hr/VacationManagement/VacationRequestDialog.tsx @@ -24,6 +24,7 @@ import { import type { VacationRequestFormData, LeaveType } from './types'; import { LEAVE_TYPE_LABELS } from './types'; import { getActiveEmployees, type EmployeeOption } from './actions'; +import { toast } from 'sonner'; interface VacationRequestDialogProps { open: boolean; @@ -86,15 +87,15 @@ export function VacationRequestDialog({ const handleSave = () => { if (!formData.employeeId) { - alert('사원을 선택해주세요.'); + toast.warning('사원을 선택해주세요.'); return; } if (!formData.startDate || !formData.endDate) { - alert('휴가 기간을 선택해주세요.'); + toast.warning('휴가 기간을 선택해주세요.'); return; } if (formData.endDate < formData.startDate) { - alert('종료일은 시작일 이후여야 합니다.'); + toast.warning('종료일은 시작일 이후여야 합니다.'); return; } onSave(formData); diff --git a/src/components/hr/VacationManagement/index.tsx b/src/components/hr/VacationManagement/index.tsx index 1a048609..9cac6d3b 100644 --- a/src/components/hr/VacationManagement/index.tsx +++ b/src/components/hr/VacationManagement/index.tsx @@ -60,6 +60,7 @@ import { REQUEST_STATUS_COLORS, } from './types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; +import { toast } from 'sonner'; // ===== Mock 데이터 생성 (request 탭용 - 신청 현황은 leaves API 사용 예정) ===== @@ -751,13 +752,13 @@ export function VacationManagement() { await fetchGrantData(); await fetchUsageData(); } else { - alert(`휴가 부여 실패: ${result.error}`); + toast.error(`휴가 부여 실패: ${result.error}`); console.error('[VacationManagement] 휴가 부여 실패:', result.error); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[VacationManagement] 휴가 부여 에러:', error); - alert('휴가 부여 중 오류가 발생했습니다.'); + toast.error('휴가 부여 중 오류가 발생했습니다.'); } finally { setGrantDialogOpen(false); } @@ -781,13 +782,13 @@ export function VacationManagement() { await fetchLeaveRequests(); await fetchUsageData(); } else { - alert(`휴가 신청 실패: ${result.error}`); + toast.error(`휴가 신청 실패: ${result.error}`); console.error('[VacationManagement] 휴가 신청 실패:', result.error); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[VacationManagement] 휴가 신청 에러:', error); - alert('휴가 신청 중 오류가 발생했습니다.'); + toast.error('휴가 신청 중 오류가 발생했습니다.'); } finally { setRequestDialogOpen(false); } diff --git a/src/components/items/BOMManagementSection.tsx b/src/components/items/BOMManagementSection.tsx index d184a312..f1d752f5 100644 --- a/src/components/items/BOMManagementSection.tsx +++ b/src/components/items/BOMManagementSection.tsx @@ -11,6 +11,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Badge } from '@/components/ui/badge'; import { Plus, Edit, Trash2, Package, GripVertical } from 'lucide-react'; import { toast } from 'sonner'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import type { BOMItem } from '@/contexts/ItemMasterContext'; interface BOMManagementSectionProps { @@ -109,11 +110,17 @@ export function BOMManagementSection({ handleClose(); }; + const [deleteTargetId, setDeleteTargetId] = useState(null); + const handleDelete = (id: number) => { - if (confirm('이 BOM 품목을 삭제하시겠습니까?')) { - onDeleteItem(id); - toast.success('BOM 품목이 삭제되었습니다'); - } + setDeleteTargetId(id); + }; + + const handleDeleteConfirm = () => { + if (deleteTargetId === null) return; + onDeleteItem(deleteTargetId); + toast.success('BOM 품목이 삭제되었습니다'); + setDeleteTargetId(null); }; return ( @@ -288,6 +295,14 @@ export function BOMManagementSection({ + + !open && setDeleteTargetId(null)} + onConfirm={handleDeleteConfirm} + title="BOM 품목 삭제" + description="이 BOM 품목을 삭제하시겠습니까?" + /> ); } \ No newline at end of file diff --git a/src/components/items/DynamicItemForm/hooks/useFileHandling.ts b/src/components/items/DynamicItemForm/hooks/useFileHandling.ts index 2f9526e5..3895e06a 100644 --- a/src/components/items/DynamicItemForm/hooks/useFileHandling.ts +++ b/src/components/items/DynamicItemForm/hooks/useFileHandling.ts @@ -5,6 +5,7 @@ import { deleteItemFile, ItemFileType } from '@/lib/api/items'; import { downloadFileById } from '@/lib/utils/fileDownload'; import { BendingDetail } from '@/types/item'; import { ItemType } from '@/types/item'; +import { toast } from 'sonner'; /** * 파일 정보 타입 (API 응답) @@ -217,7 +218,7 @@ export function useFileHandling({ await downloadFileById(fileId, fileName); } catch (error) { console.error('[useFileHandling] 다운로드 실패:', error); - alert('파일 다운로드에 실패했습니다.'); + toast.error('파일 다운로드에 실패했습니다.'); } }; @@ -247,7 +248,7 @@ export function useFileHandling({ if (!fileId) { console.error('[useFileHandling] 파일 ID를 찾을 수 없습니다:', fileType); - alert('파일 ID를 찾을 수 없습니다.'); + toast.error('파일 ID를 찾을 수 없습니다.'); return; } @@ -276,10 +277,10 @@ export function useFileHandling({ setExistingCertificationFileId(null); } - alert('파일이 삭제되었습니다.'); + toast.success('파일이 삭제되었습니다.'); } catch (error) { console.error('[useFileHandling] 파일 삭제 실패:', error); - alert('파일 삭제에 실패했습니다.'); + toast.error('파일 삭제에 실패했습니다.'); } finally { setIsDeletingFile(null); } diff --git a/src/components/items/DynamicItemForm/index.tsx b/src/components/items/DynamicItemForm/index.tsx index 3e72724e..c127bba3 100644 --- a/src/components/items/DynamicItemForm/index.tsx +++ b/src/components/items/DynamicItemForm/index.tsx @@ -35,6 +35,7 @@ import type { ItemFieldResponse } from '@/types/item-master-api'; import { uploadItemFile, ItemFileType, checkItemCodeDuplicate, DuplicateCheckResult } from '@/lib/api/items'; import { DuplicateCodeError } from '@/lib/api/error-handler'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; +import { toast } from 'sonner'; /** * 메인 DynamicItemForm 컴포넌트 @@ -400,7 +401,7 @@ export default function DynamicItemForm({ if (fileUploadErrors.length > 0) { console.warn('[DynamicItemForm] 일부 파일 업로드 실패:', fileUploadErrors.join(', ')); // 품목은 저장되었으므로 경고만 표시하고 진행 - alert(`품목이 저장되었습니다.\n\n일부 파일 업로드에 실패했습니다: ${fileUploadErrors.join(', ')}\n수정 화면에서 다시 업로드해 주세요.`); + toast.warning(`품목이 저장되었습니다. 일부 파일 업로드에 실패했습니다: ${fileUploadErrors.join(', ')}. 수정 화면에서 다시 업로드해 주세요.`); } } diff --git a/src/components/items/ItemDetailClient.tsx b/src/components/items/ItemDetailClient.tsx index 3e20d6c7..1ae5099e 100644 --- a/src/components/items/ItemDetailClient.tsx +++ b/src/components/items/ItemDetailClient.tsx @@ -32,6 +32,7 @@ import { downloadFileById } from '@/lib/utils/fileDownload'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { useMenuStore } from '@/stores/menuStore'; import { usePermission } from '@/hooks/usePermission'; +import { toast } from 'sonner'; interface ItemDetailClientProps { item: ItemMaster; @@ -66,7 +67,7 @@ async function handleFileDownload(fileId: number | undefined, fileName?: string) } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[ItemDetailClient] 다운로드 실패:', error); - alert('파일 다운로드에 실패했습니다.'); + toast.error('파일 다운로드에 실패했습니다.'); } } diff --git a/src/components/items/ItemForm/BendingDiagramSection.tsx b/src/components/items/ItemForm/BendingDiagramSection.tsx index 8bb5091a..1522856b 100644 --- a/src/components/items/ItemForm/BendingDiagramSection.tsx +++ b/src/components/items/ItemForm/BendingDiagramSection.tsx @@ -12,6 +12,7 @@ import type { BendingDetail } from '@/types/item'; import type { UseFormSetValue } from 'react-hook-form'; import type { CreateItemFormData } from '@/lib/utils/validation'; import { downloadFileById } from '@/lib/utils/fileDownload'; +import { toast } from 'sonner'; export interface BendingDiagramSectionProps { selectedPartType: string; @@ -63,7 +64,7 @@ export default function BendingDiagramSection({ // 기존 파일 다운로드 핸들러 const handleDownloadExistingFile = async () => { if (!existingBendingDiagramFileId) { - alert('파일 ID가 없습니다.'); + toast.error('파일 ID가 없습니다.'); return; } @@ -72,7 +73,7 @@ export default function BendingDiagramSection({ await downloadFileById(existingBendingDiagramFileId, fileName); } catch (error) { console.error('[BendingDiagramSection] 파일 다운로드 실패:', error); - alert('파일 다운로드에 실패했습니다.'); + toast.error('파일 다운로드에 실패했습니다.'); } }; // 폭 합계 업데이트 헬퍼 diff --git a/src/components/items/ItemForm/index.tsx b/src/components/items/ItemForm/index.tsx index be414ab5..b5d25bd9 100644 --- a/src/components/items/ItemForm/index.tsx +++ b/src/components/items/ItemForm/index.tsx @@ -33,6 +33,7 @@ import BendingDiagramSection from './BendingDiagramSection'; import BOMSection from './BOMSection'; import { MaterialForm, ProductForm, ProductCertificationSection, PartForm } from './forms'; import { useItemFormState } from './hooks'; +import { toast } from 'sonner'; export default function ItemForm({ mode, initialData, onSubmit }: ItemFormProps) { const router = useRouter(); @@ -177,7 +178,7 @@ export default function ItemForm({ mode, initialData, onSubmit }: ItemFormProps) router.push('/production/screen-production'); router.refresh(); } catch { - alert('품목 저장에 실패했습니다.'); + toast.error('품목 저장에 실패했습니다.'); } finally { setIsSubmitting(false); } diff --git a/src/components/items/ItemListClient.tsx b/src/components/items/ItemListClient.tsx index 4220c4a0..97972230 100644 --- a/src/components/items/ItemListClient.tsx +++ b/src/components/items/ItemListClient.tsx @@ -32,6 +32,7 @@ import { type StatCard, } from '@/components/templates/UniversalListPage'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; +import { toast } from 'sonner'; // Debounce 훅 function useDebounce(value: T, delay: number): T { @@ -182,7 +183,7 @@ export default function ItemListClient() { } catch (error) { if (isNextRedirectError(error)) throw error; console.error('품목 삭제 실패:', error); - alert(error instanceof Error ? error.message : '품목 삭제에 실패했습니다.'); + toast.error(error instanceof Error ? error.message : '품목 삭제에 실패했습니다.'); } finally { setDeleteDialogOpen(false); setItemToDelete(null); @@ -225,10 +226,10 @@ export default function ItemListClient() { } if (successCount > 0) { - alert(`${successCount}개 품목이 삭제되었습니다.${failCount > 0 ? ` (${failCount}개 실패)` : ''}`); + toast.success(`${successCount}개 품목이 삭제되었습니다.${failCount > 0 ? ` (${failCount}개 실패)` : ''}`); refresh(); } else { - alert('품목 삭제에 실패했습니다.'); + toast.error('품목 삭제에 실패했습니다.'); } }; @@ -316,22 +317,22 @@ export default function ItemListClient() { const errorMessages = result.errors.slice(0, 5).map( (err) => `${err.row}행: ${err.message}` ).join('\n'); - alert(`업로드 오류:\n${errorMessages}${result.errors.length > 5 ? `\n... 외 ${result.errors.length - 5}건` : ''}`); + toast.error('업로드 오류', { description: `${errorMessages}${result.errors.length > 5 ? ` (외 ${result.errors.length - 5}건)` : ''}` }); return; } if (result.data.length === 0) { - alert('업로드할 데이터가 없습니다.'); + toast.warning('업로드할 데이터가 없습니다.'); return; } // TODO: 실제 API 호출로 데이터 저장 // 지금은 파싱 결과만 확인 - alert(`${result.data.length}건의 데이터가 파싱되었습니다.\n(실제 등록 기능은 추후 구현 예정)`); + toast.info(`${result.data.length}건의 데이터가 파싱되었습니다. (실제 등록 기능은 추후 구현 예정)`); } catch (error) { console.error('[Excel Upload] 오류:', error); - alert('파일 업로드에 실패했습니다.'); + toast.error('파일 업로드에 실패했습니다.'); } finally { // input 초기화 (같은 파일 재선택 가능하도록) if (fileInputRef.current) { diff --git a/src/components/items/ItemMasterDataManagement/tabs/HierarchyTab/index.tsx b/src/components/items/ItemMasterDataManagement/tabs/HierarchyTab/index.tsx index b21c08d5..be2e2f3a 100644 --- a/src/components/items/ItemMasterDataManagement/tabs/HierarchyTab/index.tsx +++ b/src/components/items/ItemMasterDataManagement/tabs/HierarchyTab/index.tsx @@ -1,11 +1,17 @@ -import type { Dispatch, SetStateAction } from 'react'; +import { useState, useCallback, type Dispatch, type SetStateAction } from 'react'; import type { ItemPage, ItemSection, ItemField, BOMItem } from '@/contexts/ItemMasterContext'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Plus, Edit, Trash2, Link, Copy, Download } from 'lucide-react'; import { toast } from 'sonner'; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; + +type HierarchyConfirmAction = + | { type: 'deletePage'; pageId: number } + | { type: 'unlinkSection'; pageId: number; sectionId: number } + | { type: 'unlinkField'; pageId: string; sectionId: string; fieldId: string }; import { DraggableSection, DraggableField } from '../../components'; import { BOMManagementSection } from '@/components/items/BOMManagementSection'; @@ -121,6 +127,37 @@ export function HierarchyTab({ updateBOMItem, deleteBOMItem, }: HierarchyTabProps) { + const [confirmAction, setConfirmAction] = useState(null); + + const handleConfirmAction = useCallback(() => { + if (!confirmAction) return; + switch (confirmAction.type) { + case 'deletePage': + deleteItemPage(confirmAction.pageId); + if (selectedPageId === confirmAction.pageId) { + setSelectedPageId(itemPages[0]?.id || null); + } + toast.success('섹션이 삭제되었습니다'); + break; + case 'unlinkSection': + unlinkSection(confirmAction.pageId, confirmAction.sectionId); + break; + case 'unlinkField': + deleteField(confirmAction.pageId, confirmAction.sectionId, confirmAction.fieldId); + toast.success('항목 연결이 해제되었습니다'); + break; + } + setConfirmAction(null); + }, [confirmAction, deleteItemPage, selectedPageId, setSelectedPageId, itemPages, unlinkSection, deleteField]); + + const confirmDialogConfig = confirmAction + ? { + deletePage: { title: '섹션 삭제', description: '이 섹션과 모든 하위섹션, 항목을 삭제하시겠습니까?' }, + unlinkSection: { title: '섹션 연결 해제', description: '이 섹션을 페이지에서 연결 해제하시겠습니까?\n(섹션 데이터는 섹션 탭에 유지됩니다)' }, + unlinkField: { title: '항목 연결 해제', description: '이 항목을 섹션에서 연결 해제하시겠습니까?\n(항목 데이터는 항목 탭에 유지됩니다)' }, + }[confirmAction.type] + : { title: '', description: '' }; + return (
{/* 섹션 목록 */} @@ -211,13 +248,7 @@ export function HierarchyTab({ className="h-6 w-6 p-0" onClick={(e) => { e.stopPropagation(); - if (confirm('이 섹션과 모든 하위섹션, 항목을 삭제하시겠습니까?')) { - deleteItemPage(page.id); - if (selectedPageId === page.id) { - setSelectedPageId(itemPages[0]?.id || null); - } - toast.success('섹션이 삭제되었습니다'); - } + setConfirmAction({ type: 'deletePage', pageId: page.id }); }} title="삭제" > @@ -256,7 +287,7 @@ export function HierarchyTab({ // Modern API 시도 (브라우저 환경 체크) if (typeof window !== 'undefined' && window.navigator.clipboard && window.navigator.clipboard.writeText) { window.navigator.clipboard.writeText(text) - .then(() => alert('경로가 클립보드에 복사되었습니다')) + .then(() => toast.success('경로가 클립보드에 복사되었습니다')) .catch(() => { // Fallback 방식 const textArea = document.createElement('textarea'); @@ -267,9 +298,9 @@ export function HierarchyTab({ textArea.select(); try { document.execCommand('copy'); - alert('경로가 클립보드에 복사되었습니다'); + toast.success('경로가 클립보드에 복사되었습니다'); } catch { - alert('복사에 실패했습니다'); + toast.error('복사에 실패했습니다'); } document.body.removeChild(textArea); }); @@ -283,9 +314,9 @@ export function HierarchyTab({ textArea.select(); try { document.execCommand('copy'); - alert('경로가 클립보드에 복사되었습니다'); + toast.success('경로가 클립보드에 복사되었습니다'); } catch { - alert('복사에 실패했습니다'); + toast.error('복사에 실패했습니다'); } document.body.removeChild(textArea); } @@ -353,9 +384,7 @@ export function HierarchyTab({ moveSection(dragIndex, hoverIndex); }} onDelete={() => { - if (confirm('이 섹션을 페이지에서 연결 해제하시겠습니까?\n(섹션 데이터는 섹션 탭에 유지됩니다)')) { - unlinkSection(selectedPage.id, section.id); - } + setConfirmAction({ type: 'unlinkSection', pageId: selectedPage.id, sectionId: section.id }); }} onEditTitle={handleEditSectionTitle} editingSectionId={editingSectionId} @@ -447,10 +476,7 @@ export function HierarchyTab({ index={fieldIndex} moveField={(dragFieldId, hoverFieldId) => moveField(section.id, dragFieldId, hoverFieldId)} onDelete={() => { - if (confirm('이 항목을 섹션에서 연결 해제하시겠습니까?\n(항목 데이터는 항목 탭에 유지됩니다)')) { - deleteField(String(selectedPage.id), String(section.id), String(field.id)); - toast.success('항목 연결이 해제되었습니다'); - } + setConfirmAction({ type: 'unlinkField', pageId: String(selectedPage.id), sectionId: String(section.id), fieldId: String(field.id) }); }} onEdit={() => handleEditField(String(section.id), field)} /> @@ -493,6 +519,15 @@ export function HierarchyTab({ )} + + !open && setConfirmAction(null)} + onConfirm={handleConfirmAction} + title={confirmDialogConfig.title} + description={confirmDialogConfig.description} + variant="destructive" + />
); } diff --git a/src/components/material/ReceivingManagement/InspectionCreate.tsx b/src/components/material/ReceivingManagement/InspectionCreate.tsx index 0f63c4dd..905f10d5 100644 --- a/src/components/material/ReceivingManagement/InspectionCreate.tsx +++ b/src/components/material/ReceivingManagement/InspectionCreate.tsx @@ -13,7 +13,7 @@ import { useState, useCallback, useMemo, useEffect } from 'react'; import { useRouter } from 'next/navigation'; -import { getTodayString } from '@/utils/date'; +import { getTodayString } from '@/lib/utils/date'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { materialInspectionCreateConfig } from './inspectionConfig'; import { ContentSkeleton } from '@/components/ui/skeleton'; diff --git a/src/components/material/ReceivingManagement/ReceivingReceiptContent.tsx b/src/components/material/ReceivingManagement/ReceivingReceiptContent.tsx index 05f04ddd..2269585e 100644 --- a/src/components/material/ReceivingManagement/ReceivingReceiptContent.tsx +++ b/src/components/material/ReceivingManagement/ReceivingReceiptContent.tsx @@ -9,7 +9,7 @@ import type { ReceivingDetail } from './types'; import { DocumentHeader } from '@/components/document-system'; -import { getTodayString } from '@/utils/date'; +import { getTodayString } from '@/lib/utils/date'; interface ReceivingReceiptContentProps { data: ReceivingDetail; diff --git a/src/components/material/StockStatus/mockData.ts b/src/components/material/StockStatus/mockData.ts index 052f0aac..1c7b3699 100644 --- a/src/components/material/StockStatus/mockData.ts +++ b/src/components/material/StockStatus/mockData.ts @@ -3,7 +3,7 @@ */ import type { StockItem, StockDetail, StockStats, FilterTab } from './types'; -import { getLocalDateString } from '@/utils/date'; +import { getLocalDateString } from '@/lib/utils/date'; // 재고 상태 결정 함수 function getStockStatus(stockQty: number, safetyStock: number): 'normal' | 'low' | 'out' { diff --git a/src/components/molecules/DateRangeSelector.tsx b/src/components/molecules/DateRangeSelector.tsx index c253a065..8150d15f 100644 --- a/src/components/molecules/DateRangeSelector.tsx +++ b/src/components/molecules/DateRangeSelector.tsx @@ -4,6 +4,7 @@ import { ReactNode, useCallback } from 'react'; import { format, startOfYear, endOfYear, subMonths, startOfMonth, endOfMonth, subDays } from 'date-fns'; import { Button } from '@/components/ui/button'; import { DatePicker } from '@/components/ui/date-picker'; +import { DateRangePicker } from '@/components/ui/date-range-picker'; import { ScrollableButtonGroup } from '@/components/atoms/ScrollableButtonGroup'; /** @@ -54,6 +55,8 @@ interface DateRangeSelectorProps { dateInputWidth?: string; /** 프리셋 버튼 위치: 'inline' (날짜 옆), 'below' (별도 줄) */ presetsPosition?: 'inline' | 'below'; + /** 날짜 입력 변형: 'split' (DatePicker 2개), 'combined' (DateRangePicker 1개) */ + variant?: 'split' | 'combined'; } /** @@ -89,6 +92,7 @@ export function DateRangeSelector({ hideDateInputs = false, dateInputWidth = 'w-[140px]', presetsPosition = 'inline', + variant = 'combined', }: DateRangeSelectorProps) { // 프리셋 클릭 핸들러 @@ -167,33 +171,53 @@ export function DateRangeSelector({ ); }; + // 날짜 입력 영역 렌더링 + const renderDateInputs = () => { + if (hideDateInputs) return null; + + if (variant === 'combined') { + return ( +
+ +
+ ); + } + + return ( +
+ + ~ + +
+ ); + }; + // presetsPosition이 'below'일 때: 달력+extraActions 같은 줄, 프리셋은 아래 줄 if (presetsPosition === 'below') { return (
{/* 1줄: 날짜 + extraActions */}
- {/* 날짜 범위 선택 */} - {!hideDateInputs && ( -
- - ~ - -
- )} - {/* extraActions (검색창 등) */} + {renderDateInputs()} {extraActions}
@@ -210,25 +234,7 @@ export function DateRangeSelector({ return (
{/* 날짜 범위 선택 */} - {!hideDateInputs && ( -
- - ~ - -
- )} + {renderDateInputs()} {/* 기간 버튼들 - 달력 바로 옆 */} {renderPresets()} diff --git a/src/components/orders/OrderRegistration.tsx b/src/components/orders/OrderRegistration.tsx index 4e772e8f..ccc09b3d 100644 --- a/src/components/orders/OrderRegistration.tsx +++ b/src/components/orders/OrderRegistration.tsx @@ -58,7 +58,7 @@ import { FormSection } from "@/components/organisms/FormSection"; import { QuotationSelectDialog } from "./QuotationSelectDialog"; import { type QuotationForSelect, type QuotationItem } from "./actions"; import { ItemAddDialog, OrderItem } from "./ItemAddDialog"; -import { formatAmount } from "@/utils/formatAmount"; +import { formatAmount } from "@/lib/utils/amount"; import { cn } from "@/lib/utils"; import { useDevFill } from "@/components/dev"; import { generateOrderData } from "@/components/dev/generators/orderData"; diff --git a/src/components/orders/OrderSalesDetailEdit.tsx b/src/components/orders/OrderSalesDetailEdit.tsx index 525cad7e..2e55b7b7 100644 --- a/src/components/orders/OrderSalesDetailEdit.tsx +++ b/src/components/orders/OrderSalesDetailEdit.tsx @@ -39,7 +39,7 @@ import { toast } from "sonner"; import { IntegratedDetailTemplate } from "@/components/templates/IntegratedDetailTemplate"; import { orderSalesConfig } from "./orderSalesConfig"; import { BadgeSm } from "@/components/atoms/BadgeSm"; -import { formatAmount } from "@/utils/formatAmount"; +import { formatAmount } from "@/lib/utils/amount"; import { OrderItem, getOrderById, diff --git a/src/components/orders/OrderSalesDetailView.tsx b/src/components/orders/OrderSalesDetailView.tsx index c643a6a0..835faf9c 100644 --- a/src/components/orders/OrderSalesDetailView.tsx +++ b/src/components/orders/OrderSalesDetailView.tsx @@ -38,7 +38,7 @@ import { toast } from "sonner"; import { IntegratedDetailTemplate } from "@/components/templates/IntegratedDetailTemplate"; import { orderSalesConfig } from "./orderSalesConfig"; import { BadgeSm } from "@/components/atoms/BadgeSm"; -import { formatAmount } from "@/utils/formatAmount"; +import { formatAmount } from "@/lib/utils/amount"; import { Dialog, DialogContent, diff --git a/src/components/orders/QuotationSelectDialog.tsx b/src/components/orders/QuotationSelectDialog.tsx index 55d07412..17423640 100644 --- a/src/components/orders/QuotationSelectDialog.tsx +++ b/src/components/orders/QuotationSelectDialog.tsx @@ -10,7 +10,7 @@ import { useCallback } from 'react'; import { FileText, Check } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; -import { formatAmount } from '@/utils/formatAmount'; +import { formatAmount } from '@/lib/utils/amount'; import { cn } from '@/lib/utils'; import { SearchableSelectionModal } from '@/components/organisms/SearchableSelectionModal'; import { getQuotesForSelect, type QuotationForSelect } from './actions'; diff --git a/src/components/orders/documents/ContractDocument.tsx b/src/components/orders/documents/ContractDocument.tsx index 2b4c23e5..4ac2670d 100644 --- a/src/components/orders/documents/ContractDocument.tsx +++ b/src/components/orders/documents/ContractDocument.tsx @@ -7,7 +7,7 @@ * - DocumentHeader: simple 레이아웃 (결재란 없음) */ -import { formatAmount } from "@/utils/formatAmount"; +import { formatAmount } from "@/lib/utils/amount"; import { OrderItem } from "../actions"; import { DocumentHeader } from "@/components/document-system"; diff --git a/src/components/orders/documents/PurchaseOrderDocument.tsx b/src/components/orders/documents/PurchaseOrderDocument.tsx index a288eb61..56b5288a 100644 --- a/src/components/orders/documents/PurchaseOrderDocument.tsx +++ b/src/components/orders/documents/PurchaseOrderDocument.tsx @@ -5,7 +5,7 @@ * - 스크린샷 형식 + 지출결의서 디자인 스타일 */ -import { getTodayString } from "@/utils/date"; +import { getTodayString } from "@/lib/utils/date"; import { OrderItem } from "../actions"; /** diff --git a/src/components/orders/documents/SalesOrderDocument.tsx b/src/components/orders/documents/SalesOrderDocument.tsx index e36069c1..317ab590 100644 --- a/src/components/orders/documents/SalesOrderDocument.tsx +++ b/src/components/orders/documents/SalesOrderDocument.tsx @@ -8,7 +8,7 @@ */ import { useState } from "react"; -import { getTodayString } from "@/utils/date"; +import { getTodayString } from "@/lib/utils/date"; import { OrderItem } from "../actions"; import { ProductInfo } from "./OrderDocumentModal"; import { ConstructionApprovalTable } from "@/components/document-system"; diff --git a/src/components/orders/documents/TransactionDocument.tsx b/src/components/orders/documents/TransactionDocument.tsx index e3a238ed..e717b899 100644 --- a/src/components/orders/documents/TransactionDocument.tsx +++ b/src/components/orders/documents/TransactionDocument.tsx @@ -7,7 +7,7 @@ * - DocumentHeader: simple 레이아웃 (결재란 없음) */ -import { formatAmount } from "@/utils/formatAmount"; +import { formatAmount } from "@/lib/utils/amount"; import { OrderItem } from "../actions"; import { DocumentHeader } from "@/components/document-system"; diff --git a/src/components/outbound/ShipmentManagement/ShipmentCreate.tsx b/src/components/outbound/ShipmentManagement/ShipmentCreate.tsx index de59c71c..82763328 100644 --- a/src/components/outbound/ShipmentManagement/ShipmentCreate.tsx +++ b/src/components/outbound/ShipmentManagement/ShipmentCreate.tsx @@ -8,7 +8,7 @@ import { useState, useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { Plus, X as XIcon, ChevronDown, Search } from 'lucide-react'; -import { getTodayString } from '@/utils/date'; +import { getTodayString } from '@/lib/utils/date'; import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; import { Label } from '@/components/ui/label'; diff --git a/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx b/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx index 50f289af..beabf024 100644 --- a/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx +++ b/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx @@ -72,6 +72,7 @@ import type { import { ShippingSlip } from './documents/ShippingSlip'; import { DeliveryConfirmation } from './documents/DeliveryConfirmation'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; +import { toast } from 'sonner'; interface ShipmentDetailProps { id: string; @@ -152,12 +153,12 @@ export function ShipmentDetail({ id }: ShipmentDetailProps) { if (result.success) { router.push('/ko/outbound/shipments'); } else { - alert(result.error || '삭제에 실패했습니다.'); + toast.error(result.error || '삭제에 실패했습니다.'); } } catch (err) { if (isNextRedirectError(err)) throw err; console.error('[ShipmentDetail] handleDelete error:', err); - alert('삭제 중 오류가 발생했습니다.'); + toast.error('삭제 중 오류가 발생했습니다.'); } finally { setIsDeleting(false); setShowDeleteDialog(false); @@ -205,12 +206,12 @@ export function ShipmentDetail({ id }: ShipmentDetailProps) { setDetail(result.data); setShowStatusDialog(false); } else { - alert(result.error || '상태 변경에 실패했습니다.'); + toast.error(result.error || '상태 변경에 실패했습니다.'); } } catch (err) { if (isNextRedirectError(err)) throw err; console.error('[ShipmentDetail] handleStatusChange error:', err); - alert('상태 변경 중 오류가 발생했습니다.'); + toast.error('상태 변경 중 오류가 발생했습니다.'); } finally { setIsChangingStatus(false); } diff --git a/src/components/outbound/ShipmentManagement/ShipmentList.tsx b/src/components/outbound/ShipmentManagement/ShipmentList.tsx index 9235958b..281a6b6d 100644 --- a/src/components/outbound/ShipmentManagement/ShipmentList.tsx +++ b/src/components/outbound/ShipmentManagement/ShipmentList.tsx @@ -12,7 +12,7 @@ * - 하단 출고 스케줄 캘린더 (시간축 주간 뷰) */ -import { useState, useMemo, useCallback } from 'react'; +import { useState, useEffect, useMemo, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { useStatsLoader } from '@/hooks/useStatsLoader'; import { @@ -45,6 +45,7 @@ import { DELIVERY_METHOD_LABELS, } from './types'; import type { ShipmentItem, ShipmentStatus, ShipmentStats } from './types'; +import { parseISO } from 'date-fns'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; // 페이지당 항목 수 @@ -72,6 +73,16 @@ export function ShipmentList() { const [scheduleView, setScheduleView] = useState('day-time'); const [shipmentData, setShipmentData] = useState([]); + // startDate 변경 시 캘린더 월 자동 이동 + useEffect(() => { + if (startDate) { + const parsed = parseISO(startDate); + if (!isNaN(parsed.getTime())) { + setCalendarDate(parsed); + } + } + }, [startDate]); + // ===== 행 클릭 핸들러 ===== const handleRowClick = useCallback( (item: ShipmentItem) => { diff --git a/src/components/pricing/PricingFormClient.tsx b/src/components/pricing/PricingFormClient.tsx index 197ecad8..394da55c 100644 --- a/src/components/pricing/PricingFormClient.tsx +++ b/src/components/pricing/PricingFormClient.tsx @@ -13,7 +13,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { useRouter } from 'next/navigation'; -import { getTodayString } from '@/utils/date'; +import { getTodayString } from '@/lib/utils/date'; import { DollarSign, Package, diff --git a/src/components/process-management/RuleModal.tsx b/src/components/process-management/RuleModal.tsx index 207148b2..37a9f44e 100644 --- a/src/components/process-management/RuleModal.tsx +++ b/src/components/process-management/RuleModal.tsx @@ -30,6 +30,7 @@ import { import { Search } from 'lucide-react'; import type { ClassificationRule } from '@/types/process'; import { PROCESS_CATEGORY_OPTIONS } from '@/types/process'; +import { toast } from 'sonner'; // 공정 필터 옵션 const PROCESS_FILTER_OPTIONS = [ @@ -179,7 +180,7 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule, processId, proc // 저장 const handleSubmit = () => { if (selectedItemIds.size === 0) { - alert('품목을 최소 1개 이상 선택해주세요.'); + toast.warning('품목을 최소 1개 이상 선택해주세요.'); return; } diff --git a/src/components/production/WorkOrders/WorkOrderEdit.tsx b/src/components/production/WorkOrders/WorkOrderEdit.tsx index 9a3da2bb..d084d83b 100644 --- a/src/components/production/WorkOrders/WorkOrderEdit.tsx +++ b/src/components/production/WorkOrders/WorkOrderEdit.tsx @@ -34,6 +34,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { AssigneeSelectModal } from './AssigneeSelectModal'; import { toast } from 'sonner'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { getWorkOrderById, updateWorkOrder, getProcessOptions, type ProcessOption } from './actions'; import type { WorkOrder, WorkOrderItem, ProcessStep } from './types'; @@ -98,6 +99,7 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) { note: '', }); const [isAssigneeModalOpen, setIsAssigneeModalOpen] = useState(false); + const [deleteTargetItemId, setDeleteTargetItemId] = useState(null); const [assigneeNames, setAssigneeNames] = useState([]); const [validationErrors, setValidationErrors] = useState({}); const [isLoading, setIsLoading] = useState(true); @@ -322,11 +324,16 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) { // 품목 삭제 const handleItemDelete = useCallback((itemId: string) => { - if (!confirm('정말 삭제하시겠습니까?')) return; - setItems(prev => prev.filter(item => item.id !== itemId)); + setDeleteTargetItemId(itemId); + }, []); + + const handleItemDeleteConfirm = useCallback(() => { + if (!deleteTargetItemId) return; + setItems(prev => prev.filter(item => item.id !== deleteTargetItemId)); // TODO: API 호출로 서버에서 삭제 toast.success('품목이 삭제되었습니다.'); - }, []); + setDeleteTargetItemId(null); + }, [deleteTargetItemId]); // 동적 config (작업지시 번호 포함) const dynamicConfig = { @@ -651,6 +658,15 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) { setAssigneeNames(names); }} /> + + {/* 품목 삭제 확인 다이얼로그 */} + !open && setDeleteTargetItemId(null)} + onConfirm={handleItemDeleteConfirm} + title="품목 삭제" + description="이 품목을 삭제하시겠습니까?" + /> ); } \ No newline at end of file diff --git a/src/components/quality/InspectionManagement/InspectionList.tsx b/src/components/quality/InspectionManagement/InspectionList.tsx index 3b81e5fe..ddbaf9e9 100644 --- a/src/components/quality/InspectionManagement/InspectionList.tsx +++ b/src/components/quality/InspectionManagement/InspectionList.tsx @@ -45,6 +45,7 @@ import { ScheduleCalendar } from '@/components/common/ScheduleCalendar/ScheduleC import type { ScheduleEvent, CalendarView } from '@/components/common/ScheduleCalendar/types'; import { getInspections, getInspectionStats, getInspectionCalendar } from './actions'; import { statusColorMap } from './mockData'; +import { parseISO } from 'date-fns'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import type { ProductInspection, InspectionStats, InspectionStatus } from './types'; @@ -74,6 +75,16 @@ export function InspectionList() { const [calendarStatusFilter, setCalendarStatusFilter] = useState('전체'); const [calendarInspectorFilter, setCalendarInspectorFilter] = useState('전체'); + // startDate 변경 시 캘린더 월 자동 이동 + useEffect(() => { + if (startDate) { + const parsed = parseISO(startDate); + if (!isNaN(parsed.getTime())) { + setCalendarDate(parsed); + } + } + }, [startDate]); + // 캘린더 데이터 로드 const loadCalendarData = useCallback(async () => { try { diff --git a/src/components/quotes/QuoteManagementClient.tsx b/src/components/quotes/QuoteManagementClient.tsx index ce9db2eb..e48ad60c 100644 --- a/src/components/quotes/QuoteManagementClient.tsx +++ b/src/components/quotes/QuoteManagementClient.tsx @@ -53,7 +53,7 @@ import { StandardDialog } from '@/components/molecules/StandardDialog'; import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { useDeleteDialog } from '@/hooks/useDeleteDialog'; import { toast } from 'sonner'; -import { formatAmount, formatAmountManwon } from '@/utils/formatAmount'; +import { formatAmount, formatAmountManwon } from '@/lib/utils/amount'; import type { Quote, QuoteFilterType } from './types'; import { PRODUCT_CATEGORY_LABELS } from './types'; import { getQuotes, deleteQuote, bulkDeleteQuotes } from './actions'; diff --git a/src/components/quotes/QuoteRegistration.tsx b/src/components/quotes/QuoteRegistration.tsx index c04caf9d..84d19d0b 100644 --- a/src/components/quotes/QuoteRegistration.tsx +++ b/src/components/quotes/QuoteRegistration.tsx @@ -52,7 +52,7 @@ import { isNextRedirectError } from "@/lib/utils/redirect-error"; import { useDevFill } from "@/components/dev/useDevFill"; import type { Vendor } from "../accounting/VendorManagement"; import type { BomMaterial, CalculationResults, BomCalculationResultItem } from "./types"; -import { getLocalDateString, getDateAfterDays } from "@/utils/date"; +import { getLocalDateString, getDateAfterDays } from "@/lib/utils/date"; // ============================================================================= // 타입 정의 diff --git a/src/components/quotes/types.ts b/src/components/quotes/types.ts index c15ad936..27b07f69 100644 --- a/src/components/quotes/types.ts +++ b/src/components/quotes/types.ts @@ -6,7 +6,7 @@ * - product_category: screen, steel */ -import { formatDateForInput } from "@/utils/date"; +import { formatDateForInput } from "@/lib/utils/date"; // ===== 견적 상태 ===== export type QuoteStatus = diff --git a/src/components/settings/PopupManagement/PopupForm.tsx b/src/components/settings/PopupManagement/PopupForm.tsx index c7f1d541..b0854564 100644 --- a/src/components/settings/PopupManagement/PopupForm.tsx +++ b/src/components/settings/PopupManagement/PopupForm.tsx @@ -34,7 +34,9 @@ import { CardHeader, CardTitle, } from '@/components/ui/card'; -import { RichTextEditor } from '@/components/board/RichTextEditor'; +import dynamic from 'next/dynamic'; + +const RichTextEditor = dynamic(() => import('@/components/board/RichTextEditor'), { ssr: false }); import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { type Popup, diff --git a/src/components/templates/IntegratedListTemplateV2.tsx b/src/components/templates/IntegratedListTemplateV2.tsx index eea9060c..3020c702 100644 --- a/src/components/templates/IntegratedListTemplateV2.tsx +++ b/src/components/templates/IntegratedListTemplateV2.tsx @@ -115,6 +115,8 @@ export interface IntegratedListTemplateV2Props { presetLabels?: Partial>; /** 프리셋 버튼 위치 */ presetsPosition?: 'inline' | 'below'; + /** 날짜 입력 변형: 'split' (DatePicker 2개), 'combined' (DateRangePicker 1개) */ + variant?: 'split' | 'combined'; startDate?: string; endDate?: string; onStartDateChange?: (date: string) => void; @@ -612,6 +614,7 @@ export function IntegratedListTemplateV2({ presets={dateRangeSelector.presets} presetLabels={dateRangeSelector.presetLabels} presetsPosition={dateRangeSelector.presetsPosition} + variant={dateRangeSelector.variant} extraActions={ <> {/* hideSearch=true면 검색창 자동 추가 (extraActions 앞에) */} diff --git a/src/components/templates/UniversalListPage/types.ts b/src/components/templates/UniversalListPage/types.ts index 41f42d70..64374705 100644 --- a/src/components/templates/UniversalListPage/types.ts +++ b/src/components/templates/UniversalListPage/types.ts @@ -289,6 +289,8 @@ export interface UniversalListConfig { presetLabels?: Partial>; /** 프리셋 버튼 위치: 'inline' (날짜 옆), 'below' (별도 줄) */ presetsPosition?: 'inline' | 'below'; + /** 날짜 입력 변형: 'split' (DatePicker 2개), 'combined' (DateRangePicker 1개) */ + variant?: 'split' | 'combined'; startDate?: string; endDate?: string; onStartDateChange?: (date: string) => void; diff --git a/src/components/ui/date-range-picker.tsx b/src/components/ui/date-range-picker.tsx new file mode 100644 index 00000000..d0691b8d --- /dev/null +++ b/src/components/ui/date-range-picker.tsx @@ -0,0 +1,320 @@ +"use client"; + +import * as React from "react"; +import { format, parse, isValid } from "date-fns"; +import { ko } from "date-fns/locale"; +import { CalendarIcon, ChevronLeft, ChevronRight } from "lucide-react"; +import type { DateRange } from "react-day-picker"; + +import { cn } from "./utils"; +import { Button } from "./button"; +import { Calendar } from "./calendar"; +import { Popover, PopoverContent, PopoverTrigger } from "./popover"; + +interface DateRangePickerProps { + /** 시작 날짜 (yyyy-MM-dd 형식) */ + startDate?: string; + /** 종료 날짜 (yyyy-MM-dd 형식) */ + endDate?: string; + /** 시작 날짜 변경 핸들러 */ + onStartDateChange?: (date: string) => void; + /** 종료 날짜 변경 핸들러 */ + onEndDateChange?: (date: string) => void; + /** 플레이스홀더 텍스트 */ + placeholder?: string; + /** 비활성화 여부 */ + disabled?: boolean; + /** 추가 className */ + className?: string; + /** 트리거 버튼 크기 */ + size?: "default" | "sm" | "lg"; + /** 날짜 표시 형식 (date-fns format) */ + displayFormat?: string; + /** 최소 선택 가능 날짜 */ + minDate?: Date; + /** 최대 선택 가능 날짜 */ + maxDate?: Date; + /** 팝오버 정렬 */ + align?: "start" | "center" | "end"; +} + +const MONTH_LABELS = [ + "1월", "2월", "3월", "4월", "5월", "6월", + "7월", "8월", "9월", "10월", "11월", "12월", +]; + +function DateRangePicker({ + startDate, + endDate, + onStartDateChange, + onEndDateChange, + placeholder = "기간 선택", + disabled = false, + className, + size = "default", + displayFormat = "yyyy년 MM월 dd일", + minDate, + maxDate, + align = "start", +}: DateRangePickerProps) { + const [open, setOpen] = React.useState(false); + const [displayMonth, setDisplayMonth] = React.useState(); + const [showMonthPicker, setShowMonthPicker] = React.useState(false); + const [pickerYear, setPickerYear] = React.useState(new Date().getFullYear()); + + // 내부 범위 상태 (팝오버 내 선택 중간 상태 관리) + const [internalRange, setInternalRange] = React.useState(); + // 선택 단계: 'idle' → 'selecting-end' (시작일 선택 후 종료일 대기) + const [selectPhase, setSelectPhase] = React.useState<'idle' | 'selecting-end'>('idle'); + + // 문자열 → Date 변환 (외부 prop → 버튼 텍스트용) + const parsedStart = React.useMemo(() => { + if (!startDate) return undefined; + const parsed = parse(startDate, "yyyy-MM-dd", new Date()); + return isValid(parsed) ? parsed : undefined; + }, [startDate]); + + const parsedEnd = React.useMemo(() => { + if (!endDate) return undefined; + const parsed = parse(endDate, "yyyy-MM-dd", new Date()); + return isValid(parsed) ? parsed : undefined; + }, [endDate]); + + // 팝오버 열릴 때 → 기존 범위 표시, idle 상태로 시작 + React.useEffect(() => { + if (open) { + const date = parsedStart ?? new Date(); + setDisplayMonth(date); + setPickerYear(date.getFullYear()); + setShowMonthPicker(false); + // 기존 범위를 내부 상태에 반영 (하이라이트 표시용) + setInternalRange(parsedStart ? { from: parsedStart, to: parsedEnd } : undefined); + setSelectPhase('idle'); + } + }, [open, parsedStart, parsedEnd]); + + // 날짜 클릭 핸들러 (react-day-picker onSelect 우회, 직접 2단계 관리) + const handleDayClick = React.useCallback( + (day: Date) => { + if (selectPhase === 'idle') { + // 첫 클릭 → 시작일만 설정, 종료일 대기 + setInternalRange({ from: day, to: undefined }); + setSelectPhase('selecting-end'); + } else { + // 두 번째 클릭 → 종료일 설정 + const from = internalRange?.from; + if (!from) return; + + let rangeFrom = from; + let rangeTo = day; + + // 종료일이 시작일보다 이전이면 스왑 + if (rangeTo < rangeFrom) { + [rangeFrom, rangeTo] = [rangeTo, rangeFrom]; + } + + // 외부 상태 반영 + onStartDateChange?.(format(rangeFrom, "yyyy-MM-dd")); + onEndDateChange?.(format(rangeTo, "yyyy-MM-dd")); + + // 내부 상태 업데이트 & 팝오버 닫기 + setInternalRange({ from: rangeFrom, to: rangeTo }); + setSelectPhase('idle'); + setOpen(false); + } + }, + [selectPhase, internalRange, onStartDateChange, onEndDateChange] + ); + + // react-day-picker onSelect (내부 하이라이트 업데이트용, 닫기 로직은 handleDayClick에서 처리) + const handleSelect = React.useCallback( + (_range: DateRange | undefined) => { + // handleDayClick에서 모든 로직 처리, 여기서는 무시 + }, + [] + ); + + // 연월 피커에서 월 선택 + const handleMonthSelect = React.useCallback( + (month: number) => { + setDisplayMonth(new Date(pickerYear, month, 1)); + setShowMonthPicker(false); + }, + [pickerYear] + ); + + // 연월 텍스트 클릭 → 연월 피커 열기 + const handleCaptionClick = React.useCallback(() => { + setPickerYear(displayMonth?.getFullYear() ?? new Date().getFullYear()); + setShowMonthPicker(true); + }, [displayMonth]); + + // 오늘로 이동 + const handleGoToToday = React.useCallback(() => { + const today = new Date(); + setDisplayMonth(today); + setShowMonthPicker(false); + }, []); + + // 표시 텍스트 + const displayText = React.useMemo(() => { + if (!parsedStart && !parsedEnd) return placeholder; + const startText = parsedStart + ? format(parsedStart, displayFormat, { locale: ko }) + : "..."; + const endText = parsedEnd + ? format(parsedEnd, displayFormat, { locale: ko }) + : "..."; + return `${startText} ~ ${endText}`; + }, [parsedStart, parsedEnd, displayFormat, placeholder]); + + // 버튼 크기 스타일 + const sizeClasses = { + default: "h-10 px-3", + sm: "h-8 px-2 text-sm", + lg: "h-12 px-4 text-base", + }; + + return ( + + + + + +
+ {showMonthPicker ? ( + /* 연월 선택 피커 */ +
+ {/* 연도 네비게이션 */} +
+ + {pickerYear}년 + +
+ {/* 월 그리드 (4행 3열) */} +
+ {MONTH_LABELS.map((label, i) => { + const isActive = + displayMonth?.getMonth() === i && + displayMonth?.getFullYear() === pickerYear; + return ( + + ); + })} +
+ {/* 오늘 버튼 */} + +
+ ) : ( + /* 달력 뷰 */ +
+ {/* 연월 텍스트 클릭 영역 (화살표 사이) */} + +
+ )} +
+
+
+ ); +} + +export { DateRangePicker }; +export type { DateRangePickerProps }; diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx deleted file mode 100644 index b0e9a387..00000000 --- a/src/contexts/ThemeContext.tsx +++ /dev/null @@ -1,57 +0,0 @@ -"use client"; - -import { createContext, useContext, useState, useEffect, ReactNode } from "react"; - -type Theme = "light" | "dark" | "senior"; - -interface ThemeContextType { - theme: Theme; - setTheme: (theme: Theme) => void; -} - -const ThemeContext = createContext(undefined); - -export function ThemeProvider({ children }: { children: ReactNode }) { - const [theme, setThemeState] = useState("light"); - - useEffect(() => { - // Load theme from localStorage on mount - const savedTheme = localStorage.getItem("theme") as Theme; - if (savedTheme) { - setThemeState(savedTheme); - applyTheme(savedTheme); - } - }, []); - - const setTheme = (newTheme: Theme) => { - if (typeof window === 'undefined') return; - setThemeState(newTheme); - localStorage.setItem("theme", newTheme); - applyTheme(newTheme); - }; - - const applyTheme = (theme: Theme) => { - if (typeof window === 'undefined') return; - const root = document.documentElement; - - // Remove all theme classes - root.classList.remove("light", "dark", "senior"); - - // Add new theme class - root.classList.add(theme); - }; - - return ( - - {children} - - ); -} - -export function useTheme() { - const context = useContext(ThemeContext); - if (context === undefined) { - throw new Error("useTheme must be used within a ThemeProvider"); - } - return context; -} \ No newline at end of file diff --git a/src/layouts/AuthenticatedLayout.tsx b/src/layouts/AuthenticatedLayout.tsx index 1e1ff007..e2034b3f 100644 --- a/src/layouts/AuthenticatedLayout.tsx +++ b/src/layouts/AuthenticatedLayout.tsx @@ -42,7 +42,7 @@ import { import Sidebar from '@/components/layout/Sidebar'; import HeaderFavoritesBar from '@/components/layout/HeaderFavoritesBar'; import CommandMenuSearch, { type CommandMenuSearchRef } from '@/components/layout/CommandMenuSearch'; -import { useTheme } from '@/contexts/ThemeContext'; +import { useThemeStore } from '@/stores/themeStore'; import { useAuth } from '@/contexts/AuthContext'; import { deserializeMenuItems } from '@/lib/utils/menuTransform'; import { stripLocalePrefix } from '@/lib/utils/locale'; @@ -96,7 +96,7 @@ interface AuthenticatedLayoutProps { export default function AuthenticatedLayout({ children }: AuthenticatedLayoutProps) { const { menuItems, activeMenu, setActiveMenu, setMenuItems, sidebarCollapsed, toggleSidebar, _hasHydrated } = useMenuStore(); - const { theme, setTheme } = useTheme(); + const { theme, setTheme } = useThemeStore(); const { logout } = useAuth(); const router = useRouter(); const pathname = usePathname(); // 현재 경로 추적 diff --git a/src/lib/auth/logout.ts b/src/lib/auth/logout.ts index a185548c..cffad69c 100644 --- a/src/lib/auth/logout.ts +++ b/src/lib/auth/logout.ts @@ -12,7 +12,7 @@ */ import { useMasterDataStore } from '@/stores/masterDataStore'; -import { useItemStore } from '@/stores/itemStore'; +import { useItemMasterStore } from '@/stores/item-master/useItemMasterStore'; // FCM은 Capacitor 환경에서만 사용 (동적 import로 웹 빌드 에러 방지) @@ -91,9 +91,9 @@ export function resetZustandStores(): void { const masterDataStore = useMasterDataStore.getState(); masterDataStore.reset(); - // itemStore 초기화 - const itemStore = useItemStore.getState(); - itemStore.reset(); + // itemMasterStore 초기화 + const itemMasterStore = useItemMasterStore.getState(); + itemMasterStore.reset(); } catch (error) { console.error('[Logout] Failed to reset Zustand stores:', error); } @@ -213,7 +213,7 @@ export function debugCacheStatus(): void { // Zustand const masterDataState = useMasterDataStore.getState(); - const itemState = useItemStore.getState(); + const itemMasterState = useItemMasterStore.getState(); console.groupEnd(); } \ No newline at end of file diff --git a/src/lib/print-utils.ts b/src/lib/print-utils.ts index e28cb7f2..d6ae1a32 100644 --- a/src/lib/print-utils.ts +++ b/src/lib/print-utils.ts @@ -4,6 +4,7 @@ */ import { sanitizeHTMLForPrint } from '@/lib/sanitize'; +import { toast } from 'sonner'; interface PrintOptions { /** 문서 제목 (브라우저 인쇄 다이얼로그에 표시) */ @@ -44,7 +45,7 @@ export function printElement( const printWindow = window.open('', '_blank', 'width=800,height=600'); if (!printWindow) { console.error('팝업 창을 열 수 없습니다. 팝업 차단을 확인해주세요.'); - alert('인쇄 창을 열 수 없습니다. 팝업 차단을 해제해주세요.'); + toast.error('인쇄 창을 열 수 없습니다. 팝업 차단을 해제해주세요.'); return; } diff --git a/src/utils/formatAmount.ts b/src/lib/utils/amount.ts similarity index 100% rename from src/utils/formatAmount.ts rename to src/lib/utils/amount.ts diff --git a/src/utils/date.ts b/src/lib/utils/date.ts similarity index 100% rename from src/utils/date.ts rename to src/lib/utils/date.ts diff --git a/src/stores/itemStore.ts b/src/stores/itemStore.ts deleted file mode 100644 index 08d0bf4b..00000000 --- a/src/stores/itemStore.ts +++ /dev/null @@ -1,323 +0,0 @@ -/** - * 품목 관리 Zustand Store - * - * React Context 대신 Zustand를 사용한 클라이언트 상태 관리 - * DataContext.tsx에서 마이그레이션 - */ - -import { create } from 'zustand'; -import { devtools } from 'zustand/middleware'; -import type { ItemMaster, ItemType, BOMLine } from '@/types/item'; - -// ===== Store 타입 정의 ===== - -interface ItemStore { - // === State === - items: ItemMaster[]; // 품목 목록 (캐시) - selectedItem: ItemMaster | null; // 현재 선택된 품목 - isLoading: boolean; // 로딩 상태 - error: string | null; // 에러 메시지 - - // 필터 상태 - filters: { - itemType?: ItemType; - search?: string; - category1?: string; - category2?: string; - category3?: string; - isActive?: boolean; - }; - - // === Actions === - - // 품목 목록 관리 - setItems: (items: ItemMaster[]) => void; - addItem: (item: ItemMaster) => void; - updateItem: (itemCode: string, updates: Partial) => void; - deleteItem: (itemCode: string) => void; - - // 선택 관리 - selectItem: (item: ItemMaster | null) => void; - - // 로딩/에러 상태 - setLoading: (isLoading: boolean) => void; - setError: (error: string | null) => void; - - // 필터 관리 - setFilters: (filters: Partial) => void; - clearFilters: () => void; - - // === Helpers === - - // 품목 검색 - getItemByCode: (itemCode: string) => ItemMaster | undefined; - getItemsByType: (itemType: ItemType) => ItemMaster[]; - getFilteredItems: () => ItemMaster[]; - - // BOM 관리 - updateBOM: (itemCode: string, bom: BOMLine[]) => void; - - // 초기화 - reset: () => void; -} - -// ===== 초기 상태 ===== - -const initialState = { - items: [], - selectedItem: null, - isLoading: false, - error: null, - filters: {}, -}; - -// ===== Store 생성 ===== - -export const useItemStore = create()( - devtools( - // persist( // 필요시 localStorage 영구 저장 활성화 - (set, get) => ({ - // Initial state - ...initialState, - - // ===== 품목 목록 관리 ===== - - setItems: (items) => - set( - { items }, - false, - 'setItems' - ), - - addItem: (item) => - set( - (state) => ({ - items: [...state.items, item], - }), - false, - 'addItem' - ), - - updateItem: (itemCode, updates) => - set( - (state) => ({ - items: state.items.map((item) => - item.itemCode === itemCode ? { ...item, ...updates } : item - ), - // 선택된 품목도 업데이트 - selectedItem: - state.selectedItem?.itemCode === itemCode - ? { ...state.selectedItem, ...updates } - : state.selectedItem, - }), - false, - 'updateItem' - ), - - deleteItem: (itemCode) => - set( - (state) => ({ - items: state.items.filter((item) => item.itemCode !== itemCode), - // 선택된 품목이 삭제되면 선택 해제 - selectedItem: - state.selectedItem?.itemCode === itemCode ? null : state.selectedItem, - }), - false, - 'deleteItem' - ), - - // ===== 선택 관리 ===== - - selectItem: (item) => - set( - { selectedItem: item }, - false, - 'selectItem' - ), - - // ===== 로딩/에러 상태 ===== - - setLoading: (isLoading) => - set( - { isLoading }, - false, - 'setLoading' - ), - - setError: (error) => - set( - { error }, - false, - 'setError' - ), - - // ===== 필터 관리 ===== - - setFilters: (filters) => - set( - (state) => ({ - filters: { ...state.filters, ...filters }, - }), - false, - 'setFilters' - ), - - clearFilters: () => - set( - { filters: {} }, - false, - 'clearFilters' - ), - - // ===== Helpers ===== - - getItemByCode: (itemCode) => { - return get().items.find((item) => item.itemCode === itemCode); - }, - - getItemsByType: (itemType) => { - return get().items.filter((item) => item.itemType === itemType); - }, - - getFilteredItems: () => { - const { items, filters } = get(); - - return items.filter((item) => { - // 품목 유형 필터 - if (filters.itemType && item.itemType !== filters.itemType) { - return false; - } - - // 검색어 필터 (품목코드 또는 품목명) - if (filters.search) { - const searchLower = filters.search.toLowerCase(); - const matchesCode = item.itemCode.toLowerCase().includes(searchLower); - const matchesName = item.itemName.toLowerCase().includes(searchLower); - if (!matchesCode && !matchesName) { - return false; - } - } - - // 대분류 필터 - if (filters.category1 && item.category1 !== filters.category1) { - return false; - } - - // 중분류 필터 - if (filters.category2 && item.category2 !== filters.category2) { - return false; - } - - // 소분류 필터 - if (filters.category3 && item.category3 !== filters.category3) { - return false; - } - - // 활성/비활성 필터 - if (filters.isActive !== undefined && item.isActive !== filters.isActive) { - return false; - } - - return true; - }); - }, - - // BOM 업데이트 - updateBOM: (itemCode, bom) => - set( - (state) => ({ - items: state.items.map((item) => - item.itemCode === itemCode ? { ...item, bom } : item - ), - selectedItem: - state.selectedItem?.itemCode === itemCode - ? { ...state.selectedItem, bom } - : state.selectedItem, - }), - false, - 'updateBOM' - ), - - // 초기화 - reset: () => - set( - initialState, - false, - 'reset' - ), - }), - // { - // name: 'item-store', // localStorage 키 - // partialize: (state) => ({ - // // 필요한 필드만 영구 저장 - // filters: state.filters, - // }), - // } - // ), - { - name: 'ItemStore', // Redux DevTools에 표시될 이름 - } - ) -); - -// ===== Selector Hooks (성능 최적화) ===== - -/** - * 필터링된 품목 목록만 구독 - */ -export const useFilteredItems = () => - useItemStore((state) => state.getFilteredItems()); - -/** - * 특정 품목 유형만 구독 - */ -export const useItemsByType = (itemType: ItemType) => - useItemStore((state) => state.getItemsByType(itemType)); - -/** - * 선택된 품목만 구독 - */ -export const useSelectedItem = () => - useItemStore((state) => state.selectedItem); - -/** - * 로딩 상태만 구독 - */ -export const useItemLoading = () => - useItemStore((state) => state.isLoading); - -/** - * 에러 상태만 구독 - */ -export const useItemError = () => - useItemStore((state) => state.error); - -/** - * 필터 상태만 구독 - */ -export const useItemFilters = () => - useItemStore((state) => state.filters); - -// ===== 액션 훅 (성능 최적화) ===== - -/** - * 품목 관련 액션만 가져오기 (리렌더링 방지) - */ -export const useItemActions = () => - useItemStore((state) => ({ - setItems: state.setItems, - addItem: state.addItem, - updateItem: state.updateItem, - deleteItem: state.deleteItem, - selectItem: state.selectItem, - setLoading: state.setLoading, - setError: state.setError, - setFilters: state.setFilters, - clearFilters: state.clearFilters, - updateBOM: state.updateBOM, - reset: state.reset, - })); - -// ===== 타입 추출 (외부에서 사용) ===== - -export type { ItemStore }; \ No newline at end of file diff --git a/src/stores/themeStore.ts b/src/stores/themeStore.ts index 0ad63264..96a1f885 100644 --- a/src/stores/themeStore.ts +++ b/src/stores/themeStore.ts @@ -15,8 +15,10 @@ export const useThemeStore = create()( theme: 'light', setTheme: (theme: Theme) => { - // HTML 클래스 업데이트 - document.documentElement.className = theme === 'light' ? '' : theme; + // HTML 클래스 업데이트 (다른 클래스 보존 - 폰트 등) + const root = document.documentElement; + root.classList.remove('light', 'dark', 'senior'); + root.classList.add(theme); set({ theme }); }, @@ -32,7 +34,9 @@ export const useThemeStore = create()( // Zustand persist 재수화 시 HTML 클래스 복원 onRehydrateStorage: () => (state) => { if (state?.theme) { - document.documentElement.className = state.theme === 'light' ? '' : state.theme; + const root = document.documentElement; + root.classList.remove('light', 'dark', 'senior'); + root.classList.add(state.theme); } }, }