diff options
Diffstat (limited to 'components/common/vendor/vendor-selector-dialog-single.tsx')
| -rw-r--r-- | components/common/vendor/vendor-selector-dialog-single.tsx | 215 |
1 files changed, 215 insertions, 0 deletions
diff --git a/components/common/vendor/vendor-selector-dialog-single.tsx b/components/common/vendor/vendor-selector-dialog-single.tsx new file mode 100644 index 00000000..da9a9a74 --- /dev/null +++ b/components/common/vendor/vendor-selector-dialog-single.tsx @@ -0,0 +1,215 @@ +"use client" + +import React, { useState, useCallback } from "react" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { VendorSelector } from "./vendor-selector" +import { VendorSearchItem } from "./vendor-service" + +/** + * 벤더 단일 선택 Dialog 컴포넌트 + * + * @description + * - VendorSelector를 Dialog로 래핑한 단일 선택 컴포넌트 + * - 버튼 클릭 시 Dialog가 열리고, 벤더를 선택하면 Dialog가 닫히며 결과를 반환 + * + * @VendorSearchItem_Structure + * 상태에서 관리되는 벤더 객체의 형태: + * ```typescript + * interface VendorSearchItem { + * id: number; // 벤더 ID + * vendorName: string; // 벤더명 + * vendorCode: string | null; // 벤더코드 (없을 수 있음) + * status: string; // 벤더 상태 (ACTIVE, PENDING_REVIEW 등) + * displayText: string; // 표시용 텍스트 (vendorName + vendorCode) + * } + * ``` + * + * @state + * - open: Dialog 열림/닫힘 상태 + * - selectedVendor: 현재 선택된 벤더 (단일) + * - tempSelectedVendor: Dialog 내에서 임시로 선택된 벤더 (확인 버튼 클릭 전까지) + * + * @callback + * - onVendorSelect: 벤더 선택 완료 시 호출되는 콜백 + * - 매개변수: VendorSearchItem | null + * - 선택된 벤더 정보 또는 null (선택 해제 시) + * + * @usage + * ```tsx + * <VendorSelectorDialogSingle + * triggerLabel="벤더 선택" + * selectedVendor={selectedVendor} + * onVendorSelect={(vendor) => { + * setSelectedVendor(vendor); + * console.log('선택된 벤더:', vendor); + * }} + * placeholder="벤더를 검색하세요..." + * /> + * ``` + */ + +interface VendorSelectorDialogSingleProps { + /** Dialog를 여는 트리거 버튼 텍스트 */ + triggerLabel?: string + /** 현재 선택된 벤더 */ + selectedVendor?: VendorSearchItem | null + /** 벤더 선택 완료 시 호출되는 콜백 */ + onVendorSelect?: (vendor: VendorSearchItem | null) => void + /** 검색 입력창 placeholder */ + placeholder?: string + /** Dialog 제목 */ + title?: string + /** Dialog 설명 */ + description?: string + /** 트리거 버튼 비활성화 여부 */ + disabled?: boolean + /** 트리거 버튼 variant */ + triggerVariant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" + /** 제외할 벤더 ID들 */ + excludeVendorIds?: Set<number> + /** 초기 데이터 표시 여부 */ + showInitialData?: boolean + /** 벤더 상태 필터 */ + statusFilter?: string +} + +export function VendorSelectorDialogSingle({ + triggerLabel = "벤더 선택", + selectedVendor = null, + onVendorSelect, + placeholder = "벤더를 검색하세요...", + title = "벤더 선택", + description = "원하는 벤더를 검색하고 선택해주세요.", + disabled = false, + triggerVariant = "outline", + excludeVendorIds, + showInitialData = true, + statusFilter +}: VendorSelectorDialogSingleProps) { + // Dialog 열림/닫힘 상태 + const [open, setOpen] = useState(false) + + // Dialog 내에서 임시로 선택된 벤더 (확인 버튼 클릭 전까지) + const [tempSelectedVendor, setTempSelectedVendor] = useState<VendorSearchItem | null>(null) + + // Dialog 열림 시 현재 선택된 벤더로 임시 선택 초기화 + const handleOpenChange = useCallback((newOpen: boolean) => { + setOpen(newOpen) + if (newOpen) { + setTempSelectedVendor(selectedVendor || null) + } + }, [selectedVendor]) + + // 벤더 선택 처리 (Dialog 내에서) + const handleVendorChange = useCallback((vendors: VendorSearchItem[]) => { + setTempSelectedVendor(vendors.length > 0 ? vendors[0] : null) + }, []) + + // 확인 버튼 클릭 시 선택 완료 + const handleConfirm = useCallback(() => { + onVendorSelect?.(tempSelectedVendor) + setOpen(false) + }, [tempSelectedVendor, onVendorSelect]) + + // 취소 버튼 클릭 시 + const handleCancel = useCallback(() => { + setTempSelectedVendor(selectedVendor || null) + setOpen(false) + }, [selectedVendor]) + + // 선택 해제 + const handleClear = useCallback(() => { + setTempSelectedVendor(null) + }, []) + + return ( + <Dialog open={open} onOpenChange={handleOpenChange}> + <DialogTrigger asChild> + <Button variant={triggerVariant} disabled={disabled}> + {selectedVendor ? ( + <span className="truncate"> + {selectedVendor.displayText} + </span> + ) : ( + triggerLabel + )} + </Button> + </DialogTrigger> + + <DialogContent className="max-w-md"> + <DialogHeader> + <DialogTitle>{title}</DialogTitle> + <DialogDescription>{description}</DialogDescription> + </DialogHeader> + + <div className="py-4"> + <VendorSelector + selectedVendors={tempSelectedVendor ? [tempSelectedVendor] : []} + onVendorsChange={handleVendorChange} + singleSelect={true} + placeholder={placeholder} + noValuePlaceHolder="벤더를 선택해주세요" + closeOnSelect={false} + excludeVendorIds={excludeVendorIds} + showInitialData={showInitialData} + statusFilter={statusFilter} + /> + </div> + + <DialogFooter className="gap-2"> + <Button variant="outline" onClick={handleCancel}> + 취소 + </Button> + {tempSelectedVendor && ( + <Button variant="ghost" onClick={handleClear}> + 선택 해제 + </Button> + )} + <Button onClick={handleConfirm}> + 확인 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +} + +/** + * 사용 예시: + * + * ```tsx + * const [selectedVendor, setSelectedVendor] = useState<VendorSearchItem | null>(null); + * + * return ( + * <VendorSelectorDialogSingle + * triggerLabel="벤더 선택" + * selectedVendor={selectedVendor} + * onVendorSelect={(vendor) => { + * setSelectedVendor(vendor); + * if (vendor) { + * console.log('선택된 벤더:', { + * id: vendor.id, + * name: vendor.vendorName, + * code: vendor.vendorCode, + * status: vendor.status + * }); + * } else { + * console.log('벤더 선택이 해제되었습니다.'); + * } + * }} + * title="벤더 선택" + * description="협력업체를 검색하고 선택해주세요." + * statusFilter="ACTIVE" // ACTIVE 상태의 벤더만 표시 + * /> + * ); + * ``` + */ |
