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

Fix: 리뷰 요청 페이지 에러 수정

See merge request s11-s-project/S11P21S002!243
This commit is contained in:
김진현 2024-09-30 13:31:45 +09:00
commit 4eed4889b3
4 changed files with 102 additions and 75 deletions

View File

@ -1,4 +1,3 @@
// ImageSelection.tsx
import { Label } from '@/components/ui/label';
import useRecursiveSavedImages from '@/hooks/useRecursiveSavedImages';
import { Button } from '@/components/ui/button';
@ -7,27 +6,44 @@ import { ScrollArea } from '@/components/ui/scroll-area';
interface ImageSelectionProps {
projectId: string;
selectedImages: number[];
setSelectedImages: React.Dispatch<React.SetStateAction<number[]>>;
setSelectedImages: (images: number[]) => void;
}
export default function ImageSelection({ projectId, selectedImages, setSelectedImages }: ImageSelectionProps) {
const { allSavedImages } = useRecursiveSavedImages(projectId, 0);
const handleImageSelect = (imageId: number) => {
// 상태 업데이트 안전하게 관리
setSelectedImages((prevSelectedImages) => {
// 이미 선택된 이미지가 있는 경우 필터링하여 제거
if (prevSelectedImages.includes(imageId)) {
return prevSelectedImages.filter((id) => id !== imageId);
const updatedImages = selectedImages.includes(imageId)
? selectedImages.filter((id) => id !== imageId)
: [...selectedImages, imageId];
setSelectedImages(updatedImages);
};
const handleSelectAll = () => {
if (allSavedImages) {
if (selectedImages.length === allSavedImages.length) {
setSelectedImages([]);
} else {
setSelectedImages(allSavedImages.map((image) => image.id));
}
// 선택되지 않은 이미지를 배열에 추가
return [...prevSelectedImages, imageId];
});
}
};
return (
<div className="mb-4">
<Label> ( )</Label>
<div className="mb-2 flex items-center justify-between">
<Label> ( )</Label>
<Button
variant="outline"
size="sm"
onClick={handleSelectAll}
type="button"
className="px-4 py-2"
>
{allSavedImages && selectedImages.length === allSavedImages.length ? '전체 선택 해제' : '전체 선택'}
</Button>
</div>
<ScrollArea className="max-h-64 overflow-auto border p-2">
<ul className="space-y-2">
{allSavedImages && allSavedImages.length > 0 ? (
@ -42,11 +58,12 @@ export default function ImageSelection({ projectId, selectedImages, setSelectedI
<div className="flex items-center space-x-2">
<Button
variant={selectedImages.includes(image.id) ? 'destructive' : 'outline'}
size="xs"
size="sm"
onClick={() => handleImageSelect(image.id)}
className="p-0"
className="px-3 py-1"
type="button"
>
{selectedImages.includes(image.id) ? '선택 해제' : '선택'}
{selectedImages.includes(image.id) ? '해제' : '선택'}
</Button>
</div>
</li>

View File

@ -56,6 +56,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'}
projectId={numericProjectId}
selectedModel={selectedModel}
setSelectedModel={setSelectedModel}
@ -65,6 +66,7 @@ export default function TrainingTab({ projectId }: TrainingTabProps) {
className="h-full"
/>
<TrainingGraph
key={selectedModel?.isTrain ? 'training' : 'graph'}
projectId={numericProjectId}
selectedModel={selectedModel}
className="h-full"

View File

@ -1,38 +1,63 @@
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import ImageSelection from '@/components/ImageSelection';
import { useForm } from 'react-hook-form';
import { Button } from '@/components/ui/button';
import { useNavigate } from 'react-router-dom';
interface ReviewFormProps {
projects: { id: string; title: string }[];
selectedProjectId: string | null;
setSelectedProjectId: (id: string) => void;
selectedImages: number[];
setSelectedImages: React.Dispatch<React.SetStateAction<number[]>>;
onSubmit: (data: { title: string; content: string }) => void;
onSubmit: (data: ReviewFormData) => void;
}
export default function ReviewForm({
projects,
selectedProjectId,
setSelectedProjectId,
selectedImages,
setSelectedImages,
onSubmit,
}: ReviewFormProps): JSX.Element {
const reviewFormSchema = z.object({
projectId: z.string().min(1, '프로젝트를 선택해주세요.'),
title: z.string().min(1, '제목을 입력해주세요.'),
content: z.string().min(1, '내용을 입력해주세요.'),
imageIds: z.array(z.number()),
});
type ReviewFormData = z.infer<typeof reviewFormSchema>;
export default function ReviewForm({ projects, onSubmit }: ReviewFormProps): JSX.Element {
const navigate = useNavigate();
const {
register,
handleSubmit,
formState: { errors },
} = useForm<{ title: string; content: string }>();
setValue,
watch,
} = useForm<ReviewFormData>({
resolver: zodResolver(reviewFormSchema),
defaultValues: {
projectId: '',
title: '',
content: '',
imageIds: [],
},
});
const selectedProjectId = watch('projectId');
const selectedImages = watch('imageIds');
const setSelectedProjectId = (value: string) => {
setValue('projectId', value);
setValue('imageIds', []); // 프로젝트 변경 시 이미지 초기화
};
const setSelectedImages = (images: number[]) => {
setValue('imageIds', images);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-4">
<Label htmlFor="project"> </Label>
<Select onValueChange={(value) => setSelectedProjectId(value)}>
<Select onValueChange={setSelectedProjectId}>
<SelectTrigger id="project">
<SelectValue placeholder="프로젝트를 선택하세요" />
</SelectTrigger>
@ -56,6 +81,7 @@ export default function ReviewForm({
)}
</SelectContent>
</Select>
{errors.projectId && <p className="text-red-500">{errors.projectId.message}</p>}
</div>
<div className="mb-4">
@ -63,7 +89,7 @@ export default function ReviewForm({
<Input
id="title"
placeholder="리뷰 제목을 입력하세요"
{...register('title', { required: '제목을 입력해주세요.' })}
{...register('title')}
/>
{errors.title && <p className="text-red-500">{errors.title.message}</p>}
</div>
@ -73,7 +99,7 @@ export default function ReviewForm({
<Textarea
id="content"
placeholder="리뷰 내용을 입력하세요"
{...register('content', { required: '내용을 입력해주세요.' })}
{...register('content')}
/>
{errors.content && <p className="text-red-500">{errors.content.message}</p>}
</div>
@ -85,6 +111,23 @@ export default function ReviewForm({
setSelectedImages={setSelectedImages}
/>
)}
{errors.imageIds && errors.imageIds.message && <p className="text-red-500">{errors.imageIds.message}</p>}
<div className="actions mt-6 flex justify-end space-x-2">
<Button
variant="destructive"
type="button"
onClick={() => navigate(-1)}
>
</Button>
<Button
variant="default"
type="submit"
>
</Button>
</div>
</form>
);
}

View File

@ -1,48 +1,34 @@
import { Button } from '@/components/ui/button';
import { useNavigate } from 'react-router-dom';
import useCreateReviewQuery from '@/queries/reviews/useCreateReviewQuery';
import useAuthStore from '@/stores/useAuthStore';
import { useParams } from 'react-router-dom';
import ReviewForm from '@/components/ReviewForm';
import useReviewRequest from '@/hooks/useReviewRequest';
import { useState } from 'react';
export default function ReviewRequest(): JSX.Element {
const { profile } = useAuthStore((state) => state);
const memberId = profile?.id ?? 0;
const navigate = useNavigate();
const { workspaceId } = useParams<{ workspaceId?: string }>();
const { projects, selectedProjectId, setSelectedProjectId, selectedImages, setSelectedImages } = useReviewRequest();
const handleSuccess = () => {
navigate(`/admin/${workspaceId}/reviews`);
};
const { projects } = useReviewRequest();
const createReview = useCreateReviewQuery();
const [formData, setFormData] = useState<{ title: string; content: string } | null>(null);
const handleReviewSubmit = (data: { title: string; content: string }) => {
setFormData(data);
};
const handleButtonClick = () => {
if (!formData) return;
const handleReviewSubmit = (data: { projectId: string; title: string; content: string; imageIds: number[] }) => {
const reviewData = {
title: formData.title,
content: formData.content,
imageIds: selectedImages,
title: data.title,
content: data.content,
imageIds: data.imageIds,
};
createReview.mutate(
{
projectId: Number(selectedProjectId),
projectId: Number(data.projectId),
memberId,
reviewData,
},
{
onSuccess: handleSuccess,
onSuccess: () => navigate(`/admin/${workspaceId}/reviews`),
}
);
};
@ -53,29 +39,8 @@ export default function ReviewRequest(): JSX.Element {
<ReviewForm
projects={projects.map((project) => ({ id: project.id.toString(), title: project.title }))}
selectedProjectId={selectedProjectId}
setSelectedProjectId={setSelectedProjectId}
selectedImages={selectedImages}
setSelectedImages={setSelectedImages}
onSubmit={handleReviewSubmit}
/>
<div className="actions mt-6 flex justify-end space-x-2">
<Button
variant="destructive"
type="button"
onClick={() => navigate(-1)}
>
</Button>
<Button
variant="default"
onClick={handleButtonClick} // 버튼 클릭 시에만 mutate 실행
disabled={!selectedProjectId || !formData}
>
</Button>
</div>
</div>
);
}