diff --git a/plans/product-code-traceability-plan.md b/plans/product-code-traceability-plan.md new file mode 100644 index 0000000..2a630bd --- /dev/null +++ b/plans/product-code-traceability-plan.md @@ -0,0 +1,685 @@ +# 품목(제품코드) 연결 구조 개선 계획 + +> **작성일**: 2026-02-25 +> **목적**: 견적 → 수주 → 생산 → 출하 → 품질 전 단계에서 제품코드(모델코드) 추적성 확보 +> **상태**: 🔄 계획 수립 완료, 실행 대기 +> **리뷰**: v2 - SuperClaude 3개 페르소나 리뷰 반영 (Backend Architect, System Architect, Quality Engineer) + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | 전체 데이터 흐름 분석 + 페르소나 리뷰 반영 | +| **다음 작업** | Phase 0 - 사전 데이터 조사 | +| **진행률** | 0/4 Phase (0%) | +| **마지막 업데이트** | 2026-02-25 | + +--- + +## 1. 개요 + +### 1.1 배경 + +SAM ERP에서 **1개소(1틀) = 1셔터 = 1제품모델**이 기본 추적 단위이다. +제품코드(모델코드, 예: `FG-KQTS01-측면형-SUS`)는 견적 단계에서 생성되어 수주 → 생산 → 출하 → 품질까지 일관되게 추적되어야 한다. + +**현재 문제**: 제품코드가 `order_nodes.options`까지만 전달되고, 그 이후 단계(작업지시, 출하, 품질)로 흐르지 않아 **추적성이 끊어진 상태**이다. + +### 1.2 용어 정의 + +| 용어 | 설명 | 예시 | 네이밍 규칙 | +|------|------|------|------------| +| `product_code` | 완제품 모델코드 (제품 추적 단위) | `FG-KQTS01-측면형-SUS` | Backend JSON: `product_code` (snake_case), Frontend: `productCode` (camelCase) | +| `item_code` | 품목 마스터 코드 (원자재/부품) | `EST-RAW-슬랫-방화`, `SUS304` | items.code 컬럼 | +| `product_name` | 완제품명 | `측면형 스크린 셔터` | Backend JSON: `product_name`, Frontend: `productName` | +| `개소(틀)` | 1셔터 = 1제품모델 단위 | order_nodes 1행 = 1개소 | - | + +> **주의**: `item_code`(원자재)와 `product_code`(완제품)는 완전히 다른 데이터. 혼동 금지. + +### 1.3 핵심 데이터 흐름 (AS-IS) + +``` +견적(quotes) + └─ calculation_inputs JSON → items[].productCode (camelCase) + └─ product_code 컬럼 → ❌ 비어있음 (미활용) + │ + ▼ +수주(orders) + └─ item_id → ❌ NULL (미설정) + └─ order_nodes.options → ✅ product_code (snake_case) 존재 + │ + ▼ +작업지시(work_orders) + └─ work_order_items.options → ❌ product_code 누락 (복사 안됨) + └─ work_results → ❌ product_code 없음 + │ + ▼ +출하(shipments) + └─ shipment_items.item_code → 원자재 코드만 (제품코드 아님) + │ + ▼ +품질(inspections) + └─ lot_no 문자열 매칭만 → ❌ work_order_id FK 없음 +``` + +### 1.4 핵심 데이터 흐름 (TO-BE) + +``` +견적(quotes) + └─ calculation_inputs JSON → items[].productCode + └─ product_code 컬럼 → ✅ 대표 제품코드 저장 + │ + ▼ +수주(orders) + └─ order_nodes.options → ✅ product_code, product_name + │ + ▼ +작업지시(work_orders) + └─ work_order_items.options → ✅ product_code, product_name (전 경로에서 복사) + └─ work_results → ✅ work_order_item FK로 역추적 가능 + │ + ▼ +출하(shipments) + └─ shipment_items → ✅ product_code 포함 or work_order_item 참조 + │ + ▼ +품질(inspections) + └─ ✅ work_order_id FK 추가 → 직접 연결 +``` + +### 1.5 기준 원칙 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 🎯 핵심 원칙 │ +├─────────────────────────────────────────────────────────────────┤ +│ 1. 컬럼 추가 정책: FK/조인키만 컬럼, 나머지는 options JSON │ +│ 2. 기존 데이터 보존: 파괴적 변경 없이 점진적 개선 │ +│ 3. 역추적 가능: 어떤 단계에서든 원래 제품코드로 돌아갈 수 있어야 함│ +│ 4. 최소 변경: 현재 동작하는 로직에 영향을 주지 않는 범위에서 진행 │ +│ 5. 네이밍 통일: Backend JSON=snake_case, Frontend=camelCase │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 1.6 변경 승인 정책 + +| 분류 | 예시 | 승인 | +|------|------|------| +| ✅ 즉시 가능 | options JSON에 필드 추가, 프론트 표시 변경 | 불필요 | +| ⚠️ 컨펌 필요 | 서비스 로직 변경, 쿼리 변경, 마이그레이션 | **필수** | +| 🔴 금지 | 기존 테이블 컬럼 삭제, 기존 기능 제거 | 별도 협의 | + +--- + +## 2. 문제 목록 (우선순위별) + +### 🔴 P0 - 즉시 수정 필요 + +| # | 문제 | 위치 | 영향 | +|---|------|------|------| +| P0-1 | `product_code`가 `work_order_items.options`에 복사되지 않음 | `OrderService::createProductionOrder` (L1410) | 생산 현장에서 제품코드 확인 불가 | +| P0-2 | `product_name`도 동일하게 누락 | 위와 동일 | 제품명 확인 불가 | +| P0-3 | `WorkOrderService::store` 수주복사 경로도 동일 누락 | `WorkOrderService::store` (L287-296) | 수주 기반 직접 생성 시에도 누락 | +| P0-4 | `WorkOrderService::store` 직접 입력 경로에 product_code 전달 방법 없음 | `WorkOrderService::store` (L311-317) | 수동 생성 작업지시는 product_code 전달 자체가 불가 | +| P0-5 | `WorkOrderService::update` 품목 추가/수정 시 options 미전달 | `WorkOrderService::update` (L416-438) | 수정 시 product_code 소실 가능 | + +### 🟡 P1 - 단기 개선 + +| # | 문제 | 위치 | 영향 | +|---|------|------|------| +| P1-1 | `quotes.product_code` 컬럼이 비어있음 | `QuoteService` (견적 저장 로직) | 견적 → 수주 변환 시 제품코드 전달 부정확 | +| P1-2 | `orders.item_id` NULL | `OrderService::createFromQuote` | 수주에서 대표 품목 참조 불가 (⚠️ Phase 4 FG 마스터 등록에 의존) | +| P1-3 | `inspections.work_order_id` FK 없음 | 마이그레이션 필요 | 품질검사 ↔ 작업지시 직접 연결 불가, lot_no 문자열 매칭에 의존 | + +### 🟢 P2 - 중장기 과제 + +| # | 문제 | 위치 | 영향 | +|---|------|------|------| +| P2-1 | 완제품 마스터(FG-KQTS01) 미등록 | items 테이블 | 품목 기준 통합 관리 불가 | +| P2-2 | LOT 번호 연결 일관성 부족 | inspections ↔ stock_lots | FK 대신 문자열 매칭 | +| P2-3 | 출하 시 제품코드 미포함 | shipment_items | 출하 현황에서 모델별 추적 어려움 | +| P2-4 | `work_order_items.product_code` 장기적 컬럼 승격 필요 | work_order_items 테이블 | 통계/GROUP BY 시 JSON 쿼리 성능 병목 | + +--- + +## 3. 대상 범위 + +### 3.0 Phase 0: 사전 데이터 조사 (Phase 1 선행 필수) ⏳ + +**목표**: 마이그레이션 영향 범위 파악 및 보정 가능 건수 확인 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 0.1 | `order_nodes.options`에 `product_code` 보유율 조사 | ⏳ | SQL 쿼리 | +| 0.2 | `work_order_items`에서 `source_order_item_id` NULL 비율 조사 | ⏳ | 보정 불가 건수 파악 | +| 0.3 | soft deleted된 `order_items`/`order_nodes` 건수 조사 | ⏳ | withTrashed 필요 여부 | +| 0.4 | `stock_lots.lot_no` 중복 건수 조사 | ⏳ | Phase 3 역추적 신뢰성 | + +**조사 쿼리:** +```sql +-- 0.1: order_nodes의 product_code 보유율 +SELECT COUNT(*) as total, + SUM(CASE WHEN JSON_EXTRACT(options, '$.product_code') IS NOT NULL THEN 1 ELSE 0 END) as has_code, + ROUND(SUM(CASE WHEN JSON_EXTRACT(options, '$.product_code') IS NOT NULL THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 1) as pct +FROM order_nodes WHERE deleted_at IS NULL; + +-- 0.2: work_order_items의 source_order_item_id NULL 비율 +SELECT COUNT(*) as total, + SUM(CASE WHEN source_order_item_id IS NULL THEN 1 ELSE 0 END) as no_source, + ROUND(SUM(CASE WHEN source_order_item_id IS NULL THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 1) as pct +FROM work_order_items WHERE deleted_at IS NULL; + +-- 0.3: soft deleted된 원본 데이터 +SELECT 'order_items' as tbl, COUNT(*) as deleted_count FROM order_items WHERE deleted_at IS NOT NULL +UNION ALL +SELECT 'order_nodes', COUNT(*) FROM order_nodes WHERE deleted_at IS NOT NULL; + +-- 0.4: lot_no 중복 확인 +SELECT lot_no, COUNT(*) as cnt FROM stock_lots +WHERE deleted_at IS NULL GROUP BY lot_no HAVING COUNT(*) > 1; +``` + +### 3.1 Phase 1: product_code 전달 수정 (P0) ⏳ + +**목표**: 모든 work_order_items 생성/수정 경로에서 product_code, product_name 전달 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 1.1 | `OrderService::createProductionOrder` options 복사에 product_code/product_name 추가 | ⏳ | L1410-1419 | +| 1.2 | `WorkOrderService::store` 수주복사 로직에도 동일 추가 | ⏳ | L287-296 | +| 1.3 | `WorkOrderService::store` 직접 입력 경로 — 프론트에서 options.product_code 전달 | ⏳ | L311-317 (범위 확인 필요) | +| 1.4 | `WorkOrderService::update` 품목 수정 시 기존 options 보존 확인 | ⏳ | L416-438 | +| 1.5 | 기존 work_order_items 데이터 보정 (마이그레이션) | ⏳ | 스냅샷 백업 후 실행 | +| 1.6 | 프론트엔드 WorkerScreen에 제품코드 표시 | ⏳ | actions.ts + index.tsx | +| 1.7 | 프론트엔드 ProductionDashboard에 제품코드 표시 | ⏳ | actions.ts | + +> **배포 순서**: 백엔드 배포 → 마이그레이션 실행 → 프론트엔드 배포 + +### 3.2 Phase 2: 견적 → 수주 데이터 정합성 (P1-1) ⏳ + +**목표**: quotes.product_code 컬럼 활용 + +> **⚠️ 의존성 주의**: `orders.item_id` 설정은 items 테이블에 FG 품목이 등록되어야 가능하므로 Phase 4에 의존함. Phase 2에서는 item_id 설정을 **보류**하고 `order_nodes.options.product_code`를 통한 추적에 집중. + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 2.1 | 견적 저장 시 quotes.product_code 컬럼에 대표 제품코드 저장 | ⏳ | 다중 개소: 첫 번째 개소 코드 저장 | +| 2.2 | 견적→수주 변환 시 productCode(camelCase) → product_code(snake_case) 변환 확인 | ⏳ | OrderService::createFromQuote | +| 2.3 | 기존 데이터 보정 스크립트 | ⏳ | calculation_inputs에서 추출 | + +**다중 개소 정책**: quotes.product_code에는 **첫 번째 개소의 코드를 대표값**으로 저장. 전체 목록은 `calculation_inputs.items[].productCode`를 참조. + +### 3.3 Phase 3: 품질검사 연결 강화 (P1-3) ⏳ + +**목표**: inspections ↔ work_orders 직접 FK 연결 + +> **Phase 2와 병렬 실행 가능** — 서로 독립적인 경로 (견적-수주 vs 생산-품질) + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 3.1 | inspections 테이블에 work_order_id FK 마이그레이션 추가 | ⏳ | nullable | +| 3.2 | Inspection 모델에 `workOrder()` 관계 메서드 추가 | ⏳ | N+1 쿼리 방지 | +| 3.3 | 품질검사 생성 시 work_order_id 설정 로직 추가 | ⏳ | InspectionService | +| 3.4 | 기존 inspections 데이터에 work_order_id 보정 | ⏳ | lot_no 기반 역추적 (중복 lot_no 사전 확인 필수) | + +### 3.4 Phase 4: 완제품 마스터 및 출하 연결 (P2) ⏳ + +**목표**: 완제품 코드 등록, 출하 시 제품코드 포함, orders.item_id 설정 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 4.1 | 완제품(FG) 품목 자동 등록 방안 설계 | ⏳ | 견적 확정 시 or 수주 확정 시 | +| 4.2 | orders.item_id 설정 로직 추가 (Phase 2에서 보류한 것) | ⏳ | FG 품목 등록 후 가능 | +| 4.3 | shipment_items에 product_code 포함 방안 | ⏳ | 부분 출하 시 개소별 매핑 고려 | +| 4.4 | work_order_items.product_code 컬럼 승격 검토 | ⏳ | 통계 쿼리 성능용 (JSON → 컬럼) | +| 4.5 | 출하 → 품질 → 재고 전체 제품코드 추적 검증 | ⏳ | E2E 테스트 | + +--- + +## 4. 상세 작업 내용 + +### 4.1 Phase 1 상세: product_code 전달 수정 + +#### 4.1.1 백엔드 수정 — 전체 5개 경로 + +**경로 1: `OrderService::createProductionOrder` (L1410-1419)** + +현재 코드: +```php +$woItemOptions = array_filter([ + 'floor' => $orderItem->floor_code, + 'code' => $orderItem->symbol_code, + 'width' => $nodeOptions['width'] ?? $nodeOptions['open_width'] ?? null, + 'height' => $nodeOptions['height'] ?? $nodeOptions['open_height'] ?? null, + 'cutting_info' => $nodeOptions['cutting_info'] ?? null, + 'slat_info' => $slatInfo, + 'bending_info' => $nodeOptions['bending_info'] ?? null, + 'wip_info' => $nodeOptions['wip_info'] ?? null, +], fn ($v) => $v !== null); +``` + +수정 후: +```php +$woItemOptions = array_filter([ + 'floor' => $orderItem->floor_code, + 'code' => $orderItem->symbol_code, + 'product_code' => !empty($nodeOptions['product_code']) ? $nodeOptions['product_code'] : null, + 'product_name' => !empty($nodeOptions['product_name']) ? $nodeOptions['product_name'] : null, + 'width' => $nodeOptions['width'] ?? $nodeOptions['open_width'] ?? null, + 'height' => $nodeOptions['height'] ?? $nodeOptions['open_height'] ?? null, + 'cutting_info' => $nodeOptions['cutting_info'] ?? null, + 'slat_info' => $slatInfo, + 'bending_info' => $nodeOptions['bending_info'] ?? null, + 'wip_info' => $nodeOptions['wip_info'] ?? null, +], fn ($v) => $v !== null); +``` + +> **참고**: `!empty()` 사용으로 빈 문자열("")도 필터링. `?? null` 대신 사용. + +**경로 2: `WorkOrderService::store` 수주복사 (L287-296)** + +동일하게 `product_code`, `product_name` 추가. + +> **⚠️ 주의**: 이 경로는 OrderService와 달리 `slat_info` 자동계산 로직이 없음 (단순 nodeOptions 복사). 이 차이는 별도 이슈로 추적. + +**경로 3: `WorkOrderService::store` 직접 입력 (L311-317)** + +프론트에서 `items[].options`에 product_code를 포함시켜 전달해야 함. 수동 생성이므로 수주 연결 없이 product_code가 없는 것을 **허용** (nullable). 프론트 UI에 product_code 입력 필드 추가는 Phase 1 범위 밖. + +**경로 4: `WorkOrderService::update` 품목 수정 (L416-438)** + +현재 update 시 `options` 필드가 itemData에 미포함. 기존 options가 덮어씌워지지 않는지 확인 필요. +- `update(['item_name' => ...])` 식으로 특정 필드만 업데이트하면 options 보존됨 (OK) +- `items()->updateOrCreate(...)` 패턴이면 options 소실 위험 → 점검 필요 + +**경로 5: `WorkOrderService::update` 품목 신규 추가 (L435)** + +경로 3과 동일 — 프론트 전달 의존. 수동 추가이므로 product_code nullable 허용. + +#### 4.1.2 기존 데이터 보정 마이그레이션 + +```php +// ⚠️ 보정 전 스냅샷 백업 +DB::statement('CREATE TABLE IF NOT EXISTS work_order_items_backup_product_code + AS SELECT id, options FROM work_order_items'); + +// ⚠️ BelongsToTenant 글로벌 스코프 우회 + SoftDeletes 포함 +WorkOrderItem::withoutGlobalScopes() + ->whereNull(DB::raw("JSON_EXTRACT(options, '$.product_code')")) + ->whereNotNull('source_order_item_id') + ->chunk(100, function ($items) { + // bulk 조회로 N+1 방지 + $orderItemIds = $items->pluck('source_order_item_id')->filter()->unique(); + $orderItems = OrderItem::withTrashed() + ->with(['orderNode' => fn($q) => $q->withTrashed()]) + ->whereIn('id', $orderItemIds) + ->get() + ->keyBy('id'); + + foreach ($items as $item) { + $orderItem = $orderItems->get($item->source_order_item_id); + if ($orderItem?->orderNode) { + $nodeOptions = $orderItem->orderNode->options ?? []; + $productCode = !empty($nodeOptions['product_code']) ? $nodeOptions['product_code'] : null; + $productName = !empty($nodeOptions['product_name']) ? $nodeOptions['product_name'] : null; + if ($productCode) { + $options = $item->options ?? []; + $options['product_code'] = $productCode; + if ($productName) $options['product_name'] = $productName; + $item->updateQuietly(['options' => $options]); // 이벤트 미발생 + } + } + } + }); + +// 보정 결과 로그 +$total = WorkOrderItem::withoutGlobalScopes()->whereNull('deleted_at')->count(); +$withCode = WorkOrderItem::withoutGlobalScopes() + ->whereNull('deleted_at') + ->whereNotNull(DB::raw("JSON_EXTRACT(options, '$.product_code')")) + ->count(); +Log::info("product_code 보정 완료: {$withCode}/{$total} ({pct}%)"); +``` + +> **source_order_item_id가 NULL인 건**: 수동 생성 작업지시로 보정 불가. Phase 0 조사에서 건수 파악 후 감수 범위로 문서화. + +#### 4.1.3 프론트엔드 수정 + +**WorkerScreen/actions.ts** - API 응답에서 productCode 매핑: +```typescript +// work_order_items의 options에서 product_code 추출 +const productCode = api.items?.[0]?.options?.product_code || '-'; +const productName = api.items?.[0]?.options?.product_name || api.items?.[0]?.item_name || '-'; +``` + +> **다중 개소 표시**: items[0]만 가져오므로 다중 개소 작업지시 시 첫 번째만 표시됨. 향후 UI 개선 시 items 전체 순회 필요. + +**WorkerScreen/index.tsx** - 작업 카드에 제품코드 표시: +```typescript +itemName: productCode !== '-' ? `${productCode} - ${productName}` : productName, +``` + +**ProductionDashboard/actions.ts** - 대시보드에도 동일 적용. + +--- + +## 5. DB 테이블 관계도 (현재) + +``` +quotes ─────────────────────────────────────── items (product_id FK, 미활용) + │ calculation_inputs.items[].productCode (camelCase) + │ + ▼ (createFromQuote) +orders ─────────────────────────────────────── items (item_id FK, NULL) + │ + ├── order_nodes ──── options: {product_code, product_name, width, height, ...} + │ │ + │ └── order_items ── item_id → items (원자재) + │ │ + ▼ ▼ (createProductionOrder) +work_orders + │ + ├── work_order_items ── options: {floor, code, width, height, slat_info, ...} + │ │ ❌ product_code 없음 (TO-BE: 추가) + │ │ + │ ├── source_order_item_id → order_items (역추적 가능) + │ ├── work_order_material_inputs ── work_order_item_id FK (자재 투입) + │ └── work_order_step_progress ── work_order_item_id FK (공정 진행) + │ + ├── work_results ── work_order_id FK (작업 실적, product_name만 있음) + │ + ▼ + stock_lots ── work_order_id FK ✅ + │ + ▼ + stocks ── item_id → items + │ + ▼ + shipment_items ── stock_lot_id FK ✅, item_code (문자열, 원자재코드) + │ + ▼ + inspections ── lot_no (문자열 매칭), ❌ work_order_id 없음 +``` + +--- + +## 6. 롤백 전략 + +| Phase | 위험도 | 롤백 방법 | +|-------|:------:|----------| +| Phase 1 (options 필드 추가) | **낮음** | options에서 `product_code`, `product_name` 키 제거 스크립트 | +| Phase 1 (데이터 보정) | **중간** | `work_order_items_backup_product_code` 테이블에서 복원 | +| Phase 3 (inspections FK) | **중간** | `work_order_id` 컬럼 drop 마이그레이션 (down 메서드) | +| Phase 4 (FG 품목 등록) | **높음** | 자동 등록 FG 품목에 `auto_generated` 플래그 → 식별 후 삭제 | + +**필수 규칙:** +1. 모든 데이터 보정 마이그레이션에 `down()` 메서드 구현 +2. 보정 전 반드시 스냅샷 백업 테이블 생성 +3. 각 Phase 완료 후 검증 통과 확인 → 다음 Phase 진행 + +--- + +## 7. 컨펌 대기 목록 + +| # | 항목 | 변경 내용 | 영향 범위 | 상태 | +|---|------|----------|----------|------| +| 1 | Phase 0 사전 조사 실행 | 4개 SQL 쿼리 실행 | 읽기 전용 | ⚠️ 대기 | +| 2 | Phase 1 실행 승인 | 5개 경로 options 복사에 product_code 추가 | 작업지시 생성/수정 | ⚠️ 대기 | +| 3 | 데이터 보정 마이그레이션 | 기존 work_order_items에 product_code 역추적 보정 | 기존 데이터 | ⚠️ 대기 | +| 4 | inspections.work_order_id FK 추가 | 마이그레이션 + 품질검사 로직 수정 | inspections 테이블 | ⚠️ 대기 | +| 5 | 완제품 마스터 자동 등록 | items 테이블에 FG 유형 품목 자동 생성 | items, 견적/수주 로직 | ⚠️ 대기 | + +--- + +## 8. 작업 절차 요약 + +``` +Phase 0 (사전 조사) ─── 읽기 전용, 위험 없음 + ├── SQL 4개 실행 → 영향 범위 파악 + └── 결과에 따라 Phase 1 보정 전략 조정 + +Phase 1 (P0 수정) ─── 즉시 실행 가능, 영향 범위 최소 + ├── Step 1: OrderService.php 경로 1 (createProductionOrder) + ├── Step 2: WorkOrderService.php 경로 2-5 (store 수주복사, store 직접, update x2) + ├── Step 3: 스냅샷 백업 → 데이터 보정 마이그레이션 + ├── Step 4: 프론트 WorkerScreen에 제품코드 표시 + └── Step 5: 프론트 ProductionDashboard에 제품코드 표시 + ※ 배포 순서: 백엔드 → 마이그레이션 → 프론트 + +Phase 2 (P1 개선) ─── 견적/수주 데이터 정합성 ──┐ 병렬 실행 가능 + ├── Step 1: QuoteService에서 quotes.product_code 저장│ + ├── Step 2: 다중 개소 대표 코드 정책 적용 │ + └── Step 3: 기존 데이터 보정 │ + │ +Phase 3 (P1 개선) ─── 품질검사 연결 ────────────────┘ + ├── Step 1: inspections.work_order_id 마이그레이션 + ├── Step 2: Inspection 모델 관계 + InspectionService 수정 + └── Step 3: lot_no 기반 기존 데이터 보정 + +Phase 4 (P2 중장기) ─── 완제품 마스터 + 출하 + item_id + ├── Step 1: FG 품목 자동 등록 설계 + ├── Step 2: orders.item_id 설정 (FG 등록 후) + ├── Step 3: 출하 product_code 포함 (부분 출하 고려) + ├── Step 4: product_code 컬럼 승격 검토 + └── Step 5: E2E 추적 검증 +``` + +--- + +## 9. 성공 기준 + +| 기준 | 측정 방법 | 수치 목표 | +|------|----------|----------| +| WorkerScreen에서 제품코드 표시 | 다중 개소 수주 작업지시 5건에서 확인 | 100% 표시 | +| 신규 작업지시 생성 시 product_code 포함 | `SELECT JSON_EXTRACT(options, '$.product_code') FROM work_order_items WHERE ...` | NOT NULL | +| 기존 데이터 보정율 | source_order_item_id 있는 건 중 보정 비율 | 90% 이상 | +| 보정 데이터 정확도 | 원본 order_nodes와 대조 검증 | MATCH 100% | +| 기존 기능 회귀 없음 | 작업지시 목록/상세 API 정상 응답 | 에러 0건 | +| API 성능 영향 없음 | options 필드 추가로 인한 응답 시간 변화 | 5% 미만 | +| Phase 3: inspections FK 보정 정확도 | lot_no 기반 역추적 MATCH 비율 | 95% 이상 | +| Phase 4: E2E 추적 | 견적→수주→작업지시→완료→출하→품질 전 과정 product_code 일관성 | 100% | + +--- + +## 10. 참고 파일 + +### 백엔드 +| 파일 | 역할 | 주요 메서드/라인 | +|------|------|-----------------| +| `api/app/Services/OrderService.php` | 수주 → 작업지시 변환 | `createProductionOrder` (L1177), options 복사 (L1410-1419) | +| `api/app/Services/WorkOrderService.php` | 작업지시 서비스 | `store` 수주복사 (L287-296), `store` 직접입력 (L311-317), `update` (L416-438), `copyWorkOrderItemsToShipment` (L716-773) | +| `api/app/Services/Quote/QuoteService.php` | 견적 서비스 | product_code 저장 (L324), 개소별 노드 생성 (L645-674) | +| `api/app/Services/InspectionService.php` | 품질검사 서비스 | 검사 생성 로직 | +| `api/app/Services/WorkResultService.php` | 작업실적 서비스 | 실적 기록 (product_code 미포함) | +| `api/app/Models/Production/WorkOrderItem.php` | 작업지시 품목 모델 | options 캐스트, `setResult` (L155), `completeWithResult` (L164) | +| `api/app/Models/Production/WorkResult.php` | 작업실적 모델 | product_name만 있음, product_code 없음 | +| `api/app/Models/OrderNode.php` | 수주 노드 모델 | options 캐스트 | + +### 프론트엔드 +| 파일 | 역할 | +|------|------| +| `react/src/components/production/WorkerScreen/actions.ts` | 작업자 화면 서버 액션 | +| `react/src/components/production/WorkerScreen/index.tsx` | 작업자 화면 메인 컴포넌트 | +| `react/src/components/production/ProductionDashboard/actions.ts` | 대시보드 서버 액션 | +| `react/src/components/production/ProductionDashboard/types.ts` | 공통 타입 정의 | + +### DB 테이블 +| 테이블 | 핵심 컬럼/필드 | +|--------|---------------| +| `quotes` | product_code, product_name, calculation_inputs (JSON) | +| `orders` | item_id, quote_id | +| `order_nodes` | options (JSON: product_code, product_name, width, height, ...) | +| `order_items` | order_node_id, item_id, floor_code, symbol_code | +| `work_orders` | sales_order_id | +| `work_order_items` | source_order_item_id, options (JSON: ❌ product_code 누락) | +| `work_results` | work_order_id, product_name (product_code 없음) | +| `work_order_material_inputs` | work_order_item_id (자재 투입 이력) | +| `work_order_step_progress` | work_order_item_id (공정 단계 진행) | +| `inspections` | lot_no (❌ work_order_id 없음) | +| `stock_lots` | work_order_id, lot_no | +| `shipment_items` | stock_lot_id, item_code | + +--- + +## 11. 변경 이력 + +| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | +|------|------|----------|------|------| +| 2026-02-25 | 문서 초안 | 전체 분석 결과 기반 계획 문서 작성 | - | - | +| 2026-02-25 | v2 리뷰 반영 | SuperClaude 3개 페르소나 리뷰 결과 반영 (아래 상세) | - | - | + +### v2 리뷰 반영 상세 + +| # | 반영 항목 | 출처 | +|---|----------|------| +| 1 | Phase 0 (사전 데이터 조사) 추가 | Backend Architect, Quality Engineer | +| 2 | work_order_items 생성 경로 3곳 추가 (P0-4, P0-5 + Phase 1 작업 1.3, 1.4) | System Architect | +| 3 | 용어 정의 섹션 (1.2) 추가 — product_code vs item_code, 네이밍 규칙 | System Architect | +| 4 | 롤백 전략 섹션 (6) 추가 — 스냅샷 백업, down() 마이그레이션 | Quality Engineer | +| 5 | 마이그레이션 코드 보강 — withTrashed, withoutGlobalScopes, bulk 조회, empty 체크 | Backend Architect | +| 6 | DB 관계도에 work_results, work_order_material_inputs, work_order_step_progress 추가 | System Architect | +| 7 | Phase 2↔4 의존성 해결 — item_id 설정을 Phase 4로 이동 | Quality Engineer | +| 8 | Phase 2/3 병렬 실행 가능 명시 | System Architect | +| 9 | 성공 기준 수치화 — 보정율 90%+, MATCH 100%, 성능 5% 미만 등 | Quality Engineer | +| 10 | 다중 개소 대표 코드 정책 정의 (첫 번째 개소) | Quality Engineer | +| 11 | 배포 순서 명시 (백엔드 → 마이그레이션 → 프론트) | Backend Architect | +| 12 | P2-4 추가 — product_code 컬럼 승격 로드맵 (장기 통계 성능용) | System Architect | +| 13 | 검증 섹션 전 Phase 테스트 케이스 확장 | Quality Engineer | + +### 리뷰에서 별도 이슈로 추적할 항목 (이 계획 범위 밖) + +| 항목 | 설명 | 우선순위 | +|------|------|---------| +| WorkOrderService slat_info 로직 차이 | OrderService에는 자동계산 있고 WorkOrderService에는 없음 | Medium | +| 견적 수정 시 하위 데이터 동기화 | 수주 후 견적 수정 시 product_code 불일치 가능 | Low | +| 부분 출하 시 개소별 N:M 매핑 | shipment_items ↔ work_order_items 매핑 설계 | Phase 4에서 | +| options 조합 로직 리팩토링 | OrderService와 WorkOrderService 중복 코드 통합 | Low | + +--- + +## 12. 세션 및 메모리 관리 정책 + +### 12.1 세션 시작 시 +``` +1. 이 문서(product-code-traceability-plan.md) 읽기 +2. 진행 상태 테이블 확인 → 마지막 완료 작업 파악 +3. 다음 작업 시작 +``` + +### 12.2 작업 중 관리 +- Phase 완료 시 이 문서의 상태 테이블 업데이트 +- 컨펌 필요 사항 발생 시 컨펌 대기 목록에 추가 + +### 12.3 세션 종료 시 +- 변경 이력 섹션에 최종 업데이트 기록 + +--- + +## 13. 검증 결과 + +> 작업 완료 후 이 섹션에 검증 결과 추가 + +### 13.1 Phase 0 사전 조사 결과 + +| 조사 항목 | 결과 | 판단 | +|----------|------|------| +| order_nodes product_code 보유율 | | ⏳ | +| work_order_items source_order_item_id NULL 비율 | | ⏳ | +| soft deleted 원본 데이터 건수 | | ⏳ | +| lot_no 중복 건수 | | ⏳ | + +### 13.2 Phase 1 검증 + +| 테스트 | 예상 결과 | 실제 결과 | 상태 | +|--------|----------|----------|------| +| 신규 작업지시 생성 (OrderService 경로) | options에 product_code 포함 | | ⏳ | +| 신규 작업지시 생성 (WorkOrderService 수주복사) | options에 product_code 포함 | | ⏳ | +| product_code가 NULL인 order_nodes에서 생성 | 오류 없이 product_code NULL 저장 | | ⏳ | +| product_code가 빈 문자열인 경우 | empty 체크로 필터링, options에 미포함 | | ⏳ | +| 기존 데이터 보정 (source_order_item_id 있는 건) | product_code 채워짐 | | ⏳ | +| 기존 데이터 보정 (source_order_item_id NULL) | skip, 오류 없음 | | ⏳ | +| 기존 데이터 보정 (soft deleted 원본) | withTrashed로 정상 조회 | | ⏳ | +| WorkerScreen 표시 | "FG-KQTS01-측면형-SUS - 슬랫 방화" | | ⏳ | +| WorkerScreen - product_code 없는 건 | 기존과 동일 표시 (productName만) | | ⏳ | +| 기존 API 회귀 테스트 | 작업지시 목록/상세 정상 응답 | | ⏳ | + +**데이터 검증 쿼리:** +```sql +-- 보정 후 성공률 +SELECT COUNT(*) as total, + COUNT(CASE WHEN JSON_EXTRACT(options, '$.product_code') IS NOT NULL THEN 1 END) as with_code, + ROUND(COUNT(CASE WHEN JSON_EXTRACT(options, '$.product_code') IS NOT NULL THEN 1 END) * 100.0 / COUNT(*), 1) as pct +FROM work_order_items WHERE deleted_at IS NULL; + +-- 보정 데이터 정확도 (원본 대조) +SELECT woi.id, + JSON_EXTRACT(woi.options, '$.product_code') as wo_code, + JSON_EXTRACT(onode.options, '$.product_code') as node_code, + CASE WHEN JSON_EXTRACT(woi.options, '$.product_code') = JSON_EXTRACT(onode.options, '$.product_code') + THEN 'MATCH' ELSE 'MISMATCH' END as status +FROM work_order_items woi +JOIN order_items oi ON woi.source_order_item_id = oi.id +JOIN order_nodes onode ON oi.order_node_id = onode.id +WHERE woi.deleted_at IS NULL + AND JSON_EXTRACT(woi.options, '$.product_code') IS NOT NULL; +``` + +### 13.3 Phase 2 검증 + +| 테스트 | 예상 결과 | 실제 결과 | 상태 | +|--------|----------|----------|------| +| 견적 저장 시 quotes.product_code 저장 | 첫 번째 개소 코드 저장 | | ⏳ | +| 다중 개소 견적의 대표 코드 | 첫 번째 개소 코드가 quotes.product_code에 | | ⏳ | +| 견적→수주 변환 시 productCode→product_code 변환 | snake_case로 저장 | | ⏳ | + +### 13.4 Phase 3 검증 + +| 테스트 | 예상 결과 | 실제 결과 | 상태 | +|--------|----------|----------|------| +| inspections.work_order_id FK 추가 | 마이그레이션 성공, nullable | | ⏳ | +| 기존 inspection 조회 정상 동작 | 회귀 없음 | | ⏳ | +| lot_no 기반 역추적 보정 정확도 | MATCH 95% 이상 | | ⏳ | + +### 13.5 Phase 4 검증 + +| 테스트 | 예상 결과 | 실제 결과 | 상태 | +|--------|----------|----------|------| +| FG 품목 자동 등록 | items에 FG-KQTS01 등록 | | ⏳ | +| E2E: 견적→출하→품질 전 구간 product_code 일관성 | 100% 추적 가능 | | ⏳ | + +--- + +## 14. 자기완결성 점검 결과 + +### 14.1 체크리스트 검증 + +| # | 검증 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 1 | 작업 목적이 명확한가? | ✅ | 전 단계 제품코드 추적성 확보 | +| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 9 - 수치 목표 포함 | +| 3 | 작업 범위가 구체적인가? | ✅ | Phase 0-4, 전체 5개 코드 경로 명시 | +| 4 | 의존성이 명시되어 있는가? | ✅ | Phase 2↔4 의존, Phase 2/3 병렬 가능 | +| 5 | 참고 파일 경로가 정확한가? | ✅ | 메서드명 + 라인 번호 병행 참조 | +| 6 | 단계별 절차가 실행 가능한가? | ✅ | 코드 변경 예시 + 사전 조사 쿼리 포함 | +| 7 | 검증 방법이 명시되어 있는가? | ✅ | 전 Phase 테스트 케이스 + SQL 검증 쿼리 | +| 8 | 모호한 표현이 없는가? | ✅ | 용어 정의, 네이밍 규칙, 다중 개소 정책 명시 | +| 9 | 롤백 전략이 있는가? | ✅ | 섹션 6 - Phase별 롤백 방법 | +| 10 | 범위 밖 항목이 명시되어 있는가? | ✅ | 별도 이슈 추적 테이블 | + +### 14.2 새 세션 시뮬레이션 테스트 + +| 질문 | 답변 가능 | 참조 섹션 | +|------|:--------:|----------| +| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | +| Q2. 어디서부터 시작해야 하는가? | ✅ | 3.0 Phase 0 사전 조사 | +| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 10. 참고 파일 (메서드명+라인) | +| Q4. 작업 완료 확인 방법은? | ✅ | 9. 성공 기준 + 13. 검증 결과 (SQL 포함) | +| Q5. 막혔을 때 참고 문서는? | ✅ | 10. 참고 파일 + 5. DB 관계도 | +| Q6. 실패 시 어떻게 복원하는가? | ✅ | 6. 롤백 전략 | +| Q7. 이 계획 범위 밖인 것은? | ✅ | 11. 별도 이슈 추적 테이블 | + +--- + +*이 문서는 /plan 스킬로 생성되었습니다. v2: SuperClaude 페르소나 리뷰 반영.* \ No newline at end of file