diff options
Diffstat (limited to 'components/common/selectors')
| -rw-r--r-- | components/common/selectors/place-of-shipping/place-of-shipping-selector.tsx | 250 |
1 files changed, 249 insertions, 1 deletions
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({ </Select> </div> ) +} + +/** + * 선적지/하역지 단일 선택 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 + * <PlaceOfShippingSelectorDialogSingle + * triggerLabel="장소 선택" + * selectedPlace={selectedPlace} + * onPlaceSelect={(place) => { + * 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<PlaceOfShippingData | null>(null) + + // 장소 데이터 + const [placeOfShippingData, setPlaceOfShippingData] = useState<PlaceOfShippingData[]>([]) + 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 ( + <Dialog open={open} onOpenChange={handleOpenChange}> + <DialogTrigger asChild> + <Button variant={triggerVariant} disabled={disabled}> + {selectedPlace ? ( + <span className="truncate"> + {selectedPlace.code} - {selectedPlace.description} + </span> + ) : ( + triggerLabel + )} + </Button> + </DialogTrigger> + + <DialogContent className="max-w-4xl max-h-[80vh]"> + <DialogHeader> + <DialogTitle>{title}</DialogTitle> + <DialogDescription>{description}</DialogDescription> + </DialogHeader> + + <div className="space-y-4"> + <div className="flex items-center space-x-2"> + <Search className="h-4 w-4" /> + <Input + placeholder={placeholder} + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + className="flex-1" + /> + </div> + + {isLoading ? ( + <div className="flex justify-center py-8"> + <div className="text-sm text-muted-foreground">장소 데이터를 불러오는 중...</div> + </div> + ) : ( + <div className="border rounded-md max-h-96 overflow-auto"> + <Table> + <TableHeader> + <TableRow> + <TableHead className="w-12"></TableHead> + <TableHead>장소코드</TableHead> + <TableHead>장소명</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {filteredData.length === 0 ? ( + <TableRow> + <TableCell colSpan={3} className="text-center py-8 text-muted-foreground"> + {searchTerm ? "검색 결과가 없습니다" : "데이터가 없습니다"} + </TableCell> + </TableRow> + ) : ( + filteredData.map((item) => ( + <TableRow + key={item.code} + className={`cursor-pointer hover:bg-muted/50 ${ + tempSelectedPlace?.code === item.code ? "bg-muted" : "" + }`} + onClick={() => handlePlaceChange(item)} + > + <TableCell> + {tempSelectedPlace?.code === item.code && ( + <Check className="h-4 w-4 text-primary" /> + )} + </TableCell> + <TableCell className="font-mono">{item.code}</TableCell> + <TableCell>{item.description}</TableCell> + </TableRow> + )) + )} + </TableBody> + </Table> + </div> + )} + </div> + + <DialogFooter className="gap-2"> + <Button variant="outline" onClick={handleCancel}> + 취소 + </Button> + {tempSelectedPlace && ( + <Button variant="ghost" onClick={handleClear}> + 선택 해제 + </Button> + )} + <Button onClick={handleConfirm}> + 확인 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) }
\ No newline at end of file |
