Merge branch 'fe/develop' into 'fe/feat/203-auto-label-api'

# Conflicts:
#   frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx
This commit is contained in:
조현수 2024-09-25 15:02:04 +09:00
commit 74e5e9b8ab
11 changed files with 76 additions and 73 deletions

View File

@ -6,4 +6,9 @@ export default {
component: CanvasControlBar,
};
export const Default = () => <CanvasControlBar saveJson={() => {}} />;
export const Default = () => (
<CanvasControlBar
saveJson={() => {}}
projectType="segmentation"
/>
);

View File

@ -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}

View File

@ -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>

View File

@ -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

View File

@ -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>
);
}

View File

@ -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}

View File

@ -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)}
>

View File

@ -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] })),

View File

@ -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;

View 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;

View File

@ -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