From 681601a46e21db1fd220db85fe8fc53b1542a814 Mon Sep 17 00:00:00 2001 From: seongminn Date: Mon, 27 Nov 2023 16:06:31 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=B0=8F=20=EC=B5=9C=EC=B4=88=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/match.ts | 17 +++++++++++ src/app/match/[id]/page.tsx | 18 ++++++++++++ src/components/match/CommentForm/index.tsx | 30 ++++++++++++++++++++ src/queries/useMatchCommentById/Fetcher.tsx | 31 +++++++++++++++++++++ src/queries/useMatchCommentById/query.ts | 21 ++++++++++++++ src/queries/useSaveCommentMutation/query.ts | 10 +++++++ src/types/match.ts | 8 +++++- 7 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 src/components/match/CommentForm/index.tsx create mode 100644 src/queries/useMatchCommentById/Fetcher.tsx create mode 100644 src/queries/useMatchCommentById/query.ts create mode 100644 src/queries/useSaveCommentMutation/query.ts diff --git a/src/api/match.ts b/src/api/match.ts index 30c8f5a..a8342a8 100644 --- a/src/api/match.ts +++ b/src/api/match.ts @@ -1,5 +1,6 @@ import { MatchCheerType, + MatchCommentPayload, MatchCommentType, MatchLineupType, MatchListType, @@ -75,3 +76,19 @@ export const getMatchVideoById = async (matchId: string) => { return data; }; + +export const getMatchCommentById = async ( + matchId: string, + cursor: number | null, + size = 10, +) => { + const { data } = await instance.get( + `/games/${matchId}/comments?cursor=${cursor}&size=${size}`, + ); + + return data; +}; + +export const postMatchComment = async (payload: MatchCommentPayload) => { + await instance.post(`/comments`, payload); +}; diff --git a/src/app/match/[id]/page.tsx b/src/app/match/[id]/page.tsx index 06e1dbd..8dad396 100644 --- a/src/app/match/[id]/page.tsx +++ b/src/app/match/[id]/page.tsx @@ -4,17 +4,21 @@ import { Suspense } from 'react'; import MatchBanner from '@/components/match/Banner'; import Cheer from '@/components/match/Cheer'; +import CommentForm from '@/components/match/CommentForm'; import Lineup from '@/components/match/LineupList'; import Panel from '@/components/match/Panel'; import RecordList from '@/components/match/RecordList'; import Video from '@/components/match/Video'; import MatchByIdFetcher from '@/queries/useMatchById/Fetcher'; import MatchCheerByIdFetcher from '@/queries/useMatchCheerById/Fetcher'; +import MatchCommentFetcher from '@/queries/useMatchCommentById/Fetcher'; import MatchLineupFetcher from '@/queries/useMatchLineupById/Fetcher'; import MatchTimelineFetcher from '@/queries/useMatchTimelineById/Fetcher'; import MatchVideoFetcher from '@/queries/useMatchVideoById/Fetcher'; +import useSaveCommentMutation from '@/queries/useSaveCommentMutation/query'; export default function Match({ params }: { params: { id: string } }) { + const { mutate } = useSaveCommentMutation(); const options = [ { label: '라인업' }, { label: '응원댓글' }, @@ -58,6 +62,20 @@ export default function Match({ params }: { params: { id: string } }) { )} )} + {selected === '응원댓글' && ( + + {data => ( +
    + {data.commentList.pages.map(page => + page.map(comment => ( +
  • {comment.content}
  • + )), + )} + +
+ )} +
+ )} {selected === '경기영상' && ( {data => ( diff --git a/src/components/match/CommentForm/index.tsx b/src/components/match/CommentForm/index.tsx new file mode 100644 index 0000000..8250b99 --- /dev/null +++ b/src/components/match/CommentForm/index.tsx @@ -0,0 +1,30 @@ +import { UseMutateFunction } from '@tanstack/react-query'; +import { FormEvent, useState } from 'react'; + +import { MatchCommentPayload } from '@/types/match'; + +type CommentFormProps = { + mutate: UseMutateFunction; +}; + +export default function CommentForm({ mutate }: CommentFormProps) { + const [inputValue, setInputValue] = useState(''); + const handleCommentSubmit = ( + e: FormEvent, + payload: MatchCommentPayload, + ) => { + e.preventDefault(); + mutate(payload); + }; + + return ( +
+ handleCommentSubmit(e, { gameTeamId: 1, content: inputValue }) + } + > + setInputValue(e.target.value)} /> + +
+ ); +} diff --git a/src/queries/useMatchCommentById/Fetcher.tsx b/src/queries/useMatchCommentById/Fetcher.tsx new file mode 100644 index 0000000..1edaba7 --- /dev/null +++ b/src/queries/useMatchCommentById/Fetcher.tsx @@ -0,0 +1,31 @@ +import { InfiniteData } from '@tanstack/react-query'; +import { ReactNode } from 'react'; + +import { MatchCommentType } from '@/types/match'; + +import useMatchCommentById from './query'; + +type MatchCommentFetcher = { + matchId: string; + children: ({ + commentList, + fetchNextPage, + hasNextPage, + }: { + commentList: InfiniteData; + fetchNextPage: () => void; + hasNextPage: boolean; + }) => ReactNode; +}; + +export default function MatchCommentFetcher({ + matchId, + children, +}: MatchCommentFetcher) { + const { commentList, error, fetchNextPage, hasNextPage } = + useMatchCommentById(matchId); + + if (error) throw error; + + return children({ commentList, fetchNextPage, hasNextPage }); +} diff --git a/src/queries/useMatchCommentById/query.ts b/src/queries/useMatchCommentById/query.ts new file mode 100644 index 0000000..a8d9283 --- /dev/null +++ b/src/queries/useMatchCommentById/query.ts @@ -0,0 +1,21 @@ +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; + +import { getMatchCommentById } from '@/api/match'; +export default function useMatchCommentById(matchId: string) { + const { data, error, fetchNextPage, hasNextPage } = useSuspenseInfiniteQuery({ + queryKey: ['match-comment', matchId], + initialPageParam: 0, + queryFn: ({ pageParam }) => getMatchCommentById(matchId, pageParam), + getPreviousPageParam: firstPage => { + return firstPage[0].commentId || null; + }, + getNextPageParam: lastPage => lastPage[0]?.commentId || null, + }); + + return { + commentList: data, + fetchNextPage, + hasNextPage, + error, + }; +} diff --git a/src/queries/useSaveCommentMutation/query.ts b/src/queries/useSaveCommentMutation/query.ts new file mode 100644 index 0000000..0c16d76 --- /dev/null +++ b/src/queries/useSaveCommentMutation/query.ts @@ -0,0 +1,10 @@ +import { useMutation } from '@tanstack/react-query'; + +import { postMatchComment } from '@/api/match'; + +export default function useSaveCommentMutation() { + return useMutation({ + mutationKey: ['save-comment'], + mutationFn: postMatchComment, + }); +} diff --git a/src/types/match.ts b/src/types/match.ts index f9e0aeb..4acda13 100644 --- a/src/types/match.ts +++ b/src/types/match.ts @@ -47,12 +47,18 @@ export type MatchPlayerType = { }; export type MatchCommentType = { - id: number; + commentId: number; content: string; + gameTeamId: number; createdAt: string; isBlocked: boolean; }; +export type MatchCommentPayload = Pick< + MatchCommentType, + 'gameTeamId' | 'content' +>; + export type MatchVideoType = { videoId: string; }; From fec0105b8f0d8303121394c831b229cc4e7075d1 Mon Sep 17 00:00:00 2001 From: seongminn Date: Tue, 28 Nov 2023 01:05:21 +0900 Subject: [PATCH 02/10] =?UTF-8?q?build:=20socket=20=ED=99=9C=EC=9A=A9?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20stompjs=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/hooks/useSocket.ts | 44 ++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 5 +++++ 3 files changed, 50 insertions(+) create mode 100644 src/hooks/useSocket.ts diff --git a/package.json b/package.json index 3c18d55..dbe5580 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-icons": "^1.3.0", "@sentry/nextjs": "^7.73.0", + "@stomp/stompjs": "^7.0.0", "@tanstack/react-query": "^5.8.2", "@tanstack/react-query-devtools": "^5.8.2", "axios": "^1.5.1", diff --git a/src/hooks/useSocket.ts b/src/hooks/useSocket.ts new file mode 100644 index 0000000..c3b9639 --- /dev/null +++ b/src/hooks/useSocket.ts @@ -0,0 +1,44 @@ +import { Client } from '@stomp/stompjs'; +import { useRef } from 'react'; + +type useSocketParams = { + url: string; + destination: string; + callback: (message: T) => void; +}; + +export default function useSocket({ + url, + destination, + callback, +}: useSocketParams) { + const stompRef = useRef(null); + const client = new Client({ + brokerURL: url, + }); + + const connect = () => { + if (stompRef.current) return; + + stompRef.current = client; + + client.activate(); + client.onConnect = () => { + client.subscribe(destination, message => { + try { + callback(JSON.parse(message.body)); + } catch (error) { + console.error(error); + } + }); + }; + }; + + const disconnect = () => { + if (!stompRef.current) return; + + client.deactivate(); + }; + + return { connect, disconnect, stompRef }; +} diff --git a/yarn.lock b/yarn.lock index f1197b8..023201f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -703,6 +703,11 @@ "@sentry/cli" "^1.74.6" webpack-sources "^2.0.0 || ^3.0.0" +"@stomp/stompjs@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@stomp/stompjs/-/stompjs-7.0.0.tgz#46b5c454a9dc8262e0b20f3b3dbacaa113993077" + integrity sha512-fGdq4wPDnSV/KyOsjq4P+zLc8MFWC3lMmP5FBgLWKPJTYcuCbAIrnRGjB7q2jHZdYCOD5vxLuFoKIYLy5/u8Pw== + "@swc/helpers@0.5.2": version "0.5.2" resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d" From 74b962487d8455f3c979580a860337fcdf5113ae Mon Sep 17 00:00:00 2001 From: seongminn Date: Tue, 28 Nov 2023 01:07:54 +0900 Subject: [PATCH 03/10] =?UTF-8?q?chore:=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EB=A5=BC=20=EB=AC=B4=ED=95=9C=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=EB=A1=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/match.ts | 4 ++-- src/queries/useMatchCommentById/Fetcher.tsx | 6 ++++-- src/queries/useMatchCommentById/query.ts | 21 ++++++++++++--------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/api/match.ts b/src/api/match.ts index a8342a8..265e101 100644 --- a/src/api/match.ts +++ b/src/api/match.ts @@ -79,8 +79,8 @@ export const getMatchVideoById = async (matchId: string) => { export const getMatchCommentById = async ( matchId: string, - cursor: number | null, - size = 10, + cursor: number | string, + size = 20, ) => { const { data } = await instance.get( `/games/${matchId}/comments?cursor=${cursor}&size=${size}`, diff --git a/src/queries/useMatchCommentById/Fetcher.tsx b/src/queries/useMatchCommentById/Fetcher.tsx index 1edaba7..a1cb1ad 100644 --- a/src/queries/useMatchCommentById/Fetcher.tsx +++ b/src/queries/useMatchCommentById/Fetcher.tsx @@ -11,10 +11,12 @@ type MatchCommentFetcher = { commentList, fetchNextPage, hasNextPage, + isFetching, }: { commentList: InfiniteData; fetchNextPage: () => void; hasNextPage: boolean; + isFetching: boolean; }) => ReactNode; }; @@ -22,10 +24,10 @@ export default function MatchCommentFetcher({ matchId, children, }: MatchCommentFetcher) { - const { commentList, error, fetchNextPage, hasNextPage } = + const { commentList, error, fetchNextPage, hasNextPage, isFetching } = useMatchCommentById(matchId); if (error) throw error; - return children({ commentList, fetchNextPage, hasNextPage }); + return children({ commentList, fetchNextPage, hasNextPage, isFetching }); } diff --git a/src/queries/useMatchCommentById/query.ts b/src/queries/useMatchCommentById/query.ts index a8d9283..ae4d537 100644 --- a/src/queries/useMatchCommentById/query.ts +++ b/src/queries/useMatchCommentById/query.ts @@ -2,20 +2,23 @@ import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; import { getMatchCommentById } from '@/api/match'; export default function useMatchCommentById(matchId: string) { - const { data, error, fetchNextPage, hasNextPage } = useSuspenseInfiniteQuery({ - queryKey: ['match-comment', matchId], - initialPageParam: 0, - queryFn: ({ pageParam }) => getMatchCommentById(matchId, pageParam), - getPreviousPageParam: firstPage => { - return firstPage[0].commentId || null; - }, - getNextPageParam: lastPage => lastPage[0]?.commentId || null, - }); + const { data, error, fetchNextPage, hasNextPage, isFetching } = + useSuspenseInfiniteQuery({ + queryKey: ['match-comment', matchId], + initialPageParam: 0, + queryFn: ({ pageParam }) => getMatchCommentById(matchId, pageParam || ''), + getNextPageParam: lastPage => lastPage[0]?.commentId || null, + select: data => ({ + pages: [...data.pages].reverse(), + pageParams: [...data.pageParams].reverse(), + }), + }); return { commentList: data, fetchNextPage, hasNextPage, + isFetching, error, }; } From 3e1a4731b546c326b2566203f05b07080c7a2d14 Mon Sep 17 00:00:00 2001 From: seongminn Date: Tue, 28 Nov 2023 01:15:21 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EC=8A=A4=ED=83=80=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/match/[id]/page.tsx | 54 ++++++++++++++++++---- src/components/match/CommentForm/index.tsx | 32 +++++++++++-- src/components/match/CommentItem/index.tsx | 54 ++++++++++++++++++++++ src/components/match/CommentList/index.tsx | 34 ++++++++++++++ 4 files changed, 160 insertions(+), 14 deletions(-) create mode 100644 src/components/match/CommentItem/index.tsx create mode 100644 src/components/match/CommentList/index.tsx diff --git a/src/app/match/[id]/page.tsx b/src/app/match/[id]/page.tsx index 8dad396..1b754a4 100644 --- a/src/app/match/[id]/page.tsx +++ b/src/app/match/[id]/page.tsx @@ -1,14 +1,16 @@ 'use client'; -import { Suspense } from 'react'; +import { Suspense, useRef, useState } from 'react'; import MatchBanner from '@/components/match/Banner'; import Cheer from '@/components/match/Cheer'; import CommentForm from '@/components/match/CommentForm'; +import CommentList from '@/components/match/CommentList'; import Lineup from '@/components/match/LineupList'; import Panel from '@/components/match/Panel'; import RecordList from '@/components/match/RecordList'; import Video from '@/components/match/Video'; +import useSocket from '@/hooks/useSocket'; import MatchByIdFetcher from '@/queries/useMatchById/Fetcher'; import MatchCheerByIdFetcher from '@/queries/useMatchCheerById/Fetcher'; import MatchCommentFetcher from '@/queries/useMatchCommentById/Fetcher'; @@ -16,8 +18,25 @@ import MatchLineupFetcher from '@/queries/useMatchLineupById/Fetcher'; import MatchTimelineFetcher from '@/queries/useMatchTimelineById/Fetcher'; import MatchVideoFetcher from '@/queries/useMatchVideoById/Fetcher'; import useSaveCommentMutation from '@/queries/useSaveCommentMutation/query'; +import { MatchCommentType } from '@/types/match'; export default function Match({ params }: { params: { id: string } }) { + const [comments, setComments] = useState([]); + + const handleSocketMessage = (comment: MatchCommentType) => { + if (comment) { + setComments(prev => [...prev, comment]); + } + }; + + const { connect } = useSocket({ + url: 'wss://api.hufstreaming.site/ws', + destination: `/topic/games/${params.id}`, + callback: handleSocketMessage, + }); + + connect(); + const { mutate } = useSaveCommentMutation(); const options = [ { label: '라인업' }, @@ -26,6 +45,13 @@ export default function Match({ params }: { params: { id: string } }) { { label: '타임라인' }, ]; + const scrollRef = useRef(null); + const scrollToBottom = () => { + if (!scrollRef.current) return; + + (scrollRef.current as HTMLDivElement).scrollIntoView(); + }; + return (
배너 로딩중...}> @@ -64,15 +90,23 @@ export default function Match({ params }: { params: { id: string } }) { )} {selected === '응원댓글' && ( - {data => ( -
    - {data.commentList.pages.map(page => - page.map(comment => ( -
  • {comment.content}
  • - )), - )} - -
+ {({ commentList, ...data }) => ( +
+
    + + +
    +
+ +
)}
)} diff --git a/src/components/match/CommentForm/index.tsx b/src/components/match/CommentForm/index.tsx index 8250b99..0f075fb 100644 --- a/src/components/match/CommentForm/index.tsx +++ b/src/components/match/CommentForm/index.tsx @@ -4,10 +4,16 @@ import { FormEvent, useState } from 'react'; import { MatchCommentPayload } from '@/types/match'; type CommentFormProps = { + matchId: string; mutate: UseMutateFunction; + scrollToBottom: () => void; }; -export default function CommentForm({ mutate }: CommentFormProps) { +export default function CommentForm({ + matchId, + mutate, + scrollToBottom, +}: CommentFormProps) { const [inputValue, setInputValue] = useState(''); const handleCommentSubmit = ( e: FormEvent, @@ -15,16 +21,34 @@ export default function CommentForm({ mutate }: CommentFormProps) { ) => { e.preventDefault(); mutate(payload); + setInputValue(''); + scrollToBottom(); }; return (
- handleCommentSubmit(e, { gameTeamId: 1, content: inputValue }) + handleCommentSubmit(e, { + gameTeamId: Number(matchId), + content: inputValue, + }) } > - setInputValue(e.target.value)} /> - +
+ setInputValue(e.target.value)} + placeholder="응원하는 팀에 댓글을 남겨보세요!" + /> + +
); } diff --git a/src/components/match/CommentItem/index.tsx b/src/components/match/CommentItem/index.tsx new file mode 100644 index 0000000..9675873 --- /dev/null +++ b/src/components/match/CommentItem/index.tsx @@ -0,0 +1,54 @@ +import { $ } from '@/utils/core'; + +type CommentItemProps = { + content: string; + order: number; + isBlocked: boolean; + createdAt: string; +}; + +export default function CommentItem({ + content, + order, + isBlocked, + createdAt, +}: CommentItemProps) { + const isEven = order % 2 === 0; + + return ( +
  • + {isBlocked ? ( +
    + ⚠️ 관리자에 의해 차단된 댓글입니다. +
    + ) : ( +
    + {content} +
    + )} +
    + +
    신고 🚨
    +
    +
  • + ); +} diff --git a/src/components/match/CommentList/index.tsx b/src/components/match/CommentList/index.tsx new file mode 100644 index 0000000..7157183 --- /dev/null +++ b/src/components/match/CommentList/index.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { useEffect } from 'react'; + +import { MatchCommentType } from '@/types/match'; + +import CommentItem from '../CommentItem'; + +type CommentListProps = { + commentList: MatchCommentType[]; + hasNextPage?: boolean; + fetchNextPage?: () => void; + isFetching?: boolean; + scrollToBottom?: () => void; +}; + +export default function CommentList({ + commentList, + scrollToBottom, +}: CommentListProps) { + useEffect(() => { + if (!scrollToBottom) return; + + scrollToBottom(); + }, [scrollToBottom]); + + return ( + <> + {commentList.map(({ commentId, ...comment }) => ( + + ))} + + ); +} From de244f89f53f1f72d7e383159565046f1cc3d35d Mon Sep 17 00:00:00 2001 From: seongminn Date: Tue, 28 Nov 2023 01:25:11 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=8B=A0?= =?UTF-8?q?=EA=B3=A0=ED=95=98=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/match.ts | 4 ++++ src/components/match/CommentItem/index.tsx | 15 ++++++++++++++- src/components/match/CommentList/index.tsx | 4 ++-- src/queries/useReportCommentMutation/query.ts | 10 ++++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 src/queries/useReportCommentMutation/query.ts diff --git a/src/api/match.ts b/src/api/match.ts index 265e101..e341ffd 100644 --- a/src/api/match.ts +++ b/src/api/match.ts @@ -92,3 +92,7 @@ export const getMatchCommentById = async ( export const postMatchComment = async (payload: MatchCommentPayload) => { await instance.post(`/comments`, payload); }; + +export const postReportComment = async (payload: { commentId: number }) => { + await instance.post(`/reports`, payload); +}; diff --git a/src/components/match/CommentItem/index.tsx b/src/components/match/CommentItem/index.tsx index 9675873..3e6faad 100644 --- a/src/components/match/CommentItem/index.tsx +++ b/src/components/match/CommentItem/index.tsx @@ -1,6 +1,8 @@ +import useReportCommentMutation from '@/queries/useReportCommentMutation/query'; import { $ } from '@/utils/core'; type CommentItemProps = { + commentId: number; content: string; order: number; isBlocked: boolean; @@ -8,11 +10,17 @@ type CommentItemProps = { }; export default function CommentItem({ + commentId, content, order, isBlocked, createdAt, }: CommentItemProps) { + const { mutate } = useReportCommentMutation(); + const handleClickReportButton = (payload: { commentId: number }) => { + mutate(payload); + }; + const isEven = order % 2 === 0; return ( @@ -47,7 +55,12 @@ export default function CommentItem({ > {createdAt} -
    신고 🚨
    + ); diff --git a/src/components/match/CommentList/index.tsx b/src/components/match/CommentList/index.tsx index 7157183..d03934b 100644 --- a/src/components/match/CommentList/index.tsx +++ b/src/components/match/CommentList/index.tsx @@ -26,8 +26,8 @@ export default function CommentList({ return ( <> - {commentList.map(({ commentId, ...comment }) => ( - + {commentList.map(comment => ( + ))} ); diff --git a/src/queries/useReportCommentMutation/query.ts b/src/queries/useReportCommentMutation/query.ts new file mode 100644 index 0000000..a2238d4 --- /dev/null +++ b/src/queries/useReportCommentMutation/query.ts @@ -0,0 +1,10 @@ +import { useMutation } from '@tanstack/react-query'; + +import { postReportComment } from '@/api/match'; + +export default function useReportCommentMutation() { + return useMutation({ + mutationKey: ['report-comment'], + mutationFn: postReportComment, + }); +} From 67e7b61fee8b49abfe787d30ffb630c0e168a77f Mon Sep 17 00:00:00 2001 From: seongminn Date: Tue, 28 Nov 2023 01:31:06 +0900 Subject: [PATCH 06/10] =?UTF-8?q?chore:=20=EC=8B=A0=EA=B3=A0=20=EC=9D=B4?= =?UTF-8?q?=EB=AA=A8=EC=A7=80=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/match/CommentItem/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/match/CommentItem/index.tsx b/src/components/match/CommentItem/index.tsx index 3e6faad..265db3d 100644 --- a/src/components/match/CommentItem/index.tsx +++ b/src/components/match/CommentItem/index.tsx @@ -59,7 +59,7 @@ export default function CommentItem({ onClick={() => handleClickReportButton({ commentId })} className="mx-2 text-red-400" > - 신고 🚨 + 신고 From 9526e861a01c65681ed7f3a1b1c308b814665a02 Mon Sep 17 00:00:00 2001 From: seongminn Date: Tue, 28 Nov 2023 02:06:17 +0900 Subject: [PATCH 07/10] =?UTF-8?q?chore:=20=EC=8B=9C=EA=B0=84=EC=9D=84=20?= =?UTF-8?q?=EA=B0=80=EB=8F=85=EC=84=B1=20=EC=9E=88=EA=B2=8C=20=ED=91=9C?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/MatchCard/pieces/Label.tsx | 8 ++++- src/components/match/CommentItem/index.tsx | 4 ++- src/utils/time.ts | 16 ++++++++++ src/utils/utc-times.ts | 30 ------------------- 4 files changed, 26 insertions(+), 32 deletions(-) create mode 100644 src/utils/time.ts delete mode 100644 src/utils/utc-times.ts diff --git a/src/components/common/MatchCard/pieces/Label.tsx b/src/components/common/MatchCard/pieces/Label.tsx index a4dfeb9..7e8e16b 100644 --- a/src/components/common/MatchCard/pieces/Label.tsx +++ b/src/components/common/MatchCard/pieces/Label.tsx @@ -1,5 +1,6 @@ import { useMatchCardContext } from '@/hooks/useMatchCardContext'; import { $ } from '@/utils/core'; +import { parseTimeString } from '@/utils/time'; type LabelProps = { className?: string; @@ -7,10 +8,15 @@ type LabelProps = { export default function Label({ className }: LabelProps) { const { gameName, sportsName, startTime } = useMatchCardContext(); + const { year, month, date, weekday } = parseTimeString(startTime); return (
    - {startTime &&
    {startTime}
    } + {startTime && ( + + )} {sportsName &&
    {sportsName}
    } {gameName &&
    {gameName}
    }
    diff --git a/src/components/match/CommentItem/index.tsx b/src/components/match/CommentItem/index.tsx index 265db3d..f1732b3 100644 --- a/src/components/match/CommentItem/index.tsx +++ b/src/components/match/CommentItem/index.tsx @@ -1,5 +1,6 @@ import useReportCommentMutation from '@/queries/useReportCommentMutation/query'; import { $ } from '@/utils/core'; +import { parseTimeString } from '@/utils/time'; type CommentItemProps = { commentId: number; @@ -22,6 +23,7 @@ export default function CommentItem({ }; const isEven = order % 2 === 0; + const { period, hours, minutes } = parseTimeString(createdAt); return (
  • @@ -53,7 +55,7 @@ export default function CommentItem({ From a8c03519fe44930689bd104c46b30a753079d4c6 Mon Sep 17 00:00:00 2001 From: seongminn Date: Tue, 28 Nov 2023 04:15:55 +0900 Subject: [PATCH 09/10] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EB=AC=B4?= =?UTF-8?q?=ED=95=9C=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/match/[id]/page.tsx | 4 +-- src/components/match/CommentList/index.tsx | 34 +++++++++++++++++++--- src/hooks/useInfiniteObserver.ts | 33 +++++++++++++++++++++ 3 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 src/hooks/useInfiniteObserver.ts diff --git a/src/app/match/[id]/page.tsx b/src/app/match/[id]/page.tsx index 1b754a4..27375d0 100644 --- a/src/app/match/[id]/page.tsx +++ b/src/app/match/[id]/page.tsx @@ -98,8 +98,8 @@ export default function Match({ params }: { params: { id: string } }) { scrollToBottom={scrollToBottom} {...data} /> - -
    + +
  • void; - isFetching?: boolean; - scrollToBottom?: () => void; + hasNextPage: boolean; + fetchNextPage: () => void; + isFetching: boolean; + scrollToBottom: () => void; }; export default function CommentList({ commentList, + fetchNextPage, + hasNextPage, + isFetching, scrollToBottom, }: CommentListProps) { + const { ref } = useInfiniteObserver( + async (entry, observer) => { + observer.unobserve(entry.target); + if (hasNextPage && !isFetching) { + fetchNextPage(); + } + }, + ); + useEffect(() => { if (!scrollToBottom) return; @@ -26,9 +39,22 @@ export default function CommentList({ return ( <> +
    {commentList.map(comment => ( ))} ); } + +CommentList.SocketList = function SocketList({ + commentList, +}: Pick) { + return ( + <> + {commentList.map(comment => ( + + ))} + + ); +}; diff --git a/src/hooks/useInfiniteObserver.ts b/src/hooks/useInfiniteObserver.ts new file mode 100644 index 0000000..3283634 --- /dev/null +++ b/src/hooks/useInfiniteObserver.ts @@ -0,0 +1,33 @@ +import { useCallback, useEffect, useRef } from 'react'; + +type IntersectHandler = ( + entry: IntersectionObserverEntry, + observer: IntersectionObserver, +) => void; + +export default function useIntersect( + onIntersect: IntersectHandler, + options?: IntersectionObserverInit, +) { + const ref = useRef(null); + const callback = useCallback( + (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => { + entries.forEach(entry => { + if (entry.isIntersecting) onIntersect(entry, observer); + }); + }, + [onIntersect], + ); + + useEffect(() => { + if (!ref.current) return; + + const observer = new IntersectionObserver(callback, options); + + observer.observe(ref.current); + + return () => observer.disconnect(); + }, [ref, options, callback]); + + return { ref }; +} From 54f634c4e4917ac23c287cca48bc6fda88fb5931 Mon Sep 17 00:00:00 2001 From: seongminn Date: Tue, 28 Nov 2023 21:57:16 +0900 Subject: [PATCH 10/10] =?UTF-8?q?chore:=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/queries/useMatchCommentById/Fetcher.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/queries/useMatchCommentById/Fetcher.tsx b/src/queries/useMatchCommentById/Fetcher.tsx index a1cb1ad..1ac5e28 100644 --- a/src/queries/useMatchCommentById/Fetcher.tsx +++ b/src/queries/useMatchCommentById/Fetcher.tsx @@ -5,7 +5,7 @@ import { MatchCommentType } from '@/types/match'; import useMatchCommentById from './query'; -type MatchCommentFetcher = { +type MatchCommentFetcherProps = { matchId: string; children: ({ commentList, @@ -23,7 +23,7 @@ type MatchCommentFetcher = { export default function MatchCommentFetcher({ matchId, children, -}: MatchCommentFetcher) { +}: MatchCommentFetcherProps) { const { commentList, error, fetchNextPage, hasNextPage, isFetching } = useMatchCommentById(matchId);