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';
|
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}
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
<Outlet />
|
||||||
<main className="h-full grow">
|
|
||||||
<Outlet />
|
|
||||||
</main>
|
|
||||||
<WorkspaceLabelBar labels={mockLabels} />
|
|
||||||
</ResizablePanel>
|
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
</div>
|
</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 { 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 />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user