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

717 lines
28 KiB
TypeScript
Raw Normal View History

"use client";
import React, { useState, useEffect, useRef } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
2025-12-16 14:38:03 +09:00
import { Search, X, Check, ChevronsUpDown } from "lucide-react";
import { EntitySearchModal } from "./EntitySearchModal";
import { EntitySearchInputProps, EntitySearchResult } from "./types";
import { cn } from "@/lib/utils";
2025-12-16 18:02:08 +09:00
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
2025-12-16 14:38:03 +09:00
import { dynamicFormApi } from "@/lib/api/dynamicForm";
import { cascadingRelationApi } from "@/lib/api/cascadingRelation";
export function EntitySearchInputComponent({
tableName,
displayField,
valueField,
searchFields = [displayField],
2025-12-16 14:38:03 +09:00
mode: modeProp,
uiMode, // EntityConfigPanel에서 저장되는 값
placeholder = "검색...",
disabled = false,
filterCondition = {},
value,
onChange,
modalTitle = "검색",
modalColumns = [],
showAdditionalInfo = false,
additionalFields = [],
className,
2025-12-16 14:38:03 +09:00
style,
// 연쇄관계 props
cascadingRelationCode,
parentValue: parentValueProp,
parentFieldId,
formData,
// 다중선택 props
multiple: multipleProp,
// 추가 props
2025-12-16 14:38:03 +09:00
component,
isInteractive,
onFormDataChange,
2025-12-16 18:02:08 +09:00
}: EntitySearchInputProps & {
2025-12-16 14:38:03 +09:00
uiMode?: string;
component?: any;
isInteractive?: boolean;
onFormDataChange?: (fieldName: string, value: any) => void;
webTypeConfig?: any; // 웹타입 설정 (연쇄관계 등)
2025-12-16 14:38:03 +09:00
}) {
// uiMode가 있으면 우선 사용, 없으면 modeProp 사용, 기본값 "combo"
const mode = (uiMode || modeProp || "combo") as "select" | "modal" | "combo" | "autocomplete";
2025-12-16 18:02:08 +09:00
// 다중선택 및 연쇄관계 설정 (props > webTypeConfig > componentConfig 순서)
const config = component?.componentConfig || component?.webTypeConfig || {};
const isMultiple = multipleProp ?? config.multiple ?? false;
// 연쇄관계 설정 추출
const effectiveCascadingRelationCode = cascadingRelationCode || config.cascadingRelationCode;
2026-01-06 17:02:42 +09:00
// cascadingParentField: ConfigPanel에서 저장되는 필드명
const effectiveParentFieldId = parentFieldId || config.cascadingParentField || config.parentFieldId;
const effectiveCascadingRole = config.cascadingRole; // "parent" | "child" | undefined
// 부모 역할이면 연쇄관계 로직 적용 안함 (자식만 부모 값에 따라 필터링됨)
const isChildRole = effectiveCascadingRole === "child";
const shouldApplyCascading = effectiveCascadingRelationCode && isChildRole;
const [modalOpen, setModalOpen] = useState(false);
2025-12-16 14:38:03 +09:00
const [selectOpen, setSelectOpen] = useState(false);
const [displayValue, setDisplayValue] = useState("");
const [selectedData, setSelectedData] = useState<EntitySearchResult | null>(null);
2025-12-16 14:38:03 +09:00
const [options, setOptions] = useState<EntitySearchResult[]>([]);
const [isLoadingOptions, setIsLoadingOptions] = useState(false);
const [optionsLoaded, setOptionsLoaded] = useState(false);
// 다중선택 상태 (콤마로 구분된 값들)
const [selectedValues, setSelectedValues] = useState<string[]>([]);
const [selectedDataList, setSelectedDataList] = useState<EntitySearchResult[]>([]);
// 연쇄관계 상태
const [cascadingOptions, setCascadingOptions] = useState<EntitySearchResult[]>([]);
const [isCascadingLoading, setIsCascadingLoading] = useState(false);
const previousParentValue = useRef<any>(null);
// 다중선택 초기값 설정
useEffect(() => {
if (isMultiple && value) {
const vals =
typeof value === "string" ? value.split(",").filter(Boolean) : Array.isArray(value) ? value : [value];
setSelectedValues(vals.map(String));
} else if (isMultiple && !value) {
setSelectedValues([]);
setSelectedDataList([]);
}
}, [isMultiple, value]);
// 부모 필드 값 결정 (직접 전달 또는 formData에서 추출) - 자식 역할일 때만 필요
const parentValue = isChildRole
? (parentValueProp ?? (effectiveParentFieldId && formData ? formData[effectiveParentFieldId] : undefined))
: undefined;
2025-12-16 14:38:03 +09:00
// filterCondition을 문자열로 변환하여 비교 (객체 참조 문제 해결)
const filterConditionKey = JSON.stringify(filterCondition || {});
// 연쇄관계가 설정된 경우: 부모 값이 변경되면 자식 옵션 로드 (자식 역할일 때만)
2025-12-16 14:38:03 +09:00
useEffect(() => {
const loadCascadingOptions = async () => {
if (!shouldApplyCascading) return;
// 부모 값이 없으면 옵션 초기화
if (!parentValue) {
setCascadingOptions([]);
// 부모 값이 변경되면 현재 값도 초기화
if (previousParentValue.current !== null && previousParentValue.current !== parentValue) {
handleClear();
}
previousParentValue.current = parentValue;
return;
}
// 부모 값이 동일하면 스킵
if (previousParentValue.current === parentValue) {
return;
}
previousParentValue.current = parentValue;
setIsCascadingLoading(true);
try {
console.log("🔗 연쇄관계 옵션 로드:", { effectiveCascadingRelationCode, parentValue });
const response = await cascadingRelationApi.getOptions(effectiveCascadingRelationCode, String(parentValue));
if (response.success && response.data) {
// 옵션을 EntitySearchResult 형태로 변환
const formattedOptions = response.data.map((opt: any) => ({
[valueField]: opt.value,
[displayField]: opt.label,
...opt, // 추가 필드도 포함
}));
setCascadingOptions(formattedOptions);
console.log("✅ 연쇄관계 옵션 로드 완료:", formattedOptions.length, "개");
// 현재 선택된 값이 새 옵션에 없으면 초기화
if (value && !formattedOptions.find((opt: any) => opt[valueField] === value)) {
handleClear();
}
} else {
setCascadingOptions([]);
}
} catch (error) {
console.error("❌ 연쇄관계 옵션 로드 실패:", error);
setCascadingOptions([]);
} finally {
setIsCascadingLoading(false);
}
};
loadCascadingOptions();
}, [shouldApplyCascading, effectiveCascadingRelationCode, parentValue, valueField, displayField]);
// select 모드일 때 옵션 로드 (연쇄관계가 없거나 부모 역할인 경우)
useEffect(() => {
if (mode === "select" && tableName && !optionsLoaded && !shouldApplyCascading) {
2025-12-16 14:38:03 +09:00
loadOptions();
setOptionsLoaded(true);
}
}, [mode, tableName, filterConditionKey, optionsLoaded, shouldApplyCascading]);
2025-12-16 14:38:03 +09:00
const loadOptions = async () => {
if (!tableName) return;
2025-12-16 18:02:08 +09:00
2025-12-16 14:38:03 +09:00
setIsLoadingOptions(true);
try {
const response = await dynamicFormApi.getTableData(tableName, {
page: 1,
pageSize: 100, // 최대 100개까지 로드
filters: filterCondition,
});
2025-12-16 18:02:08 +09:00
2025-12-16 14:38:03 +09:00
if (response.success && response.data) {
setOptions(response.data);
}
} catch (error) {
console.error("옵션 로드 실패:", error);
} finally {
setIsLoadingOptions(false);
}
};
// 실제 사용할 옵션 목록 (자식 역할이고 연쇄관계가 있으면 연쇄 옵션 사용)
const effectiveOptions = shouldApplyCascading ? cascadingOptions : options;
const isLoading = shouldApplyCascading ? isCascadingLoading : isLoadingOptions;
2025-12-16 18:02:08 +09:00
// value가 변경되면 표시값 업데이트 (외래키 값으로 데이터 조회)
useEffect(() => {
2025-12-16 18:02:08 +09:00
const loadDisplayValue = async () => {
2025-12-18 15:24:20 +09:00
// value가 없으면 초기화
if (!value) {
setDisplayValue("");
setSelectedData(null);
return;
}
// 이미 selectedData가 있고 value와 일치하면 표시값만 업데이트
if (selectedData && String(selectedData[valueField]) === String(value)) {
2025-12-16 18:02:08 +09:00
setDisplayValue(selectedData[displayField] || "");
2025-12-18 15:24:20 +09:00
return;
}
// select 모드에서 options가 로드된 경우 먼저 옵션에서 찾기
if (mode === "select" && effectiveOptions.length > 0) {
// 타입 변환하여 비교 (숫자 vs 문자열 문제 해결)
const found = effectiveOptions.find((opt) => String(opt[valueField]) === String(value));
2025-12-16 18:02:08 +09:00
if (found) {
setSelectedData(found);
setDisplayValue(found[displayField] || "");
2025-12-18 15:24:20 +09:00
console.log("✅ [EntitySearchInput] 옵션에서 초기값 찾음:", { value, found });
return;
2025-12-16 18:02:08 +09:00
}
2025-12-18 15:24:20 +09:00
// 옵션에서 찾지 못한 경우 API로 조회 진행
console.log("⚠️ [EntitySearchInput] 옵션에서 찾지 못함, API로 조회:", {
value,
optionsCount: effectiveOptions.length,
});
}
// API로 해당 데이터 조회
if (tableName) {
2025-12-16 18:02:08 +09:00
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));
}
2025-12-16 14:38:03 +09:00
}
2025-12-16 18:02:08 +09:00
};
loadDisplayValue();
}, [value, displayField, effectiveOptions, mode, valueField, tableName, selectedData]);
const handleSelect = (newValue: any, fullData: EntitySearchResult) => {
if (isMultiple) {
// 다중선택 모드
const valueStr = String(newValue);
const isAlreadySelected = selectedValues.includes(valueStr);
let newSelectedValues: string[];
let newSelectedDataList: EntitySearchResult[];
if (isAlreadySelected) {
// 이미 선택된 항목이면 제거
newSelectedValues = selectedValues.filter((v) => v !== valueStr);
newSelectedDataList = selectedDataList.filter((d) => String(d[valueField]) !== valueStr);
} else {
// 선택되지 않은 항목이면 추가
newSelectedValues = [...selectedValues, valueStr];
newSelectedDataList = [...selectedDataList, fullData];
}
setSelectedValues(newSelectedValues);
setSelectedDataList(newSelectedDataList);
const joinedValue = newSelectedValues.join(",");
onChange?.(joinedValue, newSelectedDataList);
if (isInteractive && onFormDataChange && component?.columnName) {
onFormDataChange(component.columnName, joinedValue);
console.log("📤 EntitySearchInput (multiple) -> onFormDataChange:", component.columnName, joinedValue);
}
} else {
// 단일선택 모드
setSelectedData(fullData);
setDisplayValue(fullData[displayField] || "");
onChange?.(newValue, fullData);
if (isInteractive && onFormDataChange && component?.columnName) {
onFormDataChange(component.columnName, newValue);
console.log("📤 EntitySearchInput -> onFormDataChange:", component.columnName, newValue);
}
}
};
// 다중선택 모드에서 개별 항목 제거
const handleRemoveValue = (valueToRemove: string) => {
const newSelectedValues = selectedValues.filter((v) => v !== valueToRemove);
const newSelectedDataList = selectedDataList.filter((d) => String(d[valueField]) !== valueToRemove);
setSelectedValues(newSelectedValues);
setSelectedDataList(newSelectedDataList);
const joinedValue = newSelectedValues.join(",");
onChange?.(joinedValue || null, newSelectedDataList);
2025-12-16 18:02:08 +09:00
2025-12-16 14:38:03 +09:00
if (isInteractive && onFormDataChange && component?.columnName) {
onFormDataChange(component.columnName, joinedValue || null);
console.log("📤 EntitySearchInput (remove) -> onFormDataChange:", component.columnName, joinedValue);
2025-12-16 14:38:03 +09:00
}
};
const handleClear = () => {
if (isMultiple) {
setSelectedValues([]);
setSelectedDataList([]);
onChange?.(null, []);
} else {
setDisplayValue("");
setSelectedData(null);
onChange?.(null, null);
}
2025-12-16 18:02:08 +09:00
2025-12-16 14:38:03 +09:00
if (isInteractive && onFormDataChange && component?.columnName) {
onFormDataChange(component.columnName, null);
console.log("📤 EntitySearchInput -> onFormDataChange (clear):", component.columnName, null);
}
};
const handleOpenModal = () => {
if (!disabled) {
setModalOpen(true);
}
};
2025-12-16 14:38:03 +09:00
const handleSelectOption = (option: EntitySearchResult) => {
handleSelect(option[valueField], option);
// 다중선택이 아닌 경우에만 드롭다운 닫기
if (!isMultiple) {
setSelectOpen(false);
}
2025-12-16 14:38:03 +09:00
};
// 높이 계산 (style에서 height가 있으면 사용, 없으면 기본값)
const componentHeight = style?.height;
2025-12-16 18:02:08 +09:00
const inputStyle: React.CSSProperties = componentHeight ? { height: componentHeight } : {};
2025-12-16 14:38:03 +09:00
// select 모드: 검색 가능한 드롭다운
if (mode === "select") {
// 다중선택 모드
if (isMultiple) {
return (
<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={cn(
"box-border flex min-h-[40px] w-full flex-wrap items-center gap-2 rounded-md border bg-white px-3 py-2",
!disabled && "hover:border-primary/50",
disabled && "cursor-not-allowed bg-gray-100 opacity-60",
)}
onClick={() => !disabled && !isLoading && setSelectOpen(true)}
style={{ cursor: disabled ? "not-allowed" : "pointer" }}
>
{selectedValues.length > 0 ? (
selectedValues.map((val) => {
const opt = effectiveOptions.find((o) => String(o[valueField]) === val);
const label = opt?.[displayField] || val;
return (
<span
key={val}
className="flex items-center gap-1 rounded bg-blue-100 px-2 py-1 text-sm text-blue-800"
>
{label}
{!disabled && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleRemoveValue(val);
}}
className="ml-1 text-blue-600 hover:text-blue-800"
>
×
</button>
)}
</span>
);
})
) : (
<span className="text-muted-foreground text-sm">
{isLoading
? "로딩 중..."
: shouldApplyCascading && !parentValue
? "상위 항목을 먼저 선택하세요"
: placeholder}
</span>
)}
<ChevronsUpDown className="ml-auto h-4 w-4 shrink-0 opacity-50" />
</div>
{/* 옵션 드롭다운 */}
{selectOpen && !disabled && (
<div className="absolute top-full left-0 z-50 mt-1 w-full rounded-md border bg-white shadow-md">
<Command>
<CommandInput placeholder={`${displayField} 검색...`} className="text-xs sm:text-sm" />
<CommandList className="max-h-60">
<CommandEmpty className="py-4 text-center text-xs sm:text-sm"> .</CommandEmpty>
<CommandGroup>
{effectiveOptions.map((option, index) => {
const isSelected = selectedValues.includes(String(option[valueField]));
return (
<CommandItem
key={option[valueField] || index}
value={`${option[displayField] || ""}-${option[valueField] || ""}`}
onSelect={() => handleSelectOption(option)}
className="text-xs sm:text-sm"
>
<Check className={cn("mr-2 h-4 w-4", isSelected ? "opacity-100" : "opacity-0")} />
<div className="flex flex-col">
<span className="font-medium">{option[displayField]}</span>
{valueField !== displayField && (
<span className="text-muted-foreground text-[10px]">{option[valueField]}</span>
)}
</div>
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
{/* 닫기 버튼 */}
<div className="border-t p-2">
<Button variant="outline" size="sm" onClick={() => setSelectOpen(false)} className="w-full text-xs">
</Button>
</div>
</div>
)}
{/* 외부 클릭 시 닫기 */}
{selectOpen && <div className="fixed inset-0 z-40" onClick={() => setSelectOpen(false)} />}
</div>
);
}
// 단일선택 모드 (기존 로직)
2025-12-16 14:38:03 +09:00
return (
2025-12-16 18:02:08 +09:00
<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>
)}
2025-12-16 14:38:03 +09:00
<Popover open={selectOpen} onOpenChange={setSelectOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={selectOpen}
disabled={disabled || isLoading || Boolean(shouldApplyCascading && !parentValue)}
2025-12-16 14:38:03 +09:00
className={cn(
"w-full justify-between font-normal",
!componentHeight && "h-8 text-xs sm:h-10 sm:text-sm",
2025-12-16 18:02:08 +09:00
!value && "text-muted-foreground",
2025-12-16 14:38:03 +09:00
)}
style={inputStyle}
>
{isLoading
? "로딩 중..."
: shouldApplyCascading && !parentValue
? "상위 항목을 먼저 선택하세요"
: displayValue || placeholder}
2025-12-16 14:38:03 +09:00
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
2025-12-16 18:02:08 +09:00
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
2025-12-16 14:38:03 +09:00
<Command>
2025-12-16 18:02:08 +09:00
<CommandInput placeholder={`${displayField} 검색...`} className="text-xs sm:text-sm" />
2025-12-16 14:38:03 +09:00
<CommandList>
2025-12-16 18:02:08 +09:00
<CommandEmpty className="py-4 text-center text-xs sm:text-sm"> .</CommandEmpty>
2025-12-16 14:38:03 +09:00
<CommandGroup>
{effectiveOptions.map((option, index) => (
2025-12-16 14:38:03 +09:00
<CommandItem
key={option[valueField] || index}
value={`${option[displayField] || ""}-${option[valueField] || ""}`}
onSelect={() => handleSelectOption(option)}
className="text-xs sm:text-sm"
>
<Check
2025-12-16 18:02:08 +09:00
className={cn("mr-2 h-4 w-4", value === option[valueField] ? "opacity-100" : "opacity-0")}
2025-12-16 14:38:03 +09:00
/>
<div className="flex flex-col">
<span className="font-medium">{option[displayField]}</span>
{valueField !== displayField && (
2025-12-16 18:02:08 +09:00
<span className="text-muted-foreground text-[10px]">{option[valueField]}</span>
2025-12-16 14:38:03 +09:00
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{/* 추가 정보 표시 */}
{showAdditionalInfo && selectedData && additionalFields.length > 0 && (
2025-12-16 18:02:08 +09:00
<div className="text-muted-foreground mt-1 space-y-1 px-2 text-xs">
2025-12-16 14:38:03 +09:00
{additionalFields.map((field) => (
<div key={field} className="flex gap-2">
<span className="font-medium">{field}:</span>
<span>{selectedData[field] || "-"}</span>
</div>
))}
</div>
)}
</div>
);
}
// modal, combo, autocomplete 모드
// 다중선택 모드
if (isMultiple) {
return (
<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 h-full gap-2">
<div
className={cn(
"box-border flex min-h-[40px] flex-1 flex-wrap items-center gap-2 rounded-md border bg-white px-3 py-2",
!disabled && "hover:border-primary/50",
disabled && "cursor-not-allowed bg-gray-100 opacity-60",
)}
>
{selectedValues.length > 0 ? (
selectedValues.map((val) => {
// selectedDataList에서 먼저 찾고, 없으면 effectiveOptions에서 찾기
const dataFromList = selectedDataList.find((d) => String(d[valueField]) === val);
const opt = dataFromList || effectiveOptions.find((o) => String(o[valueField]) === val);
const label = opt?.[displayField] || val;
return (
<span
key={val}
className="flex items-center gap-1 rounded bg-blue-100 px-2 py-1 text-sm text-blue-800"
>
{label}
{!disabled && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleRemoveValue(val);
}}
className="ml-1 text-blue-600 hover:text-blue-800"
>
×
</button>
)}
</span>
);
})
) : (
<span className="text-muted-foreground text-sm">{placeholder}</span>
)}
</div>
{/* 모달 버튼: modal 또는 combo 모드일 때만 표시 */}
{(mode === "modal" || mode === "combo") && (
<Button
type="button"
onClick={handleOpenModal}
disabled={disabled}
className={cn(!componentHeight && "h-8 text-xs sm:h-10 sm:text-sm")}
style={inputStyle}
>
<Search className="h-4 w-4" />
</Button>
)}
</div>
{/* 검색 모달: modal 또는 combo 모드일 때만 렌더링 */}
{(mode === "modal" || mode === "combo") && (
<EntitySearchModal
open={modalOpen}
onOpenChange={setModalOpen}
tableName={tableName}
displayField={displayField}
valueField={valueField}
searchFields={searchFields}
filterCondition={filterCondition}
modalTitle={modalTitle}
modalColumns={modalColumns}
onSelect={handleSelect}
multiple={isMultiple}
selectedValues={selectedValues}
/>
)}
</div>
);
}
// 단일선택 모드 (기존 로직)
return (
2025-12-16 18:02:08 +09:00
<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>
)}
{/* 입력 필드 */}
2025-12-16 18:02:08 +09:00
<div className="flex h-full gap-2">
<div className="relative flex-1">
<Input
value={displayValue}
onChange={(e) => setDisplayValue(e.target.value)}
placeholder={placeholder}
disabled={disabled}
readOnly={mode === "modal" || mode === "combo"}
2025-12-16 14:38:03 +09:00
className={cn("w-full pr-8", !componentHeight && "h-8 text-xs sm:h-10 sm:text-sm")}
style={inputStyle}
/>
{displayValue && !disabled && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={handleClear}
2025-12-16 18:02:08 +09:00
className="absolute top-1/2 right-1 h-6 w-6 -translate-y-1/2 p-0"
>
<X className="h-3 w-3" />
</Button>
)}
</div>
2025-12-16 14:38:03 +09:00
{/* 모달 버튼: modal 또는 combo 모드일 때만 표시 */}
{(mode === "modal" || mode === "combo") && (
<Button
type="button"
onClick={handleOpenModal}
disabled={disabled}
2025-12-16 14:38:03 +09:00
className={cn(!componentHeight && "h-8 text-xs sm:h-10 sm:text-sm")}
style={inputStyle}
>
<Search className="h-4 w-4" />
</Button>
)}
</div>
{/* 추가 정보 표시 */}
{showAdditionalInfo && selectedData && additionalFields.length > 0 && (
2025-12-16 18:02:08 +09:00
<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>
<span>{selectedData[field] || "-"}</span>
</div>
))}
</div>
)}
2025-12-16 14:38:03 +09:00
{/* 검색 모달: modal 또는 combo 모드일 때만 렌더링 */}
{(mode === "modal" || mode === "combo") && (
<EntitySearchModal
open={modalOpen}
onOpenChange={setModalOpen}
tableName={tableName}
displayField={displayField}
valueField={valueField}
searchFields={searchFields}
filterCondition={filterCondition}
modalTitle={modalTitle}
modalColumns={modalColumns}
onSelect={handleSelect}
/>
)}
</div>
);
}