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";
|
2026-03-03 16:04:11 +09:00
|
|
|
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) => {
|
2026-03-10 18:30:18 +09:00
|
|
|
const extension = fileName.split(".").pop()?.toLowerCase() || "";
|
2025-10-02 14:34:15 +09:00
|
|
|
const iconProps = { size, className: "text-muted-foreground" };
|
2025-09-29 13:29:03 +09:00
|
|
|
|
2026-03-10 18:30:18 +09:00
|
|
|
if (["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes(extension)) {
|
2025-10-02 14:34:15 +09:00
|
|
|
return <Image {...iconProps} className="text-primary" />;
|
2025-09-29 13:29:03 +09:00
|
|
|
}
|
2026-03-10 18:30:18 +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" />;
|
|
|
|
|
}
|
2026-03-10 18:30:18 +09:00
|
|
|
if (["mp3", "wav", "flac", "aac", "ogg"].includes(extension)) {
|
2025-09-29 13:29:03 +09:00
|
|
|
return <Music {...iconProps} className="text-green-600" />;
|
|
|
|
|
}
|
2026-03-10 18:30:18 +09:00
|
|
|
if (["zip", "rar", "7z", "tar", "gz"].includes(extension)) {
|
2025-09-29 13:29:03 +09:00
|
|
|
return <Archive {...iconProps} className="text-yellow-600" />;
|
|
|
|
|
}
|
2026-03-10 18:30:18 +09:00
|
|
|
if (["txt", "md", "doc", "docx", "pdf", "rtf"].includes(extension)) {
|
2025-10-02 14:34:15 +09:00
|
|
|
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();
|
2026-03-10 18:30:18 +09:00
|
|
|
|
2025-09-29 13:29:03 +09:00
|
|
|
setAllFiles(files);
|
|
|
|
|
setRegistryInfo(info);
|
2026-03-10 18:30:18 +09:00
|
|
|
|
2025-09-29 13:29:03 +09:00
|
|
|
// 탭에 따른 필터링
|
|
|
|
|
filterFilesByTab(files, selectedTab, searchQuery);
|
2026-03-10 18:30:18 +09:00
|
|
|
|
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") {
|
2026-03-10 18:30:18 +09:00
|
|
|
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") {
|
2026-03-10 18:30:18 +09:00
|
|
|
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") {
|
2026-03-10 18:30:18 +09:00
|
|
|
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();
|
2026-03-10 18:30:18 +09:00
|
|
|
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);
|
2026-03-03 16:04:11 +09:00
|
|
|
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>
|
2026-03-10 18:30:18 +09:00
|
|
|
<div className="flex items-center justify-between">
|
2025-09-29 13:29:03 +09:00
|
|
|
<CardTitle className="flex items-center gap-2">
|
2026-03-10 18:30:18 +09:00
|
|
|
<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">
|
2026-03-10 18:30:18 +09:00
|
|
|
<Info className="h-3 w-3" />
|
2025-09-29 13:29:03 +09:00
|
|
|
{registryInfo.accessibleFiles}개 파일
|
|
|
|
|
</Badge>
|
2026-03-10 18:30:18 +09:00
|
|
|
<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">
|
2026-03-10 18:30:18 +09:00
|
|
|
<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">
|
2026-03-10 18:30:18 +09:00
|
|
|
<div className="space-y-2 overflow-y-auto" style={{ maxHeight }}>
|
2025-09-29 13:29:03 +09:00
|
|
|
{filteredFiles.length === 0 ? (
|
2026-03-10 18:30:18 +09:00
|
|
|
<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">
|
2026-03-10 18:30:18 +09:00
|
|
|
<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)}
|
2026-03-10 18:30:18 +09:00
|
|
|
<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">
|
2026-03-10 18:30:18 +09:00
|
|
|
<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">
|
2026-03-10 18:30:18 +09:00
|
|
|
<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">
|
2026-03-10 18:30:18 +09:00
|
|
|
<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"
|
|
|
|
|
>
|
2026-03-10 18:30:18 +09:00
|
|
|
<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"
|
|
|
|
|
>
|
2026-03-10 18:30:18 +09:00
|
|
|
<Download className="h-3 w-3" />
|
2025-09-29 13:29:03 +09:00
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => handleRemove(file)}
|
2026-03-10 18:30:18 +09:00
|
|
|
className="text-destructive flex items-center gap-1 hover:text-red-700"
|
2025-09-29 13:29:03 +09:00
|
|
|
>
|
2026-03-10 18:30:18 +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>
|
|
|
|
|
);
|
|
|
|
|
};
|