feat(WEB): 발주처 검색 모달 추가 및 견적 할인 기능 개선

- SupplierSearchModal: 매입 가능 거래처 검색 모달 신규 생성
- QuoteRegistrationV2: 할인율/할인금액을 formData로 통합하여 저장/로드 연동
- QuoteFooterBar: view 모드에서 할인 버튼 비활성화
- types.ts: discountRate/discountAmount 필드 추가, 할인 반영 총액 계산 수정
- quote-management page: 저장 실패 시 에러 메시지 정확히 표시하도록 throw 방식 변경
This commit is contained in:
2026-01-30 11:23:35 +09:00
parent 5c8fe8e04c
commit a486977b80
5 changed files with 297 additions and 16 deletions

View File

@@ -140,10 +140,11 @@ export function QuoteFooterBar({
</Button>
)}
{/* 할인하기 */}
{/* 할인하기 - view 모드에서는 비활성 */}
{onDiscount && (
<Button
onClick={onDiscount}
disabled={isViewMode}
variant="outline"
className="gap-2 px-6 border-orange-300 text-orange-600 hover:bg-orange-50"
>

View File

@@ -96,6 +96,8 @@ export interface QuoteFormDataV2 {
vatType: "included" | "excluded"; // 부가세 (포함/별도)
remarks: string; // 비고
status: "draft" | "temporary" | "final"; // 작성중, 임시저장, 최종저장
discountRate: number; // 할인율 (%)
discountAmount: number; // 할인 금액
locations: LocationItem[];
}
@@ -133,6 +135,8 @@ const INITIAL_FORM_DATA: QuoteFormDataV2 = {
vatType: "included", // 기본값: 부가세 포함
remarks: "",
status: "draft",
discountRate: 0,
discountAmount: 0,
locations: [],
};
@@ -181,8 +185,9 @@ export function QuoteRegistrationV2({
const [transactionPreviewOpen, setTransactionPreviewOpen] = useState(false);
const [discountModalOpen, setDiscountModalOpen] = useState(false);
const [formulaViewOpen, setFormulaViewOpen] = useState(false);
const [discountRate, setDiscountRate] = useState(0);
const [discountAmount, setDiscountAmount] = useState(0);
// 할인율/할인금액은 formData에서 관리 (저장/로드 연동)
const discountRate = formData.discountRate ?? 0;
const discountAmount = formData.discountAmount ?? 0;
const pendingAutoCalculateRef = useRef(false);
// API 데이터
@@ -309,8 +314,7 @@ export function QuoteRegistrationV2({
// 할인 적용 핸들러
const handleApplyDiscount = useCallback((rate: number, amount: number) => {
setDiscountRate(rate);
setDiscountAmount(amount);
setFormData(prev => ({ ...prev, discountRate: rate, discountAmount: amount }));
toast.success(`할인이 적용되었습니다. (${rate.toFixed(1)}%, ${amount.toLocaleString()}원)`);
}, []);
@@ -650,7 +654,8 @@ export function QuoteRegistrationV2({
toast.success(saveType === "temporary" ? "저장되었습니다." : "견적이 확정되었습니다.");
} catch (error) {
if (isNextRedirectError(error)) throw error;
toast.error("저장 중 오류가 발생했습니다.");
const message = error instanceof Error ? error.message : "저장 중 오류가 발생했습니다.";
toast.error(message);
} finally {
setIsSaving(false);
}

View File

@@ -78,6 +78,8 @@ export interface Quote {
supplyAmount: number;
taxAmount: number;
totalAmount: number;
discountRate: number;
discountAmount: number;
status: QuoteStatus;
currentRevision: number;
isFinal: boolean;
@@ -247,6 +249,8 @@ export function transformApiToFrontend(apiData: QuoteApiData): Quote {
supplyAmount: parseFloat(String(apiData.supply_amount)) || 0,
taxAmount: parseFloat(String(apiData.tax_amount)) || 0,
totalAmount: parseFloat(String(apiData.total_amount)) || 0,
discountRate: Number(apiData.discount_rate) || 0,
discountAmount: Number(apiData.discount_amount) || 0,
status: apiData.status,
currentRevision: apiData.current_revision || 0,
isFinal: apiData.is_final || false,
@@ -837,10 +841,12 @@ export function transformV2ToApi(
}));
}
// 3. 총액 계산
// 3. 총액 계산 (할인 반영)
const totalSupply = items.reduce((sum, item) => sum + item.total_price, 0);
const totalTax = Math.round(totalSupply * 0.1);
const grandTotal = totalSupply + totalTax;
const discountAmt = data.discountAmount || 0;
const discountedSupply = totalSupply - discountAmt;
const totalTax = Math.round(discountedSupply * 0.1);
const grandTotal = discountedSupply + totalTax;
// 4. API 요청 객체 반환
return {
@@ -858,7 +864,7 @@ export function transformV2ToApi(
unit_symbol: '개소',
total_amount: grandTotal,
discount_rate: data.discountRate || 0,
discount_amount: data.discountAmount || 0,
discount_amount: discountAmt,
status: data.status === 'final' ? 'finalized' : 'draft',
is_final: data.status === 'final',
calculation_inputs: calculationInputs,
@@ -955,6 +961,8 @@ export function transformApiToV2(apiData: QuoteApiData): QuoteFormDataV2 {
managerContact?: string;
deliveryDate?: string;
description?: string;
discountRate?: number;
discountAmount?: number;
};
return {
@@ -978,8 +986,9 @@ export function transformApiToV2(apiData: QuoteApiData): QuoteFormDataV2 {
// raw API: remarks || description, transformed: description
remarks: apiData.remarks || apiData.description || transformed.description || '',
status: mapStatus(apiData.status),
discountRate: Number(apiData.discount_rate) || 0,
discountAmount: Number(apiData.discount_amount) || 0,
// raw API: discount_rate, transformed: discountRate
discountRate: Number(apiData.discount_rate) || transformed.discountRate || 0,
discountAmount: Number(apiData.discount_amount) || transformed.discountAmount || 0,
locations: locations,
};
}