ERP-node/frontend/components/GlobalFileViewer.tsx

294 lines
11 KiB
TypeScript
Raw Normal View History

2025-09-29 13:29:03 +09:00
"use client";
import React, { useState, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { GlobalFileManager, GlobalFileInfo } from "@/lib/api/globalFile";
import { downloadFile } from "@/lib/api/file";
import { FileViewerModal } from "@/lib/registry/components/file-upload/FileViewerModal";
import { formatFileSize } from "@/lib/utils";
import { toast } from "sonner";
import { showErrorToast } from "@/lib/utils/toastUtils";
2025-09-29 13:29:03 +09:00
import {
File,
FileText,
Image,
Video,
Music,
Archive,
Download,
Eye,
Search,
Trash2,
Clock,
MapPin,
Monitor,
RefreshCw,
Info,
} from "lucide-react";
interface GlobalFileViewerProps {
showControls?: boolean;
maxHeight?: string;
className?: string;
}
export const GlobalFileViewer: React.FC<GlobalFileViewerProps> = ({
showControls = true,
maxHeight = "600px",
className = "",
}) => {
const [allFiles, setAllFiles] = useState<GlobalFileInfo[]>([]);
const [filteredFiles, setFilteredFiles] = useState<GlobalFileInfo[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [selectedTab, setSelectedTab] = useState("all");
const [viewerFile, setViewerFile] = useState<GlobalFileInfo | null>(null);
const [isViewerOpen, setIsViewerOpen] = useState(false);
const [registryInfo, setRegistryInfo] = useState({
totalFiles: 0,
accessibleFiles: 0,
pages: [] as string[],
screens: [] as number[],
});
// 파일 아이콘 가져오기
const getFileIcon = (fileName: string, size: number = 16) => {
const extension = fileName.split(".").pop()?.toLowerCase() || "";
const iconProps = { size, className: "text-muted-foreground" };
2025-09-29 13:29:03 +09:00
if (["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes(extension)) {
return <Image {...iconProps} className="text-primary" />;
2025-09-29 13:29:03 +09:00
}
if (["mp4", "avi", "mov", "wmv", "flv", "webm"].includes(extension)) {
2025-09-29 13:29:03 +09:00
return <Video {...iconProps} className="text-purple-600" />;
}
if (["mp3", "wav", "flac", "aac", "ogg"].includes(extension)) {
2025-09-29 13:29:03 +09:00
return <Music {...iconProps} className="text-green-600" />;
}
if (["zip", "rar", "7z", "tar", "gz"].includes(extension)) {
2025-09-29 13:29:03 +09:00
return <Archive {...iconProps} className="text-yellow-600" />;
}
if (["txt", "md", "doc", "docx", "pdf", "rtf"].includes(extension)) {
return <FileText {...iconProps} className="text-destructive" />;
2025-09-29 13:29:03 +09:00
}
return <File {...iconProps} />;
};
// 파일 목록 새로고침
const refreshFiles = () => {
const files = GlobalFileManager.getAllAccessibleFiles();
const info = GlobalFileManager.getRegistryInfo();
2025-09-29 13:29:03 +09:00
setAllFiles(files);
setRegistryInfo(info);
2025-09-29 13:29:03 +09:00
// 탭에 따른 필터링
filterFilesByTab(files, selectedTab, searchQuery);
2025-09-29 13:29:03 +09:00
console.log("🔄 전역 파일 목록 새로고침:", files.length + "개");
};
// 탭별 파일 필터링
const filterFilesByTab = (files: GlobalFileInfo[], tab: string, query: string) => {
let filtered = files;
// 탭별 필터링
if (tab === "images") {
filtered = files.filter((file) => {
const ext = file.realFileName?.split(".").pop()?.toLowerCase() || "";
return ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes(ext);
2025-09-29 13:29:03 +09:00
});
} else if (tab === "documents") {
filtered = files.filter((file) => {
const ext = file.realFileName?.split(".").pop()?.toLowerCase() || "";
return ["txt", "md", "doc", "docx", "pdf", "rtf", "hwp", "hwpx"].includes(ext);
2025-09-29 13:29:03 +09:00
});
} else if (tab === "recent") {
filtered = files.sort((a, b) => new Date(b.uploadTime).getTime() - new Date(a.uploadTime).getTime()).slice(0, 20);
2025-09-29 13:29:03 +09:00
}
// 검색 필터링
if (query.trim()) {
const lowerQuery = query.toLowerCase();
filtered = filtered.filter(
(file) =>
file.realFileName?.toLowerCase().includes(lowerQuery) ||
file.savedFileName?.toLowerCase().includes(lowerQuery) ||
file.uploadPage?.toLowerCase().includes(lowerQuery),
2025-09-29 13:29:03 +09:00
);
}
setFilteredFiles(filtered);
};
// 파일 다운로드
const handleDownload = async (file: GlobalFileInfo) => {
try {
await downloadFile({
fileId: file.objid,
originalName: file.realFileName || file.savedFileName || "download",
});
toast.success(`파일 다운로드 시작: ${file.realFileName}`);
} catch (error) {
console.error("파일 다운로드 오류:", error);
showErrorToast("파일 다운로드에 실패했습니다", error, { guidance: "파일이 존재하는지 확인하고 다시 시도해 주세요." });
2025-09-29 13:29:03 +09:00
}
};
// 파일 뷰어 열기
const handleView = (file: GlobalFileInfo) => {
setViewerFile(file);
setIsViewerOpen(true);
};
// 파일 접근 불가능하게 설정 (삭제 대신)
const handleRemove = (file: GlobalFileInfo) => {
GlobalFileManager.setFileAccessible(file.objid, false);
refreshFiles();
toast.success(`파일이 목록에서 제거되었습니다: ${file.realFileName}`);
};
// 초기 로드 및 검색/탭 변경 시 필터링
useEffect(() => {
refreshFiles();
}, []);
useEffect(() => {
filterFilesByTab(allFiles, selectedTab, searchQuery);
}, [allFiles, selectedTab, searchQuery]);
return (
<div className={`w-full ${className}`}>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
2025-09-29 13:29:03 +09:00
<CardTitle className="flex items-center gap-2">
<File className="h-5 w-5" />
2025-09-29 13:29:03 +09:00
</CardTitle>
{showControls && (
<div className="flex items-center gap-2">
<Badge variant="secondary" className="flex items-center gap-1">
<Info className="h-3 w-3" />
2025-09-29 13:29:03 +09:00
{registryInfo.accessibleFiles}
</Badge>
<Button variant="outline" size="sm" onClick={refreshFiles} className="flex items-center gap-1">
<RefreshCw className="h-3 w-3" />
2025-09-29 13:29:03 +09:00
</Button>
</div>
)}
</div>
{showControls && (
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute top-1/2 left-2 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
2025-09-29 13:29:03 +09:00
<Input
placeholder="파일명으로 검색..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-8"
/>
</div>
</div>
)}
</CardHeader>
<CardContent>
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="all"></TabsTrigger>
<TabsTrigger value="recent"></TabsTrigger>
<TabsTrigger value="images"></TabsTrigger>
<TabsTrigger value="documents"></TabsTrigger>
</TabsList>
<TabsContent value={selectedTab} className="mt-4">
<div className="space-y-2 overflow-y-auto" style={{ maxHeight }}>
2025-09-29 13:29:03 +09:00
{filteredFiles.length === 0 ? (
<div className="py-8 text-center text-gray-500">
2025-09-29 13:29:03 +09:00
{searchQuery ? "검색 결과가 없습니다." : "저장된 파일이 없습니다."}
</div>
) : (
filteredFiles.map((file) => (
<Card key={file.objid} className="p-3">
<div className="flex items-center justify-between">
<div className="flex min-w-0 flex-1 items-center gap-3">
2025-09-29 13:29:03 +09:00
{getFileIcon(file.realFileName || file.savedFileName || "", 20)}
<div className="min-w-0 flex-1">
<div className="truncate font-medium">{file.realFileName || file.savedFileName}</div>
<div className="flex items-center gap-2 text-sm text-gray-500">
2025-09-29 13:29:03 +09:00
<span>{formatFileSize(file.fileSize)}</span>
<div className="flex items-center gap-1">
<Clock className="h-3 w-3" />
2025-09-29 13:29:03 +09:00
{new Date(file.uploadTime).toLocaleDateString()}
</div>
<div className="flex items-center gap-1">
<MapPin className="h-3 w-3" />
{file.uploadPage.split("/").pop() || "Unknown"}
2025-09-29 13:29:03 +09:00
</div>
{file.screenId && (
<div className="flex items-center gap-1">
<Monitor className="h-3 w-3" />
2025-09-29 13:29:03 +09:00
Screen {file.screenId}
</div>
)}
</div>
</div>
</div>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleView(file)}
className="flex items-center gap-1"
>
<Eye className="h-3 w-3" />
2025-09-29 13:29:03 +09:00
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDownload(file)}
className="flex items-center gap-1"
>
<Download className="h-3 w-3" />
2025-09-29 13:29:03 +09:00
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleRemove(file)}
className="text-destructive flex items-center gap-1 hover:text-red-700"
2025-09-29 13:29:03 +09:00
>
<Trash2 className="h-3 w-3" />
2025-09-29 13:29:03 +09:00
</Button>
</div>
</div>
</Card>
))
)}
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
{/* 파일 뷰어 모달 */}
{viewerFile && (
<FileViewerModal
file={viewerFile}
isOpen={isViewerOpen}
onClose={() => {
setIsViewerOpen(false);
setViewerFile(null);
}}
/>
)}
</div>
);
};