# 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 체크리스트 - [x] products 비표준 타입 삭제 - [x] 관련 BOM 데이터 정리 - [x] 삭제 건수 확인 --- ## 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 체크리스트 - [x] items 마이그레이션 생성 - [x] item_details 마이그레이션 생성 - [x] item_attributes 마이그레이션 생성 - [x] 데이터 이관 스크립트 실행 - [x] 건수 검증 (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 체크리스트 - [x] Item 모델 생성 - [x] ItemDetail 모델 생성 - [x] ItemAttribute 모델 생성 - [x] ItemService 생성 - [x] 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 체크리스트 - [x] ItemPage 모델 수정 (getTargetModelClass) - [x] item_pages.source_table 마이그레이션 - [x] 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 체크리스트 - [x] ItemController 생성 - [x] ItemIndexRequest, ItemStoreRequest 등 생성 - [x] 라우트 등록 - [x] Swagger 문서 작성 - [x] 기존 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 체크리스트 - [x] 각 참조 테이블 마이그레이션 작성 - [x] 관련 모델 관계 업데이트 - [x] 데이터 검증 --- ## Phase 6: 정리 ### 6.1 체크리스트 - [x] CRUD 테스트 (전체 item_type) - [x] BOM 계산 테스트 - [x] Item-Master 연동 테스트 - [x] 참조 무결성 테스트 - [x] products 테이블 삭제 - [x] materials 테이블 삭제 - [x] 기존 Product, Material 모델 삭제 - [x] 기존 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 | 정리 | ✅ 완료 | > **완료일**: 2025-12-15 > **관련 커밋**: `039fd62` (products/materials 테이블 삭제), `a93dfe7` (Phase 6 완료) --- ## 리스크 | 리스크 | 대응 | |--------|------| | 데이터 이관 누락 | 이관 전후 건수 검증 | | Item-Master 연동 오류 | source_table 변경 전 테스트 | | BOM 순환 참조 | 저장 시 검증 로직 추가 | | Code 중복 (products↔materials) | 개발 중이므로 품목관리 완료 후 경동기업 데이터 전체 삭제 후 재세팅 예정. 중복 데이터는 삭제 처리 | --- ## 롤백 계획 각 Phase는 독립적 마이그레이션으로 구성: ```bash # Phase 1 롤백 php artisan migrate:rollback --step=3 # 데이터 복구 (products/materials 테이블 유지 상태에서) # 신규 테이블만 삭제하면 됨 ```