Feat: 프로젝트 structure 코드 분리
This commit is contained in:
parent
1d3c9c4a69
commit
e44d443088
@ -100,11 +100,13 @@ export default function ImageCanvas() {
|
|||||||
queryClient.invalidateQueries({ queryKey: ['folder', project!.id.toString(), folderId] });
|
queryClient.invalidateQueries({ queryKey: ['folder', project!.id.toString(), folderId] });
|
||||||
toast({
|
toast({
|
||||||
title: '저장 성공',
|
title: '저장 성공',
|
||||||
|
duration: 1500,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast({
|
toast({
|
||||||
title: '저장 실패',
|
title: '저장 실패',
|
||||||
|
duration: 1500,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -50,10 +50,10 @@ export default function AutoLabelButton({ projectId }: { projectId: number }) {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['folder', projectId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['folder', projectId.toString()] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['labelJson'] });
|
queryClient.invalidateQueries({ queryKey: ['labelJson'] });
|
||||||
toast({ title: '레이블링 성공' });
|
toast({ title: '레이블링 성공', duration: 1500 });
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast({ title: '레이블링 중 오류가 발생했습니다.' });
|
toast({ title: '레이블링 중 오류가 발생했습니다.', duration: 1500 });
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
41
frontend/src/components/WorkspaceSidebar/FileStatusIcon.tsx
Normal file
41
frontend/src/components/WorkspaceSidebar/FileStatusIcon.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { ImageStatus } from '@/types';
|
||||||
|
import { Minus, Loader, ArrowDownToLine, Send, CircleSlash, Check } from 'lucide-react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function FileStatusIcon({ imageStatus }: { imageStatus: ImageStatus }) {
|
||||||
|
return imageStatus === 'PENDING' ? (
|
||||||
|
<Minus
|
||||||
|
size={12}
|
||||||
|
className="shrink-0 stroke-gray-400"
|
||||||
|
/>
|
||||||
|
) : imageStatus === 'IN_PROGRESS' ? (
|
||||||
|
<Loader
|
||||||
|
size={12}
|
||||||
|
className="shrink-0 stroke-yellow-400"
|
||||||
|
/>
|
||||||
|
) : imageStatus === 'SAVE' ? (
|
||||||
|
<ArrowDownToLine
|
||||||
|
size={12}
|
||||||
|
className="shrink-0 stroke-gray-400"
|
||||||
|
/>
|
||||||
|
) : imageStatus === 'REVIEW_REQUEST' ? (
|
||||||
|
<Send
|
||||||
|
size={12}
|
||||||
|
className="shrink-0 stroke-blue-400"
|
||||||
|
/>
|
||||||
|
) : imageStatus === 'REVIEW_REJECT' ? (
|
||||||
|
<CircleSlash
|
||||||
|
size={12}
|
||||||
|
className="shrink-0 stroke-red-400"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Check
|
||||||
|
size={12}
|
||||||
|
className="shrink-0 stroke-green-400"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemoFileStatusIcon = React.memo(FileStatusIcon);
|
||||||
|
|
||||||
|
export default MemoFileStatusIcon;
|
@ -1,7 +1,8 @@
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { ImageResponse } from '@/types';
|
import { ImageResponse } from '@/types';
|
||||||
import { ArrowDownToLine, Check, CircleSlash, Image, Loader, Minus, Send } from 'lucide-react';
|
import { Image } from 'lucide-react';
|
||||||
import useCanvasStore from '@/stores/useCanvasStore';
|
import useCanvasStore from '@/stores/useCanvasStore';
|
||||||
|
import MemoFileStatusIcon from './FileStatusIcon';
|
||||||
|
|
||||||
export default function ProjectFileItem({
|
export default function ProjectFileItem({
|
||||||
className = '',
|
className = '',
|
||||||
@ -40,37 +41,7 @@ export default function ProjectFileItem({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="grow overflow-hidden text-ellipsis whitespace-nowrap text-left">{item.imageTitle}</span>
|
<span className="grow overflow-hidden text-ellipsis whitespace-nowrap text-left">{item.imageTitle}</span>
|
||||||
{item.status === 'PENDING' ? (
|
<MemoFileStatusIcon imageStatus={item.status} />
|
||||||
<Minus
|
|
||||||
size={12}
|
|
||||||
className="shrink-0 stroke-gray-400"
|
|
||||||
/>
|
|
||||||
) : item.status === 'IN_PROGRESS' ? (
|
|
||||||
<Loader
|
|
||||||
size={12}
|
|
||||||
className="shrink-0 stroke-yellow-400"
|
|
||||||
/>
|
|
||||||
) : item.status === 'SAVE' ? (
|
|
||||||
<ArrowDownToLine
|
|
||||||
size={12}
|
|
||||||
className="shrink-0 stroke-gray-400"
|
|
||||||
/>
|
|
||||||
) : item.status === 'REVIEW_REQUEST' ? (
|
|
||||||
<Send
|
|
||||||
size={12}
|
|
||||||
className="shrink-0 stroke-blue-400"
|
|
||||||
/>
|
|
||||||
) : item.status === 'REVIEW_REJECT' ? (
|
|
||||||
<CircleSlash
|
|
||||||
size={12}
|
|
||||||
className="shrink-0 stroke-red-400"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Check
|
|
||||||
size={12}
|
|
||||||
className="shrink-0 stroke-green-400"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,13 +8,13 @@ import useMoveImageQuery from '@/queries/images/useMoveImageQuery';
|
|||||||
import { Project, ImageResponse } from '@/types';
|
import { Project, ImageResponse } from '@/types';
|
||||||
import WorkspaceDropdownMenu from '../WorkspaceDropdownMenu';
|
import WorkspaceDropdownMenu from '../WorkspaceDropdownMenu';
|
||||||
import AutoLabelButton from './AutoLabelButton';
|
import AutoLabelButton from './AutoLabelButton';
|
||||||
import { Folder, Image as ImageIcon, Minus, Loader, ArrowDownToLine, Send, CircleSlash, Check } from 'lucide-react';
|
import { Folder, Image as ImageIcon } from 'lucide-react';
|
||||||
import { Spinner } from '../ui/spinner';
|
import { Spinner } from '../ui/spinner';
|
||||||
import { ImageStatus } from '@/types';
|
|
||||||
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
||||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import useFolderQuery from '@/queries/folders/useFolderQuery';
|
import useFolderQuery from '@/queries/folders/useFolderQuery';
|
||||||
|
import MemoFileStatusIcon from './FileStatusIcon';
|
||||||
|
|
||||||
interface FlatNode extends TreeNode {
|
interface FlatNode extends TreeNode {
|
||||||
depth: number;
|
depth: number;
|
||||||
@ -57,26 +57,19 @@ export default function ProjectStructure({ project }: { project: Project }) {
|
|||||||
|
|
||||||
const onToggle = useCallback(
|
const onToggle = useCallback(
|
||||||
async (node: TreeNode, toggled: boolean) => {
|
async (node: TreeNode, toggled: boolean) => {
|
||||||
if (!node.imageData) {
|
if (node.imageData) return;
|
||||||
if (toggled && (!node.children || node.children.length === 0)) {
|
if (toggled && (!node.children || node.children.length === 0)) {
|
||||||
await fetchNodeData(node);
|
await fetchNodeData(node);
|
||||||
}
|
|
||||||
|
|
||||||
const updateNode = (currentNode: TreeNode): TreeNode => {
|
|
||||||
if (currentNode.id === node.id) {
|
|
||||||
return { ...currentNode, toggled };
|
|
||||||
}
|
|
||||||
if (currentNode.children) {
|
|
||||||
return {
|
|
||||||
...currentNode,
|
|
||||||
children: currentNode.children.map(updateNode),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return currentNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
setTreeData((prevData) => prevData && updateNode(prevData));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateNode = (currentNode: TreeNode): TreeNode => {
|
||||||
|
if (currentNode.id === node.id) {
|
||||||
|
return { ...currentNode, toggled };
|
||||||
|
}
|
||||||
|
return currentNode.children ? { ...currentNode, children: currentNode.children.map(updateNode) } : currentNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
setTreeData((prevData) => prevData && updateNode(prevData));
|
||||||
},
|
},
|
||||||
[fetchNodeData, setTreeData]
|
[fetchNodeData, setTreeData]
|
||||||
);
|
);
|
||||||
@ -91,59 +84,6 @@ export default function ProjectStructure({ project }: { project: Project }) {
|
|||||||
[setImage, setFolderId]
|
[setImage, setFolderId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderStatusIcon = (status: ImageStatus) => {
|
|
||||||
const iconProps = { size: 12, className: 'shrink-0' };
|
|
||||||
const iconColor = {
|
|
||||||
PENDING: 'stroke-gray-400',
|
|
||||||
IN_PROGRESS: 'animate-spin stroke-yellow-400',
|
|
||||||
SAVE: 'stroke-gray-400',
|
|
||||||
REVIEW_REQUEST: 'stroke-blue-400',
|
|
||||||
REVIEW_REJECT: 'stroke-red-400',
|
|
||||||
COMPLETED: 'stroke-green-400',
|
|
||||||
};
|
|
||||||
|
|
||||||
const iconMapping = {
|
|
||||||
PENDING: (
|
|
||||||
<Minus
|
|
||||||
{...iconProps}
|
|
||||||
className={`${iconProps.className} ${iconColor.PENDING}`}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
IN_PROGRESS: (
|
|
||||||
<Loader
|
|
||||||
{...iconProps}
|
|
||||||
className={`${iconProps.className} ${iconColor.IN_PROGRESS}`}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
SAVE: (
|
|
||||||
<ArrowDownToLine
|
|
||||||
{...iconProps}
|
|
||||||
className={`${iconProps.className} ${iconColor.SAVE}`}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
REVIEW_REQUEST: (
|
|
||||||
<Send
|
|
||||||
{...iconProps}
|
|
||||||
className={`${iconProps.className} ${iconColor.REVIEW_REQUEST}`}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
REVIEW_REJECT: (
|
|
||||||
<CircleSlash
|
|
||||||
{...iconProps}
|
|
||||||
className={`${iconProps.className} ${iconColor.REVIEW_REJECT}`}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
COMPLETED: (
|
|
||||||
<Check
|
|
||||||
{...iconProps}
|
|
||||||
className={`${iconProps.className} ${iconColor.COMPLETED}`}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
return iconMapping[status] || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const flattenTree = useCallback((nodes: TreeNode[], depth: number = 0, parent?: FlatNode): FlatNode[] => {
|
const flattenTree = useCallback((nodes: TreeNode[], depth: number = 0, parent?: FlatNode): FlatNode[] => {
|
||||||
let flatList: FlatNode[] = [];
|
let flatList: FlatNode[] = [];
|
||||||
|
|
||||||
@ -187,47 +127,34 @@ export default function ProjectStructure({ project }: { project: Project }) {
|
|||||||
return { ...node, children: newChildren };
|
return { ...node, children: newChildren };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.children) {
|
return node.children ? { ...node, children: node.children.map(moveNodeInTree) } : node;
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
children: node.children.map(moveNodeInTree),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return node;
|
|
||||||
})(treeData!);
|
})(treeData!);
|
||||||
|
|
||||||
setTreeData(updatedTreeData);
|
setTreeData(updatedTreeData);
|
||||||
|
|
||||||
if (dragItem.imageData) {
|
if (!dragItem.imageData) return;
|
||||||
moveImageMutation.mutate({
|
moveImageMutation.mutate({
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
folderId: Number(dragItem.parent?.id),
|
folderId: Number(dragItem.parent?.id),
|
||||||
imageId: dragItem.imageData.id,
|
imageId: dragItem.imageData.id,
|
||||||
moveRequest: {
|
moveRequest: {
|
||||||
moveFolderId: Number(hoverItem.parent?.id),
|
moveFolderId: Number(hoverItem.parent?.id),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[treeData, setTreeData, moveImageMutation, project.id]
|
[treeData, setTreeData, moveImageMutation, project.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
const Row = ({ index, style, data }: ListChildComponentProps<FlatNode[]>) => {
|
const Row = ({ index, style, data }: ListChildComponentProps<FlatNode[]>) => {
|
||||||
const node = data[index];
|
const node = data[index];
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
const [, drop] = useDrop({
|
const [, drop] = useDrop({
|
||||||
accept: ItemTypes.NODE,
|
accept: ItemTypes.NODE,
|
||||||
drop(item: FlatNode) {
|
drop(item: FlatNode) {
|
||||||
const dragItem = item;
|
if (item.id === node.id) return;
|
||||||
const hoverItem = node;
|
|
||||||
|
|
||||||
if (dragItem.id === hoverItem.id) {
|
moveNode(item, node);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
moveNode(dragItem, hoverItem);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -242,17 +169,13 @@ export default function ProjectStructure({ project }: { project: Project }) {
|
|||||||
drag(drop(ref));
|
drag(drop(ref));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
style={{
|
style={{
|
||||||
...style,
|
...style,
|
||||||
opacity: isDragging ? 0.5 : 1,
|
paddingLeft: `${node.depth * 12}px`,
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingLeft: `${node.depth * 20}px`,
|
|
||||||
cursor: 'pointer',
|
|
||||||
backgroundColor: node.imageData && selectedImage?.id === node.imageData.id ? '#e5e7eb' : 'transparent',
|
|
||||||
}}
|
}}
|
||||||
|
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={() => {
|
onClick={() => {
|
||||||
if (node.imageData) {
|
if (node.imageData) {
|
||||||
handleImageClick(node.imageData as ImageResponse, node.parent);
|
handleImageClick(node.imageData as ImageResponse, node.parent);
|
||||||
@ -261,7 +184,7 @@ export default function ProjectStructure({ project }: { project: Project }) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ marginRight: '5px' }}>
|
<div className="flex items-center">
|
||||||
{!node.imageData ? (
|
{!node.imageData ? (
|
||||||
<Folder
|
<Folder
|
||||||
size={16}
|
size={16}
|
||||||
@ -274,17 +197,18 @@ export default function ProjectStructure({ project }: { project: Project }) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span style={{ color: '#4a4a4a', flexGrow: 1 }}>{node.name}</span>
|
<span className="grow overflow-hidden text-ellipsis whitespace-nowrap text-left text-gray-900">
|
||||||
{node.imageData && <div style={{ marginRight: '20px' }}>{renderStatusIcon(node.imageData.status)}</div>}
|
{node.name}
|
||||||
</div>
|
</span>
|
||||||
|
{node.imageData && <MemoFileStatusIcon imageStatus={node.imageData.status} />}
|
||||||
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<div
|
<div
|
||||||
className="box-border flex h-full min-h-0 flex-col bg-gray-50 p-2"
|
className="box-border flex h-full min-h-0 flex-col overflow-x-hidden bg-gray-50"
|
||||||
style={{ overflowX: 'hidden' }}
|
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
>
|
>
|
||||||
<div className="flex h-full flex-col gap-2 overflow-hidden px-1 pb-2">
|
<div className="flex h-full flex-col gap-2 overflow-hidden px-1 pb-2">
|
||||||
@ -308,14 +232,13 @@ export default function ProjectStructure({ project }: { project: Project }) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<List
|
<List
|
||||||
height={Math.min(flatData.length * 40, containerHeight)}
|
height={Math.min(flatData.length * 20, containerHeight)}
|
||||||
itemCount={flatData.length}
|
itemCount={flatData.length}
|
||||||
itemSize={40}
|
itemSize={20}
|
||||||
width={'100%'}
|
width={'100%'}
|
||||||
itemData={flatData}
|
itemData={flatData}
|
||||||
itemKey={getItemKey}
|
itemKey={getItemKey}
|
||||||
className="flex-1 overflow-auto"
|
className="flex-1 overflow-x-hidden"
|
||||||
style={{ overflowX: 'hidden' }}
|
|
||||||
>
|
>
|
||||||
{Row}
|
{Row}
|
||||||
</List>
|
</List>
|
||||||
|
@ -4,7 +4,7 @@ import { ImageResponse, ChildFolder } from '@/types';
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { getFolder } from '@/api/folderApi';
|
import { getFolder } from '@/api/folderApi';
|
||||||
|
|
||||||
export function useFolder(projectId: string, folderId: number) {
|
function useFolder(projectId: string, folderId: number) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['folder', projectId, folderId],
|
queryKey: ['folder', projectId, folderId],
|
||||||
queryFn: () => getFolder(projectId, folderId),
|
queryFn: () => getFolder(projectId, folderId),
|
||||||
@ -12,7 +12,7 @@ export function useFolder(projectId: string, folderId: number) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useChildFolder(projectId: string, folderId: number, enabled: boolean) {
|
function useChildFolder(projectId: string, folderId: number, enabled: boolean) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['folder', projectId, folderId],
|
queryKey: ['folder', projectId, folderId],
|
||||||
queryFn: () => getFolder(projectId, folderId),
|
queryFn: () => getFolder(projectId, folderId),
|
||||||
@ -33,6 +33,7 @@ export default function useTreeData(projectId: string) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log('root changed');
|
||||||
if (rootFolder) {
|
if (rootFolder) {
|
||||||
const childFolders: TreeNode[] =
|
const childFolders: TreeNode[] =
|
||||||
rootFolder.children?.map((child: ChildFolder) => ({
|
rootFolder.children?.map((child: ChildFolder) => ({
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { LabelCategoryResponse, Project } from '@/types';
|
import { FolderResponse, LabelCategoryResponse, Project } from '@/types';
|
||||||
|
|
||||||
interface ProjectState {
|
interface ProjectState {
|
||||||
project: Project | null;
|
project: Project | null;
|
||||||
@ -8,6 +8,7 @@ interface ProjectState {
|
|||||||
setFolderId: (folderId: number) => void;
|
setFolderId: (folderId: number) => void;
|
||||||
categories: LabelCategoryResponse[];
|
categories: LabelCategoryResponse[];
|
||||||
setCategories: (categories: LabelCategoryResponse[]) => void;
|
setCategories: (categories: LabelCategoryResponse[]) => void;
|
||||||
|
projectFolder: FolderResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useProjectStore = create<ProjectState>((set) => ({
|
const useProjectStore = create<ProjectState>((set) => ({
|
||||||
@ -16,6 +17,12 @@ const useProjectStore = create<ProjectState>((set) => ({
|
|||||||
folderId: 0,
|
folderId: 0,
|
||||||
setFolderId: (folderId) => set({ folderId }),
|
setFolderId: (folderId) => set({ folderId }),
|
||||||
categories: [],
|
categories: [],
|
||||||
|
projectFolder: {
|
||||||
|
id: 0,
|
||||||
|
title: '',
|
||||||
|
children: [],
|
||||||
|
images: [],
|
||||||
|
},
|
||||||
setCategories: (categories) => set({ categories }),
|
setCategories: (categories) => set({ categories }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user