From 2639724f9f9490433a378531dcc1d5b140584309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Thu, 5 Feb 2026 15:57:49 +0900 Subject: [PATCH] =?UTF-8?q?feat(WEB):=20=EC=83=81=ED=83=9C=20=EB=B1=83?= =?UTF-8?q?=EC=A7=80=20=EA=B3=B5=ED=86=B5=ED=99=94=20=EB=B0=8F=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=9B=85=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 공통화: - status-config.ts 신규 추가 (상태 설정 중앙 관리) - StatusBadge 컴포넌트 개선 - 뱃지 공통화 가이드 문서 추가 상세 페이지 훅 시스템: - useDetailData, useDetailPageState, useDetailPermissions, useCRUDHandlers 훅 신규 추가 - hooks/index.ts 진입점 추가 - BillDetailV2 신규 컴포넌트 추가 리팩토링: - 회계(매입/어음/거래처), 품목, 단가, 견적, 주문 상태 뱃지 공통화 적용 - 생산(작업지시서/대시보드/작업자화면), 품질(검사관리) 상태 뱃지 적용 - 공사관리(칸반/프로젝트카드) 상태 뱃지 적용 - 고객센터(문의관리), 인사(직원CSV업로드) 개선 - 리스트 페이지 공통화 현황 분석 문서 추가 상수 정리: - lib/constants/ 디렉토리 추가 Co-Authored-By: Claude Opus 4.5 --- ...6-02-05] list-page-commonization-status.md | 505 ++++++++++++++++ ...2026-02-05] detail-hooks-migration-plan.md | 304 ++++++++++ .../guides/badge-commonization-guide.md | 276 +++++++++ .../accounting/bills/[id]/page.tsx | 3 +- .../(protected)/accounting/bills/new/page.tsx | 3 +- .../(protected)/accounting/bills/page.tsx | 3 +- .../accounting/BillManagement/BillDetail.tsx | 16 +- .../BillManagement/BillDetailV2.tsx | 544 ++++++++++++++++++ .../PurchaseManagement/PurchaseDetail.tsx | 3 +- .../PurchaseDetailModal.tsx | 3 +- .../accounting/PurchaseManagement/index.tsx | 3 +- .../CreditAnalysisDocument.tsx | 3 +- .../construction/management/DetailCard.tsx | 3 +- .../construction/management/KanbanColumn.tsx | 3 +- .../construction/management/ProjectCard.tsx | 3 +- .../construction/management/StageCard.tsx | 3 +- .../InquiryManagement/InquiryList.tsx | 13 +- .../hr/EmployeeManagement/CSVUploadDialog.tsx | 5 +- .../sections/DynamicBOMSection.tsx | 3 +- src/components/items/ItemDetailClient.tsx | 14 +- src/components/items/ItemForm/BOMSection.tsx | 3 +- src/components/items/ItemListClient.tsx | 14 +- .../tabs/MasterFieldTab/index.tsx | 3 +- src/components/molecules/StatusBadge.tsx | 1 + src/components/orders/OrderRegistration.tsx | 5 +- .../PriceDistributionDetail.tsx | 3 +- src/components/pricing/PricingFormClient.tsx | 7 +- .../pricing/PricingHistoryDialog.tsx | 3 +- .../production/ProductionDashboard/index.tsx | 5 +- .../production/WorkOrders/WorkOrderList.tsx | 38 +- .../WorkerScreen/ProcessDetailSection.tsx | 3 +- .../production/WorkerScreen/WorkCard.tsx | 5 +- .../InspectionManagement/InspectionCreate.tsx | 5 +- .../InspectionManagement/InspectionDetail.tsx | 9 +- .../InspectionManagement/InspectionList.tsx | 5 +- src/components/quotes/QuoteRegistration.tsx | 5 +- src/hooks/index.ts | 53 ++ src/hooks/useCRUDHandlers.ts | 292 ++++++++++ src/hooks/useDetailData.ts | 129 +++++ src/hooks/useDetailPageState.ts | 183 ++++++ src/hooks/useDetailPermissions.ts | 114 ++++ src/lib/constants/filter-presets.ts | 286 +++++++++ src/lib/utils/status-config.ts | 163 ++++++ 43 files changed, 2944 insertions(+), 103 deletions(-) create mode 100644 claudedocs/[ANALYSIS-2026-02-05] list-page-commonization-status.md create mode 100644 claudedocs/[IMPL-2026-02-05] detail-hooks-migration-plan.md create mode 100644 claudedocs/guides/badge-commonization-guide.md create mode 100644 src/components/accounting/BillManagement/BillDetailV2.tsx create mode 100644 src/hooks/index.ts create mode 100644 src/hooks/useCRUDHandlers.ts create mode 100644 src/hooks/useDetailData.ts create mode 100644 src/hooks/useDetailPageState.ts create mode 100644 src/hooks/useDetailPermissions.ts create mode 100644 src/lib/constants/filter-presets.ts diff --git a/claudedocs/[ANALYSIS-2026-02-05] list-page-commonization-status.md b/claudedocs/[ANALYSIS-2026-02-05] list-page-commonization-status.md new file mode 100644 index 00000000..a94830a4 --- /dev/null +++ b/claudedocs/[ANALYSIS-2026-02-05] list-page-commonization-status.md @@ -0,0 +1,505 @@ +# 리스트 페이지 공통화 현황 분석 + +> 작성일: 2026-02-05 +> 목적: 리스트 페이지 반복 패턴 식별 및 공통화 가능성 분석 + +--- + +## 📊 전체 현황 + +| 구분 | 수량 | +|------|------| +| 총 리스트 페이지 | 37개 | +| UniversalListPage 사용 | 15개+ | +| IntegratedListTemplateV2 직접 사용 | 5개+ | +| 레거시 패턴 | 10개+ | + +--- + +## 🏗️ 템플릿 계층 구조 + +``` +UniversalListPage (최상위 - config 기반) + └── IntegratedListTemplateV2 (하위 - props 기반) + └── 공통 UI 컴포넌트 + ├── PageLayout + ├── PageHeader + ├── StatCards + ├── DateRangeSelector + ├── MobileFilter + ├── ListMobileCard + └── Table, Pagination 등 +``` + +--- + +## 📁 리스트 페이지 목록 및 사용 템플릿 + +### UniversalListPage 사용 (최신 패턴) + +| 파일 | 도메인 | 특징 | +|------|--------|------| +| `items/ItemListClient.tsx` | 품목관리 | 외부 훅(useItemList) 사용, 엑셀 업로드/다운로드 | +| `pricing/PricingListClient.tsx` | 가격관리 | 외부 훅 사용 | +| `production/WorkOrders/WorkOrderList.tsx` | 생산 | 공정 기반 탭, 외부 통계 API | +| `outbound/ShipmentManagement/ShipmentList.tsx` | 출고 | 캘린더 통합, 날짜범위 필터 | +| `outbound/VehicleDispatchManagement/VehicleDispatchList.tsx` | 배차 | - | +| `material/ReceivingManagement/ReceivingList.tsx` | 입고 | - | +| `material/StockStatus/StockStatusList.tsx` | 재고 | - | +| `customer-center/NoticeManagement/NoticeList.tsx` | 공지사항 | 클라이언트 사이드 필터링 | +| `customer-center/EventManagement/EventList.tsx` | 이벤트 | 클라이언트 사이드 필터링 | +| `customer-center/InquiryManagement/InquiryList.tsx` | 문의 | - | +| `customer-center/FAQManagement/FAQList.tsx` | FAQ | - | +| `quality/InspectionManagement/InspectionList.tsx` | 품질검사 | - | +| `process-management/ProcessListClient.tsx` | 공정관리 | - | +| `pricing-table-management/PricingTableListClient.tsx` | 단가표 | - | +| `pricing-distribution/PriceDistributionList.tsx` | 가격배포 | - | + +### 건설 도메인 (UniversalListPage 사용) + +| 파일 | 기능 | +|------|------| +| `construction/management/ProjectListClient.tsx` | 프로젝트 목록 | +| `construction/management/ConstructionManagementListClient.tsx` | 공사관리 목록 | +| `construction/contract/ContractListClient.tsx` | 계약 목록 | +| `construction/estimates/EstimateListClient.tsx` | 견적 목록 | +| `construction/bidding/BiddingListClient.tsx` | 입찰 목록 | +| `construction/pricing-management/PricingListClient.tsx` | 단가관리 목록 | +| `construction/partners/PartnerListClient.tsx` | 협력사 목록 | +| `construction/order-management/OrderManagementListClient.tsx` | 발주관리 목록 | +| `construction/site-management/SiteManagementListClient.tsx` | 현장관리 목록 | +| `construction/site-briefings/SiteBriefingListClient.tsx` | 현장브리핑 목록 | +| `construction/handover-report/HandoverReportListClient.tsx` | 인수인계 목록 | +| `construction/issue-management/IssueManagementListClient.tsx` | 이슈관리 목록 | +| `construction/structure-review/StructureReviewListClient.tsx` | 구조검토 목록 | +| `construction/utility-management/UtilityManagementListClient.tsx` | 유틸리티 목록 | +| `construction/worker-status/WorkerStatusListClient.tsx` | 작업자 현황 | +| `construction/progress-billing/ProgressBillingManagementListClient.tsx` | 기성관리 목록 | + +### 기타/레거시 + +| 파일 | 비고 | +|------|------| +| `settings/PopupManagement/PopupList.tsx` | 팝업관리 | +| `production/WorkResults/WorkResultList.tsx` | 작업실적 | +| `quality/PerformanceReportManagement/PerformanceReportList.tsx` | 성과보고서 | +| `board/BoardList/BoardListUnified.tsx` | 통합 게시판 | + +--- + +## 🔄 반복 패턴 분석 + +### 1. Badge 색상 매핑 (매우 반복적) + +각 페이지마다 개별 정의되어 있는 패턴: + +```typescript +// ItemListClient.tsx +const badges: Record = { + FG: { variant: 'default', className: 'bg-purple-100 text-purple-700 border-purple-200' }, + PT: { variant: 'default', className: 'bg-orange-100 text-orange-700 border-orange-200' }, + // ... +}; + +// WorkOrderList.tsx +const PRIORITY_COLORS: Record = { + '긴급': 'bg-red-100 text-red-700', + '우선': 'bg-orange-100 text-orange-700', + '일반': 'bg-gray-100 text-gray-700', +}; + +// ShipmentList.tsx - types.ts에서 import +export const SHIPMENT_STATUS_STYLES: Record = { ... }; +``` + +**현황**: +- `src/lib/utils/status-config.ts`에 `createStatusConfig` 유틸 존재 +- 일부 페이지만 사용 중 (대부분 개별 정의) + +### 2. 상태 라벨 정의 (반복적) + +```typescript +// WorkOrderList.tsx - types.ts에서 import +export const WORK_ORDER_STATUS_LABELS: Record = { + pending: '대기', + in_progress: '진행중', + completed: '완료', +}; + +// ShipmentList.tsx - types.ts에서 import +export const SHIPMENT_STATUS_LABELS: Record = { ... }; +``` + +**현황**: 각 도메인 types.ts에서 개별 정의 + +### 3. 필터 설정 (filterConfig) + +```typescript +// WorkOrderList.tsx +const filterConfig: FilterFieldConfig[] = [ + { + key: 'status', + label: '상태', + type: 'single', + options: [ + { value: 'waiting', label: '작업대기' }, + { value: 'in_progress', label: '진행중' }, + { value: 'completed', label: '작업완료' }, + ], + }, + { + key: 'priority', + label: '우선순위', + type: 'single', + options: [ + { value: 'urgent', label: '긴급' }, + { value: 'priority', label: '우선' }, + { value: 'normal', label: '일반' }, + ], + }, +]; +``` + +**공통 필터 패턴**: +- 상태 필터 (대기/진행/완료) +- 우선순위 필터 (긴급/우선/일반) +- 유형 필터 (전체/유형1/유형2...) + +### 4. 행 클릭 핸들러 패턴 + +```typescript +// 모든 페이지에서 동일한 패턴 +const handleRowClick = useCallback( + (item: SomeType) => { + router.push(`/ko/${basePath}/${item.id}?mode=view`); + }, + [router] +); +``` + +### 5. 테이블 행 렌더링 (renderTableRow) + +```typescript +// 공통 구조 + handleRowClick(item)}> + e.stopPropagation()}> + + + {globalIndex} + {/* 데이터 컬럼들 */} + + + {getStatusLabel(item.status)} + + + +``` + +--- + +## ✅ 이미 공통화된 것 + +| 유틸/컴포넌트 | 위치 | 사용률 | +|--------------|------|--------| +| `UniversalListPage` | templates/ | 높음 (15개+) | +| `IntegratedListTemplateV2` | templates/ | 높음 | +| `ListMobileCard`, `InfoField` | organisms/ | 높음 | +| `MobileFilter` | molecules/ | 높음 | +| `DateRangeSelector` | molecules/ | 높음 | +| `StatCards` | organisms/ | 높음 | +| `createStatusConfig` | lib/utils/ | **낮음** (일부만 사용) | + +--- + +## ❌ 공통화 필요한 것 + +### 높은 우선순위 (ROI 높음) + +| 패턴 | 현황 | 공통화 방안 | +|------|------|-------------| +| **Badge 색상 매핑** | 각 페이지 개별 정의 | `src/lib/utils/badge-styles.ts` 생성 | +| **공통 필터 프리셋** | 각 페이지 개별 정의 | `src/lib/constants/filter-presets.ts` 생성 | +| **우선순위 색상** | 각 페이지 개별 정의 | 공통 상수로 추출 | + +### 중간 우선순위 + +| 패턴 | 현황 | 공통화 방안 | +|------|------|-------------| +| 상태 라벨 | 도메인별 types.ts | 도메인별 유지 (비즈니스 로직) | +| 행 클릭 핸들러 | 각 페이지 개별 | UniversalListPage에서 처리 중 | + +--- + +## 📋 공통화 대상 상세 + +### 1. Badge 스타일 공통화 + +**현재 분산된 위치**: +- `items/ItemListClient.tsx` - getItemTypeBadge() +- `production/WorkOrders/types.ts` - WORK_ORDER_STATUS_COLORS +- `outbound/ShipmentManagement/types.ts` - SHIPMENT_STATUS_STYLES +- 기타 각 도메인별 개별 정의 + +**이미 존재하는 공통 유틸** (`src/lib/utils/status-config.ts`): +```typescript +export const BADGE_STYLE_PRESETS: Record = { + default: 'bg-gray-100 text-gray-800', + success: 'bg-green-100 text-green-800', + warning: 'bg-yellow-100 text-yellow-800', + destructive: 'bg-red-100 text-red-800', + info: 'bg-blue-100 text-blue-800', + muted: 'bg-gray-100 text-gray-500', + orange: 'bg-orange-100 text-orange-800', + purple: 'bg-purple-100 text-purple-800', +}; +``` + +**문제**: 존재하지만 대부분의 페이지에서 사용하지 않음 + +### 2. 공통 필터 프리셋 + +**추출 가능한 공통 필터**: +```typescript +// 상태 필터 (거의 모든 페이지) +export const COMMON_STATUS_FILTER: FilterFieldConfig = { + key: 'status', + label: '상태', + type: 'single', + options: [ + { value: 'pending', label: '대기' }, + { value: 'in_progress', label: '진행중' }, + { value: 'completed', label: '완료' }, + ], +}; + +// 우선순위 필터 (생산, 출고 등) +export const COMMON_PRIORITY_FILTER: FilterFieldConfig = { + key: 'priority', + label: '우선순위', + type: 'single', + options: [ + { value: 'urgent', label: '긴급' }, + { value: 'priority', label: '우선' }, + { value: 'normal', label: '일반' }, + ], +}; +``` + +### 3. 우선순위 색상 통합 + +**현재 상태**: 여러 파일에서 동일한 색상 반복 +```typescript +// 긴급: bg-red-100 text-red-700 +// 우선: bg-orange-100 text-orange-700 +// 일반: bg-gray-100 text-gray-700 +``` + +--- + +## 🎯 권장 액션 + +### Phase 1: 즉시 실행 가능 + +1. **`createStatusConfig` 사용률 높이기** + - 기존 유틸 활용도 확인 + - 새 페이지 작성 시 필수 사용 권장 + +2. **공통 필터 프리셋 파일 생성** + - 위치: `src/lib/constants/filter-presets.ts` + - 상태/우선순위/유형 필터 템플릿 + +3. **우선순위 색상 상수 통합** + - 위치: `src/lib/utils/status-config.ts`에 추가 + +### Phase 2: 점진적 적용 + +1. 신규 페이지는 공통 유틸 필수 사용 +2. 기존 페이지는 수정 시 점진적 마이그레이션 +3. 기능 변경 없이 import만 변경 + +--- + +## 📊 공통화 효과 예측 + +| 항목 | Before | After | +|------|--------|-------| +| Badge 정의 위치 | 37개 파일에 분산 | 1개 파일 (+ import) | +| 필터 프리셋 | 각 페이지 개별 | 공통 상수 재사용 | +| 색상 변경 시 수정 범위 | 37개 파일 | 1개 파일 | +| 신규 페이지 개발 시간 | 기존 페이지 참고 필요 | 공통 유틸 import만 | + +--- + +## 📝 결론 + +1. **UniversalListPage는 이미 잘 구축됨** - 대부분 리스트가 사용 중 +2. **Badge/필터 공통화가 주요 개선점** - 반복 코드 제거 가능 +3. **기존 유틸(`createStatusConfig`) 활용도 낮음** - 홍보/가이드 필요 +4. **기능 변경 없이 공통화 가능** - 리팩토링 리스크 낮음 + +--- + +## ✅ 공통화 작업 완료 현황 (2026-02-05) + +### 생성된 파일 + +| 파일 | 설명 | +|------|------| +| `src/lib/constants/filter-presets.ts` | 공통 필터 프리셋 (상태/우선순위/품목유형 등) | +| `claudedocs/guides/badge-commonization-guide.md` | Badge 공통화 사용 가이드 | + +### 수정된 파일 + +| 파일 | 변경 내용 | +|------|----------| +| `src/lib/utils/status-config.ts` | 우선순위/품목유형 설정 추가, 한글 라벨 지원 | +| `src/components/production/WorkOrders/WorkOrderList.tsx` | 공통 유틸 적용 (샘플 마이그레이션) | + +### 추가된 공통 유틸 + +**filter-presets.ts**: +- `COMMON_STATUS_FILTER` - 대기/진행/완료 +- `WORK_STATUS_FILTER` - 작업대기/진행중/작업완료 +- `COMMON_PRIORITY_FILTER` - 긴급/우선/일반 +- `ITEM_TYPE_FILTER` - 품목유형 +- `createSingleFilter()`, `createMultiFilter()` - 커스텀 필터 생성 + +**status-config.ts**: +- `getPriorityLabel()`, `getPriorityStyle()` - 우선순위 (한글/영문 모두 지원) +- `getItemTypeLabel()`, `getItemTypeStyle()` - 품목유형 +- `COMMON_STATUS_CONFIG`, `WORK_STATUS_CONFIG` 등 - 미리 정의된 상태 설정 + +### 샘플 마이그레이션 결과 (WorkOrderList.tsx) + +**Before**: +```tsx +// 개별 정의 +const PRIORITY_COLORS: Record = { + '긴급': 'bg-red-100 text-red-700', + '우선': 'bg-orange-100 text-orange-700', + '일반': 'bg-gray-100 text-gray-700', +}; + +const filterConfig: FilterFieldConfig[] = [ + { key: 'status', label: '상태', type: 'single', options: [...] }, + { key: 'priority', label: '우선순위', type: 'single', options: [...] }, +]; + + +``` + +**After**: +```tsx +// 공통 유틸 사용 +import { WORK_STATUS_FILTER, COMMON_PRIORITY_FILTER } from '@/lib/constants/filter-presets'; +import { getPriorityStyle } from '@/lib/utils/status-config'; + +const filterConfig = [WORK_STATUS_FILTER, COMMON_PRIORITY_FILTER]; + + +``` + +**효과**: +- 코드 라인 20줄 → 3줄 +- 필터 옵션 중복 정의 제거 +- 색상 일관성 보장 + +--- + +## 🔄 추가 마이그레이션 (2026-02-05 업데이트) + +### 완료된 마이그레이션 + +| 파일 | 적용 내용 | 효과 | +|------|----------|------| +| `WorkOrderList.tsx` | WORK_STATUS_FILTER + COMMON_PRIORITY_FILTER + getPriorityStyle | 20줄 → 3줄 | +| `ItemListClient.tsx` | getItemTypeStyle (품목유형 Badge) | 17줄 → 4줄 | +| `ItemDetailClient.tsx` | getItemTypeStyle (품목유형 Badge) | 17줄 → 4줄 | + +### 마이그레이션 제외 대상 (도메인 특화 설정) + +| 파일 | 제외 사유 | +|------|----------| +| `PricingListClient.tsx` | 다른 색상 체계 (SM=cyan, BENDING 추가 타입) | +| `StockStatus/types.ts` | 레거시 타입 지원 (raw_material, bent_part 등) | +| `ShipmentManagement/types.ts` | 다른 우선순위 라벨 (보통/낮음) | +| `issue-management/types.ts` | 2단계 우선순위 (긴급/일반만) | +| `WipProductionModal.tsx` | 버튼 스타일 우선순위 (Badge 아님) | +| `ReceivingList.tsx` | 도메인 특화 상태 (입고대기/입고완료/검사완료) | +| HR 페이지들 | 도메인 특화 상태 설정 | +| 건설 도메인 페이지들 | 도메인 특화 상태 설정 | + +### 분석 결과 요약 + +1. **공통 유틸 적용 완료 페이지**: 3개 (WorkOrderList, ItemListClient, ItemDetailClient) +2. **도메인 특화 설정 페이지**: 34개 (개별 유지가 적절) +3. **결론**: 대부분의 페이지는 도메인별 특화된 상태/라벨/색상을 사용하며, 이는 비즈니스 로직을 명확히 반영하기 위해 의도된 설계 + +### 공통 유틸 권장 사용 시나리오 + +1. **신규 리스트 페이지 생성 시**: 표준 패턴(대기/진행/완료, 긴급/우선/일반) 사용 +2. **품목유형 Badge**: 일관된 색상 적용 필요 시 `getItemTypeStyle` 사용 +3. **우선순위 Badge**: 표준 3단계(긴급/우선/일반) 사용 시 `getPriorityStyle` 사용 + +--- + +## 🎨 getPresetStyle 마이그레이션 완료 (2026-02-05 최종) + +### 마이그레이션 완료 파일 (22개) + +| 파일 | 적용 내용 | +|------|----------| +| `orders/OrderRegistration.tsx` | success, info preset | +| `pricing-distribution/PriceDistributionDetail.tsx` | success preset | +| `pricing/PricingFormClient.tsx` | purple, info, success preset | +| `quality/InspectionManagement/InspectionList.tsx` | success, destructive preset | +| `quality/InspectionManagement/InspectionCreate.tsx` | success, destructive preset | +| `quality/InspectionManagement/InspectionDetail.tsx` | success, destructive preset | +| `accounting/PurchaseManagement/index.tsx` | info preset | +| `accounting/PurchaseManagement/PurchaseDetail.tsx` | orange preset (기존) | +| `accounting/PurchaseManagement/PurchaseDetailModal.tsx` | orange preset (기존) | +| `accounting/VendorManagement/CreditAnalysisModal/CreditAnalysisDocument.tsx` | info preset | +| `quotes/QuoteRegistration.tsx` | success preset | +| `pricing/PricingHistoryDialog.tsx` | info preset | +| `business/construction/management/KanbanColumn.tsx` | info preset | +| `business/construction/management/DetailCard.tsx` | warning preset | +| `business/construction/management/StageCard.tsx` | warning preset | +| `business/construction/management/ProjectCard.tsx` | info preset | +| `production/WorkerScreen/WorkCard.tsx` | success, destructive preset | +| `production/WorkerScreen/ProcessDetailSection.tsx` | warning preset | +| `production/ProductionDashboard/index.tsx` | orange, success preset (기존) | +| `items/ItemForm/BOMSection.tsx` | info preset (기존) | +| `items/DynamicItemForm/sections/DynamicBOMSection.tsx` | info preset (기존) | +| `items/ItemMasterDataManagement/tabs/MasterFieldTab/index.tsx` | info preset | +| `customer-center/InquiryManagement/InquiryList.tsx` | warning, success preset (기존) | +| `hr/EmployeeManagement/CSVUploadDialog.tsx` | success, destructive preset (기존) | + +### 마이그레이션 제외 파일 (유지) + +| 파일 | 제외 사유 | +|------|----------| +| `business/MainDashboard.tsx` | CEO 대시보드 - 다양한 데이터 시각화용 고유 색상 (achievement %, overdue days 등) | +| `pricing/PricingListClient.tsx` | 도메인 특화 색상 체계 (SM=cyan, BENDING type 등) | +| `business/CEODashboard/sections/TodayIssueSection.tsx` | 알림 유형별 고유 색상+아이콘 (notification_type 기반) | +| `dev/DevToolbar.tsx` | 개발 도구 (운영 무관) | +| `ui/status-badge.tsx` | 이미 status-config.ts 사용 중 | +| `items/ItemDetailClient.tsx` | getItemTypeStyle 사용 (도메인 특화) | +| `items/ItemListClient.tsx` | getItemTypeStyle 사용 (도메인 특화) | + +### 사용된 Preset 유형 통계 + +| Preset | 사용 횟수 | 용도 | +|--------|----------|------| +| `success` | 15+ | 완료, 일치, 활성, 긍정적 상태 | +| `info` | 10+ | 정보성 라벨, 진행 상태, 문서 타입 | +| `warning` | 6+ | 진행중, 주의 필요, 선행 생산 | +| `destructive` | 5+ | 오류, 불일치, 긴급 | +| `orange` | 3+ | 품의서/지출결의서, 지연 | +| `purple` | 2+ | 최종 확정, 특수 상태 | + +### 마이그레이션 효과 + +1. **코드 일관성**: 22개 파일에서 동일한 유틸리티 함수 사용 +2. **유지보수성**: 색상 변경 시 `status-config.ts` 한 곳만 수정 +3. **가독성 향상**: `getPresetStyle('success')` vs `bg-green-100 text-green-700 border-green-200` +4. **타입 안전성**: TypeScript로 프리셋 이름 자동완성 \ No newline at end of file diff --git a/claudedocs/[IMPL-2026-02-05] detail-hooks-migration-plan.md b/claudedocs/[IMPL-2026-02-05] detail-hooks-migration-plan.md new file mode 100644 index 00000000..79baa12b --- /dev/null +++ b/claudedocs/[IMPL-2026-02-05] detail-hooks-migration-plan.md @@ -0,0 +1,304 @@ +# 상세 페이지 훅 마이그레이션 계획서 + +> 작성일: 2026-02-05 +> 상태: 계획 수립 + +--- + +## 1. 개요 + +### 1.1 목적 +- 상세/등록/수정 페이지의 반복 코드를 공통 훅으로 통합 +- 코드 일관성 확보 및 유지보수성 향상 +- 서비스 런칭 전 기술 부채 최소화 + +### 1.2 생성된 공통 훅 +| 훅 | 위치 | 역할 | +|----|------|------| +| `useDetailPageState` | `src/hooks/useDetailPageState.ts` | 페이지 상태 관리 (mode, id, navigation) | +| `useDetailData` | `src/hooks/useDetailData.ts` | 데이터 로딩 + 로딩/에러 상태 | +| `useCRUDHandlers` | `src/hooks/useCRUDHandlers.ts` | 등록/수정/삭제 + toast/redirect | +| `useDetailPermissions` | `src/hooks/useDetailPermissions.ts` | 권한 체크 | + +### 1.3 테스트 완료 +- [x] `BillDetail.tsx` → `BillDetailV2.tsx` 마이그레이션 성공 +- [x] 조회/수정/등록 모드 정상 작동 확인 +- [x] 유효성 검사 정상 작동 확인 + +--- + +## 2. 마이그레이션 대상 + +### 2.1 전체 현황 +| 구분 | 개수 | 비고 | +|------|------|------| +| IntegratedDetailTemplate 사용 | 47개 | 훅 마이그레이션 대상 | +| 레거시/커스텀 패턴 | 36개 | 별도 검토 (이번 범위 외) | +| **총계** | 83개 | | + +### 2.2 복잡도별 분류 +| 복잡도 | 기준 | 개수 | +|--------|------|------| +| 단순 | < 200줄, useState 3~4개 | 12개 | +| 보통 | 200~500줄, useState 5~7개 | 18개 | +| 복잡 | > 500줄, useState 8~11개 | 17개 | + +--- + +## 3. 도메인별 대상 목록 + +### 3.1 회계관리 (10개) + +| # | 파일 | 라인 | 복잡도 | 상태 | +|---|------|------|--------|------| +| 1 | `accounting/BadDebtCollection/BadDebtDetail.tsx` | 966 | 복잡 | ⬜ | +| 2 | `accounting/BillManagement/BillDetail.tsx` | 474 | 보통 | ✅ 완료 | +| 3 | `accounting/CardTransactionInquiry/CardTransactionDetailClient.tsx` | 138 | 단순 | ⬜ | +| 4 | `accounting/DepositManagement/DepositDetailClientV2.tsx` | 143 | 단순 | ⬜ | +| 5 | `accounting/PurchaseManagement/PurchaseDetail.tsx` | 698 | 복잡 | ⬜ | +| 6 | `accounting/SalesManagement/SalesDetail.tsx` | 581 | 복잡 | ⬜ | +| 7 | `accounting/VendorLedger/VendorLedgerDetail.tsx` | 385 | 보통 | ⬜ | +| 8 | `accounting/VendorManagement/VendorDetail.tsx` | 683 | 복잡 | ⬜ | +| 9 | `accounting/VendorManagement/VendorDetailClient.tsx` | 585 | 복잡 | ⬜ | +| 10 | `accounting/WithdrawalManagement/WithdrawalDetail.tsx` | 327 | 보통 | ⬜ | + +### 3.2 건설관리 (13개) + +| # | 파일 | 라인 | 복잡도 | 상태 | +|---|------|------|--------|------| +| 1 | `construction/bidding/BiddingDetailForm.tsx` | 544 | 복잡 | ⬜ | +| 2 | `construction/contract/ContractDetailForm.tsx` | 546 | 복잡 | ⬜ | +| 3 | `construction/estimates/EstimateDetailForm.tsx` | 763 | 복잡 | ⬜ | +| 4 | `construction/handover-report/HandoverReportDetailForm.tsx` | 699 | 복잡 | ⬜ | +| 5 | `construction/issue-management/IssueDetailForm.tsx` | 627 | 복잡 | ⬜ | +| 6 | `construction/item-management/ItemDetailClient.tsx` | 486 | 보통 | ⬜ | +| 7 | `construction/labor-management/LaborDetailClientV2.tsx` | 120 | 단순 | ⬜ | +| 8 | `construction/management/ConstructionDetailClient.tsx` | 739 | 복잡 | ⬜ | +| 9 | `construction/order-management/OrderDetailForm.tsx` | 275 | 보통 | ⬜ | +| 10 | `construction/pricing-management/PricingDetailClientV2.tsx` | 134 | 단순 | ⬜ | +| 11 | `construction/progress-billing/ProgressBillingDetailForm.tsx` | 193 | 단순 | ⬜ | +| 12 | `construction/site-management/SiteDetailForm.tsx` | 385 | 보통 | ⬜ | +| 13 | `construction/structure-review/StructureReviewDetailForm.tsx` | 392 | 보통 | ⬜ | + +### 3.3 기타 도메인 (24개) + +#### 고객센터 (3개) +| # | 파일 | 라인 | 복잡도 | 상태 | +|---|------|------|--------|------| +| 1 | `customer-center/EventManagement/EventDetail.tsx` | 101 | 단순 | ⬜ | +| 2 | `customer-center/InquiryManagement/InquiryDetail.tsx` | 357 | 보통 | ⬜ | +| 3 | `customer-center/NoticeManagement/NoticeDetail.tsx` | 101 | 단순 | ⬜ | + +#### 인사관리 (1개) +| # | 파일 | 라인 | 복잡도 | 상태 | +|---|------|------|--------|------| +| 1 | `hr/EmployeeManagement/EmployeeDetail.tsx` | 221 | 단순 | ⬜ | + +#### 자재관리 (2개) +| # | 파일 | 라인 | 복잡도 | 상태 | +|---|------|------|--------|------| +| 1 | `material/ReceivingManagement/ReceivingDetail.tsx` | ~350 | 보통 | ⬜ | +| 2 | `material/StockStatus/StockStatusDetail.tsx` | ~300 | 보통 | ⬜ | + +#### 주문관리 (2개) +| # | 파일 | 라인 | 복잡도 | 상태 | +|---|------|------|--------|------| +| 1 | `orders/OrderSalesDetailEdit.tsx` | 735 | 복잡 | ⬜ | +| 2 | `orders/OrderSalesDetailView.tsx` | 668 | 복잡 | ⬜ | + +#### 출고관리 (2개) +| # | 파일 | 라인 | 복잡도 | 상태 | +|---|------|------|--------|------| +| 1 | `outbound/ShipmentManagement/ShipmentDetail.tsx` | 670 | 복잡 | ⬜ | +| 2 | `outbound/VehicleDispatchManagement/VehicleDispatchDetail.tsx` | 180 | 단순 | ⬜ | + +#### 생산관리 (1개) +| # | 파일 | 라인 | 복잡도 | 상태 | +|---|------|------|--------|------| +| 1 | `production/WorkOrders/WorkOrderDetail.tsx` | 531 | 복잡 | ⬜ | + +#### 품질관리 (1개) +| # | 파일 | 라인 | 복잡도 | 상태 | +|---|------|------|--------|------| +| 1 | `quality/InspectionManagement/InspectionDetail.tsx` | 949 | 복잡 | ⬜ | + +#### 설정 (2개) +| # | 파일 | 라인 | 복잡도 | 상태 | +|---|------|------|--------|------| +| 1 | `settings/PermissionManagement/PermissionDetail.tsx` | 455 | 보통 | ⬜ | +| 2 | `settings/PopupManagement/PopupDetailClientV2.tsx` | 198 | 단순 | ⬜ | + +#### 거래처 (1개) +| # | 파일 | 라인 | 복잡도 | 상태 | +|---|------|------|--------|------| +| 1 | `clients/ClientDetailClientV2.tsx` | 252 | 단순 | ⬜ | + +#### 기타 (9개) +| # | 파일 | 라인 | 복잡도 | 상태 | +|---|------|------|--------|------| +| 1 | `board/BoardManagement/BoardDetail.tsx` | 119 | 단순 | ⬜ | +| 2 | `process-management/ProcessDetail.tsx` | 346 | 보통 | ⬜ | +| 3 | `process-management/StepDetail.tsx` | 143 | 단순 | ⬜ | +| 4 | `settings/AccountManagement/AccountDetail.tsx` | 355 | 보통 | ⬜ | +| 5 | `accounting/DepositManagement/DepositDetail.tsx` | 327 | 보통 | ⬜ | +| 6 | `clients/ClientDetail.tsx` | 253 | 보통 | ⬜ | +| 7 | `construction/labor-management/LaborDetailClient.tsx` | 471 | 보통 | ⬜ | +| 8 | `construction/pricing-management/PricingDetailClient.tsx` | 464 | 보통 | ⬜ | +| 9 | `quotes/LocationDetailPanel.tsx` | 826 | 복잡 | ⬜ | + +--- + +## 4. 작업 방식 + +### 4.1 Git 브랜치 전략 +``` +main + └── feature/detail-hooks-migration + ├── 회계관리 커밋 + ├── 건설관리 커밋 + └── 기타 도메인 커밋 +``` + +### 4.2 파일별 작업 순서 +1. 파일 읽기 및 현재 패턴 파악 +2. `useDetailData` 적용 (데이터 로딩 부분) +3. `useCRUDHandlers` 적용 (CRUD 핸들러 부분) +4. 개별 useState → 통합 formData 객체로 변환 (선택) +5. 기능 테스트 +6. 커밋 + +### 4.3 적용할 변경 패턴 + +#### Before (기존) +```tsx +const [data, setData] = useState(null); +const [isLoading, setIsLoading] = useState(true); +const [error, setError] = useState(null); + +useEffect(() => { + fetchData(id).then(result => { + if (result.success) setData(result.data); + else setError(result.error); + }).finally(() => setIsLoading(false)); +}, [id]); + +const handleSubmit = async () => { + const result = await updateData(id, formData); + if (result.success) { + toast.success('저장되었습니다.'); + router.push('/list'); + } else { + toast.error(result.error); + } +}; +``` + +#### After (신규) +```tsx +const { data, isLoading, error } = useDetailData(id, fetchData); + +const { handleUpdate, isSubmitting } = useCRUDHandlers({ + onUpdate: updateData, + successRedirect: '/list', + successMessages: { update: '저장되었습니다.' }, +}); +``` + +--- + +## 5. 일정 계획 + +| Phase | 대상 | 파일 수 | 예상 기간 | +|-------|------|---------|----------| +| Phase 1 | 회계관리 | 10개 | 1일 | +| Phase 2 | 건설관리 | 13개 | 1.5일 | +| Phase 3 | 기타 도메인 | 24개 | 2일 | +| Phase 4 | 통합 테스트 | - | 1일 | +| **총계** | | **47개** | **약 5~6일** | + +--- + +## 6. 체크리스트 + +### 6.1 사전 준비 +- [x] 공통 훅 4개 생성 완료 +- [x] 테스트 마이그레이션 (BillDetail) 완료 +- [x] 계획서 작성 +- [ ] 브랜치 생성 + +### 6.2 Phase 1: 회계관리 (0/10) +- [ ] BadDebtDetail.tsx +- [x] BillDetail.tsx ✅ +- [ ] CardTransactionDetailClient.tsx +- [ ] DepositDetailClientV2.tsx +- [ ] PurchaseDetail.tsx +- [ ] SalesDetail.tsx +- [ ] VendorLedgerDetail.tsx +- [ ] VendorDetail.tsx +- [ ] VendorDetailClient.tsx +- [ ] WithdrawalDetail.tsx + +### 6.3 Phase 2: 건설관리 (0/13) +- [ ] BiddingDetailForm.tsx +- [ ] ContractDetailForm.tsx +- [ ] EstimateDetailForm.tsx +- [ ] HandoverReportDetailForm.tsx +- [ ] IssueDetailForm.tsx +- [ ] ItemDetailClient.tsx +- [ ] LaborDetailClientV2.tsx +- [ ] ConstructionDetailClient.tsx +- [ ] OrderDetailForm.tsx +- [ ] PricingDetailClientV2.tsx +- [ ] ProgressBillingDetailForm.tsx +- [ ] SiteDetailForm.tsx +- [ ] StructureReviewDetailForm.tsx + +### 6.4 Phase 3: 기타 도메인 (0/24) +- [ ] EventDetail.tsx +- [ ] InquiryDetail.tsx +- [ ] NoticeDetail.tsx +- [ ] EmployeeDetail.tsx +- [ ] ReceivingDetail.tsx +- [ ] StockStatusDetail.tsx +- [ ] OrderSalesDetailEdit.tsx +- [ ] OrderSalesDetailView.tsx +- [ ] ShipmentDetail.tsx +- [ ] VehicleDispatchDetail.tsx +- [ ] WorkOrderDetail.tsx +- [ ] InspectionDetail.tsx +- [ ] PermissionDetail.tsx +- [ ] PopupDetailClientV2.tsx +- [ ] ClientDetailClientV2.tsx +- [ ] BoardDetail.tsx +- [ ] ProcessDetail.tsx +- [ ] StepDetail.tsx +- [ ] AccountDetail.tsx +- [ ] DepositDetail.tsx +- [ ] ClientDetail.tsx +- [ ] LaborDetailClient.tsx +- [ ] PricingDetailClient.tsx +- [ ] LocationDetailPanel.tsx + +### 6.5 완료 후 +- [ ] 전체 기능 테스트 +- [ ] 코드 리뷰 +- [ ] PR 머지 +- [ ] BillDetailV2.tsx 정리 (원본으로 교체) + +--- + +## 7. 위험 요소 및 대응 + +| 위험 | 가능성 | 대응 | +|------|--------|------| +| 기존 기능 손상 | 중 | 파일별 테스트, Git 롤백 준비 | +| 예상보다 복잡한 파일 | 중 | 복잡한 파일은 부분 적용 허용 | +| 타입 에러 | 높 | 래퍼 함수로 타입 호환성 확보 | + +--- + +## 8. 참고 자료 + +- 공통 훅 소스: `src/hooks/index.ts` +- 테스트 케이스: `BillDetailV2.tsx` +- 기존 템플릿: `IntegratedDetailTemplate.tsx` diff --git a/claudedocs/guides/badge-commonization-guide.md b/claudedocs/guides/badge-commonization-guide.md new file mode 100644 index 00000000..b8cde9ba --- /dev/null +++ b/claudedocs/guides/badge-commonization-guide.md @@ -0,0 +1,276 @@ +# Badge 공통화 가이드 + +> 작성일: 2026-02-05 +> 목적: 리스트 페이지에서 Badge 스타일을 공통 유틸로 통일하는 방법 안내 + +--- + +## 📦 공통 유틸 위치 + +``` +src/lib/utils/status-config.ts ← Badge 스타일 및 상태 설정 +src/lib/constants/filter-presets.ts ← 필터 프리셋 +``` + +--- + +## 🎨 1. 프리셋 스타일 사용하기 + +### 사용 가능한 프리셋 + +| 프리셋 | 스타일 | 용도 | +|--------|--------|------| +| `default` | `bg-gray-100 text-gray-800` | 기본/일반 | +| `success` | `bg-green-100 text-green-800` | 완료/성공/활성 | +| `warning` | `bg-yellow-100 text-yellow-800` | 대기/주의 | +| `destructive` | `bg-red-100 text-red-800` | 오류/반려/긴급 | +| `info` | `bg-blue-100 text-blue-800` | 진행중/정보 | +| `muted` | `bg-gray-100 text-gray-500` | 비활성/무효 | +| `orange` | `bg-orange-100 text-orange-800` | 우선/경고 | +| `purple` | `bg-purple-100 text-purple-800` | 제품/특수 | + +### 기본 사용법 + +```tsx +import { getPresetStyle } from '@/lib/utils/status-config'; + +// 프리셋 스타일 가져오기 +const style = getPresetStyle('success'); // "bg-green-100 text-green-800" + +// Badge에 적용 +대기 +``` + +--- + +## 🏷️ 2. 우선순위 Badge + +### 공통 유틸 사용 + +```tsx +import { getPriorityLabel, getPriorityStyle } from '@/lib/utils/status-config'; + +// 우선순위 Badge 렌더링 +function PriorityBadge({ priority }: { priority: string }) { + return ( + + {getPriorityLabel(priority)} + + ); +} + +// 사용 + // 긴급 (빨간색) + // 우선 (주황색) + // 일반 (회색) +``` + +### Before (개별 정의) ❌ + +```tsx +// 각 페이지마다 반복되던 코드 +const PRIORITY_COLORS: Record = { + '긴급': 'bg-red-100 text-red-700', + '우선': 'bg-orange-100 text-orange-700', + '일반': 'bg-gray-100 text-gray-700', +}; +``` + +### After (공통 유틸) ✅ + +```tsx +import { getPriorityLabel, getPriorityStyle } from '@/lib/utils/status-config'; + + + {getPriorityLabel(item.priority)} + +``` + +--- + +## 📦 3. 품목 유형 Badge + +### 공통 유틸 사용 + +```tsx +import { getItemTypeLabel, getItemTypeStyle } from '@/lib/utils/status-config'; + +// 품목 유형 Badge 렌더링 +function ItemTypeBadge({ itemType }: { itemType: string }) { + return ( + + {getItemTypeLabel(itemType)} + + ); +} + +// 사용 + // 제품 (보라색) + // 부품 (주황색) + // 부자재 (녹색) + // 원자재 (파란색) + // 소모품 (회색) +``` + +--- + +## 🔧 4. 커스텀 상태 설정 만들기 + +### createStatusConfig 사용법 + +```tsx +import { createStatusConfig } from '@/lib/utils/status-config'; + +// 도메인별 커스텀 상태 정의 +const { + STATUS_OPTIONS, // Select 옵션용 + STATUS_LABELS, // 라벨 맵 + STATUS_STYLES, // 스타일 맵 + getStatusLabel, // 라벨 헬퍼 + getStatusStyle, // 스타일 헬퍼 +} = createStatusConfig({ + draft: { label: '임시저장', style: 'muted' }, + pending: { label: '승인대기', style: 'warning' }, + approved: { label: '승인완료', style: 'success' }, + rejected: { label: '반려', style: 'destructive' }, +}, { includeAll: true, allLabel: '전체' }); + +// 사용 + + {getStatusLabel(item.status)} + + +// Select 옵션으로도 사용 가능 + +``` + +--- + +## ✅ 5. 미리 정의된 공통 설정 사용하기 + +### 바로 사용 가능한 설정들 + +```tsx +import { + COMMON_STATUS_CONFIG, // 대기/진행/완료 + WORK_STATUS_CONFIG, // 작업대기/진행중/작업완료 + APPROVAL_STATUS_CONFIG, // 승인대기/승인완료/반려 + ACTIVE_STATUS_CONFIG, // 활성/비활성 + SHIPMENT_STATUS_CONFIG, // 출고예정/대기/중/완료 + RECEIVING_STATUS_CONFIG, // 입고예정/대기/검사중/완료/반품 +} from '@/lib/utils/status-config'; + +// 사용 예시 + + {COMMON_STATUS_CONFIG.getStatusLabel(item.status)} + + +// 필터 옵션으로 사용 +filterConfig: [ + { + key: 'status', + label: '상태', + type: 'single', + options: WORK_STATUS_CONFIG.STATUS_OPTIONS.filter(opt => opt.value !== 'all'), + }, +], +``` + +--- + +## 🔄 6. 마이그레이션 체크리스트 + +### 기존 코드에서 공통 유틸로 전환하기 + +1. **우선순위 색상 정의 찾기** + ```bash + # 검색 + grep -r "PRIORITY_COLORS" src/components/ + grep -r "'긴급'" src/components/ + ``` + +2. **import 추가** + ```tsx + import { getPriorityLabel, getPriorityStyle } from '@/lib/utils/status-config'; + ``` + +3. **기존 코드 교체** + ```tsx + // Before + const color = PRIORITY_COLORS[item.priority] || ''; + {item.priority} + + // After + + {getPriorityLabel(item.priority)} + + ``` + +4. **기존 상수 정의 삭제** + ```tsx + // 삭제 + const PRIORITY_COLORS: Record = { ... }; + ``` + +--- + +## 📋 7. 필터 프리셋 함께 사용하기 + +### filter-presets.ts와 연계 + +```tsx +import { COMMON_PRIORITY_FILTER, WORK_STATUS_FILTER } from '@/lib/constants/filter-presets'; +import { getPriorityStyle, WORK_STATUS_CONFIG } from '@/lib/utils/status-config'; + +// UniversalListPage config +const config: UniversalListConfig = { + // ... + + // 필터 설정 (공통 프리셋 사용) + filterConfig: [ + WORK_STATUS_FILTER, + COMMON_PRIORITY_FILTER, + ], + + // 테이블 행에서 Badge 사용 (공통 스타일 사용) + renderTableRow: (item, index, globalIndex, handlers) => ( + + {/* ... */} + + + {WORK_STATUS_CONFIG.getStatusLabel(item.status)} + + + + + {getPriorityLabel(item.priority)} + + + + ), +}; +``` + +--- + +## 📊 변경 효과 + +| 항목 | Before | After | +|------|--------|-------| +| 우선순위 색상 정의 | 각 페이지 개별 | 1곳 (status-config.ts) | +| 품목유형 색상 정의 | 각 페이지 개별 | 1곳 (status-config.ts) | +| 색상 변경 시 | 모든 파일 수정 | 1개 파일만 수정 | +| 일관성 | 파일마다 다를 수 있음 | 항상 동일 | +| 신규 개발 시 | 기존 코드 복사 필요 | import만 하면 됨 | + +--- + +## 🚨 주의사항 + +1. **기존 기능 유지**: 마이그레이션 시 동작이 동일한지 확인 +2. **점진적 적용**: 한 번에 모든 파일 변경하지 말고, 수정하는 파일만 적용 +3. **테스트**: Badge 색상이 기존과 동일하게 표시되는지 확인 diff --git a/src/app/[locale]/(protected)/accounting/bills/[id]/page.tsx b/src/app/[locale]/(protected)/accounting/bills/[id]/page.tsx index 7775d30a..215c5152 100644 --- a/src/app/[locale]/(protected)/accounting/bills/[id]/page.tsx +++ b/src/app/[locale]/(protected)/accounting/bills/[id]/page.tsx @@ -1,7 +1,8 @@ 'use client'; import { useParams, useSearchParams } from 'next/navigation'; -import { BillDetail } from '@/components/accounting/BillManagement/BillDetail'; +// V2 테스트: 새 훅 적용 버전 +import { BillDetailV2 as BillDetail } from '@/components/accounting/BillManagement/BillDetailV2'; export default function BillDetailPage() { const params = useParams(); diff --git a/src/app/[locale]/(protected)/accounting/bills/new/page.tsx b/src/app/[locale]/(protected)/accounting/bills/new/page.tsx index e6159c62..832c33b1 100644 --- a/src/app/[locale]/(protected)/accounting/bills/new/page.tsx +++ b/src/app/[locale]/(protected)/accounting/bills/new/page.tsx @@ -1,6 +1,7 @@ 'use client'; -import { BillDetail } from '@/components/accounting/BillManagement/BillDetail'; +// V2: 새 훅 적용 버전 +import { BillDetailV2 as BillDetail } from '@/components/accounting/BillManagement/BillDetailV2'; export default function BillNewPage() { return ; diff --git a/src/app/[locale]/(protected)/accounting/bills/page.tsx b/src/app/[locale]/(protected)/accounting/bills/page.tsx index 95db618a..ecb2407a 100644 --- a/src/app/[locale]/(protected)/accounting/bills/page.tsx +++ b/src/app/[locale]/(protected)/accounting/bills/page.tsx @@ -3,7 +3,8 @@ import { useEffect, useState } from 'react'; import { useSearchParams } from 'next/navigation'; import { BillManagementClient } from '@/components/accounting/BillManagement/BillManagementClient'; -import { BillDetail } from '@/components/accounting/BillManagement/BillDetail'; +// V2 테스트: 새 훅 적용 버전 +import { BillDetailV2 as BillDetail } from '@/components/accounting/BillManagement/BillDetailV2'; import { getBills } from '@/components/accounting/BillManagement/actions'; import type { BillRecord } from '@/components/accounting/BillManagement/types'; diff --git a/src/components/accounting/BillManagement/BillDetail.tsx b/src/components/accounting/BillManagement/BillDetail.tsx index d8026e50..285a3e1f 100644 --- a/src/components/accounting/BillManagement/BillDetail.tsx +++ b/src/components/accounting/BillManagement/BillDetail.tsx @@ -122,6 +122,14 @@ export function BillDetail({ billId, mode }: BillDetailProps) { toast.error('금액을 입력해주세요.'); return { success: false, error: '금액을 입력해주세요.' }; } + if (!issueDate) { + toast.error('발행일을 입력해주세요.'); + return { success: false, error: '발행일을 입력해주세요.' }; + } + if (!maturityDate) { + toast.error('만기일을 입력해주세요.'); + return { success: false, error: '만기일을 입력해주세요.' }; + } // 차수 유효성 검사 for (let i = 0; i < installments.length; i++) { @@ -290,7 +298,9 @@ export function BillDetail({ billId, mode }: BillDetailProps) { {/* 발행일 */}
- + - + ([]); + + // ===== 폼 상태 (통합된 단일 state) ===== + const [formData, setFormData] = useState(INITIAL_FORM_DATA); + + // ===== 폼 필드 업데이트 헬퍼 ===== + const updateField = useCallback(( + field: K, + value: BillFormData[K] + ) => { + setFormData(prev => ({ ...prev, [field]: value })); + }, []); + + // ===== 거래처 목록 로드 ===== + useEffect(() => { + async function loadClients() { + const result = await getClients(); + if (result.success && result.data) { + setClients(result.data.map(c => ({ id: String(c.id), name: c.name }))); + } + } + loadClients(); + }, []); + + // ===== 새 훅: useDetailData로 데이터 로딩 ===== + // 타입 래퍼: 훅은 string | number를 받지만 actions는 string만 받음 + const fetchBillWrapper = useCallback( + (id: string | number) => getBill(String(id)), + [] + ); + + const { + data: billData, + isLoading, + error: loadError, + } = useDetailData( + billId !== 'new' ? billId : null, + fetchBillWrapper, + { skip: isNewMode } + ); + + // ===== 데이터 로드 시 폼에 반영 ===== + useEffect(() => { + if (billData) { + setFormData({ + billNumber: billData.billNumber, + billType: billData.billType, + vendorId: billData.vendorId, + amount: billData.amount, + issueDate: billData.issueDate, + maturityDate: billData.maturityDate, + status: billData.status, + note: billData.note, + installments: billData.installments, + }); + } + }, [billData]); + + // ===== 로드 에러 처리 ===== + useEffect(() => { + if (loadError) { + toast.error(loadError); + router.push('/ko/accounting/bills'); + } + }, [loadError, router]); + + // ===== 유효성 검사 함수 ===== + const validateForm = useCallback((): { valid: boolean; error?: string } => { + if (!formData.billNumber.trim()) { + return { valid: false, error: '어음번호를 입력해주세요.' }; + } + if (!formData.vendorId) { + return { valid: false, error: '거래처를 선택해주세요.' }; + } + if (formData.amount <= 0) { + return { valid: false, error: '금액을 입력해주세요.' }; + } + if (!formData.issueDate) { + return { valid: false, error: '발행일을 입력해주세요.' }; + } + if (!formData.maturityDate) { + return { valid: false, error: '만기일을 입력해주세요.' }; + } + + // 차수 유효성 검사 + for (let i = 0; i < formData.installments.length; i++) { + const inst = formData.installments[i]; + if (!inst.date) { + return { valid: false, error: `차수 ${i + 1}번의 일자를 입력해주세요.` }; + } + if (inst.amount <= 0) { + return { valid: false, error: `차수 ${i + 1}번의 금액을 입력해주세요.` }; + } + } + + return { valid: true }; + }, [formData]); + + // ===== 타입 래퍼: 훅은 string | number를 받지만 actions는 string만 받음 ===== + const updateBillWrapper = useCallback( + (id: string | number, data: Partial) => updateBill(String(id), data), + [] + ); + + const deleteBillWrapper = useCallback( + (id: string | number) => deleteBill(String(id)), + [] + ); + + // ===== 새 훅: useCRUDHandlers로 CRUD 처리 ===== + const { + handleCreate, + handleUpdate, + handleDelete: crudDelete, + isSubmitting, + isDeleting, + } = useCRUDHandlers, Partial>({ + onCreate: createBill, + onUpdate: updateBillWrapper, + onDelete: deleteBillWrapper, + successRedirect: '/ko/accounting/bills', + successMessages: { + create: '어음이 등록되었습니다.', + update: '어음이 수정되었습니다.', + delete: '어음이 삭제되었습니다.', + }, + // 수정 성공 시 view 모드로 이동 + disableRedirect: !isNewMode, + onSuccess: (action) => { + if (action === 'update') { + router.push(`/ko/accounting/bills/${billId}?mode=view`); + } + }, + }); + + // ===== 저장 핸들러 (유효성 검사 + CRUD 훅 사용) ===== + const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => { + // 유효성 검사 + const validation = validateForm(); + if (!validation.valid) { + toast.error(validation.error!); + return { success: false, error: validation.error }; + } + + const billData: Partial = { + ...formData, + vendorName: clients.find(c => c.id === formData.vendorId)?.name || '', + }; + + if (isNewMode) { + return handleCreate(billData); + } else { + return handleUpdate(billId, billData); + } + }, [formData, clients, isNewMode, billId, handleCreate, handleUpdate, validateForm]); + + // ===== 삭제 핸들러 ===== + const handleDelete = useCallback(async (): Promise<{ success: boolean; error?: string }> => { + return crudDelete(billId); + }, [billId, crudDelete]); + + // ===== 차수 관리 핸들러 ===== + const handleAddInstallment = useCallback(() => { + const newInstallment: InstallmentRecord = { + id: `inst-${Date.now()}`, + date: '', + amount: 0, + note: '', + }; + setFormData(prev => ({ + ...prev, + installments: [...prev.installments, newInstallment], + })); + }, []); + + const handleRemoveInstallment = useCallback((id: string) => { + setFormData(prev => ({ + ...prev, + installments: prev.installments.filter(inst => inst.id !== id), + })); + }, []); + + const handleUpdateInstallment = useCallback(( + id: string, + field: keyof InstallmentRecord, + value: string | number + ) => { + setFormData(prev => ({ + ...prev, + installments: prev.installments.map(inst => + inst.id === id ? { ...inst, [field]: value } : inst + ), + })); + }, []); + + // ===== 상태 옵션 (구분에 따라 변경) ===== + const statusOptions = useMemo( + () => getBillStatusOptions(formData.billType), + [formData.billType] + ); + + // ===== 폼 콘텐츠 렌더링 ===== + const renderFormContent = () => ( + <> + {/* 기본 정보 섹션 */} + + + 기본 정보 + + +
+ {/* 어음번호 */} +
+ + updateField('billNumber', e.target.value)} + placeholder="어음번호를 입력해주세요" + disabled={isViewMode} + /> +
+ + {/* 구분 */} +
+ + +
+ + {/* 거래처 */} +
+ + +
+ + {/* 금액 */} +
+ + updateField('amount', value ?? 0)} + placeholder="금액을 입력해주세요" + disabled={isViewMode} + /> +
+ + {/* 발행일 */} +
+ + updateField('issueDate', e.target.value)} + disabled={isViewMode} + /> +
+ + {/* 만기일 */} +
+ + updateField('maturityDate', e.target.value)} + disabled={isViewMode} + /> +
+ + {/* 상태 */} +
+ + +
+ + {/* 비고 */} +
+ + updateField('note', e.target.value)} + placeholder="비고를 입력해주세요" + disabled={isViewMode} + /> +
+
+
+
+ + {/* 차수 관리 섹션 */} + + + + * 차수 관리 + + {!isViewMode && ( + + )} + + + + + + No + 일자 + 금액 + 비고 + {!isViewMode && 삭제} + + + + {formData.installments.length === 0 ? ( + + + 등록된 차수가 없습니다 + + + ) : ( + formData.installments.map((inst, index) => ( + + {index + 1} + + handleUpdateInstallment(inst.id, 'date', e.target.value)} + disabled={isViewMode} + className="w-full" + /> + + + handleUpdateInstallment(inst.id, 'amount', value ?? 0)} + disabled={isViewMode} + className="w-full" + /> + + + handleUpdateInstallment(inst.id, 'note', e.target.value)} + disabled={isViewMode} + className="w-full" + /> + + {!isViewMode && ( + + + + )} + + )) + )} + +
+
+
+ + ); + + // ===== 템플릿 모드 및 동적 설정 ===== + const templateMode = isNewMode ? 'create' : mode; + const dynamicConfig = { + ...billConfig, + title: isViewMode ? '어음 상세' : '어음', + actions: { + ...billConfig.actions, + submitLabel: isNewMode ? '등록' : '저장', + }, + }; + + return ( + renderFormContent()} + renderForm={() => renderFormContent()} + /> + ); +} diff --git a/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx b/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx index 3f31f07d..d4df6a27 100644 --- a/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx +++ b/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx @@ -7,6 +7,7 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { QuantityInput } from '@/components/ui/quantity-input'; import { CurrencyInput } from '@/components/ui/currency-input'; import { @@ -269,7 +270,7 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) { <> {/* 문서 타입 및 열람 버튼 */}
- + {sourceDocument.type === 'proposal' ? '품의서' : '지출결의서'} 연결된 문서가 있습니다 diff --git a/src/components/accounting/PurchaseManagement/PurchaseDetailModal.tsx b/src/components/accounting/PurchaseManagement/PurchaseDetailModal.tsx index a5142071..2d0493c0 100644 --- a/src/components/accounting/PurchaseManagement/PurchaseDetailModal.tsx +++ b/src/components/accounting/PurchaseManagement/PurchaseDetailModal.tsx @@ -14,6 +14,7 @@ import { QuantityInput } from '@/components/ui/quantity-input'; import { CurrencyInput } from '@/components/ui/currency-input'; import { Switch } from '@/components/ui/switch'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { Select, SelectContent, @@ -180,7 +181,7 @@ export function PurchaseDetailModal({ {/* 근거 문서 */} {data.sourceDocument && (
- + {data.sourceDocument.type === 'proposal' ? '품의서' : '지출결의서'} {data.sourceDocument.documentNo} diff --git a/src/components/accounting/PurchaseManagement/index.tsx b/src/components/accounting/PurchaseManagement/index.tsx index 748b45f5..974b1695 100644 --- a/src/components/accounting/PurchaseManagement/index.tsx +++ b/src/components/accounting/PurchaseManagement/index.tsx @@ -26,6 +26,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { Switch } from '@/components/ui/switch'; import { Dialog, @@ -443,7 +444,7 @@ export function PurchaseManagement() { {item.vendorName} {item.sourceDocument ? ( - + {item.sourceDocument.type === 'proposal' ? '품의서' : '지출결의서'} ) : ( diff --git a/src/components/accounting/VendorManagement/CreditAnalysisModal/CreditAnalysisDocument.tsx b/src/components/accounting/VendorManagement/CreditAnalysisModal/CreditAnalysisDocument.tsx index 0e8814ab..efadfc72 100644 --- a/src/components/accounting/VendorManagement/CreditAnalysisModal/CreditAnalysisDocument.tsx +++ b/src/components/accounting/VendorManagement/CreditAnalysisModal/CreditAnalysisDocument.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import { AlertTriangle, CheckCircle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { cn } from '@/lib/utils'; import { CreditSignal } from './CreditSignal'; @@ -55,7 +56,7 @@ export function CreditAnalysisDocument({
{/* 기업 정보 */}
- + 신규거래 신용정보 조회

diff --git a/src/components/business/construction/management/DetailCard.tsx b/src/components/business/construction/management/DetailCard.tsx index d2473499..886bca8f 100644 --- a/src/components/business/construction/management/DetailCard.tsx +++ b/src/components/business/construction/management/DetailCard.tsx @@ -1,6 +1,7 @@ 'use client'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { cn } from '@/lib/utils'; import type { StageDetail, StageCardStatus } from './types'; import { DETAIL_CONFIG } from './types'; @@ -20,7 +21,7 @@ export default function DetailCard({ detail, onClick }: DetailCardProps) { case 'completed': return 완료; case 'in_progress': - return 진행중; + return 진행중; case 'waiting': return 대기; default: diff --git a/src/components/business/construction/management/KanbanColumn.tsx b/src/components/business/construction/management/KanbanColumn.tsx index 37c69db8..866636cc 100644 --- a/src/components/business/construction/management/KanbanColumn.tsx +++ b/src/components/business/construction/management/KanbanColumn.tsx @@ -3,6 +3,7 @@ import { ReactNode } from 'react'; import { cn } from '@/lib/utils'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; interface KanbanColumnProps { title: string; @@ -30,7 +31,7 @@ export default function KanbanColumn({

{title}

{count !== undefined && ( - {count}건 + {count}건 )}
{headerAction} diff --git a/src/components/business/construction/management/ProjectCard.tsx b/src/components/business/construction/management/ProjectCard.tsx index e56c426f..9b719258 100644 --- a/src/components/business/construction/management/ProjectCard.tsx +++ b/src/components/business/construction/management/ProjectCard.tsx @@ -1,6 +1,7 @@ 'use client'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { cn } from '@/lib/utils'; import type { ProjectDetail, ProjectStatus } from './types'; @@ -20,7 +21,7 @@ export default function ProjectCard({ project, isSelected, onClick }: ProjectCar case 'completed': return 완료; case 'in_progress': - return 진행; + return 진행; default: return {status}; } diff --git a/src/components/business/construction/management/StageCard.tsx b/src/components/business/construction/management/StageCard.tsx index 7e97e157..e57aa651 100644 --- a/src/components/business/construction/management/StageCard.tsx +++ b/src/components/business/construction/management/StageCard.tsx @@ -1,6 +1,7 @@ 'use client'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { cn } from '@/lib/utils'; import type { Stage, StageCardStatus } from './types'; import { STAGE_LABELS, STAGE_CARD_STATUS_LABELS } from './types'; @@ -18,7 +19,7 @@ export default function StageCard({ stage, isSelected, onClick }: StageCardProps case 'completed': return 완료; case 'in_progress': - return 진행중; + return 진행중; case 'waiting': return 대기; default: diff --git a/src/components/customer-center/InquiryManagement/InquiryList.tsx b/src/components/customer-center/InquiryManagement/InquiryList.tsx index 86d115b8..e7955773 100644 --- a/src/components/customer-center/InquiryManagement/InquiryList.tsx +++ b/src/components/customer-center/InquiryManagement/InquiryList.tsx @@ -16,6 +16,7 @@ import { TableCell, TableRow } from '@/components/ui/table'; import { Card, CardContent } from '@/components/ui/card'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { UniversalListPage, type UniversalListConfig, @@ -55,17 +56,11 @@ export function InquiryList() { router.push('/ko/customer-center/qna?mode=new'); }, [router]); - // ===== 상태 Badge 색상 ===== + // ===== 상태 Badge 색상 (공통 유틸 사용) ===== const getStatusBadge = useCallback((status: InquiryStatus) => { - if (status === 'waiting') { - return ( - - {INQUIRY_STATUS_LABELS[status]} - - ); - } + const style = status === 'waiting' ? getPresetStyle('warning') : getPresetStyle('success'); return ( - + {INQUIRY_STATUS_LABELS[status]} ); diff --git a/src/components/hr/EmployeeManagement/CSVUploadDialog.tsx b/src/components/hr/EmployeeManagement/CSVUploadDialog.tsx index 851b6f8a..226d8aab 100644 --- a/src/components/hr/EmployeeManagement/CSVUploadDialog.tsx +++ b/src/components/hr/EmployeeManagement/CSVUploadDialog.tsx @@ -19,6 +19,7 @@ import { TableRow, } from '@/components/ui/table'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { FileSpreadsheet, AlertCircle, CheckCircle } from 'lucide-react'; import { FileDropzone } from '@/components/ui/file-dropzone'; import type { Employee, CSVEmployeeRow, CSVValidationResult } from './types'; @@ -211,9 +212,9 @@ export function CSVUploadDialog({ {result.row} {result.isValid ? ( - 유효 + 유효 ) : ( - 오류 + 오류 )} {result.data.name || '-'} diff --git a/src/components/items/DynamicItemForm/sections/DynamicBOMSection.tsx b/src/components/items/DynamicItemForm/sections/DynamicBOMSection.tsx index 1b54ddde..def66927 100644 --- a/src/components/items/DynamicItemForm/sections/DynamicBOMSection.tsx +++ b/src/components/items/DynamicItemForm/sections/DynamicBOMSection.tsx @@ -13,6 +13,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { NumberInput } from '@/components/ui/number-input'; import { Table, @@ -489,7 +490,7 @@ function BOMLineRow({
- + 절곡품 전개도 정보
diff --git a/src/components/items/ItemDetailClient.tsx b/src/components/items/ItemDetailClient.tsx index 1b3a20df..038e7d75 100644 --- a/src/components/items/ItemDetailClient.tsx +++ b/src/components/items/ItemDetailClient.tsx @@ -9,6 +9,7 @@ import { useRouter } from 'next/navigation'; import type { ItemMaster } from '@/types/item'; import { ITEM_TYPE_LABELS, PART_TYPE_LABELS, PART_USAGE_LABELS, PRODUCT_CATEGORY_LABELS } from '@/types/item'; +import { getItemTypeStyle } from '@/lib/utils/status-config'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Label } from '@/components/ui/label'; @@ -38,20 +39,11 @@ interface ItemDetailClientProps { /** * 품목 유형별 Badge 반환 + * - 공통 유틸 getItemTypeStyle 사용 */ function getItemTypeBadge(itemType: string) { - const badges: Record = { - FG: { className: 'bg-purple-50 text-purple-700 border-purple-200' }, - PT: { className: 'bg-orange-50 text-orange-700 border-orange-200' }, - SM: { className: 'bg-green-50 text-green-700 border-green-200' }, - RM: { className: 'bg-blue-50 text-blue-700 border-blue-200' }, - CS: { className: 'bg-gray-50 text-gray-700 border-gray-200' }, - }; - - const config = badges[itemType] || { className: '' }; - return ( - + {ITEM_TYPE_LABELS[itemType as keyof typeof ITEM_TYPE_LABELS]} ); diff --git a/src/components/items/ItemForm/BOMSection.tsx b/src/components/items/ItemForm/BOMSection.tsx index f2f3337d..6f971587 100644 --- a/src/components/items/ItemForm/BOMSection.tsx +++ b/src/components/items/ItemForm/BOMSection.tsx @@ -8,6 +8,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { NumberInput } from '@/components/ui/number-input'; import { CurrencyInput } from '@/components/ui/currency-input'; import { @@ -332,7 +333,7 @@ export default function BOMSection({
- + 절곡품 전개도 정보
diff --git a/src/components/items/ItemListClient.tsx b/src/components/items/ItemListClient.tsx index 2798aa59..c61e4fe9 100644 --- a/src/components/items/ItemListClient.tsx +++ b/src/components/items/ItemListClient.tsx @@ -12,6 +12,7 @@ import { useState, useEffect, useRef, useMemo } from 'react'; import { useRouter } from 'next/navigation'; import type { ItemMaster } from '@/types/item'; import { ITEM_TYPE_LABELS } from '@/types/item'; +import { getItemTypeStyle } from '@/lib/utils/status-config'; import { useCommonCodes } from '@/hooks/useCommonCodes'; import { Badge } from '@/components/ui/badge'; import { Checkbox } from '@/components/ui/checkbox'; @@ -46,20 +47,11 @@ function useDebounce(value: T, delay: number): T { /** * 품목 유형별 Badge 색상 반환 + * - 공통 유틸 getItemTypeStyle 사용 */ function getItemTypeBadge(itemType: string) { - const badges: Record = { - FG: { variant: 'default', className: 'bg-purple-100 text-purple-700 border-purple-200' }, - PT: { variant: 'default', className: 'bg-orange-100 text-orange-700 border-orange-200' }, - SM: { variant: 'default', className: 'bg-green-100 text-green-700 border-green-200' }, - RM: { variant: 'default', className: 'bg-blue-100 text-blue-700 border-blue-200' }, - CS: { variant: 'default', className: 'bg-gray-100 text-gray-700 border-gray-200' }, - }; - - const config = badges[itemType] || { variant: 'outline' as const, className: '' }; - return ( - + {ITEM_TYPE_LABELS[itemType as keyof typeof ITEM_TYPE_LABELS]} ); diff --git a/src/components/items/ItemMasterDataManagement/tabs/MasterFieldTab/index.tsx b/src/components/items/ItemMasterDataManagement/tabs/MasterFieldTab/index.tsx index 29e9c2fe..6ffd5ff4 100644 --- a/src/components/items/ItemMasterDataManagement/tabs/MasterFieldTab/index.tsx +++ b/src/components/items/ItemMasterDataManagement/tabs/MasterFieldTab/index.tsx @@ -10,6 +10,7 @@ import type { ItemMasterField } from '@/contexts/ItemMasterContext'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { Plus, Edit, Trash2 } from 'lucide-react'; // 입력방식 옵션 (ItemMasterDataManagement에서 사용하는 상수) @@ -95,7 +96,7 @@ export function MasterFieldTab({ {field.category} )} {field.properties?.attributeType && field.properties.attributeType !== 'custom' && ( - + {field.properties.attributeType === 'unit' ? '단위 연동' : field.properties.attributeType === 'material' ? '재질 연동' : '표면처리 연동'} diff --git a/src/components/molecules/StatusBadge.tsx b/src/components/molecules/StatusBadge.tsx index db2df478..a1de77fc 100644 --- a/src/components/molecules/StatusBadge.tsx +++ b/src/components/molecules/StatusBadge.tsx @@ -3,6 +3,7 @@ import { Badge } from "@/components/ui/badge"; import { BadgeSm } from "@/components/atoms/BadgeSm"; import { LucideIcon } from "lucide-react"; +import { BADGE_STYLE_PRESETS, type StatusStylePreset } from "@/lib/utils/status-config"; /** * 상태 뱃지 컴포넌트 diff --git a/src/components/orders/OrderRegistration.tsx b/src/components/orders/OrderRegistration.tsx index 59a613af..27d46ead 100644 --- a/src/components/orders/OrderRegistration.tsx +++ b/src/components/orders/OrderRegistration.tsx @@ -23,6 +23,7 @@ import { PhoneInput } from "@/components/ui/phone-input"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; +import { getPresetStyle } from "@/lib/utils/status-config"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Select, @@ -553,7 +554,7 @@ export function OrderRegistration({ {form.selectedQuotation.quoteNumber} - + {form.selectedQuotation.grade} (우량)
@@ -907,7 +908,7 @@ export function OrderRegistration({
- + {group.label} diff --git a/src/components/pricing-distribution/PriceDistributionDetail.tsx b/src/components/pricing-distribution/PriceDistributionDetail.tsx index 37594d5d..a7f9352e 100644 --- a/src/components/pricing-distribution/PriceDistributionDetail.tsx +++ b/src/components/pricing-distribution/PriceDistributionDetail.tsx @@ -17,6 +17,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { Checkbox } from '@/components/ui/checkbox'; import { Select, @@ -429,7 +430,7 @@ export function PriceDistributionDetail({ id, mode: propMode }: Props) { {item.marginRate}% {formatPrice(item.salesPrice)} - + {item.status} diff --git a/src/components/pricing/PricingFormClient.tsx b/src/components/pricing/PricingFormClient.tsx index 44d8956e..eeebdcee 100644 --- a/src/components/pricing/PricingFormClient.tsx +++ b/src/components/pricing/PricingFormClient.tsx @@ -34,6 +34,7 @@ import { Textarea } from '@/components/ui/textarea'; import { CurrencyInput } from '@/components/ui/currency-input'; import { NumberInput } from '@/components/ui/number-input'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { Separator } from '@/components/ui/separator'; import { Select, @@ -352,19 +353,19 @@ export function PricingFormClient({ {isEditMode && initialData && (
{initialData.isFinal && ( - + 최종 확정됨 )} {initialData.currentRevision > 0 && ( - + 수정 {initialData.currentRevision}차 )} {initialData.status === 'active' && !initialData.isFinal && ( - + 활성 )} diff --git a/src/components/pricing/PricingHistoryDialog.tsx b/src/components/pricing/PricingHistoryDialog.tsx index 4047402a..a11ce919 100644 --- a/src/components/pricing/PricingHistoryDialog.tsx +++ b/src/components/pricing/PricingHistoryDialog.tsx @@ -12,6 +12,7 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { Separator } from '@/components/ui/separator'; import { History } from 'lucide-react'; import type { PricingData } from './types'; @@ -50,7 +51,7 @@ export function PricingHistoryDialog({
- 현재 버전 + 현재 버전 수정 {pricingData.currentRevision}차 diff --git a/src/components/production/ProductionDashboard/index.tsx b/src/components/production/ProductionDashboard/index.tsx index 0eb0454c..2f743411 100644 --- a/src/components/production/ProductionDashboard/index.tsx +++ b/src/components/production/ProductionDashboard/index.tsx @@ -18,6 +18,7 @@ import { StatCardGridSkeleton } from '@/components/ui/skeleton'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { Button } from '@/components/ui/button'; import { PageLayout } from '@/components/organisms/PageLayout'; import { toast } from 'sonner'; @@ -263,7 +264,7 @@ export default function ProductionDashboard() { 지연 작업 - + {stats.delayed} @@ -292,7 +293,7 @@ export default function ProductionDashboard() { 최근 완료 - + {stats.completed} diff --git a/src/components/production/WorkOrders/WorkOrderList.tsx b/src/components/production/WorkOrders/WorkOrderList.tsx index 413d742e..33b8c8c2 100644 --- a/src/components/production/WorkOrders/WorkOrderList.tsx +++ b/src/components/production/WorkOrders/WorkOrderList.tsx @@ -27,7 +27,6 @@ import { type StatCard, type ListParams, } from '@/components/templates/UniversalListPage'; -import type { FilterFieldConfig } from '@/components/molecules/MobileFilter'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; import { getWorkOrders, getWorkOrderStats, getProcessOptions } from './actions'; import type { ProcessOption } from './actions'; @@ -40,6 +39,9 @@ import { } from './types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { WipProductionModal } from './WipProductionModal'; +// 공통 유틸 import +import { WORK_STATUS_FILTER, COMMON_PRIORITY_FILTER } from '@/lib/constants/filter-presets'; +import { getPriorityStyle } from '@/lib/utils/status-config'; // 페이지당 항목 수 const ITEMS_PER_PAGE = 20; @@ -61,36 +63,8 @@ const PROCESS_CODE_TO_TAB: Record = { 'BENDING': 'bending', }; -// 우선순위 뱃지 색상 -const PRIORITY_COLORS: Record = { - '긴급': 'bg-red-100 text-red-700', - '우선': 'bg-orange-100 text-orange-700', - '일반': 'bg-gray-100 text-gray-700', -}; - -// 필터 설정: 상태 + 우선순위 -const filterConfig: FilterFieldConfig[] = [ - { - key: 'status', - label: '상태', - type: 'single', - options: [ - { value: 'waiting', label: '작업대기' }, - { value: 'in_progress', label: '진행중' }, - { value: 'completed', label: '작업완료' }, - ], - }, - { - key: 'priority', - label: '우선순위', - type: 'single', - options: [ - { value: 'urgent', label: '긴급' }, - { value: 'priority', label: '우선' }, - { value: 'normal', label: '일반' }, - ], - }, -]; +// 필터 설정: 공통 프리셋 사용 (상태 + 우선순위) +const filterConfig = [WORK_STATUS_FILTER, COMMON_PRIORITY_FILTER]; export function WorkOrderList() { const router = useRouter(); @@ -408,7 +382,7 @@ export function WorkOrderList() { - + {item.priorityLabel} diff --git a/src/components/production/WorkerScreen/ProcessDetailSection.tsx b/src/components/production/WorkerScreen/ProcessDetailSection.tsx index c90fbc38..dbc95b48 100644 --- a/src/components/production/WorkerScreen/ProcessDetailSection.tsx +++ b/src/components/production/WorkerScreen/ProcessDetailSection.tsx @@ -16,6 +16,7 @@ import { useState, useEffect, useCallback } from 'react'; import { ChevronDown, Loader2 } from 'lucide-react'; import { ContentSkeleton } from '@/components/ui/skeleton'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { Button } from '@/components/ui/button'; import { AlertDialog, @@ -366,7 +367,7 @@ function ProcessStepItemCard({ item, index, isCompleted }: ProcessStepItemCardPr {item.location} {item.isPriority && ( - + 선행 생산 )} diff --git a/src/components/production/WorkerScreen/WorkCard.tsx b/src/components/production/WorkerScreen/WorkCard.tsx index 71aeca88..cdd4cbd0 100644 --- a/src/components/production/WorkerScreen/WorkCard.tsx +++ b/src/components/production/WorkerScreen/WorkCard.tsx @@ -18,6 +18,7 @@ import { useState } from 'react'; import { ChevronDown } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { Button } from '@/components/ui/button'; import type { WorkOrder } from '../ProductionDashboard/types'; import { STATUS_LABELS } from '../ProductionDashboard/types'; @@ -72,13 +73,13 @@ export function WorkCard({
{/* 순위 뱃지 */} {order.priority <= 3 && ( - + {order.priority}순위 )} {/* 긴급 뱃지 */} {order.isUrgent && ( - + 긴급 )} diff --git a/src/components/quality/InspectionManagement/InspectionCreate.tsx b/src/components/quality/InspectionManagement/InspectionCreate.tsx index c83a17d8..75330c90 100644 --- a/src/components/quality/InspectionManagement/InspectionCreate.tsx +++ b/src/components/quality/InspectionManagement/InspectionCreate.tsx @@ -15,6 +15,7 @@ import { useRouter } from 'next/navigation'; import { Plus, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -185,9 +186,9 @@ export function InspectionCreate() { {item.constructionHeight} {isSame ? ( - 일치 + 일치 ) : ( - 불일치 + 불일치 )} {item.changeReason || '-'} diff --git a/src/components/quality/InspectionManagement/InspectionDetail.tsx b/src/components/quality/InspectionManagement/InspectionDetail.tsx index fe990fa2..07f15ab9 100644 --- a/src/components/quality/InspectionManagement/InspectionDetail.tsx +++ b/src/components/quality/InspectionManagement/InspectionDetail.tsx @@ -22,6 +22,7 @@ import { } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -301,9 +302,9 @@ export function InspectionDetail({ id }: InspectionDetailProps) { {item.constructionHeight} {isSame ? ( - 일치 + 일치 ) : ( - 불일치 + 불일치 )} {item.changeReason || '-'} @@ -354,9 +355,9 @@ export function InspectionDetail({ id }: InspectionDetailProps) { {item.constructionHeight} {isSame ? ( - 일치 + 일치 ) : ( - 불일치 + 불일치 )} {item.changeReason || '-'} diff --git a/src/components/quality/InspectionManagement/InspectionList.tsx b/src/components/quality/InspectionManagement/InspectionList.tsx index f369e861..6bee9404 100644 --- a/src/components/quality/InspectionManagement/InspectionList.tsx +++ b/src/components/quality/InspectionManagement/InspectionList.tsx @@ -21,6 +21,7 @@ import { } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; +import { getPresetStyle } from '@/lib/utils/status-config'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; import { @@ -323,9 +324,9 @@ export function InspectionList() { {item.locationCount} {item.requiredInfo === '완료' ? ( - 완료 + 완료 ) : ( - {item.requiredInfo} + {item.requiredInfo} )} {item.inspectionPeriod} diff --git a/src/components/quotes/QuoteRegistration.tsx b/src/components/quotes/QuoteRegistration.tsx index b8ec5064..16ed2b7b 100644 --- a/src/components/quotes/QuoteRegistration.tsx +++ b/src/components/quotes/QuoteRegistration.tsx @@ -24,6 +24,7 @@ import { import { Button } from "../ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; import { Badge } from "../ui/badge"; +import { getPresetStyle } from "@/lib/utils/status-config"; import { Alert, AlertDescription } from "../ui/alert"; import { FileText, @@ -1110,7 +1111,7 @@ export function QuoteRegistration({ 견적 산출 결과 - + 총 {calculatedGrandTotal.toLocaleString()}원
@@ -1125,7 +1126,7 @@ export function QuoteRegistration({
- + 견적 {itemResult.index + 1} diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 00000000..c63b2908 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,53 @@ +/** + * SAM ERP 공통 훅 모음 + * + * @example + * import { useDetailPageState, useDetailData, useCRUDHandlers } from '@/hooks'; + */ + +// ===== 상세 페이지 관련 ===== +export { useDetailPageState } from './useDetailPageState'; +export type { + DetailMode, + UseDetailPageStateOptions, + UseDetailPageStateReturn, +} from './useDetailPageState'; + +export { useDetailData } from './useDetailData'; +export type { + ApiResponse, + FetchFunction, + UseDetailDataOptions, + UseDetailDataReturn, +} from './useDetailData'; + +export { useCRUDHandlers } from './useCRUDHandlers'; +export type { + CRUDResult, + SuccessMessages, + ErrorMessages, + UseCRUDHandlersOptions, + UseCRUDHandlersReturn, +} from './useCRUDHandlers'; + +export { useDetailPermissions } from './useDetailPermissions'; +export type { + DetailPermissionConfig, + UseDetailPermissionsOptions, + UseDetailPermissionsReturn, +} from './useDetailPermissions'; + +// ===== 기존 훅 ===== +export { usePermission } from './usePermission'; +export { useAuthGuard } from './useAuthGuard'; +export { useUserRole } from './useUserRole'; +export { useCommonCodes } from './useCommonCodes'; +export { useClientList } from './useClientList'; +export { useClientGroupList } from './useClientGroupList'; +export { useItemList } from './useItemList'; +export { useDaumPostcode } from './useDaumPostcode'; +export { useCurrentTime } from './useCurrentTime'; +export { useFCM } from './useFCM'; +export { useMenuPolling } from './useMenuPolling'; +export { useCEODashboard } from './useCEODashboard'; +export { useCardManagementModals } from './useCardManagementModals'; diff --git a/src/hooks/useCRUDHandlers.ts b/src/hooks/useCRUDHandlers.ts new file mode 100644 index 00000000..f65c132f --- /dev/null +++ b/src/hooks/useCRUDHandlers.ts @@ -0,0 +1,292 @@ +'use client'; + +import { useCallback, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { toast } from 'sonner'; + +/** + * API 응답 타입 (Server Action 표준 형식) + */ +export interface ApiResponse { + success: boolean; + data?: T; + error?: string; +} + +/** + * CRUD 핸들러 반환 타입 + */ +export interface CRUDResult { + success: boolean; + error?: string; +} + +/** + * 성공 메시지 설정 + */ +export interface SuccessMessages { + create?: string; + update?: string; + delete?: string; +} + +/** + * 에러 메시지 설정 + */ +export interface ErrorMessages { + create?: string; + update?: string; + delete?: string; +} + +export interface UseCRUDHandlersOptions { + /** 등록 API 함수 */ + onCreate?: (data: TCreate) => Promise; + /** 수정 API 함수 */ + onUpdate?: (id: string | number, data: TUpdate) => Promise; + /** 삭제 API 함수 */ + onDelete?: (id: string | number) => Promise; + /** 성공 후 이동할 경로 (절대 경로 권장) */ + successRedirect?: string; + /** 성공 메시지 (기본값 제공) */ + successMessages?: SuccessMessages; + /** 에러 메시지 (기본값 제공) */ + errorMessages?: ErrorMessages; + /** 성공 후 콜백 (리다이렉트 전) */ + onSuccess?: (action: 'create' | 'update' | 'delete') => void | Promise; + /** 에러 발생 시 콜백 */ + onError?: (action: 'create' | 'update' | 'delete', error: string) => void; + /** 리다이렉트 비활성화 (커스텀 처리 시) */ + disableRedirect?: boolean; +} + +export interface UseCRUDHandlersReturn { + /** 등록 핸들러 */ + handleCreate: (data: TCreate) => Promise; + /** 수정 핸들러 */ + handleUpdate: (id: string | number, data: TUpdate) => Promise; + /** 삭제 핸들러 */ + handleDelete: (id: string | number) => Promise; + /** 등록/수정 통합 핸들러 (isCreateMode에 따라 분기) */ + handleSubmit: ( + data: TCreate | TUpdate, + options: { isCreateMode: boolean; id?: string | number } + ) => Promise; + /** 처리 중 상태 */ + isSubmitting: boolean; + /** 삭제 중 상태 */ + isDeleting: boolean; +} + +const DEFAULT_SUCCESS_MESSAGES: SuccessMessages = { + create: '등록되었습니다.', + update: '저장되었습니다.', + delete: '삭제되었습니다.', +}; + +const DEFAULT_ERROR_MESSAGES: ErrorMessages = { + create: '등록에 실패했습니다.', + update: '저장에 실패했습니다.', + delete: '삭제에 실패했습니다.', +}; + +/** + * CRUD 핸들러 통합 훅 + * + * 등록/수정/삭제 로직을 표준화하고 toast 알림, 리다이렉트를 자동 처리합니다. + * + * @example + * ```tsx + * const { handleSubmit, handleDelete, isSubmitting } = useCRUDHandlers({ + * onCreate: createBankAccount, + * onUpdate: updateBankAccount, + * onDelete: deleteBankAccount, + * successRedirect: '/ko/settings/accounts', + * successMessages: { + * create: '계좌가 등록되었습니다.', + * update: '계좌가 수정되었습니다.', + * delete: '계좌가 삭제되었습니다.', + * }, + * }); + * + * // 폼 제출 + * const onSubmit = async () => { + * const result = await handleSubmit(formData, { isCreateMode, id }); + * // result.success로 추가 처리 가능 + * }; + * + * // 삭제 + * const onDelete = async () => { + * await handleDelete(id); + * }; + * ``` + */ +export function useCRUDHandlers( + options: UseCRUDHandlersOptions = {} +): UseCRUDHandlersReturn { + const { + onCreate, + onUpdate, + onDelete, + successRedirect, + successMessages = {}, + errorMessages = {}, + onSuccess, + onError, + disableRedirect = false, + } = options; + + const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + + // 메시지 병합 + const messages = { + success: { ...DEFAULT_SUCCESS_MESSAGES, ...successMessages }, + error: { ...DEFAULT_ERROR_MESSAGES, ...errorMessages }, + }; + + // 리다이렉트 처리 + const handleRedirect = useCallback(() => { + if (!disableRedirect && successRedirect) { + router.push(successRedirect); + } + }, [router, successRedirect, disableRedirect]); + + // 등록 핸들러 + const handleCreate = useCallback( + async (data: TCreate): Promise => { + if (!onCreate) { + console.warn('[useCRUDHandlers] onCreate is not defined'); + return { success: false, error: 'onCreate is not defined' }; + } + + setIsSubmitting(true); + try { + const result = await onCreate(data); + + if (result.success) { + toast.success(messages.success.create); + await onSuccess?.('create'); + handleRedirect(); + return { success: true }; + } else { + const errorMsg = result.error || messages.error.create!; + toast.error(errorMsg); + onError?.('create', errorMsg); + return { success: false, error: errorMsg }; + } + } catch (err) { + const errorMsg = err instanceof Error ? err.message : messages.error.create!; + console.error('[useCRUDHandlers] Create error:', err); + toast.error(errorMsg); + onError?.('create', errorMsg); + return { success: false, error: errorMsg }; + } finally { + setIsSubmitting(false); + } + }, + [onCreate, messages, onSuccess, onError, handleRedirect] + ); + + // 수정 핸들러 + const handleUpdate = useCallback( + async (id: string | number, data: TUpdate): Promise => { + if (!onUpdate) { + console.warn('[useCRUDHandlers] onUpdate is not defined'); + return { success: false, error: 'onUpdate is not defined' }; + } + + setIsSubmitting(true); + try { + const result = await onUpdate(id, data); + + if (result.success) { + toast.success(messages.success.update); + await onSuccess?.('update'); + handleRedirect(); + return { success: true }; + } else { + const errorMsg = result.error || messages.error.update!; + toast.error(errorMsg); + onError?.('update', errorMsg); + return { success: false, error: errorMsg }; + } + } catch (err) { + const errorMsg = err instanceof Error ? err.message : messages.error.update!; + console.error('[useCRUDHandlers] Update error:', err); + toast.error(errorMsg); + onError?.('update', errorMsg); + return { success: false, error: errorMsg }; + } finally { + setIsSubmitting(false); + } + }, + [onUpdate, messages, onSuccess, onError, handleRedirect] + ); + + // 삭제 핸들러 + const handleDelete = useCallback( + async (id: string | number): Promise => { + if (!onDelete) { + console.warn('[useCRUDHandlers] onDelete is not defined'); + return { success: false, error: 'onDelete is not defined' }; + } + + setIsDeleting(true); + try { + const result = await onDelete(id); + + if (result.success) { + toast.success(messages.success.delete); + await onSuccess?.('delete'); + handleRedirect(); + return { success: true }; + } else { + const errorMsg = result.error || messages.error.delete!; + toast.error(errorMsg); + onError?.('delete', errorMsg); + return { success: false, error: errorMsg }; + } + } catch (err) { + const errorMsg = err instanceof Error ? err.message : messages.error.delete!; + console.error('[useCRUDHandlers] Delete error:', err); + toast.error(errorMsg); + onError?.('delete', errorMsg); + return { success: false, error: errorMsg }; + } finally { + setIsDeleting(false); + } + }, + [onDelete, messages, onSuccess, onError, handleRedirect] + ); + + // 등록/수정 통합 핸들러 + const handleSubmit = useCallback( + async ( + data: TCreate | TUpdate, + { isCreateMode, id }: { isCreateMode: boolean; id?: string | number } + ): Promise => { + if (isCreateMode) { + return handleCreate(data as TCreate); + } else { + if (!id) { + const errorMsg = 'ID is required for update'; + toast.error(errorMsg); + return { success: false, error: errorMsg }; + } + return handleUpdate(id, data as TUpdate); + } + }, + [handleCreate, handleUpdate] + ); + + return { + handleCreate, + handleUpdate, + handleDelete, + handleSubmit, + isSubmitting, + isDeleting, + }; +} diff --git a/src/hooks/useDetailData.ts b/src/hooks/useDetailData.ts new file mode 100644 index 00000000..66a3abd5 --- /dev/null +++ b/src/hooks/useDetailData.ts @@ -0,0 +1,129 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; + +/** + * API 응답 타입 (Server Action 표준 형식) + */ +export interface ApiResponse { + success: boolean; + data?: T; + error?: string; +} + +/** + * 데이터 fetch 함수 타입 + */ +export type FetchFunction = (id: string | number) => Promise>; + +export interface UseDetailDataOptions { + /** fetch 건너뛰기 (등록 모드 등) */ + skip?: boolean; + /** ID가 없어도 fetch (선택적) */ + allowNullId?: boolean; +} + +export interface UseDetailDataReturn { + /** 로드된 데이터 */ + data: T | null; + /** 데이터 직접 설정 (폼 리셋 등) */ + setData: (data: T | null) => void; + /** 로딩 상태 */ + isLoading: boolean; + /** 에러 메시지 */ + error: string | null; + /** 에러 설정 */ + setError: (error: string | null) => void; + /** 데이터 새로고침 */ + refetch: () => Promise; + /** 로드 성공 여부 */ + isSuccess: boolean; + /** 로드 실패 여부 */ + isError: boolean; +} + +/** + * 상세 데이터 로딩 훅 + * + * ID 기반으로 데이터를 fetch하고 로딩/에러 상태를 관리합니다. + * + * @example + * ```tsx + * // 기본 사용 + * const { data, isLoading, error } = useDetailData(id, getBankAccount); + * + * // 등록 모드에서 skip + * const { data, isLoading } = useDetailData(id, getBankAccount, { skip: isCreateMode }); + * + * // 데이터 새로고침 + * const { data, refetch } = useDetailData(id, getBankAccount); + * await refetch(); + * ``` + */ +export function useDetailData( + id: string | number | null | undefined, + fetchFn: FetchFunction, + options: UseDetailDataOptions = {} +): UseDetailDataReturn { + const { skip = false, allowNullId = false } = options; + + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(!skip && (!!id || allowNullId)); + const [error, setError] = useState(null); + + // fetch 함수 + const fetchData = useCallback(async () => { + // skip 조건 체크 + if (skip) { + setIsLoading(false); + return; + } + + // ID 체크 (allowNullId가 false면 ID 필수) + if (!id && !allowNullId) { + setIsLoading(false); + return; + } + + setIsLoading(true); + setError(null); + + try { + const result = await fetchFn(id as string | number); + + if (result.success && result.data) { + setData(result.data); + } else { + setError(result.error || '데이터를 불러오는데 실패했습니다.'); + setData(null); + } + } catch (err) { + console.error('[useDetailData] Fetch error:', err); + setError(err instanceof Error ? err.message : '알 수 없는 오류가 발생했습니다.'); + setData(null); + } finally { + setIsLoading(false); + } + }, [id, fetchFn, skip, allowNullId]); + + // ID 변경 시 자동 fetch + useEffect(() => { + fetchData(); + }, [fetchData]); + + // refetch 함수 (외부에서 호출 가능) + const refetch = useCallback(async () => { + await fetchData(); + }, [fetchData]); + + return { + data, + setData, + isLoading, + error, + setError, + refetch, + isSuccess: !isLoading && !error && data !== null, + isError: !isLoading && error !== null, + }; +} diff --git a/src/hooks/useDetailPageState.ts b/src/hooks/useDetailPageState.ts new file mode 100644 index 00000000..5e4f877d --- /dev/null +++ b/src/hooks/useDetailPageState.ts @@ -0,0 +1,183 @@ +'use client'; + +import { useState, useCallback, useMemo } from 'react'; +import { useRouter, useParams, useSearchParams } from 'next/navigation'; + +/** + * 상세 페이지 모드 타입 + * - view: 조회 모드 + * - edit: 수정 모드 + * - create: 등록 모드 + */ +export type DetailMode = 'view' | 'edit' | 'create'; + +export interface UseDetailPageStateOptions { + /** 기본 모드 (기본값: 'view') */ + defaultMode?: DetailMode; + /** 목록 페이지 경로 (뒤로가기용) */ + listPath?: string; +} + +export interface UseDetailPageStateReturn { + // ===== 라우터 정보 ===== + /** 현재 아이템 ID (params.id) */ + id: string | null; + /** 현재 locale */ + locale: string; + /** Next.js router */ + router: ReturnType; + + // ===== 모드 관리 ===== + /** 현재 모드 */ + mode: DetailMode; + /** 모드 변경 */ + setMode: (mode: DetailMode) => void; + /** 조회 모드 여부 */ + isViewMode: boolean; + /** 수정 모드 여부 */ + isEditMode: boolean; + /** 등록 모드 여부 */ + isCreateMode: boolean; + + // ===== 데이터 상태 ===== + /** 데이터 */ + data: T | null; + /** 데이터 설정 */ + setData: (data: T | null) => void; + /** 로딩 상태 */ + isLoading: boolean; + /** 로딩 상태 설정 */ + setIsLoading: (loading: boolean) => void; + /** 에러 메시지 */ + error: string | null; + /** 에러 설정 */ + setError: (error: string | null) => void; + + // ===== 네비게이션 ===== + /** 목록으로 이동 */ + goToList: () => void; + /** 수정 모드로 전환 (URL도 변경) */ + goToEdit: () => void; + /** 조회 모드로 전환 */ + goToView: () => void; +} + +/** + * 상세 페이지 공통 상태 관리 훅 + * + * 등록/수정/상세 페이지에서 반복되는 상태 및 라우터 로직을 통합합니다. + * + * @example + * ```tsx + * // 상세/수정 페이지 + * const { + * id, mode, isViewMode, isEditMode, + * data, setData, isLoading, setIsLoading, + * goToList, goToEdit + * } = useDetailPageState({ listPath: '/settings/accounts' }); + * + * // 등록 페이지 + * const { mode, isCreateMode, goToList } = useDetailPageState({ + * defaultMode: 'create', + * listPath: '/settings/accounts' + * }); + * ``` + */ +export function useDetailPageState( + options: UseDetailPageStateOptions = {} +): UseDetailPageStateReturn { + const { defaultMode = 'view', listPath } = options; + + // ===== 라우터 ===== + const router = useRouter(); + const params = useParams(); + const searchParams = useSearchParams(); + + // ID 추출 (params.id가 string | string[] | undefined일 수 있음) + const id = useMemo(() => { + const rawId = params?.id; + if (Array.isArray(rawId)) return rawId[0] || null; + return rawId || null; + }, [params?.id]); + + // locale 추출 + const locale = useMemo(() => { + const rawLocale = params?.locale; + if (Array.isArray(rawLocale)) return rawLocale[0] || 'ko'; + return rawLocale || 'ko'; + }, [params?.locale]); + + // ===== 모드 관리 ===== + // URL의 ?mode=edit 파라미터 확인 + const urlMode = searchParams?.get('mode'); + const initialMode = urlMode === 'edit' ? 'edit' : defaultMode; + + const [mode, setModeState] = useState(initialMode); + + const setMode = useCallback((newMode: DetailMode) => { + setModeState(newMode); + }, []); + + const isViewMode = mode === 'view'; + const isEditMode = mode === 'edit'; + const isCreateMode = mode === 'create'; + + // ===== 데이터 상태 ===== + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(!isCreateMode); // 등록 모드는 로딩 불필요 + const [error, setError] = useState(null); + + // ===== 네비게이션 ===== + const goToList = useCallback(() => { + if (listPath) { + router.push(`/${locale}${listPath}`); + } else { + router.back(); + } + }, [router, locale, listPath]); + + const goToEdit = useCallback(() => { + setModeState('edit'); + if (id) { + // URL에 mode=edit 추가 (현재 경로 유지) + const currentPath = window.location.pathname; + router.push(`${currentPath}?mode=edit`); + } + }, [router, id]); + + const goToView = useCallback(() => { + setModeState('view'); + if (id) { + // URL에서 mode 파라미터 제거 + const currentPath = window.location.pathname; + router.push(currentPath); + } + }, [router, id]); + + return { + // 라우터 정보 + id, + locale, + router, + + // 모드 관리 + mode, + setMode, + isViewMode, + isEditMode, + isCreateMode, + + // 데이터 상태 + data, + setData, + isLoading, + setIsLoading, + error, + setError, + + // 네비게이션 + goToList, + goToEdit, + goToView, + }; +} diff --git a/src/hooks/useDetailPermissions.ts b/src/hooks/useDetailPermissions.ts new file mode 100644 index 00000000..468ac33d --- /dev/null +++ b/src/hooks/useDetailPermissions.ts @@ -0,0 +1,114 @@ +'use client'; + +import { useMemo } from 'react'; +import { usePermission } from './usePermission'; + +/** + * 상세 페이지 권한 설정 타입 + */ +export interface DetailPermissionConfig { + /** 수정 권한 오버라이드 (boolean 또는 함수) */ + canEdit?: boolean | (() => boolean); + /** 삭제 권한 오버라이드 */ + canDelete?: boolean | (() => boolean); + /** 등록 권한 오버라이드 */ + canCreate?: boolean | (() => boolean); + /** 조회 권한 오버라이드 */ + canView?: boolean | (() => boolean); + /** 승인 권한 오버라이드 */ + canApprove?: boolean | (() => boolean); + /** 내보내기 권한 오버라이드 */ + canExport?: boolean | (() => boolean); +} + +export interface UseDetailPermissionsOptions { + /** 권한 설정 (config에서 전달) */ + config?: DetailPermissionConfig; + /** 권한 체크할 URL 직접 지정 (다른 메뉴 권한 체크 시) */ + overrideUrl?: string; +} + +export interface UseDetailPermissionsReturn { + /** 조회 가능 여부 */ + canView: boolean; + /** 등록 가능 여부 */ + canCreate: boolean; + /** 수정 가능 여부 */ + canEdit: boolean; + /** 삭제 가능 여부 */ + canDelete: boolean; + /** 승인 가능 여부 */ + canApprove: boolean; + /** 내보내기 가능 여부 */ + canExport: boolean; + /** 권한 로딩 중 */ + isLoading: boolean; +} + +/** + * 권한 값 해석 헬퍼 + * - boolean: 그대로 반환 + * - function: 실행 결과 반환 + * - undefined: 기본값 반환 + */ +function resolvePermission( + configValue: boolean | (() => boolean) | undefined, + defaultValue: boolean +): boolean { + if (configValue === undefined) { + return defaultValue; + } + if (typeof configValue === 'function') { + return configValue(); + } + return configValue; +} + +/** + * 상세 페이지 권한 통합 훅 + * + * usePermission 훅을 래핑하여 config 기반 권한 오버라이드를 지원합니다. + * IntegratedDetailTemplate과 함께 사용하거나 단독으로 사용 가능합니다. + * + * @example + * ```tsx + * // 기본 사용 (URL 기반 자동 권한) + * const { canEdit, canDelete } = useDetailPermissions(); + * + * // config 오버라이드 + * const { canEdit, canDelete } = useDetailPermissions({ + * config: { + * canEdit: isOwner, // boolean + * canDelete: () => isAdmin, // function + * } + * }); + * + * // 다른 메뉴 권한 체크 + * const { canApprove } = useDetailPermissions({ + * overrideUrl: '/approval/inbox' + * }); + * ``` + */ +export function useDetailPermissions( + options: UseDetailPermissionsOptions = {} +): UseDetailPermissionsReturn { + const { config, overrideUrl } = options; + + // 기본 권한 조회 (URL 기반) + const basePermissions = usePermission(overrideUrl); + + // config 오버라이드 적용 + const permissions = useMemo(() => { + return { + canView: resolvePermission(config?.canView, basePermissions.canView), + canCreate: resolvePermission(config?.canCreate, basePermissions.canCreate), + canEdit: resolvePermission(config?.canEdit, basePermissions.canUpdate), + canDelete: resolvePermission(config?.canDelete, basePermissions.canDelete), + canApprove: resolvePermission(config?.canApprove, basePermissions.canApprove), + canExport: resolvePermission(config?.canExport, basePermissions.canExport), + isLoading: basePermissions.isLoading, + }; + }, [config, basePermissions]); + + return permissions; +} diff --git a/src/lib/constants/filter-presets.ts b/src/lib/constants/filter-presets.ts new file mode 100644 index 00000000..5e8219f9 --- /dev/null +++ b/src/lib/constants/filter-presets.ts @@ -0,0 +1,286 @@ +/** + * 공통 필터 프리셋 + * + * 리스트 페이지에서 반복적으로 사용되는 필터 설정을 공통화 + * MobileFilter 및 UniversalListPage의 filterConfig에서 사용 + * + * @example + * import { COMMON_STATUS_FILTER, COMMON_PRIORITY_FILTER, createStatusFilter } from '@/lib/constants/filter-presets'; + * + * const filterConfig = [ + * COMMON_STATUS_FILTER, + * COMMON_PRIORITY_FILTER, + * createStatusFilter('approval', '승인상태', APPROVAL_STATUS_OPTIONS), + * ]; + */ + +import type { FilterFieldConfig, FilterOption } from '@/components/molecules/MobileFilter'; + +// ============================================================ +// 공통 필터 옵션 +// ============================================================ + +/** + * 상태 필터 옵션 (대기/진행/완료) + */ +export const STATUS_OPTIONS: readonly FilterOption[] = [ + { value: 'pending', label: '대기' }, + { value: 'in_progress', label: '진행중' }, + { value: 'completed', label: '완료' }, +] as const; + +/** + * 작업 상태 필터 옵션 (작업대기/진행중/작업완료) + */ +export const WORK_STATUS_OPTIONS: readonly FilterOption[] = [ + { value: 'waiting', label: '작업대기' }, + { value: 'in_progress', label: '진행중' }, + { value: 'completed', label: '작업완료' }, +] as const; + +/** + * 우선순위 필터 옵션 + */ +export const PRIORITY_OPTIONS: readonly FilterOption[] = [ + { value: 'urgent', label: '긴급' }, + { value: 'priority', label: '우선' }, + { value: 'normal', label: '일반' }, +] as const; + +/** + * 활성/비활성 상태 옵션 + */ +export const ACTIVE_STATUS_OPTIONS: readonly FilterOption[] = [ + { value: 'active', label: '활성' }, + { value: 'inactive', label: '비활성' }, +] as const; + +/** + * 승인 상태 옵션 + */ +export const APPROVAL_STATUS_OPTIONS: readonly FilterOption[] = [ + { value: 'pending', label: '승인대기' }, + { value: 'approved', label: '승인완료' }, + { value: 'rejected', label: '반려' }, +] as const; + +/** + * 정렬 옵션 (게시판 등) + */ +export const SORT_OPTIONS: readonly FilterOption[] = [ + { value: 'latest', label: '최신순' }, + { value: 'oldest', label: '오래된순' }, + { value: 'views', label: '조회순' }, +] as const; + +/** + * 품목 유형 옵션 + */ +export const ITEM_TYPE_OPTIONS: readonly FilterOption[] = [ + { value: 'FG', label: '제품' }, + { value: 'PT', label: '부품' }, + { value: 'SM', label: '부자재' }, + { value: 'RM', label: '원자재' }, + { value: 'CS', label: '소모품' }, +] as const; + +/** + * 배송 방식 옵션 + */ +export const DELIVERY_METHOD_OPTIONS: readonly FilterOption[] = [ + { value: 'delivery', label: '배송' }, + { value: 'pickup', label: '픽업' }, + { value: 'direct', label: '직접배송' }, +] as const; + +// ============================================================ +// 공통 필터 설정 (FilterFieldConfig) +// ============================================================ + +/** + * 공통 상태 필터 (대기/진행/완료) + */ +export const COMMON_STATUS_FILTER: FilterFieldConfig = { + key: 'status', + label: '상태', + type: 'single', + options: STATUS_OPTIONS, + allOptionLabel: '전체', +}; + +/** + * 작업 상태 필터 (작업대기/진행중/작업완료) + */ +export const WORK_STATUS_FILTER: FilterFieldConfig = { + key: 'status', + label: '상태', + type: 'single', + options: WORK_STATUS_OPTIONS, + allOptionLabel: '전체', +}; + +/** + * 우선순위 필터 + */ +export const COMMON_PRIORITY_FILTER: FilterFieldConfig = { + key: 'priority', + label: '우선순위', + type: 'single', + options: PRIORITY_OPTIONS, + allOptionLabel: '전체', +}; + +/** + * 활성/비활성 필터 + */ +export const ACTIVE_STATUS_FILTER: FilterFieldConfig = { + key: 'isActive', + label: '상태', + type: 'single', + options: ACTIVE_STATUS_OPTIONS, + allOptionLabel: '전체', +}; + +/** + * 승인 상태 필터 + */ +export const APPROVAL_STATUS_FILTER: FilterFieldConfig = { + key: 'approvalStatus', + label: '승인상태', + type: 'single', + options: APPROVAL_STATUS_OPTIONS, + allOptionLabel: '전체', +}; + +/** + * 정렬 필터 (게시판 등) + */ +export const SORT_FILTER: FilterFieldConfig = { + key: 'sort', + label: '정렬', + type: 'single', + options: SORT_OPTIONS, +}; + +/** + * 품목 유형 필터 + */ +export const ITEM_TYPE_FILTER: FilterFieldConfig = { + key: 'itemType', + label: '품목유형', + type: 'single', + options: ITEM_TYPE_OPTIONS, + allOptionLabel: '전체', +}; + +/** + * 품목 유형 다중선택 필터 + */ +export const ITEM_TYPE_MULTI_FILTER: FilterFieldConfig = { + key: 'itemTypes', + label: '품목유형', + type: 'multi', + options: ITEM_TYPE_OPTIONS, +}; + +/** + * 배송 방식 필터 + */ +export const DELIVERY_METHOD_FILTER: FilterFieldConfig = { + key: 'deliveryMethod', + label: '배송방식', + type: 'single', + options: DELIVERY_METHOD_OPTIONS, + allOptionLabel: '전체', +}; + +// ============================================================ +// 필터 생성 유틸리티 함수 +// ============================================================ + +/** + * 커스텀 단일선택 필터 생성 + * + * @param key - 필터 키 + * @param label - 필터 라벨 + * @param options - 옵션 배열 + * @param allOptionLabel - '전체' 옵션 라벨 (기본: '전체') + */ +export function createSingleFilter( + key: string, + label: string, + options: readonly FilterOption[], + allOptionLabel = '전체' +): FilterFieldConfig { + return { + key, + label, + type: 'single', + options, + allOptionLabel, + }; +} + +/** + * 커스텀 다중선택 필터 생성 + * + * @param key - 필터 키 + * @param label - 필터 라벨 + * @param options - 옵션 배열 + */ +export function createMultiFilter( + key: string, + label: string, + options: readonly FilterOption[] +): FilterFieldConfig { + return { + key, + label, + type: 'multi', + options, + }; +} + +/** + * API 응답에서 필터 옵션 생성 + * + * @param items - API 응답 아이템 배열 + * @param valueKey - value로 사용할 키 + * @param labelKey - label로 사용할 키 + */ +export function createOptionsFromApi>( + items: T[], + valueKey: keyof T, + labelKey: keyof T +): FilterOption[] { + return items.map((item) => ({ + value: String(item[valueKey]), + label: String(item[labelKey]), + })); +} + +// ============================================================ +// 초기 필터 값 헬퍼 +// ============================================================ + +/** + * 기본 필터 초기값 생성 + * + * @param filters - 필터 설정 배열 + * @returns 초기값 객체 (single: 'all', multi: []) + */ +export function createInitialFilterValues( + filters: FilterFieldConfig[] +): Record { + const values: Record = {}; + + for (const filter of filters) { + if (filter.type === 'single') { + values[filter.key] = 'all'; + } else { + values[filter.key] = []; + } + } + + return values; +} diff --git a/src/lib/utils/status-config.ts b/src/lib/utils/status-config.ts index b13c61f9..5b387e6a 100644 --- a/src/lib/utils/status-config.ts +++ b/src/lib/utils/status-config.ts @@ -151,4 +151,167 @@ export function getPresetStyle( return presets[preset] || presets.default; } +// ============================================================ +// 공통 우선순위 설정 +// ============================================================ + +/** + * 우선순위 타입 + */ +export type PriorityType = 'urgent' | 'priority' | 'normal'; + +/** + * 우선순위 라벨 + */ +export const PRIORITY_LABELS: Record = { + urgent: '긴급', + priority: '우선', + normal: '일반', +}; + +/** + * 우선순위 Badge 스타일 + */ +export const PRIORITY_BADGE_STYLES: Record = { + urgent: 'bg-red-100 text-red-700 border-red-200', + priority: 'bg-orange-100 text-orange-700 border-orange-200', + normal: 'bg-gray-100 text-gray-700 border-gray-200', +}; + +/** + * 한글 우선순위 라벨 → 영문 키 매핑 + */ +const PRIORITY_LABEL_TO_KEY: Record = { + '긴급': 'urgent', + '우선': 'priority', + '일반': 'normal', +}; + +/** + * 우선순위 라벨 가져오기 + * @param priority - 영문 키('urgent') 또는 한글 라벨('긴급') + */ +export function getPriorityLabel(priority: string): string { + // 이미 한글이면 그대로 반환 + if (PRIORITY_LABEL_TO_KEY[priority]) { + return priority; + } + return PRIORITY_LABELS[priority as PriorityType] || priority; +} + +/** + * 우선순위 스타일 가져오기 + * @param priority - 영문 키('urgent') 또는 한글 라벨('긴급') + */ +export function getPriorityStyle(priority: string): string { + // 한글 라벨이면 영문 키로 변환 + const key = PRIORITY_LABEL_TO_KEY[priority] || (priority as PriorityType); + return PRIORITY_BADGE_STYLES[key] || PRIORITY_BADGE_STYLES.normal; +} + +// ============================================================ +// 공통 품목 유형 설정 +// ============================================================ + +/** + * 품목 유형 타입 + */ +export type ItemType = 'FG' | 'PT' | 'SM' | 'RM' | 'CS'; + +/** + * 품목 유형 라벨 + */ +export const ITEM_TYPE_LABELS: Record = { + FG: '제품', + PT: '부품', + SM: '부자재', + RM: '원자재', + CS: '소모품', +}; + +/** + * 품목 유형 Badge 스타일 + */ +export const ITEM_TYPE_BADGE_STYLES: Record = { + FG: 'bg-purple-100 text-purple-700 border-purple-200', + PT: 'bg-orange-100 text-orange-700 border-orange-200', + SM: 'bg-green-100 text-green-700 border-green-200', + RM: 'bg-blue-100 text-blue-700 border-blue-200', + CS: 'bg-gray-100 text-gray-700 border-gray-200', +}; + +/** + * 품목 유형 라벨 가져오기 + */ +export function getItemTypeLabel(itemType: string): string { + return ITEM_TYPE_LABELS[itemType as ItemType] || itemType; +} + +/** + * 품목 유형 스타일 가져오기 + */ +export function getItemTypeStyle(itemType: string): string { + return ITEM_TYPE_BADGE_STYLES[itemType as ItemType] || ITEM_TYPE_BADGE_STYLES.CS; +} + +// ============================================================ +// 공통 상태 설정 (미리 생성된 설정) +// ============================================================ + +/** + * 기본 상태 설정 (대기/진행/완료) + */ +export const COMMON_STATUS_CONFIG = createStatusConfig({ + pending: { label: '대기', style: 'warning' }, + in_progress: { label: '진행중', style: 'info' }, + completed: { label: '완료', style: 'success' }, +}, { includeAll: true }); + +/** + * 작업 상태 설정 (작업대기/진행중/작업완료) + */ +export const WORK_STATUS_CONFIG = createStatusConfig({ + waiting: { label: '작업대기', style: 'warning' }, + in_progress: { label: '진행중', style: 'info' }, + completed: { label: '작업완료', style: 'success' }, +}, { includeAll: true }); + +/** + * 승인 상태 설정 + */ +export const APPROVAL_STATUS_CONFIG = createStatusConfig({ + pending: { label: '승인대기', style: 'warning' }, + approved: { label: '승인완료', style: 'success' }, + rejected: { label: '반려', style: 'destructive' }, +}, { includeAll: true }); + +/** + * 활성/비활성 상태 설정 + */ +export const ACTIVE_STATUS_CONFIG = createStatusConfig({ + active: { label: '활성', style: 'success' }, + inactive: { label: '비활성', style: 'muted' }, +}, { includeAll: true }); + +/** + * 출고 상태 설정 + */ +export const SHIPMENT_STATUS_CONFIG = createStatusConfig({ + scheduled: { label: '출고예정', style: 'default' }, + ready: { label: '출고대기', style: 'warning' }, + shipping: { label: '출고중', style: 'info' }, + completed: { label: '출고완료', style: 'success' }, +}, { includeAll: true }); + +/** + * 입고 상태 설정 + */ +export const RECEIVING_STATUS_CONFIG = createStatusConfig({ + scheduled: { label: '입고예정', style: 'default' }, + pending: { label: '입고대기', style: 'warning' }, + inspecting: { label: '검사중', style: 'info' }, + completed: { label: '입고완료', style: 'success' }, + rejected: { label: '반품', style: 'destructive' }, +}, { includeAll: true }); + export default createStatusConfig;