Files
sam-docs/dev/dev_plans/bending-preproduction-stock-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

838 lines
39 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.

# 절곡품 선생산 → 재고 적재 흐름 통합 개발 계획
> **작성일**: 2026-02-21
> **목적**: 레거시 5130 절곡품(가이드레일/셔터박스/바텀바) 관리를 SAM 기존 재고 시스템에 통합하고, 선생산→재고적재 흐름 구현
> **기준 문서**: `api/app/Services/StockService.php`, `api/app/Services/WorkOrderService.php`, `docs/dev_plans/bending-info-auto-generation-plan.md`
> **상태**: 🔄 Phase 3 완료 (3.5 마이그레이션 제외)
---
## 📍 현재 진행 상태
| 항목 | 내용 |
|------|------|
| **마지막 완료 작업** | 3.5 레거시 데이터 마이그레이션 커맨드 작성 완료 |
| **다음 작업** | 마이그레이션 실행 및 검증 |
| **진행률** | 14/14 (100%) |
| **마지막 업데이트** | 2026-02-21 |
---
## 0. 용어 및 비즈니스 배경
### 0.1 절곡품이란?
- **절곡(Bending)**: 금속판(철판, SUS, EGI)을 절곡기로 구부려 만드는 부품
- **주요 절곡품 3종**:
- **가이드레일**: 방화셔터가 상하로 이동하는 레일 (벽면형/측면형, SUS/EGI 마감)
- **셔터박스(케이스)**: 방화셔터가 말려 들어가는 상부 박스 (양면/밑면/후면 점검구)
- **바텀바(하단마감재)**: 방화셔터 하부를 마감하는 부품 (스크린/철재)
- **연기차단재**: 가이드레일/케이스에 부착하는 연기 차단용 부자재 (W50 레일용, W80 케이스용)
### 0.2 선생산 운영 방식
- 절곡품은 **수주와 무관하게 미리 대량 생산**하여 재고로 비축
- 수주 발생 시 비축된 재고에서 **투입(차감)**하여 사용
- 이유: 절곡 공정은 셋업 시간이 길어 건별 생산보다 일괄 생산이 효율적
### 0.3 SAM 프로젝트 구조
```
SAM/
├── api/ # Laravel 12 REST API (백엔드)
├── react/ # Next.js 15 프론트엔드
├── mng/ # 관리자 패널 (Plain Laravel)
├── 5130/ # 레거시 시스템 소스코드 (참조용)
└── docs/ # 기술 문서
```
### 0.4 SAM 핵심 아키텍처 규칙
- **Service-First**: 비즈니스 로직은 반드시 Service 레이어
- **Multi-tenancy**: 모든 모델에 `BelongsToTenant` trait, tenant_id 필수
- **컬럼 추가 정책**: FK/조인키만 컬럼 추가, 나머지 속성은 `options` JSON 활용
- **FormRequest**: Controller에서 검증 금지, FormRequest 사용
---
## 1. 개요
### 1.1 배경
레거시 5130에서 절곡품(가이드레일, 셔터박스, 바텀바)은 **수주와 무관하게 미리 생산하여 재고로 관리**하는 형태.
수주 발생 시 재고에서 투입(차감)하는 방식으로 운영됨.
SAM에는 이미 재고 관리 시스템(`stocks` + `stock_lots` + `stock_transactions`)이 구축되어 있으나,
**생산 완료 → 재고 입고** 경로가 없어 절곡품 선생산 흐름을 지원하지 못함.
### 1.2 레거시 5130 절곡품 관리 구조
```
[5130 시스템]
┌─────────────────────────────────────────────────────────────┐
│ 절곡품 마스터 (3종) │
│ ├── guiderail 테이블 (가이드레일) │
│ │ ├── 대분류: 스크린/철재 │
│ │ ├── 인정/비인정, 제품코드(KSS01 등) │
│ │ ├── 치수: rail_width × rail_length │
│ │ ├── material_summary (소요자재량 JSON) │
│ │ └── bending_components (절곡 구성품) │
│ ├── shutterbox 테이블 (셔터박스) │
│ │ ├── 점검구 형태: 양면/밑면/후면 │
│ │ └── 치수: box_width × box_height │
│ └── bottombar 테이블 (바텀바/하단마감재) │
│ ├── 대분류: 스크린/철재 │
│ └── 치수: bar_width × bar_height │
│ │
│ 재고 관리 │
│ ├── lot 테이블 (생산 LOT) │
│ │ ├── 3코드 식별: prod + spec + slength │
│ │ ├── lot_number, surang(수량), rawLot(원자재LOT) │
│ │ └── 재고 = SUM(lot.surang) - SUM(bending_work_log.qty) │
│ └── bending_work_log 테이블 (사용 이력) │
│ └── quantity, reg_date, lot_no │
└─────────────────────────────────────────────────────────────┘
```
### 1.3 SAM 현재 상태 (AS-IS)
```
[수주 기반 흐름만 존재]
Order(수주) ──→ WorkOrder(생산지시) ──→ 자재투입 ──→ 완료 ──→ Shipment(출하)
│ │ │
│ sales_order_id 필수 │ 재고차감 │ ⚠️ 재고입고 없이
│ (비즈니스 로직상) │ (기존 OK) │ 바로 출하
[구매입고 흐름 (별도)]
Receiving(입고) ──→ StockService::increaseFromReceiving() (라인 241)
│ Stock + StockLot 생성
│ StockTransaction(IN, receiving)
└─ FIFO 순서 부여
```
### 1.4 목표 흐름 (TO-BE)
```
[선생산 흐름 (신규)]
선생산 작업지시 ──→ 자재투입 ──→ 생산완료
│ sales_order_id = NULL │
│ mode = 'manual' (프론트) │
⭐ 재고 입고 (신규)
StockService::increaseFromProduction()
Stock + StockLot 생성
StockTransaction(IN, production_output)
[완성품 재고 적재]
LOT 추적, FIFO 관리
[수주 발생 시]
재고 확인 → reserve() → 부족분만 생산지시
[기존 수주 기반 흐름 (변경 없음)]
Order ──→ WorkOrder ──→ 완료 ──→ Shipment (기존 유지)
```
### 1.5 핵심 설계 결정
```
┌─────────────────────────────────────────────────────────────────┐
│ 🎯 설계 원칙 │
├─────────────────────────────────────────────────────────────────┤
│ 1. 기존 재고 시스템(stocks/stock_lots/stock_transactions) 재활용 │
│ 2. Receiving은 구매입고 전용 유지 → 생산입고는 직접 StockService │
│ 3. 멀티테넌시 정책: FK만 컬럼, 나머지는 options JSON │
│ 4. items.options 체계 활용 (production_source, lot_managed 등) │
│ 5. 절곡품 전용 페이지 불필요 → 기존 재고현황에 필터 추가 │
└─────────────────────────────────────────────────────────────────┘
```
### 1.6 변경 승인 정책
| 분류 | 예시 | 승인 |
|------|------|------|
| ✅ 즉시 가능 | 상수 추가, 필터 파라미터 추가, options JSON 활용 | 불필요 |
| ⚠️ 컨펌 필요 | 신규 메서드 추가, 비즈니스 로직 분기, 프론트 UI 변경 | **필수** |
| 🔴 금지 | 기존 입출고 로직 변경, stocks 테이블 구조 변경, 기존 API 스펙 변경 | 별도 협의 |
### 1.7 준수 규칙
- `CLAUDE.md` - Service-First, FormRequest, BelongsToTenant
- `SAM_QUICK_REFERENCE.md` - API 규칙
- `docs/dev_plans/bending-info-auto-generation-plan.md` - BendingInfoBuilder 참조
- `docs/dev_plans/bending-worklog-reimplementation-plan.md` - 프론트 절곡 컴포넌트 참조
---
## 2. 대상 범위
### 2.1 Phase 1: 재고 입고 기반 구축 (백엔드)
| # | 작업 항목 | 상태 | 영향 파일 |
|---|----------|:----:|----------|
| 1.1 | StockTransaction REASON 상수 추가 | ✅ | `api/app/Models/Tenants/StockTransaction.php` (라인 41-57) |
| 1.2 | StockLot에 work_order_id 컬럼 추가 | ✅ | `api/database/migrations/` (신규), `api/app/Models/Tenants/StockLot.php` |
| 1.3 | StockService::increaseFromProduction() 구현 | ✅ | `api/app/Services/StockService.php` (라인 241 참조) |
| 1.4 | WorkOrderService 완료 처리 분기 로직 | ✅ | `api/app/Services/WorkOrderService.php` (라인 563-593) |
### 2.2 Phase 2: 선생산 작업지시 흐름 (백엔드 + 프론트)
| # | 작업 항목 | 상태 | 영향 파일 |
|---|----------|:----:|----------|
| 2.1 | 수주 없는 작업지시 API 보완 | ✅ | 이미 지원됨 (sales_order_id nullable, items 직접 전달 가능) |
| 2.2 | items.options 기반 비즈니스 로직 분기 | ✅ | Phase 1에서 shouldStockIn()으로 구현 완료 |
| 2.3 | 작업지시 생성 프론트 UI 보완 (manual 모드) | ✅ | `react/.../WorkOrderCreate.tsx` + `actions.ts` (품목 검색/추가 UI, items 파라미터) |
| 2.4 | 재고현황 item_category 필터 추가 (API) | ✅ | `api/app/Services/StockService.php`, `StockController.php` |
| 2.5 | 재고현황 절곡품 필터 추가 (프론트) | ✅ | `react/.../StockStatusList.tsx` + `actions.ts` (카테고리 필터 드롭다운) |
### 2.3 Phase 3: 수주 연동 고도화
| # | 작업 항목 | 상태 | 영향 파일 |
|---|----------|:----:|----------|
| 3.1 | 수주의 절곡 BOM 품목별 재고 확인 API | ✅ | `api/app/Services/OrderService.php`, `OrderController.php`, `routes/api/v1/sales.php` |
| 3.2 | 가용 재고 자동 예약(reserve) 로직 | ✅ | 기존 `reserveForOrder()` (라인 639-642)에서 이미 처리됨 |
| 3.3 | 부족분 수동 처리 (사용자 결정) | ✅ | 프론트에서 부족 현황 표시 → 사용자가 수동으로 선생산 작업지시 생성 |
| 3.4 | 수주화면 절곡 재고 현황 표시 (프론트) | ✅ | `react/src/components/orders/actions.ts`, `orders/index.ts`, `order-management-sales/[id]/page.tsx` |
| 3.5 | 5130 레거시 데이터 마이그레이션 | ⏳ | `api/database/seeders/` 또는 마이그레이션 스크립트 (별도 진행) |
---
## 3. 작업 절차
### 3.1 Phase 1 상세 절차
```
Step 1.1: StockTransaction REASON 상수 추가
├── 파일: api/app/Models/Tenants/StockTransaction.php
├── 위치: 라인 49 (REASON_ORDER_CANCEL 다음)
├── 추가: const REASON_PRODUCTION_OUTPUT = 'production_output';
├── REASONS 배열에도 추가 (라인 51-57)
└── 검증: 모델 상수 선언 확인
Step 1.2: StockLot에 work_order_id 컬럼 추가
├── 마이그레이션 파일 생성
│ └── stock_lots 테이블에 work_order_id (nullable, FK → work_orders.id) 추가
│ └── 위치: receiving_id (라인 47) 다음
├── StockLot 모델 수정 (api/app/Models/Tenants/StockLot.php)
│ ├── fillable에 'work_order_id' 추가 (라인 15-34)
│ └── workOrder() 관계 추가: belongsTo(WorkOrder::class)
├── 멀티테넌시 정책: work_order_id는 FK이므로 컬럼 추가 정당
└── 검증: migrate:status, 모델 관계 확인
Step 1.3: StockService::increaseFromProduction() 구현
├── 파일: api/app/Services/StockService.php
├── 기존 increaseFromReceiving() (라인 241-314) 참고하여 구현
│ ├── getOrCreateStock() 재사용 (라인 423-466)
│ ├── getNextFifoOrder() 재사용 (라인 474)
│ ├── StockLot 생성 (work_order_id 참조, receiving_id는 null)
│ ├── Stock.refreshFromLots() 호출 (Stock.php 라인 149-164)
│ ├── recordTransaction() 호출 (라인 1232)
│ └── logStockChange() 호출 (라인 1274)
├── 차이점: receiving_id 대신 work_order_id 사용, supplier 관련 필드 null
├── LOT 번호: WorkOrderService::generateLotNo() (라인 845-866) 에서 생성한 것 수신
└── 검증: 단위 테스트 (입고 후 재고량 증가 확인)
Step 1.4: WorkOrderService 완료 처리 분기 로직
├── 파일: api/app/Services/WorkOrderService.php
├── 수정 위치: updateStatus() 라인 591-593
│ 현재 코드:
│ if ($status === WorkOrder::STATUS_COMPLETED) {
│ $this->createShipmentFromWorkOrder($workOrder, $tenantId, $userId);
│ }
│ 변경:
│ if ($status === WorkOrder::STATUS_COMPLETED) {
│ if ($workOrder->sales_order_id) {
│ $this->createShipmentFromWorkOrder($workOrder, $tenantId, $userId);
│ } else {
│ $this->stockInFromProduction($workOrder);
│ }
│ }
├── saveItemResults() (라인 805-840)는 양쪽 모두 실행됨 (라인 563-568, 분기 전에 호출)
├── generateLotNo() (라인 845-866) 에서 LOT 번호 자동 생성 (KD-SA-YYMMDD-NN 형식)
└── 검증: 선생산 WO 완료 시 재고 증가 확인, 기존 수주 WO는 변경 없음
```
### 3.2 Phase 2 상세 절차
```
Step 2.1: 수주 없는 작업지시 API 보완
├── WorkOrderService::store() 메서드 확인
│ └── sales_order_id 없이도 items 직접 전달 가능 (기존 경로 활용)
├── work_orders.sales_order_id는 DB에서 이미 nullable
├── 프론트: WorkOrderCreate.tsx의 RegistrationMode (라인 52)
│ └── 현재: type RegistrationMode = 'linked' | 'manual'
│ └── 'manual' 선택 시 수주 연동 없이 생성 가능
│ └── ⚠️ 주의: 'source_type' 필드는 현재 존재하지 않음 → 필요시 신규 추가
└── 검증: Postman으로 수주 없는 작업지시 생성 테스트
Step 2.2: items.options 기반 비즈니스 로직 분기
├── Item.options 참조 위치 정리
│ ├── production_source: 'purchased' | 'self_produced' | 'both'
│ ├── lot_managed: boolean
│ └── consumption_method: 'auto' | 'manual' | 'none'
├── 생산완료 시: production_source === 'self_produced' && lot_managed → 재고 입고
├── 자재투입 시: consumption_method에 따른 차감 방식 분기
└── 검증: 절곡 품목의 options 값 시더 데이터 확인
Step 2.3: 작업지시 생성 프론트 UI 보완
├── 파일: react/src/components/production/WorkOrders/WorkOrderCreate.tsx
├── 현재 manual 모드 UI (라인 278-305):
│ └── RadioGroup에 'linked' | 'manual' 선택지, Label: "수동 등록 (재고생산)"
├── 보완 필요:
│ ├── 품목 검색/선택 UI (items 마스터에서 BENDING 카테고리 필터)
│ ├── 수량 입력
│ └── 공정 선택 (절곡 공정 기본 선택)
├── 생산완료 버튼 UI 변경 (선생산 WO: "재고 입고" / 수주 WO: "출하")
└── 검증: 프론트에서 선생산 작업지시 생성 → 완료 → 재고 확인
Step 2.4: 재고현황 item_category 필터 추가 (API)
├── 파일: api/app/Services/StockService.php
├── index() 메서드 (라인 45) 파라미터에 item_category 추가
│ └── whereHas('item', fn($q) => $q->where('item_category', $category))
├── StockController 파라미터 바인딩
└── 검증: API 호출로 BENDING 카테고리 필터링 확인
Step 2.5: 재고현황 절곡품 필터 추가 (프론트)
├── 파일: react/src/components/material/StockStatus/StockStatusList.tsx
├── 관련 파일:
│ ├── StockStatusDetail.tsx (상세)
│ ├── stockStatusConfig.ts (설정)
│ ├── actions.ts (API 호출)
│ └── types.ts (타입 정의)
├── 카테고리 탭 또는 드롭다운 추가
│ └── 전체 | 원자재 | 절곡품(BENDING) | 부자재 | 소모품
├── API 호출 시 item_category 파라미터 전달
└── 검증: 절곡품 필터 적용하여 재고 목록 확인
```
### 3.3 Phase 3 상세 절차
```
Step 3.1: 수주 확정 시 재고 자동 확인
├── OrderService::confirmOrder() 또는 createProductionOrder() 수정
│ ├── BOM에서 절곡 품목 추출 (item_category === 'BENDING')
│ ├── 각 품목의 가용 재고 조회: StockService::getAvailableStock() (라인 796)
│ └── 재고 현황 반환 (충족/부족 품목별)
├── 프론트에 재고 확인 결과 표시
└── 검증: 수주 확정 시 재고 현황 표시 확인
Step 3.2: 가용 재고 자동 예약
├── 기존 메서드 활용:
│ ├── StockService::reserve() (라인 832)
│ └── StockService::releaseReservation() (라인 948)
├── 예약 시점: 수주 확정 시 자동 예약 (사용자 확인 후)
├── 예약 해제: 수주 취소 시 releaseReservation()
└── 검증: 예약 후 available_qty 감소 확인
Step 3.3: 부족분 자동 생산지시
├── 수주 확정 시 재고 부족 품목에 대해 자동 생산지시 생성
│ └── createProductionOrder()에 부족 수량만 반영
├── 또는 수동: 부족 품목 목록을 사용자에게 표시 → 선생산 지시 유도
└── 검증: 재고 10개, 필요 15개 → 5개만 생산지시 확인
Step 3.4: 수주화면 재고 현황 표시
├── 수주 상세/편집 화면에 절곡 품목별 재고 현황 표시
│ └── 품목명 | 필요수량 | 가용재고 | 부족수량
└── 검증: UI 렌더링 확인
Step 3.5: 5130 레거시 데이터 마이그레이션
├── lot 테이블 → stocks + stock_lots 매핑
│ ├── prod+spec+slength → items.code (BD-* 패턴) 매핑
│ ├── surang → stock_lots.qty
│ └── rawLot → stock_lots.options (원자재 LOT 추적)
├── bending_work_log → stock_transactions 매핑
│ └── quantity → stock_transactions (TYPE_OUT)
├── guiderail/shutterbox/bottombar → items 테이블 매핑
│ └── item_category = 'BENDING', item_type = 'PT'
└── 검증: 마이그레이션 전후 재고량 일치 확인
```
---
## 4. 상세 작업 내용
### 4.1 현재 DB 스키마 (수정 대상)
#### stocks 테이블 (`2025_12_26_132806_create_stocks_table.php`)
```
id, tenant_id, item_id, item_code, item_name, item_type,
specification, unit, stock_qty, safety_stock,
reserved_qty, available_qty, lot_count, oldest_lot_date,
location, status, last_receipt_date, last_issue_date,
created_by, updated_by, timestamps, softDeletes, deleted_by
```
#### stock_lots 테이블 (`2025_12_26_132842_create_stock_lots_table.php`)
```
id, tenant_id, stock_id(FK→stocks), lot_no, fifo_order(default:1),
receipt_date, qty(decimal 15,3), reserved_qty, available_qty,
unit(default:'EA'), supplier, supplier_lot, po_number,
location, status(default:'available'), receiving_id(nullable),
created_by, updated_by, timestamps, softDeletes, deleted_by
인덱스: tenant_id, stock_id, lot_no, status, (stock_id+fifo_order) 복합
유니크: (tenant_id, stock_id, lot_no)
```
#### stock_transactions 테이블 (`2026_01_29_000001_create_stock_transactions_table.php`)
```
id, tenant_id, stock_id, stock_lot_id, type(IN/OUT/RESERVE/RELEASE),
qty, balance_qty, reference_type, reference_id, lot_no,
reason, remark, item_code, item_name, created_by, timestamps
```
### 4.2 현재 코드 레퍼런스 (라인번호 포함)
#### StockTransaction 상수 (`api/app/Models/Tenants/StockTransaction.php`)
```php
// 라인 25-31: TYPE 상수
const TYPE_IN = 'IN'; // 라인 25
const TYPE_OUT = 'OUT'; // 라인 27
const TYPE_RESERVE = 'RESERVE'; // 라인 29
const TYPE_RELEASE = 'RELEASE'; // 라인 31
// 라인 41-57: REASON 상수
const REASON_RECEIVING = 'receiving'; // 라인 41
const REASON_WORK_ORDER_INPUT = 'work_order_input'; // 라인 43
const REASON_SHIPMENT = 'shipment'; // 라인 45
const REASON_ORDER_CONFIRM = 'order_confirm'; // 라인 47
const REASON_ORDER_CANCEL = 'order_cancel'; // 라인 49
const REASONS = [ ... ]; // 라인 51-57
```
#### StockService 주요 메서드 (`api/app/Services/StockService.php`)
```
라인 45: index(array $params): LengthAwarePaginator
라인 109: stats(): array
라인 159: show(int $id): Item
라인 176: findByItemCode(string $itemCode): ?Item
라인 192: statsByItemType(): array
라인 241: increaseFromReceiving(Receiving $receiving): StockLot ← 참조 대상
라인 325: adjustFromReceiving(Receiving $receiving, float $newQty): void
라인 423: getOrCreateStock(int $itemId, ?Receiving $receiving = null): Stock ← 재사용
라인 474: getNextFifoOrder(int $stockId): int ← 재사용
라인 493: decreaseFIFO(int $itemId, float $qty, string $reason, int $referenceId): array
라인 618: decreaseFromLot(int $stockLotId, float $qty, string $reason, int $referenceId): array
라인 710: increaseToLot(int $stockLotId, float $qty, string $reason, int $referenceId): array
라인 796: getAvailableStock(int $itemId): ?array
라인 832: reserve(int $itemId, float $qty, int $orderId): void
라인 948: releaseReservation(int $itemId, float $qty, int $orderId): void
라인 1050: reserveForOrder($orderItems, int $orderId): void
라인 1071: releaseReservationForOrder($orderItems, int $orderId): void
라인 1099: decreaseForShipment(int $itemId, float $qty, int $shipmentId, ?int $stockLotId = null): array
라인 1232: [private] recordTransaction(...)
라인 1274: [private] logStockChange(...)
```
#### WorkOrderService 완료 처리 (`api/app/Services/WorkOrderService.php`)
```php
// 라인 563-568: completed 케이스 (saveItemResults 호출)
case WorkOrder::STATUS_COMPLETED:
$workOrder->started_at = $workOrder->started_at ?? now();
$workOrder->completed_at = now();
$this->saveItemResults($workOrder, $resultData, $userId);
break;
// 라인 591-593: 완료 후 출하 자동 생성 (← 여기에 분기 삽입)
if ($status === WorkOrder::STATUS_COMPLETED) {
$this->createShipmentFromWorkOrder($workOrder, $tenantId, $userId);
}
// 라인 606: 출하 생성 메서드
private function createShipmentFromWorkOrder(WorkOrder $workOrder, int $tenantId, int $userId): ?Shipment
// 라인 805: 결과 데이터 저장 (LOT 번호 생성 포함)
private function saveItemResults(WorkOrder $workOrder, ?array $resultData, int $userId): void
// 라인 845-866: LOT 번호 생성
private function generateLotNo(WorkOrder $workOrder): string
// 패턴: KD-SA-YYMMDD-NN (예: KD-SA-260221-01)
```
#### Stock 모델 refreshFromLots (`api/app/Models/Tenants/Stock.php`)
```php
// 라인 149-164
public function refreshFromLots(): void
{
$lots = $this->lots()->where('status', '!=', 'used')->get();
$this->lot_count = $lots->count();
$this->stock_qty = $lots->sum('qty');
$this->reserved_qty = $lots->sum('reserved_qty');
$this->available_qty = $lots->sum('available_qty');
$oldestLot = $lots->sortBy('receipt_date')->first();
$this->oldest_lot_date = $oldestLot?->receipt_date;
$this->last_receipt_date = $lots->max('receipt_date');
$this->status = $this->calculateStatus();
$this->save();
}
```
### 4.3 increaseFromReceiving() 실제 코드 (참조용)
신규 `increaseFromProduction()` 구현 시 아래 코드를 기반으로 작성:
```php
// api/app/Services/StockService.php 라인 241-314
public function increaseFromReceiving(Receiving $receiving): StockLot
{
if (! $receiving->item_id) {
throw new \Exception(__('error.stock.item_id_required'));
}
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($receiving, $tenantId, $userId) {
$stock = $this->getOrCreateStock($receiving->item_id, $receiving);
$fifoOrder = $this->getNextFifoOrder($stock->id);
$stockLot = new StockLot;
$stockLot->tenant_id = $tenantId;
$stockLot->stock_id = $stock->id;
$stockLot->lot_no = $receiving->lot_no;
$stockLot->fifo_order = $fifoOrder;
$stockLot->receipt_date = $receiving->receiving_date;
$stockLot->qty = $receiving->receiving_qty;
$stockLot->reserved_qty = 0;
$stockLot->available_qty = $receiving->receiving_qty;
$stockLot->unit = $receiving->order_unit ?? 'EA';
$stockLot->supplier = $receiving->supplier; // ← 생산입고: null
$stockLot->supplier_lot = $receiving->supplier_lot; // ← 생산입고: null
$stockLot->po_number = $receiving->order_no; // ← 생산입고: null
$stockLot->location = $receiving->receiving_location;
$stockLot->status = 'available';
$stockLot->receiving_id = $receiving->id; // ← 생산입고: null, work_order_id 대신 사용
$stockLot->created_by = $userId;
$stockLot->updated_by = $userId;
$stockLot->save();
$stock->refreshFromLots();
$this->recordTransaction(
stock: $stock,
type: StockTransaction::TYPE_IN,
qty: $receiving->receiving_qty,
reason: StockTransaction::REASON_RECEIVING, // ← 생산입고: REASON_PRODUCTION_OUTPUT
referenceType: 'receiving', // ← 생산입고: 'work_order'
referenceId: $receiving->id, // ← 생산입고: $workOrder->id
lotNo: $receiving->lot_no,
stockLotId: $stockLot->id
);
$this->logStockChange(...);
return $stockLot;
});
}
```
### 4.4 increaseFromProduction() 구현 설계
```php
/**
* 생산 완료 시 완성품 재고 입고
* increaseFromReceiving()을 기반으로 구현
*
* @param WorkOrder $workOrder 선생산 작업지시
* @param WorkOrderItem $woItem 작업지시 품목
* @param float $goodQty 양품 수량 (saveItemResults에서 기록)
* @param string $lotNo LOT 번호 (generateLotNo에서 생성)
*/
public function increaseFromProduction(
WorkOrder $workOrder,
WorkOrderItem $woItem,
float $goodQty,
string $lotNo
): StockLot {
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($workOrder, $woItem, $goodQty, $lotNo, $tenantId, $userId) {
// 1. Stock 조회 또는 생성
// getOrCreateStock()의 두 번째 파라미터(Receiving)는 null
// → specification, unit은 Item에서 가져옴
$stock = $this->getOrCreateStock($woItem->item_id);
// 2. FIFO 순서
$fifoOrder = $this->getNextFifoOrder($stock->id);
// 3. StockLot 생성
$stockLot = new StockLot;
$stockLot->tenant_id = $tenantId;
$stockLot->stock_id = $stock->id;
$stockLot->lot_no = $lotNo;
$stockLot->fifo_order = $fifoOrder;
$stockLot->receipt_date = now()->toDateString();
$stockLot->qty = $goodQty;
$stockLot->reserved_qty = 0;
$stockLot->available_qty = $goodQty;
$stockLot->unit = $woItem->unit ?? 'EA';
$stockLot->supplier = null; // 구매입고 전용 필드
$stockLot->supplier_lot = null;
$stockLot->po_number = null;
$stockLot->location = null;
$stockLot->status = 'available';
$stockLot->receiving_id = null; // 구매입고가 아님
$stockLot->work_order_id = $workOrder->id; // ★ 생산입고 참조
$stockLot->created_by = $userId;
$stockLot->updated_by = $userId;
$stockLot->save();
// 4. Stock 합계 갱신
$stock->refreshFromLots();
// 5. 거래 이력 기록
$this->recordTransaction(
stock: $stock,
type: StockTransaction::TYPE_IN,
qty: $goodQty,
reason: StockTransaction::REASON_PRODUCTION_OUTPUT,
referenceType: 'work_order',
referenceId: $workOrder->id,
lotNo: $lotNo,
stockLotId: $stockLot->id
);
// 6. 감사 로그
$this->logStockChange(
stock: $stock,
action: 'production_in',
details: [
'work_order_id' => $workOrder->id,
'work_order_item_id' => $woItem->id,
'qty' => $goodQty,
'lot_no' => $lotNo,
]
);
return $stockLot;
});
}
```
### 4.5 WorkOrderService 완료 분기 구현 설계
```php
// 라인 591-593 변경: updateStatus() 내부
if ($status === WorkOrder::STATUS_COMPLETED) {
if ($workOrder->sales_order_id) {
// 기존 로직: 수주 연동 → 출하 자동 생성
$this->createShipmentFromWorkOrder($workOrder, $tenantId, $userId);
} else {
// 신규 로직: 선생산 → 재고 입고
$this->stockInFromProduction($workOrder);
}
}
// 신규 private 메서드
private function stockInFromProduction(WorkOrder $workOrder): void
{
foreach ($workOrder->items as $woItem) {
if ($this->shouldStockIn($woItem)) {
$resultData = $woItem->options['result'] ?? [];
$goodQty = $resultData['good_qty'] ?? $woItem->quantity;
$lotNo = $resultData['lot_no'] ?? '';
if ($goodQty > 0 && $lotNo) {
$this->stockService->increaseFromProduction(
$workOrder, $woItem, $goodQty, $lotNo
);
}
}
}
}
private function shouldStockIn(WorkOrderItem $woItem): bool
{
$item = $woItem->item;
$options = $item->options ?? [];
return ($options['production_source'] ?? null) === 'self_produced'
&& ($options['lot_managed'] ?? false) === true;
}
```
### 4.6 데이터 매핑 (5130 → SAM)
#### 절곡품 마스터 매핑
| 5130 | SAM | 비고 |
|------|-----|------|
| guiderail.model_name | items.code (BD-가이드레일-*) | item_category=BENDING |
| guiderail.rail_width × rail_length | items.options.dimensions | JSON |
| guiderail.material_summary | items.options.material_summary | JSON |
| guiderail.finishing_type | items.options.finishing_type | JSON |
| shutterbox.box_width × box_height | items.code (BD-케이스-*) | 치수 코드화 |
| bottombar.bar_width × bar_height | items.code (BD-하단마감재-*) | 치수 코드화 |
#### 재고 매핑
| 5130 | SAM | 비고 |
|------|-----|------|
| lot.lot_number | stock_lots.lot_no | 1:1 |
| lot.surang | stock_lots.qty | 생산 수량 |
| lot.prod+spec+slength | items.code → stocks.item_id | 3코드→품목코드 변환 |
| lot.rawLot | stock_lots.options.raw_lot | JSON |
| lot.fabric_lot | stock_lots.options.fabric_lot | JSON |
| bending_work_log.quantity | stock_transactions.qty (TYPE_OUT) | 사용 이력 |
#### 3코드 → 품목코드 변환 규칙
| prod | spec | slength | SAM item_code |
|------|------|---------|---------------|
| R(벽면형) | S(SUS) | 53(W50x3000) | BD-가이드레일-벽면형-SUS-W50x3000 |
| R(벽면형) | E(EGI) | 84(W80x4000) | BD-가이드레일-벽면형-EGI-W80x4000 |
| C(케이스) | M(본체) | 30(3000) | BD-케이스-본체-3000 |
| B(하단마감재스크린) | A(스크린용) | 30(3000) | BD-하단마감재-스크린-3000 |
---
## 5. 컨펌 대기 목록
| # | 항목 | 변경 내용 | 영향 범위 | 상태 |
|---|------|----------|----------|------|
| C1 | StockLot에 work_order_id 컬럼 추가 | DB 마이그레이션 | stock_lots 테이블 | ⚠️ 컨펌 필요 |
| C2 | WorkOrderService 완료 로직 분기 | 비즈니스 로직 변경 | 생산 완료 프로세스 | ⚠️ 컨펌 필요 |
| C3 | Phase 3 수주→재고 자동 매칭 설계 | 신규 비즈니스 프로세스 | OrderService | ⚠️ Phase 3 착수 전 별도 협의 |
---
## 6. 변경 이력
| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|------|------|----------|------|------|
| 2026-02-21 | - | 문서 초안 작성 | - | - |
| 2026-02-21 | 보완 | 용어설명, 파일경로 수정, 코드 레퍼런스 추가, DB 스키마 추가 | - | - |
| 2026-02-21 | Phase 1 구현 | 1.1~1.4 전체 완료 | StockTransaction, StockLot, StockService, WorkOrderService | ✅ |
---
## 7. 참고 문서
### 직접 관련 문서
- `docs/dev_plans/bending-info-auto-generation-plan.md` - BendingInfoBuilder 자동 생성 계획
- `docs/dev_plans/bending-worklog-reimplementation-plan.md` - 절곡 작업일지 프론트 재구현 (완료)
- `docs/projects/legacy-5130/04_PRODUCTION.md` - 레거시 생산 시스템 분석
### 핵심 코드 파일 (⚠️ 경로 주의: Models는 Tenants 네임스페이스)
**백엔드 서비스**:
- `api/app/Services/StockService.php` - 재고 서비스 (increaseFromReceiving 라인 241)
- `api/app/Services/WorkOrderService.php` - 작업지시 서비스 (updateStatus 라인 521, saveItemResults 라인 805)
- `api/app/Services/OrderService.php` - 수주 서비스 (createProductionOrder)
- `api/app/Services/Production/BendingInfoBuilder.php` - 절곡 정보 자동 생성
**백엔드 모델** (⚠️ `Models/Tenants/` 경로):
- `api/app/Models/Tenants/Stock.php` - 재고 모델 (refreshFromLots 라인 149)
- `api/app/Models/Tenants/StockLot.php` - 재고 LOT 모델 (fillable 라인 15-34)
- `api/app/Models/Tenants/StockTransaction.php` - 재고 거래 이력 모델 (상수 라인 25-57)
**DB 마이그레이션**:
- `api/database/migrations/2025_12_26_132806_create_stocks_table.php`
- `api/database/migrations/2025_12_26_132842_create_stock_lots_table.php`
- `api/database/migrations/2026_01_29_000001_create_stock_transactions_table.php`
### 프론트 코드 파일
- `react/src/components/production/WorkOrders/WorkOrderCreate.tsx` - 작업지시 생성 (RegistrationMode 라인 52, manual UI 라인 278-305)
- `react/src/components/material/StockStatus/StockStatusList.tsx` - 재고 현황 목록
- `react/src/components/material/StockStatus/` - 재고 현황 전체 디렉토리 (Detail, Audit, actions, types, config, mockData)
- `react/src/components/production/WorkOrders/documents/bending/` - 절곡 작업일지 컴포넌트
---
## 8. 세션 및 메모리 관리 정책 (Serena Optimized)
### 8.1 세션 시작 시 (Load Strategy)
```javascript
read_memory("bending-preproduction-state") // 1. 상태 파악
read_memory("bending-preproduction-snapshot") // 2. 사고 흐름 복구
read_memory("bending-preproduction-active-symbols") // 3. 작업 대상 파악
```
### 8.2 작업 중 관리 (Context Defense)
| 컨텍스트 잔량 | Action | 내용 |
|--------------|--------|------|
| **30% 이하** | Snapshot | `write_memory("bending-preproduction-snapshot", "코드변경+논의요약")` |
| **20% 이하** | Context Purge | `write_memory("bending-preproduction-active-symbols", "수정 파일/함수")` |
| **10% 이하** | Stop & Save | 최종 상태 저장 후 세션 교체 권고 |
### 8.3 Serena 메모리 구조
- `bending-preproduction-state`: { phase, progress, next_step, last_decision }
- `bending-preproduction-snapshot`: 현재까지의 논의 및 코드 변경점 요약
- `bending-preproduction-rules`: 불변 규칙 (Receiving 우회, options JSON 정책 등)
- `bending-preproduction-active-symbols`: 현재 수정 중인 파일/심볼 리스트
---
## 9. 검증 결과
> 작업 완료 후 이 섹션에 검증 결과 추가
### 9.1 Phase 1 테스트 케이스
| # | 시나리오 | 입력 | 예상 결과 | 실제 결과 | 상태 |
|---|---------|------|----------|----------|------|
| T1.1 | 선생산 WO 완료 시 재고 입고 | WO(sales_order_id=null) 완료 | Stock/StockLot 생성, qty 증가 | | ⏳ |
| T1.2 | 기존 수주 WO 완료 시 변경 없음 | WO(sales_order_id=43) 완료 | 기존대로 Shipment 생성 | | ⏳ |
| T1.3 | LOT 번호 자동 생성 | 선생산 WO 완료 | KD-SA-YYMMDD-NN 형식 LOT | | ⏳ |
| T1.4 | StockTransaction 기록 | 생산 입고 | TYPE_IN, reason=production_output | | ⏳ |
### 9.2 Phase 2 테스트 케이스
| # | 시나리오 | 입력 | 예상 결과 | 실제 결과 | 상태 |
|---|---------|------|----------|----------|------|
| T2.1 | 수주 없이 작업지시 생성 | manual 모드 + 절곡 품목 | WO 생성, sales_order_id=null | | ⏳ |
| T2.2 | 재고현황 절곡품 필터 | item_category=BENDING | 절곡품만 표시 | | ⏳ |
| T2.3 | FIFO 출고 | 재고 투입 | 가장 오래된 LOT부터 차감 | | ⏳ |
### 9.3 Phase 3 테스트 케이스
| # | 시나리오 | 입력 | 예상 결과 | 실제 결과 | 상태 |
|---|---------|------|----------|----------|------|
| T3.1 | 수주 확정 시 재고 확인 | 재고 10, 필요 15 | 부족 5 표시 | | ⏳ |
| T3.2 | 가용 재고 자동 예약 | 재고 10, 필요 5 | reserved_qty=5, available_qty=5 | | ⏳ |
| T3.3 | 부족분 생산지시 | 재고 10, 필요 15 | 5개 생산지시 자동 생성 | | ⏳ |
### 9.4 성공 기준
| 기준 | 달성 | 비고 |
|------|------|------|
| 선생산 WO → 재고 입고 정상 동작 | ⏳ | Phase 1 핵심 |
| 기존 수주 WO 흐름 변경 없음 | ⏳ | 회귀 테스트 |
| 절곡품 재고현황 필터링 가능 | ⏳ | Phase 2 |
| 수주 시 재고 자동 매칭 | ⏳ | Phase 3 |
| 5130 데이터 마이그레이션 완료 | ⏳ | Phase 3 |
---
## 10. 자기완결성 점검 결과
### 10.1 체크리스트 검증
| # | 검증 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1 | 작업 목적이 명확한가? | ✅ | 0.2 선생산 운영 방식 + 1.1 배경 |
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.4 성공 기준 참조 |
| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1~3, 14개 작업 항목 |
| 4 | 의존성이 명시되어 있는가? | ✅ | 기존 bending 계획 문서 참조 |
| 5 | 참고 파일 경로가 정확한가? | ✅ | 검증 완료 (Models/Tenants/, material/StockStatus/) |
| 6 | 단계별 절차가 실행 가능한가? | ✅ | 라인번호 + 실제 코드 바디 포함 |
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9 테스트 케이스 참조 |
| 8 | 모호한 표현이 없는가? | ✅ | 코드 수준 상세 기술 + 용어 설명 포함 |
### 10.2 새 세션 시뮬레이션 테스트
| 질문 | 답변 가능 | 참조 섹션 |
|------|:--------:|----------|
| Q1. 절곡품이 뭔가? 왜 선생산하는가? | ✅ | 0.1, 0.2 용어 및 비즈니스 배경 |
| Q2. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 |
| Q3. 어디서부터 시작해야 하는가? | ✅ | 2.1 Phase 1 + 3.1 절차 |
| Q4. 어떤 파일을 수정해야 하는가? | ✅ | 2.1~2.3 영향 파일 (정확한 경로) |
| Q5. 기존 코드 구조가 어떻게 되어 있는가? | ✅ | 4.1~4.3 DB 스키마 + 코드 레퍼런스 |
| Q6. 신규 메서드를 어떻게 구현해야 하는가? | ✅ | 4.4~4.5 구현 설계 (전체 코드) |
| Q7. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 |
| Q8. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 |
---
*이 문서는 /plan 스킬로 생성되었습니다.*