버튼별로 데이터 필터링기능

This commit is contained in:
kjs
2025-12-16 18:02:08 +09:00
parent a73b37f558
commit d6f40f3cd3
12 changed files with 909 additions and 113 deletions

View File

@@ -7,19 +7,8 @@ import { Search, X, Check, ChevronsUpDown } from "lucide-react";
import { EntitySearchModal } from "./EntitySearchModal";
import { EntitySearchInputProps, EntitySearchResult } from "./types";
import { cn } from "@/lib/utils";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { dynamicFormApi } from "@/lib/api/dynamicForm";
export function EntitySearchInputComponent({
@@ -44,7 +33,7 @@ export function EntitySearchInputComponent({
component,
isInteractive,
onFormDataChange,
}: EntitySearchInputProps & {
}: EntitySearchInputProps & {
uiMode?: string;
component?: any;
isInteractive?: boolean;
@@ -52,7 +41,7 @@ export function EntitySearchInputComponent({
}) {
// uiMode가 있으면 우선 사용, 없으면 modeProp 사용, 기본값 "combo"
const mode = (uiMode || modeProp || "combo") as "select" | "modal" | "combo" | "autocomplete";
const [modalOpen, setModalOpen] = useState(false);
const [selectOpen, setSelectOpen] = useState(false);
const [displayValue, setDisplayValue] = useState("");
@@ -74,7 +63,7 @@ export function EntitySearchInputComponent({
const loadOptions = async () => {
if (!tableName) return;
setIsLoadingOptions(true);
try {
const response = await dynamicFormApi.getTableData(tableName, {
@@ -82,7 +71,7 @@ export function EntitySearchInputComponent({
pageSize: 100, // 최대 100개까지 로드
filters: filterCondition,
});
if (response.success && response.data) {
setOptions(response.data);
}
@@ -93,28 +82,73 @@ export function EntitySearchInputComponent({
}
};
// value가 변경되면 표시값 업데이트
// value가 변경되면 표시값 업데이트 (외래키 값으로 데이터 조회)
useEffect(() => {
if (value && selectedData) {
setDisplayValue(selectedData[displayField] || "");
} else if (value && mode === "select" && options.length > 0) {
// select 모드에서 value가 있고 options가 로드된 경우
const found = options.find(opt => opt[valueField] === value);
if (found) {
setSelectedData(found);
setDisplayValue(found[displayField] || "");
const loadDisplayValue = async () => {
if (value && selectedData) {
// 이미 selectedData가 있으면 표시값만 업데이트
setDisplayValue(selectedData[displayField] || "");
} else if (value && mode === "select" && options.length > 0) {
// select 모드에서 value가 있고 options가 로드된 경우
const found = options.find((opt) => opt[valueField] === value);
if (found) {
setSelectedData(found);
setDisplayValue(found[displayField] || "");
}
} else if (value && !selectedData && tableName) {
// value는 있지만 selectedData가 없는 경우 (초기 로드 시)
// API로 해당 데이터 조회
try {
console.log("🔍 [EntitySearchInput] 초기값 조회:", { value, tableName, valueField });
const response = await dynamicFormApi.getTableData(tableName, {
filters: { [valueField]: value },
pageSize: 1,
});
if (response.success && response.data) {
// 데이터 추출 (중첩 구조 처리)
const responseData = response.data as any;
const dataArray = Array.isArray(responseData)
? responseData
: responseData?.data
? Array.isArray(responseData.data)
? responseData.data
: [responseData.data]
: [];
if (dataArray.length > 0) {
const foundData = dataArray[0];
setSelectedData(foundData);
setDisplayValue(foundData[displayField] || "");
console.log("✅ [EntitySearchInput] 초기값 로드 완료:", foundData);
} else {
// 데이터를 찾지 못한 경우 value 자체를 표시
console.log("⚠️ [EntitySearchInput] 초기값 데이터 없음, value 표시:", value);
setDisplayValue(String(value));
}
} else {
console.log("⚠️ [EntitySearchInput] API 응답 실패, value 표시:", value);
setDisplayValue(String(value));
}
} catch (error) {
console.error("❌ [EntitySearchInput] 초기값 조회 실패:", error);
// 에러 시 value 자체를 표시
setDisplayValue(String(value));
}
} else if (!value) {
setDisplayValue("");
setSelectedData(null);
}
} else if (!value) {
setDisplayValue("");
setSelectedData(null);
}
}, [value, displayField, options, mode, valueField]);
};
loadDisplayValue();
}, [value, displayField, options, mode, valueField, tableName, selectedData]);
const handleSelect = (newValue: any, fullData: EntitySearchResult) => {
setSelectedData(fullData);
setDisplayValue(fullData[displayField] || "");
onChange?.(newValue, fullData);
// 🆕 onFormDataChange 호출 (formData에 값 저장)
if (isInteractive && onFormDataChange && component?.columnName) {
onFormDataChange(component.columnName, newValue);
@@ -126,7 +160,7 @@ export function EntitySearchInputComponent({
setDisplayValue("");
setSelectedData(null);
onChange?.(null, null);
// 🆕 onFormDataChange 호출 (formData에서 값 제거)
if (isInteractive && onFormDataChange && component?.columnName) {
onFormDataChange(component.columnName, null);
@@ -147,14 +181,19 @@ export function EntitySearchInputComponent({
// 높이 계산 (style에서 height가 있으면 사용, 없으면 기본값)
const componentHeight = style?.height;
const inputStyle: React.CSSProperties = componentHeight
? { height: componentHeight }
: {};
const inputStyle: React.CSSProperties = componentHeight ? { height: componentHeight } : {};
// select 모드: 검색 가능한 드롭다운
if (mode === "select") {
return (
<div className={cn("flex flex-col", className)} style={style}>
<div className={cn("relative flex flex-col", className)} style={style}>
{/* 라벨 렌더링 */}
{component?.label && component?.style?.labelDisplay !== false && (
<label className="text-muted-foreground absolute -top-6 left-0 text-sm font-medium">
{component.label}
{component.required && <span className="text-destructive">*</span>}
</label>
)}
<Popover open={selectOpen} onOpenChange={setSelectOpen}>
<PopoverTrigger asChild>
<Button
@@ -165,30 +204,19 @@ export function EntitySearchInputComponent({
className={cn(
"w-full justify-between font-normal",
!componentHeight && "h-8 text-xs sm:h-10 sm:text-sm",
!value && "text-muted-foreground"
!value && "text-muted-foreground",
)}
style={inputStyle}
>
{isLoadingOptions
? "로딩 중..."
: displayValue || placeholder}
{isLoadingOptions ? "로딩 중..." : displayValue || placeholder}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
style={{ width: "var(--radix-popover-trigger-width)" }}
align="start"
>
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
<Command>
<CommandInput
placeholder={`${displayField} 검색...`}
className="text-xs sm:text-sm"
/>
<CommandInput placeholder={`${displayField} 검색...`} className="text-xs sm:text-sm" />
<CommandList>
<CommandEmpty className="text-xs sm:text-sm py-4 text-center">
.
</CommandEmpty>
<CommandEmpty className="py-4 text-center text-xs sm:text-sm"> .</CommandEmpty>
<CommandGroup>
{options.map((option, index) => (
<CommandItem
@@ -198,17 +226,12 @@ export function EntitySearchInputComponent({
className="text-xs sm:text-sm"
>
<Check
className={cn(
"mr-2 h-4 w-4",
value === option[valueField] ? "opacity-100" : "opacity-0"
)}
className={cn("mr-2 h-4 w-4", value === option[valueField] ? "opacity-100" : "opacity-0")}
/>
<div className="flex flex-col">
<span className="font-medium">{option[displayField]}</span>
{valueField !== displayField && (
<span className="text-[10px] text-muted-foreground">
{option[valueField]}
</span>
<span className="text-muted-foreground text-[10px]">{option[valueField]}</span>
)}
</div>
</CommandItem>
@@ -221,7 +244,7 @@ export function EntitySearchInputComponent({
{/* 추가 정보 표시 */}
{showAdditionalInfo && selectedData && additionalFields.length > 0 && (
<div className="text-xs text-muted-foreground space-y-1 px-2 mt-1">
<div className="text-muted-foreground mt-1 space-y-1 px-2 text-xs">
{additionalFields.map((field) => (
<div key={field} className="flex gap-2">
<span className="font-medium">{field}:</span>
@@ -236,9 +259,16 @@ export function EntitySearchInputComponent({
// modal, combo, autocomplete 모드
return (
<div className={cn("flex flex-col", className)} style={style}>
<div className={cn("relative flex flex-col", className)} style={style}>
{/* 라벨 렌더링 */}
{component?.label && component?.style?.labelDisplay !== false && (
<label className="text-muted-foreground absolute -top-6 left-0 text-sm font-medium">
{component.label}
{component.required && <span className="text-destructive">*</span>}
</label>
)}
{/* 입력 필드 */}
<div className="flex gap-2 h-full">
<div className="flex h-full gap-2">
<div className="relative flex-1">
<Input
value={displayValue}
@@ -255,7 +285,7 @@ export function EntitySearchInputComponent({
variant="ghost"
size="sm"
onClick={handleClear}
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6 p-0"
className="absolute top-1/2 right-1 h-6 w-6 -translate-y-1/2 p-0"
>
<X className="h-3 w-3" />
</Button>
@@ -278,7 +308,7 @@ export function EntitySearchInputComponent({
{/* 추가 정보 표시 */}
{showAdditionalInfo && selectedData && additionalFields.length > 0 && (
<div className="text-xs text-muted-foreground space-y-1 px-2 mt-1">
<div className="text-muted-foreground mt-1 space-y-1 px-2 text-xs">
{additionalFields.map((field) => (
<div key={field} className="flex gap-2">
<span className="font-medium">{field}:</span>
@@ -306,4 +336,3 @@ export function EntitySearchInputComponent({
</div>
);
}