Skip to content

책 이야기 오류 해결 최신화 - #220

Merged
shinwokkang merged 28 commits into
mainfrom
dev
Mar 20, 2026
Merged

shinwokkang merged 28 commits into
mainfrom
dev

Conversation

@shinwokkang

@shinwokkang shinwokkang commented Mar 20, 2026 •

Copy link
Copy Markdown
Contributor

📌 개요 (Summary)

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

🛠️ 변경 사항 (Changes)

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

📸 스크린샷 (Screenshots)

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

✅ 체크리스트 (Checklist)

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

Summary by CodeRabbit

  • New Features

    • Notice editing with image uploads and vote-deadline controls
    • Notice voting (including multi-select where applicable)
    • Live 1000-character counter and enforcement for notice content
    • Enhanced comment system: nested replies, async pagination, edit/delete/report flows
    • Member reporting UI and submission across comments and stories
    • Account withdrawal flow with confirmation and login-status display
  • Bug Fixes

    • Robust handling when comment author info is missing
  • Improvements

    • Admin story controls now include delete with confirmation
    • Various comment UX and loading improvements

@vercel

vercel Bot commented Mar 20, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
check-mo Ready Ready Preview, Comment Mar 20, 2026 3:47pm
checkmo Ready Ready Preview, Comment Mar 20, 2026 3:47pm

@coderabbitai

coderabbitai Bot commented Mar 20, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Refactors 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

Cohort / File(s) Summary
Comment Core & Item
src/components/base-ui/Comment/comment_section.tsx, src/components/base-ui/Comment/comment_item.tsx, src/components/base-ui/Comment/comment_list_notice.tsx
Replaced monolithic section with per-item UI + capability flags (canEdit/canDelete/canReport), async-capable callbacks, outside-click/menu handling, and intersection-observer load-more support for notices.
Comment Containers
src/components/base-ui/Admin/stories/comment_section.tsx, src/components/base-ui/Comment/comment_section_bookcase.tsx, src/components/base-ui/Comment/comment_section_notice.tsx
Added specialized comment containers: admin stories, bookcase, and notice variants — each maps API data to nested UI state, wires create/update/delete/report mutations, handles auth gating, modals, and pagination.
Notice Detail & Admin Edit
src/components/base-ui/Group/notice_detail.tsx, src/app/groups/[id]/admin/notice/[noticeId]/page.tsx, src/app/groups/[id]/admin/notice/new/page.tsx, src/components/base-ui/Group/vote_notice_detail.tsx
Consolidated voting into NoticeDetail (vote state, submit/revote, member option derivation), added admin edit/delete flows and complete edit form (images, meeting/book selection, vote deadline, validations); removed standalone VoteNoticeDetail.
Admin Bookstory Changes
src/app/(admin)/admin/(app)/stories/[id]/page.tsx, src/components/base-ui/Admin/stories/bookstory_detail.tsx, src/lib/api/admin/endpoints/stories.ts, src/lib/api/admin/stories.ts
Switched admin story page to admin CommentSection import, added admin story deletion endpoint/service and delete handler in BookstoryDetail with confirm + redirect.
Story Page Import Update
src/app/(main)/stories/[id]/page.tsx
Switched to bookcase-specific CommentSection import.
Club Notice Comments API & Hooks
src/services/clubNotificationService.ts, src/lib/api/endpoints/Clubnotification.ts, src/hooks/queries/useClubNoticeCommentQueries.ts, src/hooks/mutations/useClubNoticeCommentMutations.ts, src/hooks/mutations/useClubNotificationMutations.ts, src/hooks/queries/useClubNotificationQueries.ts
Added comment/vote endpoints and service methods; added infinite comments query and comment CRUD mutation hooks; expanded/standardized clubNoticeQueryKeys and adjusted cache invalidation for create/update/delete/vote.
Reporting & Member APIs
src/services/reportService.ts, src/hooks/mutations/useReportMemberMutations.ts, src/types/report.ts
Added reportService.reportMember, report mutation hook, and report types (ReportType, ReportMemberRequest/Response).
Member Login & Withdraw
src/hooks/queries/useMemberQueries.ts, src/hooks/mutations/useMemberMutations.ts, src/services/memberService.ts, src/lib/api/endpoints/member.ts, src/types/member.ts, src/app/(main)/setting/account-status/page.tsx
Added login-status query and endpoint, withdraw mutation + service and endpoint, UI wiring for withdrawal confirmation and dynamic social-login display.
Types & Small Fixes
src/types/clubnotification.ts, src/types/story.ts, src/components/base-ui/Settings/SocialLogin/SocialLoginCard.tsx, tsconfig.json
Updated clubnotification types (vote/comment shapes, optional meeting/vote), allowed nullable comment authors, added LoginProvider/LoginStatusResponse, added local provider styling, and removed duplicate tsconfig include globs.

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
Loading
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
Loading
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
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

✨ feat

Poem

🐰
I nibble through comments, tidy and bright,
Votes and reports by the moonlit night.
Admins can prune with one brave hop,
Pages refresh — no stumble, no flop.
Hooray for features that make code delight!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "책 이야기 오류 해결 최신화" is vague and does not clearly convey the specific changes in this large, multi-faceted changeset. Revise the title to be more specific about the primary change (e.g., if the main focus is comment refactoring, notice voting, or admin story deletion), such as "Add comment management and notice voting features" or "Refactor comment components and add admin story deletion".
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

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

이 PR은 책 이야기 및 클럽 공지사항 전반에 걸쳐 댓글 기능을 대폭 개선하고, 공지사항 상세 페이지의 유연성과 관리 기능을 향상시키는 데 중점을 둡니다. 기존 댓글 섹션을 리팩토링하여 완전한 CRUD(생성, 읽기, 업데이트, 삭제) 및 신고 기능을 지원하도록 변경했으며, 이를 사용자 및 관리자 인터페이스 모두에 통합했습니다. 또한, 관리자가 책 이야기를 삭제할 수 있는 기능을 도입하고, 공지사항 내용에 글자 수 제한을 추가하여 사용자 경험과 데이터 무결성을 개선했습니다. 공지사항 상세 페이지는 이제 모임 및 투표 세부 정보를 동적으로 렌더링하며, 관리자에게 공지사항 편집 및 삭제와 같은 특정 액션을 제공합니다.

Highlights

  • 댓글 관리 기능 개선: 책 이야기 및 공지사항 댓글 섹션에 생성, 수정, 삭제, 신고 기능을 추가하여 댓글 관리 기능을 강화했습니다.
  • 관리자 책 이야기 삭제 기능: 관리자가 책 이야기를 삭제할 수 있는 기능을 구현했습니다.
  • 공지사항 상세 페이지 개선: 공지사항 상세 페이지를 동적으로 개선하여 투표 및 모임 상세 정보를 표시하고, 관리자용 편집 및 삭제 액션을 추가했습니다.
  • 공지사항 내용 글자 수 제한: 공지사항 내용에 1000자 글자 수 제한을 추가하고, 입력 시 글자 수를 표시하도록 업데이트했습니다.

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

Using Gemini Code Assist

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

Invoking Gemini

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

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

Customization

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

Limitations & Feedback

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

Footnotes

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

이 PR은 책 이야기 및 공지사항 댓글 기능에 대한 중요한 리팩토링과 개선 사항을 포함하고 있습니다. 특히 공지사항 상세 페이지에서 투표 기능이 NoticeDetail 컴포넌트 내부로 통합되었고, 댓글 시스템은 react-query를 사용하여 비동기 데이터 처리를 개선했습니다. 관리자 페이지의 책 이야기 삭제 기능과 댓글 관리 기능도 추가되었습니다. 전반적으로 기능 개선과 코드 구조화에 기여하는 변경 사항입니다.

{/* 드롭다운 메뉴 */}
{menuOpen && (
<div className="absolute right-0 top-full mt-1 w-[137px] rounded-lg bg-White shadow-lg z-10 overflow-hidden">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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" />
                      )}
                    </>
                  )}

Comment on lines +298 to +300
...(isVoteEnabled && voteDeadlineISO
? { vote: { deadline: voteDeadlineISO } }
: {}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

UpdateClubNoticeRequest의 vote 객체는 deadline만 포함하고 있습니다. 이는 공지사항 수정 시 투표 항목, 익명성, 중복 허용 여부와 같은 다른 투표 세부 정보는 수정할 수 없음을 의미합니다. 현재 UI에서는 투표 항목이 비활성화되어 있지만, 만약 공지사항의 투표 기능을 완전히 편집할 수 있도록 의도했다면, 이러한 필드도 UpdateClubNoticeRequest에 포함되어야 합니다. 의도된 디자인 결정이라면 주석으로 명시하는 것이 좋습니다.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 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 | 🟠 Major

Make the delete affordance consistent across breakpoints.

This only wires handleDelete into 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 the cursorId assignment.

The ?? null is redundant since pageParam is already typed as number | null:

-    cursorId: (pageParam as number | null) ?? null,
+    cursorId: pageParam,

Also, note that per the type definition in src/types/clubnotification.ts, nextCursor is typed as number (not number | null). If the backend returns null when 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 for meData.

The isAdmin flag is derived from meData?.staff, but while useClubMeQuery is loading, meData will be undefined and isAdmin will be false. This could cause the UI to briefly render without admin controls, then flicker to show them once meData loads.

If this causes a noticeable UX issue, consider gating the render on meData loading 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: Replace any with unknown for caught errors.

ESLint correctly flags the use of any for caught exceptions. Use unknown and 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: isMine prop is declared but unused.

The isMine prop is defined in CommentItemProps (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 of canEdit/canDelete/canReport, consider either:

  1. Removing it from the type definition, or
  2. 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 mousedown listener 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 dateString is invalid, new Date(dateString) returns an Invalid Date object, and subsequent methods will return NaN. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 498de60 and 0d3612e.

📒 Files selected for processing (29)
  • src/app/(admin)/admin/(app)/stories/[id]/page.tsx
  • src/app/(main)/stories/[id]/page.tsx
  • src/app/groups/[id]/admin/notice/[noticeId]/page.tsx
  • src/app/groups/[id]/admin/notice/new/page.tsx
  • src/app/groups/[id]/notice/[noticeId]/page.tsx
  • src/components/base-ui/Admin/stories/bookstory_detail.tsx
  • src/components/base-ui/Admin/stories/comment_section.tsx
  • src/components/base-ui/Comment/comment_item.tsx
  • src/components/base-ui/Comment/comment_list_notice.tsx
  • src/components/base-ui/Comment/comment_section.tsx
  • src/components/base-ui/Comment/comment_section_bookcase.tsx
  • src/components/base-ui/Comment/comment_section_notice.tsx
  • src/components/base-ui/Group/notice_detail.tsx
  • src/components/base-ui/Group/vote_notice_detail.tsx
  • src/hooks/mutations/useClubNoticeCommentMutations.ts
  • src/hooks/mutations/useClubNotificationMutations.ts
  • src/hooks/mutations/useReportMemberMutations.ts
  • src/hooks/queries/useClubNoticeCommentQueries.ts
  • src/hooks/queries/useClubNotificationQueries.ts
  • src/lib/api/admin/endpoints/stories.ts
  • src/lib/api/admin/stories.ts
  • src/lib/api/endpoints/Clubnotification.ts
  • src/services/clubNoticeCommentService.ts
  • src/services/clubNotificationService.ts
  • src/services/reportService.ts
  • src/types/clubnotification.ts
  • src/types/report.ts
  • src/types/story.ts
  • tsconfig.json
💤 Files with no reviewable changes (2)
  • tsconfig.json
  • src/components/base-ui/Group/vote_notice_detail.tsx

Comment on lines +85 to +101
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]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 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 -5

Repository: 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 -10

Repository: checkmo2025/FE

Length of output: 291


🏁 Script executed:

# Search more broadly for the target file
find . -type d -name "\[noticeId\]" 2>/dev/null

Repository: 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 -20

Repository: 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 -10

Repository: 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.tsx

Repository: 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.tsx

Repository: checkmo2025/FE

Length of output: 1190


🏁 Script executed:

# Find the hook definition
rg "useClubsBookshelfSimpleInfiniteQuery" --type ts --type tsx -A 5

Repository: 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 -40

Repository: 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 f

Repository: checkmo2025/FE

Length of output: 105


🏁 Script executed:

# Search for the hook definition
rg "useClubsBookshelfSimpleInfiniteQuery" -A 20 --max-count=1

Repository: checkmo2025/FE

Length of output: 6505


🏁 Script executed:

# Read the entire hook definition file
cat -n ./src/hooks/queries/useClubsBookshelfQueries.ts

Repository: checkmo2025/FE

Length of output: 5144


🏁 Script executed:

# Also check what clubsBookshelfService.getSimpleBookshelves returns
rg "getSimpleBookshelves" -A 15

Repository: checkmo2025/FE

Length of output: 2381


🏁 Script executed:

# Find the type definition for ClubsBookshelfSimpleResult
rg "ClubsBookshelfSimpleResult|ClubsBookshelfSimpleResponse" -A 10

Repository: 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.ts

Repository: checkmo2025/FE

Length of output: 871


🏁 Script executed:

# Find the BookShelfInfoItem type definition
rg "interface BookShelfInfoItem|type BookShelfInfoItem" -A 15

Repository: 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 10

Repository: 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.ts

Repository: 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 -5

Repository: checkmo2025/FE

Length of output: 40


🏁 Script executed:

# Also search for eslint config at repo root
ls -la .eslintrc* eslint.config.* 2>/dev/null

Repository: checkmo2025/FE

Length of output: 123


🏁 Script executed:

# Read the ESLint config
cat -n ./eslint.config.mjs

Repository: 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 -50

Repository: 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 5

Repository: 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.

Suggested change
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.

Comment on lines +312 to +321
} 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 || "공지사항 수정 실패");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

wc -l src/app/groups/[id]/admin/notice/[noticeId]/page.tsx

Repository: checkmo2025/FE

Length of output: 114


🏁 Script executed:

sed -n '300,330p' src/app/groups/[id]/admin/notice/[noticeId]/page.tsx

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


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.

Suggested change
} 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.

Comment on lines +36 to +49
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
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't 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.

Comment on lines +267 to +274
<CommentList
comments={comments}
onAddComment={handleAddComment}
onAddReply={handleAddReply}
onEditComment={handleEditComment}
onDeleteComment={handleDeleteComment}
onReportComment={handleReportComment}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +173 to 189

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +110 to +130
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 || '투표에 실패했습니다.');
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix inconsistent indentation and type the error properly.

Two issues in this handler:

  1. The function body has incorrect indentation (should be 4 spaces inside the function)
  2. 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.

Suggested change
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.

Comment on lines +148 to +150
} catch (e: any) {
const msg = e?.message ?? '';
toast.error(msg || '공지사항 삭제에 실패했습니다.');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
} 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.

Comment on lines +99 to +111
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +161 to +164
export interface ClubNoticeCommentAuthorInfo {
nickname: string;
profileImageUrl: string;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if other author/member types use nullable profileImageUrl
rg -n "profileImageUrl" --type ts -C2

Repository: 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.

Comment thread src/types/report.ts
Comment on lines +3 to +8
export type ReportType = "GENERAL" | "BOOK_STORY";

export interface ReportMemberRequest {
reportedMemberNickname: string;
reportType: ReportType;
content: string;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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*=' src

Repository: 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.ts with a narrow ReportType ("GENERAL" | "BOOK_STORY")
  • src/types/member.ts with 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 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 isPending state 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 the accessToken cookie 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: Replace any with a proper error type.

The static analysis tool flagged error: any. Use a more specific type or unknown with 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 AxiosError type 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0d3612e and 3293364.

📒 Files selected for processing (7)
  • src/app/(main)/setting/account-status/page.tsx
  • src/components/base-ui/Settings/SocialLogin/SocialLoginCard.tsx
  • src/hooks/mutations/useMemberMutations.ts
  • src/hooks/queries/useMemberQueries.ts
  • src/lib/api/endpoints/member.ts
  • src/services/memberService.ts
  • src/types/member.ts
✅ Files skipped from review due to trivial changes (2)
  • src/types/member.ts
  • src/lib/api/endpoints/member.ts

Comment on lines +10 to +13
local: {
bgColor: "bg-White border border-Gray-2",
icon: "/profile10.svg",
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 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 f

Repository: checkmo2025/FE

Length of output: 281


🏁 Script executed:

cat -n tailwind.config.js

Repository: 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.

@shinwokkang
shinwokkang merged commit ab5a52a into main Mar 20, 2026
4 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Mar 26, 2026
3 of 8 tasks
@coderabbitai coderabbitai Bot mentioned this pull request Apr 24, 2026
8 tasks
This was referenced May 21, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Jul 20, 2026
8 tasks

This branch was successfully deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants