Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (1)
Walkthrough채팅 UX 개선으로 채팅 검색/채팅방 추가/채팅방 정보 흐름을 변경했습니다. 라우트에 Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ 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.
Pull request overview
채팅 검색/이동 흐름, 채팅방 정보 화면, 채팅방 추가 UX, 공용 아바타 컴포넌트 추출, 타이머 랭킹 상호작용 개선을 통해 전반적인 채팅 UX 및 공통 인터랙션을 정리하는 PR입니다. (Issue #284)
Changes:
- 채팅 검색 결과에서 메시지 위치로 이동 후 복귀(backPath) 동작을 정리하고, 메시지/방 리스트 아이템을 공통 베이스로 리팩터링
- 채팅방 정보(/info) 화면을 추가하고, 채팅 헤더를 정보 화면과 연동되도록 개선(알림 토글 포함)
- 채팅방 추가 화면의 로딩/선택 UX를 개선하고, 초대 응답 타입을 명확한 유니온 타입으로 정리 + 공용 MemberAvatar 추출
- 타이머 랭킹 탭/정렬 전환 시 렌더링 반응성 개선(useDeferredValue)
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/pages/Timer/index.tsx | 랭킹 탭/정렬 전환 시 deferred 값 기반으로 UI 반응성/펜딩 표시 개선 |
| src/pages/Manager/ManagedSheetImportPreview/index.tsx | MemberAvatar 공용 컴포넌트로 import 경로 정리 |
| src/pages/Manager/ManagedMemberList/components/RoleManageModal.tsx | MemberAvatar 공용 컴포넌트로 import 경로 정리 |
| src/pages/Manager/ManagedMemberList/components/MemberCard.tsx | 로컬 MemberAvatar 제거 후 공용 MemberAvatar 사용 |
| src/pages/Chat/hooks/useChatRoomScroll.ts | targetMessageId 기반 하단 infinite scroll(이전/다음 페이지) 지원 추가 |
| src/pages/Chat/hooks/useChatMutations.ts | 메시지 전송 후 invalidate 키를 room-prefix(messagesByRoom)로 변경 |
| src/pages/Chat/hooks/useChat.ts | infinite query 이전/다음 페이지 노출 + 메시지 중복 제거 로직 추가 |
| src/pages/Chat/components/ChatRoomListItem.tsx | 방/메시지 검색 결과 아이템을 공통 베이스 컴포넌트로 통합, backPath state 전달 지원 |
| src/pages/Chat/ChatSearch.tsx | 검색어를 URL 쿼리에 반영 + placeholderData 기반 로딩/결과 UX 개선 |
| src/pages/Chat/ChatRoomInfo.tsx | 채팅방 정보 화면 신규 추가(멤버 목록/나가기/1:1 채팅 등) |
| src/pages/Chat/ChatRoom.tsx | targetMessageId 기반 이전/다음 페이지 스크롤 로딩 훅 연동 + bottomRef 추가 |
| src/pages/Chat/AddChatRoom.tsx | 초대 리스트 로딩/선택 UX 개선 + 그룹 생성 mutation 직접 사용 + 타입 변경 반영 |
| src/components/layout/Header/headerConfig.ts | /chats/:id 및 /chats/:id/info에서 chat 헤더 매칭되도록 정규식 확장 |
| src/components/layout/Header/components/ChatSearchHeader.tsx | (삭제) 별도 ChatSearchHeader 제거 |
| src/components/layout/Header/components/ChatHeader.tsx | /info 화면 분기(알림 토글/뒤로가기/정보 진입)로 동작 개선 |
| src/components/layout/Header/components/ChatAddHeader.tsx | 확인 버튼 disabled 상태 지원 |
| src/components/common/MemberAvatar.tsx | 공용 MemberAvatar 컴포넌트 신규 추가 |
| src/components/common/Dropdown.tsx | 옵션 렌더 순서/스타일 일부 정리 |
| src/apis/chat/queries.ts | 메시지 queryKey를 room-prefix + messageId suffix 구조로 재정의, prev/next pageParam 지원 |
| src/apis/chat/index.ts | 초대 API 타입을 InvitableFriends*로 정리된 엔티티로 변경 |
| src/apis/chat/entity.ts | 초대 응답을 grouped/flat 유니온 타입으로 명확화 + 네이밍 정리 |
| src/App.tsx | ChatRoomInfo 라우트 추가 및 chats/search, chats/add 라우트 위치 정리 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <div className="flex h-full flex-col items-center px-5 pt-6"> | ||
| <div className="flex w-full shrink-0 items-center overflow-hidden rounded-full bg-white px-5 py-2.5"> | ||
| <input | ||
| type="text" | ||
| value={keyword} | ||
| onChange={(e) => handleChange(e.target.value)} | ||
| className="h-full flex-1 bg-white px-3" | ||
| className="flex-1 text-[15px] leading-[1.6] text-indigo-300" | ||
| placeholder="채팅방명, 채팅으로 검색" | ||
| /> | ||
| <Search /> | ||
| </label> | ||
| <SearchIcon /> | ||
| </div> |
There was a problem hiding this comment.
The search input no longer has an associated label (it was previously wrapped in a <label>). For accessibility, add an explicit label (e.g., visually-hidden <label>) or at least an aria-label so screen readers can identify the control.
| <div className="flex w-full shrink-0 items-center overflow-hidden rounded-full bg-white px-5 py-2.5"> | ||
| <input | ||
| type="text" | ||
| value={keyword} | ||
| onChange={(e) => handleChange(e.target.value)} | ||
| className="h-full flex-1 bg-white px-3" | ||
| className="flex-1 text-[15px] leading-[1.6] text-indigo-300" | ||
| placeholder="이름, 학번 검색" | ||
| /> | ||
| <Search /> | ||
| </label> | ||
| <div className="mt-6 h-full w-87.5 overflow-y-auto rounded-t-2xl bg-white py-4"> | ||
| <div className="flex w-full px-5"> | ||
| <span className="text-#344352 flex-1 translate-y-2 text-[15px]">친구 선택({data?.currentCount})</span> | ||
| <SearchIcon /> | ||
| </div> |
There was a problem hiding this comment.
This input is missing an accessible name (no <label> and no aria-label). Please add a proper label (visually hidden if needed) or an aria-label so the friend search field is discoverable to assistive tech users.
| const handleBack = () => { | ||
| if (isInfoPage && chatRoomId) { | ||
| navigate(`/chats/${chatRoomId}`, { state }); | ||
| return; | ||
| } | ||
|
|
||
| <div className="flex min-w-0 items-center gap-1"> | ||
| <span className="truncate leading-5 font-bold text-indigo-700">{chatRoom?.roomName ?? ''}</span> | ||
| {isGroup && <span className="text-text-700 text-[13px] leading-5">{clubMembers.length}</span>} | ||
| </div> | ||
| </div> | ||
|
|
||
| <button type="button" aria-label="채팅방 정보 열기" onClick={openSidebar} className="shrink-0"> | ||
| <HamburgerIcon /> | ||
| </button> | ||
| </header> | ||
| smartBack(); | ||
| }; |
There was a problem hiding this comment.
handleBack uses navigate(/chats/${chatRoomId}, { state }), which pushes a new history entry when leaving the info page. This can lead to an unexpected back-stack (e.g., back from the chat room returns to the info page again). Use history back (navigate(-1) / smartBack()) or navigate with { replace: true } when returning from /info to avoid stacking duplicate entries.
| const allMessages = | ||
| chatMessagesData?.pages | ||
| .flatMap((page) => page.messages) | ||
| .filter( | ||
| (message, index, messages) => | ||
| index === messages.findIndex((candidate) => candidate.messageId === message.messageId) | ||
| ) ?? []; | ||
|
|
There was a problem hiding this comment.
The message de-duplication uses filter(... findIndex ...), which is O(n²) over the flattened message list and can become expensive as more pages load. Consider de-duping in a single pass using a Set (or Map) keyed by messageId to keep this linear-time.
| const allMessages = | |
| chatMessagesData?.pages | |
| .flatMap((page) => page.messages) | |
| .filter( | |
| (message, index, messages) => | |
| index === messages.findIndex((candidate) => candidate.messageId === message.messageId) | |
| ) ?? []; | |
| const allMessages = (() => { | |
| const messages = chatMessagesData?.pages.flatMap((page) => page.messages); | |
| if (!messages) { | |
| return []; | |
| } | |
| const seenMessageIds = new Set<number>(); | |
| return messages.filter((message) => { | |
| if (seenMessageIds.has(message.messageId)) { | |
| return false; | |
| } | |
| seenMessageIds.add(message.messageId); | |
| return true; | |
| }); | |
| })(); |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/common/Dropdown.tsx (1)
66-75:⚠️ Potential issue | 🟡 Minor선택 점 표시 색상도 토큰으로 바꿔주세요.
Line 74의
bg-[#69BFDF]는 하드코드 색상입니다. 동일하게 토큰 클래스로 맞추는 게 좋습니다.수정 예시
- {option.value === value ? <span aria-hidden="true" className="size-1 rounded-full bg-[`#69BFDF`]" /> : null} + {option.value === value ? <span aria-hidden="true" className="size-1 rounded-full bg-primary-500" /> : null}As per coding guidelines, "Prioritize color tokens from
src/styles/theme.css(e.g.,indigo-*,blue-*,background,primary) over hardcoded colors".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/Dropdown.tsx` around lines 66 - 75, The selection indicator currently uses a hardcoded color class bg-[`#69BFDF`] inside the Dropdown component's options.map button (the span rendered when option.value === value); replace that hardcoded color with a theme token class (e.g., one of the project's color tokens like blue-*/indigo-*/primary/background) so the selection dot uses the centralized token from src/styles/theme.css and matches other UI colors; update the span inside the options.map rendering in Dropdown.tsx to use the chosen token class instead of bg-[`#69BFDF`] and ensure styling remains a rounded-full size-1 indicator.
🧹 Nitpick comments (4)
src/pages/Timer/index.tsx (1)
34-35:useDeferredValue를 각각 분리하면 랭킹 쿼리 조합이 잠깐 꼬일 수 있어요.Line 34-35처럼
activeTab/sort를 따로 defer하면 빠르게 연속 변경할 때RankingList가 실제로 선택된 적 없는 조합(예: 이전 탭 + 새 정렬값)을 받을 수 있습니다. 지금은 Line 108-109의 query key가 그 값에 바로 연결돼 있어서 불필요한 fetch가 나가고, Line 42의 pending 상태도 그 중간 조합 기준으로 흔들릴 수 있습니다. 랭킹 파라미터를 하나로 묶어서 defer하는 쪽이 안전합니다.예시 수정안
-import { useDeferredValue, useRef, useState } from 'react'; +import { useDeferredValue, useMemo, useRef, useState } from 'react'; ... - const deferredActiveTab = useDeferredValue(activeTab); - const deferredSort = useDeferredValue(sort); + const rankingParams = useMemo( + () => ({ + type: TAB_TO_TYPE[activeTab], + sort, + }), + [activeTab, sort] + ); + const deferredRankingParams = useDeferredValue(rankingParams); ... - const isRankingPending = activeTab !== deferredActiveTab || sort !== deferredSort; + const isRankingPending = + rankingParams.type !== deferredRankingParams.type || rankingParams.sort !== deferredRankingParams.sort; ... - type={TAB_TO_TYPE[deferredActiveTab]} - sort={deferredSort} + type={deferredRankingParams.type} + sort={deferredRankingParams.sort}Also applies to: 42-42, 108-109
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Timer/index.tsx` around lines 34 - 35, Currently activeTab and sort are deferred separately (deferredActiveTab, deferredSort) which can produce transient, invalid combinations for RankingList and cause unnecessary fetches; instead, group the ranking params into a single object (e.g., rankingParams = { activeTab, sort }), call useDeferredValue once on that object (e.g., deferredRankingParams = useDeferredValue(rankingParams)), and then change all uses—the query key (currently tied to deferredActiveTab/deferredSort around the RankingList query at the code labeled with lines ~108-109) and the pending state check (the pending flag near line ~42)—to read from deferredRankingParams so the component and fetches always use a consistent, atomically-deferred parameter set.src/pages/Chat/hooks/useChat.ts (1)
36-42: 메시지 중복 제거 로직 - 성능 고려
findIndex를 사용한 중복 제거는 O(n²) 복잡도입니다. 현재 페이지네이션으로 n이 제한되어 있어 실질적 문제는 없지만, 메시지가 많아지면Map또는Set기반 dedup을 고려해볼 수 있습니다.♻️ 성능 개선 제안 (선택사항)
const allMessages = chatMessagesData?.pages .flatMap((page) => page.messages) - .filter( - (message, index, messages) => - index === messages.findIndex((candidate) => candidate.messageId === message.messageId) - ) ?? []; + .reduce<ChatMessage[]>((acc, message) => { + if (!acc.some((m) => m.messageId === message.messageId)) { + acc.push(message); + } + return acc; + }, []) ?? [];또는
Map사용:const allMessages = chatMessagesData?.pages ? [...new Map( chatMessagesData.pages.flatMap((page) => page.messages) .map((m) => [m.messageId, m]) ).values()] : [];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Chat/hooks/useChat.ts` around lines 36 - 42, The current dedup logic for allMessages in useChat.ts uses findIndex causing O(n²) behavior; replace it with a Map/Set-based dedup to get O(n) performance: iterate chatMessagesData?.pages.flatMap(page => page.messages) and insert each message into a Map keyed by message.messageId (or use a Set to track seen messageIds) to keep the first (or last) occurrence and then return Array.from(map.values()) (or filter by seen Set) as allMessages; reference the allMessages declaration, chatMessagesData.pages, page.messages, and messageId when making the change.src/pages/Chat/ChatSearch.tsx (1)
31-33:key속성 개선 권장
index를 key의 일부로 사용하면 메시지 순서가 바뀔 때 리렌더링 이슈가 발생할 수 있습니다. 서버에서 고유 식별자를 제공한다면 그것을 사용하는 것이 좋습니다.♻️ 개선 제안
{data?.messageMatches?.messages?.map((message, index) => ( <ChatMessageListItem - key={`${message.roomId}-${index}`} + key={`${message.roomId}-${message.messageId}`} message={message} keyword={keyword} navigationState={navigationState} /> ))}만약
message.messageId가 없다면 현재 방식도 괜찮습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Chat/ChatSearch.tsx` around lines 31 - 33, The current key on ChatMessageListItem uses `${message.roomId}-${index}` which can cause unstable keys when message order changes; update the key in src/pages/Chat/ChatSearch.tsx to use the message's unique identifier (e.g. message.messageId or message.id) instead of the array index, e.g. use `message.messageId ?? `${message.roomId}-${index}`` as the key when rendering data?.messageMatches?.messages in the ChatMessageListItem map so you prefer a stable server-provided id and fall back to the previous value only if no unique id exists.src/pages/Chat/ChatRoomInfo.tsx (1)
77-77: 하드코딩된 색상#ff4e4e대신 테마 토큰 사용 권장
src/styles/theme.css의 색상 토큰 사용을 권장합니다. 빨간색 관련 토큰이 없다면 추가하거나 기존 토큰 활용을 검토하세요.Also applies to: 214-214
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Chat/ChatRoomInfo.tsx` at line 77, The element in ChatRoomInfo (the JSX with className containing bg-[`#ff4e4e`]) uses a hardcoded red color; replace that hardcoded color with the appropriate theme token from src/styles/theme.css (e.g., bg-[var(--color-danger)] or the project's token naming) and, if no suitable red/danger token exists, add one to theme.css and use it here; apply the same replacement for the other occurrence of bg-[`#ff4e4e`] in this file so both uses reference the theme token instead of the hex literal.
🤖 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/pages/Chat/AddChatRoom.tsx`:
- Around line 29-35: The button for each member is only as wide as its content
and lacks an accessible toggled state; update the button element (the one using
onClick={() => onToggle(userId)}) to span the full width (e.g., add a
container/full-width class to the button so empty space is clickable) and add
aria-pressed={isSelected} so the toggle state is exposed to assistive tech; keep
the existing onToggle(userId), MemberAvatar, name/studentNumber span and
conditional CheckIcon usage but ensure the button has the full-width class and
the aria-pressed attribute tied to isSelected.
- Around line 173-180: The input's class currently uses text-indigo-300 which
applies the same faded color to both the typed text and the placeholder; update
the input element in AddChatRoom.tsx (the <input ... value={keyword}
onChange={(e) => handleChange(e.target.value)} ... />) to use a stronger color
for actual text (e.g., text-indigo-900 or text-gray-900) and a lighter color
specifically for the placeholder using Tailwind's placeholder: utility (e.g.,
placeholder:text-indigo-300); modify the className to remove text-indigo-300 and
add both the main text color and placeholder:text-indigo-300 so typed text
appears darker while the placeholder remains muted.
- Line 2: 해당 컴포넌트에서 useQuery + keepPreviousData 패턴을 useSuspenseQuery 패턴으로 바꿔주세요:
AddChatRoom 컴포넌트에서 현재 사용 중인 useQuery와 keepPreviousData를 제거하고 대신
'@tanstack/react-query'의 useSuspenseQuery를 import/사용하도록 변경하며, 쿼리 옵션에서
keepPreviousData/placeholderData를 제거(간혹 suspense: true가 필요함)하고 데이터 전환 시에는 이미
import된 startTransition을 사용해 상태 업데이트(예: 채팅 룸 선택/파라미터 변경)를 startTransition 블록 안에서
수행해 이전 UI를 유지하도록 처리하세요; 또한 suspense 경계(Suspense/ReactQuerySuspense boundary)가
존재하는지 확인해 주세요.
In `@src/pages/Chat/ChatRoomInfo.tsx`:
- Line 100: The code converts chatRoomId to a number with const numericRoomId =
Number(chatRoomId) which yields NaN when chatRoomId is undefined; update
ChatRoomInfo to validate chatRoomId (e.g., check typeof chatRoomId !== 'string'
or !chatRoomId) before conversion, and handle invalid cases with an early return
or redirect (render a fallback UI or call navigation/redirect) rather than
proceeding with numericRoomId; use the validated/parsed value only when
Number(chatRoomId) is finite (Number.isFinite) and reference numericRoomId and
chatRoomId in the guard so the component never operates on NaN.
In `@src/pages/Chat/components/ChatRoomListItem.tsx`:
- Around line 122-139: In ChatMessageListItem, guard against an empty keyword
before calling message.matchedMessage.split(keyword): if keyword is falsy or
empty, set parts to [message.matchedMessage] (or render matchedMessage directly)
so you don't call split('') and won't bold every character; update the parts
computation and the preview mapping (used in ChatListItemBase preview prop) to
handle this branch so only intended keyword occurrences are highlighted.
---
Outside diff comments:
In `@src/components/common/Dropdown.tsx`:
- Around line 66-75: The selection indicator currently uses a hardcoded color
class bg-[`#69BFDF`] inside the Dropdown component's options.map button (the span
rendered when option.value === value); replace that hardcoded color with a theme
token class (e.g., one of the project's color tokens like
blue-*/indigo-*/primary/background) so the selection dot uses the centralized
token from src/styles/theme.css and matches other UI colors; update the span
inside the options.map rendering in Dropdown.tsx to use the chosen token class
instead of bg-[`#69BFDF`] and ensure styling remains a rounded-full size-1
indicator.
---
Nitpick comments:
In `@src/pages/Chat/ChatRoomInfo.tsx`:
- Line 77: The element in ChatRoomInfo (the JSX with className containing
bg-[`#ff4e4e`]) uses a hardcoded red color; replace that hardcoded color with the
appropriate theme token from src/styles/theme.css (e.g.,
bg-[var(--color-danger)] or the project's token naming) and, if no suitable
red/danger token exists, add one to theme.css and use it here; apply the same
replacement for the other occurrence of bg-[`#ff4e4e`] in this file so both uses
reference the theme token instead of the hex literal.
In `@src/pages/Chat/ChatSearch.tsx`:
- Around line 31-33: The current key on ChatMessageListItem uses
`${message.roomId}-${index}` which can cause unstable keys when message order
changes; update the key in src/pages/Chat/ChatSearch.tsx to use the message's
unique identifier (e.g. message.messageId or message.id) instead of the array
index, e.g. use `message.messageId ?? `${message.roomId}-${index}`` as the key
when rendering data?.messageMatches?.messages in the ChatMessageListItem map so
you prefer a stable server-provided id and fall back to the previous value only
if no unique id exists.
In `@src/pages/Chat/hooks/useChat.ts`:
- Around line 36-42: The current dedup logic for allMessages in useChat.ts uses
findIndex causing O(n²) behavior; replace it with a Map/Set-based dedup to get
O(n) performance: iterate chatMessagesData?.pages.flatMap(page => page.messages)
and insert each message into a Map keyed by message.messageId (or use a Set to
track seen messageIds) to keep the first (or last) occurrence and then return
Array.from(map.values()) (or filter by seen Set) as allMessages; reference the
allMessages declaration, chatMessagesData.pages, page.messages, and messageId
when making the change.
In `@src/pages/Timer/index.tsx`:
- Around line 34-35: Currently activeTab and sort are deferred separately
(deferredActiveTab, deferredSort) which can produce transient, invalid
combinations for RankingList and cause unnecessary fetches; instead, group the
ranking params into a single object (e.g., rankingParams = { activeTab, sort }),
call useDeferredValue once on that object (e.g., deferredRankingParams =
useDeferredValue(rankingParams)), and then change all uses—the query key
(currently tied to deferredActiveTab/deferredSort around the RankingList query
at the code labeled with lines ~108-109) and the pending state check (the
pending flag near line ~42)—to read from deferredRankingParams so the component
and fetches always use a consistent, atomically-deferred parameter set.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 03f85ac3-87e4-46ae-818f-1afb55d51eb7
📒 Files selected for processing (22)
src/App.tsxsrc/apis/chat/entity.tssrc/apis/chat/index.tssrc/apis/chat/queries.tssrc/components/common/Dropdown.tsxsrc/components/common/MemberAvatar.tsxsrc/components/layout/Header/components/ChatAddHeader.tsxsrc/components/layout/Header/components/ChatHeader.tsxsrc/components/layout/Header/components/ChatSearchHeader.tsxsrc/components/layout/Header/headerConfig.tssrc/pages/Chat/AddChatRoom.tsxsrc/pages/Chat/ChatRoom.tsxsrc/pages/Chat/ChatRoomInfo.tsxsrc/pages/Chat/ChatSearch.tsxsrc/pages/Chat/components/ChatRoomListItem.tsxsrc/pages/Chat/hooks/useChat.tssrc/pages/Chat/hooks/useChatMutations.tssrc/pages/Chat/hooks/useChatRoomScroll.tssrc/pages/Manager/ManagedMemberList/components/MemberCard.tsxsrc/pages/Manager/ManagedMemberList/components/RoleManageModal.tsxsrc/pages/Manager/ManagedSheetImportPreview/index.tsxsrc/pages/Timer/index.tsx
💤 Files with no reviewable changes (1)
- src/components/layout/Header/components/ChatSearchHeader.tsx
| import { Fragment, useState } from 'react'; | ||
| import { useSuspenseQuery } from '@tanstack/react-query'; | ||
| import { startTransition, useState } from 'react'; | ||
| import { keepPreviousData, useQuery } from '@tanstack/react-query'; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
TanStack Query의 useSuspenseQuery가 placeholderData 또는 keep previous data 패턴을 현재 버전에서 지원하는지 확인해주세요.
💡 Result:
TanStack Query의 useSuspenseQuery는 현재 버전(v5.99.0 기준)에서 placeholderData와 keep previous data 패턴(placeholderData: keepPreviousData)을 지원하지 않습니다. 공식 문서에 따르면 useSuspenseQuery의 옵션은 useQuery와 동일하지만 placeholderData, enabled, throwOnError를 제외합니다. 이는 Suspense와의 호환성 때문으로, placeholderData를 사용하면 pending 상태를 건너뛰어 Suspense가 제대로 작동하지 않기 때문입니다. 대신 React의 useTransition이나 useDeferredValue를 사용하여 이전 데이터를 유지하면서 fetching 상태를 처리하세요.
Citations:
- 1: https://tanstack.com/query/v5/docs/react/reference/useSuspenseQuery
- 2: https://tanstack.dev/query/latest/docs/framework/react/reference/useSuspenseQuery
- 3: https://tanstack.dev/query/v5/docs/framework/react/reference/useSuspenseQuery
- 4: I found useSuspenseQuery placeholderData have been removed in v5.24.2,how to use it without breaking the UI? TanStack/query#7013
- 5: Suspense Query(ies) would benefit from keepPreviousData over startTransition TanStack/query#7661
- 6: Unexpected useSuspenseQuery behavior with placeholderData TanStack/query#6950
🏁 Script executed:
# Check the actual code in AddChatRoom.tsx to understand context
cat -n src/pages/Chat/AddChatRoom.tsx | head -100Repository: BCSDLab/KONECT_FRONT_END
Length of output: 4781
🏁 Script executed:
# Search for useTransition usage in the codebase to see if it's a known pattern
rg "useTransition" src/ -A 2 -B 2Repository: BCSDLab/KONECT_FRONT_END
Length of output: 50
🏁 Script executed:
# Check if there are other pages that use keepPreviousData with useSuspenseQuery attempts
rg "keepPreviousData|useSuspenseQuery" src/pages/ -B 2 -A 2Repository: BCSDLab/KONECT_FRONT_END
Length of output: 25801
useSuspenseQuery 패턴을 따르세요.
이 페이지에서만 useQuery + keepPreviousData를 사용하고 있어 서버 상태 관리 방식이 레포 전체 관례와 맞지 않습니다. 이미 startTransition을 import했으니 useSuspenseQuery로 변경하고 트랜지션 처리로 이전 데이터 유지를 구현하세요. (TanStack Query v5+에서 useSuspenseQuery는 placeholderData 미지원)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Chat/AddChatRoom.tsx` at line 2, 해당 컴포넌트에서 useQuery +
keepPreviousData 패턴을 useSuspenseQuery 패턴으로 바꿔주세요: AddChatRoom 컴포넌트에서 현재 사용 중인
useQuery와 keepPreviousData를 제거하고 대신 '@tanstack/react-query'의 useSuspenseQuery를
import/사용하도록 변경하며, 쿼리 옵션에서 keepPreviousData/placeholderData를 제거(간혹 suspense:
true가 필요함)하고 데이터 전환 시에는 이미 import된 startTransition을 사용해 상태 업데이트(예: 채팅 룸 선택/파라미터
변경)를 startTransition 블록 안에서 수행해 이전 UI를 유지하도록 처리하세요; 또한 suspense
경계(Suspense/ReactQuerySuspense boundary)가 존재하는지 확인해 주세요.
| <button type="button" className="flex items-center gap-3 text-left" onClick={() => onToggle(userId)}> | ||
| <MemberAvatar name={name} /> | ||
| <span className="flex w-full items-center text-[15px] leading-[1.6] font-semibold text-indigo-700"> | ||
| {name} ({studentNumber}) | ||
| </span> | ||
| {isSelected && <CheckIcon className="size-6.5" />} | ||
| </button> |
There was a problem hiding this comment.
선택 행 버튼이 너무 좁고 상태도 노출되지 않습니다.
지금은 버튼이 콘텐츠 너비만큼만 클릭 가능해서 모바일에서 빈 영역 탭이 먹지 않습니다. 토글형 액션이라 aria-pressed도 같이 주는 편이 안전합니다.
제안 코드
- <button type="button" className="flex items-center gap-3 text-left" onClick={() => onToggle(userId)}>
+ <button
+ type="button"
+ className="flex w-full items-center gap-3 text-left"
+ onClick={() => onToggle(userId)}
+ aria-pressed={isSelected}
+ >
<MemberAvatar name={name} />
- <span className="flex w-full items-center text-[15px] leading-[1.6] font-semibold text-indigo-700">
+ <span className="min-w-0 flex-1 truncate text-[15px] leading-[1.6] font-semibold text-indigo-700">
{name} ({studentNumber})
</span>
{isSelected && <CheckIcon className="size-6.5" />}
</button>📝 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.
| <button type="button" className="flex items-center gap-3 text-left" onClick={() => onToggle(userId)}> | |
| <MemberAvatar name={name} /> | |
| <span className="flex w-full items-center text-[15px] leading-[1.6] font-semibold text-indigo-700"> | |
| {name} ({studentNumber}) | |
| </span> | |
| {isSelected && <CheckIcon className="size-6.5" />} | |
| </button> | |
| <button | |
| type="button" | |
| className="flex w-full items-center gap-3 text-left" | |
| onClick={() => onToggle(userId)} | |
| aria-pressed={isSelected} | |
| > | |
| <MemberAvatar name={name} /> | |
| <span className="min-w-0 flex-1 truncate text-[15px] leading-[1.6] font-semibold text-indigo-700"> | |
| {name} ({studentNumber}) | |
| </span> | |
| {isSelected && <CheckIcon className="size-6.5" />} | |
| </button> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Chat/AddChatRoom.tsx` around lines 29 - 35, The button for each
member is only as wide as its content and lacks an accessible toggled state;
update the button element (the one using onClick={() => onToggle(userId)}) to
span the full width (e.g., add a container/full-width class to the button so
empty space is clickable) and add aria-pressed={isSelected} so the toggle state
is exposed to assistive tech; keep the existing onToggle(userId), MemberAvatar,
name/studentNumber span and conditional CheckIcon usage but ensure the button
has the full-width class and the aria-pressed attribute tied to isSelected.
| <div className="flex w-full shrink-0 items-center overflow-hidden rounded-full bg-white px-5 py-2.5"> | ||
| <input | ||
| type="text" | ||
| value={keyword} | ||
| onChange={(e) => handleChange(e.target.value)} | ||
| className="h-full flex-1 bg-white px-3" | ||
| className="flex-1 text-[15px] leading-[1.6] text-indigo-300" | ||
| placeholder="이름, 학번 검색" | ||
| /> |
There was a problem hiding this comment.
입력값 색상과 placeholder 색상을 분리해주세요.
text-indigo-300가 실제 입력 텍스트에도 적용돼서, 사용자가 타이핑해도 placeholder처럼 보입니다. 본문 색상으로 두고 placeholder만 옅게 처리하는 쪽이 더 자연스럽습니다.
제안 코드
- className="flex-1 text-[15px] leading-[1.6] text-indigo-300"
+ className="text-text-700 placeholder:text-indigo-300 flex-1 text-[15px] leading-[1.6]"📝 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.
| <div className="flex w-full shrink-0 items-center overflow-hidden rounded-full bg-white px-5 py-2.5"> | |
| <input | |
| type="text" | |
| value={keyword} | |
| onChange={(e) => handleChange(e.target.value)} | |
| className="h-full flex-1 bg-white px-3" | |
| className="flex-1 text-[15px] leading-[1.6] text-indigo-300" | |
| placeholder="이름, 학번 검색" | |
| /> | |
| <input | |
| type="text" | |
| value={keyword} | |
| onChange={(e) => handleChange(e.target.value)} | |
| className="text-text-700 placeholder:text-indigo-300 flex-1 text-[15px] leading-[1.6]" | |
| placeholder="이름, 학번 검색" | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Chat/AddChatRoom.tsx` around lines 173 - 180, The input's class
currently uses text-indigo-300 which applies the same faded color to both the
typed text and the placeholder; update the input element in AddChatRoom.tsx (the
<input ... value={keyword} onChange={(e) => handleChange(e.target.value)} ...
/>) to use a stronger color for actual text (e.g., text-indigo-900 or
text-gray-900) and a lighter color specifically for the placeholder using
Tailwind's placeholder: utility (e.g., placeholder:text-indigo-300); modify the
className to remove text-indigo-300 and add both the main text color and
placeholder:text-indigo-300 so typed text appears darker while the placeholder
remains muted.
| function ChatRoomInfo() { | ||
| const navigate = useNavigate(); | ||
| const { chatRoomId } = useParams(); | ||
| const numericRoomId = Number(chatRoomId); |
There was a problem hiding this comment.
chatRoomId undefined 시 NaN 처리 필요
Number(undefined)는 NaN을 반환합니다. 유효하지 않은 경우 리다이렉트 또는 early return 고려하세요.
🛡️ 제안
const { chatRoomId } = useParams();
const numericRoomId = Number(chatRoomId);
+
+ if (!chatRoomId || Number.isNaN(numericRoomId)) {
+ return <Navigate to="/chats" replace />;
+ }📝 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.
| const numericRoomId = Number(chatRoomId); | |
| const { chatRoomId } = useParams(); | |
| const numericRoomId = Number(chatRoomId); | |
| if (!chatRoomId || Number.isNaN(numericRoomId)) { | |
| return <Navigate to="/chats" replace />; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Chat/ChatRoomInfo.tsx` at line 100, The code converts chatRoomId to
a number with const numericRoomId = Number(chatRoomId) which yields NaN when
chatRoomId is undefined; update ChatRoomInfo to validate chatRoomId (e.g., check
typeof chatRoomId !== 'string' or !chatRoomId) before conversion, and handle
invalid cases with an early return or redirect (render a fallback UI or call
navigation/redirect) rather than proceeding with numericRoomId; use the
validated/parsed value only when Number(chatRoomId) is finite (Number.isFinite)
and reference numericRoomId and chatRoomId in the guard so the component never
operates on NaN.
| export function ChatMessageListItem({ message, keyword, navigationState }: ChatMessageListItemProps) { | ||
| const parts = message.matchedMessage.split(keyword); | ||
|
|
||
| return ( | ||
| <ChatListItemBase | ||
| to={`/chats/${message.roomId}?messageId=${message.matchedMessageId}`} | ||
| roomImageUrl={message.roomImageUrl} | ||
| roomName={message.roomName} | ||
| sentAt={message.matchedMessageSentAt} | ||
| navigationState={navigationState} | ||
| preview={parts.map((part, index) => ( | ||
| <Fragment key={index}> | ||
| {part} | ||
| {index < parts.length - 1 && <span className="font-bold">{keyword}</span>} | ||
| </Fragment> | ||
| ))} | ||
| /> | ||
| ); |
There was a problem hiding this comment.
빈 keyword 엣지케이스 처리 필요
split('')은 모든 문자를 분리합니다. keyword가 빈 문자열일 경우를 방어하세요.
🛡️ 제안
export function ChatMessageListItem({ message, keyword, navigationState }: ChatMessageListItemProps) {
- const parts = message.matchedMessage.split(keyword);
+ const parts = keyword ? message.matchedMessage.split(keyword) : [message.matchedMessage];
return (📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function ChatMessageListItem({ message, keyword, navigationState }: ChatMessageListItemProps) { | |
| const parts = message.matchedMessage.split(keyword); | |
| return ( | |
| <ChatListItemBase | |
| to={`/chats/${message.roomId}?messageId=${message.matchedMessageId}`} | |
| roomImageUrl={message.roomImageUrl} | |
| roomName={message.roomName} | |
| sentAt={message.matchedMessageSentAt} | |
| navigationState={navigationState} | |
| preview={parts.map((part, index) => ( | |
| <Fragment key={index}> | |
| {part} | |
| {index < parts.length - 1 && <span className="font-bold">{keyword}</span>} | |
| </Fragment> | |
| ))} | |
| /> | |
| ); | |
| export function ChatMessageListItem({ message, keyword, navigationState }: ChatMessageListItemProps) { | |
| const parts = keyword ? message.matchedMessage.split(keyword) : [message.matchedMessage]; | |
| return ( | |
| <ChatListItemBase | |
| to={`/chats/${message.roomId}?messageId=${message.matchedMessageId}`} | |
| roomImageUrl={message.roomImageUrl} | |
| roomName={message.roomName} | |
| sentAt={message.matchedMessageSentAt} | |
| navigationState={navigationState} | |
| preview={parts.map((part, index) => ( | |
| <Fragment key={index}> | |
| {part} | |
| {index < parts.length - 1 && <span className="font-bold">{keyword}</span>} | |
| </Fragment> | |
| ))} | |
| /> | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Chat/components/ChatRoomListItem.tsx` around lines 122 - 139, In
ChatMessageListItem, guard against an empty keyword before calling
message.matchedMessage.split(keyword): if keyword is falsy or empty, set parts
to [message.matchedMessage] (or render matchedMessage directly) so you don't
call split('') and won't bold every character; update the parts computation and
the preview mapping (used in ChatListItemBase preview prop) to handle this
branch so only intended keyword occurrences are highlighted.
✨ 요약
😎 해결한 이슈
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선사항