"use client"; import React, { useState, useEffect, useRef } from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; 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 { dynamicFormApi } from "@/lib/api/dynamicForm"; import { cascadingRelationApi } from "@/lib/api/cascadingRelation"; export function EntitySearchInputComponent({ tableName, displayField, valueField, searchFields = [displayField], mode: modeProp, uiMode, // EntityConfigPanel에서 저장되는 값 placeholder = "검색...", disabled = false, filterCondition = {}, value, onChange, modalTitle = "검색", modalColumns = [], showAdditionalInfo = false, additionalFields = [], className, style, // 연쇄관계 props cascadingRelationCode, parentValue: parentValueProp, parentFieldId, formData, // 다중선택 props multiple: multipleProp, // 추가 props component, isInteractive, onFormDataChange, }: EntitySearchInputProps & { uiMode?: string; component?: any; isInteractive?: boolean; onFormDataChange?: (fieldName: string, value: any) => void; webTypeConfig?: any; // 웹타입 설정 (연쇄관계 등) }) { // uiMode가 있으면 우선 사용, 없으면 modeProp 사용, 기본값 "combo" const mode = (uiMode || modeProp || "combo") as "select" | "modal" | "combo" | "autocomplete"; // 다중선택 및 연쇄관계 설정 (props > webTypeConfig > componentConfig 순서) const config = component?.componentConfig || component?.webTypeConfig || {}; const isMultiple = multipleProp ?? config.multiple ?? false; // 연쇄관계 설정 추출 const effectiveCascadingRelationCode = cascadingRelationCode || config.cascadingRelationCode; // 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); const [selectOpen, setSelectOpen] = useState(false); const [displayValue, setDisplayValue] = useState(""); const [selectedData, setSelectedData] = useState(null); const [options, setOptions] = useState([]); const [isLoadingOptions, setIsLoadingOptions] = useState(false); const [optionsLoaded, setOptionsLoaded] = useState(false); // 다중선택 상태 (콤마로 구분된 값들) const [selectedValues, setSelectedValues] = useState([]); const [selectedDataList, setSelectedDataList] = useState([]); // 연쇄관계 상태 const [cascadingOptions, setCascadingOptions] = useState([]); const [isCascadingLoading, setIsCascadingLoading] = useState(false); const previousParentValue = useRef(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; // filterCondition을 문자열로 변환하여 비교 (객체 참조 문제 해결) const filterConditionKey = JSON.stringify(filterCondition || {}); // 연쇄관계가 설정된 경우: 부모 값이 변경되면 자식 옵션 로드 (자식 역할일 때만) 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) { loadOptions(); setOptionsLoaded(true); } }, [mode, tableName, filterConditionKey, optionsLoaded, shouldApplyCascading]); const loadOptions = async () => { if (!tableName) return; setIsLoadingOptions(true); try { const response = await dynamicFormApi.getTableData(tableName, { page: 1, pageSize: 100, // 최대 100개까지 로드 filters: filterCondition, }); 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; // value가 변경되면 표시값 업데이트 (외래키 값으로 데이터 조회) useEffect(() => { const loadDisplayValue = async () => { // value가 없으면 초기화 if (!value) { setDisplayValue(""); setSelectedData(null); return; } // 이미 selectedData가 있고 value와 일치하면 표시값만 업데이트 if (selectedData && String(selectedData[valueField]) === String(value)) { setDisplayValue(selectedData[displayField] || ""); return; } // select 모드에서 options가 로드된 경우 먼저 옵션에서 찾기 if (mode === "select" && effectiveOptions.length > 0) { // 타입 변환하여 비교 (숫자 vs 문자열 문제 해결) const found = effectiveOptions.find((opt) => String(opt[valueField]) === String(value)); if (found) { setSelectedData(found); setDisplayValue(found[displayField] || ""); console.log("✅ [EntitySearchInput] 옵션에서 초기값 찾음:", { value, found }); return; } // 옵션에서 찾지 못한 경우 API로 조회 진행 console.log("⚠️ [EntitySearchInput] 옵션에서 찾지 못함, API로 조회:", { value, optionsCount: effectiveOptions.length, }); } // API로 해당 데이터 조회 if (tableName) { 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)); } } }; 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); if (isInteractive && onFormDataChange && component?.columnName) { onFormDataChange(component.columnName, joinedValue || null); console.log("📤 EntitySearchInput (remove) -> onFormDataChange:", component.columnName, joinedValue); } }; const handleClear = () => { if (isMultiple) { setSelectedValues([]); setSelectedDataList([]); onChange?.(null, []); } else { setDisplayValue(""); setSelectedData(null); onChange?.(null, null); } if (isInteractive && onFormDataChange && component?.columnName) { onFormDataChange(component.columnName, null); console.log("📤 EntitySearchInput -> onFormDataChange (clear):", component.columnName, null); } }; const handleOpenModal = () => { if (!disabled) { setModalOpen(true); } }; const handleSelectOption = (option: EntitySearchResult) => { handleSelect(option[valueField], option); // 다중선택이 아닌 경우에만 드롭다운 닫기 if (!isMultiple) { setSelectOpen(false); } }; // 높이 계산 (style에서 height가 있으면 사용, 없으면 기본값) const componentHeight = style?.height; const inputStyle: React.CSSProperties = componentHeight ? { height: componentHeight } : {}; // select 모드: 검색 가능한 드롭다운 if (mode === "select") { // 다중선택 모드 if (isMultiple) { return (
{/* 라벨 렌더링 */} {component?.label && component?.style?.labelDisplay !== false && ( )} {/* 선택된 항목들 표시 (태그 형식) */}
!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 ( {label} {!disabled && ( )} ); }) ) : ( {isLoading ? "로딩 중..." : shouldApplyCascading && !parentValue ? "상위 항목을 먼저 선택하세요" : placeholder} )}
{/* 옵션 드롭다운 */} {selectOpen && !disabled && (
항목을 찾을 수 없습니다. {effectiveOptions.map((option, index) => { const isSelected = selectedValues.includes(String(option[valueField])); return ( handleSelectOption(option)} className="text-xs sm:text-sm" >
{option[displayField]} {valueField !== displayField && ( {option[valueField]} )}
); })}
{/* 닫기 버튼 */}
)} {/* 외부 클릭 시 닫기 */} {selectOpen &&
setSelectOpen(false)} />}
); } // 단일선택 모드 (기존 로직) return (
{/* 라벨 렌더링 */} {component?.label && component?.style?.labelDisplay !== false && ( )} 항목을 찾을 수 없습니다. {effectiveOptions.map((option, index) => ( handleSelectOption(option)} className="text-xs sm:text-sm" >
{option[displayField]} {valueField !== displayField && ( {option[valueField]} )}
))}
{/* 추가 정보 표시 */} {showAdditionalInfo && selectedData && additionalFields.length > 0 && (
{additionalFields.map((field) => (
{field}: {selectedData[field] || "-"}
))}
)}
); } // modal, combo, autocomplete 모드 // 다중선택 모드 if (isMultiple) { return (
{/* 라벨 렌더링 */} {component?.label && component?.style?.labelDisplay !== false && ( )} {/* 선택된 항목들 표시 (태그 형식) + 검색 버튼 */}
{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 ( {label} {!disabled && ( )} ); }) ) : ( {placeholder} )}
{/* 모달 버튼: modal 또는 combo 모드일 때만 표시 */} {(mode === "modal" || mode === "combo") && ( )}
{/* 검색 모달: modal 또는 combo 모드일 때만 렌더링 */} {(mode === "modal" || mode === "combo") && ( )}
); } // 단일선택 모드 (기존 로직) return (
{/* 라벨 렌더링 */} {component?.label && component?.style?.labelDisplay !== false && ( )} {/* 입력 필드 */}
setDisplayValue(e.target.value)} placeholder={placeholder} disabled={disabled} readOnly={mode === "modal" || mode === "combo"} className={cn("w-full pr-8", !componentHeight && "h-8 text-xs sm:h-10 sm:text-sm")} style={inputStyle} /> {displayValue && !disabled && ( )}
{/* 모달 버튼: modal 또는 combo 모드일 때만 표시 */} {(mode === "modal" || mode === "combo") && ( )}
{/* 추가 정보 표시 */} {showAdditionalInfo && selectedData && additionalFields.length > 0 && (
{additionalFields.map((field) => (
{field}: {selectedData[field] || "-"}
))}
)} {/* 검색 모달: modal 또는 combo 모드일 때만 렌더링 */} {(mode === "modal" || mode === "combo") && ( )}
); }