"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 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"; // 연쇄관계 설정 추출 (webTypeConfig 또는 component.componentConfig에서) const config = component?.componentConfig || {}; const effectiveCascadingRelationCode = cascadingRelationCode || config.cascadingRelationCode; const effectiveParentFieldId = parentFieldId || 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 [cascadingOptions, setCascadingOptions] = useState([]); const [isCascadingLoading, setIsCascadingLoading] = useState(false); const previousParentValue = useRef(null); // 부모 필드 값 결정 (직접 전달 또는 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 () => { if (value && selectedData) { // 이미 selectedData가 있으면 표시값만 업데이트 setDisplayValue(selectedData[displayField] || ""); } else if (value && mode === "select" && effectiveOptions.length > 0) { // select 모드에서 value가 있고 options가 로드된 경우 const found = effectiveOptions.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); } }; loadDisplayValue(); }, [value, displayField, effectiveOptions, 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); console.log("📤 EntitySearchInput -> onFormDataChange:", component.columnName, newValue); } }; const handleClear = () => { setDisplayValue(""); setSelectedData(null); onChange?.(null, null); // 🆕 onFormDataChange 호출 (formData에서 값 제거) 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); setSelectOpen(false); }; // 높이 계산 (style에서 height가 있으면 사용, 없으면 기본값) const componentHeight = style?.height; const inputStyle: React.CSSProperties = componentHeight ? { height: componentHeight } : {}; // select 모드: 검색 가능한 드롭다운 if (mode === "select") { 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 모드 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") && ( )}
); }