ERP-node/frontend/components/screen/ScreenGroupTreeView.tsx

973 lines
39 KiB
TypeScript
Raw Normal View History

"use client";
import { useState, useEffect } from "react";
import { cn } from "@/lib/utils";
import {
ChevronRight,
ChevronDown,
Monitor,
FolderOpen,
Folder,
Plus,
MoreVertical,
Edit,
Trash2,
FolderInput,
} from "lucide-react";
import { ScreenDefinition } from "@/types/screen";
import {
ScreenGroup,
getScreenGroups,
deleteScreenGroup,
addScreenToGroup,
removeScreenFromGroup,
} from "@/lib/api/screenGroup";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Check, ChevronsUpDown } from "lucide-react";
import { ScreenGroupModal } from "./ScreenGroupModal";
import { toast } from "sonner";
interface ScreenGroupTreeViewProps {
screens: ScreenDefinition[];
selectedScreen: ScreenDefinition | null;
onScreenSelect: (screen: ScreenDefinition) => void;
onScreenDesign: (screen: ScreenDefinition) => void;
onGroupSelect?: (group: { id: number; name: string; company_code?: string } | null) => void;
onScreenSelectInGroup?: (group: { id: number; name: string; company_code?: string }, screenId: number) => void;
companyCode?: string;
}
interface TreeNode {
type: "group" | "screen";
id: string;
name: string;
data?: ScreenDefinition | ScreenGroup;
children?: TreeNode[];
expanded?: boolean;
}
export function ScreenGroupTreeView({
screens,
selectedScreen,
onScreenSelect,
onScreenDesign,
onGroupSelect,
onScreenSelectInGroup,
companyCode,
}: ScreenGroupTreeViewProps) {
const [groups, setGroups] = useState<ScreenGroup[]>([]);
const [loading, setLoading] = useState(true);
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
const [groupScreensMap, setGroupScreensMap] = useState<Map<number, number[]>>(new Map());
// 그룹 모달 상태
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false);
const [editingGroup, setEditingGroup] = useState<ScreenGroup | null>(null);
// 삭제 확인 다이얼로그 상태
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [deletingGroup, setDeletingGroup] = useState<ScreenGroup | null>(null);
// 화면 이동 메뉴 상태
const [movingScreen, setMovingScreen] = useState<ScreenDefinition | null>(null);
const [isMoveMenuOpen, setIsMoveMenuOpen] = useState(false);
const [selectedGroupForMove, setSelectedGroupForMove] = useState<number | null>(null);
const [screenRole, setScreenRole] = useState<string>("");
const [isGroupSelectOpen, setIsGroupSelectOpen] = useState(false);
const [displayOrder, setDisplayOrder] = useState<number>(1);
// 그룹 목록 및 그룹별 화면 로드
useEffect(() => {
loadGroupsData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [companyCode]);
// 그룹에 속한 화면 ID들을 가져오기
const getGroupedScreenIds = (): Set<number> => {
const ids = new Set<number>();
groupScreensMap.forEach((screenIds) => {
screenIds.forEach((id) => ids.add(id));
});
return ids;
};
// 미분류 화면들 (어떤 그룹에도 속하지 않은 화면)
const getUngroupedScreens = (): ScreenDefinition[] => {
const groupedIds = getGroupedScreenIds();
return screens.filter((screen) => !groupedIds.has(screen.screenId));
};
// 그룹에 속한 화면들 (display_order 오름차순 정렬)
const getScreensInGroup = (groupId: number): ScreenDefinition[] => {
const group = groups.find((g) => g.id === groupId);
if (!group?.screens) {
const screenIds = groupScreensMap.get(groupId) || [];
return screens.filter((screen) => screenIds.includes(screen.screenId));
}
// 그룹의 screens 배열에서 display_order 정보를 가져와서 정렬
const sortedScreenIds = [...group.screens]
.sort((a, b) => (a.display_order || 999) - (b.display_order || 999))
.map((s) => s.screen_id);
return sortedScreenIds
.map((id) => screens.find((screen) => screen.screenId === id))
.filter((screen): screen is ScreenDefinition => screen !== undefined);
};
const toggleGroup = (groupId: string) => {
const newExpanded = new Set(expandedGroups);
if (newExpanded.has(groupId)) {
newExpanded.delete(groupId);
// 그룹 접으면 선택 해제
if (onGroupSelect) {
onGroupSelect(null);
}
} else {
newExpanded.add(groupId);
// 그룹 펼치면 해당 그룹 선택
if (onGroupSelect && groupId !== "ungrouped") {
const group = groups.find((g) => String(g.id) === groupId);
if (group) {
onGroupSelect({ id: group.id, name: group.group_name, company_code: group.company_code });
}
}
}
setExpandedGroups(newExpanded);
};
const handleScreenClick = (screen: ScreenDefinition) => {
onScreenSelect(screen);
};
// 그룹 내 화면 클릭 핸들러 (그룹 전체 표시 + 해당 화면 포커스)
const handleScreenClickInGroup = (screen: ScreenDefinition, group: ScreenGroup) => {
if (onScreenSelectInGroup) {
onScreenSelectInGroup(
{ id: group.id, name: group.group_name, company_code: group.company_code },
screen.screenId
);
} else {
// fallback: 기존 동작
onScreenSelect(screen);
}
};
const handleScreenDoubleClick = (screen: ScreenDefinition) => {
onScreenDesign(screen);
};
// 그룹 추가 버튼 클릭
const handleAddGroup = () => {
setEditingGroup(null);
setIsGroupModalOpen(true);
};
// 그룹 수정 버튼 클릭
const handleEditGroup = (group: ScreenGroup, e: React.MouseEvent) => {
e.stopPropagation();
setEditingGroup(group);
setIsGroupModalOpen(true);
};
// 그룹 삭제 버튼 클릭
const handleDeleteGroup = (group: ScreenGroup, e: React.MouseEvent) => {
e.stopPropagation();
setDeletingGroup(group);
setIsDeleteDialogOpen(true);
};
// 그룹 삭제 확인
const confirmDeleteGroup = async () => {
if (!deletingGroup) return;
try {
const response = await deleteScreenGroup(deletingGroup.id);
if (response.success) {
toast.success("그룹이 삭제되었습니다");
loadGroupsData();
} else {
toast.error(response.message || "그룹 삭제에 실패했습니다");
}
} catch (error) {
console.error("그룹 삭제 실패:", error);
toast.error("그룹 삭제에 실패했습니다");
} finally {
setIsDeleteDialogOpen(false);
setDeletingGroup(null);
}
};
// 화면 이동 메뉴 열기
const handleMoveScreen = (screen: ScreenDefinition, e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setMovingScreen(screen);
// 현재 화면이 속한 그룹 정보 찾기
let currentGroupId: number | null = null;
let currentScreenRole: string = "";
let currentDisplayOrder: number = 1;
// 현재 화면이 속한 그룹 찾기
for (const group of groups) {
if (group.screens && Array.isArray(group.screens)) {
const screenInfo = group.screens.find((s: any) => Number(s.screen_id) === Number(screen.screenId));
if (screenInfo) {
currentGroupId = group.id;
currentScreenRole = screenInfo.screen_role || "";
currentDisplayOrder = screenInfo.display_order || 1;
break;
}
}
}
setSelectedGroupForMove(currentGroupId);
setScreenRole(currentScreenRole);
setDisplayOrder(currentDisplayOrder);
setIsMoveMenuOpen(true);
};
// 화면을 특정 그룹으로 이동
const moveScreenToGroup = async (targetGroupId: number | null) => {
if (!movingScreen) return;
try {
// 현재 그룹에서 제거
const currentGroupId = Array.from(groupScreensMap.entries()).find(([_, screenIds]) =>
screenIds.includes(movingScreen.screenId)
)?.[0];
if (currentGroupId) {
// screen_group_screens에서 해당 연결 찾아서 삭제
const currentGroup = groups.find((g) => g.id === currentGroupId);
if (currentGroup && currentGroup.screens) {
const screenGroupScreen = currentGroup.screens.find(
(s: any) => s.screen_id === movingScreen.screenId
);
if (screenGroupScreen) {
await removeScreenFromGroup(screenGroupScreen.id);
}
}
}
// 새 그룹에 추가 (미분류가 아닌 경우)
if (targetGroupId !== null) {
await addScreenToGroup({
group_id: targetGroupId,
screen_id: movingScreen.screenId,
screen_role: screenRole,
display_order: displayOrder,
is_default: "N",
});
}
toast.success("화면이 이동되었습니다");
loadGroupsData();
} catch (error) {
console.error("화면 이동 실패:", error);
toast.error("화면 이동에 실패했습니다");
} finally {
setIsMoveMenuOpen(false);
setMovingScreen(null);
setSelectedGroupForMove(null);
setScreenRole("");
setDisplayOrder(1);
}
};
// 그룹 경로 가져오기 (계층 구조 표시용)
const getGroupPath = (groupId: number): string => {
const group = groups.find((g) => g.id === groupId);
if (!group) return "";
const path: string[] = [group.group_name];
let currentGroup = group;
while (currentGroup.parent_group_id) {
const parent = groups.find((g) => g.id === currentGroup.parent_group_id);
if (parent) {
path.unshift(parent.group_name);
currentGroup = parent;
} else {
break;
}
}
return path.join(" > ");
};
// 그룹 레벨 가져오기 (들여쓰기용)
const getGroupLevel = (groupId: number): number => {
const group = groups.find((g) => g.id === groupId);
return group?.group_level || 1;
};
// 그룹을 계층 구조로 정렬
const getSortedGroups = (): typeof groups => {
const result: typeof groups = [];
const addChildren = (parentId: number | null, level: number) => {
const children = groups
.filter((g) => g.parent_group_id === parentId)
.sort((a, b) => (a.display_order || 0) - (b.display_order || 0));
for (const child of children) {
result.push({ ...child, group_level: level });
addChildren(child.id, level + 1);
}
};
addChildren(null, 1);
return result;
};
// 그룹 데이터 새로고침
const loadGroupsData = async () => {
try {
setLoading(true);
const response = await getScreenGroups({ size: 1000 }); // 모든 그룹 가져오기
if (response.success && response.data) {
setGroups(response.data);
// 각 그룹별 화면 목록 매핑
const screenMap = new Map<number, number[]>();
for (const group of response.data) {
if (group.screens && Array.isArray(group.screens)) {
screenMap.set(
group.id,
group.screens.map((s: any) => s.screen_id)
);
}
}
setGroupScreensMap(screenMap);
}
} catch (error) {
console.error("그룹 목록 로드 실패:", error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center p-8">
<div className="text-sm text-muted-foreground"> ...</div>
</div>
);
}
const ungroupedScreens = getUngroupedScreens();
return (
<div className="h-full flex flex-col overflow-hidden">
{/* 그룹 추가 버튼 */}
<div className="flex-shrink-0 border-b p-2">
<Button
onClick={handleAddGroup}
variant="outline"
size="sm"
className="w-full gap-2"
>
<Plus className="h-4 w-4" />
</Button>
</div>
{/* 트리 목록 */}
<div className="flex-1 overflow-auto p-2">
{/* 그룹화된 화면들 (대분류만 먼저 렌더링) */}
{groups
.filter((g) => !(g as any).parent_group_id) // 대분류만 (parent_group_id가 null)
.map((group) => {
const groupId = String(group.id);
const isExpanded = expandedGroups.has(groupId);
const groupScreens = getScreensInGroup(group.id);
// 하위 그룹들 찾기
const childGroups = groups.filter((g) => (g as any).parent_group_id === group.id);
return (
<div key={groupId} className="mb-1">
{/* 그룹 헤더 */}
<div
className={cn(
"flex items-center gap-2 rounded-md px-2 py-1.5 cursor-pointer hover:bg-accent transition-colors",
"text-sm font-medium group/item"
)}
onClick={() => toggleGroup(groupId)}
>
{isExpanded ? (
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" />
) : (
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />
)}
{isExpanded ? (
<FolderOpen className="h-4 w-4 shrink-0 text-amber-500" />
) : (
<Folder className="h-4 w-4 shrink-0 text-amber-500" />
)}
<span className="truncate flex-1">{group.group_name}</span>
<Badge variant="secondary" className="text-xs">
{groupScreens.length}
</Badge>
{/* 그룹 메뉴 버튼 */}
<DropdownMenu>
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 opacity-0 group-hover/item:opacity-100"
>
<MoreVertical className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={(e) => handleEditGroup(group, e as any)}>
<Edit className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={(e) => handleDeleteGroup(group, e as any)}
className="text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* 그룹 내 하위 그룹들 */}
{isExpanded && childGroups.length > 0 && (
<div className="ml-6 mt-1 space-y-0.5">
{childGroups.map((childGroup) => {
const childGroupId = String(childGroup.id);
const isChildExpanded = expandedGroups.has(childGroupId);
const childScreens = getScreensInGroup(childGroup.id);
// 손자 그룹들 (3단계)
const grandChildGroups = groups.filter((g) => (g as any).parent_group_id === childGroup.id);
return (
<div key={childGroupId}>
{/* 중분류 헤더 */}
<div
className={cn(
"flex items-center gap-2 rounded-md px-2 py-1.5 cursor-pointer hover:bg-accent transition-colors",
"text-xs font-medium group/item"
)}
onClick={() => toggleGroup(childGroupId)}
>
{isChildExpanded ? (
<ChevronDown className="h-3 w-3 shrink-0 text-muted-foreground" />
) : (
<ChevronRight className="h-3 w-3 shrink-0 text-muted-foreground" />
)}
{isChildExpanded ? (
<FolderOpen className="h-3 w-3 shrink-0 text-blue-500" />
) : (
<Folder className="h-3 w-3 shrink-0 text-blue-500" />
)}
<span className="truncate flex-1">{childGroup.group_name}</span>
<Badge variant="secondary" className="text-[10px] h-4">
{childScreens.length}
</Badge>
<DropdownMenu>
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
<Button
variant="ghost"
size="icon"
className="h-5 w-5 opacity-0 group-hover/item:opacity-100"
>
<MoreVertical className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={(e) => handleEditGroup(childGroup, e as any)}>
<Edit className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={(e) => handleDeleteGroup(childGroup, e as any)}
className="text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* 중분류 내 손자 그룹들 (소분류) */}
{isChildExpanded && grandChildGroups.length > 0 && (
<div className="ml-6 mt-1 space-y-0.5">
{grandChildGroups.map((grandChild) => {
const grandChildId = String(grandChild.id);
const isGrandExpanded = expandedGroups.has(grandChildId);
const grandScreens = getScreensInGroup(grandChild.id);
return (
<div key={grandChildId}>
{/* 소분류 헤더 */}
<div
className={cn(
"flex items-center gap-2 rounded-md px-2 py-1.5 cursor-pointer hover:bg-accent transition-colors",
"text-xs group/item"
)}
onClick={() => toggleGroup(grandChildId)}
>
{isGrandExpanded ? (
<ChevronDown className="h-3 w-3 shrink-0 text-muted-foreground" />
) : (
<ChevronRight className="h-3 w-3 shrink-0 text-muted-foreground" />
)}
{isGrandExpanded ? (
<FolderOpen className="h-3 w-3 shrink-0 text-green-500" />
) : (
<Folder className="h-3 w-3 shrink-0 text-green-500" />
)}
<span className="truncate flex-1">{grandChild.group_name}</span>
<Badge variant="outline" className="text-[10px] h-4">
{grandScreens.length}
</Badge>
<DropdownMenu>
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
<Button
variant="ghost"
size="icon"
className="h-5 w-5 opacity-0 group-hover/item:opacity-100"
>
<MoreVertical className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={(e) => handleEditGroup(grandChild, e as any)}>
<Edit className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={(e) => handleDeleteGroup(grandChild, e as any)}
className="text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* 소분류 내 화면들 */}
{isGrandExpanded && (
<div className="ml-6 mt-1 space-y-0.5">
{grandScreens.length === 0 ? (
<div className="pl-6 py-2 text-xs text-muted-foreground">
</div>
) : (
grandScreens.map((screen) => (
<div
key={screen.screenId}
className={cn(
"flex items-center gap-2 rounded-md px-2 py-1.5 cursor-pointer transition-colors",
"text-xs hover:bg-accent",
selectedScreen?.screenId === screen.screenId && "bg-accent"
)}
onClick={() => handleScreenClickInGroup(screen, grandChild)}
onDoubleClick={() => handleScreenDoubleClick(screen)}
onContextMenu={(e) => handleMoveScreen(screen, e)}
>
<Monitor className="h-3 w-3 shrink-0 text-blue-500" />
<span className="truncate flex-1">{screen.screenName}</span>
<span className="text-[10px] text-muted-foreground truncate max-w-[80px]">
{screen.screenCode}
</span>
</div>
))
)}
</div>
)}
</div>
);
})}
</div>
)}
{/* 중분류 내 화면들 */}
{isChildExpanded && (
<div className="ml-6 mt-1 space-y-0.5">
{childScreens.length === 0 && grandChildGroups.length === 0 ? (
<div className="pl-6 py-2 text-xs text-muted-foreground">
</div>
) : (
childScreens.map((screen) => (
<div
key={screen.screenId}
className={cn(
"flex items-center gap-2 rounded-md px-2 py-1.5 cursor-pointer transition-colors",
"text-xs hover:bg-accent",
selectedScreen?.screenId === screen.screenId && "bg-accent"
)}
onClick={() => handleScreenClickInGroup(screen, childGroup)}
onDoubleClick={() => handleScreenDoubleClick(screen)}
onContextMenu={(e) => handleMoveScreen(screen, e)}
>
<Monitor className="h-3 w-3 shrink-0 text-blue-500" />
<span className="truncate flex-1">{screen.screenName}</span>
<span className="text-[10px] text-muted-foreground truncate max-w-[80px]">
{screen.screenCode}
</span>
</div>
))
)}
</div>
)}
</div>
);
})}
</div>
)}
{/* 그룹 내 화면들 (대분류 직속) */}
{isExpanded && (
<div className="ml-4 mt-1 space-y-0.5">
{groupScreens.length === 0 && childGroups.length === 0 ? (
<div className="pl-6 py-2 text-xs text-muted-foreground">
</div>
) : (
groupScreens.map((screen) => (
<div
key={screen.screenId}
className={cn(
"flex items-center gap-2 rounded-md px-2 py-1.5 cursor-pointer transition-colors",
"text-sm hover:bg-accent group/screen",
selectedScreen?.screenId === screen.screenId && "bg-accent"
)}
onClick={() => handleScreenClickInGroup(screen, group)}
onDoubleClick={() => handleScreenDoubleClick(screen)}
onContextMenu={(e) => handleMoveScreen(screen, e)}
>
<Monitor className="h-4 w-4 shrink-0 text-blue-500" />
<span className="truncate flex-1">{screen.screenName}</span>
<span className="text-xs text-muted-foreground truncate max-w-[100px]">
{screen.screenCode}
</span>
</div>
))
)}
</div>
)}
</div>
);
})}
{/* 미분류 화면들 */}
{ungroupedScreens.length > 0 && (
<div className="mb-1">
<div
className={cn(
"flex items-center gap-2 rounded-md px-2 py-1.5 cursor-pointer hover:bg-accent transition-colors",
"text-sm font-medium text-muted-foreground"
)}
onClick={() => toggleGroup("ungrouped")}
>
{expandedGroups.has("ungrouped") ? (
<ChevronDown className="h-4 w-4 shrink-0" />
) : (
<ChevronRight className="h-4 w-4 shrink-0" />
)}
<Folder className="h-4 w-4 shrink-0" />
<span className="truncate flex-1"></span>
<Badge variant="outline" className="text-xs">
{ungroupedScreens.length}
</Badge>
</div>
{expandedGroups.has("ungrouped") && (
<div className="ml-4 mt-1 space-y-0.5">
{ungroupedScreens.map((screen) => (
<div
key={screen.screenId}
className={cn(
"flex items-center gap-2 rounded-md px-2 py-1.5 cursor-pointer transition-colors",
"text-sm hover:bg-accent",
selectedScreen?.screenId === screen.screenId && "bg-accent"
)}
onClick={() => handleScreenClick(screen)}
onDoubleClick={() => handleScreenDoubleClick(screen)}
onContextMenu={(e) => handleMoveScreen(screen, e)}
>
<Monitor className="h-4 w-4 shrink-0 text-blue-500" />
<span className="truncate flex-1">{screen.screenName}</span>
<span className="text-xs text-muted-foreground truncate max-w-[100px]">
{screen.screenCode}
</span>
</div>
))}
</div>
)}
</div>
)}
{groups.length === 0 && ungroupedScreens.length === 0 && (
<div className="flex flex-col items-center justify-center py-8 text-center">
<Monitor className="h-12 w-12 text-muted-foreground/50 mb-2" />
<p className="text-sm text-muted-foreground"> </p>
</div>
)}
</div>
{/* 그룹 추가/수정 모달 */}
<ScreenGroupModal
isOpen={isGroupModalOpen}
onClose={() => {
setIsGroupModalOpen(false);
setEditingGroup(null);
}}
onSuccess={loadGroupsData}
group={editingGroup}
/>
{/* 그룹 삭제 확인 다이얼로그 */}
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<AlertDialogContent className="max-w-[95vw] sm:max-w-[500px]">
<AlertDialogHeader>
<AlertDialogTitle className="text-base sm:text-lg"> </AlertDialogTitle>
<AlertDialogDescription className="text-xs sm:text-sm">
"{deletingGroup?.group_name}" ?
<br />
.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="gap-2 sm:gap-0">
<AlertDialogCancel className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDeleteGroup}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm bg-destructive hover:bg-destructive/90"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* 화면 이동 메뉴 (다이얼로그) */}
<Dialog open={isMoveMenuOpen} onOpenChange={setIsMoveMenuOpen}>
<DialogContent className="max-w-[95vw] sm:max-w-[400px]">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg"> </DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
"{movingScreen?.screenName}"
</DialogDescription>
</DialogHeader>
<div className="space-y-3 sm:space-y-4">
{/* 그룹 선택 (트리 구조 + 검색) */}
<div>
<Label htmlFor="target-group" className="text-xs sm:text-sm">
*
</Label>
<Popover open={isGroupSelectOpen} onOpenChange={setIsGroupSelectOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={isGroupSelectOpen}
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
>
{selectedGroupForMove === null
? "미분류"
: getGroupPath(selectedGroupForMove) || "그룹 선택"}
<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-xs sm:text-sm"
/>
<CommandList>
<CommandEmpty className="text-xs sm:text-sm py-2 text-center">
</CommandEmpty>
<CommandGroup>
{/* 미분류 옵션 */}
<CommandItem
value="none"
onSelect={() => {
setSelectedGroupForMove(null);
setIsGroupSelectOpen(false);
}}
className="text-xs sm:text-sm"
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedGroupForMove === null ? "opacity-100" : "opacity-0"
)}
/>
<Folder className="mr-2 h-4 w-4 text-muted-foreground" />
</CommandItem>
{/* 계층 구조로 그룹 표시 */}
{getSortedGroups().map((group) => (
<CommandItem
key={group.id}
value={`${group.group_name} ${getGroupPath(group.id)}`}
onSelect={() => {
setSelectedGroupForMove(group.id);
setIsGroupSelectOpen(false);
}}
className="text-xs sm:text-sm"
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedGroupForMove === group.id ? "opacity-100" : "opacity-0"
)}
/>
{/* 들여쓰기로 계층 표시 */}
<span
style={{ marginLeft: `${((group.group_level || 1) - 1) * 16}px` }}
className="flex items-center"
>
<Folder className="mr-2 h-4 w-4 text-muted-foreground" />
{group.group_name}
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
. .
</p>
</div>
{/* 화면 역할 입력 (그룹이 선택된 경우만) */}
{selectedGroupForMove !== null && (
<>
<div>
<Label htmlFor="screen-role" className="text-xs sm:text-sm">
()
</Label>
<Input
id="screen-role"
value={screenRole}
onChange={(e) => setScreenRole(e.target.value)}
placeholder="예: 목록, 등록, 조회, 팝업..."
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
</p>
</div>
<div>
<Label htmlFor="display-order" className="text-xs sm:text-sm">
*
</Label>
<Input
id="display-order"
type="number"
value={displayOrder}
onChange={(e) => setDisplayOrder(parseInt(e.target.value) || 1)}
min={1}
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
(1: 메인 2: 등록 3: 팝업)
</p>
</div>
</>
)}
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={() => {
setIsMoveMenuOpen(false);
setMovingScreen(null);
setSelectedGroupForMove(null);
setScreenRole("");
setDisplayOrder(1);
}}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
<Button
onClick={() => moveScreenToGroup(selectedGroupForMove)}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}