304 lines
11 KiB
TypeScript
304 lines
11 KiB
TypeScript
"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 {
|
|
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" };
|
|
|
|
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(extension)) {
|
|
return <Image {...iconProps} className="text-primary" />;
|
|
}
|
|
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm'].includes(extension)) {
|
|
return <Video {...iconProps} className="text-purple-600" />;
|
|
}
|
|
if (['mp3', 'wav', 'flac', 'aac', 'ogg'].includes(extension)) {
|
|
return <Music {...iconProps} className="text-green-600" />;
|
|
}
|
|
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension)) {
|
|
return <Archive {...iconProps} className="text-yellow-600" />;
|
|
}
|
|
if (['txt', 'md', 'doc', 'docx', 'pdf', 'rtf'].includes(extension)) {
|
|
return <FileText {...iconProps} className="text-destructive" />;
|
|
}
|
|
return <File {...iconProps} />;
|
|
};
|
|
|
|
// 파일 목록 새로고침
|
|
const refreshFiles = () => {
|
|
const files = GlobalFileManager.getAllAccessibleFiles();
|
|
const info = GlobalFileManager.getRegistryInfo();
|
|
|
|
setAllFiles(files);
|
|
setRegistryInfo(info);
|
|
|
|
// 탭에 따른 필터링
|
|
filterFilesByTab(files, selectedTab, searchQuery);
|
|
|
|
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);
|
|
});
|
|
} 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);
|
|
});
|
|
} else if (tab === "recent") {
|
|
filtered = files
|
|
.sort((a, b) => new Date(b.uploadTime).getTime() - new Date(a.uploadTime).getTime())
|
|
.slice(0, 20);
|
|
}
|
|
|
|
// 검색 필터링
|
|
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)
|
|
);
|
|
}
|
|
|
|
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);
|
|
toast.error("파일 다운로드에 실패했습니다.");
|
|
}
|
|
};
|
|
|
|
// 파일 뷰어 열기
|
|
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 justify-between items-center">
|
|
<CardTitle className="flex items-center gap-2">
|
|
<File className="w-5 h-5" />
|
|
전역 파일 저장소
|
|
</CardTitle>
|
|
{showControls && (
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="secondary" className="flex items-center gap-1">
|
|
<Info className="w-3 h-3" />
|
|
{registryInfo.accessibleFiles}개 파일
|
|
</Badge>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={refreshFiles}
|
|
className="flex items-center gap-1"
|
|
>
|
|
<RefreshCw className="w-3 h-3" />
|
|
새로고침
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{showControls && (
|
|
<div className="flex items-center gap-2">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
|
<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 }}
|
|
>
|
|
{filteredFiles.length === 0 ? (
|
|
<div className="text-center py-8 text-gray-500">
|
|
{searchQuery ? "검색 결과가 없습니다." : "저장된 파일이 없습니다."}
|
|
</div>
|
|
) : (
|
|
filteredFiles.map((file) => (
|
|
<Card key={file.objid} className="p-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
{getFileIcon(file.realFileName || file.savedFileName || "", 20)}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="font-medium truncate">
|
|
{file.realFileName || file.savedFileName}
|
|
</div>
|
|
<div className="text-sm text-gray-500 flex items-center gap-2">
|
|
<span>{formatFileSize(file.fileSize)}</span>
|
|
<div className="flex items-center gap-1">
|
|
<Clock className="w-3 h-3" />
|
|
{new Date(file.uploadTime).toLocaleDateString()}
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<MapPin className="w-3 h-3" />
|
|
{file.uploadPage.split('/').pop() || 'Unknown'}
|
|
</div>
|
|
{file.screenId && (
|
|
<div className="flex items-center gap-1">
|
|
<Monitor className="w-3 h-3" />
|
|
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="w-3 h-3" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleDownload(file)}
|
|
className="flex items-center gap-1"
|
|
>
|
|
<Download className="w-3 h-3" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleRemove(file)}
|
|
className="flex items-center gap-1 text-destructive hover:text-red-700"
|
|
>
|
|
<Trash2 className="w-3 h-3" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
))
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 파일 뷰어 모달 */}
|
|
{viewerFile && (
|
|
<FileViewerModal
|
|
file={viewerFile}
|
|
isOpen={isViewerOpen}
|
|
onClose={() => {
|
|
setIsViewerOpen(false);
|
|
setViewerFile(null);
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|