# FG 제품코드 통합 계획

> **작성일**: 2026-02-19
> **목적**: FG 제품코드에서 설치유형/마감재질을 분리하여 위치별 설정으로 이동, 18개 FG 품목을 6개로 통합
> **기준 문서**: `docs/rules/item-policy.md`, `docs/features/quotes/README.md`
> **상태**: 🔄 진행중

---

## 📍 현재 진행 상태

| 항목 | 내용 |
|------|------|
| **마지막 완료 작업** | 영향도 분석 완료, 혼합형 validation 수정 커밋 완료 |
| **다음 작업** | Phase 1: DB 마이그레이션 |
| **진행률** | 0/8 (0%) |
| **마지막 업데이트** | 2026-02-19 |

---

## 1. 개요

### 1.1 배경

현재 경동기업(tenant_id=287) FG 품목 코드 체계:
```
FG-KWE01-벽면형-SUS    (모델: KWE01, 설치유형: 벽면형, 마감재질: SUS)
FG-KWE01-벽면형-EGI    (모델: KWE01, 설치유형: 벽면형, 마감재질: EGI)
FG-KWE01-측면형-SUS    (모델: KWE01, 설치유형: 측면형, 마감재질: SUS)
... (총 18개 = 6모델 × {벽면형,측면형} × {SUS,EGI} + 혼합형 추가 예정)
```

문제점:
- 설치유형/마감재질은 **위치(Location)별 설정**이지 제품 자체의 속성이 아님
- 같은 모델(KWE01)인데 FG 코드가 4개 이상으로 분산
- 혼합형 추가 시 FG 품목이 계속 늘어남 (6모델 × 3설치유형 × 2마감재질 = 36개)

### 1.2 목표 코드 체계
```
AS-IS: FG-KWE01-벽면형-SUS  →  TO-BE: KWE01
```
- "FG-" 접두사 제거: `item_type = 'FG'` 컬럼이 이미 완제품 구분 담당
- 설치유형(벽면형/측면형/혼합형) 제거: 위치별 `guideRailType` 파라미터로 전달
- 마감재질(SUS/EGI) 제거: 위치별 `finishingType` 파라미터로 전달

### 1.3 기준 원칙
```
┌─────────────────────────────────────────────────────────────────┐
│  🎯 핵심 원칙                                                    │
├─────────────────────────────────────────────────────────────────┤
│  1. 코어 계산 로직(KyungdongFormulaHandler) 변경 없음             │
│  2. BOM은 child_item_id FK 기반 → 코드 변경에 안전               │
│  3. product_model/finishing_type은 이미 별도 파라미터 전달 중      │
│  4. 기존 quote_items에 FG 코드 참조 데이터 없음 (마이그레이션 부담 ↓) │
└─────────────────────────────────────────────────────────────────┘
```

### 1.4 변경 승인 정책

| 분류 | 예시 | 승인 |
|------|------|------|
| ✅ 즉시 가능 | React UI에 마감재질 Select 추가, validation 규칙 수정 | 불필요 |
| ⚠️ 컨펌 필요 | items 테이블 데이터 통합, BOM parent_item_id 재매핑, 시더 수정 | **필수** |
| 🔴 금지 | items 테이블 스키마 변경, 기존 BOM 삭제, 견적 계산 코어 로직 변경 | 별도 협의 |

### 1.5 준수 규칙
- `docs/rules/item-policy.md` - 품목 정책
- `docs/standards/quality-checklist.md` - 품질 체크리스트
- `docs/features/quotes/README.md` - 견적 시스템

---

## 2. 대상 범위

### 2.1 Phase 1: DB 마이그레이션 (items 통합)

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1.1 | 18개 FG 품목 → 6개로 통합 마이그레이션 스크립트 | ⏳ | items.code 변경 |
| 1.2 | BOM parent_item_id 재매핑 | ⏳ | 통합된 item_id로 변경 |
| 1.3 | 통합 대상 외 12개 FG 품목 soft delete | ⏳ | 연결된 BOM 확인 후 |
| 1.4 | MapItemsToProcesses globalExcludes 수정 | ⏳ | 'FG-%' → item_type 기반 |

### 2.2 Phase 2: API 수정

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 2.1 | FormulaEvaluatorService: finishing_type 파라미터 수신 | ⏳ | 마감재질 매핑 추가 |
| 2.2 | QuoteBomCalculateRequest: finishingType validation 추가 | ⏳ | SUS/EGI |
| 2.3 | QuoteBomBulkCalculateRequest: finishingType validation 추가 | ⏳ | SUS/EGI |
| 2.4 | KyungdongItemSeeder 수정 (향후 시딩용) | ⏳ | FG-코드 생성 로직 |

### 2.3 Phase 3: React 프론트엔드

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 3.1 | LocationDetailPanel: 마감재질 Select UI 추가 | ⏳ | SUS/EGI 선택 |
| 3.2 | LocationListPanel: 마감재질 컬럼/폼필드 추가 | ⏳ | 위치 추가 시 |
| 3.3 | types.ts: QuoteLocation에 finishingType 추가 | ⏳ | |
| 3.4 | actions.ts: BOM 산출 요청에 finishingType 포함 | ⏳ | |
| 3.5 | QuoteRegistration.tsx: mock 데이터 업데이트 | ⏳ | |
| 3.6 | QuoteSummaryPanel/PreviewContent: 마감재질 표시 | ⏳ | |

### 2.4 Phase 4: 검증

| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 4.1 | 통합 전후 BOM 계산 결과 비교 테스트 | ⏳ | 동일 입력 → 동일 결과 |
| 4.2 | 견적 등록 → 산출 → 저장 E2E 테스트 | ⏳ | |

---

## 3. 작업 절차

### 3.1 단계별 절차

```
Step 1: DB 마이그레이션 스크립트 작성
├── 6개 모델별 대표 FG 품목 선정 (유지할 item_id 결정)
├── BOM parent_item_id를 대표 item_id로 재매핑
├── 대표 품목의 code를 통합 코드로 변경 (KWE01 등)
├── 대표 품목의 attributes에서 guiderail_type/finishing_type 제거
└── 나머지 12개 FG 품목 soft delete

Step 2: API 수정
├── FormRequest에 finishingType/FT validation 추가
├── FormulaEvaluatorService에 FT → finishing_type 매핑 추가
├── MapItemsToProcesses globalExcludes → item_type 기반 변경
└── KyungdongItemSeeder 코드 생성 로직 수정

Step 3: React 프론트엔드
├── types.ts에 finishingType 필드 추가
├── LocationDetailPanel에 마감재질 Select 추가
├── LocationListPanel에 마감재질 폼필드/컬럼 추가
├── actions.ts BOM 산출 요청에 finishingType 포함
└── Summary/Preview에 마감재질 표시

Step 4: 검증
├── 동일 입력(KWE01 + wall + SUS)으로 기존 결과와 비교
├── 모든 조합 테스트 (6모델 × 3설치 × 2마감)
└── 견적 등록 → 산출 → 저장 E2E
```

---

## 4. 상세 작업 내용 (코드 스니펫 포함)

### 4.1 현재 FG 품목 현황 (tenant_id=287)

| 모델 | 벽면형-SUS | 벽면형-EGI | 측면형-SUS | 측면형-EGI | 통합 코드 | item_category |
|------|-----------|-----------|-----------|-----------|----------|:------------:|
| KWE01 | FG-KWE01-벽면형-SUS | FG-KWE01-벽면형-EGI | FG-KWE01-측면형-SUS | FG-KWE01-측면형-EGI | **KWE01** | SCREEN |
| KWE02 | (동일 패턴) | | | | **KWE02** | SCREEN |
| KWE03 | | | | | **KWE03** | SCREEN |
| KWS01 | | | | | **KWS01** | STEEL |
| KWS02 | | | | | **KWS02** | STEEL |
| KWS03 | | | | | **KWS03** | STEEL |

> KWE = 스크린(SCREEN), KWS = 철재(STEEL). item_category는 유지됨 (계산 분기에 사용)

FG 코드 생성 원본 (`api/database/seeders/Kyungdong/KyungdongItemSeeder.php:305-307`):
```php
$finishingShort = self::FINISHING_MAP[$model->finishing_type] ?? 'STD';
$code = "FG-{$model->model_name}-{$model->guiderail_type}-{$finishingShort}";
$name = "{$model->model_name} {$model->major_category} {$model->finishing_type} {$model->guiderail_type}";
```

FINISHING_MAP (`KyungdongItemSeeder.php:39-42`):
```php
private const FINISHING_MAP = [
    'SUS마감' => 'SUS',
    'EGI마감' => 'EGI',
];
```

items.attributes 구조:
```json
{
  "model_name": "KWE01",
  "major_category": "스크린",
  "finishing_type": "SUS마감",
  "guiderail_type": "벽면형",
  "legacy_source": "models",
  "legacy_model_id": 123
}
```

### 4.2 BOM 재매핑 전략

BOM은 FG 품목(parent)의 `items.bom` JSON 컬럼에 저장:
```json
[
  { "child_item_id": 123, "quantity": 1 },
  { "child_item_id": 456, "quantity": 2 }
]
```

마이그레이션 SQL 전략:
```sql
-- Step 1: 모델별 대표 FG 품목 선정 (벽면형-SUS를 대표로)
-- 대표 선정 기준: 같은 model_name 중 가장 작은 id

-- Step 2: 대표 품목의 code 변경
UPDATE items SET code = 'KWE01'
WHERE id = (대표_item_id) AND tenant_id = 287;

-- Step 3: 대표 품목의 attributes에서 guiderail_type/finishing_type 제거
-- (이 속성들은 더 이상 품목 고유 속성이 아님)

-- Step 4: 비대표 품목의 BOM을 대표 품목으로 이관
-- (동일 모델의 BOM은 동일하므로, BOM이 있는 품목의 bom을 대표로 복사)

-- Step 5: 비대표 12개 품목 soft delete
UPDATE items SET deleted_at = NOW(), deleted_by = 1
WHERE tenant_id = 287 AND item_type = 'FG'
  AND id NOT IN (대표_item_ids);
```

핵심 안전 요소:
- BOM의 `child_item_id`는 PT/SM 품목 → FG 통합과 **무관**
- `FormulaEvaluatorService::getItemDetails()` (line 1110-1112)에서 `->where('code', $itemCode)` 조회
- 통합 후 code가 'KWE01'이 되면 `getItemDetails('KWE01')`로 정상 조회

### 4.3 API 파라미터 흐름 (통합 후)

```
Frontend (LocationDetailPanel)
  ├── productCode: "KWE01" (통합 코드)
  ├── guideRailType: "wall" | "floor" | "mixed"
  ├── finishingType: "SUS" | "EGI"          ← 새로 추가
  └── motorPower: "single" | "three"
      ↓
actions.ts::calculateBomBulk() - POST /api/v1/quotes/calculate/bom/bulk
  body: { items: [{ finished_goods_code, openWidth, openHeight, guideRailType, motorPower, finishingType, ... }] }
      ↓
QuoteBomBulkCalculateRequest::normalizeInputVariables() (line 122-135)
  ├── 'W0' => openWidth, 'H0' => openHeight
  ├── 'GT' => guideRailType, 'MP' => motorPower
  └── 'FT' => finishingType                 ← 새로 추가
      ↓
FormulaEvaluatorService::calculateKyungdongBom() (line 1574~)
  ├── getItemDetails("KWE01", tenantId) → items.code = "KWE01" 조회 (line 1110-1112)
  ├── $finishingType: FT → SUS/EGI          ← 기존 line 1677 수정
  ├── $installationType: GT → 벽면형/측면형/혼합형 (line 1680-1684)
  └── $motorVoltage: MP → 220V/380V (line 1687-1690)
      ↓
$calculatedVariables = array_merge() (line 1692-1708)
  'finishing_type' => $finishingType  (line 1705)  ← 이미 포함됨
      ↓
KyungdongFormulaHandler (변경 없음)
  ├── calculateSteelItems() line 458: $rawFinish = $params['finishing_type'] ?? 'SUS'
  ├── calculateGuideRails() line 540: $finishingType 파라미터
  └── getBottomBarPrice() line 561: $finishingType 파라미터
```

### 4.4 핵심 파일별 변경 상세

---

#### 4.4.1 `api/app/Services/Quote/FormulaEvaluatorService.php`

**현재 코드 (line 1676-1677):**
```php
$productModel = $inputVariables['product_model'] ?? 'KSS01';
$finishingType = $inputVariables['finishing_type'] ?? 'SUS';
```

**수정 후:**
```php
$productModel = $inputVariables['product_model'] ?? 'KSS01';

// 마감재질: 프론트 FT(SUS/EGI) → finishing_type 매핑
$finishingType = $inputVariables['finishing_type'] ?? match ($inputVariables['FT'] ?? 'SUS') {
    'EGI' => 'EGI',
    default => 'SUS',
};
```

> `$calculatedVariables` array_merge (line 1705)에는 이미 `'finishing_type' => $finishingType` 포함됨

---

#### 4.4.2 `api/app/Http/Requests/Quote/QuoteBomCalculateRequest.php`

**현재 rules() (line 20-39)에 추가:**
```php
// 기존
'GT' => 'nullable|string|in:wall,ceiling,floor,mixed',
'MP' => 'nullable|string|in:single,three',
// 추가
'FT' => 'nullable|string|in:SUS,EGI',
```

**현재 getInputVariables() (line 74-89)에 추가:**
```php
// 기존
'MP' => $validated['MP'] ?? 'single',
// 추가
'FT' => $validated['FT'] ?? 'SUS',
```

---

#### 4.4.3 `api/app/Http/Requests/Quote/QuoteBomBulkCalculateRequest.php`

**rules() (line 21-54)에 추가:**
```php
// React 필드명 (camelCase)
'items.*.finishingType' => 'nullable|string|in:SUS,EGI',
// API 변수명 (약어)
'items.*.FT' => 'nullable|string|in:SUS,EGI',
```

**normalizeInputVariables() (line 122-135)에 추가:**
```php
// 기존
'MP' => $item['motorPower'] ?? $item['MP'] ?? 'single',
// 추가
'FT' => $item['finishingType'] ?? $item['FT'] ?? 'SUS',
```

---

#### 4.4.4 `api/app/Console/Commands/MapItemsToProcesses.php`

**현재 (line 48):**
```php
private array $globalExcludes = ['FG-%', 'RM-%', 'EST-INSPECTION'];
```

**수정 후:**
```php
private array $globalExcludes = ['RM-%', 'EST-INSPECTION'];
// FG 제외는 item_type 기반으로 처리 (아래 쿼리에서 ->where('item_type', '!=', 'FG') 추가)
```

> 해당 명령어에서 items 조회 시 `->whereNotIn('item_type', ['FG'])` 조건 추가

---

#### 4.4.5 `api/database/seeders/Kyungdong/KyungdongItemSeeder.php`

**현재 (line 305-307):**
```php
$finishingShort = self::FINISHING_MAP[$model->finishing_type] ?? 'STD';
$code = "FG-{$model->model_name}-{$model->guiderail_type}-{$finishingShort}";
$name = "{$model->model_name} {$model->major_category} {$model->finishing_type} {$model->guiderail_type}";
```

**수정 후:**
```php
$code = $model->model_name;  // KWE01, KWS01 등
$name = "{$model->model_name} {$model->major_category}";
```

> 중복 방지: 같은 model_name은 하나만 생성 (기존: 설치유형×마감재질 조합별 생성 → 모델별 1개)

---

#### 4.4.6 `react/src/components/quotes/types.ts`

**LocationItem 인터페이스 (line 664-686)에 추가:**
```typescript
export interface LocationItem {
  // ... 기존 필드
  guideRailType: string;   // 가이드레일 설치 유형
  finishingType: string;   // 마감재질 (SUS/EGI)  ← 추가
  motorPower: string;      // 모터 전원
  // ...
}
```

---

#### 4.4.7 `react/src/components/quotes/actions.ts`

**BomCalculateItem 인터페이스 (line 343-354)에 추가:**
```typescript
export interface BomCalculateItem {
  finished_goods_code: string;
  openWidth: number;
  openHeight: number;
  quantity?: number;
  guideRailType?: string;
  finishingType?: string;   // ← 추가
  motorPower?: string;
  controller?: string;
  wingSize?: number;
  inspectionFee?: number;
}
```

---

#### 4.4.8 `react/src/components/quotes/LocationDetailPanel.tsx`

**상수 추가 (line 75 뒤):**
```typescript
// 마감재질
const FINISHING_TYPES = [
  { value: "SUS", label: "SUS (스테인리스)" },
  { value: "EGI", label: "EGI (아연도금)" },
];
```

**2행 그리드 변경 (line 358-423):**
현재 `grid-cols-3` (가이드레일, 전원, 제어기) → `grid-cols-4`로 변경하고 마감재질 Select 추가:
```tsx
{/* 2행: 가이드레일, 마감재질, 전원, 제어기 */}
<div className="grid grid-cols-4 gap-3">
  {/* 가이드레일 (기존) */}
  <div>...</div>
  {/* 마감재질 (새로 추가) */}
  <div>
    <label className="text-xs text-gray-600 flex items-center gap-1">
      🔩 마감재질
    </label>
    <Select
      value={location.finishingType}
      onValueChange={(value) => handleFieldChange("finishingType", value)}
      disabled={disabled}
    >
      <SelectTrigger className="h-8 text-sm">
        <SelectValue />
      </SelectTrigger>
      <SelectContent>
        {FINISHING_TYPES.map((type) => (
          <SelectItem key={type.value} value={type.value}>
            {type.label}
          </SelectItem>
        ))}
      </SelectContent>
    </Select>
  </div>
  {/* 전원 (기존) */}
  <div>...</div>
  {/* 제어기 (기존) */}
  <div>...</div>
</div>
```

---

#### 4.4.9 `react/src/components/quotes/LocationListPanel.tsx`

**formData 초기값 (line 110-120)에 추가:**
```typescript
const [formData, setFormData] = useState({
  // ... 기존
  guideRailType: "wall",
  finishingType: "SUS",    // ← 추가
  motorPower: "single",
  // ...
});
```

**2행 폼 (line ~380 이후)에 마감재질 Select 추가** (가이드레일 Select 패턴과 동일)

---

#### 4.4.10 `react/src/components/quotes/QuoteRegistration.tsx`

**BOM 계산 페이로드 (line 459-469)에 finishingType 추가:**
```typescript
const bomItem = {
  finished_goods_code: newLocation.productCode,
  openWidth: newLocation.openWidth,
  openHeight: newLocation.openHeight,
  quantity: newLocation.quantity,
  guideRailType: newLocation.guideRailType,
  finishingType: newLocation.finishingType,    // ← 추가
  motorPower: newLocation.motorPower,
  controller: newLocation.controller,
  wingSize: newLocation.wingSize,
  inspectionFee: newLocation.inspectionFee,
};
```

**다건 산출 (line 594-606)도 동일하게 finishingType 추가:**
```typescript
const bomItems = formData.locations.map((loc) => ({
  finished_goods_code: loc.productCode,
  // ...
  finishingType: loc.finishingType,    // ← 추가
  // ...
}));
```

**기본값 (line 117):**
```typescript
// 기존
guideRailType: "wall",
// 추가
finishingType: "SUS",
```

**mock 데이터 (line 248):**
```typescript
// 기존: productCode: randomProduct?.item_code || "FG-SCR-001"
// 수정: productCode: randomProduct?.item_code || "KWE01"
```

---

#### 4.4.11 `react/src/components/quotes/QuoteSummaryPanel.tsx` & `QuotePreviewContent.tsx`

위치 정보 표시 영역에 마감재질 추가:
```typescript
// QuoteSummaryPanel.tsx line 172 근처
{loc.productCode} ({loc.finishingType}) × {loc.quantity}

// QuotePreviewContent.tsx line 209 근처
<td>{loc.productCode}</td>
<td>{loc.finishingType}</td>  // 또는 기존 컬럼에 병합
```

---

#### 4.4.12 `react/src/app/[locale]/(protected)/sales/quote-management/[id]/page.tsx`

**기존 견적 조회 시 BOM 재계산 페이로드 (line 60-70):**
```typescript
const bomItems: BomCalculateItem[] = locationsNeedingRecalc.map(loc => ({
  finished_goods_code: loc.productCode,
  openWidth: loc.openWidth,
  openHeight: loc.openHeight,
  quantity: loc.quantity,
  guideRailType: loc.guideRailType,
  // finishingType: loc.finishingType,  ← 추가 필요
  motorPower: loc.motorPower,
  controller: loc.controller,
  wingSize: loc.wingSize,
  inspectionFee: loc.inspectionFee,
}));
```

### 4.5 DB 마이그레이션 사전 검증 쿼리

마이그레이션 실행 전 반드시 확인할 쿼리:

```sql
-- 1. 현재 FG 품목 전체 목록 확인
SELECT id, code, name, item_category,
       JSON_EXTRACT(attributes, '$.model_name') as model_name,
       JSON_EXTRACT(attributes, '$.guiderail_type') as guiderail_type,
       JSON_EXTRACT(attributes, '$.finishing_type') as finishing_type,
       bom IS NOT NULL AND bom != '[]' as has_bom
FROM items
WHERE tenant_id = 287 AND item_type = 'FG' AND deleted_at IS NULL
ORDER BY code;

-- 2. 모델별 BOM 동일성 검증 (같은 model_name의 bom이 동일한지)
SELECT JSON_EXTRACT(attributes, '$.model_name') as model_name,
       COUNT(DISTINCT bom) as distinct_bom_count,
       COUNT(*) as total_count
FROM items
WHERE tenant_id = 287 AND item_type = 'FG' AND deleted_at IS NULL
GROUP BY JSON_EXTRACT(attributes, '$.model_name');
-- distinct_bom_count = 1 이면 안전 (동일 모델의 BOM이 같음)

-- 3. 다른 테이블에서 FG item_id 참조 확인
SELECT 'quote_items' as tbl, COUNT(*) as cnt
FROM quote_items WHERE item_id IN (
  SELECT id FROM items WHERE tenant_id = 287 AND item_type = 'FG'
)
UNION ALL
SELECT 'work_order_items', COUNT(*)
FROM work_order_items WHERE item_id IN (
  SELECT id FROM items WHERE tenant_id = 287 AND item_type = 'FG'
);
-- 모두 0이면 안전하게 통합 가능
```

---

### 4.6 핵심 API 메서드 참조 (읽기 전용)

아래 메서드들은 **변경하지 않지만** 동작을 이해하기 위해 참조:

**`FormulaEvaluatorService::getItemDetails()` (line 1102-1134):**
```php
public function getItemDetails(string $itemCode, ?int $tenantId = null): ?array
{
    $item = DB::table('items')
        ->where('tenant_id', $tenantId)
        ->where('code', $itemCode)     // ← 여기서 code로 조회
        ->whereNull('deleted_at')
        ->first();
    // ... id, code, name, item_type, item_category, bom 등 반환
}
```
→ 통합 후 `getItemDetails('KWE01')` 호출 시 code='KWE01' 품목 정상 조회

**`FormulaEvaluatorService::calculateKyungdongBom()` 핵심 흐름 (line 1574~):**
```
1. getItemDetails($finishedGoodsCode) → 완제품 조회
2. $productCategory = $finishedGoods['item_category'] → 'SCREEN' 또는 'STEEL'
3. $productModel, $finishingType, $installationType, $motorVoltage 결정
4. $calculatedVariables = array_merge($inputVariables, [...])
5. KyungdongFormulaHandler::calculateDynamicItems($calculatedVariables) 호출
```
→ `item_category`는 items 레코드에서 가져오므로 통합 후에도 정상 (KWE01 → SCREEN)

**`KyungdongFormulaHandler` finishing_type 사용처:**
- `calculateSteelItems()` line 458: `$rawFinish = $params['finishing_type'] ?? 'SUS'`
- `calculateGuideRails()` line 540: 파라미터로 수신
- `getBottomBarPrice()` line 561: 가격 조회에 사용
- `getGuideRailPrice()` line 696: 가격 조회에 사용
→ 모두 `$calculatedVariables['finishing_type']`에서 값을 가져오므로 매핑만 추가하면 됨

**React `getFinishedGoods()` (actions.ts line 302-317):**
```typescript
const result = await executeServerAction<FGApiResponse>({
  url: buildApiUrl('/api/v1/items', {
    item_type: 'FG',
    has_bom: '1',
    size: '5000',
  }),
});
```
→ `item_type='FG'`로 조회하므로 code 변경 영향 없음. 통합 후 6개만 반환됨.

---

## 5. 컨펌 대기 목록

| # | 항목 | 변경 내용 | 영향 범위 | 상태 |
|---|------|----------|----------|------|
| 1 | FG 품목 통합 마이그레이션 | 18개 → 6개, BOM 재매핑 | DB, 모든 FG 참조 | ⏳ 대기 |
| 2 | 12개 FG 품목 soft delete | 통합 후 불필요 품목 삭제 | DB | ⏳ 대기 |

---

## 6. 변경 이력

| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|------|------|----------|------|------|
| 2026-02-19 | - | 문서 초안 작성 | - | - |
| 2026-02-19 | 혼합형 지원 | GT validation에 mixed 추가 | QuoteBomCalculateRequest, QuoteBomBulkCalculateRequest | ✅ |
| 2026-02-19 | 모터 전압 | MP → motor_voltage 매핑 추가 | FormulaEvaluatorService | ✅ |
| 2026-02-19 | 가이드레일 | GT → installation_type 매핑 추가 | FormulaEvaluatorService | ✅ |
| 2026-02-19 | 혼합형 UI | GUIDE_RAIL_TYPES에 mixed 옵션 추가 | LocationDetailPanel | ✅ |

---

## 7. 참고 문서

- **품목 정책**: `docs/rules/item-policy.md`
- **견적 시스템**: `docs/features/quotes/README.md`
- **DB 스키마**: `docs/specs/database-schema.md`
- **품질 체크리스트**: `docs/standards/quality-checklist.md`
- **빠른 시작**: `docs/quickstart/quick-start.md`
- **견적 계산 계획**: `docs/dev_plans/kd-quote-logic-plan.md`
- **경동 품목 시더**: `api/database/seeders/Kyungdong/KyungdongItemSeeder.php`

---

## 8. 세션 및 메모리 관리 정책

### 8.1 세션 시작 시
```
read_memory("fg-consolidation-state")
read_memory("fg-consolidation-snapshot")
계획 문서 읽기 → docs/dev_plans/fg-code-consolidation-plan.md
```

### 8.2 작업 중 관리
| 컨텍스트 잔량 | Action | 내용 |
|--------------|--------|------|
| **30% 이하** | Snapshot | `write_memory("fg-consolidation-snapshot", ...)` |
| **20% 이하** | Symbol Tracking | `write_memory("fg-consolidation-active-symbols", ...)` |
| **10% 이하** | Stop & Save | 최종 상태 저장 후 세션 교체 권고 |

### 8.3 Serena 메모리 구조
- `fg-consolidation-state`: { phase, progress, next_step, last_decision }
- `fg-consolidation-snapshot`: 코드 변경점 + 논의 요약
- `fg-consolidation-rules`: 불변 규칙 (코어 로직 변경 없음, BOM FK 안전 등)
- `fg-consolidation-active-symbols`: 수정 중인 파일/심볼 리스트

---

## 9. 검증 결과

### 9.1 테스트 케이스

| 입력값 | 예상 결과 | 실제 결과 | 상태 |
|--------|----------|----------|------|
| KWE01 + wall + SUS + W0=2000 + H0=3000 | FG-KWE01-벽면형-SUS 동일 결과 | - | ⏳ |
| KWE01 + floor + EGI + W0=2000 + H0=3000 | FG-KWE01-측면형-EGI 동일 결과 | - | ⏳ |
| KWE01 + mixed + SUS + W0=2000 + H0=3000 | 혼합형 계산 정상 | - | ⏳ |
| KWS01 + wall + SUS + W0=2000 + H0=3000 | FG-KWS01-벽면형-SUS 동일 결과 | - | ⏳ |
| KWE01 + three + SUS + W0=5000 + H0=5000 | 삼상 모터 + SUS 정상 | - | ⏳ |

### 9.2 성공 기준

| 기준 | 달성 | 비고 |
|------|------|------|
| FG 품목 18개 → 6개 통합 | ⏳ | |
| BOM 계산 결과 통합 전후 동일 | ⏳ | 모든 조합 |
| 견적 등록 → 산출 → 저장 정상 | ⏳ | |
| 마감재질 선택 UI 동작 | ⏳ | |
| 기존 기능 회귀 없음 | ⏳ | |

---

## 10. 자기완결성 점검 결과

### 10.1 체크리스트 검증

| # | 검증 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1 | 작업 목적이 명확한가? | ✅ | 1.1 배경, 1.2 목표 |
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.2 성공 기준 |
| 3 | 작업 범위가 구체적인가? | ✅ | 2. 대상 범위 13개 항목 |
| 4 | 의존성이 명시되어 있는가? | ✅ | Phase 순서 = 의존성 |
| 5 | 참고 파일 경로가 정확한가? | ✅ | 4.4 핵심 파일 변경 목록 |
| 6 | 단계별 절차가 실행 가능한가? | ✅ | 3.1 + 4.x 상세 |
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 9.1 테스트 케이스 |
| 8 | 모호한 표현이 없는가? | ✅ | 구체적 코드/파일명 명시 |

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

| 질문 | 답변 가능 | 참조 섹션 |
|------|:--------:|----------|
| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1, 1.2 |
| Q2. 어디서부터 시작해야 하는가? | ✅ | 3.1 Step 1 |
| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 4.4 핵심 파일 변경 목록 |
| Q4. 작업 완료 확인 방법은? | ✅ | 9.1, 9.2 |
| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 |

**결과**: 5/5 통과 → ✅ 자기완결성 확보

---

## 11. 리스크 및 롤백

### 11.1 리스크 평가

| 리스크 | 확률 | 영향 | 대응 |
|--------|:----:|:----:|------|
| BOM parent_item_id 누락 | 중 | 높 | 마이그레이션 전 BOM 전수 검증 쿼리 실행 |
| 견적 계산 결과 불일치 | 낮 | 높 | 통합 전후 동일 입력 비교 테스트 5건 이상 |
| 기존 데이터 호환성 깨짐 | 낮 | 낮 | 현재 quote_items에 FG 코드 참조 데이터 없음 |
| 프론트 productCode 참조 오류 | 중 | 중 | 46개 참조 지점 전수 확인 |

### 11.2 롤백 전략

- DB 마이그레이션은 Laravel down() 메서드로 롤백 가능하도록 작성
- 마이그레이션 실행 전 items + BOM 데이터 백업 쿼리 준비
- API/React 변경은 git revert로 원복 가능

---

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