Files
sam-docs/plans/items-table-unification-plan.md
hskwon 011bcafcb1 docs: Items BOM 테스트 및 테이블 통합 계획 추가
- Items BOM API 플로우 테스트 JSON 추가 (items-bom-test.json)
  - GET /items/{id} BOM 확장 데이터 검증
  - GET /items/{id}/bom, /items/{id}/bom/tree 엔드포인트 테스트
- Items 테이블 통합 계획 문서 추가 (items-table-unification-plan.md)
  - Products/Materials → Items 통합 마이그레이션 설계
- MNG 필드 관리 계획 필수 참조 문서 섹션 보강
2025-12-11 23:15:43 +09:00

20 KiB

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

개요

목적

현재 이원화된 products/materials 테이블을 통합 items 테이블 구조로 전환하여:

  • BOM 관리 시 child_item_type 불필요 (ID만으로 유일 식별)
  • 단일 쿼리로 모든 품목 조회 가능
  • Service/Controller 코드 50% 감소
  • 유지보수성 및 확장성 향상

설계 변경 이력

  • 2025-12-11 (v1): 단일 items 테이블에 모든 필드 통합
  • 2025-12-11 (v2): 4개 테이블로 분리하여 성능 최적화
    • items: 핵심 필드 (목록 조회, BOM 계산)
    • item_details: 필수/인덱싱 필드 (검색, 필터링)
    • item_attributes: 동적 속성 (JSON 기반)
    • item_bending: 절곡 정보 (별도 관리)

현재 상태 분석

테이블 구조 비교

구분 Products Materials 통합 Items
ID id (1~808) id (1~417) id (새 통합 ID)
타입 product_type (FG, PT, PRODUCT, SUBASSEMBLY, PART, CS) material_type (SM, RM, CS) item_type
코드 code material_code code
이름 name name name
단위 unit unit unit
규격 - specification attributes (JSON)
BOM bom (JSON) - bom (JSON)
속성 attributes, options attributes, options attributes, options

데이터 현황 (2025-12-11)

  • Products: 808건
  • Materials: 417건
  • 총합: 1,225건

참조 테이블 현황

테이블 참조 방식 데이터
product_components parent_product_id, ref_type+ref_id 16건
bom_template_items ref_type+ref_id 1건
orders product_id 0건
order_items product_id 0건
quotes product_id 0건
material_receipts material_id 0건
lots material_id 0건
price_histories item_type+item_id 조회 필요

Phase 1: 테이블 생성 및 데이터 이관

1.1 items 테이블 (핵심 정보)

CREATE TABLE items (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID',

    -- 기본 정보
    item_type VARCHAR(15) NOT NULL COMMENT '품목 유형: FG, PT, SM, RM, CS, PRODUCT, SUBASSEMBLY, PART',
    code VARCHAR(100) NOT NULL COMMENT '품목 코드',
    name VARCHAR(255) NOT NULL COMMENT '품목명',
    unit VARCHAR(20) NULL COMMENT '단위',
    category_id BIGINT UNSIGNED NULL COMMENT '카테고리 ID',

    -- BOM
    bom JSON NULL COMMENT 'BOM [{child_item_id, quantity}, ...]',

    -- 상태
    is_active TINYINT(1) DEFAULT 1 COMMENT '활성 상태',

    -- 레거시 참조 (전환기간용)
    legacy_table VARCHAR(20) NULL COMMENT '원본 테이블: products | materials',
    legacy_id BIGINT UNSIGNED NULL COMMENT '원본 테이블 ID',

    -- 감사 필드
    created_by BIGINT UNSIGNED NULL COMMENT '생성자',
    updated_by BIGINT UNSIGNED NULL COMMENT '수정자',
    deleted_by BIGINT UNSIGNED NULL COMMENT '삭제자',
    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),
    INDEX idx_items_tenant_active (tenant_id, is_active),
    INDEX idx_items_legacy (legacy_table, legacy_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
COMMENT='Products + Materials 통합 품목 테이블 (핵심 정보)';

1.2 item_details 테이블 (필수/인덱싱 필드)

CREATE TABLE item_details (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    item_id BIGINT UNSIGNED NOT NULL COMMENT 'items 테이블 FK',

    -- Products 전용 (인덱싱/필터링)
    is_sellable TINYINT(1) DEFAULT 1 COMMENT '판매 가능',
    is_purchasable TINYINT(1) DEFAULT 0 COMMENT '구매 가능',
    is_producible TINYINT(1) DEFAULT 0 COMMENT '생산 가능',
    safety_stock INT NULL COMMENT '안전 재고',
    lead_time INT NULL COMMENT '리드타임 (일)',
    is_variable_size TINYINT(1) DEFAULT 0 COMMENT '가변 사이즈 여부',
    product_category VARCHAR(50) NULL COMMENT '제품 카테고리',
    part_type VARCHAR(50) NULL COMMENT '부품 유형',

    -- Materials 전용 (인덱싱/필터링)
    is_inspection VARCHAR(1) DEFAULT 'N' COMMENT '검사 대상 여부',

    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,

    -- 인덱스
    UNIQUE KEY uq_item_details_item_id (item_id),
    INDEX idx_item_details_sellable (is_sellable),
    INDEX idx_item_details_purchasable (is_purchasable),
    INDEX idx_item_details_producible (is_producible),
    INDEX idx_item_details_inspection (is_inspection),

    -- 외래키
    FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='품목 상세 정보 (필수/인덱싱 필드)';

1.3 item_attributes 테이블 (동적 속성)

CREATE TABLE item_attributes (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    item_id BIGINT UNSIGNED NOT NULL COMMENT 'items 테이블 FK',

    -- 동적 속성
    attributes JSON NULL COMMENT '동적 속성 (규격 정보 포함)',
    options JSON NULL COMMENT '옵션 (파일, 인증, 설명 등)',

    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
COMMENT='품목 동적 속성 (JSON 기반)';

1.4 item_bending 테이블 (절곡 정보)

CREATE TABLE item_bending (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    item_id BIGINT UNSIGNED NOT NULL COMMENT 'items 테이블 FK',

    -- 절곡 정보
    diagram VARCHAR(255) NULL COMMENT '절곡 도면 파일',
    details JSON NULL COMMENT '절곡 상세 정보',

    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,

    -- 인덱스
    UNIQUE KEY uq_item_bending_item_id (item_id),

    -- 외래키
    FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='품목 절곡 정보';

1.5 item_id_mappings 테이블 (전환기간용)

CREATE TABLE item_id_mappings (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    new_item_id BIGINT UNSIGNED NOT NULL COMMENT '새 items 테이블 ID',
    legacy_table VARCHAR(20) NOT NULL COMMENT '원본 테이블: products | materials',
    legacy_id BIGINT UNSIGNED NOT NULL COMMENT '원본 테이블 ID',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    -- 인덱스
    UNIQUE KEY uq_legacy (legacy_table, legacy_id),
    INDEX idx_new_item (new_item_id),

    -- 외래키
    FOREIGN KEY (new_item_id) REFERENCES items(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Products/Materials → Items ID 매핑 (전환기간용)';

1.6 데이터 이관 스크립트

// 1. 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,
        legacy_table, legacy_id
    )
    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,
        'products', id
    FROM products
");

// 2. Products → item_details 이관
DB::statement("
    INSERT INTO item_details (
        item_id, is_sellable, is_purchasable, is_producible,
        safety_stock, lead_time, is_variable_size,
        product_category, part_type, is_inspection,
        created_at, updated_at
    )
    SELECT
        i.id, p.is_sellable, p.is_purchasable, p.is_producible,
        p.safety_stock, p.lead_time, p.is_variable_size,
        p.product_category, p.part_type, 'N',
        p.created_at, p.updated_at
    FROM products p
    JOIN items i ON i.legacy_table = 'products' AND i.legacy_id = p.id
");

// 3. Products → item_attributes 이관
DB::statement("
    INSERT INTO item_attributes (
        item_id, attributes, options, created_at, updated_at
    )
    SELECT
        i.id, p.attributes, p.options, p.created_at, p.updated_at
    FROM products p
    JOIN items i ON i.legacy_table = 'products' AND i.legacy_id = p.id
");

// 4. Products → item_bending 이관 (절곡 정보 있는 경우만)
DB::statement("
    INSERT INTO item_bending (
        item_id, diagram, details, created_at, updated_at
    )
    SELECT
        i.id, p.bending_diagram, p.bending_details, p.created_at, p.updated_at
    FROM products p
    JOIN items i ON i.legacy_table = 'products' AND i.legacy_id = p.id
    WHERE p.bending_diagram IS NOT NULL OR p.bending_details IS NOT NULL
");

// 5. 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,
        legacy_table, legacy_id
    )
    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,
        'materials', id
    FROM materials
");

// 6. Materials → item_details 이관
DB::statement("
    INSERT INTO item_details (
        item_id, is_sellable, is_purchasable, is_producible,
        safety_stock, lead_time, is_variable_size,
        product_category, part_type, is_inspection,
        created_at, updated_at
    )
    SELECT
        i.id, 0, 1, 0,
        NULL, NULL, 0,
        NULL, NULL, m.is_inspection,
        m.created_at, m.updated_at
    FROM materials m
    JOIN items i ON i.legacy_table = 'materials' AND i.legacy_id = m.id
");

// 7. Materials → item_attributes 이관
// options에 기존 Materials 전용 필드 포함
DB::statement("
    INSERT INTO item_attributes (
        item_id, attributes, options, created_at, updated_at
    )
    SELECT
        i.id,
        m.attributes,
        JSON_OBJECT(
            'specification', m.specification,
            'item_name', m.item_name,
            'search_tag', m.search_tag,
            'remarks', m.remarks
        ),
        m.created_at, m.updated_at
    FROM materials m
    JOIN items i ON i.legacy_table = 'materials' AND i.legacy_id = m.id
");

// 8. ID 매핑 테이블 생성
DB::statement("
    INSERT INTO item_id_mappings (new_item_id, legacy_table, legacy_id)
    SELECT id, legacy_table, legacy_id
    FROM items
    WHERE legacy_table IS NOT NULL AND legacy_id IS NOT NULL
");

1.7 체크리스트

  • items 테이블 마이그레이션 생성
  • item_details 테이블 마이그레이션 생성
  • item_attributes 테이블 마이그레이션 생성
  • item_bending 테이블 마이그레이션 생성
  • item_id_mappings 테이블 마이그레이션 생성
  • Products 데이터 이관 스크립트 실행
  • Materials 데이터 이관 스크립트 실행
  • 데이터 검증 (건수, 필드값 일치)

Phase 2: Item 모델 및 Service 생성

2.1 Item 모델

// app/Models/Items/Item.php
class Item extends Model
{
    use BelongsToTenant, ModelTrait, SoftDeletes;

    protected $fillable = [
        'tenant_id', 'item_type', 'code', 'name', 'unit', 'category_id',
        'bom', 'is_active', 'created_by', 'updated_by',
    ];

    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 bending() { return $this->hasOne(ItemBending::class); }

    // 타입별 스코프
    public function scopeType($q, string $type) { return $q->where('item_type', $type); }
    public function scopeProducts($q) { return $q->whereIn('item_type', ['FG', 'PT', 'PRODUCT', 'SUBASSEMBLY', 'PART']); }
    public function scopeMaterials($q) { return $q->whereIn('item_type', ['SM', 'RM', 'CS']); }
}

2.2 관련 모델

// app/Models/Items/ItemDetail.php
class ItemDetail extends Model
{
    protected $fillable = [
        'item_id', 'is_sellable', 'is_purchasable', 'is_producible',
        'safety_stock', 'lead_time', 'is_variable_size',
        'product_category', 'part_type', 'is_inspection',
    ];

    protected $casts = [
        'is_sellable' => 'boolean',
        'is_purchasable' => 'boolean',
        'is_producible' => 'boolean',
        'is_variable_size' => 'boolean',
    ];

    public function item() { return $this->belongsTo(Item::class); }
}

// app/Models/Items/ItemAttribute.php
class ItemAttribute extends Model
{
    protected $fillable = ['item_id', 'attributes', 'options'];
    protected $casts = ['attributes' => 'array', 'options' => 'array'];
    public function item() { return $this->belongsTo(Item::class); }
}

// app/Models/Items/ItemBending.php
class ItemBending extends Model
{
    protected $fillable = ['item_id', 'diagram', 'details'];
    protected $casts = ['details' => 'array'];
    public function item() { return $this->belongsTo(Item::class); }
}

2.3 체크리스트

  • Item 모델 생성
  • ItemDetail 모델 생성
  • ItemAttribute 모델 생성
  • ItemBending 모델 생성
  • UnifiedItemsService 생성
  • ItemRequest (FormRequest) 생성
  • 단위 테스트 작성

Phase 3: API 엔드포인트 전환

3.1 신규 API (v2)

메서드 엔드포인트 설명
GET /api/v2/items 통합 품목 목록
GET /api/v2/items/{id} 품목 상세 (with details, attributes, bending)
POST /api/v2/items 품목 생성
PUT /api/v2/items/{id} 품목 수정
DELETE /api/v2/items/{id} 품목 삭제
DELETE /api/v2/items/batch 일괄 삭제

3.2 체크리스트

  • v2 라우트 정의
  • UnifiedItemsController 생성
  • Swagger 문서 작성 (v2)
  • v1 API 호환 레이어 구현
  • API 테스트 작성

Phase 4: 참조 테이블 마이그레이션

4.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 마이그레이션
quotes product_id → item_id 마이그레이션
material_receipts material_id → item_id 마이그레이션
lots material_id → item_id 마이그레이션
price_histories item_type+item_id → item_id 마이그레이션

4.2 체크리스트

  • 각 참조 테이블 마이그레이션 스크립트 작성
  • 관련 모델 업데이트
  • 데이터 검증

Phase 5: 레거시 정리

5.1 체크리스트

  • 모든 참조 테이블 전환 완료 확인
  • v1 API 사용량 모니터링 (0 확인)
  • items 레거시 컬럼 제거 (legacy_table, legacy_id)
  • item_id_mappings 테이블 제거
  • products 테이블 제거
  • materials 테이블 제거
  • 최종 테스트

테이블 구조 요약

┌─────────────────────────────────────────────────────────────────┐
│                         items (핵심)                             │
├─────────────────────────────────────────────────────────────────┤
│ id, tenant_id, item_type, code, name, unit, category_id        │
│ bom (JSON), is_active, legacy_table, legacy_id                 │
│ timestamps + soft deletes                                       │
└───────────────────────────┬─────────────────────────────────────┘
                            │ 1:1
        ┌───────────────────┼───────────────────┬─────────────────┐
        ▼                   ▼                   ▼                 │
┌───────────────┐   ┌───────────────┐   ┌───────────────┐        │
│ item_details  │   │item_attributes│   │ item_bending  │        │
├───────────────┤   ├───────────────┤   ├───────────────┤        │
│ item_id (UK)  │   │ item_id (UK)  │   │ item_id (UK)  │        │
│ is_sellable   │   │ attributes    │   │ diagram       │        │
│ is_purchasable│   │ options       │   │ details       │        │
│ is_producible │   └───────────────┘   └───────────────┘        │
│ safety_stock  │                                                 │
│ lead_time     │   ┌───────────────────────────────────┐        │
│ is_variable.. │   │       item_id_mappings            │        │
│ product_cat.. │   ├───────────────────────────────────┤        │
│ part_type     │   │ new_item_id ──────────────────────┼────────┘
│ is_inspection │   │ legacy_table, legacy_id           │
└───────────────┘   └───────────────────────────────────┘

사용 패턴별 쿼리

패턴 빈도 JOIN 쿼리
목록 조회 80%+ SELECT * FROM items WHERE ...
검색 빈번 SELECT * FROM items WHERE code LIKE ...
BOM 계산 5% 자기참조 SELECT * FROM items WHERE id IN (...)
상세 조회 15% 3-4테이블 SELECT * FROM items JOIN item_details ...
필터 (판매가능) 간헐 1테이블 JOIN item_details WHERE is_sellable = 1

일정 계획

Phase 작업 내용 상태
Phase 1 테이블 생성 + 데이터 이관 대기
Phase 2 모델 + Service 생성 대기
Phase 3 API 엔드포인트 전환 대기
Phase 4 참조 테이블 마이그레이션 대기
Phase 5 레거시 정리 대기

리스크 및 대응

리스크 1: ID 변경으로 인한 기존 연동 깨짐

  • 대응: legacy_table + legacy_id 컬럼으로 매핑 유지
  • 전환기간: v1 API 호환 레이어 제공

리스크 2: 데이터 이관 중 누락

  • 대응: 이관 전후 건수 검증
  • 롤백 계획: 이관 스크립트 역순 실행

리스크 3: JOIN 성능

  • 분석: 1:1 JOIN + UNIQUE 인덱스로 10만건도 수 ms 이내
  • 모니터링: 쿼리 성능 측정