피벗그리드 고침

This commit is contained in:
leeheejin 2026-01-16 10:18:11 +09:00
parent 98c489ee22
commit 484c98da9e
4 changed files with 121 additions and 8 deletions

View File

@ -254,7 +254,10 @@ class DataService {
key !== "limit" && key !== "limit" &&
key !== "offset" && key !== "offset" &&
key !== "orderBy" && key !== "orderBy" &&
key !== "userLang" key !== "userLang" &&
key !== "page" &&
key !== "pageSize" &&
key !== "size"
) { ) {
// 컬럼명 검증 (SQL 인젝션 방지) // 컬럼명 검증 (SQL 인젝션 방지)
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) { if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {

View File

@ -303,6 +303,17 @@ export const PivotGridComponent: React.FC<PivotGridProps> = ({
externalDataLength: externalData?.length, externalDataLength: externalData?.length,
initialFieldsLength: initialFields?.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<PivotFieldConfig[]>(initialFields); const [fields, setFields] = useState<PivotFieldConfig[]>(initialFields);
@ -312,6 +323,9 @@ export const PivotGridComponent: React.FC<PivotGridProps> = ({
sortConfig: null, sortConfig: null,
filterConfig: {}, filterConfig: {},
}); });
// 🆕 초기 로드 시 자동 확장 (첫 레벨만)
const [isInitialExpanded, setIsInitialExpanded] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
const [showFieldPanel, setShowFieldPanel] = useState(false); // 기본적으로 접힌 상태 const [showFieldPanel, setShowFieldPanel] = useState(false); // 기본적으로 접힌 상태
const [showFieldChooser, setShowFieldChooser] = useState(false); const [showFieldChooser, setShowFieldChooser] = useState(false);
@ -494,13 +508,44 @@ export const PivotGridComponent: React.FC<PivotGridProps> = ({
return null; return null;
} }
return processPivotData( const result = processPivotData(
filteredData, filteredData,
visibleFields, visibleFields,
pivotState.expandedRowPaths, pivotState.expandedRowPaths,
pivotState.expandedColumnPaths 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]); }, [filteredData, fields, pivotState.expandedRowPaths, pivotState.expandedColumnPaths]);
// 🆕 초기 로드 시 첫 레벨 자동 확장
useEffect(() => {
if (!isInitialExpanded && pivotResult && pivotResult.flatRows.length > 0) {
// 첫 레벨 행들의 경로 수집 (level 0인 행들)
const firstLevelPaths = pivotResult.flatRows
.filter(row => row.level === 0 && row.hasChildren)
.map(row => row.path);
if (firstLevelPaths.length > 0) {
console.log("🔶 초기 자동 확장:", firstLevelPaths);
setPivotState(prev => ({
...prev,
expandedRowPaths: firstLevelPaths,
}));
setIsInitialExpanded(true);
}
}
}, [pivotResult, isInitialExpanded]);
// 조건부 서식용 전체 값 수집 // 조건부 서식용 전체 값 수집
const allCellValues = useMemo(() => { const allCellValues = useMemo(() => {
@ -665,6 +710,8 @@ export const PivotGridComponent: React.FC<PivotGridProps> = ({
// 행 확장/축소 // 행 확장/축소
const handleToggleRowExpand = useCallback( const handleToggleRowExpand = useCallback(
(path: string[]) => { (path: string[]) => {
console.log("🔶 행 확장/축소 클릭:", path);
setPivotState((prev) => { setPivotState((prev) => {
const pathKey = pathToKey(path); const pathKey = pathToKey(path);
const existingIndex = prev.expandedRowPaths.findIndex( const existingIndex = prev.expandedRowPaths.findIndex(
@ -673,13 +720,16 @@ export const PivotGridComponent: React.FC<PivotGridProps> = ({
let newPaths: string[][]; let newPaths: string[][];
if (existingIndex >= 0) { if (existingIndex >= 0) {
console.log("🔶 행 축소:", path);
newPaths = prev.expandedRowPaths.filter( newPaths = prev.expandedRowPaths.filter(
(_, i) => i !== existingIndex (_, i) => i !== existingIndex
); );
} else { } else {
console.log("🔶 행 확장:", path);
newPaths = [...prev.expandedRowPaths, path]; newPaths = [...prev.expandedRowPaths, path];
} }
console.log("🔶 새로운 확장 경로:", newPaths);
onExpandChange?.(newPaths); onExpandChange?.(newPaths);
return { return {

View File

@ -1,12 +1,13 @@
"use client"; "use client";
import React from "react"; import React, { useEffect, useState } from "react";
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer"; import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
import { createComponentDefinition } from "../../utils/createComponentDefinition"; import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { ComponentCategory } from "@/types/component"; import { ComponentCategory } from "@/types/component";
import { PivotGridComponent } from "./PivotGridComponent"; import { PivotGridComponent } from "./PivotGridComponent";
import { PivotGridConfigPanel } from "./PivotGridConfigPanel"; import { PivotGridConfigPanel } from "./PivotGridConfigPanel";
import { PivotFieldConfig } from "./types"; import { PivotFieldConfig } from "./types";
import { dataApi } from "@/lib/api/data";
// ==================== 샘플 데이터 (미리보기용) ==================== // ==================== 샘플 데이터 (미리보기용) ====================
@ -95,6 +96,48 @@ const PivotGridWrapper: React.FC<any> = (props) => {
const configFields = componentConfig.fields || props.fields; const configFields = componentConfig.fields || props.fields;
const configData = props.data; const configData = props.data;
// 🆕 테이블에서 데이터 자동 로딩
const [loadedData, setLoadedData] = useState<any[]>([]);
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:", { console.log("🔷 PivotGridWrapper props:", {
isDesignMode: props.isDesignMode, isDesignMode: props.isDesignMode,
@ -103,23 +146,28 @@ const PivotGridWrapper: React.FC<any> = (props) => {
hasConfig: !!props.config, hasConfig: !!props.config,
hasData: !!configData, hasData: !!configData,
dataLength: configData?.length, dataLength: configData?.length,
hasLoadedData: loadedData.length > 0,
loadedDataLength: loadedData.length,
hasFields: !!configFields, hasFields: !!configFields,
fieldsLength: configFields?.length, fieldsLength: configFields?.length,
isLoading,
}); });
// 디자인 모드 판단: // 디자인 모드 판단:
// 1. isDesignMode === true // 1. isDesignMode === true
// 2. isInteractive === false (편집 모드) // 2. isInteractive === false (편집 모드)
// 3. 데이터가 없는 경우
const isDesignMode = props.isDesignMode === true || props.isInteractive === false; 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 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 finalFields = hasValidFields ? configFields : SAMPLE_FIELDS;
const finalTitle = usePreviewData const finalTitle = usePreviewData
? (componentConfig.title || props.title || "피벗 그리드") + " (미리보기)" ? (componentConfig.title || props.title || "피벗 그리드") + " (미리보기)"
@ -140,6 +188,18 @@ const PivotGridWrapper: React.FC<any> = (props) => {
showColumnTotals: true, showColumnTotals: true,
}; };
// 🆕 로딩 중 표시
if (isLoading) {
return (
<div className="flex items-center justify-center h-64 bg-muted/30 rounded-lg">
<div className="text-center space-y-2">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
<p className="text-sm text-muted-foreground"> ...</p>
</div>
</div>
);
}
return ( return (
<PivotGridComponent <PivotGridComponent
title={finalTitle} title={finalTitle}

View File

@ -401,7 +401,7 @@ export const FieldChooser: React.FC<FieldChooserProps> = ({
</div> </div>
{/* 필드 목록 */} {/* 필드 목록 */}
<ScrollArea className="flex-1 -mx-6 px-6"> <ScrollArea className="flex-1 -mx-6 px-6 max-h-[40vh] overflow-y-auto">
<div className="space-y-2 py-2"> <div className="space-y-2 py-2">
{filteredFields.length === 0 ? ( {filteredFields.length === 0 ? (
<div className="text-center py-8 text-muted-foreground"> <div className="text-center py-8 text-muted-foreground">