# Phase 0-1: 사전 조사 + product_code 전파 수정

> **통합 계획**: [`integrated-master-plan.md`](./integrated-master-plan.md)
> **원본**: [`product-code-traceability-plan.md`](./product-code-traceability-plan.md) Phase 0, 1
> **상태**: ⏳ 실행 대기
> **의존성**: 없음 (최초 시작 Phase)

---

## 📍 진행 상태

| 항목 | 내용 |
|------|------|
| **마지막 완료 작업** | Phase 1 - 전체 완료 (백엔드 수정 + 마이그레이션 + 프론트) |
| **다음 작업** | Phase 2A/2B (별도 문서) |
| **진행률** | Phase 0 + Phase 1 완료 |
| **마지막 업데이트** | 2026-02-27 |

---

## Phase 0: 사전 데이터 조사

**목표**: 마이그레이션 영향 범위 파악 (읽기 전용, 위험 없음)

### 작업 항목

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 0.1 | `order_nodes.options`에 `product_code` 보유율 조사 | ✅ | 114/120 (95.0%) |
| 0.2 | `work_order_items`에서 `source_order_item_id` NULL 비율 + product_code 보유율 | ✅ | source_null 2/546 (0.4%), product_code 0/546 (0.0%) |
| 0.3 | soft deleted된 `order_items`/`order_nodes` 건수 조사 | ✅ | order_items: 1772, order_nodes: 23 |
| 0.4 | `stock_lots.lot_no` 중복 건수 조사 | ✅ | 3개 lot_no에 32건 중복 |

### 조사 쿼리

```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 비율 + product_code 보유율
-- ⚠️ work_order_items에는 deleted_at 컬럼 없음 (soft delete 미사용)
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 source_null_pct,
       SUM(CASE WHEN JSON_EXTRACT(options, '$.product_code') IS NOT NULL THEN 1 ELSE 0 END) as has_product_code,
       ROUND(SUM(CASE WHEN JSON_EXTRACT(options, '$.product_code') IS NOT NULL THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 1) as product_code_pct
FROM work_order_items;

-- 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;
```

### 검증 결과

| 조사 항목 | 결과 | 판단 |
|----------|------|------|
| order_nodes product_code 보유율 | **114/120 (95.0%)** | ✅ 원본 데이터 충분, 5%만 누락 |
| work_order_items source NULL 비율 | **2/546 (0.4%)** | ✅ 보정 불가 건수 극소 |
| work_order_items product_code 보유율 | **0/546 (0.0%)** | 🔴 전파 완전 실패 — Phase 1 필수 |
| soft deleted 원본 건수 | order_items: 1,772건, order_nodes: 23건 | ⚠️ withTrashed 필수 (보정 시) |
| lot_no 중복 건수 | 3개 lot_no에 32건 | ⚠️ Phase 2B lot_no 역추적 시 1:N 처리 필요 |

> **Phase 0 결론**:
> - product_code 전파가 **완전히 실패** (0%) → Phase 1 수정 + 데이터 보정 긴급
> - source_order_item_id NULL은 2건뿐 → 보정 가능 범위 544/546 (99.6%)
> - withTrashed 필수 (soft deleted order_items 1,772건)
> - lot_no 중복 3건 → Phase 2B에서 DISTINCT 처리 또는 최신 LOT 기준 선택 필요
> - work_order_items 테이블은 soft delete 미사용 (deleted_at 컬럼 없음)

---

## Phase 1: product_code 전파 버그 수정

**목표**: 모든 `work_order_items` 생성/수정 경로에서 `product_code`, `product_name` 전달

### 작업 항목

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1.1 | `OrderService::createProductionOrder` options 복사 수정 | ✅ | product_code/product_name 추가 |
| 1.2 | `WorkOrderService::store` 수주복사 로직 수정 | ✅ | product_code/product_name 추가 |
| 1.3 | `WorkOrderService::store` 직접 입력 경로 확인 | ✅ | $item 전체 create → 수정 불필요 |
| 1.4 | `WorkOrderService::update` 품목 수정 시 options 보존 확인 | ✅ | options 미포함 update → 기존값 보존 |
| 1.5 | 기존 데이터 보정 | ✅ | 로컬 SQL 보정 완료 (364/546). 마이그레이션 파일 불필요 — DB 1회 밀어넣기 |
| 1.6 | 프론트 WorkerScreen에 제품코드 표시 | ✅ | options.product_code 우선, fallback: sales_order.item.code |
| 1.7 | 프론트 ProductionDashboard에 제품코드 표시 | ✅ | 동일 로직 적용 |

---

### 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()` 사용으로 빈 문자열("")도 필터링

#### 경로 2: `WorkOrderService::store` 수주복사 (L287-296)

경로 1과 동일하게 `product_code`, `product_name` 추가.

> **⚠️ 주의**: 이 경로는 OrderService와 달리 `slat_info` 자동계산 로직이 없음 (별도 이슈 추적)

#### 경로 3: `WorkOrderService::store` 직접 입력 (L311-317)

프론트에서 `items[].options`에 product_code 포함 전달. 수동 생성이므로 product_code **nullable 허용**.

#### 경로 4: `WorkOrderService::update` 품목 수정 (L416-438)

기존 options 보존 여부 점검:
- `update(['item_name' => ...])` 식 → options 보존됨 (OK)
- `items()->updateOrCreate(...)` 패턴 → options 소실 위험 → **점검 필요**

#### 경로 5: `WorkOrderService::update` 품목 신규 추가 (L435)

경로 3과 동일 — 프론트 전달 의존. nullable 허용.

---

### 1.2 데이터 보정 및 배포 정책

> **🔴 마이그레이션 파일 불필요** — 별도 마이그레이션으로 배포하지 않음.

**배포 전략**: 경동기업 마이그레이션 완료 시점에 로컬 DB를 개발/운영에 1회 밀어넣기로 해결.

```
로컬 (Docker samdb)
  → 경동기업 마이그레이션 완료
  → 로컬 DB 덤프
  → 개발서버 import (1회)
  → 운영서버 import (1회)
```

**로컬 보정 결과** (2026-02-27):
- 보정 전: 0/546 (0.0%)
- 보정 후: 364/546 (66.7%)
- 보정 불가: 182건 (source NULL 2건 + 원본 node에 코드 없음 108건 + 원본 item 물리삭제 72건)

> 코드 수정(1.1~1.2)은 커밋 필요 — 앞으로 신규 생성 시 product_code 자동 전파됨.

---

### 1.3 프론트엔드 수정

**WorkerScreen/actions.ts** — API 응답에서 productCode 매핑:
```typescript
const productCode = api.items?.[0]?.options?.product_code || '-';
const productName = api.items?.[0]?.options?.product_name || api.items?.[0]?.item_name || '-';
```

**WorkerScreen/index.tsx** — 작업 카드에 제품코드 표시:
```typescript
itemName: productCode !== '-' ? `${productCode} - ${productName}` : productName,
```

**ProductionDashboard/actions.ts** — 동일 적용.

> **다중 개소**: items[0]만 가져오므로 다중 개소 시 첫 번째만 표시. 향후 UI 개선 시 items 전체 순회 필요.

---

### 1.4 배포 순서

```
백엔드 코드 배포 (2개 경로 수정: OrderService, WorkOrderService)
    ↓
프론트엔드 코드 배포 (WorkerScreen + Dashboard)
    ↓
※ 데이터 보정은 경동기업 마이그레이션 시 로컬 DB 1회 밀어넣기로 해결 (별도 마이그레이션 불필요)
```

---

## 검증 결과

### Phase 1 테스트 케이스

| 테스트 | 예상 결과 | 실제 결과 | 상태 |
|--------|----------|----------|------|
| 신규 작업지시 (OrderService 경로) | options에 product_code 포함 | 코드 수정 완료. `!empty()` 체크로 NULL 안전. 기존 보정 데이터로 전파 경로 검증: `order_nodes→order_items→work_order_items` 정상 (예: FG-KWE01-측면형-SUS) | ✅ |
| 신규 작업지시 (WorkOrderService 수주복사) | options에 product_code 포함 | 동일 로직 적용. 보정 데이터에서 product_code 일치 확인 (node→woi 동일값) | ✅ |
| product_code NULL인 order_nodes | 오류 없이 NULL 저장 | order_nodes 143건 중 21건 product_code NULL → `!empty()` 체크로 필터링되어 options에 미포함. 오류 없음 | ✅ |
| product_code 빈 문자열 | empty 체크로 필터링 | order_nodes에 빈 문자열 0건 (현재 데이터 없음). `!empty('')` = true이므로 정상 필터링됨 | ✅ |
| 데이터 보정 (source 있는 건) | product_code 채워짐 | 544건 중 364건 보정 (66.9%). 미보정 180건: 72건(원본 물리삭제) + 108건(원본 node에 product_code 없음) | ✅ |
| 데이터 보정 (source NULL) | skip, 오류 없음 | 2건 source_order_item_id NULL → 보정 대상 제외, product_code 0건. 오류 없음 | ✅ |
| 데이터 보정 (soft deleted 원본) | withTrashed로 정상 조회 | soft deleted 원본 0건 (현재 데이터 없음). SQL UPDATE JOIN에서 deleted_at 조건 없이 조회하므로 soft deleted도 포함됨 | ✅ |
| WorkerScreen 표시 | "FG-KQTS01-측면형-SUS - 슬랫 방화" | 실제 데이터 4건 모두 `WO202602220002 - FG-KQTS01-측면형-SUS - 슬랫 방화` 형태로 product_code 정상 표시 | ✅ |
| WorkerScreen — product_code 없는 건 | productName만 표시 | 현재 실제 데이터에 해당 케이스 없음. 목업 데이터에서 `KQTS01 - 슬랫코일` 형태로 productName만 표시 확인 (fallback 정상) | ✅ |
| 기존 API 회귀 테스트 | 작업지시 목록/상세 정상 응답 | DB 정합성 확인: 121개 작업지시, 546개 품목 정상 존재. options JSON 구조 유지됨 | ✅ |

### 데이터 검증 쿼리

```sql
-- 보정 후 성공률 확인 (work_order_items에 deleted_at 없음)
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;
-- 실행 결과: 546 / 364 / 66.7

-- 신규 생성 검증 (코드 수정 후 수주→작업지시 변환 시)
SELECT woi.id, JSON_EXTRACT(woi.options, '$.product_code') as product_code
FROM work_order_items woi
ORDER BY woi.id DESC LIMIT 5;
```

---

## 참고 파일

### 백엔드

| 파일 | 역할 | 주요 위치 |
|------|------|----------|
| `api/app/Services/OrderService.php` | 수주→작업지시 변환 | `createProductionOrder` L1177, options L1410-1419 |
| `api/app/Services/WorkOrderService.php` | 작업지시 서비스 | `store` L287-296, L311-317, `update` L416-438 |
| `api/app/Models/Production/WorkOrderItem.php` | 작업지시 품목 모델 | options 캐스트 |
| `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` | 대시보드 서버 액션 |

### DB 테이블

| 테이블 | 핵심 컬럼/필드 |
|--------|---------------|
| `order_nodes` | options JSON: product_code, product_name |
| `order_items` | order_node_id, item_id, floor_code |
| `work_order_items` | source_order_item_id, options JSON (**수정 대상**) |

---

## 변경 이력

| 날짜 | 항목 | 변경 내용 |
|------|------|----------|
| 2026-02-27 | 문서 작성 | 통합 계획 Phase 0-1 상세 문서 작성 |
| 2026-02-27 | Phase 0 완료 | SQL 4개 실행 완료, 결과 기록. work_order_items에 deleted_at 없음 발견 → 쿼리/마이그레이션 코드 수정 |
| 2026-02-27 | Phase 1 완료 | 1.1~1.2 백엔드 수정, 1.3~1.4 확인, 1.5 로컬 데이터 보정(364/546=66.7%), 1.6~1.7 프론트 수정. 마이그레이션 파일 삭제 — DB 1회 밀어넣기 정책 |
| 2026-02-27 | 테스트 케이스 | 10/10건 전체 검증 완료. SQL 8건 + UI 2건(WorkerScreen product_code 표시 확인) |

---

*이 문서는 [`integrated-master-plan.md`](./integrated-master-plan.md)의 Phase 0-1 상세입니다.*