feat: Item Master 하이브리드 구조 전환 및 독립 API 추가
- CASCADE FK → 독립 엔티티 + entity_relationships 링크 테이블 - 독립 API 10개 추가 (섹션/필드/BOM CRUD, clone, usage) - SectionTemplate 모델 제거 → ItemSection.is_template 통합 - 페이지-섹션, 섹션-필드, 섹션-BOM 링크/언링크 API 14개 추가 - Swagger 문서 업데이트
This commit is contained in:
1297
docs/front/[API-2025-11-20] item-master-specification.md
Normal file
1297
docs/front/[API-2025-11-20] item-master-specification.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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<number, TabColumnResponse[]>; // 탭별 컬럼 설정
|
||||
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<ItemPageRequest>`
|
||||
|
||||
**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<ItemSectionRequest>`
|
||||
|
||||
**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<string, any>;
|
||||
properties?: Record<string, any>;
|
||||
|
||||
// 조건부 표시 설정 (신규 기능)
|
||||
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<ItemFieldRequest>`
|
||||
|
||||
**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<string, any>;
|
||||
}>;
|
||||
}
|
||||
```
|
||||
|
||||
**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<string, any>;
|
||||
properties?: Record<string, any>;
|
||||
}
|
||||
```
|
||||
|
||||
**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
|
||||
@@ -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`
|
||||
Reference in New Issue
Block a user