import React, { useState, useEffect, useRef } from "react"; import { commonCodeApi } from "../../../api/commonCode"; import { tableTypeApi } from "../../../api/screen"; import { filterDOMProps } from "@/lib/utils/domPropsFilter"; interface Option { value: string; label: string; } export interface SelectBasicComponentProps { component: any; componentConfig: any; screenId?: number; onUpdate?: (field: string, value: any) => void; isSelected?: boolean; isDesignMode?: boolean; isInteractive?: boolean; onFormDataChange?: (fieldName: string, value: any) => void; className?: string; style?: React.CSSProperties; onClick?: () => void; onDragStart?: () => void; onDragEnd?: () => void; value?: any; // 외부에서 전달받는 값 [key: string]: any; } // 🚀 전역 상태 관리: 모든 컴포넌트가 공유하는 상태 interface GlobalState { tableCategories: Map; // tableName.columnName -> codeCategory codeOptions: Map; // codeCategory -> options activeRequests: Map>; // 진행 중인 요청들 subscribers: Set<() => void>; // 상태 변경 구독자들 } const globalState: GlobalState = { tableCategories: new Map(), codeOptions: new Map(), activeRequests: new Map(), subscribers: new Set(), }; // 전역 상태 변경 알림 const notifyStateChange = () => { globalState.subscribers.forEach((callback) => callback()); }; // 캐시 유효 시간 (5분) const CACHE_DURATION = 5 * 60 * 1000; // 🔧 전역 테이블 코드 카테고리 로딩 (중복 방지) const loadGlobalTableCodeCategory = async (tableName: string, columnName: string): Promise => { const key = `${tableName}.${columnName}`; // 이미 진행 중인 요청이 있으면 대기 if (globalState.activeRequests.has(`table_${key}`)) { try { await globalState.activeRequests.get(`table_${key}`); } catch (error) { console.error(`❌ 테이블 설정 로딩 대기 중 오류:`, error); } } // 캐시된 값이 있으면 반환 if (globalState.tableCategories.has(key)) { const cachedCategory = globalState.tableCategories.get(key); console.log(`✅ 캐시된 테이블 설정 사용: ${key} -> ${cachedCategory}`); return cachedCategory || null; } // 새로운 요청 생성 const request = (async () => { try { console.log(`🔍 테이블 코드 카테고리 조회: ${key}`); const columns = await tableTypeApi.getColumns(tableName); const targetColumn = columns.find((col) => col.columnName === columnName); const codeCategory = targetColumn?.codeCategory && targetColumn.codeCategory !== "none" ? targetColumn.codeCategory : null; // 전역 상태에 저장 globalState.tableCategories.set(key, codeCategory || ""); console.log(`✅ 테이블 설정 조회 완료: ${key} -> ${codeCategory}`); // 상태 변경 알림 notifyStateChange(); return codeCategory; } catch (error) { console.error(`❌ 테이블 코드 카테고리 조회 실패: ${key}`, error); return null; } finally { globalState.activeRequests.delete(`table_${key}`); } })(); globalState.activeRequests.set(`table_${key}`, request); return request; }; // 🔧 전역 코드 옵션 로딩 (중복 방지) const loadGlobalCodeOptions = async (codeCategory: string): Promise => { if (!codeCategory || codeCategory === "none") { return []; } // 이미 진행 중인 요청이 있으면 대기 if (globalState.activeRequests.has(`code_${codeCategory}`)) { try { await globalState.activeRequests.get(`code_${codeCategory}`); } catch (error) { console.error(`❌ 코드 옵션 로딩 대기 중 오류:`, error); } } // 캐시된 값이 유효하면 반환 const cached = globalState.codeOptions.get(codeCategory); if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { console.log(`✅ 캐시된 코드 옵션 사용: ${codeCategory} (${cached.options.length}개)`); return cached.options; } // 새로운 요청 생성 const request = (async () => { try { console.log(`🔄 코드 옵션 로딩: ${codeCategory}`); const response = await commonCodeApi.codes.getList(codeCategory, { isActive: true }); console.log(`🔍 [API 응답 원본] ${codeCategory}:`, { response, success: response.success, data: response.data, dataType: typeof response.data, isArray: Array.isArray(response.data), dataLength: response.data?.length, firstItem: response.data?.[0], }); if (response.success && response.data) { const options = response.data.map((code: any, index: number) => { console.log(`🔍 [코드 매핑] ${index}:`, { originalCode: code, codeKeys: Object.keys(code), values: Object.values(code), // 가능한 모든 필드 확인 code: code.code, codeName: code.codeName, name: code.name, label: code.label, // 대문자 버전 CODE: code.CODE, CODE_NAME: code.CODE_NAME, NAME: code.NAME, LABEL: code.LABEL, // 스네이크 케이스 code_name: code.code_name, code_value: code.code_value, // 기타 가능한 필드들 value: code.value, text: code.text, title: code.title, description: code.description, }); // 실제 값 찾기 시도 (우선순위 순) const actualValue = code.code || code.CODE || code.value || code.code_value || `code_${index}`; const actualLabel = code.codeName || code.code_name || // 스네이크 케이스 추가! code.name || code.CODE_NAME || code.NAME || code.label || code.LABEL || code.text || code.title || code.description || actualValue; console.log(`✨ [최종 매핑] ${index}:`, { actualValue, actualLabel, hasValue: !!actualValue, hasLabel: !!actualLabel, }); return { value: actualValue, label: actualLabel, }; }); console.log(`🔍 [최종 옵션 배열] ${codeCategory}:`, { optionsLength: options.length, options: options.map((opt, idx) => ({ index: idx, value: opt.value, label: opt.label, hasLabel: !!opt.label, hasValue: !!opt.value, })), }); // 전역 상태에 저장 globalState.codeOptions.set(codeCategory, { options, timestamp: Date.now(), }); console.log(`✅ 코드 옵션 로딩 완료: ${codeCategory} (${options.length}개)`); // 상태 변경 알림 notifyStateChange(); return options; } else { console.log(`⚠️ 빈 응답: ${codeCategory}`); return []; } } catch (error) { console.error(`❌ 코드 옵션 로딩 실패: ${codeCategory}`, error); return []; } finally { globalState.activeRequests.delete(`code_${codeCategory}`); } })(); globalState.activeRequests.set(`code_${codeCategory}`, request); return request; }; const SelectBasicComponent: React.FC = ({ component, componentConfig, screenId, onUpdate, isSelected = false, isDesignMode = false, isInteractive = false, onFormDataChange, className, style, onClick, onDragStart, onDragEnd, value: externalValue, // 명시적으로 value prop 받기 ...props }) => { const [isOpen, setIsOpen] = useState(false); // webTypeConfig 또는 componentConfig 사용 (DynamicWebTypeRenderer 호환성) const config = (props as any).webTypeConfig || componentConfig || {}; // 외부에서 전달받은 value가 있으면 우선 사용, 없으면 config.value 사용 const [selectedValue, setSelectedValue] = useState(externalValue || config?.value || ""); const [selectedLabel, setSelectedLabel] = useState(""); console.log("🔍 SelectBasicComponent 초기화:", { componentId: component.id, externalValue, componentConfigValue: componentConfig?.value, webTypeConfigValue: (props as any).webTypeConfig?.value, configValue: config?.value, finalSelectedValue: externalValue || config?.value || "", props: Object.keys(props), }); const [codeOptions, setCodeOptions] = useState([]); const [isLoadingCodes, setIsLoadingCodes] = useState(false); const [dynamicCodeCategory, setDynamicCodeCategory] = useState(null); const [globalStateVersion, setGlobalStateVersion] = useState(0); // 전역 상태 변경 감지용 const selectRef = useRef(null); // 코드 카테고리 결정: 동적 카테고리 > 설정 카테고리 const codeCategory = dynamicCodeCategory || config?.codeCategory; // 외부 value prop 변경 시 selectedValue 업데이트 useEffect(() => { const newValue = externalValue || config?.value || ""; // 값이 실제로 다른 경우에만 업데이트 (빈 문자열도 유효한 값으로 처리) if (newValue !== selectedValue) { console.log(`🔄 SelectBasicComponent value 업데이트: "${selectedValue}" → "${newValue}"`); console.log(`🔍 업데이트 조건 분석:`, { externalValue, componentConfigValue: componentConfig?.value, configValue: config?.value, newValue, selectedValue, shouldUpdate: newValue !== selectedValue, }); setSelectedValue(newValue); } }, [externalValue, config?.value]); // 🚀 전역 상태 구독 및 동기화 useEffect(() => { const updateFromGlobalState = () => { setGlobalStateVersion((prev) => prev + 1); }; // 전역 상태 변경 구독 globalState.subscribers.add(updateFromGlobalState); return () => { globalState.subscribers.delete(updateFromGlobalState); }; }, []); // 🔧 테이블 코드 카테고리 로드 (전역 상태 사용) const loadTableCodeCategory = async () => { if (!component.tableName || !component.columnName) return; try { console.log(`🔍 [${component.id}] 전역 테이블 코드 카테고리 조회`); const category = await loadGlobalTableCodeCategory(component.tableName, component.columnName); if (category !== dynamicCodeCategory) { console.log(`🔄 [${component.id}] 코드 카테고리 변경: ${dynamicCodeCategory} → ${category}`); setDynamicCodeCategory(category); } } catch (error) { console.error(`❌ [${component.id}] 테이블 코드 카테고리 조회 실패:`, error); } }; // 🔧 코드 옵션 로드 (전역 상태 사용) const loadCodeOptions = async (category: string) => { if (!category || category === "none") { setCodeOptions([]); setIsLoadingCodes(false); return; } try { setIsLoadingCodes(true); console.log(`🔄 [${component.id}] 전역 코드 옵션 로딩: ${category}`); const options = await loadGlobalCodeOptions(category); setCodeOptions(options); console.log(`✅ [${component.id}] 코드 옵션 업데이트 완료: ${category} (${options.length}개)`); } catch (error) { console.error(`❌ [${component.id}] 코드 옵션 로딩 실패:`, error); setCodeOptions([]); } finally { setIsLoadingCodes(false); } }; // 초기 테이블 코드 카테고리 로드 useEffect(() => { loadTableCodeCategory(); }, [component.tableName, component.columnName]); // 전역 상태 변경 시 동기화 useEffect(() => { if (component.tableName && component.columnName) { const key = `${component.tableName}.${component.columnName}`; const cachedCategory = globalState.tableCategories.get(key); if (cachedCategory && cachedCategory !== dynamicCodeCategory) { console.log(`🔄 [${component.id}] 전역 상태 동기화: ${dynamicCodeCategory} → ${cachedCategory}`); setDynamicCodeCategory(cachedCategory || null); } } }, [globalStateVersion, component.tableName, component.columnName]); // 코드 카테고리 변경 시 옵션 로드 useEffect(() => { if (codeCategory && codeCategory !== "none") { // 전역 캐시된 옵션부터 확인 const cached = globalState.codeOptions.get(codeCategory); if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { console.log(`🚀 [${component.id}] 전역 캐시 즉시 적용: ${codeCategory} (${cached.options.length}개)`); setCodeOptions(cached.options); setIsLoadingCodes(false); } else { loadCodeOptions(codeCategory); } } else { setCodeOptions([]); setIsLoadingCodes(false); } }, [codeCategory]); // 전역 상태에서 코드 옵션 변경 감지 useEffect(() => { if (codeCategory) { const cached = globalState.codeOptions.get(codeCategory); if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { if (JSON.stringify(cached.options) !== JSON.stringify(codeOptions)) { console.log(`🔄 [${component.id}] 전역 옵션 변경 감지: ${codeCategory}`); setCodeOptions(cached.options); } } } }, [globalStateVersion, codeCategory]); // 선택된 값에 따른 라벨 업데이트 useEffect(() => { const getAllOptions = () => { const configOptions = config.options || []; return [...codeOptions, ...configOptions]; }; const options = getAllOptions(); const selectedOption = options.find((option) => option.value === selectedValue); // 🎯 코드 타입의 경우 코드값과 코드명을 모두 고려하여 라벨 찾기 let newLabel = selectedOption?.label || ""; // selectedOption이 없고 selectedValue가 있다면, 코드명으로도 검색해보기 if (!selectedOption && selectedValue && codeOptions.length > 0) { // 1) selectedValue가 코드명인 경우 (예: "국내") const labelMatch = options.find((option) => option.label === selectedValue); if (labelMatch) { newLabel = labelMatch.label; console.log(`🔍 [${component.id}] 코드명으로 매치 발견: "${selectedValue}" → "${newLabel}"`); } else { // 2) selectedValue가 코드값인 경우라면 원래 로직대로 라벨을 찾되, 없으면 원값 표시 newLabel = selectedValue; // 코드값 그대로 표시 (예: "555") console.log(`🔍 [${component.id}] 코드값 원본 유지: "${selectedValue}"`); } } console.log(`🏷️ [${component.id}] 라벨 업데이트:`, { selectedValue, selectedOption: selectedOption ? { value: selectedOption.value, label: selectedOption.label } : null, newLabel, optionsCount: options.length, allOptionsValues: options.map((o) => o.value), allOptionsLabels: options.map((o) => o.label), }); if (newLabel !== selectedLabel) { setSelectedLabel(newLabel); } }, [selectedValue, codeOptions, config.options]); // 클릭 이벤트 핸들러 (전역 상태 새로고침) const handleToggle = () => { if (isDesignMode) return; console.log(`🖱️ [${component.id}] 드롭다운 토글: ${isOpen} → ${!isOpen}`); console.log(`📊 [${component.id}] 현재 상태:`, { isDesignMode, isLoadingCodes, allOptionsLength: allOptions.length, allOptions: allOptions.map((o) => ({ value: o.value, label: o.label })), }); // 드롭다운을 열 때 전역 상태 새로고침 if (!isOpen) { console.log(`🖱️ [${component.id}] 셀렉트박스 클릭 - 전역 상태 새로고침`); // 테이블 설정 캐시 무효화 후 재로드 if (component.tableName && component.columnName) { const key = `${component.tableName}.${component.columnName}`; globalState.tableCategories.delete(key); // 현재 코드 카테고리의 캐시도 무효화 if (dynamicCodeCategory) { globalState.codeOptions.delete(dynamicCodeCategory); console.log(`🗑️ [${component.id}] 코드 옵션 캐시 무효화: ${dynamicCodeCategory}`); // 강제로 새로운 API 호출 수행 console.log(`🔄 [${component.id}] 강제 코드 옵션 재로드 시작: ${dynamicCodeCategory}`); loadCodeOptions(dynamicCodeCategory); } loadTableCodeCategory(); } } setIsOpen(!isOpen); }; // 옵션 선택 핸들러 const handleOptionSelect = (value: string, label: string) => { setSelectedValue(value); setSelectedLabel(label); setIsOpen(false); // 디자인 모드에서의 컴포넌트 속성 업데이트 if (onUpdate) { onUpdate("value", value); } // 인터랙티브 모드에서 폼 데이터 업데이트 (TextInputComponent와 동일한 로직) if (isInteractive && onFormDataChange && component.columnName) { console.log(`📤 SelectBasicComponent -> onFormDataChange 호출: ${component.columnName} = "${value}"`); onFormDataChange(component.columnName, value); } else { console.log("❌ SelectBasicComponent onFormDataChange 조건 미충족:", { isInteractive, hasOnFormDataChange: !!onFormDataChange, hasColumnName: !!component.columnName, }); } console.log(`✅ [${component.id}] 옵션 선택:`, { value, label }); }; // 외부 클릭 시 드롭다운 닫기 useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (selectRef.current && !selectRef.current.contains(event.target as Node)) { setIsOpen(false); } }; if (isOpen) { document.addEventListener("mousedown", handleClickOutside); } return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [isOpen]); // 🚀 실시간 업데이트를 위한 이벤트 리스너 useEffect(() => { const handleFocus = () => { console.log(`👁️ [${component.id}] 윈도우 포커스 - 전역 상태 새로고침`); if (component.tableName && component.columnName) { const key = `${component.tableName}.${component.columnName}`; globalState.tableCategories.delete(key); // 캐시 무효화 loadTableCodeCategory(); } }; const handleVisibilityChange = () => { if (!document.hidden) { console.log(`👁️ [${component.id}] 페이지 가시성 변경 - 전역 상태 새로고침`); if (component.tableName && component.columnName) { const key = `${component.tableName}.${component.columnName}`; globalState.tableCategories.delete(key); // 캐시 무효화 loadTableCodeCategory(); } } }; window.addEventListener("focus", handleFocus); document.addEventListener("visibilitychange", handleVisibilityChange); return () => { window.removeEventListener("focus", handleFocus); document.removeEventListener("visibilitychange", handleVisibilityChange); }; }, [component.tableName, component.columnName]); // 모든 옵션 가져오기 const getAllOptions = () => { const configOptions = config.options || []; console.log(`🔧 [${component.id}] 옵션 병합:`, { codeOptionsLength: codeOptions.length, codeOptions: codeOptions.map((o) => ({ value: o.value, label: o.label })), configOptionsLength: configOptions.length, configOptions: configOptions.map((o) => ({ value: o.value, label: o.label })), }); return [...codeOptions, ...configOptions]; }; const allOptions = getAllOptions(); const placeholder = componentConfig.placeholder || "선택하세요"; // DOM props에서 React 전용 props 필터링 const { component: _component, componentConfig: _componentConfig, screenId: _screenId, onUpdate: _onUpdate, isSelected: _isSelected, isDesignMode: _isDesignMode, className: _className, style: _style, onClick: _onClick, onDragStart: _onDragStart, onDragEnd: _onDragEnd, ...otherProps } = props; const safeDomProps = filterDOMProps(otherProps); return (
{/* 라벨 렌더링 */} {component.label && component.style?.labelDisplay !== false && ( )} {/* 커스텀 셀렉트 박스 */}
{selectedLabel || placeholder} {/* 드롭다운 아이콘 */}
{/* 드롭다운 옵션 */} {isOpen && !isDesignMode && (
{(() => { console.log(`🎨 [${component.id}] 드롭다운 렌더링:`, { isOpen, isDesignMode, isLoadingCodes, allOptionsLength: allOptions.length, allOptions: allOptions.map((o) => ({ value: o.value, label: o.label })), }); return null; })()} {isLoadingCodes ? (
로딩 중...
) : allOptions.length > 0 ? ( allOptions.map((option, index) => (
handleOptionSelect(option.value, option.label)} > {option.label || option.value || `옵션 ${index + 1}`}
)) ) : (
옵션이 없습니다
)}
)}
); }; // Wrapper 컴포넌트 (기존 호환성을 위해) export const SelectBasicWrapper = SelectBasicComponent; // 기본 export export { SelectBasicComponent };