Skip to content

Comments

feat: 메인페이지 구현 (#70)#80

Merged
mgYang53 merged 8 commits intodevelopfrom
feat/main-page-70
Feb 19, 2026
Merged

feat: 메인페이지 구현 (#70)#80
mgYang53 merged 8 commits intodevelopfrom
feat/main-page-70

Conversation

@mgYang53
Copy link
Contributor

@mgYang53 mgYang53 commented Feb 16, 2026

🚀 풀 리퀘스트 제안

📋 작업 내용

로그인한 사용자의 대시보드 역할을 하는 메인페이지를 구현했습니다.
읽고 있는 책, 내 약속 리스트, 즐겨찾는 모임 3개 섹션으로 구성됩니다.

🔧 변경 사항

  • 지금 읽고 있는 책: BookCarousel 컴포넌트 (호버 시 좌우 화살표, 스크롤 감지), HomeBookCard
  • 내 약속: 커서 기반 무한 페이지네이션 (useInfiniteQuery), 탭별 카운트 배지, 펼치기/접기, 상태별 액션 버튼 (예정→사전 의견 작성, 종료→개인 회고 작성)
  • 즐겨찾는 모임: 3열 그리드 GatheringCard, 즐겨찾기 토글
  • 스켈레톤 UI: Figma 디자인 규칙 기반 (base: grey-300, text radius: 4px, 텍스트 높이 스펙 준수)
  • useDeferredLoading 훅: 지연 표시(200ms) + 최소 표시 시간(500ms)으로 스켈레톤 깜빡임 방지
  • 내 약속 API 레이어 추가 (GET /api/meetings/me, GET /api/meetings/me/tab-counts)

📸 스크린샷 (선택 사항)

스크린샷 2026-02-12 오후 11 21 53 스크린샷 2026-02-16 오후 2 53 46 스크린샷 2026-02-16 오후 2 53 52

📄 기타

  • 사전 의견 템플릿 미제작 시 버튼 disabled 처리는 API 필드 추가 후 대응 예정 (TODO 주석)

Summary by CodeRabbit

  • 새로운 기능

    • 홈페이지 재구성: 읽고 있는 책, 내 약속, 즐겨찾는 모임 섹션 추가
    • 가로 스크롤 책 카로셀과 북/모임 카드(홈용) 컴포넌트 추가
    • 즐겨찾는 모임 섹션(목록·즐겨찾기 토글) 및 독서 섹션(탭·CTA) 추가
    • 내 약속 섹션: 탭 필터(전체/예정/완료), 탭별 카운트 배지, 페이징 및 펼치기/접기
  • 사용성 개선

    • 약속 생성 시 툴팁 안내 표시 및 내비게이션 상태 처리로 중복 안내 방지
    • 로딩 UX 개선을 위한 지연·최소 표시 스켈레톤 도입

읽고 있는 책, 내 약속 리스트, 즐겨찾는 모임 3개 섹션 구현.
- BookCarousel: 호버 시 좌우 화살표, 스크롤 감지 캐러셀
- MyMeetingsSection: 커서 기반 페이지네이션, 펼치기/접기
- FavoriteGatheringsSection: 3열 그리드 즐겨찾기 모임 카드
- 각 섹션 스켈레톤 UI 적용 (Figma 디자인 규칙 기반)
- useDeferredLoading 훅: 지연 표시 + 최소 표시 시간으로 깜빡임 방지
@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

Walkthrough

홈 페이지 섹션 추가(ReadingBooks, MyMeetings, FavoriteGatherings), BookCarousel 컴포넌트 도입, 개인 미팅 관련 타입·엔드포인트·API·훅 및 useDeferredLoading 훅과 PAGE_SIZES.MY_MEETINGS 상수 추가, Tooltip 임포트 경로 통합, 생성 후 툴팁 상태 처리 로직 추가.

Changes

Cohort / File(s) Summary
BookCarousel 컴포넌트
src/features/book/components/BookCarousel.tsx, src/features/book/components/index.ts, src/features/book/index.ts
수평 스크롤 캐러셀 추가. 좌/우 네비게이션, ResizeObserver, 스무스 스크롤, 컴포넌트 재수출.
MyMeetings 타입·엔드포인트·API
src/features/meetings/meetings.types.ts, src/features/meetings/meetings.endpoints.ts, src/features/meetings/meetings.api.ts, src/features/meetings/index.ts
MyMeeting 관련 타입(MyMeeting* 등), 엔드포인트(MY_MEETINGS, MY_MEETING_TAB_COUNTS), API 함수(getMyMeetings, getMyMeetingTabCounts) 추가.
MyMeetings 훅/쿼리
src/features/meetings/hooks/myMeetingQueryKeys.ts, src/features/meetings/hooks/useMyMeetings.ts, src/features/meetings/hooks/useMyMeetingTabCounts.ts, src/features/meetings/hooks/index.ts
react-query 키 팩토리 및 useInfiniteQuery 기반 useMyMeetings, 탭 카운트용 useMyMeetingTabCounts 추가; 훅 인덱스에 재수출.
홈 페이지 섹션·카드 컴포넌트
src/pages/Home/HomePage.tsx, src/pages/Home/components/...
ReadingBooksSection, MyMeetingsSection, FavoriteGatheringsSection 및 HomeBookCard, HomeMeetingCard, HomeSectionHeader 등 섹션·카드 추가 및 레이아웃/탭·확장/페이징 로직 추가.
지연 로딩 훅 및 재수출
src/shared/hooks/useDeferredLoading.ts, src/shared/hooks/index.ts
로딩 깜빡임 방지용 useDeferredLoading 훅 추가(딜레이·최소기간) 및 훅 인덱스에 재수출.
Tooltip 임포트 통합
src/features/topics/components/TopicHeader.tsx, src/pages/ComponentGuide/ComponentGuidePage.tsx
Tooltip 관련 컴포넌트 임포트를 shared UI 배럴로 통합(임포트 경로 변경).
Gathering 툴팁 상태·네비게이션 상태 전달
src/features/gatherings/components/GatheringMeetingSection.tsx, src/pages/Gatherings/CreateGatheringPage.tsx
네비게이션 상태로 생성 직후 표시할 툴팁 제어 및 네비게이션 시 { justCreated: true } 전달 추가.
상수 변경
src/shared/constants/pagination.ts
PAGE_SIZES에 MY_MEETINGS: 4 추가.

Sequence Diagram

sequenceDiagram
    participant User as 사용자
    participant MyMeetingsSection as MyMeetingsSection
    participant Hook as useMyMeetings Hook
    participant API as Meetings API
    participant Card as HomeMeetingCard

    User->>MyMeetingsSection: 페이지 진입 / 탭 선택
    MyMeetingsSection->>Hook: 쿼리 요청(myMeetingQueryKeys.list(filter))
    Hook->>API: getMyMeetings(filter, startDateTime?, meetingId?)
    API-->>Hook: 응답({ items, nextCursor, hasNext })
    Hook-->>MyMeetingsSection: 데이터, isLoading, hasNextPage
    MyMeetingsSection->>Card: 항목 렌더링(최대 표시 개수)
    User->>MyMeetingsSection: '펼치기' 클릭
    MyMeetingsSection->>Hook: fetchNextPage()
    Hook->>API: 다음 페이지 요청(nextCursor)
    API-->>Hook: 추가 항목 반환
    Hook-->>MyMeetingsSection: 새로운 페이지 데이터
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • choiyoungae
  • haruyam15
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목 '메인페이지 구현'은 핵심 변경사항을 명확하게 요약하고 있으며, 실제 변경 내용(메인페이지 대시보드 UI, 북 캐러셀, 내 약속 섹션, 즐겨찾는 모임 섹션 등)과 정확히 부합합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/main-page-70

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mgYang53 mgYang53 self-assigned this Feb 16, 2026
@mgYang53 mgYang53 added the feat 새로운 기능 추가 label Feb 16, 2026
@mgYang53
Copy link
Contributor Author

@coderabbitai

최근 4개 커밋만 리뷰해줘

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

🧩 Analysis chain

@mgYang53 최근 4개 커밋을 확인하고 리뷰를 시작하겠습니다.


🏁 Script executed:

#!/bin/bash
# 최근 4개 커밋의 정보 확인
git log -4 --oneline --no-decorate

Length of output: 152


@mgYang53 최근 4개 커밋을 확인했습니다:

  1. 438e644 - style: prettier 포맷팅 수정
  2. f86b75d - feat: 모임 생성 직후 약속 만들기 툴팁 표시
  3. 2395268 - style: prettier 포맷팅 수정
  4. ac5ab0c - feat: 메인페이지 대시보드 UI 구현

해당 커밋들에 대한 리뷰를 시작하겠습니다.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mgYang53 mgYang53 changed the title feat: 메인페이지 대시보드 UI 구현 (#70) feat: 메인페이지 구현 (#70) Feb 16, 2026
@mgYang53 mgYang53 changed the base branch from main to develop February 16, 2026 06:55
Copy link
Contributor

@haruyam15 haruyam15 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로딩 상태의 깜빡임을 방지하는 훅 멋지네여

Comment on lines 14 to 34
function formatMeetingDate(dateStr: string) {
const date = new Date(dateStr)
const yy = String(date.getFullYear()).slice(2)
const mm = String(date.getMonth() + 1).padStart(2, '0')
const dd = String(date.getDate()).padStart(2, '0')
const day = DAY_NAMES[date.getDay()]
const hh = String(date.getHours()).padStart(2, '0')
const min = String(date.getMinutes()).padStart(2, '0')
return `${yy}.${mm}.${dd}(${day}) ${hh}:${min}`
}

function getDDay(startDateTime: string): string {
const now = new Date()
const start = new Date(startDateTime)
const diffTime = start.getTime() - now.getTime()
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
if (diffDays === 0) return 'D-Day'
if (diffDays > 0) return `D-${diffDays}`
return ''
}

Copy link
Contributor

@haruyam15 haruyam15 Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용중인 date-fns 라이브러리 활용하면 짧게 할수있어요
(바꾸라는 말은 아님)

예시)

import { ko } from 'date-fns/locale'

function formatMeetingDate(dateStr: string) {
  return format(new Date(dateStr), 'yy.MM.dd(eee) HH:mm', {
    locale: ko,
  })
}

function getDDay(startDateTime: string): string {
  const diffDays = differenceInCalendarDays(
    new Date(startDateTime),
    new Date()
  )

  if (diffDays === 0) return 'D-Day'
  if (diffDays > 0) return `D-${diffDays}`
  return ''
}

import type { MyMeetingFilter, MyMeetingListResponse } from '../meetings.types'
import { myMeetingQueryKeys } from './myMeetingQueryKeys'

const MY_MEETINGS_PAGE_SIZE = 4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상수 모아둔 곳으로 이사가는것이 어떤지요

@mgYang53 mgYang53 linked an issue Feb 19, 2026 that may be closed by this pull request
71 tasks
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/features/meetings/hooks/index.ts (1)

1-15: ⚠️ Potential issue | 🔴 Critical

CI 파이프라인 실패 — export 정렬 오류를 수정해야 합니다.

useMyMeetingsuseMyMeetingTabCounts가 알파벳 순서를 위반하여 ESLint simple-import-sort/exports 규칙 오류가 발생합니다. useMeetingFormusePlaceSearch 사이로 이동해야 합니다.

🔧 수정 제안
 export * from './meetingQueryKeys'
 export * from './myMeetingQueryKeys'
 export * from './useCancelJoinMeeting'
 export * from './useConfirmMeeting'
 export * from './useCreateMeeting'
 export * from './useDeleteMeeting'
 export * from './useJoinMeeting'
 export * from './useMeetingApprovals'
 export * from './useMeetingDetail'
 export * from './useMeetingForm'
+export * from './useMyMeetings'
+export * from './useMyMeetingTabCounts'
 export * from './usePlaceSearch'
-export * from './useMyMeetings'
-export * from './useMyMeetingTabCounts'
 export * from './useRejectMeeting'
 export * from './useUpdateMeeting'

또는 npx eslint --fix src/features/meetings/hooks/index.ts로 자동 수정할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/meetings/hooks/index.ts` around lines 1 - 15, The exports in the
meetings hooks barrel are out-of-order and violate simple-import-sort/exports:
move the exports for useMyMeetings and useMyMeetingTabCounts so they appear
after useMeetingForm and before usePlaceSearch (i.e., reorder the lines
exporting useMyMeetings and useMyMeetingTabCounts in
src/features/meetings/hooks/index.ts), or run npx eslint --fix to auto-fix;
ensure the exported symbol order now follows alphabetical order including
useMeetingForm, useMyMeetings, useMyMeetingTabCounts, usePlaceSearch.
🧹 Nitpick comments (8)
src/pages/Home/components/FavoriteGatheringsSection.tsx (1)

53-53: onFavoriteToggle 래퍼 제거 가능합니다.

mutate 시그니처가 (id: number) => voidonFavoriteToggle과 동일하므로 인라인 화살표 함수가 불필요합니다.

♻️ 리팩터 제안
-              onFavoriteToggle={(id) => toggleFavorite(id)}
+              onFavoriteToggle={toggleFavorite}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Home/components/FavoriteGatheringsSection.tsx` at line 53, The
inline arrow wrapper around onFavoriteToggle is unnecessary because
toggleFavorite already matches the signature (id: number) => void; replace
onFavoriteToggle={(id) => toggleFavorite(id)} with
onFavoriteToggle={toggleFavorite} in FavoriteGatheringsSection.tsx to remove the
redundant wrapper and simplify the prop passing.
src/pages/Home/components/MyMeetingsSection.tsx (2)

22-22: 4 매직 넘버가 MY_MEETINGS_PAGE_SIZE와 분리됨

allItems.slice(0, 4)4useMyMeetings.tsMY_MEETINGS_PAGE_SIZE와 동일한 값이어야 하지만, 해당 상수가 export되지 않아 각자 독립적으로 하드코딩되어 있습니다. MY_MEETINGS_PAGE_SIZE 변경 시 이 값도 수동으로 변경해야 합니다.

Line 60의 [...Array(4).keys()] (스켈레톤 아이템 수)도 동일하게 해당 상수를 참조해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Home/components/MyMeetingsSection.tsx` at line 22, Replace the
hardcoded page-size literals in MyMeetingsSection (the slice call
allItems.slice(0, 4) and the skeleton generator [...Array(4).keys()]) with the
shared constant MY_MEETINGS_PAGE_SIZE: export MY_MEETINGS_PAGE_SIZE from
useMyMeetings.ts (or wherever it’s defined), import it into
MyMeetingsSection.tsx, and use MY_MEETINGS_PAGE_SIZE for both the isCollapsed
slice (displayItems) and the skeleton array length so the page size is
maintained from a single source of truth.

24-27: value as MyMeetingFilter 타입 캐스트 개선 여지

onValueChangevalue: stringMyMeetingFilter로 캐스팅합니다. 현재는 TabsTrigger value가 정확히 'ALL' | 'UPCOMING' | 'DONE'으로 하드코딩되어 있어 런타임 안전하지만, 탭 값과 타입 간 정적 연결이 없습니다.

♻️ 간단한 개선 예시
+const MY_MEETING_FILTERS = ['ALL', 'UPCOMING', 'DONE'] as const satisfies MyMeetingFilter[]
+
 const handleTabChange = (value: string) => {
-  setActiveTab(value as MyMeetingFilter)
+  if ((MY_MEETING_FILTERS as readonly string[]).includes(value)) {
+    setActiveTab(value as MyMeetingFilter)
+  }
   setIsCollapsed(false)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Home/components/MyMeetingsSection.tsx` around lines 24 - 27,
handleTabChange currently casts a string to MyMeetingFilter (setActiveTab(value
as MyMeetingFilter)), which is brittle; change the handler and its caller so the
tab value is typed as MyMeetingFilter instead of string (e.g., declare
handleTabChange(value: MyMeetingFilter) and update the Tabs/TabsTrigger
onValueChange typing) or explicitly validate/map the incoming string to a
MyMeetingFilter before calling setActiveTab. Update references: handleTabChange,
setActiveTab, MyMeetingFilter, TabsTrigger, and the onValueChange usage so the
value is statically typed and no runtime cast is required.
src/features/meetings/hooks/useMyMeetings.ts (1)

7-7: MY_MEETINGS_PAGE_SIZE를 export하거나 공유 상수로 이동 권장

이 상수가 MyMeetingsSection.tsxslice(0, 4)Array(4)와 값을 공유해야 하지만, 현재 모듈 내부에만 있어 두 곳에서 4가 각자 하드코딩됩니다. 페이지 크기 변경 시 둘 다 수동으로 바꿔야 하는 유지보수 리스크가 생깁니다.

♻️ 제안: 상수 export
-const MY_MEETINGS_PAGE_SIZE = 4
+export const MY_MEETINGS_PAGE_SIZE = 4

MyMeetingsSection.tsx (및 skeleton Array 생성)에서 import해서 사용:

+import { MY_MEETINGS_PAGE_SIZE } from '@/features/meetings/hooks/useMyMeetings'
...
-const displayItems = isCollapsed ? allItems.slice(0, 4) : allItems
+const displayItems = isCollapsed ? allItems.slice(0, MY_MEETINGS_PAGE_SIZE) : allItems
...
-{[...Array(4).keys()].map((i) => (
+{[...Array(MY_MEETINGS_PAGE_SIZE).keys()].map((i) => (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/meetings/hooks/useMyMeetings.ts` at line 7, 현재 모듈 내부에 하드코딩된
MY_MEETINGS_PAGE_SIZE(값 4)를 export하거나 공통 상수 모듈로 옮겨 MyMeetingsSection.tsx에서
slice(0, 4)와 Array(4) 대신 해당 상수를 import해 사용하도록 변경하세요; 구체적으로 useMyMeetings.ts의
MY_MEETINGS_PAGE_SIZE를 export하거나 src/constants 등 공통 파일로 이동한 후
MyMeetingsSection.tsx의 slice 및 스켈레톤 생성 부분에서 4를 MY_MEETINGS_PAGE_SIZE로 대체해 일관된 단일
소스에서 페이지 크기를 관리하도록 수정합니다.
src/pages/Home/components/HomeMeetingCard.tsx (1)

128-132: isDone 버튼 클릭 시 아무 동작 없음 (TODO 미구현)

"개인 회고 작성하기" 버튼이 handleActionClick을 호출하지만, 핸들러 내부에서 isDone 분기가 없어 stopPropagation만 실행되고 네비게이션이 발생하지 않습니다. 사용자 입장에서는 반응 없는 버튼처럼 보입니다.

개인 회고 작성 페이지 라우트가 확정되면 연결하는 코드를 생성해 드릴 수 있습니다. 새 이슈로 추적하길 원하시면 알려주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Home/components/HomeMeetingCard.tsx` around lines 128 - 132, The
button labeled "개인 회고 작성하기" calls handleActionClick but currently only
stopPropagation is implemented; update handleActionClick in HomeMeetingCard to
handle the isDone case by performing navigation to the personal retrospective
page (e.g., calling your router/navigation utility such as router.push or
useNavigate) when isDone is true, ensuring you still call
event.stopPropagation() first and pass any needed meeting identifiers
(meetingId/meeting) from the component props so the target route can load the
correct context.
src/features/meetings/meetings.types.ts (1)

204-204: 같은 파일에 유사한 ProgressStatus 타입 두 개 공존

MeetingProgressStatus = 'PRE' | 'ONGOING' | 'POST'(line 204)와 MyMeetingProgressStatus = 'UPCOMING' | 'ONGOING' | 'DONE' | 'UNKNOWN'(line 262)가 'ONGOING'을 공유하면서도 서로 다른 값 집합을 가집니다. 백엔드 컨텍스트(상세 조회 vs 대시보드 목록)가 달라 분리가 필요함을 이해하지만, 타입 이름만으로는 둘의 차이가 명확하지 않아 혼동을 유발할 수 있습니다. 짧은 구분 주석 추가를 권장합니다.

♻️ 구분 주석 추가 예시
+/** 약속 상세 조회(GetMeetingDetailResponse)용 진행 상태 */
 export type MeetingProgressStatus = 'PRE' | 'ONGOING' | 'POST'

 // ...

+/** 메인페이지 대시보드 목록(MyMeetingListItem)용 진행 상태 — 서버 시간 기준 */
 export type MyMeetingProgressStatus = 'UPCOMING' | 'ONGOING' | 'DONE' | 'UNKNOWN'

Also applies to: 262-262

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/meetings/meetings.types.ts` at line 204, Add a short
distinguishing comment above each of the two similar types to clarify their
intended context and value meanings: for MeetingProgressStatus ('PRE' |
'ONGOING' | 'POST') note it represents backend detail-level lifecycle stages
(pre-meeting, in-progress, post-meeting), and for MyMeetingProgressStatus
('UPCOMING' | 'ONGOING' | 'DONE' | 'UNKNOWN') indicate it’s used for
dashboard/listing views (upcoming vs completed vs unknown); reference the type
names MeetingProgressStatus and MyMeetingProgressStatus so future readers
understand why both exist and how their shared 'ONGOING' differs semantically.
src/features/book/components/BookCarousel.tsx (1)

49-52: scroll-smooth CSS 클래스 중복 설정

scrollBybehavior 옵션에 'smooth'를 명시적으로 전달하면 CSS scroll-behavior 값은 무시됩니다. 화살표 버튼 클릭은 모두 scrollBy({ behavior: 'smooth' })를 사용하므로, 컨테이너에 적용된 scroll-smooth Tailwind 클래스는 실질적으로 효과가 없습니다. 이 캐러셀 안에는 앵커 링크도 없어 CSS 클래스가 동작할 다른 경로도 없습니다.

♻️ 제안
-      <div ref={scrollRef} className="flex gap-large overflow-x-auto scrollbar-hide scroll-smooth">
+      <div ref={scrollRef} className="flex gap-large overflow-x-auto scrollbar-hide">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/book/components/BookCarousel.tsx` around lines 49 - 52, The
carousel container in BookCarousel currently includes the Tailwind class
'scroll-smooth' which is redundant because all programmatic scrolling uses
scrollBy({ behavior: 'smooth' }); remove the 'scroll-smooth' class from the div
with ref={scrollRef} (the element rendering children in BookCarousel) to avoid
duplicate/unused CSS; ensure you only alter the className string (leave
cn('group/carousel relative', className) and the scrollRef/children usage
unchanged.
src/pages/Home/components/ReadingBooksSection.tsx (1)

15-15: activeTab 상태가 데이터 패칭에 연결되지 않음 — 탭 활성화 시 필터링 무동작

activeTabuseBooks 호출에 전달되지 않습니다. 현재 pre/post 탭이 disabled라 사용자가 선택할 수 없지만, 향후 탭을 활성화할 때 상태는 바뀌나 데이터가 변하지 않는 버그가 잠재합니다. 지금 당장 수정이 어렵다면 TODO 주석으로 명시해두는 것을 권장합니다.

♻️ 향후 탭 필터 연결 시 구조 참고
-  const { data, isLoading } = useBooks({ status: 'READING' })
+  // TODO: pre/post 탭 활성화 시 filter 파라미터 연결 필요
+  // const bookFilter = activeTab === 'pre' ? 'BEFORE_MEETING' : activeTab === 'post' ? 'AFTER_MEETING' : undefined
+  const { data, isLoading } = useBooks({ status: 'READING' /*, filter: bookFilter */ })

Also applies to: 31-43

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Home/components/ReadingBooksSection.tsx` at line 15, activeTab
state is not passed into the data fetching hook, so changing tabs won't update
results; update the useBooks call to accept activeTab (or its derived filter) so
the hook/query refetches when activeTab changes, e.g., wire activeTab into the
dependency/parameter of useBooks used in ReadingBooksSection (reference
activeTab, setActiveTab, and useBooks), or if you can't implement it now add a
clear TODO comment next to the useState declaration explaining that activeTab
must be forwarded to useBooks to enable tab-based filtering in future.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/Home/components/FavoriteGatheringsSection.tsx`:
- Around line 25-58: The empty-state is flashing during the 0–200ms loading gap
because showSkeleton can be false while the fetched data is still undefined,
causing gatherings to be treated as an empty array and the "즐겨찾기한 모임이 없어요."
message to render; update the render branches in FavoriteGatheringsSection to
explicitly check for undefined data (the variable used to derive gatherings) and
return null when data === undefined (i.e., before rendering either skeleton or
empty state), and apply the same fix to MyMeetingsSection and
ReadingBooksSection so they also skip rendering until the data is received.

In `@src/pages/Home/components/HomeMeetingCard.tsx`:
- Around line 25-33: getDDay currently uses Math.ceil on millisecond diff which
makes same-day future meetings show as D-1; change getDDay to compare calendar
dates instead: create midnight-truncated Date objects for now and start (e.g.,
zero out hours/mins/secs or use start.toDateString()/now.toDateString()),
compute diffDays = (startMidnight.getTime() - nowMidnight.getTime()) /
(1000*60*60*24) and then return 'D-Day' when diffDays === 0, 'D-{diffDays}' when
diffDays > 0, otherwise ''. Keep this logic inside getDDay to preserve behavior
elsewhere.

In `@src/pages/Home/components/HomeSectionHeader.tsx`:
- Line 10: The build fails because TypeScript references React.ReactNode without
importing React; update the components that declare children?: React.ReactNode
(notably HomeSectionHeader and Card) to import the type from React and use it
directly — e.g., add import type { ReactNode } from 'react' and change the prop
type to children?: ReactNode (or alternatively add import * as React from
'react' if you prefer the namespace import) so the compiler finds the React
type.

In `@src/pages/Home/HomePage.tsx`:
- Around line 13-16: The greeting renders an empty name while useUserProfile is
loading; update the HomePage component to check the loading/ready state from
useUserProfile (e.g., isLoading or user being undefined) and render a
placeholder or skeleton instead of using user?.nickname ?? '' in the h1;
specifically change the h1 that currently uses user?.nickname to conditionally
render `안녕하세요, ${user.nickname}님!` only when user exists (or show a Skeleton
component) and otherwise render a neutral placeholder or nothing until
useUserProfile indicates loaded.

---

Outside diff comments:
In `@src/features/meetings/hooks/index.ts`:
- Around line 1-15: The exports in the meetings hooks barrel are out-of-order
and violate simple-import-sort/exports: move the exports for useMyMeetings and
useMyMeetingTabCounts so they appear after useMeetingForm and before
usePlaceSearch (i.e., reorder the lines exporting useMyMeetings and
useMyMeetingTabCounts in src/features/meetings/hooks/index.ts), or run npx
eslint --fix to auto-fix; ensure the exported symbol order now follows
alphabetical order including useMeetingForm, useMyMeetings,
useMyMeetingTabCounts, usePlaceSearch.

---

Nitpick comments:
In `@src/features/book/components/BookCarousel.tsx`:
- Around line 49-52: The carousel container in BookCarousel currently includes
the Tailwind class 'scroll-smooth' which is redundant because all programmatic
scrolling uses scrollBy({ behavior: 'smooth' }); remove the 'scroll-smooth'
class from the div with ref={scrollRef} (the element rendering children in
BookCarousel) to avoid duplicate/unused CSS; ensure you only alter the className
string (leave cn('group/carousel relative', className) and the
scrollRef/children usage unchanged.

In `@src/features/meetings/hooks/useMyMeetings.ts`:
- Line 7: 현재 모듈 내부에 하드코딩된 MY_MEETINGS_PAGE_SIZE(값 4)를 export하거나 공통 상수 모듈로 옮겨
MyMeetingsSection.tsx에서 slice(0, 4)와 Array(4) 대신 해당 상수를 import해 사용하도록 변경하세요;
구체적으로 useMyMeetings.ts의 MY_MEETINGS_PAGE_SIZE를 export하거나 src/constants 등 공통 파일로
이동한 후 MyMeetingsSection.tsx의 slice 및 스켈레톤 생성 부분에서 4를 MY_MEETINGS_PAGE_SIZE로 대체해
일관된 단일 소스에서 페이지 크기를 관리하도록 수정합니다.

In `@src/features/meetings/meetings.types.ts`:
- Line 204: Add a short distinguishing comment above each of the two similar
types to clarify their intended context and value meanings: for
MeetingProgressStatus ('PRE' | 'ONGOING' | 'POST') note it represents backend
detail-level lifecycle stages (pre-meeting, in-progress, post-meeting), and for
MyMeetingProgressStatus ('UPCOMING' | 'ONGOING' | 'DONE' | 'UNKNOWN') indicate
it’s used for dashboard/listing views (upcoming vs completed vs unknown);
reference the type names MeetingProgressStatus and MyMeetingProgressStatus so
future readers understand why both exist and how their shared 'ONGOING' differs
semantically.

In `@src/pages/Home/components/FavoriteGatheringsSection.tsx`:
- Line 53: The inline arrow wrapper around onFavoriteToggle is unnecessary
because toggleFavorite already matches the signature (id: number) => void;
replace onFavoriteToggle={(id) => toggleFavorite(id)} with
onFavoriteToggle={toggleFavorite} in FavoriteGatheringsSection.tsx to remove the
redundant wrapper and simplify the prop passing.

In `@src/pages/Home/components/HomeMeetingCard.tsx`:
- Around line 128-132: The button labeled "개인 회고 작성하기" calls handleActionClick
but currently only stopPropagation is implemented; update handleActionClick in
HomeMeetingCard to handle the isDone case by performing navigation to the
personal retrospective page (e.g., calling your router/navigation utility such
as router.push or useNavigate) when isDone is true, ensuring you still call
event.stopPropagation() first and pass any needed meeting identifiers
(meetingId/meeting) from the component props so the target route can load the
correct context.

In `@src/pages/Home/components/MyMeetingsSection.tsx`:
- Line 22: Replace the hardcoded page-size literals in MyMeetingsSection (the
slice call allItems.slice(0, 4) and the skeleton generator [...Array(4).keys()])
with the shared constant MY_MEETINGS_PAGE_SIZE: export MY_MEETINGS_PAGE_SIZE
from useMyMeetings.ts (or wherever it’s defined), import it into
MyMeetingsSection.tsx, and use MY_MEETINGS_PAGE_SIZE for both the isCollapsed
slice (displayItems) and the skeleton array length so the page size is
maintained from a single source of truth.
- Around line 24-27: handleTabChange currently casts a string to MyMeetingFilter
(setActiveTab(value as MyMeetingFilter)), which is brittle; change the handler
and its caller so the tab value is typed as MyMeetingFilter instead of string
(e.g., declare handleTabChange(value: MyMeetingFilter) and update the
Tabs/TabsTrigger onValueChange typing) or explicitly validate/map the incoming
string to a MyMeetingFilter before calling setActiveTab. Update references:
handleTabChange, setActiveTab, MyMeetingFilter, TabsTrigger, and the
onValueChange usage so the value is statically typed and no runtime cast is
required.

In `@src/pages/Home/components/ReadingBooksSection.tsx`:
- Line 15: activeTab state is not passed into the data fetching hook, so
changing tabs won't update results; update the useBooks call to accept activeTab
(or its derived filter) so the hook/query refetches when activeTab changes,
e.g., wire activeTab into the dependency/parameter of useBooks used in
ReadingBooksSection (reference activeTab, setActiveTab, and useBooks), or if you
can't implement it now add a clear TODO comment next to the useState declaration
explaining that activeTab must be forwarded to useBooks to enable tab-based
filtering in future.

- hooks/index.ts: simple-import-sort 위반 수정 (useMyMeetings, useMyMeetingTabCounts 순서)
- HomeMeetingCard: date-fns로 날짜 포맷 변경, getDDay 캘린더 기준 계산으로 수정
- pagination.ts: MY_MEETINGS 페이지 사이즈 상수 추가
- useMyMeetings: 로컬 상수 제거 후 PAGE_SIZES.MY_MEETINGS 사용
- HomeSectionHeader: ReactNode import type 추가
- BookCarousel: ReactNode import type 추가, 중복 scroll-smooth 제거
- HomePage: 로딩 중 닉네임 빈 문자열 노출 방지
- FavoriteGatheringsSection: data 미로드 시 null 반환, toggleFavorite 불필요한 래퍼 제거
- MyMeetingsSection: 하드코딩된 숫자를 PAGE_SIZES.MY_MEETINGS로 교체
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/pages/Home/components/HomeSectionHeader.tsx (1)

29-37: [선택] 장식용 아이콘에 aria-hidden 추가 권장

ChevronRight는 장식용 아이콘이지만 aria-hidden이 없어 스크린 리더가 아이콘 이름을 함께 읽을 수 있습니다.

♻️ 제안 수정
- <ChevronRight className="size-4" />
+ <ChevronRight className="size-4" aria-hidden="true" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Home/components/HomeSectionHeader.tsx` around lines 29 - 37,
ChevronRight is a decorative icon so add aria-hidden="true" (and if applicable
focusable="false") on the ChevronRight element in the HomeSectionHeader
component to prevent screen readers from announcing it; update the JSX where
ChevronRight is rendered (the ChevronRight component usage inside the Link) to
pass aria-hidden="true" so the icon is ignored by assistive tech.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/Home/components/HomeMeetingCard.tsx`:
- Around line 57-63: In HomeMeetingCard update the incomplete handleActionClick
in the HomeMeetingCard component so clicks on the "isDone" / 개인 회고 작성하기 button
don't block card navigation without doing anything: either (A) make the isDone
branch set the button to disabled (or return early) so the click only calls
e.stopPropagation() and shows the button as disabled, or (B) remove the button
from render until the 회고 page implementation exists; locate the logic in
handleActionClick and the button render near the isDone-related branches (also
applied at the duplicate location around lines where the other isDone handling
exists) and ensure UX indicates the action is unavailable rather than silently
swallowing clicks.

---

Duplicate comments:
In `@src/pages/Home/components/HomeMeetingCard.tsx`:
- Around line 19-24: Update the getDDay implementation to use
differenceInCalendarDays (already done) instead of the prior Math.ceil approach:
in function getDDay(startDateTime: string) call differenceInCalendarDays(new
Date(startDateTime), new Date()), return 'D-Day' when diffDays === 0, return
`D-${diffDays}` when diffDays > 0, and return an empty string otherwise; ensure
getDDay is the single source of truth for D-Day formatting and remove any
remaining Math.ceil-based logic elsewhere.

In `@src/pages/Home/components/HomeSectionHeader.tsx`:
- Around line 1-5: The previous compile error caused by missing React.ReactNode
is resolved by adding the explicit import line "import type { ReactNode } from
'react'" in HomeSectionHeader (and using ReactNode directly), so no code changes
are required—confirm the import of ReactNode remains and keep using ReactNode in
HomeSectionHeader.tsx.

---

Nitpick comments:
In `@src/pages/Home/components/HomeSectionHeader.tsx`:
- Around line 29-37: ChevronRight is a decorative icon so add aria-hidden="true"
(and if applicable focusable="false") on the ChevronRight element in the
HomeSectionHeader component to prevent screen readers from announcing it; update
the JSX where ChevronRight is rendered (the ChevronRight component usage inside
the Link) to pass aria-hidden="true" so the icon is ignored by assistive tech.

- BookCarousel.tsx: Prettier 포맷 수정 (CI format:check 실패 원인)
- HomeMeetingCard: 개인 회고 페이지 미연결 상태에서 isDone 버튼 disabled 처리
  (클릭 시 stopPropagation만 호출되어 카드 네비게이션을 막는 문제 수정)
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/pages/Home/components/HomeMeetingCard.tsx (1)

15-17: new Date() 래핑 제거 가능 (date-fns v4)

date-fns v4에서는 format(), differenceInCalendarDays()가 문자열 argument를 직접 지원하므로:

function formatMeetingDate(dateStr: string) {
- return format(new Date(dateStr), 'yy.MM.dd(eee) HH:mm', { locale: ko })
+ return format(dateStr, 'yy.MM.dd(eee) HH:mm', { locale: ko })
}

function getDDay(startDateTime: string): string {
- const diffDays = differenceInCalendarDays(new Date(startDateTime), new Date())
+ const diffDays = differenceInCalendarDays(startDateTime, new Date())
}

실제 startDateTime'2026-02-01T14:00:00' 형식(Z suffix 없음)이므로 현재 타임존 처리는 의도대로 동작합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Home/components/HomeMeetingCard.tsx` around lines 15 - 17,
formatMeetingDate currently wraps the string in new Date before calling format;
in date-fns v4 you can pass the ISO string directly, so remove the new Date(...)
wrapper in formatMeetingDate and pass dateStr straight to format (keep the
'yy.MM.dd(eee) HH:mm' pattern and { locale: ko } locale). Also, if you have
other date-fns usages like differenceInCalendarDays elsewhere that take
startDateTime (e.g., '2026-02-01T14:00:00'), update them similarly to accept the
string directly so timezone handling remains consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/pages/Home/components/HomeMeetingCard.tsx`:
- Around line 119-124: The Button rendered when isDone currently has disabled
set to true as a temporary guard; when you implement navigation to the personal
review page, remove the disabled prop and add an onClick handler that navigates
to the personal review route (or triggers the open-review flow) from
HomeMeetingCard.tsx — update the Button component instance and the surrounding
TODO comment (remove or replace with TODO: implement navigation) so the Button
no longer blocks interaction once onClick is wired.
- Around line 19-24: The D-Day calculation previously showed "D-1" for same-day
events; confirm the getDDay function uses differenceInCalendarDays to compare
new Date(startDateTime) with new Date() and returns 'D-Day' when diffDays === 0,
'D-{diffDays}' when diffDays > 0, and '' otherwise—no further changes required
beyond this implementation in getDDay.

---

Nitpick comments:
In `@src/pages/Home/components/HomeMeetingCard.tsx`:
- Around line 15-17: formatMeetingDate currently wraps the string in new Date
before calling format; in date-fns v4 you can pass the ISO string directly, so
remove the new Date(...) wrapper in formatMeetingDate and pass dateStr straight
to format (keep the 'yy.MM.dd(eee) HH:mm' pattern and { locale: ko } locale).
Also, if you have other date-fns usages like differenceInCalendarDays elsewhere
that take startDateTime (e.g., '2026-02-01T14:00:00'), update them similarly to
accept the string directly so timezone handling remains consistent.

@mgYang53 mgYang53 merged commit 58197ce into develop Feb 19, 2026
2 checks passed
@mgYang53 mgYang53 deleted the feat/main-page-70 branch February 19, 2026 06:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 메인페이지 구현

2 participants