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

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

693 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# 절곡 세부품목 → 자재투입 → LOT 매핑 통합 개발 계획
> **작성일**: 2026-02-21
> **목적**: 절곡 작업일지의 4대 제품 카테고리(가이드레일/하단마감재/셔터박스/연기차단재) 세부품목을 items 테이블과 연동하고, BOM 기반 자재투입 → LOT 추적 파이프라인 구축
> **기준 문서**: `5130/output/viewBendingWork_UA.php`, `api/app/Services/Production/BendingInfoBuilder.php`, `docs/dev_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/dev_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/dev_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, 롤백, 개선 권장) |