Files
sam-docs/plans/archive/material-input-per-item-mapping-plan.md
권혁성 28b69e5449 docs: archive 37개 + COMPLETED 3개 복원 - 향후 docs/ 정식 문서화 시 참조용
- 완료 문서의 상세 내용은 추후 docs/ 구조화 시 정식 문서에 반영 예정
- HISTORY.md는 요약 인덱스로 유지, 개별 파일은 상세 참조용 보관

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 22:32:20 +09:00

17 KiB
Raw Blame History

개소별 자재 투입 매핑 계획

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

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

  • 용도: 특정 개소에 자재 투입 등록
  • 요청:
{
  "inputs": [
    { "stock_lot_id": 456, "qty": 100 }
  ]
}
  • 처리 순서:
    1. StockService::decreaseFromLot() 호출 (기존 재고 차감 로직 재사용)
    2. work_order_material_inputs 레코드 생성 (개소 매핑)
    3. 감사 로그 기록
  • 응답:
{
  "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

  • 용도: 특정 개소의 투입 이력 조회
  • 응답:
{
  "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 확장

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 추가

// 개소별 자재 조회
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 스킬로 생성되었습니다.