Files
sam-docs/dev/dev_plans/fg-code-consolidation-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

754 lines
26 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.

# 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 스킬로 생성되었습니다.*