From 459873f983cf1468f778109df4c7953c5d40743d Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 4 Aug 2025 09:40:21 +0000
Subject: (최겸) 기술영업 요구사항 반영
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../table/detail-table/add-vendor-dialog.tsx       | 403 +++++++++++++++------
 1 file changed, 302 insertions(+), 101 deletions(-)

(limited to 'lib/techsales-rfq/table/detail-table/add-vendor-dialog.tsx')

diff --git a/lib/techsales-rfq/table/detail-table/add-vendor-dialog.tsx b/lib/techsales-rfq/table/detail-table/add-vendor-dialog.tsx
index 69953217..ea982407 100644
--- a/lib/techsales-rfq/table/detail-table/add-vendor-dialog.tsx
+++ b/lib/techsales-rfq/table/detail-table/add-vendor-dialog.tsx
@@ -6,21 +6,37 @@ import { useForm } from "react-hook-form"
 import { zodResolver } from "@hookform/resolvers/zod"
 import { z } from "zod"
 import { toast } from "sonner"
-import { Check, X, Search, Loader2, Star } from "lucide-react"
+import { Check, X, Search, Loader2, Star, ChevronRight, ChevronLeft } from "lucide-react"
 import { useSession } from "next-auth/react"
 
 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
 import { Button } from "@/components/ui/button"
-import { Form, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
+import { Form, FormField, FormLabel } from "@/components/ui/form"
 import { Input } from "@/components/ui/input"
 import { ScrollArea } from "@/components/ui/scroll-area"
 import { Badge } from "@/components/ui/badge"
 import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
+import { Checkbox } from "@/components/ui/checkbox"
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
 import { addTechVendorsToTechSalesRfq, getTechSalesRfqCandidateVendors, searchTechVendors } from "@/lib/techsales-rfq/service"
 
-// 폼 유효성 검증 스키마 - 간단화
+// 벤더 구분자 타입 정의
+type VendorFlags = {
+  isCustomerPreferred?: boolean;      // 고객(선주) 선호 벤더
+  isNewDiscovery?: boolean;           // 신규 발굴 벤더
+  isProjectApproved?: boolean;        // Project Approved Vendor
+  isShiProposal?: boolean;           // SHI Proposal Vendor
+}
+
+// 폼 유효성 검증 스키마 - 벤더 선택 + 구분자 정보
 const vendorFormSchema = z.object({
   vendorIds: z.array(z.number()).min(1, "최소 하나의 벤더를 선택해주세요"),
+  vendorFlags: z.record(z.string(), z.object({
+    isCustomerPreferred: z.boolean().optional(),
+    isNewDiscovery: z.boolean().optional(),
+    isProjectApproved: z.boolean().optional(),
+    isShiProposal: z.boolean().optional(),
+  })),
 })
 
 type VendorFormValues = z.infer<typeof vendorFormSchema>
@@ -72,16 +88,21 @@ export function AddVendorDialog({
   const [hasCandidatesLoaded, setHasCandidatesLoaded] = useState(false)
   // 선택된 벤더들을 별도로 관리하여 검색과 독립적으로 유지
   const [selectedVendorData, setSelectedVendorData] = useState<VendorSearchResult[]>([])
-  const [activeTab, setActiveTab] = useState("candidates")
+  const [activeTab, setActiveTab] = useState("step1")
+  const [currentStep, setCurrentStep] = useState(1)
 
   const form = useForm<VendorFormValues>({
     resolver: zodResolver(vendorFormSchema),
     defaultValues: {
       vendorIds: [],
+      vendorFlags: {},
     },
   })
 
   const selectedVendorIds = form.watch("vendorIds")
+  const vendorFlags = form.watch("vendorFlags")
+
+
 
   // 후보 벤더 로드 함수
   const loadCandidateVendors = useCallback(async () => {
@@ -166,6 +187,11 @@ export function AddVendorDialog({
       const newSelectedData = selectedVendorData.filter(v => v.id !== vendor.id)
       form.setValue("vendorIds", newIds, { shouldValidate: true })
       setSelectedVendorData(newSelectedData)
+      
+      // 구분자 정보도 제거
+      const currentFlags = form.getValues("vendorFlags")
+      const { [vendor.id.toString()]: _, ...newFlags } = currentFlags
+      form.setValue("vendorFlags", newFlags)
     } else {
       // 선택 추가
       const newIds = [...currentIds, vendor.id]
@@ -182,6 +208,41 @@ export function AddVendorDialog({
     const newSelectedData = selectedVendorData.filter(v => v.id !== vendorId)
     form.setValue("vendorIds", newIds, { shouldValidate: true })
     setSelectedVendorData(newSelectedData)
+    
+    // 구분자 정보도 제거
+    const currentFlags = form.getValues("vendorFlags")
+    const { [vendorId.toString()]: _, ...newFlags } = currentFlags
+    form.setValue("vendorFlags", newFlags)
+  }
+
+  // 벤더 구분자 변경 핸들러
+  const handleVendorFlagChange = (vendorId: number, flagKey: keyof VendorFlags, checked: boolean) => {
+    const currentFlags = form.getValues("vendorFlags")
+    const vendorFlags = currentFlags[vendorId.toString()] || {}
+    
+    form.setValue("vendorFlags", {
+      ...currentFlags,
+      [vendorId.toString()]: {
+        ...vendorFlags,
+        [flagKey]: checked,
+      }
+    })
+  }
+
+  // 다음 단계로 이동
+  const handleNextStep = () => {
+    if (selectedVendorIds.length === 0) {
+      toast.error("최소 하나의 벤더를 선택해주세요")
+      return
+    }
+    setCurrentStep(2)
+    setActiveTab("step2")
+  }
+
+  // 이전 단계로 이동
+  const handlePrevStep = () => {
+    setCurrentStep(1)
+    setActiveTab("step1")
   }
 
   // 폼 제출 핸들러
@@ -199,10 +260,11 @@ export function AddVendorDialog({
     try {
       setIsSubmitting(true)
 
-      // 새로운 서비스 함수 호출
+      // 새로운 서비스 함수 호출 (구분자 정보 포함)
       const result = await addTechVendorsToTechSalesRfq({
         rfqId: selectedRfq.id,
         vendorIds: values.vendorIds,
+        vendorFlags: values.vendorFlags, // 구분자 정보 추가
         createdBy: Number(session.user.id),
       })
 
@@ -220,6 +282,8 @@ export function AddVendorDialog({
         setHasSearched(false)
         setHasCandidatesLoaded(false)
         setSelectedVendorData([])
+        setCurrentStep(1)
+        setActiveTab("step1")
         onSuccess?.()
       }
     } catch (error) {
@@ -240,7 +304,8 @@ export function AddVendorDialog({
       setHasSearched(false)
       setHasCandidatesLoaded(false)
       setSelectedVendorData([])
-      setActiveTab("candidates")
+      setCurrentStep(1)
+      setActiveTab("step1")
     }
   }, [open, form])
 
@@ -296,9 +361,104 @@ export function AddVendorDialog({
     </ScrollArea>
   )
 
+  // 벤더 구분자 설정 UI
+  const renderVendorFlagsStep = () => (
+    <div className="space-y-4">
+      <div className="text-sm text-muted-foreground">
+        선택된 벤더들의 구분자를 설정해주세요. 각 벤더별로 여러 구분자를 선택할 수 있습니다.
+      </div>
+      
+      {selectedVendorData.length > 0 ? (
+        <div className="space-y-4">
+          {selectedVendorData.map((vendor) => (
+            <Card key={vendor.id}>
+              <CardHeader className="pb-3">
+                <CardTitle className="text-base">{vendor.vendorName}</CardTitle>
+                <CardDescription>
+                  {vendor.vendorCode || 'N/A'} {vendor.country && `• ${vendor.country}`}
+                </CardDescription>
+              </CardHeader>
+              <CardContent className="space-y-3">
+                <div className="grid grid-cols-2 gap-3">
+                  <div className="flex items-center space-x-2">
+                                         <Checkbox
+                       id={`customer-preferred-${vendor.id}`}
+                       checked={vendorFlags[vendor.id.toString()]?.isCustomerPreferred || false}
+                       onCheckedChange={(checked) => 
+                         handleVendorFlagChange(vendor.id, 'isCustomerPreferred', checked as boolean)
+                       }
+                     />
+                    <label 
+                      htmlFor={`customer-preferred-${vendor.id}`}
+                      className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+                    >
+                      고객(선주) 선호 벤더
+                    </label>
+                  </div>
+                  
+                                     <div className="flex items-center space-x-2">
+                     <Checkbox
+                       id={`new-discovery-${vendor.id}`}
+                       checked={vendorFlags[vendor.id.toString()]?.isNewDiscovery || false}
+                       onCheckedChange={(checked) => 
+                         handleVendorFlagChange(vendor.id, 'isNewDiscovery', checked as boolean)
+                       }
+                     />
+                    <label 
+                      htmlFor={`new-discovery-${vendor.id}`}
+                      className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+                    >
+                      신규 발굴 벤더
+                    </label>
+                  </div>
+                  
+                                     <div className="flex items-center space-x-2">
+                     <Checkbox
+                       id={`project-approved-${vendor.id}`}
+                       checked={vendorFlags[vendor.id.toString()]?.isProjectApproved || false}
+                       onCheckedChange={(checked) => 
+                         handleVendorFlagChange(vendor.id, 'isProjectApproved', checked as boolean)
+                       }
+                     />
+                    <label 
+                      htmlFor={`project-approved-${vendor.id}`}
+                      className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+                    >
+                      Project Approved Vendor
+                    </label>
+                  </div>
+                  
+                                     <div className="flex items-center space-x-2">
+                     <Checkbox
+                       id={`shi-proposal-${vendor.id}`}
+                       checked={vendorFlags[vendor.id.toString()]?.isShiProposal || false}
+                       onCheckedChange={(checked) => 
+                         handleVendorFlagChange(vendor.id, 'isShiProposal', checked as boolean)
+                       }
+                     />
+                    <label 
+                      htmlFor={`shi-proposal-${vendor.id}`}
+                      className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+                    >
+                      SHI Proposal Vendor
+                    </label>
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+          ))}
+        </div>
+      ) : (
+        <div className="text-center py-8 text-muted-foreground border rounded-md">
+          선택된 벤더가 없습니다
+        </div>
+      )}
+    </div>
+  )
+
   return (
     <Dialog open={open} onOpenChange={onOpenChange}>
-      <DialogContent className="sm:max-w-[800px] max-h-[80vh] flex flex-col">
+      <DialogContent className="sm:max-w-[900px] max-h-[80vh] flex flex-col">
         {/* 헤더 */}
         <DialogHeader>
           <DialogTitle>벤더 추가</DialogTitle>
@@ -313,102 +473,123 @@ export function AddVendorDialog({
           </DialogDescription>
         </DialogHeader>
 
+        {/* 단계 표시 */}
+        <div className="flex items-center justify-center space-x-4 mb-4">
+          <div className={`flex items-center space-x-2 ${currentStep >= 1 ? 'text-primary' : 'text-muted-foreground'}`}>
+            <div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${
+              currentStep >= 1 ? 'bg-primary text-primary-foreground' : 'bg-muted'
+            }`}>
+              1
+            </div>
+            <span className="text-sm">벤더 선택</span>
+          </div>
+          <ChevronRight className="h-4 w-4 text-muted-foreground" />
+          <div className={`flex items-center space-x-2 ${currentStep >= 2 ? 'text-primary' : 'text-muted-foreground'}`}>
+            <div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${
+              currentStep >= 2 ? 'bg-primary text-primary-foreground' : 'bg-muted'
+            }`}>
+              2
+            </div>
+            <span className="text-sm">구분자 설정</span>
+          </div>
+        </div>
+
         {/* 콘텐츠 */}
         <div className="flex-1 overflow-y-auto">
           <Form {...form}>
             <form id="vendor-form" onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
+              {/* 숨겨진 FormField - 폼 검증을 위해 필요 */}
+              <FormField
+                control={form.control}
+                name="vendorIds"
+                render={() => <></>}
+              />
+              <FormField
+                control={form.control}
+                name="vendorFlags"
+                render={() => <></>}
+              />
+
               {/* 탭 메뉴 */}
               <Tabs value={activeTab} onValueChange={setActiveTab}>
                 <TabsList className="grid w-full grid-cols-2">
-                  <TabsTrigger value="candidates">
-                    후보 벤더 ({candidateVendors.length})
+                  <TabsTrigger value="step1" disabled={currentStep === 2}>
+                    벤더 선택 ({selectedVendorData.length}개)
                   </TabsTrigger>
-                  <TabsTrigger value="search">
-                    벤더 검색
+                  <TabsTrigger value="step2" disabled={currentStep === 1}>
+                    구분자 설정
                   </TabsTrigger>
                 </TabsList>
 
-                {/* 후보 벤더 탭 */}
-                <TabsContent value="candidates" className="space-y-4">
-                  <div className="space-y-2">
-                    <div className="flex items-center justify-between">
-                      <label className="text-sm font-medium">추천 후보 벤더</label>
-                      <Button
-                        type="button"
-                        variant="outline"
-                        size="sm"
-                        onClick={() => {
-                          setHasCandidatesLoaded(false)
-                          loadCandidateVendors()
-                        }}
-                        disabled={isLoadingCandidates}
-                      >
-                        {isLoadingCandidates ? (
-                          <Loader2 className="h-4 w-4 animate-spin" />
-                        ) : (
-                          "새로고침"
-                        )}
-                      </Button>
-                    </div>
-                    
-                    {isLoadingCandidates ? (
-                      <div className="h-60 border rounded-md flex items-center justify-center">
-                        <div className="flex items-center gap-2">
-                          <Loader2 className="h-4 w-4 animate-spin" />
-                          <span>후보 벤더를 불러오는 중...</span>
-                        </div>
+                {/* 1단계: 벤더 선택 탭 */}
+                <TabsContent value="step1" className="space-y-4">
+                  <div className="space-y-4">
+                    {/* 후보 벤더 */}
+                    <div className="space-y-2">
+                      <div className="flex items-center justify-between">
+                        <label className="text-sm font-medium">추천 후보 벤더</label>
+                        <Button
+                          type="button"
+                          variant="outline"
+                          size="sm"
+                          onClick={() => {
+                            setHasCandidatesLoaded(false)
+                            loadCandidateVendors()
+                          }}
+                          disabled={isLoadingCandidates}
+                        >
+                          {isLoadingCandidates ? (
+                            <Loader2 className="h-4 w-4 animate-spin" />
+                          ) : (
+                            "새로고침"
+                          )}
+                        </Button>
                       </div>
-                    ) : (
-                      renderVendorList(candidateVendors, true)
-                    )}
-                    
-                    <div className="text-xs text-muted-foreground bg-blue-50 p-2 rounded">
-                      💡 RFQ 아이템과 매칭되는 벤더들이 매칭 아이템 수가 많은 순으로 표시됩니다.
-                    </div>
-                  </div>
-                </TabsContent>
-
-                {/* 벤더 검색 탭 */}
-                <TabsContent value="search" className="space-y-4">
-                  {/* 벤더 검색 필드 */}
-                  <div className="space-y-2">
-                    <label className="text-sm font-medium">벤더 검색</label>
-                    <div className="relative">
-                      <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
-                      <Input
-                        placeholder="벤더명 또는 벤더코드로 검색..."
-                        value={searchTerm}
-                        onChange={(e) => setSearchTerm(e.target.value)}
-                        className="pl-10"
-                      />
-                      {isSearching && (
-                        <Loader2 className="absolute right-3 top-1/2 transform -translate-y-1/2 h-4 w-4 animate-spin text-muted-foreground" />
+                      
+                      {isLoadingCandidates ? (
+                        <div className="h-60 border rounded-md flex items-center justify-center">
+                          <div className="flex items-center gap-2">
+                            <Loader2 className="h-4 w-4 animate-spin" />
+                            <span>후보 벤더를 불러오는 중...</span>
+                          </div>
+                        </div>
+                      ) : (
+                        renderVendorList(candidateVendors, true)
                       )}
+                      
+                      <div className="text-xs text-muted-foreground bg-blue-50 p-2 rounded">
+                        💡 RFQ 아이템과 매칭되는 벤더들이 매칭 아이템 수가 많은 순으로 표시됩니다.
+                      </div>
                     </div>
-                  </div>
 
-                  {/* 검색 결과 */}
-                  {hasSearched ? (
+                    {/* 벤더 검색 */}
                     <div className="space-y-2">
-                      <div className="text-sm font-medium">
-                        검색 결과 ({searchResults.length}개)
+                      <label className="text-sm font-medium">벤더 검색</label>
+                      <div className="relative">
+                        <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
+                        <Input
+                          placeholder="벤더명 또는 벤더코드로 검색..."
+                          value={searchTerm}
+                          onChange={(e) => setSearchTerm(e.target.value)}
+                          className="pl-10"
+                        />
+                        {isSearching && (
+                          <Loader2 className="absolute right-3 top-1/2 transform -translate-y-1/2 h-4 w-4 animate-spin text-muted-foreground" />
+                        )}
                       </div>
-                      {renderVendorList(searchResults)}
                     </div>
-                  ) : (
-                    <div className="text-center py-8 text-muted-foreground border rounded-md">
-                      벤더명 또는 벤더코드를 입력하여 검색해주세요
-                    </div>
-                  )}
-                </TabsContent>
-              </Tabs>
 
-              {/* 선택된 벤더 목록 - 하단에 항상 표시 */}
-              <FormField
-                control={form.control}
-                name="vendorIds"
-                render={() => (
-                  <FormItem>
+                    {/* 검색 결과 */}
+                    {hasSearched && (
+                      <div className="space-y-2">
+                        <div className="text-sm font-medium">
+                          검색 결과 ({searchResults.length}개)
+                        </div>
+                        {renderVendorList(searchResults)}
+                      </div>
+                    )}
+
+                    {/* 선택된 벤더 목록 */}
                     <div className="space-y-2">
                       <FormLabel>선택된 벤더 ({selectedVendorData.length}개)</FormLabel>
                       <div className="min-h-[60px] p-3 border rounded-md bg-muted/50">
@@ -435,17 +616,14 @@ export function AddVendorDialog({
                         )}
                       </div>
                     </div>
-                    <FormMessage />
-                  </FormItem>
-                )}
-              />
+                  </div>
+                </TabsContent>
 
-              {/* 안내 메시지
-              <div className="text-sm text-muted-foreground bg-muted p-3 rounded-md">
-                <p>• 후보 벤더는 RFQ 아이템 코드와 매칭되는 기술영업 벤더들입니다.</p>
-                <p>• 선택된 벤더들은 Draft 상태로 추가됩니다.</p>
-                <p>• 벤더별 견적 정보는 추가 후 개별적으로 입력할 수 있습니다.</p>
-              </div> */}
+                {/* 2단계: 구분자 설정 탭 */}
+                <TabsContent value="step2" className="space-y-4">
+                  {renderVendorFlagsStep()}
+                </TabsContent>
+              </Tabs>
             </form>
           </Form>
         </div>
@@ -460,13 +638,36 @@ export function AddVendorDialog({
           >
             취소
           </Button>
-          <Button
-            type="submit"
-            form="vendor-form"
-            disabled={isSubmitting || selectedVendorIds.length === 0}
-          >
-            {isSubmitting ? "처리 중..." : `${selectedVendorIds.length}개 벤더 추가`}
-          </Button>
+          
+          {currentStep === 1 ? (
+            <Button
+              type="button"
+              onClick={handleNextStep}
+              disabled={selectedVendorIds.length === 0}
+            >
+              다음 단계
+              <ChevronRight className="ml-2 h-4 w-4" />
+            </Button>
+          ) : (
+            <div className="flex space-x-2">
+              <Button
+                type="button"
+                variant="outline"
+                onClick={handlePrevStep}
+                disabled={isSubmitting}
+              >
+                <ChevronLeft className="mr-2 h-4 w-4" />
+                이전 단계
+              </Button>
+              <Button
+                type="submit"
+                form="vendor-form"
+                disabled={isSubmitting || selectedVendorIds.length === 0}
+              >
+                {isSubmitting ? "처리 중..." : `${selectedVendorIds.length}개 벤더 추가`}
+              </Button>
+            </div>
+          )}
         </DialogFooter>
       </DialogContent>
     </Dialog>
-- 
cgit v1.2.3

