- 개발팀 전용 폴더 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>
1047 lines
43 KiB
Markdown
1047 lines
43 KiB
Markdown
# 생산지시 시 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 스킬로 생성되었습니다.*
|