summaryrefslogtreecommitdiff
path: root/components/file-manager
diff options
context:
space:
mode:
Diffstat (limited to 'components/file-manager')
-rw-r--r--components/file-manager/CreateSubfolderForm.tsx127
-rw-r--r--components/file-manager/FileManager.tsx156
-rw-r--r--components/file-manager/SecurePDFViewer.tsx8
3 files changed, 262 insertions, 29 deletions
diff --git a/components/file-manager/CreateSubfolderForm.tsx b/components/file-manager/CreateSubfolderForm.tsx
new file mode 100644
index 00000000..0afdf755
--- /dev/null
+++ b/components/file-manager/CreateSubfolderForm.tsx
@@ -0,0 +1,127 @@
+// CreateSubfolderForm component - English version
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Loader2 } from "lucide-react";
+
+interface CreateSubfolderFormProps {
+ parentFolder: any;
+ onSubmit: (name: string, category: string) => Promise<void>;
+ onCancel: () => void;
+}
+
+export function CreateSubfolderForm({
+ parentFolder,
+ onSubmit,
+ onCancel
+}: CreateSubfolderFormProps) {
+ const [folderName, setFolderName] = useState("");
+ const [category, setCategory] = useState(parentFolder?.category || "general");
+ const [isCreating, setIsCreating] = useState(false);
+ const [error, setError] = useState("");
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!folderName.trim()) {
+ setError("Please enter a folder name");
+ return;
+ }
+
+ // Special character validation
+ const invalidChars = /[<>:"|?*\/\\]/;
+ if (invalidChars.test(folderName)) {
+ setError("Folder name contains invalid characters");
+ return;
+ }
+
+ setIsCreating(true);
+ setError("");
+
+ try {
+ await onSubmit(folderName, category);
+ setFolderName("");
+ setCategory(parentFolder?.category || "general");
+ } catch (error: any) {
+ setError(error.message || "Failed to create folder");
+ } finally {
+ setIsCreating(false);
+ }
+ };
+
+ return (
+ <form onSubmit={handleSubmit} className="space-y-4 py-4">
+ <div className="space-y-2">
+ <Label htmlFor="folder-name">Folder Name</Label>
+ <Input
+ id="folder-name"
+ value={folderName}
+ onChange={(e) => {
+ setFolderName(e.target.value);
+ setError("");
+ }}
+ placeholder="Enter folder name"
+ disabled={isCreating}
+ autoFocus
+ />
+ {error && (
+ <p className="text-sm text-destructive">{error}</p>
+ )}
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="folder-category">Security Category</Label>
+ <Select
+ value={category}
+ onValueChange={setCategory}
+ disabled={isCreating}
+ >
+ <SelectTrigger id="folder-category">
+ <SelectValue placeholder="Select category" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="general">General</SelectItem>
+ <SelectItem value="confidential">Confidential</SelectItem>
+ <SelectItem value="internal">Internal</SelectItem>
+ </SelectContent>
+ </Select>
+ <p className="text-xs text-muted-foreground">
+ Sub-folders inherit the security category from their parent folder by default
+ </p>
+ </div>
+
+ <div className="flex justify-end gap-2">
+ <Button
+ type="button"
+ variant="outline"
+ onClick={onCancel}
+ disabled={isCreating}
+ >
+ Cancel
+ </Button>
+ <Button
+ type="submit"
+ disabled={isCreating || !folderName.trim()}
+ >
+ {isCreating ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ Creating...
+ </>
+ ) : (
+ "Create Folder"
+ )}
+ </Button>
+ </div>
+ </form>
+ );
+} \ No newline at end of file
diff --git a/components/file-manager/FileManager.tsx b/components/file-manager/FileManager.tsx
index a95d8c06..c56bb16a 100644
--- a/components/file-manager/FileManager.tsx
+++ b/components/file-manager/FileManager.tsx
@@ -95,6 +95,7 @@ import { decryptWithServerAction } from '@/components/drm/drmUtils';
import { Progress } from '@/components/ui/progress';
// Import the secure viewer component
import { SecurePDFViewer } from './SecurePDFViewer';
+import { CreateSubfolderForm } from './CreateSubfolderForm';
interface FileItem {
id: string;
@@ -153,6 +154,7 @@ const TreeItem: React.FC<{
onShare: (item: FileItem) => void;
onRename: (item: FileItem) => void;
onCategoryChange: (item: FileItem) => void;
+ onCreateSubfolder: (item: FileItem) => void; // 추가
isInternalUser: boolean;
}> = ({
item,
@@ -169,6 +171,7 @@ const TreeItem: React.FC<{
onShare,
onRename,
onCategoryChange,
+ onCreateSubfolder, // 추가
isInternalUser
}) => {
const hasChildren = item.type === 'folder' && item.children && item.children.length > 0;
@@ -263,10 +266,19 @@ const TreeItem: React.FC<{
)}
{item.type === 'folder' && (
- <DropdownMenuItem onClick={() => onDownloadFolder(item)}>
- <Download className="h-4 w-4 mr-2" />
- Download Folder
- </DropdownMenuItem>
+ <>
+ {/* Create Sub-folder 추가 */}
+ {isInternalUser && (
+ <DropdownMenuItem onClick={() => onCreateSubfolder(item)}>
+ <FolderPlus className="h-4 w-4 mr-2" />
+ Create Sub-folder
+ </DropdownMenuItem>
+ )}
+ <DropdownMenuItem onClick={() => onDownloadFolder(item)}>
+ <Download className="h-4 w-4 mr-2" />
+ Download Folder
+ </DropdownMenuItem>
+ </>
)}
{isInternalUser && (
@@ -320,6 +332,13 @@ const TreeItem: React.FC<{
{item.type === 'folder' && (
<>
+ {/* Create Sub-folder 추가 (Context Menu) */}
+ {isInternalUser && (
+ <ContextMenuItem onClick={() => onCreateSubfolder(item)}>
+ <FolderPlus className="h-4 w-4 mr-2" />
+ Create Sub-folder
+ </ContextMenuItem>
+ )}
<ContextMenuItem onClick={() => onDoubleClick(item)}>
<Folder className="h-4 w-4 mr-2" />
{isExpanded ? 'Collapse' : 'Expand'}
@@ -381,6 +400,7 @@ const TreeItem: React.FC<{
onShare={onShare}
onRename={onRename}
onCategoryChange={onCategoryChange}
+ onCreateSubfolder={onCreateSubfolder} // 추가
isInternalUser={isInternalUser}
/>
))}
@@ -402,6 +422,59 @@ export function FileManager({ projectId }: FileManagerProps) {
const [searchQuery, setSearchQuery] = useState('');
const [loading, setLoading] = useState(false);
+ const [subfolderDialogOpen, setSubfolderDialogOpen] = useState(false);
+ const [selectedParentFolder, setSelectedParentFolder] = useState(null);
+ const [newFolderName, setNewFolderName] = useState('');
+ const [newFolderCategory, setNewFolderCategory] = useState('general');
+
+ const handleCreateSubfolder = (parentFolder) => {
+ setSelectedParentFolder(parentFolder);
+ setSubfolderDialogOpen(true);
+ };
+
+ const createSubfolder = async (name, category) => {
+ try {
+ const response = await fetch(`/api/data-room/${projectId}/folders`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ name,
+ parentId: selectedParentFolder?.id || null,
+ category: category || selectedParentFolder?.category || 'general',
+ }),
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || 'Failed to create folder');
+ }
+
+ const newFolder = await response.json();
+
+ // 기존 fetchItems 함수를 재사용하여 목록 새로고침
+ await fetchItems(); // 기존에 있던 fetchItems 함수 재사용
+
+ toast({
+ title: "Folder Created",
+ description: `Folder "${name}" has been created successfully.`,
+ });
+
+ setSubfolderDialogOpen(false);
+ setSelectedParentFolder(null);
+
+ return newFolder;
+ } catch (error) {
+ toast({
+ title: "Failed to Create Folder",
+ description: error.message || "An error occurred while creating the folder.",
+ variant: "destructive",
+ });
+ throw error;
+ }
+ };
+
// Upload states
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([]);
@@ -419,6 +492,10 @@ export function FileManager({ projectId }: FileManagerProps) {
const [categoryDialogOpen, setCategoryDialogOpen] = useState(false);
const [applyToChildren, setApplyToChildren] = useState(false);
const [newCategory, setNewCategory] = useState('confidential');
+ const [subfolderDialog, setSubfolderDialog] = useState<{
+ open: boolean;
+ parentFolder: FileItem | null;
+ }>({ open: false, parentFolder: null });
// Dialog data
const [dialogValue, setDialogValue] = useState('');
@@ -564,22 +641,22 @@ export function FileManager({ projectId }: FileManagerProps) {
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 {
@@ -593,7 +670,7 @@ export function FileManager({ projectId }: FileManagerProps) {
parentId: parentId,
}),
});
-
+
if (response.ok) {
const newFolder = await response.json();
folderMap.set(currentFolderPath, newFolder.id);
@@ -606,7 +683,7 @@ export function FileManager({ projectId }: FileManagerProps) {
parentId = folderMap.get(currentFolderPath) || null;
}
}
-
+
// 폴더가 생성되었으면 해당 폴더에 파일 업로드
await uploadSingleFile(file, i, parentId);
} else {
@@ -851,7 +928,7 @@ export function FileManager({ projectId }: FileManagerProps) {
// View file with PDFTron
const viewFile = async (file: FileItem) => {
- try {
+ try {
setViewerFileUrl(file.filePath || '');
setSelectedFile(file);
setViewerDialogOpen(true);
@@ -1051,17 +1128,17 @@ export function FileManager({ projectId }: FileManagerProps) {
// 재귀적으로 트리 항목 검색
const searchTreeItems = (items: FileItem[], query: string): FileItem[] => {
const result: FileItem[] = [];
-
+
for (const item of items) {
// 현재 항목이 검색어와 일치하는지 확인
const matches = item.name.toLowerCase().includes(query.toLowerCase());
-
+
// 하위 항목 재귀적으로 검색
let childrenMatches: FileItem[] = [];
if (item.children && item.children.length > 0) {
childrenMatches = searchTreeItems(item.children, query);
}
-
+
// 현재 항목이나 하위 항목 중 하나라도 일치하면 결과에 추가
if (matches || childrenMatches.length > 0) {
// 하위 항목이 일치하는 경우 현재 항목도 표시하기 위해 확장된 상태로 복제
@@ -1072,7 +1149,7 @@ export function FileManager({ projectId }: FileManagerProps) {
result.push(clonedItem);
}
}
-
+
return result;
};
@@ -1362,6 +1439,12 @@ export function FileManager({ projectId }: FileManagerProps) {
setSelectedFile(item);
setShareDialogOpen(true);
}}
+ onCreateSubfolder={(item) => {
+ setSelectedParentFolder(item);
+ setNewFolderName('');
+ setNewFolderCategory(item.category || 'general');
+ setSubfolderDialogOpen(true); // setCreateSubfolderDialogOpen 대신 setSubfolderDialogOpen 사용
+ }}
onRename={(item) => {
setSelectedFile(item);
setDialogValue(item.name);
@@ -1465,21 +1548,21 @@ export function FileManager({ projectId }: FileManagerProps) {
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) => {
@@ -1509,7 +1592,7 @@ export function FileManager({ projectId }: FileManagerProps) {
});
}
};
-
+
await traverseFileTree(entry);
resolve();
})
@@ -1517,10 +1600,10 @@ export function FileManager({ projectId }: FileManagerProps) {
}
}
}
-
+
// 모든 파일 처리 완료 대기
await Promise.all(filePromises);
-
+
// 파일이 없으면 일반 파일 처리 (폴더가 아닌 경우)
if (files.length === 0) {
const droppedFiles = Array.from(e.dataTransfer.files);
@@ -1778,6 +1861,29 @@ export function FileManager({ projectId }: FileManagerProps) {
</DialogContent>
</Dialog>
+ <Dialog open={subfolderDialogOpen} onOpenChange={setSubfolderDialogOpen}>
+ <DialogContent className="sm:max-w-[425px]">
+ <DialogHeader>
+ <DialogTitle>Create Sub-folder</DialogTitle>
+ <DialogDescription>
+ {selectedParentFolder ? (
+ <>
+ Create a new sub-folder under <span className="font-medium">{selectedParentFolder.name}</span>
+ </>
+ ) : (
+ "Create a new folder in the current directory"
+ )}
+ </DialogDescription>
+ </DialogHeader>
+
+ <CreateSubfolderForm
+ parentFolder={selectedParentFolder}
+ onSubmit={createSubfolder}
+ onCancel={() => setSubfolderDialogOpen(false)}
+ />
+ </DialogContent>
+ </Dialog>
+
{/* Rename Dialog */}
<Dialog open={renameDialogOpen} onOpenChange={setRenameDialogOpen}>
<DialogContent>
@@ -1908,7 +2014,7 @@ export function FileManager({ projectId }: FileManagerProps) {
<Button
onClick={() => {
if (selectedFile) {
- changeCategory(selectedFile.id, newCategory,
+ changeCategory(selectedFile.id, newCategory,
selectedFile.type === 'folder' && newCategory !== 'public' ? true : applyToChildren
);
setCategoryDialogOpen(false);
diff --git a/components/file-manager/SecurePDFViewer.tsx b/components/file-manager/SecurePDFViewer.tsx
index 704c4f24..0deb96b6 100644
--- a/components/file-manager/SecurePDFViewer.tsx
+++ b/components/file-manager/SecurePDFViewer.tsx
@@ -91,10 +91,6 @@ export function SecurePDFViewer({ documentUrl, fileName, category, onClose }: Se
'ribbons',
'toggleNotesButton'
]);
-
- const { Core } = instance;
- Core.Tools.Tool.disableAutoSwitch();
- Core.Tools.Tool.disableTextSelection();
// CSS 적용으로 추가 보안
const iframeWindow = instance.UI.iframeWindow;
@@ -241,6 +237,10 @@ export function SecurePDFViewer({ documentUrl, fileName, category, onClose }: Se
}
annotationManager.drawAnnotations(documentViewer.getCurrentPage());
+
+ const { Core } = instance;
+ Core.Tools.Tool.disableAutoSwitch();
+ Core.Tools.Tool.disableTextSelection();
}
// Pan 모드로 설정 (텍스트 선택 불가)