Merge branch 'fe/develop' into fe/refactor/fcm

This commit is contained in:
홍창기 2024-09-30 01:00:08 +09:00
commit 876b8108d1
No known key found for this signature in database
GPG Key ID: 9FF142041B4A91B9
37 changed files with 761 additions and 461 deletions

View 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

View 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

View 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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
export interface AlarmResponse {
id: number;
isRead: boolean;
createdAt: string;
type: string;
}

View File

@ -0,0 +1,9 @@
// 카테고리 관련 DTO
export interface CategoryRequest {
categoryName: string;
}
export interface CategoryResponse {
id: number;
name: string;
}

View 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[];
}

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

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

View 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[];
}

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

View 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[];
}

View File

@ -0,0 +1,11 @@
// 멤버 관련 DTO
export interface MemberResponse {
id: number;
nickname: string;
profileImage: string;
email: string;
}
// 리프레시 토큰 응답 DTO
export interface RefreshTokenResponse {
accessToken: string;
}

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

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

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

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

View 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[];
}