Files
sam-docs/plans/integrated-phase-2.md

446 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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절 업데이트 |