ERP-node/frontend/lib/registry/components/pivot-grid/PivotGridRenderer.tsx

367 lines
14 KiB
TypeScript
Raw Normal View History

"use client";
2026-01-16 10:18:11 +09:00
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";
2026-01-16 10:18:11 +09:00
import { dataApi } from "@/lib/api/data";
// ==================== 샘플 데이터 (미리보기용) ====================
const SAMPLE_DATA = [
{ region: "서울", product: "노트북", quarter: "Q1", sales: 1500000, quantity: 15 },
{ region: "서울", product: "노트북", quarter: "Q2", sales: 1800000, quantity: 18 },
{ region: "서울", product: "노트북", quarter: "Q3", sales: 2100000, quantity: 21 },
{ region: "서울", product: "노트북", quarter: "Q4", sales: 2500000, quantity: 25 },
{ region: "서울", product: "스마트폰", quarter: "Q1", sales: 2000000, quantity: 40 },
{ region: "서울", product: "스마트폰", quarter: "Q2", sales: 2200000, quantity: 44 },
{ region: "서울", product: "스마트폰", quarter: "Q3", sales: 2500000, quantity: 50 },
{ region: "서울", product: "스마트폰", quarter: "Q4", sales: 3000000, quantity: 60 },
{ region: "서울", product: "태블릿", quarter: "Q1", sales: 800000, quantity: 10 },
{ region: "서울", product: "태블릿", quarter: "Q2", sales: 900000, quantity: 11 },
{ region: "서울", product: "태블릿", quarter: "Q3", sales: 1000000, quantity: 12 },
{ region: "서울", product: "태블릿", quarter: "Q4", sales: 1200000, quantity: 15 },
{ region: "부산", product: "노트북", quarter: "Q1", sales: 1000000, quantity: 10 },
{ region: "부산", product: "노트북", quarter: "Q2", sales: 1200000, quantity: 12 },
{ region: "부산", product: "노트북", quarter: "Q3", sales: 1400000, quantity: 14 },
{ region: "부산", product: "노트북", quarter: "Q4", sales: 1600000, quantity: 16 },
{ region: "부산", product: "스마트폰", quarter: "Q1", sales: 1500000, quantity: 30 },
{ region: "부산", product: "스마트폰", quarter: "Q2", sales: 1700000, quantity: 34 },
{ region: "부산", product: "스마트폰", quarter: "Q3", sales: 1900000, quantity: 38 },
{ region: "부산", product: "스마트폰", quarter: "Q4", sales: 2200000, quantity: 44 },
{ region: "부산", product: "태블릿", quarter: "Q1", sales: 500000, quantity: 6 },
{ region: "부산", product: "태블릿", quarter: "Q2", sales: 600000, quantity: 7 },
{ region: "부산", product: "태블릿", quarter: "Q3", sales: 700000, quantity: 8 },
{ region: "부산", product: "태블릿", quarter: "Q4", sales: 800000, quantity: 10 },
{ region: "대구", product: "노트북", quarter: "Q1", sales: 700000, quantity: 7 },
{ region: "대구", product: "노트북", quarter: "Q2", sales: 850000, quantity: 8 },
{ region: "대구", product: "노트북", quarter: "Q3", sales: 900000, quantity: 9 },
{ region: "대구", product: "노트북", quarter: "Q4", sales: 1100000, quantity: 11 },
{ region: "대구", product: "스마트폰", quarter: "Q1", sales: 1000000, quantity: 20 },
{ region: "대구", product: "스마트폰", quarter: "Q2", sales: 1200000, quantity: 24 },
{ region: "대구", product: "스마트폰", quarter: "Q3", sales: 1300000, quantity: 26 },
{ region: "대구", product: "스마트폰", quarter: "Q4", sales: 1500000, quantity: 30 },
{ region: "대구", product: "태블릿", quarter: "Q1", sales: 400000, quantity: 5 },
{ region: "대구", product: "태블릿", quarter: "Q2", sales: 450000, quantity: 5 },
{ region: "대구", product: "태블릿", quarter: "Q3", sales: 500000, quantity: 6 },
{ region: "대구", product: "태블릿", quarter: "Q4", sales: 600000, quantity: 7 },
];
const SAMPLE_FIELDS: PivotFieldConfig[] = [
{
field: "region",
caption: "지역",
area: "row",
areaIndex: 0,
dataType: "string",
visible: true,
},
{
field: "product",
caption: "제품",
area: "row",
areaIndex: 1,
dataType: "string",
visible: true,
},
{
field: "quarter",
caption: "분기",
area: "column",
areaIndex: 0,
dataType: "string",
visible: true,
},
{
field: "sales",
caption: "매출",
area: "data",
areaIndex: 0,
dataType: "number",
summaryType: "sum",
format: { type: "number", precision: 0 },
visible: true,
},
];
/**
* PivotGrid ( )
*/
const PivotGridWrapper: React.FC<any> = (props) => {
// 컴포넌트 설정에서 값 추출
const componentConfig = props.componentConfig || props.config || {};
const configFields = componentConfig.fields || props.fields;
const configData = props.data;
2026-01-16 10:18:11 +09:00
// 🆕 테이블에서 데이터 자동 로딩
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:", {
isDesignMode: props.isDesignMode,
isInteractive: props.isInteractive,
hasComponentConfig: !!props.componentConfig,
hasConfig: !!props.config,
hasData: !!configData,
dataLength: configData?.length,
2026-01-16 10:18:11 +09:00
hasLoadedData: loadedData.length > 0,
loadedDataLength: loadedData.length,
hasFields: !!configFields,
fieldsLength: configFields?.length,
2026-01-16 10:18:11 +09:00
isLoading,
});
// 디자인 모드 판단:
// 1. isDesignMode === true
// 2. isInteractive === false (편집 모드)
const isDesignMode = props.isDesignMode === true || props.isInteractive === false;
2026-01-16 10:18:11 +09:00
// 🆕 실제 데이터 우선순위: 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;
// 디자인 모드이거나 데이터가 없으면 샘플 데이터 사용
2026-01-16 10:18:11 +09:00
const usePreviewData = isDesignMode || (!hasValidData && !isLoading);
// 최종 데이터/필드 결정
2026-01-16 10:18:11 +09:00
const finalData = usePreviewData ? SAMPLE_DATA : actualData;
const finalFields = hasValidFields ? configFields : SAMPLE_FIELDS;
const finalTitle = usePreviewData
? (componentConfig.title || props.title || "피벗 그리드") + " (미리보기)"
: (componentConfig.title || props.title);
console.log("🔷 PivotGridWrapper final:", {
isDesignMode,
usePreviewData,
finalDataLength: finalData?.length,
finalFieldsLength: finalFields?.length,
});
// 총계 설정
const totalsConfig = componentConfig.totals || props.totals || {
showRowGrandTotals: true,
showColumnGrandTotals: true,
showRowTotals: true,
showColumnTotals: true,
};
2026-01-16 10:18:11 +09:00
// 🆕 로딩 중 표시
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 (
<PivotGridComponent
title={finalTitle}
data={finalData}
fields={finalFields}
totals={totalsConfig}
style={componentConfig.style || props.style}
fieldChooser={componentConfig.fieldChooser || props.fieldChooser}
chart={componentConfig.chart || props.chart}
allowExpandAll={componentConfig.allowExpandAll !== false}
2026-01-16 14:03:07 +09:00
height="100%"
maxHeight={componentConfig.maxHeight || props.maxHeight}
exportConfig={componentConfig.exportConfig || props.exportConfig || { excel: true }}
onCellClick={props.onCellClick}
onCellDoubleClick={props.onCellDoubleClick}
onFieldDrop={props.onFieldDrop}
onExpandChange={props.onExpandChange}
/>
);
};
/**
* PivotGrid
*/
const PivotGridDefinition = createComponentDefinition({
id: "pivot-grid",
name: "피벗 그리드",
nameEng: "PivotGrid Component",
description: "다차원 데이터 분석을 위한 피벗 테이블 컴포넌트",
category: ComponentCategory.DISPLAY,
webType: "text",
component: PivotGridWrapper, // 래퍼 컴포넌트 사용
defaultConfig: {
dataSource: {
type: "table",
tableName: "",
},
fields: SAMPLE_FIELDS,
// 미리보기용 샘플 데이터
sampleData: SAMPLE_DATA,
totals: {
showRowGrandTotals: true,
showColumnGrandTotals: true,
showRowTotals: true,
showColumnTotals: true,
},
style: {
theme: "default",
headerStyle: "default",
cellPadding: "normal",
borderStyle: "light",
alternateRowColors: true,
highlightTotals: true,
},
allowExpandAll: true,
exportConfig: {
excel: true,
},
height: "400px",
},
defaultSize: { width: 800, height: 500 },
configPanel: PivotGridConfigPanel,
icon: "BarChart3",
tags: ["피벗", "분석", "집계", "그리드", "데이터"],
version: "1.0.0",
author: "개발팀",
documentation: "",
});
/**
* PivotGrid
*
*/
export class PivotGridRenderer extends AutoRegisteringComponentRenderer {
static componentDefinition = PivotGridDefinition;
render(): React.ReactElement {
const props = this.props as any;
// 컴포넌트 설정에서 값 추출
const componentConfig = props.componentConfig || props.config || {};
const configFields = componentConfig.fields || props.fields;
const configData = props.data;
// 디버깅 로그
console.log("🔷 PivotGridRenderer props:", {
isDesignMode: props.isDesignMode,
isInteractive: props.isInteractive,
hasComponentConfig: !!props.componentConfig,
hasConfig: !!props.config,
hasData: !!configData,
dataLength: configData?.length,
hasFields: !!configFields,
fieldsLength: configFields?.length,
});
// 디자인 모드 판단:
// 1. isDesignMode === true
// 2. isInteractive === false (편집 모드)
// 3. 데이터가 없는 경우
const isDesignMode = props.isDesignMode === true || props.isInteractive === false;
const hasValidData = configData && Array.isArray(configData) && configData.length > 0;
const hasValidFields = configFields && Array.isArray(configFields) && configFields.length > 0;
// 디자인 모드이거나 데이터가 없으면 샘플 데이터 사용
const usePreviewData = isDesignMode || !hasValidData;
// 최종 데이터/필드 결정
const finalData = usePreviewData ? SAMPLE_DATA : configData;
const finalFields = hasValidFields ? configFields : SAMPLE_FIELDS;
const finalTitle = usePreviewData
? (componentConfig.title || props.title || "피벗 그리드") + " (미리보기)"
: (componentConfig.title || props.title);
console.log("🔷 PivotGridRenderer final:", {
isDesignMode,
usePreviewData,
finalDataLength: finalData?.length,
finalFieldsLength: finalFields?.length,
});
// 총계 설정
const totalsConfig = componentConfig.totals || props.totals || {
showRowGrandTotals: true,
showColumnGrandTotals: true,
showRowTotals: true,
showColumnTotals: true,
};
return (
<PivotGridComponent
title={finalTitle}
data={finalData}
fields={finalFields}
totals={totalsConfig}
style={componentConfig.style || props.style}
fieldChooser={componentConfig.fieldChooser || props.fieldChooser}
chart={componentConfig.chart || props.chart}
allowExpandAll={componentConfig.allowExpandAll !== false}
2026-01-16 14:03:07 +09:00
height="100%"
maxHeight={componentConfig.maxHeight || props.maxHeight}
exportConfig={componentConfig.exportConfig || props.exportConfig || { excel: true }}
onCellClick={props.onCellClick}
onCellDoubleClick={props.onCellDoubleClick}
onFieldDrop={props.onFieldDrop}
onExpandChange={props.onExpandChange}
/>
);
}
}
// 자동 등록 실행
PivotGridRenderer.registerSelf();
// 강제 등록 (디버깅용)
if (typeof window !== "undefined") {
setTimeout(() => {
try {
PivotGridRenderer.registerSelf();
} catch (error) {
console.error("❌ PivotGrid 강제 등록 실패:", error);
}
}, 1000);
}