Files
sam-docs/plans/archive/quote-auto-calculation-development-plan.md
권혁성 00023b2d69 chore: 계획 문서 정리 및 아카이브 이동
- 완료된 계획 문서 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 업데이트
2026-01-20 19:05:43 +09:00

26 KiB
Raw Blame History

견적 자동산출 개발 계획

작성일: 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