Merge branch 'fe/develop' of https://lab.ssafy.com/s11-s-project/S11P21S002 into fe/refactor/member

This commit is contained in:
정현조 2024-09-20 17:44:08 +09:00
commit b5d5c82a1e
11 changed files with 133 additions and 79 deletions

View File

@ -0,0 +1,6 @@
import { LabelJson } from '@/types';
import axios from 'axios';
export async function getLabelJson(jsonPath: string) {
return axios.get<LabelJson>(jsonPath).then(({ data }) => data);
}

View File

@ -7,8 +7,13 @@ import LabelRect from './LabelRect';
import { Vector2d } from 'konva/lib/types'; import { Vector2d } from 'konva/lib/types';
import LabelPolygon from './LabelPolygon'; import LabelPolygon from './LabelPolygon';
import CanvasControlBar from '../CanvasControlBar'; import CanvasControlBar from '../CanvasControlBar';
import useLabelJsonQuery from '@/queries/labelJson/useLabelJsonQuery';
import { Label } from '@/types';
export default function ImageCanvas() { export default function ImageCanvas() {
const { imagePath, dataPath } = useCanvasStore((state) => state.image)!;
const { data: labelData } = useLabelJsonQuery(dataPath);
const { shapes } = labelData;
const selectedLabelId = useCanvasStore((state) => state.selectedLabelId); const selectedLabelId = useCanvasStore((state) => state.selectedLabelId);
const setSelectedLabelId = useCanvasStore((state) => state.setSelectedLabelId); const setSelectedLabelId = useCanvasStore((state) => state.setSelectedLabelId);
const sidebarSize = useCanvasStore((state) => state.sidebarSize); const sidebarSize = useCanvasStore((state) => state.sidebarSize);
@ -17,14 +22,27 @@ export default function ImageCanvas() {
const stageRef = useRef<Konva.Stage>(null); const stageRef = useRef<Konva.Stage>(null);
const dragLayerRef = useRef<Konva.Layer>(null); const dragLayerRef = useRef<Konva.Layer>(null);
const scale = useRef<number>(0); const scale = useRef<number>(0);
const imageUrl = useCanvasStore((state) => state.image);
const labels = useCanvasStore((state) => state.labels) ?? []; const labels = useCanvasStore((state) => state.labels) ?? [];
const [image] = useImage(imageUrl); const [image] = useImage(imagePath);
const [rectPoints, setRectPoints] = useState<[number, number][]>([]);
const [polygonPoints, setPolygonPoints] = useState<[number, number][]>([]);
const drawState = useCanvasStore((state) => state.drawState); const drawState = useCanvasStore((state) => state.drawState);
const setDrawState = useCanvasStore((state) => state.setDrawState); const setDrawState = useCanvasStore((state) => state.setDrawState);
const addLabel = useCanvasStore((state) => state.addLabel); const addLabel = useCanvasStore((state) => state.addLabel);
const setLabels = useCanvasStore((state) => state.setLabels);
const [rectPoints, setRectPoints] = useState<[number, number][]>([]);
const [polygonPoints, setPolygonPoints] = useState<[number, number][]>([]);
useEffect(() => {
setLabels(
shapes.map<Label>(({ label, color, points, shape_type }, index) => ({
id: index,
name: label,
color,
type: shape_type === 'polygon' ? 'polygon' : 'rect',
coordinates: points,
}))
);
}, [setLabels, shapes]);
const startDrawRect = () => { const startDrawRect = () => {
const { x, y } = stageRef.current!.getRelativePointerPosition()!; const { x, y } = stageRef.current!.getRelativePointerPosition()!;
setRectPoints([ setRectPoints([

View File

@ -34,10 +34,7 @@ export default function WorkspaceLayout() {
(project): Project => ({ (project): Project => ({
id: project.id, id: project.id,
name: project.title, name: project.title,
type: (project.projectType.charAt(0).toUpperCase() + project.projectType.slice(1)) as type: project.projectType,
| 'Classification'
| 'Detection'
| 'Segmentation',
children: [], children: [],
}) })
); );

View File

@ -6,7 +6,7 @@ import useCanvasStore from '@/stores/useCanvasStore';
export default function ProjectFileItem({ export default function ProjectFileItem({
className = '', className = '',
item, item,
depth = 1, depth = 0,
selected, selected,
}: { }: {
className?: string; className?: string;
@ -15,22 +15,11 @@ export default function ProjectFileItem({
selected: boolean; selected: boolean;
}) { }) {
const paddingLeft = depth * 12; const paddingLeft = depth * 12;
const changeImage = useCanvasStore((state) => state.changeImage); // const changeImage = useCanvasStore((state) => state.changeImage);
const setImage = useCanvasStore((state) => state.setImage);
const handleClick = () => { const handleClick = () => {
// TODO: fetch image setImage(item);
changeImage(item.imageUrl, [
{
id: item.id,
name: item.imageTitle,
type: 'rect',
color: '#FF0000',
coordinates: [
[0, 0],
[100, 100],
],
},
]);
}; };
return ( return (

View File

@ -9,6 +9,18 @@ export default function ProjectStructure({ project }: { project: Project }) {
const image = useCanvasStore((state) => state.image); const image = useCanvasStore((state) => state.image);
const { data: folderData } = useFolderQuery(project.id.toString(), 0); const { data: folderData } = useFolderQuery(project.id.toString(), 0);
// TODO: 더미 데이터 제거
folderData.images = [
{
id: 1,
imagePath: 'https://worlabel-file-bucket.s3.ap-northeast-2.amazonaws.com/dummy.jpg',
// dataPath: 'https://worlabel-file-bucket.s3.ap-northeast-2.amazonaws.com/detection.json',
dataPath: 'https://worlabel-file-bucket.s3.ap-northeast-2.amazonaws.com/segmentation.json',
imageTitle: 'dummy',
status: 'PENDING',
},
];
return ( return (
<div className="flex h-full flex-col overflow-y-auto px-1 pb-2"> <div className="flex h-full flex-col overflow-y-auto px-1 pb-2">
<header className="flex w-full items-center gap-2 rounded p-1"> <header className="flex w-full items-center gap-2 rounded p-1">
@ -45,7 +57,7 @@ export default function ProjectStructure({ project }: { project: Project }) {
<ProjectFileItem <ProjectFileItem
key={`${project.id}-${item.imageTitle}`} key={`${project.id}-${item.imageTitle}`}
item={item} item={item}
selected={image === item.imageUrl} selected={image?.id === item.id}
/> />
))} ))}
</div> </div>

View File

@ -19,7 +19,7 @@ const projects: Project[] = [
{ {
id: 1, id: 1,
name: 'project-111', name: 'project-111',
type: 'Segmentation', type: 'segmentation',
children: [ children: [
{ {
id: 12, id: 12,
@ -45,7 +45,7 @@ const projects: Project[] = [
{ {
id: 2, id: 2,
name: 'very-extremely-long-long-project-name-222', name: 'very-extremely-long-long-project-name-222',
type: 'Classification', type: 'classification',
children: [ children: [
{ {
id: 23, id: 23,

View File

@ -242,51 +242,54 @@
// return HttpResponse.json(members); // return HttpResponse.json(members);
// }), // }),
// // Folder and Image Handlers // Folder and Image Handlers
// http.get('/api/projects/:projectId/folders/:folderId', ({ params }) => { http.get('/api/projects/:projectId/folders/:folderId', ({ params }) => {
// const { folderId } = params; const { folderId } = params;
// const response: FolderResponse = { const response: FolderResponse = {
// id: parseInt(folderId as string, 10), id: parseInt(folderId as string, 10),
// title: 'My Folder', title: 'My Folder',
// images: [ images: [
// { {
// id: 1, id: 1,
// imageTitle: 'image.jpg', imageTitle: 'image.jpg',
// imageUrl: 'https://example.com/image.jpg', imagePath: 'https://example.com/image.jpg',
// status: 'PENDING', dataPath: 'https://example.com/image.json',
// }, status: 'PENDING',
// { },
// id: 2, {
// imageTitle: 'another_image.jpg', id: 2,
// imageUrl: 'https://example.com/another_image.jpg', imageTitle: 'another_image.jpg',
// status: 'IN_PROGRESS', imagePath: 'https://example.com/another_image.jpg',
// }, dataPath: 'https://example.com/another_image.json',
// ], status: 'IN_PROGRESS',
// children: [ },
// { ],
// id: 1, children: [
// title: 'Car', {
// }, id: 1,
// { title: 'Car',
// id: 2, },
// title: 'Bike', {
// }, id: 2,
// ], title: 'Bike',
// }; },
// return HttpResponse.json(response); ],
// }), };
return HttpResponse.json(response);
}),
// http.get('/api/projects/:projectId/folders/:folderId/images/:imageId', ({ params }) => { http.get('/api/projects/:projectId/folders/:folderId/images/:imageId', ({ params }) => {
// // 이미지 조회 핸들러 // 이미지 조회 핸들러
// const { imageId } = params; const { imageId } = params;
// const response: ImageResponse = { const response: ImageResponse = {
// id: parseInt(imageId as string, 10), id: parseInt(imageId as string, 10),
// imageTitle: 'Image Title', imageTitle: 'Image Title',
// imageUrl: 'image-url.jpg', imagePath: 'image-url.jpg',
// status: 'PENDING', dataPath: 'data-url.json',
// }; status: 'PENDING',
// return HttpResponse.json(response); };
// }), return HttpResponse.json(response);
}),
// // Auto Labeling Handler // // Auto Labeling Handler
// http.post('/api/projects/:projectId/label/auto', () => { // http.post('/api/projects/:projectId/label/auto', () => {

View File

@ -5,13 +5,13 @@ import useCanvasStore from '@/stores/useCanvasStore';
import { Suspense } from 'react'; import { Suspense } from 'react';
export default function LabelCanvas() { export default function LabelCanvas() {
const imageUrl = useCanvasStore((state) => state.image); const image = useCanvasStore((state) => state.image);
return ( return (
<ResizablePanel className="flex w-full items-center"> <ResizablePanel className="flex w-full items-center">
<Suspense fallback={<div></div>}> <Suspense fallback={<div></div>}>
<main className="h-full grow"> <main className="h-full grow">
{imageUrl ? ( {image ? (
<ImageCanvas /> <ImageCanvas />
) : ( ) : (
<div className="body flex h-full w-full select-none items-center justify-center bg-gray-200 text-gray-400"> <div className="body flex h-full w-full select-none items-center justify-center bg-gray-200 text-gray-400">

View File

@ -0,0 +1,9 @@
import { getLabelJson } from '@/api/labelJsonApi';
import { useSuspenseQuery } from '@tanstack/react-query';
export default function useLabelJsonQuery(jsonPath: string) {
return useSuspenseQuery({
queryKey: ['labelJson', jsonPath],
queryFn: () => getLabelJson(jsonPath),
});
}

View File

@ -1,14 +1,14 @@
import { Label } from '@/types'; import { ImageResponse, Label } from '@/types';
import { create } from 'zustand'; import { create } from 'zustand';
interface CanvasState { interface CanvasState {
sidebarSize: number; sidebarSize: number;
image: string; image: ImageResponse | null;
labels: Label[]; labels: Label[];
drawState: 'pen' | 'rect' | 'pointer'; drawState: 'pen' | 'rect' | 'pointer';
selectedLabelId: number | null; selectedLabelId: number | null;
setSidebarSize: (width: number) => void; setSidebarSize: (width: number) => void;
changeImage: (image: string, labels: Label[]) => void; setImage: (image: ImageResponse) => void;
setLabels: (labels: Label[]) => void; setLabels: (labels: Label[]) => void;
addLabel: (label: Label) => void; addLabel: (label: Label) => void;
removeLabel: (labelId: number) => void; removeLabel: (labelId: number) => void;
@ -19,12 +19,12 @@ interface CanvasState {
const useCanvasStore = create<CanvasState>()((set) => ({ const useCanvasStore = create<CanvasState>()((set) => ({
sidebarSize: 20, sidebarSize: 20,
image: '', image: null,
labels: [], labels: [],
drawState: 'pointer', drawState: 'pointer',
selectedLabelId: null, selectedLabelId: null,
setSidebarSize: (width) => set({ sidebarSize: width }), setSidebarSize: (width) => set({ sidebarSize: width }),
changeImage: (image: string, labels: Label[]) => set({ image, labels }), setImage: (image) => set({ image }),
addLabel: (label: Label) => set((state) => ({ labels: [...state.labels, label] })), addLabel: (label: Label) => set((state) => ({ labels: [...state.labels, label] })),
setLabels: (labels) => set({ labels }), setLabels: (labels) => set({ labels }),
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) })),

View File

@ -18,7 +18,7 @@ export type DirectoryItem = {
export type Project = { export type Project = {
id: number; id: number;
name: string; name: string;
type: 'Classification' | 'Detection' | 'Segmentation'; type: 'classification' | 'detection' | 'segmentation';
children: Array<DirectoryItem | FileItem>; children: Array<DirectoryItem | FileItem>;
}; };
@ -71,7 +71,8 @@ export interface FolderResponse {
export interface ImageResponse { export interface ImageResponse {
id: number; id: number;
imageTitle: string; imageTitle: string;
imageUrl: string; imagePath: string;
dataPath: string;
status: 'PENDING' | 'IN_PROGRESS' | 'SAVE' | 'REVIEW_REQUEST' | 'COMPLETED'; status: 'PENDING' | 'IN_PROGRESS' | 'SAVE' | 'REVIEW_REQUEST' | 'COMPLETED';
} }
@ -246,3 +247,22 @@ export interface ImageDetailResponse {
export interface RefreshTokenResponse { export interface RefreshTokenResponse {
accessToken: string; accessToken: string;
} }
export interface Shape {
label: string;
color: string;
points: [number, number][];
group_id: number;
shape_type: 'polygon' | 'rectangle';
flags: Record<string, never>;
}
export interface LabelJson {
version: string;
task_type: 'det' | 'seg';
shapes: Shape[];
split: string;
imageHeight: number;
imageWidth: number;
imageDepth: number;
}