summaryrefslogtreecommitdiff
path: root/lib/rfq-last/quotation-compare-view.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-15 14:41:01 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-15 14:41:01 +0000
commit4ee8b24cfadf47452807fa2af801385ed60ab47c (patch)
treee1d1fb029f0cf5519c517494bf9a545505c35700 /lib/rfq-last/quotation-compare-view.tsx
parent265859d691a01cdcaaf9154f93c38765bc34df06 (diff)
(대표님) 작업사항 - rfqLast, tbeLast, pdfTron, userAuth
Diffstat (limited to 'lib/rfq-last/quotation-compare-view.tsx')
-rw-r--r--lib/rfq-last/quotation-compare-view.tsx755
1 files changed, 755 insertions, 0 deletions
diff --git a/lib/rfq-last/quotation-compare-view.tsx b/lib/rfq-last/quotation-compare-view.tsx
new file mode 100644
index 00000000..0e15a7bf
--- /dev/null
+++ b/lib/rfq-last/quotation-compare-view.tsx
@@ -0,0 +1,755 @@
+"use client";
+
+import * as React from "react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ Trophy,
+ TrendingUp,
+ TrendingDown,
+ AlertCircle,
+ CheckCircle,
+ XCircle,
+ ChevronDown,
+ ChevronUp,
+ Info,
+ DollarSign,
+ Calendar,
+ Package,
+ Globe,
+ FileText,
+ Truck,
+ AlertTriangle,
+} from "lucide-react";
+import { cn } from "@/lib/utils";
+import { format } from "date-fns";
+import { ko } from "date-fns/locale";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/ui/collapsible";
+import type { ComparisonData, VendorComparison, PrItemComparison } from "../actions";
+
+interface QuotationCompareViewProps {
+ data: ComparisonData;
+}
+
+export function QuotationCompareView({ data }: QuotationCompareViewProps) {
+ const [expandedItems, setExpandedItems] = React.useState<Set<number>>(new Set());
+ const [selectedMetric, setSelectedMetric] = React.useState<"price" | "delivery" | "compliance">("price");
+
+ // 아이템 확장/축소 토글
+ const toggleItemExpansion = (itemId: number) => {
+ setExpandedItems((prev) => {
+ const newSet = new Set(prev);
+ if (newSet.has(itemId)) {
+ newSet.delete(itemId);
+ } else {
+ newSet.add(itemId);
+ }
+ return newSet;
+ });
+ };
+
+ // 순위에 따른 색상
+ const getRankColor = (rank: number) => {
+ switch (rank) {
+ case 1:
+ return "text-green-600 bg-green-50";
+ case 2:
+ return "text-blue-600 bg-blue-50";
+ case 3:
+ return "text-orange-600 bg-orange-50";
+ default:
+ return "text-gray-600 bg-gray-50";
+ }
+ };
+
+ // 가격 차이 색상
+ const getVarianceColor = (variance: number) => {
+ if (variance < -5) return "text-green-600";
+ if (variance > 5) return "text-red-600";
+ return "text-gray-600";
+ };
+
+ // 조건 일치 여부 아이콘
+ const getComplianceIcon = (matches: boolean) => {
+ return matches ? (
+ <CheckCircle className="h-4 w-4 text-green-500" />
+ ) : (
+ <XCircle className="h-4 w-4 text-red-500" />
+ );
+ };
+
+ // 금액 포맷
+ const formatAmount = (amount: number, currency: string = "USD") => {
+ return new Intl.NumberFormat("ko-KR", {
+ style: "currency",
+ currency: currency,
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 2,
+ }).format(amount);
+ };
+
+ return (
+ <div className="space-y-6">
+ {/* 요약 카드 */}
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
+ {/* 최저가 벤더 */}
+ <Card>
+ <CardHeader className="pb-3">
+ <CardTitle className="text-sm font-medium flex items-center gap-2">
+ <Trophy className="h-4 w-4 text-yellow-500" />
+ 최저가 벤더
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <p className="text-lg font-bold">{data.summary.lowestBidder}</p>
+ <p className="text-sm text-muted-foreground">
+ {formatAmount(data.summary.priceRange.min, data.summary.currency)}
+ </p>
+ </CardContent>
+ </Card>
+
+ {/* 평균 가격 */}
+ <Card>
+ <CardHeader className="pb-3">
+ <CardTitle className="text-sm font-medium flex items-center gap-2">
+ <DollarSign className="h-4 w-4" />
+ 평균 가격
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <p className="text-lg font-bold">
+ {formatAmount(data.summary.priceRange.average, data.summary.currency)}
+ </p>
+ <p className="text-sm text-muted-foreground">
+ {data.vendors.length}개 업체 평균
+ </p>
+ </CardContent>
+ </Card>
+
+ {/* 가격 범위 */}
+ <Card>
+ <CardHeader className="pb-3">
+ <CardTitle className="text-sm font-medium flex items-center gap-2">
+ <TrendingUp className="h-4 w-4" />
+ 가격 범위
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <p className="text-lg font-bold">
+ {((data.summary.priceRange.max - data.summary.priceRange.min) / data.summary.priceRange.min * 100).toFixed(1)}%
+ </p>
+ <p className="text-sm text-muted-foreground">
+ 최저가 대비 최고가 차이
+ </p>
+ </CardContent>
+ </Card>
+
+ {/* 조건 불일치 */}
+ <Card>
+ <CardHeader className="pb-3">
+ <CardTitle className="text-sm font-medium flex items-center gap-2">
+ <AlertCircle className="h-4 w-4 text-orange-500" />
+ 조건 불일치
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <p className="text-lg font-bold">
+ {data.vendors.filter(v => v.conditionDifferences.hasDifferences).length}개
+ </p>
+ <p className="text-sm text-muted-foreground">
+ 제시 조건과 차이 있음
+ </p>
+ </CardContent>
+ </Card>
+ </div>
+
+ {/* 탭 뷰 */}
+ <Tabs defaultValue="overview" className="w-full">
+ <TabsList className="grid w-full grid-cols-4">
+ <TabsTrigger value="overview">종합 비교</TabsTrigger>
+ <TabsTrigger value="conditions">조건 비교</TabsTrigger>
+ <TabsTrigger value="items">아이템별 비교</TabsTrigger>
+ <TabsTrigger value="analysis">상세 분석</TabsTrigger>
+ </TabsList>
+
+ {/* 종합 비교 */}
+ <TabsContent value="overview" className="space-y-4">
+ <Card>
+ <CardHeader>
+ <CardTitle>가격 순위</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-4">
+ {data.vendors.map((vendor) => (
+ <div
+ key={vendor.vendorId}
+ className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50 transition-colors"
+ >
+ <div className="flex items-center gap-4">
+ <div
+ className={cn(
+ "w-10 h-10 rounded-full flex items-center justify-center font-bold",
+ getRankColor(vendor.rank || 0)
+ )}
+ >
+ {vendor.rank}
+ </div>
+ <div>
+ <p className="font-semibold">{vendor.vendorName}</p>
+ <p className="text-sm text-muted-foreground">
+ {vendor.vendorCode} • {vendor.vendorCountry}
+ </p>
+ </div>
+ </div>
+
+ <div className="flex items-center gap-6">
+ {/* 조건 차이 표시 */}
+ {vendor.conditionDifferences.criticalDifferences.length > 0 && (
+ <TooltipProvider>
+ <Tooltip>
+ <TooltipTrigger>
+ <Badge variant="destructive" className="gap-1">
+ <AlertTriangle className="h-3 w-3" />
+ 중요 차이 {vendor.conditionDifferences.criticalDifferences.length}
+ </Badge>
+ </TooltipTrigger>
+ <TooltipContent>
+ <div className="space-y-1">
+ {vendor.conditionDifferences.criticalDifferences.map((diff, idx) => (
+ <p key={idx} className="text-xs">{diff}</p>
+ ))}
+ </div>
+ </TooltipContent>
+ </Tooltip>
+ </TooltipProvider>
+ )}
+
+ {/* 가격 정보 */}
+ <div className="text-right">
+ <p className="text-lg font-bold">
+ {formatAmount(vendor.totalAmount, vendor.currency)}
+ </p>
+ <p className={cn("text-sm", getVarianceColor(vendor.priceVariance || 0))}>
+ {vendor.priceVariance && vendor.priceVariance > 0 ? "+" : ""}
+ {vendor.priceVariance?.toFixed(1)}% vs 평균
+ </p>
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ </CardContent>
+ </Card>
+ </TabsContent>
+
+ {/* 조건 비교 */}
+ <TabsContent value="conditions" className="space-y-4">
+ <Card>
+ <CardHeader>
+ <CardTitle>거래 조건 비교</CardTitle>
+ </CardHeader>
+ <CardContent className="overflow-x-auto">
+ <table className="w-full">
+ <thead>
+ <tr className="border-b">
+ <th className="text-left p-2">항목</th>
+ <th className="text-left p-2">구매자 제시</th>
+ {data.vendors.map((vendor) => (
+ <th key={vendor.vendorId} className="text-left p-2">
+ {vendor.vendorName}
+ </th>
+ ))}
+ </tr>
+ </thead>
+ <tbody className="divide-y">
+ {/* 통화 */}
+ <tr>
+ <td className="p-2 font-medium">통화</td>
+ <td className="p-2">{data.vendors[0]?.buyerConditions.currency}</td>
+ {data.vendors.map((vendor) => (
+ <td key={vendor.vendorId} className="p-2">
+ <div className="flex items-center gap-2">
+ {vendor.vendorConditions.currency || vendor.buyerConditions.currency}
+ {vendor.vendorConditions.currency !== vendor.buyerConditions.currency && (
+ <Badge variant="outline" className="text-xs">변경</Badge>
+ )}
+ </div>
+ </td>
+ ))}
+ </tr>
+
+ {/* 지급조건 */}
+ <tr>
+ <td className="p-2 font-medium">지급조건</td>
+ <td className="p-2">
+ <TooltipProvider>
+ <Tooltip>
+ <TooltipTrigger>
+ {data.vendors[0]?.buyerConditions.paymentTermsCode}
+ </TooltipTrigger>
+ <TooltipContent>
+ {data.vendors[0]?.buyerConditions.paymentTermsDesc}
+ </TooltipContent>
+ </Tooltip>
+ </TooltipProvider>
+ </td>
+ {data.vendors.map((vendor) => (
+ <td key={vendor.vendorId} className="p-2">
+ <div className="flex items-center gap-2">
+ <TooltipProvider>
+ <Tooltip>
+ <TooltipTrigger>
+ {vendor.vendorConditions.paymentTermsCode || vendor.buyerConditions.paymentTermsCode}
+ </TooltipTrigger>
+ <TooltipContent>
+ {vendor.vendorConditions.paymentTermsDesc || vendor.buyerConditions.paymentTermsDesc}
+ </TooltipContent>
+ </Tooltip>
+ </TooltipProvider>
+ {vendor.vendorConditions.paymentTermsCode &&
+ vendor.vendorConditions.paymentTermsCode !== vendor.buyerConditions.paymentTermsCode && (
+ <Badge variant="outline" className="text-xs">변경</Badge>
+ )}
+ </div>
+ </td>
+ ))}
+ </tr>
+
+ {/* 인코텀즈 */}
+ <tr>
+ <td className="p-2 font-medium">인코텀즈</td>
+ <td className="p-2">{data.vendors[0]?.buyerConditions.incotermsCode}</td>
+ {data.vendors.map((vendor) => (
+ <td key={vendor.vendorId} className="p-2">
+ <div className="flex items-center gap-2">
+ {vendor.vendorConditions.incotermsCode || vendor.buyerConditions.incotermsCode}
+ {vendor.vendorConditions.incotermsCode !== vendor.buyerConditions.incotermsCode && (
+ <Badge variant="outline" className="text-xs">변경</Badge>
+ )}
+ </div>
+ </td>
+ ))}
+ </tr>
+
+ {/* 납기 */}
+ <tr>
+ <td className="p-2 font-medium">납기</td>
+ <td className="p-2">
+ {data.vendors[0]?.buyerConditions.deliveryDate
+ ? format(new Date(data.vendors[0].buyerConditions.deliveryDate), "yyyy-MM-dd")
+ : "-"}
+ </td>
+ {data.vendors.map((vendor) => {
+ const vendorDate = vendor.vendorConditions.deliveryDate || vendor.buyerConditions.deliveryDate;
+ const isDelayed = vendorDate && vendor.buyerConditions.deliveryDate &&
+ new Date(vendorDate) > new Date(vendor.buyerConditions.deliveryDate);
+
+ return (
+ <td key={vendor.vendorId} className="p-2">
+ <div className="flex items-center gap-2">
+ {vendorDate ? format(new Date(vendorDate), "yyyy-MM-dd") : "-"}
+ {isDelayed && (
+ <Badge variant="destructive" className="text-xs">지연</Badge>
+ )}
+ </div>
+ </td>
+ );
+ })}
+ </tr>
+
+ {/* 초도품 */}
+ <tr>
+ <td className="p-2 font-medium">초도품</td>
+ <td className="p-2">
+ {data.vendors[0]?.buyerConditions.firstYn ? "요구" : "해당없음"}
+ </td>
+ {data.vendors.map((vendor) => (
+ <td key={vendor.vendorId} className="p-2">
+ {vendor.buyerConditions.firstYn && (
+ <Badge
+ variant={
+ vendor.vendorConditions.firstAcceptance === "수용"
+ ? "default"
+ : vendor.vendorConditions.firstAcceptance === "부분수용"
+ ? "secondary"
+ : vendor.vendorConditions.firstAcceptance === "거부"
+ ? "destructive"
+ : "outline"
+ }
+ >
+ {vendor.vendorConditions.firstAcceptance || "미응답"}
+ </Badge>
+ )}
+ {!vendor.buyerConditions.firstYn && "-"}
+ </td>
+ ))}
+ </tr>
+
+ {/* 스페어파트 */}
+ <tr>
+ <td className="p-2 font-medium">스페어파트</td>
+ <td className="p-2">
+ {data.vendors[0]?.buyerConditions.sparepartYn ? "요구" : "해당없음"}
+ </td>
+ {data.vendors.map((vendor) => (
+ <td key={vendor.vendorId} className="p-2">
+ {vendor.buyerConditions.sparepartYn && (
+ <Badge
+ variant={
+ vendor.vendorConditions.sparepartAcceptance === "수용"
+ ? "default"
+ : vendor.vendorConditions.sparepartAcceptance === "부분수용"
+ ? "secondary"
+ : vendor.vendorConditions.sparepartAcceptance === "거부"
+ ? "destructive"
+ : "outline"
+ }
+ >
+ {vendor.vendorConditions.sparepartAcceptance || "미응답"}
+ </Badge>
+ )}
+ {!vendor.buyerConditions.sparepartYn && "-"}
+ </td>
+ ))}
+ </tr>
+
+ {/* 연동제 */}
+ <tr>
+ <td className="p-2 font-medium">연동제</td>
+ <td className="p-2">
+ {data.vendors[0]?.buyerConditions.materialPriceRelatedYn ? "적용" : "미적용"}
+ </td>
+ {data.vendors.map((vendor) => (
+ <td key={vendor.vendorId} className="p-2">
+ <div className="flex items-center gap-2">
+ {vendor.vendorConditions.materialPriceRelatedYn !== undefined
+ ? vendor.vendorConditions.materialPriceRelatedYn ? "적용" : "미적용"
+ : vendor.buyerConditions.materialPriceRelatedYn ? "적용" : "미적용"}
+ {vendor.vendorConditions.materialPriceRelatedReason && (
+ <TooltipProvider>
+ <Tooltip>
+ <TooltipTrigger>
+ <Info className="h-3 w-3" />
+ </TooltipTrigger>
+ <TooltipContent>
+ <p className="max-w-xs text-xs">
+ {vendor.vendorConditions.materialPriceRelatedReason}
+ </p>
+ </TooltipContent>
+ </Tooltip>
+ </TooltipProvider>
+ )}
+ </div>
+ </td>
+ ))}
+ </tr>
+ </tbody>
+ </table>
+ </CardContent>
+ </Card>
+ </TabsContent>
+
+ {/* 아이템별 비교 */}
+ <TabsContent value="items" className="space-y-4">
+ <Card>
+ <CardHeader>
+ <CardTitle>PR 아이템별 가격 비교</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-2">
+ {data.prItems.map((item) => (
+ <Collapsible
+ key={item.prItemId}
+ open={expandedItems.has(item.prItemId)}
+ onOpenChange={() => toggleItemExpansion(item.prItemId)}
+ >
+ <div className="border rounded-lg">
+ <CollapsibleTrigger className="w-full p-4 hover:bg-gray-50 transition-colors">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-4 text-left">
+ <div className="flex items-center gap-2">
+ {expandedItems.has(item.prItemId) ? (
+ <ChevronUp className="h-4 w-4" />
+ ) : (
+ <ChevronDown className="h-4 w-4" />
+ )}
+ <Package className="h-4 w-4 text-muted-foreground" />
+ </div>
+ <div>
+ <p className="font-medium">{item.materialDescription}</p>
+ <p className="text-sm text-muted-foreground">
+ {item.materialCode} • {item.prNo} • {item.requestedQuantity} {item.uom}
+ </p>
+ </div>
+ </div>
+ <div className="text-right">
+ <p className="text-sm text-muted-foreground">단가 범위</p>
+ <p className="font-semibold">
+ {formatAmount(item.priceAnalysis.lowestPrice)} ~ {formatAmount(item.priceAnalysis.highestPrice)}
+ </p>
+ </div>
+ </div>
+ </CollapsibleTrigger>
+
+ <CollapsibleContent>
+ <div className="p-4 pt-0">
+ <table className="w-full">
+ <thead>
+ <tr className="border-b text-sm">
+ <th className="text-left p-2">벤더</th>
+ <th className="text-right p-2">단가</th>
+ <th className="text-right p-2">총액</th>
+ <th className="text-right p-2">수량</th>
+ <th className="text-left p-2">납기</th>
+ <th className="text-left p-2">제조사</th>
+ <th className="text-center p-2">순위</th>
+ </tr>
+ </thead>
+ <tbody className="divide-y">
+ {item.vendorQuotes.map((quote) => (
+ <tr key={quote.vendorId} className="text-sm">
+ <td className="p-2 font-medium">{quote.vendorName}</td>
+ <td className="p-2 text-right">
+ {formatAmount(quote.unitPrice, quote.currency)}
+ </td>
+ <td className="p-2 text-right">
+ {formatAmount(quote.totalPrice, quote.currency)}
+ </td>
+ <td className="p-2 text-right">{quote.quotedQuantity}</td>
+ <td className="p-2">
+ {quote.deliveryDate
+ ? format(new Date(quote.deliveryDate), "yyyy-MM-dd")
+ : quote.leadTime
+ ? `${quote.leadTime}일`
+ : "-"}
+ </td>
+ <td className="p-2">
+ {quote.manufacturer && (
+ <div>
+ <p>{quote.manufacturer}</p>
+ {quote.modelNo && (
+ <p className="text-xs text-muted-foreground">{quote.modelNo}</p>
+ )}
+ </div>
+ )}
+ </td>
+ <td className="p-2 text-center">
+ <Badge className={cn("", getRankColor(quote.priceRank || 0))}>
+ #{quote.priceRank}
+ </Badge>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+
+ {/* 가격 분석 요약 */}
+ <div className="mt-4 p-3 bg-gray-50 rounded-lg">
+ <div className="grid grid-cols-4 gap-4 text-sm">
+ <div>
+ <p className="text-muted-foreground">평균 단가</p>
+ <p className="font-semibold">
+ {formatAmount(item.priceAnalysis.averagePrice)}
+ </p>
+ </div>
+ <div>
+ <p className="text-muted-foreground">가격 편차</p>
+ <p className="font-semibold">
+ ±{formatAmount(item.priceAnalysis.priceVariance)}
+ </p>
+ </div>
+ <div>
+ <p className="text-muted-foreground">최저가 업체</p>
+ <p className="font-semibold">
+ {item.vendorQuotes.find(q => q.priceRank === 1)?.vendorName}
+ </p>
+ </div>
+ <div>
+ <p className="text-muted-foreground">가격 차이</p>
+ <p className="font-semibold">
+ {((item.priceAnalysis.highestPrice - item.priceAnalysis.lowestPrice) /
+ item.priceAnalysis.lowestPrice * 100).toFixed(1)}%
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </CollapsibleContent>
+ </div>
+ </Collapsible>
+ ))}
+ </div>
+ </CardContent>
+ </Card>
+ </TabsContent>
+
+ {/* 상세 분석 */}
+ <TabsContent value="analysis" className="space-y-4">
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ {/* 위험 요소 분석 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <AlertTriangle className="h-5 w-5 text-orange-500" />
+ 위험 요소 분석
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-3">
+ {data.vendors.map((vendor) => {
+ if (!vendor.conditionDifferences.hasDifferences) return null;
+
+ return (
+ <div key={vendor.vendorId} className="p-3 border rounded-lg">
+ <p className="font-medium mb-2">{vendor.vendorName}</p>
+ {vendor.conditionDifferences.criticalDifferences.length > 0 && (
+ <div className="space-y-1 mb-2">
+ <p className="text-xs font-medium text-red-600">중요 차이점:</p>
+ {vendor.conditionDifferences.criticalDifferences.map((diff, idx) => (
+ <p key={idx} className="text-xs text-red-600 pl-2">• {diff}</p>
+ ))}
+ </div>
+ )}
+ {vendor.conditionDifferences.differences.length > 0 && (
+ <div className="space-y-1">
+ <p className="text-xs font-medium text-orange-600">일반 차이점:</p>
+ {vendor.conditionDifferences.differences.map((diff, idx) => (
+ <p key={idx} className="text-xs text-orange-600 pl-2">• {diff}</p>
+ ))}
+ </div>
+ )}
+ </div>
+ );
+ })}
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 추천 사항 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <Info className="h-5 w-5 text-blue-500" />
+ 선정 추천
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-4">
+ {/* 가격 기준 추천 */}
+ <div className="p-3 bg-green-50 border border-green-200 rounded-lg">
+ <p className="font-medium text-green-800 mb-1">가격 우선 선정</p>
+ <p className="text-sm text-green-700">
+ {data.vendors[0]?.vendorName} - {formatAmount(data.vendors[0]?.totalAmount || 0)}
+ </p>
+ {data.vendors[0]?.conditionDifferences.hasDifferences && (
+ <p className="text-xs text-orange-600 mt-1">
+ ⚠️ 조건 차이 검토 필요
+ </p>
+ )}
+ </div>
+
+ {/* 조건 준수 기준 추천 */}
+ <div className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
+ <p className="font-medium text-blue-800 mb-1">조건 준수 우선 선정</p>
+ {(() => {
+ const compliantVendor = data.vendors.find(v => !v.conditionDifferences.hasDifferences);
+ if (compliantVendor) {
+ return (
+ <div>
+ <p className="text-sm text-blue-700">
+ {compliantVendor.vendorName} - {formatAmount(compliantVendor.totalAmount)}
+ </p>
+ <p className="text-xs text-blue-600 mt-1">
+ 모든 조건 충족 (가격 순위: #{compliantVendor.rank})
+ </p>
+ </div>
+ );
+ }
+ return (
+ <p className="text-sm text-blue-700">
+ 모든 조건을 충족하는 벤더 없음
+ </p>
+ );
+ })()}
+ </div>
+
+ {/* 균형 추천 */}
+ <div className="p-3 bg-purple-50 border border-purple-200 rounded-lg">
+ <p className="font-medium text-purple-800 mb-1">균형 선정 (추천)</p>
+ {(() => {
+ // 가격 순위와 조건 차이를 고려한 점수 계산
+ const scoredVendors = data.vendors.map(v => ({
+ ...v,
+ 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 (
+ <div>
+ <p className="text-sm text-purple-700">
+ {recommended.vendorName} - {formatAmount(recommended.totalAmount)}
+ </p>
+ <p className="text-xs text-purple-600 mt-1">
+ 가격 순위 #{recommended.rank}, 조건 차이 최소화
+ </p>
+ </div>
+ );
+ })()}
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+
+ {/* 벤더별 비고사항 */}
+ {data.vendors.some(v => v.generalRemark || v.technicalProposal) && (
+ <Card>
+ <CardHeader>
+ <CardTitle>벤더 제안사항 및 비고</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-4">
+ {data.vendors.map((vendor) => {
+ if (!vendor.generalRemark && !vendor.technicalProposal) return null;
+
+ return (
+ <div key={vendor.vendorId} className="border rounded-lg p-4">
+ <p className="font-medium mb-2">{vendor.vendorName}</p>
+ {vendor.generalRemark && (
+ <div className="mb-2">
+ <p className="text-sm font-medium text-muted-foreground">일반 비고:</p>
+ <p className="text-sm">{vendor.generalRemark}</p>
+ </div>
+ )}
+ {vendor.technicalProposal && (
+ <div>
+ <p className="text-sm font-medium text-muted-foreground">기술 제안:</p>
+ <p className="text-sm">{vendor.technicalProposal}</p>
+ </div>
+ )}
+ </div>
+ );
+ })}
+ </div>
+ </CardContent>
+ </Card>
+ )}
+ </TabsContent>
+ </Tabs>
+ </div>
+ );
+} \ No newline at end of file