-
Notifications
You must be signed in to change notification settings - Fork 2
Feat(client): CardList 컴포넌트 구현 #284
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
base: develop
Are you sure you want to change the base?
Changes from all commits
8a1f9e6
066fc76
97868e5
e53ecaa
44dda1e
de84b41
0595db9
473c0d1
c69e696
8aab738
385d4cd
d11a36b
d9662ca
27f081b
2e6481c
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 |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { style } from '@vanilla-extract/css'; | ||
|
|
||
| const THREE_COLUMN_MIN_WIDTH = '1060px'; | ||
| const MEMO_LIST_CONTAINER = 'memoList'; | ||
|
|
||
| const columns = (count: number) => `repeat(${count}, minmax(34rem, 38rem))`; | ||
|
|
||
| export const memoListContainer = style({ | ||
| width: '100%', | ||
| containerType: 'inline-size', | ||
| containerName: MEMO_LIST_CONTAINER, | ||
| }); | ||
|
|
||
| export const memoListGrid = style({ | ||
| display: 'grid', | ||
| gridTemplateColumns: columns(2), | ||
| justifyContent: 'safe center', | ||
| gap: '2rem', | ||
|
|
||
| '@container': { | ||
| [`${MEMO_LIST_CONTAINER} (min-width: ${THREE_COLUMN_MIN_WIDTH})`]: { | ||
| gridTemplateColumns: columns(3), | ||
| }, | ||
| }, | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import Card from '@shared/components/card/card'; | ||
| import { components } from '@shared/types/schema'; | ||
|
|
||
| import * as styles from './memo-list.css'; | ||
|
|
||
| type CardType = components['schemas']['MemoDashboardResponse']; | ||
|
|
||
| interface MemoListProps { | ||
| cards: CardType[]; | ||
| isSelected: boolean; | ||
| isDragging: boolean; | ||
| onClickCard: () => void; | ||
| } | ||
|
|
||
| const MemoList = ({ | ||
| cards, | ||
| isSelected, | ||
| isDragging, | ||
| onClickCard, | ||
| }: MemoListProps) => { | ||
| return ( | ||
| <div className={styles.memoListContainer}> | ||
| <div className={styles.memoListGrid}> | ||
| {cards.map((card) => ( | ||
| <Card | ||
| key={card.memoId} | ||
| card={{ | ||
|
Comment on lines
+25
to
+27
Member
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. 행님 이곳에 isNew가 누락된 것 같습니다. isNew를 심폐소생술로 부활시켜야할 것 같다데스⚔️ |
||
| tagList: card.tagList, | ||
| title: card.title ?? '', | ||
| content: card.content ?? '', | ||
| fileCount: card.fileCount ?? 0, | ||
| imageCount: card.imageCount ?? 0, | ||
| createAt: card.createdAt ?? '', | ||
| }} | ||
|
Comment on lines
+27
to
+34
Collaborator
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. props로 보내는게 많아져서 card 객체를 통째로 보내고 |
||
| isSelected={isSelected} | ||
| isDragging={isDragging} | ||
| onClick={onClickCard} | ||
| /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default MemoList; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { useEffect, useRef } from 'react'; | ||
| import { type FetchNextPageOptions } from '@tanstack/react-query'; | ||
|
|
||
| // 바닥에 닿기 전에 미리 불러와 로딩을 기다리는 구간을 없앤다 | ||
| const LOAD_MORE_ROOT_MARGIN = '200px'; | ||
|
|
||
| interface UseInfiniteScrollProps { | ||
| hasNextPage: boolean; | ||
| isFetchingNextPage: boolean; | ||
| fetchNextPage: (options?: FetchNextPageOptions) => void; | ||
| } | ||
|
|
||
| /** | ||
| * 목록 끝에 둔 요소가 화면에 들어오면 다음 페이지를 불러오는 무한 스크롤 훅 | ||
| * | ||
| * | ||
| * @param hasNextPage - false면 관측하지 않는다 | ||
| * @param isFetchingNextPage - 패칭 중에는 관측을 멈춘다. 패칭이 끝나면 옵저버를 | ||
| * 다시 만들어 관측 요소가 계속 보이는 상태에서도 다음 페이지가 이어진다 | ||
| * @param fetchNextPage - cancelRefetch 기본값(true)은 진행 중인 요청을 취소하고 | ||
| * 재시작하므로 false로 넘겨 호출한다 | ||
| * @returns 목록 마지막에 배치할 요소의 ref | ||
| */ | ||
| export const useInfiniteScroll = ({ | ||
| hasNextPage, | ||
| isFetchingNextPage, | ||
| fetchNextPage, | ||
| }: UseInfiniteScrollProps) => { | ||
| const loadMoreRef = useRef<HTMLDivElement>(null); | ||
|
|
||
| useEffect(() => { | ||
| const loadMoreTarget = loadMoreRef.current; | ||
| if (!loadMoreTarget || !hasNextPage || isFetchingNextPage) return; | ||
|
Collaborator
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. p4) 인지 관점에서 아래 코드처럼 하는 것도 깔끔해져요! if (!loadMoreTarget) return;
if (!hasNextPage) return;
if (isFetchingNextPage) return; |
||
|
|
||
| const observer = new IntersectionObserver( | ||
| ([entry]) => { | ||
| if (entry?.isIntersecting) fetchNextPage({ cancelRefetch: false }); | ||
| }, | ||
| { rootMargin: LOAD_MORE_ROOT_MARGIN }, | ||
| ); | ||
| observer.observe(loadMoreTarget); | ||
|
|
||
| return () => observer.disconnect(); | ||
| }, [hasNextPage, isFetchingNextPage, fetchNextPage]); | ||
|
|
||
| return loadMoreRef; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,8 @@ | ||
| import { style } from '@vanilla-extract/css'; | ||
|
|
||
| import { themeVars } from '@cds/ui'; | ||
|
|
||
| export const container = style({ | ||
| ...themeVars.fontStyles.display_sb_36, | ||
| backgroundImage: themeVars.color.gradient02, | ||
| color: themeVars.color.grey700, | ||
| width: '30rem', | ||
| width: '100%', | ||
| paddingInline: '4rem', | ||
| display: 'flex', | ||
| justifyContent: 'center', | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,43 +2,62 @@ import { PATH } from '@router/path'; | |
| import { useNavigate, useSearchParams } from 'react-router'; | ||
|
|
||
| import EmptyView from '@shared/components/empty-view/empty-view'; | ||
| import MemoListView from '@shared/components/memo-list-view/memo-list-view'; | ||
|
|
||
| import { useGetAllMemo, useGetMemoTotalCount } from './apis/queries'; | ||
| import MemoList from './components/memo-list/memo-list'; | ||
| import { useInfiniteScroll } from './hooks/use-infinite-scroll'; | ||
|
|
||
| import * as styles from './memo-page.css'; | ||
|
|
||
| import emptyImage from '/empty.svg'; | ||
|
|
||
| const AllMemoPage = () => { | ||
| const navigate = useNavigate(); | ||
| const [searchParams] = useSearchParams(); | ||
| const tagParam = searchParams.get('tag'); | ||
| const tagId = tagParam !== null ? [Number(tagParam)] : undefined; | ||
| const tagIds = tagParam !== null ? [Number(tagParam)] : undefined; | ||
|
|
||
| const { | ||
| data: filteredMemos, | ||
| data: memosList, | ||
| hasNextPage, | ||
| isFetchingNextPage, | ||
| fetchNextPage, | ||
| } = useGetAllMemo(tagId); | ||
| const { data: totalCount } = useGetMemoTotalCount(tagId); | ||
|
|
||
| return totalCount === 0 ? ( | ||
| <EmptyView | ||
| imgSrc={emptyImage} | ||
| title="작성된 메모가 없습니다." | ||
| description="새 메모 창에 들어가서 새로운 메모를 생성해보세요." | ||
| buttonText="메모 작성하러 가기" | ||
| onButtonClick={() => navigate(PATH.NEW_MEMO)} | ||
| /> | ||
| ) : ( | ||
| <MemoListView | ||
| title="전체 메모" | ||
| initialMemos={filteredMemos} | ||
| hasNextPage={hasNextPage} | ||
| isFetchingNextPage={isFetchingNextPage} | ||
| fetchNextPage={fetchNextPage} | ||
| totalCount={totalCount ?? 0} | ||
| /> | ||
| } = useGetAllMemo(tagIds); | ||
| const { data: totalCount } = useGetMemoTotalCount(tagIds); | ||
|
|
||
| const loadMoreRef = useInfiniteScroll({ | ||
| hasNextPage, | ||
| isFetchingNextPage, | ||
| fetchNextPage, | ||
| }); | ||
|
|
||
| return ( | ||
| <> | ||
| {/* MemoHeader 컴포넌트 */} | ||
|
|
||
| <div className={styles.container}> | ||
| {totalCount === 0 ? ( | ||
| <EmptyView | ||
| imgSrc={emptyImage} | ||
| title="작성된 메모가 없습니다." | ||
| description="새 메모 창에 들어가서 새로운 메모를 생성해보세요." | ||
| buttonText="메모 작성하러 가기" | ||
| onButtonClick={() => navigate(PATH.NEW_MEMO)} | ||
| /> | ||
| ) : ( | ||
| <> | ||
| {/* 카드 선택/드래그, 상세 페이지 연결 전까지 임시 값 전달 */} | ||
| <MemoList | ||
| cards={memosList ?? []} | ||
| isSelected={false} | ||
| isDragging={false} | ||
| onClickCard={() => {}} | ||
| /> | ||
| <div ref={loadMoreRef} /> | ||
|
Comment on lines
+49
to
+56
Member
Author
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. 임시로 이렇게 둘께욥 ! |
||
| </> | ||
| )} | ||
| </div> | ||
| </> | ||
| ); | ||
| }; | ||
|
|
||
|
|
||
|
Collaborator
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.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,43 +3,45 @@ import { ComponentProps } from 'react'; | |
| import { Icon } from '@cds/icon'; | ||
| import { Label, Title } from '@cds/ui'; | ||
|
|
||
| import { components } from '@shared/types/schema'; | ||
| import { formatDate } from '@shared/utils/format-date'; | ||
|
|
||
| import * as styles from './card.css'; | ||
|
|
||
| // TODO: Tag 백엔드 타입에 맞게 변경 | ||
| type TagType = { | ||
| tagId?: number; | ||
| name?: string; | ||
| }; | ||
|
|
||
| type CardInfoType = { | ||
| tagList?: TagType[]; | ||
| tagList?: components['schemas']['TagResponse'][]; | ||
| title: string; | ||
| content: string; | ||
| fileCount: number; | ||
| imageCount: number; | ||
| createAt: string; | ||
| isNew?: boolean; | ||
| }; | ||
| interface CardProps extends ComponentProps<'article'> { | ||
| card: CardInfoType; | ||
| isSelected?: boolean; | ||
| isDragging?: boolean; | ||
| isNewAi?: boolean; | ||
| } | ||
|
|
||
| const Card = ({ | ||
|
Collaborator
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. Card 컴포넌트의 이름 더 구체화하면 어떨까요? |
||
| card: { tagList = [], title, content, fileCount, imageCount, createAt }, | ||
| card: { | ||
| tagList = [], | ||
| title, | ||
| content, | ||
| fileCount, | ||
| imageCount, | ||
| createAt, | ||
| isNew = false, | ||
| }, | ||
| isSelected = false, | ||
| isDragging = false, | ||
| isNewAi = false, | ||
| ...props | ||
| }: CardProps) => { | ||
| return ( | ||
| <article | ||
| className={styles.cardContainer({ isSelected, isDragging, isNewAi })} | ||
| draggable | ||
| {...props} | ||
| className={styles.cardContainer({ isSelected, isDragging, isNew })} | ||
| draggable | ||
| > | ||
| <div className={styles.mainInfoContainer}> | ||
| <div className={styles.tagContainer}> | ||
|
|
||
This file was deleted.
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.
이것은 뭔밍 정말 처음봐요 안전한 센터 참 신기하네요