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
3 changes: 3 additions & 0 deletions apps/client/src/app/[user]/components/CommentInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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("");

Expand All @@ -33,6 +35,7 @@ const CommentInput = ({

postComment(comment);
setComment("");
onCommentCountPlus();
};

return (
Expand Down
36 changes: 36 additions & 0 deletions apps/client/src/app/[user]/components/FeedItem/index.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
});
42 changes: 35 additions & 7 deletions apps/client/src/app/[user]/components/FeedItem/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import {
feedItemContainer,
infoTextWrapper,
infoWrapper,
moreCommentButtonStyle,
moreCommentContainer,
moreCommentWrapper,
nameStyle,
studyNameStyle,
} from "@/app/[user]/components/FeedItem/index.css";
Expand All @@ -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: [
Expand All @@ -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,
Expand All @@ -68,6 +79,10 @@ const FeedItem = ({ solutionId, groupId }: FeedItemProps) => {
triggerComment?.createdAt,
];

const handleCommentCountPlus = () => {
myAddedCommentsRef.current += 1;
};

return (
<li>
<article className={feedItemContainer}>
Expand Down Expand Up @@ -103,7 +118,7 @@ const FeedItem = ({ solutionId, groupId }: FeedItemProps) => {
/>

<ul className={commentListStyle}>
{comments?.map((comment) => (
{comments?.slice(0, displayedCommentCount).map((comment) => (
<li
key={comment.commentId}
className={commentItemStyle}
Expand All @@ -122,7 +137,20 @@ const FeedItem = ({ solutionId, groupId }: FeedItemProps) => {
))}
</ul>

{comments?.length > displayedCommentCount && (
<Link
href={`/group/${groupId}/solved-detail/${solutionId}`}
className={moreCommentContainer}
>
<div className={moreCommentWrapper}>
<span>{`댓글 +${comments?.length - displayedCommentCount}`}</span>
<span className={moreCommentButtonStyle}>더보기</span>
</div>
</Link>
)}
Comment on lines +140 to +150
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment 관련된 건 FeedItem 컴포넌트에서 분리됐으면 좋겠어요!
comment 없는 경우 해당 컴포넌트를 아예 렌더링하지 않게끔!
지금은 comment 관련해서 optional 체이닝이 많아서 관심사 분리가 안 되고 컴포넌트 최적화가 안 돼보여요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어차피 지금 피드가 뜨는게 댓글이 달려있는경우에 뜨는거라 FeedItem에서 댓글이 없는 경우는 없어요!
그리고 FeedItem 전반에서 comment 데이터가 사용되어서 컴포넌트 분리를 해도 크게 관심사 분리가 되지않는다고 생각해서 같이 두었어요.
또한 옵셔널 체이닝도 더보기 버튼 하나 말고는 없어서 분리하는것보다 현상태를 유지하는게 좋다고 생각합니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아래 사항은 readAllNotifications가 mutate 함수라서 바로 넣으면 event 객체를 매개변수로 받지 않아서 에러가 떠요.
따라서 화살표 함수로 한번 감싸서 넣어줘야 타입에러가 안터지네요!


<CommentInput
onCommentCountPlus={handleCommentCountPlus}
solutionId={solutionId}
profileUrl={solution?.profileImage}
nickname={solution?.nickname}
Expand Down
123 changes: 86 additions & 37 deletions apps/client/src/app/api/notifications/mutation.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

문제, 코멘트, 스터디 탭에서 지운 알림이 전체 탭에는 그대로 남아 있는 문제가 있으므로 useReadNotiItemMutationonSettled에서 NotificationType.ALL을 처리하도록 추가해야 해요.

이 부분을 invalidate로 처리하기 보다는 setQueryDataArray.filter 메서드를 사용해서 데이터를 직접 처리하는 게 좋아요. 단순히 전체 리스트에서 해당 알림 하나만 지우면 되는 부분이고, 페이지네이션을 쓰고 있거나 또 다른 쿼리를 사용하여 조합하는 복잡한 구성의 데이터가 아니므로 invalidate를 사용하지 않아도 되기 때문이에요.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

삭제는 useDeleteNotiMutation 담당이라 해당 훅에 반영했습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전체 탭의 알림, 즉 다른 탭의 알림을 삭제해주는 로직이어서 굳이 onSettled에서 처리하기 보다는 onSuccess에서 삭제 성공시 처리해주는게 효율적이라 생각해서 onSuccess에 로직 추가해주었어요.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { HTTPError } from "ky";
import {
deleteNotification,
patchAllNotificationRead,
patchNotificationRead,
patchNotificationsSettings,
} from "./index";
Expand Down Expand Up @@ -80,28 +81,34 @@ export const useReadNotiItemMutation = (notificationType: NotificationType) => {
mutationFn: (id: number) => patchNotificationRead(id),
onMutate: async (id: number) => {
await queryClient.cancelQueries({
queryKey: notificationQueryKey.lists(notificationType),
queryKey: notificationQueryKey.typeList(notificationType),
});
const prev = queryClient.getQueryData<NotificationItem[]>(
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<NotificationItem[]>(
{ 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,
);
},
Expand All @@ -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<NotificationItem[]>(
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<NotificationItem[]>(
{ 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<NotificationItem[]>(
// 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<NotificationItem[]>({
queryKey: notificationQueryKey.list(),
});

// 모든 쿼리를 optimistic update
queryClient.setQueriesData<NotificationItem[]>(
{ 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);
}
}
},
});
};
7 changes: 4 additions & 3 deletions apps/client/src/app/api/notifications/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () =>
Expand All @@ -19,6 +20,6 @@ export const useNotificationsQueryObject = (
notificationType: NotificationType,
) =>
queryOptions({
queryKey: notificationQueryKey.lists(notificationType),
queryKey: notificationQueryKey.typeList(notificationType),
queryFn: () => getNotificationList({ notificationType }),
});
29 changes: 29 additions & 0 deletions apps/client/src/shared/component/Header/Notification/index.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Loading