Repository navigation
Feat-244 : Websocket 연동 완 - #245
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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, 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
🧠 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 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. Footnotes
|
There was a problem hiding this comment.
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.
| const hasNotice = Boolean(latestNotice?.id); | ||
| const noticeUrl = `/groups/${groupId}/notice`; | ||
| const noticenumber = latestNotice?.id; | ||
| const noticeUrl = `/groups/${groupId}/notice/${noticenumber}`; |
There was a problem hiding this comment.
공지사항 URL을 생성하는 로직이 잠재적인 오류를 유발할 수 있습니다. latestNotice.id가 없는 경우 noticeUrl이 /groups/123/notice/undefined와 같은 잘못된 경로가 될 수 있습니다. hasNotice 체크로 현재는 문제가 없지만, 코드를 더 명확하고 안전하게 개선하는 것이 좋습니다.
아래와 같이 수정하면 noticeUrl은 유효한 URL이거나 null이 되어 타입 안정성이 높아지고, 이후 router.push에서 ! 단언을 사용하지 않아도 됩니다.
| 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; |
| memberId: event.senderMemberId, | ||
| nickname: event.senderNickname, | ||
| profileImageUrl: event.senderProfileImageUrl, |
There was a problem hiding this comment.
newMessage 객체의 sender 속성에 memberId를 추가하고 있습니다. 하지만 useMeetingChatsInfiniteQuery가 반환하는 기존 데이터의 sender 객체(MeetingChatSenderInfo 타입)에는 memberId가 없어 데이터 구조가 일관되지 않게 됩니다. 이로 인해 예기치 않은 동작이 발생할 수 있습니다. sender 객체에서 memberId를 제거하여 데이터 일관성을 유지해야 합니다.
| memberId: event.senderMemberId, | |
| nickname: event.senderNickname, | |
| profileImageUrl: event.senderProfileImageUrl, | |
| nickname: event.senderNickname, | |
| profileImageUrl: event.senderProfileImageUrl, |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (7)
src/components/base-ui/Bookcase/ItemMoreMenu.tsx (1)
74-89: Minor: Redundant null check ononReport.Since
showReportalready requires!!onReportto be true (line 28), the additionalonReport &&guard on line 77 is unnecessary. The button only renders whenonReportis 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.
handleOpenTopicReportandhandleOpenReviewReportare 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 onnoticeUrl.
noticeUrlis always a string (nevernullorundefined) 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 justmember.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
finallyblock 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. AddingmaxLength={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.
TopicsQueryDataandChatPageDataalready drift from the actual cache contracts, andChatPageData.senderaddsmemberIdeven thoughsrc/types/groups/meetingChats.ts:8-22does not. Keeping parallel local shapes here makes it easy for realtime cache writes to diverge from whatuseMeetingTopicsQuery/useMeetingChatsInfiniteQueryreaders 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
⛔ Files ignored due to path filters (2)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlpublic/Lockicon.svgis excluded by!**/*.svg
📒 Files selected for processing (21)
package.jsonsrc/app/groups/[id]/admin/members/page.tsxsrc/app/groups/[id]/bookcase/[bookId]/meeting/page.tsxsrc/app/groups/[id]/bookcase/[bookId]/page.tsxsrc/app/groups/[id]/page.tsxsrc/components/base-ui/Bookcase/BookcaseCard.tsxsrc/components/base-ui/Bookcase/ItemMoreMenu.tsxsrc/components/base-ui/Bookcase/MeetingChatModal.tsxsrc/components/base-ui/Bookcase/MeetingTeamMemberPopover.tsxsrc/components/base-ui/Bookcase/bookid/ReviewList.tsxsrc/components/base-ui/Bookcase/bookid/TeamMemberItem.tsxsrc/components/base-ui/Bookcase/bookid/TeamSection.tsxsrc/components/base-ui/Group-Search/search_club_apply_modal.tsxsrc/components/base-ui/Group-Search/search_clublist/search_clublist_item.tsxsrc/components/base-ui/Group/DebateList.tsxsrc/components/base-ui/Group/notice_detail.tsxsrc/hooks/realtime/useMeetingRealtime.tssrc/lib/socket/createMeetingStompClient.tssrc/lib/socket/meetingRealtimeCache.tssrc/lib/socket/meetingRealtimeDestinations.tssrc/types/groups/meetingRealtime.ts
💤 Files with no reviewable changes (1)
- src/components/base-ui/Bookcase/bookid/TeamSection.tsx
| 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 || "신고 접수에 실패했습니다."); | ||
| } | ||
| }; |
There was a problem hiding this comment.
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.
| 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.
| const noticenumber = latestNotice?.id; | ||
| const noticeUrl = `/groups/${groupId}/notice/${noticenumber}`; |
There was a problem hiding this comment.
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 definedOr 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.
| 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.
| onClick={onTopicClick} | ||
| onKeyDown={(e) => { | ||
| if (!onMeetingClick) return; | ||
| if (!onTopicClick) return; | ||
| if (e.key === "Enter" || e.key === " ") { | ||
| e.preventDefault(); | ||
| onMeetingClick(); | ||
| onTopicClick(); |
There was a problem hiding this comment.
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.
| 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.
| onSendMessage: (content: string) => void; | ||
| isSocketConnected: boolean; | ||
| canSendMessage: boolean; |
There was a problem hiding this comment.
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.
| 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, | ||
| }); | ||
| } | ||
| ); |
There was a problem hiding this comment.
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.
📌 개요 (Summary)
🛠️ 변경 사항 (Changes)
📸 스크린샷 (Screenshots)
2026-03-26.124301.mp4
✅ 체크리스트 (Checklist)
pnpm build)pnpm lint)Summary by CodeRabbit
New Features
Bug Fixes
Improvements