- integrated-master-plan.md: 7 Phase 통합 마스터 (의존성 맵, 진행 관리) - integrated-phase-0-1.md: 사전 조사 + product_code 전파 수정 상세 - integrated-phase-2.md: 절곡 분석/설계 + 견적/품질 개선 상세 - integrated-phase-3.md: 절곡 검사 동적 구현 상세 - 원본 2개 문서 아카이브 전환 (통합 문서 링크 추가) - INDEX.md 통합 문서 등록
305 lines
12 KiB
Markdown
305 lines
12 KiB
Markdown
# 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 0 - SQL 4개 실행 |
|
|
| **진행률** | 0% |
|
|
| **마지막 업데이트** | 2026-02-27 |
|
|
|
|
---
|
|
|
|
## Phase 0: 사전 데이터 조사
|
|
|
|
**목표**: 마이그레이션 영향 범위 파악 (읽기 전용, 위험 없음)
|
|
|
|
### 작업 항목
|
|
|
|
| # | 작업 항목 | 상태 | 비고 |
|
|
|---|----------|:----:|------|
|
|
| 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 2B 역추적 신뢰성 |
|
|
|
|
### 조사 쿼리
|
|
|
|
```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;
|
|
```
|
|
|
|
### 검증 결과
|
|
|
|
| 조사 항목 | 결과 | 판단 |
|
|
|----------|------|------|
|
|
| order_nodes product_code 보유율 | | ⏳ |
|
|
| work_order_items source NULL 비율 | | ⏳ |
|
|
| soft deleted 원본 건수 | | ⏳ |
|
|
| lot_no 중복 건수 | | ⏳ |
|
|
|
|
> Phase 0 결과에 따라 Phase 1 보정 전략 조정
|
|
|
|
---
|
|
|
|
## Phase 1: product_code 전파 버그 수정
|
|
|
|
**목표**: 모든 `work_order_items` 생성/수정 경로에서 `product_code`, `product_name` 전달
|
|
|
|
### 작업 항목
|
|
|
|
| # | 작업 항목 | 상태 | 비고 |
|
|
|---|----------|:----:|------|
|
|
| 1.1 | `OrderService::createProductionOrder` options 복사 수정 | ⏳ | L1410-1419 |
|
|
| 1.2 | `WorkOrderService::store` 수주복사 로직 수정 | ⏳ | L287-296 |
|
|
| 1.3 | `WorkOrderService::store` 직접 입력 경로 확인 | ⏳ | L311-317 (프론트 전달 의존) |
|
|
| 1.4 | `WorkOrderService::update` 품목 수정 시 options 보존 확인 | ⏳ | L416-438 |
|
|
| 1.5 | 기존 데이터 보정 마이그레이션 | ⏳ | 스냅샷 백업 후 실행 |
|
|
| 1.6 | 프론트 WorkerScreen에 제품코드 표시 | ⏳ | actions.ts + index.tsx |
|
|
| 1.7 | 프론트 ProductionDashboard에 제품코드 표시 | ⏳ | actions.ts |
|
|
|
|
---
|
|
|
|
### 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 기존 데이터 보정 마이그레이션
|
|
|
|
```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}");
|
|
```
|
|
|
|
> `source_order_item_id`가 NULL인 건: 수동 생성 작업지시로 보정 불가. Phase 0 조사에서 건수 파악 후 감수 범위로 문서화.
|
|
|
|
---
|
|
|
|
### 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 배포 순서
|
|
|
|
```
|
|
백엔드 배포 (5개 경로 수정)
|
|
↓
|
|
마이그레이션 실행 (스냅샷 백업 → 보정)
|
|
↓
|
|
프론트엔드 배포 (WorkerScreen + Dashboard)
|
|
```
|
|
|
|
---
|
|
|
|
## 검증 결과
|
|
|
|
### Phase 1 테스트 케이스
|
|
|
|
| 테스트 | 예상 결과 | 실제 결과 | 상태 |
|
|
|--------|----------|----------|------|
|
|
| 신규 작업지시 (OrderService 경로) | options에 product_code 포함 | | ⏳ |
|
|
| 신규 작업지시 (WorkOrderService 수주복사) | options에 product_code 포함 | | ⏳ |
|
|
| product_code NULL인 order_nodes | 오류 없이 NULL 저장 | | ⏳ |
|
|
| product_code 빈 문자열 | empty 체크로 필터링 | | ⏳ |
|
|
| 데이터 보정 (source 있는 건) | product_code 채워짐 | | ⏳ |
|
|
| 데이터 보정 (source 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;
|
|
```
|
|
|
|
---
|
|
|
|
## 참고 파일
|
|
|
|
### 백엔드
|
|
|
|
| 파일 | 역할 | 주요 위치 |
|
|
|------|------|----------|
|
|
| `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 상세 문서 작성 |
|
|
|
|
---
|
|
|
|
*이 문서는 [`integrated-master-plan.md`](./integrated-master-plan.md)의 Phase 0-1 상세입니다.* |