From 8c89b9cf8613d31f121d1907529cb662885ab1e0 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 23 Oct 2025 16:50:41 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(main)/screens/[screenId]/page.tsx | 49 +- frontend/components/layout/AppLayout.tsx | 2 +- .../table-list/TableListComponent.tsx | 2315 ++++++----------- 3 files changed, 868 insertions(+), 1498 deletions(-) diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index a2c0e05e..afa93a69 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -41,6 +41,10 @@ export default function ScreenViewPage() { modalDescription?: string; }>({}); + // 자동 스케일 조정 (사용자 화면 크기에 맞춤) + const [scale, setScale] = useState(1); + const containerRef = React.useRef(null); + useEffect(() => { const initComponents = async () => { try { @@ -125,6 +129,40 @@ export default function ScreenViewPage() { } }, [screenId]); + // 자동 스케일 조정 useEffect (항상 화면에 꽉 차게) + useEffect(() => { + const updateScale = () => { + if (containerRef.current && layout) { + const screenWidth = layout?.screenResolution?.width || 1200; + const containerWidth = containerRef.current.offsetWidth; + const availableWidth = containerWidth - 32; // 좌우 패딩 16px * 2 + + // 항상 화면에 맞춰서 스케일 조정 (늘리거나 줄임) + const newScale = availableWidth / screenWidth; + + console.log("📏 스케일 계산 (화면 꽉 차게):", { + screenWidth, + containerWidth, + availableWidth, + scale: newScale, + }); + + setScale(newScale); + } + }; + + // 초기 측정 (DOM이 완전히 렌더링된 후) + const timer = setTimeout(() => { + updateScale(); + }, 100); + + window.addEventListener("resize", updateScale); + return () => { + clearTimeout(timer); + window.removeEventListener("resize", updateScale); + }; + }, [layout]); + if (loading) { return (
@@ -158,14 +196,19 @@ export default function ScreenViewPage() { const screenHeight = layout?.screenResolution?.height || 800; return ( -
+
{/* 절대 위치 기반 렌더링 */} {layout && layout.components.length > 0 ? (
{/* 최상위 컴포넌트들 렌더링 */} diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index 81aab11b..c6313714 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -457,7 +457,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { {/* 가운데 컨텐츠 영역 - overflow 문제 해결 */} -
{children}
+
{children}
{/* 프로필 수정 모달 */} diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 4887de68..a4557c8e 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -6,24 +6,50 @@ import { WebType } from "@/types/common"; import { tableTypeApi } from "@/lib/api/screen"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { codeCache } from "@/lib/caching/codeCache"; +import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization"; +import { Button } from "@/components/ui/button"; +import { + ChevronLeft, + ChevronRight, + ChevronsLeft, + ChevronsRight, + RefreshCw, + ArrowUp, + ArrowDown, + TableIcon, + Settings, + X, +} from "lucide-react"; +import { Checkbox } from "@/components/ui/checkbox"; +import { cn } from "@/lib/utils"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { AdvancedSearchFilters } from "@/components/screen/filters/AdvancedSearchFilters"; +import { SingleTableWithSticky } from "./SingleTableWithSticky"; +import { CardModeRenderer } from "./CardModeRenderer"; + +// ======================================== +// 캐시 및 유틸리티 +// ======================================== -// 전역 테이블 캐시 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); @@ -31,41 +57,30 @@ const cleanupTableCache = () => { } }; -// 주기적으로 캐시 정리 (10분마다) 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) { @@ -80,25 +95,10 @@ const debouncedApiCall = (key: string, fn: (...args: T) => P }); }; }; -import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization"; -import { Button } from "@/components/ui/button"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { - ChevronLeft, - ChevronRight, - ChevronsLeft, - ChevronsRight, - RefreshCw, - ArrowUpDown, - ArrowUp, - ArrowDown, - TableIcon, -} from "lucide-react"; -import { Checkbox } from "@/components/ui/checkbox"; -import { cn } from "@/lib/utils"; -import { AdvancedSearchFilters } from "@/components/screen/filters/AdvancedSearchFilters"; -import { SingleTableWithSticky } from "./SingleTableWithSticky"; -import { CardModeRenderer } from "./CardModeRenderer"; + +// ======================================== +// Props 인터페이스 +// ======================================== export interface TableListComponentProps { component: any; @@ -113,8 +113,6 @@ export interface TableListComponentProps { formData?: Record; onFormDataChange?: (data: any) => void; config?: TableListConfig; - - // 추가 props (DOM에 전달되지 않음) size?: { width: number; height: number }; position?: { x: number; y: number; z?: number }; componentConfig?: any; @@ -125,21 +123,15 @@ export interface TableListComponentProps { onRefresh?: () => void; onClose?: () => void; screenId?: string; - - // 선택된 행 정보 전달 핸들러 onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void; - - // 설정 변경 핸들러 (상세설정과 연동) onConfigChange?: (config: any) => void; - - // 테이블 새로고침 키 refreshKey?: number; } -/** - * TableList 컴포넌트 - * 데이터베이스 테이블의 데이터를 목록으로 표시하는 컴포넌트 - */ +// ======================================== +// 메인 컴포넌트 +// ======================================== + export const TableListComponent: React.FC = ({ component, isDesignMode = false, @@ -155,38 +147,86 @@ export const TableListComponent: React.FC = ({ onSelectedRowsChange, onConfigChange, refreshKey, - tableName, // 화면의 기본 테이블명 (screenInfo에서 전달) + tableName, }) => { - // 컴포넌트 설정 + // ======================================== + // 설정 및 스타일 + // ======================================== + const tableConfig = { ...config, ...component.config, ...componentConfig, - // selectedTable이 없으면 화면의 기본 테이블 사용 - selectedTable: - componentConfig?.selectedTable || component.config?.selectedTable || config?.selectedTable || tableName, } as TableListConfig; + // selectedTable 안전하게 추출 (문자열인지 확인) + let finalSelectedTable = + componentConfig?.selectedTable || component.config?.selectedTable || config?.selectedTable || tableName; + console.log("🔍 TableListComponent 초기화:", { componentConfigSelectedTable: componentConfig?.selectedTable, + componentConfigSelectedTableType: typeof componentConfig?.selectedTable, componentConfigSelectedTable2: component.config?.selectedTable, + componentConfigSelectedTable2Type: typeof component.config?.selectedTable, configSelectedTable: config?.selectedTable, + configSelectedTableType: typeof config?.selectedTable, screenTableName: tableName, - finalSelectedTable: tableConfig.selectedTable, + screenTableNameType: typeof tableName, + finalSelectedTable, + finalSelectedTableType: typeof finalSelectedTable, }); - // 🎨 동적 색상 설정 (속성편집 모달의 "색상" 필드와 연동) - const buttonColor = component.style?.labelColor || "#212121"; // 기본 파란색 + // 객체인 경우 tableName 속성 추출 시도 + if (typeof finalSelectedTable === "object" && finalSelectedTable !== null) { + console.warn("⚠️ selectedTable이 객체입니다:", finalSelectedTable); + finalSelectedTable = (finalSelectedTable as any).tableName || (finalSelectedTable as any).name || tableName; + console.log("✅ 객체에서 추출한 테이블명:", finalSelectedTable); + } + + tableConfig.selectedTable = finalSelectedTable; + + console.log( + "✅ 최종 tableConfig.selectedTable:", + tableConfig.selectedTable, + "타입:", + typeof tableConfig.selectedTable, + ); + + const buttonColor = component.style?.labelColor || "#212121"; const buttonTextColor = component.config?.buttonTextColor || "#ffffff"; - const buttonStyle = { - backgroundColor: buttonColor, - color: buttonTextColor, - borderColor: buttonColor, + + const gridColumns = component.gridColumns || 1; + let calculatedWidth: string; + + if (isDesignMode) { + if (gridColumns === 1) { + calculatedWidth = "400px"; + } else if (gridColumns === 2) { + calculatedWidth = "800px"; + } else { + calculatedWidth = "100%"; + } + } else { + calculatedWidth = "100%"; + } + + const componentStyle: React.CSSProperties = { + width: calculatedWidth, + height: isDesignMode ? "auto" : "100%", + minHeight: isDesignMode ? "300px" : "100%", + display: "flex", + flexDirection: "column", + backgroundColor: "#ffffff", + border: isSelected ? "2px solid #3b82f6" : "1px solid #e5e7eb", + borderRadius: "8px", + overflow: "hidden", + ...style, }; - // 디버깅 로그 제거 (성능상 이유로) - + // ======================================== // 상태 관리 + // ======================================== + const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -198,627 +238,222 @@ export const TableListComponent: React.FC = ({ const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); const [columnLabels, setColumnLabels] = useState>({}); const [tableLabel, setTableLabel] = useState(""); - const [localPageSize, setLocalPageSize] = useState(tableConfig.pagination?.pageSize || 20); // 로컬 페이지 크기 상태 - const [displayColumns, setDisplayColumns] = useState([]); // 🎯 표시할 컬럼 (Entity 조인 적용됨) - - // 🎯 조인 컬럼 매핑 상태 + const [localPageSize, setLocalPageSize] = useState(tableConfig.pagination?.pageSize || 20); + const [displayColumns, setDisplayColumns] = useState([]); 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 [columnMeta, setColumnMeta] = useState>({}); const [searchValues, setSearchValues] = useState>({}); - - // 체크박스 상태 관리 const [selectedRows, setSelectedRows] = useState>(new Set()); - - // 드래그 상태 관리 const [isDragging, setIsDragging] = useState(false); - const [draggedRowIndex, setDraggedRowIndex] = useState(null); // 선택된 행들의 키 집합 - const [isAllSelected, setIsAllSelected] = useState(false); // 전체 선택 상태 + const [draggedRowIndex, setDraggedRowIndex] = useState(null); + const [isAllSelected, setIsAllSelected] = useState(false); + + // 필터 설정 관련 상태 + const [isFilterSettingOpen, setIsFilterSettingOpen] = useState(false); + const [visibleFilterColumns, setVisibleFilterColumns] = useState>(new Set()); - // 🎯 Entity 조인 최적화 훅 사용 const { optimizedConvertCode } = useEntityJoinOptimization(columnMeta, { enableBatchLoading: true, preloadCommonCodes: true, maxBatchSize: 5, }); - // 높이 계산 함수 (메모이제이션) - const optimalHeight = useMemo(() => { - // 실제 데이터 개수에 맞춰서 높이 계산 (최소 5개, 최대 20개) - const actualDataCount = data.length; - const displayPageSize = Math.min(Math.max(actualDataCount, 5), 20); + // ======================================== + // 컬럼 라벨 가져오기 + // ======================================== - const headerHeight = 50; // 테이블 헤더 - const rowHeight = 42; // 각 행 높이 - const searchHeight = tableConfig.filter?.enabled ? 80 : 0; // 검색 영역 - const footerHeight = tableConfig.showFooter ? 60 : 0; // 페이지네이션 - const titleHeight = tableConfig.showHeader ? 60 : 0; // 제목 영역 - const padding = 40; // 여백 - - const calculatedHeight = - titleHeight + searchHeight + headerHeight + displayPageSize * rowHeight + footerHeight + padding; - - console.log("🔍 테이블 높이 계산:", { - actualDataCount, - localPageSize, - displayPageSize, - isDesignMode, - titleHeight, - searchHeight, - headerHeight, - rowHeight, - footerHeight, - padding, - calculatedHeight, - finalHeight: `${calculatedHeight}px`, - }); - - // 추가 디버깅: 실제 데이터 상황 - console.log("🔍 실제 데이터 상황:", { - actualDataLength: data.length, - localPageSize, - currentPage, - totalItems, - totalPages, - }); - - return calculatedHeight; - }, []); - - // 🎯 강제로 그리드 컬럼수에 맞는 크기 적용 (디자인 모드에서는 더 큰 크기 허용) - const gridColumns = component.gridColumns || 1; - let calculatedWidth: string; - - if (isDesignMode) { - // 디자인 모드에서는 더 큰 최소 크기 적용 - if (gridColumns === 1) { - calculatedWidth = "400px"; // 1컬럼일 때 400px (디자인 모드) - } else if (gridColumns === 2) { - calculatedWidth = "600px"; // 2컬럼일 때 600px (디자인 모드) - } else if (gridColumns <= 6) { - calculatedWidth = `${gridColumns * 250}px`; // 컬럼당 250px (디자인 모드) - } else { - calculatedWidth = "100%"; // 7컬럼 이상은 전체 - } - } else { - // 일반 모드는 기존 크기 유지 - if (gridColumns === 1) { - calculatedWidth = "200px"; // 1컬럼일 때 200px 고정 - } else if (gridColumns === 2) { - calculatedWidth = "400px"; // 2컬럼일 때 400px - } else if (gridColumns <= 6) { - calculatedWidth = `${gridColumns * 200}px`; // 컬럼당 200px - } else { - calculatedWidth = "100%"; // 7컬럼 이상은 전체 - } - } - - // 디버깅 로그 제거 (성능상 이유로) - - // 스타일 계산 (컨테이너에 맞춤) - const componentStyle: React.CSSProperties = { - width: "100%", // 컨테이너 전체 너비 사용 - maxWidth: "100%", // 최대 너비 제한 - height: "auto", // 항상 자동 높이로 테이블 크기에 맞춤 - minHeight: isDesignMode ? "200px" : "300px", // 최소 높이만 보장 - maxHeight: isDesignMode ? "600px" : "800px", // 최대 높이 제한으로 스크롤 활성화 - ...component.style, - ...style, - display: "flex", - flexDirection: "column", - boxSizing: "border-box", // 패딩/보더 포함한 크기 계산 - // overflow는 CSS 클래스로 처리 - }; - - // 🎯 tableContainerStyle 제거 - componentStyle만 사용 - - // 디자인 모드 스타일 - if (isDesignMode) { - componentStyle.border = "2px dashed #cbd5e1"; - componentStyle.borderColor = isSelected ? "#3b82f6" : "#cbd5e1"; - componentStyle.borderRadius = "8px"; - componentStyle.padding = "4px"; // 약간의 패딩으로 구분감 확보 - componentStyle.margin = "2px"; // 외부 여백으로 레이아웃과 구분 - // 🎯 컨테이너에 맞춤 - componentStyle.width = "calc(100% - 12px)"; // margin + padding 보정 - componentStyle.maxWidth = "calc(100% - 12px)"; - componentStyle.minWidth = "calc(100% - 12px)"; - componentStyle.overflow = "hidden !important"; // 넘치는 부분 숨김 (강제) - componentStyle.boxSizing = "border-box"; // 패딩 포함 크기 계산 - componentStyle.position = "relative"; // 위치 고정 - // 자동 높이로 테이블 전체를 감쌈 - } - - // 컬럼 라벨 정보 가져오기 (캐싱 적용) const fetchColumnLabels = async () => { if (!tableConfig.selectedTable) return; - // 캐시 확인 - const cacheKey = tableConfig.selectedTable; - const cached = tableColumnCache.get(cacheKey); - const now = Date.now(); + try { + const cacheKey = `columns_${tableConfig.selectedTable}`; + const cached = tableColumnCache.get(cacheKey); + if (cached && Date.now() - cached.timestamp < TABLE_CACHE_TTL) { + const labels: Record = {}; + const meta: Record = {}; - let columns: any[] = []; + cached.columns.forEach((col: any) => { + labels[col.columnName] = col.displayName || col.comment || col.columnName; + meta[col.columnName] = { + webType: col.webType, + codeCategory: col.codeCategory, + }; + }); - 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 || []; - - // 캐시 저장 - tableColumnCache.set(cacheKey, { columns, timestamp: now }); - console.log(`✅ 테이블 컬럼 캐시 저장: ${cacheKey} (${columns.length}개 컬럼)`); - } catch (error) { - console.log("컬럼 라벨 정보를 가져올 수 없습니다:", error); + setColumnLabels(labels); + setColumnMeta(meta); return; } + + const columns = await tableTypeApi.getColumns(tableConfig.selectedTable); + + tableColumnCache.set(cacheKey, { + columns, + timestamp: Date.now(), + }); + + const labels: Record = {}; + const meta: Record = {}; + + columns.forEach((col: any) => { + labels[col.columnName] = col.displayName || col.comment || col.columnName; + meta[col.columnName] = { + webType: col.webType, + codeCategory: col.codeCategory, + }; + }); + + setColumnLabels(labels); + setColumnMeta(meta); + } catch (error) { + console.error("컬럼 라벨 가져오기 실패:", error); } - - 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; - // 캐시 확인 - 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); + try { + const cacheKey = `table_info_${tableConfig.selectedTable}`; + const cached = tableInfoCache.get(cacheKey); + if (cached && Date.now() - cached.timestamp < TABLE_CACHE_TTL) { + const tables = cached.tables || []; + const tableInfo = tables.find((t: any) => t.tableName === tableConfig.selectedTable); + const label = + tableInfo?.displayName || (tableInfo as any)?.comment || tableInfo?.description || tableConfig.selectedTable; + setTableLabel(label); return; } - } - 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); + const tables = await tableTypeApi.getTables(); + + tableInfoCache.set(cacheKey, { + tables, + timestamp: Date.now(), + }); + + const tableInfo = tables.find((t: any) => t.tableName === tableConfig.selectedTable); + const label = + tableInfo?.displayName || (tableInfo as any)?.comment || tableInfo?.description || tableConfig.selectedTable; + setTableLabel(label); + } catch (error) { + console.error("테이블 라벨 가져오기 실패:", error); } }; - // 실제 테이블 데이터 가져오기 함수 + // ======================================== + // 데이터 가져오기 + // ======================================== + const fetchTableDataInternal = useCallback(async () => { - if (!tableConfig.selectedTable) { + if (!tableConfig.selectedTable || isDesignMode) { setData([]); + setTotalPages(0); + setTotalItems(0); return; } + // 테이블명 확인 로그 + console.log("🔍 fetchTableDataInternal - selectedTable:", tableConfig.selectedTable); + console.log("🔍 selectedTable 타입:", typeof tableConfig.selectedTable); + console.log("🔍 전체 tableConfig:", tableConfig); + setLoading(true); setError(null); try { - // 🎯 Entity 조인 API 사용 - Entity 조인이 포함된 데이터 조회 + const page = tableConfig.pagination?.currentPage || currentPage; + const pageSize = localPageSize; + const sortBy = sortColumn || undefined; + const sortOrder = sortDirection; + const search = searchTerm || undefined; + const filters = Object.keys(searchValues).length > 0 ? searchValues : undefined; - // Entity 조인 컬럼 추출 (isEntityJoin === true인 컬럼들) - const entityJoinColumns = tableConfig.columns?.filter((col) => col.isEntityJoin && col.entityJoinInfo) || []; + const entityJoinColumns = (tableConfig.columns || []) + .filter((col) => col.additionalJoinInfo) + .map((col) => ({ + sourceTable: col.additionalJoinInfo!.sourceTable, + sourceColumn: col.additionalJoinInfo!.sourceColumn, + joinAlias: col.additionalJoinInfo!.joinAlias, + referenceTable: col.additionalJoinInfo!.referenceTable, + })); - // 🎯 조인 탭에서 추가한 컬럼들 추출 (additionalJoinInfo가 있는 컬럼들) - const manualJoinColumns = - tableConfig.columns?.filter((col) => { - return col.additionalJoinInfo !== undefined; - }) || []; + const hasEntityJoins = entityJoinColumns.length > 0; - console.log( - "🔗 수동 조인 컬럼 감지:", - manualJoinColumns.map((c) => ({ - columnName: c.columnName, - additionalJoinInfo: c.additionalJoinInfo, - })), - ); - - // 🎯 추가 조인 컬럼 정보 구성 - const additionalJoinColumns: Array<{ - sourceTable: string; - sourceColumn: string; - joinAlias: string; - referenceTable?: string; - }> = []; - - // Entity 조인 컬럼들 - entityJoinColumns.forEach((col) => { - additionalJoinColumns.push({ - sourceTable: col.entityJoinInfo!.sourceTable, - sourceColumn: col.entityJoinInfo!.sourceColumn, - joinAlias: col.entityJoinInfo!.joinAlias, + let response; + if (hasEntityJoins) { + response = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, { + page, + size: pageSize, + sortBy, + sortOrder, + search: filters, + enableEntityJoin: true, + additionalJoinColumns: entityJoinColumns, + }); + } else { + response = await tableTypeApi.getTableData(tableConfig.selectedTable, { + page, + size: pageSize, + sortBy, + sortOrder, + search: filters, }); - }); - - // 수동 조인 컬럼들 - 저장된 조인 정보 사용 - manualJoinColumns.forEach((col) => { - if (col.additionalJoinInfo) { - additionalJoinColumns.push({ - sourceTable: col.additionalJoinInfo.sourceTable, - sourceColumn: col.additionalJoinInfo.sourceColumn, - joinAlias: col.additionalJoinInfo.joinAlias, - referenceTable: col.additionalJoinInfo.referenceTable, - }); - } - }); - - console.log("🔗 최종 추가 조인 컬럼:", additionalJoinColumns); - - const result = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, { - page: currentPage, - size: localPageSize, - search: (() => { - // 고급 필터 값이 있으면 우선 사용 - const hasAdvancedFilters = Object.values(searchValues).some((value) => { - if (typeof value === "string") return value.trim() !== ""; - if (typeof value === "object" && value !== null) { - return Object.values(value).some((v) => v !== "" && v !== null && v !== undefined); - } - return value !== null && value !== undefined && value !== ""; - }); - - if (hasAdvancedFilters) { - console.log("🔍 고급 검색 필터 사용:", searchValues); - console.log("🔍 고급 검색 필터 상세:", JSON.stringify(searchValues, null, 2)); - return searchValues; - } - - // 고급 필터가 없으면 기존 단순 검색 사용 - if (searchTerm?.trim()) { - // 스마트한 단일 컬럼 선택 로직 (서버가 OR 검색을 지원하지 않음) - let searchColumn = sortColumn; // 정렬된 컬럼 우선 - - if (!searchColumn) { - // 1순위: name 관련 컬럼 (가장 검색에 적합) - const nameColumns = visibleColumns.filter( - (col) => - col.columnName.toLowerCase().includes("name") || - col.columnName.toLowerCase().includes("title") || - col.columnName.toLowerCase().includes("subject"), - ); - - // 2순위: text/varchar 타입 컬럼 - const textColumns = visibleColumns.filter((col) => col.dataType === "text" || col.dataType === "varchar"); - - // 3순위: description 관련 컬럼 - const descColumns = visibleColumns.filter( - (col) => - col.columnName.toLowerCase().includes("desc") || - col.columnName.toLowerCase().includes("comment") || - col.columnName.toLowerCase().includes("memo"), - ); - - // 우선순위에 따라 선택 - if (nameColumns.length > 0) { - searchColumn = nameColumns[0].columnName; - } else if (textColumns.length > 0) { - searchColumn = textColumns[0].columnName; - } else if (descColumns.length > 0) { - searchColumn = descColumns[0].columnName; - } else { - // 마지막 대안: 첫 번째 컬럼 - searchColumn = visibleColumns[0]?.columnName || "id"; - } - } - - console.log("🔍 기존 검색 방식 사용:", { [searchColumn]: searchTerm }); - return { [searchColumn]: searchTerm }; - } - - return undefined; - })(), - sortBy: sortColumn || undefined, - sortOrder: sortDirection, - enableEntityJoin: true, // 🎯 Entity 조인 활성화 - additionalJoinColumns: additionalJoinColumns.length > 0 ? additionalJoinColumns : undefined, // 추가 조인 컬럼 - }); - - if (result) { - // console.log("🎯 API 응답 결과:", result); - // console.log("🎯 데이터 개수:", result.data?.length || 0); - // console.log("🎯 전체 페이지:", result.totalPages); - // console.log("🎯 총 아이템:", result.total); - setData(result.data || []); - setTotalPages(result.totalPages || 1); - setTotalItems(result.total || 0); - - // 🎯 Entity 조인 정보 로깅 - if (result.entityJoinInfo) { - console.log("🔗 Entity 조인 적용됨:", { - strategy: result.entityJoinInfo.strategy, - joinConfigs: result.entityJoinInfo.joinConfigs, - performance: result.entityJoinInfo.performance, - }); - } else { - console.log("🔗 Entity 조인 없음"); - } - - // 🎯 코드 컬럼들의 캐시 미리 로드 (전역 캐시 사용) - const codeColumns = Object.entries(columnMeta).filter( - ([_, meta]) => meta.webType === "code" && meta.codeCategory, - ); - - if (codeColumns.length > 0) { - console.log( - "📋 코드 컬럼 감지:", - codeColumns.map(([col, meta]) => `${col}(${meta.codeCategory})`), - ); - - // 필요한 코드 카테고리들을 추출하여 배치 로드 (중복 제거) - const categoryList = [ - ...new Set(codeColumns.map(([, meta]) => meta.codeCategory).filter(Boolean)), - ] as string[]; - - // 이미 캐시된 카테고리는 제외 - 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("📋 모든 코드 카테고리가 이미 캐시됨"); - } - } - - // 🎯 Entity 조인된 컬럼 처리 - 사용자가 설정한 컬럼들만 사용 - let processedColumns = [...(tableConfig.columns || [])]; - - // 초기 컬럼이 있으면 먼저 설정 - if (processedColumns.length > 0) { - console.log( - "🔍 사용자 설정 컬럼들:", - processedColumns.map((c) => c.columnName), - ); - - // 🎯 API 응답과 비교하여 존재하지 않는 컬럼 필터링 - if (result.data.length > 0) { - const actualApiColumns = Object.keys(result.data[0]); - console.log("🔍 API 응답의 실제 컬럼들:", actualApiColumns); - - // 🎯 조인 컬럼 매핑 테이블 - 동적 생성 - // API 응답에 실제로 존재하는 컬럼과 사용자 설정 컬럼을 비교하여 자동 매핑 - const newJoinColumnMapping: Record = {}; - - processedColumns.forEach((col) => { - // API 응답에 정확히 일치하는 컬럼이 있으면 그대로 사용 - if (actualApiColumns.includes(col.columnName)) { - newJoinColumnMapping[col.columnName] = col.columnName; - } - }); - - // 🎯 조인 컬럼 매핑 상태 업데이트 - setJoinColumnMapping(newJoinColumnMapping); - - console.log("🔍 조인 컬럼 매핑 테이블:", newJoinColumnMapping); - console.log("🔍 실제 API 응답 컬럼들:", actualApiColumns); - - // 🎯 컬럼명 매핑 및 유효성 검사 - const validColumns = processedColumns - .map((col) => { - // 체크박스는 그대로 유지 - if (col.columnName === "__checkbox__") return col; - - // 조인 컬럼 매핑 적용 - const mappedColumnName = newJoinColumnMapping[col.columnName] || col.columnName; - - console.log(`🔍 컬럼 매핑 처리: ${col.columnName} → ${mappedColumnName}`); - - // API 응답에 존재하는지 확인 - const existsInApi = actualApiColumns.includes(mappedColumnName); - - if (!existsInApi) { - console.log(`🔍 제거될 컬럼: ${col.columnName} → ${mappedColumnName} (API에 존재하지 않음)`); - return null; - } - - // 컬럼명이 변경된 경우 업데이트 - if (mappedColumnName !== col.columnName) { - console.log(`🔄 컬럼명 매핑: ${col.columnName} → ${mappedColumnName}`); - return { - ...col, - columnName: mappedColumnName, - }; - } - - console.log(`✅ 컬럼 유지: ${col.columnName}`); - return col; - }) - .filter((col) => col !== null) as ColumnConfig[]; - - if (validColumns.length !== processedColumns.length) { - console.log( - "🔍 필터링된 컬럼들:", - validColumns.map((c) => c.columnName), - ); - console.log( - "🔍 제거된 컬럼들:", - processedColumns - .filter((col) => { - const mappedName = newJoinColumnMapping[col.columnName] || col.columnName; - return !actualApiColumns.includes(mappedName) && col.columnName !== "__checkbox__"; - }) - .map((c) => c.columnName), - ); - processedColumns = validColumns; - } - } - } - if (result.entityJoinInfo?.joinConfigs) { - result.entityJoinInfo.joinConfigs.forEach((joinConfig) => { - // 원본 컬럼을 조인된 컬럼으로 교체 - const originalColumnIndex = processedColumns.findIndex((col) => col.columnName === joinConfig.sourceColumn); - - if (originalColumnIndex !== -1) { - console.log(`🔄 컬럼 교체: ${joinConfig.sourceColumn} → ${joinConfig.aliasColumn}`); - const originalColumn = processedColumns[originalColumnIndex]; - processedColumns[originalColumnIndex] = { - ...originalColumn, - columnName: joinConfig.aliasColumn, // dept_code → dept_code_name - displayName: - columnLabels[originalColumn.columnName] || originalColumn.displayName || originalColumn.columnName, // 올바른 라벨 사용 - // isEntityJoined: true, // 🎯 임시 주석 처리 (타입 에러 해결 후 복원) - } as ColumnConfig; - console.log( - `✅ 조인 컬럼 라벨 유지: ${joinConfig.sourceColumn} → "${columnLabels[originalColumn.columnName] || originalColumn.displayName || originalColumn.columnName}"`, - ); - } - }); - } - - // 🎯 컬럼 설정이 없으면 API 응답 기반으로 생성 - if ((!processedColumns || processedColumns.length === 0) && result.data.length > 0) { - const autoColumns: ColumnConfig[] = Object.keys(result.data[0]).map((key, index) => ({ - columnName: key, - displayName: columnLabels[key] || key, - visible: true, - sortable: true, - searchable: true, - align: "left", - format: "text", - order: index, - })); - - console.log( - "🎯 자동 생성된 컬럼들:", - autoColumns.map((c) => c.columnName), - ); - - // 컴포넌트 설정 업데이트 (부모 컴포넌트에 알림) - if (onFormDataChange) { - onFormDataChange({ - ...component, - config: { - ...tableConfig, - columns: autoColumns, - }, - }); - } - processedColumns = autoColumns; - } - - // 🎯 표시할 컬럼 상태 업데이트 - setDisplayColumns(processedColumns); - // console.log("🎯 displayColumns 업데이트됨:", processedColumns); - // console.log("🎯 데이터 개수:", result.data?.length || 0); - // console.log("🎯 전체 데이터:", result.data); } - } catch (err) { - console.error("테이블 데이터 로딩 오류:", err); - setError(err instanceof Error ? err.message : "데이터를 불러오는 중 오류가 발생했습니다."); + + setData(response.data || []); + setTotalPages(response.totalPages || 0); + setTotalItems(response.total || 0); + setError(null); + } catch (err: any) { + console.error("데이터 가져오기 실패:", err); setData([]); + setTotalPages(0); + setTotalItems(0); + setError(err.message || "데이터를 불러오지 못했습니다."); } finally { setLoading(false); } }, [ tableConfig.selectedTable, + tableConfig.pagination?.currentPage, tableConfig.columns, currentPage, localPageSize, - searchTerm, sortColumn, sortDirection, + searchTerm, searchValues, + isDesignMode, ]); - // 디바운싱된 테이블 데이터 가져오기 const fetchTableDataDebounced = useCallback( - debouncedApiCall( - `fetchTableData_${tableConfig.selectedTable}_${currentPage}_${localPageSize}`, - async () => { - return fetchTableDataInternal(); - }, - 200, // 200ms 디바운스 - ), - [ - tableConfig.selectedTable, - currentPage, - localPageSize, - searchTerm, - sortColumn, - sortDirection, - searchValues, - fetchTableDataInternal, - ], + (...args: Parameters) => { + const key = `fetchData_${tableConfig.selectedTable}_${currentPage}_${sortColumn}_${sortDirection}`; + return debouncedApiCall(key, fetchTableDataInternal, 300)(...args); + }, + [fetchTableDataInternal, tableConfig.selectedTable, currentPage, sortColumn, sortDirection], ); - // 페이지 변경 - const handlePageChange = (newPage: number) => { - setCurrentPage(newPage); + // ======================================== + // 이벤트 핸들러 + // ======================================== - // 상세설정에 현재 페이지 정보 알림 (필요한 경우) - if (onConfigChange && tableConfig.pagination) { - // console.log("📤 테이블에서 페이지 변경을 상세설정에 알림:", newPage); - onConfigChange({ - ...tableConfig, - pagination: { - ...tableConfig.pagination, - currentPage: newPage, // 현재 페이지 정보 추가 - }, - }); - } else if (!onConfigChange) { - // console.warn("⚠️ onConfigChange가 정의되지 않음 - 페이지 변경 상세설정과 연동 불가"); + const handlePageChange = (newPage: number) => { + if (newPage < 1 || newPage > totalPages) return; + setCurrentPage(newPage); + if (tableConfig.pagination) { + tableConfig.pagination.currentPage = newPage; + } + if (onConfigChange) { + onConfigChange({ ...tableConfig, pagination: { ...tableConfig.pagination, currentPage: newPage } }); } }; - // 정렬 변경 const handleSort = (column: string) => { if (sortColumn === column) { setSortDirection(sortDirection === "asc" ? "desc" : "asc"); @@ -828,12 +463,8 @@ export const TableListComponent: React.FC = ({ } }; - // 고급 필터 핸들러 const handleSearchValueChange = (columnName: string, value: any) => { - setSearchValues((prev) => ({ - ...prev, - [columnName]: value, - })); + setSearchValues((prev) => ({ ...prev, [columnName]: value })); }; const handleAdvancedSearch = () => { @@ -847,15 +478,12 @@ export const TableListComponent: React.FC = ({ fetchTableDataDebounced(); }; - // 새로고침 const handleRefresh = () => { fetchTableDataDebounced(); }; - // 체크박스 핸들러들 const getRowKey = (row: any, index: number) => { - // 기본키가 있으면 사용, 없으면 인덱스 사용 - return row.id || row.objid || row.pk || index.toString(); + return row.id || row.uuid || `row-${index}`; }; const handleRowSelection = (rowKey: string, checked: boolean) => { @@ -866,468 +494,54 @@ export const TableListComponent: React.FC = ({ newSelectedRows.delete(rowKey); } setSelectedRows(newSelectedRows); - setIsAllSelected(newSelectedRows.size === data.length && data.length > 0); - // 선택된 실제 데이터를 상위 컴포넌트로 전달 - const selectedKeys = Array.from(newSelectedRows); - const selectedData = selectedKeys - .map((key) => { - // rowKey를 사용하여 데이터 찾기 (ID 기반 또는 인덱스 기반) - return data.find((row, index) => { - const currentRowKey = getRowKey(row, index); - return currentRowKey === key; - }); - }) - .filter(Boolean); - - console.log("🔍 handleRowSelection 디버그:", { - rowKey, - checked, - selectedKeys, - selectedData, - dataCount: data.length, - }); - - onSelectedRowsChange?.(selectedKeys, selectedData); - - if (tableConfig.onSelectionChange) { - tableConfig.onSelectionChange(selectedData); + const selectedRowsData = data.filter((row, index) => newSelectedRows.has(getRowKey(row, index))); + if (onSelectedRowsChange) { + onSelectedRowsChange(Array.from(newSelectedRows), selectedRowsData); } + if (onFormDataChange) { + onFormDataChange({ selectedRows: Array.from(newSelectedRows), selectedRowsData }); + } + + const allRowsSelected = data.every((row, index) => newSelectedRows.has(getRowKey(row, index))); + setIsAllSelected(allRowsSelected && data.length > 0); }; const handleSelectAll = (checked: boolean) => { if (checked) { const allKeys = data.map((row, index) => getRowKey(row, index)); - setSelectedRows(new Set(allKeys)); + const newSelectedRows = new Set(allKeys); + setSelectedRows(newSelectedRows); setIsAllSelected(true); - // 선택된 실제 데이터를 상위 컴포넌트로 전달 - onSelectedRowsChange?.(allKeys, data); - - if (tableConfig.onSelectionChange) { - tableConfig.onSelectionChange(data); + if (onSelectedRowsChange) { + onSelectedRowsChange(Array.from(newSelectedRows), data); } - } else { - setSelectedRows(new Set()); - setIsAllSelected(false); - - // 빈 선택을 상위 컴포넌트로 전달 - onSelectedRowsChange?.([], []); - - if (tableConfig.onSelectionChange) { - tableConfig.onSelectionChange([]); - } - } - }; - - // 효과 - useEffect(() => { - if (tableConfig.selectedTable) { - fetchColumnLabels(); - fetchTableLabel(); - } - }, [tableConfig.selectedTable]); - - // 컬럼 라벨이 로드되면 기존 컬럼의 displayName을 업데이트 - useEffect(() => { - if (Object.keys(columnLabels).length > 0 && tableConfig.columns && tableConfig.columns.length > 0) { - const updatedColumns = tableConfig.columns.map((col) => ({ - ...col, - displayName: columnLabels[col.columnName] || col.displayName, - })); - - // 부모 컴포넌트에 업데이트된 컬럼 정보 전달 if (onFormDataChange) { - onFormDataChange({ - ...component, - componentConfig: { - ...tableConfig, - columns: updatedColumns, - }, - }); + onFormDataChange({ selectedRows: Array.from(newSelectedRows), selectedRowsData: data }); } - } - }, [columnLabels]); - - // 🎯 컬럼 개수와 컬럼명을 문자열로 변환하여 의존성 추적 - const columnsKey = useMemo(() => { - if (!tableConfig.columns) return ""; - return tableConfig.columns.map((col) => col.columnName).join(","); - }, [tableConfig.columns]); - - useEffect(() => { - // autoLoad가 undefined거나 true일 때 자동 로드 (기본값: true) - const shouldAutoLoad = tableConfig.autoLoad !== false; - - console.log("🔍 TableList 데이터 로드 조건 체크:", { - shouldAutoLoad, - isDesignMode, - selectedTable: tableConfig.selectedTable, - autoLoadSetting: tableConfig.autoLoad, - willLoad: shouldAutoLoad && !isDesignMode, - }); - - if (shouldAutoLoad && !isDesignMode) { - console.log("✅ 테이블 데이터 로드 시작:", tableConfig.selectedTable); - fetchTableDataInternal(); } else { - console.warn("⚠️ 테이블 데이터 로드 차단:", { - reason: !shouldAutoLoad ? "autoLoad=false" : "isDesignMode=true", - shouldAutoLoad, - isDesignMode, - }); - } - }, [ - tableConfig.selectedTable, - columnsKey, // 🎯 컬럼이 추가/변경될 때 데이터 다시 로드 (문자열 비교) - localPageSize, - currentPage, - searchTerm, - sortColumn, - sortDirection, - columnLabels, - searchValues, - fetchTableDataInternal, // 의존성 배열에 추가 - ]); - - // refreshKey 변경 시 테이블 데이터 새로고침 - useEffect(() => { - if (refreshKey && refreshKey > 0 && !isDesignMode) { - console.log("🔄 refreshKey 변경 감지, 테이블 데이터 새로고침:", refreshKey); - // 선택된 행 상태 초기화 setSelectedRows(new Set()); setIsAllSelected(false); - // 부모 컴포넌트에 빈 선택 상태 전달 - console.log("🔄 선택 상태 초기화 - 빈 배열 전달"); - onSelectedRowsChange?.([], []); - // 테이블 데이터 새로고침 - fetchTableDataDebounced(); - } - }, [refreshKey]); - // 🆕 전역 테이블 새로고침 이벤트 리스너 - useEffect(() => { - const handleRefreshTable = () => { - console.log("🔄 TableListComponent: 전역 새로고침 이벤트 수신"); - if (tableConfig.selectedTable && !isDesignMode) { - // 선택된 행 상태 초기화 - setSelectedRows(new Set()); - setIsAllSelected(false); - onSelectedRowsChange?.([], []); - // 테이블 데이터 새로고침 - fetchTableDataDebounced(); + if (onSelectedRowsChange) { + onSelectedRowsChange([], []); } - }; - - window.addEventListener("refreshTable", handleRefreshTable); - - return () => { - window.removeEventListener("refreshTable", handleRefreshTable); - }; - }, [tableConfig.selectedTable, isDesignMode, fetchTableDataDebounced, onSelectedRowsChange]); - - // 상세설정에서 페이지네이션 설정 변경 시 로컬 상태 동기화 - useEffect(() => { - // 페이지 크기 동기화 - if (tableConfig.pagination?.pageSize && tableConfig.pagination.pageSize !== localPageSize) { - // console.log("🔄 상세설정에서 페이지 크기 변경 감지:", tableConfig.pagination.pageSize); - setLocalPageSize(tableConfig.pagination.pageSize); - setCurrentPage(1); // 페이지를 1로 리셋 - } - - // 현재 페이지 동기화 (상세설정에서 페이지를 직접 변경한 경우) - if (tableConfig.pagination?.currentPage && tableConfig.pagination.currentPage !== currentPage) { - // console.log("🔄 상세설정에서 현재 페이지 변경 감지:", tableConfig.pagination.currentPage); - setCurrentPage(tableConfig.pagination.currentPage); - } - }, [tableConfig.pagination?.pageSize, tableConfig.pagination?.currentPage]); - - // 표시할 컬럼 계산 (Entity 조인 적용됨 + 체크박스 컬럼 추가 + 숨김 기능) - const visibleColumns = useMemo(() => { - // 기본값 처리: checkbox 설정이 없으면 기본값 사용 - const checkboxConfig = tableConfig.checkbox || { - enabled: true, - multiple: true, - position: "left", - selectAll: true, - }; - - let columns: ColumnConfig[] = []; - - // displayColumns가 있으면 우선 사용 (Entity 조인 적용된 컬럼들) - if (displayColumns && displayColumns.length > 0) { - // 디버깅 로그 제거 (성능상 이유로) - const filteredColumns = displayColumns.filter((col) => { - // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 - if (isDesignMode) { - return col.visible; // 디자인 모드에서는 visible만 체크 - } else { - return col.visible && !col.hidden; // 실제 화면에서는 visible이면서 hidden이 아닌 것만 - } - }); - // 디버깅 로그 제거 (성능상 이유로) - columns = filteredColumns.sort((a, b) => a.order - b.order); - } else if (tableConfig.columns && tableConfig.columns.length > 0) { - // displayColumns가 없으면 기본 컬럼 사용 - // 디버깅 로그 제거 (성능상 이유로) - columns = tableConfig.columns - .filter((col) => { - // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 - if (isDesignMode) { - return col.visible; // 디자인 모드에서는 visible만 체크 - } else { - return col.visible && !col.hidden; // 실제 화면에서는 visible이면서 hidden이 아닌 것만 - } - }) - .sort((a, b) => a.order - b.order); - } else { - // console.log("🎯 사용할 컬럼이 없음"); - return []; - } - - // 체크박스가 활성화되고 실제 데이터 컬럼이 있는 경우에만 체크박스 컬럼을 추가 - if (checkboxConfig.enabled && columns.length > 0) { - const checkboxColumn: ColumnConfig = { - columnName: "__checkbox__", - displayName: "", - visible: true, - sortable: false, - searchable: false, - width: 50, - align: "center", - order: -1, // 가장 앞에 위치 - fixed: checkboxConfig.position === "left" ? "left" : false, - fixedOrder: 0, // 가장 앞에 고정 - }; - - // 체크박스 위치에 따라 추가 - if (checkboxConfig.position === "left") { - columns.unshift(checkboxColumn); - } else { - columns.push(checkboxColumn); + if (onFormDataChange) { + onFormDataChange({ selectedRows: [], selectedRowsData: [] }); } } - - // 디버깅 로그 제거 (성능상 이유로) - return columns; - }, [displayColumns, tableConfig.columns, tableConfig.checkbox, isDesignMode]); - - // columnsByPosition은 SingleTableWithSticky에서 사용하지 않으므로 제거 - // 기존 테이블에서만 필요한 경우 다시 추가 가능 - - // 가로 스크롤이 필요한지 계산 - const needsHorizontalScroll = useMemo(() => { - if (!tableConfig.horizontalScroll?.enabled) { - console.log("🚫 가로 스크롤 비활성화됨"); - return false; - } - - const maxVisible = tableConfig.horizontalScroll.maxVisibleColumns || 8; - const totalColumns = visibleColumns.length; - const result = totalColumns > maxVisible; - - console.log( - `🔍 가로 스크롤 계산: ${totalColumns}개 컬럼 > ${maxVisible}개 최대 = ${result ? "스크롤 필요" : "스크롤 불필요"}`, - ); - console.log("📊 가로 스크롤 설정:", tableConfig.horizontalScroll); - console.log( - "📋 현재 컬럼들:", - visibleColumns.map((c) => c.columnName), - ); - - return result; - }, [visibleColumns.length, tableConfig.horizontalScroll]); - - // 컬럼 너비 계산 - 내용 길이에 맞게 자동 조정 - const getColumnWidth = (column: ColumnConfig) => { - if (column.width) return column.width; - - // 체크박스 컬럼인 경우 고정 너비 - if (column.columnName === "__checkbox__") { - return 50; - } - - // 컬럼 헤더 텍스트 길이 기반으로 계산 - const headerText = columnLabels[column.columnName] || column.displayName || column.columnName; - const headerLength = headerText.length; - - // 데이터 셀의 최대 길이 추정 (실제 데이터가 있다면 더 정확하게 계산 가능) - const estimatedContentLength = Math.max(headerLength, 10); // 최소 10자 - - // 문자당 약 8px 정도로 계산하고, 패딩 및 여백 고려 - const calculatedWidth = estimatedContentLength * 8 + 40; // 40px는 패딩과 여백 - - // 최소 너비만 보장하고, 최대 너비 제한은 제거 - const minWidth = 80; - - return Math.max(minWidth, calculatedWidth); }; - // 체크박스 헤더 렌더링 - const renderCheckboxHeader = () => { - // 기본값 처리: checkbox 설정이 없으면 기본값 사용 - const checkboxConfig = tableConfig.checkbox || { - enabled: true, - multiple: true, - position: "left", - selectAll: true, - }; - - if (!checkboxConfig.enabled || !checkboxConfig.selectAll) { - return null; - } - - return ( - - ); - }; - - // 체크박스 셀 렌더링 - const renderCheckboxCell = (row: any, index: number) => { - // 기본값 처리: checkbox 설정이 없으면 기본값 사용 - const checkboxConfig = tableConfig.checkbox || { - enabled: true, - multiple: true, - position: "left", - selectAll: true, - }; - - if (!checkboxConfig.enabled) { - return null; - } - - const rowKey = getRowKey(row, index); - const isSelected = selectedRows.has(rowKey); - - return ( - handleRowSelection(rowKey, checked as boolean)} - aria-label={`행 ${index + 1} 선택`} - style={{ zIndex: 1 }} - /> - ); - }; - - // 🎯 값 포맷팅 (전역 코드 캐시 사용) - const formatCellValue = useMemo(() => { - return (value: any, format?: string, columnName?: string) => { - if (value === null || value === undefined) return ""; - - // 디버깅 로그 제거 (성능상 이유로) - - // 🎯 코드 컬럼인 경우 최적화된 코드명 변환 사용 - if (columnName && columnMeta[columnName]?.webType === "code" && columnMeta[columnName]?.codeCategory) { - const categoryCode = columnMeta[columnName].codeCategory!; - const convertedValue = optimizedConvertCode(categoryCode, String(value)); - - // 코드 변환 로그 제거 (성능상 이유로) - - value = convertedValue; - } - - switch (format) { - case "number": - return typeof value === "number" ? value.toLocaleString() : value; - case "currency": - return typeof value === "number" ? `₩${value.toLocaleString()}` : value; - case "date": - return value instanceof Date ? value.toLocaleDateString() : value; - case "boolean": - return value ? "예" : "아니오"; - default: - return String(value); - } - }; - }, [columnMeta, optimizedConvertCode]); // 최적화된 변환 함수 의존성 추가 - - // 이벤트 핸들러 - const handleClick = (e: React.MouseEvent) => { - e.stopPropagation(); - onClick?.(); - }; - - // 행 클릭 핸들러 const handleRowClick = (row: any) => { - if (tableConfig.onRowClick) { - tableConfig.onRowClick(row); - } + console.log("행 클릭:", row); }; - // 드래그 핸들러 (그리드 스냅 지원) const handleRowDragStart = (e: React.DragEvent, row: any, index: number) => { setIsDragging(true); setDraggedRowIndex(index); - - // 드래그 데이터에 그리드 정보 포함 - const dragData = { - ...row, - _dragType: "table-row", - _gridSize: { width: 4, height: 1 }, // 기본 그리드 크기 (4칸 너비, 1칸 높이) - _snapToGrid: true, - }; - - 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, #212121 0%, #000000 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"; - dragImage.innerHTML = ` -
📋
- ${firstValue} -
4×1
- `; - - document.body.appendChild(dragImage); - e.dataTransfer.setDragImage(dragImage, 20, 20); - - // 정리 - setTimeout(() => { - if (document.body.contains(dragImage)) { - document.body.removeChild(dragImage); - } - }, 0); + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("application/json", JSON.stringify(row)); }; const handleRowDragEnd = (e: React.DragEvent) => { @@ -1335,497 +549,610 @@ export const TableListComponent: React.FC = ({ setDraggedRowIndex(null); }; - // DOM에 전달할 수 있는 기본 props만 정의 - const domProps = { - onClick: handleClick, - onDragStart, - onDragEnd, + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation(); + onClick?.(); }; - // 플레이스홀더 제거 - 디자인 모드에서도 바로 테이블 표시 + // ======================================== + // 컬럼 관련 + // ======================================== - return ( -
- {/* 헤더 */} - {tableConfig.showHeader && ( -
-
- {(tableConfig.title || tableLabel) && ( -

{tableConfig.title || tableLabel}

- )} -
+ const visibleColumns = useMemo(() => { + let cols = (tableConfig.columns || []).filter((col) => col.visible !== false); -
- {/* 선택된 항목 정보 표시 */} - {selectedRows.size > 0 && ( -
- {selectedRows.size}개 선택됨 -
- )} + if (tableConfig.checkbox?.enabled) { + const checkboxCol: ColumnConfig = { + columnName: "__checkbox__", + displayName: "", + visible: true, + sortable: false, + searchable: false, + width: 50, + align: "center", + order: -1, + }; - {/* 새로고침 */} - -
-
- )} + if (tableConfig.checkbox.position === "right") { + cols = [...cols, checkboxCol]; + } else { + cols = [checkboxCol, ...cols]; + } + } - {/* 고급 검색 필터 - 항상 표시 (컬럼 정보 기반 자동 생성) */} - {tableConfig.filter?.enabled && visibleColumns && visibleColumns.length > 0 && ( - <> -
-
- ({ - 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} - /> -
- - )} + return cols.sort((a, b) => (a.order || 0) - (b.order || 0)); + }, [tableConfig.columns, tableConfig.checkbox]); - {/* 테이블 컨텐츠 */} + const getColumnWidth = (column: ColumnConfig) => { + if (column.columnName === "__checkbox__") return 50; + if (column.width) return column.width; + + switch (column.format) { + case "date": + return 120; + case "number": + case "currency": + return 100; + case "boolean": + return 80; + default: + return 150; + } + }; + + const renderCheckboxHeader = () => { + if (!tableConfig.checkbox?.selectAll) return null; + + return ; + }; + + const renderCheckboxCell = (row: any, index: number) => { + const rowKey = getRowKey(row, index); + const isChecked = selectedRows.has(rowKey); + + return ( + handleRowSelection(rowKey, checked as boolean)} + aria-label={`행 ${index + 1} 선택`} + /> + ); + }; + + const formatCellValue = useCallback( + (value: any, column: ColumnConfig) => { + if (value === null || value === undefined) return "-"; + + const meta = columnMeta[column.columnName]; + if (meta?.webType && meta?.codeCategory) { + const convertedValue = optimizedConvertCode(value, meta.codeCategory); + if (convertedValue !== value) return convertedValue; + } + + switch (column.format) { + case "date": + if (value) { + try { + const date = new Date(value); + return date.toLocaleDateString("ko-KR"); + } catch { + return value; + } + } + return "-"; + case "number": + return typeof value === "number" ? value.toLocaleString() : value; + case "currency": + return typeof value === "number" ? `₩${value.toLocaleString()}` : value; + case "boolean": + return value ? "예" : "아니오"; + default: + return String(value); + } + }, + [columnMeta, optimizedConvertCode], + ); + + // ======================================== + // useEffect 훅 + // ======================================== + + // 필터 설정 localStorage 키 생성 + const filterSettingKey = useMemo(() => { + if (!tableConfig.selectedTable) return null; + return `tableList_filterSettings_${tableConfig.selectedTable}`; + }, [tableConfig.selectedTable]); + + // 저장된 필터 설정 불러오기 + useEffect(() => { + if (!filterSettingKey) return; + + try { + const saved = localStorage.getItem(filterSettingKey); + if (saved) { + const savedFilters = JSON.parse(saved); + setVisibleFilterColumns(new Set(savedFilters)); + } else { + // 초기값: 모든 필터 표시 + const allFilters = (tableConfig.filter?.filters || []).map((f) => f.columnName); + setVisibleFilterColumns(new Set(allFilters)); + } + } catch (error) { + console.error("필터 설정 불러오기 실패:", error); + // 기본값으로 모든 필터 표시 + const allFilters = (tableConfig.filter?.filters || []).map((f) => f.columnName); + setVisibleFilterColumns(new Set(allFilters)); + } + }, [filterSettingKey, tableConfig.filter?.filters]); + + // 필터 설정 저장 + const saveFilterSettings = useCallback(() => { + if (!filterSettingKey) return; + + try { + localStorage.setItem(filterSettingKey, JSON.stringify(Array.from(visibleFilterColumns))); + setIsFilterSettingOpen(false); + } catch (error) { + console.error("필터 설정 저장 실패:", error); + } + }, [filterSettingKey, visibleFilterColumns]); + + // 필터 토글 + const toggleFilterVisibility = useCallback((columnName: string) => { + setVisibleFilterColumns((prev) => { + const newSet = new Set(prev); + if (newSet.has(columnName)) { + newSet.delete(columnName); + } else { + newSet.add(columnName); + } + return newSet; + }); + }, []); + + // 표시할 필터 목록 + const activeFilters = useMemo(() => { + return (tableConfig.filter?.filters || []).filter((f) => visibleFilterColumns.has(f.columnName)); + }, [tableConfig.filter?.filters, visibleFilterColumns]); + + useEffect(() => { + fetchColumnLabels(); + fetchTableLabel(); + }, [tableConfig.selectedTable]); + + useEffect(() => { + if (!isDesignMode && tableConfig.selectedTable) { + fetchTableDataDebounced(); + } + }, [ + tableConfig.selectedTable, + currentPage, + localPageSize, + sortColumn, + sortDirection, + searchTerm, + refreshKey, + isDesignMode, + ]); + + useEffect(() => { + if (tableConfig.refreshInterval && !isDesignMode) { + const interval = setInterval(() => { + fetchTableDataDebounced(); + }, tableConfig.refreshInterval * 1000); + + return () => clearInterval(interval); + } + }, [tableConfig.refreshInterval, isDesignMode]); + + // ======================================== + // 페이지네이션 JSX + // ======================================== + + const paginationJSX = useMemo(() => { + if (!tableConfig.pagination?.enabled || isDesignMode) return null; + + return (
- {loading ? ( -
-
-
-
- -
-
+ {/* 중앙 페이지네이션 컨트롤 */} +
+ + + + + {currentPage} / {totalPages || 1} + + + + + + + 전체 {totalItems.toLocaleString()}개 + +
+ + {/* 우측 새로고침 버튼 */} + +
+ ); + }, [tableConfig.pagination, isDesignMode, currentPage, totalPages, totalItems, loading]); + + // ======================================== + // 렌더링 + // ======================================== + + const domProps = { + onClick: handleClick, + onDragStart: isDesignMode ? onDragStart : undefined, + onDragEnd: isDesignMode ? onDragEnd : undefined, + draggable: isDesignMode, + className: cn(className, isDesignMode && "cursor-move"), + style: componentStyle, + }; + + // 카드 모드 + if (tableConfig.displayMode === "card" && !isDesignMode) { + return ( +
+ + {paginationJSX} +
+ ); + } + + // SingleTableWithSticky 모드 + if (tableConfig.stickyHeader && !isDesignMode) { + return ( +
+ {tableConfig.showHeader && ( +
+

{tableConfig.title || tableLabel}

+
+ )} + + {tableConfig.filter?.enabled && ( +
+
+
+
-
데이터를 불러오는 중...
-
잠시만 기다려주세요
+
- ) : error ? ( -
-
-
-
- ! -
+ )} + +
+ { + const column = visibleColumns.find((c) => c.columnName === columnName); + return column ? formatCellValue(value, column) : String(value); + }} + getColumnWidth={getColumnWidth} + containerWidth={calculatedWidth} + /> +
+ + {paginationJSX} +
+ ); + } + + // 일반 테이블 모드 (네이티브 HTML 테이블) + return ( + <> +
+ {/* 헤더 */} + {tableConfig.showHeader && ( +
+

{tableConfig.title || tableLabel}

+
+ )} + + {/* 필터 */} + {tableConfig.filter?.enabled && ( +
+
+
+
-
오류가 발생했습니다
-
{error}
+
- ) : tableConfig.displayMode === "card" ? ( - // 카드 모드 렌더링 -
- { - const rowIndex = data.findIndex((d) => d === row); - const rowKey = getRowKey(row, rowIndex); - handleRowSelection(rowKey, selected); - }} - selectedRows={Array.from(selectedRows)} - showActions={tableConfig.actions?.showActions} - /> -
- ) : needsHorizontalScroll ? ( - // 가로 스크롤이 필요한 경우 - 단일 테이블에서 sticky 컬럼 사용 -
- -
- ) : ( - // 기존 테이블 (가로 스크롤이 필요한 경우) -
- + {/* 스크롤 영역 */} +
+ {/* 테이블 */} +
- - - {visibleColumns.map((column, colIndex) => ( - + {visibleColumns.map((column) => ( + ))} - - - - {data.length === 0 ? ( - - -
-
- -
-
데이터가 없습니다
-
조건을 변경하거나 새로운 데이터를 추가해보세요
+
+ + + {/* 바디 (스크롤) */} + + {loading ? ( + + + + ) : error ? ( + + + + ) : data.length === 0 ? ( + + + ) : ( data.map((row, index) => ( - handleRowDragStart(e, row, index)} onDragEnd={handleRowDragEnd} - className={cn( - "group relative h-12 cursor-pointer border-b border-gray-100/60 transition-all duration-200", - // 기본 스타일 - tableConfig.tableStyle?.hoverEffect && - "hover:bg-gradient-to-r hover:from-orange-50/80 hover:to-orange-100/60 hover:shadow-sm", - tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/40", - // 드래그 상태 스타일 (미묘하게) - draggedRowIndex === index && - "border-blue-200/60 bg-gradient-to-r from-blue-50/60 to-indigo-50/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%", + borderBottom: "1px solid #f1f5f9", + cursor: "pointer", + transition: "background-color 0.2s", + backgroundColor: index % 2 === 1 ? "#f9fafb" : "white", }} + onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = "#fef3f2")} + onMouseLeave={(e) => + (e.currentTarget.style.backgroundColor = index % 2 === 1 ? "#f9fafb" : "white") + } onClick={() => handleRowClick(row)} > - {visibleColumns.map((column, colIndex) => ( - - {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"; + {visibleColumns.map((column) => { + const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; + const cellValue = row[mappedColumnName]; - // 첫 번째 컬럼에 드래그 핸들과 아바타 추가 - const isFirstColumn = - colIndex === (visibleColumns[0]?.columnName === "__checkbox__" ? 1 : 0); - - return ( -
- {isFirstColumn && !isDesignMode && ( -
- {/* 그리드 스냅 가이드 아이콘 */} -
-
-
-
-
-
-
-
-
-
-
- )} - {formattedValue} -
- ); - })()} -
- ))} -
+ return ( + + ); + })} + )) )} - -
column.sortable && handleSort(column.columnName)} > {column.columnName === "__checkbox__" ? ( renderCheckboxHeader() ) : ( -
+
{columnLabels[column.columnName] || column.displayName} {column.sortable && sortColumn === column.columnName && ( -
- {sortDirection === "asc" ? ( - - ) : ( - - )} -
+ {sortDirection === "asc" ? "↑" : "↓"} )}
)} - +
+
+ +
로딩 중...
- - +
+
+
오류 발생
+
{error}
+
+
+
+ +
데이터가 없습니다
+
+ 조건을 변경하거나 새로운 데이터를 추가해보세요 +
+
+
+ {column.columnName === "__checkbox__" + ? renderCheckboxCell(row, index) + : formatCellValue(cellValue, column)} +
-
- )} -
- - {/* 푸터/페이지네이션 */} - {/* showFooter와 pagination.enabled의 기본값은 true */} - {tableConfig.showFooter !== false && tableConfig.pagination?.enabled !== false && ( -
- {/* 페이지 정보 - 가운데 정렬 */} - {tableConfig.pagination?.showPageInfo && ( -
-
- - 전체 {totalItems.toLocaleString()}건 중{" "} - - {(currentPage - 1) * localPageSize + 1}-{Math.min(currentPage * localPageSize, totalItems)} - {" "} - 표시 - -
- )} - - {/* 페이지 크기 선택과 페이지네이션 버튼 - 가운데 정렬 */} -
- {/* 페이지 크기 선택 - 임시로 항상 표시 (테스트용) */} - {true && ( - - )} - - {/* 페이지네이션 버튼 */} -
- - - -
- {currentPage} - / - {totalPages} -
- - - -
+ +
- )} -
+ + {/* 페이지네이션 */} + {paginationJSX} +
+ + {/* 필터 설정 다이얼로그 */} + + + + 검색 필터 설정 + + 표시할 검색 필터를 선택하세요. 선택하지 않은 필터는 숨겨집니다. + + + +
+ {(tableConfig.filter?.filters || []).map((filter) => ( +
+ toggleFilterVisibility(filter.columnName)} + /> + +
+ ))} +
+ + + + + +
+
+ ); }; -/** - * TableList 래퍼 컴포넌트 - * 추가적인 로직이나 상태 관리가 필요한 경우 사용 - */ export const TableListWrapper: React.FC = (props) => { return ; }; From 901fae981445bb167887d02c91e1b91726e7f45e Mon Sep 17 00:00:00 2001 From: leeheejin Date: Thu, 23 Oct 2025 17:12:55 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=EA=B3=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=ED=95=9C=20=ED=95=B4=EC=83=81=EB=8F=84=20?= =?UTF-8?q?=ED=81=AC=EA=B8=B0=EC=99=80=20=EC=8B=A4=EC=A0=9C=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=ED=81=AC=EA=B8=B0=EA=B0=80=20=EB=8B=A4=EB=A5=B8=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/layout/AppLayout.tsx | 4 ++-- frontend/components/screen/ScreenDesigner.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index c6313714..3aaf7e6b 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -456,8 +456,8 @@ function AppLayoutInner({ children }: AppLayoutProps) {
- {/* 가운데 컨텐츠 영역 - overflow 문제 해결 */} -
{children}
+ {/* 가운데 컨텐츠 영역 - 스크롤 가능 */} +
{children}
{/* 프로필 수정 모달 */} diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 8f399542..314a0a9f 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -4185,7 +4185,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD className="flex justify-center" style={{ width: "100%", - minHeight: Math.max(screenResolution.height, 800) * zoomLevel, + minHeight: screenResolution.height * zoomLevel, }} > {/* 실제 작업 캔버스 (해상도 크기) - 고정 크기 + 줌 적용 */} @@ -4193,7 +4193,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD className="bg-background border-border border shadow-lg" style={{ width: `${screenResolution.width}px`, - height: `${Math.max(screenResolution.height, 800)}px`, + height: `${screenResolution.height}px`, minWidth: `${screenResolution.width}px`, maxWidth: `${screenResolution.width}px`, minHeight: `${screenResolution.height}px`,