Skip to content

[feat] 상대방 프로필 조회 API 구현#149

Open
shinwokkang wants to merge 32 commits intomainfrom
feat/148-other-profile-api
Open

[feat] 상대방 프로필 조회 API 구현#149
shinwokkang wants to merge 32 commits intomainfrom
feat/148-other-profile-api

Conversation

@shinwokkang
Copy link
Contributor

@shinwokkang shinwokkang commented Feb 27, 2026

📌 개요 (Summary)

  • 변경 사항에 대한 간략한 요약을 적어주세요.
  • 관련 이슈가 있다면 링크를 걸어주세요 (예: [fix] cicd / build 문제 #123).

🛠️ 변경 사항 (Changes)

  • 새로운 기능 추가
  • 버그 수정
  • 코드 리팩토링
  • 문서 업데이트
  • 기타 (설명: )

📸 스크린샷 (Screenshots)

(UI 변경 사항이 있다면 첨부해주세요)

✅ 체크리스트 (Checklist)

  • 빌드가 성공적으로 수행되었나요? (pnpm build)
  • 린트 에러가 없나요? (pnpm lint)
  • 불필요한 콘솔 로그나 주석을 제거했나요?

Summary by CodeRabbit

  • New Features

    • Follow/unfollow authors and members with live subscription state
    • Like/unlike stories with immediate visual feedback
    • Club discovery: search, filters, infinite scroll, recommendations, and join/apply flow
    • Multi-step club creation wizard with image upload and name-availability check
    • View other users’ profiles and browse their stories
  • Improvements

    • Consistent subscribe button labels (e.g., "구독 중") and improved profile/story interactions
    • New utility to hide scrollbars for cleaner lists

김기현 and others added 30 commits February 25, 2026 15:27
[fix/style] build 오류 해결 , 내모임이 많을 떄 스크롤 안생기게끔
@shinwokkang shinwokkang self-assigned this Feb 27, 2026
@shinwokkang shinwokkang added the ✨ feat 새로운 기능 추가 label Feb 27, 2026
@shinwokkang shinwokkang linked an issue Feb 27, 2026 that may be closed by this pull request
4 tasks
@vercel
Copy link

vercel bot commented Feb 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
check-mo Ready Ready Preview, Comment Feb 27, 2026 1:56pm
checkmo Ready Ready Preview, Comment Feb 27, 2026 1:56pm

@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

📝 Walkthrough

Walkthrough

Introduces like/follow mutations with optimistic updates and wiring across story cards and pages; adds club creation/search flows with image upload and join mutation; new member/profile endpoints and queries; adds no-scrollbar CSS and several new API endpoint/type definitions. (49 words)

Changes

Cohort / File(s) Summary
Configuration & Styling
next.config.ts, src/app/globals.css
Allowed S3 image pathname /** added; added cross‑browser .no-scrollbar utility.
Story UI Components
src/components/base-ui/BookStory/bookstory_card.tsx, src/components/base-ui/BookStory/bookstory_card_large.tsx, src/components/base-ui/BookStory/bookstory_detail.tsx
Added interactive props (id, likedByMe, onLikeClick, isFollowing, onProfileClick), dynamic heart icon/styling, and profile click handling.
Pages — Stories / Home / Book
src/app/(main)/page.tsx, src/app/(main)/stories/page.tsx, src/app/(main)/stories/[id]/page.tsx, src/app/(main)/books/[id]/page.tsx
Wired useToggleStoryLikeMutation and useToggleFollowMutation; propagate like/follow handlers and state to story components and adjust subscribe text/handlers.
Profile & Other User
src/app/(main)/profile/[nickname]/page.tsx, src/components/base-ui/Profile/*, src/components/base-ui/Profile/OtherUser/OtherUserProfileTabs.tsx, src/components/base-ui/Profile/BookStoryList.tsx
Added nickname prop to tabs, integrated useOtherProfileQuery and follow mutation, implemented other-member story infinite list and follow toggle wiring.
Club Creation & Search
src/app/groups/create/page.tsx, src/app/groups/page.tsx, src/app/groups/groupSearchDummy.ts, src/components/base-ui/Group-Search/*
Reworked club create into multi-step with image upload, name check, create mutation; replaced dummy data with real queries, infinite club search, join mutation, and mapping utilities.
Subscribe/List Components
src/components/base-ui/home/list_subscribe_element.tsx, src/components/base-ui/home/list_subscribe_large.tsx, src/components/base-ui/home/home_bookclub.tsx
Added isFollowing prop and conditional styling/onSubscribeClick signatures; integrated .no-scrollbar where needed.
Hooks — Queries & Mutations
src/hooks/mutations/*, src/hooks/queries/*
Added useToggleStoryLikeMutation, useToggleFollowMutation, useClubJoinMutation, useUploadClubImageMutation, useCreateClubMutation, useClubNameCheckQuery, useMyClubsQuery, useClubRecommendationsQuery, useInfiniteClubSearchQuery, useOtherMemberInfiniteStoriesQuery, useOtherProfileQuery (optimistic updates, throttling, invalidations).
Services & Endpoints
src/services/*, src/lib/api/endpoints/*, src/lib/api/types.ts
Added/updated clubService, imageService, memberService, storyService; new endpoint modules (Clubs, Image), member/story endpoint helpers, and ApiResponse<T> type.
Types & Mappers
src/types/groups/*, src/types/member.ts, src/components/base-ui/Group-Search/search_clublist/search_clublist_item.tsx, src/utils/groupMapper.ts
Added comprehensive club-related DTO/types, OtherProfileResponse, ClubSummary UI type, mapping utilities, and moved/updated import paths.
MyPage & Misc UI
src/components/base-ui/MyPage/MyBookStoryList.tsx, src/components/base-ui/Group-Search/search_club_apply_modal.tsx, src/components/base-ui/Group-Search/search_mybookclub.tsx
Enabled navigation and like-follow wiring in lists; adjusted apply modal onSubmit signature to accept clubId; added isLoading prop handling for MyBookclub.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Client UI
    participant Hook as Mutation Hook
    participant Service as Service (story/member)
    participant API as Backend API
    participant QC as QueryClient / Cache

    UI->>Hook: user clicks like / follow
    Hook->>Service: call toggle endpoint (toggleLike/toggleFollow)
    Service->>API: POST /like or POST /follow
    API-->>Service: 200 OK (result)
    Service-->>Hook: returns result
    Hook->>QC: optimistic update onMutate (update lists/detail)
    QC-->>UI: UI reflects optimistic state
    Hook->>QC: onSettled -> invalidate relevant queries
    QC-->>UI: UI syncs with server state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 I hopped through code with eager paws,
Added hearts and follows, fixed some laws.
Clubs bloom with uploads, names are checked,
Optimistic hops keep UI decked.
Hooray — the rabbit dances, bugs kept at bay! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.26% 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 The title clearly describes the main feature: implementing an API for querying other users' profiles, which is the primary focus of the PR changes.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/148-other-profile-api

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.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 다른 사용자의 프로필을 조회하고 상호작용하는 기능을 도입하며, 모임 생성 및 검색 기능을 실제 API와 연동하여 사용자 경험을 크게 개선합니다. 좋아요 및 팔로우 기능에 낙관적 업데이트를 적용하여 UI 반응성을 높였고, 전반적인 데이터 흐름을 실제 백엔드 서비스와 통합했습니다.

Highlights

  • 상대방 프로필 조회 API 구현: 다른 사용자의 프로필 정보를 조회하고, 해당 사용자의 책 이야기를 무한 스크롤로 불러오는 기능이 추가되었습니다. 프로필 페이지에서 팔로우/언팔로우 및 좋아요 기능이 연동됩니다.
  • 모임 생성 및 검색 기능 개선: 모임 생성 시 이름 중복 확인, 프로필 이미지 업로드, 모임 생성 API 연동 등 실제 데이터 기반의 모임 생성 플로우가 구현되었습니다. 또한, 모임 검색 페이지에서 더미 데이터 대신 실제 API를 통해 모임 추천 및 검색 결과를 제공하며, 무한 스크롤이 적용되었습니다.
  • 좋아요 및 팔로우 기능 최적화: 책 이야기 좋아요 및 사용자 팔로우/언팔로우 기능에 낙관적 업데이트(Optimistic Update)와 스로틀링(Throttling)이 적용되어 사용자 경험이 향상되었습니다. 이는 UI 반응성을 높이고 불필요한 API 호출을 줄입니다.
  • UI 컴포넌트 기능 확장: BookStoryCard, BookStoryCardLarge, BookstoryDetail, ListSubscribeElement, ListSubscribeLarge 등 여러 UI 컴포넌트에 id, likedByMe, isFollowing, onLikeClick, onSubscribeClick, onProfileClick 등의 prop이 추가되어 좋아요 및 팔로우 상태를 표시하고 관련 액션을 처리할 수 있게 되었습니다.
  • API 엔드포인트 및 서비스 계층 추가: 클럽 생성, 이미지 업로드, 클럽 검색, 클럽 가입, 다른 멤버 프로필 조회, 팔로우/언팔로우, 책 이야기 좋아요 등 다양한 API 엔드포인트와 이를 처리하는 서비스 및 쿼리/뮤테이션 훅이 새로 추가되거나 업데이트되었습니다.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • next.config.ts
    • S3 이미지 호스트 설정에 모든 경로를 허용하도록 pathname: "/**"를 추가했습니다.
  • src/app/(main)/books/[id]/page.tsx
    • BookStoryCardLarge 컴포넌트에 id prop을 추가했습니다.
  • src/app/(main)/page.tsx
    • 좋아요 및 팔로우 뮤테이션 훅을 임포트하고 초기화했습니다.
    • BookStoryCardLarge 및 BookStoryCard 컴포넌트에 id, likedByMe, isFollowing, onSubscribeClick, onLikeClick prop을 전달하도록 업데이트했습니다.
    • ListSubscribeLarge 컴포넌트에 onSubscribeClick prop을 전달했습니다.
  • src/app/(main)/profile/[nickname]/page.tsx
    • OtherUserProfileTabs 컴포넌트에 nickname prop을 전달했습니다.
  • src/app/(main)/stories/[id]/page.tsx
    • 좋아요 및 팔로우 뮤테이션 훅을 임포트하고 초기화했습니다.
    • BookstoryDetail 컴포넌트에 likedByMe, onLikeClick, subscribeText, isFollowing, onSubscribeClick prop을 전달했습니다.
  • src/app/(main)/stories/page.tsx
    • 좋아요 및 팔로우 뮤테이션 훅을 임포트하고 초기화했습니다.
    • BookStoryCardLarge 및 BookStoryCard 컴포넌트에 id, likedByMe, isFollowing, onSubscribeClick, onLikeClick, onProfileClick prop을 전달했습니다.
    • ListSubscribeLarge 컴포넌트에 onSubscribeClick prop을 전달했습니다.
  • src/app/globals.css
    • 스크롤바를 숨기는 no-scrollbar 유틸리티 클래스를 추가했습니다.
  • src/app/groups/create/page.tsx
    • 클럽 이름 중복 확인, 이미지 업로드, 클럽 생성 관련 API 훅을 임포트하고 연동했습니다.
    • 모임 공개 여부 상태 관리 변수를 visibility에서 open (boolean)으로 변경했습니다.
    • 이미지 업로드 및 모임 생성 시 로딩 상태를 반영하도록 canNext 로직을 업데이트했습니다.
    • 버튼 스타일 및 이미지 처리 로직을 개선했습니다.
  • src/app/groups/groupSearchDummy.ts
    • 더미 데이터 파일을 제거했습니다.
  • src/app/groups/page.tsx
    • 더미 데이터 사용을 제거하고 실제 API 훅(useClubRecommendationsQuery, useInfiniteClubSearchQuery, useMyClubsQuery, useClubJoinMutation)을 연동했습니다.
    • 클럽 검색 및 추천 결과를 처리하기 위한 매핑 함수들을 추가했습니다.
    • 검색 결과에 무한 스크롤 기능을 구현했습니다.
    • Mybookclub 및 SearchClubListItem 컴포넌트가 실제 데이터를 사용하도록 업데이트했습니다.
  • src/components/base-ui/BookStory/bookstory_card.tsx
    • id, likedByMe, onLikeClick, isFollowing, onProfileClick prop을 추가했습니다.
    • 좋아요 아이콘 및 구독 버튼 스타일/로직을 업데이트하고 클릭 핸들러를 추가했습니다.
  • src/components/base-ui/BookStory/bookstory_card_large.tsx
    • id, likedByMe, onLikeClick, isFollowing, onProfileClick prop을 추가했습니다.
    • 좋아요 아이콘 및 구독 버튼 스타일/로직을 업데이트하고 클릭 핸들러를 추가했습니다.
  • src/components/base-ui/BookStory/bookstory_detail.tsx
    • isFollowing, id, likedByMe, onLikeClick prop을 추가했습니다.
    • 구독 버튼 및 좋아요 아이콘 스타일/로직을 업데이트하고 클릭 핸들러를 추가했습니다.
  • src/components/base-ui/Group-Search/search_club_apply_modal.tsx
    • ClubSummary 타입 임포트 경로를 수정했습니다.
    • 모달의 둥근 모서리 스타일을 rounded-[8px]에서 rounded-[12px]로 변경했습니다.
    • 가입 신청 버튼의 onSubmit 타입 및 스타일을 업데이트했습니다.
  • src/components/base-ui/Group-Search/search_clublist/search_clublist_item.tsx
    • ClubSummary 타입 정의를 이 파일로 이동했습니다.
    • 이미지 소스를 안전하게 처리하는 safeImageSrc 함수를 추가했습니다.
    • 모바일 레이아웃 및 버튼 스타일을 개선했습니다.
  • src/components/base-ui/Group-Search/search_mybookclub.tsx
    • isLoading prop을 추가하고 로딩 상태에 따라 로고를 표시하도록 했습니다.
    • 그룹 항목 클릭 시 라우팅 기능을 추가했습니다.
    • 오버플로우 스크롤 영역에 no-scrollbar 클래스를 적용했습니다.
  • src/components/base-ui/MyPage/MyBookStoryList.tsx
    • 라우터 및 좋아요 뮤테이션 훅을 임포트하고 연동했습니다.
    • BookStoryCard 컴포넌트에 id, likedByMe, onLikeClick, onClick prop을 전달했습니다.
  • src/components/base-ui/Profile/BookStoryList.tsx
    • 더미 스토리를 제거하고 useOtherMemberInfiniteStoriesQuery를 사용하여 다른 멤버의 책 이야기를 불러오도록 변경했습니다.
    • 무한 스크롤, 로딩 및 에러 상태 처리를 구현했습니다.
    • BookStoryCard 컴포넌트에 필요한 prop들을 전달하도록 업데이트했습니다.
  • src/components/base-ui/Profile/OtherUser/OtherUserProfileTabs.tsx
    • nickname prop을 추가하고 BookStoryList 컴포넌트에 전달했습니다.
  • src/components/base-ui/Profile/OtherUser/ProfileUserInfo.tsx
    • useOtherProfileQueryuseToggleFollowMutation 훅을 연동하여 실제 프로필 데이터를 표시하고 팔로우 기능을 구현했습니다.
    • 팔로우 상태에 따른 ActionButtonvariantlabel을 동적으로 변경했습니다.
    • 로딩 및 에러 상태 처리를 추가했습니다.
  • src/components/base-ui/home/home_bookclub.tsx
    • 오버플로우 스크롤 영역에 no-scrollbar 클래스를 적용했습니다.
  • src/components/base-ui/home/list_subscribe_element.tsx
    • isFollowing prop을 추가하고 구독 버튼의 스타일을 팔로우 상태에 따라 변경했습니다.
  • src/components/base-ui/home/list_subscribe_large.tsx
    • isFollowing prop을 추가하고 구독 버튼의 스타일 및 텍스트를 팔로우 상태에 따라 변경했습니다.
  • src/hooks/mutations/useCreateClubMutation.ts
    • 클럽 이미지 업로드(useUploadClubImageMutation) 및 클럽 생성(useCreateClubMutation)을 위한 새로운 뮤테이션 훅을 추가했습니다.
  • src/hooks/mutations/useMemberMutations.ts
    • 멤버 팔로우/언팔로우(useToggleFollowMutation) 기능을 추가하고 낙관적 업데이트 및 스로틀링 로직을 구현했습니다.
    • 추천 멤버, 무한 스크롤 스토리, 스토리 목록, 다른 사용자 프로필의 팔로우 상태를 업데이트하는 헬퍼 함수들을 추가했습니다.
  • src/hooks/mutations/useSearchClubMutations.ts
    • 클럽 가입(useClubJoinMutation)을 위한 새로운 뮤테이션 훅을 추가했습니다.
  • src/hooks/mutations/useStoryMutations.ts
    • 책 이야기 좋아요 토글(useToggleStoryLikeMutation) 기능을 추가하고 낙관적 업데이트 및 스로틀링 로직을 구현했습니다.
    • 댓글 생성 및 삭제 뮤테이션에 댓글 수 낙관적 업데이트 로직을 추가했습니다.
    • 스토리 목록, 무한 스크롤 스토리 목록, 스토리 상세 정보의 좋아요 및 댓글 수를 업데이트하는 헬퍼 함수들을 추가했습니다.
  • src/hooks/queries/useCreateClubQueries.ts
    • 클럽 이름 중복 확인(useClubNameCheckQuery)을 위한 새로운 쿼리 훅을 추가했습니다.
  • src/hooks/queries/useMemberQueries.ts
    • 다른 사용자 프로필 조회(useOtherProfileQuery)를 위한 쿼리 훅과 관련 쿼리 키를 추가했습니다.
  • src/hooks/queries/useSearchClubQueries.ts
    • 내 모임 조회(useMyClubsQuery), 모임 추천(useClubRecommendationsQuery), 모임 검색(useInfiniteClubSearchQuery)을 위한 새로운 쿼리 훅과 관련 쿼리 키를 추가했습니다.
  • src/hooks/queries/useStoryQueries.ts
    • 다른 멤버의 책 이야기 조회(useOtherMemberInfiniteStoriesQuery)를 위한 쿼리 훅과 관련 쿼리 키를 추가했습니다.
  • src/lib/api/endpoints/Clubs.ts
    • 클럽 관련 API 엔드포인트들을 정의한 파일을 추가했습니다.
  • src/lib/api/endpoints/Image.ts
    • 이미지 업로드 관련 API 엔드포인트들을 정의한 파일을 추가했습니다.
  • src/lib/api/endpoints/bookstory.ts
    • 다른 멤버의 책 이야기 조회 및 책 이야기 좋아요 관련 엔드포인트를 추가했습니다.
  • src/lib/api/endpoints/index.ts
    • 새로 추가된 클럽 및 이미지 엔드포인트들을 익스포트했습니다.
  • src/lib/api/endpoints/member.ts
    • 다른 사용자 프로필 조회 및 팔로우/언팔로우 관련 엔드포인트를 추가했습니다.
  • src/lib/api/types.ts
    • API 응답의 공통 타입인 ApiResponse를 정의한 파일을 추가했습니다.
  • src/services/clubService.ts
    • 클럽 이름 중복 확인, 클럽 생성, 내 모임 조회, 모임 추천, 모임 검색, 클럽 가입 기능을 위한 서비스 로직을 구현했습니다.
  • src/services/imageService.ts
    • 이미지 업로드 URL 요청 및 S3에 이미지 업로드 기능을 위한 서비스 로직을 구현했습니다.
  • src/services/memberService.ts
    • 다른 사용자 프로필 조회, 멤버 팔로우, 멤버 언팔로우 기능을 위한 서비스 로직을 구현했습니다.
  • src/services/storyService.ts
    • 다른 멤버의 책 이야기 조회 및 책 이야기 좋아요 토글 기능을 위한 서비스 로직을 구현했습니다.
  • src/types/groups/clubCreate.ts
    • 클럽 생성 요청 및 관련 카테고리 매핑을 위한 타입 정의 파일을 추가했습니다.
  • src/types/groups/clubsearch.ts
    • 모임 검색, 추천, 가입 요청 및 응답을 위한 타입 정의 파일을 추가했습니다.
  • src/types/member.ts
    • 추천 멤버 인터페이스에 isFollowing 속성을 추가하고, 다른 사용자 프로필 응답(OtherProfileResponse) 인터페이스를 추가했습니다.
  • src/utils/groupMapper.ts
    • Category 및 ParticipantType 임포트 경로를 수정했습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

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: 18

Caution

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

⚠️ Outside diff range comments (2)
src/components/base-ui/Group-Search/search_mybookclub.tsx (1)

75-85: 🛠️ Refactor suggestion | 🟠 Major

Use <Link> instead of a clickable <div> for navigation.

The current implementation uses a <div> with an onClick handler for navigation, which has accessibility issues:

  • Not keyboard-accessible (no tabIndex, onKeyDown, or role)
  • Loses native link behaviors (right-click to open in new tab, etc.)

For navigation, prefer Next.js <Link> component.

♻️ Proposed fix using Link

First, add the import:

 import { useRouter } from "next/navigation";
+import Link from "next/link";

Then replace the clickable div:

-            {displayGroups.map((group) => (
-              <div
-                key={group.id}
-                onClick={() => router.push(`/groups/${group.id}`)}
-                className="flex w-full h-[36px] t:h-[52px] py-3 px-4 items-center rounded-lg bg-white hover:brightness-98 hover:-translate-y-[1px] cursor-pointer"
-              >
-                <span className="text-Gray-7 body_1_2 t:subhead_4_1">
-                  {group.name}
-                </span>
-              </div>
-            ))}
+            {displayGroups.map((group) => (
+              <Link
+                key={group.id}
+                href={`/groups/${group.id}`}
+                className="flex w-full h-[36px] t:h-[52px] py-3 px-4 items-center rounded-lg bg-white hover:brightness-98 hover:-translate-y-[1px] cursor-pointer"
+              >
+                <span className="text-Gray-7 body_1_2 t:subhead_4_1">
+                  {group.name}
+                </span>
+              </Link>
+            ))}

Note: If you switch to <Link>, the useRouter import on line 5 and the router variable on line 27 can be removed if they're no longer used elsewhere.

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

In `@src/components/base-ui/Group-Search/search_mybookclub.tsx` around lines 75 -
85, Replace the clickable div used in displayGroups.map (the element with
key={group.id} and onClick={() => router.push(`/groups/${group.id}`)}) with
Next.js Link navigation: import Link from 'next/link', use <Link
href={`/groups/${group.id}`} ...> to preserve the existing className, children
(the span showing group.name) and key, and remove the useRouter import and
router variable if they are no longer used elsewhere; this restores native link
behavior (keyboard, context menu, new-tab) and fixes the accessibility issues
from using a div with onClick.
src/app/(main)/page.tsx (1)

106-124: ⚠️ Potential issue | 🟠 Major

Home story cards expose like state but don’t wire the like action.

likedByMe is passed, but onLikeClick is missing in all three card mappings, so like taps become no-ops.

🛠️ Suggested fix
               <BookStoryCardLarge
                 key={story.bookStoryId}
                 id={story.bookStoryId}
@@
                 likedByMe={story.likedByMe}
+                onLikeClick={() => toggleLike(story.bookStoryId)}
                 coverImgSrc={story.bookInfo.imgUrl}
@@
               <BookStoryCard
                 key={story.bookStoryId}
                 id={story.bookStoryId}
@@
                 likedByMe={story.likedByMe}
+                onLikeClick={() => toggleLike(story.bookStoryId)}
                 coverImgSrc={story.bookInfo.imgUrl}
@@
                 <BookStoryCard
                   key={story.bookStoryId}
                   id={story.bookStoryId}
@@
                   likedByMe={story.likedByMe}
+                  onLikeClick={() => toggleLike(story.bookStoryId)}
                   coverImgSrc={story.bookInfo.imgUrl}

Also applies to: 162-180, 220-238

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

In `@src/app/`(main)/page.tsx around lines 106 - 124, The BookStoryCardLarge
instances are rendering the current likedByMe state but not wiring the like
action, so clicks do nothing; add an onLikeClick prop to each BookStoryCardLarge
that invokes the existing like handler (e.g., call toggleLike or the component's
like handler used elsewhere) with the story identity and current liked state
(for example pass story.bookStoryId and story.likedByMe or an object the handler
expects). Update the mappings that create BookStoryCardLarge (the shown block
and the other two similar mappings) to pass onLikeClick={() => toggleLike({
bookStoryId: story.bookStoryId, liked: story.likedByMe })} (or the equivalent
signature your toggleLike accepts) so likes are dispatched when the user taps
the like button.
🧹 Nitpick comments (3)
src/lib/api/endpoints/index.ts (1)

6-7: Inconsistent file naming convention.

The new exports "./Clubs" and "./Image" use PascalCase, while existing modules (base, auth, bookstory, member, book) use lowercase. Consider renaming for consistency.

♻️ Suggested consistency fix
-export * from "./Clubs";
-export * from "./Image";
+export * from "./clubs";
+export * from "./image";

This would require renaming the actual files Clubs.tsclubs.ts and Image.tsimage.ts.

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

In `@src/lib/api/endpoints/index.ts` around lines 6 - 7, The exports in index.ts
use PascalCase ("./Clubs", "./Image") which is inconsistent with the existing
lowercase module names; rename the module files Clubs.ts → clubs.ts and Image.ts
→ image.ts and update the exports in src/lib/api/endpoints/index.ts to export *
from "./clubs" and export * from "./image" so the filenames and import/exports
follow the lowercase convention used by base, auth, bookstory, member, and book.
src/lib/api/endpoints/Image.ts (1)

1-1: Unify API_BASE_URL source across endpoint modules.

This file imports from ../endpoints while sibling endpoint files use ./base. Keeping one source avoids accidental base URL drift between modules.

♻️ Suggested alignment
-import { API_BASE_URL } from "../endpoints";
+import { API_BASE_URL } from "./base";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/api/endpoints/Image.ts` at line 1, The import of API_BASE_URL in
Image.ts currently comes from "../endpoints" which diverges from sibling
modules; update the import to use the common source (import API_BASE_URL from
"./base") so Image.ts uses the same base URL provider as other endpoint
modules—locate the import statement at the top of Image.ts and replace the
"../endpoints" module specifier with "./base" (keep the API_BASE_URL identifier
and any existing named/default import shape intact).
src/types/groups/clubsearch.ts (1)

1-1: Use a type-only import for ApiResponse.

Line 1 uses ApiResponse only in type aliases, so import type is safer and cleaner for TS module emit modes.

Proposed fix
-import { ApiResponse } from "@/lib/api/types";
+import type { ApiResponse } from "@/lib/api/types";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types/groups/clubsearch.ts` at line 1, The current import brings in
ApiResponse as a value import even though it is only used in type aliases;
change the import to a type-only import so TypeScript emits no value import
(replace the existing import of ApiResponse with a type-only import), updating
the import statement that references ApiResponse from "@/lib/api/types" and
ensure all uses of ApiResponse remain as type annotations only (e.g., in any
type aliases or interfaces).
🤖 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/app/globals.css`:
- Around line 179-185: Move the WebKit scrollbar pseudo-element rule inside the
Tailwind utility definition so all no-scrollbar styles are together: update the
`@utility` no-scrollbar block (the existing block containing -ms-overflow-style
and scrollbar-width) to also include the .no-scrollbar::-webkit-scrollbar rule
instead of having it defined separately; ensure the pseudo-element selector
(.no-scrollbar::-webkit-scrollbar) is nested/placed within the same `@utility`
no-scrollbar declaration so the WebKit rule is applied wherever the utility is
used.

In `@src/app/groups/create/page.tsx`:
- Around line 294-315: The textarea for clubDescription (the JSX element using
value={clubDescription}, onChange calling setClubDescription and autoResize) is
missing an enforced character limit; add a maxLength={500} attribute to that
textarea and ensure the onChange handler (setClubDescription) also
guards/truncates input to 500 chars. Do the same for the other field referenced
in the comment (the shorter text input around the second diff, e.g., the
title/input with 40자 제한) by adding maxLength={40} and enforcing/truncating in
its onChange handler so the UI constraint is actually applied.
- Around line 122-136: The refetch result from nameQuery.refetch() must be
validated because TanStack Query v5 returns a result object rather than
throwing; after calling nameQuery.refetch() check r.isError and ensure r.data is
a boolean before using it: if r.isError or typeof r.data !== "boolean"
setNameCheck("idle") and toast.error("이름 중복 확인 실패"); only when r.data ===
true/false proceed to setNameCheck("duplicate"/"available") and call the
corresponding toast; alternatively pass { throwOnError: true } to
nameQuery.refetch() and keep the existing try/catch but prefer explicit
r.isError/type guard for clarity.

In `@src/app/groups/page.tsx`:
- Around line 226-227: Change the catch clause to accept unknown instead of any
and narrow the error before using its message: replace the catch parameter in
the try/catch around the group join flow with (e: unknown), then check (e
instanceof Error) to call toast.error(e.message) and fall back to
toast.error("가입 신청에 실패했습니다.") for non-Error values; this uses the existing
toast.error call and matches the error-handling pattern in PasswordEntry.tsx and
useProfileImage.ts.
- Around line 231-246: The onSubmitSearch handler calls refetchSearch
immediately after setAppliedParams which causes a stale manual refetch; remove
the refetchSearch() call from onSubmitSearch and let React Query auto-refetch
based on the changed appliedParams (i.e., update only inside onSubmitSearch:
call setAppliedParams({...}) and return), or if you truly need an immediate
fetch, compute the new params into a local const and call refetchSearch with
those explicit params instead of relying on state.

In `@src/components/base-ui/BookStory/bookstory_card_large.tsx`:
- Around line 56-64: Replace the non-semantic, clickable divs that handle
profile and like actions with semantic <button type="button"> elements so they
are keyboard-focusable and actionable (preserve visual classes by moving
className to the button), keep the existing handlers (onProfileClick,
onLikeClick) but move e.stopPropagation() into the button onClick and remove any
manual onKey handlers; also add an appropriate aria-label for screen readers
(e.g., "Open profile" / "Like story") and ensure buttons do not submit forms by
specifying type="button". This change applies to the block using onProfileClick
and the analogous like-action block (the second clickable div).

In `@src/components/base-ui/BookStory/bookstory_card.tsx`:
- Around line 137-146: Replace the clickable div used for the like control with
a semantic button element (same place where onClick stops propagation and calls
onLikeClick, referencing onLikeClick, likedByMe and likeCount) for both mobile
and desktop footers; keep the existing classes but move them to a <button
type="button">, preserve e.stopPropagation() in the onClick handler, add an
accessible label/aria-pressed (e.g., aria-pressed={likedByMe}) and ensure the
Image and span remain inside the button so keyboard users and screen readers can
interact correctly.

In `@src/components/base-ui/BookStory/bookstory_detail.tsx`:
- Around line 189-197: Replace the clickable divs used for the like control with
semantic button elements to enable keyboard access and ARIA state; in the mobile
block (the element wrapping Image heartIcon and the span showing 좋아요
{likeCount}) and the tablet/desktop block do the following: change the outer
<div> to a <button>, keep the onClick handler (onLikeClick) and
e.stopPropagation logic, add type="button", add aria-pressed={likedByMe} and an
accessible aria-label (e.g., `aria-label={likedByMe ? '좋아요 취소' : '좋아요'}`),
preserve classes (cursor, hover, p-1, rounded-full, transition-colors) and the
conditional text color using likedByMe; ensure the Image and span remain
children and remove any duplicate keyboard/role workarounds so both likeCount
and toggle behavior remain unchanged.

In `@src/components/base-ui/Group-Search/search_club_apply_modal.tsx`:
- Around line 67-75: The modal container div (the element that builds className
via ["w-full max-w-[1040px]", "flex flex-col items-end gap-4", ...].join(" "))
must expose dialog semantics: add role="dialog" and aria-modal="true", and set
aria-labelledby to the id of the modal title (create an id like
"apply-modal-title" on the title/header element). Also add an optional
aria-describedby pointing to the element with the modal body or instructions if
present, and ensure the title element has that matching id; apply the same
changes to the other modal container instance noted in the file (around the
second occurrence at 88).

In `@src/components/base-ui/Profile/OtherUser/ProfileUserInfo.tsx`:
- Around line 117-118: The ProfileUserInfo component is rendering hardcoded
zeros via the StatItem calls; update ProfileUserInfo to not render those
StatItem elements until real values are available by conditionally rendering
them (e.g. render <StatItem label="구독 중" count={subscriptionCount} /> and
<StatItem label="구독자" count={subscriberCount} /> only when
subscriptionCount/subscriberCount are non-null/defined or the API call has
returned), or hide the whole stats block during loading; tie the checks to the
actual data source used in this component (e.g. props/state like
subscriptionCount, subscriberCount or the fetchUserStats/useUser hook) so dummy
0s are never shown.

In `@src/hooks/mutations/useMemberMutations.ts`:
- Line 186: Replace the explicit any types used for the onError callback
parameters with unknown in src/hooks/mutations/useMemberMutations.ts — update
the onError signatures (the callbacks currently declared as onError: (error:
any, variables, context) => { ... }) to use onError: (error: unknown, variables,
context) => { ... } for all three occurrences (the handlers around lines ~55,
~117, and ~186), and if the handler body uses the error value, narrow or
stringify it (e.g., String(error) or check typeof) before passing to
console.error or other APIs to satisfy TypeScript safety.
- Around line 127-135: The throttling check currently inside mutationFn (using
followThrottleMap and now/lastTime) causes onMutate to run for throttled calls
and produce optimistic updates; move that throttle logic out so the mutation
never starts when blocked: remove the early-return throttle from mutationFn and
instead perform the 500ms check in the caller/wrapper that invokes the mutation
(e.g., the function that calls followMutation.mutate or the exported
toggleFollow handler), using followThrottleMap[nickname] to gate calling
followMutation.mutate so onMutate/onSettled never run for throttled clicks.

In `@src/hooks/mutations/useStoryMutations.ts`:
- Around line 149-160: Move the throttle logic out of mutationFn into onMutate:
in onMutate check likeThrottleMap[bookStoryId] and if throttled set
likeThrottleMap and return a context object {throttled: true} so you can skip
optimistic updates in onMutate and have onSettled check context.throttled to
skip cache invalidation; also keep a guard at the top of mutationFn (still
referencing likeThrottleMap and storyService.toggleLikeStory) to early-return if
throttled to avoid making the API call; finally, include storyKeys.all in the
invalidation list used by onSettled (alongside infiniteList, myList, list,
detail) so otherMember caches are refreshed.
- Around line 171-190: The otherMember caches (storyKeys.otherMember(nickname))
are not updated when toggling likes; update them optimistically in onMutate and
invalidate them in onSettled similar to the infinite/my lists. In onMutate, if
previousOtherMember (or however you store the previous cache for otherMember)
exists call
queryClient.setQueryData<InfiniteData<BookStoryListResponse>>(storyKeys.otherMember(nickname),
old => updateLikeInInfiniteList(old, bookStoryId)) or
queryClient.setQueryData<BookStoryListResponse>(storyKeys.otherMember(nickname),
old => updateLikeInStoryList(old, bookStoryId)) to match the shape; in onSettled
call queryClient.invalidateQueries(storyKeys.otherMember(nickname)) to refetch.
Ensure you reference storyKeys.otherMember, updateLikeInInfiniteList,
updateLikeInStoryList, queryClient.setQueryData and
queryClient.invalidateQueries when applying the change.

In `@src/lib/api/types.ts`:
- Around line 1-7: Remove the duplicate ApiResponse type declaration in this
file and instead rely on the canonical ApiResponse<T = unknown> defined in
src/types/auth.ts; either delete the local export of ApiResponse or replace it
with an import and re-export (e.g., import { ApiResponse } from 'src/types/auth'
and export type { ApiResponse }), ensuring no duplicate type declarations remain
and that usages across authService and other services reference the single
canonical type.

In `@src/services/clubService.ts`:
- Line 35: Replace the explicit any on the sanitization object by typing it
instead of using any: change "const cleaned: any = { ...params }" to a typed
version such as "const cleaned: Partial<typeof params> = { ...params }" or, if
there is a declared request param interface (e.g. ListClubsParams or
ClubQueryParams), use that like "const cleaned: Partial<ListClubsParams> = {
...params }"; ensure you import/use the actual params type where available so
the linter rule `@typescript-eslint/no-explicit-any` is satisfied and type safety
is preserved when cleaning request parameters.

In `@src/services/imageService.ts`:
- Around line 43-45: Check that the presigned response is successful and
contains a result before destructuring; update the code that uses
presigned.result (the variable presigned and its properties presigned.result,
presignedUrl, imageUrl) to first assert presigned?.isSuccess is true and
presigned?.result exists, throw or return a descriptive error if not, and only
then call putToPresignedUrl(presignedUrl, file, contentType); this ensures you
don't destructure undefined and surfaces a clear error when the presign API
fails.

In `@src/services/memberService.ts`:
- Around line 37-42: The getOtherProfile function currently returns
response.result! without guarding for failure; update getOtherProfile in
memberService.ts to check the ApiResponse (e.g. response.isSuccess and that
response.result is defined) after awaiting
apiClient.get(MEMBER_ENDPOINTS.GET_OTHER_PROFILE(nickname)) and throw a
descriptive error (or reject) when isSuccess is false or result is missing
instead of using the non-null assertion on response.result; use the ApiResponse
type and MEMBER_ENDPOINTS.GET_OTHER_PROFILE reference to locate the code.

---

Outside diff comments:
In `@src/app/`(main)/page.tsx:
- Around line 106-124: The BookStoryCardLarge instances are rendering the
current likedByMe state but not wiring the like action, so clicks do nothing;
add an onLikeClick prop to each BookStoryCardLarge that invokes the existing
like handler (e.g., call toggleLike or the component's like handler used
elsewhere) with the story identity and current liked state (for example pass
story.bookStoryId and story.likedByMe or an object the handler expects). Update
the mappings that create BookStoryCardLarge (the shown block and the other two
similar mappings) to pass onLikeClick={() => toggleLike({ bookStoryId:
story.bookStoryId, liked: story.likedByMe })} (or the equivalent signature your
toggleLike accepts) so likes are dispatched when the user taps the like button.

In `@src/components/base-ui/Group-Search/search_mybookclub.tsx`:
- Around line 75-85: Replace the clickable div used in displayGroups.map (the
element with key={group.id} and onClick={() =>
router.push(`/groups/${group.id}`)}) with Next.js Link navigation: import Link
from 'next/link', use <Link href={`/groups/${group.id}`} ...> to preserve the
existing className, children (the span showing group.name) and key, and remove
the useRouter import and router variable if they are no longer used elsewhere;
this restores native link behavior (keyboard, context menu, new-tab) and fixes
the accessibility issues from using a div with onClick.

---

Nitpick comments:
In `@src/lib/api/endpoints/Image.ts`:
- Line 1: The import of API_BASE_URL in Image.ts currently comes from
"../endpoints" which diverges from sibling modules; update the import to use the
common source (import API_BASE_URL from "./base") so Image.ts uses the same base
URL provider as other endpoint modules—locate the import statement at the top of
Image.ts and replace the "../endpoints" module specifier with "./base" (keep the
API_BASE_URL identifier and any existing named/default import shape intact).

In `@src/lib/api/endpoints/index.ts`:
- Around line 6-7: The exports in index.ts use PascalCase ("./Clubs", "./Image")
which is inconsistent with the existing lowercase module names; rename the
module files Clubs.ts → clubs.ts and Image.ts → image.ts and update the exports
in src/lib/api/endpoints/index.ts to export * from "./clubs" and export * from
"./image" so the filenames and import/exports follow the lowercase convention
used by base, auth, bookstory, member, and book.

In `@src/types/groups/clubsearch.ts`:
- Line 1: The current import brings in ApiResponse as a value import even though
it is only used in type aliases; change the import to a type-only import so
TypeScript emits no value import (replace the existing import of ApiResponse
with a type-only import), updating the import statement that references
ApiResponse from "@/lib/api/types" and ensure all uses of ApiResponse remain as
type annotations only (e.g., in any type aliases or interfaces).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 126709a and 0bbffe8.

📒 Files selected for processing (45)
  • next.config.ts
  • src/app/(main)/books/[id]/page.tsx
  • src/app/(main)/page.tsx
  • src/app/(main)/profile/[nickname]/page.tsx
  • src/app/(main)/stories/[id]/page.tsx
  • src/app/(main)/stories/page.tsx
  • src/app/globals.css
  • src/app/groups/create/page.tsx
  • src/app/groups/groupSearchDummy.ts
  • src/app/groups/page.tsx
  • src/components/base-ui/BookStory/bookstory_card.tsx
  • src/components/base-ui/BookStory/bookstory_card_large.tsx
  • src/components/base-ui/BookStory/bookstory_detail.tsx
  • src/components/base-ui/Group-Search/search_club_apply_modal.tsx
  • src/components/base-ui/Group-Search/search_clublist/search_clublist_item.tsx
  • src/components/base-ui/Group-Search/search_mybookclub.tsx
  • src/components/base-ui/MyPage/MyBookStoryList.tsx
  • src/components/base-ui/Profile/BookStoryList.tsx
  • src/components/base-ui/Profile/OtherUser/OtherUserProfileTabs.tsx
  • src/components/base-ui/Profile/OtherUser/ProfileUserInfo.tsx
  • src/components/base-ui/home/home_bookclub.tsx
  • src/components/base-ui/home/list_subscribe_element.tsx
  • src/components/base-ui/home/list_subscribe_large.tsx
  • src/hooks/mutations/useCreateClubMutation.ts
  • src/hooks/mutations/useMemberMutations.ts
  • src/hooks/mutations/useSearchClubMutations.ts
  • src/hooks/mutations/useStoryMutations.ts
  • src/hooks/queries/useCreateClubQueries.ts
  • src/hooks/queries/useMemberQueries.ts
  • src/hooks/queries/useSearchClubQueries.ts
  • src/hooks/queries/useStoryQueries.ts
  • src/lib/api/endpoints/Clubs.ts
  • src/lib/api/endpoints/Image.ts
  • src/lib/api/endpoints/bookstory.ts
  • src/lib/api/endpoints/index.ts
  • src/lib/api/endpoints/member.ts
  • src/lib/api/types.ts
  • src/services/clubService.ts
  • src/services/imageService.ts
  • src/services/memberService.ts
  • src/services/storyService.ts
  • src/types/groups/clubCreate.ts
  • src/types/groups/clubsearch.ts
  • src/types/member.ts
  • src/utils/groupMapper.ts
💤 Files with no reviewable changes (1)
  • src/app/groups/groupSearchDummy.ts

Comment on lines +179 to +185
@utility no-scrollbar {
-ms-overflow-style: none; /* IE/Edge */
scrollbar-width: none; /* Firefox */
}
.no-scrollbar::-webkit-scrollbar {
display: none;
} No newline at end of file
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

WebKit scrollbar rule should be inside the @utility block.

The -ms-overflow-style and scrollbar-width properties are inside @utility no-scrollbar, but the ::-webkit-scrollbar pseudo-element rule (lines 183-185) is defined as a separate class selector outside the utility. This inconsistency may cause the WebKit rule to not apply correctly when using the utility class.

🔧 Proposed fix to consolidate the rules
 `@utility` no-scrollbar {
   -ms-overflow-style: none; /* IE/Edge */
   scrollbar-width: none; /* Firefox */
+  &::-webkit-scrollbar {
+    display: none;
+  }
 }
-.no-scrollbar::-webkit-scrollbar {
-  display: none;
-}

Note: The static analysis errors about @utility are false positives—this is valid Tailwind CSS v4 syntax, which the linters aren't configured to recognize.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@utility no-scrollbar {
-ms-overflow-style: none; /* IE/Edge */
scrollbar-width: none; /* Firefox */
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
`@utility` no-scrollbar {
-ms-overflow-style: none; /* IE/Edge */
scrollbar-width: none; /* Firefox */
&::-webkit-scrollbar {
display: none;
}
}
🧰 Tools
🪛 Biome (2.4.4)

[error] 179-182: Tailwind-specific syntax is disabled.

(parse)

🪛 Stylelint (17.3.0)

[error] 179-179: Unexpected unknown at-rule "@Utility" (scss/at-rule-no-unknown)

(scss/at-rule-no-unknown)

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

In `@src/app/globals.css` around lines 179 - 185, Move the WebKit scrollbar
pseudo-element rule inside the Tailwind utility definition so all no-scrollbar
styles are together: update the `@utility` no-scrollbar block (the existing block
containing -ms-overflow-style and scrollbar-width) to also include the
.no-scrollbar::-webkit-scrollbar rule instead of having it defined separately;
ensure the pseudo-element selector (.no-scrollbar::-webkit-scrollbar) is
nested/placed within the same `@utility` no-scrollbar declaration so the WebKit
rule is applied wherever the utility is used.

Comment on lines +122 to +136
try {
const r = await nameQuery.refetch();
const isDuplicate = r.data; // boolean

if (isDuplicate) {
setNameCheck("duplicate");
toast.error("이미 존재하는 모임 이름입니다.");
} else {
setNameCheck("available");
toast.success("사용 가능한 모임 이름입니다.");
}
} catch {
setNameCheck("idle");
toast.error("이름 중복 확인 실패");
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

TanStack React Query v5 refetch behavior error handling

💡 Result:

Refetch behavior in TanStack React Query v5 (and how errors are handled)

When refetching happens

A query can refetch automatically when it’s stale and one of these events happens (defaults depend on option):

  • Window/tab becomes visible again (refetchOnWindowFocus, default true) [1]
  • Reconnect / remount (controlled by refetchOnReconnect, refetchOnMount)
  • Intervals (refetchInterval) [2]

If you don’t want those background refetches, disable them per-query (or globally), e.g. refetchOnWindowFocus: false [1].

What “error state” means (and retries)

  • If your queryFn throws, the query goes to status: 'error' / isError: true and exposes the error on error [3].
  • By default, Query will retry failed queries (default retry count is 3). During retries, the error is exposed via failureReason, and only becomes error after the final failed attempt [2].

Manual refetch() and errors (important in v5)

  • Calling refetch() does not throw by default when the refetch fails; it “only logs”.
  • If you want refetch() to throw, call it with throwOnError: true (also supports cancelRefetch) [4].
await refetch({ throwOnError: true })

queryClient.refetchQueries() / invalidateQueries() and throwing on refetch failures

Similarly, queryClient.refetchQueries() (and invalidateQueries which uses it for refetching) will not throw by default if some refetches fail—but you can opt into throwing with throwOnError: true [5].

v5 change: onError / onSuccess / onSettled removed from queries

In v5, query callbacks like onError, onSuccess, onSettled were removed from useQuery / QueryObserver (mutations still have them). Handle side-effects in React (e.g. useEffect) or other patterns instead [6].

Error boundaries in v5

  • useErrorBoundary was renamed to throwOnError (for useQuery) [6].
  • If you use error boundaries, use useQueryErrorResetBoundary() to reset query errors when the boundary resets [7].

Sources

[1] TanStack docs: Window Focus Refetching (v5) (tanstack.com)
[2] TanStack docs: Query Retries (v5) (tanstack.com)
[3] TanStack docs: Queries guide (states: pending/error/success, error, isFetching) (tanstack.com)
[4] TanStack docs: useQuery reference (refetch({ throwOnError })) (zh-hant.tanstack.dev)
[5] TanStack docs: QueryClient (refetchQueries / invalidateQueries + throwOnError) (tanstack.com)
[6] TanStack docs: Migrating to v5 (removed query callbacks; useErrorBoundary -> throwOnError) (tanstack.com)
[7] TanStack docs: useQueryErrorResetBoundary (tanstack.com)

Citations:


🏁 Script executed:

# Examine the file and context around the flagged lines
cat -n src/app/groups/create/page.tsx | sed -n '115,145p'

Repository: checkmo2025/FE

Length of output: 1051


🏁 Script executed:

# Check package.json for TanStack Query version
cat package.json | grep -A 5 -B 5 "react-query\|@tanstack"

Repository: checkmo2025/FE

Length of output: 423


🏁 Script executed:

# Search for nameQuery definition in the file
rg "nameQuery" src/app/groups/create/page.tsx -B 5 -A 2

Repository: checkmo2025/FE

Length of output: 506


Handle failed name refetch explicitly—don't let undefined r.data silently pass as "available".

In TanStack Query v5, refetch() does not throw on failure by default; it returns a result object with isError: true. Since the code calls refetch() without throwOnError: true, the catch block never executes on network/API failures. This means failed name checks fall through to the "else" branch, incorrectly marking duplicate checks as "available".

Add an explicit guard after refetch to check both r.isError and the type of r.data before proceeding:

🛠️ Suggested fix
     try {
       const r = await nameQuery.refetch();
+      if (r.isError || typeof r.data !== "boolean") {
+        setNameCheck("idle");
+        toast.error("이름 중복 확인 실패");
+        return;
+      }
       const isDuplicate = r.data;

       if (isDuplicate) {
         setNameCheck("duplicate");
         toast.error("이미 존재하는 모임 이름입니다.");
       } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const r = await nameQuery.refetch();
const isDuplicate = r.data; // boolean
if (isDuplicate) {
setNameCheck("duplicate");
toast.error("이미 존재하는 모임 이름입니다.");
} else {
setNameCheck("available");
toast.success("사용 가능한 모임 이름입니다.");
}
} catch {
setNameCheck("idle");
toast.error("이름 중복 확인 실패");
}
try {
const r = await nameQuery.refetch();
if (r.isError || typeof r.data !== "boolean") {
setNameCheck("idle");
toast.error("이름 중복 확인 실패");
return;
}
const isDuplicate = r.data; // boolean
if (isDuplicate) {
setNameCheck("duplicate");
toast.error("이미 존재하는 모임 이름입니다.");
} else {
setNameCheck("available");
toast.success("사용 가능한 모임 이름입니다.");
}
} catch {
setNameCheck("idle");
toast.error("이름 중복 확인 실패");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/groups/create/page.tsx` around lines 122 - 136, The refetch result
from nameQuery.refetch() must be validated because TanStack Query v5 returns a
result object rather than throwing; after calling nameQuery.refetch() check
r.isError and ensure r.data is a boolean before using it: if r.isError or typeof
r.data !== "boolean" setNameCheck("idle") and toast.error("이름 중복 확인 실패"); only
when r.data === true/false proceed to setNameCheck("duplicate"/"available") and
call the corresponding toast; alternatively pass { throwOnError: true } to
nameQuery.refetch() and keep the existing try/catch but prefer explicit
r.isError/type guard for clarity.

Comment on lines +294 to +315
<textarea
value={clubDescription}
onChange={(e) => {
setClubDescription(e.target.value);
autoResize(e.currentTarget);
}}
onInput={(e) => autoResize(e.currentTarget)}
placeholder="자유롭게 입력해주세요! (500자 제한)"
className="
w-full
min-h-[200px] t:min-h-[260px]
rounded-[8px]
border border-Subbrown-4
bg-White
p-5 mt-5
no-scrollbar
resize-none
outline-none
Body_1_3
placeholder:text-Gray-3
"
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Input constraints advertised in placeholders should be enforced in the fields.

The UI says “500자 제한 / 40자 제한”, but maxLength is missing on those inputs.

✏️ Suggested fix
               <textarea
                 value={clubDescription}
@@
                 placeholder="자유롭게 입력해주세요! (500자 제한)"
+                maxLength={500}
                 className="
@@
               <input
                 value={activityArea}
                 onChange={(e) => setActivityArea(e.target.value)}
                 placeholder="활동 지역을 입력해주세요 (40자 제한)"
+                maxLength={40}
                 className="mt-4 w-full h-[44px] t:h-[56px] rounded-[8px] border border-[`#EAE5E2`] body_1_3 bg-white px-4 outline-none"
               />

Also applies to: 511-516

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

In `@src/app/groups/create/page.tsx` around lines 294 - 315, The textarea for
clubDescription (the JSX element using value={clubDescription}, onChange calling
setClubDescription and autoResize) is missing an enforced character limit; add a
maxLength={500} attribute to that textarea and ensure the onChange handler
(setClubDescription) also guards/truncates input to 500 chars. Do the same for
the other field referenced in the comment (the shorter text input around the
second diff, e.g., the title/input with 40자 제한) by adding maxLength={40} and
enforcing/truncating in its onChange handler so the UI constraint is actually
applied.

Comment on lines +226 to +227
} catch (e: any) {
toast.error(e?.message ?? "가입 신청에 실패했습니다.");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and get its size
wc -l src/app/groups/page.tsx 2>/dev/null || echo "File not found"

Repository: checkmo2025/FE

Length of output: 85


🏁 Script executed:

# Read the relevant section around lines 226-227
sed -n '220,235p' src/app/groups/page.tsx

Repository: checkmo2025/FE

Length of output: 400


🏁 Script executed:

# Search for all catch blocks in the file to identify similar patterns
rg -n "catch\s*\(" src/app/groups/page.tsx

Repository: checkmo2025/FE

Length of output: 84


🏁 Script executed:

# Check for lint configuration files
ls -la | grep -E "(eslint|tsconfig|typescript)" || echo "Checking for config files..."
find . -maxdepth 2 -name ".*eslintrc*" -o -name "tsconfig*" -o -name ".eslintignore" 2>/dev/null | head -20

Repository: checkmo2025/FE

Length of output: 209


🏁 Script executed:

# Search for other catch blocks in the file to understand error handling patterns
rg -n "catch\s*\(" src/app/groups/page.tsx -A 2

Repository: checkmo2025/FE

Length of output: 149


🏁 Script executed:

# Search for catch blocks in the entire codebase to see common patterns
rg -n "catch\s*\(" --type tsx --type ts -c | head -20

Repository: checkmo2025/FE

Length of output: 85


🏁 Script executed:

# Check tsconfig.json for strict type settings
cat tsconfig.json 2>/dev/null | head -40

Repository: checkmo2025/FE

Length of output: 809


🏁 Script executed:

# Read the ESLint config
cat eslint.config.mjs

Repository: checkmo2025/FE

Length of output: 522


🏁 Script executed:

# Search for catch blocks with unknown type to see if codebase uses that pattern
rg "catch\s*\(\s*\w+:\s*unknown\s*\)" --type js --type ts -A 2 | head -30

Repository: checkmo2025/FE

Length of output: 761


🏁 Script executed:

# Search for catch blocks in all TypeScript/JavaScript files to see patterns
rg "catch\s*\(\s*\w+" -o | sort | uniq -c | sort -rn

Repository: checkmo2025/FE

Length of output: 839


🏁 Script executed:

# Look at other error handling in the same file context
sed -n '210,240p' src/app/groups/page.tsx

Repository: checkmo2025/FE

Length of output: 822


Replace any in error handling with unknown + narrowing.

Line 226 currently uses any, which violates ESLint rules (from eslint-config-next/typescript) and is inconsistent with the error handling pattern already established elsewhere in the codebase (e.g., PasswordEntry.tsx, useProfileImage.ts).

Proposed fix
-    } catch (e: any) {
-      toast.error(e?.message ?? "가입 신청에 실패했습니다.");
+    } catch (e: unknown) {
+      const message = e instanceof Error ? e.message : "가입 신청에 실패했습니다.";
+      toast.error(message);
     }
🧰 Tools
🪛 ESLint

[error] 226-226: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

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

In `@src/app/groups/page.tsx` around lines 226 - 227, Change the catch clause to
accept unknown instead of any and narrow the error before using its message:
replace the catch parameter in the try/catch around the group join flow with (e:
unknown), then check (e instanceof Error) to call toast.error(e.message) and
fall back to toast.error("가입 신청에 실패했습니다.") for non-Error values; this uses the
existing toast.error call and matches the error-handling pattern in
PasswordEntry.tsx and useProfileImage.ts.

Comment on lines +231 to +246
const onSubmitSearch = () => {
const keyword = q.trim();

// 검색없으면 추천모드로
if (!keyword) {
setAppliedParams(null);
return;
}
setAppliedParams({
outputFilter: mapCategoryToOutputFilter(category),
inputFilter: mapInputFilter(group, region),
keyword: keyword,
});

refetchSearch();
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/app/groups/page.tsx | head -300 | tail -100

Repository: checkmo2025/FE

Length of output: 3661


🏁 Script executed:

rg "refetchSearch|appliedParams" src/app/groups/page.tsx -B 2 -A 2

Repository: checkmo2025/FE

Length of output: 711


🏁 Script executed:

# Search for the refetchSearch function definition
rg "const refetchSearch|function refetchSearch" src/app/groups/ -A 10

Repository: checkmo2025/FE

Length of output: 40


🏁 Script executed:

# Search for how appliedParams is used in effects or queries
rg "appliedParams" src/app/groups/ -B 3 -A 3

Repository: checkmo2025/FE

Length of output: 1377


🏁 Script executed:

fd "useInfiniteClubSearchQuery|clubSearch" src/ -type f -name "*.ts" -o -name "*.tsx" | grep -E "\.(ts|tsx)$"

Repository: checkmo2025/FE

Length of output: 228


🏁 Script executed:

rg "useInfiniteClubSearchQuery" src/ -l

Repository: checkmo2025/FE

Length of output: 123


🏁 Script executed:

rg "export.*useInfiniteClubSearchQuery|function useInfiniteClubSearchQuery" src/ -A 20

Repository: checkmo2025/FE

Length of output: 1468


🏁 Script executed:

rg "clubQueryKeys.search" src/ -B 5 -A 5

Repository: checkmo2025/FE

Length of output: 947


🏁 Script executed:

cat -n src/hooks/queries/useSearchClubQueries.ts

Repository: checkmo2025/FE

Length of output: 2044


Avoid immediate refetchSearch() after setAppliedParams—the manual refetch executes with stale params.

When setAppliedParams() is called, the state update is asynchronous. The refetchSearch() call on line 245 executes before state updates, causing it to refetch with the previous appliedParams value. This creates a redundant network request. React Query will automatically refetch once the queryKey changes (when appliedParams updates and the component re-renders), making the manual refetch unnecessary and causing duplicate requests.

Proposed fix
 const onSubmitSearch = () => {
   const keyword = q.trim();

   // 검색없으면 추천모드로
   if (!keyword) {
     setAppliedParams(null); 
     return;
   }
-  setAppliedParams({
+  const nextParams = {
     outputFilter: mapCategoryToOutputFilter(category),
     inputFilter: mapInputFilter(group, region),
-    keyword: keyword,
-  });
-
-  refetchSearch();
+    keyword,
+  } satisfies Omit<ClubSearchParams, "cursorId">;
+
+  const isSameParams =
+    appliedParams?.keyword === nextParams.keyword &&
+    appliedParams?.inputFilter === nextParams.inputFilter &&
+    appliedParams?.outputFilter === nextParams.outputFilter;
+
+  if (isSameParams) {
+    refetchSearch();
+    return;
+  }
+
+  setAppliedParams(nextParams);
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const onSubmitSearch = () => {
const keyword = q.trim();
// 검색없으면 추천모드로
if (!keyword) {
setAppliedParams(null);
return;
}
setAppliedParams({
outputFilter: mapCategoryToOutputFilter(category),
inputFilter: mapInputFilter(group, region),
keyword: keyword,
});
refetchSearch();
};
const onSubmitSearch = () => {
const keyword = q.trim();
// 검색없으면 추천모드로
if (!keyword) {
setAppliedParams(null);
return;
}
const nextParams = {
outputFilter: mapCategoryToOutputFilter(category),
inputFilter: mapInputFilter(group, region),
keyword,
} satisfies Omit<ClubSearchParams, "cursorId">;
const isSameParams =
appliedParams?.keyword === nextParams.keyword &&
appliedParams?.inputFilter === nextParams.inputFilter &&
appliedParams?.outputFilter === nextParams.outputFilter;
if (isSameParams) {
refetchSearch();
return;
}
setAppliedParams(nextParams);
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/groups/page.tsx` around lines 231 - 246, The onSubmitSearch handler
calls refetchSearch immediately after setAppliedParams which causes a stale
manual refetch; remove the refetchSearch() call from onSubmitSearch and let
React Query auto-refetch based on the changed appliedParams (i.e., update only
inside onSubmitSearch: call setAppliedParams({...}) and return), or if you truly
need an immediate fetch, compute the new params into a local const and call
refetchSearch with those explicit params instead of relying on state.

Comment on lines +1 to +7
// ~/types/auth.ts에 정의되어 있는건 아는데 거기 있는 거보단 여기가 맞을 거 같아서 빼두겠습니다.
export type ApiResponse<T> = {
isSuccess: boolean;
code: string;
message: string;
result: T;
}; No newline at end of file
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check if `@/lib/api/types` is imported anywhere
echo "=== Searching for imports from `@/lib/api/types` ==="
rg "@/lib/api/types" --type=ts --type=tsx

# Check for ApiResponse definition in auth.ts
echo -e "\n=== Searching for ApiResponse in auth.ts files ==="
rg -n "ApiResponse" --type=ts -g '*auth*'

# Check all ApiResponse definitions across the codebase
echo -e "\n=== All ApiResponse definitions ==="
rg "export.*type.*ApiResponse|export.*interface.*ApiResponse" --type=ts -A2

# Check where ApiResponse is used
echo -e "\n=== ApiResponse usage across codebase ==="
rg "ApiResponse" --type=ts --type=tsx -c | head -20

Repository: checkmo2025/FE

Length of output: 3731


Remove the duplicate ApiResponse type definition from this file.

The new ApiResponse type in src/lib/api/types.ts is not imported anywhere in the codebase. The actual definition being used is ApiResponse<T = unknown> in src/types/auth.ts (line 15), which is actively used throughout authService.ts and other services. This file contains unused duplicate code; either delete it or clarify its intended purpose.

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

In `@src/lib/api/types.ts` around lines 1 - 7, Remove the duplicate ApiResponse
type declaration in this file and instead rely on the canonical ApiResponse<T =
unknown> defined in src/types/auth.ts; either delete the local export of
ApiResponse or replace it with an import and re-export (e.g., import {
ApiResponse } from 'src/types/auth' and export type { ApiResponse }), ensuring
no duplicate type declarations remain and that usages across authService and
other services reference the single canonical type.

},

searchClubs: async (params: ClubSearchParams) => {
const cleaned: any = { ...params };
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check file existence and size
wc -l src/services/clubService.ts

Repository: checkmo2025/FE

Length of output: 88


🏁 Script executed:

# Read the file around line 35 to see the exact code
sed -n '25,45p' src/services/clubService.ts

Repository: checkmo2025/FE

Length of output: 665


🏁 Script executed:

# Search for ClubSearchParams type definition
rg -n 'type\s+ClubSearchParams|interface\s+ClubSearchParams' -t ts

Repository: checkmo2025/FE

Length of output: 124


🏁 Script executed:

# Check the full function signature containing line 35
sed -n '1,50p' src/services/clubService.ts | grep -n "searchClubs\|cleaned\|const\|function" -A 2

Repository: checkmo2025/FE

Length of output: 972


🏁 Script executed:

# Check the ClubSearchParams type definition
sed -n '80,110p' src/types/groups/clubsearch.ts

Repository: checkmo2025/FE

Length of output: 505


🏁 Script executed:

# Also check if there are any other usages of 'any' type in this file
rg -n ':.*any' src/services/clubService.ts

Repository: checkmo2025/FE

Length of output: 98


Replace explicit any in request-param sanitization.

const cleaned: any violates the @typescript-eslint/no-explicit-any lint rule and removes type safety when cleaning up API parameters before passing them to the request.

Suggested fix
-  const cleaned: any = { ...params };
+  const cleaned: Partial<ClubSearchParams> = { ...params };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const cleaned: any = { ...params };
const cleaned: Partial<ClubSearchParams> = { ...params };
🧰 Tools
🪛 ESLint

[error] 35-35: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

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

In `@src/services/clubService.ts` at line 35, Replace the explicit any on the
sanitization object by typing it instead of using any: change "const cleaned:
any = { ...params }" to a typed version such as "const cleaned: Partial<typeof
params> = { ...params }" or, if there is a declared request param interface
(e.g. ListClubsParams or ClubQueryParams), use that like "const cleaned:
Partial<ListClubsParams> = { ...params }"; ensure you import/use the actual
params type where available so the linter rule
`@typescript-eslint/no-explicit-any` is satisfied and type safety is preserved
when cleaning request parameters.

Comment on lines +43 to +45
const { presignedUrl, imageUrl } = presigned.result;

await putToPresignedUrl(presignedUrl, file, contentType);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate presigned response before destructuring result.

If the presigned URL API returns isSuccess=false or no result, this will throw at runtime before a meaningful error is surfaced.

💡 Proposed fix
-    const { presignedUrl, imageUrl } = presigned.result;
+    if (!presigned.isSuccess || !presigned.result) {
+      throw new Error(presigned.message || "Presigned URL 발급 실패");
+    }
+    const { presignedUrl, imageUrl } = presigned.result;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { presignedUrl, imageUrl } = presigned.result;
await putToPresignedUrl(presignedUrl, file, contentType);
if (!presigned.isSuccess || !presigned.result) {
throw new Error(presigned.message || "Presigned URL 발급 실패");
}
const { presignedUrl, imageUrl } = presigned.result;
await putToPresignedUrl(presignedUrl, file, contentType);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/imageService.ts` around lines 43 - 45, Check that the presigned
response is successful and contains a result before destructuring; update the
code that uses presigned.result (the variable presigned and its properties
presigned.result, presignedUrl, imageUrl) to first assert presigned?.isSuccess
is true and presigned?.result exists, throw or return a descriptive error if
not, and only then call putToPresignedUrl(presignedUrl, file, contentType); this
ensures you don't destructure undefined and surfaces a clear error when the
presign API fails.

Comment on lines +37 to +42
getOtherProfile: async (nickname: string): Promise<OtherProfileResponse> => {
const response = await apiClient.get<ApiResponse<OtherProfileResponse>>(
MEMBER_ENDPOINTS.GET_OTHER_PROFILE(nickname)
);
return response.result!;
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard getOtherProfile response before returning result!.

If the backend returns isSuccess=false (or missing result), this resolves with invalid data instead of a proper thrown error.

💡 Proposed fix
     getOtherProfile: async (nickname: string): Promise<OtherProfileResponse> => {
         const response = await apiClient.get<ApiResponse<OtherProfileResponse>>(
             MEMBER_ENDPOINTS.GET_OTHER_PROFILE(nickname)
         );
-        return response.result!;
+        if (!response.isSuccess || !response.result) {
+            throw new Error(response.message || "Failed to fetch other profile");
+        }
+        return response.result;
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getOtherProfile: async (nickname: string): Promise<OtherProfileResponse> => {
const response = await apiClient.get<ApiResponse<OtherProfileResponse>>(
MEMBER_ENDPOINTS.GET_OTHER_PROFILE(nickname)
);
return response.result!;
},
getOtherProfile: async (nickname: string): Promise<OtherProfileResponse> => {
const response = await apiClient.get<ApiResponse<OtherProfileResponse>>(
MEMBER_ENDPOINTS.GET_OTHER_PROFILE(nickname)
);
if (!response.isSuccess || !response.result) {
throw new Error(response.message || "Failed to fetch other profile");
}
return response.result;
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/memberService.ts` around lines 37 - 42, The getOtherProfile
function currently returns response.result! without guarding for failure; update
getOtherProfile in memberService.ts to check the ApiResponse (e.g.
response.isSuccess and that response.result is defined) after awaiting
apiClient.get(MEMBER_ENDPOINTS.GET_OTHER_PROFILE(nickname)) and throw a
descriptive error (or reject) when isSuccess is false or result is missing
instead of using the non-null assertion on response.result; use the ApiResponse
type and MEMBER_ENDPOINTS.GET_OTHER_PROFILE reference to locate the code.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

이 PR은 모임 생성, 검색, 추천 기능과 사용자 프로필 조회 및 팔로우/언팔로우, 책 이야기 좋아요/취소 기능을 구현하며 상당한 기능 개선을 이루었습니다. API 연동, UI 업데이트, 낙관적 업데이트 및 스로틀링 로직 등 전반적으로 견고하게 구현되었습니다. 코드 구조화 및 재사용성 측면에서도 좋은 개선이 이루어졌습니다.

function safeImageSrc(src?: string | null) {
if (!src) return DEFAULT_CLUB_IMG;

if (src === "string") return DEFAULT_CLUB_IMG;
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

src === "string" 조건은 srcstring | null | undefined 타입이므로 항상 false입니다. !srcnull, undefined, ""를 모두 처리하므로 이 줄은 제거하거나 typeof src !== "string"으로 변경하는 것이 더 정확합니다. 현재는 !src 조건으로 인해 기능상 문제는 없지만, 혼란을 줄 수 있습니다.

Suggested change
if (src === "string") return DEFAULT_CLUB_IMG;
if (src.startsWith("/")) return src;

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.

♻️ Duplicate comments (1)
src/hooks/mutations/useStoryMutations.ts (1)

155-167: ⚠️ Potential issue | 🟠 Major

Throttle check is placed too late, so throttled clicks still run optimistic + invalidation flow.

At Line 155, the throttle is in mutationFn, but onMutate (Line 167) runs first in React Query. Throttled clicks still do optimistic updates and then trigger Line 247 invalidation, causing flicker/refetch without an API call.

Suggested fix
         mutationFn: async (bookStoryId: number) => {
             const now = Date.now();
             const lastTime = likeThrottleMap[bookStoryId] || 0;

             // Throttle: 500ms
             if (now - lastTime < 500) {
                 return;
             }
             likeThrottleMap[bookStoryId] = now;

             return storyService.toggleLikeStory(bookStoryId);
         },
         onMutate: async (bookStoryId) => {
+            const now = Date.now();
+            const lastTime = likeThrottleMap[bookStoryId] || 0;
+            if (now - lastTime < 500) {
+                return { throttled: true as const };
+            }
+
             // Cancel any outgoing refetches
             await queryClient.cancelQueries({ queryKey: storyKeys.all });

             // Snapshot the previous values
             const previousInfiniteStories = queryClient.getQueryData(storyKeys.infiniteList());
@@
             return {
+                throttled: false as const,
                 previousInfiniteStories,
                 previousMyStories,
                 previousStories,
                 previousStoryDetail,
                 previousOtherMemberStories,
             };
         },
         onError: (err, bookStoryId, context) => {
+            if (context?.throttled) return;
             console.error("Failed to toggle like:", err);
             toast.error("좋아요 상태 업데이트에 실패했습니다.");
@@
-        onSettled: (data, err, bookStoryId) => {
+        onSettled: (data, err, bookStoryId, context) => {
+            if (context?.throttled) return;
             // Invalidate queries to ensure sync with server
             queryClient.invalidateQueries({ queryKey: storyKeys.infiniteList() });
             queryClient.invalidateQueries({ queryKey: storyKeys.myList() });
             queryClient.invalidateQueries({ queryKey: [...storyKeys.all, "otherMember"] });
             queryClient.invalidateQueries({ queryKey: storyKeys.list() });
             queryClient.invalidateQueries({ queryKey: storyKeys.detail(bookStoryId) });
         },
#!/bin/bash
# Verify throttle guard placement and throttled-context skips in mutation lifecycle.
rg -n "mutationFn: async|onMutate: async|if \\(now - lastTime < 500\\)|throttled|onError:|onSettled:" src/hooks/mutations/useStoryMutations.ts -C 2

Expected after fix:

  • Throttle check exists at top of onMutate.
  • onError and onSettled both short-circuit on context?.throttled.

Also applies to: 167-170, 247-253

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

In `@src/hooks/mutations/useStoryMutations.ts` around lines 155 - 167, Move the
500ms throttle check out of mutationFn and into the very first lines of onMutate
so clicks are short-circuited before any optimistic update; use the existing
likeThrottleMap and bookStoryId to compute now/lastTime and, if throttled,
return a context object like { throttled: true } so the mutation lifecycle knows
to skip further work. In addition, update onError and onSettled to guard on
context?.throttled and immediately return (no rollback or invalidation) when
true. Keep mutationFn calling storyService.toggleLikeStory(bookStoryId) only for
non-throttled cases. Ensure the same pattern is applied for handlers referencing
onError/onSettled to prevent flicker/refetch.
🧹 Nitpick comments (1)
src/hooks/mutations/useStoryMutations.ts (1)

54-55: Defensively clamp commentCount to zero when applying deltas.

commentCount + delta can show negative values in stale-cache edge cases (especially delete path). Clamping avoids invalid UI states.

Suggested fix
-                    commentCount: story.commentCount + delta,
+                    commentCount: Math.max(0, story.commentCount + delta),
-                        commentCount: story.commentCount + delta,
+                        commentCount: Math.max(0, story.commentCount + delta),

Also applies to: 72-73

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

In `@src/hooks/mutations/useStoryMutations.ts` around lines 54 - 55, The update
that sets commentCount can go negative in stale-cache edge cases; change the
assignment in useStoryMutations that currently does "commentCount:
story.commentCount + delta" to clamp at zero (e.g., use Math.max(0,
story.commentCount + delta)) so commentCount never becomes negative, and apply
the same change to the second occurrence around lines 72-73.
🤖 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/hooks/mutations/useStoryMutations.ts`:
- Around line 155-167: Move the 500ms throttle check out of mutationFn and into
the very first lines of onMutate so clicks are short-circuited before any
optimistic update; use the existing likeThrottleMap and bookStoryId to compute
now/lastTime and, if throttled, return a context object like { throttled: true }
so the mutation lifecycle knows to skip further work. In addition, update
onError and onSettled to guard on context?.throttled and immediately return (no
rollback or invalidation) when true. Keep mutationFn calling
storyService.toggleLikeStory(bookStoryId) only for non-throttled cases. Ensure
the same pattern is applied for handlers referencing onError/onSettled to
prevent flicker/refetch.

---

Nitpick comments:
In `@src/hooks/mutations/useStoryMutations.ts`:
- Around line 54-55: The update that sets commentCount can go negative in
stale-cache edge cases; change the assignment in useStoryMutations that
currently does "commentCount: story.commentCount + delta" to clamp at zero
(e.g., use Math.max(0, story.commentCount + delta)) so commentCount never
becomes negative, and apply the same change to the second occurrence around
lines 72-73.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0bbffe8 and 8644e86.

📒 Files selected for processing (1)
  • src/hooks/mutations/useStoryMutations.ts

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] 상대방 프로필 조회 API 구현

2 participants