docs: 2025-11 문서 아카이브 이동

- history/2025-11/front-requests/ 프론트 요청 문서 이동
- history/2025-11/item-master-archived/ Item Master 구버전 문서 이동
This commit is contained in:
2025-12-09 20:30:39 +09:00
parent 73b8b9325b
commit ceae830e41
12 changed files with 12415 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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": "스펙정보" }]
}
```

View File

@@ -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`

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,276 @@
# Item Master API 백엔드 처리 요구사항
**작성일**: 2025-11-23
**작성자**: Claude Code (Frontend 타입 에러 수정 및 API 연결 테스트)
**목적**: Item Master 기능 API 연동을 위한 백엔드 설정 및 확인 필요 사항 정리
---
## 🚨 우선순위 1: CORS 설정 필요
### 현재 발생 중인 에러
```
Access to fetch at 'https://api.codebridge-x.com/item-master/init'
from origin 'http://localhost:3001'
has been blocked by CORS policy:
Request header field x-api-key is not allowed by
Access-Control-Allow-Headers in preflight response.
```
### 필요한 조치
**API 서버 CORS 설정에 `X-API-Key` 헤더 추가 필요**
```yaml
# 현재 설정 (추정)
Access-Control-Allow-Headers: Content-Type, Authorization
# 필요한 설정
Access-Control-Allow-Headers: Content-Type, Authorization, X-API-Key
```
### 영향받는 엔드포인트
- 모든 Item Master API 엔드포인트 (`/item-master/*`)
- Frontend에서 모든 요청에 `x-api-key` 헤더를 포함하여 전송
### 테스트 방법
```bash
# CORS preflight 테스트
curl -X OPTIONS https://api.codebridge-x.com/item-master/init \
-H "Origin: http://localhost:3001" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: x-api-key" \
-v
# 예상 응답 헤더
Access-Control-Allow-Origin: http://localhost:3001
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-API-Key
```
---
## ⚙️ 우선순위 2: API 엔드포인트 구조 확인
### Frontend에서 호출하는 엔드포인트
| 메서드 | 엔드포인트 | 용도 | 상태 |
|--------|------------|------|------|
| GET | `/item-master/init` | 초기 데이터 로드 (페이지, 섹션, 필드) | ❓ 미확인 |
| POST | `/item-master/pages` | 새 페이지 생성 | ❓ 미확인 |
| PUT | `/item-master/pages/:id` | 페이지 수정 | ❓ 미확인 |
| DELETE | `/item-master/pages/:id` | 페이지 삭제 | ❓ 미확인 |
| POST | `/item-master/sections` | 새 섹션 생성 | ❓ 미확인 |
| PUT | `/item-master/sections/:id` | 섹션 수정 | ❓ 미확인 |
| DELETE | `/item-master/sections/:id` | 섹션 삭제 | ❓ 미확인 |
| POST | `/item-master/fields` | 새 필드 생성 | ❓ 미확인 |
| PUT | `/item-master/fields/:id` | 필드 수정 | ❓ 미확인 |
| DELETE | `/item-master/fields/:id` | 필드 삭제 | ❓ 미확인 |
| POST | `/item-master/bom` | BOM 항목 추가 | ❓ 미확인 |
| PUT | `/item-master/bom/:id` | BOM 항목 수정 | ❓ 미확인 |
| DELETE | `/item-master/bom/:id` | BOM 항목 삭제 | ❓ 미확인 |
### 확인 필요 사항
- [ ] 각 엔드포인트가 구현되어 있는지 확인
- [ ] Base URL이 `https://api.codebridge-x.com`가 맞는지 확인
- [ ] 인증 방식이 `X-API-Key` 헤더 방식이 맞는지 확인
- [ ] Response 형식이 Frontend 기대값과 일치하는지 확인
---
## 🔑 우선순위 3: 환경 변수 및 API 키 확인
### 현재 Frontend 설정
```env
# .env.local
NEXT_PUBLIC_API_URL=https://api.codebridge-x.com
NEXT_PUBLIC_API_KEY=42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a
```
### Frontend 코드에서 사용 중
```typescript
// src/lib/api/item-master.ts
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://api.sam.kr/api/v1';
```
### 문제점
- `.env.local`에는 `NEXT_PUBLIC_API_URL`로 정의
- 코드에서는 `NEXT_PUBLIC_API_BASE_URL` 참조
- 현재는 fallback URL(`http://api.sam.kr/api/v1`)을 사용 중
### 확인 필요 사항
- [ ] Item Master API Base URL이 기존 Auth API와 동일한지 (`https://api.codebridge-x.com`)
- [ ] API 키가 Item Master 엔드포인트에서 유효한지 확인
- [ ] API 키 권한에 Item Master 관련 권한이 포함되어 있는지 확인
### 권장 조치
**옵션 1**: 동일 Base URL 사용
```env
NEXT_PUBLIC_API_URL=https://api.codebridge-x.com
NEXT_PUBLIC_API_KEY=42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a
```
→ Frontend 코드 수정 필요: `NEXT_PUBLIC_API_BASE_URL``NEXT_PUBLIC_API_URL`
**옵션 2**: 별도 Base URL 사용
```env
NEXT_PUBLIC_API_URL=https://api.codebridge-x.com # Auth용
NEXT_PUBLIC_API_BASE_URL=https://api.codebridge-x.com # Item Master용
NEXT_PUBLIC_API_KEY=42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a
```
→ 추가 환경 변수 설정 필요
---
## 📋 예상 API Response 형식
### GET /item-master/init
```typescript
{
"success": true,
"data": {
"itemPages": [
{
"id": number,
"tenant_id": number,
"page_name": string,
"page_order": number,
"item_type": string,
"absolute_path": string | null,
"sections": [
{
"id": number,
"tenant_id": number,
"page_id": number,
"section_title": string,
"section_type": "fields" | "bom_table",
"section_order": number,
"fields": Field[], // section_type이 "fields"일 때
"bomItems": BOMItem[] // section_type이 "bom_table"일 때
}
]
}
]
}
}
```
### Field 타입
```typescript
{
"id": number,
"tenant_id": number,
"section_id": number,
"field_name": string,
"field_type": "text" | "number" | "select" | "date" | "textarea",
"field_order": number,
"is_required": boolean,
"default_value": string | null,
"options": string[] | null, // field_type이 "select"일 때
"validation_rules": object | null,
"created_at": string,
"updated_at": string
}
```
### BOMItem 타입
```typescript
{
"id": number,
"tenant_id": number,
"section_id": number,
"item_code": string | null,
"item_name": string,
"quantity": number,
"unit": string | null,
"unit_price": number | null,
"total_price": number | null,
"spec": string | null,
"note": string | null,
"created_by": number | null,
"updated_by": number | null,
"created_at": string,
"updated_at": string
}
```
---
## ✅ Frontend에서 완료된 작업
### 1. TypeScript 타입 에러 수정 완료
- ✅ BOMItem 생성 시 `section_id`, `updated_at` 누락 수정
- ✅ 미사용 변수 ESLint 에러 해결 (underscore prefix)
- ✅ Navigator API SSR 호환성 수정 (`typeof window` 체크)
- ✅ 상수 조건식 에러 해결 (주석 처리)
- ✅ 미사용 import 제거 (Badge)
**수정 파일**: `/src/components/items/ItemMasterDataManagement/tabs/HierarchyTab/index.tsx`
### 2. API 클라이언트 구현 완료
**파일**: `/src/lib/api/item-master.ts`
구현된 함수:
- `initItemMaster()` - 초기 데이터 로드
- `createItemPage()` - 페이지 생성
- `updateItemPage()` - 페이지 수정
- `deleteItemPage()` - 페이지 삭제
- `createSection()` - 섹션 생성
- `updateSection()` - 섹션 수정
- `deleteSection()` - 섹션 삭제
- `createField()` - 필드 생성
- `updateField()` - 필드 수정
- `deleteField()` - 필드 삭제
- `createBOMItem()` - BOM 항목 생성
- `updateBOMItem()` - BOM 항목 수정
- `deleteBOMItem()` - BOM 항목 삭제
모든 함수에 에러 핸들링 및 로깅 포함
---
## 🧪 테스트 계획 (백엔드 준비 완료 후)
### 1단계: CORS 설정 확인
```bash
curl -X OPTIONS https://api.codebridge-x.com/item-master/init \
-H "Origin: http://localhost:3001" \
-H "Access-Control-Request-Headers: x-api-key" \
-v
```
### 2단계: Init API 테스트
```bash
curl -X GET https://api.codebridge-x.com/item-master/init \
-H "x-api-key: 42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a" \
-v
```
### 3단계: Frontend 통합 테스트
- [ ] 페이지 로드 시 init API 호출 성공
- [ ] 새 페이지 생성 및 저장
- [ ] 섹션 추가/수정/삭제
- [ ] 필드 추가/수정/삭제
- [ ] BOM 항목 추가/수정/삭제
- [ ] 에러 핸들링 (네트워크 에러, 인증 에러 등)
---
## 📞 연락 필요 사항
**백엔드 팀 확인 후 회신 필요:**
1. CORS 설정 완료 예정일
2. Item Master API 엔드포인트 구현 상태
3. API Base URL 및 인증 방식 확인
4. Response 형식 최종 확인
**Frontend 팀 대기 중:**
- 백엔드 준비 완료 후 즉시 통합 테스트 진행 가능
- 현재 TypeScript 컴파일 에러 없음, UI 구현 완료
---
## 📎 참고 파일
- API 클라이언트: `/src/lib/api/item-master.ts`
- Context 정의: `/src/contexts/ItemMasterContext.tsx`
- UI 컴포넌트: `/src/components/items/ItemMasterDataManagement/tabs/HierarchyTab/index.tsx`
- 환경 변수: `/.env.local`

View File

@@ -0,0 +1,971 @@
# 품목기준관리 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[]; // 단위 옵션 목록
materialOptions: MaterialOptionResponse[]; // 재질 옵션 목록
surfaceOptions: SurfaceOptionResponse[]; // 표면처리 옵션 목록
}
```
**중요**: `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" }`
---
### 2.10 재질 옵션 API
#### `GET /api/v1/item-master/material-options`
**목적**: 재질 옵션 목록 조회
**Response**: `MaterialOptionResponse[]`
```typescript
interface MaterialOptionResponse {
id: number;
tenant_id: number;
label: string; // 표시명 (예: "스테인리스")
value: string; // 값 (예: "SUS")
properties?: { // 추가 속성 (선택)
columns?: Array<{ // 멀티 컬럼 설정
key: string;
name: string;
value: string;
}>;
};
created_by: number | null;
created_at: string;
updated_at: string;
}
```
---
#### `POST /api/v1/item-master/material-options`
**목적**: 새 재질 옵션 추가
**Request Body**:
```typescript
interface MaterialOptionRequest {
label: string; // 표시명 (필수)
value: string; // 값 (필수)
properties?: { // 추가 속성 (선택)
columns?: Array<{
key: string;
name: string;
value: string;
}>;
};
}
```
**Response**: `MaterialOptionResponse`
---
#### `PUT /api/v1/item-master/material-options/{id}`
**목적**: 재질 옵션 수정
**Response**: `MaterialOptionResponse`
---
#### `DELETE /api/v1/item-master/material-options/{id}`
**목적**: 재질 옵션 삭제
**Response**: `{ success: true, message: "message.deleted" }`
---
### 2.11 표면처리 옵션 API
#### `GET /api/v1/item-master/surface-options`
**목적**: 표면처리 옵션 목록 조회
**Response**: `SurfaceOptionResponse[]`
```typescript
interface SurfaceOptionResponse {
id: number;
tenant_id: number;
label: string; // 표시명 (예: "아노다이징")
value: string; // 값 (예: "ANODIZING")
properties?: { // 추가 속성 (선택)
columns?: Array<{
key: string;
name: string;
value: string;
}>;
};
created_by: number | null;
created_at: string;
updated_at: string;
}
```
---
#### `POST /api/v1/item-master/surface-options`
**목적**: 새 표면처리 옵션 추가
**Request Body**:
```typescript
interface SurfaceOptionRequest {
label: string; // 표시명 (필수)
value: string; // 값 (필수)
properties?: { // 추가 속성 (선택)
columns?: Array<{
key: string;
name: string;
value: string;
}>;
};
}
```
**Response**: `SurfaceOptionResponse`
---
#### `PUT /api/v1/item-master/surface-options/{id}`
**목적**: 표면처리 옵션 수정
**Response**: `SurfaceOptionResponse`
---
#### `DELETE /api/v1/item-master/surface-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

View File

@@ -0,0 +1,280 @@
# CSS 비교 분석 - 품목 관리 리스트 페이지
**날짜**: 2025-11-17
**React 파일**: `sma-react-v2.0/src/components/ItemManagement.tsx` (lines 1956-2200)
**Next.js 파일**: `sam-react-prod/src/components/items/ItemListClient.tsx` (lines 206-330)
---
## 🔍 발견된 CSS 차이점
### 1. CardTitle (타이틀)
| 항목 | React | Next.js | 상태 |
|------|-------|---------|------|
| className | `text-sm md:text-base` | `text-base font-semibold` | ❌ MISMATCH |
| **수정 필요** | → `text-sm md:text-base` | | |
### 2. TabsList (탭 리스트)
| 항목 | React | Next.js | 상태 |
|------|-------|---------|------|
| 래퍼 div | `overflow-x-auto -mx-2 px-2 mb-6` | 없음 | ❌ MISSING |
| className | `inline-flex w-auto min-w-full md:grid md:w-full md:max-w-2xl md:grid-cols-6` | `grid w-full grid-cols-6 mb-6` | ❌ MISMATCH |
| **수정 필요** | → 래퍼 추가 + React className 적용 | | |
### 3. TabsTrigger (탭 버튼)
| 항목 | React | Next.js | 상태 |
|------|-------|---------|------|
| className | `whitespace-nowrap` | 없음 | ❌ MISSING |
| **수정 필요** | → `whitespace-nowrap` 추가 | | |
### 4. TabsContent
| 항목 | React | Next.js | 상태 |
|------|-------|---------|------|
| className | `mt-0` | `mt-0` | ✅ MATCH |
### 5. 테이블 래퍼
| 항목 | React | Next.js | 상태 |
|------|-------|---------|------|
| className | `hidden lg:block rounded-md border` | `border rounded-lg overflow-hidden` | ❌ MISMATCH |
| **수정 필요** | → `hidden lg:block rounded-md border` | | |
---
## 📋 테이블 구조 차이점
### **TableHeader - 컬럼 구조**
#### React 컬럼 순서 (8개):
1. 체크박스 (`w-[50px]`)
2. **번호** (`hidden md:table-cell`) ⭐
3. **품목코드** (`min-w-[100px]`)
4. **품목유형** (`min-w-[80px]`)
5. **품목명** (`min-w-[120px]`)
6. **규격** (`hidden md:table-cell`)
7. **단위** (`hidden md:table-cell`)
8. **작업** (`text-right min-w-[100px]`)
#### Next.js 목표 컬럼 순서 (10개) - 개선안:
1.**체크박스** (`w-[50px]`) - 추가 필요
2.**번호** (`hidden md:table-cell`) - 추가 필요
3. **품목 코드** (`min-w-[100px]`) - width 수정
4. **품목유형** (`min-w-[80px]`) - 위치 이동
5. **품목명** (`min-w-[120px]`) - 위치 이동
6. **규격** (`hidden md:table-cell`) - 위치 이동
7. **단위** (`hidden md:table-cell`) - 위치 이동
8. ~~**판매 단가**~~ - 🚨 **제거**
9. **품목 상태** (`w-[80px]`) - ✅ **유지** (컬럼명 변경: "상태" → "품목 상태")
10. **작업** (`text-right min-w-[100px]`) - 정렬 수정
### 🚨 주요 문제점
| # | 문제 | React | Next.js | 개선안 |
|---|------|-------|---------|---------|
| 1 | 체크박스 컬럼 | ✅ 있음 (`w-[50px]`) | ❌ 없음 | ✅ 추가 |
| 2 | 번호 컬럼 | ✅ 있음 (`hidden md:table-cell`) | ❌ 없음 | ✅ 추가 |
| 3 | 품목코드 width | `min-w-[100px]` | `w-[120px]` | ✅ `min-w-[100px]`로 수정 |
| 4 | 컬럼 순서 | 코드→유형→명→규격→단위 | 코드→명→유형→단위→규격 | ✅ React 순서로 변경 |
| 5 | 판매단가 | ❌ 없음 | ✅ 있음 | 🚨 **제거** |
| 6 | 품목 상태 | ❌ 없음 | ✅ 있음 ("상태") | ✅ **유지** (컬럼명: "품목 상태") |
| 7 | 작업 정렬 | `text-right` | `text-center` ❌ | ✅ `text-right`로 수정 |
---
## 🎨 TableCell 상세 CSS 비교
### 번호 컬럼 (React만 있음)
```tsx
// React
<TableCell className="text-muted-foreground cursor-pointer hidden md:table-cell">
{filteredItems.length - (startIndex + index)}
</TableCell>
// Next.js: 없음 (추가 필요)
```
### 품목코드 컬럼
```tsx
// React
<TableCell className="cursor-pointer">
<code className="text-xs bg-gray-100 px-2 py-1 rounded">
{formatItemCodeForAssembly(item) || '-'}
</code>
</TableCell>
// Next.js
<TableCell className="font-mono text-sm">
{item.itemCode}
</TableCell>
```
**차이점**:
-`cursor-pointer` 누락
-`<code>` 태그 없음
-`text-xs bg-gray-100 px-2 py-1 rounded` 배경색 스타일 없음
### 품목유형 컬럼
```tsx
// React
<TableCell className="cursor-pointer">
{getItemTypeBadge(item.itemType)}
{/* + 부품인 경우 추가 뱃지 */}
</TableCell>
// Next.js
<TableCell>
<Badge variant="outline">
{ITEM_TYPE_LABELS[item.itemType]}
</Badge>
</TableCell>
```
**차이점**:
-`cursor-pointer` 누락
-`getItemTypeBadge()` 함수 사용 안함 (색상 없음)
- ❌ 부품 타입별 추가 뱃지 없음
### 품목명 컬럼
```tsx
// React
<TableCell className="cursor-pointer">
<div className="flex items-center gap-2">
<span className="truncate max-w-[150px] md:max-w-none">{item.itemName}</span>
{/* + 견적산출용 뱃지 */}
</div>
</TableCell>
// Next.js
<TableCell className="font-medium">
{item.itemName}
</TableCell>
```
**차이점**:
-`cursor-pointer` 누락
-`flex items-center gap-2` 구조 없음
-`truncate max-w-[150px] md:max-w-none` 말줄임 없음
- ❌ 견적산출용 뱃지 없음
### 규격 컬럼
```tsx
// React
<TableCell className="text-sm text-muted-foreground cursor-pointer hidden md:table-cell">
{item.itemCode?.includes('-') ? item.itemCode.split('-').slice(1).join('-') : (item.specification || "-")}
</TableCell>
// Next.js
<TableHead>규격</TableHead>
<TableCell className="text-sm text-gray-600">
{item.specification || '-'}
</TableCell>
```
**차이점**:
-`cursor-pointer` 누락
-`hidden md:table-cell` 반응형 숨김 없음
-`text-muted-foreground``text-gray-600` (다른 색상)
- ❌ itemCode 파싱 로직 없음
### 단위 컬럼
```tsx
// React
<TableCell className="cursor-pointer hidden md:table-cell">
<Badge variant="secondary">{item.unit || "-"}</Badge>
</TableCell>
// Next.js
<TableHead className="w-[80px]">단위</TableHead>
<TableCell>{item.unit}</TableCell>
```
**차이점**:
-`cursor-pointer` 누락
-`hidden md:table-cell` 반응형 숨김 없음
-`<Badge>` 없음 (단순 텍스트)
### 작업 컬럼
```tsx
// React
<TableHead className="text-right min-w-[100px]">작업</TableHead>
<TableCell className="text-right">
<TableActionButtons
onView={() => handleViewChange("view", item)}
onEdit={() => handleViewChange("edit", item)}
onDelete={() => {...}}
/>
</TableCell>
// Next.js
<TableHead className="w-[150px] text-center">작업</TableHead>
<TableCell>
<div className="flex items-center justify-center gap-1">
<Button variant="ghost" size="sm">
<Eye className="w-4 h-4" /> {/* ❌ 아이콘 틀림 */}
</Button>
{/* ... */}
</div>
</TableCell>
```
**차이점**:
-`text-right``text-center` (정렬 틀림)
-`min-w-[100px]``w-[150px]`
-`TableActionButtons` 컴포넌트 대신 직접 구현
- ❌ 아이콘: `Search``Eye` (돋보기 → 눈)
---
## 📝 수정 체크리스트
### 구조 변경
- [ ] CardTitle: `text-sm md:text-base` 적용
- [ ] TabsList 래퍼 div 추가: `overflow-x-auto -mx-2 px-2 mb-6`
- [ ] TabsList: `inline-flex w-auto min-w-full md:grid md:w-full md:max-w-2xl md:grid-cols-6`
- [ ] TabsTrigger: `whitespace-nowrap` 추가
- [ ] 테이블 래퍼: `hidden lg:block rounded-md border`
### 테이블 컬럼 재구성
- [ ] 체크박스 컬럼 추가 (첫 번째, `w-[50px]`)
- [ ] 번호 컬럼 추가 (두 번째, `hidden md:table-cell`)
- [ ] 컬럼 순서 변경: 체크박스 → 번호 → 코드 → 유형 → 명 → 규격 → 단위 → 품목상태 → 작업
- [ ] 판매단가 컬럼 제거 🚨
- [ ] 상태 컬럼명 변경: "상태" → "품목 상태" ✅ (유지)
- [ ] 작업 컬럼 정렬: `text-center``text-right`, width: `w-[150px]``min-w-[100px]`
### CSS 클래스 적용
- [ ] 품목코드: `cursor-pointer` + `<code>` 태그 + `text-xs bg-gray-100 px-2 py-1 rounded`
- [ ] 품목유형: `cursor-pointer` + `getItemTypeBadge()` 함수 사용
- [ ] 품목명: `cursor-pointer` + `flex items-center gap-2` + `truncate max-w-[150px] md:max-w-none`
- [ ] 규격: `cursor-pointer hidden md:table-cell text-muted-foreground` + itemCode 파싱 로직
- [ ] 단위: `cursor-pointer hidden md:table-cell` + `<Badge variant="secondary">`
- [ ] 작업: `text-right` + `Search` 아이콘
### 기능 추가
- [ ] `getItemTypeBadge()` 함수 구현 (유형별 색상)
- [ ] `formatItemCodeForAssembly()` 함수 구현
- [ ] 체크박스 선택 기능
- [ ] 견적산출용 뱃지 로직
- [ ] 부품 타입별 추가 뱃지
---
## 🎯 우선순위
### 긴급 (시각적 영향 큼)
1. 번호 컬럼 추가
2. 품목코드 배경색 (`bg-gray-100`)
3. 품목유형 색상 (Badge)
4. 컬럼 순서 변경
5. 작업 정렬 수정 (`text-center``text-right`)
### 중요
6. 체크박스 컬럼 추가
7. 판매단가 컬럼 제거 🚨
8. 상태 컬럼명 변경: "상태" → "품목 상태" ✅
9. 아이콘 변경 (Eye → Search)
10. TabsList 반응형
### 보통
11. cursor-pointer 일괄 적용
12. 견적산출용 뱃지
13. 부품 타입 뱃지