- 완료된 계획 문서 12개 → plans/archive/ 이동 - 완료된 하위 계획 2개 → plans/sub/archive/ 이동 - 새 계획 문서 추가: - 5130-bom-migration-plan.md (완료) - 5130-sam-data-migration-plan.md (완료) - bidding-api-implementation-plan.md (완료) - dashboard-api-integration-plan.md - order-workorder-shipment-integration-plan.md - dev-toolbar-plan.md - AI 리포트 키워드 색상체계 가이드 v1.4 추가 - index_plans.md 업데이트
26 KiB
26 KiB
견적 자동산출 개발 계획
작성일: 2025-12-22 상태: ✅ 구현 완료 목표: MNG 견적수식 데이터 셋팅 + React 견적관리 자동산출 기능 구현 완료일: 2025-12-22 실제 소요 시간: 약 2시간
0. 빠른 시작 가이드
폴더 구조 이해 (중요!)
| 폴더 | 포트 | 역할 | 비고 |
|---|---|---|---|
design/ |
localhost:3002 | 디자인 프로토타입 | UI 참고용 |
react/ |
localhost:3000 | 실제 프론트엔드 | 구현 대상 ✅ |
mng/ |
mng.sam.kr | 관리자 패널 | 수식 데이터 관리 |
api/ |
api.sam.kr | REST API | 견적 산출 엔진 |
이 문서만으로 작업을 시작하려면:
# 1. Docker 서비스 시작
cd /Users/hskwon/Works/@KD_SAM/SAM
docker-compose up -d
# 2. MNG 시더 실행 (Phase 1 완료 후)
cd mng
php artisan quote:seed-formulas --tenant=1
# 3. React 개발 서버 (실제 구현 대상)
cd react
npm run dev
# http://localhost:3000 접속
핵심 파일 위치
| 구분 | 파일 경로 | 역할 |
|---|---|---|
| MNG 시더 | mng/app/Console/Commands/SeedQuoteFormulasCommand.php |
🆕 생성 필요 |
| React 자동산출 | react/src/components/quotes/QuoteRegistration.tsx |
⚡ 수정 필요 (line 332) |
| API 클라이언트 | react/src/lib/api/client.ts |
참조 |
| API 엔드포인트 | api/app/Http/Controllers/Api/V1/QuoteController.php |
✅ 구현됨 |
| 수식 엔진 | api/app/Services/Quote/QuoteCalculationService.php |
✅ 구현됨 |
1. 현황 분석
1.1 시스템 구조
┌───────────────────────────────────────────────────────────────────────────────┐
│ SAM 시스템 │
├───────────────────────────────────────────────────────────────────────────────┤
│ MNG (mng.sam.kr) │ React (react/ 폴더) │ Design │
│ ├── 기준정보관리 │ ├── 판매관리 │ (참고용) │
│ │ └── 견적수식관리 ✅ │ │ └── 견적관리 │ │
│ │ - 카테고리 CRUD │ │ └── 자동견적산출 │ design/ │
│ │ - 수식 CRUD │ │ UI 있음 ✅ │ :3002 │
│ │ - 범위/매핑/품목 탭 │ │ API 연동 ❌ │ │
│ │ │ │ │ │
│ └── DB: quote_formulas 테이블 │ └── API 호출: │ │
│ (데이터 없음! ❌) │ POST /v1/quotes/calculate │ │
└───────────────────────────────────────────────────────────────────────────────┘
※ design/ 폴더 (localhost:3002)는 UI 프로토타입용이며, 실제 구현은 react/ 폴더에서 진행
1.2 React 견적등록 컴포넌트 현황
파일: react/src/components/quotes/QuoteRegistration.tsx
// 현재 상태 (line 332-335)
const handleAutoCalculate = () => {
toast.info(`자동 견적 산출 (${formData.items.length}개 항목) - API 연동 필요`);
};
// 입력 필드 (이미 구현됨):
interface QuoteItem {
openWidth: string; // W0 (오픈사이즈 가로)
openHeight: string; // H0 (오픈사이즈 세로)
productCategory: string; // screen | steel
quantity: number;
// ... 기타 필드
}
1.3 API 엔드포인트 현황
파일: api/app/Http/Controllers/Api/V1/QuoteController.php
// 이미 구현됨 (line 135-145)
public function calculate(QuoteCalculateRequest $request)
{
return ApiResponse::handle(function () use ($request) {
$validated = $request->validated();
return $this->calculationService->calculate(
$validated['inputs'] ?? $validated,
$validated['product_category'] ?? null
);
}, __('message.quote.calculated'));
}
1.4 수식 시더 데이터 (API)
파일: api/database/seeders/QuoteFormulaSeeder.php
| 카테고리 | 수식 수 | 설명 |
|---|---|---|
| OPEN_SIZE | 2 | W0, H0 입력값 |
| MAKE_SIZE | 4 | 제작사이즈 계산 |
| AREA | 1 | 면적 = W1 * H1 / 1000000 |
| WEIGHT | 2 | 중량 계산 (스크린/철재) |
| GUIDE_RAIL | 5 | 가이드레일 자동 선택 |
| CASE | 3 | 케이스 자동 선택 |
| MOTOR | 1 | 모터 자동 선택 (범위 9개) |
| CONTROLLER | 2 | 제어기 매핑 |
| EDGE_WING | 1 | 마구리 수량 |
| INSPECTION | 1 | 검사비 |
| PRICE_FORMULA | 8 | 단가 수식 |
| 합계 | 30개 | + 범위 18개 |
2. 개발 상세 계획
Phase 1: MNG 시더 데이터 생성 (1일)
2.1 Artisan 명령어 생성
생성할 파일: mng/app/Console/Commands/SeedQuoteFormulasCommand.php
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class SeedQuoteFormulasCommand extends Command
{
protected $signature = 'quote:seed-formulas
{--tenant=1 : 테넌트 ID}
{--only= : categories|formulas|ranges|mappings|items}
{--fresh : 기존 데이터 삭제 후 재생성}';
protected $description = '견적수식 시드 데이터를 생성합니다';
public function handle(): int
{
$tenantId = (int) $this->option('tenant');
$only = $this->option('only');
$fresh = $this->option('fresh');
if ($fresh) {
$this->warn('기존 데이터를 삭제합니다...');
$this->truncateTables($tenantId);
}
if (!$only || $only === 'categories') {
$this->seedCategories($tenantId);
}
if (!$only || $only === 'formulas') {
$this->seedFormulas($tenantId);
}
if (!$only || $only === 'ranges') {
$this->seedRanges($tenantId);
}
$this->info('✅ 견적수식 시드 완료!');
return Command::SUCCESS;
}
private function seedCategories(int $tenantId): void
{
$categories = [
['code' => 'OPEN_SIZE', 'name' => '오픈사이즈', 'sort_order' => 1],
['code' => 'MAKE_SIZE', 'name' => '제작사이즈', 'sort_order' => 2],
['code' => 'AREA', 'name' => '면적', 'sort_order' => 3],
['code' => 'WEIGHT', 'name' => '중량', 'sort_order' => 4],
['code' => 'GUIDE_RAIL', 'name' => '가이드레일', 'sort_order' => 5],
['code' => 'CASE', 'name' => '케이스', 'sort_order' => 6],
['code' => 'MOTOR', 'name' => '모터', 'sort_order' => 7],
['code' => 'CONTROLLER', 'name' => '제어기', 'sort_order' => 8],
['code' => 'EDGE_WING', 'name' => '마구리', 'sort_order' => 9],
['code' => 'INSPECTION', 'name' => '검사', 'sort_order' => 10],
['code' => 'PRICE_FORMULA', 'name' => '단가수식', 'sort_order' => 11],
];
foreach ($categories as $cat) {
DB::table('quote_formula_categories')->updateOrInsert(
['tenant_id' => $tenantId, 'code' => $cat['code']],
array_merge($cat, [
'tenant_id' => $tenantId,
'is_active' => true,
'created_at' => now(),
'updated_at' => now(),
])
);
}
$this->info("카테고리 " . count($categories) . "개 생성됨");
}
private function seedFormulas(int $tenantId): void
{
// API 시더와 동일한 데이터 (api/database/seeders/QuoteFormulaSeeder.php 참조)
$formulas = $this->getFormulaData();
$categoryMap = DB::table('quote_formula_categories')
->where('tenant_id', $tenantId)
->pluck('id', 'code')
->toArray();
$count = 0;
foreach ($formulas as $formula) {
$categoryId = $categoryMap[$formula['category_code']] ?? null;
if (!$categoryId) continue;
DB::table('quote_formulas')->updateOrInsert(
['tenant_id' => $tenantId, 'variable' => $formula['variable']],
[
'tenant_id' => $tenantId,
'category_id' => $categoryId,
'variable' => $formula['variable'],
'name' => $formula['name'],
'type' => $formula['type'],
'formula' => $formula['formula'] ?? null,
'output_type' => 'variable',
'description' => $formula['description'] ?? null,
'sort_order' => $formula['sort_order'] ?? 0,
'is_active' => $formula['is_active'] ?? true,
'created_at' => now(),
'updated_at' => now(),
]
);
$count++;
}
$this->info("수식 {$count}개 생성됨");
}
private function getFormulaData(): array
{
return [
// 오픈사이즈
['category_code' => 'OPEN_SIZE', 'variable' => 'W0', 'name' => '오픈사이즈 W0 (가로)', 'type' => 'input', 'formula' => null, 'sort_order' => 1],
['category_code' => 'OPEN_SIZE', 'variable' => 'H0', 'name' => '오픈사이즈 H0 (세로)', 'type' => 'input', 'formula' => null, 'sort_order' => 2],
// 제작사이즈
['category_code' => 'MAKE_SIZE', 'variable' => 'W1_SCREEN', 'name' => '제작사이즈 W1 (스크린)', 'type' => 'calculation', 'formula' => 'W0 + 140', 'sort_order' => 1],
['category_code' => 'MAKE_SIZE', 'variable' => 'H1_SCREEN', 'name' => '제작사이즈 H1 (스크린)', 'type' => 'calculation', 'formula' => 'H0 + 350', 'sort_order' => 2],
['category_code' => 'MAKE_SIZE', 'variable' => 'W1_STEEL', 'name' => '제작사이즈 W1 (철재)', 'type' => 'calculation', 'formula' => 'W0 + 110', 'sort_order' => 3],
['category_code' => 'MAKE_SIZE', 'variable' => 'H1_STEEL', 'name' => '제작사이즈 H1 (철재)', 'type' => 'calculation', 'formula' => 'H0 + 350', 'sort_order' => 4],
// 면적
['category_code' => 'AREA', 'variable' => 'M', 'name' => '면적 계산', 'type' => 'calculation', 'formula' => 'W1 * H1 / 1000000', 'sort_order' => 1],
// 중량
['category_code' => 'WEIGHT', 'variable' => 'K_SCREEN', 'name' => '중량 계산 (스크린)', 'type' => 'calculation', 'formula' => 'M * 2 + W0 / 1000 * 14.17', 'sort_order' => 1],
['category_code' => 'WEIGHT', 'variable' => 'K_STEEL', 'name' => '중량 계산 (철재)', 'type' => 'calculation', 'formula' => 'M * 25', 'sort_order' => 2],
// 가이드레일
['category_code' => 'GUIDE_RAIL', 'variable' => 'G', 'name' => '가이드레일 제작길이', 'type' => 'calculation', 'formula' => 'H0 + 250', 'sort_order' => 1],
['category_code' => 'GUIDE_RAIL', 'variable' => 'GR_AUTO_SELECT', 'name' => '가이드레일 자재 자동 선택', 'type' => 'range', 'formula' => null, 'sort_order' => 2],
// 케이스
['category_code' => 'CASE', 'variable' => 'S_SCREEN', 'name' => '케이스 사이즈 (스크린)', 'type' => 'calculation', 'formula' => 'W0 + 220', 'sort_order' => 1],
['category_code' => 'CASE', 'variable' => 'S_STEEL', 'name' => '케이스 사이즈 (철재)', 'type' => 'calculation', 'formula' => 'W0 + 240', 'sort_order' => 2],
['category_code' => 'CASE', 'variable' => 'CASE_AUTO_SELECT', 'name' => '케이스 자재 자동 선택', 'type' => 'range', 'formula' => null, 'sort_order' => 3],
// 모터
['category_code' => 'MOTOR', 'variable' => 'MOTOR_AUTO_SELECT', 'name' => '모터 자동 선택', 'type' => 'range', 'formula' => null, 'sort_order' => 1],
// 제어기
['category_code' => 'CONTROLLER', 'variable' => 'CONTROLLER_TYPE', 'name' => '제어기 유형', 'type' => 'input', 'formula' => null, 'sort_order' => 0],
['category_code' => 'CONTROLLER', 'variable' => 'CTRL_AUTO_SELECT', 'name' => '제어기 자동 선택', 'type' => 'mapping', 'formula' => null, 'sort_order' => 1],
// 검사
['category_code' => 'INSPECTION', 'variable' => 'INSP_FEE', 'name' => '검사비', 'type' => 'calculation', 'formula' => '1', 'sort_order' => 1],
];
}
// ... 나머지 메서드 (seedRanges, truncateTables 등)
}
2.2 작업 순서
# 1. 명령어 파일 생성
# mng/app/Console/Commands/SeedQuoteFormulasCommand.php
# 2. 실행
cd mng
php artisan quote:seed-formulas --tenant=1
# 3. 확인
php artisan tinker
>>> \App\Models\Quote\QuoteFormula::count()
# 예상: 30
# 4. 시뮬레이터 테스트
# mng.sam.kr/quote-formulas/simulator
# 입력: W0=3000, H0=2500
Phase 2: React 자동산출 기능 구현 (2-3일)
2.1 API 클라이언트 추가
수정할 파일: react/src/lib/api/quote.ts (신규)
// react/src/lib/api/quote.ts
import { ApiClient } from './client';
import { AUTH_CONFIG } from './auth/auth-config';
// API 응답 타입
interface CalculationResult {
inputs: Record<string, number | string>;
outputs: Record<string, {
name: string;
value: number;
category: string;
type: string;
}>;
items: Array<{
item_code: string;
item_name: string;
specification?: string;
unit?: string;
quantity: number;
unit_price: number;
total_price: number;
formula_variable: string;
}>;
costs: {
material_cost: number;
labor_cost: number;
install_cost: number;
subtotal: number;
};
errors: string[];
}
interface CalculateRequest {
inputs: {
W0: number;
H0: number;
QTY?: number;
INSTALL_TYPE?: string;
CONTROL_TYPE?: string;
};
product_category: 'screen' | 'steel';
}
// Quote API 클라이언트
class QuoteApiClient extends ApiClient {
constructor() {
super({
mode: 'bearer',
apiKey: AUTH_CONFIG.apiKey,
getToken: () => {
if (typeof window !== 'undefined') {
return localStorage.getItem('auth_token');
}
return null;
},
});
}
/**
* 자동 견적 산출
*/
async calculate(request: CalculateRequest): Promise<{ success: boolean; data: CalculationResult; message: string }> {
return this.post('/api/v1/quotes/calculate', request);
}
/**
* 입력 스키마 조회
*/
async getCalculationSchema(productCategory?: string): Promise<{ success: boolean; data: Record<string, unknown> }> {
const query = productCategory ? `?product_category=${productCategory}` : '';
return this.get(`/api/v1/quotes/calculation-schema${query}`);
}
}
export const quoteApi = new QuoteApiClient();
2.2 QuoteRegistration.tsx 수정
수정할 파일: react/src/components/quotes/QuoteRegistration.tsx
// 추가할 import
import { quoteApi } from '@/lib/api/quote';
import { useState } from 'react';
// 상태 추가 (컴포넌트 내부)
const [calculationResult, setCalculationResult] = useState<CalculationResult | null>(null);
const [isCalculating, setIsCalculating] = useState(false);
// handleAutoCalculate 수정 (line 332-335)
const handleAutoCalculate = async () => {
const item = formData.items[activeItemIndex];
if (!item.openWidth || !item.openHeight) {
toast.error('오픈사이즈(W0, H0)를 입력해주세요.');
return;
}
setIsCalculating(true);
try {
const response = await quoteApi.calculate({
inputs: {
W0: parseFloat(item.openWidth),
H0: parseFloat(item.openHeight),
QTY: item.quantity,
INSTALL_TYPE: item.guideRailType,
CONTROL_TYPE: item.controller,
},
product_category: item.productCategory as 'screen' | 'steel' || 'screen',
});
if (response.success) {
setCalculationResult(response.data);
toast.success('자동 산출이 완료되었습니다.');
} else {
toast.error(response.message || '산출 중 오류가 발생했습니다.');
}
} catch (error) {
console.error('자동 산출 오류:', error);
toast.error('서버 연결에 실패했습니다.');
} finally {
setIsCalculating(false);
}
};
// 산출 결과 반영 함수 추가
const handleApplyCalculation = () => {
if (!calculationResult) return;
// 산출된 품목을 견적 항목에 반영
const newItems = calculationResult.items.map((item, index) => ({
id: `calc-${Date.now()}-${index}`,
floor: formData.items[activeItemIndex].floor,
code: item.item_code,
productCategory: formData.items[activeItemIndex].productCategory,
productName: item.item_name,
openWidth: formData.items[activeItemIndex].openWidth,
openHeight: formData.items[activeItemIndex].openHeight,
guideRailType: formData.items[activeItemIndex].guideRailType,
motorPower: formData.items[activeItemIndex].motorPower,
controller: formData.items[activeItemIndex].controller,
quantity: item.quantity,
wingSize: formData.items[activeItemIndex].wingSize,
inspectionFee: item.unit_price,
unitPrice: item.unit_price,
totalAmount: item.total_price,
}));
setFormData({
...formData,
items: [...formData.items.slice(0, activeItemIndex), ...newItems, ...formData.items.slice(activeItemIndex + 1)],
});
setCalculationResult(null);
toast.success(`${newItems.length}개 품목이 반영되었습니다.`);
};
2.3 산출 결과 표시 UI 추가
{/* 자동 견적 산출 버튼 아래에 추가 */}
{calculationResult && (
<Card className="border-green-200 bg-green-50/50">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Calculator className="h-4 w-4" />
산출 결과
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* 계산 변수 */}
<div className="grid grid-cols-4 gap-2 text-sm">
{Object.entries(calculationResult.outputs).map(([key, val]) => (
<div key={key} className="bg-white p-2 rounded border">
<div className="text-gray-500 text-xs">{val.name}</div>
<div className="font-medium">{typeof val.value === 'number' ? val.value.toFixed(2) : val.value}</div>
</div>
))}
</div>
{/* 산출 품목 */}
<table className="w-full text-sm border-collapse">
<thead>
<tr className="bg-gray-100">
<th className="p-2 text-left">품목코드</th>
<th className="p-2 text-left">품목명</th>
<th className="p-2 text-right">수량</th>
<th className="p-2 text-right">단가</th>
<th className="p-2 text-right">금액</th>
</tr>
</thead>
<tbody>
{calculationResult.items.map((item, i) => (
<tr key={i} className="border-b">
<td className="p-2 font-mono text-xs">{item.item_code}</td>
<td className="p-2">{item.item_name}</td>
<td className="p-2 text-right">{item.quantity}</td>
<td className="p-2 text-right">{item.unit_price.toLocaleString()}</td>
<td className="p-2 text-right font-medium">{item.total_price.toLocaleString()}</td>
</tr>
))}
</tbody>
<tfoot>
<tr className="bg-gray-50 font-medium">
<td colSpan={4} className="p-2 text-right">합계</td>
<td className="p-2 text-right">{calculationResult.costs.subtotal.toLocaleString()}원</td>
</tr>
</tfoot>
</table>
{/* 반영 버튼 */}
<Button onClick={handleApplyCalculation} className="w-full bg-green-600 hover:bg-green-700">
<Check className="h-4 w-4 mr-2" />
품목에 반영하기
</Button>
</CardContent>
</Card>
)}
Phase 3: 통합 테스트 (1일)
3.1 테스트 시나리오
| 번호 | 테스트 케이스 | 입력값 | 예상 결과 |
|---|---|---|---|
| 1 | 기본 스크린 산출 | W0=3000, H0=2500 | 가이드레일 PT-GR-3000, 모터 PT-MOTOR-150 |
| 2 | 대형 스크린 산출 | W0=5000, H0=4000 | 모터 규격 상향 (300K 이상) |
| 3 | 철재 산출 | W0=2000, H0=2000, steel | 중량 M*25 적용 |
| 4 | 품목 반영 | 산출 후 반영 클릭 | 견적 항목에 추가됨 |
| 5 | 에러 처리 | W0/H0 미입력 | "오픈사이즈를 입력해주세요" |
3.2 검증 체크리스트
□ MNG 시뮬레이터에서 수식 계산 정확도 확인
□ React 자동산출 버튼 클릭 → API 호출 확인
□ 산출 결과 테이블 정상 표시
□ "품목에 반영하기" 클릭 → 견적 항목 추가 확인
□ 견적 저장 시 calculation_inputs 필드 저장 확인
□ 에러 시 적절한 메시지 표시
3. SAM 개발 규칙 요약
3.1 API 개발 규칙 (CLAUDE.md 참조)
// Controller: FormRequest + ApiResponse 패턴
public function calculate(QuoteCalculateRequest $request)
{
return ApiResponse::handle(function () use ($request) {
return $this->calculationService->calculate($request->validated());
}, __('message.quote.calculated'));
}
// Service: 비즈니스 로직 분리
class QuoteCalculationService extends Service
{
public function calculate(array $inputs, ?string $productCategory = null): array
{
$tenantId = $this->tenantId(); // 필수
// ...
}
}
// 응답 형식
{
"success": true,
"message": "견적이 산출되었습니다.",
"data": { ... }
}
3.2 React 개발 패턴
// API 클라이언트 패턴 (react/src/lib/api/client.ts)
class ApiClient {
async post<T>(endpoint: string, data?: unknown): Promise<T>
async get<T>(endpoint: string): Promise<T>
}
// 컴포넌트 패턴
// - shadcn/ui 컴포넌트 사용
// - toast (sonner) 알림
// - FormField, Card, Button 등
3.3 MNG 개발 패턴
// Artisan 명령어 패턴
protected $signature = 'quote:seed-formulas {--tenant=1}';
// 모델 사용
use App\Models\Quote\QuoteFormula;
use App\Models\Quote\QuoteFormulaCategory;
// 서비스 패턴
class QuoteFormulaService {
public function __construct(
private FormulaEvaluatorService $evaluator
) {}
}
4. 파일 구조
SAM/
├── mng/
│ ├── app/Console/Commands/
│ │ └── SeedQuoteFormulasCommand.php # 🆕 Phase 1
│ ├── app/Models/Quote/
│ │ ├── QuoteFormula.php # ✅ 있음
│ │ ├── QuoteFormulaCategory.php # ✅ 있음
│ │ └── QuoteFormulaRange.php # ✅ 있음
│ └── app/Services/Quote/
│ └── FormulaEvaluatorService.php # ✅ 있음
│
├── api/
│ ├── app/Http/Controllers/Api/V1/
│ │ └── QuoteController.php # ✅ calculate() 있음
│ ├── app/Services/Quote/
│ │ ├── QuoteCalculationService.php # ✅ 있음
│ │ └── FormulaEvaluatorService.php # ✅ 있음
│ └── database/seeders/
│ └── QuoteFormulaSeeder.php # 참조용 데이터
│
├── react/
│ ├── src/lib/api/
│ │ ├── client.ts # ✅ ApiClient 클래스
│ │ └── quote.ts # 🆕 Phase 2
│ └── src/components/quotes/
│ └── QuoteRegistration.tsx # ⚡ Phase 2 수정
│
└── docs/plans/
└── quote-auto-calculation-development-plan.md # 이 문서
5. 수식 계산 예시
입력: W0=3000mm, H0=2500mm, product_category=screen
계산 순서:
1. W1 = W0 + 140 = 3140mm (스크린 제작 가로)
2. H1 = H0 + 350 = 2850mm (스크린 제작 세로)
3. M = W1 * H1 / 1000000 = 8.949㎡ (면적)
4. K = M * 2 + W0 / 1000 * 14.17 = 60.41kg (중량)
5. G = H0 + 250 = 2750mm (가이드레일 길이)
6. S = W0 + 220 = 3220mm (케이스 사이즈)
범위 자동 선택:
- 가이드레일: G=2750 → 2438 < G ≤ 3000 → PT-GR-3000 × 2개
- 케이스: S=3220 → 3000 < S ≤ 3600 → PT-CASE-3600 × 1개
- 모터: K=60.41 → 0 < K ≤ 150 → PT-MOTOR-150 × 1개
6. 일정 요약
| Phase | 작업 | 예상 기간 | 상태 |
|---|---|---|---|
| 1 | MNG 시더 명령어 생성 | 1일 | ✅ 완료 |
| 2 | React Quote API 클라이언트 생성 | 0.5일 | ✅ 완료 |
| 3 | React handleAutoCalculate API 연동 | 0.5일 | ✅ 완료 |
| 4 | 산출 결과 UI 추가 | 0.5일 | ✅ 완료 |
| 5 | 문서 업데이트 | 0.5시간 | ✅ 완료 |
| 합계 | 약 2시간 | ✅ |
7. 완료된 구현 내역
생성된 파일
| 파일 경로 | 역할 |
|---|---|
mng/app/Console/Commands/SeedQuoteFormulasCommand.php |
MNG 견적수식 시더 명령어 |
react/src/lib/api/quote.ts |
React Quote API 클라이언트 |
수정된 파일
| 파일 경로 | 변경 내용 |
|---|---|
react/src/components/quotes/QuoteRegistration.tsx |
handleAutoCalculate API 연동, 산출 결과 UI 추가 |
MNG 시더 실행 결과
✅ 견적수식 시드 완료!
카테고리: 11개
수식: 18개
범위: 18개
React 기능 구현
handleAutoCalculate: API 호출 및 로딩 상태 관리handleApplyCalculation: 산출 결과를 견적 항목에 반영- 산출 결과 UI: 변수, 품목 테이블, 비용 합계 표시
- 에러 처리: 입력값 검증, API 에러 토스트
문서 버전: 3.0 (구현 완료) 작성자: Claude Code 최종 업데이트: 2025-12-22