- 완료 문서의 상세 내용은 추후 docs/ 구조화 시 정식 문서에 반영 예정 - HISTORY.md는 요약 인덱스로 유지, 개별 파일은 상세 참조용 보관 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
589 lines
19 KiB
Markdown
589 lines
19 KiB
Markdown
# 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 테이블 유지 상태에서)
|
|
# 신규 테이블만 삭제하면 됨
|
|
``` |