From 6ce5fc84a87857def8cf011b35075f8f2c459e8b Mon Sep 17 00:00:00 2001 From: dohyeons Date: Mon, 29 Sep 2025 16:55:39 +0900 Subject: [PATCH 1/3] Revert React Query changes --- frontend/hooks/queries/useCodes.ts | 169 ----------------------------- 1 file changed, 169 deletions(-) delete mode 100644 frontend/hooks/queries/useCodes.ts diff --git a/frontend/hooks/queries/useCodes.ts b/frontend/hooks/queries/useCodes.ts deleted file mode 100644 index 48b20641..00000000 --- a/frontend/hooks/queries/useCodes.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { commonCodeApi } from "@/lib/api/commonCode"; -import { queryKeys } from "@/lib/queryKeys"; -import type { CodeFilter, CreateCodeData, UpdateCodeData } from "@/lib/schemas/commonCode"; - -/** - * 코드 목록 조회 훅 - */ -export function useCodes(categoryCode: string, filters?: CodeFilter) { - return useQuery({ - queryKey: queryKeys.codes.list(categoryCode, filters), - queryFn: () => commonCodeApi.codes.getList(categoryCode, filters), - select: (data) => data.data || [], - enabled: !!categoryCode, // categoryCode가 있을 때만 실행 - }); -} - -/** - * 코드 생성 뮤테이션 훅 - */ -export function useCreateCode() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ categoryCode, data }: { categoryCode: string; data: CreateCodeData }) => - commonCodeApi.codes.create(categoryCode, data), - onSuccess: (_, variables) => { - // 해당 카테고리의 모든 코드 관련 쿼리 무효화 (일반 목록 + 무한 스크롤) - queryClient.invalidateQueries({ - queryKey: queryKeys.codes.all, - }); - // 무한 스크롤 쿼리도 명시적으로 무효화 - queryClient.invalidateQueries({ - queryKey: queryKeys.codes.infiniteList(variables.categoryCode), - }); - }, - onError: (error) => { - console.error("코드 생성 실패:", error); - }, - }); -} - -/** - * 코드 수정 뮤테이션 훅 - */ -export function useUpdateCode() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ - categoryCode, - codeValue, - data, - }: { - categoryCode: string; - codeValue: string; - data: UpdateCodeData; - }) => commonCodeApi.codes.update(categoryCode, codeValue, data), - onSuccess: (_, variables) => { - // 해당 코드 상세 쿼리 무효화 - queryClient.invalidateQueries({ - queryKey: queryKeys.codes.detail(variables.categoryCode, variables.codeValue), - }); - // 해당 카테고리의 모든 코드 관련 쿼리 무효화 (일반 목록 + 무한 스크롤) - queryClient.invalidateQueries({ - queryKey: queryKeys.codes.all, - }); - // 무한 스크롤 쿼리도 명시적으로 무효화 - queryClient.invalidateQueries({ - queryKey: queryKeys.codes.infiniteList(variables.categoryCode), - }); - }, - onError: (error) => { - console.error("코드 수정 실패:", error); - }, - }); -} - -/** - * 코드 삭제 뮤테이션 훅 - */ -export function useDeleteCode() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ categoryCode, codeValue }: { categoryCode: string; codeValue: string }) => - commonCodeApi.codes.delete(categoryCode, codeValue), - onSuccess: (_, variables) => { - // 해당 코드 관련 쿼리 무효화 및 캐시 제거 - queryClient.invalidateQueries({ - queryKey: queryKeys.codes.all, - }); - // 무한 스크롤 쿼리도 명시적으로 무효화 - queryClient.invalidateQueries({ - queryKey: queryKeys.codes.infiniteList(variables.categoryCode), - }); - queryClient.removeQueries({ - queryKey: queryKeys.codes.detail(variables.categoryCode, variables.codeValue), - }); - }, - onError: (error) => { - console.error("코드 삭제 실패:", error); - }, - }); -} - -/** - * 코드 순서 변경 뮤테이션 훅 - */ -export function useReorderCodes() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ - categoryCode, - codes, - }: { - categoryCode: string; - codes: Array<{ codeValue: string; sortOrder: number }>; - }) => commonCodeApi.codes.reorder(categoryCode, codes), - onMutate: async ({ categoryCode, codes }) => { - // 진행 중인 쿼리들을 취소해서 optimistic update가 덮어쓰이지 않도록 함 - await queryClient.cancelQueries({ queryKey: queryKeys.codes.list(categoryCode) }); - - // 이전 데이터를 백업 - const previousCodes = queryClient.getQueryData(queryKeys.codes.list(categoryCode)); - - // Optimistic update: 새로운 순서로 즉시 업데이트 - if (previousCodes && (previousCodes as any).data && Array.isArray((previousCodes as any).data)) { - const previousCodesArray = (previousCodes as any).data; - - // 기존 데이터를 복사하고 sort_order만 업데이트 - const updatedCodes = [...previousCodesArray].map((code: any) => { - const newCodeData = codes.find((c) => c.codeValue === code.code_value); - return newCodeData ? { ...code, sort_order: newCodeData.sortOrder } : code; - }); - - // sort_order로 정렬 - updatedCodes.sort((a: any, b: any) => a.sort_order - b.sort_order); - - // API 응답 형태로 캐시에 저장 (기존 구조 유지) - queryClient.setQueryData(queryKeys.codes.list(categoryCode), { - ...(previousCodes as any), - data: updatedCodes, - }); - } - - // 롤백용 데이터 반환 - return { previousCodes }; - }, - onError: (error, variables, context) => { - console.error("코드 순서 변경 실패:", error); - // 에러 시 이전 데이터로 롤백 - if (context?.previousCodes) { - queryClient.setQueryData(queryKeys.codes.list(variables.categoryCode), context.previousCodes); - } - }, - onSettled: (_, __, variables) => { - // 성공/실패와 관계없이 최종적으로 서버 데이터로 동기화 - queryClient.invalidateQueries({ - queryKey: queryKeys.codes.all, - }); - // 무한 스크롤 쿼리도 명시적으로 무효화 - queryClient.invalidateQueries({ - queryKey: queryKeys.codes.infiniteList(variables.categoryCode), - }); - }, - }); -} From 808a317ed05b87d385be38e8d30991297be75c28 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Mon, 29 Sep 2025 17:24:06 +0900 Subject: [PATCH 2/3] =?UTF-8?q?38=EA=B0=9C=EB=A1=9C=20=EA=B0=90=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(main)/screens/[screenId]/page.tsx | 4 +- frontend/hooks/queries/useCodes.ts | 228 +++++++ .../lib/registry/DynamicWebTypeRenderer.tsx | 16 +- .../select-basic/SelectBasicComponent.tsx | 477 +++---------- .../table-list/TableListComponent.tsx | 642 ++++++++++-------- 5 files changed, 690 insertions(+), 677 deletions(-) create mode 100644 frontend/hooks/queries/useCodes.ts diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index dd48479d..395a2be8 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -338,13 +338,13 @@ export default function ScreenViewPage() { }, onFormDataChange: (fieldName, value) => { console.log(`🎯 page.tsx onFormDataChange 호출: ${fieldName} = "${value}"`); - console.log(`📋 현재 formData:`, formData); + console.log("📋 현재 formData:", formData); setFormData((prev) => { const newFormData = { ...prev, [fieldName]: value, }; - console.log(`📝 업데이트된 formData:`, newFormData); + console.log("📝 업데이트된 formData:", newFormData); return newFormData; }); }, diff --git a/frontend/hooks/queries/useCodes.ts b/frontend/hooks/queries/useCodes.ts new file mode 100644 index 00000000..14c6ee5d --- /dev/null +++ b/frontend/hooks/queries/useCodes.ts @@ -0,0 +1,228 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { commonCodeApi } from "@/lib/api/commonCode"; +import { tableTypeApi } from "@/lib/api/screen"; +import { useMemo } from "react"; + +// Query Keys +export const queryKeys = { + codes: { + all: ["codes"] as const, + list: (categoryCode: string) => ["codes", "list", categoryCode] as const, + options: (categoryCode: string) => ["codes", "options", categoryCode] as const, + detail: (categoryCode: string, codeValue: string) => + ["codes", "detail", categoryCode, codeValue] as const, + infiniteList: (categoryCode: string, filters?: any) => + ["codes", "infiniteList", categoryCode, filters] as const, + }, + tables: { + all: ["tables"] as const, + columns: (tableName: string) => ["tables", "columns", tableName] as const, + codeCategory: (tableName: string, columnName: string) => + ["tables", "codeCategory", tableName, columnName] as const, + }, + categories: { + all: ["categories"] as const, + list: (filters?: any) => ["categories", "list", filters] as const, + }, +}; + +// 테이블 컬럼의 코드 카테고리 조회 +export function useTableCodeCategory(tableName?: string, columnName?: string) { + return useQuery({ + queryKey: queryKeys.tables.codeCategory(tableName || "", columnName || ""), + queryFn: async () => { + if (!tableName || !columnName) return null; + + console.log(`🔍 [React Query] 테이블 코드 카테고리 조회: ${tableName}.${columnName}`); + const columns = await tableTypeApi.getColumns(tableName); + const targetColumn = columns.find((col) => col.columnName === columnName); + + const codeCategory = targetColumn?.codeCategory && targetColumn.codeCategory !== "none" + ? targetColumn.codeCategory + : null; + + console.log(`✅ [React Query] 테이블 코드 카테고리 결과: ${tableName}.${columnName} -> ${codeCategory}`); + return codeCategory; + }, + enabled: !!(tableName && columnName), + staleTime: 10 * 60 * 1000, // 10분 캐시 + gcTime: 30 * 60 * 1000, // 30분 가비지 컬렉션 + }); +} + +// 코드 옵션 조회 (select용) +export function useCodeOptions(codeCategory?: string, enabled: boolean = true) { + const query = useQuery({ + queryKey: queryKeys.codes.options(codeCategory || ""), + queryFn: async () => { + if (!codeCategory || codeCategory === "none") return []; + + console.log(`🔍 [React Query] 코드 옵션 조회: ${codeCategory}`); + const response = await commonCodeApi.codes.getList(codeCategory, { isActive: true }); + + if (response.success && response.data) { + const options = response.data.map((code: any) => { + const actualValue = code.code || code.CODE || code.value || code.code_value || code.codeValue; + 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; + + return { + value: actualValue, + label: actualLabel, + }; + }); + + console.log(`✅ [React Query] 코드 옵션 결과: ${codeCategory} (${options.length}개)`); + return options; + } + + return []; + }, + enabled: enabled && !!(codeCategory && codeCategory !== "none"), + staleTime: 10 * 60 * 1000, // 10분 캐시 + gcTime: 30 * 60 * 1000, // 30분 가비지 컬렉션 + }); + + return { + options: query.data || [], + isLoading: query.isLoading, + isFetching: query.isFetching, + error: query.error, + refetch: query.refetch, + }; +} + +// 코드 생성 +export function useCreateCode() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ categoryCode, data }: { categoryCode: string; data: any }) => + commonCodeApi.codes.create(categoryCode, data), + onSuccess: (_, variables) => { + // 해당 카테고리의 모든 코드 관련 쿼리 무효화 + queryClient.invalidateQueries({ + queryKey: queryKeys.codes.all, + }); + // 무한 스크롤 쿼리도 명시적으로 무효화 + queryClient.invalidateQueries({ + queryKey: queryKeys.codes.infiniteList(variables.categoryCode), + }); + }, + }); +} + +// 코드 수정 +export function useUpdateCode() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + categoryCode, + codeValue, + data, + }: { + categoryCode: string; + codeValue: string; + data: any; + }) => commonCodeApi.codes.update(categoryCode, codeValue, data), + onSuccess: (_, variables) => { + // 해당 코드 상세 쿼리 무효화 + queryClient.invalidateQueries({ + queryKey: queryKeys.codes.detail(variables.categoryCode, variables.codeValue), + }); + // 해당 카테고리의 모든 코드 관련 쿼리 무효화 + queryClient.invalidateQueries({ + queryKey: queryKeys.codes.all, + }); + // 무한 스크롤 쿼리도 명시적으로 무효화 + queryClient.invalidateQueries({ + queryKey: queryKeys.codes.infiniteList(variables.categoryCode), + }); + }, + }); +} + +// 코드 삭제 +export function useDeleteCode() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ categoryCode, codeValue }: { categoryCode: string; codeValue: string }) => + commonCodeApi.codes.delete(categoryCode, codeValue), + onSuccess: (_, variables) => { + // 해당 코드 관련 쿼리 무효화 및 캐시 제거 + queryClient.invalidateQueries({ + queryKey: queryKeys.codes.all, + }); + // 무한 스크롤 쿼리도 명시적으로 무효화 + queryClient.invalidateQueries({ + queryKey: queryKeys.codes.infiniteList(variables.categoryCode), + }); + queryClient.removeQueries({ + queryKey: queryKeys.codes.detail(variables.categoryCode, variables.codeValue), + }); + }, + }); +} + +// 코드 순서 변경 +export function useReorderCodes() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + categoryCode, + codes, + }: { + categoryCode: string; + codes: Array<{ codeValue: string; sortOrder: number }>; + }) => commonCodeApi.codes.reorder(categoryCode, codes), + onMutate: async ({ categoryCode, codes }) => { + // 진행 중인 쿼리들을 취소해서 optimistic update가 덮어쓰이지 않도록 함 + await queryClient.cancelQueries({ queryKey: queryKeys.codes.list(categoryCode) }); + + // 이전 데이터를 백업 + const previousCodes = queryClient.getQueryData(queryKeys.codes.list(categoryCode)); + + // Optimistic update: 새로운 순서로 즉시 업데이트 + if (previousCodes && Array.isArray((previousCodes as any).data)) { + const previousCodesArray = (previousCodes as any).data; + + // 기존 데이터를 복사하고 sort_order만 업데이트 + const updatedCodes = [...previousCodesArray].map((code: any) => { + const newCodeData = codes.find((c) => c.codeValue === code.code_value); + return newCodeData ? { ...code, sort_order: newCodeData.sortOrder } : code; + }); + + // 순서대로 정렬 + updatedCodes.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0)); + + // API 응답 형태로 캐시에 저장 (기존 구조 유지) + queryClient.setQueryData(queryKeys.codes.list(categoryCode), { + ...(previousCodes as any), + data: updatedCodes, + }); + } + + return { previousCodes }; + }, + onError: (err, variables, context) => { + // 에러 시 이전 데이터로 롤백 + if (context?.previousCodes) { + queryClient.setQueryData(queryKeys.codes.list(variables.categoryCode), context.previousCodes); + } + }, + onSettled: (_, __, variables) => { + // 성공/실패와 관계없이 최종적으로 서버 데이터로 동기화 + queryClient.invalidateQueries({ + queryKey: queryKeys.codes.all, + }); + // 무한 스크롤 쿼리도 명시적으로 무효화 + queryClient.invalidateQueries({ + queryKey: queryKeys.codes.infiniteList(variables.categoryCode), + }); + }, + }); +} \ No newline at end of file diff --git a/frontend/lib/registry/DynamicWebTypeRenderer.tsx b/frontend/lib/registry/DynamicWebTypeRenderer.tsx index b49f5129..dc84dcf9 100644 --- a/frontend/lib/registry/DynamicWebTypeRenderer.tsx +++ b/frontend/lib/registry/DynamicWebTypeRenderer.tsx @@ -51,15 +51,15 @@ export const DynamicWebTypeRenderer: React.FC = ({ if (dbWebType?.component_name) { try { console.log(`웹타입 "${webType}" → DB 지정 컴포넌트 "${dbWebType.component_name}" 사용`); - console.log(`DB 웹타입 정보:`, dbWebType); - + console.log("DB 웹타입 정보:", dbWebType); + // FileWidget의 경우 FileUploadComponent 직접 사용 if (dbWebType.component_name === "FileWidget" || webType === "file") { const { FileUploadComponent } = require("@/lib/registry/components/file-upload/FileUploadComponent"); - console.log(`✅ FileWidget → FileUploadComponent 사용`); + console.log("✅ FileWidget → FileUploadComponent 사용"); return ; } - + // 다른 컴포넌트들은 기존 로직 유지 // const ComponentByName = getWidgetComponentByName(dbWebType.component_name); // console.log(`컴포넌트 "${dbWebType.component_name}" 성공적으로 로드됨:`, ComponentByName); @@ -78,7 +78,7 @@ export const DynamicWebTypeRenderer: React.FC = ({ // 파일 웹타입의 경우 FileUploadComponent 직접 사용 if (webType === "file") { const { FileUploadComponent } = require("@/lib/registry/components/file-upload/FileUploadComponent"); - console.log(`✅ 파일 웹타입 → FileUploadComponent 사용`); + console.log("✅ 파일 웹타입 → FileUploadComponent 사용"); return ; } @@ -106,14 +106,14 @@ export const DynamicWebTypeRenderer: React.FC = ({ // 3순위: 웹타입명으로 자동 매핑 (폴백) try { console.warn(`웹타입 "${webType}" → 자동 매핑 폴백 사용`); - + // 파일 웹타입의 경우 FileUploadComponent 직접 사용 (최종 폴백) if (webType === "file") { const { FileUploadComponent } = require("@/lib/registry/components/file-upload/FileUploadComponent"); - console.log(`✅ 폴백: 파일 웹타입 → FileUploadComponent 사용`); + console.log("✅ 폴백: 파일 웹타입 → FileUploadComponent 사용"); return ; } - + // const FallbackComponent = getWidgetComponentByWebType(webType); // return ; console.warn(`웹타입 "${webType}" 폴백 기능 임시 비활성화`); diff --git a/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx b/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx index dedba749..7d87a4a9 100644 --- a/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx +++ b/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx @@ -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; // 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; -}; +// ✅ React Query를 사용하여 중복 요청 방지 및 자동 캐싱 처리 +// - 동일한 queryKey에 대해서는 자동으로 중복 요청 제거 +// - 10분 staleTime으로 적절한 캐시 관리 +// - 30분 gcTime으로 메모리 효율성 확보 const SelectBasicComponent: React.FC = ({ component, @@ -248,6 +47,22 @@ const SelectBasicComponent: React.FC = ({ 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 = ({ 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([]); - const [isLoadingCodes, setIsLoadingCodes] = useState(false); - const [dynamicCodeCategory, setDynamicCodeCategory] = useState(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(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 = ({ } }, [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 = ({ } }, [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 = ({ }; }, [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 = ({ 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; })()} diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 03754e7f..ac2f49b8 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -6,6 +6,35 @@ import { WebType } from "@/types/common"; import { tableTypeApi } from "@/lib/api/screen"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { codeCache } from "@/lib/caching/codeCache"; + +// 전역 테이블 캐시 +const tableColumnCache = new Map(); +const tableInfoCache = new Map(); +const TABLE_CACHE_TTL = 5 * 60 * 1000; // 5분 + +// 캐시 정리 함수 +const cleanupTableCache = () => { + const now = Date.now(); + + // 컬럼 캐시 정리 + for (const [key, entry] of tableColumnCache.entries()) { + if (now - entry.timestamp > TABLE_CACHE_TTL) { + tableColumnCache.delete(key); + } + } + + // 테이블 정보 캐시 정리 + for (const [key, entry] of tableInfoCache.entries()) { + if (now - entry.timestamp > TABLE_CACHE_TTL) { + tableInfoCache.delete(key); + } + } +}; + +// 주기적으로 캐시 정리 (10분마다) +if (typeof window !== "undefined") { + setInterval(cleanupTableCache, 10 * 60 * 1000); +} import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization"; import { Button } from "@/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; @@ -90,12 +119,12 @@ export const TableListComponent: React.FC = ({ } as TableListConfig; // 🎨 동적 색상 설정 (속성편집 모달의 "색상" 필드와 연동) - const buttonColor = component.style?.labelColor || '#3b83f6'; // 기본 파란색 - const buttonTextColor = component.config?.buttonTextColor || '#ffffff'; + const buttonColor = component.style?.labelColor || "#3b83f6"; // 기본 파란색 + const buttonTextColor = component.config?.buttonTextColor || "#ffffff"; const buttonStyle = { backgroundColor: buttonColor, color: buttonTextColor, - borderColor: buttonColor + borderColor: buttonColor, }; // 디버깅 로그 제거 (성능상 이유로) @@ -183,7 +212,7 @@ export const TableListComponent: React.FC = ({ // 🎯 강제로 그리드 컬럼수에 맞는 크기 적용 (디자인 모드에서는 더 큰 크기 허용) const gridColumns = component.gridColumns || 1; let calculatedWidth: string; - + if (isDesignMode) { // 디자인 모드에서는 더 큰 최소 크기 적용 if (gridColumns === 1) { @@ -208,8 +237,7 @@ export const TableListComponent: React.FC = ({ } } - // 디버깅 로그 제거 (성능상 이유로) - + // 디버깅 로그 제거 (성능상 이유로) // 스타일 계산 (컨테이너에 맞춤) const componentStyle: React.CSSProperties = { @@ -244,63 +272,99 @@ export const TableListComponent: React.FC = ({ // 자동 높이로 테이블 전체를 감쌈 } - // 컬럼 라벨 정보 가져오기 + // 컬럼 라벨 정보 가져오기 (캐싱 적용) const fetchColumnLabels = async () => { if (!tableConfig.selectedTable) return; - try { - const response = await tableTypeApi.getColumns(tableConfig.selectedTable); - // API 응답 구조 확인 및 컬럼 배열 추출 - const columns = Array.isArray(response) ? response : (response as any).columns || []; - const labels: Record = {}; - const meta: Record = {}; + // 캐시 확인 + const cacheKey = tableConfig.selectedTable; + const cached = tableColumnCache.get(cacheKey); + const now = Date.now(); - columns.forEach((column: any) => { - // 🎯 Entity 조인된 컬럼의 경우 표시 컬럼명 사용 - let displayLabel = column.displayName || column.columnName; + let columns: any[] = []; - // Entity 타입인 경우 - if (column.webType === "entity") { - // 우선 기준 테이블의 컬럼 라벨을 사용 - displayLabel = column.displayName || column.columnName; - console.log( - `🎯 Entity 조인 컬럼 라벨 설정: ${column.columnName} → "${displayLabel}" (기준 테이블 라벨 사용)`, - ); - } + if (cached && now - cached.timestamp < TABLE_CACHE_TTL) { + console.log(`🚀 테이블 컬럼 캐시 사용: ${cacheKey}`); + columns = cached.columns; + } else { + try { + console.log(`🔄 테이블 컬럼 API 호출: ${cacheKey}`); + const response = await tableTypeApi.getColumns(tableConfig.selectedTable); + // API 응답 구조 확인 및 컬럼 배열 추출 + columns = Array.isArray(response) ? response : (response as any).columns || []; - labels[column.columnName] = displayLabel; - // 🎯 웹타입과 코드카테고리 정보 저장 - meta[column.columnName] = { - webType: column.webType, - codeCategory: column.codeCategory, - }; - }); - - setColumnLabels(labels); - setColumnMeta(meta); - console.log("🔍 컬럼 라벨 설정 완료:", labels); - console.log("🔍 컬럼 메타정보 설정 완료:", meta); - } catch (error) { - console.log("컬럼 라벨 정보를 가져올 수 없습니다:", error); + // 캐시 저장 + tableColumnCache.set(cacheKey, { columns, timestamp: now }); + console.log(`✅ 테이블 컬럼 캐시 저장: ${cacheKey} (${columns.length}개 컬럼)`); + } catch (error) { + console.log("컬럼 라벨 정보를 가져올 수 없습니다:", error); + return; + } } + + const labels: Record = {}; + const meta: Record = {}; + + columns.forEach((column: any) => { + // 🎯 Entity 조인된 컬럼의 경우 표시 컬럼명 사용 + let displayLabel = column.displayName || column.columnName; + + // Entity 타입인 경우 + if (column.webType === "entity") { + // 우선 기준 테이블의 컬럼 라벨을 사용 + displayLabel = column.displayName || column.columnName; + console.log(`🎯 Entity 조인 컬럼 라벨 설정: ${column.columnName} → "${displayLabel}" (기준 테이블 라벨 사용)`); + } + + labels[column.columnName] = displayLabel; + // 🎯 웹타입과 코드카테고리 정보 저장 + meta[column.columnName] = { + webType: column.webType, + codeCategory: column.codeCategory, + }; + }); + + setColumnLabels(labels); + setColumnMeta(meta); + console.log("🔍 컬럼 라벨 설정 완료:", labels); + console.log("🔍 컬럼 메타정보 설정 완료:", meta); }; // 🎯 전역 코드 캐시 사용으로 함수 제거 (codeCache.convertCodeToName 사용) - // 테이블 라벨명 가져오기 + // 테이블 라벨명 가져오기 (캐싱 적용) const fetchTableLabel = async () => { if (!tableConfig.selectedTable) return; - try { - const tables = await tableTypeApi.getTables(); - const table = tables.find((t: any) => t.tableName === tableConfig.selectedTable); - if (table && table.displayName && table.displayName !== table.tableName) { - setTableLabel(table.displayName); - } else { + // 캐시 확인 + const cacheKey = "all_tables"; + const cached = tableInfoCache.get(cacheKey); + const now = Date.now(); + + let tables: any[] = []; + + if (cached && now - cached.timestamp < TABLE_CACHE_TTL) { + console.log(`🚀 테이블 정보 캐시 사용: ${cacheKey}`); + tables = cached.tables; + } else { + try { + console.log(`🔄 테이블 정보 API 호출: ${cacheKey}`); + tables = await tableTypeApi.getTables(); + + // 캐시 저장 + tableInfoCache.set(cacheKey, { tables, timestamp: now }); + console.log(`✅ 테이블 정보 캐시 저장: ${cacheKey} (${tables.length}개 테이블)`); + } catch (error) { + console.log("테이블 라벨 정보를 가져올 수 없습니다:", error); setTableLabel(tableConfig.selectedTable); + return; } - } catch (error) { - console.log("테이블 라벨 정보를 가져올 수 없습니다:", error); + } + + const table = tables.find((t: any) => t.tableName === tableConfig.selectedTable); + if (table && table.displayName && table.displayName !== table.tableName) { + setTableLabel(table.displayName); + } else { setTableLabel(tableConfig.selectedTable); } }; @@ -659,7 +723,7 @@ export const TableListComponent: React.FC = ({ // 페이지 변경 const handlePageChange = (newPage: number) => { setCurrentPage(newPage); - + // 상세설정에 현재 페이지 정보 알림 (필요한 경우) if (onConfigChange && tableConfig.pagination) { // console.log("📤 테이블에서 페이지 변경을 상세설정에 알림:", newPage); @@ -844,7 +908,7 @@ export const TableListComponent: React.FC = ({ setLocalPageSize(tableConfig.pagination.pageSize); setCurrentPage(1); // 페이지를 1로 리셋 } - + // 현재 페이지 동기화 (상세설정에서 페이지를 직접 변경한 경우) if (tableConfig.pagination?.currentPage && tableConfig.pagination.currentPage !== currentPage) { // console.log("🔄 상세설정에서 현재 페이지 변경 감지:", tableConfig.pagination.currentPage); @@ -987,7 +1051,14 @@ export const TableListComponent: React.FC = ({ return null; } - return ; + return ( + + ); }; // 체크박스 셀 렌더링 @@ -1066,41 +1137,41 @@ export const TableListComponent: React.FC = ({ const handleRowDragStart = (e: React.DragEvent, row: any, index: number) => { setIsDragging(true); setDraggedRowIndex(index); - + // 드래그 데이터에 그리드 정보 포함 const dragData = { ...row, - _dragType: 'table-row', + _dragType: "table-row", _gridSize: { width: 4, height: 1 }, // 기본 그리드 크기 (4칸 너비, 1칸 높이) - _snapToGrid: true + _snapToGrid: true, }; - - e.dataTransfer.setData('application/json', JSON.stringify(dragData)); - e.dataTransfer.effectAllowed = 'copy'; // move 대신 copy로 변경 - + + e.dataTransfer.setData("application/json", JSON.stringify(dragData)); + e.dataTransfer.effectAllowed = "copy"; // move 대신 copy로 변경 + // 드래그 이미지를 더 깔끔하게 const dragElement = e.currentTarget as HTMLElement; - + // 커스텀 드래그 이미지 생성 (저장 버튼과 어울리는 스타일) - const dragImage = document.createElement('div'); - dragImage.style.position = 'absolute'; - dragImage.style.top = '-1000px'; - dragImage.style.left = '-1000px'; - dragImage.style.background = 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)'; - dragImage.style.color = 'white'; - dragImage.style.padding = '12px 16px'; - dragImage.style.borderRadius = '8px'; - dragImage.style.fontSize = '14px'; - dragImage.style.fontWeight = '600'; - dragImage.style.boxShadow = '0 4px 12px rgba(59, 130, 246, 0.4)'; - dragImage.style.display = 'flex'; - dragImage.style.alignItems = 'center'; - dragImage.style.gap = '8px'; - dragImage.style.minWidth = '200px'; - dragImage.style.whiteSpace = 'nowrap'; - + const dragImage = document.createElement("div"); + dragImage.style.position = "absolute"; + dragImage.style.top = "-1000px"; + dragImage.style.left = "-1000px"; + dragImage.style.background = "linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)"; + dragImage.style.color = "white"; + dragImage.style.padding = "12px 16px"; + dragImage.style.borderRadius = "8px"; + dragImage.style.fontSize = "14px"; + dragImage.style.fontWeight = "600"; + dragImage.style.boxShadow = "0 4px 12px rgba(59, 130, 246, 0.4)"; + dragImage.style.display = "flex"; + dragImage.style.alignItems = "center"; + dragImage.style.gap = "8px"; + dragImage.style.minWidth = "200px"; + dragImage.style.whiteSpace = "nowrap"; + // 아이콘과 텍스트 추가 - const firstValue = Object.values(row)[0] || 'Row'; + const firstValue = Object.values(row)[0] || "Row"; dragImage.innerHTML = `
4×1
`; - + document.body.appendChild(dragImage); e.dataTransfer.setDragImage(dragImage, 20, 20); - + // 정리 setTimeout(() => { if (document.body.contains(dragImage)) { @@ -1150,12 +1221,12 @@ export const TableListComponent: React.FC = ({ return (
-
-
+
+
-
테이블 리스트
-
+
테이블 리스트
+
설정 패널에서 테이블을 선택해주세요
@@ -1165,59 +1236,52 @@ export const TableListComponent: React.FC = ({ } return ( -
{/* 헤더 */} {tableConfig.showHeader && ( -
{(tableConfig.title || tableLabel) && ( -

- {tableConfig.title || tableLabel} -

+

{tableConfig.title || tableLabel}

)}
{/* 선택된 항목 정보 표시 */} {selectedRows.size > 0 && ( -
+
{selectedRows.size}개 선택됨
)} {/* 새로고침 */} - @@ -1604,19 +1672,15 @@ export const TableListComponent: React.FC = ({ size="sm" onClick={() => handlePageChange(currentPage - 1)} disabled={currentPage === 1} - className="h-8 w-8 p-0 disabled:opacity-50 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-300" + className="h-8 w-8 p-0 hover:border-blue-300 hover:bg-blue-50 hover:text-blue-600 disabled:opacity-50" > -
- - {currentPage} - - / - - {totalPages} - +
+ {currentPage} + / + {totalPages}
@@ -1633,7 +1697,7 @@ export const TableListComponent: React.FC = ({ size="sm" onClick={() => handlePageChange(totalPages)} disabled={currentPage === totalPages} - className="h-8 w-8 p-0 disabled:opacity-50 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-300" + className="h-8 w-8 p-0 hover:border-blue-300 hover:bg-blue-50 hover:text-blue-600 disabled:opacity-50" > From 6e8f529cd310dc9e0c77e6eee8c5b3f01c2e0c33 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Mon, 29 Sep 2025 17:29:58 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EA=B0=90=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table-list/TableListComponent.tsx | 106 +++++++++++++++--- 1 file changed, 91 insertions(+), 15 deletions(-) diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index ac2f49b8..6d0067c1 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect, useMemo } from "react"; +import React, { useState, useEffect, useMemo, useCallback, useRef } from "react"; import { TableListConfig, ColumnConfig } from "./types"; import { WebType } from "@/types/common"; import { tableTypeApi } from "@/lib/api/screen"; @@ -35,6 +35,51 @@ const cleanupTableCache = () => { if (typeof window !== "undefined") { setInterval(cleanupTableCache, 10 * 60 * 1000); } + +// 요청 디바운싱을 위한 전역 타이머 +const debounceTimers = new Map(); + +// 진행 중인 요청 추적 (중복 요청 방지) +const activeRequests = new Map>(); + +// 디바운싱된 API 호출 함수 (중복 요청 방지 포함) +const debouncedApiCall = (key: string, fn: (...args: T) => Promise, delay: number = 300) => { + return (...args: T): Promise => { + // 이미 진행 중인 동일한 요청이 있으면 그 결과를 반환 + const activeRequest = activeRequests.get(key); + if (activeRequest) { + console.log(`🔄 진행 중인 요청 재사용: ${key}`); + return activeRequest as Promise; + } + + return new Promise((resolve, reject) => { + // 기존 타이머 제거 + const existingTimer = debounceTimers.get(key); + if (existingTimer) { + clearTimeout(existingTimer); + } + + // 새 타이머 설정 + const timer = setTimeout(async () => { + try { + // 요청 시작 시 활성 요청으로 등록 + const requestPromise = fn(...args); + activeRequests.set(key, requestPromise); + + const result = await requestPromise; + resolve(result); + } catch (error) { + reject(error); + } finally { + debounceTimers.delete(key); + activeRequests.delete(key); + } + }, delay); + + debounceTimers.set(key, timer); + }); + }; +}; import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization"; import { Button } from "@/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; @@ -148,6 +193,15 @@ export const TableListComponent: React.FC = ({ const [joinColumnMapping, setJoinColumnMapping] = useState>({}); const [columnMeta, setColumnMeta] = useState>({}); // 🎯 컬럼 메타정보 (웹타입, 코드카테고리) + // 컬럼 정보 메모이제이션 + const memoizedColumnInfo = useMemo(() => { + return { + labels: columnLabels, + meta: columnMeta, + visibleColumns: (tableConfig.columns || []).filter((col) => col.visible !== false), + }; + }, [columnLabels, columnMeta, tableConfig.columns]); + // 고급 필터 관련 state const [searchValues, setSearchValues] = useState>({}); @@ -369,8 +423,20 @@ export const TableListComponent: React.FC = ({ } }; - // 테이블 데이터 가져오기 - const fetchTableData = async () => { + // 디바운싱된 테이블 데이터 가져오기 + const fetchTableDataDebounced = useCallback( + debouncedApiCall( + `fetchTableData_${tableConfig.selectedTable}_${currentPage}_${localPageSize}`, + async () => { + return fetchTableDataInternal(); + }, + 200, // 200ms 디바운스 + ), + [tableConfig.selectedTable, currentPage, localPageSize, searchTerm, sortColumn, sortDirection, searchValues], + ); + + // 실제 테이블 데이터 가져오기 함수 + const fetchTableDataInternal = async () => { if (!tableConfig.selectedTable) { setData([]); return; @@ -558,14 +624,24 @@ export const TableListComponent: React.FC = ({ codeColumns.map(([col, meta]) => `${col}(${meta.codeCategory})`), ); - // 필요한 코드 카테고리들을 추출하여 배치 로드 - const categoryList = codeColumns.map(([, meta]) => meta.codeCategory).filter(Boolean) as string[]; + // 필요한 코드 카테고리들을 추출하여 배치 로드 (중복 제거) + const categoryList = [ + ...new Set(codeColumns.map(([, meta]) => meta.codeCategory).filter(Boolean)), + ] as string[]; - try { - await codeCache.preloadCodes(categoryList); - console.log("📋 모든 코드 캐시 로드 완료 (전역 캐시)"); - } catch (error) { - console.error("❌ 코드 캐시 로드 중 오류:", error); + // 이미 캐시된 카테고리는 제외 + const uncachedCategories = categoryList.filter((category) => !codeCache.getCodeSync(category)); + + if (uncachedCategories.length > 0) { + try { + console.log(`📋 새로운 코드 카테고리 로딩: ${uncachedCategories.join(", ")}`); + await codeCache.preloadCodes(uncachedCategories); + console.log("📋 모든 코드 캐시 로드 완료 (전역 캐시)"); + } catch (error) { + console.error("❌ 코드 캐시 로드 중 오류:", error); + } + } else { + console.log("📋 모든 코드 카테고리가 이미 캐시됨"); } } @@ -759,18 +835,18 @@ export const TableListComponent: React.FC = ({ const handleAdvancedSearch = () => { setCurrentPage(1); - fetchTableData(); + fetchTableDataDebounced(); }; const handleClearAdvancedFilters = () => { setSearchValues({}); setCurrentPage(1); - fetchTableData(); + fetchTableDataDebounced(); }; // 새로고침 const handleRefresh = () => { - fetchTableData(); + fetchTableDataDebounced(); }; // 체크박스 핸들러들 @@ -872,7 +948,7 @@ export const TableListComponent: React.FC = ({ useEffect(() => { if (tableConfig.autoLoad && !isDesignMode) { - fetchTableData(); + fetchTableDataDebounced(); } }, [ tableConfig.selectedTable, @@ -896,7 +972,7 @@ export const TableListComponent: React.FC = ({ console.log("🔄 선택 상태 초기화 - 빈 배열 전달"); onSelectedRowsChange?.([], []); // 테이블 데이터 새로고침 - fetchTableData(); + fetchTableDataDebounced(); } }, [refreshKey]);