Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6e6fa47
Merge pull request #45 from Clubber2024/refactor/mobile
naraeng Aug 29, 2025
44056c2
Merge pull request #46 from Clubber2024/main
Kangyeeun0 Sep 4, 2025
c80d7c4
feat: 동아리 상세 리뷰 세팅
naraeng Sep 15, 2025
b8a04ce
Merge pull request #47 from Clubber2024/feat/review-page
naraeng Sep 15, 2025
c0e401c
refactor: 리뷰 UI 구현중
naraeng Sep 17, 2025
e409739
Merge pull request #48 from Clubber2024/feat/review-page
naraeng Sep 17, 2025
12de9e0
fix: 에러
Kangyeeun0 Sep 20, 2025
780b103
feat: 리뷰 좋아요
Kangyeeun0 Sep 24, 2025
830f488
Merge pull request #49 from Clubber2024/feat/review-like
Kangyeeun0 Sep 24, 2025
4876cb6
feat: 리뷰 신고 기능
Kangyeeun0 Oct 1, 2025
ab599b6
Merge pull request #50 from Clubber2024/feat/reveiw-report
Kangyeeun0 Oct 1, 2025
44bbf09
feat: 리뷰작성 UI 및 API 연동
naraeng Oct 1, 2025
35d6fe0
fix: 충돌 해결
naraeng Oct 1, 2025
c616b26
Merge pull request #52 from Clubber2024/feat/review-page
naraeng Oct 1, 2025
88b8324
fix:리뷰 좋아요/삭제
Kangyeeun0 Oct 14, 2025
af82eb1
feat: 리뷰작성 분기처리, 나의 리뷰목록, 리뷰수정삭제
naraeng Nov 4, 2025
1d5a74a
Merge pull request #53 from Clubber2024/feat/review-page
naraeng Nov 4, 2025
47df254
fix: 리뷰 통계
Kangyeeun0 Nov 6, 2025
dddc80e
Merge branch 'dev' into fix/review
Kangyeeun0 Nov 6, 2025
6104be2
Merge pull request #54 from Clubber2024/fix/review
Kangyeeun0 Nov 6, 2025
b8f3e87
fix/review
Kangyeeun0 Nov 6, 2025
ed51dec
fix: 리뷰 신고
Kangyeeun0 Nov 6, 2025
99c52b4
fix: 리뷰 좋아요 수 표시 문제
naraeng Nov 7, 2025
c048640
Merge branch 'dev' into feat/review-page
naraeng Nov 7, 2025
3e47208
fix: 충돌 해결
naraeng Nov 7, 2025
37635bc
Merge pull request #55 from Clubber2024/feat/review-page
naraeng Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/app/clubInfo/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import ClubInfo from "@/components/features/club/ClubInfo"
import ClubInfo from '@/components/features/club/ClubInfo';

export default function ClubInfoPage({
searchParams,
}:{
}: {
searchParams: { [key: string]: string };
}){
}) {
const clubId = searchParams.clubId ?? '';
return <ClubInfo clubId={clubId}/>
}
return <ClubInfo clubId={clubId} />;
}
288 changes: 288 additions & 0 deletions src/app/mypage/reviews/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
'use client';

import { useEffect, useState } from 'react';
import { getUserReviews, UserReview } from '@/components/features/club/review/api/reviewWrite';
import { Card } from '@/components/ui/card';
import { useRouter } from 'next/navigation';
import Loading from '@/components/common/Loading';
import { ChevronLeft, MoreVertical, Pencil, Trash2 } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { deleteReview, patchReview } from '@/components/features/club/review/api/ReviewApi';
import Modal from '@/components/common/Modal';
import ReviewEdit from '@/components/features/club/review/write/ReviewEdit';

export default function MypageReviewsPage() {
const router = useRouter();
const [reviews, setReviews] = useState<UserReview[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isOpenDeleteModal, setIsOpenDeleteModal] = useState(false);
const [selectedReviewId, setSelectedReviewId] = useState<number | null>(null);
const [editingReview, setEditingReview] = useState<UserReview | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false);

const fetchMyReviews = async () => {
try {
setIsLoading(true);
setError(null);
const userReviewsData = await getUserReviews();
setReviews(userReviewsData.userReviews);
} catch {
setError('리뷰를 불러오는데 실패했습니다.');
} finally {
setIsLoading(false);
}
};

useEffect(() => {
fetchMyReviews();
}, []);

const handleReviewDeleted = () => {
fetchMyReviews();
};

const handleEdit = (review: UserReview) => {
setEditingReview(review);
};

const handleEditSubmit = async (content: string) => {
if (!editingReview) return;

setIsSubmitting(true);
try {
const res = await patchReview(editingReview.reviewId, content);
if (res) {
setIsSuccessModalOpen(true);
} else {
setError('리뷰 수정에 실패했습니다.');
}
} catch {
setError('리뷰 수정에 실패했습니다.');
} finally {
setIsSubmitting(false);
}
};

const handleSuccessModalClose = () => {
setIsSuccessModalOpen(false);
setEditingReview(null);
fetchMyReviews();
};

const handleCancelEdit = () => {
setEditingReview(null);
};

const handleDeleteClick = (reviewId: number) => {
setSelectedReviewId(reviewId);
setIsOpenDeleteModal(true);
};

const handleDelete = async () => {
if (!selectedReviewId) return;
const res = await deleteReview(selectedReviewId);
if (res) {
setIsOpenDeleteModal(false);
setSelectedReviewId(null);
handleReviewDeleted();
}
};

const formatDate = (dateString: string) => {
try {
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}.${month}.${day}`;
} catch {
return dateString;
}
};

if (isLoading) {
return <Loading />;
}

if (error) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<p className="text-red-500 mb-4">{error}</p>
<button
onClick={() => router.back()}
className="px-4 py-2 bg-primary text-white rounded hover:bg-primary/90"
>
뒤로 가기
</button>
</div>
</div>
);
}

return (
<>
{/* 상단 헤더 바 - 전체 너비 */}
<div
className="bg-primary py-3 w-screen"
style={{ marginLeft: 'calc(-50vw + 50%)', marginRight: 'calc(-50vw + 50%)' }}
>
<div className="flex items-center gap-2 max-w-6xl mx-auto px-4">
<button
onClick={() => {
if (editingReview) {
handleCancelEdit();
} else {
router.back();
}
}}
className="flex items-center text-gray-700 hover:text-gray-900 cursor-pointer"
>
<ChevronLeft size={20} />
</button>
<span className="text-lg font-medium">마이페이지</span>
</div>
</div>

<div className="min-h-screen bg-white">
<div className="max-w-2xl mx-auto px-4 py-8">
{editingReview ? (
/* 수정 모드 */
<Card className="p-6">
<ReviewEdit
keywords={editingReview.keywords}
initialContent={editingReview.content}
onSubmit={handleEditSubmit}
isSubmitting={isSubmitting}
clubId={editingReview.clubId}
/>
</Card>
) : (
/* 리뷰 목록 모드 */
<>
{/* 제목 및 옵션 영역 */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-center mb-6">리뷰 목록</h1>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-lg font-medium">리뷰 {reviews.length}</span>
</div>
</div>
</div>

{/* 리뷰 목록 */}
{reviews.length > 0 ? (
<div className="flex flex-col gap-4">
{reviews.map((review) => (
<Card key={review.reviewId} className="p-4">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<button
onClick={() =>
router.push(`/clubInfo?clubId=${review.clubId}&tab=review`)
}
className="text-md font-semibold text-primary hover:underline"
>
{review.clubName}
</button>
<span className="text-sm font-semibold text-gray-700">
익명{review.reviewId}
</span>
<span className="text-xs text-gray-500">
{formatDate(review.dateTime)}
</span>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex items-center justify-center w-8 h-8 rounded hover:bg-gray-100">
<MoreVertical size={16} className="text-gray-500" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[120px]">
<DropdownMenuItem
onClick={() => handleEdit(review)}
className="cursor-pointer"
>
<Pencil size={16} className="mr-2 text-gray-500" />
수정하기
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDeleteClick(review.reviewId)}
className="cursor-pointer text-red-500"
>
<Trash2 size={16} className="mr-2" />
삭제하기
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>

{/* 리뷰 내용 */}
<p className="text-base text-gray-900">{review.content}</p>

{/* 키워드 태그 */}
{review.keywords && review.keywords.length > 0 && (
<div className="flex flex-row flex-wrap gap-2">
{review.keywords.map((keyword, index) => (
<span
key={index}
className="text-sm px-3 py-1.5 bg-gray-100 border border-gray-200 rounded-md text-gray-700"
>
{keyword}
</span>
))}
</div>
)}
</div>
</Card>
))}
</div>
) : (
<Card className="py-12">
<div className="text-center text-gray-500">
<p className="text-lg mb-2">작성한 리뷰가 없습니다.</p>
<p className="text-sm">동아리에 대한 리뷰를 작성해보세요.</p>
</div>
</Card>
)}
</>
)}
</div>
</div>

{/* 삭제 확인 모달 */}
{isOpenDeleteModal && (
<Modal
isOpen={isOpenDeleteModal}
message="정말 리뷰를 삭제하시겠습니까?"
onClose={() => {
setIsOpenDeleteModal(false);
setSelectedReviewId(null);
}}
onConfirm={handleDelete}
showConfirmButton={true}
confirmText="삭제"
cancelText="취소"
/>
)}

{/* 수정 성공 모달 */}
{isSuccessModalOpen && (
<Modal
isOpen={isSuccessModalOpen}
message="리뷰가 성공적으로 수정되었습니다!"
onClose={handleSuccessModalClose}
showConfirmButton={false}
/>
)}
</>
);
}
Loading