Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello @hongik-luke, 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! 이 Pull Request는 모임 관련 페이지들의 백엔드 API 연동을 완료하고, 운영진을 위한 모임 관리 기능을 대폭 강화하는 데 중점을 두었습니다. 기존 더미 데이터를 실제 데이터로 대체하고, 모임 가입 신청 승인/거절, 회원 역할 변경, 모임 정보 수정 등 핵심 관리 기능을 사용자 인터페이스와 함께 구현하여 모임 운영의 편의성을 향상시켰습니다. Highlights
🧠 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
Activity
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
Code Review
This pull request implements group home and management features, transitioning from dummy data to actual API integration and utilizing react-query for clean asynchronous state management. The effort to improve code structure by grouping multiple API requests into a single custom hook is commendable. However, a critical security vulnerability was identified: a potential unsafe navigation issue where user-supplied nicknames are used in URL construction without proper encoding. This requires immediate attention. Additionally, the review highlighted areas for improvement concerning unfinished features, type safety, and enhancing hook reusability. Overall, this is a strong contribution, and other data handling practices, such as URL sanitization for external links, appear to follow security best practices.
| // TODO: useClubNameCheckQuery(name).refetch() 연결 | ||
| // 임시: 무조건 available 처리 | ||
| setNameCheck("available"); | ||
| toast.success("사용 가능한 모임 이름입니다."); |
| const goProfile = (nickname: string) => { | ||
| router.push(`/profile/${nickname}`); | ||
| }; |
There was a problem hiding this comment.
The nickname variable is used to construct a URL path without proper encoding. If a nickname contains special characters (e.g., .., /, ?, #), it could lead to unexpected navigation or path traversal within the application. It is recommended to use encodeURIComponent to ensure the URL is constructed safely, consistent with the implementation in src/app/groups/[id]/admin/members/page.tsx.
| const goProfile = (nickname: string) => { | |
| router.push(`/profile/${nickname}`); | |
| }; | |
| const goProfile = (nickname: string) => { | |
| router.push(`/profile/${encodeURIComponent(nickname)}`); | |
| }; |
| open: open === true, | ||
| }; | ||
|
|
||
| await updateClub.mutateAsync(payload as any); |
There was a problem hiding this comment.
| onSuccess: async (_data, variables) => { | ||
| // 가입 신청 관리 페이지는 PENDING만 보면 되니까 이거 하나만 갱신해도 충분 | ||
| await qc.invalidateQueries({ | ||
| queryKey: clubMemberQueryKeys.members(variables.clubId, "PENDING"), | ||
| }); | ||
| }, |
There was a problem hiding this comment.
useUpdateClubMemberStatusMutation의 onSuccess 핸들러가 PENDING 상태의 쿼리만 무효화하도록 구현되어 있어 '가입 신청 관리' 페이지에 강하게 결합되어 있습니다. 이로 인해 다른 페이지(예: '모임 회원 관리' 페이지)에서 이 훅을 재사용하기 어렵고, 해당 페이지에서는 수동으로 refetch를 호출해야만 데이터가 갱신됩니다.
onSuccess 핸들러를 더 범용적으로 만드는 것이 좋습니다. 예를 들어, clubMemberQueryKeys.members(variables.clubId)와 같이 상위 쿼리 키를 무효화하여 모든 상태 필터에 대한 쿼리가 갱신되도록 할 수 있습니다. 이렇게 하면 훅의 재사용성이 높아지고 예기치 않은 데이터 불일치 문제를 방지할 수 있습니다.
onSuccess: async (_data, variables) => {
// PENDING, ALL 등 관련된 모든 멤버 목록 쿼리를 무효화합니다.
await qc.invalidateQueries({
queryKey: clubMemberQueryKeys.members(variables.clubId, "ALL"),
});
await qc.invalidateQueries({
queryKey: clubMemberQueryKeys.members(variables.clubId, "PENDING"),
});
},There was a problem hiding this comment.
Pull request overview
Adds client-side support (types, services, React Query hooks, and pages) for the “모임 홈화면” and “모임 관리(가입 신청/회원 관리/모임 수정)” API endpoints described in the PR.
Changes:
- Added new API types and endpoints for club home, latest notice, next meeting, member management, and admin club detail/update.
- Implemented services + React Query hooks for fetching/updating club home/admin/member data.
- Updated group detail/admin pages to replace dummy data with real API-backed UI flows (including new “모임 수정” route).
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| src/types/groups/grouphome.ts | Defines response/result types for club home-related endpoints (and adds a category-code mapping). |
| src/types/groups/clubMembers.ts | Adds member list/status/command request types for admin member management. |
| src/types/groups/clubAdminEdit.ts | Adds types for admin club detail + update payload/response. |
| src/services/clubService.ts | Extends club service with new home/admin endpoints and “latest notice/next meeting” helpers. |
| src/services/clubMemberService.ts | Adds service for member list and member status patch API. |
| src/lib/api/endpoints/Clubs.ts | Adds new club endpoints (me, home, latestNotice, nextMeeting, members, member, detail, update). |
| src/hooks/queries/useClubhomeQueries.ts | Adds React Query hooks to fetch “me/home/latestNotice/nextMeeting” for club detail screen. |
| src/hooks/queries/useClubMemberQueries.ts | Adds infinite-query hook for club members list. |
| src/hooks/queries/useClubAdminEditQueries.ts | Adds query hook for admin club detail. |
| src/hooks/mutations/useClubMemberMutations.ts | Adds mutation hook for member status updates + cache invalidation. |
| src/hooks/mutations/useClubAdminEditMutations.ts | Adds mutation hook for admin club update + cache invalidation. |
| src/components/base-ui/Group/group_admin_menu.tsx | Adds “모임 수정” navigation item to admin menu. |
| src/components/base-ui/Group-Search/search_clublist/search_clublist_item.tsx | Updates club summary category type to DTO list. |
| src/components/base-ui/Group-Search/search_clublist/search_club_category_tags.tsx | Updates category tag rendering to accept both legacy number[] and new DTO list. |
| src/app/groups/page.tsx | Adapts group list mapping to the new category DTO shape and minor cleanup. |
| src/app/groups/[id]/page.tsx | Replaces dummy club home with API-backed queries + link modal wiring. |
| src/app/groups/[id]/dummy.ts | Removes dummy data used by the group detail page. |
| src/app/groups/[id]/admin/members/page.tsx | Replaces dummy members with API-backed infinite list + PATCH actions. |
| src/app/groups/[id]/admin/edit/page.tsx | Adds admin edit page (create UI reused) wired to admin detail/update APIs (currently with TODOs). |
| src/app/groups/[id]/admin/edit/layout.tsx | Adds layout wrapper for the admin edit route. |
| src/app/groups/[id]/admin/applicant/page.tsx | Replaces dummy applicants with API-backed PENDING list + approve/reject actions. |
Comments suppressed due to low confidence (1)
src/services/clubService.ts:49
searchClubs내부에서any를 쓰고 있고, 현재 들여쓰기/블록 정렬이 깨져 있어(Prettier/린트 적용 시) CI에서 포맷 실패가 날 수 있습니다.const cleaned: Partial<ClubSearchParams> = { ...params }처럼 타입을 유지하면서 불필요한 필드만 제거하도록 정리해 주세요.
searchClubs: async (params: ClubSearchParams) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cleaned: any = { ...params };
if (cleaned.cursorId == null) delete cleaned.cursorId;
if (typeof cleaned.keyword === "string" && cleaned.keyword.trim() === "") {
delete cleaned.keyword;
}
if (cleaned.inputFilter == null) delete cleaned.inputFilter;
const res = await apiClient.get<ClubSearchResponse>(CLUBS.search, {
params: cleaned,
});
return res.result;
},
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| profileImageUrl: string; | ||
| region: string; |
There was a problem hiding this comment.
profileImageUrl/region를 non-null string으로 바꿨는데, 다른 DTO들(예: ClubDTO.profileImageUrl: string | null) 및 UI 코드에서는 null 가능성을 전제로 처리하고 있습니다. 실제 API가 null을 줄 수 있다면 런타임에서는 null이 오는데 타입이 이를 막아 잘못된 가정이 생기니, 응답 스펙에 맞게 string | null로 맞추는 게 좋습니다.
| profileImageUrl: string; | |
| region: string; | |
| profileImageUrl: string | null; | |
| region: string | null; |
| export const CLUB_CATEGORY_CODE_TO_NUM: Record<string, number> = { | ||
| TRAVEL: 1, | ||
| FOREIGN_LANGUAGE: 2, | ||
| CHILD_TEEN: 3, | ||
| RELIGION_PHILOSOPHY: 4, | ||
| FICTION_POETRY_DRAMA: 5, | ||
| ESSAY: 6, | ||
| HUMANITIES: 7, | ||
| SCIENCE: 8, | ||
| COMPUTER_IT: 9, | ||
| ECONOMY_BUSINESS: 10, | ||
| SELF_IMPROVEMENT: 11, | ||
| SOCIAL_SCIENCE: 12, | ||
| POLITICS_DIPLOMACY_DEFENSE: 13, | ||
| HISTORY_CULTURE: 14, | ||
| ART_POP_CULTURE: 15, |
There was a problem hiding this comment.
CLUB_CATEGORY_CODE_TO_NUM의 키가 clubCreate.ts의 ClubCategoryCode(예: CHILDREN_BOOKS, ECONOMY_MANAGEMENT, SELF_DEVELOPMENT)와 다릅니다(CHILD_TEEN, ECONOMY_BUSINESS, SELF_IMPROVEMENT). 이 매핑을 실제로 사용할 경우 변환이 실패할 수 있으니, 백엔드 enum 값/프론트 공용 enum과 동일한 코드명으로 통일하거나, 정확한 매핑 규칙을 근거와 함께 맞춰 주세요.
| export type ClubAdminDetail = { | ||
| clubId: number; | ||
| name: string; | ||
| description: string; | ||
| profileImageUrl: string; | ||
| region: string; | ||
| category: CodeDescItem[]; // [{code, description}] | ||
| participantTypes: CodeDescItem[]; // [{code, description}] | ||
| links: ClubLinkItem[]; // [{link, label}] | ||
| open: boolean; | ||
| }; | ||
|
|
||
| export type ClubAdminDetailResponse = ApiResponse<ClubAdminDetail>; | ||
|
|
||
| /** | ||
| * [운영진] 독서 모임 정보 수정 PUT /api/clubs/{clubId} | ||
| */ | ||
| export type UpdateClubAdminRequest = { | ||
| name: string; | ||
| description: string; | ||
| profileImageUrl: string; | ||
| open: boolean; | ||
| region: string; | ||
| category: string[]; | ||
| participantTypes: string[]; | ||
| links: ClubLinkItem[]; | ||
| }; |
There was a problem hiding this comment.
ClubAdminDetail.profileImageUrl과 UpdateClubAdminRequest.profileImageUrl가 string으로 고정되어 있는데, 생성 요청(CreateClubRequest)에서는 string | null로 정의되어 있고(edit 페이지에서도 default 프로필 선택 시 null을 보내도록 구현되어 있음) 타입/동작이 충돌합니다. API가 null을 허용한다면 여기 타입도 string | null로 맞추고, 허용하지 않는다면 edit 페이지에서 null 대신 기본 이미지 URL(또는 기존 값 유지)을 보내도록 수정해야 합니다.
| getLatestNotice: async (clubId: number) => { | ||
| try { | ||
| const res = await apiClient.get<LatestNoticeResponse>(CLUBS.latestNotice(clubId)); | ||
| return res.result; | ||
| } catch (e: any) { | ||
| const msg = e?.message ?? ""; | ||
| if ( | ||
| msg.includes("공지") && (msg.includes("없") || msg.includes("존재하지")) | ||
| ) { | ||
| return null; | ||
| } | ||
| throw e; | ||
| } |
There was a problem hiding this comment.
catch (e: any)는 @typescript-eslint/no-explicit-any 규칙에 걸릴 가능성이 큽니다(같은 파일에서 이미 해당 룰을 disable하고 있음). catch (e: unknown)로 받고 e instanceof ApiError로 좁혀서 e.code/e.message를 사용하도록 바꾸는 게 안전합니다.
| getLatestNotice: async (clubId: number) => { | ||
| try { | ||
| const res = await apiClient.get<LatestNoticeResponse>(CLUBS.latestNotice(clubId)); | ||
| return res.result; | ||
| } catch (e: any) { | ||
| const msg = e?.message ?? ""; | ||
| if ( | ||
| msg.includes("공지") && (msg.includes("없") || msg.includes("존재하지")) | ||
| ) { | ||
| return null; | ||
| } | ||
| throw e; | ||
| } | ||
| }, | ||
|
|
||
| getNextMeeting: async (clubId: number) => { | ||
| try { | ||
| const res = await apiClient.get<NextMeetingResponse>(CLUBS.nextMeeting(clubId)); | ||
| return res.result; | ||
| } catch (e: any) { | ||
| const msg = e?.message ?? ""; | ||
| if (msg.includes("다음 정기모임이 존재하지 않습니다")) { | ||
| return null; | ||
| } | ||
| if (msg.includes("정기모임") && (msg.includes("없") || msg.includes("존재하지"))) { | ||
| return null; | ||
| } | ||
| throw e; | ||
| } |
There was a problem hiding this comment.
getLatestNotice/getNextMeeting에서 “공지/정기모임 없음”을 에러 메시지 한글 포함 여부로 판별하고 있는데, 메시지 문구가 바뀌면 바로 오동작합니다. apiClient가 ApiError에 code를 넣어주고 있으니(HTTP404 등), 가능하면 e instanceof ApiError + e.code(또는 e.response?.code) 기반으로 분기하도록 바꾸는 게 유지보수에 유리합니다.
| import Link from 'next/link'; | ||
| import { useRouter, useParams } from 'next/navigation'; | ||
| import Image from "next/image"; | ||
| import Link from "next/link"; |
There was a problem hiding this comment.
현재 Link를 import 했지만 JSX에서는 사용하지 않습니다. next/link import는 제거하거나, 실제로 <Link>로 감싸서 사용할지 중 하나로 정리하지 않으면 린트(no-unused-vars) 에러가 날 수 있습니다.
| import Link from "next/link"; |
|
|
||
| const noticeText = latestNotice?.title ?? "공지사항이 없습니다."; | ||
| const hasNotice = Boolean(latestNotice?.id); | ||
| const noticeUrl = `/groups/${groupId}/notice`; |
There was a problem hiding this comment.
const noticeUrl = ...를 선언했지만 실제 이동은 하드코딩된 문자열로 router.push를 호출하고 있어 noticeUrl이 미사용 변수입니다. 선언을 제거하거나, 클릭/키다운 핸들러에서 noticeUrl을 사용하도록 통일해 주세요.
| const noticeUrl = `/groups/${groupId}/notice`; |
| const onCheckName = async () => { | ||
| const name = clubName.trim(); | ||
| if (!name) return; | ||
|
|
||
| // ✅ 이름이 원래랑 같으면 그냥 통과 | ||
| if (name === initialName) { | ||
| setNameCheck("available"); | ||
| toast.success("사용 가능한 모임 이름입니다."); | ||
| return; | ||
| } | ||
|
|
||
| // 여기서 실제 중복체크 훅이 있다면 create 페이지처럼 refetch하면 됨. | ||
| // 지금은 “UI 동일”이 목표라, 최소 동작만(나중에 hook 연결) 형태로 둠. | ||
| setNameCheck("checking"); | ||
| try { | ||
| // TODO: useClubNameCheckQuery(name).refetch() 연결 | ||
| // 임시: 무조건 available 처리 | ||
| setNameCheck("available"); | ||
| toast.success("사용 가능한 모임 이름입니다."); | ||
| } catch { | ||
| setNameCheck("idle"); | ||
| toast.error("이름 중복 확인 실패"); | ||
| } | ||
| }; |
There was a problem hiding this comment.
모임 이름 중복 확인 로직이 현재는 TODO 상태로, 이름이 바뀐 경우에도 무조건 available로 처리합니다. 이 상태로는 UI가 잘못된 성공 메시지를 보여주고, 서버에서 중복으로 실패해도 원인을 알기 어렵습니다. create 페이지처럼 useClubNameCheckQuery를 연결해 실제 API 결과에 따라 available/duplicate를 설정하도록 구현해 주세요.
| const payload = { | ||
| name: clubName.trim(), | ||
| description: clubDescription.trim(), | ||
| profileImageUrl: profileMode === "upload" ? profileImageUrl : null, | ||
| region: activityArea.trim(), | ||
| category, | ||
| participantTypes, | ||
| links: linksPayload, | ||
| open: open === true, | ||
| }; | ||
|
|
||
| await updateClub.mutateAsync(payload as any); | ||
| toast.success("모임 정보가 수정되었습니다."); |
There was a problem hiding this comment.
업데이트 payload에서 profileImageUrl에 null을 넣을 수 있는데(profileMode !== "upload"), UpdateClubAdminRequest.profileImageUrl 타입은 string이고 호출부에서 payload as any로 우회하고 있습니다. 타입을 스펙에 맞게(string | null) 정리하고, as any 캐스팅 없이 mutateAsync를 호출하도록 수정해 주세요(서버가 null 미허용이면 기본값/기존값 유지로 처리 필요).
| const endIndex = startIndex + itemsPerPage; | ||
| const currentApplicants = applicants.slice(startIndex, endIndex); | ||
| const goProfile = (nickname: string) => { | ||
| router.push(`/profile/${nickname}`); |
There was a problem hiding this comment.
프로필 페이지 이동에서 닉네임을 그대로 path에 붙이고 있어 공백/한글/특수문자 닉네임이면 라우팅이 깨질 수 있습니다. router.push(/profile/${encodeURIComponent(nickname)})처럼 인코딩해서 이동하는 쪽이 안전합니다.
| router.push(`/profile/${nickname}`); | |
| router.push(`/profile/${encodeURIComponent(nickname)}`); |
📌 개요 (Summary)
[모임 홈화면]
0. 나의 상태 조회 GET /api/clubs/{clubId}/me -> 관리자 유무
-> 넷 다 GET요청
[모임 관리]
page1 : 모임 가입 신청 관리
독서 모임 회원 관리(받아오기) : GET /api/clubs/{clubId}/members
독서 모임 회원 등급 수정(수정하기) : PATCH /api/clubs/{clubId}/members/{clubMemberId}
page2 : 모임 회원 관리
독서 모임 회원 관리(받아오기) : GET /api/clubs/{clubId}/members
독서 모임 회원 등급 수정(수정하기) : PATCH /api/clubs/{clubId}/members/{clubMemberId}
page3 : 모임 수정 (생성 페이지 사용 UI만들기)
[운영진] 독서 모임 상세 조회 GET /api/clubs/{clubId}
[운영진] 독서 모임 정보 수정 PUT /api/clubs/{clubId}
🛠️ 변경 사항 (Changes)
📸 스크린샷 (Screenshots)
(UI 변경 사항이 있다면 첨부해주세요)
✅ 체크리스트 (Checklist)
pnpm build)pnpm lint)