diff --git a/apps/client/src/app/[user]/components/CommentInput/index.tsx b/apps/client/src/app/[user]/components/CommentInput/index.tsx
index 230cf99f..42b44226 100644
--- a/apps/client/src/app/[user]/components/CommentInput/index.tsx
+++ b/apps/client/src/app/[user]/components/CommentInput/index.tsx
@@ -13,12 +13,14 @@ interface CommentInputProps {
profileUrl?: string;
nickname?: string;
solutionId: number;
+ onCommentCountPlus: () => void;
}
const CommentInput = ({
profileUrl,
nickname,
solutionId,
+ onCommentCountPlus,
}: CommentInputProps) => {
const [comment, setComment] = useState("");
@@ -33,6 +35,7 @@ const CommentInput = ({
postComment(comment);
setComment("");
+ onCommentCountPlus();
};
return (
diff --git a/apps/client/src/app/[user]/components/FeedItem/index.css.ts b/apps/client/src/app/[user]/components/FeedItem/index.css.ts
index 78f19d8f..d057bb5b 100644
--- a/apps/client/src/app/[user]/components/FeedItem/index.css.ts
+++ b/apps/client/src/app/[user]/components/FeedItem/index.css.ts
@@ -74,3 +74,39 @@ export const commentStyle = style({
...theme.font.Caption1_R_12,
color: theme.color.mg1,
});
+
+export const moreCommentContainer = style({
+ alignSelf: "flex-start",
+
+ paddingLeft: "2rem",
+
+ color: theme.color.mg2,
+
+ cursor: "pointer",
+ ":hover": {
+ color: theme.color.mg1,
+ },
+});
+
+export const moreCommentWrapper = style({
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ gap: "0.8rem",
+
+ padding: "0.6rem 0",
+
+ color: "inherit",
+ ...theme.font.Caption1_R_12,
+});
+
+export const moreCommentButtonStyle = style({
+ padding: "0.2rem 0.8rem",
+
+ backgroundColor: theme.color.mg5,
+ borderRadius: "4px",
+
+ color: "inherit",
+
+ cursor: "pointer",
+});
diff --git a/apps/client/src/app/[user]/components/FeedItem/index.tsx b/apps/client/src/app/[user]/components/FeedItem/index.tsx
index e59b0a19..de857ef1 100644
--- a/apps/client/src/app/[user]/components/FeedItem/index.tsx
+++ b/apps/client/src/app/[user]/components/FeedItem/index.tsx
@@ -14,6 +14,9 @@ import {
feedItemContainer,
infoTextWrapper,
infoWrapper,
+ moreCommentButtonStyle,
+ moreCommentContainer,
+ moreCommentWrapper,
nameStyle,
studyNameStyle,
} from "@/app/[user]/components/FeedItem/index.css";
@@ -23,13 +26,21 @@ import { useGroupInfoQueryObject } from "@/app/api/groups/query";
import { useSolutionQueryObject } from "@/app/api/solutions/query";
import { formatDistanceDate } from "@/common/util/date";
import { useSuspenseQueries } from "@tanstack/react-query";
+import Link from "next/link";
+import { useMemo, useRef } from "react";
interface FeedItemProps {
solutionId: number;
groupId: number;
}
+const DEFAULT_COMMENT_COUNT = 3;
+
const FeedItem = ({ solutionId, groupId }: FeedItemProps) => {
+ const myAddedCommentsRef = useRef(0);
+ const displayedCommentCount =
+ myAddedCommentsRef.current + DEFAULT_COMMENT_COUNT;
+
const [{ data: solution }, { data: comments }, { data: group }] =
useSuspenseQueries({
queries: [
@@ -50,14 +61,14 @@ const FeedItem = ({ solutionId, groupId }: FeedItemProps) => {
});
// 피드에 뜨게한 댓글 찾기 - 나를 제외한 최신 댓글
- const triggerComment = comments?.find(
- (comment) => comment.writerNickname !== solution?.nickname,
+ const triggerComment = useMemo(
+ () =>
+ comments?.find(
+ (comment) => comment.writerNickname !== solution?.nickname,
+ ),
+ [comments, solution?.nickname],
);
- if (!triggerComment) {
- return null;
- }
-
const [
triggerCommentWritterName,
triggerCommentWritterProfileImage,
@@ -68,6 +79,10 @@ const FeedItem = ({ solutionId, groupId }: FeedItemProps) => {
triggerComment?.createdAt,
];
+ const handleCommentCountPlus = () => {
+ myAddedCommentsRef.current += 1;
+ };
+
return (
@@ -103,7 +118,7 @@ const FeedItem = ({ solutionId, groupId }: FeedItemProps) => {
/>
- {comments?.map((comment) => (
+ {comments?.slice(0, displayedCommentCount).map((comment) => (
- {
))}
+ {comments?.length > displayedCommentCount && (
+
+
+ {`댓글 +${comments?.length - displayedCommentCount}`}
+ 더보기
+
+
+ )}
+
{
mutationFn: (id: number) => patchNotificationRead(id),
onMutate: async (id: number) => {
await queryClient.cancelQueries({
- queryKey: notificationQueryKey.lists(notificationType),
+ queryKey: notificationQueryKey.typeList(notificationType),
});
const prev = queryClient.getQueryData(
- notificationQueryKey.lists(notificationType),
+ notificationQueryKey.typeList(notificationType),
);
const newData = prev?.map((item) =>
item.id === id ? { ...item, isRead: true } : item,
);
queryClient.setQueryData(
- notificationQueryKey.lists(notificationType),
+ notificationQueryKey.typeList(notificationType),
newData,
);
return { prev };
},
- onSettled: async () => {
- await queryClient.invalidateQueries({
- queryKey: notificationQueryKey.lists(notificationType),
- });
+ onSuccess: (_data, id) => {
+ queryClient.setQueriesData(
+ { queryKey: [...notificationQueryKey.all(), "list"] },
+ (oldData) => {
+ if (!oldData) return [];
+ return oldData.map((item) =>
+ item.id === id ? { ...item, isRead: true } : item,
+ );
+ },
+ );
},
onError: (_err, _new, context) => {
queryClient.setQueryData(
- notificationQueryKey.lists(notificationType),
+ notificationQueryKey.typeList(notificationType),
context?.prev,
);
},
@@ -113,37 +120,79 @@ export const useDeleteNotiMutation = (notificationType: NotificationType) => {
return useMutation({
mutationFn: (id: number) => deleteNotification(id),
- onSuccess: () => {
- queryClient.invalidateQueries({
- queryKey: notificationQueryKey.lists(notificationType),
+ onMutate: async (id: number) => {
+ await queryClient.cancelQueries({
+ queryKey: notificationQueryKey.typeList(notificationType),
});
+ const prev = queryClient.getQueryData(
+ notificationQueryKey.typeList(notificationType),
+ );
+ const newData = prev?.filter((item) => item.id !== id);
+ queryClient.setQueryData(
+ notificationQueryKey.typeList(notificationType),
+ newData,
+ );
+ return { prev, id };
+ },
+ onSuccess: (_data, id) => {
+ // 모든 알림 리스트 쿼리에서 해당 알림 제거
+ queryClient.setQueriesData(
+ { queryKey: [...notificationQueryKey.all(), "list"] },
+ (oldData) => {
+ if (!oldData) return [];
+ return oldData.filter((item) => item.id !== id);
+ },
+ );
+ },
+ onError: (_err, _new, context) => {
+ if (context?.prev) {
+ queryClient.setQueryData(
+ notificationQueryKey.typeList(notificationType),
+ context.prev,
+ );
+ }
},
});
};
-// export const useReadAllNotiMutation = () => {
-// const queryClient = useQueryClient();
-
-// return useMutation({
-// mutationFn: patchAllNotificationRead,
-// onMutate: async () => {
-// await queryClient.cancelQueries({
-// queryKey: notificationQueryKey.lists(),
-// });
-// const prev = queryClient.getQueryData(
-// notificationQueryKey.lists(),
-// );
-// const newData = prev?.map((item) => ({ ...item, isRead: true }));
-// queryClient.setQueryData(notificationQueryKey.lists(), newData);
-// return { prev };
-// },
-// onSettled: async () => {
-// await queryClient.invalidateQueries({
-// queryKey: notificationQueryKey.lists(),
-// });
-// },
-// onError: (_err, _new, context) => {
-// queryClient.setQueryData(notificationQueryKey.lists(), context?.prev);
-// },
-// });
-// };
+export const useReadAllNotiMutation = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: patchAllNotificationRead,
+ onMutate: async () => {
+ await queryClient.cancelQueries({
+ queryKey: notificationQueryKey.list(),
+ });
+
+ // 모든 매칭되는 쿼리의 이전 데이터 저장
+ const previousQueries = queryClient.getQueriesData({
+ queryKey: notificationQueryKey.list(),
+ });
+
+ // 모든 쿼리를 optimistic update
+ queryClient.setQueriesData(
+ { queryKey: notificationQueryKey.list() },
+ (oldData) => {
+ if (!oldData) return [];
+ return oldData.map((item) => ({ ...item, isRead: true }));
+ },
+ );
+
+ return { previousQueries };
+ },
+ onSettled: async () => {
+ await queryClient.invalidateQueries({
+ queryKey: notificationQueryKey.list(),
+ });
+ },
+ onError: (_err, _new, context) => {
+ // 실패 시 모든 쿼리를 이전 데이터로 롤백
+ if (context?.previousQueries) {
+ for (const [queryKey, data] of context.previousQueries) {
+ queryClient.setQueryData(queryKey, data);
+ }
+ }
+ },
+ });
+};
diff --git a/apps/client/src/app/api/notifications/query.ts b/apps/client/src/app/api/notifications/query.ts
index a6c87531..db6f9bc1 100644
--- a/apps/client/src/app/api/notifications/query.ts
+++ b/apps/client/src/app/api/notifications/query.ts
@@ -5,8 +5,9 @@ import { getNotificationList, getNotificationsSettings } from "./index";
export const notificationQueryKey = {
all: () => ["notifications"] as const,
settings: () => [...notificationQueryKey.all(), "settings"] as const,
- lists: (notificationType: NotificationType) =>
- [...notificationQueryKey.all(), "list", notificationType] as const,
+ list: () => [...notificationQueryKey.all(), "list"] as const,
+ typeList: (notificationType?: NotificationType) =>
+ [...notificationQueryKey.list(), notificationType] as const,
};
export const useNotificationSettingListQueryObject = () =>
@@ -19,6 +20,6 @@ export const useNotificationsQueryObject = (
notificationType: NotificationType,
) =>
queryOptions({
- queryKey: notificationQueryKey.lists(notificationType),
+ queryKey: notificationQueryKey.typeList(notificationType),
queryFn: () => getNotificationList({ notificationType }),
});
diff --git a/apps/client/src/shared/component/Header/Notification/index.css.ts b/apps/client/src/shared/component/Header/Notification/index.css.ts
index d24e0ff8..73dec818 100644
--- a/apps/client/src/shared/component/Header/Notification/index.css.ts
+++ b/apps/client/src/shared/component/Header/Notification/index.css.ts
@@ -22,6 +22,14 @@ export const notificationContainer = style({
});
export const headerStyle = style({
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-between",
+
+ marginRight: "1.6rem",
+});
+
+export const titleWrapper = style({
display: "flex",
alignItems: "center",
gap: "0.8rem",
@@ -32,6 +40,27 @@ export const titleStyle = style({
...theme.font.Head2_B_18,
});
+export const readAllButtonStyle = recipe({
+ base: {
+ padding: "0.4rem 0.8rem",
+
+ borderRadius: "17px",
+ backgroundColor: theme.color.mg5,
+
+ ...theme.font.Body1_M_14,
+ },
+ variants: {
+ isAllRead: {
+ true: {
+ color: theme.color.mg3,
+ },
+ false: {
+ color: theme.color.lg1,
+ },
+ },
+ },
+});
+
export const countChipStyle = style({
display: "flex",
alignItems: "center",
diff --git a/apps/client/src/shared/component/Header/Notification/index.tsx b/apps/client/src/shared/component/Header/Notification/index.tsx
index d56d0457..2edad436 100644
--- a/apps/client/src/shared/component/Header/Notification/index.tsx
+++ b/apps/client/src/shared/component/Header/Notification/index.tsx
@@ -1,15 +1,18 @@
"use client";
+import { useReadAllNotiMutation } from "@/app/api/notifications/mutation";
import { useNotificationsQueryObject } from "@/app/api/notifications/query";
import { IcnBellHeader } from "@/asset/svg";
import NotificationList from "@/shared/component/Header/Notification/NotificationList";
import NotificationTab from "@/shared/component/Header/Notification/NotificationTab";
-import { notificationTabListStyle } from "@/shared/component/Header/Notification/index.css";
import {
countChipStyle,
countStyle,
headerStyle,
notificationContainer,
+ notificationTabListStyle,
+ readAllButtonStyle,
titleStyle,
+ titleWrapper,
} from "@/shared/component/Header/Notification/index.css";
import { iconStyle } from "@/shared/component/Header/index.css";
import { useQuery } from "@tanstack/react-query";
@@ -39,6 +42,8 @@ const Notification = () => {
const { data } = useQuery(useNotificationsQueryObject(notificationType));
const notiCounts = data ? data.filter((item) => !item.isRead).length : 0;
+ const { mutate: readAllNotifications } = useReadAllNotiMutation();
+
const shrinkList = () => {
setIsExpanded(false);
};
@@ -50,10 +55,19 @@ const Notification = () => {
return (
-
- 알림
-
- {`신규 ${notiCounts}`}
+
+
+ 알림
+
+
{`신규 ${notiCounts}`}
+
+