summaryrefslogtreecommitdiff
path: root/components/file-manager/FileManager.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-29 13:31:40 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-29 13:31:40 +0000
commit4614210aa9878922cfa1e424ce677ef893a1b6b2 (patch)
tree5e7edcce05fbee207230af0a43ed08cd351d7c4f /components/file-manager/FileManager.tsx
parente41e3af4e72870d44a94b03e0f3246d6ccaaca48 (diff)
(대표님) 구매 권한설정, data room 등
Diffstat (limited to 'components/file-manager/FileManager.tsx')
-rw-r--r--components/file-manager/FileManager.tsx1215
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