-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: CommentItem 컴포넌트 추가 * feat: 댓글 목록 가져오는 쿼리 추가 * feat: 상품 상세 페이지에 댓글 컴포넌트 추가 * feat: Input 컴포넌트 속성에 minWidth값 추가 * feat: CommentInput 컴포넌트 추가 * feat: 댓글 등록 기능 구현 * feat: 사용자가 입력한 글자수 UI 추가 * feat: 리뷰 반영 * feat: text area 텍스트 크기 수정 * feat: CommentList 컴포넌트 추가 * feat: 디자인 수정 * feat: api 변경 적용 * refactor: CommentInput -> CommentForm으로 네이밍 수정 * feat: data fetching 로직을 CommentList내부로 이동 * feat: 댓글 무한 스크롤로 변경 * fix: 토스트 컴포넌트가 가운데 정렬되지 않는 문제 해결 * feat: 전송 아이콘 추가 * feat: 댓글 컴포넌트를 fixed로 변경 * feat: 댓글 컴포넌트 사이 공백 추가 * feat: Response 객체에 totalElements 값 추가 * feat: pageParam의 기본값 추가 * feat: index.ts에서 export문 추가
- Loading branch information
Showing
22 changed files
with
399 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
frontend/src/components/Recipe/CommentForm/CommentForm.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
|
||
import CommentForm from './CommentForm'; | ||
|
||
const meta: Meta<typeof CommentForm> = { | ||
title: 'recipe/CommentForm', | ||
component: CommentForm, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof meta>; | ||
|
||
export const Default: Story = {}; |
101 changes: 101 additions & 0 deletions
101
frontend/src/components/Recipe/CommentForm/CommentForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { Button, Spacing, Text, Textarea, useTheme } from '@fun-eat/design-system'; | ||
import type { ChangeEventHandler, FormEventHandler } from 'react'; | ||
import { useState } from 'react'; | ||
import styled from 'styled-components'; | ||
|
||
import { SvgIcon } from '@/components/Common'; | ||
import { useToastActionContext } from '@/hooks/context'; | ||
import { useRecipeCommentMutation } from '@/hooks/queries/recipe'; | ||
|
||
interface CommentFormProps { | ||
recipeId: number; | ||
} | ||
|
||
const MAX_COMMENT_LENGTH = 200; | ||
|
||
const CommentForm = ({ recipeId }: CommentFormProps) => { | ||
const [commentValue, setCommentValue] = useState(''); | ||
const { mutate } = useRecipeCommentMutation(recipeId); | ||
|
||
const theme = useTheme(); | ||
const { toast } = useToastActionContext(); | ||
|
||
const handleCommentInput: ChangeEventHandler<HTMLTextAreaElement> = (e) => { | ||
setCommentValue(e.target.value); | ||
}; | ||
|
||
const handleSubmitComment: FormEventHandler<HTMLFormElement> = (e) => { | ||
e.preventDefault(); | ||
|
||
mutate( | ||
{ comment: commentValue }, | ||
{ | ||
onSuccess: () => { | ||
setCommentValue(''); | ||
toast.success('댓글이 등록되었습니다.'); | ||
}, | ||
onError: (error) => { | ||
if (error instanceof Error) { | ||
toast.error(error.message); | ||
return; | ||
} | ||
|
||
toast.error('댓글을 등록하는데 오류가 발생했습니다.'); | ||
}, | ||
} | ||
); | ||
}; | ||
|
||
return ( | ||
<CommentFormContainer> | ||
<Form onSubmit={handleSubmitComment}> | ||
<CommentTextarea | ||
placeholder="댓글을 입력하세요. (200자)" | ||
value={commentValue} | ||
onChange={handleCommentInput} | ||
maxLength={MAX_COMMENT_LENGTH} | ||
/> | ||
<SubmitButton variant="transparent" disabled={commentValue.length === 0}> | ||
<SvgIcon | ||
variant="plane" | ||
width={30} | ||
height={30} | ||
color={commentValue.length === 0 ? theme.colors.gray2 : theme.colors.gray4} | ||
/> | ||
</SubmitButton> | ||
</Form> | ||
<Spacing size={8} /> | ||
<Text size="xs" color={theme.textColors.info} align="right"> | ||
{commentValue.length}자 / {MAX_COMMENT_LENGTH}자 | ||
</Text> | ||
</CommentFormContainer> | ||
); | ||
}; | ||
|
||
export default CommentForm; | ||
|
||
const CommentFormContainer = styled.div` | ||
position: fixed; | ||
bottom: 0; | ||
width: calc(100% - 40px); | ||
max-width: 540px; | ||
padding: 16px 0; | ||
background: ${({ theme }) => theme.backgroundColors.default}; | ||
`; | ||
|
||
const Form = styled.form` | ||
display: flex; | ||
gap: 4px; | ||
justify-content: space-around; | ||
align-items: center; | ||
`; | ||
|
||
const CommentTextarea = styled(Textarea)` | ||
height: 50px; | ||
padding: 8px; | ||
font-size: 1.4rem; | ||
`; | ||
|
||
const SubmitButton = styled(Button)` | ||
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; | ||
`; |
18 changes: 18 additions & 0 deletions
18
frontend/src/components/Recipe/CommentItem/CommentItem.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
|
||
import CommentItem from './CommentItem'; | ||
|
||
import comments from '@/mocks/data/comments.json'; | ||
|
||
const meta: Meta<typeof CommentItem> = { | ||
title: 'recipe/CommentItem', | ||
component: CommentItem, | ||
args: { | ||
recipeComment: comments.comments[0], | ||
}, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof meta>; | ||
|
||
export const Default: Story = {}; |
50 changes: 50 additions & 0 deletions
50
frontend/src/components/Recipe/CommentItem/CommentItem.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { Divider, Spacing, Text, useTheme } from '@fun-eat/design-system'; | ||
import styled from 'styled-components'; | ||
|
||
import type { Comment } from '@/types/recipe'; | ||
import { getFormattedDate } from '@/utils/date'; | ||
|
||
interface CommentItemProps { | ||
recipeComment: Comment; | ||
} | ||
|
||
const CommentItem = ({ recipeComment }: CommentItemProps) => { | ||
const theme = useTheme(); | ||
const { author, comment, createdAt } = recipeComment; | ||
|
||
return ( | ||
<> | ||
<AuthorWrapper> | ||
<AuthorProfileImage src={author.profileImage} alt={`${author.nickname}님의 프로필`} width={32} height={32} /> | ||
<div> | ||
<Text size="xs" color={theme.textColors.info}> | ||
{author.nickname} 님 | ||
</Text> | ||
<Text size="xs" color={theme.textColors.info}> | ||
{getFormattedDate(createdAt)} | ||
</Text> | ||
</div> | ||
</AuthorWrapper> | ||
<CommentContent size="sm">{comment}</CommentContent> | ||
<Divider variant="disabled" /> | ||
<Spacing size={16} /> | ||
</> | ||
); | ||
}; | ||
|
||
export default CommentItem; | ||
|
||
const AuthorWrapper = styled.div` | ||
display: flex; | ||
gap: 12px; | ||
align-items: center; | ||
`; | ||
|
||
const AuthorProfileImage = styled.img` | ||
border: 1px solid ${({ theme }) => theme.colors.primary}; | ||
border-radius: 50%; | ||
`; | ||
|
||
const CommentContent = styled(Text)` | ||
margin: 16px 0; | ||
`; |
13 changes: 13 additions & 0 deletions
13
frontend/src/components/Recipe/CommentList/CommentList.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
|
||
import CommentList from './CommentList'; | ||
|
||
const meta: Meta<typeof CommentList> = { | ||
title: 'recipe/CommentList', | ||
component: CommentList, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof meta>; | ||
|
||
export const Default: Story = {}; |
35 changes: 35 additions & 0 deletions
35
frontend/src/components/Recipe/CommentList/CommentList.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { Heading, Spacing } from '@fun-eat/design-system'; | ||
import { useRef } from 'react'; | ||
|
||
import CommentItem from '../CommentItem/CommentItem'; | ||
|
||
import { useIntersectionObserver } from '@/hooks/common'; | ||
import { useInfiniteRecipeCommentQuery } from '@/hooks/queries/recipe'; | ||
|
||
interface CommentListProps { | ||
recipeId: number; | ||
} | ||
|
||
const CommentList = ({ recipeId }: CommentListProps) => { | ||
const scrollRef = useRef<HTMLDivElement>(null); | ||
|
||
const { fetchNextPage, hasNextPage, data } = useInfiniteRecipeCommentQuery(Number(recipeId)); | ||
useIntersectionObserver<HTMLDivElement>(fetchNextPage, scrollRef, hasNextPage); | ||
|
||
const comments = data.pages.flatMap((page) => page.comments); | ||
|
||
return ( | ||
<section> | ||
<Heading as="h3" size="lg"> | ||
댓글 ({comments.length}개) | ||
</Heading> | ||
<Spacing size={12} /> | ||
{comments.map((comment) => ( | ||
<CommentItem key={comment.id} recipeComment={comment} /> | ||
))} | ||
<div ref={scrollRef} style={{ height: '1px' }} aria-hidden /> | ||
</section> | ||
); | ||
}; | ||
|
||
export default CommentList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
36 changes: 36 additions & 0 deletions
36
frontend/src/hooks/queries/recipe/useInfiniteRecipeCommentQuery.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { useSuspendedInfiniteQuery } from '../useSuspendedInfiniteQuery'; | ||
|
||
import { recipeApi } from '@/apis'; | ||
import type { CommentResponse } from '@/types/response'; | ||
|
||
interface PageParam { | ||
lastId: number; | ||
totalElements: number | null; | ||
} | ||
|
||
const fetchRecipeComments = async (pageParam: PageParam, recipeId: number) => { | ||
const { lastId, totalElements } = pageParam; | ||
const response = await recipeApi.get({ | ||
params: `/${recipeId}/comments`, | ||
queries: `?lastId=${lastId}&totalElements=${totalElements}`, | ||
}); | ||
const data: CommentResponse = await response.json(); | ||
return data; | ||
}; | ||
|
||
const useInfiniteRecipeCommentQuery = (recipeId: number) => { | ||
return useSuspendedInfiniteQuery( | ||
['recipeComment', recipeId], | ||
({ pageParam = { lastId: 0, totalElements: null } }) => fetchRecipeComments(pageParam, recipeId), | ||
{ | ||
getNextPageParam: (prevResponse: CommentResponse) => { | ||
const lastId = prevResponse.comments[prevResponse.comments.length - 1].id; | ||
const totalElements = prevResponse.totalElements; | ||
const lastCursor = { lastId: lastId, totalElements: totalElements }; | ||
return prevResponse.hasNext ? lastCursor : undefined; | ||
}, | ||
} | ||
); | ||
}; | ||
|
||
export default useInfiniteRecipeCommentQuery; |
24 changes: 24 additions & 0 deletions
24
frontend/src/hooks/queries/recipe/useRecipeCommentMutation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { useMutation, useQueryClient } from '@tanstack/react-query'; | ||
|
||
import { recipeApi } from '@/apis'; | ||
|
||
interface RecipeCommentRequestBody { | ||
comment: string; | ||
} | ||
|
||
const headers = { 'Content-Type': 'application/json' }; | ||
|
||
const postRecipeComment = (recipeId: number, body: RecipeCommentRequestBody) => { | ||
return recipeApi.post({ params: `/${recipeId}/comments`, credentials: true }, headers, body); | ||
}; | ||
|
||
const useRecipeCommentMutation = (recipeId: number) => { | ||
const queryClient = useQueryClient(); | ||
|
||
return useMutation({ | ||
mutationFn: (body: RecipeCommentRequestBody) => postRecipeComment(recipeId, body), | ||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['recipeComment', recipeId] }), | ||
}); | ||
}; | ||
|
||
export default useRecipeCommentMutation; |
Oops, something went wrong.