diff --git a/frontend/src/api/authApi.ts b/frontend/src/api/authApi.ts index 72402b7..1844164 100644 --- a/frontend/src/api/authApi.ts +++ b/frontend/src/api/authApi.ts @@ -6,11 +6,11 @@ export async function reissueToken() { } export async function getProfile() { - return api - .get('/auth/profile', { - withCredentials: true, - }) - .then(({ data }) => data); + return api.get('/auth/profile').then(({ data }) => data); +} + +export async function logout() { + return api.post('/auth/logout').then(({ data }) => data); } export async function saveFcmToken(token: string) { diff --git a/frontend/src/api/axiosConfig.ts b/frontend/src/api/axiosConfig.ts index 600f8db..c6b8e14 100644 --- a/frontend/src/api/axiosConfig.ts +++ b/frontend/src/api/axiosConfig.ts @@ -27,9 +27,8 @@ api.interceptors.response.use( return api .post(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); } diff --git a/frontend/src/api/imageApi.ts b/frontend/src/api/imageApi.ts index 5fe3572..aaa0aec 100644 --- a/frontend/src/api/imageApi.ts +++ b/frontend/src/api/imageApi.ts @@ -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); diff --git a/frontend/src/api/lablingApi.ts b/frontend/src/api/lablingApi.ts index 2d103ec..0c922c9 100644 --- a/frontend/src/api/lablingApi.ts +++ b/frontend/src/api/lablingApi.ts @@ -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); } diff --git a/frontend/src/api/modelApi.ts b/frontend/src/api/modelApi.ts index 106b735..0f7e99e 100644 --- a/frontend/src/api/modelApi.ts +++ b/frontend/src/api/modelApi.ts @@ -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(`/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(`/models/${modelId}/categories`).then(({ data }) => data); } + +export async function getModelResults(modelId: number) { + return api.get(`/results/model/${modelId}`).then(({ data }) => data); +} + +export async function getModelReports(projectId: number, modelId: number) { + return api.get(`/projects/${projectId}/reports/model/${modelId}`).then(({ data }) => data); +} diff --git a/frontend/src/components/AdminLayout/index.tsx b/frontend/src/components/AdminLayout/index.tsx index a413caa..80e6f89 100644 --- a/frontend/src/components/AdminLayout/index.tsx +++ b/frontend/src/components/AdminLayout/index.tsx @@ -13,7 +13,7 @@ export default function AdminLayout() { -
+
diff --git a/frontend/src/components/CanvasControlBar/index.stories.tsx b/frontend/src/components/CanvasControlBar/index.stories.tsx index a910ce8..52d945d 100644 --- a/frontend/src/components/CanvasControlBar/index.stories.tsx +++ b/frontend/src/components/CanvasControlBar/index.stories.tsx @@ -6,4 +6,9 @@ export default { component: CanvasControlBar, }; -export const Default = () => {}} />; +export const Default = () => ( + {}} + projectType="segmentation" + /> +); diff --git a/frontend/src/components/CanvasControlBar/index.tsx b/frontend/src/components/CanvasControlBar/index.tsx index 56fd7c1..8f86cbf 100644 --- a/frontend/src/components/CanvasControlBar/index.tsx +++ b/frontend/src/components/CanvasControlBar/index.tsx @@ -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 }) ); })} - +
+
+ ); +} diff --git a/frontend/src/components/Header/UserProfileModal.tsx b/frontend/src/components/Header/UserProfileModal.tsx new file mode 100644 index 0000000..9e92679 --- /dev/null +++ b/frontend/src/components/Header/UserProfileModal.tsx @@ -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 ( + + + + + + + + + + ); +} diff --git a/frontend/src/components/Header/index.tsx b/frontend/src/components/Header/index.tsx index 0375839..d1a2fab 100644 --- a/frontend/src/components/Header/index.tsx +++ b/frontend/src/components/Header/index.tsx @@ -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 {} @@ -60,7 +61,7 @@ export default function Header({ className, ...props }: HeaderProps) { {!isHomePage && (
- +
)} diff --git a/frontend/src/components/ImageCanvas/index.tsx b/frontend/src/components/ImageCanvas/index.tsx index 2a04e5d..01e2074 100644 --- a/frontend/src/components/ImageCanvas/index.tsx +++ b/frontend/src/components/ImageCanvas/index.tsx @@ -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(null); const dragLayerRef = useRef(null); @@ -344,7 +345,10 @@ export default function ImageCanvas() { - + ) : (
diff --git a/frontend/src/components/ImageUploadFileModal/ImageUploadFileForm.tsx b/frontend/src/components/ImageUploadFileModal/ImageUploadFileForm.tsx index 6f8e01c..74c9e5f 100644 --- a/frontend/src/components/ImageUploadFileModal/ImageUploadFileForm.tsx +++ b/frontend/src/components/ImageUploadFileModal/ImageUploadFileForm.tsx @@ -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) => { @@ -120,16 +122,40 @@ export default function ImageUploadFileForm({ className={cn('flex items-center justify-between p-1')} > {file.webkitRelativePath || file.name} - + {isUploading ? ( +
+ {isUploaded ? ( + + ) : isFailed ? ( + + ) : ( + + )} +
+ ) : ( + + )} ))} diff --git a/frontend/src/components/ImageUploadFolderModal/ImageUploadFolderForm.tsx b/frontend/src/components/ImageUploadFolderModal/ImageUploadFolderForm.tsx index cde32d5..0995671 100644 --- a/frontend/src/components/ImageUploadFolderModal/ImageUploadFolderForm.tsx +++ b/frontend/src/components/ImageUploadFolderModal/ImageUploadFolderForm.tsx @@ -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) => { @@ -105,16 +107,40 @@ export default function ImageUploadFolderForm({ onClose, projectId }: { onClose: className={cn('flex items-center justify-between p-1')} > {file.webkitRelativePath || file.name} - + {isUploading ? ( +
+ {isUploaded ? ( + + ) : isFailed ? ( + + ) : ( + + )} +
+ ) : ( + + )} ))} diff --git a/frontend/src/components/ImageUploadZipModal/ImageUploadZipForm.tsx b/frontend/src/components/ImageUploadZipModal/ImageUploadZipForm.tsx index 9cb0646..647bdf4 100644 --- a/frontend/src/components/ImageUploadZipModal/ImageUploadZipForm.tsx +++ b/frontend/src/components/ImageUploadZipModal/ImageUploadZipForm.tsx @@ -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) => { @@ -103,16 +105,40 @@ export default function ImageUploadZipForm({ onClose, projectId }: { onClose: () {file && (
{file.webkitRelativePath || file.name} - + {isUploading ? ( +
+ {isUploaded ? ( + + ) : isFailed ? ( + + ) : ( + + )} +
+ ) : ( + + )}
)} {isUploading ? ( diff --git a/frontend/src/components/ModelLineChart/index.tsx b/frontend/src/components/ModelLineChart/index.tsx deleted file mode 100644 index 2627611..0000000 --- a/frontend/src/components/ModelLineChart/index.tsx +++ /dev/null @@ -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 ( - - - Model Training Metrics - Loss and Fitness over Epochs - - - - - - `Epoch ${value}`} - /> - } - /> - - - - - - - - -
-
-
- Trending up by 5.2% this epoch -
-
- Showing training loss and fitness for the current model -
-
-
-
-
- ); -} diff --git a/frontend/src/components/ModelManage/EvaluationTab.tsx b/frontend/src/components/ModelManage/EvaluationTab.tsx index 5824787..9dd0639 100644 --- a/frontend/src/components/ModelManage/EvaluationTab.tsx +++ b/frontend/src/components/ModelManage/EvaluationTab.tsx @@ -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(null); + const { data: models } = useProjectModelsQuery(projectId ?? 0); + return (
-
- - -
+ {selectedModel && ( -
-
- -
-
- -
-
+ )}
); } -function LabelingPreview() { +interface ModelSelectionProps { + models: Array<{ id: number; name: string }> | undefined; + setSelectedModel: (modelId: number) => void; +} + +function ModelSelection({ models, setSelectedModel }: ModelSelectionProps) { return ( -
-

레이블링 프리뷰

+
+ + +
+ ); +} + +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 = ( +
+
+ Epochs +

{resultData[0]?.epochs}

+
+
+ Batch Size +

{resultData[0]?.batch}

+
+
+ Learning Rate (Start) +

{resultData[0]?.lr0}

+
+
+ Learning Rate (End) +

{resultData[0]?.lrf}

+
+
+ Optimizer +

{resultData[0]?.optimizer}

+
+
+ ); + + return ( +
+ {trainingInfoRow} {/* 학습 정보 표시 */} +
+ {' '} + {/* grid와 높이 설정 */} +
+ {' '} + {/* 차트의 높이를 100%로 맞춤 */} + +
+
+ {' '} + {/* 차트의 높이를 100%로 맞춤 */} + +
+
); } diff --git a/frontend/src/components/ModelManage/InputWithLabel.tsx b/frontend/src/components/ModelManage/InputWithLabel.tsx new file mode 100644 index 0000000..c01f878 --- /dev/null +++ b/frontend/src/components/ModelManage/InputWithLabel.tsx @@ -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) => void; + disabled?: boolean; +} + +export default function InputWithLabel({ label, id, placeholder, value, disabled, onChange }: InputWithLabelProps) { + return ( +
+ + +
+ ); +} diff --git a/frontend/src/components/ModelBarChart/index.tsx b/frontend/src/components/ModelManage/ModelBarChart.tsx similarity index 76% rename from frontend/src/components/ModelBarChart/index.tsx rename to frontend/src/components/ModelManage/ModelBarChart.tsx index abbae01..8e95802 100644 --- a/frontend/src/components/ModelBarChart/index.tsx +++ b/frontend/src/components/ModelManage/ModelBarChart.tsx @@ -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 ( - + Model Metrics Performance metrics of the model @@ -86,12 +84,6 @@ export default function ModelBarChart({ data }: ModelBarChartProps) { - -
- Model metrics are trending well -
-
Showing current performance metrics
-
); } diff --git a/frontend/src/components/ModelManage/ModelLineChart.tsx b/frontend/src/components/ModelManage/ModelLineChart.tsx new file mode 100644 index 0000000..cae6dd6 --- /dev/null +++ b/frontend/src/components/ModelManage/ModelLineChart.tsx @@ -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 ( + + + Model Training Metrics + + + {latestData && latestData.totalEpochs !== Number(latestData.epoch) && ( +
+

현재 에포크: {latestData.epoch}

+

총 에포크: {latestData.totalEpochs}

+

예상 남은시간: {latestData.leftSecond}초

+
+ )} + + + + `Epoch ${value}`} + /> + + + + + + + + + +
+
+ ); +} diff --git a/frontend/src/components/ModelManage/SelectWithLabel.tsx b/frontend/src/components/ModelManage/SelectWithLabel.tsx new file mode 100644 index 0000000..b6c7ad8 --- /dev/null +++ b/frontend/src/components/ModelManage/SelectWithLabel.tsx @@ -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 ( +
+ + +
+ ); +} diff --git a/frontend/src/components/ModelManage/SettingsForm.tsx b/frontend/src/components/ModelManage/SettingsForm.tsx deleted file mode 100644 index 5fdf721..0000000 --- a/frontend/src/components/ModelManage/SettingsForm.tsx +++ /dev/null @@ -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(null); - const [ratio, setRatio] = useState(0.8); - const [epochs, setEpochs] = useState(50); - const [batchSize, setBatchSize] = useState(32); - const [optimizer, setOptimizer] = useState('SGD'); - const [lr0, setLr0] = useState(0.01); - const [lrf, setLrf] = useState(0.001); - - const handleSubmit = () => { - if (onSubmit) { - onSubmit({ - projectId: numericProjectId, - selectedModel, - ratio, - epochs, - batchSize, - optimizer, - lr0, - lrf, - }); - } - }; - - return ( -
-
- 모델 설정 - - {/* 모델 선택 */} -
- - -
- - {/* 훈련/검증 비율 및 학습 파라미터 */} -
- setRatio(parseFloat(e.target.value))} - /> - setEpochs(parseInt(e.target.value, 10))} - /> - setBatchSize(parseInt(e.target.value, 10))} - /> - - setLr0(parseFloat(e.target.value))} - /> - setLrf(parseFloat(e.target.value))} - /> -
- - -
-
- ); -} - -interface InputWithLabelProps { - label: string; - id: string; - placeholder: string; - value: number; - onChange: (e: React.ChangeEvent) => void; -} - -function InputWithLabel({ label, id, placeholder, value, onChange }: InputWithLabelProps) { - return ( -
- - -
- ); -} - -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 ( -
- - -
- ); -} diff --git a/frontend/src/components/ModelManage/TrainingGraph.tsx b/frontend/src/components/ModelManage/TrainingGraph.tsx new file mode 100644 index 0000000..2072916 --- /dev/null +++ b/frontend/src/components/ModelManage/TrainingGraph.tsx @@ -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 ( + + ); +} diff --git a/frontend/src/components/ModelManage/TrainingSettings.tsx b/frontend/src/components/ModelManage/TrainingSettings.tsx new file mode 100644 index 0000000..a2cdb57 --- /dev/null +++ b/frontend/src/components/ModelManage/TrainingSettings.tsx @@ -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(0.8); + const [epochs, setEpochs] = useState(50); + const [batchSize, setBatchSize] = useState(32); + const [optimizer, setOptimizer] = useState<'SGD' | 'AUTO' | 'ADAM' | 'ADAMW' | 'NADAM' | 'RADAM' | 'RMSPROP'>('AUTO'); + const [lr0, setLr0] = useState(0.01); + const [lrf, setLrf] = useState(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 ( +
+ {' '} + 모델 설정 +
+ ({ + label: model.name, + value: model.id.toString(), + })) || [] + } + placeholder="모델을 선택하세요" + value={selectedModel ? selectedModel.toString() : ''} + onChange={(value) => setSelectedModel(parseInt(value, 10))} + disabled={isTraining} + /> +
+
+ setRatio(parseFloat(e.target.value))} + disabled={isTraining} + /> + setEpochs(parseInt(e.target.value, 10))} + disabled={isTraining} + /> + setBatchSize(parseInt(e.target.value, 10))} + disabled={isTraining} + /> + setOptimizer(value as 'AUTO' | 'SGD' | 'ADAM' | 'ADAMW' | 'NADAM' | 'RADAM' | 'RMSPROP')} + disabled={isTraining} // 학습 중일 때 옵티마이저 선택 비활성화 + /> + setLr0(parseFloat(e.target.value))} + disabled={isTraining} + /> + setLrf(parseFloat(e.target.value))} + disabled={isTraining} + /> +
+ +
+ ); +} diff --git a/frontend/src/components/ModelManage/TrainingTab.tsx b/frontend/src/components/ModelManage/TrainingTab.tsx index 15866df..c2a365a 100644 --- a/frontend/src/components/ModelManage/TrainingTab.tsx +++ b/frontend/src/components/ModelManage/TrainingTab.tsx @@ -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 ( -
-
- - -
+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, + })); -
- ({ - epoch: data.epoch.toString(), - loss1: data.box_loss, - loss2: data.cls_loss, - loss3: data.dfl_loss, - fitness: data.fitness, - }))} - /> -
+ 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 ( +
+ setSelectedModel(numericProjectId?.toString() || '', modelId)} + handleTrainingStart={handleTrainingStart} + handleTrainingStop={handleTrainingStop} + className="h-full" + /> + +
); } diff --git a/frontend/src/components/ModelManage/index.tsx b/frontend/src/components/ModelManage/index.tsx index e1e5938..144645a 100644 --- a/frontend/src/components/ModelManage/index.tsx +++ b/frontend/src/components/ModelManage/index.tsx @@ -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(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 (
@@ -41,22 +24,12 @@ export default function ModelManage() { 모델 평가 - {/* 학습 탭 */} - + - {/* 평가 탭 */} - +
diff --git a/frontend/src/components/WorkspaceBrowseLayout/index.tsx b/frontend/src/components/WorkspaceBrowseLayout/index.tsx index d5def81..018bd50 100644 --- a/frontend/src/components/WorkspaceBrowseLayout/index.tsx +++ b/frontend/src/components/WorkspaceBrowseLayout/index.tsx @@ -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(); diff --git a/frontend/src/components/WorkspaceDropdownMenu/index.tsx b/frontend/src/components/WorkspaceDropdownMenu/index.tsx index 472a159..b558029 100644 --- a/frontend/src/components/WorkspaceDropdownMenu/index.tsx +++ b/frontend/src/components/WorkspaceDropdownMenu/index.tsx @@ -45,7 +45,10 @@ export default function WorkspaceDropdownMenu({ <> - + 파일 업로드 - 폴더 업로드 + 폴더 업로드 (임시) 폴더 압축파일 업로드 @@ -83,7 +86,7 @@ export default function WorkspaceDropdownMenu({ > - + 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 ( -
+

레이블 목록

- {labels.map((label) => ( - - ))} + {image && + labels.map((label) => ( + + ))}
-
- -
); } diff --git a/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx b/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx index c51c465..216631f 100644 --- a/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx +++ b/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx @@ -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 ( -
-
+
+
-
-

{project.type}

+
+

{project.type}

) : ( -
+
{folderData.children.map((item) => ( -
+
// 404 에러 방지 + // 404 에러 방지 ) : ( <> - -
+ const projectReviews = data?.pages.flat() || []; - -
+ const loadMoreRef = useRef(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 ( +
}> +
+
+

프로젝트 리뷰

+ + + +
+ + + + {isFetchingNextPage} + +
+
+ ); } diff --git a/frontend/src/pages/WorkspaceReviewList.tsx b/frontend/src/pages/WorkspaceReviewList.tsx index 92f0a7c..9c01c1c 100644 --- a/frontend/src/pages/WorkspaceReviewList.tsx +++ b/frontend/src/pages/WorkspaceReviewList.tsx @@ -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(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 ( -
-
-

워크스페이스 리뷰

- - - -
- -
+
}> +
+
+

워크스페이스 리뷰

+ + + +
+ + + + {isFetchingNextPage} + +
+
+ ); } diff --git a/frontend/src/queries/auth/useLogoutQuery.ts b/frontend/src/queries/auth/useLogoutQuery.ts new file mode 100644 index 0000000..2379128 --- /dev/null +++ b/frontend/src/queries/auth/useLogoutQuery.ts @@ -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'] }); + }, + }); +} diff --git a/frontend/src/queries/auth/useReissueTokenQuery.ts b/frontend/src/queries/auth/useReissueTokenQuery.ts index 1cba196..3f5a9ae 100644 --- a/frontend/src/queries/auth/useReissueTokenQuery.ts +++ b/frontend/src/queries/auth/useReissueTokenQuery.ts @@ -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'] }); }, }); diff --git a/frontend/src/queries/models/useModelReportsQuery.ts b/frontend/src/queries/models/useModelReportsQuery.ts new file mode 100644 index 0000000..81f3d31 --- /dev/null +++ b/frontend/src/queries/models/useModelReportsQuery.ts @@ -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({ + queryKey: ['modelReports', projectId, modelId], + queryFn: () => getModelReports(projectId, modelId), + }); +} diff --git a/frontend/src/queries/models/useModelResultsQuery.ts b/frontend/src/queries/models/useModelResultsQuery.ts new file mode 100644 index 0000000..124520e --- /dev/null +++ b/frontend/src/queries/models/useModelResultsQuery.ts @@ -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({ + queryKey: ['modelResults', modelId], + queryFn: () => getModelResults(modelId), + }); +} diff --git a/frontend/src/queries/models/usePollingModelReportsQuery.ts b/frontend/src/queries/models/usePollingModelReportsQuery.ts new file mode 100644 index 0000000..85a64ee --- /dev/null +++ b/frontend/src/queries/models/usePollingModelReportsQuery.ts @@ -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({ + queryKey: ['pollingModelReports', projectId, modelId], + queryFn: () => getModelReports(projectId, modelId), + refetchInterval: 5000, + enabled, + }); +} diff --git a/frontend/src/queries/models/useTrainModelQuery.ts b/frontend/src/queries/models/useTrainModelQuery.ts index 822c7e3..4a44d22 100644 --- a/frontend/src/queries/models/useTrainModelQuery.ts +++ b/frontend/src/queries/models/useTrainModelQuery.ts @@ -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), }); } diff --git a/frontend/src/queries/projects/useAutoLabelQuery.ts b/frontend/src/queries/projects/useAutoLabelQuery.ts new file mode 100644 index 0000000..8c0420b --- /dev/null +++ b/frontend/src/queries/projects/useAutoLabelQuery.ts @@ -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), + }); +} diff --git a/frontend/src/queries/reviews/useReviewByStatusQuery.ts b/frontend/src/queries/reviews/useReviewByStatusQuery.ts index d677d8e..97793d7 100644 --- a/frontend/src/queries/reviews/useReviewByStatusQuery.ts +++ b/frontend/src/queries/reviews/useReviewByStatusQuery.ts @@ -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({ 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, }); } diff --git a/frontend/src/queries/workspaces/useWorkspaceReviewsQuery.tsx b/frontend/src/queries/workspaces/useWorkspaceReviewsQuery.tsx index 2e03385..7832914 100644 --- a/frontend/src/queries/workspaces/useWorkspaceReviewsQuery.tsx +++ b/frontend/src/queries/workspaces/useWorkspaceReviewsQuery.tsx @@ -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({ + 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, }); } diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx index d200ab7..35c44a6 100644 --- a/frontend/src/router/index.tsx +++ b/frontend/src/router/index.tsx @@ -47,7 +47,6 @@ const router = createBrowserRouter([ ], }, { - // FIXME: index에서 오류나지 않게 수정 path: webPath.browse(), element: ( }> @@ -66,7 +65,6 @@ const router = createBrowserRouter([ ], }, { - // FIXME: index에서 오류나지 않게 수정 path: `${webPath.workspace()}/:workspaceId`, element: (
}> diff --git a/frontend/src/stores/useAuthStore.ts b/frontend/src/stores/useAuthStore.ts index 0da5f17..fa48c8a 100644 --- a/frontend/src/stores/useAuthStore.ts +++ b/frontend/src/stores/useAuthStore.ts @@ -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()( 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', diff --git a/frontend/src/stores/useCanvasStore.ts b/frontend/src/stores/useCanvasStore.ts index 6b26508..1767735 100644 --- a/frontend/src/stores/useCanvasStore.ts +++ b/frontend/src/stores/useCanvasStore.ts @@ -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()((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] })), diff --git a/frontend/src/stores/useFolderStore.ts b/frontend/src/stores/useFolderStore.ts deleted file mode 100644 index c6d377b..0000000 --- a/frontend/src/stores/useFolderStore.ts +++ /dev/null @@ -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((set) => ({ - folder: null, - loading: false, - error: null, - setFolder: (folder) => set({ folder }), - setLoading: (loading) => set({ loading }), - setError: (error) => set({ error }), -})); - -export default useFolderStore; diff --git a/frontend/src/stores/useModelStore.ts b/frontend/src/stores/useModelStore.ts new file mode 100644 index 0000000..f6ba8a5 --- /dev/null +++ b/frontend/src/stores/useModelStore.ts @@ -0,0 +1,56 @@ +import { create } from 'zustand'; +import { ReportResponse } from '@/types'; + +interface ModelStoreState { + trainingDataByProject: Record; + isTrainingByProject: Record; + selectedModelByProject: Record; + 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((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; diff --git a/frontend/src/stores/useProjectStore.ts b/frontend/src/stores/useProjectStore.ts new file mode 100644 index 0000000..4a5ef93 --- /dev/null +++ b/frontend/src/stores/useProjectStore.ts @@ -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((set) => ({ + project: null, + setProject: (project) => set({ project }), +})); + +export default useProjectStore; diff --git a/frontend/src/stores/useTrainStore.ts b/frontend/src/stores/useTrainStore.ts deleted file mode 100644 index 6aeffd9..0000000 --- a/frontend/src/stores/useTrainStore.ts +++ /dev/null @@ -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((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; diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index d41cf5b..a8f39cd 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -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 {} - -// 카테고리 요청 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; }