ERP-node/frontend/app/(main)/admin/cascading-management/tabs/HierarchyTab.tsx

848 lines
33 KiB
TypeScript

"use client";
import React, { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import {
Check,
ChevronsUpDown,
Layers,
Plus,
RefreshCw,
Search,
Pencil,
Trash2,
ChevronRight,
ChevronDown,
} from "lucide-react";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { hierarchyApi, HierarchyGroup, HierarchyLevel, HIERARCHY_TYPES } from "@/lib/api/cascadingHierarchy";
import { tableManagementApi } from "@/lib/api/tableManagement";
export default function HierarchyTab() {
// 목록 상태
const [groups, setGroups] = useState<HierarchyGroup[]>([]);
const [tables, setTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
const [loading, setLoading] = useState(true);
const [searchText, setSearchText] = useState("");
// 확장된 그룹 (레벨 표시)
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
const [groupLevels, setGroupLevels] = useState<Record<string, HierarchyLevel[]>>({});
// 모달 상태
const [isModalOpen, setIsModalOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [editingGroup, setEditingGroup] = useState<HierarchyGroup | null>(null);
const [deletingGroupCode, setDeletingGroupCode] = useState<string | null>(null);
// 레벨 모달
const [isLevelModalOpen, setIsLevelModalOpen] = useState(false);
const [editingLevel, setEditingLevel] = useState<HierarchyLevel | null>(null);
const [currentGroupCode, setCurrentGroupCode] = useState<string>("");
const [levelColumns, setLevelColumns] = useState<Array<{ columnName: string; displayName: string }>>([]);
// 폼 데이터
const [formData, setFormData] = useState<Partial<HierarchyGroup>>({
groupName: "",
description: "",
hierarchyType: "MULTI_TABLE",
maxLevels: undefined,
isFixedLevels: "Y",
emptyMessage: "선택해주세요",
noOptionsMessage: "옵션이 없습니다",
loadingMessage: "로딩 중...",
});
// 레벨 폼 데이터
const [levelFormData, setLevelFormData] = useState<Partial<HierarchyLevel>>({
levelOrder: 1,
levelName: "",
levelCode: "",
tableName: "",
valueColumn: "",
labelColumn: "",
parentKeyColumn: "",
orderColumn: "",
orderDirection: "ASC",
placeholder: "",
isRequired: "Y",
isSearchable: "N",
});
// 테이블 Combobox 상태
const [tableComboOpen, setTableComboOpen] = useState(false);
// snake_case를 camelCase로 변환하는 함수
const transformGroup = (g: any): HierarchyGroup => ({
groupId: g.group_id || g.groupId,
groupCode: g.group_code || g.groupCode,
groupName: g.group_name || g.groupName,
description: g.description,
hierarchyType: g.hierarchy_type || g.hierarchyType,
maxLevels: g.max_levels || g.maxLevels,
isFixedLevels: g.is_fixed_levels || g.isFixedLevels,
selfRefTable: g.self_ref_table || g.selfRefTable,
selfRefIdColumn: g.self_ref_id_column || g.selfRefIdColumn,
selfRefParentColumn: g.self_ref_parent_column || g.selfRefParentColumn,
selfRefValueColumn: g.self_ref_value_column || g.selfRefValueColumn,
selfRefLabelColumn: g.self_ref_label_column || g.selfRefLabelColumn,
selfRefLevelColumn: g.self_ref_level_column || g.selfRefLevelColumn,
selfRefOrderColumn: g.self_ref_order_column || g.selfRefOrderColumn,
bomTable: g.bom_table || g.bomTable,
bomParentColumn: g.bom_parent_column || g.bomParentColumn,
bomChildColumn: g.bom_child_column || g.bomChildColumn,
bomItemTable: g.bom_item_table || g.bomItemTable,
bomItemIdColumn: g.bom_item_id_column || g.bomItemIdColumn,
bomItemLabelColumn: g.bom_item_label_column || g.bomItemLabelColumn,
bomQtyColumn: g.bom_qty_column || g.bomQtyColumn,
bomLevelColumn: g.bom_level_column || g.bomLevelColumn,
emptyMessage: g.empty_message || g.emptyMessage,
noOptionsMessage: g.no_options_message || g.noOptionsMessage,
loadingMessage: g.loading_message || g.loadingMessage,
companyCode: g.company_code || g.companyCode,
isActive: g.is_active || g.isActive,
createdBy: g.created_by || g.createdBy,
createdDate: g.created_date || g.createdDate,
updatedBy: g.updated_by || g.updatedBy,
updatedDate: g.updated_date || g.updatedDate,
levelCount: g.level_count || g.levelCount || 0,
levels: g.levels,
});
// 목록 로드
const loadGroups = useCallback(async () => {
setLoading(true);
try {
const response = await hierarchyApi.getGroups();
if (response.success && response.data) {
// snake_case를 camelCase로 변환
const transformedData = response.data.map(transformGroup);
setGroups(transformedData);
}
} catch (error) {
console.error("계층 그룹 목록 로드 실패:", error);
toast.error("목록을 불러오는데 실패했습니다.");
} finally {
setLoading(false);
}
}, []);
// 테이블 목록 로드
const loadTables = useCallback(async () => {
try {
const response = await tableManagementApi.getTableList();
if (response.success && response.data) {
setTables(response.data);
}
} catch (error) {
console.error("테이블 목록 로드 실패:", error);
}
}, []);
useEffect(() => {
loadGroups();
loadTables();
}, [loadGroups, loadTables]);
// 그룹 레벨 로드
const loadGroupLevels = async (groupCode: string) => {
try {
const response = await hierarchyApi.getDetail(groupCode);
if (response.success && response.data?.levels) {
setGroupLevels((prev) => ({
...prev,
[groupCode]: response.data!.levels || [],
}));
}
} catch (error) {
console.error("레벨 로드 실패:", error);
}
};
// 그룹 확장 토글
const toggleGroupExpand = async (groupCode: string) => {
const newExpanded = new Set(expandedGroups);
if (newExpanded.has(groupCode)) {
newExpanded.delete(groupCode);
} else {
newExpanded.add(groupCode);
if (!groupLevels[groupCode]) {
await loadGroupLevels(groupCode);
}
}
setExpandedGroups(newExpanded);
};
// 컬럼 로드 (레벨 폼용)
const loadLevelColumns = async (tableName: string) => {
if (!tableName) {
setLevelColumns([]);
return;
}
try {
const response = await tableManagementApi.getColumnList(tableName);
if (response.success && response.data?.columns) {
setLevelColumns(response.data.columns);
}
} catch (error) {
console.error("컬럼 목록 로드 실패:", error);
}
};
// 필터된 목록
const filteredGroups = groups.filter(
(g) =>
g.groupName?.toLowerCase().includes(searchText.toLowerCase()) ||
g.groupCode?.toLowerCase().includes(searchText.toLowerCase()),
);
// 모달 열기 (생성)
const handleOpenCreate = () => {
setEditingGroup(null);
setFormData({
groupName: "",
description: "",
hierarchyType: "MULTI_TABLE",
maxLevels: undefined,
isFixedLevels: "Y",
emptyMessage: "선택해주세요",
noOptionsMessage: "옵션이 없습니다",
loadingMessage: "로딩 중...",
});
setIsModalOpen(true);
};
// 모달 열기 (수정)
const handleOpenEdit = (group: HierarchyGroup) => {
setEditingGroup(group);
setFormData({
groupCode: group.groupCode,
groupName: group.groupName,
description: group.description || "",
hierarchyType: group.hierarchyType,
maxLevels: group.maxLevels,
isFixedLevels: group.isFixedLevels || "Y",
emptyMessage: group.emptyMessage || "선택해주세요",
noOptionsMessage: group.noOptionsMessage || "옵션이 없습니다",
loadingMessage: group.loadingMessage || "로딩 중...",
});
setIsModalOpen(true);
};
// 삭제 확인
const handleDeleteConfirm = (groupCode: string) => {
setDeletingGroupCode(groupCode);
setIsDeleteDialogOpen(true);
};
// 삭제 실행
const handleDelete = async () => {
if (!deletingGroupCode) return;
try {
const response = await hierarchyApi.deleteGroup(deletingGroupCode);
if (response.success) {
toast.success("계층 그룹이 삭제되었습니다.");
loadGroups();
} else {
toast.error(response.error || "삭제에 실패했습니다.");
}
} catch (error) {
toast.error("삭제 중 오류가 발생했습니다.");
} finally {
setIsDeleteDialogOpen(false);
setDeletingGroupCode(null);
}
};
// 저장
const handleSave = async () => {
if (!formData.groupName || !formData.hierarchyType) {
toast.error("필수 항목을 모두 입력해주세요.");
return;
}
try {
let response;
if (editingGroup) {
response = await hierarchyApi.updateGroup(editingGroup.groupCode!, formData);
} else {
response = await hierarchyApi.createGroup(formData as HierarchyGroup);
}
if (response.success) {
toast.success(editingGroup ? "수정되었습니다." : "생성되었습니다.");
setIsModalOpen(false);
loadGroups();
} else {
toast.error(response.error || "저장에 실패했습니다.");
}
} catch (error) {
toast.error("저장 중 오류가 발생했습니다.");
}
};
// 레벨 모달 열기 (생성)
const handleOpenCreateLevel = (groupCode: string) => {
setCurrentGroupCode(groupCode);
setEditingLevel(null);
const existingLevels = groupLevels[groupCode] || [];
setLevelFormData({
levelOrder: existingLevels.length + 1,
levelName: "",
levelCode: "",
tableName: "",
valueColumn: "",
labelColumn: "",
parentKeyColumn: "",
orderColumn: "",
orderDirection: "ASC",
placeholder: "",
isRequired: "Y",
isSearchable: "N",
});
setLevelColumns([]);
setIsLevelModalOpen(true);
};
// 레벨 모달 열기 (수정)
const handleOpenEditLevel = async (level: HierarchyLevel) => {
setCurrentGroupCode(level.groupCode);
setEditingLevel(level);
setLevelFormData({
levelOrder: level.levelOrder,
levelName: level.levelName,
levelCode: level.levelCode || "",
tableName: level.tableName,
valueColumn: level.valueColumn,
labelColumn: level.labelColumn,
parentKeyColumn: level.parentKeyColumn || "",
orderColumn: level.orderColumn || "",
orderDirection: level.orderDirection || "ASC",
placeholder: level.placeholder || "",
isRequired: level.isRequired || "Y",
isSearchable: level.isSearchable || "N",
});
await loadLevelColumns(level.tableName);
setIsLevelModalOpen(true);
};
// 레벨 저장
const handleSaveLevel = async () => {
if (
!levelFormData.levelName ||
!levelFormData.tableName ||
!levelFormData.valueColumn ||
!levelFormData.labelColumn
) {
toast.error("필수 항목을 모두 입력해주세요.");
return;
}
try {
let response;
if (editingLevel) {
response = await hierarchyApi.updateLevel(editingLevel.levelId!, levelFormData);
} else {
response = await hierarchyApi.addLevel(currentGroupCode, levelFormData);
}
if (response.success) {
toast.success(editingLevel ? "레벨이 수정되었습니다." : "레벨이 추가되었습니다.");
setIsLevelModalOpen(false);
await loadGroupLevels(currentGroupCode);
} else {
toast.error(response.error || "저장에 실패했습니다.");
}
} catch (error) {
toast.error("저장 중 오류가 발생했습니다.");
}
};
// 레벨 삭제
const handleDeleteLevel = async (levelId: number, groupCode: string) => {
if (!confirm("이 레벨을 삭제하시겠습니까?")) return;
try {
const response = await hierarchyApi.deleteLevel(levelId);
if (response.success) {
toast.success("레벨이 삭제되었습니다.");
await loadGroupLevels(groupCode);
} else {
toast.error(response.error || "삭제에 실패했습니다.");
}
} catch (error) {
toast.error("삭제 중 오류가 발생했습니다.");
}
};
// 계층 타입 라벨
const getHierarchyTypeLabel = (type: string) => {
return HIERARCHY_TYPES.find((t) => t.value === type)?.label || type;
};
return (
<div className="space-y-6">
{/* 검색 */}
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-4">
<div className="relative flex-1">
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<Input
placeholder="그룹 코드, 이름으로 검색..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="pl-10"
/>
</div>
<Button variant="outline" onClick={loadGroups}>
<RefreshCw className="mr-2 h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
{/* 목록 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Layers className="h-5 w-5" />
</CardTitle>
<CardDescription>
&gt; &gt; . ( {filteredGroups.length})
</CardDescription>
</div>
<Button onClick={handleOpenCreate}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-8">
<RefreshCw className="h-6 w-6 animate-spin" />
<span className="ml-2"> ...</span>
</div>
) : filteredGroups.length === 0 ? (
<div className="text-muted-foreground space-y-4 py-8 text-center">
<div className="text-sm">{searchText ? "검색 결과가 없습니다." : "등록된 계층 그룹이 없습니다."}</div>
<div className="mx-auto max-w-md space-y-3 text-left">
<div className="rounded-lg border p-4">
<div className="text-foreground mb-2 text-sm font-medium">예시: 지역 </div>
<div className="text-muted-foreground text-xs"> &gt; / &gt; // &gt; //</div>
</div>
<div className="rounded-lg border p-4">
<div className="text-foreground mb-2 text-sm font-medium">예시: 조직 </div>
<div className="text-muted-foreground text-xs"> &gt; &gt; ( )</div>
</div>
</div>
</div>
) : (
<div className="space-y-2">
{filteredGroups.map((group) => (
<div key={group.groupCode} className="rounded-lg border">
<div
className="hover:bg-muted/50 flex cursor-pointer items-center justify-between p-4"
onClick={() => toggleGroupExpand(group.groupCode)}
>
<div className="flex items-center gap-3">
{expandedGroups.has(group.groupCode) ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
<div>
<div className="font-medium">{group.groupName}</div>
<div className="text-muted-foreground text-xs">{group.groupCode}</div>
</div>
</div>
<div className="flex items-center gap-4">
<Badge variant="outline">{getHierarchyTypeLabel(group.hierarchyType)}</Badge>
<Badge variant="secondary">{group.levelCount || 0} </Badge>
<Badge variant={group.isActive === "Y" ? "default" : "secondary"}>
{group.isActive === "Y" ? "활성" : "비활성"}
</Badge>
<div className="flex gap-1" onClick={(e) => e.stopPropagation()}>
<Button variant="ghost" size="icon" onClick={() => handleOpenEdit(group)}>
<Pencil className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" onClick={() => handleDeleteConfirm(group.groupCode)}>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</div>
</div>
</div>
{/* 레벨 목록 */}
{expandedGroups.has(group.groupCode) && (
<div className="bg-muted/20 border-t p-4">
<div className="mb-3 flex items-center justify-between">
<span className="text-sm font-medium"> </span>
<Button size="sm" variant="outline" onClick={() => handleOpenCreateLevel(group.groupCode)}>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
{(groupLevels[group.groupCode] || []).length === 0 ? (
<div className="text-muted-foreground py-4 text-center text-sm"> .</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-16"></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead> </TableHead>
<TableHead> </TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{(groupLevels[group.groupCode] || []).map((level) => (
<TableRow key={level.levelId}>
<TableCell>{level.levelOrder}</TableCell>
<TableCell className="font-medium">{level.levelName}</TableCell>
<TableCell className="font-mono text-xs">{level.tableName}</TableCell>
<TableCell className="font-mono text-xs">{level.valueColumn}</TableCell>
<TableCell className="font-mono text-xs">{level.parentKeyColumn || "-"}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="icon" onClick={() => handleOpenEditLevel(level)}>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeleteLevel(level.levelId!, group.groupCode)}
>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
)}
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* 그룹 생성/수정 모달 */}
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>{editingGroup ? "계층 그룹 수정" : "계층 그룹 생성"}</DialogTitle>
<DialogDescription> .</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label> *</Label>
<Input
value={formData.groupName}
onChange={(e) => setFormData({ ...formData, groupName: e.target.value })}
placeholder="예: 지역 계층"
/>
</div>
<div className="space-y-2">
<Label> *</Label>
<Select
value={formData.hierarchyType}
onValueChange={(v: any) => setFormData({ ...formData, hierarchyType: v })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{HIERARCHY_TYPES.map((t) => (
<SelectItem key={t.value} value={t.value}>
{t.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="계층 구조에 대한 설명"
rows={2}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label> </Label>
<Input
type="number"
value={formData.maxLevels || ""}
onChange={(e) =>
setFormData({ ...formData, maxLevels: e.target.value ? Number(e.target.value) : undefined })
}
placeholder="예: 4"
/>
</div>
<div className="space-y-2">
<Label> </Label>
<Select
value={formData.isFixedLevels}
onValueChange={(v) => setFormData({ ...formData, isFixedLevels: v })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Y"></SelectItem>
<SelectItem value="N"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsModalOpen(false)}>
</Button>
<Button onClick={handleSave}>{editingGroup ? "수정" : "생성"}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 레벨 생성/수정 모달 */}
<Dialog open={isLevelModalOpen} onOpenChange={setIsLevelModalOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>{editingLevel ? "레벨 수정" : "레벨 추가"}</DialogTitle>
<DialogDescription> .</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label> *</Label>
<Input
type="number"
value={levelFormData.levelOrder}
onChange={(e) => setLevelFormData({ ...levelFormData, levelOrder: Number(e.target.value) })}
min={1}
/>
</div>
<div className="space-y-2">
<Label> *</Label>
<Input
value={levelFormData.levelName}
onChange={(e) => setLevelFormData({ ...levelFormData, levelName: e.target.value })}
placeholder="예: 시/도"
/>
</div>
</div>
<div className="space-y-2">
<Label> *</Label>
<Popover open={tableComboOpen} onOpenChange={setTableComboOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={tableComboOpen}
className="h-10 w-full justify-between text-sm"
>
{levelFormData.tableName
? tables.find((t) => t.tableName === levelFormData.tableName)?.displayName ||
levelFormData.tableName
: "테이블 선택"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
<Command>
<CommandInput placeholder="테이블명 또는 라벨로 검색..." className="text-sm" />
<CommandList className="max-h-[300px] overflow-y-auto overscroll-contain">
<CommandEmpty className="text-sm"> .</CommandEmpty>
<CommandGroup>
{tables.map((t) => (
<CommandItem
key={t.tableName}
value={`${t.tableName} ${t.displayName || ""}`}
onSelect={async () => {
setLevelFormData({
...levelFormData,
tableName: t.tableName,
valueColumn: "",
labelColumn: "",
parentKeyColumn: "",
});
await loadLevelColumns(t.tableName);
setTableComboOpen(false);
}}
className="text-sm"
>
<Check
className={cn(
"mr-2 h-4 w-4",
levelFormData.tableName === t.tableName ? "opacity-100" : "opacity-0",
)}
/>
<div className="flex flex-col">
<span className="font-medium">{t.displayName || t.tableName}</span>
{t.displayName && t.displayName !== t.tableName && (
<span className="text-muted-foreground text-xs">{t.tableName}</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label> *</Label>
<Select
value={levelFormData.valueColumn}
onValueChange={(v) => setLevelFormData({ ...levelFormData, valueColumn: v })}
disabled={!levelFormData.tableName}
>
<SelectTrigger>
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{levelColumns.map((c) => (
<SelectItem key={c.columnName} value={c.columnName}>
{c.displayName || c.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label> *</Label>
<Select
value={levelFormData.labelColumn}
onValueChange={(v) => setLevelFormData({ ...levelFormData, labelColumn: v })}
disabled={!levelFormData.tableName}
>
<SelectTrigger>
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{levelColumns.map((c) => (
<SelectItem key={c.columnName} value={c.columnName}>
{c.displayName || c.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label> ( 2 )</Label>
<Select
value={levelFormData.parentKeyColumn || "__none__"}
onValueChange={(v) =>
setLevelFormData({ ...levelFormData, parentKeyColumn: v === "__none__" ? "" : v })
}
disabled={!levelFormData.tableName}
>
<SelectTrigger>
<SelectValue placeholder="선택 (선택사항)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__"></SelectItem>
{levelColumns.map((c) => (
<SelectItem key={c.columnName} value={c.columnName}>
{c.displayName || c.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-muted-foreground text-xs"> .</p>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value={levelFormData.placeholder}
onChange={(e) => setLevelFormData({ ...levelFormData, placeholder: e.target.value })}
placeholder={`${levelFormData.levelName || "레벨"} 선택`}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsLevelModalOpen(false)}>
</Button>
<Button onClick={handleSaveLevel}>{editingLevel ? "수정" : "추가"}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 삭제 확인 다이얼로그 */}
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription>
? .
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={handleDelete} className="bg-red-500 hover:bg-red-600">
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}