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:
commit
5d50a1a943
@ -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}
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</>
|
||||
|
14
frontend/src/pages/LabelCanvas.tsx
Normal file
14
frontend/src/pages/LabelCanvas.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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 />,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user