Merge branch 'fe/develop' into fe/refactor/fcm
This commit is contained in:
commit
9ebd18bfc7
@ -31,7 +31,13 @@ export async function changeImageStatus(
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
|
||||
export async function uploadImageFile(memberId: number, projectId: number, folderId: number, files: File[]) {
|
||||
export async function uploadImageFile(
|
||||
memberId: number,
|
||||
projectId: number,
|
||||
folderId: number,
|
||||
files: File[],
|
||||
processCallback: (progress: number) => void
|
||||
) {
|
||||
const formData = new FormData();
|
||||
files.forEach((file) => {
|
||||
formData.append('imageList', file);
|
||||
@ -40,30 +46,60 @@ export async function uploadImageFile(memberId: number, projectId: number, folde
|
||||
return api
|
||||
.post(`/projects/${projectId}/folders/${folderId}/images/file`, formData, {
|
||||
params: { memberId },
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
processCallback(progress);
|
||||
}
|
||||
},
|
||||
})
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
|
||||
export async function uploadImageFolder(memberId: number, projectId: number, files: File[]) {
|
||||
export async function uploadImageFolder(
|
||||
memberId: number,
|
||||
projectId: number,
|
||||
folderId: number,
|
||||
files: File[],
|
||||
processCallback: (progress: number) => void
|
||||
) {
|
||||
const formData = new FormData();
|
||||
files.forEach((file) => {
|
||||
formData.append('imageList', file);
|
||||
});
|
||||
|
||||
return api
|
||||
.post(`/projects/${projectId}/folders/${0}/images/file`, formData, {
|
||||
.post(`/projects/${projectId}/folders/${folderId}/images/file`, formData, {
|
||||
params: { memberId },
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
processCallback(progress);
|
||||
}
|
||||
},
|
||||
})
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
|
||||
export async function uploadImageZip(memberId: number, projectId: number, file: File) {
|
||||
export async function uploadImageZip(
|
||||
memberId: number,
|
||||
projectId: number,
|
||||
folderId: number,
|
||||
file: File,
|
||||
processCallback: (progress: number) => void
|
||||
) {
|
||||
const formData = new FormData();
|
||||
formData.append('folderZip', file);
|
||||
|
||||
return api
|
||||
.post(`/projects/${projectId}/folders/${0}/images/zip`, formData, {
|
||||
.post(`/projects/${projectId}/folders/${folderId}/images/zip`, formData, {
|
||||
params: { memberId },
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
processCallback(progress);
|
||||
}
|
||||
},
|
||||
})
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button } from '../ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import useAuthStore from '@/stores/useAuthStore';
|
||||
@ -7,10 +7,14 @@ import useUploadImageFileQuery from '@/queries/projects/useUploadImageFileQuery'
|
||||
|
||||
export default function ImageUploadFileForm({
|
||||
onClose,
|
||||
onRefetch,
|
||||
onFileCount,
|
||||
projectId,
|
||||
folderId,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onRefetch?: () => void;
|
||||
onFileCount: (fileCount: number) => void;
|
||||
projectId: number;
|
||||
folderId: number;
|
||||
}) {
|
||||
@ -22,6 +26,7 @@ export default function ImageUploadFileForm({
|
||||
const [isUploading, setIsUploading] = useState<boolean>(false);
|
||||
const [isUploaded, setIsUploaded] = useState<boolean>(false);
|
||||
const [isFailed, setIsFailed] = useState<boolean>(false);
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
|
||||
const uploadImageFile = useUploadImageFileQuery();
|
||||
|
||||
@ -29,6 +34,12 @@ export default function ImageUploadFileForm({
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleRefetch = () => {
|
||||
if (onRefetch) {
|
||||
onRefetch();
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newFiles = event.target.files;
|
||||
|
||||
@ -71,9 +82,13 @@ export default function ImageUploadFileForm({
|
||||
projectId,
|
||||
folderId,
|
||||
files,
|
||||
progressCallback: (progress: number) => {
|
||||
setProgress(progress);
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleRefetch();
|
||||
setIsUploaded(true);
|
||||
},
|
||||
onError: () => {
|
||||
@ -83,6 +98,10 @@ export default function ImageUploadFileForm({
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onFileCount(files.length);
|
||||
}, [files, onFileCount]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
{!isUploading && (
|
||||
@ -115,11 +134,11 @@ export default function ImageUploadFileForm({
|
||||
</div>
|
||||
)}
|
||||
{files.length > 0 && (
|
||||
<ul className="m-0 max-h-[200px] list-none overflow-y-auto p-0">
|
||||
<ul className="m-0 max-h-[260px] list-none overflow-y-auto p-0">
|
||||
{files.map((file, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={cn('flex items-center justify-between p-1')}
|
||||
className="flex items-center justify-between p-1"
|
||||
>
|
||||
<span className="truncate">{file.webkitRelativePath || file.name}</span>
|
||||
{isUploading ? (
|
||||
@ -127,19 +146,19 @@ export default function ImageUploadFileForm({
|
||||
{isUploaded ? (
|
||||
<CircleCheckBig
|
||||
className="stroke-green-500"
|
||||
size={20}
|
||||
size={16}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
) : isFailed ? (
|
||||
<CircleX
|
||||
className="stroke-red-500"
|
||||
size={20}
|
||||
size={16}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
) : (
|
||||
<CircleDashed
|
||||
className="stroke-gray-500"
|
||||
size={20}
|
||||
size={16}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
)}
|
||||
@ -171,7 +190,7 @@ export default function ImageUploadFileForm({
|
||||
}
|
||||
disabled={!isUploaded && !isFailed}
|
||||
>
|
||||
{isFailed ? '업로드 실패 (닫기)' : isUploaded ? '업로드 완료 (닫기)' : '업로드 중...'}
|
||||
{isFailed ? '업로드 실패 (닫기)' : isUploaded ? '업로드 완료 (닫기)' : `업로드 중... ${progress}%`}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
|
@ -4,10 +4,14 @@ import { Plus } from 'lucide-react';
|
||||
import ImageUploadFileForm from './ImageUploadFileForm';
|
||||
|
||||
export default function ImageUploadFileModal({ projectId, folderId }: { projectId: number; folderId: number }) {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [isOpen, setIsOpen] = React.useState<boolean>(false);
|
||||
const [fileCount, setFileCount] = React.useState<number>(0);
|
||||
|
||||
const handleOpen = () => setIsOpen(true);
|
||||
const handleClose = () => setIsOpen(false);
|
||||
const handleFileCount = (fileCount: number) => {
|
||||
setFileCount(fileCount);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@ -23,9 +27,10 @@ export default function ImageUploadFileModal({ projectId, folderId }: { projectI
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader title="파일 업로드" />
|
||||
<DialogHeader title={fileCount > 0 ? `파일 업로드 (${fileCount})` : '파일 업로드'} />
|
||||
<ImageUploadFileForm
|
||||
onClose={handleClose}
|
||||
onFileCount={handleFileCount}
|
||||
projectId={projectId}
|
||||
folderId={folderId}
|
||||
/>
|
||||
|
@ -5,7 +5,17 @@ import useAuthStore from '@/stores/useAuthStore';
|
||||
import { CircleCheckBig, CircleDashed, CircleX, X } from 'lucide-react';
|
||||
import useUploadImageFolderQuery from '@/queries/projects/useUploadImageFolderQuery';
|
||||
|
||||
export default function ImageUploadFolderForm({ onClose, projectId }: { onClose: () => void; projectId: number }) {
|
||||
export default function ImageUploadFolderForm({
|
||||
onClose,
|
||||
onRefetch,
|
||||
projectId,
|
||||
folderId,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onRefetch?: () => void;
|
||||
projectId: number;
|
||||
folderId: number;
|
||||
}) {
|
||||
const profile = useAuthStore((state) => state.profile);
|
||||
const memberId = profile?.id || 0;
|
||||
|
||||
@ -14,6 +24,7 @@ export default function ImageUploadFolderForm({ onClose, projectId }: { onClose:
|
||||
const [isUploading, setIsUploading] = useState<boolean>(false);
|
||||
const [isUploaded, setIsUploaded] = useState<boolean>(false);
|
||||
const [isFailed, setIsFailed] = useState<boolean>(false);
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
|
||||
const uploadImageFolder = useUploadImageFolderQuery();
|
||||
|
||||
@ -21,6 +32,12 @@ export default function ImageUploadFolderForm({ onClose, projectId }: { onClose:
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleRefetch = () => {
|
||||
if (onRefetch) {
|
||||
onRefetch();
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newFiles = event.target.files;
|
||||
|
||||
@ -56,10 +73,15 @@ export default function ImageUploadFolderForm({ onClose, projectId }: { onClose:
|
||||
{
|
||||
memberId,
|
||||
projectId,
|
||||
folderId,
|
||||
files,
|
||||
progressCallback: (progress: number) => {
|
||||
setProgress(progress);
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleRefetch();
|
||||
setIsUploaded(true);
|
||||
},
|
||||
onError: () => {
|
||||
@ -100,11 +122,11 @@ export default function ImageUploadFolderForm({ onClose, projectId }: { onClose:
|
||||
</div>
|
||||
)}
|
||||
{files.length > 0 && (
|
||||
<ul className="m-0 max-h-[200px] list-none overflow-y-auto p-0">
|
||||
<ul className="m-0 max-h-[260px] list-none overflow-y-auto p-0">
|
||||
{files.map((file, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={cn('flex items-center justify-between p-1')}
|
||||
className="flex items-center justify-between p-1"
|
||||
>
|
||||
<span className="truncate">{file.webkitRelativePath || file.name}</span>
|
||||
{isUploading ? (
|
||||
@ -112,19 +134,19 @@ export default function ImageUploadFolderForm({ onClose, projectId }: { onClose:
|
||||
{isUploaded ? (
|
||||
<CircleCheckBig
|
||||
className="stroke-green-500"
|
||||
size={20}
|
||||
size={16}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
) : isFailed ? (
|
||||
<CircleX
|
||||
className="stroke-red-500"
|
||||
size={20}
|
||||
size={16}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
) : (
|
||||
<CircleDashed
|
||||
className="stroke-gray-500"
|
||||
size={20}
|
||||
size={16}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
)}
|
||||
@ -156,7 +178,7 @@ export default function ImageUploadFolderForm({ onClose, projectId }: { onClose:
|
||||
}
|
||||
disabled={!isUploaded && !isFailed}
|
||||
>
|
||||
{isFailed ? '업로드 실패 (닫기)' : isUploaded ? '업로드 완료 (닫기)' : '업로드 중...'}
|
||||
{isFailed ? '업로드 실패 (닫기)' : isUploaded ? '업로드 완료 (닫기)' : `업로드 중... ${progress}%`}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
|
@ -3,7 +3,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialog
|
||||
import { Plus } from 'lucide-react';
|
||||
import ImageUploadFolderForm from './ImageUploadFolderForm';
|
||||
|
||||
export default function ImageUploadFolderModal({ projectId }: { projectId: number }) {
|
||||
export default function ImageUploadFolderModal({ projectId, folderId }: { projectId: number; folderId: number }) {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
const handleOpen = () => setIsOpen(true);
|
||||
@ -27,6 +27,7 @@ export default function ImageUploadFolderModal({ projectId }: { projectId: numbe
|
||||
<ImageUploadFolderForm
|
||||
onClose={handleClose}
|
||||
projectId={projectId}
|
||||
folderId={folderId}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
@ -5,7 +5,17 @@ import useAuthStore from '@/stores/useAuthStore';
|
||||
import { CircleCheckBig, CircleDashed, CircleX, X } from 'lucide-react';
|
||||
import useUploadImageZipQuery from '@/queries/projects/useUploadImageZipQuery';
|
||||
|
||||
export default function ImageUploadZipForm({ onClose, projectId }: { onClose: () => void; projectId: number }) {
|
||||
export default function ImageUploadZipForm({
|
||||
onClose,
|
||||
onRefetch,
|
||||
projectId,
|
||||
folderId,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onRefetch?: () => void;
|
||||
projectId: number;
|
||||
folderId: number;
|
||||
}) {
|
||||
const profile = useAuthStore((state) => state.profile);
|
||||
const memberId = profile?.id || 0;
|
||||
|
||||
@ -14,6 +24,7 @@ export default function ImageUploadZipForm({ onClose, projectId }: { onClose: ()
|
||||
const [isUploading, setIsUploading] = useState<boolean>(false);
|
||||
const [isUploaded, setIsUploaded] = useState<boolean>(false);
|
||||
const [isFailed, setIsFailed] = useState<boolean>(false);
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
|
||||
const uploadImageZip = useUploadImageZipQuery();
|
||||
|
||||
@ -21,6 +32,12 @@ export default function ImageUploadZipForm({ onClose, projectId }: { onClose: ()
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleRefetch = () => {
|
||||
if (onRefetch) {
|
||||
onRefetch();
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newFiles = event.target.files;
|
||||
|
||||
@ -57,10 +74,15 @@ export default function ImageUploadZipForm({ onClose, projectId }: { onClose: ()
|
||||
{
|
||||
memberId,
|
||||
projectId,
|
||||
folderId,
|
||||
file,
|
||||
progressCallback: (progress: number) => {
|
||||
setProgress(progress);
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleRefetch();
|
||||
setIsUploaded(true);
|
||||
},
|
||||
onError: () => {
|
||||
@ -110,19 +132,19 @@ export default function ImageUploadZipForm({ onClose, projectId }: { onClose: ()
|
||||
{isUploaded ? (
|
||||
<CircleCheckBig
|
||||
className="stroke-green-500"
|
||||
size={20}
|
||||
size={16}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
) : isFailed ? (
|
||||
<CircleX
|
||||
className="stroke-red-500"
|
||||
size={20}
|
||||
size={16}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
) : (
|
||||
<CircleDashed
|
||||
className="stroke-gray-500"
|
||||
size={20}
|
||||
size={16}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
)}
|
||||
@ -152,7 +174,7 @@ export default function ImageUploadZipForm({ onClose, projectId }: { onClose: ()
|
||||
}
|
||||
disabled={!isUploaded && !isFailed}
|
||||
>
|
||||
{isFailed ? '업로드 실패 (닫기)' : isUploaded ? '업로드 완료 (닫기)' : '업로드 중...'}
|
||||
{isFailed ? '업로드 실패 (닫기)' : isUploaded ? '업로드 완료 (닫기)' : `업로드 중... ${progress}%`}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
|
@ -3,7 +3,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialog
|
||||
import { Plus } from 'lucide-react';
|
||||
import ImageUploadZipForm from './ImageUploadZipForm';
|
||||
|
||||
export default function ImageUploadZipModal({ projectId }: { projectId: number }) {
|
||||
export default function ImageUploadZipModal({ projectId, folderId }: { projectId: number; folderId: number }) {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
const handleOpen = () => setIsOpen(true);
|
||||
@ -27,6 +27,7 @@ export default function ImageUploadZipModal({ projectId }: { projectId: number }
|
||||
<ImageUploadZipForm
|
||||
onClose={handleClose}
|
||||
projectId={projectId}
|
||||
folderId={folderId}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import ModelLineChart from './ModelLineChart';
|
||||
import usePollingModelReportsQuery from '@/queries/reports/usePollingModelReportsQuery';
|
||||
import usePollingTrainingModelReport from '@/queries/reports/usePollingModelReportsQuery';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { ModelResponse } from '@/types';
|
||||
|
||||
interface TrainingGraphProps {
|
||||
@ -10,20 +11,22 @@ interface TrainingGraphProps {
|
||||
}
|
||||
|
||||
export default function TrainingGraph({ projectId, selectedModel, className }: TrainingGraphProps) {
|
||||
const [isPolling, setIsPolling] = useState(false);
|
||||
const { data: trainingDataList } = usePollingModelReportsQuery(
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: trainingDataList } = usePollingTrainingModelReport(
|
||||
projectId as number,
|
||||
selectedModel?.id as number,
|
||||
isPolling
|
||||
selectedModel?.isTrain || false
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedModel) {
|
||||
setIsPolling(true);
|
||||
} else {
|
||||
setIsPolling(false);
|
||||
if (!selectedModel || !selectedModel.isTrain) {
|
||||
queryClient.resetQueries({
|
||||
queryKey: [{ type: 'modelReports', projectId, modelId: selectedModel?.id }],
|
||||
exact: true,
|
||||
});
|
||||
}
|
||||
}, [selectedModel]);
|
||||
}, [selectedModel, queryClient, projectId]);
|
||||
|
||||
return (
|
||||
<ModelLineChart
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import SelectWithLabel from './SelectWithLabel';
|
||||
import InputWithLabel from './InputWithLabel';
|
||||
import useProjectModelsQuery from '@/queries/models/useProjectModelsQuery';
|
||||
import { ModelTrainRequest, ModelResponse } from '@/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
interface TrainingSettingsProps {
|
||||
projectId: number | null;
|
||||
@ -13,6 +12,7 @@ interface TrainingSettingsProps {
|
||||
setSelectedModel: (model: ModelResponse | null) => void;
|
||||
handleTrainingStart: (trainData: ModelTrainRequest) => void;
|
||||
handleTrainingStop: () => void;
|
||||
isPolling: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ export default function TrainingSettings({
|
||||
setSelectedModel,
|
||||
handleTrainingStart,
|
||||
handleTrainingStop,
|
||||
isPolling,
|
||||
className,
|
||||
}: TrainingSettingsProps) {
|
||||
const { data: models } = useProjectModelsQuery(projectId ?? 0);
|
||||
@ -31,14 +32,9 @@ export default function TrainingSettings({
|
||||
const [optimizer, setOptimizer] = useState<'SGD' | 'AUTO' | 'ADAM' | 'ADAMW' | 'NADAM' | 'RADAM' | 'RMSPROP'>('AUTO');
|
||||
const [lr0, setLr0] = useState<number>(0.01);
|
||||
const [lrf, setLrf] = useState<number>(0.001);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (selectedModel?.isTrain) {
|
||||
handleTrainingStop();
|
||||
} else if (selectedModel) {
|
||||
if (selectedModel) {
|
||||
const trainData: ModelTrainRequest = {
|
||||
modelId: selectedModel.id,
|
||||
ratio,
|
||||
@ -48,34 +44,10 @@ export default function TrainingSettings({
|
||||
lr0,
|
||||
lrf,
|
||||
};
|
||||
setIsSubmitting(true);
|
||||
handleTrainingStart(trainData);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isSubmitting) {
|
||||
intervalRef.current = setInterval(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['projectModels', projectId] });
|
||||
}, 1000);
|
||||
} else if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
};
|
||||
}, [isSubmitting, queryClient, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedModel?.isTrain) {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}, [selectedModel]);
|
||||
|
||||
return (
|
||||
<fieldset className={cn('grid gap-6 rounded-lg border p-4', className)}>
|
||||
<legend className="-ml-1 px-1 text-sm font-medium">모델 설정</legend>
|
||||
@ -158,12 +130,21 @@ export default function TrainingSettings({
|
||||
variant="outlinePrimary"
|
||||
size="lg"
|
||||
onClick={handleSubmit}
|
||||
disabled={!selectedModel || isSubmitting}
|
||||
disabled={!selectedModel || isPolling}
|
||||
>
|
||||
{isSubmitting ? '기다리는 중...' : '학습 시작'}
|
||||
{isPolling ? '대기 중...' : '학습 시작'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{selectedModel?.isTrain && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
onClick={handleTrainingStop}
|
||||
>
|
||||
학습 중단
|
||||
</Button>
|
||||
)}
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
@ -1,24 +1,57 @@
|
||||
import useTrainModelQuery from '@/queries/models/useTrainModelQuery';
|
||||
import { useState, useEffect } from 'react';
|
||||
import TrainingSettings from './TrainingSettings';
|
||||
import TrainingGraph from './TrainingGraph';
|
||||
import useTrainModelQuery from '@/queries/models/useTrainModelQuery';
|
||||
import { ModelTrainRequest, ModelResponse } from '@/types';
|
||||
import { useState } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
interface TrainingTabProps {
|
||||
projectId: number | null;
|
||||
}
|
||||
|
||||
export default function TrainingTab({ projectId }: TrainingTabProps) {
|
||||
const numericProjectId = projectId ? parseInt(projectId.toString(), 10) : null;
|
||||
const numericProjectId = projectId !== null ? Number(projectId) : null;
|
||||
const [selectedModel, setSelectedModel] = useState<ModelResponse | null>(null);
|
||||
const [isPolling, setIsPolling] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: startTraining } = useTrainModelQuery(numericProjectId as number);
|
||||
const { mutate: startTraining } = useTrainModelQuery(numericProjectId as number, {
|
||||
onSuccess: () => {
|
||||
setIsPolling(true);
|
||||
},
|
||||
onError: () => {
|
||||
alert('학습 요청 실패');
|
||||
setIsPolling(false);
|
||||
},
|
||||
});
|
||||
|
||||
const handleTrainingStart = (trainData: ModelTrainRequest) => {
|
||||
startTraining(trainData);
|
||||
if (numericProjectId !== null) {
|
||||
startTraining(trainData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTrainingStop = () => {};
|
||||
useEffect(() => {
|
||||
if (!selectedModel || !numericProjectId || !isPolling) return;
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['projectModels', numericProjectId] });
|
||||
}, 2000);
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
clearInterval(intervalId);
|
||||
setIsPolling(false);
|
||||
}, 30000);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [selectedModel, numericProjectId, queryClient, isPolling]);
|
||||
|
||||
const handleTrainingStop = () => {
|
||||
setIsPolling(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-rows-[auto_1fr] gap-8 md:grid-cols-2">
|
||||
@ -28,9 +61,9 @@ export default function TrainingTab({ projectId }: TrainingTabProps) {
|
||||
setSelectedModel={setSelectedModel}
|
||||
handleTrainingStart={handleTrainingStart}
|
||||
handleTrainingStop={handleTrainingStop}
|
||||
isPolling={isPolling}
|
||||
className="h-full"
|
||||
/>
|
||||
|
||||
<TrainingGraph
|
||||
projectId={numericProjectId}
|
||||
selectedModel={selectedModel}
|
||||
|
@ -15,32 +15,40 @@ import ImageUploadZipForm from '../ImageUploadZipModal/ImageUploadZipForm';
|
||||
export default function WorkspaceDropdownMenu({
|
||||
projectId,
|
||||
folderId,
|
||||
refetch,
|
||||
onRefetch,
|
||||
}: {
|
||||
projectId: number;
|
||||
folderId: number;
|
||||
refetch: () => void;
|
||||
onRefetch: () => void;
|
||||
}) {
|
||||
const [isOpenUploadFile, setIsOpenUploadFile] = React.useState(false);
|
||||
const [isOpenUploadFolder, setIsOpenUploadFolder] = React.useState(false);
|
||||
const [isOpenUploadZip, setIsOpenUploadZip] = React.useState(false);
|
||||
const [isOpenUploadFile, setIsOpenUploadFile] = React.useState<boolean>(false);
|
||||
const [fileCount, setFileCount] = React.useState<number>(0);
|
||||
const [isOpenUploadFolder, setIsOpenUploadFolder] = React.useState<boolean>(false);
|
||||
const [isOpenUploadZip, setIsOpenUploadZip] = React.useState<boolean>(false);
|
||||
|
||||
const handleOpenUploadFile = () => setIsOpenUploadFile(true);
|
||||
|
||||
const handleCloseUploadFile = () => {
|
||||
refetch();
|
||||
setIsOpenUploadFile(false);
|
||||
};
|
||||
|
||||
const handleOpenUploadFolder = () => setIsOpenUploadFolder(true);
|
||||
|
||||
const handleCloseUploadFolder = () => {
|
||||
refetch();
|
||||
setIsOpenUploadFolder(false);
|
||||
};
|
||||
|
||||
const handleOpenUploadZip = () => setIsOpenUploadZip(true);
|
||||
|
||||
const handleCloseUploadZip = () => {
|
||||
refetch();
|
||||
setIsOpenUploadZip(false);
|
||||
};
|
||||
|
||||
const handleFileCount = (fileCount: number) => {
|
||||
console.log(fileCount);
|
||||
setFileCount(fileCount);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
@ -71,9 +79,11 @@ export default function WorkspaceDropdownMenu({
|
||||
>
|
||||
<DialogTrigger asChild></DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader title="파일 업로드" />
|
||||
<DialogHeader title={fileCount > 0 ? `파일 업로드 (${fileCount})` : '파일 업로드'} />
|
||||
<ImageUploadFileForm
|
||||
onClose={handleCloseUploadFile}
|
||||
onRefetch={onRefetch}
|
||||
onFileCount={handleFileCount}
|
||||
projectId={projectId}
|
||||
folderId={folderId}
|
||||
/>
|
||||
@ -89,7 +99,9 @@ export default function WorkspaceDropdownMenu({
|
||||
<DialogHeader title="폴더 업로드 (임시)" />
|
||||
<ImageUploadFolderForm
|
||||
onClose={handleCloseUploadFolder}
|
||||
onRefetch={onRefetch}
|
||||
projectId={projectId}
|
||||
folderId={folderId}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@ -103,7 +115,9 @@ export default function WorkspaceDropdownMenu({
|
||||
<DialogHeader title="폴더 압축파일 업로드" />
|
||||
<ImageUploadZipForm
|
||||
onClose={handleCloseUploadZip}
|
||||
onRefetch={onRefetch}
|
||||
projectId={projectId}
|
||||
folderId={folderId}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
@ -40,7 +40,7 @@ export default function ProjectStructure({ project }: { project: Project }) {
|
||||
<WorkspaceDropdownMenu
|
||||
projectId={project.id}
|
||||
folderId={0}
|
||||
refetch={refetch}
|
||||
onRefetch={refetch}
|
||||
/>
|
||||
</header>
|
||||
{folderData.children.length === 0 && folderData.images.length === 0 ? (
|
||||
|
@ -1,9 +1,24 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { trainModel } from '@/api/modelApi';
|
||||
import { ModelTrainRequest } from '@/types';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
export default function useTrainModelQuery(projectId: number) {
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
interface UseTrainModelOptions {
|
||||
onSuccess?: () => void;
|
||||
onError?: (error: unknown) => void;
|
||||
}
|
||||
|
||||
export default function useTrainModelQuery(projectId: number, options?: UseTrainModelOptions) {
|
||||
return useMutation({
|
||||
mutationFn: (trainData: ModelTrainRequest) => trainModel(projectId, trainData),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['projectModels', projectId] });
|
||||
options?.onSuccess?.();
|
||||
},
|
||||
onError: (error) => {
|
||||
options?.onError?.(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -8,11 +8,13 @@ export default function useUploadImageFileQuery() {
|
||||
projectId,
|
||||
folderId,
|
||||
files,
|
||||
progressCallback,
|
||||
}: {
|
||||
memberId: number;
|
||||
projectId: number;
|
||||
folderId: number;
|
||||
files: File[];
|
||||
}) => uploadImageFile(memberId, projectId, folderId, files),
|
||||
progressCallback: (progress: number) => void;
|
||||
}) => uploadImageFile(memberId, projectId, folderId, files, progressCallback),
|
||||
});
|
||||
}
|
||||
|
@ -3,7 +3,18 @@ import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
export default function useUploadImageFolderQuery() {
|
||||
return useMutation({
|
||||
mutationFn: ({ memberId, projectId, files }: { memberId: number; projectId: number; files: File[] }) =>
|
||||
uploadImageFolder(memberId, projectId, files),
|
||||
mutationFn: ({
|
||||
memberId,
|
||||
projectId,
|
||||
folderId,
|
||||
files,
|
||||
progressCallback,
|
||||
}: {
|
||||
memberId: number;
|
||||
projectId: number;
|
||||
folderId: number;
|
||||
files: File[];
|
||||
progressCallback: (progress: number) => void;
|
||||
}) => uploadImageFolder(memberId, projectId, folderId, files, progressCallback),
|
||||
});
|
||||
}
|
||||
|
@ -3,7 +3,18 @@ import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
export default function useUploadImageZipQuery() {
|
||||
return useMutation({
|
||||
mutationFn: ({ memberId, projectId, file }: { memberId: number; projectId: number; file: File }) =>
|
||||
uploadImageZip(memberId, projectId, file),
|
||||
mutationFn: ({
|
||||
memberId,
|
||||
projectId,
|
||||
folderId,
|
||||
file,
|
||||
progressCallback,
|
||||
}: {
|
||||
memberId: number;
|
||||
projectId: number;
|
||||
folderId: number;
|
||||
file: File;
|
||||
progressCallback: (progress: number) => void;
|
||||
}) => uploadImageZip(memberId, projectId, folderId, file, progressCallback),
|
||||
});
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ export default function usePollingTrainingModelReport(projectId: number, modelId
|
||||
return useQuery<ReportResponse[]>({
|
||||
queryKey: ['modelReports', projectId, modelId],
|
||||
queryFn: () => getTrainingModelReport(projectId, modelId),
|
||||
refetchInterval: 5000,
|
||||
refetchInterval: enabled ? 5000 : false,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user