summaryrefslogtreecommitdiff
path: root/components/common/vendor/vendor-selector-dialog-multi.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/common/vendor/vendor-selector-dialog-multi.tsx')
-rw-r--r--components/common/vendor/vendor-selector-dialog-multi.tsx320
1 files changed, 320 insertions, 0 deletions
diff --git a/components/common/vendor/vendor-selector-dialog-multi.tsx b/components/common/vendor/vendor-selector-dialog-multi.tsx
new file mode 100644
index 00000000..32c8fa54
--- /dev/null
+++ b/components/common/vendor/vendor-selector-dialog-multi.tsx
@@ -0,0 +1,320 @@
+"use client"
+
+import React, { useState, useCallback } from "react"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import { VendorSelector } from "./vendor-selector"
+import { VendorSearchItem } from "./vendor-service"
+import { X } from "lucide-react"
+
+/**
+ * 벤더 다중 선택 Dialog 컴포넌트
+ *
+ * @description
+ * - VendorSelector를 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 열림/닫힘 상태
+ * - selectedVendors: 현재 선택된 벤더들 (배열)
+ * - tempSelectedVendors: Dialog 내에서 임시로 선택된 벤더들 (확인 버튼 클릭 전까지)
+ *
+ * @callback
+ * - onVendorsSelect: 벤더 선택 완료 시 호출되는 콜백
+ * - 매개변수: VendorSearchItem[]
+ * - 선택된 벤더들의 배열 (빈 배열일 수도 있음)
+ *
+ * @usage
+ * ```tsx
+ * <VendorSelectorDialogMulti
+ * triggerLabel="벤더 선택 (다중)"
+ * selectedVendors={selectedVendors}
+ * onVendorsSelect={(vendors) => {
+ * setSelectedVendors(vendors);
+ * console.log('선택된 벤더들:', vendors);
+ * }}
+ * maxSelections={5}
+ * placeholder="벤더를 검색하세요..."
+ * />
+ * ```
+ */
+
+interface VendorSelectorDialogMultiProps {
+ /** Dialog를 여는 트리거 버튼 텍스트 */
+ triggerLabel?: string
+ /** 현재 선택된 벤더들 */
+ selectedVendors?: VendorSearchItem[]
+ /** 벤더 선택 완료 시 호출되는 콜백 */
+ onVendorsSelect?: (vendors: VendorSearchItem[]) => void
+ /** 최대 선택 가능한 벤더 개수 */
+ maxSelections?: number
+ /** 검색 입력창 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
+ /** 트리거 버튼에서 선택된 벤더들을 표시할지 여부 */
+ showSelectedInTrigger?: boolean
+ /** 벤더 상태 필터 */
+ statusFilter?: string
+}
+
+export function VendorSelectorDialogMulti({
+ triggerLabel = "벤더 선택",
+ selectedVendors = [],
+ onVendorsSelect,
+ maxSelections,
+ placeholder = "벤더를 검색하세요...",
+ title = "벤더 선택 (다중)",
+ description,
+ disabled = false,
+ triggerVariant = "outline",
+ excludeVendorIds,
+ showInitialData = true,
+ showSelectedInTrigger = true,
+ statusFilter
+}: VendorSelectorDialogMultiProps) {
+ // Dialog 열림/닫힘 상태
+ const [open, setOpen] = useState(false)
+
+ // Dialog 내에서 임시로 선택된 벤더들 (확인 버튼 클릭 전까지)
+ const [tempSelectedVendors, setTempSelectedVendors] = useState<VendorSearchItem[]>([])
+
+ // Dialog 설명 동적 생성
+ const dialogDescription = description ||
+ (maxSelections
+ ? `원하는 벤더를 검색하고 선택해주세요. (최대 ${maxSelections}개)`
+ : "원하는 벤더를 검색하고 선택해주세요."
+ )
+
+ // Dialog 열림 시 현재 선택된 벤더들로 임시 선택 초기화
+ const handleOpenChange = useCallback((newOpen: boolean) => {
+ setOpen(newOpen)
+ if (newOpen) {
+ setTempSelectedVendors([...selectedVendors])
+ }
+ }, [selectedVendors])
+
+ // 벤더 선택 처리 (Dialog 내에서)
+ const handleVendorsChange = useCallback((vendors: VendorSearchItem[]) => {
+ setTempSelectedVendors(vendors)
+ }, [])
+
+ // 확인 버튼 클릭 시 선택 완료
+ const handleConfirm = useCallback(() => {
+ onVendorsSelect?.(tempSelectedVendors)
+ setOpen(false)
+ }, [tempSelectedVendors, onVendorsSelect])
+
+ // 취소 버튼 클릭 시
+ const handleCancel = useCallback(() => {
+ setTempSelectedVendors([...selectedVendors])
+ setOpen(false)
+ }, [selectedVendors])
+
+ // 전체 선택 해제
+ const handleClearAll = useCallback(() => {
+ setTempSelectedVendors([])
+ }, [])
+
+ // 개별 벤더 제거 (트리거 버튼에서)
+ const handleRemoveVendor = useCallback((vendorToRemove: VendorSearchItem, e: React.MouseEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+ const newVendors = selectedVendors.filter(
+ (vendor) => vendor.id !== vendorToRemove.id
+ )
+ onVendorsSelect?.(newVendors)
+ }, [selectedVendors, onVendorsSelect])
+
+ // 벤더 상태별 색상
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'ACTIVE': return 'bg-green-100 text-green-800'
+ case 'APPROVED': return 'bg-blue-100 text-blue-800'
+ case 'PENDING_REVIEW': return 'bg-yellow-100 text-yellow-800'
+ case 'INACTIVE': return 'bg-gray-100 text-gray-800'
+ default: return 'bg-gray-100 text-gray-800'
+ }
+ }
+
+ // 트리거 버튼 렌더링
+ const renderTriggerContent = () => {
+ if (selectedVendors.length === 0) {
+ return triggerLabel
+ }
+
+ if (!showSelectedInTrigger) {
+ return `${triggerLabel} (${selectedVendors.length}개)`
+ }
+
+ return (
+ <div className="flex flex-wrap gap-1 items-center max-w-full">
+ <span className="shrink-0">{triggerLabel}:</span>
+ {selectedVendors.slice(0, 2).map((vendor) => (
+ <Badge
+ key={vendor.id}
+ variant="secondary"
+ className="gap-1 pr-1 max-w-[120px]"
+ >
+ <span className="truncate text-xs">
+ {vendor.vendorName}
+ </span>
+ <Badge className={`${getStatusColor(vendor.status)} ml-1 text-xs`}>
+ {vendor.status}
+ </Badge>
+ {!disabled && (
+ <button
+ type="button"
+ className="ml-1 h-3 w-3 rounded-sm hover:bg-red-100 flex items-center justify-center"
+ onClick={(e) => handleRemoveVendor(vendor, e)}
+ >
+ <X className="h-3 w-3 hover:text-red-500" />
+ </button>
+ )}
+ </Badge>
+ ))}
+ {selectedVendors.length > 2 && (
+ <Badge variant="outline" className="text-xs">
+ +{selectedVendors.length - 2}개
+ </Badge>
+ )}
+ </div>
+ )
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={handleOpenChange}>
+ <DialogTrigger asChild>
+ <Button
+ variant={triggerVariant}
+ disabled={disabled}
+ className="min-h-[2.5rem] h-auto justify-start"
+ >
+ {renderTriggerContent()}
+ </Button>
+ </DialogTrigger>
+
+ <DialogContent className="max-w-md">
+ <DialogHeader>
+ <DialogTitle>{title}</DialogTitle>
+ <DialogDescription>{dialogDescription}</DialogDescription>
+ </DialogHeader>
+
+ <div className="py-4">
+ <VendorSelector
+ selectedVendors={tempSelectedVendors}
+ onVendorsChange={handleVendorsChange}
+ singleSelect={false}
+ maxSelections={maxSelections}
+ placeholder={placeholder}
+ noValuePlaceHolder="벤더를 선택해주세요"
+ closeOnSelect={false}
+ excludeVendorIds={excludeVendorIds}
+ showInitialData={showInitialData}
+ statusFilter={statusFilter}
+ />
+
+ {/* 선택된 벤더 개수 표시 */}
+ <div className="mt-2 text-sm text-muted-foreground">
+ {maxSelections ? (
+ `선택됨: ${tempSelectedVendors.length}/${maxSelections}개`
+ ) : (
+ `선택됨: ${tempSelectedVendors.length}개`
+ )}
+ </div>
+ </div>
+
+ <DialogFooter className="gap-2">
+ <Button variant="outline" onClick={handleCancel}>
+ 취소
+ </Button>
+ {tempSelectedVendors.length > 0 && (
+ <Button variant="ghost" onClick={handleClearAll}>
+ 전체 해제
+ </Button>
+ )}
+ <Button onClick={handleConfirm}>
+ 확인 ({tempSelectedVendors.length}개)
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+}
+
+/**
+ * 사용 예시:
+ *
+ * ```tsx
+ * const [selectedVendors, setSelectedVendors] = useState<VendorSearchItem[]>([]);
+ *
+ * return (
+ * <VendorSelectorDialogMulti
+ * triggerLabel="벤더 선택"
+ * selectedVendors={selectedVendors}
+ * onVendorsSelect={(vendors) => {
+ * setSelectedVendors(vendors);
+ * console.log('선택된 벤더들:', vendors.map(v => ({
+ * id: v.id,
+ * name: v.vendorName,
+ * code: v.vendorCode,
+ * status: v.status
+ * })));
+ * }}
+ * maxSelections={5}
+ * title="협력업체 선택"
+ * description="프로젝트에 참여할 협력업체들을 선택해주세요."
+ * showSelectedInTrigger={true}
+ * statusFilter="ACTIVE"
+ * />
+ * );
+ * ```
+ *
+ * @advanced_usage
+ * ```tsx
+ * // 제외할 벤더가 있는 경우
+ * const excludedVendorIds = new Set([1, 2, 3]);
+ *
+ * <VendorSelectorDialogMulti
+ * selectedVendors={selectedVendors}
+ * onVendorsSelect={setSelectedVendors}
+ * excludeVendorIds={excludedVendorIds}
+ * maxSelections={3}
+ * triggerVariant="default"
+ * showSelectedInTrigger={false}
+ * statusFilter="APPROVED"
+ * />
+ * ```
+ */