From ceae830e4174af27d9eb1ace50da2177b362ccf8 Mon Sep 17 00:00:00 2001 From: hskwon Date: Tue, 9 Dec 2025 20:30:39 +0900 Subject: [PATCH] =?UTF-8?q?docs:=202025-11=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=95=84=EC=B9=B4=EC=9D=B4=EB=B8=8C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - history/2025-11/front-requests/ 프론트 요청 문서 이동 - history/2025-11/item-master-archived/ Item Master 구버전 문서 이동 --- ...I-2025-11-20] item-master-specification.md | 1297 +++++++++ ...item-master-data-management-api-request.md | 841 ++++++ ...API-2025-11-26] item-master-api-changes.md | 120 + ...2025-11-25] section-template-fields-api.md | 588 ++++ ...[ANALYSIS-2025-11-21] item-master-notes.md | 1060 +++++++ .../[ANALYSIS] item-master-data-management.md | 2588 +++++++++++++++++ ...11-23] item-master-backend-requirements.md | 276 ++ ...11-24] item-management-dynamic-api-spec.md | 1595 ++++++++++ ...item-master-data-management-api-request.md | 971 +++++++ ...11-24] item-management-dynamic-frontend.md | 1128 +++++++ .../[IMPL-2025-11-17] item-list-css-sync.md | 280 ++ ...] item-master-api-integration-checklist.md | 1671 +++++++++++ 12 files changed, 12415 insertions(+) create mode 100644 history/2025-11/front-requests/[API-2025-11-20] item-master-specification.md create mode 100644 history/2025-11/front-requests/[API-2025-11-25] item-master-data-management-api-request.md create mode 100644 history/2025-11/front-requests/[API-2025-11-26] item-master-api-changes.md create mode 100644 history/2025-11/front-requests/[API-REQUEST-2025-11-25] section-template-fields-api.md create mode 100644 history/2025-11/item-master-archived/[ANALYSIS-2025-11-21] item-master-notes.md create mode 100644 history/2025-11/item-master-archived/[ANALYSIS] item-master-data-management.md create mode 100644 history/2025-11/item-master-archived/[API-2025-11-23] item-master-backend-requirements.md create mode 100644 history/2025-11/item-master-archived/[API-2025-11-24] item-management-dynamic-api-spec.md create mode 100644 history/2025-11/item-master-archived/[API-2025-11-25] item-master-data-management-api-request.md create mode 100644 history/2025-11/item-master-archived/[DESIGN-2025-11-24] item-management-dynamic-frontend.md create mode 100644 history/2025-11/item-master-archived/[IMPL-2025-11-17] item-list-css-sync.md create mode 100644 history/2025-11/item-master-archived/[IMPL-2025-11-20] item-master-api-integration-checklist.md diff --git a/history/2025-11/front-requests/[API-2025-11-20] item-master-specification.md b/history/2025-11/front-requests/[API-2025-11-20] item-master-specification.md new file mode 100644 index 0000000..243a782 --- /dev/null +++ b/history/2025-11/front-requests/[API-2025-11-20] item-master-specification.md @@ -0,0 +1,1297 @@ + # 품목기준관리 API 명세서 + +**작성일**: 2025-11-20 +**버전**: v1.0 +**작성자**: 프론트엔드 개발팀 +**수신**: 백엔드 개발팀 + +--- + +## 📋 목차 + +1. [개요](#1-개요) +2. [인증 및 공통 사항](#2-인증-및-공통-사항) +3. [데이터베이스 테이블 정의](#3-데이터베이스-테이블-정의) +4. [API 엔드포인트](#4-api-엔드포인트) +5. [요청/응답 예시](#5-요청응답-예시) +6. [에러 처리](#6-에러-처리) +7. [구현 우선순위](#7-구현-우선순위) + +--- + +## 1. 개요 + +### 1.1 목적 +품목기준관리 화면에서 사용할 API 개발 요청 + +### 1.2 주요 기능 +- 품목 유형별 페이지 관리 (FG, PT, SM, RM, CS) +- 계층구조 기반 섹션 및 필드 관리 +- BOM(Bill of Materials) 항목 관리 +- 섹션 템플릿 및 마스터 필드 관리 +- 커스텀 탭 및 단위 관리 + +### 1.3 기술 요구사항 +- ✅ **Service-First 패턴** 적용 +- ✅ **Multi-tenant**: `tenant_id` 기반 격리, `BelongsToTenant` 스코프 +- ✅ **Soft Delete**: 모든 테이블 적용 +- ✅ **감사 로그**: 생성/수정/삭제 시 `audit_logs` 기록 +- ✅ **i18n**: 메시지는 `__('message.xxx')` 키만 사용 +- ✅ **실시간 저장**: 모든 CUD 작업 즉시 처리 (일괄 저장 없음) + +### 1.4 저장 전략 +**중요**: 프론트엔드에서 **실시간 저장** 방식 사용 +- 페이지/섹션/필드 생성 즉시 API 호출 +- 수정/삭제/순서변경 즉시 API 호출 +- 일괄 저장(Batch Save) API 불필요 + +--- + +## 2. 인증 및 공통 사항 + +### 2.1 인증 +``` +Headers: + X-API-KEY: {api_key} + Authorization: Bearer {sanctum_token} +``` + +### 2.2 Base URL +``` +http://api.sam.kr/api/v1/item-master +``` + +### 2.3 공통 응답 형식 + +**성공 응답**: +```json +{ + "success": true, + "message": "message.created", + "data": { ... } +} +``` + +**에러 응답**: +```json +{ + "success": false, + "message": "error.validation_failed", + "errors": { + "page_name": ["페이지명은 필수입니다."] + } +} +``` + +### 2.4 공통 컬럼 +모든 테이블에 다음 컬럼 포함: +- `tenant_id` (BIGINT, NOT NULL, INDEX) +- `created_by` (BIGINT, NULL) +- `updated_by` (BIGINT, NULL) +- `deleted_by` (BIGINT, NULL) +- `created_at` (TIMESTAMP) +- `updated_at` (TIMESTAMP) +- `deleted_at` (TIMESTAMP, NULL) - Soft Delete + +--- + +## 3. 데이터베이스 테이블 정의 + +### 3.1 item_pages (품목 페이지) + +```sql +CREATE TABLE item_pages ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', + page_name VARCHAR(255) NOT NULL COMMENT '페이지명', + item_type ENUM('FG', 'PT', 'SM', 'RM', 'CS') NOT NULL COMMENT '품목 유형 (완제품/반제품/부자재/원자재/소모품)', + absolute_path VARCHAR(500) NULL COMMENT '절대 경로', + is_active TINYINT(1) DEFAULT 1 COMMENT '활성 여부', + created_by BIGINT UNSIGNED NULL COMMENT '생성자 ID', + updated_by BIGINT UNSIGNED NULL COMMENT '수정자 ID', + deleted_by BIGINT UNSIGNED NULL COMMENT '삭제자 ID', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_id (tenant_id), + INDEX idx_item_type (item_type), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='품목 페이지'; +``` + +### 3.2 item_sections (섹션 인스턴스) + +```sql +CREATE TABLE item_sections ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', + page_id BIGINT UNSIGNED NOT NULL COMMENT '페이지 ID', + title VARCHAR(255) NOT NULL COMMENT '섹션 제목', + type ENUM('fields', 'bom') NOT NULL DEFAULT 'fields' COMMENT '섹션 타입 (필드형/BOM형)', + order_no INT NOT NULL DEFAULT 0 COMMENT '정렬 순서', + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_page (tenant_id, page_id), + INDEX idx_order (page_id, order_no), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (page_id) REFERENCES item_pages(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='품목 섹션 인스턴스'; +``` + +### 3.3 item_fields (필드) + +```sql +CREATE TABLE item_fields ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', + section_id BIGINT UNSIGNED NOT NULL COMMENT '섹션 ID', + field_name VARCHAR(255) NOT NULL COMMENT '필드명', + field_type ENUM('textbox', 'number', 'dropdown', 'checkbox', 'date', 'textarea') NOT NULL COMMENT '필드 타입', + order_no INT NOT NULL DEFAULT 0 COMMENT '정렬 순서', + is_required TINYINT(1) DEFAULT 0 COMMENT '필수 여부', + default_value TEXT NULL COMMENT '기본값', + placeholder VARCHAR(255) NULL COMMENT '플레이스홀더', + display_condition JSON NULL COMMENT '표시 조건 {"field_id": "1", "operator": "equals", "value": "true"}', + validation_rules JSON NULL COMMENT '검증 규칙 {"min": 0, "max": 100, "pattern": "regex"}', + options JSON NULL COMMENT '드롭다운 옵션 [{"label": "옵션1", "value": "val1"}]', + properties JSON NULL COMMENT '필드 속성 {"unit": "mm", "precision": 2, "format": "YYYY-MM-DD"}', + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_section (tenant_id, section_id), + INDEX idx_order (section_id, order_no), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (section_id) REFERENCES item_sections(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='품목 필드'; +``` + +### 3.4 item_bom_items (BOM 항목) + +```sql +CREATE TABLE item_bom_items ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', + section_id BIGINT UNSIGNED NOT NULL COMMENT '섹션 ID', + item_code VARCHAR(100) NULL COMMENT '품목 코드', + item_name VARCHAR(255) NOT NULL COMMENT '품목명', + quantity DECIMAL(15, 4) NOT NULL DEFAULT 0 COMMENT '수량', + unit VARCHAR(50) NULL COMMENT '단위', + unit_price DECIMAL(15, 2) NULL COMMENT '단가', + total_price DECIMAL(15, 2) NULL COMMENT '총액', + spec TEXT NULL COMMENT '사양', + note TEXT NULL COMMENT '비고', + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_section (tenant_id, section_id), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (section_id) REFERENCES item_sections(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='BOM 항목'; +``` + +### 3.5 section_templates (섹션 템플릿) + +```sql +CREATE TABLE section_templates ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', + title VARCHAR(255) NOT NULL COMMENT '템플릿명', + type ENUM('fields', 'bom') NOT NULL DEFAULT 'fields' COMMENT '섹션 타입', + description TEXT NULL COMMENT '설명', + is_default TINYINT(1) DEFAULT 0 COMMENT '기본 템플릿 여부', + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant (tenant_id), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='섹션 템플릿'; +``` + +### 3.6 item_master_fields (마스터 필드) + +```sql +CREATE TABLE item_master_fields ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', + field_name VARCHAR(255) NOT NULL COMMENT '필드명', + field_type ENUM('textbox', 'number', 'dropdown', 'checkbox', 'date', 'textarea') NOT NULL COMMENT '필드 타입', + category VARCHAR(100) NULL COMMENT '카테고리', + description TEXT NULL COMMENT '설명', + is_common TINYINT(1) DEFAULT 0 COMMENT '공통 필드 여부', + default_value TEXT NULL COMMENT '기본값', + options JSON NULL COMMENT '옵션', + validation_rules JSON NULL COMMENT '검증 규칙', + properties JSON NULL COMMENT '속성', + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant (tenant_id), + INDEX idx_category (category), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='마스터 필드'; +``` + +### 3.7 custom_tabs (커스텀 탭) + +```sql +CREATE TABLE custom_tabs ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', + label VARCHAR(255) NOT NULL COMMENT '탭 라벨', + icon VARCHAR(100) NULL COMMENT '아이콘', + is_default TINYINT(1) DEFAULT 0 COMMENT '기본 탭 여부', + order_no INT NOT NULL DEFAULT 0 COMMENT '정렬 순서', + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant (tenant_id), + INDEX idx_order (tenant_id, order_no), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='커스텀 탭'; +``` + +### 3.8 tab_columns (탭별 컬럼 설정) + +```sql +CREATE TABLE tab_columns ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', + tab_id BIGINT UNSIGNED NOT NULL COMMENT '탭 ID', + columns JSON NOT NULL COMMENT '컬럼 설정 [{"key": "name", "label": "품목명", "visible": true, "order": 0}]', + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + UNIQUE KEY uk_tenant_tab (tenant_id, tab_id), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (tab_id) REFERENCES custom_tabs(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='탭별 컬럼 설정'; +``` + +### 3.9 unit_options (단위 옵션) + +```sql +CREATE TABLE unit_options ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', + label VARCHAR(100) NOT NULL COMMENT '단위 라벨', + value VARCHAR(50) NOT NULL COMMENT '단위 값', + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant (tenant_id), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='단위 옵션'; +``` + +--- + +## 4. API 엔드포인트 + +### 4.1 초기화 API + +#### `GET /v1/item-master/init` +**목적**: 화면 진입 시 전체 초기 데이터 로드 + +**Request**: 없음 + +**Response**: +```json +{ + "success": true, + "message": "message.fetched", + "data": { + "pages": [ + { + "id": 1, + "page_name": "완제품 A", + "item_type": "FG", + "absolute_path": "/FG/완제품 A", + "is_active": true, + "sections": [ + { + "id": 1, + "title": "기본 정보", + "type": "fields", + "order_no": 0, + "fields": [...] + } + ] + } + ], + "sectionTemplates": [...], + "masterFields": [...], + "customTabs": [...], + "tabColumns": {...}, + "unitOptions": [...] + } +} +``` + +**참고**: +- `pages`는 `sections`, `fields`, `bomItems`를 중첩(nested) 포함 +- 한 번의 API 호출로 모든 데이터 로드 + +--- + +### 4.2 페이지 관리 + +#### `GET /v1/item-master/pages` +**목적**: 페이지 목록 조회 (섹션/필드 포함) + +**Query Parameters**: +| 파라미터 | 타입 | 필수 | 설명 | +|---------|------|------|------| +| `item_type` | string | 선택 | 품목 유형 필터 (FG, PT, SM, RM, CS) | + +**Response**: 초기화 API의 `data.pages`와 동일 + +--- + +#### `POST /v1/item-master/pages` +**목적**: 페이지 생성 + +**Request Body**: +```json +{ + "page_name": "완제품 A", + "item_type": "FG", + "absolute_path": "/FG/완제품 A" +} +``` + +**Validation**: +- `page_name`: required, string, max:255 +- `item_type`: required, in:FG,PT,SM,RM,CS +- `absolute_path`: nullable, string, max:500 + +**Response**: +```json +{ + "success": true, + "message": "message.created", + "data": { + "id": 1, + "page_name": "완제품 A", + "item_type": "FG", + "absolute_path": "/FG/완제품 A", + "is_active": true, + "sections": [], + "created_at": "2025-11-20T10:00:00.000000Z", + "updated_at": "2025-11-20T10:00:00.000000Z" + } +} +``` + +--- + +#### `PUT /v1/item-master/pages/{id}` +**목적**: 페이지 수정 + +**Request Body**: +```json +{ + "page_name": "완제품 A (수정)", + "absolute_path": "/FG/완제품 A (수정)" +} +``` + +**Validation**: +- `page_name`: string, max:255 +- `absolute_path`: nullable, string, max:500 + +**Response**: 수정된 페이지 정보 반환 + +--- + +#### `DELETE /v1/item-master/pages/{id}` +**목적**: 페이지 삭제 (Soft Delete) + +**Request**: 없음 + +**Response**: +```json +{ + "success": true, + "message": "message.deleted" +} +``` + +**참고**: +- Soft Delete 처리 (`deleted_at` 업데이트) +- 하위 섹션/필드도 함께 Soft Delete 처리 (Cascade) + +--- + +### 4.3 섹션 관리 + +#### `POST /v1/item-master/pages/{pageId}/sections` +**목적**: 섹션 생성 + +**Request Body**: +```json +{ + "title": "기본 정보", + "type": "fields" +} +``` + +**Validation**: +- `title`: required, string, max:255 +- `type`: required, in:fields,bom + +**Response**: +```json +{ + "success": true, + "message": "message.created", + "data": { + "id": 1, + "page_id": 1, + "title": "기본 정보", + "type": "fields", + "order_no": 0, + "fields": [] + } +} +``` + +**참고**: +- `order_no`는 자동 계산 (해당 페이지의 마지막 섹션 order + 1) + +--- + +#### `PUT /v1/item-master/sections/{id}` +**목적**: 섹션 수정 + +**Request Body**: +```json +{ + "title": "기본 정보 (수정)" +} +``` + +**Validation**: +- `title`: string, max:255 + +**Response**: 수정된 섹션 정보 반환 + +--- + +#### `DELETE /v1/item-master/sections/{id}` +**목적**: 섹션 삭제 (Soft Delete) + +**Request**: 없음 + +**Response**: +```json +{ + "success": true, + "message": "message.deleted" +} +``` + +**참고**: 하위 필드도 함께 Soft Delete 처리 + +--- + +#### `PUT /v1/item-master/pages/{pageId}/sections/reorder` +**목적**: 섹션 순서 변경 (드래그 앤 드롭) + +**Request Body**: +```json +{ + "section_orders": [ + {"id": 2, "order_no": 0}, + {"id": 1, "order_no": 1}, + {"id": 3, "order_no": 2} + ] +} +``` + +**Validation**: +- `section_orders`: required, array +- `section_orders.*.id`: required, exists:item_sections,id +- `section_orders.*.order_no`: required, integer, min:0 + +**Response**: +```json +{ + "success": true, + "message": "message.reordered" +} +``` + +--- + +### 4.4 필드 관리 + +#### `POST /v1/item-master/sections/{sectionId}/fields` +**목적**: 필드 생성 + +**Request Body**: +```json +{ + "field_name": "제품명", + "field_type": "textbox", + "is_required": true, + "placeholder": "제품명을 입력하세요", + "validation_rules": { + "min": 1, + "max": 100 + }, + "properties": { + "unit": "mm", + "precision": 2 + } +} +``` + +**Validation**: +- `field_name`: required, string, max:255 +- `field_type`: required, in:textbox,number,dropdown,checkbox,date,textarea +- `is_required`: boolean +- `placeholder`: nullable, string, max:255 +- `validation_rules`: nullable, json +- `properties`: nullable, json +- `options`: nullable, json (field_type=dropdown일 때 필수) + +**Response**: +```json +{ + "success": true, + "message": "message.created", + "data": { + "id": 1, + "section_id": 1, + "field_name": "제품명", + "field_type": "textbox", + "order_no": 0, + "is_required": true, + "placeholder": "제품명을 입력하세요", + "validation_rules": {...}, + "properties": {...} + } +} +``` + +--- + +#### `PUT /v1/item-master/fields/{id}` +**목적**: 필드 수정 + +**Request Body**: 생성과 동일 (모든 필드 선택적) + +**Response**: 수정된 필드 정보 반환 + +--- + +#### `DELETE /v1/item-master/fields/{id}` +**목적**: 필드 삭제 (Soft Delete) + +**Request**: 없음 + +**Response**: +```json +{ + "success": true, + "message": "message.deleted" +} +``` + +--- + +#### `PUT /v1/item-master/sections/{sectionId}/fields/reorder` +**목적**: 필드 순서 변경 + +**Request Body**: +```json +{ + "field_orders": [ + {"id": 3, "order_no": 0}, + {"id": 1, "order_no": 1}, + {"id": 2, "order_no": 2} + ] +} +``` + +**Validation**: 섹션 순서 변경과 동일 + +**Response**: +```json +{ + "success": true, + "message": "message.reordered" +} +``` + +--- + +### 4.5 BOM 관리 + +#### `POST /v1/item-master/sections/{sectionId}/bom-items` +**목적**: BOM 항목 생성 + +**Request Body**: +```json +{ + "item_code": "MAT-001", + "item_name": "철판", + "quantity": 10.5, + "unit": "kg", + "unit_price": 5000.00, + "total_price": 52500.00, + "spec": "두께 2mm, 스테인리스", + "note": "비고 사항" +} +``` + +**Validation**: +- `item_code`: nullable, string, max:100 +- `item_name`: required, string, max:255 +- `quantity`: required, numeric, min:0 +- `unit`: nullable, string, max:50 +- `unit_price`: nullable, numeric, min:0 +- `total_price`: nullable, numeric, min:0 +- `spec`: nullable, string +- `note`: nullable, string + +**Response**: +```json +{ + "success": true, + "message": "message.created", + "data": { + "id": 1, + "section_id": 1, + "item_code": "MAT-001", + "item_name": "철판", + "quantity": 10.5, + "unit": "kg", + "unit_price": 5000.00, + "total_price": 52500.00, + "spec": "두께 2mm, 스테인리스", + "note": "비고 사항" + } +} +``` + +--- + +#### `PUT /v1/item-master/bom-items/{id}` +**목적**: BOM 항목 수정 + +**Request Body**: 생성과 동일 (모든 필드 선택적) + +**Response**: 수정된 BOM 항목 반환 + +--- + +#### `DELETE /v1/item-master/bom-items/{id}` +**목적**: BOM 항목 삭제 (Soft Delete) + +**Request**: 없음 + +**Response**: +```json +{ + "success": true, + "message": "message.deleted" +} +``` + +--- + +### 4.6 섹션 템플릿 + +#### `GET /v1/item-master/section-templates` +**목적**: 섹션 템플릿 목록 조회 + +**Request**: 없음 + +**Response**: +```json +{ + "success": true, + "message": "message.fetched", + "data": [ + { + "id": 1, + "title": "기본 정보 템플릿", + "type": "fields", + "description": "제품 기본 정보 입력용 템플릿", + "is_default": true + } + ] +} +``` + +--- + +#### `POST /v1/item-master/section-templates` +**목적**: 섹션 템플릿 생성 + +**Request Body**: +```json +{ + "title": "기본 정보 템플릿", + "type": "fields", + "description": "제품 기본 정보 입력용 템플릿", + "is_default": false +} +``` + +**Validation**: +- `title`: required, string, max:255 +- `type`: required, in:fields,bom +- `description`: nullable, string +- `is_default`: boolean + +**Response**: 생성된 템플릿 정보 반환 + +--- + +#### `PUT /v1/item-master/section-templates/{id}` +**목적**: 섹션 템플릿 수정 + +**Request Body**: 생성과 동일 (모든 필드 선택적) + +**Response**: 수정된 템플릿 정보 반환 + +--- + +#### `DELETE /v1/item-master/section-templates/{id}` +**목적**: 섹션 템플릿 삭제 (Soft Delete) + +**Request**: 없음 + +**Response**: +```json +{ + "success": true, + "message": "message.deleted" +} +``` + +--- + +### 4.7 마스터 필드 + +#### `GET /v1/item-master/master-fields` +**목적**: 마스터 필드 목록 조회 + +**Query Parameters**: +| 파라미터 | 타입 | 필수 | 설명 | +|---------|------|------|------| +| `category` | string | 선택 | 카테고리 필터 | + +**Response**: +```json +{ + "success": true, + "message": "message.fetched", + "data": [ + { + "id": 1, + "field_name": "제품명", + "field_type": "textbox", + "category": "기본정보", + "description": "제품 이름", + "is_common": true, + "options": null, + "validation_rules": {"max": 100}, + "properties": null + } + ] +} +``` + +--- + +#### `POST /v1/item-master/master-fields` +**목적**: 마스터 필드 생성 + +**Request Body**: +```json +{ + "field_name": "제품명", + "field_type": "textbox", + "category": "기본정보", + "description": "제품 이름", + "is_common": true, + "validation_rules": {"max": 100} +} +``` + +**Validation**: +- `field_name`: required, string, max:255 +- `field_type`: required, in:textbox,number,dropdown,checkbox,date,textarea +- `category`: nullable, string, max:100 +- `description`: nullable, string +- `is_common`: boolean +- `options`: nullable, json +- `validation_rules`: nullable, json +- `properties`: nullable, json + +**Response**: 생성된 마스터 필드 정보 반환 + +--- + +#### `PUT /v1/item-master/master-fields/{id}` +**목적**: 마스터 필드 수정 + +**Request Body**: 생성과 동일 (모든 필드 선택적) + +**Response**: 수정된 마스터 필드 정보 반환 + +--- + +#### `DELETE /v1/item-master/master-fields/{id}` +**목적**: 마스터 필드 삭제 (Soft Delete) + +**Request**: 없음 + +**Response**: +```json +{ + "success": true, + "message": "message.deleted" +} +``` + +--- + +### 4.8 커스텀 탭 + +#### `GET /v1/item-master/custom-tabs` +**목적**: 커스텀 탭 목록 조회 + +**Request**: 없음 + +**Response**: +```json +{ + "success": true, + "message": "message.fetched", + "data": [ + { + "id": 1, + "label": "품질", + "icon": "quality-icon", + "is_default": false, + "order_no": 0 + } + ] +} +``` + +--- + +#### `POST /v1/item-master/custom-tabs` +**목적**: 커스텀 탭 생성 + +**Request Body**: +```json +{ + "label": "품질", + "icon": "quality-icon", + "is_default": false +} +``` + +**Validation**: +- `label`: required, string, max:255 +- `icon`: nullable, string, max:100 +- `is_default`: boolean + +**Response**: 생성된 탭 정보 반환 (order_no 자동 계산) + +--- + +#### `PUT /v1/item-master/custom-tabs/{id}` +**목적**: 커스텀 탭 수정 + +**Request Body**: 생성과 동일 (모든 필드 선택적) + +**Response**: 수정된 탭 정보 반환 + +--- + +#### `DELETE /v1/item-master/custom-tabs/{id}` +**목적**: 커스텀 탭 삭제 (Soft Delete) + +**Request**: 없음 + +**Response**: +```json +{ + "success": true, + "message": "message.deleted" +} +``` + +--- + +#### `PUT /v1/item-master/custom-tabs/reorder` +**목적**: 탭 순서 변경 + +**Request Body**: +```json +{ + "tab_orders": [ + {"id": 2, "order_no": 0}, + {"id": 1, "order_no": 1} + ] +} +``` + +**Validation**: 섹션 순서 변경과 동일 + +**Response**: +```json +{ + "success": true, + "message": "message.reordered" +} +``` + +--- + +#### `PUT /v1/item-master/custom-tabs/{id}/columns` +**목적**: 탭별 컬럼 설정 + +**Request Body**: +```json +{ + "columns": [ + {"key": "name", "label": "품목명", "visible": true, "order": 0}, + {"key": "code", "label": "품목코드", "visible": true, "order": 1}, + {"key": "price", "label": "가격", "visible": false, "order": 2} + ] +} +``` + +**Validation**: +- `columns`: required, array +- `columns.*.key`: required, string +- `columns.*.label`: required, string +- `columns.*.visible`: required, boolean +- `columns.*.order`: required, integer + +**Response**: +```json +{ + "success": true, + "message": "message.updated", + "data": { + "tab_id": 1, + "columns": [...] + } +} +``` + +--- + +### 4.9 단위 관리 + +#### `GET /v1/item-master/units` +**목적**: 단위 목록 조회 + +**Request**: 없음 + +**Response**: +```json +{ + "success": true, + "message": "message.fetched", + "data": [ + {"id": 1, "label": "킬로그램", "value": "kg"}, + {"id": 2, "label": "미터", "value": "m"} + ] +} +``` + +--- + +#### `POST /v1/item-master/units` +**목적**: 단위 생성 + +**Request Body**: +```json +{ + "label": "킬로그램", + "value": "kg" +} +``` + +**Validation**: +- `label`: required, string, max:100 +- `value`: required, string, max:50 + +**Response**: 생성된 단위 정보 반환 + +--- + +#### `DELETE /v1/item-master/units/{id}` +**목적**: 단위 삭제 (Soft Delete) + +**Request**: 없음 + +**Response**: +```json +{ + "success": true, + "message": "message.deleted" +} +``` + +--- + +## 5. 요청/응답 예시 + +### 5.1 페이지 생성 → 섹션 추가 → 필드 추가 흐름 + +**1단계: 페이지 생성** +```bash +POST /v1/item-master/pages +{ + "page_name": "완제품 A", + "item_type": "FG", + "absolute_path": "/FG/완제품 A" +} + +→ Response: {"data": {"id": 1, ...}} +``` + +**2단계: 섹션 추가** +```bash +POST /v1/item-master/pages/1/sections +{ + "title": "기본 정보", + "type": "fields" +} + +→ Response: {"data": {"id": 1, "page_id": 1, ...}} +``` + +**3단계: 필드 추가** +```bash +POST /v1/item-master/sections/1/fields +{ + "field_name": "제품명", + "field_type": "textbox", + "is_required": true +} + +→ Response: {"data": {"id": 1, "section_id": 1, ...}} +``` + +--- + +### 5.2 BOM 섹션 생성 → BOM 항목 추가 + +**1단계: BOM 섹션 생성** +```bash +POST /v1/item-master/pages/1/sections +{ + "title": "자재 목록", + "type": "bom" +} + +→ Response: {"data": {"id": 2, "type": "bom", ...}} +``` + +**2단계: BOM 항목 추가** +```bash +POST /v1/item-master/sections/2/bom-items +{ + "item_name": "철판", + "quantity": 10.5, + "unit": "kg", + "unit_price": 5000 +} + +→ Response: {"data": {"id": 1, "section_id": 2, ...}} +``` + +--- + +## 6. 에러 처리 + +### 6.1 에러 응답 형식 + +```json +{ + "success": false, + "message": "error.validation_failed", + "errors": { + "page_name": ["페이지명은 필수입니다."], + "item_type": ["올바른 품목 유형을 선택하세요."] + } +} +``` + +### 6.2 주요 에러 코드 + +| HTTP 상태 | message 키 | 설명 | +|----------|-----------|------| +| 400 | error.validation_failed | 유효성 검증 실패 | +| 401 | error.unauthorized | 인증 실패 | +| 403 | error.forbidden | 권한 없음 | +| 404 | error.not_found | 리소스를 찾을 수 없음 | +| 422 | error.unprocessable | 처리할 수 없는 요청 | +| 500 | error.internal_server | 서버 내부 오류 | + +### 6.3 Tenant 격리 에러 + +```json +{ + "success": false, + "message": "error.forbidden", + "errors": { + "tenant_id": ["다른 테넌트의 리소스에 접근할 수 없습니다."] + } +} +``` + +**참고**: `BelongsToTenant` 스코프가 자동으로 처리하므로 404 반환 + +--- + +## 7. 구현 우선순위 + +### 🔴 우선순위 1 (필수 - 화면 기본 동작) + +1. **초기화 API**: `GET /v1/item-master/init` +2. **페이지 CRUD**: + - `GET /v1/item-master/pages` + - `POST /v1/item-master/pages` + - `PUT /v1/item-master/pages/{id}` + - `DELETE /v1/item-master/pages/{id}` +3. **섹션 CRUD**: + - `POST /v1/item-master/pages/{pageId}/sections` + - `PUT /v1/item-master/sections/{id}` + - `DELETE /v1/item-master/sections/{id}` +4. **필드 CRUD**: + - `POST /v1/item-master/sections/{sectionId}/fields` + - `PUT /v1/item-master/fields/{id}` + - `DELETE /v1/item-master/fields/{id}` + +### 🟡 우선순위 2 (중요 - 핵심 기능) + +5. **BOM 관리**: + - `POST /v1/item-master/sections/{sectionId}/bom-items` + - `PUT /v1/item-master/bom-items/{id}` + - `DELETE /v1/item-master/bom-items/{id}` +6. **순서 변경**: + - `PUT /v1/item-master/pages/{pageId}/sections/reorder` + - `PUT /v1/item-master/sections/{sectionId}/fields/reorder` +7. **단위 관리**: + - `GET /v1/item-master/units` + - `POST /v1/item-master/units` + - `DELETE /v1/item-master/units/{id}` + +### 🟢 우선순위 3 (부가 기능) + +8. **섹션 템플릿**: 전체 CRUD +9. **마스터 필드**: 전체 CRUD +10. **커스텀 탭**: 전체 CRUD + 컬럼 설정 + +--- + +## 📌 참고 사항 + +### 1. Cascade 삭제 정책 +- 페이지 삭제 시 → 하위 섹션/필드 모두 Soft Delete +- 섹션 삭제 시 → 하위 필드 모두 Soft Delete +- 모두 `deleted_at` 업데이트로 처리 + +### 2. order_no 자동 계산 +- 새로운 섹션/필드 생성 시 자동으로 마지막 순서 + 1 +- 프론트엔드에서 order_no 전달 불필요 + +### 3. Nested 조회 최적화 +- `GET /v1/item-master/pages`: with(['sections.fields', 'sections.bomItems']) +- Eager Loading으로 N+1 문제 방지 + +### 4. 감사 로그 +- 모든 생성/수정/삭제 시 `audit_logs` 기록 +- `action`: created, updated, deleted +- `before`, `after` 필드에 변경 전후 데이터 JSON 저장 + +### 5. i18n 메시지 키 +```php +// lang/ko/message.php +return [ + 'fetched' => '조회되었습니다.', + 'created' => '생성되었습니다.', + 'updated' => '수정되었습니다.', + 'deleted' => '삭제되었습니다.', + 'reordered' => '순서가 변경되었습니다.', +]; +``` + +--- + +## ✅ 체크리스트 + +백엔드 개발 완료 전 확인사항: + +``` +□ Service-First 패턴 적용 (Controller는 DI + Service 호출만) +□ BelongsToTenant scope 모든 모델에 적용 +□ SoftDeletes 모든 모델에 적용 +□ 공통 컬럼 (tenant_id, created_by, updated_by, deleted_by) 포함 +□ 감사 로그 생성/수정/삭제 시 기록 +□ i18n 메시지 키 사용 (__('message.xxx')) +□ FormRequest 검증 +□ Swagger 문서화 (app/Swagger/v1/ItemMasterApi.php) +□ Cascade 삭제 정책 적용 +□ Nested 조회 최적화 (Eager Loading) +□ order_no 자동 계산 로직 +□ 실시간 저장 지원 (일괄 저장 없음) +``` + +--- + +## 📞 문의 + +**프론트엔드 개발팀**: [연락처] +**백엔드 개발팀**: [연락처] + +--- + +**문서 버전**: v1.0 +**작성일**: 2025-11-20 +**다음 리뷰 예정일**: 백엔드 구현 완료 후 diff --git a/history/2025-11/front-requests/[API-2025-11-25] item-master-data-management-api-request.md b/history/2025-11/front-requests/[API-2025-11-25] item-master-data-management-api-request.md new file mode 100644 index 0000000..ccc09db --- /dev/null +++ b/history/2025-11/front-requests/[API-2025-11-25] item-master-data-management-api-request.md @@ -0,0 +1,841 @@ +# 품목기준관리 API 요청서 + +**작성일**: 2025-11-25 +**요청자**: 프론트엔드 개발팀 +**대상**: 백엔드 개발팀 +**프로젝트**: SAM MES System - 품목기준관리 (Item Master Data Management) + +--- + +## 1. 개요 + +### 1.1 목적 +품목기준관리 화면에서 품목의 메타데이터(페이지, 섹션, 필드)를 동적으로 정의하기 위한 백엔드 API 개발 요청 + +### 1.2 프론트엔드 구현 현황 +- 프론트엔드 UI 구현 완료 +- API 클라이언트 코드 작성 완료 (`src/lib/api/item-master.ts`) +- 타입 정의 완료 (`src/types/item-master-api.ts`) +- Next.js API 프록시 구조 적용 (HttpOnly 쿠키 인증) + +### 1.3 API 기본 정보 +| 항목 | 값 | +|------|-----| +| Base URL | `/api/v1/item-master` | +| 인증 방식 | `auth.apikey + auth:sanctum` (HttpOnly Cookie) | +| Content-Type | `application/json` | +| 응답 형식 | 표준 API 응답 래퍼 사용 | + +### 1.4 표준 응답 형식 +```json +{ + "success": true, + "message": "message.fetched", + "data": { ... } +} +``` + +--- + +## 2. 필수 API 엔드포인트 + +### 2.1 초기화 API (최우선) + +#### `GET /api/v1/item-master/init` + +**목적**: 화면 진입 시 전체 데이터를 한 번에 로드 + +**Request**: 없음 (JWT에서 tenant_id 자동 추출) + +**Response**: +```typescript +interface InitResponse { + pages: ItemPageResponse[]; // 페이지 목록 (섹션, 필드 포함) + sectionTemplates: SectionTemplateResponse[]; // 섹션 템플릿 목록 + masterFields: MasterFieldResponse[]; // 마스터 필드 목록 + customTabs: CustomTabResponse[]; // 커스텀 탭 목록 + tabColumns: Record; // 탭별 컬럼 설정 + unitOptions: UnitOptionResponse[]; // 단위 옵션 목록 +} +``` + +**중요**: `pages` 응답 시 `sections`와 `fields`를 Nested로 포함해야 함 + +**예시 응답**: +```json +{ + "success": true, + "message": "message.fetched", + "data": { + "pages": [ + { + "id": 1, + "page_name": "기본정보", + "item_type": "FG", + "is_active": true, + "sections": [ + { + "id": 1, + "title": "품목코드 정보", + "type": "fields", + "order_no": 1, + "fields": [ + { + "id": 1, + "field_name": "품목코드", + "field_type": "textbox", + "is_required": true, + "master_field_id": null, + "order_no": 1 + } + ] + } + ] + } + ], + "sectionTemplates": [...], + "masterFields": [...], + "customTabs": [...], + "tabColumns": {...}, + "unitOptions": [...] + } +} +``` + +--- + +### 2.2 페이지 관리 API + +#### `POST /api/v1/item-master/pages` +**목적**: 새 페이지 생성 + +**Request Body**: +```typescript +interface ItemPageRequest { + page_name: string; // 페이지명 (필수) + item_type: 'FG' | 'PT' | 'SM' | 'RM' | 'CS'; // 품목유형 (필수) + absolute_path?: string; // 절대경로 (선택) + is_active?: boolean; // 활성화 여부 (기본: true) +} +``` + +**Response**: `ItemPageResponse` + +--- + +#### `PUT /api/v1/item-master/pages/{id}` +**목적**: 페이지 수정 + +**Path Parameter**: `id` - 페이지 ID + +**Request Body**: `Partial` + +**Response**: `ItemPageResponse` + +--- + +#### `DELETE /api/v1/item-master/pages/{id}` +**목적**: 페이지 삭제 (Soft Delete) + +**Path Parameter**: `id` - 페이지 ID + +**Response**: `{ success: true, message: "message.deleted" }` + +--- + +### 2.3 섹션 관리 API + +#### `POST /api/v1/item-master/pages/{pageId}/sections` +**목적**: 페이지에 새 섹션 추가 + +**Path Parameter**: `pageId` - 페이지 ID + +**Request Body**: +```typescript +interface ItemSectionRequest { + title: string; // 섹션명 (필수) + type: 'fields' | 'bom'; // 섹션 타입 (필수) + template_id?: number; // 템플릿 ID (선택) - 템플릿에서 생성 시 +} +``` + +**중요 - 템플릿 적용 로직**: +- `template_id`가 전달되면 해당 템플릿의 필드들을 복사하여 새 섹션에 추가 +- 템플릿의 필드들은 `master_field_id` 연결 관계도 복사 + +**Response**: `ItemSectionResponse` (생성된 섹션 + 필드 포함) + +--- + +#### `PUT /api/v1/item-master/sections/{id}` +**목적**: 섹션 수정 (제목 변경 등) + +**Path Parameter**: `id` - 섹션 ID + +**Request Body**: `Partial` + +**Response**: `ItemSectionResponse` + +--- + +#### `DELETE /api/v1/item-master/sections/{id}` +**목적**: 섹션 삭제 + +**Path Parameter**: `id` - 섹션 ID + +**Response**: `{ success: true, message: "message.deleted" }` + +--- + +#### `PUT /api/v1/item-master/pages/{pageId}/sections/reorder` +**목적**: 섹션 순서 변경 (드래그앤드롭) + +**Path Parameter**: `pageId` - 페이지 ID + +**Request Body**: +```typescript +interface SectionReorderRequest { + section_orders: Array<{ + id: number; // 섹션 ID + order_no: number; // 새 순서 + }>; +} +``` + +**Response**: `ItemSectionResponse[]` + +--- + +### 2.4 필드 관리 API + +#### `POST /api/v1/item-master/sections/{sectionId}/fields` +**목적**: 섹션에 새 필드 추가 + +**Path Parameter**: `sectionId` - 섹션 ID + +**Request Body**: +```typescript +interface ItemFieldRequest { + field_name: string; // 필드명 (필수) + field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'; // 필드 타입 (필수) + + // 마스터 필드 연결 (핵심 기능) + master_field_id?: number; // 마스터 필드 ID (마스터에서 선택한 경우) + + // 선택 속성 + is_required?: boolean; + placeholder?: string; + default_value?: string; + options?: Array<{ label: string; value: string }>; // dropdown 옵션 + validation_rules?: Record; + properties?: Record; + + // 조건부 표시 설정 (신규 기능) + display_condition?: { + field_key: string; // 조건 필드 키 + expected_value: string; // 예상 값 + target_field_ids?: string[]; // 표시할 필드 ID 목록 + target_section_ids?: string[]; // 표시할 섹션 ID 목록 + }[]; +} +``` + +**중요 - master_field_id 처리**: +- 프론트엔드에서 "마스터 항목 선택" 모드로 필드 추가 시 `master_field_id` 전달 +- 백엔드에서 해당 마스터 필드의 속성을 참조하여 기본값 설정 +- 마스터 필드가 수정되면 연결된 필드도 동기화 필요 (옵션) + +**Response**: `ItemFieldResponse` + +--- + +#### `PUT /api/v1/item-master/fields/{id}` +**목적**: 필드 수정 + +**Path Parameter**: `id` - 필드 ID + +**Request Body**: `Partial` + +**Response**: `ItemFieldResponse` + +--- + +#### `DELETE /api/v1/item-master/fields/{id}` +**목적**: 필드 삭제 + +**Path Parameter**: `id` - 필드 ID + +**Response**: `{ success: true, message: "message.deleted" }` + +--- + +#### `PUT /api/v1/item-master/sections/{sectionId}/fields/reorder` +**목적**: 필드 순서 변경 (드래그앤드롭) + +**Path Parameter**: `sectionId` - 섹션 ID + +**Request Body**: +```typescript +interface FieldReorderRequest { + field_orders: Array<{ + id: number; // 필드 ID + order_no: number; // 새 순서 + }>; +} +``` + +**Response**: `ItemFieldResponse[]` + +--- + +### 2.5 섹션 템플릿 API + +#### `GET /api/v1/item-master/section-templates` +**목적**: 섹션 템플릿 목록 조회 + +**Response**: `SectionTemplateResponse[]` + +--- + +#### `POST /api/v1/item-master/section-templates` +**목적**: 새 섹션 템플릿 생성 + +**Request Body**: +```typescript +interface SectionTemplateRequest { + title: string; // 템플릿명 (필수) + type: 'fields' | 'bom'; // 타입 (필수) + description?: string; // 설명 (선택) + is_default?: boolean; // 기본 템플릿 여부 (선택) + + // 템플릿에 포함될 필드들 + fields?: Array<{ + field_name: string; + field_type: string; + master_field_id?: number; + is_required?: boolean; + options?: Array<{ label: string; value: string }>; + properties?: Record; + }>; +} +``` + +**Response**: `SectionTemplateResponse` + +--- + +#### `PUT /api/v1/item-master/section-templates/{id}` +**목적**: 섹션 템플릿 수정 + +**Response**: `SectionTemplateResponse` + +--- + +#### `DELETE /api/v1/item-master/section-templates/{id}` +**목적**: 섹션 템플릿 삭제 + +**Response**: `{ success: true, message: "message.deleted" }` + +--- + +### 2.6 마스터 필드 API + +#### `GET /api/v1/item-master/master-fields` +**목적**: 마스터 필드 목록 조회 + +**Response**: `MasterFieldResponse[]` + +--- + +#### `POST /api/v1/item-master/master-fields` +**목적**: 새 마스터 필드 생성 + +**Request Body**: +```typescript +interface MasterFieldRequest { + field_name: string; // 필드명 (필수) + field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'; // 필드 타입 (필수) + category?: string; // 카테고리 (선택) - 예: "기본정보", "스펙정보" + description?: string; // 설명 (선택) + is_common?: boolean; // 공통 항목 여부 (선택) + default_value?: string; + options?: Array<{ label: string; value: string }>; + validation_rules?: Record; + properties?: Record; +} +``` + +**Response**: `MasterFieldResponse` + +--- + +#### `PUT /api/v1/item-master/master-fields/{id}` +**목적**: 마스터 필드 수정 + +**Response**: `MasterFieldResponse` + +--- + +#### `DELETE /api/v1/item-master/master-fields/{id}` +**목적**: 마스터 필드 삭제 + +**주의**: 해당 마스터 필드를 참조하는 필드(`master_field_id`)가 있을 경우 처리 방안 필요 +- 옵션 1: 삭제 불가 (참조 무결성) +- 옵션 2: 참조 해제 후 삭제 + +**Response**: `{ success: true, message: "message.deleted" }` + +--- + +### 2.7 BOM 관리 API + +#### `POST /api/v1/item-master/sections/{sectionId}/bom-items` +**목적**: BOM 항목 추가 + +**Request Body**: +```typescript +interface BomItemRequest { + item_code?: string; + item_name: string; // 필수 + quantity: number; // 필수 + unit?: string; + unit_price?: number; + total_price?: number; + spec?: string; + note?: string; +} +``` + +**Response**: `BomItemResponse` + +--- + +#### `PUT /api/v1/item-master/bom-items/{id}` +**목적**: BOM 항목 수정 + +**Response**: `BomItemResponse` + +--- + +#### `DELETE /api/v1/item-master/bom-items/{id}` +**목적**: BOM 항목 삭제 + +**Response**: `{ success: true, message: "message.deleted" }` + +--- + +### 2.8 커스텀 탭 API + +#### `GET /api/v1/item-master/custom-tabs` +**목적**: 커스텀 탭 목록 조회 + +**Response**: `CustomTabResponse[]` + +--- + +#### `POST /api/v1/item-master/custom-tabs` +**목적**: 새 커스텀 탭 생성 + +**Request Body**: +```typescript +interface CustomTabRequest { + label: string; // 탭 레이블 (필수) + icon?: string; // 아이콘 (선택) + is_default?: boolean; // 기본 탭 여부 (선택) +} +``` + +**Response**: `CustomTabResponse` + +--- + +#### `PUT /api/v1/item-master/custom-tabs/{id}` +**목적**: 커스텀 탭 수정 + +**Response**: `CustomTabResponse` + +--- + +#### `DELETE /api/v1/item-master/custom-tabs/{id}` +**목적**: 커스텀 탭 삭제 + +**Response**: `{ success: true, message: "message.deleted" }` + +--- + +#### `PUT /api/v1/item-master/custom-tabs/reorder` +**목적**: 탭 순서 변경 + +**Request Body**: +```typescript +interface TabReorderRequest { + tab_orders: Array<{ + id: number; + order_no: number; + }>; +} +``` + +**Response**: `{ success: true }` + +--- + +#### `PUT /api/v1/item-master/custom-tabs/{id}/columns` +**목적**: 탭별 컬럼 설정 업데이트 + +**Request Body**: +```typescript +interface TabColumnUpdateRequest { + columns: Array<{ + key: string; + label: string; + visible: boolean; + order: number; + }>; +} +``` + +**Response**: `TabColumnResponse[]` + +--- + +### 2.9 단위 옵션 API + +#### `GET /api/v1/item-master/unit-options` +**목적**: 단위 옵션 목록 조회 + +**Response**: `UnitOptionResponse[]` + +--- + +#### `POST /api/v1/item-master/unit-options` +**목적**: 새 단위 옵션 추가 + +**Request Body**: +```typescript +interface UnitOptionRequest { + label: string; // 표시명 (예: "개") + value: string; // 값 (예: "EA") +} +``` + +**Response**: `UnitOptionResponse` + +--- + +#### `DELETE /api/v1/item-master/unit-options/{id}` +**목적**: 단위 옵션 삭제 + +**Response**: `{ success: true, message: "message.deleted" }` + +--- + +## 3. 데이터베이스 스키마 제안 + +### 3.1 item_master_pages +```sql +CREATE TABLE item_master_pages ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL, + page_name VARCHAR(100) NOT NULL, + item_type ENUM('FG', 'PT', 'SM', 'RM', 'CS') NOT NULL, + absolute_path VARCHAR(500) NULL, + order_no INT NOT NULL DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant (tenant_id), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) +); +``` + +### 3.2 item_master_sections +```sql +CREATE TABLE item_master_sections ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL, + page_id BIGINT UNSIGNED NOT NULL, + title VARCHAR(100) NOT NULL, + type ENUM('fields', 'bom') NOT NULL DEFAULT 'fields', + order_no INT NOT NULL DEFAULT 0, + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_page (tenant_id, page_id), + FOREIGN KEY (tenant_id) REFERENCES tenants(id), + FOREIGN KEY (page_id) REFERENCES item_master_pages(id) ON DELETE CASCADE +); +``` + +### 3.3 item_master_fields +```sql +CREATE TABLE item_master_fields ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL, + section_id BIGINT UNSIGNED NOT NULL, + master_field_id BIGINT UNSIGNED NULL, -- 마스터 필드 참조 + field_name VARCHAR(100) NOT NULL, + field_type ENUM('textbox', 'number', 'dropdown', 'checkbox', 'date', 'textarea') NOT NULL, + order_no INT NOT NULL DEFAULT 0, + is_required BOOLEAN NOT NULL DEFAULT FALSE, + placeholder VARCHAR(200) NULL, + default_value VARCHAR(500) NULL, + display_condition JSON NULL, -- 조건부 표시 설정 + validation_rules JSON NULL, + options JSON NULL, -- dropdown 옵션 + properties JSON NULL, -- 추가 속성 (컬럼 설정 등) + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_section (tenant_id, section_id), + INDEX idx_master_field (master_field_id), + FOREIGN KEY (tenant_id) REFERENCES tenants(id), + FOREIGN KEY (section_id) REFERENCES item_master_sections(id) ON DELETE CASCADE, + FOREIGN KEY (master_field_id) REFERENCES item_master_master_fields(id) ON DELETE SET NULL +); +``` + +### 3.4 item_master_master_fields (마스터 필드) +```sql +CREATE TABLE item_master_master_fields ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL, + field_name VARCHAR(100) NOT NULL, + field_type ENUM('textbox', 'number', 'dropdown', 'checkbox', 'date', 'textarea') NOT NULL, + category VARCHAR(50) NULL, + description TEXT NULL, + is_common BOOLEAN NOT NULL DEFAULT FALSE, + default_value VARCHAR(500) NULL, + options JSON NULL, + validation_rules JSON NULL, + properties JSON NULL, + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant (tenant_id), + INDEX idx_category (tenant_id, category), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) +); +``` + +### 3.5 item_master_section_templates (섹션 템플릿) +```sql +CREATE TABLE item_master_section_templates ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL, + title VARCHAR(100) NOT NULL, + type ENUM('fields', 'bom') NOT NULL DEFAULT 'fields', + description TEXT NULL, + is_default BOOLEAN NOT NULL DEFAULT FALSE, + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant (tenant_id), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) +); +``` + +### 3.6 item_master_template_fields (템플릿 필드) +```sql +CREATE TABLE item_master_template_fields ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL, + template_id BIGINT UNSIGNED NOT NULL, + master_field_id BIGINT UNSIGNED NULL, + field_name VARCHAR(100) NOT NULL, + field_type ENUM('textbox', 'number', 'dropdown', 'checkbox', 'date', 'textarea') NOT NULL, + order_no INT NOT NULL DEFAULT 0, + is_required BOOLEAN NOT NULL DEFAULT FALSE, + placeholder VARCHAR(200) NULL, + default_value VARCHAR(500) NULL, + options JSON NULL, + properties JSON NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + + INDEX idx_template (template_id), + FOREIGN KEY (tenant_id) REFERENCES tenants(id), + FOREIGN KEY (template_id) REFERENCES item_master_section_templates(id) ON DELETE CASCADE, + FOREIGN KEY (master_field_id) REFERENCES item_master_master_fields(id) ON DELETE SET NULL +); +``` + +--- + +## 4. 핵심 비즈니스 로직 + +### 4.1 마스터 필드 연결 (`master_field_id`) + +**시나리오**: 사용자가 필드 추가 시 "마스터 항목 선택" 모드로 추가 + +**프론트엔드 동작**: +1. 마스터 필드 목록에서 선택 +2. 선택된 마스터 필드의 속성을 폼에 자동 채움 +3. 저장 시 `master_field_id` 포함하여 전송 + +**백엔드 처리**: +```php +// ItemFieldService.php +public function create(int $sectionId, array $data): ItemField +{ + // master_field_id가 있으면 마스터 필드에서 기본값 가져오기 + if (!empty($data['master_field_id'])) { + $masterField = MasterField::findOrFail($data['master_field_id']); + + // 마스터 필드의 속성을 기본값으로 사용 (명시적 값이 없는 경우) + $data = array_merge([ + 'field_type' => $masterField->field_type, + 'options' => $masterField->options, + 'validation_rules' => $masterField->validation_rules, + 'properties' => $masterField->properties, + ], $data); + } + + return ItemField::create($data); +} +``` + +### 4.2 섹션 템플릿 적용 + +**시나리오**: 사용자가 섹션 추가 시 "템플릿에서 선택" 모드로 추가 + +**프론트엔드 동작**: +1. 템플릿 목록에서 선택 +2. 선택된 템플릿 정보로 섹션 생성 요청 +3. `template_id` 포함하여 전송 + +**백엔드 처리**: +```php +// ItemSectionService.php +public function create(int $pageId, array $data): ItemSection +{ + $section = ItemSection::create([ + 'page_id' => $pageId, + 'title' => $data['title'], + 'type' => $data['type'], + ]); + + // template_id가 있으면 템플릿의 필드들을 복사 + if (!empty($data['template_id'])) { + $templateFields = TemplateField::where('template_id', $data['template_id']) + ->orderBy('order_no') + ->get(); + + foreach ($templateFields as $index => $tf) { + ItemField::create([ + 'section_id' => $section->id, + 'master_field_id' => $tf->master_field_id, // 마스터 연결 유지 + 'field_name' => $tf->field_name, + 'field_type' => $tf->field_type, + 'order_no' => $index, + 'is_required' => $tf->is_required, + 'options' => $tf->options, + 'properties' => $tf->properties, + ]); + } + } + + return $section->load('fields'); +} +``` + +### 4.3 조건부 표시 설정 + +**JSON 구조**: +```json +{ + "display_condition": [ + { + "field_key": "item_type", + "expected_value": "FG", + "target_field_ids": ["5", "6", "7"] + }, + { + "field_key": "item_type", + "expected_value": "PT", + "target_section_ids": ["3"] + } + ] +} +``` + +**활용**: 프론트엔드에서 품목 데이터 입력 시 해당 조건에 따라 필드/섹션을 동적으로 표시/숨김 + +--- + +## 5. 우선순위 + +### Phase 1 (필수 - 즉시) +1. `GET /api/v1/item-master/init` - 초기화 API +2. 페이지 CRUD API +3. 섹션 CRUD API (순서변경 포함) +4. 필드 CRUD API (순서변경 포함, `master_field_id` 지원) + +### Phase 2 (중요 - 1주 내) +5. 마스터 필드 CRUD API +6. 섹션 템플릿 CRUD API +7. 템플릿 필드 관리 + +### Phase 3 (선택 - 2주 내) +8. BOM 항목 관리 API +9. 커스텀 탭 API +10. 단위 옵션 API + +--- + +## 6. 참고 사항 + +### 6.1 프론트엔드 코드 위치 +- API 클라이언트: `src/lib/api/item-master.ts` +- 타입 정의: `src/types/item-master-api.ts` +- 메인 컴포넌트: `src/components/items/ItemMasterDataManagement.tsx` + +### 6.2 기존 API 문서 +- `claudedocs/[API-2025-11-24] item-management-dynamic-api-spec.md` - 품목관리 동적 화면 API + +### 6.3 Multi-Tenancy +- 모든 테이블에 `tenant_id` 컬럼 필수 +- JWT에서 tenant_id 자동 추출 +- `BelongsToTenant` Trait 적용 필요 + +### 6.4 에러 응답 형식 +```json +{ + "success": false, + "message": "error.validation_failed", + "errors": { + "field_name": ["필드명은 필수입니다."], + "field_type": ["유효하지 않은 필드 타입입니다."] + } +} +``` + +--- + +## 7. 연락처 + +질문이나 협의 사항이 있으면 언제든 연락 바랍니다. + +**프론트엔드 담당**: [담당자명] +**작성일**: 2025-11-25 \ No newline at end of file diff --git a/history/2025-11/front-requests/[API-2025-11-26] item-master-api-changes.md b/history/2025-11/front-requests/[API-2025-11-26] item-master-api-changes.md new file mode 100644 index 0000000..d2eeb40 --- /dev/null +++ b/history/2025-11/front-requests/[API-2025-11-26] item-master-api-changes.md @@ -0,0 +1,120 @@ +# Item Master API 변경사항 + +**작성일**: 2025-11-26 +**대상**: 프론트엔드 개발팀 +**관련 문서**: `[API-2025-11-25] item-master-data-management-api-request.md` + +--- + +## 구조 변경 + +**`section_templates` 테이블 삭제** → `item_sections`의 `is_template=true`로 통합 + +--- + +## 변경된 API + +### 섹션 템플릿 필드/BOM API + +| 요청서 | 실제 구현 | +|--------|----------| +| `POST /section-templates/{id}/fields` | `POST /sections/{id}/fields` | +| `POST /section-templates/{id}/bom-items` | `POST /sections/{id}/bom-items` | + +→ 템플릿도 섹션이므로 동일 API 사용 + +--- + +## 신규 API + +### 1. 독립 섹션 API + +| API | 설명 | +|-----|------| +| `GET /sections?is_template=true` | 템플릿 목록 조회 | +| `GET /sections?is_template=false` | 일반 섹션 목록 | +| `POST /sections` | 독립 섹션 생성 | +| `POST /sections/{id}/clone` | 섹션 복제 | +| `GET /sections/{id}/usage` | 사용처 조회 (어느 페이지에서 사용중인지) | + +**Request** (`POST /sections`): +```json +{ + "group_id": 1, + "title": "섹션명", + "type": "fields|bom", + "is_template": false, + "is_default": false, + "description": null +} +``` + +### 2. 독립 필드 API + +| API | 설명 | +|-----|------| +| `GET /fields` | 필드 목록 | +| `POST /fields` | 독립 필드 생성 | +| `POST /fields/{id}/clone` | 필드 복제 | +| `GET /fields/{id}/usage` | 사용처 조회 | + +**Request** (`POST /fields`): +```json +{ + "group_id": 1, + "field_name": "필드명", + "field_type": "textbox|number|dropdown|checkbox|date|textarea", + "is_required": false, + "default_value": null, + "placeholder": null, + "options": [], + "properties": [] +} +``` + +### 3. 독립 BOM API + +| API | 설명 | +|-----|------| +| `GET /bom-items` | BOM 목록 | +| `POST /bom-items` | 독립 BOM 생성 | + +**Request** (`POST /bom-items`): +```json +{ + "group_id": 1, + "item_code": null, + "item_name": "품목명", + "quantity": 0, + "unit": null, + "unit_price": 0, + "spec": null, + "note": null +} +``` + +### 4. 링크 관리 API + +| API | 설명 | +|-----|------| +| `POST /pages/{id}/link-section` | 페이지에 섹션 연결 | +| `DELETE /pages/{id}/unlink-section/{sectionId}` | 연결 해제 | +| `POST /sections/{id}/link-field` | 섹션에 필드 연결 | +| `DELETE /sections/{id}/unlink-field/{fieldId}` | 연결 해제 | +| `GET /pages/{id}/structure` | 페이지 전체 구조 조회 | + +**Request** (link 계열): +```json +{ + "target_id": 1, + "order_no": 0 +} +``` + +**Response** (usage 계열): +```json +{ + "used_in_pages": [{ "id": 1, "page_name": "기본정보" }], + "used_in_sections": [{ "id": 2, "title": "스펙정보" }] +} +``` diff --git a/history/2025-11/front-requests/[API-REQUEST-2025-11-25] section-template-fields-api.md b/history/2025-11/front-requests/[API-REQUEST-2025-11-25] section-template-fields-api.md new file mode 100644 index 0000000..493dd79 --- /dev/null +++ b/history/2025-11/front-requests/[API-REQUEST-2025-11-25] section-template-fields-api.md @@ -0,0 +1,588 @@ +# 품목기준관리 API 추가 요청 - 섹션 템플릿 하위 데이터 + +**요청일**: 2025-11-25 +**버전**: v1.1 +**작성자**: 프론트엔드 개발팀 +**수신**: 백엔드 개발팀 +**긴급도**: 🔴 높음 + +--- + +## 📋 목차 + +1. [요청 배경](#1-요청-배경) +2. [데이터베이스 테이블 추가](#2-데이터베이스-테이블-추가) +3. [API 엔드포인트 추가](#3-api-엔드포인트-추가) +4. [init API 응답 수정](#4-init-api-응답-수정) +5. [구현 우선순위](#5-구현-우선순위) + +--- + +## 1. 요청 배경 + +### 1.1 문제 상황 +- 섹션탭 > 일반 섹션에 항목(필드) 추가 후 **새로고침 시 데이터 사라짐** +- 섹션탭 > 모듈 섹션(BOM)에 BOM 품목 추가 후 **새로고침 시 데이터 사라짐** +- 원인: 섹션 템플릿 하위 데이터를 저장/조회하는 API 없음 + +### 1.2 현재 상태 비교 + +| 구분 | 계층구조 (정상) | 섹션 템플릿 (문제) | +|------|----------------|-------------------| +| 섹션/템플릿 CRUD | ✅ 있음 | ✅ 있음 | +| 필드 CRUD | ✅ `/sections/{id}/fields` | ❌ **없음** | +| BOM 품목 CRUD | ✅ `/sections/{id}/bom-items` | ❌ **없음** | +| init 응답에 중첩 포함 | ✅ `fields`, `bomItems` 포함 | ❌ **미포함** | + +### 1.3 요청 내용 +1. 섹션 템플릿 필드 테이블 및 CRUD API 추가 +2. 섹션 템플릿 BOM 품목 테이블 및 CRUD API 추가 +3. init API 응답에 섹션 템플릿 하위 데이터 중첩 포함 +4. **🔴 [추가] 계층구조 섹션 ↔ 섹션 템플릿 데이터 동기화** + +--- + +## 2. 데이터베이스 테이블 추가 + +### 2.0 section_templates 테이블 수정 (데이터 동기화용) + +**요구사항**: 계층구조에서 생성한 섹션과 섹션탭의 템플릿이 **동일한 데이터**로 연동되어야 함 + +**현재 문제**: +``` +계층구조 섹션 생성 시: +├── item_sections 테이블에 저장 (id: 1) +└── section_templates 테이블에 저장 (id: 1) + → 두 개의 별도 데이터! 연결 없음! +``` + +**해결 방안**: `section_templates`에 `section_id` 컬럼 추가 + +```sql +ALTER TABLE section_templates +ADD COLUMN section_id BIGINT UNSIGNED NULL COMMENT '연결된 계층구조 섹션 ID (동기화용)' AFTER tenant_id, +ADD INDEX idx_section_id (section_id), +ADD FOREIGN KEY (section_id) REFERENCES item_sections(id) ON DELETE SET NULL; +``` + +**동기화 동작**: +| 액션 | 동작 | +|------|------| +| 계층구조에서 섹션 생성 | `item_sections` + `section_templates` 생성, `section_id`로 연결 | +| 계층구조에서 섹션 수정 | `item_sections` 수정 → 연결된 `section_templates`도 수정 | +| 계층구조에서 섹션 삭제 | `item_sections` 삭제 → 연결된 `section_templates`의 `section_id` = NULL | +| 섹션탭에서 템플릿 수정 | `section_templates` 수정 → 연결된 `item_sections`도 수정 | +| 섹션탭에서 템플릿 삭제 | `section_templates` 삭제 → 연결된 `item_sections`는 유지 | + +**init API 응답 수정** (section_id 포함): +```json +{ + "sectionTemplates": [ + { + "id": 1, + "section_id": 5, // 연결된 계층구조 섹션 ID (없으면 null) + "title": "일반 섹션", + "type": "fields", + ... + } + ] +} +``` + +--- + +### 2.1 section_template_fields (섹션 템플릿 필드) + +**참고**: 기존 `item_fields` 테이블 구조와 유사하게 설계 + +```sql +CREATE TABLE section_template_fields ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', + template_id BIGINT UNSIGNED NOT NULL COMMENT '섹션 템플릿 ID', + field_name VARCHAR(255) NOT NULL COMMENT '필드명', + field_key VARCHAR(100) NOT NULL COMMENT '필드 키 (영문)', + field_type ENUM('textbox', 'number', 'dropdown', 'checkbox', 'date', 'textarea') NOT NULL COMMENT '필드 타입', + order_no INT NOT NULL DEFAULT 0 COMMENT '정렬 순서', + is_required TINYINT(1) DEFAULT 0 COMMENT '필수 여부', + options JSON NULL COMMENT '드롭다운 옵션 ["옵션1", "옵션2"]', + multi_column TINYINT(1) DEFAULT 0 COMMENT '다중 컬럼 여부', + column_count INT NULL COMMENT '컬럼 수', + column_names JSON NULL COMMENT '컬럼명 목록 ["컬럼1", "컬럼2"]', + description TEXT NULL COMMENT '설명', + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_template (tenant_id, template_id), + INDEX idx_order (template_id, order_no), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (template_id) REFERENCES section_templates(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='섹션 템플릿 필드'; +``` + +### 2.2 section_template_bom_items (섹션 템플릿 BOM 품목) + +**참고**: 기존 `item_bom_items` 테이블 구조와 유사하게 설계 + +```sql +CREATE TABLE section_template_bom_items ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', + template_id BIGINT UNSIGNED NOT NULL COMMENT '섹션 템플릿 ID', + item_code VARCHAR(100) NULL COMMENT '품목 코드', + item_name VARCHAR(255) NOT NULL COMMENT '품목명', + quantity DECIMAL(15, 4) NOT NULL DEFAULT 0 COMMENT '수량', + unit VARCHAR(50) NULL COMMENT '단위', + unit_price DECIMAL(15, 2) NULL COMMENT '단가', + total_price DECIMAL(15, 2) NULL COMMENT '총액', + spec TEXT NULL COMMENT '규격/사양', + note TEXT NULL COMMENT '비고', + order_no INT NOT NULL DEFAULT 0 COMMENT '정렬 순서', + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_template (tenant_id, template_id), + INDEX idx_order (template_id, order_no), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (template_id) REFERENCES section_templates(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='섹션 템플릿 BOM 품목'; +``` + +--- + +## 3. API 엔드포인트 추가 + +### 3.1 섹션 템플릿 필드 관리 (우선순위 1) + +#### `POST /v1/item-master/section-templates/{templateId}/fields` +**목적**: 템플릿 필드 생성 + +**Request Body**: +```json +{ + "field_name": "품목코드", + "field_key": "item_code", + "field_type": "textbox", + "is_required": true, + "options": null, + "multi_column": false, + "column_count": null, + "column_names": null, + "description": "품목 고유 코드" +} +``` + +**Validation**: +- `field_name`: required, string, max:255 +- `field_key`: required, string, max:100, alpha_dash +- `field_type`: required, in:textbox,number,dropdown,checkbox,date,textarea +- `is_required`: boolean +- `options`: nullable, array (dropdown 타입일 경우) +- `multi_column`: boolean +- `column_count`: nullable, integer, min:2, max:10 +- `column_names`: nullable, array +- `description`: nullable, string + +**Response**: +```json +{ + "success": true, + "message": "message.created", + "data": { + "id": 1, + "template_id": 1, + "field_name": "품목코드", + "field_key": "item_code", + "field_type": "textbox", + "order_no": 0, + "is_required": true, + "options": null, + "multi_column": false, + "column_count": null, + "column_names": null, + "description": "품목 고유 코드", + "created_at": "2025-11-25T10:00:00.000000Z", + "updated_at": "2025-11-25T10:00:00.000000Z" + } +} +``` + +**참고**: +- `order_no`는 자동 계산 (해당 템플릿의 마지막 필드 order + 1) + +--- + +#### `PUT /v1/item-master/section-templates/{templateId}/fields/{fieldId}` +**목적**: 템플릿 필드 수정 + +**Request Body**: +```json +{ + "field_name": "품목코드 (수정)", + "field_type": "dropdown", + "options": ["옵션1", "옵션2"], + "is_required": false +} +``` + +**Validation**: POST와 동일 (모든 필드 optional) + +**Response**: 수정된 필드 정보 반환 + +--- + +#### `DELETE /v1/item-master/section-templates/{templateId}/fields/{fieldId}` +**목적**: 템플릿 필드 삭제 (Soft Delete) + +**Request**: 없음 + +**Response**: +```json +{ + "success": true, + "message": "message.deleted" +} +``` + +--- + +#### `PUT /v1/item-master/section-templates/{templateId}/fields/reorder` +**목적**: 템플릿 필드 순서 변경 + +**Request Body**: +```json +{ + "field_orders": [ + { "id": 3, "order_no": 0 }, + { "id": 1, "order_no": 1 }, + { "id": 2, "order_no": 2 } + ] +} +``` + +**Validation**: +- `field_orders`: required, array +- `field_orders.*.id`: required, exists:section_template_fields,id +- `field_orders.*.order_no`: required, integer, min:0 + +**Response**: +```json +{ + "success": true, + "message": "message.updated", + "data": [ + { "id": 3, "order_no": 0 }, + { "id": 1, "order_no": 1 }, + { "id": 2, "order_no": 2 } + ] +} +``` + +--- + +### 3.2 섹션 템플릿 BOM 품목 관리 (우선순위 2) + +#### `POST /v1/item-master/section-templates/{templateId}/bom-items` +**목적**: 템플릿 BOM 품목 생성 + +**Request Body**: +```json +{ + "item_code": "PART-001", + "item_name": "부품 A", + "quantity": 2, + "unit": "EA", + "unit_price": 15000, + "spec": "100x50x20", + "note": "필수 부품" +} +``` + +**Validation**: +- `item_code`: nullable, string, max:100 +- `item_name`: required, string, max:255 +- `quantity`: required, numeric, min:0 +- `unit`: nullable, string, max:50 +- `unit_price`: nullable, numeric, min:0 +- `spec`: nullable, string +- `note`: nullable, string + +**Response**: +```json +{ + "success": true, + "message": "message.created", + "data": { + "id": 1, + "template_id": 2, + "item_code": "PART-001", + "item_name": "부품 A", + "quantity": 2, + "unit": "EA", + "unit_price": 15000, + "total_price": 30000, + "spec": "100x50x20", + "note": "필수 부품", + "order_no": 0, + "created_at": "2025-11-25T10:00:00.000000Z", + "updated_at": "2025-11-25T10:00:00.000000Z" + } +} +``` + +**참고**: +- `total_price`는 서버에서 자동 계산 (`quantity * unit_price`) +- `order_no`는 자동 계산 (해당 템플릿의 마지막 BOM 품목 order + 1) + +--- + +#### `PUT /v1/item-master/section-templates/{templateId}/bom-items/{itemId}` +**목적**: 템플릿 BOM 품목 수정 + +**Request Body**: +```json +{ + "item_name": "부품 A (수정)", + "quantity": 3, + "unit_price": 12000 +} +``` + +**Validation**: POST와 동일 (모든 필드 optional) + +**Response**: 수정된 BOM 품목 정보 반환 + +--- + +#### `DELETE /v1/item-master/section-templates/{templateId}/bom-items/{itemId}` +**목적**: 템플릿 BOM 품목 삭제 (Soft Delete) + +**Request**: 없음 + +**Response**: +```json +{ + "success": true, + "message": "message.deleted" +} +``` + +--- + +#### `PUT /v1/item-master/section-templates/{templateId}/bom-items/reorder` +**목적**: 템플릿 BOM 품목 순서 변경 + +**Request Body**: +```json +{ + "item_orders": [ + { "id": 3, "order_no": 0 }, + { "id": 1, "order_no": 1 }, + { "id": 2, "order_no": 2 } + ] +} +``` + +**Validation**: +- `item_orders`: required, array +- `item_orders.*.id`: required, exists:section_template_bom_items,id +- `item_orders.*.order_no`: required, integer, min:0 + +**Response**: +```json +{ + "success": true, + "message": "message.updated", + "data": [...] +} +``` + +--- + +## 4. init API 응답 수정 + +### 4.1 현재 응답 (문제) + +```json +{ + "success": true, + "data": { + "sectionTemplates": [ + { + "id": 1, + "title": "일반 섹션", + "type": "fields", + "description": null, + "is_default": false + }, + { + "id": 2, + "title": "BOM 섹션", + "type": "bom", + "description": null, + "is_default": false + } + ] + } +} +``` + +### 4.2 수정 요청 + +`sectionTemplates`에 하위 데이터 중첩 포함: + +```json +{ + "success": true, + "data": { + "sectionTemplates": [ + { + "id": 1, + "title": "일반 섹션", + "type": "fields", + "description": null, + "is_default": false, + "fields": [ + { + "id": 1, + "field_name": "품목코드", + "field_key": "item_code", + "field_type": "textbox", + "order_no": 0, + "is_required": true, + "options": null, + "multi_column": false, + "column_count": null, + "column_names": null, + "description": "품목 고유 코드" + } + ] + }, + { + "id": 2, + "title": "BOM 섹션", + "type": "bom", + "description": null, + "is_default": false, + "bomItems": [ + { + "id": 1, + "item_code": "PART-001", + "item_name": "부품 A", + "quantity": 2, + "unit": "EA", + "unit_price": 15000, + "total_price": 30000, + "spec": "100x50x20", + "note": "필수 부품", + "order_no": 0 + } + ] + } + ] + } +} +``` + +**참고**: +- `type: "fields"` 템플릿: `fields` 배열 포함 +- `type: "bom"` 템플릿: `bomItems` 배열 포함 +- 기존 `pages` 응답의 중첩 구조와 동일한 패턴 + +--- + +## 5. 구현 우선순위 + +| 우선순위 | 작업 내용 | 예상 공수 | +|---------|----------|----------| +| 🔴 0 | `section_templates`에 `section_id` 컬럼 추가 (동기화용) | 0.5일 | +| 🔴 0 | 계층구조 섹션 생성 시 `section_templates` 자동 생성 로직 | 0.5일 | +| 🔴 1 | `section_template_fields` 테이블 생성 | 0.5일 | +| 🔴 1 | 섹션 템플릿 필드 CRUD API (5개) | 1일 | +| 🔴 1 | init API 응답에 `fields` 중첩 포함 | 0.5일 | +| 🟡 2 | `section_template_bom_items` 테이블 생성 | 0.5일 | +| 🟡 2 | 섹션 템플릿 BOM 품목 CRUD API (5개) | 1일 | +| 🟡 2 | init API 응답에 `bomItems` 중첩 포함 | 0.5일 | +| 🟢 3 | 양방향 동기화 로직 (섹션↔템플릿 수정 시 상호 반영) | 1일 | +| 🟢 3 | Swagger 문서 업데이트 | 0.5일 | + +**총 예상 공수**: 백엔드 6.5일 + +--- + +## 6. 프론트엔드 연동 계획 + +### 6.1 API 완료 후 프론트엔드 작업 + +| 작업 | 설명 | 의존성 | +|------|------|--------| +| 타입 정의 수정 | `SectionTemplateResponse`에 `fields`, `bomItems`, `section_id` 추가 | init API 수정 후 | +| Context 수정 | 섹션 템플릿 필드/BOM API 호출 로직 추가 | CRUD API 완료 후 | +| 로컬 상태 제거 | `default_fields` 로컬 관리 로직 → API 연동으로 교체 | CRUD API 완료 후 | +| 동기화 UI | 계층구조↔섹션탭 간 데이터 자동 반영 | section_id 추가 후 | + +### 6.2 타입 수정 예시 + +**현재** (`src/types/item-master-api.ts`): +```typescript +export interface SectionTemplateResponse { + id: number; + title: string; + type: 'fields' | 'bom'; + description?: string; + is_default: boolean; +} +``` + +**수정 후**: +```typescript +export interface SectionTemplateResponse { + id: number; + section_id?: number | null; // 연결된 계층구조 섹션 ID + title: string; + type: 'fields' | 'bom'; + description?: string; + is_default: boolean; + fields?: SectionTemplateFieldResponse[]; // type='fields'일 때 + bomItems?: SectionTemplateBomItemResponse[]; // type='bom'일 때 +} +``` + +### 6.3 동기화 시나리오 정리 + +``` +[시나리오 1] 계층구조에서 섹션 생성 + └─ 백엔드: item_sections + section_templates 동시 생성 (section_id로 연결) + └─ 프론트: init 재조회 → 양쪽 탭에 데이터 표시 + +[시나리오 2] 계층구조에서 필드 추가/수정 + └─ 백엔드: item_fields 저장 → 연결된 section_template_fields도 동기화 + └─ 프론트: init 재조회 → 섹션탭에 필드 반영 + +[시나리오 3] 섹션탭에서 필드 추가/수정 + └─ 백엔드: section_template_fields 저장 → 연결된 item_fields도 동기화 + └─ 프론트: init 재조회 → 계층구조탭에 필드 반영 + +[시나리오 4] 섹션탭에서 독립 템플릿 생성 (section_id = null) + └─ 백엔드: section_templates만 생성 (계층구조와 무관) + └─ 프론트: 섹션탭에서만 사용 가능한 템플릿 +``` + +--- + +## 📞 문의 + +질문 있으시면 프론트엔드 팀으로 연락 주세요. + +--- + +**작성일**: 2025-11-25 +**기준 문서**: `[API-2025-11-20] item-master-specification.md` diff --git a/history/2025-11/item-master-archived/[ANALYSIS-2025-11-21] item-master-notes.md b/history/2025-11/item-master-archived/[ANALYSIS-2025-11-21] item-master-notes.md new file mode 100644 index 0000000..2b5e3f2 --- /dev/null +++ b/history/2025-11/item-master-archived/[ANALYSIS-2025-11-21] item-master-notes.md @@ -0,0 +1,1060 @@ +/** + * @OA\Tag(name="ItemMaster", description="품목기준관리 API") + * + * ======================================== + * 모델 스키마 + * ======================================== + * + * @OA\Schema( + * schema="ItemPage", + * type="object", + * + * @OA\Property(property="id", type="integer", example=1), + * @OA\Property(property="tenant_id", type="integer", example=1), + * @OA\Property(property="page_name", type="string", example="기본 정보"), + * @OA\Property(property="item_type", type="string", enum={"FG","PT","SM","RM","CS"}, example="FG"), + * @OA\Property(property="absolute_path", type="string", nullable=true, example="/items/fg/basic"), + * @OA\Property(property="is_active", type="boolean", example=true), + * @OA\Property(property="created_at", type="string", example="2025-11-20 10:00:00"), + * @OA\Property(property="updated_at", type="string", example="2025-11-20 10:00:00"), + * @OA\Property( + * property="sections", + * type="array", + * + * @OA\Items(ref="#/components/schemas/ItemSection") + * ) + * ) + * + * @OA\Schema( + * schema="ItemSection", + * type="object", + * + * @OA\Property(property="id", type="integer", example=1), + * @OA\Property(property="tenant_id", type="integer", example=1), + * @OA\Property(property="page_id", type="integer", example=1), + * @OA\Property(property="title", type="string", example="제품 상세"), + * @OA\Property(property="type", type="string", enum={"fields","bom"}, example="fields"), + * @OA\Property(property="order_no", type="integer", example=0), + * @OA\Property(property="created_at", type="string", example="2025-11-20 10:00:00"), + * @OA\Property(property="updated_at", type="string", example="2025-11-20 10:00:00"), + * @OA\Property( + * property="fields", + * type="array", + * + * @OA\Items(ref="#/components/schemas/ItemField") + * ), + * + * @OA\Property( + * property="bomItems", + * type="array", + * + * @OA\Items(ref="#/components/schemas/ItemBomItem") + * ) + * ) + * + * @OA\Schema( + * schema="ItemField", + * type="object", + * + * @OA\Property(property="id", type="integer", example=1), + * @OA\Property(property="tenant_id", type="integer", example=1), + * @OA\Property(property="section_id", type="integer", example=1), + * @OA\Property(property="field_name", type="string", example="제품명"), + * @OA\Property(property="field_type", type="string", enum={"textbox","number","dropdown","checkbox","date","textarea"}, example="textbox"), + * @OA\Property(property="order_no", type="integer", example=0), + * @OA\Property(property="is_required", type="boolean", example=true), + * @OA\Property(property="default_value", type="string", nullable=true, example=null), + * @OA\Property(property="placeholder", type="string", nullable=true, example="제품명을 입력하세요"), + * @OA\Property(property="display_condition", type="object", nullable=true, example=null), + * @OA\Property(property="validation_rules", type="object", nullable=true, example=null), + * @OA\Property(property="options", type="object", nullable=true, example=null), + * @OA\Property(property="properties", type="object", nullable=true, example=null), + * @OA\Property(property="created_at", type="string", example="2025-11-20 10:00:00"), + * @OA\Property(property="updated_at", type="string", example="2025-11-20 10:00:00") + * ) + * + * @OA\Schema( + * schema="ItemBomItem", + * type="object", + * + * @OA\Property(property="id", type="integer", example=1), + * @OA\Property(property="tenant_id", type="integer", example=1), + * @OA\Property(property="section_id", type="integer", example=1), + * @OA\Property(property="item_code", type="string", nullable=true, example="ITEM001"), + * @OA\Property(property="item_name", type="string", example="부품 A"), + * @OA\Property(property="quantity", type="number", format="float", example=1.5), + * @OA\Property(property="unit", type="string", nullable=true, example="EA"), + * @OA\Property(property="unit_price", type="number", format="float", nullable=true, example=10000), + * @OA\Property(property="total_price", type="number", format="float", nullable=true, example=15000), + * @OA\Property(property="spec", type="string", nullable=true, example="규격 정보"), + * @OA\Property(property="note", type="string", nullable=true, example="비고"), + * @OA\Property(property="created_at", type="string", example="2025-11-20 10:00:00"), + * @OA\Property(property="updated_at", type="string", example="2025-11-20 10:00:00") + * ) + * + * @OA\Schema( + * schema="SectionTemplate", + * type="object", + * + * @OA\Property(property="id", type="integer", example=1), + * @OA\Property(property="tenant_id", type="integer", example=1), + * @OA\Property(property="title", type="string", example="기본 템플릿"), + * @OA\Property(property="type", type="string", enum={"fields","bom"}, example="fields"), + * @OA\Property(property="description", type="string", nullable=true, example="설명"), + * @OA\Property(property="is_default", type="boolean", example=false), + * @OA\Property(property="created_at", type="string", example="2025-11-20 10:00:00"), + * @OA\Property(property="updated_at", type="string", example="2025-11-20 10:00:00") + * ) + * + * @OA\Schema( + * schema="ItemMasterField", + * type="object", + * + * @OA\Property(property="id", type="integer", example=1), + * @OA\Property(property="tenant_id", type="integer", example=1), + * @OA\Property(property="field_name", type="string", example="제품명"), + * @OA\Property(property="field_type", type="string", enum={"textbox","number","dropdown","checkbox","date","textarea"}, example="textbox"), + * @OA\Property(property="category", type="string", nullable=true, example="basic"), + * @OA\Property(property="description", type="string", nullable=true, example="설명"), + * @OA\Property(property="is_common", type="boolean", example=true), + * @OA\Property(property="default_value", type="string", nullable=true, example=null), + * @OA\Property(property="options", type="object", nullable=true, example=null), + * @OA\Property(property="validation_rules", type="object", nullable=true, example=null), + * @OA\Property(property="properties", type="object", nullable=true, example=null), + * @OA\Property(property="created_at", type="string", example="2025-11-20 10:00:00"), + * @OA\Property(property="updated_at", type="string", example="2025-11-20 10:00:00") + * ) + * + * @OA\Schema( + * schema="CustomTab", + * type="object", + * + * @OA\Property(property="id", type="integer", example=1), + * @OA\Property(property="tenant_id", type="integer", example=1), + * @OA\Property(property="label", type="string", example="커스텀 탭"), + * @OA\Property(property="icon", type="string", nullable=true, example="icon-name"), + * @OA\Property(property="is_default", type="boolean", example=false), + * @OA\Property(property="order_no", type="integer", example=0), + * @OA\Property(property="created_at", type="string", example="2025-11-20 10:00:00"), + * @OA\Property(property="updated_at", type="string", example="2025-11-20 10:00:00"), + * @OA\Property(property="columnSetting", type="object", nullable=true) + * ) + * + * @OA\Schema( + * schema="UnitOption", + * type="object", + * + * @OA\Property(property="id", type="integer", example=1), + * @OA\Property(property="tenant_id", type="integer", example=1), + * @OA\Property(property="label", type="string", example="개"), + * @OA\Property(property="value", type="string", example="EA"), + * @OA\Property(property="created_at", type="string", example="2025-11-20 10:00:00"), + * @OA\Property(property="updated_at", type="string", example="2025-11-20 10:00:00") + * ) + * + * ======================================== + * Request 스키마 + * ======================================== + * + * @OA\Schema( + * schema="ItemPageStoreRequest", + * type="object", + * required={"page_name","item_type"}, + * + * @OA\Property(property="page_name", type="string", maxLength=255, example="기본 정보"), + * @OA\Property(property="item_type", type="string", enum={"FG","PT","SM","RM","CS"}, example="FG"), + * @OA\Property(property="absolute_path", type="string", nullable=true, maxLength=500, example="/items/fg/basic") + * ) + * + * @OA\Schema( + * schema="ItemPageUpdateRequest", + * type="object", + * + * @OA\Property(property="page_name", type="string", maxLength=255, example="기본 정보"), + * @OA\Property(property="absolute_path", type="string", nullable=true, maxLength=500, example="/items/fg/basic") + * ) + * + * @OA\Schema( + * schema="ItemSectionStoreRequest", + * type="object", + * required={"title","type"}, + * + * @OA\Property(property="title", type="string", maxLength=255, example="제품 상세"), + * @OA\Property(property="type", type="string", enum={"fields","bom"}, example="fields") + * ) + * + * @OA\Schema( + * schema="ItemSectionUpdateRequest", + * type="object", + * + * @OA\Property(property="title", type="string", maxLength=255, example="제품 상세") + * ) + * + * @OA\Schema( + * schema="ItemFieldStoreRequest", + * type="object", + * required={"field_name","field_type"}, + * + * @OA\Property(property="field_name", type="string", maxLength=255, example="제품명"), + * @OA\Property(property="field_type", type="string", enum={"textbox","number","dropdown","checkbox","date","textarea"}, example="textbox"), + * @OA\Property(property="is_required", type="boolean", example=true), + * @OA\Property(property="default_value", type="string", nullable=true, example=null), + * @OA\Property(property="placeholder", type="string", nullable=true, maxLength=255, example="제품명을 입력하세요"), + * @OA\Property(property="display_condition", type="object", nullable=true, example=null), + * @OA\Property(property="validation_rules", type="object", nullable=true, example=null), + * @OA\Property(property="options", type="object", nullable=true, example=null), + * @OA\Property(property="properties", type="object", nullable=true, example=null) + * ) + * + * @OA\Schema( + * schema="ItemFieldUpdateRequest", + * type="object", + * + * @OA\Property(property="field_name", type="string", maxLength=255, example="제품명"), + * @OA\Property(property="field_type", type="string", enum={"textbox","number","dropdown","checkbox","date","textarea"}, example="textbox"), + * @OA\Property(property="is_required", type="boolean", example=true), + * @OA\Property(property="default_value", type="string", nullable=true, example=null), + * @OA\Property(property="placeholder", type="string", nullable=true, maxLength=255, example="제품명을 입력하세요"), + * @OA\Property(property="display_condition", type="object", nullable=true, example=null), + * @OA\Property(property="validation_rules", type="object", nullable=true, example=null), + * @OA\Property(property="options", type="object", nullable=true, example=null), + * @OA\Property(property="properties", type="object", nullable=true, example=null) + * ) + * + * @OA\Schema( + * schema="ItemBomItemStoreRequest", + * type="object", + * required={"item_name"}, + * + * @OA\Property(property="item_code", type="string", nullable=true, maxLength=100, example="ITEM001"), + * @OA\Property(property="item_name", type="string", maxLength=255, example="부품 A"), + * @OA\Property(property="quantity", type="number", format="float", example=1.5), + * @OA\Property(property="unit", type="string", nullable=true, maxLength=50, example="EA"), + * @OA\Property(property="unit_price", type="number", format="float", nullable=true, example=10000), + * @OA\Property(property="total_price", type="number", format="float", nullable=true, example=15000), + * @OA\Property(property="spec", type="string", nullable=true, example="규격 정보"), + * @OA\Property(property="note", type="string", nullable=true, example="비고") + * ) + * + * @OA\Schema( + * schema="ItemBomItemUpdateRequest", + * type="object", + * + * @OA\Property(property="item_code", type="string", nullable=true, maxLength=100, example="ITEM001"), + * @OA\Property(property="item_name", type="string", maxLength=255, example="부품 A"), + * @OA\Property(property="quantity", type="number", format="float", example=1.5), + * @OA\Property(property="unit", type="string", nullable=true, maxLength=50, example="EA"), + * @OA\Property(property="unit_price", type="number", format="float", nullable=true, example=10000), + * @OA\Property(property="total_price", type="number", format="float", nullable=true, example=15000), + * @OA\Property(property="spec", type="string", nullable=true, example="규격 정보"), + * @OA\Property(property="note", type="string", nullable=true, example="비고") + * ) + * + * @OA\Schema( + * schema="SectionTemplateStoreRequest", + * type="object", + * required={"title","type"}, + * + * @OA\Property(property="title", type="string", maxLength=255, example="기본 템플릿"), + * @OA\Property(property="type", type="string", enum={"fields","bom"}, example="fields"), + * @OA\Property(property="description", type="string", nullable=true, example="설명"), + * @OA\Property(property="is_default", type="boolean", example=false) + * ) + * + * @OA\Schema( + * schema="SectionTemplateUpdateRequest", + * type="object", + * + * @OA\Property(property="title", type="string", maxLength=255, example="기본 템플릿"), + * @OA\Property(property="type", type="string", enum={"fields","bom"}, example="fields"), + * @OA\Property(property="description", type="string", nullable=true, example="설명"), + * @OA\Property(property="is_default", type="boolean", example=false) + * ) + * + * @OA\Schema( + * schema="ItemMasterFieldStoreRequest", + * type="object", + * required={"field_name","field_type"}, + * + * @OA\Property(property="field_name", type="string", maxLength=255, example="제품명"), + * @OA\Property(property="field_type", type="string", enum={"textbox","number","dropdown","checkbox","date","textarea"}, example="textbox"), + * @OA\Property(property="category", type="string", nullable=true, maxLength=100, example="basic"), + * @OA\Property(property="description", type="string", nullable=true, example="설명"), + * @OA\Property(property="is_common", type="boolean", example=true), + * @OA\Property(property="default_value", type="string", nullable=true, example=null), + * @OA\Property(property="options", type="object", nullable=true, example=null), + * @OA\Property(property="validation_rules", type="object", nullable=true, example=null), + * @OA\Property(property="properties", type="object", nullable=true, example=null) + * ) + * + * @OA\Schema( + * schema="ItemMasterFieldUpdateRequest", + * type="object", + * + * @OA\Property(property="field_name", type="string", maxLength=255, example="제품명"), + * @OA\Property(property="field_type", type="string", enum={"textbox","number","dropdown","checkbox","date","textarea"}, example="textbox"), + * @OA\Property(property="category", type="string", nullable=true, maxLength=100, example="basic"), + * @OA\Property(property="description", type="string", nullable=true, example="설명"), + * @OA\Property(property="is_common", type="boolean", example=true), + * @OA\Property(property="default_value", type="string", nullable=true, example=null), + * @OA\Property(property="options", type="object", nullable=true, example=null), + * @OA\Property(property="validation_rules", type="object", nullable=true, example=null), + * @OA\Property(property="properties", type="object", nullable=true, example=null) + * ) + * + * @OA\Schema( + * schema="CustomTabStoreRequest", + * type="object", + * required={"label"}, + * + * @OA\Property(property="label", type="string", maxLength=255, example="커스텀 탭"), + * @OA\Property(property="icon", type="string", nullable=true, maxLength=100, example="icon-name"), + * @OA\Property(property="is_default", type="boolean", example=false) + * ) + * + * @OA\Schema( + * schema="CustomTabUpdateRequest", + * type="object", + * + * @OA\Property(property="label", type="string", maxLength=255, example="커스텀 탭"), + * @OA\Property(property="icon", type="string", nullable=true, maxLength=100, example="icon-name"), + * @OA\Property(property="is_default", type="boolean", example=false) + * ) + * + * @OA\Schema( + * schema="UnitOptionStoreRequest", + * type="object", + * required={"label","value"}, + * + * @OA\Property(property="label", type="string", maxLength=100, example="개"), + * @OA\Property(property="value", type="string", maxLength=50, example="EA") + * ) + * + * @OA\Schema( + * schema="ReorderRequest", + * type="object", + * required={"items"}, + * + * @OA\Property( + * property="items", + * type="array", + * + * @OA\Items( + * type="object", + * required={"id","order_no"}, + * + * @OA\Property(property="id", type="integer", example=1), + * @OA\Property(property="order_no", type="integer", example=0) + * ), + * example={{"id":1,"order_no":0},{"id":2,"order_no":1}} + * ) + * ) + * + * ======================================== + * Response 스키마 + * ======================================== + * + * @OA\Schema( + * schema="ItemMasterInitResponse", + * type="object", + * + * @OA\Property( + * property="pages", + * type="array", + * + * @OA\Items(ref="#/components/schemas/ItemPage") + * ), + * + * @OA\Property( + * property="sectionTemplates", + * type="array", + * + * @OA\Items(ref="#/components/schemas/SectionTemplate") + * ), + * + * @OA\Property( + * property="masterFields", + * type="array", + * + * @OA\Items(ref="#/components/schemas/ItemMasterField") + * ), + * + * @OA\Property( + * property="customTabs", + * type="array", + * + * @OA\Items(ref="#/components/schemas/CustomTab") + * ), + * + * @OA\Property( + * property="unitOptions", + * type="array", + * + * @OA\Items(ref="#/components/schemas/UnitOption") + * ) + * ) + */ +class ItemMasterApi +{ + /** + * @OA\Get( + * path="/api/v1/item-master/init", + * tags={"ItemMaster"}, + * summary="품목기준관리 초기 데이터 로드", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Response(response=200, description="조회 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ItemMasterInitResponse")) + * }) + * ), + * + * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function init() {} + + /** + * @OA\Get( + * path="/api/v1/item-master/pages", + * tags={"ItemMaster"}, + * summary="페이지 목록 조회", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="item_type", in="query", description="품목 유형 필터", @OA\Schema(type="string", enum={"FG","PT","SM","RM","CS"})), + * + * @OA\Response(response=200, description="조회 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/ItemPage"))) + * }) + * ), + * + * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function indexPages() {} + + /** + * @OA\Post( + * path="/api/v1/item-master/pages", + * tags={"ItemMaster"}, + * summary="페이지 생성", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ItemPageStoreRequest")), + * + * @OA\Response(response=200, description="생성 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ItemPage")) + * }) + * ), + * + * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function storePages() {} + + /** + * @OA\Put( + * path="/api/v1/item-master/pages/{id}", + * tags={"ItemMaster"}, + * summary="페이지 수정", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ItemPageUpdateRequest")), + * + * @OA\Response(response=200, description="수정 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ItemPage")) + * }) + * ), + * + * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function updatePages() {} + + /** + * @OA\Delete( + * path="/api/v1/item-master/pages/{id}", + * tags={"ItemMaster"}, + * summary="페이지 삭제 (Cascade)", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), + * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function destroyPages() {} + + /** + * @OA\Post( + * path="/api/v1/item-master/pages/{pageId}/sections", + * tags={"ItemMaster"}, + * summary="섹션 생성", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="pageId", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ItemSectionStoreRequest")), + * + * @OA\Response(response=200, description="생성 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ItemSection")) + * }) + * ), + * + * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function storeSections() {} + + /** + * @OA\Put( + * path="/api/v1/item-master/sections/{id}", + * tags={"ItemMaster"}, + * summary="섹션 수정", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ItemSectionUpdateRequest")), + * + * @OA\Response(response=200, description="수정 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ItemSection")) + * }) + * ), + * + * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function updateSections() {} + + /** + * @OA\Delete( + * path="/api/v1/item-master/sections/{id}", + * tags={"ItemMaster"}, + * summary="섹션 삭제 (Cascade)", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), + * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function destroySections() {} + + /** + * @OA\Put( + * path="/api/v1/item-master/pages/{pageId}/sections/reorder", + * tags={"ItemMaster"}, + * summary="섹션 순서 변경", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="pageId", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ReorderRequest")), + * + * @OA\Response(response=200, description="순서 변경 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), + * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function reorderSections() {} + + /** + * @OA\Post( + * path="/api/v1/item-master/sections/{sectionId}/fields", + * tags={"ItemMaster"}, + * summary="필드 생성", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="sectionId", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ItemFieldStoreRequest")), + * + * @OA\Response(response=200, description="생성 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ItemField")) + * }) + * ), + * + * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function storeFields() {} + + /** + * @OA\Put( + * path="/api/v1/item-master/fields/{id}", + * tags={"ItemMaster"}, + * summary="필드 수정", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ItemFieldUpdateRequest")), + * + * @OA\Response(response=200, description="수정 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ItemField")) + * }) + * ), + * + * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function updateFields() {} + + /** + * @OA\Delete( + * path="/api/v1/item-master/fields/{id}", + * tags={"ItemMaster"}, + * summary="필드 삭제", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), + * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function destroyFields() {} + + /** + * @OA\Put( + * path="/api/v1/item-master/sections/{sectionId}/fields/reorder", + * tags={"ItemMaster"}, + * summary="필드 순서 변경", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="sectionId", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ReorderRequest")), + * + * @OA\Response(response=200, description="순서 변경 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), + * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function reorderFields() {} + + /** + * @OA\Post( + * path="/api/v1/item-master/sections/{sectionId}/bom-items", + * tags={"ItemMaster"}, + * summary="BOM 항목 생성", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="sectionId", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ItemBomItemStoreRequest")), + * + * @OA\Response(response=200, description="생성 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ItemBomItem")) + * }) + * ), + * + * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function storeBomItems() {} + + /** + * @OA\Put( + * path="/api/v1/item-master/bom-items/{id}", + * tags={"ItemMaster"}, + * summary="BOM 항목 수정", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ItemBomItemUpdateRequest")), + * + * @OA\Response(response=200, description="수정 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ItemBomItem")) + * }) + * ), + * + * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function updateBomItems() {} + + /** + * @OA\Delete( + * path="/api/v1/item-master/bom-items/{id}", + * tags={"ItemMaster"}, + * summary="BOM 항목 삭제", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), + * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function destroyBomItems() {} + + /** + * @OA\Get( + * path="/api/v1/item-master/section-templates", + * tags={"ItemMaster"}, + * summary="섹션 템플릿 목록", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Response(response=200, description="조회 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/SectionTemplate"))) + * }) + * ), + * + * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function indexSectionTemplates() {} + + /** + * @OA\Post( + * path="/api/v1/item-master/section-templates", + * tags={"ItemMaster"}, + * summary="섹션 템플릿 생성", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/SectionTemplateStoreRequest")), + * + * @OA\Response(response=200, description="생성 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/SectionTemplate")) + * }) + * ), + * + * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function storeSectionTemplates() {} + + /** + * @OA\Put( + * path="/api/v1/item-master/section-templates/{id}", + * tags={"ItemMaster"}, + * summary="섹션 템플릿 수정", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/SectionTemplateUpdateRequest")), + * + * @OA\Response(response=200, description="수정 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/SectionTemplate")) + * }) + * ), + * + * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function updateSectionTemplates() {} + + /** + * @OA\Delete( + * path="/api/v1/item-master/section-templates/{id}", + * tags={"ItemMaster"}, + * summary="섹션 템플릿 삭제", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), + * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function destroySectionTemplates() {} + + /** + * @OA\Get( + * path="/api/v1/item-master/master-fields", + * tags={"ItemMaster"}, + * summary="마스터 필드 목록", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Response(response=200, description="조회 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/ItemMasterField"))) + * }) + * ), + * + * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function indexMasterFields() {} + + /** + * @OA\Post( + * path="/api/v1/item-master/master-fields", + * tags={"ItemMaster"}, + * summary="마스터 필드 생성", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ItemMasterFieldStoreRequest")), + * + * @OA\Response(response=200, description="생성 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ItemMasterField")) + * }) + * ), + * + * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function storeMasterFields() {} + + /** + * @OA\Put( + * path="/api/v1/item-master/master-fields/{id}", + * tags={"ItemMaster"}, + * summary="마스터 필드 수정", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ItemMasterFieldUpdateRequest")), + * + * @OA\Response(response=200, description="수정 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ItemMasterField")) + * }) + * ), + * + * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function updateMasterFields() {} + + /** + * @OA\Delete( + * path="/api/v1/item-master/master-fields/{id}", + * tags={"ItemMaster"}, + * summary="마스터 필드 삭제", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), + * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function destroyMasterFields() {} + + /** + * @OA\Get( + * path="/api/v1/item-master/custom-tabs", + * tags={"ItemMaster"}, + * summary="커스텀 탭 목록", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Response(response=200, description="조회 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/CustomTab"))) + * }) + * ), + * + * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function indexCustomTabs() {} + + /** + * @OA\Post( + * path="/api/v1/item-master/custom-tabs", + * tags={"ItemMaster"}, + * summary="커스텀 탭 생성", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/CustomTabStoreRequest")), + * + * @OA\Response(response=200, description="생성 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/CustomTab")) + * }) + * ), + * + * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function storeCustomTabs() {} + + /** + * @OA\Put( + * path="/api/v1/item-master/custom-tabs/{id}", + * tags={"ItemMaster"}, + * summary="커스텀 탭 수정", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/CustomTabUpdateRequest")), + * + * @OA\Response(response=200, description="수정 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/CustomTab")) + * }) + * ), + * + * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function updateCustomTabs() {} + + /** + * @OA\Delete( + * path="/api/v1/item-master/custom-tabs/{id}", + * tags={"ItemMaster"}, + * summary="커스텀 탭 삭제", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), + * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function destroyCustomTabs() {} + + /** + * @OA\Put( + * path="/api/v1/item-master/custom-tabs/reorder", + * tags={"ItemMaster"}, + * summary="커스텀 탭 순서 변경", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ReorderRequest")), + * + * @OA\Response(response=200, description="순서 변경 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), + * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function reorderCustomTabs() {} + + /** + * @OA\Get( + * path="/api/v1/item-master/unit-options", + * tags={"ItemMaster"}, + * summary="단위 옵션 목록", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Response(response=200, description="조회 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/UnitOption"))) + * }) + * ), + * + * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function indexUnitOptions() {} + + /** + * @OA\Post( + * path="/api/v1/item-master/unit-options", + * tags={"ItemMaster"}, + * summary="단위 옵션 생성", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/UnitOptionStoreRequest")), + * + * @OA\Response(response=200, description="생성 성공", + * + * @OA\JsonContent(allOf={ + * + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/UnitOption")) + * }) + * ), + * + * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function storeUnitOptions() {} + + /** + * @OA\Delete( + * path="/api/v1/item-master/unit-options/{id}", + * tags={"ItemMaster"}, + * summary="단위 옵션 삭제", + * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, + * + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * + * @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), + * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function destroyUnitOptions() {} +} \ No newline at end of file diff --git a/history/2025-11/item-master-archived/[ANALYSIS] item-master-data-management.md b/history/2025-11/item-master-archived/[ANALYSIS] item-master-data-management.md new file mode 100644 index 0000000..e05cad0 --- /dev/null +++ b/history/2025-11/item-master-archived/[ANALYSIS] item-master-data-management.md @@ -0,0 +1,2588 @@ +1# 품목기준관리 시스템 분석 및 구현 계획 + +> **작성일**: 2025-11-17 +> **소스**: React 프로젝트 ItemMasterDataManagement.tsx (1,413줄) +> **타겟**: Next.js 15 마이그레이션 +> **상태**: 📊 분석 완료, 구현 대기 + +--- + +## 📑 목차 + +1. [프로젝트 배경 및 목표](#1-프로젝트-배경-및-목표) +2. [시스템 개요](#2-시스템-개요) +3. [데이터 구조 분석](#3-데이터-구조-분석) +4. [기능 분석](#4-기능-분석) +5. [API 요구사항](#5-api-요구사항) +6. [버전관리 시스템](#6-버전관리-시스템) +7. [멀티테넌시 및 데이터 로딩 전략](#7-멀티테넌시-및-데이터-로딩-전략) +8. [Next.js 15 마이그레이션 계획](#8-nextjs-15-마이그레이션-계획) +9. [구현 우선순위](#9-구현-우선순위) +10. [다음 단계](#10-다음-단계) + +--- + +## 1. 프로젝트 배경 및 목표 + +### 1.1 배경 + +현재 구현된 **품목등록 화면** (`ItemForm.tsx`)은 고정된 값(템플릿)으로 구성되어 있습니다. +실제 운영 환경에서는 이 고정값들을 **동적으로 설정/편집**할 수 있어야 하며, +이를 위한 별도의 **품목기준관리 페이지**가 필요합니다. + +``` +품목기준관리 페이지 (ItemMasterDataManagement) + ↓ 설정/편집 +품목등록 화면 (ItemForm) + ↓ 사용자 입력 +실제 품목 데이터 +``` + +### 1.2 핵심 요구사항 + +1. ✅ **드롭다운 옵션 관리**: 단위, 재질, 표면처리 등 +2. ✅ **페이지 구조 관리**: 섹션, 하위섹션, 항목 계층 구조 +3. ✅ **마스터 항목 템플릿**: 재사용 가능한 항목 정의 +4. ✅ **조건부 표시 로직**: 특정 값에 따라 항목 표시/숨김 +5. ✅ **품목 코드 자동생성 규칙**: 동적으로 설정 가능한 규칙 체계 +6. ✅ **다양한 업종 지원**: 고객사별 자유로운 페이지 구성 +7. ✅ **버전관리 시스템**: 품목 수정 이력 추적 및 이전 버전 조회 + +### 1.3 목표 + +**1차 목표**: 품목기준관리 페이지 구축 및 Laravel API 연동 +**2차 목표**: 품목등록 화면을 동적 템플릿 시스템으로 전환 (선택적) + +--- + +## 2. 시스템 개요 + +### 2.1 React 프로젝트 파일 정보 + +**파일 경로**: `/sma-react-v2.0/src/components/ItemMasterDataManagement.tsx` +**파일 크기**: 1,413줄 +**주요 의존성**: +- `DataContext` - 전역 상태 관리 (6,697줄) +- `SpecificationManagement` - 규격 관리 서브 컴포넌트 +- `DraggableField` - 드래그 앤 드롭 항목 + +### 2.2 6개 탭 구조 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 품목기준관리 메인 화면 │ +└─────────────────────────────────────────────────────────────┘ + │ + ├─ 📊 계층구조 (hierarchy) + │ └─ 섹션 > 하위섹션 > 항목 트리 구조 + │ + ├─ 📋 항목 (items) + │ └─ 마스터 항목 템플릿 (제품/부품/부자재/원자재별) + │ + ├─ 📏 단위 (units) + │ └─ EA, SET, KG, M, L, BOX, PCS 등 + │ + ├─ 🧱 재질 (materials) + │ └─ EGI 1.2T, SUS 1.2T, SPHC-SD 등 + │ + ├─ 🎨 표면처리 (surface) + │ └─ 무도장, 파우더도장, 아연도금 등 + │ + └─ 📐 규격 (specifications) + └─ 별도 SpecificationManagement 컴포넌트 +``` + +### 2.3 데이터 흐름 + +``` +사용자 + ↓ +품목기준관리 페이지 + ↓ 설정 저장 +Laravel API (DB) + ↓ 조회 +품목등록 화면 (ItemForm) + ↓ 드롭다운, 필드 옵션 표시 +사용자 입력 +``` + +--- + +## 3. 데이터 구조 분석 + +### 3.1 계층구조 데이터 + +#### ItemPage (섹션) + +```typescript +interface ItemPage { + id: string; // 예: "PAGE-1234567890" + pageName: string; // 예: "품목 등록" + itemType: 'FG' | 'PT' | 'SM' | 'RM' | 'CS'; // 품목 유형 + sections: ItemSection[]; // 하위섹션 배열 + isActive: boolean; // 활성 여부 + absolutePath?: string; // 절대경로 (선택적) + createdAt: string; // 생성일 + updatedAt?: string; // 수정일 +} +``` + +**예시**: +```json +{ + "id": "PAGE-001", + "pageName": "제품(FG) 등록", + "itemType": "FG", + "sections": [...], + "isActive": true, + "createdAt": "2025-01-10" +} +``` + +#### ItemSection (하위섹션) + +```typescript +interface ItemSection { + id: string; // 예: "SECTION-1234567890" + title: string; // 예: "기본정보" + description?: string; // 설명 (선택) + category?: string[]; // 카테고리 조건 (선택) + fields: ItemField[]; // 항목 배열 + type?: 'fields' | 'bom'; // 섹션 타입 (선택) + order: number; // 표시 순서 + isCollapsible: boolean; // 접기/펼치기 가능 여부 + isCollapsed: boolean; // 기본 접힘 상태 + createdAt: string; // 생성일 +} +``` + +**예시**: +```json +{ + "id": "SECTION-001", + "title": "기본정보", + "description": "품목의 기본 정보를 입력합니다", + "fields": [...], + "order": 1, + "isCollapsible": true, + "isCollapsed": false, + "createdAt": "2025-01-10" +} +``` + +#### ItemField (항목) + +```typescript +interface ItemField { + id: string; // 예: "FIELD-1234567890" + name: string; // 예: "품목코드" + fieldKey: string; // 예: "itemCode" + property: ItemFieldProperty; // 입력 속성 + description?: string; // 설명 (선택) + displayCondition?: FieldDisplayCondition; // 조건부 표시 (선택) + masterFieldId?: string; // 마스터 항목 ID (선택) + order?: number; // 표시 순서 (선택) + createdAt: string; // 생성일 +} +``` + +**예시**: +```json +{ + "id": "FIELD-001", + "name": "품목코드", + "fieldKey": "itemCode", + "property": { + "inputType": "textbox", + "required": true, + "row": 1, + "col": 1 + }, + "description": "품목을 식별하는 고유 코드", + "createdAt": "2025-01-10" +} +``` + +#### ItemFieldProperty (입력 속성) + +```typescript +interface ItemFieldProperty { + inputType: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'; + required: boolean; // 필수 여부 + row: number; // 레이아웃 행 + col: number; // 레이아웃 열 + options?: string[]; // 드롭다운 옵션 (dropdown일 때) +} +``` + +**예시 (드롭다운)**: +```json +{ + "inputType": "dropdown", + "required": true, + "row": 1, + "col": 1, + "options": ["EA", "SET", "KG", "M"] +} +``` + +#### FieldDisplayCondition (조건부 표시) + +```typescript +interface FieldDisplayCondition { + fieldKey: string; // 조건 필드 키 + expectedValue: string; // 기대 값 +} +``` + +**예시**: +```json +{ + "fieldKey": "itemType", + "expectedValue": "FG" +} +``` +**의미**: `itemType` 필드 값이 `"FG"`일 때만 이 항목을 표시 + +--- + +### 3.2 마스터 항목 데이터 + +#### ItemMasterField + +```typescript +interface ItemMasterField { + id: string; // 예: "MASTER-1234567890" + name: string; // 예: "품목코드" + fieldKey: string; // 예: "itemCode" + property: ItemFieldProperty; // 입력 속성 + category?: string; // 카테고리 (예: "공통", "제품", "부품") + description?: string; // 설명 + isActive: boolean; // 활성 여부 + createdAt: string; // 생성일 +} +``` + +**역할**: +- 재사용 가능한 항목 템플릿 +- 여러 섹션/페이지에서 참조 가능 +- 품목 분류별로 관리 (공통, 제품, 부품, 부자재, 원자재) + +**예시**: +```json +{ + "id": "MASTER-001", + "name": "품목코드", + "fieldKey": "itemCode", + "property": { + "inputType": "textbox", + "required": true, + "row": 1, + "col": 1 + }, + "category": "공통", + "description": "모든 품목에 공통으로 사용되는 품목코드", + "isActive": true, + "createdAt": "2025-01-10" +} +``` + +--- + +### 3.3 옵션 데이터 (단위/재질/표면처리) + +#### MasterOption + +```typescript +interface MasterOption { + id: string; // 예: "unit-1", "mat-1", "surf-1" + value: string; // 코드 값 (예: "EA", "EGI 1.2T") + label: string; // 표시명 (예: "EA (개)", "EGI 1.2T") + isActive: boolean; // 활성 여부 +} +``` + +**관리 대상**: +1. **단위 (units)**: `EA`, `SET`, `KG`, `M`, `L`, `BOX`, `PCS` +2. **재질 (materials)**: `EGI 1.2T`, `EGI 2.0T`, `SUS 1.2T`, `SPHC-SD 1.6T` +3. **표면처리 (surface)**: `무도장`, `파우더도장`, `아연도금` + +**예시 (단위)**: +```json +[ + { "id": "unit-1", "value": "EA", "label": "EA (개)", "isActive": true }, + { "id": "unit-2", "value": "SET", "label": "SET (세트)", "isActive": true }, + { "id": "unit-3", "value": "KG", "label": "KG (킬로그램)", "isActive": true } +] +``` + +--- + +### 3.4 로컬스토리지 키 구조 + +React 프로젝트에서는 로컬스토리지를 사용하지만, +Next.js에서는 **Laravel API**로 대체합니다. + +**현재 (React)**: +```javascript +const UNIT_OPTIONS_KEY = 'unit-options'; +const MATERIAL_OPTIONS_KEY = 'material-options'; +const SURFACE_TREATMENT_OPTIONS_KEY = 'surface-treatment-options'; +const ITEM_CLASSIFICATIONS_KEY = 'item-classifications'; +``` + +**목표 (Next.js + Laravel)**: +``` +GET /api/master-data/units +GET /api/master-data/materials +GET /api/master-data/surface-treatments +GET /api/master-data/pages +GET /api/master-data/fields +``` + +--- + +## 4. 기능 분석 + +### 4.1 계층구조 탭 + +**주요 기능**: +1. ✅ **섹션 생성**: 품목 유형별 페이지 생성 (FG/PT/SM/RM/CS) +2. ✅ **하위섹션 추가**: 섹션 내 그룹 추가 (기본정보, BOM, 가격정보 등) +3. ✅ **항목 추가/편집**: 입력 필드 정의 및 수정 +4. ✅ **드래그 앤 드롭**: 항목 순서 변경 +5. ✅ **조건부 표시**: 특정 값에 따라 항목 표시/숨김 +6. ✅ **마스터 항목 연동**: 마스터 항목 템플릿 선택 + +**UI 구조**: +``` +┌─────────────────────────────────────────────────────┐ +│ 섹션 목록 (좌측) │ 계층구조 상세 (우측) │ +│ │ │ +│ - 제품(FG) 등록 │ ┌─ 기본정보 섹션 │ +│ - 부품(PT) 등록 │ │ - 품목코드 │ +│ - 부자재(SM) 등록 │ │ - 품목명 │ +│ - 원자재(RM) 등록 │ │ - 단위 │ +│ - 소모품(CS) 등록 │ │ [+ 항목 추가] │ +│ │ │ │ +│ [+ 섹션 추가] │ └─ BOM 섹션 │ +│ │ - 하위 품목코드 │ +│ │ - 수량 │ +│ │ [+ 항목 추가] │ +└─────────────────────────────────────────────────────┘ +``` + +**항목 추가 다이얼로그**: +- 마스터 항목에서 불러오기 (토글) +- 항목명, 필드 키 +- 입력 방식 (텍스트박스, 드롭다운, 체크박스, 숫자, 날짜, 텍스트영역) +- 필수 항목 여부 +- 드롭다운 옵션 (쉼표로 구분) +- 조건부 항목 설정 (조건 필드, 조건 값) +- 설명 + +--- + +### 4.2 항목 탭 + +**주요 기능**: +1. ✅ **마스터 항목 추가/편집/삭제** +2. ✅ **품목 분류별 필터링**: 공통, 제품, 부품, 부자재, 원자재 +3. ✅ **재사용 가능한 템플릿 관리** + +**UI 구조**: +``` +┌─────────────────────────────────────────────────────┐ +│ 마스터 항목 관리 [+ 항목 추가] │ +├─────────────────────────────────────────────────────┤ +│ [제품] [부품] [부자재] [원자재] (서브 탭) │ +├─────────────────────────────────────────────────────┤ +│ ┌─────────────────────────────────────────────┐ │ +│ │ 품목코드 [텍스트박스] [필수] [공통] │ │ +│ │ 필드키: itemCode [편집][삭제]│ +│ └─────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ 단위 [드롭다운] [필수] [공통] │ │ +│ │ 필드키: unit [편집][삭제]│ +│ │ 옵션: EA, SET, KG, M │ │ +│ └─────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────┘ +``` + +--- + +### 4.3 단위/재질/표면처리 탭 + +**공통 기능**: +1. ✅ **옵션 추가**: 값(코드), 라벨(표시명) +2. ✅ **옵션 삭제** +3. ✅ **활성/비활성 관리** + +**UI 구조** (단위 탭 예시): +``` +┌─────────────────────────────────────────────────────┐ +│ 단위 관리 [+ 추가] │ +├─────────────────────────────────────────────────────┤ +│ 번호 │ 값 (Value) │ 라벨 (Label) │ 작업 │ +│ 1 │ EA │ EA (개) │ [삭제] │ +│ 2 │ SET │ SET (세트) │ [삭제] │ +│ 3 │ KG │ KG (킬로그램) │ [삭제] │ +└─────────────────────────────────────────────────────┘ +``` + +--- + +### 4.4 규격 탭 + +**구현**: +- 별도 `SpecificationManagement` 컴포넌트 +- React 프로젝트에서 추가 분석 필요 + +--- + +## 5. API 요구사항 + +### 5.1 계층구조 관리 API + +#### 섹션 (ItemPage) 관리 + +```typescript +// 섹션 목록 조회 +GET /api/master-data/pages +Response: { + success: true, + data: ItemPage[] +} + +// 섹션 생성 +POST /api/master-data/pages +Request: { + pageName: string, + itemType: 'FG' | 'PT' | 'SM' | 'RM' | 'CS' +} +Response: { + success: true, + data: ItemPage +} + +// 섹션 수정 +PUT /api/master-data/pages/:pageId +Request: { + pageName?: string, + isActive?: boolean +} + +// 섹션 삭제 +DELETE /api/master-data/pages/:pageId +``` + +#### 하위섹션 (ItemSection) 관리 + +```typescript +// 하위섹션 추가 +POST /api/master-data/pages/:pageId/sections +Request: { + title: string, + description?: string, + order: number, + isCollapsible: boolean, + isCollapsed: boolean +} +Response: { + success: true, + data: ItemSection +} + +// 하위섹션 수정 +PUT /api/master-data/pages/:pageId/sections/:sectionId +Request: { + title?: string, + description?: string, + order?: number +} + +// 하위섹션 삭제 +DELETE /api/master-data/pages/:pageId/sections/:sectionId +``` + +#### 항목 (ItemField) 관리 + +```typescript +// 항목 추가 +POST /api/master-data/pages/:pageId/sections/:sectionId/fields +Request: { + name: string, + fieldKey: string, + property: ItemFieldProperty, + description?: string, + displayCondition?: FieldDisplayCondition, + masterFieldId?: string, + order?: number +} +Response: { + success: true, + data: ItemField +} + +// 항목 수정 +PUT /api/master-data/pages/:pageId/sections/:sectionId/fields/:fieldId +Request: { + name?: string, + fieldKey?: string, + property?: ItemFieldProperty, + description?: string, + displayCondition?: FieldDisplayCondition +} + +// 항목 순서 변경 +PUT /api/master-data/pages/:pageId/sections/:sectionId/fields/reorder +Request: { + fieldIds: string[] // 새로운 순서의 필드 ID 배열 +} + +// 항목 삭제 +DELETE /api/master-data/pages/:pageId/sections/:sectionId/fields/:fieldId +``` + +--- + +### 5.2 마스터 항목 API + +```typescript +// 마스터 항목 목록 조회 (카테고리별) +GET /api/master-data/fields?category=공통 +GET /api/master-data/fields?category=제품 +GET /api/master-data/fields?category=부품 +Response: { + success: true, + data: ItemMasterField[] +} + +// 마스터 항목 생성 +POST /api/master-data/fields +Request: { + name: string, + fieldKey: string, + property: ItemFieldProperty, + category?: string, + description?: string +} +Response: { + success: true, + data: ItemMasterField +} + +// 마스터 항목 수정 +PUT /api/master-data/fields/:fieldId +Request: { + name?: string, + fieldKey?: string, + property?: ItemFieldProperty, + category?: string, + description?: string +} + +// 마스터 항목 삭제 +DELETE /api/master-data/fields/:fieldId +``` + +--- + +### 5.3 옵션 관리 API + +```typescript +// 단위 목록 조회 +GET /api/master-data/units +Response: { + success: true, + data: MasterOption[] +} + +// 단위 추가 +POST /api/master-data/units +Request: { + value: string, + label: string +} + +// 단위 삭제 +DELETE /api/master-data/units/:unitId + +// 재질 목록 조회 +GET /api/master-data/materials +Response: { + success: true, + data: MasterOption[] +} + +// 재질 추가 +POST /api/master-data/materials +Request: { + value: string, + label: string +} + +// 재질 삭제 +DELETE /api/master-data/materials/:materialId + +// 표면처리 목록 조회 +GET /api/master-data/surface-treatments +Response: { + success: true, + data: MasterOption[] +} + +// 표면처리 추가 +POST /api/master-data/surface-treatments +Request: { + value: string, + label: string +} + +// 표면처리 삭제 +DELETE /api/master-data/surface-treatments/:surfaceId +``` + +--- + +### 5.4 통합 조회 API + +품목등록 화면에서 사용할 마스터 데이터를 한 번에 조회: + +```typescript +GET /api/master-data +Response: { + success: true, + data: { + units: MasterOption[], + materials: MasterOption[], + surfaceTreatments: MasterOption[], + pages: ItemPage[], + masterFields: ItemMasterField[] + } +} +``` + +--- + +## 6. 버전관리 시스템 + +### 6.1 시스템 개요 + +품목기준관리 시스템은 **포괄적인 버전관리 시스템**을 포함하여 모든 데이터 수정 이력을 추적합니다. +이는 단순한 감사 로그가 아니라, 이전 버전으로의 롤백과 변경 이력 비교가 가능한 **완전한 버전 관리** 시스템입니다. + +**핵심 특징**: +- ✅ **전체 데이터 스냅샷**: 수정 시마다 이전 상태 전체를 저장 +- ✅ **필수 수정 사유**: 모든 수정 작업에 사유(revision reason) 입력 강제 +- ✅ **버전 번호 자동 증가**: currentRevision 필드로 현재 버전 추적 +- ✅ **이력 조회**: 특정 품목의 전체 수정 이력 확인 +- ✅ **이전 버전 복원**: previousData를 활용한 롤백 기능 +- ✅ **다중 엔티티 지원**: 품목, 견적, 수주, 계산식, 단가 등 시스템 전반 적용 + +**버전관리 대상 엔티티**: +``` +✅ ItemMaster (품목) +✅ Quote (견적) +✅ Order (수주) +✅ CalculationFormula (계산식) +✅ PricingData (단가) +✅ FormulaRule (수식 규칙) +``` + +--- + +### 6.2 데이터 구조 + +#### ItemRevision (품목 수정 이력) + +```typescript +export interface ItemRevision { + revisionNumber: number; // 수정 버전 번호 (1부터 시작) + revisionDate: string; // 수정 날짜 (ISO 8601 형식) + revisionBy: string; // 수정자 (사용자 ID 또는 이름) + revisionReason?: string; // 수정 사유 (필수) + changedFields?: string[]; // 변경된 필드 목록 (선택) + previousData: any; // 이전 상태 전체 스냅샷 +} +``` + +**예시**: +```json +{ + "revisionNumber": 3, + "revisionDate": "2025-01-15T10:30:00Z", + "revisionBy": "admin@example.com", + "revisionReason": "품목코드 생성 규칙 변경", + "changedFields": ["autoGenerationRule", "codePattern"], + "previousData": { + "id": "PAGE-001", + "pageName": "제품(FG) 등록", + "itemType": "FG", + "sections": [...], + "currentRevision": 2, + "revisions": [...] + } +} +``` + +--- + +### 6.3 버전관리 필드 + +모든 버전관리 대상 엔티티는 다음 필드를 포함합니다: + +```typescript +interface VersionedEntity { + // ... 기본 필드들 + + // 버전관리 필드 + currentRevision: number; // 현재 버전 번호 (기본값: 0) + revisions: ItemRevision[]; // 수정 이력 배열 + isFinal?: boolean; // 최종 확정 여부 (선택) +} +``` + +**예시 (ItemPage with Version)**: +```typescript +export interface ItemPage { + id: string; + pageName: string; + itemType: 'FG' | 'PT' | 'SM' | 'RM' | 'CS'; + sections: ItemSection[]; + isActive: boolean; + absolutePath?: string; + createdAt: string; + updatedAt?: string; + + // 버전관리 필드 + currentRevision: number; // 현재 버전 (예: 3) + revisions: ItemRevision[]; // 수정 이력 배열 + isFinal?: boolean; // 최종 확정 여부 +} +``` + +--- + +### 6.4 버전관리 워크플로우 + +#### 수정 프로세스 + +``` +사용자가 수정 요청 + ↓ +수정 사유 다이얼로그 표시 (필수) + ↓ +사용자가 수정 사유 입력 + ↓ +현재 데이터 전체를 previousData로 저장 + ↓ +currentRevision 증가 (예: 2 → 3) + ↓ +revisions 배열에 새 ItemRevision 추가 + ↓ +수정된 데이터 저장 + ↓ +성공 메시지 표시 +``` + +**React 프로젝트 코드 예시** (ItemManagement.tsx:1637-1651): +```typescript +// 버전 관리 - 수정 시 이력 저장 +const currentRevision = selectedItem.currentRevision || 0; +const newRevisionNumber = currentRevision + 1; + +const revision = { + revisionNumber: newRevisionNumber, + revisionDate: new Date().toISOString().split("T")[0], + revisionBy: "관리자", // TODO: 실제 사용자 정보로 교체 + revisionReason: revisionReason, + previousData: { ...selectedItem } +}; + +pendingUpdates.currentRevision = newRevisionNumber; +pendingUpdates.revisions = [...(selectedItem.revisions || []), revision]; +``` + +--- + +### 6.5 UI/UX 구성 + +#### 수정 사유 입력 다이얼로그 + +```tsx + + + 수정 사유 입력 + + 데이터를 수정하는 이유를 입력해주세요. 이 정보는 수정 이력에 기록됩니다. + + + +
+