Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 11 additions & 7 deletions src/components/base-ui/BookStory/bookstory_card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Props = {
commentCount?: number;
onSubscribeClick?: () => void;
subscribeText?: string;
hideSubscribeButton?: boolean;
};

import { formatTimeAgo } from "@/utils/time";
Expand All @@ -30,6 +31,7 @@ export default function BookStoryCard({
commentCount = 1,
onSubscribeClick,
subscribeText = "구독",
hideSubscribeButton = false,
}: Props) {
return (
<div
Expand All @@ -56,13 +58,15 @@ export default function BookStoryCard({
{formatTimeAgo(createdAt)} 조회수 {viewCount}
</p>
</div>
<button
type="button"
onClick={onSubscribeClick}
className="h-8 rounded-lg bg-primary-2 px-[17px] body_2_1 text-White whitespace-nowrap"
>
{subscribeText}
</button>
{!hideSubscribeButton && (
<button
type="button"
onClick={onSubscribeClick}
className="h-8 rounded-lg bg-primary-2 px-[17px] body_2_1 text-White whitespace-nowrap"
>
{subscribeText}
</button>
)}
</div>

{/* 2. 책 이미지 (모바일: flex-1 / 데스크탑: h-36) */}
Expand Down
4 changes: 2 additions & 2 deletions src/components/base-ui/Join/JoinButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ const JoinButton: React.FC<JoinButtonProps> = ({
const variants = {
primary: disabled
? "bg-[#DADADA] text-[#8D8D8D] cursor-not-allowed"
: "bg-[#7B6154] text-[#FFF] hover:bg-[#7B6154] hover:text-[#FFF]",
secondary: "bg-[#EAE5E2] text-[#5E4A40] border border-[#D2C5B6]",
: "bg-[#7B6154] text-[#FFF] hover:bg-[#6A5246] hover:text-[#FFF] cursor-pointer",
secondary: "bg-[#EAE5E2] text-[#5E4A40] border border-[#D2C5B6] hover:bg-[#D2C5B6] cursor-pointer",
};

// Determine classes to avoid conflicts with className prop
Expand Down
59 changes: 49 additions & 10 deletions src/components/base-ui/MyPage/MyBookStoryList.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,67 @@
"use client";

import React from "react";
import React, { useEffect } from "react";
import BookStoryCard from "@/components/base-ui/BookStory/bookstory_card";
import { DUMMY_MY_STORIES } from "@/constants/mocks/mypage";
import { useMyInfiniteStoriesQuery } from "@/hooks/queries/useStoryQueries";
import { useInView } from "react-intersection-observer";

const MyBookStoryList = () => {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
isError,
} = useMyInfiniteStoriesQuery();

const { ref, inView } = useInView();

useEffect(() => {
if (inView && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);

const stories = data?.pages.flatMap((page) => page.basicInfoList) || [];

return (
<div className="flex flex-col items-center w-full max-w-[1048px] mx-auto gap-[20px] px-[18px] md:px-[40px] lg:px-0">

{isLoading && <p className="text-Gray-4 text-center py-4">로딩 중...</p>}

{!isLoading && isError && (
<p className="text-red-500 text-center py-4">책 이야기를 불러오는 데 실패했습니다.</p>
)}

{!isLoading && !isError && stories.length === 0 && (
<p className="text-Gray-4 text-center py-4">작성한 책 이야기가 없습니다.</p>
)}

<div className="grid grid-cols-2 min-[540px]:grid-cols-3 md:grid-cols-2 lg:grid-cols-3 gap-[20px] md:gap-[12px] lg:gap-[20px] w-fit">
{DUMMY_MY_STORIES.map((story) => (
{stories.map((story) => (
<BookStoryCard
key={story.id}
authorName={story.authorName}
key={story.bookStoryId}
authorName={story.authorInfo.nickname}
createdAt={story.createdAt}
viewCount={story.viewCount}
title={story.title}
content={story.content}
likeCount={story.likeCount}
title={story.bookStoryTitle}
content={story.description}
likeCount={story.likes}
commentCount={story.commentCount}
coverImgSrc={story.coverImgSrc}
profileImgSrc="/profile2.svg"
coverImgSrc={story.bookInfo.imgUrl}
profileImgSrc={story.authorInfo.profileImageUrl}
hideSubscribeButton={true}
/>
))}
</div>

{/* Infinite Scroll Trigger */}
<div ref={ref} className="h-4 w-full" />

{isFetchingNextPage && (
<p className="text-Gray-4 text-center py-4">추가 이야기를 불러오는 중...</p>
)}
</div>
);
};
Expand Down
35 changes: 32 additions & 3 deletions src/components/base-ui/MyPage/MyMeetingList.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,43 @@
"use client";

import React from "react";
import { DUMMY_MEETINGS } from "@/constants/mocks/mypage";
import MyMeetingCard from "./items/MyMeetingCard";
import { useMyClubsQuery } from "@/hooks/queries/useClubQueries";

const MyMeetingList = () => {
const { data, isLoading, isError } = useMyClubsQuery();
const clubs = data?.clubList || [];

if (isLoading) {
return (
<div className="flex flex-col items-center justify-center py-10 w-full text-Gray-4 text-sm font-medium">
불러오는 중...
</div>
);
}

if (isError) {
return (
<div className="flex flex-col items-center justify-center py-10 w-full text-red-500 text-sm font-medium">
독서 모임을 불러오는 데 실패했습니다.
</div>
);
}

if (clubs.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-20 w-full">
<p className="text-Gray-4 text-sm font-medium whitespace-pre-wrap text-center">
가입한 독서 모임이 없습니다.
</p>
</div>
);
}

return (
<div className="flex flex-col items-start gap-[8px] w-full max-w-[1048px] px-[18px] md:px-[40px] lg:px-0 mx-auto">
{DUMMY_MEETINGS.map((meeting) => (
<MyMeetingCard key={meeting.id} meeting={meeting} />
{clubs.map((club) => (
<MyMeetingCard key={club.clubId} club={club} />
))}
</div>
);
Expand Down
55 changes: 51 additions & 4 deletions src/components/base-ui/MyPage/MyNotificationList.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,62 @@
"use client";

import React from "react";
import { DUMMY_NOTIFICATIONS } from "@/constants/mocks/mypage";
import React, { useEffect, useMemo } from "react";
import MyNotificationItem from "./items/MyNotificationItem";
import { useInfiniteNotificationsQuery } from "@/hooks/queries/useNotificationQueries";
import { useInView } from "react-intersection-observer";

const MyNotificationList = () => {
const { ref, inView } = useInView();
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError } =
useInfiniteNotificationsQuery();

const notifications = useMemo(() => {
return data?.pages.flatMap((page) => page.notifications) || [];
}, [data]);

useEffect(() => {
if (inView && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);

if (isLoading) {
return (
<div className="flex flex-col items-center justify-center py-10 w-full text-Gray-4 text-sm font-medium">
불러오는 중...
</div>
);
}

if (isError) {
return (
<div className="flex flex-col items-center justify-center py-10 w-full text-red-500 text-sm font-medium">
알림을 불러오는 데 실패했습니다.
</div>
);
}

if (notifications.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-20 w-full">
<p className="text-Gray-4 text-sm font-medium whitespace-pre-wrap text-center">
새로운 알림이 없습니다.
</p>
</div>
);
}

return (
<div className="flex flex-col items-start gap-[8px] w-full max-w-[1048px] mx-auto px-[18px] md:px-[40px] lg:px-0">
{DUMMY_NOTIFICATIONS.map((notification) => (
<MyNotificationItem key={notification.id} notification={notification} />
{notifications.map((notification) => (
<MyNotificationItem key={notification.notificationId} notification={notification} />
))}
<div ref={ref} className="h-10" />
{isFetchingNextPage && (
<div className="w-full text-center py-4 text-sm text-gray-500">
추가 알림을 불러오는 중...
</div>
)}
</div>
);
};
Expand Down
23 changes: 14 additions & 9 deletions src/components/base-ui/MyPage/UserProfile.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import React from "react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import JoinButton from "@/components/base-ui/Join/JoinButton";
import { DUMMY_USER_PROFILE } from "@/constants/mocks/mypage";
import { useAuthStore } from "@/store/useAuthStore";
import { useProfileQuery } from "@/hooks/queries/useMemberQueries";
import FloatingFab from "../Float";

const UserProfile = () => {
const { user: authUser } = useAuthStore();
const router = useRouter();
const { data: profileData, isLoading, isError } = useProfileQuery();

// 서버 데이터가 있으면 사용하고, 없으면 더미 데이터 사용 (구독자 수 등은 현재 API에 없음)
const user = {
...DUMMY_USER_PROFILE,
name: authUser?.nickname || authUser?.email || DUMMY_USER_PROFILE.name,
intro: authUser?.description || DUMMY_USER_PROFILE.intro,
profileImage: authUser?.profileImageUrl || DUMMY_USER_PROFILE.profileImage,
name: profileData?.nickname || DUMMY_USER_PROFILE.name,
intro: profileData?.description || DUMMY_USER_PROFILE.intro,
profileImage: profileData?.profileImageUrl || DUMMY_USER_PROFILE.profileImage,
};

return (
Expand Down Expand Up @@ -104,18 +106,21 @@ const UserProfile = () => {
</div>
</div>

{/* Action Buttons */}
<div className="flex items-center justify-center md:justify-start gap-[12px] md:gap-[24px] self-stretch">
<JoinButton className="w-[160px] h-[32px] md:w-[355px] md:h-[48px] p-[12px_16px] gap-[10px] rounded-[8px] bg-[#7B6154] text-[#FFF] font-sans text-[14px] font-semibold md:text-[18px] md:font-medium leading-[135%]">
<JoinButton
onClick={() => router.push("/stories/new")}
className="w-[160px] h-[32px] md:w-[355px] md:h-[48px] p-[12px_16px] gap-[10px] rounded-[8px] font-sans text-[14px] font-semibold md:text-[18px] md:font-medium leading-[135%]"
>
내 책 이야기 쓰기
</JoinButton>
<JoinButton className="w-[160px] h-[32px] md:w-[355px] md:h-[48px] p-[12px_16px] gap-[10px] rounded-[8px] bg-[#7B6154] text-[#FFF] font-sans text-[14px] font-semibold md:text-[18px] md:font-medium leading-[135%]">
<JoinButton className="w-[160px] h-[32px] md:w-[355px] md:h-[48px] p-[12px_16px] gap-[10px] rounded-[8px] font-sans text-[14px] font-semibold md:text-[18px] md:font-medium leading-[135%]">
소식 문의하기
</JoinButton>
</div>
<FloatingFab
iconSrc="/icons_pencil.svg"
iconAlt="문의하기"
iconAlt="글쓰기"
onClick={() => router.push("/stories/new")}
/>
</div>
</div>
Expand Down
8 changes: 4 additions & 4 deletions src/components/base-ui/MyPage/items/MyMeetingCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

import React from "react";
import Image from "next/image";
import { MyPageMeeting } from "@/types/mypage";
import { MyClubInfo } from "@/types/club";

interface MyMeetingCardProps {
meeting: MyPageMeeting;
club: MyClubInfo;
}

const MyMeetingCard = ({ meeting }: MyMeetingCardProps) => {
const MyMeetingCard = ({ club }: MyMeetingCardProps) => {
return (
<div className="flex w-full px-[18px] py-[12px] md:p-[20px] justify-between items-center rounded-[8px] bg-white border border-[#EAE5E2] gap-[12px]">
<span className="text-[#5C5C5C] font-sans text-[16px] md:text-[24px] font-medium md:font-semibold leading-[135%] tracking-[-0.024px] truncate flex-1">
{meeting.title}
{club.clubName}
</span>
<button type="button" className="flex items-center justify-center shrink-0">
<Image
Expand Down
47 changes: 40 additions & 7 deletions src/components/base-ui/MyPage/items/MyNotificationItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,50 @@

import React from "react";
import Image from "next/image";
import { MyPageNotification } from "@/types/mypage";
import { NotificationBasicInfo } from "@/types/notification";
import { formatTimeAgo } from "@/utils/time";
import { useReadNotificationMutation } from "@/hooks/mutations/useNotificationMutations";

interface MyNotificationItemProps {
notification: MyPageNotification;
notification: NotificationBasicInfo;
}

const getNotificationText = (notification: NotificationBasicInfo): string => {
const name = notification.displayName;
switch (notification.notificationType) {
case "LIKE":
return `${name}님이 회원의 책 이야기에 좋아요를 남기셨습니다.`;
case "COMMENT":
return `${name}님이 회원의 책 이야기에 댓글을 작성했습니다.`;
case "FOLLOW":
return `${name}님이 나를 팔로우하기 시작했습니다.`;
case "JOIN_CLUB":
return `${name}님이 나를 독서 모임에 초대했습니다.`;
case "CLUB_MEETING_CREATED":
return `${name} 독서 모임에 새로운 일정이 생성되었습니다.`;
case "CLUB_NOTICE_CREATED":
return `${name} 독서 모임에 새로운 공지가 등록되었습니다.`;
default:
return "새로운 알림이 도착했습니다.";
}
};

const MyNotificationItem = ({ notification }: MyNotificationItemProps) => {
const { mutate: readNotification } = useReadNotificationMutation();

const handleClick = () => {
if (!notification.read) {
readNotification(notification.notificationId);
}
};

return (
<div className="flex w-full p-[12px_20px] md:p-[28px_20px] justify-between items-center rounded-[8px] bg-white border border-[#EAE5E2] gap-[12px]">
<div
onClick={handleClick}
className="flex w-full p-[12px_20px] md:p-[28px_20px] justify-between items-center rounded-[8px] bg-white border border-[#EAE5E2] gap-[12px] cursor-pointer hover:bg-gray-50 transition-colors"
>
<div className="flex items-center gap-[12px] flex-1 min-w-0">
{!notification.isRead && (
{!notification.read && (
<Image
src="/icon_alert.svg"
alt="New"
Expand All @@ -22,14 +55,14 @@ const MyNotificationItem = ({ notification }: MyNotificationItemProps) => {
className="shrink-0"
/>
)}
{notification.isRead && <div className="w-[24px] h-[24px] shrink-0" />}
{notification.read && <div className="w-[24px] h-[24px] shrink-0" />}

<span className="text-Gray-5 body_2_3 md:subhead_4_1 truncate flex-1">
{notification.content}
{getNotificationText(notification)}
</span>
</div>
<span className="text-[#BBB] font-sans text-[12px] md:text-[14px] font-normal leading-[145%] tracking-[-0.014px] shrink-0 whitespace-nowrap ml-[8px]">
{notification.time}
{formatTimeAgo(notification.createdAt)}
</span>
</div>
);
Expand Down
Loading