feat: 개소 추가 시 자동 BOM 계산 및 BOM 있는 제품만 필터

- 개소 추가 시 BOM 계산 자동 실행 (성공 시에만 추가)
- BOM 계산 실패 시 폼 초기화 방지, 에러 메시지 표시
- getFinishedGoods에 has_bom=1 파라미터 추가
- 제품 드롭다운에 코드+이름 함께 표시
- handleAddLocation을 async/await로 변경, boolean 반환
This commit is contained in:
2026-01-27 12:47:38 +09:00
parent 55e92bc7b4
commit 815ed9267e
3 changed files with 109 additions and 28 deletions

View File

@@ -69,7 +69,7 @@ interface LocationListPanelProps {
locations: LocationItem[];
selectedLocationId: string | null;
onSelectLocation: (id: string) => void;
onAddLocation: (location: Omit<LocationItem, "id">) => void;
onAddLocation: (location: Omit<LocationItem, "id">) => Promise<boolean>;
onDeleteLocation: (id: string) => void;
onUpdateLocation: (locationId: string, updates: Partial<LocationItem>) => void;
onExcelUpload: (locations: Omit<LocationItem, "id">[]) => 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({
<SelectContent>
{finishedGoods.map((fg) => (
<SelectItem key={fg.item_code} value={fg.item_code}>
{fg.item_code}
{fg.item_code} {fg.item_name}
</SelectItem>
))}
</SelectContent>

View File

@@ -140,6 +140,8 @@ interface QuoteRegistrationV2Props {
onBack: () => void;
onSave?: (data: QuoteFormDataV2, saveType: "temporary" | "final") => Promise<void>;
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<LocationItem, "id">) => {
// 개소 추가 (BOM 계산 성공 시에만 추가, 성공/실패 반환)
const handleAddLocation = useCallback(async (location: Omit<LocationItem, "id">): Promise<boolean> => {
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}
/>
{/* 견적서 미리보기 모달 */}

View File

@@ -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);
}