diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-29 13:31:40 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-29 13:31:40 +0000 |
| commit | 4614210aa9878922cfa1e424ce677ef893a1b6b2 (patch) | |
| tree | 5e7edcce05fbee207230af0a43ed08cd351d7c4f /components/file-manager/FileManager.tsx | |
| parent | e41e3af4e72870d44a94b03e0f3246d6ccaaca48 (diff) | |
(대표님) 구매 권한설정, data room 등
Diffstat (limited to 'components/file-manager/FileManager.tsx')
| -rw-r--r-- | components/file-manager/FileManager.tsx | 1215 |
1 files changed, 779 insertions, 436 deletions
diff --git a/components/file-manager/FileManager.tsx b/components/file-manager/FileManager.tsx index 483ef773..587beb22 100644 --- a/components/file-manager/FileManager.tsx +++ b/components/file-manager/FileManager.tsx @@ -1,12 +1,12 @@ 'use client'; import React, { useState, useEffect, useCallback } from 'react'; -import { - Folder, - File, - FolderPlus, - Upload, - Trash2, +import { + Folder, + File, + FolderPlus, + Upload, + Trash2, Edit2, Download, Share2, @@ -93,6 +93,8 @@ import { } from "@/components/ui/file-list"; import { decryptWithServerAction } from '@/components/drm/drmUtils'; import { Progress } from '@/components/ui/progress'; +// Import the secure viewer component +import { SecurePDFViewer } from './SecurePDFViewer'; interface FileItem { id: string; @@ -126,12 +128,12 @@ interface FileManagerProps { projectId: string; } -// 카테고리별 아이콘과 색상 +// Category configuration with icons and colors const categoryConfig = { - public: { icon: Globe, color: 'text-green-500', label: '공개' }, - restricted: { icon: Eye, color: 'text-yellow-500', label: '제한' }, - confidential: { icon: Lock, color: 'text-red-500', label: '기밀' }, - internal: { icon: Shield, color: 'text-blue-500', label: '내부' }, + public: { icon: Globe, color: 'text-green-500', label: 'Public' }, + restricted: { icon: Eye, color: 'text-yellow-500', label: 'Restricted' }, + confidential: { icon: Lock, color: 'text-red-500', label: 'Confidential' }, + internal: { icon: Shield, color: 'text-blue-500', label: 'Internal' }, }; // Tree Item Component @@ -143,172 +145,183 @@ const TreeItem: React.FC<{ onToggleExpand: (id: string) => void; onSelectItem: (id: string) => void; onDoubleClick: (item: FileItem) => void; + onView: (item: FileItem) => void; onDownload: (item: FileItem) => void; onDownloadFolder: (item: FileItem) => void; onDelete: (ids: string[]) => void; onShare: (item: FileItem) => void; onRename: (item: FileItem) => void; isInternalUser: boolean; -}> = ({ - item, - level, - expandedFolders, - selectedItems, - onToggleExpand, +}> = ({ + item, + level, + expandedFolders, + selectedItems, + onToggleExpand, onSelectItem, onDoubleClick, + onView, onDownload, onDownloadFolder, onDelete, onShare, onRename, - isInternalUser + isInternalUser }) => { - const hasChildren = item.type === 'folder' && item.children && item.children.length > 0; - const isExpanded = expandedFolders.has(item.id); - const isSelected = selectedItems.has(item.id); - const CategoryIcon = categoryConfig[item.category].icon; - const categoryColor = categoryConfig[item.category].color; - const categoryLabel = categoryConfig[item.category].label; + const hasChildren = item.type === 'folder' && item.children && item.children.length > 0; + const isExpanded = expandedFolders.has(item.id); + const isSelected = selectedItems.has(item.id); + const CategoryIcon = categoryConfig[item.category].icon; + const categoryColor = categoryConfig[item.category].color; + const categoryLabel = categoryConfig[item.category].label; - const formatFileSize = (bytes?: number) => { - if (!bytes) return '-'; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; - }; + const formatFileSize = (bytes?: number) => { + if (!bytes) return '-'; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; + }; - return ( - <> - <div - className={cn( - "flex items-center p-2 rounded-lg cursor-pointer transition-colors", - "hover:bg-accent", - isSelected && "bg-accent" - )} - style={{ paddingLeft: `${level * 24 + 8}px` }} - onClick={() => onSelectItem(item.id)} - onDoubleClick={() => onDoubleClick(item)} - > - <div className="flex items-center mr-2"> - {item.type === 'folder' && ( - <button - onClick={(e) => { - e.stopPropagation(); - onToggleExpand(item.id); - }} - className="p-0.5 hover:bg-gray-200 rounded" - > - {isExpanded ? ( - <ChevronDown className="h-4 w-4" /> - ) : ( - <ChevronRight className="h-4 w-4" /> - )} - </button> + return ( + <> + <div + className={cn( + "flex items-center p-2 rounded-lg cursor-pointer transition-colors", + "hover:bg-accent", + isSelected && "bg-accent" )} - {item.type === 'file' && ( - <div className="w-5" /> - )} - </div> - - {item.type === 'folder' ? ( - <Folder className="h-5 w-5 text-blue-500 mr-2" /> - ) : ( - <File className="h-5 w-5 text-gray-500 mr-2" /> - )} - - <span className="flex-1">{item.name}</span> - - <Badge variant="outline" className="mr-2"> - <CategoryIcon className={cn("h-3 w-3 mr-1", categoryColor)} /> - {categoryLabel} - </Badge> - - <span className="text-sm text-muted-foreground mr-4"> - {formatFileSize(item.size)} - </span> - <span className="text-sm text-muted-foreground mr-2"> - {new Date(item.updatedAt).toLocaleDateString()} - </span> - - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button variant="ghost" size="sm"> - <MoreVertical className="h-4 w-4" /> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent> - {item.type === 'file' && item.permissions?.canDownload && ( - <DropdownMenuItem onClick={() => onDownload(item)}> - <Download className="h-4 w-4 mr-2" /> - 다운로드 - </DropdownMenuItem> - )} - + style={{ paddingLeft: `${level * 24 + 8}px` }} + onClick={() => onSelectItem(item.id)} + onDoubleClick={() => onDoubleClick(item)} + > + <div className="flex items-center mr-2"> {item.type === 'folder' && ( - <DropdownMenuItem onClick={() => onDownloadFolder(item)}> - <Download className="h-4 w-4 mr-2" /> - 폴더 전체 다운로드 - </DropdownMenuItem> - )} - - {isInternalUser && ( - <> - <DropdownMenuItem onClick={() => onShare(item)}> - <Share2 className="h-4 w-4 mr-2" /> - 공유 - </DropdownMenuItem> - - {item.permissions?.canEdit && ( - <DropdownMenuItem onClick={() => onRename(item)}> - <Edit2 className="h-4 w-4 mr-2" /> - 이름 변경 - </DropdownMenuItem> + <button + onClick={(e) => { + e.stopPropagation(); + onToggleExpand(item.id); + }} + className="p-0.5 hover:bg-gray-200 rounded" + > + {isExpanded ? ( + <ChevronDown className="h-4 w-4" /> + ) : ( + <ChevronRight className="h-4 w-4" /> )} - </> + </button> )} - - {item.permissions?.canDelete && ( - <> - <DropdownMenuSeparator /> - <DropdownMenuItem - className="text-destructive" - onClick={() => onDelete([item.id])} - > - <Trash2 className="h-4 w-4 mr-2" /> - 삭제 - </DropdownMenuItem> - </> + {item.type === 'file' && ( + <div className="w-5" /> )} - </DropdownMenuContent> - </DropdownMenu> - </div> - - {item.type === 'folder' && isExpanded && item.children && ( - <div> - {item.children.map((child) => ( - <TreeItem - key={child.id} - item={child} - level={level + 1} - expandedFolders={expandedFolders} - selectedItems={selectedItems} - onToggleExpand={onToggleExpand} - onSelectItem={onSelectItem} - onDoubleClick={onDoubleClick} - onDownload={onDownload} - onDownloadFolder={onDownloadFolder} - onDelete={onDelete} - onShare={onShare} - onRename={onRename} - isInternalUser={isInternalUser} - /> - ))} + </div> + + {item.type === 'folder' ? ( + <Folder className="h-5 w-5 text-blue-500 mr-2" /> + ) : ( + <File className="h-5 w-5 text-gray-500 mr-2" /> + )} + + <span className="flex-1">{item.name}</span> + + <Badge variant="outline" className="mr-2"> + <CategoryIcon className={cn("h-3 w-3 mr-1", categoryColor)} /> + {categoryLabel} + </Badge> + + <span className="text-sm text-muted-foreground mr-4"> + {formatFileSize(item.size)} + </span> + <span className="text-sm text-muted-foreground mr-2"> + {new Date(item.updatedAt).toLocaleDateString()} + </span> + + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" size="sm"> + <MoreVertical className="h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent> + {item.type === 'file' && ( + <> + <DropdownMenuItem onClick={() => onView(item)}> + <Eye className="h-4 w-4 mr-2" /> + View + </DropdownMenuItem> + {item.permissions?.canDownload && ( + <DropdownMenuItem onClick={() => onDownload(item)}> + <Download className="h-4 w-4 mr-2" /> + Download + </DropdownMenuItem> + )} + </> + )} + + {item.type === 'folder' && ( + <DropdownMenuItem onClick={() => onDownloadFolder(item)}> + <Download className="h-4 w-4 mr-2" /> + Download Folder + </DropdownMenuItem> + )} + + {isInternalUser && ( + <> + <DropdownMenuItem onClick={() => onShare(item)}> + <Share2 className="h-4 w-4 mr-2" /> + Share + </DropdownMenuItem> + + {item.permissions?.canEdit && ( + <DropdownMenuItem onClick={() => onRename(item)}> + <Edit2 className="h-4 w-4 mr-2" /> + Rename + </DropdownMenuItem> + )} + </> + )} + + {item.permissions?.canDelete && ( + <> + <DropdownMenuSeparator /> + <DropdownMenuItem + className="text-destructive" + onClick={() => onDelete([item.id])} + > + <Trash2 className="h-4 w-4 mr-2" /> + Delete + </DropdownMenuItem> + </> + )} + </DropdownMenuContent> + </DropdownMenu> </div> - )} - </> - ); -}; + + {item.type === 'folder' && isExpanded && item.children && ( + <div> + {item.children.map((child) => ( + <TreeItem + key={child.id} + item={child} + level={level + 1} + expandedFolders={expandedFolders} + selectedItems={selectedItems} + onToggleExpand={onToggleExpand} + onSelectItem={onSelectItem} + onDoubleClick={onDoubleClick} + onView={onView} + onDownload={onDownload} + onDownloadFolder={onDownloadFolder} + onDelete={onDelete} + onShare={onShare} + onRename={onRename} + isInternalUser={isInternalUser} + /> + ))} + </div> + )} + </> + ); + }; export function FileManager({ projectId }: FileManagerProps) { const { data: session } = useSession(); @@ -321,19 +334,23 @@ export function FileManager({ projectId }: FileManagerProps) { const [viewMode, setViewMode] = useState<'grid' | 'list'>('list'); const [searchQuery, setSearchQuery] = useState(''); const [loading, setLoading] = useState(false); - - // 업로드 상태 + + console.log(items,"items") + + // Upload states const [uploadDialogOpen, setUploadDialogOpen] = useState(false); const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([]); const [uploadCategory, setUploadCategory] = useState<string>('confidential'); - - // 다이얼로그 상태 + + // Dialog states const [folderDialogOpen, setFolderDialogOpen] = useState(false); const [shareDialogOpen, setShareDialogOpen] = useState(false); const [permissionDialogOpen, setPermissionDialogOpen] = useState(false); const [renameDialogOpen, setRenameDialogOpen] = useState(false); - - // 다이얼로그 데이터 + const [viewerDialogOpen, setViewerDialogOpen] = useState(false); + const [viewerFileUrl, setViewerFileUrl] = useState<string | null>(null); + + // Dialog data const [dialogValue, setDialogValue] = useState(''); const [selectedCategory, setSelectedCategory] = useState<string>('confidential'); const [selectedFile, setSelectedFile] = useState<FileItem | null>(null); @@ -343,76 +360,76 @@ export function FileManager({ projectId }: FileManagerProps) { expiresAt: '', maxDownloads: '', }); - + const { toast } = useToast(); - // 사용자가 내부 사용자인지 확인 + // Check if user is internal const isInternalUser = session?.user?.domain !== 'partners'; - // 트리 구조 생성 함수 + // Build tree structure function const buildTree = (flatItems: FileItem[]): FileItem[] => { const itemMap = new Map<string, FileItem>(); const rootItems: FileItem[] = []; - - // 모든 아이템을 맵에 저장 (children 초기화) + + // Store all items in map (initialize children) flatItems.forEach(item => { itemMap.set(item.id, { ...item, children: [] }); }); - - // 부모-자식 관계 설정 + + // Set parent-child relationships flatItems.forEach(item => { const mappedItem = itemMap.get(item.id)!; - + if (!item.parentId) { - // parentId가 없으면 루트 아이템 + // No parentId means root item rootItems.push(mappedItem); } else { - // parentId가 있으면 부모의 children에 추가 + // Has parentId, add to parent's children const parent = itemMap.get(item.parentId); if (parent) { if (!parent.children) parent.children = []; parent.children.push(mappedItem); } else { - // 부모를 찾을 수 없으면 루트로 처리 + // Can't find parent, treat as root rootItems.push(mappedItem); } } }); - + return rootItems; }; - // 파일 목록 가져오기 + // Fetch file list const fetchItems = useCallback(async () => { setLoading(true); try { const params = new URLSearchParams(); - - // 트리 뷰일 때는 전체 목록을 가져옴 + + // For tree view, get entire list if (viewMode === 'list') { params.append('viewMode', 'tree'); - // 트리 뷰에서도 현재 경로 정보는 유지 (하이라이팅 등에 사용) + // Keep current path info for tree view (used for highlighting, etc.) if (currentParentId) params.append('currentParentId', currentParentId); } else { - // 그리드 뷰일 때는 현재 폴더의 내용만 가져옴 + // For grid view, only get current folder contents if (currentParentId) params.append('parentId', currentParentId); } - + const response = await fetch(`/api/data-room/${projectId}?${params}`); if (!response.ok) throw new Error('Failed to fetch files'); - + const data = await response.json(); setItems(data); - - // 트리 구조 생성 + + // Build tree structure if (viewMode === 'list') { const tree = buildTree(data); setTreeItems(tree); } } catch (error) { toast({ - title: '오류', - description: '파일을 불러오는데 실패했습니다.', + title: 'Error', + description: 'Failed to load files.', variant: 'destructive', }); } finally { @@ -424,7 +441,7 @@ export function FileManager({ projectId }: FileManagerProps) { fetchItems(); }, [fetchItems]); - // 폴더 생성 + // Create folder const createFolder = async () => { try { const response = await fetch(`/api/data-room/${projectId}`, { @@ -446,66 +463,66 @@ export function FileManager({ projectId }: FileManagerProps) { await fetchItems(); setFolderDialogOpen(false); setDialogValue(''); - + toast({ - title: '성공', - description: '폴더가 생성되었습니다.', + title: 'Success', + description: 'Folder created successfully.', }); } catch (error: any) { toast({ - title: '오류', - description: error.message || '폴더 생성에 실패했습니다.', + title: 'Error', + description: error.message || 'Failed to create folder.', variant: 'destructive', }); } }; - // 파일 업로드 처리 + // Handle file upload const handleFileUpload = async (files: FileList | File[]) => { const fileArray = Array.from(files); - - // 업로드 파일 목록 초기화 + + // Initialize uploading file list const newUploadingFiles: UploadingFile[] = fileArray.map(file => ({ file, progress: 0, status: 'pending' as const })); - + setUploadingFiles(newUploadingFiles); - - // 각 파일 업로드 처리 + + // Process each file upload for (let i = 0; i < fileArray.length; i++) { const file = fileArray[i]; - + try { - // 상태 업데이트: 업로드 중 - setUploadingFiles(prev => prev.map((f, idx) => + // Update status: uploading + setUploadingFiles(prev => prev.map((f, idx) => idx === i ? { ...f, status: 'uploading', progress: 20 } : f )); - // DRM 복호화 - setUploadingFiles(prev => prev.map((f, idx) => + // DRM decryption + setUploadingFiles(prev => prev.map((f, idx) => idx === i ? { ...f, status: 'processing', progress: 40 } : f )); - + const decryptedData = await decryptWithServerAction(file); - - // FormData 생성 + + // Create FormData const formData = new FormData(); const blob = new Blob([decryptedData], { type: file.type }); formData.append('file', blob, file.name); formData.append('category', uploadCategory); - formData.append('fileSize', file.size.toString()); // 파일 크기 전달 + formData.append('fileSize', file.size.toString()); // Pass file size if (currentParentId) { formData.append('parentId', currentParentId); } - - // 업로드 진행률 업데이트 - setUploadingFiles(prev => prev.map((f, idx) => + + // Update upload progress + setUploadingFiles(prev => prev.map((f, idx) => idx === i ? { ...f, progress: 60 } : f )); - // API 호출 + // API call const response = await fetch(`/api/data-room/${projectId}/upload`, { method: 'POST', body: formData, @@ -515,99 +532,99 @@ export function FileManager({ projectId }: FileManagerProps) { const error = await response.json(); throw new Error(error.error || 'Upload failed'); } - - // 성공 - setUploadingFiles(prev => prev.map((f, idx) => + + // Success + setUploadingFiles(prev => prev.map((f, idx) => idx === i ? { ...f, status: 'completed', progress: 100 } : f )); - + } catch (error: any) { - // 실패 - setUploadingFiles(prev => prev.map((f, idx) => - idx === i ? { - ...f, - status: 'error', - error: error.message || '업로드 실패' + // Failure + setUploadingFiles(prev => prev.map((f, idx) => + idx === i ? { + ...f, + status: 'error', + error: error.message || 'Upload failed' } : f )); } } - - // 모든 업로드 완료 후 목록 새로고침 + + // Refresh list after all uploads complete await fetchItems(); - - // 성공한 파일이 있으면 토스트 표시 + + // Show toast if any files succeeded const successCount = newUploadingFiles.filter(f => f.status === 'completed').length; if (successCount > 0) { toast({ - title: '업로드 완료', - description: `${successCount}개 파일이 업로드되었습니다.`, + title: 'Upload Complete', + description: `${successCount} file(s) uploaded successfully.`, }); } }; - // 폴더 다운로드 + // Download folder const downloadFolder = async (folder: FileItem) => { if (folder.type !== 'folder') return; - + try { toast({ - title: '권한 확인 중', - description: '폴더 내 파일들의 다운로드 권한을 확인하고 있습니다...', + title: 'Checking Permissions', + description: 'Verifying download permissions for folder contents...', }); - // 폴더 다운로드 API 호출 + // Call folder download API const response = await fetch(`/api/data-room/${projectId}/download-folder/${folder.id}`, { method: 'GET', }); - + if (!response.ok) { const error = await response.json(); - - // 권한이 없는 파일이 있는 경우 상세 정보 제공 + + // If there are files without permission, provide details if (error.unauthorizedFiles) { toast({ - title: '다운로드 권한 부족', - description: `${error.unauthorizedFiles.length}개 파일에 대한 권한이 없습니다: ${error.unauthorizedFiles.join(', ')}`, + title: 'Insufficient Permissions', + description: `No permission for ${error.unauthorizedFiles.length} file(s): ${error.unauthorizedFiles.join(', ')}`, variant: 'destructive', }); return; } - - throw new Error(error.error || '폴더 다운로드 실패'); + + throw new Error(error.error || 'Folder download failed'); } const blob = await response.blob(); const url = window.URL.createObjectURL(blob); - - // 폴더명을 파일명에 포함 + + // Include folder name in filename const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); const fileName = `${folder.name}_${timestamp}.zip`; - + const a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); - + window.URL.revokeObjectURL(url); document.body.removeChild(a); toast({ - title: '다운로드 완료', - description: `${folder.name} 폴더가 다운로드되었습니다.`, + title: 'Download Complete', + description: `${folder.name} folder downloaded successfully.`, }); } catch (error: any) { toast({ - title: '오류', - description: error.message || '폴더 다운로드에 실패했습니다.', + title: 'Error', + description: error.message || 'Failed to download folder.', variant: 'destructive', }); } }; - // 파일 공유 + // Share file const shareFile = async () => { if (!selectedFile) return; @@ -626,45 +643,45 @@ export function FileManager({ projectId }: FileManagerProps) { } const data = await response.json(); - - // 공유 링크 복사 + + // Copy share link to clipboard await navigator.clipboard.writeText(data.shareUrl); - + toast({ - title: '공유 링크 생성됨', - description: '링크가 클립보드에 복사되었습니다.', + title: 'Share Link Created', + description: 'Link copied to clipboard.', }); - + setShareDialogOpen(false); setSelectedFile(null); } catch (error) { toast({ - title: '오류', - description: '공유 링크 생성에 실패했습니다.', + title: 'Error', + description: 'Failed to create share link.', variant: 'destructive', }); } }; - // 다중 파일 다운로드 + // Download multiple files const downloadMultipleFiles = async (itemIds: string[]) => { - // 선택된 파일들 중 실제 파일만 필터링 (폴더 제외) - const filesToDownload = items.filter(item => - itemIds.includes(item.id) && - item.type === 'file' && - item.permissions?.canDownload + // Filter only actual files (exclude folders) that can be downloaded + const filesToDownload = items.filter(item => + itemIds.includes(item.id) && + item.type === 'file' && + item.permissions?.canDownload === 'true' ); if (filesToDownload.length === 0) { toast({ - title: '알림', - description: '다운로드 가능한 파일이 없습니다.', + title: 'Notice', + description: 'No downloadable files selected.', variant: 'default', }); return; } - // 단일 파일인 경우 일반 다운로드 사용 + // Use regular download for single file if (filesToDownload.length === 1) { await downloadFile(filesToDownload[0]); return; @@ -672,98 +689,119 @@ export function FileManager({ projectId }: FileManagerProps) { try { toast({ - title: '다운로드 준비 중', - description: `${filesToDownload.length}개 파일을 압축하고 있습니다...`, + title: 'Preparing Download', + description: `Compressing ${filesToDownload.length} files...`, }); - // 여러 파일 다운로드 API 호출 + // Call multiple files download API const response = await fetch(`/api/data-room/${projectId}/download-multiple`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileIds: filesToDownload.map(f => f.id) }) }); - + if (!response.ok) { - throw new Error('다운로드 실패'); + throw new Error('Download failed'); } const blob = await response.blob(); const url = window.URL.createObjectURL(blob); - - // 현재 날짜시간을 파일명에 포함 + + // Include timestamp in filename const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); const fileName = `files_${timestamp}.zip`; - + const a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); - + window.URL.revokeObjectURL(url); document.body.removeChild(a); toast({ - title: '다운로드 완료', - description: `${filesToDownload.length}개 파일이 다운로드되었습니다.`, + title: 'Download Complete', + description: `${filesToDownload.length} files downloaded successfully.`, }); } catch (error) { - console.error('다중 다운로드 오류:', error); - - // 실패 시 개별 다운로드 옵션 제공 + console.error('Multiple download error:', error); + + // Offer individual downloads on failure toast({ - title: '압축 다운로드 실패', - description: '개별 다운로드를 시도하시겠습니까?', + title: 'Batch Download Failed', + description: 'Would you like to try individual downloads?', action: ( <Button size="sm" variant="outline" onClick={() => { - // 개별 다운로드 실행 + // Execute individual downloads filesToDownload.forEach(async (file, index) => { - // 다운로드 간격을 두어 브라우저 부하 감소 + // Add delay between downloads to reduce browser load setTimeout(() => downloadFile(file), index * 500); }); }} > - 개별 다운로드 + Download Individually </Button> ), }); } }; - // 파일 다운로드 + // View file with PDFTron + const viewFile = async (file: FileItem) => { + try { + + + + setViewerFileUrl(file.filePath); + setSelectedFile(file); + setViewerDialogOpen(true); + + + + } catch (error) { + toast({ + title: 'Error', + description: 'Failed to open file for viewing.', + variant: 'destructive', + }); + } + }; + + // Download file const downloadFile = async (file: FileItem) => { try { const response = await fetch(`/api/data-room/${projectId}/${file.id}/download`); - + if (!response.ok) { throw new Error('Download failed'); } const blob = await response.blob(); const url = window.URL.createObjectURL(blob); - + const a = document.createElement('a'); a.href = url; a.download = file.name; document.body.appendChild(a); a.click(); - + window.URL.revokeObjectURL(url); document.body.removeChild(a); } catch (error) { toast({ - title: '오류', - description: '다운로드에 실패했습니다.', + title: 'Error', + description: 'Download failed.', variant: 'destructive', }); } }; - // 파일 삭제 + // Delete files const deleteItems = async (itemIds: string[]) => { try { await Promise.all( @@ -774,27 +812,106 @@ export function FileManager({ projectId }: FileManagerProps) { await fetchItems(); setSelectedItems(new Set()); - + + toast({ + title: 'Success', + description: 'Selected items deleted successfully.', + }); + } catch (error) { + toast({ + title: 'Error', + description: 'Failed to delete items.', + variant: 'destructive', + }); + } + }; + + // Rename item + const renameItem = async () => { + if (!selectedFile) return; + + try { + const response = await fetch( + `/api/data-room/${projectId}/${selectedFile.id}`, + { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: dialogValue }), + } + ); + + if (!response.ok) { + throw new Error('Failed to rename'); + } + + await fetchItems(); + setRenameDialogOpen(false); + setSelectedFile(null); + setDialogValue(''); + + toast({ + title: 'Success', + description: 'Item renamed successfully.', + }); + } catch (error) { + toast({ + title: 'Error', + description: 'Failed to rename item.', + variant: 'destructive', + }); + } + }; + + // Change category + const changeCategory = async ( + itemId: string, + newCategory: string, + applyToChildren: boolean = false + ) => { + try { + const response = await fetch( + `/api/data-room/${projectId}/${itemId}`, + { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + category: newCategory, + applyToChildren + }), + } + ); + + if (!response.ok) { + throw new Error('Failed to change category'); + } + + await fetchItems(); + toast({ - title: '성공', - description: '선택한 항목이 삭제되었습니다.', + title: 'Success', + description: 'Category updated successfully.', }); } catch (error) { toast({ - title: '오류', - description: '삭제에 실패했습니다.', + title: 'Error', + description: 'Failed to change category.', variant: 'destructive', }); } }; - // 폴더 더블클릭 처리 + // Category change dialog states + const [categoryDialogOpen, setCategoryDialogOpen] = useState(false); + const [applyToChildren, setApplyToChildren] = useState(false); + const [newCategory, setNewCategory] = useState('confidential'); + + // Handle folder double click const handleFolderOpen = (folder: FileItem) => { if (viewMode === 'grid') { setCurrentPath([...currentPath, folder.name]); setCurrentParentId(folder.id); } else { - // 트리 뷰에서는 expand/collapse + // In tree view, expand/collapse const newExpanded = new Set(expandedFolders); if (newExpanded.has(folder.id)) { newExpanded.delete(folder.id); @@ -806,7 +923,7 @@ export function FileManager({ projectId }: FileManagerProps) { setSelectedItems(new Set()); }; - // 폴더 확장 토글 + // Toggle folder expansion const toggleFolderExpand = (folderId: string) => { const newExpanded = new Set(expandedFolders); if (newExpanded.has(folderId)) { @@ -817,7 +934,7 @@ export function FileManager({ projectId }: FileManagerProps) { setExpandedFolders(newExpanded); }; - // 아이템 선택 + // Toggle item selection const toggleItemSelection = (itemId: string) => { const newSelected = new Set(selectedItems); if (newSelected.has(itemId)) { @@ -828,18 +945,18 @@ export function FileManager({ projectId }: FileManagerProps) { setSelectedItems(newSelected); }; - // 경로 탐색 + // Navigate to path const navigateToPath = (index: number) => { if (index === -1) { setCurrentPath([]); setCurrentParentId(null); } else { setCurrentPath(currentPath.slice(0, index + 1)); - // parentId 업데이트 로직 필요 + // Need to update parentId logic } }; - // 필터링된 아이템 + // Filtered items const filteredItems = items.filter(item => item.name.toLowerCase().includes(searchQuery.toLowerCase()) ); @@ -848,7 +965,7 @@ export function FileManager({ projectId }: FileManagerProps) { item.name.toLowerCase().includes(searchQuery.toLowerCase()) ); - // 파일 크기 포맷 + // Format file size const formatFileSize = (bytes?: number) => { if (!bytes) return '-'; const sizes = ['B', 'KB', 'MB', 'GB']; @@ -858,7 +975,7 @@ export function FileManager({ projectId }: FileManagerProps) { return ( <div className="flex flex-col h-full"> - {/* 툴바 */} + {/* Toolbar */} <div className="border-b p-4"> <div className="flex items-center justify-between mb-3"> <div className="flex items-center gap-2"> @@ -869,40 +986,40 @@ export function FileManager({ projectId }: FileManagerProps) { onClick={() => setFolderDialogOpen(true)} > <FolderPlus className="h-4 w-4 mr-1" /> - 새 폴더 + New Folder </Button> - <Button - size="sm" + <Button + size="sm" variant="outline" onClick={() => setUploadDialogOpen(true)} > <Upload className="h-4 w-4 mr-1" /> - 업로드 + Upload </Button> </> )} - + {selectedItems.size > 0 && ( <> - {/* 다중 다운로드 버튼 */} - {items.filter(item => - selectedItems.has(item.id) && - item.type === 'file' && - item.permissions?.canDownload + {/* Multiple download button */} + {items.filter(item => + selectedItems.has(item.id) && + item.type === 'file' && + item.permissions?.canDownload ==='true' ).length > 0 && ( - <Button - size="sm" - variant="outline" - onClick={() => downloadMultipleFiles(Array.from(selectedItems))} - > - <Download className="h-4 w-4 mr-1" /> - 다운로드 ({items.filter(item => - selectedItems.has(item.id) && item.type === 'file' - ).length}) - </Button> - )} - - {/* 삭제 버튼 */} + <Button + size="sm" + variant="outline" + onClick={() => downloadMultipleFiles(Array.from(selectedItems))} + > + <Download className="h-4 w-4 mr-1" /> + Download ({items.filter(item => + selectedItems.has(item.id) && item.type === 'file' + ).length}) + </Button> + )} + + {/* Delete button */} {items.find(item => selectedItems.has(item.id))?.permissions?.canDelete && ( <Button size="sm" @@ -910,31 +1027,31 @@ export function FileManager({ projectId }: FileManagerProps) { onClick={() => deleteItems(Array.from(selectedItems))} > <Trash2 className="h-4 w-4 mr-1" /> - 삭제 ({selectedItems.size}) + Delete ({selectedItems.size}) </Button> )} </> )} - + {!isInternalUser && ( <Badge variant="secondary" className="ml-2"> <Shield className="h-3 w-3 mr-1" /> - 외부 사용자 + External User </Badge> )} </div> - + <div className="flex items-center gap-2"> <div className="relative"> <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Input - placeholder="검색..." + placeholder="Search..." className="pl-8 w-64" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} /> </div> - + <Button size="sm" variant="ghost" @@ -947,41 +1064,41 @@ export function FileManager({ projectId }: FileManagerProps) { {/* Breadcrumb */} <Breadcrumb> - <BreadcrumbList> - <BreadcrumbItem> - <BreadcrumbLink onClick={() => navigateToPath(-1)}> - Home - </BreadcrumbLink> - </BreadcrumbItem> - {currentPath.map((path, index) => ( - <BreadcrumbItem key={index}> - <ChevronRight className="h-4 w-4" /> - <BreadcrumbLink onClick={() => navigateToPath(index)}> - {path} + <BreadcrumbList> + <BreadcrumbItem> + <BreadcrumbLink onClick={() => navigateToPath(-1)}> + Home </BreadcrumbLink> </BreadcrumbItem> - ))} + {currentPath.map((path, index) => ( + <BreadcrumbItem key={index}> + <ChevronRight className="h-4 w-4" /> + <BreadcrumbLink onClick={() => navigateToPath(index)}> + {path} + </BreadcrumbLink> + </BreadcrumbItem> + ))} </BreadcrumbList> </Breadcrumb> </div> - {/* 파일 목록 */} + {/* File List */} <ScrollArea className="flex-1 p-4"> {loading ? ( <div className="flex justify-center items-center h-64"> - <div className="text-muted-foreground">로딩 중...</div> + <div className="text-muted-foreground">Loading...</div> </div> ) : filteredItems.length === 0 ? ( <div className="flex flex-col items-center justify-center h-64"> <Folder className="h-12 w-12 text-muted-foreground mb-2" /> - <p className="text-muted-foreground">비어있음</p> + <p className="text-muted-foreground">Empty</p> </div> ) : viewMode === 'grid' ? ( <div className="grid grid-cols-6 gap-4"> {filteredItems.map((item) => { const CategoryIcon = categoryConfig[item.category].icon; const categoryColor = categoryConfig[item.category].color; - + return ( <ContextMenu key={item.id}> <ContextMenuTrigger> @@ -1006,11 +1123,11 @@ export function FileManager({ projectId }: FileManagerProps) { )} <CategoryIcon className={cn("h-4 w-4 absolute -bottom-1 -right-1", categoryColor)} /> </div> - + <span className="mt-2 text-sm text-center truncate w-full"> {item.name} </span> - + {item.viewCount !== undefined && ( <div className="flex items-center gap-2 mt-1"> <span className="text-xs text-muted-foreground flex items-center"> @@ -1027,45 +1144,66 @@ export function FileManager({ projectId }: FileManagerProps) { )} </div> </ContextMenuTrigger> - + <ContextMenuContent> {item.type === 'folder' && ( <> <ContextMenuItem onClick={() => handleFolderOpen(item)}> - 열기 + Open </ContextMenuItem> <ContextMenuItem onClick={() => downloadFolder(item)}> <Download className="h-4 w-4 mr-2" /> - 폴더 전체 다운로드 + Download Folder </ContextMenuItem> </> )} - - {item.type === 'file' && item.permissions?.canDownload && ( - <ContextMenuItem onClick={() => downloadFile(item)}> - <Download className="h-4 w-4 mr-2" /> - 다운로드 - </ContextMenuItem> + + {item.type === 'file' && ( + <> + <ContextMenuItem onClick={() => viewFile(item)}> + <Eye className="h-4 w-4 mr-2" /> + View + </ContextMenuItem> + {item.permissions?.canDownload === 'true' && ( + <ContextMenuItem onClick={() => downloadFile(item)}> + <Download className="h-4 w-4 mr-2" /> + Download + </ContextMenuItem> + )} + </> )} - + {isInternalUser && ( <> <ContextMenuSeparator /> <ContextMenuSub> <ContextMenuSubTrigger> <Shield className="h-4 w-4 mr-2" /> - 카테고리 변경 + Change Category </ContextMenuSubTrigger> <ContextMenuSubContent> {Object.entries(categoryConfig).map(([key, config]) => ( - <ContextMenuItem key={key}> + <ContextMenuItem + key={key} + onClick={() => { + if (item.type === 'folder') { + // Show dialog for folders + setSelectedFile(item); + setNewCategory(key); + setCategoryDialogOpen(true); + } else { + // Change immediately for files + changeCategory(item.id, key, false); + } + }} + > <config.icon className={cn("h-4 w-4 mr-2", config.color)} /> {config.label} </ContextMenuItem> ))} </ContextMenuSubContent> </ContextMenuSub> - + <ContextMenuItem onClick={() => { setSelectedFile(item); @@ -1073,9 +1211,9 @@ export function FileManager({ projectId }: FileManagerProps) { }} > <Share2 className="h-4 w-4 mr-2" /> - 공유 + Share </ContextMenuItem> - + {item.permissions?.canEdit && ( <ContextMenuItem onClick={() => { setSelectedFile(item); @@ -1083,12 +1221,12 @@ export function FileManager({ projectId }: FileManagerProps) { setRenameDialogOpen(true); }}> <Edit2 className="h-4 w-4 mr-2" /> - 이름 변경 + Rename </ContextMenuItem> )} </> )} - + {item.permissions?.canDelete && ( <> <ContextMenuSeparator /> @@ -1097,7 +1235,7 @@ export function FileManager({ projectId }: FileManagerProps) { onClick={() => deleteItems([item.id])} > <Trash2 className="h-4 w-4 mr-2" /> - 삭제 + Delete </ContextMenuItem> </> )} @@ -1119,6 +1257,7 @@ export function FileManager({ projectId }: FileManagerProps) { onToggleExpand={toggleFolderExpand} onSelectItem={toggleItemSelection} onDoubleClick={handleFolderOpen} + onView={viewFile} onDownload={downloadFile} onDownloadFolder={downloadFolder} onDelete={deleteItems} @@ -1138,20 +1277,20 @@ export function FileManager({ projectId }: FileManagerProps) { )} </ScrollArea> - {/* 업로드 다이얼로그 */} + {/* Upload Dialog */} <Dialog open={uploadDialogOpen} onOpenChange={setUploadDialogOpen}> <DialogContent className="max-w-2xl"> <DialogHeader> - <DialogTitle>파일 업로드</DialogTitle> + <DialogTitle>Upload Files</DialogTitle> <DialogDescription> - 파일을 드래그 앤 드롭하거나 클릭하여 선택하세요. + Drag and drop files or click to select. </DialogDescription> </DialogHeader> - + <div className="space-y-4"> - {/* 카테고리 선택 */} + {/* Category Selection */} <div> - <Label htmlFor="upload-category">카테고리</Label> + <Label htmlFor="upload-category">Category</Label> <Select value={uploadCategory} onValueChange={setUploadCategory}> <SelectTrigger> <SelectValue /> @@ -1170,7 +1309,7 @@ export function FileManager({ projectId }: FileManagerProps) { </div> {/* Dropzone */} - <Dropzone + <Dropzone onDrop={(acceptedFiles: File[]) => { handleFileUpload(acceptedFiles); }} @@ -1198,16 +1337,16 @@ export function FileManager({ projectId }: FileManagerProps) { <DropzoneInput /> <div className="flex flex-col items-center justify-center h-full"> <DropzoneUploadIcon className="h-12 w-12 text-muted-foreground mb-4" /> - <DropzoneTitle>파일을 드래그하거나 클릭하여 업로드</DropzoneTitle> - <DropzoneDescription>여러 파일을 동시에 업로드할 수 있습니다</DropzoneDescription> + <DropzoneTitle>Drag files or click to upload</DropzoneTitle> + <DropzoneDescription>Multiple files can be uploaded simultaneously</DropzoneDescription> </div> </DropzoneZone> </Dropzone> - {/* 업로드 중인 파일 목록 */} + {/* Uploading File List */} {uploadingFiles.length > 0 && ( <FileList> - <FileListHeader>업로드 중인 파일</FileListHeader> + <FileListHeader>Uploading Files</FileListHeader> {uploadingFiles.map((uploadFile, index) => ( <FileListItem key={index}> <FileListIcon> @@ -1218,10 +1357,10 @@ export function FileManager({ projectId }: FileManagerProps) { <FileListDescription> <div className="flex items-center gap-2"> <FileListSize>{uploadFile.file.size}</FileListSize> - {uploadFile.status === 'uploading' && <span>업로드 중...</span>} - {uploadFile.status === 'processing' && <span>처리 중...</span>} + {uploadFile.status === 'uploading' && <span>Uploading...</span>} + {uploadFile.status === 'processing' && <span>Processing...</span>} {uploadFile.status === 'completed' && ( - <span className="text-green-600">완료</span> + <span className="text-green-600">Complete</span> )} {uploadFile.status === 'error' && ( <span className="text-red-600">{uploadFile.error}</span> @@ -1238,7 +1377,7 @@ export function FileManager({ projectId }: FileManagerProps) { size="sm" variant="ghost" onClick={() => { - setUploadingFiles(prev => + setUploadingFiles(prev => prev.filter((_, i) => i !== index) ); }} @@ -1252,44 +1391,44 @@ export function FileManager({ projectId }: FileManagerProps) { </FileList> )} </div> - + <DialogFooter> - <Button - variant="outline" + <Button + variant="outline" onClick={() => { setUploadDialogOpen(false); setUploadingFiles([]); }} > - 닫기 + Close </Button> </DialogFooter> </DialogContent> </Dialog> - {/* 폴더 생성 다이얼로그 */} + {/* Create Folder Dialog */} <Dialog open={folderDialogOpen} onOpenChange={setFolderDialogOpen}> <DialogContent> <DialogHeader> - <DialogTitle>새 폴더 만들기</DialogTitle> + <DialogTitle>Create New Folder</DialogTitle> <DialogDescription> - 폴더 이름과 접근 권한 카테고리를 설정하세요. + Set the folder name and access permission category. </DialogDescription> </DialogHeader> - + <div className="space-y-4"> <div> - <Label htmlFor="folder-name">폴더 이름</Label> + <Label htmlFor="folder-name">Folder Name</Label> <Input id="folder-name" value={dialogValue} onChange={(e) => setDialogValue(e.target.value)} - placeholder="폴더 이름 입력" + placeholder="Enter folder name" /> </div> - + <div> - <Label htmlFor="folder-category">카테고리</Label> + <Label htmlFor="folder-category">Category</Label> <Select value={selectedCategory} onValueChange={setSelectedCategory}> <SelectTrigger> <SelectValue /> @@ -1307,38 +1446,38 @@ export function FileManager({ projectId }: FileManagerProps) { </Select> </div> </div> - + <DialogFooter> <Button variant="outline" onClick={() => setFolderDialogOpen(false)}> - 취소 + Cancel </Button> - <Button onClick={createFolder}>생성</Button> + <Button onClick={createFolder}>Create</Button> </DialogFooter> </DialogContent> </Dialog> - {/* 파일 공유 다이얼로그 */} + {/* File Share Dialog */} <Dialog open={shareDialogOpen} onOpenChange={setShareDialogOpen}> <DialogContent className="max-w-md"> <DialogHeader> - <DialogTitle>파일 공유</DialogTitle> + <DialogTitle>Share File</DialogTitle> <DialogDescription> - {selectedFile?.name}을(를) 공유합니다. + Sharing {selectedFile?.name}. </DialogDescription> </DialogHeader> - + <Tabs defaultValue="link" className="w-full"> <TabsList className="grid w-full grid-cols-2"> - <TabsTrigger value="link">링크 공유</TabsTrigger> - <TabsTrigger value="permission">권한 설정</TabsTrigger> + <TabsTrigger value="link">Link Sharing</TabsTrigger> + <TabsTrigger value="permission">Permission Settings</TabsTrigger> </TabsList> - + <TabsContent value="link" className="space-y-4"> <div> - <Label htmlFor="access-level">접근 레벨</Label> - <Select - value={shareSettings.accessLevel} - onValueChange={(value) => setShareSettings({...shareSettings, accessLevel: value})} + <Label htmlFor="access-level">Access Level</Label> + <Select + value={shareSettings.accessLevel} + onValueChange={(value) => setShareSettings({ ...shareSettings, accessLevel: value })} > <SelectTrigger> <SelectValue /> @@ -1347,101 +1486,305 @@ export function FileManager({ projectId }: FileManagerProps) { <SelectItem value="view_only"> <div className="flex items-center"> <Eye className="h-4 w-4 mr-2" /> - 보기만 가능 + View Only </div> </SelectItem> <SelectItem value="view_download"> <div className="flex items-center"> <Download className="h-4 w-4 mr-2" /> - 보기 + 다운로드 + View + Download </div> </SelectItem> </SelectContent> </Select> </div> - + <div> - <Label htmlFor="password">비밀번호 (선택)</Label> + <Label htmlFor="password">Password (Optional)</Label> <Input id="password" type="password" value={shareSettings.password} - onChange={(e) => setShareSettings({...shareSettings, password: e.target.value})} - placeholder="비밀번호 입력" + onChange={(e) => setShareSettings({ ...shareSettings, password: e.target.value })} + placeholder="Enter password" /> </div> - + <div> - <Label htmlFor="expires">만료일 (선택)</Label> + <Label htmlFor="expires">Expiry Date (Optional)</Label> <Input id="expires" type="datetime-local" value={shareSettings.expiresAt} - onChange={(e) => setShareSettings({...shareSettings, expiresAt: e.target.value})} + onChange={(e) => setShareSettings({ ...shareSettings, expiresAt: e.target.value })} /> </div> - + <div> - <Label htmlFor="max-downloads">최대 다운로드 횟수 (선택)</Label> + <Label htmlFor="max-downloads">Max Downloads (Optional)</Label> <Input id="max-downloads" type="number" value={shareSettings.maxDownloads} - onChange={(e) => setShareSettings({...shareSettings, maxDownloads: e.target.value})} - placeholder="무제한" + onChange={(e) => setShareSettings({ ...shareSettings, maxDownloads: e.target.value })} + placeholder="Unlimited" /> </div> </TabsContent> - + <TabsContent value="permission" className="space-y-4"> <div> - <Label htmlFor="target-domain">대상 도메인</Label> + <Label htmlFor="target-domain">Target Domain</Label> <Select> <SelectTrigger> - <SelectValue placeholder="도메인 선택" /> + <SelectValue placeholder="Select domain" /> </SelectTrigger> <SelectContent> - <SelectItem value="partners">파트너</SelectItem> - <SelectItem value="internal">내부</SelectItem> + <SelectItem value="partners">Partners</SelectItem> + <SelectItem value="internal">Internal</SelectItem> </SelectContent> </Select> </div> - + <div className="space-y-2"> - <Label>권한</Label> + <Label>Permissions</Label> <div className="space-y-2"> <div className="flex items-center justify-between"> - <Label htmlFor="can-view" className="text-sm font-normal">보기</Label> + <Label htmlFor="can-view" className="text-sm font-normal">View</Label> <Switch id="can-view" defaultChecked /> </div> <div className="flex items-center justify-between"> - <Label htmlFor="can-download" className="text-sm font-normal">다운로드</Label> + <Label htmlFor="can-download" className="text-sm font-normal">Download</Label> <Switch id="can-download" /> </div> <div className="flex items-center justify-between"> - <Label htmlFor="can-edit" className="text-sm font-normal">수정</Label> + <Label htmlFor="can-edit" className="text-sm font-normal">Edit</Label> <Switch id="can-edit" /> </div> <div className="flex items-center justify-between"> - <Label htmlFor="can-share" className="text-sm font-normal">공유</Label> + <Label htmlFor="can-share" className="text-sm font-normal">Share</Label> <Switch id="can-share" /> </div> </div> </div> </TabsContent> </Tabs> - + <DialogFooter> <Button variant="outline" onClick={() => setShareDialogOpen(false)}> - 취소 + Cancel </Button> <Button onClick={shareFile}> <Share2 className="h-4 w-4 mr-2" /> - 공유 링크 생성 + Create Share Link + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + + {/* Rename Dialog */} + <Dialog open={renameDialogOpen} onOpenChange={setRenameDialogOpen}> + <DialogContent> + <DialogHeader> + <DialogTitle>Rename</DialogTitle> + <DialogDescription> + {selectedFile?.type === 'file' + ? 'Enter the file name. (Extension will be preserved automatically)' + : 'Enter the folder name.' + } + </DialogDescription> + </DialogHeader> + + <div> + <Label htmlFor="item-name">New Name</Label> + <Input + id="item-name" + value={dialogValue} + onChange={(e) => setDialogValue(e.target.value)} + placeholder={ + selectedFile?.type === 'file' + ? selectedFile.name.substring(0, selectedFile.name.lastIndexOf('.')) + : selectedFile?.name + } + onKeyDown={(e) => { + if (e.key === 'Enter') { + renameItem(); + } + }} + /> + {selectedFile?.type === 'file' && ( + <p className="text-sm text-muted-foreground mt-1"> + Extension: {selectedFile.name.substring(selectedFile.name.lastIndexOf('.'))} + </p> + )} + </div> + + <DialogFooter> + <Button + variant="outline" + onClick={() => { + setRenameDialogOpen(false); + setSelectedFile(null); + setDialogValue(''); + }} + > + Cancel + </Button> + <Button onClick={renameItem}>Rename</Button> + </DialogFooter> + </DialogContent> + </Dialog> + + {/* Category Change Dialog (for folders) */} + <Dialog open={categoryDialogOpen} onOpenChange={setCategoryDialogOpen}> + <DialogContent> + <DialogHeader> + <DialogTitle>Change Category</DialogTitle> + <DialogDescription> + Changing category for {selectedFile?.name} folder. + </DialogDescription> + </DialogHeader> + + <div className="space-y-4"> + <div> + <Label>New Category</Label> + <div className="mt-2 space-y-2"> + {Object.entries(categoryConfig).map(([key, config]) => ( + <div + key={key} + className={cn( + "flex items-center p-3 rounded-lg border cursor-pointer transition-colors", + newCategory === key && "bg-accent border-primary" + )} + onClick={() => setNewCategory(key)} + > + <config.icon className={cn("h-5 w-5 mr-3", config.color)} /> + <div className="flex-1"> + <div className="font-medium">{config.label}</div> + <div className="text-sm text-muted-foreground"> + {key === 'public' && 'External users can access freely'} + {key === 'restricted' && 'External users can only view'} + {key === 'confidential' && 'External users cannot access'} + {key === 'internal' && 'Internal use only'} + </div> + </div> + </div> + ))} + </div> + </div> + + {selectedFile?.type === 'folder' && ( + <div className="flex items-center space-x-2"> + <Switch + id="apply-to-children" + checked={applyToChildren} + onCheckedChange={setApplyToChildren} + /> + <Label htmlFor="apply-to-children"> + Apply to all files and subfolders + </Label> + </div> + )} + </div> + + <DialogFooter> + <Button + variant="outline" + onClick={() => { + setCategoryDialogOpen(false); + setSelectedFile(null); + setApplyToChildren(false); + }} + > + Cancel + </Button> + <Button + onClick={() => { + if (selectedFile) { + changeCategory(selectedFile.id, newCategory, applyToChildren); + setCategoryDialogOpen(false); + setSelectedFile(null); + setApplyToChildren(false); + } + }} + > + Change </Button> </DialogFooter> </DialogContent> </Dialog> + + {/* Secure Document Viewer Dialog */} + <Dialog + open={viewerDialogOpen} + onOpenChange={(open) => { + if (!open) { + setViewerDialogOpen(false); + setViewerFileUrl(null); + setSelectedFile(null); + } + }} + > + <DialogContent className="max-w-[90vw] max-h-[90vh] w-full h-full p-0"> + <DialogHeader className="px-6 py-4 border-b"> + <DialogTitle className="flex items-center justify-between"> + <div className="flex items-center gap-2"> + <Eye className="h-5 w-5" /> + Secure Document Viewer + </div> + <div className="flex items-center gap-2 text-sm text-muted-foreground"> + <Lock className="h-4 w-4" /> + View Only Mode + </div> + </DialogTitle> + <DialogDescription> + <div className="flex items-center justify-between mt-2"> + <span>Viewing: {selectedFile?.name}</span> + <Badge variant="destructive" className="text-xs"> + <AlertCircle className="h-3 w-3 mr-1" /> + Protected Document - No Download/Copy/Print + </Badge> + </div> + </DialogDescription> + </DialogHeader> + + <div className="relative flex-1 h-[calc(90vh-120px)]"> + {viewerFileUrl && selectedFile && ( + <SecurePDFViewer + documentUrl={viewerFileUrl} + fileName={selectedFile.name} + onClose={() => { + setViewerDialogOpen(false); + setViewerFileUrl(null); + setSelectedFile(null); + }} + /> + )} + </div> + + <div className="px-6 py-3 border-t bg-muted/50"> + <div className="flex items-center justify-between text-xs text-muted-foreground"> + <div className="flex items-center gap-4"> + <span>Viewer: {session?.user?.email}</span> + <span>Time: {new Date().toLocaleString()}</span> + <span>IP logged for security</span> + </div> + <Button + size="sm" + variant="outline" + onClick={() => { + setViewerDialogOpen(false); + setViewerFileUrl(null); + setSelectedFile(null); + }} + > + <X className="h-4 w-4 mr-1" /> + Close Viewer + </Button> + </div> + </div> + </DialogContent> + </Dialog> </div> ); }
\ No newline at end of file |
