Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions src/api/fileTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,24 +77,22 @@ export const createFile = async (
return response.data;
};

// TODO: 파일 업로드 (외부 드래그앤드롭용). 추후 API 생성시 수정 필요
// 파일 업로드 (외부 드래그앤드롭용) - 백엔드 API 스펙에 맞게 수정
export const uploadFile = async (
repositoryId: number,
file: File,
parentPath?: string
parentId: number
): Promise<FileOperationResponse> => {
const formData = new FormData();
formData.append('file', file);
if (parentPath) {
formData.append('parentPath', parentPath);
}
formData.append('parentId', parentId.toString());

try {
console.log(`📤 파일 업로드 시작:`, {
repositoryId,
fileName: file.name,
fileSize: file.size,
parentPath: parentPath || '(루트)',
parentId,
url: `api/repositories/${repositoryId}/files/upload`,
});

Expand All @@ -111,6 +109,7 @@ export const uploadFile = async (
console.log('📤 파일 업로드 성공:', {
status: response.status,
fileName: file.name,
uploadedFile: response.data.data,
});

return response.data;
Expand Down
4 changes: 4 additions & 0 deletions src/features/Repo/fileTree/FileTree.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,10 @@
}
}

.dragMessage {
color: gray;
}

// 애니메이션들
@keyframes slide-up {
from {
Expand Down
72 changes: 65 additions & 7 deletions src/features/Repo/fileTree/FileTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const FileTree: React.FC<ExtendedFileTreeProps> = ({
} = useFileTreeOperations({
repositoryId: repositoryId || 0,
onSuccess: handleOperationSuccess,
rootFolderId: treeData?.[0]?.fileId,
rootFolderId: treeData?.[0]?.fileId || undefined,
});

// 내부 드래그앤드롭 훅
Expand All @@ -102,10 +102,13 @@ const FileTree: React.FC<ExtendedFileTreeProps> = ({
handleDragOver,
handleDragLeave,
handleDrop,
handleContainerDragOver,
handleContainerDrop,
isDragging,
isDropTarget,
getDropPosition,
canDrop,
isRootDropTarget,
} = useFileTreeDragDrop({
onMoveNode: moveItem,
});
Expand All @@ -122,6 +125,7 @@ const FileTree: React.FC<ExtendedFileTreeProps> = ({
handleNodeExternalDrop,
} = useFileTreeExternalDrop({
onFileUpload: uploadFiles,
rootFolderId: treeData?.find(node => node.parentId === null)?.fileId,
});

// 파일트리 데이터가 변경될 때마다 탭과 동기화
Expand Down Expand Up @@ -204,6 +208,46 @@ const FileTree: React.FC<ExtendedFileTreeProps> = ({
});
}, [repoId, repositoryId, enableCollaboration, treeData?.length, yMap, isLoading, error]);

// 외부 드래그 오버레이 관리
useEffect(() => {
const fileTreeContainer = document.querySelector('[data-file-tree-container]');
if (!fileTreeContainer) return;

const cleanupOverlay = () => {
const existingOverlay = fileTreeContainer.querySelector('.file-tree-drag-overlay');
if (existingOverlay) {
existingOverlay.remove();
}
};

if (externalDropState.isDragOver && !externalDropState.dropTarget) {
cleanupOverlay();

const overlay = document.createElement('div');
overlay.className = 'file-tree-drag-overlay';
overlay.style.cssText = `
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 3px dashed var(--filetree-external-drag-border);
border-radius: 12px;
background: transparent;
animation: external-drag-border-pulse 2s ease-in-out infinite;
pointer-events: none;
z-index: 1;
`;

fileTreeContainer.appendChild(overlay);
console.log('전체 영역 오버레이 생성됨');
} else {
cleanupOverlay();
}

return cleanupOverlay;
}, [externalDropState.isDragOver, externalDropState.dropTarget]);

// 전역 드래그 이벤트 방지
useEffect(() => {
const preventGlobalDrop = (e: DragEvent) => {
Expand Down Expand Up @@ -356,10 +400,7 @@ const FileTree: React.FC<ExtendedFileTreeProps> = ({
onDrop={(node, e) => handleDrop(node, e)}
getDropPosition={nodeId => getDropPosition(nodeId)}
// 외부 파일 드롭
isExternalDragOver={
externalDropState.dropTarget?.nodeId === nodeId ||
(externalDropState.isDragOver && !externalDropState.dropTarget)
}
isExternalDragOver={externalDropState.dropTarget?.nodeId === nodeId}
onExternalDragOver={(node, e) => handleNodeExternalDragOver(node, e)}
onExternalDragLeave={(node, e) => handleNodeExternalDragLeave(node, e)}
onExternalDrop={(node, e) => handleNodeExternalDrop(node, e)}
Expand All @@ -380,12 +421,19 @@ const FileTree: React.FC<ExtendedFileTreeProps> = ({
<div
className={clsx(styles.fileTree, className, {
[styles.collaborationMode]: enableCollaboration,
[styles.rootDropTarget]: isRootDropTarget,
})}
data-file-tree-container
onDragEnter={handleExternalDragEnter}
onDragOver={handleExternalDragOver}
onDragOver={e => {
handleExternalDragOver(e);
handleContainerDragOver(e);
}}
onDragLeave={handleExternalDragLeave}
onDrop={handleExternalDrop}
onDrop={e => {
handleExternalDrop(e);
handleContainerDrop(e);
}}
>
{/* 협업 상태 표시 */}
{renderCollaborationStatus()}
Expand All @@ -409,6 +457,16 @@ const FileTree: React.FC<ExtendedFileTreeProps> = ({
</div>
)}

{/* 최상위 폴더 드롭 피드백 */}
{isRootDropTarget && (
<div className={styles.rootDropOverlay}>
<div className={styles.rootDropMessage}>
<span className={styles.rootDropIcon}>📁</span>
<span>최상위 폴더로 이동</span>
</div>
</div>
)}

{/* 외부 드래그 피드백 */}
{isExternalDragActive() && (
<div className={styles.dragOverlay}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ const FileTreeItem: React.FC<FileTreeItemProps> = ({
onDrop={handleCombinedDrop}
// 최상단 레벨 여부를 data attribute로 전달
data-is-top-level={isTopLevel}
data-file-tree-item
title={
tabStatus.isOpen
? `${node.fileName}${tabStatus.isActive ? ' (활성 탭)' : ' (열린 탭)'}${tabStatus.isDirty ? ' (변경됨)' : ''}`
Expand Down
5 changes: 3 additions & 2 deletions src/features/Repo/fileTree/hooks/useFileTreeApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '@/api/fileTree';
import type { CreateFileRequest, MoveFileRequest, RenameFileRequest, FileTreeNode } from '../types';
import { useYjsFileTree } from '@/hooks/repo/useYjsFileTree';

// 파일 트리 조회
export const useFileTreeQuery = (repositoryId: number) => {
return useQuery<FileTreeNode[]>({
Expand Down Expand Up @@ -164,8 +165,8 @@ export const useUploadFileMutation = (repositoryId: number) => {
const { broadcastFileTreeUpdate } = useYjsFileTree(repositoryId);

return useMutation({
mutationFn: async ({ file, parentPath }: { file: File; parentPath?: string }) => {
const response = await uploadFile(repositoryId, file, parentPath);
mutationFn: async ({ file, parentId }: { file: File; parentId: number }) => {
const response = await uploadFile(repositoryId, file, parentId);
return response.data as FileTreeNode;
},
onSuccess: async (uploadedNode: FileTreeNode) => {
Expand Down
Loading