"use client"; import React, { useState, useEffect, useRef } from "react"; import { Input } from "@/components/ui/input"; import { X, Loader2, ChevronDown } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useEntitySearch } from "../entity-search-input/useEntitySearch"; import { EntitySearchResult } from "../entity-search-input/types"; import { cn } from "@/lib/utils"; import { AutocompleteSearchInputConfig } from "./types"; import { ComponentRendererProps } from "../../DynamicComponentRenderer"; export interface AutocompleteSearchInputProps extends ComponentRendererProps { config?: AutocompleteSearchInputConfig; tableName?: string; displayField?: string; valueField?: string; searchFields?: string[]; filterCondition?: Record; placeholder?: string; showAdditionalInfo?: boolean; additionalFields?: string[]; } export function AutocompleteSearchInputComponent({ component, config, tableName: propTableName, displayField: propDisplayField, valueField: propValueField, searchFields: propSearchFields, filterCondition = {}, placeholder: propPlaceholder, disabled = false, value, onChange, className, isInteractive = false, onFormDataChange, formData, }: AutocompleteSearchInputProps) { // config prop 우선, 없으면 개별 prop 사용 const tableName = config?.tableName || propTableName || ""; const displayField = config?.displayField || propDisplayField || ""; const displayFields = config?.displayFields || (displayField ? [displayField] : []); // 다중 표시 필드 const displaySeparator = config?.displaySeparator || " → "; // 구분자 // valueField 결정: fieldMappings 기반으로 추론 (config.valueField가 fieldMappings에 없으면 무시) const getValueField = () => { // fieldMappings가 있으면 그 안에서 추론 (가장 신뢰할 수 있는 소스) if (config?.fieldMappings && config.fieldMappings.length > 0) { // config.valueField가 fieldMappings의 sourceField에 있으면 사용 if (config?.valueField) { const hasValueFieldInMappings = config.fieldMappings.some( (m: any) => m.sourceField === config.valueField ); if (hasValueFieldInMappings) { return config.valueField; } // fieldMappings에 없으면 무시하고 추론 } // _code 또는 _id로 끝나는 필드 우선 (보통 PK나 코드 필드) const codeMapping = config.fieldMappings.find( (m: any) => m.sourceField?.endsWith("_code") || m.sourceField?.endsWith("_id") ); if (codeMapping) { return codeMapping.sourceField; } // 없으면 첫 번째 매핑 사용 return config.fieldMappings[0].sourceField || ""; } // fieldMappings가 없으면 기존 방식 if (config?.valueField) return config.valueField; if (propValueField) return propValueField; return ""; }; const valueField = getValueField(); const searchFields = config?.searchFields || propSearchFields || displayFields; // 검색 필드도 다중 표시 필드 사용 const placeholder = config?.placeholder || propPlaceholder || "검색..."; // 다중 필드 값을 조합하여 표시 문자열 생성 const getDisplayValue = (item: EntitySearchResult): string => { if (displayFields.length > 1) { // 여러 필드를 구분자로 조합 const values = displayFields .map((field) => item[field]) .filter((v) => v !== null && v !== undefined && v !== "") .map((v) => String(v)); return values.join(displaySeparator); } // 단일 필드 return item[displayField] || ""; }; const [inputValue, setInputValue] = useState(""); const [isOpen, setIsOpen] = useState(false); const [selectedData, setSelectedData] = useState(null); const containerRef = useRef(null); const { searchText, setSearchText, results, loading, clearSearch } = useEntitySearch({ tableName, searchFields, filterCondition, }); // 선택된 데이터를 ref로도 유지 (리렌더링 시 초기화 방지) const selectedDataRef = useRef(null); const inputValueRef = useRef(""); const initialValueLoadedRef = useRef(null); // 초기값 로드 추적 // formData에서 현재 값 가져오기 (isInteractive 모드) // 우선순위: 1) component.columnName, 2) fieldMappings에서 valueField에 매핑된 targetField const getCurrentValue = () => { if (!isInteractive || !formData) { return value; } // 1. component.columnName으로 직접 바인딩된 경우 if (component?.columnName && formData[component.columnName] !== undefined) { return formData[component.columnName]; } // 2. fieldMappings에서 valueField와 매핑된 targetField에서 값 가져오기 if (config?.fieldMappings && Array.isArray(config.fieldMappings)) { const valueFieldMapping = config.fieldMappings.find( (mapping: any) => mapping.sourceField === valueField ); if (valueFieldMapping) { const targetField = valueFieldMapping.targetField || valueFieldMapping.targetColumn; if (targetField && formData[targetField] !== undefined) { return formData[targetField]; } } } return value; }; const currentValue = getCurrentValue(); // selectedData 변경 시 ref도 업데이트 useEffect(() => { if (selectedData) { selectedDataRef.current = selectedData; inputValueRef.current = inputValue; } }, [selectedData, inputValue]); // 리렌더링 시 ref에서 값 복원 useEffect(() => { if (!selectedData && selectedDataRef.current) { setSelectedData(selectedDataRef.current); setInputValue(inputValueRef.current); } }, []); // 초기값이 있을 때 해당 값의 표시 텍스트를 조회하여 설정 useEffect(() => { const loadInitialDisplayValue = async () => { // 이미 로드된 값이거나, 값이 없거나, 이미 선택된 데이터가 있으면 스킵 if (!currentValue || selectedData || selectedDataRef.current) { return; } // 이미 같은 값을 로드한 적이 있으면 스킵 if (initialValueLoadedRef.current === currentValue) { return; } // 테이블명과 필드 정보가 없으면 스킵 if (!tableName || !valueField) { return; } console.log("🔄 AutocompleteSearchInput 초기값 로드:", { currentValue, tableName, valueField, displayFields, }); try { // API를 통해 해당 값의 표시 텍스트 조회 const { apiClient } = await import("@/lib/api/client"); const filterConditionWithValue = { ...filterCondition, [valueField]: currentValue, }; const params = new URLSearchParams({ searchText: "", searchFields: searchFields.join(","), filterCondition: JSON.stringify(filterConditionWithValue), page: "1", limit: "10", }); const response = await apiClient.get<{ success: boolean; data: EntitySearchResult[] }>( `/entity-search/${tableName}?${params.toString()}` ); if (response.data.success && response.data.data && response.data.data.length > 0) { const matchedItem = response.data.data.find((item: EntitySearchResult) => String(item[valueField]) === String(currentValue) ); if (matchedItem) { const displayText = getDisplayValue(matchedItem); console.log("✅ 초기값 표시 텍스트 로드 성공:", { currentValue, displayText, matchedItem, }); setSelectedData(matchedItem); setInputValue(displayText); selectedDataRef.current = matchedItem; inputValueRef.current = displayText; initialValueLoadedRef.current = currentValue; } } } catch (error) { console.error("❌ 초기값 표시 텍스트 로드 실패:", error); } }; loadInitialDisplayValue(); }, [currentValue, tableName, valueField, displayFields, filterCondition, searchFields, selectedData]); // value가 변경되면 표시값 업데이트 - 단, selectedData가 있으면 유지 useEffect(() => { // selectedData가 있으면 표시값 유지 (사용자가 방금 선택한 경우) if (selectedData || selectedDataRef.current) { return; } if (!currentValue) { setInputValue(""); initialValueLoadedRef.current = null; // 값이 없어지면 초기화 } }, [currentValue, selectedData]); // 외부 클릭 감지 useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setIsOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); const handleInputChange = (e: React.ChangeEvent) => { const newValue = e.target.value; setInputValue(newValue); setSearchText(newValue); setIsOpen(true); }; const handleSelect = (item: EntitySearchResult) => { setSelectedData(item); setInputValue(getDisplayValue(item)); console.log("🔍 AutocompleteSearchInput handleSelect:", { item, valueField, value: item[valueField], config, isInteractive, hasOnFormDataChange: !!onFormDataChange, columnName: component?.columnName, }); // isInteractive 모드에서만 저장 if (isInteractive && onFormDataChange) { // 필드 매핑 처리 if (config?.fieldMappings && Array.isArray(config.fieldMappings)) { console.log("📋 필드 매핑 처리 시작:", config.fieldMappings); config.fieldMappings.forEach((mapping: any, index: number) => { const targetField = mapping.targetField || mapping.targetColumn; console.log(` 매핑 ${index + 1}:`, { sourceField: mapping.sourceField, targetField, label: mapping.label, }); if (mapping.sourceField && targetField) { const sourceValue = item[mapping.sourceField]; console.log(` 값: ${mapping.sourceField} = ${sourceValue}`); if (sourceValue !== undefined) { console.log(` ✅ 저장: ${targetField} = ${sourceValue}`); onFormDataChange(targetField, sourceValue); } else { console.warn(` ⚠️ sourceField "${mapping.sourceField}"의 값이 undefined입니다`); } } else { console.warn(` ⚠️ 매핑 불완전: sourceField=${mapping.sourceField}, targetField=${targetField}`); } }); } // 기본 필드 저장 (columnName이 설정된 경우) if (component?.columnName) { console.log(`💾 기본 필드 저장: ${component.columnName} = ${item[valueField]}`); onFormDataChange(component.columnName, item[valueField]); } } // onChange 콜백 호출 (호환성) onChange?.(item[valueField], item); setIsOpen(false); }; const handleClear = () => { setInputValue(""); setSelectedData(null); onChange?.(null, null); setIsOpen(false); }; const handleInputFocus = () => { // 포커스 시 항상 검색 실행 (빈 값이면 전체 목록) if (!selectedData) { setSearchText(inputValue || ""); setIsOpen(true); } }; return (
{/* 입력 필드 */}
{loading && ( )} {inputValue && !disabled && ( )}
{/* 드롭다운 결과 */} {isOpen && (results.length > 0 || loading) && (
{loading && results.length === 0 ? (
검색 중...
) : results.length === 0 ? (
검색 결과가 없습니다
) : (
{results.map((item, index) => ( ))}
)}
)}
); }