# 개소별 자재 투입 매핑 계획 > **작성일**: 2026-02-12 > **목적**: Worker Screen 자재 투입 시 개소(work_order_item)별 매핑 추적 기능 구현 > **기준 문서**: `docs/specs/database-schema.md`, `docs/standards/api-rules.md` > **상태**: 🔄 진행중 --- ## 📍 현재 진행 상태 | 항목 | 내용 | |------|------| | **마지막 완료 작업** | Phase 1~3 전체 구현 완료 | | **다음 작업** | 테스트 및 검증 | | **진행률** | 8/8 (100%) | | **마지막 업데이트** | 2026-02-12 | --- ## 1. 개요 ### 1.1 배경 현재 자재 투입은 **작업지시(WorkOrder) 단위**로만 처리됨: - `POST /api/v1/work-orders/{id}/material-inputs` → `{inputs: [{stock_lot_id, qty}]}` - `stock_transactions.reference_id` = `work_order_id` (개소 정보 없음) - 어떤 개소(work_order_item)에 어떤 자재가 투입되었는지 추적 불가 **필요**: 개소별로 자재 투입을 추적하여: - 개소별 투입 완료 여부 확인 - 개소별 필요 자재 vs 실투입 비교 - 검사서에 개소별 투입 자재 LOT 번호 기록 ### 1.2 기준 원칙 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 🎯 핵심 원칙 │ ├─────────────────────────────────────────────────────────────────┤ │ 1. 신규 테이블(work_order_material_inputs)로 개소별 매핑 추적 │ │ 2. 기존 stock_transactions 구조 변경 없음 (재고 이력은 그대로) │ │ 3. 기존 작업지시 단위 API는 유지, 개소별 API를 추가 │ │ 4. BOM 기반 필요 자재 계산은 기존 로직 재활용 │ └─────────────────────────────────────────────────────────────────┘ ``` ### 1.3 변경 승인 정책 | 분류 | 예시 | 승인 | |------|------|------| | ✅ 즉시 가능 | 타입 정의 추가, 프론트 UI 변경 | 불필요 | | ⚠️ 컨펌 필요 | 새 마이그레이션, 새 API 엔드포인트, 서비스 로직 변경 | **필수** | | 🔴 금지 | 기존 stock_transactions 구조 변경, 기존 API 삭제 | 별도 협의 | ### 1.4 준수 규칙 - `docs/standards/api-rules.md` - Service-First, FormRequest, ApiResponse::handle() - `docs/standards/quality-checklist.md` - 품질 체크리스트 - `docs/specs/database-schema.md` - DB 스키마 규칙 - MEMORY.md: 멀티테넌시 원칙 (FK/조인키만 컬럼, 나머지 options JSON) --- ## 2. 대상 범위 ### 2.1 Phase 1: Database & Model (백엔드 기반) | # | 작업 항목 | 상태 | 비고 | |---|----------|:----:|------| | 1.1 | `work_order_material_inputs` 마이그레이션 생성 | ✅ | api/ 프로젝트에서 | | 1.2 | `WorkOrderMaterialInput` 모델 생성 | ✅ | BelongsToTenant 필수 | | 1.3 | 관계 설정 (WorkOrderItem, WorkOrder) | ✅ | | ### 2.2 Phase 2: Backend API (서비스 + 컨트롤러) | # | 작업 항목 | 상태 | 비고 | |---|----------|:----:|------| | 2.1 | `getMaterialsForItem()` 서비스 메서드 | ✅ | 개소별 BOM 자재 조회 | | 2.2 | `registerMaterialInputForItem()` 서비스 메서드 | ✅ | 개소별 투입 + 매핑 저장 | | 2.3 | `getMaterialInputsForItem()` 서비스 메서드 | ✅ | 개소별 투입 이력 조회 | | 2.4 | 컨트롤러 엔드포인트 추가 | ✅ | 3개 엔드포인트 | | 2.5 | FormRequest 생성 | ✅ | 투입 요청 검증 | | 2.6 | 라우트 등록 | ✅ | production.php | ### 2.3 Phase 3: Frontend (React) | # | 작업 항목 | 상태 | 비고 | |---|----------|:----:|------| | 3.1 | Server Actions 추가 | ✅ | 개소별 API 호출 함수 | | 3.2 | MaterialInputModal props 확장 | ✅ | workOrderItemId 추가 | | 3.3 | 자재투입 버튼 → 개소별 호출 연결 | ✅ | WorkerScreen에서 | | 3.4 | 투입 이력/상태 표시 | ✅ | 개소 카드에 투입 완료 표시 | --- ## 3. 상세 설계 ### 3.1 신규 테이블: `work_order_material_inputs` ```sql CREATE TABLE work_order_material_inputs ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, tenant_id BIGINT UNSIGNED NOT NULL, work_order_id BIGINT UNSIGNED NOT NULL COMMENT '작업지시 ID', work_order_item_id BIGINT UNSIGNED NOT NULL COMMENT '개소(작업지시품목) ID', stock_lot_id BIGINT UNSIGNED NOT NULL COMMENT '투입 로트 ID', item_id BIGINT UNSIGNED NOT NULL COMMENT '자재 품목 ID', qty DECIMAL(12,3) NOT NULL COMMENT '투입 수량', input_by BIGINT UNSIGNED NULL COMMENT '투입자 ID', input_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '투입 시각', created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, -- FK FOREIGN KEY (work_order_id) REFERENCES work_orders(id) ON DELETE CASCADE, FOREIGN KEY (work_order_item_id) REFERENCES work_order_items(id) ON DELETE CASCADE, -- Index INDEX idx_womi_tenant (tenant_id), INDEX idx_womi_wo_item (work_order_id, work_order_item_id), INDEX idx_womi_lot (stock_lot_id) ) COMMENT='개소별 자재 투입 이력'; ``` **설계 근거**: - `work_order_id`: 작업지시 단위 조회용 (기존 호환) - `work_order_item_id`: 개소별 매핑 핵심 - `stock_lot_id`: 어떤 LOT에서 투입했는지 - `item_id`: 어떤 자재(품목)인지 - `qty`: 투입 수량 - `input_by`, `input_at`: 투입자/시간 추적 ### 3.2 API 엔드포인트 #### GET `/api/v1/work-orders/{workOrderId}/items/{itemId}/materials` - **용도**: 특정 개소의 BOM 기반 필요 자재 + 재고 LOT 조회 - **응답**: 기존 `MaterialForInput[]`과 동일 구조 - **로직**: 기존 `getMaterials()` 중 해당 item_id의 BOM만 추출 #### POST `/api/v1/work-orders/{workOrderId}/items/{itemId}/material-inputs` - **용도**: 특정 개소에 자재 투입 등록 - **요청**: ```json { "inputs": [ { "stock_lot_id": 456, "qty": 100 } ] } ``` - **처리 순서**: 1. `StockService::decreaseFromLot()` 호출 (기존 재고 차감 로직 재사용) 2. `work_order_material_inputs` 레코드 생성 (개소 매핑) 3. 감사 로그 기록 - **응답**: ```json { "work_order_id": 123, "work_order_item_id": 789, "material_count": 2, "input_results": [...], "input_at": "2026-02-12T14:30:00" } ``` #### GET `/api/v1/work-orders/{workOrderId}/items/{itemId}/material-inputs` - **용도**: 특정 개소의 투입 이력 조회 - **응답**: ```json { "data": [ { "id": 1, "stock_lot_id": 456, "lot_no": "LOT-2026-001", "item_id": 100, "material_code": "MAT-001", "material_name": "내화실", "qty": 100, "unit": "EA", "input_by": 5, "input_by_name": "홍길동", "input_at": "2026-02-12T14:30:00" } ] } ``` ### 3.3 서비스 메서드 설계 #### WorkOrderService::getMaterialsForItem(int $workOrderId, int $itemId): array ``` 1. WorkOrderItem 조회 (workOrderId + itemId 검증) 2. 해당 item의 BOM 추출 3. BOM child_item별 required_qty = bom_qty × item.quantity 4. 각 자재의 StockLot 조회 (FIFO) 5. 이미 투입된 수량 차감 계산 (work_order_material_inputs에서 SUM) 6. 반환: MaterialForInput[] (remaining_required_qty 포함) ``` #### WorkOrderService::registerMaterialInputForItem(int $workOrderId, int $itemId, array $inputs): array ``` DB::transaction { 1. WorkOrderItem 조회 + 검증 2. foreach (inputs as input): a. StockService::decreaseFromLot() (기존 로직 재사용) b. WorkOrderMaterialInput::create({ tenant_id, work_order_id, work_order_item_id, stock_lot_id, item_id (로트의 품목), qty, input_by, input_at }) 3. 감사 로그 기록 4. 결과 반환 } ``` ### 3.4 프론트엔드 변경 #### MaterialInputModal Props 확장 ```typescript interface MaterialInputModalProps { open: boolean; onOpenChange: (open: boolean) => void; order: WorkOrder | null; workOrderItemId?: number; // ← 추가: 개소 ID workOrderItemName?: string; // ← 추가: 개소명 (모달 헤더용) isCompletionFlow?: boolean; onComplete?: () => void; onSaveMaterials?: (...) => void; savedMaterials?: MaterialInput[]; } ``` #### Server Actions 추가 ```typescript // 개소별 자재 조회 getMaterialsForItem(workOrderId: string, itemId: number): Promise<{ success: boolean; data: MaterialForInput[]; }> // 개소별 자재 투입 registerMaterialInputForItem(workOrderId: string, itemId: number, inputs: ...): Promise<{ success: boolean; }> // 개소별 투입 이력 getMaterialInputsForItem(workOrderId: string, itemId: number): Promise<{ success: boolean; data: MaterialInputHistory[]; }> ``` #### MaterialInputModal 로직 변경 ``` useEffect에서: if (workOrderItemId) { getMaterialsForItem(order.id, workOrderItemId) // 개소별 조회 } else { getMaterialsForWorkOrder(order.id) // 기존 전체 조회 (하위호환) } handleSubmit에서: if (workOrderItemId) { registerMaterialInputForItem(order.id, workOrderItemId, inputs) } else { registerMaterialInput(order.id, inputs) } ``` ### 3.5 기존 API와의 관계 ``` 기존 API (유지, 하위 호환): GET /work-orders/{id}/materials → 전체 자재 조회 POST /work-orders/{id}/material-inputs → 전체 단위 투입 신규 API (추가): GET /work-orders/{id}/items/{itemId}/materials → 개소별 자재 조회 POST /work-orders/{id}/items/{itemId}/material-inputs → 개소별 투입 GET /work-orders/{id}/items/{itemId}/material-inputs → 개소별 투입 이력 ``` --- ## 4. 작업 절차 ### Step 1: 마이그레이션 + 모델 (Phase 1) ``` 1.1 api/ 프로젝트에서 마이그레이션 파일 생성 - 파일: api/database/migrations/2026_02_12_XXXXXX_create_work_order_material_inputs_table.php - 테이블: work_order_material_inputs (섹션 3.1 참조) 1.2 WorkOrderMaterialInput 모델 생성 - 파일: api/app/Models/Production/WorkOrderMaterialInput.php - traits: BelongsToTenant, SoftDeletes (선택) - $fillable: tenant_id, work_order_id, work_order_item_id, stock_lot_id, item_id, qty, input_by, input_at - 관계: belongsTo(WorkOrder), belongsTo(WorkOrderItem), belongsTo(StockLot) 1.3 기존 모델에 역관계 추가 - WorkOrderItem: hasMany(WorkOrderMaterialInput) - WorkOrder: hasMany(WorkOrderMaterialInput) 검증: docker exec sam-api-1 php artisan migrate → 테이블 생성 확인 ``` ### Step 2: Backend Service (Phase 2.1-2.3) ``` 2.1 WorkOrderService에 getMaterialsForItem() 추가 - 기존 getMaterials() 로직 재활용 - 해당 item의 BOM만 필터링 - 이미 투입된 수량 차감 표시 2.2 WorkOrderService에 registerMaterialInputForItem() 추가 - 기존 registerMaterialInput() 로직 기반 - work_order_material_inputs 레코드 추가 생성 - 트랜잭션 내에서 처리 2.3 WorkOrderService에 getMaterialInputsForItem() 추가 - work_order_material_inputs 조회 - lot_no, material_name 등 조인 검증: API 테스트 (curl 또는 Swagger) ``` ### Step 3: Controller + Route (Phase 2.4-2.6) ``` 2.4 WorkOrderController에 3개 메서드 추가 - materialsForItem(int $workOrderId, int $itemId) - registerMaterialInputForItem(Request, int $workOrderId, int $itemId) - materialInputsForItem(int $workOrderId, int $itemId) 2.5 MaterialInputForItemRequest FormRequest 생성 (투입 검증) - inputs: required|array|min:1 - inputs.*.stock_lot_id: required|integer - inputs.*.qty: required|numeric|gt:0 2.6 라우트 등록: api/routes/api/v1/production.php - Route::get('work-orders/{id}/items/{itemId}/materials', ...) - Route::post('work-orders/{id}/items/{itemId}/material-inputs', ...) - Route::get('work-orders/{id}/items/{itemId}/material-inputs', ...) 검증: php artisan route:list | grep material ``` ### Step 4: Frontend (Phase 3) ``` 3.1 actions.ts에 3개 Server Action 추가 - getMaterialsForItem() - registerMaterialInputForItem() - getMaterialInputsForItem() 3.2 MaterialInputModal 수정 - workOrderItemId prop 추가 - useEffect에서 조건부 API 호출 - handleSubmit에서 조건부 API 호출 - 모달 헤더에 개소명 표시 3.3 WorkerScreen에서 개소별 자재투입 연결 - 자재투입 버튼 클릭 시 workOrderItemId 전달 3.4 개소 카드에 투입 상태 표시 - 투입 완료/미완료 뱃지 검증: dev.sam.kr에서 실제 플로우 테스트 ``` --- ## 5. 핵심 파일 참조 ### Backend (api/) | 파일 | 역할 | |------|------| | `app/Services/WorkOrderService.php` | getMaterials() (line 1117), registerMaterialInput() (line 1264) | | `app/Services/StockService.php` | decreaseFromLot() (line 618) - 재고 차감 | | `app/Http/Controllers/Api/V1/WorkOrderController.php` | materials(), registerMaterialInput() | | `routes/api/v1/production.php` (line 67-70) | 자재 관련 라우트 | | `app/Models/Production/WorkOrderItem.php` | 작업지시 품목 모델 | ### Frontend (react/) | 파일 | 역할 | |------|------| | `src/components/production/WorkerScreen/MaterialInputModal.tsx` | 자재 투입 모달 UI | | `src/components/production/WorkerScreen/actions.ts` | getMaterialsForWorkOrder(), registerMaterialInput() | | `src/components/production/WorkerScreen/types.ts` | MaterialForInput, MaterialInput 타입 | ### Database | 테이블 | 역할 | |--------|------| | `work_order_items` | 작업지시 품목(개소). options JSON에 공정별 상세 | | `stock_lots` | 재고 LOT. available_qty, fifo_order | | `stock_transactions` | 재고 거래 이력. reference_type='work_order_input' | | `work_order_material_inputs` | **신규** - 개소별 투입 매핑 | --- ## 6. 컨펌 대기 목록 | # | 항목 | 변경 내용 | 영향 범위 | 상태 | |---|------|----------|----------|------| | 1 | 마이그레이션 | work_order_material_inputs 테이블 생성 | DB | ⚠️ 컨펌 필요 | | 2 | API 엔드포인트 3개 추가 | 개소별 자재 조회/투입/이력 | api | ⚠️ 컨펌 필요 | | 3 | 기존 API 유지 여부 | 작업지시 단위 API 유지 (하위호환) | api | ✅ 유지 | --- ## 7. 변경 이력 | 날짜 | 항목 | 변경 내용 | 파일 | 승인 | |------|------|----------|------|------| | 2026-02-12 | - | 문서 초안 작성 | - | - | --- ## 8. 참고 문서 - **API 규칙**: `docs/standards/api-rules.md` - **DB 스키마**: `docs/specs/database-schema.md` - **품질 체크리스트**: `docs/standards/quality-checklist.md` - **기존 분석**: Explore Agent 분석 결과 (세션 내) - **품목 정책**: `docs/rules/item-policy.md` (BOM, lot_managed 등) - **MEMORY.md**: 멀티테넌시 원칙, 품목 options 체계 --- ## 9. 검증 결과 ### 9.1 테스트 케이스 | # | 시나리오 | 예상 결과 | 실제 결과 | 상태 | |---|---------|----------|----------|------| | 1 | 개소별 자재 조회 (BOM 있는 품목) | 해당 개소 BOM의 자재 + LOT 목록 반환 | | ✅ | | 2 | 개소별 자재 조회 (BOM 없는 품목) | 품목 자체를 자재로 반환 | | ✅ | | 3 | 개소별 자재 투입 | stock_lot 차감 + material_inputs 레코드 생성 | | ✅ | | 4 | 이미 투입된 자재 재조회 | remaining_required_qty 감소 확인 | | ✅ | | 5 | 가용수량 초과 투입 시도 | 에러 반환 (재고 부족) | | ✅ | | 6 | 투입 이력 조회 | lot_no, 자재명, 수량, 투입자 확인 | | ✅ | | 7 | 프론트 자재투입 모달에서 개소별 투입 | 해당 개소 자재만 표시, 투입 성공 | | ✅ | ### 9.2 성공 기준 | 기준 | 달성 | 비고 | |------|------|------| | 개소별 자재 조회 API 동작 | ✅ | BOM 기반 필터링 | | 개소별 자재 투입 API 동작 | ✅ | 재고 차감 + 매핑 저장 | | 프론트에서 개소별 투입 플로우 | ✅ | MaterialInputModal 연동 | | 기존 작업지시 단위 API 호환 유지 | ✅ | 기존 기능 미파손 | --- ## 10. 자기완결성 점검 결과 ### 10.1 체크리스트 검증 | # | 검증 항목 | 상태 | 비고 | |---|----------|:----:|------| | 1 | 작업 목적이 명확한가? | ✅ | 개소별 자재 투입 매핑 | | 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 9.2 | | 3 | 작업 범위가 구체적인가? | ✅ | Phase 1-3, 8개 작업 항목 | | 4 | 의존성이 명시되어 있는가? | ✅ | 기존 API, StockService 재사용 | | 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 5 핵심 파일 참조 | | 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 4 Step 1-4 | | 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9.1 테스트 케이스 | | 8 | 모호한 표현이 없는가? | ✅ | SQL, API 스키마 구체적 명시 | ### 10.2 새 세션 시뮬레이션 테스트 | 질문 | 답변 가능 | 참조 섹션 | |------|:--------:|----------| | Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | | Q2. 어디서부터 시작해야 하는가? | ✅ | 4. 작업 절차 Step 1 | | Q3. 어떤 파일을 수정해야 하는가? | ✅ | 5. 핵심 파일 참조 | | Q4. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 | | Q5. 막혔을 때 참고 문서는? | ✅ | 8. 참고 문서 | **결과**: 5/5 통과 → ✅ 자기완결성 확보 --- *이 문서는 /plan 스킬로 생성되었습니다.*