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/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..565469ff --- /dev/null +++ b/claudedocs/guides/[DESIGN-2026-01-02] document-modal-common-component.md @@ -0,0 +1,181 @@ +# 문서 모달 공통 컴포넌트 설계 요구사항 + +## 현황 분석 + +### 기존 문서 모달 목록 (6개) +| 컴포넌트 | 용도 | 헤더 구성 | +|---------|------|----------| +| ProcessWorkLogPreviewModal | 공정 작업일지 | 로고 + 제목 + 결재(3열) | +| WorkLogModal | 생산 작업일지 | 로고 + 제목 + 결재(3열) | +| EstimateDocumentModal | 견적서 | 제목 + 결재(3열) | +| OrderDocumentModal | 수주문서(3종) | 제목만 | +| ContractDocumentModal | 계약서 | PDF iframe | +| HandoverReportDocumentModal | 인수인계보고서 | 결재(4열) 먼저 | + +### 공통 패턴 ✅ +``` +1. 모달 프레임: Radix UI Dialog +2. 인쇄 처리: print-hidden + print-area 클래스 +3. 인쇄 유틸: printArea() 함수 (lib/print-utils.ts) +4. 용지 크기: max-w-[210mm] (A4 기준) +5. 레이아웃: 고정 헤더 + 스크롤 문서 영역 +``` + +### 변동 영역 🔄 +``` +1. 문서 헤더 (가장 복잡) + - 결재라인: 3열/4열/없음 + - 로고: 있음/없음, 좌측/우측 + - 제목: 중앙/좌측, 부제목 유무 + - 문서번호/날짜: 위치 다양 + +2. 버튼 영역 + - 인쇄만/수정+인쇄/상신+인쇄+삭제 등 + +3. 본문 테이블 + - 컬럼 구성, 합계행, 소계 등 +``` + +--- + +## 공통 컴포넌트 제안 + +### 1. PrintableDocumentModal (Base) +모달 프레임 + 인쇄 기능만 담당 + +```tsx +interface PrintableDocumentModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; // 모달 타이틀 + width?: 'sm' | 'md' | 'lg'; // 800px | 900px | 1000px + actions?: ReactNode; // 버튼 영역 (수정/상신/삭제 등) + children: ReactNode; // 문서 본문 +} +``` + +**제공 기능**: +- Dialog 래퍼 + print-hidden 헤더 +- 인쇄 버튼 (기본 제공) +- print-area 문서 영역 + A4 스타일 + +### 2. DocumentHeader (Composable) +문서 헤더 조합용 컴포넌트들 + +```tsx +// 결재라인 컴포넌트 + + +// 문서 타이틀 컴포넌트 + + +// 회사 로고 컴포넌트 + +``` + +### 3. 헤더 레이아웃 프리셋 + +```tsx +// 3열 레이아웃: 로고 | 타이틀 | 결재 + + + + + + +// 2열 레이아웃: 타이틀 | 결재 + + + + + +// 1열 레이아웃: 타이틀만 (중앙) + + + +``` + +--- + +## 컴포넌트 구조 제안 + +``` +src/components/common/document/ +├── PrintableDocumentModal.tsx # 기본 모달 프레임 +├── DocumentHeader/ +│ ├── index.tsx # 헤더 레이아웃 +│ ├── ApprovalLine.tsx # 결재라인 +│ ├── DocumentTitle.tsx # 문서 타이틀 +│ └── CompanyLogo.tsx # 회사 로고 +├── DocumentTable/ +│ ├── index.tsx # 기본 문서 테이블 +│ ├── SummaryRow.tsx # 합계행 +│ └── InfoGrid.tsx # 정보 그리드 (2×4 등) +└── index.ts # 배럴 export +``` + +--- + +## 마이그레이션 우선순위 + +| 우선순위 | 컴포넌트 | 이유 | +|---------|---------|------| +| 1 | WorkLogModal 계열 (2개) | 구조 동일, 가장 표준적 | +| 2 | EstimateDocumentModal | 결재라인 + 복잡한 테이블 | +| 3 | OrderDocumentModal | 3종 문서 분기 포함 | +| 4 | HandoverReportDocumentModal | 다른 헤더 구성 | +| 5 | ContractDocumentModal | PDF 특수 케이스 | + +--- + +## 결정 필요 사항 + +### Q1. 헤더 구성 접근법 +- **A) 프리셋 기반**: 3가지 레이아웃 프리셋으로 제한 +- **B) 완전 조합형**: 블록 컴포넌트 자유 조합 + +### Q2. 결재라인 데이터 +- **A) Props 직접 전달**: 사용처에서 데이터 구성 +- **B) Context/Hook**: 문서별 결재 설정 중앙 관리 + +### Q3. 테이블 공통화 범위 +- **A) 스타일만 공통화**: 테이블 래퍼 + CSS +- **B) 구조까지 공통화**: columns 정의 + 렌더링 로직 + +--- + +## 예상 작업량 + +| 단계 | 내용 | 예상 파일 수 | +|------|------|-------------| +| 1 | 공통 컴포넌트 생성 | 8개 | +| 2 | 기존 모달 리팩토링 | 6개 | +| 3 | 테스트 및 검증 | - | + +--- + +## 참고: 인쇄 유틸리티 + +```ts +// src/lib/print-utils.ts +printArea(options?: { title?: string; styles?: string }) +``` + +- `.print-area` 클래스 요소를 새 창에서 인쇄 +- A4 용지 설정 자동 적용 +- 기존 스타일시트 자동 로드 \ 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/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-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/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/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/VendorManagement/VendorDetailClient.tsx b/src/components/accounting/VendorManagement/VendorDetailClient.tsx index 03f376c2..14606279 100644 --- a/src/components/accounting/VendorManagement/VendorDetailClient.tsx +++ b/src/components/accounting/VendorManagement/VendorDetailClient.tsx @@ -1,6 +1,6 @@ '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 { Button } from '@/components/ui/button'; @@ -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 })); @@ -438,11 +448,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 && ( + + )} + )}
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/business/juil/bidding/BiddingDetailForm.tsx b/src/components/business/juil/bidding/BiddingDetailForm.tsx new file mode 100644 index 00000000..50215e66 --- /dev/null +++ b/src/components/business/juil/bidding/BiddingDetailForm.tsx @@ -0,0 +1,605 @@ +'use client'; + +import { useState, useCallback, useMemo } from 'react'; +import { useRouter } from 'next/navigation'; +import { Loader2 } 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 ? ( +