diff --git a/.pnp.cjs b/.pnp.cjs index 1dbfe132..3d47edb7 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -2135,6 +2135,7 @@ const RAW_RUNTIME_STATE = ["class-variance-authority", "npm:0.7.1"],\ ["clsx", "npm:2.1.1"],\ ["date-fns", "npm:4.1.0"],\ + ["es-toolkit", "npm:1.41.0"],\ ["identity-obj-proxy", "npm:3.0.0"],\ ["jest", "virtual:aa30a3c749b831fda0e7d0c36b75cc1150505c665cee2065ceaf5d3590984e9a3e86499ef1cb3e6374dd2ee412223e8ce7a4cbd0b9b5d40fbad1a1b5395f2ac4#npm:30.1.3"],\ ["jest-environment-jsdom", "virtual:aa30a3c749b831fda0e7d0c36b75cc1150505c665cee2065ceaf5d3590984e9a3e86499ef1cb3e6374dd2ee412223e8ce7a4cbd0b9b5d40fbad1a1b5395f2ac4#npm:30.1.2"],\ @@ -6138,6 +6139,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["es-toolkit", [\ + ["npm:1.41.0", {\ + "packageLocation": "./.yarn/cache/es-toolkit-npm-1.41.0-11db103f64-ff7ba3130c.zip/node_modules/es-toolkit/",\ + "packageDependencies": [\ + ["es-toolkit", "npm:1.41.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["esbuild", [\ ["npm:0.21.5", {\ "packageLocation": "./.yarn/unplugged/esbuild-npm-0.21.5-d85dfbc965/node_modules/esbuild/",\ diff --git a/fundamentals/today-i-learned/package.json b/fundamentals/today-i-learned/package.json index c290aaba..2cc6b078 100644 --- a/fundamentals/today-i-learned/package.json +++ b/fundamentals/today-i-learned/package.json @@ -24,6 +24,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", + "es-toolkit": "^1.41.0", "lucide-react": "^0.525.0", "overlay-kit": "^1.8.4", "react": "^18.2.0", diff --git a/fundamentals/today-i-learned/src/api/hooks/useDiscussions.ts b/fundamentals/today-i-learned/src/api/hooks/useDiscussions.ts index 98344439..df3921b1 100644 --- a/fundamentals/today-i-learned/src/api/hooks/useDiscussions.ts +++ b/fundamentals/today-i-learned/src/api/hooks/useDiscussions.ts @@ -2,6 +2,7 @@ import { useAuth } from "@/contexts/AuthContext"; import { ENV_CONFIG } from "@/utils/env"; import { useInfiniteQuery, + useSuspenseQuery, useMutation, useQuery, useQueryClient @@ -122,12 +123,11 @@ export function useInfiniteDiscussions({ export function useWeeklyTopDiscussions({ owner = ENV_CONFIG.GITHUB_OWNER, repo = ENV_CONFIG.GITHUB_REPO, - limit, - enabled = true + limit }: Omit & { limit?: number } = {}) { const { user } = useAuth(); - return useQuery({ + return useSuspenseQuery({ queryKey: DISCUSSIONS_QUERY_KEYS.weekly(), queryFn: async () => { const discussions = await fetchWeeklyTopDiscussions({ @@ -137,7 +137,6 @@ export function useWeeklyTopDiscussions({ }); return limit ? discussions.slice(0, limit) : discussions; }, - enabled, staleTime: 1000 * 60 * 30, // 30분 gcTime: 1000 * 60 * 60, // 1시간 retry: 1 @@ -240,14 +239,13 @@ export function useMyContributions({ export function useDiscussionDetail(id: string) { const { user } = useAuth(); - return useQuery({ + return useSuspenseQuery({ queryKey: DISCUSSIONS_QUERY_KEYS.detail(id), queryFn: () => fetchDiscussionDetail({ id, accessToken: user?.accessToken }), - enabled: !!id, staleTime: 1000 * 60 * 5, // 5분 gcTime: 1000 * 60 * 30, // 30분 retry: 2 diff --git a/fundamentals/today-i-learned/src/components/features/discussions/PostDetail.tsx b/fundamentals/today-i-learned/src/components/features/discussions/PostDetail.tsx deleted file mode 100644 index 26e849c7..00000000 --- a/fundamentals/today-i-learned/src/components/features/discussions/PostDetail.tsx +++ /dev/null @@ -1,512 +0,0 @@ -import { useState } from "react"; -import { Avatar } from "@/components/shared/ui/Avatar"; -import { MarkdownRenderer } from "@/components/shared/ui/MarkdownRenderer"; -import { InteractionButtons } from "@/components/shared/ui/InteractionButtons"; -import type { GitHubDiscussion } from "@/api/remote/discussions"; -import { - useDiscussionDetail, - useAddDiscussionComment, - useAddDiscussionCommentReply, - useToggleDiscussionReaction -} from "@/api/hooks/useDiscussions"; -import { useAuth } from "@/contexts/AuthContext"; -import { CommentList } from "@/pages/timeline/components/CommentList"; -import { - hasUserReacted, - getHeartAndUpvoteCounts, - getUserReactionStates -} from "@/utils/reactions"; -import type { GitHubComment } from "@/api/remote/discussions"; -import { css } from "@styled-system/css"; - -interface PostDetailProps { - discussion: GitHubDiscussion; - onLike?: (postId: string) => void; - onUpvote?: (postId: string) => void; - showComments?: boolean; -} - -function formatTimeAgo(dateString: string): string { - const now = new Date(); - const date = new Date(dateString); - const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000); - - if (diffInSeconds < 60) { - return "방금 전"; - } - if (diffInSeconds < 3600) { - return `${Math.floor(diffInSeconds / 60)}분 전`; - } - if (diffInSeconds < 86400) { - return `${Math.floor(diffInSeconds / 3600)}시간 전`; - } - return `${Math.floor(diffInSeconds / 86400)}일 전`; -} - -export function PostDetail({ - discussion, - onLike, - onUpvote, - showComments = true -}: PostDetailProps) { - const [commentText, setCommentText] = useState(""); - const { user } = useAuth(); - - const { data: discussionDetail, isLoading: isDetailLoading } = - useDiscussionDetail(discussion.id); - const addCommentMutation = useAddDiscussionComment(); - const addCommentReplyMutation = useAddDiscussionCommentReply(); - const toggleReactionMutation = useToggleDiscussionReaction(); - - const actualDiscussion = discussionDetail || discussion; - const comments = discussionDetail?.comments?.nodes || []; - - // Helper function to find comment by ID (including nested replies) - const findCommentById = ( - comments: GitHubComment[], - id: string - ): GitHubComment | null => { - for (const comment of comments) { - if (comment.id === id) return comment; - if (comment.replies?.nodes) { - const found = findCommentById(comment.replies.nodes, id); - if (found) return found; - } - } - return null; - }; - - const authorInfo = { - src: actualDiscussion.author.avatarUrl, - alt: actualDiscussion.author.login, - fallback: actualDiscussion.author.login, - name: actualDiscussion.author.login, - username: actualDiscussion.author.login - }; - - const handleCommentSubmit = async () => { - if (commentText.trim() && user?.accessToken) { - try { - await addCommentMutation.mutateAsync({ - discussionId: discussion.id, - body: commentText - }); - setCommentText(""); - } catch (error) { - console.error("댓글 작성 실패:", error); - } - } - }; - - const handleReaction = async (type: "like" | "upvote") => { - if (!user?.accessToken || !user?.login) { - return; - } - - try { - const reactionContent = type === "like" ? "HEART" : "THUMBS_UP"; - - const { hasLiked: currentHasLiked, hasUpvoted: currentHasUpvoted } = - getUserReactionStates(actualDiscussion.reactions, user.login); - const isCurrentlyReacted = - type === "like" ? currentHasLiked : currentHasUpvoted; - - await toggleReactionMutation.mutateAsync({ - subjectId: discussion.id, - isReacted: isCurrentlyReacted, - content: reactionContent - }); - - if (type === "like" && onLike) { - onLike(discussion.id); - } - if (type === "upvote" && onUpvote) { - onUpvote(discussion.id); - } - } catch (error) { - console.error("반응 처리 실패:", error); - } - }; - - const handleCommentUpvote = async (commentId: string) => { - if (!user?.accessToken || !user?.login) { - return; - } - - try { - const comment = findCommentById(comments, commentId); - const isCurrentlyReacted = comment - ? hasUserReacted(comment.reactions, user.login, "THUMBS_UP") - : false; - - await toggleReactionMutation.mutateAsync({ - subjectId: commentId, - isReacted: isCurrentlyReacted, - content: "THUMBS_UP" - }); - } catch (error) { - console.error("댓글 업보트 실패:", error); - } - }; - - const handleCommentReply = async (commentId: string, content: string) => { - if (!user?.accessToken) { - return; - } - - try { - await addCommentReplyMutation.mutateAsync({ - discussionId: discussion.id, - replyToId: commentId, - body: content - }); - } catch (error) { - console.error("댓글 답글 실패:", error); - } - }; - - const handleCommentLike = async (commentId: string) => { - if (!user?.accessToken || !user?.login) { - return; - } - - try { - const comment = findCommentById(comments, commentId); - const isCurrentlyReacted = comment - ? hasUserReacted(comment.reactions, user.login, "HEART") - : false; - - await toggleReactionMutation.mutateAsync({ - subjectId: commentId, - isReacted: isCurrentlyReacted, - content: "HEART" - }); - } catch (error) { - console.error("댓글 좋아요 실패:", error); - } - }; - - const { heartCount, upvoteCount } = getHeartAndUpvoteCounts( - actualDiscussion.reactions - ); - const { hasLiked: hasUserLiked, hasUpvoted: hasUserUpvoted } = - getUserReactionStates(actualDiscussion.reactions, user?.login); - return ( -
- {/* 헤더: 사용자 정보 */} -
- -
-

{authorInfo.name}

-
- @{authorInfo.username} - · - - {formatTimeAgo(actualDiscussion.createdAt)} - -
-
-
- - {/* 본문 */} -
- {/* 제목 */} -

{actualDiscussion.title}

- - {/* 내용 */} -
- -
-
- - handleReaction("like")} - onUpvote={() => handleReaction("upvote")} - hasUserLiked={hasUserLiked} - hasUserUpvoted={hasUserUpvoted} - heartCount={heartCount} - upvoteCount={upvoteCount} - variant="detail" - /> - - {/* 구분선 */} - {showComments && ( -
-
-
- )} - - {/* 댓글 입력 */} - {showComments && user && ( -
-
- -