diff --git a/src/entities/auction/ui/user-item-card/ui/user-item-card.tsx b/src/entities/auction/ui/user-item-card/ui/user-item-card.tsx index 7ddd4f46..901f79e9 100644 --- a/src/entities/auction/ui/user-item-card/ui/user-item-card.tsx +++ b/src/entities/auction/ui/user-item-card/ui/user-item-card.tsx @@ -1,3 +1,4 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ import React from "react"; import Image from "next/image"; @@ -44,50 +45,21 @@ export function UserItemCard({ footerNode, onClick, }: UserItemCardProps) { - const isInteractive = !!onClick; + const isInteractive = !!onClick || !!imageHref; - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key !== "Enter" && e.key !== " ") return; - e.preventDefault(); - onClick?.(); + const handleKeyDown = (e: React.KeyboardEvent) => { + if (onClick && (e.key === "Enter" || e.key === " ")) { + e.preventDefault(); + onClick(); + } }; - const interactiveProps = isInteractive - ? { - role: "button" as const, - tabIndex: 0, - onKeyDown: handleKeyDown, - onClick, - } - : {}; - - const priceClassName = cn( - "font-bold tracking-tight", - isPriceGray ? "text-muted-foreground" : "text-brand-text" - ); - - const renderImage = () => { - const content = imageUrl ? ( - {name} - ) : ( - - ); - - if (!imageHref) return content; - - return ( - e.stopPropagation()} - aria-label={`${name} 상세로 이동`} - > - {content} - + const renderPrice = () => { + const priceClassName = cn( + "font-bold tracking-tight", + isPriceGray ? "text-muted-foreground" : "text-brand-text" ); - }; - const renderPrice = () => { if (price != null && discountRate != null && discountRate > 0) { return (
@@ -110,40 +82,55 @@ export function UserItemCard({ }; return ( -
-
-
- {renderImage()} - {overlayNode} -
+ {imageHref && ( + + )} -
-
-
- {badgeNode} - {actionNode} -
+
+
+
+ {imageUrl ? ( + {name} + ) : ( + + )} + {overlayNode &&
{overlayNode}
} +
-
-

- {name} -

-
{renderPrice()}
+
+
+
+ {badgeNode} + {actionNode} +
+ +
+

+ {name} +

+
{renderPrice()}
+
+

{date}

- -

{date}

-
- {footerNode} -
+ {footerNode &&
{footerNode}
} +
+ ); } diff --git a/src/features/notification-preference/index.ts b/src/features/notification-preference/index.ts index 03339b61..d1d231aa 100644 --- a/src/features/notification-preference/index.ts +++ b/src/features/notification-preference/index.ts @@ -2,6 +2,7 @@ export { NotificationPreferenceItemCard } from "./ui/notification-preference-ite export { NotificationPreferenceSettingsModal } from "./ui/notification-preference-settings-modal"; export type { NotificationPreferenceItemType } from "./model/types"; export { + notificationKeys, useNotificationList, useNotificationSettings, useUpdateNotificationSettings, diff --git a/src/features/review/api/use-reviews.ts b/src/features/review/api/use-reviews.ts index 48d29678..24f95c07 100644 --- a/src/features/review/api/use-reviews.ts +++ b/src/features/review/api/use-reviews.ts @@ -1,6 +1,12 @@ "use client"; -import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + useInfiniteQuery, + useMutation, + useQuery, + useQueryClient, + type InfiniteData, +} from "@tanstack/react-query"; import dayjs from "dayjs"; import type { ReviewType } from "@/entities/review"; @@ -77,7 +83,7 @@ const fetchUserReviews = async ( slice: [], hasNext: false, page: pageParam, - size: 10, + size: 5, timeStamp: "", }; } @@ -149,11 +155,38 @@ export function useCreateReview() { export function useUpdateReview() { const queryClient = useQueryClient(); + return useMutation({ mutationFn: ({ reviewId, data }: { reviewId: number; data: UpdateReviewRequest }) => httpClient(API_ENDPOINTS.reviewDetail(reviewId), { method: "PATCH", body: data }), + onSuccess: (_, vars) => { - queryClient.invalidateQueries({ queryKey: reviewKeys.detail(vars.reviewId) }); + const { reviewId, data: updatedData } = vars; + + queryClient.setQueryData(reviewKeys.detail(reviewId), (old) => + old ? { ...old, rating: updatedData.rating, content: updatedData.content } : old + ); + + queryClient.setQueriesData>>( + { queryKey: reviewKeys.all }, + (oldData) => { + if (!oldData?.pages) return oldData; + + return { + ...oldData, + pages: oldData.pages.map((page) => ({ + ...page, + slice: page.slice.map((review) => + review.id === reviewId + ? { ...review, rating: updatedData.rating, content: updatedData.content } + : review + ), + })), + }; + } + ); + + queryClient.invalidateQueries({ queryKey: reviewKeys.all }); queryClient.invalidateQueries({ queryKey: purchaseKeys.all }); }, }); @@ -165,6 +198,7 @@ export function useDeleteReview() { mutationFn: (reviewId: number) => httpClient(API_ENDPOINTS.reviewDetail(reviewId), { method: "DELETE" }), onSuccess: () => { + queryClient.invalidateQueries({ queryKey: reviewKeys.all }); queryClient.invalidateQueries({ queryKey: purchaseKeys.all }); }, }); diff --git a/src/widgets/notification-preference/ui/notification-preference-list.tsx b/src/widgets/notification-preference/ui/notification-preference-list.tsx index 883cd1d5..904159a1 100644 --- a/src/widgets/notification-preference/ui/notification-preference-list.tsx +++ b/src/widgets/notification-preference/ui/notification-preference-list.tsx @@ -2,17 +2,19 @@ import { useMemo, useState, useEffect } from "react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { BellOff } from "lucide-react"; import { useInView } from "react-intersection-observer"; import { UserItemCardFilter } from "@/entities/auction"; +import { getAuctionNotificationSettings } from "@/entities/notification/api/notification-setting"; import { NotificationPreferenceItemCard, NotificationPreferenceItemType, NotificationPreferenceSettingsModal, useNotificationList, - useNotificationSettings, useUpdateNotificationSettings, + notificationKeys, } from "@/features/notification-preference"; import { filterItemsByStatus, @@ -26,6 +28,8 @@ import { CommonItemListSkeleton } from "@/widgets/user/ui/skeletons"; const NOTI_STATUSES = ["판매중", "판매 완료", "경매 예정", "경매 종료"]; export function NotificationPreferenceList({ label }: { label?: React.ReactNode }) { + const queryClient = useQueryClient(); + const { data, isPending: isListPending, @@ -37,9 +41,14 @@ export function NotificationPreferenceList({ label }: { label?: React.ReactNode const [filterStatus, setFilterStatus] = useState("전체"); const [selectedNoti, setSelectedNoti] = useState(null); + const [isFetching, setIsFetching] = useState(false); - const activeAuctionId = selectedNoti ? Number(selectedNoti.id) : 0; - const { data: currentSettings } = useNotificationSettings(activeAuctionId); + const { data: remoteSettings } = useQuery({ + queryKey: notificationKeys.settings(Number(selectedNoti?.id)), + queryFn: () => getAuctionNotificationSettings(selectedNoti!.id), + enabled: !!selectedNoti, + staleTime: 1000 * 60, + }); const { mutate: updateSettings } = useUpdateNotificationSettings(); @@ -54,6 +63,24 @@ export function NotificationPreferenceList({ label }: { label?: React.ReactNode } }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); + const handleOpenSettings = async (item: NotificationPreferenceItemType) => { + if (isFetching) return; + + setIsFetching(true); + try { + await queryClient.fetchQuery({ + queryKey: notificationKeys.settings(Number(item.id)), + queryFn: () => getAuctionNotificationSettings(item.id), + }); + + setSelectedNoti(item); + } catch { + showToast.error("설정 정보를 불러오는 데 실패했습니다."); + } finally { + setIsFetching(false); + } + }; + const notifications = useMemo(() => data?.pages.flatMap((page) => page.slice) ?? [], [data]); const filteredNotis = useMemo( @@ -62,10 +89,10 @@ export function NotificationPreferenceList({ label }: { label?: React.ReactNode ); const handleSaveSettings = (settingsData: Parameters[0]["data"]) => { - if (!activeAuctionId) return; + if (!selectedNoti) return; updateSettings( - { auctionId: activeAuctionId, data: settingsData }, + { auctionId: Number(selectedNoti.id), data: settingsData }, { onSuccess: () => { showToast.success("알림 설정이 저장되었습니다."); @@ -89,7 +116,7 @@ export function NotificationPreferenceList({ label }: { label?: React.ReactNode ))} @@ -136,10 +163,12 @@ export function NotificationPreferenceList({ label }: { label?: React.ReactNode { - if (!open) setSelectedNoti(null); + if (!open) { + setSelectedNoti(null); + } }} item={selectedNoti} - initialSettings={currentSettings} + initialSettings={remoteSettings ?? undefined} onSave={handleSaveSettings} />