feat: 개소 추가 시 자동 BOM 계산 및 BOM 있는 제품만 필터
- 개소 추가 시 BOM 계산 자동 실행 (성공 시에만 추가) - BOM 계산 실패 시 폼 초기화 방지, 에러 메시지 표시 - getFinishedGoods에 has_bom=1 파라미터 추가 - 제품 드롭다운에 코드+이름 함께 표시 - handleAddLocation을 async/await로 변경, boolean 반환
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
{/* 견적서 미리보기 모달 */}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user