- Replaced existing toast error messages with the new `showErrorToast` utility across multiple components, improving consistency in error reporting. - Updated error messages to provide more specific guidance for users, enhancing the overall user experience during error scenarios. - Ensured that all relevant error handling in batch management, external call configurations, cascading management, and screen management components now utilizes the new utility for better maintainability.
305 lines
11 KiB
TypeScript
305 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 { showErrorToast } from "@/lib/utils/toastUtils";
|
|
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);
|
|
showErrorToast("파일 다운로드에 실패했습니다", error, { guidance: "파일이 존재하는지 확인하고 다시 시도해 주세요." });
|
|
}
|
|
};
|
|
|
|
// 파일 뷰어 열기
|
|
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>
|
|
);
|
|
};
|