# BOM 아이템 ↔ Items Master 매핑 작업 계획

> **작성일**: 2026-02-05
> **목적**: BOM 산출 로직에서 생성하는 모든 아이템에 SAM items master의 item_code/item_id를 매핑하여, 수주 등록 시 코드 기반 아이템 관리가 가능하도록 함
> **상태**: ✅ Phase 1, 2 완료 (검증 대기)

---

## 📍 현재 진행 상태

| 항목 | 내용 |
|------|------|
| **마지막 완료 작업** | Phase 2: BOM 산출 로직 매핑 수정 완료 |
| **다음 작업** | Phase 3: 수주 등록 화면에서 검증 |
| **진행률** | 2/3 Phase (66%) |
| **마지막 업데이트** | 2026-02-05 |

---

## 1. 개요

### 1.1 문제 상황
```
현재 상태:
- KyungdongFormulaHandler에서 22종 아이템 생성
- 그 중 5종은 item_code/item_id 없이 이름만으로 생성됨:
  1. 케이스 마구리 (calculateSteelItems)
  2. L바 (calculateSteelItems)
  3. 무게평철12T (calculateSteelItems)
  4. 검사비 (calculateDynamicItems) ← 유일한 SAM 미등록 아이템
  5. 주자재(스크린/슬랫) (calculateDynamicItems) ← KD-* 코드 사용

문제점:
- 수주 등록 시 아이템 그룹핑/집계에서 코드 기반 매칭 불가
- item_code가 없으면 item_name으로만 집계되어 중복 발생
```

### 1.2 목표 상태
```
목표:
- BOM 산출 결과의 모든 22종 아이템에 item_code + item_id 필수
- 수주 등록 시 동일 item_code 기준으로 수량/금액 집계

기대 효과:
- 3개소에 "환봉" 각 1개씩 → item_code "90201" 기준 3개로 집계
```

### 1.3 핵심 원칙
```
┌─────────────────────────────────────────────────────────────────┐
│  🎯 핵심 원칙 (CRITICAL)                                         │
├─────────────────────────────────────────────────────────────────┤
│  1. items master에 등록된 제품을 매핑해야 함                       │
│     → 코드를 임의로 만들어내면 안됨                                │
│  2. 기존 EST-/BD-/PT- 코드 체계를 활용                            │
│  3. BOM 산출 결과의 모든 아이템에 item_code + item_id 필수          │
└─────────────────────────────────────────────────────────────────┘
```

### 1.4 변경 승인 정책

| 분류 | 예시 | 승인 |
|------|------|------|
| ✅ 즉시 가능 | BOM 로직 내 item_code/item_id 매핑 추가 | 불필요 |
| ⚠️ 컨펌 필요 | items master에 신규 아이템 등록 | **필수** |
| 🔴 금지 | items 테이블 구조 변경, 기존 코드 체계 변경 | 별도 협의 |

---

## 2. 전체 아이템 매핑 테이블 (22종)

### 2.1 calculateSteelItems (절곡품) - 10종

| # | BOM 아이템명 | 현재 item_code | SAM 매핑 코드 | SAM item_id | 매핑 방법 |
|---|-------------|---------------|--------------|-------------|----------|
| 1 | 케이스 | null | `BD-케이스-{규격}` | lookup | 규격으로 items 검색 (예: BD-케이스-500*350) |
| 2 | 케이스용 연기차단재 | null | `EST-SMOKE-케이스용` | 14912 | 고정 매핑 |
| 3 | **케이스 마구리** | **null** | `BD-마구리-{규격}` | lookup | 규격으로 items 검색 (예: BD-마구리-505*355) |
| 4 | 가이드레일 | null | `BD-가이드레일-{모델}-{재질}-{규격}` | lookup | 모델+재질+규격으로 검색 |
| 5 | 레일용 연기차단재 | null | `EST-SMOKE-레일용` | 14911 | 고정 매핑 |
| 6 | 하장바 | null | `00035` 또는 `00036` | 14158/14159 | 재질에 따라 분기 |
| 7 | **L바** | **null** | `BD-L-BAR-{모델}-{규격}` | lookup | 모델+규격으로 검색 (예: BD-L-BAR-KWE01-17*60) |
| 8 | 보강평철 | null | `BD-보강평철-50` | 14790 | 고정 매핑 |
| 9 | **무게평철12T** | **null** | `00021` | 14147 | 고정 매핑 (평철12T와 동일) |
| 10 | 환봉 | null | `90201`~`90204` | 14407~14410 | 파이 규격에 따라 분기 (30/35/45/50파이) |

### 2.2 calculatePartItems (부자재) - 5종

| # | BOM 아이템명 | 현재 item_code | SAM 매핑 코드 | SAM item_id | 매핑 방법 |
|---|-------------|---------------|--------------|-------------|----------|
| 11 | 감기샤프트 | null | `EST-SHAFT-{인치}-{길이}` | lookup | 인치+길이로 검색 (예: EST-SHAFT-4-6) |
| 12 | 각파이프 | null | `EST-PIPE-{두께}-{길이}` | lookup | 두께+길이로 검색 (예: EST-PIPE-1.4-6000) |
| 13 | 모터받침 앵글 | null | `EST-ANGLE-BRACKET-{타입}` | lookup | 타입으로 검색 (스크린용/철제300K/400K/800K) |
| 14 | 앵글 | null | `EST-ANGLE-MAIN-{타입}` | lookup | 앵글타입+길이로 검색 |
| 15 | 조인트바 | null | `800361` | 14733 | 고정 매핑 |

### 2.3 calculateDynamicItems (동적항목) - 7종

| # | BOM 아이템명 | 현재 item_code | SAM 매핑 코드 | SAM item_id | 매핑 방법 |
|---|-------------|---------------|--------------|-------------|----------|
| 16 | **검사비** | **KD-INSPECTION** | `EST-INSPECTION` | **(신규등록)** | **items master 신규 등록 필요** |
| 17 | 주자재(스크린) | KD-SCREEN | `EST-RAW-스크린-{타입}` | lookup | 타입으로 검색 (실리카/화이바/와이어) |
| 18 | 주자재(슬랫) | KD-SLAT | `EST-RAW-슬랫-{타입}` | lookup | 타입으로 검색 (방범/방화) |
| 19 | 모터 | KD-MOTOR-{용량} | `EST-MOTOR-{전압}-{용량}` | lookup | 전압+용량으로 검색 |
| 20 | 제어기 | KD-CTRL-{타입} | `EST-CTRL-{타입}` | lookup | 타입으로 검색 (노출형/매립형) |
| 21 | 뒷박스 | KD-CTRL-BACKBOX | `EST-CTRL-뒷박스` | 14863 | 고정 매핑 |
| 22 | 브라켓 | (모터에 포함) | `KD브라켓트*` 또는 `EST-*` | lookup | 모터 용량에 따라 분기 |

> **굵은 글씨**: 현재 미매핑 상태 (작업 대상)

---

## 3. 대상 범위

### 3.1 Phase 1: 미등록 아이템 등록 (사용자 승인 필요)

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1.1 | 검사비(EST-INSPECTION) items master 신규 등록 | ✅ | ID: 14913 |
| 1.2 | 무게평철12T → 00021(평철12T) 동일 아이템 확인 | ✅ | ID: 14147 확인 완료 |

### 3.2 Phase 2: BOM 산출 로직 매핑 수정

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 2.1 | calculateSteelItems: 10종 아이템에 item_code/item_id 매핑 | ✅ | withItemMapping 헬퍼 사용 |
| 2.2 | calculatePartItems: 5종 아이템에 item_code/item_id 매핑 | ✅ | withItemMapping 헬퍼 사용 |
| 2.3 | calculateDynamicItems: KD-* → EST-* 코드 변환 | ✅ | 모터/제어기/주자재 매핑 완료 |

### 3.3 Phase 3: 검증

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 3.1 | 견적 산출 실행 → 모든 아이템 item_code/item_id 확인 | ✅ | 18종 아이템 모두 매핑됨 |
| 3.2 | 수주 등록 → 코드 기반 아이템 그룹핑/집계 정상 동작 | ⏳ | 화면 검증 필요 |

---

## 4. 실행 환경 및 명령어

### 4.1 Docker 환경
```bash
# API 컨테이너 접속
docker exec -it sam-api-1 bash

# PHP Tinker 실행
docker exec sam-api-1 php artisan tinker --execute='...'
```

### 4.2 주요 확인 명령어

```bash
# SAM items master에서 특정 코드 검색
docker exec sam-api-1 php artisan tinker --execute='
$item = \App\Models\Items\Item::where("tenant_id", 287)
    ->where("code", "EST-INSPECTION")
    ->first();
echo $item ? "ID: {$item->id}, Code: {$item->code}, Name: {$item->name}" : "NOT FOUND";
'

# 코드 패턴으로 검색 (BD-*, EST-* 등)
docker exec sam-api-1 php artisan tinker --execute='
$items = \App\Models\Items\Item::where("tenant_id", 287)
    ->where("code", "like", "BD-마구리%")
    ->get(["id", "code", "name"]);
foreach ($items as $item) {
    echo "{$item->id} | {$item->code} | {$item->name}" . PHP_EOL;
}
'

# 5130 chandj DB 연결 테스트
docker exec sam-api-1 php artisan tinker --execute='
$count = \Illuminate\Support\Facades\DB::connection("chandj")
    ->table("KDunitprice")->count();
echo "KDunitprice 총 건수: {$count}";
'
```

### 4.3 검사비 신규 등록 (Phase 1.1)

```bash
# 검사비 아이템 신규 등록
docker exec sam-api-1 php artisan tinker --execute='
$item = \App\Models\Items\Item::create([
    "tenant_id" => 287,
    "code" => "EST-INSPECTION",
    "name" => "검사비",
    "unit" => "EA",
    "item_type" => "product",
    "is_active" => true,
]);
echo "Created: ID={$item->id}, Code={$item->code}";
'
```

---

## 5. 코드 수정 가이드

### 5.1 수정 대상 파일
```
api/app/Services/Quote/FormulaHandlers/KyungdongFormulaHandler.php
```

### 5.2 수정 패턴 (예시: calculateSteelItems 내 케이스 마구리)

**현재 코드 (item_code 없음):**
```php
$items[] = [
    'item_name' => '케이스 마구리',
    'item_code' => null,  // ❌ 없음
    'item_id' => null,    // ❌ 없음
    'quantity' => $quantity,
    'unit_price' => $unitPrice,
    'total_price' => $quantity * $unitPrice,
    // ...
];
```

**수정 후 (item_code/item_id 매핑):**
```php
// 규격 계산 (예: 505*355)
$spec = "{$caseWidth}*{$caseDepth}";
$itemCode = "BD-마구리-{$spec}";

// items master에서 lookup
$item = \App\Models\Items\Item::where('tenant_id', $this->tenantId)
    ->where('code', $itemCode)
    ->first();

$items[] = [
    'item_name' => '케이스 마구리',
    'item_code' => $item?->code ?? $itemCode,  // ✅ 코드 매핑
    'item_id' => $item?->id,                    // ✅ ID 매핑
    'quantity' => $quantity,
    'unit_price' => $unitPrice,
    'total_price' => $quantity * $unitPrice,
    // ...
];
```

### 5.3 아이템 lookup 헬퍼 메서드 추가 (권장)

```php
/**
 * items master에서 코드로 아이템 조회
 */
private function lookupItem(string $code): ?Item
{
    return Item::where('tenant_id', $this->tenantId)
        ->where('code', $code)
        ->first();
}

/**
 * 아이템 배열에 item_code/item_id 추가
 */
private function withItemMapping(array $item, string $code): array
{
    $masterItem = $this->lookupItem($code);
    return array_merge($item, [
        'item_code' => $masterItem?->code ?? $code,
        'item_id' => $masterItem?->id,
    ]);
}
```

---

## 6. 검증 방법

### 6.1 견적 산출 후 BOM 결과 확인

```bash
# 최근 견적의 BOM 결과에서 item_code 확인
docker exec sam-api-1 php artisan tinker --execute='
$quote = \App\Models\Quote::where("tenant_id", 287)
    ->whereNotNull("calculation_result")
    ->latest()
    ->first();

$items = $quote->calculation_result["items"] ?? [];
$noCode = array_filter($items, fn($i) => empty($i["item_code"]));

echo "총 아이템: " . count($items) . "개" . PHP_EOL;
echo "item_code 없음: " . count($noCode) . "개" . PHP_EOL;

foreach ($noCode as $i) {
    echo "  - {$i["item_name"]}" . PHP_EOL;
}
'
```

### 6.2 수주 등록 화면 확인
1. `/orders/create?quoteId={ID}`로 수주 등록 화면 진입
2. 아이템 목록에서 동일 아이템이 코드 기준으로 집계되는지 확인
3. 3개소에 "환봉" 각 1개 → "환봉" 1행, 수량 3개로 표시되어야 함

---

## 7. 성공 기준

| 기준 | 검증 방법 | 달성 |
|------|----------|:----:|
| BOM 산출 22종 아이템 전부 item_code 보유 | Phase 3.1 검증 쿼리 | ⏳ |
| BOM 산출 22종 아이템 전부 item_id 보유 | Phase 3.1 검증 쿼리 | ⏳ |
| 수주 등록 시 코드 기반 아이템 집계 정상 동작 | Phase 3.2 화면 확인 | ⏳ |
| 기존 견적 산출 금액에 영향 없음 | 기존 견적 재산출 후 금액 비교 | ⏳ |

---

## 8. 관련 소스 파일

| 파일 | 수정 여부 | 용도 |
|------|:--------:|------|
| `api/app/Services/Quote/FormulaHandlers/KyungdongFormulaHandler.php` | **수정** | BOM 산출 매핑 로직 추가 |
| `api/app/Models/Items/Item.php` | 읽기 | items lookup |
| `react/src/components/orders/OrderRegistration.tsx` | 검증 | 수주 등록 아이템 그룹핑 |
| `react/src/components/orders/actions.ts` | 검증 | 수주 데이터 변환 |

---

## 9. 참고 정보

### 9.1 SAM 견적 전용 코드 체계

| 접두사 | 용도 | 예시 |
|--------|------|------|
| BD- | 절곡품 (모델/규격별) | BD-케이스-500*350, BD-마구리-505*355 |
| EST- | 견적 산출 전용 | EST-MOTOR-220V-300K, EST-INSPECTION |
| PT- | 품목 템플릿 (규격 미포함) | PT-케이스, PT-가이드레일 |
| PM- | 제어기 부품 | PM-020 (제어기 노출형) |

### 9.2 5130 DB 연결 정보

```
# api/.env
CHANDJ_DB_HOST=sam-mysql-1
CHANDJ_DB_DATABASE=chandj
CHANDJ_DB_USERNAME=root
CHANDJ_DB_PASSWORD=root
```

### 9.3 상세 분석 문서
- 전체 분석 결과: `docs/data/analysis/bom-item-mapping-analysis.md`
- 견적 시스템 구조: `docs/features/quotes/README.md`

---

## 10. 컨펌 대기 목록

| # | 항목 | 변경 내용 | 승인 |
|---|------|----------|:----:|
| 1 | 검사비 신규 등록 | items master에 EST-INSPECTION 추가 | ⏳ |
| 2 | 무게평철12T 동일성 | BOM의 무게평철12T = 00021 평철12T 인지 | ⏳ |

---

## 11. 변경 이력

| 날짜 | 작업 | 변경 내용 | 파일 |
|------|------|----------|------|
| 2026-02-05 | 분석 | 22종 아이템 매핑 상태 분석 완료 | bom-item-mapping-analysis.md |
| 2026-02-05 | 계획 | 작업 계획 문서 작성 | bom-item-mapping-plan.md |
| 2026-02-05 | Phase 1 | 검사비(EST-INSPECTION) ID:14913 신규 등록 | items master |
| 2026-02-05 | Phase 2 | KyungdongFormulaHandler 매핑 로직 추가 | KyungdongFormulaHandler.php |

---

*이 문서는 /sc:plan 스킬로 생성되었습니다.*