From 9b6b2f4c113c80d8d9e38d51b945d2a76f9af076 Mon Sep 17 00:00:00 2001 From: minseokim Date: Tue, 6 Jan 2026 15:30:08 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=8A=A4=EC=BC=80=EC=A4=84=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20=EB=B0=8F=20=EC=8A=A4=EC=BC=88=EB=A0=88?= =?UTF-8?q?=ED=86=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{current.tsx => current/index.tsx} | 19 ++- .../{history.tsx => history/index.tsx} | 19 ++- src/app/schedule/_components/meetings.tsx | 139 ------------------ .../_components/{ => meetings}/constants.tsx | 1 + .../schedule/_components/meetings/index.tsx | 96 ++++++++++++ .../meetings/meetings-content/index.tsx | 56 +++++++ .../meetings/meetings-empty/index.tsx | 31 ++++ .../meetings-infinite-scroll/index.tsx | 48 ++++++ .../meetings/meetings-loading/index.tsx | 16 ++ .../_components/{my.tsx => my/index.tsx} | 19 ++- src/app/schedule/page.tsx | 17 ++- .../shared/card/card-skeleton/index.tsx | 9 +- .../use-group-infinite-list/index.ts | 6 +- 13 files changed, 312 insertions(+), 164 deletions(-) rename src/app/schedule/_components/{current.tsx => current/index.tsx} (74%) rename src/app/schedule/_components/{history.tsx => history/index.tsx} (73%) delete mode 100644 src/app/schedule/_components/meetings.tsx rename src/app/schedule/_components/{ => meetings}/constants.tsx (93%) create mode 100644 src/app/schedule/_components/meetings/index.tsx create mode 100644 src/app/schedule/_components/meetings/meetings-content/index.tsx create mode 100644 src/app/schedule/_components/meetings/meetings-empty/index.tsx create mode 100644 src/app/schedule/_components/meetings/meetings-infinite-scroll/index.tsx create mode 100644 src/app/schedule/_components/meetings/meetings-loading/index.tsx rename src/app/schedule/_components/{my.tsx => my/index.tsx} (75%) diff --git a/src/app/schedule/_components/current.tsx b/src/app/schedule/_components/current/index.tsx similarity index 74% rename from src/app/schedule/_components/current.tsx rename to src/app/schedule/_components/current/index.tsx index 058e4612..879f883c 100644 --- a/src/app/schedule/_components/current.tsx +++ b/src/app/schedule/_components/current/index.tsx @@ -3,22 +3,25 @@ import { API } from '@/api'; import { useInfiniteScroll } from '@/hooks/use-group/use-group-infinite-list'; import { useIntersectionObserver } from '@/hooks/use-intersection-observer'; -import { INTERSECTION_OBSERVER_THRESHOLD } from '@/lib/constants/group-list'; +import { GROUP_LIST_PAGE_SIZE, INTERSECTION_OBSERVER_THRESHOLD } from '@/lib/constants/group-list'; import { groupKeys } from '@/lib/query-key/query-key-group'; import { GroupListItemResponse } from '@/types/service/group'; -import { Meetings } from './meetings'; +import { Meetings } from '../meetings/index'; export default function Current() { + const queryKey = groupKeys.myGroupsList('current') as ['myGroups', 'current']; + const { items, error, fetchNextPage, hasNextPage, isFetchingNextPage, - isFetching, + isLoading, completedMessage, - } = useInfiniteScroll({ + refetch, + } = useInfiniteScroll({ queryFn: async ({ cursor, size }) => { return await API.groupService.getMyGroups({ type: 'current', @@ -27,8 +30,8 @@ export default function Current() { myStatuses: ['ATTEND', 'PENDING'], }); }, - queryKey: groupKeys.myGroupsList('current') as ['myGroups', 'current'], - pageSize: 10, + queryKey, + pageSize: GROUP_LIST_PAGE_SIZE, errorMessage: '현재 모임 목록을 불러오는데 실패했습니다.', completedMessage: '모든 현재 모임을 불러왔습니다.', }); @@ -45,12 +48,14 @@ export default function Current() { return ( ({ + refetch, + } = useInfiniteScroll({ queryFn: async ({ cursor, size }) => { return await API.groupService.getMyGroups({ type: 'past', cursor, size }); }, - queryKey: groupKeys.myGroupsList('past') as ['myGroups', 'past'], - pageSize: 10, + queryKey, + pageSize: GROUP_LIST_PAGE_SIZE, errorMessage: '모임 이력을 불러오는데 실패했습니다.', completedMessage: '모든 모임 이력을 불러왔습니다.', }); @@ -40,12 +43,14 @@ export default function History() { return ( { - if (tabType === 'myPost' || (tabType === 'current' && meeting.myMembership?.role === 'HOST')) { - return 'delete'; - } - if (tabType === 'current' && meeting.myMembership?.status === 'PENDING') { - return 'pending'; - } - return 'leave'; -}; - -type MeetingsProps = { - meetings: GroupListItemResponse[]; - tabType: TabType; - emptyStateType: TabType; - emptyStatePath: string; - showActions: boolean; - error?: Error | null; - hasNextPage?: boolean; - isLoading?: boolean; - sentinelRef?: RefObject; - completedMessage?: string; -}; - -const MIN_HEIGHT = 'min-h-[calc(100vh-156px)]'; - -export const Meetings = ({ - meetings, - tabType, - emptyStateType, - emptyStatePath, - showActions, - error, - hasNextPage, - isLoading, - sentinelRef, - completedMessage, -}: MeetingsProps) => { - const router = useRouter(); - - const isEmpty = meetings.length === 0; - const hasError = !!error; - const isLoadingState = !!isLoading; - const showLoading = isEmpty && isLoadingState && !hasError; - const showEmptyState = isEmpty && !hasError && !isLoadingState; - const showErrorOnly = hasError && isEmpty; - const showErrorWithData = hasError && !isEmpty; - - if (showLoading) { - return ( -
-
로딩 중...
-
- ); - } - - if (showEmptyState) { - const config = EMPTY_STATE_CONFIG[emptyStateType]; - const handleEmptyStateClick = () => router.push(emptyStatePath); - - return ( -
- {config.text} - - -
- ); - } - - const handleRetry = () => window.location.reload(); - - return ( -
- {showErrorOnly && ( - - )} - - {meetings.map((meeting) => { - const groupId = String(meeting.id); - const myMembership = meeting.myMembership; - const isPending = myMembership?.status === 'PENDING'; - const isFinished = meeting.status === 'FINISHED'; - const isHost = myMembership?.role === 'HOST'; - const createdBy = meeting.createdBy; - const shouldFetchChatRoomId = showActions && !isPending && !isFinished; - - return ( - - ); - })} - - {showErrorWithData && ( - - )} - - {hasNextPage && !hasError && sentinelRef &&
} - - {!hasNextPage && !isEmpty && !hasError && completedMessage && ( -
{completedMessage}
- )} -
- ); -}; diff --git a/src/app/schedule/_components/constants.tsx b/src/app/schedule/_components/meetings/constants.tsx similarity index 93% rename from src/app/schedule/_components/constants.tsx rename to src/app/schedule/_components/meetings/constants.tsx index 1530bf4a..b4d858c7 100644 --- a/src/app/schedule/_components/constants.tsx +++ b/src/app/schedule/_components/meetings/constants.tsx @@ -3,6 +3,7 @@ import { type ReactNode } from 'react'; export type TabType = 'current' | 'myPost' | 'past'; const DEFAULT_BUTTON_WIDTH = 'w-31'; +export const SCHEDULE_MIN_HEIGHT = 'min-h-[calc(100vh-156px)]' as const; export const EMPTY_STATE_CONFIG: Record< TabType, diff --git a/src/app/schedule/_components/meetings/index.tsx b/src/app/schedule/_components/meetings/index.tsx new file mode 100644 index 00000000..95b9c0a3 --- /dev/null +++ b/src/app/schedule/_components/meetings/index.tsx @@ -0,0 +1,96 @@ +'use client'; + +import { type RefObject } from 'react'; + +import { ErrorMessage } from '@/components/shared'; +import { GroupListItemResponse } from '@/types/service/group'; + +import { type TabType } from './constants'; +import { MeetingsContent } from './meetings-content'; +import { MeetingsEmpty } from './meetings-empty'; +import { MeetingsInfiniteScroll } from './meetings-infinite-scroll'; +import { MeetingsLoading } from './meetings-loading'; + +type MeetingsProps = { + meetings: GroupListItemResponse[]; + tabType: TabType; + emptyStateType: TabType; + emptyStatePath: string; + showActions: boolean; + error?: Error | null; + hasNextPage?: boolean; + isLoading?: boolean; + isFetchingNextPage?: boolean; + sentinelRef?: RefObject; + completedMessage?: string; + refetch?: () => Promise; +}; + +export const Meetings = ({ + meetings, + tabType, + emptyStateType, + emptyStatePath, + showActions, + error, + hasNextPage, + isLoading, + isFetchingNextPage, + sentinelRef, + completedMessage, + refetch, +}: MeetingsProps) => { + const isEmpty = meetings.length === 0; + const hasError = !!error; + const hasItems = meetings.length > 0; + const hasNoItems = isEmpty && !error && !isLoading; + const showErrorOnly = hasError && isEmpty; + const showErrorWithData = hasError && !isEmpty; + const showEmptyState = hasNoItems; + + const handleRetry = () => { + if (refetch) { + refetch(); + } else { + window.location.reload(); + } + }; + + if (isLoading) { + return ; + } + + return ( + <> + {showEmptyState && ( + + )} + + {hasItems && ( +
+ {showErrorOnly && ( +
+ +
+ )} + + + + {showErrorWithData && ( +
+ +
+ )} + + +
+ )} + + ); +}; diff --git a/src/app/schedule/_components/meetings/meetings-content/index.tsx b/src/app/schedule/_components/meetings/meetings-content/index.tsx new file mode 100644 index 00000000..13ffbc9b --- /dev/null +++ b/src/app/schedule/_components/meetings/meetings-content/index.tsx @@ -0,0 +1,56 @@ +import { GroupListItemResponse } from '@/types/service/group'; + +import { ScheduleCard } from '../../card'; +import { type TabType } from '../constants'; + +const getModalType = ( + meeting: GroupListItemResponse, + tabType: TabType, +): 'pending' | 'leave' | 'delete' => { + if (tabType === 'myPost' || (tabType === 'current' && meeting.myMembership?.role === 'HOST')) { + return 'delete'; + } + if (tabType === 'current' && meeting.myMembership?.status === 'PENDING') { + return 'pending'; + } + return 'leave'; +}; + +interface MeetingsContentProps { + meetings: GroupListItemResponse[]; + tabType: TabType; + showActions: boolean; +} + +export const MeetingsContent = ({ meetings, tabType, showActions }: MeetingsContentProps) => { + return ( +
+ {meetings.map((meeting) => { + const groupId = String(meeting.id); + const myMembership = meeting.myMembership; + const isPending = myMembership?.status === 'PENDING'; + const isFinished = meeting.status === 'FINISHED'; + const isHost = myMembership?.role === 'HOST'; + const createdBy = meeting.createdBy; + const shouldFetchChatRoomId = showActions && !isPending && !isFinished; + + return ( + + ); + })} +
+ ); +}; diff --git a/src/app/schedule/_components/meetings/meetings-empty/index.tsx b/src/app/schedule/_components/meetings/meetings-empty/index.tsx new file mode 100644 index 00000000..13ec73e5 --- /dev/null +++ b/src/app/schedule/_components/meetings/meetings-empty/index.tsx @@ -0,0 +1,31 @@ +import { useRouter } from 'next/navigation'; + +import { EmptyState } from '@/components/layout/empty-state'; +import { Button } from '@/components/ui'; + +import { EMPTY_STATE_CONFIG, SCHEDULE_MIN_HEIGHT, type TabType } from '../constants'; + +interface MeetingsEmptyProps { + emptyStateType: TabType; + emptyStatePath: string; +} + +export const MeetingsEmpty = ({ emptyStateType, emptyStatePath }: MeetingsEmptyProps) => { + const router = useRouter(); + const config = EMPTY_STATE_CONFIG[emptyStateType]; + + const handleEmptyStateClick = () => router.push(emptyStatePath); + + return ( +
+ {config.text} + + +
+ ); +}; diff --git a/src/app/schedule/_components/meetings/meetings-infinite-scroll/index.tsx b/src/app/schedule/_components/meetings/meetings-infinite-scroll/index.tsx new file mode 100644 index 00000000..cd367cc2 --- /dev/null +++ b/src/app/schedule/_components/meetings/meetings-infinite-scroll/index.tsx @@ -0,0 +1,48 @@ +import { type RefObject } from 'react'; + +interface MeetingsInfiniteScrollProps { + sentinelRef?: RefObject; + hasNextPage: boolean; + isFetchingNextPage: boolean; + completedMessage: string; + hasError: boolean; +} + +export const MeetingsInfiniteScroll = ({ + sentinelRef, + hasNextPage, + isFetchingNextPage, + completedMessage, + hasError, +}: MeetingsInfiniteScrollProps) => { + if (hasNextPage && !hasError) { + return ( + <> + {sentinelRef &&