From 815ed9267e722b92af209ac7d711e8f4af91b56e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Tue, 27 Jan 2026 12:47:38 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B0=9C=EC=86=8C=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EC=8B=9C=20=EC=9E=90=EB=8F=99=20BOM=20=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?=EB=B0=8F=20BOM=20=EC=9E=88=EB=8A=94=20=EC=A0=9C=ED=92=88?= =?UTF-8?q?=EB=A7=8C=20=ED=95=84=ED=84=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 개소 추가 시 BOM 계산 자동 실행 (성공 시에만 추가) - BOM 계산 실패 시 폼 초기화 방지, 에러 메시지 표시 - getFinishedGoods에 has_bom=1 파라미터 추가 - 제품 드롭다운에 코드+이름 함께 표시 - handleAddLocation을 async/await로 변경, boolean 반환 --- src/components/quotes/LocationListPanel.tsx | 31 +++--- src/components/quotes/QuoteRegistrationV2.tsx | 105 +++++++++++++++--- src/components/quotes/actions.ts | 1 + 3 files changed, 109 insertions(+), 28 deletions(-) diff --git a/src/components/quotes/LocationListPanel.tsx b/src/components/quotes/LocationListPanel.tsx index 79bee6a0..79f793a7 100644 --- a/src/components/quotes/LocationListPanel.tsx +++ b/src/components/quotes/LocationListPanel.tsx @@ -69,7 +69,7 @@ interface LocationListPanelProps { locations: LocationItem[]; selectedLocationId: string | null; onSelectLocation: (id: string) => void; - onAddLocation: (location: Omit) => void; + onAddLocation: (location: Omit) => Promise; onDeleteLocation: (id: string) => void; onUpdateLocation: (locationId: string, updates: Partial) => void; onExcelUpload: (locations: Omit[]) => void; @@ -124,8 +124,8 @@ export function LocationListPanel({ setFormData((prev) => ({ ...prev, [field]: value })); }, []); - // 개소 추가 - const handleAdd = useCallback(() => { + // 개소 추가 (BOM 계산 성공 시에만 폼 초기화) + const handleAdd = useCallback(async () => { // 유효성 검사 if (!formData.floor || !formData.code) { toast.error("층과 부호를 입력해주세요."); @@ -157,17 +157,20 @@ export function LocationListPanel({ inspectionFee: 50000, }; - onAddLocation(newLocation); + // BOM 계산 성공 시에만 폼 초기화 + const success = await onAddLocation(newLocation); - // 폼 초기화 (일부 필드 유지) - setFormData((prev) => ({ - ...prev, - floor: "", - code: "", - openWidth: "", - openHeight: "", - quantity: "1", - })); + if (success) { + // 폼 초기화 (일부 필드 유지) + setFormData((prev) => ({ + ...prev, + floor: "", + code: "", + openWidth: "", + openHeight: "", + quantity: "1", + })); + } }, [formData, finishedGoods, onAddLocation]); // 엑셀 양식 다운로드 @@ -438,7 +441,7 @@ export function LocationListPanel({ {finishedGoods.map((fg) => ( - {fg.item_code} + {fg.item_code} {fg.item_name} ))} diff --git a/src/components/quotes/QuoteRegistrationV2.tsx b/src/components/quotes/QuoteRegistrationV2.tsx index 0d05c134..306f52f8 100644 --- a/src/components/quotes/QuoteRegistrationV2.tsx +++ b/src/components/quotes/QuoteRegistrationV2.tsx @@ -140,6 +140,8 @@ interface QuoteRegistrationV2Props { onBack: () => void; onSave?: (data: QuoteFormDataV2, saveType: "temporary" | "final") => Promise; onCalculate?: () => void; + onEdit?: () => void; + onOrderRegister?: () => void; initialData?: QuoteFormDataV2 | null; isLoading?: boolean; /** IntegratedDetailTemplate 사용 시 타이틀 영역 숨김 */ @@ -155,6 +157,8 @@ export function QuoteRegistrationV2({ onBack, onSave, onCalculate, + onEdit, + onOrderRegister, initialData, isLoading = false, hideHeader = false, @@ -397,18 +401,67 @@ export function QuoteRegistrationV2({ })); }, [clients]); - // 개소 추가 - const handleAddLocation = useCallback((location: Omit) => { + // 개소 추가 (BOM 계산 성공 시에만 추가, 성공/실패 반환) + const handleAddLocation = useCallback(async (location: Omit): Promise => { const newLocation: LocationItem = { ...location, id: `loc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, }; - setFormData((prev) => ({ - ...prev, - locations: [...prev.locations, newLocation], - })); - setSelectedLocationId(newLocation.id); - toast.success("개소가 추가되었습니다."); + + // 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; + } }, []); // 개소 삭제 @@ -484,21 +537,43 @@ export function QuoteRegistrationV2({ firstItem: bomResponseItems[0], }); - // 결과 반영 + // 결과 반영 (수동 추가 품목 보존) 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; + console.log(`[QuoteRegistrationV2] Location ${index} bomResult:`, { items: bomResult.items?.length, + manualItems: manualItems.length, + mergedItems: mergedItems.length, subtotals: bomResult.subtotals, - grand_total: bomResult.grand_total, + grand_total: mergedGrandTotal, }); + return { ...loc, - unitPrice: bomResult.grand_total, - totalPrice: bomResult.grand_total * loc.quantity, - bomResult: bomResult, + unitPrice: mergedGrandTotal, + totalPrice: mergedGrandTotal * loc.quantity, + bomResult: { + ...bomResult, + items: mergedItems, + grand_total: mergedGrandTotal, + }, }; } return loc; @@ -738,9 +813,11 @@ export function QuoteRegistrationV2({ onSaveTemporary={() => handleSave("temporary")} onSaveFinal={() => handleSave("final")} onBack={onBack} + onEdit={onEdit} + onOrderRegister={onOrderRegister} isCalculating={isCalculating} isSaving={isSaving} - disabled={isViewMode} + isViewMode={isViewMode} /> {/* 견적서 미리보기 모달 */} diff --git a/src/components/quotes/actions.ts b/src/components/quotes/actions.ts index 29e42bed..57c326a0 100644 --- a/src/components/quotes/actions.ts +++ b/src/components/quotes/actions.ts @@ -809,6 +809,7 @@ export async function getFinishedGoods(category?: string): Promise<{ try { const searchParams = new URLSearchParams(); searchParams.set('item_type', 'FG'); + searchParams.set('has_bom', '1'); // BOM이 있는 제품만 조회 if (category) { searchParams.set('item_category', category); }