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 { LucideIcon, MousePointer2, PenTool, Plus, Save, Square } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { MessageSquare } from 'lucide-react';
|
||||
|
||||
export default function CanvasControlBar({
|
||||
saveJson,
|
||||
@ -10,13 +11,14 @@ export default function CanvasControlBar({
|
||||
projectType: 'classification' | 'detection' | 'segmentation';
|
||||
}) {
|
||||
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 activeButtonClassName = 'bg-primary stroke-white';
|
||||
|
||||
const controls: { [key: string]: LucideIcon } = {
|
||||
pointer: MousePointer2,
|
||||
...(projectType === 'segmentation' ? { pen: PenTool } : projectType === 'detection' ? { rect: Square } : null),
|
||||
comment: MessageSquare,
|
||||
};
|
||||
|
||||
return (
|
||||
@ -36,6 +38,7 @@ export default function CanvasControlBar({
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
{projectType === 'classification' && labels.length === 0 && (
|
||||
<button
|
||||
className={cn(buttonClassName, buttonBaseClassName)}
|
||||
@ -57,7 +60,9 @@ export default function CanvasControlBar({
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="h-5 w-0.5 rounded bg-gray-400" />
|
||||
|
||||
<button
|
||||
className={cn(buttonClassName, buttonBaseClassName)}
|
||||
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 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 { useEffect, useRef, useState } from 'react';
|
||||
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 useSaveImageLabelsQuery from '@/queries/projects/useSaveImageLabelsQuery';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import CommentLabel from './CommentLabel';
|
||||
import useAuthStore from '@/stores/useAuthStore';
|
||||
|
||||
export default function ImageCanvas() {
|
||||
const { project, folderId, categories } = useProjectStore();
|
||||
@ -33,6 +40,13 @@ export default function ImageCanvas() {
|
||||
const queryClient = useQueryClient();
|
||||
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(() => {
|
||||
setLabels(
|
||||
shapes.map<Label>(({ group_id, color, points, shape_type }, index) => ({
|
||||
@ -49,11 +63,23 @@ export default function ImageCanvas() {
|
||||
setSelectedLabelId(null);
|
||||
}, [image, setSelectedLabelId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (commentList) {
|
||||
setComments(
|
||||
commentList.map((comment) => ({
|
||||
...comment,
|
||||
isOpen: false,
|
||||
}))
|
||||
);
|
||||
}
|
||||
}, [commentList, setComments]);
|
||||
|
||||
const setLabel = (index: number) => (coordinates: [number, number][]) => {
|
||||
const newLabels = [...labels];
|
||||
newLabels[index].coordinates = coordinates;
|
||||
setLabels(newLabels);
|
||||
};
|
||||
|
||||
const saveJson = () => {
|
||||
const json = JSON.stringify({
|
||||
...labelData,
|
||||
@ -86,6 +112,7 @@ export default function ImageCanvas() {
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const startDrawRect = () => {
|
||||
const { x, y } = stageRef.current!.getRelativePointerPosition()!;
|
||||
setRectPoints([
|
||||
@ -93,6 +120,7 @@ export default function ImageCanvas() {
|
||||
[x, y],
|
||||
]);
|
||||
};
|
||||
|
||||
const addPointToPolygon = () => {
|
||||
const { x, y } = stageRef.current!.getRelativePointerPosition()!;
|
||||
if (polygonPoints.length === 0) {
|
||||
@ -116,20 +144,24 @@ export default function ImageCanvas() {
|
||||
}
|
||||
setPolygonPoints([...polygonPoints, [x, y]]);
|
||||
};
|
||||
|
||||
const removeLastPointOfPolygon = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
if (polygonPoints.length === 0) return;
|
||||
setPolygonPoints(polygonPoints.slice(0, -1));
|
||||
};
|
||||
|
||||
const moveLastPointOfPolygon = () => {
|
||||
if (polygonPoints.length < 2) return;
|
||||
const { x, y } = stageRef.current!.getRelativePointerPosition()!;
|
||||
setPolygonPoints([...polygonPoints.slice(0, -1), [x, y]]);
|
||||
};
|
||||
|
||||
const endDrawPolygon = () => {
|
||||
if (drawState !== 'pen' || polygonPoints.length === 0) return;
|
||||
setDrawState('pointer');
|
||||
setPolygonPoints([]);
|
||||
|
||||
if (polygonPoints.length < 4) return;
|
||||
|
||||
const color = Math.floor(Math.random() * 0xffffff)
|
||||
@ -146,12 +178,14 @@ export default function ImageCanvas() {
|
||||
setDrawState('pointer');
|
||||
setSelectedLabelId(id);
|
||||
};
|
||||
|
||||
const updateDrawingRect = () => {
|
||||
if (rectPoints.length === 0) return;
|
||||
|
||||
const { x, y } = stageRef.current!.getRelativePointerPosition()!;
|
||||
setRectPoints([rectPoints[0], [x, y]]);
|
||||
};
|
||||
|
||||
const endDrawRect = () => {
|
||||
if (drawState !== 'rect' || rectPoints.length === 0) return;
|
||||
if (rectPoints[0][0] === rectPoints[1][0] && rectPoints[0][1] === rectPoints[1][1]) {
|
||||
@ -159,6 +193,7 @@ export default function ImageCanvas() {
|
||||
return;
|
||||
}
|
||||
setRectPoints([]);
|
||||
|
||||
const color = Math.floor(Math.random() * 0xffffff)
|
||||
.toString(16)
|
||||
.padStart(6, '0');
|
||||
@ -173,12 +208,47 @@ export default function ImageCanvas() {
|
||||
setDrawState('pointer');
|
||||
setSelectedLabelId(id);
|
||||
};
|
||||
|
||||
const handleClick = (e: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => {
|
||||
e.evt.preventDefault();
|
||||
e.evt.stopPropagation();
|
||||
const isLeftClicked = e.evt.type === 'mousedown' && (e.evt as MouseEvent).button === 0;
|
||||
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)) {
|
||||
stageRef.current?.stopDrag();
|
||||
if (drawState === 'rect') {
|
||||
@ -193,6 +263,7 @@ export default function ImageCanvas() {
|
||||
setSelectedLabelId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseMove = () => {
|
||||
if (drawState === 'rect' && rectPoints.length) {
|
||||
updateDrawingRect();
|
||||
@ -201,6 +272,7 @@ export default function ImageCanvas() {
|
||||
moveLastPointOfPolygon();
|
||||
}
|
||||
};
|
||||
|
||||
const handleZoom = (e: Konva.KonvaEventObject<WheelEvent>) => {
|
||||
const scaleBy = 1.05;
|
||||
const oldScale = scale.current;
|
||||
@ -219,6 +291,7 @@ export default function ImageCanvas() {
|
||||
stageRef.current?.position(newPos);
|
||||
stageRef.current?.batchDraw();
|
||||
};
|
||||
|
||||
const handleScroll = (e: Konva.KonvaEventObject<WheelEvent>) => {
|
||||
const delta = -e.evt.deltaY;
|
||||
const x = stageRef.current?.x();
|
||||
@ -235,6 +308,7 @@ export default function ImageCanvas() {
|
||||
|
||||
e.evt.ctrlKey ? handleZoom(e) : handleScroll(e);
|
||||
};
|
||||
|
||||
const getScale = (): Vector2d => {
|
||||
if (scale.current) return { x: scale.current, y: scale.current };
|
||||
const widthRatio = stageWidth / image!.width;
|
||||
@ -271,7 +345,7 @@ export default function ImageCanvas() {
|
||||
width={stageWidth}
|
||||
height={stageHeight}
|
||||
className="overflow-hidden bg-gray-200"
|
||||
draggable
|
||||
draggable={drawState !== 'comment'}
|
||||
onWheel={handleWheel}
|
||||
onMouseDown={handleClick}
|
||||
onTouchStart={handleClick}
|
||||
@ -285,6 +359,7 @@ export default function ImageCanvas() {
|
||||
<Layer>
|
||||
<Image image={image} />
|
||||
</Layer>
|
||||
|
||||
{project?.type !== 'classification' && (
|
||||
<Layer listening={drawState === 'pointer'}>
|
||||
{labels.map((label) =>
|
||||
@ -357,6 +432,30 @@ export default function ImageCanvas() {
|
||||
</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} />
|
||||
</Stage>
|
||||
<CanvasControlBar
|
||||
|
@ -115,7 +115,7 @@ export default function ImageUploadFileForm({
|
||||
</div>
|
||||
)}
|
||||
{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) => (
|
||||
<li
|
||||
key={index}
|
||||
|
@ -100,7 +100,7 @@ export default function ImageUploadFolderForm({ onClose, projectId }: { onClose:
|
||||
</div>
|
||||
)}
|
||||
{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) => (
|
||||
<li
|
||||
key={index}
|
||||
|
@ -27,6 +27,10 @@ const chartConfig = {
|
||||
label: 'Fitness',
|
||||
color: '#FFD700',
|
||||
},
|
||||
segLoss: {
|
||||
label: 'Segmentation Loss',
|
||||
color: '#FF1493',
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
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) => ({
|
||||
epoch: (i + 1).toString(),
|
||||
boxLoss: null,
|
||||
classLoss: null,
|
||||
clsLoss: null,
|
||||
dflLoss: null,
|
||||
fitness: null,
|
||||
segLoss: null,
|
||||
}));
|
||||
|
||||
const filledData = emptyData.map((item, index) => ({
|
||||
@ -46,6 +51,18 @@ export default function ModelLineChart({ data, className }: ModelLineChartProps)
|
||||
...(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 (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
@ -79,34 +96,11 @@ export default function ModelLineChart({ data, className }: ModelLineChartProps)
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Line
|
||||
dataKey="boxLoss"
|
||||
type="monotone"
|
||||
stroke={chartConfig.boxLoss.color}
|
||||
strokeWidth={2}
|
||||
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}
|
||||
/>
|
||||
{renderLine('boxLoss', chartConfig.boxLoss.color)}
|
||||
{renderLine('clsLoss', chartConfig.classLoss.color)}
|
||||
{renderLine('dflLoss', chartConfig.dflLoss.color)}
|
||||
{renderLine('fitness', chartConfig.fitness.color)}
|
||||
{renderLine('segLoss', chartConfig.segLoss.color)}
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ModelLineChart from './ModelLineChart';
|
||||
import usePollingTrainingModelReport from '@/queries/reports/usePollingModelReportsQuery';
|
||||
import usePollingModelReportsQuery from '@/queries/reports/usePollingModelReportsQuery';
|
||||
import { ModelResponse } from '@/types';
|
||||
|
||||
interface TrainingGraphProps {
|
||||
@ -10,24 +10,24 @@ interface TrainingGraphProps {
|
||||
}
|
||||
|
||||
export default function TrainingGraph({ projectId, selectedModel, className }: TrainingGraphProps) {
|
||||
const isTraining = selectedModel?.isTrain || false;
|
||||
|
||||
const { data: fetchedTrainingDataList } = usePollingTrainingModelReport(
|
||||
const [isPolling, setIsPolling] = useState(false);
|
||||
const { data: trainingDataList } = usePollingModelReportsQuery(
|
||||
projectId as number,
|
||||
selectedModel?.id as number,
|
||||
isTraining
|
||||
isPolling
|
||||
);
|
||||
|
||||
const trainingDataList = useMemo(() => {
|
||||
if (!isTraining) {
|
||||
return [];
|
||||
useEffect(() => {
|
||||
if (selectedModel) {
|
||||
setIsPolling(true);
|
||||
} else {
|
||||
setIsPolling(false);
|
||||
}
|
||||
return fetchedTrainingDataList || [];
|
||||
}, [isTraining, fetchedTrainingDataList]);
|
||||
}, [selectedModel]);
|
||||
|
||||
return (
|
||||
<ModelLineChart
|
||||
data={trainingDataList}
|
||||
data={trainingDataList || []}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import SelectWithLabel from './SelectWithLabel';
|
||||
import InputWithLabel from './InputWithLabel';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import useProjectModelsQuery from '@/queries/models/useProjectModelsQuery';
|
||||
import { ModelTrainRequest, ModelResponse } from '@/types';
|
||||
import { useState } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
interface TrainingSettingsProps {
|
||||
projectId: number | null;
|
||||
@ -30,6 +31,9 @@ export default function TrainingSettings({
|
||||
const [optimizer, setOptimizer] = useState<'SGD' | 'AUTO' | 'ADAM' | 'ADAMW' | 'NADAM' | 'RADAM' | 'RMSPROP'>('AUTO');
|
||||
const [lr0, setLr0] = useState<number>(0.01);
|
||||
const [lrf, setLrf] = useState<number>(0.001);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (selectedModel?.isTrain) {
|
||||
@ -44,10 +48,34 @@ export default function TrainingSettings({
|
||||
lr0,
|
||||
lrf,
|
||||
};
|
||||
setIsSubmitting(true);
|
||||
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 (
|
||||
<fieldset className={cn('grid gap-6 rounded-lg border p-4', className)}>
|
||||
<legend className="-ml-1 px-1 text-sm font-medium">모델 설정</legend>
|
||||
@ -130,9 +158,9 @@ export default function TrainingSettings({
|
||||
variant="outlinePrimary"
|
||||
size="lg"
|
||||
onClick={handleSubmit}
|
||||
disabled={!selectedModel}
|
||||
disabled={!selectedModel || isSubmitting}
|
||||
>
|
||||
{selectedModel?.isTrain ? '학습 중단' : '학습 시작'}
|
||||
{isSubmitting ? '기다리는 중...' : '학습 시작'}
|
||||
</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 approveReviewMutation = useApproveReviewQuery({ projectId: Number(projectId), reviewId: Number(reviewId) });
|
||||
const rejectReviewMutation = useRejectReviewQuery({ projectId: Number(projectId), reviewId: Number(reviewId) });
|
||||
const approveReviewMutation = useApproveReviewQuery({
|
||||
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 [isReviewed, setIsReviewed] = useState(
|
||||
reviewDetail?.reviewStatus === 'APPROVED' || reviewDetail?.reviewStatus === 'REJECTED'
|
||||
);
|
||||
|
||||
const handleApprove = () => {
|
||||
approveReviewMutation.mutate(undefined, {
|
||||
onSuccess: () => setIsReviewed(true),
|
||||
onSuccess: () => {},
|
||||
});
|
||||
};
|
||||
|
||||
const handleReject = () => {
|
||||
rejectReviewMutation.mutate(undefined, {
|
||||
onSuccess: () => setIsReviewed(true),
|
||||
onSuccess: () => {},
|
||||
});
|
||||
};
|
||||
|
||||
@ -124,24 +129,20 @@ export default function ReviewDetail(): JSX.Element {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isReviewed && (
|
||||
{reviewDetail.reviewStatus !== 'APPROVED' && reviewDetail.reviewStatus !== 'REJECTED' && (
|
||||
<div className="actions mt-6 flex justify-end space-x-2">
|
||||
{reviewDetail.reviewStatus !== 'APPROVED' && (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={handleApprove}
|
||||
>
|
||||
{'승인'}
|
||||
</Button>
|
||||
)}
|
||||
{reviewDetail.reviewStatus !== 'REJECTED' && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleReject}
|
||||
>
|
||||
{'거부'}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleReject}
|
||||
>
|
||||
{'거부'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={handleApprove}
|
||||
>
|
||||
{'승인'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { getCommentList } from '@/api/commentAPi';
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
|
||||
export default function useGetCommentListQuery(projectId: number, imageId: number) {
|
||||
export default function useCommentListQuery(projectId: number, imageId: number) {
|
||||
return useSuspenseQuery({
|
||||
queryKey: ['commentList', projectId, imageId],
|
||||
queryFn: () => getCommentList(projectId, imageId),
|
@ -1,7 +1,7 @@
|
||||
import { getComment } from '@/api/commentAPi';
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
|
||||
export default function useGetCommentQuery(projectId: number, commentId: number) {
|
||||
export default function useCommentQuery(projectId: number, commentId: number) {
|
||||
return useSuspenseQuery({
|
||||
queryKey: ['comment', projectId, commentId],
|
||||
queryFn: () => getComment(projectId, commentId),
|
@ -2,7 +2,7 @@ import { createComment } from '@/api/commentAPi';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { CommentResponse } from '@/types';
|
||||
|
||||
export default function useCreateCommentMutation(projectId: number, imageId: number) {
|
||||
export default function useCreateCommentQuery(projectId: number, imageId: number) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
@ -1,11 +1,11 @@
|
||||
import { deleteComment } from '@/api/commentAPi';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
export default function useDeleteCommentMutation(projectId: number, commentId: number) {
|
||||
export default function useDeleteCommentQuery(projectId: number) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => deleteComment(projectId, commentId),
|
||||
mutationFn: (commentId: number) => deleteComment(projectId, commentId),
|
||||
onSuccess: () => {
|
||||
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 {
|
||||
projectId: number;
|
||||
reviewId: number;
|
||||
memberId: number;
|
||||
}
|
||||
|
||||
export default function useApproveReviewQuery({ projectId, reviewId }: ReviewStatusChangeProps) {
|
||||
export default function useApproveReviewQuery({ projectId, reviewId, memberId }: ReviewStatusChangeProps) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => approveReview(projectId, reviewId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['reviewDetail', reviewId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['reviewDetail', projectId, reviewId, memberId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -4,15 +4,16 @@ import { rejectReview } from '@/api/reviewApi';
|
||||
interface ReviewStatusChangeProps {
|
||||
projectId: number;
|
||||
reviewId: number;
|
||||
memberId: number;
|
||||
}
|
||||
|
||||
export default function useRejectReviewQuery({ projectId, reviewId }: ReviewStatusChangeProps) {
|
||||
export default function useRejectReviewQuery({ projectId, reviewId, memberId }: ReviewStatusChangeProps) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => rejectReview(projectId, reviewId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['reviewDetail', reviewId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['reviewDetail', projectId, reviewId, memberId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ interface CanvasState {
|
||||
sidebarSize: number;
|
||||
image: ImageResponse | null;
|
||||
labels: Label[];
|
||||
drawState: 'pen' | 'rect' | 'pointer';
|
||||
drawState: 'pen' | 'rect' | 'pointer' | 'comment';
|
||||
selectedLabelId: number | null;
|
||||
setSidebarSize: (width: number) => void;
|
||||
setImage: (image: ImageResponse | null) => void;
|
||||
@ -13,7 +13,7 @@ interface CanvasState {
|
||||
addLabel: (label: Label) => void;
|
||||
removeLabel: (labelId: number) => void;
|
||||
updateLabel: (label: Label) => void;
|
||||
setDrawState: (state: 'pen' | 'rect' | 'pointer') => void;
|
||||
setDrawState: (state: 'pen' | 'rect' | 'pointer' | 'comment') => void;
|
||||
setSelectedLabelId: (labelId: number | null) => void;
|
||||
}
|
||||
|
||||
|
@ -4,34 +4,33 @@ import { CommentResponse } from '@/types';
|
||||
interface CommentWithToggle extends CommentResponse {
|
||||
isOpen?: boolean;
|
||||
}
|
||||
|
||||
interface CommentState {
|
||||
comments: CommentWithToggle[];
|
||||
addComment: (comment: CommentResponse) => void;
|
||||
updateComment: (updatedComment: CommentResponse) => void;
|
||||
setComments: (comments: CommentWithToggle[]) => void;
|
||||
addComment: (comment: CommentWithToggle) => void;
|
||||
updateComment: (updatedComment: CommentWithToggle) => void;
|
||||
deleteComment: (commentId: number) => void;
|
||||
toggleComment: (commentId: number) => void;
|
||||
}
|
||||
|
||||
const useCommentStore = create<CommentState>((set) => ({
|
||||
comments: [],
|
||||
|
||||
setComments: (comments) => set({ comments }),
|
||||
addComment: (comment) =>
|
||||
set((state) => ({
|
||||
comments: [...state.comments, { ...comment, isOpen: false }],
|
||||
comments: [...state.comments, { ...comment, isOpen: true }],
|
||||
})),
|
||||
|
||||
updateComment: (updatedComment) =>
|
||||
set((state) => ({
|
||||
comments: state.comments.map((comment) =>
|
||||
comment.id === updatedComment.id ? { ...updatedComment, isOpen: comment.isOpen } : comment
|
||||
),
|
||||
})),
|
||||
|
||||
deleteComment: (commentId) =>
|
||||
set((state) => ({
|
||||
comments: state.comments.filter((comment) => comment.id !== commentId),
|
||||
})),
|
||||
|
||||
toggleComment: (commentId) =>
|
||||
set((state) => ({
|
||||
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 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>;
|
||||
};
|
||||
|
||||
// 레이블 관련 타입
|
||||
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;
|
||||
}
|
||||
export * from './modelTypes';
|
||||
export * from './imageTypes';
|
||||
export * from './reviewTypes';
|
||||
export * from './alarmTypes';
|
||||
export * from './memberTypes';
|
||||
export * from './categoryTypes';
|
||||
export * from './workspaceTypes';
|
||||
export * from './commentTypes';
|
||||
export * from './folderTypes';
|
||||
export * from './projectTypes';
|
||||
export * from './labelTypes';
|
||||
export * from './fileTypes';
|
||||
export * from './reportTypes';
|
||||
|
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