Conversation
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. Walkthrough모임 상세 페이지를 새로운 FullWidthLayout으로 구현하고, 헤더·정보·모임·책장 섹션 컴포넌트를 추가했습니다. 관련 API 엔드포인트, React Query 훅, 타입을 정의하고, EmptyState를 데이터 기반 구조로 리팩토링했습니다. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In `@src/features/gatherings/components/GatheringMeetingSection.tsx`:
- Around line 1-207: The file fails Prettier formatting; run Prettier to fix
formatting issues (e.g., run `prettier --write` on this file or your repo) and
commit the formatted changes so CI passes; ensure you format the React component
GatheringMeetingSection and related constants (TAB_FILTERS, FILTER_LABELS,
ITEMS_PER_PAGE) and keep only stylistic changes (no logic edits) before pushing.
- Around line 80-102: tabCounts computed inside the useMemo currently derives
counts from rawMeetings (filtered view) and uses a placeholder JOINED value,
causing incorrect badge numbers; update the logic so tabCounts is computed from
the full dataset (or server-provided aggregate counts) instead of
rawMeetings—move the counting out of the filtered/rawMeetings view or call the
server aggregate endpoint, use getMeetingDisplayStatus(meeting.startDateTime,
meeting.endDateTime) to determine UPCOMING/DONE as before, and remove the
Math.floor placeholder for JOINED (either compute JOINED from real participation
data or hide/disable the JOINED badge until real data is available); ensure this
change is applied where tabCounts is defined (the useMemo block) and consumers
of tabCounts expect the new reliable counts.
- Around line 49-109: The component currently builds
rawMeetings/tabCounts/pagination from only the first useGatheringMeetings page;
fix by ensuring all infinite-query pages are loaded before computing these
values: either (A) eagerly load all pages by calling fetchNextPage repeatedly
while hasNextPage in a useEffect and only compute
rawMeetings/tabCounts/totalPages/otherMeetings after data.pages includes all
pages, or (B) on pagination changes, call fetchNextPage in a useEffect when
currentPage requires items not yet present (use data.pages length and
ITEMS_PER_PAGE to decide); update references to useGatheringMeetings,
fetchNextPage, rawMeetings, tabCounts, otherMeetings and totalPages so
computations use the fully-loaded data (or trigger fetchNextPage) before
slicing/paging.
In `@src/features/gatherings/hooks/useGatheringBooks.ts`:
- Line 1: The file has Prettier formatting issues detected by CI for the import
line (the import of useInfiniteQuery), so run the project's formatter to fix it:
execute prettier --write across the repo (or target this file) and reformat the
import and surrounding code so it matches the project's Prettier rules, then
re-run lint/CI; ensure the import statement for useInfiniteQuery remains correct
and saved before committing.
In `@src/features/gatherings/lib/meetingStatus.ts`:
- Around line 41-50: The DONE branch in meetingStatus.ts wrongly compares start
times; change it to compute endA = new Date(a.endDateTime) and endB = new
Date(b.endDateTime) (instead of startA/startB) and return endB.getTime() -
endA.getTime() so DONE items are sorted by most-recent endDateTime (descending);
ensure variables used in that else branch (previously startA/startB) are
replaced or new endA/endB are introduced in the comparison.
In `@src/pages/Gatherings/GatheringDetailPage.tsx`:
- Around line 17-21: The code converts route param id directly via const
gatheringId = Number(id), which yields NaN when id is undefined; change this to
defensively parse and validate the param (e.g., check id truthiness and that
parsed value is a positive integer) before using it: compute gatheringId only
when id is defined and a valid numeric string (fallback to undefined or null
otherwise), update any consumers (like the useGatheringDetail enabled flag) to
rely on this validated value (e.g., enabled: typeof gatheringId === 'number' &&
gatheringId > 0) and ensure other uses of gatheringId handle the undefined/null
case rather than NaN.
🧹 Nitpick comments (11)
src/shared/ui/Badge.tsx (1)
39-43: JSDoc에 xsmall size 설명 누락.size 옵션 설명에
xsmall이 추가되었으나 문서에 반영되지 않음.📝 JSDoc 업데이트 제안
/** * Badge (상태/라벨 배지) - * - `size`로 배지 크기를 지정합니다: small, medium + * - `size`로 배지 크기를 지정합니다: xsmall, small, medium * - `color`로 배지 색상을 지정합니다: red, blue, grey, purple, green, yellow * - `effect`로 그림자 효과를 지정합니다: on, offsrc/features/gatherings/components/GatheringMeetingCard.tsx (1)
40-56: TODO 핸들러 구현 필요.
console.log로 처리된 핸들러들이 있음. 추후 구현 누락 방지를 위해 이슈 트래킹 권장.이 TODO 항목들을 추적할 이슈를 생성해 드릴까요?
src/features/gatherings/hooks/useToggleFavorite.ts (1)
101-104: 주석이 실제 동작과 불일치.주석에는 "즐겨찾기 목록만" 갱신한다고 되어 있으나, 실제로는
detail쿼리도 invalidate함.📝 주석 수정 제안
onSettled: (_data, _error, gatheringId) => { - // 즐겨찾기 목록만 최신 데이터로 갱신 (전체 목록은 optimistic update로 충분) + // 즐겨찾기 목록과 상세 페이지를 최신 데이터로 갱신 (전체 목록은 optimistic update로 충분) queryClient.invalidateQueries({ queryKey: gatheringQueryKeys.favorites() }) queryClient.invalidateQueries({ queryKey: gatheringQueryKeys.detail(gatheringId) }) },src/shared/lib/date.ts (1)
114-137:getDdayText에서new Date()직접 호출 - 테스트 어려움 가능성.문제:
new Date()를 직접 호출하면 단위 테스트 시 시간 모킹이 필요합니다.
영향: 날짜 경계 케이스 테스트가 어려워질 수 있습니다.
대안: 필요시now파라미터를 옵션으로 받거나, 테스트에서vi.useFakeTimers()사용을 고려하세요.현재 구현은 기능적으로 정확하며, 추후 테스트 필요 시 리팩토링 가능합니다.
src/routes/index.tsx (1)
95-128:MainLayout라우트 그룹이 분리됨 - 의도적 구조인지 확인 필요.문제:
MainLayout이 두 개의 별도 라우트 그룹(Lines 64-96, 108-128)으로 분리되어 있습니다.
영향: 기능적으로는 정상 동작하지만, 라우트 구조의 가독성이 다소 떨어집니다.
대안:FullWidthLayout라우트를MainLayout그룹 외부로 분리하거나, 주석으로 의도를 명시하는 것을 고려해보세요.현재 구조가 의도된 것이라면 그대로 유지해도 무방합니다.
라우트 구조 대안 예시
// 메인 페이지들 (GNB 있음) { element: <MainLayout />, children: [ // ... 기존 라우트들 ], }, + // 전체 너비 레이아웃 페이지 (GNB 있음, 컨텐츠 패딩 없음) { element: <FullWidthLayout />, children: [ { path: `${ROUTES.GATHERINGS}/:id`, element: <GatheringDetailPage />, }, + // 추후 전체 너비가 필요한 페이지들 추가 ], }, - // 메인 페이지들 계속 (GNB 있음) - { - element: <MainLayout />, - children: [ + // 또는 아래 라우트들을 첫 번째 MainLayout 그룹으로 통합src/features/gatherings/hooks/useGatheringBooks.ts (1)
30-43:queryKey에size포함 여부 검토 필요현재
queryKey에size가 포함되어 있어size가 변경되면 별도 캐시 엔트리가 생성됩니다.
size가 런타임에 변경되지 않는다면gatheringQueryKeys.books(gatheringId)만으로 충분합니다. 의도적으로 size별 캐시 분리가 필요한 경우라면 현재 구현이 맞습니다.♻️ size를 queryKey에서 제거하는 경우
export const useGatheringBooks = ({ gatheringId, size = DEFAULT_PAGE_SIZE }: UseGatheringBooksOptions) => { return useInfiniteQuery({ - queryKey: [...gatheringQueryKeys.books(gatheringId), size], + queryKey: gatheringQueryKeys.books(gatheringId), queryFn: async ({ pageParam }) => {src/features/gatherings/components/GatheringBookCard.tsx (1)
21-53: 참고: 키보드 접근성 개선 가능
div에onClick만 있어 키보드 사용자가 접근하기 어렵습니다.GatheringCard.tsx처럼role="button",tabIndex={0},onKeyDown핸들러 추가를 고려해 보세요.♻️ 제안 코드
<div className="flex flex-col gap-base cursor-pointer group w-40" onClick={handleClick} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + handleClick() + } + }} >src/features/gatherings/components/GatheringBookshelfSection.tsx (2)
29-41:fetchNextPage호출 후 즉시 스크롤 시 타이밍 이슈 가능
fetchNextPage()호출 후 새 데이터가 렌더링되기 전에scrollBy가 실행됩니다. 새 책 카드가 아직 DOM에 없어서 스크롤 범위가 제한될 수 있습니다.♻️ 데이터 로드 후 스크롤 처리
const handleScrollRight = () => { const container = scrollContainerRef.current if (!container) return const isAtEnd = container.scrollLeft + container.clientWidth >= container.scrollWidth - 10 if (isAtEnd && hasNextPage && !isFetchingNextPage) { - fetchNextPage() + fetchNextPage().then(() => { + // 다음 프레임에서 스크롤 (새 데이터 렌더링 후) + requestAnimationFrame(() => { + container.scrollBy({ left: SCROLL_AMOUNT, behavior: 'smooth' }) + }) + }) + return } container.scrollBy({ left: SCROLL_AMOUNT, behavior: 'smooth' }) }
22-23: 선택:books배열 메모이제이션 고려
data?.pages.flatMap(...)이 매 렌더마다 실행됩니다. 목록이 커지면useMemo로 최적화할 수 있습니다.♻️ useMemo 적용
+import { useRef, useMemo } from 'react' ... - const books = data?.pages.flatMap((page) => page.items) ?? [] + const books = useMemo(() => data?.pages.flatMap((page) => page.items) ?? [], [data?.pages])src/pages/Gatherings/GatheringDetailPage.tsx (1)
32-45:useCallback의존성 최적화 가능
gathering이 의존성에 포함되어 있지만 실제로는 null 체크용으로만 사용됩니다. 데이터가 갱신될 때마다 콜백이 재생성됩니다.♻️ 의존성 최적화
const handleFavoriteToggle = useCallback(() => { - if (!gathering) return + // gathering 존재 여부는 렌더링 조건에서 이미 보장됨 + // 또는 gatheringId > 0 체크로 대체 가능 toggleFavorite(gatheringId, { onError: (error: ApiError) => { if (error.is(ErrorCode.FAVORITE_LIMIT_EXCEEDED)) { openAlert('알림', '즐겨찾기는 최대 4개까지만 등록할 수 있습니다.') } else { openAlert('오류', '즐겨찾기 변경에 실패했습니다.') } }, }) - }, [gatheringId, gathering, toggleFavorite, openAlert]) + }, [gatheringId, toggleFavorite, openAlert])src/features/gatherings/gatherings.types.ts (1)
116-133: description 네이밍이 기존 필드와 불일치합니다
문제: 목록/생성은gatheringDescription인데 상세 응답만description으로 정의돼 있습니다.
영향: 매핑 누락 시 설명이undefined로 표시될 위험이 있습니다.
대안: API 어댑터에서description → gatheringDescription으로 정규화하거나 네이밍을 통일해 주세요.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/pages/Gatherings/GatheringDetailPage.tsx`:
- Around line 66-72: The openError callback currently calls navigate(-1) which
can send users outside the app when there is no previous history; update the
callback used in useEffect (where error, openError, and navigate are referenced)
to detect history availability (e.g., check window.history.length > 1) and only
call navigate(-1) when safe, otherwise navigate to a defined in-app fallback
route (e.g., the gatherings list or '/'); ensure this change is applied inside
the useEffect error branch so openError's onClose handler uses the safe
navigation logic.
🧹 Nitpick comments (2)
src/features/gatherings/lib/meetingStatus.ts (1)
28-55:sortMeetings내부에서getMeetingDisplayStatus가 비교마다 재계산됩니다.문제: sort comparator 내부에서 매 비교(O(n log n))마다 두 아이템의 상태를
new Date()와 함께 재계산합니다.
영향: 목록 크기가 작으면 문제 없지만, 정렬 도중new Date()가 미세하게 달라지면 경계값 아이템의 상태가 비교 중간에 바뀔 수 있어 정렬 불안정을 유발할 수 있습니다.
대안: Schwartzian transform(decorate-sort-undecorate)으로 상태를 한 번만 계산하세요.♻️ 제안 수정
export const sortMeetings = (meetings: GatheringMeetingItem[]): GatheringMeetingItem[] => { - return [...meetings].sort((a, b) => { - const statusA = getMeetingDisplayStatus(a.startDateTime, a.endDateTime) - const statusB = getMeetingDisplayStatus(b.startDateTime, b.endDateTime) + const statusOrder: Record<MeetingDisplayStatus, number> = { + IN_PROGRESS: 0, + UPCOMING: 1, + DONE: 2, + } + + const decorated = meetings.map((m) => ({ + meeting: m, + status: getMeetingDisplayStatus(m.startDateTime, m.endDateTime), + })) + + decorated.sort((a, b) => { + const statusA = a.status + const statusB = b.status - // 약속 중이 최상단 - if (statusA === 'IN_PROGRESS' && statusB !== 'IN_PROGRESS') return -1 - if (statusA !== 'IN_PROGRESS' && statusB === 'IN_PROGRESS') return 1 - - // 예정 > 종료 순서 - if (statusA === 'UPCOMING' && statusB === 'DONE') return -1 - if (statusA === 'DONE' && statusB === 'UPCOMING') return 1 + // 상태 그룹 순서: IN_PROGRESS > UPCOMING > DONE + if (statusA !== statusB) return statusOrder[statusA] - statusOrder[statusB] // 같은 상태 내에서 정렬 - const startA = new Date(a.startDateTime) - const startB = new Date(b.startDateTime) + const startA = new Date(a.meeting.startDateTime) + const startB = new Date(b.meeting.startDateTime) if (statusA === 'UPCOMING') { - // 예정: 가까운 미래 순 (오름차순) return startA.getTime() - startB.getTime() } else { - // 종료: 최근 종료 순 (내림차순) - endDateTime 기준 - const endA = new Date(a.endDateTime) - const endB = new Date(b.endDateTime) + const endA = new Date(a.meeting.endDateTime) + const endB = new Date(b.meeting.endDateTime) return endB.getTime() - endA.getTime() } }) + + return decorated.map((d) => d.meeting) }src/pages/Gatherings/GatheringDetailPage.tsx (1)
33-45:onError콜백 내error파라미터가 외부 스코프error를 섀도잉합니다.가독성 관점에서 콜백 파라미터명을
mutationError등으로 변경하면 혼동을 줄일 수 있습니다.♻️ 제안 수정
- toggleFavorite(gatheringId, { - onError: (error: ApiError) => { - if (error.is(ErrorCode.FAVORITE_LIMIT_EXCEEDED)) { + toggleFavorite(gatheringId, { + onError: (mutationError: ApiError) => { + if (mutationError.is(ErrorCode.FAVORITE_LIMIT_EXCEEDED)) {
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/features/gatherings/components/GatheringMeetingSection.tsx`:
- Around line 53-86: The hook useGatheringMeetings is called without a size so
it defaults to the server's 50-item cap causing items beyond 50 to be missing;
update the call in GatheringMeetingSection to request more items (e.g., pass
size: 1000) or implement server-side pagination by wiring useGatheringMeetings
to accept page/size parameters and driving it from currentPage (and using the
server's total/count to compute totalPages) so otherMeetings/paginatedMeetings
operate on the full result set rather than the truncated default.
In `@src/pages/Gatherings/GatheringDetailPage.tsx`:
- Line 22: The page computes gatheringId with const gatheringId = id ?
Number(id) : 0 which yields NaN for non-numeric ids and causes the query to
never run (enabled: gatheringId > 0), leaving users stuck on the isLoading ||
!gathering guard; update GatheringDetailPage to validate the route param early:
parse id safely (e.g., use Number.isFinite/Number.isInteger or parseInt and
isNaN checks) and if invalid immediately redirect or render a not-found/error
state via router.push or a 404 component instead of continuing to call the fetch
logic—adjust references to gatheringId and the isLoading || !gathering guard
accordingly so invalid ids never cause an infinite loading screen.
🧹 Nitpick comments (3)
src/features/gatherings/gatherings.types.ts (1)
117-141:GatheringBase와 필드명 불일치 확인 필요
GatheringBase(Line 9)는gatheringDescription?: string을,GatheringDetailResponse는description: string | null을 사용합니다. 서버 API 스펙이 실제로 다른 필드명을 반환한다면 문제없지만, 같은 도메인 내 동일 개념의 네이밍이 다르면 혼동의 원인이 됩니다.영향: 타입 재사용이나
extends활용이 어려워짐.
대안: 서버 스펙을 확인하고, 가능하면 필드명을 통일하거나GatheringBase를 확장하는 방식을 검토하세요.src/features/gatherings/components/GatheringMeetingSection.tsx (1)
168-184: 닉네임 문자열 비교로 호스트를 판별하는 방식은 취약할 수 있습니다
meeting.meetingLeaderName === currentUserNickname비교는 동명이인이나 닉네임 변경 시 오판될 수 있습니다.영향: 잘못된 호스트 UI 표시.
대안: 가능하면meetingLeaderId(userId) 기반 비교를 고려하세요. API 스펙 상 어렵다면 현재 방식을 유지하되 주석으로 한계를 명시해 두는 것을 권장합니다.src/pages/Gatherings/GatheringDetailPage.tsx (1)
49-51:ROUTES상수 대신 하드코딩된 경로 사용Line 50에서
`/gatherings/${gatheringId}/settings`를 직접 작성했지만, 다른 핸들러에서는ROUTES.MEETING_SETTING,ROUTES.MEETING_CREATE등 상수를 사용합니다.영향: 라우트 변경 시 이 경로만 누락될 수 있습니다.
대안:ROUTES에 모임 설정 경로 헬퍼를 추가하고 여기서 활용하세요.
| } | ||
|
|
||
| /** 페이지당 표시할 약속 수 */ | ||
| const PAGE_SIZE = 5 |
| import { getGatheringBooks } from '../gatherings.api' | ||
| import { gatheringQueryKeys } from './gatheringQueryKeys' | ||
|
|
||
| const DEFAULT_PAGE_SIZE = 12 |
| const previousDetail = queryClient.getQueryData(gatheringQueryKeys.detail(gatheringId)) | ||
|
|
||
| // Optimistic update - 목록에서 isFavorite 토글 | ||
| queryClient.setQueryData<{ pages: GatheringListResponse[]; pageParams: unknown[] }>( |
There was a problem hiding this comment.
InfiniteData 제네릭 지원 해주더라고여!
queryClient.setQueryData<InfiniteData<GatheringListResponse[]>>
https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientfetchinfinitequery
| import { useGatheringMeetings } from '../hooks/useGatheringMeetings' | ||
| import { useMeetingTabCounts } from '../hooks/useMeetingTabCounts' |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/features/gatherings/components/GatheringMeetingSection.tsx`:
- Line 36: Remove the unused ref declaration: delete the const sectionRef =
useRef<HTMLElement>(null) in the GatheringMeetingSection component and remove
any unused imports related to useRef; ensure no other references to sectionRef
remain in the component (search for "sectionRef") so ESLint/type checker stops
reporting an unused variable.
- Line 155: The host check uses nickname equality
(isHost={meeting.meetingLeaderName === currentUserNickname}) which misidentifies
hosts when nicknames are duplicated; update the model and UI to compare stable
IDs instead: add meetingLeaderId to the GatheringMeetingItem type and ensure the
API returns it, then change the isHost prop to compare meeting.meetingLeaderId
with the current user's id (e.g., currentUserId) wherever isHost is computed
(including GatheringMeetingSection and any related components that expect host
logic) so host UI (badge/permissions) is driven by IDs not nicknames.
🧹 Nitpick comments (3)
src/features/gatherings/hooks/useToggleFavorite.ts (2)
32-36:cancelQueries3개를Promise.all로 병렬 처리 가능순차
await는 불필요한 직렬 대기를 유발합니다. 독립적인 취소 요청이므로 병렬로 실행하면onMutate응답이 빨라집니다.♻️ 제안
- await queryClient.cancelQueries({ queryKey: gatheringQueryKeys.lists() }) - await queryClient.cancelQueries({ queryKey: gatheringQueryKeys.favorites() }) - await queryClient.cancelQueries({ queryKey: gatheringQueryKeys.detail(gatheringId) }) + await Promise.all([ + queryClient.cancelQueries({ queryKey: gatheringQueryKeys.lists() }), + queryClient.cancelQueries({ queryKey: gatheringQueryKeys.favorites() }), + queryClient.cancelQueries({ queryKey: gatheringQueryKeys.detail(gatheringId) }), + ])
101-104:lists쿼리 미갱신 — 의도된 설계인지 확인 필요
favorites와detail은invalidateQueries로 서버 동기화하지만,lists는 낙관적 업데이트에만 의존합니다. 뮤테이션이 실패→롤백 후onSettled에서lists를 갱신하지 않으면, 롤백이 최종 상태가 됩니다(서버 재검증 없음).실패 빈도가 낮다면 현행 유지도 괜찮지만,
favorites처럼lists도invalidate하는 편이 일관적입니다.src/features/gatherings/components/GatheringMeetingSection.tsx (1)
60-69: 이중 정렬을 하나로 합치는 것을 권장합니다.문제:
sortMeetings()의 결과를 다시.sort()로 재정렬하고 있어 의도 파악이 어렵고,.sort()는 배열을 in-place로 변이시킵니다.
영향: 가독성 저하 및sortMeetings내부 정렬 의도가 두 번째 sort에 의해 부분적으로 덮어씌워질 수 있습니다.
대안: 하나의 정렬 함수로 통합하거나,sortMeetings에 IN_PROGRESS 우선 로직을 포함시키세요.♻️ 정렬 로직 통합 예시
- // 정렬 후 진행 중 약속을 상단에 배치 - const sortedMeetings = sortMeetings(meetings) - const displayMeetings = sortedMeetings.sort((a, b) => { - const statusA = getMeetingDisplayStatus(a.startDateTime, a.endDateTime) - const statusB = getMeetingDisplayStatus(b.startDateTime, b.endDateTime) - // 진행 중인 약속을 맨 앞으로 - if (statusA === 'IN_PROGRESS' && statusB !== 'IN_PROGRESS') return -1 - if (statusA !== 'IN_PROGRESS' && statusB === 'IN_PROGRESS') return 1 - return 0 - }) + // 진행 중 약속 우선 → 기존 정렬 기준 유지 + const displayMeetings = [...sortMeetings(meetings)].sort((a, b) => { + const statusA = getMeetingDisplayStatus(a.startDateTime, a.endDateTime) + const statusB = getMeetingDisplayStatus(b.startDateTime, b.endDateTime) + if (statusA === 'IN_PROGRESS' && statusB !== 'IN_PROGRESS') return -1 + if (statusA !== 'IN_PROGRESS' && statusB === 'IN_PROGRESS') return 1 + return 0 + })최소한 스프레드(
[...arr])로 원본 변이를 방지하거나,sortMeetings자체에 IN_PROGRESS 우선 정렬을 포함시키는 것이 더 깔끔합니다.
bf948a4 to
94a7b59
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@src/features/gatherings/components/GatheringBookshelfSection.tsx`:
- Around line 29-41: In handleScrollRight, awaiting fetchNextPage is necessary
so the DOM updates before calling container.scrollBy; change the logic in
handleScrollRight to await the promise returned by fetchNextPage (or listen for
the query hook’s completion/state change) when isAtEnd && hasNextPage &&
!isFetchingNextPage, then call container.scrollBy({ left: SCROLL_AMOUNT,
behavior: 'smooth' }) only after fetchNextPage resolves (or when
isFetchingNextPage becomes false), referencing scrollContainerRef,
fetchNextPage, isFetchingNextPage, hasNextPage and SCROLL_AMOUNT to locate and
update the flow.
In `@src/features/gatherings/components/GatheringDetailInfo.tsx`:
- Around line 23-26: The remainingCount miscalculates when leader is undefined
because it always subtracts 1; update the logic to account for leader existence:
set otherMembers to members.filter(m => m.role !== 'LEADER') only when leader
exists (otherwise otherMembers = members), compute visibleMembers =
otherMembers.slice(0, MAX_VISIBLE_MEMBERS) as before, and compute remainingCount
= totalMembers - (leader ? 1 : 0) - visibleMembers.length so you only subtract
the leader if one is present; use the existing symbols leader, otherMembers,
visibleMembers, totalMembers, MAX_VISIBLE_MEMBERS to locate and update the code.
In `@src/features/gatherings/components/GatheringMeetingSection.tsx`:
- Around line 61-69: Remove the redundant second .sort() that re-sorts the
already-ordered result from sortMeetings; use the sortedMeetings array directly
instead of assigning displayMeetings = sortedMeetings.sort(...). Specifically,
delete the additional .sort(...) block that references getMeetingDisplayStatus
and set displayMeetings = sortedMeetings so sortMeetings retains its IN_PROGRESS
→ UPCOMING → DONE grouping and intra-group ordering.
🧹 Nitpick comments (5)
src/routes/index.tsx (1)
95-128:MainLayout이 두 블록으로 분리되어 있습니다.문제는 아니지만,
MainLayoutchildren이 Line 64-96과 Line 108-128로 나뉘어 있어 가독성이 떨어집니다. React Router v6에서 동일 레이아웃의 라우트를 한 블록으로 모으고,FullWidthLayout블록을 별도로 두는 구조가 더 읽기 쉽습니다.♻️ 구조 정리 제안
// 메인 페이지들 (GNB 있음) { element: <MainLayout />, children: [ { path: ROUTES.HOME, element: <HomePage /> }, { path: ROUTES.HOME_ALIAS, element: <Navigate to={ROUTES.HOME} replace /> }, { path: ROUTES.BOOKS, element: <BookListPage /> }, { path: `${ROUTES.BOOKS}/:id`, element: <BookDetailPage /> }, { path: `${ROUTES.BOOKS}/:id/reviews`, element: <BookReviewHistoryPage /> }, { path: ROUTES.GATHERINGS, element: <GatheringListPage /> }, { path: ROUTES.GATHERING_CREATE, element: <CreateGatheringPage /> }, + { path: `${ROUTES.GATHERINGS}/:gatheringId/meetings/:meetingId`, element: <MeetingDetailPage /> }, + { path: `${ROUTES.GATHERINGS}/:gatheringId/meetings/setting`, element: <MeetingSettingPage /> }, + { path: `${ROUTES.GATHERINGS}/:id/meetings/create`, element: <MeetingCreatePage /> }, + { path: ROUTES.RECORDS, element: <RecordListPage /> }, ], }, // 전체 너비 레이아웃 페이지 { element: <FullWidthLayout />, children: [ { path: `${ROUTES.GATHERINGS}/:id`, element: <GatheringDetailPage /> }, ], }, - // 메인 페이지들 계속 (GNB 있음) - { - element: <MainLayout />, - children: [ - ... - ], - },src/features/gatherings/components/GatheringBookCard.tsx (1)
22-22: 클릭 가능한div→ 키보드 접근성 부재문제:
div+onClick만 있어 키보드 사용자가 접근할 수 없습니다 (Tab포커스,Enter/Space동작 불가).
대안:<button>또는role="link"+tabIndex={0}+onKeyDown처리, 혹은 React Router<Link>로 감싸는 방법을 고려해 주세요. (참고 사항)src/features/gatherings/components/GatheringBookshelfSection.tsx (1)
43-50: 로딩 상태 UI가 단순 텍스트스켈레톤 UI가 아닌 "로딩 중..." 텍스트만 표시됩니다. 다른 섹션과 일관성 있는 로딩 처리를 추후 고려해보세요. 현재 단계에서는 목데이터 기반이므로 큰 문제는 아닙니다.
src/pages/Gatherings/GatheringDetailPage.tsx (1)
35-47:handleFavoriteToggle의gathering의존성 → 불필요한 재생성
gathering은 쿼리 데이터라 매 fetch마다 새 참조가 됩니다. 실제로는gathering이 truthy인지만 확인하므로,!!gathering을 별도 변수로 빼면 재생성을 줄일 수 있습니다.♻️ 제안
+ const hasGathering = !!gathering + const handleFavoriteToggle = useCallback(() => { - if (!gathering) return + if (!hasGathering) return toggleFavorite(gatheringId, { onError: (error: ApiError) => { if (error.is(ErrorCode.FAVORITE_LIMIT_EXCEEDED)) { openAlert('알림', '즐겨찾기는 최대 4개까지만 등록할 수 있습니다.') } else { openAlert('오류', '즐겨찾기 변경에 실패했습니다.') } }, }) - }, [gatheringId, gathering, toggleFavorite, openAlert]) + }, [gatheringId, hasGathering, toggleFavorite, openAlert])src/features/gatherings/gatherings.types.ts (1)
118-141:GatheringDetailResponse와GatheringBase의 필드 중복
gatheringName,totalMembers,daysFromCreation,totalMeetings,invitationLink등이GatheringBase와 겹칩니다. 또한 설명 필드명이descriptionvsgatheringDescription으로 다릅니다.API 응답 스키마가 다르다면 현재 구조가 맞지만, 동일한 스키마라면
GatheringBase를 확장하여 중복을 줄이는 것을 고려해보세요.

🚀 풀 리퀘스트 제안
📋 작업 내용
모임 상세 페이지를 구현했습니다. 모임 정보, 약속 목록, 책장을 한 화면에서 조회할 수 있습니다.
🔧 변경 사항
공유 레이어 확장
xsmall사이즈 추가gap-medium디자인 토큰으로 통일date.ts에formatToDateTimeRange,getDdayText함수 추가theme.css에scrollbar-hide,shadow-drop-bottom유틸리티 추가z-20으로 조정 (상세 헤더와 충돌 방지)useScrollCollapse공유 훅 추가모임 상세 데이터 레이어
useGatheringDetail,useGatheringMeetings,useGatheringBooks훅 구현useGatheringBooks를useInfiniteQuery로 구현 (무한 스크롤)useToggleFavorite에 상세 페이지 optimistic update 추가meetingStatus유틸을lib/디렉토리로 분리모임 상세 컴포넌트
GatheringDetailHeader: 즐겨찾기/설정/초대링크 sticky 헤더GatheringDetailInfo: 모임 통계, 멤버 목록, 설명 표시GatheringMeetingSection: 약속 탭 필터/페이지네이션/정렬GatheringMeetingCard: 약속 중/예정/종료 상태별 카드GatheringBookshelfSection: 캐러셀 + 무한스크롤 책장GatheringBookCard: 책 표지/평점 카드EmptyState에meetings,bookshelf타입 추가레이아웃/라우팅
FullWidthLayout추가 (GNB 있고 컨텐츠 패딩 없는 레이아웃)FullWidthLayout하위로 라우팅페이지
GatheringDetailPage전체 구현 (헤더/정보/약속/책장 섹션 조합)📸 스크린샷 (선택 사항)
📄 기타
Summary by CodeRabbit
Release Notes
New Features