Merge branch 'fe/refactor/upload' into 'fe/develop'
Feat: 컨텍스트 메뉴 구현 리팩토링 필요 See merge request s11-s-project/S11P21S002!297
This commit is contained in:
commit
86854c207e
21
frontend/package-lock.json
generated
21
frontend/package-lock.json
generated
@ -31,6 +31,7 @@
|
||||
"konva": "^9.3.14",
|
||||
"lucide-react": "^0.436.0",
|
||||
"react": "^18.3.1",
|
||||
"react-contexify": "^6.0.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@ -12804,6 +12805,26 @@
|
||||
"react": "^16.3.0 || ^17.0.1 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-contexify": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-contexify/-/react-contexify-6.0.0.tgz",
|
||||
"integrity": "sha512-jMhz6yZI81Jv3UDj7TXqCkhdkCFEEmvwGCPXsQuA2ZUC8EbCuVQ6Cy8FzKMXa0y454XTDClBN2YFvvmoFlrFkg==",
|
||||
"dependencies": {
|
||||
"clsx": "^1.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16",
|
||||
"react-dom": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/react-contexify/node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dnd": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
|
||||
|
@ -37,6 +37,7 @@
|
||||
"konva": "^9.3.14",
|
||||
"lucide-react": "^0.436.0",
|
||||
"react": "^18.3.1",
|
||||
"react-contexify": "^6.0.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
@ -4,13 +4,8 @@ import { FolderRequest, FolderResponse } from '@/types';
|
||||
export async function getFolder(projectId: string, folderId: number) {
|
||||
return api.get<FolderResponse>(`/projects/${projectId}/folders/${folderId}`).then(({ data }) => data);
|
||||
}
|
||||
|
||||
export async function updateFolder(projectId: number, folderId: number, memberId: number, folderData: FolderRequest) {
|
||||
return api
|
||||
.put(`/projects/${projectId}/folders/${folderId}`, folderData, {
|
||||
params: { memberId },
|
||||
})
|
||||
.then(({ data }) => data);
|
||||
export async function updateFolder(projectId: number, folderId: number, folderData: FolderRequest) {
|
||||
return api.put(`/projects/${projectId}/folders/${folderId}`, folderData).then(({ data }) => data);
|
||||
}
|
||||
|
||||
export async function deleteFolder(projectId: number, folderId: number, memberId: number) {
|
||||
@ -21,14 +16,9 @@ export async function deleteFolder(projectId: number, folderId: number, memberId
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
|
||||
export async function createFolder(projectId: number, memberId: number, folderData: FolderRequest) {
|
||||
return api
|
||||
.post(`/projects/${projectId}/folders`, folderData, {
|
||||
params: { memberId },
|
||||
})
|
||||
.then(({ data }) => data);
|
||||
export async function createFolder(projectId: number, folderData: FolderRequest) {
|
||||
return api.post(`/projects/${projectId}/folders`, folderData).then(({ data }) => data);
|
||||
}
|
||||
|
||||
export async function getFolderReviewList(projectId: number, folderId: number, memberId: number) {
|
||||
return api
|
||||
.get(`/projects/${projectId}/folders/${folderId}/review`, {
|
||||
|
@ -10,10 +10,8 @@ export async function moveImage(projectId: number, folderId: number, imageId: nu
|
||||
return api.put(`/projects/${projectId}/folders/${folderId}/images/${imageId}`, moveRequest);
|
||||
}
|
||||
|
||||
export async function deleteImage(imageId: number, memberId: number) {
|
||||
return api.delete(`/images/${imageId}`, {
|
||||
params: { memberId },
|
||||
});
|
||||
export async function deleteImage(projectId: number, folderId: number, imageId: number) {
|
||||
return api.delete(`/projects/${projectId}/folders/${folderId}/images/${imageId}`);
|
||||
}
|
||||
|
||||
export async function changeImageStatus(
|
||||
|
222
frontend/src/components/WorkspaceSidebar/ProjectContextMenu.tsx
Normal file
222
frontend/src/components/WorkspaceSidebar/ProjectContextMenu.tsx
Normal file
@ -0,0 +1,222 @@
|
||||
import React from 'react';
|
||||
import { Menu, Item, useContextMenu, ItemParams } from 'react-contexify';
|
||||
import 'react-contexify/ReactContexify.css';
|
||||
import ImagePreSignedForm from '../ImagePreSignedForm';
|
||||
import useDeleteImageQuery from '@/queries/images/useDeleteImageQuery';
|
||||
import useDeleteFolderQuery from '@/queries/folders/useDeleteFolderQuery';
|
||||
import useUpdateFolderQuery from '@/queries/folders/useUpdateFolderQuery';
|
||||
import useCreateFolderQuery from '@/queries/folders/useCreateFolderQuery';
|
||||
import { FolderRequest } from '@/types';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
|
||||
import useAuthStore from '@/stores/useAuthStore';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
interface ProjectContextMenuProps {
|
||||
projectId: number;
|
||||
folderId: number;
|
||||
node?: { id: number; type: 'folder' | 'image'; name?: string };
|
||||
onRefetch: () => void;
|
||||
}
|
||||
|
||||
const MENU_ID = 'project-menu';
|
||||
|
||||
export default function ProjectContextMenu({ projectId, folderId, node, onRefetch }: ProjectContextMenuProps) {
|
||||
const [isOpenUpload, setIsOpenUpload] = React.useState<boolean>(false);
|
||||
const [uploadType, setUploadType] = React.useState<'file' | 'folder' | 'zip'>('file');
|
||||
const [fileCount, setFileCount] = React.useState<number>(0);
|
||||
|
||||
const { profile } = useAuthStore();
|
||||
const memberId = profile?.id ?? 0;
|
||||
|
||||
const deleteImageMutation = useDeleteImageQuery();
|
||||
const deleteFolderMutation = useDeleteFolderQuery();
|
||||
const updateFolderMutation = useUpdateFolderQuery();
|
||||
const createFolderMutation = useCreateFolderQuery();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { show } = useContextMenu({ id: MENU_ID });
|
||||
|
||||
const handleContextMenu = (event: React.MouseEvent) => {
|
||||
show({ event });
|
||||
};
|
||||
|
||||
const handleItemClick = ({ id }: ItemParams) => {
|
||||
switch (id) {
|
||||
case 'rename':
|
||||
handleRename();
|
||||
break;
|
||||
case 'delete':
|
||||
handleDelete();
|
||||
break;
|
||||
case 'createFolder':
|
||||
handleCreateFolder();
|
||||
break;
|
||||
case 'uploadFile':
|
||||
setUploadType('file');
|
||||
setIsOpenUpload(true);
|
||||
break;
|
||||
case 'uploadFolder':
|
||||
setUploadType('folder');
|
||||
setIsOpenUpload(true);
|
||||
break;
|
||||
case 'uploadZip':
|
||||
setUploadType('zip');
|
||||
setIsOpenUpload(true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileCount = (count: number) => {
|
||||
setFileCount(count);
|
||||
};
|
||||
|
||||
const handleRename = () => {
|
||||
if (node?.type === 'folder') {
|
||||
const newName = prompt('폴더의 새 이름을 입력하세요:', node.name);
|
||||
if (newName && node.id) {
|
||||
updateFolderMutation.mutate(
|
||||
{
|
||||
projectId,
|
||||
folderId: node.id,
|
||||
folderData: {
|
||||
title: newName,
|
||||
parentId: folderId,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['folder', projectId, folderId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['folder', projectId, node.id] });
|
||||
onRefetch();
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (node?.type === 'folder') {
|
||||
deleteFolderMutation.mutate(
|
||||
{ projectId, folderId: node.id, memberId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['folder', projectId, folderId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['project', projectId] });
|
||||
onRefetch();
|
||||
},
|
||||
}
|
||||
);
|
||||
} else if (node?.type === 'image') {
|
||||
deleteImageMutation.mutate(
|
||||
{ projectId, folderId, imageId: node.id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['folder', projectId, folderId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['image', node.id] });
|
||||
queryClient.invalidateQueries({ queryKey: ['project', projectId] });
|
||||
onRefetch();
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateFolder = () => {
|
||||
const newFolderName = prompt('새 폴더의 이름을 입력하세요:');
|
||||
if (newFolderName) {
|
||||
createFolderMutation.mutate(
|
||||
{
|
||||
projectId,
|
||||
folderData: { title: newFolderName, parentId: node?.id || folderId } as FolderRequest,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
console.log(folderId, node?.id);
|
||||
queryClient.invalidateQueries({ queryKey: ['folder', projectId, folderId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['folder', projectId, node?.id] });
|
||||
|
||||
onRefetch();
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div onContextMenu={handleContextMenu}></div>
|
||||
|
||||
<Menu id={MENU_ID}>
|
||||
{node?.type === 'folder' && (
|
||||
<>
|
||||
<Item
|
||||
id="rename"
|
||||
onClick={handleItemClick}
|
||||
>
|
||||
폴더 이름 수정
|
||||
</Item>
|
||||
<Item
|
||||
id="delete"
|
||||
onClick={handleItemClick}
|
||||
>
|
||||
폴더 삭제
|
||||
</Item>
|
||||
<Item
|
||||
id="createFolder"
|
||||
onClick={handleItemClick}
|
||||
>
|
||||
새 폴더 생성
|
||||
</Item>
|
||||
<Item
|
||||
id="uploadFile"
|
||||
onClick={handleItemClick}
|
||||
>
|
||||
파일 업로드
|
||||
</Item>
|
||||
<Item
|
||||
id="uploadFolder"
|
||||
onClick={handleItemClick}
|
||||
>
|
||||
폴더 업로드
|
||||
</Item>
|
||||
<Item
|
||||
id="uploadZip"
|
||||
onClick={handleItemClick}
|
||||
>
|
||||
ZIP 파일 업로드
|
||||
</Item>
|
||||
</>
|
||||
)}
|
||||
{node?.type === 'image' && (
|
||||
<Item
|
||||
id="delete"
|
||||
onClick={handleItemClick}
|
||||
>
|
||||
이미지 삭제
|
||||
</Item>
|
||||
)}
|
||||
</Menu>
|
||||
|
||||
<Dialog
|
||||
open={isOpenUpload}
|
||||
onOpenChange={setIsOpenUpload}
|
||||
>
|
||||
<DialogTrigger asChild></DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader title={fileCount > 0 ? `업로드 (${fileCount})` : '업로드'} />
|
||||
<ImagePreSignedForm
|
||||
onClose={() => setIsOpenUpload(false)}
|
||||
onRefetch={onRefetch}
|
||||
onFileCount={handleFileCount}
|
||||
projectId={projectId}
|
||||
folderId={node?.id ?? folderId}
|
||||
uploadType={uploadType}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
@ -16,6 +16,10 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import useFolderQuery from '@/queries/folders/useFolderQuery';
|
||||
import MemoFileStatusIcon from './FileStatusIcon';
|
||||
import moveNodeInTree from '@/utils/moveNodeInTree';
|
||||
import ProjectContextMenu from './ProjectContextMenu';
|
||||
import { useContextMenu } from 'react-contexify';
|
||||
import 'react-contexify/ReactContexify.css';
|
||||
|
||||
interface FlatNode extends TreeNode {
|
||||
depth: number;
|
||||
isLeaf: boolean;
|
||||
@ -27,6 +31,8 @@ const ItemTypes = {
|
||||
NODE: 'node',
|
||||
};
|
||||
|
||||
const MENU_ID = 'project-menu';
|
||||
|
||||
export default function ProjectStructure({ project }: { project: Project }) {
|
||||
const { setProject, setCategories, setFolderId } = useProjectStore();
|
||||
const { image: selectedImage, setImage } = useCanvasStore();
|
||||
@ -39,6 +45,9 @@ export default function ProjectStructure({ project }: { project: Project }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [containerHeight, setContainerHeight] = useState<number>(400);
|
||||
|
||||
const { show } = useContextMenu({ id: MENU_ID });
|
||||
const [contextNode, setContextNode] = useState<FlatNode | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (categories) {
|
||||
setCategories(categories);
|
||||
@ -78,7 +87,7 @@ export default function ProjectStructure({ project }: { project: Project }) {
|
||||
(image: ImageResponse, parent?: FlatNode) => {
|
||||
setImage(image);
|
||||
if (parent) {
|
||||
setFolderId(Number(parent.id)); // 클릭된 이미지의 상위 폴더 ID 설정
|
||||
setFolderId(Number(parent.id));
|
||||
}
|
||||
},
|
||||
[setImage, setFolderId]
|
||||
@ -130,6 +139,12 @@ export default function ProjectStructure({ project }: { project: Project }) {
|
||||
[treeData, setTreeData, moveImageMutation, project.id]
|
||||
);
|
||||
|
||||
const handleContextMenu = (event: React.MouseEvent, node: FlatNode) => {
|
||||
event.preventDefault();
|
||||
setContextNode(node);
|
||||
show({ event });
|
||||
};
|
||||
|
||||
const Row = ({ index, style, data }: ListChildComponentProps<FlatNode[]>) => {
|
||||
const node = data[index];
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
@ -160,7 +175,9 @@ export default function ProjectStructure({ project }: { project: Project }) {
|
||||
...style,
|
||||
paddingLeft: `${node.depth * 12}px`,
|
||||
}}
|
||||
className={`caption } flex w-full items-center gap-2 rounded-md py-0.5 pr-1 ${node.imageData && selectedImage?.id === node.imageData?.id ? 'bg-gray-200' : 'hover:bg-gray-100'} ${isDragging ? 'opacity-50' : ''}`}
|
||||
className={`caption flex w-full items-center gap-2 rounded-md py-0.5 pr-1 ${
|
||||
node.imageData && selectedImage?.id === node.imageData?.id ? 'bg-gray-200' : 'hover:bg-gray-100'
|
||||
} ${isDragging ? 'opacity-50' : ''}`}
|
||||
onClick={() => {
|
||||
if (node.imageData) {
|
||||
handleImageClick(node.imageData as ImageResponse, node.parent);
|
||||
@ -168,6 +185,7 @@ export default function ProjectStructure({ project }: { project: Project }) {
|
||||
onToggle(node, !node.toggled);
|
||||
}
|
||||
}}
|
||||
onContextMenu={(event) => handleContextMenu(event, node)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{!node.imageData ? (
|
||||
@ -232,6 +250,19 @@ export default function ProjectStructure({ project }: { project: Project }) {
|
||||
<AutoLabelButton projectId={project.id} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProjectContextMenu
|
||||
projectId={project.id}
|
||||
folderId={contextNode?.parent?.id ? Number(contextNode.parent.id) : 0}
|
||||
node={
|
||||
contextNode && contextNode.imageData
|
||||
? { id: Number(contextNode.id), type: 'image', name: contextNode.name }
|
||||
: contextNode
|
||||
? { id: Number(contextNode.id), type: 'folder', name: contextNode.name }
|
||||
: undefined
|
||||
}
|
||||
onRefetch={refetch}
|
||||
/>
|
||||
</DndProvider>
|
||||
);
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ export default function useTreeData(projectId: string) {
|
||||
currentFolderId || 0,
|
||||
currentFolderId !== null
|
||||
);
|
||||
|
||||
const updateTreeData = useCallback((folder: FolderResponse, isRoot: boolean = false) => {
|
||||
if (!folder) return;
|
||||
|
||||
@ -42,7 +41,7 @@ export default function useTreeData(projectId: string) {
|
||||
const updateNode = (currentNode: TreeNode): TreeNode => {
|
||||
if (currentNode.id !== folder.id.toString()) {
|
||||
return currentNode.children
|
||||
? { ...currentNode, children: currentNode.children.map(updateNode) }
|
||||
? { ...currentNode, children: currentNode.children.map(updateNode).filter(Boolean) }
|
||||
: currentNode;
|
||||
}
|
||||
|
||||
|
@ -1,23 +1,14 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { createFolder } from '@/api/folderApi';
|
||||
import { FolderRequest } from '@/types';
|
||||
|
||||
interface CreateFolderMutationVariables {
|
||||
projectId: number;
|
||||
memberId: number;
|
||||
folderData: FolderRequest;
|
||||
}
|
||||
|
||||
export default function useCreateFolderQuery() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ projectId, memberId, folderData }: CreateFolderMutationVariables) =>
|
||||
createFolder(projectId, memberId, folderData),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['folderList', variables.projectId, variables.memberId],
|
||||
});
|
||||
},
|
||||
mutationFn: ({ projectId, folderData }: CreateFolderMutationVariables) => createFolder(projectId, folderData),
|
||||
});
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { deleteFolder } from '@/api/folderApi';
|
||||
|
||||
interface DeleteFolderMutationVariables {
|
||||
@ -8,15 +8,8 @@ interface DeleteFolderMutationVariables {
|
||||
}
|
||||
|
||||
export default function useDeleteFolderQuery() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ projectId, folderId, memberId }: DeleteFolderMutationVariables) =>
|
||||
deleteFolder(projectId, folderId, memberId),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['folderList', variables.projectId, variables.folderId],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -1,24 +1,16 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { updateFolder } from '@/api/folderApi';
|
||||
import { FolderRequest } from '@/types';
|
||||
|
||||
interface UpdateFolderMutationVariables {
|
||||
projectId: number;
|
||||
folderId: number;
|
||||
memberId: number;
|
||||
folderData: FolderRequest;
|
||||
}
|
||||
|
||||
export default function useUpdateFolderQuery() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ projectId, folderId, memberId, folderData }: UpdateFolderMutationVariables) =>
|
||||
updateFolder(projectId, folderId, memberId, folderData),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['folderList', variables.projectId, variables.folderId],
|
||||
});
|
||||
},
|
||||
mutationFn: ({ projectId, folderId, folderData }: UpdateFolderMutationVariables) =>
|
||||
updateFolder(projectId, folderId, folderData),
|
||||
});
|
||||
}
|
||||
|
@ -1,18 +1,15 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { deleteImage } from '@/api/imageApi';
|
||||
|
||||
interface DeleteImageMutationVariables {
|
||||
projectId: number;
|
||||
folderId: number;
|
||||
imageId: number;
|
||||
memberId: number;
|
||||
}
|
||||
|
||||
export default function useDeleteImageQuery() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ imageId, memberId }: DeleteImageMutationVariables) => deleteImage(imageId, memberId),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['image', variables.imageId] });
|
||||
},
|
||||
mutationFn: ({ projectId, folderId, imageId }: DeleteImageMutationVariables) =>
|
||||
deleteImage(projectId, folderId, imageId),
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user