"use client"; /** * pop-dashboard 설정 패널 (디자이너용) * * 3개 탭: * [기본 설정] - 표시 모드, 간격, 인디케이터 * [아이템 관리] - 아이템 추가/삭제/순서변경, 데이터 소스 설정 * [레이아웃] - grid 모드 셀 분할/병합 */ import React, { useState, useEffect, useCallback } from "react"; import { Plus, Trash2, ChevronDown, ChevronUp, GripVertical, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import type { PopDashboardConfig, DashboardItem, DashboardSubType, DashboardDisplayMode, DataSourceConfig, FormulaConfig, ItemVisibility, DashboardCell, } from "../types"; import { fetchTableColumns, type ColumnInfo } from "./utils/dataFetcher"; import { validateExpression } from "./utils/formula"; // ===== Props ===== interface ConfigPanelProps { config: PopDashboardConfig | undefined; onChange: (config: PopDashboardConfig) => void; } // ===== 기본값 ===== const DEFAULT_CONFIG: PopDashboardConfig = { items: [], displayMode: "arrows", autoSlideInterval: 5, autoSlideResumeDelay: 3, showIndicator: true, gap: 8, gridColumns: 2, gridRows: 2, gridCells: [], }; const DEFAULT_VISIBILITY: ItemVisibility = { showLabel: true, showValue: true, showUnit: true, showTrend: true, showSubLabel: false, showTarget: true, }; const DEFAULT_DATASOURCE: DataSourceConfig = { tableName: "", filters: [], sort: [], }; // ===== 라벨 상수 ===== const DISPLAY_MODE_LABELS: Record = { arrows: "좌우 버튼", "auto-slide": "자동 슬라이드", grid: "그리드", scroll: "스크롤", }; const SUBTYPE_LABELS: Record = { "kpi-card": "KPI 카드", chart: "차트", gauge: "게이지", "stat-card": "통계 카드", }; // ===== 데이터 소스 편집기 ===== function DataSourceEditor({ dataSource, onChange, }: { dataSource: DataSourceConfig; onChange: (ds: DataSourceConfig) => void; }) { const [columns, setColumns] = useState([]); const [loadingCols, setLoadingCols] = useState(false); // 테이블 변경 시 컬럼 목록 조회 useEffect(() => { if (!dataSource.tableName) { setColumns([]); return; } setLoadingCols(true); fetchTableColumns(dataSource.tableName) .then(setColumns) .finally(() => setLoadingCols(false)); }, [dataSource.tableName]); return (
{/* 테이블명 입력 */}
onChange({ ...dataSource, tableName: e.target.value }) } placeholder="예: production" className="h-8 text-xs" />
{/* 집계 함수 */}
{/* 집계 대상 컬럼 */} {dataSource.aggregation && (
)}
{/* 새로고침 주기 */}
onChange({ ...dataSource, refreshInterval: parseInt(e.target.value) || 0, }) } className="h-8 text-xs" min={0} />
); } // ===== 수식 편집기 ===== function FormulaEditor({ formula, onChange, }: { formula: FormulaConfig; onChange: (f: FormulaConfig) => void; }) { const availableIds = formula.values.map((v) => v.id); const isValid = formula.expression ? validateExpression(formula.expression, availableIds) : true; return (

계산식 설정

{/* 값 목록 */} {formula.values.map((fv, index) => (
{fv.id} { const newValues = [...formula.values]; newValues[index] = { ...fv, label: e.target.value }; onChange({ ...formula, values: newValues }); }} placeholder="라벨 (예: 생산량)" className="h-7 flex-1 text-xs" /> {formula.values.length > 2 && ( )}
{ const newValues = [...formula.values]; newValues[index] = { ...fv, dataSource: ds }; onChange({ ...formula, values: newValues }); }} />
))} {/* 값 추가 */} {/* 수식 입력 */}
onChange({ ...formula, expression: e.target.value }) } placeholder="예: A / B * 100" className={`h-8 text-xs ${!isValid ? "border-destructive" : ""}`} /> {!isValid && (

수식에 정의되지 않은 변수가 있습니다

)}
{/* 표시 형태 */}
); } // ===== 아이템 편집기 ===== function ItemEditor({ item, index, onUpdate, onDelete, onMoveUp, onMoveDown, isFirst, isLast, }: { item: DashboardItem; index: number; onUpdate: (item: DashboardItem) => void; onDelete: () => void; onMoveUp: () => void; onMoveDown: () => void; isFirst: boolean; isLast: boolean; }) { const [expanded, setExpanded] = useState(false); const [dataMode, setDataMode] = useState<"single" | "formula">( item.formula?.enabled ? "formula" : "single" ); return (
{/* 헤더 */}
{item.label || `아이템 ${index + 1}`} {SUBTYPE_LABELS[item.subType]} {/* 이동 버튼 */} {/* 보이기/숨기기 */} onUpdate({ ...item, visible: checked }) } className="scale-75" /> {/* 접기/펼치기 */} {/* 삭제 */}
{/* 상세 설정 (접힘) */} {expanded && (
{/* 라벨 */}
onUpdate({ ...item, label: e.target.value })} className="h-8 text-xs" placeholder="아이템 이름" />
{/* 서브타입 */}
{/* 데이터 모드 선택 */}
{/* 데이터 소스 / 수식 편집 */} {dataMode === "formula" && item.formula ? ( onUpdate({ ...item, formula: f })} /> ) : ( onUpdate({ ...item, dataSource: ds })} /> )} {/* 요소별 보이기/숨기기 */}
{( [ ["showLabel", "라벨"], ["showValue", "값"], ["showUnit", "단위"], ["showTrend", "증감율"], ["showSubLabel", "보조라벨"], ["showTarget", "목표값"], ] as const ).map(([key, label]) => ( ))}
{/* 서브타입별 추가 설정 */} {item.subType === "kpi-card" && (
onUpdate({ ...item, kpiConfig: { ...item.kpiConfig, unit: e.target.value }, }) } placeholder="EA, 톤, 원" className="h-8 text-xs" />
)} {item.subType === "chart" && (
)} {item.subType === "gauge" && (
onUpdate({ ...item, gaugeConfig: { min: parseInt(e.target.value) || 0, max: item.gaugeConfig?.max ?? 100, ...item.gaugeConfig, }, }) } className="h-8 text-xs" />
onUpdate({ ...item, gaugeConfig: { min: item.gaugeConfig?.min ?? 0, max: parseInt(e.target.value) || 100, ...item.gaugeConfig, }, }) } className="h-8 text-xs" />
onUpdate({ ...item, gaugeConfig: { min: item.gaugeConfig?.min ?? 0, max: item.gaugeConfig?.max ?? 100, ...item.gaugeConfig, target: parseInt(e.target.value) || undefined, }, }) } className="h-8 text-xs" />
)}
)}
); } // ===== 그리드 레이아웃 편집기 ===== function GridLayoutEditor({ cells, gridColumns, gridRows, items, onChange, }: { cells: DashboardCell[]; gridColumns: number; gridRows: number; items: DashboardItem[]; onChange: ( cells: DashboardCell[], cols: number, rows: number ) => void; }) { // 셀이 없으면 기본 그리드 생성 const ensuredCells = cells.length > 0 ? cells : Array.from({ length: gridColumns * gridRows }, (_, i) => ({ id: `cell-${i}`, gridColumn: `${(i % gridColumns) + 1} / ${(i % gridColumns) + 2}`, gridRow: `${Math.floor(i / gridColumns) + 1} / ${Math.floor(i / gridColumns) + 2}`, itemId: null as string | null, })); return (
{/* 열/행 수 */}
{ const newCols = Math.max(1, parseInt(e.target.value) || 1); onChange(ensuredCells, newCols, gridRows); }} className="h-8 text-xs" min={1} max={6} />
{ const newRows = Math.max(1, parseInt(e.target.value) || 1); onChange(ensuredCells, gridColumns, newRows); }} className="h-8 text-xs" min={1} max={6} />
{/* 셀 미리보기 + 아이템 배정 */}
{ensuredCells.map((cell) => (
))}
{/* 셀 재생성 */}
); } // ===== 메인 설정 패널 ===== export function PopDashboardConfigPanel({ config, onChange, }: ConfigPanelProps) { const cfg = config ?? DEFAULT_CONFIG; const [activeTab, setActiveTab] = useState<"basic" | "items" | "layout">( "basic" ); // 설정 변경 헬퍼 const updateConfig = useCallback( (partial: Partial) => { onChange({ ...cfg, ...partial }); }, [cfg, onChange] ); // 아이템 추가 const addItem = useCallback( (subType: DashboardSubType) => { const newItem: DashboardItem = { id: `item-${Date.now()}`, label: `${SUBTYPE_LABELS[subType]} ${cfg.items.length + 1}`, visible: true, subType, dataSource: { ...DEFAULT_DATASOURCE }, visibility: { ...DEFAULT_VISIBILITY }, }; updateConfig({ items: [...cfg.items, newItem] }); }, [cfg.items, updateConfig] ); // 아이템 업데이트 const updateItem = useCallback( (index: number, item: DashboardItem) => { const newItems = [...cfg.items]; newItems[index] = item; updateConfig({ items: newItems }); }, [cfg.items, updateConfig] ); // 아이템 삭제 (grid 셀 배정도 해제) const deleteItem = useCallback( (index: number) => { const deletedId = cfg.items[index].id; const newItems = cfg.items.filter((_, i) => i !== index); // grid 셀에서 해당 아이템 배정 해제 const newCells = cfg.gridCells?.map((cell) => cell.itemId === deletedId ? { ...cell, itemId: null } : cell ); updateConfig({ items: newItems, gridCells: newCells }); }, [cfg.items, cfg.gridCells, updateConfig] ); // 아이템 순서 변경 const moveItem = useCallback( (from: number, to: number) => { if (to < 0 || to >= cfg.items.length) return; const newItems = [...cfg.items]; const [moved] = newItems.splice(from, 1); newItems.splice(to, 0, moved); updateConfig({ items: newItems }); }, [cfg.items, updateConfig] ); return (
{/* 탭 헤더 */}
{( [ ["basic", "기본 설정"], ["items", "아이템"], ["layout", "레이아웃"], ] as const ).map(([key, label]) => ( ))}
{/* ===== 기본 설정 탭 ===== */} {activeTab === "basic" && (
{/* 표시 모드 */}
{/* 자동 슬라이드 설정 */} {cfg.displayMode === "auto-slide" && (
updateConfig({ autoSlideInterval: parseInt(e.target.value) || 5, }) } className="h-8 text-xs" min={1} />
updateConfig({ autoSlideResumeDelay: parseInt(e.target.value) || 3, }) } className="h-8 text-xs" min={1} />
)} {/* 인디케이터 */}
updateConfig({ showIndicator: checked }) } />
{/* 간격 */}
updateConfig({ gap: parseInt(e.target.value) || 8 }) } className="h-8 text-xs" min={0} />
{/* 배경색 */}
updateConfig({ backgroundColor: e.target.value || undefined }) } placeholder="예: #f0f0f0" className="h-8 text-xs" />
)} {/* ===== 아이템 관리 탭 ===== */} {activeTab === "items" && (
{/* 아이템 목록 */} {cfg.items.map((item, index) => ( updateItem(index, updated)} onDelete={() => deleteItem(index)} onMoveUp={() => moveItem(index, index - 1)} onMoveDown={() => moveItem(index, index + 1)} isFirst={index === 0} isLast={index === cfg.items.length - 1} /> ))} {/* 아이템 추가 버튼 */}
{(Object.keys(SUBTYPE_LABELS) as DashboardSubType[]).map( (subType) => ( ) )}
)} {/* ===== 레이아웃 탭 (grid 모드 전용) ===== */} {activeTab === "layout" && (
{cfg.displayMode === "grid" ? ( updateConfig({ gridCells: cells, gridColumns: cols, gridRows: rows, }) } /> ) : (

그리드 모드에서만 레이아웃을 편집할 수 있습니다.
기본 설정에서 표시 모드를 "그리드"로 변경하세요.

)}
)}
); }