docs: 절곡/품목 관련 신규 계획 문서 추가
- 절곡 정보 자동 생성 계획 (bending-info-auto-generation) - 절곡 자재투입 매핑 GAP 분석 (bending-material-input-mapping) - FG 코드 통합 계획 (fg-code-consolidation) - 품목 재고 관리 계획 (item-inventory-management) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1046
plans/bending-info-auto-generation-plan.md
Normal file
1046
plans/bending-info-auto-generation-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
692
plans/bending-material-input-mapping-plan.md
Normal file
692
plans/bending-material-input-mapping-plan.md
Normal file
@@ -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, 롤백, 개선 권장) |
|
||||||
754
plans/fg-code-consolidation-plan.md
Normal file
754
plans/fg-code-consolidation-plan.md
Normal file
@@ -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행: 가이드레일, 마감재질, 전원, 제어기 */}
|
||||||
|
<div className="grid grid-cols-4 gap-3">
|
||||||
|
{/* 가이드레일 (기존) */}
|
||||||
|
<div>...</div>
|
||||||
|
{/* 마감재질 (새로 추가) */}
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-gray-600 flex items-center gap-1">
|
||||||
|
🔩 마감재질
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={location.finishingType}
|
||||||
|
onValueChange={(value) => handleFieldChange("finishingType", value)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-sm">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{FINISHING_TYPES.map((type) => (
|
||||||
|
<SelectItem key={type.value} value={type.value}>
|
||||||
|
{type.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
{/* 전원 (기존) */}
|
||||||
|
<div>...</div>
|
||||||
|
{/* 제어기 (기존) */}
|
||||||
|
<div>...</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 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 근처
|
||||||
|
<td>{loc.productCode}</td>
|
||||||
|
<td>{loc.finishingType}</td> // 또는 기존 컬럼에 병합
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 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<FGApiResponse>({
|
||||||
|
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 스킬로 생성되었습니다.*
|
||||||
167
plans/item-inventory-management-plan.md
Normal file
167
plans/item-inventory-management-plan.md
Normal file
@@ -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`
|
||||||
Reference in New Issue
Block a user