Merge branch 'fe/develop' into fe/feat/fcm-token

This commit is contained in:
홍창기 2024-09-27 15:03:50 +09:00
commit 765aa52912
24 changed files with 411 additions and 464 deletions

View File

@ -1,13 +1,5 @@
import api from '@/api/axiosConfig';
import {
ModelRequest,
ModelResponse,
ProjectModelsResponse,
ModelCategoryResponse,
ModelTrainRequest,
ResultResponse,
ReportResponse,
} from '@/types';
import { ModelRequest, ModelResponse, ProjectModelsResponse, ModelCategoryResponse, ModelTrainRequest } from '@/types';
export async function updateModelName(projectId: number, modelId: number, modelData: ModelRequest) {
return api.put<ModelResponse>(`/projects/${projectId}/models/${modelId}`, modelData).then(({ data }) => data);
@ -28,11 +20,3 @@ 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);
}

View File

@ -0,0 +1,12 @@
import api from '@/api/axiosConfig';
import { ReportResponse } from '@/types';
export async function getCompletedModelReport(projectId: number, modelId: number) {
return api.get<ReportResponse[]>(`/projects/${projectId}/reports/models/${modelId}`).then(({ data }) => data);
}
export async function getTrainingModelReport(projectId: number, modelId: number) {
return api
.get<ReportResponse[]>(`/projects/${projectId}/reports/models/${modelId}/progress`)
.then(({ data }) => data);
}

View File

@ -0,0 +1,6 @@
import api from '@/api/axiosConfig';
import { ResultResponse } from '@/types';
export async function getModelResult(modelId: number) {
return api.get<ResultResponse[]>(`/results/model/${modelId}`).then(({ data }) => data);
}

View File

@ -37,18 +37,14 @@ export async function deleteReview(projectId: number, reviewId: number, memberId
.then(({ data }) => data);
}
// 리뷰 상태 변경
export async function updateReviewStatus(projectId: number, reviewId: number, memberId: number, reviewStatus: string) {
return api
.put<ReviewResponse>(
`/projects/${projectId}/reviews/${reviewId}/status`,
{ reviewStatus },
{
params: { memberId },
}
)
.then(({ data }) => data);
export async function approveReview(projectId: number, reviewId: number) {
return api.put(`/projects/${projectId}/reviews/${reviewId}/approve`);
}
export async function rejectReview(projectId: number, reviewId: number) {
return api.put(`/projects/${projectId}/reviews/${reviewId}/reject`);
}
export async function getReviewByStatus(
projectId: number,
memberId: number,

View File

@ -13,6 +13,10 @@ export default function WorkspaceNavigation() {
const activeWorkspaceId = workspaceId ?? workspaces[0]?.id;
if (workspaces.length === 0) {
return <div></div>;
}
return (
<nav className="hidden items-center gap-5 md:flex">
<Link

View File

@ -1,9 +1,10 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
import { Bell } from 'lucide-react';
import { useLocation, Link } from 'react-router-dom';
import UserProfileModal from './UserProfileModal';
import WorkspaceNavigation from './WorkspaceNavigation';
import useAuthStore from '@/stores/useAuthStore';
import { Suspense } from 'react';
export interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
@ -11,6 +12,8 @@ export default function Header({ className, ...props }: HeaderProps) {
const location = useLocation();
const isHomePage = location.pathname === '/';
const profile = useAuthStore((state) => state.profile);
return (
<header
className={cn(
@ -28,10 +31,14 @@ export default function Header({ className, ...props }: HeaderProps) {
WorLabel
</Link>
{!isHomePage && <WorkspaceNavigation />}
{!isHomePage && profile && (
<Suspense fallback={<div></div>}>
<WorkspaceNavigation />
</Suspense>
)}
</div>
{!isHomePage && (
{!isHomePage && profile && (
<div className="flex items-center gap-4 md:gap-5">
<Bell className="h-4 w-4 text-black sm:h-5 sm:w-5" />
<UserProfileModal />

View File

@ -1,8 +1,8 @@
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import useProjectModelsQuery from '@/queries/models/useProjectModelsQuery';
import useModelReportsQuery from '@/queries/models/useModelReportsQuery';
import useModelResultsQuery from '@/queries/models/useModelResultsQuery';
import useCompletedModelReport from '@/queries/reports/useCompletedModelReport';
import useModelResultsQuery from '@/queries/results/useModelResultQuery';
import ModelBarChart from './ModelBarChart';
import ModelLineChart from './ModelLineChart';
import { useState } from 'react';
@ -68,7 +68,7 @@ interface ModelEvaluationProps {
}
function ModelEvaluation({ projectId, selectedModel }: ModelEvaluationProps) {
const { data: reportData } = useModelReportsQuery(projectId, selectedModel);
const { data: reportData } = useCompletedModelReport(projectId, selectedModel);
const { data: resultData } = useModelResultsQuery(selectedModel);
if (!reportData || !resultData) return null;

View File

@ -1,84 +1,29 @@
import { useEffect, useMemo } from 'react';
import { useMemo } from 'react';
import ModelLineChart from './ModelLineChart';
import usePollingModelReportsQuery from '@/queries/models/usePollingModelReportsQuery';
import useModelStore from '@/stores/useModelStore';
import usePollingTrainingModelReport from '@/queries/reports/usePollingModelReportsQuery';
import { ModelResponse } from '@/types';
interface TrainingGraphProps {
projectId: number | null;
selectedModel: number | null;
selectedModel: ModelResponse | null;
className?: string;
}
export default function TrainingGraph({ projectId, selectedModel, className }: TrainingGraphProps) {
const projectKey = projectId?.toString() || '';
const isTraining = selectedModel?.isTrain || false;
const {
isTrainingByProject,
isTrainingCompleteByProject,
setIsTraining,
setIsTrainingComplete,
saveTrainingData,
resetTrainingData,
trainingDataByProject,
selectModel,
} = useModelStore((state) => ({
isTrainingByProject: state.isTrainingByProject,
isTrainingCompleteByProject: state.isTrainingCompleteByProject,
setIsTraining: state.setIsTraining,
setIsTrainingComplete: state.setIsTrainingComplete,
saveTrainingData: state.saveTrainingData,
resetTrainingData: state.resetTrainingData,
trainingDataByProject: state.trainingDataByProject,
selectModel: state.selectModel,
}));
const isTraining = isTrainingByProject[projectKey] || false;
const isTrainingComplete = isTrainingCompleteByProject[projectKey] || false;
useEffect(() => {
if (projectId !== null) {
selectModel(projectKey, selectedModel);
}
}, [selectedModel, projectId, projectKey, selectModel]);
const { data: fetchedTrainingDataList } = usePollingModelReportsQuery(
const { data: fetchedTrainingDataList } = usePollingTrainingModelReport(
projectId as number,
selectedModel as number,
isTraining && !!projectId && !!selectedModel
selectedModel?.id as number,
isTraining
);
const trainingDataList = useMemo(() => {
if (!isTraining) {
return [];
}
return trainingDataByProject[projectKey] || fetchedTrainingDataList || [];
}, [isTraining, projectKey, trainingDataByProject, fetchedTrainingDataList]);
useEffect(() => {
if (fetchedTrainingDataList) {
saveTrainingData(projectKey, fetchedTrainingDataList);
}
}, [fetchedTrainingDataList, projectKey, saveTrainingData]);
useEffect(() => {
if (isTraining && trainingDataList.length > 0) {
const latestData = trainingDataList[trainingDataList.length - 1];
if (latestData.epoch === latestData.totalEpochs && latestData.totalEpochs > 0) {
setIsTrainingComplete(projectKey, true);
} else {
setIsTrainingComplete(projectKey, false);
}
}
}, [trainingDataList, setIsTrainingComplete, projectKey, isTraining]);
useEffect(() => {
if (isTrainingComplete) {
alert('학습이 완료되었습니다!');
setIsTraining(projectKey, false);
resetTrainingData(projectKey);
setIsTrainingComplete(projectKey, false);
}
}, [isTrainingComplete, setIsTraining, resetTrainingData, setIsTrainingComplete, projectKey]);
return fetchedTrainingDataList || [];
}, [isTraining, fetchedTrainingDataList]);
return (
<ModelLineChart

View File

@ -2,15 +2,14 @@ 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 { ModelTrainRequest, ModelResponse } from '@/types';
import { useState } from 'react';
import { cn } from '@/lib/utils';
interface TrainingSettingsProps {
projectId: number | null;
selectedModel: number | null;
setSelectedModel: (model: number | null) => void;
selectedModel: ModelResponse | null;
setSelectedModel: (model: ModelResponse | null) => void;
handleTrainingStart: (trainData: ModelTrainRequest) => void;
handleTrainingStop: () => void;
className?: string;
@ -25,9 +24,6 @@ export default function TrainingSettings({
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);
@ -36,11 +32,11 @@ export default function TrainingSettings({
const [lrf, setLrf] = useState<number>(0.001);
const handleSubmit = () => {
if (isTraining) {
if (selectedModel?.isTrain) {
handleTrainingStop();
} else if (selectedModel !== null) {
} else if (selectedModel) {
const trainData: ModelTrainRequest = {
modelId: selectedModel,
modelId: selectedModel.id,
ratio,
epochs,
batch: batchSize,
@ -54,7 +50,6 @@ export default function TrainingSettings({
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
@ -62,83 +57,85 @@ export default function TrainingSettings({
id="model"
options={
models?.map((model) => ({
label: model.name,
label: `${model.name}${model.isTrain ? ' (학습 중)' : ''}${model.isDefault ? ' (기본)' : ''}`,
value: model.id.toString(),
})) || []
}
placeholder="모델을 선택하세요"
value={selectedModel ? selectedModel.toString() : ''}
onChange={(value) => setSelectedModel(parseInt(value, 10))}
disabled={isTraining}
value={selectedModel ? selectedModel.id.toString() : ''}
onChange={(value) => {
const selected = models?.find((model) => model.id === parseInt(value, 10));
setSelectedModel(selected || null);
}}
/>
</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>
{!selectedModel?.isTrain && (
<>
<div className="grid grid-cols-2 gap-4">
<InputWithLabel
label="훈련/검증 비율"
id="ratio"
value={ratio}
onChange={(e) => setRatio(parseFloat(e.target.value))}
placeholder="훈련/검증 비율"
/>
<InputWithLabel
label="에포크 수"
id="epochs"
value={epochs}
onChange={(e) => setEpochs(parseInt(e.target.value, 10))}
placeholder="에포크 수"
/>
<InputWithLabel
label="Batch 크기"
id="batch"
value={batchSize}
onChange={(e) => setBatchSize(parseInt(e.target.value, 10))}
placeholder="Batch 크기"
/>
<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' },
]}
value={optimizer}
onChange={(value) =>
setOptimizer(value as 'AUTO' | 'SGD' | 'ADAM' | 'ADAMW' | 'NADAM' | 'RADAM' | 'RMSPROP')
}
placeholder="옵티마이저"
/>
<InputWithLabel
label="학습률(LR0)"
id="lr0"
value={lr0}
onChange={(e) => setLr0(parseFloat(e.target.value))}
placeholder="초기 학습률"
/>
<InputWithLabel
label="최종 학습률(LRF)"
id="lrf"
value={lrf}
onChange={(e) => setLrf(parseFloat(e.target.value))}
placeholder="최종 학습률"
/>
</div>
<Button
variant="outlinePrimary"
size="lg"
onClick={handleSubmit}
disabled={!selectedModel}
>
{selectedModel?.isTrain ? '학습 중단' : '학습 시작'}
</Button>
</>
)}
</fieldset>
);
}

View File

@ -1,50 +1,31 @@
import useTrainModelQuery from '@/queries/models/useTrainModelQuery';
import useModelStore from '@/stores/useModelStore';
import TrainingSettings from './TrainingSettings';
import TrainingGraph from './TrainingGraph';
import { ModelTrainRequest } from '@/types';
import { ModelTrainRequest, ModelResponse } from '@/types';
import { useState } from 'react';
interface TrainingTabProps {
projectId: number | null;
}
//Todo : 로직 수정, isTrain을 서버 단에서 받고, 셀렉트 됐을 때 개별 조회로 isTrain을 판단해서, 학습 중이면 리패치하는 방식으로 관리한다.
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.selectModel,
resetTrainingData: state.resetTrainingData,
}));
const projectKey = numericProjectId?.toString() || '';
const isTraining = isTrainingByProject[projectKey] || false;
const selectedModel = selectedModelByProject[projectKey];
const [selectedModel, setSelectedModel] = useState<ModelResponse | null>(null);
const { mutate: startTraining } = useTrainModelQuery(numericProjectId as number);
const handleTrainingStart = (trainData: ModelTrainRequest) => {
if (!isTraining && selectedModel !== null) {
setIsTraining(projectKey, true);
startTraining(trainData);
}
startTraining(trainData);
};
const handleTrainingStop = () => {
if (isTraining) {
setIsTraining(projectKey, false);
resetTrainingData(projectKey);
}
};
const handleTrainingStop = () => {};
return (
<div className="grid grid-rows-[auto_1fr] gap-8 md:grid-cols-2">
<TrainingSettings
projectId={numericProjectId}
selectedModel={selectedModel}
setSelectedModel={(modelId) => setSelectedModel(projectKey, modelId)}
setSelectedModel={setSelectedModel}
handleTrainingStart={handleTrainingStart}
handleTrainingStop={handleTrainingStop}
className="h-full"

View File

@ -6,10 +6,14 @@ import { Input } from '../ui/input';
import { Button } from '../ui/button';
const formSchema = z.object({
workspaceName: z.string().max(50).min(1, {
message: '이름을 입력해주세요.',
}),
workspaceDescription: z.string().max(200).optional(),
workspaceName: z
.string()
.min(1, { message: '이름을 입력해주세요.' })
.max(50, { message: '이름은 50자 이내여야 합니다.' }),
workspaceDescription: z
.string()
.min(1, { message: '설명을 입력해주세요.' })
.max(200, { message: '설명은 200자 이내여야 합니다.' }),
});
export type WorkSpaceCreateFormValues = z.infer<typeof formSchema>;

View File

@ -5,7 +5,7 @@ import { categoryHandlers } from './categoryHandlers';
import { memberHandlers } from './memberHandlers';
import { workspaceHandlers } from './workspaceHandlers';
import { folderHandlers } from './folderHandler';
import { modelHandlers } from './modelHandlers';
// import { modelHandlers } from './modelHandlers';
import { imageHandlers } from './imageHandlers';
import { projectHandlers } from './projectHandlers';
@ -18,7 +18,7 @@ export const handlers = [
...memberHandlers,
...workspaceHandlers,
...folderHandlers,
...modelHandlers,
// ...modelHandlers,
...imageHandlers,
...projectHandlers,
];

View File

@ -1,203 +1,203 @@
import { http, HttpResponse } from 'msw';
import {
ModelRequest,
ModelResponse,
ProjectModelsResponse,
ModelCategoryResponse,
ModelTrainRequest,
ResultResponse,
ReportResponse,
} from '@/types';
// 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;
// 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,
isDefault: false,
};
// const updatedModel: ModelResponse = {
// id: modelId,
// name: modelData.name,
// isDefault: false,
// };
return HttpResponse.json(updatedModel);
}),
// 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);
// // 모델 학습 핸들러
// 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;
// const trainData = (await request.json()) as ModelTrainRequest;
return HttpResponse.json({
message: `Model training started for project ${projectId}`,
trainData,
});
}),
// 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);
// // 프로젝트의 모델 리스트 조회 핸들러
// 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', isDefault: true },
{ id: 2, name: 'Model 2', isDefault: false },
];
// const models: ProjectModelsResponse = [
// { id: 1, name: 'Model 1', isDefault: true },
// { id: 2, name: 'Model 2', isDefault: false },
// ];
return HttpResponse.json(models);
}),
// 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);
// // 모델 추가 핸들러
// 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 modelData = (await request.json()) as ModelRequest;
// console.log(projectId);
const newModel: ModelResponse = {
id: Math.floor(Math.random() * 1000), // 임의로 ID 생성
name: modelData.name,
isDefault: false,
};
// const newModel: ModelResponse = {
// id: Math.floor(Math.random() * 1000), // 임의로 ID 생성
// name: modelData.name,
// isDefault: false,
// };
return HttpResponse.json(newModel);
}),
// 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' },
];
// // 모델 카테고리 조회 핸들러
// 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);
}),
// 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,
},
];
// // 모델 결과 조회 핸들러
// 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);
}),
// 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,
},
];
// // 모델 보고서 조회 핸들러
// 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);
}),
];
// return HttpResponse.json(reports);
// }),
// ];

View File

@ -2,7 +2,8 @@ import { useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import Slider from 'react-slick';
import useReviewDetailQuery from '@/queries/reviews/useReviewDetailQuery';
import useUpdateReviewStatusQuery from '@/queries/reviews/useUpdateReviewStatusQuery';
import useApproveReviewQuery from '@/queries/reviews/useApproveReviewQuery';
import useRejectReviewQuery from '@/queries/reviews/useRejectReviewQuery';
import useAuthStore from '@/stores/useAuthStore';
import { Button } from '@/components/ui/button';
import 'slick-carousel/slick/slick.css';
@ -20,38 +21,24 @@ export default function ReviewDetail(): JSX.Element {
const { data: reviewDetail } = useReviewDetailQuery(Number(projectId), Number(reviewId), memberId);
const updateReviewStatus = useUpdateReviewStatusQuery();
const approveReviewMutation = useApproveReviewQuery({ projectId: Number(projectId), reviewId: Number(reviewId) });
const rejectReviewMutation = useRejectReviewQuery({ projectId: Number(projectId), reviewId: Number(reviewId) });
const [activeTab, setActiveTab] = useState<'content' | 'images'>('content');
const [isReviewed, setIsReviewed] = useState(
reviewDetail?.reviewStatus === 'APPROVED' || reviewDetail?.reviewStatus === 'REJECTED'
);
const handleApprove = () => {
updateReviewStatus.mutate(
{
projectId: Number(projectId),
reviewId: Number(reviewId),
memberId,
reviewStatus: 'APPROVED',
},
{
onSuccess: () => setIsReviewed(true),
}
);
approveReviewMutation.mutate(undefined, {
onSuccess: () => setIsReviewed(true),
});
};
const handleReject = () => {
updateReviewStatus.mutate(
{
projectId: Number(projectId),
reviewId: Number(reviewId),
memberId,
reviewStatus: 'REJECTED',
},
{
onSuccess: () => setIsReviewed(true),
}
);
rejectReviewMutation.mutate(undefined, {
onSuccess: () => setIsReviewed(true),
});
};
const settings = {
@ -144,7 +131,7 @@ export default function ReviewDetail(): JSX.Element {
variant="default"
onClick={handleApprove}
>
{'승인'}
</Button>
)}
{reviewDetail.reviewStatus !== 'REJECTED' && (
@ -152,7 +139,7 @@ export default function ReviewDetail(): JSX.Element {
variant="destructive"
onClick={handleReject}
>
{'거부'}
</Button>
)}
</div>

View File

@ -1,12 +0,0 @@
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,
});
}

View File

@ -0,0 +1,10 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import { getCompletedModelReport } from '@/api/reportApi';
import { ReportResponse } from '@/types';
export default function useCompletedModelReport(projectId: number, modelId: number) {
return useSuspenseQuery<ReportResponse[]>({
queryKey: ['modelReport', projectId, modelId],
queryFn: () => getCompletedModelReport(projectId, modelId),
});
}

View File

@ -0,0 +1,12 @@
import { useQuery } from '@tanstack/react-query';
import { getTrainingModelReport } from '@/api/reportApi';
import { ReportResponse } from '@/types';
export default function usePollingTrainingModelReport(projectId: number, modelId: number, enabled: boolean) {
return useQuery<ReportResponse[]>({
queryKey: ['modelReports', projectId, modelId],
queryFn: () => getTrainingModelReport(projectId, modelId),
refetchInterval: 5000,
enabled,
});
}

View File

@ -1,10 +1,10 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import { getModelReports } from '@/api/modelApi';
import { getTrainingModelReport } from '@/api/reportApi';
import { ReportResponse } from '@/types';
export default function useModelReportsQuery(projectId: number, modelId: number) {
export default function useTrainingModelReport(projectId: number, modelId: number) {
return useSuspenseQuery<ReportResponse[]>({
queryKey: ['modelReports', projectId, modelId],
queryFn: () => getModelReports(projectId, modelId),
queryFn: () => getTrainingModelReport(projectId, modelId),
});
}

View File

@ -1,10 +1,10 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import { getModelResults } from '@/api/modelApi';
import { getModelResult } from '@/api/resultApi';
import { ResultResponse } from '@/types';
export default function useModelResultsQuery(modelId: number) {
return useSuspenseQuery<ResultResponse[]>({
queryKey: ['modelResults', modelId],
queryFn: () => getModelResults(modelId),
queryFn: () => getModelResult(modelId),
});
}

View File

@ -0,0 +1,18 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { approveReview } from '@/api/reviewApi';
interface ReviewStatusChangeProps {
projectId: number;
reviewId: number;
}
export default function useApproveReviewQuery({ projectId, reviewId }: ReviewStatusChangeProps) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => approveReview(projectId, reviewId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reviewDetail', reviewId] });
},
});
}

View File

@ -0,0 +1,18 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { rejectReview } from '@/api/reviewApi';
interface ReviewStatusChangeProps {
projectId: number;
reviewId: number;
}
export default function useRejectReviewQuery({ projectId, reviewId }: ReviewStatusChangeProps) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => rejectReview(projectId, reviewId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reviewDetail', reviewId] });
},
});
}

View File

@ -1,23 +0,0 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { updateReviewStatus } from '@/api/reviewApi';
export default function useUpdateReviewStatusQuery() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
projectId,
reviewId,
memberId,
reviewStatus,
}: {
projectId: number;
reviewId: number;
memberId: number;
reviewStatus: string;
}) => updateReviewStatus(projectId, reviewId, memberId, reviewStatus),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['reviewDetail', variables.projectId, variables.reviewId] });
},
});
}

View File

@ -3,7 +3,7 @@ import { useSuspenseQuery } from '@tanstack/react-query';
export default function useWorkspaceListQuery(memberId: number, lastWorkspaceId?: number, limit?: number) {
return useSuspenseQuery({
queryKey: ['workspaceList'],
queryKey: ['workspaceList', memberId, lastWorkspaceId, limit],
queryFn: () => getWorkspaceList(memberId, lastWorkspaceId, limit),
});
}

View File

@ -309,6 +309,7 @@ export interface ModelResponse {
id: number;
name: string;
isDefault: boolean;
isTrain: boolean;
}
// 프로젝트 모델 리스트 응답 DTO