Files
sam-docs/dev/dev_plans/bending-info-auto-generation-plan.md
권혁성 db63fcff85 refactor: [docs] 팀별 폴더 구조 재편 (공유/개발/프론트/기획)
- 개발팀 전용 폴더 dev/ 생성 (standards, guides, quickstart, changes, deploys, data, history, dev_plans 이동)
- 프론트엔드 전용 폴더 frontend/ 생성 (api/ → frontend/api-specs/)
- 기획팀 폴더 requests/ 생성
- plans/ → dev/dev_plans/ 이름 변경
- README.md 신규 (사람용 안내), INDEX.md 재작성 (Claude Code용)
- resources.md 신규 (노션 링크용, assets/brochure 이관 예정)
- CURRENT_WORKS.md 삭제, TODO.md → dev/ 이동
- 전체 참조 경로 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 16:46:03 +09:00

43 KiB
Raw Blame History

생산지시 시 bending_info 자동 생성 계획

작성일: 2026-02-19 목적: 수주 → 생산지시 시 절곡 공정의 bending_info JSON을 work_orders.options에 자동 삽입 기준 문서: api/app/Services/OrderService.php (createProductionOrder), react/.../bending/types.ts 상태: 🔄 진행중


📍 현재 진행 상태

항목 내용
마지막 완료 작업 분석 완료 + 계획 문서 작성
다음 작업 Phase 1.1: BendingInfoBuilder 서비스 생성
진행률 0/7 (0%)
마지막 업데이트 2026-02-20

1. 개요

1.1 배경

현재 절곡 작업일지(BendingWorkLogContent)에 표시할 bending_info 데이터를 수동으로 DB에 INSERT 해야 함. 수주(Order) → 생산지시(WorkOrder) 생성 시 OrderService::createProductionOrder()에서 자동으로 bending_info를 생성하여 work_orders.options.bending_info에 저장하는 로직이 필요함.

1.2 현재 데이터 흐름 vs 목표

현재 (Before)

OrderNode.options
  ├─ product_code: "FG-KSS02-벽면형-SUS"
  ├─ width: 3560, height: 4450
  └─ bom_result.items[]: (steel category BOM 품목)

→ OrderService::createProductionOrder() (라인 959)
  → WorkOrder::create() (라인 1111)
    → ⚠️ options 미설정 (null)
  → work_order_items INSERT (라인 1183)
    → options.bending_info = node.options.bending_info ?? null  (라인 1179)
    → ⚠️ 현재 order_nodes.options에 bending_info 없음 → null 저장

목표 (After)

OrderService::createProductionOrder() (라인 959)
  │
  ├─ 공정별 아이템 그룹핑 (라인 1035~1089)
  │   └─ $itemsByProcess[$processId] = [items...]
  │
  ├─ foreach ($itemsByProcess) → WorkOrder 생성 (라인 1103)
  │   │
  │   ├─ 절곡 공정인지 확인 (process.process_name === '절곡')
  │   │   └─ YES → BendingInfoBuilder::build($order, $processId)
  │   │            ├─ OrderNode.options.product_code 파싱
  │   │            ├─ OrderNode.options.bom_result.items 분석
  │   │            └─ bending_info JSON 조립
  │   │
  │   └─ WorkOrder::create([
  │        ...기존 필드들,
  │        'options' => ['bending_info' => $bendingInfo]  ← 신규
  │      ]) (라인 1111)
  │
  └─ work_order_items INSERT (라인 1183, 기존 유지)

핸들러 자동 생성 원리

BendingInfoBuilder::build($order, $processId)
    │
    ├─ 1. 절곡 공정 확인 (process.process_name === '절곡')
    │
    ├─ 2. product_code 파싱
    │   └─ "FG-KSS02-벽면형-SUS" → productCode: "KSS02", guideType: "벽면형", finishMaterial: "SUS마감"
    │
    ├─ 3. BOM items 카테고리 분류 (item_code 패턴 매칭)
    │   ├─ BD-가이드레일-* → guideRail
    │   ├─ BD-케이스-* → shutterBox
    │   ├─ BD-마구리-* → shutterBox (마구리)
    │   ├─ *하장바* → bottomBar
    │   ├─ EST-SMOKE-* → smokeBarrier
    │   ├─ BD-L-BAR-* → detailParts
    │   └─ BD-보강평철-* → detailParts
    │
    ├─ 4. 다중 노드 집계 (길이별 수량 합산)
    │   ├─ height → 가이드레일 길이별 수량
    │   ├─ width → 셔터박스/하단마감재 길이별 수량
    │   └─ BOM quantity × 노드 수 → 총 수량
    │
    └─ 5. BendingInfoExtended 구조 JSON 반환

1.3 기준 원칙

┌─────────────────────────────────────────────────────────────────┐
│  🎯 핵심 원칙                                                    │
├─────────────────────────────────────────────────────────────────┤
│  1. BendingInfoBuilder는 독립 서비스 (OrderService 변경 최소화)    │
│  2. 기존 createProductionOrder 흐름은 유지, options 삽입만 추가    │
│  3. order_nodes.options.bom_result + product_code가 유일한 소스   │
│  4. 프론트엔드 BendingInfoExtended 인터페이스 완전 호환            │
│  5. 절곡 공정이 아닌 WorkOrder에는 절대 bending_info 미생성        │
│  6. 기존 work_order_items.options.bending_info 흐름은 유지 (하위호환) │
└─────────────────────────────────────────────────────────────────┘

1.4 변경 승인 정책

분류 예시 승인
즉시 가능 BendingInfoBuilder 서비스 클래스 신규 생성, 헬퍼 메서드 추가 불필요
⚠️ 컨펌 필요 OrderService::createProductionOrder 수정 (options 삽입), BOM 파싱 규칙 확정 필수
🔴 금지 기존 BOM 계산 로직 변경, order_nodes 스키마 변경, 기존 work_order_items.options 구조 변경 별도 협의

2. 현황 분석

2.1 OrderService::createProductionOrder 흐름 (라인 959~1214)

현재 createProductionOrder는 다음 순서로 동작:

1. 수주 로드 (라인 966)
   $order = Order::with(['items', 'rootNodes'])->findOrFail($orderId)

2. 공정별 아이템 매핑 조회 (라인 1008~1014)
   DB::table('process_items') → $itemProcessMap

3. 아이템을 공정별로 그룹핑 (라인 1035~1089)
   3단계 fallback:
   ├─ item_id → process_items 직접 매핑 (라인 1041~1042)
   ├─ order_node_id → BOM item_name → process_items (라인 1045~1050)
   └─ item_code → item_id → process_items (라인 1054~1078)
   결과: $itemsByProcess[$processId] = ['items' => [...], 'processId' => int]

4. 공정별 WorkOrder 생성 (라인 1103)
   foreach ($itemsByProcess as $key => $group) {
       $workOrder = WorkOrder::create([...])  // 라인 1111~1124
       // ⚠️ 현재 'options' 필드 미설정
   }

5. work_order_items INSERT (라인 1183~1197)
   $woItemOptions = [
       'floor', 'code', 'width', 'height',
       'cutting_info', 'slat_info',
       'bending_info' => $nodeOptions['bending_info'] ?? null,  // 라인 1179
       'wip_info'
   ]

핵심 발견: WorkOrder::create (라인 1111~1124)에 options 필드가 전혀 설정되지 않음. bending_info는 work_order_items.options에만 들어가는데, 이마저도 order_nodes.options.bending_info가 null이면 null 저장.

2.2 order_nodes.options 구조 (실제 데이터)

order_id=43 (WO 74의 원천 수주)의 root_nodes (id=116~125, 5개소 × 2=10 노드):

{
  "product_code": "FG-KSS02-벽면형-SUS",
  "width": 3560,
  "height": 4450,
  "bom_result": {
    "items": [
      { "item_code": "BD-케이스-500*380", "item_name": "케이스 500*380", "category": "steel", "quantity": 3.62 },
      { "item_code": "BD-마구리-505*385", "item_name": "마구리 505*385", "category": "steel", "quantity": 1 },
      { "item_code": "BD-가이드레일-KSS02-SUS-120*70", "item_name": "가이드레일 KSS02...", "category": "steel", "quantity": 8.7 },
      { "item_code": "EST-SMOKE-레일용", "item_name": "연기차단재(레일)", "category": "steel", "quantity": 8.7 },
      { "item_code": "EST-SMOKE-케이스용", "item_name": "연기차단재(케이스)", "category": "steel", "quantity": 3.62 },
      { "item_code": "00035", "item_name": "철재용하장바(SUS)3000", "category": "steel", "quantity": 3.4 },
      { "item_code": "BD-L-BAR-KSS02-17*60", "item_name": "L-BAR KSS02...", "category": "steel", "quantity": 3.62 },
      { "item_code": "BD-보강평철-50", "item_name": "보강평철 50", "category": "steel", "quantity": 3.62 },
      // ... (parts, motor, controller 등 다른 category도 포함)
    ]
  }
}

중요: bom_result.items[]에는 category: "steel"인 아이템만 절곡(bending) 관련. parts, motor, controller 등은 다른 공정용.

2.3 work_orders.options 현재 상태

work_order_id process_name options
74 (수동 삽입) 절곡 {"bending_info": {...전체 JSON...}}
기타 절곡 외 null
  • WO 74만 수동으로 bending_info를 삽입한 상태
  • 다른 WorkOrder는 options가 null (createProductionOrder에서 미설정)

2.4 프론트엔드 데이터 소비 경로

API: GET /work-orders/{id}
  → WorkOrderService::show()
  → WorkOrder 모델 (options JSON 자동 디코딩)
  → API 응답: { ..., options: { bending_info: {...} } }

React: transformApiToFrontend() (types.ts 라인 495)
  → bendingInfo: api.options?.bending_info || undefined
  → BendingWorkLogContent에 props.bendingInfo로 전달
  → BendingInfoExtended 인터페이스로 사용

2.5 BendingInfoExtended 구조 (프론트엔드 목표 스키마)

// react/src/components/production/WorkOrders/documents/bending/types.ts (라인 32~68)
interface BendingInfoExtended {
  productCode: string;        // "KSS02"
  finishMaterial: string;     // "SUS마감"
  common: {
    kind: string;             // "혼합형 120X70"
    type: string;             // "벽면형(120*70)"
    lengthQuantities: LengthQuantity[];  // [{length: 4450, quantity: 5}]
  };
  detailParts: Array<{
    partName: string;         // "엘바", "하장바"
    material: string;         // "EGI 1.6T"
    barcyInfo: string;        // "16 I 75"
  }>;
  guideRail: {
    wall: GuideRailTypeData | null;   // 벽면형 가이드레일
    side: GuideRailTypeData | null;   // 측면형 가이드레일
  };
  bottomBar: {
    material: string;         // "SUS 1.5T"
    extraFinish: string;      // "없음"
    length3000Qty: number;
    length4000Qty: number;
  };
  shutterBox: ShutterBoxData[];  // [{direction, size, lengths[]}]
  smokeBarrier: {
    w50: LengthQuantity[];    // 레일용 W50
    w80Qty: number;           // 케이스용 W80 수량
  };
}

2.6 BOM item_code → bending_info 카테고리 매핑

item_code 패턴 bending_info 필드 추출 정보 확인된 실제 코드
BD-케이스-{W}*{H} shutterBox[].size 사이즈 "500*380" BD-케이스-500*380
BD-마구리-{W}*{H} shutterBox[].finCoverQty 마구리 수량 BD-마구리-505*385
BD-가이드레일-{model}-{finish}-{W}*{H} guideRail.wall/side baseSize "120*70" BD-가이드레일-KSS02-SUS-120*70
EST-SMOKE-레일용 smokeBarrier.w50 레일 연기차단재 수량 EST-SMOKE-레일용
EST-SMOKE-케이스용 smokeBarrier.w80Qty 케이스 연기차단재 수량 EST-SMOKE-케이스용
BD-L-BAR-{model}-{W}*{H} detailParts[] L-BAR 상세 BD-L-BAR-KSS02-17*60
BD-보강평철-{size} detailParts[] 보강평철 상세 BD-보강평철-50
*하장바* (item_name 기준) bottomBar 하장바 길이/마감 철재용하장바(SUS)3000 (코드: 00035)

2.7 product_code 파싱 규칙

FG-KSS02-벽면형-SUS 패턴:

세그먼트 위치 의미 추출 규칙 예시값
0 접두사 무시 (항상 "FG") FG
1 제품 모델 productCode KSS02
2 가이드레일 타입 guideType (벽면형/측면형/혼합형) 벽면형
3 마감재 finishMaterial → "SUS" → "SUS마감", "EGI" → "EGI마감" SUS

2.8 재질 매핑 (getMaterialMapping 기반)

Group 1 - SUS 전용 (KQTS01, KSS01, KSS02):
  guideRailFinish: "SUS 1.2T"
  bodyMaterial: "EGI 1.55T"
  bottomBarFinish: "SUS 1.5T"

Group 2 - KTE01 (마감유형 분기):
  SUS마감 → guideRailFinish: "EGI 1.55T" + extraFinish: "SUS 1.2T"
  EGI마감 → guideRailFinish: "EGI 1.55T" (extra 없음)

Group 3 - 기타 (KSE01, KWE01):
  SUS마감 → guideRailFinish: "EGI 1.55T" + extraFinish: "SUS 1.2T"
  EGI마감 → guideRailFinish: "EGI 1.55T" (extra 없음)

3. 대상 범위

Phase 1: BendingInfoBuilder 서비스 (백엔드 핵심)

# 작업 항목 상태 비고
1.1 BendingInfoBuilder 서비스 클래스 생성 api/app/Services/Production/BendingInfoBuilder.php
1.2 parseProductCode() 구현 "FG-KSS02-벽면형-SUS" → productCode, guideType, finishMaterial
1.3 categorizeBomItem() 구현 item_code 패턴 → 8개 카테고리 분류
1.4 aggregateNodes() 구현 다중 노드 → 길이별 수량 합산, 셔터박스 집계
1.5 build() 메인 메서드 구현 전체 조립 → BendingInfoExtended JSON 반환

Phase 2: createProductionOrder 통합

# 작업 항목 상태 비고
2.1 OrderService (라인 1111) WorkOrder::create에 options 추가 ⚠️ 컨펌 필요
2.2 절곡 공정 감지 로직 추가 Process 모델 조회 → process_name === '절곡'

Phase 3: 검증 및 테스트

# 작업 항목 상태 비고
3.1 WO 74 데이터로 역검증 (order_id=43, 동일 입력 → 동일 출력)
3.2 프론트엔드 작업일지 정상 렌더링 확인 BendingWorkLogContent
3.3 비절곡 공정 WorkOrder에 bending_info 미생성 확인

4. 작업 절차

4.1 단계별 절차

Phase 1: BendingInfoBuilder 서비스 생성
├── 1.1 파일 생성: api/app/Services/Production/BendingInfoBuilder.php
│   ├── 클래스: BendingInfoBuilder
│   └── 메서드: build(Order $order, int $processId): ?array
│
├── 1.2 product_code 파서 구현
│   ├── private parseProductCode(string $fullCode): array
│   ├── 입력: "FG-KSS02-벽면형-SUS"
│   └── 출력: ['productCode'=>'KSS02', 'guideType'=>'벽면형', 'finishMaterial'=>'SUS마감']
│
├── 1.3 BOM item_code 카테고리 분류기 구현
│   ├── private categorizeBomItem(array $bomItem): ?string
│   ├── 8개 패턴 매칭 (부록 A 참조)
│   └── 미매칭 → null 반환 (절곡 무관 품목)
│
├── 1.4 노드 집계 로직 구현
│   ├── private aggregateNodes(Collection $nodes): array
│   ├── height → 가이드레일 길이별 수량 (guideRailLengths)
│   ├── width → 셔터박스 길이별 수량 (shutterBoxLengths)
│   ├── BOM steel items → 카테고리별 수량 합산
│   └── 길이별 그룹핑 (동일 치수 노드는 수량 합산)
│
└── 1.5 build() 메인 메서드 조립
    ├── 절곡 공정 확인 → 아닌 경우 null 반환
    ├── parseProductCode → productCode, guideType, finishMaterial
    ├── aggregateNodes → 집계 데이터
    └── BendingInfoExtended 구조 JSON 조립 (부록 B 참조)

Phase 2: createProductionOrder 통합
├── 2.1 OrderService.php 수정 (라인 1111 부근)
│   ├── WorkOrder::create 호출 전 BendingInfoBuilder::build() 실행
│   ├── 절곡 공정인 경우에만 options 설정
│   └── 'options' => $bendingInfo ? ['bending_info' => $bendingInfo] : null
│
└── 2.2 Process 모델 사전 로드
    ├── 라인 1103 foreach 내에서 Process 조회
    └── 또는 $itemsByProcess에 process 정보 포함 (기존 로직 활용)

Phase 3: 검증
├── 3.1 order_id=43 (KSS02 벽면형 SUS 5개소 3560x4450) 데이터로 빌더 실행
│   ├── 기존 WO 74 bending_info와 구조 비교
│   └── productCode, guideRail, shutterBox, bottomBar, smokeBarrier 검증
│
├── 3.2 프론트엔드 렌더링 확인
│   ├── 새로 생성된 WorkOrder의 작업일지 열기
│   └── 4개 섹션 (가이드레일, 셔터박스, 하단마감재, 연기차단재) 정상 표시
│
└── 3.3 비절곡 공정 확인
    ├── 같은 수주에서 생성된 비절곡 WorkOrder의 options 확인
    └── bending_info가 없어야 함 (options: null 또는 bending_info 키 없음)

4.2 BendingInfoBuilder 서비스 설계

// api/app/Services/Production/BendingInfoBuilder.php
namespace App\Services\Production;

use App\Models\Orders\Order;
use App\Models\Production\Process;
use Illuminate\Support\Collection;

class BendingInfoBuilder
{
    /**
     * 수주의 노드/BOM 데이터로 bending_info JSON 생성
     *
     * @param Order $order  수주 (rootNodes eager loaded)
     * @param int $processId  공정 ID (절곡 공정 확인용)
     * @return array|null  bending_info JSON 또는 null (절곡 아닌 경우)
     */
    public function build(Order $order, int $processId): ?array
    {
        // 1. 절곡 공정인지 확인
        $process = Process::find($processId);
        if (!$process || $process->process_name !== '절곡') {
            return null;
        }

        // 2. 루트 노드가 없으면 null
        $nodes = $order->rootNodes;
        if ($nodes->isEmpty()) {
            return null;
        }

        // 3. 첫 번째 루트 노드에서 product_code 추출
        $firstNode = $nodes->first();
        $productInfo = $this->parseProductCode(
            $firstNode->options['product_code'] ?? ''
        );

        // 4. product_code 파싱 실패 시 null
        if (empty($productInfo['productCode'])) {
            return null;
        }

        // 5. 모든 노드의 BOM items 수집 및 집계
        $aggregated = $this->aggregateNodes($nodes, $productInfo);

        // 6. bending_info 구조 조립 (부록 B 참조)
        return $this->assembleBendingInfo($productInfo, $aggregated, $nodes);
    }
}

4.3 product_code 파서

/**
 * "FG-KSS02-벽면형-SUS" → ['productCode'=>'KSS02', 'guideType'=>'벽면형', 'finishMaterial'=>'SUS마감']
 */
private function parseProductCode(string $fullCode): array
{
    $parts = explode('-', $fullCode);

    // FG 접두사 제거
    if (($parts[0] ?? '') === 'FG') {
        array_shift($parts);
    }

    $finish = $parts[2] ?? 'EGI';

    return [
        'productCode' => $parts[0] ?? '',           // KSS02
        'guideType' => $parts[1] ?? '벽면형',        // 벽면형/측면형/혼합형
        'finishMaterial' => $finish === 'SUS' ? 'SUS마감' : 'EGI마감',
    ];
}

4.4 BOM item_code 카테고리 분류기

/**
 * BOM 아이템을 카테고리별로 분류
 * 반환값: guideRail, shutterBox_case, shutterBox_finCover, bottomBar,
 *         smokeBarrier_rail, smokeBarrier_case, detail_lbar, detail_reinforce, null
 */
private function categorizeBomItem(array $bomItem): ?string
{
    $code = $bomItem['item_code'] ?? '';
    $name = $bomItem['item_name'] ?? '';

    if (str_starts_with($code, 'BD-가이드레일'))      return 'guideRail';
    if (str_starts_with($code, 'BD-케이스'))           return 'shutterBox_case';
    if (str_starts_with($code, 'BD-마구리'))           return 'shutterBox_finCover';
    if (str_contains($name, '하장바'))                 return 'bottomBar';
    if ($code === 'EST-SMOKE-레일용')                  return 'smokeBarrier_rail';
    if ($code === 'EST-SMOKE-케이스용')                return 'smokeBarrier_case';
    if (str_starts_with($code, 'BD-L-BAR'))           return 'detail_lbar';
    if (str_starts_with($code, 'BD-보강평철'))         return 'detail_reinforce';

    return null; // 절곡 무관 품목 (parts, motor 등)
}

4.5 createProductionOrder 변경 포인트

// OrderService.php 라인 1103~1130 (수정 부분)

foreach ($itemsByProcess as $key => $group) {
    $processId = $group['processId'];
    $workOrderNo = $this->generateWorkOrderNo($tenantId, $order->id, $processId);

    // ★ 신규: 절곡 공정이면 bending_info 생성
    $options = null;
    if ($processId) {
        $bendingInfoBuilder = app(BendingInfoBuilder::class);
        $bendingInfo = $bendingInfoBuilder->build($order, $processId);
        if ($bendingInfo) {
            $options = ['bending_info' => $bendingInfo];
        }
    }

    $workOrder = WorkOrder::create([
        'tenant_id' => $tenantId,
        'work_order_no' => $workOrderNo,
        'sales_order_id' => $order->id,
        'project_name' => $order->order_no,
        'process_id' => $processId,
        'status' => WorkOrder::STATUS_PENDING,
        'assignee_id' => $data['assignee_id'] ?? null,
        'team_id' => $data['team_id'] ?? null,
        'scheduled_date' => $data['scheduled_date'] ?? null,
        'memo' => $data['memo'] ?? null,
        'options' => $options,                              // ★ 신규
        'is_active' => true,
        'created_by' => apiUserId(),
        'updated_by' => apiUserId(),
    ]);

    // ... 기존 work_order_items INSERT 로직 유지
}

5. 컨펌 대기 목록

# 항목 변경 내용 영향 범위 상태
1 OrderService 수정 createProductionOrder 라인 1111에 options 추가 생산지시 생성 전체 ⚠️ 컨펌 필요
2 item_code 패턴 매핑 BD-, EST-SMOKE-, 하장바 패턴으로 카테고리 분류 절곡 BOM 품목 인식 ⚠️ 컨펌 필요
3 product_code 파싱 FG-{code}-{type}-{finish} 4세그먼트 패턴 가정 모든 절곡 제품 ⚠️ 컨펌 필요

6. 변경 이력

날짜 항목 변경 내용 파일 승인
2026-02-19 - 문서 초안 작성 - -
2026-02-20 - formula-engine-real-data-plan.md 형식으로 전면 개편 (현황 분석, 코드 위치, 부록 추가) - -

7. 참고 문서

  • 현재 bending_info 구조: react/src/components/production/WorkOrders/documents/bending/types.ts (라인 32~68)
  • 재질 매핑 로직: react/src/components/production/WorkOrders/documents/bending/utils.ts (라인 77~108)
  • 생산지시 서비스: api/app/Services/OrderService.php (createProductionOrder, 라인 959~1214)
  • WorkOrder 서비스: api/app/Services/WorkOrderService.php (store, 라인 238~323)
  • WorkOrder 모델: api/app/Models/Production/WorkOrder.php
  • Order 모델: api/app/Models/Orders/Order.php (rootNodes, 라인 172~178)
  • 레거시 참고: 5130/output/proc/viewBendingWork_slat.php
  • WO 74 실데이터: order_id=43, order_nodes id=116~125 (KSS02 벽면형 SUS, 3560x4450)

8. 관련 파일 및 코드 위치

8.1 API (api/) - 핵심 코드 위치

파일 메서드/요소 라인 역할
Services/OrderService.php createProductionOrder() 959 메인 엔트리 (수주→생산지시)
같은 파일 Order::with(['items', 'rootNodes']) 966 수주 + 루트노드 로드
같은 파일 $itemsByProcess 그룹핑 1035~1089 공정별 아이템 분류 (3단계 fallback)
같은 파일 foreach ($itemsByProcess) 1103 공정별 WorkOrder 생성 루프
같은 파일 WorkOrder::create([...]) 1111~1124 ★ 변경 포인트: options 추가
같은 파일 $woItemOptions 구성 1172~1181 work_order_items.options 조립
같은 파일 'bending_info' => $nodeOptions['bending_info'] ?? null 1179 items 레벨 bending_info (기존, 유지)
같은 파일 DB::table('work_order_items')->insert() 1183~1197 items INSERT
같은 파일 $order->status_code = Order::STATUS_IN_PROGRESS 1204 수주 상태 변경
같은 파일 generateWorkOrderNo() 1270 작업지시 번호 생성
Services/WorkOrderService.php store() 238 대체 생성 경로 (수동 생성용)
같은 파일 'bending_info' => $nodeOptions['bending_info'] ?? null 279 items 레벨 bending_info (유지)
같은 파일 $workOrder->isBending() 306 절곡 공정 확인
신규 Services/Production/BendingInfoBuilder.php build() - Phase 1에서 생성
Models/Production/WorkOrder.php $fillable (options 포함) 32~51 options 필드 (라인 47)
같은 파일 $casts (options => json) 53~60 JSON 자동 변환 (라인 59)
같은 파일 isBending() 342~345 process.process_name === '절곡'
같은 파일 process() 관계 139~144 belongsTo(Process::class)
같은 파일 PROCESS_BENDING (deprecated) 80 상수 (미사용, FK 방식으로 전환됨)
Models/Orders/Order.php rootNodes() 172~178 hasMany(OrderNode)->whereNull('parent_id')

8.2 React (react/) - 프론트엔드 코드 위치

파일 요소 라인 역할
types.ts (WorkOrders/) WorkOrderApi.options 343 options?: { bending_info?: Record<string, unknown> }
같은 파일 transformApiToFrontend() 495 bendingInfo: api.options?.bending_info || undefined
같은 파일 item 레벨 bendingInfo (deprecated) 487 bendingInfo: undefined (명시적 무시)
같은 파일 WorkOrder.bendingInfo 210 프론트 모델 필드 정의
bending/types.ts BendingInfoExtended 32~68 목표 JSON 스키마
같은 파일 GuideRailTypeData 5~13 가이드레일 타입 데이터
같은 파일 ShutterBoxData 15~22 셔터박스 데이터
같은 파일 LengthQuantity 24~27 길이-수량 쌍
bending/utils.ts getMaterialMapping() 77~108 productCode → 재질 매핑

8.3 DB 테이블

work_orders 테이블 (변경 대상)

컬럼 타입 NULL 설명
id bigint unsigned NO PK
tenant_id bigint unsigned NO 테넌트
work_order_no varchar(50) NO 작업지시 번호
sales_order_id bigint unsigned YES 수주 FK
process_id bigint unsigned YES 공정 FK
options json YES ★ bending_info 저장 대상
status varchar(20) NO 상태
... ... ... (기타 필드)

order_nodes 테이블 (입력 소스)

컬럼 타입 NULL 설명
id bigint unsigned NO PK
order_id bigint unsigned NO 수주 FK
parent_id bigint unsigned YES 부모 노드 (root=NULL)
options json YES product_code, width, height, bom_result
sort_order int NO 정렬
quantity int NO 수량 (기본 1)

processes 테이블 (공정 판별)

컬럼 타입 NULL 설명
id bigint unsigned NO PK
tenant_id bigint unsigned NO 테넌트
process_name varchar(100) NO 공정명 ('절곡', '스크린', '슬랫' 등)

9. 검증 결과

9.1 테스트 케이스

입력값 예상 결과 실제 결과 상태
order_id=43 (KSS02 벽면형 SUS 5개소 3560x4450) productCode="KSS02", guideRail.wall 5개, shutterBox 1개 -
절곡 공정이 아닌 WorkOrder bending_info = null, options = null -
product_code 없는 노드 graceful fallback (null 반환) -
혼합형 제품 (벽면+측면) guideRail.wall + guideRail.side 둘 다 생성 -
동일 치수 복수 노드 수량 합산 (길이별 그룹핑) -
BOM에 steel 외 category 무시 (null → 스킵) -

9.2 성공 기준 달성 현황

기준 달성 비고
생산지시 시 절곡 WorkOrder에 bending_info 자동 생성
WO 74 수동 데이터와 동일 구조의 JSON 생성
프론트엔드 BendingWorkLogContent에서 정상 렌더링
비절곡 공정 WorkOrder에 bending_info 미생성
product_code 파싱 실패 시 graceful null 반환

부록 A. BOM item_code → bending_info 카테고리 전체 매핑

A.1 패턴 매칭 규칙 (우선순위 순)

순서 매칭 방식 패턴 카테고리 bending_info 필드
1 str_starts_with(code) BD-가이드레일-* guideRail guideRail.wall 또는 guideRail.side
2 str_starts_with(code) BD-케이스-* shutterBox_case shutterBox[].size
3 str_starts_with(code) BD-마구리-* shutterBox_finCover shutterBox[].finCoverQty
4 str_contains(name) *하장바* bottomBar bottomBar.length3000Qty/length4000Qty
5 exact match(code) EST-SMOKE-레일용 smokeBarrier_rail smokeBarrier.w50[]
6 exact match(code) EST-SMOKE-케이스용 smokeBarrier_case smokeBarrier.w80Qty
7 str_starts_with(code) BD-L-BAR-* detail_lbar detailParts[]
8 str_starts_with(code) BD-보강평철-* detail_reinforce detailParts[]
- 미매칭 (기타) null 무시 (절곡 무관)

A.2 가이드레일 item_code 파싱

BD-가이드레일-KSS02-SUS-120*70 → 세그먼트 분리:

세그먼트 용도
BD-가이드레일 접두사 카테고리 식별
KSS02 모델코드 (검증용)
SUS 마감재 (검증용)
120*70 baseSize guideRail.wall.baseSize 또는 guideRail.side.baseSize

A.3 셔터박스 item_code 파싱

케이스: BD-케이스-500*380shutterBox[].size = "500*380" 마구리: BD-마구리-505*385shutterBox[].finCoverQty += BOM quantity

A.4 하장바 item_name 파싱

철재용하장바(SUS)3000 → item_name 마지막 4자리 숫자 추출 → 3000/4000 분류

  • 3000 → bottomBar.length3000Qty += BOM quantity × 노드수
  • 4000 → bottomBar.length4000Qty += BOM quantity × 노드수

부록 B. bending_info JSON 조립 상세

B.1 목표 출력 구조 (WO 74 실데이터 기준)

{
  "productCode": "KSS02",
  "finishMaterial": "SUS마감",
  "common": {
    "kind": "벽면형 120X70",
    "type": "벽면형(120*70)",
    "lengthQuantities": [
      { "length": 4450, "quantity": 5 }
    ]
  },
  "detailParts": [
    { "partName": "엘바", "material": "EGI 1.6T", "barcyInfo": "16 I 75" },
    { "partName": "보강평철", "material": "50T", "barcyInfo": "" }
  ],
  "guideRail": {
    "wall": {
      "baseSize": "120*70",
      "finish": "SUS 1.2T",
      "extraFinish": "",
      "lengthQuantities": [
        { "length": 4450, "quantity": 5 }
      ]
    },
    "side": null
  },
  "bottomBar": {
    "material": "SUS 1.5T",
    "extraFinish": "없음",
    "length3000Qty": 17,
    "length4000Qty": 0
  },
  "shutterBox": [
    {
      "direction": "양면",
      "size": "500*380",
      "finCoverQty": 5,
      "lengths": [
        { "length": 3560, "quantity": 5 }
      ]
    }
  ],
  "smokeBarrier": {
    "w50": [
      { "length": 4450, "quantity": 5 }
    ],
    "w80Qty": 5
  }
}

B.2 조립 규칙 (필드별)

필드 소스 변환 규칙
productCode parseProductCode(product_code)[0] "KSS02"
finishMaterial parseProductCode(product_code)[2] "SUS" → "SUS마감"
common.kind guideType + baseSize "벽면형 120X70"
common.type guideType + "(baseSize)" "벽면형(120*70)"
common.lengthQuantities 노드 height별 수량 집계 [{length: 4450, quantity: 5}]
guideRail.wall/side guideType으로 분기 + getMaterialMapping baseSize, finish, lengthQuantities
bottomBar.material getMaterialMapping.bottomBarFinish "SUS 1.5T"
bottomBar.extraFinish getMaterialMapping.bottomBarExtraFinish "없음"
bottomBar.length3000Qty 하장바 BOM item_name → 3000 수량 합산 17 (= 3.4 × 5)
shutterBox[].direction 기본 "양면" (방향 정보 없음) "양면"
shutterBox[].size BD-케이스 item_code → 사이즈 추출 "500*380"
shutterBox[].finCoverQty BD-마구리 BOM quantity × 노드수 5
shutterBox[].lengths 노드 width별 수량 집계 [{length: 3560, quantity: 5}]
smokeBarrier.w50 EST-SMOKE-레일용 수량 → height 기준 집계 [{length: 4450, quantity: 5}]
smokeBarrier.w80Qty EST-SMOKE-케이스용 수량 합산 → 노드수 5

B.3 detailParts 매핑

BOM item_code 패턴 partName material 결정 방식 barcyInfo
BD-L-BAR-{model}-{W}*{H} "엘바" "{H}T" 에서 추출 (e.g., 17*60 → "EGI 1.6T") "{H/10} I {W}" (e.g., "16 I 75")
BD-보강평철-{size} "보강평철" "{size}T" (e.g., 50 → "50T") ""

detailParts의 정확한 material/barcyInfo 계산은 레거시 코드 참조 필요. Phase 1 구현 시 WO 74 실데이터와 비교하여 확정.


부록 C. 코드 변경 포인트 상세

C.1 OrderService.php 변경 (Phase 2.1)

파일: api/app/Services/OrderService.php 위치: 라인 1103~1130 (foreach ($itemsByProcess) 내부)

// 변경 전 (라인 1111~1124):
$workOrder = WorkOrder::create([
    'tenant_id' => $tenantId,
    'work_order_no' => $workOrderNo,
    'sales_order_id' => $order->id,
    'project_name' => $order->order_no,
    'process_id' => $processId,
    'status' => WorkOrder::STATUS_PENDING,
    'assignee_id' => $data['assignee_id'] ?? null,
    'team_id' => $data['team_id'] ?? null,
    'scheduled_date' => $data['scheduled_date'] ?? null,
    'memo' => $data['memo'] ?? null,
    // ⚠️ 'options' 없음
    'is_active' => true,
    'created_by' => apiUserId(),
    'updated_by' => apiUserId(),
]);

// 변경 후:
// ★ 절곡 공정이면 bending_info 생성
$options = null;
if ($processId) {
    $bendingInfo = app(BendingInfoBuilder::class)->build($order, $processId);
    if ($bendingInfo) {
        $options = ['bending_info' => $bendingInfo];
    }
}

$workOrder = WorkOrder::create([
    'tenant_id' => $tenantId,
    'work_order_no' => $workOrderNo,
    'sales_order_id' => $order->id,
    'project_name' => $order->order_no,
    'process_id' => $processId,
    'status' => WorkOrder::STATUS_PENDING,
    'assignee_id' => $data['assignee_id'] ?? null,
    'team_id' => $data['team_id'] ?? null,
    'scheduled_date' => $data['scheduled_date'] ?? null,
    'memo' => $data['memo'] ?? null,
    'options' => $options,                              // ★ 신규
    'is_active' => true,
    'created_by' => apiUserId(),
    'updated_by' => apiUserId(),
]);

C.2 BendingInfoBuilder 신규 생성 (Phase 1)

파일: api/app/Services/Production/BendingInfoBuilder.php (신규) 예상 코드 라인 수: 200~250줄

메서드 목록:
├── public build(Order $order, int $processId): ?array          (메인 엔트리)
├── private parseProductCode(string $fullCode): array           (product_code 파싱)
├── private categorizeBomItem(array $bomItem): ?string          (BOM 카테고리 분류)
├── private aggregateNodes(Collection $nodes, array $productInfo): array  (노드 집계)
├── private assembleBendingInfo(array $productInfo, array $aggregated, Collection $nodes): array  (JSON 조립)
├── private getMaterialMapping(string $productCode, string $finishMaterial): array  (재질 매핑)
├── private extractBaseSize(string $guideRailCode): string      (가이드레일 baseSize 추출)
└── private extractBottomBarLength(string $itemName): int       (하장바 길이 추출)

C.3 use 문 추가 (OrderService.php)

파일: api/app/Services/OrderService.php 위치: 파일 상단 use 섹션

use App\Services\Production\BendingInfoBuilder;

부록 D. 가이드레일 baseSize 규칙

D.1 모델별 baseSize 매핑

모델 guideType BD 품목 코드 예시 baseSize
KSS01 벽면형 BD-가이드레일-KSS01-SUS-120*70 120*70
KSS01 측면형 BD-가이드레일-KSS01-SUS-120*120 120*120
KSS02 벽면형 BD-가이드레일-KSS02-SUS-120*70 120*70
KSS02 측면형 BD-가이드레일-KSS02-SUS-120*120 120*120
KSE01 벽면형 BD-가이드레일-KSE01-{SUS/EGI}-120*70 120*70
KSE01 측면형 BD-가이드레일-KSE01-{SUS/EGI}-120*120 120*120
KWE01 벽면형 BD-가이드레일-KWE01-{SUS/EGI}-120*70 120*70
KWE01 측면형 BD-가이드레일-KWE01-{SUS/EGI}-120*120 120*120
KQTS01 벽면형 BD-가이드레일-KQTS01-SUS-130*75 130*75
KQTS01 측면형 BD-가이드레일-KQTS01-SUS-130*125 130*125
KTE01 벽면형 BD-가이드레일-KTE01-{SUS/EGI}-130*75 130*75
KTE01 측면형 BD-가이드레일-KTE01-{SUS/EGI}-130*125 130*125
KDSS01 벽면형 BD-가이드레일-KDSS01-SUS-150*150 150*150
KDSS01 측면형 BD-가이드레일-KDSS01-SUS-150*212 150*212

D.2 혼합형 처리

혼합형(guideType === '혼합형')인 경우:

  • guideRail.wall = 해당 모델의 벽면형 baseSize
  • guideRail.side = 해당 모델의 측면형 baseSize
  • BOM에 두 종류 가이드레일이 모두 포함됨

baseSize는 BOM의 BD-가이드레일-* item_code에서 마지막 세그먼트로 직접 추출 가능. 별도 매핑 테이블 불필요.


부록 E. 셔터박스/하단마감재/연기차단재 규칙

E.1 셔터박스 방향 결정

조건 direction 값
노드 1개 "양면" (기본값)
여러 노드 + 동일 치수 "양면" (기본값)
방향 정보 없음 (현재) "양면" 기본값 사용

방향 정보는 현재 order_nodes.options에 저장되지 않음. Phase 1에서는 "양면" 기본값 사용. 추후 BOM 확장 시 방향 필드 추가 가능.

E.2 하단마감재 길이 분류

BOM item_name 길이 추출 방법 분류
철재용하장바(SUS)3000 마지막 4자리 숫자 → 3000 bottomBar.length3000Qty
철재용하장바(SUS)4000 마지막 4자리 숫자 → 4000 bottomBar.length4000Qty
철재용하장바(EGI)3000 마지막 4자리 숫자 → 3000 bottomBar.length3000Qty

계산: BOM quantity × 노드 수 = 총 수량 (예: 3.4 × 5개소 = 17)

E.3 연기차단재 수량 계산

BOM item_code bending_info 필드 수량 계산
EST-SMOKE-레일용 smokeBarrier.w50[] height 기준 길이별 수량 집계
EST-SMOKE-케이스용 smokeBarrier.w80Qty BOM quantity × 노드수 → 정수 변환

E.4 재질 매핑 (getMaterialMapping 재현)

private function getMaterialMapping(string $productCode, string $finishMaterial): array
{
    // Group 1: SUS 전용 (KQTS01, KSS01, KSS02)
    if (in_array($productCode, ['KQTS01', 'KSS01', 'KSS02'])) {
        return [
            'guideRailFinish' => 'SUS 1.2T',
            'bodyMaterial' => 'EGI 1.55T',
            'guideRailExtraFinish' => '',
            'bottomBarFinish' => 'SUS 1.5T',
            'bottomBarExtraFinish' => '없음',
        ];
    }

    // Group 2: KTE01 (마감유형 분기)
    if ($productCode === 'KTE01') {
        $isSUS = $finishMaterial === 'SUS마감';
        return [
            'guideRailFinish' => 'EGI 1.55T',
            'bodyMaterial' => 'EGI 1.55T',
            'guideRailExtraFinish' => $isSUS ? 'SUS 1.2T' : '',
            'bottomBarFinish' => 'EGI 1.55T',
            'bottomBarExtraFinish' => $isSUS ? 'SUS 1.2T' : '없음',
        ];
    }

    // Group 3: 기타 (KSE01, KWE01 등)
    $isSUS = str_contains($finishMaterial, 'SUS');
    return [
        'guideRailFinish' => 'EGI 1.55T',
        'bodyMaterial' => 'EGI 1.55T',
        'guideRailExtraFinish' => $isSUS ? 'SUS 1.2T' : '',
        'bottomBarFinish' => 'EGI 1.55T',
        'bottomBarExtraFinish' => $isSUS ? 'SUS 1.2T' : '없음',
    ];
}

부록 F. WO 74 역검증용 데이터

F.1 입력 데이터 (order_id=43)

항목
수주 ID 43
root_nodes id=116~125 (10개, 5개소 × 2)
product_code FG-KSS02-벽면형-SUS
width 3560
height 4450
노드 수 5 (동일 치수)

F.2 기대 출력 (WO 74 기존 데이터와 일치해야 함)

필드 기대값
productCode "KSS02"
finishMaterial "SUS마감"
common.type "벽면형(120*70)"
common.lengthQuantities [{length: 4450, quantity: 5}]
guideRail.wall.baseSize "120*70"
guideRail.wall.finish "SUS 1.2T"
guideRail.wall.lengthQuantities [{length: 4450, quantity: 5}]
guideRail.side null
bottomBar.material "SUS 1.5T"
bottomBar.length3000Qty 17 (= 3.4 × 5)
bottomBar.length4000Qty 0
shutterBox[0].direction "양면"
shutterBox[0].size "500*380"
shutterBox[0].finCoverQty 5
shutterBox[0].lengths [{length: 3560, quantity: 5}]
smokeBarrier.w50 [{length: 4450, quantity: 5}]
smokeBarrier.w80Qty 5

10. 자기완결성 점검 결과

10.1 체크리스트 검증

# 검증 항목 상태 비고
1 작업 목적이 명확한가? 1.1 배경
2 성공 기준이 정의되어 있는가? 9.2
3 작업 범위가 구체적인가? 3 Phase + 부록
4 의존성이 명시되어 있는가? Phase 순서 = 의존성
5 참고 파일 경로 + 라인번호가 정확한가? 섹션 8 + 부록 C
6 단계별 절차가 실행 가능한가? 4.1~4.5 (코드 포함)
7 검증 방법이 명시되어 있는가? 섹션 9 + 부록 F
8 모호한 표현이 없는가? 구체적 코드/건수/라인번호

10.2 새 세션 시뮬레이션 테스트

질문 답변 가능 참조 섹션
Q1. 이 작업의 목적은 무엇인가? 1.1 배경
Q2. 어디서부터 시작해야 하는가? 3 Phase 1, 4.1 단계별 절차
Q3. OrderService의 어느 줄을 수정해야 하는가? 8.1 코드 위치 (라인 1111), 부록 C.1
Q4. BOM item_code 매핑 규칙은? 2.6 + 부록 A
Q5. product_code 파싱 방법은? 2.7 + 4.3 (코드 포함)
Q6. 프론트엔드 목표 스키마는? 2.5 BendingInfoExtended + 부록 B
Q7. 재질 매핑 규칙은? 2.8 + 부록 E.4 (코드 포함)
Q8. 어떻게 검증하는가? 9.1 테스트 케이스 + 부록 F
Q9. 가이드레일 baseSize는 어떻게 결정하는가? 부록 D (모델별 전체 매핑)
Q10. 기존 코드에 미치는 영향은? 1.3 원칙 6번, 부록 C (변경 포인트 상세)

결과: 10/10 통과 → 자기완결성 확보

10.3 보완 이력

날짜 항목 원본 보완 내용
2026-02-20 전체 초안 (간략 구조) formula-engine-real-data-plan.md 형식으로 전면 개편
2026-02-20 섹션 2 미존재 현황 분석 추가 (DB 데이터, 코드 분석, 스키마 상세)
2026-02-20 섹션 8 간략 목록 관련 파일 및 코드 위치 (정확한 라인번호 포함)
2026-02-20 부록 A~F 일부만 존재 6개 부록 완비 (BOM 매핑, JSON 조립, 코드 변경, 가이드레일, 셔터박스/하단마감재, 역검증)

이 문서는 /sc:plan 스킬로 생성되었습니다.