Files
sam-docs/plans/integrated-phase-0-1.md
권혁성 666c80c350 docs: [통합계획] 제품코드 추적성 + 검사 단위 구조 통합 계획 수립
- integrated-master-plan.md: 7 Phase 통합 마스터 (의존성 맵, 진행 관리)
- integrated-phase-0-1.md: 사전 조사 + product_code 전파 수정 상세
- integrated-phase-2.md: 절곡 분석/설계 + 견적/품질 개선 상세
- integrated-phase-3.md: 절곡 검사 동적 구현 상세
- 원본 2개 문서 아카이브 전환 (통합 문서 링크 추가)
- INDEX.md 통합 문서 등록
2026-02-27 10:15:19 +09:00

12 KiB

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

통합 계획: integrated-master-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.optionsproduct_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 역추적 신뢰성

조사 쿼리

-- 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)

현재 코드:

$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);

수정 후:

$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::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 매핑:

const productCode = api.items?.[0]?.options?.product_code || '-';
const productName = api.items?.[0]?.options?.product_name || api.items?.[0]?.item_name || '-';

WorkerScreen/index.tsx — 작업 카드에 제품코드 표시:

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 회귀 테스트 작업지시 목록/상세 정상 응답

데이터 검증 쿼리

-- 보정 후 성공률
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의 Phase 0-1 상세입니다.