diff --git a/claudedocs/[REF] juil-pages-test-urls.md b/claudedocs/[REF] juil-pages-test-urls.md index 703c1606..696f9edd 100644 --- a/claudedocs/[REF] juil-pages-test-urls.md +++ b/claudedocs/[REF] juil-pages-test-urls.md @@ -1,5 +1,5 @@ # Juil Enterprise Test URLs -Last Updated: 2025-12-30 +Last Updated: 2026-01-05 ### 대시보드 | 페이지 | URL | 상태 | @@ -17,7 +17,29 @@ Last Updated: 2025-12-30 |---|---|---| | **거래처 관리** | `/ko/juil/project/bidding/partners` | ✅ 완료 | | **현장설명회관리** | `/ko/juil/project/bidding/site-briefings` | ✅ 완료 | -| **견적관리** | `/ko/juil/project/bidding/estimates` | 🆕 NEW | +| **견적관리** | `/ko/juil/project/bidding/estimates` | ✅ 완료 | +| **입찰관리** | `/ko/juil/project/bidding` | ✅ 완료 | + +### 계약관리 (Contract) +| 페이지 | URL | 상태 | +|---|---|---| +| **계약관리** | `/ko/juil/project/contract` | 🆕 NEW | +| **인수인계보고서관리** | `/ko/juil/project/contract/handover-report` | 🆕 NEW | + +### 발주관리 (Order) +| 페이지 | URL | 상태 | +|---|---|---| +| **현장관리** | `/ko/juil/order/site-management` | 🆕 NEW | +| **구조검토관리** | `/ko/juil/order/structure-review` | 🆕 NEW | +| **발주관리** | `/ko/juil/order/order-management` | 🆕 NEW | + +### 기준정보 (Base Info) - 발주관리 하위 +| 페이지 | URL | 상태 | +|---|---|---| +| **카테고리관리** | `/ko/juil/order/base-info/categories` | 🆕 NEW | +| **품목관리** | `/ko/juil/order/base-info/items` | 🆕 NEW | +| **단가관리** | `/ko/juil/order/base-info/pricing` | 🆕 NEW | +| **노임관리** | `/ko/juil/order/base-info/labor` | 🆕 NEW | ## 공사 관리 (Construction) ### 인수인계 / 실측 / 발주 / 시공 diff --git a/claudedocs/_index.md b/claudedocs/_index.md index 3f8fda2c..6e5ec2ef 100644 --- a/claudedocs/_index.md +++ b/claudedocs/_index.md @@ -1,6 +1,6 @@ # claudedocs 문서 맵 -> 프로젝트 기술 문서 인덱스 (Last Updated: 2025-12-30) +> 프로젝트 기술 문서 인덱스 (Last Updated: 2026-01-02) ## ⭐ 빠른 참조 @@ -154,7 +154,9 @@ claudedocs/ | 파일 | 설명 | |------|------| -| `[GUIDE-2025-12-29] vercel-deployment.md` | 🔴 **NEW** - Vercel 배포 가이드 (환경변수, CORS, 테스트 체크리스트) | +| `[DESIGN-2026-01-02] document-modal-common-component.md` | 🔴 **NEW** - 문서 모달 공통 컴포넌트 설계 요구사항 (6개 모달 분석, 헤더/결재라인/테이블 조합형) | +| `[GUIDE] print-area-utility.md` | 인쇄 모달 printArea 유틸리티 가이드 (8개 모달 적용, print-utils.ts) | +| `[GUIDE-2025-12-29] vercel-deployment.md` | Vercel 배포 가이드 (환경변수, CORS, 테스트 체크리스트) | | `[PLAN-2025-12-23] common-component-extraction-plan.md` | 공통 컴포넌트 추출 계획서 (Phase 1-4, 체크리스트 포함, ~1,900줄 절감) | | `[ANALYSIS-2025-12-23] common-component-extraction-candidates.md` | 📋 공통 컴포넌트 추출 후보 분석 (다이얼로그 102개 중복, ~2,370줄 절감 예상) | | `[PLAN-2025-12-19] project-health-improvement.md` | ✅ **Phase 1 완료** - 프로젝트 헬스 개선 계획서 (타입에러 0개, API키 보안, SSR 수정) | @@ -214,6 +216,9 @@ claudedocs/ | 파일 | 설명 | |------|------| +| `[IMPL-2026-01-05] item-management-checklist.md` | 🔴 **NEW** - 품목관리 구현 체크리스트 (발주관리 > 기준정보 > 품목관리) | +| `[IMPL-2026-01-05] category-management-checklist.md` | 🔴 **NEW** - 카테고리관리 구현 체크리스트 (발주관리 > 기준정보) | +| `[PLAN-2026-01-05] order-management-implementation.md` | 발주관리 페이지 구현 계획서 (달력+리스트, ScheduleCalendar 공통 컴포넌트) | | `[NEXT-2025-12-30] partner-management-session-context.md` | ⭐ **세션 체크포인트** - 거래처 관리 리스트 완료, 등록/상세/수정 예정 | | `[REF] juil-project-structure.md` | 주일 프로젝트 구조 가이드 (경로, 컴포넌트, 테스트 URL) | diff --git a/claudedocs/architecture/[DESIGN-2025-12-20] item-master-zustand-refactoring.md b/claudedocs/architecture/[DESIGN-2025-12-20] item-master-zustand-refactoring.md new file mode 100644 index 00000000..2639c3f2 --- /dev/null +++ b/claudedocs/architecture/[DESIGN-2025-12-20] item-master-zustand-refactoring.md @@ -0,0 +1,538 @@ +# 품목기준관리 Zustand 리팩토링 설계서 + +> **핵심 목표**: 모든 기능을 100% 동일하게 유지하면서, 수정 절차를 간단화 + +## 📌 핵심 원칙 + +``` +⚠️ 중요: 모든 품목기준관리 기능을 그대로 가져와야 함 +⚠️ 중요: 수정 절차 간단화가 핵심 (3방향 동기화 → 1곳 수정) +⚠️ 중요: 모든 기능이 정확히 동일하게 작동해야 함 +``` + +## 🔴 최종 검증 기준 (가장 중요!) + +### 페이지 관계도 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ [DB / API] │ +│ (단일 진실 공급원) │ +└─────────────────────────────────────────────────────────────┘ + ↑ ↑ ↓ + │ │ │ +┌───────┴───────┐ ┌────────┴────────┐ ┌────────┴────────┐ +│ 품목기준관리 │ │ 품목기준관리 │ │ 품목관리 │ +│ 테스트 페이지 │ │ 페이지 (기존) │ │ 페이지 │ +│ (Zustand) │ │ (Context) │ │ (동적 폼 렌더링) │ +└───────────────┘ └──────────────────┘ └──────────────────┘ + [신규] [기존] [최종 사용처] +``` + +### 검증 시나리오 + +``` +1. 테스트 페이지에서 섹션/필드 수정 + ↓ +2. API 호출 → DB 저장 + ↓ +3. 품목기준관리 페이지 (기존)에서 동일하게 표시되어야 함 + ↓ +4. 품목관리 페이지에서 동적 폼이 변경된 구조로 렌더링되어야 함 +``` + +### 필수 검증 항목 + +| # | 검증 항목 | 설명 | +|---|----------|------| +| 1 | **API 동일성** | 테스트 페이지가 기존 페이지와 동일한 API 엔드포인트 사용 | +| 2 | **데이터 동일성** | API 응답/요청 데이터 형식 100% 동일 | +| 3 | **기존 페이지 반영** | 테스트 페이지에서 수정 → 기존 품목기준관리 페이지에 반영 | +| 4 | **품목관리 반영** | 테스트 페이지에서 수정 → 품목관리 동적 폼에 반영 | + +### 왜 이게 중요한가? + +``` +테스트 페이지 (Zustand) ──┐ + ├──→ 같은 API ──→ 같은 DB ──→ 품목관리 페이지 +기존 페이지 (Context) ────┘ + +→ 상태 관리 방식만 다르고, API/DB는 공유 +→ 테스트 페이지에서 수정한 내용이 품목관리 페이지에 그대로 적용되어야 함 +→ 이것이 성공하면 Zustand 리팩토링이 완전히 검증된 것 +``` + +--- + +## 1. 현재 문제점 분석 + +### 1.1 중복 상태 관리 (3방향 동기화) + +현재 `ItemMasterContext.tsx`에서 섹션 수정 시: + +```typescript +// updateSection() 함수 내부 (Line 1464-1486) +setItemPages(...) // 1. 계층구조 탭 +setSectionTemplates(...) // 2. 섹션 탭 +setIndependentSections(...) // 3. 독립 섹션 +``` + +**문제점**: +- 같은 데이터를 3곳에서 중복 관리 +- 한 곳 업데이트 누락 시 데이터 불일치 +- 모든 CRUD 함수에 동일 패턴 반복 +- 새 기능 추가 시 3곳 모두 수정 필요 + +### 1.2 현재 상태 변수 목록 (16개) + +| # | 상태 변수 | 설명 | 중복 여부 | +|---|----------|------|----------| +| 1 | `itemMasters` | 품목 마스터 | - | +| 2 | `specificationMasters` | 규격 마스터 | - | +| 3 | `materialItemNames` | 자재 품목명 | - | +| 4 | `itemCategories` | 품목 분류 | - | +| 5 | `itemUnits` | 단위 | - | +| 6 | `itemMaterials` | 재질 | - | +| 7 | `surfaceTreatments` | 표면처리 | - | +| 8 | `partTypeOptions` | 부품유형 옵션 | - | +| 9 | `partUsageOptions` | 부품용도 옵션 | - | +| 10 | `guideRailOptions` | 가이드레일 옵션 | - | +| 11 | `sectionTemplates` | 섹션 템플릿 | ⚠️ 중복 | +| 12 | `itemMasterFields` | 필드 마스터 | ⚠️ 중복 | +| 13 | `itemPages` | 페이지 (섹션/필드 포함) | ⚠️ 중복 | +| 14 | `independentSections` | 독립 섹션 | ⚠️ 중복 | +| 15 | `independentFields` | 독립 필드 | ⚠️ 중복 | +| 16 | `independentBomItems` | 독립 BOM | ⚠️ 중복 | + +**중복 문제가 있는 엔티티**: +- **섹션**: `sectionTemplates`, `itemPages.sections`, `independentSections` +- **필드**: `itemMasterFields`, `itemPages.sections.fields`, `independentFields` +- **BOM**: `itemPages.sections.bom_items`, `independentBomItems` + +--- + +## 2. 리팩토링 설계 + +### 2.1 정규화된 상태 구조 (Normalized State) + +```typescript +// stores/useItemMasterStore.ts +interface ItemMasterState { + // ===== 정규화된 엔티티 (ID 기반 딕셔너리) ===== + entities: { + pages: Record; + sections: Record; + fields: Record; + bomItems: Record; + }; + + // ===== ID 목록 (순서 관리) ===== + ids: { + pages: number[]; + independentSections: number[]; // page_id가 null인 섹션 + independentFields: number[]; // section_id가 null인 필드 + independentBomItems: number[]; // section_id가 null인 BOM + }; + + // ===== 참조 데이터 (중복 없음) ===== + references: { + itemMasters: ItemMaster[]; + specificationMasters: SpecificationMaster[]; + materialItemNames: MaterialItemName[]; + itemCategories: ItemCategory[]; + itemUnits: ItemUnit[]; + itemMaterials: ItemMaterial[]; + surfaceTreatments: SurfaceTreatment[]; + partTypeOptions: PartTypeOption[]; + partUsageOptions: PartUsageOption[]; + guideRailOptions: GuideRailOption[]; + }; + + // ===== UI 상태 ===== + ui: { + isLoading: boolean; + error: string | null; + selectedPageId: number | null; + selectedSectionId: number | null; + }; +} +``` + +### 2.2 엔티티 구조 + +```typescript +// 페이지 엔티티 (섹션 ID만 참조) +interface PageEntity { + id: number; + page_name: string; + item_type: string; + description?: string; + order_no: number; + is_active: boolean; + sectionIds: number[]; // 섹션 객체 대신 ID만 저장 + created_at?: string; + updated_at?: string; +} + +// 섹션 엔티티 (필드/BOM ID만 참조) +interface SectionEntity { + id: number; + title: string; + page_id: number | null; // null이면 독립 섹션 + order_no: number; + is_collapsible: boolean; + default_open: boolean; + fieldIds: number[]; // 필드 ID 목록 + bomItemIds: number[]; // BOM ID 목록 + created_at?: string; + updated_at?: string; +} + +// 필드 엔티티 +interface FieldEntity { + id: number; + field_key: string; + field_name: string; + field_type: string; + section_id: number | null; // null이면 독립 필드 + order_no: number; + is_required: boolean; + options?: any; + default_value?: any; + created_at?: string; + updated_at?: string; +} + +// BOM 엔티티 +interface BOMItemEntity { + id: number; + section_id: number | null; // null이면 독립 BOM + child_item_code: string; + child_item_name: string; + quantity: number; + unit: string; + order_no: number; + created_at?: string; + updated_at?: string; +} +``` + +### 2.3 수정 절차 비교 + +#### Before (현재): 3방향 동기화 + +```typescript +const updateSection = async (sectionId, updates) => { + const response = await api.update(sectionId, updates); + + // 1. itemPages 업데이트 + setItemPages(prev => prev.map(page => ({ + ...page, + sections: page.sections.map(s => s.id === sectionId ? {...s, ...updates} : s) + }))); + + // 2. sectionTemplates 업데이트 + setSectionTemplates(prev => prev.map(t => + t.id === sectionId ? {...t, ...updates} : t + )); + + // 3. independentSections 업데이트 + setIndependentSections(prev => prev.map(s => + s.id === sectionId ? {...s, ...updates} : s + )); +}; +``` + +#### After (Zustand): 1곳만 수정 + +```typescript +const updateSection = async (sectionId, updates) => { + const response = await api.update(sectionId, updates); + + // 딱 1곳만 수정하면 끝! + set((state) => ({ + entities: { + ...state.entities, + sections: { + ...state.entities.sections, + [sectionId]: { ...state.entities.sections[sectionId], ...updates } + } + } + })); +}; +``` + +### 2.4 파생 상태 (Selectors) + +```typescript +// 계층구조 탭용: 페이지 + 섹션 + 필드 조합 +const usePageWithDetails = (pageId: number) => { + return useItemMasterStore((state) => { + const page = state.entities.pages[pageId]; + if (!page) return null; + + return { + ...page, + sections: page.sectionIds.map(sId => { + const section = state.entities.sections[sId]; + return { + ...section, + fields: section.fieldIds.map(fId => state.entities.fields[fId]), + bom_items: section.bomItemIds.map(bId => state.entities.bomItems[bId]), + }; + }), + }; + }); +}; + +// 섹션 탭용: 모든 섹션 (페이지 연결 여부 무관) +const useAllSections = () => { + return useItemMasterStore((state) => + Object.values(state.entities.sections) + ); +}; + +// 독립 섹션만 +const useIndependentSections = () => { + return useItemMasterStore((state) => + state.ids.independentSections.map(id => state.entities.sections[id]) + ); +}; +``` + +--- + +## 3. 기능 매핑 체크리스트 + +### 3.1 페이지 관리 + +| 기존 함수 | 새 함수 | 상태 | +|----------|--------|------| +| `loadItemPages` | `loadPages` | ⬜ | +| `addItemPage` | `createPage` | ⬜ | +| `updateItemPage` | `updatePage` | ⬜ | +| `deleteItemPage` | `deletePage` | ⬜ | + +### 3.2 섹션 관리 + +| 기존 함수 | 새 함수 | 상태 | +|----------|--------|------| +| `loadSectionTemplates` | `loadSections` | ⬜ | +| `loadIndependentSections` | (loadSections에 통합) | ⬜ | +| `addSectionTemplate` | `createSection` | ⬜ | +| `addSectionToPage` | `createSectionInPage` | ⬜ | +| `createIndependentSection` | `createSection` (page_id: null) | ⬜ | +| `updateSectionTemplate` | `updateSection` | ⬜ | +| `updateSection` | `updateSection` | ⬜ | +| `deleteSectionTemplate` | `deleteSection` | ⬜ | +| `deleteSection` | `deleteSection` | ⬜ | +| `linkSectionToPage` | `linkSectionToPage` | ⬜ | +| `unlinkSectionFromPage` | `unlinkSectionFromPage` | ⬜ | +| `getSectionUsage` | `getSectionUsage` | ⬜ | + +### 3.3 필드 관리 + +| 기존 함수 | 새 함수 | 상태 | +|----------|--------|------| +| `loadItemMasterFields` | `loadFields` | ⬜ | +| `loadIndependentFields` | (loadFields에 통합) | ⬜ | +| `addItemMasterField` | `createField` | ⬜ | +| `addFieldToSection` | `createFieldInSection` | ⬜ | +| `createIndependentField` | `createField` (section_id: null) | ⬜ | +| `updateItemMasterField` | `updateField` | ⬜ | +| `updateField` | `updateField` | ⬜ | +| `deleteItemMasterField` | `deleteField` | ⬜ | +| `deleteField` | `deleteField` | ⬜ | +| `linkFieldToSection` | `linkFieldToSection` | ⬜ | +| `unlinkFieldFromSection` | `unlinkFieldFromSection` | ⬜ | +| `getFieldUsage` | `getFieldUsage` | ⬜ | + +### 3.4 BOM 관리 + +| 기존 함수 | 새 함수 | 상태 | +|----------|--------|------| +| `loadIndependentBomItems` | `loadBomItems` | ⬜ | +| `addBOMItem` | `createBomItem` | ⬜ | +| `createIndependentBomItem` | `createBomItem` (section_id: null) | ⬜ | +| `updateBOMItem` | `updateBomItem` | ⬜ | +| `deleteBOMItem` | `deleteBomItem` | ⬜ | + +### 3.5 참조 데이터 관리 + +| 기존 함수 | 새 함수 | 상태 | +|----------|--------|------| +| `addItemMaster` / `updateItemMaster` / `deleteItemMaster` | `itemMasterActions` | ⬜ | +| `addSpecificationMaster` / `updateSpecificationMaster` / `deleteSpecificationMaster` | `specificationActions` | ⬜ | +| `addMaterialItemName` / `updateMaterialItemName` / `deleteMaterialItemName` | `materialItemNameActions` | ⬜ | +| `addItemCategory` / `updateItemCategory` / `deleteItemCategory` | `categoryActions` | ⬜ | +| `addItemUnit` / `updateItemUnit` / `deleteItemUnit` | `unitActions` | ⬜ | +| `addItemMaterial` / `updateItemMaterial` / `deleteItemMaterial` | `materialActions` | ⬜ | +| `addSurfaceTreatment` / `updateSurfaceTreatment` / `deleteSurfaceTreatment` | `surfaceTreatmentActions` | ⬜ | +| `addPartTypeOption` / `updatePartTypeOption` / `deletePartTypeOption` | `partTypeActions` | ⬜ | +| `addPartUsageOption` / `updatePartUsageOption` / `deletePartUsageOption` | `partUsageActions` | ⬜ | +| `addGuideRailOption` / `updateGuideRailOption` / `deleteGuideRailOption` | `guideRailActions` | ⬜ | + +--- + +## 4. 구현 계획 + +### Phase 1: 기반 구축 ✅ 완료 (2025-12-20) + +- [x] Zustand, Immer 설치 +- [x] 테스트 페이지 라우트 생성 (`/items-management-test`) +- [x] 기본 스토어 구조 생성 (`useItemMasterStore.ts`) +- [x] 타입 정의 (`types.ts`) + +### Phase 2: API 연동 ✅ 완료 (2025-12-20) + +- [x] 기존 API 구조 분석 (`item-master.ts`) +- [x] API 응답 → 정규화 상태 변환 함수 (`normalizers.ts`) +- [x] 스토어에 `initFromApi()` 함수 구현 +- [x] 테스트 페이지에서 실제 API 데이터 로드 기능 추가 + +**생성된 파일**: +- `src/stores/item-master/normalizers.ts` - API 응답 정규화 함수 + +**테스트 페이지 기능**: +- "실제 API 로드" 버튼 - 백엔드 API에서 실제 데이터 로드 +- "테스트 데이터 로드" 버튼 - 하드코딩된 테스트 데이터 로드 +- 데이터 소스 표시 (API/테스트/없음) + +### Phase 3: 핵심 엔티티 구현 + +- [x] 페이지 CRUD 구현 (로컬 상태) +- [x] 섹션 CRUD 구현 (로컬 상태) +- [x] 필드 CRUD 구현 (로컬 상태) +- [x] BOM CRUD 구현 (로컬 상태) +- [x] link/unlink 기능 구현 (로컬 상태) +- [ ] API 연동 CRUD (DB 저장) - **다음 단계** + +### Phase 3: 참조 데이터 구현 + +- [ ] 품목 마스터 관리 +- [ ] 규격 마스터 관리 +- [ ] 분류/단위/재질 등 옵션 관리 + +### Phase 4: 파생 상태 & 셀렉터 + +- [ ] 계층구조 뷰용 셀렉터 +- [ ] 섹션 탭용 셀렉터 +- [ ] 필드 탭용 셀렉터 +- [ ] 독립 항목 셀렉터 + +### Phase 5: UI 연동 + +- [ ] 테스트 페이지 컴포넌트 생성 +- [ ] 기존 컴포넌트 재사용 (스토어만 교체) +- [ ] 동작 검증 + +### Phase 6: 검증 & 마이그레이션 + +- [ ] 기존 페이지와 1:1 동작 비교 +- [ ] 엣지 케이스 테스트 +- [ ] 성능 비교 +- [ ] 기존 페이지 마이그레이션 결정 + +--- + +## 5. 파일 구조 + +``` +src/ +├── stores/ +│ └── item-master/ +│ ├── useItemMasterStore.ts # 메인 스토어 +│ ├── slices/ +│ │ ├── pageSlice.ts # 페이지 액션 +│ │ ├── sectionSlice.ts # 섹션 액션 +│ │ ├── fieldSlice.ts # 필드 액션 +│ │ ├── bomSlice.ts # BOM 액션 +│ │ └── referenceSlice.ts # 참조 데이터 액션 +│ ├── selectors/ +│ │ ├── pageSelectors.ts # 페이지 파생 상태 +│ │ ├── sectionSelectors.ts # 섹션 파생 상태 +│ │ └── fieldSelectors.ts # 필드 파생 상태 +│ └── types.ts # 타입 정의 +│ +├── app/[locale]/(protected)/ +│ └── items-management-test/ +│ └── page.tsx # 테스트 페이지 +``` + +--- + +## 6. 테스트 시나리오 + +### 6.1 섹션 수정 동기화 테스트 + +``` +시나리오: 섹션 이름 수정 +1. 계층구조 탭에서 섹션 선택 +2. 섹션 이름 "기본정보" → "기본 정보" 수정 +3. 검증: + - [ ] 계층구조 탭에 반영 + - [ ] 섹션 탭에 반영 + - [ ] 독립 섹션(연결 해제 시) 반영 + - [ ] API 호출 1회만 발생 +``` + +### 6.2 필드 이동 테스트 + +``` +시나리오: 필드를 다른 섹션으로 이동 +1. 섹션 A에서 필드 선택 +2. 섹션 B로 이동 (unlink → link) +3. 검증: + - [ ] 섹션 A에서 필드 제거 + - [ ] 섹션 B에 필드 추가 + - [ ] 계층구조 탭 반영 + - [ ] 필드 탭에서 section_id 변경 +``` + +### 6.3 독립 → 연결 테스트 + +``` +시나리오: 독립 섹션을 페이지에 연결 +1. 독립 섹션 선택 +2. 페이지에 연결 (linkSectionToPage) +3. 검증: + - [ ] 독립 섹션 목록에서 제거 + - [ ] 페이지의 섹션 목록에 추가 + - [ ] 섹션 탭에서 page_id 변경 +``` + +--- + +## 7. 롤백 계획 + +문제 발생 시: +1. 테스트 페이지 라우트 제거 +2. 스토어 코드 삭제 +3. 기존 `ItemMasterContext` 그대로 사용 + +**리스크 최소화**: +- 기존 코드 수정 없음 +- 새 코드만 추가 +- 언제든 롤백 가능 + +--- + +## 8. 성공 기준 + +| 항목 | 기준 | +|-----|------| +| **기능 동등성** | 기존 모든 기능 100% 동작 | +| **동기화** | 1곳 수정으로 모든 뷰 업데이트 | +| **코드량** | CRUD 함수 코드 50% 이상 감소 | +| **버그** | 데이터 불일치 버그 0건 | +| **성능** | 기존 대비 동등 또는 향상 | + +--- + +## 변경 이력 + +| 날짜 | 작성자 | 내용 | +|-----|--------|------| +| 2025-12-20 | Claude | 초안 작성 | +| 2025-12-20 | Claude | Phase 1 완료 - 기반 구축 | +| 2025-12-20 | Claude | Phase 2 완료 - API 연동 (normalizers.ts, initFromApi) | \ No newline at end of file diff --git a/claudedocs/architecture/[NEXT-2025-12-20] zustand-refactoring-session-context.md b/claudedocs/architecture/[NEXT-2025-12-20] zustand-refactoring-session-context.md new file mode 100644 index 00000000..a2a38694 --- /dev/null +++ b/claudedocs/architecture/[NEXT-2025-12-20] zustand-refactoring-session-context.md @@ -0,0 +1,344 @@ +# 품목기준관리 Zustand 리팩토링 - 세션 컨텍스트 + +> 다음 세션에서 이 문서를 먼저 읽고 작업 이어가기 + +## 🎯 프로젝트 목표 + +**핵심 목표:** +1. 품목기준관리 100% 동일 기능 구현 +2. **더 유연한 데이터 관리** (Zustand 정규화 구조) +3. **개선된 UX** (Context 3방향 동기화 → Zustand 1곳 수정) + +**접근 방식:** +- 기존 컴포넌트 재사용 ❌ +- 테스트 페이지에서 완전히 새로 구현 ✅ +- 분리된 상태 유지 → 복구 시나리오 보장 + +--- + +## 세션 요약 (2025-12-22 - 11차 세션) + +### ✅ 오늘 완료된 작업 + +1. **기존 품목기준관리와 상세 기능 비교** + - 구현 완료율: 약 72% + - 핵심 CRUD 기능 모두 구현 확인 + +2. **누락된 핵심 기능 식별** + - 🔴 절대경로(absolute_path) 수정 - PathEditDialog + - 🔴 페이지 복제 - handleDuplicatePage + - 🔴 필드 조건부 표시 - ConditionalDisplayUI + - 🟡 칼럼 관리 - ColumnManageDialog + - 🟡 섹션/필드 사용 현황 표시 + +3. **브랜치 분리 완료** + - `feature/item-master-zustand` 브랜치 생성 + - 29개 파일, 8,248줄 커밋 + - master와 분리 관리 가능 + +--- + +## 세션 요약 (2025-12-21 - 10차 세션) + +### ✅ 오늘 완료된 작업 + +1. **기존 품목기준관리와 기능 비교 분석** + - 기존 페이지의 모든 핵심 기능 구현 확인 + - 커스텀 탭 관리는 기존 페이지에서도 비활성화(주석 처리)됨 + - 탭 관리 기능은 로컬 상태만 사용 (백엔드 미연동, 새로고침 시 초기화) + +2. **Phase D-2 (커스텀 탭 관리) 분석 결과** + - 기존 페이지의 "탭 관리" 버튼: 주석 처리됨 (미사용) + - 속성 하위 탭 관리: 로컬 상태로만 동작 (영속성 없음) + - **결론**: 선택적 기능으로 분류, 핵심 기능 구현 완료 + +--- + +## 세션 요약 (2025-12-21 - 9차 세션) + +### ✅ 완료된 작업 + +1. **속성 CRUD API 연동 완료** + - `types.ts`: PropertyActions 인터페이스 추가 + - `useItemMasterStore.ts`: addUnit, updateUnit, deleteUnit, addMaterial, updateMaterial, deleteMaterial, addTreatment, updateTreatment, deleteTreatment 구현 + - `item-master-api.ts`: UnitOptionRequest/Response 타입 수정 (unit_code, unit_name 사용) + +2. **Import 기능 구현 완료** + - `ImportSectionDialog.tsx`: 독립 섹션 목록에서 선택하여 페이지에 연결 + - `ImportFieldDialog.tsx`: 독립 필드 목록에서 선택하여 섹션에 연결 + - `dialogs/index.ts`: Import 다이얼로그 export 추가 + - `HierarchyTab.tsx`: 불러오기 버튼에 Import 다이얼로그 연결 + +3. **섹션 복제 API 연동 완료** + - `SectionsTab.tsx`: handleCloneSection 함수 구현 (API 연동 + toast 알림) + +4. **타입 수정** + - `transformers.ts`: transformUnitOptionResponse 수정 (unit_name, unit_code 사용) + - `useFormStructure.ts`: 단위 옵션 매핑 수정 (unit_name, unit_code 사용) + +--- + +### ✅ 완료된 Phase + +| Phase | 내용 | 상태 | +|-------|------|------| +| Phase 1 | Zustand 스토어 기본 구조 | ✅ | +| Phase 2 | API 연동 (initFromApi) | ✅ | +| Phase 3 | API CRUD 연동 (update 함수들) | ✅ | +| Phase A-1 | 계층구조 기본 표시 | ✅ | +| Phase A-2 | 드래그앤드롭 순서 변경 | ✅ | +| Phase A-3 | 인라인 편집 (페이지/섹션/경로) | ✅ | +| Phase B-1 | 페이지 CRUD 다이얼로그 | ✅ | +| Phase B-2 | 섹션 CRUD 다이얼로그 | ✅ | +| Phase B-3 | 필드 CRUD 다이얼로그 | ✅ | +| Phase B-4 | BOM 관리 UI | ✅ | +| Phase C-1 | 섹션 탭 구현 (SectionsTab.tsx) | ✅ | +| Phase C-2 | 항목 탭 구현 (FieldsTab.tsx) | ✅ | +| Phase D-1 | 속성 탭 기본 구조 (PropertiesTab.tsx) | ✅ | +| Phase E | Import 기능 (섹션/필드 불러오기) | ✅ | + +### ✅ 현재 상태: 핵심 기능 구현 완료 + +**Phase D-2 (커스텀 탭 관리)**: 선택적 기능으로 분류됨 +- 기존 페이지에서도 "탭 관리" 버튼은 주석 처리 (미사용) +- 속성 하위 탭 관리도 로컬 상태로만 동작 (백엔드 미연동) +- 필요 시 추후 구현 가능 + +--- + +## 📋 기능 비교 결과 + +### ✅ 구현 완료된 핵심 기능 + +| 기능 | 테스트 페이지 | 기존 페이지 | +|------|-------------|------------| +| 계층구조 관리 | ✅ | ✅ | +| 페이지 CRUD | ✅ | ✅ | +| 섹션 CRUD | ✅ | ✅ | +| 필드 CRUD | ✅ | ✅ | +| BOM 관리 | ✅ | ✅ | +| 드래그앤드롭 순서 변경 | ✅ | ✅ | +| 인라인 편집 | ✅ | ✅ | +| Import (섹션/필드) | ✅ | ✅ | +| 섹션 복제 | ✅ | ✅ | +| 단위/재질/표면처리 CRUD | ✅ | ✅ | +| 검색/필터 | ✅ | ✅ | + +### ⚠️ 선택적 기능 (기존 페이지에서도 제한적 사용) + +| 기능 | 상태 | 비고 | +|------|------|------| +| 커스텀 메인 탭 관리 | 미구현 | 기존 페이지에서 주석 처리됨 | +| 속성 하위 탭 관리 | 미구현 | 로컬 상태만 (영속성 없음) | +| 칼럼 관리 | 미구현 | 로컬 상태만 (영속성 없음) | + +--- + +## 📋 전체 기능 체크리스트 + +### Phase A: 기본 UI 구조 (계층구조 탭 완성) ✅ + +#### A-1. 계층구조 기본 표시 ✅ 완료 +- [x] 페이지 목록 표시 (좌측 패널) +- [x] 페이지 선택 시 섹션 목록 표시 (우측 패널) +- [x] 섹션 내부 필드 목록 표시 +- [x] 필드 타입별 뱃지 표시 +- [x] BOM 타입 섹션 구분 표시 + +#### A-2. 드래그앤드롭 순서 변경 ✅ 완료 +- [x] 섹션 드래그앤드롭 순서 변경 +- [x] 필드 드래그앤드롭 순서 변경 +- [x] 스토어 reorderSections 함수 구현 +- [x] 스토어 reorderFields 함수 구현 +- [x] DraggableSection 컴포넌트 생성 +- [x] DraggableField 컴포넌트 생성 + +#### A-3. 인라인 편집 ✅ 완료 +- [x] InlineEdit 재사용 컴포넌트 생성 +- [x] 페이지 이름 더블클릭 인라인 수정 +- [x] 섹션 제목 더블클릭 인라인 수정 +- [x] 절대경로 인라인 수정 + +--- + +### Phase B: CRUD 다이얼로그 ✅ + +#### B-1. 페이지 관리 ✅ 완료 +- [x] PageDialog 컴포넌트 (페이지 추가/수정) +- [x] DeleteConfirmDialog (재사용 가능한 삭제 확인) +- [x] 페이지 추가 버튼 연결 +- [x] 페이지 삭제 버튼 연결 + +#### B-2. 섹션 관리 ✅ 완료 +- [x] SectionDialog 컴포넌트 (섹션 추가/수정) +- [x] 섹션 삭제 다이얼로그 +- [x] 섹션 연결해제 다이얼로그 +- [x] 섹션 추가 버튼 연결 +- [x] ImportSectionDialog (섹션 불러오기) ✅ + +#### B-3. 필드 관리 ✅ 완료 +- [x] FieldDialog 컴포넌트 (필드 추가/수정) +- [x] 드롭다운 옵션 동적 관리 +- [x] 필드 삭제 다이얼로그 +- [x] 필드 연결해제 다이얼로그 +- [x] 필드 추가 버튼 연결 +- [x] ImportFieldDialog (필드 불러오기) ✅ + +#### B-4. BOM 관리 ✅ 완료 +- [x] BOMDialog 컴포넌트 (BOM 추가/수정) +- [x] BOM 항목 삭제 다이얼로그 +- [x] BOM 추가 버튼 연결 +- [x] BOM 수정 버튼 연결 + +--- + +### Phase C: 섹션 탭 + 항목 탭 ✅ + +#### C-1. 섹션 탭 ✅ 완료 +- [x] 모든 섹션 목록 표시 (연결된 + 독립) +- [x] 섹션 상세 정보 표시 +- [x] 섹션 내부 필드 표시 (확장/축소) +- [x] 일반 섹션 / BOM 섹션 탭 분리 +- [x] 페이지 연결 상태 표시 +- [x] 섹션 추가/수정/삭제 다이얼로그 연동 +- [x] 섹션 복제 기능 (API 연동 완료) ✅ + +#### C-2. 항목 탭 (마스터 필드) ✅ 완료 +- [x] 모든 필드 목록 표시 +- [x] 필드 상세 정보 표시 +- [x] 검색 기능 (필드명, 필드키, 타입) +- [x] 필터 기능 (전체/독립/연결된 필드) +- [x] 필드 추가/수정/삭제 다이얼로그 연동 +- [x] 독립 필드 → 섹션 연결 기능 + +--- + +### Phase D: 속성 탭 (진행 중) + +#### D-1. 속성 관리 ✅ 완료 +- [x] PropertiesTab.tsx 기본 구조 +- [x] 단위 관리 (CRUD) - API 연동 완료 +- [x] 재질 관리 (CRUD) - API 연동 완료 +- [x] 표면처리 관리 (CRUD) - API 연동 완료 +- [x] PropertyDialog (속성 옵션 추가) + +#### D-2. 탭 관리 (예정) +- [ ] 커스텀 탭 추가/수정/삭제 +- [ ] 속성 하위 탭 추가/수정/삭제 +- [ ] 탭 순서 변경 + +--- + +### Phase E: Import 기능 ✅ + +- [x] ImportSectionDialog (섹션 불러오기) +- [x] ImportFieldDialog (필드 불러오기) +- [x] HierarchyTab 불러오기 버튼 연결 + +--- + +## 📁 파일 구조 + +``` +src/stores/item-master/ +├── types.ts # 정규화된 엔티티 타입 + PropertyActions +├── useItemMasterStore.ts # Zustand 스토어 +├── normalizers.ts # API 응답 정규화 + +src/app/[locale]/(protected)/items-management-test/ +├── page.tsx # 테스트 페이지 메인 +├── components/ # 테스트 페이지 전용 컴포넌트 +│ ├── HierarchyTab.tsx # 계층구조 탭 ✅ +│ ├── DraggableSection.tsx # 드래그 섹션 ✅ +│ ├── DraggableField.tsx # 드래그 필드 ✅ +│ ├── InlineEdit.tsx # 인라인 편집 컴포넌트 ✅ +│ ├── SectionsTab.tsx # 섹션 탭 ✅ (복제 기능 추가) +│ ├── FieldsTab.tsx # 항목 탭 ✅ +│ ├── PropertiesTab.tsx # 속성 탭 ✅ +│ └── dialogs/ # 다이얼로그 컴포넌트 ✅ +│ ├── index.ts # 인덱스 ✅ +│ ├── DeleteConfirmDialog.tsx # 삭제 확인 ✅ +│ ├── PageDialog.tsx # 페이지 다이얼로그 ✅ +│ ├── SectionDialog.tsx # 섹션 다이얼로그 ✅ +│ ├── FieldDialog.tsx # 필드 다이얼로그 ✅ +│ ├── BOMDialog.tsx # BOM 다이얼로그 ✅ +│ ├── PropertyDialog.tsx # 속성 다이얼로그 ✅ +│ ├── ImportSectionDialog.tsx # 섹션 불러오기 ✅ +│ └── ImportFieldDialog.tsx # 필드 불러오기 ✅ +``` + +--- + +## 핵심 파일 위치 + +| 파일 | 용도 | +|-----|------| +| `claudedocs/architecture/[DESIGN-2025-12-20] item-master-zustand-refactoring.md` | 📋 설계 문서 | +| `src/stores/item-master/useItemMasterStore.ts` | 🏪 Zustand 스토어 | +| `src/stores/item-master/types.ts` | 📝 타입 정의 | +| `src/stores/item-master/normalizers.ts` | 🔄 API 응답 정규화 | +| `src/app/[locale]/(protected)/items-management-test/page.tsx` | 🧪 테스트 페이지 | +| `src/components/items/ItemMasterDataManagement.tsx` | 📚 기존 페이지 (참조용) | + +--- + +## 테스트 페이지 접속 + +``` +http://localhost:3000/ko/items-management-test +``` + +--- + +## 브랜치 정보 + +| 항목 | 값 | +|------|-----| +| 작업 브랜치 | `feature/item-master-zustand` | +| 기본 브랜치 | `master` (테스트 페이지 없음) | + +### 브랜치 작업 명령어 + +```bash +# 테스트 페이지 작업 시 +git checkout feature/item-master-zustand + +# master 최신 내용 반영 +git merge master + +# 테스트 완료 후 master에 합치기 +git checkout master +git merge feature/item-master-zustand +``` + +--- + +## 다음 세션 시작 명령 + +``` +누락된 기능 구현해줘 - 절대경로 수정부터 +``` + +또는 + +``` +테스트 페이지 실사용 테스트하고 버그 수정해줘 +``` + +--- + +## 남은 작업 + +### 🔴 누락된 핵심 기능 (100% 구현 위해 필요) +1. **절대경로(absolute_path) 수정** - PathEditDialog +2. **페이지 복제** - handleDuplicatePage +3. **필드 조건부 표시** - ConditionalDisplayUI + +### 🟡 추가 기능 +4. **칼럼 관리** - ColumnManageDialog +5. **섹션/필드 사용 현황 표시** + +### 🟢 마이그레이션 +6. **실사용 테스트**: 테스트 페이지에서 실제 데이터로 CRUD 테스트 +7. **버그 수정**: 발견되는 버그 즉시 수정 +8. **마이그레이션**: 테스트 완료 후 기존 페이지 대체 \ No newline at end of file diff --git a/claudedocs/guides/[DESIGN-2026-01-02] document-modal-common-component.md b/claudedocs/guides/[DESIGN-2026-01-02] document-modal-common-component.md new file mode 100644 index 00000000..1a3681bc --- /dev/null +++ b/claudedocs/guides/[DESIGN-2026-01-02] document-modal-common-component.md @@ -0,0 +1,276 @@ +# 문서 모달 공통 컴포넌트 설계 요구사항 + +> Last Updated: 2026-01-06 + +## 현황 분석 + +### 전체 문서 모달 목록 (10개) + +#### A. juil 비즈니스 모달 (프린트 중심) +| 컴포넌트 | 용도 | 헤더 구성 | 결재라인 | +|---------|------|----------|---------| +| ProcessWorkLogPreviewModal | 공정 작업일지 | 로고 + 제목 + 결재 | 3열 (자체 구현) | +| WorkLogModal | 생산 작업일지 | 로고 + 제목 + 결재 | 3열 (자체 구현) | +| EstimateDocumentModal | 견적서 | 제목 + 결재 | 3열 (자체 구현) | +| ContractDocumentModal | 계약서 | PDF iframe | 없음 | +| HandoverReportDocumentModal | 인수인계보고서 | 결재 먼저 | 4열 (자체 구현) | +| **OrderDocumentModal (juil)** | 🆕 발주서 | 제목만 | 없음 | + +#### B. 수주 문서 모달 +| 컴포넌트 | 용도 | 헤더 구성 | +|---------|------|----------| +| OrderDocumentModal (orders) | 수주문서 3종 | 제목만 (분기) | + +#### C. 전자결재 문서 (approval) +| 컴포넌트 | 용도 | 결재라인 | +|---------|------|---------| +| ProposalDocument | 품의서 | ⭐ **ApprovalLineBox** 사용 | +| ExpenseReportDocument | 지출결의서 | ⭐ **ApprovalLineBox** 사용 | +| ExpenseEstimateDocument | 지출예상내역서 | ⭐ **ApprovalLineBox** 사용 | + +--- + +## ⭐ 기존 공통 컴포넌트 발견 + +### ApprovalLineBox (이미 존재!) +**위치**: `src/components/approval/DocumentDetail/ApprovalLineBox.tsx` + +```tsx +interface ApprovalLineBoxProps { + drafter: Approver; // 작성자 + approvers: Approver[]; // 결재자 배열 (동적 열 개수) +} + +interface Approver { + id: string; + name: string; + position: string; + department: string; + status: 'pending' | 'approved' | 'rejected' | 'none'; + approvedAt?: string; +} +``` + +**특징**: +- ✅ 동적 열 개수 지원 (approvers 배열 길이에 따라) +- ✅ 상태 아이콘 표시 (승인/반려/대기) +- ✅ 구분/이름/부서 3행 구조 +- ⚠️ 현재 approval 문서에서만 사용 중 + +### 문제점 +- juil 문서들은 **자체 결재라인 구현** (코드 중복) +- 각 문서마다 결재라인 구조가 미묘하게 다름 + - 작업일지: 작성/검토/승인 + 날짜행 + - 견적서: 작성/승인 (2열) + - 인수인계: 작성/검토/승인/승인 (4열) + +--- + +## 공통 패턴 분석 + +### ✅ 완전히 동일한 패턴 +``` +1. 모달 프레임: Radix UI Dialog +2. 인쇄 처리: print-hidden + print-area 클래스 +3. 인쇄 유틸: printArea() 함수 (lib/print-utils.ts) +4. 용지 크기: max-w-[210mm] (A4 기준) +5. 레이아웃: 고정 헤더 + 버튼 영역 + 스크롤 문서 영역 +6. 모달 크기: max-w-[95vw] md:max-w-[800px] lg:max-w-[900px] +``` + +### 🔄 변동이 심한 영역 + +#### 1. 문서 헤더 레이아웃 +| 유형 | 문서 | 구조 | +|------|------|------| +| 3열 | 작업일지 | `[로고] [제목+코드] [결재]` | +| 2열 | 견적서, 품의서 | `[제목+번호] [결재]` | +| 1열+우측 | 인수인계 | `[결재 먼저] + [기본정보]` | +| 1열 중앙 | 발주서, 수주문서 | `[제목 중앙]` | + +#### 2. 결재라인 구성 +| 문서 | 열 구조 | 행 구조 | +|------|---------|---------| +| 작업일지 | 작성/검토/승인 | 구분/이름/부서/날짜 | +| 견적서 | 작성/승인 | 구분/이름/부서 | +| 인수인계 | 작성/검토/승인/승인 | 구분/이름/부서 | +| 전자결재 | **동적** (ApprovalLineBox) | 구분/이름/부서 | + +#### 3. 버튼 영역 +| 문서 | 버튼 구성 | +|------|----------| +| 견적서 | 수정, 상신, 인쇄 | +| 발주서 | 수정, 삭제, 인쇄 | +| 전자결재 | 수정, 복사, 승인, 반려, 상신 | + +--- + +## 공통 컴포넌트 제안 (수정) + +### 1. PrintableDocumentModal (Base) +모달 프레임 + 인쇄 기능만 담당 (변경 없음) + +```tsx +interface PrintableDocumentModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; + width?: 'sm' | 'md' | 'lg'; + actions?: ReactNode; // 버튼 영역 + children: ReactNode; // 문서 본문 +} +``` + +### 2. ApprovalLine (확장) +**기존 ApprovalLineBox 확장 또는 새로 통합** + +```tsx +interface ApprovalLineProps { + // 방법 1: 단순 열 지정 + columns?: 2 | 3 | 4; + approvers?: Array<{ + role: string; // '작성' | '검토' | '승인' + name: string; + department?: string; + date?: string; + status?: 'pending' | 'approved' | 'rejected'; + }>; + + // 방법 2: 기존 ApprovalLineBox 호환 + drafter?: Approver; + dynamicApprovers?: Approver[]; + + // 옵션 + showDateRow?: boolean; // 날짜행 표시 여부 + showStatusIcon?: boolean; // 상태 아이콘 표시 여부 +} +``` + +### 3. DocumentHeaderLayout (프리셋) + +```tsx +type HeaderVariant = + | 'three-column' // [로고] [제목] [결재] + | 'two-column' // [제목+번호] [결재] + | 'single-center' // [제목 중앙] + | 'approval-first' // [결재] + [정보 테이블] + + + + + + +``` + +--- + +## 컴포넌트 구조 제안 (수정) + +``` +src/components/common/document/ +├── PrintableDocumentModal.tsx # 기본 모달 프레임 +├── DocumentHeader/ +│ ├── index.tsx # 헤더 레이아웃 프리셋 +│ ├── DocumentTitle.tsx # 문서 타이틀 +│ └── CompanyLogo.tsx # 회사 로고 +├── ApprovalLine/ +│ ├── index.tsx # 통합 결재라인 (★ 핵심) +│ └── ApprovalLineBox.tsx # 기존 컴포넌트 이동/확장 +├── DocumentTable/ +│ ├── index.tsx # 기본 문서 테이블 +│ ├── InfoGrid.tsx # 정보 그리드 (2×4 등) +│ └── SummaryRow.tsx # 합계행 +└── index.ts # 배럴 export +``` + +--- + +## 마이그레이션 전략 + +### Phase 1: ApprovalLine 통합 (우선) +1. 기존 `ApprovalLineBox` → `common/document/ApprovalLine/`로 이동 +2. columns 기반 간단 모드 추가 +3. showDateRow, showStatusIcon 옵션 추가 + +### Phase 2: PrintableDocumentModal 생성 +1. 모달 프레임 공통화 +2. print-hidden/print-area 자동 적용 +3. 버튼 영역 슬롯 제공 + +### Phase 3: 기존 모달 리팩토링 +| 순서 | 모달 | 작업량 | +|------|------|-------| +| 1 | WorkLogModal 계열 | 구조 동일, 리팩토링 쉬움 | +| 2 | EstimateDocumentModal | 결재라인 교체 | +| 3 | 전자결재 문서들 | ApprovalLineBox 경로 변경만 | +| 4 | OrderDocumentModal (juil) | 결재라인 없음, 프레임만 적용 | +| 5 | HandoverReportDocumentModal | 4열 결재라인 | + +--- + +## 결정 필요 사항 + +### Q1. ApprovalLine 통합 방식 +- **A) 확장**: 기존 ApprovalLineBox에 옵션 추가 +- **B) 새로 작성**: columns 기반 단순 버전 + 기존 호환 어댑터 + +### Q2. 위치 결정 +- **A) common/document/**: 문서 전용 공통 컴포넌트 +- **B) approval/에서 re-export**: 기존 위치 유지, 공용 export + +### Q3. 날짜행 처리 +- **A) 옵션화**: `showDateRow={true}` +- **B) 별도 컴포넌트**: `ApprovalLineWithDate` + +--- + +## 예상 작업량 (수정) + +| 단계 | 내용 | 파일 수 | +|------|------|--------| +| 1 | ApprovalLine 통합 | 3개 | +| 2 | PrintableDocumentModal | 2개 | +| 3 | DocumentHeader 컴포넌트 | 3개 | +| 4 | 기존 모달 리팩토링 | 10개 | + +**총 예상**: ~18개 파일 수정/생성 + +--- + +## 참고: 인쇄 유틸리티 + +```ts +// src/lib/print-utils.ts +printArea(options?: { title?: string; styles?: string }) +``` + +- `.print-area` 클래스 요소를 새 창에서 인쇄 +- A4 용지 설정 자동 적용 +- 기존 스타일시트 자동 로드 + +--- + +## 관련 파일 경로 + +``` +문서 모달 관련 파일들: + +src/components/ +├── process-management/ +│ └── ProcessWorkLogPreviewModal.tsx +├── production/WorkerScreen/ +│ └── WorkLogModal.tsx +├── orders/documents/ +│ └── OrderDocumentModal.tsx (수주) +├── approval/DocumentDetail/ +│ ├── ApprovalLineBox.tsx ⭐ 기존 공통 +│ ├── ProposalDocument.tsx +│ ├── ExpenseReportDocument.tsx +│ ├── ExpenseEstimateDocument.tsx +│ └── types.ts +└── business/juil/ + ├── estimates/modals/EstimateDocumentModal.tsx + ├── contract/modals/ContractDocumentModal.tsx + ├── handover-report/modals/HandoverReportDocumentModal.tsx + └── order-management/modals/OrderDocumentModal.tsx 🆕 +``` \ No newline at end of file diff --git a/claudedocs/guides/[GUIDE] print-area-utility.md b/claudedocs/guides/[GUIDE] print-area-utility.md new file mode 100644 index 00000000..ed38dad9 --- /dev/null +++ b/claudedocs/guides/[GUIDE] print-area-utility.md @@ -0,0 +1,194 @@ +# 인쇄 모달 printArea 유틸리티 적용 가이드 + +> 작성일: 2026-01-02 +> 적용 범위: 모든 인쇄 가능한 모달/다이얼로그 + +## 개요 + +기존 `window.print()` 방식은 Radix UI Dialog 포털 구조로 인해 CSS `@media print` 제어가 어렵고, 인쇄 시 모달 헤더/버튼이 함께 출력되거나 여러 페이지로 나뉘는 문제가 있었습니다. + +이를 해결하기 위해 JavaScript 기반 `printArea()` 유틸리티를 도입하여 `.print-area` 영역만 새 창에서 인쇄하도록 통일했습니다. + +## 공통 컴포넌트 변경 + +### 1. print-utils.ts (신규) + +**파일 위치**: `/src/lib/print-utils.ts` + +```typescript +interface PrintOptions { + title?: string; // 브라우저 인쇄 다이얼로그에 표시될 제목 + styles?: string; // 추가 CSS 스타일 + closeAfterPrint?: boolean; // 인쇄 후 창 닫기 (기본: true) +} + +// 특정 요소 인쇄 +export function printElement( + elementOrSelector: HTMLElement | string, + options?: PrintOptions +): void; + +// .print-area 클래스 요소 인쇄 (주로 사용) +export function printArea(options?: PrintOptions): void; +``` + +**동작 방식**: +1. 새 창 열기 +2. 현재 페이지의 스타일시트 복사 +3. `.print-area` 요소 내용 복제 +4. `.print-hidden` 요소 제거 +5. A4 용지에 맞는 인쇄 스타일 적용 +6. 자동 인쇄 실행 후 창 닫기 + +### 2. globals.css 인쇄 스타일 (간소화) + +**파일 위치**: `/src/app/globals.css` + +```css +@media print { + @page { + size: A4 portrait; + margin: 10mm; + } + + * { + -webkit-print-color-adjust: exact !important; + print-color-adjust: exact !important; + } + + html, body { + background: white !important; + } + + .print-hidden { + display: none !important; + } +} +``` + +## 적용된 모달 목록 + +| 컴포넌트 | 파일 경로 | 인쇄 제목 | +|---------|----------|----------| +| DocumentDetailModal | `src/components/approval/DocumentDetail/index.tsx` | 문서 타입별 (품의서, 기안서 등) | +| ProcessWorkLogPreviewModal | `src/components/process-management/ProcessWorkLogPreviewModal.tsx` | 작업일지 템플릿명 | +| ReceivingReceiptDialog | `src/components/material/ReceivingManagement/ReceivingReceiptDialog.tsx` | 입고증 인쇄 | +| WorkLogModal | `src/components/production/WorkerScreen/WorkLogModal.tsx` | 작업일지 인쇄 | +| OrderDocumentModal | `src/components/orders/documents/OrderDocumentModal.tsx` | 계약서/거래명세서/발주서 | +| ShipmentDetail | `src/components/outbound/ShipmentManagement/ShipmentDetail.tsx` | 출고증/거래명세서/납품확인서 | +| EstimateDocumentModal | `src/components/business/juil/estimates/modals/EstimateDocumentModal.tsx` | 견적서 인쇄 | +| ContractDocumentModal | `src/components/business/juil/contract/modals/ContractDocumentModal.tsx` | 계약서 인쇄 | + +## 사용 방법 + +### 기본 사용법 + +```tsx +import { printArea } from '@/lib/print-utils'; + +// 인쇄 핸들러 +const handlePrint = () => { + printArea({ title: '문서 인쇄' }); +}; +``` + +### 모달 구조 규칙 + +인쇄 가능한 모달은 다음 구조를 따라야 합니다: + +```tsx + + + {/* 헤더 영역 - 인쇄에서 제외 */} +
+

문서 제목

+ + +
+ + {/* 버튼 영역 - 인쇄에서 제외 */} +
+ + +
+ + {/* 문서 영역 - 이 영역만 인쇄됨 */} +
+ {/* 실제 문서 내용 */} +
+
+
+``` + +### CSS 클래스 규칙 + +| 클래스 | 용도 | +|--------|------| +| `.print-area` | 인쇄될 영역 (필수) | +| `.print-hidden` | 인쇄에서 제외할 영역 (헤더, 버튼 등) | + +## 이전 방식 vs 새 방식 + +### 이전 방식 (문제점) + +```tsx +const handlePrint = () => { + window.print(); // 전체 페이지 인쇄 시도 +}; +``` + +**문제점**: +- Radix UI 포털 구조로 CSS `@media print` 제어 어려움 +- `visibility: hidden` 사용 시 빈 공간으로 인해 3-4페이지로 출력 +- `display: none` 사용 시 빈 페이지 출력 +- 모달 헤더/버튼이 함께 인쇄됨 + +### 새 방식 (해결) + +```tsx +const handlePrint = () => { + printArea({ title: '문서 인쇄' }); +}; +``` + +**장점**: +- 새 창에서 `.print-area` 내용만 추출하여 인쇄 +- Radix UI 포털 구조 영향 없음 +- 항상 1페이지로 깔끔하게 인쇄 +- 문서 내용만 인쇄 (헤더/버튼 제외) + +## 새 인쇄 모달 추가 시 + +1. `printArea` import 추가 +2. `handlePrint` 함수에서 `printArea()` 호출 +3. 모달 구조에 `.print-hidden` / `.print-area` 클래스 적용 + +```tsx +import { printArea } from '@/lib/print-utils'; + +export function NewDocumentModal() { + const handlePrint = () => { + printArea({ title: '새 문서 인쇄' }); + }; + + return ( + + +
+ {/* 헤더/버튼 */} +
+
+ {/* 인쇄될 문서 내용 */} +
+
+
+ ); +} +``` + +## 주의사항 + +1. **`.print-area` 클래스 필수**: 인쇄 영역에 반드시 `.print-area` 클래스 적용 +2. **중첩 `.print-area` 금지**: 하나의 모달에 `.print-area`는 하나만 존재해야 함 +3. **스타일 복제**: 인쇄 시 현재 페이지의 스타일시트가 자동으로 복사됨 +4. **팝업 차단 주의**: 브라우저 팝업 차단 시 인쇄 창이 열리지 않을 수 있음 diff --git a/claudedocs/guides/[IMPL-2026-01-05] stat-cards-grid-layout.md b/claudedocs/guides/[IMPL-2026-01-05] stat-cards-grid-layout.md new file mode 100644 index 00000000..fe266070 --- /dev/null +++ b/claudedocs/guides/[IMPL-2026-01-05] stat-cards-grid-layout.md @@ -0,0 +1,60 @@ +# StatCards 컴포넌트 레이아웃 변경 + +## 변경일 +2026-01-05 + +## 변경 파일 +`/src/components/organisms/StatCards.tsx` + +## 변경 내용 + +### Before (flex 기반) +```tsx +
+``` +- 모바일: 세로 1열 +- SM 이상: 가로 한 줄로 모든 카드 표시 (`flex-1`) + +### After (grid 기반) +```tsx +
+``` +- 모바일: 2열 그리드 +- SM 이상: 3열 그리드 + +## 변경 사유 + +### 문제점 +- 급여관리 등 카드가 6개인 페이지에서 한 줄에 모든 카드가 들어가면 각 카드가 너무 좁아짐 +- PC 화면에서도 카드 내용이 빽빽하게 보여 가독성 저하 + +### 해결 +- grid 기반 레이아웃으로 변경하여 PC에서 3개씩 2줄로 표시 +- 각 카드가 충분한 너비를 확보하여 가독성 향상 +- 카드 개수에 따라 자연스럽게 줄바꿈 + +## 영향 범위 +`StatCards` 컴포넌트는 공통 컴포넌트로, 다음 템플릿에서 사용: +- `IntegratedListTemplateV2` +- `ListPageTemplate` + +해당 템플릿을 사용하는 모든 페이지에 적용됨. + +## 레이아웃 예시 + +### 카드 6개 (급여관리) +``` +| 카드1 | 카드2 | 카드3 | +| 카드4 | 카드5 | 카드6 | +``` + +### 카드 4개 +``` +| 카드1 | 카드2 | 카드3 | +| 카드4 | | | +``` + +### 카드 3개 +``` +| 카드1 | 카드2 | 카드3 | +``` \ No newline at end of file diff --git a/claudedocs/item-master/[IMPL-2025-12-24] item-master-test-and-zustand.md b/claudedocs/item-master/[IMPL-2025-12-24] item-master-test-and-zustand.md new file mode 100644 index 00000000..d24f87dd --- /dev/null +++ b/claudedocs/item-master/[IMPL-2025-12-24] item-master-test-and-zustand.md @@ -0,0 +1,132 @@ +# 품목기준관리 테스트 및 Zustand 도입 체크리스트 + +> **브랜치**: `feature/item-master-zustand` +> **작성일**: 2025-12-24 +> **목표**: 훅 분리 완료 후 수동 테스트 → Zustand 도입 + +--- + +## 현재 상태 + +| 항목 | 상태 | +|------|------| +| 훅 분리 작업 | ✅ 완료 (2025-12-24) | +| 메인 컴포넌트 줄 수 | 1,799줄 → 971줄 (46% 감소) | +| 타입 에러 수정 | ✅ 완료 (55개 → 0개) | +| 무한 로딩 버그 수정 | ✅ 완료 | +| **Zustand 연동** | ✅ 완료 (2025-12-24) | +| 빌드 | ✅ 성공 | + +--- + +## Phase 1: 훅 분리 작업 ✅ + +### 완료된 훅 (11개) + +| # | 훅 이름 | 용도 | 상태 | +|---|--------|------|------| +| 1 | usePageManagement | 페이지 CRUD | ✅ 기존 | +| 2 | useSectionManagement | 섹션 CRUD | ✅ 기존 | +| 3 | useFieldManagement | 필드 CRUD | ✅ 기존 | +| 4 | useMasterFieldManagement | 마스터 필드 | ✅ 기존 | +| 5 | useTemplateManagement | 템플릿 관리 | ✅ 기존 | +| 6 | useAttributeManagement | 속성/옵션 관리 | ✅ 기존 | +| 7 | useTabManagement | 탭 관리 | ✅ 기존 | +| 8 | useInitialDataLoading | 초기 데이터 로딩 | ✅ 신규 | +| 9 | useImportManagement | 섹션/필드 불러오기 | ✅ 신규 | +| 10 | useReorderManagement | 순서 변경 | ✅ 신규 | +| 11 | useDeleteManagement | 삭제 관리 | ✅ 신규 | + +### UI 컴포넌트 분리 (1개) + +| # | 컴포넌트 | 용도 | 상태 | +|---|---------|------|------| +| 1 | AttributeTabContent | 속성 탭 (~500줄) | ✅ 완료 | + +--- + +## Phase 2: Zustand 연동 ✅ + +### 2.1 구조 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ useInitialDataLoading │ +│ ┌─────────────────────┐ ┌─────────────────────────────┐│ +│ │ Context 로드 │ AND │ Zustand Store 로드 ││ +│ │ (기존 호환성 유지) │ │ (정규화된 상태) ││ +│ └─────────────────────┘ └─────────────────────────────┘│ +│ ↓ ↓ │ +│ 기존 컴포넌트 → Context 새 컴포넌트 → useItemMasterStore│ +└─────────────────────────────────────────────────────────────┘ +``` + +### 2.2 연동 방식: 병행 운영 + +- **Context**: 기존 컴포넌트 호환성 유지 +- **Zustand**: 새 컴포넌트에서 직접 사용 가능 +- **점진적 마이그레이션**: Context → Zustand로 단계적 전환 + +### 2.3 수정된 파일 + +| 파일 | 변경 내용 | +|------|----------| +| `useInitialDataLoading.ts` | `useItemMasterStore` import, `initFromApi()` 호출 | + +### 2.4 Zustand Store 기능 + +`src/stores/item-master/useItemMasterStore.ts` (1,139줄) + +| 영역 | 기능 | +|------|------| +| 페이지 | loadPages, createPage, updatePage, deletePage | +| 섹션 | loadSections, createSection, updateSection, deleteSection, reorderSections | +| 필드 | loadFields, createField, updateField, deleteField, reorderFields | +| BOM | loadBomItems, createBomItem, updateBomItem, deleteBomItem | +| 속성 | addUnit, updateUnit, deleteUnit, addMaterial, updateMaterial, deleteMaterial | +| API | initFromApi() - API 호출 후 정규화된 상태 저장 | + +--- + +## Phase 3: 테스트 (다음 단계) + +### 3.1 품목 유형별 등록 테스트 + +| # | 품목 유형 | 테스트 URL | 상태 | +|---|----------|-----------|------| +| 1 | FG (제품) | `/ko/items/create` → 제품 선택 | ⬜ | +| 2 | PT (부품) - 절곡 | `/ko/items/create` → 부품 → 절곡부품 | ⬜ | +| 3 | PT (부품) - 조립 | `/ko/items/create` → 부품 → 조립부품 | ⬜ | +| 4 | PT (부품) - 구매 | `/ko/items/create` → 부품 → 구매부품 | ⬜ | +| 5 | SM (부자재) | `/ko/items/create` → 부자재 선택 | ⬜ | +| 6 | RM (원자재) | `/ko/items/create` → 원자재 선택 | ⬜ | +| 7 | CS (소모품) | `/ko/items/create` → 소모품 선택 | ⬜ | + +--- + +## 작업 로그 + +| 날짜 | 작업 내용 | 커밋 | +|------|----------|------| +| 2025-12-24 | Phase 1+2 훅/컴포넌트 분리 | `a823ae0` | +| 2025-12-24 | unused 코드 정리 및 import 최적화 | `1664599` | +| 2025-12-24 | 타입 에러 및 무한 로딩 버그 수정 | `028932d` | +| 2025-12-24 | **Zustand 연동 완료** | (현재) | + +--- + +## 다음 단계 + +1. 수동 테스트 진행 +2. 새 컴포넌트에서 `useItemMasterStore` 직접 사용 +3. Context 의존성 점진적 제거 +4. 동적 페이지 생성 구현 + +--- + +## 참고 문서 + +- `[PLAN-2025-12-24] hook-extraction-plan.md` - 훅 분리 계획서 +- `src/stores/item-master/useItemMasterStore.ts` - Zustand Store +- `src/stores/item-master/types.ts` - Store 타입 정의 +- `src/stores/item-master/normalizers.ts` - API 응답 정규화 diff --git a/claudedocs/item-master/[NEXT-2025-12-24] item-master-refactoring-session.md b/claudedocs/item-master/[NEXT-2025-12-24] item-master-refactoring-session.md new file mode 100644 index 00000000..32d16fe9 --- /dev/null +++ b/claudedocs/item-master/[NEXT-2025-12-24] item-master-refactoring-session.md @@ -0,0 +1,134 @@ +# 품목기준관리 리팩토링 세션 컨텍스트 + +> **브랜치**: `feature/item-master-zustand` +> **날짜**: 2025-12-24 +> **상태**: Phase 2 완료, 커밋 대기 + +--- + +## 세션 요약 (12차 세션) + +### 완료된 작업 +- [x] 브랜치 상태 확인 (`feature/item-master-zustand`) +- [x] 기존 작업 혼동 정리 (품목관리 CRUD vs 품목기준관리 설정) +- [x] 작업 대상 파일 확인 (`ItemMasterDataManagement.tsx` - 1,799줄) +- [x] 기존 훅 분리 상태 파악 (7개 훅 이미 존재) +- [x] `ItemMasterDataManagement.tsx` 상세 분석 완료 +- [x] 훅 분리 계획서 작성 (`[PLAN-2025-12-24] hook-extraction-plan.md`) +- [x] **Phase 1: 신규 훅 4개 생성** + - `useInitialDataLoading.ts` - 초기 데이터 로딩 (~130줄) + - `useImportManagement.ts` - 섹션/필드 Import (~100줄) + - `useReorderManagement.ts` - 드래그앤드롭 순서 변경 (~80줄) + - `useDeleteManagement.ts` - 삭제/언링크 핸들러 (~100줄) +- [x] **Phase 2: UI 컴포넌트 2개 생성** + - `AttributeTabContent.tsx` - 속성 탭 콘텐츠 (~340줄) + - `ItemMasterDialogs.tsx` - 다이얼로그 통합 (~540줄) +- [x] 빌드 테스트 통과 + +### 현재 상태 +- **메인 컴포넌트**: 1,799줄 → ~1,478줄 (약 320줄 감소) +- **신규 훅**: 4개 생성 및 통합 +- **신규 UI 컴포넌트**: 2개 생성 (향후 추가 통합 가능) +- **빌드**: 통과 + +### 다음 TODO (커밋 후) +1. Git 커밋 (Phase 1, 2 변경사항) +2. Phase 3: 추가 코드 정리 (선택적) + - 속성 탭 내용을 `AttributeTabContent`로 완전 대체 (추가 ~500줄 감소 가능) + - 다이얼로그들을 `ItemMasterDialogs`로 완전 대체 +3. Zustand 도입 (3방향 동기화 문제 해결) + +--- + +## 핵심 정보 + +### 페이지 구분 (중요!) + +| 페이지 | URL | 컴포넌트 | 상태 | +|--------|-----|----------|------| +| 품목관리 CRUD | `/items/` | `DynamicItemForm` | ✅ 훅 분리 완료 (master 적용됨) | +| **품목기준관리 설정** | `/master-data/item-master-data-management` | `ItemMasterDataManagement` | ⏳ **훅 분리 진행 중** | + +### 현재 파일 구조 + +``` +src/components/items/ItemMasterDataManagement/ +├── ItemMasterDataManagement.tsx ← ~1,478줄 (리팩토링 후) +├── hooks/ (11개 - 7개 기존 + 4개 신규) +│ ├── usePageManagement.ts +│ ├── useSectionManagement.ts +│ ├── useFieldManagement.ts +│ ├── useMasterFieldManagement.ts +│ ├── useTemplateManagement.ts +│ ├── useAttributeManagement.ts +│ ├── useTabManagement.ts +│ ├── useInitialDataLoading.ts ← NEW +│ ├── useImportManagement.ts ← NEW +│ ├── useReorderManagement.ts ← NEW +│ └── useDeleteManagement.ts ← NEW +├── components/ (5개 - 3개 기존 + 2개 신규) +│ ├── DraggableSection.tsx +│ ├── DraggableField.tsx +│ ├── ConditionalDisplayUI.tsx +│ ├── AttributeTabContent.tsx ← NEW +│ └── ItemMasterDialogs.tsx ← NEW +├── services/ (6개) +├── dialogs/ (13개) +├── tabs/ (4개) +└── utils/ (1개) +``` + +### 브랜치 상태 + +``` +master (원본 보존) + │ + └── feature/item-master-zustand (현재) + ├── Zustand 테스트 페이지 (/items-management-test/) - 놔둠 + ├── Zustand 스토어 (stores/item-master/) - 나중에 사용 + └── 기존 품목기준관리 페이지 - 훅 분리 진행 중 +``` + +### 작업 진행률 + +``` +시작: ItemMasterDataManagement.tsx 1,799줄 + ↓ Phase 1: 훅 분리 (4개 신규 훅) +현재: ~1,478줄 (-321줄, -18%) + ↓ Phase 2: UI 컴포넌트 분리 (2개 신규 컴포넌트 생성) + ↓ Phase 3: 추가 통합 (선택적) +목표: ~500줄 (메인 컴포넌트) + ↓ Zustand 적용 +최종: 3방향 동기화 문제 해결 +``` + +--- + +## 생성된 파일 목록 + +### 신규 훅 (Phase 1) +1. `hooks/useInitialDataLoading.ts` - 초기 데이터 로딩, 에러 처리 +2. `hooks/useImportManagement.ts` - 섹션/필드 Import 다이얼로그 상태 및 핸들러 +3. `hooks/useReorderManagement.ts` - 드래그앤드롭 순서 변경 +4. `hooks/useDeleteManagement.ts` - 삭제, 언링크, 초기화 핸들러 + +### 신규 UI 컴포넌트 (Phase 2) +1. `components/AttributeTabContent.tsx` - 속성 탭 전체 UI +2. `components/ItemMasterDialogs.tsx` - 모든 다이얼로그 통합 렌더링 + +--- + +## 참고 문서 + +- `[PLAN-2025-12-24] hook-extraction-plan.md` - 훅 분리 계획서 (상세) +- `[DESIGN-2025-12-20] item-master-zustand-refactoring.md` - Zustand 설계서 +- `[IMPL-2025-12-24] item-master-test-and-zustand.md` - 테스트 체크리스트 + +--- + +## 다음 세션 시작 명령 + +``` +품목기준관리 설정 페이지(ItemMasterDataManagement.tsx) 추가 리팩토링 또는 Zustand 도입 진행해줘. +[NEXT-2025-12-24] item-master-refactoring-session.md 문서 확인하고 시작해. +``` diff --git a/claudedocs/item-master/[PLAN-2025-12-24] hook-extraction-plan.md b/claudedocs/item-master/[PLAN-2025-12-24] hook-extraction-plan.md new file mode 100644 index 00000000..b4a6bcc1 --- /dev/null +++ b/claudedocs/item-master/[PLAN-2025-12-24] hook-extraction-plan.md @@ -0,0 +1,270 @@ +# ItemMasterDataManagement 훅 분리 계획서 + +> **날짜**: 2025-12-24 +> **대상 파일**: `src/components/items/ItemMasterDataManagement.tsx` (1,799줄) +> **목표**: ~500줄로 축소 + +--- + +## 현재 구조 분석 + +### 파일 구성 + +| 구간 | 줄 수 | 내용 | +|------|-------|------| +| Import | 1-61 | React, UI, 다이얼로그, 훅 import | +| 상수 | 63-91 | ITEM_TYPE_OPTIONS, INPUT_TYPE_OPTIONS | +| Context 구조분해 | 94-124 | useItemMaster에서 20+개 함수/상태 | +| 훅 초기화 | 127-286 | 7개 훅 + 150+개 상태 구조분해 | +| useMemo | 298-372 | sectionsAsTemplates 변환 | +| useState/useEffect | 374-504 | 로딩, 에러, 모바일, Import 상태 | +| 핸들러 | 519-743 | Import, Clone, Delete, Reorder | +| UI 렌더링 | 746-1799 | Tabs + 13개 다이얼로그 | + +### 기존 훅 (7개) + +``` +src/components/items/ItemMasterDataManagement/hooks/ +├── usePageManagement.ts - 페이지 CRUD +├── useSectionManagement.ts - 섹션 CRUD +├── useFieldManagement.ts - 필드 CRUD +├── useMasterFieldManagement.ts - 마스터 필드 CRUD +├── useTemplateManagement.ts - 템플릿 관리 +├── useAttributeManagement.ts - 속성 관리 +└── useTabManagement.ts - 탭 관리 +``` + +--- + +## 분리 계획 + +### Phase 1: 신규 훅 생성 (4개) + +#### 1. `useInitialDataLoading` (~100줄 분리) + +**분리 대상:** +- 초기 데이터 로딩 useEffect (387-492줄) +- 로딩/에러 상태 (isInitialLoading, error) +- transformers 호출 로직 + +**반환값:** +```typescript +{ + isInitialLoading: boolean; + error: string | null; + reload: () => Promise; +} +``` + +#### 2. `useImportManagement` (~80줄 분리) + +**분리 대상:** +- Import 다이얼로그 상태 (512-516줄) +- handleImportSection (519-530줄) +- handleImportField (540-559줄) +- handleCloneSection (562-570줄) + +**반환값:** +```typescript +{ + // 상태 + isImportSectionDialogOpen, setIsImportSectionDialogOpen, + isImportFieldDialogOpen, setIsImportFieldDialogOpen, + selectedImportSectionId, setSelectedImportSectionId, + selectedImportFieldId, setSelectedImportFieldId, + importFieldTargetSectionId, setImportFieldTargetSectionId, + // 핸들러 + handleImportSection, + handleImportField, + handleCloneSection, +} +``` + +#### 3. `useReorderManagement` (~60줄 분리) + +**분리 대상:** +- moveSection (650-668줄) +- moveField (672-702줄) + +**반환값:** +```typescript +{ + moveSection: (dragIndex: number, hoverIndex: number) => Promise; + moveField: (sectionId: number, dragFieldId: number, hoverFieldId: number) => Promise; +} +``` + +#### 4. `useDeleteManagement` (~50줄 분리) + +**분리 대상:** +- handleDeletePageWithTracking (582-588줄) +- handleDeleteSectionWithTracking (591-597줄) +- handleUnlinkFieldWithTracking (601-609줄) +- handleResetAllData (705-743줄) + +**반환값:** +```typescript +{ + handleDeletePage: (pageId: number) => void; + handleDeleteSection: (pageId: number, sectionId: number) => void; + handleUnlinkField: (pageId: string, sectionId: string, fieldId: string) => Promise; + handleResetAllData: () => void; +} +``` + +### Phase 2: UI 컴포넌트 분리 (2개) + +#### 1. `AttributeTabContent` (~400줄 분리) + +**분리 대상:** +- 속성 탭 내용 (807-1331줄) +- 단위/재질/표면처리 반복 UI 통합 + +**Props:** +```typescript +interface AttributeTabContentProps { + activeAttributeTab: string; + attributeSubTabs: AttributeSubTab[]; + unitOptions: UnitOption[]; + materialOptions: MaterialOption[]; + surfaceTreatmentOptions: SurfaceTreatmentOption[]; + customAttributeOptions: Record; + attributeColumns: Record; + itemMasterFields: ItemMasterField[]; + // 핸들러들... +} +``` + +#### 2. `ItemMasterDialogs` (~280줄 분리) + +**분리 대상:** +- 13개 다이얼로그 렌더링 (1442-1797줄) + +**Props:** +```typescript +interface ItemMasterDialogsProps { + // 모든 다이얼로그 관련 props +} +``` + +### Phase 3: 코드 정리 + +1. **래퍼 함수 제거** (~30줄) + - `handleAddSectionWrapper` 등을 훅 내부로 이동 + - `selectedPage`를 훅 파라미터로 전달 + +2. **unused 변수 정리** + - `_mounted`, `_isLoading` 등 제거 + +3. **Import 최적화** + - 사용하지 않는 import 제거 + +--- + +## 예상 결과 + +### 줄 수 변화 + +| 항목 | 현재 | 분리 후 | +|------|------|---------| +| Import | 61 | 40 | +| 상수 | 28 | 28 | +| 훅 사용 | 160 | 60 | +| useMemo | 75 | 75 | +| 상태/Effect | 130 | 20 | +| 핸들러 | 225 | 30 | +| UI 렌더링 | 1,053 | 300 | +| **합계** | **1,799** | **~550** | + +### 새 파일 구조 + +``` +src/components/items/ItemMasterDataManagement/ +├── ItemMasterDataManagement.tsx ← ~550줄 (메인) +├── hooks/ +│ ├── index.ts +│ ├── usePageManagement.ts (기존) +│ ├── useSectionManagement.ts (기존) +│ ├── useFieldManagement.ts (기존) +│ ├── useMasterFieldManagement.ts (기존) +│ ├── useTemplateManagement.ts (기존) +│ ├── useAttributeManagement.ts (기존) +│ ├── useTabManagement.ts (기존) +│ ├── useInitialDataLoading.ts ← NEW +│ ├── useImportManagement.ts ← NEW +│ ├── useReorderManagement.ts ← NEW +│ └── useDeleteManagement.ts ← NEW +├── components/ +│ ├── AttributeTabContent.tsx ← NEW +│ └── ItemMasterDialogs.tsx ← NEW +├── dialogs/ (기존 13개) +├── tabs/ (기존 4개) +├── services/ (기존 6개) +└── utils/ (기존 1개) +``` + +--- + +## 작업 순서 + +### Step 1: useInitialDataLoading 훅 생성 +- [ ] 훅 파일 생성 +- [ ] 로딩/에러 상태 이동 +- [ ] useEffect 이동 +- [ ] 메인 컴포넌트에서 사용 + +### Step 2: useImportManagement 훅 생성 +- [ ] 훅 파일 생성 +- [ ] Import 상태 이동 +- [ ] 핸들러 이동 +- [ ] 메인 컴포넌트에서 사용 + +### Step 3: useReorderManagement 훅 생성 +- [ ] 훅 파일 생성 +- [ ] moveSection, moveField 이동 +- [ ] 메인 컴포넌트에서 사용 + +### Step 4: useDeleteManagement 훅 생성 +- [ ] 훅 파일 생성 +- [ ] Delete/Unlink 핸들러 이동 +- [ ] 메인 컴포넌트에서 사용 + +### Step 5: AttributeTabContent 컴포넌트 분리 +- [ ] 컴포넌트 파일 생성 +- [ ] 속성 탭 UI 이동 +- [ ] 반복 코드 통합 + +### Step 6: ItemMasterDialogs 컴포넌트 분리 +- [ ] 컴포넌트 파일 생성 +- [ ] 13개 다이얼로그 이동 + +### Step 7: 정리 및 테스트 +- [ ] 래퍼 함수 정리 +- [ ] unused 코드 제거 +- [ ] 빌드 확인 +- [ ] 수동 테스트 + +--- + +## 리스크 및 주의사항 + +1. **Context 의존성**: 훅들이 `useItemMaster` Context에 의존 + - 해결: 필요한 함수만 훅 파라미터로 전달 + +2. **상태 공유**: 여러 훅에서 동일 상태 사용 + - 해결: 공통 상태는 메인 컴포넌트에서 관리 + +3. **타입 호환성**: 기존 훅의 setter 타입 문제 + - 해결: `as any` 임시 사용 또는 타입 수정 + +4. **테스트**: 페이지 기능이 많아 수동 테스트 필요 + - 해결: 체크리스트 작성하여 순차 테스트 + +--- + +## 다음 단계 + +1. 이 계획서 확인 후 작업 시작 +2. Step 1부터 순차 진행 +3. 각 Step 완료 후 빌드 확인 +4. 최종 수동 테스트 \ No newline at end of file diff --git a/claudedocs/juil/[IMPL-2026-01-05] category-management-checklist.md b/claudedocs/juil/[IMPL-2026-01-05] category-management-checklist.md new file mode 100644 index 00000000..59090938 --- /dev/null +++ b/claudedocs/juil/[IMPL-2026-01-05] category-management-checklist.md @@ -0,0 +1,98 @@ +# [IMPL-2026-01-05] 카테고리관리 페이지 구현 체크리스트 + +## 개요 +- **위치**: 발주관리 > 기준정보 > 카테고리관리 +- **URL**: `/ko/juil/order/base-info/categories` +- **참조 페이지**: `/ko/settings/ranks` (직급관리) +- **기능**: 동일, 텍스트/라벨만 다름 + +## 스크린샷 분석 + +### UI 구성 +| 구성요소 | 내용 | +|---------|------| +| 타이틀 | 카테고리관리 | +| 설명 | 카테고리를 등록하고 관리합니다. | +| 입력필드 라벨 | 카테고리 | +| 입력필드 placeholder | 카테고리를 입력해주세요 | +| 테이블 컬럼 | 카테고리, 작업 | +| 기본 데이터 | 슬라이드 OPEN 사이즈, 모터, 공정자재, 철물 | + +### Description 영역 (참고용, UI 미구현) +1. 추가 버튼 클릭 시 목록 최하단에 추가 +2. 드래그&드롭으로 순서 변경 +3. 수정 버튼 → 수정 팝업 +4. 삭제 버튼 → 조건별 Alert: + - 품목 사용 중: "(카테고리명)을 사용하고 있는 품목이 있습니다. 모두 변경 후 삭제가 가능합니다." + - 미사용: "정말 삭제하시겠습니까?" → "삭제가 되었습니다." + - 기본 카테고리: "기본 카테고리는 삭제가 불가합니다." + +## 구현 체크리스트 + +### Phase 1: 파일 구조 생성 +- [x] `src/app/[locale]/(protected)/juil/order/base-info/categories/page.tsx` 생성 +- [x] `src/components/business/juil/category-management/` 디렉토리 생성 + +### Phase 2: 컴포넌트 구현 (RankManagement 복제 + 수정) +- [x] `index.tsx` - CategoryManagement 메인 컴포넌트 + - 타이틀: "카테고리관리" + - 설명: "카테고리를 등록하고 관리합니다. 드래그하여 순서를 변경할 수 있습니다." + - 아이콘: `FolderTree` + - 입력 placeholder: "카테고리를 입력해주세요" +- [x] `types.ts` - Category 타입 정의 +- [x] `actions.ts` - Server Actions (목데이터) +- [x] `CategoryDialog.tsx` - 수정 다이얼로그 + +### Phase 3: 텍스트 변경 사항 +| 원본 (ranks) | 변경 (categories) | 상태 | +|-------------|-------------------|------| +| 직급 | 카테고리 | ✅ | +| 직급관리 | 카테고리관리 | ✅ | +| 사원의 직급을 관리합니다 | 카테고리를 등록하고 관리합니다 | ✅ | +| 직급명을 입력하세요 | 카테고리를 입력해주세요 | ✅ | +| 직급이 추가되었습니다 | 카테고리가 추가되었습니다 | ✅ | +| 직급이 수정되었습니다 | 카테고리가 수정되었습니다 | ✅ | +| 직급이 삭제되었습니다 | 카테고리가 삭제되었습니다 | ✅ | +| 등록된 직급이 없습니다 | 등록된 카테고리가 없습니다 | ✅ | + +### Phase 4: 삭제 로직 (삭제 조건 처리) +- [x] 기본 카테고리 삭제 불가 로직 추가 (`isDefault` 플래그) +- [x] 조건별 Alert 메시지 분기 (actions.ts의 `errorType` 반환) +- [ ] 품목 사용 여부 체크 로직 추가 (추후 API 연동 시) + +### Phase 5: 목데이터 설정 +- [x] 기본 카테고리 4개 설정 완료 +```typescript +const mockCategories = [ + { id: '1', name: '슬라이드 OPEN 사이즈', order: 1, isDefault: true }, + { id: '2', name: '모터', order: 2, isDefault: true }, + { id: '3', name: '공정자재', order: 3, isDefault: true }, + { id: '4', name: '철물', order: 4, isDefault: true }, +]; +``` + +### Phase 6: 테스트 URL 문서 업데이트 +- [x] `claudedocs/[REF] juil-pages-test-urls.md` 업데이트 + - 발주관리 > 기준정보 섹션 추가 + - 카테고리관리 URL 추가 + +## 파일 구조 + +``` +src/ +├── app/[locale]/(protected)/juil/order/ +│ └── base-info/ +│ └── categories/ +│ └── page.tsx +└── components/business/juil/ + └── category-management/ + ├── index.tsx + ├── types.ts + ├── actions.ts + └── CategoryDialog.tsx +``` + +## 진행 상태 +- 생성일: 2026-01-05 +- 상태: ✅ 완료 (목데이터 기반) +- 남은 작업: API 연동 시 품목 사용 여부 체크 로직 추가 \ No newline at end of file diff --git a/claudedocs/juil/[IMPL-2026-01-05] item-management-checklist.md b/claudedocs/juil/[IMPL-2026-01-05] item-management-checklist.md new file mode 100644 index 00000000..37156b4b --- /dev/null +++ b/claudedocs/juil/[IMPL-2026-01-05] item-management-checklist.md @@ -0,0 +1,209 @@ +# [IMPL-2026-01-05] 품목관리 페이지 구현 체크리스트 + +## 개요 +- **위치**: 발주관리 > 기준정보 > 품목관리 +- **URL**: `/ko/juil/order/base-info/items` +- **참조 템플릿**: IntegratedListTemplateV2 (리스트 페이지 표준) +- **기능**: 품목 CRUD, 필터링, 검색, 정렬 + +## 스크린샷 분석 + +### 헤더 영역 +| 구성요소 | 내용 | +|---------|------| +| 타이틀 | 품목관리 | +| 설명 | 품목을 등록하여 관리합니다. | +| 날짜 필터 | 날짜 범위 선택 (DateRangePicker) | +| 빠른 날짜 버튼 | 전체년도, 전전월, 전월, 당월, 어제, 오늘 | +| 액션 버튼 | 품목 등록 (빨간색 primary) | + +### 통계 카드 +| 카드 | 내용 | +|------|------| +| 전체 품목 | 전체 품목 수 표시 | +| 사용 품목 | 사용 중인 품목 수 표시 | + +### 검색 및 필터 영역 +| 구성요소 | 내용 | +|---------|------| +| 검색 입력 | 품목명 검색 | +| 선택 카운트 | N건 / N건 선택 | +| 삭제 버튼 | 선택된 항목 일괄 삭제 | + +### 테이블 컬럼 +| 컬럼 | 타입 | 필터 옵션 | +|------|------|----------| +| 체크박스 | checkbox | - | +| 품목번호 | text | - | +| 물품유형 | select filter | 전체, 제품, 부품, 소모품, 공과 | +| 카테고리 | select filter + search | 전체, 기본, (카테고리 목록) | +| 품목명 | text | - | +| 규격 | select filter | 전체, 인정, 비인정 | +| 단위 | text | - | +| 구분 | select filter | 전체, 경품발주, 원자재발주, 외주발주 | +| 상태 | badge | 승인, 작업 | +| 작업 | actions | 수정(연필 아이콘) | + +### Description 영역 (참고용, UI 미구현) +1. 품목 등록 버튼 - 클릭 시 품목 상세 등록 화면으로 이동 +2. 물품유형 셀렉트 박스 - 전체/제품/부품/소모품/공과 (디폴트: 전체) +3. 카테고리 셀렉트 박스, 검색 - 전체/기본/카테고리 목록 (디폴트: 전체) +4. 규격 셀렉트 박스 - 전체/인정/비인정 (디폴트: 전체) +5. 구분 셀렉트 박스 - 전체/경품발주/원자재발주/외주발주 (디폴트: 전체) +6. 상태 셀렉트 박스 - 전체/사용/중지 (디폴트: 전체) +7. 정렬 셀렉트 박스 - 최신순/등록순 (디폴트: 최신순) + +## 구현 체크리스트 + +### Phase 1: 파일 구조 생성 +- [x] `src/app/[locale]/(protected)/juil/order/base-info/items/page.tsx` 생성 +- [x] `src/components/business/juil/item-management/` 디렉토리 생성 + +### Phase 2: 타입 및 상수 정의 +- [x] `types.ts` - Item 타입 정의 + ```typescript + interface Item { + id: string; + itemNumber: string; // 품목번호 + itemType: ItemType; // 물품유형 + categoryId: string; // 카테고리 ID + categoryName: string; // 카테고리명 + itemName: string; // 품목명 + specification: string; // 규격 (인쇄/비인쇄) + unit: string; // 단위 + orderType: OrderType; // 구분 + status: ItemStatus; // 상태 + createdAt: string; + updatedAt: string; + } + ``` +- [x] `constants.ts` - 필터 옵션 상수 정의 + ```typescript + // 물품유형 + const ITEM_TYPES = ['전체', '제품', '부품', '소모품', '공과']; + + // 규격 + const SPECIFICATIONS = ['전체', '인정', '비인정']; + + // 구분 + const ORDER_TYPES = ['전체', '경품발주', '원자재발주', '외주발주']; + + // 상태 + const ITEM_STATUSES = ['전체', '사용', '중지']; + + // 정렬 + const SORT_OPTIONS = ['최신순', '등록순']; + ``` + +### Phase 3: 메인 컴포넌트 구현 +- [x] `index.tsx` - ItemManagement 메인 컴포넌트 (export) +- [x] `ItemManagementClient.tsx` - 클라이언트 컴포넌트 + - IntegratedListTemplateV2 사용 + - 헤더: 타이틀, 설명, 날짜필터, 품목등록 버튼 + - 통계 카드: StatCards 컴포넌트 활용 + - 테이블: 컬럼 헤더 필터 포함 + - 검색 및 삭제 기능 + +### Phase 4: 테이블 컬럼 설정 +- [x] 테이블 컬럼 정의 (ItemManagementClient.tsx 내 포함) + - 체크박스 컬럼 + - 품목번호 컬럼 + - 물품유형 컬럼 (헤더 필터 Select) + - 카테고리 컬럼 (헤더 필터 Select + 검색) + - 품목명 컬럼 + - 규격 컬럼 (헤더 필터 Select) + - 단위 컬럼 + - 구분 컬럼 (헤더 필터 Select) + - 상태 컬럼 (Badge 표시) + - 작업 컬럼 (수정 버튼) + +### Phase 5: Server Actions (목데이터) +- [x] `actions.ts` - Server Actions 구현 + - `getItemList()` - 품목 목록 조회 + - `getItemStats()` - 통계 조회 + - `deleteItem()` - 품목 삭제 + - `deleteItems()` - 품목 일괄 삭제 + - `getCategoryOptions()` - 카테고리 목록 조회 + +### Phase 6: 목데이터 설정 +```typescript +const mockItems: Item[] = [ + { id: '1', itemNumber: '123123', itemType: '제품', categoryName: '카테고리명', itemName: '품목명', specification: '인쇄', unit: 'SET', orderType: '외주발주', status: '승인' }, + { id: '2', itemNumber: '123123', itemType: '부품', categoryName: '카테고리명', itemName: '품목명', specification: '비인쇄', unit: 'SET', orderType: '외주발주', status: '승인' }, + { id: '3', itemNumber: '123123', itemType: '소모품', categoryName: '카테고리명', itemName: '품목명', specification: '인쇄', unit: 'SET', orderType: '외주발주', status: '승인' }, + { id: '4', itemNumber: '123123', itemType: '공과', categoryName: '카테고리명', itemName: '품목명', specification: '비인쇄', unit: 'EA', orderType: '공과', status: '작업' }, + { id: '5', itemNumber: '123123', itemType: '부품', categoryName: '카테고리명', itemName: '품목명', specification: '인쇄', unit: 'EA', orderType: '원자재발주', status: '작업' }, + { id: '6', itemNumber: '123123', itemType: '소모품', categoryName: '카테고리명', itemName: '품목명', specification: '비인쇄', unit: '승인', orderType: '외주발주', status: '작업' }, + { id: '7', itemNumber: '123123', itemType: '소모품', categoryName: '카테고리명', itemName: '품목명', specification: '인쇄', unit: '승인', orderType: '공과', status: '작업' }, +]; + +const mockStats = { + totalItems: 7, + activeItems: 5, +}; +``` + +### Phase 7: 헤더 필터 컴포넌트 +- [x] tableHeaderActions 영역에 Select 필터 구현 + - 물품유형 필터 + - 규격 필터 + - 구분 필터 + - 정렬 필터 + +### Phase 8: 등록/상세/수정 페이지 구현 +- [x] 품목 등록 버튼 클릭 → `/ko/juil/order/base-info/items/new` 이동 +- [x] 수정 버튼 클릭 → `/ko/juil/order/base-info/items/[id]?mode=edit` 이동 +- [x] 등록/수정/상세 페이지 구현 (ItemDetailClient.tsx) +- [x] Server Actions (getItem, createItem, updateItem) 구현 +- [x] 발주 항목 동적 추가/삭제 기능 + +### Phase 9: 테스트 URL 문서 업데이트 +- [x] `claudedocs/[REF] juil-pages-test-urls.md` 업데이트 + - 품목관리 URL 추가 + +## 파일 구조 + +``` +src/ +├── app/[locale]/(protected)/juil/order/ +│ └── base-info/ +│ └── items/ +│ ├── page.tsx +│ ├── new/ +│ │ └── page.tsx +│ └── [id]/ +│ └── page.tsx +└── components/business/juil/ + └── item-management/ + ├── index.tsx + ├── ItemManagementClient.tsx + ├── ItemDetailClient.tsx + ├── types.ts + ├── constants.ts + └── actions.ts +``` + +## 참조 컴포넌트 +- `IntegratedListTemplateV2` - 리스트 템플릿 +- `StatCards` - 통계 카드 +- `DateRangePicker` - 날짜 범위 선택 +- `Select` - 필터 셀렉트박스 +- `Badge` - 상태 표시 +- `Button` - 버튼 +- `Checkbox` - 체크박스 + +## UI 구현 참고 +- 컬럼 헤더 내 필터 Select: 기존 프로젝트 내 유사 구현 검색 필요 +- 날짜 빠른 선택 버튼 그룹: 기존 컴포넌트 활용 또는 신규 구현 + +## 진행 상태 +- 생성일: 2026-01-05 +- 상태: ✅ 전체 완료 (리스트 + 상세/등록/수정) + +## 히스토리 +| 날짜 | 작업 내용 | 상태 | +|------|----------|------| +| 2026-01-05 | 체크리스트 작성 | ✅ | +| 2026-01-05 | 리스트 페이지 구현 (Phase 1-7, 9) | ✅ | +| 2026-01-05 | 규격 필터 수정 (인쇄/비인쇄 → 인정/비인정) | ✅ | +| 2026-01-05 | 상세/등록/수정 페이지 구현 (Phase 8) | ✅ | diff --git a/claudedocs/juil/[IMPL-2026-01-05] pricing-management-checklist.md b/claudedocs/juil/[IMPL-2026-01-05] pricing-management-checklist.md new file mode 100644 index 00000000..93a91da0 --- /dev/null +++ b/claudedocs/juil/[IMPL-2026-01-05] pricing-management-checklist.md @@ -0,0 +1,119 @@ +# [IMPL-2026-01-05] 단가관리 리스트 페이지 구현 체크리스트 + +## 개요 +- **위치**: 발주관리 > 기준정보 > 단가관리 +- **URL**: `/ko/juil/order/base-info/pricing` +- **참조 페이지**: `/ko/juil/order/order-management` (OrderManagementListClient) +- **패턴**: IntegratedListTemplateV2 + StatCards + +## 스크린샷 분석 + +### UI 구성 + +#### 1. 헤더 영역 +| 구성요소 | 내용 | +|---------|------| +| 타이틀 | 단가관리 | +| 설명 | 단가를 등록하고 관리합니다. | + +#### 2. 달력 + 액션 버튼 영역 +| 구성요소 | 내용 | +|---------|------| +| 날짜 선택 | DateRangeSelector (2025-09-01 ~ 2025-09-03) | +| 액션 버튼들 | 담당단가, 진행단가, 확정, 발행, 이력, 오류, **단가 등록** | + +#### 3. StatCards (통계 카드) +| 카드 | 값 | 설명 | +|------|-----|------| +| 미완료 | 9 | 미완료 단가 | +| 확정 | 5 | 확정된 단가 | +| 발행 | 4 | 발행된 단가 | + +#### 4. 필터 영역 (테이블 헤더) +| 필터 | 옵션 | 기본값 | +|------|------|--------| +| 품목유형 | 전체, 박스, 부속, 소모품, 공과 | 전체 | +| 카테고리 | 전기, (카테고리 목록) | - | +| 규격 | 전체, 진행, 미진행 | 전체 | +| 구분 | 전체, 금동량, 임의적용가, 미구분 | 전체 | +| 상세 | 전체, 사용, 유지, 미등록 | 전체 | +| 정렬 | 최신순, 등록순 | 최신순 | + +#### 5. 테이블 컬럼 +| 컬럼 | 설명 | +|------|------| +| 체크박스 | 행 선택 | +| 단가번호 | 단가 고유번호 | +| 품목유형 | 박스/부속/소모품/공과 | +| 카테고리 | 품목 카테고리 | +| 품목 | 품목명 | +| 금액량 | 수량 정보 | +| 정량 | 정량 정보 | +| 단가 | 단가 금액 | +| 구매처 | 구매처 정보 | +| 예상단가 | 예상 단가 | +| 이전단가 | 이전 단가 | +| 판매단가 | 판매 단가 | +| 실적 | 실적 정보 | + +## 구현 체크리스트 + +### Phase 1: 파일 구조 생성 +- [x] `src/app/[locale]/(protected)/juil/order/base-info/pricing/page.tsx` 생성 +- [x] `src/components/business/juil/pricing-management/` 디렉토리 생성 + +### Phase 2: 타입 및 상수 정의 +- [x] `types.ts` - Pricing 타입, 필터 옵션, 상태 스타일 + - Pricing 인터페이스 + - PricingStats 인터페이스 + - 품목유형 옵션 (ITEM_TYPE_OPTIONS) + - 규격 옵션 (SPEC_OPTIONS) + - 구분 옵션 (DIVISION_OPTIONS) + - 상세 옵션 (DETAIL_OPTIONS) + - 정렬 옵션 (SORT_OPTIONS) + - 상태 스타일 (PRICING_STATUS_STYLES) + +### Phase 3: Server Actions (목데이터) +- [x] `actions.ts` + - getPricingList() - 목록 조회 + - getPricingStats() - 통계 조회 + - deletePricing() - 단일 삭제 + - deletePricings() - 일괄 삭제 + +### Phase 4: 리스트 컴포넌트 +- [x] `PricingListClient.tsx` + - IntegratedListTemplateV2 사용 + - DateRangeSelector (날짜 범위 선택) + - StatCards (미완료/확정/발행) + - 필터 셀렉트 박스들 (품목유형, 규격, 구분, 상세, 정렬) + - 액션 버튼들 (담당단가, 진행단가, 확정, 발행, 이력, 오류, 단가 등록) + - 테이블 렌더링 + - 모바일 카드 렌더링 + - 삭제 다이얼로그 + +### Phase 5: 목데이터 설정 +- [x] 7개 목데이터 설정 완료 + +### Phase 6: 테스트 URL 문서 업데이트 +- [x] `claudedocs/[REF] juil-pages-test-urls.md` 업데이트 + +## 파일 구조 + +``` +src/ +├── app/[locale]/(protected)/juil/order/ +│ └── base-info/ +│ └── pricing/ +│ └── page.tsx +└── components/business/juil/ + └── pricing-management/ + ├── index.ts + ├── types.ts + ├── actions.ts + └── PricingListClient.tsx +``` + +## 진행 상태 +- 생성일: 2026-01-05 +- 상태: ✅ 완료 (목데이터 기반) +- 남은 작업: API 연동 시 실제 데이터 연결 \ No newline at end of file diff --git a/claudedocs/juil/[PLAN-2026-01-02] estimate-detail-form-refactoring.md b/claudedocs/juil/[PLAN-2026-01-02] estimate-detail-form-refactoring.md new file mode 100644 index 00000000..d00beb25 --- /dev/null +++ b/claudedocs/juil/[PLAN-2026-01-02] estimate-detail-form-refactoring.md @@ -0,0 +1,231 @@ +# EstimateDetailForm.tsx 파일 분할 계획서 + +## 현황 분석 + +- **파일 위치**: `src/components/business/juil/estimates/EstimateDetailForm.tsx` +- **현재 라인 수**: 2,088줄 +- **문제점**: 단일 파일에 모든 섹션, 핸들러, 상태 관리가 집중되어 유지보수 어려움 + +## 파일 구조 분석 + +### 현재 구조 (라인 범위) + +| 구분 | 라인 | 설명 | +|------|------|------| +| Imports | 1-56 | React, UI 컴포넌트, 타입 | +| 상수/유틸 | 58-75 | MOCK_MATERIALS, MOCK_EXPENSES, formatAmount | +| Props | 77-81 | EstimateDetailFormProps | +| State | 88-127 | formData, 로딩, 다이얼로그, 모달 상태 | +| 핸들러 - 네비게이션 | 130-140 | handleBack, handleEdit, handleCancel | +| 핸들러 - 저장/삭제 | 143-182 | handleSave, handleConfirmSave, handleDelete, handleConfirmDelete | +| 핸들러 - 견적 요약 | 185-227 | handleAddSummaryItem, handleRemoveSummaryItem, handleSummaryItemChange | +| 핸들러 - 공과 상세 | 230-259 | handleAddExpenseItem, handleRemoveExpenseItem, handleExpenseItemChange | +| 핸들러 - 단가 조정 | 262-283 | handlePriceAdjustmentChange | +| 핸들러 - 견적 상세 | 286-343 | handleAddDetailItem, handleRemoveDetailItem, handleDetailItemChange | +| 핸들러 - 파일 업로드 | 346-435 | handleDocumentUpload, handleDocumentRemove, 드래그앤드롭 | +| useMemo | 438-482 | pageTitle, pageDescription, headerActions | +| JSX - 견적 정보 | 496-526 | 견적 정보 Card | +| JSX - 현장설명회 | 528-551 | 현장설명회 정보 Card | +| JSX - 입찰 정보 | 553-736 | 입찰 정보 Card + 파일 업로드 | +| JSX - 견적 요약 | 738-890 | 견적 요약 정보 Table | +| JSX - 공과 상세 | 892-1071 | 공과 상세 Table | +| JSX - 단가 조정 | 1073-1224 | 품목 단가 조정 Table | +| JSX - 견적 상세 | 1226-2017 | 견적 상세 Table (가장 큰 섹션) | +| 모달/다이얼로그 | 2020-2085 | 전자결재, 견적서, 삭제/저장 다이얼로그 | + +--- + +## 분할 계획 + +### 1단계: 섹션 컴포넌트 분리 + +``` +src/components/business/juil/estimates/ +├── EstimateDetailForm.tsx # 메인 컴포넌트 (축소) +├── sections/ +│ ├── index.ts # 섹션 export +│ ├── EstimateInfoSection.tsx # 견적 정보 + 현장설명회 + 입찰 정보 +│ ├── EstimateSummarySection.tsx # 견적 요약 정보 +│ ├── ExpenseDetailSection.tsx # 공과 상세 +│ ├── PriceAdjustmentSection.tsx # 품목 단가 조정 +│ └── EstimateDetailTableSection.tsx # 견적 상세 테이블 +├── hooks/ +│ ├── index.ts # hooks export +│ └── useEstimateCalculations.ts # 계산 로직 (면적, 무게, 단가 등) +└── utils/ + ├── index.ts # utils export + ├── constants.ts # MOCK_MATERIALS, MOCK_EXPENSES + └── formatters.ts # formatAmount +``` + +### 2단계: 각 파일 상세 + +#### 2.1 constants.ts (~20줄) +```typescript +// MOCK_MATERIALS, MOCK_EXPENSES 이동 +export const MOCK_MATERIALS = [...]; +export const MOCK_EXPENSES = [...]; +``` + +#### 2.2 formatters.ts (~10줄) +```typescript +// formatAmount 함수 이동 +export function formatAmount(amount: number): string { ... } +``` + +#### 2.3 useEstimateCalculations.ts (~100줄) +```typescript +// 견적 상세 테이블의 계산 로직 분리 +// - 면적, 무게, 철제스크린, 코킹, 레일, 하장 등 계산 +// - 합계 계산 로직 +export function useEstimateCalculations( + item: EstimateDetailItem, + priceAdjustmentData: PriceAdjustmentData, + useAdjustedPrice: boolean +) { ... } + +export function calculateTotals( + items: EstimateDetailItem[], + priceAdjustmentData: PriceAdjustmentData, + useAdjustedPrice: boolean +) { ... } +``` + +#### 2.4 EstimateInfoSection.tsx (~250줄) +```typescript +// 견적 정보 + 현장설명회 + 입찰 정보 Card 3개 +// 파일 업로드 영역 포함 +interface EstimateInfoSectionProps { + formData: EstimateDetailFormData; + setFormData: React.Dispatch>; + isViewMode: boolean; + documentInputRef: React.RefObject; +} +``` + +#### 2.5 EstimateSummarySection.tsx (~200줄) +```typescript +// 견적 요약 정보 테이블 +interface EstimateSummarySectionProps { + summaryItems: EstimateSummaryItem[]; + summaryMemo: string; + isViewMode: boolean; + onAddItem: () => void; + onRemoveItem: (id: string) => void; + onItemChange: (id: string, field: keyof EstimateSummaryItem, value: string | number) => void; + onMemoChange: (memo: string) => void; +} +``` + +#### 2.6 ExpenseDetailSection.tsx (~200줄) +```typescript +// 공과 상세 테이블 +interface ExpenseDetailSectionProps { + expenseItems: ExpenseItem[]; + isViewMode: boolean; + onAddItems: (count: number) => void; + onRemoveSelected: () => void; + onItemChange: (id: string, field: keyof ExpenseItem, value: string | number) => void; + onSelectItem: (id: string, selected: boolean) => void; + onSelectAll: (selected: boolean) => void; +} +``` + +#### 2.7 PriceAdjustmentSection.tsx (~200줄) +```typescript +// 품목 단가 조정 테이블 +interface PriceAdjustmentSectionProps { + priceAdjustmentData: PriceAdjustmentData; + isViewMode: boolean; + onPriceChange: (key: string, value: number) => void; + onSave: () => void; + onApplyAll: () => void; + onReset: () => void; +} +``` + +#### 2.8 EstimateDetailTableSection.tsx (~600줄) +```typescript +// 견적 상세 테이블 (가장 큰 섹션) +interface EstimateDetailTableSectionProps { + detailItems: EstimateDetailItem[]; + priceAdjustmentData: PriceAdjustmentData; + useAdjustedPrice: boolean; + isViewMode: boolean; + onAddItems: (count: number) => void; + onRemoveItem: (id: string) => void; + onRemoveSelected: () => void; + onItemChange: (id: string, field: keyof EstimateDetailItem, value: string | number) => void; + onSelectItem: (id: string, selected: boolean) => void; + onSelectAll: (selected: boolean) => void; + onApplyAdjustedPrice: () => void; + onReset: () => void; +} +``` + +--- + +## 분할 후 예상 라인 수 + +| 파일 | 예상 라인 수 | +|------|-------------| +| EstimateDetailForm.tsx (메인) | ~300줄 | +| EstimateInfoSection.tsx | ~250줄 | +| EstimateSummarySection.tsx | ~200줄 | +| ExpenseDetailSection.tsx | ~200줄 | +| PriceAdjustmentSection.tsx | ~200줄 | +| EstimateDetailTableSection.tsx | ~600줄 | +| useEstimateCalculations.ts | ~100줄 | +| constants.ts | ~20줄 | +| formatters.ts | ~10줄 | +| **총합** | ~1,880줄 (약 10% 감소) | + +--- + +## 실행 순서 + +### Phase 1: 유틸리티 분리 (5분) +- [ ] `utils/constants.ts` 생성 +- [ ] `utils/formatters.ts` 생성 +- [ ] `utils/index.ts` 생성 + +### Phase 2: 계산 로직 분리 (10분) +- [ ] `hooks/useEstimateCalculations.ts` 생성 +- [ ] `hooks/index.ts` 생성 + +### Phase 3: 섹션 컴포넌트 분리 (30분) +- [ ] `sections/EstimateInfoSection.tsx` 생성 +- [ ] `sections/EstimateSummarySection.tsx` 생성 +- [ ] `sections/ExpenseDetailSection.tsx` 생성 +- [ ] `sections/PriceAdjustmentSection.tsx` 생성 +- [ ] `sections/EstimateDetailTableSection.tsx` 생성 +- [ ] `sections/index.ts` 생성 + +### Phase 4: 메인 컴포넌트 리팩토링 (10분) +- [ ] EstimateDetailForm.tsx에서 분리된 컴포넌트 import +- [ ] 핸들러 정리 및 props 전달 +- [ ] 불필요한 코드 제거 + +### Phase 5: 검증 (5분) +- [ ] TypeScript 빌드 확인 +- [ ] 기능 동작 확인 + +--- + +## 주의사항 + +1. **상태 관리**: formData, setFormData는 메인 컴포넌트에서 관리, 섹션에 props로 전달 +2. **타입 일관성**: 기존 types.ts의 타입 그대로 사용 +3. **핸들러 위치**: 핸들러는 메인 컴포넌트에 유지, 섹션에 콜백으로 전달 +4. **조정단가 상태**: appliedPrices, useAdjustedPrice는 메인 컴포넌트에서 관리 + +--- + +## 5가지 수정사항 (분할 후 진행) + +| # | 항목 | 수정 위치 (분할 후) | +|---|------|-------------------| +| 2 | 품목 단가 초기화 → 품목 단가만 | PriceAdjustmentSection.tsx | +| 3 | 견적 상세 인풋 필드 추가 | EstimateDetailTableSection.tsx | +| 4 | 견적 상세 초기화 버튼 수정 | EstimateDetailTableSection.tsx | +| 5 | 각 섹션별 초기화 분리 | 각 Section 컴포넌트 | \ No newline at end of file diff --git a/claudedocs/juil/[PLAN-2026-01-05] order-detail-form-separation.md b/claudedocs/juil/[PLAN-2026-01-05] order-detail-form-separation.md new file mode 100644 index 00000000..8c636d20 --- /dev/null +++ b/claudedocs/juil/[PLAN-2026-01-05] order-detail-form-separation.md @@ -0,0 +1,292 @@ +# OrderDetailForm.tsx 분리 계획서 + +**생성일**: 2026-01-05 +**현재 파일 크기**: 1,273줄 +**목표**: 유지보수성 향상을 위한 컴포넌트 분리 + +--- + +## 현재 파일 구조 분석 + +| 영역 | 라인 | 비율 | 내용 | +|------|------|------|------| +| Import & Types | 1-69 | 5% | 의존성 및 타입 import | +| Props Interface | 70-74 | 0.5% | 컴포넌트 props | +| State & Hooks | 76-113 | 3% | 상태 관리 (12개 useState) | +| Handlers | 114-433 | 25% | 핸들러 함수들 (20+개) | +| JSX Render | 435-1271 | 66% | UI 렌더링 | + +### 주요 핸들러 분류 (114-433줄) +- **Navigation**: handleBack, handleEdit, handleCancel (114-125) +- **Form Field**: handleFieldChange (127-133) +- **CRUD Operations**: handleSave, handleDelete, handleDuplicate (135-199) +- **Category Operations**: handleAddCategory, handleDeleteCategory, handleCategoryChange (206-247) +- **Item Operations**: handleAddItems, handleDeleteSelectedItems, handleDeleteAllItems, handleItemChange (249-327) +- **Selection**: handleToggleSelection, handleToggleSelectAll (330-357) +- **Calendar**: handleCalendarDateClick, handleCalendarMonthChange (359-385) + +### 주요 JSX 영역 (435-1271줄) +- **발주 정보 Card**: 447-559 (112줄) +- **계약 정보 Card**: 561-694 (133줄) +- **발주 스케줄 Calendar**: 696-715 (19줄) +- **발주 상세 테이블**: 717-1172 (455줄) ⚠️ **가장 큰 부분** +- **카테고리 추가 버튼**: 1174-1182 (8줄) +- **비고 Card**: 1184-1198 (14줄) +- **Dialogs**: 1201-1261 (60줄) +- **Document Modal**: 1263-1270 (7줄) + +--- + +## 분리 계획 + +### Phase 1: 커스텀 훅 분리 + +**파일**: `hooks/useOrderDetailForm.ts` +**예상 크기**: ~250줄 + +```typescript +// 추출할 내용 +- formData 상태 관리 +- selectedItems, addCounts, categoryFilters 상태 +- calendarDate, selectedCalendarDate 상태 +- 모든 핸들러 함수들 +- calendarEvents useMemo +``` + +**장점**: +- 비즈니스 로직과 UI 분리 +- 테스트 용이성 향상 +- 재사용 가능 + +--- + +### Phase 2: 카드 컴포넌트 분리 + +#### 2-1. `cards/OrderInfoCard.tsx` +**예상 크기**: ~120줄 + +```typescript +interface OrderInfoCardProps { + formData: OrderDetailFormData; + isViewMode: boolean; + onFieldChange: (field: keyof OrderDetailFormData, value: any) => void; +} +``` + +**포함 내용**: 발주번호, 발주일, 구분, 상태, 발주담당자, 화물도착지 + +--- + +#### 2-2. `cards/ContractInfoCard.tsx` +**예상 크기**: ~150줄 + +```typescript +interface ContractInfoCardProps { + formData: OrderDetailFormData; + isViewMode: boolean; + isEditMode: boolean; + onFieldChange: (field: keyof OrderDetailFormData, value: any) => void; +} +``` + +**포함 내용**: 거래처명, 현장명, 계약번호, 공사PM, 공사담당자 + +--- + +#### 2-3. `cards/OrderScheduleCard.tsx` +**예상 크기**: ~50줄 + +```typescript +interface OrderScheduleCardProps { + events: ScheduleEvent[]; + currentDate: Date; + selectedDate: Date | null; + onDateClick: (date: Date) => void; + onMonthChange: (date: Date) => void; +} +``` + +**포함 내용**: ScheduleCalendar 래핑 + +--- + +#### 2-4. `cards/OrderMemoCard.tsx` +**예상 크기**: ~40줄 + +```typescript +interface OrderMemoCardProps { + memo: string; + isViewMode: boolean; + onMemoChange: (value: string) => void; +} +``` + +**포함 내용**: 비고 Textarea + +--- + +### Phase 3: 테이블 컴포넌트 분리 (가장 중요) + +#### 3-1. `tables/OrderDetailItemTable.tsx` +**예상 크기**: ~350줄 + +```typescript +interface OrderDetailItemTableProps { + category: OrderDetailCategory; + isEditMode: boolean; + isViewMode: boolean; + selectedItems: Set; + addCount: number; + onAddCountChange: (count: number) => void; + onAddItems: (count: number) => void; + onDeleteSelectedItems: () => void; + onDeleteAllItems: () => void; + onCategoryChange: (field: keyof OrderDetailCategory, value: string) => void; + onItemChange: (itemId: string, field: keyof OrderDetailItem, value: any) => void; + onToggleSelection: (itemId: string) => void; + onToggleSelectAll: () => void; +} +``` + +**포함 내용**: +- 카드 헤더 (왼쪽: 발주 상세/N건 선택/삭제, 오른쪽: 숫자/추가/카테고리/🗑️) +- 테이블 전체 (TableHeader + TableBody) +- 합계 행 + +--- + +#### 3-2. `tables/OrderDetailItemRow.tsx` (선택적) +**예상 크기**: ~150줄 + +```typescript +interface OrderDetailItemRowProps { + item: OrderDetailItem; + index: number; + isEditMode: boolean; + isSelected: boolean; + onItemChange: (field: keyof OrderDetailItem, value: any) => void; + onToggleSelection: () => void; +} +``` + +**포함 내용**: 단일 테이블 행 렌더링 + +--- + +### Phase 4: 다이얼로그 분리 + +#### 4-1. `dialogs/OrderDialogs.tsx` +**예상 크기**: ~80줄 + +```typescript +interface OrderDialogsProps { + // 저장 다이얼로그 + showSaveDialog: boolean; + onSaveDialogChange: (open: boolean) => void; + onConfirmSave: () => void; + // 삭제 다이얼로그 + showDeleteDialog: boolean; + onDeleteDialogChange: (open: boolean) => void; + onConfirmDelete: () => void; + // 카테고리 삭제 다이얼로그 + showCategoryDeleteDialog: string | null; + onCategoryDeleteDialogChange: (categoryId: string | null) => void; + onConfirmDeleteCategory: () => void; + // 공통 + isLoading: boolean; +} +``` + +--- + +## 분리 후 예상 구조 + +``` +src/components/business/juil/order-management/ +├── OrderDetailForm.tsx (~200줄, 메인 컴포넌트) +├── hooks/ +│ └── useOrderDetailForm.ts (~250줄, 비즈니스 로직) +├── cards/ +│ ├── OrderInfoCard.tsx (~120줄) +│ ├── ContractInfoCard.tsx (~150줄) +│ ├── OrderScheduleCard.tsx (~50줄) +│ └── OrderMemoCard.tsx (~40줄) +├── tables/ +│ ├── OrderDetailItemTable.tsx (~350줄) +│ └── OrderDetailItemRow.tsx (~150줄, 선택적) +├── dialogs/ +│ └── OrderDialogs.tsx (~80줄) +├── modals/ +│ └── OrderDocumentModal.tsx (기존) +├── actions.ts (기존) +└── types.ts (기존) +``` + +--- + +## 분리 전후 비교 + +| 지표 | Before | After | +|------|--------|-------| +| 메인 파일 크기 | 1,273줄 | ~200줄 | +| 가장 큰 파일 | 1,273줄 | ~350줄 | +| 파일 개수 | 1 | 8-9 | +| 테스트 용이성 | 낮음 | 높음 | +| 재사용성 | 낮음 | 중간 | + +--- + +## 실행 체크리스트 + +### Phase 1: 커스텀 훅 분리 +- [ ] `hooks/useOrderDetailForm.ts` 생성 +- [ ] 상태 변수들 이동 +- [ ] 핸들러 함수들 이동 +- [ ] useMemo 이동 +- [ ] OrderDetailForm.tsx에서 훅 사용 + +### Phase 2: 카드 컴포넌트 분리 +- [ ] `cards/OrderInfoCard.tsx` 생성 +- [ ] `cards/ContractInfoCard.tsx` 생성 +- [ ] `cards/OrderScheduleCard.tsx` 생성 +- [ ] `cards/OrderMemoCard.tsx` 생성 +- [ ] OrderDetailForm.tsx에서 import 및 사용 + +### Phase 3: 테이블 컴포넌트 분리 +- [ ] `tables/OrderDetailItemTable.tsx` 생성 +- [ ] `tables/OrderDetailItemRow.tsx` 생성 (선택적) +- [ ] OrderDetailForm.tsx에서 import 및 사용 + +### Phase 4: 다이얼로그 분리 +- [ ] `dialogs/OrderDialogs.tsx` 생성 +- [ ] OrderDetailForm.tsx에서 import 및 사용 + +### Phase 5: 최종 검증 +- [ ] TypeScript 타입 오류 없음 +- [ ] ESLint 경고 없음 +- [ ] 빌드 성공 +- [ ] 기능 테스트 (view/edit 모드) +- [ ] 불필요한 import 제거 + +--- + +## 우선순위 권장 + +1. **Phase 1 (Hook)** + **Phase 3 (Table)** 먼저 진행 + - 가장 큰 효과 (전체 코드의 ~60% 분리) + - 테이블이 455줄로 가장 큼 + +2. Phase 2 (Cards) 진행 + - 추가 ~360줄 분리 + +3. Phase 4 (Dialogs) 진행 + - 마무리 정리 + +--- + +## 주의사항 + +- **타입 export**: 새 컴포넌트에서 사용할 타입들 types.ts에서 export 확인 +- **props drilling**: 너무 깊어지면 Context 고려 +- **테스트**: 분리 후 view/edit 모드 모두 테스트 필수 +- **점진적 진행**: 한 번에 모든 분리보다 단계별 진행 권장 diff --git a/claudedocs/juil/[PLAN-2026-01-05] order-management-implementation.md b/claudedocs/juil/[PLAN-2026-01-05] order-management-implementation.md new file mode 100644 index 00000000..a2f3ae03 --- /dev/null +++ b/claudedocs/juil/[PLAN-2026-01-05] order-management-implementation.md @@ -0,0 +1,323 @@ +# 발주관리 페이지 구현 계획서 + +> **작성일**: 2026-01-05 +> **작업 경로**: `/juil/order/order-management` +> **상태**: ✅ 구현 완료 + +--- + +## 📋 스크린샷 분석 결과 + +### 화면 구성 + +#### 1. 상단 - 발주 스케줄 (달력 영역) +| 요소 | 설명 | +|------|------| +| **뷰 전환** | 주(Week) / 월(Month) 탭 전환 | +| **년월 네비게이션** | 2025년 12월 ◀ ▶ 버튼 | +| **필터** | 작업반장별 필터 (이번년+8주 화살표 버튼) | +| **일정 바(Bar)** | "담당자 - 현장명 / 발주번호" 형태로 여러 날에 걸쳐 표시 | +| **일정 색상** | 회색(완료), 파란색(진행중) 구분 | +| **일자 뱃지** | 빨간 원 안에 숫자 (06, 07, 08 등) - 상태/건수 표시 | +| **더보기** | +15 형태로 해당 일자에 추가 일정 있음 표시 | +| **달력 클릭** | 특정 일자 클릭 시 아래 리스트에 해당 일자 데이터만 필터링 | + +#### 2. 하단 - 발주 목록 (리스트 영역) +| 요소 | 설명 | +|------|------| +| **날짜 범위** | 2025-09-01 ~ 2025-09-03 형태 | +| **빠른 필터 탭** | 당해년도 / 전년도 / 전월 / 당월 / 어제 / 오늘 | +| **검색** | 검색창 + 건수 표시 (7건, 12건 선택) | +| **상태 필터** | 빨간 원 숫자 버튼들 (전체/상태별) | +| **삭제 버튼** | 선택된 항목 삭제 | + +#### 3. 테이블 컬럼 +| 컬럼 | 설명 | +|------|------| +| 체크박스 | 선택 | +| 계약일련번호 | - | +| 거래처 | 회사명 | +| 현장명 | 작업 현장 | +| 병동 | - | +| 공 | - | +| 시APM | 담당 PM | +| 발주번호 | 발주 식별 번호 | +| 발주번 담자 | 발주 담당자 | +| 발주처 | - | +| 작업반 시공품 | 작업 내용 | +| 기간 | 작업 기간 | +| 구분 | 상태 구분 | +| 실적 납품일 | 실제 납품 완료일 | +| 납품일 | 예정 납품일 | + +#### 4. 작업 버튼 (선택 시) +- 수정 버튼 +- 삭제 버튼 + +--- + +## 🏗️ 구현 범위 + +### Phase 1: 공통 달력 컴포넌트 (ScheduleCalendar) +**재사용 가능한 스케줄 달력 컴포넌트** + +``` +src/components/common/ +└── ScheduleCalendar/ + ├── index.tsx # 메인 컴포넌트 + ├── ScheduleCalendar.tsx # 달력 본체 + ├── CalendarHeader.tsx # 헤더 (년월/뷰전환/필터) + ├── MonthView.tsx # 월간 뷰 + ├── WeekView.tsx # 주간 뷰 + ├── ScheduleBar.tsx # 일정 바 컴포넌트 + ├── DayCell.tsx # 일자 셀 컴포넌트 + ├── MorePopover.tsx # +N 더보기 팝오버 + ├── types.ts # 타입 정의 + └── utils.ts # 유틸리티 함수 +``` + +**기능 요구사항**: +- [ ] 월간/주간 뷰 전환 +- [ ] 년월 네비게이션 (이전/다음) +- [ ] 일정 바(Bar) 렌더링 (여러 날에 걸침) +- [ ] 일정 색상 구분 (상태별) +- [ ] 일자별 뱃지 숫자 표시 +- [ ] +N 더보기 기능 (3개 초과 시) +- [ ] 일자 클릭 이벤트 콜백 +- [ ] 필터 영역 slot (외부에서 주입) +- [ ] 반응형 디자인 + +### Phase 2: 발주관리 리스트 페이지 +**페이지 및 컴포넌트 구조** + +``` +src/app/[locale]/(protected)/juil/order/ +└── order-management/ + └── page.tsx # 페이지 엔트리 + +src/components/business/juil/order-management/ +├── OrderManagementListClient.tsx # 메인 클라이언트 컴포넌트 +├── OrderCalendarSection.tsx # 달력 섹션 (ScheduleCalendar 사용) +├── OrderListSection.tsx # 리스트 섹션 +├── OrderStatusFilter.tsx # 상태 필터 (빨간 원 숫자) +├── OrderDateFilter.tsx # 날짜 빠른 필터 (당해년도/전월 등) +├── types.ts # 타입 정의 +├── actions.ts # Server Actions +└── index.ts # 배럴 export +``` + +**기능 요구사항**: +- [ ] 달력과 리스트 통합 레이아웃 +- [ ] 달력 일자 클릭 → 리스트 필터 연동 +- [ ] 날짜 범위 선택 +- [ ] 빠른 날짜 필터 (당해년도/전년도/전월/당월/어제/오늘) +- [ ] 상태별 필터 (빨간 원 숫자 버튼) +- [ ] 검색 기능 +- [ ] 테이블 (체크박스/정렬/페이지네이션) +- [ ] 선택 시 작업 버튼 표시 +- [ ] 삭제 기능 + +--- + +## 📦 기술 의존성 + +### 새로 설치 필요 +```bash +# FullCalendar 라이브러리 (또는 커스텀 구현) +npm install @fullcalendar/core @fullcalendar/react @fullcalendar/daygrid @fullcalendar/timegrid @fullcalendar/interaction +``` + +**대안**: FullCalendar 없이 커스텀 달력 컴포넌트로 구현 +- 장점: 번들 사이즈 감소, 완전한 커스터마이징 +- 단점: 구현 복잡도 증가 + +### 기존 사용 +- `IntegratedListTemplateV2` - 리스트 템플릿 +- `DateRangeSelector` - 날짜 범위 선택 +- `date-fns` - 날짜 유틸리티 + +--- + +## 🔧 세부 구현 체크리스트 + +### Phase 1: 공통 달력 컴포넌트 (ScheduleCalendar) + +#### 1.1 기본 구조 및 타입 정의 +- [ ] `types.ts` 생성 (ScheduleEvent, CalendarView, CalendarProps 등) +- [ ] `utils.ts` 생성 (날짜 계산, 일정 위치 계산 등) +- [ ] 컴포넌트 폴더 구조 생성 + +#### 1.2 CalendarHeader 컴포넌트 +- [ ] 년월 표시 및 네비게이션 (◀ ▶) +- [ ] 주/월 뷰 전환 탭 +- [ ] 필터 slot (children으로 외부 주입) + +#### 1.3 MonthView 컴포넌트 +- [ ] 월간 그리드 레이아웃 (7x6) +- [ ] 요일 헤더 (일~토) +- [ ] 날짜 셀 렌더링 +- [ ] 이전/다음 달 날짜 표시 (opacity 처리) +- [ ] 오늘 날짜 하이라이트 + +#### 1.4 WeekView 컴포넌트 +- [ ] 주간 그리드 레이아웃 (7 컬럼) +- [ ] 요일 헤더 (날짜 + 요일) +- [ ] 날짜 셀 렌더링 + +#### 1.5 DayCell 컴포넌트 +- [ ] 날짜 숫자 표시 +- [ ] 뱃지 숫자 표시 (빨간 원) +- [ ] 클릭 이벤트 처리 +- [ ] 선택 상태 스타일 + +#### 1.6 ScheduleBar 컴포넌트 +- [ ] 일정 바 렌더링 (시작~종료 날짜) +- [ ] 여러 날에 걸치는 바 계산 (주 단위 분할) +- [ ] 색상 구분 (상태별) +- [ ] 호버/클릭 이벤트 +- [ ] 텍스트 truncate 처리 + +#### 1.7 MorePopover 컴포넌트 +- [ ] +N 버튼 렌더링 +- [ ] 팝오버로 숨겨진 일정 목록 표시 +- [ ] 일정 항목 클릭 이벤트 + +#### 1.8 메인 ScheduleCalendar 컴포넌트 +- [ ] 상태 관리 (현재 월, 뷰 모드, 선택된 날짜) +- [ ] 일정 데이터 받아서 렌더링 +- [ ] 이벤트 콜백 (onDateClick, onEventClick, onMonthChange) +- [ ] 반응형 처리 + +### Phase 2: 발주관리 리스트 페이지 + +#### 2.1 타입 및 설정 +- [ ] `types.ts` - Order 타입, 필터 옵션, 상태 정의 +- [ ] `actions.ts` - Server Actions (목업 데이터) + +#### 2.2 page.tsx +- [ ] 페이지 라우트 생성 +- [ ] 메타데이터 설정 +- [ ] 클라이언트 컴포넌트 import + +#### 2.3 OrderDateFilter 컴포넌트 +- [ ] 빠른 날짜 필터 버튼 (당해년도/전년도/전월/당월/어제/오늘) +- [ ] 클릭 시 날짜 범위 계산 +- [ ] 활성화 상태 스타일 + +#### 2.4 OrderStatusFilter 컴포넌트 +- [ ] 상태별 필터 버튼 (빨간 원 숫자) +- [ ] 전체/상태별 카운트 표시 +- [ ] 선택 상태 스타일 + +#### 2.5 OrderCalendarSection 컴포넌트 +- [ ] ScheduleCalendar 사용 +- [ ] 필터 영역 (작업반장 셀렉트) +- [ ] 일자 클릭 이벤트 → 리스트 필터 연동 +- [ ] 스케줄 데이터 매핑 + +#### 2.6 OrderListSection 컴포넌트 +- [ ] IntegratedListTemplateV2 기반 +- [ ] 테이블 컬럼 정의 +- [ ] 행 렌더링 (체크박스, 데이터, 작업 버튼) +- [ ] 선택 시 작업 버튼 표시 +- [ ] 모바일 카드 렌더링 + +#### 2.7 OrderManagementListClient 컴포넌트 +- [ ] 전체 상태 관리 (달력 + 리스트 연동) +- [ ] 달력 일자 선택 → 리스트 필터 +- [ ] 날짜 범위 필터 +- [ ] 상태 필터 +- [ ] 검색 필터 +- [ ] 정렬 +- [ ] 페이지네이션 +- [ ] 삭제 기능 + +### Phase 3: 통합 테스트 및 마무리 +- [ ] 달력-리스트 연동 테스트 +- [ ] 반응형 테스트 +- [ ] 목업 데이터 검증 +- [ ] 테스트 URL 등록 + +--- + +## 🎨 디자인 명세 + +### 달력 색상 +| 상태 | 바 색상 | 뱃지 색상 | +|------|---------|-----------| +| 완료 | 회색 (`bg-gray-400`) | - | +| 진행중 | 파란색 (`bg-blue-500`) | 빨간색 (`bg-red-500`) | +| 대기 | 노란색 (`bg-yellow-500`) | 빨간색 (`bg-red-500`) | + +### 레이아웃 +``` ++--------------------------------------------------+ +| 📅 발주관리 [발주 등록] | ++--------------------------------------------------+ +| [발주 스케줄] | +| +----------------------------------------------+ | +| | 2025년 12월 [주] [월] [작업반장 ▼] | | +| | ◀ ▶ | | +| |----------------------------------------------| +| | 일 | 월 | 화 | 수 | 목 | 금 | 토 | | +| |----------------------------------------------| +| | | | 1 | 2 | 3 | 4 | 5 | | +| | 📊 | | ━━━━━━━━━━━━━━━━━━━ 일정바 ━━━━━━ | | +| |----------------------------------------------| +| | 6 | 7 | 8 | 9 | 10 | 11 | 12 | | +| | ⓪ | ⓪ | | | | | | | +| +----------------------------------------------+ | ++--------------------------------------------------+ +| [발주 목록] | +| +----------------------------------------------+ | +| | 2025-09-01 ~ 2025-09-03 | | +| | [당해년도][전년도][전월][당월][어제][오늘] | | +| |----------------------------------------------| +| | 🔍 검색... 7건 | ⓿ ❶ ❷ ❸ | [삭제] | | +| |----------------------------------------------| +| | ☐ | 번호 | 거래처 | 현장명 | ... | 작업 | | +| | ☐ | 1 | A사 | 현장1 | ... | [버튼들] | | +| +----------------------------------------------+ | ++--------------------------------------------------+ +``` + +--- + +## 📝 참고사항 + +### 달력 라이브러리 선택 +**추천: 커스텀 구현** +- FullCalendar는 기능이 과도하고 번들 사이즈가 큼 +- 스크린샷의 요구사항은 커스텀으로 충분히 구현 가능 +- `date-fns` 활용하여 날짜 계산 + +### 기존 패턴 준수 +- `IntegratedListTemplateV2` 사용 +- `DateRangeSelector` 재사용 +- `StructureReviewListClient` 패턴 참조 + +### 향후 확장 +- 다른 페이지에서 ScheduleCalendar 재사용 +- 일정 등록/수정 모달 추가 예정 +- 드래그 앤 드롭 일정 이동 (선택적) + +--- + +## ✅ 작업 순서 + +1. **Phase 1.1-1.2**: 타입 정의 및 CalendarHeader +2. **Phase 1.3-1.4**: MonthView / WeekView +3. **Phase 1.5-1.6**: DayCell / ScheduleBar +4. **Phase 1.7-1.8**: MorePopover / 메인 컴포넌트 +5. **Phase 2.1-2.2**: 발주관리 타입 및 페이지 +6. **Phase 2.3-2.4**: 날짜/상태 필터 +7. **Phase 2.5-2.6**: 달력/리스트 섹션 +8. **Phase 2.7**: 메인 클라이언트 컴포넌트 +9. **Phase 3**: 통합 테스트 + +--- + +## 🔗 관련 문서 +- `[REF] juil-project-structure.md` - 주일 프로젝트 구조 +- `StructureReviewListClient.tsx` - 리스트 패턴 참조 +- `IntegratedListTemplateV2.tsx` - 템플릿 참조 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ada4b46f..bb423423 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,19 +40,20 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", + "immer": "^11.0.1", "lucide-react": "^0.552.0", - "next": "^15.5.7", + "next": "^15.5.9", "next-intl": "^4.4.0", - "react": "^19.2.1", + "react": "^19.2.3", "react-day-picker": "^9.11.1", - "react-dom": "^19.2.1", + "react-dom": "^19.2.3", "react-hook-form": "^7.66.0", "recharts": "^3.4.1", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "vaul": "^1.1.2", "zod": "^4.1.12", - "zustand": "^5.0.8" + "zustand": "^5.0.9" }, "devDependencies": { "@playwright/test": "^1.57.0", @@ -944,9 +945,9 @@ } }, "node_modules/@next/env": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", - "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz", + "integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -3443,6 +3444,16 @@ } } }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@remirror/core-constants": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", @@ -6912,9 +6923,9 @@ } }, "node_modules/immer": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", - "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.0.1.tgz", + "integrity": "sha512-naDCyggtcBWANtIrjQEajhhBEuL9b0Zg4zmlWK2CzS6xCWSE39/vvf4LqnMjUAWHBhot4m9MHCM/Z+mfWhUkiA==", "license": "MIT", "funding": { "type": "opencollective", @@ -8011,12 +8022,12 @@ } }, "node_modules/next": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", - "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", + "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", "license": "MIT", "dependencies": { - "@next/env": "15.5.7", + "@next/env": "15.5.9", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -8702,9 +8713,9 @@ "license": "MIT" }, "node_modules/react": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", - "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8732,15 +8743,15 @@ } }, "node_modules/react-dom": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", - "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.1" + "react": "^19.2.3" } }, "node_modules/react-hook-form": { @@ -8887,6 +8898,16 @@ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/recharts/node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", @@ -10104,9 +10125,9 @@ } }, "node_modules/zustand": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", - "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz", + "integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==", "license": "MIT", "engines": { "node": ">=12.20.0" diff --git a/package.json b/package.json index 1b36a3df..2d225644 100644 --- a/package.json +++ b/package.json @@ -44,19 +44,20 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", + "immer": "^11.0.1", "lucide-react": "^0.552.0", - "next": "^15.5.7", + "next": "^15.5.9", "next-intl": "^4.4.0", - "react": "^19.2.1", + "react": "^19.2.3", "react-day-picker": "^9.11.1", - "react-dom": "^19.2.1", + "react-dom": "^19.2.3", "react-hook-form": "^7.66.0", "recharts": "^3.4.1", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "vaul": "^1.1.2", "zod": "^4.1.12", - "zustand": "^5.0.8" + "zustand": "^5.0.9" }, "devDependencies": { "@playwright/test": "^1.57.0", diff --git a/src/app/[locale]/(protected)/hr/attendance/page.tsx b/src/app/[locale]/(protected)/hr/attendance/page.tsx index d2039dce..0c54b435 100644 --- a/src/app/[locale]/(protected)/hr/attendance/page.tsx +++ b/src/app/[locale]/(protected)/hr/attendance/page.tsx @@ -66,7 +66,7 @@ export default function MobileAttendancePage() { const fetchTodayAttendance = async () => { try { - const result = await getTodayAttendance(userId); + const result = await getTodayAttendance(); if (result.success && result.data) { // 이미 출근한 경우 if (result.data.checkIn) { 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 949a1456..997f7a4f 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 @@ -21,13 +21,18 @@ export default function EmployeeEditPage() { 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]); + }, [params.id, router]); useEffect(() => { fetchEmployee(); 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 962c5496..fd0646d5 100644 --- a/src/app/[locale]/(protected)/hr/employee-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/hr/employee-management/[id]/page.tsx @@ -33,13 +33,18 @@ export default function EmployeeDetailPage() { 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('[EmployeeDetailPage] fetchEmployee error:', error); } finally { setIsLoading(false); } - }, [params.id]); + }, [params.id, router]); useEffect(() => { fetchEmployee(); diff --git a/src/app/[locale]/(protected)/juil/order/base-info/categories/page.tsx b/src/app/[locale]/(protected)/juil/order/base-info/categories/page.tsx new file mode 100644 index 00000000..67802033 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/base-info/categories/page.tsx @@ -0,0 +1,5 @@ +import { CategoryManagement } from '@/components/business/juil/category-management'; + +export default function CategoriesPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/order/base-info/items/[id]/page.tsx b/src/app/[locale]/(protected)/juil/order/base-info/items/[id]/page.tsx new file mode 100644 index 00000000..d92bb56f --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/base-info/items/[id]/page.tsx @@ -0,0 +1,14 @@ +import { ItemDetailClient } from '@/components/business/juil/item-management'; + +interface ItemDetailPageProps { + params: Promise<{ id: string }>; + searchParams: Promise<{ mode?: string }>; +} + +export default async function ItemDetailPage({ params, searchParams }: ItemDetailPageProps) { + const { id } = await params; + const { mode } = await searchParams; + const isEditMode = mode === 'edit'; + + return ; +} diff --git a/src/app/[locale]/(protected)/juil/order/base-info/items/new/page.tsx b/src/app/[locale]/(protected)/juil/order/base-info/items/new/page.tsx new file mode 100644 index 00000000..08cd9ca4 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/base-info/items/new/page.tsx @@ -0,0 +1,5 @@ +import { ItemDetailClient } from '@/components/business/juil/item-management'; + +export default function ItemNewPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/order/base-info/items/page.tsx b/src/app/[locale]/(protected)/juil/order/base-info/items/page.tsx new file mode 100644 index 00000000..b7375be1 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/base-info/items/page.tsx @@ -0,0 +1,5 @@ +import { ItemManagementClient } from '@/components/business/juil/item-management'; + +export default function ItemManagementPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/juil/order/base-info/labor/[id]/page.tsx b/src/app/[locale]/(protected)/juil/order/base-info/labor/[id]/page.tsx new file mode 100644 index 00000000..967f9e54 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/base-info/labor/[id]/page.tsx @@ -0,0 +1,14 @@ +import { LaborDetailClient } from '@/components/business/juil/labor-management'; + +interface LaborDetailPageProps { + params: Promise<{ id: string }>; + searchParams: Promise<{ mode?: string }>; +} + +export default async function LaborDetailPage({ params, searchParams }: LaborDetailPageProps) { + const { id } = await params; + const { mode } = await searchParams; + const isEditMode = mode === 'edit'; + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/order/base-info/labor/new/page.tsx b/src/app/[locale]/(protected)/juil/order/base-info/labor/new/page.tsx new file mode 100644 index 00000000..5110107d --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/base-info/labor/new/page.tsx @@ -0,0 +1,5 @@ +import { LaborDetailClient } from '@/components/business/juil/labor-management'; + +export default function LaborNewPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/order/base-info/labor/page.tsx b/src/app/[locale]/(protected)/juil/order/base-info/labor/page.tsx new file mode 100644 index 00000000..72c4b0c3 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/base-info/labor/page.tsx @@ -0,0 +1,5 @@ +import { LaborManagementClient } from '@/components/business/juil/labor-management'; + +export default function LaborManagementPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/juil/order/base-info/pricing/[id]/edit/page.tsx b/src/app/[locale]/(protected)/juil/order/base-info/pricing/[id]/edit/page.tsx new file mode 100644 index 00000000..ab040adf --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/base-info/pricing/[id]/edit/page.tsx @@ -0,0 +1,11 @@ +import PricingDetailClient from '@/components/business/juil/pricing-management/PricingDetailClient'; + +interface PageProps { + params: Promise<{ id: string }>; +} + +export default async function PricingEditPage({ params }: PageProps) { + const { id } = await params; + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/order/base-info/pricing/[id]/page.tsx b/src/app/[locale]/(protected)/juil/order/base-info/pricing/[id]/page.tsx new file mode 100644 index 00000000..7dc36188 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/base-info/pricing/[id]/page.tsx @@ -0,0 +1,11 @@ +import PricingDetailClient from '@/components/business/juil/pricing-management/PricingDetailClient'; + +interface PageProps { + params: Promise<{ id: string }>; +} + +export default async function PricingDetailPage({ params }: PageProps) { + const { id } = await params; + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/order/base-info/pricing/new/page.tsx b/src/app/[locale]/(protected)/juil/order/base-info/pricing/new/page.tsx new file mode 100644 index 00000000..6b69184b --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/base-info/pricing/new/page.tsx @@ -0,0 +1,5 @@ +import PricingDetailClient from '@/components/business/juil/pricing-management/PricingDetailClient'; + +export default function PricingNewPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/order/base-info/pricing/page.tsx b/src/app/[locale]/(protected)/juil/order/base-info/pricing/page.tsx new file mode 100644 index 00000000..a999df1f --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/base-info/pricing/page.tsx @@ -0,0 +1,5 @@ +import PricingListClient from '@/components/business/juil/pricing-management/PricingListClient'; + +export default function PricingPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/order/order-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/juil/order/order-management/[id]/edit/page.tsx new file mode 100644 index 00000000..aa310045 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/order-management/[id]/edit/page.tsx @@ -0,0 +1,19 @@ +import { OrderDetailForm } from '@/components/business/juil/order-management'; +import { getOrderDetailFull } from '@/components/business/juil/order-management/actions'; +import { notFound } from 'next/navigation'; + +interface OrderEditPageProps { + params: Promise<{ id: string }>; +} + +export default async function OrderEditPage({ params }: OrderEditPageProps) { + const { id } = await params; + + const result = await getOrderDetailFull(id); + + if (!result.success || !result.data) { + notFound(); + } + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/order/order-management/[id]/page.tsx b/src/app/[locale]/(protected)/juil/order/order-management/[id]/page.tsx new file mode 100644 index 00000000..9f2b52c3 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/order-management/[id]/page.tsx @@ -0,0 +1,19 @@ +import { OrderDetailForm } from '@/components/business/juil/order-management'; +import { getOrderDetailFull } from '@/components/business/juil/order-management/actions'; +import { notFound } from 'next/navigation'; + +interface OrderDetailPageProps { + params: Promise<{ id: string }>; +} + +export default async function OrderDetailPage({ params }: OrderDetailPageProps) { + const { id } = await params; + + const result = await getOrderDetailFull(id); + + if (!result.success || !result.data) { + notFound(); + } + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/order/order-management/page.tsx b/src/app/[locale]/(protected)/juil/order/order-management/page.tsx new file mode 100644 index 00000000..d091b519 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/order-management/page.tsx @@ -0,0 +1,5 @@ +import { OrderManagementListClient } from '@/components/business/juil/order-management'; + +export default function OrderManagementPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/order/site-management/[id]/edit/page.tsx b/src/app/[locale]/(protected)/juil/order/site-management/[id]/edit/page.tsx new file mode 100644 index 00000000..d3ebe988 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/site-management/[id]/edit/page.tsx @@ -0,0 +1,27 @@ +import SiteDetailForm from '@/components/business/juil/site-management/SiteDetailForm'; + +// 목업 데이터 +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 { + params: Promise<{ id: string }>; +} + +export default async function SiteEditPage({ params }: PageProps) { + const { id } = await params; + + // TODO: API에서 현장 정보 조회 + const site = { ...MOCK_SITE, id }; + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/order/site-management/[id]/page.tsx b/src/app/[locale]/(protected)/juil/order/site-management/[id]/page.tsx new file mode 100644 index 00000000..8076a556 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/site-management/[id]/page.tsx @@ -0,0 +1,27 @@ +import SiteDetailForm from '@/components/business/juil/site-management/SiteDetailForm'; + +// 목업 데이터 +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 { + params: Promise<{ id: string }>; +} + +export default async function SiteDetailPage({ params }: PageProps) { + const { id } = await params; + + // TODO: API에서 현장 정보 조회 + const site = { ...MOCK_SITE, id }; + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/order/site-management/page.tsx b/src/app/[locale]/(protected)/juil/order/site-management/page.tsx new file mode 100644 index 00000000..7dd03391 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/site-management/page.tsx @@ -0,0 +1,5 @@ +import { SiteManagementListClient } from '@/components/business/juil/site-management'; + +export default function SiteManagementPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/order/structure-review/[id]/edit/page.tsx b/src/app/[locale]/(protected)/juil/order/structure-review/[id]/edit/page.tsx new file mode 100644 index 00000000..a879592a --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/structure-review/[id]/edit/page.tsx @@ -0,0 +1,32 @@ +import StructureReviewDetailForm from '@/components/business/juil/structure-review/StructureReviewDetailForm'; + +// 목업 데이터 +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 { + params: Promise<{ id: string }>; +} + +export default async function StructureReviewEditPage({ params }: PageProps) { + const { id } = await params; + + // TODO: API에서 구조검토 정보 조회 + const review = { ...MOCK_REVIEW, id }; + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/order/structure-review/[id]/page.tsx b/src/app/[locale]/(protected)/juil/order/structure-review/[id]/page.tsx new file mode 100644 index 00000000..16594f41 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/structure-review/[id]/page.tsx @@ -0,0 +1,32 @@ +import StructureReviewDetailForm from '@/components/business/juil/structure-review/StructureReviewDetailForm'; + +// 목업 데이터 +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 { + params: Promise<{ id: string }>; +} + +export default async function StructureReviewDetailPage({ params }: PageProps) { + const { id } = await params; + + // TODO: API에서 구조검토 정보 조회 + const review = { ...MOCK_REVIEW, id }; + + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/order/structure-review/page.tsx b/src/app/[locale]/(protected)/juil/order/structure-review/page.tsx new file mode 100644 index 00000000..dbf46ac3 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/order/structure-review/page.tsx @@ -0,0 +1,5 @@ +import StructureReviewListClient from '@/components/business/juil/structure-review/StructureReviewListClient'; + +export default function StructureReviewListPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/project/bidding/[id]/edit/page.tsx b/src/app/[locale]/(protected)/juil/project/bidding/[id]/edit/page.tsx new file mode 100644 index 00000000..9a940af6 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/project/bidding/[id]/edit/page.tsx @@ -0,0 +1,18 @@ +import { BiddingDetailForm, getBiddingDetail } from '@/components/business/juil/bidding'; + +interface BiddingEditPageProps { + params: Promise<{ id: string }>; +} + +export default async function BiddingEditPage({ params }: BiddingEditPageProps) { + const { id } = await params; + const result = await getBiddingDetail(id); + + return ( + + ); +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/project/bidding/[id]/page.tsx b/src/app/[locale]/(protected)/juil/project/bidding/[id]/page.tsx new file mode 100644 index 00000000..f4a30dfb --- /dev/null +++ b/src/app/[locale]/(protected)/juil/project/bidding/[id]/page.tsx @@ -0,0 +1,18 @@ +import { BiddingDetailForm, getBiddingDetail } from '@/components/business/juil/bidding'; + +interface BiddingDetailPageProps { + params: Promise<{ id: string }>; +} + +export default async function BiddingDetailPage({ params }: BiddingDetailPageProps) { + const { id } = await params; + const result = await getBiddingDetail(id); + + return ( + + ); +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/project/bidding/estimates/[id]/edit/page.tsx b/src/app/[locale]/(protected)/juil/project/bidding/estimates/[id]/edit/page.tsx new file mode 100644 index 00000000..c64a407c --- /dev/null +++ b/src/app/[locale]/(protected)/juil/project/bidding/estimates/[id]/edit/page.tsx @@ -0,0 +1,201 @@ +import { EstimateDetailForm } from '@/components/business/juil/estimates'; +import type { EstimateDetail } from '@/components/business/juil/estimates'; + +interface EstimateEditPageProps { + params: Promise<{ id: string }>; +} + +// 목업 데이터 - 추후 API 연동 +async function getEstimateDetail(id: string): Promise { + // TODO: 실제 API 연동 + const mockData: EstimateDetail = { + id, + estimateCode: '123123', + partnerId: '1', + partnerName: '회사명', + projectName: '현장명', + estimatorId: 'hong', + estimatorName: '이름', + itemCount: 21, + estimateAmount: 1420000, + completedDate: null, + bidDate: '2025-12-12', + status: 'pending', + createdAt: '2025-12-01', + updatedAt: '2025-12-01', + createdBy: 'hong', + siteBriefing: { + briefingCode: '123123', + partnerName: '회사명', + companyName: '회사명', + briefingDate: '2025-12-12', + attendee: '이름', + }, + bidInfo: { + projectName: '현장명', + bidDate: '2025-12-12', + siteCount: 21, + constructionPeriod: '2026-01-01 ~ 2026-12-10', + constructionStartDate: '2026-01-01', + constructionEndDate: '2026-12-10', + vatType: 'excluded', + workReport: '업무 보고 내용', + documents: [ + { + id: '1', + fileName: 'abc.zip', + fileUrl: '#', + fileSize: 1024000, + }, + ], + }, + summaryItems: [ + { + id: '1', + name: '서터 심창측공사', + quantity: 1, + unit: '식', + materialCost: 78540000, + laborCost: 15410000, + totalCost: 93950000, + remarks: '', + }, + ], + expenseItems: [ + { + id: '1', + name: 'public_1', + amount: 10000, + }, + ], + priceAdjustments: [ + { + id: '1', + category: '배합비', + unitPrice: 10000, + coating: 10000, + batting: 10000, + boxReinforce: 10500, + painting: 10500, + total: 51000, + }, + { + id: '2', + category: '재단비', + unitPrice: 1375, + coating: 0, + batting: 0, + boxReinforce: 0, + painting: 0, + total: 1375, + }, + { + id: '3', + category: '판매단가', + unitPrice: 0, + coating: 10000, + batting: 10000, + boxReinforce: 10500, + painting: 10500, + total: 41000, + }, + { + id: '4', + category: '조립단가', + unitPrice: 10300, + coating: 10300, + batting: 10300, + boxReinforce: 10500, + painting: 10200, + total: 51600, + }, + ], + detailItems: [ + { + id: '1', + no: 1, + name: 'FS530외/주차', + material: 'screen', + width: 2350, + height: 2500, + quantity: 1, + box: 1, + assembly: 0, + coating: 0, + batting: 0, + mounting: 0, + fitting: 0, + controller: 0, + widthConstruction: 0, + heightConstruction: 0, + materialCost: 1420000, + laborCost: 510000, + quantityPrice: 1930000, + expenseQuantity: 5500, + expenseTotal: 5500, + totalCost: 1930000, + otherCost: 0, + marginCost: 0, + totalPrice: 1930000, + unitPrice: 1420000, + expense: 0, + marginRate: 0, + unitQuantity: 1, + expenseResult: 0, + marginActual: 0, + }, + { + id: '2', + no: 2, + name: 'FS530외/주차', + material: 'screen', + width: 7500, + height: 2500, + quantity: 1, + box: 1, + assembly: 0, + coating: 0, + batting: 0, + mounting: 0, + fitting: 0, + controller: 0, + widthConstruction: 0, + heightConstruction: 0, + materialCost: 4720000, + laborCost: 780000, + quantityPrice: 5500000, + expenseQuantity: 5500, + expenseTotal: 5500, + totalCost: 5500000, + otherCost: 0, + marginCost: 0, + totalPrice: 5500000, + unitPrice: 4720000, + expense: 0, + marginRate: 0, + unitQuantity: 1, + expenseResult: 0, + marginActual: 0, + }, + ], + approval: { + approvers: [], + references: [], + }, + }; + + return mockData; +} + +export default async function EstimateEditPage({ params }: EstimateEditPageProps) { + const { id } = await params; + const detail = await getEstimateDetail(id); + + return ( + + ); +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/project/bidding/estimates/[id]/page.tsx b/src/app/[locale]/(protected)/juil/project/bidding/estimates/[id]/page.tsx new file mode 100644 index 00000000..73b9524a --- /dev/null +++ b/src/app/[locale]/(protected)/juil/project/bidding/estimates/[id]/page.tsx @@ -0,0 +1,201 @@ +import { EstimateDetailForm } from '@/components/business/juil/estimates'; +import type { EstimateDetail } from '@/components/business/juil/estimates'; + +interface EstimateDetailPageProps { + params: Promise<{ id: string }>; +} + +// 목업 데이터 - 추후 API 연동 +async function getEstimateDetail(id: string): Promise { + // TODO: 실제 API 연동 + const mockData: EstimateDetail = { + id, + estimateCode: '123123', + partnerId: '1', + partnerName: '회사명', + projectName: '현장명', + estimatorId: 'hong', + estimatorName: '이름', + itemCount: 21, + estimateAmount: 1420000, + completedDate: null, + bidDate: '2025-12-12', + status: 'pending', + createdAt: '2025-12-01', + updatedAt: '2025-12-01', + createdBy: 'hong', + siteBriefing: { + briefingCode: '123123', + partnerName: '회사명', + companyName: '회사명', + briefingDate: '2025-12-12', + attendee: '이름', + }, + bidInfo: { + projectName: '현장명', + bidDate: '2025-12-12', + siteCount: 21, + constructionPeriod: '2026-01-01 ~ 2026-12-10', + constructionStartDate: '2026-01-01', + constructionEndDate: '2026-12-10', + vatType: 'excluded', + workReport: '업무 보고 내용', + documents: [ + { + id: '1', + fileName: 'abc.zip', + fileUrl: '#', + fileSize: 1024000, + }, + ], + }, + summaryItems: [ + { + id: '1', + name: '서터 심창측공사', + quantity: 1, + unit: '식', + materialCost: 78540000, + laborCost: 15410000, + totalCost: 93950000, + remarks: '', + }, + ], + expenseItems: [ + { + id: '1', + name: 'public_1', + amount: 10000, + }, + ], + priceAdjustments: [ + { + id: '1', + category: '배합비', + unitPrice: 10000, + coating: 10000, + batting: 10000, + boxReinforce: 10500, + painting: 10500, + total: 51000, + }, + { + id: '2', + category: '재단비', + unitPrice: 1375, + coating: 0, + batting: 0, + boxReinforce: 0, + painting: 0, + total: 1375, + }, + { + id: '3', + category: '판매단가', + unitPrice: 0, + coating: 10000, + batting: 10000, + boxReinforce: 10500, + painting: 10500, + total: 41000, + }, + { + id: '4', + category: '조립단가', + unitPrice: 10300, + coating: 10300, + batting: 10300, + boxReinforce: 10500, + painting: 10200, + total: 51600, + }, + ], + detailItems: [ + { + id: '1', + no: 1, + name: 'FS530외/주차', + material: 'screen', + width: 2350, + height: 2500, + quantity: 1, + box: 1, + assembly: 0, + coating: 0, + batting: 0, + mounting: 0, + fitting: 0, + controller: 0, + widthConstruction: 0, + heightConstruction: 0, + materialCost: 1420000, + laborCost: 510000, + quantityPrice: 1930000, + expenseQuantity: 5500, + expenseTotal: 5500, + totalCost: 1930000, + otherCost: 0, + marginCost: 0, + totalPrice: 1930000, + unitPrice: 1420000, + expense: 0, + marginRate: 0, + unitQuantity: 1, + expenseResult: 0, + marginActual: 0, + }, + { + id: '2', + no: 2, + name: 'FS530외/주차', + material: 'screen', + width: 7500, + height: 2500, + quantity: 1, + box: 1, + assembly: 0, + coating: 0, + batting: 0, + mounting: 0, + fitting: 0, + controller: 0, + widthConstruction: 0, + heightConstruction: 0, + materialCost: 4720000, + laborCost: 780000, + quantityPrice: 5500000, + expenseQuantity: 5500, + expenseTotal: 5500, + totalCost: 5500000, + otherCost: 0, + marginCost: 0, + totalPrice: 5500000, + unitPrice: 4720000, + expense: 0, + marginRate: 0, + unitQuantity: 1, + expenseResult: 0, + marginActual: 0, + }, + ], + approval: { + approvers: [], + references: [], + }, + }; + + return mockData; +} + +export default async function EstimateDetailPage({ params }: EstimateDetailPageProps) { + const { id } = await params; + const detail = await getEstimateDetail(id); + + return ( + + ); +} diff --git a/src/app/[locale]/(protected)/juil/project/bidding/page.tsx b/src/app/[locale]/(protected)/juil/project/bidding/page.tsx new file mode 100644 index 00000000..064193d7 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/project/bidding/page.tsx @@ -0,0 +1,5 @@ +import { BiddingListClient } from '@/components/business/juil/bidding'; + +export default function BiddingPage() { + return ; +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/project/contract/[id]/edit/page.tsx b/src/app/[locale]/(protected)/juil/project/contract/[id]/edit/page.tsx new file mode 100644 index 00000000..7de01752 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/project/contract/[id]/edit/page.tsx @@ -0,0 +1,19 @@ +import ContractDetailForm from '@/components/business/juil/contract/ContractDetailForm'; +import { getContractDetail } from '@/components/business/juil/contract'; + +interface ContractEditPageProps { + params: Promise<{ id: string }>; +} + +export default async function ContractEditPage({ params }: ContractEditPageProps) { + const { id } = await params; + const result = await getContractDetail(id); + + return ( + + ); +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/project/contract/[id]/page.tsx b/src/app/[locale]/(protected)/juil/project/contract/[id]/page.tsx new file mode 100644 index 00000000..59041075 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/project/contract/[id]/page.tsx @@ -0,0 +1,19 @@ +import ContractDetailForm from '@/components/business/juil/contract/ContractDetailForm'; +import { getContractDetail } from '@/components/business/juil/contract'; + +interface ContractDetailPageProps { + params: Promise<{ id: string }>; +} + +export default async function ContractDetailPage({ params }: ContractDetailPageProps) { + const { id } = await params; + const result = await getContractDetail(id); + + return ( + + ); +} diff --git a/src/app/[locale]/(protected)/juil/project/contract/handover-report/[id]/edit/page.tsx b/src/app/[locale]/(protected)/juil/project/contract/handover-report/[id]/edit/page.tsx new file mode 100644 index 00000000..b1593cd0 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/project/contract/handover-report/[id]/edit/page.tsx @@ -0,0 +1,23 @@ +import { HandoverReportDetailForm, getHandoverReportDetail } from '@/components/business/juil/handover-report'; + +interface HandoverReportEditPageProps { + params: Promise<{ + id: string; + locale: string; + }>; +} + +export default async function HandoverReportEditPage({ params }: HandoverReportEditPageProps) { + const { id } = await params; + + // 서버에서 상세 데이터 조회 + const result = await getHandoverReportDetail(id); + + return ( + + ); +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/project/contract/handover-report/[id]/page.tsx b/src/app/[locale]/(protected)/juil/project/contract/handover-report/[id]/page.tsx new file mode 100644 index 00000000..77d515e0 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/project/contract/handover-report/[id]/page.tsx @@ -0,0 +1,23 @@ +import { HandoverReportDetailForm, getHandoverReportDetail } from '@/components/business/juil/handover-report'; + +interface HandoverReportDetailPageProps { + params: Promise<{ + id: string; + locale: string; + }>; +} + +export default async function HandoverReportDetailPage({ params }: HandoverReportDetailPageProps) { + const { id } = await params; + + // 서버에서 상세 데이터 조회 + const result = await getHandoverReportDetail(id); + + return ( + + ); +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/juil/project/contract/handover-report/page.tsx b/src/app/[locale]/(protected)/juil/project/contract/handover-report/page.tsx new file mode 100644 index 00000000..4ee2ca98 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/project/contract/handover-report/page.tsx @@ -0,0 +1,5 @@ +import { HandoverReportListClient } from '@/components/business/juil/handover-report'; + +export default function HandoverReportPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/juil/project/contract/page.tsx b/src/app/[locale]/(protected)/juil/project/contract/page.tsx new file mode 100644 index 00000000..c3d21660 --- /dev/null +++ b/src/app/[locale]/(protected)/juil/project/contract/page.tsx @@ -0,0 +1,5 @@ +import { ContractListClient } from '@/components/business/juil/contract'; + +export default function ContractPage() { + return ; +} diff --git a/src/app/[locale]/(protected)/settings/accounts/[id]/page.tsx b/src/app/[locale]/(protected)/settings/accounts/[id]/page.tsx index f5964262..2f23ea43 100644 --- a/src/app/[locale]/(protected)/settings/accounts/[id]/page.tsx +++ b/src/app/[locale]/(protected)/settings/accounts/[id]/page.tsx @@ -7,102 +7,122 @@ import type { Account } from '@/components/settings/AccountManagement/types'; // Mock 데이터 (API 연동 전 임시) const mockAccounts: Account[] = [ { - id: 'account-1', + id: 1, bankCode: 'shinhan', + bankName: '신한은행', accountNumber: '1234-1234-1234-1234', accountName: '운영계좌 1', accountHolder: '예금주1', status: 'active', + isPrimary: true, createdAt: '2025-12-19T00:00:00.000Z', updatedAt: '2025-12-19T00:00:00.000Z', }, { - id: 'account-2', + id: 2, bankCode: 'kb', + bankName: 'KB국민은행', accountNumber: '1234-1234-1234-1235', accountName: '운영계좌 2', accountHolder: '예금주2', status: 'inactive', + isPrimary: false, createdAt: '2025-12-19T00:00:00.000Z', updatedAt: '2025-12-19T00:00:00.000Z', }, { - id: 'account-3', + id: 3, bankCode: 'woori', + bankName: '우리은행', accountNumber: '1234-1234-1234-1236', accountName: '운영계좌 3', accountHolder: '예금주3', status: 'active', + isPrimary: false, createdAt: '2025-12-19T00:00:00.000Z', updatedAt: '2025-12-19T00:00:00.000Z', }, { - id: 'account-4', + id: 4, bankCode: 'hana', + bankName: '하나은행', accountNumber: '1234-1234-1234-1237', accountName: '운영계좌 4', accountHolder: '예금주4', status: 'inactive', + isPrimary: false, createdAt: '2025-12-19T00:00:00.000Z', updatedAt: '2025-12-19T00:00:00.000Z', }, { - id: 'account-5', + id: 5, bankCode: 'nh', + bankName: 'NH농협은행', accountNumber: '1234-1234-1234-1238', accountName: '운영계좌 5', accountHolder: '예금주5', status: 'active', + isPrimary: false, createdAt: '2025-12-19T00:00:00.000Z', updatedAt: '2025-12-19T00:00:00.000Z', }, { - id: 'account-6', + id: 6, bankCode: 'ibk', + bankName: 'IBK기업은행', accountNumber: '1234-1234-1234-1239', accountName: '운영계좌 6', accountHolder: '예금주6', status: 'inactive', + isPrimary: false, createdAt: '2025-12-19T00:00:00.000Z', updatedAt: '2025-12-19T00:00:00.000Z', }, { - id: 'account-7', + id: 7, bankCode: 'shinhan', + bankName: '신한은행', accountNumber: '1234-1234-1234-1240', accountName: '운영계좌 7', accountHolder: '예금주7', status: 'active', + isPrimary: false, createdAt: '2025-12-19T00:00:00.000Z', updatedAt: '2025-12-19T00:00:00.000Z', }, { - id: 'account-8', + id: 8, bankCode: 'kb', + bankName: 'KB국민은행', accountNumber: '1234-1234-1234-1241', accountName: '운영계좌 8', accountHolder: '예금주8', status: 'inactive', + isPrimary: false, createdAt: '2025-12-19T00:00:00.000Z', updatedAt: '2025-12-19T00:00:00.000Z', }, { - id: 'account-9', + id: 9, bankCode: 'woori', + bankName: '우리은행', accountNumber: '1234-1234-1234-1242', accountName: '운영계좌 9', accountHolder: '예금주9', status: 'active', + isPrimary: false, createdAt: '2025-12-19T00:00:00.000Z', updatedAt: '2025-12-19T00:00:00.000Z', }, { - id: 'account-10', + id: 10, bankCode: 'hana', + bankName: '하나은행', accountNumber: '1234-1234-1234-1243', accountName: '운영계좌 10', accountHolder: '예금주10', status: 'inactive', + isPrimary: false, createdAt: '2025-12-19T00:00:00.000Z', updatedAt: '2025-12-19T00:00:00.000Z', }, @@ -110,7 +130,7 @@ const mockAccounts: Account[] = [ export default function AccountDetailPage() { const params = useParams(); - const accountId = params.id as string; + const accountId = Number(params.id); // Mock: 계좌 조회 const account = mockAccounts.find(a => a.id === accountId); diff --git a/src/app/globals.css b/src/app/globals.css index e20b6fd1..6c521682 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -453,4 +453,93 @@ html { [data-slot="sheet-overlay"][data-state="closed"] { animation: fadeOut 200ms ease-out forwards; +} + +/* ========================================== + Print Styles - 인쇄 시 문서만 출력 + ========================================== */ +@media print { + /* A4 용지 설정 */ + @page { + size: A4 portrait; + margin: 10mm; + } + + /* 배경색 유지 */ + * { + -webkit-print-color-adjust: exact !important; + print-color-adjust: exact !important; + color-adjust: exact !important; + } + + /* ======================================== + 인쇄 스타일 (JavaScript printArea 사용 시 기본값) + ======================================== */ + + /* 기본 설정 - printArea 유틸리티가 새 창에서 인쇄하므로 간단하게 유지 */ + html, body { + background: white !important; + } + + /* print-hidden 클래스 숨김 */ + .print-hidden { + display: none !important; + } + + /* ======================================== + 테이블 & 페이지 설정 + ======================================== */ + + /* 페이지 나눔 방지 */ + table, figure, .page-break-avoid { + page-break-inside: avoid; + } + + /* 인쇄용 테이블 스타일 */ + .print-area table { + border-collapse: collapse !important; + } + + .print-area th, + .print-area td { + border: 1px solid #000 !important; + } + + /* print-area 내부 문서 wrapper - transform 제거 */ + .print-area > div { + max-width: none !important; + width: 100% !important; + margin: 0 !important; + padding: 0 !important; + box-shadow: none !important; + transform: none !important; + } + + /* 실제 문서 컨테이너 - A4에 맞게 조정 */ + .print-area > div > div { + width: 100% !important; + max-width: 190mm !important; + min-height: auto !important; + margin: 0 auto !important; + padding: 5mm !important; + box-shadow: none !important; + font-size: 10pt !important; + } + + /* 테이블 폰트 크기 축소 */ + .print-area table { + font-size: 9pt !important; + } + + .print-area .text-xs { + font-size: 8pt !important; + } + + .print-area .text-sm { + font-size: 9pt !important; + } + + .print-area .text-3xl { + font-size: 18pt !important; + } } \ No newline at end of file diff --git a/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx b/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx index 625ad803..bcd2104e 100644 --- a/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx +++ b/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx @@ -334,7 +334,7 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp -
@@ -345,7 +345,7 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp -
@@ -917,7 +917,7 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp className="flex-1 bg-white" rows={2} /> - @@ -988,7 +988,7 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp 취소 저장 diff --git a/src/components/accounting/BillManagement/BillDetail.tsx b/src/components/accounting/BillManagement/BillDetail.tsx index b8e6a399..e8f2de11 100644 --- a/src/components/accounting/BillManagement/BillDetail.tsx +++ b/src/components/accounting/BillManagement/BillDetail.tsx @@ -7,6 +7,7 @@ import { Plus, X, Loader2, + List, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -274,6 +275,7 @@ export function BillDetail({ billId, mode }: BillDetailProps) { {isViewMode ? ( <> - @@ -295,7 +297,7 @@ export function BillDetail({ billId, mode }: BillDetailProps) { diff --git a/src/components/accounting/BillManagement/index.tsx b/src/components/accounting/BillManagement/index.tsx index b3d462fd..2bb34800 100644 --- a/src/components/accounting/BillManagement/index.tsx +++ b/src/components/accounting/BillManagement/index.tsx @@ -472,7 +472,7 @@ export function BillManagement({ initialVendorId, initialBillType }: BillManagem {/* 저장 버튼 */} - diff --git a/src/components/accounting/CardTransactionInquiry/index.tsx b/src/components/accounting/CardTransactionInquiry/index.tsx index 81fcee20..19a871b5 100644 --- a/src/components/accounting/CardTransactionInquiry/index.tsx +++ b/src/components/accounting/CardTransactionInquiry/index.tsx @@ -364,7 +364,7 @@ export function CardTransactionInquiry({ ))} - @@ -487,7 +487,7 @@ export function CardTransactionInquiry({ - @@ -202,7 +204,7 @@ export function DepositDetail({ depositId, mode }: DepositDetailProps) { @@ -584,7 +584,7 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan diff --git a/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx b/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx index 55e6a3f8..75fdf040 100644 --- a/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx +++ b/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx @@ -34,7 +34,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; -import { FileText, Plus, X, Eye, Receipt } from 'lucide-react'; +import { FileText, Plus, X, Eye, Receipt, List } from 'lucide-react'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import { DocumentDetailModal } from '@/components/approval/DocumentDetail'; @@ -293,6 +293,7 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) { {isViewMode ? ( <> - @@ -312,7 +313,7 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) { - diff --git a/src/components/accounting/PurchaseManagement/index.tsx b/src/components/accounting/PurchaseManagement/index.tsx index f34ae85b..b282f409 100644 --- a/src/components/accounting/PurchaseManagement/index.tsx +++ b/src/components/accounting/PurchaseManagement/index.tsx @@ -539,7 +539,7 @@ export function PurchaseManagement() { ))} - @@ -622,7 +622,7 @@ export function PurchaseManagement() { diff --git a/src/components/accounting/ReceivablesStatus/actions.ts b/src/components/accounting/ReceivablesStatus/actions.ts index add6d554..cf4f2b1a 100644 --- a/src/components/accounting/ReceivablesStatus/actions.ts +++ b/src/components/accounting/ReceivablesStatus/actions.ts @@ -55,6 +55,7 @@ function transformItem(item: VendorReceivablesApi): VendorReceivables { total: cat.amounts.total, }, })), + memo: (item as VendorReceivablesApi & { memo?: string }).memo ?? '', }; } diff --git a/src/components/accounting/ReceivablesStatus/index.tsx b/src/components/accounting/ReceivablesStatus/index.tsx index 7884bd72..34d6230b 100644 --- a/src/components/accounting/ReceivablesStatus/index.tsx +++ b/src/components/accounting/ReceivablesStatus/index.tsx @@ -83,6 +83,7 @@ export function ReceivablesStatus({ highlightVendorId, initialData, initialSumma overdueVendorCount: 0, }); const [isLoading, setIsLoading] = useState(!initialData?.items?.length); + const [expandedMemos, setExpandedMemos] = useState>(new Set()); // ===== 데이터 로드 ===== const loadData = useCallback(async () => { @@ -193,6 +194,19 @@ export function ReceivablesStatus({ highlightVendorId, initialData, initialSumma )); }, []); + // ===== 메모 펼치기/접기 토글 ===== + const toggleMemoExpand = useCallback((vendorId: string) => { + setExpandedMemos(prev => { + const newSet = new Set(prev); + if (newSet.has(vendorId)) { + newSet.delete(vendorId); + } else { + newSet.add(vendorId); + } + return newSet; + }); + }, []); + // ===== 엑셀 다운로드 핸들러 ===== const handleExcelDownload = useCallback(async () => { const result = await exportReceivablesExcel({ @@ -315,6 +329,9 @@ export function ReceivablesStatus({ highlightVendorId, initialData, initialSumma // ===== 월 개수 (동적) ===== const monthCount = monthLabels.length || 12; + // ===== 카테고리 순서 (메모 제외 - 별도 렌더링) ===== + const categoryOrder: CategoryType[] = ['sales', 'deposit', 'bill', 'receivable']; + return ( {/* 페이지 헤더 */} @@ -395,7 +412,7 @@ export function ReceivablesStatus({ highlightVendorId, initialData, initialSumma size="sm" onClick={handleSave} disabled={isPending || totalChangedCount === 0} - className="bg-orange-500 hover:bg-orange-600 disabled:opacity-50" + className="bg-blue-500 hover:bg-blue-600 disabled:opacity-50" > {isPending ? ( diff --git a/src/components/accounting/ReceivablesStatus/types.ts b/src/components/accounting/ReceivablesStatus/types.ts index 7fa06251..e67a7fdb 100644 --- a/src/components/accounting/ReceivablesStatus/types.ts +++ b/src/components/accounting/ReceivablesStatus/types.ts @@ -47,6 +47,7 @@ export interface VendorReceivables { carryForwardBalance: number; // 이월잔액 monthLabels: string[]; // 동적 월 레이블 (ex: ['25.02', '25.03', ...]) categories: CategoryData[]; + memo?: string; // 거래처별 메모 (단일 텍스트) } /** diff --git a/src/components/accounting/SalesManagement/SalesDetail.tsx b/src/components/accounting/SalesManagement/SalesDetail.tsx index 9ebb65ab..2682290c 100644 --- a/src/components/accounting/SalesManagement/SalesDetail.tsx +++ b/src/components/accounting/SalesManagement/SalesDetail.tsx @@ -12,6 +12,7 @@ import { Send, FileText, Loader2, + List, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -310,6 +311,7 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) { {isViewMode ? ( <> - @@ -329,7 +331,7 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) { - diff --git a/src/components/accounting/SalesManagement/index.tsx b/src/components/accounting/SalesManagement/index.tsx index ddb6974b..2d3188d3 100644 --- a/src/components/accounting/SalesManagement/index.tsx +++ b/src/components/accounting/SalesManagement/index.tsx @@ -582,7 +582,7 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem ))} - @@ -641,7 +641,7 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem diff --git a/src/components/accounting/VendorLedger/VendorLedgerDetail.tsx b/src/components/accounting/VendorLedger/VendorLedgerDetail.tsx index 8d901405..49be9045 100644 --- a/src/components/accounting/VendorLedger/VendorLedgerDetail.tsx +++ b/src/components/accounting/VendorLedger/VendorLedgerDetail.tsx @@ -3,7 +3,7 @@ import { useState, useMemo, useCallback, useEffect } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { format, startOfMonth, endOfMonth } from 'date-fns'; -import { FileText, Download, Pencil, Loader2 } from 'lucide-react'; +import { FileText, Download, Pencil, Loader2, List } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { @@ -171,6 +171,7 @@ export function VendorLedgerDetail({ diff --git a/src/components/accounting/VendorManagement/VendorDetail.tsx b/src/components/accounting/VendorManagement/VendorDetail.tsx index 4318add1..73452545 100644 --- a/src/components/accounting/VendorManagement/VendorDetail.tsx +++ b/src/components/accounting/VendorManagement/VendorDetail.tsx @@ -301,7 +301,7 @@ export function VendorDetail({ mode, vendorId }: VendorDetailProps) { > 삭제 - @@ -312,7 +312,7 @@ export function VendorDetail({ mode, vendorId }: VendorDetailProps) { - @@ -632,7 +632,7 @@ export function VendorDetail({ mode, vendorId }: VendorDetailProps) { className="flex-1 bg-white" rows={2} /> - @@ -708,7 +708,7 @@ export function VendorDetail({ mode, vendorId }: VendorDetailProps) { 취소 {isSaving ? '처리중...' : '확인'} diff --git a/src/components/accounting/VendorManagement/VendorDetailClient.tsx b/src/components/accounting/VendorManagement/VendorDetailClient.tsx index 03f376c2..27b76827 100644 --- a/src/components/accounting/VendorManagement/VendorDetailClient.tsx +++ b/src/components/accounting/VendorManagement/VendorDetailClient.tsx @@ -1,8 +1,8 @@ 'use client'; -import { useState, useCallback, useMemo } from 'react'; +import { useState, useCallback, useMemo, useEffect } from 'react'; import { useRouter } from 'next/navigation'; -import { Building2, Plus, X, Loader2 } from 'lucide-react'; +import { Building2, Plus, X, Loader2, List } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -125,6 +125,16 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail // 새 메모 입력 const [newMemo, setNewMemo] = useState(''); + // 상세/수정 모드에서 로고 목데이터 초기화 + useEffect(() => { + if (initialData && !formData.logoUrl) { + setFormData(prev => ({ + ...prev, + logoUrl: 'https://placehold.co/750x250/3b82f6/white?text=Vendor+Logo', + })); + } + }, [initialData]); + // 필드 변경 핸들러 const handleChange = useCallback((field: string, value: string | number | boolean) => { setFormData(prev => ({ ...prev, [field]: value })); @@ -257,12 +267,13 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail return (
-
@@ -273,7 +284,7 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail - @@ -438,11 +449,21 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail
-

750 X 250px, 10MB 이하의 PNG, JPEG, GIF

- {!isViewMode && ( - + {formData.logoUrl ? ( + 회사 로고 + ) : ( + <> +

750 X 250px, 10MB 이하의 PNG, JPEG, GIF

+ {!isViewMode && ( + + )} + )}
@@ -528,7 +549,7 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail className="flex-1 bg-white" rows={2} /> - @@ -605,7 +626,7 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail 취소 {isLoading && } diff --git a/src/components/accounting/VendorManagement/types.ts b/src/components/accounting/VendorManagement/types.ts index b51e8bab..6b7eda13 100644 --- a/src/components/accounting/VendorManagement/types.ts +++ b/src/components/accounting/VendorManagement/types.ts @@ -1,6 +1,64 @@ +// ===== API 응답 타입 ===== +export interface ApiResponse { + success: boolean; + data?: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page?: number; + size?: number; +} + +// ===== API 데이터 타입 (백엔드에서 오는 원본 데이터) ===== +export interface ClientApiData { + id: number; + client_code: string; + name: string; + client_type: 'SALES' | 'PURCHASE' | 'BOTH'; + business_no: string; + contact_person: string; + phone: string; + mobile: string; + fax: string; + email: string; + address: string; + manager_name: string; + manager_tel: string; + system_manager: string; + purchase_payment_day: string; + sales_payment_day: string; + business_type: string; + business_item: string; + bad_debt: boolean; + memo: string; + is_active: boolean; + account_id: string; + outstanding_amount: number; + bad_debt_total: number; + has_bad_debt: boolean; + created_at: string; + updated_at: string; +} + // ===== 거래처 구분 ===== export type VendorCategory = 'sales' | 'purchase' | 'both'; +// ===== API client_type ↔ VendorCategory 변환 ===== +export const CLIENT_TYPE_TO_CATEGORY: Record = { + SALES: 'sales', + PURCHASE: 'purchase', + BOTH: 'both', +}; + +export const CATEGORY_TO_CLIENT_TYPE: Record = { + sales: 'SALES', + purchase: 'PURCHASE', + both: 'BOTH', +}; + export const VENDOR_CATEGORY_LABELS: Record = { sales: '매출', purchase: '매입', diff --git a/src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx b/src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx index d8a24a03..56d3ec5c 100644 --- a/src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx +++ b/src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx @@ -4,6 +4,7 @@ import { useState, useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { Banknote, + List, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -180,6 +181,7 @@ export function WithdrawalDetail({ withdrawalId, mode }: WithdrawalDetailProps) {isViewMode ? ( <> - @@ -202,7 +204,7 @@ export function WithdrawalDetail({ withdrawalId, mode }: WithdrawalDetailProps) @@ -578,7 +578,7 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra diff --git a/src/components/approval/DocumentDetail/index.tsx b/src/components/approval/DocumentDetail/index.tsx index bcd486e7..3414fecf 100644 --- a/src/components/approval/DocumentDetail/index.tsx +++ b/src/components/approval/DocumentDetail/index.tsx @@ -30,6 +30,7 @@ import { import { ProposalDocument } from './ProposalDocument'; import { ExpenseReportDocument } from './ExpenseReportDocument'; import { ExpenseEstimateDocument } from './ExpenseEstimateDocument'; +import { printArea } from '@/lib/print-utils'; import type { DocumentType, DocumentDetailModalProps, @@ -68,7 +69,7 @@ export function DocumentDetailModal({ }; const handlePrint = () => { - window.print(); + printArea({ title: `${getDocumentTitle()} 인쇄` }); }; const handleSharePdf = () => { @@ -107,8 +108,8 @@ export function DocumentDetailModal({ {getDocumentTitle()} 상세 - {/* 헤더 영역 - 고정 */} -
+ {/* 헤더 영역 - 고정 (인쇄 시 숨김) */} +

{getDocumentTitle()} 상세

- {/* 버튼 영역 - 고정 */} -
+ {/* 버튼 영역 - 고정 (인쇄 시 숨김) */} +
{/* 기안함 모드 + 임시저장 상태: 복제, 상신, 인쇄 */} {mode === 'draft' && documentStatus === 'draft' && ( <> @@ -191,8 +192,8 @@ export function DocumentDetailModal({ */}
- {/* 문서 영역 - 스크롤 */} -
+ {/* 문서 영역 - 스크롤 (인쇄 시 이 영역만 출력) */} +
{renderDocument()}
diff --git a/src/components/attendance/actions.ts b/src/components/attendance/actions.ts index a2030166..a22123db 100644 --- a/src/components/attendance/actions.ts +++ b/src/components/attendance/actions.ts @@ -9,7 +9,7 @@ 'use server'; -import { cookies } from 'next/headers'; +import { serverFetch, getServerApiHeaders } from '@/lib/api/fetch-wrapper'; // ============================================ // 타입 정의 @@ -155,12 +155,10 @@ export async function checkIn( */ export async function checkOut( data: CheckOutRequest -): Promise<{ success: boolean; data?: AttendanceRecord; error?: string }> { +): Promise<{ success: boolean; data?: AttendanceRecord; error?: string; __authError?: boolean }> { try { - const headers = await getApiHeaders(); - const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances/check-out`, { + const { response, error } = await serverFetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances/check-out`, { method: 'POST', - headers, body: JSON.stringify({ user_id: data.userId, check_out: data.checkOut, @@ -195,11 +193,11 @@ export async function checkOut( success: false, error: result.message || '퇴근 기록에 실패했습니다.', }; - } catch (error) { - console.error('[checkOut] Error:', error); + } catch (err) { + console.error('[checkOut] Error:', err); return { success: false, - error: error instanceof Error ? error.message : '퇴근 기록에 실패했습니다.', + error: err instanceof Error ? err.message : '퇴근 기록에 실패했습니다.', }; } } diff --git a/src/components/business/juil/bidding/BiddingDetailForm.tsx b/src/components/business/juil/bidding/BiddingDetailForm.tsx new file mode 100644 index 00000000..f6d1180b --- /dev/null +++ b/src/components/business/juil/bidding/BiddingDetailForm.tsx @@ -0,0 +1,606 @@ +'use client'; + +import { useState, useCallback, useMemo } from 'react'; +import { useRouter } from 'next/navigation'; +import { Loader2, List } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { PageLayout } from '@/components/organisms/PageLayout'; +import { PageHeader } from '@/components/organisms/PageHeader'; +import { toast } from 'sonner'; +import type { BiddingDetail, BiddingDetailFormData } from './types'; +import { + BIDDING_STATUS_OPTIONS, + BIDDING_STATUS_STYLES, + BIDDING_STATUS_LABELS, + VAT_TYPE_OPTIONS, + getEmptyBiddingDetailFormData, + biddingDetailToFormData, +} from './types'; +import { updateBidding } from './actions'; + +// 금액 포맷팅 +function formatAmount(amount: number): string { + return new Intl.NumberFormat('ko-KR').format(amount); +} + +interface BiddingDetailFormProps { + mode: 'view' | 'edit'; + biddingId: string; + initialData?: BiddingDetail; +} + +export default function BiddingDetailForm({ + mode, + biddingId, + initialData, +}: BiddingDetailFormProps) { + const router = useRouter(); + const isViewMode = mode === 'view'; + const _isEditMode = mode === 'edit'; + + // 폼 데이터 + const [formData, setFormData] = useState( + initialData ? biddingDetailToFormData(initialData) : getEmptyBiddingDetailFormData() + ); + + // 로딩 상태 + const [isLoading, setIsLoading] = useState(false); + + // 다이얼로그 상태 + const [showSaveDialog, setShowSaveDialog] = useState(false); + + // 공과 합계 계산 + const expenseTotal = useMemo(() => { + return formData.expenseItems.reduce((sum, item) => sum + item.amount, 0); + }, [formData.expenseItems]); + + // 견적 상세 합계 계산 (견적 상세 페이지와 동일) + const estimateDetailTotals = useMemo(() => { + return formData.estimateDetailItems.reduce( + (acc, item) => ({ + weight: acc.weight + (item.weight || 0), + area: acc.area + (item.area || 0), + steelScreen: acc.steelScreen + (item.steelScreen || 0), + caulking: acc.caulking + (item.caulking || 0), + rail: acc.rail + (item.rail || 0), + bottom: acc.bottom + (item.bottom || 0), + boxReinforce: acc.boxReinforce + (item.boxReinforce || 0), + shaft: acc.shaft + (item.shaft || 0), + painting: acc.painting + (item.painting || 0), + motor: acc.motor + (item.motor || 0), + controller: acc.controller + (item.controller || 0), + widthConstruction: acc.widthConstruction + (item.widthConstruction || 0), + heightConstruction: acc.heightConstruction + (item.heightConstruction || 0), + unitPrice: acc.unitPrice + (item.unitPrice || 0), + expense: acc.expense + (item.expense || 0), + quantity: acc.quantity + (item.quantity || 0), + cost: acc.cost + (item.cost || 0), + costExecution: acc.costExecution + (item.costExecution || 0), + marginCost: acc.marginCost + (item.marginCost || 0), + marginCostExecution: acc.marginCostExecution + (item.marginCostExecution || 0), + expenseExecution: acc.expenseExecution + (item.expenseExecution || 0), + }), + { + weight: 0, + area: 0, + steelScreen: 0, + caulking: 0, + rail: 0, + bottom: 0, + boxReinforce: 0, + shaft: 0, + painting: 0, + motor: 0, + controller: 0, + widthConstruction: 0, + heightConstruction: 0, + unitPrice: 0, + expense: 0, + quantity: 0, + cost: 0, + costExecution: 0, + marginCost: 0, + marginCostExecution: 0, + expenseExecution: 0, + } + ); + }, [formData.estimateDetailItems]); + + // 네비게이션 핸들러 + const handleBack = useCallback(() => { + router.push('/ko/juil/project/bidding'); + }, [router]); + + const handleEdit = useCallback(() => { + router.push(`/ko/juil/project/bidding/${biddingId}/edit`); + }, [router, biddingId]); + + const handleCancel = useCallback(() => { + router.push(`/ko/juil/project/bidding/${biddingId}`); + }, [router, biddingId]); + + // 저장 핸들러 + const handleSave = useCallback(() => { + setShowSaveDialog(true); + }, []); + + const handleConfirmSave = useCallback(async () => { + setIsLoading(true); + try { + const result = await updateBidding(biddingId, formData); + if (result.success) { + toast.success('수정이 완료되었습니다.'); + setShowSaveDialog(false); + router.push(`/ko/juil/project/bidding/${biddingId}`); + router.refresh(); + } else { + toast.error(result.error || '저장에 실패했습니다.'); + } + } catch (error) { + toast.error(error instanceof Error ? error.message : '저장에 실패했습니다.'); + } finally { + setIsLoading(false); + } + }, [router, biddingId, formData]); + + // 필드 변경 핸들러 + const handleFieldChange = useCallback( + (field: keyof BiddingDetailFormData, value: string | number) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + }, + [] + ); + + // 헤더 액션 버튼 + const headerActions = isViewMode ? ( +
+ + +
+ ) : ( +
+ + +
+ ); + + return ( + + + +
+ {/* 입찰 정보 섹션 */} + + + 입찰 정보 + + +
+ {/* 입찰번호 */} +
+ + +
+ + {/* 입찰자 */} +
+ + +
+ + {/* 거래처명 */} +
+ + +
+ + {/* 현장명 */} +
+ + +
+ + {/* 입찰일자 */} +
+ + {isViewMode ? ( + + ) : ( + handleFieldChange('biddingDate', e.target.value)} + /> + )} +
+ + {/* 개소 */} +
+ + +
+ + {/* 공사기간 */} +
+ +
+ {isViewMode ? ( + + ) : ( + <> + + handleFieldChange('constructionStartDate', e.target.value) + } + className="flex-1" + /> + ~ + + handleFieldChange('constructionEndDate', e.target.value) + } + className="flex-1" + /> + + )} +
+
+ + {/* 부가세 */} +
+ + {isViewMode ? ( + opt.value === formData.vatType)?.label || + formData.vatType + } + disabled + className="bg-muted" + /> + ) : ( + + )} +
+ + {/* 입찰금액 */} +
+ + +
+ + {/* 상태 */} +
+ + {isViewMode ? ( +
+ {BIDDING_STATUS_LABELS[formData.status]} +
+ ) : ( + + )} +
+ + {/* 투찰일 */} +
+ + {isViewMode ? ( + + ) : ( + handleFieldChange('submissionDate', e.target.value)} + /> + )} +
+ + {/* 확정일 */} +
+ + {isViewMode ? ( + + ) : ( + handleFieldChange('confirmDate', e.target.value)} + /> + )} +
+
+ + {/* 비고 */} +
+ + {isViewMode ? ( +