From 2b3023ea769ae9943152ed2a2b3abe3848632645 Mon Sep 17 00:00:00 2001 From: kim minseo Date: Fri, 21 Feb 2025 10:49:41 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=95=9C=EC=A4=84=20=ED=9B=84?= =?UTF-8?q?=EA=B8=B0=20=20=EC=A2=8B=EC=95=84=EC=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/event/short-review.ts | 27 ++++++++ src/api/review/point.ts | 4 +- src/api/review/purchase.ts | 0 src/hooks/event/useEventPage.ts | 116 +++++++++++++++++++++++--------- src/pages/EventPage2.tsx | 5 +- src/pages/ReviewPage7.tsx | 2 +- src/types/review/purchase.ts | 0 7 files changed, 118 insertions(+), 36 deletions(-) create mode 100644 src/api/review/purchase.ts create mode 100644 src/types/review/purchase.ts diff --git a/src/api/event/short-review.ts b/src/api/event/short-review.ts index e4f0a12..db3efa0 100644 --- a/src/api/event/short-review.ts +++ b/src/api/event/short-review.ts @@ -72,3 +72,30 @@ 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 { + const response = await instance.post( + `/events/short-reviews/${reviewId}/reaction`, + { reactionType }, + ); + return response.data; + } catch (err) { + console.error('Error toggling short review reaction:', err); + throw err; + } +}; 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/pages/ReviewPage7.tsx b/src/pages/ReviewPage7.tsx index 2984471..457a67c 100644 --- a/src/pages/ReviewPage7.tsx +++ b/src/pages/ReviewPage7.tsx @@ -188,7 +188,7 @@ const ReviewPage7 = () => { if (response.isSuccess) { alert('리뷰가 성공적으로 등록되었습니다.'); - navigate('/reviews'); + navigate('/review6'); } } catch (error) { console.error('리뷰 등록 실패:', error); diff --git a/src/types/review/purchase.ts b/src/types/review/purchase.ts new file mode 100644 index 0000000..e69de29 From 591f8f7388eab02a876347275c5fb0d2e94aea2a Mon Sep 17 00:00:00 2001 From: kim minseo Date: Fri, 21 Feb 2025 13:12:55 +0900 Subject: [PATCH 2/8] =?UTF-8?q?=EC=B6=A9=EB=8F=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/event/short-review.ts | 11 +++++++++-- src/api/review/review.ts | 13 +++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/api/event/short-review.ts b/src/api/event/short-review.ts index db3efa0..ab6ddef 100644 --- a/src/api/event/short-review.ts +++ b/src/api/event/short-review.ts @@ -89,13 +89,20 @@ export const toggleShortReviewReaction = async ( reactionType: 0 | 1, ): Promise => { try { + // 객체가 아닌 reactionType 값만 직접 전송 const response = await instance.post( `/events/short-reviews/${reviewId}/reaction`, - { reactionType }, + reactionType, ); return response.data; - } catch (err) { + } 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/review.ts b/src/api/review/review.ts index 65c2fce..8f814a3 100644 --- a/src/api/review/review.ts +++ b/src/api/review/review.ts @@ -2,10 +2,19 @@ import instance from '@/api/axios'; import { ApiResponse, ReviewDetail, ReviewType } from '@/types/review/review'; -export const getReviewDetail = (reviewId: number, type: ReviewType = 'PLACE') => { +/** + * 리뷰 상세 정보를 가져오는 함수 + * @param reviewId 리뷰 ID + * @param type 리뷰 타입 (기본값: 데이터에 따라 결정됨) + * @returns API 응답 + */ +export const getReviewDetail = (reviewId: number, type?: ReviewType) => { + // type이 제공되지 않은 경우, 백엔드가 데이터에 맞게 처리하도록 함 + const params = type ? { type } : {}; + return instance .get>(`/reviews/${reviewId}`, { - params: { type }, + params: params, }) .then((response) => response.data); }; From 44526315a30c5a216a7b290331395e748dbd26f6 Mon Sep 17 00:00:00 2001 From: kim minseo Date: Mon, 24 Feb 2025 19:33:18 +0900 Subject: [PATCH 3/8] =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/ReviewPage3.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ReviewPage3.tsx b/src/pages/ReviewPage3.tsx index d995df1..96964d2 100644 --- a/src/pages/ReviewPage3.tsx +++ b/src/pages/ReviewPage3.tsx @@ -68,7 +68,7 @@ const ReviewPage3 = () => { } // Fetch reviews - const reviewResponse = await fetchReviews(placeId, currentPage, 10, sort); + const reviewResponse = await fetchReviews(placeId, currentPage - 1, 10, sort); if (reviewResponse.isSuccess) { setReviewData(reviewResponse.result); setError(null); From 1327173ffa9b92645e1798a1fd09b07d531f6f84 Mon Sep 17 00:00:00 2001 From: kim minseo Date: Mon, 24 Feb 2025 19:38:21 +0900 Subject: [PATCH 4/8] =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/review/PlaceReview.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/review/PlaceReview.ts b/src/api/review/PlaceReview.ts index d9dfa74..bc0f476 100644 --- a/src/api/review/PlaceReview.ts +++ b/src/api/review/PlaceReview.ts @@ -1,5 +1,5 @@ // api/review/review.ts - +import instance from '@/api/axios'; import { ReviewResponse, SortType } from '../../types/review/PlaceReview'; export const fetchReviews = async ( @@ -12,8 +12,8 @@ export const fetchReviews = async ( // localStorage에서 토큰 가져오기 const token = localStorage.getItem('accessToken'); // 또는 다른 방식으로 저장된 토큰 - const response = await fetch( - `/api/places/${placeId}/reviews?page=${page}&size=${size}&sort=${sort}`, + const response = await instance.get( + `/places/${placeId}/reviews?page=${page}&size=${size}&sort=${sort}`, { headers: { Authorization: `Bearer ${token}`, // Bearer 토큰 방식 사용 From 3513c8f721e3e031c323d735cd42b7e9dbcdfe04 Mon Sep 17 00:00:00 2001 From: kim minseo Date: Mon, 24 Feb 2025 19:44:55 +0900 Subject: [PATCH 5/8] =?UTF-8?q?=EC=B6=A9=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/review/review.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/api/review/review.ts b/src/api/review/review.ts index 40c2c4c..3a8bff4 100644 --- a/src/api/review/review.ts +++ b/src/api/review/review.ts @@ -4,23 +4,13 @@ import { ApiResponse, ReviewDetail, ReviewType } from '@/types/review/review'; /** * 리뷰 상세 정보를 가져오는 함수 * @param reviewId 리뷰 ID -<<<<<<< HEAD - * @param type 리뷰 타입 (기본값: 데이터에 따라 결정됨) - * @returns API 응답 - */ -export const getReviewDetail = (reviewId: number, type?: ReviewType) => { - // type이 제공되지 않은 경우, 백엔드가 데이터에 맞게 처리하도록 함 - const params = type ? { type } : {}; - -======= * @param type 리뷰 타입 (기본값: 'PLACE') * @returns API 응답 */ export const getReviewDetail = (reviewId: number, type: ReviewType = 'PLACE') => { ->>>>>>> 79a812df1b6f67adff23fb3934cc9b829012823a return instance .get>(`/reviews/${reviewId}`, { - params: params, + params: { type }, }) .then((response) => response.data); }; From 020e893b69053ee8a6422227868af5004bde9a8f Mon Sep 17 00:00:00 2001 From: kim minseo Date: Mon, 24 Feb 2025 19:58:29 +0900 Subject: [PATCH 6/8] =?UTF-8?q?=EC=B6=A9=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/review/PlaceReview.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/api/review/PlaceReview.ts b/src/api/review/PlaceReview.ts index bc0f476..e7b1da6 100644 --- a/src/api/review/PlaceReview.ts +++ b/src/api/review/PlaceReview.ts @@ -12,15 +12,18 @@ export const fetchReviews = async ( // localStorage에서 토큰 가져오기 const token = localStorage.getItem('accessToken'); // 또는 다른 방식으로 저장된 토큰 - const response = await instance.get( - `/places/${placeId}/reviews?page=${page}&size=${size}&sort=${sort}`, - { - headers: { - Authorization: `Bearer ${token}`, // Bearer 토큰 방식 사용 - 'Content-Type': 'application/json', - }, + const response = await instance.get(`/places/${placeId}/reviews`, { + params: { + // URL 파라미터를 params 객체로 전달 + page, + size, + sort, }, - ); + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); if (!response.ok) { throw new Error('Failed to fetch reviews'); From eb079c2d1616e40d7129eebef138fd82143fa34a Mon Sep 17 00:00:00 2001 From: kim minseo Date: Mon, 24 Feb 2025 20:03:35 +0900 Subject: [PATCH 7/8] =?UTF-8?q?=EC=9E=A5=EC=86=8C=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/review/PlaceReview.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/api/review/PlaceReview.ts b/src/api/review/PlaceReview.ts index e7b1da6..698def0 100644 --- a/src/api/review/PlaceReview.ts +++ b/src/api/review/PlaceReview.ts @@ -9,12 +9,10 @@ export const fetchReviews = async ( sort: SortType = 'latest', ): Promise => { try { - // localStorage에서 토큰 가져오기 - const token = localStorage.getItem('accessToken'); // 또는 다른 방식으로 저장된 토큰 + const token = localStorage.getItem('accessToken'); const response = await instance.get(`/places/${placeId}/reviews`, { params: { - // URL 파라미터를 params 객체로 전달 page, size, sort, @@ -25,12 +23,10 @@ export const fetchReviews = async ( }, }); - if (!response.ok) { - throw new Error('Failed to fetch reviews'); - } - - return await response.json(); + // axios에서는 바로 response.data를 반환 + return response.data; } catch (error) { + console.error('Review fetch error:', error); throw new Error(error instanceof Error ? error.message : 'Error fetching reviews'); } }; From 68dfd3d8184425e33c78d7492c71ed1c12202958 Mon Sep 17 00:00:00 2001 From: kim minseo Date: Mon, 24 Feb 2025 20:12:35 +0900 Subject: [PATCH 8/8] =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/ReviewPage3.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pages/ReviewPage3.tsx b/src/pages/ReviewPage3.tsx index 96964d2..3e16e1f 100644 --- a/src/pages/ReviewPage3.tsx +++ b/src/pages/ReviewPage3.tsx @@ -199,6 +199,9 @@ const ReviewPage3 = () => { setSort(newSort); setCurrentPage(1); // Reset to first page when sorting changes }; + const handleReviewClick = (reviewId: number) => { + navigate(`/review/${reviewId}`); + }; if (loading) return
Loading...
; if (error) return
Error: {error}
; @@ -301,7 +304,7 @@ const ReviewPage3 = () => { {selectedAnimation && reviewData?.animationGroups .find((group) => group.animationId === selectedAnimation.animationId) - ?.hashTags.map((tag) => #{tag.name})} + ?.hashTags.map((tag) => {tag.name})} @@ -333,7 +336,11 @@ const ReviewPage3 = () => {

{group.animationName}

{group.reviews.map((review) => ( - + handleReviewClick(review.reviewId)} + style={{ cursor: 'pointer' }} // 클릭 가능함을 시각적으로 표시 + > {review.title} {review.content}