'use client' /** * 선적지/하역지 선택기 * * placeOfShipping 테이블 기준으로 선적지/하역지에 쓰이는 장소코드 및 장소명 선택 */ import { Select, SelectItem, SelectContent } from "@/components/ui/select" import { SelectTrigger } from "@/components/ui/select" import { SelectValue } from "@/components/ui/select" import { Input } from "@/components/ui/input" import { useState, useEffect, useMemo, useCallback } from "react" import { getPlaceOfShippingForSelection } from "./place-of-shipping-service" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { Search, Check } from "lucide-react" interface PlaceOfShippingData { code: string description: string } export interface PlaceOfShippingSelectorProps { value?: string onValueChange?: (value: string) => void placeholder?: string disabled?: boolean className?: string } export function PlaceOfShippingSelector({ value = "", onValueChange, placeholder = "선적지/하역지 선택", disabled = false, className }: PlaceOfShippingSelectorProps) { const [placeOfShippingData, setPlaceOfShippingData] = useState([]) const [isLoading, setIsLoading] = useState(true) const [searchTerm, setSearchTerm] = useState("") const filteredData = useMemo(() => { if (!searchTerm) return placeOfShippingData return placeOfShippingData.filter(item => item.code.toLowerCase().includes(searchTerm.toLowerCase()) || item.description.toLowerCase().includes(searchTerm.toLowerCase()) ) }, [placeOfShippingData, searchTerm]) useEffect(() => { const loadData = async () => { try { const data = await getPlaceOfShippingForSelection() setPlaceOfShippingData(data) } catch (error) { console.error('선적지/하역지 데이터 로드 실패:', error) setPlaceOfShippingData([]) } finally { setIsLoading(false) } } loadData() }, []) return (
setSearchTerm(e.target.value)} className="w-full" />
) } /** * 선적지/하역지 단일 선택 Dialog 컴포넌트 * * @description * - PlaceOfShippingSelector를 Dialog로 래핑한 단일 선택 컴포넌트 * - 버튼 클릭 시 Dialog가 열리고, 장소를 선택하면 Dialog가 닫히며 결과를 반환 * * @PlaceOfShippingData_Structure * 선택된 장소 객체의 형태: * ```typescript * interface PlaceOfShippingData { * code: string; // 장소코드 * description: string; // 장소명 * } * ``` * * @state * - open: Dialog 열림/닷힘 상태 * - selectedPlace: 현재 선택된 장소 (단일) * - tempSelectedPlace: Dialog 내에서 임시로 선택된 장소 (확인 버튼 클릭 전까지) * * @callback * - onPlaceSelect: 장소 선택 완료 시 호출되는 콜백 * - 매개변수: PlaceOfShippingData | null * - 선택된 장소 정보 또는 null (선택 해제 시) * * @usage * ```tsx * { * setSelectedPlace(place); * console.log('선택된 장소:', place); * }} * placeholder="장소를 검색하세요..." * /> * ``` */ interface PlaceOfShippingSelectorDialogSingleProps { /** Dialog를 여는 트리거 버튼 텍스트 */ triggerLabel?: string /** 현재 선택된 장소 */ selectedPlace?: PlaceOfShippingData | null /** 장소 선택 완료 시 호출되는 콜백 */ onPlaceSelect?: (place: PlaceOfShippingData | null) => void /** 검색 입력창 placeholder */ placeholder?: string /** Dialog 제목 */ title?: string /** Dialog 설명 */ description?: string /** 트리거 버튼 비활성화 여부 */ disabled?: boolean /** 트리거 버튼 variant */ triggerVariant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" } export function PlaceOfShippingSelectorDialogSingle({ triggerLabel = "장소 선택", selectedPlace = null, onPlaceSelect, placeholder = "장소를 검색하세요...", title = "장소 선택", description = "원하는 장소를 검색하고 선택해주세요.", disabled = false, triggerVariant = "outline", }: PlaceOfShippingSelectorDialogSingleProps) { // Dialog 열림/닫힘 상태 const [open, setOpen] = useState(false) // Dialog 내에서 임시로 선택된 장소 (확인 버튼 클릭 전까지) const [tempSelectedPlace, setTempSelectedPlace] = useState(null) // 장소 데이터 const [placeOfShippingData, setPlaceOfShippingData] = useState([]) const [isLoading, setIsLoading] = useState(true) const [searchTerm, setSearchTerm] = useState("") const filteredData = useMemo(() => { if (!searchTerm) return placeOfShippingData return placeOfShippingData.filter(item => item.code.toLowerCase().includes(searchTerm.toLowerCase()) || item.description.toLowerCase().includes(searchTerm.toLowerCase()) ) }, [placeOfShippingData, searchTerm]) // Dialog 열림 시 현재 선택된 장소로 임시 선택 초기화 const handleOpenChange = useCallback((newOpen: boolean) => { setOpen(newOpen) if (newOpen) { setTempSelectedPlace(selectedPlace || null) } }, [selectedPlace]) // 장소 선택 처리 (Dialog 내에서) const handlePlaceChange = useCallback((place: PlaceOfShippingData) => { setTempSelectedPlace(place) }, []) // 확인 버튼 클릭 시 선택 완료 const handleConfirm = useCallback(() => { onPlaceSelect?.(tempSelectedPlace) setOpen(false) }, [tempSelectedPlace, onPlaceSelect]) // 취소 버튼 클릭 시 const handleCancel = useCallback(() => { setTempSelectedPlace(selectedPlace || null) setOpen(false) }, [selectedPlace]) // 선택 해제 const handleClear = useCallback(() => { setTempSelectedPlace(null) }, []) useEffect(() => { if (open && placeOfShippingData.length === 0) { const loadData = async () => { setIsLoading(true) try { const data = await getPlaceOfShippingForSelection() setPlaceOfShippingData(data) } catch (error) { console.error('선적지/하역지 데이터 로드 실패:', error) setPlaceOfShippingData([]) } finally { setIsLoading(false) } } loadData() } }, [open, placeOfShippingData.length]) return ( {title} {description}
setSearchTerm(e.target.value)} className="flex-1" />
{isLoading ? (
장소 데이터를 불러오는 중...
) : (
장소코드 장소명 {filteredData.length === 0 ? ( {searchTerm ? "검색 결과가 없습니다" : "데이터가 없습니다"} ) : ( filteredData.map((item) => ( handlePlaceChange(item)} > {tempSelectedPlace?.code === item.code && ( )} {item.code} {item.description} )) )}
)}
{tempSelectedPlace && ( )}
) }