"use client"; import React, { useEffect, useState, Component, ErrorInfo, ReactNode } 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"; import { AlertCircle, RefreshCw } from "lucide-react"; import { Button } from "@/components/ui/button"; // ==================== ์—๋Ÿฌ ๊ฒฝ๊ณ„ ==================== interface ErrorBoundaryState { hasError: boolean; error?: Error; } class PivotGridErrorBoundary extends Component< { children: ReactNode; onReset?: () => void }, ErrorBoundaryState > { constructor(props: { children: ReactNode; onReset?: () => void }) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error("๐Ÿ”ด [PivotGrid] ๋ Œ๋”๋ง ์—๋Ÿฌ:", error); console.error("๐Ÿ”ด [PivotGrid] ์—๋Ÿฌ ์ •๋ณด:", errorInfo); } handleReset = () => { this.setState({ hasError: false, error: undefined }); this.props.onReset?.(); }; render() { if (this.state.hasError) { return (

ํ”ผ๋ฒ— ๊ทธ๋ฆฌ๋“œ ์˜ค๋ฅ˜

{this.state.error?.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."}

); } return this.props.children; } } // ==================== ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ (๋ฏธ๋ฆฌ๋ณด๊ธฐ์šฉ) ==================== 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 = (props) => { // ์ปดํฌ๋„ŒํŠธ ์„ค์ •์—์„œ ๊ฐ’ ์ถ”์ถœ const componentConfig = props.componentConfig || props.config || {}; 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 { const response = await dataApi.getTableData(tableName, { page: 1, size: 10000, // ํ”ผ๋ฒ— ๋ถ„์„์šฉ ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ }); // dataApi.getTableData๋Š” { data, total, page, size, totalPages } ๊ตฌ์กฐ if (response.data && Array.isArray(response.data)) { setLoadedData(response.data); } else { console.error("โŒ [PivotGrid] ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹คํŒจ: ์‘๋‹ต์— data ๋ฐฐ์—ด์ด ์—†์Œ"); setLoadedData([]); } } catch (error) { console.error("โŒ [PivotGrid] ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์—๋Ÿฌ:", error); } finally { setIsLoading(false); } }; loadTableData(); }, [componentConfig.dataSource?.tableName, configData, props.isDesignMode]); // ๋””์ž์ธ ๋ชจ๋“œ ํŒ๋‹จ: // 1. isDesignMode === true // 2. isInteractive === false (ํŽธ์ง‘ ๋ชจ๋“œ) const isDesignMode = props.isDesignMode === true || props.isInteractive === false; // ๐Ÿ†• ์‹ค์ œ ๋ฐ์ดํ„ฐ ์šฐ์„ ์ˆœ์œ„: 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 && !isLoading); // ์ตœ์ข… ๋ฐ์ดํ„ฐ/ํ•„๋“œ ๊ฒฐ์ • const finalData = usePreviewData ? SAMPLE_DATA : actualData; const finalFields = hasValidFields ? configFields : SAMPLE_FIELDS; const finalTitle = usePreviewData ? (componentConfig.title || props.title || "ํ”ผ๋ฒ— ๊ทธ๋ฆฌ๋“œ") + " (๋ฏธ๋ฆฌ๋ณด๊ธฐ)" : (componentConfig.title || props.title); // ์ด๊ณ„ ์„ค์ • const totalsConfig = componentConfig.totals || props.totals || { showRowGrandTotals: true, showColumnGrandTotals: true, showRowTotals: true, showColumnTotals: true, }; // ๐Ÿ†• ๋กœ๋”ฉ ์ค‘ ํ‘œ์‹œ if (isLoading) { return (

๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์ค‘...

); } // ์—๋Ÿฌ ๊ฒฝ๊ณ„๋กœ ๊ฐ์‹ธ์„œ ๋ Œ๋”๋ง ์—๋Ÿฌ ์‹œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์™„์ „ํžˆ ์‚ฌ๋ผ์ง€์ง€ ์•Š๋„๋ก ํ•จ return ( ); }; /** * 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; // ๋””์ž์ธ ๋ชจ๋“œ ํŒ๋‹จ: // 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); // ์ด๊ณ„ ์„ค์ • const totalsConfig = componentConfig.totals || props.totals || { showRowGrandTotals: true, showColumnGrandTotals: true, showRowTotals: true, showColumnTotals: true, }; return ( ); } } // ์ž๋™ ๋“ฑ๋ก ์‹คํ–‰ PivotGridRenderer.registerSelf(); // ๊ฐ•์ œ ๋“ฑ๋ก (๋””๋ฒ„๊น…์šฉ) if (typeof window !== "undefined") { setTimeout(() => { try { PivotGridRenderer.registerSelf(); } catch (error) { console.error("โŒ PivotGrid ๊ฐ•์ œ ๋“ฑ๋ก ์‹คํŒจ:", error); } }, 1000); }