Merge branch 'fe/refactor/review-detail' into 'fe/develop'

Refactor: 학습 부분 리팩토링 완료

See merge request s11-s-project/S11P21S002!250
This commit is contained in:
조현수 2024-09-30 16:22:08 +09:00
commit 8ecd21a34b
5 changed files with 122 additions and 55 deletions

View File

@ -1,17 +1,23 @@
import { useEffect, useState } from 'react';
import { Image, Layer, Stage, Line, Rect } from 'react-konva';
import { Image, Layer, Stage, Line, Rect, Text, Group } from 'react-konva';
import useImage from 'use-image';
import { Label, Shape } from '@/types';
import useCommentListQuery from '@/queries/comments/useCommentListQuery';
import { Button } from '@/components/ui/button';
interface ImageWithLabelsProps {
imagePath: string;
labelData: string;
projectId: number;
imageId: number;
}
export default function ImageWithLabels({ imagePath, labelData }: ImageWithLabelsProps) {
export default function ImageWithLabels({ imagePath, labelData, projectId, imageId }: ImageWithLabelsProps) {
const [image] = useImage(imagePath);
const [labels, setLabels] = useState<Label[]>([]);
const [stageDimensions, setStageDimensions] = useState({ width: window.innerWidth, height: window.innerHeight });
const { data: commentList } = useCommentListQuery(projectId, imageId);
const [showComments, setShowComments] = useState(true);
useEffect(() => {
const fetchLabelData = async () => {
@ -55,41 +61,77 @@ export default function ImageWithLabels({ imagePath, labelData }: ImageWithLabel
return { x: scale, y: scale };
};
return image ? (
<Stage
width={stageDimensions.width}
height={stageDimensions.height}
className="overflow-hidden bg-gray-200"
scale={getScale()}
>
<Layer>{image && <Image image={image} />}</Layer>
<Layer>
{labels.map((label) =>
label.type === 'rectangle' ? (
<Rect
key={label.id}
x={label.coordinates[0][0]}
y={label.coordinates[0][1]}
width={label.coordinates[1][0] - label.coordinates[0][0]}
height={label.coordinates[1][1] - label.coordinates[0][1]}
stroke={label.color}
strokeWidth={2}
listening={false}
/>
) : (
<Line
key={label.id}
points={label.coordinates.flat()}
stroke={label.color}
strokeWidth={2}
closed
listening={false}
/>
)
return (
<div>
<Button
variant="outline"
size="sm"
onClick={() => setShowComments((prev) => !prev)}
className="mb-4"
>
{showComments ? '댓글 숨기기' : '댓글 보기'}
</Button>
<Stage
width={stageDimensions.width}
height={stageDimensions.height}
className="overflow-hidden bg-gray-200"
scale={getScale()}
>
<Layer>{image && <Image image={image} />}</Layer>
<Layer>
{labels.map((label) =>
label.type === 'rectangle' ? (
<Rect
key={label.id}
x={label.coordinates[0][0]}
y={label.coordinates[0][1]}
width={label.coordinates[1][0] - label.coordinates[0][0]}
height={label.coordinates[1][1] - label.coordinates[0][1]}
stroke={label.color}
strokeWidth={2}
listening={false}
/>
) : (
<Line
key={label.id}
points={label.coordinates.flat()}
stroke={label.color}
strokeWidth={2}
closed
listening={false}
/>
)
)}
</Layer>
{showComments && (
<Layer>
{commentList?.map((comment) => (
<Group
key={comment.id}
x={comment.positionX}
y={comment.positionY}
>
<Rect
width={150}
height={50}
fill="white"
cornerRadius={10}
shadowBlur={5}
/>
<Text
x={10}
y={10}
text={comment.content}
fontSize={14}
fill="black"
width={130}
listening={false}
/>
</Group>
))}
</Layer>
)}
</Layer>
</Stage>
) : (
<div></div>
</Stage>
</div>
);
}

View File

@ -48,9 +48,13 @@ export default function TrainingSettings({
}
};
const isTraining = selectedModel?.isTrain;
const isWaiting = isPolling && !isTraining;
return (
<fieldset className={cn('grid gap-6 rounded-lg border p-4', className)}>
<legend className="-ml-1 px-1 text-sm font-medium"> </legend>
<div className="grid gap-3">
<SelectWithLabel
label="모델 선택"
@ -69,7 +73,7 @@ export default function TrainingSettings({
}}
/>
</div>
{!selectedModel?.isTrain && (
{!isPolling && !isTraining && (
<>
<div className="grid grid-cols-2 gap-4">
<InputWithLabel
@ -130,19 +134,30 @@ export default function TrainingSettings({
variant="outlinePrimary"
size="lg"
onClick={handleSubmit}
disabled={!selectedModel || isPolling}
disabled={!selectedModel}
>
{isPolling ? '대기 중...' : '학습 시작'}
</Button>
</>
)}
{selectedModel?.isTrain && (
{isWaiting && (
<Button
variant="secondary"
size="lg"
onClick={handleTrainingStop}
>
</Button>
)}
{isTraining && (
<Button
variant="secondary"
size="lg"
onClick={handleTrainingStop}
>
</Button>
)}
</fieldset>

View File

@ -16,9 +16,6 @@ export default function TrainingTab({ projectId }: TrainingTabProps) {
const queryClient = useQueryClient();
const { mutate: startTraining } = useTrainModelQuery(numericProjectId as number, {
onSuccess: () => {
setIsPolling(true);
},
onError: () => {
alert('학습 요청 실패');
setIsPolling(false);
@ -28,24 +25,34 @@ export default function TrainingTab({ projectId }: TrainingTabProps) {
const handleTrainingStart = (trainData: ModelTrainRequest) => {
if (numericProjectId !== null) {
startTraining(trainData);
setIsPolling(true);
}
};
useEffect(() => {
if (!selectedModel || !numericProjectId || !isPolling) return;
const intervalId = setInterval(() => {
queryClient.invalidateQueries({ queryKey: ['projectModels', numericProjectId] });
}, 2000);
const intervalId = setInterval(async () => {
await queryClient.invalidateQueries({ queryKey: ['projectModels', numericProjectId] });
const timeoutId = setTimeout(() => {
clearInterval(intervalId);
setIsPolling(false);
}, 30000);
const models = await queryClient.getQueryData<ModelResponse[]>(['projectModels', numericProjectId]);
const updatedModel = models?.find((model) => model.id === selectedModel.id);
if (updatedModel) {
setSelectedModel(updatedModel);
if (updatedModel.isTrain) {
setIsPolling(true);
} else if (!updatedModel.isTrain && selectedModel.isTrain) {
setIsPolling(false);
setSelectedModel(null);
}
}
}, 2000);
return () => {
clearInterval(intervalId);
clearTimeout(timeoutId);
};
}, [selectedModel, numericProjectId, queryClient, isPolling]);
@ -56,7 +63,7 @@ export default function TrainingTab({ projectId }: TrainingTabProps) {
return (
<div className="grid grid-rows-[auto_1fr] gap-8 md:grid-cols-2">
<TrainingSettings
key={selectedModel?.isTrain ? 'training' : 'settings'}
key={`${selectedModel?.isTrain ? 'training' : 'settings'}-${isPolling}`}
projectId={numericProjectId}
selectedModel={selectedModel}
setSelectedModel={setSelectedModel}
@ -66,7 +73,7 @@ export default function TrainingTab({ projectId }: TrainingTabProps) {
className="h-full"
/>
<TrainingGraph
key={selectedModel?.isTrain ? 'training' : 'graph'}
key={`${selectedModel?.isTrain ? 'training' : 'graph'}-${isPolling}`}
projectId={numericProjectId}
selectedModel={selectedModel}
className="h-full"

View File

@ -95,6 +95,8 @@ export default function ReviewDetail(): JSX.Element {
<ImageWithLabels
imagePath={image.imagePath}
labelData={image.dataPath}
projectId={Number(projectId)}
imageId={image.id}
/>
</div>
))}

View File

@ -5,5 +5,6 @@ export default function useProjectModelsQuery(projectId: number) {
return useSuspenseQuery({
queryKey: ['projectModels', projectId],
queryFn: () => getProjectModels(projectId),
refetchOnWindowFocus: false,
});
}