Repository navigation
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refactor: SEO 메타데이터 추가
feat: Google Tag Manager 및 GA4 연동 설정
Feat/326 modified api
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis 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. ChangesUser Blocking Feature
Analytics and Branding
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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.
| isBlocked: !c.deleted && ( | ||
| c.authorInfo?.nickname === BLOCKED_USER_MASK || | ||
| (c.authorInfo?.nickname ? checkLocalBlocked(c.authorInfo.nickname) : false) | ||
| ), |
| initializeBlocks: async () => { | ||
| if (get().isInitialized) return; |
| const apiError = error as { code?: string } | null; | ||
| const message = | ||
| (apiError?.code === "BLOCK_404" || apiError?.code === "BLOCK_405") | ||
| ? getErrorMessage(apiError.code) | ||
| : "해당 책 이야기를 찾을 수 없습니다."; |
There was a problem hiding this comment.
ApiError.ts에 추가된 hasErrorCode 유틸리티 함수를 사용하여 에러 코드를 확인하는 것이 타입 안정성 측면에서 더 좋습니다. 수동 타입 캐스팅(as)을 피하고 코드의 가독성을 높일 수 있습니다. (참고: hasErrorCode 임포트가 필요할 수 있습니다.)
const message =
(hasErrorCode(error) && (error.code === "BLOCK_404" || error.code === "BLOCK_405"))
? getErrorMessage(error.code)
: "해당 책 이야기를 찾을 수 없습니다.";
| const apiError = error as { code?: string } | null; | ||
| const message = | ||
| (apiError?.code === "BLOCK_404" || apiError?.code === "BLOCK_405") | ||
| ? getErrorMessage(apiError.code) | ||
| : "프로필 정보를 불러올 수 없습니다."; |
| }>) { | ||
| return ( | ||
| <html lang="en"> | ||
| <GoogleTagManager gtmId={process.env.NEXT_PUBLIC_GTM_ID!} /> |
There was a problem hiding this comment.
| queryClient.invalidateQueries({ queryKey: memberKeys.blocks() }); | ||
| queryClient.invalidateQueries({ queryKey: storyKeys.all, refetchType: 'none' }); |
There was a problem hiding this comment.
사용자를 차단할 때 해당 사용자의 프로필 정보도 최신화되어야 합니다. onSuccess에서 memberKeys.otherProfile(nickname) 쿼리도 함께 무효화하여 프로필 페이지의 UI가 즉시 업데이트되도록 수정하는 것을 권장합니다.
queryClient.invalidateQueries({ queryKey: memberKeys.blocks() });
queryClient.invalidateQueries({ queryKey: memberKeys.otherProfile(nickname) });
queryClient.invalidateQueries({ queryKey: storyKeys.all, refetchType: 'none' });| queryClient.invalidateQueries({ queryKey: memberKeys.blocks() }); | ||
| queryClient.invalidateQueries({ queryKey: storyKeys.all, refetchType: 'none' }); |
There was a problem hiding this comment.
There was a problem hiding this comment.
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 winFix React hook ordering: avoid conditional
useStoryDetailQuerycall
src/app/(main)/stories/[id]/page.tsxcallsuseStoryDetailQueryonly after an early-return guard for!id || isNaN(storyId), which violates the Rules of Hooks. This is exactly the pattern thatreact-hooks/rules-of-hooksflags, since hook invocation must be unconditional and consistent across renders.Also,
src/hooks/queries/useStoryQueries.tsdefinesexport const useStoryDetailQuery = (id: number) => useQuery(...)with noenabled/options parameter in its signature, so the proposed “enabled-style” fix requires changing the hook API (or moving the guard inside the query viaenabledwhile 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
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (24)
package.jsonsrc/app/(main)/setting/block/page.tsxsrc/app/(main)/stories/[id]/page.tsxsrc/app/layout.tsxsrc/components/base-ui/Comment/comment_item.tsxsrc/components/base-ui/Comment/comment_list.tsxsrc/components/base-ui/Comment/comment_section_bookcase.tsxsrc/components/base-ui/Landing/LandingNav.tsxsrc/components/base-ui/Profile/OtherUser/ProfileUserInfo.tsxsrc/components/layout/AdminHeader.tsxsrc/components/layout/Header.tsxsrc/constants/masking.tssrc/hooks/mutations/useMemberMutations.tssrc/hooks/mutations/useStoryMutations.tssrc/hooks/queries/useMemberQueries.tssrc/hooks/useReportBlockFlow.tssrc/lib/api/endpoints/member.tssrc/lib/api/errorMapper.tssrc/lib/api/errors/ApiError.tssrc/lib/api/errors/errorMapper.tssrc/services/memberService.tssrc/store/useAuthStore.tssrc/store/useBlockStore.tssrc/types/member.ts
| }>) { | ||
| return ( | ||
| <html lang="en"> | ||
| <GoogleTagManager gtmId={process.env.NEXT_PUBLIC_GTM_ID!} /> |
There was a problem hiding this comment.
🧩 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/ emptygtmId: The Next.js docs don’t describe any special handling for an empty orundefinedgtmId; they only state thatgtmIdis required, except whengtmScriptUrlis provided (to support Google tag gateway). [1]
In the actual implementation,gtmIdis only appended to the GTM script URL when it’s truthy (if (gtmId) { scriptUrl.searchParams.set('id', gtmId) }). So anundefinedor""gtmIdresults in the component still rendering the scripts, but thegtm.jsrequest is made without theidquery param. [2] -
Is conditional rendering recommended when env vars are missing? The Next.js documentation for
@next/third-parties/googledoes not recommend conditional rendering as a pattern for missing env vars; it just documents the prop requirements/optionality (gtmIdrequired*,gtmScriptUrloptional*). [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:
- 1: https://nextjs.org/docs/app/building-your-application/optimizing/third-party-libraries
- 2: https://github.com/vercel/next.js/blob/canary/packages/third-parties/src/google/gtm.tsx
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.
| <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, |
There was a problem hiding this comment.
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.
| const { isBlocked: checkLocalBlocked, initializeBlocks } = useBlockStore(); | ||
|
|
||
| useEffect(() => { | ||
| if (isLoggedIn) { | ||
| initializeBlocks(); | ||
| } | ||
| }, [isLoggedIn, initializeBlocks]); |
There was a problem hiding this comment.
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.
| onError: (error: any) => { | ||
| console.error("Failed to block member:", error); | ||
| const errorMessage = error.response?.data?.message || error.message || "차단에 실패했습니다."; | ||
| toast.error(errorMessage); | ||
| }, |
There was a problem hiding this comment.
🧩 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.tsRepository: 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) => {
+ onError: (error: unknown) => {
console.error("Failed to block member:", error);
- const errorMessage = error.response?.data?.message || error.message || "차단에 실패했습니다.";
+ 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);
},
...
- onError: (error: any) => {
+ onError: (error: unknown) => {
console.error("Failed to unblock member:", error);
- const errorMessage = error.response?.data?.message || error.message || "차단 해제에 실패했습니다.";
+ 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);
},📝 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.
| 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.
| // Block Errors | ||
| BLOCK_404: "차단한 사용자입니다.", | ||
| BLOCK_405: "조회가 불가능한 프로필입니다.", |
There was a problem hiding this comment.
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.
| if (cursorId) { | ||
| url.searchParams.append("cursorId", cursorId.toString()); | ||
| params.append("cursorId", cursorId.toString()); | ||
| } |
There was a problem hiding this comment.
🧩 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" . || trueRepository: 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)" . || trueRepository: 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)" . || trueRepository: 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 || trueRepository: 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.*\\)" || trueRepository: 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.
| 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.
| hasNext = data.hasNext; | ||
| cursorId = data.nextCursor ?? undefined; | ||
| if (!data.hasNext || !data.nextCursor) { | ||
| break; |
There was a problem hiding this comment.
🧩 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 || trueRepository: 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 || trueRepository: 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.tsRepository: 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
PYRepository: 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
PYRepository: 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.tsRepository: 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]}")
PYRepository: 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.
| 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).
📌 개요 (Summary)
05.22 최신화 dev -> main 머지
Summary by CodeRabbit
Release Notes
New Features
Improvements
Chores