-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FE] feat: 꿀조합 댓글 기능 구현 #744
Merged
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
162d561
feat: CommentItem 컴포넌트 추가
xodms0309 6a47996
feat: 댓글 목록 가져오는 쿼리 추가
xodms0309 7d849bc
feat: 상품 상세 페이지에 댓글 컴포넌트 추가
xodms0309 5dc497f
feat: Input 컴포넌트 속성에 minWidth값 추가
xodms0309 e713a04
feat: CommentInput 컴포넌트 추가
xodms0309 f5c90fb
feat: 댓글 등록 기능 구현
xodms0309 33b36c0
feat: 사용자가 입력한 글자수 UI 추가
xodms0309 9a09d11
feat: 리뷰 반영
xodms0309 b0a25cc
feat: text area 텍스트 크기 수정
xodms0309 0e5fa37
feat: CommentList 컴포넌트 추가
xodms0309 02001e1
feat: 디자인 수정
xodms0309 78a91ba
feat: api 변경 적용
xodms0309 7dba7c3
refactor: CommentInput -> CommentForm으로 네이밍 수정
xodms0309 89af090
feat: data fetching 로직을 CommentList내부로 이동
xodms0309 67a109b
feat: 댓글 무한 스크롤로 변경
xodms0309 996e9d0
fix: 토스트 컴포넌트가 가운데 정렬되지 않는 문제 해결
xodms0309 6b4e689
feat: 전송 아이콘 추가
xodms0309 d6ed26b
feat: 댓글 컴포넌트를 fixed로 변경
xodms0309 a860bb6
feat: 댓글 컴포넌트 사이 공백 추가
xodms0309 838d38e
feat: Response 객체에 totalElements 값 추가
xodms0309 980e205
feat: pageParam의 기본값 추가
xodms0309 a08dd8e
feat: index.ts에서 export문 추가
xodms0309 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
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); | ||
Comment on lines
+16
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ♾️👍 |
||
|
||
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; | ||
Comment on lines
+26
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 대박..👍 |
||
}, | ||
} | ||
); | ||
}; | ||
|
||
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.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
짱👍