- 완료된 계획 문서 22개를 plans/archive/로 이동 - tracked 16개 (git mv): bending-lot-pipeline, docs-update, fcm-notification 등 - untracked 6개 (mv): bending-worklog, formula-engine, mng-item 등 - index_plans.md 전면 업데이트 - 진행중 44개 / 완료 37개 현황 반영 - 각 문서별 실제 진행률 기재 (0%~94%) - 카테고리별 재정리 (견적/생산/품목/문서/마이그레이션/시스템/UI) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
17 KiB
17 KiB
개소별 자재 투입 매핑 계획
작성일: 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 }
]
}
- 처리 순서:
StockService::decreaseFromLot()호출 (기존 재고 차감 로직 재사용)work_order_material_inputs레코드 생성 (개소 매핑)- 감사 로그 기록
- 응답:
{
"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 스킬로 생성되었습니다.