# Phase 2: 절곡 검사 분석/설계 + 견적/품질 개선

> **통합 계획**: [`integrated-master-plan.md`](./integrated-master-plan.md)
> **원본**:
> - [`document-system-improvement-plan.md`](./document-system-improvement-plan.md) Phase 1
> - [`product-code-traceability-plan.md`](./product-code-traceability-plan.md) Phase 2, 3
> **상태**: ✅ Phase 2A+2B 완료
> **의존성**: Phase 2A는 독립 (Phase 1과 병렬 가능), Phase 2B는 Phase 1 완료 필수

---

## 1. Phase 2A: 절곡 검사 분석/설계

**목표**: 절곡 구성품(검사 항목) 정보를 API에서 제공하는 구조 설계
**Phase 1과 병렬 가능** (분석 전용, 코드 변경 없음)

### 1.1 작업 항목

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 2A.1 | 절곡 제품코드별 구성품(BOM) 데이터 구조 분석 | ✅ | 7개 모델 18종 FG, 150+ BD 구성품, `dynamic_bom` 발견 |
| 2A.2 | 마감유형(S1/S2/S3)별 차이 분석 | ✅ | 5130에서 S1/S2/S3별 갭 포인트 수·값 차이 확인 |
| 2A.3 | `inspection-config` 범용 API 설계 | ✅ | `dynamic_bom` 활용으로 설계 단순화. 상세 1.6절 참조 |
| 2A.4 | `DEFAULT_GAP_PROFILES` 기준치 5130 대조 | ✅ | Template 3값 오류, 측면형 전면 불일치. 상세 1.7절 참조 |

### 1.2 구성품 데이터 소스 분석 결과

```
분석 대상 → 결과:
1. items 테이블 — FG 18종, BD 150+ 종 등록 확인 ✅
2. bom_templates — 1건만 존재 (MATERIAL 참조). 제품별 BOM은 미등록
3. order_nodes.options.bending_info — 데이터 없음 (0건)
4. work_order_items.options.dynamic_bom — ✅ 핵심 발견! BOM 기반 구성품이 이미 저장됨
5. 5130/output/viewMidInspectBending.php — 갭 기준치 원본 (S1/S2/S3별)
6. TemplateInspectionContent DEFAULT_GAP_PROFILES — 일부 오류 확인 (1.7절)
```

### 1.2.1 핵심 발견: `dynamic_bom`

`work_order_items.options`에 BOM 기반 구성품이 **카테고리별로 이미 저장**되어 있음:

```json
{
  "dynamic_bom": [
    { "category": "guideRail", "part_type": "마감재", "child_item_code": "BD-SS-35", "length_mm": 3500, "qty": 6 },
    { "category": "guideRail", "part_type": "본체", "child_item_code": "BD-SM-35", "length_mm": 3500, "qty": 6 },
    { "category": "bottomBar", "part_type": "메인", "child_item_code": "BD-BS-40", "length_mm": 4000, "qty": 3 },
    { "category": "bottomBar", "part_type": "L-Bar", "child_item_code": "BD-LA-40", "length_mm": 4000, "qty": 3 },
    { "category": "shutterBox", "part_type": "전면부", "child_item_code": "BD-XX-35", "length_mm": 3500, "qty": 3 },
    { "category": "smokeBarrier", "part_type": "연기차단재(W50)", "child_item_code": "BD-GI-54", "length_mm": 4000, "qty": 6 },
    { "category": "smokeBarrier", "part_type": "연기차단재(W80)", "child_item_code": "BD-GI-83", "length_mm": 3000, "qty": 9 }
  ]
}
```

**카테고리 매핑**:

| dynamic_bom category | 검사 대상 구성품 | part_type 예시 |
|---------------------|----------------|---------------|
| `guideRail` | 가이드레일 | 마감재, 본체, C형, D형, 하부BASE |
| `bottomBar` | 하단마감재 | 메인, L-Bar, 보강평철 |
| `shutterBox` | 케이스 | 전면부, 린텔부, 점검구, 후면코너부, 상부덮개, 마구리 |
| `smokeBarrier` | 연기차단재 | W50, W80 |

### 1.2.2 완제품(FG) 품목 현황

| 모델 | 설치형 | 마감 | FG 코드 | 절곡 구성품 규격 |
|------|--------|------|---------|----------------|
| KWE01 | 벽면/측면 | SUS, EGI | FG-KWE01-* | 가이드레일 120×120/70, 하단마감재 64×43/60×40, L-BAR 17×60 |
| KSS01 | 벽면/측면 | SUS | FG-KSS01-* | 가이드레일 120×120/70, 하단마감재 60×40, L-BAR 17×60 |
| KSS02 | 벽면/측면 | SUS | FG-KSS02-* | 가이드레일 120×120/70, 하단마감재 60×40, L-BAR 17×60 |
| KQTS01 | 벽면/측면 | SUS | FG-KQTS01-* | 가이드레일 130×125/75, 하단마감재 60×30 |
| KTE01 | 벽면/측면 | SUS, EGI | FG-KTE01-* | 가이드레일 130×125/75, 하단마감재 64×34/60×30 |
| KSE01 | 벽면/측면 | SUS, EGI | FG-KSE01-* | 가이드레일 120×120/70, 하단마감재 64×43/60×40, L-BAR 17×60 |
| KDSS01 | — | SUS | — | 가이드레일 150×150/212, 하단마감재 140×78, L-BAR 17×100 |

### 1.3 구성품 결정 로직 (설계안)

```
입력: work_order_id
  ↓
1차: 작업지시 → 공정 자동 판별 (inspection-config API)
  ↓
2차: product_code → BOM 테이블에서 하위 구성품 조회
  ↓ (BOM 미등록 시)
3차: DEFAULT_GAP_PROFILES 기본값 사용
  ↓ (템플릿 미설정 시 = 레거시)
4차: INITIAL_PRODUCTS fallback (BendingInspectionContent, KWE01 하위호환)
```

각 단계의 역할은 다음과 같다.

- **1차 (공정 판별)**: `work_order_id`로부터 작업지시의 공정 타입(`bending`, `screen`, `slat`)을 자동 판별한다. 비절곡 공정이면 빈 `items` 배열을 반환하고 종료한다.
- **2차 (BOM 조회)**: 해당 제품코드의 BOM에 등록된 구성품 목록을 조회한다. BOM 데이터가 있으면 이를 기준으로 검사 항목을 구성한다.
- **3차 (기본 프로파일)**: BOM에 구성품이 등록되지 않은 경우, `DEFAULT_GAP_PROFILES`에 정의된 기본 갭 기준치를 사용한다.
- **4차 (레거시 fallback)**: 템플릿 설정도 없는 경우, 기존 `BendingInspectionContent`의 `INITIAL_PRODUCTS` 7개 항목을 KWE01 전용 하위호환으로 사용한다.

### 1.4 inspection-config API 설계안 (I5 정책 결정)

```
GET /api/v1/work-orders/{id}/inspection-config

※ BelongsToTenant 스코프 필수 (M1)
※ 공정 타입 자동 판별
```

**절곡 Response**:

```json
{
  "data": {
    "work_order_id": 123,
    "process_type": "bending",
    "product_code": "FG-KQTS01",
    "finish_type": "S1",
    "template_id": 60,
    "items": [
      {
        "id": "guide-rail-wall",
        "category": "KWE01",
        "product_name": "가이드레일",
        "product_type": "벽면형",
        "length_design": "3000",
        "width_design": "N/A",
        "gap_points": [
          { "point": "1", "design_value": "30" },
          { "point": "2", "design_value": "78" }
        ]
      }
    ]
  }
}
```

**비절곡 Response (스크린/슬랫)**:

```json
{
  "data": {
    "work_order_id": 456,
    "process_type": "screen",
    "product_code": "FG-KQTS01",
    "template_id": 12,
    "items": []
  }
}
```

**응답 필드 설명**:

| 필드 | 타입 | 설명 |
|------|------|------|
| `work_order_id` | `integer` | 작업지시 ID |
| `process_type` | `string` | 공정 타입 (`bending`, `screen`, `slat`) |
| `product_code` | `string` | 제품코드 |
| `finish_type` | `string\|null` | 마감유형 (절곡 전용: `S1`, `S2`, `S3`) |
| `template_id` | `integer\|null` | 검사 양식 ID |
| `items` | `array` | 검사 대상 구성품 목록 (비절곡 시 빈 배열) |
| `items[].id` | `string` | 구성품 식별자 (kebab-case) |
| `items[].category` | `string` | 제품 카테고리 코드 |
| `items[].product_name` | `string` | 구성품 명칭 |
| `items[].product_type` | `string` | 구성품 유형/규격 |
| `items[].length_design` | `string` | 설계 길이 |
| `items[].width_design` | `string` | 설계 폭 (`N/A` 가능) |
| `items[].gap_points` | `array` | 갭 측정 포인트 목록 |

### 1.6 inspection-config API 설계 (수정안)

`dynamic_bom` 발견으로 기존 설계안 대비 단순화됨:

```
입력: work_order_id
  ↓
1차: work_order → process 자동 판별 (bending/screen/slat)
  ↓ (비절곡이면 빈 items 반환)
2차: work_order_items.options.dynamic_bom → 카테고리별 구성품 추출
  ↓
3차: 카테고리 + 마감유형(S타입) → 갭 기준치 매핑 (GAP_PROFILES 테이블 or 상수)
  ↓ (dynamic_bom 없으면)
4차: DEFAULT_GAP_PROFILES fallback
  ↓ (템플릿 미설정 = 레거시)
5차: INITIAL_PRODUCTS fallback (BendingInspectionContent)
```

**기존 설계 대비 변경점**:
- 2차에서 BOM 테이블 조회 → `dynamic_bom` JSON 직접 사용 (DB 조회 불필요)
- 케이스 갭 포인트: 고정값 → `dynamic_bom`의 `length_mm` 기반 동적 계산

### 1.7 DEFAULT_GAP_PROFILES 5130 대조 결과

**원본 소스**: `5130/output/viewMidInspectBending.php` (L170-786)

#### 가이드레일 벽면형

| 포인트 | 5130 S1 | 5130 S2 | 5130 S3 | Template (신규) | Bending (레거시) | 판정 |
|:------:|:-------:|:-------:|:-------:|:---------------:|:---------------:|:----:|
| (1) | 30 | 30 | 30 | 30 | 30 | ✅ |
| (2) | **80** | **80** | **80** | **78** | **80** | ❌ Template |
| (3) | **45** | **45** | **45** | **25** | **45** | ❌ Template |
| (4) | **40** | — | **40** | **45** | **40** | ❌ Template |
| (5) | — | — | 34 | — | 34 | S3 전용 |

→ **Bending(레거시)이 5130과 일치**, Template에 3개 값 오류

#### 가이드레일 측면형

| 포인트 | 5130 S1 | Template | Bending | 판정 |
|:------:|:-------:|:--------:|:-------:|:----:|
| (1) | **30** | 28 | 28 | ❌ 둘 다 |
| (2) | **70** | 75 | 75 | ❌ 둘 다 |
| (3) | **45** | 42 | 42 | ❌ 둘 다 |
| (4) | **35** | 38 | 38 | ❌ 둘 다 |
| (5) | **95** | 32 | 32 | ❌ 둘 다 |
| (6) | 90 | — | — | 누락 |

→ **Template과 Bending 모두 5130 S1과 불일치** — 별도 근거 확인 필요 (다른 S타입 or 버전)

#### 케이스

| 포인트 | 5130 | Template | Bending | 판정 |
|:------:|:----:|:--------:|:-------:|:----:|
| (1) | boxheight (동적) | 550 | 380 | 5130=동적계산 |
| (2) | frontbottom/50 (동적) | 50 | 50 | — |
| (3) | 계산식 (동적) | 385 | 240 | 5130=동적계산 |
| (4) | frontbottom/boxheight | 50 | 50 | — |
| (5) | boxheight-140 | 410 | — | 5130=동적계산 |

→ **5130은 주문 정보(boxwidth/boxheight)에서 동적 계산** — 두 컴포넌트 모두 특정 사이즈 고정값

#### 하단마감재

| 포인트 | 5130 S1/S2 | 5130 S3 | Template | Bending | 판정 |
|:------:|:----------:|:-------:|:--------:|:-------:|:----:|
| (1) | 60 | 60 | 60 | 60 | ✅ |
| (2) | — | 64 | — | 64 | S3 전용 |

→ Bending이 S3까지 커버, Template은 S1/S2만

#### 하단 L-BAR / 연기차단재

| 항목 | 5130 | Template | Bending | 판정 |
|------|:----:|:--------:|:-------:|:----:|
| L-BAR (1) | 17 | — | 17 | ✅ (Template에 없음) |
| 연기차단재 W50 | 50, 12 | 50, 12 | 50, 12 | ✅ 3자 일치 |
| 연기차단재 W80 | 80, 12 | 80, 12 | 80, 12 | ✅ 3자 일치 |

#### 5130 마감유형별 갭 포인트 수 정리

| 구성품 | S1 | S2 (KSS02형) | S3 (별도마감형) |
|--------|:--:|:--:|:--:|
| 가이드레일 벽면 | 4점 | 3점 | 5점 |
| 가이드레일 측면 | 6점 | 5점 | 7점 |
| 하단마감재 | 1점 | 1점 | 2점 |
| L-BAR | 1점 | 1점 | 1점 |
| 케이스 | 동적 (점검구 방향별) | 동적 | 동적 |
| 연기차단재 | 2점 | 2점 | 2점 |

#### Phase 3 대응 방향 (I1: Single Source of Truth)

1. **DEFAULT_GAP_PROFILES 수정 필요**: 벽면형 3값 오류 → 5130 기준으로 보정
2. **측면형 재검토**: Template/Bending 모두 5130 S1과 다름 → 사용 중인 실제 버전 확인 필요
3. **케이스 동적 계산**: `dynamic_bom`의 치수 정보 활용하여 동적 계산 구현
4. **마감유형 분기**: S1/S2/S3별 갭 포인트 수 차이 → inspection-config API에서 처리

### 1.8 현재 하드코딩 현황 (레거시 동결 — C3)

`BendingInspectionContent.tsx`의 `INITIAL_PRODUCTS` (7개, KWE01 전용):

| # | 항목 ID | productName | productType | gapPoints 수 |
|---|---------|-------------|-------------|:----------:|
| 1 | `guide-rail-wall` | 가이드레일 | 벽면형 | 5 |
| 2 | `guide-rail-side` | 가이드레일 | 측면형 | 5 |
| 3 | `case` | 케이스 | 500X380 | 4 |
| 4 | `bottom-finish` | 하단마감재 | 60X40 | 2 |
| 5 | `bottom-l-bar` | 하단L-BAR | 17X60 | 1 |
| 6 | `smoke-w50` | 연기차단재 | W50 가이드레일용 | 2 |
| 7 | `smoke-w80` | 연기차단재 | W80 케이스용 | 2 |

Phase 2A 분석 완료 후, 이 하드코딩 항목들은 `inspection-config` API 응답으로 대체될 예정이다. 현재는 C3(레거시 동결) 정책에 따라 수정하지 않는다.

---

## 2. Phase 2B: 견적/수주 정합성 + 품질검사 FK

**목표**: `quotes.product_code` 활용 + `inspections` ↔ `work_orders` FK 연결
**선행 조건**: Phase 1 완료
**내부 병렬성**: 2B-견적과 2B-품질은 독립 경로

```
Phase 2B 내부 구조:

  Phase 1 완료
       |
  +----+----+
  |         |
2B-견적   2B-품질
(2B.1~3)  (2B.4~7)
  |         |
  +----+----+
       |
    Phase 2B 완료
```

### 2.1 견적 데이터 정합성 (원본 Phase 2)

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 2B.1 | 견적 저장 시 `quotes.product_code` 저장 | ✅ | `extractProductCodeFromInputs()` 자동 추출 추가 |
| 2B.2 | 견적→수주 변환 시 `camelCase`→`snake_case` 변환 확인 | ✅ | L673 이미 정상 동작 확인 |
| 2B.3 | 기존 데이터 보정 스크립트 | ✅ | 25/46건 보정 완료 (21건 초기 데이터 productCode 없음) |

**다중 개소 정책**: `quotes.product_code`에는 첫 번째 개소 코드를 대표값으로 저장한다. 전체 목록은 `calculation_inputs.items[].productCode`를 참조한다.

**의존성 주의**: `orders.item_id` 설정은 `items` 테이블에 FG 품목 등록이 필요하므로 Phase 5에서 처리한다.

**데이터 보정 스크립트 상세**:

```php
// 2B.3 보정 로직 개요
// quotes 테이블에서 product_code가 null인 레코드 대상
// calculation_inputs JSON에서 items[0].productCode 추출하여 저장

$quotes = Quote::whereNull('product_code')
    ->whereNotNull('calculation_inputs')
    ->get();

foreach ($quotes as $quote) {
    $inputs = json_decode($quote->calculation_inputs, true);
    $productCode = $inputs['items'][0]['productCode'] ?? null;
    if ($productCode) {
        $quote->update(['product_code' => $productCode]);
    }
}
```

### 2.2 품질검사 연결 강화 (원본 Phase 3)

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 2B.4 | `inspections` 테이블에 `work_order_id` FK 마이그레이션 | ✅ | 마이그레이션 실행 완료, nullable + nullOnDelete |
| 2B.5 | `Inspection` 모델에 `workOrder()` 관계 메서드 추가 | ✅ | 양방향: Inspection→workOrder, WorkOrder→inspections |
| 2B.6 | 품질검사 생성 시 `work_order_id` 설정 로직 | ✅ | store/show/index + transformToFrontend 업데이트 |
| 2B.7 | 기존 `inspections` 데이터에 `work_order_id` 보정 | ✅ | 대상 0건 — 보정 불필요 |

**마이그레이션 설계 (2B.4)**:

```php
Schema::table('inspections', function (Blueprint $table) {
    $table->unsignedBigInteger('work_order_id')->nullable()->after('id');
    $table->foreign('work_order_id')
          ->references('id')
          ->on('work_orders')
          ->nullOnDelete();
    $table->index('work_order_id');
});
```

**모델 관계 (2B.5)**:

```php
// Inspection.php
public function workOrder(): BelongsTo
{
    return $this->belongsTo(WorkOrder::class);
}

// WorkOrder.php
public function inspections(): HasMany
{
    return $this->hasMany(Inspection::class);
}
```

**역추적 보정 로직 (2B.7)**:

```
inspections.lot_no → work_order_items.lot_no → work_orders.id
  |
중복 검사: 동일 lot_no에 다수 work_order 매칭 시 경고 로그
  |
MATCH: work_order_id 설정
NO_MATCH: null 유지 (수동 보정 대상)
```

---

## 3. 검증 결과

### 3.1 Phase 2A 검증

| 조사 항목 | 결과 | 판단 |
|----------|------|------|
| KWE01 구성품 | 가이드레일(SUS/EGI 120×120/70), 하단마감재(SUS 64×43, EGI 60×40), L-BAR 17×60 | ✅ items 등록 확인 |
| KSS01 구성품 | 가이드레일(SUS 120×120/70), 하단마감재(SUS 60×40), L-BAR 17×60 | ✅ items 등록 확인 |
| KSS02 구성품 | 가이드레일(SUS 120×120/70), 하단마감재(SUS 60×40), L-BAR 17×60 | ✅ items 등록 확인 |
| KQTS01 구성품 | 가이드레일(SUS 130×125/75), 하단마감재(SUS 60×30) | ✅ L-BAR 없음 |
| `dynamic_bom` 존재 | `work_order_items.options`에 카테고리별 BOM 저장됨 (1건 확인) | ✅ 핵심 발견 |
| 마감유형(S1/S2/S3)별 차이 | 갭 포인트 수·값 차이. 벽면 S1=4점, S2=3점, S3=5점 | ✅ 상세 1.7절 |
| DEFAULT_GAP_PROFILES 5130 대조 | 벽면형 3값 오류, 측면형 전면 불일치, 케이스 동적계산 | ✅ 상세 1.7절 |
| inspections 테이블 | 0건 (데이터 없음), `work_order_id` 컬럼 미존재 | ✅ 2B.4 FK 추가 필요 |
| quotes.product_code | 컬럼 존재, 49건 중 0건 채워짐 | ✅ 2B.1 저장 로직 + 2B.3 보정 필요 |

### 3.2 Phase 2B 검증

| 테스트 | 예상 결과 | 실제 결과 | 상태 |
|--------|----------|----------|------|
| 견적 저장 시 `quotes.product_code` | 첫 번째 개소 코드 | T5.1: `extractProductCodeFromInputs('FG-TEST-001')` → `FG-TEST-001` ✅ | ✅ |
| 다중 개소 대표 코드 | 첫 번째 개소 | T5.4: 2개소 `[FG-FIRST, FG-SECOND]` → `FG-FIRST` 반환 ✅ | ✅ |
| CI 없는 경우 null 반환 | null | T5.2: `{}` → `null`, T5.3: `{items:[]}` → `null` ✅ | ✅ |
| 견적→수주 변환 `camelCase`→`snake_case` | 정상 변환 | T4: order_nodes 5건 확인 — `$.product_code` 존재, `$.productCode` NULL | ✅ |
| `inspections.work_order_id` FK 마이그레이션 | 성공, `nullable` | T2: `bigint unsigned`, MUL 인덱스, FK→`work_orders.id` 확인 | ✅ |
| Inspection 모델/서비스 회귀 | 정상 | T3: fillable YES, workOrder() YES, inspections() YES, index() total=0 OK | ✅ |
| 기존 데이터 보정 (`quotes`) | 보정 완료 | T1: 25/49건 보정 (21건 CI에 productCode 없음, 3건 CI 자체 없음) | ✅ |

---

## 4. 참고 파일

### 4.1 Phase 2A 관련

| 파일 | 역할 |
|------|------|
| `react/.../documents/TemplateInspectionContent.tsx` | `DEFAULT_GAP_PROFILES` (L184-214), `buildBendingProducts` (L217-282) |
| `react/.../documents/BendingInspectionContent.tsx` | `INITIAL_PRODUCTS` (L71-135, 레거시 동결) |
| `5130/output/viewMidInspectBending.php` | 절곡 중간검사 성적서 원본 (L170-786, 갭 기준치) |
| `5130/estimate/common/common_addrowJS.php` | 레거시 구성품 정의 |

### 4.2 Phase 2B 관련

| 파일 | 역할 |
|------|------|
| `api/app/Services/Quote/QuoteService.php` | 견적 서비스 (`product_code` L324) |
| `api/app/Services/InspectionService.php` | 품질검사 서비스 |
| `api/app/Models/Quality/Inspection.php` | 검사 모델 |

---

## 5. 변경 이력

| 날짜 | 항목 | 변경 내용 |
|------|------|----------|
| 2026-02-27 | 문서 작성 | 통합 계획 Phase 2 상세 문서 작성 |
| 2026-02-27 | 2A 분석 완료 | BOM 구조 분석(dynamic_bom 발견), 마감유형 S1/S2/S3 차이 분석, inspection-config API 재설계, DEFAULT_GAP_PROFILES 5130 대조 완료. 1.2~1.7절 추가 |
| 2026-02-27 | 2B 구현 완료 | 견적 product_code 자동추출(2B.1), camelCase 확인(2B.2), 25건 보정(2B.3), inspections.work_order_id FK(2B.4), 양방향 관계(2B.5), 서비스 업데이트(2B.6), 0건 보정(2B.7) |
| 2026-02-27 | 2B 테스트 완료 | SQL 4건(T1~T4) + 단위테스트 4건(T5.1~T5.4) 전항 PASS. 검증 결과 3.2절 업데이트 |
