chore: 구버전 문서 정리 (docs 저장소로 이관)
- Item Master 관련 구버전 문서 삭제 (docs 저장소로 이관) - 프론트엔드 요청서 삭제 (history 폴더로 아카이브) - HR API 규칙 문서 삭제 (docs 저장소로 통합)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,841 +0,0 @@
|
||||
# 품목기준관리 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
|
||||
@@ -1,120 +0,0 @@
|
||||
# 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": "스펙정보" }]
|
||||
}
|
||||
```
|
||||
@@ -1,588 +0,0 @@
|
||||
# 품목기준관리 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`
|
||||
@@ -1,220 +0,0 @@
|
||||
# Attendance API (근태관리 API) 규칙
|
||||
|
||||
## 개요
|
||||
|
||||
근태관리 API는 테넌트 내 사용자의 출퇴근 및 근태 정보를 관리하는 API입니다.
|
||||
`attendances` 테이블을 사용하며, 상세 출퇴근 정보는 `json_details` 필드에 저장합니다.
|
||||
|
||||
## 핵심 모델
|
||||
|
||||
### Attendance
|
||||
|
||||
- **위치**: `App\Models\Tenants\Attendance`
|
||||
- **역할**: 일별 근태 기록
|
||||
- **특징**:
|
||||
- `BelongsToTenant` 트레이트 사용 (멀티테넌트 자동 스코핑)
|
||||
- `SoftDeletes` 적용
|
||||
- `json_details` 필드에 상세 출퇴근 정보 저장
|
||||
|
||||
## 엔드포인트
|
||||
|
||||
| Method | Path | 설명 |
|
||||
|--------|------|------|
|
||||
| GET | `/v1/attendances` | 근태 목록 조회 |
|
||||
| GET | `/v1/attendances/{id}` | 근태 상세 조회 |
|
||||
| POST | `/v1/attendances` | 근태 등록 |
|
||||
| PATCH | `/v1/attendances/{id}` | 근태 수정 |
|
||||
| DELETE | `/v1/attendances/{id}` | 근태 삭제 |
|
||||
| DELETE | `/v1/attendances/bulk` | 근태 일괄 삭제 |
|
||||
| POST | `/v1/attendances/check-in` | 출근 기록 |
|
||||
| POST | `/v1/attendances/check-out` | 퇴근 기록 |
|
||||
| GET | `/v1/attendances/monthly-stats` | 월간 통계 |
|
||||
|
||||
## 데이터 구조
|
||||
|
||||
### 기본 필드
|
||||
|
||||
| 필드 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| `id` | int | PK |
|
||||
| `tenant_id` | int | 테넌트 ID |
|
||||
| `user_id` | int | 사용자 ID (FK → users) |
|
||||
| `base_date` | date | 기준 일자 |
|
||||
| `status` | string | 근태 상태 |
|
||||
| `json_details` | json | 상세 출퇴근 정보 |
|
||||
| `remarks` | string | 비고 (500자 제한) |
|
||||
| `created_by` | int | 생성자 |
|
||||
| `updated_by` | int | 수정자 |
|
||||
| `deleted_by` | int | 삭제자 |
|
||||
| `deleted_at` | timestamp | Soft Delete |
|
||||
|
||||
### 근태 상태 (status)
|
||||
|
||||
| 상태 | 설명 |
|
||||
|------|------|
|
||||
| `onTime` | 정상 출근 (기본값) |
|
||||
| `late` | 지각 |
|
||||
| `absent` | 결근 |
|
||||
| `vacation` | 휴가 |
|
||||
| `businessTrip` | 출장 |
|
||||
| `fieldWork` | 외근 |
|
||||
| `overtime` | 야근 |
|
||||
| `remote` | 재택근무 |
|
||||
|
||||
### json_details 필드 구조
|
||||
|
||||
```json
|
||||
{
|
||||
"check_in": "09:00:00",
|
||||
"check_out": "18:00:00",
|
||||
"gps_data": {
|
||||
"check_in": {
|
||||
"lat": 37.5665,
|
||||
"lng": 126.9780,
|
||||
"accuracy": 10
|
||||
},
|
||||
"check_out": {
|
||||
"lat": 37.5665,
|
||||
"lng": 126.9780,
|
||||
"accuracy": 10
|
||||
}
|
||||
},
|
||||
"external_work": {
|
||||
"location": "고객사",
|
||||
"purpose": "미팅",
|
||||
"start_time": "14:00:00",
|
||||
"end_time": "16:00:00"
|
||||
},
|
||||
"multiple_entries": [
|
||||
{ "in": "09:00:00", "out": "12:00:00" },
|
||||
{ "in": "13:00:00", "out": "18:00:00" }
|
||||
],
|
||||
"work_minutes": 480,
|
||||
"overtime_minutes": 60,
|
||||
"late_minutes": 30,
|
||||
"early_leave_minutes": 0,
|
||||
"vacation_type": "annual|half|sick"
|
||||
}
|
||||
```
|
||||
|
||||
### 허용된 json_details 키
|
||||
|
||||
```php
|
||||
$allowedKeys = [
|
||||
'check_in', // 출근 시간 (HH:MM:SS)
|
||||
'check_out', // 퇴근 시간 (HH:MM:SS)
|
||||
'gps_data', // GPS 데이터 (출퇴근 위치)
|
||||
'external_work', // 외근 정보
|
||||
'multiple_entries', // 다중 출퇴근 기록
|
||||
'work_minutes', // 총 근무 시간 (분)
|
||||
'overtime_minutes', // 초과 근무 시간 (분)
|
||||
'late_minutes', // 지각 시간 (분)
|
||||
'early_leave_minutes',// 조퇴 시간 (분)
|
||||
'vacation_type', // 휴가 유형
|
||||
];
|
||||
```
|
||||
|
||||
## 비즈니스 규칙
|
||||
|
||||
### 출근 기록 (check-in)
|
||||
|
||||
1. 오늘 기록이 있으면 업데이트, 없으면 새로 생성
|
||||
2. `check_in` 시간과 GPS 데이터 저장
|
||||
3. 출근 시간 기준으로 상태 자동 결정 (09:00 기준 지각 판단)
|
||||
|
||||
```php
|
||||
// 상태 자동 결정 로직
|
||||
if ($checkIn > '09:00:00') {
|
||||
$status = 'late';
|
||||
} else {
|
||||
$status = 'onTime';
|
||||
}
|
||||
```
|
||||
|
||||
### 퇴근 기록 (check-out)
|
||||
|
||||
1. 오늘 출근 기록이 없으면 에러 반환
|
||||
2. `check_out` 시간과 GPS 데이터 저장
|
||||
3. 근무 시간(work_minutes) 자동 계산
|
||||
|
||||
```php
|
||||
// 근무 시간 계산
|
||||
$checkIn = Carbon::createFromFormat('H:i:s', $jsonDetails['check_in']);
|
||||
$checkOut = Carbon::createFromFormat('H:i:s', $checkOutTime);
|
||||
$jsonDetails['work_minutes'] = $checkOut->diffInMinutes($checkIn);
|
||||
```
|
||||
|
||||
### 근태 등록 (store)
|
||||
|
||||
1. 같은 날 같은 사용자 기록이 있으면 에러 반환
|
||||
2. `json_details` 직접 전달 또는 개별 필드에서 구성
|
||||
|
||||
```php
|
||||
// json_details 처리 방식
|
||||
$jsonDetails = isset($data['json_details']) && is_array($data['json_details'])
|
||||
? $data['json_details']
|
||||
: $this->buildJsonDetails($data);
|
||||
```
|
||||
|
||||
### 월간 통계 (monthly-stats)
|
||||
|
||||
통계 항목:
|
||||
- 총 근무일수
|
||||
- 상태별 일수 (정상, 지각, 결근, 휴가, 출장, 외근, 야근, 재택)
|
||||
- 총 근무 시간 (분)
|
||||
- 총 초과 근무 시간 (분)
|
||||
|
||||
## 검색/필터 파라미터
|
||||
|
||||
| 파라미터 | 타입 | 설명 |
|
||||
|----------|------|------|
|
||||
| `user_id` | int | 사용자 필터 |
|
||||
| `date` | date | 특정 날짜 필터 |
|
||||
| `date_from` | date | 시작 날짜 |
|
||||
| `date_to` | date | 종료 날짜 |
|
||||
| `status` | string | 근태 상태 필터 |
|
||||
| `department_id` | int | 부서 필터 (사용자의 부서) |
|
||||
| `sort_by` | string | 정렬 기준 (기본: base_date) |
|
||||
| `sort_dir` | string | 정렬 방향 (기본: desc) |
|
||||
| `per_page` | int | 페이지당 항목 수 (기본: 20) |
|
||||
|
||||
## 관계 (Relationships)
|
||||
|
||||
```php
|
||||
public function user(): BelongsTo // 사용자 정보
|
||||
public function creator(): BelongsTo // 생성자
|
||||
public function updater(): BelongsTo // 수정자
|
||||
```
|
||||
|
||||
## 스코프 (Scopes)
|
||||
|
||||
```php
|
||||
$query->onDate('2024-01-15'); // 특정 날짜
|
||||
$query->betweenDates('2024-01-01', '2024-01-31'); // 날짜 범위
|
||||
$query->forUser(123); // 특정 사용자
|
||||
$query->withStatus('late'); // 특정 상태
|
||||
```
|
||||
|
||||
## Accessor
|
||||
|
||||
```php
|
||||
$attendance->check_in; // json_details['check_in']
|
||||
$attendance->check_out; // json_details['check_out']
|
||||
$attendance->gps_data; // json_details['gps_data']
|
||||
$attendance->external_work; // json_details['external_work']
|
||||
$attendance->multiple_entries; // json_details['multiple_entries']
|
||||
$attendance->work_minutes; // json_details['work_minutes']
|
||||
$attendance->overtime_minutes; // json_details['overtime_minutes']
|
||||
$attendance->late_minutes; // json_details['late_minutes']
|
||||
$attendance->early_leave_minutes;// json_details['early_leave_minutes']
|
||||
$attendance->vacation_type; // json_details['vacation_type']
|
||||
```
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **중복 방지**: 같은 날짜 + 같은 사용자 조합은 유일해야 함
|
||||
2. **멀티테넌트**: BelongsToTenant 트레이트로 자동 스코핑
|
||||
3. **Soft Delete**: deleted_by 기록 후 삭제
|
||||
4. **Audit**: created_by/updated_by 자동 기록
|
||||
5. **시간 형식**: check_in/check_out은 HH:MM:SS 형식
|
||||
6. **표준 출근 시간**: 기본 09:00:00 (회사별 설정 필요)
|
||||
@@ -1,258 +0,0 @@
|
||||
# Department Tree API (부서트리 조회 API) 규칙
|
||||
|
||||
## 개요
|
||||
|
||||
부서트리 API는 테넌트 내 조직도를 계층 구조로 조회하는 API입니다.
|
||||
`departments` 테이블의 `parent_id`를 통한 자기참조 관계로 무한 depth 계층 구조를 지원합니다.
|
||||
|
||||
## 핵심 모델
|
||||
|
||||
### Department
|
||||
|
||||
- **위치**: `App\Models\Tenants\Department`
|
||||
- **역할**: 부서/조직 정보
|
||||
- **특징**:
|
||||
- `parent_id` 자기참조로 계층 구조
|
||||
- `HasRoles` 트레이트 (부서도 권한/역할 보유 가능)
|
||||
- `ModelTrait` 적용 (is_active, 날짜 처리)
|
||||
|
||||
## 엔드포인트
|
||||
|
||||
### 부서 트리 전용
|
||||
|
||||
| Method | Path | 설명 |
|
||||
|--------|------|------|
|
||||
| GET | `/v1/departments/tree` | 부서 트리 조회 |
|
||||
|
||||
### 기본 CRUD (참고)
|
||||
|
||||
| Method | Path | 설명 |
|
||||
|--------|------|------|
|
||||
| GET | `/v1/departments` | 부서 목록 조회 |
|
||||
| GET | `/v1/departments/{id}` | 부서 상세 조회 |
|
||||
| POST | `/v1/departments` | 부서 생성 |
|
||||
| PATCH | `/v1/departments/{id}` | 부서 수정 |
|
||||
| DELETE | `/v1/departments/{id}` | 부서 삭제 |
|
||||
|
||||
### 부서-사용자 관리
|
||||
|
||||
| Method | Path | 설명 |
|
||||
|--------|------|------|
|
||||
| GET | `/v1/departments/{id}/users` | 부서 사용자 목록 |
|
||||
| POST | `/v1/departments/{id}/users` | 사용자 배정 |
|
||||
| DELETE | `/v1/departments/{id}/users/{user}` | 사용자 제거 |
|
||||
| PATCH | `/v1/departments/{id}/users/{user}/primary` | 주부서 설정 |
|
||||
|
||||
## 데이터 구조
|
||||
|
||||
### 기본 필드
|
||||
|
||||
| 필드 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| `id` | int | PK |
|
||||
| `tenant_id` | int | 테넌트 ID |
|
||||
| `parent_id` | int | 상위 부서 ID (nullable, 최상위는 null) |
|
||||
| `code` | string | 부서 코드 (unique) |
|
||||
| `name` | string | 부서명 |
|
||||
| `description` | string | 부서 설명 |
|
||||
| `is_active` | bool | 활성화 상태 |
|
||||
| `sort_order` | int | 정렬 순서 |
|
||||
| `created_by` | int | 생성자 |
|
||||
| `updated_by` | int | 수정자 |
|
||||
| `deleted_by` | int | 삭제자 |
|
||||
|
||||
### 트리 응답 구조
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"tenant_id": 1,
|
||||
"parent_id": null,
|
||||
"code": "DEPT001",
|
||||
"name": "경영지원본부",
|
||||
"is_active": true,
|
||||
"sort_order": 1,
|
||||
"children": [
|
||||
{
|
||||
"id": 2,
|
||||
"tenant_id": 1,
|
||||
"parent_id": 1,
|
||||
"code": "DEPT002",
|
||||
"name": "인사팀",
|
||||
"is_active": true,
|
||||
"sort_order": 1,
|
||||
"children": [],
|
||||
"users": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"tenant_id": 1,
|
||||
"parent_id": 1,
|
||||
"code": "DEPT003",
|
||||
"name": "재무팀",
|
||||
"is_active": true,
|
||||
"sort_order": 2,
|
||||
"children": [],
|
||||
"users": []
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
{ "id": 1, "name": "홍길동", "email": "hong@example.com" }
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 트리 조회 로직
|
||||
|
||||
### tree() 메서드 구현
|
||||
|
||||
```php
|
||||
public function tree(array $params = []): array
|
||||
{
|
||||
// 1. 파라미터 검증
|
||||
$withUsers = filter_var($params['with_users'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
// 2. 최상위 부서 조회 (parent_id가 null)
|
||||
$query = Department::query()
|
||||
->whereNull('parent_id')
|
||||
->orderBy('sort_order')
|
||||
->orderBy('name');
|
||||
|
||||
// 3. 재귀적으로 자식 부서 로드
|
||||
$query->with(['children' => function ($q) use ($withUsers) {
|
||||
$q->orderBy('sort_order')->orderBy('name');
|
||||
$this->loadChildrenRecursive($q, $withUsers);
|
||||
}]);
|
||||
|
||||
// 4. 사용자 포함 옵션
|
||||
if ($withUsers) {
|
||||
$query->with(['users:id,name,email']);
|
||||
}
|
||||
|
||||
return $query->get()->toArray();
|
||||
}
|
||||
|
||||
// 재귀 로딩 헬퍼
|
||||
private function loadChildrenRecursive($query, bool $withUsers): void
|
||||
{
|
||||
$query->with(['children' => function ($q) use ($withUsers) {
|
||||
$q->orderBy('sort_order')->orderBy('name');
|
||||
$this->loadChildrenRecursive($q, $withUsers);
|
||||
}]);
|
||||
|
||||
if ($withUsers) {
|
||||
$query->with(['users:id,name,email']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 정렬 규칙
|
||||
|
||||
1. `sort_order` 오름차순
|
||||
2. `name` 오름차순 (동일 sort_order일 때)
|
||||
|
||||
## 요청 파라미터
|
||||
|
||||
### GET /v1/departments/tree
|
||||
|
||||
| 파라미터 | 타입 | 기본값 | 설명 |
|
||||
|----------|------|--------|------|
|
||||
| `with_users` | bool | false | 부서별 사용자 목록 포함 |
|
||||
|
||||
### 예시
|
||||
|
||||
```bash
|
||||
# 기본 트리 조회
|
||||
GET /v1/departments/tree
|
||||
|
||||
# 사용자 포함 트리 조회
|
||||
GET /v1/departments/tree?with_users=1
|
||||
```
|
||||
|
||||
## 관계 (Relationships)
|
||||
|
||||
```php
|
||||
public function parent(): BelongsTo // 상위 부서
|
||||
public function children() // 하위 부서들 (HasMany)
|
||||
public function users() // 소속 사용자들 (BelongsToMany)
|
||||
public function departmentUsers() // 부서-사용자 pivot (HasMany)
|
||||
public function permissionOverrides() // 권한 오버라이드 (MorphMany)
|
||||
```
|
||||
|
||||
## 부서-사용자 관계 (Pivot)
|
||||
|
||||
### department_user 테이블
|
||||
|
||||
| 필드 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| `department_id` | int | 부서 ID |
|
||||
| `user_id` | int | 사용자 ID |
|
||||
| `tenant_id` | int | 테넌트 ID |
|
||||
| `is_primary` | bool | 주부서 여부 |
|
||||
| `joined_at` | timestamp | 배정일 |
|
||||
| `left_at` | timestamp | 해제일 |
|
||||
| `deleted_at` | timestamp | Soft Delete |
|
||||
|
||||
### 주부서 규칙
|
||||
|
||||
- 한 사용자는 여러 부서에 소속 가능
|
||||
- 주부서(`is_primary`)는 사용자당 1개만 가능
|
||||
- 주부서 설정 시 기존 주부서는 자동 해제
|
||||
|
||||
## 권한 관리
|
||||
|
||||
### 부서 권한 시스템
|
||||
|
||||
부서는 Spatie Permission과 연동되어 권한을 가질 수 있습니다.
|
||||
|
||||
- **ALLOW**: `model_has_permissions` 테이블
|
||||
- **DENY**: `permission_overrides` 테이블 (effect: -1)
|
||||
|
||||
### 관련 엔드포인트
|
||||
|
||||
| Method | Path | 설명 |
|
||||
|--------|------|------|
|
||||
| GET | `/v1/departments/{id}/permissions` | 부서 권한 목록 |
|
||||
| POST | `/v1/departments/{id}/permissions` | 권한 부여/차단 |
|
||||
| DELETE | `/v1/departments/{id}/permissions/{permission}` | 권한 제거 |
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **무한 재귀 방지**: Eloquent eager loading으로 처리, 별도 depth 제한 없음
|
||||
2. **성능 고려**: 대규모 조직도의 경우 `with_users` 사용 시 응답 시간 증가
|
||||
3. **정렬 일관성**: 모든 레벨에서 동일한 정렬 규칙 적용
|
||||
4. **멀티테넌트**: tenant_id 기반 자동 스코핑
|
||||
5. **주부서 제약**: 사용자당 주부서 1개만 허용
|
||||
6. **Soft Delete**: department_user pivot도 Soft Delete 적용
|
||||
|
||||
## 트리 구축 예시
|
||||
|
||||
### 조직도 예시
|
||||
|
||||
```
|
||||
경영지원본부 (parent_id: null)
|
||||
├── 인사팀 (parent_id: 1)
|
||||
│ ├── 채용파트 (parent_id: 2)
|
||||
│ └── 교육파트 (parent_id: 2)
|
||||
├── 재무팀 (parent_id: 1)
|
||||
└── 총무팀 (parent_id: 1)
|
||||
|
||||
개발본부 (parent_id: null)
|
||||
├── 프론트엔드팀 (parent_id: 4)
|
||||
├── 백엔드팀 (parent_id: 4)
|
||||
└── QA팀 (parent_id: 4)
|
||||
```
|
||||
|
||||
### SQL 예시 (데이터 삽입)
|
||||
|
||||
```sql
|
||||
-- 최상위 부서
|
||||
INSERT INTO departments (tenant_id, parent_id, code, name, sort_order)
|
||||
VALUES (1, NULL, 'HQ', '경영지원본부', 1);
|
||||
|
||||
-- 하위 부서
|
||||
INSERT INTO departments (tenant_id, parent_id, code, name, sort_order)
|
||||
VALUES (1, 1, 'HR', '인사팀', 1);
|
||||
```
|
||||
@@ -1,181 +0,0 @@
|
||||
# Employee API (사원관리 API) 규칙
|
||||
|
||||
## 개요
|
||||
|
||||
사원관리 API는 테넌트 내 사원 정보를 관리하는 API입니다.
|
||||
`users` 테이블과 `tenant_user_profiles` 테이블을 조합하여 사원 정보를 구성합니다.
|
||||
|
||||
## 핵심 모델
|
||||
|
||||
### TenantUserProfile
|
||||
|
||||
- **위치**: `App\Models\Tenants\TenantUserProfile`
|
||||
- **역할**: 테넌트별 사용자 프로필 (사원 정보)
|
||||
- **특징**: `json_extra` 필드에 사원 상세 정보 저장
|
||||
|
||||
### User
|
||||
|
||||
- **위치**: `App\Models\Members\User`
|
||||
- **역할**: 기본 사용자 계정 (이름, 이메일, 비밀번호)
|
||||
|
||||
## 엔드포인트
|
||||
|
||||
| Method | Path | 설명 |
|
||||
|--------|------|------|
|
||||
| GET | `/v1/employees` | 사원 목록 조회 |
|
||||
| GET | `/v1/employees/{id}` | 사원 상세 조회 |
|
||||
| POST | `/v1/employees` | 사원 등록 |
|
||||
| PATCH | `/v1/employees/{id}` | 사원 수정 |
|
||||
| DELETE | `/v1/employees/{id}` | 사원 삭제 (상태 변경) |
|
||||
| DELETE | `/v1/employees/bulk` | 사원 일괄 삭제 |
|
||||
| GET | `/v1/employees/stats` | 사원 통계 |
|
||||
| POST | `/v1/employees/{id}/account` | 시스템 계정 생성 |
|
||||
|
||||
## 데이터 구조
|
||||
|
||||
### 기본 필드 (TenantUserProfile)
|
||||
|
||||
| 필드 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| `tenant_id` | int | 테넌트 ID |
|
||||
| `user_id` | int | 사용자 ID (FK → users) |
|
||||
| `department_id` | int | 부서 ID (nullable) |
|
||||
| `position_key` | string | 직위 코드 |
|
||||
| `job_title_key` | string | 직책 코드 |
|
||||
| `work_location_key` | string | 근무지 코드 |
|
||||
| `employment_type_key` | string | 고용 형태 코드 |
|
||||
| `employee_status` | string | 고용 상태 (active/leave/resigned) |
|
||||
| `manager_user_id` | int | 상위 관리자 ID (nullable) |
|
||||
| `profile_photo_path` | string | 프로필 사진 경로 |
|
||||
| `display_name` | string | 표시명 |
|
||||
| `json_extra` | json | 확장 사원 정보 |
|
||||
|
||||
### json_extra 필드 구조
|
||||
|
||||
```json
|
||||
{
|
||||
"employee_code": "EMP001",
|
||||
"resident_number": "encrypted_value",
|
||||
"gender": "male|female",
|
||||
"address": "서울시 강남구...",
|
||||
"salary": 5000000,
|
||||
"hire_date": "2024-01-15",
|
||||
"rank": "대리",
|
||||
"bank_account": {
|
||||
"bank": "국민은행",
|
||||
"account": "123-456-789",
|
||||
"holder": "홍길동"
|
||||
},
|
||||
"work_type": "regular|contract|part_time",
|
||||
"contract_info": {
|
||||
"start_date": "2024-01-15",
|
||||
"end_date": "2025-01-14"
|
||||
},
|
||||
"emergency_contact": {
|
||||
"name": "김부모",
|
||||
"phone": "010-1234-5678",
|
||||
"relation": "부모"
|
||||
},
|
||||
"education": [],
|
||||
"certifications": []
|
||||
}
|
||||
```
|
||||
|
||||
### 허용된 json_extra 키
|
||||
|
||||
```php
|
||||
$allowedKeys = [
|
||||
'employee_code', // 사원번호
|
||||
'resident_number', // 주민등록번호 (암호화 필수)
|
||||
'gender', // 성별
|
||||
'address', // 주소
|
||||
'salary', // 급여
|
||||
'hire_date', // 입사일
|
||||
'rank', // 직급
|
||||
'bank_account', // 급여계좌
|
||||
'work_type', // 근무유형
|
||||
'contract_info', // 계약 정보
|
||||
'emergency_contact', // 비상연락처
|
||||
'education', // 학력
|
||||
'certifications', // 자격증
|
||||
];
|
||||
```
|
||||
|
||||
## 비즈니스 규칙
|
||||
|
||||
### 사원 등록 (store)
|
||||
|
||||
1. `users` 테이블에 사용자 생성
|
||||
2. `user_tenants` pivot에 관계 추가 (is_default: true)
|
||||
3. `tenant_user_profiles` 생성
|
||||
4. `json_extra`에 사원 정보 설정
|
||||
|
||||
```php
|
||||
// 자동 생성되는 user_id 형식
|
||||
$userId = strtolower(explode('@', $email)[0] . '_' . Str::random(4));
|
||||
```
|
||||
|
||||
### 사원 삭제 (destroy)
|
||||
|
||||
- **Hard Delete 하지 않음**
|
||||
- `employee_status`를 `resigned`로 변경
|
||||
- 사용자 계정은 유지됨
|
||||
|
||||
### 사원 상태 (employee_status)
|
||||
|
||||
| 상태 | 설명 |
|
||||
|------|------|
|
||||
| `active` | 재직 중 |
|
||||
| `leave` | 휴직 |
|
||||
| `resigned` | 퇴사 |
|
||||
|
||||
### 시스템 계정 (has_account)
|
||||
|
||||
- 시스템 계정 = `users.password`가 NULL이 아닌 경우
|
||||
- `POST /employees/{id}/account`로 비밀번호 설정 시 계정 생성
|
||||
- 첫 로그인 시 비밀번호 변경 필요 (`must_change_password: true`)
|
||||
|
||||
## 검색/필터 파라미터
|
||||
|
||||
| 파라미터 | 타입 | 설명 |
|
||||
|----------|------|------|
|
||||
| `q` | string | 이름/이메일/사원코드 검색 |
|
||||
| `status` | string | 고용 상태 필터 |
|
||||
| `department_id` | int | 부서 필터 |
|
||||
| `has_account` | bool | 시스템 계정 보유 여부 |
|
||||
| `sort_by` | string | 정렬 기준 (기본: created_at) |
|
||||
| `sort_dir` | string | 정렬 방향 (asc/desc) |
|
||||
| `per_page` | int | 페이지당 항목 수 (기본: 20) |
|
||||
|
||||
## 관계 (Relationships)
|
||||
|
||||
```php
|
||||
// TenantUserProfile
|
||||
public function user(): BelongsTo // 기본 사용자 정보
|
||||
public function department(): BelongsTo // 소속 부서
|
||||
public function manager(): BelongsTo // 상위 관리자
|
||||
```
|
||||
|
||||
## 스코프 (Scopes)
|
||||
|
||||
```php
|
||||
$query->active(); // employee_status = 'active'
|
||||
$query->onLeave(); // employee_status = 'leave'
|
||||
$query->resigned(); // employee_status = 'resigned'
|
||||
```
|
||||
|
||||
## Accessor
|
||||
|
||||
```php
|
||||
$profile->employee_code; // json_extra['employee_code']
|
||||
$profile->hire_date; // json_extra['hire_date']
|
||||
$profile->address; // json_extra['address']
|
||||
$profile->emergency_contact; // json_extra['emergency_contact']
|
||||
```
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **주민등록번호**: 반드시 암호화하여 저장
|
||||
2. **멀티테넌트**: tenant_id 자동 스코핑
|
||||
3. **Audit**: created_by/updated_by 자동 기록
|
||||
4. **삭제**: Hard Delete 금지, employee_status 변경으로 처리
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,200 +0,0 @@
|
||||
# Item Master field_key 검증 정책
|
||||
|
||||
## 개요
|
||||
|
||||
field_key 저장 및 검증 정책을 변경하여 시스템 필드(고정 컬럼)와의 충돌을 방지합니다.
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 1. field_key 저장 정책 변경
|
||||
|
||||
**변경 전:**
|
||||
```
|
||||
field_key = {id}_{입력값}
|
||||
예: 98_code, 99_name
|
||||
```
|
||||
|
||||
**변경 후:**
|
||||
```
|
||||
field_key = {입력값}
|
||||
예: code, name (단, 시스템 예약어는 사용 불가)
|
||||
```
|
||||
|
||||
### 2. 시스템 필드 예약어 검증 추가
|
||||
|
||||
#### 검증 흐름
|
||||
```
|
||||
field_key 입력
|
||||
↓
|
||||
source_table 확인 (products / materials)
|
||||
↓
|
||||
해당 테이블 예약어 체크
|
||||
↓
|
||||
기존 필드 중복 체크
|
||||
↓
|
||||
저장
|
||||
```
|
||||
|
||||
#### source_table 기반 예약어 매핑
|
||||
|
||||
| source_table | 대상 테이블 | 예약어 목록 |
|
||||
|--------------|-------------|-------------|
|
||||
| `products` | products | code, name, unit, product_type, ... |
|
||||
| `materials` | materials | name, material_code, material_type, ... |
|
||||
| `null` | 전체 | products + materials 예약어 모두 체크 (안전 모드) |
|
||||
|
||||
## 구현 상세
|
||||
|
||||
### 파일 구조
|
||||
|
||||
```
|
||||
app/
|
||||
├── Constants/
|
||||
│ └── SystemFields.php # 신규: 예약어 상수 클래스
|
||||
└── Services/
|
||||
└── ItemMaster/
|
||||
└── ItemFieldService.php # 수정: 예약어 검증 추가
|
||||
```
|
||||
|
||||
### SystemFields 상수 클래스
|
||||
|
||||
```php
|
||||
// app/Constants/SystemFields.php
|
||||
|
||||
class SystemFields
|
||||
{
|
||||
// 소스 테이블 상수
|
||||
public const SOURCE_TABLE_PRODUCTS = 'products';
|
||||
public const SOURCE_TABLE_MATERIALS = 'materials';
|
||||
|
||||
// 그룹 ID 상수
|
||||
public const GROUP_ITEM_MASTER = 1;
|
||||
|
||||
// products 테이블 고정 컬럼
|
||||
public const PRODUCTS = [
|
||||
'code', 'name', 'unit', 'category_id', 'product_type', 'description',
|
||||
'is_sellable', 'is_purchasable', 'is_producible', 'is_variable_size', 'is_active',
|
||||
'safety_stock', 'lead_time', 'product_category', 'part_type',
|
||||
'bending_diagram', 'bending_details',
|
||||
'specification_file', 'specification_file_name',
|
||||
'certification_file', 'certification_file_name',
|
||||
'certification_number', 'certification_start_date', 'certification_end_date',
|
||||
'attributes', 'attributes_archive',
|
||||
];
|
||||
|
||||
// materials 테이블 고정 컬럼
|
||||
public const MATERIALS = [
|
||||
'name', 'item_name', 'specification', 'material_code', 'material_type',
|
||||
'unit', 'category_id', 'is_inspection', 'is_active',
|
||||
'search_tag', 'remarks', 'attributes', 'options',
|
||||
];
|
||||
|
||||
// 공통 시스템 컬럼
|
||||
public const COMMON = [
|
||||
'id', 'tenant_id', 'created_by', 'updated_by', 'deleted_by',
|
||||
'created_at', 'updated_at', 'deleted_at',
|
||||
];
|
||||
|
||||
// source_table 기반 예약어 조회
|
||||
public static function getReservedKeys(string $sourceTable): array;
|
||||
|
||||
// 예약어 여부 확인
|
||||
public static function isReserved(string $fieldKey, string $sourceTable): bool;
|
||||
|
||||
// 그룹 내 전체 예약어 조회 (안전 모드)
|
||||
public static function getAllReservedKeysForGroup(int $groupId): array;
|
||||
|
||||
// 그룹 내 예약어 여부 확인
|
||||
public static function isReservedInGroup(string $fieldKey, int $groupId): bool;
|
||||
}
|
||||
```
|
||||
|
||||
### ItemFieldService 검증 메서드
|
||||
|
||||
```php
|
||||
private function validateFieldKeyUnique(
|
||||
string $fieldKey,
|
||||
int $tenantId,
|
||||
?string $sourceTable = null,
|
||||
int $groupId = 1,
|
||||
?int $excludeId = null
|
||||
): void {
|
||||
// 1. 시스템 필드(예약어) 체크
|
||||
if ($sourceTable) {
|
||||
if (SystemFields::isReserved($fieldKey, $sourceTable)) {
|
||||
throw ValidationException::withMessages([
|
||||
'field_key' => [__('error.field_key_reserved', ['field_key' => $fieldKey])],
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
// 안전 모드: 그룹 내 모든 테이블 예약어 체크
|
||||
if (SystemFields::isReservedInGroup($fieldKey, $groupId)) {
|
||||
throw ValidationException::withMessages([
|
||||
'field_key' => [__('error.field_key_reserved', ['field_key' => $fieldKey])],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 기존 필드 중복 체크
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 호출 예시
|
||||
|
||||
```php
|
||||
// 독립 필드 생성 시
|
||||
$this->validateFieldKeyUnique(
|
||||
$data['field_key'],
|
||||
$tenantId,
|
||||
$data['source_table'] ?? null, // 'products' 또는 'materials'
|
||||
$data['group_id'] ?? 1
|
||||
);
|
||||
|
||||
// 필드 수정 시
|
||||
$this->validateFieldKeyUnique(
|
||||
$data['field_key'],
|
||||
$tenantId,
|
||||
$data['source_table'] ?? null,
|
||||
$field->group_id ?? 1,
|
||||
$id // excludeId
|
||||
);
|
||||
```
|
||||
|
||||
## 에러 메시지
|
||||
|
||||
| 상황 | 메시지 키 | 메시지 |
|
||||
|------|----------|--------|
|
||||
| 시스템 예약어 충돌 | `error.field_key_reserved` | `"code"은(는) 시스템 예약어로 사용할 수 없습니다.` |
|
||||
| 기존 필드 중복 | `validation.unique` | `field_key은(는) 이미 사용 중입니다.` |
|
||||
|
||||
```php
|
||||
// lang/ko/error.php
|
||||
'field_key_reserved' => '":field_key"은(는) 시스템 예약어로 사용할 수 없습니다.',
|
||||
|
||||
// lang/ko/validation.php (Laravel 기본)
|
||||
'unique' => ':attribute은(는) 이미 사용 중입니다.',
|
||||
```
|
||||
|
||||
## clone 메서드 field_key 복제 정책
|
||||
|
||||
```
|
||||
원본 field_key: custom_field
|
||||
복제본: custom_field_copy
|
||||
|
||||
중복 시: custom_field_copy2, custom_field_copy3, ...
|
||||
```
|
||||
|
||||
## 관련 파일
|
||||
|
||||
| 파일 | 변경 유형 | 설명 |
|
||||
|------|----------|------|
|
||||
| `app/Constants/SystemFields.php` | 신규 | 예약어 상수 클래스 |
|
||||
| `app/Services/ItemMaster/ItemFieldService.php` | 수정 | 검증 로직 추가 |
|
||||
| `lang/ko/error.php` | 수정 | 에러 메시지 추가 |
|
||||
|
||||
## 참고
|
||||
|
||||
- ItemPage 테이블의 `source_table` 컬럼: 실제 저장 테이블명 (products, materials)
|
||||
- ItemPage 테이블의 `item_type` 컬럼: FG, PT, SM, RM, CS (품목 유형 코드)
|
||||
- `group_id`: 카테고리 격리용 (1 = 품목관리)
|
||||
Reference in New Issue
Block a user