Merge branch 'fe/develop' into fe/feat/fcm-notification
This commit is contained in:
commit
628503dd27
@ -6,11 +6,11 @@ export async function reissueToken() {
|
||||
}
|
||||
|
||||
export async function getProfile() {
|
||||
return api
|
||||
.get<MemberResponse>('/auth/profile', {
|
||||
withCredentials: true,
|
||||
})
|
||||
.then(({ data }) => data);
|
||||
return api.get<MemberResponse>('/auth/profile').then(({ data }) => data);
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
return api.post('/auth/logout').then(({ data }) => data);
|
||||
}
|
||||
|
||||
export async function saveFcmToken(token: string) {
|
||||
|
@ -27,9 +27,8 @@ api.interceptors.response.use(
|
||||
return api
|
||||
.post<RefreshTokenResponse>(REFRESH_URL)
|
||||
.then(({ data }) => {
|
||||
console.log(data);
|
||||
const { accessToken } = data;
|
||||
useAuthStore.getState().setLoggedIn(true, accessToken);
|
||||
useAuthStore.getState().setToken(accessToken);
|
||||
if (error.config) {
|
||||
return api(error.config);
|
||||
}
|
||||
|
@ -47,11 +47,11 @@ export async function uploadImageFile(memberId: number, projectId: number, folde
|
||||
export async function uploadImageFolder(memberId: number, projectId: number, files: File[]) {
|
||||
const formData = new FormData();
|
||||
files.forEach((file) => {
|
||||
formData.append('folderZip', file);
|
||||
formData.append('imageList', file);
|
||||
});
|
||||
|
||||
return api
|
||||
.post(`/projects/${projectId}/folders/${0}/images/zip`, formData, {
|
||||
.post(`/projects/${projectId}/folders/${0}/images/file`, formData, {
|
||||
params: { memberId },
|
||||
})
|
||||
.then(({ data }) => data);
|
||||
|
@ -10,14 +10,6 @@ export async function saveImageLabels(
|
||||
return api.post(`/projects/${projectId}/images/${imageId}/label`, data).then(({ data }) => data);
|
||||
}
|
||||
|
||||
export async function runAutoLabel(projectId: number, memberId: number) {
|
||||
return api
|
||||
.post(
|
||||
`/projects/${projectId}/label/auto`,
|
||||
{},
|
||||
{
|
||||
params: { memberId },
|
||||
}
|
||||
)
|
||||
.then(({ data }) => data);
|
||||
export async function runAutoLabel(projectId: number, modelId = 1) {
|
||||
return api.post(`/projects/${projectId}/auto`, { modelId }).then(({ data }) => data);
|
||||
}
|
||||
|
@ -1,12 +1,20 @@
|
||||
import api from '@/api/axiosConfig';
|
||||
import { ModelRequest, ModelResponse, ProjectModelsResponse, ModelCategoryResponse } from '@/types';
|
||||
import {
|
||||
ModelRequest,
|
||||
ModelResponse,
|
||||
ProjectModelsResponse,
|
||||
ModelCategoryResponse,
|
||||
ModelTrainRequest,
|
||||
ResultResponse,
|
||||
ReportResponse,
|
||||
} from '@/types';
|
||||
|
||||
export async function updateModelName(projectId: number, modelId: number, modelData: ModelRequest) {
|
||||
return api.put<ModelResponse>(`/projects/${projectId}/models/${modelId}`, modelData).then(({ data }) => data);
|
||||
}
|
||||
|
||||
export async function trainModel(projectId: number) {
|
||||
return api.post(`/projects/${projectId}/train`).then(({ data }) => data);
|
||||
export async function trainModel(projectId: number, trainData: ModelTrainRequest) {
|
||||
return api.post(`/projects/${projectId}/train`, trainData).then(({ data }) => data);
|
||||
}
|
||||
|
||||
export async function getProjectModels(projectId: number) {
|
||||
@ -20,3 +28,11 @@ export async function addProjectModel(projectId: number, modelData: ModelRequest
|
||||
export async function getModelCategories(modelId: number) {
|
||||
return api.get<ModelCategoryResponse[]>(`/models/${modelId}/categories`).then(({ data }) => data);
|
||||
}
|
||||
|
||||
export async function getModelResults(modelId: number) {
|
||||
return api.get<ResultResponse[]>(`/results/model/${modelId}`).then(({ data }) => data);
|
||||
}
|
||||
|
||||
export async function getModelReports(projectId: number, modelId: number) {
|
||||
return api.get<ReportResponse[]>(`/projects/${projectId}/reports/model/${modelId}`).then(({ data }) => data);
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ export default function AdminLayout() {
|
||||
<AdminProjectSidebar />
|
||||
|
||||
<ResizablePanel className="flex w-full items-center">
|
||||
<main className="h-full grow">
|
||||
<main className="h-full grow overflow-y-auto">
|
||||
<Outlet />
|
||||
</main>
|
||||
</ResizablePanel>
|
||||
|
@ -6,4 +6,9 @@ export default {
|
||||
component: CanvasControlBar,
|
||||
};
|
||||
|
||||
export const Default = () => <CanvasControlBar saveJson={() => {}} />;
|
||||
export const Default = () => (
|
||||
<CanvasControlBar
|
||||
saveJson={() => {}}
|
||||
projectType="segmentation"
|
||||
/>
|
||||
);
|
||||
|
@ -2,16 +2,22 @@ import useCanvasStore from '@/stores/useCanvasStore';
|
||||
import { LucideIcon, MousePointer2, PenTool, Save, Square } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export default function CanvasControlBar({ saveJson }: { saveJson: () => void }) {
|
||||
export default function CanvasControlBar({
|
||||
saveJson,
|
||||
projectType,
|
||||
}: {
|
||||
saveJson: () => void;
|
||||
projectType: 'classification' | 'detection' | 'segmentation';
|
||||
}) {
|
||||
const drawState = useCanvasStore((state) => state.drawState);
|
||||
const setDrawState = useCanvasStore((state) => state.setDrawState);
|
||||
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,
|
||||
rect: Square,
|
||||
pen: PenTool,
|
||||
...(projectType === 'segmentation' ? { pen: PenTool } : { rect: Square }),
|
||||
};
|
||||
|
||||
return (
|
||||
@ -31,7 +37,7 @@ export default function CanvasControlBar({ saveJson }: { saveJson: () => void })
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="h-5 w-0.5 rounded bg-gray-400" />
|
||||
<button
|
||||
className={cn(buttonClassName, buttonBaseClassName)}
|
||||
onClick={saveJson}
|
||||
|
46
frontend/src/components/Header/UserProfileForm.tsx
Normal file
46
frontend/src/components/Header/UserProfileForm.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '../ui/button';
|
||||
import useAuthStore from '@/stores/useAuthStore';
|
||||
import useLogoutQuery from '@/queries/auth/useLogoutQuery';
|
||||
export default function UserProfileForm({ onClose }: { onClose: () => void }) {
|
||||
const profile = useAuthStore((state) => state.profile);
|
||||
const { nickname, profileImage } = profile || { nickname: '', profileImage: '' };
|
||||
|
||||
const logoutMutation = useLogoutQuery();
|
||||
const [isLoggingOut, setIsLoggingOut] = useState<boolean>(false);
|
||||
|
||||
const handleLogout = async () => {
|
||||
setIsLoggingOut(true);
|
||||
logoutMutation.mutate(undefined, {
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex items-center gap-4">
|
||||
{profileImage ? (
|
||||
<img
|
||||
src={profileImage}
|
||||
alt={`${nickname}'s profile`}
|
||||
className="h-16 w-16 rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-16 w-16 rounded-full bg-gray-300"></div>
|
||||
)}
|
||||
|
||||
<div className="text-lg font-bold">{nickname || 'Guest'}</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleLogout}
|
||||
variant="outlinePrimary"
|
||||
className="mt-4"
|
||||
disabled={isLoggingOut}
|
||||
>
|
||||
{isLoggingOut ? '로그아웃 중...' : '로그아웃'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
31
frontend/src/components/Header/UserProfileModal.tsx
Normal file
31
frontend/src/components/Header/UserProfileModal.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
|
||||
import { User } from 'lucide-react';
|
||||
import UserProfileForm from './UserProfileForm';
|
||||
|
||||
export default function UserProfileModal() {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
const handleOpen = () => setIsOpen(true);
|
||||
const handleClose = () => setIsOpen(false);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<button
|
||||
className="flex items-center justify-center p-2"
|
||||
onClick={handleOpen}
|
||||
>
|
||||
<User className="h-4 w-4 text-black sm:h-5 sm:w-5" />
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader title="프로필" />
|
||||
<UserProfileForm onClose={handleClose} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Bell, User } from 'lucide-react';
|
||||
import { Bell } from 'lucide-react';
|
||||
import { useLocation, Link, useParams } from 'react-router-dom';
|
||||
import UserProfileModal from './UserProfileModal';
|
||||
|
||||
export interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
@ -60,7 +61,7 @@ export default function Header({ className, ...props }: HeaderProps) {
|
||||
{!isHomePage && (
|
||||
<div className="flex items-center gap-4 md:gap-5">
|
||||
<Bell className="h-4 w-4 text-black sm:h-5 sm:w-5" />
|
||||
<User className="h-4 w-4 text-black sm:h-5 sm:w-5" />
|
||||
<UserProfileModal />
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
@ -10,16 +10,17 @@ import CanvasControlBar from '../CanvasControlBar';
|
||||
import { Label } from '@/types';
|
||||
import useLabelJson from '@/hooks/useLabelJson';
|
||||
import { saveImageLabels } from '@/api/lablingApi';
|
||||
import useProjectStore from '@/stores/useProjectStore';
|
||||
|
||||
export default function ImageCanvas() {
|
||||
const project = useCanvasStore((state) => state.project)!;
|
||||
const project = useProjectStore((state) => state.project)!;
|
||||
const { id: imageId, imagePath, dataPath } = useCanvasStore((state) => state.image)!;
|
||||
const { data: labelData, refetch } = useLabelJson(dataPath, project);
|
||||
const { shapes } = labelData || [];
|
||||
const selectedLabelId = useCanvasStore((state) => state.selectedLabelId);
|
||||
const setSelectedLabelId = useCanvasStore((state) => state.setSelectedLabelId);
|
||||
const sidebarSize = useCanvasStore((state) => state.sidebarSize);
|
||||
const stageWidth = window.innerWidth * ((100 - sidebarSize) / 100) - 280;
|
||||
const stageWidth = window.innerWidth * ((100 - sidebarSize) / 100) - 200;
|
||||
const stageHeight = window.innerHeight - 64;
|
||||
const stageRef = useRef<Konva.Stage>(null);
|
||||
const dragLayerRef = useRef<Konva.Layer>(null);
|
||||
@ -344,7 +345,10 @@ export default function ImageCanvas() {
|
||||
|
||||
<Layer ref={dragLayerRef} />
|
||||
</Stage>
|
||||
<CanvasControlBar saveJson={saveJson} />
|
||||
<CanvasControlBar
|
||||
saveJson={saveJson}
|
||||
projectType={project.type}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
|
@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import { Button } from '../ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import useAuthStore from '@/stores/useAuthStore';
|
||||
import { X } from 'lucide-react';
|
||||
import { CircleCheckBig, CircleDashed, CircleX, X } from 'lucide-react';
|
||||
import useUploadImageFileQuery from '@/queries/projects/useUploadImageFileQuery';
|
||||
|
||||
export default function ImageUploadFileForm({
|
||||
@ -40,6 +40,8 @@ export default function ImageUploadFileForm({
|
||||
|
||||
setFiles((prevFiles) => [...prevFiles, ...newImages]);
|
||||
}
|
||||
|
||||
event.target.value = '';
|
||||
};
|
||||
|
||||
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
@ -120,16 +122,40 @@ export default function ImageUploadFileForm({
|
||||
className={cn('flex items-center justify-between p-1')}
|
||||
>
|
||||
<span className="truncate">{file.webkitRelativePath || file.name}</span>
|
||||
<button
|
||||
className={'cursor-pointer p-2'}
|
||||
onClick={() => handleRemoveFile(index)}
|
||||
>
|
||||
<X
|
||||
color="red"
|
||||
size={16}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</button>
|
||||
{isUploading ? (
|
||||
<div className="p-2">
|
||||
{isUploaded ? (
|
||||
<CircleCheckBig
|
||||
className="stroke-green-500"
|
||||
size={20}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
) : isFailed ? (
|
||||
<CircleX
|
||||
className="stroke-red-500"
|
||||
size={20}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
) : (
|
||||
<CircleDashed
|
||||
className="stroke-gray-500"
|
||||
size={20}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className={'cursor-pointer p-2'}
|
||||
onClick={() => handleRemoveFile(index)}
|
||||
>
|
||||
<X
|
||||
color="red"
|
||||
size={16}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import { Button } from '../ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import useAuthStore from '@/stores/useAuthStore';
|
||||
import { X } from 'lucide-react';
|
||||
import { CircleCheckBig, CircleDashed, CircleX, X } from 'lucide-react';
|
||||
import useUploadImageFolderQuery from '@/queries/projects/useUploadImageFolderQuery';
|
||||
|
||||
export default function ImageUploadFolderForm({ onClose, projectId }: { onClose: () => void; projectId: number }) {
|
||||
@ -27,6 +27,8 @@ export default function ImageUploadFolderForm({ onClose, projectId }: { onClose:
|
||||
if (newFiles) {
|
||||
setFiles((prevFiles) => [...prevFiles, ...Array.from(newFiles)]);
|
||||
}
|
||||
|
||||
event.target.value = '';
|
||||
};
|
||||
|
||||
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
@ -105,16 +107,40 @@ export default function ImageUploadFolderForm({ onClose, projectId }: { onClose:
|
||||
className={cn('flex items-center justify-between p-1')}
|
||||
>
|
||||
<span className="truncate">{file.webkitRelativePath || file.name}</span>
|
||||
<button
|
||||
className={'cursor-pointer p-2'}
|
||||
onClick={() => handleRemoveFile(index)}
|
||||
>
|
||||
<X
|
||||
color="red"
|
||||
size={16}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</button>
|
||||
{isUploading ? (
|
||||
<div className="p-2">
|
||||
{isUploaded ? (
|
||||
<CircleCheckBig
|
||||
className="stroke-green-500"
|
||||
size={20}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
) : isFailed ? (
|
||||
<CircleX
|
||||
className="stroke-red-500"
|
||||
size={20}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
) : (
|
||||
<CircleDashed
|
||||
className="stroke-gray-500"
|
||||
size={20}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className={'cursor-pointer p-2'}
|
||||
onClick={() => handleRemoveFile(index)}
|
||||
>
|
||||
<X
|
||||
color="red"
|
||||
size={16}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import { Button } from '../ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import useAuthStore from '@/stores/useAuthStore';
|
||||
import { X } from 'lucide-react';
|
||||
import { CircleCheckBig, CircleDashed, CircleX, X } from 'lucide-react';
|
||||
import useUploadImageZipQuery from '@/queries/projects/useUploadImageZipQuery';
|
||||
|
||||
export default function ImageUploadZipForm({ onClose, projectId }: { onClose: () => void; projectId: number }) {
|
||||
@ -27,6 +27,8 @@ export default function ImageUploadZipForm({ onClose, projectId }: { onClose: ()
|
||||
if (newFiles) {
|
||||
setFile(newFiles[0]);
|
||||
}
|
||||
|
||||
event.target.value = '';
|
||||
};
|
||||
|
||||
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
@ -103,16 +105,40 @@ export default function ImageUploadZipForm({ onClose, projectId }: { onClose: ()
|
||||
{file && (
|
||||
<div className={'flex items-center justify-between p-1'}>
|
||||
<span className="truncate">{file.webkitRelativePath || file.name}</span>
|
||||
<button
|
||||
className={'cursor-pointer p-2'}
|
||||
onClick={() => handleRemoveFile()}
|
||||
>
|
||||
<X
|
||||
color="red"
|
||||
size={16}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</button>
|
||||
{isUploading ? (
|
||||
<div className="p-2">
|
||||
{isUploaded ? (
|
||||
<CircleCheckBig
|
||||
className="stroke-green-500"
|
||||
size={20}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
) : isFailed ? (
|
||||
<CircleX
|
||||
className="stroke-red-500"
|
||||
size={20}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
) : (
|
||||
<CircleDashed
|
||||
className="stroke-gray-500"
|
||||
size={20}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className={'cursor-pointer p-2'}
|
||||
onClick={() => handleRemoveFile()}
|
||||
>
|
||||
<X
|
||||
color="red"
|
||||
size={16}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isUploading ? (
|
||||
|
@ -1,114 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { TrendingUp } from 'lucide-react';
|
||||
import { CartesianGrid, Line, LineChart, XAxis } from 'recharts';
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart';
|
||||
|
||||
interface MetricData {
|
||||
epoch: string;
|
||||
loss1: number;
|
||||
loss2: number;
|
||||
loss3: number;
|
||||
fitness: number;
|
||||
}
|
||||
|
||||
interface ModelLineChartProps {
|
||||
data: MetricData[];
|
||||
}
|
||||
|
||||
const chartConfig = {
|
||||
loss1: {
|
||||
label: 'Loss 1',
|
||||
color: '#FF6347', // 토마토색
|
||||
},
|
||||
loss2: {
|
||||
label: 'Loss 2',
|
||||
color: '#1E90FF', // 다저블루색
|
||||
},
|
||||
loss3: {
|
||||
label: 'Loss 3',
|
||||
color: '#32CD32', // 라임색
|
||||
},
|
||||
fitness: {
|
||||
label: 'Fitness',
|
||||
color: '#FFD700', // 골드색
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export default function ModelLineChart({ data }: ModelLineChartProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Model Training Metrics</CardTitle>
|
||||
<CardDescription>Loss and Fitness over Epochs</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={data}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="epoch"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => `Epoch ${value}`}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent />}
|
||||
/>
|
||||
<Line
|
||||
dataKey="loss1"
|
||||
type="monotone"
|
||||
stroke={chartConfig.loss1.color}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
<Line
|
||||
dataKey="loss2"
|
||||
type="monotone"
|
||||
stroke={chartConfig.loss2.color}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
<Line
|
||||
dataKey="loss3"
|
||||
type="monotone"
|
||||
stroke={chartConfig.loss3.color}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
<Line
|
||||
dataKey="fitness"
|
||||
type="monotone"
|
||||
stroke={chartConfig.fitness.color}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex w-full items-start gap-2 text-sm">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this epoch <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="text-muted-foreground flex items-center gap-2 leading-none">
|
||||
Showing training loss and fitness for the current model
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
@ -1,55 +1,130 @@
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import ModelBarChart from '@/components/ModelBarChart';
|
||||
import useProjectModelsQuery from '@/queries/models/useProjectModelsQuery';
|
||||
import useModelReportsQuery from '@/queries/models/useModelReportsQuery';
|
||||
import useModelResultsQuery from '@/queries/models/useModelResultsQuery';
|
||||
import ModelBarChart from './ModelBarChart';
|
||||
import ModelLineChart from './ModelLineChart';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface EvaluationTabProps {
|
||||
selectedModel: string | null;
|
||||
setSelectedModel: (model: string | null) => void;
|
||||
projectId: number | null;
|
||||
}
|
||||
|
||||
export default function EvaluationTab({ selectedModel, setSelectedModel }: EvaluationTabProps) {
|
||||
export default function EvaluationTab({ projectId }: EvaluationTabProps) {
|
||||
const [selectedModel, setSelectedModel] = useState<number | null>(null);
|
||||
const { data: models } = useProjectModelsQuery(projectId ?? 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<Label htmlFor="select-model">모델 선택</Label>
|
||||
<Select onValueChange={setSelectedModel}>
|
||||
<SelectTrigger id="select-model">
|
||||
<SelectValue placeholder="모델을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="genesis">Genesis</SelectItem>
|
||||
<SelectItem value="explorer">Explorer</SelectItem>
|
||||
<SelectItem value="quantum">Quantum</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<ModelSelection
|
||||
models={models}
|
||||
setSelectedModel={setSelectedModel}
|
||||
/>
|
||||
|
||||
{selectedModel && (
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div className="flex flex-col gap-6">
|
||||
<ModelBarChart
|
||||
data={[
|
||||
{ name: 'precision', value: 0.734, fill: 'var(--color-precision)' },
|
||||
{ name: 'recall', value: 0.75, fill: 'var(--color-recall)' },
|
||||
{ name: 'mAP50', value: 0.995, fill: 'var(--color-map50)' },
|
||||
{ name: 'mAP50_95', value: 0.97, fill: 'var(--color-map50-95)' },
|
||||
{ name: 'fitness', value: 0.973, fill: 'var(--color-fitness)' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center">
|
||||
<LabelingPreview />
|
||||
</div>
|
||||
</div>
|
||||
<ModelEvaluation
|
||||
projectId={projectId as number}
|
||||
selectedModel={selectedModel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LabelingPreview() {
|
||||
interface ModelSelectionProps {
|
||||
models: Array<{ id: number; name: string }> | undefined;
|
||||
setSelectedModel: (modelId: number) => void;
|
||||
}
|
||||
|
||||
function ModelSelection({ models, setSelectedModel }: ModelSelectionProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-center rounded-lg border bg-white p-4">
|
||||
<p>레이블링 프리뷰</p>
|
||||
<div className="mb-4">
|
||||
<Label htmlFor="select-model">모델 선택</Label>
|
||||
<Select onValueChange={(value) => setSelectedModel(parseInt(value))}>
|
||||
<SelectTrigger id="select-model">
|
||||
<SelectValue placeholder="모델을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{models?.map((model) => (
|
||||
<SelectItem
|
||||
key={model.id}
|
||||
value={model.id.toString()}
|
||||
>
|
||||
{model.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ModelEvaluationProps {
|
||||
projectId: number;
|
||||
selectedModel: number;
|
||||
}
|
||||
|
||||
function ModelEvaluation({ projectId, selectedModel }: ModelEvaluationProps) {
|
||||
const { data: reportData } = useModelReportsQuery(projectId, selectedModel);
|
||||
const { data: resultData } = useModelResultsQuery(selectedModel);
|
||||
|
||||
if (!reportData || !resultData) return null;
|
||||
|
||||
const trainingInfoRow = (
|
||||
<div className="flex justify-between rounded-lg bg-gray-100 p-4">
|
||||
<div className="flex-1 text-center">
|
||||
<strong>Epochs</strong>
|
||||
<p>{resultData[0]?.epochs}</p>
|
||||
</div>
|
||||
<div className="flex-1 text-center">
|
||||
<strong>Batch Size</strong>
|
||||
<p>{resultData[0]?.batch}</p>
|
||||
</div>
|
||||
<div className="flex-1 text-center">
|
||||
<strong>Learning Rate (Start)</strong>
|
||||
<p>{resultData[0]?.lr0}</p>
|
||||
</div>
|
||||
<div className="flex-1 text-center">
|
||||
<strong>Learning Rate (End)</strong>
|
||||
<p>{resultData[0]?.lrf}</p>
|
||||
</div>
|
||||
<div className="flex-1 text-center">
|
||||
<strong>Optimizer</strong>
|
||||
<p>{resultData[0]?.optimizer}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{trainingInfoRow} {/* 학습 정보 표시 */}
|
||||
<div className="mt-4 grid h-[400px] gap-8 md:grid-cols-2">
|
||||
{' '}
|
||||
{/* grid와 높이 설정 */}
|
||||
<div className="flex h-full flex-col gap-6">
|
||||
{' '}
|
||||
{/* 차트의 높이를 100%로 맞춤 */}
|
||||
<ModelBarChart
|
||||
data={[
|
||||
{ name: 'precision', value: resultData[0]?.precision, fill: 'var(--color-precision)' },
|
||||
{ name: 'recall', value: resultData[0]?.recall, fill: 'var(--color-recall)' },
|
||||
{ name: 'mAP50', value: resultData[0]?.map50, fill: 'var(--color-map50)' },
|
||||
{ name: 'mAP50_95', value: resultData[0]?.map5095, fill: 'var(--color-map50-95)' },
|
||||
{ name: 'fitness', value: resultData[0]?.fitness, fill: 'var(--color-fitness)' },
|
||||
]}
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-full flex-col gap-6">
|
||||
{' '}
|
||||
{/* 차트의 높이를 100%로 맞춤 */}
|
||||
<ModelLineChart
|
||||
data={reportData}
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
26
frontend/src/components/ModelManage/InputWithLabel.tsx
Normal file
26
frontend/src/components/ModelManage/InputWithLabel.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '../ui/input';
|
||||
interface InputWithLabelProps {
|
||||
label: string;
|
||||
id: string;
|
||||
placeholder: string;
|
||||
value: number;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function InputWithLabel({ label, id, placeholder, value, disabled, onChange }: InputWithLabelProps) {
|
||||
return (
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
<Input
|
||||
id={id}
|
||||
type="number"
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { TrendingUp } from 'lucide-react';
|
||||
import { Bar, BarChart, CartesianGrid, Rectangle, XAxis } from 'recharts';
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart';
|
||||
|
||||
interface MetricData {
|
||||
@ -14,10 +13,9 @@ interface MetricData {
|
||||
|
||||
interface ModelBarChartProps {
|
||||
data: MetricData[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const description = 'A bar chart with an active bar';
|
||||
|
||||
const chartConfig = {
|
||||
precision: {
|
||||
label: 'Precision',
|
||||
@ -41,9 +39,9 @@ const chartConfig = {
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export default function ModelBarChart({ data }: ModelBarChartProps) {
|
||||
export default function ModelBarChart({ data, className }: ModelBarChartProps) {
|
||||
return (
|
||||
<Card>
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle>Model Metrics</CardTitle>
|
||||
<CardDescription>Performance metrics of the model</CardDescription>
|
||||
@ -86,12 +84,6 @@ export default function ModelBarChart({ data }: ModelBarChartProps) {
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 font-medium leading-none">
|
||||
Model metrics are trending well <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="text-muted-foreground leading-none">Showing current performance metrics</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
115
frontend/src/components/ModelManage/ModelLineChart.tsx
Normal file
115
frontend/src/components/ModelManage/ModelLineChart.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
'use client';
|
||||
|
||||
import { CartesianGrid, Line, LineChart, XAxis, YAxis, Tooltip, Legend } from 'recharts';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { ChartConfig, ChartContainer } from '@/components/ui/chart';
|
||||
import { ReportResponse } from '@/types';
|
||||
|
||||
interface ModelLineChartProps {
|
||||
data: ReportResponse[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const chartConfig = {
|
||||
boxLoss: {
|
||||
label: 'Box Loss',
|
||||
color: '#FF6347',
|
||||
},
|
||||
classLoss: {
|
||||
label: 'Class Loss',
|
||||
color: '#1E90FF',
|
||||
},
|
||||
dflLoss: {
|
||||
label: 'DFL Loss',
|
||||
color: '#32CD32',
|
||||
},
|
||||
fitness: {
|
||||
label: 'Fitness',
|
||||
color: '#FFD700',
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export default function ModelLineChart({ data, className }: ModelLineChartProps) {
|
||||
const latestData = data.length > 0 ? data[data.length - 1] : undefined;
|
||||
|
||||
const totalEpochs = latestData?.totalEpochs || 0;
|
||||
const emptyData = Array.from({ length: totalEpochs }, (_, i) => ({
|
||||
epoch: (i + 1).toString(),
|
||||
boxLoss: null,
|
||||
classLoss: null,
|
||||
dflLoss: null,
|
||||
fitness: null,
|
||||
}));
|
||||
|
||||
const filledData = emptyData.map((item, index) => ({
|
||||
...item,
|
||||
...(data[index] || {}),
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle>Model Training Metrics</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{latestData && latestData.totalEpochs !== Number(latestData.epoch) && (
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p>현재 에포크: {latestData.epoch}</p>
|
||||
<p>총 에포크: {latestData.totalEpochs}</p>
|
||||
<p>예상 남은시간: {latestData.leftSecond}초</p>
|
||||
</div>
|
||||
)}
|
||||
<ChartContainer config={chartConfig}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={filledData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="epoch"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => `Epoch ${value}`}
|
||||
/>
|
||||
<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}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
53
frontend/src/components/ModelManage/SelectWithLabel.tsx
Normal file
53
frontend/src/components/ModelManage/SelectWithLabel.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
interface SelectWithLabelOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface SelectWithLabelProps {
|
||||
label: string;
|
||||
id: string;
|
||||
options: SelectWithLabelOption[];
|
||||
placeholder: string;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export default function SelectWithLabel({
|
||||
label,
|
||||
id,
|
||||
options,
|
||||
placeholder,
|
||||
value,
|
||||
disabled,
|
||||
onChange,
|
||||
}: SelectWithLabelProps) {
|
||||
return (
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger id={id}>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,189 +0,0 @@
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import useProjectModelsQuery from '@/queries/models/useProjectModelsQuery';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface SettingsFormProps {
|
||||
projectId: string | null; // projectId를 프랍으로 받음
|
||||
onSubmit?: (data: SettingsFormData) => void;
|
||||
}
|
||||
|
||||
export interface SettingsFormData {
|
||||
projectId: number | null;
|
||||
selectedModel: string | null;
|
||||
ratio: number;
|
||||
epochs: number;
|
||||
batchSize: number;
|
||||
optimizer: string;
|
||||
lr0: number;
|
||||
lrf: number;
|
||||
}
|
||||
|
||||
export default function SettingsForm({ projectId, onSubmit }: SettingsFormProps) {
|
||||
const numericProjectId = projectId ? parseInt(projectId, 10) : null;
|
||||
|
||||
const { data: models } = useProjectModelsQuery(numericProjectId ?? 0);
|
||||
const [selectedModel, setSelectedModel] = useState<string | null>(null);
|
||||
const [ratio, setRatio] = useState<number>(0.8);
|
||||
const [epochs, setEpochs] = useState<number>(50);
|
||||
const [batchSize, setBatchSize] = useState<number>(32);
|
||||
const [optimizer, setOptimizer] = useState<string>('SGD');
|
||||
const [lr0, setLr0] = useState<number>(0.01);
|
||||
const [lrf, setLrf] = useState<number>(0.001);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (onSubmit) {
|
||||
onSubmit({
|
||||
projectId: numericProjectId,
|
||||
selectedModel,
|
||||
ratio,
|
||||
epochs,
|
||||
batchSize,
|
||||
optimizer,
|
||||
lr0,
|
||||
lrf,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
className="grid w-full gap-6"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<fieldset className="grid gap-6 rounded-lg border p-4">
|
||||
<legend className="-ml-1 px-1 text-sm font-medium">모델 설정</legend>
|
||||
|
||||
{/* 모델 선택 */}
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="model">모델 선택</Label>
|
||||
<Select onValueChange={setSelectedModel}>
|
||||
<SelectTrigger id="model">
|
||||
<SelectValue placeholder="모델을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{models?.map((model) => (
|
||||
<SelectItem
|
||||
key={model.id}
|
||||
value={model.name}
|
||||
>
|
||||
{model.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 훈련/검증 비율 및 학습 파라미터 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<InputWithLabel
|
||||
label="훈련/검증 비율"
|
||||
placeholder="예: 0.8 (80% 훈련, 20% 검증)"
|
||||
id="ratio"
|
||||
value={ratio}
|
||||
onChange={(e) => setRatio(parseFloat(e.target.value))}
|
||||
/>
|
||||
<InputWithLabel
|
||||
label="에포크 수"
|
||||
placeholder="예: 50 (총 반복 횟수)"
|
||||
id="epochs"
|
||||
value={epochs}
|
||||
onChange={(e) => setEpochs(parseInt(e.target.value, 10))}
|
||||
/>
|
||||
<InputWithLabel
|
||||
label="Batch 크기"
|
||||
placeholder="예: 32 (한번에 처리할 샘플 수)"
|
||||
id="batch"
|
||||
value={batchSize}
|
||||
onChange={(e) => setBatchSize(parseInt(e.target.value, 10))}
|
||||
/>
|
||||
<SelectWithLabel
|
||||
label="옵티마이저"
|
||||
id="optimizer"
|
||||
options={['SGD', 'Adam', 'AdamW', 'NAdam', 'RAdam', 'RMSProp']}
|
||||
value={optimizer}
|
||||
onChange={setOptimizer}
|
||||
placeholder="옵티마이저 선택"
|
||||
/>
|
||||
<InputWithLabel
|
||||
label="학습률(LR0)"
|
||||
placeholder="예: 0.01 (초기 학습률)"
|
||||
id="lr0"
|
||||
value={lr0}
|
||||
onChange={(e) => setLr0(parseFloat(e.target.value))}
|
||||
/>
|
||||
<InputWithLabel
|
||||
label="최종 학습률(LRF)"
|
||||
placeholder="예: 0.001 (최종 학습률)"
|
||||
id="lrf"
|
||||
value={lrf}
|
||||
onChange={(e) => setLrf(parseFloat(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-primary"
|
||||
>
|
||||
설정 저장
|
||||
</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
interface InputWithLabelProps {
|
||||
label: string;
|
||||
id: string;
|
||||
placeholder: string;
|
||||
value: number;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
function InputWithLabel({ label, id, placeholder, value, onChange }: InputWithLabelProps) {
|
||||
return (
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
<Input
|
||||
id={id}
|
||||
type="number"
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SelectWithLabelProps {
|
||||
label: string;
|
||||
id: string;
|
||||
options: string[];
|
||||
placeholder: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
function SelectWithLabel({ label, id, options, placeholder, onChange }: SelectWithLabelProps) {
|
||||
return (
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
<Select onValueChange={onChange}>
|
||||
<SelectTrigger id={id}>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((option) => (
|
||||
<SelectItem
|
||||
key={option}
|
||||
value={option}
|
||||
>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
64
frontend/src/components/ModelManage/TrainingGraph.tsx
Normal file
64
frontend/src/components/ModelManage/TrainingGraph.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import ModelLineChart from './ModelLineChart';
|
||||
import usePollingModelReportsQuery from '@/queries/models/usePollingModelReportsQuery';
|
||||
import useModelStore from '@/stores/useModelStore';
|
||||
|
||||
interface TrainingGraphProps {
|
||||
projectId: number | null;
|
||||
selectedModel: number | null;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function TrainingGraph({ projectId, selectedModel, className }: TrainingGraphProps) {
|
||||
const { isTrainingByProject, setIsTraining, saveTrainingData, resetTrainingData, trainingDataByProject } =
|
||||
useModelStore((state) => ({
|
||||
isTrainingByProject: state.isTrainingByProject,
|
||||
setIsTraining: state.setIsTraining,
|
||||
saveTrainingData: state.saveTrainingData,
|
||||
resetTrainingData: state.resetTrainingData,
|
||||
trainingDataByProject: state.trainingDataByProject,
|
||||
}));
|
||||
|
||||
const isTraining = isTrainingByProject[projectId?.toString() || ''] || false;
|
||||
|
||||
const { data: fetchedTrainingDataList } = usePollingModelReportsQuery(
|
||||
projectId as number,
|
||||
selectedModel ?? 0,
|
||||
isTraining && !!projectId && !!selectedModel
|
||||
);
|
||||
|
||||
const trainingDataList = useMemo(() => {
|
||||
return trainingDataByProject[projectId?.toString() || ''] || fetchedTrainingDataList || [];
|
||||
}, [projectId, trainingDataByProject, fetchedTrainingDataList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchedTrainingDataList) {
|
||||
saveTrainingData(projectId?.toString() || '', fetchedTrainingDataList);
|
||||
}
|
||||
}, [fetchedTrainingDataList, projectId, saveTrainingData]);
|
||||
|
||||
const latestData = useMemo(() => {
|
||||
return (
|
||||
trainingDataList?.[trainingDataList.length - 1] || {
|
||||
epoch: 0,
|
||||
totalEpochs: 0,
|
||||
leftSecond: 0,
|
||||
}
|
||||
);
|
||||
}, [trainingDataList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (latestData.epoch === latestData.totalEpochs && latestData.totalEpochs > 0) {
|
||||
alert('학습이 완료되었습니다!');
|
||||
setIsTraining(projectId?.toString() || '', false);
|
||||
resetTrainingData(projectId?.toString() || '');
|
||||
}
|
||||
}, [latestData.epoch, latestData.totalEpochs, setIsTraining, resetTrainingData, projectId]);
|
||||
|
||||
return (
|
||||
<ModelLineChart
|
||||
data={trainingDataList}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
144
frontend/src/components/ModelManage/TrainingSettings.tsx
Normal file
144
frontend/src/components/ModelManage/TrainingSettings.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import SelectWithLabel from './SelectWithLabel';
|
||||
import InputWithLabel from './InputWithLabel';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import useProjectModelsQuery from '@/queries/models/useProjectModelsQuery';
|
||||
import useModelStore from '@/stores/useModelStore';
|
||||
import { ModelTrainRequest } from '@/types';
|
||||
import { useState } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface TrainingSettingsProps {
|
||||
projectId: number | null;
|
||||
selectedModel: number | null;
|
||||
setSelectedModel: (model: number | null) => void;
|
||||
handleTrainingStart: (trainData: ModelTrainRequest) => void;
|
||||
handleTrainingStop: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function TrainingSettings({
|
||||
projectId,
|
||||
selectedModel,
|
||||
setSelectedModel,
|
||||
handleTrainingStart,
|
||||
handleTrainingStop,
|
||||
className,
|
||||
}: TrainingSettingsProps) {
|
||||
const { data: models } = useProjectModelsQuery(projectId ?? 0);
|
||||
|
||||
const isTraining = useModelStore((state) => state.isTrainingByProject[projectId?.toString() || ''] || false);
|
||||
|
||||
const [ratio, setRatio] = useState<number>(0.8);
|
||||
const [epochs, setEpochs] = useState<number>(50);
|
||||
const [batchSize, setBatchSize] = useState<number>(32);
|
||||
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 handleSubmit = () => {
|
||||
if (isTraining) {
|
||||
handleTrainingStop();
|
||||
} else if (selectedModel !== null) {
|
||||
const trainData: ModelTrainRequest = {
|
||||
modelId: selectedModel,
|
||||
ratio,
|
||||
epochs,
|
||||
batch: batchSize,
|
||||
optimizer,
|
||||
lr0,
|
||||
lrf,
|
||||
};
|
||||
handleTrainingStart(trainData);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<fieldset className={cn('grid gap-6 rounded-lg border p-4', className)}>
|
||||
{' '}
|
||||
<legend className="-ml-1 px-1 text-sm font-medium">모델 설정</legend>
|
||||
<div className="grid gap-3">
|
||||
<SelectWithLabel
|
||||
label="모델 선택"
|
||||
id="model"
|
||||
options={
|
||||
models?.map((model) => ({
|
||||
label: model.name,
|
||||
value: model.id.toString(),
|
||||
})) || []
|
||||
}
|
||||
placeholder="모델을 선택하세요"
|
||||
value={selectedModel ? selectedModel.toString() : ''}
|
||||
onChange={(value) => setSelectedModel(parseInt(value, 10))}
|
||||
disabled={isTraining}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<InputWithLabel
|
||||
label="훈련/검증 비율"
|
||||
placeholder="예: 0.8 (80% 훈련, 20% 검증)"
|
||||
id="ratio"
|
||||
value={ratio}
|
||||
onChange={(e) => setRatio(parseFloat(e.target.value))}
|
||||
disabled={isTraining}
|
||||
/>
|
||||
<InputWithLabel
|
||||
label="에포크 수"
|
||||
placeholder="예: 50 (총 반복 횟수)"
|
||||
id="epochs"
|
||||
value={epochs}
|
||||
onChange={(e) => setEpochs(parseInt(e.target.value, 10))}
|
||||
disabled={isTraining}
|
||||
/>
|
||||
<InputWithLabel
|
||||
label="Batch 크기"
|
||||
placeholder="예: 32 (한번에 처리할 샘플 수)"
|
||||
id="batch"
|
||||
value={batchSize}
|
||||
onChange={(e) => setBatchSize(parseInt(e.target.value, 10))}
|
||||
disabled={isTraining}
|
||||
/>
|
||||
<SelectWithLabel
|
||||
label="옵티마이저"
|
||||
id="optimizer"
|
||||
options={[
|
||||
{ label: 'AUTO', value: 'AUTO' },
|
||||
{ label: 'SGD', value: 'SGD' },
|
||||
{ label: 'ADAM', value: 'ADAM' },
|
||||
{ label: 'ADAMW', value: 'ADAMW' },
|
||||
{ label: 'NADAM', value: 'NADAM' },
|
||||
{ label: 'RADAM', value: 'RADAM' },
|
||||
{ label: 'RMSPROP', value: 'RMSPROP' },
|
||||
]}
|
||||
placeholder="옵티마이저 선택"
|
||||
value={optimizer}
|
||||
onChange={(value) => setOptimizer(value as 'AUTO' | 'SGD' | 'ADAM' | 'ADAMW' | 'NADAM' | 'RADAM' | 'RMSPROP')}
|
||||
disabled={isTraining} // 학습 중일 때 옵티마이저 선택 비활성화
|
||||
/>
|
||||
<InputWithLabel
|
||||
label="학습률(LR0)"
|
||||
placeholder="예: 0.01 (초기 학습률)"
|
||||
id="lr0"
|
||||
value={lr0}
|
||||
onChange={(e) => setLr0(parseFloat(e.target.value))}
|
||||
disabled={isTraining}
|
||||
/>
|
||||
<InputWithLabel
|
||||
label="최종 학습률(LRF)"
|
||||
placeholder="예: 0.001 (최종 학습률)"
|
||||
id="lrf"
|
||||
value={lrf}
|
||||
onChange={(e) => setLrf(parseFloat(e.target.value))}
|
||||
disabled={isTraining}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outlinePrimary"
|
||||
size="lg"
|
||||
onClick={handleSubmit}
|
||||
disabled={!selectedModel}
|
||||
>
|
||||
{isTraining ? '학습 중단' : '학습 시작'}
|
||||
</Button>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
@ -1,45 +1,59 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import ModelLineChart from '@/components/ModelLineChart';
|
||||
import SettingsForm from './SettingsForm';
|
||||
import useTrainModelQuery from '@/queries/models/useTrainModelQuery';
|
||||
import useModelStore from '@/stores/useModelStore';
|
||||
import TrainingSettings from './TrainingSettings';
|
||||
import TrainingGraph from './TrainingGraph';
|
||||
import { ModelTrainRequest } from '@/types';
|
||||
|
||||
interface TrainingTabProps {
|
||||
training: boolean;
|
||||
handleTrainingToggle: () => void;
|
||||
trainingDataList: {
|
||||
epoch: number;
|
||||
box_loss: number;
|
||||
cls_loss: number;
|
||||
dfl_loss: number;
|
||||
fitness: number;
|
||||
}[];
|
||||
projectId: string | null; // projectId를 프랍으로 받음
|
||||
projectId: number | null;
|
||||
}
|
||||
|
||||
export default function TrainingTab({ training, handleTrainingToggle, trainingDataList, projectId }: TrainingTabProps) {
|
||||
return (
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div className="flex flex-col gap-6">
|
||||
<SettingsForm projectId={projectId} />
|
||||
<Button
|
||||
variant={training ? 'destructive' : 'outlinePrimary'}
|
||||
size="lg"
|
||||
onClick={handleTrainingToggle}
|
||||
>
|
||||
{training ? '학습 중단' : '학습 시작'}
|
||||
</Button>
|
||||
</div>
|
||||
export default function TrainingTab({ projectId }: TrainingTabProps) {
|
||||
const numericProjectId = projectId ? parseInt(projectId.toString(), 10) : null;
|
||||
const { isTrainingByProject, setIsTraining, selectedModelByProject, setSelectedModel, resetTrainingData } =
|
||||
useModelStore((state) => ({
|
||||
isTrainingByProject: state.isTrainingByProject,
|
||||
setIsTraining: state.setIsTraining,
|
||||
selectedModelByProject: state.selectedModelByProject,
|
||||
setSelectedModel: state.setSelectedModel,
|
||||
resetTrainingData: state.resetTrainingData,
|
||||
}));
|
||||
|
||||
<div className="flex flex-col justify-center">
|
||||
<ModelLineChart
|
||||
data={trainingDataList.map((data) => ({
|
||||
epoch: data.epoch.toString(),
|
||||
loss1: data.box_loss,
|
||||
loss2: data.cls_loss,
|
||||
loss3: data.dfl_loss,
|
||||
fitness: data.fitness,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
const isTraining = isTrainingByProject[numericProjectId?.toString() || ''] || false;
|
||||
const selectedModel = selectedModelByProject[numericProjectId?.toString() || ''];
|
||||
|
||||
const { mutate: startTraining } = useTrainModelQuery(numericProjectId as number);
|
||||
|
||||
const handleTrainingStart = (trainData: ModelTrainRequest) => {
|
||||
if (!isTraining && selectedModel !== null) {
|
||||
setIsTraining(numericProjectId?.toString() || '', true);
|
||||
startTraining(trainData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTrainingStop = () => {
|
||||
if (isTraining) {
|
||||
setIsTraining(numericProjectId?.toString() || '', false);
|
||||
resetTrainingData(numericProjectId?.toString() || '');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-rows-[auto_1fr] gap-8 md:grid-cols-2">
|
||||
<TrainingSettings
|
||||
projectId={numericProjectId}
|
||||
selectedModel={selectedModel}
|
||||
setSelectedModel={(modelId) => setSelectedModel(numericProjectId?.toString() || '', modelId)}
|
||||
handleTrainingStart={handleTrainingStart}
|
||||
handleTrainingStop={handleTrainingStop}
|
||||
className="h-full"
|
||||
/>
|
||||
|
||||
<TrainingGraph
|
||||
projectId={numericProjectId}
|
||||
selectedModel={selectedModel}
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,28 +1,11 @@
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import useTrainWebSocket from '@/hooks/useTrainPolling';
|
||||
import useTrainStore from '@/stores/useTrainStore';
|
||||
import TrainingTab from './TrainingTab';
|
||||
import EvaluationTab from './EvaluationTab';
|
||||
|
||||
export default function ModelManage() {
|
||||
const { projectId } = useParams<{ projectId?: string }>();
|
||||
const [training, setTraining] = useState(false);
|
||||
const [selectedModel, setSelectedModel] = useState<string | null>(null);
|
||||
|
||||
const numericProjectId = projectId ?? null;
|
||||
|
||||
useTrainWebSocket(training, numericProjectId);
|
||||
|
||||
const { trainingDataList } = useTrainStore((state) => ({
|
||||
trainingDataList: numericProjectId ? state.trainingDataByProject[numericProjectId] || [] : [],
|
||||
}));
|
||||
|
||||
const handleTrainingToggle = () => {
|
||||
setTraining((prev) => !prev);
|
||||
};
|
||||
const numericProjectId = projectId ? parseInt(projectId, 10) : null;
|
||||
|
||||
return (
|
||||
<div className="grid h-screen w-full">
|
||||
@ -41,22 +24,12 @@ export default function ModelManage() {
|
||||
<TabsTrigger value="results">모델 평가</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 학습 탭 */}
|
||||
<TabsContent value="train">
|
||||
<TrainingTab
|
||||
training={training}
|
||||
handleTrainingToggle={handleTrainingToggle}
|
||||
trainingDataList={trainingDataList}
|
||||
projectId={numericProjectId}
|
||||
/>
|
||||
<TrainingTab projectId={numericProjectId} />
|
||||
</TabsContent>
|
||||
|
||||
{/* 평가 탭 */}
|
||||
<TabsContent value="results">
|
||||
<EvaluationTab
|
||||
selectedModel={selectedModel}
|
||||
setSelectedModel={setSelectedModel}
|
||||
/>
|
||||
<EvaluationTab projectId={numericProjectId} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</main>
|
||||
|
@ -8,15 +8,15 @@ import useWorkspaceListQuery from '@/queries/workspaces/useWorkspaceListQuery';
|
||||
import useCreateWorkspaceQuery from '@/queries/workspaces/useCreateWorkspaceQuery';
|
||||
|
||||
export default function WorkspaceBrowseLayout() {
|
||||
const { profile, isLoggedIn } = useAuthStore();
|
||||
const { profile } = useAuthStore();
|
||||
const memberId = profile?.id ?? 0;
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn || memberId == 0) {
|
||||
if (memberId == 0) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [isLoggedIn, memberId, navigate]);
|
||||
}, [memberId, navigate]);
|
||||
|
||||
const { data: workspacesResponse } = useWorkspaceListQuery(memberId ?? 0);
|
||||
const createWorkspace = useCreateWorkspaceQuery();
|
||||
|
@ -45,7 +45,10 @@ export default function WorkspaceDropdownMenu({
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Menu size={20} />
|
||||
<Menu
|
||||
size={16}
|
||||
className="stroke-gray-900"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
<DropdownMenuItem
|
||||
@ -57,7 +60,7 @@ export default function WorkspaceDropdownMenu({
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleOpenUploadFile}>파일 업로드</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleOpenUploadFolder}>폴더 업로드</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleOpenUploadFolder}>폴더 업로드 (임시)</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleOpenUploadZip}>폴더 압축파일 업로드</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@ -83,7 +86,7 @@ export default function WorkspaceDropdownMenu({
|
||||
>
|
||||
<DialogTrigger asChild></DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader title="폴더 업로드" />
|
||||
<DialogHeader title="폴더 업로드 (임시)" />
|
||||
<ImageUploadFolderForm
|
||||
onClose={handleCloseUploadFolder}
|
||||
projectId={projectId}
|
||||
|
@ -1,46 +1,29 @@
|
||||
import LabelButton from './LabelButton';
|
||||
import { Button } from '../ui/button';
|
||||
import { Play } from 'lucide-react';
|
||||
import useCanvasStore from '@/stores/useCanvasStore';
|
||||
|
||||
export default function WorkspaceLabelBar() {
|
||||
const labels = useCanvasStore((state) => state.labels);
|
||||
const { labels, image } = useCanvasStore();
|
||||
const selectedLabelId = useCanvasStore((state) => state.selectedLabelId);
|
||||
const setSelectedLabelId = useCanvasStore((state) => state.setSelectedLabelId);
|
||||
const handleAutoLabeling = () => {
|
||||
console.log('Auto labeling');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-[280px] flex-col justify-between gap-2 border-l border-gray-300 bg-gray-50 p-3">
|
||||
<div className="flex h-full w-[200px] flex-col justify-between gap-2 border-l border-gray-300 bg-gray-50 p-3">
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<header className="subheading flex w-full items-center gap-2">
|
||||
<h1 className="w-full overflow-hidden text-ellipsis whitespace-nowrap">레이블 목록</h1>
|
||||
</header>
|
||||
<div className="flex flex-col gap-1">
|
||||
{labels.map((label) => (
|
||||
<LabelButton
|
||||
key={label.id}
|
||||
{...label}
|
||||
selected={selectedLabelId === label.id}
|
||||
setSelectedLabelId={setSelectedLabelId}
|
||||
/>
|
||||
))}
|
||||
{image &&
|
||||
labels.map((label) => (
|
||||
<LabelButton
|
||||
key={label.id}
|
||||
{...label}
|
||||
selected={selectedLabelId === label.id}
|
||||
setSelectedLabelId={setSelectedLabelId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex p-2.5">
|
||||
<Button
|
||||
variant="outlinePrimary"
|
||||
className="w-full"
|
||||
onClick={handleAutoLabeling}
|
||||
>
|
||||
<Play
|
||||
size={16}
|
||||
className="mr-1"
|
||||
/>
|
||||
<span>자동 레이블링</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -7,22 +7,25 @@ import useCanvasStore from '@/stores/useCanvasStore';
|
||||
import { Button } from '../ui/button';
|
||||
import { useEffect } from 'react';
|
||||
import WorkspaceDropdownMenu from '../WorkspaceDropdownMenu';
|
||||
import useAutoLabelQuery from '@/queries/projects/useAutoLabelQuery';
|
||||
import useProjectStore from '@/stores/useProjectStore';
|
||||
|
||||
export default function ProjectStructure({ project }: { project: Project }) {
|
||||
const setProject = useCanvasStore((state) => state.setProject);
|
||||
const setProject = useProjectStore((state) => state.setProject);
|
||||
const image = useCanvasStore((state) => state.image);
|
||||
const { data: folderData, refetch } = useFolderQuery(project.id.toString(), 0);
|
||||
const requestAutoLabel = useAutoLabelQuery();
|
||||
|
||||
useEffect(() => {
|
||||
setProject(project);
|
||||
}, [project, setProject]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col justify-between">
|
||||
<div className="flex flex-col overflow-y-auto px-1 pb-2">
|
||||
<div className="flex h-full min-h-0 grow-0 flex-col">
|
||||
<div className="flex h-full flex-col overflow-hidden px-1 pb-2">
|
||||
<header className="flex w-full items-center gap-2 rounded p-1">
|
||||
<div className="flex w-full items-center gap-1 overflow-hidden pr-1">
|
||||
<h2 className="caption overflow-hidden text-ellipsis whitespace-nowrap">{project.type}</h2>
|
||||
<div className="flex w-full min-w-0 items-center gap-1 pr-1">
|
||||
<h2 className="caption overflow-hidden text-ellipsis whitespace-nowrap text-gray-500">{project.type}</h2>
|
||||
</div>
|
||||
<WorkspaceDropdownMenu
|
||||
projectId={project.id}
|
||||
@ -35,7 +38,7 @@ export default function ProjectStructure({ project }: { project: Project }) {
|
||||
빈 프로젝트입니다.
|
||||
</div>
|
||||
) : (
|
||||
<div className="caption flex flex-col">
|
||||
<div className="caption flex flex-col overflow-y-auto">
|
||||
{folderData.children.map((item) => (
|
||||
<ProjectDirectoryItem
|
||||
key={`${project.id}-${item.title}`}
|
||||
@ -55,11 +58,21 @@ export default function ProjectStructure({ project }: { project: Project }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex p-2.5">
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="outlinePrimary"
|
||||
className="w-full"
|
||||
onClick={() => console.log('autolabel')}
|
||||
onClick={() => {
|
||||
requestAutoLabel.mutate(
|
||||
{ projectId: project.id },
|
||||
{
|
||||
onSuccess: refetch,
|
||||
onError: () => {
|
||||
alert('자동 레이블링을 요청하는 중 오류가 발생했습니다.');
|
||||
},
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Play
|
||||
size={16}
|
||||
|
@ -5,24 +5,31 @@ import { Project } from '@/types';
|
||||
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '../ui/select';
|
||||
import useCanvasStore from '@/stores/useCanvasStore';
|
||||
import { webPath } from '@/router';
|
||||
import { Suspense } from 'react';
|
||||
import { Suspense, useEffect } from 'react';
|
||||
|
||||
export default function WorkspaceSidebar({ workspaceName, projects }: { workspaceName: string; projects: Project[] }) {
|
||||
const { projectId: selectedProjectId } = useParams<{ projectId: string }>();
|
||||
const { setImage } = useCanvasStore();
|
||||
const { projectId: selectedProjectId, workspaceId } = useParams<{ projectId: string; workspaceId: string }>();
|
||||
const selectedProject = projects.find((project) => project.id.toString() === selectedProjectId) || null;
|
||||
const setSidebarSize = useCanvasStore((state) => state.setSidebarSize);
|
||||
const navigate = useNavigate();
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||
const handleSelectProject = (projectId: string) => {
|
||||
setImage(null);
|
||||
navigate(`${webPath.workspace()}/${workspaceId}/${projectId}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedProject) {
|
||||
setImage(null);
|
||||
}
|
||||
}, [selectedProject, setImage]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResizablePanel
|
||||
minSize={10}
|
||||
maxSize={35}
|
||||
defaultSize={20}
|
||||
defaultSize={15}
|
||||
className="flex h-full flex-col gap-2 bg-gray-50 p-3"
|
||||
onResize={(size) => setSidebarSize(size)}
|
||||
>
|
||||
|
@ -4,11 +4,11 @@ import useProfileQuery from '@/queries/auth/useProfileQuery';
|
||||
export default function useHandleOAuthCallback() {
|
||||
const queryParams = new URLSearchParams(window.location.search);
|
||||
const accessToken = queryParams.get('accessToken');
|
||||
const setLoggedIn = useAuthStore((state) => state.setLoggedIn);
|
||||
const setToken = useAuthStore((state) => state.setToken);
|
||||
const setProfile = useAuthStore((state) => state.setProfile);
|
||||
|
||||
if (accessToken) {
|
||||
setLoggedIn(true, accessToken);
|
||||
setToken(accessToken);
|
||||
}
|
||||
|
||||
const { data: profile } = useProfileQuery();
|
||||
|
@ -1,49 +0,0 @@
|
||||
// 임시 가짜 훅
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import axios from 'axios';
|
||||
import useTrainStore from '@/stores/useTrainStore';
|
||||
|
||||
export default function useTrainPolling(start: boolean, projectId?: string | null) {
|
||||
const { addTrainingData, resetTrainingData } = useTrainStore((state) => ({
|
||||
addTrainingData: state.addTrainingData,
|
||||
resetTrainingData: state.resetTrainingData,
|
||||
}));
|
||||
|
||||
const intervalIdRef = useRef<number | null>(null);
|
||||
// 함수 api 후 교체 예정
|
||||
const fetchTrainingData = useCallback(async () => {
|
||||
if (projectId) {
|
||||
try {
|
||||
const response = await axios.get(`/api/바보=${projectId}`);
|
||||
const data = response.data;
|
||||
|
||||
addTrainingData(projectId, {
|
||||
epoch: data.epoch,
|
||||
total_epochs: data.total_epochs,
|
||||
box_loss: data.box_loss,
|
||||
cls_loss: data.cls_loss,
|
||||
dfl_loss: data.dfl_loss,
|
||||
fitness: data.fitness,
|
||||
epoch_time: data.epoch_time,
|
||||
left_second: data.left_second,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fetching error:', error);
|
||||
}
|
||||
}
|
||||
}, [projectId, addTrainingData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (start && projectId) {
|
||||
resetTrainingData(projectId);
|
||||
intervalIdRef.current = window.setInterval(fetchTrainingData, 5000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalIdRef.current) {
|
||||
clearInterval(intervalIdRef.current);
|
||||
intervalIdRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [start, projectId, fetchTrainingData, resetTrainingData]);
|
||||
}
|
19
frontend/src/mocks/authHandlers.ts
Normal file
19
frontend/src/mocks/authHandlers.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { RefreshTokenResponse, MemberResponse } from '@/types';
|
||||
|
||||
export const authHandlers = [
|
||||
http.post('/api/auth/reissue', () => {
|
||||
const response: RefreshTokenResponse = { accessToken: 'newAccessToken' };
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
http.get('/api/auth/profile', () => {
|
||||
const response: MemberResponse = {
|
||||
id: 1,
|
||||
nickname: 'javajoha',
|
||||
profileImage: 'profile.jpg',
|
||||
email: 'j9@naver.com',
|
||||
};
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
];
|
77
frontend/src/mocks/categoryHandlers.ts
Normal file
77
frontend/src/mocks/categoryHandlers.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { LabelCategoryRequest, LabelCategoryResponse } from '@/types';
|
||||
|
||||
export const categoryHandlers = [
|
||||
// 레이블 카테고리 리스트 조회 핸들러
|
||||
http.get('/api/projects/:projectId/categories', ({ params }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
console.log(projectId);
|
||||
const categories: LabelCategoryResponse[] = [
|
||||
{ id: 1, name: 'Category 1' },
|
||||
{ id: 2, name: 'Category 2' },
|
||||
];
|
||||
|
||||
return HttpResponse.json(categories);
|
||||
}),
|
||||
|
||||
// 레이블 카테고리 추가 핸들러
|
||||
http.post('/api/projects/:projectId/categories', async ({ params, request }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
|
||||
const categoryData = (await request.json()) as LabelCategoryRequest;
|
||||
console.log(categoryData);
|
||||
|
||||
const newCategory: LabelCategoryResponse = {
|
||||
id: Math.floor(Math.random() * 1000), // 임의로 ID 생성
|
||||
name: `New Category for project ${projectId}`,
|
||||
};
|
||||
|
||||
return HttpResponse.json(newCategory);
|
||||
}),
|
||||
|
||||
// 레이블 카테고리 단일 조회 핸들러
|
||||
http.get('/api/projects/:projectId/categories/:categoryId', ({ params }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
const categoryId = Array.isArray(params.categoryId)
|
||||
? parseInt(params.categoryId[0], 10)
|
||||
: parseInt(params.categoryId as string, 10);
|
||||
console.log(projectId);
|
||||
const category: LabelCategoryResponse = {
|
||||
id: categoryId,
|
||||
name: `Category ${categoryId}`,
|
||||
};
|
||||
|
||||
return HttpResponse.json(category);
|
||||
}),
|
||||
|
||||
// 레이블 카테고리 삭제 핸들러
|
||||
http.delete('/api/projects/:projectId/categories/:categoryId', ({ params }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
const categoryId = Array.isArray(params.categoryId)
|
||||
? parseInt(params.categoryId[0], 10)
|
||||
: parseInt(params.categoryId as string, 10);
|
||||
|
||||
return HttpResponse.json({ message: `Category ${categoryId} deleted from project ${projectId}` });
|
||||
}),
|
||||
|
||||
// 레이블 카테고리 존재 여부 조회 핸들러
|
||||
http.get('/api/projects/:projectId/categories/exist', ({ params }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
console.log(projectId);
|
||||
const categoryName = Array.isArray(params.categoryName) ? params.categoryName[0] : params.categoryName;
|
||||
|
||||
const exists = categoryName === 'Category 1'; // 임의로 'Category 1'만 존재하는 것으로 설정
|
||||
|
||||
return HttpResponse.json(exists);
|
||||
}),
|
||||
];
|
115
frontend/src/mocks/folderHandler.ts
Normal file
115
frontend/src/mocks/folderHandler.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { FolderResponse, FolderRequest } from '@/types';
|
||||
|
||||
export const folderHandlers = [
|
||||
// 특정 폴더 조회 핸들러
|
||||
http.get('/api/projects/:projectId/folders/:folderId', ({ params }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10); // string으로 캐스팅
|
||||
const folderId = Array.isArray(params.folderId)
|
||||
? parseInt(params.folderId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
console.log(projectId);
|
||||
|
||||
const folderResponse: FolderResponse = {
|
||||
id: folderId,
|
||||
title: `Folder ${folderId}`,
|
||||
images: [
|
||||
{
|
||||
id: 1,
|
||||
imageTitle: 'image1.jpg',
|
||||
imagePath: 'https://example.com/image1.jpg',
|
||||
dataPath: 'https://example.com/data1.json',
|
||||
status: 'PENDING',
|
||||
},
|
||||
],
|
||||
children: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Subfolder 1',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return HttpResponse.json(folderResponse);
|
||||
}),
|
||||
|
||||
// 폴더 생성 핸들러
|
||||
http.post('/api/projects/:projectId/folders', async ({ params, request }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10); // string으로 캐스팅
|
||||
|
||||
const folderData = (await request.json()) as FolderRequest;
|
||||
|
||||
const createdFolder: FolderResponse = {
|
||||
id: Math.floor(Math.random() * 1000), // 임의로 폴더 ID 생성
|
||||
title: folderData.title,
|
||||
images: [],
|
||||
children: [],
|
||||
};
|
||||
console.log(projectId);
|
||||
|
||||
return HttpResponse.json(createdFolder);
|
||||
}),
|
||||
|
||||
// 폴더 수정 핸들러
|
||||
http.put('/api/projects/:projectId/folders/:folderId', async ({ params, request }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10); // string으로 캐스팅
|
||||
const folderId = Array.isArray(params.folderId)
|
||||
? parseInt(params.folderId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
console.log(projectId);
|
||||
const folderData = (await request.json()) as FolderRequest;
|
||||
|
||||
const updatedFolder: FolderResponse = {
|
||||
id: folderId,
|
||||
title: folderData.title,
|
||||
images: [],
|
||||
children: [],
|
||||
};
|
||||
|
||||
return HttpResponse.json(updatedFolder);
|
||||
}),
|
||||
|
||||
// 폴더 삭제 핸들러
|
||||
http.delete('/api/projects/:projectId/folders/:folderId', ({ params }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10); // string으로 캐스팅
|
||||
const folderId = Array.isArray(params.folderId)
|
||||
? parseInt(params.folderId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
return HttpResponse.json({ message: `Folder ${folderId} deleted from project ${projectId}` });
|
||||
}),
|
||||
|
||||
// 폴더 리뷰 목록 조회 핸들러
|
||||
http.get('/api/projects/:projectId/folders/:folderId/review', ({ params }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
const folderId = Array.isArray(params.folderId)
|
||||
? parseInt(params.folderId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
console.log(projectId, folderId);
|
||||
const reviews = [
|
||||
{
|
||||
reviewId: 1,
|
||||
title: 'First Review',
|
||||
content: 'Review for the first folder',
|
||||
status: 'APPROVED',
|
||||
},
|
||||
{
|
||||
reviewId: 2,
|
||||
title: 'Second Review',
|
||||
content: 'Review for the second folder',
|
||||
status: 'REQUESTED',
|
||||
},
|
||||
];
|
||||
|
||||
return HttpResponse.json(reviews);
|
||||
}),
|
||||
];
|
@ -1,331 +1,24 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import {
|
||||
ProjectResponse,
|
||||
FolderResponse,
|
||||
ImageResponse,
|
||||
WorkspaceResponse,
|
||||
MemberResponse,
|
||||
RefreshTokenResponse,
|
||||
AutoLabelingResponse,
|
||||
ErrorResponse,
|
||||
} from '@/types';
|
||||
import { authHandlers } from './authHandlers';
|
||||
import { labelHandlers } from './labelHandlers';
|
||||
import { reviewHandlers } from './reviewHandlers';
|
||||
import { categoryHandlers } from './categoryHandlers';
|
||||
import { memberHandlers } from './memberHandlers';
|
||||
import { workspaceHandlers } from './workspaceHandlers';
|
||||
import { folderHandlers } from './folderHandler';
|
||||
import { modelHandlers } from './modelHandlers';
|
||||
import { imageHandlers } from './imageHandlers';
|
||||
import { projectHandlers } from './projectHandlers';
|
||||
|
||||
// 모든 핸들러를 배열로 통합
|
||||
export const handlers = [
|
||||
// Auth Handlers
|
||||
http.post('/api/auth/reissue', () => {
|
||||
// 토큰 재발급 핸들러
|
||||
const response: RefreshTokenResponse = {
|
||||
accessToken: 'newAccessToken',
|
||||
};
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
http.get('/api/auth/profile', () => {
|
||||
// 사용자 프로필 핸들러
|
||||
const response: MemberResponse = {
|
||||
id: 1,
|
||||
nickname: 'javajoha',
|
||||
profileImage: 'profile.jpg',
|
||||
email: 'j9@naver.com',
|
||||
};
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
// Workspace Handlers
|
||||
http.get('/api/workspaces/:workspaceId', ({ params }) => {
|
||||
// 워크스페이스 조회 핸들러
|
||||
const { workspaceId } = params;
|
||||
const response: WorkspaceResponse = {
|
||||
id: parseInt(workspaceId as string, 10),
|
||||
memberId: 'abc1324',
|
||||
title: 'workspace1',
|
||||
content: '갤럭시 s24 불량 검증',
|
||||
createdAt: '2024-09-18T05:04:44.668Z',
|
||||
updatedAt: '2024-09-18T05:04:44.668Z',
|
||||
};
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
http.put('/api/workspaces/:workspaceId', ({ params }) => {
|
||||
// 워크스페이스 수정 핸들러
|
||||
const { workspaceId } = params;
|
||||
const response: WorkspaceResponse = {
|
||||
id: parseInt(workspaceId as string, 10),
|
||||
memberId: 'abc1324',
|
||||
title: 'Updated Workspace Title',
|
||||
content: 'Updated Workspace Content',
|
||||
createdAt: '2024-09-18T05:04:44.668Z',
|
||||
updatedAt: '2024-09-18T06:00:00.668Z',
|
||||
};
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
http.delete('/api/workspaces/:workspaceId', ({ params }) => {
|
||||
const { workspaceId } = params;
|
||||
console.log(workspaceId);
|
||||
return HttpResponse.json({});
|
||||
}),
|
||||
|
||||
http.get('/api/workspaces', () => {
|
||||
// 워크스페이스 목록 조회 핸들러
|
||||
const response: WorkspaceResponse[] = [
|
||||
{
|
||||
id: 1,
|
||||
memberId: 'abc1324',
|
||||
title: 'Workspace 1',
|
||||
content: 'Content 1',
|
||||
createdAt: '2024-09-18T05:04:44.668Z',
|
||||
updatedAt: '2024-09-18T05:04:44.668Z',
|
||||
},
|
||||
];
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
// Project Handlers
|
||||
http.get('/api/workspaces/:workspaceId/projects', ({ request, params }) => {
|
||||
const workspaceId = parseInt(params.workspaceId as string, 10);
|
||||
|
||||
const url = new URL(request.url);
|
||||
const lastProjectId = parseInt(url.searchParams.get('lastProjectId') || '0', 10);
|
||||
const limit = parseInt(url.searchParams.get('limit') || '10', 10);
|
||||
|
||||
const projects: ProjectResponse[] = Array.from({ length: limit }, (_, index) => ({
|
||||
id: lastProjectId + index + 1,
|
||||
title: `프로젝트 ${lastProjectId + index + 1}`,
|
||||
workspaceId,
|
||||
projectType: ['classification', 'detection', 'segmentation'][index % 3] as
|
||||
| 'classification'
|
||||
| 'detection'
|
||||
| 'segmentation',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
thumbnail: `thumbnail_${lastProjectId + index + 1}.jpg`,
|
||||
}));
|
||||
|
||||
// 응답 생성
|
||||
const response: ProjectResponse[] = projects;
|
||||
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
http.get('/api/projects/:projectId', ({ params }) => {
|
||||
// 프로젝트 조회 핸들러
|
||||
const { projectId } = params;
|
||||
const response: ProjectResponse = {
|
||||
id: parseInt(projectId as string, 10),
|
||||
title: 'Project Title',
|
||||
workspaceId: 1,
|
||||
projectType: 'classification',
|
||||
createdAt: '2024-09-18T05:04:44.668Z',
|
||||
updatedAt: '2024-09-18T05:04:44.668Z',
|
||||
};
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
http.post('/api/workspaces/:workspaceId/projects', () => {
|
||||
// 프로젝트 생성 핸들러
|
||||
const response: ProjectResponse = {
|
||||
id: 3,
|
||||
title: 'New Project',
|
||||
workspaceId: 1,
|
||||
projectType: 'detection',
|
||||
createdAt: '2024-09-18T05:04:44.668Z',
|
||||
updatedAt: '2024-09-18T05:04:44.668Z',
|
||||
};
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
http.put('/api/projects/:projectId', ({ params }) => {
|
||||
// 프로젝트 수정 핸들러
|
||||
const { projectId } = params;
|
||||
const response: ProjectResponse = {
|
||||
id: parseInt(projectId as string, 10),
|
||||
title: 'Updated Project Title',
|
||||
workspaceId: 1,
|
||||
projectType: 'segmentation',
|
||||
createdAt: '2024-09-18T05:04:44.668Z',
|
||||
updatedAt: '2024-09-18T06:00:00.668Z',
|
||||
};
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
http.delete('/api/projects/:projectId', ({ params }) => {
|
||||
const { projectId } = params;
|
||||
console.log(projectId);
|
||||
return HttpResponse.json({});
|
||||
}),
|
||||
|
||||
http.post('/api/projects/:projectId/label/auto', () => {
|
||||
const response: AutoLabelingResponse = {
|
||||
imageId: 1,
|
||||
imageUrl: 'image-url.jpg',
|
||||
data: `{
|
||||
"version": "0.1.0",
|
||||
"task_type": "cls",
|
||||
"shapes": [
|
||||
{
|
||||
"label": "NG",
|
||||
"color": "#FF0000",
|
||||
"points": [[0, 0]],
|
||||
"group_id": null,
|
||||
"shape_type": "point",
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"split": "none",
|
||||
"imageHeight": 2000,
|
||||
"imageWidth": 4000,
|
||||
"imageDepth": 4
|
||||
}`,
|
||||
};
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
// DELETE: 프로젝트 멤버 제거 핸들러
|
||||
http.delete('/api/projects/:projectId/members', ({ params }) => {
|
||||
const { projectId } = params;
|
||||
|
||||
return HttpResponse.json({ message: `프로젝트 ${projectId}에서 멤버 제거 성공` });
|
||||
}),
|
||||
// PUT: 프로젝트 멤버 권한 수정 핸들러
|
||||
http.put('/api/projects/:projectId/members', () => {
|
||||
return HttpResponse.json({});
|
||||
}),
|
||||
// POST: 워크스페이스 멤버 추가 핸들러
|
||||
http.post('/api/workspaces/:workspaceId/members/:memberId', ({ params }) => {
|
||||
const { workspaceId, memberId } = params;
|
||||
|
||||
if (!workspaceId || !memberId) {
|
||||
const errorResponse: ErrorResponse = {
|
||||
status: 400,
|
||||
code: 1002,
|
||||
message: '잘못된 요청입니다. 요청을 확인해주세요.',
|
||||
isSuccess: false,
|
||||
};
|
||||
return HttpResponse.json(errorResponse, { status: 400 });
|
||||
}
|
||||
|
||||
// 성공 응답
|
||||
const response: WorkspaceResponse = {
|
||||
id: parseInt(workspaceId as string, 10),
|
||||
memberId: 'abc1324',
|
||||
title: 'Workspace 1',
|
||||
content: 'Workspace for testing',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return HttpResponse.json(response, { status: 200 });
|
||||
}),
|
||||
|
||||
// GET: 프로젝트 멤버 리스트 조회 핸들러 (가상)
|
||||
// 실제 구현 시 API 경로와 메서드를 확인 후 업데이트 필요
|
||||
http.get('/api/projects/:projectId/members', () => {
|
||||
const members: MemberResponse[] = [
|
||||
{ id: 1, nickname: 'admin', profileImage: 'admin.jpg', email: 'j9@naver.com' },
|
||||
{ id: 2, nickname: 'editor', profileImage: 'editor.jpg', email: 'j9@naver.com' },
|
||||
{ id: 3, nickname: 'viewer', profileImage: 'viewer.jpg', email: 'j9@naver.com' },
|
||||
];
|
||||
|
||||
return HttpResponse.json(members);
|
||||
}),
|
||||
|
||||
// GET: 워크스페이스 멤버 리스트 조회 핸들러 (가상)
|
||||
// 실제 구현 시 API 경로와 메서드를 확인 후 업데이트 필요
|
||||
http.get('/api/workspaces/:workspaceId/members', () => {
|
||||
const members: MemberResponse[] = [
|
||||
{ id: 1, nickname: 'admin', profileImage: 'admin.jpg', email: 'j9@naver.com' },
|
||||
{ id: 2, nickname: 'editor', profileImage: 'editor.jpg', email: 'j9@naver.com' },
|
||||
{ id: 3, nickname: 'viewer', profileImage: 'viewer.jpg', email: 'j9@naver.com' },
|
||||
];
|
||||
|
||||
return HttpResponse.json(members);
|
||||
}),
|
||||
|
||||
// Folder and Image Handlers
|
||||
http.get('/api/projects/:projectId/folders/:folderId', ({ params }) => {
|
||||
const { folderId } = params;
|
||||
const response: FolderResponse = {
|
||||
id: parseInt(folderId as string, 10),
|
||||
title: 'My Folder',
|
||||
images: [
|
||||
{
|
||||
id: 1,
|
||||
imageTitle: 'image.jpg',
|
||||
imagePath: 'https://example.com/image.jpg',
|
||||
dataPath: 'https://example.com/image.json',
|
||||
status: 'PENDING',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
imageTitle: 'another_image.jpg',
|
||||
imagePath: 'https://example.com/another_image.jpg',
|
||||
dataPath: 'https://example.com/another_image.json',
|
||||
status: 'IN_PROGRESS',
|
||||
},
|
||||
],
|
||||
children: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Car',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Bike',
|
||||
},
|
||||
],
|
||||
};
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
http.get('/api/projects/:projectId/folders/:folderId/images/:imageId', ({ params }) => {
|
||||
// 이미지 조회 핸들러
|
||||
const { imageId } = params;
|
||||
const response: ImageResponse = {
|
||||
id: parseInt(imageId as string, 10),
|
||||
imageTitle: 'Image Title',
|
||||
imagePath: 'image-url.jpg',
|
||||
dataPath: 'data-url.json',
|
||||
status: 'PENDING',
|
||||
};
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
// Auto Labeling Handler
|
||||
http.post('/api/projects/:projectId/label/auto', () => {
|
||||
const response: AutoLabelingResponse = {
|
||||
imageId: 1,
|
||||
imageUrl: 'image-url.jpg',
|
||||
data: `{
|
||||
"version": "0.1.0",
|
||||
"task_type": "cls",
|
||||
"shapes": [
|
||||
{
|
||||
"label": "NG",
|
||||
"color": "#FF0000",
|
||||
"points": [[0, 0]],
|
||||
"group_id": null,
|
||||
"shape_type": "point",
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"split": "none",
|
||||
"imageHeight": 2000,
|
||||
"imageWidth": 4000,
|
||||
"imageDepth": 4
|
||||
}`,
|
||||
};
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
// Error Handler Example
|
||||
http.get('/api/error', () => {
|
||||
const errorResponse: ErrorResponse = {
|
||||
status: 400,
|
||||
code: 1003,
|
||||
message: '필수 요청 파라미터가 입력되지 않았습니다.',
|
||||
isSuccess: false,
|
||||
};
|
||||
return HttpResponse.json(errorResponse);
|
||||
}),
|
||||
...authHandlers,
|
||||
...labelHandlers,
|
||||
...reviewHandlers,
|
||||
...categoryHandlers,
|
||||
...memberHandlers,
|
||||
...workspaceHandlers,
|
||||
...folderHandlers,
|
||||
...modelHandlers,
|
||||
...imageHandlers,
|
||||
...projectHandlers,
|
||||
];
|
||||
|
122
frontend/src/mocks/imageHandlers.ts
Normal file
122
frontend/src/mocks/imageHandlers.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { ImageMoveRequest, ImageStatusChangeRequest } from '@/types';
|
||||
|
||||
export const imageHandlers = [
|
||||
// 이미지 조회 핸들러
|
||||
http.get('/api/images/:imageId', ({ params }) => {
|
||||
const imageId = Array.isArray(params.imageId)
|
||||
? parseInt(params.imageId[0], 10)
|
||||
: parseInt(params.imageId as string, 10);
|
||||
const memberId = Array.isArray(params.memberId)
|
||||
? parseInt(params.memberId[0], 10)
|
||||
: parseInt(params.memberId as string, 10);
|
||||
|
||||
const imageResponse = {
|
||||
id: imageId,
|
||||
title: `Image ${imageId}`,
|
||||
url: `https://example.com/images/${imageId}`,
|
||||
memberId,
|
||||
};
|
||||
|
||||
return HttpResponse.json(imageResponse);
|
||||
}),
|
||||
|
||||
// 이미지 이동 핸들러
|
||||
http.put('/api/images/:imageId', async ({ params, request }) => {
|
||||
const imageId = Array.isArray(params.imageId)
|
||||
? parseInt(params.imageId[0], 10)
|
||||
: parseInt(params.imageId as string, 10);
|
||||
const memberId = Array.isArray(params.memberId)
|
||||
? parseInt(params.memberId[0], 10)
|
||||
: parseInt(params.memberId as string, 10);
|
||||
|
||||
const moveRequest = (await request.json()) as ImageMoveRequest;
|
||||
|
||||
return HttpResponse.json({
|
||||
message: `Image ${imageId} moved to folder ${moveRequest.moveFolderId} by member ${memberId}`,
|
||||
});
|
||||
}),
|
||||
|
||||
// 이미지 삭제 핸들러
|
||||
http.delete('/api/images/:imageId', ({ params }) => {
|
||||
const imageId = Array.isArray(params.imageId)
|
||||
? parseInt(params.imageId[0], 10)
|
||||
: parseInt(params.imageId as string, 10);
|
||||
const memberId = Array.isArray(params.memberId)
|
||||
? parseInt(params.memberId[0], 10)
|
||||
: parseInt(params.memberId as string, 10);
|
||||
|
||||
return HttpResponse.json({ message: `Image ${imageId} deleted by member ${memberId}` });
|
||||
}),
|
||||
|
||||
// 이미지 상태 변경 핸들러
|
||||
http.put('/api/images/:imageId/status', async ({ params, request }) => {
|
||||
const imageId = Array.isArray(params.imageId)
|
||||
? parseInt(params.imageId[0], 10)
|
||||
: parseInt(params.imageId as string, 10);
|
||||
const memberId = Array.isArray(params.memberId)
|
||||
? parseInt(params.memberId[0], 10)
|
||||
: parseInt(params.memberId as string, 10);
|
||||
|
||||
const statusChangeRequest = (await request.json()) as ImageStatusChangeRequest;
|
||||
|
||||
return HttpResponse.json({
|
||||
message: `Image ${imageId} status changed to ${statusChangeRequest.labelStatus} by member ${memberId}`,
|
||||
});
|
||||
}),
|
||||
|
||||
// 이미지 파일 업로드 핸들러
|
||||
http.post('/api/projects/:projectId/folders/:folderId/images/file', async ({ params, request }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
const folderId = Array.isArray(params.folderId)
|
||||
? parseInt(params.folderId[0], 10)
|
||||
: parseInt(params.folderId as string, 10);
|
||||
const memberId = Array.isArray(params.memberId)
|
||||
? parseInt(params.memberId[0], 10)
|
||||
: parseInt(params.memberId as string, 10);
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
const files = formData.getAll('imageList') as File[];
|
||||
|
||||
return HttpResponse.json({
|
||||
message: `Uploaded ${files.length} images to folder ${folderId} in project ${projectId} by member ${memberId}`,
|
||||
});
|
||||
}),
|
||||
|
||||
// 이미지 폴더 업로드 핸들러
|
||||
http.post('/api/projects/:projectId/folders/:folderId/images/file', async ({ params, request }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
const memberId = Array.isArray(params.memberId)
|
||||
? parseInt(params.memberId[0], 10)
|
||||
: parseInt(params.memberId as string, 10);
|
||||
|
||||
const formData = await request.formData();
|
||||
const files = formData.getAll('imageList') as File[];
|
||||
|
||||
return HttpResponse.json({
|
||||
message: `Uploaded ${files.length} images to folder 0 in project ${projectId} by member ${memberId}`,
|
||||
});
|
||||
}),
|
||||
|
||||
// 이미지 ZIP 파일 업로드 핸들러
|
||||
http.post('/api/projects/:projectId/folders/:folderId/images/zip', async ({ params, request }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
const memberId = Array.isArray(params.memberId)
|
||||
? parseInt(params.memberId[0], 10)
|
||||
: parseInt(params.memberId as string, 10);
|
||||
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('folderZip') as File;
|
||||
|
||||
return HttpResponse.json({
|
||||
message: `Uploaded zip file "${file.name}" to project ${projectId} by member ${memberId}`,
|
||||
});
|
||||
}),
|
||||
];
|
36
frontend/src/mocks/labelHandlers.ts
Normal file
36
frontend/src/mocks/labelHandlers.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
export const labelHandlers = [
|
||||
// 이미지 레이블 저장 핸들러
|
||||
http.post('/api/projects/:projectId/images/:imageId/label', async ({ params, request }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
const imageId = Array.isArray(params.imageId)
|
||||
? parseInt(params.imageId[0], 10)
|
||||
: parseInt(params.imageId as string, 10);
|
||||
|
||||
const labelData = (await request.json()) as { data: string };
|
||||
|
||||
return HttpResponse.json({
|
||||
message: `Label saved for image ${imageId} in project ${projectId}`,
|
||||
labelData: labelData.data,
|
||||
});
|
||||
}),
|
||||
|
||||
// 자동 레이블링 실행 핸들러
|
||||
http.post('/api/projects/:projectId/label/auto', ({ params }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
const memberId = Array.isArray(params.memberId)
|
||||
? parseInt(params.memberId[0], 10)
|
||||
: parseInt(params.memberId as string, 10);
|
||||
|
||||
// 여기에서는 예를 들어 자동 레이블링 작업이 성공적으로 완료된 상황을 가정합니다.
|
||||
return HttpResponse.json({
|
||||
message: `Auto-labeling started for project ${projectId} by member ${memberId}`,
|
||||
status: 'IN_PROGRESS',
|
||||
});
|
||||
}),
|
||||
];
|
18
frontend/src/mocks/memberHandlers.ts
Normal file
18
frontend/src/mocks/memberHandlers.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { MemberResponse } from '@/types';
|
||||
|
||||
export const memberHandlers = [
|
||||
http.get('/api/members', ({ params }) => {
|
||||
const keyword = Array.isArray(params.keyword) ? params.keyword[0] : params.keyword;
|
||||
|
||||
const members: MemberResponse[] = [
|
||||
{ id: 1, nickname: 'john_doe', profileImage: 'john.jpg', email: 'john@example.com' },
|
||||
{ id: 2, nickname: 'jane_doe', profileImage: 'jane.jpg', email: 'jane@example.com' },
|
||||
{ id: 3, nickname: 'sam_smith', profileImage: 'sam.jpg', email: 'sam@example.com' },
|
||||
];
|
||||
|
||||
const filteredMembers = members.filter((member) => member.email.includes(keyword || ''));
|
||||
|
||||
return HttpResponse.json(filteredMembers);
|
||||
}),
|
||||
];
|
201
frontend/src/mocks/modelHandlers.ts
Normal file
201
frontend/src/mocks/modelHandlers.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import {
|
||||
ModelRequest,
|
||||
ModelResponse,
|
||||
ProjectModelsResponse,
|
||||
ModelCategoryResponse,
|
||||
ModelTrainRequest,
|
||||
ResultResponse,
|
||||
ReportResponse,
|
||||
} from '@/types';
|
||||
|
||||
export const modelHandlers = [
|
||||
// 모델 이름 업데이트 핸들러
|
||||
http.put('/api/projects/:projectId/models/:modelId', async ({ params, request }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
const modelId = Array.isArray(params.modelId)
|
||||
? parseInt(params.modelId[0], 10)
|
||||
: parseInt(params.modelId as string, 10);
|
||||
console.log(projectId);
|
||||
const modelData = (await request.json()) as ModelRequest;
|
||||
|
||||
const updatedModel: ModelResponse = {
|
||||
id: modelId,
|
||||
name: modelData.name,
|
||||
};
|
||||
|
||||
return HttpResponse.json(updatedModel);
|
||||
}),
|
||||
|
||||
// 모델 학습 핸들러
|
||||
http.post('/api/projects/:projectId/train', async ({ params, request }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
|
||||
const trainData = (await request.json()) as ModelTrainRequest;
|
||||
|
||||
return HttpResponse.json({
|
||||
message: `Model training started for project ${projectId}`,
|
||||
trainData,
|
||||
});
|
||||
}),
|
||||
|
||||
// 프로젝트의 모델 리스트 조회 핸들러
|
||||
http.get('/api/projects/:projectId/models', ({ params }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
console.log(projectId);
|
||||
|
||||
const models: ProjectModelsResponse = [
|
||||
{ id: 1, name: 'Model 1' },
|
||||
{ id: 2, name: 'Model 2' },
|
||||
];
|
||||
|
||||
return HttpResponse.json(models);
|
||||
}),
|
||||
|
||||
// 모델 추가 핸들러
|
||||
http.post('/api/projects/:projectId/models', async ({ params, request }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
|
||||
const modelData = (await request.json()) as ModelRequest;
|
||||
console.log(projectId);
|
||||
|
||||
const newModel: ModelResponse = {
|
||||
id: Math.floor(Math.random() * 1000), // 임의로 ID 생성
|
||||
name: modelData.name,
|
||||
};
|
||||
|
||||
return HttpResponse.json(newModel);
|
||||
}),
|
||||
|
||||
// 모델 카테고리 조회 핸들러
|
||||
http.get('/api/models/:modelId/categories', ({ params }) => {
|
||||
const modelId = Array.isArray(params.modelId)
|
||||
? parseInt(params.modelId[0], 10)
|
||||
: parseInt(params.modelId as string, 10);
|
||||
console.log(modelId);
|
||||
const categories: ModelCategoryResponse[] = [
|
||||
{ id: 1, name: 'Category 1' },
|
||||
{ id: 2, name: 'Category 2' },
|
||||
];
|
||||
|
||||
return HttpResponse.json(categories);
|
||||
}),
|
||||
|
||||
// 모델 결과 조회 핸들러
|
||||
http.get('/api/results/model/:modelId', ({ params }) => {
|
||||
const modelId = Array.isArray(params.modelId)
|
||||
? parseInt(params.modelId[0], 10)
|
||||
: parseInt(params.modelId as string, 10);
|
||||
console.log(modelId);
|
||||
const results: ResultResponse[] = [
|
||||
{
|
||||
id: 1,
|
||||
precision: 0.85,
|
||||
recall: 0.8,
|
||||
fitness: 0.9,
|
||||
ratio: 0.75,
|
||||
epochs: 50,
|
||||
batch: 32,
|
||||
lr0: 0.001,
|
||||
lrf: 0.0001,
|
||||
optimizer: 'ADAM',
|
||||
map50: 0.92,
|
||||
map5095: 0.88,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
precision: 0.87,
|
||||
recall: 0.82,
|
||||
fitness: 0.91,
|
||||
ratio: 0.77,
|
||||
epochs: 40,
|
||||
batch: 16,
|
||||
lr0: 0.001,
|
||||
lrf: 0.00005,
|
||||
optimizer: 'SGD',
|
||||
map50: 0.93,
|
||||
map5095: 0.89,
|
||||
},
|
||||
];
|
||||
|
||||
return HttpResponse.json(results);
|
||||
}),
|
||||
|
||||
// 모델 보고서 조회 핸들러
|
||||
http.get('/api/projects/:projectId/reports/model/:modelId', ({ params }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
const modelId = Array.isArray(params.modelId)
|
||||
? parseInt(params.modelId[0], 10)
|
||||
: parseInt(params.modelId as string, 10);
|
||||
console.log(projectId);
|
||||
const reports: ReportResponse[] = [
|
||||
{
|
||||
modelId: modelId,
|
||||
totalEpochs: 5,
|
||||
epoch: 1,
|
||||
boxLoss: 0.05,
|
||||
clsLoss: 0.04,
|
||||
dflLoss: 0.03,
|
||||
fitness: 0.88,
|
||||
epochTime: 110,
|
||||
leftSecond: 1000,
|
||||
},
|
||||
{
|
||||
modelId: modelId,
|
||||
totalEpochs: 5,
|
||||
epoch: 2,
|
||||
boxLoss: 0.04,
|
||||
clsLoss: 0.035,
|
||||
dflLoss: 0.025,
|
||||
fitness: 0.89,
|
||||
epochTime: 115,
|
||||
leftSecond: 900,
|
||||
},
|
||||
{
|
||||
modelId: modelId,
|
||||
totalEpochs: 5,
|
||||
epoch: 3,
|
||||
boxLoss: 0.03,
|
||||
clsLoss: 0.03,
|
||||
dflLoss: 0.02,
|
||||
fitness: 0.9,
|
||||
epochTime: 120,
|
||||
leftSecond: 800,
|
||||
},
|
||||
{
|
||||
modelId: modelId,
|
||||
totalEpochs: 5,
|
||||
epoch: 4,
|
||||
boxLoss: 0.025,
|
||||
clsLoss: 0.028,
|
||||
dflLoss: 0.018,
|
||||
fitness: 0.91,
|
||||
epochTime: 125,
|
||||
leftSecond: 700,
|
||||
},
|
||||
{
|
||||
modelId: modelId,
|
||||
totalEpochs: 5,
|
||||
epoch: 5,
|
||||
boxLoss: 0.02,
|
||||
clsLoss: 0.025,
|
||||
dflLoss: 0.015,
|
||||
fitness: 0.92,
|
||||
epochTime: 130,
|
||||
leftSecond: 600,
|
||||
},
|
||||
];
|
||||
|
||||
return HttpResponse.json(reports);
|
||||
}),
|
||||
];
|
205
frontend/src/mocks/projectHandlers.ts
Normal file
205
frontend/src/mocks/projectHandlers.ts
Normal file
@ -0,0 +1,205 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { ProjectMemberResponse, ProjectResponse } from '@/types';
|
||||
|
||||
export const projectHandlers = [
|
||||
// 프로젝트 목록 조회 핸들러
|
||||
http.get('/api/workspaces/:workspaceId/projects', ({ params, request }) => {
|
||||
const workspaceId = Array.isArray(params.workspaceId)
|
||||
? parseInt((params.workspaceId as string[])[0], 10)
|
||||
: parseInt(params.workspaceId as string, 10);
|
||||
|
||||
const url = new URL(request.url);
|
||||
const lastProjectId = parseInt(url.searchParams.get('lastProjectId') || '0', 10);
|
||||
const limit = parseInt(url.searchParams.get('limit') || '10', 10);
|
||||
|
||||
const projects: ProjectResponse[] = Array.from({ length: limit }, (_, index) => ({
|
||||
id: lastProjectId + index + 1,
|
||||
title: `프로젝트 ${lastProjectId + index + 1}`,
|
||||
workspaceId,
|
||||
projectType: ['classification', 'detection', 'segmentation'][index % 3] as
|
||||
| 'classification'
|
||||
| 'detection'
|
||||
| 'segmentation',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
thumbnail: `thumbnail_${lastProjectId + index + 1}.jpg`,
|
||||
}));
|
||||
|
||||
return HttpResponse.json(projects);
|
||||
}),
|
||||
|
||||
// 특정 프로젝트 조회 핸들러
|
||||
http.get('/api/projects/:projectId', ({ params }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt((params.projectId as string[])[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
|
||||
const response: ProjectResponse = {
|
||||
id: projectId,
|
||||
title: 'Project Title',
|
||||
workspaceId: 1,
|
||||
projectType: 'classification',
|
||||
createdAt: '2024-09-18T05:04:44.668Z',
|
||||
updatedAt: '2024-09-18T05:04:44.668Z',
|
||||
};
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
// 프로젝트 생성 핸들러
|
||||
http.post('/api/workspaces/:workspaceId/projects', async ({ params, request }) => {
|
||||
const workspaceId = Array.isArray(params.workspaceId)
|
||||
? parseInt((params.workspaceId as string[])[0], 10)
|
||||
: parseInt(params.workspaceId as string, 10);
|
||||
|
||||
// body의 타입을 명시적으로 정의
|
||||
const body = (await request.json()) as {
|
||||
title: string;
|
||||
projectType: 'classification' | 'detection' | 'segmentation';
|
||||
};
|
||||
const { title, projectType } = body;
|
||||
|
||||
const newProject: ProjectResponse = {
|
||||
id: Math.floor(Math.random() * 1000),
|
||||
title,
|
||||
workspaceId,
|
||||
projectType,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return HttpResponse.json(newProject);
|
||||
}),
|
||||
|
||||
// 프로젝트 수정 핸들러
|
||||
http.put('/api/projects/:projectId', async ({ params, request }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt((params.projectId as string[])[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
|
||||
// body의 타입을 명시적으로 정의
|
||||
const body = (await request.json()) as {
|
||||
title: string;
|
||||
projectType: 'classification' | 'detection' | 'segmentation';
|
||||
};
|
||||
const { title, projectType } = body;
|
||||
|
||||
const updatedProject: ProjectResponse = {
|
||||
id: projectId,
|
||||
title,
|
||||
workspaceId: 1,
|
||||
projectType,
|
||||
createdAt: '2024-09-18T05:04:44.668Z',
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return HttpResponse.json(updatedProject);
|
||||
}),
|
||||
|
||||
// 프로젝트 삭제 핸들러
|
||||
http.delete('/api/projects/:projectId', ({ params }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt((params.projectId as string[])[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
console.log(`Deleted project ${projectId}`);
|
||||
return HttpResponse.json({ message: `Project ${projectId} deleted successfully` });
|
||||
}),
|
||||
// 프로젝트 멤버 조회 핸들러
|
||||
http.get('/api/projects/:projectId/members', ({ params }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt((params.projectId as string[])[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
console.log(projectId);
|
||||
|
||||
const members: ProjectMemberResponse[] = [
|
||||
{
|
||||
memberId: 1,
|
||||
nickname: 'adminUser',
|
||||
profileImage: 'admin.jpg',
|
||||
privilegeType: 'ADMIN',
|
||||
},
|
||||
{
|
||||
memberId: 2,
|
||||
nickname: 'managerUser',
|
||||
profileImage: 'manager.jpg',
|
||||
privilegeType: 'MANAGER',
|
||||
},
|
||||
{
|
||||
memberId: 3,
|
||||
nickname: 'editorUser',
|
||||
profileImage: 'editor.jpg',
|
||||
privilegeType: 'EDITOR',
|
||||
},
|
||||
{
|
||||
memberId: 4,
|
||||
nickname: 'viewerUser',
|
||||
profileImage: 'viewer.jpg',
|
||||
privilegeType: 'VIEWER',
|
||||
},
|
||||
];
|
||||
|
||||
return HttpResponse.json(members);
|
||||
}),
|
||||
|
||||
// 프로젝트 멤버 추가 핸들러
|
||||
http.post('/api/projects/:projectId/members', async ({ params, request }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt((params.projectId as string[])[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
console.log(projectId);
|
||||
|
||||
const newMember = (await request.json()) as {
|
||||
nickname: string;
|
||||
profileImage: string;
|
||||
privilegeType: 'ADMIN' | 'MANAGER' | 'EDITOR' | 'VIEWER';
|
||||
};
|
||||
|
||||
const addedMember: ProjectMemberResponse = {
|
||||
memberId: Math.floor(Math.random() * 1000), // 임의의 ID 생성
|
||||
nickname: newMember.nickname,
|
||||
profileImage: newMember.profileImage,
|
||||
privilegeType: newMember.privilegeType,
|
||||
};
|
||||
|
||||
return HttpResponse.json(addedMember);
|
||||
}),
|
||||
|
||||
// 프로젝트 멤버 권한 수정 핸들러
|
||||
http.put('/api/projects/:projectId/members', async ({ params, request }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt((params.projectId as string[])[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
console.log(projectId);
|
||||
const privilegeData = (await request.json()) as {
|
||||
memberId: number;
|
||||
privilegeType: 'ADMIN' | 'MANAGER' | 'EDITOR' | 'VIEWER';
|
||||
};
|
||||
|
||||
const updatedMember: ProjectMemberResponse = {
|
||||
memberId: privilegeData.memberId,
|
||||
nickname: 'Updated User',
|
||||
profileImage: 'updated.jpg',
|
||||
privilegeType: privilegeData.privilegeType,
|
||||
};
|
||||
|
||||
return HttpResponse.json(updatedMember);
|
||||
}),
|
||||
|
||||
// 프로젝트 멤버 삭제 핸들러
|
||||
http.delete('/api/projects/:projectId/members', async ({ params, request }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt((params.projectId as string[])[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
|
||||
const { targetMemberId } = (await request.json()) as { targetMemberId: number };
|
||||
|
||||
return HttpResponse.json({ message: `Member ${targetMemberId} removed from project ${projectId}` });
|
||||
}),
|
||||
|
||||
// 프로젝트 삭제 핸들러
|
||||
http.delete('/api/projects/:projectId', ({ params }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt((params.projectId as string[])[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
console.log(`Deleted project ${projectId}`);
|
||||
return HttpResponse.json({ message: `Project ${projectId} deleted successfully` });
|
||||
}),
|
||||
];
|
158
frontend/src/mocks/reviewHandlers.ts
Normal file
158
frontend/src/mocks/reviewHandlers.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { ReviewDetailResponse, ReviewRequest, ReviewResponse, ReviewStatusRequest } from '@/types';
|
||||
|
||||
export const reviewHandlers = [
|
||||
// 리뷰 단건 조회 핸들러
|
||||
http.get('/api/projects/:projectId/reviews/:reviewId', ({ params }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
const reviewId = Array.isArray(params.reviewId)
|
||||
? parseInt(params.reviewId[0], 10)
|
||||
: parseInt(params.reviewId as string, 10);
|
||||
console.log(projectId);
|
||||
const reviewDetail: ReviewDetailResponse = {
|
||||
reviewId: reviewId,
|
||||
title: 'Sample Review Title',
|
||||
content: 'This is a detailed review content.',
|
||||
reviewStatus: 'REQUESTED',
|
||||
images: [
|
||||
{
|
||||
id: 1,
|
||||
imageTitle: 'Image 1',
|
||||
status: 'PENDING',
|
||||
imagePath: 'https://example.com/image1.jpg',
|
||||
dataPath: 'https://example.com/data1.json',
|
||||
},
|
||||
],
|
||||
createAt: new Date().toISOString(),
|
||||
updateAt: new Date().toISOString(),
|
||||
author: { id: 1, nickname: 'Author', profileImage: '', email: 'author@example.com' },
|
||||
reviewer: { id: 2, nickname: 'Reviewer', profileImage: '', email: 'reviewer@example.com' },
|
||||
};
|
||||
|
||||
return HttpResponse.json(reviewDetail);
|
||||
}),
|
||||
|
||||
// 리뷰 생성 핸들러
|
||||
http.post('/api/projects/:projectId/reviews', async ({ params, request }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
console.log(projectId);
|
||||
|
||||
const reviewData = (await request.json()) as ReviewRequest;
|
||||
|
||||
const newReview: ReviewResponse = {
|
||||
projectId,
|
||||
reviewId: Math.floor(Math.random() * 1000), // 임의로 생성된 ID
|
||||
title: reviewData.title,
|
||||
content: reviewData.content,
|
||||
status: 'REQUESTED',
|
||||
createAt: new Date().toISOString(),
|
||||
updateAt: new Date().toISOString(),
|
||||
author: { id: 1, nickname: 'Author', profileImage: '', email: 'author@example.com' },
|
||||
};
|
||||
|
||||
return HttpResponse.json(newReview);
|
||||
}),
|
||||
|
||||
// 리뷰 수정 핸들러
|
||||
http.put('/api/projects/:projectId/reviews/:reviewId', async ({ params, request }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
const reviewId = Array.isArray(params.reviewId)
|
||||
? parseInt(params.reviewId[0], 10)
|
||||
: parseInt(params.reviewId as string, 10);
|
||||
console.log(projectId);
|
||||
|
||||
const reviewData = (await request.json()) as ReviewRequest;
|
||||
|
||||
const updatedReview: ReviewResponse = {
|
||||
projectId,
|
||||
reviewId,
|
||||
title: reviewData.title,
|
||||
content: reviewData.content,
|
||||
status: 'REQUESTED',
|
||||
createAt: new Date().toISOString(),
|
||||
updateAt: new Date().toISOString(),
|
||||
author: { id: 1, nickname: 'Author', profileImage: '', email: 'author@example.com' },
|
||||
};
|
||||
|
||||
return HttpResponse.json(updatedReview);
|
||||
}),
|
||||
|
||||
// 리뷰 삭제 핸들러
|
||||
http.delete('/api/projects/:projectId/reviews/:reviewId', ({ params }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
const reviewId = Array.isArray(params.reviewId)
|
||||
? parseInt(params.reviewId[0], 10)
|
||||
: parseInt(params.reviewId as string, 10);
|
||||
|
||||
return HttpResponse.json({ message: `Review ${reviewId} from project ${projectId} deleted successfully.` });
|
||||
}),
|
||||
|
||||
// 리뷰 상태 변경 핸들러
|
||||
http.put('/api/projects/:projectId/reviews/:reviewId/status', async ({ params, request }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
const reviewId = Array.isArray(params.reviewId)
|
||||
? parseInt(params.reviewId[0], 10)
|
||||
: parseInt(params.reviewId as string, 10);
|
||||
console.log(projectId);
|
||||
|
||||
const statusRequest = (await request.json()) as ReviewStatusRequest;
|
||||
|
||||
const updatedReview: ReviewResponse = {
|
||||
projectId,
|
||||
reviewId,
|
||||
title: `Updated Review ${reviewId}`,
|
||||
content: 'Updated content',
|
||||
status: statusRequest.reviewStatus,
|
||||
createAt: new Date().toISOString(),
|
||||
updateAt: new Date().toISOString(),
|
||||
author: { id: 1, nickname: 'Author', profileImage: '', email: 'author@example.com' },
|
||||
};
|
||||
|
||||
return HttpResponse.json(updatedReview);
|
||||
}),
|
||||
|
||||
http.get('/api/projects/:projectId/reviews', ({ params }) => {
|
||||
const projectId = Array.isArray(params.projectId)
|
||||
? parseInt(params.projectId[0], 10)
|
||||
: parseInt(params.projectId as string, 10);
|
||||
|
||||
const reviewStatus = Array.isArray(params.reviewStatus) ? params.reviewStatus[0] : params.reviewStatus;
|
||||
|
||||
const lastReviewId = Array.isArray(params.lastReviewId)
|
||||
? parseInt(params.lastReviewId[0], 10)
|
||||
: parseInt(params.lastReviewId as string, 10) || 0;
|
||||
|
||||
const limitPage = Array.isArray(params.limitPage)
|
||||
? parseInt(params.limitPage[0], 10)
|
||||
: parseInt(params.limitPage as string, 10) || 10;
|
||||
|
||||
// 총 100개의 리뷰를 생성
|
||||
const totalReviews = 100;
|
||||
const reviews: ReviewResponse[] = Array.from({ length: totalReviews }, (_, index) => ({
|
||||
projectId,
|
||||
reviewId: index + 1,
|
||||
title: `Review ${index + 1}`,
|
||||
content: `Review content ${index + 1}`,
|
||||
status: (reviewStatus || 'REQUESTED') as 'REQUESTED' | 'APPROVED' | 'REJECTED',
|
||||
createAt: new Date().toISOString(),
|
||||
updateAt: new Date().toISOString(),
|
||||
author: { id: 1, nickname: 'Author', profileImage: '', email: 'author@example.com' },
|
||||
}));
|
||||
|
||||
// 마지막 리뷰 ID 기준으로 데이터를 잘라서 반환
|
||||
const startIndex = lastReviewId > 0 ? lastReviewId : 0;
|
||||
const slicedReviews = reviews.slice(startIndex, startIndex + limitPage);
|
||||
|
||||
return HttpResponse.json(slicedReviews);
|
||||
}),
|
||||
];
|
166
frontend/src/mocks/workspaceHandlers.ts
Normal file
166
frontend/src/mocks/workspaceHandlers.ts
Normal file
@ -0,0 +1,166 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { WorkspaceResponse, WorkspaceListResponse, ReviewResponse, WorkspaceMemberResponse } from '@/types';
|
||||
|
||||
export const workspaceHandlers = [
|
||||
// 워크스페이스 조회 핸들러
|
||||
http.get('/api/workspaces/:workspaceId', ({ params }) => {
|
||||
const { workspaceId } = params;
|
||||
const response: WorkspaceResponse = {
|
||||
id: parseInt(workspaceId as string, 10),
|
||||
memberId: 'abc1324',
|
||||
title: 'workspace1',
|
||||
content: '갤럭시 s24 불량 검증',
|
||||
createdAt: '2024-09-18T05:04:44.668Z',
|
||||
updatedAt: '2024-09-18T05:04:44.668Z',
|
||||
};
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
// 워크스페이스 수정 핸들러
|
||||
http.put('/api/workspaces/:workspaceId', ({ params }) => {
|
||||
const { workspaceId } = params;
|
||||
const response: WorkspaceResponse = {
|
||||
id: parseInt(workspaceId as string, 10),
|
||||
memberId: 'abc1324',
|
||||
title: 'Updated Workspace Title',
|
||||
content: 'Updated Workspace Content',
|
||||
createdAt: '2024-09-18T05:04:44.668Z',
|
||||
updatedAt: '2024-09-18T06:00:00.668Z',
|
||||
};
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
// 워크스페이스 삭제 핸들러
|
||||
http.delete('/api/workspaces/:workspaceId', ({ params }) => {
|
||||
const { workspaceId } = params;
|
||||
console.log(`Workspace ${workspaceId} deleted`);
|
||||
return HttpResponse.json({});
|
||||
}),
|
||||
|
||||
http.get('/api/workspaces', () => {
|
||||
console.log('워크스페이스 목록 전체 조회');
|
||||
|
||||
const workspaces: WorkspaceListResponse = {
|
||||
workspaceResponses: [
|
||||
{
|
||||
id: 1,
|
||||
memberId: 'abc1324',
|
||||
title: 'Workspace 1',
|
||||
content: 'Content 1',
|
||||
createdAt: '2024-09-18T05:04:44.668Z',
|
||||
updatedAt: '2024-09-18T05:04:44.668Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
memberId: 'xyz5678',
|
||||
title: 'Workspace 2',
|
||||
content: 'Content 2',
|
||||
createdAt: '2024-09-19T05:04:44.668Z',
|
||||
updatedAt: '2024-09-19T05:04:44.668Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return HttpResponse.json(workspaces);
|
||||
}),
|
||||
// 워크스페이스 멤버 추가 핸들러
|
||||
http.post('/api/workspaces/:workspaceId/members/:newMemberId', async ({ params }) => {
|
||||
const workspaceId = Array.isArray(params.workspaceId)
|
||||
? parseInt((params.workspaceId as string[])[0], 10)
|
||||
: parseInt(params.workspaceId as string, 10);
|
||||
const newMemberId = Array.isArray(params.newMemberId)
|
||||
? parseInt((params.newMemberId as string[])[0], 10)
|
||||
: parseInt(params.newMemberId as string, 10);
|
||||
console.log(workspaceId);
|
||||
const addedMember: WorkspaceMemberResponse = {
|
||||
id: newMemberId,
|
||||
nickname: `Member${newMemberId}`,
|
||||
profileImage: `profile${newMemberId}.jpg`,
|
||||
};
|
||||
|
||||
return HttpResponse.json(addedMember);
|
||||
}),
|
||||
|
||||
// 워크스페이스 멤버 삭제 핸들러
|
||||
http.delete('/api/workspaces/:workspaceId/members/:targetMemberId', ({ params }) => {
|
||||
const workspaceId = Array.isArray(params.workspaceId)
|
||||
? parseInt((params.workspaceId as string[])[0], 10)
|
||||
: parseInt(params.workspaceId as string, 10);
|
||||
const targetMemberId = Array.isArray(params.targetMemberId)
|
||||
? parseInt((params.targetMemberId as string[])[0], 10)
|
||||
: parseInt(params.targetMemberId as string, 10);
|
||||
|
||||
return HttpResponse.json({ message: `Member ${targetMemberId} removed from workspace ${workspaceId}` });
|
||||
}),
|
||||
|
||||
// 워크스페이스 멤버 조회 핸들러
|
||||
http.get('/api/workspaces/:workspaceId/members', ({ params }) => {
|
||||
const workspaceId = Array.isArray(params.workspaceId)
|
||||
? parseInt((params.workspaceId as string[])[0], 10)
|
||||
: parseInt(params.workspaceId as string, 10);
|
||||
console.log(workspaceId);
|
||||
|
||||
const members: WorkspaceMemberResponse[] = [
|
||||
{
|
||||
id: 1,
|
||||
nickname: 'adminUser',
|
||||
profileImage: 'admin.jpg',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
nickname: 'managerUser',
|
||||
profileImage: 'manager.jpg',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
nickname: 'editorUser',
|
||||
profileImage: 'editor.jpg',
|
||||
},
|
||||
];
|
||||
|
||||
return HttpResponse.json(members);
|
||||
}),
|
||||
|
||||
// 워크스페이스 리뷰 조회 핸들러
|
||||
http.get('/api/workspaces/:workspaceId/reviews', ({ params }) => {
|
||||
const workspaceId = Array.isArray(params.workspaceId)
|
||||
? parseInt((params.workspaceId as string[])[0], 10)
|
||||
: parseInt(params.workspaceId as string, 10);
|
||||
console.log(workspaceId);
|
||||
|
||||
const reviews: ReviewResponse[] = [
|
||||
{
|
||||
reviewId: 1,
|
||||
projectId: 1,
|
||||
title: 'First Review',
|
||||
content: 'This is a review content.',
|
||||
status: 'APPROVED',
|
||||
author: {
|
||||
id: 1,
|
||||
nickname: 'Reviewer1',
|
||||
profileImage: 'reviewer1.jpg',
|
||||
email: 'reviewer1@example.com',
|
||||
},
|
||||
createAt: new Date().toISOString(),
|
||||
updateAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
reviewId: 2,
|
||||
projectId: 2,
|
||||
title: 'Second Review',
|
||||
content: 'This is another review content.',
|
||||
status: 'REQUESTED',
|
||||
author: {
|
||||
id: 2,
|
||||
nickname: 'Reviewer2',
|
||||
profileImage: 'reviewer2.jpg',
|
||||
email: 'reviewer2@example.com',
|
||||
},
|
||||
createAt: new Date().toISOString(),
|
||||
updateAt: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
return HttpResponse.json(reviews);
|
||||
}),
|
||||
];
|
@ -1,25 +1,12 @@
|
||||
import { useRef } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import GoogleLogo from '@/assets/icons/web_neutral_rd_ctn@1x.png';
|
||||
import useAuthStore from '@/stores/useAuthStore';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { getProfile } from '@/api/authApi';
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
export default function Home() {
|
||||
const { isLoggedIn, accessToken, setLoggedIn, profile, setProfile } = useAuthStore();
|
||||
const hasFetchedProfile = useRef(false);
|
||||
|
||||
if (!isLoggedIn && !profile && !hasFetchedProfile.current && accessToken) {
|
||||
setLoggedIn(true, accessToken);
|
||||
getProfile().then((data) => {
|
||||
setProfile(data);
|
||||
hasFetchedProfile.current = true;
|
||||
});
|
||||
}
|
||||
const handleGoogleSignIn = () => {
|
||||
window.location.href = `${BASE_URL}/login/oauth2/authorization/google`;
|
||||
};
|
||||
const { accessToken } = useAuthStore();
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center bg-gray-50 p-8">
|
||||
@ -42,9 +29,9 @@ export default function Home() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!isLoggedIn ? (
|
||||
<button
|
||||
onClick={handleGoogleSignIn}
|
||||
{!accessToken ? (
|
||||
<a
|
||||
href={`${BASE_URL}/login/oauth2/authorization/google`}
|
||||
className="mb-4 transition hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-gray-300 active:opacity-80"
|
||||
>
|
||||
<img
|
||||
@ -52,7 +39,7 @@ export default function Home() {
|
||||
alt="Sign in with Google"
|
||||
className="h-auto w-full"
|
||||
/>
|
||||
</button> // 404 에러 방지
|
||||
</a> // 404 에러 방지
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { Suspense, useState, useEffect, useRef } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import useReviewByStatusQuery from '@/queries/reviews/useReviewByStatusQuery';
|
||||
import useAuthStore from '@/stores/useAuthStore';
|
||||
@ -14,33 +14,70 @@ export default function ProjectReviewList() {
|
||||
const [, setSearchQuery] = useState('');
|
||||
const [sortValue, setSortValue] = useState('latest');
|
||||
|
||||
const { data: projectReviews = [] } = useReviewByStatusQuery(
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useReviewByStatusQuery(
|
||||
Number(projectId),
|
||||
memberId,
|
||||
activeTab !== 'all' ? activeTab : undefined
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<header className="bg-background sticky top-0 z-10 flex h-[57px] items-center gap-1 border-b px-4">
|
||||
<h1 className="text-xl font-semibold">프로젝트 리뷰</h1>
|
||||
<Link
|
||||
to={`/admin/${workspaceId}/reviews/request`}
|
||||
className="ml-auto"
|
||||
>
|
||||
<Button variant="outlinePrimary">리뷰 요청</Button>
|
||||
</Link>
|
||||
</header>
|
||||
const projectReviews = data?.pages.flat() || [];
|
||||
|
||||
<ReviewList
|
||||
reviews={projectReviews}
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
setSearchQuery={setSearchQuery}
|
||||
sortValue={sortValue}
|
||||
setSortValue={setSortValue}
|
||||
workspaceId={Number(workspaceId)}
|
||||
/>
|
||||
</div>
|
||||
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasNextPage || isFetchingNextPage) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
fetchNextPage();
|
||||
}
|
||||
},
|
||||
{ threshold: 1.0 }
|
||||
);
|
||||
|
||||
const currentLoadMoreRef = loadMoreRef.current;
|
||||
if (currentLoadMoreRef) {
|
||||
observer.observe(currentLoadMoreRef);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (currentLoadMoreRef) {
|
||||
observer.unobserve(currentLoadMoreRef);
|
||||
}
|
||||
};
|
||||
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div></div>}>
|
||||
<div>
|
||||
<header className="sticky top-0 z-10 flex h-[57px] items-center gap-1 border-b bg-white px-4">
|
||||
<h1 className="text-xl font-semibold">프로젝트 리뷰</h1>
|
||||
<Link
|
||||
to={`/admin/${workspaceId}/reviews/request`}
|
||||
className="ml-auto"
|
||||
>
|
||||
<Button variant="outlinePrimary">리뷰 요청</Button>
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<ReviewList
|
||||
reviews={projectReviews}
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
setSearchQuery={setSearchQuery}
|
||||
sortValue={sortValue}
|
||||
setSortValue={setSortValue}
|
||||
workspaceId={Number(workspaceId)}
|
||||
/>
|
||||
|
||||
{isFetchingNextPage}
|
||||
|
||||
<div
|
||||
ref={loadMoreRef}
|
||||
className="h-1"
|
||||
/>
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import useWorkspaceReviewsQuery from '@/queries/workspaces/useWorkspaceReviewsQuery';
|
||||
import useAuthStore from '@/stores/useAuthStore';
|
||||
import ReviewList from '@/components/ReviewList';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
import { Suspense } from 'react';
|
||||
export default function WorkspaceReviewList() {
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||
const profile = useAuthStore((state) => state.profile);
|
||||
@ -14,32 +14,70 @@ export default function WorkspaceReviewList() {
|
||||
const [, setSearchQuery] = useState('');
|
||||
const [sortValue, setSortValue] = useState('latest');
|
||||
|
||||
const { data: workspaceReviews = [] } = useWorkspaceReviewsQuery(
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useWorkspaceReviewsQuery(
|
||||
Number(workspaceId),
|
||||
memberId,
|
||||
activeTab !== 'all' ? activeTab : undefined
|
||||
);
|
||||
|
||||
const workspaceReviews = data?.pages.flat() || [];
|
||||
|
||||
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasNextPage || isFetchingNextPage) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
fetchNextPage();
|
||||
}
|
||||
},
|
||||
{ threshold: 1.0 }
|
||||
);
|
||||
|
||||
const currentLoadMoreRef = loadMoreRef.current;
|
||||
if (currentLoadMoreRef) {
|
||||
observer.observe(currentLoadMoreRef);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (currentLoadMoreRef) {
|
||||
observer.unobserve(currentLoadMoreRef);
|
||||
}
|
||||
};
|
||||
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<header className="bg-background sticky top-0 z-10 flex h-[57px] items-center gap-1 border-b px-4">
|
||||
<h1 className="text-xl font-semibold">워크스페이스 리뷰</h1>
|
||||
<Link
|
||||
to={`/admin/${workspaceId}/reviews/request`}
|
||||
className="ml-auto"
|
||||
>
|
||||
<Button variant="outlinePrimary">리뷰 요청</Button>
|
||||
</Link>
|
||||
</header>
|
||||
<ReviewList
|
||||
reviews={workspaceReviews}
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
setSearchQuery={setSearchQuery}
|
||||
sortValue={sortValue}
|
||||
setSortValue={setSortValue}
|
||||
workspaceId={Number(workspaceId)}
|
||||
/>
|
||||
</div>
|
||||
<Suspense fallback={<div></div>}>
|
||||
<div>
|
||||
<header className="sticky top-0 z-10 flex h-[57px] items-center gap-1 border-b bg-white px-4">
|
||||
<h1 className="text-xl font-semibold">워크스페이스 리뷰</h1>
|
||||
<Link
|
||||
to={`/admin/${workspaceId}/reviews/request`}
|
||||
className="ml-auto"
|
||||
>
|
||||
<Button variant="outlinePrimary">리뷰 요청</Button>
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<ReviewList
|
||||
reviews={workspaceReviews}
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
setSearchQuery={setSearchQuery}
|
||||
sortValue={sortValue}
|
||||
setSortValue={setSortValue}
|
||||
workspaceId={Number(workspaceId)}
|
||||
/>
|
||||
|
||||
{isFetchingNextPage}
|
||||
|
||||
<div
|
||||
ref={loadMoreRef}
|
||||
className="h-1"
|
||||
/>
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
16
frontend/src/queries/auth/useLogoutQuery.ts
Normal file
16
frontend/src/queries/auth/useLogoutQuery.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import useAuthStore from '@/stores/useAuthStore';
|
||||
import { logout } from '@/api/authApi';
|
||||
|
||||
export default function useLogoutQuery() {
|
||||
const queryClient = useQueryClient();
|
||||
const { clearAuth } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: logout,
|
||||
onSuccess: () => {
|
||||
clearAuth();
|
||||
queryClient.invalidateQueries({ queryKey: ['profile'] });
|
||||
},
|
||||
});
|
||||
}
|
@ -4,12 +4,12 @@ import { reissueToken } from '@/api/authApi';
|
||||
|
||||
export default function useReissueTokenQuery() {
|
||||
const queryClient = useQueryClient();
|
||||
const { setLoggedIn } = useAuthStore();
|
||||
const { setToken } = useAuthStore();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: reissueToken,
|
||||
onSuccess: (data) => {
|
||||
setLoggedIn(true, data.accessToken);
|
||||
setToken(data.accessToken);
|
||||
queryClient.invalidateQueries({ queryKey: ['profile'] });
|
||||
},
|
||||
});
|
||||
|
10
frontend/src/queries/models/useModelReportsQuery.ts
Normal file
10
frontend/src/queries/models/useModelReportsQuery.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { getModelReports } from '@/api/modelApi';
|
||||
import { ReportResponse } from '@/types';
|
||||
|
||||
export default function useModelReportsQuery(projectId: number, modelId: number) {
|
||||
return useSuspenseQuery<ReportResponse[]>({
|
||||
queryKey: ['modelReports', projectId, modelId],
|
||||
queryFn: () => getModelReports(projectId, modelId),
|
||||
});
|
||||
}
|
10
frontend/src/queries/models/useModelResultsQuery.ts
Normal file
10
frontend/src/queries/models/useModelResultsQuery.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { getModelResults } from '@/api/modelApi';
|
||||
import { ResultResponse } from '@/types';
|
||||
|
||||
export default function useModelResultsQuery(modelId: number) {
|
||||
return useSuspenseQuery<ResultResponse[]>({
|
||||
queryKey: ['modelResults', modelId],
|
||||
queryFn: () => getModelResults(modelId),
|
||||
});
|
||||
}
|
12
frontend/src/queries/models/usePollingModelReportsQuery.ts
Normal file
12
frontend/src/queries/models/usePollingModelReportsQuery.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getModelReports } from '@/api/modelApi';
|
||||
import { ReportResponse } from '@/types';
|
||||
|
||||
export default function usePollingModelReportsQuery(projectId: number, modelId: number, enabled: boolean) {
|
||||
return useQuery<ReportResponse[]>({
|
||||
queryKey: ['pollingModelReports', projectId, modelId],
|
||||
queryFn: () => getModelReports(projectId, modelId),
|
||||
refetchInterval: 5000,
|
||||
enabled,
|
||||
});
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { trainModel } from '@/api/modelApi';
|
||||
import { ModelTrainRequest } from '@/types';
|
||||
|
||||
export default function useTrainModelQuery(projectId: number) {
|
||||
return useMutation({
|
||||
mutationFn: () => trainModel(projectId),
|
||||
mutationFn: (trainData: ModelTrainRequest) => trainModel(projectId, trainData),
|
||||
});
|
||||
}
|
||||
|
9
frontend/src/queries/projects/useAutoLabelQuery.ts
Normal file
9
frontend/src/queries/projects/useAutoLabelQuery.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { runAutoLabel } from '@/api/lablingApi';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
export default function useAutoLabelQuery() {
|
||||
return useMutation({
|
||||
mutationFn: ({ projectId, modelId = 1 }: { projectId: number; modelId?: number }) =>
|
||||
runAutoLabel(projectId, modelId),
|
||||
});
|
||||
}
|
@ -1,13 +1,22 @@
|
||||
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
|
||||
import { getReviewByStatus } from '@/api/reviewApi';
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { ReviewResponse } from '@/types';
|
||||
|
||||
export default function useReviewByStatusQuery(
|
||||
projectId: number,
|
||||
memberId: number,
|
||||
reviewStatus: 'REQUESTED' | 'APPROVED' | 'REJECTED' | undefined
|
||||
) {
|
||||
return useSuspenseQuery({
|
||||
return useSuspenseInfiniteQuery<ReviewResponse[]>({
|
||||
queryKey: ['reviewByStatus', projectId, reviewStatus],
|
||||
queryFn: () => getReviewByStatus(projectId, memberId, reviewStatus),
|
||||
queryFn: ({ pageParam = undefined }) => {
|
||||
return getReviewByStatus(projectId, memberId, reviewStatus, pageParam as number | undefined);
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (lastPage.length === 0) return undefined;
|
||||
const lastReview = lastPage[lastPage.length - 1];
|
||||
return lastReview.reviewId;
|
||||
},
|
||||
initialPageParam: undefined,
|
||||
});
|
||||
}
|
||||
|
@ -1,15 +1,24 @@
|
||||
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
|
||||
import { getWorkspaceReviews } from '@/api/workspaceApi';
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { ReviewResponse } from '@/types';
|
||||
|
||||
export default function useWorkspaceReviewsQuery(
|
||||
workspaceId: number,
|
||||
memberId: number,
|
||||
reviewStatus?: 'REQUESTED' | 'APPROVED' | 'REJECTED',
|
||||
lastReviewId?: number,
|
||||
limitPage?: number
|
||||
limitPage: number = 10
|
||||
) {
|
||||
return useSuspenseQuery({
|
||||
queryKey: ['workspaceReviews', workspaceId, reviewStatus, lastReviewId],
|
||||
queryFn: () => getWorkspaceReviews(workspaceId, memberId, reviewStatus, lastReviewId, limitPage),
|
||||
return useSuspenseInfiniteQuery<ReviewResponse[]>({
|
||||
queryKey: ['workspaceReviews', workspaceId, reviewStatus],
|
||||
queryFn: ({ pageParam = undefined }) =>
|
||||
getWorkspaceReviews(workspaceId, memberId, reviewStatus, pageParam as number | undefined, limitPage),
|
||||
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (lastPage.length === 0) return undefined;
|
||||
const lastReview = lastPage[lastPage.length - 1];
|
||||
return lastReview.reviewId;
|
||||
},
|
||||
|
||||
initialPageParam: undefined,
|
||||
});
|
||||
}
|
||||
|
@ -47,7 +47,6 @@ const router = createBrowserRouter([
|
||||
],
|
||||
},
|
||||
{
|
||||
// FIXME: index에서 오류나지 않게 수정
|
||||
path: webPath.browse(),
|
||||
element: (
|
||||
<Suspense fallback={<PageLayout />}>
|
||||
@ -66,7 +65,6 @@ const router = createBrowserRouter([
|
||||
],
|
||||
},
|
||||
{
|
||||
// FIXME: index에서 오류나지 않게 수정
|
||||
path: `${webPath.workspace()}/:workspaceId`,
|
||||
element: (
|
||||
<Suspense fallback={<div></div>}>
|
||||
|
@ -3,10 +3,9 @@ import { persist } from 'zustand/middleware';
|
||||
import { MemberResponse } from '@/types';
|
||||
|
||||
interface AuthState {
|
||||
isLoggedIn: boolean;
|
||||
accessToken: string;
|
||||
profile: MemberResponse | null;
|
||||
setLoggedIn: (status: boolean, token: string) => void;
|
||||
setToken: (token: string) => void;
|
||||
setProfile: (profile: MemberResponse) => void;
|
||||
clearAuth: () => void;
|
||||
}
|
||||
@ -14,12 +13,11 @@ interface AuthState {
|
||||
const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
isLoggedIn: false,
|
||||
accessToken: '',
|
||||
profile: null,
|
||||
setLoggedIn: (status: boolean, token: string) => set({ isLoggedIn: status, accessToken: token }),
|
||||
setToken: (token: string) => set({ accessToken: token }),
|
||||
setProfile: (profile: MemberResponse) => set({ profile }),
|
||||
clearAuth: () => set({ isLoggedIn: false, accessToken: '', profile: null }),
|
||||
clearAuth: () => set({ accessToken: '', profile: null }),
|
||||
}),
|
||||
{
|
||||
name: 'auth-storage',
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { ImageResponse, Label, Project } from '@/types';
|
||||
import { ImageResponse, Label } from '@/types';
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface CanvasState {
|
||||
project: Project | null;
|
||||
// project: Project | null;
|
||||
sidebarSize: number;
|
||||
image: ImageResponse | null;
|
||||
labels: Label[];
|
||||
drawState: 'pen' | 'rect' | 'pointer';
|
||||
selectedLabelId: number | null;
|
||||
setProject: (project: Project | null) => void;
|
||||
// setProject: (project: Project | null) => void;
|
||||
setSidebarSize: (width: number) => void;
|
||||
setImage: (image: ImageResponse) => void;
|
||||
setImage: (image: ImageResponse | null) => void;
|
||||
setLabels: (labels: Label[]) => void;
|
||||
addLabel: (label: Label) => void;
|
||||
removeLabel: (labelId: number) => void;
|
||||
@ -20,13 +20,13 @@ interface CanvasState {
|
||||
}
|
||||
|
||||
const useCanvasStore = create<CanvasState>()((set) => ({
|
||||
project: null,
|
||||
// project: null,
|
||||
sidebarSize: 20,
|
||||
image: null,
|
||||
labels: [],
|
||||
drawState: 'pointer',
|
||||
selectedLabelId: null,
|
||||
setProject: (project) => set({ project }),
|
||||
// setProject: (project) => set({ project }),
|
||||
setSidebarSize: (width) => set({ sidebarSize: width }),
|
||||
setImage: (image) => set({ image }),
|
||||
addLabel: (label: Label) => set((state) => ({ labels: [...state.labels, label] })),
|
||||
|
@ -1,22 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { FolderResponse } from '@/types';
|
||||
|
||||
interface FolderState {
|
||||
folder: FolderResponse | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
setFolder: (folder: FolderResponse | null) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
}
|
||||
|
||||
const useFolderStore = create<FolderState>((set) => ({
|
||||
folder: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
setFolder: (folder) => set({ folder }),
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setError: (error) => set({ error }),
|
||||
}));
|
||||
|
||||
export default useFolderStore;
|
56
frontend/src/stores/useModelStore.ts
Normal file
56
frontend/src/stores/useModelStore.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { create } from 'zustand';
|
||||
import { ReportResponse } from '@/types';
|
||||
|
||||
interface ModelStoreState {
|
||||
trainingDataByProject: Record<string, ReportResponse[]>;
|
||||
isTrainingByProject: Record<string, boolean>;
|
||||
selectedModelByProject: Record<string, number | null>;
|
||||
setIsTraining: (projectId: string, status: boolean) => void;
|
||||
saveTrainingData: (projectId: string, data: ReportResponse[]) => void;
|
||||
setSelectedModel: (projectId: string, modelId: number | null) => void;
|
||||
resetTrainingData: (projectId: string) => void;
|
||||
}
|
||||
|
||||
const useModelStore = create<ModelStoreState>((set) => ({
|
||||
trainingDataByProject: {},
|
||||
isTrainingByProject: {},
|
||||
selectedModelByProject: {},
|
||||
setIsTraining: (projectId, status) =>
|
||||
set((state) => ({
|
||||
isTrainingByProject: {
|
||||
...state.isTrainingByProject,
|
||||
[projectId]: status,
|
||||
},
|
||||
})),
|
||||
saveTrainingData: (projectId, data) =>
|
||||
set((state) => ({
|
||||
trainingDataByProject: {
|
||||
...state.trainingDataByProject,
|
||||
[projectId]: data,
|
||||
},
|
||||
})),
|
||||
setSelectedModel: (projectId, modelId) =>
|
||||
set((state) => ({
|
||||
selectedModelByProject: {
|
||||
...state.selectedModelByProject,
|
||||
[projectId]: modelId,
|
||||
},
|
||||
})),
|
||||
resetTrainingData: (projectId) =>
|
||||
set((state) => ({
|
||||
trainingDataByProject: {
|
||||
...state.trainingDataByProject,
|
||||
[projectId]: [],
|
||||
},
|
||||
selectedModelByProject: {
|
||||
...state.selectedModelByProject,
|
||||
[projectId]: null,
|
||||
},
|
||||
isTrainingByProject: {
|
||||
...state.isTrainingByProject,
|
||||
[projectId]: false,
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
export default useModelStore;
|
14
frontend/src/stores/useProjectStore.ts
Normal file
14
frontend/src/stores/useProjectStore.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { create } from 'zustand';
|
||||
import { Project } from '@/types';
|
||||
|
||||
interface ProjectState {
|
||||
project: Project | null;
|
||||
setProject: (project: Project | null) => void;
|
||||
}
|
||||
|
||||
const useProjectStore = create<ProjectState>((set) => ({
|
||||
project: null,
|
||||
setProject: (project) => set({ project }),
|
||||
}));
|
||||
|
||||
export default useProjectStore;
|
@ -1,40 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface TrainingData {
|
||||
epoch: number;
|
||||
total_epochs: number;
|
||||
box_loss: number;
|
||||
cls_loss: number;
|
||||
dfl_loss: number;
|
||||
fitness: number;
|
||||
epoch_time: number;
|
||||
left_second: number;
|
||||
}
|
||||
|
||||
interface StoreState {
|
||||
trainingDataByProject: { [projectId: string]: TrainingData[] };
|
||||
addTrainingData: (projectId: string, data: TrainingData) => void;
|
||||
resetTrainingData: (projectId: string) => void;
|
||||
}
|
||||
|
||||
const useTrainStore = create<StoreState>((set) => ({
|
||||
trainingDataByProject: {},
|
||||
|
||||
addTrainingData: (projectId: string, data: TrainingData) =>
|
||||
set((state) => ({
|
||||
trainingDataByProject: {
|
||||
...state.trainingDataByProject,
|
||||
[projectId]: [...(state.trainingDataByProject[projectId] || []), data],
|
||||
},
|
||||
})),
|
||||
|
||||
resetTrainingData: (projectId: string) =>
|
||||
set((state) => ({
|
||||
trainingDataByProject: {
|
||||
...state.trainingDataByProject,
|
||||
[projectId]: [],
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
export default useTrainStore;
|
@ -68,12 +68,14 @@ export interface FolderResponse {
|
||||
children: ChildFolder[];
|
||||
}
|
||||
|
||||
export type ImageStatus = 'PENDING' | 'IN_PROGRESS' | 'SAVE' | 'REVIEW_REQUEST' | 'COMPLETED';
|
||||
|
||||
export interface ImageResponse {
|
||||
id: number;
|
||||
imageTitle: string;
|
||||
imagePath: string;
|
||||
dataPath: string;
|
||||
status: 'PENDING' | 'IN_PROGRESS' | 'SAVE' | 'REVIEW_REQUEST' | 'COMPLETED';
|
||||
status: ImageStatus;
|
||||
}
|
||||
|
||||
// 이미지 이동 및 상태변경 요청 DTO
|
||||
@ -82,7 +84,7 @@ export interface ImageMoveRequest {
|
||||
}
|
||||
|
||||
export interface ImageStatusChangeRequest {
|
||||
labelStatus: 'PENDING' | 'IN_PROGRESS' | 'SAVE' | 'REVIEW_REQUEST' | 'COMPLETED';
|
||||
labelStatus: ImageStatus;
|
||||
}
|
||||
|
||||
// 멤버 관련 DTO
|
||||
@ -277,6 +279,25 @@ export interface ImageFolderRequest {
|
||||
parentId: number;
|
||||
files: File[];
|
||||
}
|
||||
export interface LabelCategoryResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
// 카테고리 요청 DTO
|
||||
export interface LabelCategoryRequest {
|
||||
labelCategoryList: number[];
|
||||
}
|
||||
|
||||
// 카테고리 응답 DTO
|
||||
export interface LabelCategoryResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
// 모델 카테고리 응답 DTO
|
||||
export interface ModelCategoryResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// 모델 요청 DTO (API로 전달할 데이터 타입)
|
||||
export interface ModelRequest {
|
||||
@ -289,22 +310,41 @@ export interface ModelResponse {
|
||||
name: string;
|
||||
}
|
||||
|
||||
// 모델 카테고리 응답 DTO
|
||||
export interface ModelCategoryResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// 프로젝트 모델 리스트 응답 DTO
|
||||
export interface ProjectModelsResponse extends Array<ModelResponse> {}
|
||||
|
||||
// 카테고리 요청 DTO
|
||||
export interface LabelCategoryRequest {
|
||||
labelCategoryList: number[];
|
||||
// 모델 훈련 요청 DTO
|
||||
export interface ModelTrainRequest {
|
||||
modelId: number;
|
||||
ratio: number;
|
||||
epochs: number;
|
||||
batch: number;
|
||||
lr0: number;
|
||||
lrf: number;
|
||||
optimizer: 'AUTO' | 'SGD' | 'ADAM' | 'ADAMW' | 'NADAM' | 'RADAM' | 'RMSPROP';
|
||||
}
|
||||
|
||||
// 카테고리 응답 DTO
|
||||
export interface LabelCategoryResponse {
|
||||
export interface ResultResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user