ERP-node/frontend/lib/registry/components/entity-search-input/EntitySearchModal.tsx

227 lines
6.9 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Search, Loader2 } from "lucide-react";
import { useEntitySearch } from "./useEntitySearch";
import { EntitySearchResult } from "./types";
interface EntitySearchModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
tableName: string;
displayField: string;
valueField: string;
searchFields?: string[];
filterCondition?: Record<string, any>;
modalTitle?: string;
modalColumns?: string[];
onSelect: (value: any, fullData: EntitySearchResult) => void;
}
export function EntitySearchModal({
open,
onOpenChange,
tableName,
displayField,
valueField,
searchFields = [displayField],
filterCondition = {},
modalTitle = "검색",
modalColumns = [],
onSelect,
}: EntitySearchModalProps) {
const [localSearchText, setLocalSearchText] = useState("");
const {
results,
loading,
error,
pagination,
search,
clearSearch,
loadMore,
} = useEntitySearch({
tableName,
searchFields,
filterCondition,
});
// 모달 열릴 때 초기 검색
useEffect(() => {
if (open) {
search("", 1); // 빈 검색어로 전체 목록 조회
} else {
clearSearch();
setLocalSearchText("");
}
}, [open]);
const handleSearch = () => {
search(localSearchText, 1);
};
const handleSelect = (item: EntitySearchResult) => {
onSelect(item[valueField], item);
onOpenChange(false);
};
// 표시할 컬럼 결정
const displayColumns = modalColumns.length > 0 ? modalColumns : [displayField];
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[95vw] sm:max-w-[800px] max-h-[90vh] overflow-hidden">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg">{modalTitle}</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
</DialogDescription>
</DialogHeader>
{/* 검색 입력 */}
<div className="flex gap-2">
<Input
placeholder="검색어를 입력하세요"
value={localSearchText}
onChange={(e) => setLocalSearchText(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleSearch();
}
}}
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
<Button
onClick={handleSearch}
disabled={loading}
className="h-8 text-xs sm:h-10 sm:text-sm"
>
{loading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Search className="h-4 w-4" />
)}
<span className="ml-2"></span>
</Button>
</div>
{/* 오류 메시지 */}
{error && (
<div className="text-sm text-destructive bg-destructive/10 p-3 rounded-md">
{error}
</div>
)}
{/* 검색 결과 테이블 */}
<div className="border rounded-md overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-xs sm:text-sm">
<thead className="bg-muted">
<tr>
{displayColumns.map((col) => (
<th
key={col}
className="px-4 py-2 text-left font-medium text-muted-foreground"
>
{col}
</th>
))}
<th className="px-4 py-2 text-left font-medium text-muted-foreground w-24">
</th>
</tr>
</thead>
<tbody>
{loading && results.length === 0 ? (
<tr>
<td colSpan={displayColumns.length + 1} className="px-4 py-8 text-center">
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
<p className="mt-2 text-muted-foreground"> ...</p>
</td>
</tr>
) : results.length === 0 ? (
<tr>
<td colSpan={displayColumns.length + 1} className="px-4 py-8 text-center text-muted-foreground">
</td>
</tr>
) : (
results.map((item, index) => {
const uniqueKey = item[valueField] !== undefined ? `${item[valueField]}` : `row-${index}`;
return (
<tr
key={uniqueKey}
className="border-t hover:bg-accent cursor-pointer transition-colors"
onClick={() => handleSelect(item)}
>
{displayColumns.map((col) => (
<td key={`${uniqueKey}-${col}`} className="px-4 py-2">
{item[col] || "-"}
</td>
))}
<td className="px-4 py-2">
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
handleSelect(item);
}}
className="h-7 text-xs"
>
</Button>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
{/* 페이지네이션 정보 */}
{results.length > 0 && (
<div className="flex justify-between items-center text-xs sm:text-sm text-muted-foreground">
<span>
{pagination.total} {results.length}
</span>
{pagination.page * pagination.limit < pagination.total && (
<Button
variant="outline"
size="sm"
onClick={loadMore}
disabled={loading}
className="h-7 text-xs"
>
</Button>
)}
</div>
)}
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={() => onOpenChange(false)}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}