From 3fff99095e37385aa0275a4bea0aed7db6aaec83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Sun, 22 Feb 2026 03:05:10 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EC=A0=88=EA=B3=A1/=ED=92=88=EB=AA=A9?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=8B=A0=EA=B7=9C=20=EA=B3=84=ED=9A=8D?= =?UTF-8?q?=20=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 절곡 정보 자동 생성 계획 (bending-info-auto-generation) - 절곡 자재투입 매핑 GAP 분석 (bending-material-input-mapping) - FG 코드 통합 계획 (fg-code-consolidation) - 품목 재고 관리 계획 (item-inventory-management) Co-Authored-By: Claude Opus 4.6 --- plans/bending-info-auto-generation-plan.md | 1046 ++++++++++++++++++ plans/bending-material-input-mapping-plan.md | 692 ++++++++++++ plans/fg-code-consolidation-plan.md | 754 +++++++++++++ plans/item-inventory-management-plan.md | 167 +++ 4 files changed, 2659 insertions(+) create mode 100644 plans/bending-info-auto-generation-plan.md create mode 100644 plans/bending-material-input-mapping-plan.md create mode 100644 plans/fg-code-consolidation-plan.md create mode 100644 plans/item-inventory-management-plan.md diff --git a/plans/bending-info-auto-generation-plan.md b/plans/bending-info-auto-generation-plan.md new file mode 100644 index 0000000..d9e5ec0 --- /dev/null +++ b/plans/bending-info-auto-generation-plan.md @@ -0,0 +1,1046 @@ +# 생산지시 시 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 }` | +| 같은 파일 | `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 스킬로 생성되었습니다.* diff --git a/plans/bending-material-input-mapping-plan.md b/plans/bending-material-input-mapping-plan.md new file mode 100644 index 0000000..926d4a2 --- /dev/null +++ b/plans/bending-material-input-mapping-plan.md @@ -0,0 +1,692 @@ +# 절곡 세부품목 → 자재투입 → LOT 매핑 통합 개발 계획 + +> **작성일**: 2026-02-21 +> **목적**: 절곡 작업일지의 4대 제품 카테고리(가이드레일/하단마감재/셔터박스/연기차단재) 세부품목을 items 테이블과 연동하고, BOM 기반 자재투입 → LOT 추적 파이프라인 구축 +> **기준 문서**: `5130/output/viewBendingWork_UA.php`, `api/app/Services/Production/BendingInfoBuilder.php`, `docs/plans/bending-preproduction-stock-plan.md` +> **상태**: 📋 분석 완료, 개발 계획 수립 중 + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | LOT 추적 데이터 누락 분석 (7개 GAP 발견, 조치 계획 수립) | +| **다음 작업** | GAP 1 즉시 수정 (registerMaterialInput 통일) → 방안 B 구현 | +| **진행률** | 분석 완료, GAP 해결 및 개발 착수 전 | +| **마지막 업데이트** | 2026-02-22 | + +--- + +## 1. 개요 + +### 1.1 배경 + +절곡 작업일지(WorkerScreen)에는 4대 제품 카테고리가 표시되며, 각 카테고리별 세부품목에 LOT 번호를 입력하여 자재를 투입해야 한다. + +``` +작업일지 (절곡 WO202602210027) +├── 1. 가이드레일 (세부: 마감재, 본체, C형, D형, 하부BASE) +├── 2. 하단마감재 (세부: 하단마감재, 보강엘바, 보강평철, 별도마감) +├── 3. 셔터박스 (세부: 전면부, 린텔부, 점검구, 후면부, 상부덮개, 마구리) +└── 4. 연기차단재 (세부: 레일용 W50, 케이스용 W80) +``` + +현재 상태: +- **구현 완료**: BendingInfoBuilder(bending_info 자동생성), Items Master(BD-XX-XX 품목 등록), getMaterials API, 자재투입/LOT 연동 API +- **미구현(핵심 Gap)**: 세부품목이 items 테이블의 BOM으로 연결되지 않아 자재투입 시 세부품목별 LOT 매핑 불가 + +### 1.2 핵심 문제 + +``` +현재 흐름 (불완전): + 견적 → bom_result에 부모 품목 저장 (BD-가이드레일-KSS01-SUS-120*70, qty=8.5m) + → 작업지시 → BendingInfoBuilder가 길이 버킷팅 (4300mm×1, 4000mm×1) + → work_order_items에 부모 품목 등록 + → getMaterials() 호출 시 item.bom이 null + → fallback: 부모 품목 자체를 자재로 표시 (1건) + → 세부품목(BD-RS-43, BD-RM-40 등) LOT 매핑 불가 + +목표 흐름 (방안 B 채택): + 견적 → bom_result에 부모 품목 저장 (기존 그대로, 수정 불필요) + → 작업지시 생성 시 BendingInfoBuilder 확장: + 길이 버킷팅 결과로 BD-XX-NN 세부품목 조회 → 동적 BOM 생성 + → work_order_items.options.dynamic_bom에 세부품목 저장 + → getMaterials()에서 dynamic_bom 우선 사용 + → 각 세부품목별 StockLot 조회 → LOT 입력 → 자재투입 완료 +``` + +### 1.3 성공 기준 + +| 기준 | 측정 방법 | +|------|----------| +| 작업일지의 4대 카테고리 세부품목이 items와 1:1 매핑 | 각 세부품목의 item_id 존재 확인 | +| 자재투입 화면에서 세부품목별 LOT 입력 가능 | getMaterials API가 세부품목 리스트 반환 | +| LOT 번호 입력 시 재고 차감 정상 동작 | stock_transactions 기록 확인 | +| 레거시 5130과 동일한 LOT prefix 체계 유지 | LOT prefix 코드 일치 검증 | + +--- + +## 2. 레거시 5130 절곡품 체계 분석 + +### 2.1 제품코드 시스템 + +> **참고**: 제품코드는 작업일지 4대 카테고리(가이드레일/하단마감재/셔터박스/연기차단재)와 별개 개념. +> 제품코드는 스크린/철재 × SUS/EGI 조합에 의한 **제품 모델 구분**이며, 각 모델별로 전개치수가 다르다. + +| 제품코드 | 마감재질 | 설명 | +|---------|---------|------| +| KSS01 | SUS 1.2T (기본) | 스크린 SUS | +| KSS02 | SUS 1.2T | 스크린 SUS (변형) | +| KSE01 | EGI 1.55T (기본) + 옵션 SUS | 스크린 EGI (표준) | +| KWE01 | EGI 1.55T (기본) + 옵션 SUS | 스크린 EGI (광폭) | +| KTE01 | EGI/SUS | 철재 | +| KDSS01 | SUS | 디딤형 SUS | +| KQTS01 | SUS | 특수형 | + +**마감재질 결정 로직** (`5130/output/viewBendingWork_UA.php:317-355`): +``` +KSS01/KSS02 → GuidrailFinish = SUS 1.2T, bodyMaterial = EGI 1.55T +KSE01/KWE01 + SUS마감 → GuidrailFinish = EGI 1.55T, GuidrailExtraFinish = SUS 1.2T +KSE01/KWE01 + EGI마감 → GuidrailFinish = EGI 1.55T, GuidrailExtraFinish = EGI 1.55T +``` + +### 2.2 LOT Prefix 전체 맵 + +#### 2.2.1 가이드레일 (Guide Rail) + +**벽면형 (Wall type, 412*350)** + +| 세부품목 | KSS01 prefix | KSE01/KWE01 EGI prefix | KSE01/KWE01 SUS prefix | +|---------|-------------|----------------------|----------------------| +| ①마감재 | RS | RE | RE | +| ②본체 | RM | RM | RM | +| ③C형 | RC | RC | RC | +| ④D형 | RD | RD | RD | +| ⑤별도마감 | - | - | YY | +| 하부BASE | XX | XX | XX | + +**측면형 (Side type, 120*120)** + +| 세부품목 | KSS01 prefix | KSE01/KWE01 EGI prefix | KSE01/KWE01 SUS prefix | +|---------|-------------|----------------------|----------------------| +| ①②마감재 | SS | SE | SE | +| ③본체 | SM | SM | SM | +| ④본체디딤 | SC | SC | SC | +| ⑤C형 | SD | SD | SD | +| ⑥D형 | SM | SM | SM | +| ⑦⑧별도마감 | - | - | YY | +| 하부BASE | XX | XX | XX | + +#### 2.2.2 하단마감재 (Bottom Bar) + +| 세부품목 | EGI prefix | SUS prefix | 재질 | 전개치수 | +|---------|-----------|-----------|------|---------| +| ①하단마감재 | BE | BS | EGI 1.55T / SUS 1.2T | (60*40) | +| ②보강엘바 | LA | LA | EGI 1.55T | (60*17) | +| ③보강평철 | HH | HH | EGI 1.15T | - | +| ④별도마감재 | YY | - | SUS 1.2T (SUS마감 시만) | - | + +**하단마감재 prefix 결정 로직** (`5130:718-721`): +```php +if ($GuidrailFinish == 'EGI 1.55T') → $BTmat = 'BE'; +else → $BTmat = 'BS'; +``` + +#### 2.2.3 셔터박스 (Shutter Box) + +**표준 사이즈 (500*380)** + +| 세부품목 | prefix | 치수 계산 | +|---------|--------|----------| +| ①전면부 | CF | boxheight + 122 | +| ②린텔부 | CL | boxwidth - 330 | +| ③점검구 | CP | boxwidth - 200 | +| ④후면코너부/후면부 | CB | 170 또는 boxheight + 170 | +| ⑥상부덮개 | XX | - | +| ⑦마구리(측면부) | XX | - | + +**비표준 사이즈**: 모든 세부품목에 XX prefix 사용 + +#### 2.2.4 연기차단재 (Smoke Barrier) + +| 세부품목 | prefix | 재질 | +|---------|--------|------| +| 레일용 W50 | GI | EGI 0.8T + 화이바 글라스 코팅직물 | +| 케이스용 W80 | GI | EGI 0.8T + 화이바 글라스 코팅직물 | + +### 2.3 길이 코드 매핑 (getSLengthCode) + +| 길이(mm) | 코드 | 카테고리 | +|---------|------|---------| +| 1219 | 12 | 기타 | +| 2438 | 24 | 기타 | +| 3000 | 30 | 기타 | +| 3500 | 35 | 기타 | +| 4000 | 40 | 기타 | +| 4150 | 41 | 기타 | +| 4200 | 42 | 기타 | +| 4300 | 43 | 기타 | +| 3000 | 53 | 연기차단재50 | +| 4000 | 54 | 연기차단재50 | +| 3000 | 83 | 연기차단재80 | +| 4000 | 84 | 연기차단재80 | + +### 2.4 동적 품목코드 생성 규칙 + +5130에서 LOT 입력 시 사용되는 `data-itemname` 속성: +``` +[PREFIX]-[LENGTH_CODE] + +예시: + RS-40 = 가이드레일 벽면형 SUS 마감재 4000mm + RM-35 = 가이드레일 본체 3500mm + BE-30 = 하단마감재 EGI 3000mm + CF-24 = 셔터박스 전면부 2438mm + GI-53 = 연기차단재 W50 3000mm +``` + +**핵심**: 품목코드가 **길이에 따라 동적으로 결정**됨. 같은 "마감재"라도 3000mm면 `RS-30`, 4000mm면 `RS-40`이 된다. + +--- + +## 3. SAM 현재 구현 현황 + +### 3.1 구현 완료 + +| 기능 | 위치 | 설명 | +|------|------|------| +| BendingInfoBuilder | `api/app/Services/Production/BendingInfoBuilder.php` | 수주→작업지시 시 bending_info JSON 자동생성 | +| categorizeBomItem() | 위 파일 :96-130 | BOM 아이템을 8개 카테고리로 분류 | +| Items Master (BD-*) | items 테이블 (로컬+dev) | 절곡 품목 148개 (제품 마스터형 58 + LOT prefix형 90) | +| getMaterials API | `WorkOrderService.php:1183` | work_order_items 순회 → item.bom 확인 → StockLot 조회 | +| getMaterialsForItem API | `WorkOrderService.php:2678` | 개별 품목 자재 조회 | +| registerMaterialInput | `react/.../WorkerScreen/actions.ts:288` | 자재투입 등록 POST API | +| increaseFromProduction | `api/app/Services/StockService.php` | 생산완료 → 재고입고 | +| 선생산 재고 흐름 | `docs/plans/bending-preproduction-stock-plan.md` | Phase 1-3 완료 | + +### 3.2 BD-* 품목 현황 (로컬 DB 확인 완료) + +**총 148개** BD-* 품목 (2026-02-21 확인): + +**A. 제품 마스터형 (58개)** — 부모 품목 (제품코드+재질+전개치수) +``` +BD-가이드레일-KSS01-SUS-120*70 (20개: KSS01/KSS02/KSE01/KWE01/KTE01/KDSS01/KQTS01별) +BD-하단마감재-KSE01-EGI-60*40 (10개) +BD-케이스-500*380 (10개: 사이즈별) +BD-마구리-505*355 (10개: 사이즈별) +BD-L-BAR-KSS01-17*60 (5개) +BD-보강평철-50 (1개) +BD-가이드레일용 연기차단재 (1개) +BD-케이스용 연기차단재 (1개) +``` + +**B. LOT prefix형 (90개)** — 자재투입 대상 세부품목 (길이별) +| prefix | 개수 | 설명 | +|--------|------|------| +| BD-RS | 5 | 가이드레일(벽면) SUS 마감재 | +| BD-RM | 6 | 가이드레일(벽면) 본체 | +| BD-RC | 6 | 가이드레일(벽면) C형 | +| BD-RD | 6 | 가이드레일(벽면) D형 | +| BD-RT | 2 | 가이드레일(벽면) 본체(철재) | +| BD-SS | 4 | 가이드레일(측면) SUS 마감재 | +| BD-SM | 5 | 가이드레일(측면) 본체/D형 | +| BD-SC | 5 | 가이드레일(측면) C형 | +| BD-SD | 5 | 가이드레일(측면) D형 | +| BD-ST | 1 | 가이드레일(측면) 본체(철재) | +| BD-SU | 4 | 가이드레일(측면) SUS2 (별도마감) | +| BD-BE | 2 | 하단마감재(스크린) EGI | +| BD-BS | 5 | 하단마감재(스크린) SUS | +| BD-TS | 1 | 하단마감재(철재) SUS | +| BD-LA | 2 | L-Bar 스크린용 | +| BD-CF | 6 | 케이스 전면부 | +| BD-CL | 6 | 케이스 린텔부 | +| BD-CP | 6 | 케이스 점검구 | +| BD-CB | 6 | 케이스 후면코너부 | +| BD-GI | 7 | 연기차단재 화이바원단 | + +> XX(하부BASE), YY(별도SUS마감), HH(보강평철)은 미등록 → 방안 B 구현 전 BD-XX-NN, BD-YY-NN, BD-HH-NN 형태로 등록 예정 + +### 3.3 미구현 Gap → 해결 방향 + +> **방안 B 확정(섹션 4) 및 LOT GAP 분석(섹션 7)으로 모두 해결 방향 확정됨.** + +| Gap | 해결 방향 | 참조 | +|-----|----------|------| +| items.bom 연결 (bom = null) | dynamic_bom으로 대체 (items.bom 수정 불필요) | 섹션 4.4, 4.5 | +| 가변 세부품목 배정 | BendingInfoBuilder 확장으로 길이별 동적 품목 결정 | 섹션 4.3 | +| order_items 세부품목 | bom_result 기반으로 BendingInfoBuilder가 직접 생성, order_items 수정 불필요 | 섹션 4.3 | +| LOT prefix 매핑 | dynamic_bom JSON에 lot_prefix 필드 포함 | 섹션 4.4 | +| XX/YY/HH 미등록 품목 | BD-XX-NN, BD-YY-NN, BD-HH-NN 형태로 items에 등록 예정 | 섹션 3.2 | + +--- + +## 4. 아키텍처 설계 (방안 B 확정) + +### 4.1 방안 선택 근거 + +**방안 B (작업지시 시 동적 BOM 생성)** 채택. + +| 근거 | 설명 | +|------|------| +| 견적 금액과 무관 | 견적은 "부모 품목 × 총길이(m) × 단가"로 계산. 세부품목은 금액에 영향 없음 | +| 길이 버킷팅 이미 구현됨 | BendingInfoBuilder에 `heightLengthData()`, `bottomBarDistribution()`, `shutterBoxDistribution()` 존재 | +| 수정 범위 최소 | BendingInfoBuilder에 BD-XX-NN 조회 로직만 추가. 견적 로직 수정 불필요 | +| bom_result 일관성 유지 | 견적 결과(bom_result)를 변경하지 않고, 그 위에 세부 매핑만 추가 | + +> **참고**: 견적과 작업지시는 동일한 BOM 산출 결과(`order_nodes.options.bom_result`)를 공유한다. 견적 계산과 자재투입은 같은 기준을 사용해야 일관성 유지. + +### 4.2 bom_result 실제 데이터 구조 (DB 확인 완료) + +견적 시 `order_nodes.options.bom_result.items`에 저장되는 절곡 관련 부모 품목: + +``` +BD-가이드레일-KSS01-SUS-120*70 qty=8.5m ← 부모 품목 (전개치수 기준) +BD-케이스-500*380 qty=3.22m +BD-마구리-505*385 qty=1 +00035 (하장바) qty=3 +BD-L-BAR-KSS01-17*60 qty=3.22m +BD-보강평철-50 qty=3.22m +EST-SMOKE-레일용 qty=8.5 +EST-SMOKE-케이스용 qty=3.22 +``` + +이 부모 품목들은 **길이별 세부품목(BD-RS-40 등)으로 분해**되어야 자재투입이 가능. + +### 4.3 동적 BOM 생성 흐름 + +``` +[견적] (기존 그대로, 수정 불필요) + QuoteCalculationService.calculateBom() + → bom_result: { BD-가이드레일-KSS01-SUS-120*70, qty=8.5m, ... } + → order_nodes.options.bom_result에 저장 + ↓ +[수주 확정 → 작업지시 생성] + BendingInfoBuilder.build() ← 확장 대상 + ① bom_result에서 부모 품목 읽기 (기존) + ② 치수별 길이 버킷팅 (기존: heightLengthData 등) + 예: 8.5m → 4300mm×1개 + 4000mm×1개 + ③ [신규] 길이코드 + LOT prefix → BD-XX-NN 품목 조회 + 예: 4300mm → 코드43, 마감재 RS → BD-RS-43 (item_id 조회) + ④ [신규] dynamic_bom 생성 → work_order_items.options에 저장 + ↓ +[자재투입] + getMaterials(workOrderId) ← 소폭 수정 + → work_order_items 순회 + → [수정] options.dynamic_bom이 있으면 우선 사용 + → 없으면 기존 item.bom fallback + → 각 세부품목(BD-RS-43 등)의 StockLot 조회 + ↓ +[자재투입 등록] + registerMaterialInput() (기존 그대로) + → stock_transactions 기록 + → stock_lots 차감 +``` + +### 4.4 dynamic_bom JSON 구조 (work_order_items.options) + +```json +{ + "dynamic_bom": [ + { + "child_item_id": 15812, + "child_item_code": "BD-RS-43", + "lot_prefix": "RS", + "part_type": "마감재", + "category": "guideRail", + "material_type": "SUS", + "length_mm": 4300, + "qty": 1 + }, + { + "child_item_id": 15809, + "child_item_code": "BD-RS-40", + "lot_prefix": "RS", + "part_type": "마감재", + "category": "guideRail", + "material_type": "SUS", + "length_mm": 4000, + "qty": 1 + }, + { + "child_item_id": 15826, + "child_item_code": "BD-RM-43", + "lot_prefix": "RM", + "part_type": "본체", + "category": "guideRail", + "material_type": "EGI", + "length_mm": 4300, + "qty": 1 + } + ] +} +``` + +### 4.5 getMaterials() 수정 범위 + +`WorkOrderService.php:1198-1238`에서 기존 `item.bom` 체크 앞에 `dynamic_bom` 체크 추가: + +``` +foreach (work_order_items as woItem): + // [신규] dynamic_bom 우선 체크 + dynamicBom = woItem.options.dynamic_bom ?? null + if (dynamicBom is not empty): + foreach (dynamicBom as bomItem): + childItem = Item::find(bomItem.child_item_id) + materialItems[] = {item: childItem, bom_qty: bomItem.qty, ...} + + // [기존] items.bom fallback + elseif (item.bom is not empty): + ... 기존 로직 ... + + // [기존] 최종 fallback: 품목 자체를 자재로 + else: + ... +``` + +--- + +## 5. LOT Prefix → BD 코드 대응 관계 (실제 DB 확인) + +| LOT Prefix | 5130 세부품목 | SAM 품목코드 패턴 | 등록 수 | 카테고리 | +|-----------|-------------|-----------------|:------:|---------| +| RS | 벽면형 SUS 마감재 | BD-RS-[길이코드] | 5 | 가이드레일 | +| RM | 벽면형 본체 | BD-RM-[길이코드] | 6 | 가이드레일 | +| RC | 벽면형 C형 | BD-RC-[길이코드] | 6 | 가이드레일 | +| RD | 벽면형 D형 | BD-RD-[길이코드] | 6 | 가이드레일 | +| RT | 벽면형 본체(철재) | BD-RT-[길이코드] | 2 | 가이드레일 | +| SS | 측면형 SUS 마감재 | BD-SS-[길이코드] | 4 | 가이드레일 | +| SM | 측면형 본체/D형 | BD-SM-[길이코드] | 5 | 가이드레일 | +| SC | 측면형 C형 | BD-SC-[길이코드] | 5 | 가이드레일 | +| SD | 측면형 D형 | BD-SD-[길이코드] | 5 | 가이드레일 | +| ST | 측면형 본체(철재) | BD-ST-[길이코드] | 1 | 가이드레일 | +| SU | 측면형 SUS2 (별도마감) | BD-SU-[길이코드] | 4 | 가이드레일 | +| BE | 하단마감재(스크린) EGI | BD-BE-[길이코드] | 2 | 하단마감재 | +| BS | 하단마감재(스크린) SUS | BD-BS-[길이코드] | 5 | 하단마감재 | +| TS | 하단마감재(철재) SUS | BD-TS-[길이코드] | 1 | 하단마감재 | +| LA | L-Bar 스크린용 | BD-LA-[길이코드] | 2 | 하단마감재 | +| CF | 케이스 전면부 | BD-CF-[길이코드] | 6 | 셔터박스 | +| CL | 케이스 린텔부 | BD-CL-[길이코드] | 6 | 셔터박스 | +| CP | 케이스 점검구 | BD-CP-[길이코드] | 6 | 셔터박스 | +| CB | 케이스 후면코너부 | BD-CB-[길이코드] | 6 | 셔터박스 | +| GI | 연기차단재 화이바원단 | BD-GI-[길이코드] | 7 | 연기차단재 | + +--- + +## 6. 프론트엔드 매핑 검토 결과 + +### 6.1 작업일지 세부품명 → BD-* 매핑: **가능 ✅** + +각 세부품목에 `lotPrefix` 필드가 이미 정의되어 있다. + +| 섹션 | LOT Prefix (utils.ts 하드코딩) | BD-* 매핑 예시 | +|------|-------------------------------|---------------| +| 가이드레일(벽면) | RS, RT, RC, RD, XX(하부BASE) | `BD-RS-40`, `BD-RT-43` | +| 가이드레일(측면) | SS, ST, SC, SD, XX(하부BASE) | `BD-SS-40`, `BD-ST-43` | +| 하단바 | BE, BS, LA | `BD-BE-40`, `BD-BS-35` | +| 셔터박스 | CF, CL, CP, CB | `BD-CF-40`, `BD-CL-35` | +| 방연 | GI | `BD-GI-53`, `BD-GI-83` | + +**매핑 공식**: `lotPrefix` + `getSLengthCode(길이mm)` → `BD-{prefix}-{lengthCode}` → items 테이블 code 컬럼 +**현재 한계**: LOT NO 컬럼이 `"-"`으로 하드코딩 → `dynamic_bom` 연동 후 실제 LOT 번호 표시 가능 +**프론트 수정 범위**: 소규모 + +### 6.2 자재투입 모달 세부품목 선택: **현재 불가 ❌ → 수정 필요** + +| 항목 | 현재 상태 | 방안 B 적용 후 | +|------|----------|--------------| +| 자재 그룹핑 | 부모 품목 단위 | 세부품목(BD-RS-40 등) 단위 | +| LOT 선택 | 부모 품목의 StockLot만 표시 | 세부품목의 StockLot 표시 | +| FIFO 배분 | 품목 단위 | 세부품목 단위 | + +**핵심**: 백엔드 `getMaterials()` 수정(섹션 4.5)이 완료되면 응답에 세부품목이 포함되므로, 프론트 모달은 **기존 렌더링 로직 그대로** 세부품목을 표시할 수 있다. +**프론트 수정 범위**: 중규모 — 그룹 헤더에 세부품목명 표시, 선택적 UX 개선 + +### 6.3 종합 연결 흐름 + +``` +작업일지 세부품명 ──── lotPrefix + lengthCode ────→ BD-XX-NN (items 테이블) + │ │ + ▼ ▼ + LOT NO 표시 ◄──── dynamic_bom ────────────────── getMaterials() + │ │ + ▼ ▼ +자재투입 모달 ◄──── 세부품목 단위 LOT 선택 ────── FIFO 배분 +``` + +**구현 순서**: BendingInfoBuilder 확장(dynamic_bom 생성) → getMaterials() 수정 → 프론트 모달 수정 → 작업일지 LOT NO 표시 + +--- + +## 7. LOT 추적 데이터 누락 분석 (2026-02-22) + +### 7.1 현재 LOT 추적 인프라 + +``` +수주(orders) ──FK──→ 작업지시(work_orders) ──FK──→ 산출물 LOT(stock_lots) + │ │ │ + │ source_order_item_id │ work_order_material_inputs│ work_order_id + ▼ ▼ ▼ +order_items ←── work_order_items ──→ 투입 LOT(stock_lots) ──→ stock_transactions +``` + +| 연결 | FK/테이블 | 상태 | +|------|----------|:----:| +| 수주 → 작업지시 | `work_orders.sales_order_id` | ✅ | +| 수주품목 → 작업지시품목 | `work_order_items.source_order_item_id` | ✅ | +| 생산완료 → 산출물 LOT | `stock_lots.work_order_id` | ✅ | +| 구매입고 → 원자재 LOT | `stock_lots.receiving_id` | ✅ | +| 자재투입 이력 | `work_order_material_inputs` | ✅ | +| 거래 이력 | `stock_transactions` | ✅ | + +### 7.2 발견된 GAP + +#### 🔴 GAP 1: `registerMaterialInput()`에서 투입 이력 레코드 미생성 + +**위치**: `WorkOrderService.php` L1330-1390 + +``` +registerMaterialInput() (L1330) ← 작업지시 전체 단위 + → 재고 차감 ✅, 감사 로그 ✅, WorkOrderMaterialInput 레코드 ❌ + +registerMaterialInputForItem() (L2821) ← 개소(품목) 단위 + → 재고 차감 ✅, 감사 로그 ✅, WorkOrderMaterialInput 레코드 ✅ +``` + +**해결**: `registerMaterialInputForItem()`으로 API 통일 +**우선순위**: 🔴 즉시 (방안 B와 독립적으로 수정 가능) + +#### 🔴 GAP 2: dynamic_bom 미구현 → 절곡 세부품목 LOT 추적 불가 + +현재 `items.bom`만 체크 → 절곡 부모 품목의 bom이 null → 세부품목이 자재 목록에 미포함. +**해결**: 방안 B 구현 (섹션 4.5) +**우선순위**: 🔴 방안 B와 동시 + +#### 🔴 GAP 5: bending_info ↔ dynamic_bom 정합성 보장 메커니즘 없음 + +별도 생성 시 작업일지 표시 ≠ 자재투입 대상 불일치 위험. +**해결**: BendingInfoBuilder에서 **동시에 생성**하여 같은 길이 버킷팅 결과 공유 +**우선순위**: 🔴 방안 B와 동시 (설계 시 반영 필수) + +#### 🔴 GAP 4: 수주 연결 작업지시 산출물이 stock_lots 안 거침 + +**위치**: `WorkOrderService.php` L576-583 (`updateStatus()`) + +```php +if ($workOrder->sales_order_id) { + $this->createShipmentFromWorkOrder(...); // 출하 직행, stock_lots 미거침 +} else { + $this->stockInFromProduction($workOrder); // 재고 입고 → LOT 생성 +} +``` + +**원인**: 출하 시스템이 아직 러프하게 구성된 상태 (의도된 설계 아님) +**해결 (권장)**: **"생산완료 → 항상 재고 입고(stock_lots)" 통일** + +| 항목 | 현재 | 권장 변경 | +|------|------|----------| +| 선생산 완료 | `stockInFromProduction()` → stock_lots ✅ | 변경 없음 | +| 수주 연결 완료 | `createShipmentFromWorkOrder()` → 출하 직행 | `stockInFromProduction()` → stock_lots 생성 → 출하는 별도 프로세스 | + +**우선순위**: 🔴 출하 시스템 설계 시 함께 해결 + +#### 🟡 GAP 3: 투입 LOT → 산출 LOT 직접 연결 없음 + +간접 추적 가능 (`산출 LOT → work_order_id → material_inputs → 투입 LOT`). 직접 연결 테이블(`lot_genealogy`)은 향후 고도화. + +#### 🟢 GAP 6, 7 + +- **GAP 6**: 불량 LOT 별도 관리 없음 → 품질 관리 고도화 시 +- **GAP 7**: 공정 간 반제품 LOT 연결 → 기존 `registerMaterialInputForItem()` 구조로 충분 + +### 7.3 우선순위별 조치 계획 + +| 우선순위 | GAP | 조치 | 시점 | +|:--------:|-----|------|------| +| 🔴 | #1 registerMaterialInput 이력 미기록 | `registerMaterialInputForItem()`으로 API 통일 | 즉시 | +| 🔴 | #2 dynamic_bom 미구현 | getMaterials()에 dynamic_bom 우선 체크 | 방안 B 동시 | +| 🔴 | #5 bending_info ↔ dynamic_bom 정합성 | BendingInfoBuilder에서 동시 생성 | 방안 B 동시 | +| 🔴 | #4 수주 연결 산출물 LOT 미생성 | 생산완료 → 항상 stock_lots 입고 통일 | 출하 시스템 설계 시 | +| 🟡 | #3 투입↔산출 LOT 직접 연결 | lot_genealogy 테이블 고려 | 향후 고도화 | + +### 7.4 방안 B 적용 후 목표 LOT 추적 체인 + +``` +[수주] orders + └─ order_nodes.options.bom_result (부모 품목 + 총길이) + │ + ▼ source_order_item_id +[작업지시] work_orders + work_order_items + ├─ options.bending_info (작업일지 표시) ─┐ + └─ options.dynamic_bom (세부품목 매핑) ─┤ 같은 BendingInfoBuilder에서 동시 생성 + │ └─ 정합성 자동 보장 + ▼ getMaterials() → dynamic_bom 우선 체크 +[자재투입] work_order_material_inputs + ├─ work_order_item_id (부모 품목 개소) + ├─ item_id = BD-RS-43 (세부품목) + └─ stock_lot_id = LOT-XXXX (투입 LOT) + │ + ▼ 재고 차감 (stock_transactions: OUT, work_order_input) +[생산완료] stock_lots (work_order_id = 작업지시 ID) + ├─ 선생산: stock_lots 생성 ✅ (현재 동작) + └─ 수주 연결: stock_lots 생성 ✅ (GAP 4 해결 후) + │ + ▼ 역추적 +산출물 LOT → work_order → material_inputs → 투입 LOT → receiving → 공급업체 +``` + +--- + +## 8. 개발 영향 분석 및 위험 평가 (2026-02-22) + +### 8.1 과제별 효과 및 위험 + +#### 과제 1: registerMaterialInput() API 통일 (GAP #1) + +**효과**: 자재투입 이력이 `work_order_material_inputs`에 빠짐없이 기록 → 역추적 체인 완성 + +**위험**: +- 기존 `registerMaterialInput()`은 `work_order_item_id` 파라미터 미수신 → 프론트에서 해당 값 전달하도록 수정 필요 +- L2860-2861 `StockLot::find()` → `$lot->stock->item_id` 역추적 시 Eager Loading 없으면 N+1 쿼리 + +#### 과제 2: BendingInfoBuilder 확장 — dynamic_bom 생성 (GAP #2, #5) + +**효과**: 견적 로직 수정 없이 세부품목별 LOT 추적 가능. bending_info와 동시 생성으로 정합성 보장. + +**위험**: + +| 위험 | 상세 | 대응 | +|------|------|------| +| items 미매칭 | `bucketToStandardLength()`가 표준 길이 초과 시 원본 반환(L862-864) → `BD-RS-4500` 같은 비표준 코드 생성 | 아이템 미발견 시 fallback + 경고 로그 | +| prefix 결정 복잡성 | KSS01→RS, KSE01→RE. SUS마감 여부로 YY 포함. 벽면/측면 prefix 세트 상이 | **PrefixResolver 클래스 분리** (하드코딩 지양) | +| 혼합형 가이드레일 | `buildGuideRail()`에서 wall+side 동시 생성 시 prefix 분기 복잡 | 벽면/측면 각각 독립 dynamic_bom 생성 | +| 생성 이후 수정 | 치수/품목 변경 시 bending_info + dynamic_bom 동시 재생성 필요 | 업데이트 메커니즘 설계 | +| JSON 검증 부재 | dynamic_bom은 JSON → DB 레벨 제약 없음 | Application 레벨 DTO/Validator | + +#### 과제 3: getMaterials() 수정 — dynamic_bom 우선 체크 + +**효과**: 프론트 MaterialInputModal이 세부품목 단위로 LOT 선택 가능 + +**위험**: +- **N+1 쿼리 누적**: 현재 getMaterials() 자체가 N+1 다수. dynamic_bom 추가 시 세부품목 15-25개만큼 쿼리 추가(총 30-50회). `Item::whereIn()` 배치 조회로 개선 필수 +- **uniqueMaterials 합산 시 정보 소실**: L1240-1248에서 같은 item_id면 required_qty 합산 → 어느 `work_order_item`에 속하는지 소실. `registerMaterialInputForItem()` 호출 시 `work_order_item_id` 지정 어려움 → 합산 단위를 `(item_id, work_order_item_id)` 쌍으로 변경 권장 + +#### 과제 4: 수주 연결 산출물 LOT 생성 (GAP #4) + +**효과**: 모든 생산 완료 건에 stock_lots 기록 → 완전한 LOT 추적 체인 + +**위험**: +- **출하 시스템 의존성**: `createShipmentFromWorkOrder()` 단순 제거 시 현재 출하 흐름 깨짐 → 출하 재설계와 병행 필수 +- **재고 이중 계상**: stock_lots 입고~출하 시간 차 동안 재고로 잡힘 → 다른 주문에 배정될 위험 + +### 8.2 Race Condition 분석 + +| 시나리오 | 리스크 | 대응 | +|---------|-------|------| +| 자재투입 동시 요청 | 두 작업자가 같은 LOT 동시 차감 → 초과 차감 | `lockForUpdate()` 비관적 잠금 | +| getMaterials→투입 시간 차 | 조회 후 다른 작업지시에서 같은 LOT 소진 | 투입 시 available_qty 재검증 (decreaseFromLot에서 수행), 부족 시 명확한 오류 | + +### 8.3 마이그레이션/롤백 평가 + +| 항목 | 평가 | +|------|------| +| DB 스키마 변경 | **없음** — 기존 options JSON 컬럼 활용 | +| 코드 롤백 | Git 롤백으로 복원 가능 | +| 데이터 롤백 | dynamic_bom이 있는 건도 코드 롤백 시 기존 fallback 동작 → **하위 호환성 확보** | +| items 마스터 롤백 | dynamic_bom의 child_item_id가 참조 가능 → 주의 | + +### 8.4 개선 권장사항 + +| 영역 | 제안 | 시점 | +|------|------|------| +| 쿼리 최적화 | getMaterials() 내 `whereIn()` 배치 조회 + Eager Loading | 방안 B 구현 시 | +| Prefix 매핑 | BendingInfoBuilder 하드코딩 대신 **PrefixResolver 클래스** 분리 | 방안 B 구현 시 | +| 검증 레이어 | dynamic_bom JSON DTO/Validator 클래스 | 방안 B 구현 시 | +| 마스터 데이터 검증 | prefix × lengthCode 전체 조합 items 존재 확인 스크립트 | 방안 B 구현 전 | +| 아이템 미발견 처리 | 로그 경고 + 관리자 알림 + graceful fallback | 방안 B 구현 시 | +| dynamic_bom 메타정보 | 생성 시각/빌더 버전을 options에 포함 → 디버깅 용이 | 방안 B 구현 시 | +| 테스트 | productCode × guideType 전 조합 단위 테스트 + getMaterials→투입 통합 테스트 | 방안 B 구현 후 | + +### 8.5 종합 평가 + +**방안 B는 기술적으로 타당.** 견적 로직 미변경, 기존 JSON options 패턴 활용, 하위 호환성 유지. + +**핵심 리스크 2가지**: +1. **items 마스터 데이터 완전성** — 19종 prefix × 7-12개 길이코드 조합이 items에 정확히 존재해야 함 +2. **LOT prefix 결정 로직의 복잡성** — 제품코드/마감재질/가이드타입에 따른 분기 다수 → 하드코딩 시 유지보수 어려움 + +→ **마스터 데이터 검증 스크립트**와 **PrefixResolver 분리**를 개발 초기에 확보할 것 + +--- + +## 9. 참고 문서 + +| 문서 | 경로 | +|------|------| +| 선생산 재고 계획 | `docs/plans/bending-preproduction-stock-plan.md` | +| BendingInfoBuilder | `api/app/Services/Production/BendingInfoBuilder.php` | +| QuoteCalculationService | `api/app/Services/Quote/QuoteCalculationService.php` | +| FormulaEvaluatorService | `api/app/Services/Quote/FormulaEvaluatorService.php` | +| EstimatePriceService | `api/app/Services/Quote/EstimatePriceService.php` | +| WorkOrderService | `api/app/Services/WorkOrderService.php` | +| StockService | `api/app/Services/StockService.php` | +| WorkOrderMaterialInput 모델 | `api/app/Models/Production/WorkOrderMaterialInput.php` | +| 자재투입 마이그레이션 | `api/database/migrations/2026_02_12_100000_create_work_order_material_inputs_table.php` | +| stock_lots work_order_id FK | `api/database/migrations/2026_02_21_100000_add_work_order_id_to_stock_lots_table.php` | +| MaterialInputModal | `react/src/components/production/WorkerScreen/MaterialInputModal.tsx` | +| 5130 작업일지 | `5130/output/viewBendingWork_UA.php` | +| Bending types/utils | `react/src/components/production/WorkOrders/documents/bending/` | + +--- + +## 10. 변경 이력 + +| 날짜 | 변경 내용 | +|------|----------| +| 2026-02-21 | 문서 초안 작성 (현황 분석, 5130 체계 정리) | +| 2026-02-21 | 로컬 DB BD-* 148개 확인, 제품코드 7종 추가, 추가 prefix(RT/ST/SU/TS) 발견 | +| 2026-02-21 | **방안 B 확정**: 작업지시 시 BendingInfoBuilder 확장으로 동적 BOM 생성 | +| 2026-02-21 | 프론트엔드 매핑 검토 추가 (lotPrefix→BD-* 매핑 가능, 자재투입 모달 수정 필요) | +| 2026-02-22 | LOT 추적 데이터 누락 분석: 7개 GAP 발견, 우선순위별 조치 계획 수립 | +| 2026-02-22 | 문서 정리: 중복/해소 항목 제거, dynamic_bom에 category/material_type 추가 | +| 2026-02-22 | 섹션 8 추가: 개발 영향 분석 및 위험 평가 (과제별 효과/위험, race condition, 롤백, 개선 권장) | diff --git a/plans/fg-code-consolidation-plan.md b/plans/fg-code-consolidation-plan.md new file mode 100644 index 0000000..08cb519 --- /dev/null +++ b/plans/fg-code-consolidation-plan.md @@ -0,0 +1,754 @@ +# FG 제품코드 통합 계획 + +> **작성일**: 2026-02-19 +> **목적**: FG 제품코드에서 설치유형/마감재질을 분리하여 위치별 설정으로 이동, 18개 FG 품목을 6개로 통합 +> **기준 문서**: `docs/rules/item-policy.md`, `docs/features/quotes/README.md` +> **상태**: 🔄 진행중 + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | 영향도 분석 완료, 혼합형 validation 수정 커밋 완료 | +| **다음 작업** | Phase 1: DB 마이그레이션 | +| **진행률** | 0/8 (0%) | +| **마지막 업데이트** | 2026-02-19 | + +--- + +## 1. 개요 + +### 1.1 배경 + +현재 경동기업(tenant_id=287) FG 품목 코드 체계: +``` +FG-KWE01-벽면형-SUS (모델: KWE01, 설치유형: 벽면형, 마감재질: SUS) +FG-KWE01-벽면형-EGI (모델: KWE01, 설치유형: 벽면형, 마감재질: EGI) +FG-KWE01-측면형-SUS (모델: KWE01, 설치유형: 측면형, 마감재질: SUS) +... (총 18개 = 6모델 × {벽면형,측면형} × {SUS,EGI} + 혼합형 추가 예정) +``` + +문제점: +- 설치유형/마감재질은 **위치(Location)별 설정**이지 제품 자체의 속성이 아님 +- 같은 모델(KWE01)인데 FG 코드가 4개 이상으로 분산 +- 혼합형 추가 시 FG 품목이 계속 늘어남 (6모델 × 3설치유형 × 2마감재질 = 36개) + +### 1.2 목표 코드 체계 +``` +AS-IS: FG-KWE01-벽면형-SUS → TO-BE: KWE01 +``` +- "FG-" 접두사 제거: `item_type = 'FG'` 컬럼이 이미 완제품 구분 담당 +- 설치유형(벽면형/측면형/혼합형) 제거: 위치별 `guideRailType` 파라미터로 전달 +- 마감재질(SUS/EGI) 제거: 위치별 `finishingType` 파라미터로 전달 + +### 1.3 기준 원칙 +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 🎯 핵심 원칙 │ +├─────────────────────────────────────────────────────────────────┤ +│ 1. 코어 계산 로직(KyungdongFormulaHandler) 변경 없음 │ +│ 2. BOM은 child_item_id FK 기반 → 코드 변경에 안전 │ +│ 3. product_model/finishing_type은 이미 별도 파라미터 전달 중 │ +│ 4. 기존 quote_items에 FG 코드 참조 데이터 없음 (마이그레이션 부담 ↓) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 1.4 변경 승인 정책 + +| 분류 | 예시 | 승인 | +|------|------|------| +| ✅ 즉시 가능 | React UI에 마감재질 Select 추가, validation 규칙 수정 | 불필요 | +| ⚠️ 컨펌 필요 | items 테이블 데이터 통합, BOM parent_item_id 재매핑, 시더 수정 | **필수** | +| 🔴 금지 | items 테이블 스키마 변경, 기존 BOM 삭제, 견적 계산 코어 로직 변경 | 별도 협의 | + +### 1.5 준수 규칙 +- `docs/rules/item-policy.md` - 품목 정책 +- `docs/standards/quality-checklist.md` - 품질 체크리스트 +- `docs/features/quotes/README.md` - 견적 시스템 + +--- + +## 2. 대상 범위 + +### 2.1 Phase 1: DB 마이그레이션 (items 통합) + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 1.1 | 18개 FG 품목 → 6개로 통합 마이그레이션 스크립트 | ⏳ | items.code 변경 | +| 1.2 | BOM parent_item_id 재매핑 | ⏳ | 통합된 item_id로 변경 | +| 1.3 | 통합 대상 외 12개 FG 품목 soft delete | ⏳ | 연결된 BOM 확인 후 | +| 1.4 | MapItemsToProcesses globalExcludes 수정 | ⏳ | 'FG-%' → item_type 기반 | + +### 2.2 Phase 2: API 수정 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 2.1 | FormulaEvaluatorService: finishing_type 파라미터 수신 | ⏳ | 마감재질 매핑 추가 | +| 2.2 | QuoteBomCalculateRequest: finishingType validation 추가 | ⏳ | SUS/EGI | +| 2.3 | QuoteBomBulkCalculateRequest: finishingType validation 추가 | ⏳ | SUS/EGI | +| 2.4 | KyungdongItemSeeder 수정 (향후 시딩용) | ⏳ | FG-코드 생성 로직 | + +### 2.3 Phase 3: React 프론트엔드 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 3.1 | LocationDetailPanel: 마감재질 Select UI 추가 | ⏳ | SUS/EGI 선택 | +| 3.2 | LocationListPanel: 마감재질 컬럼/폼필드 추가 | ⏳ | 위치 추가 시 | +| 3.3 | types.ts: QuoteLocation에 finishingType 추가 | ⏳ | | +| 3.4 | actions.ts: BOM 산출 요청에 finishingType 포함 | ⏳ | | +| 3.5 | QuoteRegistration.tsx: mock 데이터 업데이트 | ⏳ | | +| 3.6 | QuoteSummaryPanel/PreviewContent: 마감재질 표시 | ⏳ | | + +### 2.4 Phase 4: 검증 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 4.1 | 통합 전후 BOM 계산 결과 비교 테스트 | ⏳ | 동일 입력 → 동일 결과 | +| 4.2 | 견적 등록 → 산출 → 저장 E2E 테스트 | ⏳ | | + +--- + +## 3. 작업 절차 + +### 3.1 단계별 절차 + +``` +Step 1: DB 마이그레이션 스크립트 작성 +├── 6개 모델별 대표 FG 품목 선정 (유지할 item_id 결정) +├── BOM parent_item_id를 대표 item_id로 재매핑 +├── 대표 품목의 code를 통합 코드로 변경 (KWE01 등) +├── 대표 품목의 attributes에서 guiderail_type/finishing_type 제거 +└── 나머지 12개 FG 품목 soft delete + +Step 2: API 수정 +├── FormRequest에 finishingType/FT validation 추가 +├── FormulaEvaluatorService에 FT → finishing_type 매핑 추가 +├── MapItemsToProcesses globalExcludes → item_type 기반 변경 +└── KyungdongItemSeeder 코드 생성 로직 수정 + +Step 3: React 프론트엔드 +├── types.ts에 finishingType 필드 추가 +├── LocationDetailPanel에 마감재질 Select 추가 +├── LocationListPanel에 마감재질 폼필드/컬럼 추가 +├── actions.ts BOM 산출 요청에 finishingType 포함 +└── Summary/Preview에 마감재질 표시 + +Step 4: 검증 +├── 동일 입력(KWE01 + wall + SUS)으로 기존 결과와 비교 +├── 모든 조합 테스트 (6모델 × 3설치 × 2마감) +└── 견적 등록 → 산출 → 저장 E2E +``` + +--- + +## 4. 상세 작업 내용 (코드 스니펫 포함) + +### 4.1 현재 FG 품목 현황 (tenant_id=287) + +| 모델 | 벽면형-SUS | 벽면형-EGI | 측면형-SUS | 측면형-EGI | 통합 코드 | item_category | +|------|-----------|-----------|-----------|-----------|----------|:------------:| +| KWE01 | FG-KWE01-벽면형-SUS | FG-KWE01-벽면형-EGI | FG-KWE01-측면형-SUS | FG-KWE01-측면형-EGI | **KWE01** | SCREEN | +| KWE02 | (동일 패턴) | | | | **KWE02** | SCREEN | +| KWE03 | | | | | **KWE03** | SCREEN | +| KWS01 | | | | | **KWS01** | STEEL | +| KWS02 | | | | | **KWS02** | STEEL | +| KWS03 | | | | | **KWS03** | STEEL | + +> KWE = 스크린(SCREEN), KWS = 철재(STEEL). item_category는 유지됨 (계산 분기에 사용) + +FG 코드 생성 원본 (`api/database/seeders/Kyungdong/KyungdongItemSeeder.php:305-307`): +```php +$finishingShort = self::FINISHING_MAP[$model->finishing_type] ?? 'STD'; +$code = "FG-{$model->model_name}-{$model->guiderail_type}-{$finishingShort}"; +$name = "{$model->model_name} {$model->major_category} {$model->finishing_type} {$model->guiderail_type}"; +``` + +FINISHING_MAP (`KyungdongItemSeeder.php:39-42`): +```php +private const FINISHING_MAP = [ + 'SUS마감' => 'SUS', + 'EGI마감' => 'EGI', +]; +``` + +items.attributes 구조: +```json +{ + "model_name": "KWE01", + "major_category": "스크린", + "finishing_type": "SUS마감", + "guiderail_type": "벽면형", + "legacy_source": "models", + "legacy_model_id": 123 +} +``` + +### 4.2 BOM 재매핑 전략 + +BOM은 FG 품목(parent)의 `items.bom` JSON 컬럼에 저장: +```json +[ + { "child_item_id": 123, "quantity": 1 }, + { "child_item_id": 456, "quantity": 2 } +] +``` + +마이그레이션 SQL 전략: +```sql +-- Step 1: 모델별 대표 FG 품목 선정 (벽면형-SUS를 대표로) +-- 대표 선정 기준: 같은 model_name 중 가장 작은 id + +-- Step 2: 대표 품목의 code 변경 +UPDATE items SET code = 'KWE01' +WHERE id = (대표_item_id) AND tenant_id = 287; + +-- Step 3: 대표 품목의 attributes에서 guiderail_type/finishing_type 제거 +-- (이 속성들은 더 이상 품목 고유 속성이 아님) + +-- Step 4: 비대표 품목의 BOM을 대표 품목으로 이관 +-- (동일 모델의 BOM은 동일하므로, BOM이 있는 품목의 bom을 대표로 복사) + +-- Step 5: 비대표 12개 품목 soft delete +UPDATE items SET deleted_at = NOW(), deleted_by = 1 +WHERE tenant_id = 287 AND item_type = 'FG' + AND id NOT IN (대표_item_ids); +``` + +핵심 안전 요소: +- BOM의 `child_item_id`는 PT/SM 품목 → FG 통합과 **무관** +- `FormulaEvaluatorService::getItemDetails()` (line 1110-1112)에서 `->where('code', $itemCode)` 조회 +- 통합 후 code가 'KWE01'이 되면 `getItemDetails('KWE01')`로 정상 조회 + +### 4.3 API 파라미터 흐름 (통합 후) + +``` +Frontend (LocationDetailPanel) + ├── productCode: "KWE01" (통합 코드) + ├── guideRailType: "wall" | "floor" | "mixed" + ├── finishingType: "SUS" | "EGI" ← 새로 추가 + └── motorPower: "single" | "three" + ↓ +actions.ts::calculateBomBulk() - POST /api/v1/quotes/calculate/bom/bulk + body: { items: [{ finished_goods_code, openWidth, openHeight, guideRailType, motorPower, finishingType, ... }] } + ↓ +QuoteBomBulkCalculateRequest::normalizeInputVariables() (line 122-135) + ├── 'W0' => openWidth, 'H0' => openHeight + ├── 'GT' => guideRailType, 'MP' => motorPower + └── 'FT' => finishingType ← 새로 추가 + ↓ +FormulaEvaluatorService::calculateKyungdongBom() (line 1574~) + ├── getItemDetails("KWE01", tenantId) → items.code = "KWE01" 조회 (line 1110-1112) + ├── $finishingType: FT → SUS/EGI ← 기존 line 1677 수정 + ├── $installationType: GT → 벽면형/측면형/혼합형 (line 1680-1684) + └── $motorVoltage: MP → 220V/380V (line 1687-1690) + ↓ +$calculatedVariables = array_merge() (line 1692-1708) + 'finishing_type' => $finishingType (line 1705) ← 이미 포함됨 + ↓ +KyungdongFormulaHandler (변경 없음) + ├── calculateSteelItems() line 458: $rawFinish = $params['finishing_type'] ?? 'SUS' + ├── calculateGuideRails() line 540: $finishingType 파라미터 + └── getBottomBarPrice() line 561: $finishingType 파라미터 +``` + +### 4.4 핵심 파일별 변경 상세 + +--- + +#### 4.4.1 `api/app/Services/Quote/FormulaEvaluatorService.php` + +**현재 코드 (line 1676-1677):** +```php +$productModel = $inputVariables['product_model'] ?? 'KSS01'; +$finishingType = $inputVariables['finishing_type'] ?? 'SUS'; +``` + +**수정 후:** +```php +$productModel = $inputVariables['product_model'] ?? 'KSS01'; + +// 마감재질: 프론트 FT(SUS/EGI) → finishing_type 매핑 +$finishingType = $inputVariables['finishing_type'] ?? match ($inputVariables['FT'] ?? 'SUS') { + 'EGI' => 'EGI', + default => 'SUS', +}; +``` + +> `$calculatedVariables` array_merge (line 1705)에는 이미 `'finishing_type' => $finishingType` 포함됨 + +--- + +#### 4.4.2 `api/app/Http/Requests/Quote/QuoteBomCalculateRequest.php` + +**현재 rules() (line 20-39)에 추가:** +```php +// 기존 +'GT' => 'nullable|string|in:wall,ceiling,floor,mixed', +'MP' => 'nullable|string|in:single,three', +// 추가 +'FT' => 'nullable|string|in:SUS,EGI', +``` + +**현재 getInputVariables() (line 74-89)에 추가:** +```php +// 기존 +'MP' => $validated['MP'] ?? 'single', +// 추가 +'FT' => $validated['FT'] ?? 'SUS', +``` + +--- + +#### 4.4.3 `api/app/Http/Requests/Quote/QuoteBomBulkCalculateRequest.php` + +**rules() (line 21-54)에 추가:** +```php +// React 필드명 (camelCase) +'items.*.finishingType' => 'nullable|string|in:SUS,EGI', +// API 변수명 (약어) +'items.*.FT' => 'nullable|string|in:SUS,EGI', +``` + +**normalizeInputVariables() (line 122-135)에 추가:** +```php +// 기존 +'MP' => $item['motorPower'] ?? $item['MP'] ?? 'single', +// 추가 +'FT' => $item['finishingType'] ?? $item['FT'] ?? 'SUS', +``` + +--- + +#### 4.4.4 `api/app/Console/Commands/MapItemsToProcesses.php` + +**현재 (line 48):** +```php +private array $globalExcludes = ['FG-%', 'RM-%', 'EST-INSPECTION']; +``` + +**수정 후:** +```php +private array $globalExcludes = ['RM-%', 'EST-INSPECTION']; +// FG 제외는 item_type 기반으로 처리 (아래 쿼리에서 ->where('item_type', '!=', 'FG') 추가) +``` + +> 해당 명령어에서 items 조회 시 `->whereNotIn('item_type', ['FG'])` 조건 추가 + +--- + +#### 4.4.5 `api/database/seeders/Kyungdong/KyungdongItemSeeder.php` + +**현재 (line 305-307):** +```php +$finishingShort = self::FINISHING_MAP[$model->finishing_type] ?? 'STD'; +$code = "FG-{$model->model_name}-{$model->guiderail_type}-{$finishingShort}"; +$name = "{$model->model_name} {$model->major_category} {$model->finishing_type} {$model->guiderail_type}"; +``` + +**수정 후:** +```php +$code = $model->model_name; // KWE01, KWS01 등 +$name = "{$model->model_name} {$model->major_category}"; +``` + +> 중복 방지: 같은 model_name은 하나만 생성 (기존: 설치유형×마감재질 조합별 생성 → 모델별 1개) + +--- + +#### 4.4.6 `react/src/components/quotes/types.ts` + +**LocationItem 인터페이스 (line 664-686)에 추가:** +```typescript +export interface LocationItem { + // ... 기존 필드 + guideRailType: string; // 가이드레일 설치 유형 + finishingType: string; // 마감재질 (SUS/EGI) ← 추가 + motorPower: string; // 모터 전원 + // ... +} +``` + +--- + +#### 4.4.7 `react/src/components/quotes/actions.ts` + +**BomCalculateItem 인터페이스 (line 343-354)에 추가:** +```typescript +export interface BomCalculateItem { + finished_goods_code: string; + openWidth: number; + openHeight: number; + quantity?: number; + guideRailType?: string; + finishingType?: string; // ← 추가 + motorPower?: string; + controller?: string; + wingSize?: number; + inspectionFee?: number; +} +``` + +--- + +#### 4.4.8 `react/src/components/quotes/LocationDetailPanel.tsx` + +**상수 추가 (line 75 뒤):** +```typescript +// 마감재질 +const FINISHING_TYPES = [ + { value: "SUS", label: "SUS (스테인리스)" }, + { value: "EGI", label: "EGI (아연도금)" }, +]; +``` + +**2행 그리드 변경 (line 358-423):** +현재 `grid-cols-3` (가이드레일, 전원, 제어기) → `grid-cols-4`로 변경하고 마감재질 Select 추가: +```tsx +{/* 2행: 가이드레일, 마감재질, 전원, 제어기 */} +
+ {/* 가이드레일 (기존) */} +
...
+ {/* 마감재질 (새로 추가) */} +
+ + +
+ {/* 전원 (기존) */} +
...
+ {/* 제어기 (기존) */} +
...
+
+``` + +--- + +#### 4.4.9 `react/src/components/quotes/LocationListPanel.tsx` + +**formData 초기값 (line 110-120)에 추가:** +```typescript +const [formData, setFormData] = useState({ + // ... 기존 + guideRailType: "wall", + finishingType: "SUS", // ← 추가 + motorPower: "single", + // ... +}); +``` + +**2행 폼 (line ~380 이후)에 마감재질 Select 추가** (가이드레일 Select 패턴과 동일) + +--- + +#### 4.4.10 `react/src/components/quotes/QuoteRegistration.tsx` + +**BOM 계산 페이로드 (line 459-469)에 finishingType 추가:** +```typescript +const bomItem = { + finished_goods_code: newLocation.productCode, + openWidth: newLocation.openWidth, + openHeight: newLocation.openHeight, + quantity: newLocation.quantity, + guideRailType: newLocation.guideRailType, + finishingType: newLocation.finishingType, // ← 추가 + motorPower: newLocation.motorPower, + controller: newLocation.controller, + wingSize: newLocation.wingSize, + inspectionFee: newLocation.inspectionFee, +}; +``` + +**다건 산출 (line 594-606)도 동일하게 finishingType 추가:** +```typescript +const bomItems = formData.locations.map((loc) => ({ + finished_goods_code: loc.productCode, + // ... + finishingType: loc.finishingType, // ← 추가 + // ... +})); +``` + +**기본값 (line 117):** +```typescript +// 기존 +guideRailType: "wall", +// 추가 +finishingType: "SUS", +``` + +**mock 데이터 (line 248):** +```typescript +// 기존: productCode: randomProduct?.item_code || "FG-SCR-001" +// 수정: productCode: randomProduct?.item_code || "KWE01" +``` + +--- + +#### 4.4.11 `react/src/components/quotes/QuoteSummaryPanel.tsx` & `QuotePreviewContent.tsx` + +위치 정보 표시 영역에 마감재질 추가: +```typescript +// QuoteSummaryPanel.tsx line 172 근처 +{loc.productCode} ({loc.finishingType}) × {loc.quantity} + +// QuotePreviewContent.tsx line 209 근처 +{loc.productCode} +{loc.finishingType} // 또는 기존 컬럼에 병합 +``` + +--- + +#### 4.4.12 `react/src/app/[locale]/(protected)/sales/quote-management/[id]/page.tsx` + +**기존 견적 조회 시 BOM 재계산 페이로드 (line 60-70):** +```typescript +const bomItems: BomCalculateItem[] = locationsNeedingRecalc.map(loc => ({ + finished_goods_code: loc.productCode, + openWidth: loc.openWidth, + openHeight: loc.openHeight, + quantity: loc.quantity, + guideRailType: loc.guideRailType, + // finishingType: loc.finishingType, ← 추가 필요 + motorPower: loc.motorPower, + controller: loc.controller, + wingSize: loc.wingSize, + inspectionFee: loc.inspectionFee, +})); +``` + +### 4.5 DB 마이그레이션 사전 검증 쿼리 + +마이그레이션 실행 전 반드시 확인할 쿼리: + +```sql +-- 1. 현재 FG 품목 전체 목록 확인 +SELECT id, code, name, item_category, + JSON_EXTRACT(attributes, '$.model_name') as model_name, + JSON_EXTRACT(attributes, '$.guiderail_type') as guiderail_type, + JSON_EXTRACT(attributes, '$.finishing_type') as finishing_type, + bom IS NOT NULL AND bom != '[]' as has_bom +FROM items +WHERE tenant_id = 287 AND item_type = 'FG' AND deleted_at IS NULL +ORDER BY code; + +-- 2. 모델별 BOM 동일성 검증 (같은 model_name의 bom이 동일한지) +SELECT JSON_EXTRACT(attributes, '$.model_name') as model_name, + COUNT(DISTINCT bom) as distinct_bom_count, + COUNT(*) as total_count +FROM items +WHERE tenant_id = 287 AND item_type = 'FG' AND deleted_at IS NULL +GROUP BY JSON_EXTRACT(attributes, '$.model_name'); +-- distinct_bom_count = 1 이면 안전 (동일 모델의 BOM이 같음) + +-- 3. 다른 테이블에서 FG item_id 참조 확인 +SELECT 'quote_items' as tbl, COUNT(*) as cnt +FROM quote_items WHERE item_id IN ( + SELECT id FROM items WHERE tenant_id = 287 AND item_type = 'FG' +) +UNION ALL +SELECT 'work_order_items', COUNT(*) +FROM work_order_items WHERE item_id IN ( + SELECT id FROM items WHERE tenant_id = 287 AND item_type = 'FG' +); +-- 모두 0이면 안전하게 통합 가능 +``` + +--- + +### 4.6 핵심 API 메서드 참조 (읽기 전용) + +아래 메서드들은 **변경하지 않지만** 동작을 이해하기 위해 참조: + +**`FormulaEvaluatorService::getItemDetails()` (line 1102-1134):** +```php +public function getItemDetails(string $itemCode, ?int $tenantId = null): ?array +{ + $item = DB::table('items') + ->where('tenant_id', $tenantId) + ->where('code', $itemCode) // ← 여기서 code로 조회 + ->whereNull('deleted_at') + ->first(); + // ... id, code, name, item_type, item_category, bom 등 반환 +} +``` +→ 통합 후 `getItemDetails('KWE01')` 호출 시 code='KWE01' 품목 정상 조회 + +**`FormulaEvaluatorService::calculateKyungdongBom()` 핵심 흐름 (line 1574~):** +``` +1. getItemDetails($finishedGoodsCode) → 완제품 조회 +2. $productCategory = $finishedGoods['item_category'] → 'SCREEN' 또는 'STEEL' +3. $productModel, $finishingType, $installationType, $motorVoltage 결정 +4. $calculatedVariables = array_merge($inputVariables, [...]) +5. KyungdongFormulaHandler::calculateDynamicItems($calculatedVariables) 호출 +``` +→ `item_category`는 items 레코드에서 가져오므로 통합 후에도 정상 (KWE01 → SCREEN) + +**`KyungdongFormulaHandler` finishing_type 사용처:** +- `calculateSteelItems()` line 458: `$rawFinish = $params['finishing_type'] ?? 'SUS'` +- `calculateGuideRails()` line 540: 파라미터로 수신 +- `getBottomBarPrice()` line 561: 가격 조회에 사용 +- `getGuideRailPrice()` line 696: 가격 조회에 사용 +→ 모두 `$calculatedVariables['finishing_type']`에서 값을 가져오므로 매핑만 추가하면 됨 + +**React `getFinishedGoods()` (actions.ts line 302-317):** +```typescript +const result = await executeServerAction({ + url: buildApiUrl('/api/v1/items', { + item_type: 'FG', + has_bom: '1', + size: '5000', + }), +}); +``` +→ `item_type='FG'`로 조회하므로 code 변경 영향 없음. 통합 후 6개만 반환됨. + +--- + +## 5. 컨펌 대기 목록 + +| # | 항목 | 변경 내용 | 영향 범위 | 상태 | +|---|------|----------|----------|------| +| 1 | FG 품목 통합 마이그레이션 | 18개 → 6개, BOM 재매핑 | DB, 모든 FG 참조 | ⏳ 대기 | +| 2 | 12개 FG 품목 soft delete | 통합 후 불필요 품목 삭제 | DB | ⏳ 대기 | + +--- + +## 6. 변경 이력 + +| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | +|------|------|----------|------|------| +| 2026-02-19 | - | 문서 초안 작성 | - | - | +| 2026-02-19 | 혼합형 지원 | GT validation에 mixed 추가 | QuoteBomCalculateRequest, QuoteBomBulkCalculateRequest | ✅ | +| 2026-02-19 | 모터 전압 | MP → motor_voltage 매핑 추가 | FormulaEvaluatorService | ✅ | +| 2026-02-19 | 가이드레일 | GT → installation_type 매핑 추가 | FormulaEvaluatorService | ✅ | +| 2026-02-19 | 혼합형 UI | GUIDE_RAIL_TYPES에 mixed 옵션 추가 | LocationDetailPanel | ✅ | + +--- + +## 7. 참고 문서 + +- **품목 정책**: `docs/rules/item-policy.md` +- **견적 시스템**: `docs/features/quotes/README.md` +- **DB 스키마**: `docs/specs/database-schema.md` +- **품질 체크리스트**: `docs/standards/quality-checklist.md` +- **빠른 시작**: `docs/quickstart/quick-start.md` +- **견적 계산 계획**: `docs/plans/kd-quote-logic-plan.md` +- **경동 품목 시더**: `api/database/seeders/Kyungdong/KyungdongItemSeeder.php` + +--- + +## 8. 세션 및 메모리 관리 정책 + +### 8.1 세션 시작 시 +``` +read_memory("fg-consolidation-state") +read_memory("fg-consolidation-snapshot") +계획 문서 읽기 → docs/plans/fg-code-consolidation-plan.md +``` + +### 8.2 작업 중 관리 +| 컨텍스트 잔량 | Action | 내용 | +|--------------|--------|------| +| **30% 이하** | Snapshot | `write_memory("fg-consolidation-snapshot", ...)` | +| **20% 이하** | Symbol Tracking | `write_memory("fg-consolidation-active-symbols", ...)` | +| **10% 이하** | Stop & Save | 최종 상태 저장 후 세션 교체 권고 | + +### 8.3 Serena 메모리 구조 +- `fg-consolidation-state`: { phase, progress, next_step, last_decision } +- `fg-consolidation-snapshot`: 코드 변경점 + 논의 요약 +- `fg-consolidation-rules`: 불변 규칙 (코어 로직 변경 없음, BOM FK 안전 등) +- `fg-consolidation-active-symbols`: 수정 중인 파일/심볼 리스트 + +--- + +## 9. 검증 결과 + +### 9.1 테스트 케이스 + +| 입력값 | 예상 결과 | 실제 결과 | 상태 | +|--------|----------|----------|------| +| KWE01 + wall + SUS + W0=2000 + H0=3000 | FG-KWE01-벽면형-SUS 동일 결과 | - | ⏳ | +| KWE01 + floor + EGI + W0=2000 + H0=3000 | FG-KWE01-측면형-EGI 동일 결과 | - | ⏳ | +| KWE01 + mixed + SUS + W0=2000 + H0=3000 | 혼합형 계산 정상 | - | ⏳ | +| KWS01 + wall + SUS + W0=2000 + H0=3000 | FG-KWS01-벽면형-SUS 동일 결과 | - | ⏳ | +| KWE01 + three + SUS + W0=5000 + H0=5000 | 삼상 모터 + SUS 정상 | - | ⏳ | + +### 9.2 성공 기준 + +| 기준 | 달성 | 비고 | +|------|------|------| +| FG 품목 18개 → 6개 통합 | ⏳ | | +| BOM 계산 결과 통합 전후 동일 | ⏳ | 모든 조합 | +| 견적 등록 → 산출 → 저장 정상 | ⏳ | | +| 마감재질 선택 UI 동작 | ⏳ | | +| 기존 기능 회귀 없음 | ⏳ | | + +--- + +## 10. 자기완결성 점검 결과 + +### 10.1 체크리스트 검증 + +| # | 검증 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 1 | 작업 목적이 명확한가? | ✅ | 1.1 배경, 1.2 목표 | +| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.2 성공 기준 | +| 3 | 작업 범위가 구체적인가? | ✅ | 2. 대상 범위 13개 항목 | +| 4 | 의존성이 명시되어 있는가? | ✅ | Phase 순서 = 의존성 | +| 5 | 참고 파일 경로가 정확한가? | ✅ | 4.4 핵심 파일 변경 목록 | +| 6 | 단계별 절차가 실행 가능한가? | ✅ | 3.1 + 4.x 상세 | +| 7 | 검증 방법이 명시되어 있는가? | ✅ | 9.1 테스트 케이스 | +| 8 | 모호한 표현이 없는가? | ✅ | 구체적 코드/파일명 명시 | + +### 10.2 새 세션 시뮬레이션 테스트 + +| 질문 | 답변 가능 | 참조 섹션 | +|------|:--------:|----------| +| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1, 1.2 | +| Q2. 어디서부터 시작해야 하는가? | ✅ | 3.1 Step 1 | +| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 4.4 핵심 파일 변경 목록 | +| Q4. 작업 완료 확인 방법은? | ✅ | 9.1, 9.2 | +| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 | + +**결과**: 5/5 통과 → ✅ 자기완결성 확보 + +--- + +## 11. 리스크 및 롤백 + +### 11.1 리스크 평가 + +| 리스크 | 확률 | 영향 | 대응 | +|--------|:----:|:----:|------| +| BOM parent_item_id 누락 | 중 | 높 | 마이그레이션 전 BOM 전수 검증 쿼리 실행 | +| 견적 계산 결과 불일치 | 낮 | 높 | 통합 전후 동일 입력 비교 테스트 5건 이상 | +| 기존 데이터 호환성 깨짐 | 낮 | 낮 | 현재 quote_items에 FG 코드 참조 데이터 없음 | +| 프론트 productCode 참조 오류 | 중 | 중 | 46개 참조 지점 전수 확인 | + +### 11.2 롤백 전략 + +- DB 마이그레이션은 Laravel down() 메서드로 롤백 가능하도록 작성 +- 마이그레이션 실행 전 items + BOM 데이터 백업 쿼리 준비 +- API/React 변경은 git revert로 원복 가능 + +--- + +*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/item-inventory-management-plan.md b/plans/item-inventory-management-plan.md new file mode 100644 index 0000000..191ab54 --- /dev/null +++ b/plans/item-inventory-management-plan.md @@ -0,0 +1,167 @@ +# 품목 재고 관리 체계 설계 + +> 작성일: 2026-02-12 +> 상태: 설계 확정, 단계별 구현 예정 + +## 1. 배경 + +### 문제 +- 5130(레거시)에서 관리하던 "내화실" 등 품목이 SAM에 제대로 반영되지 않음 +- 기존 item_type(FG/PT/SM/RM/CS) 분류만으로는 다양한 관리 방식을 표현할 수 없음 +- 소모품 중 LOT 관리가 필요한 품목과 불필요한 품목 구분 불가 +- 자체생산 재고품(중간재) 개념 부재 + +### 현재 item_type 체계 +| 코드 | 의미 | 비고 | +|------|------|------| +| FG | 완제품 (Finished Goods) | 출하 대상 | +| PT | 부품 (Parts) | BOM 구성 | +| SM | 부자재 (Sub Materials) | 구매품 | +| RM | 원자재 (Raw Materials) | 구매품, LOT 관리 | +| CS | 소모품 (Consumables) | 단순 소진 | + +## 2. 설계: items.options JSON 기반 관리 속성 + +### 핵심 원칙 +- **컬럼 추가 금지**: FK/조인키만 컬럼 추가, 나머지는 JSON (멀티테넌시 원칙) +- **item_type은 "뭐냐"**, **options는 "어떻게 관리하냐"**를 구분 + +### options 필드 정의 + +| 키 | 타입 | 값 | 설명 | +|----|------|-----|------| +| `lot_managed` | boolean | true/false | LOT 번호 추적 여부 | +| `consumption_method` | string | auto/manual/none | 소진 처리 방식 | +| `production_source` | string | purchased/self_produced/both | 조달 구분 | +| `input_tracking` | boolean | true/false | 원자재 투입 추적 여부 | +| `material` | string | - | 재질 정보 (선택) | + +### 필드 상세 + +**lot_managed** +- `true`: 입고 시 LOT 번호 필수, stock_lots 테이블에 LOT별 수량 추적 +- `false`: LOT 없이 총량만 관리 + +**consumption_method** +- `auto`: 생산 완료 시 BOM 기준 자동 차감 +- `manual`: 사용자가 직접 수량 입력하여 소진 처리 +- `none`: 소진 추적 안 함 (완제품 등) + +**production_source** +- `purchased`: 구매 입고만 (원자재, 부자재, 소모품) +- `self_produced`: 자체 생산으로 입고 (중간재, 반제품) +- `both`: 구매 + 자체 생산 모두 가능 + +**input_tracking** +- `true`: 생산 시 BOM 기반 원자재 투입 기록 +- `false`: 잔재/스크랩 활용 생산 → 투입 추적 불가, 산출물 입고만 기록 + +## 3. 품목 유형별 적용 + +### 유형 분류표 + +| 유형 | 예시 | item_type | lot | consumption | source | input_tracking | +|------|------|-----------|-----|------------|--------|---------------| +| 구매 소모품 (LOT) | 내화실 | SM | true | manual | purchased | - | +| 구매 소모품 (비LOT) | 장갑, 테이프 | CS | false | manual | purchased | - | +| 원자재 | 실리카원단, EGI코일 | RM | true | auto | purchased | - | +| 일반 자체생산 | 슬랫, 절곡물 | PT | true | auto | self_produced | true | +| 잔재 활용 생산 | 조인트바 | PT | true | auto | self_produced | false | +| 완제품 | 방화스크린 | FG | true | none | self_produced | true | + +### 유형별 처리 흐름 + +#### 구매 소모품 - LOT 관리 (내화실) +``` +납품 → 수입검사 → 검사 합격 + → stock_transactions(IN) + LOT 생성 + → 작업일지에 사용 LOT 기록 (추적용) + → 수동 소진 처리: 사용자가 수량 입력 → stock_transactions(OUT, manual_consumption) +``` + +#### 구매 소모품 - 비LOT (장갑, 테이프) +``` +구매 입고 → stock_transactions(IN), LOT 없음 + → 수동 소진 처리: 수량 입력 → stock_transactions(OUT, manual_consumption) +``` + +#### 일반 자체생산 (슬랫, 절곡물) +``` +작업지시 시작 + → BOM 기준 원자재 자동 차감: stock_transactions(OUT, work_order_input) + → 생산 완료 + → 산출물 입고: stock_transactions(IN, production_output) + LOT 생성 + → 상위 조립 시 BOM 기준 자동 차감 +``` + +#### 잔재 활용 생산 (조인트바) +``` +다른 공정 잔재/스크랩 활용 + → 원자재 투입 기록 없음 (이미 다른 공정에서 차감됨) + → 생산 완료 + → 산출물 입고만: stock_transactions(IN, production_output) + LOT 생성 + → 상위 조립 시 BOM 기준 자동 차감 +``` + +## 4. 내화실 품목 업데이트 (완료) + +### 변경 내역 +| 필드 | 변경 전 | 변경 후 | +|------|--------|---------| +| code | 80019 | 내화실-WY-MA12 | +| name | 실 | 내화실 | +| unit | m | 콘 | +| attributes.spec | (비어있음) | WY-MA12 | +| options | null | 아래 참조 | + +### options 값 +```json +{ + "lot_managed": true, + "consumption_method": "manual", + "production_source": "purchased", + "material": "SUS316L + Para aramid" +} +``` + +### 배포 +- 시더: `api/database/seeders/data/kyungdong/items.json` (커밋 완료) +- SQL: `docs/deploys/item-naehwasil-update-20260212.sql` + +## 5. 구현 로드맵 + +### Phase 1: 품목 마스터 정비 (현재) +- [x] options 체계 설계 +- [x] 내화실 품목 데이터 업데이트 +- [ ] 슬랫, 절곡물, 조인트바 등 자체생산품 options 설정 +- [ ] 기존 품목 일괄 options 매핑 + +### Phase 2: 수동 소진 처리 +- [ ] API: 소모품 사용 처리 엔드포인트 (POST /stocks/{id}/consume) +- [ ] React: 소모품 사용 처리 화면 +- [ ] stock_transactions reason에 `manual_consumption` 추가 + +### Phase 3: 자체생산품 입고 연동 +- [ ] 작업지시 완료 시 산출물 자동 입고 로직 +- [ ] stock_transactions reason에 `production_output` 추가 +- [ ] 작업지시번호 기반 LOT 자동 생성 규칙 +- [ ] input_tracking=false인 경우 투입 차감 스킵 로직 + +### Phase 4: BOM 기반 자동 차감 +- [ ] consumption_method=auto인 품목 자동 차감 로직 +- [ ] 작업지시 완료 → BOM 순회 → 해당 품목 stock_transactions(OUT) +- [ ] 부족 재고 경고 알림 + +## 6. 참고 + +### 관련 파일 +- Item 모델: `api/app/Models/Items/Item.php` +- Stock 모델: `api/app/Models/Tenants/Stock.php` +- StockTransaction 모델: `api/app/Models/Tenants/StockTransaction.php` +- StockLot 모델: `api/app/Models/Tenants/StockLot.php` +- 시더 데이터: `api/database/seeders/data/kyungdong/items.json` + +### 5130 참고 파일 +- 내화실 수입검사: `5130/instock/i_fireproofWire.php` +- 스크린 작업일지: `5130/output/viewScreenWork.php` +- LOT 조회: `5130/output/fetch_lot.php`