diff --git a/src/api/event/short-review.ts b/src/api/event/short-review.ts index e4f0a12..ab6ddef 100644 --- a/src/api/event/short-review.ts +++ b/src/api/event/short-review.ts @@ -72,3 +72,37 @@ export const deleteShortReview = async ( throw err; } }; + +interface ShortReviewReactionResponse { + isSuccess: boolean; + message?: string; + result: { + likeCount: number; + dislikeCount: number; + isLiked: boolean; + isDisliked: boolean; + }; +} + +export const toggleShortReviewReaction = async ( + reviewId: number, + reactionType: 0 | 1, +): Promise => { + try { + // 객체가 아닌 reactionType 값만 직접 전송 + const response = await instance.post( + `/events/short-reviews/${reviewId}/reaction`, + reactionType, + ); + return response.data; + } catch (err: any) { + // 타입을 any로 명시 + console.error('Error toggling short review reaction:', err); + + // 에러 응답 로깅 추가 (타입 가드 사용) + if (err.response) { + console.error('Error response:', err.response.data); + } + throw err; + } +}; diff --git a/src/api/review/WriteReview.ts b/src/api/review/WriteReview.ts index 59fdf5f..fc6b574 100644 --- a/src/api/review/WriteReview.ts +++ b/src/api/review/WriteReview.ts @@ -10,26 +10,47 @@ export const writeReview = async ( images?: File[], ): Promise => { try { - const formData = new FormData(); + // 먼저 이미지 업로드 처리 + let imageUrls: string[] = []; - // JSON 데이터를 request 키로 추가 - formData.append( - 'request', - new Blob([JSON.stringify(reviewData)], { type: 'application/json' }), - ); - - // 이미지 파일들 추가 if (images?.length) { - images.forEach((image) => { - formData.append('image', image); - }); + imageUrls = await Promise.all( + images.map(async (image) => { + const imageFormData = new FormData(); + // 백엔드 요구사항에 맞게 폴더 정보 추가 + imageFormData.append('folder', JSON.stringify({ folder: 'review' })); + imageFormData.append('image', image); + + // instance에 이미 /api가 포함되어 있으므로 /images로 요청 + const imageResponse = await instance.post('/images', imageFormData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + // 이미지 URL 반환 (응답 구조에 맞게 수정 필요) + return imageResponse.data.result; + }), + ); } - const response = await instance.post(REVIEW_API_ENDPOINT, formData, { - headers: { - 'Content-Type': 'multipart/form-data', + // 이미지 URL을 reviewData에 추가 + const updatedReviewData = { + ...reviewData, + imageUrls: imageUrls, // 백엔드 API 요구사항에 맞게 필드명 조정 + }; + + // 리뷰 데이터 전송 (REVIEW_API_ENDPOINT는 '/reviews'이므로 전체 경로는 '/api/reviews') + const response = await instance.post( + REVIEW_API_ENDPOINT, + updatedReviewData, + { + headers: { + 'Content-Type': 'application/json', + }, }, - }); + ); + return response.data; } catch (error) { if (error instanceof AxiosError) { diff --git a/src/api/review/point.ts b/src/api/review/point.ts index 6f07195..13d475e 100644 --- a/src/api/review/point.ts +++ b/src/api/review/point.ts @@ -4,7 +4,5 @@ import instance from '@/api/axios'; import { PointBalanceResponse } from '@/types/review/point'; export const getPointBalance = () => { - return instance - .get('/api/points/balance') - .then((response) => response.data); + return instance.get('/points/balance').then((response) => response.data); }; diff --git a/src/api/review/purchase.ts b/src/api/review/purchase.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/hooks/event/useEventPage.ts b/src/hooks/event/useEventPage.ts index 95d2c7e..6bb76bd 100644 --- a/src/hooks/event/useEventPage.ts +++ b/src/hooks/event/useEventPage.ts @@ -3,7 +3,12 @@ import { useState, useEffect } from 'react'; import { EventDetails } from '@/types/event/details'; import { EventShortReview } from '@/types/event/short-review'; import { getEventDetails } from '@/api/event/details'; -import { createShortReview, updateShortReview, deleteShortReview } from '@/api/event/short-review'; +import { + createShortReview, + updateShortReview, + deleteShortReview, + toggleShortReviewReaction, +} from '@/api/event/short-review'; // EventDetails hook (기존 코드 동일) export const useEventDetails = (eventId: number) => { @@ -45,6 +50,7 @@ export const useReviews = (initialReviews: EventShortReview[]) => { const [editText, setEditText] = useState(''); const [editRating, setEditRating] = useState(0); const [editError, setEditError] = useState(null); + const [isReactionLoading, setIsReactionLoading] = useState(false); const handleEditStart = (review: EventShortReview) => { if (!review || typeof review.id !== 'number') { @@ -120,37 +126,86 @@ export const useReviews = (initialReviews: EventShortReview[]) => { } }; - const handleLike = (reviewId: number) => { - setReviews( - reviews.map((review) => { - if (review.id === reviewId) { - const newLikes = review.userVote === 'like' ? review.likes - 1 : review.likes + 1; - return { - ...review, - likes: newLikes, - userVote: review.userVote === 'like' ? null : 'like', - }; - } - return review; - }), - ); + const handleLike = async (reviewId: number, isLoggedIn: boolean) => { + if (!isLoggedIn) return; + if (isReactionLoading) return; + + try { + setIsReactionLoading(true); + + // 좋아요는 reactionType: 1로 설정 + const response = await toggleShortReviewReaction(reviewId, 1); + + if (response.isSuccess) { + setReviews((prevReviews) => + prevReviews.map((review) => { + if (review.id === reviewId) { + // API 응답에 따라 좋아요/싫어요 상태와 개수를 업데이트 + return { + ...review, + likes: response.result.likeCount, + dislikes: response.result.dislikeCount, + isLiked: response.result.isLiked, + isDisliked: response.result.isDisliked, + userVote: response.result.isLiked + ? 'like' + : response.result.isDisliked + ? 'dislike' + : null, + }; + } + return review; + }), + ); + } else { + console.error('Failed to toggle like:', response.message); + } + } catch (error) { + console.error('Error when liking review:', error); + } finally { + setIsReactionLoading(false); + } }; - const handleDislike = (reviewId: number) => { - setReviews( - reviews.map((review) => { - if (review.id === reviewId) { - const newDislikes = - review.userVote === 'dislike' ? review.dislikes - 1 : review.dislikes + 1; - return { - ...review, - dislikes: newDislikes, - userVote: review.userVote === 'dislike' ? null : 'dislike', - }; - } - return review; - }), - ); + const handleDislike = async (reviewId: number, isLoggedIn: boolean) => { + if (!isLoggedIn) return; + if (isReactionLoading) return; + + try { + setIsReactionLoading(true); + + // 싫어요는 reactionType: 0으로 설정 + const response = await toggleShortReviewReaction(reviewId, 0); + + if (response.isSuccess) { + setReviews((prevReviews) => + prevReviews.map((review) => { + if (review.id === reviewId) { + // API 응답에 따라 좋아요/싫어요 상태와 개수를 업데이트 + return { + ...review, + likes: response.result.likeCount, + dislikes: response.result.dislikeCount, + isLiked: response.result.isLiked, + isDisliked: response.result.isDisliked, + userVote: response.result.isLiked + ? 'like' + : response.result.isDisliked + ? 'dislike' + : null, + }; + } + return review; + }), + ); + } else { + console.error('Failed to toggle dislike:', response.message); + } + } catch (error) { + console.error('Error when disliking review:', error); + } finally { + setIsReactionLoading(false); + } }; return { @@ -168,6 +223,7 @@ export const useReviews = (initialReviews: EventShortReview[]) => { handleDelete, handleLike, handleDislike, + isReactionLoading, }; }; diff --git a/src/pages/EventPage2.tsx b/src/pages/EventPage2.tsx index 0468e1f..f5355f8 100644 --- a/src/pages/EventPage2.tsx +++ b/src/pages/EventPage2.tsx @@ -79,6 +79,7 @@ const EventPage = () => { handleDelete, handleLike, handleDislike, + isReactionLoading, } = useReviews([]); const { @@ -498,14 +499,14 @@ const EventPage = () => { handleLike(review.id, isLoggedIn)} - disabled={!isLoggedIn} + disabled={!isLoggedIn || isReactionLoading} > {review.likes || 0} handleDislike(review.id, isLoggedIn)} - disabled={!isLoggedIn} + disabled={!isLoggedIn || isReactionLoading} > {review.dislikes || 0} diff --git a/src/types/review/purchase.ts b/src/types/review/purchase.ts new file mode 100644 index 0000000..e69de29