631 lines
26 KiB
TypeScript
631 lines
26 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "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 { Loader2, Plus, Trash2, GripVertical } from "lucide-react";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Separator } from "@/components/ui/separator";
|
|
|
|
// 계층 레벨 설정 인터페이스
|
|
export interface HierarchyLevel {
|
|
level: number;
|
|
name: string;
|
|
tableName: string;
|
|
keyColumn: string;
|
|
nameColumn: string;
|
|
parentKeyColumn: string;
|
|
typeColumn?: string;
|
|
objectTypes: string[];
|
|
}
|
|
|
|
// 전체 계층 구조 설정
|
|
export interface HierarchyConfig {
|
|
warehouseKey: string; // 이 레이아웃이 속한 창고 키 (예: "DY99")
|
|
warehouse?: {
|
|
tableName: string; // 창고 테이블명 (예: "MWARMA")
|
|
keyColumn: string;
|
|
nameColumn: string;
|
|
};
|
|
levels: HierarchyLevel[];
|
|
material?: {
|
|
tableName: string;
|
|
keyColumn: string;
|
|
locationKeyColumn: string;
|
|
layerColumn?: string;
|
|
quantityColumn?: string;
|
|
displayColumns?: Array<{ column: string; label: string }>; // 우측 패널에 표시할 컬럼들 (컬럼명 + 표시명)
|
|
};
|
|
}
|
|
|
|
interface TableInfo {
|
|
table_name: string;
|
|
description?: string;
|
|
}
|
|
|
|
interface ColumnInfo {
|
|
column_name: string;
|
|
data_type?: string;
|
|
description?: string;
|
|
}
|
|
|
|
interface HierarchyConfigPanelProps {
|
|
externalDbConnectionId: number | null;
|
|
hierarchyConfig: HierarchyConfig | null;
|
|
onHierarchyConfigChange: (config: HierarchyConfig) => void;
|
|
availableTables: TableInfo[];
|
|
onLoadTables: () => Promise<void>;
|
|
onLoadColumns: (tableName: string) => Promise<ColumnInfo[]>;
|
|
}
|
|
|
|
export default function HierarchyConfigPanel({
|
|
externalDbConnectionId,
|
|
hierarchyConfig,
|
|
onHierarchyConfigChange,
|
|
availableTables,
|
|
onLoadTables,
|
|
onLoadColumns,
|
|
}: HierarchyConfigPanelProps) {
|
|
const [localConfig, setLocalConfig] = useState<HierarchyConfig>(
|
|
hierarchyConfig || {
|
|
warehouseKey: "",
|
|
levels: [],
|
|
},
|
|
);
|
|
|
|
const [loadingColumns, setLoadingColumns] = useState(false);
|
|
const [columnsCache, setColumnsCache] = useState<{ [tableName: string]: ColumnInfo[] }>({});
|
|
|
|
// 외부에서 변경된 경우 동기화
|
|
useEffect(() => {
|
|
if (hierarchyConfig) {
|
|
setLocalConfig(hierarchyConfig);
|
|
}
|
|
}, [hierarchyConfig]);
|
|
|
|
// 테이블 선택 시 컬럼 로드
|
|
const handleTableChange = async (tableName: string, type: "warehouse" | "material" | number) => {
|
|
if (columnsCache[tableName]) return; // 이미 로드된 경우 스킵
|
|
|
|
setLoadingColumns(true);
|
|
try {
|
|
const columns = await onLoadColumns(tableName);
|
|
setColumnsCache((prev) => ({ ...prev, [tableName]: columns }));
|
|
} catch (error) {
|
|
console.error("컬럼 로드 실패:", error);
|
|
} finally {
|
|
setLoadingColumns(false);
|
|
}
|
|
};
|
|
|
|
// 창고 키 변경 (제거됨 - 상위 컴포넌트에서 처리)
|
|
|
|
// 레벨 추가
|
|
const handleAddLevel = () => {
|
|
const maxLevel = localConfig.levels.length > 0 ? Math.max(...localConfig.levels.map((l) => l.level)) : 0;
|
|
const newLevel: HierarchyLevel = {
|
|
level: maxLevel + 1,
|
|
name: `레벨 ${maxLevel + 1}`,
|
|
tableName: "",
|
|
keyColumn: "",
|
|
nameColumn: "",
|
|
parentKeyColumn: "",
|
|
objectTypes: [],
|
|
};
|
|
|
|
const newConfig = {
|
|
...localConfig,
|
|
levels: [...localConfig.levels, newLevel],
|
|
};
|
|
setLocalConfig(newConfig);
|
|
// onHierarchyConfigChange(newConfig); // 즉시 전달하지 않음
|
|
};
|
|
|
|
// 레벨 삭제
|
|
const handleRemoveLevel = (level: number) => {
|
|
const newConfig = {
|
|
...localConfig,
|
|
levels: localConfig.levels.filter((l) => l.level !== level),
|
|
};
|
|
setLocalConfig(newConfig);
|
|
// onHierarchyConfigChange(newConfig); // 즉시 전달하지 않음
|
|
};
|
|
|
|
// 레벨 설정 변경
|
|
const handleLevelChange = (level: number, field: keyof HierarchyLevel, value: any) => {
|
|
const newConfig = {
|
|
...localConfig,
|
|
levels: localConfig.levels.map((l) => (l.level === level ? { ...l, [field]: value } : l)),
|
|
};
|
|
setLocalConfig(newConfig);
|
|
// onHierarchyConfigChange(newConfig); // 즉시 전달하지 않음
|
|
};
|
|
|
|
// 자재 설정 변경
|
|
const handleMaterialChange = (field: keyof NonNullable<HierarchyConfig["material"]>, value: string) => {
|
|
const newConfig = {
|
|
...localConfig,
|
|
material: {
|
|
...localConfig.material,
|
|
[field]: value,
|
|
} as NonNullable<HierarchyConfig["material"]>,
|
|
};
|
|
setLocalConfig(newConfig);
|
|
// onHierarchyConfigChange(newConfig); // 즉시 전달하지 않음
|
|
};
|
|
|
|
// 창고 설정 변경
|
|
const handleWarehouseChange = (field: keyof NonNullable<HierarchyConfig["warehouse"]>, value: string) => {
|
|
const newWarehouse = {
|
|
...localConfig.warehouse,
|
|
[field]: value,
|
|
} as NonNullable<HierarchyConfig["warehouse"]>;
|
|
setLocalConfig({ ...localConfig, warehouse: newWarehouse });
|
|
};
|
|
|
|
// 설정 적용
|
|
const handleApplyConfig = () => {
|
|
onHierarchyConfigChange(localConfig);
|
|
};
|
|
|
|
if (!externalDbConnectionId) {
|
|
return <div className="text-muted-foreground p-4 text-center text-sm">외부 DB를 먼저 선택하세요</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* 창고 설정 */}
|
|
<Card>
|
|
<CardHeader className="p-4 pb-3">
|
|
<CardTitle className="text-sm">창고 설정</CardTitle>
|
|
<CardDescription className="text-[10px]">창고 테이블 및 컬럼 매핑</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2 p-4 pt-0">
|
|
{/* 창고 테이블 선택 */}
|
|
<div>
|
|
<Label className="text-[10px]">테이블</Label>
|
|
<Select
|
|
value={localConfig.warehouse?.tableName || ""}
|
|
onValueChange={async (value) => {
|
|
handleWarehouseChange("tableName", value);
|
|
await handleTableChange(value, "warehouse");
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-7 text-[10px]">
|
|
<SelectValue placeholder="테이블 선택..." />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{availableTables.map((table) => (
|
|
<SelectItem key={table.table_name} value={table.table_name} className="text-[10px]">
|
|
<div className="flex flex-col">
|
|
<span>{table.table_name}</span>
|
|
{table.description && (
|
|
<span className="text-[9px] text-muted-foreground">{table.description}</span>
|
|
)}
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 창고 컬럼 매핑 */}
|
|
{localConfig.warehouse?.tableName && columnsCache[localConfig.warehouse.tableName] && (
|
|
<div className="space-y-2">
|
|
<div>
|
|
<Label className="text-[10px]">ID 컬럼</Label>
|
|
<Select
|
|
value={localConfig.warehouse.keyColumn || ""}
|
|
onValueChange={(value) => handleWarehouseChange("keyColumn", value)}
|
|
>
|
|
<SelectTrigger className="h-7 text-[10px]">
|
|
<SelectValue placeholder="선택..." />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{columnsCache[localConfig.warehouse.tableName].map((col) => (
|
|
<SelectItem key={col.column_name} value={col.column_name} className="text-[10px]">
|
|
<div className="flex flex-col">
|
|
<span>{col.column_name}</span>
|
|
{col.description && (
|
|
<span className="text-[8px] text-muted-foreground">{col.description}</span>
|
|
)}
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label className="text-[10px]">이름 컬럼</Label>
|
|
<Select
|
|
value={localConfig.warehouse.nameColumn || ""}
|
|
onValueChange={(value) => handleWarehouseChange("nameColumn", value)}
|
|
>
|
|
<SelectTrigger className="h-7 text-[10px]">
|
|
<SelectValue placeholder="선택..." />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{columnsCache[localConfig.warehouse.tableName].map((col) => (
|
|
<SelectItem key={col.column_name} value={col.column_name} className="text-[10px]">
|
|
<div className="flex flex-col">
|
|
<span>{col.column_name}</span>
|
|
{col.description && (
|
|
<span className="text-[8px] text-muted-foreground">{col.description}</span>
|
|
)}
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 계층 레벨 목록 */}
|
|
<Card>
|
|
<CardHeader className="p-4 pb-3">
|
|
<CardTitle className="text-sm">계층 레벨</CardTitle>
|
|
<CardDescription className="text-[10px]">영역, 하위 영역 등</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3 p-4 pt-0">
|
|
{localConfig.levels.length === 0 && (
|
|
<div className="text-muted-foreground py-6 text-center text-xs">레벨을 추가하세요</div>
|
|
)}
|
|
|
|
{localConfig.levels.map((level) => (
|
|
<Card key={level.level} className="border-muted">
|
|
<CardHeader className="flex flex-row items-center justify-between p-3">
|
|
<div className="flex items-center gap-2">
|
|
<GripVertical className="text-muted-foreground h-4 w-4" />
|
|
<Input
|
|
value={level.name || ""}
|
|
onChange={(e) => handleLevelChange(level.level, "name", e.target.value)}
|
|
className="h-7 w-32 text-xs"
|
|
placeholder="레벨명"
|
|
/>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleRemoveLevel(level.level)}
|
|
className="h-7 w-7 p-0"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2 p-3 pt-0">
|
|
<div>
|
|
<Label className="text-[10px]">테이블</Label>
|
|
<Select
|
|
value={level.tableName || ""}
|
|
onValueChange={(val) => {
|
|
handleLevelChange(level.level, "tableName", val);
|
|
handleTableChange(val, level.level);
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-7 text-[10px]">
|
|
<SelectValue placeholder="테이블 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{availableTables.map((table) => (
|
|
<SelectItem key={table.table_name} value={table.table_name} className="text-xs">
|
|
<div className="flex flex-col">
|
|
<span>{table.table_name}</span>
|
|
{table.description && (
|
|
<span className="text-[10px] text-muted-foreground">{table.description}</span>
|
|
)}
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{level.tableName && columnsCache[level.tableName] && (
|
|
<>
|
|
<div>
|
|
<Label className="text-[10px]">ID 컬럼</Label>
|
|
<Select
|
|
value={level.keyColumn || ""}
|
|
onValueChange={(val) => handleLevelChange(level.level, "keyColumn", val)}
|
|
>
|
|
<SelectTrigger className="h-7 text-[10px]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{columnsCache[level.tableName].map((col) => (
|
|
<SelectItem key={col.column_name} value={col.column_name} className="text-xs">
|
|
<div className="flex flex-col">
|
|
<span>{col.column_name}</span>
|
|
{col.description && (
|
|
<span className="text-[9px] text-muted-foreground">{col.description}</span>
|
|
)}
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label className="text-[10px]">이름 컬럼</Label>
|
|
<Select
|
|
value={level.nameColumn || ""}
|
|
onValueChange={(val) => handleLevelChange(level.level, "nameColumn", val)}
|
|
>
|
|
<SelectTrigger className="h-7 text-[10px]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{columnsCache[level.tableName].map((col) => (
|
|
<SelectItem key={col.column_name} value={col.column_name} className="text-xs">
|
|
<div className="flex flex-col">
|
|
<span>{col.column_name}</span>
|
|
{col.description && (
|
|
<span className="text-[9px] text-muted-foreground">{col.description}</span>
|
|
)}
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label className="text-[10px]">부모 키 컬럼</Label>
|
|
<Select
|
|
value={level.parentKeyColumn || ""}
|
|
onValueChange={(val) => handleLevelChange(level.level, "parentKeyColumn", val)}
|
|
>
|
|
<SelectTrigger className="h-7 text-[10px]">
|
|
<SelectValue placeholder="부모 키 컬럼" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{columnsCache[level.tableName].map((col) => (
|
|
<SelectItem key={col} value={col} className="text-xs">
|
|
{col}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label className="text-[10px]">타입 컬럼 (선택)</Label>
|
|
<Select
|
|
value={level.typeColumn || "__none__"}
|
|
onValueChange={(val) =>
|
|
handleLevelChange(level.level, "typeColumn", val === "__none__" ? undefined : val)
|
|
}
|
|
>
|
|
<SelectTrigger className="h-7 text-[10px]">
|
|
<SelectValue placeholder="타입 컬럼 (선택)" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="__none__">없음</SelectItem>
|
|
{columnsCache[level.tableName].map((col) => (
|
|
<SelectItem key={col} value={col} className="text-xs">
|
|
{col}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
|
|
<Button variant="outline" size="sm" onClick={handleAddLevel} className="h-8 w-full text-xs">
|
|
<Plus className="mr-1 h-3 w-3" />
|
|
레벨 추가
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 자재 설정 */}
|
|
<Card>
|
|
<CardHeader className="p-4 pb-3">
|
|
<CardTitle className="text-sm">자재 설정</CardTitle>
|
|
<CardDescription className="text-[10px]">최하위 레벨의 데이터</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3 p-4 pt-0">
|
|
<div>
|
|
<Label className="text-[10px]">테이블</Label>
|
|
<Select
|
|
value={localConfig.material?.tableName || ""}
|
|
onValueChange={(val) => {
|
|
handleMaterialChange("tableName", val);
|
|
handleTableChange(val, "material");
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue placeholder="테이블 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{availableTables.map((table) => (
|
|
<SelectItem key={table.table_name} value={table.table_name} className="text-xs">
|
|
<div className="flex flex-col">
|
|
<span>{table.table_name}</span>
|
|
{table.description && (
|
|
<span className="text-[10px] text-muted-foreground">{table.description}</span>
|
|
)}
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{localConfig.material?.tableName && columnsCache[localConfig.material.tableName] && (
|
|
<>
|
|
<div>
|
|
<Label className="text-[10px]">ID 컬럼</Label>
|
|
<Select
|
|
value={localConfig.material.keyColumn || ""}
|
|
onValueChange={(val) => handleMaterialChange("keyColumn", val)}
|
|
>
|
|
<SelectTrigger className="h-7 text-[10px]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{columnsCache[localConfig.material.tableName].map((col) => (
|
|
<SelectItem key={col.column_name} value={col.column_name} className="text-xs">
|
|
<div className="flex flex-col">
|
|
<span>{col.column_name}</span>
|
|
{col.description && (
|
|
<span className="text-[9px] text-muted-foreground">{col.description}</span>
|
|
)}
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label className="text-[10px]">위치 키 컬럼</Label>
|
|
<Select
|
|
value={localConfig.material.locationKeyColumn || ""}
|
|
onValueChange={(val) => handleMaterialChange("locationKeyColumn", val)}
|
|
>
|
|
<SelectTrigger className="h-7 text-[10px]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{columnsCache[localConfig.material.tableName].map((col) => (
|
|
<SelectItem key={col.column_name} value={col.column_name} className="text-xs">
|
|
<div className="flex flex-col">
|
|
<span>{col.column_name}</span>
|
|
{col.description && (
|
|
<span className="text-[9px] text-muted-foreground">{col.description}</span>
|
|
)}
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label className="text-[10px]">레이어 컬럼 (선택)</Label>
|
|
<Select
|
|
value={localConfig.material.layerColumn || "__none__"}
|
|
onValueChange={(val) => handleMaterialChange("layerColumn", val === "__none__" ? undefined : val)}
|
|
>
|
|
<SelectTrigger className="h-7 text-[10px]">
|
|
<SelectValue placeholder="레이어 컬럼" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="__none__">없음</SelectItem>
|
|
{columnsCache[localConfig.material.tableName].map((col) => (
|
|
<SelectItem key={col.column_name} value={col.column_name} className="text-xs">
|
|
<div className="flex flex-col">
|
|
<span>{col.column_name}</span>
|
|
{col.description && (
|
|
<span className="text-[9px] text-muted-foreground">{col.description}</span>
|
|
)}
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label className="text-[10px]">수량 컬럼 (선택)</Label>
|
|
<Select
|
|
value={localConfig.material.quantityColumn || "__none__"}
|
|
onValueChange={(val) => handleMaterialChange("quantityColumn", val === "__none__" ? undefined : val)}
|
|
>
|
|
<SelectTrigger className="h-7 text-[10px]">
|
|
<SelectValue placeholder="수량 컬럼" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="__none__">없음</SelectItem>
|
|
{columnsCache[localConfig.material.tableName].map((col) => (
|
|
<SelectItem key={col.column_name} value={col.column_name} className="text-xs">
|
|
<div className="flex flex-col">
|
|
<span>{col.column_name}</span>
|
|
{col.description && (
|
|
<span className="text-[9px] text-muted-foreground">{col.description}</span>
|
|
)}
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<Separator className="my-3" />
|
|
|
|
{/* 표시 컬럼 선택 */}
|
|
<div>
|
|
<Label className="text-[10px]">우측 패널 표시 컬럼</Label>
|
|
<p className="text-muted-foreground mb-2 text-[9px]">
|
|
자재 클릭 시 표시할 정보를 선택하고 라벨을 입력하세요
|
|
</p>
|
|
<div className="max-h-60 space-y-2 overflow-y-auto rounded border p-2">
|
|
{columnsCache[localConfig.material.tableName].map((col) => {
|
|
const displayItem = localConfig.material?.displayColumns?.find((d) => d.column === col.column_name);
|
|
const isSelected = !!displayItem;
|
|
return (
|
|
<div key={col.column_name} className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={isSelected}
|
|
onChange={(e) => {
|
|
const currentDisplay = localConfig.material?.displayColumns || [];
|
|
const newDisplay = e.target.checked
|
|
? [...currentDisplay, { column: col.column_name, label: col.column_name }]
|
|
: currentDisplay.filter((d) => d.column !== col.column_name);
|
|
handleMaterialChange("displayColumns", newDisplay);
|
|
}}
|
|
className="h-3 w-3 shrink-0"
|
|
/>
|
|
<div className="flex w-24 shrink-0 flex-col">
|
|
<span className="text-[10px]">{col.column_name}</span>
|
|
{col.description && (
|
|
<span className="text-[8px] text-muted-foreground">{col.description}</span>
|
|
)}
|
|
</div>
|
|
{isSelected && (
|
|
<Input
|
|
value={displayItem?.label ?? ""}
|
|
onChange={(e) => {
|
|
const currentDisplay = localConfig.material?.displayColumns || [];
|
|
const newDisplay = currentDisplay.map((d) =>
|
|
d.column === col.column_name ? { ...d, label: e.target.value } : d,
|
|
);
|
|
handleMaterialChange("displayColumns", newDisplay);
|
|
}}
|
|
placeholder="표시명 입력..."
|
|
className="h-6 flex-1 text-[10px]"
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 적용 버튼 */}
|
|
<div className="flex justify-end">
|
|
<Button onClick={handleApplyConfig} className="h-10 gap-2 text-sm font-medium">
|
|
설정 적용
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|