docs: items 테이블 통합 계획 업데이트 및 완료 문서 삭제
- mng-item-field-management-plan.md 삭제 (MNG 구현 100% 완료) - items-table-unification-plan.md 업데이트: - Phase 0 데이터 정규화 섹션 추가 - item_type 표준화 체계 정립 (FG, PT, SM, RM, CS) - scopeProducts에서 레거시 코드 제거 - BOM API 요청/응답 변경 상세화 (프론트엔드 전달용) - Phase 5.1에 item_fields 테이블 추가 - 일정 테이블에 Phase 0 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
410
plans/flow-tests/item-delete-force-delete.json
Normal file
410
plans/flow-tests/item-delete-force-delete.json
Normal file
@@ -0,0 +1,410 @@
|
||||
{
|
||||
"name": "품목 삭제 테스트 (Force Delete & 사용중 체크)",
|
||||
"description": "품목 삭제 기능 테스트: 1) 사용되지 않는 품목은 Force Delete 2) 사용 중인 품목은 삭제 불가 에러 반환",
|
||||
"version": "1.0",
|
||||
"config": {
|
||||
"baseUrl": "",
|
||||
"timeout": 30000,
|
||||
"stopOnFailure": true
|
||||
},
|
||||
"variables": {
|
||||
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
|
||||
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}",
|
||||
"ts": "{{$timestamp}}"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"id": "login",
|
||||
"name": "로그인",
|
||||
"method": "POST",
|
||||
"endpoint": "/login",
|
||||
"body": {
|
||||
"user_id": "{{user_id}}",
|
||||
"user_pwd": "{{user_pwd}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.access_token": "@isString"
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"token": "$.access_token"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "create_test_product",
|
||||
"name": "테스트용 제품 생성 (FG)",
|
||||
"method": "POST",
|
||||
"endpoint": "/items",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"body": {
|
||||
"product_type": "FG",
|
||||
"code": "TEST-DEL-FG-{{ts}}",
|
||||
"name": "삭제테스트용 완제품",
|
||||
"unit": "EA",
|
||||
"is_active": true
|
||||
},
|
||||
"expect": {
|
||||
"status": [200, 201],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.id": "@isNumber"
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"product_id": "$.data.id",
|
||||
"product_code": "$.data.code"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "create_test_material",
|
||||
"name": "테스트용 자재 생성 (RM)",
|
||||
"method": "POST",
|
||||
"endpoint": "/items",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"body": {
|
||||
"product_type": "RM",
|
||||
"code": "TEST-DEL-RM-{{ts}}",
|
||||
"name": "삭제테스트용 원자재",
|
||||
"unit": "EA",
|
||||
"is_active": true
|
||||
},
|
||||
"expect": {
|
||||
"status": [200, 201],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.id": "@isNumber"
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"material_id": "$.data.id",
|
||||
"material_code": "$.data.code"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "delete_unused_product",
|
||||
"name": "사용되지 않는 제품 삭제 (Force Delete 성공)",
|
||||
"method": "DELETE",
|
||||
"endpoint": "/items/{{create_test_product.product_id}}?item_type=FG",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.success": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "verify_product_deleted",
|
||||
"name": "제품 영구 삭제 확인 (404 응답)",
|
||||
"method": "GET",
|
||||
"endpoint": "/items/{{create_test_product.product_id}}",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [404],
|
||||
"jsonPath": {
|
||||
"$.success": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "delete_unused_material",
|
||||
"name": "사용되지 않는 자재 삭제 (Force Delete 성공)",
|
||||
"method": "DELETE",
|
||||
"endpoint": "/items/{{create_test_material.material_id}}?item_type=RM",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.success": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "verify_material_deleted",
|
||||
"name": "자재 영구 삭제 확인 (404 응답)",
|
||||
"method": "GET",
|
||||
"endpoint": "/items/{{create_test_material.material_id}}?item_type=RM",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [404],
|
||||
"jsonPath": {
|
||||
"$.success": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "create_parent_product",
|
||||
"name": "BOM 상위 제품 생성",
|
||||
"method": "POST",
|
||||
"endpoint": "/items",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"body": {
|
||||
"product_type": "FG",
|
||||
"code": "TEST-BOM-PARENT-{{ts}}",
|
||||
"name": "BOM 상위 제품",
|
||||
"unit": "EA",
|
||||
"is_active": true
|
||||
},
|
||||
"expect": {
|
||||
"status": [200, 201],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.id": "@isNumber"
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"parent_id": "$.data.id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "create_child_material",
|
||||
"name": "BOM 구성품용 자재 생성",
|
||||
"method": "POST",
|
||||
"endpoint": "/items",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"body": {
|
||||
"product_type": "RM",
|
||||
"code": "TEST-BOM-CHILD-{{ts}}",
|
||||
"name": "BOM 구성품 자재",
|
||||
"unit": "EA",
|
||||
"is_active": true
|
||||
},
|
||||
"expect": {
|
||||
"status": [200, 201],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.id": "@isNumber"
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"child_material_id": "$.data.id",
|
||||
"child_material_code": "$.data.code"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "add_bom_component",
|
||||
"name": "BOM 구성품 추가 (자재를 상위 제품에 연결)",
|
||||
"method": "POST",
|
||||
"endpoint": "/items/{{create_parent_product.parent_id}}/bom",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"body": {
|
||||
"items": [
|
||||
{
|
||||
"ref_type": "MATERIAL",
|
||||
"ref_id": "{{create_child_material.child_material_id}}",
|
||||
"quantity": 2,
|
||||
"unit": "EA"
|
||||
}
|
||||
]
|
||||
},
|
||||
"expect": {
|
||||
"status": [200, 201],
|
||||
"jsonPath": {
|
||||
"$.success": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "delete_used_material",
|
||||
"name": "사용 중인 자재 삭제 시도 (400 에러 - BOM 구성품)",
|
||||
"method": "DELETE",
|
||||
"endpoint": "/items/{{create_child_material.child_material_id}}?item_type=RM",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [400],
|
||||
"jsonPath": {
|
||||
"$.success": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "create_batch_products",
|
||||
"name": "일괄 삭제용 제품 1 생성",
|
||||
"method": "POST",
|
||||
"endpoint": "/items",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"body": {
|
||||
"product_type": "FG",
|
||||
"code": "TEST-BATCH-1-{{ts}}",
|
||||
"name": "일괄삭제 테스트 1",
|
||||
"unit": "EA",
|
||||
"is_active": true
|
||||
},
|
||||
"expect": {
|
||||
"status": [200, 201],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.id": "@isNumber"
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"batch_id_1": "$.data.id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "create_batch_products_2",
|
||||
"name": "일괄 삭제용 제품 2 생성",
|
||||
"method": "POST",
|
||||
"endpoint": "/items",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"body": {
|
||||
"product_type": "FG",
|
||||
"code": "TEST-BATCH-2-{{ts}}",
|
||||
"name": "일괄삭제 테스트 2",
|
||||
"unit": "EA",
|
||||
"is_active": true
|
||||
},
|
||||
"expect": {
|
||||
"status": [200, 201],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.id": "@isNumber"
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"batch_id_2": "$.data.id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "batch_delete_unused",
|
||||
"name": "사용되지 않는 제품들 일괄 삭제 (성공)",
|
||||
"method": "DELETE",
|
||||
"endpoint": "/items/batch",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"body": {
|
||||
"item_type": "FG",
|
||||
"ids": ["{{create_batch_products.batch_id_1}}", "{{create_batch_products_2.batch_id_2}}"]
|
||||
},
|
||||
"expect": {
|
||||
"status": [200],
|
||||
"jsonPath": {
|
||||
"$.success": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "create_batch_for_fail",
|
||||
"name": "일괄 삭제 실패 테스트용 제품 생성",
|
||||
"method": "POST",
|
||||
"endpoint": "/items",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"body": {
|
||||
"product_type": "FG",
|
||||
"code": "TEST-BATCH-FAIL-{{ts}}",
|
||||
"name": "일괄삭제 실패 테스트",
|
||||
"unit": "EA",
|
||||
"is_active": true
|
||||
},
|
||||
"expect": {
|
||||
"status": [200, 201],
|
||||
"jsonPath": {
|
||||
"$.success": true,
|
||||
"$.data.id": "@isNumber"
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"batch_fail_id": "$.data.id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "batch_delete_with_used",
|
||||
"name": "사용 중인 자재 일괄 삭제 시도 (400 에러)",
|
||||
"method": "DELETE",
|
||||
"endpoint": "/items/batch",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"body": {
|
||||
"item_type": "RM",
|
||||
"ids": ["{{create_child_material.child_material_id}}"]
|
||||
},
|
||||
"expect": {
|
||||
"status": [400],
|
||||
"jsonPath": {
|
||||
"$.success": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cleanup_bom",
|
||||
"name": "정리: BOM 구성품 전체 삭제 (빈 배열로 교체)",
|
||||
"method": "POST",
|
||||
"endpoint": "/items/{{create_parent_product.parent_id}}/bom/replace",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"body": {
|
||||
"items": []
|
||||
},
|
||||
"expect": {
|
||||
"status": [200, 201, 404]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cleanup_parent",
|
||||
"name": "정리: 상위 제품 삭제",
|
||||
"method": "DELETE",
|
||||
"endpoint": "/items/{{create_parent_product.parent_id}}?item_type=FG",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200, 404]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cleanup_child",
|
||||
"name": "정리: 구성품 자재 삭제 (BOM 해제 후)",
|
||||
"method": "DELETE",
|
||||
"endpoint": "/items/{{create_child_material.child_material_id}}?item_type=RM",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200, 404]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cleanup_batch_fail",
|
||||
"name": "정리: 일괄삭제 테스트용 제품 삭제",
|
||||
"method": "DELETE",
|
||||
"endpoint": "/items/{{create_batch_for_fail.batch_fail_id}}?item_type=FG",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{login.token}}"
|
||||
},
|
||||
"expect": {
|
||||
"status": [200, 404]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
586
plans/items-table-unification-plan.md
Normal file
586
plans/items-table-unification-plan.md
Normal file
@@ -0,0 +1,586 @@
|
||||
# Items 테이블 통합 마이그레이션 계획
|
||||
|
||||
## 참조 문서
|
||||
|
||||
### 필수 확인
|
||||
|
||||
| 문서 | 경로 | 내용 |
|
||||
|------|------|------|
|
||||
| **ItemMaster 연동 설계서** | [specs/item-master-integration.md](../specs/item-master-integration.md) | source_table, EntityRelationship 구조 |
|
||||
| **DB 스키마** | [specs/database-schema.md](../specs/database-schema.md) | 테이블 구조, Multi-tenant 아키텍처 |
|
||||
|
||||
### 참고 문서
|
||||
|
||||
| 문서 | 경로 | 내용 |
|
||||
|------|------|------|
|
||||
| **품목관리 마이그레이션 가이드** | [projects/mes/ITEM_MANAGEMENT_MIGRATION_GUIDE.md](../projects/mes/ITEM_MANAGEMENT_MIGRATION_GUIDE.md) | 프론트엔드 마이그레이션 |
|
||||
| **API 품목 분석 요약** | [projects/mes/00_baseline/docs_breakdown/api_item_analysis_summary.md](../projects/mes/00_baseline/docs_breakdown/api_item_analysis_summary.md) | 기존 API 분석, price_histories |
|
||||
| **Swagger 가이드** | [guides/swagger-guide.md](../guides/swagger-guide.md) | API 문서화 규칙 |
|
||||
|
||||
### 관련 코드
|
||||
|
||||
| 파일 | 경로 | 역할 |
|
||||
|------|------|------|
|
||||
| ItemPage 모델 | `api/app/Models/ItemMaster/ItemPage.php` | source_table 매핑 |
|
||||
| EntityRelationship 모델 | `api/app/Models/ItemMaster/EntityRelationship.php` | 엔티티 관계 관리 |
|
||||
| ItemMasterService | `api/app/Services/ItemMaster/ItemMasterService.php` | init API, 메타데이터 조회 |
|
||||
| ProductService | `api/app/Services/ProductService.php` | 기존 Products API (제거 예정) |
|
||||
| MaterialService | `api/app/Services/MaterialService.php` | 기존 Materials API (제거 예정) |
|
||||
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
### 목적
|
||||
`products`/`materials` 테이블을 `items` 테이블로 통합하여:
|
||||
- BOM 관리 시 `child_item_type` 불필요 (ID만으로 유일 식별)
|
||||
- 단일 쿼리로 모든 품목 조회 가능
|
||||
- Item-Master 시스템과 일관된 구조
|
||||
|
||||
### 현재 상황
|
||||
- **개발 단계**: 미오픈 (레거시 호환 불필요)
|
||||
- **Item-Master**: 메타데이터 시스템 운영 중 (pages, sections, fields)
|
||||
- **이전 시도**: 12/11 items 생성 → 12/12 롤백 (정책 정리 필요)
|
||||
|
||||
### 현재 시스템 구조
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Item-Master (메타데이터) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ item_pages (source_table: 'products'|'materials') │
|
||||
│ ↓ EntityRelationship │
|
||||
│ item_sections → item_fields, item_bom_items │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓ 참조
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 실제 데이터 테이블 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ products (808건) ← ProductController, ProductService │
|
||||
│ materials (417건) ← MaterialController, MaterialService │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 목표 구조
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Item-Master (메타데이터) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ item_pages (source_table: 'items') │
|
||||
│ ↓ EntityRelationship │
|
||||
│ item_sections → item_fields, item_bom_items │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓ 참조
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 통합 데이터 테이블 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ items ← ItemController, ItemService │
|
||||
│ item_type: FG, PT, SM, RM, CS │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: 데이터 정규화
|
||||
|
||||
### 0.1 item_type 표준화
|
||||
|
||||
개발 중이므로 비표준 데이터는 삭제 처리. 품목관리 완료 후 경동기업 데이터 전체 재세팅 예정.
|
||||
|
||||
**표준 item_type 체계**:
|
||||
|
||||
| 코드 | 설명 | 출처 |
|
||||
|------|------|------|
|
||||
| FG | 완제품 (Finished Goods) | products |
|
||||
| PT | 부품 (Parts) | products |
|
||||
| SM | 부자재 (Sub-materials) | materials |
|
||||
| RM | 원자재 (Raw Materials) | materials |
|
||||
| CS | 소모품 (Consumables) | materials만 |
|
||||
|
||||
**비표준 데이터 삭제**:
|
||||
```sql
|
||||
-- products에서 비표준 타입 삭제 (PRODUCT, SUBASSEMBLY, PART, CS)
|
||||
DELETE FROM products WHERE product_type NOT IN ('FG', 'PT');
|
||||
|
||||
-- materials는 이미 표준 타입만 사용 (SM, RM, CS)
|
||||
```
|
||||
|
||||
### 0.2 BOM 데이터 정리
|
||||
|
||||
통합 시 문제되는 BOM 데이터 삭제:
|
||||
```sql
|
||||
-- 삭제될 products/materials를 참조하는 BOM 항목 제거
|
||||
-- (Phase 1 이관 전에 실행)
|
||||
```
|
||||
|
||||
### 0.3 체크리스트
|
||||
|
||||
- [ ] products 비표준 타입 삭제
|
||||
- [ ] 관련 BOM 데이터 정리
|
||||
- [ ] 삭제 건수 확인
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: items 테이블 생성 + 데이터 이관
|
||||
|
||||
### 1.1 items 테이블
|
||||
|
||||
```sql
|
||||
CREATE TABLE items (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
tenant_id BIGINT UNSIGNED NOT NULL,
|
||||
|
||||
-- 기본 정보
|
||||
item_type VARCHAR(15) NOT NULL COMMENT 'FG, PT, SM, RM, CS',
|
||||
code VARCHAR(100) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
unit VARCHAR(20) NULL,
|
||||
category_id BIGINT UNSIGNED NULL,
|
||||
|
||||
-- BOM (JSON)
|
||||
bom JSON NULL COMMENT '[{child_item_id, quantity}, ...]',
|
||||
|
||||
-- 상태
|
||||
is_active TINYINT(1) DEFAULT 1,
|
||||
|
||||
-- 감사 필드
|
||||
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_items_tenant_type (tenant_id, item_type),
|
||||
INDEX idx_items_tenant_code (tenant_id, code),
|
||||
INDEX idx_items_tenant_category (tenant_id, category_id),
|
||||
UNIQUE KEY uq_items_tenant_code (tenant_id, code, deleted_at),
|
||||
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
|
||||
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
### 1.2 item_details 테이블 (확장 필드)
|
||||
|
||||
```sql
|
||||
CREATE TABLE item_details (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
item_id BIGINT UNSIGNED NOT NULL,
|
||||
|
||||
-- Products 전용 필드
|
||||
is_sellable TINYINT(1) DEFAULT 1,
|
||||
is_purchasable TINYINT(1) DEFAULT 0,
|
||||
is_producible TINYINT(1) DEFAULT 0,
|
||||
safety_stock INT NULL,
|
||||
lead_time INT NULL,
|
||||
is_variable_size TINYINT(1) DEFAULT 0,
|
||||
product_category VARCHAR(50) NULL,
|
||||
part_type VARCHAR(50) NULL,
|
||||
|
||||
-- Materials 전용 필드
|
||||
is_inspection VARCHAR(1) DEFAULT 'N',
|
||||
|
||||
created_at TIMESTAMP NULL,
|
||||
updated_at TIMESTAMP NULL,
|
||||
|
||||
UNIQUE KEY uq_item_details_item_id (item_id),
|
||||
FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
### 1.3 item_attributes 테이블 (동적 속성)
|
||||
|
||||
```sql
|
||||
CREATE TABLE item_attributes (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
item_id BIGINT UNSIGNED NOT NULL,
|
||||
|
||||
attributes JSON NULL,
|
||||
options JSON NULL,
|
||||
|
||||
created_at TIMESTAMP NULL,
|
||||
updated_at TIMESTAMP NULL,
|
||||
|
||||
UNIQUE KEY uq_item_attributes_item_id (item_id),
|
||||
FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
### 1.4 데이터 이관 스크립트
|
||||
|
||||
```php
|
||||
// Products → Items
|
||||
DB::statement("
|
||||
INSERT INTO items (tenant_id, item_type, code, name, unit, category_id, bom,
|
||||
is_active, created_by, updated_by, deleted_by, created_at, updated_at, deleted_at)
|
||||
SELECT tenant_id, product_type, code, name, unit, category_id, bom,
|
||||
is_active, created_by, updated_by, deleted_by, created_at, updated_at, deleted_at
|
||||
FROM products
|
||||
");
|
||||
|
||||
// Materials → Items
|
||||
DB::statement("
|
||||
INSERT INTO items (tenant_id, item_type, code, name, unit, category_id,
|
||||
is_active, created_by, updated_by, deleted_by, created_at, updated_at, deleted_at)
|
||||
SELECT tenant_id, material_type, material_code, name, unit, category_id,
|
||||
is_active, created_by, updated_by, deleted_by, created_at, updated_at, deleted_at
|
||||
FROM materials
|
||||
");
|
||||
```
|
||||
|
||||
### 1.5 체크리스트
|
||||
|
||||
- [ ] items 마이그레이션 생성
|
||||
- [ ] item_details 마이그레이션 생성
|
||||
- [ ] item_attributes 마이그레이션 생성
|
||||
- [ ] 데이터 이관 스크립트 실행
|
||||
- [ ] 건수 검증 (1,225건)
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Item 모델 + Service 생성
|
||||
|
||||
### 2.1 Item 모델
|
||||
|
||||
```php
|
||||
// app/Models/Item.php
|
||||
class Item extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id', 'item_type', 'code', 'name', 'unit',
|
||||
'category_id', 'bom', 'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'bom' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
// 1:1 관계
|
||||
public function details() { return $this->hasOne(ItemDetail::class); }
|
||||
public function attributes() { return $this->hasOne(ItemAttribute::class); }
|
||||
|
||||
// 타입별 스코프
|
||||
public function scopeProducts($q) {
|
||||
return $q->whereIn('item_type', ['FG', 'PT']);
|
||||
}
|
||||
public function scopeMaterials($q) {
|
||||
return $q->whereIn('item_type', ['SM', 'RM', 'CS']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 ItemService
|
||||
|
||||
```php
|
||||
// app/Services/ItemService.php
|
||||
class ItemService extends Service
|
||||
{
|
||||
public function index(array $params): LengthAwarePaginator
|
||||
{
|
||||
$query = Item::where('tenant_id', $this->tenantId());
|
||||
|
||||
// item_type 필터
|
||||
if ($itemType = $params['item_type'] ?? null) {
|
||||
$query->where('item_type', strtoupper($itemType));
|
||||
}
|
||||
|
||||
// 검색
|
||||
if ($search = $params['search'] ?? null) {
|
||||
$query->where(fn($q) => $q
|
||||
->where('code', 'like', "%{$search}%")
|
||||
->orWhere('name', 'like', "%{$search}%")
|
||||
);
|
||||
}
|
||||
|
||||
return $query->with(['details', 'attributes'])->paginate($params['per_page'] ?? 15);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 체크리스트
|
||||
|
||||
- [ ] Item 모델 생성
|
||||
- [ ] ItemDetail 모델 생성
|
||||
- [ ] ItemAttribute 모델 생성
|
||||
- [ ] ItemService 생성
|
||||
- [ ] ItemRequest 생성
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Item-Master 연동 수정
|
||||
|
||||
### 3.1 ItemPage.source_table 변경
|
||||
|
||||
```php
|
||||
// app/Models/ItemMaster/ItemPage.php
|
||||
|
||||
// 기존
|
||||
$mapping = [
|
||||
'products' => \App\Models\Product::class,
|
||||
'materials' => \App\Models\Material::class,
|
||||
];
|
||||
|
||||
// 변경
|
||||
$mapping = [
|
||||
'items' => \App\Models\Item::class,
|
||||
];
|
||||
```
|
||||
|
||||
### 3.2 item_pages 데이터 업데이트
|
||||
|
||||
```sql
|
||||
-- source_table 통합
|
||||
UPDATE item_pages SET source_table = 'items' WHERE source_table IN ('products', 'materials');
|
||||
```
|
||||
|
||||
### 3.3 체크리스트
|
||||
|
||||
- [ ] ItemPage 모델 수정 (getTargetModelClass)
|
||||
- [ ] item_pages.source_table 마이그레이션
|
||||
- [ ] ItemMasterService 연동 테스트
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: API 통합
|
||||
|
||||
### 4.1 API 구조 변경
|
||||
|
||||
```
|
||||
기존 (분리):
|
||||
/api/v1/products → ProductController
|
||||
/api/v1/products/materials → MaterialController
|
||||
|
||||
통합 후:
|
||||
/api/v1/items → ItemController
|
||||
/api/v1/items?item_type=FG → Products 조회
|
||||
/api/v1/items?item_type=SM → Materials 조회
|
||||
```
|
||||
|
||||
### 4.2 ItemController
|
||||
|
||||
```php
|
||||
// app/Http/Controllers/Api/V1/ItemController.php
|
||||
class ItemController extends Controller
|
||||
{
|
||||
public function __construct(private ItemService $service) {}
|
||||
|
||||
public function index(ItemIndexRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(fn() => [
|
||||
'data' => $this->service->index($request->validated()),
|
||||
], __('message.fetched'));
|
||||
}
|
||||
|
||||
public function store(ItemStoreRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(fn() => [
|
||||
'data' => $this->service->store($request->validated()),
|
||||
], __('message.created'));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 라우트
|
||||
|
||||
```php
|
||||
// routes/api_v1.php
|
||||
Route::prefix('items')->group(function () {
|
||||
Route::get('/', [ItemController::class, 'index']);
|
||||
Route::post('/', [ItemController::class, 'store']);
|
||||
Route::get('/{id}', [ItemController::class, 'show']);
|
||||
Route::patch('/{id}', [ItemController::class, 'update']);
|
||||
Route::delete('/{id}', [ItemController::class, 'destroy']);
|
||||
});
|
||||
```
|
||||
|
||||
### 4.4 체크리스트
|
||||
|
||||
- [ ] ItemController 생성
|
||||
- [ ] ItemIndexRequest, ItemStoreRequest 등 생성
|
||||
- [ ] 라우트 등록
|
||||
- [ ] Swagger 문서 작성
|
||||
- [ ] 기존 ProductController, MaterialController 제거
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: 참조 테이블 마이그레이션
|
||||
|
||||
### 5.1 변경 대상
|
||||
|
||||
| 테이블 | 기존 | 변경 |
|
||||
|--------|------|------|
|
||||
| product_components | ref_type + ref_id | child_item_id |
|
||||
| bom_template_items | ref_type + ref_id | item_id |
|
||||
| orders | product_id | item_id |
|
||||
| order_items | product_id | item_id |
|
||||
| material_receipts | material_id | item_id |
|
||||
| lots | material_id | item_id |
|
||||
| price_histories | item_type + item_id | item_id |
|
||||
| item_fields | source_table 'products'\|'materials' | source_table 'items' |
|
||||
|
||||
### 5.2 체크리스트
|
||||
|
||||
- [ ] 각 참조 테이블 마이그레이션 작성
|
||||
- [ ] 관련 모델 관계 업데이트
|
||||
- [ ] 데이터 검증
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: 정리
|
||||
|
||||
### 6.1 체크리스트
|
||||
|
||||
- [ ] CRUD 테스트 (전체 item_type)
|
||||
- [ ] BOM 계산 테스트
|
||||
- [ ] Item-Master 연동 테스트
|
||||
- [ ] 참조 무결성 테스트
|
||||
- [ ] products 테이블 삭제
|
||||
- [ ] materials 테이블 삭제
|
||||
- [ ] 기존 Product, Material 모델 삭제
|
||||
- [ ] 기존 ProductService, MaterialService 삭제
|
||||
|
||||
---
|
||||
|
||||
## 테이블 구조 요약
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ items (핵심) │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ id, tenant_id, item_type, code, name, unit │
|
||||
│ category_id, bom (JSON), is_active │
|
||||
│ timestamps + soft deletes │
|
||||
└─────────────────────┬───────────────────────────────┘
|
||||
│ 1:1
|
||||
┌───────────────┴───────────────┐
|
||||
▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│item_details │ │item_attrs │
|
||||
├─────────────┤ ├─────────────┤
|
||||
│ is_sellable │ │ attributes │
|
||||
│ is_purch... │ │ options │
|
||||
│ safety_stk │ └─────────────┘
|
||||
│ lead_time │
|
||||
│ is_inspect │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## BOM 계산 로직
|
||||
|
||||
### 통합 전
|
||||
```php
|
||||
foreach ($bom as $item) {
|
||||
if ($item['child_item_type'] === 'product') {
|
||||
$child = Product::find($item['child_item_id']);
|
||||
} else {
|
||||
$child = Material::find($item['child_item_id']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 통합 후
|
||||
```php
|
||||
$childIds = collect($bom)->pluck('child_item_id');
|
||||
$children = Item::whereIn('id', $childIds)->get()->keyBy('id');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 프론트엔드 전달 사항
|
||||
|
||||
### API 엔드포인트 변경
|
||||
|
||||
| 기존 | 통합 |
|
||||
|------|------|
|
||||
| `GET /api/v1/products` | `GET /api/v1/items?item_type=FG` |
|
||||
| `GET /api/v1/products?product_type=PART` | `GET /api/v1/items?item_type=PART` |
|
||||
| `GET /api/v1/products/materials` | `GET /api/v1/items?item_type=SM` |
|
||||
|
||||
### 응답 필드 변경
|
||||
|
||||
| 기존 | 통합 |
|
||||
|------|------|
|
||||
| `product_type` | `item_type` |
|
||||
| `material_type` | `item_type` |
|
||||
| `material_code` | `code` |
|
||||
|
||||
### BOM 요청/응답 변경
|
||||
|
||||
**요청 (Request)**:
|
||||
```json
|
||||
// 기존: BOM 저장 시 ref_type 지정 필요
|
||||
{
|
||||
"bom": [
|
||||
{ "ref_type": "PRODUCT", "ref_id": 5, "quantity": 2 },
|
||||
{ "ref_type": "MATERIAL", "ref_id": 10, "quantity": 1 }
|
||||
]
|
||||
}
|
||||
|
||||
// 통합: item_id만 사용
|
||||
{
|
||||
"bom": [
|
||||
{ "child_item_id": 5, "quantity": 2 },
|
||||
{ "child_item_id": 10, "quantity": 1 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**응답 (Response)**:
|
||||
```json
|
||||
// 기존
|
||||
{ "child_item_type": "product", "child_item_id": 5, "quantity": 2 }
|
||||
|
||||
// 통합
|
||||
{ "child_item_id": 5, "quantity": 2 }
|
||||
```
|
||||
|
||||
**프론트엔드 수정 포인트**:
|
||||
- BOM 구성품 추가 시 `ref_type` 선택 UI 제거
|
||||
- 품목 검색 시 `/api/v1/items` 단일 엔드포인트 사용
|
||||
- BOM 저장 payload에서 `ref_type`, `ref_id` → `child_item_id`로 변경
|
||||
|
||||
---
|
||||
|
||||
## 일정
|
||||
|
||||
| Phase | 작업 | 상태 |
|
||||
|-------|------|------|
|
||||
| 0 | 데이터 정규화 (비표준 item_type/BOM 삭제) | ⬜ |
|
||||
| 1 | items 테이블 생성 + 데이터 이관 | ⬜ |
|
||||
| 2 | Item 모델 + Service 생성 | ⬜ |
|
||||
| 3 | Item-Master 연동 수정 | ⬜ |
|
||||
| 4 | API 통합 | ⬜ |
|
||||
| 5 | 참조 테이블 마이그레이션 | ⬜ |
|
||||
| 6 | 정리 | ⬜ |
|
||||
|
||||
---
|
||||
|
||||
## 리스크
|
||||
|
||||
| 리스크 | 대응 |
|
||||
|--------|------|
|
||||
| 데이터 이관 누락 | 이관 전후 건수 검증 |
|
||||
| Item-Master 연동 오류 | source_table 변경 전 테스트 |
|
||||
| BOM 순환 참조 | 저장 시 검증 로직 추가 |
|
||||
| Code 중복 (products↔materials) | 개발 중이므로 품목관리 완료 후 경동기업 데이터 전체 삭제 후 재세팅 예정. 중복 데이터는 삭제 처리 |
|
||||
|
||||
---
|
||||
|
||||
## 롤백 계획
|
||||
|
||||
각 Phase는 독립적 마이그레이션으로 구성:
|
||||
```bash
|
||||
# Phase 1 롤백
|
||||
php artisan migrate:rollback --step=3
|
||||
|
||||
# 데이터 복구 (products/materials 테이블 유지 상태에서)
|
||||
# 신규 테이블만 삭제하면 됨
|
||||
```
|
||||
@@ -1,531 +0,0 @@
|
||||
# MNG 품목기준 필드 관리 개발 계획
|
||||
|
||||
> 테넌트별 품목기준관리(ItemMaster) 시스템 필드 시딩 및 커스텀 필드 관리 기능
|
||||
|
||||
**작성일**: 2025-12-09
|
||||
**상태**: 계획 중
|
||||
**관련 문서**:
|
||||
- `docs/specs/item-master-field-integration.md`
|
||||
- `docs/specs/item-master-field-key-validation.md`
|
||||
- `docs/specs/ITEM-MASTER-INDEX.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요
|
||||
|
||||
### 1.1 목적
|
||||
- 테넌트별 품목기준 **시스템 필드(고정 컬럼)** 일괄 등록/삭제
|
||||
- 신규 테넌트 생성 시 초기 필드 데이터 자동 시딩
|
||||
- 기존 테넌트에 필드 데이터 수동 시딩
|
||||
- **커스텀 필드** 추가/삭제 관리
|
||||
- 향후 회계, 생산 등 다양한 도메인 확장 지원
|
||||
|
||||
### 1.2 핵심 개념
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 품목기준 필드 구분 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ 시스템 필드 (System Fields) │ │
|
||||
│ │ ───────────────────────────────── │ │
|
||||
│ │ - products 테이블 고정 컬럼 │ │
|
||||
│ │ (code, name, unit, is_active...) │ │
|
||||
│ │ - materials 테이블 고정 컬럼 │ │
|
||||
│ │ (material_code, name, spec...) │ │
|
||||
│ │ - storage_type = 'column' │ │
|
||||
│ │ - 시딩으로 일괄 등록 │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ 커스텀 필드 (Custom Fields) │ │
|
||||
│ │ ───────────────────────────────── │ │
|
||||
│ │ - 테넌트별 추가 필드 │ │
|
||||
│ │ - attributes JSON에 저장 │ │
|
||||
│ │ - storage_type = 'json' │ │
|
||||
│ │ - MNG에서 수동 추가/삭제 │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.3 대상 테이블 (source_table)
|
||||
|
||||
| source_table | 설명 | item_type |
|
||||
|--------------|------|-----------|
|
||||
| `products` | 제품 테이블 | FG (완제품), PT (부품) |
|
||||
| `materials` | 자재 테이블 | SM (부자재), RM (원자재), CS (소모품) |
|
||||
| `product_components` | BOM 테이블 | - |
|
||||
| `material_inspections` | 자재 검수 | - |
|
||||
| `material_receipts` | 자재 입고 | - |
|
||||
|
||||
---
|
||||
|
||||
## 2. 기능 설계
|
||||
|
||||
### 2.1 메인 화면: 품목기준 필드 관리
|
||||
|
||||
**URL**: `GET /item-master/fields`
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 품목기준 필드 관리 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 테넌트: [현재 테넌트명] │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ 시스템 필드 시딩 │ │
|
||||
│ │ ───────────────────────────────────────────────────────│ │
|
||||
│ │ │ │
|
||||
│ │ 소스 테이블별 상태: │ │
|
||||
│ │ ┌────────────────┬──────────┬─────────┬────────────┐ │ │
|
||||
│ │ │ 테이블 │ 필드 수 │ 상태 │ 액션 │ │ │
|
||||
│ │ ├────────────────┼──────────┼─────────┼────────────┤ │ │
|
||||
│ │ │ products │ 12/12 │ ●완료 │ [초기화] │ │ │
|
||||
│ │ │ materials │ 0/8 │ ○미등록 │ [시딩] │ │ │
|
||||
│ │ │ product_comp.. │ 5/5 │ ●완료 │ [초기화] │ │ │
|
||||
│ │ │ material_ins.. │ 0/8 │ ○미등록 │ [시딩] │ │ │
|
||||
│ │ │ material_rec.. │ 0/10 │ ○미등록 │ [시딩] │ │ │
|
||||
│ │ └────────────────┴──────────┴─────────┴────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ [전체 시딩] [전체 초기화] │ │
|
||||
│ │ │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ 커스텀 필드 관리 [+ 추가] │ │
|
||||
│ │ ───────────────────────────────────────────────────────│ │
|
||||
│ │ │ │
|
||||
│ │ 필터: [소스 테이블 ▼] [필드 타입 ▼] │ │
|
||||
│ │ │ │
|
||||
│ │ ┌───┬─────────┬───────────┬────────┬────────┬───────┐│ │
|
||||
│ │ │ □ │ 필드키 │ 필드명 │ 타입 │ 소스 │ 액션 ││ │
|
||||
│ │ ├───┼─────────┼───────────┼────────┼────────┼───────┤│ │
|
||||
│ │ │ □ │ weight │ 무게 │ number │products│ [삭제]││ │
|
||||
│ │ │ □ │ grade │ 등급 │dropdown│materials│[삭제]││ │
|
||||
│ │ │ □ │ color │ 색상 │ textbox│products│ [삭제]││ │
|
||||
│ │ └───┴─────────┴───────────┴────────┴────────┴───────┘│ │
|
||||
│ │ │ │
|
||||
│ │ [선택 삭제] │ │
|
||||
│ │ │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 시스템 필드 시딩
|
||||
|
||||
**기능**: 특정 source_table의 시스템 필드를 item_fields에 일괄 등록
|
||||
|
||||
**시딩 데이터 예시** (products):
|
||||
```php
|
||||
[
|
||||
['field_key' => 'code', 'field_name' => '품목코드', 'field_type' => 'textbox', 'is_required' => true],
|
||||
['field_key' => 'name', 'field_name' => '품목명', 'field_type' => 'textbox', 'is_required' => true],
|
||||
['field_key' => 'unit', 'field_name' => '단위', 'field_type' => 'dropdown', 'is_required' => true],
|
||||
['field_key' => 'product_type', 'field_name' => '제품유형', 'field_type' => 'dropdown'],
|
||||
['field_key' => 'is_sellable', 'field_name' => '판매가능', 'field_type' => 'checkbox'],
|
||||
['field_key' => 'is_purchasable', 'field_name' => '구매가능', 'field_type' => 'checkbox'],
|
||||
['field_key' => 'is_producible', 'field_name' => '생산가능', 'field_type' => 'checkbox'],
|
||||
['field_key' => 'is_active', 'field_name' => '활성화', 'field_type' => 'checkbox'],
|
||||
// ...
|
||||
]
|
||||
```
|
||||
|
||||
**저장 시 설정**:
|
||||
```php
|
||||
[
|
||||
'tenant_id' => $currentTenantId,
|
||||
'source_table' => 'products',
|
||||
'source_column' => 'code', // field_key와 동일
|
||||
'storage_type' => 'column', // DB 컬럼 직접 저장
|
||||
'is_system' => true, // 시스템 필드 표시 (선택적)
|
||||
]
|
||||
```
|
||||
|
||||
### 2.3 시스템 필드 초기화
|
||||
|
||||
**기능**: 특정 source_table의 시스템 필드를 삭제하고 다시 시딩
|
||||
|
||||
**주의사항**:
|
||||
- 커스텀 필드는 유지 (storage_type = 'json'인 필드)
|
||||
- 확인 다이얼로그 필수
|
||||
- 삭제 전 관련 entity_relationships도 정리 필요
|
||||
|
||||
### 2.4 커스텀 필드 추가
|
||||
|
||||
**URL**: `POST /item-master/fields/custom`
|
||||
|
||||
**추가 모달**:
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 커스텀 필드 추가 [닫기] │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 소스 테이블: [products ▼] │
|
||||
│ │
|
||||
│ 필드 키: [_______________] │
|
||||
│ * 영문, 숫자, 언더스코어만 허용 │
|
||||
│ * 시스템 예약어 사용 불가 │
|
||||
│ │
|
||||
│ 필드명: [_______________] │
|
||||
│ │
|
||||
│ 필드 타입: [textbox ▼] │
|
||||
│ - textbox, number, dropdown, │
|
||||
│ checkbox, date, textarea │
|
||||
│ │
|
||||
│ 필수 여부: [ ] 필수 │
|
||||
│ │
|
||||
│ 기본값: [_______________] (선택) │
|
||||
│ │
|
||||
│ 옵션 (dropdown 선택 시): │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ 라벨 │ 값 │ │ │
|
||||
│ │ [옵션1 ] │ [val1 ] │ [+ 추가] │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [취소] [저장] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**저장 시 설정**:
|
||||
```php
|
||||
[
|
||||
'tenant_id' => $currentTenantId,
|
||||
'source_table' => 'products',
|
||||
'source_column' => null, // 커스텀 필드는 null
|
||||
'storage_type' => 'json', // JSON 저장
|
||||
'json_path' => 'attributes.custom_weight', // 저장 경로
|
||||
'is_system' => false,
|
||||
]
|
||||
```
|
||||
|
||||
### 2.5 커스텀 필드 삭제
|
||||
|
||||
**기능**: 선택한 커스텀 필드 삭제
|
||||
|
||||
**주의사항**:
|
||||
- 시스템 필드(storage_type = 'column')는 삭제 불가
|
||||
- 이미 데이터가 있는 경우 경고
|
||||
- entity_relationships 정리 필요
|
||||
|
||||
---
|
||||
|
||||
## 3. 데이터 구조
|
||||
|
||||
### 3.1 item_fields 테이블 (기존 + 확장)
|
||||
|
||||
```sql
|
||||
item_fields
|
||||
├── id
|
||||
├── tenant_id
|
||||
├── field_key -- 고유 키 (code, name, custom_weight)
|
||||
├── field_name -- 표시명 (품목코드, 품목명, 커스텀 무게)
|
||||
├── field_type -- 필드 타입 (textbox, dropdown...)
|
||||
├── is_required -- 필수 여부
|
||||
├── order_no -- 정렬 순서
|
||||
├── default_value -- 기본값
|
||||
├── options -- 드롭다운 옵션 JSON
|
||||
├── properties -- 속성 JSON
|
||||
├── validation_rules -- 검증 규칙 JSON
|
||||
├── source_table -- 소스 테이블 (products, materials)
|
||||
├── source_column -- 소스 컬럼 (시스템 필드만)
|
||||
├── storage_type -- 저장 방식 (column, json)
|
||||
├── json_path -- JSON 저장 경로 (커스텀 필드만)
|
||||
├── is_active
|
||||
├── created_at / updated_at / deleted_at
|
||||
```
|
||||
|
||||
### 3.2 시스템 필드 정의 (SystemFields 상수)
|
||||
|
||||
```php
|
||||
// app/Constants/SystemFields.php 활용
|
||||
// 이미 구현된 상수 클래스에서 시딩 데이터 추출
|
||||
|
||||
class SystemFields
|
||||
{
|
||||
public const PRODUCTS = [
|
||||
'code', 'name', 'unit', 'product_type', 'category_id',
|
||||
'is_sellable', 'is_purchasable', 'is_producible', 'is_active',
|
||||
'certification_number', 'certification_date', 'certification_expiry',
|
||||
// ...
|
||||
];
|
||||
|
||||
public const MATERIALS = [
|
||||
'material_code', 'name', 'item_name', 'specification',
|
||||
'unit', 'category_id', 'is_inspection', 'search_tag',
|
||||
// ...
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 파일 구조
|
||||
|
||||
```
|
||||
mng/
|
||||
├── app/
|
||||
│ ├── Http/
|
||||
│ │ └── Controllers/
|
||||
│ │ └── ItemFieldController.php # 신규
|
||||
│ ├── Models/
|
||||
│ │ └── ItemField.php # 신규 (MNG 전용)
|
||||
│ ├── Services/
|
||||
│ │ └── ItemFieldSeedingService.php # 신규: 시딩 로직
|
||||
│ └── Constants/
|
||||
│ └── SystemFieldDefinitions.php # 신규: 시딩 데이터 정의
|
||||
├── resources/
|
||||
│ └── views/
|
||||
│ └── item-fields/
|
||||
│ ├── index.blade.php # 메인 화면
|
||||
│ ├── _seeding-section.blade.php # 시딩 섹션 (partial)
|
||||
│ ├── _custom-fields-section.blade.php # 커스텀 필드 섹션
|
||||
│ └── _create-modal.blade.php # 추가 모달
|
||||
└── routes/
|
||||
└── web.php # 라우트 추가
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 구현 단계
|
||||
|
||||
### Phase 1: 기반 구조 (0.5일)
|
||||
|
||||
| 작업 | 파일 | 설명 |
|
||||
|------|------|------|
|
||||
| ItemField 모델 | `app/Models/ItemField.php` | API DB 참조 모델 |
|
||||
| 상수 클래스 | `app/Constants/SystemFieldDefinitions.php` | 테이블별 시딩 데이터 |
|
||||
| 라우트 등록 | `routes/web.php` | CRUD 라우트 |
|
||||
| 메뉴 추가 | 사이드바 | 품목기준 > 필드 관리 |
|
||||
|
||||
### Phase 2: 시딩 기능 (1일)
|
||||
|
||||
| 작업 | 파일 | 설명 |
|
||||
|------|------|------|
|
||||
| 시딩 서비스 | `ItemFieldSeedingService.php` | 시딩/초기화 로직 |
|
||||
| 메인 화면 | `index.blade.php` | 시딩 상태 표시 |
|
||||
| 시딩 API | `ItemFieldController.php` | 시딩/초기화 엔드포인트 |
|
||||
| 확인 다이얼로그 | JS | 초기화 확인 |
|
||||
|
||||
### Phase 3: 커스텀 필드 관리 (1일)
|
||||
|
||||
| 작업 | 파일 | 설명 |
|
||||
|------|------|------|
|
||||
| 커스텀 필드 목록 | `index.blade.php` | 목록 표시 |
|
||||
| 추가 모달 | `_create-modal.blade.php` | 필드 추가 UI |
|
||||
| 필드 키 검증 | `ItemFieldController.php` | 예약어/중복 체크 |
|
||||
| 삭제 기능 | `ItemFieldController.php` | 삭제 처리 |
|
||||
|
||||
### Phase 4: 테스트/마무리 (0.5일)
|
||||
|
||||
| 작업 | 설명 |
|
||||
|------|------|
|
||||
| 시딩 테스트 | 전체/개별 시딩 |
|
||||
| 초기화 테스트 | 초기화 후 재시딩 |
|
||||
| 커스텀 필드 테스트 | 추가/삭제 |
|
||||
| 예약어 검증 테스트 | 시스템 필드명 입력 시도 |
|
||||
|
||||
---
|
||||
|
||||
## 6. API 엔드포인트
|
||||
|
||||
| 메서드 | URL | 설명 |
|
||||
|--------|-----|------|
|
||||
| GET | `/item-master/fields` | 필드 관리 메인 화면 |
|
||||
| GET | `/item-master/fields/status` | 테이블별 시딩 상태 조회 (AJAX) |
|
||||
| POST | `/item-master/fields/seed` | 시스템 필드 시딩 |
|
||||
| POST | `/item-master/fields/reset` | 시스템 필드 초기화 |
|
||||
| GET | `/item-master/fields/custom` | 커스텀 필드 목록 (AJAX) |
|
||||
| POST | `/item-master/fields/custom` | 커스텀 필드 추가 |
|
||||
| DELETE | `/item-master/fields/custom/{id}` | 커스텀 필드 삭제 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 체크리스트
|
||||
|
||||
### Phase 1: 기반 구조
|
||||
- [ ] ItemField 모델 생성 (API DB 참조)
|
||||
- [ ] SystemFieldDefinitions 상수 클래스 생성
|
||||
- [ ] 라우트 등록
|
||||
- [ ] 사이드바 메뉴 추가
|
||||
|
||||
### Phase 2: 시딩 기능
|
||||
- [ ] ItemFieldSeedingService 생성
|
||||
- [ ] 테이블별 시딩 상태 조회
|
||||
- [ ] 단일 테이블 시딩
|
||||
- [ ] 전체 테이블 시딩
|
||||
- [ ] 시스템 필드 초기화
|
||||
- [ ] 확인 다이얼로그
|
||||
|
||||
### Phase 3: 커스텀 필드
|
||||
- [ ] 커스텀 필드 목록 조회
|
||||
- [ ] 커스텀 필드 추가 모달
|
||||
- [ ] field_key 예약어 검증 연동
|
||||
- [ ] 커스텀 필드 삭제
|
||||
- [ ] 일괄 삭제
|
||||
|
||||
### Phase 4: 마무리
|
||||
- [ ] 시딩 테스트
|
||||
- [ ] 초기화 테스트
|
||||
- [ ] 커스텀 필드 테스트
|
||||
- [ ] 에러 처리
|
||||
|
||||
---
|
||||
|
||||
## 8. SystemFieldDefinitions 상수 클래스
|
||||
|
||||
```php
|
||||
<?php
|
||||
// mng/app/Constants/SystemFieldDefinitions.php
|
||||
|
||||
namespace App\Constants;
|
||||
|
||||
class SystemFieldDefinitions
|
||||
{
|
||||
/**
|
||||
* products 테이블 시스템 필드 정의
|
||||
*/
|
||||
public const PRODUCTS = [
|
||||
['field_key' => 'code', 'field_name' => '품목코드', 'field_type' => 'textbox', 'is_required' => true, 'order_no' => 1],
|
||||
['field_key' => 'name', 'field_name' => '품목명', 'field_type' => 'textbox', 'is_required' => true, 'order_no' => 2],
|
||||
['field_key' => 'unit', 'field_name' => '단위', 'field_type' => 'dropdown', 'is_required' => true, 'order_no' => 3],
|
||||
['field_key' => 'product_type', 'field_name' => '제품유형', 'field_type' => 'dropdown', 'order_no' => 4, 'options' => [
|
||||
['label' => '완제품', 'value' => 'FG'],
|
||||
['label' => '부품', 'value' => 'PT'],
|
||||
]],
|
||||
['field_key' => 'category_id', 'field_name' => '카테고리', 'field_type' => 'dropdown', 'order_no' => 5],
|
||||
['field_key' => 'is_sellable', 'field_name' => '판매가능', 'field_type' => 'checkbox', 'order_no' => 6, 'default_value' => 'true'],
|
||||
['field_key' => 'is_purchasable', 'field_name' => '구매가능', 'field_type' => 'checkbox', 'order_no' => 7],
|
||||
['field_key' => 'is_producible', 'field_name' => '생산가능', 'field_type' => 'checkbox', 'order_no' => 8, 'default_value' => 'true'],
|
||||
['field_key' => 'is_active', 'field_name' => '활성화', 'field_type' => 'checkbox', 'order_no' => 9, 'default_value' => 'true'],
|
||||
['field_key' => 'certification_number', 'field_name' => '인증번호', 'field_type' => 'textbox', 'order_no' => 10],
|
||||
['field_key' => 'certification_date', 'field_name' => '인증일자', 'field_type' => 'date', 'order_no' => 11],
|
||||
['field_key' => 'certification_expiry', 'field_name' => '인증만료일', 'field_type' => 'date', 'order_no' => 12],
|
||||
];
|
||||
|
||||
/**
|
||||
* materials 테이블 시스템 필드 정의
|
||||
*/
|
||||
public const MATERIALS = [
|
||||
['field_key' => 'material_code', 'field_name' => '자재코드', 'field_type' => 'textbox', 'is_required' => true, 'order_no' => 1],
|
||||
['field_key' => 'name', 'field_name' => '자재명', 'field_type' => 'textbox', 'is_required' => true, 'order_no' => 2],
|
||||
['field_key' => 'item_name', 'field_name' => '품목명', 'field_type' => 'textbox', 'order_no' => 3],
|
||||
['field_key' => 'specification', 'field_name' => '규격', 'field_type' => 'textbox', 'order_no' => 4],
|
||||
['field_key' => 'unit', 'field_name' => '단위', 'field_type' => 'dropdown', 'is_required' => true, 'order_no' => 5],
|
||||
['field_key' => 'category_id', 'field_name' => '카테고리', 'field_type' => 'dropdown', 'order_no' => 6],
|
||||
['field_key' => 'is_inspection', 'field_name' => '검수필요', 'field_type' => 'checkbox', 'order_no' => 7],
|
||||
['field_key' => 'search_tag', 'field_name' => '검색태그', 'field_type' => 'textarea', 'order_no' => 8],
|
||||
];
|
||||
|
||||
/**
|
||||
* product_components 테이블 (BOM) 시스템 필드 정의
|
||||
*/
|
||||
public const PRODUCT_COMPONENTS = [
|
||||
['field_key' => 'ref_type', 'field_name' => '참조유형', 'field_type' => 'dropdown', 'order_no' => 1, 'options' => [
|
||||
['label' => '제품', 'value' => 'product'],
|
||||
['label' => '자재', 'value' => 'material'],
|
||||
]],
|
||||
['field_key' => 'ref_id', 'field_name' => '참조품목', 'field_type' => 'dropdown', 'order_no' => 2],
|
||||
['field_key' => 'quantity', 'field_name' => '수량', 'field_type' => 'number', 'is_required' => true, 'order_no' => 3],
|
||||
['field_key' => 'formula', 'field_name' => '계산공식', 'field_type' => 'textbox', 'order_no' => 4],
|
||||
['field_key' => 'note', 'field_name' => '비고', 'field_type' => 'textarea', 'order_no' => 5],
|
||||
];
|
||||
|
||||
/**
|
||||
* material_inspections 테이블 시스템 필드 정의
|
||||
*/
|
||||
public const MATERIAL_INSPECTIONS = [
|
||||
['field_key' => 'inspection_date', 'field_name' => '검수일', 'field_type' => 'date', 'is_required' => true, 'order_no' => 1],
|
||||
['field_key' => 'inspector_id', 'field_name' => '검수자', 'field_type' => 'dropdown', 'order_no' => 2],
|
||||
['field_key' => 'status', 'field_name' => '검수상태', 'field_type' => 'dropdown', 'order_no' => 3, 'options' => [
|
||||
['label' => '대기', 'value' => 'pending'],
|
||||
['label' => '진행중', 'value' => 'in_progress'],
|
||||
['label' => '완료', 'value' => 'completed'],
|
||||
['label' => '불합격', 'value' => 'rejected'],
|
||||
]],
|
||||
['field_key' => 'lot_no', 'field_name' => 'LOT번호', 'field_type' => 'textbox', 'order_no' => 4],
|
||||
['field_key' => 'quantity', 'field_name' => '검수수량', 'field_type' => 'number', 'order_no' => 5],
|
||||
['field_key' => 'passed_quantity', 'field_name' => '합격수량', 'field_type' => 'number', 'order_no' => 6],
|
||||
['field_key' => 'rejected_quantity', 'field_name' => '불합격수량', 'field_type' => 'number', 'order_no' => 7],
|
||||
['field_key' => 'note', 'field_name' => '비고', 'field_type' => 'textarea', 'order_no' => 8],
|
||||
];
|
||||
|
||||
/**
|
||||
* material_receipts 테이블 시스템 필드 정의
|
||||
*/
|
||||
public const MATERIAL_RECEIPTS = [
|
||||
['field_key' => 'receipt_date', 'field_name' => '입고일', 'field_type' => 'date', 'is_required' => true, 'order_no' => 1],
|
||||
['field_key' => 'lot_no', 'field_name' => 'LOT번호', 'field_type' => 'textbox', 'order_no' => 2],
|
||||
['field_key' => 'quantity', 'field_name' => '입고수량', 'field_type' => 'number', 'is_required' => true, 'order_no' => 3],
|
||||
['field_key' => 'unit_price', 'field_name' => '단가', 'field_type' => 'number', 'order_no' => 4],
|
||||
['field_key' => 'total_price', 'field_name' => '금액', 'field_type' => 'number', 'order_no' => 5],
|
||||
['field_key' => 'supplier_id', 'field_name' => '공급업체', 'field_type' => 'dropdown', 'order_no' => 6],
|
||||
['field_key' => 'warehouse_id', 'field_name' => '입고창고', 'field_type' => 'dropdown', 'order_no' => 7],
|
||||
['field_key' => 'po_number', 'field_name' => '발주번호', 'field_type' => 'textbox', 'order_no' => 8],
|
||||
['field_key' => 'invoice_number', 'field_name' => '송장번호', 'field_type' => 'textbox', 'order_no' => 9],
|
||||
['field_key' => 'note', 'field_name' => '비고', 'field_type' => 'textarea', 'order_no' => 10],
|
||||
];
|
||||
|
||||
/**
|
||||
* 소스 테이블 목록
|
||||
*/
|
||||
public const SOURCE_TABLES = [
|
||||
'products' => '제품',
|
||||
'materials' => '자재',
|
||||
'product_components' => 'BOM',
|
||||
'material_inspections' => '자재검수',
|
||||
'material_receipts' => '자재입고',
|
||||
];
|
||||
|
||||
/**
|
||||
* 소스 테이블별 필드 정의 가져오기
|
||||
*/
|
||||
public static function getFieldsFor(string $sourceTable): array
|
||||
{
|
||||
return match ($sourceTable) {
|
||||
'products' => self::PRODUCTS,
|
||||
'materials' => self::MATERIALS,
|
||||
'product_components' => self::PRODUCT_COMPONENTS,
|
||||
'material_inspections' => self::MATERIAL_INSPECTIONS,
|
||||
'material_receipts' => self::MATERIAL_RECEIPTS,
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체 테이블 필드 수 조회
|
||||
*/
|
||||
public static function getTotalFieldCount(string $sourceTable): int
|
||||
{
|
||||
return count(self::getFieldsFor($sourceTable));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 향후 확장
|
||||
|
||||
### 9.1 신규 도메인 추가 시
|
||||
1. `SystemFieldDefinitions`에 상수 추가
|
||||
2. `SOURCE_TABLES`에 테이블 추가
|
||||
3. MNG에서 해당 테이블 시딩
|
||||
|
||||
### 9.2 예정 도메인
|
||||
- [ ] 회계 (journals, accounts)
|
||||
- [ ] 생산 (work_orders, production_records)
|
||||
- [ ] 재고 (inventories, stock_movements)
|
||||
- [ ] 품질 (quality_controls)
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
|
||||
| 날짜 | 내용 |
|
||||
|------|------|
|
||||
| 2025-12-09 | 문서 목적 수정: 권한 관리 → 품목기준 필드 시딩/관리 |
|
||||
| 2025-12-09 | 초안 작성 |
|
||||
Reference in New Issue
Block a user