Merge branch 'fe/develop' into fe/feat/fcm-token
This commit is contained in:
commit
765aa52912
@ -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);
|
||||
}
|
||||
|
12
frontend/src/api/reportApi.ts
Normal file
12
frontend/src/api/reportApi.ts
Normal 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);
|
||||
}
|
6
frontend/src/api/resultApi.ts
Normal file
6
frontend/src/api/resultApi.ts
Normal 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);
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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 />
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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>;
|
||||
|
@ -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,
|
||||
];
|
||||
|
@ -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);
|
||||
// }),
|
||||
// ];
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
10
frontend/src/queries/reports/useCompletedModelReport.ts
Normal file
10
frontend/src/queries/reports/useCompletedModelReport.ts
Normal 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),
|
||||
});
|
||||
}
|
12
frontend/src/queries/reports/usePollingModelReportsQuery.ts
Normal file
12
frontend/src/queries/reports/usePollingModelReportsQuery.ts
Normal 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,
|
||||
});
|
||||
}
|
@ -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),
|
||||
});
|
||||
}
|
@ -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),
|
||||
});
|
||||
}
|
18
frontend/src/queries/reviews/useApproveReviewQuery.ts
Normal file
18
frontend/src/queries/reviews/useApproveReviewQuery.ts
Normal 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] });
|
||||
},
|
||||
});
|
||||
}
|
18
frontend/src/queries/reviews/useRejectReviewQuery.ts
Normal file
18
frontend/src/queries/reviews/useRejectReviewQuery.ts
Normal 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] });
|
||||
},
|
||||
});
|
||||
}
|
@ -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] });
|
||||
},
|
||||
});
|
||||
}
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
@ -309,6 +309,7 @@ export interface ModelResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
isDefault: boolean;
|
||||
isTrain: boolean;
|
||||
}
|
||||
|
||||
// 프로젝트 모델 리스트 응답 DTO
|
||||
|
Loading…
Reference in New Issue
Block a user