diff --git a/src/components/common/modal-delete.tsx b/src/components/common/modal-delete.tsx index 6858b75..7b723a9 100644 --- a/src/components/common/modal-delete.tsx +++ b/src/components/common/modal-delete.tsx @@ -1,5 +1,3 @@ -"use client"; - import { useState, useEffect } from "react"; import Button from "@/components/common/Button"; import { deleteWine, deleteReview } from "@/api/wine.api"; @@ -9,6 +7,7 @@ type DeleteModalProps = { onCancel: () => void; id: number; type: "wine" | "review"; + onDeleted?: () => void; // 삭제 후 콜백 }; export default function DeleteModal({ @@ -16,8 +15,10 @@ export default function DeleteModal({ onCancel, id, type, + onDeleted, }: DeleteModalProps) { const [message, setMessage] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); useEffect(() => { if (isOpen) { @@ -32,6 +33,10 @@ export default function DeleteModal({ }, [isOpen]); const handleDelete = async () => { + // 삭제 중복 방지 + if (isDeleting) return; + + setIsDeleting(true); try { if (type === "wine") { await deleteWine({ id }); @@ -40,30 +45,36 @@ export default function DeleteModal({ await deleteReview({ id }); setMessage("리뷰가 삭제되었습니다."); } + + // 1초 후에 모달을 닫고 콜백 호출 + setTimeout(() => { + handleCloseMessage(); + if (onDeleted) { + onDeleted(); // 리뷰 목록 새로고침 등의 작업 수행 + } + }, 1000); + } catch (error) { console.error(`${type} 삭제 오류:`, error); setMessage(`${type === "wine" ? "와인" : "리뷰"} 삭제에 실패했습니다.`); + setIsDeleting(false); } }; const handleCloseMessage = () => { setMessage(null); - onCancel(); // 삭제 결과 모달을 닫으면서 삭제 모달도 닫음 + setIsDeleting(false); + onCancel(); }; if (!isOpen && !message) return null; return (
- {/* 배경 레이어 */}
- {/* 삭제 확인 모달 */} {isOpen && !message && ( -
+

정말 삭제하시겠습니까?

@@ -75,6 +86,7 @@ export default function DeleteModal({ addClassName="!text-gray-500 text-[1.6rem] rounded-[1rem] font-bold flex items-center justify-center min-h-[5rem] tablet:min-h-[5.4rem]" onClick={onCancel} style={{ flexGrow: "1" }} + disabled={isDeleting} > 취소 @@ -85,19 +97,16 @@ export default function DeleteModal({ addClassName="text-white text-[1.6rem] rounded-[1rem] font-bold flex items-center justify-center min-h-[5rem] tablet:min-h-[5.4rem]" style={{ flexGrow: "1" }} onClick={handleDelete} + disabled={isDeleting} > - 삭제하기 + {isDeleting ? '삭제 중...' : '삭제하기'}
)} - {/* 삭제 결과 메시지 모달 */} {message && ( -
+

{message}

); } diff --git a/src/components/myprofile/my-wine-card.tsx b/src/components/myprofile/my-wine-card.tsx index 9dbceab..f422ea5 100644 --- a/src/components/myprofile/my-wine-card.tsx +++ b/src/components/myprofile/my-wine-card.tsx @@ -19,9 +19,10 @@ export interface Wine { interface MyWineCardProps { wine: Wine; onUpdate?: (updatedWine: Wine) => void; + onDelete?: () => void; // 삭제 후 동작을 위한 콜백 추가 } -export default function MyWineCard({ wine, onUpdate }: MyWineCardProps) { +export default function MyWineCard({ wine, onUpdate, onDelete }: MyWineCardProps) { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false); @@ -114,6 +115,7 @@ export default function MyWineCard({ wine, onUpdate }: MyWineCardProps) { onCancel={closeDeleteModal} id={wine.id} type="wine" + onDeleted={onDelete} // 삭제 완료 후 동작 설정 />
); diff --git a/src/components/myprofile/profile-tab.tsx b/src/components/myprofile/profile-tab.tsx index dd5e988..84fcbfb 100644 --- a/src/components/myprofile/profile-tab.tsx +++ b/src/components/myprofile/profile-tab.tsx @@ -21,11 +21,10 @@ export default function ProfileTab() { const initialLimit = 5; // 데이터 로딩 개수 - // 리뷰 데이터 가져오기 (useCallback으로 메모이제이션) + // 리뷰 데이터 가져오기 const fetchReviews = useCallback( async (cursor: number | null) => { - if (isLoading || !hasMoreReviews) return; // 로딩 중이거나 더 이상 불러올 데이터가 없으면 return - console.log("Fetching reviews with cursor:", cursor); + if (isLoading || !hasMoreReviews) return; try { setIsLoading(true); const response = await instance.get("/users/me/reviews", { @@ -33,42 +32,30 @@ export default function ProfileTab() { }); const { list, totalCount, nextCursor } = response.data; - console.log("Reviews data:", list); - console.log("Total reviews count:", totalCount); - console.log("Next cursor for reviews:", nextCursor); - - // 중복되지 않는 데이터만 추가 - setReviews((prevReviews) => { - const uniqueReviews = [ - ...prevReviews, - ...list.filter( - (newReview: Review) => - !prevReviews.some((review) => review.id === newReview.id), - ), - ]; - return uniqueReviews; - }); + setReviews((prevReviews) => [ + ...prevReviews, + ...list.filter( + (newReview: Review) => + !prevReviews.some((review) => review.id === newReview.id), + ), + ]); setReveiwTotal(totalCount); - setReviewCursor(nextCursor); // 커서 저장 - - if (!nextCursor) { - setHasMoreReviews(false); // 더 이상 로드할 데이터 없으면 false - } + setReviewCursor(nextCursor); + setHasMoreReviews(!!nextCursor); } catch (error) { console.error("리뷰 데이터 불러오기 실패", error); } finally { setIsLoading(false); } }, - [isLoading, hasMoreReviews], + [isLoading, hasMoreReviews, initialLimit], ); - // 와인 데이터 가져오기 (useCallback으로 메모이제이션) + // 와인 데이터 가져오기 const fetchWines = useCallback( async (cursor: number | null) => { - if (isLoading || !hasMoreWines) return; // 로딩 중이거나 더 이상 불러올 데이터가 없으면 return - console.log("Fetching wines with cursor:", cursor); + if (isLoading || !hasMoreWines) return; try { setIsLoading(true); const response = await instance.get("/users/me/wines", { @@ -76,45 +63,26 @@ export default function ProfileTab() { }); const { list, totalCount, nextCursor } = response.data; - console.log("Wines data:", list); - console.log("Total wines count:", totalCount); - console.log("Next cursor for wines:", nextCursor); - - // 중복되지 않는 데이터만 추가 - setWines((prevWines) => { - const uniqueWines = [ - ...prevWines, - ...list.filter( - (newWine: Wine) => - !prevWines.some((wine) => wine.id === newWine.id), - ), - ]; - return uniqueWines; - }); + setWines((prevWines) => [ + ...prevWines, + ...list.filter( + (newWine: Wine) => + !prevWines.some((wine) => wine.id === newWine.id), + ), + ]); setWineTotal(totalCount); - setWineCursor(nextCursor); // 커서 저장 - if (!nextCursor) { - setHasMoreWines(false); // 더 이상 로드할 데이터 없으면 false - } + setWineCursor(nextCursor); + setHasMoreWines(!!nextCursor); } catch (error) { console.error("와인 데이터 불러오기 실패", error); } finally { setIsLoading(false); } }, - [isLoading, hasMoreWines], + [isLoading, hasMoreWines, initialLimit], ); - // 탭 변경 시 데이터 가져오기 - // useEffect(() => { - // if (activeTab === "reviews") { - // fetchReviews(reviewCursor); - // } else { - // fetchWines(wineCursor); - // } - // }, [activeTab, reviewCursor, wineCursor, fetchReviews, fetchWines]); - return (
@@ -123,9 +91,10 @@ export default function ProfileTab() { className={` desktop:w-[9.6rem] desktop:h-[3.2rem] desktop:text-[2rem] desktop:leading-[3.2rem] tablet:w-[9.6rem] tablet:h-[3.2rem] tablet:text-[2rem] tablet:leading-[3.2rem] - mobile:w-auto mobile:h-[2.6rem] mobile:text-[1.8rem] mobile:leading-[2.6rem] - - font-semibold ${activeTab === "reviews" ? "text-gray-800" : "text-gray-500"}`} + mobile:w-auto mobile:h-[2.6rem] mobile:text-[1.8rem] mobile:leading-[2.6rem] + font-semibold ${ + activeTab === "reviews" ? "text-gray-800" : "text-gray-500" + }`} onClick={() => setActiveTab("reviews")} > 내가 쓴 후기 @@ -136,8 +105,9 @@ export default function ProfileTab() { tablet:w-[13.1rem] tablet:h-[3.2rem] tablet:text-[2rem] tablet:leading-[3.2rem] mobile:w-auto mobile:h-[2.6rem] mobile:text-[1.8rem] mobile:leading-[2.6rem] desktop:ml-[3.2rem] tablet:ml-[3.2rem] mobile:ml-[1.6rem] - - font-semibold ${activeTab === "wines" ? "text-gray-800" : "text-gray-500"}`} + font-semibold ${ + activeTab === "wines" ? "text-gray-800" : "text-gray-500" + }`} onClick={() => setActiveTab("wines")} > 내가 등록한 와인 @@ -160,10 +130,18 @@ export default function ProfileTab() { >
{reviews.map((review) => ( - + { + setReviews((prevReviews) => + prevReviews.filter((r) => r.id !== review.id), + ); + setReveiwTotal((prev) => prev - 1); + }} + /> ))}
- {/* 더 이상 데이터가 없을 때 메시지 표시 */} {!hasMoreReviews && !isLoading && (
더 이상 데이터가 없습니다. @@ -179,10 +157,18 @@ export default function ProfileTab() { >
{wines.map((wine) => ( - + { + setWines((prevWines) => + prevWines.filter((w) => w.id !== wine.id), + ); + setWineTotal((prev) => prev - 1); + }} + /> ))}
- {/* 더 이상 데이터가 없을 때 메시지 표시 */} {!hasMoreWines && !isLoading && (
더 이상 데이터가 없습니다. diff --git a/src/components/wines/detail/detail-review-card.tsx b/src/components/wines/detail/detail-review-card.tsx index 3784b57..9338f4e 100644 --- a/src/components/wines/detail/detail-review-card.tsx +++ b/src/components/wines/detail/detail-review-card.tsx @@ -10,7 +10,7 @@ import MenuIcon from "@/../public/icons/menu.svg"; import DropDownMenu from "@/components/common/dropdown-menu"; import DetailWineTag, { Aroma, AromaMapping } from "./detail-wine-tag"; import StarFill from "@/../public/icons/star_fill.svg"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; import axios from "axios"; import AddReviewModal from "@/components/modal-review/AddReviewModal"; import instance from "@/api/api"; @@ -41,10 +41,19 @@ interface User { image: string; } +interface RatingData { + avgRating: number; + reviewCount: number; + avgRatings: { + [key: number]: number; + }; +} + interface DetailReviewCardProps { wineid: string; } + function timeAgo(createdAt: string): string { const now = new Date(); const createdDate = new Date(createdAt); @@ -68,12 +77,32 @@ function timeAgo(createdAt: string): string { export default function DetailReviewCard({ wineid }: DetailReviewCardProps) { const [reviews, setReviews] = useState([]); + const [ratingData, setRatingData] = useState(null); // 타입 지정 const [isExpand, setIsExpand] = useState>({}); const [isLoading, setIsLoading] = useState(true); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - // const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [selectedReviewId, setSelectedReviewId] = useState(null); - const [alertText, setAlertText] = useState(null); // 추가 + + const refreshData = async () => { + await fetchReviews(); // 리뷰 목록 새로고침 + const ratingDetailsComponent = document.querySelector("#rating-details"); + if (ratingDetailsComponent) { + const event = new CustomEvent("refreshRatings"); + ratingDetailsComponent.dispatchEvent(event); + } + }; + + +// 모달 닫힐 때 호출 +const handleModalClose = async () => { + setIsModalOpen(false); + setIsEditing(false); + setInitialData(null); + await refreshData(); // 리뷰 목록과 별점 모두 업데이트 +}; + + + const [alertText, setAlertText] = useState(null); const { user } = useAuth(); @@ -150,23 +179,31 @@ export default function DetailReviewCard({ wineid }: DetailReviewCardProps) { } } - useEffect(() => { - async function fetchReviews() { - try { - setIsLoading(true); - const response = await instance.get<{ reviews: Review[] }>( - `/wines/${wineid}`, - ); - setReviews(response.data.reviews || []); - } catch (error) { - console.error("리뷰 가져오기 실패", error); - } finally { - setIsLoading(false); - } + const fetchReviews = useCallback(async () => { + try { + setIsLoading(true); + const response = await instance.get(`/wines/${wineid}`); + setReviews(response.data.reviews || []); + // rating 관련 데이터만 추출해서 저장 + const { avgRating, reviewCount, avgRatings } = response.data; + setRatingData({ avgRating, reviewCount, avgRatings }); + } catch (error) { + console.error("리뷰 가져오기 실패", error); + } finally { + setIsLoading(false); } - - fetchReviews(); }, [wineid]); + + useEffect(() => { + fetchReviews(); + }, [fetchReviews]); // fetchReviews만 의존성으로 등록 + + + function closeDeleteModal() { + setSelectedReviewId(null); + setIsDeleteModalOpen(false); + fetchReviews(); // 삭제 후 리뷰 목록 갱신 + } if (isLoading) { return
; @@ -184,10 +221,6 @@ export default function DetailReviewCard({ wineid }: DetailReviewCardProps) { setIsDeleteModalOpen(true); } - function closeDeleteModal() { - setSelectedReviewId(null); - setIsDeleteModalOpen(false); - } // function openEditModal(review: Review) { // setSelectedReviewId(review.id); @@ -197,7 +230,9 @@ export default function DetailReviewCard({ wineid }: DetailReviewCardProps) { return (
- +
{reviews.length > 0 ? (
@@ -426,7 +461,7 @@ export default function DetailReviewCard({ wineid }: DetailReviewCardProps) { {isModalOpen && ( setIsModalOpen(false)} + onClick={handleModalClose} // 수정됨 wineId={wineid} id={selectedReviewId!} isEditing={isEditing} diff --git a/src/components/wines/detail/rating-details.tsx b/src/components/wines/detail/rating-details.tsx index b98f22d..9348473 100644 --- a/src/components/wines/detail/rating-details.tsx +++ b/src/components/wines/detail/rating-details.tsx @@ -4,59 +4,27 @@ import Button from "@/components/common/Button"; import Image from "next/image"; import defaultStar from "../../../../public/icons/star.svg"; import purpleStar from "../../../../public/icons/star_fill.svg"; -import instance from "@/api/api"; -import { AxiosError } from "axios"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import AddReviewModal from "@/components/modal-review/AddReviewModal"; import ReviewProvider from "@/provider/usereviewmodals"; interface RatingDetailsProps { - avgRating: number; - reviewCount: number; - avgRatings: { - [key: number]: number; - }; -} - -interface WineID { id: string; + ratingData?: { + avgRating: number; + reviewCount: number; + avgRatings: { + [key: number]: number; + }; + }; } -interface ErrorResponse { - message: string; -} - -export default function RatingDetails({ id }: WineID) { - const [ratingDetails, setRatingDetails] = useState({ - avgRating: 0, - reviewCount: 0, - avgRatings: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }, - }); - +export default function RatingDetails({ id, ratingData }: RatingDetailsProps) { const [isOpenModal, setIsOpenModal] = useState(false); - useEffect(() => { - async function getRatings(id: string) { - try { - const res = await instance.get(`/wines/${id}`); - setRatingDetails(res.data); - } catch (error) { - const axiosError = error as AxiosError; - console.error(axiosError.response?.data || axiosError.message); + // useEffect와 getRatings 제거 (더 이상 필요 없음) - const errorData = axiosError.response?.data as ErrorResponse; - console.error(errorData); - alert( - errorData?.message || - axiosError.message || - "알 수 없는 오류가 발생했습니다.", - ); - } - } - getRatings(id); - }, [id]); - - return ratingDetails.reviewCount ? ( + return ratingData?.reviewCount ? (

- {ratingDetails.avgRating?.toFixed(1)} + {ratingData.avgRating?.toFixed(1)}

@@ -91,11 +59,10 @@ export default function RatingDetails({ id }: WineID) { key={index} className="relative desktop:w-[2.4rem] desktop:h-[2.4rem] tablet:w-[2.4rem] tablet:h-[2.4rem] mobile:w-[1.8rem] mobile:h-[1.8rem]" > - {/* 48x48 크기로 컨테이너 설정 */}

- {ratingDetails.reviewCount}개의 후기 + {ratingData.reviewCount}개의 후기

@@ -124,11 +91,11 @@ export default function RatingDetails({ id }: WineID) {
{[ - { label: "5점", value: ratingDetails.avgRatings?.[5] || 0 }, - { label: "4점", value: ratingDetails.avgRatings?.[4] || 0 }, - { label: "3점", value: ratingDetails.avgRatings?.[3] || 0 }, - { label: "2점", value: ratingDetails.avgRatings?.[2] || 0 }, - { label: "1점", value: ratingDetails.avgRatings?.[1] || 0 }, + { label: "5점", value: ratingData.avgRatings?.[5] || 0 }, + { label: "4점", value: ratingData.avgRatings?.[4] || 0 }, + { label: "3점", value: ratingData.avgRatings?.[3] || 0 }, + { label: "2점", value: ratingData.avgRatings?.[2] || 0 }, + { label: "1점", value: ratingData.avgRatings?.[1] || 0 }, ].map((ratings, index) => (
@@ -174,4 +141,4 @@ export default function RatingDetails({ id }: WineID) {
) : null; -} +} \ No newline at end of file