Feat: 프로젝트 structure 코드 분리

This commit is contained in:
jhynsoo 2024-10-04 11:20:21 +09:00
parent 1d3c9c4a69
commit e44d443088
7 changed files with 99 additions and 154 deletions

View File

@ -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,
}); });
}, },
} }

View File

@ -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 });
}, },
} }
); );

View 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;

View File

@ -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>
); );
} }

View File

@ -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>

View File

@ -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) => ({

View File

@ -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 }),
})); }));