From 9d0cb073bab613630724f7f3cb8671eb92173386 Mon Sep 17 00:00:00 2001 From: byeongcheolryu Date: Fri, 28 Nov 2025 15:25:33 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=8B=9C=20=EC=84=B9=EC=85=98=20=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=ED=99=94=20=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 페이지 삭제 시 독립 섹션 목록 갱신 추가 (독립 엔티티 아키텍처) - ItemForm 컴포넌트 분리 완료 (1607→415줄, 74% 감소) - ItemMasterDataManagement 중복 코드 제거 (getInputTypeLabel 헬퍼) - 문서 업데이트 (realtime-sync-fixes.md) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- claudedocs/_index.md | 6 +- ...-2025-11-28] dynamic-page-rendering-api.md | 1114 +++++++++++++ .../[IMPL-2025-11-27] realtime-sync-fixes.md | 81 +- ...5-11-27] item-form-component-separation.md | 58 +- src/app/[locale]/(protected)/loading.tsx | 19 +- src/components/auth/LoginPage.tsx | 2 +- src/components/auth/SignupPage.tsx | 6 +- src/components/business/Dashboard.tsx | 13 +- src/components/items/ItemForm/index.tsx | 1448 ++--------------- .../items/ItemMasterDataManagement.tsx | 95 +- .../components/DraggableField.tsx | 5 +- .../components/DraggableSection.tsx | 4 +- src/components/items/ItemTypeSelect.tsx | 4 +- src/components/ui/loading-spinner.tsx | 29 +- src/contexts/ItemMasterContext.tsx | 11 + tsconfig.tsbuildinfo | 2 +- 16 files changed, 1462 insertions(+), 1435 deletions(-) create mode 100644 claudedocs/item-master/[API-REQUEST-2025-11-28] dynamic-page-rendering-api.md diff --git a/claudedocs/_index.md b/claudedocs/_index.md index 81d4da1b..ff727e51 100644 --- a/claudedocs/_index.md +++ b/claudedocs/_index.md @@ -1,6 +1,6 @@ # claudedocs 문서 맵 -> 프로젝트 기술 문서 인덱스 (Last Updated: 2025-11-27) +> 프로젝트 기술 문서 인덱스 (Last Updated: 2025-11-28) ## 폴더 구조 @@ -39,7 +39,9 @@ claudedocs/ | 파일 | 설명 | |------|------| -| `[IMPL-2025-11-27] realtime-sync-fixes.md` | ⭐ **최신** - 실시간 동기화 수정 (BOM, 섹션 복제, 항목 수정) | +| `[API-REQUEST-2025-11-28] dynamic-page-rendering-api.md` | ⭐ **최신** - 동적 페이지 렌더링 API 요청서 | +| `[PLAN-2025-11-27] item-form-component-separation.md` | ✅ **완료** - ItemForm 컴포넌트 분리 (1607→415줄, 74% 감소) | +| `[IMPL-2025-11-27] realtime-sync-fixes.md` | 실시간 동기화 수정 (BOM, 섹션 복제, 항목 수정, **페이지 삭제 시 섹션 동기화** 2025-11-28) | | `item-master-api-pending-tasks.md` | 진행중인 API 연동 작업 | | `item-master-pending-integration.md` | 대기중인 통합 작업 | | `item-master-specification.md` | API 명세 | diff --git a/claudedocs/item-master/[API-REQUEST-2025-11-28] dynamic-page-rendering-api.md b/claudedocs/item-master/[API-REQUEST-2025-11-28] dynamic-page-rendering-api.md new file mode 100644 index 00000000..94dbe2f9 --- /dev/null +++ b/claudedocs/item-master/[API-REQUEST-2025-11-28] dynamic-page-rendering-api.md @@ -0,0 +1,1114 @@ +# 품목관리 페이지 전체 API 요청서 + +## 작업 일자: 2025-11-28 + +## 문서 버전 +| 버전 | 날짜 | 작성자 | 내용 | +|------|------|--------|------| +| 1.0 | 2025-11-28 | Claude | 초안 작성 (동적 렌더링만) | +| 2.0 | 2025-11-28 | Claude | 전체 CRUD + 리스트 + 페이징 추가 | +| 3.0 | 2025-11-28 | Claude | 백엔드 스키마/기존 API 기반 재검토 | +| 3.1 | 2025-11-28 | Claude | **ID 기반으로 통일** (백엔드 결정 반영) | + +--- + +## 🔴 백엔드 검토 결과 요약 + +### 기존 API와의 정합성 분석 (v3.1 - ID 기반 통일) + +| 항목 | 최종 결정 | 비고 | +|------|----------|------| +| 품목 수정/삭제 | `/{id}` (ID 기반) | ✅ **ID 기반으로 통일** | +| 품목 조회 | `item_type` 파라미터 선택 | 서버에서 자동 감지 가능 | +| 페이징 파라미터 | `size` | 기존 백엔드 컨벤션 유지 | +| item-master init | 기존 API 활용 | `ItemMasterService@init` | +| BOM API | `/items/{id}/bom` | ✅ **ID 기반으로 통일** | +| 파일 API | `/files/upload` | 별도 파일 API | +| 테넌트 격리 | `tenant_id` 자동 | 서버 자동 처리 | + +### 주요 백엔드 아키텍처 특성 (반영 필요) + +```yaml +multi_tenancy: + - 모든 테이블에 tenant_id 필수 + - BelongsToTenant 글로벌 스코프 적용 + - API 레벨에서 자동 격리 (클라이언트 처리 불필요) + +soft_delete: + - deleted_at, deleted_by 컬럼 사용 + - 삭제 시 실제 삭제가 아닌 soft delete + +audit_columns: + - created_by, updated_by, deleted_by + - 사용자 추적 자동 처리 + +api_response_format: + success: true/false + data: {} | [] + message: string (i18n key) +``` + +--- + +## 작업 체크리스트 + +### Phase 1: 분석 단계 +- [x] 품목기준관리 API 저장/수정/삭제 검토 +- [x] 현재 품목관리 페이지 구조 분석 +- [x] 품목기준관리 저장 데이터 구조 확인 +- [x] 품목관리 리스트/상세/등록/수정 페이지 분석 + +### Phase 2: 설계 단계 +- [x] 품목 리스트 API (페이징, 필터, 검색) +- [x] 품목 CRUD API (생성, 조회, 수정, 삭제) +- [x] 동적 페이지 렌더링 API +- [x] BOM 관리 API +- [x] 파일 업로드 API +- [x] 통계/대시보드 API + +### Phase 3: 검토 단계 ✅ 완료 +- [x] 백엔드 DB 스키마 검토 (`database-schema.md`) +- [x] 기존 item-master-spec 검토 (`item-master-spec.md`) +- [x] sam-api 기존 API 구조 확인 (`api.php`, Controllers) +- [x] API 스펙 정합성 조정 + +### Phase 4: 확정 단계 +- [ ] 백엔드 팀 최종 리뷰 +- [ ] API 스펙 확정 +- [ ] 프론트엔드 연동 테스트 + +--- + +## 1. 개요 + +### 1.1 목적 +품목관리 페이지(`/items/*`)에서 필요한 **모든 API**를 정의합니다: +- 품목 목록 조회 (페이징, 필터, 검색) +- 품목 CRUD (생성, 조회, 수정, 삭제) +- 동적 폼 렌더링 (품목기준관리 연동) +- BOM 관리 +- 파일 업로드/다운로드 +- 통계 정보 + +### 1.2 관련 페이지 +| 경로 | 페이지 | 필요 API | +|------|--------|----------| +| `/items` | 품목 목록 | 리스트, 검색, 필터, 페이징, 통계, 삭제 | +| `/items/create` | 품목 등록 | 동적 폼 구조, 생성, BOM 검색, 파일 업로드 | +| `/items/[id]` | 품목 상세 | 단건 조회, BOM 조회 | +| `/items/[id]/edit` | 품목 수정 | 단건 조회, 수정, BOM 관리, 파일 업로드 | + +--- + +## 2. 품목 목록 API (List) + +### API 2.1: 품목 목록 조회 (페이징) + +> ⚠️ **기존 API 존재**: `ItemsController@index` - 파라미터명 조정 필요 + +``` +GET /api/v1/items +``` + +**Query Parameters** (기존 백엔드 기준): +| 파라미터 | 타입 | 필수 | 기본값 | 설명 | +|----------|------|------|--------|------| +| page | number | N | 1 | 페이지 번호 | +| size | number | N | 20 | 페이지당 항목 수 (기존: `size`, 변경 금지) | +| type | string | N | - | 품목유형 필터 (FG, PT, SM, RM, CS) | +| search | string | N | - | 검색어 (품목코드, 품목명) | +| q | string | N | - | 검색어 (search와 동일, 호환용) | +| category_id | number | N | - | 카테고리 ID 필터 | +| is_active | boolean | N | - | 활성 상태 필터 **(신규 요청)** | +| sort_by | string | N | created_at | 정렬 기준 **(신규 요청)** | +| sort_order | string | N | desc | 정렬 순서 **(신규 요청)** | + +**Request Example**: +``` +GET /api/v1/items?page=1&size=20&type=FG&search=스크린 +``` + +**Response**: +```json +{ + "success": true, + "data": { + "items": [ + { + "id": 1, + "item_code": "KD-FG-001", + "item_name": "스크린 제품 A", + "item_type": "FG", + "unit": "EA", + "specification": "2000x2000", + "is_active": true, + "category1": "본체부품", + "category2": "가이드시스템", + "sales_price": 150000, + "purchase_price": 100000, + "product_category": "SCREEN", + "current_revision": 0, + "is_final": false, + "created_at": "2025-01-10T00:00:00Z", + "updated_at": "2025-01-10T00:00:00Z" + } + ], + "pagination": { + "current_page": 1, + "per_page": 20, + "total_items": 150, + "total_pages": 8, + "has_next": true, + "has_prev": false + } + } +} +``` + +--- + +### API 2.2: 품목 통계 조회 + +> 🆕 **신규 API 요청** + +``` +GET /api/v1/items/stats +``` + +**Response**: +```json +{ + "success": true, + "data": { + "total_count": 500, + "by_item_type": { + "FG": 50, + "PT": 200, + "SM": 100, + "RM": 100, + "CS": 50 + }, + "active_count": 450, + "inactive_count": 50 + }, + "message": "message.fetched" +} +``` + +--- + +### API 2.3: 품목 검색 (BOM용 자동완성) +``` +GET /api/v1/items/search +``` + +**Query Parameters**: +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| q | string | Y | 검색어 (최소 2자) | +| item_type | string | N | 품목유형 필터 (복수 가능: PT,SM) | +| limit | number | N | 결과 개수 제한 (기본 10, 최대 50) | + +**Request Example**: +``` +GET /api/v1/items/search?q=가이드&item_type=PT&limit=10 +``` + +**Response**: +```json +{ + "success": true, + "data": [ + { + "id": 2, + "item_code": "KD-PT-001", + "item_name": "가이드레일(벽면형)", + "item_type": "PT", + "unit": "EA", + "specification": "2438mm", + "sales_price": 50000 + } + ] +} +``` + +--- + +## 3. 품목 CRUD API + +### API 3.1: 품목 단건 조회 (ID 기반) + +> ⚠️ **기존 API 존재**: `ItemsController@show` - `item_type` 파라미터 필수 + +``` +GET /api/v1/items/{id} +``` + +**Path Parameters**: +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| id | number | Y | 품목 ID | + +**Query Parameters** (기존 백엔드 기준): +| 파라미터 | 타입 | 필수 | 기본값 | 설명 | +|----------|------|------|--------|------| +| item_type | string | Y | PRODUCT | 품목 유형 (PRODUCT, MATERIAL) | +| include_price | boolean | N | false | 가격 정보 포함 여부 | +| client_id | number | N | - | 고객별 가격 조회 시 | +| price_date | string | N | - | 가격 기준일 (YYYY-MM-DD) | + +### API 3.1.1: 품목 단건 조회 (Code 기반) + +> ✅ **기존 API 존재**: `ItemsController@showByCode` + +``` +GET /api/v1/items/code/{code} +``` + +**Path Parameters**: +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| code | string | Y | 품목 코드 (예: KD-FG-001) | + +**Query Parameters**: +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| include_bom | boolean | N | BOM 정보 포함 여부 | + +**Response**: +```json +{ + "success": true, + "data": { + "id": 1, + "item_code": "KD-FG-001", + "item_name": "스크린 제품 A", + "item_type": "FG", + "unit": "EA", + "specification": "2000x2000", + "is_active": true, + "product_category": "SCREEN", + "lot_abbreviation": "KD", + + "category1": "본체부품", + "category2": "가이드시스템", + "category3": null, + + "purchase_price": 100000, + "sales_price": 150000, + "margin_rate": 50, + "processing_cost": 10000, + "labor_cost": 5000, + "install_cost": 3000, + + "certification_number": "인정번호-001", + "certification_start_date": "2025-01-01", + "certification_end_date": "2028-01-01", + "specification_file": "/files/spec-001.pdf", + "specification_file_name": "시방서.pdf", + "certification_file": "/files/cert-001.pdf", + "certification_file_name": "인정서.pdf", + "note": "비고 내용", + + "current_revision": 0, + "is_final": false, + "finalized_date": null, + "finalized_by": null, + + "bom": [ + { + "id": 1, + "child_item_code": "KD-PT-001", + "child_item_name": "가이드레일(벽면형)", + "quantity": 2, + "unit": "EA", + "unit_price": 50000, + "quantity_formula": "H / 1000", + "note": "높이에 따라 수량 변동" + } + ], + + "revisions": [], + "created_at": "2025-01-10T00:00:00Z", + "updated_at": "2025-01-10T00:00:00Z", + "created_by": 1, + "updated_by": 1 + } +} +``` + +--- + +### API 3.2: 품목 생성 +``` +POST /api/v1/items +``` + +**Request Body**: +```json +{ + "item_type": "FG", + "item_name": "스크린 제품 신규", + "unit": "EA", + "specification": "2500x2500", + "is_active": true, + "product_category": "SCREEN", + "lot_abbreviation": "KD", + + "category1": "본체부품", + "category2": "가이드시스템", + + "purchase_price": 120000, + "sales_price": 180000, + + "certification_number": "인정번호-002", + "certification_start_date": "2025-01-01", + "certification_end_date": "2028-01-01", + "note": "신규 제품", + + "bom": [ + { + "child_item_id": 2, + "quantity": 2, + "unit": "EA", + "quantity_formula": "H / 1000", + "note": "높이에 따라 수량 변동" + } + ] +} +``` + +**Response**: +```json +{ + "success": true, + "data": { + "id": 10, + "item_code": "KD-FG-010", + "item_name": "스크린 제품 신규", + "message": "품목이 성공적으로 생성되었습니다." + } +} +``` + +--- + +### API 3.3: 품목 수정 + +> ⚠️ **기존 API 존재**: `ItemsController@update` - **ID 기반 경로** + +``` +PUT /api/v1/items/{id} +``` + +**Path Parameters**: +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| id | number | Y | 품목 ID | + +**Request Body**: (생성과 동일, 변경할 필드만 전송) +```json +{ + "item_name": "스크린 제품 A (수정)", + "sales_price": 160000, + "note": "가격 조정됨", + "bom": [ + { + "id": 1, + "quantity": 3 + }, + { + "child_item_id": 5, + "quantity": 10, + "unit": "EA" + } + ] +} +``` + +**Response**: +```json +{ + "success": true, + "data": { + "id": 1, + "item_code": "KD-FG-001", + "message": "품목이 성공적으로 수정되었습니다." + } +} +``` + +--- + +### API 3.4: 품목 삭제 + +> ⚠️ **기존 API 존재**: `ItemsController@destroy` - **ID 기반 경로** + +``` +DELETE /api/v1/items/{id} +``` + +**Path Parameters**: +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| id | number | Y | 품목 ID | + +**Response**: +```json +{ + "success": true, + "data": { + "message": "품목이 성공적으로 삭제되었습니다." + } +} +``` + +**Error Response** (사용 중인 품목): +```json +{ + "success": false, + "error": { + "code": "ITEM_IN_USE", + "message": "해당 품목은 다른 BOM에서 사용 중이므로 삭제할 수 없습니다.", + "details": { + "used_in": [ + {"item_code": "KD-FG-001", "item_name": "스크린 제품 A"} + ] + } + } +} +``` + +--- + +### API 3.5: 품목 일괄 삭제 + +> 🆕 **신규 API 요청** - ID 기반 일괄 삭제 + +``` +DELETE /api/v1/items/batch +``` + +**Request Body**: +```json +{ + "ids": [1, 2, 3] +} +``` + +**Response**: +```json +{ + "success": true, + "data": { + "deleted_count": 2, + "failed_count": 1, + "failed_items": [ + { + "id": 3, + "item_code": "KD-PT-003", + "reason": "BOM에서 사용 중" + } + ], + "message": "2개 품목이 삭제되었습니다. 1개 품목은 삭제할 수 없습니다." + } +} +``` + +--- + +## 4. 동적 폼 렌더링 API + +### API 4.1: 품목 유형별 폼 구조 조회 +``` +GET /api/v1/item-master/form-structure/{item_type} +``` + +**Path Parameters**: +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| item_type | string | Y | 품목 유형 (FG, PT, SM, RM, CS) | + +**Query Parameters**: +| 파라미터 | 타입 | 필수 | 기본값 | 설명 | +|----------|------|------|--------|------| +| include_conditions | boolean | N | true | 조건부 필드 포함 여부 | + +**Response**: +```json +{ + "success": true, + "data": { + "page": { + "id": 1, + "page_name": "제품 등록", + "item_type": "FG", + "description": "제품(완제품) 등록 페이지" + }, + "sections": [ + { + "id": 101, + "title": "기본 정보", + "section_type": "BASIC", + "order_no": 1, + "is_collapsible": false, + "is_default_open": true, + "fields": [ + { + "id": 1001, + "field_name": "품목명", + "field_key": "item_name", + "field_type": "textbox", + "order_no": 1, + "is_required": true, + "placeholder": "품목명을 입력하세요", + "validation_rules": { + "maxLength": 100 + }, + "grid_row": 1, + "grid_col": 1, + "grid_span": 2 + }, + { + "id": 1002, + "field_name": "품목 상태", + "field_key": "status", + "field_type": "dropdown", + "order_no": 2, + "is_required": true, + "default_value": "DEV", + "options": [ + {"label": "개발", "value": "DEV"}, + {"label": "양산", "value": "PROD"}, + {"label": "단종", "value": "EOL"} + ], + "grid_row": 2, + "grid_col": 1, + "grid_span": 1 + } + ] + }, + { + "id": 102, + "title": "인정 정보", + "section_type": "BASIC", + "order_no": 2, + "is_collapsible": true, + "is_default_open": false, + "fields": [ + { + "id": 1010, + "field_name": "인정번호", + "field_key": "certification_number", + "field_type": "textbox", + "order_no": 1, + "is_required": false + }, + { + "id": 1011, + "field_name": "시방서", + "field_key": "specification_file", + "field_type": "file", + "order_no": 2, + "is_required": false, + "component_type": "file-upload", + "properties": { + "accept": ".pdf,.doc,.docx", + "max_size_mb": 10 + } + } + ] + }, + { + "id": 103, + "title": "부품 구성 (BOM)", + "section_type": "BOM", + "order_no": 3, + "is_collapsible": true, + "is_default_open": false, + "bom_config": { + "columns": [ + {"key": "child_item_code", "label": "품목코드", "width": 120, "editable": false}, + {"key": "child_item_name", "label": "품목명", "width": 200, "editable": false}, + {"key": "quantity", "label": "수량", "width": 80, "type": "number", "editable": true}, + {"key": "unit", "label": "단위", "width": 60, "editable": false}, + {"key": "quantity_formula", "label": "수량식", "width": 120, "editable": true}, + {"key": "note", "label": "비고", "width": 150, "editable": true} + ], + "allow_search": true, + "search_endpoint": "/api/v1/items/search", + "searchable_item_types": ["PT", "SM", "RM"] + } + } + ], + "conditional_sections": [ + { + "condition": { + "field_key": "needs_bom", + "operator": "equals", + "value": true + }, + "show_sections": [103] + } + ] + } +} +``` + +--- + +### API 4.2: 부품(PT) 조건부 필드 조회 +``` +GET /api/v1/item-master/form-structure/PT/conditional +``` + +**Query Parameters**: +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| part_type | string | Y | 부품유형 (ASSEMBLY, BENDING, PURCHASED) | +| category1 | string | N | 대분류 값 | + +**Request Example**: +``` +GET /api/v1/item-master/form-structure/PT/conditional?part_type=BENDING&category1=가이드레일 +``` + +**Response**: +```json +{ + "success": true, + "data": { + "part_type": "BENDING", + "category1": "가이드레일", + "additional_sections": [ + { + "id": 201, + "title": "절곡품 정보", + "section_type": "CUSTOM", + "order_no": 2, + "fields": [ + { + "id": 2001, + "field_name": "재질", + "field_key": "material", + "field_type": "dropdown", + "is_required": true, + "options": [ + {"label": "EGI 1.55T", "value": "EGI_155"}, + {"label": "SUS 1.2T", "value": "SUS_12"}, + {"label": "SPCC 1.6T", "value": "SPCC_16"} + ] + }, + { + "id": 2002, + "field_name": "길이", + "field_key": "length", + "field_type": "dropdown", + "is_required": true, + "options": [ + {"label": "2438mm", "value": "2438"}, + {"label": "3000mm", "value": "3000"}, + {"label": "4000mm", "value": "4000"} + ] + } + ] + }, + { + "id": 202, + "title": "전개도", + "section_type": "CUSTOM", + "order_no": 3, + "fields": [ + { + "id": 2010, + "field_name": "전개도 이미지", + "field_key": "bending_diagram", + "field_type": "custom", + "component_type": "drawing-canvas", + "is_required": false, + "properties": { + "width": 800, + "height": 400, + "tools": ["pen", "line", "eraser", "text"] + } + }, + { + "id": 2011, + "field_name": "전개도 상세", + "field_key": "bending_details", + "field_type": "custom", + "component_type": "bending-detail-table", + "is_required": false + } + ] + } + ] + } +} +``` + +--- + +## 5. BOM 관리 API + +### API 5.1: 품목 BOM 조회 +``` +GET /api/v1/items/{id}/bom +``` + +**Response**: +```json +{ + "success": true, + "data": { + "item_id": 1, + "item_code": "KD-FG-001", + "item_name": "스크린 제품 A", + "bom_lines": [ + { + "id": 1, + "child_item_id": 2, + "child_item_code": "KD-PT-001", + "child_item_name": "가이드레일(벽면형)", + "quantity": 2, + "unit": "EA", + "unit_price": 50000, + "total_price": 100000, + "quantity_formula": "H / 1000", + "note": "높이에 따라 수량 변동", + "order_no": 1 + } + ], + "total_cost": 250000 + } +} +``` + +--- + +### API 5.2: BOM 항목 추가 +``` +POST /api/v1/items/{id}/bom +``` + +**Request Body**: +```json +{ + "child_item_id": 5, + "quantity": 10, + "unit": "EA", + "quantity_formula": null, + "note": "추가 부품" +} +``` + +--- + +### API 5.3: BOM 항목 수정 +``` +PUT /api/v1/items/{item_id}/bom/{bom_id} +``` + +**Request Body**: +```json +{ + "quantity": 15, + "note": "수량 증가" +} +``` + +--- + +### API 5.4: BOM 항목 삭제 +``` +DELETE /api/v1/items/{item_id}/bom/{bom_id} +``` + +--- + +## 6. 파일 관리 API + +### API 6.1: 파일 업로드 +``` +POST /api/v1/files/upload +``` + +**Content-Type**: `multipart/form-data` + +**Form Data**: +| 필드 | 타입 | 필수 | 설명 | +|------|------|------|------| +| file | File | Y | 업로드할 파일 | +| type | string | Y | 파일 유형 (specification, certification, bending_diagram) | +| item_id | number | N | 연결할 품목 ID (수정 시) | + +**Response**: +```json +{ + "success": true, + "data": { + "file_id": "uuid-1234-5678", + "file_name": "시방서.pdf", + "file_url": "/files/uuid-1234-5678/시방서.pdf", + "file_size": 1024000, + "mime_type": "application/pdf" + } +} +``` + +--- + +### API 6.2: 파일 다운로드 +``` +GET /api/v1/files/{file_id}/download +``` + +--- + +### API 6.3: 파일 삭제 +``` +DELETE /api/v1/files/{file_id} +``` + +--- + +## 7. 마스터 데이터 조회 API + +### API 7.1: 단위 목록 조회 +``` +GET /api/v1/master/units +``` + +**Response**: +```json +{ + "success": true, + "data": [ + {"value": "EA", "label": "EA (개)"}, + {"value": "SET", "label": "SET (세트)"}, + {"value": "M", "label": "M (미터)"}, + {"value": "KG", "label": "KG (킬로그램)"}, + {"value": "L", "label": "L (리터)"} + ] +} +``` + +--- + +### API 7.2: 카테고리 목록 조회 +``` +GET /api/v1/master/categories +``` + +**Query Parameters**: +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| item_type | string | N | 품목유형별 필터 | +| parent_id | number | N | 상위 카테고리 ID | + +--- + +### API 7.3: 재질 목록 조회 +``` +GET /api/v1/master/materials +``` + +**Response**: +```json +{ + "success": true, + "data": [ + {"value": "EGI_155", "label": "EGI 1.55T", "thickness": "1.55"}, + {"value": "SUS_12", "label": "SUS 1.2T", "thickness": "1.2"}, + {"value": "SPCC_16", "label": "SPCC 1.6T", "thickness": "1.6"} + ] +} +``` + +--- + +### API 7.4: 규격 목록 조회 (원자재/부자재) +``` +GET /api/v1/master/specifications +``` + +**Query Parameters**: +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| item_type | string | Y | RM 또는 SM | +| item_name | string | N | 품목명 필터 (예: SPHC-SD) | + +--- + +## 8. API 요약 테이블 + +### 8.1 필수 API (MVP) +| 우선순위 | 메서드 | 엔드포인트 | 설명 | 상태 | +|----------|--------|------------|------|------| +| 🔴 P0 | GET | `/api/v1/items` | 품목 목록 조회 (페이징) | ⚠️ 기존 | +| 🔴 P0 | GET | `/api/v1/items/{id}` | 품목 단건 조회 (ID) | ⚠️ 기존 | +| 🔴 P0 | GET | `/api/v1/items/code/{code}` | 품목 단건 조회 (코드) | ⚠️ 기존 | +| 🔴 P0 | POST | `/api/v1/items` | 품목 생성 | ⚠️ 기존 | +| 🔴 P0 | PUT | `/api/v1/items/{id}` | 품목 수정 (ID 기반) | ⚠️ 기존 | +| 🔴 P0 | DELETE | `/api/v1/items/{id}` | 품목 삭제 (ID 기반) | ⚠️ 기존 | +| 🔴 P0 | GET | `/api/v1/items/search` | 품목 검색 (BOM용) | 🆕 신규 | + +### 8.2 중요 API +| 우선순위 | 메서드 | 엔드포인트 | 설명 | +|----------|--------|------------|------| +| 🟡 P1 | GET | `/api/v1/item-master/form-structure/{type}` | 동적 폼 구조 조회 | +| 🟡 P1 | GET | `/api/v1/items/stats` | 품목 통계 | +| 🟡 P1 | DELETE | `/api/v1/items/batch` | 품목 일괄 삭제 | +| 🟡 P1 | POST | `/api/v1/files/upload` | 파일 업로드 | + +### 8.3 추가 API +| 우선순위 | 메서드 | 엔드포인트 | 설명 | +|----------|--------|------------|------| +| 🟢 P2 | GET | `/api/v1/item-master/form-structure/PT/conditional` | PT 조건부 필드 | +| 🟢 P2 | GET | `/api/v1/items/{id}/bom` | BOM 조회 | +| 🟢 P2 | POST | `/api/v1/items/{id}/bom` | BOM 항목 추가 | +| 🟢 P2 | PUT | `/api/v1/items/{id}/bom/{bom_id}` | BOM 항목 수정 | +| 🟢 P2 | DELETE | `/api/v1/items/{id}/bom/{bom_id}` | BOM 항목 삭제 | +| 🟢 P2 | GET | `/api/v1/master/units` | 단위 목록 | +| 🟢 P2 | GET | `/api/v1/master/categories` | 카테고리 목록 | +| 🟢 P2 | GET | `/api/v1/master/materials` | 재질 목록 | +| 🟢 P2 | GET | `/api/v1/master/specifications` | 규격 목록 | +| 🟢 P2 | GET | `/api/v1/files/{id}/download` | 파일 다운로드 | +| 🟢 P2 | DELETE | `/api/v1/files/{id}` | 파일 삭제 | + +--- + +## 9. 에러 응답 형식 + +### 공통 에러 응답 +```json +{ + "success": false, + "error": { + "code": "ERROR_CODE", + "message": "사용자 친화적 에러 메시지", + "details": {} + } +} +``` + +### 에러 코드 목록 +| 코드 | HTTP Status | 설명 | +|------|-------------|------| +| VALIDATION_ERROR | 400 | 입력값 검증 실패 | +| ITEM_NOT_FOUND | 404 | 품목을 찾을 수 없음 | +| ITEM_IN_USE | 409 | 품목이 다른 곳에서 사용 중 | +| DUPLICATE_ITEM_CODE | 409 | 중복된 품목코드 | +| FILE_TOO_LARGE | 413 | 파일 크기 초과 | +| UNAUTHORIZED | 401 | 인증 필요 | +| FORBIDDEN | 403 | 권한 없음 | +| INTERNAL_ERROR | 500 | 서버 내부 오류 | + +--- + +## 10. 데이터 모델 참조 + +### 10.1 품목(Item) 테이블 필드 +```sql +CREATE TABLE items ( + id SERIAL PRIMARY KEY, + tenant_id INTEGER NOT NULL, + item_code VARCHAR(50) UNIQUE NOT NULL, + item_name VARCHAR(200) NOT NULL, + item_type VARCHAR(10) NOT NULL, -- FG, PT, SM, RM, CS + unit VARCHAR(20) NOT NULL, + specification VARCHAR(200), + is_active BOOLEAN DEFAULT true, + + -- 제품(FG) 전용 + product_category VARCHAR(20), -- SCREEN, STEEL + lot_abbreviation VARCHAR(10), + certification_number VARCHAR(100), + certification_start_date DATE, + certification_end_date DATE, + specification_file VARCHAR(500), + specification_file_name VARCHAR(200), + certification_file VARCHAR(500), + certification_file_name VARCHAR(200), + + -- 부품(PT) 전용 + part_type VARCHAR(20), -- ASSEMBLY, BENDING, PURCHASED + part_usage VARCHAR(50), + installation_type VARCHAR(50), + assembly_type VARCHAR(10), + assembly_length VARCHAR(20), + side_spec_width VARCHAR(20), + side_spec_height VARCHAR(20), + material VARCHAR(50), + length VARCHAR(20), + bending_diagram TEXT, -- Base64 이미지 + bending_details JSONB, -- 전개도 상세 데이터 + + -- 분류 + category1 VARCHAR(100), + category2 VARCHAR(100), + category3 VARCHAR(100), + + -- 가격 + purchase_price DECIMAL(15,2), + sales_price DECIMAL(15,2), + margin_rate DECIMAL(5,2), + processing_cost DECIMAL(15,2), + labor_cost DECIMAL(15,2), + install_cost DECIMAL(15,2), + + -- 재고 + safety_stock INTEGER, + lead_time INTEGER, + + -- 버전 관리 + current_revision INTEGER DEFAULT 0, + is_final BOOLEAN DEFAULT false, + finalized_date TIMESTAMP, + finalized_by INTEGER, + + note TEXT, + created_by INTEGER, + updated_by INTEGER, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +``` + +### 10.2 BOM 테이블 +```sql +CREATE TABLE item_bom ( + id SERIAL PRIMARY KEY, + tenant_id INTEGER NOT NULL, + parent_item_id INTEGER REFERENCES items(id), + child_item_id INTEGER REFERENCES items(id), + quantity DECIMAL(10,3) NOT NULL, + unit VARCHAR(20), + unit_price DECIMAL(15,2), + quantity_formula VARCHAR(100), -- 예: "H / 1000" + note TEXT, + order_no INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +``` + +--- + +## 11. 참고 문서 + +- `src/components/items/ItemListClient.tsx` - 품목 목록 UI +- `src/components/items/ItemForm/index.tsx` - 품목 등록/수정 폼 +- `src/contexts/ItemMasterContext.tsx` - 품목기준관리 데이터 구조 +- `[API-2025-11-25] item-master-data-management-api-request.md` - 품목기준관리 API + +--- + +## 변경 이력 + +| 날짜 | 버전 | 변경 내용 | +|------|------|----------| +| 2025-11-28 | 1.0 | 초안 작성 (동적 렌더링만) | +| 2025-11-28 | 2.0 | **전체 CRUD + 리스트 + 페이징 + BOM + 파일 + 마스터 데이터 추가** | +| 2025-11-28 | 3.0 | **백엔드 스키마/기존 API 기반 재검토**: `/{id}` → `/{code}` 경로 변경, `per_page` → `size` 파라미터 변경, 기존 API 주석 추가, 일괄삭제 코드 기반 변경 | +| 2025-11-28 | 3.1 | **ID 기반으로 통일** (백엔드 결정): PUT/DELETE `/{code}` → `/{id}` 롤백, 일괄삭제 `codes` → `ids` 변경, 문서 전체 정합성 검토 완료 | \ No newline at end of file diff --git a/claudedocs/item-master/[IMPL-2025-11-27] realtime-sync-fixes.md b/claudedocs/item-master/[IMPL-2025-11-27] realtime-sync-fixes.md index 5a396ac1..6f88d504 100644 --- a/claudedocs/item-master/[IMPL-2025-11-27] realtime-sync-fixes.md +++ b/claudedocs/item-master/[IMPL-2025-11-27] realtime-sync-fixes.md @@ -171,11 +171,84 @@ console.log('[ItemMasterDataManagement] 📋 sectionsAsTemplates changed:', {... --- +### 4. 페이지 삭제 시 섹션 동기화 (`ItemMasterContext.tsx`) - 2025-11-28 + +**문제**: 계층구조에서 페이지 삭제 시 연결된 섹션들이 섹션탭에서 사라짐 + +**예상 동작**: 페이지 삭제 → 연결된 섹션은 unlink만 되고 독립 섹션으로 복귀 → 섹션탭에서 표시 + +**실제 동작**: 페이지 삭제 → 섹션이 UI에서 완전히 사라짐 (삭제된 것처럼 보임) + +**원인 분석**: +1. **백엔드는 정상**: `ItemPageService.php`의 `destroy()` 메서드가 독립 엔티티 아키텍처 준수 + - `entity_relationships`에서 페이지-섹션 관계만 삭제 (섹션 자체는 유지) + - 섹션/필드는 독립 엔티티로 계속 존재 +2. **프론트엔드 상태 동기화 누락**: + - `deleteItemPage`가 `setItemPages`만 업데이트 + - `independentSections` 상태는 갱신 안 됨 + - `sectionsAsTemplates` useMemo가 `[itemPages, independentSections]` 의존 + - 결과: 섹션이 `itemPages`에서 제거되었지만 `independentSections`에도 없어서 UI에서 사라짐 + +**해결**: 페이지 삭제 후 `refreshIndependentSections()` 호출 추가 + +```typescript +// deleteItemPage 함수 수정 +const deleteItemPage = async (id: number) => { + try { + const response = await itemMasterApi.pages.delete(id); + + if (!response.success) { + throw new Error(response.message || '페이지 삭제 실패'); + } + + // state 업데이트 + setItemPages(prev => prev.filter(page => page.id !== id)); + + // 2025-11-28: 페이지 삭제 후 독립 섹션 목록 갱신 + // 백엔드에서 섹션은 삭제되지 않고 연결만 해제되므로 (독립 엔티티 아키텍처) + // 독립 섹션 목록을 새로고침해야 섹션 탭에서 해당 섹션이 표시됨 + try { + await refreshIndependentSections(); + console.log('[ItemMasterContext] 페이지 삭제 후 독립 섹션 갱신 완료'); + } catch (refreshError) { + // 갱신 실패해도 페이지 삭제는 성공한 상태이므로 경고만 출력 + console.warn('[ItemMasterContext] 독립 섹션 갱신 실패:', refreshError); + } + + console.log('[ItemMasterContext] 페이지 삭제 성공:', id); + } catch (error) { + // ... + } +}; +``` + +**백엔드 코드 참조** (`ItemPageService.php:95-129`): +```php +/** + * 페이지 삭제 (Soft Delete) + * 독립 엔티티 아키텍처: 페이지만 삭제하고 연결된 섹션/필드는 unlink만 수행 + */ +public function destroy(int $id): void +{ + // 1. entity_relationships에서 이 페이지의 모든 자식 관계 해제 + EntityRelationship::where('parent_type', EntityRelationship::TYPE_PAGE) + ->where('parent_id', $id) + ->where('is_locked', false) + ->delete(); + + // 2. 페이지만 Soft Delete (섹션/필드는 독립 엔티티로 유지) + $page->update(['deleted_by' => $userId]); + $page->delete(); +} +``` + +--- + ## 수정된 파일 목록 | 파일 | 수정 내용 | |---|---| -| `src/contexts/ItemMasterContext.tsx` | BOM 동기화, 섹션 복제 수정 | +| `src/contexts/ItemMasterContext.tsx` | BOM 동기화, 섹션 복제 수정, **페이지 삭제 시 독립 섹션 갱신 (2025-11-28)** | | `src/components/items/ItemMasterDataManagement/hooks/useTemplateManagement.ts` | 항목 수정 로직 추가 | | `src/components/items/ItemMasterDataManagement/dialogs/TemplateFieldDialog.tsx` | prop 타입 수정 (void → void \| Promise) | @@ -185,4 +258,8 @@ console.log('[ItemMasterDataManagement] 📋 sectionsAsTemplates changed:', {... 1. **양방향 동기화 필수**: 데이터 변경 시 `itemPages`와 `independentSections` 모두 업데이트해야 함 2. **null vs undefined**: API 응답의 null 값이 transformer 거치면서 undefined로 바뀔 수 있음 → `== null` 사용 권장 -3. **useMemo 의존성**: `sectionsAsTemplates`의 의존성 배열 확인 → 두 상태가 모두 업데이트되어야 재계산됨 \ No newline at end of file +3. **useMemo 의존성**: `sectionsAsTemplates`의 의존성 배열 확인 → 두 상태가 모두 업데이트되어야 재계산됨 +4. **독립 엔티티 아키텍처 인지**: 페이지/섹션/필드는 각각 독립 엔티티이며 `entity_relationships`로 연결됨 + - 페이지 삭제 시 → 섹션은 삭제 안 됨 (연결만 해제) → `refreshIndependentSections()` 필요 + - 섹션 삭제 시 → 필드는 삭제 안 됨 (연결만 해제) → `refreshIndependentFields()` 필요 (해당 시) + - 프론트엔드에서 연결 해제된 엔티티들이 적절히 표시되도록 상태 갱신 필수 \ No newline at end of file diff --git a/claudedocs/item-master/[PLAN-2025-11-27] item-form-component-separation.md b/claudedocs/item-master/[PLAN-2025-11-27] item-form-component-separation.md index 67d7874c..d973f114 100644 --- a/claudedocs/item-master/[PLAN-2025-11-27] item-form-component-separation.md +++ b/claudedocs/item-master/[PLAN-2025-11-27] item-form-component-separation.md @@ -1,6 +1,14 @@ # ItemForm.tsx 컴포넌트 분리 계획 -## 작업 일자: 2025-11-27 (분석) +## 작업 일자: 2025-11-27 (분석) → 2025-11-28 (완료) + +## ✅ 최종 결과 요약 + +| 항목 | Before | After | 감소율 | +|------|--------|-------|--------| +| **index.tsx** | 1,607줄 | **415줄** | **74%** | +| **useState 수** | 25개+ | **0개** (훅으로 이동) | **100%** | +| **파일 수** | 1개 | **21개** | 모듈화 완료 | --- @@ -264,13 +272,55 @@ src/components/items/ItemForm/ ### Phase 5: 훅 & 컨텍스트 ✅ 완료 (2025-11-27) - [x] context/ItemFormContext.tsx 생성 (~80줄) - [x] context/index.ts export 파일 생성 -- [x] hooks/useItemFormState.ts 생성 (~280줄) - 25+ useState 통합 +- [x] hooks/useItemFormState.ts 생성 (~364줄) - 25+ useState 통합 - [x] hooks/useBOMManagement.ts 생성 (~180줄) - BOM 라인 관리 - [x] hooks/useBendingDetails.ts 생성 (~150줄) - 전개도 계산 - [x] hooks/index.ts export 파일 생성 -### Phase 6: 테스트 & 검증 +### Phase 6: index.tsx에 훅 적용 ✅ 완료 (2025-11-28) +- [x] useItemFormState 훅을 index.tsx에 적용 +- [x] 25+ useState → 훅에서 구조 분해 할당 +- [x] handleItemTypeChange → resetAllStates 헬퍼 함수 활용 +- [x] 미사용 import 정리 +- [x] 빌드 테스트 통과 + +### Phase 7: 테스트 & 검증 (추후 진행) - [ ] 모든 품목 유형 등록 테스트 - [ ] 수정 모드 테스트 - [ ] 폼 검증 테스트 -- [ ] BOM 추가/삭제 테스트 \ No newline at end of file +- [ ] BOM 추가/삭제 테스트 + +--- + +## 9. 최종 파일 구조 + +``` +src/components/items/ItemForm/ +├── index.tsx (415줄) ← 1,607줄에서 74% 감소 +├── constants.ts (72줄) +├── types.ts (50줄) +├── ValidationAlert.tsx (45줄) +├── FormHeader.tsx (63줄) +├── BendingDiagramSection.tsx (~300줄) +├── BOMSection.tsx (~280줄) +├── context/ +│ ├── ItemFormContext.tsx (~80줄) +│ └── index.ts +├── hooks/ +│ ├── useItemFormState.ts (364줄) ← 상태 관리 통합 +│ ├── useBOMManagement.ts (~180줄) +│ ├── useBendingDetails.ts (~150줄) +│ └── index.ts +└── forms/ + ├── index.ts + ├── ProductForm.tsx (~120줄) + ├── MaterialForm.tsx (~350줄) + ├── PartForm.tsx (~273줄) + └── parts/ + ├── index.ts + ├── AssemblyPartForm.tsx (~300줄) + ├── BendingPartForm.tsx (~280줄) + └── PurchasedPartForm.tsx (~270줄) +``` + +**총 21개 파일로 모듈화 완료** \ No newline at end of file diff --git a/src/app/[locale]/(protected)/loading.tsx b/src/app/[locale]/(protected)/loading.tsx index cf5bdcf0..6266aa94 100644 --- a/src/app/[locale]/(protected)/loading.tsx +++ b/src/app/[locale]/(protected)/loading.tsx @@ -1,4 +1,4 @@ -import { Loader2 } from 'lucide-react'; +import { PageLoadingSpinner } from '@/components/ui/loading-spinner'; /** * Protected Group Loading UI @@ -7,20 +7,13 @@ import { Loader2 } from 'lucide-react'; * - DashboardLayout 내에서 표시됨 (사이드바, 헤더 유지) * - React Suspense 자동 적용 * - 페이지 전환 시 즉각적인 피드백 + * - 대시보드 스타일로 통일 */ export default function ProtectedLoading() { return ( -
-
-
-
- -
-
-

페이지를 불러오는 중...

-

잠시만 기다려주세요

-
-
-
+ ); } \ No newline at end of file diff --git a/src/components/auth/LoginPage.tsx b/src/components/auth/LoginPage.tsx index b1762bd2..94bc37f6 100644 --- a/src/components/auth/LoginPage.tsx +++ b/src/components/auth/LoginPage.tsx @@ -279,7 +279,7 @@ export function LoginPage() { > {isLoggingIn ? ( <> -
+
{t('loggingIn') || '로그인 중...'} ) : ( diff --git a/src/components/auth/SignupPage.tsx b/src/components/auth/SignupPage.tsx index 3a9d2ae5..c7f33c2d 100644 --- a/src/components/auth/SignupPage.tsx +++ b/src/components/auth/SignupPage.tsx @@ -258,9 +258,9 @@ export function SignupPage() { if (isChecking) { return (
-
-
-

Loading...

+
+
+

불러오는 중...

); diff --git a/src/components/business/Dashboard.tsx b/src/components/business/Dashboard.tsx index 01cd6739..887277f1 100644 --- a/src/components/business/Dashboard.tsx +++ b/src/components/business/Dashboard.tsx @@ -2,6 +2,7 @@ import { Suspense } from "react"; import { MainDashboard } from "./MainDashboard"; +import { PageLoadingSpinner } from "@/components/ui/loading-spinner"; /** * Dashboard - 통합 대시보드 컴포넌트 @@ -14,20 +15,10 @@ import { MainDashboard } from "./MainDashboard"; * - 권한 제어: 백엔드에서 역할에 따라 데이터 제한 */ -// 공통 로딩 컴포넌트 -const DashboardLoading = () => ( -
-
-
-

대시보드를 불러오는 중...

-
-
-); - export function Dashboard() { console.log('🎨 Dashboard component rendering...'); return ( - }> + }> ); diff --git a/src/components/items/ItemForm/index.tsx b/src/components/items/ItemForm/index.tsx index 472b5a2f..25d302d7 100644 --- a/src/components/items/ItemForm/index.tsx +++ b/src/components/items/ItemForm/index.tsx @@ -6,122 +6,90 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import type { ItemMaster, ItemType, BendingDetail, BOMLine } from '@/types/item'; +import type { ItemType } from '@/types/item'; import { createItemFormSchema, type CreateItemFormData } from '@/lib/utils/validation'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Checkbox } from '@/components/ui/checkbox'; -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 { X } from 'lucide-react'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; import { Alert, AlertDescription } from '@/components/ui/alert'; import ItemTypeSelect from '../ItemTypeSelect'; -import FileUpload from '../FileUpload'; import { DrawingCanvas } from '../DrawingCanvas'; // Local imports -import { PART_TYPE_CATEGORIES, PART_ITEM_NAMES, FIELD_NAME_MAP } from './constants'; -import type { ItemFormProps, BOMSearchState } from './types'; +import { PART_TYPE_CATEGORIES, PART_ITEM_NAMES } from './constants'; +import type { ItemFormProps } from './types'; import ValidationAlert from './ValidationAlert'; import FormHeader from './FormHeader'; import BendingDiagramSection from './BendingDiagramSection'; import BOMSection from './BOMSection'; import { MaterialForm, ProductForm, ProductCertificationSection, PartForm } from './forms'; +import { useItemFormState } from './hooks'; export default function ItemForm({ mode, initialData, onSubmit }: ItemFormProps) { const router = useRouter(); - const [isSubmitting, setIsSubmitting] = useState(false); - const [selectedItemType, setSelectedItemType] = useState( - mode === 'edit' ? (initialData?.itemType || 'FG') : '' - ); - const [bomLines, setBomLines] = useState(initialData?.bom || []); - const [bomSearchStates, setBomSearchStates] = useState>({}); - // 파일 상태 - const [specificationFile, setSpecificationFile] = useState(null); - const [certificationFile, setCertificationFile] = useState(null); - const [_bendingDiagramFile, setBendingDiagramFile] = useState(null); - const [bendingDiagram, setBendingDiagram] = useState(initialData?.bendingDiagram || ''); - const [bendingDiagramInputMethod, setBendingDiagramInputMethod] = useState<'file' | 'drawing'>('file'); - const [isDrawingOpen, setIsDrawingOpen] = useState(false); - - // FG(제품) 상태 - const [productName, setProductName] = useState(initialData?.itemName || ''); - const [productStatus, setProductStatus] = useState( - initialData?.isActive !== undefined ? String(initialData.isActive) : 'true' - ); - - // PT(부품) 상태 - const [selectedPartType, setSelectedPartType] = useState(initialData?.partType || ''); - const [partStatus, setPartStatus] = useState( - initialData?.isActive !== undefined ? String(initialData.isActive) : 'true' - ); - - // SM/RM/CS 상태 - const [itemName, setItemName] = useState(initialData?.itemName || ''); - const [selectedCategory1, setSelectedCategory1] = useState(initialData?.category1 || ''); - const [selectedInstallationType, setSelectedInstallationType] = useState( - initialData?.installationType || '' - ); - const [materialStatus, setMaterialStatus] = useState( - initialData?.isActive !== undefined ? String(initialData.isActive) : 'true' - ); - const [selectedSpecification, setSelectedSpecification] = useState(initialData?.specification || ''); - - // ASSEMBLY 부품 상태 - const [sideSpecWidth, setSideSpecWidth] = useState(initialData?.sideSpecWidth || ''); - const [sideSpecHeight, setSideSpecHeight] = useState(initialData?.sideSpecHeight || ''); - const [assemblyLength, setAssemblyLength] = useState(initialData?.assemblyLength || ''); - const [assemblyUnit, setAssemblyUnit] = useState(initialData?.unit || 'EA'); - - // 전동개폐기 상태 - const [electricOpenerPower, setElectricOpenerPower] = useState(''); - const [electricOpenerCapacity, setElectricOpenerCapacity] = useState(''); - - // 모터/체인 상태 - const [motorVoltage, setMotorVoltage] = useState(''); - const [chainSpec, setChainSpec] = useState(''); - - // BENDING 부품 상태 - const [selectedBendingItemType, setSelectedBendingItemType] = useState(''); - const [material, setMaterial] = useState(initialData?.material || ''); - const [bendingLength, setBendingLength] = useState(initialData?.bendingLength || ''); - const [widthSum, setWidthSum] = useState(initialData?.length || ''); - const [partUnit, setPartUnit] = useState(initialData?.unit || 'EA'); - - // BENDING 전개도 상세 입력 (치수 계산) - const [bendingDetails, setBendingDetails] = useState( - initialData?.bendingDetails || [] - ); - - // 단위 상태 (RM/SM/CS 공통) - const [selectedUnit, setSelectedUnit] = useState(initialData?.unit || ''); - - // 기타 정보 상태 - const [_otherInfoStatus, _setOtherInfoStatus] = useState('true'); - - // BOM 필요 여부 - const [needsBOM, setNeedsBOM] = useState(false); - - // 비고 (FG 전용) - const [remarks, setRemarks] = useState(initialData?.note || ''); + // 커스텀 훅으로 상태 관리 통합 + const { + // 기본 상태 + isSubmitting, setIsSubmitting, + selectedItemType, setSelectedItemType, + // BOM 상태 + bomLines, setBomLines, + bomSearchStates, setBomSearchStates, + // 파일 상태 + specificationFile, setSpecificationFile, + certificationFile, setCertificationFile, + setBendingDiagramFile, + bendingDiagram, setBendingDiagram, + bendingDiagramInputMethod, setBendingDiagramInputMethod, + isDrawingOpen, setIsDrawingOpen, + // FG(제품) 상태 + productName, setProductName, + productStatus, setProductStatus, + // PT(부품) 상태 + selectedPartType, setSelectedPartType, + partStatus, setPartStatus, + // SM/RM/CS 상태 + itemName, setItemName, + selectedCategory1, setSelectedCategory1, + selectedInstallationType, setSelectedInstallationType, + materialStatus, setMaterialStatus, + selectedSpecification, setSelectedSpecification, + selectedUnit, setSelectedUnit, + // ASSEMBLY 부품 상태 + sideSpecWidth, setSideSpecWidth, + sideSpecHeight, setSideSpecHeight, + assemblyLength, setAssemblyLength, + assemblyUnit, setAssemblyUnit, + // 전동개폐기 상태 + electricOpenerPower, setElectricOpenerPower, + electricOpenerCapacity, setElectricOpenerCapacity, + // 모터/체인 상태 + motorVoltage, setMotorVoltage, + chainSpec, setChainSpec, + // BENDING 부품 상태 + selectedBendingItemType, setSelectedBendingItemType, + material, setMaterial, + bendingLength, setBendingLength, + widthSum, setWidthSum, + partUnit, setPartUnit, + bendingDetails, setBendingDetails, + // BOM 필요 여부 + needsBOM, setNeedsBOM, + // 비고 + remarks, setRemarks, + // 헬퍼 함수 + resetAllStates, + } = useItemFormState({ mode, initialData }); const { register, @@ -130,8 +98,8 @@ export default function ItemForm({ mode, initialData, onSubmit }: ItemFormProps) setValue, getValues, clearErrors, - // eslint-disable-next-line @typescript-eslint/no-explicit-any } = useForm({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any resolver: zodResolver(createItemFormSchema) as any, defaultValues: initialData || { itemType: 'FG', @@ -152,87 +120,59 @@ export default function ItemForm({ mode, initialData, onSubmit }: ItemFormProps) setWidthSum(totalSum.toFixed(1)); setValue('length', totalSum.toFixed(1)); } - }, [bendingDetails, setValue]); + }, [bendingDetails, setValue, setWidthSum]); // 품목코드 자동 생성 함수 const generateItemCode = () => { - // 절곡 부품인 경우 if (selectedPartType === "BENDING" && selectedCategory1 && selectedBendingItemType) { - // 품목명 카테고리 코드 가져오기 const categoryData = PART_TYPE_CATEGORIES.BENDING.categories.find( cat => cat.value === selectedCategory1 ); const categoryCode = categoryData?.code || ''; - - // 종류 코드 가져오기 (selectedBendingItemType은 label 값이므로 label로 비교) const itemTypeData = PART_ITEM_NAMES[selectedCategory1]?.find( item => item.label === selectedBendingItemType ); const itemTypeCode = itemTypeData?.code || ''; - // 길이 코드 계산 let lengthCode = ""; if (bendingLength) { - // W50x3000 형식 처리 if (bendingLength.startsWith("W")) { if (bendingLength === "W50x3000") lengthCode = "53"; else if (bendingLength === "W50x4000") lengthCode = "54"; else if (bendingLength === "W80x3000") lengthCode = "83"; else if (bendingLength === "W80x4000") lengthCode = "84"; else { - // 파싱: W50x3000 -> 50, 3000 const match = bendingLength.match(/W(\d+)x(\d+)/); if (match) { const width = parseInt(match[1]); const length = parseInt(match[2]); - const widthDigit = Math.floor(width / 10); - const lengthDigit = Math.floor(length / 1000); - lengthCode = `${widthDigit}${lengthDigit}`; + lengthCode = `${Math.floor(width / 10)}${Math.floor(length / 1000)}`; } } } else { - // 숫자만 있는 형식 (길이만) const lengthNum = parseInt(bendingLength); - if (lengthNum === 1219) lengthCode = "12"; - else if (lengthNum === 2438) lengthCode = "24"; - else if (lengthNum === 3000) lengthCode = "30"; - else if (lengthNum === 3500) lengthCode = "35"; - else if (lengthNum === 4000) lengthCode = "40"; - else if (lengthNum === 4150) lengthCode = "41"; - else if (lengthNum === 4200) lengthCode = "42"; - else if (lengthNum === 4300) lengthCode = "43"; - else { - lengthCode = Math.floor(lengthNum / 100).toString().padStart(2, '0'); - } + const lengthMap: Record = { + 1219: "12", 2438: "24", 3000: "30", 3500: "35", + 4000: "40", 4150: "41", 4200: "42", 4300: "43" + }; + lengthCode = lengthMap[lengthNum] || Math.floor(lengthNum / 100).toString().padStart(2, '0'); } } - - // 형식: 품목명코드 + 종류코드 + 길이코드 (예: RC24, RM12, CF30, BS24) return `${categoryCode}${itemTypeCode}${lengthCode}`; } - return ""; }; const handleFormSubmit = async (data: CreateItemFormData) => { setIsSubmitting(true); try { - // BOM 데이터, 절곡 상세, 파일 포함 const finalData = { ...data, bom: bomLines.length > 0 ? bomLines : undefined, bendingDetails: bendingDetails.length > 0 ? bendingDetails : undefined, - // 파일은 실제 구현 시 FormData로 전송하거나 Base64로 인코딩 - // 현재는 파일명만 저장 (실제 업로드는 API 연동 시 구현) specificationFileName: specificationFile?.name, certificationFileName: certificationFile?.name, }; - - // TODO: 실제 구현 시 파일 업로드 처리 - // const formData = new FormData(); - // if (specificationFile) formData.append('specificationFile', specificationFile); - // if (certificationFile) formData.append('certificationFile', certificationFile); - await onSubmit(finalData); router.push('/items'); router.refresh(); @@ -246,72 +186,7 @@ export default function ItemForm({ mode, initialData, onSubmit }: ItemFormProps) const handleItemTypeChange = (type: ItemType) => { setSelectedItemType(type); setValue('itemType', type); - - // 품목 유형 변경 시 모든 입력 필드 초기화 - // FG(제품) 상태 초기화 - setProductName(''); - setProductStatus('true'); - - // PT(부품) 상태 초기화 - setSelectedPartType(''); - setPartStatus('true'); - - // SM/RM/CS 상태 초기화 - setItemName(''); - setSelectedCategory1(''); - setSelectedInstallationType(''); - setMaterialStatus('true'); - setSelectedSpecification(''); - setSelectedUnit(''); - - // ASSEMBLY 부품 상태 초기화 - setSideSpecWidth(''); - setSideSpecHeight(''); - setAssemblyLength(''); - setAssemblyUnit('EA'); - - // 전동개폐기 상태 초기화 - setElectricOpenerPower(''); - setElectricOpenerCapacity(''); - - // 모터/체인 상태 초기화 - setMotorVoltage(''); - setChainSpec(''); - - // BENDING 부품 상태 초기화 - setSelectedBendingItemType(''); - setMaterial(''); - setBendingLength(''); - setWidthSum(''); - setPartUnit('EA'); - - // 기타 정보 상태 초기화 - _setOtherInfoStatus('true'); - - // BOM 및 파일 초기화 - setNeedsBOM(false); - setBomLines([]); - setSpecificationFile(null); - setCertificationFile(null); - setBendingDiagramFile(null); - setBendingDiagram(''); - - // react-hook-form 필드 초기화 (itemType 제외) - setValue('itemCode', ''); - setValue('itemName', ''); - // SM/RM/CS는 unit 필수이므로 빈 문자열로 초기화, FG/PT는 'EA' - setValue('unit', (type === 'SM' || type === 'RM' || type === 'CS') ? '' : 'EA'); - setValue('specification', ''); - setValue('purchasePrice', 0); - setValue('salesPrice', 0); - setValue('processingCost', 0); - setValue('laborCost', 0); - setValue('installCost', 0); - setValue('isActive', true); - // needsBOM은 스키마 필드가 아닌 로컬 상태로 관리됨 - - // 검증 에러 초기화 (에러 카드 숨김) - clearErrors(); + resetAllStates(setValue, clearErrors, type); }; return ( @@ -380,1136 +255,69 @@ export default function ItemForm({ mode, initialData, onSubmit }: ItemFormProps) {/* 부품(PT)인 경우 */} {selectedItemType === 'PT' && ( - <> - {/* 부품 유형 - 항상 표시 */} -
- - - {errors.partType && ( -

- {errors.partType.message} -

- )} - {!errors.partType && selectedPartType === 'BENDING' && ( -

- * 절곡 부품은 전개도(바라시)만 있으며, 부품 구성(BOM)은 사용하지 않습니다. -

- )} -
- - {/* ASSEMBLY 부품인 경우 */} - {selectedPartType === 'ASSEMBLY' && ( - <> -
- - - {errors.category1 && ( -

- {errors.category1.message} -

- )} -
- - {/* 가이드레일: 설치 유형 */} - {selectedCategory1 === 'guide_rail' && ( -
- - -
- )} - - {/* 케이스: 설치 유형 */} - {selectedCategory1 === 'case' && ( -
- - -
- )} - - {/* 하단마감재: 설치 유형 */} - {selectedCategory1 === 'bottom_finish' && ( -
- - -
- )} - - {/* ASSEMBLY 공통: 단위, 비고, 측면규격 및 길이 - 부품유형 선택 시 바로 표시 */} -
- - -
- -
- - -
- - {/* 측면 규격 및 길이 */} -
-

측면 규격 및 길이

-
-
- - { - setSideSpecWidth(e.target.value); - setValue('sideSpecWidth', e.target.value); - }} - /> -
-
- - { - setSideSpecHeight(e.target.value); - setValue('sideSpecHeight', e.target.value); - }} - /> -
-
- - -
-
-

- * 품목코드: {(() => { - const itemName = selectedCategory1 === 'guide_rail' ? '가이드레일' : - selectedCategory1 === 'case' ? '케이스' : - selectedCategory1 === 'bottom_finish' ? '하단마감재' : ''; - const installationTypeMap: Record = { - "standard": "표준형", - "wall": "벽면형", - "side": "측면형", - "steel": "스크린", - "iron": "철재" - }; - const installTypeText = installationTypeMap[selectedInstallationType] || selectedInstallationType; - const length = assemblyLength ? parseInt(assemblyLength) : 0; - let lengthCode = ""; - if (length === 1219) lengthCode = "12"; - else if (length === 2438) lengthCode = "24"; - else if (length === 3000) lengthCode = "30"; - else if (length === 3500) lengthCode = "35"; - else if (length === 4000) lengthCode = "40"; - else if (length === 4150) lengthCode = "41"; - else if (length === 4200) lengthCode = "42"; - else if (length === 4300) lengthCode = "43"; - else lengthCode = Math.floor(length / 100).toString().padStart(2, '0'); - - if (itemName && installTypeText && sideSpecWidth && sideSpecHeight && assemblyLength) { - return `${itemName} ${installTypeText}-${sideSpecWidth}*${sideSpecHeight}*${lengthCode}`; - } - return "품목명 설치유형-?*?*?"; - })()} -

- - {/* 품목 상태 */} -
- - -

- * 비활성 시 품목 사용이 제한됩니다 -

-
- - {/* 부품구성 (BOM) 필요 여부 - ASSEMBLY 전용, 카드 내부 */} - {selectedCategory1 && ( -
-
- setNeedsBOM(checked as boolean)} - /> - -
-

- * 이 부품이 하위 구성품을 포함하는 경우 체크하세요 -

-
- )} -
- - )} - - {/* BENDING 또는 PURCHASED 부품 */} - {(selectedPartType === 'BENDING' || selectedPartType === 'PURCHASED') && ( - <> -
- - -
- - {/* BENDING: 종류 선택 */} - {selectedPartType === 'BENDING' && selectedCategory1 && PART_ITEM_NAMES[selectedCategory1] && ( -
- - -
- )} - - {/* BENDING 3필드 purple section - 종류 선택 후 바로 표시 */} - {selectedPartType === 'BENDING' && selectedBendingItemType && ( -
-
- - - {errors.material && ( -

- {errors.material.message} -

- )} -
- -
- -
- { - setWidthSum(e.target.value); - setValue('length', e.target.value); - }} - placeholder="전개도 상세를 입력해주세요" - readOnly={bendingDetails.length > 0} - className={`${bendingDetails.length > 0 ? "bg-blue-50 font-medium" : ""} ${errors.length ? 'border-red-500' : ''}`} - /> - mm -
- {errors.length && ( -

- {errors.length.message} -

- )} - {!errors.length && bendingDetails.length > 0 && ( -

- * 전개도 상세 입력의 합계가 자동 반영됩니다 -

- )} -
- -
- - - {errors.bendingLength && ( -

- {errors.bendingLength.message} -

- )} -
-
- )} - - {/* 전동개폐기 전용 필드 (PURCHASED만) */} - {selectedPartType === 'PURCHASED' && selectedCategory1 === 'electric_opener' && ( - <> -
- - - {errors.electricOpenerPower && ( -

- {errors.electricOpenerPower.message} -

- )} -
-
- - - {errors.electricOpenerCapacity && ( -

- {errors.electricOpenerCapacity.message} -

- )} -
- - )} - - {/* 모터 전용 필드 (PURCHASED만) */} - {selectedPartType === 'PURCHASED' && selectedCategory1 === 'motor' && ( -
-
- - -
-
- - - {errors.motorVoltage && ( -

- {errors.motorVoltage.message} -

- )} -
-
- )} - - {/* 체인 전용 필드 (PURCHASED만) */} - {selectedPartType === 'PURCHASED' && selectedCategory1 === 'chain' && ( -
-
- - - {errors.chainSpec && ( -

- {errors.chainSpec.message} -

- )} -
-
- - -
-
- )} - - {/* PURCHASED: 품목명 선택 후에만 단위, 비고 표시 */} - {selectedPartType === 'PURCHASED' && selectedCategory1 && ( - <> -
- - -
- -
- - -
- - {/* 품목코드 자동생성 */} -
- - -

- * 품목코드는 '품목명-규격' 형식으로 자동 생성됩니다 -

-
- - {/* 품목 상태 */} -
- - -

- * 비활성 시 품목 사용이 제한됩니다 -

-
- - {/* 부품구성 (BOM) 필요 여부 - PURCHASED 전용, 카드 내부 */} -
-
- setNeedsBOM(checked as boolean)} - /> - -
-

- * 이 부품이 하위 구성품을 포함하는 경우 체크하세요 -

-
- - )} - - {/* BENDING: 단위, 비고 (품목명 선택 후 표시) */} - {selectedPartType === 'BENDING' && selectedBendingItemType && ( - <> -
- - -
- -
- - -
- - {/* 품목코드 자동생성 */} -
- - -

- {(selectedCategory1 === "guide_rail_wall" || selectedCategory1 === "guide_rail_side") - ? "* 가이드레일 품목코드는 '제품구분(R/S)+종류(M/T/C/D/S/U)+모양&길이' 형식으로 자동 생성됩니다 (예: RD30, SM53)" - : "* 절곡 부품 품목코드는 '품목명+종류+길이축약' 형식으로 자동 생성됩니다 (예: 케이스후면부30)"} -

-
- - )} - - {/* 부품 활성/비활성 - BENDING만 표시 (PURCHASED는 품목명 다음에 표시) */} - {selectedPartType === 'BENDING' && ( -
- - -

- * 비활성 시 품목 사용이 제한됩니다 -

-
- )} - - )} - + )} {/* SM/RM/CS 공통 섹션 */} {(selectedItemType === 'RM' || selectedItemType === 'SM' || selectedItemType === 'CS') && ( - <> -
- - {/* 원자재/부자재는 목록에서 선택, 소모품은 직접 입력 */} - {selectedItemType === 'RM' ? ( - <> - - {errors.itemName && ( -

- {errors.itemName.message} -

- )} - - ) : selectedItemType === 'SM' ? ( - <> - - {errors.itemName && ( -

- {errors.itemName.message} -

- )} - - ) : ( - <> - { - const newName = e.target.value; - setItemName(newName); - setValue('itemName', newName); - // 품목코드 자동생성 - const spec = getValues('specification') || ''; - setValue('itemCode', spec ? `${newName}-${spec}` : newName); - }} - className={errors.itemName ? 'border-red-500' : ''} - /> - {errors.itemName && ( -

- {errors.itemName.message} -

- )} - - )} -
- - {/* 규격(사양) */} - {selectedItemType === 'CS' ? ( -
- - { - // 품목코드 자동생성 - const spec = e.target.value; - const name = itemName || ''; - setValue('itemCode', name && spec ? `${name}-${spec}` : name); - } - })} - className={errors.specification ? 'border-red-500' : ''} - /> - {errors.specification && ( -

- {errors.specification.message} -

- )} -
- ) : ( -
- - - {errors.specification && ( -

- {errors.specification.message} -

- )} - {!errors.specification && ( -

- * 규격은 품목명 선택 시 자동으로 필터링됩니다 -

- )} -
- )} - - {/* 품목코드 (자동생성) */} -
- - { - const name = itemName || ''; - const spec = getValues('specification') || ''; - return spec ? `${name}-${spec}` : name; - })()} - disabled - className="bg-muted text-muted-foreground" - /> -

- * 품목코드는 '품목명-규격' 형식으로 자동 생성됩니다 -

-
- - {/* 품목 상태 (RM/SM만) */} - {(selectedItemType === 'RM' || selectedItemType === 'SM') && ( -
- - -

- * 비활성 시 품목 사용이 제한됩니다 -

-
- )} - - {/* 단위 (RM/SM/CS 공통) */} -
- - - {errors.unit && ( -

- {errors.unit.message} -

- )} -
- - - + )} )} diff --git a/src/components/items/ItemMasterDataManagement.tsx b/src/components/items/ItemMasterDataManagement.tsx index 4cbf4baa..6c53c3e2 100644 --- a/src/components/items/ItemMasterDataManagement.tsx +++ b/src/components/items/ItemMasterDataManagement.tsx @@ -30,7 +30,6 @@ import { transformPagesResponse, transformSectionsResponse, transformSectionTemplatesResponse, - transformMasterFieldsResponse, transformFieldsResponse, transformCustomTabsResponse, transformUnitOptionsResponse, @@ -78,41 +77,40 @@ const INPUT_TYPE_OPTIONS = [ { value: 'textarea', label: '텍스트영역' } ]; +// 입력 타입 라벨 변환 헬퍼 함수 (중복 코드 제거) +const getInputTypeLabel = (inputType: string | undefined): string => { + const labels: Record = { + textbox: '텍스트박스', + number: '숫자', + dropdown: '드롭다운', + checkbox: '체크박스', + date: '날짜', + textarea: '텍스트영역', + }; + return labels[inputType || ''] || '텍스트박스'; +}; + export function ItemMasterDataManagement() { const { itemPages, loadItemPages, - addItemPage: _addItemPage, updateItemPage, deleteItemPage, - addSectionToPage: _addSectionToPage, updateSection, deleteSection, - addFieldToSection: _addFieldToSection, - updateField: _updateField, - deleteField: _deleteField, reorderFields, itemMasterFields, loadItemMasterFields, - addItemMasterField: _addItemMasterField, - updateItemMasterField: _updateItemMasterField, - deleteItemMasterField: _deleteItemMasterField, sectionTemplates, loadSectionTemplates, - addSectionTemplate: _addSectionTemplate, - updateSectionTemplate: _updateSectionTemplate, - deleteSectionTemplate: _deleteSectionTemplate, resetAllData, - tenantId: _tenantId, // 2025-11-26 추가: 독립 엔티티 관리 independentSections, loadIndependentSections, - independentFields: _independentFields, loadIndependentFields, refreshIndependentSections, refreshIndependentFields, linkSectionToPage, - unlinkSectionFromPage: _unlinkSectionFromPage, linkFieldToSection, unlinkFieldFromSection, getSectionUsage, @@ -133,7 +131,7 @@ export function ItemMasterDataManagement() { pageCount: itemPages.length, pages: itemPages.map(p => ({ id: p.id, - name: p.name, + name: p.page_name, sectionsCount: p.sections.length, sections: p.sections.map(s => ({ id: s.id, @@ -160,8 +158,7 @@ export function ItemMasterDataManagement() { isPageDialogOpen, setIsPageDialogOpen, newPageName, setNewPageName, newPageItemType, setNewPageItemType, editingPathPageId, setEditingPathPageId, editingAbsolutePath, setEditingAbsolutePath, - handleAddPage, handleDuplicatePage, handleDeletePage: _handleDeletePage, - handleUpdatePageName: _handleUpdatePageName, handleUpdateAbsolutePath: _handleUpdateAbsolutePath, + handleAddPage, handleDuplicatePage, } = pageManagement; const { @@ -173,10 +170,9 @@ export function ItemMasterDataManagement() { newSectionType, setNewSectionType, sectionInputMode, setSectionInputMode, selectedSectionTemplateId, setSelectedSectionTemplateId, - expandedSections: _expandedSections, setExpandedSections: _setExpandedSections, handleAddSection, handleLinkTemplate, handleEditSectionTitle, handleSaveSectionTitle, - handleUnlinkSection, handleDeleteSection: _handleDeleteSection, toggleSection: _toggleSection, + handleUnlinkSection, } = sectionManagement; const { @@ -202,7 +198,7 @@ export function ItemMasterDataManagement() { newFieldConditionFields, setNewFieldConditionFields, newFieldConditionSections, setNewFieldConditionSections, tempConditionValue, setTempConditionValue, - handleAddField, handleEditField, handleDeleteField: _handleDeleteField, + handleAddField, handleEditField, } = fieldManagement; const { @@ -864,15 +860,7 @@ export function ItemMasterDataManagement() { {unitOptions.map((option) => { const columns = attributeColumns['units'] || []; const hasColumns = columns.length > 0 && option.columnValues; - const inputTypeLabel = - option.inputType === 'textbox' ? '텍스트박스' : - option.inputType === 'number' ? '숫자' : - option.inputType === 'dropdown' ? '드롭다운' : - option.inputType === 'checkbox' ? '체크박스' : - option.inputType === 'date' ? '날짜' : - option.inputType === 'textarea' ? '텍스트영역' : - '텍스트박스'; - + return (
@@ -880,7 +868,7 @@ export function ItemMasterDataManagement() {
{option.label} {option.inputType && ( - {inputTypeLabel} + {getInputTypeLabel(option.inputType)} )} {option.required && ( 필수 @@ -966,15 +954,7 @@ export function ItemMasterDataManagement() { {materialOptions.map((option) => { const columns = attributeColumns['materials'] || []; const hasColumns = columns.length > 0 && option.columnValues; - const inputTypeLabel = - option.inputType === 'textbox' ? '텍스트박스' : - option.inputType === 'number' ? '숫자' : - option.inputType === 'dropdown' ? '드롭다운' : - option.inputType === 'checkbox' ? '체크박스' : - option.inputType === 'date' ? '날짜' : - option.inputType === 'textarea' ? '텍스트영역' : - '텍스트박스'; - + return (
@@ -982,7 +962,7 @@ export function ItemMasterDataManagement() {
{option.label} {option.inputType && ( - {inputTypeLabel} + {getInputTypeLabel(option.inputType)} )} {option.required && ( 필수 @@ -1068,15 +1048,8 @@ export function ItemMasterDataManagement() { {surfaceTreatmentOptions.map((option) => { const columns = attributeColumns['surface'] || []; const hasColumns = columns.length > 0 && option.columnValues; - const inputTypeLabel = - option.inputType === 'textbox' ? '텍스트박스' : - option.inputType === 'number' ? '숫자' : - option.inputType === 'dropdown' ? '드롭다운' : - option.inputType === 'checkbox' ? '체크박스' : - option.inputType === 'date' ? '날짜' : - option.inputType === 'textarea' ? '텍스트영역' : - '텍스트박스'; - + const inputTypeLabel = getInputTypeLabel(option.inputType); + return (
@@ -1173,15 +1146,8 @@ export function ItemMasterDataManagement() {
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} {propertiesArray.map((property: any) => { - const inputTypeLabel = - property.type === 'textbox' ? '텍스트박스' : - property.type === 'number' ? '숫자' : - property.type === 'dropdown' ? '드롭다운' : - property.type === 'checkbox' ? '체크박스' : - property.type === 'date' ? '날짜' : - property.type === 'textarea' ? '텍스트영역' : - '텍스트박스'; - + const inputTypeLabel = getInputTypeLabel(property.type); + return (
@@ -1283,15 +1249,8 @@ export function ItemMasterDataManagement() { {currentOptions.map((option) => { const columns = attributeColumns[currentTabKey] || []; const hasColumns = columns.length > 0 && option.columnValues; - const inputTypeLabel = - option.inputType === 'textbox' ? '텍스트박스' : - option.inputType === 'number' ? '숫자' : - option.inputType === 'dropdown' ? '드롭다운' : - option.inputType === 'checkbox' ? '체크박스' : - option.inputType === 'date' ? '날짜' : - option.inputType === 'textarea' ? '텍스트영역' : - '텍스트박스'; - + const inputTypeLabel = getInputTypeLabel(option.inputType); + return (
diff --git a/src/components/items/ItemMasterDataManagement/components/DraggableField.tsx b/src/components/items/ItemMasterDataManagement/components/DraggableField.tsx index 4aec39df..b646dcf6 100644 --- a/src/components/items/ItemMasterDataManagement/components/DraggableField.tsx +++ b/src/components/items/ItemMasterDataManagement/components/DraggableField.tsx @@ -5,7 +5,7 @@ import { Badge } from '@/components/ui/badge'; import { GripVertical, Edit, - X + Unlink } from 'lucide-react'; // 입력방식 옵션 (ItemMasterDataManagement에서 사용하는 상수) @@ -111,8 +111,9 @@ export function DraggableField({ field, index, moveField, onDelete, onEdit }: Dr size="sm" variant="ghost" onClick={onDelete} + title="섹션에서 연결 해제" > - +
diff --git a/src/components/items/ItemMasterDataManagement/components/DraggableSection.tsx b/src/components/items/ItemMasterDataManagement/components/DraggableSection.tsx index 2e901792..1e69c26e 100644 --- a/src/components/items/ItemMasterDataManagement/components/DraggableSection.tsx +++ b/src/components/items/ItemMasterDataManagement/components/DraggableSection.tsx @@ -8,7 +8,7 @@ import { Edit, Check, X, - Trash2 + Unlink } from 'lucide-react'; interface DraggableSectionProps { @@ -120,7 +120,7 @@ export function DraggableSection({ onClick={onDelete} title="페이지에서 연결 해제" > - +
diff --git a/src/components/items/ItemTypeSelect.tsx b/src/components/items/ItemTypeSelect.tsx index f31894ad..c1847b5f 100644 --- a/src/components/items/ItemTypeSelect.tsx +++ b/src/components/items/ItemTypeSelect.tsx @@ -17,7 +17,7 @@ import { import { type ItemType } from '@/types/item'; interface ItemTypeSelectProps { - value?: ItemType; + value?: ItemType | ''; onChange: (value: ItemType) => void; disabled?: boolean; required?: boolean; @@ -56,7 +56,7 @@ export default function ItemTypeSelect({ )}