# 생산지시 시 bending_info 자동 생성 계획

> **작성일**: 2026-02-19
> **목적**: 수주 → 생산지시 시 절곡 공정의 bending_info JSON을 work_orders.options에 자동 삽입
> **기준 문서**: `api/app/Services/OrderService.php` (createProductionOrder), `react/.../bending/types.ts`
> **상태**: 🔄 진행중

---

## 📍 현재 진행 상태

| 항목 | 내용 |
|------|------|
| **마지막 완료 작업** | 분석 완료 + 계획 문서 작성 |
| **다음 작업** | Phase 1.1: BendingInfoBuilder 서비스 생성 |
| **진행률** | 0/7 (0%) |
| **마지막 업데이트** | 2026-02-20 |

---

## 1. 개요

### 1.1 배경

현재 절곡 작업일지(BendingWorkLogContent)에 표시할 bending_info 데이터를 **수동으로 DB에 INSERT** 해야 함.
수주(Order) → 생산지시(WorkOrder) 생성 시 `OrderService::createProductionOrder()`에서 자동으로 bending_info를
생성하여 `work_orders.options.bending_info`에 저장하는 로직이 필요함.

### 1.2 현재 데이터 흐름 vs 목표

#### 현재 (Before)
```
OrderNode.options
  ├─ product_code: "FG-KSS02-벽면형-SUS"
  ├─ width: 3560, height: 4450
  └─ bom_result.items[]: (steel category BOM 품목)

→ OrderService::createProductionOrder() (라인 959)
  → WorkOrder::create() (라인 1111)
    → ⚠️ options 미설정 (null)
  → work_order_items INSERT (라인 1183)
    → options.bending_info = node.options.bending_info ?? null  (라인 1179)
    → ⚠️ 현재 order_nodes.options에 bending_info 없음 → null 저장
```

#### 목표 (After)
```
OrderService::createProductionOrder() (라인 959)
  │
  ├─ 공정별 아이템 그룹핑 (라인 1035~1089)
  │   └─ $itemsByProcess[$processId] = [items...]
  │
  ├─ foreach ($itemsByProcess) → WorkOrder 생성 (라인 1103)
  │   │
  │   ├─ 절곡 공정인지 확인 (process.process_name === '절곡')
  │   │   └─ YES → BendingInfoBuilder::build($order, $processId)
  │   │            ├─ OrderNode.options.product_code 파싱
  │   │            ├─ OrderNode.options.bom_result.items 분석
  │   │            └─ bending_info JSON 조립
  │   │
  │   └─ WorkOrder::create([
  │        ...기존 필드들,
  │        'options' => ['bending_info' => $bendingInfo]  ← 신규
  │      ]) (라인 1111)
  │
  └─ work_order_items INSERT (라인 1183, 기존 유지)
```

#### 핸들러 자동 생성 원리
```
BendingInfoBuilder::build($order, $processId)
    │
    ├─ 1. 절곡 공정 확인 (process.process_name === '절곡')
    │
    ├─ 2. product_code 파싱
    │   └─ "FG-KSS02-벽면형-SUS" → productCode: "KSS02", guideType: "벽면형", finishMaterial: "SUS마감"
    │
    ├─ 3. BOM items 카테고리 분류 (item_code 패턴 매칭)
    │   ├─ BD-가이드레일-* → guideRail
    │   ├─ BD-케이스-* → shutterBox
    │   ├─ BD-마구리-* → shutterBox (마구리)
    │   ├─ *하장바* → bottomBar
    │   ├─ EST-SMOKE-* → smokeBarrier
    │   ├─ BD-L-BAR-* → detailParts
    │   └─ BD-보강평철-* → detailParts
    │
    ├─ 4. 다중 노드 집계 (길이별 수량 합산)
    │   ├─ height → 가이드레일 길이별 수량
    │   ├─ width → 셔터박스/하단마감재 길이별 수량
    │   └─ BOM quantity × 노드 수 → 총 수량
    │
    └─ 5. BendingInfoExtended 구조 JSON 반환
```

### 1.3 기준 원칙

```
┌─────────────────────────────────────────────────────────────────┐
│  🎯 핵심 원칙                                                    │
├─────────────────────────────────────────────────────────────────┤
│  1. BendingInfoBuilder는 독립 서비스 (OrderService 변경 최소화)    │
│  2. 기존 createProductionOrder 흐름은 유지, options 삽입만 추가    │
│  3. order_nodes.options.bom_result + product_code가 유일한 소스   │
│  4. 프론트엔드 BendingInfoExtended 인터페이스 완전 호환            │
│  5. 절곡 공정이 아닌 WorkOrder에는 절대 bending_info 미생성        │
│  6. 기존 work_order_items.options.bending_info 흐름은 유지 (하위호환) │
└─────────────────────────────────────────────────────────────────┘
```

### 1.4 변경 승인 정책

| 분류 | 예시 | 승인 |
|------|------|------|
| ✅ 즉시 가능 | BendingInfoBuilder 서비스 클래스 신규 생성, 헬퍼 메서드 추가 | 불필요 |
| ⚠️ 컨펌 필요 | OrderService::createProductionOrder 수정 (options 삽입), BOM 파싱 규칙 확정 | **필수** |
| 🔴 금지 | 기존 BOM 계산 로직 변경, order_nodes 스키마 변경, 기존 work_order_items.options 구조 변경 | 별도 협의 |

---

## 2. 현황 분석

### 2.1 OrderService::createProductionOrder 흐름 (라인 959~1214)

현재 `createProductionOrder`는 다음 순서로 동작:

```
1. 수주 로드 (라인 966)
   $order = Order::with(['items', 'rootNodes'])->findOrFail($orderId)

2. 공정별 아이템 매핑 조회 (라인 1008~1014)
   DB::table('process_items') → $itemProcessMap

3. 아이템을 공정별로 그룹핑 (라인 1035~1089)
   3단계 fallback:
   ├─ item_id → process_items 직접 매핑 (라인 1041~1042)
   ├─ order_node_id → BOM item_name → process_items (라인 1045~1050)
   └─ item_code → item_id → process_items (라인 1054~1078)
   결과: $itemsByProcess[$processId] = ['items' => [...], 'processId' => int]

4. 공정별 WorkOrder 생성 (라인 1103)
   foreach ($itemsByProcess as $key => $group) {
       $workOrder = WorkOrder::create([...])  // 라인 1111~1124
       // ⚠️ 현재 'options' 필드 미설정
   }

5. work_order_items INSERT (라인 1183~1197)
   $woItemOptions = [
       'floor', 'code', 'width', 'height',
       'cutting_info', 'slat_info',
       'bending_info' => $nodeOptions['bending_info'] ?? null,  // 라인 1179
       'wip_info'
   ]
```

**핵심 발견**: WorkOrder::create (라인 1111~1124)에 `options` 필드가 **전혀 설정되지 않음**. bending_info는 `work_order_items.options`에만 들어가는데, 이마저도 `order_nodes.options.bending_info`가 null이면 null 저장.

### 2.2 order_nodes.options 구조 (실제 데이터)

order_id=43 (WO 74의 원천 수주)의 root_nodes (id=116~125, 5개소 × 2=10 노드):

```json
{
  "product_code": "FG-KSS02-벽면형-SUS",
  "width": 3560,
  "height": 4450,
  "bom_result": {
    "items": [
      { "item_code": "BD-케이스-500*380", "item_name": "케이스 500*380", "category": "steel", "quantity": 3.62 },
      { "item_code": "BD-마구리-505*385", "item_name": "마구리 505*385", "category": "steel", "quantity": 1 },
      { "item_code": "BD-가이드레일-KSS02-SUS-120*70", "item_name": "가이드레일 KSS02...", "category": "steel", "quantity": 8.7 },
      { "item_code": "EST-SMOKE-레일용", "item_name": "연기차단재(레일)", "category": "steel", "quantity": 8.7 },
      { "item_code": "EST-SMOKE-케이스용", "item_name": "연기차단재(케이스)", "category": "steel", "quantity": 3.62 },
      { "item_code": "00035", "item_name": "철재용하장바(SUS)3000", "category": "steel", "quantity": 3.4 },
      { "item_code": "BD-L-BAR-KSS02-17*60", "item_name": "L-BAR KSS02...", "category": "steel", "quantity": 3.62 },
      { "item_code": "BD-보강평철-50", "item_name": "보강평철 50", "category": "steel", "quantity": 3.62 },
      // ... (parts, motor, controller 등 다른 category도 포함)
    ]
  }
}
```

**중요**: `bom_result.items[]`에는 `category: "steel"`인 아이템만 절곡(bending) 관련. `parts`, `motor`, `controller` 등은 다른 공정용.

### 2.3 work_orders.options 현재 상태

| work_order_id | process_name | options |
|---------------|-------------|---------|
| 74 (수동 삽입) | 절곡 | `{"bending_info": {...전체 JSON...}}` |
| 기타 | 절곡 외 | `null` |

- WO 74만 수동으로 bending_info를 삽입한 상태
- 다른 WorkOrder는 options가 null (createProductionOrder에서 미설정)

### 2.4 프론트엔드 데이터 소비 경로

```
API: GET /work-orders/{id}
  → WorkOrderService::show()
  → WorkOrder 모델 (options JSON 자동 디코딩)
  → API 응답: { ..., options: { bending_info: {...} } }

React: transformApiToFrontend() (types.ts 라인 495)
  → bendingInfo: api.options?.bending_info || undefined
  → BendingWorkLogContent에 props.bendingInfo로 전달
  → BendingInfoExtended 인터페이스로 사용
```

### 2.5 BendingInfoExtended 구조 (프론트엔드 목표 스키마)

```typescript
// react/src/components/production/WorkOrders/documents/bending/types.ts (라인 32~68)
interface BendingInfoExtended {
  productCode: string;        // "KSS02"
  finishMaterial: string;     // "SUS마감"
  common: {
    kind: string;             // "혼합형 120X70"
    type: string;             // "벽면형(120*70)"
    lengthQuantities: LengthQuantity[];  // [{length: 4450, quantity: 5}]
  };
  detailParts: Array<{
    partName: string;         // "엘바", "하장바"
    material: string;         // "EGI 1.6T"
    barcyInfo: string;        // "16 I 75"
  }>;
  guideRail: {
    wall: GuideRailTypeData | null;   // 벽면형 가이드레일
    side: GuideRailTypeData | null;   // 측면형 가이드레일
  };
  bottomBar: {
    material: string;         // "SUS 1.5T"
    extraFinish: string;      // "없음"
    length3000Qty: number;
    length4000Qty: number;
  };
  shutterBox: ShutterBoxData[];  // [{direction, size, lengths[]}]
  smokeBarrier: {
    w50: LengthQuantity[];    // 레일용 W50
    w80Qty: number;           // 케이스용 W80 수량
  };
}
```

### 2.6 BOM item_code → bending_info 카테고리 매핑

| item_code 패턴 | bending_info 필드 | 추출 정보 | 확인된 실제 코드 |
|----------------|-------------------|----------|----------------|
| `BD-케이스-{W}*{H}` | `shutterBox[].size` | 사이즈 "500*380" | BD-케이스-500*380 |
| `BD-마구리-{W}*{H}` | `shutterBox[].finCoverQty` | 마구리 수량 | BD-마구리-505*385 |
| `BD-가이드레일-{model}-{finish}-{W}*{H}` | `guideRail.wall/side` | baseSize "120*70" | BD-가이드레일-KSS02-SUS-120*70 |
| `EST-SMOKE-레일용` | `smokeBarrier.w50` | 레일 연기차단재 수량 | EST-SMOKE-레일용 |
| `EST-SMOKE-케이스용` | `smokeBarrier.w80Qty` | 케이스 연기차단재 수량 | EST-SMOKE-케이스용 |
| `BD-L-BAR-{model}-{W}*{H}` | `detailParts[]` | L-BAR 상세 | BD-L-BAR-KSS02-17*60 |
| `BD-보강평철-{size}` | `detailParts[]` | 보강평철 상세 | BD-보강평철-50 |
| `*하장바*` (item_name 기준) | `bottomBar` | 하장바 길이/마감 | 철재용하장바(SUS)3000 (코드: 00035) |

### 2.7 product_code 파싱 규칙

`FG-KSS02-벽면형-SUS` 패턴:

| 세그먼트 위치 | 의미 | 추출 규칙 | 예시값 |
|--------------|------|----------|--------|
| 0 | 접두사 | 무시 (항상 "FG") | FG |
| 1 | 제품 모델 | `productCode` | KSS02 |
| 2 | 가이드레일 타입 | `guideType` (벽면형/측면형/혼합형) | 벽면형 |
| 3 | 마감재 | `finishMaterial` → "SUS" → "SUS마감", "EGI" → "EGI마감" | SUS |

### 2.8 재질 매핑 (getMaterialMapping 기반)

```
Group 1 - SUS 전용 (KQTS01, KSS01, KSS02):
  guideRailFinish: "SUS 1.2T"
  bodyMaterial: "EGI 1.55T"
  bottomBarFinish: "SUS 1.5T"

Group 2 - KTE01 (마감유형 분기):
  SUS마감 → guideRailFinish: "EGI 1.55T" + extraFinish: "SUS 1.2T"
  EGI마감 → guideRailFinish: "EGI 1.55T" (extra 없음)

Group 3 - 기타 (KSE01, KWE01):
  SUS마감 → guideRailFinish: "EGI 1.55T" + extraFinish: "SUS 1.2T"
  EGI마감 → guideRailFinish: "EGI 1.55T" (extra 없음)
```

---

## 3. 대상 범위

### Phase 1: BendingInfoBuilder 서비스 (백엔드 핵심)

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1.1 | BendingInfoBuilder 서비스 클래스 생성 | ⏳ | `api/app/Services/Production/BendingInfoBuilder.php` |
| 1.2 | parseProductCode() 구현 | ⏳ | "FG-KSS02-벽면형-SUS" → productCode, guideType, finishMaterial |
| 1.3 | categorizeBomItem() 구현 | ⏳ | item_code 패턴 → 8개 카테고리 분류 |
| 1.4 | aggregateNodes() 구현 | ⏳ | 다중 노드 → 길이별 수량 합산, 셔터박스 집계 |
| 1.5 | build() 메인 메서드 구현 | ⏳ | 전체 조립 → BendingInfoExtended JSON 반환 |

### Phase 2: createProductionOrder 통합

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 2.1 | OrderService (라인 1111) WorkOrder::create에 options 추가 | ⏳ | ⚠️ 컨펌 필요 |
| 2.2 | 절곡 공정 감지 로직 추가 | ⏳ | Process 모델 조회 → process_name === '절곡' |

### Phase 3: 검증 및 테스트

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 3.1 | WO 74 데이터로 역검증 (order_id=43, 동일 입력 → 동일 출력) | ⏳ | |
| 3.2 | 프론트엔드 작업일지 정상 렌더링 확인 | ⏳ | BendingWorkLogContent |
| 3.3 | 비절곡 공정 WorkOrder에 bending_info 미생성 확인 | ⏳ | |

---

## 4. 작업 절차

### 4.1 단계별 절차

```
Phase 1: BendingInfoBuilder 서비스 생성
├── 1.1 파일 생성: api/app/Services/Production/BendingInfoBuilder.php
│   ├── 클래스: BendingInfoBuilder
│   └── 메서드: build(Order $order, int $processId): ?array
│
├── 1.2 product_code 파서 구현
│   ├── private parseProductCode(string $fullCode): array
│   ├── 입력: "FG-KSS02-벽면형-SUS"
│   └── 출력: ['productCode'=>'KSS02', 'guideType'=>'벽면형', 'finishMaterial'=>'SUS마감']
│
├── 1.3 BOM item_code 카테고리 분류기 구현
│   ├── private categorizeBomItem(array $bomItem): ?string
│   ├── 8개 패턴 매칭 (부록 A 참조)
│   └── 미매칭 → null 반환 (절곡 무관 품목)
│
├── 1.4 노드 집계 로직 구현
│   ├── private aggregateNodes(Collection $nodes): array
│   ├── height → 가이드레일 길이별 수량 (guideRailLengths)
│   ├── width → 셔터박스 길이별 수량 (shutterBoxLengths)
│   ├── BOM steel items → 카테고리별 수량 합산
│   └── 길이별 그룹핑 (동일 치수 노드는 수량 합산)
│
└── 1.5 build() 메인 메서드 조립
    ├── 절곡 공정 확인 → 아닌 경우 null 반환
    ├── parseProductCode → productCode, guideType, finishMaterial
    ├── aggregateNodes → 집계 데이터
    └── BendingInfoExtended 구조 JSON 조립 (부록 B 참조)

Phase 2: createProductionOrder 통합
├── 2.1 OrderService.php 수정 (라인 1111 부근)
│   ├── WorkOrder::create 호출 전 BendingInfoBuilder::build() 실행
│   ├── 절곡 공정인 경우에만 options 설정
│   └── 'options' => $bendingInfo ? ['bending_info' => $bendingInfo] : null
│
└── 2.2 Process 모델 사전 로드
    ├── 라인 1103 foreach 내에서 Process 조회
    └── 또는 $itemsByProcess에 process 정보 포함 (기존 로직 활용)

Phase 3: 검증
├── 3.1 order_id=43 (KSS02 벽면형 SUS 5개소 3560x4450) 데이터로 빌더 실행
│   ├── 기존 WO 74 bending_info와 구조 비교
│   └── productCode, guideRail, shutterBox, bottomBar, smokeBarrier 검증
│
├── 3.2 프론트엔드 렌더링 확인
│   ├── 새로 생성된 WorkOrder의 작업일지 열기
│   └── 4개 섹션 (가이드레일, 셔터박스, 하단마감재, 연기차단재) 정상 표시
│
└── 3.3 비절곡 공정 확인
    ├── 같은 수주에서 생성된 비절곡 WorkOrder의 options 확인
    └── bending_info가 없어야 함 (options: null 또는 bending_info 키 없음)
```

### 4.2 BendingInfoBuilder 서비스 설계

```php
// api/app/Services/Production/BendingInfoBuilder.php
namespace App\Services\Production;

use App\Models\Orders\Order;
use App\Models\Production\Process;
use Illuminate\Support\Collection;

class BendingInfoBuilder
{
    /**
     * 수주의 노드/BOM 데이터로 bending_info JSON 생성
     *
     * @param Order $order  수주 (rootNodes eager loaded)
     * @param int $processId  공정 ID (절곡 공정 확인용)
     * @return array|null  bending_info JSON 또는 null (절곡 아닌 경우)
     */
    public function build(Order $order, int $processId): ?array
    {
        // 1. 절곡 공정인지 확인
        $process = Process::find($processId);
        if (!$process || $process->process_name !== '절곡') {
            return null;
        }

        // 2. 루트 노드가 없으면 null
        $nodes = $order->rootNodes;
        if ($nodes->isEmpty()) {
            return null;
        }

        // 3. 첫 번째 루트 노드에서 product_code 추출
        $firstNode = $nodes->first();
        $productInfo = $this->parseProductCode(
            $firstNode->options['product_code'] ?? ''
        );

        // 4. product_code 파싱 실패 시 null
        if (empty($productInfo['productCode'])) {
            return null;
        }

        // 5. 모든 노드의 BOM items 수집 및 집계
        $aggregated = $this->aggregateNodes($nodes, $productInfo);

        // 6. bending_info 구조 조립 (부록 B 참조)
        return $this->assembleBendingInfo($productInfo, $aggregated, $nodes);
    }
}
```

### 4.3 product_code 파서

```php
/**
 * "FG-KSS02-벽면형-SUS" → ['productCode'=>'KSS02', 'guideType'=>'벽면형', 'finishMaterial'=>'SUS마감']
 */
private function parseProductCode(string $fullCode): array
{
    $parts = explode('-', $fullCode);

    // FG 접두사 제거
    if (($parts[0] ?? '') === 'FG') {
        array_shift($parts);
    }

    $finish = $parts[2] ?? 'EGI';

    return [
        'productCode' => $parts[0] ?? '',           // KSS02
        'guideType' => $parts[1] ?? '벽면형',        // 벽면형/측면형/혼합형
        'finishMaterial' => $finish === 'SUS' ? 'SUS마감' : 'EGI마감',
    ];
}
```

### 4.4 BOM item_code 카테고리 분류기

```php
/**
 * BOM 아이템을 카테고리별로 분류
 * 반환값: guideRail, shutterBox_case, shutterBox_finCover, bottomBar,
 *         smokeBarrier_rail, smokeBarrier_case, detail_lbar, detail_reinforce, null
 */
private function categorizeBomItem(array $bomItem): ?string
{
    $code = $bomItem['item_code'] ?? '';
    $name = $bomItem['item_name'] ?? '';

    if (str_starts_with($code, 'BD-가이드레일'))      return 'guideRail';
    if (str_starts_with($code, 'BD-케이스'))           return 'shutterBox_case';
    if (str_starts_with($code, 'BD-마구리'))           return 'shutterBox_finCover';
    if (str_contains($name, '하장바'))                 return 'bottomBar';
    if ($code === 'EST-SMOKE-레일용')                  return 'smokeBarrier_rail';
    if ($code === 'EST-SMOKE-케이스용')                return 'smokeBarrier_case';
    if (str_starts_with($code, 'BD-L-BAR'))           return 'detail_lbar';
    if (str_starts_with($code, 'BD-보강평철'))         return 'detail_reinforce';

    return null; // 절곡 무관 품목 (parts, motor 등)
}
```

### 4.5 createProductionOrder 변경 포인트

```php
// OrderService.php 라인 1103~1130 (수정 부분)

foreach ($itemsByProcess as $key => $group) {
    $processId = $group['processId'];
    $workOrderNo = $this->generateWorkOrderNo($tenantId, $order->id, $processId);

    // ★ 신규: 절곡 공정이면 bending_info 생성
    $options = null;
    if ($processId) {
        $bendingInfoBuilder = app(BendingInfoBuilder::class);
        $bendingInfo = $bendingInfoBuilder->build($order, $processId);
        if ($bendingInfo) {
            $options = ['bending_info' => $bendingInfo];
        }
    }

    $workOrder = WorkOrder::create([
        'tenant_id' => $tenantId,
        'work_order_no' => $workOrderNo,
        'sales_order_id' => $order->id,
        'project_name' => $order->order_no,
        'process_id' => $processId,
        'status' => WorkOrder::STATUS_PENDING,
        'assignee_id' => $data['assignee_id'] ?? null,
        'team_id' => $data['team_id'] ?? null,
        'scheduled_date' => $data['scheduled_date'] ?? null,
        'memo' => $data['memo'] ?? null,
        'options' => $options,                              // ★ 신규
        'is_active' => true,
        'created_by' => apiUserId(),
        'updated_by' => apiUserId(),
    ]);

    // ... 기존 work_order_items INSERT 로직 유지
}
```

---

## 5. 컨펌 대기 목록

| # | 항목 | 변경 내용 | 영향 범위 | 상태 |
|---|------|----------|----------|------|
| 1 | OrderService 수정 | createProductionOrder 라인 1111에 options 추가 | 생산지시 생성 전체 | ⚠️ 컨펌 필요 |
| 2 | item_code 패턴 매핑 | BD-*, EST-SMOKE-*, 하장바 패턴으로 카테고리 분류 | 절곡 BOM 품목 인식 | ⚠️ 컨펌 필요 |
| 3 | product_code 파싱 | FG-{code}-{type}-{finish} 4세그먼트 패턴 가정 | 모든 절곡 제품 | ⚠️ 컨펌 필요 |

---

## 6. 변경 이력

| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|------|------|----------|------|------|
| 2026-02-19 | - | 문서 초안 작성 | - | - |
| 2026-02-20 | - | formula-engine-real-data-plan.md 형식으로 전면 개편 (현황 분석, 코드 위치, 부록 추가) | - | - |

---

## 7. 참고 문서

- **현재 bending_info 구조**: `react/src/components/production/WorkOrders/documents/bending/types.ts` (라인 32~68)
- **재질 매핑 로직**: `react/src/components/production/WorkOrders/documents/bending/utils.ts` (라인 77~108)
- **생산지시 서비스**: `api/app/Services/OrderService.php` (createProductionOrder, 라인 959~1214)
- **WorkOrder 서비스**: `api/app/Services/WorkOrderService.php` (store, 라인 238~323)
- **WorkOrder 모델**: `api/app/Models/Production/WorkOrder.php`
- **Order 모델**: `api/app/Models/Orders/Order.php` (rootNodes, 라인 172~178)
- **레거시 참고**: `5130/output/proc/viewBendingWork_slat.php`
- **WO 74 실데이터**: order_id=43, order_nodes id=116~125 (KSS02 벽면형 SUS, 3560x4450)

---

## 8. 관련 파일 및 코드 위치

### 8.1 API (api/) - 핵심 코드 위치

| 파일 | 메서드/요소 | 라인 | 역할 |
|------|------------|------|------|
| `Services/OrderService.php` | `createProductionOrder()` | 959 | 메인 엔트리 (수주→생산지시) |
| 같은 파일 | `Order::with(['items', 'rootNodes'])` | 966 | 수주 + 루트노드 로드 |
| 같은 파일 | `$itemsByProcess` 그룹핑 | 1035~1089 | 공정별 아이템 분류 (3단계 fallback) |
| 같은 파일 | `foreach ($itemsByProcess)` | 1103 | **공정별 WorkOrder 생성 루프** |
| 같은 파일 | `WorkOrder::create([...])` | 1111~1124 | **★ 변경 포인트: options 추가** |
| 같은 파일 | `$woItemOptions` 구성 | 1172~1181 | work_order_items.options 조립 |
| 같은 파일 | `'bending_info' => $nodeOptions['bending_info'] ?? null` | 1179 | items 레벨 bending_info (기존, 유지) |
| 같은 파일 | `DB::table('work_order_items')->insert()` | 1183~1197 | items INSERT |
| 같은 파일 | `$order->status_code = Order::STATUS_IN_PROGRESS` | 1204 | 수주 상태 변경 |
| 같은 파일 | `generateWorkOrderNo()` | 1270 | 작업지시 번호 생성 |
| `Services/WorkOrderService.php` | `store()` | 238 | 대체 생성 경로 (수동 생성용) |
| 같은 파일 | `'bending_info' => $nodeOptions['bending_info'] ?? null` | 279 | items 레벨 bending_info (유지) |
| 같은 파일 | `$workOrder->isBending()` | 306 | 절곡 공정 확인 |
| **신규** `Services/Production/BendingInfoBuilder.php` | `build()` | - | **Phase 1에서 생성** |
| `Models/Production/WorkOrder.php` | `$fillable` (options 포함) | 32~51 | options 필드 (라인 47) |
| 같은 파일 | `$casts` (options => json) | 53~60 | JSON 자동 변환 (라인 59) |
| 같은 파일 | `isBending()` | 342~345 | `process.process_name === '절곡'` |
| 같은 파일 | `process()` 관계 | 139~144 | `belongsTo(Process::class)` |
| 같은 파일 | `PROCESS_BENDING` (deprecated) | 80 | 상수 (미사용, FK 방식으로 전환됨) |
| `Models/Orders/Order.php` | `rootNodes()` | 172~178 | `hasMany(OrderNode)->whereNull('parent_id')` |

### 8.2 React (react/) - 프론트엔드 코드 위치

| 파일 | 요소 | 라인 | 역할 |
|------|------|------|------|
| `types.ts` (WorkOrders/) | `WorkOrderApi.options` | 343 | `options?: { bending_info?: Record<string, unknown> }` |
| 같은 파일 | `transformApiToFrontend()` | 495 | `bendingInfo: api.options?.bending_info \|\| undefined` |
| 같은 파일 | item 레벨 bendingInfo (deprecated) | 487 | `bendingInfo: undefined` (명시적 무시) |
| 같은 파일 | `WorkOrder.bendingInfo` | 210 | 프론트 모델 필드 정의 |
| `bending/types.ts` | `BendingInfoExtended` | 32~68 | **목표 JSON 스키마** |
| 같은 파일 | `GuideRailTypeData` | 5~13 | 가이드레일 타입 데이터 |
| 같은 파일 | `ShutterBoxData` | 15~22 | 셔터박스 데이터 |
| 같은 파일 | `LengthQuantity` | 24~27 | 길이-수량 쌍 |
| `bending/utils.ts` | `getMaterialMapping()` | 77~108 | productCode → 재질 매핑 |

### 8.3 DB 테이블

#### work_orders 테이블 (변경 대상)

| 컬럼 | 타입 | NULL | 설명 |
|------|------|------|------|
| id | bigint unsigned | NO | PK |
| tenant_id | bigint unsigned | NO | 테넌트 |
| work_order_no | varchar(50) | NO | 작업지시 번호 |
| sales_order_id | bigint unsigned | YES | 수주 FK |
| process_id | bigint unsigned | YES | 공정 FK |
| **options** | **json** | **YES** | **★ bending_info 저장 대상** |
| status | varchar(20) | NO | 상태 |
| ... | ... | ... | (기타 필드) |

#### order_nodes 테이블 (입력 소스)

| 컬럼 | 타입 | NULL | 설명 |
|------|------|------|------|
| id | bigint unsigned | NO | PK |
| order_id | bigint unsigned | NO | 수주 FK |
| parent_id | bigint unsigned | YES | 부모 노드 (root=NULL) |
| options | json | YES | **product_code, width, height, bom_result** |
| sort_order | int | NO | 정렬 |
| quantity | int | NO | 수량 (기본 1) |

#### processes 테이블 (공정 판별)

| 컬럼 | 타입 | NULL | 설명 |
|------|------|------|------|
| id | bigint unsigned | NO | PK |
| tenant_id | bigint unsigned | NO | 테넌트 |
| process_name | varchar(100) | NO | 공정명 ('절곡', '스크린', '슬랫' 등) |

---

## 9. 검증 결과

### 9.1 테스트 케이스

| 입력값 | 예상 결과 | 실제 결과 | 상태 |
|--------|----------|----------|------|
| order_id=43 (KSS02 벽면형 SUS 5개소 3560x4450) | productCode="KSS02", guideRail.wall 5개, shutterBox 1개 | - | ⏳ |
| 절곡 공정이 아닌 WorkOrder | bending_info = null, options = null | - | ⏳ |
| product_code 없는 노드 | graceful fallback (null 반환) | - | ⏳ |
| 혼합형 제품 (벽면+측면) | guideRail.wall + guideRail.side 둘 다 생성 | - | ⏳ |
| 동일 치수 복수 노드 | 수량 합산 (길이별 그룹핑) | - | ⏳ |
| BOM에 steel 외 category | 무시 (null → 스킵) | - | ⏳ |

### 9.2 성공 기준 달성 현황

| 기준 | 달성 | 비고 |
|------|------|------|
| 생산지시 시 절곡 WorkOrder에 bending_info 자동 생성 | ⏳ | |
| WO 74 수동 데이터와 동일 구조의 JSON 생성 | ⏳ | |
| 프론트엔드 BendingWorkLogContent에서 정상 렌더링 | ⏳ | |
| 비절곡 공정 WorkOrder에 bending_info 미생성 | ⏳ | |
| product_code 파싱 실패 시 graceful null 반환 | ⏳ | |

---

## 부록 A. BOM item_code → bending_info 카테고리 전체 매핑

### A.1 패턴 매칭 규칙 (우선순위 순)

| 순서 | 매칭 방식 | 패턴 | 카테고리 | bending_info 필드 |
|------|----------|------|---------|------------------|
| 1 | str_starts_with(code) | `BD-가이드레일-*` | guideRail | `guideRail.wall` 또는 `guideRail.side` |
| 2 | str_starts_with(code) | `BD-케이스-*` | shutterBox_case | `shutterBox[].size` |
| 3 | str_starts_with(code) | `BD-마구리-*` | shutterBox_finCover | `shutterBox[].finCoverQty` |
| 4 | str_contains(name) | `*하장바*` | bottomBar | `bottomBar.length3000Qty/length4000Qty` |
| 5 | exact match(code) | `EST-SMOKE-레일용` | smokeBarrier_rail | `smokeBarrier.w50[]` |
| 6 | exact match(code) | `EST-SMOKE-케이스용` | smokeBarrier_case | `smokeBarrier.w80Qty` |
| 7 | str_starts_with(code) | `BD-L-BAR-*` | detail_lbar | `detailParts[]` |
| 8 | str_starts_with(code) | `BD-보강평철-*` | detail_reinforce | `detailParts[]` |
| - | 미매칭 | (기타) | null | 무시 (절곡 무관) |

### A.2 가이드레일 item_code 파싱

`BD-가이드레일-KSS02-SUS-120*70` → 세그먼트 분리:

| 세그먼트 | 값 | 용도 |
|----------|-----|------|
| BD-가이드레일 | 접두사 | 카테고리 식별 |
| KSS02 | 모델코드 | (검증용) |
| SUS | 마감재 | (검증용) |
| 120*70 | baseSize | `guideRail.wall.baseSize` 또는 `guideRail.side.baseSize` |

### A.3 셔터박스 item_code 파싱

케이스: `BD-케이스-500*380` → `shutterBox[].size = "500*380"`
마구리: `BD-마구리-505*385` → `shutterBox[].finCoverQty += BOM quantity`

### A.4 하장바 item_name 파싱

`철재용하장바(SUS)3000` → item_name 마지막 4자리 숫자 추출 → 3000/4000 분류
- 3000 → `bottomBar.length3000Qty += BOM quantity × 노드수`
- 4000 → `bottomBar.length4000Qty += BOM quantity × 노드수`

---

## 부록 B. bending_info JSON 조립 상세

### B.1 목표 출력 구조 (WO 74 실데이터 기준)

```json
{
  "productCode": "KSS02",
  "finishMaterial": "SUS마감",
  "common": {
    "kind": "벽면형 120X70",
    "type": "벽면형(120*70)",
    "lengthQuantities": [
      { "length": 4450, "quantity": 5 }
    ]
  },
  "detailParts": [
    { "partName": "엘바", "material": "EGI 1.6T", "barcyInfo": "16 I 75" },
    { "partName": "보강평철", "material": "50T", "barcyInfo": "" }
  ],
  "guideRail": {
    "wall": {
      "baseSize": "120*70",
      "finish": "SUS 1.2T",
      "extraFinish": "",
      "lengthQuantities": [
        { "length": 4450, "quantity": 5 }
      ]
    },
    "side": null
  },
  "bottomBar": {
    "material": "SUS 1.5T",
    "extraFinish": "없음",
    "length3000Qty": 17,
    "length4000Qty": 0
  },
  "shutterBox": [
    {
      "direction": "양면",
      "size": "500*380",
      "finCoverQty": 5,
      "lengths": [
        { "length": 3560, "quantity": 5 }
      ]
    }
  ],
  "smokeBarrier": {
    "w50": [
      { "length": 4450, "quantity": 5 }
    ],
    "w80Qty": 5
  }
}
```

### B.2 조립 규칙 (필드별)

| 필드 | 소스 | 변환 규칙 |
|------|------|----------|
| `productCode` | parseProductCode(product_code)[0] | "KSS02" |
| `finishMaterial` | parseProductCode(product_code)[2] | "SUS" → "SUS마감" |
| `common.kind` | guideType + baseSize | "벽면형 120X70" |
| `common.type` | guideType + "(baseSize)" | "벽면형(120*70)" |
| `common.lengthQuantities` | 노드 height별 수량 집계 | [{length: 4450, quantity: 5}] |
| `guideRail.wall/side` | guideType으로 분기 + getMaterialMapping | baseSize, finish, lengthQuantities |
| `bottomBar.material` | getMaterialMapping.bottomBarFinish | "SUS 1.5T" |
| `bottomBar.extraFinish` | getMaterialMapping.bottomBarExtraFinish | "없음" |
| `bottomBar.length3000Qty` | 하장바 BOM item_name → 3000 수량 합산 | 17 (= 3.4 × 5) |
| `shutterBox[].direction` | 기본 "양면" (방향 정보 없음) | "양면" |
| `shutterBox[].size` | BD-케이스 item_code → 사이즈 추출 | "500*380" |
| `shutterBox[].finCoverQty` | BD-마구리 BOM quantity × 노드수 | 5 |
| `shutterBox[].lengths` | 노드 width별 수량 집계 | [{length: 3560, quantity: 5}] |
| `smokeBarrier.w50` | EST-SMOKE-레일용 수량 → height 기준 집계 | [{length: 4450, quantity: 5}] |
| `smokeBarrier.w80Qty` | EST-SMOKE-케이스용 수량 합산 → 노드수 | 5 |

### B.3 detailParts 매핑

| BOM item_code 패턴 | partName | material 결정 방식 | barcyInfo |
|--------------------|----------|-------------------|-----------|
| `BD-L-BAR-{model}-{W}*{H}` | "엘바" | "{H}T" 에서 추출 (e.g., 17*60 → "EGI 1.6T") | "{H/10} I {W}" (e.g., "16 I 75") |
| `BD-보강평철-{size}` | "보강평철" | "{size}T" (e.g., 50 → "50T") | "" |

> detailParts의 정확한 material/barcyInfo 계산은 레거시 코드 참조 필요.
> Phase 1 구현 시 WO 74 실데이터와 비교하여 확정.

---

## 부록 C. 코드 변경 포인트 상세

### C.1 OrderService.php 변경 (Phase 2.1)

**파일**: `api/app/Services/OrderService.php`
**위치**: 라인 1103~1130 (`foreach ($itemsByProcess)` 내부)

```php
// 변경 전 (라인 1111~1124):
$workOrder = WorkOrder::create([
    'tenant_id' => $tenantId,
    'work_order_no' => $workOrderNo,
    'sales_order_id' => $order->id,
    'project_name' => $order->order_no,
    'process_id' => $processId,
    'status' => WorkOrder::STATUS_PENDING,
    'assignee_id' => $data['assignee_id'] ?? null,
    'team_id' => $data['team_id'] ?? null,
    'scheduled_date' => $data['scheduled_date'] ?? null,
    'memo' => $data['memo'] ?? null,
    // ⚠️ 'options' 없음
    'is_active' => true,
    'created_by' => apiUserId(),
    'updated_by' => apiUserId(),
]);

// 변경 후:
// ★ 절곡 공정이면 bending_info 생성
$options = null;
if ($processId) {
    $bendingInfo = app(BendingInfoBuilder::class)->build($order, $processId);
    if ($bendingInfo) {
        $options = ['bending_info' => $bendingInfo];
    }
}

$workOrder = WorkOrder::create([
    'tenant_id' => $tenantId,
    'work_order_no' => $workOrderNo,
    'sales_order_id' => $order->id,
    'project_name' => $order->order_no,
    'process_id' => $processId,
    'status' => WorkOrder::STATUS_PENDING,
    'assignee_id' => $data['assignee_id'] ?? null,
    'team_id' => $data['team_id'] ?? null,
    'scheduled_date' => $data['scheduled_date'] ?? null,
    'memo' => $data['memo'] ?? null,
    'options' => $options,                              // ★ 신규
    'is_active' => true,
    'created_by' => apiUserId(),
    'updated_by' => apiUserId(),
]);
```

### C.2 BendingInfoBuilder 신규 생성 (Phase 1)

**파일**: `api/app/Services/Production/BendingInfoBuilder.php` (신규)
**예상 코드 라인 수**: 200~250줄

```
메서드 목록:
├── public build(Order $order, int $processId): ?array          (메인 엔트리)
├── private parseProductCode(string $fullCode): array           (product_code 파싱)
├── private categorizeBomItem(array $bomItem): ?string          (BOM 카테고리 분류)
├── private aggregateNodes(Collection $nodes, array $productInfo): array  (노드 집계)
├── private assembleBendingInfo(array $productInfo, array $aggregated, Collection $nodes): array  (JSON 조립)
├── private getMaterialMapping(string $productCode, string $finishMaterial): array  (재질 매핑)
├── private extractBaseSize(string $guideRailCode): string      (가이드레일 baseSize 추출)
└── private extractBottomBarLength(string $itemName): int       (하장바 길이 추출)
```

### C.3 use 문 추가 (OrderService.php)

**파일**: `api/app/Services/OrderService.php`
**위치**: 파일 상단 use 섹션

```php
use App\Services\Production\BendingInfoBuilder;
```

---

## 부록 D. 가이드레일 baseSize 규칙

### D.1 모델별 baseSize 매핑

| 모델 | guideType | BD 품목 코드 예시 | baseSize |
|------|-----------|-----------------|----------|
| KSS01 | 벽면형 | BD-가이드레일-KSS01-SUS-120*70 | 120*70 |
| KSS01 | 측면형 | BD-가이드레일-KSS01-SUS-120*120 | 120*120 |
| KSS02 | 벽면형 | BD-가이드레일-KSS02-SUS-120*70 | 120*70 |
| KSS02 | 측면형 | BD-가이드레일-KSS02-SUS-120*120 | 120*120 |
| KSE01 | 벽면형 | BD-가이드레일-KSE01-{SUS/EGI}-120*70 | 120*70 |
| KSE01 | 측면형 | BD-가이드레일-KSE01-{SUS/EGI}-120*120 | 120*120 |
| KWE01 | 벽면형 | BD-가이드레일-KWE01-{SUS/EGI}-120*70 | 120*70 |
| KWE01 | 측면형 | BD-가이드레일-KWE01-{SUS/EGI}-120*120 | 120*120 |
| KQTS01 | 벽면형 | BD-가이드레일-KQTS01-SUS-130*75 | 130*75 |
| KQTS01 | 측면형 | BD-가이드레일-KQTS01-SUS-130*125 | 130*125 |
| KTE01 | 벽면형 | BD-가이드레일-KTE01-{SUS/EGI}-130*75 | 130*75 |
| KTE01 | 측면형 | BD-가이드레일-KTE01-{SUS/EGI}-130*125 | 130*125 |
| KDSS01 | 벽면형 | BD-가이드레일-KDSS01-SUS-150*150 | 150*150 |
| KDSS01 | 측면형 | BD-가이드레일-KDSS01-SUS-150*212 | 150*212 |

### D.2 혼합형 처리

혼합형(guideType === '혼합형')인 경우:
- `guideRail.wall` = 해당 모델의 벽면형 baseSize
- `guideRail.side` = 해당 모델의 측면형 baseSize
- BOM에 두 종류 가이드레일이 모두 포함됨

> baseSize는 BOM의 `BD-가이드레일-*` item_code에서 마지막 세그먼트로 직접 추출 가능.
> 별도 매핑 테이블 불필요.

---

## 부록 E. 셔터박스/하단마감재/연기차단재 규칙

### E.1 셔터박스 방향 결정

| 조건 | direction 값 |
|------|-------------|
| 노드 1개 | "양면" (기본값) |
| 여러 노드 + 동일 치수 | "양면" (기본값) |
| 방향 정보 없음 (현재) | "양면" 기본값 사용 |

> 방향 정보는 현재 order_nodes.options에 저장되지 않음.
> Phase 1에서는 "양면" 기본값 사용. 추후 BOM 확장 시 방향 필드 추가 가능.

### E.2 하단마감재 길이 분류

| BOM item_name | 길이 추출 방법 | 분류 |
|---------------|--------------|------|
| 철재용하장바(SUS)3000 | 마지막 4자리 숫자 → 3000 | `bottomBar.length3000Qty` |
| 철재용하장바(SUS)4000 | 마지막 4자리 숫자 → 4000 | `bottomBar.length4000Qty` |
| 철재용하장바(EGI)3000 | 마지막 4자리 숫자 → 3000 | `bottomBar.length3000Qty` |

계산: `BOM quantity × 노드 수 = 총 수량` (예: 3.4 × 5개소 = 17)

### E.3 연기차단재 수량 계산

| BOM item_code | bending_info 필드 | 수량 계산 |
|---------------|------------------|----------|
| EST-SMOKE-레일용 | `smokeBarrier.w50[]` | height 기준 길이별 수량 집계 |
| EST-SMOKE-케이스용 | `smokeBarrier.w80Qty` | BOM quantity × 노드수 → 정수 변환 |

### E.4 재질 매핑 (getMaterialMapping 재현)

```php
private function getMaterialMapping(string $productCode, string $finishMaterial): array
{
    // Group 1: SUS 전용 (KQTS01, KSS01, KSS02)
    if (in_array($productCode, ['KQTS01', 'KSS01', 'KSS02'])) {
        return [
            'guideRailFinish' => 'SUS 1.2T',
            'bodyMaterial' => 'EGI 1.55T',
            'guideRailExtraFinish' => '',
            'bottomBarFinish' => 'SUS 1.5T',
            'bottomBarExtraFinish' => '없음',
        ];
    }

    // Group 2: KTE01 (마감유형 분기)
    if ($productCode === 'KTE01') {
        $isSUS = $finishMaterial === 'SUS마감';
        return [
            'guideRailFinish' => 'EGI 1.55T',
            'bodyMaterial' => 'EGI 1.55T',
            'guideRailExtraFinish' => $isSUS ? 'SUS 1.2T' : '',
            'bottomBarFinish' => 'EGI 1.55T',
            'bottomBarExtraFinish' => $isSUS ? 'SUS 1.2T' : '없음',
        ];
    }

    // Group 3: 기타 (KSE01, KWE01 등)
    $isSUS = str_contains($finishMaterial, 'SUS');
    return [
        'guideRailFinish' => 'EGI 1.55T',
        'bodyMaterial' => 'EGI 1.55T',
        'guideRailExtraFinish' => $isSUS ? 'SUS 1.2T' : '',
        'bottomBarFinish' => 'EGI 1.55T',
        'bottomBarExtraFinish' => $isSUS ? 'SUS 1.2T' : '없음',
    ];
}
```

---

## 부록 F. WO 74 역검증용 데이터

### F.1 입력 데이터 (order_id=43)

| 항목 | 값 |
|------|-----|
| 수주 ID | 43 |
| root_nodes | id=116~125 (10개, 5개소 × 2) |
| product_code | FG-KSS02-벽면형-SUS |
| width | 3560 |
| height | 4450 |
| 노드 수 | 5 (동일 치수) |

### F.2 기대 출력 (WO 74 기존 데이터와 일치해야 함)

| 필드 | 기대값 |
|------|--------|
| productCode | "KSS02" |
| finishMaterial | "SUS마감" |
| common.type | "벽면형(120*70)" |
| common.lengthQuantities | [{length: 4450, quantity: 5}] |
| guideRail.wall.baseSize | "120*70" |
| guideRail.wall.finish | "SUS 1.2T" |
| guideRail.wall.lengthQuantities | [{length: 4450, quantity: 5}] |
| guideRail.side | null |
| bottomBar.material | "SUS 1.5T" |
| bottomBar.length3000Qty | 17 (= 3.4 × 5) |
| bottomBar.length4000Qty | 0 |
| shutterBox[0].direction | "양면" |
| shutterBox[0].size | "500*380" |
| shutterBox[0].finCoverQty | 5 |
| shutterBox[0].lengths | [{length: 3560, quantity: 5}] |
| smokeBarrier.w50 | [{length: 4450, quantity: 5}] |
| smokeBarrier.w80Qty | 5 |

---

## 10. 자기완결성 점검 결과

### 10.1 체크리스트 검증

| # | 검증 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1 | 작업 목적이 명확한가? | ✅ | 1.1 배경 |
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.2 |
| 3 | 작업 범위가 구체적인가? | ✅ | 3 Phase + 부록 |
| 4 | 의존성이 명시되어 있는가? | ✅ | Phase 순서 = 의존성 |
| 5 | 참고 파일 경로 + 라인번호가 정확한가? | ✅ | 섹션 8 + 부록 C |
| 6 | 단계별 절차가 실행 가능한가? | ✅ | 4.1~4.5 (코드 포함) |
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9 + 부록 F |
| 8 | 모호한 표현이 없는가? | ✅ | 구체적 코드/건수/라인번호 |

### 10.2 새 세션 시뮬레이션 테스트

| 질문 | 답변 가능 | 참조 섹션 |
|------|:--------:|----------|
| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 |
| Q2. 어디서부터 시작해야 하는가? | ✅ | 3 Phase 1, 4.1 단계별 절차 |
| Q3. OrderService의 어느 줄을 수정해야 하는가? | ✅ | 8.1 코드 위치 (라인 1111), 부록 C.1 |
| Q4. BOM item_code 매핑 규칙은? | ✅ | 2.6 + 부록 A |
| Q5. product_code 파싱 방법은? | ✅ | 2.7 + 4.3 (코드 포함) |
| Q6. 프론트엔드 목표 스키마는? | ✅ | 2.5 BendingInfoExtended + 부록 B |
| Q7. 재질 매핑 규칙은? | ✅ | 2.8 + 부록 E.4 (코드 포함) |
| Q8. 어떻게 검증하는가? | ✅ | 9.1 테스트 케이스 + 부록 F |
| Q9. 가이드레일 baseSize는 어떻게 결정하는가? | ✅ | 부록 D (모델별 전체 매핑) |
| Q10. 기존 코드에 미치는 영향은? | ✅ | 1.3 원칙 6번, 부록 C (변경 포인트 상세) |

**결과**: 10/10 통과 → ✅ 자기완결성 확보

### 10.3 보완 이력

| 날짜 | 항목 | 원본 | 보완 내용 |
|------|------|------|----------|
| 2026-02-20 | 전체 | 초안 (간략 구조) | formula-engine-real-data-plan.md 형식으로 전면 개편 |
| 2026-02-20 | 섹션 2 | 미존재 | 현황 분석 추가 (DB 데이터, 코드 분석, 스키마 상세) |
| 2026-02-20 | 섹션 8 | 간략 목록 | 관련 파일 및 코드 위치 (정확한 라인번호 포함) |
| 2026-02-20 | 부록 A~F | 일부만 존재 | 6개 부록 완비 (BOM 매핑, JSON 조립, 코드 변경, 가이드레일, 셔터박스/하단마감재, 역검증) |

---

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