/** * 견적 등록/수정 컴포넌트 V2 * * 새로운 레이아웃: * - 좌우 분할: 발주 개소 목록 | 선택 개소 상세 * - 하단: 견적 금액 요약 (개소별 + 상세별) * - 푸터: 총 금액 + 버튼들 (견적서 산출, 임시저장, 최종저장) */ "use client"; import { useState, useEffect, useMemo, useCallback, useRef } from "react"; import { FileText, Calculator } from "lucide-react"; import { toast } from "sonner"; import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; import { Badge } from "../ui/badge"; import { Input } from "../ui/input"; import { DatePicker } from "../ui/date-picker"; import { PhoneInput } from "../ui/phone-input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "../ui/select"; import { SearchableSelect } from "../ui/searchable-select"; import { LocationListPanel } from "./LocationListPanel"; import { LocationDetailPanel } from "./LocationDetailPanel"; import { QuoteSummaryPanel } from "./QuoteSummaryPanel"; import { QuoteFooterBar } from "./QuoteFooterBar"; import { QuotePreviewModal } from "./QuotePreviewModal"; import { QuoteTransactionModal } from "./QuoteTransactionModal"; import { DiscountModal } from "./DiscountModal"; import { FormulaViewModal } from "./FormulaViewModal"; import { getFinishedGoods, calculateBomBulk, getQuoteReferenceData, type FinishedGoods, type BomBulkResponse, } from "./actions"; import type { BomCalculationResult } from "./types"; import { getClients } from "../accounting/VendorManagement/actions"; import { isNextRedirectError } from "@/lib/utils/redirect-error"; // 실제 로그인 사용자 정보는 localStorage('user')에 저장됨 (LoginPage.tsx 참조) import { useDevFill } from "@/components/dev/useDevFill"; import type { Vendor } from "../accounting/VendorManagement"; import type { BomCalculationResultItem } from "./types"; import { getLocalDateString } from "@/lib/utils/date"; import { formatNumber } from "@/lib/utils/amount"; // ============================================================================= // 타입 정의 // ============================================================================= // 발주 개소 항목 export interface LocationItem { id: string; floor: string; // 층 code: string; // 부호 openWidth: number; // 가로 (오픈사이즈 W) openHeight: number; // 세로 (오픈사이즈 H) productCode: string; // 제품코드 productName: string; // 제품명 itemCategory?: string; // 품목 카테고리 (스크린/철재) quantity: number; // 수량 guideRailType: string; // 가이드레일 설치 유형 motorPower: string; // 모터 전원 controller: string; // 연동제어기 wingSize: number; // 마구리 날개치수 inspectionFee: number; // 검사비 // 계산 결과 manufactureWidth?: number; // 제작사이즈 W manufactureHeight?: number; // 제작사이즈 H weight?: number; // 산출중량 (kg) area?: number; // 산출면적 (m²) unitPrice?: number; // 단가 totalPrice?: number; // 합계 bomResult?: BomCalculationResult; // BOM 계산 결과 } // 견적 폼 데이터 V2 export interface QuoteFormDataV2 { id?: string; quoteNumber: string; // 견적번호 registrationDate: string; // 접수일 writer: string; // 작성자 clientId: string; // 수주처 ID clientName: string; // 수주처명 siteName: string; // 현장명 manager: string; // 담당자 contact: string; // 연락처 vatType: "included" | "excluded"; // 부가세 (포함/별도) remarks: string; // 비고 status: "draft" | "temporary" | "final" | "converted"; // 작성중, 임시저장, 최종저장, 수주전환 discountRate: number; // 할인율 (%) discountAmount: number; // 할인 금액 locations: LocationItem[]; orderId?: number | null; // 연결된 수주 ID (수주전환 시 설정) } // ============================================================================= // 상수 // ============================================================================= // 초기 개소 항목 const _createNewLocation = (): LocationItem => ({ id: `loc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, floor: "", code: "", openWidth: 0, openHeight: 0, productCode: "", productName: "", quantity: 1, guideRailType: "wall", motorPower: "single", controller: "basic", wingSize: 50, inspectionFee: 50000, }); // 초기 폼 데이터 const INITIAL_FORM_DATA: QuoteFormDataV2 = { quoteNumber: "", // 자동생성 또는 서버에서 부여 registrationDate: getLocalDateString(new Date()), writer: "", // useAuth()에서 currentUser.name으로 설정됨 clientId: "", clientName: "", siteName: "", manager: "", contact: "", vatType: "included", // 기본값: 부가세 포함 remarks: "", status: "draft", discountRate: 0, discountAmount: 0, locations: [], }; // ============================================================================= // Props // ============================================================================= interface QuoteRegistrationProps { mode: "create" | "view" | "edit"; onBack: () => void; onSave?: (data: QuoteFormDataV2, saveType: "temporary" | "final") => Promise; onCalculate?: () => void; onEdit?: () => void; onOrderRegister?: () => void; /** 수주 보기 (이미 수주가 있는 경우) */ onOrderView?: () => void; initialData?: QuoteFormDataV2 | null; isLoading?: boolean; /** IntegratedDetailTemplate 사용 시 타이틀 영역 숨김 */ hideHeader?: boolean; } // ============================================================================= // 메인 컴포넌트 // ============================================================================= export function QuoteRegistration({ mode, onBack, onSave, onCalculate: _onCalculate, onEdit, onOrderRegister, onOrderView, initialData, isLoading: _isLoading = false, hideHeader = false, }: QuoteRegistrationProps) { // --------------------------------------------------------------------------- // 상태 // --------------------------------------------------------------------------- const [formData, setFormData] = useState( initialData || INITIAL_FORM_DATA ); const [selectedLocationId, setSelectedLocationId] = useState(null); const [isSaving, setIsSaving] = useState(false); const [isCalculating, setIsCalculating] = useState(false); const [quotePreviewOpen, setQuotePreviewOpen] = useState(false); const [transactionPreviewOpen, setTransactionPreviewOpen] = useState(false); const [discountModalOpen, setDiscountModalOpen] = useState(false); const [formulaViewOpen, setFormulaViewOpen] = useState(false); // 할인율/할인금액은 formData에서 관리 (저장/로드 연동) const discountRate = formData.discountRate ?? 0; const discountAmount = formData.discountAmount ?? 0; const pendingAutoCalculateRef = useRef(false); // API 데이터 const [clients, setClients] = useState([]); const [finishedGoods, setFinishedGoods] = useState([]); const [siteNames, setSiteNames] = useState([]); const [locationCodes, setLocationCodes] = useState([]); const [isLoadingClients, setIsLoadingClients] = useState(false); const [, setIsLoadingProducts] = useState(false); // handleCalculate 참조 (DevFill에서 사용) const calculateRef = useRef<(() => Promise) | null>(null); // 디버그용: formData를 window에 노출 useEffect(() => { if (typeof window !== "undefined") { (window as unknown as { __QUOTE_DEBUG__: { formData: QuoteFormDataV2; selectedLocationId: string | null } }).__QUOTE_DEBUG__ = { formData, selectedLocationId, }; } }, [formData, selectedLocationId]); // --------------------------------------------------------------------------- // DevFill (개발/테스트용 자동 채우기) // --------------------------------------------------------------------------- useDevFill("quoteV2", useCallback(() => { // 제품 1개 고정 선택 (모델별 인증이라 섞을 수 없음) const fixedProduct = finishedGoods.length > 0 ? finishedGoods[Math.floor(Math.random() * finishedGoods.length)] : null; // 층 순서 (정렬된 상태로 순차 할당) const sortedFloors = ["B2", "B1", "1F", "2F", "3F", "4F", "5F", "R"]; // 부호 접두사 1개 고정 const codePrefixes = ["SD", "FSS", "FD", "SS", "DS"]; const fixedPrefix = codePrefixes[Math.floor(Math.random() * codePrefixes.length)]; const guideRailTypes = ["wall", "floor", "mixed"]; const motorPowers = ["single", "three"]; const controllers = ["basic", "smart", "premium"]; // 1~5개 랜덤 개소 생성 const locationCount = Math.floor(Math.random() * 5) + 1; // 층을 순차 할당할 시작 인덱스 (랜덤 시작점, 순서대로 올라감) const maxStartIdx = Math.max(0, sortedFloors.length - locationCount); const floorStartIdx = Math.floor(Math.random() * (maxStartIdx + 1)); const testLocations: LocationItem[] = []; for (let i = 0; i < locationCount; i++) { const floorIdx = Math.min(floorStartIdx + i, sortedFloors.length - 1); const randomWidth = (Math.floor(Math.random() * 40) + 20) * 100; const randomHeight = (Math.floor(Math.random() * 30) + 20) * 100; testLocations.push({ id: `loc-${Date.now()}-${i}`, floor: sortedFloors[floorIdx], code: `${fixedPrefix}-${String(i + 1).padStart(2, "0")}`, openWidth: randomWidth, openHeight: randomHeight, productCode: fixedProduct?.item_code || "FG-SCR-001", productName: fixedProduct?.item_name || "방화 스크린 셔터 (소형)", quantity: 1, guideRailType: guideRailTypes[Math.floor(Math.random() * guideRailTypes.length)], motorPower: motorPowers[Math.floor(Math.random() * motorPowers.length)], controller: controllers[Math.floor(Math.random() * controllers.length)], wingSize: [50, 60, 70][Math.floor(Math.random() * 3)], inspectionFee: [50000, 60000, 70000][Math.floor(Math.random() * 3)], }); } // 로그인 사용자 정보 가져오기 let writerName = ""; try { const userStr = localStorage.getItem("user"); if (userStr) { const user = JSON.parse(userStr); writerName = user?.name || ""; } } catch (e) { console.error("[DevFill] 사용자 정보 로드 실패:", e); } const testData: QuoteFormDataV2 = { quoteNumber: "", registrationDate: getLocalDateString(new Date()), writer: writerName, clientId: clients[0]?.id?.toString() || "", clientName: clients[0]?.vendorName || "테스트 거래처", siteName: "테스트 현장", manager: "홍길동", contact: "010-1234-5678", vatType: "included", remarks: "[DevFill] 테스트 견적입니다.", status: "draft", discountRate: 0, discountAmount: 0, locations: testLocations, }; setFormData(testData); setSelectedLocationId(testLocations[0].id); toast.success(`[DevFill] 테스트 데이터가 채워졌습니다. (${locationCount}개 개소)`); // 자동 견적 산출 트리거 pendingAutoCalculateRef.current = true; }, [clients, finishedGoods])); // --------------------------------------------------------------------------- // 계산된 값 // --------------------------------------------------------------------------- // 선택된 개소 const selectedLocation = useMemo(() => { return formData.locations.find((loc) => loc.id === selectedLocationId) || null; }, [formData.locations, selectedLocationId]); // 총 금액 (할인 전) const totalAmount = useMemo(() => { return formData.locations.reduce((sum, loc) => sum + (loc.totalPrice || 0), 0); }, [formData.locations]); // 할인 적용 후 총 금액 const discountedTotalAmount = useMemo(() => { return totalAmount - discountAmount; }, [totalAmount, discountAmount]); // 할인 적용 핸들러 const handleApplyDiscount = useCallback((rate: number, amount: number) => { setFormData(prev => ({ ...prev, discountRate: rate, discountAmount: amount })); toast.success(`할인이 적용되었습니다. (${rate.toFixed(1)}%, ${formatNumber(amount)}원)`); }, []); // 개소별 합계 const _locationTotals = useMemo(() => { return formData.locations.map((loc) => ({ id: loc.id, label: `${loc.floor} / ${loc.code}`, productCode: loc.productCode, quantity: loc.quantity, unitPrice: loc.unitPrice || 0, totalPrice: loc.totalPrice || 0, })); }, [formData.locations]); // BOM 결과 유무 const hasBomResult = useMemo(() => { return formData.locations.some((loc) => loc.bomResult); }, [formData.locations]); // --------------------------------------------------------------------------- // 작성자 자동 설정 (create 모드에서 로그인 사용자 정보 로드) // --------------------------------------------------------------------------- useEffect(() => { if (mode === "create" && !formData.writer) { // 실제 로그인 사용자 정보는 localStorage('user')에 저장됨 try { const userStr = localStorage.getItem("user"); if (userStr) { const user = JSON.parse(userStr); if (user?.name) { setFormData((prev) => ({ ...prev, writer: user.name })); } } } catch (e) { console.error("[QuoteRegistration] 사용자 정보 로드 실패:", e); } } }, [mode, formData.writer]); // --------------------------------------------------------------------------- // 초기 데이터 로드 // --------------------------------------------------------------------------- useEffect(() => { const loadInitialData = async () => { // 거래처 로드 setIsLoadingClients(true); try { const result = await getClients(); if (result.success) { setClients(result.data); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error("거래처 로드 실패:", error); } finally { setIsLoadingClients(false); } // 완제품 로드 setIsLoadingProducts(true); try { const result = await getFinishedGoods(); if (result.success) { setFinishedGoods(result.data); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error("완제품 로드 실패:", error); } finally { setIsLoadingProducts(false); } // 참조 데이터 로드 (현장명, 부호) try { const result = await getQuoteReferenceData(); if (result.success) { setSiteNames(result.data.siteNames); setLocationCodes(result.data.locationCodes); } } catch (error) { if (isNextRedirectError(error)) throw error; } }; loadInitialData(); }, []); // initialData 변경 시 formData 업데이트 useEffect(() => { if (initialData) { setFormData(initialData); // 첫 번째 개소 자동 선택 if (initialData.locations.length > 0 && !selectedLocationId) { setSelectedLocationId(initialData.locations[0].id); } } }, [initialData]); // --------------------------------------------------------------------------- // 핸들러 // --------------------------------------------------------------------------- // 기본 정보 변경 const handleFieldChange = useCallback((field: keyof QuoteFormDataV2, value: string) => { setFormData((prev) => ({ ...prev, [field]: value })); }, []); // 개소 추가 (BOM 계산 성공 시에만 추가, 성공/실패 반환) const handleAddLocation = useCallback(async (location: Omit): Promise => { const newLocation: LocationItem = { ...location, id: `loc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, }; // BOM 계산 필수 조건 체크 if (!newLocation.productCode || newLocation.openWidth <= 0 || newLocation.openHeight <= 0) { toast.error("제품, 가로, 세로를 모두 입력해주세요."); return false; } // 먼저 BOM 계산 API 호출 try { const bomItem = { finished_goods_code: newLocation.productCode, openWidth: newLocation.openWidth, openHeight: newLocation.openHeight, quantity: newLocation.quantity, guideRailType: newLocation.guideRailType, motorPower: newLocation.motorPower, controller: newLocation.controller, wingSize: newLocation.wingSize, inspectionFee: newLocation.inspectionFee, }; const result = await calculateBomBulk([bomItem]); if (result.success && result.data) { const apiData = result.data as BomBulkResponse; const bomResponseItems = apiData.items || []; const bomResult = bomResponseItems[0]?.result; if (bomResult) { // BOM 계산 성공 시에만 개소 추가 const locationWithBom: LocationItem = { ...newLocation, unitPrice: bomResult.grand_total, totalPrice: bomResult.grand_total * newLocation.quantity, bomResult: bomResult, }; setFormData((prev) => ({ ...prev, locations: [...prev.locations, locationWithBom], })); setSelectedLocationId(newLocation.id); toast.success("개소가 추가되고 BOM이 계산되었습니다."); return true; } } // API 에러 메시지 표시 (개소 추가 안 함) toast.error(result.error || "BOM 계산 실패 - 개소가 추가되지 않았습니다."); return false; } catch (error) { console.error("[handleAddLocation] BOM 계산 실패:", error); toast.error("BOM 계산 중 오류가 발생했습니다."); return false; } }, []); // 개소 삭제 // 개소 복제 (부호 자동 채번) const handleCloneLocation = useCallback((locationId: string) => { const source = formData.locations.find((loc) => loc.id === locationId); if (!source) return; // 층/부호가 없거나 "-"이면 그대로 유지 let newFloor = source.floor || "-"; let newCode = source.code || "-"; if (newCode !== "-") { // 부호에서 접두어와 번호 분리 (예: "DS-01" → prefix="DS-", num=1) const codeMatch = source.code.match(/^(.*?)(\d+)$/); if (codeMatch) { const prefix = codeMatch[1]; // "DS-" const numLength = codeMatch[2].length; // 2 (자릿수 보존) // 같은 접두어를 가진 부호 중 최대 번호 찾기 let maxNum = 0; formData.locations.forEach((loc) => { const m = loc.code.match(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\d+)$`)); if (m) { maxNum = Math.max(maxNum, parseInt(m[1], 10)); } }); newCode = prefix + String(maxNum + 1).padStart(numLength, "0"); } else { newCode = source.code + "-copy"; } } const clonedLocation: LocationItem = { ...source, id: `loc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, floor: newFloor, code: newCode, }; setFormData((prev) => ({ ...prev, locations: [...prev.locations, clonedLocation], })); setSelectedLocationId(clonedLocation.id); toast.success(`개소가 복제되었습니다. (${newCode})`); }, [formData.locations]); // 개소 삭제 const handleDeleteLocation = useCallback((locationId: string) => { setFormData((prev) => ({ ...prev, locations: prev.locations.filter((loc) => loc.id !== locationId), })); if (selectedLocationId === locationId) { setSelectedLocationId(formData.locations[0]?.id || null); } toast.success("개소가 삭제되었습니다."); }, [selectedLocationId, formData.locations]); // 개소 수정 const handleUpdateLocation = useCallback((locationId: string, updates: Partial) => { setFormData((prev) => ({ ...prev, locations: prev.locations.map((loc) => loc.id === locationId ? { ...loc, ...updates } : loc ), })); }, []); // 엑셀 업로드 const handleExcelUpload = useCallback((locations: Omit[]) => { const newLocations: LocationItem[] = locations.map((loc) => ({ ...loc, id: `loc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, })); setFormData((prev) => ({ ...prev, locations: [...prev.locations, ...newLocations], })); if (newLocations.length > 0) { setSelectedLocationId(newLocations[0].id); } toast.success(`${newLocations.length}개 개소가 추가되었습니다.`); }, []); // 견적 산출 const handleCalculate = useCallback(async () => { if (formData.locations.length === 0) { toast.error("산출할 개소가 없습니다."); return; } setIsCalculating(true); try { const bomItems = formData.locations.map((loc) => ({ finished_goods_code: loc.productCode, openWidth: loc.openWidth, openHeight: loc.openHeight, quantity: loc.quantity, guideRailType: loc.guideRailType, motorPower: loc.motorPower, controller: loc.controller, wingSize: loc.wingSize, inspectionFee: loc.inspectionFee, })); const result = await calculateBomBulk(bomItems); if (result.success && result.data) { // API 응답: { success, summary: { grand_total, ... }, items: [{ index, result: BomCalculationResult }] } const apiData = result.data as BomBulkResponse; const bomResponseItems = apiData.items || []; // 결과 반영 (수동 추가 품목 보존) const updatedLocations = formData.locations.map((loc, index) => { const bomItem = bomResponseItems.find((item) => item.index === index); const bomResult = bomItem?.result; if (bomResult) { // 기존 수동 추가 품목 추출 (is_manual: true) const manualItems = (loc.bomResult?.items || []).filter( (item: BomCalculationResultItem & { is_manual?: boolean }) => item.is_manual === true ); // 수동 추가 품목의 총 금액 const manualTotal = manualItems.reduce( (sum: number, item: BomCalculationResultItem) => sum + (item.total_price || 0), 0 ); // 새 BOM 결과에 수동 품목 병합 const mergedItems = [...(bomResult.items || []), ...manualItems]; const mergedGrandTotal = bomResult.grand_total + manualTotal; return { ...loc, unitPrice: mergedGrandTotal, totalPrice: mergedGrandTotal * loc.quantity, bomResult: { ...bomResult, items: mergedItems, grand_total: mergedGrandTotal, }, }; } return loc; }); setFormData((prev) => ({ ...prev, locations: updatedLocations })); toast.success(`${formData.locations.length}개 개소의 견적이 산출되었습니다.`); } else { toast.error(`견적 산출 실패: ${result.error}`); } } catch (error) { if (isNextRedirectError(error)) throw error; toast.error("견적 산출 중 오류가 발생했습니다."); } finally { setIsCalculating(false); } }, [formData.locations]); // handleCalculate 참조 업데이트 useEffect(() => { calculateRef.current = handleCalculate; }, [handleCalculate]); // DevFill 후 자동 견적 산출 useEffect(() => { if (pendingAutoCalculateRef.current && formData.locations.length > 0) { pendingAutoCalculateRef.current = false; // 상태 업데이트 완료 후 산출 실행 setTimeout(() => { handleCalculate(); }, 50); } }, [formData.locations, handleCalculate]); // 저장 (임시/최종) const handleSave = useCallback(async (saveType: "temporary" | "final") => { if (!onSave) return; // 확정 시 필수 필드 밸리데이션 if (saveType === "final") { const missing: string[] = []; if (!formData.clientName?.trim()) missing.push("업체명"); if (!formData.siteName?.trim()) missing.push("현장명"); if (!formData.manager?.trim()) missing.push("담당자"); if (!formData.contact?.trim()) missing.push("연락처"); if (missing.length > 0) { toast.error(`견적확정을 위해 다음 항목을 입력해주세요: ${missing.join(", ")}`); return; } } setIsSaving(true); try { const dataToSave: QuoteFormDataV2 = { ...formData, status: saveType === "temporary" ? "temporary" : "final", }; await onSave(dataToSave, saveType); // 확정 성공 시 상태 즉시 반영 → 견적확정 버튼 → 수주등록 버튼으로 전환 if (saveType === "final") { setFormData(prev => ({ ...prev, status: "final" })); } } catch (error) { if (isNextRedirectError(error)) throw error; const message = error instanceof Error ? error.message : "저장 중 오류가 발생했습니다."; toast.error(message); } finally { setIsSaving(false); } }, [formData, onSave]); // --------------------------------------------------------------------------- // 렌더링 // --------------------------------------------------------------------------- const isViewMode = mode === "view"; const pageTitle = mode === "create" ? "견적 등록 (V2 테스트)" : mode === "edit" ? "견적 수정 (V2 테스트)" : "견적 상세 (V2 테스트)"; return (
{/* 기본 정보 섹션 */}
{/* 타이틀 영역 - hideHeader 시 IntegratedDetailTemplate이 담당 */} {!hideHeader && (

{pageTitle}

{formData.status === "final" ? "견적 확정" : formData.status === "temporary" ? "저장됨" : "작성중"}
)} {/* 기본 정보 */} 기본 정보 {/* 1행: 견적번호 | 접수일 | 수주처 | 현장명 */}
handleFieldChange("registrationDate", date)} disabled={isViewMode} />
({ value: c.id, label: c.vendorName }))} value={formData.clientId} onChange={(value, option) => { setFormData((prev) => ({ ...prev, clientId: value, clientName: option.label, })); }} placeholder={isLoadingClients ? "로딩 중..." : "수주처를 선택하세요"} searchPlaceholder="수주처 검색..." emptyText="수주처가 없습니다" disabled={isViewMode || isLoadingClients} isLoading={isLoadingClients} />
({ value: name, label: name }))} value={formData.siteName} onChange={(value) => handleFieldChange("siteName", value)} placeholder="현장명을 선택하세요" searchPlaceholder="현장명 검색..." emptyText="현장명이 없습니다" disabled={isViewMode} />
{/* 2행: 담당자 | 연락처 | 작성자 | 부가세 */}
handleFieldChange("manager", e.target.value)} disabled={isViewMode} />
handleFieldChange("contact", value)} disabled={isViewMode} />
{/* 3행: 상태 | 비고 */}
handleFieldChange("remarks", e.target.value)} disabled={isViewMode} />
{/* 자동 견적 산출 섹션 */} 자동 견적 산출 {/* 좌우 분할 레이아웃 */}
{/* 왼쪽: 발주 개소 목록 + 추가 폼 */} {/* 오른쪽: 선택 개소 상세 */} { // 단일 개소 산출 const location = formData.locations.find((loc) => loc.id === locationId); if (!location) return; setIsCalculating(true); try { const bomItem = { finished_goods_code: location.productCode, openWidth: location.openWidth, openHeight: location.openHeight, quantity: location.quantity, guideRailType: location.guideRailType, motorPower: location.motorPower, controller: location.controller, wingSize: location.wingSize, inspectionFee: location.inspectionFee, }; const result = await calculateBomBulk([bomItem]); if (result.success && result.data) { const apiData = result.data as BomBulkResponse; const bomResult = apiData.items?.[0]?.result; if (bomResult) { handleUpdateLocation(locationId, { unitPrice: bomResult.grand_total, totalPrice: bomResult.grand_total * location.quantity, bomResult: bomResult, }); toast.success("견적이 산출되었습니다."); } } else { toast.error(result.error || "산출 실패"); } } catch (error) { console.error("산출 오류:", error); toast.error("산출 중 오류가 발생했습니다."); } finally { setIsCalculating(false); } }} onSaveItems={() => { toast.success("품목이 저장되었습니다."); }} finishedGoods={finishedGoods} disabled={isViewMode} isCalculating={isCalculating} />
{/* 견적 금액 요약 */}
{/* 푸터 바 (고정) */} setQuotePreviewOpen(true)} onTransactionView={() => setTransactionPreviewOpen(true)} onSave={() => handleSave("temporary")} onFinalize={() => handleSave("final")} onBack={onBack} onEdit={onEdit} onOrderRegister={onOrderRegister} onOrderView={onOrderView} orderId={formData.orderId} onDiscount={() => setDiscountModalOpen(true)} onFormulaView={() => setFormulaViewOpen(true)} hasBomResult={hasBomResult} isSaving={isSaving} isViewMode={isViewMode} /> {/* 견적서 보기 모달 */} {/* 거래명세서 보기 모달 */} {/* 할인하기 모달 */} {/* 수식보기 모달 */}
); }