"use client"; /** * CardLayoutTabs.tsx — 카드 컴포넌트 설정 탭 * * [역할] * - 카드 컴포넌트의 레이아웃 구성 / 데이터 연결 / 표시 조건을 3탭 구조로 제공 * - ComponentSettingsModal 내에 직접 임베드되어 사용 * * [사용처] * - CardProperties.tsx (section="data"일 때) */ import React, { useState, useEffect, useMemo, useCallback } from "react"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { LayoutGrid, Database, CreditCard as CreditCardIcon, Eye, Type, CreditCard, Minus, Tag, ImageIcon, Hash, Calendar, Link2, Circle, Space, FileText, } from "lucide-react"; import { reportApi } from "@/lib/api/reportApi"; import type { CardLayoutConfig, CardLayoutRow, CardDataCellElement, CardBadgeElement, CardNumberElement, CardDateElement, CardLinkElement, CardStatusElement, CardImageElement, ComponentConfig, } from "@/types/report"; import { CardElementPalette } from "./CardElementPalette"; import { CardCanvasEditor } from "./CardCanvasEditor"; import { ConditionalProperties } from "../properties/ConditionalProperties"; import type { CardColumnLabel } from "../properties/ConditionalProperties"; interface CardLayoutTabsProps { config: CardLayoutConfig; onConfigChange: (config: CardLayoutConfig) => void; component?: ComponentConfig; onComponentChange?: (updates: Partial) => void; } type TabType = "layout" | "binding" | "condition"; interface TableInfo { table_name: string; table_type: string; } interface ColumnInfo { column_name: string; data_type: string; is_nullable: string; } const getElementIcon = (type: string) => { switch (type) { case "header": return ; case "dataCell": return ; case "divider": return ; case "badge": return ; case "image": return ; case "number": return ; case "date": return ; case "link": return ; case "status": return ; case "spacer": return ; case "staticText": return ; default: return null; } }; export function CardLayoutTabs({ config, onConfigChange, component, onComponentChange, }: CardLayoutTabsProps) { const [activeTab, setActiveTab] = useState("layout"); const [tables, setTables] = useState([]); const [columns, setColumns] = useState([]); const [loadingTables, setLoadingTables] = useState(false); const [loadingColumns, setLoadingColumns] = useState(false); useEffect(() => { fetchTables(); }, []); 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) { onConfigChange({ ...config, 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) => { const colName = (el as CardDataCellElement | CardBadgeElement | CardNumberElement | CardDateElement | CardLinkElement | CardStatusElement | CardImageElement) .columnName; if (colName) used.add(colName); }); }); return used; }, [config.rows]); const handleTableChange = useCallback( (tableName: string) => { onConfigChange({ ...config, tableName, primaryKey: "", rows: config.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; }), })), }); }, [config, onConfigChange], ); const handleRowsChange = useCallback( (rows: CardLayoutRow[]) => { onConfigChange({ ...config, rows }); }, [config, onConfigChange], ); const handleColumnMapping = useCallback( (rowIndex: number, elementIndex: number, columnName: string) => { const newRows = [...config.rows]; const newElements = [...newRows[rowIndex].elements]; newElements[elementIndex] = { ...newElements[elementIndex], columnName } as typeof newElements[number]; newRows[rowIndex] = { ...newRows[rowIndex], elements: newElements }; onConfigChange({ ...config, rows: newRows }); }, [config, onConfigChange], ); const updateConfig = useCallback( (updates: Partial) => { onConfigChange({ ...config, ...updates }); }, [config, onConfigChange], ); const renderLayoutTab = () => (
); const renderBindingTab = () => (
{/* 데이터 소스 섹션 */}
{/* 카드 WYSIWYG 미리보기 + 인라인 드롭다운 */}
{config.rows.map((row, rowIndex) => (
{row.elements.map((element, elementIndex) => { const needsBinding = ["dataCell", "badge", "number", "date", "link", "status", "image"] .includes(element.type); const currentColumn = (element as CardDataCellElement | CardBadgeElement | CardNumberElement | CardDateElement | CardLinkElement | CardStatusElement | CardImageElement) .columnName; 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 || "뱃지")} {element.type === "image" && "이미지"} {element.type === "number" && ((element as CardNumberElement).label || "숫자/금액")} {element.type === "date" && ((element as CardDateElement).label || "날짜")} {element.type === "link" && ((element as CardLinkElement).label || "링크")} {element.type === "status" && "상태"} {element.type === "spacer" && "빈 공간"} {element.type === "staticText" && "고정 텍스트"}
{/* 컬럼 매핑 드롭다운 */} {needsBinding && ( )}
); })}
))}
{!config.tableName && (
테이블을 먼저 선택해주세요.
)} {config.tableName && config.rows.every((r) => r.elements.length === 0) && (
레이아웃 탭에서 요소를 먼저 추가해주세요.
)}
); const cardColumnLabels = useMemo(() => { const labels: CardColumnLabel[] = []; config.rows.forEach((row) => { row.elements.forEach((el) => { const colName = (el as any).columnName as string | undefined; if (!colName) return; let label = ""; if (el.type === "dataCell") label = (el as any).label || ""; else if (el.type === "badge") label = (el as any).label || "뱃지"; else if (el.type === "number") label = (el as any).label || "숫자/금액"; else if (el.type === "date") label = (el as any).label || "날짜"; else if (el.type === "link") label = (el as any).label || "링크"; else if (el.type === "status") label = "상태"; else if (el.type === "image") label = "이미지"; if (label) labels.push({ columnName: colName, label }); }); }); return labels; }, [config.rows]); const renderConditionTab = () => { if (!component) { return (
표시 조건을 설정하려면 컴포넌트 정보가 필요합니다.
); } return ( ); }; const tabs: { key: TabType; icon: React.ReactNode; label: string }[] = [ { key: "layout", icon: , label: "레이아웃 구성" }, { key: "binding", icon: , label: "데이터 연결" }, { key: "condition", icon: , label: "표시 조건" }, ]; return (
{/* 헤더 + 탭 */}
카드 기능 설정
{tabs.map((tab) => ( ))}
{/* 탭 콘텐츠 */}
{activeTab === "layout" && renderLayoutTab()} {activeTab === "binding" && renderBindingTab()} {activeTab === "condition" && renderConditionTab()}
); }