- 개발팀 전용 폴더 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>
754 lines
26 KiB
Markdown
754 lines
26 KiB
Markdown
# 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 스킬로 생성되었습니다.* |