From 459873f983cf1468f778109df4c7953c5d40743d Mon Sep 17 00:00:00 2001 From: dujinkim 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 @@ -72,16 +88,21 @@ export function AddVendorDialog({ const [hasCandidatesLoaded, setHasCandidatesLoaded] = useState(false) // 선택된 벤더들을 별도로 관리하여 검색과 독립적으로 유지 const [selectedVendorData, setSelectedVendorData] = useState([]) - const [activeTab, setActiveTab] = useState("candidates") + const [activeTab, setActiveTab] = useState("step1") + const [currentStep, setCurrentStep] = useState(1) const form = useForm({ 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({ ) + // 벤더 구분자 설정 UI + const renderVendorFlagsStep = () => ( +
+
+ 선택된 벤더들의 구분자를 설정해주세요. 각 벤더별로 여러 구분자를 선택할 수 있습니다. +
+ + {selectedVendorData.length > 0 ? ( +
+ {selectedVendorData.map((vendor) => ( + + + {vendor.vendorName} + + {vendor.vendorCode || 'N/A'} {vendor.country && `• ${vendor.country}`} + + + +
+
+ + handleVendorFlagChange(vendor.id, 'isCustomerPreferred', checked as boolean) + } + /> + +
+ +
+ + handleVendorFlagChange(vendor.id, 'isNewDiscovery', checked as boolean) + } + /> + +
+ +
+ + handleVendorFlagChange(vendor.id, 'isProjectApproved', checked as boolean) + } + /> + +
+ +
+ + handleVendorFlagChange(vendor.id, 'isShiProposal', checked as boolean) + } + /> + +
+
+
+
+ ))} +
+ ) : ( +
+ 선택된 벤더가 없습니다 +
+ )} +
+ ) + return ( - + {/* 헤더 */} 벤더 추가 @@ -313,102 +473,123 @@ export function AddVendorDialog({ + {/* 단계 표시 */} +
+
= 1 ? 'text-primary' : 'text-muted-foreground'}`}> +
= 1 ? 'bg-primary text-primary-foreground' : 'bg-muted' + }`}> + 1 +
+ 벤더 선택 +
+ +
= 2 ? 'text-primary' : 'text-muted-foreground'}`}> +
= 2 ? 'bg-primary text-primary-foreground' : 'bg-muted' + }`}> + 2 +
+ 구분자 설정 +
+
+ {/* 콘텐츠 */}
+ {/* 숨겨진 FormField - 폼 검증을 위해 필요 */} + <>} + /> + <>} + /> + {/* 탭 메뉴 */} - - 후보 벤더 ({candidateVendors.length}) + + 벤더 선택 ({selectedVendorData.length}개) - - 벤더 검색 + + 구분자 설정 - {/* 후보 벤더 탭 */} - -
-
- - -
- - {isLoadingCandidates ? ( -
-
- - 후보 벤더를 불러오는 중... -
+ {/* 1단계: 벤더 선택 탭 */} + +
+ {/* 후보 벤더 */} +
+
+ +
- ) : ( - renderVendorList(candidateVendors, true) - )} - -
- 💡 RFQ 아이템과 매칭되는 벤더들이 매칭 아이템 수가 많은 순으로 표시됩니다. -
-
- - - {/* 벤더 검색 탭 */} - - {/* 벤더 검색 필드 */} -
- -
- - setSearchTerm(e.target.value)} - className="pl-10" - /> - {isSearching && ( - + + {isLoadingCandidates ? ( +
+
+ + 후보 벤더를 불러오는 중... +
+
+ ) : ( + renderVendorList(candidateVendors, true) )} + +
+ 💡 RFQ 아이템과 매칭되는 벤더들이 매칭 아이템 수가 많은 순으로 표시됩니다. +
-
- {/* 검색 결과 */} - {hasSearched ? ( + {/* 벤더 검색 */}
-
- 검색 결과 ({searchResults.length}개) + +
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> + {isSearching && ( + + )}
- {renderVendorList(searchResults)}
- ) : ( -
- 벤더명 또는 벤더코드를 입력하여 검색해주세요 -
- )} - - - {/* 선택된 벤더 목록 - 하단에 항상 표시 */} - ( - + {/* 검색 결과 */} + {hasSearched && ( +
+
+ 검색 결과 ({searchResults.length}개) +
+ {renderVendorList(searchResults)} +
+ )} + + {/* 선택된 벤더 목록 */}
선택된 벤더 ({selectedVendorData.length}개)
@@ -435,17 +616,14 @@ export function AddVendorDialog({ )}
- -
- )} - /> +
+
- {/* 안내 메시지 -
-

• 후보 벤더는 RFQ 아이템 코드와 매칭되는 기술영업 벤더들입니다.

-

• 선택된 벤더들은 Draft 상태로 추가됩니다.

-

• 벤더별 견적 정보는 추가 후 개별적으로 입력할 수 있습니다.

-
*/} + {/* 2단계: 구분자 설정 탭 */} + + {renderVendorFlagsStep()} + +
@@ -460,13 +638,36 @@ export function AddVendorDialog({ > 취소 - + + {currentStep === 1 ? ( + + ) : ( +
+ + +
+ )}
-- cgit v1.2.3