Skip to content

Commit

Permalink
Feat/#558 스와이프, 댓글 기능에 React-Query 적용 (#559)
Browse files Browse the repository at this point in the history
* config: react-query, devtools 설치

* feat: 쿼리프로바이더, 데브 툴 적용

* refactor: 양방향 스와이프 로직 React Query 적용

1. useQuery, useInfiteQuery hook으로 기존 extraFetch hook 대체
2. entries api 응답값의 prev, next 사용하지 않게 되었음.

* refactor: 코멘트 작성 로직 React Query 적용

1. useQuery, useMutation hook으로 기존 로직 대체
2. remote 함수 인자타입 변경 - mutateFn의 함수는 인자를 1개로 제한
3. onSuccess는 useMutation 과 mutate 함수 순서로 실행됨

* test: 댓글 mock 핸들러 수정
  • Loading branch information
Creative-Lee authored Dec 26, 2023
1 parent 7a7d161 commit 3745893
Show file tree
Hide file tree
Showing 12 changed files with 245 additions and 84 deletions.
53 changes: 53 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
},
"license": "MIT",
"dependencies": {
"@tanstack/react-query": "^5.14.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.1",
Expand All @@ -33,6 +34,7 @@
"@storybook/react": "^7.0.27",
"@storybook/react-webpack5": "^7.0.27",
"@storybook/testing-library": "^0.0.14-next.2",
"@tanstack/react-query-devtools": "^5.14.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
Expand Down
34 changes: 21 additions & 13 deletions frontend/src/features/comments/components/CommentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,24 @@ import LoginModal from '@/features/auth/components/LoginModal';
import Avatar from '@/shared/components/Avatar';
import useModal from '@/shared/components/Modal/hooks/useModal';
import useToastContext from '@/shared/components/Toast/hooks/useToastContext';
import { useMutation } from '@/shared/hooks/useMutation';
import { postComment } from '../remotes/comments';
import { usePostCommentMutation } from '../queries';

interface CommentFormProps {
getComments: () => Promise<void>;
songId: number;
partId: number;
}

const CommentForm = ({ getComments, songId, partId }: CommentFormProps) => {
const CommentForm = ({ songId, partId }: CommentFormProps) => {
const [newComment, setNewComment] = useState('');
const { isOpen, closeModal: closeLoginModal, openModal: openLoginModal } = useModal();
const { user } = useAuthContext();

const isLoggedIn = !!user;

const { mutateData: postNewComment } = useMutation(() =>
postComment(songId, partId, newComment.trim())
);
const {
postNewComment,
mutations: { isPending: isPendingPostComment },
} = usePostCommentMutation();

const { showToast } = useToastContext();

Expand All @@ -35,14 +34,18 @@ const CommentForm = ({ getComments, songId, partId }: CommentFormProps) => {
currentTarget: { value },
}) => setNewComment(value);

const submitNewComment: React.FormEventHandler<HTMLFormElement> = async (event) => {
const submitNewComment: React.FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();

await postNewComment();

showToast('댓글이 등록되었습니다.');
resetNewComment();
await getComments();
postNewComment(
{ songId, partId, content: newComment.trim() },
{
onSuccess: () => {
showToast('댓글이 등록되었습니다.');
resetNewComment();
},
}
);
};

return (
Expand All @@ -53,6 +56,7 @@ const CommentForm = ({ getComments, songId, partId }: CommentFormProps) => {
<Avatar src={shookshook} alt="슉슉이" />
<Input
type="text"
disabled={isPendingPostComment}
value={newComment}
onChange={changeNewComment}
placeholder="댓글 추가..."
Expand Down Expand Up @@ -123,6 +127,10 @@ const Input = styled.input`
outline: none;
-webkit-box-shadow: none;
box-shadow: none;
&:disabled {
color: ${({ theme: { color } }) => color.disabledBackground};
}
`;

const FlexEnd = styled.div`
Expand Down
14 changes: 3 additions & 11 deletions frontend/src/features/comments/components/CommentList.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { useEffect } from 'react';
import { styled } from 'styled-components';
import cancelIcon from '@/assets/icon/cancel.svg';
import BottomSheet from '@/shared/components/BottomSheet/BottomSheet';
import useModal from '@/shared/components/Modal/hooks/useModal';
import Spacing from '@/shared/components/Spacing';
import SRHeading from '@/shared/components/SRHeading';
import useFetch from '@/shared/hooks/useFetch';
import { getComments } from '../remotes/comments';
import { useCommentsQuery } from '../queries';
import Comment from './Comment';
import CommentForm from './CommentForm';

Expand All @@ -17,13 +15,7 @@ interface CommentListProps {

const CommentList = ({ songId, partId }: CommentListProps) => {
const { isOpen, openModal, closeModal } = useModal(false);
const { data: comments, fetchData: refetchComments } = useFetch(() =>
getComments(songId, partId)
);

useEffect(() => {
refetchComments();
}, [partId]);
const { comments } = useCommentsQuery(songId, partId);

if (!comments) {
return null;
Expand Down Expand Up @@ -66,7 +58,7 @@ const CommentList = ({ songId, partId }: CommentListProps) => {
))}
</Comments>
<Spacing direction="vertical" size={8} />
<CommentForm getComments={refetchComments} songId={songId} partId={partId} />
<CommentForm songId={songId} partId={partId} />
</BottomSheet>
</>
);
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/features/comments/queries/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { getComments, postComment } from '../remotes/comments';

export const useCommentsQuery = (songId: number, partId: number) => {
const { data: comments, ...queries } = useQuery({
queryKey: ['comments', songId, partId],
queryFn: () => getComments(songId, partId),
});

return { comments, queries };
};

export const usePostCommentMutation = () => {
const client = useQueryClient();

const { mutate: postNewComment, ...mutations } = useMutation({
mutationFn: postComment,
onSuccess: (_, { songId, partId }) => {
client.invalidateQueries({ queryKey: ['comments', songId, partId] });
},
});

return { postNewComment, mutations };
};
10 changes: 9 additions & 1 deletion frontend/src/features/comments/remotes/comments.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { client } from '@/shared/remotes/axios';
import type { Comment } from '../types/comment.type';

export const postComment = async (songId: number, partId: number, content: string) => {
export const postComment = async ({
songId,
partId,
content,
}: {
songId: number;
partId: number;
content: string;
}) => {
await client.post(`/songs/${songId}/parts/${partId}/comments`, { content });
};

Expand Down
50 changes: 21 additions & 29 deletions frontend/src/features/songs/hooks/useExtraSongDetail.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,33 @@
import { useCallback, useRef } from 'react';
import useExtraFetch from '@/shared/hooks/useExtraFetch';
import useValidParams from '@/shared/hooks/useValidParams';
import createObserver from '@/shared/utils/createObserver';
import { getExtraNextSongDetails, getExtraPrevSongDetails } from '../remotes/songs';
import {
useExtraNextSongDetailsInfiniteQuery,
useExtraPrevSongDetailsInfiniteQuery,
} from '../queries';
import type { Genre } from '../types/Song.type';

const useExtraSongDetail = () => {
const { genre: genreParams } = useValidParams();
const { id: songIdParams, genre: genreParams } = useValidParams();

const { data: extraPrevSongDetails, fetchData: fetchExtraPrevSongDetails } = useExtraFetch(
getExtraPrevSongDetails,
'prev'
);
const {
extraPrevSongDetails,
fetchExtraPrevSongDetails,
infiniteQueries: { isLoading: isLoadingPrevSongDetails, hasPreviousPage },
} = useExtraPrevSongDetailsInfiniteQuery(Number(songIdParams), genreParams as Genre);

const { data: extraNextSongDetails, fetchData: fetchExtraNextSongDetails } = useExtraFetch(
getExtraNextSongDetails,
'next'
);
const {
extraNextSongDetails,
fetchExtraNextSongDetails,
infiniteQueries: { isLoading: isLoadingNextSongDetails, hasNextPage },
} = useExtraNextSongDetailsInfiniteQuery(Number(songIdParams), genreParams as Genre);

const prevObserverRef = useRef<IntersectionObserver | null>(null);
const nextObserverRef = useRef<IntersectionObserver | null>(null);

const getExtraPrevSongDetailsOnObserve: React.RefCallback<HTMLDivElement> = useCallback((dom) => {
if (dom !== null) {
prevObserverRef.current = createObserver(() =>
fetchExtraPrevSongDetails(getFirstSongId(dom), genreParams as Genre)
);
prevObserverRef.current = createObserver(() => fetchExtraPrevSongDetails());

prevObserverRef.current.observe(dom);
return;
Expand All @@ -36,9 +38,7 @@ const useExtraSongDetail = () => {

const getExtraNextSongDetailsOnObserve: React.RefCallback<HTMLDivElement> = useCallback((dom) => {
if (dom !== null) {
nextObserverRef.current = createObserver(() =>
fetchExtraNextSongDetails(getLastSongId(dom), genreParams as Genre)
);
nextObserverRef.current = createObserver(() => fetchExtraNextSongDetails());

nextObserverRef.current.observe(dom);
return;
Expand All @@ -47,21 +47,13 @@ const useExtraSongDetail = () => {
nextObserverRef.current?.disconnect();
}, []);

const getFirstSongId = (dom: HTMLDivElement) => {
const firstSongId = dom.nextElementSibling?.getAttribute('data-song-id') as string;

return Number(firstSongId);
};

const getLastSongId = (dom: HTMLDivElement) => {
const lastSongId = dom.previousElementSibling?.getAttribute('data-song-id') as string;

return Number(lastSongId);
};

return {
extraPrevSongDetails,
extraNextSongDetails,
isLoadingPrevSongDetails,
isLoadingNextSongDetails,
hasPreviousPage,
hasNextPage,
getExtraPrevSongDetailsOnObserve,
getExtraNextSongDetailsOnObserve,
};
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/features/songs/hooks/useSongDetailEntries.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { useCallback } from 'react';
import useFetch from '@/shared/hooks/useFetch';
import useValidParams from '@/shared/hooks/useValidParams';
import { getSongDetailEntries } from '../remotes/songs';
import { useSongDetailEntriesQuery } from '../queries';
import type { Genre } from '../types/Song.type';

const useSongDetailEntries = () => {
const { id: songIdParams, genre: genreParams } = useValidParams();

const { data: songDetailEntries } = useFetch(() =>
getSongDetailEntries(Number(songIdParams), genreParams as Genre)
);
const {
songDetailEntries,
queries: { isLoading: isLoadingSongDetailEntries },
} = useSongDetailEntriesQuery(Number(songIdParams), genreParams as Genre);

const scrollIntoCurrentSong: React.RefCallback<HTMLDivElement> = useCallback((dom) => {
if (dom !== null) dom.scrollIntoView({ behavior: 'instant', block: 'start' });
}, []);

return { songDetailEntries, scrollIntoCurrentSong };
return { songDetailEntries, isLoadingSongDetailEntries, scrollIntoCurrentSong };
};

export default useSongDetailEntries;
Loading

0 comments on commit 3745893

Please sign in to comment.