# 견적 V2 자동 견적 산출 오류 수정 계획

> **작성일**: 2026-01-26
> **목적**: 자동 견적 산출 기능의 4가지 오류 분석 및 수정
> **기준 문서**: `QuoteRegistrationV2.tsx`, `LocationDetailPanel.tsx`, `QuoteSummaryPanel.tsx`, `actions.ts`
> **상태**: ✅ 완료

---

## 📍 현재 진행 상태

| 항목 | 내용 |
|------|------|
| **마지막 완료 작업** | 테스트 및 검증 완료 |
| **다음 작업** | - |
| **진행률** | 4/4 (100%) ✅ |
| **마지막 업데이트** | 2026-01-26 |

---

## 1. 개요

### 1.1 배경
견적 V2 페이지(`/sales/quote-management/test-new`)에서 자동 견적 산출 버튼 클릭 후 다음 4가지 문제 발생:
1. 오른쪽 패널에 제품 리스트가 표시되지 않음
2. 개소별 합계(상세소계)가 표시되지 않음
3. 상세별 합계(그룹)가 표시되지 않음
4. 예상 견적금액이 0원으로 표시됨

### 1.2 기준 원칙
```
┌─────────────────────────────────────────────────────────────────┐
│  🎯 핵심 원칙                                                   │
├─────────────────────────────────────────────────────────────────┤
│  - API 응답 구조와 프론트엔드 기대 구조의 일치 확보             │
│  - Mock 데이터 fallback 로직은 디버깅/테스트용으로만 유지       │
│  - 실제 BOM 계산 결과가 UI에 정확히 반영되도록 수정             │
└─────────────────────────────────────────────────────────────────┘
```

### 1.3 변경 승인 정책

| 분류 | 예시 | 승인 |
|------|------|------|
| ✅ 즉시 가능 | 타입 캐스팅 수정, 데이터 매핑 로직 수정 | 불필요 |
| ⚠️ 컨펌 필요 | API 응답 구조 변경, 새 인터페이스 정의 | **필수** |
| 🔴 금지 | API 엔드포인트 변경, DB 스키마 변경 | 별도 협의 |

---

## 2. 근본 원인 분석

### 2.1 API 응답 구조 불일치 (핵심 원인)

**API 실제 응답** (`actions.ts:962-965`):
```typescript
return {
  success: true,
  data: result.data || [],  // 배열을 직접 반환
};
```

**API 서버 응답** (`QuoteCalculationService.php:168-178`):
```php
return [
    'success' => $failCount === 0,
    'summary' => [
        'total_count' => count($inputItems),
        'success_count' => $successCount,
        'fail_count' => $failCount,
        'grand_total' => round($grandTotal, 2),
    ],
    'items' => $results,  // items 배열 안에 결과가 있음
];
```

**컴포넌트 기대 구조** (`QuoteRegistrationV2.tsx:459-462`):
```typescript
const apiData = result.data as {
  summary?: { grand_total: number };
  items?: Array<{ index: number; result: BomCalculationResult }>;
};
const bomItems = apiData.items || [];  // ❌ result.data가 배열이면 items가 없음!
```

### 2.2 문제 발생 흐름

```
사용자 → "자동 견적 산출" 클릭
    ↓
calculateBomBulk(bomItems) 호출
    ↓
API 서버: { success, summary, items: [...] } 반환
    ↓
actions.ts: result.data = 전체 응답 객체 (또는 배열로 잘못 파싱)
    ↓
QuoteRegistrationV2.tsx: result.data.items 접근 시도
    ↓
❌ items가 undefined → bomItems = []
    ↓
locations에 bomResult 저장 안됨
    ↓
LocationDetailPanel: bomResult?.items 없음 → Mock 데이터 표시
QuoteSummaryPanel: bomResult?.subtotals 없음 → Mock 데이터 표시
    ↓
💥 모든 UI 영역에 데이터 없음
```

### 2.3 영향 받는 컴포넌트

| 컴포넌트 | 파일 | 영향 |
|----------|------|------|
| QuoteRegistrationV2 | `QuoteRegistrationV2.tsx:457-481` | bomResult 저장 안됨 |
| LocationDetailPanel | `LocationDetailPanel.tsx:152-184` | Mock 데이터로 fallback |
| QuoteSummaryPanel | `QuoteSummaryPanel.tsx:136-164` | Mock 데이터로 fallback |

---

## 3. 대상 범위

### 3.1 Phase 1: API 응답 처리 수정

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1.1 | `actions.ts` 응답 구조 확인 | ✅ | API 서버 응답과 비교 |
| 1.2 | `actions.ts` BomBulkResponse 타입 추가 | ✅ | 정확한 API 응답 구조 정의 |
| 1.3 | `QuoteRegistrationV2.tsx` handleCalculate 수정 | ✅ | 응답 매핑 로직 및 디버그 로그 추가 |

### 3.2 Phase 2: 데이터 바인딩 수정

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 2.1 | `FormulaEvaluatorService.php` items에 process_group 추가 | ✅ | addProcessGroupToItems 메서드 추가 |
| 2.2 | `LocationDetailPanel.tsx` bomItemsByTab 수정 | ✅ | process_group_key 기반 매핑 |
| 2.3 | `QuoteSummaryPanel.tsx` detailTotals 수정 | ✅ | grouped_items에서 items 가져오기 |
| 2.4 | `actions.ts` BomCalculationResult 타입 확장 | ✅ | process_group, grouped_items 필드 추가 |

---

## 4. 상세 작업 내용

### 4.1 Phase 1.2: handleCalculate 함수 수정

**현재 코드** (`QuoteRegistrationV2.tsx:457-479`):
```typescript
if (result.success && result.data) {
  // ❌ 잘못된 타입 캐스팅 - result.data 자체가 API 응답 객체임
  const apiData = result.data as {
    summary?: { grand_total: number };
    items?: Array<{ index: number; result: BomCalculationResult }>;
  };
  const bomItems = apiData.items || [];  // ❌ undefined
  // ...
}
```

**수정 방안**:
`actions.ts`의 응답 처리를 확인하여 두 가지 접근법 중 선택:

#### 방안 A: actions.ts 수정 (권장)
```typescript
// actions.ts에서 API 응답 구조 유지
return {
  success: true,
  data: {
    summary: result.data.summary,
    items: result.data.items,
  },
};
```

#### 방안 B: QuoteRegistrationV2.tsx 수정
```typescript
if (result.success && result.data) {
  // result.data가 { summary, items } 구조인지 확인
  const apiData = result.data as unknown as {
    summary?: { grand_total: number };
    items?: Array<{ index: number; result: BomCalculationResult }>;
  };
  // ...
}
```

---

## 5. 컨펌 대기 목록

| # | 항목 | 변경 내용 | 영향 범위 | 상태 |
|---|------|----------|----------|------|
| 1 | API 응답 구조 | actions.ts의 result.data 반환 방식 검토 | react/actions.ts | 대기 |

---

## 6. 변경 이력

| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|------|------|----------|------|------|
| 2026-01-26 | 분석 | 문서 초안 작성 및 근본 원인 분석 완료 | - | - |
| 2026-01-26 | 수정 | actions.ts BomBulkResponse 타입 추가 | react/src/components/quotes/actions.ts | ✅ |
| 2026-01-26 | 수정 | QuoteRegistrationV2.tsx handleCalculate 수정 | react/src/components/quotes/QuoteRegistrationV2.tsx | ✅ |
| 2026-01-26 | 수정 | FormulaEvaluatorService.php process_group 추가 | api/app/Services/Quote/FormulaEvaluatorService.php | ✅ |
| 2026-01-26 | 수정 | LocationDetailPanel.tsx bomItemsByTab 수정 | react/src/components/quotes/LocationDetailPanel.tsx | ✅ |
| 2026-01-26 | 수정 | QuoteSummaryPanel.tsx detailTotals 수정 | react/src/components/quotes/QuoteSummaryPanel.tsx | ✅ |
| 2026-01-26 | 수정 | ItemService.php has_bom 필드 추가 | api/app/Services/ItemService.php | ✅ |
| 2026-01-26 | 수정 | actions.ts FinishedGoods에 has_bom, bom 필드 추가 | react/src/components/quotes/actions.ts | ✅ |
| 2026-01-26 | 수정 | QuoteRegistrationV2.tsx DevFill BOM 필터링 | react/src/components/quotes/QuoteRegistrationV2.tsx | ✅ |
| 2026-01-26 | 검증 | 브라우저 테스트 완료 - 4가지 문제 모두 해결 확인 | - | ✅ |

---

## 7. 참고 문서

- **빠른 시작**: `docs/quickstart/quick-start.md`
- **품질 체크리스트**: `docs/standards/quality-checklist.md`
- **API 규칙**: `docs/standards/api-rules.md`

---

## 8. 검증 결과

> 브라우저 자동화 테스트 완료 (2026-01-26)

### 8.1 테스트 케이스

| 입력값 | 예상 결과 | 실제 결과 | 상태 |
|--------|----------|----------|------|
| DevFill 후 자동 견적 산출 | 제품 리스트 표시 | 볼트 M10×40, 너트 M10, 볼트 M8×30 등 6개 품목 표시 | ✅ |
| 개소 선택 | 개소별 합계 표시 | 1F / SS-01 상세소계: 3,119,555.94원 | ✅ |
| 그룹별 합계 | 상세별 합계 표시 | 절곡 공정: 735,891.24원, 철재 공정: 2,383,364.7원 | ✅ |
| 전체 금액 | 예상 견적금액 > 0 | 예상 견적금액: 3,119,555.94원 | ✅ |

### 8.2 테스트 환경

- **URL**: `http://dev.sam.kr/sales/quote-management/test-new`
- **테스트 방법**: Claude-in-Chrome 브라우저 자동화
- **데이터**: DevFill로 생성된 테스트 데이터

### 8.3 추가 발견 및 해결 사항

테스트 중 DevFill이 BOM 없는 제품을 선택하여 계산 결과가 0으로 나오는 문제 발견:

| 문제 | 원인 | 해결 |
|------|------|------|
| DevFill 후 bomItemsCount: 0 | BOM 없는 제품 선택 | DevFill에서 BOM 있는 제품만 필터링 |
| has_bom 필드 없음 | API 응답에 미포함 | ItemService.php에서 계산 필드 추가 |
| getFinishedGoods에서 필드 누락 | 매핑 시 has_bom, bom 미포함 | FinishedGoods 인터페이스 및 매핑 수정 |

### 8.4 최종 검증 결과

```
[DevFill] BOM 있는 제품: 15개 / 전체: 2017개
[BOM 계산 결과]
- bomItemsCount: 6
- bomGrandTotal: 3,119,555.94
- 공정별 그룹: 절곡, 철재
```

**모든 4가지 UI 문제 해결 확인 완료** ✅

---

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