Merge branch 'fe/develop' into 'fe/feat/203-auto-label-api'
# Conflicts: # frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx
This commit is contained in:
commit
74e5e9b8ab
@ -6,4 +6,9 @@ export default {
|
||||
component: CanvasControlBar,
|
||||
};
|
||||
|
||||
export const Default = () => <CanvasControlBar saveJson={() => {}} />;
|
||||
export const Default = () => (
|
||||
<CanvasControlBar
|
||||
saveJson={() => {}}
|
||||
projectType="segmentation"
|
||||
/>
|
||||
);
|
||||
|
@ -2,16 +2,22 @@ import useCanvasStore from '@/stores/useCanvasStore';
|
||||
import { LucideIcon, MousePointer2, PenTool, Save, Square } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export default function CanvasControlBar({ saveJson }: { saveJson: () => void }) {
|
||||
export default function CanvasControlBar({
|
||||
saveJson,
|
||||
projectType,
|
||||
}: {
|
||||
saveJson: () => void;
|
||||
projectType: 'classification' | 'detection' | 'segmentation';
|
||||
}) {
|
||||
const drawState = useCanvasStore((state) => state.drawState);
|
||||
const setDrawState = useCanvasStore((state) => state.setDrawState);
|
||||
const buttonBaseClassName = 'rounded-lg p-2 transition-colors ';
|
||||
const buttonClassName = 'hover:bg-gray-100';
|
||||
const activeButtonClassName = 'bg-primary stroke-white';
|
||||
|
||||
const controls: { [key: string]: LucideIcon } = {
|
||||
pointer: MousePointer2,
|
||||
rect: Square,
|
||||
pen: PenTool,
|
||||
...(projectType === 'segmentation' ? { pen: PenTool } : { rect: Square }),
|
||||
};
|
||||
|
||||
return (
|
||||
@ -31,7 +37,7 @@ export default function CanvasControlBar({ saveJson }: { saveJson: () => void })
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="h-5 w-0.5 rounded bg-gray-400" />
|
||||
<button
|
||||
className={cn(buttonClassName, buttonBaseClassName)}
|
||||
onClick={saveJson}
|
||||
|
@ -10,16 +10,17 @@ import CanvasControlBar from '../CanvasControlBar';
|
||||
import { Label } from '@/types';
|
||||
import useLabelJson from '@/hooks/useLabelJson';
|
||||
import { saveImageLabels } from '@/api/lablingApi';
|
||||
import useProjectStore from '@/stores/useProjectStore';
|
||||
|
||||
export default function ImageCanvas() {
|
||||
const project = useCanvasStore((state) => state.project)!;
|
||||
const project = useProjectStore((state) => state.project)!;
|
||||
const { id: imageId, imagePath, dataPath } = useCanvasStore((state) => state.image)!;
|
||||
const { data: labelData, refetch } = useLabelJson(dataPath, project);
|
||||
const { shapes } = labelData || [];
|
||||
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 stageWidth = window.innerWidth * ((100 - sidebarSize) / 100) - 200;
|
||||
const stageHeight = window.innerHeight - 64;
|
||||
const stageRef = useRef<Konva.Stage>(null);
|
||||
const dragLayerRef = useRef<Konva.Layer>(null);
|
||||
@ -344,7 +345,10 @@ export default function ImageCanvas() {
|
||||
|
||||
<Layer ref={dragLayerRef} />
|
||||
</Stage>
|
||||
<CanvasControlBar saveJson={saveJson} />
|
||||
<CanvasControlBar
|
||||
saveJson={saveJson}
|
||||
projectType={project.type}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
|
@ -45,7 +45,10 @@ export default function WorkspaceDropdownMenu({
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Menu size={20} />
|
||||
<Menu
|
||||
size={16}
|
||||
className="stroke-gray-900"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
<DropdownMenuItem
|
||||
|
@ -1,46 +1,29 @@
|
||||
import LabelButton from './LabelButton';
|
||||
import { Button } from '../ui/button';
|
||||
import { Play } from 'lucide-react';
|
||||
import useCanvasStore from '@/stores/useCanvasStore';
|
||||
|
||||
export default function WorkspaceLabelBar() {
|
||||
const labels = useCanvasStore((state) => state.labels);
|
||||
const { labels, image } = useCanvasStore();
|
||||
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 gap-2 border-l border-gray-300 bg-gray-50 p-3">
|
||||
<div className="flex h-full w-[200px] flex-col justify-between gap-2 border-l border-gray-300 bg-gray-50 p-3">
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<header className="subheading flex w-full items-center gap-2">
|
||||
<h1 className="w-full overflow-hidden text-ellipsis whitespace-nowrap">레이블 목록</h1>
|
||||
</header>
|
||||
<div className="flex flex-col gap-1">
|
||||
{labels.map((label) => (
|
||||
<LabelButton
|
||||
key={label.id}
|
||||
{...label}
|
||||
selected={selectedLabelId === label.id}
|
||||
setSelectedLabelId={setSelectedLabelId}
|
||||
/>
|
||||
))}
|
||||
{image &&
|
||||
labels.map((label) => (
|
||||
<LabelButton
|
||||
key={label.id}
|
||||
{...label}
|
||||
selected={selectedLabelId === label.id}
|
||||
setSelectedLabelId={setSelectedLabelId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex p-2.5">
|
||||
<Button
|
||||
variant="outlinePrimary"
|
||||
className="w-full"
|
||||
onClick={handleAutoLabeling}
|
||||
>
|
||||
<Play
|
||||
size={16}
|
||||
className="mr-1"
|
||||
/>
|
||||
<span>자동 레이블링</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -8,9 +8,10 @@ import { Button } from '../ui/button';
|
||||
import { useEffect } from 'react';
|
||||
import WorkspaceDropdownMenu from '../WorkspaceDropdownMenu';
|
||||
import useAutoLabelQuery from '@/queries/projects/useAutoLabelQuery';
|
||||
import useProjectStore from '@/stores/useProjectStore';
|
||||
|
||||
export default function ProjectStructure({ project }: { project: Project }) {
|
||||
const setProject = useCanvasStore((state) => state.setProject);
|
||||
const setProject = useProjectStore((state) => state.setProject);
|
||||
const image = useCanvasStore((state) => state.image);
|
||||
const { data: folderData, refetch } = useFolderQuery(project.id.toString(), 0);
|
||||
const requestAutoLabel = useAutoLabelQuery();
|
||||
@ -24,7 +25,7 @@ export default function ProjectStructure({ project }: { project: Project }) {
|
||||
<div className="flex h-full flex-col overflow-hidden px-1 pb-2">
|
||||
<header className="flex w-full items-center gap-2 rounded p-1">
|
||||
<div className="flex w-full min-w-0 items-center gap-1 pr-1">
|
||||
<h2 className="caption overflow-hidden text-ellipsis whitespace-nowrap">{project.type}</h2>
|
||||
<h2 className="caption overflow-hidden text-ellipsis whitespace-nowrap text-gray-500">{project.type}</h2>
|
||||
</div>
|
||||
<WorkspaceDropdownMenu
|
||||
projectId={project.id}
|
||||
|
@ -5,24 +5,31 @@ import { Project } from '@/types';
|
||||
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '../ui/select';
|
||||
import useCanvasStore from '@/stores/useCanvasStore';
|
||||
import { webPath } from '@/router';
|
||||
import { Suspense } from 'react';
|
||||
import { Suspense, useEffect } from 'react';
|
||||
|
||||
export default function WorkspaceSidebar({ workspaceName, projects }: { workspaceName: string; projects: Project[] }) {
|
||||
const { projectId: selectedProjectId } = useParams<{ projectId: string }>();
|
||||
const { setImage } = useCanvasStore();
|
||||
const { projectId: selectedProjectId, workspaceId } = useParams<{ projectId: string; workspaceId: string }>();
|
||||
const selectedProject = projects.find((project) => project.id.toString() === selectedProjectId) || null;
|
||||
const setSidebarSize = useCanvasStore((state) => state.setSidebarSize);
|
||||
const navigate = useNavigate();
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||
const handleSelectProject = (projectId: string) => {
|
||||
setImage(null);
|
||||
navigate(`${webPath.workspace()}/${workspaceId}/${projectId}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedProject) {
|
||||
setImage(null);
|
||||
}
|
||||
}, [selectedProject, setImage]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResizablePanel
|
||||
minSize={10}
|
||||
maxSize={35}
|
||||
defaultSize={20}
|
||||
defaultSize={15}
|
||||
className="flex h-full flex-col gap-2 bg-gray-50 p-3"
|
||||
onResize={(size) => setSidebarSize(size)}
|
||||
>
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { ImageResponse, Label, Project } from '@/types';
|
||||
import { ImageResponse, Label } from '@/types';
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface CanvasState {
|
||||
project: Project | null;
|
||||
// project: Project | null;
|
||||
sidebarSize: number;
|
||||
image: ImageResponse | null;
|
||||
labels: Label[];
|
||||
drawState: 'pen' | 'rect' | 'pointer';
|
||||
selectedLabelId: number | null;
|
||||
setProject: (project: Project | null) => void;
|
||||
// setProject: (project: Project | null) => void;
|
||||
setSidebarSize: (width: number) => void;
|
||||
setImage: (image: ImageResponse) => void;
|
||||
setImage: (image: ImageResponse | null) => void;
|
||||
setLabels: (labels: Label[]) => void;
|
||||
addLabel: (label: Label) => void;
|
||||
removeLabel: (labelId: number) => void;
|
||||
@ -20,13 +20,13 @@ interface CanvasState {
|
||||
}
|
||||
|
||||
const useCanvasStore = create<CanvasState>()((set) => ({
|
||||
project: null,
|
||||
// project: null,
|
||||
sidebarSize: 20,
|
||||
image: null,
|
||||
labels: [],
|
||||
drawState: 'pointer',
|
||||
selectedLabelId: null,
|
||||
setProject: (project) => set({ project }),
|
||||
// setProject: (project) => set({ project }),
|
||||
setSidebarSize: (width) => set({ sidebarSize: width }),
|
||||
setImage: (image) => set({ image }),
|
||||
addLabel: (label: Label) => set((state) => ({ labels: [...state.labels, label] })),
|
||||
|
@ -1,22 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { FolderResponse } from '@/types';
|
||||
|
||||
interface FolderState {
|
||||
folder: FolderResponse | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
setFolder: (folder: FolderResponse | null) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
}
|
||||
|
||||
const useFolderStore = create<FolderState>((set) => ({
|
||||
folder: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
setFolder: (folder) => set({ folder }),
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setError: (error) => set({ error }),
|
||||
}));
|
||||
|
||||
export default useFolderStore;
|
14
frontend/src/stores/useProjectStore.ts
Normal file
14
frontend/src/stores/useProjectStore.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { create } from 'zustand';
|
||||
import { Project } from '@/types';
|
||||
|
||||
interface ProjectState {
|
||||
project: Project | null;
|
||||
setProject: (project: Project | null) => void;
|
||||
}
|
||||
|
||||
const useProjectStore = create<ProjectState>((set) => ({
|
||||
project: null,
|
||||
setProject: (project) => set({ project }),
|
||||
}));
|
||||
|
||||
export default useProjectStore;
|
@ -68,12 +68,14 @@ export interface FolderResponse {
|
||||
children: ChildFolder[];
|
||||
}
|
||||
|
||||
export type ImageStatus = 'PENDING' | 'IN_PROGRESS' | 'SAVE' | 'REVIEW_REQUEST' | 'COMPLETED';
|
||||
|
||||
export interface ImageResponse {
|
||||
id: number;
|
||||
imageTitle: string;
|
||||
imagePath: string;
|
||||
dataPath: string;
|
||||
status: 'PENDING' | 'IN_PROGRESS' | 'SAVE' | 'REVIEW_REQUEST' | 'COMPLETED';
|
||||
status: ImageStatus;
|
||||
}
|
||||
|
||||
// 이미지 이동 및 상태변경 요청 DTO
|
||||
@ -82,7 +84,7 @@ export interface ImageMoveRequest {
|
||||
}
|
||||
|
||||
export interface ImageStatusChangeRequest {
|
||||
labelStatus: 'PENDING' | 'IN_PROGRESS' | 'SAVE' | 'REVIEW_REQUEST' | 'COMPLETED';
|
||||
labelStatus: ImageStatus;
|
||||
}
|
||||
|
||||
// 멤버 관련 DTO
|
||||
|
Loading…
Reference in New Issue
Block a user