247 lines
6.5 KiB
TypeScript
247 lines
6.5 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* PivotGrid 렌더러
|
||
|
|
* 화면 관리 시스템에서 PivotGrid를 렌더링하는 컴포넌트
|
||
|
|
*/
|
||
|
|
|
||
|
|
import React, { useState, useEffect, useMemo } from "react";
|
||
|
|
import { ComponentRegistry } from "../../ComponentRegistry";
|
||
|
|
import { PivotGridComponent } from "./PivotGridComponent";
|
||
|
|
import { PivotGridConfigPanel } from "./PivotGridConfigPanel";
|
||
|
|
import {
|
||
|
|
PivotGridComponentConfig,
|
||
|
|
PivotFieldConfig,
|
||
|
|
PivotCellData,
|
||
|
|
} from "./types";
|
||
|
|
import { apiClient } from "@/lib/api/client";
|
||
|
|
|
||
|
|
// ==================== 타입 ====================
|
||
|
|
|
||
|
|
interface PivotGridRendererProps {
|
||
|
|
// 위젯 ID
|
||
|
|
id?: string;
|
||
|
|
|
||
|
|
// 컴포넌트 설정
|
||
|
|
config?: PivotGridComponentConfig;
|
||
|
|
|
||
|
|
// 외부 데이터 (formData 등에서 주입)
|
||
|
|
data?: Record<string, any>[];
|
||
|
|
|
||
|
|
// 화면 관리 컨텍스트
|
||
|
|
formData?: Record<string, any>;
|
||
|
|
|
||
|
|
// 이벤트 핸들러
|
||
|
|
onCellClick?: (cellData: PivotCellData) => void;
|
||
|
|
onDataLoad?: (data: Record<string, any>[]) => void;
|
||
|
|
|
||
|
|
// 제어관리 연동
|
||
|
|
buttonControlOptions?: {
|
||
|
|
buttonId?: string;
|
||
|
|
actionType?: string;
|
||
|
|
};
|
||
|
|
|
||
|
|
// 자동 필터 (멀티테넌시)
|
||
|
|
autoFilter?: {
|
||
|
|
companyCode?: string;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==================== 메인 컴포넌트 ====================
|
||
|
|
|
||
|
|
export const PivotGridRenderer: React.FC<PivotGridRendererProps> = ({
|
||
|
|
id,
|
||
|
|
config,
|
||
|
|
data: externalData,
|
||
|
|
formData,
|
||
|
|
onCellClick,
|
||
|
|
onDataLoad,
|
||
|
|
buttonControlOptions,
|
||
|
|
autoFilter,
|
||
|
|
}) => {
|
||
|
|
const [data, setData] = useState<Record<string, any>[]>([]);
|
||
|
|
const [loading, setLoading] = useState(false);
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
|
||
|
|
// 데이터 로드
|
||
|
|
useEffect(() => {
|
||
|
|
const loadData = async () => {
|
||
|
|
// 외부 데이터가 있으면 사용
|
||
|
|
if (externalData && externalData.length > 0) {
|
||
|
|
setData(externalData);
|
||
|
|
onDataLoad?.(externalData);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 데이터 소스 설정 확인
|
||
|
|
if (!config?.dataSource?.tableName) {
|
||
|
|
setData([]);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
setLoading(true);
|
||
|
|
setError(null);
|
||
|
|
|
||
|
|
try {
|
||
|
|
// 테이블 데이터 조회
|
||
|
|
const params: any = {
|
||
|
|
tableName: config.dataSource.tableName,
|
||
|
|
};
|
||
|
|
|
||
|
|
// 멀티테넌시 필터 적용
|
||
|
|
if (autoFilter?.companyCode) {
|
||
|
|
params.companyCode = autoFilter.companyCode;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 필터 조건 적용
|
||
|
|
if (config.dataSource.filterConditions) {
|
||
|
|
const filters: Record<string, any> = {};
|
||
|
|
config.dataSource.filterConditions.forEach((cond) => {
|
||
|
|
if (cond.valueFromField && formData) {
|
||
|
|
filters[cond.field] = formData[cond.valueFromField];
|
||
|
|
} else if (cond.value !== undefined) {
|
||
|
|
filters[cond.field] = cond.value;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
params.filters = JSON.stringify(filters);
|
||
|
|
}
|
||
|
|
|
||
|
|
const response = await apiClient.get(
|
||
|
|
`/api/table-management/data/${config.dataSource.tableName}`,
|
||
|
|
{ params }
|
||
|
|
);
|
||
|
|
|
||
|
|
if (response.data.success) {
|
||
|
|
const loadedData = response.data.data || [];
|
||
|
|
setData(loadedData);
|
||
|
|
onDataLoad?.(loadedData);
|
||
|
|
} else {
|
||
|
|
throw new Error(response.data.message || "데이터 로드 실패");
|
||
|
|
}
|
||
|
|
} catch (err: any) {
|
||
|
|
console.error("PivotGrid 데이터 로드 실패:", err);
|
||
|
|
setError(err.message || "데이터를 불러오는데 실패했습니다");
|
||
|
|
setData([]);
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
loadData();
|
||
|
|
}, [
|
||
|
|
config?.dataSource?.tableName,
|
||
|
|
config?.dataSource?.filterConditions,
|
||
|
|
externalData,
|
||
|
|
formData,
|
||
|
|
autoFilter?.companyCode,
|
||
|
|
onDataLoad,
|
||
|
|
]);
|
||
|
|
|
||
|
|
// 필드 설정에서 formData 값 적용
|
||
|
|
const processedFields = useMemo<PivotFieldConfig[]>(() => {
|
||
|
|
if (!config?.fields) return [];
|
||
|
|
|
||
|
|
return config.fields.map((field) => {
|
||
|
|
// 필터 값에 formData 적용
|
||
|
|
if (field.filterValues && formData) {
|
||
|
|
return {
|
||
|
|
...field,
|
||
|
|
filterValues: field.filterValues.map((v) => {
|
||
|
|
if (typeof v === "string" && v.startsWith("{{") && v.endsWith("}}")) {
|
||
|
|
const key = v.slice(2, -2).trim();
|
||
|
|
return formData[key] ?? v;
|
||
|
|
}
|
||
|
|
return v;
|
||
|
|
}),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
return field;
|
||
|
|
});
|
||
|
|
}, [config?.fields, formData]);
|
||
|
|
|
||
|
|
// 로딩 상태
|
||
|
|
if (loading) {
|
||
|
|
return (
|
||
|
|
<div className="flex items-center justify-center p-8 text-muted-foreground">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||
|
|
<span className="text-sm">데이터 로딩 중...</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 에러 상태
|
||
|
|
if (error) {
|
||
|
|
return (
|
||
|
|
<div className="flex items-center justify-center p-8 text-destructive">
|
||
|
|
<div className="text-center">
|
||
|
|
<p className="text-sm font-medium">데이터 로드 실패</p>
|
||
|
|
<p className="text-xs mt-1">{error}</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<PivotGridComponent
|
||
|
|
id={id}
|
||
|
|
title={config?.dataSource?.tableName}
|
||
|
|
data={data}
|
||
|
|
fields={processedFields}
|
||
|
|
totals={config?.totals}
|
||
|
|
style={config?.style}
|
||
|
|
fieldChooser={config?.fieldChooser}
|
||
|
|
chart={config?.chart}
|
||
|
|
allowSortingBySummary={config?.allowSortingBySummary}
|
||
|
|
allowFiltering={config?.allowFiltering}
|
||
|
|
allowExpandAll={config?.allowExpandAll}
|
||
|
|
wordWrapEnabled={config?.wordWrapEnabled}
|
||
|
|
height={config?.height}
|
||
|
|
maxHeight={config?.maxHeight}
|
||
|
|
exportConfig={config?.exportConfig}
|
||
|
|
onCellClick={onCellClick}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
// ==================== 컴포넌트 등록 ====================
|
||
|
|
|
||
|
|
ComponentRegistry.register({
|
||
|
|
type: "pivot-grid",
|
||
|
|
label: "피벗 그리드",
|
||
|
|
category: "data",
|
||
|
|
icon: "BarChart3",
|
||
|
|
description: "다차원 데이터 분석을 위한 피벗 테이블 컴포넌트",
|
||
|
|
defaultConfig: {
|
||
|
|
dataSource: {
|
||
|
|
type: "table",
|
||
|
|
tableName: "",
|
||
|
|
},
|
||
|
|
fields: [],
|
||
|
|
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",
|
||
|
|
},
|
||
|
|
Renderer: PivotGridRenderer,
|
||
|
|
ConfigPanel: PivotGridConfigPanel,
|
||
|
|
});
|
||
|
|
|
||
|
|
export default PivotGridRenderer;
|