From 14f61e24947fb92dd71ec0a7196a6e815f8e66da Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 21 Jul 2025 07:54:26 +0000 Subject: (최겸)기술영업 RFQ 담당자 초대, 요구사항 반영 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../signup/tech-vendor-item-selector-dialog.tsx | 254 +++++++++++++++++++++ components/signup/tech-vendor-join-form.tsx | 69 +++++- components/tech-vendors/tech-vendor-container.tsx | 30 +-- .../tech-vendors/tech-vendor-items-container.tsx | 121 ---------- 4 files changed, 326 insertions(+), 148 deletions(-) create mode 100644 components/signup/tech-vendor-item-selector-dialog.tsx delete mode 100644 components/tech-vendors/tech-vendor-items-container.tsx (limited to 'components') diff --git a/components/signup/tech-vendor-item-selector-dialog.tsx b/components/signup/tech-vendor-item-selector-dialog.tsx new file mode 100644 index 00000000..a69dec5d --- /dev/null +++ b/components/signup/tech-vendor-item-selector-dialog.tsx @@ -0,0 +1,254 @@ +"use client" + +import * as React from "react" +import { useState, useEffect } from "react" +import { Search, X } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Badge } from "@/components/ui/badge" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Checkbox } from "@/components/ui/checkbox" + +interface Item { + itemCode: string + itemList: string + subItemList?: string + workType?: string + shipTypes?: string +} + +interface TechVendorItemSelectorDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + vendorType: string | string[] + onItemsSelected: (selectedItems: string[]) => void +} + +export function TechVendorItemSelectorDialog({ + open, + onOpenChange, + vendorType, + onItemsSelected, +}: TechVendorItemSelectorDialogProps) { + const [items, setItems] = useState([]) + const [filteredItems, setFilteredItems] = useState([]) + const [searchTerm, setSearchTerm] = useState("") + const [selectedItems, setSelectedItems] = useState>(new Set()) + const [isLoading, setIsLoading] = useState(false) + + // 벤더 타입에 따른 아이템 조회 + useEffect(() => { + if (open && vendorType) { + loadItemsByVendorType() + } + }, [open, vendorType]) + + // 검색 필터링 + useEffect(() => { + if (searchTerm.trim() === "") { + setFilteredItems(items) + } else { + const filtered = items.filter( + (item) => + item.itemList.toLowerCase().includes(searchTerm.toLowerCase()) || + (item.subItemList && item.subItemList.toLowerCase().includes(searchTerm.toLowerCase())) || + item.itemCode.toLowerCase().includes(searchTerm.toLowerCase()) + ) + setFilteredItems(filtered) + } + }, [searchTerm, items]) + + const loadItemsByVendorType = async () => { + setIsLoading(true) + try { + // 서버 액션으로 아이템 조회 + const { getItemsByVendorType } = await import("@/lib/tech-vendors/service") + + let allItems: any[] = [] + + // 여러 벤더 타입인 경우 각각 조회하여 합치기 + if (Array.isArray(vendorType)) { + for (const type of vendorType) { + const result = await getItemsByVendorType(type, "") + if (result && result.data && result.data.length > 0) { + allItems = [...allItems, ...result.data] + } + } + } else { + // 단일 벤더 타입인 경우 + const result = await getItemsByVendorType(vendorType, "") + if (result && result.data && result.data.length > 0) { + allItems = result.data + } + } + + if (allItems.length > 0) { + // 중복 제거 (itemCode 기준) + const uniqueItems = allItems.filter((item, index, self) => + index === self.findIndex(t => t.itemCode === item.itemCode) + ) + + const itemsData = uniqueItems.map((item: any) => ({ + itemCode: item.itemCode || "", + itemList: item.itemList || "", + subItemList: item.subItemList || "", + workType: item.workType || "", + shipTypes: item.shipTypes || "", + })) + setItems(itemsData) + setFilteredItems(itemsData) + } else { + setItems([]) + setFilteredItems([]) + } + } catch (error) { + console.error("아이템 조회 실패:", error) + setItems([]) + setFilteredItems([]) + } finally { + setIsLoading(false) + } + } + + const handleItemToggle = (itemCode: string) => { + const newSelected = new Set(selectedItems) + if (newSelected.has(itemCode)) { + newSelected.delete(itemCode) + } else { + newSelected.add(itemCode) + } + setSelectedItems(newSelected) + } + + const handleConfirm = () => { + const selectedItemCodes = Array.from(selectedItems) + onItemsSelected(selectedItemCodes) + onOpenChange(false) + // 상태 초기화 + setSelectedItems(new Set()) + setSearchTerm("") + } + + const handleCancel = () => { + onOpenChange(false) + // 상태 초기화 + setSelectedItems(new Set()) + setSearchTerm("") + } + + return ( + + + + 공급가능품목 선택 + + {Array.isArray(vendorType) ? vendorType.join(", ") : vendorType} 관련 아이템 중에서 공급 가능한 품목을 선택해주세요. + + + +
+ {/* 검색바 */} +
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+ + {/* 선택된 아이템 표시 */} + {selectedItems.size > 0 && ( +
+
선택된 아이템 ({selectedItems.size}개)
+
+ {Array.from(selectedItems).map((itemCode) => { + const item = items.find((i) => i.itemCode === itemCode) + return ( + + {item?.itemList || itemCode} + + + ) + })} +
+
+ )} + + {/* 아이템 목록 */} +
+ + {isLoading ? ( +
로딩 중...
+ ) : filteredItems.length === 0 ? ( +
+ {searchTerm ? "검색 결과가 없습니다." : "아이템이 없습니다."} +
+ ) : ( +
+ {filteredItems.map((item) => ( +
+ handleItemToggle(item.itemCode)} + className="mt-1" + /> +
+
+ {item.itemList} + + {item.itemCode} + +
+ {item.subItemList && ( +
+ {item.subItemList} +
+ )} +
+ {item.workType && ( + 공종: {item.workType} + )} + {item.shipTypes && ( + 선종: {item.shipTypes} + )} +
+
+
+ ))} +
+ )} +
+
+
+ + + + + +
+
+ ) +} \ No newline at end of file diff --git a/components/signup/tech-vendor-join-form.tsx b/components/signup/tech-vendor-join-form.tsx index db81b88c..efdee322 100644 --- a/components/signup/tech-vendor-join-form.tsx +++ b/components/signup/tech-vendor-join-form.tsx @@ -38,6 +38,7 @@ import { Check, ChevronsUpDown, Loader2, Plus, X } from "lucide-react" import { cn } from "@/lib/utils" import { createTechVendorFromSignup } from "@/lib/tech-vendors/service" +import { TechVendorItemSelectorDialog } from "./tech-vendor-item-selector-dialog" import { createTechVendorSchema, CreateTechVendorSchema } from "@/lib/tech-vendors/validations" import { VENDOR_TYPES } from "@/db/schema/techVendors" import { verifyTechVendorInvitationToken } from "@/lib/tech-vendor-invitation-token" @@ -144,6 +145,8 @@ export function TechVendorJoinForm() { const [isSubmitting, setIsSubmitting] = React.useState(false) const [isLoading, setIsLoading] = React.useState(false) const [hasValidToken, setHasValidToken] = React.useState(null) + const [isItemSelectorOpen, setIsItemSelectorOpen] = React.useState(false) + const [selectedItemCodes, setSelectedItemCodes] = React.useState([]) // React Hook Form (항상 최상위에서 호출) const form = useForm({ @@ -158,7 +161,7 @@ export function TechVendorJoinForm() { phone: "", country: "", website: "", - techVendorType: ["조선"], + techVendorType: [], representativeName: "", representativeBirth: "", representativeEmail: "", @@ -200,13 +203,15 @@ export function TechVendorJoinForm() { if (tokenPayload) { setHasValidToken(true); + console.log("tokenPayload", tokenPayload); // 토큰에서 가져온 정보로 폼 미리 채우기 form.setValue("vendorName", tokenPayload.vendorName); form.setValue("email", tokenPayload.email); + form.setValue("techVendorType", tokenPayload.vendorType as "조선" | "해양TOP" | "해양HULL" | ("조선" | "해양TOP" | "해양HULL")[]); - // 연락처 정보도 미리 채우기 - form.setValue("contacts.0.contactName", tokenPayload.vendorName); - form.setValue("contacts.0.contactEmail", tokenPayload.email); + // // 연락처 정보도 미리 채우기 + // form.setValue("contacts.0.contactName", tokenPayload.vendorName); + // form.setValue("contacts.0.contactEmail", tokenPayload.email); toast({ title: "초대 정보 로드 완료", @@ -292,6 +297,13 @@ export function TechVendorJoinForm() { form.setValue("files", updated, { shouldValidate: true }) } + const handleItemsSelected = (itemCodes: string[]) => { + setSelectedItemCodes(itemCodes) + // 선택된 아이템 코드들을 콤마로 구분하여 items 필드에 설정 + const itemsString = itemCodes.join(", ") + form.setValue("items", itemsString) + } + // Submit async function onSubmit(values: CreateTechVendorSchema) { setIsSubmitting(true) @@ -310,7 +322,7 @@ export function TechVendorJoinForm() { email: values.email, phone: values.phone, country: values.country, - techVendorType: Array.isArray(values.techVendorType) ? values.techVendorType[0] : values.techVendorType, + techVendorType: values.techVendorType as "조선" | "해양TOP" | "해양HULL" | ("조선" | "해양TOP" | "해양HULL")[], representativeName: values.representativeName || "", representativeBirth: values.representativeBirth || "", representativeEmail: values.representativeEmail || "", @@ -323,6 +335,7 @@ export function TechVendorJoinForm() { vendorData: techVendorData, files: mainFiles, contacts: values.contacts, + selectedItemCodes: selectedItemCodes, invitationToken: invitationToken || undefined, }) @@ -413,7 +426,7 @@ export function TechVendorJoinForm() { id={`techVendorType-${type}`} checked={field.value?.includes(type) || false} onChange={(e) => { - const currentValues = field.value || []; + const currentValues = Array.isArray(field.value) ? field.value : []; if (e.target.checked) { field.onChange([...currentValues, type]); } else { @@ -557,7 +570,7 @@ export function TechVendorJoinForm() { )} /> - {/* 이메일 */} + {/* 이메일 (수정 불가, 뷰 전용) */} - + @@ -616,11 +635,29 @@ export function TechVendorJoinForm() { 주요 품목 - - - +
+ + + +
+ + {selectedItemCodes.length > 0 && ( + + {selectedItemCodes.length}개 아이템 선택됨 + + )} +
+
- 회사에서 주로 다루는 품목들을 쉼표로 구분하여 입력하세요. + 공급가능품목 선택 버튼을 클릭하여 아이템을 선택하세요. 원하는 아이템이 없다면 텍스트로 입력하세요. @@ -902,6 +939,14 @@ export function TechVendorJoinForm() { + + {/* 공급가능품목 선택 다이얼로그 */} + ) } \ No newline at end of file diff --git a/components/tech-vendors/tech-vendor-container.tsx b/components/tech-vendors/tech-vendor-container.tsx index af5169b8..94536702 100644 --- a/components/tech-vendors/tech-vendor-container.tsx +++ b/components/tech-vendors/tech-vendor-container.tsx @@ -40,20 +40,20 @@ export function TechVendorContainer({ // URL에서 현재 선택된 벤더 타입 가져오기 const vendorType = searchParams.get("vendorType") || "all" - // 선택한 벤더 타입에 해당하는 이름 찾기 - const selectedVendor = vendorTypes.find((vendor) => vendor.id === vendorType)?.name || "전체" + // // 선택한 벤더 타입에 해당하는 이름 찾기 + // const selectedVendor = vendorTypes.find((vendor) => vendor.id === vendorType)?.name || "전체" - // 벤더 타입 변경 핸들러 - const handleVendorTypeChange = React.useCallback((value: string) => { - const params = new URLSearchParams(searchParams.toString()) - if (value === "all") { - params.delete("vendorType") - } else { - params.set("vendorType", value) - } + // // 벤더 타입 변경 핸들러 + // const handleVendorTypeChange = React.useCallback((value: string) => { + // const params = new URLSearchParams(searchParams.toString()) + // if (value === "all") { + // params.delete("vendorType") + // } else { + // params.set("vendorType", value) + // } - router.push(`${pathname}?${params.toString()}`) - }, [router, pathname, searchParams]) + // router.push(`${pathname}?${params.toString()}`) + // }, [router, pathname, searchParams]) return ( <> @@ -62,7 +62,7 @@ export function TechVendorContainer({ {/* 왼쪽: 타이틀 & 설명 */}
-

기술영업 벤더 관리

+

기술영업 벤더 리스트

{/*

@@ -70,7 +70,7 @@ export function TechVendorContainer({

*/}
- {/* 오른쪽: 벤더 타입 드롭다운 */} + {/* 오른쪽: 벤더 타입 드롭다운 - - - {itemTypes.map((item) => ( - handleItemTypeChange(item.id)} - className={item.id === itemType ? "bg-muted" : ""} - > - {item.name} - - ))} - - - )} - - - {/* 컨텐츠 영역 */} -
-
- {selectedItemType && ( - - )} -
-
- - ) -} \ No newline at end of file -- cgit v1.2.3