diff --git a/claudedocs/[ANALYSIS-2026-01-20] 공통화-현황-분석.md b/claudedocs/[ANALYSIS-2026-01-20] 공통화-현황-분석.md new file mode 100644 index 00000000..39a40728 --- /dev/null +++ b/claudedocs/[ANALYSIS-2026-01-20] 공통화-현황-분석.md @@ -0,0 +1,213 @@ +# 프로젝트 공통화 현황 분석 + +## 1. 핵심 지표 요약 + +| 구분 | 적용 현황 | 비고 | +|------|----------|------| +| **IntegratedDetailTemplate** | 96개 파일 (228회 사용) | 상세/수정/등록 페이지 통합 | +| **IntegratedListTemplateV2** | 50개 파일 (60회 사용) | 목록 페이지 통합 | +| **DetailConfig 파일** | 39개 생성 | 설정 기반 페이지 구성 | +| **레거시 패턴 (PageLayout 직접 사용)** | ~40-50개 파일 | 마이그레이션 대상 | + +--- + +## 2. 공통화 달성률 + +### 2.1 상세 페이지 (Detail) +``` +총 Detail 컴포넌트: ~105개 +IntegratedDetailTemplate 적용: ~65개 +적용률: 약 62% +``` + +### 2.2 목록 페이지 (List) +``` +총 List 컴포넌트: ~61개 +IntegratedListTemplateV2 적용: ~50개 +적용률: 약 82% +``` + +### 2.3 폼 컴포넌트 (Form) +``` +총 Form 컴포넌트: ~72개 +공통 템플릿 미적용 (개별 구현) +적용률: 0% +``` + +--- + +## 3. 잘 공통화된 영역 ✅ + +### 3.1 템플릿 시스템 +| 템플릿 | 용도 | 적용 현황 | +|--------|------|----------| +| IntegratedDetailTemplate | 상세/수정/등록 | 96개 파일 | +| IntegratedListTemplateV2 | 목록 페이지 | 50개 파일 | +| UniversalListPage | 범용 목록 | 7개 파일 | + +### 3.2 UI 컴포넌트 (Radix UI 기반) +- **AlertDialog**: 65개 파일에서 일관되게 사용 +- **Dialog**: 142개 파일에서 사용 +- **Toast (Sonner)**: 133개 파일에서 일관되게 사용 +- **Pagination**: 54개 파일에서 통합 사용 + +### 3.3 데이터 테이블 +- **DataTable**: 공통 컴포넌트로 추상화됨 +- **IntegratedListTemplateV2에 통합**: 자동 페이지네이션, 필터링 + +--- + +## 4. 추가 공통화 기회 🔧 + +### 4.1 우선순위 높음 (High Priority) + +#### 📋 Form 템플릿 (IntegratedFormTemplate) +**현황**: 72개 Form 컴포넌트가 개별적으로 구현됨 +**제안**: +```typescript +// 제안: IntegratedFormTemplate + } +/> +``` +**효과**: +- 폼 레이아웃 일관성 +- 버튼 영역 통합 (저장/취소/삭제) +- 유효성 검사 패턴 통합 + +#### 📝 레거시 페이지 마이그레이션 +**현황**: ~40-50개 파일이 PageLayout/PageHeader 직접 사용 +**대상 파일** (샘플): +- `SubscriptionClient.tsx` +- `SubscriptionManagement.tsx` +- `ComprehensiveAnalysis/index.tsx` +- `DailyReport/index.tsx` +- `ReceivablesStatus/index.tsx` +- `FAQManagement/FAQList.tsx` +- `DepartmentManagement/index.tsx` +- 등등 + +--- + +### 4.2 우선순위 중간 (Medium Priority) + +#### 🗑️ 삭제 확인 다이얼로그 통합 +**현황**: 각 컴포넌트에서 AlertDialog 반복 구현 +**제안**: +```typescript +// 제안: useDeleteConfirm hook +const { openDeleteConfirm, DeleteConfirmDialog } = useDeleteConfirm({ + title: '삭제 확인', + description: '정말 삭제하시겠습니까?', + onConfirm: handleDelete, +}); + +// 또는 공통 컴포넌트 + setIsOpen(false)} +/> +``` + +#### 📁 파일 업로드/다운로드 패턴 통합 +**현황**: 여러 컴포넌트에서 파일 처리 로직 중복 +**제안**: +```typescript +// 제안: useFileUpload hook +const { uploadFile, downloadFile, FileDropzone } = useFileUpload({ + accept: ['image/*', '.pdf'], + maxSize: 10 * 1024 * 1024, +}); +``` + +#### 🔄 로딩 상태 표시 통합 +**현황**: 43개 파일에서 다양한 로딩 패턴 사용 +**제안**: +- `LoadingOverlay` 컴포넌트 확대 적용 +- `Skeleton` 패턴 표준화 + +--- + +### 4.3 우선순위 낮음 (Low Priority) + +#### 📊 대시보드 카드 컴포넌트 +**현황**: CEO 대시보드, 생산 대시보드 등에서 유사 패턴 +**제안**: `DashboardCard`, `StatCard` 공통 컴포넌트 + +#### 🔍 검색/필터 패턴 +**현황**: IntegratedListTemplateV2에 이미 통합됨 +**추가**: 독립 검색 컴포넌트 표준화 + +--- + +## 5. 레거시 파일 정리 대상 + +### 5.1 _legacy 폴더 (삭제 검토) +``` +src/components/hr/CardManagement/_legacy/ + - CardDetail.tsx + - CardForm.tsx + +src/components/settings/AccountManagement/_legacy/ + - AccountDetail.tsx +``` + +### 5.2 V1/V2 중복 파일 (통합 검토) +- `LaborDetailClient.tsx` vs `LaborDetailClientV2.tsx` +- `PricingDetailClient.tsx` vs `PricingDetailClientV2.tsx` +- `DepositDetail.tsx` vs `DepositDetailClientV2.tsx` +- `WithdrawalDetail.tsx` vs `WithdrawalDetailClientV2.tsx` + +--- + +## 6. 권장 액션 플랜 + +### Phase 7: 레거시 페이지 마이그레이션 +| 순서 | 대상 | 예상 작업량 | +|------|------|------------| +| 1 | 설정 관리 페이지 (8개) | 중간 | +| 2 | 회계 관리 페이지 (5개) | 중간 | +| 3 | 인사 관리 페이지 (5개) | 중간 | +| 4 | 보고서/분석 페이지 (3개) | 낮음 | + +### Phase 8: Form 템플릿 개발 +1. IntegratedFormTemplate 설계 +2. 파일럿 적용 (2-3개 Form) +3. 점진적 마이그레이션 + +### Phase 9: 유틸리티 Hook 개발 +1. useDeleteConfirm +2. useFileUpload +3. useFormState (공통 폼 상태 관리) + +### Phase 10: 레거시 정리 +1. _legacy 폴더 삭제 +2. V1/V2 중복 파일 통합 +3. 미사용 컴포넌트 정리 + +--- + +## 7. 결론 + +### 공통화 성과 +- **상세 페이지**: 62% 공통화 달성 (Phase 6 완료) +- **목록 페이지**: 82% 공통화 달성 +- **UI 컴포넌트**: Radix UI 기반 일관성 확보 +- **토스트/알림**: Sonner로 완전 통합 + +### 남은 과제 +- **Form 템플릿**: 72개 폼 컴포넌트 공통화 필요 +- **레거시 페이지**: ~40-50개 마이그레이션 필요 +- **코드 정리**: _legacy, V1/V2 중복 파일 정리 + +### 예상 효과 (추가 공통화 시) +- 코드 중복 30% 추가 감소 +- 신규 페이지 개발 시간 50% 단축 +- 유지보수성 대폭 향상 diff --git a/claudedocs/[ANALYSIS] common-component-patterns.md b/claudedocs/[ANALYSIS] common-component-patterns.md new file mode 100644 index 00000000..538ae3b7 --- /dev/null +++ b/claudedocs/[ANALYSIS] common-component-patterns.md @@ -0,0 +1,158 @@ +# 공통 컴포넌트 패턴 분석 + +> Phase 3 마이그레이션 진행하면서 발견되는 공통화 후보 패턴 수집 +> 페이지 마이그레이션 완료 후 이 리스트 기반으로 공통 컴포넌트 설계 + +## 최종 목표: IntegratedDetailTemplate Config 통합 + +공통 컴포넌트 추출 후 `IntegratedDetailTemplate`의 필드 config 옵션으로 통합 + +**현재 지원 타입:** +```typescript +type: 'text' | 'select' | 'date' | 'textarea' | 'number' | 'checkbox' +``` + +**확장 예정 타입:** +```typescript +// 주소 입력 +{ name: 'address', label: '주소', type: 'address', withCoords?: boolean } + +// 파일 업로드 +{ name: 'files', label: '첨부파일', type: 'file-upload', maxSize?: number, multiple?: boolean, accept?: string } + +// 음성 메모 +{ name: 'memo', label: '메모', type: 'voice-memo' } + +// 삭제 확인 (페이지 레벨 옵션) +deleteConfirm?: { title: string, message: string } +``` + +**장점:** +- 페이지별 config만 수정하면 UI 자동 구성 +- 일관된 UX/UI 보장 +- 유지보수 용이 + +## 발견된 패턴 목록 + +### 1. 주소 입력 (Address Input) +| 발견 위치 | 구성 요소 | 특이사항 | +|-----------|-----------|----------| +| site-management/SiteDetailForm | 우편번호 찾기 버튼 + 주소 Input + 상세주소 | 경도/위도 필드 별도 | + +**공통 요소:** +- Daum Postcode API 연동 (`useDaumPostcode` 훅 이미 존재) +- 우편번호 찾기 버튼 +- 기본주소 (자동 입력) +- 상세주소 (수동 입력) + +**변형 가능성:** +- 경도/위도 필드 포함 여부 +- 읽기 전용 모드 지원 + +--- + +### 2. 파일 업로드 (File Upload) +| 발견 위치 | 구성 요소 | 특이사항 | +|-----------|-----------|----------| +| site-management/SiteDetailForm | 드래그앤드롭 영역 + 파일 목록 | 10MB 제한, 다중 파일 | +| structure-review/StructureReviewDetailForm | 드래그앤드롭 영역 + 파일 목록 | 10MB 제한, 다중 파일 (동일 패턴) | + +**공통 요소:** +- 드래그앤드롭 영역 (점선 박스) +- 클릭하여 파일 선택 +- 드래그 중 시각적 피드백 +- 파일 크기 검증 + +**변형 가능성:** +- 허용 파일 타입 (이미지만 / 문서만 / 전체) +- 단일 vs 다중 파일 +- 최대 파일 크기 +- 최대 파일 개수 + +--- + +### 3. 파일 목록 (File List) +| 발견 위치 | 구성 요소 | 특이사항 | +|-----------|-----------|----------| +| site-management/SiteDetailForm | 파일명 + 크기 + 다운로드/삭제 | view/edit 모드별 다른 액션 | +| structure-review/StructureReviewDetailForm | 파일명 + 크기 + 다운로드/삭제 | 동일 패턴 | + +**공통 요소:** +- 파일 아이콘 +- 파일명 표시 +- 파일 크기 표시 +- 액션 버튼 + +**변형 가능성:** +- view 모드: 다운로드 버튼 +- edit 모드: 삭제(X) 버튼 +- 미리보기 지원 (이미지) +- 업로드 날짜 표시 여부 + +--- + +### 4. 음성 녹음 (Voice Recorder) +| 발견 위치 | 구성 요소 | 특이사항 | +|-----------|-----------|----------| +| site-management/SiteDetailForm | 녹음 버튼 (Textarea 내부) | edit 모드에서만 표시 | + +**공통 요소:** +- 녹음 시작/중지 버튼 +- Textarea와 연동 (STT 결과 입력) + +**변형 가능성:** +- 버튼 위치 (Textarea 내부 / 외부) +- 녹음 시간 제한 +- 녹음 중 시각적 피드백 + +--- + +### 5. 삭제 확인 다이얼로그 (Delete Confirmation Dialog) +| 발견 위치 | 구성 요소 | 특이사항 | +|-----------|-----------|----------| +| structure-review/StructureReviewDetailForm | AlertDialog + 제목 + 설명 + 취소/삭제 버튼 | view 모드에서 삭제 버튼 클릭 시 | + +**공통 요소:** +- AlertDialog 컴포넌트 사용 +- 제목: "[항목명] 삭제" +- 설명: 삭제 확인 메시지 + 되돌릴 수 없음 경고 +- 취소/삭제 버튼 + +**변형 가능성:** +- 항목명 커스터마이징 +- 삭제 후 리다이렉트 경로 +- 추가 경고 메시지 + +--- + +## 추가 예정 + +> Phase 3 마이그레이션 진행하면서 새로운 패턴 발견 시 여기에 추가 + +### 예상 패턴 (확인 필요) +- [ ] 이미지 미리보기 (썸네일) +- [ ] 서명 입력 +- [ ] 날짜/시간 선택 +- [ ] 검색 가능한 Select (Combobox) +- [ ] 태그 입력 +- [ ] 금액 입력 (천단위 콤마) + +--- + +## 공통화 우선순위 (마이그레이션 완료 후 결정) + +| 우선순위 | 패턴 | 사용 빈도 | 복잡도 | +|----------|------|-----------|--------| +| - | 주소 입력 | 1 | 중 | +| - | 파일 업로드 | 2 | 상 | +| - | 파일 목록 | 2 | 중 | +| - | 음성 녹음 | 1 | 상 | +| - | 삭제 확인 다이얼로그 | 1 | 하 | + +> 사용 빈도는 마이그레이션 진행하면서 카운트 + +--- + +## 변경 이력 +- 2025-01-19: 초안 작성, site-management에서 4개 패턴 발견 +- 2025-01-19: structure-review에서 동일 패턴 확인 (파일 업로드/목록), 삭제 확인 다이얼로그 패턴 추가 diff --git a/claudedocs/[IMPL-2026-01-20] IntegratedDetailTemplate-migration-checklist.md b/claudedocs/[IMPL-2026-01-20] IntegratedDetailTemplate-migration-checklist.md new file mode 100644 index 00000000..9303d160 --- /dev/null +++ b/claudedocs/[IMPL-2026-01-20] IntegratedDetailTemplate-migration-checklist.md @@ -0,0 +1,137 @@ +# IntegratedDetailTemplate 마이그레이션 체크리스트 + +## 목표 +- 타이틀/버튼 영역(목록, 상세, 취소, 수정) 공통화 +- 반응형 입력 필드 통합 +- 특수 기능(테이블, 모달, 문서 미리보기 등)은 renderView/renderForm으로 유지 + +## 마이그레이션 패턴 +```typescript +// 1. config 파일 생성 +export const xxxConfig: DetailConfig = { + title: '페이지 타이틀', + description: '설명', + icon: IconComponent, + basePath: '/path/to/list', + fields: [], // renderView/renderForm 사용 시 빈 배열 + gridColumns: 2, + actions: { + showBack: true, + showDelete: true/false, + showEdit: true/false, + // ... labels + }, +}; + +// 2. 컴포넌트에서 IntegratedDetailTemplate 사용 + + onDelete={handleDelete} // Promise<{ success: boolean; error?: string }> + headerActions={customHeaderActions} // 커스텀 버튼 + renderView={() => renderContent()} + renderForm={() => renderContent()} +/> +``` + +--- + +## 적용 현황 + +### ✅ 완료 (Phase 6) + +| No | 카테고리 | 컴포넌트 | 파일 | 특이사항 | +|----|---------|---------|------|----------| +| 1 | 건설/시공 | 협력업체 | PartnerForm.tsx | - | +| 2 | 건설/시공 | 시공관리 | ConstructionDetailClient.tsx | - | +| 3 | 건설/시공 | 기성관리 | ProgressBillingDetailForm.tsx | - | +| 4 | 건설/시공 | 발주관리 | OrderDetailForm.tsx | - | +| 5 | 건설/시공 | 계약관리 | ContractDetailForm.tsx | - | +| 6 | 건설/시공 | 인수인계보고서 | HandoverReportDetailForm.tsx | - | +| 7 | 건설/시공 | 견적관리 | EstimateDetailForm.tsx | - | +| 8 | 건설/시공 | 현장브리핑 | SiteBriefingForm.tsx | - | +| 9 | 건설/시공 | 이슈관리 | IssueDetailForm.tsx | - | +| 10 | 건설/시공 | 입찰관리 | BiddingDetailForm.tsx | - | +| 11 | 영업 | 견적관리(V2) | QuoteRegistrationV2.tsx | hideHeader prop, 자동견적/푸터바 유지 | +| 12 | 영업 | 고객관리(V2) | ClientDetailClientV2.tsx | - | +| 13 | 회계 | 청구관리 | BillDetail.tsx | - | +| 14 | 회계 | 매입관리 | PurchaseDetail.tsx | - | +| 15 | 회계 | 매출관리 | SalesDetail.tsx | - | +| 16 | 회계 | 거래처관리 | VendorDetail.tsx | - | +| 17 | 회계 | 입금관리(V2) | DepositDetailClientV2.tsx | - | +| 18 | 회계 | 출금관리(V2) | WithdrawalDetailClientV2.tsx | - | +| 19 | 생산 | 작업지시 | WorkOrderDetail.tsx | 상태변경버튼, 작업일지 모달 유지 | +| 20 | 품질 | 검수관리 | InspectionDetail.tsx | 성적서 버튼 | +| 21 | 출고 | 출하관리 | ShipmentDetail.tsx | 문서 미리보기 모달, 조건부 수정/삭제 | +| 22 | 기준정보 | 단가관리(V2) | PricingDetailClientV2.tsx | - | +| 23 | 기준정보 | 노무관리(V2) | LaborDetailClientV2.tsx | - | +| 24 | 설정 | 팝업관리(V2) | PopupDetailClientV2.tsx | - | +| 25 | 설정 | 계정관리 | accounts/[id]/page.tsx | - | +| 26 | 설정 | 공정관리 | process-management/[id]/page.tsx | - | +| 27 | 설정 | 게시판관리 | board-management/[id]/page.tsx | - | +| 28 | 인사 | 명함관리 | card-management/[id]/page.tsx | - | +| 29 | 영업 | 수주관리 | OrderSalesDetailView.tsx, OrderSalesDetailEdit.tsx | 문서 모달, 상태별 버튼, 확정/취소 다이얼로그 유지 | +| 30 | 자재 | 입고관리 | ReceivingDetail.tsx | 입고증/입고처리/성공 다이얼로그, 상태별 버튼 | +| 31 | 자재 | 재고현황 | StockStatusDetail.tsx | LOT별 상세 재고 테이블, FIFO 권장 메시지 | +| 32 | 회계 | 악성채권 | BadDebtDetail.tsx | 저장 확인 다이얼로그, 파일 업로드/다운로드 | +| 33 | 회계 | 거래처원장 | VendorLedgerDetail.tsx | 기간선택, PDF 다운로드, 판매/수금 테이블 | +| 34 | 건설 | 구조검토 | StructureReviewDetailForm.tsx | view/edit/new 모드, 파일 드래그앤드롭 | +| 35 | 건설 | 현장관리 | SiteDetailForm.tsx | 다음 우편번호 API, 파일 드래그앤드롭 | +| 36 | 건설 | 품목관리 | ItemDetailClient.tsx | view/edit/new 모드, 동적 발주 항목 리스트 | +| 37 | 고객센터 | 문의관리 | InquiryDetail.tsx | 댓글 CRUD, 작성자/상태별 버튼 표시 | +| 38 | 고객센터 | 이벤트관리 | EventDetail.tsx | view 모드만 | +| 39 | 고객센터 | 공지관리 | NoticeDetail.tsx | view 모드만, 이미지/첨부파일 | +| 40 | 인사 | 직원관리 | EmployeeDetail.tsx | 기본정보/인사정보/사용자정보 카드 | +| 41 | 설정 | 권한관리 | PermissionDetail.tsx | 인라인 수정, 메뉴별 권한 테이블, 자동 저장 | + +--- + +## Config 파일 위치 + +| 컴포넌트 | Config 파일 | +|---------|------------| +| 출하관리 | shipmentConfig.ts | +| 작업지시 | workOrderConfig.ts | +| 검수관리 | inspectionConfig.ts | +| 견적관리(V2) | quoteConfig.ts | +| 수주관리 | orderSalesConfig.ts | +| 입고관리 | receivingConfig.ts | +| 재고현황 | stockStatusConfig.ts | +| 악성채권 | badDebtConfig.ts | +| 거래처원장 | vendorLedgerConfig.ts | +| 구조검토 | structureReviewConfig.ts | +| 현장관리 | siteConfig.ts | +| 품목관리 | itemConfig.ts | +| 문의관리 | inquiryConfig.ts | +| 이벤트관리 | eventConfig.ts | +| 공지관리 | noticeConfig.ts | +| 직원관리 | employeeConfig.ts | +| 권한관리 | permissionConfig.ts | + +--- + +## 작업 로그 + +### 2026-01-20 +- Phase 6 마이그레이션 시작 +- 검수관리, 작업지시, 출하관리 완료 +- 견적관리(V2 테스트) 완료 - hideHeader 패턴 적용 +- 수주관리 완료 - OrderSalesDetailView.tsx, OrderSalesDetailEdit.tsx 마이그레이션 +- 입고관리 완료 - ReceivingDetail.tsx 마이그레이션 +- 재고현황 완료 - StockStatusDetail.tsx 마이그레이션 (LOT 테이블, FIFO 권장 메시지) +- 악성채권 완료 - BadDebtDetail.tsx 마이그레이션 (저장 확인 다이얼로그, 파일 업로드/다운로드) +- 거래처원장 완료 - VendorLedgerDetail.tsx 마이그레이션 (기간선택, PDF 다운로드, 판매/수금 테이블) +- 구조검토 완료 - StructureReviewDetailForm.tsx 마이그레이션 (view/edit/new 모드, 파일 드래그앤드롭) +- 현장관리 완료 - SiteDetailForm.tsx 마이그레이션 (다음 우편번호 API, 파일 드래그앤드롭) +- 품목관리 완료 - ItemDetailClient.tsx 마이그레이션 (view/edit/new 모드, 동적 발주 항목 리스트) +- 프로젝트관리 제외 - 칸반보드 형태라 IntegratedDetailTemplate 대상 아님 +- 문의관리 완료 - InquiryDetail.tsx 마이그레이션 (댓글 CRUD, 작성자/상태별 버튼 표시) +- 이벤트관리 완료 - EventDetail.tsx 마이그레이션 (view 모드만) +- 공지관리 완료 - NoticeDetail.tsx 마이그레이션 (view 모드만, 이미지/첨부파일) +- 직원관리 완료 - EmployeeDetail.tsx 마이그레이션 (기본정보/인사정보/사용자정보 카드) +- 권한관리 완료 - PermissionDetail.tsx 마이그레이션 (인라인 수정, 메뉴별 권한 테이블, 자동 저장, AlertDialog 유지) +- **Phase 6 마이그레이션 완료** - 총 41개 컴포넌트 마이그레이션 완료 diff --git a/claudedocs/[IMPL] integrated-detail-template-checklist.md b/claudedocs/[IMPL] integrated-detail-template-checklist.md index 5560ab6f..4e59405f 100644 --- a/claudedocs/[IMPL] integrated-detail-template-checklist.md +++ b/claudedocs/[IMPL] integrated-detail-template-checklist.md @@ -1,674 +1,475 @@ -# IntegratedDetailTemplate 구현 체크리스트 +# V2 통합 마이그레이션 현황 > 브랜치: `feature/universal-detail-component` -> 작성일: 2026-01-17 -> 최종 수정: 2026-01-17 (v2 - 심층 검토 반영) +> 최종 수정: 2026-01-20 (v28 - 폼 템플릿 공통화 추가) --- -## ⚠️ 예외 처리 프로세스 +## 📊 전체 진행 현황 -### 예외 상황 발생 시 절차 - -마이그레이션 중 예외 상황 발생 시 다음 절차를 따릅니다: - -1. **즉시 중단**: 현재 모듈 작업 중단 -2. **문서화**: 아래 예외 기록 섹션에 상황 기록 -3. **분류**: 예외 유형 판단 - - **Type A**: 템플릿 수정으로 해결 가능 → Phase 0 보완 - - **Type B**: 해당 모듈만 특수 처리 필요 → `renderView/renderForm` 사용 - - **Type C**: 완전 제외 필요 → 제외 목록에 추가 -4. **계획 수정**: 체크리스트 업데이트 -5. **재개**: 다음 모듈 또는 수정된 계획으로 진행 - -### 예외 기록 - -| 날짜 | 모듈 | 예외 유형 | 상황 설명 | 조치 | -|------|------|----------|----------|------| -| - | - | - | (예외 발생 시 여기에 기록) | - | - -### 알려진 특수 케이스 - -| 모듈 | 특수 요소 | 처리 방안 | -|------|----------|----------| -| popup-management | RichTextEditor | Phase 0.3에서 richtext 타입 지원 후 진행 | -| process-management | RuleModal (분류규칙 배열) | renderForm으로 기존 컴포넌트 유지 또는 Phase 후반 | -| quote-management | QuoteDocument 모달 | renderView로 문서 모달 유지 | -| order-management-sales | OrderDocumentModal | renderView로 문서 모달 유지 | -| work-orders | WorkOrderDetail (카드 레이아웃) | renderView로 기존 상세 유지 | -| items (품목관리) | DynamicItemForm | **완전 제외** (동적 폼) | +| 단계 | 내용 | 상태 | 대상 | +|------|------|------|------| +| **Phase 1-5** | V2 URL 패턴 통합 | ✅ 완료 | 37개 | +| **Phase 6** | 폼 템플릿 공통화 | 🔄 진행중 | 37개 | --- -## 🛡️ 롤백 전략 - -### 파일 관리 규칙 - -1. **기존 컴포넌트 보존**: 마이그레이션 완료 전까지 삭제 금지 -2. **백업 위치**: `src/components/_legacy/[모듈명]/` -3. **정리 시점**: Phase 완료 후 전체 테스트 통과 시 - -### 롤백 절차 - -```bash -# 문제 발생 시 -1. git stash (현재 작업 저장) -2. _legacy 폴더에서 원본 복원 -3. page.tsx에서 import 경로 원복 -4. 문제 분석 후 예외 기록 -``` - -### Git 브랜치 전략 +## 📌 V2 URL 패턴이란? ``` -feature/universal-detail-component (메인 작업 브랜치) -├── phase-0-template (템플릿 구현) -├── phase-1-prototype (프로토타입 3개) -├── phase-2-settings (설정 모듈) -└── ... (Phase별 서브 브랜치) +기존: /[id] (조회) + /[id]/edit (수정) → 별도 페이지 +V2: /[id]?mode=view (조회) + /[id]?mode=edit (수정) → 단일 페이지 ``` -- Phase 완료 시 메인 브랜치로 머지 -- 문제 시 해당 Phase 브랜치만 롤백 +**핵심**: `searchParams.get('mode')` 로 view/edit 분기 --- -## Phase 0 사전 준비: 분석 및 설계 +## 📊 최종 현황 표 -### 0.0.1 필드 타입 인벤토리 +### 통계 요약 -> 템플릿 설계 전 사용되는 모든 필드 타입 파악 +| 구분 | 개수 | +|------|------| +| ✅ V2 완료 | 37개 | +| ❌ 제외 (복잡 구조) | 2개 | +| ⚪ 불필요 (View only 등) | 8개 | +| **합계** | **47개** | -**기본 타입 (Phase 0.1 지원)** -- [ ] `text`: 일반 텍스트 입력 -- [ ] `number`: 숫자 입력 -- [ ] `select`: 드롭다운 선택 -- [ ] `date`: 날짜 선택 -- [ ] `textarea`: 여러 줄 텍스트 +--- -**확장 타입 (Phase 0.2 지원)** -- [ ] `radio`: 라디오 버튼 그룹 -- [ ] `checkbox`: 체크박스 -- [ ] `password`: 비밀번호 입력 -- [ ] `email`: 이메일 입력 -- [ ] `tel`: 전화번호 입력 +### 🏦 회계 (Accounting) - 8개 -**복합 타입 (Phase 0.3 지원)** -- [ ] `dateRange`: 시작일~종료일 -- [ ] `richtext`: RichTextEditor (HTML) -- [ ] `file`: 파일 업로드 -- [ ] `custom`: 커스텀 렌더러 +| 페이지 | 경로 | V2 상태 | 비고 | +|--------|------|---------|------| +| 입금 | `/accounting/deposits/[id]` | ✅ 완료 | Phase 5 | +| 출금 | `/accounting/withdrawals/[id]` | ✅ 완료 | Phase 5 | +| 거래처 | `/accounting/vendors/[id]` | ✅ 완료 | 기존 V2 | +| 매출 | `/accounting/sales/[id]` | ✅ 완료 | 기존 V2 | +| 매입 | `/accounting/purchase/[id]` | ✅ 완료 | 기존 V2 | +| 세금계산서 | `/accounting/bills/[id]` | ✅ 완료 | 기존 V2 | +| 대손추심 | `/accounting/bad-debt-collection/[id]` | ✅ 완료 | Phase 3 | +| 거래처원장 | `/accounting/vendor-ledger/[id]` | ⚪ 불필요 | 조회 전용 탭 | -**특수 타입 (renderForm으로 처리)** -- [ ] `arrayModal`: 배열 데이터 + 모달 편집 (RuleModal 등) -- [ ] `nestedForm`: 중첩 폼 구조 -- [ ] `dynamicFields`: 동적 필드 생성 +--- -### 0.0.2 템플릿 인터페이스 설계 +### 🏗️ 건설 (Construction) - 16개 -- [ ] `DetailConfig` 인터페이스 정의 -- [ ] `FieldDefinition` 인터페이스 정의 -- [ ] `ActionConfig` 인터페이스 정의 -- [ ] `PermissionConfig` 인터페이스 정의 -- [ ] props 타입 정의 (`IntegratedDetailTemplateProps`) +| 페이지 | 경로 | V2 상태 | 비고 | +|--------|------|---------|------| +| 노무관리 | `/construction/base-info/labor/[id]` | ✅ 완료 | Phase 2 | +| 단가관리 | `/construction/base-info/pricing/[id]` | ✅ 완료 | Phase 5 | +| 품목관리(건설) | `/construction/base-info/items/[id]` | ✅ 완료 | 기존 V2 | +| 현장관리 | `/construction/site-management/[id]` | ✅ 완료 | Phase 3 | +| 실행내역 | `/construction/order/structure-review/[id]` | ✅ 완료 | Phase 3 | +| 입찰관리 | `/construction/project/bidding/[id]` | ✅ 완료 | Phase 3 | +| 이슈관리 | `/construction/project/issue-management/[id]` | ✅ 완료 | Phase 3 | +| 현장설명회 | `/construction/project/bidding/site-briefings/[id]` | ✅ 완료 | Phase 3 | +| 견적서 | `/construction/project/bidding/estimates/[id]` | ✅ 완료 | Phase 3 | +| 협력업체 | `/construction/partners/[id]` | ✅ 완료 | Phase 3 | +| 시공관리 | `/construction/construction-management/[id]` | ✅ 완료 | Phase 3 | +| 기성관리 | `/construction/billing/progress-billing-management/[id]` | ✅ 완료 | Phase 3 | +| 발주관리 | `/construction/order/order-management/[id]` | ✅ 완료 | Phase 4 | +| 계약관리 | `/construction/project/contract/[id]` | ✅ 완료 | Phase 4 | +| 인수인계보고서 | `/construction/project/contract/handover-report/[id]` | ✅ 완료 | Phase 4 | +| 현장종합현황 | `/construction/project/management/[id]` | ❌ 제외 | 칸반 보드 | -```typescript -// 예시 구조 (실제 구현 시 확정) -interface DetailConfig { - title: string; - icon?: LucideIcon; - description?: string; - fields: FieldDefinition[]; - sections?: SectionDefinition[]; // 필드 그룹핑 - actions?: ActionConfig; - permissions?: PermissionConfig; -} - -interface FieldDefinition { - key: string; - label: string; - type: FieldType; - required?: boolean; - disabled?: boolean | ((mode: Mode) => boolean); - options?: Option[]; // for select, radio - placeholder?: string; - validation?: ValidationRule[]; - gridSpan?: 1 | 2 | 3; // 그리드 차지 칸 수 - hideInView?: boolean; - hideInForm?: boolean; +--- + +### 💼 판매 (Sales) - 7개 + +| 페이지 | 경로 | V2 상태 | 비고 | +|--------|------|---------|------| +| 거래처(영업) | `/sales/client-management-sales-admin/[id]` | ✅ 완료 | Phase 3 | +| 견적관리 | `/sales/quote-management/[id]` | ✅ 완료 | Phase 3 | +| 견적(테스트) | `/sales/quote-management/test/[id]` | ✅ 완료 | Phase 3 | +| 판매수주관리 | `/sales/order-management-sales/[id]` | ✅ 완료 | Phase 5 | +| 단가관리 | `/sales/pricing-management/[id]` | ✅ 완료 | Phase 4 | +| 수주관리 | `/sales/order-management/[id]` | ⚪ 불필요 | 복잡 워크플로우 | +| 생산의뢰 | `/sales/production-orders/[id]` | ⚪ 불필요 | 조회 전용 | + +--- + +### 👥 인사 (HR) - 2개 + +| 페이지 | 경로 | V2 상태 | 비고 | +|--------|------|---------|------| +| 카드관리 | `/hr/card-management/[id]` | ✅ 완료 | Phase 1 | +| 사원관리 | `/hr/employee-management/[id]` | ✅ 완료 | Phase 4 | + +--- + +### 🏭 생산 (Production) - 2개 + +| 페이지 | 경로 | V2 상태 | 비고 | +|--------|------|---------|------| +| 작업지시 | `/production/work-orders/[id]` | ✅ 완료 | Phase 4 | +| 스크린생산 | `/production/screen-production/[id]` | ✅ 완료 | Phase 4 | + +--- + +### 🔍 품질 (Quality) - 1개 + +| 페이지 | 경로 | V2 상태 | 비고 | +|--------|------|---------|------| +| 검수관리 | `/quality/inspections/[id]` | ✅ 완료 | Phase 4 | + +--- + +### 📦 출고 (Outbound) - 1개 + +| 페이지 | 경로 | V2 상태 | 비고 | +|--------|------|---------|------| +| 출하관리 | `/outbound/shipments/[id]` | ✅ 완료 | Phase 4 | + +--- + +### 📥 자재 (Material) - 2개 + +| 페이지 | 경로 | V2 상태 | 비고 | +|--------|------|---------|------| +| 재고현황 | `/material/stock-status/[id]` | ⚪ 불필요 | LOT 테이블 조회 | +| 입고관리 | `/material/receiving-management/[id]` | ⚪ 불필요 | 복잡 워크플로우 | + +--- + +### 📞 고객센터 (Customer Center) - 3개 + +| 페이지 | 경로 | V2 상태 | 비고 | +|--------|------|---------|------| +| Q&A | `/customer-center/qna/[id]` | ✅ 완료 | Phase 3 | +| 공지사항 | `/customer-center/notices/[id]` | ⚪ 불필요 | View only | +| 이벤트 | `/customer-center/events/[id]` | ⚪ 불필요 | View only | + +--- + +### 📋 게시판 (Board) - 1개 + +| 페이지 | 경로 | V2 상태 | 비고 | +|--------|------|---------|------| +| 게시판관리 | `/board/board-management/[id]` | ✅ 완료 | Phase 3 | + +--- + +### ⚙️ 설정 (Settings) - 3개 + +| 페이지 | 경로 | V2 상태 | 비고 | +|--------|------|---------|------| +| 계좌관리 | `/settings/accounts/[id]` | ✅ 완료 | Phase 1 | +| 팝업관리 | `/settings/popup-management/[id]` | ✅ 완료 | Phase 3 | +| 권한관리 | `/settings/permissions/[id]` | ❌ 제외 | Matrix UI | + +--- + +### 🔧 기준정보 (Master Data) - 1개 + +| 페이지 | 경로 | V2 상태 | 비고 | +|--------|------|---------|------| +| 공정관리 | `/master-data/process-management/[id]` | ✅ 완료 | Phase 3 | + +--- + +### 📦 품목 (Items) - 1개 + +| 페이지 | 경로 | V2 상태 | 비고 | +|--------|------|---------|------| +| 품목관리 | `/items/[id]` | ✅ 완료 | Phase 5 | + +--- + +## 🔧 V2 마이그레이션 패턴 + +### Pattern A: mode prop 지원 + +기존 컴포넌트가 `mode` prop을 지원하는 경우 + +```tsx +// page.tsx +const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view'; +return ; + +// edit/page.tsx → 리다이렉트 +router.replace(`/path/${id}?mode=edit`); +``` + +### Pattern B: View/Edit 컴포넌트 분리 + +View와 Edit가 완전히 다른 구현인 경우 + +```tsx +// 새 컴포넌트: ComponentDetailView.tsx, ComponentDetailEdit.tsx +const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view'; + +if (mode === 'edit') { + return ; } +return ; ``` -### 0.0.3 API 호출 패턴 결정 +--- -- [ ] 표준 패턴 결정: ~~컴포넌트 내부~~ vs **props 콜백** -- [ ] `onSubmit`, `onDelete` 콜백 인터페이스 정의 -- [ ] 에러 처리 표준화 (toast 사용) -- [ ] 로딩 상태 관리 방식 결정 +## 📚 공통 컴포넌트 참조 -**결정된 표준 패턴**: -```typescript -// page.tsx에서 API 함수 전달 - { - const result = await createSomething(data); - if (result.success) router.push('/list'); - return result; - }} - onDelete={async (id) => { - const result = await deleteSomething(id); - if (result.success) router.push('/list'); - return result; - }} -/> -``` - -### 0.0.4 기존 모듈 복잡도 재분류 - -> Phase 1~2 대상 모듈의 실제 복잡도 검증 - -| 모듈 | 예상 복잡도 | 실제 복잡도 | 특수 요소 | Phase 배치 | -|------|------------|------------|----------|-----------| -| accounts | 단순 | ✅ 단순 | 없음 | Phase 1 | -| card-management | 단순 | ✅ 단순 | 직원 API 조회 | Phase 1 | -| permissions | 단순 | ⬜ 미확인 | - | Phase 1 | -| popup-management | 단순 | ⚠️ 중간 | RichTextEditor | Phase 2 | -| process-management | 단순 | ❌ 복잡 | RuleModal | Phase 4 이후 | -| board-management | 단순 | ⬜ 미확인 | - | Phase 2 | +| 컴포넌트 | 위치 | 용도 | +|----------|------|------| +| IntegratedDetailTemplate | `src/components/templates/IntegratedDetailTemplate/` | 상세 페이지 템플릿 | +| ErrorCard | `src/components/ui/error-card.tsx` | 에러 UI (not-found, network) | +| ServerErrorPage | `src/components/common/ServerErrorPage.tsx` | 서버 에러 페이지 | --- -## Phase 0: 템플릿 기본 구조 구현 +## 📝 변경 이력 -### 0.1 파일 생성 -- [ ] `src/components/templates/IntegratedDetailTemplate/index.tsx` 생성 -- [ ] `src/components/templates/IntegratedDetailTemplate/types.ts` 생성 -- [ ] `src/components/templates/IntegratedDetailTemplate/DetailHeader.tsx` 생성 -- [ ] `src/components/templates/IntegratedDetailTemplate/DetailForm.tsx` 생성 -- [ ] `src/components/templates/IntegratedDetailTemplate/DetailView.tsx` 생성 -- [ ] `src/components/templates/IntegratedDetailTemplate/FieldRenderer.tsx` 생성 -- [ ] `src/components/templates/IntegratedDetailTemplate/GridLayout.tsx` 생성 -- [ ] `src/components/templates/IntegratedDetailTemplate/hooks/useDetailPage.ts` 생성 -- [ ] `src/components/templates/IntegratedDetailTemplate/hooks/useFormHandler.ts` 생성 - -### 0.2 Phase 0.1 - 기본 타입 구현 -- [ ] mode props 처리 (create/edit/view) -- [ ] 헤더 레이아웃 (제목, 아이콘, 뒤로가기 버튼) -- [ ] 액션 버튼 배치 (저장, 취소, 삭제, 수정) -- [ ] 그리드 레이아웃 (2열, 3열 지원) -- [ ] 필드 렌더링: `text` -- [ ] 필드 렌더링: `number` -- [ ] 필드 렌더링: `select` -- [ ] 필드 렌더링: `date` -- [ ] 필드 렌더링: `textarea` -- [ ] 폼 유효성 검사 기본 구조 -- [ ] 로딩 상태 처리 -- [ ] 에러 상태 처리 - -### 0.3 Phase 0.2 - 확장 타입 구현 -- [ ] 필드 렌더링: `radio` -- [ ] 필드 렌더링: `checkbox` -- [ ] 필드 렌더링: `password` -- [ ] 필드 렌더링: `email` -- [ ] 필드 렌더링: `tel` - -### 0.4 Phase 0.3 - 복합 타입 및 커스텀 지원 -- [ ] 필드 렌더링: `dateRange` (시작~종료) -- [ ] 필드 렌더링: `richtext` (RichTextEditor 통합) -- [ ] 필드 렌더링: `file` (파일 업로드) -- [ ] `renderView` props 지원 (커스텀 상세 화면) -- [ ] `renderForm` props 지원 (커스텀 폼) -- [ ] `renderField` props 지원 (개별 필드 커스텀) - -### 0.5 Phase 0 검증 -- [ ] 모든 기본 타입 렌더링 확인 -- [ ] view/create/edit 모드 전환 확인 -- [ ] 반응형 그리드 확인 -- [ ] 유효성 검사 동작 확인 -- [ ] 버튼 배치/스타일 확인 - ---- - -## Phase 1: 프로토타입 (3개 모듈) - 단순 CRUD - -> 목표: 가장 단순한 모듈로 템플릿 검증 - -### 1.1 settings/accounts (계좌관리) ✅ 권장 - -#### 1.1.1 현황 분석 -- [ ] `settings/accounts/new/page.tsx` 분석 -- [ ] `settings/accounts/[id]/page.tsx` 분석 -- [ ] 기존 컴포넌트 구조 파악 (AccountDetail - 통합 모드) -- [ ] 필드 목록 정리: - - [ ] bankCode (select) - 은행 선택 - - [ ] accountNumber (text) - 계좌번호 - - [ ] accountHolder (text) - 예금주 - - [ ] accountPassword (password) - 비밀번호 - - [ ] accountName (text) - 계좌명 - - [ ] status (select) - 상태 -- [ ] API 연결 확인 (actions.ts) -- [ ] **예외 요소 확인**: 없음 ✅ - -#### 1.1.2 config 파일 작성 -- [ ] `src/components/settings/AccountManagement/accountConfig.ts` 작성 -- [ ] 필드 정의 -- [ ] 권한 설정 (canEdit, canDelete) - -#### 1.1.3 페이지 마이그레이션 -- [ ] `new/page.tsx` → IntegratedDetailTemplate (mode="create") -- [ ] `[id]/page.tsx` → IntegratedDetailTemplate (mode="view") -- [ ] 기존 AccountDetail.tsx → `_legacy/` 이동 (삭제 X) - -#### 1.1.4 기능 테스트 -- [ ] **Happy Path**: - - [ ] 등록: 모든 필드 입력 → 저장 → 목록 이동 - - [ ] 조회: 데이터 정상 표시 - - [ ] 수정: 데이터 로드 → 수정 → 저장 - - [ ] 삭제: 확인 다이얼로그 → 삭제 → 목록 이동 -- [ ] **Error Path**: - - [ ] 필수 필드 누락 시 유효성 에러 - - [ ] API 실패 시 에러 토스트 -- [ ] **UI 검증**: - - [ ] 버튼 배치 확인 (좌: 취소/목록, 우: 삭제/수정/저장) - - [ ] 그리드 레이아웃 확인 (2열) - - [ ] 반응형 확인 (모바일 1열) - -#### 1.1.5 정리 -- [ ] import 경로 정리 -- [ ] 불필요한 코드 제거 -- [ ] **예외 발생 시**: 예외 기록 섹션에 문서화 - ---- - -### 1.2 hr/card-management (카드관리) - -#### 1.2.1 현황 분석 -- [ ] `hr/card-management/new/page.tsx` 분석 -- [ ] `hr/card-management/[id]/page.tsx` 분석 -- [ ] `hr/card-management/[id]/edit/page.tsx` 분석 -- [ ] 기존 컴포넌트 구조 파악 (CardForm) -- [ ] 필드 목록 정리: - - [ ] cardCompany (select) - 카드사 - - [ ] cardNumber (text) - 카드번호 (포맷팅) - - [ ] expiryDate (text) - 유효기간 - - [ ] pinPrefix (password) - 비밀번호 앞자리 - - [ ] cardName (text) - 카드명 - - [ ] status (select) - 상태 - - [ ] userId (select) - 사용자 (직원 API 조회) -- [ ] API 연결 확인 (actions.ts) -- [ ] **예외 요소 확인**: - - [ ] 직원 목록 API 호출 필요 → select options 동적 로드 - -#### 1.2.2 config 파일 작성 -- [ ] `cardManagementConfig.ts` 작성 -- [ ] 필드 정의 -- [ ] **동적 options 로드 방식 결정**: - - Option A: config에서 `fetchOptions` 함수 정의 - - Option B: page.tsx에서 미리 로드 후 config에 전달 -- [ ] 권한 설정 - -#### 1.2.3 페이지 마이그레이션 -- [ ] `new/page.tsx` → IntegratedDetailTemplate (mode="create") -- [ ] `[id]/page.tsx` → IntegratedDetailTemplate (mode="view") -- [ ] `[id]/edit/page.tsx` → IntegratedDetailTemplate (mode="edit") -- [ ] 기존 CardForm.tsx → `_legacy/` 이동 - -#### 1.2.4 기능 테스트 -- [ ] **Happy Path**: 등록/조회/수정/삭제 -- [ ] **Error Path**: 유효성/API 에러 -- [ ] **특수 테스트**: - - [ ] 직원 select 동적 로드 확인 - - [ ] 카드번호 포맷팅 확인 -- [ ] **UI 검증**: 버튼/그리드/반응형 - -#### 1.2.5 정리 -- [ ] import 경로 정리 -- [ ] **예외 발생 시**: 문서화 - ---- - -### 1.3 settings/permissions (권한관리) - -#### 1.3.1 현황 분석 -- [ ] `settings/permissions/new/page.tsx` 분석 -- [ ] `settings/permissions/[id]/page.tsx` 분석 -- [ ] 기존 컴포넌트 구조 파악 -- [ ] 필드 목록 정리 -- [ ] API 연결 확인 -- [ ] **예외 요소 확인**: (분석 후 기록) - -#### 1.3.2 config 파일 작성 -- [ ] `permissionsConfig.ts` 작성 -- [ ] 필드 정의 -- [ ] 권한 설정 - -#### 1.3.3 페이지 마이그레이션 -- [ ] `new/page.tsx` → IntegratedDetailTemplate (mode="create") -- [ ] `[id]/page.tsx` → IntegratedDetailTemplate (mode="view") -- [ ] 기존 컴포넌트 → `_legacy/` 이동 - -#### 1.3.4 기능 테스트 -- [ ] Happy Path / Error Path / UI 검증 - -#### 1.3.5 정리 -- [ ] import 경로 정리 -- [ ] **예외 발생 시**: 문서화 - ---- - -### 1.4 Phase 1 완료 검증 - -- [ ] 3개 모듈 모두 정상 동작 확인 -- [ ] 발견된 예외 사항 정리 -- [ ] 템플릿 구조 검토 및 개선점 도출 -- [ ] **Phase 0 보완 필요 여부 판단**: - - [ ] 새로운 필드 타입 필요? - - [ ] 인터페이스 수정 필요? -- [ ] 공통 패턴 확인 및 리팩토링 -- [ ] Phase 2 진행 여부 결정 - ---- - -## Phase 2: 설정/게시판 모듈 (4개) - RichTextEditor 포함 - -> Phase 0.3 (richtext 지원) 완료 후 진행 - -### 2.1 settings/popup-management (팝업관리) ⚠️ RichTextEditor - -#### 2.1.1 현황 분석 -- [ ] 기존 컴포넌트 분석 (PopupForm + PopupDetail 분리 구조) -- [ ] 필드 목록 정리: - - [ ] target (select) - 대상 - - [ ] startDate, endDate (dateRange) - 기간 - - [ ] title (text) - 제목 - - [ ] content (richtext) - 내용 ⚠️ - - [ ] status (radio) - 상태 - - [ ] author (text, readonly) - 작성자 -- [ ] **예외 요소**: RichTextEditor → Phase 0.3 richtext 타입 사용 - -#### 2.1.2 config 파일 작성 -- [ ] `popupManagementConfig.ts` 작성 - -#### 2.1.3 페이지 마이그레이션 -- [ ] `new/page.tsx` 마이그레이션 -- [ ] `[id]/page.tsx` 마이그레이션 -- [ ] `[id]/edit/page.tsx` 마이그레이션 - -#### 2.1.4 기능 테스트 -- [ ] 특수: RichTextEditor 동작 확인 -- [ ] 특수: dateRange 동작 확인 - -#### 2.1.5 정리 - ---- - -### 2.2 board/board-management (게시판관리) -- [ ] 현황 분석 -- [ ] config 작성 -- [ ] 페이지 마이그레이션 (new, [id], [id]/edit) -- [ ] 기능 테스트 -- [ ] 정리 - -### 2.3 accounting/bad-debt-collection (악성채권추심) -- [ ] 현황 분석 -- [ ] config 작성 -- [ ] 페이지 마이그레이션 (new, [id], [id]/edit) -- [ ] 기능 테스트 -- [ ] 정리 - -### 2.4 hr/documents (증명서류) -- [ ] 현황 분석 -- [ ] config 작성 -- [ ] 페이지 마이그레이션 -- [ ] 기능 테스트 -- [ ] 정리 - -### 2.5 Phase 2 완료 검증 -- [ ] 4개 모듈 모두 정상 동작 확인 -- [ ] richtext, dateRange 필드 동작 확인 -- [ ] 예외 사항 정리 -- [ ] Phase 3 진행 여부 결정 - ---- - -## Phase 3: HR/판매 모듈 (6개) - 특수 상세 화면 포함 - -### 3.1 hr/employee-management (사원관리) -- [ ] 현황 분석 -- [ ] config 작성 -- [ ] 페이지 마이그레이션 (new, [id], [id]/edit) -- [ ] 기능 테스트 -- [ ] 정리 - -### 3.2 sales/client-management-sales-admin (거래처관리) -- [ ] 현황 분석 -- [ ] config 작성 -- [ ] 페이지 마이그레이션 (new, [id], [id]/edit) -- [ ] 기능 테스트 -- [ ] 정리 - -### 3.3 sales/quote-management (견적관리) ⚠️ 문서 모달 - -#### 특수 처리 -- [ ] `[id]/page.tsx`: `renderView` prop으로 QuoteDocument 모달 유지 -- [ ] 등록/수정은 일반 템플릿 적용 - -- [ ] 현황 분석 -- [ ] config 작성 (renderView 포함) -- [ ] 페이지 마이그레이션 -- [ ] 기능 테스트 (문서 모달 동작 확인) -- [ ] 정리 - -### 3.4 sales/order-management-sales (수주관리) ⚠️ 문서 모달 - -#### 특수 처리 -- [ ] `[id]/page.tsx`: `renderView` prop으로 OrderDocumentModal 유지 - -- [ ] 현황 분석 -- [ ] config 작성 (renderView 포함) -- [ ] 페이지 마이그레이션 -- [ ] 기능 테스트 -- [ ] 정리 - -### 3.5 sales/pricing-management (단가관리) -- [ ] 현황 분석 -- [ ] config 작성 -- [ ] 페이지 마이그레이션 (new, [id], [id]/edit) -- [ ] 기능 테스트 -- [ ] 정리 - -### 3.6 customer-center/qna (Q&A) -- [ ] 현황 분석 -- [ ] config 작성 -- [ ] 페이지 마이그레이션 (new, [id], [id]/edit) -- [ ] 기능 테스트 -- [ ] 정리 - -### 3.7 Phase 3 완료 검증 -- [ ] 6개 모듈 모두 정상 동작 확인 -- [ ] **renderView 동작 확인** (문서 모달) -- [ ] 예외 사항 정리 -- [ ] Phase 4 진행 여부 결정 - ---- - -## Phase 4: 생산/출고/품질 모듈 (4개) - 복잡한 상세 화면 - -### 4.1 production/work-orders (작업지시관리) ⚠️ 카드 레이아웃 - -#### 특수 처리 -- [ ] `[id]/page.tsx`: `renderView` prop으로 WorkOrderDetail (카드 레이아웃) 유지 - -- [ ] 현황 분석 -- [ ] config 작성 (renderView 포함) -- [ ] 페이지 마이그레이션 -- [ ] 기능 테스트 -- [ ] 정리 - -### 4.2 production/screen-production (스크린생산) ⚠️ 복합 레이아웃 - -#### 특수 처리 -- [ ] `[id]/page.tsx`: `renderView` prop으로 ItemDetailClient 유지 - -- [ ] 현황 분석 -- [ ] config 작성 (renderView 포함) -- [ ] 페이지 마이그레이션 -- [ ] 기능 테스트 -- [ ] 정리 - -### 4.3 outbound/shipments (출하관리) -- [ ] 현황 분석 -- [ ] config 작성 -- [ ] 페이지 마이그레이션 (new, [id], [id]/edit) -- [ ] 기능 테스트 -- [ ] 정리 - -### 4.4 quality/inspections (검사관리) -- [ ] 현황 분석 -- [ ] config 작성 -- [ ] 페이지 마이그레이션 (new, [id], [id]/edit) -- [ ] 기능 테스트 -- [ ] 정리 - -### 4.5 master-data/process-management (공정관리) ⚠️ RuleModal - -#### 특수 처리 -- [ ] **복잡도 재평가**: RuleModal (분류규칙 배열 관리) -- [ ] Option A: `renderForm`으로 기존 ProcessForm 유지 -- [ ] Option B: 배열 필드 타입 추가 후 마이그레이션 -- [ ] **결정 후 진행** - -- [ ] 현황 분석 -- [ ] 처리 방안 결정 -- [ ] config 또는 renderForm 작성 -- [ ] 페이지 마이그레이션 -- [ ] 기능 테스트 -- [ ] 정리 - -### 4.6 Phase 4 완료 검증 -- [ ] 모듈 정상 동작 확인 -- [ ] 복잡한 renderView 동작 확인 -- [ ] 예외 사항 정리 -- [ ] Phase 5 진행 여부 결정 - ---- - -## Phase 5: 건설 모듈 (6개) - -### 5.1 construction/project/bidding/partners (거래처-입찰) -- [ ] 현황 분석 / config 작성 / 마이그레이션 / 테스트 / 정리 - -### 5.2 construction/project/bidding/site-briefings (현장설명회) -- [ ] 현황 분석 / config 작성 / 마이그레이션 / 테스트 / 정리 - -### 5.3 construction/project/contract (계약관리) -- [ ] 현황 분석 / config 작성 / 마이그레이션 / 테스트 / 정리 - -### 5.4 construction/project/issue-management (이슈관리) -- [ ] 현황 분석 / config 작성 / 마이그레이션 / 테스트 / 정리 - -### 5.5 construction/order/base-info/pricing (단가-건설) -- [ ] 현황 분석 / config 작성 / 마이그레이션 / 테스트 / 정리 - -### 5.6 construction/order/base-info/labor (노임-건설) -- [ ] 현황 분석 / config 작성 / 마이그레이션 / 테스트 / 정리 - -### 5.7 Phase 5 완료 검증 -- [ ] 6개 모듈 모두 정상 동작 확인 -- [ ] 예외 사항 정리 -- [ ] Phase 6 진행 여부 결정 - ---- - -## Phase 6: 나머지 패턴 (B, C, D) - -### 6.1 패턴 B (등록+상세, mode로 수정) -> accounting/bills, accounting/sales, accounting/vendors - -- [ ] accounting/bills 마이그레이션 -- [ ] accounting/sales 마이그레이션 -- [ ] accounting/vendors 마이그레이션 -- [ ] 기능 테스트 - -### 6.2 패턴 C (상세+수정, 등록 없음) -> construction 모듈 대부분 - -- [ ] construction/billing/progress-billing-management -- [ ] construction/order/order-management -- [ ] construction/order/site-management -- [ ] construction/order/structure-review -- [ ] construction/project/bidding -- [ ] construction/project/bidding/estimates -- [ ] construction/project/construction-management -- [ ] construction/project/contract/handover-report -- [ ] sales/quote-management/test -- [ ] 기능 테스트 - -### 6.3 패턴 D (상세만, 조회 전용) -> 10개 모듈 - -- [ ] accounting/deposits -- [ ] accounting/purchase -- [ ] accounting/vendor-ledger -- [ ] accounting/withdrawals -- [ ] customer-center/notices -- [ ] customer-center/events -- [ ] material/receiving-management -- [ ] material/stock-status -- [ ] construction/project/management -- [ ] sales/order-management-sales/production-orders -- [ ] 기능 테스트 - -### 6.4 Phase 6 완료 검증 -- [ ] 모든 패턴 정상 동작 확인 -- [ ] 예외 사항 최종 정리 - ---- - -## 🚫 제외 대상 - -| 모듈 | 제외 사유 | -|------|----------| -| items (품목관리) 3개 페이지 | DynamicItemForm 사용 (동적 폼) | -| (추가 발견 시 기록) | - | - ---- - -## 최종 검증 - -- [ ] 전체 마이그레이션 완료 페이지 수 확인 -- [ ] 제외 대상 확인 및 문서화 -- [ ] 전체 기능 회귀 테스트 -- [ ] 성능 테스트 (로딩 속도) -- [ ] 반응형 레이아웃 전체 확인 -- [ ] `_legacy` 폴더 정리 (최종 삭제) -- [ ] 예외 기록 최종 검토 - ---- - -## 통계 - -| Phase | 모듈 수 | 예상 페이지 수 | 상태 | -|-------|--------|--------------|------| -| Phase 0 사전준비 | - | - | ⬜ 대기 | -| Phase 0 템플릿 | - | - | ⬜ 대기 | -| Phase 1 | 3 | 7 | ⬜ 대기 | -| Phase 2 | 4 | 12 | ⬜ 대기 | -| Phase 3 | 6 | 18 | ⬜ 대기 | -| Phase 4 | 5 | 15 | ⬜ 대기 | -| Phase 5 | 6 | 16 | ⬜ 대기 | -| Phase 6 | 22 | 38 | ⬜ 대기 | -| **합계** | **46** | **~106** | - | - -> 제외: 품목관리(items) 3개 페이지 (동적 폼) - ---- - -## 변경 이력 +
+전체 변경 이력 보기 (v1 ~ v27) | 날짜 | 버전 | 내용 | |------|------|------| | 2026-01-17 | v1 | 체크리스트 초기 작성 | -| 2026-01-17 | v2 | 심층 검토 반영 - Phase 1 모듈 변경, 예외 처리 프로세스 추가, 롤백 전략 추가, Phase 0 사전준비 추가, 필드 타입 인벤토리 추가 | +| 2026-01-17 | v2 | 심층 검토 반영 | +| 2026-01-19 | v3 | 내부 컴포넌트 공통화 통합 | +| 2026-01-19 | v4 | 스켈레톤 컴포넌트 추가 | +| 2026-01-19 | v5 | Chrome DevTools 동작 검증 완료 | +| 2026-01-19 | v6 | DetailField 미적용 이슈 발견 | +| 2026-01-19 | v7 | DetailField 미적용 이슈 해결 완료 | +| 2026-01-19 | v8 | 📊 47개 상세 페이지 전체 분석 완료 | +| 2026-01-19 | v9 | 📋 리스트/상세 차이 설명 추가, 🧪 기능 검수 섹션 추가 | +| 2026-01-19 | v10 | 🔧 buttonPosition prop 추가 | +| 2026-01-19 | v11 | 🚀 노무관리 마이그레이션 완료 | +| 2026-01-19 | v12 | 🚀 단가관리(건설) 마이그레이션 완료 | +| 2026-01-19 | v13 | 🚀 입금관리 마이그레이션 완료 | +| 2026-01-19 | v14 | 📊 Phase 2 분석 및 대규모 재분류 | +| 2026-01-19 | v15 | ✅ Phase 2 최종 완료 | +| 2026-01-19 | v16 | 🚀 Phase 3 라우팅 구조 변경 4개 완료, 🎨 ErrorCard 추가 | +| 2026-01-19 | v17 | 🚀 Phase 3 대손추심 완료 | +| 2026-01-19 | v18 | 🚀 Phase 3 Q&A 완료 | +| 2026-01-19 | v19 | 🚀 Phase 3 건설/판매 도메인 3개 추가 완료 | +| 2026-01-19 | v20 | 🧪 견적 테스트 페이지 V2 패턴 적용 | +| 2026-01-19 | v21 | 🚀 Phase 3 건설 도메인 4개 추가 완료 | +| 2026-01-19 | v22 | 🚨 ServerErrorPage 필수 적용 섹션 추가 | +| 2026-01-19 | v23 | 🚀 기성관리 V2 마이그레이션 완료 | +| 2026-01-19 | v24 | 📊 Phase 3 최종 분석 완료 | +| 2026-01-19 | v25 | 🚀 Phase 4 추가 (9개 페이지 식별) | +| 2026-01-19 | v26 | 🎯 Phase 5 완료 (5개 V2 URL 패턴 통합) | +| 2026-01-20 | v27 | 📋 문서 정리 - 최종 현황 표 중심으로 재구성 | +| 2026-01-20 | v28 | 🎨 Phase 6 폼 템플릿 공통화 마이그레이션 추가 | + +
+ +--- + +## 🎨 Phase 6: 폼 템플릿 공통화 마이그레이션 + +### 목표 + +모든 등록/상세/수정 페이지를 공통 템플릿 기반으로 통합하여 **한 파일 수정으로 전체 페이지 일괄 적용** 가능하게 함. + +### 공통화 대상 + +| 항목 | 컴포넌트 | 효과 | +|------|----------|------| +| 페이지 레이아웃 | `ResponsiveFormTemplate` | 헤더/버튼 위치 일괄 변경 | +| 입력 필드 그리드 | `FormFieldGrid` | PC 4열/모바일 1열 등 반응형 일괄 변경 | +| 입력 필드 스타일 | `FormField` | 라벨/에러/스타일 일괄 변경 | +| 하단 버튼 | `FormActions` | 저장/취소 버튼 sticky 고정 | + +### 사용법 + +```tsx +import { + ResponsiveFormTemplate, + FormSection, + FormFieldGrid, + FormField +} from '@/components/templates/ResponsiveFormTemplate'; + +export default function ExampleEditPage() { + return ( + + + + + + + + + + + ); +} +``` + +### 반응형 그리드 설정 + +```tsx +// FormFieldGrid.tsx - 이 파일만 수정하면 전체 적용 +const gridClasses = { + 1: "grid-cols-1", + 2: "grid-cols-1 md:grid-cols-2", + 3: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3", + 4: "grid-cols-1 md:grid-cols-2 lg:grid-cols-4", +}; +``` + +--- + +### Phase 6 체크리스트 + +#### 🏦 회계 (Accounting) - 7개 + +| 페이지 | 경로 | 폼 공통화 | +|--------|------|----------| +| 입금 | `/accounting/deposits/[id]` | ✅ 완료 | +| 출금 | `/accounting/withdrawals/[id]` | ✅ 완료 | +| 거래처 | `/accounting/vendors/[id]` | ✅ 완료 | +| 매출 | `/accounting/sales/[id]` | ✅ 완료 | +| 매입 | `/accounting/purchase/[id]` | ✅ 완료 | +| 세금계산서 | `/accounting/bills/[id]` | ✅ 완료 | +| 대손추심 | `/accounting/bad-debt-collection/[id]` | 🔶 복잡 | + +#### 🏗️ 건설 (Construction) - 15개 + +| 페이지 | 경로 | 폼 공통화 | +|--------|------|----------| +| 노무관리 | `/construction/base-info/labor/[id]` | ✅ 완료 | +| 단가관리 | `/construction/base-info/pricing/[id]` | ✅ 완료 | +| 품목관리(건설) | `/construction/base-info/items/[id]` | 🔶 복잡 | +| 현장관리 | `/construction/site-management/[id]` | 🔶 복잡 | +| 실행내역 | `/construction/order/structure-review/[id]` | 🔶 복잡 | +| 입찰관리 | `/construction/project/bidding/[id]` | ✅ 완료 | +| 이슈관리 | `/construction/project/issue-management/[id]` | ✅ 완료 | +| 현장설명회 | `/construction/project/bidding/site-briefings/[id]` | ✅ 완료 | +| 견적서 | `/construction/project/bidding/estimates/[id]` | ✅ 완료 | +| 협력업체 | `/construction/partners/[id]` | ✅ 완료 | +| 시공관리 | `/construction/construction-management/[id]` | ✅ 완료 | +| 기성관리 | `/construction/billing/progress-billing-management/[id]` | ✅ 완료 | +| 발주관리 | `/construction/order/order-management/[id]` | ⬜ 대기 | +| 계약관리 | `/construction/project/contract/[id]` | ⬜ 대기 | +| 인수인계보고서 | `/construction/project/contract/handover-report/[id]` | ⬜ 대기 | + +#### 💼 판매 (Sales) - 5개 + +| 페이지 | 경로 | 폼 공통화 | +|--------|------|----------| +| 거래처(영업) | `/sales/client-management-sales-admin/[id]` | ✅ 완료 | +| 견적관리 | `/sales/quote-management/[id]` | ⬜ 대기 | +| 견적(테스트) | `/sales/quote-management/test/[id]` | ⬜ 대기 | +| 판매수주관리 | `/sales/order-management-sales/[id]` | ⬜ 대기 | +| 단가관리 | `/sales/pricing-management/[id]` | ⬜ 대기 | + +#### 👥 인사 (HR) - 2개 + +| 페이지 | 경로 | 폼 공통화 | +|--------|------|----------| +| 카드관리 | `/hr/card-management/[id]` | ✅ 완료 | +| 사원관리 | `/hr/employee-management/[id]` | 🔶 복잡 | + +#### 🏭 생산 (Production) - 2개 + +| 페이지 | 경로 | 폼 공통화 | +|--------|------|----------| +| 작업지시 | `/production/work-orders/[id]` | ⬜ 대기 | +| 스크린생산 | `/production/screen-production/[id]` | ⬜ 대기 | + +#### 🔍 품질 (Quality) - 1개 + +| 페이지 | 경로 | 폼 공통화 | +|--------|------|----------| +| 검수관리 | `/quality/inspections/[id]` | ⬜ 대기 | + +#### 📦 출고 (Outbound) - 1개 + +| 페이지 | 경로 | 폼 공통화 | +|--------|------|----------| +| 출하관리 | `/outbound/shipments/[id]` | ⬜ 대기 | + +#### 📞 고객센터 (Customer Center) - 1개 + +| 페이지 | 경로 | 폼 공통화 | +|--------|------|----------| +| Q&A | `/customer-center/qna/[id]` | 🔶 복잡 | + +#### 📋 게시판 (Board) - 1개 + +| 페이지 | 경로 | 폼 공통화 | +|--------|------|----------| +| 게시판관리 | `/board/board-management/[id]` | 🔶 복잡 | + +#### ⚙️ 설정 (Settings) - 2개 + +| 페이지 | 경로 | 폼 공통화 | +|--------|------|----------| +| 계좌관리 | `/settings/accounts/[id]` | ✅ 완료 | +| 팝업관리 | `/settings/popup-management/[id]` | ✅ 완료 | + +#### 🔧 기준정보 (Master Data) - 1개 + +| 페이지 | 경로 | 폼 공통화 | +|--------|------|----------| +| 공정관리 | `/master-data/process-management/[id]` | 🔶 복잡 | + +--- + +### Phase 6 통계 + +| 구분 | 개수 | +|------|------| +| ✅ IntegratedDetailTemplate 적용 완료 | 19개 | +| 🔶 하위 컴포넌트 위임 (복잡 로직) | 8개 | +| ⬜ 개별 구현 (마이그레이션 대기) | 10개 | +| **합계** | **37개** | + +--- + +### ✅ IntegratedDetailTemplate 적용 완료 (19개) + +config 기반 템플릿으로 완전 마이그레이션 완료된 페이지 + +| 페이지 | 경로 | 컴포넌트 | +|--------|------|----------| +| 입금 | `/accounting/deposits/[id]` | DepositDetailClientV2 | +| 출금 | `/accounting/withdrawals/[id]` | WithdrawalDetailClientV2 | +| 팝업관리 | `/settings/popup-management/[id]` | PopupDetailClientV2 | +| 거래처(영업) | `/sales/client-management-sales-admin/[id]` | ClientDetailClientV2 | +| 노무관리 | `/construction/order/base-info/labor/[id]` | LaborDetailClientV2 | +| 단가관리 | `/construction/order/base-info/pricing/[id]` | PricingDetailClientV2 | +| 계좌관리 | `/settings/accounts/[id]` | accountConfig + IntegratedDetailTemplate | +| 카드관리 | `/hr/card-management/[id]` | cardConfig + IntegratedDetailTemplate | +| 거래처 | `/accounting/vendors/[id]` | vendorConfig + IntegratedDetailTemplate | +| 매출 | `/accounting/sales/[id]` | salesConfig + IntegratedDetailTemplate | +| 매입 | `/accounting/purchase/[id]` | purchaseConfig + IntegratedDetailTemplate | +| 세금계산서 | `/accounting/bills/[id]` | billConfig + IntegratedDetailTemplate | +| 입찰관리 | `/construction/project/bidding/[id]` | biddingConfig + IntegratedDetailTemplate | +| 이슈관리 | `/construction/project/issue-management/[id]` | issueConfig + IntegratedDetailTemplate | +| 현장설명회 | `/construction/project/bidding/site-briefings/[id]` | siteBriefingConfig + IntegratedDetailTemplate | +| 견적서 | `/construction/project/bidding/estimates/[id]` | estimateConfig + IntegratedDetailTemplate | +| 협력업체 | `/construction/partners/[id]` | partnerConfig + IntegratedDetailTemplate | +| 시공관리 | `/construction/construction-management/[id]` | constructionConfig + IntegratedDetailTemplate | +| 기성관리 | `/construction/billing/progress-billing-management/[id]` | progressBillingConfig + IntegratedDetailTemplate | + +--- + +### 🔶 하위 컴포넌트 위임 패턴 (8개) + +복잡한 커스텀 로직으로 IntegratedDetailTemplate 적용 검토 필요 + +| 페이지 | 경로 | 복잡도 이유 | +|--------|------|-------------| +| 대손추심 | `/accounting/bad-debt-collection/[id]` | 파일업로드, 메모, 우편번호 | +| 게시판관리 | `/board/board-management/[id]` | 하위 컴포넌트 분리 (BoardDetail, BoardForm) | +| 공정관리 | `/master-data/process-management/[id]` | 하위 컴포넌트 분리 (ProcessDetail, ProcessForm) | +| 현장관리 | `/construction/site-management/[id]` | 목업 데이터, API 미연동 | +| 실행내역 | `/construction/order/structure-review/[id]` | 목업 데이터, API 미연동 | +| Q&A | `/customer-center/qna/[id]` | 댓글 시스템 포함 | +| 사원관리 | `/hr/employee-management/[id]` | 970줄, 우편번호 API, 동적 배열, 프로필 이미지 업로드 | +| 품목관리(건설) | `/construction/order/base-info/items/[id]` | 597줄, 동적 발주 항목 배열 관리 | + +--- + +### ⬜ 개별 구현 (마이그레이션 대기 - 21개) diff --git a/claudedocs/[REF] items-route-consolidation.md b/claudedocs/[REF] items-route-consolidation.md new file mode 100644 index 00000000..3a443ce6 --- /dev/null +++ b/claudedocs/[REF] items-route-consolidation.md @@ -0,0 +1,145 @@ +# 품목관리 경로 통합 이슈 정리 + +> 작성일: 2026-01-20 +> 브랜치: `feature/universal-detail-component` +> 커밋: `6f457b2` + +--- + +## 문제 발견 + +### 증상 +- `/production/screen-production` 경로에서 품목 **등록 실패** +- `/production/screen-production` 경로에서 품목 **수정 시 기존 값 미표시** + +### 원인 분석 + +**중복 경로 존재:** +``` +/items → 신버전 (DynamicItemForm) +/production/screen-production → 구버전 (ItemForm) +``` + +**백엔드 메뉴 설정:** +- 사이드바 "생산관리 > 품목관리" 클릭 시 → `/production/screen-production`으로 연결 +- 메뉴 URL이 API에서 동적으로 관리되어 프론트에서 직접 변경 불가 + +**결과:** +- 사용자는 항상 `/production/screen-production` (구버전 폼)으로 접속 +- 구버전 `ItemForm`은 API 필드 매핑이 맞지 않아 등록/수정 오류 발생 +- 신버전 `DynamicItemForm` (`/items`)은 정상 작동하지만 접근 경로 없음 + +--- + +## 파일 비교 + +### 등록 페이지 (create/page.tsx) + +| 항목 | `/items/create` | `/production/screen-production/create` | +|------|-----------------|---------------------------------------| +| 폼 컴포넌트 | `DynamicItemForm` | `ItemForm` | +| 폼 타입 | 동적 (품목기준관리 API) | 정적 (하드코딩) | +| API 매핑 | 정상 | 불일치 | +| 상태 | ✅ 정상 작동 | ❌ 등록 오류 | + +### 목록/상세 페이지 + +| 항목 | `/items` | `/production/screen-production` | +|------|----------|--------------------------------| +| 목록 | `ItemListClient` | `ItemListClient` | +| 상세 | `ItemDetailView` | `ItemDetailView` | +| 수정 | `ItemDetailEdit` | `ItemDetailEdit` | +| 상태 | 동일 컴포넌트 공유 | 동일 컴포넌트 공유 | + +**결론:** 목록/상세/수정은 같은 컴포넌트를 공유하지만, **등록만 다른 폼**이 연결되어 있었음 + +--- + +## 해결 방법 + +### 선택지 + +1. **백엔드 메뉴 URL 변경**: `/production/screen-production` → `/items` + - 백엔드 DB 수정 필요 + - 프론트 단독 작업 불가 + +2. **프론트 경로 통합**: `/items` 파일들을 `/production/screen-production`으로 이동 ✅ + - 백엔드 수정 불필요 + - 프론트 단독으로 해결 가능 + +### 적용한 해결책 + +**`/items` → `/production/screen-production` 파일 이동 및 통합** + +```bash +# 1. 기존 screen-production 삭제 +rm -rf src/app/[locale]/(protected)/production/screen-production + +# 2. items 파일들을 screen-production으로 복사 +cp -r src/app/[locale]/(protected)/items/* \ + src/app/[locale]/(protected)/production/screen-production/ + +# 3. items 폴더 삭제 +rm -rf src/app/[locale]/(protected)/items +``` + +--- + +## 수정된 파일 + +### 라우트 파일 (삭제) +- `src/app/[locale]/(protected)/items/page.tsx` +- `src/app/[locale]/(protected)/items/create/page.tsx` +- `src/app/[locale]/(protected)/items/[id]/page.tsx` +- `src/app/[locale]/(protected)/items/[id]/edit/page.tsx` + +### 라우트 파일 (신버전으로 교체) +- `src/app/[locale]/(protected)/production/screen-production/page.tsx` +- `src/app/[locale]/(protected)/production/screen-production/create/page.tsx` +- `src/app/[locale]/(protected)/production/screen-production/[id]/page.tsx` +- `src/app/[locale]/(protected)/production/screen-production/[id]/edit/page.tsx` + +### 컴포넌트 경로 참조 수정 (`/items` → `/production/screen-production`) +| 파일 | 수정 개수 | +|------|----------| +| `ItemListClient.tsx` | 3개 | +| `ItemForm/index.tsx` | 1개 | +| `ItemDetailClient.tsx` | 1개 | +| `ItemDetailEdit.tsx` | 2개 | +| `DynamicItemForm/index.tsx` | 2개 | +| **합계** | **9개** | + +--- + +## 교훈 + +### 문제 원인 +- 템플릿/테스트용 페이지에 메뉴를 연결한 채로 방치 +- 신버전 개발 시 구버전 경로 정리 누락 +- 두 경로가 같은 컴포넌트 일부를 공유해서 문제 파악 지연 + +### 예방책 +1. 신버전 개발 완료 시 구버전 경로 즉시 삭제 또는 리다이렉트 처리 +2. 메뉴 URL과 실제 라우트 파일 매핑 정기 점검 +3. 중복 경로 생성 시 명확한 용도 구분 및 문서화 + +--- + +## 최종 상태 + +``` +/production/screen-production → DynamicItemForm (신버전) +/items → 삭제됨 +``` + +**품목관리 CRUD 테스트 결과:** + +| 품목 유형 | Create | Read | Update | Delete | +|-----------|--------|------|--------|--------| +| 소모품(CS) | ✅ | ✅ | ✅ | ✅ | +| 원자재(RM) | ✅ | ✅ | ✅ | ✅ | +| 부자재(SM) | ✅ | ✅ | ✅ | ✅ | +| 부품-구매(PT) | ✅ | ✅ | ✅ | ✅ | +| 부품-절곡(PT) | ✅ | ✅ | ✅ | ✅ | +| 부품-조립(PT) | ✅ | ✅ | ✅ | ✅ | +| 제품(FG) | ✅ | ✅ | ✅ | ✅ | diff --git a/claudedocs/dev/[PLAN] detail-page-pattern-classification.md b/claudedocs/dev/[PLAN] detail-page-pattern-classification.md new file mode 100644 index 00000000..5ce20c78 --- /dev/null +++ b/claudedocs/dev/[PLAN] detail-page-pattern-classification.md @@ -0,0 +1,155 @@ +# 상세/등록/수정 페이지 패턴 분류표 + +> Chrome DevTools MCP로 직접 확인한 결과 기반 (2026-01-19) + +## 패턴 분류 기준 + +### 1️⃣ 페이지 형태 - 하단 버튼 (표준 패턴) +- URL이 변경되며 별도 페이지로 이동 +- 버튼 위치: **하단** (좌: 목록/취소, 우: 삭제/수정/저장) +- **IntegratedDetailTemplate 적용 대상** + +### 2️⃣ 페이지 형태 - 상단 버튼 +- URL이 변경되며 별도 페이지로 이동 +- 버튼 위치: **상단** +- IntegratedDetailTemplate 확장 필요 (`buttonPosition="top"`) + +### 3️⃣ 모달 형태 +- URL 변경 없음, Dialog/Modal로 표시 +- **IntegratedDetailTemplate 적용 제외** + +### 4️⃣ 인라인 입력 형태 +- 리스트 페이지 내에서 직접 입력/수정 +- **IntegratedDetailTemplate 적용 제외** + +### 5️⃣ DynamicForm 형태 +- API 기반 동적 폼 생성 +- IntegratedDetailTemplate의 `renderForm` prop으로 분기 처리 + +--- + +## 📄 페이지 형태 - 하단 버튼 (통합 대상) + +| 도메인 | 페이지 | URL 패턴 | 상태 | +|--------|--------|----------|------| +| **설정** | 계좌관리 | `/settings/accounts/[id]`, `/new` | ✅ 이미 마이그레이션 완료 | +| **설정** | 카드관리 | `/hr/card-management/[id]`, `/new` | ✅ 이미 마이그레이션 완료 | +| **설정** | 팝업관리 | `/settings/popup-management/[id]`, `/new` | 🔄 대상 | +| **설정** | 게시판관리 | `/board/board-management/[id]`, `/new` | 🔄 대상 | +| **기준정보** | 공정관리 | `/master-data/process-management/[id]`, `/new` | 🔄 대상 | +| **판매** | 거래처관리 | `/sales/client-management-sales-admin/[id]`, `/new` | 🔄 대상 | +| **판매** | 견적관리 | `/sales/quote-management/[id]`, `/new` | 🔄 대상 | +| **판매** | 수주관리 | `/sales/order-management-sales/[id]`, `/new` | 🔄 대상 | +| **품질** | 검사관리 | `/quality/inspections/[id]`, `/new` | 🔄 대상 | +| **출고** | 출하관리 | `/outbound/shipments/[id]`, `/new` | 🔄 대상 | +| **고객센터** | 공지사항 | `/customer-center/notices/[id]` | 🔄 대상 | +| **고객센터** | 이벤트 | `/customer-center/events/[id]` | 🔄 대상 | + +--- + +## 📄 페이지 형태 - 상단 버튼 (확장 필요) + +| 도메인 | 페이지 | URL 패턴 | 버튼 구성 | 비고 | +|--------|--------|----------|-----------|------| +| **회계** | 거래처관리 | `/accounting/vendors/[id]`, `/new` | 목록/삭제/수정 | 다중 섹션 구조 | +| **회계** | 매출관리 | `/accounting/sales/[id]`, `/new` | - | 🔄 대상 | +| **회계** | 매입관리 | `/accounting/purchase/[id]` | - | 🔄 대상 | +| **회계** | 입금관리 | `/accounting/deposits/[id]` | - | 🔄 대상 | +| **회계** | 출금관리 | `/accounting/withdrawals/[id]` | - | 🔄 대상 | +| **회계** | 어음관리 | `/accounting/bills/[id]`, `/new` | - | 🔄 대상 | +| **회계** | 악성채권 | `/accounting/bad-debt-collection/[id]`, `/new` | - | 🔄 대상 | +| **전자결재** | 기안함 (임시저장) | `/approval/draft/new?id=:id&mode=edit` | 상세/삭제/상신/저장 | 복잡한 섹션 구조 | + +--- + +## 🔲 모달 형태 (통합 제외) + +| 도메인 | 페이지 | 모달 컴포넌트 | 비고 | +|--------|--------|--------------|------| +| **설정** | 직급관리 | `RankDialog.tsx` | 인라인 입력 + 수정 모달 | +| **설정** | 직책관리 | `TitleDialog.tsx` | 인라인 입력 + 수정 모달 | +| **인사** | 부서관리 | `DepartmentDialog.tsx` | 트리 구조 | +| **인사** | 근태관리 | `AttendanceInfoDialog.tsx` | 모달로 등록 | +| **인사** | 휴가관리 | `VacationRequestDialog.tsx` | 모달로 등록/조정 | +| **인사** | 급여관리 | `SalaryDetailDialog.tsx` | 모달로 상세 | +| **전자결재** | 기안함 (결재대기) | 품의서 상세 Dialog | 상세만 모달 | +| **건설** | 카테고리관리 | `CategoryDialog.tsx` | 모달로 등록/수정 | + +--- + +## 🔧 DynamicForm 형태 (renderForm 분기) + +| 도메인 | 페이지 | URL 패턴 | 비고 | +|--------|--------|----------|------| +| **품목** | 품목관리 | `/items/[id]` | `DynamicItemForm` 사용 | + +--- + +## ⚠️ 특수 케이스 (개별 처리 필요) + +| 도메인 | 페이지 | URL 패턴 | 특이사항 | +|--------|--------|----------|----------| +| **설정** | 권한관리 | `/settings/permissions/[id]`, `/new` | Matrix UI, 복잡한 구조 | +| **인사** | 사원관리 | `/hr/employee-management/[id]`, `/new` | 40+ 필드, 탭 구조 | +| **게시판** | 게시글 | `/board/[boardCode]/[postId]` | 동적 게시판 | +| **건설** | 다수 페이지 | `/construction/...` | 별도 분류 필요 | + +--- + +## 📊 통합 우선순위 + +### Phase 1: 단순 CRUD (우선 작업) +1. 팝업관리 +2. 게시판관리 +3. 공정관리 +4. 공지사항/이벤트 + +### Phase 2: 중간 복잡도 +1. 판매 > 거래처관리 +2. 판매 > 견적관리 +3. 품질 > 검사관리 +4. 출고 > 출하관리 + +### Phase 3: 회계 도메인 (상단 버튼 확장 후) +1. 회계 > 거래처관리 +2. 회계 > 매출/매입/입금/출금 +3. 회계 > 어음/악성채권 + +### 제외 (개별 유지) +- 권한관리 (Matrix UI) +- 사원관리 (40+ 필드) +- 부서관리 (트리 구조) +- 전자결재 (복잡한 워크플로우) +- DynamicForm 페이지 (renderForm 분기) +- 모달 형태 페이지들 + +--- + +## IntegratedDetailTemplate 확장 필요 Props + +```typescript +interface IntegratedDetailTemplateProps { + // 기존 props... + + // 버튼 위치 제어 + buttonPosition?: 'top' | 'bottom'; // default: 'bottom' + + // 뒤로가기 버튼 표시 여부 + showBackButton?: boolean; // default: true + + // 상단 버튼 커스텀 (문서 결재 등) + headerActions?: ReactNode; + + // 다중 섹션 지원 + sections?: Array<{ + title: string; + fields: FieldConfig[]; + }>; +} +``` + +--- + +## 작성일 +- 최초 작성: 2026-01-19 +- Chrome DevTools MCP 확인 완료 diff --git a/claudedocs/dev/[REF] chrome-devtools-mcp-emoji-issue.md b/claudedocs/dev/[REF] chrome-devtools-mcp-emoji-issue.md new file mode 100644 index 00000000..0342adfa --- /dev/null +++ b/claudedocs/dev/[REF] chrome-devtools-mcp-emoji-issue.md @@ -0,0 +1,118 @@ +# Chrome DevTools MCP - 이모지 JSON 직렬화 오류 + +> 작성일: 2025-01-17 + +## 문제 현상 + +Chrome DevTools MCP가 특정 페이지 접근 시 다운되는 현상 + +### 에러 메시지 +``` +API Error: 400 {"type":"error","error":{"type":"invalid_request_error", +"message":"The request body is not valid JSON: invalid high surrogate in string: +line 1 column XXXXX (char XXXXX)"},"request_id":"req_XXXXX"} +``` + +### 발생 조건 +- 페이지에 **이모지**가 많이 포함된 경우 +- `take_snapshot` 또는 다른 MCP 도구 호출 시 +- a11y tree를 JSON으로 직렬화하는 과정에서 발생 + +## 원인 + +### 유니코드 서로게이트 쌍 (Surrogate Pair) 문제 + +이모지는 UTF-16에서 **서로게이트 쌍**으로 인코딩됨: +- High surrogate: U+D800 ~ U+DBFF +- Low surrogate: U+DC00 ~ U+DFFF + +Chrome DevTools MCP가 페이지 스냅샷을 JSON으로 직렬화할 때, 이모지의 서로게이트 쌍이 깨지면서 "invalid high surrogate" 오류 발생. + +### 문제가 되는 케이스 +1. **DOM에 직접 렌더링된 이모지**: `🏠` +2. **데이터에 포함된 이모지**: API 응답, 파싱된 데이터 +3. **대량의 이모지**: 수십 개 이상의 이모지가 한 페이지에 존재 + +## 해결 방법 + +### 1. 이모지를 Lucide 아이콘으로 교체 (UI) + +**Before** +```tsx +const iconMap = { + '기본': '🏠', + '인사관리': '👥', +}; + +{category.icon} +``` + +**After** +```tsx +import { Home, Users, type LucideIcon } from 'lucide-react'; + +const iconComponents: Record = { + Home, + Users, +}; + +function CategoryIcon({ name }: { name: string }) { + const IconComponent = iconComponents[name] || FileText; + return ; +} + + +``` + +### 2. 데이터 파싱 시 이모지 제거/변환 (Server) + +```typescript +function convertEmojiToText(text: string): string { + // 특정 이모지를 의미있는 텍스트로 변환 + let result = text + .replace(/✅/g, '[완료]') + .replace(/⚠️?/g, '[주의]') + .replace(/🧪/g, '[테스트]') + .replace(/🆕/g, '[NEW]') + .replace(/•/g, '-'); + + // 모든 이모지 및 특수 유니코드 문자 제거 + result = result + .replace(/[\u{1F300}-\u{1F9FF}]/gu, '') // 이모지 범위 + .replace(/[\u{2600}-\u{26FF}]/gu, '') // 기타 기호 + .replace(/[\u{2700}-\u{27BF}]/gu, '') // 딩뱃 + .replace(/[\u{FE00}-\u{FE0F}]/gu, '') // Variation Selectors + .replace(/[\u{1F000}-\u{1F02F}]/gu, '') // 마작 타일 + .replace(/[\u{1F0A0}-\u{1F0FF}]/gu, '') // 플레잉 카드 + .replace(/[\u200D]/g, '') // Zero Width Joiner + .trim(); + + return result; +} +``` + +## 체크리스트 + +새 페이지 개발 시 Chrome DevTools MCP 호환성 확인: + +- [ ] 페이지에 이모지 직접 렌더링하지 않음 +- [ ] 아이콘은 Lucide 또는 SVG 사용 +- [ ] 외부 데이터(API, 파일) 파싱 시 이모지 제거 처리 +- [ ] status, label 등에 이모지 대신 텍스트 사용 + +## 관련 파일 + +이 문제로 수정된 파일들: + +| 파일 | 변경 내용 | +|------|----------| +| `dev/test-urls/actions.ts` | iconMap, convertEmojiToText 함수 추가 | +| `dev/test-urls/TestUrlsClient.tsx` | Lucide 아이콘 동적 렌더링 | +| `dev/construction-test-urls/actions.ts` | 동일 | +| `dev/construction-test-urls/ConstructionTestUrlsClient.tsx` | 동일 | + +## 참고 + +- 이 문제는 Chrome DevTools MCP의 JSON 직렬화 로직에서 발생 +- MCP 자체 버그일 가능성 있으나, 클라이언트에서 이모지 제거로 우회 가능 +- 다른 MCP 도구에서도 비슷한 문제 발생 가능성 있음 diff --git a/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/edit/page.tsx b/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/edit/page.tsx index 2d964ca9..2a27fd6d 100644 --- a/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/edit/page.tsx @@ -1,58 +1,27 @@ 'use client'; -import { use, useEffect, useState } from 'react'; +import { use, useEffect } from 'react'; import { useRouter } from 'next/navigation'; -import { getBadDebtById } from '@/components/accounting/BadDebtCollection/actions'; -import { BadDebtDetail } from '@/components/accounting/BadDebtCollection/BadDebtDetail'; -import type { BadDebtRecord } from '@/components/accounting/BadDebtCollection/types'; interface EditBadDebtPageProps { params: Promise<{ id: string }>; } +/** + * 하위 호환성을 위한 리다이렉트 페이지 + * /bad-debt-collection/[id]/edit → /bad-debt-collection/[id]?mode=edit + */ export default function EditBadDebtPage({ params }: EditBadDebtPageProps) { const { id } = use(params); const router = useRouter(); - const [data, setData] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); useEffect(() => { - getBadDebtById(id) - .then(result => { - if (result) { - setData(result); - } else { - setError('데이터를 찾을 수 없습니다.'); - } - }) - .catch(() => { - setError('데이터를 불러오는 중 오류가 발생했습니다.'); - }) - .finally(() => setIsLoading(false)); - }, [id]); + router.replace(`/ko/accounting/bad-debt-collection/${id}?mode=edit`); + }, [id, router]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } - - if (error || !data) { - return ( -
-
{error || '데이터를 찾을 수 없습니다.'}
- -
- ); - } - - return ; -} \ No newline at end of file + return ( +
+
리다이렉트 중...
+
+ ); +} diff --git a/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/page.tsx b/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/page.tsx index 282395bd..7a1b12c2 100644 --- a/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/page.tsx +++ b/src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/page.tsx @@ -1,10 +1,7 @@ 'use client'; -import { use, useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { getBadDebtById } from '@/components/accounting/BadDebtCollection/actions'; -import { BadDebtDetail } from '@/components/accounting/BadDebtCollection/BadDebtDetail'; -import type { BadDebtRecord } from '@/components/accounting/BadDebtCollection/types'; +import { use } from 'react'; +import { BadDebtDetailClientV2 } from '@/components/accounting/BadDebtCollection'; interface BadDebtDetailPageProps { params: Promise<{ id: string }>; @@ -12,47 +9,5 @@ interface BadDebtDetailPageProps { export default function BadDebtDetailPage({ params }: BadDebtDetailPageProps) { const { id } = use(params); - const router = useRouter(); - const [data, setData] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - getBadDebtById(id) - .then(result => { - if (result) { - setData(result); - } else { - setError('데이터를 찾을 수 없습니다.'); - } - }) - .catch(() => { - setError('데이터를 불러오는 중 오류가 발생했습니다.'); - }) - .finally(() => setIsLoading(false)); - }, [id]); - - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } - - if (error || !data) { - return ( -
-
{error || '데이터를 찾을 수 없습니다.'}
- -
- ); - } - - return ; -} \ No newline at end of file + return ; +} diff --git a/src/app/[locale]/(protected)/accounting/bad-debt-collection/new/page.tsx b/src/app/[locale]/(protected)/accounting/bad-debt-collection/new/page.tsx index 342d5bb7..dde58bff 100644 --- a/src/app/[locale]/(protected)/accounting/bad-debt-collection/new/page.tsx +++ b/src/app/[locale]/(protected)/accounting/bad-debt-collection/new/page.tsx @@ -1,7 +1,7 @@ 'use client'; -import { BadDebtDetail } from '@/components/accounting/BadDebtCollection/BadDebtDetail'; +import { BadDebtDetailClientV2 } from '@/components/accounting/BadDebtCollection'; export default function NewBadDebtPage() { - return ; -} \ No newline at end of file + return ; +} diff --git a/src/app/[locale]/(protected)/accounting/deposits/[id]/edit/page.tsx b/src/app/[locale]/(protected)/accounting/deposits/[id]/edit/page.tsx new file mode 100644 index 00000000..98e02a03 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/deposits/[id]/edit/page.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { use, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; + +interface PageProps { + params: Promise<{ id: string }>; +} + +/** + * V2 하위 호환성: /[id]/edit → /[id]?mode=edit 리다이렉트 + */ +export default function DepositEditPage({ params }: PageProps) { + const { id } = use(params); + const router = useRouter(); + + useEffect(() => { + router.replace(`/accounting/deposits/${id}?mode=edit`); + }, [id, router]); + + return ( +
+
리다이렉트 중...
+
+ ); +} diff --git a/src/app/[locale]/(protected)/accounting/deposits/[id]/page.tsx b/src/app/[locale]/(protected)/accounting/deposits/[id]/page.tsx index 56fd4747..1a14190a 100644 --- a/src/app/[locale]/(protected)/accounting/deposits/[id]/page.tsx +++ b/src/app/[locale]/(protected)/accounting/deposits/[id]/page.tsx @@ -1,13 +1,22 @@ 'use client'; -import { useParams, useSearchParams } from 'next/navigation'; -import { DepositDetail } from '@/components/accounting/DepositManagement/DepositDetail'; +/** + * 입금 상세/수정 페이지 (Client Component) + * V2 패턴: ?mode=edit로 수정 모드 전환 + */ -export default function DepositDetailPage() { - const params = useParams(); +import { use } from 'react'; +import { useSearchParams } from 'next/navigation'; +import DepositDetailClientV2 from '@/components/accounting/DepositManagement/DepositDetailClientV2'; + +interface PageProps { + params: Promise<{ id: string }>; +} + +export default function DepositDetailPage({ params }: PageProps) { + const { id } = use(params); const searchParams = useSearchParams(); - const depositId = params.id as string; const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view'; - return ; + return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/deposits/new/page.tsx b/src/app/[locale]/(protected)/accounting/deposits/new/page.tsx new file mode 100644 index 00000000..2df329bd --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/deposits/new/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import DepositDetailClientV2 from '@/components/accounting/DepositManagement/DepositDetailClientV2'; + +export default function DepositNewPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/accounting/withdrawals/[id]/edit/page.tsx b/src/app/[locale]/(protected)/accounting/withdrawals/[id]/edit/page.tsx new file mode 100644 index 00000000..ed032445 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/withdrawals/[id]/edit/page.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { use, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; + +interface PageProps { + params: Promise<{ id: string }>; +} + +/** + * V2 하위 호환성: /[id]/edit → /[id]?mode=edit 리다이렉트 + */ +export default function WithdrawalEditPage({ params }: PageProps) { + const { id } = use(params); + const router = useRouter(); + + useEffect(() => { + router.replace(`/accounting/withdrawals/${id}?mode=edit`); + }, [id, router]); + + return ( +
+
리다이렉트 중...
+
+ ); +} diff --git a/src/app/[locale]/(protected)/accounting/withdrawals/[id]/page.tsx b/src/app/[locale]/(protected)/accounting/withdrawals/[id]/page.tsx index a27b057d..20bdb861 100644 --- a/src/app/[locale]/(protected)/accounting/withdrawals/[id]/page.tsx +++ b/src/app/[locale]/(protected)/accounting/withdrawals/[id]/page.tsx @@ -1,13 +1,22 @@ 'use client'; -import { useParams, useSearchParams } from 'next/navigation'; -import { WithdrawalDetail } from '@/components/accounting/WithdrawalManagement/WithdrawalDetail'; +/** + * 출금 상세/수정 페이지 (Client Component) + * V2 패턴: ?mode=edit로 수정 모드 전환 + */ -export default function WithdrawalDetailPage() { - const params = useParams(); +import { use } from 'react'; +import { useSearchParams } from 'next/navigation'; +import WithdrawalDetailClientV2 from '@/components/accounting/WithdrawalManagement/WithdrawalDetailClientV2'; + +interface PageProps { + params: Promise<{ id: string }>; +} + +export default function WithdrawalDetailPage({ params }: PageProps) { + const { id } = use(params); const searchParams = useSearchParams(); - const withdrawalId = params.id as string; const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view'; - return ; + return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/accounting/withdrawals/new/page.tsx b/src/app/[locale]/(protected)/accounting/withdrawals/new/page.tsx new file mode 100644 index 00000000..72d3b9d0 --- /dev/null +++ b/src/app/[locale]/(protected)/accounting/withdrawals/new/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import WithdrawalDetailClientV2 from '@/components/accounting/WithdrawalManagement/WithdrawalDetailClientV2'; + +export default function WithdrawalNewPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/board/board-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/board/board-management/[id]/edit/page.tsx index ad093909..4bae1040 100644 --- a/src/app/[locale]/(protected)/board/board-management/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/board/board-management/[id]/edit/page.tsx @@ -1,116 +1,24 @@ 'use client'; -import { useRouter, useParams } from 'next/navigation'; -import { useState, useEffect, useCallback } from 'react'; -import { Loader2 } from 'lucide-react'; -import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; -import { BoardForm } from '@/components/board/BoardManagement/BoardForm'; -import { getBoardById, updateBoard } from '@/components/board/BoardManagement/actions'; -import { forceRefreshMenus } from '@/lib/utils/menuRefresh'; -import { Button } from '@/components/ui/button'; -import type { Board, BoardFormData } from '@/components/board/BoardManagement/types'; +/** + * 게시판관리 수정 페이지 (리다이렉트) + * + * 기존 URL 호환성을 위해 유지 + * /[id]/edit → /[id]?mode=edit 로 리다이렉트 + */ -export default function BoardEditPage() { +import { useEffect } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; + +export default function BoardEditRedirectPage() { const router = useRouter(); const params = useParams(); - const [board, setBoard] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [isSubmitting, setIsSubmitting] = useState(false); - - const fetchBoard = useCallback(async () => { - const id = params.id as string; - if (!id) return; - - setIsLoading(true); - setError(null); - - const result = await getBoardById(id); - - if (result.success && result.data) { - setBoard(result.data); - } else { - setError(result.error || '게시판 정보를 불러오는데 실패했습니다.'); - } - - setIsLoading(false); - }, [params.id]); + const id = params.id as string; useEffect(() => { - fetchBoard(); - }, [fetchBoard]); + router.replace(`/ko/board/board-management/${id}?mode=edit`); + }, [router, id]); - const handleSubmit = async (data: BoardFormData) => { - if (!board) return; - - setIsSubmitting(true); - setError(null); - - const result = await updateBoard(board.id, { - ...data, - boardCode: board.boardCode, - description: board.description, - }); - - if (result.success) { - // 게시판 수정 성공 시 메뉴 즉시 갱신 - await forceRefreshMenus(); - router.push(`/ko/board/board-management/${board.id}`); - } else { - setError(result.error || '수정에 실패했습니다.'); - } - - setIsSubmitting(false); - }; - - // 로딩 상태 - if (isLoading) { - return ; - } - - // 에러 상태 - if (error && !board) { - return ( -
-

{error}

- -
- ); - } - - if (!board) { - return ( -
-

게시판을 찾을 수 없습니다.

- -
- ); - } - - return ( - <> - {error && ( -
-

{error}

-
- )} - - {isSubmitting && ( -
-
- - 저장 중... -
-
- )} - - ); + return ; } diff --git a/src/app/[locale]/(protected)/board/board-management/[id]/page.tsx b/src/app/[locale]/(protected)/board/board-management/[id]/page.tsx index b93de1cc..83a5f9f7 100644 --- a/src/app/[locale]/(protected)/board/board-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/board/board-management/[id]/page.tsx @@ -1,134 +1,19 @@ 'use client'; -import { useRouter, useParams } from 'next/navigation'; -import { useState, useEffect, useCallback } from 'react'; -import { Loader2 } from 'lucide-react'; -import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; -import { BoardDetail } from '@/components/board/BoardManagement/BoardDetail'; -import { getBoardById, deleteBoard } from '@/components/board/BoardManagement/actions'; -import { Button } from '@/components/ui/button'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; -import type { Board } from '@/components/board/BoardManagement/types'; +/** + * 게시판관리 상세/수정 페이지 + * + * IntegratedDetailTemplate V2 마이그레이션 + * - /[id] → 상세 보기 (view 모드) + * - /[id]?mode=edit → 수정 (edit 모드) + */ + +import { useParams } from 'next/navigation'; +import { BoardDetailClientV2 } from '@/components/board/BoardManagement/BoardDetailClientV2'; export default function BoardDetailPage() { - const router = useRouter(); const params = useParams(); - const [board, setBoard] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); + const id = params.id as string; - const fetchBoard = useCallback(async () => { - const id = params.id as string; - if (!id) return; - - setIsLoading(true); - setError(null); - - const result = await getBoardById(id); - - if (result.success && result.data) { - setBoard(result.data); - } else { - setError(result.error || '게시판 정보를 불러오는데 실패했습니다.'); - } - - setIsLoading(false); - }, [params.id]); - - useEffect(() => { - fetchBoard(); - }, [fetchBoard]); - - const handleEdit = () => { - router.push(`/ko/board/board-management/${params.id}/edit`); - }; - - const handleDelete = () => { - setDeleteDialogOpen(true); - }; - - const confirmDelete = async () => { - if (!board) return; - - setIsDeleting(true); - const result = await deleteBoard(board.id); - - if (result.success) { - router.push('/ko/board/board-management'); - } else { - setError(result.error || '삭제에 실패했습니다.'); - setDeleteDialogOpen(false); - } - setIsDeleting(false); - }; - - // 로딩 상태 - if (isLoading) { - return ; - } - - // 에러 상태 - if (error || !board) { - return ( -
-

{error || '게시판을 찾을 수 없습니다.'}

- -
- ); - } - - return ( - <> - - - - - - 게시판 삭제 - - "{board.boardName}" 게시판을 삭제하시겠습니까? -
- - 삭제된 게시판 정보는 복구할 수 없습니다. - -
-
- - 취소 - - {isDeleting ? ( - <> - - 삭제 중... - - ) : ( - '삭제' - )} - - -
-
- - ); + return ; } diff --git a/src/app/[locale]/(protected)/board/board-management/new/page.tsx b/src/app/[locale]/(protected)/board/board-management/new/page.tsx index a5e30375..0fc6920e 100644 --- a/src/app/[locale]/(protected)/board/board-management/new/page.tsx +++ b/src/app/[locale]/(protected)/board/board-management/new/page.tsx @@ -1,64 +1,13 @@ 'use client'; -import { useRouter } from 'next/navigation'; -import { useState } from 'react'; -import { Loader2 } from 'lucide-react'; -import { BoardForm } from '@/components/board/BoardManagement/BoardForm'; -import { createBoard } from '@/components/board/BoardManagement/actions'; -import { forceRefreshMenus } from '@/lib/utils/menuRefresh'; -import type { BoardFormData } from '@/components/board/BoardManagement/types'; +/** + * 게시판관리 등록 페이지 + * + * IntegratedDetailTemplate V2 마이그레이션 + */ -// 게시판 코드 생성 (임시: 타임스탬프 기반) -const generateBoardCode = (): string => { - const timestamp = Date.now().toString(36); - const random = Math.random().toString(36).substring(2, 6); - return `board_${timestamp}_${random}`; -}; +import { BoardDetailClientV2 } from '@/components/board/BoardManagement/BoardDetailClientV2'; -export default function BoardNewPage() { - const router = useRouter(); - const [error, setError] = useState(null); - const [isSubmitting, setIsSubmitting] = useState(false); - - const handleSubmit = async (data: BoardFormData) => { - setIsSubmitting(true); - setError(null); - - const result = await createBoard({ - ...data, - boardCode: generateBoardCode(), - }); - - if (result.success && result.data) { - // 게시판 생성 성공 시 메뉴 즉시 갱신 - await forceRefreshMenus(); - router.push('/ko/board/board-management'); - } else { - setError(result.error || '게시판 생성에 실패했습니다.'); - } - - setIsSubmitting(false); - }; - - return ( - <> - {error && ( -
-

{error}

-
- )} - - {isSubmitting && ( -
-
- - 등록 중... -
-
- )} - - ); +export default function BoardCreatePage() { + return ; } diff --git a/src/app/[locale]/(protected)/construction/billing/progress-billing-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/billing/progress-billing-management/[id]/edit/page.tsx index 41cb4bcf..097d64ce 100644 --- a/src/app/[locale]/(protected)/construction/billing/progress-billing-management/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/billing/progress-billing-management/[id]/edit/page.tsx @@ -1,57 +1,27 @@ 'use client'; -import { use, useEffect, useState } from 'react'; +import { use, useEffect } from 'react'; import { useRouter } from 'next/navigation'; -import { ProgressBillingDetailForm } from '@/components/business/construction/progress-billing'; -import { getProgressBillingDetail } from '@/components/business/construction/progress-billing/actions'; interface ProgressBillingEditPageProps { params: Promise<{ id: string }>; } +/** + * 하위 호환성을 위한 리다이렉트 페이지 + * /progress-billing-management/[id]/edit → /progress-billing-management/[id]?mode=edit + */ export default function ProgressBillingEditPage({ params }: ProgressBillingEditPageProps) { const { id } = use(params); const router = useRouter(); - const [data, setData] = useState>['data']>(undefined); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); useEffect(() => { - getProgressBillingDetail(id) - .then(result => { - if (result.success && result.data) { - setData(result.data); - } else { - setError('기성청구 정보를 찾을 수 없습니다.'); - } - }) - .catch(() => { - setError('기성청구 정보를 불러오는 중 오류가 발생했습니다.'); - }) - .finally(() => setIsLoading(false)); - }, [id]); + router.replace(`/ko/construction/billing/progress-billing-management/${id}?mode=edit`); + }, [id, router]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } - - if (error || !data) { - return ( -
-
{error || '기성청구 정보를 찾을 수 없습니다.'}
- -
- ); - } - - return ; + return ( +
+
리다이렉트 중...
+
+ ); } diff --git a/src/app/[locale]/(protected)/construction/billing/progress-billing-management/[id]/page.tsx b/src/app/[locale]/(protected)/construction/billing/progress-billing-management/[id]/page.tsx index 596afd43..3be49edc 100644 --- a/src/app/[locale]/(protected)/construction/billing/progress-billing-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/billing/progress-billing-management/[id]/page.tsx @@ -1,9 +1,10 @@ 'use client'; -import { use, useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; +import { use, useEffect, useState, useCallback } from 'react'; +import { useSearchParams } from 'next/navigation'; import { ProgressBillingDetailForm } from '@/components/business/construction/progress-billing'; import { getProgressBillingDetail } from '@/components/business/construction/progress-billing/actions'; +import { ServerErrorPage } from '@/components/common/ServerErrorPage'; interface ProgressBillingDetailPageProps { params: Promise<{ id: string }>; @@ -11,12 +12,19 @@ interface ProgressBillingDetailPageProps { export default function ProgressBillingDetailPage({ params }: ProgressBillingDetailPageProps) { const { id } = use(params); - const router = useRouter(); + const searchParams = useSearchParams(); + + // V2 패턴: mode 체크 + const mode = searchParams.get('mode') || 'view'; + const isEditMode = mode === 'edit'; + const [data, setData] = useState>['data']>(undefined); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { + const fetchData = useCallback(() => { + setIsLoading(true); + setError(null); getProgressBillingDetail(id) .then(result => { if (result.success && result.data) { @@ -31,6 +39,10 @@ export default function ProgressBillingDetailPage({ params }: ProgressBillingDet .finally(() => setIsLoading(false)); }, [id]); + useEffect(() => { + fetchData(); + }, [fetchData]); + if (isLoading) { return (
@@ -41,17 +53,21 @@ export default function ProgressBillingDetailPage({ params }: ProgressBillingDet if (error || !data) { return ( -
-
{error || '기성청구 정보를 찾을 수 없습니다.'}
- -
+ ); } - return ; + return ( + + ); } diff --git a/src/app/[locale]/(protected)/construction/order/base-info/labor/[id]/page.tsx b/src/app/[locale]/(protected)/construction/order/base-info/labor/[id]/page.tsx index 45e54d8e..8bb8feb7 100644 --- a/src/app/[locale]/(protected)/construction/order/base-info/labor/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/base-info/labor/[id]/page.tsx @@ -2,7 +2,8 @@ import { use } from 'react'; import { useSearchParams } from 'next/navigation'; -import { LaborDetailClient } from '@/components/business/construction/labor-management'; +import { LaborDetailClientV2 } from '@/components/business/construction/labor-management'; +import type { DetailMode } from '@/components/templates/IntegratedDetailTemplate'; interface LaborDetailPageProps { params: Promise<{ id: string }>; @@ -12,7 +13,7 @@ export default function LaborDetailPage({ params }: LaborDetailPageProps) { const { id } = use(params); const searchParams = useSearchParams(); const mode = searchParams.get('mode'); - const isEditMode = mode === 'edit'; + const initialMode: DetailMode = mode === 'edit' ? 'edit' : 'view'; - return ; + return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/order/base-info/labor/new/page.tsx b/src/app/[locale]/(protected)/construction/order/base-info/labor/new/page.tsx index dcfb97c7..8a4064ff 100644 --- a/src/app/[locale]/(protected)/construction/order/base-info/labor/new/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/base-info/labor/new/page.tsx @@ -1,5 +1,7 @@ -import { LaborDetailClient } from '@/components/business/construction/labor-management'; +'use client'; + +import { LaborDetailClientV2 } from '@/components/business/construction/labor-management'; export default function LaborNewPage() { - return ; + return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/order/base-info/pricing/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/order/base-info/pricing/[id]/edit/page.tsx index 5298d713..eb2cf1be 100644 --- a/src/app/[locale]/(protected)/construction/order/base-info/pricing/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/base-info/pricing/[id]/edit/page.tsx @@ -1,14 +1,26 @@ 'use client'; -import { use } from 'react'; -import PricingDetailClient from '@/components/business/construction/pricing-management/PricingDetailClient'; +import { use, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; interface PageProps { params: Promise<{ id: string }>; } +/** + * V2 하위 호환성: /[id]/edit → /[id]?mode=edit 리다이렉트 + */ export default function PricingEditPage({ params }: PageProps) { const { id } = use(params); + const router = useRouter(); - return ; + useEffect(() => { + router.replace(`/construction/order/base-info/pricing/${id}?mode=edit`); + }, [id, router]); + + return ( +
+
리다이렉트 중...
+
+ ); } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/order/base-info/pricing/[id]/page.tsx b/src/app/[locale]/(protected)/construction/order/base-info/pricing/[id]/page.tsx index a5097a30..da09fbc3 100644 --- a/src/app/[locale]/(protected)/construction/order/base-info/pricing/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/base-info/pricing/[id]/page.tsx @@ -1,7 +1,13 @@ 'use client'; +/** + * 단가 상세/수정 페이지 (Client Component) + * V2 패턴: ?mode=edit로 수정 모드 전환 + */ + import { use } from 'react'; -import PricingDetailClient from '@/components/business/construction/pricing-management/PricingDetailClient'; +import { useSearchParams } from 'next/navigation'; +import { PricingDetailClientV2 } from '@/components/business/construction/pricing-management'; interface PageProps { params: Promise<{ id: string }>; @@ -9,6 +15,8 @@ interface PageProps { export default function PricingDetailPage({ params }: PageProps) { const { id } = use(params); + const searchParams = useSearchParams(); + const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view'; - return ; + return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/order/base-info/pricing/new/page.tsx b/src/app/[locale]/(protected)/construction/order/base-info/pricing/new/page.tsx index da6cc1b8..9b008da7 100644 --- a/src/app/[locale]/(protected)/construction/order/base-info/pricing/new/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/base-info/pricing/new/page.tsx @@ -1,5 +1,7 @@ -import PricingDetailClient from '@/components/business/construction/pricing-management/PricingDetailClient'; +'use client'; + +import { PricingDetailClientV2 } from '@/components/business/construction/pricing-management'; export default function PricingNewPage() { - return ; + return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/order/order-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/order/order-management/[id]/edit/page.tsx index 6416e183..328546e0 100644 --- a/src/app/[locale]/(protected)/construction/order/order-management/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/order-management/[id]/edit/page.tsx @@ -1,57 +1,26 @@ 'use client'; -import { use, useEffect, useState } from 'react'; +import { use, useEffect } from 'react'; import { useRouter } from 'next/navigation'; -import { OrderDetailForm } from '@/components/business/construction/order-management'; -import { getOrderDetailFull } from '@/components/business/construction/order-management/actions'; interface OrderEditPageProps { params: Promise<{ id: string }>; } +/** + * V2 하위 호환성: /[id]/edit → /[id]?mode=edit 리다이렉트 + */ export default function OrderEditPage({ params }: OrderEditPageProps) { const { id } = use(params); const router = useRouter(); - const [data, setData] = useState>['data']>(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); useEffect(() => { - getOrderDetailFull(id) - .then(result => { - if (result.success && result.data) { - setData(result.data); - } else { - setError('주문 정보를 찾을 수 없습니다.'); - } - }) - .catch(() => { - setError('주문 정보를 불러오는 중 오류가 발생했습니다.'); - }) - .finally(() => setIsLoading(false)); - }, [id]); + router.replace(`/construction/order/order-management/${id}?mode=edit`); + }, [id, router]); - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } - - if (error || !data) { - return ( -
-
{error || '주문 정보를 찾을 수 없습니다.'}
- -
- ); - } - - return ; -} \ No newline at end of file + return ( +
+
리다이렉트 중...
+
+ ); +} diff --git a/src/app/[locale]/(protected)/construction/order/order-management/[id]/page.tsx b/src/app/[locale]/(protected)/construction/order/order-management/[id]/page.tsx index e3f0018f..e0826311 100644 --- a/src/app/[locale]/(protected)/construction/order/order-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/order-management/[id]/page.tsx @@ -1,9 +1,15 @@ 'use client'; +/** + * 발주 상세/수정 페이지 (Client Component) + * V2 패턴: ?mode=edit로 수정 모드 전환 + */ + import { use, useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import { OrderDetailForm } from '@/components/business/construction/order-management'; import { getOrderDetailFull } from '@/components/business/construction/order-management/actions'; +import { ServerErrorPage } from '@/components/common/ServerErrorPage'; interface OrderDetailPageProps { params: Promise<{ id: string }>; @@ -12,6 +18,8 @@ interface OrderDetailPageProps { export default function OrderDetailPage({ params }: OrderDetailPageProps) { const { id } = use(params); const router = useRouter(); + const searchParams = useSearchParams(); + const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view'; const [data, setData] = useState>['data']>(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); @@ -41,17 +49,14 @@ export default function OrderDetailPage({ params }: OrderDetailPageProps) { if (error || !data) { return ( -
-
{error || '주문 정보를 찾을 수 없습니다.'}
- -
+ ); } - return ; + return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/construction/order/site-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/order/site-management/[id]/edit/page.tsx index 27e86534..f0c66aee 100644 --- a/src/app/[locale]/(protected)/construction/order/site-management/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/site-management/[id]/edit/page.tsx @@ -1,43 +1,27 @@ 'use client'; -import { use, useEffect, useState } from 'react'; -import SiteDetailForm from '@/components/business/construction/site-management/SiteDetailForm'; +import { use, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; -// 목업 데이터 -const MOCK_SITE = { - id: '1', - siteCode: '123-12-12345', - partnerId: '1', - partnerName: '거래처명', - siteName: '현장명', - address: '', - status: 'active' as const, - createdAt: '2025-09-01T00:00:00Z', - updatedAt: '2025-09-01T00:00:00Z', -}; - -interface PageProps { +interface EditSitePageProps { params: Promise<{ id: string }>; } -export default function SiteEditPage({ params }: PageProps) { +/** + * 하위 호환성을 위한 리다이렉트 페이지 + * /site-management/[id]/edit → /site-management/[id]?mode=edit + */ +export default function EditSitePage({ params }: EditSitePageProps) { const { id } = use(params); - const [site, setSite] = useState(null); - const [isLoading, setIsLoading] = useState(true); + const router = useRouter(); useEffect(() => { - // TODO: API에서 현장 정보 조회 - setSite({ ...MOCK_SITE, id }); - setIsLoading(false); - }, [id]); + router.replace(`/ko/construction/order/site-management/${id}?mode=edit`); + }, [id, router]); - if (isLoading || !site) { - return ( -
-
로딩 중...
-
- ); - } - - return ; -} \ No newline at end of file + return ( +
+
리다이렉트 중...
+
+ ); +} diff --git a/src/app/[locale]/(protected)/construction/order/site-management/[id]/page.tsx b/src/app/[locale]/(protected)/construction/order/site-management/[id]/page.tsx index 0ba0ee0d..75cc937f 100644 --- a/src/app/[locale]/(protected)/construction/order/site-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/site-management/[id]/page.tsx @@ -1,43 +1,20 @@ 'use client'; -import { use, useEffect, useState } from 'react'; -import SiteDetailForm from '@/components/business/construction/site-management/SiteDetailForm'; +/** + * 현장관리 상세 페이지 (V2) + * + * /site-management/[id] → 조회 모드 + * /site-management/[id]?mode=edit → 수정 모드 + */ -// 목업 데이터 -const MOCK_SITE = { - id: '1', - siteCode: '123-12-12345', - partnerId: '1', - partnerName: '거래처명', - siteName: '현장명', - address: '', - status: 'active' as const, - createdAt: '2025-09-01T00:00:00Z', - updatedAt: '2025-09-01T00:00:00Z', -}; +import { use } from 'react'; +import { SiteDetailClientV2 } from '@/components/business/construction/site-management/SiteDetailClientV2'; -interface PageProps { +interface SiteDetailPageProps { params: Promise<{ id: string }>; } -export default function SiteDetailPage({ params }: PageProps) { +export default function SiteDetailPage({ params }: SiteDetailPageProps) { const { id } = use(params); - const [site, setSite] = useState(null); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - // TODO: API에서 현장 정보 조회 - setSite({ ...MOCK_SITE, id }); - setIsLoading(false); - }, [id]); - - if (isLoading || !site) { - return ( -
-
로딩 중...
-
- ); - } - - return ; -} \ No newline at end of file + return ; +} diff --git a/src/app/[locale]/(protected)/construction/order/structure-review/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/order/structure-review/[id]/edit/page.tsx index c83f9720..f87991a3 100644 --- a/src/app/[locale]/(protected)/construction/order/structure-review/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/structure-review/[id]/edit/page.tsx @@ -1,48 +1,27 @@ 'use client'; -import { use, useEffect, useState } from 'react'; -import StructureReviewDetailForm from '@/components/business/construction/structure-review/StructureReviewDetailForm'; +import { use, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; -// 목업 데이터 -const MOCK_REVIEW = { - id: '1', - reviewNumber: '123123', - partnerId: '1', - partnerName: '거래처명A', - siteId: '1', - siteName: '현장A', - requestDate: '2025-12-12', - reviewCompany: '회사명', - reviewerName: '홍길동', - reviewDate: '2025-12-15', - completionDate: null, - status: 'pending' as const, - createdAt: '2025-12-01T00:00:00Z', - updatedAt: '2025-12-01T00:00:00Z', -}; - -interface PageProps { +interface EditStructureReviewPageProps { params: Promise<{ id: string }>; } -export default function StructureReviewEditPage({ params }: PageProps) { +/** + * 하위 호환성을 위한 리다이렉트 페이지 + * /structure-review/[id]/edit → /structure-review/[id]?mode=edit + */ +export default function EditStructureReviewPage({ params }: EditStructureReviewPageProps) { const { id } = use(params); - const [review, setReview] = useState(null); - const [isLoading, setIsLoading] = useState(true); + const router = useRouter(); useEffect(() => { - // TODO: API에서 구조검토 정보 조회 - setReview({ ...MOCK_REVIEW, id }); - setIsLoading(false); - }, [id]); + router.replace(`/ko/construction/order/structure-review/${id}?mode=edit`); + }, [id, router]); - if (isLoading || !review) { - return ( -
-
로딩 중...
-
- ); - } - - return ; -} \ No newline at end of file + return ( +
+
리다이렉트 중...
+
+ ); +} diff --git a/src/app/[locale]/(protected)/construction/order/structure-review/[id]/page.tsx b/src/app/[locale]/(protected)/construction/order/structure-review/[id]/page.tsx index 31d8886e..7a6dd470 100644 --- a/src/app/[locale]/(protected)/construction/order/structure-review/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/order/structure-review/[id]/page.tsx @@ -1,48 +1,20 @@ 'use client'; -import { use, useEffect, useState } from 'react'; -import StructureReviewDetailForm from '@/components/business/construction/structure-review/StructureReviewDetailForm'; +/** + * 구조검토 상세 페이지 (V2) + * + * /structure-review/[id] → 조회 모드 + * /structure-review/[id]?mode=edit → 수정 모드 + */ -// 목업 데이터 -const MOCK_REVIEW = { - id: '1', - reviewNumber: '123123', - partnerId: '1', - partnerName: '거래처명A', - siteId: '1', - siteName: '현장A', - requestDate: '2025-12-12', - reviewCompany: '회사명', - reviewerName: '홍길동', - reviewDate: '2025-12-15', - completionDate: null, - status: 'pending' as const, - createdAt: '2025-12-01T00:00:00Z', - updatedAt: '2025-12-01T00:00:00Z', -}; +import { use } from 'react'; +import { StructureReviewDetailClientV2 } from '@/components/business/construction/structure-review/StructureReviewDetailClientV2'; -interface PageProps { +interface StructureReviewDetailPageProps { params: Promise<{ id: string }>; } -export default function StructureReviewDetailPage({ params }: PageProps) { +export default function StructureReviewDetailPage({ params }: StructureReviewDetailPageProps) { const { id } = use(params); - const [review, setReview] = useState(null); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - // TODO: API에서 구조검토 정보 조회 - setReview({ ...MOCK_REVIEW, id }); - setIsLoading(false); - }, [id]); - - if (isLoading || !review) { - return ( -
-
로딩 중...
-
- ); - } - - return ; + return ; } diff --git a/src/app/[locale]/(protected)/construction/project/bidding/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/[id]/edit/page.tsx index 782b6382..77d51552 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/[id]/edit/page.tsx @@ -1,38 +1,27 @@ 'use client'; -import { use, useEffect, useState } from 'react'; -import { BiddingDetailForm, getBiddingDetail } from '@/components/business/construction/bidding'; +import { use, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; interface BiddingEditPageProps { params: Promise<{ id: string }>; } +/** + * 하위 호환성을 위한 리다이렉트 페이지 + * /bidding/[id]/edit → /bidding/[id]?mode=edit + */ export default function BiddingEditPage({ params }: BiddingEditPageProps) { const { id } = use(params); - const [data, setData] = useState>['data']>(null); - const [isLoading, setIsLoading] = useState(true); + const router = useRouter(); useEffect(() => { - getBiddingDetail(id) - .then(result => { - setData(result.data); - }) - .finally(() => setIsLoading(false)); - }, [id]); - - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } + router.replace(`/ko/construction/project/bidding/${id}?mode=edit`); + }, [id, router]); return ( - +
+
리다이렉트 중...
+
); -} \ No newline at end of file +} diff --git a/src/app/[locale]/(protected)/construction/project/bidding/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/[id]/page.tsx index 51cec9a3..a02ae61c 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/[id]/page.tsx @@ -1,7 +1,9 @@ 'use client'; -import { use, useEffect, useState } from 'react'; +import { use, useEffect, useState, useCallback } from 'react'; +import { useSearchParams } from 'next/navigation'; import { BiddingDetailForm, getBiddingDetail } from '@/components/business/construction/bidding'; +import { ServerErrorPage } from '@/components/common/ServerErrorPage'; interface BiddingDetailPageProps { params: Promise<{ id: string }>; @@ -9,17 +11,37 @@ interface BiddingDetailPageProps { export default function BiddingDetailPage({ params }: BiddingDetailPageProps) { const { id } = use(params); + const searchParams = useSearchParams(); + + // V2 패턴: mode 체크 + const mode = searchParams.get('mode') || 'view'; + const isEditMode = mode === 'edit'; + const [data, setData] = useState>['data']>(null); const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); - useEffect(() => { + const fetchData = useCallback(() => { + setIsLoading(true); + setError(null); getBiddingDetail(id) .then(result => { - setData(result.data); + if (result.success && result.data) { + setData(result.data); + } else { + setError('입찰 정보를 찾을 수 없습니다.'); + } + }) + .catch(() => { + setError('입찰 정보를 불러오는 중 오류가 발생했습니다.'); }) .finally(() => setIsLoading(false)); }, [id]); + useEffect(() => { + fetchData(); + }, [fetchData]); + if (isLoading) { return (
@@ -28,9 +50,21 @@ export default function BiddingDetailPage({ params }: BiddingDetailPageProps) { ); } + if (error) { + return ( + + ); + } + return ( diff --git a/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/edit/page.tsx index 815b3c85..64c0d318 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/edit/page.tsx @@ -1,61 +1,27 @@ 'use client'; -import { use, useEffect, useState } from 'react'; -import { EstimateDetailForm } from '@/components/business/construction/estimates'; -import type { EstimateDetail } from '@/components/business/construction/estimates'; -import { getEstimateDetail } from '@/components/business/construction/estimates/actions'; +import { use, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; interface EstimateEditPageProps { params: Promise<{ id: string }>; } +/** + * 하위 호환성을 위한 리다이렉트 페이지 + * /estimates/[id]/edit → /estimates/[id]?mode=edit + */ export default function EstimateEditPage({ params }: EstimateEditPageProps) { const { id } = use(params); - const [data, setData] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); + const router = useRouter(); useEffect(() => { - async function fetchData() { - try { - setIsLoading(true); - setError(null); - const result = await getEstimateDetail(id); - if (result.success && result.data) { - setData(result.data); - } else { - setError(result.error || '견적 정보를 불러오는데 실패했습니다.'); - } - } catch (err) { - setError(err instanceof Error ? err.message : '오류가 발생했습니다.'); - } finally { - setIsLoading(false); - } - } - fetchData(); - }, [id]); - - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } - - if (error) { - return ( -
-
{error}
-
- ); - } + router.replace(`/ko/construction/project/bidding/estimates/${id}?mode=edit`); + }, [id, router]); return ( - +
+
리다이렉트 중...
+
); -} \ No newline at end of file +} diff --git a/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/page.tsx index 4778e319..a2593926 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/estimates/[id]/page.tsx @@ -1,9 +1,11 @@ 'use client'; -import { use, useEffect, useState } from 'react'; +import { use, useEffect, useState, useCallback } from 'react'; +import { useSearchParams } from 'next/navigation'; import { EstimateDetailForm } from '@/components/business/construction/estimates'; import type { EstimateDetail } from '@/components/business/construction/estimates'; import { getEstimateDetail } from '@/components/business/construction/estimates/actions'; +import { ServerErrorPage } from '@/components/common/ServerErrorPage'; interface EstimateDetailPageProps { params: Promise<{ id: string }>; @@ -11,30 +13,37 @@ interface EstimateDetailPageProps { export default function EstimateDetailPage({ params }: EstimateDetailPageProps) { const { id } = use(params); + const searchParams = useSearchParams(); + + // V2 패턴: mode 체크 + const mode = searchParams.get('mode') || 'view'; + const isEditMode = mode === 'edit'; + const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { - async function fetchData() { - try { - setIsLoading(true); - setError(null); - const result = await getEstimateDetail(id); - if (result.success && result.data) { - setData(result.data); - } else { - setError(result.error || '견적 정보를 불러오는데 실패했습니다.'); - } - } catch (err) { - setError(err instanceof Error ? err.message : '오류가 발생했습니다.'); - } finally { - setIsLoading(false); + const fetchData = useCallback(async () => { + try { + setIsLoading(true); + setError(null); + const result = await getEstimateDetail(id); + if (result.success && result.data) { + setData(result.data); + } else { + setError(result.error || '견적 정보를 불러오는데 실패했습니다.'); } + } catch (err) { + setError(err instanceof Error ? err.message : '오류가 발생했습니다.'); + } finally { + setIsLoading(false); } - fetchData(); }, [id]); + useEffect(() => { + fetchData(); + }, [fetchData]); + if (isLoading) { return (
@@ -45,15 +54,19 @@ export default function EstimateDetailPage({ params }: EstimateDetailPageProps) if (error) { return ( -
-
{error}
-
+ ); } return ( diff --git a/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/edit/page.tsx index 8f0bc930..8a2e7576 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/edit/page.tsx @@ -1,60 +1,27 @@ 'use client'; -import { use, useEffect, useState } from 'react'; +import { use, useEffect } from 'react'; import { useRouter } from 'next/navigation'; -import PartnerForm from '@/components/business/construction/partners/PartnerForm'; -import { getPartner } from '@/components/business/construction/partners/actions'; interface PartnerEditPageProps { params: Promise<{ id: string }>; } +/** + * 하위 호환성을 위한 리다이렉트 페이지 + * /partners/[id]/edit → /partners/[id]?mode=edit + */ export default function PartnerEditPage({ params }: PartnerEditPageProps) { const { id } = use(params); const router = useRouter(); - const [data, setData] = useState>['data']>(undefined); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); useEffect(() => { - getPartner(id) - .then(result => { - if (result.success && result.data) { - setData(result.data); - } else { - setError('협력업체 정보를 찾을 수 없습니다.'); - } - }) - .catch(() => { - setError('협력업체 정보를 불러오는 중 오류가 발생했습니다.'); - }) - .finally(() => setIsLoading(false)); - }, [id]); - - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } - - if (error) { - return ( -
-
{error}
- -
- ); - } + router.replace(`/ko/construction/project/bidding/partners/${id}?mode=edit`); + }, [id, router]); return ( - +
+
리다이렉트 중...
+
); -} \ No newline at end of file +} diff --git a/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/page.tsx index 17f41dd4..e58a6845 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/partners/[id]/page.tsx @@ -1,9 +1,10 @@ 'use client'; -import { use, useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; +import { use, useEffect, useState, useCallback } from 'react'; +import { useSearchParams } from 'next/navigation'; import PartnerForm from '@/components/business/construction/partners/PartnerForm'; import { getPartner } from '@/components/business/construction/partners/actions'; +import { ServerErrorPage } from '@/components/common/ServerErrorPage'; interface PartnerDetailPageProps { params: Promise<{ id: string }>; @@ -11,12 +12,19 @@ interface PartnerDetailPageProps { export default function PartnerDetailPage({ params }: PartnerDetailPageProps) { const { id } = use(params); - const router = useRouter(); + const searchParams = useSearchParams(); + + // V2 패턴: mode 체크 + const mode = searchParams.get('mode') || 'view'; + const isEditMode = mode === 'edit'; + const [data, setData] = useState>['data']>(undefined); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { + const fetchData = useCallback(() => { + setIsLoading(true); + setError(null); getPartner(id) .then(result => { if (result.success && result.data) { @@ -31,6 +39,10 @@ export default function PartnerDetailPage({ params }: PartnerDetailPageProps) { .finally(() => setIsLoading(false)); }, [id]); + useEffect(() => { + fetchData(); + }, [fetchData]); + if (isLoading) { return (
@@ -41,18 +53,19 @@ export default function PartnerDetailPage({ params }: PartnerDetailPageProps) { if (error) { return ( -
-
{error}
- -
+ ); } return ( diff --git a/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/edit/page.tsx index 19cb1f9b..a38c7687 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/edit/page.tsx @@ -1,59 +1,27 @@ 'use client'; -import { use, useEffect, useState } from 'react'; +import { use, useEffect } from 'react'; import { useRouter } from 'next/navigation'; -import { SiteBriefingForm, getSiteBriefing } from '@/components/business/construction/site-briefings'; interface SiteBriefingEditPageProps { params: Promise<{ id: string }>; } +/** + * 하위 호환성을 위한 리다이렉트 페이지 + * /site-briefings/[id]/edit → /site-briefings/[id]?mode=edit + */ export default function SiteBriefingEditPage({ params }: SiteBriefingEditPageProps) { const { id } = use(params); const router = useRouter(); - const [data, setData] = useState>['data']>(undefined); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); useEffect(() => { - getSiteBriefing(id) - .then(result => { - if (result.success && result.data) { - setData(result.data); - } else { - setError('현장 설명회 정보를 찾을 수 없습니다.'); - } - }) - .catch(() => { - setError('현장 설명회 정보를 불러오는 중 오류가 발생했습니다.'); - }) - .finally(() => setIsLoading(false)); - }, [id]); - - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } - - if (error) { - return ( -
-
{error}
- -
- ); - } + router.replace(`/ko/construction/project/bidding/site-briefings/${id}?mode=edit`); + }, [id, router]); return ( - +
+
리다이렉트 중...
+
); -} \ No newline at end of file +} diff --git a/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/page.tsx index 2c8524c0..ac188ba4 100644 --- a/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/bidding/site-briefings/[id]/page.tsx @@ -1,8 +1,9 @@ 'use client'; -import { use, useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; +import { use, useEffect, useState, useCallback } from 'react'; +import { useSearchParams } from 'next/navigation'; import { SiteBriefingForm, getSiteBriefing } from '@/components/business/construction/site-briefings'; +import { ServerErrorPage } from '@/components/common/ServerErrorPage'; interface SiteBriefingDetailPageProps { params: Promise<{ id: string }>; @@ -10,12 +11,19 @@ interface SiteBriefingDetailPageProps { export default function SiteBriefingDetailPage({ params }: SiteBriefingDetailPageProps) { const { id } = use(params); - const router = useRouter(); + const searchParams = useSearchParams(); + + // V2 패턴: mode 체크 + const mode = searchParams.get('mode') || 'view'; + const isEditMode = mode === 'edit'; + const [data, setData] = useState>['data']>(undefined); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { + const fetchData = useCallback(() => { + setIsLoading(true); + setError(null); getSiteBriefing(id) .then(result => { if (result.success && result.data) { @@ -30,6 +38,10 @@ export default function SiteBriefingDetailPage({ params }: SiteBriefingDetailPag .finally(() => setIsLoading(false)); }, [id]); + useEffect(() => { + fetchData(); + }, [fetchData]); + if (isLoading) { return (
@@ -40,18 +52,19 @@ export default function SiteBriefingDetailPage({ params }: SiteBriefingDetailPag if (error) { return ( -
-
{error}
- -
+ ); } return ( diff --git a/src/app/[locale]/(protected)/construction/project/construction-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/project/construction-management/[id]/edit/page.tsx index 8b8285e9..1bd9ff98 100644 --- a/src/app/[locale]/(protected)/construction/project/construction-management/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/construction-management/[id]/edit/page.tsx @@ -1,7 +1,7 @@ 'use client'; -import { use } from 'react'; -import ConstructionDetailClient from '@/components/business/construction/management/ConstructionDetailClient'; +import { use, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; interface PageProps { params: Promise<{ @@ -9,8 +9,21 @@ interface PageProps { }>; } +/** + * 하위 호환성을 위한 리다이렉트 페이지 + * /construction-management/[id]/edit → /construction-management/[id]?mode=edit + */ export default function ConstructionManagementEditPage({ params }: PageProps) { const { id } = use(params); + const router = useRouter(); - return ; -} \ No newline at end of file + useEffect(() => { + router.replace(`/ko/construction/project/construction-management/${id}?mode=edit`); + }, [id, router]); + + return ( +
+
리다이렉트 중...
+
+ ); +} diff --git a/src/app/[locale]/(protected)/construction/project/construction-management/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/construction-management/[id]/page.tsx index 86244c16..d12b3a1c 100644 --- a/src/app/[locale]/(protected)/construction/project/construction-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/construction-management/[id]/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { use } from 'react'; +import { useSearchParams } from 'next/navigation'; import ConstructionDetailClient from '@/components/business/construction/management/ConstructionDetailClient'; interface PageProps { @@ -11,6 +12,11 @@ interface PageProps { export default function ConstructionManagementDetailPage({ params }: PageProps) { const { id } = use(params); + const searchParams = useSearchParams(); - return ; + // V2 패턴: mode 체크 + const mode = searchParams.get('mode') || 'view'; + const isEditMode = mode === 'edit'; + + return ; } diff --git a/src/app/[locale]/(protected)/construction/project/contract/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/project/contract/[id]/edit/page.tsx index 7a1a6b89..6ae2b940 100644 --- a/src/app/[locale]/(protected)/construction/project/contract/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/contract/[id]/edit/page.tsx @@ -1,60 +1,26 @@ 'use client'; -import { use, useEffect, useState } from 'react'; +import { use, useEffect } from 'react'; import { useRouter } from 'next/navigation'; -import ContractDetailForm from '@/components/business/construction/contract/ContractDetailForm'; -import { getContractDetail } from '@/components/business/construction/contract'; interface ContractEditPageProps { params: Promise<{ id: string }>; } +/** + * V2 하위 호환성: /[id]/edit → /[id]?mode=edit 리다이렉트 + */ export default function ContractEditPage({ params }: ContractEditPageProps) { const { id } = use(params); const router = useRouter(); - const [data, setData] = useState>['data']>(undefined); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); useEffect(() => { - getContractDetail(id) - .then(result => { - if (result.success && result.data) { - setData(result.data); - } else { - setError('계약 정보를 찾을 수 없습니다.'); - } - }) - .catch(() => { - setError('계약 정보를 불러오는 중 오류가 발생했습니다.'); - }) - .finally(() => setIsLoading(false)); - }, [id]); - - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } - - if (error) { - return ( -
-
{error}
- -
- ); - } + router.replace(`/construction/project/contract/${id}?mode=edit`); + }, [id, router]); return ( - +
+
리다이렉트 중...
+
); -} \ No newline at end of file +} diff --git a/src/app/[locale]/(protected)/construction/project/contract/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/contract/[id]/page.tsx index 654b0be4..ef91bc85 100644 --- a/src/app/[locale]/(protected)/construction/project/contract/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/contract/[id]/page.tsx @@ -1,9 +1,10 @@ 'use client'; import { use, useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import ContractDetailForm from '@/components/business/construction/contract/ContractDetailForm'; import { getContractDetail } from '@/components/business/construction/contract'; +import { ServerErrorPage } from '@/components/common/ServerErrorPage'; interface ContractDetailPageProps { params: Promise<{ id: string }>; @@ -12,6 +13,8 @@ interface ContractDetailPageProps { export default function ContractDetailPage({ params }: ContractDetailPageProps) { const { id } = use(params); const router = useRouter(); + const searchParams = useSearchParams(); + const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view'; const [data, setData] = useState>['data']>(undefined); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); @@ -41,18 +44,18 @@ export default function ContractDetailPage({ params }: ContractDetailPageProps) if (error) { return ( -
-
{error}
- -
+ ); } return ( diff --git a/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/edit/page.tsx index cbb92c74..edc29255 100644 --- a/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/edit/page.tsx @@ -1,8 +1,7 @@ 'use client'; -import { use, useEffect, useState } from 'react'; +import { use, useEffect } from 'react'; import { useRouter } from 'next/navigation'; -import { HandoverReportDetailForm, getHandoverReportDetail } from '@/components/business/construction/handover-report'; interface HandoverReportEditPageProps { params: Promise<{ @@ -11,52 +10,20 @@ interface HandoverReportEditPageProps { }>; } +/** + * V2 하위 호환성: /[id]/edit → /[id]?mode=edit 리다이렉트 + */ export default function HandoverReportEditPage({ params }: HandoverReportEditPageProps) { const { id } = use(params); const router = useRouter(); - const [data, setData] = useState>['data']>(undefined); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); useEffect(() => { - getHandoverReportDetail(id) - .then(result => { - if (result.data) { - setData(result.data); - } else { - setError('인수인계서 정보를 찾을 수 없습니다.'); - } - }) - .catch(() => { - setError('인수인계서 정보를 불러오는 중 오류가 발생했습니다.'); - }) - .finally(() => setIsLoading(false)); - }, [id]); - - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } - - if (error) { - return ( -
-
{error}
- -
- ); - } + router.replace(`/construction/project/contract/handover-report/${id}?mode=edit`); + }, [id, router]); return ( - +
+
리다이렉트 중...
+
); } diff --git a/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/page.tsx index 03ad8289..98b5cd6a 100644 --- a/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/contract/handover-report/[id]/page.tsx @@ -1,8 +1,9 @@ 'use client'; import { use, useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import { HandoverReportDetailForm, getHandoverReportDetail } from '@/components/business/construction/handover-report'; +import { ServerErrorPage } from '@/components/common/ServerErrorPage'; interface HandoverReportDetailPageProps { params: Promise<{ @@ -14,6 +15,8 @@ interface HandoverReportDetailPageProps { export default function HandoverReportDetailPage({ params }: HandoverReportDetailPageProps) { const { id } = use(params); const router = useRouter(); + const searchParams = useSearchParams(); + const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view'; const [data, setData] = useState>['data']>(undefined); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); @@ -43,18 +46,18 @@ export default function HandoverReportDetailPage({ params }: HandoverReportDetai if (error) { return ( -
-
{error}
- -
+ ); } return ( diff --git a/src/app/[locale]/(protected)/construction/project/issue-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/construction/project/issue-management/[id]/edit/page.tsx index 3b06bdca..d7a87cb6 100644 --- a/src/app/[locale]/(protected)/construction/project/issue-management/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/issue-management/[id]/edit/page.tsx @@ -1,53 +1,24 @@ 'use client'; -import { useEffect, useState } from 'react'; -import { useParams } from 'next/navigation'; -import IssueDetailForm from '@/components/business/construction/issue-management/IssueDetailForm'; -import { getIssue } from '@/components/business/construction/issue-management/actions'; -import type { Issue } from '@/components/business/construction/issue-management/types'; +import { useEffect } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +/** + * 하위 호환성을 위한 리다이렉트 페이지 + * /issue-management/[id]/edit → /issue-management/[id]?mode=edit + */ export default function IssueEditPage() { const params = useParams(); + const router = useRouter(); const id = params.id as string; - const [issue, setIssue] = useState(undefined); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - useEffect(() => { - const loadData = async () => { - try { - const result = await getIssue(id); - if (result.success && result.data) { - setIssue(result.data); - } else { - setError(result.error || '이슈를 찾을 수 없습니다.'); - } - } catch { - setError('이슈 조회에 실패했습니다.'); - } finally { - setIsLoading(false); - } - }; + router.replace(`/ko/construction/project/issue-management/${id}?mode=edit`); + }, [id, router]); - loadData(); - }, [id]); - - if (isLoading) { - return ( -
-
로딩 중...
-
- ); - } - - if (error) { - return ( -
-
{error}
-
- ); - } - - return ; -} \ No newline at end of file + return ( +
+
리다이렉트 중...
+
+ ); +} diff --git a/src/app/[locale]/(protected)/construction/project/issue-management/[id]/page.tsx b/src/app/[locale]/(protected)/construction/project/issue-management/[id]/page.tsx index 79138f40..7dbd2769 100644 --- a/src/app/[locale]/(protected)/construction/project/issue-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/construction/project/issue-management/[id]/page.tsx @@ -1,38 +1,46 @@ 'use client'; -import { useEffect, useState } from 'react'; -import { useParams } from 'next/navigation'; +import { useEffect, useState, useCallback } from 'react'; +import { useParams, useSearchParams } from 'next/navigation'; import IssueDetailForm from '@/components/business/construction/issue-management/IssueDetailForm'; import { getIssue } from '@/components/business/construction/issue-management/actions'; import type { Issue } from '@/components/business/construction/issue-management/types'; +import { ServerErrorPage } from '@/components/common/ServerErrorPage'; export default function IssueDetailPage() { const params = useParams(); + const searchParams = useSearchParams(); const id = params.id as string; + // V2 패턴: mode 체크 + const mode = searchParams.get('mode') || 'view'; + const isEditMode = mode === 'edit'; + const [issue, setIssue] = useState(undefined); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { - const loadData = async () => { - try { - const result = await getIssue(id); - if (result.success && result.data) { - setIssue(result.data); - } else { - setError(result.error || '이슈를 찾을 수 없습니다.'); - } - } catch { - setError('이슈 조회에 실패했습니다.'); - } finally { - setIsLoading(false); + const fetchData = useCallback(async () => { + try { + setIsLoading(true); + setError(null); + const result = await getIssue(id); + if (result.success && result.data) { + setIssue(result.data); + } else { + setError(result.error || '이슈를 찾을 수 없습니다.'); } - }; - - loadData(); + } catch { + setError('이슈 조회에 실패했습니다.'); + } finally { + setIsLoading(false); + } }, [id]); + useEffect(() => { + fetchData(); + }, [fetchData]); + if (isLoading) { return (
@@ -43,11 +51,15 @@ export default function IssueDetailPage() { if (error) { return ( -
-
{error}
-
+ ); } - return ; + return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/customer-center/events/[id]/page.tsx b/src/app/[locale]/(protected)/customer-center/events/[id]/page.tsx index eaca0c4b..a3b8ef95 100644 --- a/src/app/[locale]/(protected)/customer-center/events/[id]/page.tsx +++ b/src/app/[locale]/(protected)/customer-center/events/[id]/page.tsx @@ -1,10 +1,11 @@ 'use client'; import { useParams } from 'next/navigation'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { EventDetail } from '@/components/customer-center/EventManagement'; import { transformPostToEvent, type Event } from '@/components/customer-center/EventManagement/types'; import { getPost } from '@/components/customer-center/shared/actions'; +import { ServerErrorPage } from '@/components/common/ServerErrorPage'; export default function EventDetailPage() { const params = useParams(); @@ -14,38 +15,42 @@ export default function EventDetailPage() { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { - async function fetchEvent() { - setIsLoading(true); - setError(null); + const fetchEvent = useCallback(async () => { + setIsLoading(true); + setError(null); - const result = await getPost('events', eventId); + const result = await getPost('events', eventId); - if (result.success && result.data) { - setEvent(transformPostToEvent(result.data)); - } else { - setError(result.error || '이벤트를 찾을 수 없습니다.'); - } - - setIsLoading(false); + if (result.success && result.data) { + setEvent(transformPostToEvent(result.data)); + } else { + setError(result.error || '이벤트를 찾을 수 없습니다.'); } - fetchEvent(); + setIsLoading(false); }, [eventId]); + useEffect(() => { + fetchEvent(); + }, [fetchEvent]); + if (isLoading) { return ( -
-

로딩 중...

+
+
로딩 중...
); } if (error || !event) { return ( -
-

{error || '이벤트를 찾을 수 없습니다.'}

-
+ ); } diff --git a/src/app/[locale]/(protected)/customer-center/notices/[id]/page.tsx b/src/app/[locale]/(protected)/customer-center/notices/[id]/page.tsx index 3db27901..6deb1e99 100644 --- a/src/app/[locale]/(protected)/customer-center/notices/[id]/page.tsx +++ b/src/app/[locale]/(protected)/customer-center/notices/[id]/page.tsx @@ -1,10 +1,11 @@ 'use client'; import { useParams } from 'next/navigation'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { NoticeDetail } from '@/components/customer-center/NoticeManagement'; import { transformPostToNotice, type Notice } from '@/components/customer-center/NoticeManagement/types'; import { getPost } from '@/components/customer-center/shared/actions'; +import { ServerErrorPage } from '@/components/common/ServerErrorPage'; export default function NoticeDetailPage() { const params = useParams(); @@ -14,38 +15,42 @@ export default function NoticeDetailPage() { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { - async function fetchNotice() { - setIsLoading(true); - setError(null); + const fetchNotice = useCallback(async () => { + setIsLoading(true); + setError(null); - const result = await getPost('notices', id); + const result = await getPost('notices', id); - if (result.success && result.data) { - setNotice(transformPostToNotice(result.data)); - } else { - setError(result.error || '공지사항을 찾을 수 없습니다.'); - } - - setIsLoading(false); + if (result.success && result.data) { + setNotice(transformPostToNotice(result.data)); + } else { + setError(result.error || '공지사항을 찾을 수 없습니다.'); } - fetchNotice(); + setIsLoading(false); }, [id]); + useEffect(() => { + fetchNotice(); + }, [fetchNotice]); + if (isLoading) { return ( -
-

로딩 중...

+
+
로딩 중...
); } if (error || !notice) { return ( -
-

{error || '공지사항을 찾을 수 없습니다.'}

-
+ ); } diff --git a/src/app/[locale]/(protected)/customer-center/qna/[id]/edit/page.tsx b/src/app/[locale]/(protected)/customer-center/qna/[id]/edit/page.tsx index bf2770da..7d5f35cb 100644 --- a/src/app/[locale]/(protected)/customer-center/qna/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/customer-center/qna/[id]/edit/page.tsx @@ -1,57 +1,27 @@ 'use client'; +import { use, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; + +interface EditInquiryPageProps { + params: Promise<{ id: string }>; +} + /** - * 1:1 문의 수정 페이지 + * 하위 호환성을 위한 리다이렉트 페이지 + * /qna/[id]/edit → /qna/[id]?mode=edit */ - -import { useParams } from 'next/navigation'; -import { useState, useEffect } from 'react'; -import { InquiryForm } from '@/components/customer-center/InquiryManagement'; -import { transformPostToInquiry, type Inquiry } from '@/components/customer-center/InquiryManagement/types'; -import { getPost } from '@/components/customer-center/shared/actions'; - -export default function InquiryEditPage() { - const params = useParams(); - const inquiryId = params.id as string; - - const [inquiry, setInquiry] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); +export default function EditInquiryPage({ params }: EditInquiryPageProps) { + const { id } = use(params); + const router = useRouter(); useEffect(() => { - async function fetchInquiry() { - setIsLoading(true); - setError(null); + router.replace(`/ko/customer-center/qna/${id}?mode=edit`); + }, [id, router]); - const result = await getPost('qna', inquiryId); - - if (result.success && result.data) { - setInquiry(transformPostToInquiry(result.data)); - } else { - setError(result.error || '문의를 찾을 수 없습니다.'); - } - - setIsLoading(false); - } - - fetchInquiry(); - }, [inquiryId]); - - if (isLoading) { - return ( -
-

로딩 중...

-
- ); - } - - if (error || !inquiry) { - return ( -
-

{error || '문의를 찾을 수 없습니다.'}

-
- ); - } - - return ; + return ( +
+
리다이렉트 중...
+
+ ); } diff --git a/src/app/[locale]/(protected)/customer-center/qna/[id]/page.tsx b/src/app/[locale]/(protected)/customer-center/qna/[id]/page.tsx index 82ed3455..be3cb8e0 100644 --- a/src/app/[locale]/(protected)/customer-center/qna/[id]/page.tsx +++ b/src/app/[locale]/(protected)/customer-center/qna/[id]/page.tsx @@ -1,135 +1,20 @@ 'use client'; /** - * 1:1 문의 상세 페이지 + * 1:1 문의 상세 페이지 (V2) + * + * /qna/[id] → 조회 모드 + * /qna/[id]?mode=edit → 수정 모드 */ -import { useParams } from 'next/navigation'; -import { useState, useEffect, useCallback } from 'react'; -import { InquiryDetail } from '@/components/customer-center/InquiryManagement'; -import { transformPostToInquiry, type Inquiry, type Comment } from '@/components/customer-center/InquiryManagement/types'; -import { getPost, getComments, createComment, updateComment, deleteComment, deletePost } from '@/components/customer-center/shared/actions'; -import { transformApiToComment } from '@/components/customer-center/shared/types'; +import { use } from 'react'; +import { InquiryDetailClientV2 } from '@/components/customer-center/InquiryManagement'; -export default function InquiryDetailPage() { - const params = useParams(); - const inquiryId = params.id as string; - - const [inquiry, setInquiry] = useState(null); - const [comments, setComments] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [currentUserId, setCurrentUserId] = useState(''); - - // 현재 사용자 ID 가져오기 (localStorage에서) - useEffect(() => { - const userStr = localStorage.getItem('user'); - if (userStr) { - try { - const user = JSON.parse(userStr); - // user.id는 실제 DB user ID (숫자) - setCurrentUserId(String(user.id || '')); - } catch { - setCurrentUserId(''); - } - } - }, []); - - // 데이터 로드 - useEffect(() => { - async function fetchData() { - setIsLoading(true); - setError(null); - - // 게시글과 댓글 동시 로드 - const [postResult, commentsResult] = await Promise.all([ - getPost('qna', inquiryId), - getComments('qna', inquiryId), - ]); - - if (postResult.success && postResult.data) { - setInquiry(transformPostToInquiry(postResult.data)); - } else { - setError(postResult.error || '문의를 찾을 수 없습니다.'); - } - - if (commentsResult.success && commentsResult.data) { - setComments(commentsResult.data.map(transformApiToComment)); - } - - setIsLoading(false); - } - - fetchData(); - }, [inquiryId]); - - // 댓글 추가 - const handleAddComment = useCallback(async (content: string) => { - const result = await createComment('qna', inquiryId, content); - if (result.success && result.data) { - setComments((prev) => [...prev, transformApiToComment(result.data!)]); - } else { - console.error('댓글 등록 실패:', result.error); - } - }, [inquiryId]); - - // 댓글 수정 - const handleUpdateComment = useCallback(async (commentId: string, content: string) => { - const result = await updateComment('qna', inquiryId, commentId, content); - if (result.success && result.data) { - setComments((prev) => - prev.map((c) => (c.id === commentId ? transformApiToComment(result.data!) : c)) - ); - } else { - console.error('댓글 수정 실패:', result.error); - } - }, [inquiryId]); - - // 댓글 삭제 - const handleDeleteComment = useCallback(async (commentId: string) => { - const result = await deleteComment('qna', inquiryId, commentId); - if (result.success) { - setComments((prev) => prev.filter((c) => c.id !== commentId)); - } else { - console.error('댓글 삭제 실패:', result.error); - } - }, [inquiryId]); - - // 문의 삭제 - const handleDeleteInquiry = useCallback(async () => { - const result = await deletePost('qna', inquiryId); - return result.success; - }, [inquiryId]); - - if (isLoading) { - return ( -
-

로딩 중...

-
- ); - } - - if (error || !inquiry) { - return ( -
-

{error || '문의를 찾을 수 없습니다.'}

-
- ); - } - - // 답변은 추후 API 추가 시 구현 - const reply = undefined; - - return ( - - ); +interface InquiryDetailPageProps { + params: Promise<{ id: string }>; +} + +export default function InquiryDetailPage({ params }: InquiryDetailPageProps) { + const { id } = use(params); + return ; } diff --git a/src/app/[locale]/(protected)/customer-center/qna/create/page.tsx b/src/app/[locale]/(protected)/customer-center/qna/create/page.tsx index 60a1e738..2faf536a 100644 --- a/src/app/[locale]/(protected)/customer-center/qna/create/page.tsx +++ b/src/app/[locale]/(protected)/customer-center/qna/create/page.tsx @@ -1,14 +1,11 @@ +'use client'; + /** - * 1:1 문의 등록 페이지 + * 1:1 문의 등록 페이지 (V2) */ -import { InquiryForm } from '@/components/customer-center/InquiryManagement'; +import { InquiryDetailClientV2 } from '@/components/customer-center/InquiryManagement'; export default function InquiryCreatePage() { - return ; + return ; } - -export const metadata = { - title: '1:1 문의 등록', - description: '1:1 문의를 등록합니다.', -}; \ No newline at end of file diff --git a/src/app/[locale]/(protected)/dashboard/page.tsx.backup b/src/app/[locale]/(protected)/dashboard/page.tsx.backup deleted file mode 100644 index 4a4ee750..00000000 --- a/src/app/[locale]/(protected)/dashboard/page.tsx.backup +++ /dev/null @@ -1,101 +0,0 @@ -"use client"; - -import { useTranslations } from 'next-intl'; -import { useRouter } from 'next/navigation'; -import LanguageSwitcher from '@/components/LanguageSwitcher'; -import WelcomeMessage from '@/components/WelcomeMessage'; -import NavigationMenu from '@/components/NavigationMenu'; -import { Button } from '@/components/ui/button'; -import { LogOut } from 'lucide-react'; - -/** - * Dashboard Page with Internationalization - * - * Note: Authentication protection is handled by (protected)/layout.tsx - */ -export default function Dashboard() { - const t = useTranslations('common'); - const router = useRouter(); - - const handleLogout = async () => { - try { - // ✅ HttpOnly Cookie 방식: Next.js API Route로 프록시 - const response = await fetch('/api/auth/logout', { - method: 'POST', - }); - - if (response.ok) { - console.log('✅ 로그아웃 완료: HttpOnly 쿠키 삭제됨'); - } - - // 로그인 페이지로 리다이렉트 - router.push('/login'); - } catch (error) { - console.error('로그아웃 처리 중 오류:', error); - // 에러가 나도 로그인 페이지로 이동 - router.push('/login'); - } - }; - - return ( -
-
- {/* Header with Language Switcher */} -
-

- {t('appName')} -

-
- - -
-
- - {/* Main Content */} -
- {/* Welcome Section */} - - - {/* Navigation Menu */} -
-

- {t('appName')} Modules -

- -
- - {/* Information Section */} -
-

- Multi-language Support -

-

- This ERP system supports Korean (한국어), English, and Japanese (日本語). - Use the language switcher above to change the interface language. -

-
- - {/* Developer Info */} -
-

- For Developers -

-

- Check the documentation in claudedocs/i18n-usage-guide.md -

-

- Message files: src/messages/ -

-
-
-
-
- ); -} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/hr/card-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/hr/card-management/[id]/edit/page.tsx index ce079823..edc6ab74 100644 --- a/src/app/[locale]/(protected)/hr/card-management/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/hr/card-management/[id]/edit/page.tsx @@ -1,63 +1,22 @@ 'use client'; +/** + * 카드 수정 페이지 - 상세 페이지로 리다이렉트 + * + * IntegratedDetailTemplate 통합으로 인해 [id]?mode=edit로 처리됨 + */ + +import { useEffect } from 'react'; import { useRouter, useParams } from 'next/navigation'; -import { useState, useEffect } from 'react'; -import { CardForm } from '@/components/hr/CardManagement/CardForm'; -import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; -import { toast } from 'sonner'; -import type { Card, CardFormData } from '@/components/hr/CardManagement/types'; -import { getCard, updateCard } from '@/components/hr/CardManagement/actions'; export default function CardEditPage() { const router = useRouter(); const params = useParams(); - const [card, setCard] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); useEffect(() => { - const loadCard = async () => { - if (!params.id) return; + // 상세 페이지의 edit 모드로 리다이렉트 + router.replace(`/ko/hr/card-management/${params.id}?mode=edit`); + }, [router, params.id]); - setIsLoading(true); - const result = await getCard(params.id as string); - - if (result.success && result.data) { - setCard(result.data); - } else { - toast.error(result.error || '카드 정보를 불러오는데 실패했습니다.'); - router.push('/ko/hr/card-management'); - } - setIsLoading(false); - }; - - loadCard(); - }, [params.id, router]); - - const handleSubmit = async (data: CardFormData) => { - if (!params.id) return; - - setIsSaving(true); - const result = await updateCard(params.id as string, data); - - if (result.success) { - toast.success('카드가 수정되었습니다.'); - router.push(`/ko/hr/card-management/${params.id}`); - } else { - toast.error(result.error || '수정에 실패했습니다.'); - } - setIsSaving(false); - }; - - if (isLoading || !card) { - return ; - } - - return ( - - ); -} \ No newline at end of file + return null; +} diff --git a/src/app/[locale]/(protected)/hr/card-management/[id]/page.tsx b/src/app/[locale]/(protected)/hr/card-management/[id]/page.tsx index 5f6935ee..d449ca62 100644 --- a/src/app/[locale]/(protected)/hr/card-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/hr/card-management/[id]/page.tsx @@ -1,110 +1,88 @@ 'use client'; -import { useRouter, useParams } from 'next/navigation'; -import { useState, useEffect } from 'react'; -import { CardDetail } from '@/components/hr/CardManagement/CardDetail'; +/** + * 카드 상세/수정 페이지 - IntegratedDetailTemplate 적용 + */ + +import { useEffect, useState } from 'react'; +import { useParams, useSearchParams } from 'next/navigation'; +import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; +import { cardConfig } from '@/components/hr/CardManagement/cardConfig'; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; -import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; -import { toast } from 'sonner'; -import type { Card } from '@/components/hr/CardManagement/types'; -import { getCard, deleteCard } from '@/components/hr/CardManagement/actions'; + getCard, + updateCard, + deleteCard, +} from '@/components/hr/CardManagement/actions'; +import type { Card, CardFormData } from '@/components/hr/CardManagement/types'; +import type { DetailMode } from '@/components/templates/IntegratedDetailTemplate'; export default function CardDetailPage() { - const router = useRouter(); const params = useParams(); + const searchParams = useSearchParams(); + const cardId = params.id as string; + const [card, setCard] = useState(null); const [isLoading, setIsLoading] = useState(true); - const [isDeleting, setIsDeleting] = useState(false); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [error, setError] = useState(null); + // URL에서 mode 파라미터 확인 (?mode=edit) + const urlMode = searchParams.get('mode'); + const initialMode: DetailMode = urlMode === 'edit' ? 'edit' : 'view'; + + // 데이터 로드 useEffect(() => { - const loadCard = async () => { - if (!params.id) return; - + async function loadCard() { setIsLoading(true); - const result = await getCard(params.id as string); - - if (result.success && result.data) { - setCard(result.data); - } else { - toast.error(result.error || '카드 정보를 불러오는데 실패했습니다.'); - router.push('/ko/hr/card-management'); + try { + const result = await getCard(cardId); + if (result.success && result.data) { + setCard(result.data); + } else { + setError(result.error || '카드를 찾을 수 없습니다.'); + } + } catch (err) { + console.error('Failed to load card:', err); + setError('카드 조회 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); } - setIsLoading(false); - }; + } loadCard(); - }, [params.id, router]); + }, [cardId]); - const handleEdit = () => { - router.push(`/ko/hr/card-management/${params.id}/edit`); + // 수정 핸들러 + const handleSubmit = async (data: Record) => { + const result = await updateCard(cardId, data as Partial); + return { success: result.success, error: result.error }; }; - const handleDelete = () => { - setDeleteDialogOpen(true); + // 삭제 핸들러 + const handleDelete = async () => { + const result = await deleteCard(cardId); + return { success: result.success, error: result.error }; }; - const confirmDelete = async () => { - if (!params.id) return; - - setIsDeleting(true); - const result = await deleteCard(params.id as string); - - if (result.success) { - toast.success('카드가 삭제되었습니다.'); - router.push('/ko/hr/card-management'); - } else { - toast.error(result.error || '삭제에 실패했습니다.'); - setIsDeleting(false); - setDeleteDialogOpen(false); - } - }; - - if (isLoading || !card) { - return ; + // 에러 상태 + if (error && !isLoading) { + return ( +
+
+ {error} +
+
+ ); } return ( - <> - - - - - - 카드 삭제 - - "{card.cardName}" 카드를 삭제하시겠습니까? -
- - 삭제된 카드 정보는 복구할 수 없습니다. - -
-
- - 취소 - - {isDeleting ? '삭제 중...' : '삭제'} - - -
-
- + ); -} \ No newline at end of file +} diff --git a/src/app/[locale]/(protected)/hr/card-management/loading.tsx b/src/app/[locale]/(protected)/hr/card-management/loading.tsx new file mode 100644 index 00000000..51c6a92f --- /dev/null +++ b/src/app/[locale]/(protected)/hr/card-management/loading.tsx @@ -0,0 +1,58 @@ +import { Card, CardContent, CardHeader } from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; + +/** + * 카드관리 페이지 로딩 UI (Skeleton) + * + * 페이지 전환 시 스피너 대신 Skeleton으로 일관된 UX 제공 + */ +export default function CardManagementLoading() { + return ( +
+ {/* 헤더 Skeleton */} +
+ + +
+ + {/* 기본 정보 카드 Skeleton */} + + + + + +
+ {[1, 2, 3, 4, 5, 6].map((i) => ( +
+ + +
+ ))} +
+
+
+ + {/* 사용자 정보 카드 Skeleton */} + + + + + +
+ + +
+
+
+ + {/* 버튼 영역 Skeleton */} +
+ +
+ + +
+
+
+ ); +} diff --git a/src/app/[locale]/(protected)/hr/card-management/new/page.tsx b/src/app/[locale]/(protected)/hr/card-management/new/page.tsx index 704c3ce3..0a1cbb4b 100644 --- a/src/app/[locale]/(protected)/hr/card-management/new/page.tsx +++ b/src/app/[locale]/(protected)/hr/card-management/new/page.tsx @@ -1,33 +1,25 @@ 'use client'; -import { useRouter } from 'next/navigation'; -import { useState } from 'react'; -import { CardForm } from '@/components/hr/CardManagement/CardForm'; -import { toast } from 'sonner'; -import type { CardFormData } from '@/components/hr/CardManagement/types'; +/** + * 카드 등록 페이지 - IntegratedDetailTemplate 적용 + */ + +import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; +import { cardConfig } from '@/components/hr/CardManagement/cardConfig'; import { createCard } from '@/components/hr/CardManagement/actions'; +import type { CardFormData } from '@/components/hr/CardManagement/types'; -export default function CardNewPage() { - const router = useRouter(); - const [isSaving, setIsSaving] = useState(false); - - const handleSubmit = async (data: CardFormData) => { - setIsSaving(true); - const result = await createCard(data); - - if (result.success) { - toast.success('카드가 등록되었습니다.'); - router.push('/ko/hr/card-management'); - } else { - toast.error(result.error || '등록에 실패했습니다.'); - } - setIsSaving(false); +export default function NewCardPage() { + const handleSubmit = async (data: Record) => { + const result = await createCard(data as CardFormData); + return { success: result.success, error: result.error }; }; return ( - ); -} \ No newline at end of file +} diff --git a/src/app/[locale]/(protected)/hr/employee-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/hr/employee-management/[id]/edit/page.tsx index 997f7a4f..bf03b2d0 100644 --- a/src/app/[locale]/(protected)/hr/employee-management/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/hr/employee-management/[id]/edit/page.tsx @@ -1,70 +1,23 @@ 'use client'; +import { useEffect } from 'react'; import { useRouter, useParams } from 'next/navigation'; -import { useState, useEffect, useCallback } from 'react'; -import { EmployeeForm } from '@/components/hr/EmployeeManagement/EmployeeForm'; -import { getEmployeeById, updateEmployee } from '@/components/hr/EmployeeManagement/actions'; -import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; -import type { Employee, EmployeeFormData } from '@/components/hr/EmployeeManagement/types'; +/** + * V2 하위 호환성: /[id]/edit → /[id]?mode=edit 리다이렉트 + */ export default function EmployeeEditPage() { const router = useRouter(); const params = useParams(); - const [employee, setEmployee] = useState(null); - const [isLoading, setIsLoading] = useState(true); - - // 직원 데이터 조회 - const fetchEmployee = useCallback(async () => { - const id = params.id as string; - if (!id) return; - - setIsLoading(true); - try { - const data = await getEmployeeById(id); - // __authError 처리 - if (data && '__authError' in data) { - router.push('/ko/login'); - return; - } - setEmployee(data); - } catch (error) { - console.error('[EmployeeEditPage] fetchEmployee error:', error); - } finally { - setIsLoading(false); - } - }, [params.id, router]); + const id = params.id as string; useEffect(() => { - fetchEmployee(); - }, [fetchEmployee]); + router.replace(`/ko/hr/employee-management/${id}?mode=edit`); + }, [id, router]); - const handleSave = async (data: EmployeeFormData) => { - const id = params.id as string; - if (!id) return; - - try { - const result = await updateEmployee(id, data); - if (result.success) { - router.push(`/ko/hr/employee-management/${id}`); - } else { - console.error('[EmployeeEditPage] Update failed:', result.error); - } - } catch (error) { - console.error('[EmployeeEditPage] Update error:', error); - } - }; - - if (isLoading) { - return ; - } - - if (!employee) { - return ( -
-

사원 정보를 찾을 수 없습니다.

-
- ); - } - - return ; -} \ No newline at end of file + return ( +
+
리다이렉트 중...
+
+ ); +} diff --git a/src/app/[locale]/(protected)/hr/employee-management/[id]/page.tsx b/src/app/[locale]/(protected)/hr/employee-management/[id]/page.tsx index fd0646d5..aa321783 100644 --- a/src/app/[locale]/(protected)/hr/employee-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/hr/employee-management/[id]/page.tsx @@ -1,9 +1,10 @@ 'use client'; -import { useRouter, useParams } from 'next/navigation'; +import { useRouter, useParams, useSearchParams } from 'next/navigation'; import { useState, useEffect, useCallback } from 'react'; import { EmployeeForm } from '@/components/hr/EmployeeManagement/EmployeeForm'; -import { getEmployeeById, deleteEmployee } from '@/components/hr/EmployeeManagement/actions'; +import { getEmployeeById, deleteEmployee, updateEmployee } from '@/components/hr/EmployeeManagement/actions'; +import { ServerErrorPage } from '@/components/common/ServerErrorPage'; import { AlertDialog, AlertDialogAction, @@ -15,11 +16,13 @@ import { AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; -import type { Employee } from '@/components/hr/EmployeeManagement/types'; +import type { Employee, EmployeeFormData } from '@/components/hr/EmployeeManagement/types'; export default function EmployeeDetailPage() { const router = useRouter(); const params = useParams(); + const searchParams = useSearchParams(); + const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view'; const [employee, setEmployee] = useState(null); const [isLoading, setIsLoading] = useState(true); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); @@ -51,7 +54,23 @@ export default function EmployeeDetailPage() { }, [fetchEmployee]); const handleEdit = () => { - router.push(`/ko/hr/employee-management/${params.id}/edit`); + router.push(`/ko/hr/employee-management/${params.id}?mode=edit`); + }; + + const handleSave = async (data: EmployeeFormData) => { + const id = params.id as string; + if (!id) return; + + try { + const result = await updateEmployee(id, data); + if (result.success) { + router.push(`/ko/hr/employee-management/${id}`); + } else { + console.error('[EmployeeDetailPage] Update failed:', result.error); + } + } catch (error) { + console.error('[EmployeeDetailPage] Update error:', error); + } }; const handleDelete = () => { @@ -84,19 +103,24 @@ export default function EmployeeDetailPage() { if (!employee) { return ( -
-

사원 정보를 찾을 수 없습니다.

-
+ ); } return ( <> diff --git a/src/app/[locale]/(protected)/items/create/page.tsx b/src/app/[locale]/(protected)/items/create/page.tsx deleted file mode 100644 index a14c0810..00000000 --- a/src/app/[locale]/(protected)/items/create/page.tsx +++ /dev/null @@ -1,101 +0,0 @@ -/** - * 품목 등록 페이지 - * - * DynamicItemForm을 사용하여 품목기준관리 데이터 기반 동적 폼 렌더링 - */ - -'use client'; - -import { useState } from 'react'; -import DynamicItemForm from '@/components/items/DynamicItemForm'; -import type { DynamicFormData } from '@/components/items/DynamicItemForm/types'; -// 2025-12-16: options 관련 변환 로직 제거 -// 백엔드가 품목기준관리 field_key 매핑을 처리하므로 프론트에서 변환 불필요 -import { DuplicateCodeError } from '@/lib/api/error-handler'; - -// 기존 ItemForm (주석처리 - 롤백 시 사용) -// import ItemForm from '@/components/items/ItemForm'; -// import type { CreateItemFormData } from '@/lib/utils/validation'; - -export default function CreateItemPage() { - const [submitError, setSubmitError] = useState(null); - - const handleSubmit = async (data: DynamicFormData) => { - setSubmitError(null); - - // 필드명 변환: spec → specification (백엔드 API 규격) - const submitData = { ...data }; - if (submitData.spec !== undefined) { - submitData.specification = submitData.spec; - delete submitData.spec; - } - - // 2025-12-15: item_type은 Request Body에서 필수 (ItemService.store validation) - // product_type과 item_type을 동일하게 설정 - const itemType = submitData.product_type as string; - submitData.item_type = itemType; - - // API 호출 전 이미지 데이터 제거 (파일 업로드는 별도 API 사용) - // bending_diagram이 base64 데이터인 경우 제거 (JSON에 포함시키면 안됨) - if (submitData.bending_diagram && typeof submitData.bending_diagram === 'string' && (submitData.bending_diagram as string).startsWith('data:')) { - delete submitData.bending_diagram; - } - // 시방서/인정서 파일 필드도 base64면 제거 - if (submitData.specification_file && typeof submitData.specification_file === 'string' && (submitData.specification_file as string).startsWith('data:')) { - delete submitData.specification_file; - } - if (submitData.certification_file && typeof submitData.certification_file === 'string' && (submitData.certification_file as string).startsWith('data:')) { - delete submitData.certification_file; - } - - // API 호출: POST /api/proxy/items - // 백엔드에서 product_type에 따라 Product/Material 분기 처리 - const response = await fetch('/api/proxy/items', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(submitData), - }); - - const result = await response.json(); - - if (!response.ok || !result.success) { - // 2025-12-11: 백엔드 중복 에러 처리 (DuplicateCodeException) - // duplicate_id가 있으면 DuplicateCodeError throw → DynamicItemForm에서 다이얼로그 표시 - if (response.status === 400 && result.duplicate_id) { - console.warn('[CreateItemPage] 품목코드 중복 에러:', result); - throw new DuplicateCodeError( - result.message || '해당 품목코드가 이미 존재합니다.', - result.duplicate_id, - result.duplicate_code - ); - } - - const errorMessage = result.message || '품목 등록에 실패했습니다.'; - console.error('[CreateItemPage] 품목 등록 실패:', errorMessage); - setSubmitError(errorMessage); - throw new Error(errorMessage); - } - - // 성공 시 DynamicItemForm 내부에서 /items로 리다이렉트 처리됨 - // console.log('[CreateItemPage] 품목 등록 성공:', result.data); - - // 생성된 품목 ID를 포함한 데이터 반환 (파일 업로드용) - return { id: result.data.id, ...result.data }; - }; - - return ( -
- {submitError && ( -
- ⚠️ {submitError} -
- )} - -
- ); -} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/items/page.tsx b/src/app/[locale]/(protected)/items/page.tsx deleted file mode 100644 index d0ffef41..00000000 --- a/src/app/[locale]/(protected)/items/page.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/** - * 품목 관리 페이지 - * - * 품목기준관리 API 연동 - * - 품목 목록: API에서 조회 - * - 테이블 컬럼: custom-tabs API에서 동적 구성 - */ - -import ItemListClient from '@/components/items/ItemListClient'; - -/** - * 품목 목록 페이지 - */ -export default function ItemsPage() { - return ; -} - -/** - * 메타데이터 설정 - */ -export const metadata = { - title: '품목 관리', - description: '품목 목록 조회 및 관리', -}; \ No newline at end of file diff --git a/src/app/[locale]/(protected)/loading.tsx b/src/app/[locale]/(protected)/loading.tsx index 9927fc03..0a1d6586 100644 --- a/src/app/[locale]/(protected)/loading.tsx +++ b/src/app/[locale]/(protected)/loading.tsx @@ -8,7 +8,10 @@ import { PageLoadingSpinner } from '@/components/ui/loading-spinner'; * - React Suspense 자동 적용 * - 페이지 전환 시 즉각적인 피드백 * - 공통 레이아웃 스타일로 통일 (min-h-[calc(100vh-200px)]) + * + * Note: 특정 경로에서 Skeleton UI를 사용하려면 해당 경로에 + * 별도의 loading.tsx를 생성하세요. (예: settings/accounts/loading.tsx) */ export default function ProtectedLoading() { return ; -} \ No newline at end of file +} diff --git a/src/app/[locale]/(protected)/master-data/process-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/master-data/process-management/[id]/edit/page.tsx index 24457f13..81dc5bb1 100644 --- a/src/app/[locale]/(protected)/master-data/process-management/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/master-data/process-management/[id]/edit/page.tsx @@ -1,62 +1,27 @@ 'use client'; /** - * 공정 수정 페이지 (Client Component) + * 공정관리 수정 페이지 (리다이렉트) + * + * 기존 URL 호환성을 위해 유지 + * /[id]/edit → /[id]?mode=edit 로 리다이렉트 */ -import { use, useEffect, useState } from 'react'; +import { use, useEffect } from 'react'; import { useRouter } from 'next/navigation'; -import { ProcessForm } from '@/components/process-management'; -import { getProcessById } from '@/components/process-management/actions'; -import type { Process } from '@/components/process-management/types'; +import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; -export default function EditProcessPage({ +export default function ProcessEditRedirectPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = use(params); const router = useRouter(); - const [data, setData] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); useEffect(() => { - getProcessById(id) - .then(result => { - if (result.success && result.data) { - setData(result.data); - } else { - setError('공정 정보를 찾을 수 없습니다.'); - } - }) - .catch(() => { - setError('공정 정보를 불러오는 중 오류가 발생했습니다.'); - }) - .finally(() => setIsLoading(false)); - }, [id]); + router.replace(`/ko/master-data/process-management/${id}?mode=edit`); + }, [router, id]); - if (isLoading) { - return ( -
-
공정 정보를 불러오는 중...
-
- ); - } - - if (error || !data) { - return ( -
-
{error || '공정을 찾을 수 없습니다.'}
- -
- ); - } - - return ; -} \ No newline at end of file + return ; +} diff --git a/src/app/[locale]/(protected)/master-data/process-management/[id]/page.tsx b/src/app/[locale]/(protected)/master-data/process-management/[id]/page.tsx index 8f5f70f9..9a28455f 100644 --- a/src/app/[locale]/(protected)/master-data/process-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/master-data/process-management/[id]/page.tsx @@ -1,14 +1,15 @@ 'use client'; /** - * 공정 상세 페이지 (Client Component) + * 공정관리 상세/수정 페이지 + * + * IntegratedDetailTemplate V2 마이그레이션 + * - /[id] → 상세 보기 (view 모드) + * - /[id]?mode=edit → 수정 (edit 모드) */ -import { use, useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { ProcessDetail } from '@/components/process-management'; -import { getProcessById } from '@/components/process-management/actions'; -import type { Process } from '@/components/process-management/types'; +import { use } from 'react'; +import { ProcessDetailClientV2 } from '@/components/process-management'; export default function ProcessDetailPage({ params, @@ -16,47 +17,6 @@ export default function ProcessDetailPage({ params: Promise<{ id: string }>; }) { const { id } = use(params); - const router = useRouter(); - const [data, setData] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - useEffect(() => { - getProcessById(id) - .then(result => { - if (result.success && result.data) { - setData(result.data); - } else { - setError('공정 정보를 찾을 수 없습니다.'); - } - }) - .catch(() => { - setError('공정 정보를 불러오는 중 오류가 발생했습니다.'); - }) - .finally(() => setIsLoading(false)); - }, [id]); - - if (isLoading) { - return ( -
-
공정 정보를 불러오는 중...
-
- ); - } - - if (error || !data) { - return ( -
-
{error || '공정을 찾을 수 없습니다.'}
- -
- ); - } - - return ; -} \ No newline at end of file + return ; +} diff --git a/src/app/[locale]/(protected)/master-data/process-management/new/page.tsx b/src/app/[locale]/(protected)/master-data/process-management/new/page.tsx index f6984856..3e4468cd 100644 --- a/src/app/[locale]/(protected)/master-data/process-management/new/page.tsx +++ b/src/app/[locale]/(protected)/master-data/process-management/new/page.tsx @@ -1,11 +1,13 @@ -/** - * 공정 등록 페이지 - */ - 'use client'; -import { ProcessForm } from '@/components/process-management'; +/** + * 공정관리 등록 페이지 + * + * IntegratedDetailTemplate V2 마이그레이션 + */ -export default function CreateProcessPage() { - return ; -} \ No newline at end of file +import { ProcessDetailClientV2 } from '@/components/process-management'; + +export default function ProcessCreatePage() { + return ; +} diff --git a/src/app/[locale]/(protected)/outbound/shipments/[id]/edit/page.tsx b/src/app/[locale]/(protected)/outbound/shipments/[id]/edit/page.tsx index 897df92e..924dde6a 100644 --- a/src/app/[locale]/(protected)/outbound/shipments/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/outbound/shipments/[id]/edit/page.tsx @@ -1,18 +1,26 @@ 'use client'; -/** - * 출하관리 - 수정 페이지 (Client Component) - * URL: /outbound/shipments/[id]/edit - */ - -import { use } from 'react'; -import { ShipmentEdit } from '@/components/outbound/ShipmentManagement'; +import { use, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; interface ShipmentEditPageProps { params: Promise<{ id: string }>; } +/** + * V2 하위 호환성: /[id]/edit → /[id]?mode=edit 리다이렉트 + */ export default function ShipmentEditPage({ params }: ShipmentEditPageProps) { const { id } = use(params); - return ; -} \ No newline at end of file + const router = useRouter(); + + useEffect(() => { + router.replace(`/outbound/shipments/${id}?mode=edit`); + }, [id, router]); + + return ( +
+
리다이렉트 중...
+
+ ); +} diff --git a/src/app/[locale]/(protected)/outbound/shipments/[id]/page.tsx b/src/app/[locale]/(protected)/outbound/shipments/[id]/page.tsx index c6961b5b..c0c642d0 100644 --- a/src/app/[locale]/(protected)/outbound/shipments/[id]/page.tsx +++ b/src/app/[locale]/(protected)/outbound/shipments/[id]/page.tsx @@ -3,10 +3,12 @@ /** * 출하관리 - 상세 페이지 (Client Component) * URL: /outbound/shipments/[id] + * V2 패턴: ?mode=edit로 수정 모드 전환 */ import { use } from 'react'; -import { ShipmentDetail } from '@/components/outbound/ShipmentManagement'; +import { useSearchParams } from 'next/navigation'; +import { ShipmentDetail, ShipmentEdit } from '@/components/outbound/ShipmentManagement'; interface ShipmentDetailPageProps { params: Promise<{ id: string }>; @@ -14,5 +16,13 @@ interface ShipmentDetailPageProps { export default function ShipmentDetailPage({ params }: ShipmentDetailPageProps) { const { id } = use(params); + const searchParams = useSearchParams(); + const mode = searchParams.get('mode'); + const isEditMode = mode === 'edit'; + + if (isEditMode) { + return ; + } + return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/production/screen-production/[id]/edit/page.tsx b/src/app/[locale]/(protected)/production/screen-production/[id]/edit/page.tsx index 4183f961..36e14944 100644 --- a/src/app/[locale]/(protected)/production/screen-production/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/production/screen-production/[id]/edit/page.tsx @@ -1,205 +1,32 @@ -/** - * 품목 수정 페이지 - */ - 'use client'; -import { useEffect, useState } from 'react'; -import { useParams, useRouter } from 'next/navigation'; -import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; -import ItemForm from '@/components/items/ItemForm'; -import type { ItemMaster } from '@/types/item'; -import type { CreateItemFormData } from '@/lib/utils/validation'; +import { use, useEffect } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; -// Mock 데이터 (API 연동 전 임시) -const mockItems: ItemMaster[] = [ - { - id: '1', - itemCode: 'KD-FG-001', - itemName: '스크린 제품 A', - itemType: 'FG', - unit: 'EA', - specification: '2000x2000', - isActive: true, - category1: '본체부품', - category2: '가이드시스템', - salesPrice: 150000, - purchasePrice: 100000, - marginRate: 33.3, - processingCost: 20000, - laborCost: 15000, - installCost: 10000, - productCategory: 'SCREEN', - lotAbbreviation: 'KD', - note: '스크린 제품 샘플입니다.', - safetyStock: 10, - leadTime: 7, - currentRevision: 0, - isFinal: false, - createdAt: '2025-01-10T00:00:00Z', - updatedAt: '2025-01-12T00:00:00Z', - bom: [ - { - id: 'bom-1', - childItemCode: 'KD-PT-001', - childItemName: '가이드레일(벽면형)', - quantity: 2, - unit: 'EA', - unitPrice: 35000, - quantityFormula: 'H / 1000', - }, - { - id: 'bom-2', - childItemCode: 'KD-PT-002', - childItemName: '절곡품 샘플', - quantity: 4, - unit: 'EA', - unitPrice: 30000, - isBending: true, - }, - { - id: 'bom-3', - childItemCode: 'KD-SM-001', - childItemName: '볼트 M6x20', - quantity: 20, - unit: 'EA', - unitPrice: 50, - }, - ], - }, - { - id: '2', - itemCode: 'KD-PT-001', - itemName: '가이드레일(벽면형)', - itemType: 'PT', - unit: 'EA', - specification: '2438mm', - isActive: true, - category1: '본체부품', - category2: '가이드시스템', - category3: '가이드레일', - salesPrice: 50000, - purchasePrice: 35000, - marginRate: 30, - partType: 'ASSEMBLY', - partUsage: 'GUIDE_RAIL', - installationType: '벽면형', - assemblyType: 'M', - assemblyLength: '2438', - currentRevision: 0, - isFinal: false, - createdAt: '2025-01-10T00:00:00Z', - }, - { - id: '3', - itemCode: 'KD-PT-002', - itemName: '절곡품 샘플', - itemType: 'PT', - unit: 'EA', - specification: 'EGI 1.55T', - isActive: true, - partType: 'BENDING', - material: 'EGI 1.55T', - length: '2000', - salesPrice: 30000, - currentRevision: 0, - isFinal: false, - createdAt: '2025-01-10T00:00:00Z', - }, - { - id: '4', - itemCode: 'KD-RM-001', - itemName: 'SPHC-SD', - itemType: 'RM', - unit: 'KG', - specification: '1.6T x 1219 x 2438', - isActive: true, - category1: '철강재', - purchasePrice: 1500, - material: 'SPHC-SD', - currentRevision: 0, - isFinal: false, - createdAt: '2025-01-10T00:00:00Z', - }, - { - id: '5', - itemCode: 'KD-SM-001', - itemName: '볼트 M6x20', - itemType: 'SM', - unit: 'EA', - specification: 'M6x20', - isActive: true, - category1: '구조재/부속품', - category2: '볼트/너트', - purchasePrice: 50, - currentRevision: 0, - isFinal: false, - createdAt: '2025-01-10T00:00:00Z', - }, -]; +interface PageProps { + params: Promise<{ id: string }>; +} -export default function EditItemPage() { - const params = useParams(); +/** + * V2 하위 호환성: /[id]/edit → /[id]?mode=edit 리다이렉트 + * 기존 쿼리 파라미터(type, id)는 유지 + */ +export default function ItemEditPage({ params }: PageProps) { + const { id } = use(params); const router = useRouter(); - const [item, setItem] = useState(null); - const [isLoading, setIsLoading] = useState(true); + const searchParams = useSearchParams(); useEffect(() => { - // TODO: API 연동 시 fetchItemByCode() 호출 - const fetchItem = async () => { - setIsLoading(true); - try { - // params.id 타입 체크 - if (!params.id || typeof params.id !== 'string') { - alert('잘못된 품목 ID입니다.'); - router.push('/items'); - return; - } + // 기존 쿼리 파라미터 유지하면서 mode=edit 추가 + const type = searchParams.get('type') || 'FG'; + const itemId = searchParams.get('id') || ''; - // Mock: 데이터 조회 - const itemCode = decodeURIComponent(params.id); - const foundItem = mockItems.find((item) => item.itemCode === itemCode); - - if (foundItem) { - setItem(foundItem); - } else { - alert('품목을 찾을 수 없습니다.'); - router.push('/items'); - } - } catch { - alert('품목 조회에 실패했습니다.'); - router.push('/items'); - } finally { - setIsLoading(false); - } - }; - - fetchItem(); - }, [params.id, router]); - - const handleSubmit = async (data: CreateItemFormData) => { - // TODO: API 연동 시 updateItem() 호출 - console.log('품목 수정 데이터:', data); - - // Mock: 성공 메시지 - alert(`품목 "${data.itemName}" (${data.itemCode})이(가) 수정되었습니다.`); - - // API 연동 예시: - // const updatedItem = await updateItem(item.itemCode, data); - // router.push(`/items/${updatedItem.itemCode}`); - }; - - if (isLoading) { - return ; - } - - if (!item) { - return null; - } + router.replace(`/production/screen-production/${id}?type=${type}&id=${itemId}&mode=edit`); + }, [id, router, searchParams]); return ( -
- +
+
리다이렉트 중...
); -} \ No newline at end of file +} diff --git a/src/app/[locale]/(protected)/production/screen-production/[id]/page.tsx b/src/app/[locale]/(protected)/production/screen-production/[id]/page.tsx index ce3b7591..cb70c3b3 100644 --- a/src/app/[locale]/(protected)/production/screen-production/[id]/page.tsx +++ b/src/app/[locale]/(protected)/production/screen-production/[id]/page.tsx @@ -1,185 +1,34 @@ 'use client'; /** - * 품목 상세 조회 페이지 (Client Component) + * 품목 상세/수정 페이지 (Client Component) + * V2 패턴: ?mode=edit로 수정 모드 전환 */ -import { use, useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; -import ItemDetailClient from '@/components/items/ItemDetailClient'; -import type { ItemMaster } from '@/types/item'; +import { use } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { ItemDetailView } from '@/components/items/ItemDetailView'; +import { ItemDetailEdit } from '@/components/items/ItemDetailEdit'; -// Mock 데이터 (API 연동 전 임시) -const mockItems: ItemMaster[] = [ - { - id: '1', - itemCode: 'KD-FG-001', - itemName: '스크린 제품 A', - itemType: 'FG', - unit: 'EA', - specification: '2000x2000', - isActive: true, - category1: '본체부품', - category2: '가이드시스템', - salesPrice: 150000, - purchasePrice: 100000, - marginRate: 33.3, - processingCost: 20000, - laborCost: 15000, - installCost: 10000, - productCategory: 'SCREEN', - lotAbbreviation: 'KD', - note: '스크린 제품 샘플입니다.', - safetyStock: 10, - leadTime: 7, - currentRevision: 0, - isFinal: false, - createdAt: '2025-01-10T00:00:00Z', - updatedAt: '2025-01-12T00:00:00Z', - bom: [ - { - id: 'bom-1', - childItemCode: 'KD-PT-001', - childItemName: '가이드레일(벽면형)', - quantity: 2, - unit: 'EA', - unitPrice: 35000, - quantityFormula: 'H / 1000', - }, - { - id: 'bom-2', - childItemCode: 'KD-PT-002', - childItemName: '절곡품 샘플', - quantity: 4, - unit: 'EA', - unitPrice: 30000, - isBending: true, - }, - { - id: 'bom-3', - childItemCode: 'KD-SM-001', - childItemName: '볼트 M6x20', - quantity: 20, - unit: 'EA', - unitPrice: 50, - }, - ], - }, - { - id: '2', - itemCode: 'KD-PT-001', - itemName: '가이드레일(벽면형)', - itemType: 'PT', - unit: 'EA', - specification: '2438mm', - isActive: true, - category1: '본체부품', - category2: '가이드시스템', - category3: '가이드레일', - salesPrice: 50000, - purchasePrice: 35000, - marginRate: 30, - partType: 'ASSEMBLY', - partUsage: 'GUIDE_RAIL', - installationType: '벽면형', - assemblyType: 'M', - assemblyLength: '2438', - currentRevision: 0, - isFinal: false, - createdAt: '2025-01-10T00:00:00Z', - }, - { - id: '3', - itemCode: 'KD-PT-002', - itemName: '절곡품 샘플', - itemType: 'PT', - unit: 'EA', - specification: 'EGI 1.55T', - isActive: true, - partType: 'BENDING', - material: 'EGI 1.55T', - length: '2000', - salesPrice: 30000, - currentRevision: 0, - isFinal: false, - createdAt: '2025-01-10T00:00:00Z', - }, - { - id: '4', - itemCode: 'KD-RM-001', - itemName: 'SPHC-SD', - itemType: 'RM', - unit: 'KG', - specification: '1.6T x 1219 x 2438', - isActive: true, - category1: '철강재', - purchasePrice: 1500, - material: 'SPHC-SD', - currentRevision: 0, - isFinal: false, - createdAt: '2025-01-10T00:00:00Z', - }, - { - id: '5', - itemCode: 'KD-SM-001', - itemName: '볼트 M6x20', - itemType: 'SM', - unit: 'EA', - specification: 'M6x20', - isActive: true, - category1: '구조재/부속품', - category2: '볼트/너트', - purchasePrice: 50, - currentRevision: 0, - isFinal: false, - createdAt: '2025-01-10T00:00:00Z', - }, -]; - -/** - * 품목 상세 페이지 - */ -export default function ItemDetailPage({ - params, -}: { +interface PageProps { params: Promise<{ id: string }>; -}) { +} + +export default function ItemDetailPage({ params }: PageProps) { const { id } = use(params); - const router = useRouter(); - const [item, setItem] = useState(null); - const [isLoading, setIsLoading] = useState(true); + const searchParams = useSearchParams(); - useEffect(() => { - // API 연동 전 mock 데이터 사용 - const foundItem = mockItems.find( - (item) => item.itemCode === decodeURIComponent(id) - ); - setItem(foundItem || null); - setIsLoading(false); - }, [id]); + // URL에서 type, id, mode 쿼리 파라미터 읽기 + const itemType = searchParams.get('type') || 'FG'; + const itemId = searchParams.get('id') || ''; + const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view'; - if (isLoading) { - return ; + // 품목 코드 디코딩 + const itemCode = decodeURIComponent(id); + + if (mode === 'edit') { + return ; } - if (!item) { - return ( -
-
품목을 찾을 수 없습니다.
- -
- ); - } - - return ( -
- -
- ); -} \ No newline at end of file + return ; +} diff --git a/src/app/[locale]/(protected)/production/screen-production/create/page.tsx b/src/app/[locale]/(protected)/production/screen-production/create/page.tsx index 96919eb1..a14c0810 100644 --- a/src/app/[locale]/(protected)/production/screen-production/create/page.tsx +++ b/src/app/[locale]/(protected)/production/screen-production/create/page.tsx @@ -1,28 +1,101 @@ /** * 품목 등록 페이지 + * + * DynamicItemForm을 사용하여 품목기준관리 데이터 기반 동적 폼 렌더링 */ 'use client'; -import ItemForm from '@/components/items/ItemForm'; -import type { CreateItemFormData } from '@/lib/utils/validation'; +import { useState } from 'react'; +import DynamicItemForm from '@/components/items/DynamicItemForm'; +import type { DynamicFormData } from '@/components/items/DynamicItemForm/types'; +// 2025-12-16: options 관련 변환 로직 제거 +// 백엔드가 품목기준관리 field_key 매핑을 처리하므로 프론트에서 변환 불필요 +import { DuplicateCodeError } from '@/lib/api/error-handler'; + +// 기존 ItemForm (주석처리 - 롤백 시 사용) +// import ItemForm from '@/components/items/ItemForm'; +// import type { CreateItemFormData } from '@/lib/utils/validation'; export default function CreateItemPage() { - const handleSubmit = async (data: CreateItemFormData) => { - // TODO: API 연동 시 createItem() 호출 - console.log('품목 등록 데이터:', data); + const [submitError, setSubmitError] = useState(null); - // Mock: 성공 메시지 - alert(`품목 "${data.itemName}" (${data.itemCode})이(가) 등록되었습니다.`); + const handleSubmit = async (data: DynamicFormData) => { + setSubmitError(null); - // API 연동 예시: - // const newItem = await createItem(data); - // router.push(`/items/${newItem.itemCode}`); + // 필드명 변환: spec → specification (백엔드 API 규격) + const submitData = { ...data }; + if (submitData.spec !== undefined) { + submitData.specification = submitData.spec; + delete submitData.spec; + } + + // 2025-12-15: item_type은 Request Body에서 필수 (ItemService.store validation) + // product_type과 item_type을 동일하게 설정 + const itemType = submitData.product_type as string; + submitData.item_type = itemType; + + // API 호출 전 이미지 데이터 제거 (파일 업로드는 별도 API 사용) + // bending_diagram이 base64 데이터인 경우 제거 (JSON에 포함시키면 안됨) + if (submitData.bending_diagram && typeof submitData.bending_diagram === 'string' && (submitData.bending_diagram as string).startsWith('data:')) { + delete submitData.bending_diagram; + } + // 시방서/인정서 파일 필드도 base64면 제거 + if (submitData.specification_file && typeof submitData.specification_file === 'string' && (submitData.specification_file as string).startsWith('data:')) { + delete submitData.specification_file; + } + if (submitData.certification_file && typeof submitData.certification_file === 'string' && (submitData.certification_file as string).startsWith('data:')) { + delete submitData.certification_file; + } + + // API 호출: POST /api/proxy/items + // 백엔드에서 product_type에 따라 Product/Material 분기 처리 + const response = await fetch('/api/proxy/items', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(submitData), + }); + + const result = await response.json(); + + if (!response.ok || !result.success) { + // 2025-12-11: 백엔드 중복 에러 처리 (DuplicateCodeException) + // duplicate_id가 있으면 DuplicateCodeError throw → DynamicItemForm에서 다이얼로그 표시 + if (response.status === 400 && result.duplicate_id) { + console.warn('[CreateItemPage] 품목코드 중복 에러:', result); + throw new DuplicateCodeError( + result.message || '해당 품목코드가 이미 존재합니다.', + result.duplicate_id, + result.duplicate_code + ); + } + + const errorMessage = result.message || '품목 등록에 실패했습니다.'; + console.error('[CreateItemPage] 품목 등록 실패:', errorMessage); + setSubmitError(errorMessage); + throw new Error(errorMessage); + } + + // 성공 시 DynamicItemForm 내부에서 /items로 리다이렉트 처리됨 + // console.log('[CreateItemPage] 품목 등록 성공:', result.data); + + // 생성된 품목 ID를 포함한 데이터 반환 (파일 업로드용) + return { id: result.data.id, ...result.data }; }; return (
- + {submitError && ( +
+ ⚠️ {submitError} +
+ )} +
); } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/production/screen-production/page.tsx b/src/app/[locale]/(protected)/production/screen-production/page.tsx index e9c54b24..d0ffef41 100644 --- a/src/app/[locale]/(protected)/production/screen-production/page.tsx +++ b/src/app/[locale]/(protected)/production/screen-production/page.tsx @@ -1,9 +1,9 @@ -'use client'; - /** - * 품목 목록 페이지 (Client Component) + * 품목 관리 페이지 * - * Next.js 15 App Router + * 품목기준관리 API 연동 + * - 품목 목록: API에서 조회 + * - 테이블 컬럼: custom-tabs API에서 동적 구성 */ import ItemListClient from '@/components/items/ItemListClient'; @@ -13,4 +13,12 @@ import ItemListClient from '@/components/items/ItemListClient'; */ export default function ItemsPage() { return ; -} \ No newline at end of file +} + +/** + * 메타데이터 설정 + */ +export const metadata = { + title: '품목 관리', + description: '품목 목록 조회 및 관리', +}; \ No newline at end of file diff --git a/src/app/[locale]/(protected)/production/work-orders/[id]/edit/page.tsx b/src/app/[locale]/(protected)/production/work-orders/[id]/edit/page.tsx index b6656d21..d32f1109 100644 --- a/src/app/[locale]/(protected)/production/work-orders/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/production/work-orders/[id]/edit/page.tsx @@ -1,10 +1,26 @@ -import { WorkOrderEdit } from '@/components/production/WorkOrders'; +'use client'; + +import { use, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; interface PageProps { params: Promise<{ id: string }>; } -export default async function WorkOrderEditPage({ params }: PageProps) { - const { id } = await params; - return ; -} \ No newline at end of file +/** + * V2 하위 호환성: /[id]/edit → /[id]?mode=edit 리다이렉트 + */ +export default function WorkOrderEditPage({ params }: PageProps) { + const { id } = use(params); + const router = useRouter(); + + useEffect(() => { + router.replace(`/production/work-orders/${id}?mode=edit`); + }, [id, router]); + + return ( +
+
리다이렉트 중...
+
+ ); +} diff --git a/src/app/[locale]/(protected)/production/work-orders/[id]/page.tsx b/src/app/[locale]/(protected)/production/work-orders/[id]/page.tsx index 70fd23f8..0da96489 100644 --- a/src/app/[locale]/(protected)/production/work-orders/[id]/page.tsx +++ b/src/app/[locale]/(protected)/production/work-orders/[id]/page.tsx @@ -3,10 +3,12 @@ /** * 작업지시 상세 페이지 (Client Component) * URL: /production/work-orders/[id] + * V2 패턴: ?mode=edit로 수정 모드 전환 */ import { use } from 'react'; -import { WorkOrderDetail } from '@/components/production/WorkOrders'; +import { useSearchParams } from 'next/navigation'; +import { WorkOrderDetail, WorkOrderEdit } from '@/components/production/WorkOrders'; interface PageProps { params: Promise<{ @@ -16,5 +18,13 @@ interface PageProps { export default function WorkOrderDetailPage({ params }: PageProps) { const { id } = use(params); + const searchParams = useSearchParams(); + const mode = searchParams.get('mode'); + const isEditMode = mode === 'edit'; + + if (isEditMode) { + return ; + } + return ; } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/sales/client-management-sales-admin/[id]/edit/page.tsx b/src/app/[locale]/(protected)/sales/client-management-sales-admin/[id]/edit/page.tsx index 2496489e..aeb7fb44 100644 --- a/src/app/[locale]/(protected)/sales/client-management-sales-admin/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/sales/client-management-sales-admin/[id]/edit/page.tsx @@ -1,78 +1,24 @@ +'use client'; + /** - * 거래처 수정 페이지 + * 거래처(영업) 수정 페이지 (리다이렉트) + * + * 기존 URL 호환성을 위해 유지 + * /[id]/edit → /[id]?mode=edit 로 리다이렉트 */ -"use client"; +import { useEffect } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; -import { useEffect, useState } from "react"; -import { useRouter, useParams } from "next/navigation"; -import { ClientRegistration } from "@/components/clients/ClientRegistration"; -import { - useClientList, - ClientFormData, - clientToFormData, -} from "@/hooks/useClientList"; -import { toast } from "sonner"; -import { ContentLoadingSpinner } from "@/components/ui/loading-spinner"; - -export default function ClientEditPage() { +export default function ClientEditRedirectPage() { const router = useRouter(); const params = useParams(); const id = params.id as string; - const { fetchClient, updateClient, isLoading: hookLoading } = useClientList(); - const [editingClient, setEditingClient] = useState( - null - ); - const [isLoading, setIsLoading] = useState(true); - - // 데이터 로드 useEffect(() => { - const loadClient = async () => { - if (!id) return; + router.replace(`/ko/sales/client-management-sales-admin/${id}?mode=edit`); + }, [router, id]); - setIsLoading(true); - try { - const data = await fetchClient(id); - if (data) { - setEditingClient(clientToFormData(data)); - } else { - toast.error("거래처를 찾을 수 없습니다."); - router.push("/sales/client-management-sales-admin"); - } - } catch (error) { - toast.error("데이터 로드 중 오류가 발생했습니다."); - router.push("/sales/client-management-sales-admin"); - } finally { - setIsLoading(false); - } - }; - - loadClient(); - }, [id, fetchClient, router]); - - const handleBack = () => { - router.push(`/sales/client-management-sales-admin/${id}`); - }; - - const handleSave = async (formData: ClientFormData) => { - await updateClient(id, formData); - }; - - if (isLoading) { - return ; - } - - if (!editingClient) { - return null; - } - - return ( - - ); -} \ No newline at end of file + return ; +} diff --git a/src/app/[locale]/(protected)/sales/client-management-sales-admin/[id]/page.tsx b/src/app/[locale]/(protected)/sales/client-management-sales-admin/[id]/page.tsx index 7ca7884c..bc49a1a0 100644 --- a/src/app/[locale]/(protected)/sales/client-management-sales-admin/[id]/page.tsx +++ b/src/app/[locale]/(protected)/sales/client-management-sales-admin/[id]/page.tsx @@ -1,124 +1,20 @@ +'use client'; + /** - * 거래처 상세 페이지 + * 거래처(영업) 상세/수정 페이지 + * IntegratedDetailTemplate V2 마이그레이션 + * + * URL 패턴: + * - /[id] : view 모드 + * - /[id]?mode=edit : edit 모드 */ -"use client"; - -import { useEffect, useState } from "react"; -import { useRouter, useParams } from "next/navigation"; -import { ClientDetail } from "@/components/clients/ClientDetail"; -import { useClientList, Client } from "@/hooks/useClientList"; -import { toast } from "sonner"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; -import { ContentLoadingSpinner } from "@/components/ui/loading-spinner"; +import { useParams } from 'next/navigation'; +import { ClientDetailClientV2 } from '@/components/clients/ClientDetailClientV2'; export default function ClientDetailPage() { - const router = useRouter(); const params = useParams(); const id = params.id as string; - const { fetchClient, deleteClient } = useClientList(); - const [client, setClient] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isDeleting, setIsDeleting] = useState(false); - const [showDeleteDialog, setShowDeleteDialog] = useState(false); - - // 데이터 로드 - useEffect(() => { - const loadClient = async () => { - if (!id) return; - - setIsLoading(true); - try { - const data = await fetchClient(id); - if (data) { - setClient(data); - } else { - toast.error("거래처를 찾을 수 없습니다."); - router.push("/sales/client-management-sales-admin"); - } - } catch (error) { - toast.error("데이터 로드 중 오류가 발생했습니다."); - router.push("/sales/client-management-sales-admin"); - } finally { - setIsLoading(false); - } - }; - - loadClient(); - }, [id, fetchClient, router]); - - const handleBack = () => { - router.push("/sales/client-management-sales-admin"); - }; - - const handleEdit = () => { - router.push(`/sales/client-management-sales-admin/${id}/edit`); - }; - - const handleDelete = async () => { - setIsDeleting(true); - try { - await deleteClient(id); - toast.success("거래처가 삭제되었습니다."); - router.push("/sales/client-management-sales-admin"); - } catch (error) { - toast.error("삭제 중 오류가 발생했습니다."); - } finally { - setIsDeleting(false); - setShowDeleteDialog(false); - } - }; - - if (isLoading) { - return ; - } - - if (!client) { - return null; - } - - return ( - <> - setShowDeleteDialog(true)} - /> - - {/* 삭제 확인 다이얼로그 */} - - - - 거래처 삭제 - - '{client.name}' 거래처를 삭제하시겠습니까? -
- 이 작업은 되돌릴 수 없습니다. -
-
- - 취소 - - {isDeleting ? "삭제 중..." : "삭제"} - - -
-
- - ); -} \ No newline at end of file + return ; +} diff --git a/src/app/[locale]/(protected)/sales/client-management-sales-admin/new/page.tsx b/src/app/[locale]/(protected)/sales/client-management-sales-admin/new/page.tsx index 3161845d..985dfa7d 100644 --- a/src/app/[locale]/(protected)/sales/client-management-sales-admin/new/page.tsx +++ b/src/app/[locale]/(protected)/sales/client-management-sales-admin/new/page.tsx @@ -1,30 +1,12 @@ +'use client'; + /** - * 거래처 등록 페이지 + * 거래처(영업) 등록 페이지 + * IntegratedDetailTemplate V2 마이그레이션 */ -"use client"; - -import { useRouter } from "next/navigation"; -import { ClientRegistration } from "@/components/clients/ClientRegistration"; -import { useClientList, ClientFormData } from "@/hooks/useClientList"; +import { ClientDetailClientV2 } from '@/components/clients/ClientDetailClientV2'; export default function ClientNewPage() { - const router = useRouter(); - const { createClient, isLoading } = useClientList(); - - const handleBack = () => { - router.push("/sales/client-management-sales-admin"); - }; - - const handleSave = async (formData: ClientFormData) => { - await createClient(formData); - }; - - return ( - - ); -} \ No newline at end of file + return ; +} diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx index 13f67c87..1205ca29 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx @@ -1,559 +1,26 @@ -"use client"; +'use client'; + +import { use, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; + +interface PageProps { + params: Promise<{ id: string }>; +} /** - * 수주 수정 페이지 - * - * - 기본 정보 (읽기전용) - * - 수주/배송 정보 (편집 가능) - * - 비고 (편집 가능) - * - 품목 내역 (생산 시작 후 수정 불가) + * V2 하위 호환성: /[id]/edit → /[id]?mode=edit 리다이렉트 */ - -import { useState, useEffect } from "react"; -import { useRouter, useParams } from "next/navigation"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Label } from "@/components/ui/label"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { FileText, AlertTriangle } from "lucide-react"; -import { toast } from "sonner"; -import { PageLayout } from "@/components/organisms/PageLayout"; -import { PageHeader } from "@/components/organisms/PageHeader"; -import { FormActions } from "@/components/organisms/FormActions"; -import { BadgeSm } from "@/components/atoms/BadgeSm"; -import { formatAmount } from "@/utils/formatAmount"; -import { - OrderItem, - getOrderById, - updateOrder, - type OrderStatus, -} from "@/components/orders"; - -// 수정 폼 데이터 -interface EditFormData { - // 읽기전용 정보 - lotNumber: string; - quoteNumber: string; - client: string; - siteName: string; - manager: string; - contact: string; - status: OrderStatus; - - // 수정 가능 정보 - expectedShipDate: string; - expectedShipDateUndecided: boolean; - deliveryRequestDate: string; - deliveryMethod: string; - shippingCost: string; - receiver: string; - receiverContact: string; - address: string; - addressDetail: string; - remarks: string; - - // 품목 (수정 제한) - items: OrderItem[]; - canEditItems: boolean; - subtotal: number; - discountRate: number; - totalAmount: number; -} - -// 배송방식 옵션 -const DELIVERY_METHODS = [ - { value: "direct", label: "직접배차" }, - { value: "pickup", label: "상차" }, - { value: "courier", label: "택배" }, -]; - -// 운임비용 옵션 -const SHIPPING_COSTS = [ - { value: "free", label: "무료" }, - { value: "prepaid", label: "선불" }, - { value: "collect", label: "착불" }, - { value: "negotiable", label: "협의" }, -]; - - -// 상태 뱃지 헬퍼 -function getOrderStatusBadge(status: OrderStatus) { - const statusConfig: Record = { - order_registered: { label: "수주등록", className: "bg-gray-100 text-gray-700 border-gray-200" }, - order_confirmed: { label: "수주확정", className: "bg-gray-100 text-gray-700 border-gray-200" }, - production_ordered: { label: "생산지시완료", className: "bg-blue-100 text-blue-700 border-blue-200" }, - in_production: { label: "생산중", className: "bg-green-100 text-green-700 border-green-200" }, - rework: { label: "재작업중", className: "bg-orange-100 text-orange-700 border-orange-200" }, - work_completed: { label: "작업완료", className: "bg-blue-600 text-white border-blue-600" }, - shipped: { label: "출하완료", className: "bg-gray-500 text-white border-gray-500" }, - cancelled: { label: "취소", className: "bg-red-100 text-red-700 border-red-200" }, - }; - const config = statusConfig[status]; - return ( - - {config.label} - - ); -} - -export default function OrderEditPage() { +export default function OrderEditPage({ params }: PageProps) { + const { id } = use(params); const router = useRouter(); - const params = useParams(); - const orderId = params.id as string; - const [form, setForm] = useState(null); - const [loading, setLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); - - // 데이터 로드 (API) useEffect(() => { - async function loadOrder() { - try { - setLoading(true); - const result = await getOrderById(orderId); - if (result.success && result.data) { - const order = result.data; - // 상태에 따라 품목 수정 가능 여부 결정 - const canEditItems = !["in_production", "rework", "work_completed", "shipped"].includes( - order.status - ); - // Order 데이터를 EditFormData로 변환 - setForm({ - lotNumber: order.lotNumber, - quoteNumber: order.quoteNumber || "", - client: order.client, - siteName: order.siteName, - manager: order.manager || "", - contact: order.contact || "", - status: order.status, - expectedShipDate: order.expectedShipDate || "", - expectedShipDateUndecided: !order.expectedShipDate, - deliveryRequestDate: order.deliveryRequestDate || "", - deliveryMethod: order.deliveryMethod || "", - shippingCost: order.shippingCost || "", - receiver: order.receiver || "", - receiverContact: order.receiverContact || "", - address: order.address || "", - addressDetail: order.addressDetail || "", - remarks: order.remarks || "", - items: order.items || [], - canEditItems, - subtotal: order.subtotal || order.amount, - discountRate: order.discountRate || 0, - totalAmount: order.amount, - }); - } else { - toast.error(result.error || "수주 정보를 불러오는데 실패했습니다."); - router.push("/sales/order-management-sales"); - } - } catch (error) { - console.error("Error loading order:", error); - toast.error("수주 정보를 불러오는 중 오류가 발생했습니다."); - router.push("/sales/order-management-sales"); - } finally { - setLoading(false); - } - } - loadOrder(); - }, [orderId, router]); - - const handleCancel = () => { - router.push(`/sales/order-management-sales/${orderId}`); - }; - - const handleSave = async () => { - if (!form) return; - - // 유효성 검사 - if (!form.deliveryRequestDate) { - toast.error("납품요청일을 입력해주세요."); - return; - } - if (!form.receiver.trim()) { - toast.error("수신(반장/업체)을 입력해주세요."); - return; - } - if (!form.receiverContact.trim()) { - toast.error("수신처 연락처를 입력해주세요."); - return; - } - - setIsSaving(true); - try { - // API 연동 - const result = await updateOrder(orderId, { - clientId: undefined, // 기존 값 유지 - siteName: form.siteName, - expectedShipDate: form.expectedShipDateUndecided ? undefined : form.expectedShipDate, - deliveryRequestDate: form.deliveryRequestDate, - deliveryMethod: form.deliveryMethod, - shippingCost: form.shippingCost, - receiver: form.receiver, - receiverContact: form.receiverContact, - address: form.address, - addressDetail: form.addressDetail, - remarks: form.remarks, - items: form.items.map((item) => ({ - itemId: item.id ? parseInt(item.id, 10) : undefined, - itemCode: item.itemCode, - itemName: item.itemName, - specification: item.spec, - quantity: item.quantity, - unit: item.unit, - unitPrice: item.unitPrice, - })), - }); - - if (result.success) { - toast.success("수주가 수정되었습니다."); - router.push(`/sales/order-management-sales/${orderId}`); - } else { - toast.error(result.error || "수주 수정에 실패했습니다."); - } - } catch (error) { - console.error("Error updating order:", error); - toast.error("수주 수정 중 오류가 발생했습니다."); - } finally { - setIsSaving(false); - } - }; - - if (loading || !form) { - return ( - -
-
-
- - ); - } + router.replace(`/sales/order-management-sales/${id}?mode=edit`); + }, [id, router]); return ( - - {/* 헤더 */} - - - {form.lotNumber} - - {getOrderStatusBadge(form.status)} -
- } - /> - -
- {/* 기본 정보 (읽기전용) */} - - - - 기본 정보 - (읽기전용) - - - -
-
- -

{form.lotNumber}

-
-
- -

{form.quoteNumber}

-
-
- -

{form.manager}

-
-
- -

{form.client}

-
-
- -

{form.siteName}

-
-
- -

{form.contact}

-
-
-
-
- - {/* 수주/배송 정보 (편집 가능) */} - - - 수주/배송 정보 - - -
- {/* 출고예정일 */} -
- -
- - setForm({ ...form, expectedShipDate: e.target.value }) - } - disabled={form.expectedShipDateUndecided} - className="flex-1" - /> -
- - setForm({ - ...form, - expectedShipDateUndecided: checked as boolean, - expectedShipDate: checked ? "" : form.expectedShipDate, - }) - } - /> - -
-
-
- - {/* 납품요청일 */} -
- - - setForm({ ...form, deliveryRequestDate: e.target.value }) - } - /> -
- - {/* 배송방식 */} -
- - -
- - {/* 운임비용 */} -
- - -
- - {/* 수신(반장/업체) */} -
- - - setForm({ ...form, receiver: e.target.value }) - } - /> -
- - {/* 수신처 연락처 */} -
- - - setForm({ ...form, receiverContact: e.target.value }) - } - /> -
- - {/* 수신처 주소 */} -
- - - setForm({ ...form, address: e.target.value }) - } - placeholder="주소" - className="mb-2" - /> - - setForm({ ...form, addressDetail: e.target.value }) - } - placeholder="상세주소" - /> -
-
-
-
- - {/* 비고 */} - - - 비고 - - -