Repository navigation
책 이야기 오류 해결 최신화 - #220
책 이야기 오류 해결 최신화#220
Conversation
…t_section_bookcase"; 로 수정
[feat] 관리자 책 이야기 및 댓글 삭제 api 연결
Feat-178 모임 상세 API 연결 및 UI 전면 수정
fix: 책 이야기 상세보기 수정
📝 WalkthroughWalkthroughRefactors comment UI into capability-driven components, adds paginated club-notice comments with full CRUD and reporting, introduces notice voting and admin notice edit/delete flows, adds admin book-story deletion and admin comment management, expands API endpoints/services/types for comments, votes, reports, and member login/withdraw flows. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant NoticePage as NoticeDetailPage
participant NoticeDetail
participant VoteMutation as useVoteClubNoticeMutation
participant Service as clubNotificationService
participant API
User->>NoticePage: Navigate to notice
NoticePage->>NoticeDetail: Render with voteDetail
User->>NoticeDetail: Select option(s) & Click "투표하기"
NoticeDetail->>VoteMutation: mutate(selectedItemNumbers)
VoteMutation->>Service: voteNotice(clubId, noticeId, voteId, body)
Service->>API: POST /clubs/{clubId}/notices/{noticeId}/votes/{voteId}
API-->>Service: 200 OK
Service-->>VoteMutation: result
VoteMutation->>NoticeDetail: success callback (invalidate detail)
NoticeDetail->>User: show toast / update UI
sequenceDiagram
actor Admin
participant EditPage as EditNoticePage
participant Bookshelf as BookshelfModal
participant ImageService as imageService
participant UpdateMutation as useUpdateClubNoticeMutation
participant Service as clubNotificationService
participant API
Admin->>EditPage: Open edit page
EditPage->>EditPage: load notice detail -> populate form
Admin->>EditPage: update title/content, open Bookshelf
EditPage->>Bookshelf: fetch/select meeting/book
Admin->>EditPage: attach images
EditPage->>ImageService: uploadClubImage(newFiles)
ImageService->>API: POST /uploads
API-->>ImageService: imageUrls
EditPage->>UpdateMutation: mutate(UpdateClubNoticeRequest)
UpdateMutation->>Service: updateNotice(clubId, noticeId, body)
Service->>API: PATCH /clubs/{clubId}/notices/{noticeId}
API-->>Service: 200 OK
Service-->>UpdateMutation: result
UpdateMutation->>EditPage: success -> invalidate queries
EditPage->>Admin: toast + navigate to notice view
sequenceDiagram
participant CommentSection as CommentSectionNotice
participant Query as useClubNoticeCommentsInfiniteQuery
participant CommentItem
participant UpdateMut as useUpdateClubNoticeCommentMutation
participant DeleteMut as useDeleteClubNoticeCommentMutation
participant ReportMut as useReportMemberMutation
participant API
CommentSection->>Query: fetch page(cursor)
Query->>API: GET /comments?cursorId
API-->>Query: page (comments, hasNext, nextCursor)
Query-->>CommentSection: pages
CommentSection->>CommentItem: render with canEdit/canDelete/canReport
CommentItem->>CommentSection: onEdit(id, content)
CommentSection->>UpdateMut: mutate({commentId, body})
UpdateMut->>API: PATCH /comments/{commentId}
API-->>UpdateMut: success -> invalidate comments
CommentItem->>CommentSection: onDelete(id)
CommentSection->>CommentSection: open ConfirmModal
CommentSection->>DeleteMut: mutate({commentId})
DeleteMut->>API: DELETE /comments/{commentId}
API-->>DeleteMut: success -> invalidate comments
CommentItem->>CommentSection: onReport(id)
CommentSection->>ReportMut: mutate({reportedMemberNickname, reportType, content})
ReportMut->>API: POST /members/report
API-->>ReportMut: success
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
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! 이 PR은 책 이야기 및 클럽 공지사항 전반에 걸쳐 댓글 기능을 대폭 개선하고, 공지사항 상세 페이지의 유연성과 관리 기능을 향상시키는 데 중점을 둡니다. 기존 댓글 섹션을 리팩토링하여 완전한 CRUD(생성, 읽기, 업데이트, 삭제) 및 신고 기능을 지원하도록 변경했으며, 이를 사용자 및 관리자 인터페이스 모두에 통합했습니다. 또한, 관리자가 책 이야기를 삭제할 수 있는 기능을 도입하고, 공지사항 내용에 글자 수 제한을 추가하여 사용자 경험과 데이터 무결성을 개선했습니다. 공지사항 상세 페이지는 이제 모임 및 투표 세부 정보를 동적으로 렌더링하며, 관리자에게 공지사항 편집 및 삭제와 같은 특정 액션을 제공합니다. 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
|
| {/* 드롭다운 메뉴 */} | ||
| {menuOpen && ( | ||
| <div className="absolute right-0 top-full mt-1 w-[137px] rounded-lg bg-White shadow-lg z-10 overflow-hidden"> | ||
|
|
There was a problem hiding this comment.
CommentItem 컴포넌트의 메뉴 로직이 리팩토링되었지만, onReply 프롭이 제공되고 isReply가 false일 때도 "답글달기" 버튼이 드롭다운 메뉴에 더 이상 표시되지 않습니다. hasReplyAction 변수는 올바르게 계산되지만, 답글 옵션을 렌더링하는 데 사용되지 않습니다. 메뉴에서 댓글에 답글을 다는 기능이 여전히 필요한 경우, 이 기능을 다시 추가해야 합니다.
{hasReplyAction && (
<>
<button
type="button"
onClick={() => {
onReply?.(id);
setMenuOpen(false);
}}
className="flex w-full h-[44px] items-center justify-center gap-2 Body_1_2 text-Gray-4 hover:bg-Gray-1 cursor-pointer"
>
<Image src="/reply2.svg" alt="답글" width={24} height={24} />
답글달기
</button>
{(canDelete || canEdit || canReport) && (
<div className="mx-2 border-b border-Subbrown-4" />
)}
</>
)}
| ...(isVoteEnabled && voteDeadlineISO | ||
| ? { vote: { deadline: voteDeadlineISO } } | ||
| : {}), |
There was a problem hiding this comment.
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/base-ui/Admin/stories/bookstory_detail.tsx (1)
397-406:⚠️ Potential issue | 🟠 MajorMake the delete affordance consistent across breakpoints.
This only wires
handleDeleteinto the desktop dropdown. The mobile menu at Lines 219-231 still renders공유하기, so admins on small screens have no delete path, and the desktop entry still points at/gray_share.svg, which makes the destructive action look like share.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/base-ui/Admin/stories/bookstory_detail.tsx` around lines 397 - 406, The mobile menu and desktop dropdown are inconsistent: wire the mobile menu entry that currently renders "공유하기" to the same delete handler and icon as the desktop entry by changing its label to "삭제하기", attaching handleDelete as the onClick, and replacing the share icon src ("/gray_share.svg") with the proper delete icon (e.g., "/gray_delete.svg") so both the mobile entry and the desktop dropdown (which uses handleDelete and Image src "/gray_share.svg") present the same destructive affordance and behavior.
🧹 Nitpick comments (6)
src/hooks/queries/useClubNoticeCommentQueries.ts (1)
19-27: Minor: Simplify thecursorIdassignment.The
?? nullis redundant sincepageParamis already typed asnumber | null:- cursorId: (pageParam as number | null) ?? null, + cursorId: pageParam,Also, note that per the type definition in
src/types/clubnotification.ts,nextCursoris typed asnumber(notnumber | null). If the backend returnsnullwhen there's no next page, this could cause a type mismatch at runtime.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/queries/useClubNoticeCommentQueries.ts` around lines 19 - 27, The cursor assignment is over-verbose and the nextCursor typing may be unsafe: remove the redundant "?? null" in the queryFn so cursorId is passed directly as pageParam (pageParam is already number | null), and make getNextPageParam robust against a backend that returns null by returning lastPage.hasNext ? (lastPage.nextCursor ?? undefined) : undefined; confirm/update the GetClubNoticeCommentsResponseResult/nextCursor type in src/types/clubnotification.ts or handle its potential null at the call site (use lastPage.nextCursor ?? undefined) to avoid a runtime type mismatch.src/app/groups/[id]/notice/[noticeId]/page.tsx (1)
38-40: Consider handling the loading state formeData.The
isAdminflag is derived frommeData?.staff, but whileuseClubMeQueryis loading,meDatawill be undefined andisAdminwill befalse. This could cause the UI to briefly render without admin controls, then flicker to show them oncemeDataloads.If this causes a noticeable UX issue, consider gating the render on
meDataloading state or including it in the existing loading check.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/groups/`[id]/notice/[noticeId]/page.tsx around lines 38 - 40, The isAdmin boolean is derived from meData?.staff but does not account for the query loading state, causing a potential UI flicker; update the logic that computes or uses isAdmin to wait for useClubMeQuery's loading states (e.g., isLoading/isFetching) before deciding admin-only rendering or include the meData loading flag in the existing loading guard; locate the useClubMeQuery call and any conditional render branches that rely on isAdmin in page.tsx and gate admin controls until the query has settled to avoid temporarily showing non-admin UI.src/components/base-ui/Comment/comment_section_notice.tsx (1)
111-114: Replaceanywithunknownfor caught errors.ESLint correctly flags the use of
anyfor caught exceptions. Useunknownand narrow the type safely:♻️ Proposed fix for type-safe error handling
- } catch (e: any) { - const msg = e?.message ?? ""; + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : ""; toast.error(msg || "댓글 등록에 실패했습니다."); }Apply the same pattern to all four catch blocks (lines 111, 139, 166, 198).
Also applies to: 139-142, 166-169, 198-201
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/base-ui/Comment/comment_section_notice.tsx` around lines 111 - 114, Replace caught-error typed as "any" in the catch blocks in src/components/base-ui/Comment/comment_section_notice.tsx with "unknown" and narrow it before using .message: change each "catch (e: any)" to "catch (e: unknown)" and then derive the message by checking "if (e instanceof Error) msg = e.message; else msg = String(e ?? '')" (or similar safe narrowing) before passing to toast.error; apply this same pattern to all four catch blocks that currently use "any" so error handling is type-safe.src/components/base-ui/Comment/comment_item.tsx (1)
14-14: Note:isMineprop is declared but unused.The
isMineprop is defined inCommentItemProps(line 14) but is not included in the destructured parameters (lines 33-48). Multiple callers still pass this prop per the context snippets. If this prop is intentionally deprecated in favor ofcanEdit/canDelete/canReport, consider either:
- Removing it from the type definition, or
- Destructuring and ignoring it explicitly (e.g.,
isMine: _isMine) to signal intentional deprecation🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/base-ui/Comment/comment_item.tsx` at line 14, The prop isMine is declared on CommentItemProps but not used in the CommentItem component; either remove isMine from the CommentItemProps interface/type (and update callers to stop sending it) or explicitly destructure it in the CommentItem component signature as isMine: _isMine to mark it intentionally deprecated; locate the declaration of CommentItemProps and the CommentItem functional component (the props destructuring block) and apply one of these two fixes, also ensuring related booleans canEdit/canDelete/canReport remain unchanged.src/components/base-ui/Comment/comment_section.tsx (1)
61-69: Only subscribe to outside clicks while the menu is open.Every rendered comment item currently installs its own document-level
mousedownlistener on mount. In longer threads that means one global handler per comment even when no menu is open.♻️ Possible cleanup
useEffect(() => { + if (!menuOpen) return; + const handleClickOutside = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { setMenuOpen(false); } }; + document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); - }, []); + }, [menuOpen]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/base-ui/Comment/comment_section.tsx` around lines 61 - 69, The effect currently always attaches a document "mousedown" listener for every Comment component on mount; change the useEffect containing handleClickOutside to only add the listener when the menu is open by depending on the menuOpen state (or equivalent) and returning a cleanup that removes the listener; locate the useEffect, handleClickOutside, menuRef and setMenuOpen identifiers and update the hook so it subscribes on menuOpen === true and unsubscribes when menuOpen becomes false or component unmounts to avoid per-comment global listeners when menus are closed.src/components/base-ui/Group/notice_detail.tsx (1)
162-170: Consider adding validation for invalid date strings.If
dateStringis invalid,new Date(dateString)returns anInvalid Dateobject, and subsequent methods will returnNaN. Consider adding a guard or using a date library if invalid dates are possible.♻️ Optional: Add date validation
const formatVoteDate = (dateString: string) => { const date = new Date(dateString); + if (isNaN(date.getTime())) { + return dateString; // fallback to original string + } const year = date.getFullYear();🤖 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 162 - 170, The formatVoteDate function should validate the parsed Date before using getFullYear/getHours to avoid NaN from an Invalid Date; update formatVoteDate to check the Date (e.g., isNaN(date.getTime()) or similar) right after new Date(dateString) and return a safe fallback (empty string, original dateString, or a localized "Invalid date" message) or parse with a robust parser (e.g., date-fns parseISO) when invalid input is possible; ensure all callers of formatVoteDate handle the chosen fallback gracefully.
🤖 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]/admin/notice/[noticeId]/page.tsx:
- Around line 312-321: Change the catch param from any to unknown and narrow
before reading message: in the catch block that currently declares "e: any" (the
block using tid and toast), use "e: unknown" then check "if (e instanceof
Error)" to get e.message, otherwise derive a string via String(e) or an empty
string; keep the same pinned-word checks (isPinned, pinned, 고정) and toast/error
behavior but operate on the safely-narrowed message variable.
- Around line 85-101: Type the useClubsBookshelfSimpleInfiniteQuery hook with
the proper generic so downstream code can be strongly typed: update the hook
signature/use call to include ClubsBookshelfSimpleResult as the data generic
(same pattern as useBookshelfTopicsInfiniteQuery) in useClubsBookshelfQueries,
then remove the any annotations in the consumer - change
shelvesQuery.data?.pages.flatMap((p: any) => p.bookShelfInfoList) to use the
typed page shape and update the modalBooks mapping to accept the concrete
page/item types instead of any so modalBooks and Book properties are typed
correctly.
In `@src/components/base-ui/Admin/stories/comment_section.tsx`:
- Around line 267-274: The admin action handlers passed to CommentList
(handleEditComment, handleDeleteComment, handleReportComment) are never
reachable because CommentItem now gates its menu on per-comment capability flags
(canEdit/canDelete/canReport) that CommentList doesn't forward; update the
CommentList -> CommentItem rendering to include and forward those capability
flags for each comment (or change the CommentList API to accept a per-comment
capability resolver and use it when mapping comments) so CommentItem receives
canEdit, canDelete, and canReport for each comment and the existing handlers
will become reachable.
- Around line 36-49: The mapNode function currently returns null when a
CommentInfo has no authorInfo, which hides those comments; instead, keep the
comment by mapping it with a safe fallback for authorInfo (e.g., an explicit
placeholder or undefined) so admin UI can still display/manage it. Update
mapNode (and the mappedReplies logic) to stop returning null for !c.authorInfo,
ensure the return type remains Comment & { parentCommentId?: number | null }
(use a defensive author object or optional field), and preserve recursive
mapping of c.replies through the existing mapNode(r) chain so missing authorInfo
no longer removes comments from the tree.
In `@src/components/base-ui/Comment/comment_item.tsx`:
- Around line 173-189: The Report button is rendered unconditionally; update the
dropdown rendering in the Comment component so the "신고하기" button is only output
when canReport is true (and onReport exists). Locate the block using onReport,
id, and setMenuOpen (the Report button JSX) and wrap it in a conditional check
(e.g., if (canReport && onReport) render the button) so users cannot report when
they lack permission and the handler is present.
In `@src/components/base-ui/Comment/comment_section_bookcase.tsx`:
- Around line 184-210: handleReportComment only checks root comments and their
immediate replies, so deeper nested replies never get found; replace the inline
loop with a recursive (or iterative stack) search that walks the full comment
tree (use the same shape produced by mapApiToUiComments/sortReplies) to locate a
comment by id, then call setReportTargetNickname and setIsReportModalOpen as
before; refer to handleReportComment, comments, setReportTargetNickname, and
setIsReportModalOpen when updating the lookup logic.
In `@src/components/base-ui/Comment/comment_section_notice.tsx`:
- Around line 149-170: In handleConfirmDelete, ensure deleteTargetId is always
reset after the deletion attempt so the ConfirmModal closes: wrap the try/catch
in a try { await deleteComment(...) ... } catch { ... } finally {
setDeleteTargetId(null); } (and call any modal-close logic there if needed);
apply the same finally-based reset to the other comment-deletion handler that
currently only clears deleteTargetId in onCancel so both success and failure
paths clear the state.
In `@src/components/base-ui/Group/notice_detail.tsx`:
- Around line 148-150: The catch block currently types the error as `any` (catch
(e: any)) and immediately uses `e.message`; change the catch parameter to
`unknown` and add a type guard to extract the message safely before calling
`toast.error`. Specifically, modify the catch in notice_detail.tsx to `catch (e:
unknown)` then check `if (e && typeof e === 'object' && 'message' in e && typeof
(e as any).message === 'string') { msg = (e as { message: string }).message }
else { msg = String(e ?? '') }` (or equivalent safe extraction) and then call
`toast.error(msg || '공지사항 삭제에 실패했습니다.')`, ensuring no use of `any` for the catch
variable.
- Around line 110-130: The handleVoteSubmit handler has incorrect indentation
and an untyped catch parameter; reformat the function body to use consistent
4-space indentation and change the catch signature to catch (e: unknown) (or a
typed union) and safely extract the message (e.g., const msg = e instanceof
Error ? e.message : String(e)) before calling toast.error; update references
inside to use voteNotice, isRevoteMode, selectedOptions, clubId, noticeId, and
voteDetail.id as in the diff.
In `@src/lib/api/admin/stories.ts`:
- Around line 99-111: The helper deleteAdminBookStory currently calls await
res.json() before checking response status which breaks on 204 No Content or
non-JSON error bodies; update deleteAdminBookStory to first check res.status and
res.ok (or Content-Type header) and handle 204 by returning null/undefined (or
an appropriate empty result) without calling res.json(); for non-OK responses
read text safely (await res.text()) and throw a new Error using that text or
data.message, and only call await res.json() when the response is OK and
Content-Type is JSON to parse into DeleteAdminBookStoryResponse; apply the same
guarded pattern to the other DELETE helper(s) referenced in the file (the helper
at lines 121-139) so neither attempts JSON parse on empty or non-JSON responses.
In `@src/types/clubnotification.ts`:
- Around line 161-164: Update the ClubNoticeCommentAuthorInfo interface so
profileImageUrl is nullable: change the type of profileImageUrl on
ClubNoticeCommentAuthorInfo from string to string | null to match API behavior
and the handling in comment_section_notice.tsx and to align with
ClubNoticeVoteMember.profileImageUrl.
In `@src/types/report.ts`:
- Around line 3-8: report.ts defines a narrower ReportType and
ReportMemberRequest that conflict with member.ts; remove the duplicate types
from src/types/report.ts and have any code that used ReportMemberRequest or
ReportType import them from the canonical definitions in src/types/member.ts
(keep the full union in member.ts: ReportType and ReportMemberRequest) or, if
report.ts must remain distinct, rename the types there (e.g.,
ReportEndpointType) and update all usages to use the member.ts types where the
full set ("GENERAL" | "CLUB_MEETING" | "BOOK_STORY" | "COMMENT") is required;
specifically update imports/usages that reference ReportMemberRequest and
ReportType to point to the member.ts definitions (or rename the report.ts
symbols) so there is a single source of truth.
---
Outside diff comments:
In `@src/components/base-ui/Admin/stories/bookstory_detail.tsx`:
- Around line 397-406: The mobile menu and desktop dropdown are inconsistent:
wire the mobile menu entry that currently renders "공유하기" to the same delete
handler and icon as the desktop entry by changing its label to "삭제하기", attaching
handleDelete as the onClick, and replacing the share icon src
("/gray_share.svg") with the proper delete icon (e.g., "/gray_delete.svg") so
both the mobile entry and the desktop dropdown (which uses handleDelete and
Image src "/gray_share.svg") present the same destructive affordance and
behavior.
---
Nitpick comments:
In `@src/app/groups/`[id]/notice/[noticeId]/page.tsx:
- Around line 38-40: The isAdmin boolean is derived from meData?.staff but does
not account for the query loading state, causing a potential UI flicker; update
the logic that computes or uses isAdmin to wait for useClubMeQuery's loading
states (e.g., isLoading/isFetching) before deciding admin-only rendering or
include the meData loading flag in the existing loading guard; locate the
useClubMeQuery call and any conditional render branches that rely on isAdmin in
page.tsx and gate admin controls until the query has settled to avoid
temporarily showing non-admin UI.
In `@src/components/base-ui/Comment/comment_item.tsx`:
- Line 14: The prop isMine is declared on CommentItemProps but not used in the
CommentItem component; either remove isMine from the CommentItemProps
interface/type (and update callers to stop sending it) or explicitly destructure
it in the CommentItem component signature as isMine: _isMine to mark it
intentionally deprecated; locate the declaration of CommentItemProps and the
CommentItem functional component (the props destructuring block) and apply one
of these two fixes, also ensuring related booleans canEdit/canDelete/canReport
remain unchanged.
In `@src/components/base-ui/Comment/comment_section_notice.tsx`:
- Around line 111-114: Replace caught-error typed as "any" in the catch blocks
in src/components/base-ui/Comment/comment_section_notice.tsx with "unknown" and
narrow it before using .message: change each "catch (e: any)" to "catch (e:
unknown)" and then derive the message by checking "if (e instanceof Error) msg =
e.message; else msg = String(e ?? '')" (or similar safe narrowing) before
passing to toast.error; apply this same pattern to all four catch blocks that
currently use "any" so error handling is type-safe.
In `@src/components/base-ui/Comment/comment_section.tsx`:
- Around line 61-69: The effect currently always attaches a document "mousedown"
listener for every Comment component on mount; change the useEffect containing
handleClickOutside to only add the listener when the menu is open by depending
on the menuOpen state (or equivalent) and returning a cleanup that removes the
listener; locate the useEffect, handleClickOutside, menuRef and setMenuOpen
identifiers and update the hook so it subscribes on menuOpen === true and
unsubscribes when menuOpen becomes false or component unmounts to avoid
per-comment global listeners when menus are closed.
In `@src/components/base-ui/Group/notice_detail.tsx`:
- Around line 162-170: The formatVoteDate function should validate the parsed
Date before using getFullYear/getHours to avoid NaN from an Invalid Date; update
formatVoteDate to check the Date (e.g., isNaN(date.getTime()) or similar) right
after new Date(dateString) and return a safe fallback (empty string, original
dateString, or a localized "Invalid date" message) or parse with a robust parser
(e.g., date-fns parseISO) when invalid input is possible; ensure all callers of
formatVoteDate handle the chosen fallback gracefully.
In `@src/hooks/queries/useClubNoticeCommentQueries.ts`:
- Around line 19-27: The cursor assignment is over-verbose and the nextCursor
typing may be unsafe: remove the redundant "?? null" in the queryFn so cursorId
is passed directly as pageParam (pageParam is already number | null), and make
getNextPageParam robust against a backend that returns null by returning
lastPage.hasNext ? (lastPage.nextCursor ?? undefined) : undefined;
confirm/update the GetClubNoticeCommentsResponseResult/nextCursor type in
src/types/clubnotification.ts or handle its potential null at the call site (use
lastPage.nextCursor ?? undefined) to avoid a runtime type mismatch.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0b80b1f8-9289-4cfb-8f86-f91ac1766a27
📒 Files selected for processing (29)
src/app/(admin)/admin/(app)/stories/[id]/page.tsxsrc/app/(main)/stories/[id]/page.tsxsrc/app/groups/[id]/admin/notice/[noticeId]/page.tsxsrc/app/groups/[id]/admin/notice/new/page.tsxsrc/app/groups/[id]/notice/[noticeId]/page.tsxsrc/components/base-ui/Admin/stories/bookstory_detail.tsxsrc/components/base-ui/Admin/stories/comment_section.tsxsrc/components/base-ui/Comment/comment_item.tsxsrc/components/base-ui/Comment/comment_list_notice.tsxsrc/components/base-ui/Comment/comment_section.tsxsrc/components/base-ui/Comment/comment_section_bookcase.tsxsrc/components/base-ui/Comment/comment_section_notice.tsxsrc/components/base-ui/Group/notice_detail.tsxsrc/components/base-ui/Group/vote_notice_detail.tsxsrc/hooks/mutations/useClubNoticeCommentMutations.tssrc/hooks/mutations/useClubNotificationMutations.tssrc/hooks/mutations/useReportMemberMutations.tssrc/hooks/queries/useClubNoticeCommentQueries.tssrc/hooks/queries/useClubNotificationQueries.tssrc/lib/api/admin/endpoints/stories.tssrc/lib/api/admin/stories.tssrc/lib/api/endpoints/Clubnotification.tssrc/services/clubNoticeCommentService.tssrc/services/clubNotificationService.tssrc/services/reportService.tssrc/types/clubnotification.tssrc/types/report.tssrc/types/story.tstsconfig.json
💤 Files with no reviewable changes (2)
- tsconfig.json
- src/components/base-ui/Group/vote_notice_detail.tsx
| const shelves = | ||
| shelvesQuery.data?.pages.flatMap((p: any) => p.bookShelfInfoList) ?? []; | ||
|
|
||
| const modalBooks: Book[] = useMemo(() => { | ||
| return shelves.map((s: any) => ({ | ||
| id: s.meetingInfo.meetingId, | ||
| title: s.bookInfo.title, | ||
| author: s.bookInfo.author, | ||
| category: { | ||
| generation: `${s.meetingInfo.generation}기`, | ||
| genre: s.meetingInfo.tag, | ||
| }, | ||
| rating: s.meetingInfo.averageRate ?? 0, | ||
| description: "", | ||
| imageUrl: s.bookInfo.imgUrl ?? null, | ||
| })); | ||
| }, [shelves]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's check if the file exists and read the relevant section
cd /tmp && find . -name "page.tsx" -path "*groups*notice*" 2>/dev/null | head -5Repository: checkmo2025/FE
Length of output: 40
🏁 Script executed:
# List files in the groups directory to find the correct path
find . -type f -name "page.tsx" | grep -E "groups.*notice" | head -10Repository: checkmo2025/FE
Length of output: 291
🏁 Script executed:
# Search more broadly for the target file
find . -type d -name "\[noticeId\]" 2>/dev/nullRepository: checkmo2025/FE
Length of output: 143
🏁 Script executed:
# Look for TypeScript files in app directory with admin/notice pattern
find ./src -type f -name "*.tsx" 2>/dev/null | grep -i "notice" | head -20Repository: checkmo2025/FE
Length of output: 621
🏁 Script executed:
# Check the structure of src/app/groups
ls -la ./src/app/groups/ 2>/dev/null || echo "Path not found, checking alternatives"
find . -type d -path "*/groups/*" -name "*notice*" 2>/dev/null | head -10Repository: checkmo2025/FE
Length of output: 547
🏁 Script executed:
# Read the target file to verify the code section
wc -l ./src/app/groups/[id]/admin/notice/[noticeId]/page.tsxRepository: checkmo2025/FE
Length of output: 116
🏁 Script executed:
# Read lines around 85-101 to verify the comment's code snippet
sed -n '75,110p' ./src/app/groups/[id]/admin/notice/[noticeId]/page.tsxRepository: checkmo2025/FE
Length of output: 1190
🏁 Script executed:
# Find the hook definition
rg "useClubsBookshelfSimpleInfiniteQuery" --type ts --type tsx -A 5Repository: checkmo2025/FE
Length of output: 85
🏁 Script executed:
# Search for the hook's type definitions
rg "useClubsBookshelfSimpleInfiniteQuery" --type ts --type tsx -B 2 -A 10 | head -40Repository: checkmo2025/FE
Length of output: 85
🏁 Script executed:
# Check imports at the top of the file to locate where this hook comes from
head -50 ./src/app/groups/[id]/admin/notice/[noticeId]/page.tsx | grep -E "^import|^from"Repository: checkmo2025/FE
Length of output: 875
🏁 Script executed:
# Find the hook definition file
find ./src -name "*useClubsBookshelfQueries*" -type fRepository: checkmo2025/FE
Length of output: 105
🏁 Script executed:
# Search for the hook definition
rg "useClubsBookshelfSimpleInfiniteQuery" -A 20 --max-count=1Repository: checkmo2025/FE
Length of output: 6505
🏁 Script executed:
# Read the entire hook definition file
cat -n ./src/hooks/queries/useClubsBookshelfQueries.tsRepository: checkmo2025/FE
Length of output: 5144
🏁 Script executed:
# Also check what clubsBookshelfService.getSimpleBookshelves returns
rg "getSimpleBookshelves" -A 15Repository: checkmo2025/FE
Length of output: 2381
🏁 Script executed:
# Find the type definition for ClubsBookshelfSimpleResult
rg "ClubsBookshelfSimpleResult|ClubsBookshelfSimpleResponse" -A 10Repository: checkmo2025/FE
Length of output: 2612
🏁 Script executed:
# Also check the import statements in the service file to see where types come from
head -30 ./src/services/clubsBookshelfService.tsRepository: checkmo2025/FE
Length of output: 871
🏁 Script executed:
# Find the BookShelfInfoItem type definition
rg "interface BookShelfInfoItem|type BookShelfInfoItem" -A 15Repository: checkmo2025/FE
Length of output: 793
🏁 Script executed:
# Also check if the hook is properly typed with generic parameters
cat ./src/hooks/queries/useClubsBookshelfQueries.ts | grep -A 15 "useClubsBookshelfSimpleInfiniteQuery"Repository: checkmo2025/FE
Length of output: 655
🏁 Script executed:
# Check MeetingInfo and BookInfo types to understand the full structure
rg "interface MeetingInfo|interface BookInfo" -A 10Repository: checkmo2025/FE
Length of output: 1204
🏁 Script executed:
# Check if there are other similar infinite queries to see the pattern used elsewhere
grep -A 20 "useInfiniteQuery<" ./src/hooks/queries/useClubsBookshelfQueries.tsRepository: checkmo2025/FE
Length of output: 1303
🏁 Script executed:
# Check if there's an ESLint config to see if no-explicit-any is enforced
find ./src -name "eslint*" -o -name ".eslintrc*" | head -5Repository: checkmo2025/FE
Length of output: 40
🏁 Script executed:
# Also search for eslint config at repo root
ls -la .eslintrc* eslint.config.* 2>/dev/nullRepository: checkmo2025/FE
Length of output: 123
🏁 Script executed:
# Read the ESLint config
cat -n ./eslint.config.mjsRepository: checkmo2025/FE
Length of output: 648
🏁 Script executed:
# Verify what the actual error would be if we removed the any annotations
# Let's check if there are other files that use this pattern to understand the convention
rg "useClubsBookshelfSimpleInfiniteQuery" -B 3 -A 10 | head -50Repository: checkmo2025/FE
Length of output: 4048
🏁 Script executed:
# Check if the hook is used elsewhere and how
rg "flatMap.*bookShelfInfoList|map.*meetingInfo" -B 5 -A 5Repository: checkmo2025/FE
Length of output: 2682
Type the hook with proper generic parameters to eliminate the any annotations downstream.
The root issue is that useClubsBookshelfSimpleInfiniteQuery lacks generic parameters. It should be typed like the other infinite queries in the same file (e.g., useBookshelfTopicsInfiniteQuery), declaring ClubsBookshelfSimpleResult as its data type. Once the hook is properly typed, the any annotations in both flatMap and map can be safely removed without TypeScript errors.
🧩 Fix: Type the hook definition
Add generic parameters to useClubsBookshelfSimpleInfiniteQuery in src/hooks/queries/useClubsBookshelfQueries.ts:
+import type { InfiniteData } from "@tanstack/react-query";
-export function useClubsBookshelfSimpleInfiniteQuery(clubId: number, p0: { enabled: boolean; }) {
+export function useClubsBookshelfSimpleInfiniteQuery(clubId: number, p0: { enabled: boolean; }) {
return useInfiniteQuery<
+ ClubsBookshelfSimpleResult,
+ Error,
+ InfiniteData<ClubsBookshelfSimpleResult, number | undefined>,
+ ReturnType<typeof bookshelfQueryKeys.simple>,
+ number | undefined
+ >({Then in the consumer pages, remove the any annotations:
const shelves =
- shelvesQuery.data?.pages.flatMap((p: any) => p.bookShelfInfoList) ?? [];
+ shelvesQuery.data?.pages.flatMap((p) => p.bookShelfInfoList) ?? [];
const modalBooks: Book[] = useMemo(() => {
- return shelves.map((s: any) => ({
+ return shelves.map((s) => ({📝 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 shelves = | |
| shelvesQuery.data?.pages.flatMap((p: any) => p.bookShelfInfoList) ?? []; | |
| const modalBooks: Book[] = useMemo(() => { | |
| return shelves.map((s: any) => ({ | |
| id: s.meetingInfo.meetingId, | |
| title: s.bookInfo.title, | |
| author: s.bookInfo.author, | |
| category: { | |
| generation: `${s.meetingInfo.generation}기`, | |
| genre: s.meetingInfo.tag, | |
| }, | |
| rating: s.meetingInfo.averageRate ?? 0, | |
| description: "", | |
| imageUrl: s.bookInfo.imgUrl ?? null, | |
| })); | |
| }, [shelves]); | |
| const shelves = | |
| shelvesQuery.data?.pages.flatMap((p) => p.bookShelfInfoList) ?? []; | |
| const modalBooks: Book[] = useMemo(() => { | |
| return shelves.map((s) => ({ | |
| id: s.meetingInfo.meetingId, | |
| title: s.bookInfo.title, | |
| author: s.bookInfo.author, | |
| category: { | |
| generation: `${s.meetingInfo.generation}기`, | |
| genre: s.meetingInfo.tag, | |
| }, | |
| rating: s.meetingInfo.averageRate ?? 0, | |
| description: "", | |
| imageUrl: s.bookInfo.imgUrl ?? null, | |
| })); | |
| }, [shelves]); |
🧰 Tools
🪛 ESLint
[error] 86-86: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 89-89: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/groups/`[id]/admin/notice/[noticeId]/page.tsx around lines 85 - 101,
Type the useClubsBookshelfSimpleInfiniteQuery hook with the proper generic so
downstream code can be strongly typed: update the hook signature/use call to
include ClubsBookshelfSimpleResult as the data generic (same pattern as
useBookshelfTopicsInfiniteQuery) in useClubsBookshelfQueries, then remove the
any annotations in the consumer - change shelvesQuery.data?.pages.flatMap((p:
any) => p.bookShelfInfoList) to use the typed page shape and update the
modalBooks mapping to accept the concrete page/item types instead of any so
modalBooks and Book properties are typed correctly.
| } catch (e: any) { | ||
| toast.dismiss(tid); | ||
|
|
||
| const msg = e?.message ?? ""; | ||
| if (msg.includes("isPinned") || msg.includes("pinned") || msg.includes("고정")) { | ||
| toast.error("고정 공지는 최대 5개까지 가능합니다."); | ||
| return; | ||
| } | ||
|
|
||
| toast.error(msg || "공지사항 수정 실패"); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
wc -l src/app/groups/[id]/admin/notice/[noticeId]/page.tsxRepository: checkmo2025/FE
Length of output: 114
🏁 Script executed:
sed -n '300,330p' src/app/groups/[id]/admin/notice/[noticeId]/page.tsxRepository: checkmo2025/FE
Length of output: 790
🌐 Web query:
TypeScript catch block unknown vs any best practices
💡 Result:
In TypeScript catch blocks, use unknown instead of any as the best practice for the error parameter. This promotes type safety by requiring type guards or checks before accessing properties, preventing runtime errors from incorrect assumptions about the caught value. Key reasons and guidance: - JavaScript allows throwing any value (not just Errors), so TypeScript types catch variables as unknown (since TS 4.0) or any by default. - unknown forces narrowing (e.g., if (err instanceof Error)), while any bypasses checks entirely. - Enable "useUnknownInCatchVariables": true in tsconfig.json (part of strict mode since TS 4.4) to default catch variables to unknown without explicit annotation. - Official TypeScript docs explicitly recommend avoiding any unless migrating from JS, favoring unknown for unknown values. Example: try { // risky code } catch (err: unknown) { if (err instanceof Error) { console.error(err.message); } else { console.error('Unexpected error:', err); } } This pattern ensures safe error handling across TS versions up to 2026.
Citations:
- 1: https://stackoverflow.com/questions/64452484/how-can-i-safely-access-caught-error-properties-in-typescript
- 2: https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html
- 3: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html
- 4: https://www.typescriptlang.org/tsconfig/useUnknownInCatchVariables.html
- 5: https://stackoverflow.com/questions/54649465/how-to-do-try-catch-and-finally-statements-in-typescript
Use unknown in the catch block instead of any.
The current any type bypasses TypeScript's type checking, allowing unsafe access to e?.message without narrowing. TypeScript best practices require using unknown and narrowing the type (e.g., instanceof Error) before accessing properties.
🛠️ Safer catch typing
- } catch (e: any) {
+ } catch (e: unknown) {
toast.dismiss(tid);
- const msg = e?.message ?? "";
+ const msg = e instanceof Error ? e.message : "";
if (msg.includes("isPinned") || msg.includes("pinned") || msg.includes("고정")) {
toast.error("고정 공지는 최대 5개까지 가능합니다.");
return;📝 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.
| } catch (e: any) { | |
| toast.dismiss(tid); | |
| const msg = e?.message ?? ""; | |
| if (msg.includes("isPinned") || msg.includes("pinned") || msg.includes("고정")) { | |
| toast.error("고정 공지는 최대 5개까지 가능합니다."); | |
| return; | |
| } | |
| toast.error(msg || "공지사항 수정 실패"); | |
| } catch (e: unknown) { | |
| toast.dismiss(tid); | |
| const msg = e instanceof Error ? e.message : ""; | |
| if (msg.includes("isPinned") || msg.includes("pinned") || msg.includes("고정")) { | |
| toast.error("고정 공지는 최대 5개까지 가능합니다."); | |
| return; | |
| } | |
| toast.error(msg || "공지사항 수정 실패"); |
🧰 Tools
🪛 ESLint
[error] 312-312: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/groups/`[id]/admin/notice/[noticeId]/page.tsx around lines 312 - 321,
Change the catch param from any to unknown and narrow before reading message: in
the catch block that currently declares "e: any" (the block using tid and
toast), use "e: unknown" then check "if (e instanceof Error)" to get e.message,
otherwise derive a string via String(e) or an empty string; keep the same
pinned-word checks (isPinned, pinned, 고정) and toast/error behavior but operate
on the safely-narrowed message variable.
| const mapNode = ( | ||
| c: CommentInfo | ||
| ): (Comment & { parentCommentId?: number | null }) | null => { | ||
| if (!c.authorInfo) return null; | ||
|
|
||
| const mappedReplies = c.replies | ||
| ? c.replies | ||
| .map((r) => mapNode(r)) | ||
| .filter( | ||
| ( | ||
| reply | ||
| ): reply is Comment & { parentCommentId?: number | null } => | ||
| reply !== null | ||
| ) |
There was a problem hiding this comment.
Don't hide comments just because authorInfo is absent.
CommentInfo.authorInfo is already treated as optional in src/components/base-ui/Comment/comment_section_bookcase.tsx:41-46. Returning null here makes those comments disappear from the admin view entirely, so moderators lose the ability to inspect or delete them.
🧱 Safer fallback mapping
const mapNode = (
c: CommentInfo
): (Comment & { parentCommentId?: number | null }) | null => {
- if (!c.authorInfo) return null;
+ const nickname = c.authorInfo?.nickname ?? "(알 수 없음)";
+ const profileImageUrl = c.authorInfo?.profileImageUrl ?? "";
const mappedReplies = c.replies
? c.replies
.map((r) => mapNode(r))
.filter(
@@
return {
id: c.commentId,
- authorName: c.authorInfo.nickname,
- profileImgSrc: isValidUrl(c.authorInfo.profileImageUrl)
- ? c.authorInfo.profileImageUrl
+ authorName: nickname,
+ profileImgSrc: isValidUrl(profileImageUrl)
+ ? profileImageUrl
: "/profile2.svg",
content: c.content,
createdAt: c.createdAt,
- isAuthor: c.authorInfo.nickname === storyAuthorNickname,
+ isAuthor: nickname === storyAuthorNickname,
isMine: c.writtenByMe,
replies: mappedReplies,
parentCommentId: c.parentCommentId,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/base-ui/Admin/stories/comment_section.tsx` around lines 36 -
49, The mapNode function currently returns null when a CommentInfo has no
authorInfo, which hides those comments; instead, keep the comment by mapping it
with a safe fallback for authorInfo (e.g., an explicit placeholder or undefined)
so admin UI can still display/manage it. Update mapNode (and the mappedReplies
logic) to stop returning null for !c.authorInfo, ensure the return type remains
Comment & { parentCommentId?: number | null } (use a defensive author object or
optional field), and preserve recursive mapping of c.replies through the
existing mapNode(r) chain so missing authorInfo no longer removes comments from
the tree.
| <CommentList | ||
| comments={comments} | ||
| onAddComment={handleAddComment} | ||
| onAddReply={handleAddReply} | ||
| onEditComment={handleEditComment} | ||
| onDeleteComment={handleDeleteComment} | ||
| onReportComment={handleReportComment} | ||
| /> |
There was a problem hiding this comment.
These admin actions still won't surface through the current CommentList contract.
CommentItem now gates its menu on canEdit / canDelete / canReport, but this path still renders through CommentList, which only takes comments plus handlers. Unless that list component also forwards capability flags from each mapped comment, the edit/delete/report flows added here stay unreachable.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/base-ui/Admin/stories/comment_section.tsx` around lines 267 -
274, The admin action handlers passed to CommentList (handleEditComment,
handleDeleteComment, handleReportComment) are never reachable because
CommentItem now gates its menu on per-comment capability flags
(canEdit/canDelete/canReport) that CommentList doesn't forward; update the
CommentList -> CommentItem rendering to include and forward those capability
flags for each comment (or change the CommentList API to accept a per-comment
capability resolver and use it when mapping comments) so CommentItem receives
canEdit, canDelete, and canReport for each comment and the existing handlers
will become reachable.
|
|
||
| <button | ||
| type="button" | ||
| onClick={() => { | ||
| onReply(id); | ||
| onReport?.(id); | ||
| setMenuOpen(false); | ||
| }} | ||
| className="flex w-full h-[44px] items-center justify-center gap-2 Body_1_2 text-Gray-4 hover:bg-Gray-1 cursor-pointer" | ||
| > | ||
| <Image src="/reply2.svg" alt="답글" width={24} height={24} /> | ||
| 답글달기 | ||
| <Image | ||
| src="/report.svg" | ||
| alt="신고" | ||
| width={24} | ||
| height={24} | ||
| /> | ||
| 신고하기 | ||
| </button> |
There was a problem hiding this comment.
Bug: Report button is rendered unconditionally.
The "신고하기" (Report) button is always rendered in the dropdown menu, regardless of whether canReport is true. This allows users to report their own comments, which contradicts the capability-based permission model.
🐛 Proposed fix to conditionally render the report button
-
- <button
+ {canReport && (
+ <button
type="button"
onClick={() => {
onReport?.(id);
setMenuOpen(false);
}}
className="flex w-full h-[44px] items-center justify-center gap-2 Body_1_2 text-Gray-4 hover:bg-Gray-1 cursor-pointer"
>
<Image
src="/report.svg"
alt="신고"
width={24}
height={24}
/>
신고하기
</button>
-
+ )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/base-ui/Comment/comment_item.tsx` around lines 173 - 189, The
Report button is rendered unconditionally; update the dropdown rendering in the
Comment component so the "신고하기" button is only output when canReport is true
(and onReport exists). Locate the block using onReport, id, and setMenuOpen (the
Report button JSX) and wrap it in a conditional check (e.g., if (canReport &&
onReport) render the button) so users cannot report when they lack permission
and the handler is present.
| const handleVoteSubmit = async () => { | ||
| if (!voteDetail || selectedOptions.length === 0 || isVotePending) { | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| await voteNotice({ | ||
| clubId, | ||
| noticeId, | ||
| voteId: voteDetail.id, | ||
| body: { | ||
| selectedItemNumbers: selectedOptions, | ||
| }, | ||
| }); | ||
|
|
||
| toast.success(isRevoteMode ? '투표가 수정되었습니다.' : '투표가 완료되었습니다.'); | ||
| } catch (e: any) { | ||
| const msg = e?.message ?? ''; | ||
| toast.error(msg || '투표에 실패했습니다.'); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Fix inconsistent indentation and type the error properly.
Two issues in this handler:
- The function body has incorrect indentation (should be 4 spaces inside the function)
- The
catch (e: any)uses an untyped error, flagged by ESLint
🛠️ Proposed fix
const handleVoteSubmit = async () => {
- if (!voteDetail || selectedOptions.length === 0 || isVotePending) {
- return;
- }
-
- try {
- await voteNotice({
- clubId,
- noticeId,
- voteId: voteDetail.id,
- body: {
- selectedItemNumbers: selectedOptions,
- },
- });
-
- toast.success(isRevoteMode ? '투표가 수정되었습니다.' : '투표가 완료되었습니다.');
- } catch (e: any) {
- const msg = e?.message ?? '';
- toast.error(msg || '투표에 실패했습니다.');
- }
-};
+ if (!voteDetail || selectedOptions.length === 0 || isVotePending) {
+ return;
+ }
+
+ try {
+ await voteNotice({
+ clubId,
+ noticeId,
+ voteId: voteDetail.id,
+ body: {
+ selectedItemNumbers: selectedOptions,
+ },
+ });
+
+ toast.success(isRevoteMode ? '투표가 수정되었습니다.' : '투표가 완료되었습니다.');
+ } 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 handleVoteSubmit = async () => { | |
| if (!voteDetail || selectedOptions.length === 0 || isVotePending) { | |
| return; | |
| } | |
| try { | |
| await voteNotice({ | |
| clubId, | |
| noticeId, | |
| voteId: voteDetail.id, | |
| body: { | |
| selectedItemNumbers: selectedOptions, | |
| }, | |
| }); | |
| toast.success(isRevoteMode ? '투표가 수정되었습니다.' : '투표가 완료되었습니다.'); | |
| } catch (e: any) { | |
| const msg = e?.message ?? ''; | |
| toast.error(msg || '투표에 실패했습니다.'); | |
| } | |
| }; | |
| const handleVoteSubmit = async () => { | |
| if (!voteDetail || selectedOptions.length === 0 || isVotePending) { | |
| return; | |
| } | |
| try { | |
| await voteNotice({ | |
| clubId, | |
| noticeId, | |
| voteId: voteDetail.id, | |
| body: { | |
| selectedItemNumbers: selectedOptions, | |
| }, | |
| }); | |
| toast.success(isRevoteMode ? '투표가 수정되었습니다.' : '투표가 완료되었습니다.'); | |
| } catch (e: unknown) { | |
| const msg = e instanceof Error ? e.message : ''; | |
| toast.error(msg || '투표에 실패했습니다.'); | |
| } | |
| }; |
🧰 Tools
🪛 ESLint
[error] 126-126: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/base-ui/Group/notice_detail.tsx` around lines 110 - 130, The
handleVoteSubmit handler has incorrect indentation and an untyped catch
parameter; reformat the function body to use consistent 4-space indentation and
change the catch signature to catch (e: unknown) (or a typed union) and safely
extract the message (e.g., const msg = e instanceof Error ? e.message :
String(e)) before calling toast.error; update references inside to use
voteNotice, isRevoteMode, selectedOptions, clubId, noticeId, and voteDetail.id
as in the diff.
| } catch (e: any) { | ||
| const msg = e?.message ?? ''; | ||
| toast.error(msg || '공지사항 삭제에 실패했습니다.'); |
There was a problem hiding this comment.
Type the error properly instead of using any.
Same issue as above - use unknown and type guard for the error.
🛠️ Proposed fix
- } catch (e: any) {
- const msg = e?.message ?? '';
+ } 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.
| } catch (e: any) { | |
| const msg = e?.message ?? ''; | |
| toast.error(msg || '공지사항 삭제에 실패했습니다.'); | |
| } catch (e: unknown) { | |
| const msg = e instanceof Error ? e.message : ''; | |
| toast.error(msg || '공지사항 삭제에 실패했습니다.'); |
🧰 Tools
🪛 ESLint
[error] 148-148: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/base-ui/Group/notice_detail.tsx` around lines 148 - 150, The
catch block currently types the error as `any` (catch (e: any)) and immediately
uses `e.message`; change the catch parameter to `unknown` and add a type guard
to extract the message safely before calling `toast.error`. Specifically, modify
the catch in notice_detail.tsx to `catch (e: unknown)` then check `if (e &&
typeof e === 'object' && 'message' in e && typeof (e as any).message ===
'string') { msg = (e as { message: string }).message } else { msg = String(e ??
'') }` (or equivalent safe extraction) and then call `toast.error(msg || '공지사항
삭제에 실패했습니다.')`, ensuring no use of `any` for the catch variable.
| export async function deleteAdminBookStory(bookStoryId: number) { | ||
| const res = await fetch(ADMIN_STORIES.delete(bookStoryId), { | ||
| method: "DELETE", | ||
| credentials: "include", | ||
| }); | ||
|
|
||
| const data: DeleteAdminBookStoryResponse = await res.json(); | ||
|
|
||
| if (!res.ok || !data.isSuccess) { | ||
| throw new Error(data.message || "책 이야기 삭제에 실패했습니다."); | ||
| } | ||
|
|
||
| return data.result; |
There was a problem hiding this comment.
Guard DELETE responses before calling res.json().
DELETE endpoints often return 204 No Content, and proxy failures can come back as text or HTML. In both helpers, await res.json() runs before the status check, so an empty success body or non-JSON error body will surface as a parse error instead of your intended message.
Also applies to: 121-139
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/api/admin/stories.ts` around lines 99 - 111, The helper
deleteAdminBookStory currently calls await res.json() before checking response
status which breaks on 204 No Content or non-JSON error bodies; update
deleteAdminBookStory to first check res.status and res.ok (or Content-Type
header) and handle 204 by returning null/undefined (or an appropriate empty
result) without calling res.json(); for non-OK responses read text safely (await
res.text()) and throw a new Error using that text or data.message, and only call
await res.json() when the response is OK and Content-Type is JSON to parse into
DeleteAdminBookStoryResponse; apply the same guarded pattern to the other DELETE
helper(s) referenced in the file (the helper at lines 121-139) so neither
attempts JSON parse on empty or non-JSON responses.
| export interface ClubNoticeCommentAuthorInfo { | ||
| nickname: string; | ||
| profileImageUrl: string; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if other author/member types use nullable profileImageUrl
rg -n "profileImageUrl" --type ts -C2Repository: checkmo2025/FE
Length of output: 40338
Make ClubNoticeCommentAuthorInfo.profileImageUrl nullable.
ClubNoticeCommentAuthorInfo.profileImageUrl is typed as non-nullable string, but the component code handling these comments (comment_section_notice.tsx:76) defensively treats it as potentially falsy with || undefined. This inconsistency with ClubNoticeVoteMember.profileImageUrl (which correctly allows null) suggests the type definition is incomplete and the API can return null for comment author profile images. Update to string | null to match actual API behavior and maintain consistency.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/types/clubnotification.ts` around lines 161 - 164, Update the
ClubNoticeCommentAuthorInfo interface so profileImageUrl is nullable: change the
type of profileImageUrl on ClubNoticeCommentAuthorInfo from string to string |
null to match API behavior and the handling in comment_section_notice.tsx and to
align with ClubNoticeVoteMember.profileImageUrl.
| export type ReportType = "GENERAL" | "BOOK_STORY"; | ||
|
|
||
| export interface ReportMemberRequest { | ||
| reportedMemberNickname: string; | ||
| reportType: ReportType; | ||
| content: string; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "ReportType definitions:"
rg -n --type=ts --type=tsx '\btype\s+ReportType\b' src
echo
echo "Report-type literals used in report flows:"
rg -n -C2 --type=ts --type=tsx '"GENERAL"|"BOOK_STORY"|"COMMENT"|"CLUB_MEETING"|reportType\s*:|mappedType\s*=' srcRepository: checkmo2025/FE
Length of output: 126
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== ReportType type definitions ==="
rg -n --type ts 'type\s+ReportType\b' src 2>/dev/null || echo "No matches found"
echo
echo "=== Searching for report type enum/type definitions ==="
rg -n --type ts -B2 -A2 'ReportType' src 2>/dev/null || echo "No matches found"
echo
echo "=== Searching for report type literals in codebase ==="
rg -n --type ts '"GENERAL"|"BOOK_STORY"|"COMMENT"|"CLUB_MEETING"' src 2>/dev/null || echo "No literal matches found"
echo
echo "=== Checking bookstory_detail file ==="
fd 'bookstory_detail' src 2>/dev/null || echo "File not found"Repository: checkmo2025/FE
Length of output: 17296
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Imports from src/types/report.ts ==="
rg -n 'from.*types/report' src 2>/dev/null || echo "No imports found from report.ts"
echo
echo "=== ReportMemberRequest usage ==="
rg -n 'ReportMemberRequest' src 2>/dev/null || echo "No usage of ReportMemberRequest found"
echo
echo "=== Check what's actually exported from report.ts ==="
head -30 src/types/report.ts 2>/dev/null || echo "File not accessible"Repository: checkmo2025/FE
Length of output: 1778
Remove the duplicate ReportMemberRequest and ReportType from report.ts or consolidate to a single source.
The codebase now has two conflicting definitions of ReportMemberRequest:
src/types/report.tswith a narrow ReportType ("GENERAL" | "BOOK_STORY")src/types/member.tswith the complete ReportType ("GENERAL" | "CLUB_MEETING" | "BOOK_STORY" | "COMMENT")
This creates dual API contracts: reportService uses the narrow definition while memberService uses the full definition. All components currently use useMemberMutations (which pulls from member.ts), but developers could accidentally import from the wrong module, breaking type safety for COMMENT and CLUB_MEETING reports.
Keep the complete definition in member.ts and remove the duplicate from report.ts, or clarify if report.ts serves a distinct API endpoint.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/types/report.ts` around lines 3 - 8, report.ts defines a narrower
ReportType and ReportMemberRequest that conflict with member.ts; remove the
duplicate types from src/types/report.ts and have any code that used
ReportMemberRequest or ReportType import them from the canonical definitions in
src/types/member.ts (keep the full union in member.ts: ReportType and
ReportMemberRequest) or, if report.ts must remain distinct, rename the types
there (e.g., ReportEndpointType) and update all usages to use the member.ts
types where the full set ("GENERAL" | "CLUB_MEETING" | "BOOK_STORY" | "COMMENT")
is required; specifically update imports/usages that reference
ReportMemberRequest and ReportType to point to the member.ts definitions (or
rename the report.ts symbols) so there is a single source of truth.
Feat/221 withdrawal and social
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
src/app/(main)/setting/account-status/page.tsx (1)
21-23: Consider disabling the button or showing loading state during withdrawal.The confirm button remains enabled while the withdrawal mutation is in progress. If the API is slow, users might click multiple times, potentially triggering duplicate requests.
🔧 Suggested improvement
const handleConfirmWithdraw = () => { + setIsModalOpen(false); // Close modal immediately withdrawMutation.mutate(); };Alternatively, pass
isPendingstate to disable the confirm action:<ConfirmModal isOpen={isModalOpen} message="정말 탈퇴하시겠습니까? 탈퇴 시 모든 데이터가 삭제되며 복구할 수 없습니다." onConfirm={handleConfirmWithdraw} onCancel={() => setIsModalOpen(false)} // If ConfirmModal supports it: // isConfirming={withdrawMutation.isPending} />Also applies to: 56-66
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`(main)/setting/account-status/page.tsx around lines 21 - 23, The confirm button allows duplicate requests because withdrawMutation is not checked or reflected in the UI; update the ConfirmModal usage and handleConfirmWithdraw to reflect the mutation state by (1) passing withdrawMutation.isPending or withdrawMutation.isLoading to the ConfirmModal (e.g., an isConfirming or disabled prop) so the confirm button shows a loading/disabled state, and (2) guard handleConfirmWithdraw to return early if withdrawMutation.isPending/isLoading to avoid calling withdrawMutation.mutate() multiple times; reference the handleConfirmWithdraw function, withdrawMutation object, ConfirmModal component, isModalOpen and setIsModalOpen state when applying the fix.src/hooks/mutations/useMemberMutations.ts (2)
357-361: Consider clearing React Query cache before redirect for privacy.The
logout()function only removes theaccessTokencookie and clears Zustand state. It does NOT clear localStorage, sessionStorage, or invalidate React Query cache. After account withdrawal, sensitive user data might remain in the React Query cache until the page fully reloads.While
window.location.href = "/"triggers a full page reload that should clear in-memory cache, consider explicitly clearing the cache for defense-in-depth, especially if the app uses a persisted cache adapter.🔧 Suggested improvement
+import { useQueryClient } from "@tanstack/react-query"; export const useWithdrawMutation = () => { const { logout } = useAuthStore(); + const queryClient = useQueryClient(); return useMutation({ mutationFn: async () => { await memberService.withdraw(); }, onSuccess: () => { toast.success("회원 탈퇴가 완료되었습니다."); + queryClient.clear(); // Clear all cached data logout(); // Clear auth state window.location.href = "/"; // Redirect to home page },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/mutations/useMemberMutations.ts` around lines 357 - 361, In the onSuccess handler in useMemberMutations.ts, clear React Query and browser storage before redirect: obtain the React Query client (useQueryClient or your app's queryClient instance) and call queryClient.clear() (or queryClient.invalidateQueries() as appropriate), remove any persisted-query storage keys if you use a persistence adapter, and also clear localStorage/sessionStorage, then call logout() and finally set window.location.href = "/". Ensure these calls happen synchronously in onSuccess (queryClient.clear() before logout/redirect) so in-memory and persisted caches are wiped prior to navigation.
362-366: Replaceanywith a proper error type.The static analysis tool flagged
error: any. Use a more specific type orunknownwith type narrowing.🔧 Suggested fix
- onError: (error: any) => { + onError: (error: unknown) => { console.error("Failed to withdraw:", error); - const errorMessage = error.response?.data?.message || error.message || "회원 탈퇴에 실패했습니다."; + let errorMessage = "회원 탈퇴에 실패했습니다."; + if (error instanceof Error) { + errorMessage = error.message; + } + // For axios errors with response data + if (typeof error === "object" && error !== null && "response" in error) { + const axiosError = error as { response?: { data?: { message?: string } } }; + errorMessage = axiosError.response?.data?.message || errorMessage; + } toast.error(errorMessage); },Alternatively, if you have an
AxiosErrortype available:+import { AxiosError } from "axios"; - onError: (error: any) => { + onError: (error: Error | AxiosError<{ message?: string }>) => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/mutations/useMemberMutations.ts` around lines 362 - 366, In the onError handler inside useMemberMutations.ts (the onError: (error: any) => { ... } block), replace the broad any with a safer type like unknown, then narrow it before accessing properties: check if error is an AxiosError (if using axios) or if it has a response/data/message shape before reading error.response?.data?.message; alternatively use a type-guard to extract messageFallback = typeof error === "object" && error !== null && "message" in error ? (error as any).message : undefined and fall back to the default string; ensure the parameter signature becomes onError: (error: unknown) => { ... } and all property access is guarded or via a type-guard.
🤖 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/components/base-ui/Settings/SocialLogin/SocialLoginCard.tsx`:
- Around line 10-13: Update the background color class in the SocialLoginCard
component's local config: replace "bg-White" with the standard Tailwind class
"bg-white" in the local object (the object named local in SocialLoginCard.tsx
that also contains icon: "/profile10.svg") so the background color is applied
correctly.
---
Nitpick comments:
In `@src/app/`(main)/setting/account-status/page.tsx:
- Around line 21-23: The confirm button allows duplicate requests because
withdrawMutation is not checked or reflected in the UI; update the ConfirmModal
usage and handleConfirmWithdraw to reflect the mutation state by (1) passing
withdrawMutation.isPending or withdrawMutation.isLoading to the ConfirmModal
(e.g., an isConfirming or disabled prop) so the confirm button shows a
loading/disabled state, and (2) guard handleConfirmWithdraw to return early if
withdrawMutation.isPending/isLoading to avoid calling withdrawMutation.mutate()
multiple times; reference the handleConfirmWithdraw function, withdrawMutation
object, ConfirmModal component, isModalOpen and setIsModalOpen state when
applying the fix.
In `@src/hooks/mutations/useMemberMutations.ts`:
- Around line 357-361: In the onSuccess handler in useMemberMutations.ts, clear
React Query and browser storage before redirect: obtain the React Query client
(useQueryClient or your app's queryClient instance) and call queryClient.clear()
(or queryClient.invalidateQueries() as appropriate), remove any persisted-query
storage keys if you use a persistence adapter, and also clear
localStorage/sessionStorage, then call logout() and finally set
window.location.href = "/". Ensure these calls happen synchronously in onSuccess
(queryClient.clear() before logout/redirect) so in-memory and persisted caches
are wiped prior to navigation.
- Around line 362-366: In the onError handler inside useMemberMutations.ts (the
onError: (error: any) => { ... } block), replace the broad any with a safer type
like unknown, then narrow it before accessing properties: check if error is an
AxiosError (if using axios) or if it has a response/data/message shape before
reading error.response?.data?.message; alternatively use a type-guard to extract
messageFallback = typeof error === "object" && error !== null && "message" in
error ? (error as any).message : undefined and fall back to the default string;
ensure the parameter signature becomes onError: (error: unknown) => { ... } and
all property access is guarded or via a type-guard.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 76e65e33-f661-4d95-bab1-19b3ba15d62a
📒 Files selected for processing (7)
src/app/(main)/setting/account-status/page.tsxsrc/components/base-ui/Settings/SocialLogin/SocialLoginCard.tsxsrc/hooks/mutations/useMemberMutations.tssrc/hooks/queries/useMemberQueries.tssrc/lib/api/endpoints/member.tssrc/services/memberService.tssrc/types/member.ts
✅ Files skipped from review due to trivial changes (2)
- src/types/member.ts
- src/lib/api/endpoints/member.ts
| local: { | ||
| bgColor: "bg-White border border-Gray-2", | ||
| icon: "/profile10.svg", | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if bg-White is defined in Tailwind config
echo "=== Searching for White color definition in Tailwind config ==="
fd -e js -e ts -e json "tailwind" --exec cat {} 2>/dev/null | grep -i "white" || echo "No tailwind config found or no White defined"
# Alternative: search for any tailwind config files
echo -e "\n=== Listing tailwind config files ==="
fd "tailwind.config"
# Check if the icon exists
echo -e "\n=== Checking for profile10.svg ==="
fd "profile10.svg" --type fRepository: checkmo2025/FE
Length of output: 281
🏁 Script executed:
cat -n tailwind.config.jsRepository: checkmo2025/FE
Length of output: 373
Change bg-White to bg-white (lowercase).
The Tailwind configuration does not define a custom White color, and standard Tailwind CSS uses lowercase bg-white. Using bg-White would result in the background color not being applied. The /profile10.svg file exists in the public directory.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/base-ui/Settings/SocialLogin/SocialLoginCard.tsx` around lines
10 - 13, Update the background color class in the SocialLoginCard component's
local config: replace "bg-White" with the standard Tailwind class "bg-white" in
the local object (the object named local in SocialLoginCard.tsx that also
contains icon: "/profile10.svg") so the background color is applied correctly.
📌 개요 (Summary)
🛠️ 변경 사항 (Changes)
📸 스크린샷 (Screenshots)
(UI 변경 사항이 있다면 첨부해주세요)
✅ 체크리스트 (Checklist)
pnpm build)pnpm lint)Summary by CodeRabbit
New Features
Bug Fixes
Improvements