# 개소별 자재 투입 매핑 계획

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