summaryrefslogtreecommitdiff
path: root/components/file-manager
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-10-17 08:09:35 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-10-17 08:09:35 +0000
commitbea9853efe30c393b0d030bc552c1f5bbb838835 (patch)
tree820e12543f847bbcdc8d55b575016c535fb299c6 /components/file-manager
parent1540eac291761ffd8fc1947ed626e4e4a4407922 (diff)
(대표님) 데이터룸 관련 개발사항
Diffstat (limited to 'components/file-manager')
-rw-r--r--components/file-manager/FileManager.tsx874
1 files changed, 626 insertions, 248 deletions
diff --git a/components/file-manager/FileManager.tsx b/components/file-manager/FileManager.tsx
index f92f6b04..1af29e74 100644
--- a/components/file-manager/FileManager.tsx
+++ b/components/file-manager/FileManager.tsx
@@ -115,6 +115,7 @@ interface FileItem {
viewCount?: number;
parentId?: string | null;
children?: FileItem[];
+ filePath?: string;
}
interface UploadingFile {
@@ -151,6 +152,7 @@ const TreeItem: React.FC<{
onDelete: (ids: string[]) => void;
onShare: (item: FileItem) => void;
onRename: (item: FileItem) => void;
+ onCategoryChange: (item: FileItem) => void;
isInternalUser: boolean;
}> = ({
item,
@@ -166,6 +168,7 @@ const TreeItem: React.FC<{
onDelete,
onShare,
onRename,
+ onCategoryChange,
isInternalUser
}) => {
const hasChildren = item.type === 'folder' && item.children && item.children.length > 0;
@@ -184,113 +187,180 @@ const TreeItem: React.FC<{
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" />
+ <ContextMenu>
+ <ContextMenuTrigger>
+ <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>
)}
- </button>
- )}
- {item.type === 'file' && (
- <div className="w-5" />
- )}
- </div>
+ {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' && (
- <>
- <DropdownMenuItem onClick={() => onView(item)}>
- <Eye className="h-4 w-4 mr-2" />
- View
- </DropdownMenuItem>
- {item.permissions?.canDownload && (
- <DropdownMenuItem onClick={() => onDownload(item)}>
+ {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
+ Download Folder
</DropdownMenuItem>
)}
- </>
- )}
- {item.type === 'folder' && (
- <DropdownMenuItem onClick={() => onDownloadFolder(item)}>
+ {isInternalUser && (
+ <>
+ <DropdownMenuSeparator />
+ {item.permissions?.canEdit && (
+ <DropdownMenuItem onClick={() => onRename(item)}>
+ <Edit2 className="h-4 w-4 mr-2" />
+ Rename
+ </DropdownMenuItem>
+ )}
+ <DropdownMenuItem onClick={() => onCategoryChange(item)}>
+ <Shield className="h-4 w-4 mr-2" />
+ Change Category
+ </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>
+ </ContextMenuTrigger>
+
+ <ContextMenuContent>
+ {item.type === 'file' && (
+ <>
+ <ContextMenuItem onClick={() => onView(item)}>
+ <Eye className="h-4 w-4 mr-2" />
+ View
+ </ContextMenuItem>
+ {item.permissions?.canDownload && (
+ <ContextMenuItem onClick={() => onDownload(item)}>
+ <Download className="h-4 w-4 mr-2" />
+ Download
+ </ContextMenuItem>
+ )}
+ </>
+ )}
+
+ {item.type === 'folder' && (
+ <>
+ <ContextMenuItem onClick={() => onDoubleClick(item)}>
+ <Folder className="h-4 w-4 mr-2" />
+ {isExpanded ? 'Collapse' : 'Expand'}
+ </ContextMenuItem>
+ <ContextMenuItem onClick={() => onDownloadFolder(item)}>
<Download className="h-4 w-4 mr-2" />
Download Folder
- </DropdownMenuItem>
- )}
-
- {isInternalUser && (
- <>
+ </ContextMenuItem>
+ </>
+ )}
- {item.permissions?.canEdit && (
- <DropdownMenuItem onClick={() => onRename(item)}>
- <Edit2 className="h-4 w-4 mr-2" />
- Rename
- </DropdownMenuItem>
- )}
- </>
- )}
+ {isInternalUser && (
+ <>
+ <ContextMenuSeparator />
+ {item.permissions?.canEdit && (
+ <ContextMenuItem onClick={() => onRename(item)}>
+ <Edit2 className="h-4 w-4 mr-2" />
+ Rename
+ </ContextMenuItem>
+ )}
+ <ContextMenuItem onClick={() => onCategoryChange(item)}>
+ <Shield className="h-4 w-4 mr-2" />
+ Change Category
+ </ContextMenuItem>
+ </>
+ )}
- {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.permissions?.canDelete && (
+ <>
+ <ContextMenuSeparator />
+ <ContextMenuItem
+ className="text-destructive"
+ onClick={() => onDelete([item.id])}
+ >
+ <Trash2 className="h-4 w-4 mr-2" />
+ Delete
+ </ContextMenuItem>
+ </>
+ )}
+ </ContextMenuContent>
+ </ContextMenu>
{item.type === 'folder' && isExpanded && item.children && (
<div>
@@ -310,6 +380,7 @@ const TreeItem: React.FC<{
onDelete={onDelete}
onShare={onShare}
onRename={onRename}
+ onCategoryChange={onCategoryChange}
isInternalUser={isInternalUser}
/>
))}
@@ -331,12 +402,12 @@ export function FileManager({ projectId }: FileManagerProps) {
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 [selectedFilesForUpload, setSelectedFilesForUpload] = useState<File[]>([]);
const [uploadCategory, setUploadCategory] = useState<string>('confidential');
+ const [preserveFolderStructure, setPreserveFolderStructure] = useState(false);
// Dialog states
const [folderDialogOpen, setFolderDialogOpen] = useState(false);
@@ -345,6 +416,9 @@ export function FileManager({ projectId }: FileManagerProps) {
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
const [viewerDialogOpen, setViewerDialogOpen] = useState(false);
const [viewerFileUrl, setViewerFileUrl] = useState<string | null>(null);
+ const [categoryDialogOpen, setCategoryDialogOpen] = useState(false);
+ const [applyToChildren, setApplyToChildren] = useState(false);
+ const [newCategory, setNewCategory] = useState('confidential');
// Dialog data
const [dialogValue, setDialogValue] = useState('');
@@ -473,8 +547,8 @@ export function FileManager({ projectId }: FileManagerProps) {
}
};
- // Handle file upload
- const handleFileUpload = async (files: FileList | File[]) => {
+ // Handle file upload - 실제 업로드 처리
+ const handleFileUpload = async (files: File[]) => {
const fileArray = Array.from(files);
// Initialize uploading file list
@@ -486,63 +560,64 @@ export function FileManager({ projectId }: FileManagerProps) {
setUploadingFiles(newUploadingFiles);
- // Process each file upload
- for (let i = 0; i < fileArray.length; i++) {
- const file = fileArray[i];
-
- try {
- // Update status: uploading
- setUploadingFiles(prev => prev.map((f, idx) =>
- idx === i ? { ...f, status: 'uploading', progress: 20 } : f
- ));
-
- // DRM decryption
- setUploadingFiles(prev => prev.map((f, idx) =>
- idx === i ? { ...f, status: 'processing', progress: 40 } : f
- ));
-
- const decryptedData = await decryptWithServerAction(file);
-
- // 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()); // Pass file size
- if (currentParentId) {
- formData.append('parentId', currentParentId);
- }
-
- // Update upload progress
- setUploadingFiles(prev => prev.map((f, idx) =>
- idx === i ? { ...f, progress: 60 } : f
- ));
-
- // API call
- const response = await fetch(`/api/data-room/${projectId}/upload`, {
- method: 'POST',
- body: formData,
- });
-
- if (!response.ok) {
- const error = await response.json();
- throw new Error(error.error || 'Upload failed');
+ // 폴더 구조 보존 옵션이 켜져있고 상대 경로가 있는 경우
+ if (preserveFolderStructure && fileArray.some((file: any) => file.webkitRelativePath)) {
+ // 폴더 구조를 먼저 생성
+ const folderMap = new Map<string, string>(); // path -> folderId
+
+ for (let i = 0; i < fileArray.length; i++) {
+ const file = fileArray[i];
+ const relativePath = (file as any).webkitRelativePath;
+
+ if (relativePath) {
+ const pathParts = relativePath.split('/');
+ const folders = pathParts.slice(0, -1); // 파일명 제외
+
+ let currentFolderPath = '';
+ let parentId = currentParentId;
+
+ // 각 폴더를 순차적으로 생성
+ for (const folderName of folders) {
+ currentFolderPath = currentFolderPath ? `${currentFolderPath}/${folderName}` : folderName;
+
+ if (!folderMap.has(currentFolderPath)) {
+ // 폴더 생성 API 호출
+ try {
+ const response = await fetch(`/api/data-room/${projectId}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ name: folderName,
+ type: 'folder',
+ category: uploadCategory,
+ parentId: parentId,
+ }),
+ });
+
+ if (response.ok) {
+ const newFolder = await response.json();
+ folderMap.set(currentFolderPath, newFolder.id);
+ parentId = newFolder.id;
+ }
+ } catch (error) {
+ console.error('Failed to create folder:', folderName);
+ }
+ } else {
+ parentId = folderMap.get(currentFolderPath) || null;
+ }
+ }
+
+ // 폴더가 생성되었으면 해당 폴더에 파일 업로드
+ await uploadSingleFile(file, i, parentId);
+ } else {
+ // 상대 경로가 없으면 일반 업로드
+ await uploadSingleFile(file, i, currentParentId);
}
-
- // Success
- setUploadingFiles(prev => prev.map((f, idx) =>
- idx === i ? { ...f, status: 'completed', progress: 100 } : f
- ));
-
- } catch (error: any) {
- // Failure
- setUploadingFiles(prev => prev.map((f, idx) =>
- idx === i ? {
- ...f,
- status: 'error',
- error: error.message || 'Upload failed'
- } : f
- ));
+ }
+ } else {
+ // 일반 업로드 (폴더 구조 보존 없음)
+ for (let i = 0; i < fileArray.length; i++) {
+ await uploadSingleFile(fileArray[i], i, currentParentId);
}
}
@@ -550,7 +625,7 @@ export function FileManager({ projectId }: FileManagerProps) {
await fetchItems();
// Show toast if any files succeeded
- const successCount = newUploadingFiles.filter(f => f.status === 'completed').length;
+ const successCount = uploadingFiles.filter(f => f.status === 'completed').length;
if (successCount > 0) {
toast({
title: 'Upload Complete',
@@ -559,6 +634,72 @@ export function FileManager({ projectId }: FileManagerProps) {
}
};
+ // 단일 파일 업로드 함수
+ const uploadSingleFile = async (file: File, index: number, parentId: string | null) => {
+ try {
+ // Update status: uploading
+ setUploadingFiles(prev => prev.map((f, idx) =>
+ idx === index ? { ...f, status: 'uploading', progress: 20 } : f
+ ));
+
+ // DRM decryption
+ setUploadingFiles(prev => prev.map((f, idx) =>
+ idx === index ? { ...f, status: 'processing', progress: 40 } : f
+ ));
+
+ const decryptedData = await decryptWithServerAction(file);
+
+ // 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());
+ if (parentId) {
+ formData.append('parentId', parentId);
+ }
+
+ // Update upload progress
+ setUploadingFiles(prev => prev.map((f, idx) =>
+ idx === index ? { ...f, progress: 60 } : f
+ ));
+
+ // API call
+ const response = await fetch(`/api/data-room/${projectId}/upload`, {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || 'Upload failed');
+ }
+
+ // Success
+ setUploadingFiles(prev => prev.map((f, idx) =>
+ idx === index ? { ...f, status: 'completed', progress: 100 } : f
+ ));
+
+ } catch (error: any) {
+ // Failure
+ setUploadingFiles(prev => prev.map((f, idx) =>
+ idx === index ? {
+ ...f,
+ status: 'error',
+ error: error.message || 'Upload failed'
+ } : f
+ ));
+ }
+ };
+
+ // 업로드 시작 함수
+ const startUpload = async () => {
+ if (selectedFilesForUpload.length === 0) return;
+ await handleFileUpload(selectedFilesForUpload);
+ // 업로드 완료 후 선택된 파일 초기화
+ setSelectedFilesForUpload([]);
+ };
+
// Download folder
const downloadFolder = async (folder: FileItem) => {
if (folder.type !== 'folder') return;
@@ -620,14 +761,13 @@ export function FileManager({ projectId }: FileManagerProps) {
}
};
-
// Download multiple files
const downloadMultipleFiles = async (itemIds: string[]) => {
// 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'
+ item.permissions?.canDownload
);
if (filesToDownload.length === 0) {
@@ -712,15 +852,9 @@ export function FileManager({ projectId }: FileManagerProps) {
// View file with PDFTron
const viewFile = async (file: FileItem) => {
try {
-
-
-
- setViewerFileUrl(file.filePath);
+ setViewerFileUrl(file.filePath || '');
setSelectedFile(file);
setViewerDialogOpen(true);
-
-
-
} catch (error) {
toast({
title: 'Error',
@@ -858,11 +992,6 @@ export function FileManager({ projectId }: FileManagerProps) {
}
};
- // 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') {
@@ -952,7 +1081,7 @@ export function FileManager({ projectId }: FileManagerProps) {
onClick={() => {
// 현재 폴더의 카테고리를 기본값으로 설정
if (currentParentId) {
- const currentFolder = items.find(item => item.parentId === currentParentId);
+ const currentFolder = items.find(item => item.id === currentParentId);
if (currentFolder) {
setUploadCategory(currentFolder.category);
}
@@ -972,7 +1101,7 @@ export function FileManager({ projectId }: FileManagerProps) {
{items.filter(item =>
selectedItems.has(item.id) &&
item.type === 'file' &&
- item.permissions?.canDownload === 'true'
+ item.permissions?.canDownload
).length > 0 && (
<Button
size="sm"
@@ -1057,7 +1186,7 @@ export function FileManager({ projectId }: FileManagerProps) {
<div className="flex justify-center items-center h-64">
<div className="text-muted-foreground">Loading...</div>
</div>
- ) : filteredItems.length === 0 ? (
+ ) : filteredItems.length === 0 && filteredTreeItems.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">Empty</p>
@@ -1081,6 +1210,8 @@ export function FileManager({ projectId }: FileManagerProps) {
onDoubleClick={() => {
if (item.type === 'folder') {
handleFolderOpen(item);
+ } else {
+ viewFile(item);
}
}}
>
@@ -1114,7 +1245,72 @@ export function FileManager({ projectId }: FileManagerProps) {
</div>
</ContextMenuTrigger>
- {/* ... ContextMenuContent는 동일 ... */}
+ <ContextMenuContent>
+ {item.type === 'file' && (
+ <>
+ <ContextMenuItem onClick={() => viewFile(item)}>
+ <Eye className="h-4 w-4 mr-2" />
+ View
+ </ContextMenuItem>
+ {item.permissions?.canDownload && (
+ <ContextMenuItem onClick={() => downloadFile(item)}>
+ <Download className="h-4 w-4 mr-2" />
+ Download
+ </ContextMenuItem>
+ )}
+ </>
+ )}
+
+ {item.type === 'folder' && (
+ <>
+ <ContextMenuItem onClick={() => handleFolderOpen(item)}>
+ <Folder className="h-4 w-4 mr-2" />
+ Open
+ </ContextMenuItem>
+ <ContextMenuItem onClick={() => downloadFolder(item)}>
+ <Download className="h-4 w-4 mr-2" />
+ Download Folder
+ </ContextMenuItem>
+ </>
+ )}
+
+ {isInternalUser && (
+ <>
+ <ContextMenuSeparator />
+ {item.permissions?.canEdit && (
+ <ContextMenuItem onClick={() => {
+ setSelectedFile(item);
+ setDialogValue(item.name);
+ setRenameDialogOpen(true);
+ }}>
+ <Edit2 className="h-4 w-4 mr-2" />
+ Rename
+ </ContextMenuItem>
+ )}
+ <ContextMenuItem onClick={() => {
+ setSelectedFile(item);
+ setNewCategory(item.category);
+ setCategoryDialogOpen(true);
+ }}>
+ <Shield className="h-4 w-4 mr-2" />
+ Change Category
+ </ContextMenuItem>
+ </>
+ )}
+
+ {item.permissions?.canDelete && (
+ <>
+ <ContextMenuSeparator />
+ <ContextMenuItem
+ className="text-destructive"
+ onClick={() => deleteItems([item.id])}
+ >
+ <Trash2 className="h-4 w-4 mr-2" />
+ Delete
+ </ContextMenuItem>
+ </>
+ )}
+ </ContextMenuContent>
</ContextMenu>
);
})}
@@ -1145,6 +1341,11 @@ export function FileManager({ projectId }: FileManagerProps) {
setDialogValue(item.name);
setRenameDialogOpen(true);
}}
+ onCategoryChange={(item) => {
+ setSelectedFile(item);
+ setNewCategory(item.category);
+ setCategoryDialogOpen(true);
+ }}
isInternalUser={isInternalUser}
/>
))}
@@ -1154,13 +1355,20 @@ export function FileManager({ projectId }: FileManagerProps) {
</ScrollArea>
</div>
- {/* Upload Dialog */}
- <Dialog open={uploadDialogOpen} onOpenChange={setUploadDialogOpen}>
+ {/* Upload Dialog - 수정된 부분 */}
+ <Dialog open={uploadDialogOpen} onOpenChange={(open) => {
+ setUploadDialogOpen(open);
+ if (!open) {
+ setSelectedFilesForUpload([]);
+ setUploadingFiles([]);
+ setPreserveFolderStructure(false);
+ }
+ }}>
<DialogContent className="max-w-2xl max-h-[90vh] flex flex-col">
<DialogHeader>
<DialogTitle>Upload Files</DialogTitle>
<DialogDescription>
- Drag and drop files or click to select.
+ Drag and drop files or click to select. You can also select entire folders.
</DialogDescription>
</DialogHeader>
@@ -1176,15 +1384,12 @@ export function FileManager({ projectId }: FileManagerProps) {
<SelectContent>
{Object.entries(categoryConfig)
.filter(([key]) => {
- // 현재 폴더가 있는 경우
if (currentParentId) {
- const currentFolder = items.find(item => item.parentId === currentParentId);
- // 현재 폴더가 public이 아니면 public 옵션 제외
+ const currentFolder = items.find(item => item.id === currentParentId);
if (currentFolder && currentFolder.category !== 'public') {
return key !== 'public';
}
}
- // 루트 폴더이거나 현재 폴더가 public인 경우 모든 옵션 표시
return true;
})
.map(([key, config]) => (
@@ -1197,57 +1402,223 @@ export function FileManager({ projectId }: FileManagerProps) {
))}
</SelectContent>
</Select>
- {/* 현재 폴더 정보 표시 (선택사항) */}
- {currentParentId && (() => {
- const currentFolder = items.find(item => item.parentId === currentParentId);
- if (currentFolder && currentFolder.category !== 'public') {
- return (
- <p className="text-xs text-muted-foreground mt-1 flex items-center">
- <AlertCircle className="h-3 w-3 mr-1" />
- Current folder is {categoryConfig[currentFolder.category].label}.
- Public uploads are not allowed.
- </p>
- );
- }
- })()}
</div>
- {/* Dropzone */}
- <Dropzone
- onDrop={(acceptedFiles: File[]) => {
- handleFileUpload(acceptedFiles);
- }}
- accept={{
- 'application/pdf': ['.pdf'],
- 'application/msword': ['.doc'],
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
- 'application/vnd.ms-excel': ['.xls'],
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
- 'application/vnd.ms-powerpoint': ['.ppt'],
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'],
- 'text/plain': ['.txt'],
- 'text/csv': ['.csv'],
- 'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'],
- 'application/zip': ['.zip'],
- 'application/x-rar-compressed': ['.rar'],
- 'application/x-7z-compressed': ['.7z'],
- 'application/x-dwg': ['.dwg'],
- 'application/x-dxf': ['.dxf'],
- }}
- multiple={true}
- disabled={false}
- >
- <DropzoneZone className="h-48 border-2 border-dashed border-gray-300 rounded-lg">
- <DropzoneInput />
- <div className="flex flex-col items-center justify-center h-full">
- <DropzoneUploadIcon className="h-12 w-12 text-muted-foreground mb-4" />
- <DropzoneTitle>Drag files or click to upload</DropzoneTitle>
- <DropzoneDescription>Multiple files can be uploaded simultaneously</DropzoneDescription>
+ {/* Preserve Folder Structure Option - 폴더가 선택된 경우만 표시 */}
+ {selectedFilesForUpload.some((file: any) => file.webkitRelativePath) && (
+ <div className="flex items-center space-x-2 p-3 bg-muted rounded-lg">
+ <Switch
+ id="preserve-folder"
+ checked={preserveFolderStructure}
+ onCheckedChange={setPreserveFolderStructure}
+ />
+ <Label htmlFor="preserve-folder" className="cursor-pointer">
+ <div className="font-medium">Preserve folder structure</div>
+ <div className="text-xs text-muted-foreground">
+ Create folders and maintain the original directory structure
+ </div>
+ </Label>
+ </div>
+ )}
+
+ {/* Dropzone - 파일이 선택되지 않았을 때만 표시 */}
+ {selectedFilesForUpload.length === 0 && uploadingFiles.length === 0 && (
+ <div
+ className="h-48 border-2 border-dashed border-gray-300 rounded-lg flex flex-col items-center justify-center relative"
+ onDragOver={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ e.currentTarget.classList.add('border-primary', 'bg-accent');
+ }}
+ onDragLeave={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ e.currentTarget.classList.remove('border-primary', 'bg-accent');
+ }}
+ onDrop={async (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ e.currentTarget.classList.remove('border-primary', 'bg-accent');
+
+ const items = e.dataTransfer.items;
+ const files: File[] = [];
+ const filePromises: Promise<void>[] = [];
+
+ // 폴더 구조 감지를 위한 플래그
+ let hasFolderStructure = false;
+
+ // DataTransferItem을 통한 폴더 처리
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+
+ if (item.kind === 'file') {
+ const entry = item.webkitGetAsEntry();
+
+ if (entry) {
+ filePromises.push(
+ new Promise<void>(async (resolve) => {
+ const traverseFileTree = async (item: any, path: string = '') => {
+ if (item.isFile) {
+ await new Promise<void>((fileResolve) => {
+ item.file((file: File) => {
+ // 파일에 상대 경로 추가
+ Object.defineProperty(file, 'webkitRelativePath', {
+ value: path + file.name,
+ writable: false
+ });
+ files.push(file);
+ fileResolve();
+ });
+ });
+ } else if (item.isDirectory) {
+ hasFolderStructure = true;
+ const dirReader = item.createReader();
+ await new Promise<void>((dirResolve) => {
+ dirReader.readEntries(async (entries: any[]) => {
+ for (const entry of entries) {
+ await traverseFileTree(entry, path + item.name + '/');
+ }
+ dirResolve();
+ });
+ });
+ }
+ };
+
+ await traverseFileTree(entry);
+ resolve();
+ })
+ );
+ }
+ }
+ }
+
+ // 모든 파일 처리 완료 대기
+ await Promise.all(filePromises);
+
+ // 파일이 없으면 일반 파일 처리 (폴더가 아닌 경우)
+ if (files.length === 0) {
+ const droppedFiles = Array.from(e.dataTransfer.files);
+ if (droppedFiles.length > 0) {
+ setSelectedFilesForUpload(droppedFiles);
+ }
+ } else {
+ // 폴더 구조가 있으면 자동으로 옵션 활성화
+ if (hasFolderStructure) {
+ setPreserveFolderStructure(true);
+ }
+ setSelectedFilesForUpload(files);
+ }
+ }}
+ >
+ <input
+ type="file"
+ multiple
+ className="hidden"
+ id="file-input"
+ onChange={(e) => {
+ const files = Array.from(e.target.files || []);
+ setSelectedFilesForUpload(files);
+ }}
+ />
+ <DropzoneUploadIcon className="h-12 w-12 text-muted-foreground mb-4" />
+ <div className="text-center">
+ <p className="font-medium text-lg">Drag folders or files here</p>
+ <p className="text-sm text-muted-foreground mt-1">
+ Folders will maintain their structure
+ </p>
</div>
- </DropzoneZone>
- </Dropzone>
+ {/* <div className="mt-4 flex gap-2">
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => document.getElementById('file-input')?.click()}
+ >
+ <File className="h-4 w-4 mr-2" />
+ Browse Files
+ </Button>
+ <label htmlFor="folder-upload-btn" className="cursor-pointer">
+ <input
+ id="folder-upload-btn"
+ type="file"
+ // @ts-ignore
+ webkitdirectory=""
+ directory=""
+ multiple
+ className="hidden"
+ onChange={(e) => {
+ const files = Array.from(e.target.files || []);
+ setSelectedFilesForUpload(files);
+ if (files.length > 0 && (files[0] as any).webkitRelativePath) {
+ setPreserveFolderStructure(true);
+ }
+ }}
+ />
+ <Button
+ variant="outline"
+ size="sm"
+ type="button"
+ as="div"
+ >
+ <Folder className="h-4 w-4 mr-2" />
+ Browse Folder
+ </Button>
+ </label>
+ </div> */}
+ </div>
+ )}
- {/* Uploading File List */}
+ {/* Selected Files List - 업로드 전 */}
+ {selectedFilesForUpload.length > 0 && uploadingFiles.length === 0 && (
+ <div className="border rounded-lg p-4">
+ <div className="flex items-center justify-between mb-3">
+ <h4 className="font-medium text-sm">
+ Selected Files ({selectedFilesForUpload.length})
+ </h4>
+ <Button
+ size="sm"
+ variant="ghost"
+ onClick={() => {
+ setSelectedFilesForUpload([]);
+ setPreserveFolderStructure(false);
+ }}
+ >
+ <X className="h-4 w-4 mr-1" />
+ Clear All
+ </Button>
+ </div>
+ <div className="space-y-2 max-h-[300px] overflow-y-auto">
+ {selectedFilesForUpload.map((file, index) => {
+ const relativePath = (file as any).webkitRelativePath || file.name;
+ return (
+ <div key={index} className="flex items-center gap-3 p-2 bg-muted/50 rounded-md">
+ <File className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
+ <div className="flex-1 min-w-0">
+ <p className="text-sm truncate" title={relativePath}>
+ {relativePath}
+ </p>
+ <span className="text-xs text-muted-foreground">
+ {formatFileSize(file.size)}
+ </span>
+ </div>
+ <Button
+ size="sm"
+ variant="ghost"
+ onClick={() => {
+ setSelectedFilesForUpload(prev =>
+ prev.filter((_, i) => i !== index)
+ );
+ }}
+ >
+ <X className="h-3 w-3" />
+ </Button>
+ </div>
+ );
+ })}
+ </div>
+ </div>
+ )}
+
+ {/* Uploading File List - 업로드 중 */}
{uploadingFiles.length > 0 && (
<div className="border rounded-lg p-4 bg-muted/50">
<div className="flex items-center justify-between mb-3">
@@ -1290,19 +1661,6 @@ export function FileManager({ projectId }: FileManagerProps) {
<Progress value={uploadFile.progress} className="h-1.5 mt-2" />
)}
</div>
- {uploadFile.status === 'error' && (
- <Button
- size="sm"
- variant="ghost"
- onClick={() => {
- setUploadingFiles(prev =>
- prev.filter((_, i) => i !== index)
- );
- }}
- >
- <X className="h-4 w-4" />
- </Button>
- )}
</div>
))}
</div>
@@ -1316,11 +1674,30 @@ export function FileManager({ projectId }: FileManagerProps) {
variant="outline"
onClick={() => {
setUploadDialogOpen(false);
+ setSelectedFilesForUpload([]);
setUploadingFiles([]);
+ setPreserveFolderStructure(false);
}}
>
- Close
+ Cancel
</Button>
+ {/* 파일이 선택되었고 업로드 중이 아닐 때만 Upload 버튼 표시 */}
+ {selectedFilesForUpload.length > 0 && uploadingFiles.length === 0 && (
+ <Button onClick={startUpload}>
+ <Upload className="h-4 w-4 mr-2" />
+ Upload {selectedFilesForUpload.length} File{selectedFilesForUpload.length > 1 ? 's' : ''}
+ </Button>
+ )}
+ {/* 업로드 완료 후 Done 버튼 */}
+ {uploadingFiles.length > 0 && uploadingFiles.every(f => f.status === 'completed' || f.status === 'error') && (
+ <Button onClick={() => {
+ setUploadDialogOpen(false);
+ setUploadingFiles([]);
+ setSelectedFilesForUpload([]);
+ }}>
+ Done
+ </Button>
+ )}
</DialogFooter>
</DialogContent>
</Dialog>
@@ -1375,7 +1752,6 @@ export function FileManager({ projectId }: FileManagerProps) {
</DialogContent>
</Dialog>
-
{/* Rename Dialog */}
<Dialog open={renameDialogOpen} onOpenChange={setRenameDialogOpen}>
<DialogContent>
@@ -1429,13 +1805,13 @@ export function FileManager({ projectId }: FileManagerProps) {
</DialogContent>
</Dialog>
- {/* Category Change Dialog (for folders) */}
+ {/* Category Change Dialog */}
<Dialog open={categoryDialogOpen} onOpenChange={setCategoryDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Change Category</DialogTitle>
<DialogDescription>
- Changing category for {selectedFile?.name} folder.
+ Changing category for {selectedFile?.name}.
</DialogDescription>
</DialogHeader>
@@ -1506,7 +1882,9 @@ export function FileManager({ projectId }: FileManagerProps) {
<Button
onClick={() => {
if (selectedFile) {
- changeCategory(selectedFile.id, newCategory, applyToChildren);
+ changeCategory(selectedFile.id, newCategory,
+ selectedFile.type === 'folder' && newCategory !== 'public' ? true : applyToChildren
+ );
setCategoryDialogOpen(false);
setSelectedFile(null);
setApplyToChildren(false);