diff --git a/backend-node/src/services/dataService.ts b/backend-node/src/services/dataService.ts index 75c57673..8c6e63f0 100644 --- a/backend-node/src/services/dataService.ts +++ b/backend-node/src/services/dataService.ts @@ -254,7 +254,10 @@ class DataService { key !== "limit" && key !== "offset" && key !== "orderBy" && - key !== "userLang" + key !== "userLang" && + key !== "page" && + key !== "pageSize" && + key !== "size" ) { // 컬럼명 검증 (SQL 인젝션 방지) if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) { diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx index 4f4595ff..bdc00019 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx @@ -184,7 +184,7 @@ const DataCell: React.FC = ({ onClick={onClick} onDoubleClick={onDoubleClick} > - - + 0 ); } @@ -222,7 +222,7 @@ const DataCell: React.FC = ({ )} {icon && {icon}} - {values[0].formattedValue} + {values[0].formattedValue || (values[0].value === 0 ? '0' : values[0].formattedValue)} ); @@ -257,7 +257,7 @@ const DataCell: React.FC = ({ )} {icon && {icon}} - {val.formattedValue} + {val.formattedValue || (val.value === 0 ? '0' : val.formattedValue)} ))} @@ -303,6 +303,17 @@ export const PivotGridComponent: React.FC = ({ externalDataLength: externalData?.length, initialFieldsLength: initialFields?.length, }); + + // 🆕 데이터 샘플 확인 + if (externalData && externalData.length > 0) { + console.log("🔶 첫 번째 데이터 샘플:", externalData[0]); + console.log("🔶 전체 데이터 개수:", externalData.length); + } + + // 🆕 필드 설정 확인 + if (initialFields && initialFields.length > 0) { + console.log("🔶 필드 설정:", initialFields); + } // ==================== 상태 ==================== const [fields, setFields] = useState(initialFields); @@ -312,6 +323,9 @@ export const PivotGridComponent: React.FC = ({ sortConfig: null, filterConfig: {}, }); + + // 🆕 초기 로드 시 자동 확장 (첫 레벨만) + const [isInitialExpanded, setIsInitialExpanded] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); const [showFieldPanel, setShowFieldPanel] = useState(false); // 기본적으로 접힌 상태 const [showFieldChooser, setShowFieldChooser] = useState(false); @@ -494,13 +508,52 @@ export const PivotGridComponent: React.FC = ({ return null; } - return processPivotData( + const result = processPivotData( filteredData, visibleFields, pivotState.expandedRowPaths, pivotState.expandedColumnPaths ); + + // 🆕 피벗 결과 확인 + console.log("🔶 피벗 처리 결과:", { + hasResult: !!result, + flatRowsCount: result?.flatRows?.length, + flatColumnsCount: result?.flatColumns?.length, + dataMatrixSize: result?.dataMatrix?.size, + expandedRowPaths: pivotState.expandedRowPaths.length, + expandedColumnPaths: pivotState.expandedColumnPaths.length, + }); + + return result; }, [filteredData, fields, pivotState.expandedRowPaths, pivotState.expandedColumnPaths]); + + // 🆕 초기 로드 시 첫 레벨 자동 확장 + useEffect(() => { + if (pivotResult && pivotResult.flatRows.length > 0) { + console.log("🔶 피벗 결과 생성됨:", { + flatRowsCount: pivotResult.flatRows.length, + expandedRowPaths: pivotState.expandedRowPaths.length, + isInitialExpanded, + }); + + // 첫 레벨 행들의 경로 수집 (level 0인 행들) + const firstLevelRows = pivotResult.flatRows.filter(row => row.level === 0 && row.hasChildren); + + console.log("🔶 첫 레벨 행 (level 0, hasChildren):", firstLevelRows.map(r => ({ path: r.path, caption: r.caption }))); + + // 초기 확장이 안 되어 있고, 첫 레벨 행이 있으면 자동 확장 + if (!isInitialExpanded && firstLevelRows.length > 0) { + const firstLevelPaths = firstLevelRows.map(row => row.path); + console.log("🔶 초기 자동 확장 실행:", firstLevelPaths); + setPivotState(prev => ({ + ...prev, + expandedRowPaths: firstLevelPaths, + })); + setIsInitialExpanded(true); + } + } + }, [pivotResult, isInitialExpanded, pivotState.expandedRowPaths.length]); // 조건부 서식용 전체 값 수집 const allCellValues = useMemo(() => { @@ -665,6 +718,8 @@ export const PivotGridComponent: React.FC = ({ // 행 확장/축소 const handleToggleRowExpand = useCallback( (path: string[]) => { + console.log("🔶 행 확장/축소 클릭:", path); + setPivotState((prev) => { const pathKey = pathToKey(path); const existingIndex = prev.expandedRowPaths.findIndex( @@ -673,13 +728,16 @@ export const PivotGridComponent: React.FC = ({ let newPaths: string[][]; if (existingIndex >= 0) { + console.log("🔶 행 축소:", path); newPaths = prev.expandedRowPaths.filter( (_, i) => i !== existingIndex ); } else { + console.log("🔶 행 확장:", path); newPaths = [...prev.expandedRowPaths, path]; } + console.log("🔶 새로운 확장 경로:", newPaths); onExpandChange?.(newPaths); return { @@ -1557,13 +1615,13 @@ export const PivotGridComponent: React.FC = ({ {/* 열 헤더 */} - + {/* 좌상단 코너 (행 필드 라벨 + 필터) */} ))} + + {/* 행 총계 헤더 */} + {totals?.showRowGrandTotals && ( + + )} - {/* 열 필드 필터 (헤더 왼쪽에 표시) */} + {/* 열 필드 필터 (헤더 오른쪽 끝에 표시) */} {columnFields.length > 0 && ( )} - - {/* 행 총계 헤더 */} - {totals?.showRowGrandTotals && ( - - )} {/* 데이터 필드 라벨 (다중 데이터 필드인 경우) */} {dataFields.length > 1 && ( - + {flatColumns.map((col, colIdx) => ( {dataFields.map((df, dfIdx) => ( @@ -1697,7 +1756,7 @@ export const PivotGridComponent: React.FC = ({ key={`${colIdx}-${dfIdx}`} className={cn( "border-r border-b border-border", - "px-2 py-1 text-center text-xs font-normal", + "px-2 py-0.5 text-center text-xs font-normal", "text-muted-foreground cursor-pointer hover:bg-accent/50" )} onClick={() => handleSort(df.field)} @@ -1710,19 +1769,6 @@ export const PivotGridComponent: React.FC = ({ ))} ))} - {totals?.showRowGrandTotals && - dataFields.map((df, dfIdx) => ( - - ))} )} diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridRenderer.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridRenderer.tsx index 8e3563d9..191f3610 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridRenderer.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridRenderer.tsx @@ -1,12 +1,13 @@ "use client"; -import React from "react"; +import React, { useEffect, useState } from "react"; import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer"; import { createComponentDefinition } from "../../utils/createComponentDefinition"; import { ComponentCategory } from "@/types/component"; import { PivotGridComponent } from "./PivotGridComponent"; import { PivotGridConfigPanel } from "./PivotGridConfigPanel"; import { PivotFieldConfig } from "./types"; +import { dataApi } from "@/lib/api/data"; // ==================== 샘플 데이터 (미리보기용) ==================== @@ -95,6 +96,48 @@ const PivotGridWrapper: React.FC = (props) => { const configFields = componentConfig.fields || props.fields; const configData = props.data; + // 🆕 테이블에서 데이터 자동 로딩 + const [loadedData, setLoadedData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + const loadTableData = async () => { + const tableName = componentConfig.dataSource?.tableName; + + // 데이터가 이미 있거나, 테이블명이 없으면 로딩하지 않음 + if (configData || !tableName || props.isDesignMode) { + return; + } + + setIsLoading(true); + try { + console.log("🔷 [PivotGrid] 테이블 데이터 로딩 시작:", tableName); + + const response = await dataApi.getTableData(tableName, { + page: 1, + size: 10000, // 피벗 분석용 대량 데이터 (pageSize → size) + }); + + console.log("🔷 [PivotGrid] API 응답:", response); + + // dataApi.getTableData는 { data, total, page, size, totalPages } 구조 + if (response.data && Array.isArray(response.data)) { + setLoadedData(response.data); + console.log("✅ [PivotGrid] 데이터 로딩 완료:", response.data.length, "건"); + } else { + console.error("❌ [PivotGrid] 데이터 로딩 실패: 응답에 data 배열이 없음"); + setLoadedData([]); + } + } catch (error) { + console.error("❌ [PivotGrid] 데이터 로딩 에러:", error); + } finally { + setIsLoading(false); + } + }; + + loadTableData(); + }, [componentConfig.dataSource?.tableName, configData, props.isDesignMode]); + // 디버깅 로그 console.log("🔷 PivotGridWrapper props:", { isDesignMode: props.isDesignMode, @@ -103,23 +146,28 @@ const PivotGridWrapper: React.FC = (props) => { hasConfig: !!props.config, hasData: !!configData, dataLength: configData?.length, + hasLoadedData: loadedData.length > 0, + loadedDataLength: loadedData.length, hasFields: !!configFields, fieldsLength: configFields?.length, + isLoading, }); // 디자인 모드 판단: // 1. isDesignMode === true // 2. isInteractive === false (편집 모드) - // 3. 데이터가 없는 경우 const isDesignMode = props.isDesignMode === true || props.isInteractive === false; - const hasValidData = configData && Array.isArray(configData) && configData.length > 0; + + // 🆕 실제 데이터 우선순위: props.data > loadedData > 샘플 데이터 + const actualData = configData || loadedData; + const hasValidData = actualData && Array.isArray(actualData) && actualData.length > 0; const hasValidFields = configFields && Array.isArray(configFields) && configFields.length > 0; // 디자인 모드이거나 데이터가 없으면 샘플 데이터 사용 - const usePreviewData = isDesignMode || !hasValidData; + const usePreviewData = isDesignMode || (!hasValidData && !isLoading); // 최종 데이터/필드 결정 - const finalData = usePreviewData ? SAMPLE_DATA : configData; + const finalData = usePreviewData ? SAMPLE_DATA : actualData; const finalFields = hasValidFields ? configFields : SAMPLE_FIELDS; const finalTitle = usePreviewData ? (componentConfig.title || props.title || "피벗 그리드") + " (미리보기)" @@ -140,6 +188,18 @@ const PivotGridWrapper: React.FC = (props) => { showColumnTotals: true, }; + // 🆕 로딩 중 표시 + if (isLoading) { + return ( +
+
+
+

데이터 로딩 중...

+
+
+ ); + } + return ( = (props) => { fieldChooser={componentConfig.fieldChooser || props.fieldChooser} chart={componentConfig.chart || props.chart} allowExpandAll={componentConfig.allowExpandAll !== false} - height={componentConfig.height || props.height || "400px"} + height="100%" maxHeight={componentConfig.maxHeight || props.maxHeight} exportConfig={componentConfig.exportConfig || props.exportConfig || { excel: true }} onCellClick={props.onCellClick} @@ -279,7 +339,7 @@ export class PivotGridRenderer extends AutoRegisteringComponentRenderer { fieldChooser={componentConfig.fieldChooser || props.fieldChooser} chart={componentConfig.chart || props.chart} allowExpandAll={componentConfig.allowExpandAll !== false} - height={componentConfig.height || props.height || "400px"} + height="100%" maxHeight={componentConfig.maxHeight || props.maxHeight} exportConfig={componentConfig.exportConfig || props.exportConfig || { excel: true }} onCellClick={props.onCellClick} diff --git a/frontend/lib/registry/components/pivot-grid/components/FieldChooser.tsx b/frontend/lib/registry/components/pivot-grid/components/FieldChooser.tsx index de4a8948..89fe5128 100644 --- a/frontend/lib/registry/components/pivot-grid/components/FieldChooser.tsx +++ b/frontend/lib/registry/components/pivot-grid/components/FieldChooser.tsx @@ -401,7 +401,7 @@ export const FieldChooser: React.FC = ({ {/* 필드 목록 */} - +
{filteredFields.length === 0 ? (
0 ? 2 : 1} > @@ -1607,8 +1665,8 @@ export const PivotGridComponent: React.FC = ({ key={idx} className={cn( "border-r border-b border-border relative group", - "px-2 py-1.5 text-center text-xs font-medium", - "bg-muted/70 sticky top-0 z-10", + "px-2 py-1 text-center text-xs font-medium", + "bg-background sticky top-0 z-10", dataFields.length === 1 && "cursor-pointer hover:bg-accent/50" )} colSpan={dataFields.length || 1} @@ -1630,16 +1688,31 @@ export const PivotGridComponent: React.FC = ({ /> 1 ? 2 : 1} + > + 총계 + 0 ? 2 : 1} + rowSpan={dataFields.length > 1 ? 2 : 1} >
{columnFields.map((f) => ( @@ -1671,25 +1744,11 @@ export const PivotGridComponent: React.FC = ({
- 총계 -
- {df.caption} -