From bf2db28586569499e44b58999f2e0f33ed4cdeb5 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Wed, 24 Sep 2025 17:36:08 +0900 Subject: (김준회) 구매 요청사항 반영 - vendor-pool 및 avl detail (이진용 프로) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../place-of-shipping-selector.tsx | 250 ++++++++++++++++++++- 1 file changed, 249 insertions(+), 1 deletion(-) (limited to 'components/common/selectors') diff --git a/components/common/selectors/place-of-shipping/place-of-shipping-selector.tsx b/components/common/selectors/place-of-shipping/place-of-shipping-selector.tsx index 63532365..0a9916cd 100644 --- a/components/common/selectors/place-of-shipping/place-of-shipping-selector.tsx +++ b/components/common/selectors/place-of-shipping/place-of-shipping-selector.tsx @@ -13,8 +13,27 @@ 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 } from "react" +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 @@ -96,4 +115,233 @@ export function PlaceOfShippingSelector({ ) +} + +/** + * 선적지/하역지 단일 선택 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(() => { + const loadData = async () => { + try { + const data = await getPlaceOfShippingForSelection() + setPlaceOfShippingData(data) + } catch (error) { + console.error('선적지/하역지 데이터 로드 실패:', error) + setPlaceOfShippingData([]) + } finally { + setIsLoading(false) + } + } + + loadData() + }, []) + + 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 && ( + + )} + + +
+
+ ) } \ No newline at end of file -- cgit v1.2.3