"use client"; import React, { useState, useEffect, useMemo, useCallback, useRef } from "react"; import { Dialog, DialogContent, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { useUnsavedChangesGuard, UnsavedChangesDialog } from "@/components/common/UnsavedChangesGuard"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { LayoutGrid, Database, Palette, X, Type, CreditCard, Minus, Tag, } from "lucide-react"; import { reportApi } from "@/lib/api/reportApi"; import type { CardLayoutConfig, CardLayoutRow, CardElement, CardDataCellElement, CardBadgeElement, } from "@/types/report"; import { CardElementPalette } from "./CardElementPalette"; import { CardCanvasEditor } from "./CardCanvasEditor"; interface CardLayoutModalProps { open: boolean; onOpenChange: (open: boolean) => void; initialConfig?: CardLayoutConfig; onSave: (config: CardLayoutConfig) => void; } type TabType = "layout" | "binding" | "style"; interface TableInfo { table_name: string; table_type: string; } interface ColumnInfo { column_name: string; data_type: string; is_nullable: string; } const generateId = () => `row_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`; const DEFAULT_CONFIG: CardLayoutConfig = { tableName: "", primaryKey: "", rows: [ { id: generateId(), gridColumns: 4, elements: [], }, ], padding: "12px", gap: "8px", borderStyle: "solid", borderColor: "#e5e7eb", backgroundColor: "#ffffff", headerTitleFontSize: 14, headerTitleColor: "#1e40af", labelFontSize: 13, labelColor: "#374151", valueFontSize: 13, valueColor: "#000000", dividerThickness: 1, dividerColor: "#e5e7eb", }; const getElementIcon = (type: string) => { switch (type) { case "header": return ; case "dataCell": return ; case "divider": return ; case "badge": return ; default: return null; } }; export function CardLayoutModal({ open, onOpenChange, initialConfig, onSave, }: CardLayoutModalProps) { const [activeTab, setActiveTab] = useState("layout"); const [config, setConfig] = useState( initialConfig || DEFAULT_CONFIG, ); const [tables, setTables] = useState([]); const [columns, setColumns] = useState([]); const [loadingTables, setLoadingTables] = useState(false); const [loadingColumns, setLoadingColumns] = useState(false); const initialSnapshotRef = useRef(""); const hasChanges = useCallback(() => { return JSON.stringify(config) !== initialSnapshotRef.current; }, [config]); const guard = useUnsavedChangesGuard({ hasChanges, onClose: () => onOpenChange(false), }); useEffect(() => { if (open) { const initConfig = initialConfig || DEFAULT_CONFIG; setConfig(initConfig); initialSnapshotRef.current = JSON.stringify(initConfig); setActiveTab("layout"); fetchTables(); } }, [open, initialConfig]); useEffect(() => { if (config.tableName) { fetchColumns(config.tableName); } else { setColumns([]); } }, [config.tableName]); const fetchTables = async () => { setLoadingTables(true); try { const response = await reportApi.getSchemaTableList(); if (response.success) { setTables(response.data); } } catch (error) { console.error("테이블 목록 조회 실패:", error); } finally { setLoadingTables(false); } }; const fetchColumns = async (tableName: string) => { setLoadingColumns(true); try { const response = await reportApi.getSchemaTableColumns(tableName); if (response.success) { setColumns(response.data); const pkCandidate = response.data.find( (col) => col.column_name.endsWith("_id") || col.column_name === "id" || col.column_name.endsWith("_pk"), ); if (pkCandidate && !config.primaryKey) { setConfig((prev) => ({ ...prev, primaryKey: pkCandidate.column_name, })); } } } catch (error) { console.error("컬럼 목록 조회 실패:", error); } finally { setLoadingColumns(false); } }; const usedColumns = useMemo(() => { const used = new Set(); config.rows.forEach((row) => { row.elements.forEach((el) => { if (el.type === "dataCell" && (el as CardDataCellElement).columnName) { used.add((el as CardDataCellElement).columnName!); } if (el.type === "badge" && (el as CardBadgeElement).columnName) { used.add((el as CardBadgeElement).columnName!); } }); }); return used; }, [config.rows]); const handleTableChange = (tableName: string) => { setConfig((prev) => ({ ...prev, tableName, primaryKey: "", rows: prev.rows.map((row) => ({ ...row, elements: row.elements.map((el) => { if (el.type === "dataCell") { return { ...el, columnName: undefined } as CardDataCellElement; } if (el.type === "badge") { return { ...el, columnName: undefined } as CardBadgeElement; } return el; }), })), })); }; const handleRowsChange = (rows: CardLayoutRow[]) => { setConfig((prev) => ({ ...prev, rows })); }; const handleColumnMapping = ( rowIndex: number, elementIndex: number, columnName: string, ) => { setConfig((prev) => { const newRows = [...prev.rows]; const newElements = [...newRows[rowIndex].elements]; const element = newElements[elementIndex]; if (element.type === "dataCell") { newElements[elementIndex] = { ...element, columnName, } as CardDataCellElement; } else if (element.type === "badge") { newElements[elementIndex] = { ...element, columnName, } as CardBadgeElement; } newRows[rowIndex] = { ...newRows[rowIndex], elements: newElements }; return { ...prev, rows: newRows }; }); }; const handleSave = () => { onSave(config); onOpenChange(false); }; const renderLayoutTab = () => (
); const renderBindingTab = () => (
{config.rows.map((row, rowIndex) => (
{row.elements.map((element, elementIndex) => { const needsBinding = element.type === "dataCell" || element.type === "badge"; const currentColumn = element.type === "dataCell" ? (element as CardDataCellElement).columnName : element.type === "badge" ? (element as CardBadgeElement).columnName : undefined; return (
{getElementIcon(element.type)} {element.type === "header" && (element as any).title} {element.type === "dataCell" && (element as CardDataCellElement).label} {element.type === "divider" && "구분선"} {element.type === "badge" && ((element as CardBadgeElement).label || "뱃지")}
{needsBinding && ( )}
); })}
))}
{!config.tableName && (
테이블을 먼저 선택해주세요.
)} {config.tableName && config.rows.every((r) => r.elements.length === 0) && (
레이아웃 탭에서 요소를 먼저 추가해주세요.
)}
); const renderStyleTab = () => (
카드 스타일
setConfig((prev) => ({ ...prev, borderColor: e.target.value }))} className="h-9 w-full p-1" />
setConfig((prev) => ({ ...prev, backgroundColor: e.target.value }))} className="h-9 w-full p-1" />
요소별 스타일
setConfig((prev) => ({ ...prev, headerTitleFontSize: parseInt(e.target.value) || 14 }))} className="h-9 text-sm" />
setConfig((prev) => ({ ...prev, headerTitleColor: e.target.value }))} className="h-9 w-full p-1" />
setConfig((prev) => ({ ...prev, labelFontSize: parseInt(e.target.value) || 13 }))} className="h-9 text-sm" />
setConfig((prev) => ({ ...prev, labelColor: e.target.value }))} className="h-9 w-full p-1" />
setConfig((prev) => ({ ...prev, valueFontSize: parseInt(e.target.value) || 13 }))} className="h-9 text-sm" />
setConfig((prev) => ({ ...prev, valueColor: e.target.value }))} className="h-9 w-full p-1" />
setConfig((prev) => ({ ...prev, dividerThickness: parseInt(e.target.value) || 1 }))} className="h-9 text-sm" />
setConfig((prev) => ({ ...prev, dividerColor: e.target.value }))} className="h-9 w-full p-1" />
); return ( <> 카드 레이아웃 설정 카드 컴포넌트의 레이아웃, 데이터 바인딩, 스타일을 설정합니다 {/* Header */}

카드 레이아웃 설정

{/* Tab */}
{/* Content */}
{activeTab === "layout" && renderLayoutTab()} {activeTab === "binding" && renderBindingTab()} {activeTab === "style" && renderStyleTab()}
{/* Footer */}
); }