"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([]); const [loading, setLoading] = useState(true); const [expandedGroups, setExpandedGroups] = useState>(new Set()); const [groupScreensMap, setGroupScreensMap] = useState>(new Map()); // 그룹 모달 상태 const [isGroupModalOpen, setIsGroupModalOpen] = useState(false); const [editingGroup, setEditingGroup] = useState(null); // 삭제 확인 다이얼로그 상태 const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [deletingGroup, setDeletingGroup] = useState(null); // 화면 이동 메뉴 상태 const [movingScreen, setMovingScreen] = useState(null); const [isMoveMenuOpen, setIsMoveMenuOpen] = useState(false); const [selectedGroupForMove, setSelectedGroupForMove] = useState(null); const [screenRole, setScreenRole] = useState(""); const [isGroupSelectOpen, setIsGroupSelectOpen] = useState(false); const [displayOrder, setDisplayOrder] = useState(1); // 그룹 목록 및 그룹별 화면 로드 useEffect(() => { loadGroupsData(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [companyCode]); // 그룹에 속한 화면 ID들을 가져오기 const getGroupedScreenIds = (): Set => { const ids = new Set(); 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(); 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 (
로딩 중...
); } const ungroupedScreens = getUngroupedScreens(); return (
{/* 그룹 추가 버튼 */}
{/* 트리 목록 */}
{/* 그룹화된 화면들 (대분류만 먼저 렌더링) */} {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 (
{/* 그룹 헤더 */}
toggleGroup(groupId)} > {isExpanded ? ( ) : ( )} {isExpanded ? ( ) : ( )} {group.group_name} {groupScreens.length} {/* 그룹 메뉴 버튼 */} e.stopPropagation()}> handleEditGroup(group, e as any)}> 수정 handleDeleteGroup(group, e as any)} className="text-destructive" > 삭제
{/* 그룹 내 하위 그룹들 */} {isExpanded && childGroups.length > 0 && (
{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 (
{/* 중분류 헤더 */}
toggleGroup(childGroupId)} > {isChildExpanded ? ( ) : ( )} {isChildExpanded ? ( ) : ( )} {childGroup.group_name} {childScreens.length} e.stopPropagation()}> handleEditGroup(childGroup, e as any)}> 수정 handleDeleteGroup(childGroup, e as any)} className="text-destructive" > 삭제
{/* 중분류 내 손자 그룹들 (소분류) */} {isChildExpanded && grandChildGroups.length > 0 && (
{grandChildGroups.map((grandChild) => { const grandChildId = String(grandChild.id); const isGrandExpanded = expandedGroups.has(grandChildId); const grandScreens = getScreensInGroup(grandChild.id); return (
{/* 소분류 헤더 */}
toggleGroup(grandChildId)} > {isGrandExpanded ? ( ) : ( )} {isGrandExpanded ? ( ) : ( )} {grandChild.group_name} {grandScreens.length} e.stopPropagation()}> handleEditGroup(grandChild, e as any)}> 수정 handleDeleteGroup(grandChild, e as any)} className="text-destructive" > 삭제
{/* 소분류 내 화면들 */} {isGrandExpanded && (
{grandScreens.length === 0 ? (
화면이 없습니다
) : ( grandScreens.map((screen) => (
handleScreenClickInGroup(screen, grandChild)} onDoubleClick={() => handleScreenDoubleClick(screen)} onContextMenu={(e) => handleMoveScreen(screen, e)} > {screen.screenName} {screen.screenCode}
)) )}
)}
); })}
)} {/* 중분류 내 화면들 */} {isChildExpanded && (
{childScreens.length === 0 && grandChildGroups.length === 0 ? (
화면이 없습니다
) : ( childScreens.map((screen) => (
handleScreenClickInGroup(screen, childGroup)} onDoubleClick={() => handleScreenDoubleClick(screen)} onContextMenu={(e) => handleMoveScreen(screen, e)} > {screen.screenName} {screen.screenCode}
)) )}
)}
); })}
)} {/* 그룹 내 화면들 (대분류 직속) */} {isExpanded && (
{groupScreens.length === 0 && childGroups.length === 0 ? (
화면이 없습니다
) : ( groupScreens.map((screen) => (
handleScreenClickInGroup(screen, group)} onDoubleClick={() => handleScreenDoubleClick(screen)} onContextMenu={(e) => handleMoveScreen(screen, e)} > {screen.screenName} {screen.screenCode}
)) )}
)}
); })} {/* 미분류 화면들 */} {ungroupedScreens.length > 0 && (
toggleGroup("ungrouped")} > {expandedGroups.has("ungrouped") ? ( ) : ( )} 미분류 {ungroupedScreens.length}
{expandedGroups.has("ungrouped") && (
{ungroupedScreens.map((screen) => (
handleScreenClick(screen)} onDoubleClick={() => handleScreenDoubleClick(screen)} onContextMenu={(e) => handleMoveScreen(screen, e)} > {screen.screenName} {screen.screenCode}
))}
)}
)} {groups.length === 0 && ungroupedScreens.length === 0 && (

등록된 화면이 없습니다

)}
{/* 그룹 추가/수정 모달 */} { setIsGroupModalOpen(false); setEditingGroup(null); }} onSuccess={loadGroupsData} group={editingGroup} /> {/* 그룹 삭제 확인 다이얼로그 */} 그룹 삭제 "{deletingGroup?.group_name}" 그룹을 삭제하시겠습니까?
그룹에 속한 화면들은 미분류로 이동됩니다.
취소 삭제
{/* 화면 이동 메뉴 (다이얼로그) */} 화면 그룹 설정 "{movingScreen?.screenName}"의 그룹과 역할을 설정하세요
{/* 그룹 선택 (트리 구조 + 검색) */}
그룹을 찾을 수 없습니다 {/* 미분류 옵션 */} { setSelectedGroupForMove(null); setIsGroupSelectOpen(false); }} className="text-xs sm:text-sm" > 미분류 {/* 계층 구조로 그룹 표시 */} {getSortedGroups().map((group) => ( { setSelectedGroupForMove(group.id); setIsGroupSelectOpen(false); }} className="text-xs sm:text-sm" > {/* 들여쓰기로 계층 표시 */} {group.group_name} ))}

계층 구조로 표시됩니다. 검색으로 빠르게 찾을 수 있습니다.

{/* 화면 역할 입력 (그룹이 선택된 경우만) */} {selectedGroupForMove !== null && ( <>
setScreenRole(e.target.value)} placeholder="예: 목록, 등록, 조회, 팝업..." className="h-8 text-xs sm:h-10 sm:text-sm" />

화면의 용도를 자유롭게 입력하세요

setDisplayOrder(parseInt(e.target.value) || 1)} min={1} className="h-8 text-xs sm:h-10 sm:text-sm" />

화면 흐름 순서 (1: 메인 그리드 → 2: 등록 폼 → 3: 팝업)

)}
); }