Merge branch 'fe/develop' of https://lab.ssafy.com/s11-s-project/S11P21S002 into fe/refactor/member
This commit is contained in:
commit
b5d5c82a1e
6
frontend/src/api/labelJsonApi.ts
Normal file
6
frontend/src/api/labelJsonApi.ts
Normal 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);
|
||||||
|
}
|
@ -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([
|
||||||
|
@ -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: [],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -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 (
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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', () => {
|
||||||
|
@ -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">
|
||||||
|
9
frontend/src/queries/labelJson/useLabelJsonQuery.ts
Normal file
9
frontend/src/queries/labelJson/useLabelJsonQuery.ts
Normal 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),
|
||||||
|
});
|
||||||
|
}
|
@ -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) })),
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user