# 재고생산 입력 개편 — 절곡품 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