diff --git a/docs/컴포넌트_기능_현황.md b/docs/컴포넌트_기능_현황.md index a49c6b76..addc85a5 100644 --- a/docs/컴포넌트_기능_현황.md +++ b/docs/컴포넌트_기능_현황.md @@ -1,7 +1,7 @@ # 컴포넌트 기능 현황 -> 작성일: 2026-01-15 -> 현재 사용 가능한 16개 컴포넌트의 다국어 지원 및 테이블 설정 기능 현황 +> 작성일: 2026-01-16 +> 현재 사용 가능한 17개 컴포넌트의 다국어 지원 및 테이블 설정 기능 현황 --- @@ -9,8 +9,8 @@ | 기능 | 적용 완료 | 미적용 | 해당없음 | | -------------------------- | --------- | ------ | -------- | -| **다국어 지원** | 3개 | 9개 | 4개 | -| **컴포넌트별 테이블 설정** | 6개 | 4개 | 6개 | +| **다국어 지원** | 4개 | 9개 | 4개 | +| **컴포넌트별 테이블 설정** | 7개 | 4개 | 6개 | --- @@ -56,14 +56,15 @@ --- -### 유틸리티 (Utility) - 4개 +### 유틸리티 (Utility) - 5개 -| 컴포넌트 | 다국어 지원 | 테이블 설정 | 비고 | -| ---------------------- | :---------: | :---------: | ------------------------------------------- | -| **코드 채번 규칙** | ❌ 미적용 | ➖ 해당없음 | 채번 규칙 관리 전용 | -| **렉 구조 설정** | ❌ 미적용 | ➖ 해당없음 | 창고 렉 설정 전용 | -| **출발지/도착지 선택** | ❌ 미적용 | ⚠️ 부분 | `customTableName` 지원하나 Combobox UI 없음 | -| **검색 필터** | ❌ 미적용 | ⚠️ 부분 | `screenTableName` 자동 감지 | +| 컴포넌트 | 다국어 지원 | 테이블 설정 | 비고 | +| ---------------------- | :---------: | :---------: | --------------------------------------------------------- | +| **집계 위젯** | ✅ 적용 | ✅ 적용 | `customTableName` 지원, 항목별 `labelLangKey` 다국어 지원 | +| **코드 채번 규칙** | ❌ 미적용 | ➖ 해당없음 | 채번 규칙 관리 전용 | +| **렉 구조 설정** | ❌ 미적용 | ➖ 해당없음 | 창고 렉 설정 전용 | +| **출발지/도착지 선택** | ❌ 미적용 | ⚠️ 부분 | `customTableName` 지원하나 Combobox UI 없음 | +| **검색 필터** | ❌ 미적용 | ⚠️ 부분 | `screenTableName` 자동 감지 | --- @@ -73,11 +74,12 @@ 다국어 지원이란 컴포넌트의 라벨, 플레이스홀더 등 텍스트 속성에 다국어 키를 연결하여 언어별로 다른 텍스트를 표시하는 기능입니다. -**적용 완료 (3개)** +**적용 완료 (4개)** - `table-list`: 컬럼 라벨 다국어 지원 - `button-primary`: 버튼 텍스트 다국어 지원 - `split-panel-layout`: 패널 제목 다국어 지원 +- `aggregation-widget`: 집계 항목별 표시 라벨 다국어 지원 **해당없음 (4개)** @@ -99,7 +101,7 @@ 컴포넌트별 테이블 설정이란 화면의 메인 테이블과 별개로 컴포넌트가 자체적으로 사용할 테이블을 지정할 수 있는 기능입니다. -**완전 적용 (5개)** +**완전 적용 (6개)** | 컴포넌트 | 적용 방식 | | -------------------- | --------------------------------------------------------------------------------- | @@ -108,6 +110,7 @@ | `unified-list` | `TableListConfigPanel` 래핑하여 동일 기능 제공 | | `card-display` | Combobox UI로 테이블 선택, `customTableName`, `useCustomTable` 지원 | | `split-panel-layout` | 좌우 패널 각각 Combobox UI로 테이블 선택, 다국어 지원 | +| `aggregation-widget` | Combobox UI로 테이블 선택, `customTableName`, `useCustomTable` 지원 | **부분 적용 (4개)** diff --git a/frontend/components/screen/modals/MultilangSettingsModal.tsx b/frontend/components/screen/modals/MultilangSettingsModal.tsx index f9f365dc..8613d938 100644 --- a/frontend/components/screen/modals/MultilangSettingsModal.tsx +++ b/frontend/components/screen/modals/MultilangSettingsModal.tsx @@ -491,7 +491,7 @@ export const MultilangSettingsModal: React.FC = ({ const extractFromComponent = (comp: ComponentData, parentType?: string, parentLabel?: string) => { const anyComp = comp as any; - const config = anyComp.componentConfig; + const config = anyComp.componentConfig || anyComp.config; const compType = anyComp.componentType || anyComp.type; const compLabel = anyComp.label || anyComp.title || compType; @@ -728,6 +728,23 @@ export const MultilangSettingsModal: React.FC = ({ }); } + // 11. 집계 위젯 (aggregation-widget) + if (compType === "aggregation-widget" && config?.items && Array.isArray(config.items)) { + config.items.forEach((item: any, index: number) => { + if (item.columnLabel && typeof item.columnLabel === "string") { + addLabel( + `${comp.id}_agg_${item.id || index}`, + item.columnLabel, + "label", + compType, + compLabel, + item.labelLangKeyId, + item.labelLangKey + ); + } + }); + } + // 자식 컴포넌트 재귀 탐색 if (anyComp.children && Array.isArray(anyComp.children)) { anyComp.children.forEach((child: ComponentData) => { diff --git a/frontend/contexts/ScreenMultiLangContext.tsx b/frontend/contexts/ScreenMultiLangContext.tsx index 296a0ab6..1ec553c8 100644 --- a/frontend/contexts/ScreenMultiLangContext.tsx +++ b/frontend/contexts/ScreenMultiLangContext.tsx @@ -99,6 +99,14 @@ export const ScreenMultiLangProvider: React.FC = ( } }); } + // 집계 위젯 (aggregation-widget) items의 labelLangKey 수집 + if ((comp as any).componentType === "aggregation-widget" && config?.items) { + config.items.forEach((item: any) => { + if (item.labelLangKey) { + keys.push(item.labelLangKey); + } + }); + } // 자식 컴포넌트 재귀 처리 if ((comp as any).children) { collectLangKeys((comp as any).children); diff --git a/frontend/lib/registry/components/aggregation-widget/AggregationWidgetComponent.tsx b/frontend/lib/registry/components/aggregation-widget/AggregationWidgetComponent.tsx new file mode 100644 index 00000000..ce0b0325 --- /dev/null +++ b/frontend/lib/registry/components/aggregation-widget/AggregationWidgetComponent.tsx @@ -0,0 +1,312 @@ +"use client"; + +import React, { useState, useEffect, useMemo } from "react"; +import { ComponentRendererProps } from "@/types/component"; +import { AggregationWidgetConfig, AggregationItem, AggregationResult, AggregationType } from "./types"; +import { Calculator, TrendingUp, Hash, ArrowUp, ArrowDown } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { useScreenMultiLang } from "@/contexts/ScreenMultiLangContext"; + +interface AggregationWidgetComponentProps extends ComponentRendererProps { + config?: AggregationWidgetConfig; + // 외부에서 데이터를 직접 전달받을 수 있음 + externalData?: any[]; +} + +/** + * 집계 위젯 컴포넌트 + * 연결된 테이블 리스트나 리피터의 데이터를 집계하여 표시 + */ +export function AggregationWidgetComponent({ + component, + isDesignMode = false, + config: propsConfig, + externalData, +}: AggregationWidgetComponentProps) { + // 다국어 지원 + const { getText } = useScreenMultiLang(); + + const componentConfig: AggregationWidgetConfig = { + dataSourceType: "manual", + items: [], + layout: "horizontal", + showLabels: true, + showIcons: true, + gap: "16px", + ...propsConfig, + ...component?.config, + }; + + // 다국어 라벨 가져오기 + const getItemLabel = (item: AggregationItem): string => { + if (item.labelLangKey) { + const translated = getText(item.labelLangKey); + if (translated && translated !== item.labelLangKey) { + return translated; + } + } + return item.columnLabel || item.columnName || "컬럼"; + }; + + const { + dataSourceType, + dataSourceComponentId, + items, + layout, + showLabels, + showIcons, + gap, + backgroundColor, + borderRadius, + padding, + fontSize, + labelFontSize, + valueFontSize, + labelColor, + valueColor, + } = componentConfig; + + // 데이터 상태 + const [data, setData] = useState([]); + + // 외부 데이터가 있으면 사용 + useEffect(() => { + if (externalData && Array.isArray(externalData)) { + setData(externalData); + } + }, [externalData]); + + // 컴포넌트 데이터 변경 이벤트 리스닝 + useEffect(() => { + if (!dataSourceComponentId || isDesignMode) return; + + const handleDataChange = (event: CustomEvent) => { + const { componentId, data: eventData } = event.detail || {}; + if (componentId === dataSourceComponentId && Array.isArray(eventData)) { + setData(eventData); + } + }; + + // 리피터 데이터 변경 이벤트 + window.addEventListener("repeaterDataChange" as any, handleDataChange); + // 테이블 리스트 데이터 변경 이벤트 + window.addEventListener("tableListDataChange" as any, handleDataChange); + + return () => { + window.removeEventListener("repeaterDataChange" as any, handleDataChange); + window.removeEventListener("tableListDataChange" as any, handleDataChange); + }; + }, [dataSourceComponentId, isDesignMode]); + + // 집계 계산 + const aggregationResults = useMemo((): AggregationResult[] => { + if (!items || items.length === 0) { + return []; + } + + return items.map((item) => { + const values = data + .map((row) => { + const val = row[item.columnName]; + return typeof val === "number" ? val : parseFloat(val) || 0; + }) + .filter((v) => !isNaN(v)); + + let value: number = 0; + + switch (item.type) { + case "sum": + value = values.reduce((acc, v) => acc + v, 0); + break; + case "avg": + value = values.length > 0 ? values.reduce((acc, v) => acc + v, 0) / values.length : 0; + break; + case "count": + value = data.length; + break; + case "max": + value = values.length > 0 ? Math.max(...values) : 0; + break; + case "min": + value = values.length > 0 ? Math.min(...values) : 0; + break; + } + + // 포맷팅 + let formattedValue = value.toFixed(item.decimalPlaces ?? 0); + + if (item.format === "currency") { + formattedValue = new Intl.NumberFormat("ko-KR").format(value); + } else if (item.format === "percent") { + formattedValue = `${(value * 100).toFixed(item.decimalPlaces ?? 1)}%`; + } else if (item.format === "number") { + formattedValue = new Intl.NumberFormat("ko-KR").format(value); + } + + if (item.prefix) { + formattedValue = `${item.prefix}${formattedValue}`; + } + if (item.suffix) { + formattedValue = `${formattedValue}${item.suffix}`; + } + + return { + id: item.id, + label: getItemLabel(item), + value, + formattedValue, + type: item.type, + }; + }); + }, [data, items, getText]); + + // 집계 타입에 따른 아이콘 + const getIcon = (type: AggregationType) => { + switch (type) { + case "sum": + return ; + case "avg": + return ; + case "count": + return ; + case "max": + return ; + case "min": + return ; + } + }; + + // 집계 타입 라벨 + const getTypeLabel = (type: AggregationType) => { + switch (type) { + case "sum": + return "합계"; + case "avg": + return "평균"; + case "count": + return "개수"; + case "max": + return "최대"; + case "min": + return "최소"; + } + }; + + // 디자인 모드 미리보기 + if (isDesignMode) { + const previewItems: AggregationResult[] = + items.length > 0 + ? items.map((item) => ({ + id: item.id, + label: getItemLabel(item), + value: 0, + formattedValue: item.prefix ? `${item.prefix}0${item.suffix || ""}` : `0${item.suffix || ""}`, + type: item.type, + })) + : [ + { id: "1", label: "총 수량", value: 150, formattedValue: "150", type: "sum" }, + { id: "2", label: "총 금액", value: 1500000, formattedValue: "₩1,500,000", type: "sum" }, + { id: "3", label: "건수", value: 5, formattedValue: "5건", type: "count" }, + ]; + + return ( +
+ {previewItems.map((result, index) => ( +
+ {showIcons && ( + {getIcon(result.type)} + )} + {showLabels && ( + + {result.label} ({getTypeLabel(result.type)}): + + )} + + {result.formattedValue} + +
+ ))} +
+ ); + } + + // 실제 렌더링 + if (aggregationResults.length === 0) { + return ( +
+ 집계 항목을 설정해주세요 +
+ ); + } + + return ( +
+ {aggregationResults.map((result, index) => ( +
+ {showIcons && ( + {getIcon(result.type)} + )} + {showLabels && ( + + {result.label} ({getTypeLabel(result.type)}): + + )} + + {result.formattedValue} + +
+ ))} +
+ ); +} + +export const AggregationWidgetWrapper = AggregationWidgetComponent; + diff --git a/frontend/lib/registry/components/aggregation-widget/AggregationWidgetConfigPanel.tsx b/frontend/lib/registry/components/aggregation-widget/AggregationWidgetConfigPanel.tsx new file mode 100644 index 00000000..fd38d3b9 --- /dev/null +++ b/frontend/lib/registry/components/aggregation-widget/AggregationWidgetConfigPanel.tsx @@ -0,0 +1,533 @@ +"use client"; + +import React, { useState, useEffect, useMemo } from "react"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; +import { Plus, Trash2, GripVertical, Database, Table2, ChevronsUpDown, Check } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { AggregationWidgetConfig, AggregationItem, AggregationType } from "./types"; +import { tableManagementApi } from "@/lib/api/tableManagement"; +import { tableTypeApi } from "@/lib/api/screen"; + +interface AggregationWidgetConfigPanelProps { + config: AggregationWidgetConfig; + onChange: (config: Partial) => void; + screenTableName?: string; +} + +/** + * 집계 위젯 설정 패널 + */ +export function AggregationWidgetConfigPanel({ + config, + onChange, + screenTableName, +}: AggregationWidgetConfigPanelProps) { + const [columns, setColumns] = useState>([]); + const [loadingColumns, setLoadingColumns] = useState(false); + const [availableTables, setAvailableTables] = useState>([]); + const [loadingTables, setLoadingTables] = useState(false); + const [tableComboboxOpen, setTableComboboxOpen] = useState(false); + + // 실제 사용할 테이블 이름 계산 + const targetTableName = useMemo(() => { + if (config.useCustomTable && config.customTableName) { + return config.customTableName; + } + return config.tableName || screenTableName; + }, [config.useCustomTable, config.customTableName, config.tableName, screenTableName]); + + // 화면 테이블명 자동 설정 (초기 한 번만) + useEffect(() => { + if (screenTableName && !config.tableName && !config.customTableName) { + onChange({ tableName: screenTableName }); + } + }, [screenTableName, config.tableName, config.customTableName, onChange]); + + // 전체 테이블 목록 로드 + useEffect(() => { + const fetchTables = async () => { + setLoadingTables(true); + try { + const response = await tableTypeApi.getTables(); + setAvailableTables( + response.map((table: any) => ({ + tableName: table.tableName, + displayName: table.displayName || table.tableName, + })) + ); + } catch (error) { + console.error("테이블 목록 가져오기 실패:", error); + } finally { + setLoadingTables(false); + } + }; + fetchTables(); + }, []); + + // 테이블 컬럼 로드 + useEffect(() => { + const loadColumns = async () => { + if (!targetTableName) { + setColumns([]); + return; + } + + setLoadingColumns(true); + try { + const result = await tableManagementApi.getColumnList(targetTableName); + if (result.success && result.data?.columns) { + setColumns( + result.data.columns.map((col: any) => ({ + columnName: col.columnName || col.column_name, + label: col.displayName || col.columnLabel || col.column_label || col.label || col.columnName || col.column_name, + dataType: col.dataType || col.data_type, + })) + ); + } else { + setColumns([]); + } + } catch (error) { + console.error("컬럼 로드 실패:", error); + setColumns([]); + } finally { + setLoadingColumns(false); + } + }; + + loadColumns(); + }, [targetTableName]); + + // 집계 항목 추가 + const addItem = () => { + const newItem: AggregationItem = { + id: `agg-${Date.now()}`, + columnName: "", + columnLabel: "", + type: "sum", + format: "number", + decimalPlaces: 0, + }; + onChange({ + items: [...(config.items || []), newItem], + }); + }; + + // 집계 항목 삭제 + const removeItem = (id: string) => { + onChange({ + items: (config.items || []).filter((item) => item.id !== id), + }); + }; + + // 집계 항목 업데이트 + const updateItem = (id: string, updates: Partial) => { + onChange({ + items: (config.items || []).map((item) => + item.id === id ? { ...item, ...updates } : item + ), + }); + }; + + // 숫자형 컬럼만 필터링 (count 제외) + const numericColumns = columns.filter( + (col) => + col.dataType?.toLowerCase().includes("int") || + col.dataType?.toLowerCase().includes("numeric") || + col.dataType?.toLowerCase().includes("decimal") || + col.dataType?.toLowerCase().includes("float") || + col.dataType?.toLowerCase().includes("double") + ); + + return ( +
+
집계 위젯 설정
+ + {/* 테이블 설정 (컴포넌트 개발 가이드 준수) */} +
+
+

데이터 소스 테이블

+

집계할 데이터의 테이블을 선택합니다

+
+
+ + {/* 현재 선택된 테이블 표시 (카드 형태) */} +
+ +
+
+ {config.customTableName || config.tableName || screenTableName || "테이블 미선택"} +
+
+ {config.useCustomTable ? "커스텀 테이블" : "화면 기본 테이블"} +
+
+
+ + {/* 테이블 선택 Combobox */} + + + + + + + + + 테이블을 찾을 수 없습니다 + + {/* 그룹 1: 화면 기본 테이블 */} + {screenTableName && ( + + { + onChange({ + useCustomTable: false, + customTableName: undefined, + tableName: screenTableName, + items: [], // 테이블 변경 시 집계 항목 초기화 + }); + setTableComboboxOpen(false); + }} + className="text-xs cursor-pointer" + > + + + {screenTableName} + + + )} + + {/* 그룹 2: 전체 테이블 */} + + {availableTables + .filter((table) => table.tableName !== screenTableName) + .map((table) => ( + { + onChange({ + useCustomTable: true, + customTableName: table.tableName, + tableName: table.tableName, + items: [], // 테이블 변경 시 집계 항목 초기화 + }); + setTableComboboxOpen(false); + }} + className="text-xs cursor-pointer" + > + + + {table.displayName || table.tableName} + + ))} + + + + + +
+ + {/* 레이아웃 설정 */} +
+
+

레이아웃

+
+
+ +
+
+ + +
+ +
+ + onChange({ gap: e.target.value })} + placeholder="16px" + className="h-8 text-xs" + /> +
+
+ +
+
+ onChange({ showLabels: checked as boolean })} + /> + +
+ +
+ onChange({ showIcons: checked as boolean })} + /> + +
+
+
+ + {/* 집계 항목 설정 */} +
+
+

집계 항목

+ +
+
+ + {(config.items || []).length === 0 ? ( +
+ 집계 항목을 추가해주세요 +
+ ) : ( +
+ {(config.items || []).map((item, index) => ( +
+
+
+ + 항목 {index + 1} +
+ +
+ +
+ {/* 컬럼 선택 */} +
+ + +
+ + {/* 집계 타입 */} +
+ + +
+ + {/* 표시 라벨 */} +
+ + updateItem(item.id, { columnLabel: e.target.value })} + placeholder="표시될 라벨" + className="h-7 text-xs" + /> +
+ + {/* 표시 형식 */} +
+ + +
+ + {/* 접두사 */} +
+ + updateItem(item.id, { prefix: e.target.value })} + placeholder="예: ₩" + className="h-7 text-xs" + /> +
+ + {/* 접미사 */} +
+ + updateItem(item.id, { suffix: e.target.value })} + placeholder="예: 원, 개" + className="h-7 text-xs" + /> +
+
+
+ ))} +
+ )} +
+ + {/* 스타일 설정 */} +
+
+

스타일

+
+
+ +
+
+ + onChange({ backgroundColor: e.target.value })} + className="h-8" + /> +
+ +
+ + onChange({ borderRadius: e.target.value })} + placeholder="6px" + className="h-8 text-xs" + /> +
+ +
+ + onChange({ labelColor: e.target.value })} + className="h-8" + /> +
+ +
+ + onChange({ valueColor: e.target.value })} + className="h-8" + /> +
+
+
+
+ ); +} + diff --git a/frontend/lib/registry/components/aggregation-widget/AggregationWidgetRenderer.tsx b/frontend/lib/registry/components/aggregation-widget/AggregationWidgetRenderer.tsx new file mode 100644 index 00000000..c934703c --- /dev/null +++ b/frontend/lib/registry/components/aggregation-widget/AggregationWidgetRenderer.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { ComponentRegistry } from "@/lib/registry/ComponentRegistry"; +import { AggregationWidgetDefinition } from "./index"; + +// 컴포넌트 자동 등록 +if (typeof window !== "undefined") { + ComponentRegistry.registerComponent(AggregationWidgetDefinition); +} + +export {}; + diff --git a/frontend/lib/registry/components/aggregation-widget/index.ts b/frontend/lib/registry/components/aggregation-widget/index.ts new file mode 100644 index 00000000..97ad5f27 --- /dev/null +++ b/frontend/lib/registry/components/aggregation-widget/index.ts @@ -0,0 +1,42 @@ +"use client"; + +import { createComponentDefinition } from "../../utils/createComponentDefinition"; +import { ComponentCategory } from "@/types/component"; +import { AggregationWidgetWrapper } from "./AggregationWidgetComponent"; +import { AggregationWidgetConfigPanel } from "./AggregationWidgetConfigPanel"; +import type { AggregationWidgetConfig } from "./types"; + +/** + * AggregationWidget 컴포넌트 정의 + * 데이터 집계 (합계, 평균, 개수 등)를 표시하는 위젯 + */ +export const AggregationWidgetDefinition = createComponentDefinition({ + id: "aggregation-widget", + name: "집계 위젯", + nameEng: "Aggregation Widget", + description: "데이터의 합계, 평균, 개수 등 집계 결과를 표시하는 위젯", + category: ComponentCategory.DISPLAY, + webType: "text", + component: AggregationWidgetWrapper, + defaultConfig: { + dataSourceType: "manual", + items: [], + layout: "horizontal", + showLabels: true, + showIcons: true, + gap: "16px", + backgroundColor: "#f8fafc", + borderRadius: "6px", + padding: "12px", + } as Partial, + defaultSize: { width: 400, height: 60 }, + configPanel: AggregationWidgetConfigPanel, + icon: "Calculator", + tags: ["집계", "합계", "평균", "개수", "통계", "데이터"], + version: "1.0.0", + author: "개발팀", +}); + +// 타입 내보내기 +export type { AggregationWidgetConfig, AggregationItem, AggregationType, AggregationResult } from "./types"; + diff --git a/frontend/lib/registry/components/aggregation-widget/types.ts b/frontend/lib/registry/components/aggregation-widget/types.ts new file mode 100644 index 00000000..6d480599 --- /dev/null +++ b/frontend/lib/registry/components/aggregation-widget/types.ts @@ -0,0 +1,67 @@ +import { ComponentConfig } from "@/types/component"; + +/** + * 집계 타입 + */ +export type AggregationType = "sum" | "avg" | "count" | "max" | "min"; + +/** + * 개별 집계 항목 설정 + */ +export interface AggregationItem { + id: string; + columnName: string; // 집계할 컬럼 + columnLabel?: string; // 표시 라벨 + labelLangKeyId?: number; // 다국어 키 ID + labelLangKey?: string; // 다국어 키 + type: AggregationType; // 집계 타입 + format?: "number" | "currency" | "percent"; // 표시 형식 + decimalPlaces?: number; // 소수점 자릿수 + prefix?: string; // 접두사 (예: "₩") + suffix?: string; // 접미사 (예: "원", "개") +} + +/** + * 집계 위젯 설정 + */ +export interface AggregationWidgetConfig extends ComponentConfig { + // 데이터 소스 설정 + dataSourceType: "repeater" | "tableList" | "manual"; // 데이터 소스 타입 + dataSourceComponentId?: string; // 연결할 컴포넌트 ID (repeater 또는 tableList) + + // 컴포넌트별 테이블 설정 (개발 가이드 준수) + tableName?: string; // 사용할 테이블명 + customTableName?: string; // 커스텀 테이블명 + useCustomTable?: boolean; // true: customTableName 사용 + + // 집계 항목들 + items: AggregationItem[]; + + // 레이아웃 설정 + layout: "horizontal" | "vertical"; // 배치 방향 + showLabels: boolean; // 라벨 표시 여부 + showIcons: boolean; // 아이콘 표시 여부 + gap?: string; // 항목 간 간격 + + // 스타일 설정 + backgroundColor?: string; + borderRadius?: string; + padding?: string; + fontSize?: string; + labelFontSize?: string; + valueFontSize?: string; + labelColor?: string; + valueColor?: string; +} + +/** + * 집계 결과 + */ +export interface AggregationResult { + id: string; + label: string; + value: number | string; + formattedValue: string; + type: AggregationType; +} + diff --git a/frontend/lib/registry/components/index.ts b/frontend/lib/registry/components/index.ts index 7c965de0..563c184f 100644 --- a/frontend/lib/registry/components/index.ts +++ b/frontend/lib/registry/components/index.ts @@ -94,6 +94,9 @@ import "./unified-repeater/UnifiedRepeaterRenderer"; // 인라인/모달/버튼 // 🆕 피벗 그리드 컴포넌트 import "./pivot-grid/PivotGridRenderer"; // 피벗 테이블 (행/열 그룹화, 집계, 드릴다운) +// 🆕 집계 위젯 컴포넌트 +import "./aggregation-widget/AggregationWidgetRenderer"; // 데이터 집계 (합계, 평균, 개수 등) + /** * 컴포넌트 초기화 함수 */ diff --git a/frontend/lib/utils/multilangLabelExtractor.ts b/frontend/lib/utils/multilangLabelExtractor.ts index 38788bd1..1bad6194 100644 --- a/frontend/lib/utils/multilangLabelExtractor.ts +++ b/frontend/lib/utils/multilangLabelExtractor.ts @@ -89,7 +89,7 @@ export function extractMultilangLabels( const extractFromComponent = (comp: ComponentData, parentType?: string, parentLabel?: string) => { const anyComp = comp as any; - const config = anyComp.componentConfig; + const config = anyComp.componentConfig || anyComp.config; const compType = anyComp.componentType || anyComp.type; const compLabel = anyComp.label || anyComp.title || compType; @@ -326,6 +326,23 @@ export function extractMultilangLabels( }); } + // 11. 집계 위젯 (aggregation-widget) + if (compType === "aggregation-widget" && config?.items && Array.isArray(config.items)) { + config.items.forEach((item: any, index: number) => { + if (item.columnLabel && typeof item.columnLabel === "string") { + addLabel( + `${comp.id}_agg_${item.id || index}`, + item.columnLabel, + "label", + compType, + compLabel, + item.labelLangKeyId, + item.labelLangKey + ); + } + }); + } + // 자식 컴포넌트 재귀 탐색 if (anyComp.children && Array.isArray(anyComp.children)) { anyComp.children.forEach((child: ComponentData) => { @@ -401,7 +418,7 @@ export function applyMultilangMappings( const updateComponent = (comp: ComponentData): ComponentData => { const anyComp = comp as any; - const config = anyComp.componentConfig; + const config = anyComp.componentConfig || anyComp.config; let updated = { ...comp } as any; // 기본 컴포넌트 라벨 매핑 확인 @@ -591,6 +608,25 @@ export function applyMultilangMappings( }; } + // 집계 위젯 (aggregation-widget) 매핑 + if (compType === "aggregation-widget" && config?.items && Array.isArray(config.items)) { + const updatedItems = config.items.map((item: any, index: number) => { + const itemMapping = mappingMap.get(`${comp.id}_agg_${item.id || index}`); + if (itemMapping) { + return { + ...item, + labelLangKeyId: itemMapping.keyId, + labelLangKey: itemMapping.langKey, + }; + } + return item; + }); + updated.componentConfig = { + ...updated.componentConfig, + items: updatedItems, + }; + } + // 자식 컴포넌트 재귀 처리 if (anyComp.children && Array.isArray(anyComp.children)) { updated.children = anyComp.children.map((child: ComponentData) => updateComponent(child));