Merge branch 'fe/feat/147-canvas-sync' into 'fe/develop'

Feat: 캔버스 레이블 상태 연결 - S11P21S002-147

See merge request s11-s-project/S11P21S002!89
This commit is contained in:
정현조 2024-09-19 14:00:49 +09:00
commit 5d50a1a943
7 changed files with 59 additions and 128 deletions

View File

@ -9,6 +9,8 @@ import LabelPolygon from './LabelPolygon';
import CanvasControlBar from '../CanvasControlBar'; import CanvasControlBar from '../CanvasControlBar';
export default function ImageCanvas() { export default function ImageCanvas() {
const selectedLabelId = useCanvasStore((state) => state.selectedLabelId);
const setSelectedLabelId = useCanvasStore((state) => state.setSelectedLabelId);
const sidebarSize = useCanvasStore((state) => state.sidebarSize); const sidebarSize = useCanvasStore((state) => state.sidebarSize);
const stageWidth = window.innerWidth * ((100 - sidebarSize) / 100) - 280; const stageWidth = window.innerWidth * ((100 - sidebarSize) / 100) - 280;
const stageHeight = window.innerHeight - 64; const stageHeight = window.innerHeight - 64;
@ -17,7 +19,6 @@ export default function ImageCanvas() {
const scale = useRef<number>(0); const scale = useRef<number>(0);
const imageUrl = '/sample.jpg'; const imageUrl = '/sample.jpg';
const labels = useCanvasStore((state) => state.labels) ?? []; const labels = useCanvasStore((state) => state.labels) ?? [];
const [selectedId, setSelectedId] = useState<number | null>(null);
const [image, imageStatus] = useImage(imageUrl); const [image, imageStatus] = useImage(imageUrl);
const [rectPoints, setRectPoints] = useState<[number, number][]>([]); const [rectPoints, setRectPoints] = useState<[number, number][]>([]);
const [polygonPoints, setPolygonPoints] = useState<[number, number][]>([]); const [polygonPoints, setPolygonPoints] = useState<[number, number][]>([]);
@ -82,7 +83,7 @@ export default function ImageCanvas() {
coordinates: polygonPoints.slice(0, -1), coordinates: polygonPoints.slice(0, -1),
}); });
setDrawState('pointer'); setDrawState('pointer');
setSelectedId(id); setSelectedLabelId(id);
}; };
const updateDrawingRect = () => { const updateDrawingRect = () => {
if (rectPoints.length === 0) return; if (rectPoints.length === 0) return;
@ -109,7 +110,7 @@ export default function ImageCanvas() {
coordinates: rectPoints, coordinates: rectPoints,
}); });
setDrawState('pointer'); setDrawState('pointer');
setSelectedId(id); setSelectedLabelId(id);
}; };
const handleClick = (e: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => { const handleClick = (e: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => {
e.evt.preventDefault(); e.evt.preventDefault();
@ -128,7 +129,7 @@ export default function ImageCanvas() {
return; return;
} }
if (e.target === e.target.getStage() || e.target.getClassName() === 'Image') { if (e.target === e.target.getStage() || e.target.getClassName() === 'Image') {
setSelectedId(null); setSelectedLabelId(null);
} }
}; };
const handleMouseMove = () => { const handleMouseMove = () => {
@ -187,7 +188,7 @@ export default function ImageCanvas() {
stageRef.current.container().style.cursor = drawState === 'pointer' ? 'default' : 'crosshair'; stageRef.current.container().style.cursor = drawState === 'pointer' ? 'default' : 'crosshair';
if (drawState !== 'pointer') { if (drawState !== 'pointer') {
setSelectedId(null); setSelectedLabelId(null);
} }
}, [drawState]); }, [drawState]);
@ -217,16 +218,16 @@ export default function ImageCanvas() {
label.type === 'rect' ? ( label.type === 'rect' ? (
<LabelRect <LabelRect
key={label.id} key={label.id}
isSelected={label.id === selectedId} isSelected={label.id === selectedLabelId}
onSelect={() => setSelectedId(label.id)} onSelect={() => setSelectedLabelId(label.id)}
info={label} info={label}
dragLayer={dragLayerRef.current as Konva.Layer} dragLayer={dragLayerRef.current as Konva.Layer}
/> />
) : ( ) : (
<LabelPolygon <LabelPolygon
key={label.id} key={label.id}
isSelected={label.id === selectedId} isSelected={label.id === selectedLabelId}
onSelect={() => setSelectedId(label.id)} onSelect={() => setSelectedLabelId(label.id)}
info={label} info={label}
stage={stageRef.current as Konva.Stage} stage={stageRef.current as Konva.Stage}
dragLayer={dragLayerRef.current as Konva.Layer} dragLayer={dragLayerRef.current as Konva.Layer}

View File

@ -1,14 +1,17 @@
import { Label } from '@/types'; import { Label } from '@/types';
import { Edit, Trash2 } from 'lucide-react'; import { Trash2 } from 'lucide-react';
import { MouseEventHandler } from 'react'; import { MouseEventHandler } from 'react';
export default function LabelButton({ id, name, color }: Label) { export default function LabelButton({
id,
name,
color,
selected,
setSelectedLabelId,
}: Label & { selected: boolean; setSelectedLabelId: (id: number) => void }) {
const handleClick: MouseEventHandler = () => { const handleClick: MouseEventHandler = () => {
console.log(`LabelButton ${id} clicked`); console.log(`LabelButton ${id} clicked`);
}; setSelectedLabelId(id);
const handleEdit: MouseEventHandler = (event) => {
event.stopPropagation();
console.log(`Edit LabelButton ${id}`);
}; };
const handleDelete: MouseEventHandler = (event) => { const handleDelete: MouseEventHandler = (event) => {
event.stopPropagation(); event.stopPropagation();
@ -16,9 +19,11 @@ export default function LabelButton({ id, name, color }: Label) {
}; };
return ( return (
<div className="flex items-center gap-2.5 rounded-lg bg-gray-100 p-2.5 transition-colors hover:bg-gray-200"> <div
className={`flex items-center gap-2.5 rounded-lg transition-colors ${selected ? 'bg-gray-200' : 'bg-gray-50 hover:bg-gray-100'}`}
>
<button <button
className="flex grow items-center gap-2.5 text-left" className="flex grow items-center gap-2.5 p-2.5 text-left"
onClick={handleClick} onClick={handleClick}
> >
<div <div
@ -29,13 +34,10 @@ export default function LabelButton({ id, name, color }: Label) {
/> />
<span className="body grow text-gray-900">{name}</span> <span className="body grow text-gray-900">{name}</span>
</button> </button>
<button onClick={handleEdit}> <button
<Edit className="p-2.5"
size={16} onClick={handleDelete}
className="stroke-gray-500 hover:stroke-gray-600" >
/>
</button>
<button onClick={handleDelete}>
<Trash2 <Trash2
size={16} size={16}
className="stroke-red-500 hover:stroke-red-600" className="stroke-red-500 hover:stroke-red-600"

View File

@ -1,15 +1,18 @@
import { Label } from '@/types';
import LabelButton from './LabelButton'; import LabelButton from './LabelButton';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { Play } from 'lucide-react'; import { Play } from 'lucide-react';
import useCanvasStore from '@/stores/useCanvasStore';
export default function WorkspaceLabelBar({ labels }: { labels: Label[] }) { export default function WorkspaceLabelBar() {
const labels = useCanvasStore((state) => state.labels);
const selectedLabelId = useCanvasStore((state) => state.selectedLabelId);
const setSelectedLabelId = useCanvasStore((state) => state.setSelectedLabelId);
const handleAutoLabeling = () => { const handleAutoLabeling = () => {
console.log('Auto labeling'); console.log('Auto labeling');
}; };
return ( return (
<div className="flex h-full w-[280px] flex-col justify-between border-l border-gray-300 bg-gray-100"> <div className="flex h-full w-[280px] flex-col justify-between border-l border-gray-300 bg-gray-50">
<div className="flex flex-col gap-2.5"> <div className="flex flex-col gap-2.5">
<header className="subheading flex w-full items-center gap-2 px-5 py-2.5"> <header className="subheading flex w-full items-center gap-2 px-5 py-2.5">
<h1 className="w-full overflow-hidden text-ellipsis whitespace-nowrap"> </h1> <h1 className="w-full overflow-hidden text-ellipsis whitespace-nowrap"> </h1>
@ -19,6 +22,8 @@ export default function WorkspaceLabelBar({ labels }: { labels: Label[] }) {
<LabelButton <LabelButton
key={label.id} key={label.id}
{...label} {...label}
selected={selectedLabelId === label.id}
setSelectedLabelId={setSelectedLabelId}
/> />
))} ))}
</div> </div>

View File

@ -1,10 +1,9 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useParams, Outlet } from 'react-router-dom'; import { useParams, Outlet } from 'react-router-dom';
import Header from '../Header'; import Header from '../Header';
import { Label, Project } from '@/types'; import { Project } from '@/types';
import { ResizablePanelGroup, ResizablePanel } from '../ui/resizable'; import { ResizablePanelGroup, ResizablePanel } from '../ui/resizable';
import WorkspaceSidebar from '../WorkspaceSidebar'; import WorkspaceSidebar from '../WorkspaceSidebar';
import WorkspaceLabelBar from '../WorkspaceLabelBar';
import useAuthStore from '@/stores/useAuthStore'; import useAuthStore from '@/stores/useAuthStore';
import useCanvasStore from '@/stores/useCanvasStore'; import useCanvasStore from '@/stores/useCanvasStore';
import useFolderQuery from '@/queries/useFolderQuery'; import useFolderQuery from '@/queries/useFolderQuery';
@ -89,97 +88,7 @@ export default function WorkspaceLayout() {
...prev, ...prev,
projects, projects,
})); }));
}, [projectListData]); }, [folderData, projectListData]);
// useEffect(() => {
// const fetchWorkspaceData = async (workspaceId: number, memberId: number) => {
// try {
// const workspaceResponse = await getWorkspaceApi(workspaceId, memberId);
// if (workspaceResponse.isSuccess) {
// const workspaceTitle = workspaceResponse.data.title;
// setWorkspace((prev) => ({
// ...prev,
// name: workspaceTitle,
// }));
// fetchProjects(workspaceId, memberId);
// }
// } catch (error) {
// console.error('워크스페이스 조회 실패:', error);
// }
// };
// const fetchProjects = async (workspaceId: number, memberId: number) => {
// try {
// const projectResponse = await getAllProjectsApi(workspaceId, memberId);
// if (projectResponse.isSuccess) {
// const projects = await Promise.all(
// projectResponse.data.workspaceResponses.map(async (project) => {
// const children = await fetchFolderWithImages(project.id, memberId);
// return {
// id: project.id,
// name: project.title,
// type: capitalizeType(project.projectType),
// children,
// };
// })
// );
// setWorkspace((prev) => ({
// ...prev,
// projects: projects as Project[],
// }));
// }
// } catch (error) {
// console.error('프로젝트 목록 조회 실패:', error);
// }
// };
// const fetchFolderWithImages = async (projectId: number, memberId: number): Promise<DirectoryItem[]> => {
// try {
// const folderResponse = await fetchFolderApi(projectId, 0, memberId);
// if (folderResponse.isSuccess) {
// const files: FileItem[] = folderResponse.data.images.map((image) => ({
// id: image.id,
// name: image.imageTitle,
// url: image.imageUrl,
// type: 'image',
// status: image.status === 'COMPLETED' ? 'done' : 'idle',
// }));
// return [
// {
// id: folderResponse.data.id,
// name: folderResponse.data.title,
// type: 'directory',
// children: files,
// },
// ];
// }
// return [];
// } catch (error) {
// console.error('폴더 및 이미지 조회 실패:', error);
// return [];
// }
// };
// const capitalizeType = (
// type: 'classification' | 'detection' | 'segmentation'
// ): 'Classification' | 'Detection' | 'Segmentation' => {
// switch (type) {
// case 'classification':
// return 'Classification';
// case 'detection':
// return 'Detection';
// case 'segmentation':
// return 'Segmentation';
// default:
// throw new Error(`Unknown project type: ${type}`);
// }
// };
// if (workspaceId && memberId) {
// fetchWorkspaceData(Number(workspaceId), memberId);
// }
// }, [workspaceId, projectId, memberId]);
useEffect(() => { useEffect(() => {
setLabels(mockLabels); setLabels(mockLabels);
@ -194,12 +103,7 @@ export default function WorkspaceLayout() {
workspaceName={workspace.name} workspaceName={workspace.name}
projects={workspace.projects} projects={workspace.projects}
/> />
<ResizablePanel className="flex w-full items-center">
<main className="h-full grow">
<Outlet /> <Outlet />
</main>
<WorkspaceLabelBar labels={mockLabels} />
</ResizablePanel>
</ResizablePanelGroup> </ResizablePanelGroup>
</div> </div>
</> </>

View File

@ -0,0 +1,14 @@
import ImageCanvas from '@/components/ImageCanvas';
import { ResizablePanel } from '@/components/ui/resizable';
import WorkspaceLabelBar from '@/components/WorkspaceLabelBar';
export default function LabelCanvas() {
return (
<ResizablePanel className="flex w-full items-center">
<main className="h-full grow">
<ImageCanvas />
</main>
<WorkspaceLabelBar />
</ResizablePanel>
);
}

View File

@ -12,6 +12,7 @@ import { createBrowserRouter } from 'react-router-dom';
import { Suspense } from 'react'; import { Suspense } from 'react';
import WorkspaceBrowseIndex from '@/pages/WorkspaceBrowseIndex'; import WorkspaceBrowseIndex from '@/pages/WorkspaceBrowseIndex';
import AdminIndex from '@/pages/AdminIndex'; import AdminIndex from '@/pages/AdminIndex';
import LabelCanvas from '@/pages/LabelCanvas';
export const webPath = { export const webPath = {
home: () => '/', home: () => '/',
browse: () => '/browse', browse: () => '/browse',
@ -61,11 +62,11 @@ const router = createBrowserRouter([
children: [ children: [
{ {
index: true, index: true,
element: <ImageCanvas />, element: <LabelCanvas />,
}, },
{ {
path: 'project/:projectId', path: 'project/:projectId',
element: <ImageCanvas />, element: <LabelCanvas />,
}, },
], ],
}, },

View File

@ -6,6 +6,7 @@ interface CanvasState {
image: string; image: string;
labels: Label[]; labels: Label[];
drawState: 'pen' | 'rect' | 'pointer'; drawState: 'pen' | 'rect' | 'pointer';
selectedLabelId: number | null;
setSidebarSize: (width: number) => void; setSidebarSize: (width: number) => void;
changeImage: (image: string, labels: Label[]) => void; changeImage: (image: string, labels: Label[]) => void;
setLabels: (labels: Label[]) => void; setLabels: (labels: Label[]) => void;
@ -13,6 +14,7 @@ interface CanvasState {
removeLabel: (labelId: number) => void; removeLabel: (labelId: number) => void;
updateLabel: (label: Label) => void; updateLabel: (label: Label) => void;
setDrawState: (state: 'pen' | 'rect' | 'pointer') => void; setDrawState: (state: 'pen' | 'rect' | 'pointer') => void;
setSelectedLabelId: (labelId: number | null) => void;
} }
const useCanvasStore = create<CanvasState>()((set) => ({ const useCanvasStore = create<CanvasState>()((set) => ({
@ -20,6 +22,7 @@ const useCanvasStore = create<CanvasState>()((set) => ({
image: '', image: '',
labels: [], labels: [],
drawState: 'pointer', drawState: 'pointer',
selectedLabelId: null,
setSidebarSize: (width) => set({ sidebarSize: width }), setSidebarSize: (width) => set({ sidebarSize: width }),
changeImage: (image: string, labels: Label[]) => set({ image, labels }), changeImage: (image: string, labels: Label[]) => set({ image, labels }),
addLabel: (label: Label) => set((state) => ({ labels: [...state.labels, label] })), addLabel: (label: Label) => set((state) => ({ labels: [...state.labels, label] })),
@ -27,6 +30,7 @@ const useCanvasStore = create<CanvasState>()((set) => ({
removeLabel: (labelId: number) => set((state) => ({ labels: state.labels.filter((label) => label.id !== labelId) })), removeLabel: (labelId: number) => set((state) => ({ labels: state.labels.filter((label) => label.id !== labelId) })),
updateLabel: (label: Label) => set((state) => ({ labels: state.labels.map((l) => (l.id === label.id ? label : l)) })), updateLabel: (label: Label) => set((state) => ({ labels: state.labels.map((l) => (l.id === label.id ? label : l)) })),
setDrawState: (drawState) => set({ drawState }), setDrawState: (drawState) => set({ drawState }),
setSelectedLabelId: (labelId) => set({ selectedLabelId: labelId }),
})); }));
export default useCanvasStore; export default useCanvasStore;