diff --git a/plans/bending-preproduction-stock-plan.md b/plans/bending-preproduction-stock-plan.md new file mode 100644 index 0000000..0547677 --- /dev/null +++ b/plans/bending-preproduction-stock-plan.md @@ -0,0 +1,838 @@ +# 절곡품 선생산 → 재고 적재 흐름 통합 개발 계획 + +> **작성일**: 2026-02-21 +> **목적**: 레거시 5130 절곡품(가이드레일/셔터박스/바텀바) 관리를 SAM 기존 재고 시스템에 통합하고, 선생산→재고적재 흐름 구현 +> **기준 문서**: `api/app/Services/StockService.php`, `api/app/Services/WorkOrderService.php`, `docs/plans/bending-info-auto-generation-plan.md` +> **상태**: 🔄 Phase 3 완료 (3.5 마이그레이션 제외) + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | Phase 3 완료 (3.1~3.4, 3.5 마이그레이션 별도) | +| **다음 작업** | 3.5 레거시 데이터 마이그레이션 (별도 진행) | +| **진행률** | 13/14 (93%) | +| **마지막 업데이트** | 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/plans/bending-info-auto-generation-plan.md` - BendingInfoBuilder 참조 +- `docs/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/plans/bending-info-auto-generation-plan.md` - BendingInfoBuilder 자동 생성 계획 +- `docs/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 스킬로 생성되었습니다.* \ No newline at end of file