From 584788c2603614aa180f7dbf5cb4319ea0bbc59b Mon Sep 17 00:00:00 2001 From: jhynsoo Date: Thu, 19 Sep 2024 16:50:00 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EC=BA=94=EB=B2=84=EC=8A=A4=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=EC=83=81=ED=83=9C=20=EC=97=B0=EA=B2=B0=20-=20S11P2?= =?UTF-8?q?1S002-193?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/folderApi.ts | 8 +-- frontend/src/components/ImageCanvas/index.tsx | 19 +++++-- .../components/ProjectCreateModal/index.tsx | 21 ++----- .../src/components/WorkspaceLayout/index.tsx | 52 +----------------- .../WorkspaceSidebar/ProjectDirectoryItem.tsx | 34 +++--------- .../WorkspaceSidebar/ProjectFileItem.tsx | 35 +++++++----- .../WorkspaceSidebar/ProjectStructure.tsx | 55 +++++++++---------- .../src/components/WorkspaceSidebar/index.tsx | 30 +++++----- frontend/src/main.tsx | 27 --------- frontend/src/pages/LabelCanvas.tsx | 20 +++++-- frontend/src/pages/WorkspaceBrowseDetail.tsx | 2 +- .../src/queries/folders/useFolderQuery.ts | 6 +- 12 files changed, 111 insertions(+), 198 deletions(-) diff --git a/frontend/src/api/folderApi.ts b/frontend/src/api/folderApi.ts index 58caf85..630240a 100644 --- a/frontend/src/api/folderApi.ts +++ b/frontend/src/api/folderApi.ts @@ -1,12 +1,8 @@ import api from '@/api/axiosConfig'; import { FolderRequest, FolderResponse } from '@/types'; -export async function getFolder(projectId: number, folderId: number, memberId: number) { - return api - .get(`/projects/${projectId}/folders/${folderId}`, { - params: { memberId }, - }) - .then(({ data }) => data); +export async function getFolder(projectId: string, folderId: number) { + return api.get(`/projects/${projectId}/folders/${folderId}`).then(({ data }) => data); } export async function updateFolder(projectId: number, folderId: number, memberId: number, folderData: FolderRequest) { diff --git a/frontend/src/components/ImageCanvas/index.tsx b/frontend/src/components/ImageCanvas/index.tsx index 9411527..bf7a96e 100644 --- a/frontend/src/components/ImageCanvas/index.tsx +++ b/frontend/src/components/ImageCanvas/index.tsx @@ -17,9 +17,9 @@ export default function ImageCanvas() { const stageRef = useRef(null); const dragLayerRef = useRef(null); const scale = useRef(0); - const imageUrl = '/sample.jpg'; + const imageUrl = useCanvasStore((state) => state.image); const labels = useCanvasStore((state) => state.labels) ?? []; - const [image, imageStatus] = useImage(imageUrl); + const [image] = useImage(imageUrl); const [rectPoints, setRectPoints] = useState<[number, number][]>([]); const [polygonPoints, setPolygonPoints] = useState<[number, number][]>([]); const drawState = useCanvasStore((state) => state.drawState); @@ -183,6 +183,17 @@ export default function ImageCanvas() { return { x: scale.current, y: scale.current }; }; + useEffect(() => { + if (!image) { + scale.current = 0; + return; + } + const widthRatio = stageWidth / image!.width; + const heightRatio = stageHeight / image!.height; + + scale.current = Math.min(widthRatio, heightRatio); + }, [image, stageHeight, stageWidth]); + useEffect(() => { if (!stageRef.current) return; stageRef.current.container().style.cursor = drawState === 'pointer' ? 'default' : 'crosshair'; @@ -190,9 +201,9 @@ export default function ImageCanvas() { if (drawState !== 'pointer') { setSelectedLabelId(null); } - }, [drawState]); + }, [drawState, setSelectedLabelId]); - return imageStatus === 'loaded' ? ( + return image ? (
setIsOpen(true); const handleClose = () => setIsOpen(false); - const formatLabelType = ( - labelType: 'Classification' | 'Detection' | 'Segmentation' - ): ProjectRequest['projectType'] => { - switch (labelType) { - case 'Classification': - return 'classification'; - case 'Detection': - return 'detection'; - case 'Segmentation': - return 'segmentation'; - } - }; - return ( - - 프로젝트 추가 + 새 프로젝트 @@ -50,7 +36,8 @@ export default function ProjectCreateModal({ onSubmit, buttonClass = '' }: Proje onSubmit={(data: ProjectCreateFormValues) => { const formattedData: ProjectRequest = { title: data.projectName, - projectType: formatLabelType(data.labelType), + projectType: (data.labelType.charAt(0).toUpperCase() + + data.labelType.slice(1)) as ProjectRequest['projectType'], }; onSubmit(formattedData); handleClose(); diff --git a/frontend/src/components/WorkspaceLayout/index.tsx b/frontend/src/components/WorkspaceLayout/index.tsx index daa53b5..ad0605a 100644 --- a/frontend/src/components/WorkspaceLayout/index.tsx +++ b/frontend/src/components/WorkspaceLayout/index.tsx @@ -1,58 +1,16 @@ import { useEffect, useState } from 'react'; import { useParams, Outlet } from 'react-router-dom'; import Header from '../Header'; -import { Label, Project } from '@/types'; +import { Project } from '@/types'; import { ResizablePanelGroup } from '../ui/resizable'; -// import { ResizablePanel } from '../ui/resizable'; import WorkspaceSidebar from '../WorkspaceSidebar'; import useAuthStore from '@/stores/useAuthStore'; -import useCanvasStore from '@/stores/useCanvasStore'; -import useFolderQuery from '@/queries/folders/useFolderQuery'; import useWorkspaceQuery from '@/queries/workspaces/useWorkspaceQuery'; import useProjectListQuery from '@/queries/projects/useProjectListQuery'; -const mockLabels: Label[] = [ - { - id: 1, - name: 'Label 1', - color: '#FFaa33', - coordinates: [ - [700, 100], - [1200, 800], - ], - type: 'rect', - }, - { - id: 2, - name: 'Label 2', - color: '#aaFF55', - coordinates: [ - [200, 200], - [400, 200], - [500, 500], - [400, 800], - [200, 800], - [100, 500], - ], - type: 'polygon', - }, - { - id: 3, - name: 'Label 3', - color: '#77aaFF', - coordinates: [ - [1000, 1000], - [1800, 1800], - ], - type: 'rect', - }, -]; - export default function WorkspaceLayout() { - const setLabels = useCanvasStore((state) => state.setLabels); const params = useParams<{ workspaceId: string; projectId: string }>(); const workspaceId = Number(params.workspaceId); - const projectId = Number(params.projectId); const [workspace, setWorkspace] = useState<{ name: string; projects: Project[] }>({ name: '', projects: [], @@ -61,7 +19,6 @@ export default function WorkspaceLayout() { const memberId = profile?.id || 0; const { data: workspaceData } = useWorkspaceQuery(workspaceId, memberId); const { data: projectListData } = useProjectListQuery(workspaceId, memberId); - const { data: folderData } = useFolderQuery(projectId, 0, memberId); useEffect(() => { if (!workspaceData) return; @@ -73,7 +30,6 @@ export default function WorkspaceLayout() { useEffect(() => { if (!projectListData) return; - console.log(folderData); const projects = projectListData.workspaceResponses.map( (project): Project => ({ id: project.id, @@ -89,11 +45,7 @@ export default function WorkspaceLayout() { ...prev, projects, })); - }, [folderData, projectListData]); - - useEffect(() => { - setLabels(mockLabels); - }, [setLabels]); + }, [projectListData]); return ( <> diff --git a/frontend/src/components/WorkspaceSidebar/ProjectDirectoryItem.tsx b/frontend/src/components/WorkspaceSidebar/ProjectDirectoryItem.tsx index aee6484..e14c932 100644 --- a/frontend/src/components/WorkspaceSidebar/ProjectDirectoryItem.tsx +++ b/frontend/src/components/WorkspaceSidebar/ProjectDirectoryItem.tsx @@ -1,19 +1,20 @@ -import { DirectoryItem } from '@/types'; +import { ChildFolder } from '@/types'; import { ChevronRight } from 'lucide-react'; import { useState } from 'react'; -import ProjectFileItem from './ProjectFileItem'; import { cn } from '@/lib/utils'; export default function ProjectDirectoryItem({ className = '', item, - depth = 1, + depth = 0, + initialExpanded = false, }: { className?: string; - item: DirectoryItem; + item: ChildFolder; depth?: number; + initialExpanded?: boolean; }) { - const [isExpanded, setIsExpanded] = useState(true); + const [isExpanded, setIsExpanded] = useState(initialExpanded); const paddingLeft = depth * 12; return ( @@ -31,28 +32,9 @@ export default function ProjectDirectoryItem({ className={`stroke-gray-500 transition-transform ${isExpanded ? 'rotate-90' : ''}`} /> - {item.name} + {item.title}
- {item.children?.map((child) => { - const childProps = { - className: isExpanded ? '' : 'hidden', - depth: depth + 1, - }; - - return child.type === 'directory' ? ( - - ) : ( - - ); - })} + {/* TODO: nested 폴더 구조 적용 */} ); } diff --git a/frontend/src/components/WorkspaceSidebar/ProjectFileItem.tsx b/frontend/src/components/WorkspaceSidebar/ProjectFileItem.tsx index 0da9296..ffb0af9 100644 --- a/frontend/src/components/WorkspaceSidebar/ProjectFileItem.tsx +++ b/frontend/src/components/WorkspaceSidebar/ProjectFileItem.tsx @@ -1,5 +1,5 @@ import { cn } from '@/lib/utils'; -import { FileItem } from '@/types'; +import { ImageResponse } from '@/types'; import { Check, Image, Minus } from 'lucide-react'; import useCanvasStore from '@/stores/useCanvasStore'; @@ -7,29 +7,38 @@ export default function ProjectFileItem({ className = '', item, depth = 1, + selected, }: { className?: string; - item: FileItem; + item: ImageResponse; depth?: number; + selected: boolean; }) { const paddingLeft = depth * 12; const changeImage = useCanvasStore((state) => state.changeImage); const handleClick = () => { - changeImage(item.url, [ + // TODO: fetch image + changeImage(item.imageUrl, [ { id: item.id, - name: item.name, + name: item.imageTitle, type: 'rect', color: '#FF0000', - coordinates: [], + coordinates: [ + [0, 0], + [100, 100], + ], }, ]); }; return ( ); diff --git a/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx b/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx index 181740c..9a3c2ae 100644 --- a/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx +++ b/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx @@ -1,29 +1,19 @@ import { Project } from '@/types'; -import { ChevronRight, SquarePenIcon, Upload } from 'lucide-react'; -import { useState } from 'react'; +import { SquarePenIcon, Upload } from 'lucide-react'; import ProjectFileItem from './ProjectFileItem'; import ProjectDirectoryItem from './ProjectDirectoryItem'; +import useFolderQuery from '@/queries/folders/useFolderQuery'; +import useCanvasStore from '@/stores/useCanvasStore'; export default function ProjectStructure({ project }: { project: Project }) { - const [isExpanded, setIsExpanded] = useState(true); + const image = useCanvasStore((state) => state.image); + const { data: folderData } = useFolderQuery(project.id.toString(), 0); return ( -
-
-
setIsExpanded((prev) => !prev)} - > - -
-

{project.name}

-

{project.type}

-
+
+
+
+

{project.type}

-
- {project.children.map((item) => - item.type === 'directory' ? ( + {folderData.children.length === 0 && folderData.images.length === 0 ? ( +
+ 빈 프로젝트입니다. +
+ ) : ( +
+ {folderData.children.map((item) => ( - ) : ( + ))} + {folderData.images.map((item) => ( - ) - )} -
+ ))} +
+ )}
); } diff --git a/frontend/src/components/WorkspaceSidebar/index.tsx b/frontend/src/components/WorkspaceSidebar/index.tsx index 954e216..963eeb8 100644 --- a/frontend/src/components/WorkspaceSidebar/index.tsx +++ b/frontend/src/components/WorkspaceSidebar/index.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { SquarePen } from 'lucide-react'; import { ResizableHandle, ResizablePanel } from '../ui/resizable'; @@ -7,15 +6,18 @@ import { Project } from '@/types'; import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '../ui/select'; import ProjectCreateModal from '../ProjectCreateModal'; import useCanvasStore from '@/stores/useCanvasStore'; +import { webPath } from '@/router'; export default function WorkspaceSidebar({ workspaceName, projects }: { workspaceName: string; projects: Project[] }) { + const { projectId: selectedProjectId } = useParams<{ projectId: string }>(); + const selectedProject = projects.find((project) => project.id.toString() === selectedProjectId); const setSidebarSize = useCanvasStore((state) => state.setSidebarSize); const navigate = useNavigate(); const { workspaceId } = useParams<{ workspaceId: string }>(); - const [selectedProjectId, setSelectedProjectId] = useState(); + // const [selectedProjectId, setSelectedProjectId] = useState(); const handleSelectProject = (projectId: string) => { - setSelectedProjectId(projectId); - navigate(`/workspace/${workspaceId}/project/${projectId}`); + // setSelectedProjectId(projectId); + navigate(`${webPath.workspace()}/${workspaceId}/project/${projectId}`); }; return ( @@ -24,7 +26,7 @@ export default function WorkspaceSidebar({ workspaceName, projects }: { workspac minSize={10} maxSize={35} defaultSize={20} - className="flex h-full flex-col bg-gray-100" + className="flex h-full flex-col bg-gray-50" onResize={(size) => setSidebarSize(size)} >
@@ -34,11 +36,14 @@ export default function WorkspaceSidebar({ workspaceName, projects }: { workspac console.log('프로젝트 생성:', data)} - buttonClass="caption border-gray-800 bg-gray-100 flex items-center gap-2" + buttonClass="caption border-primary bg-gray-50" />
- @@ -54,16 +59,7 @@ export default function WorkspaceSidebar({ workspaceName, projects }: { workspac
-
- {projects - .filter((project) => project.id.toString() === selectedProjectId) - .map((project) => ( - - ))} -
+ {selectedProject && } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 1859d8f..93db379 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,30 +1,3 @@ -// import React from 'react'; -// import ReactDOM from 'react-dom/client'; -// import App from './App.tsx'; -// import './index.css'; - -// async function enableMocking() { -// if (!import.meta.env.DEV) { -// return; -// } - -// try { -// const { worker } = await import('./mocks/browser.ts'); -// await worker.start(); -// console.log('[MSW] Mocking enabled. Service Worker is running.'); -// } catch (error) { -// console.error('[MSW] Failed to start the Service Worker:', error); -// } -// } - -// enableMocking().then(() => { -// ReactDOM.createRoot(document.getElementById('root')!).render( -// -// -// -// ); -// }); - import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.tsx'; diff --git a/frontend/src/pages/LabelCanvas.tsx b/frontend/src/pages/LabelCanvas.tsx index 499d7e8..8fefc81 100644 --- a/frontend/src/pages/LabelCanvas.tsx +++ b/frontend/src/pages/LabelCanvas.tsx @@ -1,14 +1,26 @@ import ImageCanvas from '@/components/ImageCanvas'; import { ResizablePanel } from '@/components/ui/resizable'; import WorkspaceLabelBar from '@/components/WorkspaceLabelBar'; +import useCanvasStore from '@/stores/useCanvasStore'; +import { Suspense } from 'react'; export default function LabelCanvas() { + const imageUrl = useCanvasStore((state) => state.image); + return ( -
- -
- +
}> +
+ {imageUrl ? ( + + ) : ( +
+ 이미지를 선택하세요. +
+ )} +
+ + ); } diff --git a/frontend/src/pages/WorkspaceBrowseDetail.tsx b/frontend/src/pages/WorkspaceBrowseDetail.tsx index 269398c..0a30402 100644 --- a/frontend/src/pages/WorkspaceBrowseDetail.tsx +++ b/frontend/src/pages/WorkspaceBrowseDetail.tsx @@ -63,7 +63,7 @@ function HeaderSection({
diff --git a/frontend/src/queries/folders/useFolderQuery.ts b/frontend/src/queries/folders/useFolderQuery.ts index 5c38ad6..4678eca 100644 --- a/frontend/src/queries/folders/useFolderQuery.ts +++ b/frontend/src/queries/folders/useFolderQuery.ts @@ -1,9 +1,9 @@ import { getFolder } from '@/api/folderApi'; import { useSuspenseQuery } from '@tanstack/react-query'; -export default function useFolderQuery(projectId: number, folderId: number, memberId: number) { +export default function useFolderQuery(projectId: string, folderId: number) { return useSuspenseQuery({ - queryKey: ['folder', projectId, folderId, memberId], - queryFn: () => getFolder(projectId, folderId, memberId), + queryKey: ['folder', projectId, folderId], + queryFn: () => getFolder(projectId, folderId), }); }