Files
sam-docs/plans/archive/items-table-unification-plan.md
권혁성 28b69e5449 docs: archive 37개 + COMPLETED 3개 복원 - 향후 docs/ 정식 문서화 시 참조용
- 완료 문서의 상세 내용은 추후 docs/ 구조화 시 정식 문서에 반영 예정
- HISTORY.md는 요약 인덱스로 유지, 개별 파일은 상세 참조용 보관

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 22:32:20 +09:00

19 KiB

Items 테이블 통합 마이그레이션 계획

참조 문서

필수 확인

문서 경로 내용
ItemMaster 연동 설계서 specs/item-master-integration.md source_table, EntityRelationship 구조
DB 스키마 specs/database-schema.md 테이블 구조, Multi-tenant 아키텍처

참고 문서

문서 경로 내용
품목관리 마이그레이션 가이드 projects/mes/ITEM_MANAGEMENT_MIGRATION_GUIDE.md 프론트엔드 마이그레이션
API 품목 분석 요약 projects/mes/00_baseline/docs_breakdown/api_item_analysis_summary.md 기존 API 분석, price_histories
Swagger 가이드 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만

비표준 데이터 삭제:

-- products에서 비표준 타입 삭제 (PRODUCT, SUBASSEMBLY, PART, CS)
DELETE FROM products WHERE product_type NOT IN ('FG', 'PT');

-- materials는 이미 표준 타입만 사용 (SM, RM, CS)

0.2 BOM 데이터 정리

통합 시 문제되는 BOM 데이터 삭제:

-- 삭제될 products/materials를 참조하는 BOM 항목 제거
-- (Phase 1 이관 전에 실행)

0.3 체크리스트

  • products 비표준 타입 삭제
  • 관련 BOM 데이터 정리
  • 삭제 건수 확인

Phase 1: items 테이블 생성 + 데이터 이관

1.1 items 테이블

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 테이블 (확장 필드)

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 테이블 (동적 속성)

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 데이터 이관 스크립트

// 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 모델

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

// 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 변경

// 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 데이터 업데이트

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

// 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 라우트

// 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 계산 로직

통합 전

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']);
    }
}

통합 후

$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):

// 기존: 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):

// 기존
{ "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_idchild_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는 독립적 마이그레이션으로 구성:

# Phase 1 롤백
php artisan migrate:rollback --step=3

# 데이터 복구 (products/materials 테이블 유지 상태에서)
# 신규 테이블만 삭제하면 됨