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

39 KiB
Raw Permalink Blame History

절곡품 선생산 → 재고 적재 흐름 통합 개발 계획

작성일: 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)

// 라인 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)

// 라인 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)

// 라인 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() 구현 시 아래 코드를 기반으로 작성:

// 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() 구현 설계

/**
 * 생산 완료 시 완성품 재고 입고
 * 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 완료 분기 구현 설계

// 라인 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)

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 스킬로 생성되었습니다.