From cf8dac0c6490469dab88a560004b0c07dbd48612 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 18 Sep 2025 00:23:40 +0000 Subject: (대표님) rfq, 계약, 서명 등 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/rfq-last/quotation-compare-view.tsx | 447 +++++++++++++++++++++++++++----- 1 file changed, 385 insertions(+), 62 deletions(-) (limited to 'lib/rfq-last/quotation-compare-view.tsx') diff --git a/lib/rfq-last/quotation-compare-view.tsx b/lib/rfq-last/quotation-compare-view.tsx index 491a1962..28c8b3b1 100644 --- a/lib/rfq-last/quotation-compare-view.tsx +++ b/lib/rfq-last/quotation-compare-view.tsx @@ -46,6 +46,7 @@ import { import { ComparisonData, selectVendor, cancelVendorSelection } from "./compare-action"; import { createPO, createGeneralContract, createBidding } from "./contract-actions"; import { toast } from "sonner"; +import { useRouter } from "next/navigation" interface QuotationCompareViewProps { data: ComparisonData; @@ -61,6 +62,61 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { const [selectionReason, setSelectionReason] = React.useState(""); const [cancelReason, setCancelReason] = React.useState(""); const [isSubmitting, setIsSubmitting] = React.useState(false); + const router = useRouter() + + const [selectedGeneralContractType, setSelectedGeneralContractType] = React.useState(""); + const [contractStartDate, setContractStartDate] = React.useState(""); + const [contractEndDate, setContractEndDate] = React.useState(""); + + // 계약종류 옵션 + const contractTypes = [ + { value: 'UP', label: 'UP - 자재단가계약' }, + { value: 'LE', label: 'LE - 임대차계약' }, + { value: 'IL', label: 'IL - 개별운송계약' }, + { value: 'AL', label: 'AL - 연간운송계약' }, + { value: 'OS', label: 'OS - 외주용역계약' }, + { value: 'OW', label: 'OW - 도급계약' }, + { value: 'IS', label: 'IS - 검사계약' }, + { value: 'LO', label: 'LO - LOI (의향서)' }, + { value: 'FA', label: 'FA - Frame Agreement' }, + { value: 'SC', label: 'SC - 납품합의계약' }, + { value: 'OF', label: 'OF - 클레임상계계약' }, + { value: 'AW', label: 'AW - 사전작업합의' }, + { value: 'AD', label: 'AD - 사전납품합의' }, + { value: 'AM', label: 'AM - 설계계약' }, + { value: 'SC_SELL', label: 'SC - 폐기물매각계약' }, + ]; + + // 입찰 관련 state 추가 + const [biddingContractType, setBiddingContractType] = React.useState<"unit_price" | "general" | "sale" | "">(""); + const [biddingType, setBiddingType] = React.useState(""); + const [awardCount, setAwardCount] = React.useState<"single" | "multiple">("single"); + const [biddingStartDate, setBiddingStartDate] = React.useState(""); + const [biddingEndDate, setBiddingEndDate] = React.useState(""); + + // 입찰 옵션들 + const biddingContractTypes = [ + { value: 'unit_price', label: '단가계약' }, + { value: 'general', label: '일반계약' }, + { value: 'sale', label: '매각계약' } + ]; + + const biddingTypes = [ + { value: 'equipment', label: '기자재' }, + { value: 'construction', label: '공사' }, + { value: 'service', label: '용역' }, + { value: 'lease', label: '임차' }, + { value: 'steel_stock', label: '형강스톡' }, + { value: 'piping', label: '배관' }, + { value: 'transport', label: '운송' }, + { value: 'waste', label: '폐기물' }, + { value: 'sale', label: '매각' } + ]; + + const awardCounts = [ + { value: 'single', label: '단수' }, + { value: 'multiple', label: '복수' } + ]; // 선정된 업체 정보 확인 const selectedVendor = data.vendors.find(v => v.isSelected); @@ -76,15 +132,56 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { return; } + // 일반계약인 경우 계약종류와 날짜 확인 + if (selectedContractType === "CONTRACT") { + if (!selectedGeneralContractType) { + toast.error("계약종류를 선택해주세요."); + return; + } + if (!contractStartDate) { + toast.error("계약 시작일을 선택해주세요."); + return; + } + if (!contractEndDate) { + toast.error("계약 종료일을 선택해주세요."); + return; + } + if (new Date(contractStartDate) >= new Date(contractEndDate)) { + toast.error("계약 종료일은 시작일보다 이후여야 합니다."); + return; + } + } + + // 입찰 검증 + if (selectedContractType === "BIDDING") { + if (!biddingContractType) { + toast.error("계약구분을 선택해주세요."); + return; + } + if (!biddingType) { + toast.error("입찰유형을 선택해주세요."); + return; + } + if (!biddingStartDate || !biddingEndDate) { + toast.error("입찰 기간을 입력해주세요."); + return; + } + if (new Date(biddingStartDate) >= new Date(biddingEndDate)) { + toast.error("입찰 마감일은 시작일보다 이후여야 합니다."); + return; + } + } + if (!selectedVendor) { toast.error("선정된 업체가 없습니다."); return; } setIsSubmitting(true); + try { let result; - + switch (selectedContractType) { case "PO": result = await createPO({ @@ -94,9 +191,10 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { totalAmount: selectedVendor.totalAmount, currency: selectedVendor.currency, selectionReason: selectedVendor.selectionReason, + }); break; - + case "CONTRACT": result = await createGeneralContract({ rfqId: data.rfqInfo.id, @@ -104,9 +202,13 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { vendorName: selectedVendor.vendorName, totalAmount: selectedVendor.totalAmount, currency: selectedVendor.currency, + contractStartDate: new Date(contractStartDate), + contractEndDate: new Date(contractEndDate), + contractType: selectedGeneralContractType, // 계약종류 추가 + }); break; - + case "BIDDING": result = await createBidding({ rfqId: data.rfqInfo.id, @@ -114,9 +216,14 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { vendorName: selectedVendor.vendorName, totalAmount: selectedVendor.totalAmount, currency: selectedVendor.currency, + contractType: biddingContractType, + biddingType: biddingType, + awardCount: awardCount, + biddingStartDate: new Date(biddingStartDate), + biddingEndDate: new Date(biddingEndDate), }); break; - + default: throw new Error("올바른 계약 유형이 아닙니다."); } @@ -124,8 +231,17 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { if (result.success) { toast.success(result.message || "계약 프로세스가 시작되었습니다."); setShowContractDialog(false); + // 모든 state 초기화 setSelectedContractType(""); - window.location.reload(); + setSelectedGeneralContractType(""); + setContractStartDate(""); + setContractEndDate(""); + setBiddingContractType(""); + setBiddingType(""); + setAwardCount("single"); + setBiddingStartDate(""); + setBiddingEndDate(""); + router.refresh(); } else { throw new Error(result.error || "계약 진행 중 오류가 발생했습니다."); } @@ -224,7 +340,7 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { toast.success("업체가 성공적으로 선정되었습니다."); setShowSelectionDialog(false); setSelectionReason(""); - window.location.reload(); // 페이지 새로고침으로 선정 상태 반영 + router.refresh() } else { throw new Error(result.error || "업체 선정 중 오류가 발생했습니다."); } @@ -246,13 +362,13 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { setIsSubmitting(true); try { // 파라미터를 올바르게 전달 - const result = await cancelVendorSelection(Number(data.rfqInfo.id),cancelReason); + const result = await cancelVendorSelection(Number(data.rfqInfo.id), cancelReason); if (result.success) { toast.success("업체 선정이 취소되었습니다."); setShowCancelDialog(false); setCancelReason(""); - window.location.reload(); + router.refresh() } else { throw new Error(result.error || "선정 취소 중 오류가 발생했습니다."); } @@ -312,13 +428,13 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { {hasSelection && (
@@ -335,11 +451,11 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { {hasContract ? "계약 진행중" - : isSelectionApproved - ? "업체 선정 승인 완료" - : isPendingApproval - ? "업체 선정 승인 대기중" - : "업체 선정 완료"} + : isSelectionApproved + ? "업체 선정 승인 완료" + : isPendingApproval + ? "업체 선정 승인 대기중" + : "업체 선정 완료"}

선정 업체: {selectedVendor.vendorName} ({selectedVendor.vendorCode})

@@ -521,13 +637,13 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { key={vendor.vendorId} className={cn( "flex items-center justify-between p-4 border rounded-lg transition-colors", - vendor.isSelected - ? "bg-blue-100 border-blue-400 border-2" + vendor.isSelected + ? "bg-blue-100 border-blue-400 border-2" : hasSelection - ? "opacity-60" - : selectedVendorId === vendor.vendorId.toString() - ? "bg-blue-50 border-blue-300 cursor-pointer" - : "hover:bg-gray-50 cursor-pointer" + ? "opacity-60" + : selectedVendorId === vendor.vendorId.toString() + ? "bg-blue-50 border-blue-300 cursor-pointer" + : "hover:bg-gray-50 cursor-pointer" )} onClick={() => { if (!hasSelection) { @@ -683,10 +799,10 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { - {vendor.vendorConditions.paymentTermsCode && - vendor.vendorConditions.paymentTermsCode !== vendor.buyerConditions.paymentTermsCode && ( - 변경 - )} + {vendor.vendorConditions.paymentTermsCode && + vendor.vendorConditions.paymentTermsCode !== vendor.buyerConditions.paymentTermsCode && ( + 변경 + )}
))} @@ -770,8 +886,8 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { {quote.deliveryDate ? format(new Date(quote.deliveryDate), "yyyy-MM-dd") : quote.leadTime - ? `${quote.leadTime}일` - : "-"} + ? `${quote.leadTime}일` + : "-"} {quote.manufacturer && ( @@ -817,7 +933,7 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) {

가격 차이

- {((item.priceAnalysis.highestPrice - item.priceAnalysis.lowestPrice) / + {((item.priceAnalysis.highestPrice - item.priceAnalysis.lowestPrice) / item.priceAnalysis.lowestPrice * 100).toFixed(1)}%

@@ -848,7 +964,7 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) {
{data.vendors.map((vendor) => { if (!vendor.conditionDifferences.hasDifferences) return null; - + return (

{vendor.vendorName}

@@ -930,12 +1046,12 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { // 가격 순위와 조건 차이를 고려한 점수 계산 const scoredVendors = data.vendors.map(v => ({ ...v, - score: (v.rank || 10) + v.conditionDifferences.criticalDifferences.length * 3 + - v.conditionDifferences.differences.length + score: (v.rank || 10) + v.conditionDifferences.criticalDifferences.length * 3 + + v.conditionDifferences.differences.length })); scoredVendors.sort((a, b) => a.score - b.score); const recommended = scoredVendors[0]; - + return (

@@ -963,7 +1079,7 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) {

{data.vendors.map((vendor) => { if (!vendor.generalRemark && !vendor.technicalProposal) return null; - + return (

{vendor.vendorName}

@@ -996,7 +1112,7 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) {

{hasSelection ? "업체 재선정 확인" : "업체 선정 확인"}

- + {selectedVendorId && (
@@ -1076,7 +1192,7 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) {

업체 선정 취소

- + @@ -1141,13 +1257,13 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { {/* 계약 진행 모달 */} {showContractDialog && (
-
+

{selectedContractType === "PO" && "PO (SAP) 생성"} {selectedContractType === "CONTRACT" && "일반계약 생성"} {selectedContractType === "BIDDING" && "입찰 생성"}

- + {selectedVendor && (
@@ -1180,32 +1296,222 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { - {/* 추가 옵션이 필요한 경우 여기에 추가 */} + {/* 일반계약 선택 시 계약종류 및 기간 선택 */} {selectedContractType === "CONTRACT" && ( -
-

계약 옵션

-
- -
)} + +
); } \ No newline at end of file -- cgit v1.2.3