- 완료된 계획 문서 22개를 plans/archive/로 이동 - tracked 16개 (git mv): bending-lot-pipeline, docs-update, fcm-notification 등 - untracked 6개 (mv): bending-worklog, formula-engine, mng-item 등 - index_plans.md 전면 업데이트 - 진행중 44개 / 완료 37개 현황 반영 - 각 문서별 실제 진행률 기재 (0%~94%) - 카테고리별 재정리 (견적/생산/품목/문서/마이그레이션/시스템/UI) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1098 lines
42 KiB
Markdown
1098 lines
42 KiB
Markdown
# 절곡 자재투입 LOT 매핑 파이프라인 개발 계획
|
||
|
||
> **작성일**: 2026-02-22
|
||
> **목적**: 절곡 세부품목(BD-XX-NN)의 동적 BOM 생성 및 LOT 추적 파이프라인 구축
|
||
> **기준 문서**: `docs/plans/bending-material-input-mapping-plan.md`
|
||
> **상태**: ✅ 완료 (Serena ID: bending-lot-pipeline-state)
|
||
|
||
---
|
||
|
||
## 📍 현재 진행 상태
|
||
|
||
| 항목 | 내용 |
|
||
|------|------|
|
||
| **마지막 완료 작업** | Phase 5.2 완료 — 전체 파이프라인 완성 |
|
||
| **다음 작업** | 없음 (전체 완료) |
|
||
| **진행률** | 13/13 (100%) ✅ |
|
||
| **마지막 업데이트** | 2026-02-22 |
|
||
|
||
---
|
||
|
||
## 1. 개요
|
||
|
||
### 1.1 배경
|
||
|
||
절곡 작업일지에는 4대 카테고리(가이드레일/하단마감재/셔터박스/연기차단재)의 세부품목이 표시되나, 현재 SAM에서 이 세부품목들이 items 테이블의 BOM과 연결되지 않아 **자재투입 시 세부품목별 LOT 매핑이 불가능**하다.
|
||
|
||
**방안 B(동적 BOM 생성)** 확정: 작업지시 생성 시 BendingInfoBuilder를 확장하여 `work_order_items.options.dynamic_bom`에 세부품목 정보를 저장하고, `getMaterials()` API가 이를 우선 참조하도록 수정한다.
|
||
|
||
### 1.2 기준 원칙
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 🎯 핵심 원칙 │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ 1. 견적 로직(QuoteCalculationService) 수정 없음 │
|
||
│ 2. DB 스키마 변경 없음 — 기존 options JSON 컬럼 활용 │
|
||
│ 3. 하위 호환성 — dynamic_bom 없는 기존 데이터도 정상 동작 │
|
||
│ 4. bending_info와 dynamic_bom은 동일 Builder에서 동시 생성 │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 1.3 변경 승인 정책
|
||
|
||
| 분류 | 예시 | 승인 |
|
||
|------|------|------|
|
||
| ✅ 즉시 가능 | JSON 필드 추가, 새 Service 클래스 생성, 유틸 함수, 테스트 | 불필요 |
|
||
| ⚠️ 컨펌 필요 | getMaterials() 로직 변경, registerMaterialInput API 통일, 프론트 모달 동작 변경 | **필수** |
|
||
| 🔴 금지 | items.bom 컬럼 직접 수정, 견적 로직 변경, work_order_material_inputs 스키마 변경 | 별도 협의 |
|
||
|
||
### 1.4 준수 규칙
|
||
|
||
- `docs/standards/api-rules.md` — Service-First, FormRequest, ApiResponse
|
||
- `docs/standards/quality-checklist.md` — 품질 체크리스트
|
||
- `docs/rules/item-policy.md` — 품목 정책 (BD-* 명명 규칙)
|
||
- `api/CLAUDE.md` — SAM API 개발 규칙
|
||
|
||
### 1.5 성공 기준
|
||
|
||
| 기준 | 측정 방법 |
|
||
|------|----------|
|
||
| 작업지시 생성 시 dynamic_bom JSON 자동 생성 | work_order_items.options에 dynamic_bom 존재 확인 |
|
||
| getMaterials API가 세부품목(BD-RS-43 등) 반환 | API 응답에 세부품목 리스트 포함 확인 |
|
||
| 세부품목별 LOT 선택 → 재고 차감 정상 | stock_transactions + work_order_material_inputs 레코드 확인 |
|
||
| 자재투입 이력에 work_order_item_id 기록 | WorkOrderMaterialInput 레코드의 work_order_item_id NOT NULL |
|
||
| 레거시 5130과 동일한 LOT prefix 체계 | prefix × lengthCode 전체 조합 매칭 검증 |
|
||
|
||
### 1.6 현재 구현 컨텍스트 (새 세션 필독)
|
||
|
||
> 이 섹션은 새 세션에서 별도 파일을 읽지 않고도 작업을 시작할 수 있도록 핵심 코드 구조를 인라인합니다.
|
||
|
||
#### 1.6.1 전체 데이터 흐름
|
||
|
||
```
|
||
[견적/수주]
|
||
QuoteCalculationService.calculateBom()
|
||
→ order_nodes.options.bom_result에 부모 품목 저장
|
||
→ 예: BD-가이드레일-KSS01-SUS-120*70, qty=8.5m
|
||
↓
|
||
[작업지시 생성]
|
||
WorkOrderService.store() (L266-316)
|
||
→ salesOrder.items 순회 → work_order_items에 복사
|
||
→ nodeOptions에서 bending_info 복사: work_order_items.options.bending_info
|
||
→ ⭐ [신규] dynamic_bom도 여기서 저장: work_order_items.options.dynamic_bom
|
||
↓
|
||
BendingInfoBuilder.build(Order, processId) (L29-69)
|
||
→ 절곡 공정 확인 → rootNodes 필터링 → productCode 파싱
|
||
→ getMaterialMapping() → aggregateNodes() → assembleBendingInfo()
|
||
→ ⭐ [신규] buildDynamicBom() → 길이 버킷팅 결과로 BD-XX-NN 세부품목 매핑
|
||
↓
|
||
[자재투입 조회]
|
||
getMaterials(workOrderId) (L1183-1317)
|
||
→ work_order_items 순회
|
||
→ ⭐ [신규] options.dynamic_bom 있으면 세부품목 사용 / 없으면 item.bom fallback
|
||
→ 세부품목별 Stock → StockLot (FIFO) 조회
|
||
↓
|
||
[자재투입 등록]
|
||
registerMaterialInputForItem(workOrderId, itemId, inputs) (L2821-2907)
|
||
→ StockService.decreaseFromLot() — 재고 차감
|
||
→ WorkOrderMaterialInput::create() — 투입 이력 기록
|
||
↓
|
||
[생산완료]
|
||
updateStatus(workOrderId, 'completed') (L520-602)
|
||
→ sales_order_id 있으면: createShipmentFromWorkOrder() (출하 직행)
|
||
→ sales_order_id 없으면: stockInFromProduction() → stock_lots 생성
|
||
```
|
||
|
||
#### 1.6.2 BendingInfoBuilder 핵심 구조
|
||
|
||
**파일**: `api/app/Services/Production/BendingInfoBuilder.php`
|
||
|
||
```php
|
||
// 진입점
|
||
public function build(Order $order, int $processId, ?array $nodeIds = null): ?array
|
||
|
||
// BOM 아이템 카테고리 분류 (L96-130)
|
||
private function categorizeBomItem(array $bomItem): ?string
|
||
// 반환: 'guideRail', 'shutterBox_case', 'shutterBox_finCover', 'bottomBar',
|
||
// 'smokeBarrier_rail', 'smokeBarrier_case', 'detail_lbar', 'detail_reinforce', 'motor'
|
||
|
||
// 노드 집계 (L135-175)
|
||
private function aggregateNodes(Collection $nodes): array
|
||
// 반환: { dimensionGroups: [{height, width, qty}], totalNodeQty, bomCategories: {category => bomItem} }
|
||
|
||
// 높이 기준 버킷팅 (L760-763) — 가이드레일용
|
||
private function heightLengthData(array $dimGroups): array
|
||
// 반환: [{ length: 2438, quantity: 5 }, { length: 3000, quantity: 3 }]
|
||
// 표준 길이: [2438, 3000, 3500, 4000, 4300]
|
||
|
||
// 하단마감재 배분 (L801-834)
|
||
private function bottomBarDistribution(int $openWidth): array
|
||
// 반환: [3000mm수량, 4000mm수량]
|
||
// 예: openWidth=7000 → [1, 1] (3000×1 + 4000×1)
|
||
|
||
// 셔터박스 배분 (L411-548)
|
||
private function shutterBoxDistribution(int $openWidth): array
|
||
// 반환: [1219 => qty, 2438 => qty, 3000 => qty, 3500 => qty, 4000 => qty, 4150 => qty]
|
||
|
||
// 가이드레일 섹션 (L251-299)
|
||
private function buildGuideRail(string $guideType, string $baseSize, array $materials, array $dimGroups, string $productCode): array
|
||
// guideType: '벽면형', '측면형', '혼합형'
|
||
// 반환: { wall: {baseSize, baseDimension, lengthData}, side: {...} | null }
|
||
|
||
// 표준 길이 버킷팅 (L856-865) — ⚠️ 초과 시 원본 반환
|
||
private function bucketToStandardLength(int $dimension, array $buckets): int
|
||
```
|
||
|
||
#### 1.6.3 getMaterials() 현재 로직
|
||
|
||
**파일**: `api/app/Services/WorkOrderService.php` L1183-1317
|
||
|
||
```
|
||
Phase 1: 유니크 자재 수집
|
||
for each workOrder.items:
|
||
if item.bom 존재: ← 절곡 부모 품목은 bom=null이므로 여기 안 탐
|
||
BOM 자식 순회 → uniqueMaterials[childItemId] += qty
|
||
else: ← 현재 절곡은 여기로 빠짐 (부모 품목 자체가 자재로)
|
||
uniqueMaterials[itemId] = qty
|
||
|
||
Phase 2: StockLot 조회
|
||
for each uniqueMaterial:
|
||
stock = Stock.find(itemId) → StockLot.where(available) → FIFO 정렬
|
||
|
||
⚠️ 문제: 절곡 부모 품목(BD-가이드레일-KSS01-SUS-120*70)의 bom이 null
|
||
→ 세부품목(BD-RS-43 등)이 자재 목록에 나오지 않음
|
||
→ dynamic_bom으로 해결
|
||
```
|
||
|
||
#### 1.6.4 registerMaterialInput 두 메서드 차이
|
||
|
||
| 항목 | registerMaterialInput (L1330) | registerMaterialInputForItem (L2821) |
|
||
|------|-------------------------------|--------------------------------------|
|
||
| 파라미터 | workOrderId, inputs | workOrderId, **itemId**, inputs |
|
||
| 재고 차감 | ✅ decreaseFromLot | ✅ decreaseFromLot |
|
||
| WorkOrderMaterialInput | ❌ 미생성 | ✅ 생성 (work_order_item_id 포함) |
|
||
| 용도 | 전체 작업지시 단위 | 개소(품목) 단위 |
|
||
|
||
#### 1.6.5 프론트엔드 현재 구조
|
||
|
||
**MaterialInputModal** (`react/src/components/production/WorkerScreen/MaterialInputModal.tsx`)
|
||
|
||
```typescript
|
||
// Props — workOrderItemId 유무로 API 경로 분기
|
||
interface MaterialInputModalProps {
|
||
order: WorkOrder | null;
|
||
workOrderItemId?: number; // 있으면 개소별 API, 없으면 전체 API
|
||
workOrderItemName?: string;
|
||
}
|
||
|
||
// 품목 그룹핑 (L102-119): itemId 기준 Map<number, MaterialForInput[]>
|
||
// FIFO 배분 (L121-138): selectedLotKeys → 가용량 순서로 자동 배분
|
||
// 등록 (L261-307):
|
||
// workOrderItemId ? registerMaterialInputForItem() : registerMaterialInput()
|
||
```
|
||
|
||
**API 엔드포인트** (`react/src/components/production/WorkerScreen/actions.ts`)
|
||
|
||
| 메서드 | 경로 | 함수명 |
|
||
|--------|------|--------|
|
||
| GET | `/api/v1/work-orders/{id}/materials` | getMaterialsForWorkOrder |
|
||
| GET | `/api/v1/work-orders/{id}/items/{itemId}/materials` | getMaterialsForItem |
|
||
| POST | `/api/v1/work-orders/{id}/material-inputs` | registerMaterialInput |
|
||
| POST | `/api/v1/work-orders/{id}/items/{itemId}/material-inputs` | registerMaterialInputForItem |
|
||
| GET | `/api/v1/work-orders/{id}/items/{itemId}/material-inputs` | getMaterialInputsForItem |
|
||
| DELETE | `/api/v1/work-orders/{id}/material-inputs/{inputId}` | deleteMaterialInput |
|
||
| PATCH | `/api/v1/work-orders/{id}/material-inputs/{inputId}` | updateMaterialInput |
|
||
|
||
**절곡 유틸리티** (`react/.../documents/bending/utils.ts`)
|
||
|
||
- `getSLengthCode(length, category)` — 길이→코드 변환
|
||
- `getMaterialMapping(productCode, finishMaterial)` — 재질 매핑
|
||
- `buildWallGuideRailRows()`, `buildSideGuideRailRows()`, `buildBottomBarRows()`, `buildShutterBoxRows()`, `buildSmokeBarrierRows()` — 각 섹션 파트 행 생성 (lotPrefix 포함)
|
||
|
||
#### 1.6.6 LOT Prefix 전체 맵 (PrefixResolver 구현 기준)
|
||
|
||
**가이드레일 벽면형 (Wall)**
|
||
|
||
| 세부품목 | KSS01(SUS) | KSE01/KWE01(EGI마감) | KSE01/KWE01(SUS마감) | KTE01(철재) |
|
||
|---------|-----------|-------------------|-------------------|-----------|
|
||
| 마감재 | RS | RE | RE | RS |
|
||
| 본체 | RM | RM | RM | **RT** |
|
||
| C형 | RC | RC | RC | RC |
|
||
| D형 | RD | RD | RD | RD |
|
||
| 별도마감 | - | - | **YY** | - |
|
||
| 하부BASE | XX | XX | XX | XX |
|
||
|
||
**가이드레일 측면형 (Side)**
|
||
|
||
| 세부품목 | KSS01(SUS) | KSE01/KWE01(EGI마감) | KSE01/KWE01(SUS마감) | KTE01(철재) |
|
||
|---------|-----------|-------------------|-------------------|-----------|
|
||
| 마감재 | SS | SE | SE | SS |
|
||
| 본체 | SM | SM | SM | **ST** |
|
||
| C형 | SC | SC | SC | SC |
|
||
| D형 | SD | SD | SD | SD |
|
||
| 별도마감 | - | - | **YY** | - |
|
||
| 하부BASE | XX | XX | XX | XX |
|
||
|
||
**하단마감재**
|
||
|
||
| 세부품목 | EGI마감 | SUS마감 | 철재 |
|
||
|---------|--------|--------|------|
|
||
| 메인 | BE | BS | TS |
|
||
| L-Bar | LA | LA | LA |
|
||
| 보강평철 | HH | HH | HH |
|
||
| 별도마감 | - | YY | - |
|
||
|
||
**셔터박스** (표준 500*380 사이즈만 개별 prefix)
|
||
|
||
| 세부품목 | 표준 prefix | 비표준 prefix |
|
||
|---------|-----------|-------------|
|
||
| 전면부 | CF | XX |
|
||
| 린텔부 | CL | XX |
|
||
| 점검구 | CP | XX |
|
||
| 후면코너부 | CB | XX |
|
||
| 상부덮개 | XX | XX |
|
||
| 마구리 | XX | XX |
|
||
|
||
**연기차단재**: W50, W80 모두 → GI
|
||
|
||
#### 1.6.7 길이코드 매핑 (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 전용 |
|
||
|
||
**코드 생성 규칙**: `BD-{prefix}-{lengthCode}` → 예: `BD-RS-43` = 가이드레일 벽면 SUS 마감재 4300mm
|
||
|
||
#### 1.6.8 BD-* 마스터 현황 (items 테이블, 총 148개)
|
||
|
||
**A. 제품 마스터형 (58개)** — 부모 품목 (견적 BOM에 사용)
|
||
```
|
||
BD-가이드레일-KSS01-SUS-120*70 등 (20개: 제품코드별)
|
||
BD-하단마감재-KSE01-EGI-60*40 등 (10개)
|
||
BD-케이스-500*380 등 (10개), BD-마구리-505*355 등 (10개)
|
||
BD-L-BAR-*, BD-보강평철-*, BD-연기차단재 (8개)
|
||
```
|
||
|
||
**B. LOT prefix형 (90개 등록, XX/YY/HH 미등록)** — 세부품목 (자재투입 대상)
|
||
|
||
| prefix | 수량 | prefix | 수량 | prefix | 수량 |
|
||
|--------|:----:|--------|:----:|--------|:----:|
|
||
| BD-RS | 5 | BD-SS | 4 | BD-BE | 2 |
|
||
| BD-RM | 6 | BD-SM | 5 | BD-BS | 5 |
|
||
| BD-RC | 6 | BD-SC | 5 | BD-TS | 1 |
|
||
| BD-RD | 6 | BD-SD | 5 | BD-LA | 2 |
|
||
| BD-RT | 2 | BD-ST | 1 | BD-CF | 6 |
|
||
| | | BD-SU | 4 | BD-CL | 6 |
|
||
| | | | | BD-CP | 6 |
|
||
| | | | | BD-CB | 6 |
|
||
| | | | | BD-GI | 7 |
|
||
|
||
**미등록**: BD-XX (하부BASE/셔터 상부/마구리), BD-YY (별도SUS마감), BD-HH (보강평철) → Phase 0.1에서 등록
|
||
|
||
#### 1.6.9 dynamic_bom JSON 목표 구조
|
||
|
||
`work_order_items.options.dynamic_bom` 에 저장:
|
||
|
||
```json
|
||
[
|
||
{
|
||
"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
|
||
}
|
||
]
|
||
```
|
||
|
||
**필드 설명**:
|
||
- `child_item_id`: items 테이블 PK (getMaterials에서 Stock/StockLot 조회용)
|
||
- `child_item_code`: items.code (표시용)
|
||
- `lot_prefix`: LOT prefix (프론트 작업일지 매핑용)
|
||
- `part_type`: 세부품명 한글 (마감재, 본체, C형 등)
|
||
- `category`: 4대 카테고리 (guideRail, bottomBar, shutterBox, smokeBarrier)
|
||
- `material_type`: 재질 (SUS, EGI 등)
|
||
- `length_mm`: 표준 길이 (mm)
|
||
- `qty`: 수량
|
||
|
||
---
|
||
|
||
## 2. 대상 범위
|
||
|
||
### 2.1 Phase 0: 선행 준비 (마스터 데이터)
|
||
|
||
| # | 작업 항목 | 상태 | 비고 |
|
||
|---|----------|:----:|------|
|
||
| 0.1 | XX/YY/HH 미등록 품목 items 등록 | ✅ | 22건 등록 (13+9 추가 누락) |
|
||
| 0.2 | 마스터 데이터 검증 스크립트 작성 | ✅ | 101/101 전체 통과 |
|
||
|
||
### 2.2 Phase 1: GAP #1 해결 — API 통일
|
||
|
||
| # | 작업 항목 | 상태 | 비고 |
|
||
|---|----------|:----:|------|
|
||
| 1.1 | registerMaterialInput → registerMaterialInputForItem 통일 | ✅ | work_order_item_id 분기 + fallback + N+1 수정 |
|
||
| 1.2 | 프론트 workOrderItemId 전달 보장 | ✅ | actions.ts + MaterialInputModal work_order_item_id 전달 |
|
||
|
||
### 2.3 Phase 2: 방안 B 핵심 — dynamic_bom 생성
|
||
|
||
| # | 작업 항목 | 상태 | 비고 |
|
||
|---|----------|:----:|------|
|
||
| 2.1 | PrefixResolver 클래스 구현 | ✅ | `app/Services/Production/PrefixResolver.php` |
|
||
| 2.2 | BendingInfoBuilder 확장 — dynamic_bom 생성 | ✅ | `build()` 리턴 변경 + `buildDynamicBomForItem()` 추가, OrderService 연동 |
|
||
| 2.3 | DynamicBomEntry DTO 구현 | ✅ | `app/DTOs/Production/DynamicBomEntry.php` |
|
||
|
||
### 2.4 Phase 3: getMaterials 연동
|
||
|
||
| # | 작업 항목 | 상태 | 비고 |
|
||
|---|----------|:----:|------|
|
||
| 3.1 | getMaterials() dynamic_bom 우선 체크 | ✅ | dynamic_bom → BOM fallback, (item_id, woItem_id) 쌍 합산, 추가 필드 반환 |
|
||
| 3.2 | N+1 쿼리 최적화 + uniqueMaterials 합산 단위 변경 | ✅ | 3.1에서 함께 해결: Item/Stock/StockLot 모두 배치 조회 |
|
||
|
||
### 2.5 Phase 4: 프론트엔드 연동
|
||
|
||
| # | 작업 항목 | 상태 | 비고 |
|
||
|---|----------|:----:|------|
|
||
| 4.1 | 자재투입 모달 세부품목 단위 표시 | ✅ | MaterialInputModal groupKey + category badge + actions.ts 필드 추가 |
|
||
| 4.2 | 작업일지 LOT NO 표시 연동 | ✅ | 4개 섹션 lotNoMap prop + WorkLogModal lotNoMap 빌드 |
|
||
|
||
### 2.6 Phase 5: 테스트 및 검증
|
||
|
||
| # | 작업 항목 | 상태 | 비고 |
|
||
|---|----------|:----:|------|
|
||
| 5.1 | PrefixResolver + dynamic_bom 단위 테스트 | ✅ | 58 tests / 256 assertions 통과 |
|
||
| 5.2 | getMaterials → 자재투입 통합 테스트 | ✅ | 6 tests (4 pass + 2 skip — dynamic_bom 작업지시 미생성), 마스터 품목 전체 검증 |
|
||
|
||
### 2.7 별도 과제 (이 계획 범위 밖)
|
||
|
||
| # | 항목 | 시점 |
|
||
|---|------|------|
|
||
| X.1 | GAP #4: 수주 연결 생산완료 → stock_lots 입고 통일 | 출하 시스템 설계 시 |
|
||
| X.2 | GAP #3: lot_genealogy (투입↔산출 LOT 직접 연결) | 향후 고도화 |
|
||
|
||
---
|
||
|
||
## 3. 작업 절차
|
||
|
||
### 3.1 단계별 절차
|
||
|
||
```
|
||
Phase 0: 선행 준비
|
||
├── 0.1 XX/YY/HH 품목 등록 (items 테이블 INSERT)
|
||
└── 0.2 검증 스크립트 (Artisan Command)
|
||
└── 19종 prefix × 7-12 lengthCode 조합 → items 존재 확인
|
||
|
||
Phase 1: API 통일 (GAP #1) — Phase 0 완료 후
|
||
├── 1.1 registerMaterialInput() 내부에서 registerMaterialInputForItem() 호출하도록 통일
|
||
│ ├── WorkOrderService.php L1330-1388 수정
|
||
│ └── 기존 프론트 호출 호환성 유지
|
||
└── 1.2 프론트 workOrderItemId 전달
|
||
└── WorkerScreen/index.tsx → MaterialInputModal Props
|
||
|
||
Phase 2: dynamic_bom 생성 — Phase 0 완료 후 (Phase 1과 병행 가능)
|
||
├── 2.1 PrefixResolver 클래스
|
||
│ ├── productCode + finishMaterial + guideType → prefix 결정
|
||
│ ├── prefix + lengthMm → BD-XX-NN 코드 생성
|
||
│ └── BD-XX-NN → items.id 조회 (캐시)
|
||
├── 2.2 BendingInfoBuilder 확장
|
||
│ ├── build() 반환값에 dynamic_bom 추가
|
||
│ ├── bending_info와 동시 생성 (정합성 보장)
|
||
│ └── work_order_items.options.dynamic_bom에 저장
|
||
└── 2.3 DynamicBomValidator
|
||
└── dynamic_bom JSON 구조 검증 (child_item_id 필수 등)
|
||
|
||
Phase 3: getMaterials 수정 — Phase 2 완료 후
|
||
├── 3.1 dynamic_bom 우선 체크
|
||
│ ├── WorkOrderService.php getMaterials() L1198 이후
|
||
│ ├── options.dynamic_bom 있으면 → 세부품목 리스트 사용
|
||
│ └── 없으면 → 기존 item.bom fallback (하위 호환)
|
||
└── 3.2 N+1 최적화
|
||
├── Item::whereIn() 배치 조회
|
||
└── uniqueMaterials 합산 단위: (item_id, work_order_item_id) 쌍
|
||
|
||
Phase 4: 프론트엔드 — Phase 3 완료 후
|
||
├── 4.1 자재투입 모달 수정
|
||
│ ├── materialGroups가 세부품목 단위로 표시 (이미 itemId 기준 그룹핑)
|
||
│ └── 그룹 헤더에 세부품목명(BD-RS-43) 표시
|
||
└── 4.2 작업일지 LOT NO 표시
|
||
├── dynamic_bom에서 lotPrefix + lengthCode 조합
|
||
└── 투입 이력(getMaterialInputsForItem)에서 실제 LOT NO 반영
|
||
|
||
Phase 5: 테스트 — Phase 3 완료 후 (Phase 4와 병행 가능)
|
||
├── 5.1 단위 테스트
|
||
│ ├── PrefixResolver: 7종 productCode × 3종 finishMaterial × 3종 guideType
|
||
│ ├── dynamic_bom 생성: 실제 bom_result 데이터 기반
|
||
│ └── DynamicBomValidator: 필수/선택 필드 검증
|
||
└── 5.2 통합 테스트
|
||
├── 작업지시 생성 → dynamic_bom 저장 확인
|
||
├── getMaterials → 세부품목 반환 확인
|
||
└── 자재투입 → stock_transactions + work_order_material_inputs 확인
|
||
```
|
||
|
||
### 3.2 의존성 맵
|
||
|
||
```
|
||
Phase 0 ──→ Phase 1 (독립 진행 가능)
|
||
│
|
||
└──→ Phase 2 ──→ Phase 3 ──→ Phase 4
|
||
│
|
||
└──→ Phase 5
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 상세 작업 내용
|
||
|
||
### 4.1 Phase 0: 선행 준비
|
||
|
||
#### 0.1 XX/YY/HH 미등록 품목 등록
|
||
|
||
**현재 상태**: BD-* 품목 148개 중 XX(하부BASE), YY(별도SUS마감), HH(보강평철) 미등록
|
||
|
||
**목표 상태**: BD-XX-NN, BD-YY-NN, BD-HH-NN 패턴으로 items 테이블에 등록
|
||
|
||
**등록 대상**:
|
||
|
||
| prefix | 설명 | 등록할 길이코드 | 예상 수량 |
|
||
|--------|------|---------------|----------|
|
||
| BD-XX | 하부BASE, 셔터박스 상부덮개/마구리 | 12, 24, 30, 35, 40, 41, 43 | 7개 |
|
||
| BD-YY | 별도 SUS 마감 (SUS마감 시만) | 30, 35, 40, 43 | 4개 |
|
||
| BD-HH | 보강평철 | 30, 40 | 2개 |
|
||
|
||
**수정 파일**: 없음 (DB INSERT — Seeder 또는 Artisan Command)
|
||
|
||
**생성 파일**:
|
||
- `api/database/seeders/BendingItemSeeder.php` — BD-XX/YY/HH 품목 등록
|
||
|
||
**검증**: `items` 테이블에서 `code LIKE 'BD-XX-%'` 조회로 13개 확인
|
||
|
||
---
|
||
|
||
#### 0.2 마스터 데이터 검증 스크립트
|
||
|
||
**목적**: 19종 prefix × 가능 lengthCode 전체 조합이 items에 존재하는지 확인
|
||
|
||
**생성 파일**:
|
||
- `api/app/Console/Commands/ValidateBendingItems.php`
|
||
|
||
**로직**:
|
||
```
|
||
전체 prefix 목록 정의 (RS, RM, RC, RD, RT, SS, SM, SC, SD, ST, SU, BE, BS, TS, LA, CF, CL, CP, CB, GI, XX, YY, HH)
|
||
각 prefix별 유효 lengthCode 정의
|
||
조합별 items.code = "BD-{prefix}-{code}" 존재 확인
|
||
누락 항목 리스트 출력
|
||
```
|
||
|
||
**실행**: `php artisan bending:validate-items`
|
||
|
||
**검증**: 출력이 "All items registered" (누락 0건)
|
||
|
||
---
|
||
|
||
### 4.2 Phase 1: GAP #1 해결 — API 통일
|
||
|
||
#### 1.1 registerMaterialInput → registerMaterialInputForItem 통일
|
||
|
||
**현재 상태**:
|
||
- `registerMaterialInput()` (L1330): 재고 차감만, WorkOrderMaterialInput 레코드 미생성
|
||
- `registerMaterialInputForItem()` (L2821): 재고 차감 + WorkOrderMaterialInput 레코드 생성
|
||
|
||
**목표 상태**: 모든 자재투입이 `work_order_material_inputs`에 기록
|
||
|
||
**수정 파일**:
|
||
- `api/app/Services/WorkOrderService.php`
|
||
|
||
**수정 내용**:
|
||
```
|
||
registerMaterialInput(int $workOrderId, array $inputs) 수정:
|
||
├── $inputs 배열에 work_order_item_id 필드 추가 지원
|
||
│ { stock_lot_id: N, qty: N, work_order_item_id?: N }
|
||
├── work_order_item_id가 있으면 → registerMaterialInputForItem() 위임
|
||
└── work_order_item_id가 없으면 → 기존 동작 + WorkOrderMaterialInput 레코드 생성 추가
|
||
(work_order_item_id = 첫 번째 work_order_item의 id로 fallback)
|
||
```
|
||
|
||
**N+1 개선**: `registerMaterialInputForItem()` L2860-2861의 `StockLot::find()` → `$lot->stock->item_id` 호출을 `StockLot::with('stock')->find()` Eager Loading으로 변경
|
||
|
||
**검증**:
|
||
- POST `/work-orders/{id}/material-inputs` 호출 후 `work_order_material_inputs` 테이블에 레코드 존재 확인
|
||
- 기존 호출 형식(work_order_item_id 미포함)도 정상 동작 확인
|
||
|
||
---
|
||
|
||
#### 1.2 프론트 workOrderItemId 전달 보장
|
||
|
||
**현재 상태**: `WorkerScreen/index.tsx`에서 `MaterialInputModal`에 `workOrderItemId` Props를 전달하지만, 완료 플로우에서는 미지정 가능
|
||
|
||
**수정 파일**:
|
||
- `react/src/components/production/WorkerScreen/index.tsx`
|
||
|
||
**수정 내용**:
|
||
- 자재투입 모달 호출 시 `workOrderItemId`가 항상 전달되도록 보장
|
||
- 완료 플로우에서도 `selectedItemId` 설정
|
||
|
||
**검증**: MaterialInputModal이 항상 `registerMaterialInputForItem()` 경로로 호출되는지 확인
|
||
|
||
---
|
||
|
||
### 4.3 Phase 2: 방안 B 핵심 — dynamic_bom 생성
|
||
|
||
#### 2.1 PrefixResolver 클래스 구현
|
||
|
||
**목적**: 제품코드 + 마감재질 + 가이드타입 → LOT prefix 결정 로직을 단일 클래스로 집중
|
||
|
||
**생성 파일**:
|
||
- `api/app/Services/Production/PrefixResolver.php`
|
||
|
||
**클래스 구조**:
|
||
```php
|
||
class PrefixResolver
|
||
{
|
||
// 벽면형 prefix 맵
|
||
private const WALL_PREFIXES = [
|
||
'finish' => ['KSS' => 'RS', 'KSE' => 'RE', 'KWE' => 'RE'],
|
||
'body' => 'RM',
|
||
'c_type' => 'RC',
|
||
'd_type' => 'RD',
|
||
'extra_finish' => 'YY', // SUS 마감 시만
|
||
'base' => 'XX',
|
||
];
|
||
|
||
// 측면형 prefix 맵
|
||
private const SIDE_PREFIXES = [
|
||
'finish' => ['KSS' => 'SS', 'KSE' => 'SE', 'KWE' => 'SE'],
|
||
'body' => 'SM',
|
||
'c_type' => 'SC',
|
||
'd_type' => 'SD',
|
||
'extra_finish' => 'YY',
|
||
'base' => 'XX',
|
||
];
|
||
|
||
// 철재형 override
|
||
private const STEEL_OVERRIDES = [
|
||
'wall_body' => 'RT',
|
||
'side_body' => 'ST',
|
||
];
|
||
|
||
// 하단마감재 prefix 맵
|
||
private const BOTTOM_BAR_PREFIXES = [
|
||
'EGI' => 'BE',
|
||
'SUS' => 'BS',
|
||
'STEEL_SUS' => 'TS',
|
||
];
|
||
|
||
// 셔터박스 prefix 맵 (표준 사이즈만)
|
||
private const SHUTTER_BOX_PREFIXES = [
|
||
'front' => 'CF',
|
||
'lintel' => 'CL',
|
||
'inspection' => 'CP',
|
||
'rear_corner' => 'CB',
|
||
'top_cover' => 'XX',
|
||
'fin_cover' => 'XX',
|
||
];
|
||
|
||
// 연기차단재
|
||
private const SMOKE_PREFIXES = [
|
||
'w50' => 'GI',
|
||
'w80' => 'GI',
|
||
];
|
||
|
||
/**
|
||
* 가이드레일 세부품목의 prefix 결정
|
||
*/
|
||
public function resolveGuideRailPrefix(
|
||
string $partType, // 'finish', 'body', 'c_type', 'd_type', 'extra_finish', 'base'
|
||
string $guideType, // 'wall', 'side'
|
||
string $productCode, // 'KSS01', 'KSE01', ...
|
||
): string
|
||
|
||
/**
|
||
* 하단마감재 세부품목의 prefix 결정
|
||
*/
|
||
public function resolveBottomBarPrefix(
|
||
string $partType, // 'main', 'lbar', 'reinforce', 'extra'
|
||
string $finishMaterial, // 'EGI 1.55T', 'SUS 1.2T'
|
||
string $productCode,
|
||
): string
|
||
|
||
/**
|
||
* 셔터박스 세부품목의 prefix 결정
|
||
*/
|
||
public function resolveShutterBoxPrefix(
|
||
string $partType, // 'front', 'lintel', 'inspection', 'rear_corner', 'top_cover', 'fin_cover'
|
||
bool $isStandardSize, // 500*380인지
|
||
): string
|
||
|
||
/**
|
||
* 연기차단재 세부품목의 prefix 결정
|
||
*/
|
||
public function resolveSmokeBarrierPrefix(string $partType): string
|
||
|
||
/**
|
||
* prefix + 길이(mm) → BD-XX-NN 코드 생성
|
||
*/
|
||
public function buildItemCode(string $prefix, int $lengthMm, ?string $smokeCategory = null): string
|
||
|
||
/**
|
||
* BD-XX-NN 코드 → items.id 조회 (캐시 사용)
|
||
*/
|
||
public function resolveItemId(string $itemCode): ?int
|
||
|
||
/**
|
||
* 길이(mm) → 길이코드 변환 (getSLengthCode 동일)
|
||
*/
|
||
public static function lengthToCode(int $lengthMm, ?string $smokeCategory = null): ?string
|
||
}
|
||
```
|
||
|
||
**의존성**: `App\Models\Items\Item` (코드→ID 조회용)
|
||
|
||
**검증**: 단위 테스트에서 productCode × guideType × partType 전 조합 테스트
|
||
|
||
---
|
||
|
||
#### 2.2 BendingInfoBuilder 확장 — dynamic_bom 생성
|
||
|
||
**수정 파일**:
|
||
- `api/app/Services/Production/BendingInfoBuilder.php`
|
||
|
||
**수정 범위**:
|
||
|
||
1. **build() 메서드 (L29-69)**: 반환값에 `dynamic_bom` 배열 추가
|
||
```
|
||
현재: return assembleBendingInfo(...) // bending_info만
|
||
변경: return [
|
||
'bending_info' => assembleBendingInfo(...),
|
||
'dynamic_bom' => buildDynamicBom(...) // 신규
|
||
]
|
||
```
|
||
|
||
2. **buildDynamicBom() 신규 메서드**: bending_info 생성과 동일한 길이 버킷팅 결과를 사용
|
||
```
|
||
private function buildDynamicBom(
|
||
array $aggregated, // aggregateNodes() 결과
|
||
string $productCode,
|
||
array $materials, // getMaterialMapping() 결과
|
||
PrefixResolver $resolver,
|
||
): array
|
||
```
|
||
|
||
**로직**:
|
||
```
|
||
dynamic_bom = []
|
||
|
||
// 1. 가이드레일 세부품목
|
||
for each guideType (wall, side):
|
||
lengthData = heightLengthData(dimGroups) // 기존 버킷팅 재사용
|
||
for each (length, qty) in lengthData:
|
||
for each partType in [finish, body, c_type, d_type, extra_finish, base]:
|
||
prefix = resolver.resolveGuideRailPrefix(partType, guideType, productCode)
|
||
if prefix is empty: skip
|
||
itemCode = resolver.buildItemCode(prefix, length)
|
||
itemId = resolver.resolveItemId(itemCode)
|
||
dynamic_bom[] = {
|
||
child_item_id: itemId,
|
||
child_item_code: itemCode,
|
||
lot_prefix: prefix,
|
||
part_type: partType의 한글명,
|
||
category: 'guideRail',
|
||
material_type: materials[partType],
|
||
length_mm: length,
|
||
qty: qty
|
||
}
|
||
|
||
// 2. 하단마감재 세부품목
|
||
for each dimGroup:
|
||
[qty3000, qty4000] = bottomBarDistribution(openWidth)
|
||
for each (length, qty) in [(3000, qty3000), (4000, qty4000)]:
|
||
if qty == 0: skip
|
||
for each partType in [main, lbar, reinforce, extra]:
|
||
prefix = resolver.resolveBottomBarPrefix(partType, finishMaterial, productCode)
|
||
... dynamic_bom 추가 ...
|
||
|
||
// 3. 셔터박스 세부품목
|
||
for each dimGroup:
|
||
distribution = shutterBoxDistribution(openWidth)
|
||
for each (length, qty) in distribution:
|
||
if qty == 0: skip
|
||
isStandard = (boxSize == '500*380')
|
||
for each partType in [front, lintel, inspection, rear_corner, top_cover, fin_cover]:
|
||
prefix = resolver.resolveShutterBoxPrefix(partType, isStandard)
|
||
... dynamic_bom 추가 ...
|
||
|
||
// 4. 연기차단재 세부품목
|
||
for each smokeType (w50, w80):
|
||
for each (length, qty) in smokeLengthData:
|
||
prefix = resolver.resolveSmokeBarrierPrefix(smokeType)
|
||
smokeCategory = smokeType == 'w50' ? '연기차단재50' : '연기차단재80'
|
||
itemCode = resolver.buildItemCode(prefix, length, smokeCategory)
|
||
... dynamic_bom 추가 ...
|
||
|
||
return dynamic_bom
|
||
```
|
||
|
||
3. **work_order_items.options 저장 위치 수정**:
|
||
- `WorkOrderService.php` L275-306 (작업지시 품목 복사 로직)에서 build() 반환값의 `dynamic_bom`을 `options.dynamic_bom`에 저장
|
||
|
||
**주의사항**:
|
||
- `aggregateNodes()` L164의 `!isset` 체크: 첫 노드에서만 BOM 메타 추출 → 노드별 BOM이 다를 수 있으므로 주의
|
||
- `bucketToStandardLength()` L862-864: 표준 길이 초과 시 원본 반환 → PrefixResolver.resolveItemId()에서 null 반환 시 경고 로그 + fallback
|
||
- 혼합형 가이드레일: wall + side 각각 독립 dynamic_bom 생성
|
||
|
||
**검증**:
|
||
- 작업지시 생성 API 호출 후 `work_order_items.options` JSON에 `dynamic_bom` 배열 존재 확인
|
||
- dynamic_bom의 각 항목에 `child_item_id`가 NOT NULL인지 확인
|
||
- bending_info의 lengthData와 dynamic_bom의 length_mm/qty가 일치하는지 확인
|
||
|
||
---
|
||
|
||
#### 2.3 DynamicBomValidator DTO 구현
|
||
|
||
**생성 파일**:
|
||
- `api/app/DTOs/Production/DynamicBomEntry.php`
|
||
|
||
**구조**:
|
||
```php
|
||
class DynamicBomEntry
|
||
{
|
||
public function __construct(
|
||
public readonly int $child_item_id,
|
||
public readonly string $child_item_code,
|
||
public readonly string $lot_prefix,
|
||
public readonly string $part_type,
|
||
public readonly string $category, // guideRail, bottomBar, shutterBox, smokeBarrier
|
||
public readonly string $material_type,
|
||
public readonly int $length_mm,
|
||
public readonly int|float $qty,
|
||
) {}
|
||
|
||
public static function fromArray(array $data): self
|
||
public function toArray(): array
|
||
public static function validate(array $data): bool // child_item_id 필수 등
|
||
}
|
||
```
|
||
|
||
**검증**: 단위 테스트에서 필수 필드 누락 시 예외 발생 확인
|
||
|
||
---
|
||
|
||
### 4.4 Phase 3: getMaterials 연동
|
||
|
||
#### 3.1 getMaterials() dynamic_bom 우선 체크
|
||
|
||
**수정 파일**:
|
||
- `api/app/Services/WorkOrderService.php`
|
||
|
||
**수정 위치**: `getMaterials()` L1198 이후
|
||
|
||
**수정 내용**:
|
||
```
|
||
현재 (L1198-1238):
|
||
foreach (workOrderItems as woItem):
|
||
item = woItem.item
|
||
if (item.bom):
|
||
... BOM 순회 ...
|
||
else:
|
||
... item 자체를 자재로 ...
|
||
|
||
변경:
|
||
// Phase 1: dynamic_bom 대상 item_id 일괄 수집
|
||
allDynamicItemIds = []
|
||
foreach (workOrderItems as woItem):
|
||
dynamicBom = woItem.options['dynamic_bom'] ?? null
|
||
if (dynamicBom):
|
||
allDynamicItemIds += array_column(dynamicBom, 'child_item_id')
|
||
|
||
// Phase 2: 배치 조회 (N+1 방지)
|
||
dynamicItems = Item::whereIn('id', array_unique(allDynamicItemIds))
|
||
->get()->keyBy('id')
|
||
|
||
// Phase 3: 유니크 자재 수집
|
||
foreach (workOrderItems as woItem):
|
||
dynamicBom = woItem.options['dynamic_bom'] ?? null
|
||
if (dynamicBom):
|
||
foreach (dynamicBom as bomEntry):
|
||
childItem = dynamicItems[bomEntry['child_item_id']]
|
||
// 합산 키: (item_id, work_order_item_id) 쌍
|
||
key = bomEntry['child_item_id'] . '_' . woItem.id
|
||
uniqueMaterials[key] = {
|
||
item_id: bomEntry['child_item_id'],
|
||
work_order_item_id: woItem.id,
|
||
bom_qty: bomEntry['qty'],
|
||
item: childItem,
|
||
...
|
||
}
|
||
elseif (item.bom):
|
||
... 기존 BOM 로직 (하위 호환) ...
|
||
else:
|
||
... 기존 fallback ...
|
||
```
|
||
|
||
**반환 형식 변경**:
|
||
```
|
||
기존: { stock_lot_id, item_id, lot_no, bom_qty, required_qty, ... }
|
||
추가: { ..., work_order_item_id, lot_prefix, part_type, category }
|
||
```
|
||
|
||
**검증**:
|
||
- dynamic_bom 있는 work_order → 세부품목(BD-RS-43 등) 반환 확인
|
||
- dynamic_bom 없는 work_order → 기존 동작 그대로 (하위 호환)
|
||
- 동일 item_id가 다른 work_order_item에 속한 경우 별도 행으로 반환
|
||
|
||
---
|
||
|
||
#### 3.2 N+1 쿼리 최적화 + uniqueMaterials 합산 단위 변경
|
||
|
||
**수정 파일**: `api/app/Services/WorkOrderService.php`
|
||
|
||
**수정 내용**:
|
||
1. `Item::find()` 개별 호출 → `Item::whereIn()` 배치 조회
|
||
2. `uniqueMaterials` 합산 키를 `item_id` → `(item_id, work_order_item_id)` 쌍으로 변경
|
||
3. StockLot 조회도 `Stock::whereIn()` 배치 처리
|
||
|
||
**기대 효과**: 쿼리 수 30-50회 → 3-5회로 감소
|
||
|
||
**검증**: Laravel Debugbar 또는 DB 쿼리 로그로 쿼리 수 확인
|
||
|
||
---
|
||
|
||
### 4.5 Phase 4: 프론트엔드 연동
|
||
|
||
#### 4.1 자재투입 모달 세부품목 단위 표시
|
||
|
||
**수정 파일**:
|
||
- `react/src/components/production/WorkerScreen/MaterialInputModal.tsx`
|
||
|
||
**현재 상태**: `materialGroups`가 `itemId` 기준 그룹핑 (L102-119). getMaterials 응답이 세부품목을 반환하면 자동으로 세부품목 단위 그룹핑됨.
|
||
|
||
**수정 내용**:
|
||
- 그룹 헤더에 세부품목명(BD-RS-43 등) + part_type(마감재 등) + category(가이드레일 등) 표시
|
||
- 기존 `materialCode`/`materialName` 필드로 충분하나, 카테고리별 시각적 구분 추가
|
||
|
||
**수정 규모**: 소규모 — 그룹 헤더 렌더링 수정
|
||
|
||
**검증**: 자재투입 모달에서 세부품목별 그룹이 표시되고, 각 그룹 내 LOT 선택이 정상 동작
|
||
|
||
---
|
||
|
||
#### 4.2 작업일지 LOT NO 표시 연동
|
||
|
||
**수정 파일**:
|
||
- `react/src/components/production/WorkOrders/documents/bending/GuideRailSection.tsx`
|
||
- 해당 폴더의 다른 Section 컴포넌트 (BottomBarSection, ShutterBoxSection 등)
|
||
|
||
**현재 상태**: LOT NO 컬럼이 `"-"`로 하드코딩
|
||
|
||
**수정 내용**:
|
||
- `getMaterialInputsForItem()` API로 투입 이력 조회
|
||
- lotPrefix + lengthCode 매칭으로 실제 LOT NO 표시
|
||
- 투입 전이면 "-", 투입 후이면 실제 LOT 번호
|
||
|
||
**수정 규모**: 중규모 — 각 Section 컴포넌트에 LOT 조회 로직 추가
|
||
|
||
**검증**: 자재투입 완료 후 작업일지에 실제 LOT NO 표시
|
||
|
||
---
|
||
|
||
### 4.6 Phase 5: 테스트 및 검증
|
||
|
||
#### 5.1 단위 테스트
|
||
|
||
**생성 파일**:
|
||
- `api/tests/Unit/Services/Production/PrefixResolverTest.php`
|
||
- `api/tests/Unit/Services/Production/BendingInfoBuilderDynamicBomTest.php`
|
||
|
||
**테스트 케이스**:
|
||
|
||
| 테스트 | 입력 | 기대 결과 |
|
||
|--------|------|----------|
|
||
| KSS01 벽면형 마감재 4300mm | ('finish', 'wall', 'KSS01') | prefix='RS', code='BD-RS-43' |
|
||
| KSE01 측면형 본체 3000mm | ('body', 'side', 'KSE01') | prefix='SM', code='BD-SM-30' |
|
||
| KTE01 벽면형 본체 (철재) | ('body', 'wall', 'KTE01') | prefix='RT' |
|
||
| 하단마감재 EGI | ('main', 'EGI 1.55T', 'KSE01') | prefix='BE' |
|
||
| 셔터박스 비표준 사이즈 | ('front', false) | prefix='XX' |
|
||
| 연기차단재 W50 3000mm | resolveSmokeBarrierPrefix('w50') | prefix='GI', code='BD-GI-53' |
|
||
| 표준 길이 초과 (4500mm) | buildItemCode('RS', 4500) | 경고 로그 + null 반환 |
|
||
|
||
---
|
||
|
||
#### 5.2 통합 테스트
|
||
|
||
**생성 파일**:
|
||
- `api/tests/Feature/Production/BendingMaterialInputFlowTest.php`
|
||
|
||
**테스트 시나리오**:
|
||
|
||
```
|
||
1. 작업지시 생성 → dynamic_bom 저장 확인
|
||
- Order (KSS01, SUS마감, 오픈높이=4300, 오픈폭=3000)
|
||
- 작업지시 생성 → work_order_items.options.dynamic_bom 확인
|
||
- dynamic_bom에 RS-43, RM-43, RC-43, RD-43 세부품목 존재
|
||
|
||
2. getMaterials → 세부품목 반환 확인
|
||
- getMaterials(workOrderId) 호출
|
||
- 응답에 BD-RS-43, BD-RM-43 등 세부품목 반환
|
||
- 각 세부품목의 StockLot 정보 포함
|
||
|
||
3. 자재투입 → 이력 기록 확인
|
||
- registerMaterialInputForItem() 호출
|
||
- stock_transactions에 OUT 기록
|
||
- work_order_material_inputs에 레코드 생성
|
||
- stock_lots.available_qty 감소
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 컨펌 대기 목록
|
||
|
||
| # | 항목 | 변경 내용 | 영향 범위 | 상태 |
|
||
|---|------|----------|----------|------|
|
||
| 1 | registerMaterialInput API 통일 | 기존 API에 WorkOrderMaterialInput 레코드 생성 추가 | 프론트 호출 호환 유지 | ⏳ |
|
||
| 2 | BendingInfoBuilder.build() 반환값 변경 | 기존 array → { bending_info, dynamic_bom } | WorkOrderService 호출처 수정 필요 | ⏳ |
|
||
| 3 | getMaterials() 로직 변경 | dynamic_bom 우선 체크 + 합산 단위 변경 | MaterialInputModal 응답 형식 변경 | ⏳ |
|
||
|
||
---
|
||
|
||
## 6. 변경 이력
|
||
|
||
| 날짜 | 변경 내용 |
|
||
|------|----------|
|
||
| 2026-02-22 | 문서 초안 작성 |
|
||
| 2026-02-22 | Phase 0 완료: BD-* 22건 등록 + 검증 101/101 통과 |
|
||
| 2026-02-22 | Phase 2 완료: PrefixResolver, BendingInfoBuilder 확장(build→context+bending_info, buildDynamicBomForItem), DynamicBomEntry DTO, OrderService 연동 |
|
||
| 2026-02-22 | Phase 1.1 + 3.1/3.2 완료: registerMaterialInput 통일 (work_order_item_id 분기+fallback+WorkOrderMaterialInput 레코드 생성), getMaterials dynamic_bom 우선체크 + N+1 배치최적화 |
|
||
|
||
---
|
||
|
||
## 7. 참고 문서
|
||
|
||
| 문서 | 경로 |
|
||
|------|------|
|
||
| **분석 기준 문서** | `docs/plans/bending-material-input-mapping-plan.md` |
|
||
| 선생산 재고 계획 | `docs/plans/bending-preproduction-stock-plan.md` |
|
||
| BendingInfoBuilder | `api/app/Services/Production/BendingInfoBuilder.php` |
|
||
| WorkOrderService | `api/app/Services/WorkOrderService.php` |
|
||
| StockService | `api/app/Services/StockService.php` |
|
||
| WorkOrderMaterialInput 모델 | `api/app/Models/Production/WorkOrderMaterialInput.php` |
|
||
| MaterialInputModal | `react/src/components/production/WorkerScreen/MaterialInputModal.tsx` |
|
||
| WorkerScreen actions | `react/src/components/production/WorkerScreen/actions.ts` |
|
||
| Bending types/utils | `react/src/components/production/WorkOrders/documents/bending/` |
|
||
| API 개발 규칙 | `docs/standards/api-rules.md` |
|
||
| 품질 체크리스트 | `docs/standards/quality-checklist.md` |
|
||
|
||
---
|
||
|
||
## 8. 세션 및 메모리 관리 정책 (Serena Optimized)
|
||
|
||
### 8.1 세션 시작 시 (Load Strategy)
|
||
```javascript
|
||
read_memory("bending-lot-pipeline-state") // 1. 상태 파악
|
||
read_memory("bending-lot-pipeline-snapshot") // 2. 사고 흐름 복구
|
||
read_memory("bending-lot-pipeline-active-symbols") // 3. 작업 대상 파악
|
||
```
|
||
|
||
### 8.2 작업 중 관리 (Context Defense)
|
||
| 컨텍스트 잔량 | Action | 내용 |
|
||
|--------------|--------|------|
|
||
| **30% 이하** | Snapshot | `write_memory("bending-lot-pipeline-snapshot", "코드변경+논의요약")` |
|
||
| **20% 이하** | Context Purge | `write_memory("bending-lot-pipeline-active-symbols", "주요 수정 파일/함수")` |
|
||
| **10% 이하** | Stop & Save | 최종 상태 저장 후 세션 교체 권고 |
|
||
|
||
### 8.3 Serena 메모리 구조
|
||
- `bending-lot-pipeline-state`: { phase, progress, next_step, last_decision }
|
||
- `bending-lot-pipeline-snapshot`: 현재까지의 논의 및 코드 변경점 요약
|
||
- `bending-lot-pipeline-rules`: 해당 작업에서 결정된 불변의 규칙들
|
||
- `bending-lot-pipeline-active-symbols`: 현재 수정 중인 파일/심볼 리스트
|
||
|
||
---
|
||
|
||
## 9. 검증 결과
|
||
|
||
> 작업 완료 후 이 섹션에 검증 결과 추가
|
||
|
||
### 9.1 테스트 케이스
|
||
|
||
| 입력값 | 예상 결과 | 실제 결과 | 상태 |
|
||
|--------|----------|----------|------|
|
||
| KSS01 + SUS + 벽면형 + 4300mm | BD-RS-43 (item_id 존재) | | ⏳ |
|
||
| getMaterials (dynamic_bom 있는 WO) | 세부품목 리스트 반환 | | ⏳ |
|
||
| 자재투입 등록 | work_order_material_inputs 레코드 생성 | | ⏳ |
|
||
| getMaterials (dynamic_bom 없는 WO) | 기존 동작 (하위 호환) | | ⏳ |
|
||
|
||
### 9.2 성공 기준 달성 현황
|
||
|
||
| 기준 | 달성 | 비고 |
|
||
|------|:----:|------|
|
||
| dynamic_bom 자동 생성 | ⏳ | Phase 2 완료 후 |
|
||
| getMaterials 세부품목 반환 | ⏳ | Phase 3 완료 후 |
|
||
| 세부품목별 LOT 입력 가능 | ⏳ | Phase 4 완료 후 |
|
||
| 자재투입 이력 100% 기록 | ⏳ | Phase 1 완료 후 |
|
||
| LOT prefix 체계 일치 | ⏳ | Phase 0.2 검증 후 |
|
||
|
||
---
|
||
|
||
## 10. 자기완결성 점검 결과
|
||
|
||
### 10.1 체크리스트 검증
|
||
|
||
| # | 검증 항목 | 상태 | 비고 |
|
||
|---|----------|:----:|------|
|
||
| 1 | 작업 목적이 명확한가? | ✅ | 1.1 배경 |
|
||
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 1.5 성공 기준 |
|
||
| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 2 대상 범위 (13개 태스크) |
|
||
| 4 | 의존성이 명시되어 있는가? | ✅ | 3.2 의존성 맵 |
|
||
| 5 | 참고 파일 경로가 정확한가? | ✅ | 코드 분석 기반 확인 |
|
||
| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 4 상세 작업 내용 |
|
||
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 각 태스크별 검증 항목 |
|
||
| 8 | 모호한 표현이 없는가? | ✅ | 라인 번호, 메서드명, 파일 경로 명시 |
|
||
|
||
### 10.2 새 세션 시뮬레이션 테스트
|
||
|
||
| 질문 | 답변 가능 | 참조 섹션 |
|
||
|------|:--------:|----------|
|
||
| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 |
|
||
| Q2. 어디서부터 시작해야 하는가? | ✅ | 2.1 Phase 0 + 📍 현재 진행 상태 |
|
||
| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 섹션 4 (각 태스크별 수정/생성 파일 명시) |
|
||
| Q4. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 + 각 태스크별 검증 항목 |
|
||
| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 |
|
||
|
||
**결과**: 5/5 통과 → ✅ 자기완결성 확보
|
||
|
||
---
|
||
|
||
*이 문서는 /sc:plan 스킬로 생성되었습니다.*
|