Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough새 훅들( Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI as Component (Post/Reply/Profile/Poll)
participant Hook as usePreventDoubleClick
participant API as Backend API
participant State as Local State / Refs
User->>UI: 좋아요/저장/팔로우/투표 클릭
UI->>Hook: run(asyncAction) 호출
activate Hook
Hook->>State: 낙관적 상태 즉시 적용 (toggle, count 조정)
Hook->>UI: 로딩 표시 활성화 (isLoading)
Hook->>API: (300ms 지연 후) 요청 전송
activate API
alt API 성공
API-->>Hook: 성공 응답
Hook->>State: 상태 유지 또는 서버 응답으로 보정
Hook->>UI: 성공 메시지(스낵바) 표시 가능
else API 실패
API-->>Hook: 실패 응답/에러
Hook->>State: 이전 상태로 롤백
Hook->>UI: 에러 스낵바 표시
end
deactivate API
Hook->>UI: 로딩 표시 해제
deactivate Hook
sequenceDiagram
participant User
participant Page as Page Component
participant Hook as useInifinieScroll
participant Observer as IntersectionObserver
participant API as Backend fetchPage
participant State as Hook State
User->>Page: 페이지 진입
Page->>Hook: useInifinieScroll 초기화(fetchPage 제공)
activate Hook
Hook->>API: fetchPage(cursor=null)
activate API
API-->>Hook: items, nextCursor, isLast
deactivate API
Hook->>State: items 업데이트, sentinelRef 설정
deactivate Hook
User->>Page: 스크롤(하단)
Observer->>Hook: sentinel 교차 -> loadMore
activate Hook
Hook->>API: fetchPage(cursor=nextCursor)
activate API
API-->>Hook: next items, nextCursor, isLast
deactivate API
Hook->>State: items 병합, isLoadingMore false
deactivate Hook
Page->>User: 신규 항목 렌더링
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
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: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/components/feed/UserProfileItem.tsx (1)
2-27:⚠️ Potential issue | 🟠 Major
isFollowing변경 시 로컬 상태가 동기화되지 않음초기값만 반영되어 부모 데이터가 업데이트될 때 버튼 상태가 stale될 수 있습니다.
Profile.tsx(38-41줄)의 패턴처럼useEffect를 추가하여 동기화해야 합니다.✅ 수정 제안
-import { useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react';그리고 26줄 이후에 추가:
+ useEffect(() => { + const normalized = !!isFollowing; + followedRef.current = normalized; + setFollowed(normalized); + }, [isFollowing]);🤖 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 2 - 27, The local follow state (followed and followedRef) in UserProfileItem is only set from the initial isFollowing prop and becomes stale when the parent updates isFollowing; add a useEffect in the UserProfileItem component that watches the isFollowing prop and updates both setFollowed(!!isFollowing) and followedRef.current = !!isFollowing so the button and ref stay in sync with prop changes; reference the followed state, followedRef, and the isFollowing prop in this effect to mirror the pattern used in Profile.tsx.src/components/common/Post/ReplyList.tsx (1)
66-87:⚠️ Potential issue | 🟠 Major초기 로딩 중
LoadingSpinner와EmptyState가 동시에 표시됩니다.무한 스크롤 모드에서 초기 로딩 시
list.length === 0이므로:
- 라인 66-68:
LoadingSpinner렌더링 ✓- 라인 69 → 82-87:
hasComments가 false →EmptyState렌더링 ✓두 컴포넌트가 동시에 화면에 표시되어 "아직 댓글이 없어요" 메시지와 로딩 스피너가 함께 노출됩니다.
🐛 수정 제안
+ {isInfiniteMode && commentFeed.isLoading && list.length === 0 && ( + <LoadingSpinner size="small" fullHeight={false} /> + )} - {hasComments ? ( + {hasComments ? ( list.map((comment, commentIndex) => ( ... )) + ) : isInfiniteMode && commentFeed.isLoading ? null : ( - ) : ( <EmptyState> <div className="title">아직 댓글이 없어요</div> <div className="sub-title">첫번째 댓글을 남겨보세요</div> </EmptyState> )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/Post/ReplyList.tsx` around lines 66 - 87, The EmptyState is rendered during initial infinite-scroll loading because isInfiniteMode && commentFeed.isLoading && list.length === 0 is true; update the EmptyState rendering logic so it is suppressed while the initial load is in progress—e.g., compute a showEmpty flag and only render EmptyState when !hasComments && !(isInfiniteMode && commentFeed.isLoading && list.length === 0), or incorporate that same condition into the existing ternary that decides between list.map and EmptyState; adjust usages of isInfiniteMode, commentFeed.isLoading, list.length, hasComments, LoadingSpinner, Reply, and SubReply accordingly.src/pages/feed/Feed.tsx (1)
79-114:⚠️ Potential issue | 🟡 Minor초기 로딩 500ms 목표 유지 여부 확인 필요.
로딩 처리 흐름이 바뀌어 체감 시간이 달라질 수 있으니, 초기 로딩이 500ms 목표를 충족하는지 측정/기록해 주세요.
Based on learnings: On src/pages/feed/Feed.tsx, ensure the feed page initial loading delay is 500ms (not 1000ms). This means the user-perceived load time should be within 500ms; verify network latency and any heavy computation on initial render. Implement optimizations such as code-splitting/lazy loading, suspense, skeletons, or incremental rendering to meet the target, and document measured latency in performance notes or tests.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/feed/Feed.tsx` around lines 79 - 114, The feed initial-loading flow currently shows LoadingSpinner tied to currentFeed.isLoading; ensure the user-perceived initial load is <=500ms by removing/shortening any artificial 1s delays in the feed data-fetch path and by lazy-loading heavy UI pieces (wrap TotalFeed and MyFeed in React.lazy + Suspense or render a lightweight skeleton instead of the full component during initial render); verify currentFeed.isLoading gating doesn't wait longer than 500ms and avoid expensive sync work in Feed render; add a performance measurement around the initial render/data fetch (timestamp when Feed mounts and when first meaningful content renders) and record/assert the measured latency in performance notes or a test to confirm <500ms.
🧹 Nitpick comments (6)
src/pages/searchBook/SearchBook.tsx (1)
159-183:handleSaveButton의 낙관적 UI 패턴은 적절하나 롤백 로직에 중복이 있습니다.라인 170-174와 175-180의 롤백 로직이 동일합니다. 헬퍼 함수로 추출하면 가독성이 개선됩니다.
♻️ 중복 제거 제안
const handleSaveButton = () => { if (!isbn) return; runSave(async () => { const nextSaved = !isSavedRef.current; isSavedRef.current = nextSaved; setIsSaved(nextSaved); await new Promise(resolve => setTimeout(resolve, 300)); + const rollbackIfStale = () => { + if (isSavedRef.current === nextSaved) { + const rollback = !nextSaved; + isSavedRef.current = rollback; + setIsSaved(rollback); + } + }; + try { const response = await postSaveBook(isbn, nextSaved); - if (!response.isSuccess && isSavedRef.current === nextSaved) { - const rollback = !nextSaved; - isSavedRef.current = rollback; - setIsSaved(rollback); - } + if (!response.isSuccess) rollbackIfStale(); } catch { - if (isSavedRef.current === nextSaved) { - const rollback = !nextSaved; - isSavedRef.current = rollback; - setIsSaved(rollback); - } + rollbackIfStale(); } }); };🤖 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 159 - 183, The rollback logic in handleSaveButton is duplicated in the try/failure and catch branches; extract a small helper (e.g., rollbackIfUnchanged or performRollback) inside or next to handleSaveButton that accepts the attempted state (nextSaved) and performs the isSavedRef.current check, flips to the rollback value, and calls setIsSaved; then replace both duplicated blocks with a call to that helper when postSaveBook fails or throws, keeping references to isSavedRef, setIsSaved and nextSaved and ensuring the helper uses the same conditional (if isSavedRef.current === nextSaved) before rolling back.src/pages/mypage/SavePage.tsx (1)
72-95: 낙관적 저장 토글 패턴이 적절합니다.unsave 시 목록에서 즉시 제거하고, 실패 시
previousBooks로 롤백하는 흐름이 깔끔합니다. 단,handleSaveToggle에usePreventDoubleClick이 적용되지 않았으므로, 빠른 연속 클릭 시 중복 API 호출이 발생할 수 있습니다. 다른 컴포넌트(PollRecord, SearchBook)에서는usePreventDoubleClick으로 보호하고 있으므로 동일하게 적용하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/mypage/SavePage.tsx` around lines 72 - 95, The handleSaveToggle function performs optimistic updates but lacks protection against rapid repeated clicks; wrap or guard handleSaveToggle with the existing usePreventDoubleClick hook (same pattern used in PollRecord/SearchBook) so multiple quick invocations don’t trigger duplicate postSaveBook calls. Locate handleSaveToggle and apply usePreventDoubleClick to the exported/onClick handler (or create a wrapped version like const handleSaveToggleSafe = usePreventDoubleClick(handleSaveToggle)) ensuring the wrapped handler calls postSaveBook and still performs the same optimistic savedBooks.setItems and rollback logic on failure.src/hooks/useInifinieScroll.ts (1)
72-88:loadMore가 state 의존성으로 인해 매 상태 변경마다 재생성되어 Observer가 자주 재구독됨
loadMore의useCallback의존성에isLast와nextCursor가 포함되어 있어, 페이지를 불러올 때마다loadMore→ observer effect 재실행 →disconnect()+ 새observe()사이클이 반복됩니다. 기능적으로 문제는 없지만, 빈번한 observer teardown/re-creation은 불필요한 오버헤드입니다.
isLast와nextCursor도 ref로 관리하면loadMore가 안정적인 참조를 유지하여 observer가 재구독되지 않습니다.Also applies to: 98-113
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/useInifinieScroll.ts` around lines 72 - 88, loadMore is being re-created whenever state variables isLast or nextCursor change; convert them to refs to stabilize the callback: create isLastRef and nextCursorRef (initialized from current state), update those refs wherever you call setIsLast or setNextCursor, and inside loadMore read isLastRef.current and nextCursorRef.current instead of the state variables; keep isFetchingRef and fetchPageRef usage as-is and remove isLast and nextCursor from loadMore's useCallback dependencies so loadMore maintains a stable reference for the observer (also apply the same ref-based pattern to the other loadMore-like callback at lines 98-113).src/components/common/Post/ReplyList.tsx (1)
56-62:useCallback의존성에commentFeed전체 객체를 넣으면 매 렌더마다 불필요하게 재생성됩니다.
useInifinieScroll훅은 매 렌더마다 새로운 객체를 반환하므로, 의존성 배열에commentFeed를 넣으면handleReload가 계속 재생성됩니다. 실제로 필요한 것은commentFeed.reload(=loadFirstPage)뿐이며, 이는useCallback으로 감싸져 있어 안정적입니다.commentFeed.reload만 참조하도록 변경하세요.♻️ 수정 제안
- }, [commentFeed, isInfiniteMode, onReload]); + }, [commentFeed.reload, isInfiniteMode, onReload]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/Post/ReplyList.tsx` around lines 56 - 62, The handleReload useCallback currently depends on the whole commentFeed object which causes unnecessary re-creations because useInfiniteScroll returns a new object each render; change the dependency to reference only the stable method commentFeed.reload (i.e., use commentFeed.reload in the callback and add commentFeed.reload to the dependency array) so handleReload remains stable; update the useCallback declaration for handleReload to call commentFeed.reload() when isInfiniteMode and include only commentFeed.reload, isInfiniteMode, and onReload in the dependency list.src/pages/memory/Memory.tsx (1)
193-203: 새 기록 선삽입 시 중복 방지 고려.서버 응답에 동일 기록이 포함될 수 있어 중복 노출 가능성이 있습니다. id 기준으로 이미 존재하면 prepend를 건너뛰는 처리를 고려해주세요.
♻️ 중복 방지 예시
- setRecordItems(prev => [newRecord, ...prev]); + setRecordItems(prev => + prev.some(record => record.id === newRecord.id) ? prev : [newRecord, ...prev], + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/memory/Memory.tsx` around lines 193 - 203, When handling a newly uploaded record in the useEffect that reads location.state?.newRecord, avoid blindly prepending it via setRecordItems; instead check the existing list (recordsList.items or the currentRecords memo) for an item with the same id and only call setRecordItems(prev => [newRecord, ...prev]) if no item with newRecord.id already exists, then continue to setShowUploadProgress(true) and navigate(...). This ensures duplication is prevented when the server response already contains the same record.src/components/common/Post/PostFooter.tsx (1)
45-54: 부모에서 likeCount 갱신 시 동기화 추가 고려.props 갱신으로
initialLikeCount가 바뀔 때 로컬 상태가 stale될 수 있어 동기화용 effect를 추가하는 편이 안전합니다.♻️ 동기화 예시
+ useEffect(() => { + setLikeCount(initialLikeCount); + }, [initialLikeCount]);🤖 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 45 - 54, Props updates to initialLikeCount aren't synchronized to local state, so add an effect that updates the local likeCount state and any likeCountRef when the initialLikeCount prop changes; specifically, add a useEffect watching initialLikeCount that calls setLikeCount(initialLikeCount) and sets likeCountRef.current = initialLikeCount (mirroring the existing patterns used for isLiked/isSaved with setLiked/likedRef and setSaved/savedRef).
🤖 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/group/MyGroupModal.tsx`:
- Around line 138-140: The inline style on the sentinel div uses gridColumn
('gridColumn: "1 / -1"') but the parent Content styled component uses
flex-direction: column so gridColumn has no effect; in MyGroupModal remove the
unnecessary gridColumn style from the div that uses roomList.sentinelRef, or if
the intent was to span columns change the parent layout to CSS grid in the
Content styled component—locate the sentinel div (roomList.sentinelRef) and
either drop gridColumn or update Content to display: grid with appropriate grid
template so the spanning makes sense.
In `@src/components/memory/RecordItem/PollRecord.tsx`:
- Around line 96-105: The code in PollRecord uses
Math.max(...optimisticOptions.map(...)) which returns -Infinity for an empty
optimisticOptions; guard this by deriving maxCount safely (e.g., default to 0
when optimisticOptions is empty) before computing normalizedOptions, and ensure
optionsRef/current state logic (optionsRef.current, setCurrentOptions) uses that
safe maxCount; also remove or make the artificial await new Promise(resolve =>
setTimeout(resolve, 300)) explicit/configurable (e.g., remove the fixed 300ms
delay or replace it with a documented, optional animation delay flag) and add a
brief comment explaining any retained delay so callers understand the UX
tradeoff.
In `@src/hooks/useDebouncedCallback.ts`:
- Around line 3-6: The function generics violate the no-explicit-any rule;
update the parameter type in useDebouncedCallback so T is constrained with
unknown[] instead of any[] (e.g., change T extends (...args: any[]) => void to T
extends (...args: unknown[]) => void and adjust the callback parameter typing
accordingly) to improve type safety and satisfy
`@typescript-eslint/no-explicit-any`.
In `@src/hooks/useInifinieScroll.ts`:
- Line 20: Rename the hook symbol useInifinieScroll to useInfiniteScroll and
update its filename and all imports/exports accordingly: change the exported
function name in src/hooks/useInifinieScroll.ts to useInfiniteScroll, rename the
file to src/hooks/useInfiniteScroll.ts (or update any barrel export), then run a
repository-wide replace for "useInifinieScroll" → "useInfiniteScroll" to update
all import statements and usages so they match the corrected export and avoid
broken references.
- Around line 53-70: There's a race where reloadKey changes while a previous
fetch is in-flight causing loadFirstPage to early-return and later the stale
fetch to overwrite state; fix by adding a generation counter ref (e.g.,
generationRef) that's incremented in the effect that responds to reloadKey,
capture const gen = generationRef.current at the start of loadFirstPage, change
the isFetchingRef guard to only short-circuit if isFetchingRef.current && gen
=== generationRef.current (so a new generation can start its own fetch), and
before calling setItems/setNextCursor/setIsLast check gen ===
generationRef.current to ignore stale responses from older generations; update
references to fetchPageRef, isFetchingRef, and loadFirstPage accordingly.
In `@src/main.tsx`:
- Line 8: package.json currently includes `@tanstack/react-query` but main
bootstrap uses createRoot(...).render(<App />) without initializing or providing
a QueryClient; either remove the unused dependency from package.json or wire up
react-query by creating a QueryClient instance and wrapping the root with
QueryClientProvider (e.g., in main.tsx or inside App.tsx) so the app is rendered
as <QueryClientProvider client={queryClient}><App/></QueryClientProvider>;
update imports and ensure QueryClient is constructed before calling
createRoot(...).render.
In `@src/pages/groupSearch/GroupSearch.tsx`:
- Around line 136-168: The current useInifinieScroll usage can miss a new search
if a previous fetch is in-flight (isFetchingRef) because loadFirstPage returns
early; update useInifinieScroll to guarantee reloads when reloadKey changes by
either (A) adding a pendingReload boolean/queue that, when a reload is requested
during isFetchingRef, stores the intent and runs loadFirstPage again once the
current fetch finishes, or (B) support abortable fetches: accept an AbortSignal
into fetchPage (and forward it to getSearchRooms) and cancel the prior request
on reload so the new queryTerm/searchStatus immediately triggers a fresh fetch;
update the hook API (e.g., expose triggerReload(force=true) or ensure reloadKey
changes always call loadFirstPage) and adjust call sites (the GroupSearch use of
useInifinieScroll and fetchPage) to use the new API or pass an AbortSignal so
the latest search is always loaded.
---
Outside diff comments:
In `@src/components/common/Post/ReplyList.tsx`:
- Around line 66-87: The EmptyState is rendered during initial infinite-scroll
loading because isInfiniteMode && commentFeed.isLoading && list.length === 0 is
true; update the EmptyState rendering logic so it is suppressed while the
initial load is in progress—e.g., compute a showEmpty flag and only render
EmptyState when !hasComments && !(isInfiniteMode && commentFeed.isLoading &&
list.length === 0), or incorporate that same condition into the existing ternary
that decides between list.map and EmptyState; adjust usages of isInfiniteMode,
commentFeed.isLoading, list.length, hasComments, LoadingSpinner, Reply, and
SubReply accordingly.
In `@src/components/feed/UserProfileItem.tsx`:
- Around line 2-27: The local follow state (followed and followedRef) in
UserProfileItem is only set from the initial isFollowing prop and becomes stale
when the parent updates isFollowing; add a useEffect in the UserProfileItem
component that watches the isFollowing prop and updates both
setFollowed(!!isFollowing) and followedRef.current = !!isFollowing so the button
and ref stay in sync with prop changes; reference the followed state,
followedRef, and the isFollowing prop in this effect to mirror the pattern used
in Profile.tsx.
In `@src/pages/feed/Feed.tsx`:
- Around line 79-114: The feed initial-loading flow currently shows
LoadingSpinner tied to currentFeed.isLoading; ensure the user-perceived initial
load is <=500ms by removing/shortening any artificial 1s delays in the feed
data-fetch path and by lazy-loading heavy UI pieces (wrap TotalFeed and MyFeed
in React.lazy + Suspense or render a lightweight skeleton instead of the full
component during initial render); verify currentFeed.isLoading gating doesn't
wait longer than 500ms and avoid expensive sync work in Feed render; add a
performance measurement around the initial render/data fetch (timestamp when
Feed mounts and when first meaningful content renders) and record/assert the
measured latency in performance notes or a test to confirm <500ms.
---
Nitpick comments:
In `@src/components/common/Post/PostFooter.tsx`:
- Around line 45-54: Props updates to initialLikeCount aren't synchronized to
local state, so add an effect that updates the local likeCount state and any
likeCountRef when the initialLikeCount prop changes; specifically, add a
useEffect watching initialLikeCount that calls setLikeCount(initialLikeCount)
and sets likeCountRef.current = initialLikeCount (mirroring the existing
patterns used for isLiked/isSaved with setLiked/likedRef and setSaved/savedRef).
In `@src/components/common/Post/ReplyList.tsx`:
- Around line 56-62: The handleReload useCallback currently depends on the whole
commentFeed object which causes unnecessary re-creations because
useInfiniteScroll returns a new object each render; change the dependency to
reference only the stable method commentFeed.reload (i.e., use
commentFeed.reload in the callback and add commentFeed.reload to the dependency
array) so handleReload remains stable; update the useCallback declaration for
handleReload to call commentFeed.reload() when isInfiniteMode and include only
commentFeed.reload, isInfiniteMode, and onReload in the dependency list.
In `@src/hooks/useInifinieScroll.ts`:
- Around line 72-88: loadMore is being re-created whenever state variables
isLast or nextCursor change; convert them to refs to stabilize the callback:
create isLastRef and nextCursorRef (initialized from current state), update
those refs wherever you call setIsLast or setNextCursor, and inside loadMore
read isLastRef.current and nextCursorRef.current instead of the state variables;
keep isFetchingRef and fetchPageRef usage as-is and remove isLast and nextCursor
from loadMore's useCallback dependencies so loadMore maintains a stable
reference for the observer (also apply the same ref-based pattern to the other
loadMore-like callback at lines 98-113).
In `@src/pages/memory/Memory.tsx`:
- Around line 193-203: When handling a newly uploaded record in the useEffect
that reads location.state?.newRecord, avoid blindly prepending it via
setRecordItems; instead check the existing list (recordsList.items or the
currentRecords memo) for an item with the same id and only call
setRecordItems(prev => [newRecord, ...prev]) if no item with newRecord.id
already exists, then continue to setShowUploadProgress(true) and navigate(...).
This ensures duplication is prevented when the server response already contains
the same record.
In `@src/pages/mypage/SavePage.tsx`:
- Around line 72-95: The handleSaveToggle function performs optimistic updates
but lacks protection against rapid repeated clicks; wrap or guard
handleSaveToggle with the existing usePreventDoubleClick hook (same pattern used
in PollRecord/SearchBook) so multiple quick invocations don’t trigger duplicate
postSaveBook calls. Locate handleSaveToggle and apply usePreventDoubleClick to
the exported/onClick handler (or create a wrapped version like const
handleSaveToggleSafe = usePreventDoubleClick(handleSaveToggle)) ensuring the
wrapped handler calls postSaveBook and still performs the same optimistic
savedBooks.setItems and rollback logic on failure.
In `@src/pages/searchBook/SearchBook.tsx`:
- Around line 159-183: The rollback logic in handleSaveButton is duplicated in
the try/failure and catch branches; extract a small helper (e.g.,
rollbackIfUnchanged or performRollback) inside or next to handleSaveButton that
accepts the attempted state (nextSaved) and performs the isSavedRef.current
check, flips to the rollback value, and calls setIsSaved; then replace both
duplicated blocks with a call to that helper when postSaveBook fails or throws,
keeping references to isSavedRef, setIsSaved and nextSaved and ensuring the
helper uses the same conditional (if isSavedRef.current === nextSaved) before
rolling back.
ℹ️ 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 ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (23)
package.jsonsrc/components/common/Post/PostFooter.tsxsrc/components/common/Post/Reply.tsxsrc/components/common/Post/ReplyList.tsxsrc/components/common/Post/SubReply.tsxsrc/components/feed/Profile.tsxsrc/components/feed/UserProfileItem.tsxsrc/components/group/MyGroupModal.tsxsrc/components/memory/RecordItem/PollRecord.tsxsrc/components/search/GroupSearchResult.styled.tssrc/components/search/GroupSearchResult.tsxsrc/hooks/useDebouncedCallback.tssrc/hooks/useInifinieScroll.tssrc/hooks/usePreventDoubleClick.tssrc/main.tsxsrc/pages/feed/Feed.tsxsrc/pages/feed/FeedDetailPage.tsxsrc/pages/feed/FollowerListPage.tsxsrc/pages/groupSearch/GroupSearch.tsxsrc/pages/memory/Memory.tsxsrc/pages/mypage/SavePage.tsxsrc/pages/notice/Notice.tsxsrc/pages/searchBook/SearchBook.tsx
| {!roomList.isLast && ( | ||
| <div ref={roomList.sentinelRef} style={{ gridColumn: '1 / -1', height: 20 }} /> | ||
| )} |
There was a problem hiding this comment.
gridColumn: '1 / -1'은 부모가 flex 레이아웃이므로 효과 없음
Content 스타일드 컴포넌트는 flex-direction: column을 사용하므로 gridColumn 속성이 적용되지 않습니다. 불필요한 스타일이므로 제거하거나, 의도한 레이아웃이 grid라면 확인이 필요합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/group/MyGroupModal.tsx` around lines 138 - 140, The inline
style on the sentinel div uses gridColumn ('gridColumn: "1 / -1"') but the
parent Content styled component uses flex-direction: column so gridColumn has no
effect; in MyGroupModal remove the unnecessary gridColumn style from the div
that uses roomList.sentinelRef, or if the intent was to span columns change the
parent layout to CSS grid in the Content styled component—locate the sentinel
div (roomList.sentinelRef) and either drop gridColumn or update Content to
display: grid with appropriate grid template so the spanning makes sense.
| const maxCount = Math.max(...optimisticOptions.map(item => item.count)); | ||
| const normalizedOptions = optimisticOptions.map(item => ({ | ||
| ...item, | ||
| isHighest: item.count === maxCount, | ||
| })); | ||
|
|
||
| optionsRef.current = normalizedOptions; | ||
| setCurrentOptions(normalizedOptions); | ||
|
|
||
| await new Promise(resolve => setTimeout(resolve, 300)); |
There was a problem hiding this comment.
Math.max(...[]) 엣지 케이스 및 인위적 딜레이
-
optimisticOptions가 빈 배열이면Math.max()호출 시-Infinity가 반환됩니다. 현재 투표 옵션이 없는 경우는 실질적으로 발생하기 어렵지만, 방어 코드를 추가하면 안전합니다. -
라인 105의
setTimeout(resolve, 300)— 낙관적 UI 애니메이션을 위한 의도적 딜레이로 이해되나, API 실패 시 유저가 300ms 동안 잘못된 상태를 보게 되며, 네트워크 지연이 합쳐지면 체감 응답 시간이 길어집니다. 필요한 경우 주석으로 의도를 남겨두세요.
🤖 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 96 - 105, The
code in PollRecord uses Math.max(...optimisticOptions.map(...)) which returns
-Infinity for an empty optimisticOptions; guard this by deriving maxCount safely
(e.g., default to 0 when optimisticOptions is empty) before computing
normalizedOptions, and ensure optionsRef/current state logic
(optionsRef.current, setCurrentOptions) uses that safe maxCount; also remove or
make the artificial await new Promise(resolve => setTimeout(resolve, 300))
explicit/configurable (e.g., remove the fixed 300ms delay or replace it with a
documented, optional animation delay flag) and add a brief comment explaining
any retained delay so callers understand the UX tradeoff.
| export const useDebouncedCallback = <T extends (...args: any[]) => void>( | ||
| callback: T, | ||
| delay: number, | ||
| ) => { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check ESLint configuration for no-explicit-any rule
find . -name ".eslintrc*" -o -name "eslint.config.*" | head -5Repository: THIP-TextHip/THIP-Web
Length of output: 459
🏁 Script executed:
# Read the specific file to see current state
cat -n src/hooks/useDebouncedCallback.tsRepository: THIP-TextHip/THIP-Web
Length of output: 1064
🏁 Script executed:
# Search for ESLint rule configuration and useDebouncedCallback usages
rg -n "no-explicit-any" -A 2 -B 2
rg -n "useDebouncedCallback" src --type ts --type tsxRepository: THIP-TextHip/THIP-Web
Length of output: 92
🏁 Script executed:
# Read ESLint configuration
cat -n eslint.config.jsRepository: THIP-TextHip/THIP-Web
Length of output: 954
🏁 Script executed:
# Search for usages of useDebouncedCallback
rg -n "useDebouncedCallback" srcRepository: THIP-TextHip/THIP-Web
Length of output: 174
🏁 Script executed:
# Check if any[] in function parameter types would trigger the rule
rg -n "any\[\]" src --type ts -B 2 -A 2Repository: THIP-TextHip/THIP-Web
Length of output: 403
any[] 사용으로 ESLint 규칙 위반
tseslint.configs.recommended 구성에 포함된 @typescript-eslint/no-explicit-any 규칙을 위반합니다. unknown[]으로 대체하면 타입 안정성이 개선됩니다.
✅ 수정 제안
-export const useDebouncedCallback = <T extends (...args: any[]) => void>(
+export const useDebouncedCallback = <T extends (...args: unknown[]) => void>(📝 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.
| export const useDebouncedCallback = <T extends (...args: any[]) => void>( | |
| callback: T, | |
| delay: number, | |
| ) => { | |
| export const useDebouncedCallback = <T extends (...args: unknown[]) => void>( | |
| callback: T, | |
| delay: number, | |
| ) => { |
🧰 Tools
🪛 ESLint
[error] 3-3: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/hooks/useDebouncedCallback.ts` around lines 3 - 6, The function generics
violate the no-explicit-any rule; update the parameter type in
useDebouncedCallback so T is constrained with unknown[] instead of any[] (e.g.,
change T extends (...args: any[]) => void to T extends (...args: unknown[]) =>
void and adjust the callback parameter typing accordingly) to improve type
safety and satisfy `@typescript-eslint/no-explicit-any`.
| threshold?: number; | ||
| } | ||
|
|
||
| export const useInifinieScroll = <T>({ |
There was a problem hiding this comment.
훅 이름 오타: useInifinieScroll → useInfiniteScroll
"Inifinie"는 "Infinite"의 오타입니다. 이 이름이 파일명, 내보내기, 그리고 모든 소비자 파일의 import 경로에 전파되어 있습니다. 현재 기능에 영향은 없지만 코드베이스 전체 검색 및 가독성에 부정적입니다.
#!/bin/bash
# Description: 오타가 포함된 import 경로를 사용하는 모든 파일 확인
rg -n "useInifinieScroll" --type=ts --type=tsx -g '!node_modules'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/hooks/useInifinieScroll.ts` at line 20, Rename the hook symbol
useInifinieScroll to useInfiniteScroll and update its filename and all
imports/exports accordingly: change the exported function name in
src/hooks/useInifinieScroll.ts to useInfiniteScroll, rename the file to
src/hooks/useInfiniteScroll.ts (or update any barrel export), then run a
repository-wide replace for "useInifinieScroll" → "useInfiniteScroll" to update
all import statements and usages so they match the corrected export and avoid
broken references.
| const loadFirstPage = useCallback(async () => { | ||
| if (!enabled || isFetchingRef.current) return; | ||
| isFetchingRef.current = true; | ||
| setIsLoading(true); | ||
| setError(null); | ||
|
|
||
| try { | ||
| const res = await fetchPageRef.current(null); | ||
| setItems(res.items); | ||
| setNextCursor(res.nextCursor); | ||
| setIsLast(res.isLast); | ||
| } catch (error) { | ||
| setError(error instanceof Error ? error.message : '목록을 불러오지 못했습니다.'); | ||
| } finally { | ||
| setIsLoading(false); | ||
| isFetchingRef.current = false; | ||
| } | ||
| }, [enabled]); |
There was a problem hiding this comment.
reloadKey 변경 시 진행 중인 fetch가 있으면 stale 데이터가 표시되는 레이스 컨디션
reloadKey가 변경되면 effect(라인 90-96)가 items를 초기화하고 loadFirstPage()를 호출합니다. 그러나 이전 fetch가 진행 중이면 isFetchingRef.current가 true이므로 loadFirstPage가 즉시 반환됩니다. 이후 이전 fetch가 완료되면 stale 데이터가 setItems로 설정되고, isFetchingRef가 해제되지만 새로운 loadFirstPage는 다시 호출되지 않습니다.
결과적으로 탭 전환이나 키 변경 시 빈 목록 또는 이전 탭의 데이터가 표시될 수 있습니다.
수정 방향: 생성 카운터(generation counter) 또는 AbortController를 사용하여 stale 응답을 무시하세요.
🐛 생성 카운터 기반 수정 제안
+ const generationRef = useRef(0);
const loadFirstPage = useCallback(async () => {
if (!enabled || isFetchingRef.current) return;
+ const gen = generationRef.current;
isFetchingRef.current = true;
setIsLoading(true);
setError(null);
try {
const res = await fetchPageRef.current(null);
+ if (gen !== generationRef.current) return; // stale response
setItems(res.items);
setNextCursor(res.nextCursor);
setIsLast(res.isLast);
} catch (error) {
+ if (gen !== generationRef.current) return;
setError(error instanceof Error ? error.message : '목록을 불러오지 못했습니다.');
} finally {
setIsLoading(false);
isFetchingRef.current = false;
}
}, [enabled]);
const loadMore = useCallback(async () => {
if (!enabled || isFetchingRef.current || isLast || !nextCursor) return;
+ const gen = generationRef.current;
isFetchingRef.current = true;
setIsLoadingMore(true);
try {
const res = await fetchPageRef.current(nextCursor);
+ if (gen !== generationRef.current) return;
setItems(prev => merge(prev, res.items));
setNextCursor(res.nextCursor);
setIsLast(res.isLast);
} catch (error) {
+ if (gen !== generationRef.current) return;
setError(error instanceof Error ? error.message : '추가 목록을 불러오지 못했습니다.');
} finally {
setIsLoadingMore(false);
isFetchingRef.current = false;
}
}, [enabled, isLast, nextCursor, merge]);
useEffect(() => {
if (!enabled) return;
+ generationRef.current += 1;
+ isFetchingRef.current = false; // allow new fetch
setItems([]);
setNextCursor(null);
setIsLast(false);
void loadFirstPage();
}, [enabled, reloadKey, loadFirstPage]);Also applies to: 90-96
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/hooks/useInifinieScroll.ts` around lines 53 - 70, There's a race where
reloadKey changes while a previous fetch is in-flight causing loadFirstPage to
early-return and later the stale fetch to overwrite state; fix by adding a
generation counter ref (e.g., generationRef) that's incremented in the effect
that responds to reloadKey, capture const gen = generationRef.current at the
start of loadFirstPage, change the isFetchingRef guard to only short-circuit if
isFetchingRef.current && gen === generationRef.current (so a new generation can
start its own fetch), and before calling setItems/setNextCursor/setIsLast check
gen === generationRef.current to ignore stale responses from older generations;
update references to fetchPageRef, isFetchingRef, and loadFirstPage accordingly.
| initGA(); | ||
|
|
||
| createRoot(document.getElementById('root')!).render(<App />); | ||
| createRoot(document.getElementById('root')!).render(<App />); No newline at end of file |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for QueryClientProvider and QueryClient configuration
echo "=== Searching for QueryClientProvider and QueryClient ==="
rg -n "QueryClientProvider|QueryClient" src
echo -e "\n=== Searching for react-query imports ==="
rg -n "from.*react-query|import.*react-query|from.*@tanstack/react-query|import.*@tanstack/react-query" src
echo -e "\n=== Checking main.tsx content ==="
cat -n src/main.tsx
echo -e "\n=== Checking App.tsx content ==="
fd "App.tsx" src --exec cat -n {}Repository: THIP-TextHip/THIP-Web
Length of output: 978
🏁 Script executed:
#!/bin/bash
# Check if react-query or `@tanstack/react-query` is in package.json
echo "=== Checking package.json for react-query dependency ==="
rg -n "react-query|@tanstack/react-query" package.json
echo -e "\n=== Full dependencies section ==="
cat -n package.json | grep -A 30 '"dependencies"'Repository: THIP-TextHip/THIP-Web
Length of output: 1324
🏁 Script executed:
#!/bin/bash
# Check if react-query hooks are actually used in the codebase
echo "=== Searching for react-query hook usage (useQuery, useMutation, etc.) ==="
rg -n "useQuery|useMutation|useInfiniteQuery|useIsFetching|useQueries|useQueryClient|useQueryErrorResetBoundary" src
echo -e "\n=== Checking all imports from `@tanstack/react-query` ==="
rg -n "from.*@tanstack/react-query|import.*@tanstack/react-query" src
echo -e "\n=== Checking if QueryClientProvider exists anywhere ==="
rg -n "QueryClientProvider" .Repository: THIP-TextHip/THIP-Web
Length of output: 253
@tanstack/react-query 사용 계획 확인 필요
@tanstack/react-query가 package.json에 추가되었지만 현재 코드에서는 사용되고 있지 않습니다. 두 가지 중 하나를 진행해야 합니다:
- react-query를 사용할 계획이 없다면: 불필요한 의존성이므로 package.json에서 제거하세요.
- 향후 react-query 사용을 계획 중이라면:
QueryClientProvider를main.tsx또는App.tsx의 루트에 설정하고QueryClient를 초기화하세요. 예를 들어:
const queryClient = new QueryClient();
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main.tsx` at line 8, package.json currently includes
`@tanstack/react-query` but main bootstrap uses createRoot(...).render(<App />)
without initializing or providing a QueryClient; either remove the unused
dependency from package.json or wire up react-query by creating a QueryClient
instance and wrapping the root with QueryClientProvider (e.g., in main.tsx or
inside App.tsx) so the app is rendered as <QueryClientProvider
client={queryClient}><App/></QueryClientProvider>; update imports and ensure
QueryClient is constructed before calling createRoot(...).render.
| const queryTerm = searchStatus === 'searching' ? debouncedSearchTerm : searchTerm.trim(); | ||
| const searchResult = useInifinieScroll({ | ||
| enabled: searchStatus !== 'idle' && (searchStatus === 'searched' || queryTerm.length > 0), | ||
| reloadKey: `${searchStatus}-${queryTerm}-${selectedFilter}-${category}`, | ||
| fetchPage: async cursor => { | ||
| const isFinalized = searchStatus === 'searched'; | ||
| const isAllCategory = !queryTerm && category === ''; | ||
| if (searchStatus === 'searching' && !queryTerm) { | ||
| return { items: [], nextCursor: null, isLast: true }; | ||
| } | ||
|
|
||
| const res = await getSearchRooms( | ||
| trimmedTerm, | ||
| queryTerm, | ||
| toSortKey(selectedFilter), | ||
| nextCursor, | ||
| cursor ?? undefined, | ||
| isFinalized, | ||
| category, | ||
| isAllCategory, | ||
| ); | ||
| if (res.isSuccess) { | ||
| const { roomList, nextCursor: nc, isLast: last } = res.data; | ||
|
|
||
| setRooms(prev => [...prev, ...roomList]); | ||
| setNextCursor(nc); | ||
| setIsLast(last); | ||
| } else { | ||
| setIsLast(true); | ||
| if (!res.isSuccess) { | ||
| throw new Error(res.message || '검색 실패'); | ||
| } | ||
| } catch { | ||
| setIsLast(true); | ||
| } finally { | ||
| setIsLoadingMore(false); | ||
| } | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [searchTerm, nextCursor, isLast, isLoadingMore, selectedFilter, searchStatus, category]); | ||
|
|
||
| const lastRoomElementCallback = useCallback( | ||
| (node: HTMLDivElement | null) => { | ||
| if (isLoadingMore || isLast) return; | ||
|
|
||
| if (observerRef.current) observerRef.current.disconnect(); | ||
|
|
||
| observerRef.current = new IntersectionObserver(entries => { | ||
| if (entries[0].isIntersecting && !isLoadingMore && !isLast) { | ||
| loadMore(); | ||
| } | ||
| }); | ||
|
|
||
| if (node) observerRef.current.observe(node); | ||
| return { | ||
| items: res.data.roomList, | ||
| nextCursor: res.data.nextCursor, | ||
| isLast: res.data.isLast, | ||
| }; | ||
| }, | ||
| [isLoadingMore, isLast, loadMore], | ||
| ); | ||
| rootMargin: '100px 0px', | ||
| threshold: 0.1, | ||
| }); |
There was a problem hiding this comment.
빠른 검색어 변경 시 새 검색이 누락될 수 있습니다.
useInifinieScroll이 in-flight 상태(isFetchingRef)면 loadFirstPage를 즉시 반환하므로, 검색어가 바뀌는 타이밍에 이전 요청이 진행 중이면 새 검색이 트리거되지 않을 수 있습니다. 훅 레벨에서 pending reload 큐를 두거나, 요청을 abort/ignore하는 방식으로 최신 검색어가 반드시 로드되도록 보완해주세요. (이 훅을 쓰는 다른 화면에도 동일 영향)
🛠️ useInifinieScroll 보완 예시(개념)
+ const pendingReloadRef = useRef(false);
const loadFirstPage = useCallback(async () => {
- if (!enabled || isFetchingRef.current) return;
+ if (!enabled) return;
+ if (isFetchingRef.current) {
+ pendingReloadRef.current = true;
+ return;
+ }
isFetchingRef.current = true;
setIsLoading(true);
setError(null);
try {
const res = await fetchPageRef.current(null);
setItems(res.items);
setNextCursor(res.nextCursor);
setIsLast(res.isLast);
} finally {
setIsLoading(false);
isFetchingRef.current = false;
+ if (pendingReloadRef.current) {
+ pendingReloadRef.current = false;
+ void loadFirstPage();
+ }
}
}, [enabled]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/groupSearch/GroupSearch.tsx` around lines 136 - 168, The current
useInifinieScroll usage can miss a new search if a previous fetch is in-flight
(isFetchingRef) because loadFirstPage returns early; update useInifinieScroll to
guarantee reloads when reloadKey changes by either (A) adding a pendingReload
boolean/queue that, when a reload is requested during isFetchingRef, stores the
intent and runs loadFirstPage again once the current fetch finishes, or (B)
support abortable fetches: accept an AbortSignal into fetchPage (and forward it
to getSearchRooms) and cancel the prior request on reload so the new
queryTerm/searchStatus immediately triggers a fresh fetch; update the hook API
(e.g., expose triggerReload(force=true) or ensure reloadKey changes always call
loadFirstPage) and adjust call sites (the GroupSearch use of useInifinieScroll
and fetchPage) to use the new API or pass an AbortSignal so the latest search is
always loaded.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsx (2)
29-29: React 19 관용적useRef타입 표기로 통일하세요.React 19에서
useRef<T>(null)의 반환 타입은RefObject<T | null>로 변경되었습니다. 현재useRef<HTMLDivElement | null>(null)은 타입 파라미터에| null을 명시적으로 포함시켜 최종 타입이RefObject<HTMLDivElement | null>로 동일하게 추론되긴 하지만, 관용적 표기는useRef<HTMLDivElement>(null)입니다.♻️ 관용적 표기 제안
- const contentRef = useRef<HTMLDivElement | null>(null); + const contentRef = useRef<HTMLDivElement>(null);As per the React 19 library documentation:
useRef<T>(null)→RefObject<T | null>.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsx` at line 29, The ref declaration contentRef uses a non-idiomatic type; change the generic from useRef<HTMLDivElement | null>(null) to useRef<HTMLDivElement>(null) so the inferred type becomes RefObject<HTMLDivElement | null> per React 19 conventions; update the declaration named contentRef accordingly (search for contentRef/useRef in GlobalCommentBottomSheet) and run typecheck to confirm no other sites rely on the explicit | null annotation.
124-124:reloadKey의isOpen접미사가 항상'open'이므로 불필요합니다.
GlobalCommentBottomSheet는!isOpen일 때null을 반환(Line 110)하므로,ReplyList가 실제로 마운트되는 시점에서isOpen은 항상true입니다. 따라서${isOpen ? 'open' : 'closed'}접미사는 항상'-open'으로 평가되어 읽는 이에게 혼선을 줄 수 있습니다.♻️ 단순화 제안
- reloadKey={`${replyReloadKey}-${isOpen ? 'open' : 'closed'}`} + reloadKey={String(replyReloadKey)}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsx` at line 124, The reloadKey suffix uses the ternary `${isOpen ? 'open' : 'closed'}` but since GlobalCommentBottomSheet returns null when !isOpen and ReplyList only mounts when open, the ternary always yields 'open'; change the prop on ReplyList from reloadKey={`${replyReloadKey}-${isOpen ? 'open' : 'closed'}`} to a deterministic value (e.g., reloadKey={`${replyReloadKey}-open`} or simply reloadKey={replyReloadKey}) to remove the redundant isOpen check and clarify intent; update the line using reloadKey, referencing replyReloadKey, isOpen, GlobalCommentBottomSheet, and ReplyList.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsx`:
- Line 29: The ref declaration contentRef uses a non-idiomatic type; change the
generic from useRef<HTMLDivElement | null>(null) to useRef<HTMLDivElement>(null)
so the inferred type becomes RefObject<HTMLDivElement | null> per React 19
conventions; update the declaration named contentRef accordingly (search for
contentRef/useRef in GlobalCommentBottomSheet) and run typecheck to confirm no
other sites rely on the explicit | null annotation.
- Line 124: The reloadKey suffix uses the ternary `${isOpen ? 'open' :
'closed'}` but since GlobalCommentBottomSheet returns null when !isOpen and
ReplyList only mounts when open, the ternary always yields 'open'; change the
prop on ReplyList from reloadKey={`${replyReloadKey}-${isOpen ? 'open' :
'closed'}`} to a deterministic value (e.g., reloadKey={`${replyReloadKey}-open`}
or simply reloadKey={replyReloadKey}) to remove the redundant isOpen check and
clarify intent; update the line using reloadKey, referencing replyReloadKey,
isOpen, GlobalCommentBottomSheet, and ReplyList.
ℹ️ 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 (1)
src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsx
- 댓글 모달 margin-bottom 삭제 - 댓글 모달의 스크롤바 스타일을 데스크탑에서도 동일하게 적용
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/ReplyList.tsx`:
- Around line 67-88: The EmptyState is shown during initial infinite-mode
loading because the EmptyState render (when hasComments is false) doesn't
account for the initial loading flag; update ReplyList rendering logic to
suppress EmptyState while isInfiniteMode && commentFeed.isLoading && list.length
=== 0 (e.g., compute isInitialLoading = isInfiniteMode && commentFeed.isLoading
&& list.length === 0) and only render EmptyState when !isInitialLoading and
!hasComments, leaving the LoadingSpinner behavior unchanged; adjust the
conditional around EmptyState (and any hasComments check) accordingly so
EmptyState is not displayed during initial load.
ℹ️ 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 (4)
src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.styled.tssrc/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsxsrc/components/common/Post/ReplyList.styled.tssrc/components/common/Post/ReplyList.tsx
| <Container disableBottomMargin={disableBottomMargin}> | ||
| {isInfiniteMode && commentFeed.isLoading && list.length === 0 && ( | ||
| <LoadingSpinner size="small" fullHeight={false} /> | ||
| )} | ||
| {hasComments ? ( | ||
| commentList.map((comment, commentIndex) => ( | ||
| list.map((comment, commentIndex) => ( | ||
| <div className="comment-group" key={comment.commentId || `comment-${commentIndex}`}> | ||
| <Reply {...comment} onDelete={onReload} /> | ||
| <Reply {...comment} onDelete={handleReload} /> | ||
| {comment.replyList.map((sub, replyIndex) => ( | ||
| <SubReply | ||
| key={sub.commentId || `reply-${comment.commentId || commentIndex}-${replyIndex}`} | ||
| {...sub} | ||
| onDelete={onReload} | ||
| onDelete={handleReload} | ||
| /> | ||
| ))} | ||
| </div> | ||
| )) | ||
| ) : ( | ||
| <EmptyState> | ||
| <EmptyState disableBottomMargin={disableBottomMargin}> | ||
| <div className="title">아직 댓글이 없어요</div> | ||
| <div className="sub-title">첫번째 댓글을 남겨보세요</div> | ||
| </EmptyState> |
There was a problem hiding this comment.
초기 로딩 중 빈 상태 문구가 함께 노출됩니다.
무한 모드에서 Line 68 조건으로 스피너가 보일 때, Line 85의 EmptyState도 동시에 렌더링되어 잘못된 안내가 잠깐 노출될 수 있습니다. 초기 로딩 시에는 EmptyState를 숨기는 게 자연스럽습니다.
🛠️ 제안 수정
- ) : (
- <EmptyState disableBottomMargin={disableBottomMargin}>
+ ) : isInfiniteMode && commentFeed.isLoading ? null : (
+ <EmptyState disableBottomMargin={disableBottomMargin}>
<div className="title">아직 댓글이 없어요</div>
<div className="sub-title">첫번째 댓글을 남겨보세요</div>
</EmptyState>
)}📝 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.
| <Container disableBottomMargin={disableBottomMargin}> | |
| {isInfiniteMode && commentFeed.isLoading && list.length === 0 && ( | |
| <LoadingSpinner size="small" fullHeight={false} /> | |
| )} | |
| {hasComments ? ( | |
| commentList.map((comment, commentIndex) => ( | |
| list.map((comment, commentIndex) => ( | |
| <div className="comment-group" key={comment.commentId || `comment-${commentIndex}`}> | |
| <Reply {...comment} onDelete={onReload} /> | |
| <Reply {...comment} onDelete={handleReload} /> | |
| {comment.replyList.map((sub, replyIndex) => ( | |
| <SubReply | |
| key={sub.commentId || `reply-${comment.commentId || commentIndex}-${replyIndex}`} | |
| {...sub} | |
| onDelete={onReload} | |
| onDelete={handleReload} | |
| /> | |
| ))} | |
| </div> | |
| )) | |
| ) : ( | |
| <EmptyState> | |
| <EmptyState disableBottomMargin={disableBottomMargin}> | |
| <div className="title">아직 댓글이 없어요</div> | |
| <div className="sub-title">첫번째 댓글을 남겨보세요</div> | |
| </EmptyState> | |
| <Container disableBottomMargin={disableBottomMargin}> | |
| {isInfiniteMode && commentFeed.isLoading && list.length === 0 && ( | |
| <LoadingSpinner size="small" fullHeight={false} /> | |
| )} | |
| {hasComments ? ( | |
| list.map((comment, commentIndex) => ( | |
| <div className="comment-group" key={comment.commentId || `comment-${commentIndex}`}> | |
| <Reply {...comment} onDelete={handleReload} /> | |
| {comment.replyList.map((sub, replyIndex) => ( | |
| <SubReply | |
| key={sub.commentId || `reply-${comment.commentId || commentIndex}-${replyIndex}`} | |
| {...sub} | |
| onDelete={handleReload} | |
| /> | |
| ))} | |
| </div> | |
| )) | |
| ) : isInfiniteMode && commentFeed.isLoading ? null : ( | |
| <EmptyState disableBottomMargin={disableBottomMargin}> | |
| <div className="title">아직 댓글이 없어요</div> | |
| <div className="sub-title">첫번째 댓글을 남겨보세요</div> | |
| </EmptyState> | |
| )} | |
| </Container> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/common/Post/ReplyList.tsx` around lines 67 - 88, The
EmptyState is shown during initial infinite-mode loading because the EmptyState
render (when hasComments is false) doesn't account for the initial loading flag;
update ReplyList rendering logic to suppress EmptyState while isInfiniteMode &&
commentFeed.isLoading && list.length === 0 (e.g., compute isInitialLoading =
isInfiniteMode && commentFeed.isLoading && list.length === 0) and only render
EmptyState when !isInitialLoading and !hasComments, leaving the LoadingSpinner
behavior unchanged; adjust the conditional around EmptyState (and any
hasComments check) accordingly so EmptyState is not displayed during initial
load.
📝작업 내용
적용 파일
위키
위키
Summary by CodeRabbit
새로운 기능
개선 사항