Merge branch 'fe/feat/project-structure-seperate' into 'fe/develop'

Feat: 프로젝트 structure 코드 분리

See merge request s11-s-project/S11P21S002!279
This commit is contained in:
정현조 2024-10-04 11:53:55 +09:00
commit 2b3109341f
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] });
toast({
title: '저장 성공',
duration: 1500,
});
},
onError: () => {
toast({
title: '저장 실패',
duration: 1500,
});
},
}

View File

@ -50,10 +50,10 @@ export default function AutoLabelButton({ projectId }: { projectId: number }) {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['folder', projectId.toString()] });
queryClient.invalidateQueries({ queryKey: ['labelJson'] });
toast({ title: '레이블링 성공' });
toast({ title: '레이블링 성공', duration: 1500 });
},
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 { 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 MemoFileStatusIcon from './FileStatusIcon';
export default function ProjectFileItem({
className = '',
@ -40,37 +41,7 @@ export default function ProjectFileItem({
/>
</div>
<span className="grow overflow-hidden text-ellipsis whitespace-nowrap text-left">{item.imageTitle}</span>
{item.status === 'PENDING' ? (
<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"
/>
)}
<MemoFileStatusIcon imageStatus={item.status} />
</button>
);
}

View File

@ -8,13 +8,13 @@ import useMoveImageQuery from '@/queries/images/useMoveImageQuery';
import { Project, ImageResponse } from '@/types';
import WorkspaceDropdownMenu from '../WorkspaceDropdownMenu';
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 { ImageStatus } from '@/types';
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import useFolderQuery from '@/queries/folders/useFolderQuery';
import MemoFileStatusIcon from './FileStatusIcon';
interface FlatNode extends TreeNode {
depth: number;
@ -57,7 +57,7 @@ export default function ProjectStructure({ project }: { project: Project }) {
const onToggle = useCallback(
async (node: TreeNode, toggled: boolean) => {
if (!node.imageData) {
if (node.imageData) return;
if (toggled && (!node.children || node.children.length === 0)) {
await fetchNodeData(node);
}
@ -66,17 +66,10 @@ export default function ProjectStructure({ project }: { project: Project }) {
if (currentNode.id === node.id) {
return { ...currentNode, toggled };
}
if (currentNode.children) {
return {
...currentNode,
children: currentNode.children.map(updateNode),
};
}
return currentNode;
return currentNode.children ? { ...currentNode, children: currentNode.children.map(updateNode) } : currentNode;
};
setTreeData((prevData) => prevData && updateNode(prevData));
}
},
[fetchNodeData, setTreeData]
);
@ -91,59 +84,6 @@ export default function ProjectStructure({ project }: { project: Project }) {
[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[] => {
let flatList: FlatNode[] = [];
@ -187,19 +127,12 @@ export default function ProjectStructure({ project }: { project: Project }) {
return { ...node, children: newChildren };
}
if (node.children) {
return {
...node,
children: node.children.map(moveNodeInTree),
};
}
return node;
return node.children ? { ...node, children: node.children.map(moveNodeInTree) } : node;
})(treeData!);
setTreeData(updatedTreeData);
if (dragItem.imageData) {
if (!dragItem.imageData) return;
moveImageMutation.mutate({
projectId: project.id,
folderId: Number(dragItem.parent?.id),
@ -208,26 +141,20 @@ export default function ProjectStructure({ project }: { project: Project }) {
moveFolderId: Number(hoverItem.parent?.id),
},
});
}
},
[treeData, setTreeData, moveImageMutation, project.id]
);
const Row = ({ index, style, data }: ListChildComponentProps<FlatNode[]>) => {
const node = data[index];
const ref = useRef<HTMLDivElement>(null);
const ref = useRef<HTMLButtonElement>(null);
const [, drop] = useDrop({
accept: ItemTypes.NODE,
drop(item: FlatNode) {
const dragItem = item;
const hoverItem = node;
if (item.id === node.id) return;
if (dragItem.id === hoverItem.id) {
return;
}
moveNode(dragItem, hoverItem);
moveNode(item, node);
},
});
@ -242,17 +169,13 @@ export default function ProjectStructure({ project }: { project: Project }) {
drag(drop(ref));
return (
<div
<button
ref={ref}
style={{
...style,
opacity: isDragging ? 0.5 : 1,
display: 'flex',
alignItems: 'center',
paddingLeft: `${node.depth * 20}px`,
cursor: 'pointer',
backgroundColor: node.imageData && selectedImage?.id === node.imageData.id ? '#e5e7eb' : 'transparent',
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' : ''}`}
onClick={() => {
if (node.imageData) {
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 ? (
<Folder
size={16}
@ -274,17 +197,18 @@ export default function ProjectStructure({ project }: { project: Project }) {
/>
)}
</div>
<span style={{ color: '#4a4a4a', flexGrow: 1 }}>{node.name}</span>
{node.imageData && <div style={{ marginRight: '20px' }}>{renderStatusIcon(node.imageData.status)}</div>}
</div>
<span className="grow overflow-hidden text-ellipsis whitespace-nowrap text-left text-gray-900">
{node.name}
</span>
{node.imageData && <MemoFileStatusIcon imageStatus={node.imageData.status} />}
</button>
);
};
return (
<DndProvider backend={HTML5Backend}>
<div
className="box-border flex h-full min-h-0 flex-col bg-gray-50 p-2"
style={{ overflowX: 'hidden' }}
className="box-border flex h-full min-h-0 flex-col overflow-x-hidden bg-gray-50"
ref={containerRef}
>
<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>
) : (
<List
height={Math.min(flatData.length * 40, containerHeight)}
height={Math.min(flatData.length * 20, containerHeight)}
itemCount={flatData.length}
itemSize={40}
itemSize={20}
width={'100%'}
itemData={flatData}
itemKey={getItemKey}
className="flex-1 overflow-auto"
style={{ overflowX: 'hidden' }}
className="flex-1 overflow-x-hidden"
>
{Row}
</List>

View File

@ -4,7 +4,7 @@ import { ImageResponse, ChildFolder } from '@/types';
import { useQuery } from '@tanstack/react-query';
import { getFolder } from '@/api/folderApi';
export function useFolder(projectId: string, folderId: number) {
function useFolder(projectId: string, folderId: number) {
return useQuery({
queryKey: ['folder', 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({
queryKey: ['folder', projectId, folderId],
queryFn: () => getFolder(projectId, folderId),
@ -33,6 +33,7 @@ export default function useTreeData(projectId: string) {
);
useEffect(() => {
console.log('root changed');
if (rootFolder) {
const childFolders: TreeNode[] =
rootFolder.children?.map((child: ChildFolder) => ({

View File

@ -1,5 +1,5 @@
import { create } from 'zustand';
import { LabelCategoryResponse, Project } from '@/types';
import { FolderResponse, LabelCategoryResponse, Project } from '@/types';
interface ProjectState {
project: Project | null;
@ -8,6 +8,7 @@ interface ProjectState {
setFolderId: (folderId: number) => void;
categories: LabelCategoryResponse[];
setCategories: (categories: LabelCategoryResponse[]) => void;
projectFolder: FolderResponse;
}
const useProjectStore = create<ProjectState>((set) => ({
@ -16,6 +17,12 @@ const useProjectStore = create<ProjectState>((set) => ({
folderId: 0,
setFolderId: (folderId) => set({ folderId }),
categories: [],
projectFolder: {
id: 0,
title: '',
children: [],
images: [],
},
setCategories: (categories) => set({ categories }),
}));