177 lines
4.9 KiB
TypeScript
177 lines
4.9 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Search 메타 컴포넌트 렌더러
|
||
|
|
* - config.fields 배열로 검색 필드들을 가로로 배치
|
||
|
|
* - "검색" 버튼 클릭 시 v2EventBus로 TABLE_REFRESH + 필터 전달
|
||
|
|
* - "초기화" 버튼으로 모든 필터 클리어
|
||
|
|
* - 디자인 모드: 비활성 상태로 표시
|
||
|
|
*/
|
||
|
|
|
||
|
|
import React, { useState } from "react";
|
||
|
|
import { cn } from "@/lib/utils";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { Input } from "@/components/ui/input";
|
||
|
|
import { Label } from "@/components/ui/label";
|
||
|
|
import {
|
||
|
|
Select,
|
||
|
|
SelectContent,
|
||
|
|
SelectItem,
|
||
|
|
SelectTrigger,
|
||
|
|
SelectValue,
|
||
|
|
} from "@/components/ui/select";
|
||
|
|
import { Search, X } from "lucide-react";
|
||
|
|
import { v2EventBus } from "@/lib/v2-core/events/EventBus";
|
||
|
|
import { V2_EVENTS } from "@/lib/v2-core/events/types";
|
||
|
|
|
||
|
|
interface SearchField {
|
||
|
|
columnName: string;
|
||
|
|
label: string;
|
||
|
|
searchType?: "text" | "select" | "date";
|
||
|
|
options?: { value: string; label: string }[];
|
||
|
|
}
|
||
|
|
|
||
|
|
interface SearchRendererProps {
|
||
|
|
id: string;
|
||
|
|
config: {
|
||
|
|
fields: SearchField[];
|
||
|
|
targetTableId?: string; // 연결된 DataView ID (옵션)
|
||
|
|
targetTableName?: string; // 🔍 대상 테이블명 (필터 식별용)
|
||
|
|
};
|
||
|
|
isDesignMode?: boolean;
|
||
|
|
disabled?: boolean;
|
||
|
|
className?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function SearchRenderer({
|
||
|
|
id,
|
||
|
|
config,
|
||
|
|
isDesignMode = false,
|
||
|
|
disabled = false,
|
||
|
|
className,
|
||
|
|
}: SearchRendererProps) {
|
||
|
|
const { fields = [] } = config;
|
||
|
|
const [filters, setFilters] = useState<Record<string, any>>({});
|
||
|
|
|
||
|
|
// 검색 실행
|
||
|
|
const handleSearch = () => {
|
||
|
|
if (isDesignMode || disabled) return;
|
||
|
|
|
||
|
|
// 🔍 v2EventBus로 TABLE_REFRESH 이벤트 발행
|
||
|
|
v2EventBus.emitSync(V2_EVENTS.TABLE_REFRESH, {
|
||
|
|
filters,
|
||
|
|
sourceId: id,
|
||
|
|
targetId: config.targetTableId,
|
||
|
|
tableName: config.targetTableName, // 대상 테이블명 전달
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
// 필터 초기화
|
||
|
|
const handleReset = () => {
|
||
|
|
if (isDesignMode || disabled) return;
|
||
|
|
|
||
|
|
setFilters({});
|
||
|
|
// 🔍 빈 필터로 새로고침
|
||
|
|
v2EventBus.emitSync(V2_EVENTS.TABLE_REFRESH, {
|
||
|
|
filters: {},
|
||
|
|
sourceId: id,
|
||
|
|
targetId: config.targetTableId,
|
||
|
|
tableName: config.targetTableName, // 대상 테이블명 전달
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
// 필드 값 변경
|
||
|
|
const handleFieldChange = (columnName: string, value: any) => {
|
||
|
|
setFilters((prev) => ({ ...prev, [columnName]: value }));
|
||
|
|
};
|
||
|
|
|
||
|
|
// Enter 키 처리
|
||
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||
|
|
if (e.key === "Enter") {
|
||
|
|
handleSearch();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if (fields.length === 0) {
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
className={cn(
|
||
|
|
"flex items-center justify-center rounded-md border border-dashed p-4 text-sm text-muted-foreground",
|
||
|
|
className
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
검색 필드를 추가하세요
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className={cn("flex flex-wrap items-end gap-2", className)}>
|
||
|
|
{fields.map((field) => (
|
||
|
|
<div key={field.columnName} className="flex flex-col gap-1">
|
||
|
|
<Label className="text-xs sm:text-sm">{field.label}</Label>
|
||
|
|
|
||
|
|
{field.searchType === "select" && field.options ? (
|
||
|
|
<Select
|
||
|
|
value={filters[field.columnName] || ""}
|
||
|
|
onValueChange={(value) => handleFieldChange(field.columnName, value)}
|
||
|
|
disabled={isDesignMode || disabled}
|
||
|
|
>
|
||
|
|
<SelectTrigger className="h-8 w-32 sm:h-10 sm:w-40">
|
||
|
|
<SelectValue placeholder="선택" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
{field.options.map((opt) => (
|
||
|
|
<SelectItem key={opt.value} value={opt.value}>
|
||
|
|
{opt.label}
|
||
|
|
</SelectItem>
|
||
|
|
))}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
) : (
|
||
|
|
<Input
|
||
|
|
type={field.searchType === "date" ? "date" : "text"}
|
||
|
|
placeholder={field.label}
|
||
|
|
value={filters[field.columnName] || ""}
|
||
|
|
onChange={(e) => handleFieldChange(field.columnName, e.target.value)}
|
||
|
|
onKeyDown={handleKeyDown}
|
||
|
|
disabled={isDesignMode || disabled}
|
||
|
|
className="h-8 w-32 sm:h-10 sm:w-40"
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
|
||
|
|
{/* 검색 버튼 */}
|
||
|
|
<Button
|
||
|
|
onClick={handleSearch}
|
||
|
|
disabled={isDesignMode || disabled}
|
||
|
|
size="default"
|
||
|
|
className="h-8 sm:h-10"
|
||
|
|
>
|
||
|
|
<Search className="mr-2 h-4 w-4" />
|
||
|
|
검색
|
||
|
|
</Button>
|
||
|
|
|
||
|
|
{/* 초기화 버튼 */}
|
||
|
|
<Button
|
||
|
|
onClick={handleReset}
|
||
|
|
disabled={isDesignMode || disabled}
|
||
|
|
variant="outline"
|
||
|
|
size="default"
|
||
|
|
className="h-8 sm:h-10"
|
||
|
|
>
|
||
|
|
<X className="mr-2 h-4 w-4" />
|
||
|
|
초기화
|
||
|
|
</Button>
|
||
|
|
|
||
|
|
{/* 디자인 모드 표시 */}
|
||
|
|
{isDesignMode && (
|
||
|
|
<span className="ml-2 text-xs text-muted-foreground">
|
||
|
|
(디자인 모드: 비활성)
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|