"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Checkbox } from "@/components/ui/checkbox"; import { useAuth } from "@/hooks/useAuth"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Palette, RotateCcw, Trash } from "lucide-react"; import { ScreenDefinition } from "@/types/screen"; import { screenApi } from "@/lib/api/screen"; import CreateScreenModal from "./CreateScreenModal"; import CopyScreenModal from "./CopyScreenModal"; import dynamic from "next/dynamic"; import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; import { DynamicWebTypeRenderer } from "@/lib/registry"; import { isFileComponent, getComponentWebType } from "@/lib/utils/componentTypeUtils"; // InteractiveScreenViewer를 동적으로 import (SSR 비활성화) const InteractiveScreenViewer = dynamic( () => import("./InteractiveScreenViewer").then((mod) => mod.InteractiveScreenViewer), { ssr: false, loading: () =>
로딩 중...
, }, ); interface ScreenListProps { onScreenSelect: (screen: ScreenDefinition) => void; selectedScreen: ScreenDefinition | null; onDesignScreen: (screen: ScreenDefinition) => void; } type DeletedScreenDefinition = ScreenDefinition & { deletedDate?: Date; deletedBy?: string; deleteReason?: string; }; export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScreen }: ScreenListProps) { const { user } = useAuth(); const isSuperAdmin = user?.userType === "SUPER_ADMIN" || user?.companyCode === "*"; const [activeTab, setActiveTab] = useState("active"); const [screens, setScreens] = useState([]); const [deletedScreens, setDeletedScreens] = useState([]); const [loading, setLoading] = useState(true); // 초기 로딩 const [isSearching, setIsSearching] = useState(false); // 검색 중 로딩 (포커스 유지) const [searchTerm, setSearchTerm] = useState(""); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); const [selectedCompanyCode, setSelectedCompanyCode] = useState("all"); const [companies, setCompanies] = useState([]); const [loadingCompanies, setLoadingCompanies] = useState(false); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [isCreateOpen, setIsCreateOpen] = useState(false); const [isCopyOpen, setIsCopyOpen] = useState(false); const [screenToCopy, setScreenToCopy] = useState(null); // 검색어 디바운스를 위한 타이머 ref const debounceTimer = useRef(null); // 첫 로딩 여부를 추적 (한 번만 true) const isFirstLoad = useRef(true); // 삭제 관련 상태 const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [screenToDelete, setScreenToDelete] = useState(null); const [deleteReason, setDeleteReason] = useState(""); const [dependencies, setDependencies] = useState< Array<{ screenId: number; screenName: string; screenCode: string; componentId: string; componentType: string; referenceType: string; }> >([]); const [showDependencyWarning, setShowDependencyWarning] = useState(false); const [checkingDependencies, setCheckingDependencies] = useState(false); // 영구 삭제 관련 상태 const [permanentDeleteDialogOpen, setPermanentDeleteDialogOpen] = useState(false); const [screenToPermanentDelete, setScreenToPermanentDelete] = useState(null); // 일괄삭제 관련 상태 const [selectedScreenIds, setSelectedScreenIds] = useState([]); const [bulkDeleteDialogOpen, setBulkDeleteDialogOpen] = useState(false); const [bulkDeleting, setBulkDeleting] = useState(false); // 편집 관련 상태 const [editDialogOpen, setEditDialogOpen] = useState(false); const [screenToEdit, setScreenToEdit] = useState(null); const [editFormData, setEditFormData] = useState({ screenName: "", description: "", isActive: "Y", tableName: "", }); const [tables, setTables] = useState([]); const [loadingTables, setLoadingTables] = useState(false); // 미리보기 관련 상태 const [previewDialogOpen, setPreviewDialogOpen] = useState(false); const [screenToPreview, setScreenToPreview] = useState(null); const [previewLayout, setPreviewLayout] = useState(null); const [isLoadingPreview, setIsLoadingPreview] = useState(false); const [previewFormData, setPreviewFormData] = useState>({}); // 최고 관리자인 경우 회사 목록 로드 useEffect(() => { if (isSuperAdmin) { loadCompanies(); } }, [isSuperAdmin]); const loadCompanies = async () => { try { setLoadingCompanies(true); const { apiClient } = await import("@/lib/api/client"); // named export const response = await apiClient.get("/admin/companies"); const data = response.data.data || response.data || []; setCompanies(data.map((c: any) => ({ companyCode: c.company_code || c.companyCode, companyName: c.company_name || c.companyName, }))); } catch (error) { console.error("회사 목록 조회 실패:", error); } finally { setLoadingCompanies(false); } }; // 검색어 디바운스 처리 (150ms 지연 - 빠른 응답) useEffect(() => { // 이전 타이머 취소 if (debounceTimer.current) { clearTimeout(debounceTimer.current); } // 새 타이머 설정 debounceTimer.current = setTimeout(() => { setDebouncedSearchTerm(searchTerm); }, 150); // 클린업 return () => { if (debounceTimer.current) { clearTimeout(debounceTimer.current); } }; }, [searchTerm]); // 화면 목록 로드 (실제 API) - debouncedSearchTerm 사용 useEffect(() => { let abort = false; const load = async () => { try { // 첫 로딩인 경우에만 loading=true, 그 외에는 isSearching=true if (isFirstLoad.current) { setLoading(true); isFirstLoad.current = false; // 첫 로딩 완료 표시 } else { setIsSearching(true); } if (activeTab === "active") { const params: any = { page: currentPage, size: 20, searchTerm: debouncedSearchTerm }; // 최고 관리자이고 특정 회사를 선택한 경우 if (isSuperAdmin && selectedCompanyCode !== "all") { params.companyCode = selectedCompanyCode; } console.log("🔍 화면 목록 API 호출:", params); // 디버깅용 const resp = await screenApi.getScreens(params); console.log("✅ 화면 목록 응답:", resp); // 디버깅용 if (abort) return; setScreens(resp.data || []); setTotalPages(Math.max(1, Math.ceil((resp.total || 0) / 20))); } else if (activeTab === "trash") { const resp = await screenApi.getDeletedScreens({ page: currentPage, size: 20 }); if (abort) return; setDeletedScreens(resp.data || []); setTotalPages(Math.max(1, Math.ceil((resp.total || 0) / 20))); } } catch (e) { console.error("화면 목록 조회 실패", e); if (activeTab === "active") { setScreens([]); } else { setDeletedScreens([]); } setTotalPages(1); } finally { if (!abort) { setLoading(false); setIsSearching(false); } } }; load(); return () => { abort = true; }; }, [currentPage, debouncedSearchTerm, activeTab, selectedCompanyCode, isSuperAdmin]); const filteredScreens = screens; // 서버 필터 기준 사용 // 화면 목록 다시 로드 const reloadScreens = async () => { try { setIsSearching(true); const params: any = { page: currentPage, size: 20, searchTerm: debouncedSearchTerm }; // 최고 관리자이고 특정 회사를 선택한 경우 if (isSuperAdmin && selectedCompanyCode !== "all") { params.companyCode = selectedCompanyCode; } const resp = await screenApi.getScreens(params); setScreens(resp.data || []); setTotalPages(Math.max(1, Math.ceil((resp.total || 0) / 20))); } catch (e) { console.error("화면 목록 조회 실패", e); } finally { setIsSearching(false); } }; const handleScreenSelect = (screen: ScreenDefinition) => { onScreenSelect(screen); }; const handleEdit = async (screen: ScreenDefinition) => { setScreenToEdit(screen); setEditFormData({ screenName: screen.screenName, description: screen.description || "", isActive: screen.isActive, tableName: screen.tableName || "", }); setEditDialogOpen(true); // 테이블 목록 로드 try { setLoadingTables(true); const { tableManagementApi } = await import("@/lib/api/tableManagement"); const response = await tableManagementApi.getTableList(); if (response.success && response.data) { // tableName만 추출 (camelCase) const tableNames = response.data.map((table: any) => table.tableName); setTables(tableNames); } } catch (error) { console.error("테이블 목록 조회 실패:", error); } finally { setLoadingTables(false); } }; const handleEditSave = async () => { if (!screenToEdit) return; try { // 화면 정보 업데이트 API 호출 await screenApi.updateScreenInfo(screenToEdit.screenId, editFormData); // 목록에서 해당 화면 정보 업데이트 setScreens((prev) => prev.map((s) => s.screenId === screenToEdit.screenId ? { ...s, screenName: editFormData.screenName, description: editFormData.description, isActive: editFormData.isActive, } : s, ), ); setEditDialogOpen(false); setScreenToEdit(null); } catch (error) { console.error("화면 정보 업데이트 실패:", error); alert("화면 정보 업데이트에 실패했습니다."); } }; const handleDelete = async (screen: ScreenDefinition) => { setScreenToDelete(screen); setCheckingDependencies(true); try { // 의존성 체크 const dependencyResult = await screenApi.checkScreenDependencies(screen.screenId); if (dependencyResult.hasDependencies) { setDependencies(dependencyResult.dependencies); setShowDependencyWarning(true); } else { setDeleteDialogOpen(true); } } catch (error) { // console.error("의존성 체크 실패:", error); // 의존성 체크 실패 시에도 삭제 다이얼로그는 열어줌 setDeleteDialogOpen(true); } finally { setCheckingDependencies(false); } }; const confirmDelete = async (force: boolean = false) => { if (!screenToDelete) return; try { await screenApi.deleteScreen(screenToDelete.screenId, deleteReason, force); setScreens((prev) => prev.filter((s) => s.screenId !== screenToDelete.screenId)); setDeleteDialogOpen(false); setShowDependencyWarning(false); setScreenToDelete(null); setDeleteReason(""); setDependencies([]); } catch (error: any) { // console.error("화면 삭제 실패:", error); // 의존성 오류인 경우 경고창 표시 if (error.response?.status === 409 && error.response?.data?.code === "SCREEN_HAS_DEPENDENCIES") { setDependencies(error.response.data.dependencies || []); setShowDependencyWarning(true); setDeleteDialogOpen(false); } else { alert("화면 삭제에 실패했습니다."); } } }; const handleCancelDelete = () => { setDeleteDialogOpen(false); setShowDependencyWarning(false); setScreenToDelete(null); setDeleteReason(""); setDependencies([]); }; const handleRestore = async (screen: DeletedScreenDefinition) => { if (!confirm(`"${screen.screenName}" 화면을 복원하시겠습니까?`)) return; try { await screenApi.restoreScreen(screen.screenId); setDeletedScreens((prev) => prev.filter((s) => s.screenId !== screen.screenId)); // 활성 탭으로 이동하여 복원된 화면 확인 setActiveTab("active"); reloadScreens(); } catch (error) { // console.error("화면 복원 실패:", error); alert("화면 복원에 실패했습니다."); } }; const handlePermanentDelete = (screen: DeletedScreenDefinition) => { setScreenToPermanentDelete(screen); setPermanentDeleteDialogOpen(true); }; const confirmPermanentDelete = async () => { if (!screenToPermanentDelete) return; try { await screenApi.permanentDeleteScreen(screenToPermanentDelete.screenId); setDeletedScreens((prev) => prev.filter((s) => s.screenId !== screenToPermanentDelete.screenId)); setPermanentDeleteDialogOpen(false); setScreenToPermanentDelete(null); } catch (error) { // console.error("화면 영구 삭제 실패:", error); alert("화면 영구 삭제에 실패했습니다."); } }; // 체크박스 선택 처리 const handleScreenCheck = (screenId: number, checked: boolean) => { if (checked) { setSelectedScreenIds((prev) => [...prev, screenId]); } else { setSelectedScreenIds((prev) => prev.filter((id) => id !== screenId)); } }; // 전체 선택/해제 const handleSelectAll = (checked: boolean) => { if (checked) { setSelectedScreenIds(deletedScreens.map((screen) => screen.screenId)); } else { setSelectedScreenIds([]); } }; // 일괄삭제 실행 const handleBulkDelete = () => { if (selectedScreenIds.length === 0) { alert("삭제할 화면을 선택해주세요."); return; } setBulkDeleteDialogOpen(true); }; const confirmBulkDelete = async () => { if (selectedScreenIds.length === 0) return; try { setBulkDeleting(true); const result = await screenApi.bulkPermanentDeleteScreens(selectedScreenIds); // 삭제된 화면들을 목록에서 제거 setDeletedScreens((prev) => prev.filter((screen) => !selectedScreenIds.includes(screen.screenId))); setSelectedScreenIds([]); setBulkDeleteDialogOpen(false); // 결과 메시지 표시 let message = `${result.deletedCount}개 화면이 영구 삭제되었습니다.`; if (result.skippedCount > 0) { message += `\n${result.skippedCount}개 화면은 삭제되지 않았습니다.`; } if (result.errors.length > 0) { message += `\n오류 발생: ${result.errors.map((e) => `화면 ${e.screenId}: ${e.error}`).join(", ")}`; } alert(message); } catch (error) { // console.error("일괄 삭제 실패:", error); alert("일괄 삭제에 실패했습니다."); } finally { setBulkDeleting(false); } }; const handleCopy = (screen: ScreenDefinition) => { setScreenToCopy(screen); setIsCopyOpen(true); }; const handleView = async (screen: ScreenDefinition) => { setScreenToPreview(screen); setPreviewLayout(null); // 이전 레이아웃 초기화 setIsLoadingPreview(true); setPreviewDialogOpen(true); // 모달 먼저 열기 // 모달이 열린 후에 레이아웃 로드 setTimeout(async () => { try { // 화면 레이아웃 로드 const layoutData = await screenApi.getLayout(screen.screenId); console.log("📊 미리보기 레이아웃 로드:", layoutData); setPreviewLayout(layoutData); } catch (error) { console.error("❌ 레이아웃 로드 실패:", error); toast.error("화면 레이아웃을 불러오는데 실패했습니다."); } finally { setIsLoadingPreview(false); } }, 100); // 100ms 딜레이로 모달 애니메이션이 먼저 시작되도록 }; const handleCopySuccess = () => { // 복사 성공 후 화면 목록 다시 로드 reloadScreens(); }; if (loading) { return (
로딩 중...
); } return (
{/* 검색 및 필터 */}
{/* 최고 관리자 전용: 회사 필터 */} {isSuperAdmin && (
)} {/* 검색 입력 */}
setSearchTerm(e.target.value)} className="h-10 pl-10 text-sm" disabled={activeTab === "trash"} /> {/* 검색 중 인디케이터 */} {isSearching && (
)}
{/* 탭 구조 */} 활성 화면 휴지통 {/* 활성 화면 탭 */} {/* 데스크톱 테이블 뷰 (lg 이상) */}
화면명 테이블명 상태 생성일 작업 {screens.map((screen) => ( onDesignScreen(screen)} >
{screen.screenName}
{screen.description && (
{screen.description}
)}
{screen.tableLabel || screen.tableName} {screen.isActive === "Y" ? "활성" : "비활성"}
{screen.createdDate.toLocaleDateString()}
{screen.createdBy}
{ e.stopPropagation(); onDesignScreen(screen); }} > 화면 설계 { e.stopPropagation(); handleView(screen); }} > 미리보기 { e.stopPropagation(); handleEdit(screen); }} > 편집 { e.stopPropagation(); handleCopy(screen); }} > 복사 { e.stopPropagation(); handleDelete(screen); }} className="text-destructive" disabled={checkingDependencies && screenToDelete?.screenId === screen.screenId} > {checkingDependencies && screenToDelete?.screenId === screen.screenId ? "확인 중..." : "삭제"}
))}
{filteredScreens.length === 0 && (

검색 결과가 없습니다.

)}
{/* 모바일/태블릿 카드 뷰 (lg 미만) */}
{screens.map((screen) => (
handleScreenSelect(screen)} > {/* 헤더 */}

{screen.screenName}

{screen.isActive === "Y" ? "활성" : "비활성"}
{/* 설명 */} {screen.description &&

{screen.description}

} {/* 정보 */}
테이블 {screen.tableLabel || screen.tableName}
생성일 {screen.createdDate.toLocaleDateString()}
작성자 {screen.createdBy}
{/* 액션 */}
e.stopPropagation()}> { e.stopPropagation(); handleEdit(screen); }} > 편집 { e.stopPropagation(); handleCopy(screen); }} > 복사 { e.stopPropagation(); handleDelete(screen); }} className="text-destructive" disabled={checkingDependencies && screenToDelete?.screenId === screen.screenId} > {checkingDependencies && screenToDelete?.screenId === screen.screenId ? "확인 중..." : "삭제"}
))} {filteredScreens.length === 0 && (

검색 결과가 없습니다.

)}
{/* 휴지통 탭 */} {/* 데스크톱 테이블 뷰 (lg 이상) */}
0 && selectedScreenIds.length === deletedScreens.length} onCheckedChange={handleSelectAll} aria-label="전체 선택" /> 화면명 테이블명 삭제일 삭제자 삭제 사유 작업 {deletedScreens.map((screen) => ( handleScreenCheck(screen.screenId, checked as boolean)} aria-label={`${screen.screenName} 선택`} />
{screen.screenName}
{screen.description && (
{screen.description}
)}
{screen.tableLabel || screen.tableName}
{screen.deletedDate?.toLocaleDateString()}
{screen.deletedBy}
{screen.deleteReason || "-"}
))}
{deletedScreens.length === 0 && (

휴지통이 비어있습니다.

)}
{/* 모바일/태블릿 카드 뷰 (lg 미만) */}
{/* 헤더 */}
0 && selectedScreenIds.length === deletedScreens.length} onCheckedChange={handleSelectAll} aria-label="전체 선택" />
{selectedScreenIds.length > 0 && ( )}
{/* 카드 목록 */}
{deletedScreens.map((screen) => (
{/* 헤더 */}
handleScreenCheck(screen.screenId, checked as boolean)} className="mt-1" aria-label={`${screen.screenName} 선택`} />

{screen.screenName}

{/* 설명 */} {screen.description &&

{screen.description}

} {/* 정보 */}
테이블 {screen.tableLabel || screen.tableName}
삭제일 {screen.deletedDate?.toLocaleDateString()}
삭제자 {screen.deletedBy}
{screen.deleteReason && (
삭제 사유 {screen.deleteReason}
)}
{/* 액션 */}
))}
{deletedScreens.length === 0 && (

휴지통이 비어있습니다.

)}
{/* 페이지네이션 */} {totalPages > 1 && (
{currentPage} / {totalPages}
)} {/* 새 화면 생성 모달 */} { // 목록에 즉시 반영 (첫 페이지 기준 상단 추가) setScreens((prev) => [created, ...prev]); }} /> {/* 화면 복사 모달 */} setIsCopyOpen(false)} sourceScreen={screenToCopy} onCopySuccess={handleCopySuccess} /> {/* 삭제 확인 다이얼로그 */} 화면 삭제 확인 "{screenToDelete?.screenName}" 화면을 휴지통으로 이동하시겠습니까?
휴지통에서 언제든지 복원할 수 있습니다.