Merge branch 'fe/develop' into fe/refactor/fcm
This commit is contained in:
commit
876b8108d1
4
frontend/src/assets/icons/chevron-down.svg
Normal file
4
frontend/src/assets/icons/chevron-down.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6 9L12 15L18 9" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 336 B |
4
frontend/src/assets/icons/chevron-up.svg
Normal file
4
frontend/src/assets/icons/chevron-up.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6 15L12 9L18 15" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 337 B |
19
frontend/src/assets/icons/delete.svg
Normal file
19
frontend/src/assets/icons/delete.svg
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 -0.5 21 21" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
|
||||||
|
<title>delete [#1487]</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs>
|
||||||
|
|
||||||
|
</defs>
|
||||||
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Dribbble-Light-Preview" transform="translate(-179.000000, -360.000000)" fill="#000000">
|
||||||
|
<g id="icons" transform="translate(56.000000, 160.000000)">
|
||||||
|
<path d="M130.35,216 L132.45,216 L132.45,208 L130.35,208 L130.35,216 Z M134.55,216 L136.65,216 L136.65,208 L134.55,208 L134.55,216 Z M128.25,218 L138.75,218 L138.75,206 L128.25,206 L128.25,218 Z M130.35,204 L136.65,204 L136.65,202 L130.35,202 L130.35,204 Z M138.75,204 L138.75,200 L128.25,200 L128.25,204 L123,204 L123,206 L126.15,206 L126.15,220 L140.85,220 L140.85,206 L144,206 L144,204 L138.75,204 Z" id="delete-[#1487]">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
@ -1,6 +1,7 @@
|
|||||||
import useCanvasStore from '@/stores/useCanvasStore';
|
import useCanvasStore from '@/stores/useCanvasStore';
|
||||||
import { LucideIcon, MousePointer2, PenTool, Plus, Save, Square } from 'lucide-react';
|
import { LucideIcon, MousePointer2, PenTool, Plus, Save, Square } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { MessageSquare } from 'lucide-react';
|
||||||
|
|
||||||
export default function CanvasControlBar({
|
export default function CanvasControlBar({
|
||||||
saveJson,
|
saveJson,
|
||||||
@ -10,13 +11,14 @@ export default function CanvasControlBar({
|
|||||||
projectType: 'classification' | 'detection' | 'segmentation';
|
projectType: 'classification' | 'detection' | 'segmentation';
|
||||||
}) {
|
}) {
|
||||||
const { drawState, setDrawState, setLabels, labels } = useCanvasStore();
|
const { drawState, setDrawState, setLabels, labels } = useCanvasStore();
|
||||||
const buttonBaseClassName = 'rounded-lg p-2 transition-colors ';
|
const buttonBaseClassName = 'rounded-lg p-2 transition-colors';
|
||||||
const buttonClassName = 'hover:bg-gray-100';
|
const buttonClassName = 'hover:bg-gray-100';
|
||||||
const activeButtonClassName = 'bg-primary stroke-white';
|
const activeButtonClassName = 'bg-primary stroke-white';
|
||||||
|
|
||||||
const controls: { [key: string]: LucideIcon } = {
|
const controls: { [key: string]: LucideIcon } = {
|
||||||
pointer: MousePointer2,
|
pointer: MousePointer2,
|
||||||
...(projectType === 'segmentation' ? { pen: PenTool } : projectType === 'detection' ? { rect: Square } : null),
|
...(projectType === 'segmentation' ? { pen: PenTool } : projectType === 'detection' ? { rect: Square } : null),
|
||||||
|
comment: MessageSquare,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -36,6 +38,7 @@ export default function CanvasControlBar({
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{projectType === 'classification' && labels.length === 0 && (
|
{projectType === 'classification' && labels.length === 0 && (
|
||||||
<button
|
<button
|
||||||
className={cn(buttonClassName, buttonBaseClassName)}
|
className={cn(buttonClassName, buttonBaseClassName)}
|
||||||
@ -57,7 +60,9 @@ export default function CanvasControlBar({
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="h-5 w-0.5 rounded bg-gray-400" />
|
<div className="h-5 w-0.5 rounded bg-gray-400" />
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={cn(buttonClassName, buttonBaseClassName)}
|
className={cn(buttonClassName, buttonBaseClassName)}
|
||||||
onClick={saveJson}
|
onClick={saveJson}
|
||||||
|
105
frontend/src/components/ImageCanvas/CommentLabel.tsx
Normal file
105
frontend/src/components/ImageCanvas/CommentLabel.tsx
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Group, Rect, Text, Image } from 'react-konva';
|
||||||
|
import { CommentResponse } from '@/types';
|
||||||
|
import useImage from 'use-image';
|
||||||
|
|
||||||
|
import deleteIconSrc from '@/assets/icons/delete.svg';
|
||||||
|
import toggleUpIconSrc from '@/assets/icons/chevron-up.svg';
|
||||||
|
import toggleDownIconSrc from '@/assets/icons/chevron-down.svg';
|
||||||
|
import Konva from 'konva';
|
||||||
|
|
||||||
|
interface CommentLabelProps {
|
||||||
|
comment: CommentResponse & { isOpen?: boolean };
|
||||||
|
updateComment: (comment: CommentResponse) => void;
|
||||||
|
deleteComment: (commentId: number) => void;
|
||||||
|
toggleComment: (commentId: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CommentLabel({ comment, updateComment, deleteComment, toggleComment }: CommentLabelProps) {
|
||||||
|
const [content, setContent] = useState(comment.content);
|
||||||
|
const [deleteIcon] = useImage(deleteIconSrc);
|
||||||
|
const [toggleUpIcon] = useImage(toggleUpIconSrc);
|
||||||
|
const [toggleDownIcon] = useImage(toggleDownIconSrc);
|
||||||
|
|
||||||
|
const handleEdit = () => {
|
||||||
|
const newContent = prompt('댓글을 입력하세요', content);
|
||||||
|
if (newContent !== null) {
|
||||||
|
setContent(newContent);
|
||||||
|
updateComment({ ...comment, content: newContent });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||||
|
e.cancelBubble = true;
|
||||||
|
deleteComment(comment.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggle = (e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||||
|
e.cancelBubble = true;
|
||||||
|
toggleComment(comment.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
x={comment.positionX}
|
||||||
|
y={comment.positionY}
|
||||||
|
draggable
|
||||||
|
onDragEnd={(e) => {
|
||||||
|
const newX = e.target.x();
|
||||||
|
const newY = e.target.y();
|
||||||
|
updateComment({ ...comment, positionX: newX, positionY: newY });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Rect
|
||||||
|
width={comment.isOpen ? 200 : 50}
|
||||||
|
height={comment.isOpen ? 100 : 30}
|
||||||
|
fill="white"
|
||||||
|
stroke="black"
|
||||||
|
onClick={handleEdit}
|
||||||
|
/>
|
||||||
|
{comment.isOpen && (
|
||||||
|
<Text
|
||||||
|
x={5}
|
||||||
|
y={5}
|
||||||
|
width={190}
|
||||||
|
text={content || '내용 없음'}
|
||||||
|
fontSize={16}
|
||||||
|
fill="black"
|
||||||
|
onClick={handleEdit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{deleteIcon && (
|
||||||
|
<Image
|
||||||
|
image={deleteIcon}
|
||||||
|
x={comment.isOpen ? 175 : 25}
|
||||||
|
y={5}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
onClick={handleDelete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{comment.isOpen
|
||||||
|
? toggleUpIcon && (
|
||||||
|
<Image
|
||||||
|
image={toggleUpIcon}
|
||||||
|
x={comment.isOpen ? 150 : 0}
|
||||||
|
y={5}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
onClick={handleToggle}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: toggleDownIcon && (
|
||||||
|
<Image
|
||||||
|
image={toggleDownIcon}
|
||||||
|
x={comment.isOpen ? 150 : 0}
|
||||||
|
y={5}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
onClick={handleToggle}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,9 @@
|
|||||||
import useCanvasStore from '@/stores/useCanvasStore';
|
import useCanvasStore from '@/stores/useCanvasStore';
|
||||||
|
import useCommentStore from '@/stores/useCommentStore';
|
||||||
|
import useCommentListQuery from '@/queries/comments/useCommentListQuery';
|
||||||
|
import useCreateCommentQuery from '@/queries/comments/useCreateCommentQuery';
|
||||||
|
import useUpdateCommentQuery from '@/queries/comments/useUpdateCommentQuery';
|
||||||
|
import useDeleteCommentQuery from '@/queries/comments/useDeleteCommentQuery';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Circle, Image, Layer, Line, Rect, Stage } from 'react-konva';
|
import { Circle, Image, Layer, Line, Rect, Stage } from 'react-konva';
|
||||||
@ -13,6 +18,8 @@ import useProjectStore from '@/stores/useProjectStore';
|
|||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import useSaveImageLabelsQuery from '@/queries/projects/useSaveImageLabelsQuery';
|
import useSaveImageLabelsQuery from '@/queries/projects/useSaveImageLabelsQuery';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
import CommentLabel from './CommentLabel';
|
||||||
|
import useAuthStore from '@/stores/useAuthStore';
|
||||||
|
|
||||||
export default function ImageCanvas() {
|
export default function ImageCanvas() {
|
||||||
const { project, folderId, categories } = useProjectStore();
|
const { project, folderId, categories } = useProjectStore();
|
||||||
@ -33,6 +40,13 @@ export default function ImageCanvas() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const { profile } = useAuthStore();
|
||||||
|
const { comments, setComments } = useCommentStore();
|
||||||
|
const { data: commentList } = useCommentListQuery(project!.id, imageId);
|
||||||
|
const createCommentMutation = useCreateCommentQuery(project!.id, imageId);
|
||||||
|
const updateCommentMutation = useUpdateCommentQuery(project!.id);
|
||||||
|
const deleteCommentMutation = useDeleteCommentQuery(project!.id);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLabels(
|
setLabels(
|
||||||
shapes.map<Label>(({ group_id, color, points, shape_type }, index) => ({
|
shapes.map<Label>(({ group_id, color, points, shape_type }, index) => ({
|
||||||
@ -49,11 +63,23 @@ export default function ImageCanvas() {
|
|||||||
setSelectedLabelId(null);
|
setSelectedLabelId(null);
|
||||||
}, [image, setSelectedLabelId]);
|
}, [image, setSelectedLabelId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (commentList) {
|
||||||
|
setComments(
|
||||||
|
commentList.map((comment) => ({
|
||||||
|
...comment,
|
||||||
|
isOpen: false,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [commentList, setComments]);
|
||||||
|
|
||||||
const setLabel = (index: number) => (coordinates: [number, number][]) => {
|
const setLabel = (index: number) => (coordinates: [number, number][]) => {
|
||||||
const newLabels = [...labels];
|
const newLabels = [...labels];
|
||||||
newLabels[index].coordinates = coordinates;
|
newLabels[index].coordinates = coordinates;
|
||||||
setLabels(newLabels);
|
setLabels(newLabels);
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveJson = () => {
|
const saveJson = () => {
|
||||||
const json = JSON.stringify({
|
const json = JSON.stringify({
|
||||||
...labelData,
|
...labelData,
|
||||||
@ -86,6 +112,7 @@ export default function ImageCanvas() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const startDrawRect = () => {
|
const startDrawRect = () => {
|
||||||
const { x, y } = stageRef.current!.getRelativePointerPosition()!;
|
const { x, y } = stageRef.current!.getRelativePointerPosition()!;
|
||||||
setRectPoints([
|
setRectPoints([
|
||||||
@ -93,6 +120,7 @@ export default function ImageCanvas() {
|
|||||||
[x, y],
|
[x, y],
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addPointToPolygon = () => {
|
const addPointToPolygon = () => {
|
||||||
const { x, y } = stageRef.current!.getRelativePointerPosition()!;
|
const { x, y } = stageRef.current!.getRelativePointerPosition()!;
|
||||||
if (polygonPoints.length === 0) {
|
if (polygonPoints.length === 0) {
|
||||||
@ -116,20 +144,24 @@ export default function ImageCanvas() {
|
|||||||
}
|
}
|
||||||
setPolygonPoints([...polygonPoints, [x, y]]);
|
setPolygonPoints([...polygonPoints, [x, y]]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeLastPointOfPolygon = (e: MouseEvent) => {
|
const removeLastPointOfPolygon = (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (polygonPoints.length === 0) return;
|
if (polygonPoints.length === 0) return;
|
||||||
setPolygonPoints(polygonPoints.slice(0, -1));
|
setPolygonPoints(polygonPoints.slice(0, -1));
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveLastPointOfPolygon = () => {
|
const moveLastPointOfPolygon = () => {
|
||||||
if (polygonPoints.length < 2) return;
|
if (polygonPoints.length < 2) return;
|
||||||
const { x, y } = stageRef.current!.getRelativePointerPosition()!;
|
const { x, y } = stageRef.current!.getRelativePointerPosition()!;
|
||||||
setPolygonPoints([...polygonPoints.slice(0, -1), [x, y]]);
|
setPolygonPoints([...polygonPoints.slice(0, -1), [x, y]]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const endDrawPolygon = () => {
|
const endDrawPolygon = () => {
|
||||||
if (drawState !== 'pen' || polygonPoints.length === 0) return;
|
if (drawState !== 'pen' || polygonPoints.length === 0) return;
|
||||||
setDrawState('pointer');
|
setDrawState('pointer');
|
||||||
setPolygonPoints([]);
|
setPolygonPoints([]);
|
||||||
|
|
||||||
if (polygonPoints.length < 4) return;
|
if (polygonPoints.length < 4) return;
|
||||||
|
|
||||||
const color = Math.floor(Math.random() * 0xffffff)
|
const color = Math.floor(Math.random() * 0xffffff)
|
||||||
@ -146,12 +178,14 @@ export default function ImageCanvas() {
|
|||||||
setDrawState('pointer');
|
setDrawState('pointer');
|
||||||
setSelectedLabelId(id);
|
setSelectedLabelId(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateDrawingRect = () => {
|
const updateDrawingRect = () => {
|
||||||
if (rectPoints.length === 0) return;
|
if (rectPoints.length === 0) return;
|
||||||
|
|
||||||
const { x, y } = stageRef.current!.getRelativePointerPosition()!;
|
const { x, y } = stageRef.current!.getRelativePointerPosition()!;
|
||||||
setRectPoints([rectPoints[0], [x, y]]);
|
setRectPoints([rectPoints[0], [x, y]]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const endDrawRect = () => {
|
const endDrawRect = () => {
|
||||||
if (drawState !== 'rect' || rectPoints.length === 0) return;
|
if (drawState !== 'rect' || rectPoints.length === 0) return;
|
||||||
if (rectPoints[0][0] === rectPoints[1][0] && rectPoints[0][1] === rectPoints[1][1]) {
|
if (rectPoints[0][0] === rectPoints[1][0] && rectPoints[0][1] === rectPoints[1][1]) {
|
||||||
@ -159,6 +193,7 @@ export default function ImageCanvas() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setRectPoints([]);
|
setRectPoints([]);
|
||||||
|
|
||||||
const color = Math.floor(Math.random() * 0xffffff)
|
const color = Math.floor(Math.random() * 0xffffff)
|
||||||
.toString(16)
|
.toString(16)
|
||||||
.padStart(6, '0');
|
.padStart(6, '0');
|
||||||
@ -173,12 +208,47 @@ export default function ImageCanvas() {
|
|||||||
setDrawState('pointer');
|
setDrawState('pointer');
|
||||||
setSelectedLabelId(id);
|
setSelectedLabelId(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (e: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => {
|
const handleClick = (e: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => {
|
||||||
e.evt.preventDefault();
|
e.evt.preventDefault();
|
||||||
e.evt.stopPropagation();
|
e.evt.stopPropagation();
|
||||||
const isLeftClicked = e.evt.type === 'mousedown' && (e.evt as MouseEvent).button === 0;
|
const isLeftClicked = e.evt.type === 'mousedown' && (e.evt as MouseEvent).button === 0;
|
||||||
const isRightClicked = e.evt.type === 'mousedown' && (e.evt as MouseEvent).button === 2;
|
const isRightClicked = e.evt.type === 'mousedown' && (e.evt as MouseEvent).button === 2;
|
||||||
|
|
||||||
|
if (drawState === 'comment' && isLeftClicked) {
|
||||||
|
const stage = stageRef.current;
|
||||||
|
if (!stage) return;
|
||||||
|
|
||||||
|
const pointerPosition = stage.getRelativePointerPosition();
|
||||||
|
if (!pointerPosition) return;
|
||||||
|
|
||||||
|
if (!profile) {
|
||||||
|
console.error('User profile is not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = pointerPosition.x;
|
||||||
|
const y = pointerPosition.y;
|
||||||
|
|
||||||
|
createCommentMutation.mutate({
|
||||||
|
id: 0,
|
||||||
|
content: '',
|
||||||
|
positionX: x,
|
||||||
|
positionY: y,
|
||||||
|
memberId: profile.id,
|
||||||
|
memberNickname: profile.nickname,
|
||||||
|
memberProfileImage: profile.profileImage,
|
||||||
|
createTime: new Date().toISOString(),
|
||||||
|
author: {
|
||||||
|
id: profile.id,
|
||||||
|
nickname: profile.nickname,
|
||||||
|
profileImage: profile.profileImage,
|
||||||
|
email: profile.email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (drawState !== 'pointer' && (isLeftClicked || isRightClicked)) {
|
if (drawState !== 'pointer' && (isLeftClicked || isRightClicked)) {
|
||||||
stageRef.current?.stopDrag();
|
stageRef.current?.stopDrag();
|
||||||
if (drawState === 'rect') {
|
if (drawState === 'rect') {
|
||||||
@ -193,6 +263,7 @@ export default function ImageCanvas() {
|
|||||||
setSelectedLabelId(null);
|
setSelectedLabelId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseMove = () => {
|
const handleMouseMove = () => {
|
||||||
if (drawState === 'rect' && rectPoints.length) {
|
if (drawState === 'rect' && rectPoints.length) {
|
||||||
updateDrawingRect();
|
updateDrawingRect();
|
||||||
@ -201,6 +272,7 @@ export default function ImageCanvas() {
|
|||||||
moveLastPointOfPolygon();
|
moveLastPointOfPolygon();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleZoom = (e: Konva.KonvaEventObject<WheelEvent>) => {
|
const handleZoom = (e: Konva.KonvaEventObject<WheelEvent>) => {
|
||||||
const scaleBy = 1.05;
|
const scaleBy = 1.05;
|
||||||
const oldScale = scale.current;
|
const oldScale = scale.current;
|
||||||
@ -219,6 +291,7 @@ export default function ImageCanvas() {
|
|||||||
stageRef.current?.position(newPos);
|
stageRef.current?.position(newPos);
|
||||||
stageRef.current?.batchDraw();
|
stageRef.current?.batchDraw();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleScroll = (e: Konva.KonvaEventObject<WheelEvent>) => {
|
const handleScroll = (e: Konva.KonvaEventObject<WheelEvent>) => {
|
||||||
const delta = -e.evt.deltaY;
|
const delta = -e.evt.deltaY;
|
||||||
const x = stageRef.current?.x();
|
const x = stageRef.current?.x();
|
||||||
@ -235,6 +308,7 @@ export default function ImageCanvas() {
|
|||||||
|
|
||||||
e.evt.ctrlKey ? handleZoom(e) : handleScroll(e);
|
e.evt.ctrlKey ? handleZoom(e) : handleScroll(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getScale = (): Vector2d => {
|
const getScale = (): Vector2d => {
|
||||||
if (scale.current) return { x: scale.current, y: scale.current };
|
if (scale.current) return { x: scale.current, y: scale.current };
|
||||||
const widthRatio = stageWidth / image!.width;
|
const widthRatio = stageWidth / image!.width;
|
||||||
@ -271,7 +345,7 @@ export default function ImageCanvas() {
|
|||||||
width={stageWidth}
|
width={stageWidth}
|
||||||
height={stageHeight}
|
height={stageHeight}
|
||||||
className="overflow-hidden bg-gray-200"
|
className="overflow-hidden bg-gray-200"
|
||||||
draggable
|
draggable={drawState !== 'comment'}
|
||||||
onWheel={handleWheel}
|
onWheel={handleWheel}
|
||||||
onMouseDown={handleClick}
|
onMouseDown={handleClick}
|
||||||
onTouchStart={handleClick}
|
onTouchStart={handleClick}
|
||||||
@ -285,6 +359,7 @@ export default function ImageCanvas() {
|
|||||||
<Layer>
|
<Layer>
|
||||||
<Image image={image} />
|
<Image image={image} />
|
||||||
</Layer>
|
</Layer>
|
||||||
|
|
||||||
{project?.type !== 'classification' && (
|
{project?.type !== 'classification' && (
|
||||||
<Layer listening={drawState === 'pointer'}>
|
<Layer listening={drawState === 'pointer'}>
|
||||||
{labels.map((label) =>
|
{labels.map((label) =>
|
||||||
@ -357,6 +432,30 @@ export default function ImageCanvas() {
|
|||||||
</Layer>
|
</Layer>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Layer>
|
||||||
|
{comments.map((comment) => (
|
||||||
|
<CommentLabel
|
||||||
|
key={comment.id}
|
||||||
|
comment={comment}
|
||||||
|
updateComment={(updatedComment) => {
|
||||||
|
updateCommentMutation.mutate({
|
||||||
|
commentId: comment.id,
|
||||||
|
commentData: {
|
||||||
|
content: updatedComment.content,
|
||||||
|
positionX: updatedComment.positionX,
|
||||||
|
positionY: updatedComment.positionY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
deleteComment={(commentId) => {
|
||||||
|
deleteCommentMutation.mutate(commentId);
|
||||||
|
}}
|
||||||
|
toggleComment={(commentId) => {
|
||||||
|
useCommentStore.getState().toggleComment(commentId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Layer>
|
||||||
<Layer ref={dragLayerRef} />
|
<Layer ref={dragLayerRef} />
|
||||||
</Stage>
|
</Stage>
|
||||||
<CanvasControlBar
|
<CanvasControlBar
|
||||||
|
@ -115,7 +115,7 @@ export default function ImageUploadFileForm({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{files.length > 0 && (
|
{files.length > 0 && (
|
||||||
<ul className="m-0 list-none p-0">
|
<ul className="m-0 max-h-[200px] list-none overflow-y-auto p-0">
|
||||||
{files.map((file, index) => (
|
{files.map((file, index) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
|
@ -100,7 +100,7 @@ export default function ImageUploadFolderForm({ onClose, projectId }: { onClose:
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{files.length > 0 && (
|
{files.length > 0 && (
|
||||||
<ul className="m-0 list-none p-0">
|
<ul className="m-0 max-h-[200px] list-none overflow-y-auto p-0">
|
||||||
{files.map((file, index) => (
|
{files.map((file, index) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
|
@ -27,6 +27,10 @@ const chartConfig = {
|
|||||||
label: 'Fitness',
|
label: 'Fitness',
|
||||||
color: '#FFD700',
|
color: '#FFD700',
|
||||||
},
|
},
|
||||||
|
segLoss: {
|
||||||
|
label: 'Segmentation Loss',
|
||||||
|
color: '#FF1493',
|
||||||
|
},
|
||||||
} satisfies ChartConfig;
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
export default function ModelLineChart({ data, className }: ModelLineChartProps) {
|
export default function ModelLineChart({ data, className }: ModelLineChartProps) {
|
||||||
@ -36,9 +40,10 @@ export default function ModelLineChart({ data, className }: ModelLineChartProps)
|
|||||||
const emptyData = Array.from({ length: totalEpochs }, (_, i) => ({
|
const emptyData = Array.from({ length: totalEpochs }, (_, i) => ({
|
||||||
epoch: (i + 1).toString(),
|
epoch: (i + 1).toString(),
|
||||||
boxLoss: null,
|
boxLoss: null,
|
||||||
classLoss: null,
|
clsLoss: null,
|
||||||
dflLoss: null,
|
dflLoss: null,
|
||||||
fitness: null,
|
fitness: null,
|
||||||
|
segLoss: null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const filledData = emptyData.map((item, index) => ({
|
const filledData = emptyData.map((item, index) => ({
|
||||||
@ -46,6 +51,18 @@ export default function ModelLineChart({ data, className }: ModelLineChartProps)
|
|||||||
...(data[index] || {}),
|
...(data[index] || {}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const renderLine = (dataKey: keyof ReportResponse, color: string) => {
|
||||||
|
const hasNonZeroData = filledData.some((d) => d[dataKey] !== 0);
|
||||||
|
return hasNonZeroData ? (
|
||||||
|
<Line
|
||||||
|
dataKey={dataKey}
|
||||||
|
type="monotone"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={false}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<Card className={className}>
|
<Card className={className}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@ -79,34 +96,11 @@ export default function ModelLineChart({ data, className }: ModelLineChartProps)
|
|||||||
<YAxis />
|
<YAxis />
|
||||||
<Tooltip />
|
<Tooltip />
|
||||||
<Legend />
|
<Legend />
|
||||||
<Line
|
{renderLine('boxLoss', chartConfig.boxLoss.color)}
|
||||||
dataKey="boxLoss"
|
{renderLine('clsLoss', chartConfig.classLoss.color)}
|
||||||
type="monotone"
|
{renderLine('dflLoss', chartConfig.dflLoss.color)}
|
||||||
stroke={chartConfig.boxLoss.color}
|
{renderLine('fitness', chartConfig.fitness.color)}
|
||||||
strokeWidth={2}
|
{renderLine('segLoss', chartConfig.segLoss.color)}
|
||||||
dot={false}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
dataKey="classLoss"
|
|
||||||
type="monotone"
|
|
||||||
stroke={chartConfig.classLoss.color}
|
|
||||||
strokeWidth={2}
|
|
||||||
dot={false}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
dataKey="dflLoss"
|
|
||||||
type="monotone"
|
|
||||||
stroke={chartConfig.dflLoss.color}
|
|
||||||
strokeWidth={2}
|
|
||||||
dot={false}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
dataKey="fitness"
|
|
||||||
type="monotone"
|
|
||||||
stroke={chartConfig.fitness.color}
|
|
||||||
strokeWidth={2}
|
|
||||||
dot={false}
|
|
||||||
/>
|
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import ModelLineChart from './ModelLineChart';
|
import ModelLineChart from './ModelLineChart';
|
||||||
import usePollingTrainingModelReport from '@/queries/reports/usePollingModelReportsQuery';
|
import usePollingModelReportsQuery from '@/queries/reports/usePollingModelReportsQuery';
|
||||||
import { ModelResponse } from '@/types';
|
import { ModelResponse } from '@/types';
|
||||||
|
|
||||||
interface TrainingGraphProps {
|
interface TrainingGraphProps {
|
||||||
@ -10,24 +10,24 @@ interface TrainingGraphProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function TrainingGraph({ projectId, selectedModel, className }: TrainingGraphProps) {
|
export default function TrainingGraph({ projectId, selectedModel, className }: TrainingGraphProps) {
|
||||||
const isTraining = selectedModel?.isTrain || false;
|
const [isPolling, setIsPolling] = useState(false);
|
||||||
|
const { data: trainingDataList } = usePollingModelReportsQuery(
|
||||||
const { data: fetchedTrainingDataList } = usePollingTrainingModelReport(
|
|
||||||
projectId as number,
|
projectId as number,
|
||||||
selectedModel?.id as number,
|
selectedModel?.id as number,
|
||||||
isTraining
|
isPolling
|
||||||
);
|
);
|
||||||
|
|
||||||
const trainingDataList = useMemo(() => {
|
useEffect(() => {
|
||||||
if (!isTraining) {
|
if (selectedModel) {
|
||||||
return [];
|
setIsPolling(true);
|
||||||
|
} else {
|
||||||
|
setIsPolling(false);
|
||||||
}
|
}
|
||||||
return fetchedTrainingDataList || [];
|
}, [selectedModel]);
|
||||||
}, [isTraining, fetchedTrainingDataList]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModelLineChart
|
<ModelLineChart
|
||||||
data={trainingDataList}
|
data={trainingDataList || []}
|
||||||
className={className}
|
className={className}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import SelectWithLabel from './SelectWithLabel';
|
import SelectWithLabel from './SelectWithLabel';
|
||||||
import InputWithLabel from './InputWithLabel';
|
import InputWithLabel from './InputWithLabel';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import useProjectModelsQuery from '@/queries/models/useProjectModelsQuery';
|
import useProjectModelsQuery from '@/queries/models/useProjectModelsQuery';
|
||||||
import { ModelTrainRequest, ModelResponse } from '@/types';
|
import { ModelTrainRequest, ModelResponse } from '@/types';
|
||||||
import { useState } from 'react';
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
interface TrainingSettingsProps {
|
interface TrainingSettingsProps {
|
||||||
projectId: number | null;
|
projectId: number | null;
|
||||||
@ -30,6 +31,9 @@ export default function TrainingSettings({
|
|||||||
const [optimizer, setOptimizer] = useState<'SGD' | 'AUTO' | 'ADAM' | 'ADAMW' | 'NADAM' | 'RADAM' | 'RMSPROP'>('AUTO');
|
const [optimizer, setOptimizer] = useState<'SGD' | 'AUTO' | 'ADAM' | 'ADAMW' | 'NADAM' | 'RADAM' | 'RMSPROP'>('AUTO');
|
||||||
const [lr0, setLr0] = useState<number>(0.01);
|
const [lr0, setLr0] = useState<number>(0.01);
|
||||||
const [lrf, setLrf] = useState<number>(0.001);
|
const [lrf, setLrf] = useState<number>(0.001);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (selectedModel?.isTrain) {
|
if (selectedModel?.isTrain) {
|
||||||
@ -44,10 +48,34 @@ export default function TrainingSettings({
|
|||||||
lr0,
|
lr0,
|
||||||
lrf,
|
lrf,
|
||||||
};
|
};
|
||||||
|
setIsSubmitting(true);
|
||||||
handleTrainingStart(trainData);
|
handleTrainingStart(trainData);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSubmitting) {
|
||||||
|
intervalRef.current = setInterval(() => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['projectModels', projectId] });
|
||||||
|
}, 1000);
|
||||||
|
} else if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isSubmitting, queryClient, projectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedModel?.isTrain) {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
}, [selectedModel]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<fieldset className={cn('grid gap-6 rounded-lg border p-4', className)}>
|
<fieldset className={cn('grid gap-6 rounded-lg border p-4', className)}>
|
||||||
<legend className="-ml-1 px-1 text-sm font-medium">모델 설정</legend>
|
<legend className="-ml-1 px-1 text-sm font-medium">모델 설정</legend>
|
||||||
@ -130,9 +158,9 @@ export default function TrainingSettings({
|
|||||||
variant="outlinePrimary"
|
variant="outlinePrimary"
|
||||||
size="lg"
|
size="lg"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={!selectedModel}
|
disabled={!selectedModel || isSubmitting}
|
||||||
>
|
>
|
||||||
{selectedModel?.isTrain ? '학습 중단' : '학습 시작'}
|
{isSubmitting ? '기다리는 중...' : '학습 시작'}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
43
frontend/src/components/ui/spinner.tsx
Normal file
43
frontend/src/components/ui/spinner.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { VariantProps, cva } from 'class-variance-authority';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
|
const spinnerVariants = cva('flex-col items-center justify-center', {
|
||||||
|
variants: {
|
||||||
|
show: {
|
||||||
|
true: 'flex',
|
||||||
|
false: 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const loaderVariants = cva('animate-spin text-primary', {
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
small: 'size-6',
|
||||||
|
medium: 'size-8',
|
||||||
|
large: 'size-12',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: 'medium',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface SpinnerContentProps extends VariantProps<typeof spinnerVariants>, VariantProps<typeof loaderVariants> {
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Spinner({ size, show, children, className }: SpinnerContentProps) {
|
||||||
|
return (
|
||||||
|
<span className={spinnerVariants({ show })}>
|
||||||
|
<Loader2 className={cn(loaderVariants({ size }), className)} />
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
@ -21,23 +21,28 @@ export default function ReviewDetail(): JSX.Element {
|
|||||||
|
|
||||||
const { data: reviewDetail } = useReviewDetailQuery(Number(projectId), Number(reviewId), memberId);
|
const { data: reviewDetail } = useReviewDetailQuery(Number(projectId), Number(reviewId), memberId);
|
||||||
|
|
||||||
const approveReviewMutation = useApproveReviewQuery({ projectId: Number(projectId), reviewId: Number(reviewId) });
|
const approveReviewMutation = useApproveReviewQuery({
|
||||||
const rejectReviewMutation = useRejectReviewQuery({ projectId: Number(projectId), reviewId: Number(reviewId) });
|
projectId: Number(projectId),
|
||||||
|
reviewId: Number(reviewId),
|
||||||
|
memberId: memberId,
|
||||||
|
});
|
||||||
|
const rejectReviewMutation = useRejectReviewQuery({
|
||||||
|
projectId: Number(projectId),
|
||||||
|
reviewId: Number(reviewId),
|
||||||
|
memberId: memberId,
|
||||||
|
});
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<'content' | 'images'>('content');
|
const [activeTab, setActiveTab] = useState<'content' | 'images'>('content');
|
||||||
const [isReviewed, setIsReviewed] = useState(
|
|
||||||
reviewDetail?.reviewStatus === 'APPROVED' || reviewDetail?.reviewStatus === 'REJECTED'
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleApprove = () => {
|
const handleApprove = () => {
|
||||||
approveReviewMutation.mutate(undefined, {
|
approveReviewMutation.mutate(undefined, {
|
||||||
onSuccess: () => setIsReviewed(true),
|
onSuccess: () => {},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReject = () => {
|
const handleReject = () => {
|
||||||
rejectReviewMutation.mutate(undefined, {
|
rejectReviewMutation.mutate(undefined, {
|
||||||
onSuccess: () => setIsReviewed(true),
|
onSuccess: () => {},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -124,24 +129,20 @@ export default function ReviewDetail(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isReviewed && (
|
{reviewDetail.reviewStatus !== 'APPROVED' && reviewDetail.reviewStatus !== 'REJECTED' && (
|
||||||
<div className="actions mt-6 flex justify-end space-x-2">
|
<div className="actions mt-6 flex justify-end space-x-2">
|
||||||
{reviewDetail.reviewStatus !== 'APPROVED' && (
|
<Button
|
||||||
<Button
|
variant="destructive"
|
||||||
variant="default"
|
onClick={handleReject}
|
||||||
onClick={handleApprove}
|
>
|
||||||
>
|
{'거부'}
|
||||||
{'승인'}
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
)}
|
variant="default"
|
||||||
{reviewDetail.reviewStatus !== 'REJECTED' && (
|
onClick={handleApprove}
|
||||||
<Button
|
>
|
||||||
variant="destructive"
|
{'승인'}
|
||||||
onClick={handleReject}
|
</Button>
|
||||||
>
|
|
||||||
{'거부'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { getCommentList } from '@/api/commentAPi';
|
import { getCommentList } from '@/api/commentAPi';
|
||||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
export default function useGetCommentListQuery(projectId: number, imageId: number) {
|
export default function useCommentListQuery(projectId: number, imageId: number) {
|
||||||
return useSuspenseQuery({
|
return useSuspenseQuery({
|
||||||
queryKey: ['commentList', projectId, imageId],
|
queryKey: ['commentList', projectId, imageId],
|
||||||
queryFn: () => getCommentList(projectId, imageId),
|
queryFn: () => getCommentList(projectId, imageId),
|
@ -1,7 +1,7 @@
|
|||||||
import { getComment } from '@/api/commentAPi';
|
import { getComment } from '@/api/commentAPi';
|
||||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
export default function useGetCommentQuery(projectId: number, commentId: number) {
|
export default function useCommentQuery(projectId: number, commentId: number) {
|
||||||
return useSuspenseQuery({
|
return useSuspenseQuery({
|
||||||
queryKey: ['comment', projectId, commentId],
|
queryKey: ['comment', projectId, commentId],
|
||||||
queryFn: () => getComment(projectId, commentId),
|
queryFn: () => getComment(projectId, commentId),
|
@ -2,7 +2,7 @@ import { createComment } from '@/api/commentAPi';
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { CommentResponse } from '@/types';
|
import { CommentResponse } from '@/types';
|
||||||
|
|
||||||
export default function useCreateCommentMutation(projectId: number, imageId: number) {
|
export default function useCreateCommentQuery(projectId: number, imageId: number) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
@ -1,11 +1,11 @@
|
|||||||
import { deleteComment } from '@/api/commentAPi';
|
import { deleteComment } from '@/api/commentAPi';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
export default function useDeleteCommentMutation(projectId: number, commentId: number) {
|
export default function useDeleteCommentQuery(projectId: number) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: () => deleteComment(projectId, commentId),
|
mutationFn: (commentId: number) => deleteComment(projectId, commentId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['commentList', projectId] });
|
queryClient.invalidateQueries({ queryKey: ['commentList', projectId] });
|
||||||
},
|
},
|
@ -1,15 +0,0 @@
|
|||||||
import { updateComment } from '@/api/commentAPi';
|
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { CommentResponse } from '@/types';
|
|
||||||
|
|
||||||
export default function useUpdateCommentMutation(projectId: number, commentId: number) {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: (commentData: CommentResponse) => updateComment(projectId, commentId, commentData),
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['comment', projectId, commentId] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['commentList', projectId] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
15
frontend/src/queries/comments/useUpdateCommentQuery.ts
Normal file
15
frontend/src/queries/comments/useUpdateCommentQuery.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { updateComment } from '@/api/commentAPi';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { CommentRequest } from '@/types';
|
||||||
|
|
||||||
|
export default function useUpdateCommentQuery(projectId: number) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ commentId, commentData }: { commentId: number; commentData: CommentRequest }) =>
|
||||||
|
updateComment(projectId, commentId, commentData),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['commentList', projectId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
@ -4,15 +4,16 @@ import { approveReview } from '@/api/reviewApi';
|
|||||||
interface ReviewStatusChangeProps {
|
interface ReviewStatusChangeProps {
|
||||||
projectId: number;
|
projectId: number;
|
||||||
reviewId: number;
|
reviewId: number;
|
||||||
|
memberId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useApproveReviewQuery({ projectId, reviewId }: ReviewStatusChangeProps) {
|
export default function useApproveReviewQuery({ projectId, reviewId, memberId }: ReviewStatusChangeProps) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: () => approveReview(projectId, reviewId),
|
mutationFn: () => approveReview(projectId, reviewId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['reviewDetail', reviewId] });
|
queryClient.invalidateQueries({ queryKey: ['reviewDetail', projectId, reviewId, memberId] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -4,15 +4,16 @@ import { rejectReview } from '@/api/reviewApi';
|
|||||||
interface ReviewStatusChangeProps {
|
interface ReviewStatusChangeProps {
|
||||||
projectId: number;
|
projectId: number;
|
||||||
reviewId: number;
|
reviewId: number;
|
||||||
|
memberId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useRejectReviewQuery({ projectId, reviewId }: ReviewStatusChangeProps) {
|
export default function useRejectReviewQuery({ projectId, reviewId, memberId }: ReviewStatusChangeProps) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: () => rejectReview(projectId, reviewId),
|
mutationFn: () => rejectReview(projectId, reviewId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['reviewDetail', reviewId] });
|
queryClient.invalidateQueries({ queryKey: ['reviewDetail', projectId, reviewId, memberId] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ interface CanvasState {
|
|||||||
sidebarSize: number;
|
sidebarSize: number;
|
||||||
image: ImageResponse | null;
|
image: ImageResponse | null;
|
||||||
labels: Label[];
|
labels: Label[];
|
||||||
drawState: 'pen' | 'rect' | 'pointer';
|
drawState: 'pen' | 'rect' | 'pointer' | 'comment';
|
||||||
selectedLabelId: number | null;
|
selectedLabelId: number | null;
|
||||||
setSidebarSize: (width: number) => void;
|
setSidebarSize: (width: number) => void;
|
||||||
setImage: (image: ImageResponse | null) => void;
|
setImage: (image: ImageResponse | null) => void;
|
||||||
@ -13,7 +13,7 @@ interface CanvasState {
|
|||||||
addLabel: (label: Label) => void;
|
addLabel: (label: Label) => void;
|
||||||
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' | 'comment') => void;
|
||||||
setSelectedLabelId: (labelId: number | null) => void;
|
setSelectedLabelId: (labelId: number | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,34 +4,33 @@ import { CommentResponse } from '@/types';
|
|||||||
interface CommentWithToggle extends CommentResponse {
|
interface CommentWithToggle extends CommentResponse {
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CommentState {
|
interface CommentState {
|
||||||
comments: CommentWithToggle[];
|
comments: CommentWithToggle[];
|
||||||
addComment: (comment: CommentResponse) => void;
|
setComments: (comments: CommentWithToggle[]) => void;
|
||||||
updateComment: (updatedComment: CommentResponse) => void;
|
addComment: (comment: CommentWithToggle) => void;
|
||||||
|
updateComment: (updatedComment: CommentWithToggle) => void;
|
||||||
deleteComment: (commentId: number) => void;
|
deleteComment: (commentId: number) => void;
|
||||||
toggleComment: (commentId: number) => void;
|
toggleComment: (commentId: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useCommentStore = create<CommentState>((set) => ({
|
const useCommentStore = create<CommentState>((set) => ({
|
||||||
comments: [],
|
comments: [],
|
||||||
|
setComments: (comments) => set({ comments }),
|
||||||
addComment: (comment) =>
|
addComment: (comment) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
comments: [...state.comments, { ...comment, isOpen: false }],
|
comments: [...state.comments, { ...comment, isOpen: true }],
|
||||||
})),
|
})),
|
||||||
|
|
||||||
updateComment: (updatedComment) =>
|
updateComment: (updatedComment) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
comments: state.comments.map((comment) =>
|
comments: state.comments.map((comment) =>
|
||||||
comment.id === updatedComment.id ? { ...updatedComment, isOpen: comment.isOpen } : comment
|
comment.id === updatedComment.id ? { ...updatedComment, isOpen: comment.isOpen } : comment
|
||||||
),
|
),
|
||||||
})),
|
})),
|
||||||
|
|
||||||
deleteComment: (commentId) =>
|
deleteComment: (commentId) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
comments: state.comments.filter((comment) => comment.id !== commentId),
|
comments: state.comments.filter((comment) => comment.id !== commentId),
|
||||||
})),
|
})),
|
||||||
|
|
||||||
toggleComment: (commentId) =>
|
toggleComment: (commentId) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
comments: state.comments.map((comment) =>
|
comments: state.comments.map((comment) =>
|
||||||
|
6
frontend/src/types/alarmTypes.ts
Normal file
6
frontend/src/types/alarmTypes.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface AlarmResponse {
|
||||||
|
id: number;
|
||||||
|
isRead: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
type: string;
|
||||||
|
}
|
9
frontend/src/types/categoryTypes.ts
Normal file
9
frontend/src/types/categoryTypes.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// 카테고리 관련 DTO
|
||||||
|
export interface CategoryRequest {
|
||||||
|
categoryName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CategoryResponse {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
24
frontend/src/types/commentTypes.ts
Normal file
24
frontend/src/types/commentTypes.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { MemberResponse } from './memberTypes';
|
||||||
|
|
||||||
|
// 댓글 관련 DTO
|
||||||
|
export interface CommentRequest {
|
||||||
|
content: string;
|
||||||
|
positionX: number;
|
||||||
|
positionY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommentResponse {
|
||||||
|
id: number;
|
||||||
|
memberId: number;
|
||||||
|
memberNickname: string;
|
||||||
|
memberProfileImage: string;
|
||||||
|
positionX: number;
|
||||||
|
positionY: number;
|
||||||
|
content: string;
|
||||||
|
createTime: string; // 작성 일자 (ISO 8601 형식)
|
||||||
|
author: MemberResponse; // 추가됨
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommentListResponse {
|
||||||
|
commentResponses: CommentResponse[];
|
||||||
|
}
|
27
frontend/src/types/fileTypes.ts
Normal file
27
frontend/src/types/fileTypes.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// 파일 및 디렉터리 관련 타입
|
||||||
|
export type FileItem = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
type: 'image' | 'json';
|
||||||
|
status: 'idle' | 'done';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DirectoryItem = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: 'directory';
|
||||||
|
children: Array<DirectoryItem | FileItem>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Project = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: 'classification' | 'detection' | 'segmentation';
|
||||||
|
children: Array<DirectoryItem | FileItem>;
|
||||||
|
};
|
||||||
|
export type Workspace = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
projects: Array<Project>;
|
||||||
|
};
|
23
frontend/src/types/folderTypes.ts
Normal file
23
frontend/src/types/folderTypes.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ImageResponse } from './imageTypes';
|
||||||
|
|
||||||
|
export interface FolderRequest {
|
||||||
|
title: string;
|
||||||
|
parentId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChildFolder {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FolderResponse {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
images: ImageResponse[];
|
||||||
|
children: ChildFolder[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FolderIdResponse {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
}
|
33
frontend/src/types/imageTypes.ts
Normal file
33
frontend/src/types/imageTypes.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
export type ImageStatus = 'PENDING' | 'IN_PROGRESS' | 'SAVE' | 'REVIEW_REQUEST' | 'REVIEW_REJECTED' | 'COMPLETED';
|
||||||
|
|
||||||
|
export interface ImageResponse {
|
||||||
|
id: number;
|
||||||
|
imageTitle: string;
|
||||||
|
imagePath: string;
|
||||||
|
dataPath: string;
|
||||||
|
status: ImageStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미지 이동 및 상태변경 요청 DTO
|
||||||
|
export interface ImageMoveRequest {
|
||||||
|
moveFolderId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImageStatusChangeRequest {
|
||||||
|
labelStatus: ImageStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미지 상세 조회 응답 DTO
|
||||||
|
export interface ImageDetailResponse {
|
||||||
|
id: number;
|
||||||
|
imageTitle: string;
|
||||||
|
imageUrl: string;
|
||||||
|
data: string | null;
|
||||||
|
status: ImageStatus;
|
||||||
|
}
|
||||||
|
export interface ImageFolderRequest {
|
||||||
|
memberId: number;
|
||||||
|
projectId: number;
|
||||||
|
parentId: number;
|
||||||
|
files: File[];
|
||||||
|
}
|
@ -1,355 +1,13 @@
|
|||||||
// 파일 및 디렉터리 관련 타입
|
export * from './modelTypes';
|
||||||
export type FileItem = {
|
export * from './imageTypes';
|
||||||
id: number;
|
export * from './reviewTypes';
|
||||||
name: string;
|
export * from './alarmTypes';
|
||||||
url: string;
|
export * from './memberTypes';
|
||||||
type: 'image' | 'json';
|
export * from './categoryTypes';
|
||||||
status: 'idle' | 'done';
|
export * from './workspaceTypes';
|
||||||
};
|
export * from './commentTypes';
|
||||||
|
export * from './folderTypes';
|
||||||
export type DirectoryItem = {
|
export * from './projectTypes';
|
||||||
id: number;
|
export * from './labelTypes';
|
||||||
name: string;
|
export * from './fileTypes';
|
||||||
type: 'directory';
|
export * from './reportTypes';
|
||||||
children: Array<DirectoryItem | FileItem>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 프로젝트 관련 타입
|
|
||||||
export type Project = {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
type: 'classification' | 'detection' | 'segmentation';
|
|
||||||
children: Array<DirectoryItem | FileItem>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 워크스페이스 관련 타입
|
|
||||||
export type Workspace = {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
projects: Array<Project>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 레이블 관련 타입
|
|
||||||
export type Label = {
|
|
||||||
id: number;
|
|
||||||
categoryId: number;
|
|
||||||
color: string;
|
|
||||||
type: 'polygon' | 'rectangle' | 'point';
|
|
||||||
coordinates: Array<[number, number]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface LabelingRequest {
|
|
||||||
memberId: number;
|
|
||||||
projectId: number;
|
|
||||||
imageId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AutoLabelingResponse {
|
|
||||||
imageId: number;
|
|
||||||
imageUrl: string;
|
|
||||||
data: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 폴더 및 이미지 관련 DTO
|
|
||||||
export interface FolderRequest {
|
|
||||||
title: string;
|
|
||||||
parentId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChildFolder {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FolderResponse {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
images: ImageResponse[];
|
|
||||||
children: ChildFolder[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ImageStatus = 'PENDING' | 'IN_PROGRESS' | 'SAVE' | 'REVIEW_REQUEST' | 'REVIEW_REJECTED' | 'COMPLETED';
|
|
||||||
|
|
||||||
export interface ImageResponse {
|
|
||||||
id: number;
|
|
||||||
imageTitle: string;
|
|
||||||
imagePath: string;
|
|
||||||
dataPath: string;
|
|
||||||
status: ImageStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 이미지 이동 및 상태변경 요청 DTO
|
|
||||||
export interface ImageMoveRequest {
|
|
||||||
moveFolderId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ImageStatusChangeRequest {
|
|
||||||
labelStatus: ImageStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 멤버 관련 DTO
|
|
||||||
export interface MemberResponse {
|
|
||||||
id: number;
|
|
||||||
nickname: string;
|
|
||||||
profileImage: string;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 워크스페이스 관련 DTO
|
|
||||||
export interface WorkspaceMemberResponse {
|
|
||||||
id: number;
|
|
||||||
nickname: string;
|
|
||||||
profileImage: string;
|
|
||||||
}
|
|
||||||
export interface WorkspaceRequest {
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceResponse {
|
|
||||||
id: number;
|
|
||||||
memberId: string;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceListResponse {
|
|
||||||
workspaceResponses: WorkspaceResponse[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProjectRequest {
|
|
||||||
title: string;
|
|
||||||
projectType: 'classification' | 'detection' | 'segmentation';
|
|
||||||
categories: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ProjectResponse = {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
workspaceId: number;
|
|
||||||
projectType: 'classification' | 'detection' | 'segmentation';
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
thumbnail?: string; // Optional
|
|
||||||
};
|
|
||||||
|
|
||||||
// 댓글 관련 DTO
|
|
||||||
export interface CommentRequest {
|
|
||||||
content: string;
|
|
||||||
positionX: number;
|
|
||||||
positionY: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CommentResponse {
|
|
||||||
id: number;
|
|
||||||
memberId: number;
|
|
||||||
memberNickname: string;
|
|
||||||
memberProfileImage: string;
|
|
||||||
positionX: number;
|
|
||||||
positionY: number;
|
|
||||||
content: string;
|
|
||||||
createTime: string; // 작성 일자 (ISO 8601 형식)
|
|
||||||
author: MemberResponse; // 추가됨
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CommentListResponse {
|
|
||||||
commentResponses: CommentResponse[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 프로젝트 멤버 관련 DTO
|
|
||||||
export interface ProjectMemberRequest {
|
|
||||||
memberId: number;
|
|
||||||
privilegeType: 'ADMIN' | 'MANAGER' | 'EDITOR' | 'VIEWER';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProjectMemberResponse {
|
|
||||||
memberId: number;
|
|
||||||
nickname: string;
|
|
||||||
profileImage: string;
|
|
||||||
privilegeType: 'ADMIN' | 'MANAGER' | 'EDITOR' | 'VIEWER';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 리뷰 관련 DTO
|
|
||||||
export interface ReviewRequest {
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
imageIds: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReviewResponse {
|
|
||||||
reviewId: number;
|
|
||||||
projectId: number;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
status: 'REQUESTED' | 'APPROVED' | 'REJECTED';
|
|
||||||
author: MemberResponse;
|
|
||||||
createAt: string;
|
|
||||||
updateAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReviewStatusRequest {
|
|
||||||
reviewStatus: 'REQUESTED' | 'APPROVED' | 'REJECTED';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReviewImageResponse {
|
|
||||||
id: number;
|
|
||||||
imageTitle: string;
|
|
||||||
status: ImageStatus;
|
|
||||||
imagePath: string;
|
|
||||||
dataPath: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReviewDetailResponse {
|
|
||||||
reviewId: number;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
reviewStatus: 'REQUESTED' | 'APPROVED' | 'REJECTED';
|
|
||||||
images: ReviewImageResponse[];
|
|
||||||
createAt: string;
|
|
||||||
updateAt: string;
|
|
||||||
author: MemberResponse;
|
|
||||||
reviewer: MemberResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 카테고리 관련 DTO
|
|
||||||
export interface CategoryRequest {
|
|
||||||
categoryName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CategoryResponse {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 레이블 저장 요청 DTO
|
|
||||||
export interface LabelSaveRequest {
|
|
||||||
data: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 폴더 ID 응답 DTO
|
|
||||||
export interface FolderIdResponse {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 이미지 상세 조회 응답 DTO
|
|
||||||
export interface ImageDetailResponse {
|
|
||||||
id: number;
|
|
||||||
imageTitle: string;
|
|
||||||
imageUrl: string;
|
|
||||||
data: string | null;
|
|
||||||
status: ImageStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 리프레시 토큰 응답 DTO
|
|
||||||
export interface RefreshTokenResponse {
|
|
||||||
accessToken: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Shape {
|
|
||||||
categoryId: number;
|
|
||||||
color: string;
|
|
||||||
points: [number, number][];
|
|
||||||
group_id: number;
|
|
||||||
shape_type: 'polygon' | 'rectangle' | 'point';
|
|
||||||
flags: Record<string, never>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LabelJson {
|
|
||||||
version: string;
|
|
||||||
task_type: 'cls' | 'det' | 'seg';
|
|
||||||
shapes: Shape[];
|
|
||||||
split: string;
|
|
||||||
imageHeight: number;
|
|
||||||
imageWidth: number;
|
|
||||||
imageDepth: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ErrorResponse {
|
|
||||||
status: number;
|
|
||||||
code: number;
|
|
||||||
message: string;
|
|
||||||
isSuccess: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ImageFolderRequest {
|
|
||||||
memberId: number;
|
|
||||||
projectId: number;
|
|
||||||
parentId: number;
|
|
||||||
files: File[];
|
|
||||||
}
|
|
||||||
export interface LabelCategoryResponse {
|
|
||||||
id: number;
|
|
||||||
labelName: string;
|
|
||||||
}
|
|
||||||
// 카테고리 요청 DTO
|
|
||||||
export interface LabelCategoryRequest {
|
|
||||||
labelCategoryList: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 모델 카테고리 응답 DTO
|
|
||||||
export interface ModelCategoryResponse {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 모델 요청 DTO (API로 전달할 데이터 타입)
|
|
||||||
export interface ModelRequest {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 모델 응답 DTO (API로부터 받는 데이터 타입)
|
|
||||||
export interface ModelResponse {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
isDefault: boolean;
|
|
||||||
isTrain: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 프로젝트 모델 리스트 응답 DTO
|
|
||||||
export interface ProjectModelsResponse extends Array<ModelResponse> {}
|
|
||||||
// 모델 훈련 요청 DTO
|
|
||||||
export interface ModelTrainRequest {
|
|
||||||
modelId: number;
|
|
||||||
ratio: number;
|
|
||||||
epochs: number;
|
|
||||||
batch: number;
|
|
||||||
lr0: number;
|
|
||||||
lrf: number;
|
|
||||||
optimizer: 'AUTO' | 'SGD' | 'ADAM' | 'ADAMW' | 'NADAM' | 'RADAM' | 'RMSPROP';
|
|
||||||
}
|
|
||||||
export interface ResultResponse {
|
|
||||||
id: number;
|
|
||||||
precision: number;
|
|
||||||
recall: number;
|
|
||||||
fitness: number;
|
|
||||||
ratio: number;
|
|
||||||
epochs: number;
|
|
||||||
batch: number;
|
|
||||||
lr0: number;
|
|
||||||
lrf: number;
|
|
||||||
optimizer: 'AUTO' | 'SGD' | 'ADAM' | 'ADAMW' | 'NADAM' | 'RADAM' | 'RMSPROP';
|
|
||||||
map50: number;
|
|
||||||
map5095: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReportResponse {
|
|
||||||
modelId: number;
|
|
||||||
totalEpochs: number;
|
|
||||||
epoch: number;
|
|
||||||
boxLoss: number;
|
|
||||||
clsLoss: number;
|
|
||||||
dflLoss: number;
|
|
||||||
fitness: number;
|
|
||||||
epochTime: number;
|
|
||||||
leftSecond: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AlarmResponse {
|
|
||||||
id: number;
|
|
||||||
isRead: boolean;
|
|
||||||
createdAt: string;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
53
frontend/src/types/labelTypes.ts
Normal file
53
frontend/src/types/labelTypes.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// 레이블 관련 타입
|
||||||
|
export type Label = {
|
||||||
|
id: number;
|
||||||
|
categoryId: number;
|
||||||
|
color: string;
|
||||||
|
type: 'polygon' | 'rectangle' | 'point';
|
||||||
|
coordinates: Array<[number, number]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface LabelingRequest {
|
||||||
|
memberId: number;
|
||||||
|
projectId: number;
|
||||||
|
imageId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutoLabelingResponse {
|
||||||
|
imageId: number;
|
||||||
|
imageUrl: string;
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 레이블 저장 요청 DTO
|
||||||
|
export interface LabelSaveRequest {
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Shape {
|
||||||
|
categoryId: number;
|
||||||
|
color: string;
|
||||||
|
points: [number, number][];
|
||||||
|
group_id: number;
|
||||||
|
shape_type: 'polygon' | 'rectangle' | 'point';
|
||||||
|
flags: Record<string, never>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LabelJson {
|
||||||
|
version: string;
|
||||||
|
task_type: 'cls' | 'det' | 'seg';
|
||||||
|
shapes: Shape[];
|
||||||
|
split: string;
|
||||||
|
imageHeight: number;
|
||||||
|
imageWidth: number;
|
||||||
|
imageDepth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LabelCategoryResponse {
|
||||||
|
id: number;
|
||||||
|
labelName: string;
|
||||||
|
}
|
||||||
|
// 카테고리 요청 DTO
|
||||||
|
export interface LabelCategoryRequest {
|
||||||
|
labelCategoryList: number[];
|
||||||
|
}
|
11
frontend/src/types/memberTypes.ts
Normal file
11
frontend/src/types/memberTypes.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// 멤버 관련 DTO
|
||||||
|
export interface MemberResponse {
|
||||||
|
id: number;
|
||||||
|
nickname: string;
|
||||||
|
profileImage: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
// 리프레시 토큰 응답 DTO
|
||||||
|
export interface RefreshTokenResponse {
|
||||||
|
accessToken: string;
|
||||||
|
}
|
32
frontend/src/types/modelTypes.ts
Normal file
32
frontend/src/types/modelTypes.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// 모델 카테고리 응답 DTO
|
||||||
|
export interface ModelCategoryResponse {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 모델 요청 DTO (API로 전달할 데이터 타입)
|
||||||
|
export interface ModelRequest {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 모델 응답 DTO (API로부터 받는 데이터 타입)
|
||||||
|
export interface ModelResponse {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
isTrain: boolean;
|
||||||
|
projectType: 'classification' | 'detection' | 'segmentation';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 프로젝트 모델 리스트 응답 DTO
|
||||||
|
export interface ProjectModelsResponse extends Array<ModelResponse> {}
|
||||||
|
// 모델 훈련 요청 DTO
|
||||||
|
export interface ModelTrainRequest {
|
||||||
|
modelId: number;
|
||||||
|
ratio: number;
|
||||||
|
epochs: number;
|
||||||
|
batch: number;
|
||||||
|
lr0: number;
|
||||||
|
lrf: number;
|
||||||
|
optimizer: 'AUTO' | 'SGD' | 'ADAM' | 'ADAMW' | 'NADAM' | 'RADAM' | 'RMSPROP';
|
||||||
|
}
|
27
frontend/src/types/projectTypes.ts
Normal file
27
frontend/src/types/projectTypes.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
export interface ProjectRequest {
|
||||||
|
title: string;
|
||||||
|
projectType: 'classification' | 'detection' | 'segmentation';
|
||||||
|
categories: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProjectResponse = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
workspaceId: number;
|
||||||
|
projectType: 'classification' | 'detection' | 'segmentation';
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
thumbnail?: string; // Optional
|
||||||
|
};
|
||||||
|
// 프로젝트 멤버 관련 DTO
|
||||||
|
export interface ProjectMemberRequest {
|
||||||
|
memberId: number;
|
||||||
|
privilegeType: 'ADMIN' | 'MANAGER' | 'EDITOR' | 'VIEWER';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectMemberResponse {
|
||||||
|
memberId: number;
|
||||||
|
nickname: string;
|
||||||
|
profileImage: string;
|
||||||
|
privilegeType: 'ADMIN' | 'MANAGER' | 'EDITOR' | 'VIEWER';
|
||||||
|
}
|
27
frontend/src/types/reportTypes.ts
Normal file
27
frontend/src/types/reportTypes.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
export interface ResultResponse {
|
||||||
|
id: number;
|
||||||
|
precision: number;
|
||||||
|
recall: number;
|
||||||
|
fitness: number;
|
||||||
|
ratio: number;
|
||||||
|
epochs: number;
|
||||||
|
batch: number;
|
||||||
|
lr0: number;
|
||||||
|
lrf: number;
|
||||||
|
optimizer: 'AUTO' | 'SGD' | 'ADAM' | 'ADAMW' | 'NADAM' | 'RADAM' | 'RMSPROP';
|
||||||
|
map50: number;
|
||||||
|
map5095: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportResponse {
|
||||||
|
modelId: number;
|
||||||
|
totalEpochs: number;
|
||||||
|
epoch: number;
|
||||||
|
boxLoss: number;
|
||||||
|
clsLoss: number;
|
||||||
|
dflLoss: number;
|
||||||
|
fitness: number;
|
||||||
|
epochTime: number;
|
||||||
|
leftSecond: number;
|
||||||
|
segLoss: number;
|
||||||
|
}
|
44
frontend/src/types/reviewTypes.ts
Normal file
44
frontend/src/types/reviewTypes.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { ImageStatus } from './imageTypes';
|
||||||
|
import { MemberResponse } from './memberTypes';
|
||||||
|
|
||||||
|
// 리뷰 관련 DTO
|
||||||
|
export interface ReviewRequest {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
imageIds: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReviewResponse {
|
||||||
|
reviewId: number;
|
||||||
|
projectId: number;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
status: 'REQUESTED' | 'APPROVED' | 'REJECTED';
|
||||||
|
author: MemberResponse;
|
||||||
|
createAt: string;
|
||||||
|
updateAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReviewStatusRequest {
|
||||||
|
reviewStatus: 'REQUESTED' | 'APPROVED' | 'REJECTED';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReviewImageResponse {
|
||||||
|
id: number;
|
||||||
|
imageTitle: string;
|
||||||
|
status: ImageStatus;
|
||||||
|
imagePath: string;
|
||||||
|
dataPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReviewDetailResponse {
|
||||||
|
reviewId: number;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
reviewStatus: 'REQUESTED' | 'APPROVED' | 'REJECTED';
|
||||||
|
images: ReviewImageResponse[];
|
||||||
|
createAt: string;
|
||||||
|
updateAt: string;
|
||||||
|
author: MemberResponse;
|
||||||
|
reviewer: MemberResponse;
|
||||||
|
}
|
23
frontend/src/types/workspaceTypes.ts
Normal file
23
frontend/src/types/workspaceTypes.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// 워크스페이스 관련 DTO
|
||||||
|
export interface WorkspaceMemberResponse {
|
||||||
|
id: number;
|
||||||
|
nickname: string;
|
||||||
|
profileImage: string;
|
||||||
|
}
|
||||||
|
export interface WorkspaceRequest {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkspaceResponse {
|
||||||
|
id: number;
|
||||||
|
memberId: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkspaceListResponse {
|
||||||
|
workspaceResponses: WorkspaceResponse[];
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user