Conversation
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (3)
Walkthrough낙관적 UI 업데이트와 이중 클릭 방지 훅을 도입하고, 좋아요/저장/팔로우/투표 등 여러 컴포넌트에 해당 패턴을 적용했습니다. 또한 Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI as 컴포넌트(UI)
participant Hook as usePreventDoubleClick
participant State as 로컬 상태
participant API as 서버 API
User->>UI: 버튼 클릭 (좋아요/저장/팔로우/투표)
UI->>Hook: run(asyncFn)
Hook->>Hook: lock 설정, isLoading = true
UI->>State: 낙관적 상태 업데이트 (즉시 반영)
UI->>UI: 로딩 시각적 피드백 (아이콘 불투명도 적용)
Note over Hook,API: 요청 전/중에 로컬 상태 유지
Hook->>API: 실제 API 요청 전송
alt 성공
API-->>Hook: 성공 응답
Hook->>State: 서버 상태와 동기화
Hook->>UI: 성공 메시지(선택적)
else 실패
API-->>Hook: 오류/실패
Hook->>State: 낙관적 업데이트 롤백
Hook->>UI: 오류 스낵바 표시
end
Hook->>Hook: lock 해제, isLoading = false
UI->>UI: 로딩 해제 시각적 복구
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 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.
Actionable comments posted: 3
🧹 Nitpick comments (10)
src/components/common/Post/PostFooter.tsx (3)
62-62: 300ms 인위적 지연의 목적이 불분명합니다.
usePreventDoubleClick의 lock이 이미 동시 실행을 차단하고 있으므로,await new Promise(resolve => setTimeout(resolve, 300))은 모든 요청에 불필요한 300ms 지연만 추가합니다. 이 패턴이handleSave(Line 90)와 다른 컴포넌트(Reply, SubReply, Profile, SearchBook 등)에도 동일하게 적용되어 있습니다.진정한 디바운스가 필요하다면 이 PR에 추가된
useDebouncedCallback을 활용하거나, lock만으로 충분하다면 이 지연을 제거하는 것이 좋겠습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/Post/PostFooter.tsx` at line 62, The 300ms artificial delay (await new Promise(resolve => setTimeout(resolve, 300))) in PostFooter's async handlers (e.g., handleSave) is unnecessary because usePreventDoubleClick already prevents concurrent execution; remove that await to eliminate the constant delay and either replace it with the provided useDebouncedCallback if true debouncing is required or rely solely on the existing lock logic from usePreventDoubleClick; update other similar components (Reply, SubReply, Profile, SearchBook) to follow the same pattern so handlers only debounce when intended and do not always sleep for 300ms.
55-81: 롤백 로직 중복 — 헬퍼 함수로 추출을 권장합니다.
handleLike의!response.isSuccess분기(Lines 66-71)와catch분기(Lines 73-78)의 롤백 코드가 동일합니다.handleSave도 마찬가지(Lines 94-98 vs 101-105)입니다. 이를 내부 헬퍼로 추출하면 코드 중복을 줄이고 유지보수가 쉬워집니다.♻️ handleLike 예시
const handleLike = () => { runLike(async () => { const nextLiked = !likedRef.current; likedRef.current = nextLiked; setLiked(nextLiked); setLikeCount(prev => (nextLiked ? prev + 1 : prev - 1)); + const rollback = () => { + if (likedRef.current !== nextLiked) return; + likedRef.current = !nextLiked; + setLiked(!nextLiked); + setLikeCount(prev => (nextLiked ? prev - 1 : prev + 1)); + }; + await new Promise(resolve => setTimeout(resolve, 300)); try { const response = await postFeedLike(feedId, nextLiked); - if (!response.isSuccess && likedRef.current === nextLiked) { - const rollbackState = !nextLiked; - likedRef.current = rollbackState; - setLiked(rollbackState); - setLikeCount(prev => (nextLiked ? prev - 1 : prev + 1)); - } - } catch { - if (likedRef.current === nextLiked) { - const rollbackState = !nextLiked; - likedRef.current = rollbackState; - setLiked(rollbackState); - setLikeCount(prev => (nextLiked ? prev - 1 : prev + 1)); - } + if (!response.isSuccess) rollback(); + } catch { + rollback(); } }); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/Post/PostFooter.tsx` around lines 55 - 81, The rollback logic duplicated in handleLike (the response.isSuccess false branch and the catch block) should be extracted into a shared helper (e.g., rollbackLike) that accepts the intended nextLiked value and performs the state rollback (updating likedRef.current, setLiked, and setLikeCount) so both branches call that helper instead of repeating code; do the same extraction for the identical logic in handleSave so both handleLike and handleSave call their respective rollback helper rather than duplicating the rollback steps used around postFeedLike and the save API call.
120-120: 접근성: 클릭 가능한<img>요소에 키보드 접근성이 없습니다.좋아요/저장 아이콘이
<img onClick={...}>로 구현되어 있어 키보드 사용자가 접근할 수 없습니다.<button>요소로 래핑하거나,role="button",tabIndex={0},onKeyDown핸들러를 추가하는 것을 권장합니다. 로딩 중에는aria-disabled도 고려해 보세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/Post/PostFooter.tsx` at line 120, The like icon image (rendered as <img src={liked ? activeLike : like} onClick={handleLike} style={{ opacity: isLikeLoading ? 0.6 : 1 }} />) is not keyboard-accessible; update PostFooter to make it operable via keyboard by either replacing the <img> with a <button> (preferred) or wrapping it in an element with role="button", tabIndex={0}, and an onKeyDown handler that invokes handleLike on Enter/Space; also reflect loading state with aria-disabled={isLikeLoading} and preserve the visual opacity logic tied to isLikeLoading so screen-reader and keyboard users get the same feedback. Ensure you update the element that uses liked, activeLike, like, handleLike, and isLikeLoading so styling and click behavior remain identical.src/pages/searchBook/SearchBook.tsx (1)
125-127: 피드 로딩 오류가 완전히 무시됩니다.
loadFirstFeeds와loadMore의 catch 블록이// no-op으로 모든 오류를 삼키고 있습니다. 네트워크 장애 등으로 피드 로딩이 실패해도 사용자에게 아무런 피드백이 없으며, 빈 상태만 표시됩니다. 최소한 console.error 로깅이나 사용자에게 재시도 안내를 제공하는 것이 좋겠습니다.Also applies to: 146-148
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/searchBook/SearchBook.tsx` around lines 125 - 127, The catch blocks in loadFirstFeeds and loadMore currently swallow all errors ("// no-op"); update these catches to at minimum log the error (e.g., console.error or processLogger.error) and set a component error state (e.g., setFeedError or reuse existing state) so the UI can show an error message and retry action; ensure loadFirstFeeds and loadMore both call the same error handler/state updater and that finally blocks still run to clear loading flags.src/components/common/Post/SubReply.tsx (1)
47-79: Reply.tsx와 거의 동일한 좋아요 로직 — 공통 훅 추출을 고려해 보세요.
handleLike의 낙관적 업데이트/롤백/스낵바 패턴이Reply.tsx와 사실상 동일합니다. 이 로직을useOptimisticLike(commentId)같은 커스텀 훅으로 추출하면 두 컴포넌트의 중복을 제거할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/Post/SubReply.tsx` around lines 47 - 79, The like handling in handleLike duplicates logic in Reply.tsx; extract this optimistic-update/rollback/snackbar pattern into a reusable custom hook (e.g. useOptimisticLike) that encapsulates runLike, postLike call, optimistic state updates (liked, currentLikeCount) and rollback, and snackbar calls; update SubReply.tsx to call the hook (providing commentId and callbacks) and replace handleLike with the hook's trigger, ensuring you preserve semantics of setLiked/setCurrentLikeCount, previousLiked/previousLikeCount rollback, the 300ms delay, and error snackbar behavior (openSnackbar) so both Reply.tsx and SubReply.tsx share the same implementation.src/components/common/Post/Reply.tsx (1)
41-73: PostFooter와 롤백 추적 방식이 다릅니다 — 동작은 정상이나 통일을 권장합니다.PostFooter는
ref기반으로 현재 상태를 추적하고 prop 변경을 동기화하는 반면, Reply는 클로저 캡처 방식(previousLiked,previousLikeCount)을 사용합니다. lock이 동시 실행을 방지하므로 현재 두 방식 모두 정상 동작하지만, PR 전체의 일관성을 위해 하나의 패턴으로 통일하면 유지보수가 쉬워집니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/Post/Reply.tsx` around lines 41 - 73, Summary: Reply's handleLike uses closure-captured previousLiked/previousLikeCount for rollback, which differs from PostFooter's ref-based state tracking; make Reply follow the ref-based pattern for consistency. Fix: introduce refs (e.g., previousLikedRef, previousLikeCountRef) that are updated/synced with props like PostFooter does, set the refs before performing the optimistic update in handleLike (the function using runLike), and use those refs to restore state on failure instead of closure variables; ensure any prop-driven updates to liked/likeCount are mirrored into the refs (via useEffect) so rollback always uses the latest committed state; keep using postLike for the network call and setLiked/setLikeCount for UI updates.src/components/feed/UserProfileItem.tsx (3)
36-67:usePreventDoubleClick잠금과 300ms 지연의 조합이 불필요한 지연을 유발합니다.
usePreventDoubleClick의run은 이미lockRef로 재진입을 차단하므로, 실행 중에는 연타가 불가능합니다. 따라서 Line 44의setTimeout(300ms)은 디바운스 효과 없이 매 클릭마다 순수 지연만 추가합니다.두 가지 접근 중 하나를 선택하는 것이 맞습니다:
- Lock 방식 (현재
usePreventDoubleClick): 300ms 지연 제거 → 즉시 API 호출- Debounce 방식: lock 제거 → 300ms 내 재클릭 시 이전 호출 취소 후 마지막 클릭만 실행
현재는 두 패턴을 동시에 적용하여, lock으로 인해 debounce가 실질적으로 작동하지 않으면서 300ms 지연만 남게 됩니다. 이 패턴은 PR 내 다른 컴포넌트(PostFooter, Reply, SubReply 등)에도 동일하게 적용되어 있으므로 일괄 검토가 필요합니다.
Lock 방식 유지 시 지연 제거 예시
runFollow(async () => { const nextFollowed = !followedRef.current; followedRef.current = nextFollowed; setFollowed(nextFollowed); - await new Promise(resolve => setTimeout(resolve, 300)); - try { const response = await postFollow(userId, nextFollowed);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/feed/UserProfileItem.tsx` around lines 36 - 67, The click handler toggleFollow is using both the lock-based runFollow (usePreventDoubleClick) and an unnecessary 300ms artificial delay (the await new Promise with setTimeout) which causes pure latency; remove that delay so runFollow can immediately perform the API call. Specifically, inside toggleFollow (and similarly in other components using runFollow), delete the await new Promise(resolve => setTimeout(resolve, 300)) line, rely on followedRef/current and setFollowed for optimistic UI, keep the try/catch with postFollow(userId, nextFollowed) and rollback logic intact, and ensure runFollow remains the locking wrapper to prevent reentrancy.
46-53: Lock 기반에서 stale-check 가드가 도달 불가능(dead code)합니다.Line 48의
if (followedRef.current !== nextFollowed) return;은followedRef가 이 함수 내부에서만 변경되고,usePreventDoubleClick의 lock이 동시 실행을 차단하므로 항상false입니다. Line 61의 동일한 가드도 마찬가지입니다.현재 로직에서는
followedRef를 외부에서 변경하는 경로(예: prop syncuseEffect)가 없기 때문에 이 조건은 절대true가 되지 않습니다. 향후 debounce 방식으로 전환할 경우에만 의미가 생깁니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/feed/UserProfileItem.tsx` around lines 46 - 53, The stale-check guard using followedRef.current !== nextFollowed is dead code because followedRef is only mutated inside this locked handler and usePreventDoubleClick prevents concurrent runs; remove both occurrences of that if-guard in the follow/unfollow handler (the checks surrounding nextFollowed before and after the postFollow call) so the flow relies on the lock and the response update via postFollow/response.data.isFollowing and setFollowed; if you intend to support debounce/external updates later, replace the guards with a clear external sync (e.g., compare to a prop or an external ref updated by useEffect) instead of the current no-op check.
23-24:isFollowingprop 변경 시 로컬 상태와 동기화되지 않습니다.
useState(!!isFollowing)과useRef(!!isFollowing)는 마운트 시점에만 초기화됩니다. 부모 컴포넌트에서 데이터를 리패치하여isFollowingprop이 변경되어도followed상태와followedRef는 갱신되지 않아 UI가 서버 상태와 불일치할 수 있습니다.PollRecord 컴포넌트(Line 72-75)에서는
useEffect로 prop 변경을 동기화하고 있으므로, 동일한 패턴 적용을 권장합니다.prop 동기화 useEffect 추가 예시
const { isLoading: isFollowLoading, run: runFollow } = usePreventDoubleClick(); + useEffect(() => { + setFollowed(!!isFollowing); + followedRef.current = !!isFollowing; + }, [isFollowing]); + const handleProfileClick = () => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/feed/UserProfileItem.tsx` around lines 23 - 24, UserProfileItem's local state (followed) and ref (followedRef) are only initialized from the isFollowing prop and won't update when isFollowing changes; add a useEffect in the UserProfileItem component that watches isFollowing and updates both setFollowed(!!isFollowing) and followedRef.current = !!isFollowing (same pattern used in PollRecord) so the UI/ref stay in sync with prop changes from the parent.src/components/memory/RecordItem/PollRecord.tsx (1)
105-105: UserProfileItem과 동일하게 300ms 지연이 lock과 중복됩니다.
usePreventDoubleClick의 lock이 재진입을 차단하므로 이 지연은 debounce 효과 없이 순수 지연만 추가합니다. UserProfileItem에 대한 코멘트와 동일한 사항이며, PR 전체적으로 일괄 검토가 필요합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/memory/RecordItem/PollRecord.tsx` at line 105, PollRecord.tsx contains an explicit 300ms setTimeout delay that duplicates behavior already enforced by usePreventDoubleClick’s lock (same issue noted for UserProfileItem); remove the await new Promise(resolve => setTimeout(resolve, 300)) from the handler in PollRecord (and remove the same pattern across the PR) and either rely on usePreventDoubleClick’s reentrancy lock or, if a debounce is actually required, implement the debounce inside usePreventDoubleClick (or a shared debounce util) and call that instead so the lock and timing behavior are not duplicated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@package.json`:
- Line 19: package.json contains an unused dependency "@tanstack/react-query";
remove the dependency line for "@tanstack/react-query" from package.json (or
alternatively add the actual imports/usages where you intend to use it), run
yarn/npm install to update lockfile, and ensure no code references
"@tanstack/react-query" (search for imports of react-query or
useQuery/useMutation) before merging; if you plan to add optimistic update logic
later, move the dependency change into a separate PR.
In `@src/components/memory/RecordItem/PollRecord.tsx`:
- Around line 72-75: The useEffect that syncs pollOptions (the block calling
setCurrentOptions and optionsRef.current = pollOptions) can overwrite an ongoing
optimistic update; add a guard at the start of that effect to skip prop
synchronization when a vote/lock is in-flight (e.g., check an existing in-flight
flag like isVotingRef, isLocked, or pendingRequestRef used by your voting
logic), so that when a request is pending you do not overwrite
currentOptions/optionsRef.current; keep the existing sync behavior when no
request is pending and ensure the in-flight flag is set/cleared by the voting
functions so the stale-check logic later still works.
In `@src/hooks/useDebouncedCallback.ts`:
- Around line 3-6: The hook signature uses `any` for callback args; change it to
use `unknown[]` for type-safety by updating the generic constraint and rest-arg
types — e.g. change `T extends (...args: any[]) => void` to `T extends (...args:
unknown[]) => void` and replace any usages of `any[]` or `...args: any[]` inside
`useDebouncedCallback` with `unknown[]` or `...args: unknown[]` so the callback
parameter types and invocations remain consistent while satisfying
`@typescript-eslint/no-explicit-any`.
---
Nitpick comments:
In `@src/components/common/Post/PostFooter.tsx`:
- Line 62: The 300ms artificial delay (await new Promise(resolve =>
setTimeout(resolve, 300))) in PostFooter's async handlers (e.g., handleSave) is
unnecessary because usePreventDoubleClick already prevents concurrent execution;
remove that await to eliminate the constant delay and either replace it with the
provided useDebouncedCallback if true debouncing is required or rely solely on
the existing lock logic from usePreventDoubleClick; update other similar
components (Reply, SubReply, Profile, SearchBook) to follow the same pattern so
handlers only debounce when intended and do not always sleep for 300ms.
- Around line 55-81: The rollback logic duplicated in handleLike (the
response.isSuccess false branch and the catch block) should be extracted into a
shared helper (e.g., rollbackLike) that accepts the intended nextLiked value and
performs the state rollback (updating likedRef.current, setLiked, and
setLikeCount) so both branches call that helper instead of repeating code; do
the same extraction for the identical logic in handleSave so both handleLike and
handleSave call their respective rollback helper rather than duplicating the
rollback steps used around postFeedLike and the save API call.
- Line 120: The like icon image (rendered as <img src={liked ? activeLike :
like} onClick={handleLike} style={{ opacity: isLikeLoading ? 0.6 : 1 }} />) is
not keyboard-accessible; update PostFooter to make it operable via keyboard by
either replacing the <img> with a <button> (preferred) or wrapping it in an
element with role="button", tabIndex={0}, and an onKeyDown handler that invokes
handleLike on Enter/Space; also reflect loading state with
aria-disabled={isLikeLoading} and preserve the visual opacity logic tied to
isLikeLoading so screen-reader and keyboard users get the same feedback. Ensure
you update the element that uses liked, activeLike, like, handleLike, and
isLikeLoading so styling and click behavior remain identical.
In `@src/components/common/Post/Reply.tsx`:
- Around line 41-73: Summary: Reply's handleLike uses closure-captured
previousLiked/previousLikeCount for rollback, which differs from PostFooter's
ref-based state tracking; make Reply follow the ref-based pattern for
consistency. Fix: introduce refs (e.g., previousLikedRef, previousLikeCountRef)
that are updated/synced with props like PostFooter does, set the refs before
performing the optimistic update in handleLike (the function using runLike), and
use those refs to restore state on failure instead of closure variables; ensure
any prop-driven updates to liked/likeCount are mirrored into the refs (via
useEffect) so rollback always uses the latest committed state; keep using
postLike for the network call and setLiked/setLikeCount for UI updates.
In `@src/components/common/Post/SubReply.tsx`:
- Around line 47-79: The like handling in handleLike duplicates logic in
Reply.tsx; extract this optimistic-update/rollback/snackbar pattern into a
reusable custom hook (e.g. useOptimisticLike) that encapsulates runLike,
postLike call, optimistic state updates (liked, currentLikeCount) and rollback,
and snackbar calls; update SubReply.tsx to call the hook (providing commentId
and callbacks) and replace handleLike with the hook's trigger, ensuring you
preserve semantics of setLiked/setCurrentLikeCount,
previousLiked/previousLikeCount rollback, the 300ms delay, and error snackbar
behavior (openSnackbar) so both Reply.tsx and SubReply.tsx share the same
implementation.
In `@src/components/feed/UserProfileItem.tsx`:
- Around line 36-67: The click handler toggleFollow is using both the lock-based
runFollow (usePreventDoubleClick) and an unnecessary 300ms artificial delay (the
await new Promise with setTimeout) which causes pure latency; remove that delay
so runFollow can immediately perform the API call. Specifically, inside
toggleFollow (and similarly in other components using runFollow), delete the
await new Promise(resolve => setTimeout(resolve, 300)) line, rely on
followedRef/current and setFollowed for optimistic UI, keep the try/catch with
postFollow(userId, nextFollowed) and rollback logic intact, and ensure runFollow
remains the locking wrapper to prevent reentrancy.
- Around line 46-53: The stale-check guard using followedRef.current !==
nextFollowed is dead code because followedRef is only mutated inside this locked
handler and usePreventDoubleClick prevents concurrent runs; remove both
occurrences of that if-guard in the follow/unfollow handler (the checks
surrounding nextFollowed before and after the postFollow call) so the flow
relies on the lock and the response update via
postFollow/response.data.isFollowing and setFollowed; if you intend to support
debounce/external updates later, replace the guards with a clear external sync
(e.g., compare to a prop or an external ref updated by useEffect) instead of the
current no-op check.
- Around line 23-24: UserProfileItem's local state (followed) and ref
(followedRef) are only initialized from the isFollowing prop and won't update
when isFollowing changes; add a useEffect in the UserProfileItem component that
watches isFollowing and updates both setFollowed(!!isFollowing) and
followedRef.current = !!isFollowing (same pattern used in PollRecord) so the
UI/ref stay in sync with prop changes from the parent.
In `@src/components/memory/RecordItem/PollRecord.tsx`:
- Line 105: PollRecord.tsx contains an explicit 300ms setTimeout delay that
duplicates behavior already enforced by usePreventDoubleClick’s lock (same issue
noted for UserProfileItem); remove the await new Promise(resolve =>
setTimeout(resolve, 300)) from the handler in PollRecord (and remove the same
pattern across the PR) and either rely on usePreventDoubleClick’s reentrancy
lock or, if a debounce is actually required, implement the debounce inside
usePreventDoubleClick (or a shared debounce util) and call that instead so the
lock and timing behavior are not duplicated.
In `@src/pages/searchBook/SearchBook.tsx`:
- Around line 125-127: The catch blocks in loadFirstFeeds and loadMore currently
swallow all errors ("// no-op"); update these catches to at minimum log the
error (e.g., console.error or processLogger.error) and set a component error
state (e.g., setFeedError or reuse existing state) so the UI can show an error
message and retry action; ensure loadFirstFeeds and loadMore both call the same
error handler/state updater and that finally blocks still run to clear loading
flags.
| useEffect(() => { | ||
| setCurrentOptions(pollOptions); | ||
| optionsRef.current = pollOptions; | ||
| }, [pollOptions]); |
There was a problem hiding this comment.
prop 동기화 useEffect가 진행 중인 낙관적 업데이트를 덮어쓸 수 있습니다.
투표 중(lock 획득 상태)에 부모가 pollOptions를 새 값으로 전달하면, 이 useEffect가 optionsRef.current와 currentOptions를 prop 값으로 덮어써서 낙관적 UI가 즉시 되돌아갑니다. 이후 API 응답이 도착하면 Line 113의 stale-check로 응답이 무시되거나, 다시 상태가 변경되어 시각적 깜빡임이 발생할 수 있습니다.
진행 중인 요청이 있을 때는 prop 동기화를 건너뛰는 가드를 추가하는 것이 좋습니다.
가드 추가 예시
useEffect(() => {
+ if (isVoting) return;
setCurrentOptions(pollOptions);
optionsRef.current = pollOptions;
}, [pollOptions]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/memory/RecordItem/PollRecord.tsx` around lines 72 - 75, The
useEffect that syncs pollOptions (the block calling setCurrentOptions and
optionsRef.current = pollOptions) can overwrite an ongoing optimistic update;
add a guard at the start of that effect to skip prop synchronization when a
vote/lock is in-flight (e.g., check an existing in-flight flag like isVotingRef,
isLocked, or pendingRequestRef used by your voting logic), so that when a
request is pending you do not overwrite currentOptions/optionsRef.current; keep
the existing sync behavior when no request is pending and ensure the in-flight
flag is set/cleared by the voting functions so the stale-check logic later still
works.
ho0010
left a comment
There was a problem hiding this comment.
고생 많으셨습니다!
훅으로 반복되는 로직 분리하신 것 좋네요~
코드래빗 리뷰 사항만 반영해주시면 좋을 것 같아요!
ljh130334
left a comment
There was a problem hiding this comment.
고생하셨습니다!! 중복 클릭 방지 로직이 훅으로 깔끔하게 추상화돼서 너무 깔끔하네용!! 👍🏻 👍🏻
| setLiked(nextLiked); | ||
| setLikeCount(prev => (nextLiked ? prev + 1 : prev - 1)); | ||
|
|
||
| await new Promise(resolve => setTimeout(resolve, 300)); |
There was a problem hiding this comment.
300ms를 모든 핸들러에 적용해주셨는데 혹시 특별한 이유가 있을까요?! Optimistic Update를 쓰는 이유가 즉각적인 UI 반응이 목적인데, API를 300ms 지연 호출하는 이유가 있는지 궁금합니다!
There was a problem hiding this comment.
초기에는 중복 요청 완화를 위해 API 호출에 300ms 지연을 넣었었는데, 지금은 lock으로 중복 방지가 되고 있어 필수는 아니라 제거해도 될 것 같습니다!
| if (followedRef.current !== nextFollowed) return; | ||
| const rollbackState = !nextFollowed; | ||
| followedRef.current = rollbackState; | ||
| setFollowed(rollbackState); |
There was a problem hiding this comment.
팔로우 실패 시에도 스낵바로 사용자 피드백을 주는게 어떨까요?
There was a problem hiding this comment.
좋은 것 같습니다~ 반영해보겠습니당
- 투표 UI 상태 동기화 안정화 (props 재동기화로 인한 즉시 반영 깨짐 방지) - 투표 항목 순서 고정(voteItemId 기준 매핑으로 번호/순서 유지)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
src/components/memory/RecordItem/PollRecord.tsx (1)
99-102:⚠️ Potential issue | 🟡 Minorprop 동기화
useEffect에isVoting가드 누락투표 진행 중(
isVoting === true)에 부모가 새pollOptions를 전달하면 이useEffect가optionsRef.current와currentOptions를 덮어써 낙관적 업데이트가 즉시 되돌아갑니다. 이후 line 159–160의 stale 체크에서 서버 응답도 무시될 수 있어, 300ms 대기 구간 동안 상태 불일치가 발생할 수 있습니다.🛡️ 제안
useEffect(() => { + if (isVoting) return; setCurrentOptions(pollOptions); optionsRef.current = pollOptions; }, [pollOptions]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/memory/RecordItem/PollRecord.tsx` around lines 99 - 102, The useEffect that syncs pollOptions into currentOptions/optionsRef.current should avoid overwriting optimistic local state while a vote is in progress: add isVoting to the dependency list and wrap the update with a guard so it only runs when isVoting is false (i.e., do not call setCurrentOptions or set optionsRef.current when isVoting === true); this preserves the optimistic update and prevents the parent-provided pollOptions from clobbering state that the stale-check logic (the stale check around the server response) relies on.
🧹 Nitpick comments (5)
src/components/memory/RecordItem/RecordItem.tsx (2)
66-94:handleLikeClick에 중복 클릭 방지 처리가 없습니다PR의 핵심 목표가 "상태 토글 액션(좋아요 포함)에 중복 실행 방지 패턴을 공통 적용"임에도,
handleLikeClick은 async 함수임에도 불구하고 인플라이트 가드가 전혀 없습니다. 빠른 연타 시 여러 API 요청이 동시에 실행되어isLiked/currentLikeCount상태 충돌이 발생할 수 있습니다. 다른 파일에 도입된usePreventDoubleClick훅을 동일하게 적용하면 일관성이 확보됩니다.♻️ usePreventDoubleClick 훅 적용 예시
+import { usePreventDoubleClick } from '@/hooks/usePreventDoubleClick'; const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => { ... - const handleLikeClick = async () => { + const _handleLikeClick = async () => { try { ... } }; + + const { handler: handleLikeClick, isLoading: isLikeLoading } = usePreventDoubleClick(_handleLikeClick);그리고 버튼에
isLoading피드백 적용:<ActionButton onClick={ shouldBlur ? undefined : e => { e.stopPropagation(); handleLikeClick(); } } style={{ cursor: shouldBlur ? 'default' : 'pointer', pointerEvents: shouldBlur ? 'none' : 'auto', + opacity: isLikeLoading ? 0.5 : 1, }} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/memory/RecordItem/RecordItem.tsx` around lines 66 - 94, The handleLikeClick handler lacks an inflight guard so rapid clicks send multiple postRoomPostLike requests and race-update setIsLiked/setCurrentLikeCount; import and use the existing usePreventDoubleClick hook (or its wrapper) around handleLikeClick (or replace its export with the hook-wrapped function) so clicks are ignored while a request is pending, pass the hook's isPrevented/isLoading state to the like button for UI feedback, and keep existing postRoomPostLike/error handling logic unchanged (update locations: handleLikeClick, postRoomPostLike call, setIsLiked, setCurrentLikeCount, and openSnackbar).
56-56:currentPollOptions상태에 명시적 타입 제네릭 지정을 권장합니다
useState(pollOptions || [])형태로 초기화하면pollOptions가undefined일 때 TypeScript가never[]로 추론할 수 있어 이후setCurrentPollOptions호출 시 타입 오류가 발생할 수 있습니다.✨ 제네릭 명시 예시
- const [currentPollOptions, setCurrentPollOptions] = useState(pollOptions || []); + const [currentPollOptions, setCurrentPollOptions] = useState<NonNullable<typeof pollOptions>>(pollOptions || []);또는
PollOption타입이 명시되어 있다면:- const [currentPollOptions, setCurrentPollOptions] = useState(pollOptions || []); + const [currentPollOptions, setCurrentPollOptions] = useState<PollOption[]>(pollOptions || []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/memory/RecordItem/RecordItem.tsx` at line 56, The state initializer const [currentPollOptions, setCurrentPollOptions] = useState(pollOptions || []); can cause TypeScript to infer never[] when pollOptions is undefined; explicitly add a generic type to useState (e.g., useState<PollOption[]>(...)) or a union like useState<PollOption[] | undefined>(...) so currentPollOptions and setCurrentPollOptions have correct types; update the useState call referencing pollOptions, currentPollOptions, and setCurrentPollOptions to use the appropriate PollOption type.src/components/memory/RecordItem/PollRecord.tsx (2)
135-140:...item스프레드 이후 동일 속성 재할당 중복
isVoted와count는 이미...item에 포함되어 있으므로 아래 명시적 재할당은 불필요합니다.♻️ 제안
- return { - ...item, - isVoted: item.isVoted, - count: item.count, - }; + return { ...item };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/memory/RecordItem/PollRecord.tsx` around lines 135 - 140, In PollRecord.tsx inside the map callback that returns "{ ...item, isVoted: item.isVoted, count: item.count }", remove the redundant explicit reassignments of isVoted and count since they are already included by the spread; either return just "{ ...item }" or only explicitly set those fields when you intend to override them with different values (e.g., set derived values instead of item.isVoted/item.count).
44-68:mergeServerVoteItems함수를 컴포넌트 외부로 이동 권장컴포넌트 스코프의 props/state에 의존하지 않는 순수 유틸 함수이므로, 컴포넌트 바깥으로 호이스팅하면 매 렌더마다 재생성되는 오버헤드를 없앨 수 있습니다.
♻️ 제안 (컴포넌트 정의 바깥으로 이동)
+const mergeServerVoteItems = ( + serverVoteItems: VoteItemResult[], + baseOptions: PollOption[], +): PollOption[] => { + const serverByVoteItemId = new Map(serverVoteItems.map(item => [item.voteItemId, item])); + + const mergedInBaseOrder = baseOptions.map(base => { + const server = serverByVoteItemId.get(base.voteItemId); + if (!server) return base; + + return { + ...base, + percentage: server.percentage, + count: server.count, + isVoted: server.isVoted, + voteItemId: server.voteItemId, + }; + }); + + const maxCount = Math.max(...mergedInBaseOrder.map(item => item.count)); + return mergedInBaseOrder.map(item => ({ + ...item, + isHighest: item.count === maxCount, + })); +}; const PollRecord = ({ ... }) => { - const mergeServerVoteItems = ( ... ) => { ... }; ...🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/memory/RecordItem/PollRecord.tsx` around lines 44 - 68, The function mergeServerVoteItems is a pure utility that doesn't use component props/state and should be hoisted out of the component to avoid being recreated on every render; move the mergeServerVoteItems declaration (and related types like VoteItemResult/PollOption if needed) to module scope (above the component) and export it if reused elsewhere, updating any local references inside the component to call the now-top-level mergeServerVoteItems with the same serverVoteItems and baseOptions parameters so behavior is unchanged.src/hooks/useDebouncedCallback.ts (1)
22-32:useMemo대신useCallback사용 권장함수를 반환할 때
useMemo(() => fn, deps)는useCallback(fn, deps)와 동일하지만,useCallback이 관용적입니다.♻️ 제안
- return useMemo(() => { - return (...args: Parameters<T>) => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - - timeoutRef.current = setTimeout(() => { - callbackRef.current(...args); - }, delay); - }; - }, [delay]); + return useCallback( + (...args: Parameters<T>) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + timeoutRef.current = setTimeout(() => { + callbackRef.current(...args); + }, delay); + }, + [delay], + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/useDebouncedCallback.ts` around lines 22 - 32, The returned debounced function is wrapped with useMemo but should use useCallback for clarity and convention: replace useMemo(() => { return (...args: Parameters<T>) => { ... } }, [delay]) with useCallback to memoize the function instead; keep the same logic using timeoutRef and callbackRef and include delay in the dependency array of useCallback so the debounced function returned by useDebouncedCallback updates when delay changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/memory/RecordItem/RecordItem.tsx`:
- Around line 60-62: The effect in RecordItem currently uses pollOptions
reference as the dependency and unconditionally calls setCurrentPollOptions,
which overwrites optimistic updates from PollRecord.onVoteUpdate when the parent
recreates the record object; change the logic in the useEffect inside RecordItem
so it only updates state when the pollOptions content actually differs (e.g.,
compare JSON.stringify(pollOptions) or a deep-equal of pollOptions vs
currentPollOptions) before calling setCurrentPollOptions, or alternatively
advise memoizing pollOptions in the parent—update the useEffect that references
pollOptions and setCurrentPollOptions to perform a content-based comparison and
only overwrite state on real changes.
- Line 345: The onVoteUpdate prop expects a function of type (updatedOptions:
PollOption[]) => void but setCurrentPollOptions is a
Dispatch<SetStateAction<PollOption[]>> causing a possible type mismatch; fix by
passing an explicit callback wrapper to PollRecord like onVoteUpdate={(opts:
PollOption[]) => setCurrentPollOptions(opts)} (or adjust PollRecord's
onVoteUpdate type to accept the Dispatch signature), referencing the
setCurrentPollOptions identifier and the PollRecord/onVoteUpdate prop so
TypeScript strict mode no longer errors.
In `@src/hooks/useDebouncedCallback.ts`:
- Around line 14-20: The cleanup effect in useDebouncedCallback currently only
runs on unmount so a pending timeout remains active when delay changes; update
the effect that clears timeoutRef (the useEffect wrapping
clearTimeout(timeoutRef.current)) to include delay in its dependency array so it
runs and cancels any existing timeout whenever delay changes, ensuring no
old-timer invokes the callback under the previous delay value (refer to
timeoutRef and the useDebouncedCallback memoized callback).
---
Duplicate comments:
In `@src/components/memory/RecordItem/PollRecord.tsx`:
- Around line 99-102: The useEffect that syncs pollOptions into
currentOptions/optionsRef.current should avoid overwriting optimistic local
state while a vote is in progress: add isVoting to the dependency list and wrap
the update with a guard so it only runs when isVoting is false (i.e., do not
call setCurrentOptions or set optionsRef.current when isVoting === true); this
preserves the optimistic update and prevents the parent-provided pollOptions
from clobbering state that the stale-check logic (the stale check around the
server response) relies on.
---
Nitpick comments:
In `@src/components/memory/RecordItem/PollRecord.tsx`:
- Around line 135-140: In PollRecord.tsx inside the map callback that returns "{
...item, isVoted: item.isVoted, count: item.count }", remove the redundant
explicit reassignments of isVoted and count since they are already included by
the spread; either return just "{ ...item }" or only explicitly set those fields
when you intend to override them with different values (e.g., set derived values
instead of item.isVoted/item.count).
- Around line 44-68: The function mergeServerVoteItems is a pure utility that
doesn't use component props/state and should be hoisted out of the component to
avoid being recreated on every render; move the mergeServerVoteItems declaration
(and related types like VoteItemResult/PollOption if needed) to module scope
(above the component) and export it if reused elsewhere, updating any local
references inside the component to call the now-top-level mergeServerVoteItems
with the same serverVoteItems and baseOptions parameters so behavior is
unchanged.
In `@src/components/memory/RecordItem/RecordItem.tsx`:
- Around line 66-94: The handleLikeClick handler lacks an inflight guard so
rapid clicks send multiple postRoomPostLike requests and race-update
setIsLiked/setCurrentLikeCount; import and use the existing
usePreventDoubleClick hook (or its wrapper) around handleLikeClick (or replace
its export with the hook-wrapped function) so clicks are ignored while a request
is pending, pass the hook's isPrevented/isLoading state to the like button for
UI feedback, and keep existing postRoomPostLike/error handling logic unchanged
(update locations: handleLikeClick, postRoomPostLike call, setIsLiked,
setCurrentLikeCount, and openSnackbar).
- Line 56: The state initializer const [currentPollOptions,
setCurrentPollOptions] = useState(pollOptions || []); can cause TypeScript to
infer never[] when pollOptions is undefined; explicitly add a generic type to
useState (e.g., useState<PollOption[]>(...)) or a union like
useState<PollOption[] | undefined>(...) so currentPollOptions and
setCurrentPollOptions have correct types; update the useState call referencing
pollOptions, currentPollOptions, and setCurrentPollOptions to use the
appropriate PollOption type.
In `@src/hooks/useDebouncedCallback.ts`:
- Around line 22-32: The returned debounced function is wrapped with useMemo but
should use useCallback for clarity and convention: replace useMemo(() => {
return (...args: Parameters<T>) => { ... } }, [delay]) with useCallback to
memoize the function instead; keep the same logic using timeoutRef and
callbackRef and include delay in the dependency array of useCallback so the
debounced function returned by useDebouncedCallback updates when delay changes.
ℹ️ Review info
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
src/components/memory/RecordItem/PollRecord.tsxsrc/components/memory/RecordItem/RecordItem.tsxsrc/hooks/useDebouncedCallback.ts
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
src/components/feed/Profile.tsx (1)
64-69: 팔로우 실패 시 사용자 피드백이 없습니다.이전 리뷰에서도 지적된 사항입니다.
catch블록에서 롤백은 되지만 사용자가 실패를 인지할 수 없습니다.에러 스낵바 추가 예시
} catch { if (followedRef.current !== nextFollowed) return; const rollbackState = !nextFollowed; followedRef.current = rollbackState; setFollowed(rollbackState); + openPopup('snackbar', { + message: '팔로우 처리 중 오류가 발생했습니다.', + variant: 'top', + onClose: () => {}, + }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/feed/Profile.tsx` around lines 64 - 69, The catch block in Profile.tsx currently only rolls back the follow state (using followedRef.current, nextFollowed and setFollowed) but provides no user-visible feedback; update the catch to keep the rollback logic as-is and also display an error snackbar/toast (or call the existing showSnackbar/showToast utility) with a clear message like "Failed to update follow status" and include the error message for debugging; also optionally log the error to console or a logger so failures are visible to developers while preserving followedRef and setFollowed rollback behavior.src/components/memory/RecordItem/PollRecord.tsx (1)
99-102:⚠️ Potential issue | 🟡 Minorprop 동기화
useEffect가 진행 중인 낙관적 업데이트를 덮어쓸 수 있습니다.투표 진행 중(
isVoting === true)에 부모가pollOptions를 새 값으로 전달하면, 이useEffect가optionsRef.current를 덮어써서 낙관적 UI가 즉시 되돌아가고 이후 API 응답 처리 시 상태 불일치가 발생할 수 있습니다.가드 추가 예시
useEffect(() => { + if (isVoting) return; setCurrentOptions(pollOptions); optionsRef.current = pollOptions; }, [pollOptions]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/memory/RecordItem/PollRecord.tsx` around lines 99 - 102, The effect in PollRecord.tsx that syncs prop pollOptions (useEffect that calls setCurrentOptions and writes optionsRef.current) can clobber an in-progress optimistic update; update that effect to guard against overwriting while a vote is in flight (check the isVoting flag) — i.e., early-return if isVoting is true so you don't call setCurrentOptions or mutate optionsRef.current during an optimistic update, or implement a merge strategy that applies incoming pollOptions only after the optimistic state completes (use isVoting to defer or reconcile updates in the effect).
🧹 Nitpick comments (7)
src/pages/searchBook/SearchBook.tsx (2)
125-127: 피드 로딩 에러를 무시하는 것은 의도적이라면 괜찮지만, 디버깅이 어려워질 수 있습니다.
loadFirstFeeds와loadMore의 catch 블록이// no-op으로 에러를 무시합니다. 프로덕션에서 피드 로딩 실패 원인을 추적하기 어려울 수 있으므로, 최소한console.warn수준의 로깅을 고려해 주세요.Also applies to: 146-148
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/searchBook/SearchBook.tsx` around lines 125 - 127, The catch blocks in loadFirstFeeds and loadMore silently swallow errors (currently "catch { // no-op }"); change each catch to capture the error (e.g., catch (err)) and emit at least a console.warn or use the app logger with a clear message and the error object so feed-loading failures are visible for debugging and monitoring.
208-230: 저장 실패 시 사용자 피드백이 없습니다.
handleSaveButton에서 API 실패 시 상태 롤백은 잘 처리되지만, 사용자에게 실패를 알리는 스낵바가 없습니다.PollRecord.tsx에서는 실패 시openSnackbar로 에러 메시지를 표시하고 있어 일관성 있는 처리가 필요합니다.스낵바 피드백 추가 예시
} catch { if (isSavedRef.current === nextSaved) { const rollback = !nextSaved; isSavedRef.current = rollback; setIsSaved(rollback); } + openPopup('snackbar', { + message: '저장 처리 중 오류가 발생했습니다.', + variant: 'top', + onClose: () => usePopupStore.getState().closePopup(), + }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/searchBook/SearchBook.tsx` around lines 208 - 230, The save handler in handleSaveButton currently rolls back UI state on postSaveBook failure but provides no user feedback; update the runSave async block in handleSaveButton to call openSnackbar (same pattern used in PollRecord.tsx) when postSaveBook returns isSuccess === false or throws, passing a clear error message; keep the existing rollback logic (isSavedRef and setIsSaved) and ensure openSnackbar is imported/available in this component and invoked in both the non-success branch and the catch block so users see an error notification.src/components/common/Post/PostFooter.tsx (2)
55-79:handleLike실패 시 사용자 피드백(스낵바)이 없습니다.
Reply.tsx와SubReply.tsx에서는 좋아요 실패 시openSnackbar로 에러 메시지를 표시하지만,PostFooter에서는 조용히 롤백만 합니다. 사용자가 실패를 인지하지 못할 수 있습니다.
handleSave에도 동일하게 스낵바 피드백이 없습니다(Lines 81-104).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/Post/PostFooter.tsx` around lines 55 - 79, The optimistic like/save handlers (handleLike and handleSave) currently roll back state on failure but provide no user feedback; update both handlers to call the existing openSnackbar(errorMessage, { severity: "error" }) (or the app's snackbar utility) when postFeedLike or postFeedSave fails or throws, while preserving the current rollback logic that uses likedRef/current state; locate postFeedLike/postFeedSave calls in handleLike and handleSave and add the snackbar call in both the response.isSuccess === false branch and the catch branch, passing a clear message like "좋아요에 실패했습니다" / "저장에 실패했습니다" (or localized equivalents).
62-76:try/catch내 롤백 로직이 중복됩니다.
!response.isSuccess분기(Lines 64-69)와catch분기(Lines 71-76)의 롤백 코드가 동일합니다.handleSave(Lines 88-103)도 마찬가지입니다. 헬퍼 함수로 추출하면 유지보수성이 개선됩니다.롤백 로직 추출 예시 (handleLike)
const handleLike = () => { runLike(async () => { const nextLiked = !likedRef.current; likedRef.current = nextLiked; setLiked(nextLiked); setLikeCount(prev => (nextLiked ? prev + 1 : prev - 1)); + const rollbackLike = () => { + if (likedRef.current !== nextLiked) return; + const rollbackState = !nextLiked; + likedRef.current = rollbackState; + setLiked(rollbackState); + setLikeCount(prev => (nextLiked ? prev - 1 : prev + 1)); + }; + try { const response = await postFeedLike(feedId, nextLiked); - if (!response.isSuccess && likedRef.current === nextLiked) { - const rollbackState = !nextLiked; - likedRef.current = rollbackState; - setLiked(rollbackState); - setLikeCount(prev => (nextLiked ? prev - 1 : prev + 1)); - } + if (!response.isSuccess) rollbackLike(); } catch { - if (likedRef.current === nextLiked) { - const rollbackState = !nextLiked; - likedRef.current = rollbackState; - setLiked(rollbackState); - setLikeCount(prev => (nextLiked ? prev - 1 : prev + 1)); - } + rollbackLike(); } }); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/Post/PostFooter.tsx` around lines 62 - 76, The rollback logic that flips likedRef.current, setLiked and adjusts setLikeCount is duplicated in the postFeedLike error branch and catch block (and similarly in handleSave); extract that into a helper like rollbackLikeState(nextLiked:boolean) or rollback(actionNextState:boolean) and call it from both the !response.isSuccess branch and the catch block (and from handleSave) to centralize state rollback; ensure the helper updates likedRef.current, calls setLiked(...) and setLikeCount(prev => (nextLiked ? prev - 1 : prev + 1)) using the same nextLiked parameter so behavior is unchanged.src/components/memory/RecordItem/PollRecord.tsx (1)
44-68:mergeServerVoteItems헬퍼가 잘 구현되어 있습니다.서버 응답과 로컬 상태를 깔끔하게 병합하며,
isHighest를 max count 기준으로 재계산합니다. 다른 컴포넌트에서도 유사한 서버 응답 병합이 필요할 경우 재사용할 수 있는 좋은 패턴입니다.한 가지 사소한 점:
mergeServerVoteItems가 컴포넌트 내부에 정의되어 매 렌더마다 재생성됩니다.useCallback으로 감싸거나 컴포넌트 외부로 추출하면 불필요한 재생성을 방지할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/memory/RecordItem/PollRecord.tsx` around lines 44 - 68, mergeServerVoteItems가 컴포넌트 내부에 정의되어 매 렌더마다 재생성되므로 성능 최적화를 위해 재사용할 수 있게 변경하세요: mergeServerVoteItems 함수를 컴포넌트 바깥으로 이동하여 모듈 수준 유틸로 추출하거나(권장) 컴포넌트 내부에 그대로 두고 의존성 배열이 올바른 useCallback으로 래핑해 재생성을 방지합니다; 함수명 mergeServerVoteItems와 매개변수(serverVoteItems, baseOptions)를 유지하면서 호출 지점에서 변경사항이 없는지 확인하세요.src/components/feed/Profile.tsx (1)
43-71:Profile.tsx와UserProfileItem.tsx의 팔로우 로직이 거의 동일합니다.
toggleFollow함수,followedRef패턴, 서버 응답 reconciliation 로직이 두 컴포넌트에서 중복됩니다. 커스텀 훅(예:useOptimisticFollow)으로 추출하면 유지보수성이 크게 개선됩니다.// 예: useOptimisticFollow 훅 const useOptimisticFollow = (initialFollowing: boolean, userId?: number) => { const [followed, setFollowed] = useState(!!initialFollowing); const followedRef = useRef(!!initialFollowing); const { isLoading, run } = usePreventDoubleClick(); const toggle = () => { /* 공통 로직 */ }; return { followed, isLoading, toggle }; };Also applies to: 36-36
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/feed/Profile.tsx` around lines 43 - 71, Extract the duplicated optimistic-follow logic from toggleFollow into a reusable hook (e.g., useOptimisticFollow) and replace the inline implementation in Profile.tsx and UserProfileItem.tsx with that hook; the hook should accept initialFollowing and userId, manage state/ref (followed, followedRef), use the existing run/runFollow (usePreventDoubleClick) pattern, perform the postFollow(userId, nextFollowed) call, reconcile server response into followedRef/setFollowed, rollback on error, and expose { followed, isLoading, toggle } so callers can still call openPopup with nickname/response data after toggle resolves.src/components/feed/UserProfileItem.tsx (1)
58-63: 팔로우 실패 시 사용자 피드백이 없습니다.
catch블록에서 상태 롤백은 잘 처리되지만, 사용자에게 실패를 알리는 스낵바가 없습니다.Profile.tsx의 이전 리뷰에서도 동일한 피드백이 있었습니다.에러 스낵바 추가 예시
} catch { if (followedRef.current !== nextFollowed) return; const rollbackState = !nextFollowed; followedRef.current = rollbackState; setFollowed(rollbackState); + openPopup('snackbar', { + message: '팔로우 처리 중 오류가 발생했습니다.', + variant: 'top', + onClose: () => {}, + }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/feed/UserProfileItem.tsx` around lines 58 - 63, The catch block in UserProfileItem.tsx rolls back followedRef and setFollowed but does not notify the user of the failure; update the catch in the function that uses followedRef/current nextFollowed to also show an error snackbar/toast (use the project's existing notification hook/util such as enqueueSnackbar or showToast) while keeping the existing guard (if (followedRef.current !== nextFollowed) return) and rollback logic (followedRef.current = rollbackState; setFollowed(rollbackState)); ensure the notification includes a concise message like "Failed to update follow status" and any available error.message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/common/Post/Reply.tsx`:
- Around line 33-34: Reply.tsx initializes local state with const [liked,
setLiked] = useState(isLike) but never updates it when the isLike prop changes;
add a useEffect in Reply.tsx that watches isLike and calls setLiked(isLike)
(same pattern used in PostFooter) so the local liked state stays in sync when
the parent updates comment data—ensure you reference the existing liked and
setLiked state variables and the isLike prop.
In `@src/components/feed/Profile.tsx`:
- Around line 38-41: The useEffect that sets setFollowed and followedRef.current
from the isFollowing prop can overwrite followedRef during an in-flight
follow/unfollow API call, causing the stale-check (followedRef.current !==
nextFollowed) in the API response handler to incorrectly drop the response; add
a pending-request guard similar to PollRecord's isVoting: introduce a ref/state
like isFollowRequestRef (or isFollowingRequest) that is set true when the
follow/unfollow request starts and set false when it finishes, then change the
useEffect (the one that currently calls setFollowed(isFollowing) and sets
followedRef.current) to skip updating followedRef.current while
isFollowRequestRef.current is true (or otherwise check the guard) so in-flight
responses are not treated as stale; also ensure the API handler that does the
stale-check clears the request guard on completion.
In `@src/components/feed/UserProfileItem.tsx`:
- Around line 23-24: UserProfileItem initializes local state followed and
followedRef from the isFollowing prop but never syncs on prop changes; add a
useEffect in UserProfileItem.tsx that listens to changes of the isFollowing prop
and updates setFollowed(!!isFollowing) and followedRef.current = !!isFollowing
so the component UI stays consistent with parent updates (ensure the effect
depends on isFollowing).
---
Duplicate comments:
In `@src/components/feed/Profile.tsx`:
- Around line 64-69: The catch block in Profile.tsx currently only rolls back
the follow state (using followedRef.current, nextFollowed and setFollowed) but
provides no user-visible feedback; update the catch to keep the rollback logic
as-is and also display an error snackbar/toast (or call the existing
showSnackbar/showToast utility) with a clear message like "Failed to update
follow status" and include the error message for debugging; also optionally log
the error to console or a logger so failures are visible to developers while
preserving followedRef and setFollowed rollback behavior.
In `@src/components/memory/RecordItem/PollRecord.tsx`:
- Around line 99-102: The effect in PollRecord.tsx that syncs prop pollOptions
(useEffect that calls setCurrentOptions and writes optionsRef.current) can
clobber an in-progress optimistic update; update that effect to guard against
overwriting while a vote is in flight (check the isVoting flag) — i.e.,
early-return if isVoting is true so you don't call setCurrentOptions or mutate
optionsRef.current during an optimistic update, or implement a merge strategy
that applies incoming pollOptions only after the optimistic state completes (use
isVoting to defer or reconcile updates in the effect).
---
Nitpick comments:
In `@src/components/common/Post/PostFooter.tsx`:
- Around line 55-79: The optimistic like/save handlers (handleLike and
handleSave) currently roll back state on failure but provide no user feedback;
update both handlers to call the existing openSnackbar(errorMessage, { severity:
"error" }) (or the app's snackbar utility) when postFeedLike or postFeedSave
fails or throws, while preserving the current rollback logic that uses
likedRef/current state; locate postFeedLike/postFeedSave calls in handleLike and
handleSave and add the snackbar call in both the response.isSuccess === false
branch and the catch branch, passing a clear message like "좋아요에 실패했습니다" / "저장에
실패했습니다" (or localized equivalents).
- Around line 62-76: The rollback logic that flips likedRef.current, setLiked
and adjusts setLikeCount is duplicated in the postFeedLike error branch and
catch block (and similarly in handleSave); extract that into a helper like
rollbackLikeState(nextLiked:boolean) or rollback(actionNextState:boolean) and
call it from both the !response.isSuccess branch and the catch block (and from
handleSave) to centralize state rollback; ensure the helper updates
likedRef.current, calls setLiked(...) and setLikeCount(prev => (nextLiked ? prev
- 1 : prev + 1)) using the same nextLiked parameter so behavior is unchanged.
In `@src/components/feed/Profile.tsx`:
- Around line 43-71: Extract the duplicated optimistic-follow logic from
toggleFollow into a reusable hook (e.g., useOptimisticFollow) and replace the
inline implementation in Profile.tsx and UserProfileItem.tsx with that hook; the
hook should accept initialFollowing and userId, manage state/ref (followed,
followedRef), use the existing run/runFollow (usePreventDoubleClick) pattern,
perform the postFollow(userId, nextFollowed) call, reconcile server response
into followedRef/setFollowed, rollback on error, and expose { followed,
isLoading, toggle } so callers can still call openPopup with nickname/response
data after toggle resolves.
In `@src/components/feed/UserProfileItem.tsx`:
- Around line 58-63: The catch block in UserProfileItem.tsx rolls back
followedRef and setFollowed but does not notify the user of the failure; update
the catch in the function that uses followedRef/current nextFollowed to also
show an error snackbar/toast (use the project's existing notification hook/util
such as enqueueSnackbar or showToast) while keeping the existing guard (if
(followedRef.current !== nextFollowed) return) and rollback logic
(followedRef.current = rollbackState; setFollowed(rollbackState)); ensure the
notification includes a concise message like "Failed to update follow status"
and any available error.message.
In `@src/components/memory/RecordItem/PollRecord.tsx`:
- Around line 44-68: mergeServerVoteItems가 컴포넌트 내부에 정의되어 매 렌더마다 재생성되므로 성능 최적화를
위해 재사용할 수 있게 변경하세요: mergeServerVoteItems 함수를 컴포넌트 바깥으로 이동하여 모듈 수준 유틸로 추출하거나(권장)
컴포넌트 내부에 그대로 두고 의존성 배열이 올바른 useCallback으로 래핑해 재생성을 방지합니다; 함수명
mergeServerVoteItems와 매개변수(serverVoteItems, baseOptions)를 유지하면서 호출 지점에서 변경사항이
없는지 확인하세요.
In `@src/pages/searchBook/SearchBook.tsx`:
- Around line 125-127: The catch blocks in loadFirstFeeds and loadMore silently
swallow errors (currently "catch { // no-op }"); change each catch to capture
the error (e.g., catch (err)) and emit at least a console.warn or use the app
logger with a clear message and the error object so feed-loading failures are
visible for debugging and monitoring.
- Around line 208-230: The save handler in handleSaveButton currently rolls back
UI state on postSaveBook failure but provides no user feedback; update the
runSave async block in handleSaveButton to call openSnackbar (same pattern used
in PollRecord.tsx) when postSaveBook returns isSuccess === false or throws,
passing a clear error message; keep the existing rollback logic (isSavedRef and
setIsSaved) and ensure openSnackbar is imported/available in this component and
invoked in both the non-success branch and the catch block so users see an error
notification.
ℹ️ Review info
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
src/components/common/Post/PostFooter.tsxsrc/components/common/Post/Reply.tsxsrc/components/common/Post/SubReply.tsxsrc/components/feed/Profile.tsxsrc/components/feed/UserProfileItem.tsxsrc/components/memory/RecordItem/PollRecord.tsxsrc/pages/searchBook/SearchBook.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/common/Post/SubReply.tsx
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/components/feed/Profile.tsx (1)
34-41:⚠️ Potential issue | 🟡 Minor요청 중 prop 동기화 가드가 필요합니다.
팔로우 요청 진행 중 Line 38-41의 동기화가 followedRef를 덮어쓰면, 응답의 stale-check(Line 52)가 true가 되어 결과가 무시될 수 있습니다. 이전 리뷰와 동일 이슈이므로, 로딩 중에는 동기화를 건너뛰는 가드를 추가하세요.🛡️ 가드 추가 예시
useEffect(() => { + if (isFollowLoading) return; setFollowed(isFollowing); followedRef.current = !!isFollowing; -}, [isFollowing]); +}, [isFollowing, isFollowLoading]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/feed/Profile.tsx` around lines 34 - 41, The useEffect that syncs followed state is overwriting followedRef during an ongoing follow request, causing the stale-check in the follow response path to incorrectly ignore results; modify the useEffect for followed synchronization to skip updating setFollowed and followedRef.current while isFollowLoading is true (i.e., add a guard at the start of the useEffect that returns early when isFollowLoading), keeping the existing dependency array ([isFollowing, isFollowLoading]) so updates happen after the request completes; reference followedRef, setFollowed, isFollowing, isFollowLoading and the useEffect block around them.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/components/feed/Profile.tsx`:
- Around line 34-41: The useEffect that syncs followed state is overwriting
followedRef during an ongoing follow request, causing the stale-check in the
follow response path to incorrectly ignore results; modify the useEffect for
followed synchronization to skip updating setFollowed and followedRef.current
while isFollowLoading is true (i.e., add a guard at the start of the useEffect
that returns early when isFollowLoading), keeping the existing dependency array
([isFollowing, isFollowLoading]) so updates happen after the request completes;
reference followedRef, setFollowed, isFollowing, isFollowLoading and the
useEffect block around them.
ℹ️ Review info
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
src/components/feed/Profile.tsxsrc/components/memory/RecordItem/RecordItem.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/memory/RecordItem/RecordItem.tsx
📝작업 내용
상태 토글 액션(좋아요/저장/팔로우/투표/댓글 좋아요)에 중복 실행 방지 패턴을 공통 적용했습니다.
변경 내용
각 버튼에 동일 패턴 적용
적용범위
적용파일
기대 효과
위키
자세한 수정사항은 위키에서 참조부탁드립니다.
Summary by CodeRabbit
새로운 기능
버그 수정
잡일(Chore)