Files
sam-docs/dev/dev_plans/archive/material-input-per-item-mapping-plan.md

482 lines
17 KiB
Markdown
Raw Normal View 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`
```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 스킬로 생성되었습니다.*