From 1bbab6dc3f3fe51b3296cb28553ce09c9b2bb566 Mon Sep 17 00:00:00 2001 From: jungmyunggi Date: Sat, 4 Jan 2025 23:18:16 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=B5=9C=EC=8B=A0=ED=8F=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=90=EB=8F=99=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EB=B0=8F=20NEW=ED=83=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/api/services/posts.ts | 9 +- .../src/components/common/Card/PostCard.tsx | 3 +- .../components/common/Card/PostCardImage.tsx | 8 +- .../src/components/common/SectionHeader.tsx | 18 ++-- .../src/components/sections/LatestSection.tsx | 6 +- .../sections/LatestSectionTimer.tsx | 93 +++++++++++++++++++ client/src/hooks/queries/useUpdatePost.ts | 7 ++ client/src/types/post.ts | 6 ++ 8 files changed, 137 insertions(+), 13 deletions(-) create mode 100644 client/src/components/sections/LatestSectionTimer.tsx create mode 100644 client/src/hooks/queries/useUpdatePost.ts diff --git a/client/src/api/services/posts.ts b/client/src/api/services/posts.ts index e54dc836..aba73072 100644 --- a/client/src/api/services/posts.ts +++ b/client/src/api/services/posts.ts @@ -1,5 +1,5 @@ import { axiosInstance } from "@/api/instance"; -import { InfiniteScrollResponse, LatestPostsApiResponse, Post } from "@/types/post"; +import { InfiniteScrollResponse, LatestPostsApiResponse, Post, UpdatePostsApiResponse } from "@/types/post"; export const posts = { latest: async (params: { limit: number; lastId: number }): Promise> => { @@ -15,4 +15,11 @@ export const posts = { lastId: response.data.data.lastId, }; }, + update: async (): Promise => { + const response = await axiosInstance.get("/api/feed/recent"); + return { + message: response.data.message, + data: response.data.data, + }; + }, }; diff --git a/client/src/components/common/Card/PostCard.tsx b/client/src/components/common/Card/PostCard.tsx index 1f705f47..9738bb4c 100644 --- a/client/src/components/common/Card/PostCard.tsx +++ b/client/src/components/common/Card/PostCard.tsx @@ -14,7 +14,6 @@ interface PostCardProps { export const PostCard = ({ post, className }: PostCardProps) => { const { handlePostClick } = usePostCardActions(post); - return ( { className )} > - + ); diff --git a/client/src/components/common/Card/PostCardImage.tsx b/client/src/components/common/Card/PostCardImage.tsx index 20c043d0..4daec9fc 100644 --- a/client/src/components/common/Card/PostCardImage.tsx +++ b/client/src/components/common/Card/PostCardImage.tsx @@ -4,15 +4,21 @@ import { LazyImage } from "@/components/common/LazyImage"; interface PostCardImageProps { thumbnail?: string; + isNew?: boolean; alt: string; } -export const PostCardImage = ({ thumbnail, alt }: PostCardImageProps) => { +export const PostCardImage = ({ thumbnail, alt, isNew }: PostCardImageProps) => { return (
+ {isNew && ( + + New + + )} {thumbnail ? ( { return ( -
- {Icon && ( -
- -
- )} -

{text}

-

{description}

+
+
+ {Icon && ( +
+ +
+ )} +

{text}

+

{description}

+
); }; diff --git a/client/src/components/sections/LatestSection.tsx b/client/src/components/sections/LatestSection.tsx index d4d2011c..bfd7e220 100644 --- a/client/src/components/sections/LatestSection.tsx +++ b/client/src/components/sections/LatestSection.tsx @@ -5,6 +5,7 @@ import { Rss } from "lucide-react"; import { PostCardGrid } from "@/components/common/Card/PostCardGrid"; import { PostGridSkeleton } from "@/components/common/Card/PostCardSkeleton.tsx"; import { SectionHeader } from "@/components/common/SectionHeader"; +import LatestSectionTimer from "@/components/sections/LatestSectionTimer"; import { useInfiniteScrollQuery } from "@/hooks/queries/useInfiniteScrollQuery"; @@ -37,7 +38,10 @@ export default function LatestSection() { return (
- +
+ + +
{isLoading ? ( diff --git a/client/src/components/sections/LatestSectionTimer.tsx b/client/src/components/sections/LatestSectionTimer.tsx new file mode 100644 index 00000000..6f8e5ba5 --- /dev/null +++ b/client/src/components/sections/LatestSectionTimer.tsx @@ -0,0 +1,93 @@ +import { useEffect, useState } from "react"; + +import { RotateCw } from "lucide-react"; + +import { useUpdatePost } from "@/hooks/queries/useUpdatePost"; + +import { UpdatePostsApiResponse } from "@/types/post"; +import { Post } from "@/types/post"; +import { useQueryClient } from "@tanstack/react-query"; + +export default function LatestSectionTimer() { + const queryClient = useQueryClient(); + const update = useUpdatePost(); + const [timer, setTimer] = useState(0); + + const calculateTime = () => { + const now = new Date(); + const currentMinutes = now.getMinutes(); + + const targetMinutes = currentMinutes < 31 ? 31 : 1; + let targetHours = now.getHours(); + + if (currentMinutes >= 31) { + targetHours = (targetHours + 1) % 24; + } + + const targetTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), targetHours, targetMinutes, 0); + const remainingTime = Math.floor((targetTime.getTime() - now.getTime()) / 1000); + + return remainingTime; + }; + + useEffect(() => { + setTimer(calculateTime()); + const interval = setInterval(() => { + const time = calculateTime(); + setTimer(time); + if (time === 0) { + handleUpdate(); + setTimer(calculateTime()); + } + }, 1000); + return () => clearInterval(interval); + }, []); + + const handleUpdate = async () => { + const oldPosts = queryClient.getQueryData<{ + pageParams: number; + pages: { hasMore: boolean; lastId: string; result: Post[] }[]; + }>(["latest-posts"]); + if (update.data && oldPosts) { + oldPosts.pages.forEach((oldPost) => + oldPost.result.forEach((post) => { + post.isNew = false; + }) + ); + queryClient.setQueryData(["latest-posts"], { + ...oldPosts, + pages: [ + { + ...oldPosts.pages[0], + result: [...update.data.data, ...oldPosts.pages[0].result], + }, + ], + }); + } + }; + + return ; +} + +function Timer({ + time, + handleUpdate, + update, +}: { + time: number; + handleUpdate: () => void; + update: { data?: UpdatePostsApiResponse; isLoading: boolean; error: Error | null }; +}) { + const formatTime = (time: number) => { + const minutes = Math.floor(time / 60); + const seconds = time % 60; + return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")} 초`; + }; + + return ( + + {update.error && } + {update.isLoading ? : {formatTime(time)} 후 업데이트} + + ); +} diff --git a/client/src/hooks/queries/useUpdatePost.ts b/client/src/hooks/queries/useUpdatePost.ts new file mode 100644 index 00000000..7896f1e8 --- /dev/null +++ b/client/src/hooks/queries/useUpdatePost.ts @@ -0,0 +1,7 @@ +import { posts } from "@/api/services/posts"; +import { useQuery } from "@tanstack/react-query"; + +export const useUpdatePost = () => { + const { data, isLoading, error } = useQuery({ queryKey: ["update-posts"], queryFn: posts.update, retry: 1 }); + return { data, isLoading, error }; +}; diff --git a/client/src/types/post.ts b/client/src/types/post.ts index 065e002c..12b3e571 100644 --- a/client/src/types/post.ts +++ b/client/src/types/post.ts @@ -9,6 +9,7 @@ export interface Post { authorImageUrl?: string; tags?: string[]; likes?: number; + isNew?: boolean; blogPlatform?: string; } @@ -31,3 +32,8 @@ export interface InfiniteScrollResponse { hasMore: boolean; lastId: number | null; } + +export interface UpdatePostsApiResponse { + message: string; + data: Post[]; +}