Refactor: 리뷰 제출

This commit is contained in:
정현조 2024-09-27 01:43:27 +09:00
parent b7019dbf28
commit 101ceae953
6 changed files with 239 additions and 110 deletions

View File

@ -1,5 +1,6 @@
// ImageSelection.tsx
import { Label } from '@/components/ui/label';
import useFolderQuery from '@/queries/folders/useFolderQuery';
import useRecursiveSavedImages from '@/hooks/useRecursiveSavedImages';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
@ -10,13 +11,18 @@ interface ImageSelectionProps {
}
export default function ImageSelection({ projectId, selectedImages, setSelectedImages }: ImageSelectionProps) {
const { data: folderData } = useFolderQuery(projectId, 0);
const savedImages = folderData?.images.filter((image) => image.status === 'SAVE') || [];
const { allSavedImages } = useRecursiveSavedImages(projectId, 0);
const handleImageSelect = (imageId: number) => {
setSelectedImages((prev: number[]) =>
prev.includes(imageId) ? prev.filter((id) => id !== imageId) : [...prev, imageId]
);
// 상태 업데이트 안전하게 관리
setSelectedImages((prevSelectedImages) => {
// 이미 선택된 이미지가 있는 경우 필터링하여 제거
if (prevSelectedImages.includes(imageId)) {
return prevSelectedImages.filter((id) => id !== imageId);
}
// 선택되지 않은 이미지를 배열에 추가
return [...prevSelectedImages, imageId];
});
};
return (
@ -24,8 +30,8 @@ export default function ImageSelection({ projectId, selectedImages, setSelectedI
<Label> ( )</Label>
<ScrollArea className="max-h-64 overflow-auto border p-2">
<ul className="space-y-2">
{savedImages.length > 0 ? (
savedImages.map((image) => (
{allSavedImages && allSavedImages.length > 0 ? (
allSavedImages.map((image) => (
<li
key={image.id}
className={`relative flex items-center justify-between border p-2 ${
@ -34,16 +40,14 @@ export default function ImageSelection({ projectId, selectedImages, setSelectedI
>
<span className="truncate">{image.imageTitle}</span>
<div className="flex items-center space-x-2">
{selectedImages.includes(image.id) && (
<Button
variant="destructive"
size="xs"
onClick={() => handleImageSelect(image.id)}
className="p-0"
>
X
</Button>
)}
<Button
variant={selectedImages.includes(image.id) ? 'destructive' : 'outline'}
size="xs"
onClick={() => handleImageSelect(image.id)}
className="p-0"
>
{selectedImages.includes(image.id) ? '선택 해제' : '선택'}
</Button>
</div>
</li>
))

View File

@ -0,0 +1,90 @@
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';
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;
}
export default function ReviewForm({
projects,
selectedProjectId,
setSelectedProjectId,
selectedImages,
setSelectedImages,
onSubmit,
}: ReviewFormProps): JSX.Element {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<{ title: string; content: string }>();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-4">
<Label htmlFor="project"> </Label>
<Select onValueChange={(value) => setSelectedProjectId(value)}>
<SelectTrigger id="project">
<SelectValue placeholder="프로젝트를 선택하세요" />
</SelectTrigger>
<SelectContent>
{projects.length ? (
projects.map((project) => (
<SelectItem
key={project.id}
value={project.id}
>
{project.title}
</SelectItem>
))
) : (
<SelectItem
disabled
value={'true'}
>
</SelectItem>
)}
</SelectContent>
</Select>
</div>
<div className="mb-4">
<Label htmlFor="title"></Label>
<Input
id="title"
placeholder="리뷰 제목을 입력하세요"
{...register('title', { required: '제목을 입력해주세요.' })}
/>
{errors.title && <p className="text-red-500">{errors.title.message}</p>}
</div>
<div className="mb-4">
<Label htmlFor="content"></Label>
<Textarea
id="content"
placeholder="리뷰 내용을 입력하세요"
{...register('content', { required: '내용을 입력해주세요.' })}
/>
{errors.content && <p className="text-red-500">{errors.content.message}</p>}
</div>
{selectedProjectId && (
<ImageSelection
projectId={selectedProjectId}
selectedImages={selectedImages}
setSelectedImages={setSelectedImages}
/>
)}
</form>
);
}

View File

@ -0,0 +1,39 @@
import { useCallback, useEffect, useState } from 'react';
import useFolderQuery from '@/queries/folders/useFolderQuery';
import { ImageResponse, ChildFolder } from '@/types';
import { getFolder } from '@/api/folderApi';
export default function useRecursiveSavedImages(projectId: string, folderId: number) {
const [allSavedImages, setAllSavedImages] = useState<ImageResponse[]>([]);
const { data: folderData, isLoading, error } = useFolderQuery(projectId, folderId);
const fetchAllSavedImages = useCallback(
async (folderId: number): Promise<ImageResponse[]> => {
const folder = await getFolder(projectId, folderId);
const childFolderImagesPromises = folder.children.map(async (childFolder: ChildFolder) => {
return fetchAllSavedImages(childFolder.id);
});
const childFolderImages = await Promise.all(childFolderImagesPromises);
const savedImages = folder.images.filter((image) => image.status === 'SAVE');
console.log(savedImages);
return [...savedImages, ...childFolderImages.flat()];
},
[projectId]
);
useEffect(() => {
const getAllSavedImages = async () => {
if (folderData) {
const images = await fetchAllSavedImages(folderId);
setAllSavedImages(images);
}
};
getAllSavedImages();
}, [folderData, fetchAllSavedImages, folderId]);
return { allSavedImages, isLoading, error };
}

View File

@ -1,104 +1,81 @@
import { Button } from '@/components/ui/button';
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 useReviewRequest from '@/hooks/useReviewRequest';
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 {
register,
handleSubmit,
errors,
projects,
onSubmit,
selectedProjectId,
setSelectedProjectId,
selectedImages,
setSelectedImages,
} = useReviewRequest();
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 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 reviewData = {
title: formData.title,
content: formData.content,
imageIds: selectedImages,
};
createReview.mutate(
{
projectId: Number(selectedProjectId),
memberId,
reviewData,
},
{
onSuccess: handleSuccess,
}
);
};
return (
<div className="review-request-container p-4">
<h1 className="mb-4 text-2xl font-bold"> </h1>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-4">
<Label htmlFor="project"> </Label>
<Select onValueChange={(value) => setSelectedProjectId(value)}>
<SelectTrigger id="project">
<SelectValue placeholder="프로젝트를 선택하세요" />
</SelectTrigger>
<SelectContent>
{projects.length ? (
projects.map((project) => (
<SelectItem
key={project.id}
value={project.id.toString()}
>
{project.title}
</SelectItem>
))
) : (
<SelectItem
disabled
value={'true'}
>
</SelectItem>
)}
</SelectContent>
</Select>
</div>
<ReviewForm
projects={projects.map((project) => ({ id: project.id.toString(), title: project.title }))}
selectedProjectId={selectedProjectId}
setSelectedProjectId={setSelectedProjectId}
selectedImages={selectedImages}
setSelectedImages={setSelectedImages}
onSubmit={handleReviewSubmit}
/>
<div className="mb-4">
<Label htmlFor="title"></Label>
<Input
id="title"
placeholder="리뷰 제목을 입력하세요"
{...register('title', { required: '제목을 입력해주세요.' })}
/>
{errors.title && <p className="text-red-500">{errors.title.message}</p>}
</div>
<div className="mb-4">
<Label htmlFor="content"></Label>
<Textarea
id="content"
placeholder="리뷰 내용을 입력하세요"
{...register('content', { required: '내용을 입력해주세요.' })}
/>
{errors.content && <p className="text-red-500">{errors.content.message}</p>}
</div>
{selectedProjectId && (
<ImageSelection
projectId={selectedProjectId}
selectedImages={selectedImages}
setSelectedImages={setSelectedImages}
/>
)}
<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"
disabled={!selectedProjectId}
>
</Button>
</div>
</form>
<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>
);
}

View File

@ -0,0 +1,18 @@
import { useMutation } from '@tanstack/react-query';
import { changeImageStatus } from '@/api/imageApi';
import { ImageStatusChangeRequest } from '@/types';
export default function useChangeImageStatusQuery(onSuccess?: () => void) {
return useMutation({
mutationFn: ({
imageId,
memberId,
statusChangeRequest,
}: {
imageId: number;
memberId: number;
statusChangeRequest: ImageStatusChangeRequest;
}) => changeImageStatus(imageId, memberId, statusChangeRequest),
onSuccess,
});
}

View File

@ -15,8 +15,9 @@ export default function useCreateReviewQuery() {
memberId: number;
reviewData: ReviewRequest;
}) => createReview(projectId, memberId, reviewData),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['reviewList', variables.projectId, variables.memberId] });
queryClient.invalidateQueries({ queryKey: ['reviewDetail', variables.projectId] });
},
});
}