38개로 감소

This commit is contained in:
dohyeons
2025-09-29 17:24:06 +09:00
parent 6ce5fc84a8
commit 808a317ed0
5 changed files with 690 additions and 677 deletions

View File

@@ -1,7 +1,6 @@
import React, { useState, useEffect, useRef } from "react";
import { commonCodeApi } from "../../../api/commonCode";
import { tableTypeApi } from "../../../api/screen";
import React, { useState, useEffect, useRef, useMemo } from "react";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
import { useCodeOptions, useTableCodeCategory } from "@/hooks/queries/useCodes";
interface Option {
value: string;
@@ -26,210 +25,10 @@ export interface SelectBasicComponentProps {
[key: string]: any;
}
// 🚀 전역 상태 관리: 모든 컴포넌트가 공유하는 상태
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>; // 상태 변경 구독자들
}
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 ||
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;
};
// ✅ React Query를 사용하여 중복 요청 방지 및 자동 캐싱 처리
// - 동일한 queryKey에 대해서는 자동으로 중복 요청 제거
// - 10분 staleTime으로 적절한 캐시 관리
// - 30분 gcTime으로 메모리 효율성 확보
const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
component,
@@ -248,6 +47,22 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
value: externalValue, // 명시적으로 value prop 받기
...props
}) => {
// 🚨 최우선 디버깅: 컴포넌트가 실행되는지 확인
console.log("🚨🚨🚨 SelectBasicComponent 실행됨!!!", {
componentId: component?.id,
componentType: component?.type,
webType: component?.webType,
tableName: component?.tableName,
columnName: component?.columnName,
screenId,
timestamp: new Date().toISOString(),
});
// 브라우저 알림으로도 확인
if (typeof window !== "undefined" && !(window as any).selectBasicAlerted) {
(window as any).selectBasicAlerted = true;
alert("SelectBasicComponent가 실행되었습니다!");
}
const [isOpen, setIsOpen] = useState(false);
// webTypeConfig 또는 componentConfig 사용 (DynamicWebTypeRenderer 호환성)
@@ -257,23 +72,74 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
const [selectedValue, setSelectedValue] = useState(externalValue || config?.value || "");
const [selectedLabel, setSelectedLabel] = useState("");
console.log("🔍 SelectBasicComponent 초기화:", {
console.log("🔍 SelectBasicComponent 초기화 (React Query):", {
componentId: component.id,
externalValue,
componentConfigValue: componentConfig?.value,
webTypeConfigValue: (props as any).webTypeConfig?.value,
configValue: config?.value,
finalSelectedValue: externalValue || config?.value || "",
props: Object.keys(props),
tableName: component.tableName,
columnName: component.columnName,
staticCodeCategory: config?.codeCategory,
// React Query 디버깅 정보
timestamp: new Date().toISOString(),
mountCount: ++(window as any).selectMountCount || ((window as any).selectMountCount = 1),
});
const [codeOptions, setCodeOptions] = useState<Option[]>([]);
const [isLoadingCodes, setIsLoadingCodes] = useState(false);
const [dynamicCodeCategory, setDynamicCodeCategory] = useState<string | null>(null);
const [globalStateVersion, setGlobalStateVersion] = useState(0); // 전역 상태 변경 감지용
// 언마운트 시 로깅
useEffect(() => {
const componentId = component.id;
console.log(`🔍 [${componentId}] SelectBasicComponent 마운트됨`);
return () => {
console.log(`🔍 [${componentId}] SelectBasicComponent 언마운트됨`);
};
}, [component.id]);
const selectRef = useRef<HTMLDivElement>(null);
// 코드 카테고리 결정: 동적 카테고리 > 설정 카테고리
const codeCategory = dynamicCodeCategory || config?.codeCategory;
// 안정적인 쿼리 키를 위한 메모이제이션
const stableTableName = useMemo(() => component.tableName, [component.tableName]);
const stableColumnName = useMemo(() => component.columnName, [component.columnName]);
const staticCodeCategory = useMemo(() => config?.codeCategory, [config?.codeCategory]);
// 🚀 React Query: 테이블 코드 카테고리 조회
const { data: dynamicCodeCategory } = useTableCodeCategory(stableTableName, stableColumnName);
// 코드 카테고리 결정: 동적 카테고리 > 설정 카테고리 (메모이제이션)
const codeCategory = useMemo(() => {
const category = dynamicCodeCategory || staticCodeCategory;
console.log(`🔑 [${component.id}] 코드 카테고리 결정:`, {
dynamicCodeCategory,
staticCodeCategory,
finalCategory: category,
});
return category;
}, [dynamicCodeCategory, staticCodeCategory, component.id]);
// 🚀 React Query: 코드 옵션 조회 (안정적인 enabled 조건)
const isCodeCategoryValid = useMemo(() => {
return !!codeCategory && codeCategory !== "none";
}, [codeCategory]);
const {
options: codeOptions,
isLoading: isLoadingCodes,
isFetching,
} = useCodeOptions(codeCategory, isCodeCategoryValid);
// React Query 상태 디버깅
useEffect(() => {
console.log(`🎯 [${component.id}] React Query 상태:`, {
codeCategory,
isCodeCategoryValid,
codeOptionsLength: codeOptions.length,
isLoadingCodes,
isFetching,
cacheStatus: isFetching ? "FETCHING" : "FROM_CACHE",
});
}, [component.id, codeCategory, isCodeCategoryValid, codeOptions.length, isLoadingCodes, isFetching]);
// 외부 value prop 변경 시 selectedValue 업데이트
useEffect(() => {
@@ -293,109 +159,11 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
}
}, [externalValue, config?.value]);
// 🚀 전역 상태 구독 및 동기화
useEffect(() => {
const updateFromGlobalState = () => {
setGlobalStateVersion((prev) => prev + 1);
};
// ✅ React Query가 자동으로 처리하므로 복잡한 전역 상태 관리 제거
// - 캐싱: React Query가 자동 관리 (10분 staleTime, 30분 gcTime)
// - 중복 요청 방지: 동일한 queryKey에 대해 자동 중복 제거
// - 상태 동기화: 모든 컴포넌트가 같은 캐시 공유
// 전역 상태 변경 구독
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(() => {
@@ -438,41 +206,20 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
}
}, [selectedValue, codeOptions, config.options]);
// 클릭 이벤트 핸들러 (전역 상태 새로고침)
// 클릭 이벤트 핸들러 (React Query로 간소화)
const handleToggle = () => {
if (isDesignMode) return;
console.log(`🖱️ [${component.id}] 드롭다운 토글: ${isOpen}${!isOpen}`);
console.log(`🖱️ [${component.id}] 드롭다운 토글 (React Query): ${isOpen}${!isOpen}`);
console.log(`📊 [${component.id}] 현재 상태:`, {
isDesignMode,
codeCategory,
isLoadingCodes,
allOptionsLength: allOptions.length,
allOptions: allOptions.map((o) => ({ value: o.value, label: o.label })),
codeOptionsLength: codeOptions.length,
tableName: component.tableName,
columnName: component.columnName,
});
// 드롭다운을 열 때 전역 상태 새로고침
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();
}
}
// React Query가 자동으로 캐시 관리하므로 수동 새로고침 불필요
setIsOpen(!isOpen);
};
@@ -519,45 +266,19 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
};
}, [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]);
// ✅ React Query가 자동으로 처리하므로 수동 이벤트 리스너 불필요
// - refetchOnWindowFocus: true (기본값)
// - refetchOnReconnect: true (기본값)
// - staleTime으로 적절한 캐시 관리
// 모든 옵션 가져오기
const getAllOptions = () => {
const configOptions = config.options || [];
console.log(`🔧 [${component.id}] 옵션 병합:`, {
codeOptionsLength: codeOptions.length,
codeOptions: codeOptions.map((o) => ({ value: o.value, label: o.label })),
codeOptions: codeOptions.map((o: Option) => ({ value: o.value, label: o.label })),
configOptionsLength: configOptions.length,
configOptions: configOptions.map((o) => ({ value: o.value, label: o.label })),
configOptions: configOptions.map((o: Option) => ({ value: o.value, label: o.label })),
});
return [...codeOptions, ...configOptions];
};
@@ -649,7 +370,7 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
isDesignMode,
isLoadingCodes,
allOptionsLength: allOptions.length,
allOptions: allOptions.map((o) => ({ value: o.value, label: o.label })),
allOptions: allOptions.map((o: Option) => ({ value: o.value, label: o.label })),
});
return null;
})()}