docs: [plans] Phase 0+1 완료, DB 밀어넣기 정책 반영

- Phase 0 조사 결과 기록 (product_code 0% → 보정 66.7%)
- Phase 1 전체 작업 항목 완료 처리
- 마이그레이션 파일 삭제, DB 1회 밀어넣기 정책 문서화
This commit is contained in:
2026-02-27 12:26:32 +09:00
parent a1ff455d72
commit 9d9d769086
2 changed files with 63 additions and 83 deletions

View File

@@ -2,7 +2,7 @@
> **작성일**: 2026-02-27 > **작성일**: 2026-02-27
> **목적**: 두 개선 계획(제품코드 추적성, 검사 단위 구조)을 하나의 순차적 실행 계획으로 통합 > **목적**: 두 개선 계획(제품코드 추적성, 검사 단위 구조)을 하나의 순차적 실행 계획으로 통합
> **상태**: 🔄 통합 계획 수립 완료, Phase 0 실행 대기 > **상태**: 🔄 Phase 0+1 완료, Phase 2 실행 대기
> **원본 문서**: > **원본 문서**:
> - [`product-code-traceability-plan.md`](./product-code-traceability-plan.md) (아카이브 참조) > - [`product-code-traceability-plan.md`](./product-code-traceability-plan.md) (아카이브 참조)
> - [`document-system-improvement-plan.md`](./document-system-improvement-plan.md) (아카이브 참조) > - [`document-system-improvement-plan.md`](./document-system-improvement-plan.md) (아카이브 참조)
@@ -15,9 +15,9 @@
| 항목 | 내용 | | 항목 | 내용 |
|------|------| |------|------|
| **마지막 완료 작업** | 통합 계획 수립 + 원본 2개 문서 분석 | | **마지막 완료 작업** | Phase 1 - product_code 전파 버그 수정 완료 |
| **다음 작업** | Phase 0 - 사전 데이터 조사 | | **다음 작업** | Phase 2A (절곡 검사 분석) + Phase 2B (견적/수주 정합성) |
| **진행률** | 0/7 Phase (0%) | | **진행률** | 2/7 Phase (Phase 0+1 완료) |
| **마지막 업데이트** | 2026-02-27 | | **마지막 업데이트** | 2026-02-27 |
--- ---
@@ -41,8 +41,8 @@
| Phase | 명칭 | 원본 | 의존성 | 상태 | 상세 | | Phase | 명칭 | 원본 | 의존성 | 상태 | 상세 |
|:-----:|------|------|--------|:----:|------| |:-----:|------|------|--------|:----:|------|
| **0** | 사전 데이터 조사 | product-code P0 | 없음 | | [Phase 0-1 상세](./integrated-phase-0-1.md) | | **0** | 사전 데이터 조사 | product-code P0 | 없음 | | [Phase 0-1 상세](./integrated-phase-0-1.md) |
| **1** | product_code 전파 버그 수정 | product-code P1 | Phase 0 | | [Phase 0-1 상세](./integrated-phase-0-1.md) | | **1** | product_code 전파 버그 수정 | product-code P1 | Phase 0 | | [Phase 0-1 상세](./integrated-phase-0-1.md) |
| **2A** | 절곡 검사 분석/설계 | document-system P1 | 없음 (**Phase 1과 병렬**) | ⏳ | [Phase 2 상세](./integrated-phase-2.md) | | **2A** | 절곡 검사 분석/설계 | document-system P1 | 없음 (**Phase 1과 병렬**) | ⏳ | [Phase 2 상세](./integrated-phase-2.md) |
| **2B** | 견적/수주 정합성 + 품질 FK | product-code P2+P3 | Phase 1 | ⏳ | [Phase 2 상세](./integrated-phase-2.md) | | **2B** | 견적/수주 정합성 + 품질 FK | product-code P2+P3 | Phase 1 | ⏳ | [Phase 2 상세](./integrated-phase-2.md) |
| **3** | 절곡 검사 동적 구현 | document-system P2 | Phase 1 + 2A | ⏳ | [Phase 3 상세](./integrated-phase-3.md) | | **3** | 절곡 검사 동적 구현 | document-system P2 | Phase 1 + 2A | ⏳ | [Phase 3 상세](./integrated-phase-3.md) |

View File

@@ -11,8 +11,9 @@
| 항목 | 내용 | | 항목 | 내용 |
|------|------| |------|------|
| **다음 작업** | Phase 0 - SQL 4개 실행 | | **마지막 완료 작업** | Phase 1 - 전체 완료 (백엔드 수정 + 마이그레이션 + 프론트) |
| **진행률** | 0% | | **다음 작업** | Phase 2A/2B (별도 문서) |
| **진행률** | Phase 0 + Phase 1 완료 |
| **마지막 업데이트** | 2026-02-27 | | **마지막 업데이트** | 2026-02-27 |
--- ---
@@ -25,10 +26,10 @@
| # | 작업 항목 | 상태 | 비고 | | # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------| |---|----------|:----:|------|
| 0.1 | `order_nodes.options``product_code` 보유율 조사 | | SQL 쿼리 | | 0.1 | `order_nodes.options``product_code` 보유율 조사 | | 114/120 (95.0%) |
| 0.2 | `work_order_items`에서 `source_order_item_id` NULL 비율 조사 | | 보정 불가 건수 파악 | | 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` 건수 조사 | | withTrashed 필요 여부 | | 0.3 | soft deleted된 `order_items`/`order_nodes` 건수 조사 | | order_items: 1772, order_nodes: 23 |
| 0.4 | `stock_lots.lot_no` 중복 건수 조사 | | Phase 2B 역추적 신뢰성 | | 0.4 | `stock_lots.lot_no` 중복 건수 조사 | | 3개 lot_no에 32건 중복 |
### 조사 쿼리 ### 조사 쿼리
@@ -39,11 +40,14 @@ SELECT COUNT(*) as total,
ROUND(SUM(CASE WHEN JSON_EXTRACT(options, '$.product_code') IS NOT NULL THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 1) as pct 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; FROM order_nodes WHERE deleted_at IS NULL;
-- 0.2: work_order_items의 source_order_item_id NULL 비율 -- 0.2: work_order_items의 source_order_item_id NULL 비율 + product_code 보유율
-- ⚠️ work_order_items에는 deleted_at 컬럼 없음 (soft delete 미사용)
SELECT COUNT(*) as total, SELECT COUNT(*) as total,
SUM(CASE WHEN source_order_item_id IS NULL THEN 1 ELSE 0 END) as no_source, 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 ROUND(SUM(CASE WHEN source_order_item_id IS NULL THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 1) as source_null_pct,
FROM work_order_items WHERE deleted_at IS NULL; 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된 원본 데이터 -- 0.3: soft deleted된 원본 데이터
SELECT 'order_items' as tbl, COUNT(*) as deleted_count FROM order_items WHERE deleted_at IS NOT NULL SELECT 'order_items' as tbl, COUNT(*) as deleted_count FROM order_items WHERE deleted_at IS NOT NULL
@@ -59,12 +63,18 @@ WHERE deleted_at IS NULL GROUP BY lot_no HAVING COUNT(*) > 1;
| 조사 항목 | 결과 | 판단 | | 조사 항목 | 결과 | 판단 |
|----------|------|------| |----------|------|------|
| order_nodes product_code 보유율 | | ⏳ | | order_nodes product_code 보유율 | **114/120 (95.0%)** | ✅ 원본 데이터 충분, 5%만 누락 |
| work_order_items source NULL 비율 | | ⏳ | | work_order_items source NULL 비율 | **2/546 (0.4%)** | ✅ 보정 불가 건수 극소 |
| soft deleted 원본 건수 | | ⏳ | | work_order_items product_code 보유율 | **0/546 (0.0%)** | 🔴 전파 완전 실패 — Phase 1 필수 |
| lot_no 중복 건수 | | ⏳ | | soft deleted 원본 건수 | order_items: 1,772건, order_nodes: 23건 | ⚠️ withTrashed 필수 (보정 시) |
| lot_no 중복 건수 | 3개 lot_no에 32건 | ⚠️ Phase 2B lot_no 역추적 시 1:N 처리 필요 |
> Phase 0 결과에 따라 Phase 1 보정 전략 조정 > **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 컬럼 없음)
--- ---
@@ -76,13 +86,13 @@ WHERE deleted_at IS NULL GROUP BY lot_no HAVING COUNT(*) > 1;
| # | 작업 항목 | 상태 | 비고 | | # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------| |---|----------|:----:|------|
| 1.1 | `OrderService::createProductionOrder` options 복사 수정 | | L1410-1419 | | 1.1 | `OrderService::createProductionOrder` options 복사 수정 | | product_code/product_name 추가 |
| 1.2 | `WorkOrderService::store` 수주복사 로직 수정 | | L287-296 | | 1.2 | `WorkOrderService::store` 수주복사 로직 수정 | | product_code/product_name 추가 |
| 1.3 | `WorkOrderService::store` 직접 입력 경로 확인 | | L311-317 (프론트 전달 의존) | | 1.3 | `WorkOrderService::store` 직접 입력 경로 확인 | | $item 전체 create → 수정 불필요 |
| 1.4 | `WorkOrderService::update` 품목 수정 시 options 보존 확인 | | L416-438 | | 1.4 | `WorkOrderService::update` 품목 수정 시 options 보존 확인 | | options 미포함 update → 기존값 보존 |
| 1.5 | 기존 데이터 보정 마이그레이션 | ⏳ | 스냅샷 백업 후 실행 | | 1.5 | 기존 데이터 보정 | ✅ | 로컬 SQL 보정 완료 (364/546). 마이그레이션 파일 불필요 — DB 1회 밀어넣기 |
| 1.6 | 프론트 WorkerScreen에 제품코드 표시 | | actions.ts + index.tsx | | 1.6 | 프론트 WorkerScreen에 제품코드 표시 | | options.product_code 우선, fallback: sales_order.item.code |
| 1.7 | 프론트 ProductionDashboard에 제품코드 표시 | | actions.ts | | 1.7 | 프론트 ProductionDashboard에 제품코드 표시 | | 동일 로직 적용 |
--- ---
@@ -144,52 +154,26 @@ $woItemOptions = array_filter([
--- ---
### 1.2 기존 데이터 보정 마이그레이션 ### 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 포함 **배포 전략**: 경동기업 마이그레이션 완료 시점에 로컬 DB를 개발/운영에 1회 밀어넣기로 해결.
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); 로컬 (Docker samdb)
if ($orderItem?->orderNode) { → 경동기업 마이그레이션 완료
$nodeOptions = $orderItem->orderNode->options ?? []; → 로컬 DB 덤프
$productCode = !empty($nodeOptions['product_code']) ? $nodeOptions['product_code'] : null; → 개발서버 import (1회)
$productName = !empty($nodeOptions['product_name']) ? $nodeOptions['product_name'] : null; → 운영서버 import (1회)
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 조사에서 건수 파악 후 감수 범위로 문서화. **로컬 보정 결과** (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 자동 전파됨.
--- ---
@@ -215,11 +199,11 @@ itemName: productCode !== '-' ? `${productCode} - ${productName}` : productName,
### 1.4 배포 순서 ### 1.4 배포 순서
``` ```
백엔드 배포 (5개 경로 수정) 백엔드 코드 배포 (2개 경로 수정: OrderService, WorkOrderService)
마이그레이션 실행 (스냅샷 백업 → 보정) 프론트엔드 코드 배포 (WorkerScreen + Dashboard)
프론트엔드 배포 (WorkerScreen + Dashboard) ※ 데이터 보정은 경동기업 마이그레이션 시 로컬 DB 1회 밀어넣기로 해결 (별도 마이그레이션 불필요)
``` ```
--- ---
@@ -244,23 +228,17 @@ itemName: productCode !== '-' ? `${productCode} - ${productName}` : productName,
### 데이터 검증 쿼리 ### 데이터 검증 쿼리
```sql ```sql
-- 보정 후 성공률 -- 보정 후 성공률 확인 (work_order_items에 deleted_at 없음)
SELECT COUNT(*) as total, SELECT COUNT(*) as total,
COUNT(CASE WHEN JSON_EXTRACT(options, '$.product_code') IS NOT NULL THEN 1 END) as with_code, 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 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; FROM work_order_items;
-- 실행 결과: 546 / 364 / 66.7
-- 보정 데이터 정확도 (원본 대조) -- 신규 생성 검증 (코드 수정 후 수주→작업지시 변환 시)
SELECT woi.id, SELECT woi.id, JSON_EXTRACT(woi.options, '$.product_code') as product_code
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 FROM work_order_items woi
JOIN order_items oi ON woi.source_order_item_id = oi.id ORDER BY woi.id DESC LIMIT 5;
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;
``` ```
--- ---
@@ -299,6 +277,8 @@ WHERE woi.deleted_at IS NULL
| 날짜 | 항목 | 변경 내용 | | 날짜 | 항목 | 변경 내용 |
|------|------|----------| |------|------|----------|
| 2026-02-27 | 문서 작성 | 통합 계획 Phase 0-1 상세 문서 작성 | | 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회 밀어넣기 정책 |
--- ---