Files
sam-docs/dev/dev_plans/stock-production-lot-form-plan.md
김보곤 909d7481bd docs: [plans] 재고생산 기획 결정 사항 반영
- LOT 일련번호 suffix 전략 확정 (-001, -002)
- items 매핑: 매핑 테이블(bending_item_mappings) 확정
- 일반 재고생산 모드 분기 제거 (절곡품 전용)
2026-03-17 13:00:35 +09:00

552 lines
20 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-03-17
> **상태**: 기획
> **배경**: 레거시 5130 `lot/list.php`의 절곡품 생산 LOT 등록 방식을 SAM 서비스에 도입
---
## 1. 개요
### 1.1 현재 문제
현재 SAM 서비스의 재고생산 등록은 **일반 수주 입력 폼을 재활용**한다.
품목명/규격을 수동 텍스트로 입력하고, 품목코드를 직접 타이핑해야 한다.
절곡품 생산 현장에서는 **제품-종류-모양&길이** 조합으로 품목을 특정하고 LOT 번호를 자동 부여하는 것이 자연스럽다.
### 1.2 목표
레거시 5130의 `절곡품 생산 LOT 신규 등록` 방식을 SAM 서비스에 도입한다.
- **캐스케이딩 드롭다운**: 품목명 → 종류 → 모양&길이 (연동 필터링)
- **LOT 번호 자동 생성**: `[제품][종류][날짜코드]-[모양&길이코드]`
- **원자재/원단 LOT 연결**: 투입 자재의 추적성(traceability) 확보
- **items 테이블 연동**: 선택 조합에 맞는 `BD-*` 품목을 자동 매핑
---
## 2. LOT 번호 부여법 (레거시 준용)
### 2.1 구조
```
[제품코드][종류코드][날짜코드]-[모양&길이코드]-[일련번호]
1자리 1자리 4자리 2자리 3자리
예시: GI6317-53-001
G = 연기차단재
I = 화이바원단
6 = 2026년
3 = 3월
17 = 17일
53 = W50 × 3000
001 = 일련번호 (같은 날 같은 조합의 순번)
```
### 2.2 날짜 코드 (4자리)
| 구분 | 규칙 | 예시 |
|------|------|------|
| 년 | 끝자리 1자리 | 2026 → `6` |
| 월 | 1~9 그대로, 10=`A`, 11=`B`, 12=`C` | 3월 → `3`, 10월 → `A` |
| 일 | 2자리 zero-pad | 5일 → `05`, 17일 → `17` |
### 2.3 제품 코드 (7종)
| 코드 | 제품명 |
|------|--------|
| `R` | 가이드레일(벽면형) |
| `S` | 가이드레일(측면형) |
| `G` | 연기차단재 |
| `B` | 하단마감재(스크린) |
| `T` | 하단마감재(철재) |
| `L` | L-Bar |
| `C` | 케이스 |
### 2.4 종류 코드 (제품별 종속)
| 코드 | 종류명 | 사용 가능 제품 |
|------|--------|---------------|
| `M` | 본체 | R, S |
| `T` | 본체(철재) | R, S |
| `C` | C형 | R, S |
| `D` | D형 | R, S |
| `S` | SUS(마감) | R, S, B, T |
| `U` | SUS(마감)2 | S |
| `E` | EGI(마감) | R, S, B, T |
| `I` | 화이바원단 | G |
| `A` | 스크린용 | L |
| `F` | 전면부 | C |
| `P` | 점검구 | C |
| `L` | 린텔부 | C |
| `B` | 후면코너부 | C |
### 2.5 모양&길이 코드 (2자리)
| 코드 | 연기차단재용 | 코드 | 일반용 |
|------|-------------|------|--------|
| `53` | W50 × 3000 | `12` | 1219 |
| `54` | W50 × 4000 | `24` | 2438 |
| `83` | W80 × 3000 | `30` | 3000 |
| `84` | W80 × 4000 | `35` | 3500 |
| | | `40` | 4000 |
| | | `41` | 4150 |
| | | `42` | 4200 |
| | | `43` | 4300 |
### 2.6 제품-종류 연동 규칙
```
G(연기차단재) → I(화이바원단)
B(하단마감재스크린) → S(SUS), E(EGI)
T(하단마감재철재) → S(SUS), E(EGI)
L(L-Bar) → A(스크린용)
R(가이드레일벽면) → M(본체), T(본체철재), C(C형), D(D형), S(SUS), E(EGI)
S(가이드레일측면) → M(본체), T(본체철재), C(C형), D(D형), S(SUS), U(SUS2), E(EGI)
C(케이스) → F(전면부), P(점검구), L(린텔부), B(후면코너부)
```
### 2.7 제품+종류 → 원자재(재질) 매핑
| 제품 | 종류 | 원자재 |
|------|------|--------|
| G | I | 화이바원단 |
| B, T | S | SUS 1.2T |
| B, T | E | EGI 1.55T |
| L | A | EGI 1.55T |
| R, S | S, U | SUS 1.2T |
| R, S | M, T, C, D, E | EGI 1.55T |
| C | F, P, L, B | EGI 1.55T |
---
## 3. 화면 설계
### 3.1 입력 폼 구성 (레거시 5130 준용)
```
┌─────────────────────────────────────────────────┐
│ 절곡품 재고생산 등록 │
├─────────────────────────────────────────────────┤
│ 등록일 [2026-03-17 📅] 작성자 [홍길동] │
│ │
│ 생산품 LOT [GI6317-53 ] (자동 생성) │
│ │
│ 품목명 [연기차단재 ▾] 종류 [화이바원단 ▾] │
│ │
│ 모양&길이 [W50×3000 ▾] 수량 [100 ] │
│ │
│ 원자재(철판) LOT [클릭하여 선택] │
│ 원단 LOT(연기차단재) │
│ [클릭하여 선택] │
│ │
│ 메모 [ ] │
├─────────────────────────────────────────────────┤
│ [저장] [취소] │
└─────────────────────────────────────────────────┘
```
### 3.2 UI 동작 흐름
```
1. 품목명 선택
└→ 종류 드롭다운 옵션이 연동 필터링됨
└→ 모양&길이 드롭다운 옵션이 연동 필터링됨 (연기차단재: W50/W80, 기타: 일반)
2. 종류 선택
└→ LOT 번호 자동 갱신
└→ 원자재 재질 자동 결정 (원자재 LOT 모달 필터링에 사용)
3. 모양&길이 선택
└→ LOT 번호 완성
└→ items 테이블에서 매칭 품목 자동 검색 (item_code 매핑)
4. 원자재 LOT 클릭
└→ 모달: 해당 재질의 가용 LOT 목록 표시
└→ 선택 시 LOT 번호 입력
5. 원단 LOT 클릭 (연기차단재만 표시)
└→ 모달: 화이바원단 가용 LOT 목록 표시
6. 저장
└→ orders 테이블에 STOCK 주문 생성
└→ order_items에 매핑된 품목 저장
└→ options에 LOT 정보 저장
```
---
## 4. 백엔드 (API) 작업
### 4.1 신규 API 엔드포인트
#### 4.1.1 절곡 품목 코드맵 조회
```
GET /api/v1/bending/code-map
```
**응답**: 캐스케이딩 드롭다운에 필요한 코드 체계 전체를 반환한다.
프론트엔드에서 하드코딩하지 않고 API에서 제공하여 추후 품목 추가 시 백엔드만 수정한다.
```json
{
"products": [
{ "code": "R", "name": "가이드레일(벽면형)" },
{ "code": "S", "name": "가이드레일(측면형)" },
{ "code": "G", "name": "연기차단재" },
{ "code": "B", "name": "하단마감재(스크린)" },
{ "code": "T", "name": "하단마감재(철재)" },
{ "code": "L", "name": "L-Bar" },
{ "code": "C", "name": "케이스" }
],
"specs": [
{ "code": "M", "name": "본체", "products": ["R", "S"] },
{ "code": "S", "name": "SUS(마감)", "products": ["R", "S", "B", "T"] }
],
"lengths": {
"smoke_barrier": [
{ "code": "53", "name": "W50 × 3000" },
{ "code": "54", "name": "W50 × 4000" },
{ "code": "83", "name": "W80 × 3000" },
{ "code": "84", "name": "W80 × 4000" }
],
"general": [
{ "code": "12", "name": "1219" },
{ "code": "24", "name": "2438" },
{ "code": "30", "name": "3000" },
{ "code": "35", "name": "3500" },
{ "code": "40", "name": "4000" },
{ "code": "41", "name": "4150" },
{ "code": "42", "name": "4200" },
{ "code": "43", "name": "4300" }
]
},
"material_map": {
"G:I": "화이바원단",
"B:S": "SUS 1.2T",
"R:M": "EGI 1.55T"
}
}
```
#### 4.1.2 절곡 품목 매핑 조회
```
GET /api/v1/bending/resolve-item?prod={prodCode}&spec={specCode}&length={lengthCode}
```
**목적**: 드롭다운 선택 조합에 대응하는 `items` 테이블의 실제 품목을 반환한다.
**응답**:
```json
{
"item_id": 15604,
"item_code": "BD-RM-42",
"item_name": "가이드레일(벽면) 본체 4200mm",
"specification": "EGI 1.55T 4200mm",
"unit": "EA",
"unit_price": 0
}
```
**매핑 로직**: `bending_item_mappings` 테이블에서 `prod_code + spec_code + length_code → item_id`로 조회한다.
#### 4.1.3 원자재 LOT 목록 조회
```
GET /api/v1/stock-lots?material={materialName}&status=available
```
**목적**: 원자재(철판) LOT 선택 모달에 표시할 가용 LOT 목록이다.
기존 `StockService`의 LOT 조회를 재활용한다.
**응답**:
```json
[
{
"id": 1,
"lot_no": "RM-20260301-001",
"item_name": "SUS 1.2T",
"qty": 500,
"available_qty": 350,
"receipt_date": "2026-03-01",
"status": "available"
}
]
```
### 4.2 기존 API 수정
#### 4.2.1 OrderService::store() — STOCK 주문 options 확장
현재 `options``production_reason`, `target_stock_qty`만 저장한다.
절곡품 LOT 정보를 추가 저장한다.
```php
// options 구조 확장
'options' => [
'production_reason' => '재고생산',
'target_stock_qty' => 100,
// 절곡품 LOT 정보 (신규)
'bending_lot' => [
'lot_number' => 'GI6317-53',
'prod_code' => 'G',
'spec_code' => 'I',
'length_code' => '53',
'raw_lot_no' => 'RM-20260301-001', // 원자재 LOT
'fabric_lot_no' => 'FB-20260215-003', // 원단 LOT (연기차단재만)
'material' => '화이바원단',
],
]
```
#### 4.2.2 StoreOrderRequest — validation 추가
```php
'options.bending_lot' => 'nullable|array',
'options.bending_lot.lot_number' => 'nullable|string|max:20',
'options.bending_lot.prod_code' => 'nullable|string|max:2',
'options.bending_lot.spec_code' => 'nullable|string|max:2',
'options.bending_lot.length_code' => 'nullable|string|max:2',
'options.bending_lot.raw_lot_no' => 'nullable|string|max:50',
'options.bending_lot.fabric_lot_no' => 'nullable|string|max:50',
```
### 4.3 LOT 일련번호 생성 (백엔드)
같은 날 같은 조합의 LOT가 여러 건 등록될 수 있으므로, 저장 시 백엔드에서 일련번호를 부여한다.
```php
// BendingCodeService::generateLotNumber()
public function generateLotNumber(string $base): string
{
// base = 'GI6317-53'
// orders.options->bending_lot->lot_number 에서 같은 base로 시작하는 건 수 조회
$count = Order::where('tenant_id', $this->tenantId())
->where('order_type_code', Order::TYPE_STOCK)
->where('options->bending_lot->lot_number', 'LIKE', $base . '-%')
->count();
$seq = str_pad($count + 1, 3, '0', STR_PAD_LEFT);
return "{$base}-{$seq}"; // GI6317-53-001
}
```
### 4.4 매핑 테이블 마이그레이션
```php
// bending_item_mappings 테이블
Schema::create('bending_item_mappings', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id');
$table->string('prod_code', 2)->comment('제품코드: R,S,G,B,T,L,C');
$table->string('spec_code', 2)->comment('종류코드: M,S,I,E...');
$table->string('length_code', 2)->comment('모양&길이코드: 53,42...');
$table->unsignedBigInteger('item_id')->comment('매핑된 품목 ID');
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->unique(['tenant_id', 'prod_code', 'spec_code', 'length_code']);
$table->foreign('item_id')->references('id')->on('items');
$table->foreign('tenant_id')->references('id')->on('tenants');
});
```
### 4.5 백엔드 파일 목록
| 파일 | 작업 | 설명 |
|------|------|------|
| `app/Http/Controllers/Api/V1/BendingController.php` | 신규 | 코드맵, 품목매핑, LOT 채번 API |
| `app/Services/BendingCodeService.php` | 신규 | 코드 체계 관리, 품목 매핑, LOT 일련번호 생성 |
| `app/Models/Production/BendingItemMapping.php` | 신규 | 매핑 테이블 모델 |
| `database/migrations/xxx_create_bending_item_mappings_table.php` | 신규 | 매핑 테이블 마이그레이션 |
| `app/Http/Requests/Order/StoreOrderRequest.php` | 수정 | bending_lot validation 추가 |
| `app/Http/Requests/Order/UpdateOrderRequest.php` | 수정 | 동일 |
| `routes/api/v1/production.php` | 수정 | bending 라우트 추가 |
| `app/Swagger/v1/BendingApi.php` | 신규 | Swagger 문서 |
---
## 5. 프론트엔드 (React) 작업
### 5.1 컴포넌트 구조
> 일반 재고생산은 고려하지 않는다. 재고생산 = 절곡품 LOT 입력으로 통일한다.
```
src/components/stocks/
├── StockProductionList.tsx # 기존 (목록)
├── StockProductionDetail.tsx # 기존 (상세) — LOT 정보 표시 추가
├── StockProductionForm.tsx # 전면 교체: BendingLotForm 기반
├── BendingLotForm.tsx # 신규: 절곡품 LOT 등록 폼 (핵심)
├── RawMaterialLotModal.tsx # 신규: 원자재 LOT 선택 모달
├── FabricLotModal.tsx # 신규: 원단 LOT 선택 모달
└── actions.ts # 수정: bending API 함수 추가
```
### 5.2 BendingLotForm 핵심 로직
```typescript
// 캐스케이딩 드롭다운 상태
const [prodCode, setProdCode] = useState(''); // 품목
const [specCode, setSpecCode] = useState(''); // 종류
const [lengthCode, setLengthCode] = useState(''); // 모양&길이
const [lotNumber, setLotNumber] = useState(''); // 자동 생성 LOT
// 품목 변경 → 종류 필터링
useEffect(() => {
const filtered = codeMap.specs.filter(s => s.products.includes(prodCode));
setAvailableSpecs(filtered);
setSpecCode('');
setLengthCode('');
}, [prodCode]);
// LOT 번호 자동 생성 (일련번호는 백엔드에서 부여)
// 프론트에서는 프리뷰용으로 base 부분만 표시
useEffect(() => {
if (prodCode && specCode && lengthCode && regDate) {
const dateCode = generateDateCode(regDate);
setLotNumberBase(`${prodCode}${specCode}${dateCode}-${lengthCode}`);
// 실제 LOT: GI6317-53-001 (suffix는 저장 시 API가 부여)
}
}, [prodCode, specCode, lengthCode, regDate]);
// 날짜코드 생성
function generateDateCode(date: Date): string {
const year = date.getFullYear() % 10;
const month = date.getMonth() + 1;
const day = date.getDate();
const monthCode = month >= 10
? String.fromCharCode(55 + month) // A=10, B=11, C=12
: String(month);
return `${year}${monthCode}${String(day).padStart(2, '0')}`;
}
```
### 5.3 저장 데이터 변환
```typescript
// BendingLotForm → API 전송 데이터
function transformToStockOrder(form: BendingLotFormData): StockOrderFormData {
return {
orderTypeCode: 'STOCK',
memo: form.memo,
remarks: '',
productionReason: '절곡품 재고생산',
targetStockQty: form.quantity,
// bending_lot 정보는 options에 저장
bendingLot: {
lotNumber: form.lotNumber,
prodCode: form.prodCode,
specCode: form.specCode,
lengthCode: form.lengthCode,
rawLotNo: form.rawLotNo,
fabricLotNo: form.fabricLotNo,
material: form.material,
},
items: [{
itemId: form.resolvedItem?.item_id,
itemCode: form.resolvedItem?.item_code || '',
itemName: form.resolvedItem?.item_name || form.productDisplayName,
specification: form.resolvedItem?.specification || '',
quantity: form.quantity,
unit: 'EA',
unitPrice: 0,
}],
};
}
```
### 5.4 프론트엔드 파일 목록
| 파일 | 작업 | 설명 |
|------|------|------|
| `src/components/stocks/BendingLotForm.tsx` | 신규 | 절곡품 LOT 등록 폼 |
| `src/components/stocks/RawMaterialLotModal.tsx` | 신규 | 원자재 LOT 선택 모달 |
| `src/components/stocks/FabricLotModal.tsx` | 신규 | 원단 LOT 선택 모달 |
| `src/components/stocks/StockProductionForm.tsx` | 전면 교체 | BendingLotForm 기반으로 교체 |
| `src/components/stocks/StockProductionDetail.tsx` | 수정 | LOT 정보(bending_lot) 표시 추가 |
| `src/components/stocks/actions.ts` | 수정 | bending API 함수 추가 |
| `src/app/[locale]/(protected)/sales/stocks/page.tsx` | 수정 | 신규 등록 시 BendingLotForm 사용 |
---
## 6. 데이터 흐름
```
사용자 입력 API 처리 DB 저장
┌──────────────┐ ┌─────────────────┐ ┌──────────┐
│ 품목: G │ │ │ │ orders │
│ 종류: I │──resolve-item──→│ items 테이블 │ │ (STOCK) │
│ 길이: 53 │ │ BD-GI-53 조회 │ │ │
│ 수량: 100 │ │ │ │ │
│ 날짜: 3/17 │ │ │ │ │
├──────────────┤ │ │ ├──────────┤
│ LOT: GI6317-53│──store order───→│ OrderService │──────────→│ options │
│ 원자재LOT: xx │ │ ::store() │ │ .bending │
│ 원단LOT: yy │ │ │ │ _lot │
└──────────────┘ └─────────────────┘ └──────────┘
┌─────────────────┐
│ 확정 → 생산지시 │
│ (기존 흐름 유지) │
└─────────────────┘
```
---
## 7. 구현 순서
### Phase 1: 백엔드 API (API 프로젝트)
1. `bending_item_mappings` 마이그레이션 + 모델
2. `BendingCodeService` — 코드 체계 관리, 품목 매핑, LOT 일련번호 생성
3. `BendingController` — 코드맵 조회 + 품목 매핑 + LOT 채번 API
4. `StoreOrderRequest` / `UpdateOrderRequest` 수정 — bending_lot validation
5. 라우트 등록 + Swagger 문서
6. 매핑 데이터 시딩 (기존 `BD-*` 품목과 연결)
### Phase 2: 프론트엔드 (React 프로젝트)
1. `actions.ts` — bending API 함수 추가
2. `BendingLotForm.tsx` — 절곡품 LOT 등록 폼 (핵심)
3. `RawMaterialLotModal.tsx` — 원자재 LOT 선택
4. `FabricLotModal.tsx` — 원단 LOT 선택
5. `StockProductionForm.tsx` — BendingLotForm 기반으로 전면 교체
6. `StockProductionDetail.tsx` — LOT 정보 표시 추가
### Phase 3: 연동 검증
1. 드롭다운 연동 동작 확인
2. LOT 번호 자동 생성 확인
3. items 테이블 매핑 확인
4. 저장 → 상세 → 확정 → 생산지시 흐름 확인
---
## 8. 확정 결정 사항
| 항목 | 결정 | 비고 |
|------|------|------|
| **LOT 중복** | 일련번호 suffix 사용 (`-001`, `-002`...) | 같은 날 같은 조합이면 순번 증가. 백엔드에서 자동 부여 |
| **items 매핑** | **전략 A: 매핑 테이블** (`bending_item_mappings`) | `prod_code + spec_code + length_code → item_id` |
| **일반 재고생산** | 고려하지 않음 | 재고생산 = 절곡품 LOT 입력으로 통일. 모드 분기 불필요 |
| **모양&길이 확장** | API 코드맵에서 관리 | 추후 DB `classifications` 테이블 활용 가능 |
| **원자재 LOT** | 선택 사항 (nullable) | 레거시와 동일하게 선택 권장 |
---
## 관련 문서
- [재고생산관리 기능 설명](../../features/sales/stock-production.md)
- [재고생산관리 API 명세](../../frontend/api-specs/stock-production-api.md)
- [재고생산 변경이력](../changes/20260316_stock_production_order.md)
- [품목 정책](../../rules/item-policy.md)
- [재공품 생산 정책](../../rules/wip-production-policy.md)
---
**최종 업데이트**: 2026-03-17