Files
sam-docs/dev/dev_plans/bending-info-auto-generation-plan.md
권혁성 db63fcff85 refactor: [docs] 팀별 폴더 구조 재편 (공유/개발/프론트/기획)
- 개발팀 전용 폴더 dev/ 생성 (standards, guides, quickstart, changes, deploys, data, history, dev_plans 이동)
- 프론트엔드 전용 폴더 frontend/ 생성 (api/ → frontend/api-specs/)
- 기획팀 폴더 requests/ 생성
- plans/ → dev/dev_plans/ 이름 변경
- README.md 신규 (사람용 안내), INDEX.md 재작성 (Claude Code용)
- resources.md 신규 (노션 링크용, assets/brochure 이관 예정)
- CURRENT_WORKS.md 삭제, TODO.md → dev/ 이동
- 전체 참조 경로 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 16:46:03 +09:00

1047 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 생산지시 시 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 스킬로 생성되었습니다.*