38개로 감소
This commit is contained in:
parent
6ce5fc84a8
commit
808a317ed0
|
|
@ -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;
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -51,15 +51,15 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
|
|||
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 <FileUploadComponent {...props} {...finalProps} />;
|
||||
}
|
||||
|
||||
|
||||
// 다른 컴포넌트들은 기존 로직 유지
|
||||
// const ComponentByName = getWidgetComponentByName(dbWebType.component_name);
|
||||
// console.log(`컴포넌트 "${dbWebType.component_name}" 성공적으로 로드됨:`, ComponentByName);
|
||||
|
|
@ -78,7 +78,7 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
|
|||
// 파일 웹타입의 경우 FileUploadComponent 직접 사용
|
||||
if (webType === "file") {
|
||||
const { FileUploadComponent } = require("@/lib/registry/components/file-upload/FileUploadComponent");
|
||||
console.log(`✅ 파일 웹타입 → FileUploadComponent 사용`);
|
||||
console.log("✅ 파일 웹타입 → FileUploadComponent 사용");
|
||||
return <FileUploadComponent {...props} {...finalProps} />;
|
||||
}
|
||||
|
||||
|
|
@ -106,14 +106,14 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
|
|||
// 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 <FileUploadComponent {...props} {...finalProps} />;
|
||||
}
|
||||
|
||||
|
||||
// const FallbackComponent = getWidgetComponentByWebType(webType);
|
||||
// return <FallbackComponent {...props} />;
|
||||
console.warn(`웹타입 "${webType}" 폴백 기능 임시 비활성화`);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
})()}
|
||||
|
|
|
|||
|
|
@ -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<string, { columns: any[]; timestamp: number }>();
|
||||
const tableInfoCache = new Map<string, { tables: any[]; timestamp: number }>();
|
||||
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<TableListComponentProps> = ({
|
|||
} 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<TableListComponentProps> = ({
|
|||
// 🎯 강제로 그리드 컬럼수에 맞는 크기 적용 (디자인 모드에서는 더 큰 크기 허용)
|
||||
const gridColumns = component.gridColumns || 1;
|
||||
let calculatedWidth: string;
|
||||
|
||||
|
||||
if (isDesignMode) {
|
||||
// 디자인 모드에서는 더 큰 최소 크기 적용
|
||||
if (gridColumns === 1) {
|
||||
|
|
@ -208,8 +237,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
}
|
||||
}
|
||||
|
||||
// 디버깅 로그 제거 (성능상 이유로)
|
||||
|
||||
// 디버깅 로그 제거 (성능상 이유로)
|
||||
|
||||
// 스타일 계산 (컨테이너에 맞춤)
|
||||
const componentStyle: React.CSSProperties = {
|
||||
|
|
@ -244,63 +272,99 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
// 자동 높이로 테이블 전체를 감쌈
|
||||
}
|
||||
|
||||
// 컬럼 라벨 정보 가져오기
|
||||
// 컬럼 라벨 정보 가져오기 (캐싱 적용)
|
||||
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<string, string> = {};
|
||||
const meta: Record<string, { webType?: string; codeCategory?: string }> = {};
|
||||
// 캐시 확인
|
||||
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<string, string> = {};
|
||||
const meta: Record<string, { webType?: string; codeCategory?: string }> = {};
|
||||
|
||||
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<TableListComponentProps> = ({
|
|||
// 페이지 변경
|
||||
const handlePageChange = (newPage: number) => {
|
||||
setCurrentPage(newPage);
|
||||
|
||||
|
||||
// 상세설정에 현재 페이지 정보 알림 (필요한 경우)
|
||||
if (onConfigChange && tableConfig.pagination) {
|
||||
// console.log("📤 테이블에서 페이지 변경을 상세설정에 알림:", newPage);
|
||||
|
|
@ -844,7 +908,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
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<TableListComponentProps> = ({
|
|||
return null;
|
||||
}
|
||||
|
||||
return <Checkbox checked={isAllSelected} onCheckedChange={handleSelectAll} aria-label="전체 선택" style={{ zIndex: 1 }} />;
|
||||
return (
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
aria-label="전체 선택"
|
||||
style={{ zIndex: 1 }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// 체크박스 셀 렌더링
|
||||
|
|
@ -1066,41 +1137,41 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
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 = `
|
||||
<div style="
|
||||
width: 20px;
|
||||
|
|
@ -1121,10 +1192,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
font-weight: 500;
|
||||
">4×1</div>
|
||||
`;
|
||||
|
||||
|
||||
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<TableListComponentProps> = ({
|
|||
return (
|
||||
<div style={componentStyle} className={className} {...domProps}>
|
||||
<div className="flex h-full items-center justify-center rounded-2xl border-2 border-dashed border-blue-200 bg-gradient-to-br from-blue-50/30 to-indigo-50/20">
|
||||
<div className="text-center p-8">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-gradient-to-br from-blue-100 to-indigo-100 rounded-2xl flex items-center justify-center shadow-sm">
|
||||
<div className="p-8 text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-blue-100 to-indigo-100 shadow-sm">
|
||||
<TableIcon className="h-8 w-8 text-blue-600" />
|
||||
</div>
|
||||
<div className="text-lg font-semibold text-slate-700 mb-2">테이블 리스트</div>
|
||||
<div className="text-sm text-slate-500 bg-white/60 px-4 py-2 rounded-full">
|
||||
<div className="mb-2 text-lg font-semibold text-slate-700">테이블 리스트</div>
|
||||
<div className="rounded-full bg-white/60 px-4 py-2 text-sm text-slate-500">
|
||||
설정 패널에서 테이블을 선택해주세요
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1165,59 +1236,52 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{...componentStyle, zIndex: 10}} // 🎯 componentStyle + z-index 추가
|
||||
<div
|
||||
style={{ ...componentStyle, zIndex: 10 }} // 🎯 componentStyle + z-index 추가
|
||||
className={cn(
|
||||
"rounded-lg bg-white border border-gray-200 shadow-md shadow-blue-100/50",
|
||||
"overflow-hidden relative", // 🎯 항상 overflow-hidden 적용 + relative 추가
|
||||
className
|
||||
)}
|
||||
"rounded-lg border border-gray-200 bg-white shadow-md shadow-blue-100/50",
|
||||
"relative overflow-hidden", // 🎯 항상 overflow-hidden 적용 + relative 추가
|
||||
className,
|
||||
)}
|
||||
{...domProps}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
{tableConfig.showHeader && (
|
||||
<div
|
||||
className="flex items-center justify-between bg-gray-100/80 border-b border-gray-200 px-6 py-4"
|
||||
style={{
|
||||
width: "100%",
|
||||
<div
|
||||
className="flex items-center justify-between border-b border-gray-200 bg-gray-100/80 px-6 py-4"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
boxSizing: "border-box"
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
{(tableConfig.title || tableLabel) && (
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{tableConfig.title || tableLabel}
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-900">{tableConfig.title || tableLabel}</h3>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
{/* 선택된 항목 정보 표시 */}
|
||||
{selectedRows.size > 0 && (
|
||||
<div className="flex items-center space-x-2 bg-blue-50 px-3 py-1 rounded-md">
|
||||
<div className="flex items-center space-x-2 rounded-md bg-blue-50 px-3 py-1">
|
||||
<span className="text-sm font-medium text-blue-700">{selectedRows.size}개 선택됨</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 새로고침 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
style={buttonStyle}
|
||||
className="group relative shadow-sm rounded-lg [&:hover]:opacity-90"
|
||||
className="group relative rounded-lg shadow-sm [&:hover]:opacity-90"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="relative">
|
||||
<RefreshCw className={cn(
|
||||
"h-4 w-4",
|
||||
loading && "animate-spin"
|
||||
)} style={{ color: buttonTextColor }} />
|
||||
{loading && (
|
||||
<div className="absolute -inset-1 bg-blue-200/30 rounded-full animate-pulse"></div>
|
||||
)}
|
||||
<RefreshCw className={cn("h-4 w-4", loading && "animate-spin")} style={{ color: buttonTextColor }} />
|
||||
{loading && <div className="absolute -inset-1 animate-pulse rounded-full bg-blue-200/30"></div>}
|
||||
</div>
|
||||
<span className="text-sm font-medium" style={{ color: buttonTextColor }}>
|
||||
{loading ? "새로고침 중..." : "새로고침"}
|
||||
|
|
@ -1239,18 +1303,18 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
onSearchValueChange={handleSearchValueChange}
|
||||
onSearch={handleAdvancedSearch}
|
||||
onClearFilters={handleClearAdvancedFilters}
|
||||
tableColumns={visibleColumns.map((col) => ({
|
||||
columnName: col.columnName,
|
||||
widgetType: (columnMeta[col.columnName]?.webType || "text") as WebType,
|
||||
displayName: columnLabels[col.columnName] || col.displayName || col.columnName,
|
||||
codeCategory: columnMeta[col.columnName]?.codeCategory,
|
||||
isVisible: col.visible,
|
||||
// 추가 메타데이터 전달 (필터 자동 생성용)
|
||||
web_type: (columnMeta[col.columnName]?.webType || "text") as WebType,
|
||||
column_name: col.columnName,
|
||||
column_label: columnLabels[col.columnName] || col.displayName || col.columnName,
|
||||
code_category: columnMeta[col.columnName]?.codeCategory,
|
||||
}))}
|
||||
tableColumns={visibleColumns.map((col) => ({
|
||||
columnName: col.columnName,
|
||||
widgetType: (columnMeta[col.columnName]?.webType || "text") as WebType,
|
||||
displayName: columnLabels[col.columnName] || col.displayName || col.columnName,
|
||||
codeCategory: columnMeta[col.columnName]?.codeCategory,
|
||||
isVisible: col.visible,
|
||||
// 추가 메타데이터 전달 (필터 자동 생성용)
|
||||
web_type: (columnMeta[col.columnName]?.webType || "text") as WebType,
|
||||
column_name: col.columnName,
|
||||
column_label: columnLabels[col.columnName] || col.displayName || col.columnName,
|
||||
code_category: columnMeta[col.columnName]?.codeCategory,
|
||||
}))}
|
||||
tableName={tableConfig.selectedTable}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -1258,55 +1322,57 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
)}
|
||||
|
||||
{/* 테이블 컨텐츠 */}
|
||||
<div
|
||||
<div
|
||||
className={`w-full overflow-hidden ${localPageSize >= 50 ? "flex-1" : ""}`}
|
||||
style={{
|
||||
width: "100%",
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
boxSizing: "border-box"
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex h-full items-center justify-center bg-gradient-to-br from-slate-50/50 to-blue-50/30">
|
||||
<div className="text-center p-8">
|
||||
<div className="p-8 text-center">
|
||||
<div className="relative">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-gradient-to-br from-blue-100 to-indigo-100 rounded-2xl flex items-center justify-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-blue-100 to-indigo-100">
|
||||
<RefreshCw className="h-8 w-8 animate-spin text-blue-600" />
|
||||
</div>
|
||||
<div className="absolute -top-1 -right-1 w-4 h-4 bg-gradient-to-br from-blue-400 to-indigo-500 rounded-full animate-pulse"></div>
|
||||
<div className="absolute -top-1 -right-1 h-4 w-4 animate-pulse rounded-full bg-gradient-to-br from-blue-400 to-indigo-500"></div>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-slate-700">데이터를 불러오는 중...</div>
|
||||
<div className="text-xs text-slate-500 mt-1">잠시만 기다려주세요</div>
|
||||
<div className="mt-1 text-xs text-slate-500">잠시만 기다려주세요</div>
|
||||
</div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex h-full items-center justify-center bg-gradient-to-br from-red-50/50 to-orange-50/30">
|
||||
<div className="text-center p-8">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-gradient-to-br from-red-100 to-orange-100 rounded-2xl flex items-center justify-center">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-red-500 to-orange-500 rounded-full flex items-center justify-center">
|
||||
<span className="text-white text-sm font-bold">!</span>
|
||||
<div className="p-8 text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-red-100 to-orange-100">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gradient-to-br from-red-500 to-orange-500">
|
||||
<span className="text-sm font-bold text-white">!</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-red-700">오류가 발생했습니다</div>
|
||||
<div className="mt-1 text-xs text-red-500 bg-red-50 px-3 py-1 rounded-full">{error}</div>
|
||||
<div className="mt-1 rounded-full bg-red-50 px-3 py-1 text-xs text-red-500">{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : tableConfig.displayMode === "card" ? (
|
||||
// 카드 모드 렌더링
|
||||
<div className="w-full h-full overflow-y-auto">
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
<CardModeRenderer
|
||||
data={data}
|
||||
cardConfig={tableConfig.cardConfig || {
|
||||
idColumn: "id",
|
||||
titleColumn: "name",
|
||||
cardsPerRow: 3,
|
||||
cardSpacing: 16,
|
||||
showActions: true,
|
||||
}}
|
||||
cardConfig={
|
||||
tableConfig.cardConfig || {
|
||||
idColumn: "id",
|
||||
titleColumn: "name",
|
||||
cardsPerRow: 3,
|
||||
cardSpacing: 16,
|
||||
showActions: true,
|
||||
}
|
||||
}
|
||||
visibleColumns={visibleColumns}
|
||||
onRowClick={handleRowClick}
|
||||
onRowSelect={(row, selected) => {
|
||||
const rowIndex = data.findIndex(d => d === row);
|
||||
const rowIndex = data.findIndex((d) => d === row);
|
||||
const rowKey = getRowKey(row, rowIndex);
|
||||
handleRowSelection(rowKey, selected);
|
||||
}}
|
||||
|
|
@ -1338,25 +1404,27 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
) : (
|
||||
// 기존 테이블 (가로 스크롤이 필요 없는 경우)
|
||||
<div className="w-full overflow-hidden">
|
||||
<Table
|
||||
<Table
|
||||
className="w-full"
|
||||
style={{
|
||||
width: "100%",
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
tableLayout: "fixed" // 테이블 크기 고정
|
||||
tableLayout: "fixed", // 테이블 크기 고정
|
||||
}}
|
||||
>
|
||||
<TableHeader className={cn(
|
||||
tableConfig.stickyHeader ? "sticky top-0 z-20" : "",
|
||||
"bg-gray-100/80 border-b border-gray-200"
|
||||
)}>
|
||||
<TableRow
|
||||
style={{
|
||||
minHeight: "48px !important",
|
||||
height: "48px !important",
|
||||
<TableHeader
|
||||
className={cn(
|
||||
tableConfig.stickyHeader ? "sticky top-0 z-20" : "",
|
||||
"border-b border-gray-200 bg-gray-100/80",
|
||||
)}
|
||||
>
|
||||
<TableRow
|
||||
style={{
|
||||
minHeight: "48px !important",
|
||||
height: "48px !important",
|
||||
lineHeight: "1",
|
||||
width: "100%",
|
||||
maxWidth: "100%"
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
className="border-none"
|
||||
>
|
||||
|
|
@ -1373,13 +1441,13 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
className={cn(
|
||||
"h-12 align-middle px-4 py-3 text-sm font-semibold text-gray-800",
|
||||
className={cn(
|
||||
"h-12 px-4 py-3 align-middle text-sm font-semibold text-gray-800",
|
||||
column.columnName === "__checkbox__"
|
||||
? "text-center"
|
||||
: "cursor-pointer whitespace-nowrap select-none",
|
||||
`text-${column.align}`,
|
||||
column.sortable && "hover:bg-orange-100 transition-colors duration-150"
|
||||
column.sortable && "transition-colors duration-150 hover:bg-orange-100",
|
||||
)}
|
||||
onClick={() => column.sortable && handleSort(column.columnName)}
|
||||
>
|
||||
|
|
@ -1387,9 +1455,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
renderCheckboxHeader()
|
||||
) : (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>
|
||||
{columnLabels[column.columnName] || column.displayName}
|
||||
</span>
|
||||
<span>{columnLabels[column.columnName] || column.displayName}</span>
|
||||
{column.sortable && (
|
||||
<div className="flex flex-col">
|
||||
{sortColumn === column.columnName ? (
|
||||
|
|
@ -1409,129 +1475,131 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={visibleColumns.length} className="py-12 text-center">
|
||||
<div className="flex flex-col items-center space-y-3">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-slate-100 to-slate-200 rounded-2xl flex items-center justify-center">
|
||||
<TableIcon className="h-6 w-6 text-slate-400" />
|
||||
<TableBody>
|
||||
{data.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={visibleColumns.length} className="py-12 text-center">
|
||||
<div className="flex flex-col items-center space-y-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br from-slate-100 to-slate-200">
|
||||
<TableIcon className="h-6 w-6 text-slate-400" />
|
||||
</div>
|
||||
<div className="text-sm font-medium text-slate-600">데이터가 없습니다</div>
|
||||
<div className="text-xs text-slate-400">조건을 변경하거나 새로운 데이터를 추가해보세요</div>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-slate-600">데이터가 없습니다</div>
|
||||
<div className="text-xs text-slate-400">조건을 변경하거나 새로운 데이터를 추가해보세요</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
data.map((row, index) => (
|
||||
<TableRow
|
||||
key={index}
|
||||
draggable={!isDesignMode}
|
||||
onDragStart={(e) => handleRowDragStart(e, row, index)}
|
||||
onDragEnd={handleRowDragEnd}
|
||||
className={cn(
|
||||
"group relative h-12 cursor-pointer transition-all duration-200 border-b border-gray-100",
|
||||
// 기본 스타일
|
||||
tableConfig.tableStyle?.hoverEffect && "hover:bg-gradient-to-r hover:from-orange-200 hover:to-orange-300/90 hover:shadow-sm",
|
||||
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-100/80",
|
||||
// 드래그 상태 스타일 (미묘하게)
|
||||
draggedRowIndex === index && "bg-gradient-to-r from-blue-50 to-blue-100/40 shadow-sm border-blue-200",
|
||||
isDragging && draggedRowIndex !== index && "opacity-70",
|
||||
// 드래그 가능 표시
|
||||
!isDesignMode && "hover:cursor-grab active:cursor-grabbing"
|
||||
)}
|
||||
style={{
|
||||
minHeight: "48px",
|
||||
height: "48px",
|
||||
lineHeight: "1",
|
||||
width: "100%",
|
||||
maxWidth: "100%"
|
||||
}}
|
||||
onClick={() => handleRowClick(row)}
|
||||
>
|
||||
{visibleColumns.map((column, colIndex) => (
|
||||
<TableCell
|
||||
key={column.columnName}
|
||||
className={cn(
|
||||
"h-12 align-middle px-4 py-3 text-sm transition-all duration-200",
|
||||
`text-${column.align}`
|
||||
)}
|
||||
style={{
|
||||
minHeight: "48px",
|
||||
height: "48px",
|
||||
verticalAlign: "middle",
|
||||
width: column.width ? `${column.width}px` : undefined,
|
||||
boxSizing: "border-box",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap"
|
||||
}}
|
||||
>
|
||||
{column.columnName === "__checkbox__"
|
||||
? renderCheckboxCell(row, index)
|
||||
: (() => {
|
||||
// 🎯 매핑된 컬럼명으로 데이터 찾기
|
||||
const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName;
|
||||
const cellValue = row[mappedColumnName];
|
||||
if (index === 0) {
|
||||
// 디버깅 로그 제거 (성능상 이유로)
|
||||
}
|
||||
const formattedValue = formatCellValue(cellValue, column.format, column.columnName) || "\u00A0";
|
||||
|
||||
// 첫 번째 컬럼에 드래그 핸들과 아바타 추가
|
||||
const isFirstColumn = colIndex === (visibleColumns[0]?.columnName === "__checkbox__" ? 1 : 0);
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
{isFirstColumn && !isDesignMode && (
|
||||
<div className="opacity-60 cursor-grab active:cursor-grabbing mr-1">
|
||||
{/* 그리드 스냅 가이드 아이콘 */}
|
||||
<div className="flex space-x-0.5">
|
||||
<div className="flex flex-col space-y-0.5">
|
||||
<div className="w-0.5 h-0.5 bg-gray-400 rounded-full"></div>
|
||||
<div className="w-0.5 h-0.5 bg-gray-400 rounded-full"></div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-0.5">
|
||||
<div className="w-0.5 h-0.5 bg-gray-400 rounded-full"></div>
|
||||
<div className="w-0.5 h-0.5 bg-gray-400 rounded-full"></div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
data.map((row, index) => (
|
||||
<TableRow
|
||||
key={index}
|
||||
draggable={!isDesignMode}
|
||||
onDragStart={(e) => handleRowDragStart(e, row, index)}
|
||||
onDragEnd={handleRowDragEnd}
|
||||
className={cn(
|
||||
"group relative h-12 cursor-pointer border-b border-gray-100 transition-all duration-200",
|
||||
// 기본 스타일
|
||||
tableConfig.tableStyle?.hoverEffect &&
|
||||
"hover:bg-gradient-to-r hover:from-orange-200 hover:to-orange-300/90 hover:shadow-sm",
|
||||
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-100/80",
|
||||
// 드래그 상태 스타일 (미묘하게)
|
||||
draggedRowIndex === index &&
|
||||
"border-blue-200 bg-gradient-to-r from-blue-50 to-blue-100/40 shadow-sm",
|
||||
isDragging && draggedRowIndex !== index && "opacity-70",
|
||||
// 드래그 가능 표시
|
||||
!isDesignMode && "hover:cursor-grab active:cursor-grabbing",
|
||||
)}
|
||||
style={{
|
||||
minHeight: "48px",
|
||||
height: "48px",
|
||||
lineHeight: "1",
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
onClick={() => handleRowClick(row)}
|
||||
>
|
||||
{visibleColumns.map((column, colIndex) => (
|
||||
<TableCell
|
||||
key={column.columnName}
|
||||
className={cn(
|
||||
"h-12 px-4 py-3 align-middle text-sm transition-all duration-200",
|
||||
`text-${column.align}`,
|
||||
)}
|
||||
style={{
|
||||
minHeight: "48px",
|
||||
height: "48px",
|
||||
verticalAlign: "middle",
|
||||
width: column.width ? `${column.width}px` : undefined,
|
||||
boxSizing: "border-box",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{column.columnName === "__checkbox__"
|
||||
? renderCheckboxCell(row, index)
|
||||
: (() => {
|
||||
// 🎯 매핑된 컬럼명으로 데이터 찾기
|
||||
const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName;
|
||||
const cellValue = row[mappedColumnName];
|
||||
if (index === 0) {
|
||||
// 디버깅 로그 제거 (성능상 이유로)
|
||||
}
|
||||
const formattedValue =
|
||||
formatCellValue(cellValue, column.format, column.columnName) || "\u00A0";
|
||||
|
||||
// 첫 번째 컬럼에 드래그 핸들과 아바타 추가
|
||||
const isFirstColumn =
|
||||
colIndex === (visibleColumns[0]?.columnName === "__checkbox__" ? 1 : 0);
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
{isFirstColumn && !isDesignMode && (
|
||||
<div className="mr-1 cursor-grab opacity-60 active:cursor-grabbing">
|
||||
{/* 그리드 스냅 가이드 아이콘 */}
|
||||
<div className="flex space-x-0.5">
|
||||
<div className="flex flex-col space-y-0.5">
|
||||
<div className="h-0.5 w-0.5 rounded-full bg-gray-400"></div>
|
||||
<div className="h-0.5 w-0.5 rounded-full bg-gray-400"></div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-0.5">
|
||||
<div className="h-0.5 w-0.5 rounded-full bg-gray-400"></div>
|
||||
<div className="h-0.5 w-0.5 rounded-full bg-gray-400"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<span className="font-medium text-gray-700">
|
||||
{formattedValue}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
<span className="font-medium text-gray-700">{formattedValue}</span>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 푸터/페이지네이션 */}
|
||||
{tableConfig.showFooter && tableConfig.pagination?.enabled && (
|
||||
<div
|
||||
className="flex flex-col items-center justify-center bg-gray-100/80 border-t border-gray-200 p-6 space-y-4"
|
||||
style={{
|
||||
width: "100%",
|
||||
<div
|
||||
className="flex flex-col items-center justify-center space-y-4 border-t border-gray-200 bg-gray-100/80 p-6"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
boxSizing: "border-box"
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
{/* 페이지 정보 - 가운데 정렬 */}
|
||||
{tableConfig.pagination?.showPageInfo && (
|
||||
<div className="flex items-center justify-center space-x-2 text-sm text-slate-600">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
<div className="h-2 w-2 rounded-full bg-blue-500"></div>
|
||||
<span className="font-medium">
|
||||
전체 <span className="text-blue-600 font-semibold">{totalItems.toLocaleString()}</span>건 중{" "}
|
||||
<span className="text-slate-800 font-semibold">
|
||||
전체 <span className="font-semibold text-blue-600">{totalItems.toLocaleString()}</span>건 중{" "}
|
||||
<span className="font-semibold text-slate-800">
|
||||
{(currentPage - 1) * localPageSize + 1}-{Math.min(currentPage * localPageSize, totalItems)}
|
||||
</span>{" "}
|
||||
표시
|
||||
|
|
@ -1549,8 +1617,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
// console.log("🚀 페이지 크기 드롭다운 변경 감지:", e.target.value);
|
||||
const newPageSize = parseInt(e.target.value);
|
||||
|
||||
// console.log("🎯 페이지 크기 변경 이벤트:", {
|
||||
// from: localPageSize,
|
||||
// console.log("🎯 페이지 크기 변경 이벤트:", {
|
||||
// from: localPageSize,
|
||||
// to: newPageSize,
|
||||
// hasOnConfigChange: !!onConfigChange,
|
||||
// onConfigChangeType: typeof onConfigChange
|
||||
|
|
@ -1578,7 +1646,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
// 데이터는 useEffect에서 자동으로 다시 로드됨
|
||||
}}
|
||||
className="bg-white/80 border border-slate-200 rounded-lg px-3 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-white hover:border-slate-300 transition-colors"
|
||||
className="rounded-lg border border-slate-200 bg-white/80 px-3 py-2 text-sm font-medium text-slate-700 shadow-sm transition-colors hover:border-slate-300 hover:bg-white"
|
||||
>
|
||||
{(tableConfig.pagination?.pageSizeOptions || [10, 20, 50, 100]).map((size) => (
|
||||
<option key={size} value={size}>
|
||||
|
|
@ -1589,13 +1657,13 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
)}
|
||||
|
||||
{/* 페이지네이션 버튼 */}
|
||||
<div className="flex items-center space-x-2 bg-white rounded-lg border border-gray-200 shadow-sm p-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(1)}
|
||||
<div className="flex items-center space-x-2 rounded-lg border border-gray-200 bg-white p-1 shadow-sm">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(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"
|
||||
>
|
||||
<ChevronsLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
|
|
@ -1604,19 +1672,15 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
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"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center px-4 py-1 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-md border border-blue-100">
|
||||
<span className="text-sm font-semibold text-blue-800">
|
||||
{currentPage}
|
||||
</span>
|
||||
<span className="text-gray-400 mx-2 font-light">/</span>
|
||||
<span className="text-sm font-medium text-gray-600">
|
||||
{totalPages}
|
||||
</span>
|
||||
<div className="flex items-center rounded-md border border-blue-100 bg-gradient-to-r from-blue-50 to-indigo-50 px-4 py-1">
|
||||
<span className="text-sm font-semibold text-blue-800">{currentPage}</span>
|
||||
<span className="mx-2 font-light text-gray-400">/</span>
|
||||
<span className="text-sm font-medium text-gray-600">{totalPages}</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
|
|
@ -1624,7 +1688,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
size="sm"
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
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"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
|
|
@ -1633,7 +1697,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
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"
|
||||
>
|
||||
<ChevronsRight className="h-4 w-4" />
|
||||
</Button>
|
||||
|
|
|
|||
Loading…
Reference in New Issue