2025-09-15 15:38:48 +09:00
|
|
|
import React, { useState, useEffect, useRef } from "react";
|
|
|
|
|
import { commonCodeApi } from "../../../api/commonCode";
|
|
|
|
|
import { tableTypeApi } from "../../../api/screen";
|
2025-09-19 02:15:21 +09:00
|
|
|
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
2025-09-11 18:38:28 +09:00
|
|
|
|
2025-09-15 15:38:48 +09:00
|
|
|
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;
|
2025-09-19 02:15:21 +09:00
|
|
|
isInteractive?: boolean;
|
|
|
|
|
onFormDataChange?: (fieldName: string, value: any) => void;
|
2025-09-15 15:38:48 +09:00
|
|
|
className?: string;
|
|
|
|
|
style?: React.CSSProperties;
|
|
|
|
|
onClick?: () => void;
|
|
|
|
|
onDragStart?: () => void;
|
|
|
|
|
onDragEnd?: () => void;
|
2025-09-19 02:15:21 +09:00
|
|
|
value?: any; // 외부에서 전달받는 값
|
2025-09-15 15:38:48 +09:00
|
|
|
[key: string]: any;
|
|
|
|
|
}
|
2025-09-11 18:38:28 +09:00
|
|
|
|
2025-09-15 15:38:48 +09:00
|
|
|
// 🚀 전역 상태 관리: 모든 컴포넌트가 공유하는 상태
|
|
|
|
|
interface GlobalState {
|
|
|
|
|
tableCategories: Map<string, string>; // tableName.columnName -> codeCategory
|
|
|
|
|
codeOptions: Map<string, { options: Option[]; timestamp: number }>; // codeCategory -> options
|
|
|
|
|
activeRequests: Map<string, Promise<any>>; // 진행 중인 요청들
|
|
|
|
|
subscribers: Set<() => void>; // 상태 변경 구독자들
|
2025-09-11 18:38:28 +09:00
|
|
|
}
|
|
|
|
|
|
2025-09-15 15:38:48 +09:00
|
|
|
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<string | null> => {
|
|
|
|
|
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<Option[]> => {
|
|
|
|
|
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 ||
|
2025-09-19 02:15:21 +09:00
|
|
|
code.code_name || // 스네이크 케이스 추가!
|
2025-09-15 15:38:48 +09:00
|
|
|
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<SelectBasicComponentProps> = ({
|
2025-09-11 18:38:28 +09:00
|
|
|
component,
|
2025-09-15 15:38:48 +09:00
|
|
|
componentConfig,
|
|
|
|
|
screenId,
|
|
|
|
|
onUpdate,
|
2025-09-11 18:38:28 +09:00
|
|
|
isSelected = false,
|
2025-09-15 15:38:48 +09:00
|
|
|
isDesignMode = false,
|
2025-09-19 02:15:21 +09:00
|
|
|
isInteractive = false,
|
|
|
|
|
onFormDataChange,
|
2025-09-15 15:38:48 +09:00
|
|
|
className,
|
|
|
|
|
style,
|
2025-09-11 18:38:28 +09:00
|
|
|
onClick,
|
|
|
|
|
onDragStart,
|
|
|
|
|
onDragEnd,
|
2025-09-19 02:15:21 +09:00
|
|
|
value: externalValue, // 명시적으로 value prop 받기
|
2025-09-11 18:38:28 +09:00
|
|
|
...props
|
|
|
|
|
}) => {
|
2025-09-15 15:38:48 +09:00
|
|
|
const [isOpen, setIsOpen] = useState(false);
|
2025-09-19 02:15:21 +09:00
|
|
|
|
|
|
|
|
// webTypeConfig 또는 componentConfig 사용 (DynamicWebTypeRenderer 호환성)
|
|
|
|
|
const config = (props as any).webTypeConfig || componentConfig || {};
|
|
|
|
|
|
|
|
|
|
// 외부에서 전달받은 value가 있으면 우선 사용, 없으면 config.value 사용
|
|
|
|
|
const [selectedValue, setSelectedValue] = useState(externalValue || config?.value || "");
|
2025-09-15 15:38:48 +09:00
|
|
|
const [selectedLabel, setSelectedLabel] = useState("");
|
2025-09-19 02:15:21 +09:00
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
});
|
2025-09-15 15:38:48 +09:00
|
|
|
const [codeOptions, setCodeOptions] = useState<Option[]>([]);
|
|
|
|
|
const [isLoadingCodes, setIsLoadingCodes] = useState(false);
|
|
|
|
|
const [dynamicCodeCategory, setDynamicCodeCategory] = useState<string | null>(null);
|
|
|
|
|
const [globalStateVersion, setGlobalStateVersion] = useState(0); // 전역 상태 변경 감지용
|
|
|
|
|
const selectRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
// 코드 카테고리 결정: 동적 카테고리 > 설정 카테고리
|
2025-09-19 02:15:21 +09:00
|
|
|
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]);
|
2025-09-15 15:38:48 +09:00
|
|
|
|
|
|
|
|
// 🚀 전역 상태 구독 및 동기화
|
|
|
|
|
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);
|
|
|
|
|
}
|
2025-09-11 18:38:28 +09:00
|
|
|
};
|
|
|
|
|
|
2025-09-15 15:38:48 +09:00
|
|
|
// 🔧 코드 옵션 로드 (전역 상태 사용)
|
|
|
|
|
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 = () => {
|
2025-09-19 02:15:21 +09:00
|
|
|
const configOptions = config.options || [];
|
2025-09-15 15:38:48 +09:00
|
|
|
return [...codeOptions, ...configOptions];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const options = getAllOptions();
|
|
|
|
|
const selectedOption = options.find((option) => option.value === selectedValue);
|
2025-09-19 15:22:25 +09:00
|
|
|
|
|
|
|
|
// 🎯 코드 타입의 경우 코드값과 코드명을 모두 고려하여 라벨 찾기
|
|
|
|
|
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),
|
|
|
|
|
});
|
2025-09-15 15:38:48 +09:00
|
|
|
|
|
|
|
|
if (newLabel !== selectedLabel) {
|
|
|
|
|
setSelectedLabel(newLabel);
|
|
|
|
|
}
|
2025-09-19 02:15:21 +09:00
|
|
|
}, [selectedValue, codeOptions, config.options]);
|
2025-09-15 15:38:48 +09:00
|
|
|
|
|
|
|
|
// 클릭 이벤트 핸들러 (전역 상태 새로고침)
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-11 18:38:28 +09:00
|
|
|
|
2025-09-15 15:38:48 +09:00
|
|
|
setIsOpen(!isOpen);
|
2025-09-11 18:38:28 +09:00
|
|
|
};
|
|
|
|
|
|
2025-09-15 15:38:48 +09:00
|
|
|
// 옵션 선택 핸들러
|
|
|
|
|
const handleOptionSelect = (value: string, label: string) => {
|
|
|
|
|
setSelectedValue(value);
|
|
|
|
|
setSelectedLabel(label);
|
|
|
|
|
setIsOpen(false);
|
|
|
|
|
|
2025-09-19 02:15:21 +09:00
|
|
|
// 디자인 모드에서의 컴포넌트 속성 업데이트
|
2025-09-15 15:38:48 +09:00
|
|
|
if (onUpdate) {
|
|
|
|
|
onUpdate("value", value);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 02:15:21 +09:00
|
|
|
// 인터랙티브 모드에서 폼 데이터 업데이트 (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,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-15 15:38:48 +09:00
|
|
|
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 = () => {
|
2025-09-19 02:15:21 +09:00
|
|
|
const configOptions = config.options || [];
|
2025-09-15 15:38:48 +09:00
|
|
|
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 || "선택하세요";
|
2025-09-11 18:38:28 +09:00
|
|
|
|
2025-09-19 02:15:21 +09:00
|
|
|
// 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);
|
|
|
|
|
|
2025-09-11 18:38:28 +09:00
|
|
|
return (
|
2025-09-15 15:38:48 +09:00
|
|
|
<div
|
|
|
|
|
ref={selectRef}
|
|
|
|
|
className={`relative w-full ${className || ""}`}
|
|
|
|
|
style={style}
|
|
|
|
|
onClick={onClick}
|
|
|
|
|
onDragStart={onDragStart}
|
|
|
|
|
onDragEnd={onDragEnd}
|
2025-09-19 02:15:21 +09:00
|
|
|
{...safeDomProps}
|
2025-09-15 15:38:48 +09:00
|
|
|
>
|
2025-09-19 02:15:21 +09:00
|
|
|
{/* 라벨 렌더링 */}
|
|
|
|
|
{component.label && component.style?.labelDisplay !== false && (
|
|
|
|
|
<label
|
|
|
|
|
style={{
|
|
|
|
|
position: "absolute",
|
|
|
|
|
top: "-25px",
|
|
|
|
|
left: "0px",
|
|
|
|
|
fontSize: component.style?.labelFontSize || "14px",
|
|
|
|
|
color: component.style?.labelColor || "#374151",
|
|
|
|
|
fontWeight: "500",
|
|
|
|
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
|
|
|
|
...(isInteractive && component.style ? component.style : {}),
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{component.label}
|
|
|
|
|
{component.required && <span style={{ color: "#ef4444" }}>*</span>}
|
|
|
|
|
</label>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-09-15 15:38:48 +09:00
|
|
|
{/* 커스텀 셀렉트 박스 */}
|
|
|
|
|
<div
|
|
|
|
|
className={`flex w-full cursor-pointer items-center justify-between rounded-md border border-gray-300 bg-white px-3 py-2 ${isDesignMode ? "pointer-events-none" : "hover:border-gray-400"} ${isSelected ? "ring-2 ring-blue-500" : ""} ${isOpen ? "border-blue-500" : ""} `}
|
|
|
|
|
onClick={handleToggle}
|
|
|
|
|
style={{
|
|
|
|
|
pointerEvents: isDesignMode ? "none" : "auto",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<span className={selectedLabel ? "text-gray-900" : "text-gray-500"}>{selectedLabel || placeholder}</span>
|
|
|
|
|
|
|
|
|
|
{/* 드롭다운 아이콘 */}
|
|
|
|
|
<svg
|
|
|
|
|
className={`h-4 w-4 transition-transform ${isOpen ? "rotate-180" : ""}`}
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
>
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 드롭다운 옵션 */}
|
|
|
|
|
{isOpen && !isDesignMode && (
|
|
|
|
|
<div
|
2025-09-19 02:15:21 +09:00
|
|
|
className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md border border-gray-300 bg-white shadow-lg"
|
2025-09-11 18:38:28 +09:00
|
|
|
style={{
|
2025-09-15 15:38:48 +09:00
|
|
|
backgroundColor: "white",
|
|
|
|
|
color: "black",
|
2025-09-19 02:15:21 +09:00
|
|
|
zIndex: 99999, // 더 높은 z-index로 설정
|
2025-09-11 18:38:28 +09:00
|
|
|
}}
|
|
|
|
|
>
|
2025-09-15 15:38:48 +09:00
|
|
|
{(() => {
|
|
|
|
|
console.log(`🎨 [${component.id}] 드롭다운 렌더링:`, {
|
|
|
|
|
isOpen,
|
|
|
|
|
isDesignMode,
|
|
|
|
|
isLoadingCodes,
|
|
|
|
|
allOptionsLength: allOptions.length,
|
|
|
|
|
allOptions: allOptions.map((o) => ({ value: o.value, label: o.label })),
|
|
|
|
|
});
|
|
|
|
|
return null;
|
|
|
|
|
})()}
|
|
|
|
|
{isLoadingCodes ? (
|
|
|
|
|
<div className="bg-white px-3 py-2 text-gray-900">로딩 중...</div>
|
|
|
|
|
) : allOptions.length > 0 ? (
|
|
|
|
|
allOptions.map((option, index) => (
|
|
|
|
|
<div
|
|
|
|
|
key={`${option.value}-${index}`}
|
|
|
|
|
className="cursor-pointer bg-white px-3 py-2 text-gray-900 hover:bg-gray-100"
|
|
|
|
|
style={{
|
|
|
|
|
color: "black",
|
|
|
|
|
backgroundColor: "white",
|
|
|
|
|
minHeight: "32px",
|
|
|
|
|
border: "1px solid #e5e7eb",
|
|
|
|
|
}}
|
|
|
|
|
onClick={() => handleOptionSelect(option.value, option.label)}
|
|
|
|
|
>
|
|
|
|
|
{option.label || option.value || `옵션 ${index + 1}`}
|
|
|
|
|
</div>
|
|
|
|
|
))
|
|
|
|
|
) : (
|
|
|
|
|
<div className="bg-white px-3 py-2 text-gray-900">옵션이 없습니다</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-09-11 18:38:28 +09:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-15 15:38:48 +09:00
|
|
|
// Wrapper 컴포넌트 (기존 호환성을 위해)
|
|
|
|
|
export const SelectBasicWrapper = SelectBasicComponent;
|
|
|
|
|
|
|
|
|
|
// 기본 export
|
|
|
|
|
export { SelectBasicComponent };
|