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

View File

@ -1,14 +1,17 @@
import { Label } from '@/types';
import { Edit, Trash2 } from 'lucide-react';
import { Trash2 } from 'lucide-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 = () => {
console.log(`LabelButton ${id} clicked`);
};
const handleEdit: MouseEventHandler = (event) => {
event.stopPropagation();
console.log(`Edit LabelButton ${id}`);
setSelectedLabelId(id);
};
const handleDelete: MouseEventHandler = (event) => {
event.stopPropagation();
@ -16,9 +19,11 @@ export default function LabelButton({ id, name, color }: Label) {
};
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
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}
>
<div
@ -29,13 +34,10 @@ export default function LabelButton({ id, name, color }: Label) {
/>
<span className="body grow text-gray-900">{name}</span>
</button>
<button onClick={handleEdit}>
<Edit
size={16}
className="stroke-gray-500 hover:stroke-gray-600"
/>
</button>
<button onClick={handleDelete}>
<button
className="p-2.5"
onClick={handleDelete}
>
<Trash2
size={16}
className="stroke-red-500 hover:stroke-red-600"

View File

@ -1,15 +1,18 @@
import { Label } from '@/types';
import LabelButton from './LabelButton';
import { Button } from '../ui/button';
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 = () => {
console.log('Auto labeling');
};
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">
<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>
@ -19,6 +22,8 @@ export default function WorkspaceLabelBar({ labels }: { labels: Label[] }) {
<LabelButton
key={label.id}
{...label}
selected={selectedLabelId === label.id}
setSelectedLabelId={setSelectedLabelId}
/>
))}
</div>

View File

@ -1,10 +1,9 @@
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, ResizablePanel } from '../ui/resizable';
import WorkspaceSidebar from '../WorkspaceSidebar';
import WorkspaceLabelBar from '../WorkspaceLabelBar';
import useAuthStore from '@/stores/useAuthStore';
import useCanvasStore from '@/stores/useCanvasStore';
import useFolderQuery from '@/queries/useFolderQuery';
@ -89,97 +88,7 @@ export default function WorkspaceLayout() {
...prev,
projects,
}));
}, [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]);
}, [folderData, projectListData]);
useEffect(() => {
setLabels(mockLabels);
@ -194,12 +103,7 @@ export default function WorkspaceLayout() {
workspaceName={workspace.name}
projects={workspace.projects}
/>
<ResizablePanel className="flex w-full items-center">
<main className="h-full grow">
<Outlet />
</main>
<WorkspaceLabelBar labels={mockLabels} />
</ResizablePanel>
<Outlet />
</ResizablePanelGroup>
</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 WorkspaceBrowseIndex from '@/pages/WorkspaceBrowseIndex';
import AdminIndex from '@/pages/AdminIndex';
import LabelCanvas from '@/pages/LabelCanvas';
export const webPath = {
home: () => '/',
browse: () => '/browse',
@ -61,11 +62,11 @@ const router = createBrowserRouter([
children: [
{
index: true,
element: <ImageCanvas />,
element: <LabelCanvas />,
},
{
path: 'project/:projectId',
element: <ImageCanvas />,
element: <LabelCanvas />,
},
],
},

View File

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