버튼별로 데이터 필터링기능
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user