Skip to content

05.22 최신화 - #341

Merged
yhi9839 merged 10 commits into
mainfrom
dev
May 22, 2026
Merged

yhi9839 merged 10 commits into
mainfrom
dev

Conversation

@yhi9839

@yhi9839 yhi9839 commented May 22, 2026 •

Copy link
Copy Markdown
Contributor

📌 개요 (Summary)

05.22 최신화 dev -> main 머지

Summary by CodeRabbit

Release Notes

  • New Features

    • Added member blocking and unblocking functionality
    • Added ability to view blocked users list
    • Blocked users now display with special styling in comments and profiles
  • Improvements

    • Enhanced error handling for blocked user scenarios and related operations
    • Improved error messages throughout the app
  • Chores

    • Integrated Google Tag Manager for analytics
    • Updated brand name from "CheckMo" to "책모"
    • Added new dependencies

Review Change Stack

@vercel

vercel Bot commented May 22, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
checkmo Ready Ready Preview, Comment May 22, 2026 2:16am

@coderabbitai

coderabbitai Bot commented May 22, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR implements user blocking functionality across the application: members can block others, view their block list with infinite scrolling, see masked blocked users in comments, and receive appropriate error messages when blocked. It also includes brand updates and Google Tag Manager setup.

Changes

User Blocking Feature

Layer / File(s) Summary
Data models and error handling
src/types/member.ts, src/constants/masking.ts, src/lib/api/errors/ApiError.ts, src/lib/api/errorMapper.ts, src/lib/api/errors/errorMapper.ts
BlockedUser and BlockListResponse types, BLOCKED_USER_MASK constant, ErrorWithCode interface with hasErrorCode type guard, and error message mappings for all block-related, comment, and story error codes.
API endpoints and member service
src/lib/api/endpoints/member.ts, src/services/memberService.ts
New BLOCK and GET_MY_BLOCKS endpoints; blockMember, unblockMember, and getBlockedList service methods; refactored pagination URL construction in getFollowerList, getFollowingList, and getMyReports.
State management and hooks
src/store/useBlockStore.ts, src/hooks/queries/useMemberQueries.ts, src/hooks/mutations/useMemberMutations.ts
Zustand useBlockStore with nickname set and async initializeBlocks(); infinite query useBlockedUsersQuery; mutations useBlockMemberMutation and useUnblockMemberMutation with success/error toast and cache invalidation.
Blocked users management page
src/app/(main)/setting/block/page.tsx
Real data fetching via useBlockedUsersQuery, infinite scroll with useInView, and unblock action wired to useUnblockMemberMutation.
Comment system block integration
src/components/base-ui/Comment/comment_item.tsx, src/components/base-ui/Comment/comment_list.tsx, src/components/base-ui/Comment/comment_section_bookcase.tsx
CommentItem accepts optional isBlocked prop for conditional styling; CommentList disables reply/report/profile handlers for blocked comments; CommentSection initializes block store, detects blocked authors, and handles COMMENT_405 error code.
Profile and story block handling
src/components/base-ui/Profile/OtherUser/ProfileUserInfo.tsx, src/app/(main)/stories/[id]/page.tsx, src/hooks/mutations/useStoryMutations.ts
Profile page wires useBlockMemberMutation for real blocking; story detail and like mutations extract and display API error codes (BLOCK_404, BLOCK_405, BOOK_STORY_408).
Auth and flow updates
src/store/useAuthStore.ts, src/hooks/useReportBlockFlow.ts
Logout resets block store; block flow delegates toast to mutation hook instead of showing duplicate.

Analytics and Branding

Layer / File(s) Summary
Google Tag Manager and brand metadata
package.json, src/app/layout.tsx
Added @next/third-parties dependency; imported and injected GoogleTagManager with NEXT_PUBLIC_GTM_ID; updated metadata title, description, and OpenGraph values from "CheckMo" to "책모".
Logo alt text updates
src/components/base-ui/Landing/LandingNav.tsx, src/components/layout/AdminHeader.tsx, src/components/layout/Header.tsx
Shortened logo alt text from "책모 로고" to "책모" across all header components.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • checkmo2025/FE#336: Overlapping block/unblock integration affecting BlockPage, useBlockedUsersQuery, comment rendering, and profile error handling.
  • checkmo2025/FE#315: Refactors the same block-management page to replace placeholder logic with real query and mutation wiring.
  • checkmo2025/FE#220: Introduced comment section component being extended here with block-aware rendering and error handling.

Suggested labels

✨ feat, ♻️ refactor, 🐛 bug

Poem

A rabbit hops through the block list with glee,
Masking rude users for all eyes to see,
Comments now hidden when blocking's complete,
Errors cascade down—a feature so sweet! 🐰

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% 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 "05.22 최신화" ("05.22 update") is extremely vague and generic, providing no meaningful information about the changeset. Replace with a descriptive title that captures the main change, such as "Implement user blocking feature and Google Tag Manager integration" or similar.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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 gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request replaces mock data with a functional user blocking system, including a dedicated block management page, block/unblock mutations, and a local block store for immediate UI feedback. It also integrates Google Tag Manager, updates project metadata, and implements comprehensive error handling for blocked relationships across stories, profiles, and comments. Feedback focuses on improving reactivity by using useMemo for blocked comment states, preventing concurrent initialization calls in the block store, and utilizing the new hasErrorCode utility for safer error handling. Additionally, it is recommended to add conditional rendering for the GTM component and ensure profile queries are invalidated upon blocking or unblocking a user to maintain UI consistency.

Comment on lines +65 to +68
isBlocked: !c.deleted && (
c.authorInfo?.nickname === BLOCKED_USER_MASK ||
(c.authorInfo?.nickname ? checkLocalBlocked(c.authorInfo.nickname) : false)
),

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.

high

isBlocked 판별 로직이 useEffect 내에서만 업데이트되는 comments 상태에 포함되어 있어, 로컬 차단 상태(useBlockStore)가 변경되어도 UI에 즉시 반영되지 않는 문제가 있습니다. comments를 useState와 useEffect 대신 useMemo를 사용하여 파생 상태로 관리하고, 의존성 배열에 useBlockStore의 blockedNicknames를 추가하여 반응성을 확보해 주세요.

Comment on lines +30 to +31
initializeBlocks: async () => {
if (get().isInitialized) return;

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

initializeBlocks 함수는 비동기로 동작하며 여러 페이지를 순차적으로 호출합니다. 만약 여러 컴포넌트에서 동시에 이 함수를 호출할 경우, isInitialized가 true로 설정되기 전까지 중복된 API 요청 체인이 병렬로 실행될 수 있습니다. isInitializing과 같은 플래그를 상태에 추가하여 중복 호출을 방지하는 로직을 구현하는 것을 권장합니다.

Comment on lines +81 to +85
const apiError = error as { code?: string } | null;
const message =
(apiError?.code === "BLOCK_404" || apiError?.code === "BLOCK_405")
? getErrorMessage(apiError.code)
: "해당 책 이야기를 찾을 수 없습니다.";

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

ApiError.ts에 추가된 hasErrorCode 유틸리티 함수를 사용하여 에러 코드를 확인하는 것이 타입 안정성 측면에서 더 좋습니다. 수동 타입 캐스팅(as)을 피하고 코드의 가독성을 높일 수 있습니다. (참고: hasErrorCode 임포트가 필요할 수 있습니다.)

    const message =
      (hasErrorCode(error) && (error.code === "BLOCK_404" || error.code === "BLOCK_405"))
        ? getErrorMessage(error.code)
        : "해당 책 이야기를 찾을 수 없습니다.";

Comment on lines +104 to +108
const apiError = error as { code?: string } | null;
const message =
(apiError?.code === "BLOCK_404" || apiError?.code === "BLOCK_405")
? getErrorMessage(apiError.code)
: "프로필 정보를 불러올 수 없습니다.";

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

프로필 조회 에러 처리 시 수동 타입 캐스팅 대신 hasErrorCode 유틸리티를 사용하여 타입 안정성을 확보하는 것이 좋습니다.

    const message =
      (hasErrorCode(error) && (error.code === "BLOCK_404" || error.code === "BLOCK_405"))
        ? getErrorMessage(error.code)
        : "프로필 정보를 불러올 수 없습니다.";

Comment thread src/app/layout.tsx
}>) {
return (
<html lang="en">
<GoogleTagManager gtmId={process.env.NEXT_PUBLIC_GTM_ID!} />

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

process.env.NEXT_PUBLIC_GTM_ID 환경 변수가 설정되지 않은 경우를 대비하여 조건부 렌더링을 적용하는 것이 안전합니다. 비확정 연산자(!) 사용은 환경 변수 누락 시 런타임 오류를 유발할 수 있습니다.

Suggested change
<GoogleTagManager gtmId={process.env.NEXT_PUBLIC_GTM_ID!} />
{process.env.NEXT_PUBLIC_GTM_ID && <GoogleTagManager gtmId={process.env.NEXT_PUBLIC_GTM_ID} />}

Comment on lines +406 to +407
queryClient.invalidateQueries({ queryKey: memberKeys.blocks() });
queryClient.invalidateQueries({ queryKey: storyKeys.all, refetchType: 'none' });

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

사용자를 차단할 때 해당 사용자의 프로필 정보도 최신화되어야 합니다. onSuccess에서 memberKeys.otherProfile(nickname) 쿼리도 함께 무효화하여 프로필 페이지의 UI가 즉시 업데이트되도록 수정하는 것을 권장합니다.

            queryClient.invalidateQueries({ queryKey: memberKeys.blocks() });
            queryClient.invalidateQueries({ queryKey: memberKeys.otherProfile(nickname) });
            queryClient.invalidateQueries({ queryKey: storyKeys.all, refetchType: 'none' });

Comment on lines +427 to +428
queryClient.invalidateQueries({ queryKey: memberKeys.blocks() });
queryClient.invalidateQueries({ queryKey: storyKeys.all, refetchType: 'none' });

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

차단 해제 시에도 해당 사용자의 프로필 정보를 무효화하여 UI가 즉시 갱신되도록 처리하는 것이 좋습니다.

            queryClient.invalidateQueries({ queryKey: memberKeys.blocks() });
            queryClient.invalidateQueries({ queryKey: memberKeys.otherProfile(nickname) });
            queryClient.invalidateQueries({ queryKey: storyKeys.all, refetchType: 'none' });

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/(main)/stories/[id]/page.tsx (1)

34-43: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix React hook ordering: avoid conditional useStoryDetailQuery call

src/app/(main)/stories/[id]/page.tsx calls useStoryDetailQuery only after an early-return guard for !id || isNaN(storyId), which violates the Rules of Hooks. This is exactly the pattern that react-hooks/rules-of-hooks flags, since hook invocation must be unconditional and consistent across renders.

Also, src/hooks/queries/useStoryQueries.ts defines export const useStoryDetailQuery = (id: number) => useQuery(...) with no enabled/options parameter in its signature, so the proposed “enabled-style” fix requires changing the hook API (or moving the guard inside the query via enabled while still calling the hook unconditionally).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(main)/stories/[id]/page.tsx around lines 34 - 43, The hook call
must be unconditional: always call useStoryDetailQuery from page.tsx and gate
fetching with an enabled option; update useStoryDetailQuery in
useStoryQueries.ts to accept either an options param or an optional id (e.g.,
useStoryDetailQuery(id: number | undefined, options?: UseQueryOptions)) and pass
{ enabled: typeof id === 'number' && !isNaN(id) } into the internal useQuery,
then in page.tsx call useStoryDetailQuery(storyId, { enabled: !!id &&
!isNaN(storyId) }) so the hook is invoked on every render while preventing the
fetch when id is invalid, and keep the existing early-return UI for the
invalid-id case.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/layout.tsx`:
- Line 51: The GoogleTagManager invocation is unguarded and uses a non-null
assertion on NEXT_PUBLIC_GTM_ID; remove the `!` and only render
<GoogleTagManager ...> when process.env.NEXT_PUBLIC_GTM_ID is truthy (or supply
a fallback via the gtmScriptUrl prop) so the component receives a valid gtmId;
update the call site (GoogleTagManager gtmId={process.env.NEXT_PUBLIC_GTM_ID})
to be conditionally rendered or passed a proper gtmScriptUrl when the env var is
unset.

In `@src/components/base-ui/Comment/comment_item.tsx`:
- Line 38: The comment indicates blocked comments still show actionable menu
items because report/menu visibility is computed from !isMine; update the logic
that computes derivedCanReport to also require the comment not be blocked
(include !isBlocked) and ensure any menu/rendering checks that currently use
!isMine (or similar) instead consult derivedCanReport so blocked comments cannot
show report/menu actions; modify the prop/variable references isBlocked, isMine
and derivedCanReport accordingly.

In `@src/components/base-ui/Comment/comment_section_bookcase.tsx`:
- Around line 43-49: The comment mapping uses stale local block state because
the component only initializes blocks on mount and does not re-run the remapping
when the block list changes; update the effect(s) or render logic that compute
per-comment isBlocked values (where you reference checkLocalBlocked from
useBlockStore and the mapping around lines 65-68) to depend on the block-store
slice so remapping happens when the store updates—either include the store
selector (checkLocalBlocked or the store's block list) in the dependency array
of the useEffect that computes/memoizes the mapped comments or subscribe
directly to that slice from useBlockStore so changes trigger a re-render and
comments’ isBlocked flags are recalculated; keep initializeBlocks call for login
but ensure reactive updates after block/unblock actions.

In `@src/hooks/mutations/useMemberMutations.ts`:
- Around line 409-413: Change the onError handlers in useMemberMutations to
accept error: unknown instead of any and narrow it before accessing properties;
e.g., in the onError for the block member mutation and the other mutation in
useMemberMutations, replace (error: any) with (error: unknown) and then either
use axios' isAxiosError(error) if you have axios (import and call isAxiosError
to access error.response.data.message) or add a small type guard like function
isErrWithMsg(e: unknown): e is { response?: { data?: { message?: string } };
message?: string } { return typeof e === 'object' && e !== null; } and then
const errorMessage = isErrWithMsg(error) ? error.response?.data?.message ??
error.message ?? "차단에 실패했습니다." : "차단에 실패했습니다."; finally pass that to toast.error
and console.error.

In `@src/lib/api/errors/errorMapper.ts`:
- Around line 16-18: The block-error mappings are inconsistent: the errorMapper
that currently only defines BLOCK_404 and BLOCK_405 must be synchronized with
the other error mapper so the same block codes yield identical messages; either
add the missing block-code keys (the same set the other mapper exposes) into
this errorMapper (ensuring the same string values for BLOCK_404, BLOCK_405 and
the other block_* keys) or consolidate both modules into a single shared error
map and have both consumers import it (update the export/import to use the
common error map), so error keys like BLOCK_404 and BLOCK_405 resolve
consistently across the app.

In `@src/services/memberService.ts`:
- Around line 88-90: The pagination check incorrectly treats 0 as falsy so
cursorId 0 is not appended; update the condition that guards
params.append("cursorId", cursorId.toString()) to explicitly allow 0 (e.g.,
check for null/undefined with cursorId != null or cursorId !== undefined &&
cursorId !== null) and apply this change in the memberService functions that
build pagination params: getFollowerList, getFollowingList, getMyReports,
getBlockedList (the block around params.append("cursorId", cursorId.toString()))
so nextCursor = 0 is forwarded correctly.

In `@src/store/useBlockStore.ts`:
- Around line 40-43: The loop prematurely stops when nextCursor is 0 because it
uses falsy checks; change the condition in useBlockStore (where hasNext =
data.hasNext; cursorId = data.nextCursor ?? undefined; if (!data.hasNext ||
!data.nextCursor) break;) to use nullish checks (e.g., if (data.hasNext ===
false || data.nextCursor == null) break) and set cursorId to data.nextCursor
(allow 0) rather than coercing to undefined; also update
memberService.getBlockedList where it currently does if (cursorId) to append the
cursor parameter even when cursorId === 0 (use cursorId != null or typeof
cursorId !== 'undefined' to decide whether to include it).

---

Outside diff comments:
In `@src/app/`(main)/stories/[id]/page.tsx:
- Around line 34-43: The hook call must be unconditional: always call
useStoryDetailQuery from page.tsx and gate fetching with an enabled option;
update useStoryDetailQuery in useStoryQueries.ts to accept either an options
param or an optional id (e.g., useStoryDetailQuery(id: number | undefined,
options?: UseQueryOptions)) and pass { enabled: typeof id === 'number' &&
!isNaN(id) } into the internal useQuery, then in page.tsx call
useStoryDetailQuery(storyId, { enabled: !!id && !isNaN(storyId) }) so the hook
is invoked on every render while preventing the fetch when id is invalid, and
keep the existing early-return UI for the invalid-id case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 413cbe7c-cbaa-4158-b514-c254f8443007

📥 Commits

Reviewing files that changed from the base of the PR and between eb43f46 and 0d88983.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (24)
  • package.json
  • src/app/(main)/setting/block/page.tsx
  • src/app/(main)/stories/[id]/page.tsx
  • src/app/layout.tsx
  • src/components/base-ui/Comment/comment_item.tsx
  • src/components/base-ui/Comment/comment_list.tsx
  • src/components/base-ui/Comment/comment_section_bookcase.tsx
  • src/components/base-ui/Landing/LandingNav.tsx
  • src/components/base-ui/Profile/OtherUser/ProfileUserInfo.tsx
  • src/components/layout/AdminHeader.tsx
  • src/components/layout/Header.tsx
  • src/constants/masking.ts
  • src/hooks/mutations/useMemberMutations.ts
  • src/hooks/mutations/useStoryMutations.ts
  • src/hooks/queries/useMemberQueries.ts
  • src/hooks/useReportBlockFlow.ts
  • src/lib/api/endpoints/member.ts
  • src/lib/api/errorMapper.ts
  • src/lib/api/errors/ApiError.ts
  • src/lib/api/errors/errorMapper.ts
  • src/services/memberService.ts
  • src/store/useAuthStore.ts
  • src/store/useBlockStore.ts
  • src/types/member.ts

Comment thread src/app/layout.tsx
}>) {
return (
<html lang="en">
<GoogleTagManager gtmId={process.env.NEXT_PUBLIC_GTM_ID!} />

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 | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Next.js (@next/third-parties/google), what is the documented behavior if GoogleTagManager receives an undefined or empty gtmId, and is conditional rendering the recommended pattern when env vars are missing?

💡 Result:

  • Documented behavior for undefined / empty gtmId: The Next.js docs don’t describe any special handling for an empty or undefined gtmId; they only state that gtmId is required, except when gtmScriptUrl is provided (to support Google tag gateway). [1]
    In the actual implementation, gtmId is only appended to the GTM script URL when it’s truthy (if (gtmId) { scriptUrl.searchParams.set('id', gtmId) }). So an undefined or "" gtmId results in the component still rendering the scripts, but the gtm.js request is made without the id query param. [2]

  • Is conditional rendering recommended when env vars are missing? The Next.js documentation for @next/third-parties/google does not recommend conditional rendering as a pattern for missing env vars; it just documents the prop requirements/optionality (gtmId required*, gtmScriptUrl optional*). [1]

Sources:
[1] Next.js Docs – “How to optimize third-party libraries” (GoogleTagManager options / requiredness) (nextjs.org)
[2] Next.js source (packages/third-parties/src/google/gtm.tsx) showing gtmId is only added when truthy (github.com)

Citations:


Guard GoogleTagManager when NEXT_PUBLIC_GTM_ID is unset.

@next/third-parties/google only sets the GTM script id when gtmId is truthy; if NEXT_PUBLIC_GTM_ID is missing/empty, GTM loads without the id query param (tracking won’t work as intended). Remove the ! and conditionally render (or provide gtmScriptUrl) when missing.

Suggested patch
 export default function RootLayout({
   children,
 }: Readonly<{
   children: React.ReactNode;
 }>) {
+  const gtmId = process.env.NEXT_PUBLIC_GTM_ID;
+
   return (
     <html lang="en">
-      <GoogleTagManager gtmId={process.env.NEXT_PUBLIC_GTM_ID!} />
+      {gtmId ? <GoogleTagManager gtmId={gtmId} /> : null}
       <body
         className={`${geistSans.variable} ${geistMono.variable} antialiased pb-[70px] t:pb-0`}
       >
📝 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
<GoogleTagManager gtmId={process.env.NEXT_PUBLIC_GTM_ID!} />
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const gtmId = process.env.NEXT_PUBLIC_GTM_ID;
return (
<html lang="en">
{gtmId ? <GoogleTagManager gtmId={gtmId} /> : null}
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased pb-[70px] t:pb-0`}
>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/layout.tsx` at line 51, The GoogleTagManager invocation is unguarded
and uses a non-null assertion on NEXT_PUBLIC_GTM_ID; remove the `!` and only
render <GoogleTagManager ...> when process.env.NEXT_PUBLIC_GTM_ID is truthy (or
supply a fallback via the gtmScriptUrl prop) so the component receives a valid
gtmId; update the call site (GoogleTagManager
gtmId={process.env.NEXT_PUBLIC_GTM_ID}) to be conditionally rendered or passed a
proper gtmScriptUrl when the env var is unset.

createdAt,
isAuthor = false,
isMine = false,
isBlocked = false,

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 | ⚡ Quick win

Blocked mode still leaves menu actions effectively enabled.

With isBlocked enabled at Line 38, report/menu visibility is still derived from !isMine, so blocked comments can still render actionable menu UI. Gate report capability with !isBlocked in derivedCanReport.

Proposed fix
-  const derivedCanReport = canReport || !isMine;
+  const derivedCanReport = !isBlocked && (canReport || !isMine);

Also applies to: 84-84

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/base-ui/Comment/comment_item.tsx` at line 38, The comment
indicates blocked comments still show actionable menu items because report/menu
visibility is computed from !isMine; update the logic that computes
derivedCanReport to also require the comment not be blocked (include !isBlocked)
and ensure any menu/rendering checks that currently use !isMine (or similar)
instead consult derivedCanReport so blocked comments cannot show report/menu
actions; modify the prop/variable references isBlocked, isMine and
derivedCanReport accordingly.

Comment on lines +43 to +49
const { isBlocked: checkLocalBlocked, initializeBlocks } = useBlockStore();

useEffect(() => {
if (isLoggedIn) {
initializeBlocks();
}
}, [isLoggedIn, initializeBlocks]);

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 | ⚡ Quick win

Blocked-comment masking can go stale after block store updates.

Line 65 uses local block state in mapping, but remapping is not tied to block-list changes. After initializeBlocks() or block/unblock actions, comments may keep old isBlocked values until unrelated props change.

Suggested direction
- const { isBlocked: checkLocalBlocked, initializeBlocks } = useBlockStore();
+ const { isBlocked: checkLocalBlocked, initializeBlocks, blockedNicknames } = useBlockStore();

  useEffect(() => {
    // eslint-disable-next-line react-hooks/set-state-in-effect
    setComments(mapApiToUiComments(initialComments));
- }, [initialComments, storyAuthorNickname]);
+ }, [initialComments, storyAuthorNickname, blockedNicknames]);

(If the store key is named differently, subscribe to that equivalent state slice.)

Also applies to: 65-68

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/base-ui/Comment/comment_section_bookcase.tsx` around lines 43
- 49, The comment mapping uses stale local block state because the component
only initializes blocks on mount and does not re-run the remapping when the
block list changes; update the effect(s) or render logic that compute
per-comment isBlocked values (where you reference checkLocalBlocked from
useBlockStore and the mapping around lines 65-68) to depend on the block-store
slice so remapping happens when the store updates—either include the store
selector (checkLocalBlocked or the store's block list) in the dependency array
of the useEffect that computes/memoizes the mapped comments or subscribe
directly to that slice from useBlockStore so changes trigger a re-render and
comments’ isBlocked flags are recalculated; keep initializeBlocks call for login
but ensure reactive updates after block/unblock actions.

Comment on lines +409 to +413
onError: (error: any) => {
console.error("Failed to block member:", error);
const errorMessage = error.response?.data?.message || error.message || "차단에 실패했습니다.";
toast.error(errorMessage);
},

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify rule presence and explicit-any usage related to this file
rg -n '`@typescript-eslint/no-explicit-any`'
rg -n --type=ts '\berror:\s*any\b' src/hooks/mutations/useMemberMutations.ts

Repository: checkmo2025/FE

Length of output: 584


Replace any in useMemberMutations mutation error handlers with unknown.

src/hooks/mutations/useMemberMutations.ts uses onError: (error: any) at lines 409-413 and 430-434, which violates @typescript-eslint/no-explicit-any and can break lint/CI. Narrow the error type before reading response?.data?.message / message.

Proposed fix
-        onError: (error: any) =&gt; {
+        onError: (error: unknown) =&gt; {
             console.error("Failed to block member:", error);
-            const errorMessage = error.response?.data?.message || error.message || "차단에 실패했습니다.";
+            const errorMessage =
+                (typeof error === "object" &amp;&amp;
+                    error !== null &amp;&amp;
+                    "response" in error &amp;&amp;
+                    (error as { response?: { data?: { message?: string } } }).response?.data?.message) ||
+                (error instanceof Error ? error.message : undefined) ||
+                "차단에 실패했습니다.";
             toast.error(errorMessage);
         },
...
-        onError: (error: any) =&gt; {
+        onError: (error: unknown) =&gt; {
             console.error("Failed to unblock member:", error);
-            const errorMessage = error.response?.data?.message || error.message || "차단 해제에 실패했습니다.";
+            const errorMessage =
+                (typeof error === "object" &amp;&amp;
+                    error !== null &amp;&amp;
+                    "response" in error &amp;&amp;
+                    (error as { response?: { data?: { message?: string } } }).response?.data?.message) ||
+                (error instanceof Error ? error.message : undefined) ||
+                "차단 해제에 실패했습니다.";
             toast.error(errorMessage);
         },
📝 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
onError: (error: any) => {
console.error("Failed to block member:", error);
const errorMessage = error.response?.data?.message || error.message || "차단에 실패했습니다.";
toast.error(errorMessage);
},
onError: (error: unknown) => {
console.error("Failed to block member:", error);
const errorMessage =
(typeof error === "object" &&
error !== null &&
"response" in error &&
(error as { response?: { data?: { message?: string } } }).response?.data?.message) ||
(error instanceof Error ? error.message : undefined) ||
"차단에 실패했습니다.";
toast.error(errorMessage);
},
🧰 Tools
🪛 ESLint

[error] 409-409: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/mutations/useMemberMutations.ts` around lines 409 - 413, Change the
onError handlers in useMemberMutations to accept error: unknown instead of any
and narrow it before accessing properties; e.g., in the onError for the block
member mutation and the other mutation in useMemberMutations, replace (error:
any) with (error: unknown) and then either use axios' isAxiosError(error) if you
have axios (import and call isAxiosError to access error.response.data.message)
or add a small type guard like function isErrWithMsg(e: unknown): e is {
response?: { data?: { message?: string } }; message?: string } { return typeof e
=== 'object' && e !== null; } and then const errorMessage = isErrWithMsg(error)
? error.response?.data?.message ?? error.message ?? "차단에 실패했습니다." : "차단에
실패했습니다."; finally pass that to toast.error and console.error.

Comment on lines +16 to +18
// Block Errors
BLOCK_404: "차단한 사용자입니다.",
BLOCK_405: "조회가 불가능한 프로필입니다.",

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 | ⚡ Quick win

Synchronize block error mappings across both mapper modules.

src/lib/api/errors/errorMapper.ts now handles only BLOCK_404/BLOCK_405, while src/lib/api/errorMapper.ts includes additional block codes. This split can cause generic fallback messages depending on import path. Mirror the same block-code coverage (or consolidate to one mapper) to avoid inconsistent UX.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/api/errors/errorMapper.ts` around lines 16 - 18, The block-error
mappings are inconsistent: the errorMapper that currently only defines BLOCK_404
and BLOCK_405 must be synchronized with the other error mapper so the same block
codes yield identical messages; either add the missing block-code keys (the same
set the other mapper exposes) into this errorMapper (ensuring the same string
values for BLOCK_404, BLOCK_405 and the other block_* keys) or consolidate both
modules into a single shared error map and have both consumers import it (update
the export/import to use the common error map), so error keys like BLOCK_404 and
BLOCK_405 resolve consistently across the app.

Comment on lines 88 to +90
if (cursorId) {
url.searchParams.append("cursorId", cursorId.toString());
params.append("cursorId", cursorId.toString());
}

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/services/memberService.ts"

echo "=== File exists? ==="
ls -l "$FILE" || true

echo "=== cursorId conditionals in memberService.ts ==="
rg -n "cursorId" "$FILE" || true

echo "=== Show surrounding pagination sections (lines ~60-200) ==="
nl -ba "$FILE" | sed -n '60,220p'

echo "=== Show function signatures / types around those blocks ==="
# Print nearby text for each occurrence
for L in 88 102 133 177; do
  echo "--- Around line $L ---"
  nl -ba "$FILE" | sed -n "$((L-20)),$((L+20))p"
done

echo "=== Search for cursorId type definitions across repo ==="
rg -n --glob='*.ts' "cursorId" . || true

Repository: checkmo2025/FE

Length of output: 1069


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/services/memberService.ts"

echo "=== Display memberService.ts with line numbers around pagination code ==="
# Use cat -n for line numbers
cat -n "$FILE" | sed -n '70,115p'
cat -n "$FILE" | sed -n '115,155p'
cat -n "$FILE" | sed -n '160,200p'

echo "=== Search for cursorId handling elsewhere ==="
rg -n --glob='*.ts' --glob='*.tsx' "cursorId" . || true

echo "=== Search for hardcoded 0 cursors or cursor-related values ==="
rg -n --glob='*.ts' --glob='*.tsx' "(cursorId\\s*[:=]\\s*0|cursorId\\s*\\?\\?\\s*0|set.*Cursor.*\\(0\\)|nextCursor\\s*[:=]\\s*0)" . || true

echo "=== Search for 'nextCursor' / 'cursor' naming in responses ==="
rg -n --glob='*.ts' --glob='*.tsx' "(nextCursor|next_cursor|cursor|pageCursor)" . || true

Repository: checkmo2025/FE

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/services/memberService.ts"

echo "=== Display memberService.ts with line numbers around pagination code ==="
# Use cat -n for line numbers
cat -n "$FILE" | sed -n '70,115p'
cat -n "$FILE" | sed -n '115,155p'
cat -n "$FILE" | sed -n '160,200p'

echo "=== Search for cursorId handling elsewhere ==="
rg -n --glob='*.ts' --glob='*.tsx' "cursorId" . || true

echo "=== Search for hardcoded 0 cursors or cursor-related values ==="
rg -n --glob='*.ts' --glob='*.tsx' "(cursorId\\s*[:=]\\s*0|cursorId\\s*\\?\\?\\s*0|set.*Cursor.*\\(0\\)|nextCursor\\s*[:=]\\s*0)" . || true

echo "=== Search for 'nextCursor' / 'cursor' naming in responses ==="
rg -n --glob='*.ts' --glob='*.tsx' "(nextCursor|next_cursor|cursor|pageCursor)" . || true

Repository: checkmo2025/FE

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --glob='*.ts' --glob='*.tsx' "getFollowerList|getFollowingList|getMyReports|getBlockedList" src || true

echo "=== Search nextCursor truthiness / falsy checks ==="
rg -n --glob='*.ts' --glob='*.tsx' "nextCursor\\s*\\?\\?\\s*undefined|!data\\.nextCursor|!lastPage\\.nextCursor|nextCursor\\)\\s*\\{" src || true
rg -n --glob='*.ts' --glob='*.tsx' "if \\(.*nextCursor.*\\)" src || true

echo "=== Search cursorId usage when passed into memberService methods ==="
rg -n --glob='*.ts' --glob='*.tsx' "memberService\\.(getFollowerList|getFollowingList|getMyReports|getBlockedList)\\(.*cursorId" src || true

echo "=== Search for nextCursor assigned literal 0 anywhere ==="
rg -n --glob='*.ts' --glob='*.tsx' "nextCursor\\s*[:=]\\s*0\\b|cursorId\\s*[:=]\\s*0\\b" src || true

Repository: checkmo2025/FE

Length of output: 2767


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/store/useBlockStore.ts"
echo "=== memberService call sites ==="
rg -n "getBlockedList|nextCursor|hasNext" "$FILE" || true
echo "=== Show file excerpt ==="
cat -n "$FILE" | sed -n '1,120p'

Repository: checkmo2025/FE

Length of output: 2649


🏁 Script executed:

rg -n --glob='*.ts' --glob='*.tsx' "nextCursor\\b" src/store src/hooks src/components src/services \
  | rg -n "!.*nextCursor|if \\(.*nextCursor.*\\)" || true

Repository: checkmo2025/FE

Length of output: 277


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/hooks/queries/useMemberQueries.ts"
echo "=== member queries file excerpt ==="
cat -n "$FILE" | sed -n '1,140p'

Repository: checkmo2025/FE

Length of output: 4458


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/types/member.ts"
echo "=== member.ts excerpt around nextCursor ==="
cat -n "$FILE" | sed -n '1,160p'

echo "=== Find response interfaces that include nextCursor ==="
rg -n "nextCursor" "$FILE"

Repository: checkmo2025/FE

Length of output: 3430


Fix cursor pagination param handling for cursorId=0

memberService’s methods accept cursorId?: number, and the pagination contract returns nextCursor: number | null. In the infinite queries, getNextPageParam can pass nextCursor through as pageParam (so 0 can reach memberService). With if (cursorId), a valid 0 cursor is treated as falsy and won’t be appended, breaking pagination.

Apply the same fix to getFollowerList, getFollowingList, getMyReports, and getBlockedList.

Proposed fix
-        if (cursorId) {
+        if (cursorId !== undefined) {
             params.append("cursorId", cursorId.toString());
         }
📝 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
if (cursorId) {
url.searchParams.append("cursorId", cursorId.toString());
params.append("cursorId", cursorId.toString());
}
if (cursorId !== undefined) {
params.append("cursorId", cursorId.toString());
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/memberService.ts` around lines 88 - 90, The pagination check
incorrectly treats 0 as falsy so cursorId 0 is not appended; update the
condition that guards params.append("cursorId", cursorId.toString()) to
explicitly allow 0 (e.g., check for null/undefined with cursorId != null or
cursorId !== undefined && cursorId !== null) and apply this change in the
memberService functions that build pagination params: getFollowerList,
getFollowingList, getMyReports, getBlockedList (the block around
params.append("cursorId", cursorId.toString())) so nextCursor = 0 is forwarded
correctly.

Comment on lines +40 to +43
hasNext = data.hasNext;
cursorId = data.nextCursor ?? undefined;
if (!data.hasNext || !data.nextCursor) {
break;

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Show the relevant code around the reported lines
echo "=== useBlockStore.ts: initializeBlocks pagination loop (around lines 25-80) ==="
nl -ba src/store/useBlockStore.ts | sed -n '1,120p'

# 2) Find where nextCursor is defined/typed/assigned in this repo
echo
echo "=== Search for nextCursor definitions/types/uses ==="
rg -n "nextCursor" .

echo
echo "=== Search for cursorId usage in this file and others ==="
rg -n "cursorId" src .

echo
echo "=== Search for pagination/hasNext usage patterns ==="
rg -n "hasNext" src .

Repository: checkmo2025/FE

Length of output: 199


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ls -la src/store/useBlockStore.ts || true
command -v rg || true
command -v cat || true
command -v python3 || true

echo
echo "=== useBlockStore.ts: initializeBlocks pagination loop (around lines 25-90) ==="
python3 - <<'PY'
from pathlib import Path
p = Path("src/store/useBlockStore.ts")
lines = p.read_text().splitlines()
start, end = 20, 95
for i in range(start, min(end, len(lines))):
    print(f"{i+1:4d}\t{lines[i]}")
PY

echo
echo "=== Search for nextCursor definitions/types/uses ==="
rg -n --hidden --no-ignore-vcs "nextCursor" src . || true

echo
echo "=== Search for cursorId usage (src) ==="
rg -n "cursorId" src || true

echo
echo "=== Search for pagination loop patterns with hasNext/nextCursor ==="
rg -n "hasNext.*nextCursor|nextCursor.*hasNext|!data\\.hasNext|nextCursor \\?\\? undefined|== null" src || true

Repository: checkmo2025/FE

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for memberService.getBlockedList ==="
rg -n "getBlockedList" src || true

echo
echo "=== Search for interface/type of blocked list response near getBlockedList ==="
rg -n "blocked.*List|BlockedList|hasNext|nextCursor" src || true

echo
echo "=== Search for nextCursor only in src (no node_modules) ==="
rg -n "nextCursor" src || true

echo
echo "=== Search for nextCursor usage patterns in src ==="
rg -n "nextCursor.*\\?\\?|!.*nextCursor|nextCursor\\s*==\\s*null|nextCursor\\s*==\\s*undefined" src || true

Repository: checkmo2025/FE

Length of output: 25173


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== memberService.ts around getBlockedList ==="
python3 - <<'PY'
from pathlib import Path
p = Path("src/services/memberService.ts")
lines = p.read_text().splitlines()
# best-effort: find getBlockedList line index
needle = "getBlockedList"
idx = next((i for i,l in enumerate(lines) if needle in l), None)
if idx is None:
    raise SystemExit("getBlockedList not found")
start = max(0, idx-40)
end = min(len(lines), idx+80)
for i in range(start, end):
    print(f"{i+1:4d}\t{lines[i]}")
PY

echo
echo "=== types/member.ts around blocked list response type ==="
python3 - <<'PY'
from pathlib import Path
p = Path("src/types/member.ts")
lines = p.read_text().splitlines()
# locate "Blocked" or "BlockListResponse"
needles = ["BlockListResponse", "blocked", "BlockList", "block"]
positions = []
for i,l in enumerate(lines):
    if any(n in l for n in needles):
        positions.append(i)
# print a window around the first relevant occurrence
if not positions:
    print("No relevant block/blocked types found in src/types/member.ts")
    raise SystemExit(0)
idx = positions[0]
start = max(0, idx-60)
end = min(len(lines), idx+140)
for i in range(start, end):
    print(f"{i+1:4d}\t{lines[i]}")
PY

echo
echo "=== types/member.ts exact occurrences for nextCursor in blocked list types ==="
rg -n "nextCursor" src/types/member.ts

Repository: checkmo2025/FE

Length of output: 4489


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for cursorId query param append in src/services ==="
rg -n 'append\("cursorId",' src/services || true

echo
echo "=== Search for if (cursorId) patterns in src/services ==="
rg -n 'if\s*\(\s*cursorId\s*\)' src/services || true

echo
echo "=== memberService getBlockedList alternative cursor handling (near by) ==="
python3 - <<'PY'
from pathlib import Path
p = Path("src/services/memberService.ts")
lines = p.read_text().splitlines()
# Print around getBlockedList only (already located roughly)
for i,l in enumerate(lines):
    if "getBlockedList" in l:
        start=max(0,i-10); end=min(len(lines), i+40)
        for j in range(start,end):
            print(f"{j+1:4d}\t{lines[j]}")
        break
PY

Repository: checkmo2025/FE

Length of output: 2331


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("src/hooks/queries/useMemberQueries.ts")
lines = p.read_text().splitlines()
for i,l in enumerate(lines):
    if "getBlockedList" in l or "blocked" in l and "cursor" in l:
        pass
# print a window around where getBlockedList query is configured
for i,l in enumerate(lines):
    if "queryFn" in l and "getBlockedList" in lines[i:i+3]:
        start=max(0,i-20); end=min(len(lines), i+40)
        for j in range(start,end):
            print(f"{j+1:4d}\t{lines[j]}")
        break
else:
    # fallback: find pageParam / getNextPageParam usage
    for i,l in enumerate(lines):
        if "getNextPageParam" in l:
            start=max(0,i-20); end=min(len(lines), i+40)
            for j in range(start,end):
                print(f"{j+1:4d}\t{lines[j]}")
            break
PY

Repository: checkmo2025/FE

Length of output: 2562


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("src/hooks/queries/useMemberQueries.ts")
lines = p.read_text().splitlines()
# show sections containing "block" / "blocked"
for i,l in enumerate(lines):
    if "Blocked" in l or "block" in l and "Block" not in l:
        pass
# find first occurrence of "Blocked" case-insensitively
idx = None
for i,l in enumerate(lines):
    if "Blocked" in l or "blocked" in l:
        idx = i
        break
if idx is None:
    # fallback: show whole file tail
    start = max(0, len(lines)-120)
    for j in range(start, len(lines)):
        print(f"{j+1:4d}\t{lines[j]}")
else:
    start = max(0, idx-80)
    end = min(len(lines), idx+120)
    for j in range(start, end):
        print(f"{j+1:4d}\t{lines[j]}")
PY

echo
echo "=== Grep for nextCursor handling in blocked/list-related hooks ==="
rg -n "blocked|block.*List|getBlockedList" src/hooks/queries/useMemberQueries.ts

rg -n "getNextPageParam.*nextCursor" src/hooks/queries/useMemberQueries.ts

Repository: checkmo2025/FE

Length of output: 4497


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== notificationService.ts around cursorId ==="
python3 - <<'PY'
from pathlib import Path
p=Path("src/services/notificationService.ts")
lines=p.read_text().splitlines()
for i,l in enumerate(lines):
    if "cursorId" in l:
        start=max(0,i-20); end=min(len(lines), i+20)
        for j in range(start,end):
            print(f"{j+1:4d}\t{lines[j]}")
        break
PY

echo
echo "=== newsService.ts around cursorId ==="
python3 - <<'PY'
from pathlib import Path
p=Path("src/services/newsService.ts")
lines=p.read_text().splitlines()
idxs=[i for i,l in enumerate(lines) if "cursorId" in l]
for i in idxs[:1]:
    start=max(0,i-30); end=min(len(lines), i+30)
    for j in range(start,end):
        print(f"{j+1:4d}\t{lines[j]}")
    break
PY

echo
echo "=== useBlockStore.ts initializeBlocks full snippet context (lines 20-60) ==="
python3 - <<'PY'
from pathlib import Path
p=Path("src/store/useBlockStore.ts")
lines=p.read_text().splitlines()
for i in range(15,55):
    if i < len(lines):
        print(f"{i+1:4d}\t{lines[i]}")
PY

Repository: checkmo2025/FE

Length of output: 5413


Avoid falsy cursor checks for cursor-based pagination (support cursorId = 0)

src/store/useBlockStore.ts breaks with if (!data.hasNext || !data.nextCursor), so a valid nextCursor === 0 would terminate early. Use a nullish check instead. Also, memberService.getBlockedList currently appends cursorId only when it’s truthy (if (cursorId)), which would also drop a valid 0.

Proposed fix
-        if (!data.hasNext || !data.nextCursor) {
+        if (!data.hasNext || data.nextCursor == null) {
           break;
         }
📝 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
hasNext = data.hasNext;
cursorId = data.nextCursor ?? undefined;
if (!data.hasNext || !data.nextCursor) {
break;
hasNext = data.hasNext;
cursorId = data.nextCursor ?? undefined;
if (!data.hasNext || data.nextCursor == null) {
break;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/store/useBlockStore.ts` around lines 40 - 43, The loop prematurely stops
when nextCursor is 0 because it uses falsy checks; change the condition in
useBlockStore (where hasNext = data.hasNext; cursorId = data.nextCursor ??
undefined; if (!data.hasNext || !data.nextCursor) break;) to use nullish checks
(e.g., if (data.hasNext === false || data.nextCursor == null) break) and set
cursorId to data.nextCursor (allow 0) rather than coercing to undefined; also
update memberService.getBlockedList where it currently does if (cursorId) to
append the cursor parameter even when cursorId === 0 (use cursorId != null or
typeof cursorId !== 'undefined' to decide whether to include it).

@yhi9839
yhi9839 merged commit c69e530 into main May 22, 2026
3 checks passed
This was referenced Jun 6, 2026

This branch was successfully deployed

1 active deployment
Preview — 0d889831 Deployed May 21, 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.

2 participants