Skip to content

Feat-244 : Websocket 연동 완 - #245

Merged
hongik-luke merged 15 commits into
mainfrom
feat-244
Mar 26, 2026
Merged

hongik-luke merged 15 commits into
mainfrom
feat-244

Conversation

@hongik-luke

@hongik-luke hongik-luke commented Mar 26, 2026 •

Copy link
Copy Markdown
Contributor

📌 개요 (Summary)

  • 모임 상세/책장 화면에서 사용할 실시간 통신 구조를 추가했습니다.
  • STOMP 기반 실시간 처리 로직을 훅, 소켓 유틸, 캐시 관련 파일로 분리했습니다.
  • 채팅 모달, 팀원 팝오버, 팀 섹션, 페이지 진입부에 실시간 로직을 연결할 수 있도록 UI 연동을 반영했습니다.

🛠️ 변경 사항 (Changes)

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

📸 스크린샷 (Screenshots)

2026-03-26.124301.mp4

✅ 체크리스트 (Checklist)

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

Summary by CodeRabbit

  • New Features

    • Added real-time chat messaging and live presentation updates during meetings
    • Implemented member reporting functionality (previously a placeholder)
    • Added vote member display with interactive popover showing who voted
  • Bug Fixes

    • Corrected icon asset paths for consistency across UI components
  • Improvements

    • Enhanced notice navigation to link to specific notices
    • Replaced browser confirmation with modal dialog for notice deletion
    • Refined member interaction permissions and UI access controls

@vercel

vercel Bot commented Mar 26, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
check-mo Ready Ready Preview Mar 26, 2026 3:45am

@coderabbitai

coderabbitai Bot commented Mar 26, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request introduces real-time WebSocket support via STOMP messaging, integrates it into meeting chat and presentation features, enhances notice voting with member popovers, implements report functionality throughout the application, and updates various UI components and icon assets across the codebase.

Changes

Cohort / File(s) Summary
WebSocket & Realtime Infrastructure
package.json, src/lib/socket/createMeetingStompClient.ts, src/lib/socket/meetingRealtimeDestinations.ts, src/types/groups/meetingRealtime.ts
Added STOMP WebSocket dependency, created STOMP client factory with lifecycle callbacks, defined meeting-specific subscription/publication destinations, and introduced type definitions for chat messages, presentation events, and realtime errors.
Meeting Realtime Integration Hook
src/hooks/realtime/useMeetingRealtime.ts, src/lib/socket/meetingRealtimeCache.ts
Implemented useMeetingRealtime hook managing STOMP connections, subscriptions, and message/presentation publishing with permission validation; added cache utilities to update React Query with incoming presentation and chat events.
Meeting Chat & Presentation UI
src/app/groups/[id]/bookcase/[bookId]/meeting/page.tsx, src/components/base-ui/Bookcase/MeetingChatModal.tsx
Integrated realtime chat sending with connection status checks and pending-state tracking, added presentation toggle with staff/team gating and error handling, implemented chat team access restrictions for non-staff users, and enhanced input UI with enabled/disabled states.
Report Functionality
src/app/groups/[id]/bookcase/[bookId]/page.tsx, src/components/base-ui/Bookcase/ItemMoreMenu.tsx, src/components/base-ui/Bookcase/bookid/ReviewList.tsx, src/components/base-ui/Group/DebateList.tsx
Replaced report placeholder with full flow including modal, target tracking, and API submission; updated ItemMoreMenu to conditionally display report option via canReport flag; wired report handlers in review and debate lists based on manage permissions.
Notice & Vote Display
src/app/groups/[id]/page.tsx, src/components/base-ui/Group/notice_detail.tsx
Implemented vote member popover with profile navigation, replaced browser confirm with BookshelfDeleteConfirmModal for deletion, and updated notice navigation URL to include specific notice ID.
Bookcase & Component Updates
src/components/base-ui/Bookcase/BookcaseCard.tsx, src/components/base-ui/Bookcase/bookid/TeamMemberItem.tsx, src/components/base-ui/Bookcase/bookid/TeamSection.tsx, src/components/base-ui/Bookcase/MeetingTeamMemberPopover.tsx
Changed BookcaseCard click handler from onMeetingClick to onTopicClick, converted TeamMemberItem to navigational Link component with profile routing, removed "more" button handler, and added border radius styling to member popover items.
Icon & Path Updates
src/app/groups/[id]/admin/members/page.tsx, src/components/base-ui/Group-Search/search_club_apply_modal.tsx, src/components/base-ui/Group-Search/search_clublist/search_clublist_item.tsx
Updated SVG icon paths: /logout.svg → /Logout.svg, /lock.svg → /Lockicon.svg in member admin, group search modal, and club list components.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Meeting Page
    participant Hook as useMeetingRealtime
    participant STOMP as STOMP Broker
    participant RQ as React Query Cache
    
    Client->>Hook: Initialize with presentation/chat params
    Hook->>STOMP: Create & connect Client
    STOMP-->>Hook: onConnect (frame)
    Hook->>Hook: Subscribe to chat & presentation topics
    Hook->>RQ: Invalidate related caches
    
    Client->>Hook: publishChatMessage(teamId, content)
    Hook->>Hook: Validate connection & content
    Hook->>STOMP: Publish to /pub/...chat/message
    STOMP-->>Hook: Message delivered
    Hook->>RQ: appendChatMessageToChatsCache
    RQ-->>Client: Update cached chats (InfiniteData)
    
    Client->>Hook: publishPresentation(teamId, topicId, isSelected)
    Hook->>Hook: Validate staff/team permission
    Hook->>STOMP: Publish to /pub/.../presentation
    STOMP-->>Hook: Presentation event received
    Hook->>RQ: applyPresentationEventToTopicsCache
    RQ-->>Client: Update topic isSelected
    Hook->>Client: Invoke onPresentationEvent callback
    
    STOMP-->>Hook: Incoming chat/presentation messages
    Hook->>Hook: Parse JSON event
    Hook->>RQ: Apply to appropriate cache
    RQ-->>Client: UI reflects realtime updates
    
    Client->>Hook: Component unmount
    Hook->>STOMP: Deactivate client & cleanup subscriptions
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • #232: Directly overlaps on meeting realtime/chat stack, sharing changes to meeting services, hooks, types, and MeetingChatModal integrations
  • #220: Related through notice/comment/report surfaces, vote/comment UIs, and report service/type modifications affecting the same features
  • #154: Shares extensive changes to meeting/bookcase and members files and types that form complementary infrastructure modifications

Suggested labels

✨ feat, 📦 build

Poem

🐰 Whiskers twitch with realtime delight,
STOMP messages flow through the digital night,
Chats bounce and presentations dance free,
While reports and votes bloom like clover, you see!
A websocket dream, now perfectly done,
The meeting hall hops with newfound fun!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 PR title 'Feat-244 : Websocket 연동 완' refers directly to a core component of this changeset—websocket integration—which is a substantial and primary feature across multiple files including the new STOMP client, realtime hook, cache utilities, type definitions, and integration into meeting/bookcase pages.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-244

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
Copy Markdown
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!

이 Pull Request는 애플리케이션에 실시간 통신 기능을 통합하여 사용자 상호작용을 대폭 향상시키는 것을 목표로 합니다. 특히 모임 상세 및 책장 관련 기능에서 실시간 채팅과 발제 선택을 가능하게 하여 동적인 환경을 제공합니다. 또한, 콘텐츠 관리 및 사용자 피드백을 위한 신고 기능을 도입하고, 공지사항 관리 기능을 개선하여 전반적인 시스템의 안정성과 사용성을 강화했습니다.

Highlights

  • 실시간 통신 기능 추가: 모임 상세 및 책장 화면에 STOMP 기반의 실시간 통신 구조를 도입하여 채팅 및 발제 선택 등의 기능을 실시간으로 처리할 수 있도록 구현했습니다.
  • UI 연동 및 사용자 경험 개선: 채팅 모달, 팀원 팝오버, 팀 섹션 등 다양한 UI 요소에 실시간 로직을 연동하고, 채팅 입력 시 메시지 전송 및 발제 선택 시 즉각적인 반영을 위한 로딩 상태를 추가하여 사용자 경험을 개선했습니다.
  • 신고 기능 구현: 발제 및 한줄평에 대한 신고 기능을 추가하여 부적절한 콘텐츠를 관리할 수 있도록 했습니다. 운영진이거나 본인 작성 글은 신고할 수 없도록 제한했습니다.
  • 공지사항 상세 페이지 개선: 공지사항 상세 페이지에 투표 참여자 목록을 보여주는 팝오버를 추가하고, 공지사항 삭제 시 확인 모달을 도입하여 사용자 편의성과 안정성을 높였습니다.
  • 파일 및 경로 리팩토링: 아이콘 파일명을 Lock.svg에서 Lockicon.svg로 변경하고, 관련 참조 경로를 업데이트했습니다. 또한 TeamMemberItem 컴포넌트를 Link로 변경하여 프로필 페이지로의 이동을 개선했습니다.

🧠 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.

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.

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. ↩

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces real-time communication features for meeting pages using STOMP WebSockets, allowing users to send chat messages and toggle presentation topics in real-time. It includes a new useMeetingRealtime hook, STOMP client utilities, and cache update functions. The chat functionality now restricts non-staff members to only chat within their own team. Additionally, a report modal for topics and reviews has been added, and the notice detail page has been refactored to include a VoteMemberPopover for displaying voters. Various UI components have been updated for consistency, such as icon paths and ItemMoreMenu logic. Review comments point out a potential issue with notice URL generation if latestNotice.id is null, and an inconsistency in the sender object's memberId field within the real-time chat cache.

Comment on lines 69 to +71
const hasNotice = Boolean(latestNotice?.id);
const noticeUrl = `/groups/${groupId}/notice`;
const noticenumber = latestNotice?.id;
const noticeUrl = `/groups/${groupId}/notice/${noticenumber}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

공지사항 URL을 생성하는 로직이 잠재적인 오류를 유발할 수 있습니다. latestNotice.id가 없는 경우 noticeUrl이 /groups/123/notice/undefined와 같은 잘못된 경로가 될 수 있습니다. hasNotice 체크로 현재는 문제가 없지만, 코드를 더 명확하고 안전하게 개선하는 것이 좋습니다.

아래와 같이 수정하면 noticeUrl은 유효한 URL이거나 null이 되어 타입 안정성이 높아지고, 이후 router.push에서 ! 단언을 사용하지 않아도 됩니다.

Suggested change
const hasNotice = Boolean(latestNotice?.id);
const noticeUrl = `/groups/${groupId}/notice`;
const noticenumber = latestNotice?.id;
const noticeUrl = `/groups/${groupId}/notice/${noticenumber}`;
const noticeUrl = latestNotice?.id ? `/groups/${groupId}/notice/${latestNotice.id}` : null;
const hasNotice = !!noticeUrl;

Comment on lines +92 to +94
memberId: event.senderMemberId,
nickname: event.senderNickname,
profileImageUrl: event.senderProfileImageUrl,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

newMessage 객체의 sender 속성에 memberId를 추가하고 있습니다. 하지만 useMeetingChatsInfiniteQuery가 반환하는 기존 데이터의 sender 객체(MeetingChatSenderInfo 타입)에는 memberId가 없어 데이터 구조가 일관되지 않게 됩니다. 이로 인해 예기치 않은 동작이 발생할 수 있습니다. sender 객체에서 memberId를 제거하여 데이터 일관성을 유지해야 합니다.

Suggested change
memberId: event.senderMemberId,
nickname: event.senderNickname,
profileImageUrl: event.senderProfileImageUrl,
nickname: event.senderNickname,
profileImageUrl: event.senderProfileImageUrl,

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (7)
src/components/base-ui/Bookcase/ItemMoreMenu.tsx (1)

74-89: Minor: Redundant null check on onReport.

Since showReport already requires !!onReport to be true (line 28), the additional onReport && guard on line 77 is unnecessary. The button only renders when onReport is guaranteed to exist.

🔧 Suggested simplification
             <button
               type="button"
-              onClick={() => onReport && handle(onReport)}
+              onClick={() => handle(onReport!)}
               className="h-[44px] w-[137px] flex items-center gap-[10px] justify-center px-5 py-[10px] body_1_2 text-Gray-4 hover:text-Gray-7 cursor-pointer"
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Bookcase/ItemMoreMenu.tsx` around lines 74 - 89, The
button's onClick contains a redundant guard "onReport &&" because showReport is
only true when !!onReport is true; in ItemMoreMenu remove the extra null-check
and call handle(onReport) directly (i.e., change the onClick to invoke handle
with onReport), keeping the existing showReport conditional and preserving
handle/onReport names so behavior remains identical without the needless guard.
src/app/groups/[id]/bookcase/[bookId]/page.tsx (1)

197-231: Consider extracting shared report-opening logic.

handleOpenTopicReport and handleOpenReviewReport are nearly identical—they differ only in the source array and error message. A helper function could reduce duplication.

♻️ Optional refactor to reduce duplication
+  const handleOpenReport = <T extends { id: number | string; name: string; isAuthor: boolean }>(
+    id: number | string,
+    items: T[],
+    itemLabel: string
+  ) => {
+    const target = items.find((item) => String(item.id) === String(id));
+
+    if (!target) {
+      toast.error(`신고할 ${itemLabel}을(를) 찾을 수 없습니다.`);
+      return;
+    }
+
+    if (isStaff || target.isAuthor) {
+      toast.error("신고할 수 없는 항목입니다.");
+      return;
+    }
+
+    setReportTarget({ reportedMemberNickname: target.name });
+  };
+
-  const handleOpenTopicReport = (id: number | string) => { ... };
-  const handleOpenReviewReport = (id: number | string) => { ... };
+  const handleOpenTopicReport = (id: number | string) => handleOpenReport(id, topicItems, "발제");
+  const handleOpenReviewReport = (id: number | string) => handleOpenReport(id, reviewItems, "한줄평");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/groups/`[id]/bookcase/[bookId]/page.tsx around lines 197 - 231, The
two functions handleOpenTopicReport and handleOpenReviewReport duplicate the
same lookup, validation, toast messages and setReportTarget logic; extract a
helper like handleOpenReport(items: Array, notFoundMessage: string) that accepts
the source array (topicItems or reviewItems) and the specific "not found"
message, performs the id lookup (String(item.id) === String(id)), checks isStaff
and item.isAuthor, calls toast.error for failures, and calls setReportTarget({
reportedMemberNickname: target.name }) on success; then replace
handleOpenTopicReport and handleOpenReviewReport to call this helper with the
appropriate array and message to remove duplication while keeping existing
behavior.
src/app/groups/[id]/page.tsx (1)

120-120: Unnecessary non-null assertion on noticeUrl.

noticeUrl is always a string (never null or undefined) because it's assigned from a template literal. The ! assertion is misleading here - it suggests the value could be nullish when it cannot.

If you keep the current URL construction, remove the assertions:

- router.push(noticeUrl!);
+ router.push(noticeUrl);

Also applies to: 129-129

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

In `@src/app/groups/`[id]/page.tsx at line 120, The non-null assertion on
noticeUrl is unnecessary because noticeUrl is constructed as a template literal
and cannot be nullish; remove the exclamation mark so calls like
router.push(noticeUrl!) become router.push(noticeUrl). Update both occurrences
referenced (the push at router.push(noticeUrl!) and the similar call at the
second location) to pass noticeUrl directly and eliminate the misleading `!`
assertion.
src/components/base-ui/Group/notice_detail.tsx (2)

64-72: Consider using a more robust key if duplicate nicknames are possible.

The key ${member.nickname}-${index} works well in most cases. However, if nicknames are guaranteed unique per vote option, you could simplify to just member.nickname. If not guaranteed, the current approach with index is correct.

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

In `@src/components/base-ui/Group/notice_detail.tsx` around lines 64 - 72, The
current useMemo building normalizedMembers uses key:
`${member.nickname}-${index}`, which can be fragile if nicknames may duplicate;
update the key in the normalizedMembers mapping to use a stable unique
identifier from the member object (e.g., member.id or member.userId) instead of
relying on nickname or array index, e.g., key: member.id (falling back to
`${member.nickname}-${index}` only if no unique id exists), leaving
normalizeProfileSrc and the rest of useMemo unchanged.

282-296: Consider keeping the modal open on delete failure.

The finally block closes the modal regardless of success or failure. If deletion fails, the user sees an error toast but the modal disappears, which could be confusing. Consider closing the modal only on success:

♻️ Proposed change
     try {
       await deleteNotice({ clubId, noticeId });
       toast.success('공지사항이 삭제되었습니다.');
       router.push(`/groups/${clubId}/notice`);
+      setIsDeleteModalOpen(false);
     } catch (e: any) {
       const msg = e?.message ?? '';
       toast.error(msg || '공지사항 삭제에 실패했습니다.');
-    } finally {
-      setIsDeleteModalOpen(false);
-      setMenuOpen(false);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Group/notice_detail.tsx` around lines 282 - 296, The
handleDeleteNotice function currently closes the delete modal and menu in the
finally block, which hides the modal even when deleteNotice fails; update it so
setIsDeleteModalOpen(false) and setMenuOpen(false) are only called after a
successful delete (i.e., move those calls into the try block after await
deleteNotice(...) and toast.success(...)); leave the catch block to show the
error toast but keep the modal open so the user can retry or inspect; keep the
early-return guard (if (!isAdmin || isDeletePending) return) and any
isDeletePending state handling intact.
src/components/base-ui/Bookcase/MeetingChatModal.tsx (1)

332-345: Mirror the 400-character limit in the input.

useMeetingRealtime.publishChatMessage() already rejects messages over 400 chars, but this field allows typing past that limit and only fails at submit time. Adding maxLength={400} would make the constraint visible earlier and avoid a toast-only validation path.

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

In `@src/components/base-ui/Bookcase/MeetingChatModal.tsx` around lines 332 - 345,
The text input in MeetingChatModal (the controlled value `draft` updated by
`setDraft`) allows typing beyond the 400-char limit enforced by
`useMeetingRealtime.publishChatMessage()`, causing submit-time rejection; add a
maxLength={400} attribute to the input element (the same <input> that uses
`value={draft}`, `onChange={(e) => setDraft(e.target.value)}`, and
`onKeyDown={handleKeyDown}`) so users cannot type past 400 characters and see
the constraint earlier; ensure any related UI/disabled logic in `handleSubmit`
and the submit button remains unchanged to preserve existing validation and
toast behavior.
src/lib/socket/meetingRealtimeCache.ts (1)

7-32: Reuse the shared query response types here.

TopicsQueryData and ChatPageData already drift from the actual cache contracts, and ChatPageData.sender adds memberId even though src/types/groups/meetingChats.ts:8-22 does not. Keeping parallel local shapes here makes it easy for realtime cache writes to diverge from what useMeetingTopicsQuery / useMeetingChatsInfiniteQuery readers expect.

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

In `@src/lib/socket/meetingRealtimeCache.ts` around lines 7 - 32, The local types
TopicsQueryData and ChatPageData in meetingRealtimeCache.ts diverge from the
shared query response types and introduce an extra sender.memberId; replace
these local type definitions by importing and reusing the canonical response
types used by the hooks (the types consumed by useMeetingTopicsQuery and
useMeetingChatsInfiniteQuery) so the realtime cache writes match reader
expectations, and remove the incorrect memberId field from the sender shape (or
map to the canonical sender type) to ensure shape parity.
🤖 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/groups/`[id]/bookcase/[bookId]/page.tsx:
- Around line 233-249: The modal stays open because handleSubmitReport never
clears reportTarget after a successful report; update handleSubmitReport to call
setReportTarget(null) (or the appropriate state-clearing action) immediately
after await reportMember(...) succeeds (before or after toast.success) so the
modal closes; reference handleSubmitReport, reportMember, reportTarget, and
setReportTarget to locate and update the logic.

In `@src/app/groups/`[id]/page.tsx:
- Around line 70-71: latestNotice may be null so building noticeUrl
unconditionally yields a "/.../undefined" URL; change the code to only construct
the URL when latestNotice exists (use latestNotice?.id or guard with if
(hasNotice) before creating noticeUrl) and update the click handlers to use the
guarded value (or inline the template
`/groups/${groupId}/notice/${latestNotice.id}` inside the handlers behind the
hasNotice check) — reference latestNotice, noticenumber, noticeUrl, hasNotice
and groupId when making this change.

In `@src/components/base-ui/Bookcase/BookcaseCard.tsx`:
- Around line 49-54: The card's primary activation handlers in BookcaseCard are
wired to onTopicClick but should call the meeting handler; update the onClick
and the keyboard handler inside BookcaseCard (the onClick prop and the onKeyDown
callback that currently call onTopicClick) to call onMeetingClick when present
(fall back to onTopicClick only if onMeetingClick is absent), and ensure you
guard for undefined handlers before calling them so click and Enter/Space invoke
onMeetingClick as intended.

In `@src/components/base-ui/Bookcase/MeetingChatModal.tsx`:
- Around line 21-23: Update the onSendMessage contract so failures can be
detected and the draft is only cleared on success: change the prop type
onSendMessage to return a Promise (e.g. Promise<boolean> or Promise<void> that
rejects on failure) and update MeetingChatModal's submit handler (handleSubmit)
to await the promise and only clear the draft when the awaited call succeeds;
also propagate or rethrow errors from onSendMessage instead of swallowing them
so transient socket/server errors preserve the composed message. Ensure all
callers of onSendMessage are updated to return a Promise and to reject on
failure.

In `@src/hooks/realtime/useMeetingRealtime.ts`:
- Around line 143-159: The subscription assigned to
subscriptionsRef.current.error (client.subscribe to
meetingRealtimeDestinations.userErrorQueue) currently only logs frames; change
it to propagate parsed MeetingRealtimeErrorBody frames to the caller by invoking
the hook's exposed error callback or setter (e.g., call a provided onError or
setRealtimeError state from useMeetingRealtime) instead of console.error so the
page-level pending state can clear and display the rejection; ensure you still
parse safely (current JSON.parse try/catch) and pass along headers, body, and
parsed payload to the callback.

---

Nitpick comments:
In `@src/app/groups/`[id]/bookcase/[bookId]/page.tsx:
- Around line 197-231: The two functions handleOpenTopicReport and
handleOpenReviewReport duplicate the same lookup, validation, toast messages and
setReportTarget logic; extract a helper like handleOpenReport(items: Array,
notFoundMessage: string) that accepts the source array (topicItems or
reviewItems) and the specific "not found" message, performs the id lookup
(String(item.id) === String(id)), checks isStaff and item.isAuthor, calls
toast.error for failures, and calls setReportTarget({ reportedMemberNickname:
target.name }) on success; then replace handleOpenTopicReport and
handleOpenReviewReport to call this helper with the appropriate array and
message to remove duplication while keeping existing behavior.

In `@src/app/groups/`[id]/page.tsx:
- Line 120: The non-null assertion on noticeUrl is unnecessary because noticeUrl
is constructed as a template literal and cannot be nullish; remove the
exclamation mark so calls like router.push(noticeUrl!) become
router.push(noticeUrl). Update both occurrences referenced (the push at
router.push(noticeUrl!) and the similar call at the second location) to pass
noticeUrl directly and eliminate the misleading `!` assertion.

In `@src/components/base-ui/Bookcase/ItemMoreMenu.tsx`:
- Around line 74-89: The button's onClick contains a redundant guard "onReport
&&" because showReport is only true when !!onReport is true; in ItemMoreMenu
remove the extra null-check and call handle(onReport) directly (i.e., change the
onClick to invoke handle with onReport), keeping the existing showReport
conditional and preserving handle/onReport names so behavior remains identical
without the needless guard.

In `@src/components/base-ui/Bookcase/MeetingChatModal.tsx`:
- Around line 332-345: The text input in MeetingChatModal (the controlled value
`draft` updated by `setDraft`) allows typing beyond the 400-char limit enforced
by `useMeetingRealtime.publishChatMessage()`, causing submit-time rejection; add
a maxLength={400} attribute to the input element (the same <input> that uses
`value={draft}`, `onChange={(e) => setDraft(e.target.value)}`, and
`onKeyDown={handleKeyDown}`) so users cannot type past 400 characters and see
the constraint earlier; ensure any related UI/disabled logic in `handleSubmit`
and the submit button remains unchanged to preserve existing validation and
toast behavior.

In `@src/components/base-ui/Group/notice_detail.tsx`:
- Around line 64-72: The current useMemo building normalizedMembers uses key:
`${member.nickname}-${index}`, which can be fragile if nicknames may duplicate;
update the key in the normalizedMembers mapping to use a stable unique
identifier from the member object (e.g., member.id or member.userId) instead of
relying on nickname or array index, e.g., key: member.id (falling back to
`${member.nickname}-${index}` only if no unique id exists), leaving
normalizeProfileSrc and the rest of useMemo unchanged.
- Around line 282-296: The handleDeleteNotice function currently closes the
delete modal and menu in the finally block, which hides the modal even when
deleteNotice fails; update it so setIsDeleteModalOpen(false) and
setMenuOpen(false) are only called after a successful delete (i.e., move those
calls into the try block after await deleteNotice(...) and toast.success(...));
leave the catch block to show the error toast but keep the modal open so the
user can retry or inspect; keep the early-return guard (if (!isAdmin ||
isDeletePending) return) and any isDeletePending state handling intact.

In `@src/lib/socket/meetingRealtimeCache.ts`:
- Around line 7-32: The local types TopicsQueryData and ChatPageData in
meetingRealtimeCache.ts diverge from the shared query response types and
introduce an extra sender.memberId; replace these local type definitions by
importing and reusing the canonical response types used by the hooks (the types
consumed by useMeetingTopicsQuery and useMeetingChatsInfiniteQuery) so the
realtime cache writes match reader expectations, and remove the incorrect
memberId field from the sender shape (or map to the canonical sender type) to
ensure shape parity.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 34f367dc-8d56-4c0a-b341-1cb7714154d5

📥 Commits

Reviewing files that changed from the base of the PR and between 3f06b52 and 0fa21bc.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • public/Lockicon.svg is excluded by !**/*.svg
📒 Files selected for processing (21)
  • package.json
  • src/app/groups/[id]/admin/members/page.tsx
  • src/app/groups/[id]/bookcase/[bookId]/meeting/page.tsx
  • src/app/groups/[id]/bookcase/[bookId]/page.tsx
  • src/app/groups/[id]/page.tsx
  • src/components/base-ui/Bookcase/BookcaseCard.tsx
  • src/components/base-ui/Bookcase/ItemMoreMenu.tsx
  • src/components/base-ui/Bookcase/MeetingChatModal.tsx
  • src/components/base-ui/Bookcase/MeetingTeamMemberPopover.tsx
  • src/components/base-ui/Bookcase/bookid/ReviewList.tsx
  • src/components/base-ui/Bookcase/bookid/TeamMemberItem.tsx
  • src/components/base-ui/Bookcase/bookid/TeamSection.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/DebateList.tsx
  • src/components/base-ui/Group/notice_detail.tsx
  • src/hooks/realtime/useMeetingRealtime.ts
  • src/lib/socket/createMeetingStompClient.ts
  • src/lib/socket/meetingRealtimeCache.ts
  • src/lib/socket/meetingRealtimeDestinations.ts
  • src/types/groups/meetingRealtime.ts
💤 Files with no reviewable changes (1)
  • src/components/base-ui/Bookcase/bookid/TeamSection.tsx

Comment on lines +233 to +249
const handleSubmitReport = async (type: string, content: string) => {
const target = reportTarget;
if (!target) return;

try {
await reportMember({
reportedMemberNickname: target.reportedMemberNickname,
reportType: mapReportTypeToApi(type),
content,
});

toast.success("신고가 접수되었습니다.");
} catch (e: any) {
const msg = e?.message ?? "";
toast.error(msg || "신고 접수에 실패했습니다.");
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Modal remains open after successful report submission.

After reportMember succeeds, setReportTarget(null) is never called, so the modal stays visible even though the report was submitted. The user must manually close it.

🐛 Proposed fix to close modal on success
   const handleSubmitReport = async (type: string, content: string) => {
     const target = reportTarget;
     if (!target) return;
 
     try {
       await reportMember({
         reportedMemberNickname: target.reportedMemberNickname,
         reportType: mapReportTypeToApi(type),
         content,
       });
 
       toast.success("신고가 접수되었습니다.");
-    } catch (e: any) {
-      const msg = e?.message ?? "";
+      setReportTarget(null);
+    } catch (e: unknown) {
+      const msg = e instanceof Error ? e.message : "";
       toast.error(msg || "신고 접수에 실패했습니다.");
     }
   };
📝 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 handleSubmitReport = async (type: string, content: string) => {
const target = reportTarget;
if (!target) return;
try {
await reportMember({
reportedMemberNickname: target.reportedMemberNickname,
reportType: mapReportTypeToApi(type),
content,
});
toast.success("신고가 접수되었습니다.");
} catch (e: any) {
const msg = e?.message ?? "";
toast.error(msg || "신고 접수에 실패했습니다.");
}
};
const handleSubmitReport = async (type: string, content: string) => {
const target = reportTarget;
if (!target) return;
try {
await reportMember({
reportedMemberNickname: target.reportedMemberNickname,
reportType: mapReportTypeToApi(type),
content,
});
toast.success("신고가 접수되었습니다.");
setReportTarget(null);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : "";
toast.error(msg || "신고 접수에 실패했습니다.");
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/groups/`[id]/bookcase/[bookId]/page.tsx around lines 233 - 249, The
modal stays open because handleSubmitReport never clears reportTarget after a
successful report; update handleSubmitReport to call setReportTarget(null) (or
the appropriate state-clearing action) immediately after await reportMember(...)
succeeds (before or after toast.success) so the modal closes; reference
handleSubmitReport, reportMember, reportTarget, and setReportTarget to locate
and update the logic.

Comment on lines +70 to +71
const noticenumber = latestNotice?.id;
const noticeUrl = `/groups/${groupId}/notice/${noticenumber}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid constructing URL with potentially undefined notice ID.

When latestNotice is null/undefined, noticenumber becomes undefined, and noticeUrl becomes /groups/${groupId}/notice/undefined. While the hasNotice guard prevents navigation, this is a code smell.

🔧 Proposed fix - construct URL only when needed
  const hasNotice = Boolean(latestNotice?.id);
- const noticenumber = latestNotice?.id;
- const noticeUrl = `/groups/${groupId}/notice/${noticenumber}`;
+ const noticeUrl = hasNotice ? `/groups/${groupId}/notice/${latestNotice!.id}` : null;

Then update the navigation calls:

- router.push(noticeUrl!);
+ router.push(noticeUrl!);  // Safe: hasNotice check guarantees noticeUrl is defined

Or alternatively, inline the URL construction in the click handlers:

  if (!hasNotice) {
    toast.error("공지사항이 없습니다.");
    return;
  }
- router.push(noticeUrl!);
+ router.push(`/groups/${groupId}/notice/${latestNotice!.id}`);
📝 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 noticenumber = latestNotice?.id;
const noticeUrl = `/groups/${groupId}/notice/${noticenumber}`;
const noticeUrl = hasNotice ? `/groups/${groupId}/notice/${latestNotice!.id}` : null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/groups/`[id]/page.tsx around lines 70 - 71, latestNotice may be null
so building noticeUrl unconditionally yields a "/.../undefined" URL; change the
code to only construct the URL when latestNotice exists (use latestNotice?.id or
guard with if (hasNotice) before creating noticeUrl) and update the click
handlers to use the guarded value (or inline the template
`/groups/${groupId}/notice/${latestNotice.id}` inside the handlers behind the
hasNotice check) — reference latestNotice, noticenumber, noticeUrl, hasNotice
and groupId when making this change.

Comment on lines +49 to +54
onClick={onTopicClick}
onKeyDown={(e) => {
if (!onMeetingClick) return;
if (!onTopicClick) return;
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onMeetingClick();
onTopicClick();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Primary card activation is wired to the wrong callback.

Line 49 and Line 54 now invoke onTopicClick, so clicking/keyboard-activating the card opens the topic flow instead of the meeting flow. At call sites (src/components/base-ui/Group/notice_detail.tsx:323-336, src/app/groups/[id]/bookcase/page.tsx:130-143), separate handlers are passed for topic/review/meeting, so this rewiring changes user navigation behavior unexpectedly.

🔧 Suggested fix
-      onClick={onTopicClick}
+      onClick={onMeetingClick}
       onKeyDown={(e) => {
-        if (!onTopicClick) return;
+        if (!onMeetingClick) return;
         if (e.key === "Enter" || e.key === " ") {
           e.preventDefault();
-          onTopicClick();
+          onMeetingClick();
         }
       }}
📝 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
onClick={onTopicClick}
onKeyDown={(e) => {
if (!onMeetingClick) return;
if (!onTopicClick) return;
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onMeetingClick();
onTopicClick();
onClick={onMeetingClick}
onKeyDown={(e) => {
if (!onMeetingClick) return;
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onMeetingClick();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/base-ui/Bookcase/BookcaseCard.tsx` around lines 49 - 54, The
card's primary activation handlers in BookcaseCard are wired to onTopicClick but
should call the meeting handler; update the onClick and the keyboard handler
inside BookcaseCard (the onClick prop and the onKeyDown callback that currently
call onTopicClick) to call onMeetingClick when present (fall back to
onTopicClick only if onMeetingClick is absent), and ensure you guard for
undefined handlers before calling them so click and Enter/Space invoke
onMeetingClick as intended.

Comment on lines +21 to +23
onSendMessage: (content: string) => void;
isSocketConnected: boolean;
canSendMessage: boolean;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't clear the draft unless the send actually succeeds.

The current onSendMessage: (content) => void contract gives handleSubmit no way to detect failure, and the current parent handler catches publish errors before they bubble back here. A transient socket/server error will still clear the user's composed message. Return a success flag/Promise or let failures propagate so the draft is preserved on error.

Also applies to: 164-170

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

In `@src/components/base-ui/Bookcase/MeetingChatModal.tsx` around lines 21 - 23,
Update the onSendMessage contract so failures can be detected and the draft is
only cleared on success: change the prop type onSendMessage to return a Promise
(e.g. Promise<boolean> or Promise<void> that rejects on failure) and update
MeetingChatModal's submit handler (handleSubmit) to await the promise and only
clear the draft when the awaited call succeeds; also propagate or rethrow errors
from onSendMessage instead of swallowing them so transient socket/server errors
preserve the composed message. Ensure all callers of onSendMessage are updated
to return a Promise and to reject on failure.

Comment on lines +143 to +159
subscriptionsRef.current.error = client.subscribe(
meetingRealtimeDestinations.userErrorQueue,
(message) => {
let parsed: MeetingRealtimeErrorBody | null = null;
try {
parsed = message.body ? JSON.parse(message.body) : null;
} catch {
parsed = null;
}

console.error("[meeting realtime][error queue]", {
headers: message.headers,
body: message.body,
parsed,
});
}
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Propagate error-queue frames back to the caller instead of only logging them.

Server-side publish rejections will not throw from client.publish(). Right now this subscription just console.errors, so the page-level pending state in src/app/groups/[id]/bookcase/[bookId]/meeting/page.tsx never clears when a presentation update is rejected, and the toggle can stay disabled until the next reconnect/resync. Feed these frames into a callback/state update and handle the user-facing error there.

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

In `@src/hooks/realtime/useMeetingRealtime.ts` around lines 143 - 159, The
subscription assigned to subscriptionsRef.current.error (client.subscribe to
meetingRealtimeDestinations.userErrorQueue) currently only logs frames; change
it to propagate parsed MeetingRealtimeErrorBody frames to the caller by invoking
the hook's exposed error callback or setter (e.g., call a provided onError or
setRealtimeError state from useMeetingRealtime) instead of console.error so the
page-level pending state can clear and display the rejection; ensure you still
parse safely (current JSON.parse try/catch) and pass along headers, body, and
parsed payload to the callback.

@hongik-luke
hongik-luke merged commit 843bbce into main Mar 26, 2026
4 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jun 10, 2026

This branch was successfully deployed

2 active deployments
Preview – check-mo — 0fa21bc0 Deployed Mar 26, 2026 by vercel[bot]
Preview – checkmo — 0fa21bc0 Deployed Mar 26, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant