-
Notifications
You must be signed in to change notification settings - Fork 2
✨ Feat: 카드 목록, 모달 내 댓글 목록에 무한 스크롤 구현 #101
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,25 +1,46 @@ | ||
| import { format } from 'date-fns' | ||
| import { useState } from 'react' | ||
| import { toast } from 'sonner' | ||
|
|
||
| import { Avatar } from '@/app/shared/components/common/Avatar' | ||
|
|
||
| import useCommentsQuery from '../../api/useCommentsQuery' | ||
| import { useInfiniteComments } from '../../api/useInfiniteComments' | ||
| import { useInfiniteScroll } from '../../hooks/useInfiniteScroll' | ||
| import { Comment as CommentType } from '../../type/Comment.type' | ||
| import Comment from './Comment' | ||
|
|
||
| export default function Comments({ cardId }: { cardId: number }) { | ||
| const { data, isLoading, error } = useCommentsQuery(cardId) | ||
| const comments = data?.comments | ||
| export default function Comments({ | ||
| cardId, | ||
| scrollRef, | ||
| }: { | ||
| cardId: number | ||
| scrollRef: React.RefObject<HTMLElement> | ||
| }) { | ||
| const { | ||
| data, | ||
| fetchNextPage, | ||
| hasNextPage, | ||
| isFetchingNextPage, | ||
| isLoading, | ||
| isError, | ||
| } = useInfiniteComments(cardId) | ||
|
|
||
| useInfiniteScroll(fetchNextPage, hasNextPage, isFetchingNextPage, scrollRef) | ||
|
|
||
| if (error) { | ||
| toast.error(error?.message || '문제가 발생했습니다.') | ||
| if (isError) { | ||
| toast.error('댓글 불러오기 실패') | ||
| } | ||
| return ( | ||
| <div className="flex w-450 flex-col gap-20"> | ||
| {comments?.map((comment: CommentType) => ( | ||
| <Comment key={comment.id} comment={comment} /> | ||
| ))} | ||
| </div> | ||
| <> | ||
| <div className="flex w-450 flex-col gap-20"> | ||
| {data?.pages.map((page) => | ||
| page?.comments.map((comment: CommentType) => ( | ||
| <Comment key={comment.id} comment={comment} /> | ||
| )), | ||
| )} | ||
| </div> | ||
| {/* 무한 스크롤 관련 */} | ||
| {isFetchingNextPage && ( | ||
| <p className="text-center text-sm text-gray-400"> | ||
| 댓글을 불러오는 중... | ||
| </p> | ||
| )} | ||
| </> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,18 +1,31 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Image from 'next/image' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState } from 'react' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { toast } from 'sonner' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { cn } from '@/app/shared/lib/cn' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useCardMutation } from '../api/useCardMutation' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import useCards from '../api/useCards' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useInfiniteCards } from '../api/useInfiniteCards' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Card from '../Card/Card' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import CreateCardForm from '../Card/cardFormModals/CreateCardForm' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import CreateCardModal from '../Card/cardFormModals/CreateCardModal' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useInfiniteScroll } from '../hooks/useInfiniteScroll' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useDragStore } from '../store/useDragStore' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { Column as ColumnType } from '../type/Column.type' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default function Column({ column }: { column: ColumnType }) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { id, title }: { id: number; title: string } = column | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { data, isLoading, error } = useCards(id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchNextPage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hasNextPage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isFetchingNextPage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isLoading, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isError, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } = useInfiniteCards(id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useInfiniteScroll(fetchNextPage, hasNextPage, isFetchingNextPage) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [isDraggingover, setDraggingover] = useState(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { draggingCard, clearDraggingCard } = useDragStore() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cardMutation = useCardMutation() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -21,8 +34,8 @@ export default function Column({ column }: { column: ColumnType }) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [openCreateColumn, setOpenCreateColumn] = useState(false) //page.tsx | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [oepnConfigColumn, setConfigColumn] = useState(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isLoading) return <p>loading...</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (error) return <p>error...{error.message}</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isLoading) return <p>loading...</p> // 스켈레톤 적용???⭐️ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isError) return toast.error('할 일 불러오기 실패') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+37
to
+38
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. 🛠️ Refactor suggestion 로딩과 에러 처리 개선 필요 현재 로딩 상태에서 간단한 텍스트만 표시하고, 에러 상태에서도 컴포넌트가 계속 렌더링됩니다. 개선을 고려해보세요. - if (isLoading) return <p>loading...</p> // 스켈레톤 적용???⭐️
- if (isError) return toast.error('할 일 불러오기 실패')
+ if (isLoading) {
+ return (
+ <div className="BG-gray Border-column flex w-354 shrink-0 flex-col gap-16 p-20 tablet:w-584">
+ <div className="mb-24 flex items-center justify-between">
+ <div className="flex items-center">
+ <div className="BG-blue mb-7 mr-8 size-8 rounded-25"></div>
+ <h2 className="mb-3 mr-12 h-21 text-18 font-bold">{title}</h2>
+ </div>
+ </div>
+ <p className="text-center text-gray-500">카드를 불러오는 중...</p>
+ </div>
+ )
+ }
+ if (isError) {
+ toast.error('할 일 불러오기 실패')
+ return (
+ <div className="BG-gray Border-column flex w-354 shrink-0 flex-col gap-16 p-20 tablet:w-584">
+ <div className="mb-24 flex items-center justify-between">
+ <div className="flex items-center">
+ <div className="BG-blue mb-7 mr-8 size-8 rounded-25"></div>
+ <h2 className="mb-3 mr-12 h-21 text-18 font-bold">{title}</h2>
+ </div>
+ </div>
+ <p className="text-center text-red-500">카드를 불러올 수 없습니다.</p>
+ </div>
+ )
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -63,7 +76,7 @@ export default function Column({ column }: { column: ColumnType }) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="BG-blue mb-7 mr-8 size-8 rounded-25"></div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h2 className="mb-3 mr-12 h-21 text-18 font-bold">{title}</h2> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className="Text-gray block size-20 rounded-4 bg-[#EEEEEE] pt-3 text-center text-12 font-medium leading-tight dark:bg-[#2E2E2E]"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {data?.totalCount} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {data?.pages[0]?.totalCount ?? 0} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Image | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -88,10 +101,11 @@ export default function Column({ column }: { column: ColumnType }) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {data?.cards.map((card) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Card key={card.id} card={card} column={column} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {data?.pages.map((page) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| page.cards.map((card) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Card key={card.id} card={card} column={column} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* 카드 생성 모달 */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {openCreateCard && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <CreateCardModal> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -101,6 +115,19 @@ export default function Column({ column }: { column: ColumnType }) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </CreateCardModal> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* 무한 스크롤 관련 */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {isFetchingNextPage && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="text-center text-sm text-gray-400"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 카드를 불러오는 중... | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {!hasNextPage && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="py-4 text-center text-sm text-gray-300"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 모든 카드를 불러왔습니다 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,17 @@ | ||||||||||
| import { useInfiniteQuery } from '@tanstack/react-query' | ||||||||||
|
|
||||||||||
| import { CardResponse } from '../type/Card.type' | ||||||||||
| import { fetchCards } from './fetchCards' | ||||||||||
|
|
||||||||||
| export function useInfiniteCards(columnId: number) { | ||||||||||
| return useInfiniteQuery<CardResponse>({ | ||||||||||
| queryKey: ['columnId', columnId], | ||||||||||
|
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. 🛠️ Refactor suggestion 쿼리 키 네이밍이 부정확합니다 쿼리 키에 - queryKey: ['columnId', columnId],
+ queryKey: ['cards', columnId],📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| queryFn: ({ pageParam = null }) => | ||||||||||
| fetchCards({ columnId, cursorId: pageParam as number }), | ||||||||||
|
Comment on lines
+9
to
+10
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. 타입 캐스팅 안전성을 개선해야 합니다
queryFn: ({ pageParam = null }) =>
- fetchCards({ columnId, cursorId: pageParam as number }),
+ fetchCards({ columnId, cursorId: pageParam as number | null }),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| getNextPageParam: (lastPage) => { | ||||||||||
| // 마지막 카드의 id를 다음 cursor로 사용 | ||||||||||
| return lastPage.cards.length === 0 ? undefined : lastPage.cursorId | ||||||||||
| }, | ||||||||||
| initialPageParam: null, | ||||||||||
| }) | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,17 @@ | ||||||||||
| import { useInfiniteQuery } from '@tanstack/react-query' | ||||||||||
|
|
||||||||||
| import { CommentsResponse } from '../type/Comment.type' | ||||||||||
| import { fetchComments } from './fetchComments' | ||||||||||
|
|
||||||||||
| export function useInfiniteComments(cardId: number) { | ||||||||||
| return useInfiniteQuery<CommentsResponse>({ | ||||||||||
| queryKey: ['comments', cardId], | ||||||||||
| queryFn: ({ pageParam = null }) => | ||||||||||
| fetchComments({ cardId, cursorId: pageParam as number }), | ||||||||||
|
Comment on lines
+9
to
+10
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. 타입 캐스팅 안전성을 개선해야 합니다
queryFn: ({ pageParam = null }) =>
- fetchComments({ cardId, cursorId: pageParam as number }),
+ fetchComments({ cardId, cursorId: pageParam as number | null }),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| getNextPageParam: (lastPage) => { | ||||||||||
| // 마지막 카드의 id를 다음 cursor로 사용 | ||||||||||
| return lastPage.comments.length === 0 ? undefined : lastPage.cursorId | ||||||||||
| }, | ||||||||||
| initialPageParam: null, | ||||||||||
| }) | ||||||||||
| } | ||||||||||
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.
입력 필드 크기 불일치
Input-readOnly-217인풋 너비(217px)가 래퍼의w-207(207px)보다 커 오버플로우가 발생할 수 있습니다. 래퍼 또는 인풋 클래스 중 하나를w-217또는w-207으로 일치시키는 패치가 필요합니다.🤖 Prompt for AI Agents