feat: 리포트 타입 에러 수정
This commit is contained in:
@@ -0,0 +1,600 @@
|
||||
"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 <Type className="w-3 h-3" />;
|
||||
case "dataCell":
|
||||
return <CreditCard className="w-3 h-3" />;
|
||||
case "divider":
|
||||
return <Minus className="w-3 h-3" />;
|
||||
case "badge":
|
||||
return <Tag className="w-3 h-3" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export function CardLayoutModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
initialConfig,
|
||||
onSave,
|
||||
}: CardLayoutModalProps) {
|
||||
const [activeTab, setActiveTab] = useState<TabType>("layout");
|
||||
const [config, setConfig] = useState<CardLayoutConfig>(
|
||||
initialConfig || DEFAULT_CONFIG,
|
||||
);
|
||||
const [tables, setTables] = useState<TableInfo[]>([]);
|
||||
const [columns, setColumns] = useState<ColumnInfo[]>([]);
|
||||
const [loadingTables, setLoadingTables] = useState(false);
|
||||
const [loadingColumns, setLoadingColumns] = useState(false);
|
||||
const initialSnapshotRef = useRef<string>("");
|
||||
|
||||
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<string>();
|
||||
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 = () => (
|
||||
<div className="space-y-4">
|
||||
<CardElementPalette />
|
||||
<CardCanvasEditor rows={config.rows} onRowsChange={handleRowsChange} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderBindingTab = () => (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-teal-50 border border-teal-200 rounded-xl p-4 space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-foreground">
|
||||
테이블 선택
|
||||
</Label>
|
||||
<Select
|
||||
value={config.tableName || ""}
|
||||
onValueChange={handleTableChange}
|
||||
disabled={loadingTables}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue
|
||||
placeholder={loadingTables ? "로딩 중..." : "테이블을 선택하세요"}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{tables.map((table) => (
|
||||
<SelectItem key={table.table_name} value={table.table_name}>
|
||||
{table.table_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-muted-foreground">
|
||||
기본 키
|
||||
</Label>
|
||||
<Select
|
||||
value={config.primaryKey || ""}
|
||||
onValueChange={(pk) => setConfig((prev) => ({ ...prev, primaryKey: pk }))}
|
||||
disabled={!config.tableName || loadingColumns}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm bg-muted">
|
||||
<SelectValue placeholder="기본 키 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{columns.map((col) => (
|
||||
<SelectItem key={col.column_name} value={col.column_name}>
|
||||
{col.column_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-border rounded-xl overflow-hidden shadow-sm">
|
||||
<div
|
||||
className="p-3"
|
||||
style={{
|
||||
backgroundColor: config.backgroundColor,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: config.gap,
|
||||
}}
|
||||
>
|
||||
{config.rows.map((row, rowIndex) => (
|
||||
<div
|
||||
key={row.id}
|
||||
className="grid"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${row.gridColumns}, 1fr)`,
|
||||
gap: config.gap,
|
||||
}}
|
||||
>
|
||||
{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 (
|
||||
<div
|
||||
key={element.id}
|
||||
className="border border-gray-100 rounded px-2 py-1.5 hover:bg-gray-50"
|
||||
style={{ gridColumn: `span ${element.colspan || 1}` }}
|
||||
>
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<span className="text-gray-400">
|
||||
{getElementIcon(element.type)}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{element.type === "header" && (element as any).title}
|
||||
{element.type === "dataCell" &&
|
||||
(element as CardDataCellElement).label}
|
||||
{element.type === "divider" && "구분선"}
|
||||
{element.type === "badge" &&
|
||||
((element as CardBadgeElement).label || "뱃지")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{needsBinding && (
|
||||
<Select
|
||||
value={currentColumn || ""}
|
||||
onValueChange={(col) =>
|
||||
handleColumnMapping(rowIndex, elementIndex, col)
|
||||
}
|
||||
disabled={!config.tableName}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={`h-7 text-xs ${
|
||||
currentColumn
|
||||
? "bg-blue-50 text-blue-700 border-blue-200"
|
||||
: "border-dashed"
|
||||
}`}
|
||||
>
|
||||
<SelectValue placeholder="컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{columns.map((col) => {
|
||||
const isUsed =
|
||||
usedColumns.has(col.column_name) &&
|
||||
currentColumn !== col.column_name;
|
||||
return (
|
||||
<SelectItem
|
||||
key={col.column_name}
|
||||
value={col.column_name}
|
||||
disabled={isUsed}
|
||||
className={isUsed ? "opacity-50" : ""}
|
||||
>
|
||||
{col.column_name}
|
||||
{isUsed && " (사용 중)"}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!config.tableName && (
|
||||
<div className="text-center text-sm text-muted-foreground py-4">
|
||||
테이블을 먼저 선택해주세요.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{config.tableName && config.rows.every((r) => r.elements.length === 0) && (
|
||||
<div className="text-center text-sm text-muted-foreground py-4">
|
||||
레이아웃 탭에서 요소를 먼저 추가해주세요.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderStyleTab = () => (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-white border border-border rounded-xl p-4 space-y-3">
|
||||
<div className="text-xs font-medium text-foreground mb-2">카드 스타일</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-foreground">패딩</Label>
|
||||
<Select value={config.padding || "12px"} onValueChange={(v) => setConfig((prev) => ({ ...prev, padding: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="8px">8px</SelectItem>
|
||||
<SelectItem value="12px">12px</SelectItem>
|
||||
<SelectItem value="16px">16px</SelectItem>
|
||||
<SelectItem value="20px">20px</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-foreground">행 간격</Label>
|
||||
<Select value={config.gap || "8px"} onValueChange={(v) => setConfig((prev) => ({ ...prev, gap: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="4px">4px</SelectItem>
|
||||
<SelectItem value="8px">8px</SelectItem>
|
||||
<SelectItem value="12px">12px</SelectItem>
|
||||
<SelectItem value="16px">16px</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-foreground">테두리</Label>
|
||||
<Select value={config.borderStyle || "solid"} onValueChange={(v) => setConfig((prev) => ({ ...prev, borderStyle: v }))}>
|
||||
<SelectTrigger className="h-9 text-sm"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="solid">실선</SelectItem>
|
||||
<SelectItem value="dashed">점선</SelectItem>
|
||||
<SelectItem value="none">없음</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-foreground">테두리 색상</Label>
|
||||
<Input type="color" value={config.borderColor || "#e5e7eb"} onChange={(e) => setConfig((prev) => ({ ...prev, borderColor: e.target.value }))} className="h-9 w-full p-1" />
|
||||
</div>
|
||||
<div className="space-y-2 col-span-2">
|
||||
<Label className="text-xs font-medium text-foreground">배경색</Label>
|
||||
<Input type="color" value={config.backgroundColor || "#ffffff"} onChange={(e) => setConfig((prev) => ({ ...prev, backgroundColor: e.target.value }))} className="h-9 w-full p-1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border border-border rounded-xl p-4 space-y-3">
|
||||
<div className="text-xs font-medium text-foreground mb-2">요소별 스타일</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-foreground">헤더 폰트 크기</Label>
|
||||
<Input type="number" min={10} max={24} value={config.headerTitleFontSize || 14} onChange={(e) => setConfig((prev) => ({ ...prev, headerTitleFontSize: parseInt(e.target.value) || 14 }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-foreground">헤더 색상</Label>
|
||||
<Input type="color" value={config.headerTitleColor || "#1e40af"} onChange={(e) => setConfig((prev) => ({ ...prev, headerTitleColor: e.target.value }))} className="h-9 w-full p-1" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-foreground">라벨 폰트 크기</Label>
|
||||
<Input type="number" min={10} max={20} value={config.labelFontSize || 13} onChange={(e) => setConfig((prev) => ({ ...prev, labelFontSize: parseInt(e.target.value) || 13 }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-foreground">라벨 색상</Label>
|
||||
<Input type="color" value={config.labelColor || "#374151"} onChange={(e) => setConfig((prev) => ({ ...prev, labelColor: e.target.value }))} className="h-9 w-full p-1" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-foreground">값 폰트 크기</Label>
|
||||
<Input type="number" min={10} max={20} value={config.valueFontSize || 13} onChange={(e) => setConfig((prev) => ({ ...prev, valueFontSize: parseInt(e.target.value) || 13 }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-foreground">값 색상</Label>
|
||||
<Input type="color" value={config.valueColor || "#000000"} onChange={(e) => setConfig((prev) => ({ ...prev, valueColor: e.target.value }))} className="h-9 w-full p-1" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-foreground">구분선 두께</Label>
|
||||
<Input type="number" min={1} max={5} value={config.dividerThickness || 1} onChange={(e) => setConfig((prev) => ({ ...prev, dividerThickness: parseInt(e.target.value) || 1 }))} className="h-9 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-foreground">구분선 색상</Label>
|
||||
<Input type="color" value={config.dividerColor || "#e5e7eb"} onChange={(e) => setConfig((prev) => ({ ...prev, dividerColor: e.target.value }))} className="h-9 w-full p-1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={guard.handleOpenChange}>
|
||||
<DialogContent className="max-w-4xl h-[92vh] overflow-hidden flex flex-col p-0 [&>button]:hidden">
|
||||
<DialogTitle className="sr-only">카드 레이아웃 설정</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
카드 컴포넌트의 레이아웃, 데이터 바인딩, 스타일을 설정합니다
|
||||
</DialogDescription>
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b border-border flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutGrid className="w-4 h-4 text-blue-600" />
|
||||
<h2 className="text-base font-semibold text-foreground">
|
||||
카드 레이아웃 설정
|
||||
</h2>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={guard.tryClose}
|
||||
className="w-8 h-8"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Tab */}
|
||||
<div className="mx-6 mt-3">
|
||||
<div className="h-9 bg-muted/30 rounded-lg p-0.5 inline-flex">
|
||||
<button
|
||||
onClick={() => setActiveTab("layout")}
|
||||
className={`px-3 py-1.5 rounded-md text-xs font-medium transition-all flex items-center gap-1.5 ${
|
||||
activeTab === "layout"
|
||||
? "bg-blue-50 text-blue-700 shadow-sm"
|
||||
: "bg-transparent text-foreground hover:text-foreground/80"
|
||||
}`}
|
||||
>
|
||||
<LayoutGrid className="w-3.5 h-3.5" />
|
||||
레이아웃
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("binding")}
|
||||
className={`px-3 py-1.5 rounded-md text-xs font-medium transition-all flex items-center gap-1.5 ${
|
||||
activeTab === "binding"
|
||||
? "bg-blue-50 text-blue-700 shadow-sm"
|
||||
: "bg-transparent text-foreground hover:text-foreground/80"
|
||||
}`}
|
||||
>
|
||||
<Database className="w-3.5 h-3.5" />
|
||||
데이터 바인딩
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("style")}
|
||||
className={`px-3 py-1.5 rounded-md text-xs font-medium transition-all flex items-center gap-1.5 ${
|
||||
activeTab === "style"
|
||||
? "bg-blue-50 text-blue-700 shadow-sm"
|
||||
: "bg-transparent text-foreground hover:text-foreground/80"
|
||||
}`}
|
||||
>
|
||||
<Palette className="w-3.5 h-3.5" />
|
||||
스타일
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||
{activeTab === "layout" && renderLayoutTab()}
|
||||
{activeTab === "binding" && renderBindingTab()}
|
||||
{activeTab === "style" && renderStyleTab()}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-4 border-t border-border flex items-center justify-end gap-2">
|
||||
<Button variant="outline" onClick={guard.tryClose}>
|
||||
취소
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700" onClick={handleSave}>
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<UnsavedChangesDialog guard={guard} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user