Skip to content

feat: 독서모임 만들기 및 초대 기능 구현#41

Merged
mgYang53 merged 4 commits intomainfrom
feat/create-gatherings-38
Feb 1, 2026
Merged

feat: 독서모임 만들기 및 초대 기능 구현#41
mgYang53 merged 4 commits intomainfrom
feat/create-gatherings-38

Conversation

@mgYang53
Copy link
Contributor

@mgYang53 mgYang53 commented Feb 1, 2026

🚀 풀 리퀘스트 제안

closes #38

📋 작업 내용

Figma 디자인 기반으로 독서모임 만들기초대 링크를 통한 가입 기능을 구현했습니다.

독서모임 만들기

  • 모임 이름 입력 (필수, 최대 12자)
  • 모임 설명 입력 (선택, 최대 150자)
  • 모임 생성 API 연동
  • 생성 완료 후 초대 링크 복사 기능
  • 완료 버튼 클릭 시 모임 상세 페이지로 이동

초대 링크 가입

  • 초대 코드로 모임 정보 조회 (비로그인 상태에서도 가능)
  • 로그인 상태에 따른 버튼 텍스트 분기
  • 가입 신청 후 상태별 처리 (PENDING, ACTIVE, REJECTED)
  • 이미 가입된 회원 예외 처리 (GA008)

🔧 변경 사항

신규 파일

  • src/features/gatherings/ - gatherings feature 모듈 (types, api, hooks)
  • src/pages/Gatherings/CreateGatheringPage.tsx - 모임 생성 페이지
  • src/pages/Gatherings/InvitePage.tsx - 초대 페이지
  • src/routes/PublicRoute.tsx - 비로그인 전용 라우트 가드
  • src/shared/assets/icon/ - paper-plane, envelope SVG 추가
  • src/features/auth/auth.types.ts - CurrentUser 타입 분리
  • src/shared/lib/image/ - 이미지 파일 검증 유틸리티

수정 파일

  • src/routes/index.tsx - 라우트 추가 및 PublicRoute 적용
  • src/api/interceptors.ts - /invite 경로 401 예외 처리
  • src/shared/layout/MainLayout.tsx - GNB 아래 여백을 페이지별로 설정하도록 변경
  • src/features/auth/ - 타입 분리 및 캐싱 전략 개선

📸 스크린샷 (선택 사항)

스크린샷 2026-02-01 오후 8 59 59 스크린샷 2026-02-01 오후 8 59 18 스크린샷 2026-02-01 오후 8 59 36

📄 기타

  • 클립보드 복사 성공 시 toast 알림은 추후 구현 예정 (TODO 주석)

Summary by CodeRabbit

릴리스 노트

  • 신규 기능

    • 모임 생성 기능 추가 - 모임명과 설명을 입력하여 새로운 모임을 만들고 초대 링크 생성 가능
    • 초대 링크를 통한 모임 참여 기능 추가 - 공유받은 초대 링크로 모임에 참여 가능
  • 버그 수정

    • API 인증 오류 처리 로직 개선 - 401 오류 처리의 안정성 향상
    • 로그아웃 후 캐시 초기화 및 페이지 이동 개선
  • 리팩토링

    • 인증 모듈 구조 개선으로 코드 유지보수성 강화

✏️ Tip: You can customize this high-level summary in your review settings.

- gatherings feature 모듈 생성 (types, api, hooks)
- CreateGatheringPage: 모임 생성 폼 및 완료 화면
- InvitePage: 초대 링크 조회 및 가입 신청
- PublicRoute: 비로그인 사용자 전용 라우트 가드
- interceptors: /invite 경로 401 예외 처리
- MainLayout: GNB 아래 여백을 페이지별로 설정하도록 변경
- CurrentUser 타입을 auth.types.ts로 분리
- useAuth에 authQueryKeys 팩토리 적용
- staleTime/gcTime을 Infinity로 변경 (수동 갱신 방식)
- useLogout 타입 명시 추가
- isAllowedImageFile 함수 추가 (MIME 타입 기반 검증)
@mgYang53 mgYang53 linked an issue Feb 1, 2026 that may be closed by this pull request
17 tasks
@coderabbitai
Copy link

coderabbitai bot commented Feb 1, 2026

Walkthrough

독서모임 생성 기능을 구현하고 인증 타입 구조를 정리했습니다. 모임 생성/참여/초대 API와 React Query 훅, 두 개의 새로운 페이지를 추가하고, 공개 라우트를 통해 비인증 사용자 접근을 제어합니다.

Changes

Cohort / File(s) Summary
Auth 타입 재조직
src/features/auth/auth.api.ts, src/features/auth/auth.types.ts, src/features/auth/index.ts
CurrentUser 타입을 auth.api.ts에서 auth.types.ts로 이동하고 배럴 export 추가. nicknameprofileImageUrl이 null 허용으로 변경됨.
Auth 훅 및 API 강화
src/features/auth/auth.api.ts, src/features/auth/hooks/useAuth.ts, src/features/auth/hooks/useLogout.ts
getKakaoLoginUrl에 환경변수 검증 추가. useAuth의 캐시 전략을 staleTime/gcTime Infinity로 변경하고 authQueryKeys 중앙화. useLogout에 로그인 페이지 네비게이션 추가.
Gatherings 기본 구조
src/features/gatherings/gatherings.types.ts, src/features/gatherings/gatherings.endpoints.ts, src/features/gatherings/gatherings.api.ts
모임 생성/참여/초대 조회를 위한 타입, 엔드포인트, API 함수 3개 추가 (createGathering, joinGathering, getGatheringByInviteCode).
Gatherings React Query 훅
src/features/gatherings/hooks/gatheringQueryKeys.ts, src/features/gatherings/hooks/useCreateGathering.ts, src/features/gatherings/hooks/useGatheringByInviteCode.ts, src/features/gatherings/hooks/useJoinGathering.ts, src/features/gatherings/hooks/index.ts
쿼리 키 중앙화 및 생성/조회/참여 mutation/query 훅 추가. 성공 시 모임 목록 쿼리 무효화.
Gatherings 배럴 export
src/features/gatherings/index.ts
훅, API, 타입 전체 재export으로 모듈 진입점 통일.
모임 생성 페이지
src/pages/Gatherings/CreateGatheringPage.tsx
2단계 폼 (입력 → 완료) UI. 모임명 12자/설명 150자 제한, 완료 후 초대 링크 복사 기능 및 상세 페이지 네비게이션.
모임 초대 페이지
src/pages/Gatherings/InvitePage.tsx
초대 코드로 모임 조회 및 참여 기능. 비로그인 시 로그인 페이지 리다이렉트, 참여 상태(PENDING/ACTIVE/REJECTED)별 처리 로직 포함.
라우팅 및 인증
src/routes/PublicRoute.tsx, src/routes/index.tsx, src/pages/Gatherings/index.ts
PublicRoute 컴포넌트로 비인증 사용자만 접근 가능한 페이지 보호. /gatherings/create (private), /invite/:invitationCode (public) 라우트 추가.
레이아웃 및 유틸
src/shared/layout/MainLayout.tsx, src/shared/lib/image/imgUtils.ts, src/shared/lib/image/index.ts
MainLayout에서 top padding 제거 (페이지별 개별 설정). 허용된 이미지 MIME 타입 검증 유틸 추가.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

feat, refactor

Suggested reviewers

  • haruyam15
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 주요 변경사항인 독서모임 생성 및 초대 기능 구현을 명확하게 설명합니다.
Linked Issues check ✅ Passed PR이 #38의 모든 주요 코딩 요구사항을 충족합니다: gatherings feature 모듈 구성, CreateGatheringPage 및 InvitePage 구현, 라우트 설정 완료.
Out of Scope Changes check ✅ Passed auth 타입 분리 및 캐싱 전략 개선은 #38 범위 외이지만, 인증 흐름 개선으로 InvitePage의 비로그인 접근 지원과 직결되어 필요한 변경입니다.

✏️ 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/create-gatherings-38

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 1, 2026
@mgYang53 mgYang53 added the feat 새로운 기능 추가 label Feb 1, 2026
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: 2

🤖 Fix all issues with AI agents
In `@src/features/auth/hooks/useAuth.ts`:
- Around line 17-24: In useAuth's useQuery call (queryFn: fetchCurrentUser,
queryKey: authQueryKeys.me()) you set staleTime: Infinity while also using
refetchOnWindowFocus: true, which never triggers because data is always fresh;
fix by either removing refetchOnWindowFocus if you only need manual refresh, or
change refetchOnWindowFocus to "always" to force refetch on window focus, or set
staleTime to a finite value so refetchOnWindowFocus: true can take effect—apply
the chosen change inside the useQuery options in useAuth.

In `@src/pages/Gatherings/InvitePage.tsx`:
- Around line 42-45: InvitePage passes the original path via state: { from:
location.pathname } but no consumer persists/restores that value, so LoginPage
and the OAuth flow lose it; update LoginPage (where useLocation() is used) to
read useLocation().state?.from and persist it (e.g.,
localStorage.setItem('postLoginRedirect', from)) before initiating the Kakao
OAuth redirect, and update the OAuth callback handler/hook to read that
persisted 'postLoginRedirect' value and call navigate(persistedFrom ||
ROUTES.DEFAULT) after successful login, then clear the stored key; reference
InvitePage, LoginPage, useLocation, and the OAuth callback handler to locate
where to add the read/persist/restore logic.
🧹 Nitpick comments (4)
src/api/interceptors.ts (1)

107-116: 빈 if 블록은 early return으로 개선 가능

isInvitePage 조건의 빈 if 블록은 의도 파악이 어렵고 가독성을 해칩니다. early return 패턴으로 흐름을 명확히 하세요.

♻️ 개선 제안
       if (error.response?.status === 401) {
         const currentPath = window.location.pathname
         const isAlreadyOnLogin = currentPath === ROUTES.LOGIN
         const isInvitePage = currentPath.startsWith(ROUTES.INVITE_BASE)

-        // 초대 페이지에서는 401을 정상 흐름으로 처리 (비로그인 사용자도 접근 가능)
-        if (isInvitePage) {
-          // 캐시 삭제나 리다이렉트 없이 에러만 반환
-        } else if (!isAlreadyOnLogin) {
+        // 초대 페이지 또는 로그인 페이지에서는 리다이렉트하지 않음
+        if (isInvitePage || isAlreadyOnLogin) {
+          return Promise.reject(
+            new ApiError(code ?? 'UNKNOWN', message ?? error.message, error.response?.status ?? 500),
+          )
+        }
+
-          // React Query 캐시 전체 삭제 (인증 정보 포함)
-          // 세션이 만료되었으므로 모든 캐시된 데이터는 더 이상 유효하지 않음
-          queryClient.clear()
-          alert('로그인이 필요합니다. 로그인 페이지로 이동합니다.')
-          window.location.href = ROUTES.LOGIN
-        }
+        // React Query 캐시 전체 삭제 (인증 정보 포함)
+        queryClient.clear()
+        alert('로그인이 필요합니다. 로그인 페이지로 이동합니다.')
+        window.location.href = ROUTES.LOGIN
       }

⚠️ 위 diff 적용 시 code, message 변수가 401 블록 내에서 선언되어야 하므로, 실제로는 해당 변수 추출 로직(Line 120)을 401 블록 앞으로 이동하거나 early return 시 별도로 에러를 구성해야 합니다.

src/routes/PublicRoute.tsx (1)

16-19: 로딩 중에도 공용 화면이 노출됩니다
문제: 인증 확인 중(isLoading)에도 Outlet을 렌더링합니다.
영향: 로그인된 사용자가 잠깐 로그인/초대 화면을 보거나 중복 액션을 할 수 있어요.
대안: 로딩 동안은 로더를 보여주고, 완료 후에만 Outlet을 렌더링하세요.

🔧 개선안
 export function PublicRoute() {
   const { data: user, isLoading, isError } = useAuth()

-  // 로딩 중 또는 에러(401) → 로그인 페이지 표시
-  if (isLoading || isError || !user) {
-    return <Outlet />
-  }
+  if (isLoading) {
+    return (
+      <div className="flex min-h-screen items-center justify-center">
+        <p>Loading...</p>
+      </div>
+    )
+  }
+
+  // 에러(401) 또는 비로그인 → 공개 페이지 표시
+  if (isError || !user) {
+    return <Outlet />
+  }

As per coding guidelines: React 18 기준의 렌더링 흐름과 hooks 사용이 적절한지 확인해줘.

src/pages/Gatherings/CreateGatheringPage.tsx (2)

56-66: 클립보드 복사 실패 시 사용자 피드백 누락

navigator.clipboard.writeText 실패 시 콘솔에만 로그하고 사용자에게는 알림이 없습니다.

→ 복사 성공/실패 여부를 사용자가 알 수 없음
openError를 활용하거나 TODO의 toast로 처리 권장

💡 에러 피드백 추가 제안
   const handleCopyLink = async () => {
     const fullUrl = getFullInviteUrl()
     if (!fullUrl) return

     try {
       await navigator.clipboard.writeText(fullUrl)
       // TODO: toast 알림 표시
     } catch (error) {
       console.error('클립보드 복사 실패:', error)
+      openError('복사 실패', '클립보드 복사에 실패했습니다. 직접 링크를 복사해주세요.')
     }
   }

137-141: 성공 화면에서 trim 전 이름 표시

Line 139에서 name을 그대로 표시하지만, API에는 name.trim()이 전송됩니다.

→ 사용자가 앞뒤 공백을 입력하면 UI와 실제 저장된 값이 다를 수 있음
createdData.gatheringName 사용 권장

💡 저장된 값 사용 제안
-            <p className="typo-subtitle1 text-primary-300">'{name}'</p>
+            <p className="typo-subtitle1 text-primary-300">'{createdData?.gatheringName ?? name}'</p>

- useAuth: refetchOnWindowFocus를 'always'로 변경 (staleTime: Infinity와 호환)
- interceptors: 빈 if 블록을 명확한 조건문으로 개선
- CreateGatheringPage: 성공 화면에서 서버 응답값 사용
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.

빠르시네여

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