- Added new `updateMoldSerial` API endpoint for updating mold serial details. - Modified existing mold-related SQL queries to include `id` and `created_date` fields. - Updated frontend to handle mold serial updates and image uploads. - Improved subcontractor management table with additional fields and rendering logic. This update improves the overall functionality and user experience in managing molds and subcontractors.
590 lines
21 KiB
TypeScript
590 lines
21 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* TableSettingsModal -- 하드코딩 페이지용 테이블 설정 모달 (3탭)
|
|
*
|
|
* 탭 1: 컬럼 설정 -- 컬럼 표시/숨김, 드래그 순서 변경, 너비(px) 설정, 틀고정
|
|
* 탭 2: 필터 설정 -- 필터 활성/비활성, 필터 타입(텍스트/선택/날짜), 너비(%) 설정, 그룹별 합산
|
|
* 탭 3: 그룹 설정 -- 그룹핑 컬럼 선택
|
|
*
|
|
* 설정값은 localStorage에 저장되며, onSave 콜백으로 부모 컴포넌트에 전달
|
|
* DynamicSearchFilter, DataGrid와 함께 사용
|
|
*/
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import {
|
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
|
|
} from "@/components/ui/dialog";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import {
|
|
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { GripVertical, Settings2, SlidersHorizontal, Layers, RotateCcw, Lock } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { apiClient } from "@/lib/api/client";
|
|
import {
|
|
DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragEndEvent,
|
|
} from "@dnd-kit/core";
|
|
import {
|
|
SortableContext, verticalListSortingStrategy, useSortable, arrayMove,
|
|
} from "@dnd-kit/sortable";
|
|
import { CSS } from "@dnd-kit/utilities";
|
|
|
|
// ===== 타입 =====
|
|
|
|
export interface ColumnSetting {
|
|
columnName: string;
|
|
displayName: string;
|
|
visible: boolean;
|
|
width: number;
|
|
}
|
|
|
|
export interface FilterSetting {
|
|
columnName: string;
|
|
displayName: string;
|
|
enabled: boolean;
|
|
filterType: "text" | "select" | "date";
|
|
width: number;
|
|
}
|
|
|
|
export interface GroupSetting {
|
|
columnName: string;
|
|
displayName: string;
|
|
enabled: boolean;
|
|
}
|
|
|
|
export interface TableSettings {
|
|
columns: ColumnSetting[];
|
|
filters: FilterSetting[];
|
|
groups: GroupSetting[];
|
|
frozenCount: number;
|
|
groupSumEnabled: boolean;
|
|
}
|
|
|
|
export interface TableSettingsModalProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
/** 테이블명 (web-types API 호출용) */
|
|
tableName: string;
|
|
/** localStorage 키 분리용 고유 ID */
|
|
settingsId: string;
|
|
/** 저장 시 콜백 */
|
|
onSave?: (settings: TableSettings) => void;
|
|
/** 초기 탭 */
|
|
initialTab?: "columns" | "filters" | "groups";
|
|
/** 기본 표시 컬럼 키 목록 (GRID_COLUMNS 기준). 미지정 시 전체 표시 */
|
|
defaultVisibleKeys?: string[];
|
|
/** AUTO_COLS에서 제외하지 않을 컬럼 키 목록 (예: ["created_date", "updated_date", "writer"]) */
|
|
includeAutoColumns?: string[];
|
|
}
|
|
|
|
// ===== 상수 =====
|
|
|
|
const FILTER_TYPE_OPTIONS = [
|
|
{ value: "text", label: "텍스트" },
|
|
{ value: "select", label: "선택" },
|
|
{ value: "date", label: "날짜" },
|
|
];
|
|
|
|
const AUTO_COLS = ["id", "created_date", "updated_date", "writer", "company_code"];
|
|
|
|
// ===== 유틸 =====
|
|
|
|
function getStorageKey(settingsId: string) {
|
|
return `table_settings_${settingsId}`;
|
|
}
|
|
|
|
/** localStorage에서 저장된 설정 로드 (외부에서도 사용 가능) */
|
|
export function loadTableSettings(settingsId: string): TableSettings | null {
|
|
try {
|
|
const raw = localStorage.getItem(getStorageKey(settingsId));
|
|
if (!raw) return null;
|
|
return JSON.parse(raw);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** 저장된 컬럼 순서/설정을 API 컬럼과 병합 */
|
|
function mergeColumns(fresh: ColumnSetting[], saved: ColumnSetting[]): ColumnSetting[] {
|
|
const savedMap = new Map(saved.map((s) => [s.columnName, s]));
|
|
const ordered: ColumnSetting[] = [];
|
|
// 저장된 순서대로
|
|
for (const s of saved) {
|
|
const f = fresh.find((c) => c.columnName === s.columnName);
|
|
if (f) ordered.push({ ...f, visible: s.visible, width: s.width });
|
|
}
|
|
// 새로 추가된 컬럼은 맨 뒤에
|
|
for (const f of fresh) {
|
|
if (!savedMap.has(f.columnName)) ordered.push(f);
|
|
}
|
|
return ordered;
|
|
}
|
|
|
|
// ===== Sortable Column Row (탭 1) =====
|
|
|
|
function SortableColumnRow({
|
|
col,
|
|
onToggleVisible,
|
|
onWidthChange,
|
|
}: {
|
|
col: ColumnSetting & { _idx: number };
|
|
onToggleVisible: (idx: number) => void;
|
|
onWidthChange: (idx: number, width: number) => void;
|
|
}) {
|
|
const {
|
|
attributes, listeners, setNodeRef, transform, transition, isDragging,
|
|
} = useSortable({ id: col.columnName });
|
|
|
|
const style: React.CSSProperties = {
|
|
transform: CSS.Transform.toString(transform),
|
|
transition,
|
|
opacity: isDragging ? 0.5 : 1,
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={setNodeRef}
|
|
style={style}
|
|
className={cn(
|
|
"flex items-center gap-3 py-2 px-2 rounded hover:bg-muted/50",
|
|
isDragging && "bg-muted/50 shadow-md",
|
|
)}
|
|
>
|
|
{/* 드래그 핸들 */}
|
|
<button
|
|
{...attributes}
|
|
{...listeners}
|
|
className="cursor-grab active:cursor-grabbing shrink-0 text-muted-foreground hover:text-foreground"
|
|
>
|
|
<GripVertical className="h-4 w-4" />
|
|
</button>
|
|
|
|
{/* 표시 체크박스 */}
|
|
<Checkbox
|
|
checked={col.visible}
|
|
onCheckedChange={() => onToggleVisible(col._idx)}
|
|
/>
|
|
|
|
{/* 표시 토글 (Switch) */}
|
|
<Switch
|
|
checked={col.visible}
|
|
onCheckedChange={() => onToggleVisible(col._idx)}
|
|
className="shrink-0"
|
|
/>
|
|
|
|
{/* 컬럼명 + 기술명 */}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm font-medium truncate">{col.displayName}</div>
|
|
<div className="text-xs text-muted-foreground truncate">{col.columnName}</div>
|
|
</div>
|
|
|
|
{/* 너비 입력 */}
|
|
<div className="flex items-center gap-1.5 shrink-0">
|
|
<span className="text-xs text-muted-foreground">너비:</span>
|
|
<Input
|
|
type="number"
|
|
value={col.width}
|
|
onChange={(e) => onWidthChange(col._idx, Number(e.target.value) || 100)}
|
|
className="h-8 w-[70px] text-xs text-center"
|
|
min={50}
|
|
max={500}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ===== TableSettingsModal =====
|
|
|
|
export function TableSettingsModal({
|
|
open,
|
|
onOpenChange,
|
|
tableName,
|
|
settingsId,
|
|
onSave,
|
|
initialTab = "columns",
|
|
defaultVisibleKeys,
|
|
includeAutoColumns,
|
|
}: TableSettingsModalProps) {
|
|
const [activeTab, setActiveTab] = useState(initialTab);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
// 임시 설정 (모달 내에서만 수정, 저장 시 반영)
|
|
const [tempColumns, setTempColumns] = useState<ColumnSetting[]>([]);
|
|
const [tempFilters, setTempFilters] = useState<FilterSetting[]>([]);
|
|
const [tempGroups, setTempGroups] = useState<GroupSetting[]>([]);
|
|
const [tempFrozenCount, setTempFrozenCount] = useState(0);
|
|
const [tempGroupSum, setTempGroupSum] = useState(false);
|
|
|
|
// 원본 컬럼 (초기화용)
|
|
const [defaultColumns, setDefaultColumns] = useState<ColumnSetting[]>([]);
|
|
|
|
const sensors = useSensors(
|
|
useSensor(PointerSensor, { activationConstraint: { distance: 5 } })
|
|
);
|
|
|
|
// 모달 열릴 때 데이터 로드
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
setActiveTab(initialTab);
|
|
loadData();
|
|
}, [open]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const loadData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await apiClient.get(`/table-management/tables/${tableName}/web-types`);
|
|
const types: any[] = res.data?.data || [];
|
|
|
|
// 기본 컬럼 설정 생성
|
|
const unsortedColumns: ColumnSetting[] = types
|
|
.filter((t) => !AUTO_COLS.includes(t.columnName) || includeAutoColumns?.includes(t.columnName))
|
|
.map((t) => ({
|
|
columnName: t.columnName,
|
|
displayName: t.displayName || t.columnLabel || t.columnName,
|
|
visible: defaultVisibleKeys ? defaultVisibleKeys.includes(t.columnName) : true,
|
|
width: 120,
|
|
}));
|
|
|
|
// 활성 컬럼을 GRID_COLUMNS 순서대로 위에, 비활성을 아래에 정렬
|
|
const freshColumns = defaultVisibleKeys
|
|
? [
|
|
// 1) defaultVisibleKeys 순서대로 활성 컬럼
|
|
...defaultVisibleKeys
|
|
.map((key) => unsortedColumns.find((c) => c.columnName === key))
|
|
.filter((c): c is ColumnSetting => !!c),
|
|
// 2) 나머지 비활성 컬럼
|
|
...unsortedColumns.filter((c) => !defaultVisibleKeys.includes(c.columnName)),
|
|
]
|
|
: unsortedColumns;
|
|
|
|
// 기본 필터 설정 생성
|
|
const freshFilters: FilterSetting[] = freshColumns.map((c) => {
|
|
const wt = types.find((t) => t.columnName === c.columnName);
|
|
let filterType: "text" | "select" | "date" = "text";
|
|
if (wt?.inputType === "category" || wt?.inputType === "select") filterType = "select";
|
|
else if (wt?.inputType === "date" || wt?.inputType === "datetime") filterType = "date";
|
|
return {
|
|
columnName: c.columnName,
|
|
displayName: c.displayName,
|
|
enabled: false,
|
|
filterType,
|
|
width: 25,
|
|
};
|
|
});
|
|
|
|
// 기본 그룹 설정 생성
|
|
const freshGroups: GroupSetting[] = freshColumns.map((c) => ({
|
|
columnName: c.columnName,
|
|
displayName: c.displayName,
|
|
enabled: false,
|
|
}));
|
|
|
|
setDefaultColumns(freshColumns);
|
|
|
|
// localStorage에서 저장된 설정 복원
|
|
const saved = loadTableSettings(settingsId);
|
|
if (saved) {
|
|
setTempColumns(mergeColumns(freshColumns, saved.columns));
|
|
setTempFilters(freshFilters.map((f) => {
|
|
const s = saved.filters?.find((sf) => sf.columnName === f.columnName);
|
|
return s ? { ...f, enabled: s.enabled, filterType: s.filterType, width: s.width } : f;
|
|
}));
|
|
setTempGroups(freshGroups.map((g) => {
|
|
const s = saved.groups?.find((sg) => sg.columnName === g.columnName);
|
|
return s ? { ...g, enabled: s.enabled } : g;
|
|
}));
|
|
setTempFrozenCount(saved.frozenCount || 0);
|
|
setTempGroupSum(saved.groupSumEnabled || false);
|
|
} else {
|
|
setTempColumns(freshColumns);
|
|
setTempFilters(freshFilters);
|
|
setTempGroups(freshGroups);
|
|
setTempFrozenCount(0);
|
|
setTempGroupSum(false);
|
|
}
|
|
} catch (err) {
|
|
console.error("테이블 설정 로드 실패:", err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// 저장
|
|
const handleSave = () => {
|
|
const settings: TableSettings = {
|
|
columns: tempColumns,
|
|
filters: tempFilters,
|
|
groups: tempGroups,
|
|
frozenCount: tempFrozenCount,
|
|
groupSumEnabled: tempGroupSum,
|
|
};
|
|
localStorage.setItem(getStorageKey(settingsId), JSON.stringify(settings));
|
|
onSave?.(settings);
|
|
onOpenChange(false);
|
|
};
|
|
|
|
// 컬럼 설정 초기화
|
|
const handleResetColumns = () => {
|
|
setTempColumns(defaultColumns.map((c) => ({ ...c })));
|
|
setTempFrozenCount(0);
|
|
};
|
|
|
|
// ===== 컬럼 설정 핸들러 =====
|
|
|
|
const handleDragEnd = (event: DragEndEvent) => {
|
|
const { active, over } = event;
|
|
if (!over || active.id === over.id) return;
|
|
setTempColumns((prev) => {
|
|
const oldIdx = prev.findIndex((c) => c.columnName === active.id);
|
|
const newIdx = prev.findIndex((c) => c.columnName === over.id);
|
|
return arrayMove(prev, oldIdx, newIdx);
|
|
});
|
|
};
|
|
|
|
const toggleColumnVisible = (idx: number) => {
|
|
setTempColumns((prev) => {
|
|
const next = [...prev];
|
|
next[idx] = { ...next[idx], visible: !next[idx].visible };
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const changeColumnWidth = (idx: number, width: number) => {
|
|
setTempColumns((prev) => {
|
|
const next = [...prev];
|
|
next[idx] = { ...next[idx], width };
|
|
return next;
|
|
});
|
|
};
|
|
|
|
// ===== 필터 설정 핸들러 =====
|
|
|
|
const allFiltersEnabled = tempFilters.length > 0 && tempFilters.every((f) => f.enabled);
|
|
|
|
const toggleFilterAll = (checked: boolean) => {
|
|
setTempFilters((prev) => prev.map((f) => ({ ...f, enabled: checked })));
|
|
};
|
|
|
|
const toggleFilter = (idx: number) => {
|
|
setTempFilters((prev) => {
|
|
const next = [...prev];
|
|
next[idx] = { ...next[idx], enabled: !next[idx].enabled };
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const changeFilterType = (idx: number, filterType: "text" | "select" | "date") => {
|
|
setTempFilters((prev) => {
|
|
const next = [...prev];
|
|
next[idx] = { ...next[idx], filterType };
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const changeFilterWidth = (idx: number, width: number) => {
|
|
setTempFilters((prev) => {
|
|
const next = [...prev];
|
|
next[idx] = { ...next[idx], width };
|
|
return next;
|
|
});
|
|
};
|
|
|
|
// ===== 그룹 설정 핸들러 =====
|
|
|
|
const toggleGroup = (idx: number) => {
|
|
setTempGroups((prev) => {
|
|
const next = [...prev];
|
|
next[idx] = { ...next[idx], enabled: !next[idx].enabled };
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const visibleCount = tempColumns.filter((c) => c.visible).length;
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-2xl max-h-[80vh] flex flex-col overflow-hidden">
|
|
<DialogHeader>
|
|
<DialogTitle>테이블 설정</DialogTitle>
|
|
<DialogDescription>테이블의 컬럼, 필터, 그룹화를 설정합니다</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
|
로딩 중...
|
|
</div>
|
|
) : (
|
|
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as typeof activeTab)} className="flex-1 flex flex-col min-h-0">
|
|
<TabsList className="grid w-full grid-cols-3 shrink-0">
|
|
<TabsTrigger value="columns" className="flex items-center gap-1.5">
|
|
<Settings2 className="h-3.5 w-3.5" /> 컬럼 설정
|
|
</TabsTrigger>
|
|
<TabsTrigger value="filters" className="flex items-center gap-1.5">
|
|
<SlidersHorizontal className="h-3.5 w-3.5" /> 필터 설정
|
|
</TabsTrigger>
|
|
<TabsTrigger value="groups" className="flex items-center gap-1.5">
|
|
<Layers className="h-3.5 w-3.5" /> 그룹 설정
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* ===== 탭 1: 컬럼 설정 ===== */}
|
|
<TabsContent value="columns" className="mt-0 pt-3 flex flex-col min-h-0 max-h-[calc(80vh-220px)]">
|
|
{/* 헤더: 표시 수 / 틀고정 / 초기화 */}
|
|
<div className="flex items-center justify-between px-2 pb-3 border-b mb-2 shrink-0">
|
|
<div className="flex items-center gap-3 text-sm">
|
|
<span>
|
|
{visibleCount}/{tempColumns.length}개 컬럼 표시 중
|
|
</span>
|
|
<div className="flex items-center gap-1.5">
|
|
<Lock className="h-3.5 w-3.5 text-muted-foreground" />
|
|
<span className="text-muted-foreground">틀고정:</span>
|
|
<Input
|
|
type="number"
|
|
value={tempFrozenCount}
|
|
onChange={(e) =>
|
|
setTempFrozenCount(
|
|
Math.min(Math.max(0, Number(e.target.value) || 0), tempColumns.length)
|
|
)
|
|
}
|
|
className="h-7 w-[50px] text-xs text-center"
|
|
min={0}
|
|
max={tempColumns.length}
|
|
/>
|
|
<span className="text-muted-foreground text-sm">개 컬럼</span>
|
|
</div>
|
|
</div>
|
|
<Button variant="ghost" size="sm" onClick={handleResetColumns} className="text-xs">
|
|
초기화
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 컬럼 목록 (드래그 순서 변경 가능) */}
|
|
<div className="flex-1 overflow-y-auto min-h-0">
|
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
|
<SortableContext
|
|
items={tempColumns.map((c) => c.columnName)}
|
|
strategy={verticalListSortingStrategy}
|
|
>
|
|
<div className="space-y-0.5">
|
|
{tempColumns.map((col, idx) => (
|
|
<SortableColumnRow
|
|
key={col.columnName}
|
|
col={{ ...col, _idx: idx }}
|
|
onToggleVisible={toggleColumnVisible}
|
|
onWidthChange={changeColumnWidth}
|
|
/>
|
|
))}
|
|
</div>
|
|
</SortableContext>
|
|
</DndContext>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* ===== 탭 2: 필터 설정 ===== */}
|
|
<TabsContent value="filters" className="mt-0 pt-3 overflow-y-auto max-h-[calc(80vh-220px)]">
|
|
{/* 전체 선택 */}
|
|
<div
|
|
className="flex items-center gap-2 px-2 pb-3 border-b mb-2 cursor-pointer"
|
|
onClick={() => toggleFilterAll(!allFiltersEnabled)}
|
|
>
|
|
<Checkbox checked={allFiltersEnabled} />
|
|
<span className="text-sm">전체 선택</span>
|
|
</div>
|
|
|
|
{/* 필터 목록 */}
|
|
<div className="space-y-1">
|
|
{tempFilters.map((filter, idx) => (
|
|
<div
|
|
key={filter.columnName}
|
|
className="flex items-center gap-3 py-1.5 px-2 hover:bg-muted/50 rounded"
|
|
>
|
|
<Checkbox
|
|
checked={filter.enabled}
|
|
onCheckedChange={() => toggleFilter(idx)}
|
|
/>
|
|
<div className="flex-1 text-sm min-w-0 truncate">{filter.displayName}</div>
|
|
<Select
|
|
value={filter.filterType}
|
|
onValueChange={(v) => changeFilterType(idx, v as any)}
|
|
>
|
|
<SelectTrigger className="h-8 w-[90px] text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{FILTER_TYPE_OPTIONS.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<div className="flex items-center gap-1 shrink-0">
|
|
<Input
|
|
type="number"
|
|
value={filter.width}
|
|
onChange={(e) => changeFilterWidth(idx, Number(e.target.value) || 25)}
|
|
className="h-8 w-[55px] text-xs text-center"
|
|
min={10}
|
|
max={100}
|
|
/>
|
|
<span className="text-xs text-muted-foreground">%</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* 그룹별 합산 토글 */}
|
|
<div className="mt-4 flex items-center justify-between rounded-lg border p-3">
|
|
<div>
|
|
<div className="text-sm font-medium">그룹별 합산</div>
|
|
<div className="text-xs text-muted-foreground">같은 값끼리 그룹핑하여 합산</div>
|
|
</div>
|
|
<Switch checked={tempGroupSum} onCheckedChange={setTempGroupSum} />
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* ===== 탭 3: 그룹 설정 ===== */}
|
|
<TabsContent value="groups" className="mt-0 pt-3 overflow-y-auto max-h-[calc(80vh-220px)]">
|
|
<div className="px-2 pb-3 border-b mb-2">
|
|
<span className="text-sm font-medium">사용 가능한 컬럼</span>
|
|
</div>
|
|
|
|
<div className="space-y-0.5">
|
|
{tempGroups.map((group, idx) => (
|
|
<div
|
|
key={group.columnName}
|
|
className={cn(
|
|
"flex items-center gap-3 py-2.5 px-3 rounded cursor-pointer hover:bg-muted/50",
|
|
group.enabled && "bg-primary/5",
|
|
)}
|
|
onClick={() => toggleGroup(idx)}
|
|
>
|
|
<Checkbox checked={group.enabled} />
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm font-medium truncate">{group.displayName}</div>
|
|
<div className="text-xs text-muted-foreground truncate">{group.columnName}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
)}
|
|
|
|
<DialogFooter className="shrink-0">
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
취소
|
|
</Button>
|
|
<Button onClick={handleSave}>저장</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|