Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughRoom.chatType에 'CLUB_GROUP'을 추가해 타입을 확장했고, BottomSheet와 useBottomSheet에 제어용 position 지원 및 half/full 높이 계산과 리사이저 분기 로직이 도입되었습니다. 다수 컴포넌트에서 클래스 합성 유틸을 cn으로 통일하고 UI/마크업·스타일을 조정했으며, 채팅 그룹 판별 로직을 isDirectChatType/isGroupChatType 유틸로 중앙화했습니다. 타이머 관련으로 useTimerLayout 훅과 RankingList 컴포넌트가 추가되고 기존 useTimer 훅은 제거되었으며 TimerButton에 size prop이 추가되었습니다. 색상 토큰은 colors.css로 이동하고 theme.css에서는 관련 변수들이 제거되었습니다. Possibly related PRs
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 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
채팅/타이머 화면의 UI/레이아웃을 Figma 기준으로 재정리하고, 채팅 타입 확장 및 전역 아이콘 스타일을 단색 톤으로 통일하는 PR입니다.
Changes:
- 채팅
chatType에CLUB_GROUP를 추가하고 채팅방/메시지 UI 표시 로직을 일부 정리 - 타이머 페이지 레이아웃 재구성(뷰포트/바텀 인셋 기반 반응형 크기 계산, BottomSheet 동작/스타일 조정)
- 전역 알림 SVG 아이콘의 그라디언트 제거(단색 적용), 공용 Dropdown/BottomSheet 스타일 개선
Reviewed changes
Copilot reviewed 9 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/hooks/useBottomSheet.ts | 바텀시트 드래그 처리 로직에서 preventDefault 제거 |
| src/components/common/BottomSheet.tsx | bottomOffset/halfHeight 지원 확장 및 바텀시트 레이아웃/드래그 스타일 조정 |
| src/components/common/Dropdown.tsx | Dropdown UI 스타일 개편 및 옵션 표시 순서 변경 |
| src/pages/Timer/index.tsx | 타이머 페이지 레이아웃 재구성 및 뷰포트/바텀 인셋 기반 반응형 계산 도입 |
| src/pages/Timer/components/TimerButton.tsx | 타이머 버튼을 size 기반으로 렌더링하도록 변경(폰트 크기 포함) |
| src/pages/Timer/components/RankingItem.tsx | 랭킹 아이템 스타일/텍스트 구성 변경 및 내 랭킹 고정 노출 조건 변경 |
| src/pages/Chat/index.tsx | 채팅방 목록에서 그룹 배지 제거 |
| src/pages/Chat/ChatRoom.tsx | 메시지 행 레이아웃 조정 및 발신자명 표시 로직 변경 |
| src/apis/chat/entity.ts | Room.chatType에 CLUB_GROUP 타입 추가 |
| src/assets/svg/notifications.svg | 알림 아이콘 단색화(그라디언트 제거) |
| src/assets/svg/unread-notification.svg | 읽지 않음 알림 아이콘 단색화(그라디언트 제거) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const matchedInset = bottomOverlayInset.match(/(\d+(?:\.\d+)?)px/); | ||
|
|
||
| return matchedInset ? Number.parseFloat(matchedInset[1]) : 80; |
There was a problem hiding this comment.
bottomOverlayInset can be values like var(--sab) or calc(80px + var(--sab)) (see src/components/layout/bottomOverlay.ts). getBottomInsetPx() currently only parses a ...px literal and otherwise falls back to 80, which will miscalculate halfSheetHeight/timerSize on devices with safe-area insets or when the bottom nav is present but not measurable yet. Consider resolving the CSS length to an actual pixel number (e.g., via getComputedStyle on a temporary element or by reading --sab/--effective-bottom-safe-area and handling calc(...)), instead of using a fixed fallback.
| const matchedInset = bottomOverlayInset.match(/(\d+(?:\.\d+)?)px/); | |
| return matchedInset ? Number.parseFloat(matchedInset[1]) : 80; | |
| // Fast path: plain "123px" values | |
| const matchedInset = bottomOverlayInset.match(/(\d+(?:\.\d+)?)px/); | |
| if (matchedInset) { | |
| return Number.parseFloat(matchedInset[1]); | |
| } | |
| // Fallback: resolve complex CSS lengths (e.g. var(...), calc(...)) via the DOM | |
| if (typeof document !== 'undefined' && document.body) { | |
| try { | |
| const el = document.createElement('div'); | |
| el.style.position = 'absolute'; | |
| el.style.visibility = 'hidden'; | |
| el.style.height = bottomOverlayInset; | |
| el.style.boxSizing = 'border-box'; | |
| document.body.appendChild(el); | |
| const rect = el.getBoundingClientRect(); | |
| document.body.removeChild(el); | |
| if (Number.isFinite(rect.height) && rect.height > 0) { | |
| return rect.height; | |
| } | |
| } catch { | |
| // Ignore and fall through to default | |
| } | |
| } | |
| // Reasonable default when resolution fails | |
| return 80; |
| const getTransform = () => { | ||
| if (resizable) { | ||
| if (normalizedHalfHeight) { | ||
| return effectivePosition === 'half' | ||
| ? `translateY(${Math.min(0, currentTranslate)}px)` | ||
| : `translateY(${Math.max(0, currentTranslate)}px)`; | ||
| } |
There was a problem hiding this comment.
When halfHeight is provided, getTransform() translates the entire sheet up/down during drag. Translating the whole element upward from the bottom will temporarily detach the sheet from the bottom edge (showing a gap under it) and no longer gives the visual effect of the sheet “expanding” from the bottom. Consider keeping the sheet anchored to the bottom and applying drag feedback by adjusting the sheet’s height (clamped) while dragging, rather than translating the whole sheet.
| function ChatMessageRow({ isGroup, isSameSender, message }: ChatMessageRowProps) { | ||
| const showSenderName = isGroup && !message.isMine && !isSameSender; | ||
| function ChatMessageRow({ isSameSender, message }: ChatMessageRowProps) { | ||
| const showSenderName = !message.isMine && !isSameSender; |
There was a problem hiding this comment.
showSenderName is now shown for all non-mine messages when the sender changes, including DIRECT rooms. Previously the sender name was only shown in group chats, which avoids redundant names in 1:1 chats. If the intent is still “show sender name only for group-like rooms”, reintroduce a room-type check (e.g., derive current room from chatRoomList and treat GROUP/CLUB_GROUP as group types) and gate showSenderName behind that.
| const showSenderName = !message.isMine && !isSameSender; | |
| const isGroupRoom = message.chatRoomType === 'GROUP' || message.chatRoomType === 'CLUB_GROUP'; | |
| const showSenderName = isGroupRoom && !message.isMine && !isSameSender; |
| /** 초기 위치 (기본값: 'half') */ | ||
| defaultPosition?: SheetPosition; | ||
| /** 하단 오프셋 (px) */ | ||
| bottomOffset?: number; | ||
| bottomOffset?: number | string; | ||
| /** half 상태일 때 상단 오프셋 (px) */ | ||
| halfTopOffset?: number; | ||
| /** half 상태일 때 실제 보이는 높이 */ | ||
| halfHeight?: number | string; | ||
| /** full 상태일 때 상단 오프셋 (px) */ |
There was a problem hiding this comment.
The JSDoc for bottomOffset still says "(px)", but the prop now accepts number | string (e.g. var(...)/calc(...)). Updating the comment to reflect the accepted formats will prevent misuse/confusion for future callers.
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/BottomSheet.tsx (1)
46-58:⚠️ Potential issue | 🟠 Major
halfHeight가 기본 half 경로에서 바로 깨집니다.
resizable의 기본값이false인데, Line 78은 half 높이를halfHeight로 고정하고도 Line 58은 여전히translateY(55%)를 적용합니다. 그래서defaultPosition="half"로 쓰면 시트가 아래로 밀려 실제 노출 높이가halfHeight보다 작아집니다.수정 예시
const getTransform = () => { if (resizable) { if (normalizedHalfHeight) { return effectivePosition === 'half' ? `translateY(${Math.min(0, currentTranslate)}px)` : `translateY(${Math.max(0, currentTranslate)}px)`; } return effectivePosition === 'half' ? `translateY(calc(55% + ${currentTranslate}px))` : `translateY(${Math.max(0, currentTranslate)}px)`; } + if (normalizedHalfHeight && effectivePosition === 'half') { + return 'translateY(0)'; + } return effectivePosition === 'half' ? 'translateY(55%)' : 'translateY(0)'; };Also applies to: 73-79
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/BottomSheet.tsx` around lines 46 - 58, getTransform currently returns a hardcoded 'translateY(55%)' when not resizable and effectivePosition === 'half', which breaks the intended halfHeight behavior; update the non-resizable branch in getTransform to compute the half translate using the actual half height (prefer normalizedHalfHeight then fall back to halfHeight) e.g. return effectivePosition === 'half' ? `translateY(calc(100% - ${normalizedHalfHeight || halfHeight}px))` : 'translateY(0)'; and apply the same fix to the similar branch referenced around lines 73-79 so defaultPosition="half" uses the real half height instead of 55%.
🧹 Nitpick comments (2)
src/pages/Chat/ChatRoom.tsx (1)
85-87:chatRoomId숫자 파싱을 1회로 고정해 안정성을 높여주세요.
Number(chatRoomId)직접 전달 대신, 상수로 한 번 파싱하고 NaN을undefined로 정규화하면 훅 입력이 더 안전해집니다.♻️ 제안 수정
function ChatRoom() { const { chatRoomId } = useParams(); + const numericRoomId = Number(chatRoomId); + const safeRoomId = Number.isNaN(numericRoomId) ? undefined : numericRoomId; + const { sendMessage, chatMessages, fetchNextPage, hasNextPage, isFetchingNextPage, isSendingMessage } = useChat( - Number(chatRoomId) + safeRoomId );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Chat/ChatRoom.tsx` around lines 85 - 87, The code passes Number(chatRoomId) inline to useChat which can produce inconsistent parsing and NaN; parse chatRoomId once into a constant (e.g., const parsedChatRoomId = Number(chatRoomId);) normalize NaN to undefined (e.g., parsedChatRoomId = Number.isNaN(parsedChatRoomId) ? undefined : parsedChatRoomId) and then call useChat(parsedChatRoomId). Update references around useChat(...) in ChatRoom.tsx so sendMessage, chatMessages, fetchNextPage, hasNextPage, isFetchingNextPage and isSendingMessage come from useChat(parsedChatRoomId).src/components/common/BottomSheet.tsx (1)
68-72: 공용 컴포넌트에서는clsx보다cn()이 안전합니다.
classNameoverride를 받는 컴포넌트라clsx만 쓰면rounded-*,shadow-*같은 Tailwind 충돌이 정리되지 않습니다. 이번 구간도cn()으로 맞추는 편이 안전합니다.As per coding guidelines "Use
cn()utility fromsrc/utils/ts/cn.tsto merge Tailwind CSS classes".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/BottomSheet.tsx` around lines 68 - 72, The BottomSheet component is using clsx to build its className which can leave Tailwind class conflicts unresolved; replace the clsx usage with the project's cn() utility and update the import to use cn (from src/utils/ts/cn.ts) instead of clsx, ensuring the same conditional arguments (resizable && isDragging, className) are passed to cn so Tailwind merging/override behavior works correctly.
🤖 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/apis/chat/entity.ts`:
- Line 5: The chatType union was extended to include 'CLUB_GROUP' but the
group-detection logic in ChatHeader
(src/components/layout/Header/components/ChatHeader.tsx) still only checks for
'GROUP', causing CLUB_GROUP rooms to be treated as non-group and omit group
header info; update the condition(s) that compute isGroup (or similar
variables/expressions) to treat both 'GROUP' and 'CLUB_GROUP' as group chats (or
introduce/replace with a reusable helper like isGroupChat(chatType) that returns
true for 'GROUP' and 'CLUB_GROUP') and then use that helper/updated condition
wherever header/group-specific rendering depends on it.
In `@src/components/common/Dropdown.tsx`:
- Around line 44-79: The dropdown trigger and items lack ARIA roles/attributes;
update the trigger button (where setIsOpen and isOpen are used) to include
aria-expanded={isOpen}, aria-haspopup="listbox", and aria-controls pointing to
the menu id; give the menu div (where menuClassName is applied) a unique id and
role="listbox"; for each option (in orderedOptions rendered via
handleSelect/value) add role="option" and aria-selected={option.value ===
value}; ensure the selectedOption label remains visible and that the ids used
for aria-controls/role are unique within the component (generate a stable id if
needed).
In `@src/pages/Timer/components/RankingItem.tsx`:
- Around line 36-38: The span rendering the user name in RankingItem.tsx (the
<span> containing {displayName}) is inline so CSS ellipsis (`truncate`) doesn't
work; change that element to a block-level element (e.g., use class that makes
it block or inline-block) so the tailing ellipsis can apply and long names won't
push the study time area out of layout, keeping the existing classes
(text-text-700, truncate, text-[16px], leading-[1.6], font-semibold) and only
adding/modifying display behavior.
In `@src/pages/Timer/components/TimerButton.tsx`:
- Around line 15-17: The text sizes in TimerButton (titleFontSize, timeFontSize,
helperFontSize) use hard minimums (12/44/12) while the container padding (py-6)
creates a minimum content height that mismatches when the incoming prop size can
be zero (see Timer/index where size may be 0); fix by either (A) enforcing a
sensible minimum for the size prop at the source (clamp the size in Timer/index
before passing to TimerButton) or (B) making font sizes and vertical padding
scale proportionally with size (remove the fixed min values and compute
fonts/padding as a consistent ratio of size) so the circle and text always fit
together; update the TimerButton component (titleFontSize, timeFontSize,
helperFontSize, and the vertical padding calculation) or clamp size in the
caller (Timer/index) accordingly.
In `@src/pages/Timer/index.tsx`:
- Around line 24-27: getBottomInsetPx currently parses a numeric px value and
falls back to 80, which strips CSS calc()/safe-area vars and misaligns
halfSheetHeight and timerSize; change getBottomInsetPx to stop parsing to Number
and instead return a CSS length string (e.g., return bottomOverlayInset if
truthy, otherwise '80px' or matchedInset[0]) so callers can use the full CSS
length (including calc() and var(--sab)). Update usages that expect a number
(halfSheetHeight, timerSize) to accept/apply a CSS length (use CSS calc() or set
style values) rather than relying on a numeric pixel value.
---
Outside diff comments:
In `@src/components/common/BottomSheet.tsx`:
- Around line 46-58: getTransform currently returns a hardcoded
'translateY(55%)' when not resizable and effectivePosition === 'half', which
breaks the intended halfHeight behavior; update the non-resizable branch in
getTransform to compute the half translate using the actual half height (prefer
normalizedHalfHeight then fall back to halfHeight) e.g. return effectivePosition
=== 'half' ? `translateY(calc(100% - ${normalizedHalfHeight || halfHeight}px))`
: 'translateY(0)'; and apply the same fix to the similar branch referenced
around lines 73-79 so defaultPosition="half" uses the real half height instead
of 55%.
---
Nitpick comments:
In `@src/components/common/BottomSheet.tsx`:
- Around line 68-72: The BottomSheet component is using clsx to build its
className which can leave Tailwind class conflicts unresolved; replace the clsx
usage with the project's cn() utility and update the import to use cn (from
src/utils/ts/cn.ts) instead of clsx, ensuring the same conditional arguments
(resizable && isDragging, className) are passed to cn so Tailwind
merging/override behavior works correctly.
In `@src/pages/Chat/ChatRoom.tsx`:
- Around line 85-87: The code passes Number(chatRoomId) inline to useChat which
can produce inconsistent parsing and NaN; parse chatRoomId once into a constant
(e.g., const parsedChatRoomId = Number(chatRoomId);) normalize NaN to undefined
(e.g., parsedChatRoomId = Number.isNaN(parsedChatRoomId) ? undefined :
parsedChatRoomId) and then call useChat(parsedChatRoomId). Update references
around useChat(...) in ChatRoom.tsx so sendMessage, chatMessages, fetchNextPage,
hasNextPage, isFetchingNextPage and isSendingMessage come from
useChat(parsedChatRoomId).
🪄 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: e3b8e28c-0408-4ba2-b8e5-658a0d23555d
⛔ Files ignored due to path filters (2)
src/assets/svg/notifications.svgis excluded by!**/*.svg,!src/assets/**and included by**src/assets/svg/unread-notification.svgis excluded by!**/*.svg,!src/assets/**and included by**
📒 Files selected for processing (9)
src/apis/chat/entity.tssrc/components/common/BottomSheet.tsxsrc/components/common/Dropdown.tsxsrc/pages/Chat/ChatRoom.tsxsrc/pages/Chat/index.tsxsrc/pages/Timer/components/RankingItem.tsxsrc/pages/Timer/components/TimerButton.tsxsrc/pages/Timer/index.tsxsrc/utils/hooks/useBottomSheet.ts
💤 Files with no reviewable changes (2)
- src/utils/hooks/useBottomSheet.ts
- src/pages/Chat/index.tsx
| <button | ||
| type="button" | ||
| onClick={() => setIsOpen(!isOpen)} | ||
| className={twMerge( | ||
| 'flex items-center gap-1 rounded-lg px-2.5 py-1.5 text-xs leading-4 font-medium transition-all duration-200 ease-out', | ||
| isOpen | ||
| ? 'bg-indigo-5 border border-blue-500 text-indigo-700' | ||
| : 'border-indigo-25 bg-indigo-5 active:bg-indigo-25 border text-indigo-400' | ||
| className={cn( | ||
| 'inline-flex h-[29px] items-center justify-center overflow-hidden rounded-full bg-[#69BFDF] px-2.5 text-[13px] leading-[1.6] font-medium text-white transition-opacity active:opacity-90', | ||
| isOpen && 'opacity-95' | ||
| )} | ||
| > | ||
| <span>{selectedOption?.label}</span> | ||
| <ChevronDownIcon | ||
| aria-hidden="true" | ||
| className={twMerge('h-3.5 w-3.5 transition-transform duration-200', isOpen && 'rotate-180')} | ||
| className={cn('ml-px h-[18px] w-[18px] transition-transform duration-200', isOpen && 'rotate-180')} | ||
| /> | ||
| </button> | ||
|
|
||
| <div | ||
| className={twMerge( | ||
| 'border-indigo-25 absolute top-full right-0 z-50 mt-1 min-w-20 origin-top-right rounded-lg border bg-white p-1 shadow-lg transition-all duration-200 ease-out', | ||
| className={cn( | ||
| 'absolute top-full right-0 z-50 mt-2 origin-top-right rounded-[10px] border border-[#E7EBEF] bg-white p-3 shadow-[0_0_20px_0_rgba(0,0,0,0.03)] transition-all duration-200 ease-out', | ||
| isOpen ? 'visible translate-y-0 scale-100 opacity-100' : 'invisible -translate-y-2 scale-95 opacity-0', | ||
| menuClassName | ||
| )} | ||
| > | ||
| {options.map((option) => ( | ||
| <button | ||
| key={option.value} | ||
| type="button" | ||
| onClick={() => handleSelect(option.value)} | ||
| className={twMerge( | ||
| 'flex w-full items-center justify-between gap-1.5 rounded-md px-2 py-1.5 text-xs leading-4 font-medium transition-colors duration-150', | ||
| option.value === value ? 'bg-indigo-5 text-indigo-700' : 'active:bg-indigo-5 text-indigo-400' | ||
| )} | ||
| > | ||
| <span>{option.label}</span> | ||
| <CheckIcon | ||
| aria-hidden="true" | ||
| className={twMerge( | ||
| 'h-3.5 w-3.5 text-blue-500 transition-opacity', | ||
| option.value === value ? 'opacity-100' : 'opacity-0' | ||
| )} | ||
| /> | ||
| </button> | ||
| ))} | ||
| <div className="flex min-w-14 flex-col gap-1"> | ||
| {orderedOptions.map((option) => ( | ||
| <button | ||
| key={option.value} | ||
| type="button" | ||
| onClick={() => handleSelect(option.value)} | ||
| className="text-text-600 flex w-full items-center justify-between gap-2 rounded-md py-0.5 pl-0.5 text-[13px] leading-[1.6] font-medium" | ||
| > | ||
| <span>{option.label}</span> | ||
| {option.value === value ? <span aria-hidden="true" className="size-1 rounded-full bg-[#69BFDF]" /> : null} | ||
| </button> | ||
| ))} | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
드롭다운 상태를 ARIA로 노출해주세요.
지금 구조는 스크린리더에 단순 버튼 묶음으로만 읽혀서 열림 상태와 현재 선택값이 전달되지 않습니다. trigger에는 aria-expanded/aria-haspopup="listbox"/aria-controls, 메뉴에는 role="listbox", 옵션에는 role="option"과 aria-selected를 붙여주세요.
As per coding guidelines "src/components/**: 접근성(aria-*, role, 키보드 탐색)이 적절히 처리되는지".
🤖 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 44 - 79, The dropdown
trigger and items lack ARIA roles/attributes; update the trigger button (where
setIsOpen and isOpen are used) to include aria-expanded={isOpen},
aria-haspopup="listbox", and aria-controls pointing to the menu id; give the
menu div (where menuClassName is applied) a unique id and role="listbox"; for
each option (in orderedOptions rendered via handleSelect/value) add
role="option" and aria-selected={option.value === value}; ensure the
selectedOption label remains visible and that the ids used for
aria-controls/role are unique within the component (generate a stable id if
needed).
| <div className="min-w-0 flex-1"> | ||
| <span className="text-text-700 truncate text-[16px] leading-[1.6] font-semibold">{displayName}</span> | ||
| <div className="mt-1 text-[14px] leading-[1.6] font-medium text-[#5A6B7F]"> |
There was a problem hiding this comment.
truncate가 현재는 동작하지 않습니다.
Line 37의 <span>은 inline 요소라 ellipsis가 적용되지 않습니다. 이름이 길면 공부시간 영역을 밀어낼 수 있으니 block이나 inline-block으로 바꿔주세요.
수정 예시
- <span className="text-text-700 truncate text-[16px] leading-[1.6] font-semibold">{displayName}</span>
+ <span className="text-text-700 block truncate text-[16px] leading-[1.6] font-semibold">{displayName}</span>📝 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="min-w-0 flex-1"> | |
| <span className="text-text-700 truncate text-[16px] leading-[1.6] font-semibold">{displayName}</span> | |
| <div className="mt-1 text-[14px] leading-[1.6] font-medium text-[#5A6B7F]"> | |
| <div className="min-w-0 flex-1"> | |
| <span className="text-text-700 block truncate text-[16px] leading-[1.6] font-semibold">{displayName}</span> | |
| <div className="mt-1 text-[14px] leading-[1.6] font-medium text-[`#5A6B7F`]"> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Timer/components/RankingItem.tsx` around lines 36 - 38, The span
rendering the user name in RankingItem.tsx (the <span> containing {displayName})
is inline so CSS ellipsis (`truncate`) doesn't work; change that element to a
block-level element (e.g., use class that makes it block or inline-block) so the
tailing ellipsis can apply and long names won't push the study time area out of
layout, keeping the existing classes (text-text-700, truncate, text-[16px],
leading-[1.6], font-semibold) and only adding/modifying display behavior.
| const titleFontSize = Math.max(12, Math.min(14, Math.round(size * 0.045))); | ||
| const timeFontSize = Math.max(44, Math.min(64, Math.round(size * 0.205))); | ||
| const helperFontSize = Math.max(12, Math.min(14, Math.round(size * 0.045))); |
There was a problem hiding this comment.
작은 화면에서는 버튼보다 텍스트가 먼저 멈춥니다.
Line 15-17 때문에 최소 타이포가 12/44/12px이고, Line 43의 py-6까지 포함하면 최소 콘텐츠 높이만 대략 131px입니다. 그런데 src/pages/Timer/index.tsx:54-56에서는 size가 0까지 내려갈 수 있어서, 작은 viewport에서는 원형만 줄고 텍스트는 밖으로 넘칠 수 있습니다. size 하한을 콘텐츠 최소치와 맞추거나, 폰트/패딩도 같은 비율로 더 줄어들게 맞춰주세요.
Also applies to: 42-46
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Timer/components/TimerButton.tsx` around lines 15 - 17, The text
sizes in TimerButton (titleFontSize, timeFontSize, helperFontSize) use hard
minimums (12/44/12) while the container padding (py-6) creates a minimum content
height that mismatches when the incoming prop size can be zero (see Timer/index
where size may be 0); fix by either (A) enforcing a sensible minimum for the
size prop at the source (clamp the size in Timer/index before passing to
TimerButton) or (B) making font sizes and vertical padding scale proportionally
with size (remove the fixed min values and compute fonts/padding as a consistent
ratio of size) so the circle and text always fit together; update the
TimerButton component (titleFontSize, timeFontSize, helperFontSize, and the
vertical padding calculation) or clamp size in the caller (Timer/index)
accordingly.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
src/pages/Timer/hooks/useTimerLayout.ts (1)
52-52:timerSize가 0이 될 수 있음
Math.max(0, ...)사용으로timerSize가 0까지 내려갈 수 있습니다. 극단적으로 작은 화면에서TimerButton내부 콘텐츠가 버튼 크기를 초과할 수 있으니, 최소값 설정을 고려해보세요.최소 타이머 크기 설정 예시
+ const MIN_TIMER_SIZE = 140; // 내부 콘텐츠 최소 높이 고려 - const timerSize = Math.max(0, Math.min(preferredTimerSize, availableTimerHeight)); + const timerSize = Math.max(MIN_TIMER_SIZE, Math.min(preferredTimerSize, availableTimerHeight));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Timer/hooks/useTimerLayout.ts` at line 52, timerSize can become 0 due to Math.max(0, ...), which may make TimerButton's content overflow on very small screens; introduce a sensible minimum (e.g., minimumTimerSize) and compute timerSize as Math.max(minimumTimerSize, Math.min(preferredTimerSize, availableTimerHeight)) to guarantee the button never shrinks below the usable size; update the calculation where timerSize is defined and ensure any related layout/stylesheet assumptions (e.g., in TimerButton) are compatible with that minimum.src/pages/Timer/components/RankingList.tsx (1)
114-118:keyprop에index포함은 괜찮으나 고유 ID 권장
my-${index}-${item.rank}-${item.name}형태의 key는 동작하지만, API에서 고유 ID를 제공한다면 그것을 사용하는 것이 더 안정적입니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Timer/components/RankingList.tsx` around lines 114 - 118, The keys for the mapped RankingItem components use index-based strings (`my-${index}-${item.rank}-${item.name}` and `ranking-${index}-${item.rank}-${item.name}`) — replace these with the API-provided unique identifier (e.g., use item.id) for both maps (pinnedRankings and rankings) to ensure stable keys across re-renders; if the API ID may be absent, use a deterministic fallback like `${item.id ?? `${item.rank}-${item.name}`}` when setting the key prop on the RankingItem components.src/components/common/BottomSheet.tsx (1)
79-79:clsx대신cn()유틸리티 사용 권장프로젝트 컨벤션에 따르면
cn()유틸리티를 사용해야 합니다.clsx를cn으로 교체해주세요.수정 예시
- import clsx from 'clsx'; + import { cn } from '@/utils/ts/cn'; // Line 79 - className={clsx( + className={cn( // Line 105 - className={clsx( + className={cn(As per coding guidelines: "Use
cn()utility fromsrc/utils/ts/cn.tsto merge Tailwind CSS classes"Also applies to: 105-105
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/BottomSheet.tsx` at line 79, Replace usages of the clsx utility inside the BottomSheet component (the className={clsx(...)} occurrences) with the project cn() utility: import and use cn(...) in place of clsx(...), and remove the old clsx import; ensure both occurrences (the one at the className around the first marked spot and the second at the other occurrence) are updated so the component uses cn() consistently.
🤖 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/BottomSheet.tsx`:
- Line 107: The ternary/class array entry "resizable && isDragging &&
'transition-none'" inside BottomSheet (component in BottomSheet.tsx) is dead
because it's located outside the branch where resizable is true; remove the
"resizable &&" part (or remove the entire condition if only used for
non-resizable branch) and simplify the expression to only use "isDragging &&
'transition-none'" or delete the entry; update any classnames/arrays where that
exact token appears to avoid unreachable checks referencing resizable.
In `@src/styles/colors.css`:
- Around line 9-10: The color design tokens (--color-background,
--color-primary, and any tokens at lines 12-30 and 35-37) must be consolidated
into src/styles/theme.css as the single source-of-truth; move the CSS custom
properties from src/styles/colors.css into src/styles/theme.css, remove the
duplicates from colors.css, and update any module/import references that pull
colors.css to instead reference theme.css so all code and Tailwind v4 token
usage reads from src/styles/theme.css.
---
Nitpick comments:
In `@src/components/common/BottomSheet.tsx`:
- Line 79: Replace usages of the clsx utility inside the BottomSheet component
(the className={clsx(...)} occurrences) with the project cn() utility: import
and use cn(...) in place of clsx(...), and remove the old clsx import; ensure
both occurrences (the one at the className around the first marked spot and the
second at the other occurrence) are updated so the component uses cn()
consistently.
In `@src/pages/Timer/components/RankingList.tsx`:
- Around line 114-118: The keys for the mapped RankingItem components use
index-based strings (`my-${index}-${item.rank}-${item.name}` and
`ranking-${index}-${item.rank}-${item.name}`) — replace these with the
API-provided unique identifier (e.g., use item.id) for both maps (pinnedRankings
and rankings) to ensure stable keys across re-renders; if the API ID may be
absent, use a deterministic fallback like `${item.id ??
`${item.rank}-${item.name}`}` when setting the key prop on the RankingItem
components.
In `@src/pages/Timer/hooks/useTimerLayout.ts`:
- Line 52: timerSize can become 0 due to Math.max(0, ...), which may make
TimerButton's content overflow on very small screens; introduce a sensible
minimum (e.g., minimumTimerSize) and compute timerSize as
Math.max(minimumTimerSize, Math.min(preferredTimerSize, availableTimerHeight))
to guarantee the button never shrinks below the usable size; update the
calculation where timerSize is defined and ensure any related layout/stylesheet
assumptions (e.g., in TimerButton) are compatible with that minimum.
🪄 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: a3e4de0b-9bdd-4fb5-9a29-113f08688be5
📒 Files selected for processing (16)
src/components/common/BottomSheet.tsxsrc/components/layout/index.tsxsrc/index.csssrc/pages/Chat/index.tsxsrc/pages/Home/components/InfiniteClubCarousel.tsxsrc/pages/Manager/components/TimePicker/index.tsxsrc/pages/Schedule/components/ScheduleDetail.tsxsrc/pages/Timer/components/RankingItem.tsxsrc/pages/Timer/components/RankingList.tsxsrc/pages/Timer/components/TimerButton.tsxsrc/pages/Timer/hooks/useTimerLayout.tssrc/pages/Timer/index.tsxsrc/styles/colors.csssrc/styles/theme.csssrc/utils/hooks/useBottomSheet.tssrc/utils/hooks/useTimer.ts
💤 Files with no reviewable changes (3)
- src/pages/Chat/index.tsx
- src/utils/hooks/useTimer.ts
- src/styles/theme.css
✅ Files skipped from review due to trivial changes (3)
- src/components/layout/index.tsx
- src/pages/Manager/components/TimePicker/index.tsx
- src/index.css
| --color-background: #f4f6f9; | ||
| --color-primary: #323532; |
There was a problem hiding this comment.
토큰 소스 위치를 src/styles/theme.css로 다시 일원화해주세요.
현재 색상 디자인 토큰이 src/styles/colors.css에 정의되어, 팀 규칙 기준의 토큰 source-of-truth가 분산되었습니다. 동작 자체는 가능해도, 토큰 관리 기준과 충돌하므로 src/styles/theme.css로 이동(또는 재집중)하는 쪽이 맞습니다.
As per coding guidelines **/*.{ts,tsx,css}: Use Tailwind CSS v4 with design tokens from src/styles/theme.css.
Also applies to: 12-30, 35-37
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/styles/colors.css` around lines 9 - 10, The color design tokens
(--color-background, --color-primary, and any tokens at lines 12-30 and 35-37)
must be consolidated into src/styles/theme.css as the single source-of-truth;
move the CSS custom properties from src/styles/colors.css into
src/styles/theme.css, remove the duplicates from colors.css, and update any
module/import references that pull colors.css to instead reference theme.css so
all code and Tailwind v4 token usage reads from src/styles/theme.css.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 21 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export interface Room { | ||
| roomId: number; | ||
| chatType: 'DIRECT' | 'GROUP'; | ||
| chatType: 'DIRECT' | 'CLUB_GROUP' | 'GROUP'; | ||
| roomName: string; |
There was a problem hiding this comment.
chatType에 'CLUB_GROUP'가 추가되었지만, 코드베이스에는 여전히 chatType === 'GROUP'만을 그룹으로 처리하는 분기(예: src/components/layout/Header/components/ChatHeader.tsx:19)가 존재합니다. 이 상태에서는 CLUB_GROUP 채팅이 1:1처럼 취급되어 사이드바/멤버 표시 등 그룹 전용 UI가 동작하지 않을 수 있으니, 그룹 판별 로직을 chatType !== 'DIRECT' 또는 chatType === 'GROUP' || chatType === 'CLUB_GROUP' 형태로 정리(헬퍼 함수 도입 포함)해 주세요.
| style={{ | ||
| bottom: 0, | ||
| height: getResizableHeight(), | ||
| transition: isDragging ? 'none' : 'height 300ms cubic-bezier(0, 0, 0.2, 1)', | ||
| }} |
There was a problem hiding this comment.
resizable 렌더 경로에서 bottomOffset이 무시됩니다(항상 bottom: 0). 호출 측에서 하단 내비/세이프에어리어를 피하려고 bottomOffset을 전달해도 시트가 그 영역을 침범합니다. style.bottom을 normalizedBottomOffset으로 적용하고, 높이 계산에서도 bottomOffset을 반영해 props 의미를 유지해 주세요.
| function resolveBottomInsetPx(bottomOverlayInset: string) { | ||
| const probe = document.createElement('div'); | ||
| probe.style.position = 'fixed'; | ||
| probe.style.pointerEvents = 'none'; | ||
| probe.style.visibility = 'hidden'; |
There was a problem hiding this comment.
resolveBottomInsetPx()가 호출될 때마다 DOM 엘리먼트를 생성/스타일 적용/측정합니다. 이 함수가 resize 핸들러 경로에서 반복 호출되면(뷰포트 리사이즈/키보드 등) 불필요한 layout/reflow 비용이 커질 수 있습니다. bottomOverlayInset이 변경될 때만 1회 계산해 캐시하고, 리사이즈 시에는 뷰포트/위치만 재계산하도록 분리해 주세요.
There was a problem hiding this comment.
🧹 Nitpick comments (7)
src/pages/Chat/utils/chatType.ts (1)
7-8:isGroupChatType는 명시적 타입 비교가 더 안전합니다.지금 방식은
DIRECT가 아닌 새 타입이 추가될 때도 자동으로 그룹으로 분류됩니다.GROUP/CLUB_GROUP를 명시적으로 체크해 주세요.제안 코드
export function isGroupChatType(chatType?: Room['chatType']) { - return chatType != null && !isDirectChatType(chatType); + return chatType === 'GROUP' || chatType === 'CLUB_GROUP'; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Chat/utils/chatType.ts` around lines 7 - 8, Change isGroupChatType to perform an explicit comparison against the known group enum values instead of treating anything not-direct as a group; locate the isGroupChatType function (signature: isGroupChatType(chatType?: Room['chatType'])) and replace the current null-check + !isDirectChatType(chatType) logic with an explicit check that returns true only when chatType === 'GROUP' || chatType === 'CLUB_GROUP' (or the exact enum members used for group chats in your codebase), keeping a null/undefined guard.src/components/layout/Header/components/ChatHeader.tsx (1)
5-5: 공용 레이아웃 컴포넌트가pages레이어를 직접 참조하고 있습니다.
src/components/layout/...에서@/pages/...를 참조하면 의존 방향이 역전되어 재사용성이 떨어질 수 있습니다.chatType유틸을 공용 레이어(예:src/utils또는src/features/chat)로 이동하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/layout/Header/components/ChatHeader.tsx` at line 5, ChatHeader.tsx currently imports isGroupChatType from the pages layer which inverts dependencies; move the isGroupChatType utility out of the pages layer into a shared/common utilities module (e.g., a shared chat utils or utils module), export it from there, then update the import in ChatHeader.tsx to reference the new shared export (and update any other files that imported the old location), ensuring no references remain to the pages layer and running tests/build to verify imports resolve.src/pages/Timer/index.tsx (1)
76-76: 타이포그래피 토큰 사용 고려
text-[16px]와text-[14px]대신text-body1,text-body2등 시맨틱 타이포그래피 토큰 사용을 권장합니다.♻️ 제안
-<div className="text-text-700 text-center text-[16px] leading-[1.6] font-bold">랭킹</div> +<div className="text-text-700 text-body1 text-center font-bold">랭킹</div>프로젝트 디자인 시스템에 맞는 토큰이 있다면 활용하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Timer/index.tsx` at line 76, Replace hard-coded utility classes text-[16px] (and any other raw pixel typography like text-[14px]) in the ranking header div with the project's semantic typography tokens (e.g., text-body1, text-body2) so the component uses design-system tokens; locate the element with className "text-text-700 text-center text-[16px] leading-[1.6] font-bold" in the Timer page (the 랭킹 header) and swap text-[16px] for the appropriate token (and adjust leading/token choice if needed) to match the design system.src/components/common/BottomSheet.tsx (1)
111-116: non-resizable 브랜치 height 계산 중복
halfHeightValue(line 53)가 이미normalizedHalfHeight ?? calc(...)폴백을 처리하는데, line 115에서 동일한 폴백 로직이 반복됩니다.♻️ 중복 제거 제안
height: effectivePosition === 'full' ? fullHeight - : (normalizedHalfHeight ?? `calc(100% - ${halfTopOffset}px - ${normalizedBottomOffset})`), + : `calc(${halfHeightValue} - ${normalizedBottomOffset})`,또는 non-resizable 전용
halfHeight계산 변수를 별도로 만들어 의도를 명확히 하는 것도 방법입니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/BottomSheet.tsx` around lines 111 - 116, The height calculation repeats the same fallback logic already handled by halfHeightValue; update the height assignment in the BottomSheet component to use halfHeightValue when effectivePosition !== 'full' instead of recomputing normalizedHalfHeight ?? `calc(...)`. Locate effectivePosition, halfHeightValue, normalizedHalfHeight, normalizedBottomOffset, and halfTopOffset in the component and replace the duplicated fallback on line with a direct reference to halfHeightValue (or create a dedicated non-resizable halfHeight variable and use that) so the fallback logic is centralized and not repeated.src/pages/Timer/hooks/useTimerLayout.ts (1)
69-69: timerSectionRef 의존성 안정성 확인
RefObject는 identity가 안정적이므로 의존성 배열에 포함해도 무한 루프 위험은 없습니다. 다만,bottomOverlayInsetPx변경 시에만 재계산이 필요하다면 ref를 제외할 수도 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Timer/hooks/useTimerLayout.ts` at line 69, useTimerLayout 훅의 effect 의존성 배열에 timerSectionRef가 포함되어 있는데 RefObject는 identity가 안정적이라 무한 루프 위험은 없으므로, 만약 해당 effect가 bottomOverlayInsetPx 변경에만 반응하면 timerSectionRef를 의존성 배열에서 제거해 재계산을 bottomOverlayInsetPx 변경으로만 제한하세요; 반대로 effect 내부에서 ref.current를 읽고 ref 변경에 따라 재실행이 필요하면 timerSectionRef를 유지하도록 useTimerLayout 내부의 해당 useEffect 의존성 배열을 조정하세요.src/components/layout/bottomOverlay.ts (1)
24-32: DOM probe 재사용 고려
resolveBottomOverlayInsetPx가 호출될 때마다 DOM 요소를 생성/제거합니다. 빈번한 호출 시 성능 영향이 있을 수 있습니다.♻️ 캐싱 또는 재사용 제안
let cachedProbe: HTMLDivElement | null = null; function getOrCreateProbe(): HTMLDivElement { if (!cachedProbe) { cachedProbe = document.createElement('div'); cachedProbe.style.position = 'fixed'; cachedProbe.style.pointerEvents = 'none'; cachedProbe.style.visibility = 'hidden'; document.body.appendChild(cachedProbe); } return cachedProbe; }다만, 현재
useLayoutBottomOverlayInset의 상태 비교 로직이 불필요한 재계산을 방지하므로 실제 영향은 미미할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/layout/bottomOverlay.ts` around lines 24 - 32, The current resolveBottomOverlayInsetPx creates and removes a DOM probe on every call; change it to reuse a single cached probe element (e.g., add a module-scoped let cachedProbe: HTMLDivElement | null and a getOrCreateProbe() helper) so resolveBottomOverlayInsetPx uses getOrCreateProbe() instead of creating/removing elements each time; ensure getOrCreateProbe sets the fixed/pointerEvents/visibility styles and appends the probe to document.body once, and ensure resolveBottomOverlayInsetPx only updates probe.style.paddingBottom, reads window.getComputedStyle(probe).paddingBottom, and does not remove the probe from the DOM.src/pages/Timer/components/RankingList.tsx (1)
20-27: 불필요한 방어 코드 정리
StudyRanking타입에id속성이 없으므로'id' in item체크는 항상 false입니다. 실제로는 항상${item.rank}-${item.name}폴백을 사용하고 있으니 불필요한 조건을 제거하거나, 의도가 있다면 주석을 추가하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Timer/components/RankingList.tsx` around lines 20 - 27, The getRankingItemKey function contains an unnecessary `'id' in item` guard because StudyRanking does not have an id field; simplify the function (getRankingItemKey) to always use the deterministic fallback `${item.rank}-${item.name}` or, if you intended to support an id, update the StudyRanking type to include id and keep the conditional with a short explanatory comment—pick one approach and remove the dead `'id' in item` branch accordingly.
🤖 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/BottomSheet.tsx`:
- Around line 111-116: The height calculation repeats the same fallback logic
already handled by halfHeightValue; update the height assignment in the
BottomSheet component to use halfHeightValue when effectivePosition !== 'full'
instead of recomputing normalizedHalfHeight ?? `calc(...)`. Locate
effectivePosition, halfHeightValue, normalizedHalfHeight,
normalizedBottomOffset, and halfTopOffset in the component and replace the
duplicated fallback on line with a direct reference to halfHeightValue (or
create a dedicated non-resizable halfHeight variable and use that) so the
fallback logic is centralized and not repeated.
In `@src/components/layout/bottomOverlay.ts`:
- Around line 24-32: The current resolveBottomOverlayInsetPx creates and removes
a DOM probe on every call; change it to reuse a single cached probe element
(e.g., add a module-scoped let cachedProbe: HTMLDivElement | null and a
getOrCreateProbe() helper) so resolveBottomOverlayInsetPx uses
getOrCreateProbe() instead of creating/removing elements each time; ensure
getOrCreateProbe sets the fixed/pointerEvents/visibility styles and appends the
probe to document.body once, and ensure resolveBottomOverlayInsetPx only updates
probe.style.paddingBottom, reads window.getComputedStyle(probe).paddingBottom,
and does not remove the probe from the DOM.
In `@src/components/layout/Header/components/ChatHeader.tsx`:
- Line 5: ChatHeader.tsx currently imports isGroupChatType from the pages layer
which inverts dependencies; move the isGroupChatType utility out of the pages
layer into a shared/common utilities module (e.g., a shared chat utils or utils
module), export it from there, then update the import in ChatHeader.tsx to
reference the new shared export (and update any other files that imported the
old location), ensuring no references remain to the pages layer and running
tests/build to verify imports resolve.
In `@src/pages/Chat/utils/chatType.ts`:
- Around line 7-8: Change isGroupChatType to perform an explicit comparison
against the known group enum values instead of treating anything not-direct as a
group; locate the isGroupChatType function (signature:
isGroupChatType(chatType?: Room['chatType'])) and replace the current null-check
+ !isDirectChatType(chatType) logic with an explicit check that returns true
only when chatType === 'GROUP' || chatType === 'CLUB_GROUP' (or the exact enum
members used for group chats in your codebase), keeping a null/undefined guard.
In `@src/pages/Timer/components/RankingList.tsx`:
- Around line 20-27: The getRankingItemKey function contains an unnecessary
`'id' in item` guard because StudyRanking does not have an id field; simplify
the function (getRankingItemKey) to always use the deterministic fallback
`${item.rank}-${item.name}` or, if you intended to support an id, update the
StudyRanking type to include id and keep the conditional with a short
explanatory comment—pick one approach and remove the dead `'id' in item` branch
accordingly.
In `@src/pages/Timer/hooks/useTimerLayout.ts`:
- Line 69: useTimerLayout 훅의 effect 의존성 배열에 timerSectionRef가 포함되어 있는데 RefObject는
identity가 안정적이라 무한 루프 위험은 없으므로, 만약 해당 effect가 bottomOverlayInsetPx 변경에만 반응하면
timerSectionRef를 의존성 배열에서 제거해 재계산을 bottomOverlayInsetPx 변경으로만 제한하세요; 반대로 effect
내부에서 ref.current를 읽고 ref 변경에 따라 재실행이 필요하면 timerSectionRef를 유지하도록 useTimerLayout
내부의 해당 useEffect 의존성 배열을 조정하세요.
In `@src/pages/Timer/index.tsx`:
- Line 76: Replace hard-coded utility classes text-[16px] (and any other raw
pixel typography like text-[14px]) in the ranking header div with the project's
semantic typography tokens (e.g., text-body1, text-body2) so the component uses
design-system tokens; locate the element with className "text-text-700
text-center text-[16px] leading-[1.6] font-bold" in the Timer page (the 랭킹
header) and swap text-[16px] for the appropriate token (and adjust leading/token
choice if needed) to match the design system.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: fa3d24a2-3d1c-4bd8-9c6b-4bf2a44c635c
📒 Files selected for processing (14)
AGENTS.mdCLAUDE.mdsrc/components/common/BottomSheet.tsxsrc/components/layout/Header/components/ChatHeader.tsxsrc/components/layout/bottomOverlay.tssrc/components/layout/hooks/useLayoutBottomOverlayInset.tssrc/components/layout/hooks/useLayoutElements.tssrc/components/layout/index.tsxsrc/contexts/useLayoutElementsContext.tsxsrc/pages/Chat/index.tsxsrc/pages/Chat/utils/chatType.tssrc/pages/Timer/components/RankingList.tsxsrc/pages/Timer/hooks/useTimerLayout.tssrc/pages/Timer/index.tsx
✅ Files skipped from review due to trivial changes (2)
- CLAUDE.md
- AGENTS.md
🚧 Files skipped from review as they are similar to previous changes (2)
- src/pages/Chat/index.tsx
- src/components/layout/index.tsx
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/BottomSheet.tsx`:
- Around line 56-62: The getResizableHeight function currently applies
currentTranslate directly and can produce heights beyond the intended half/full
bounds; modify getResizableHeight to compute the target height (based on
effectivePosition using halfHeightValue and resizableFullHeight adjusted by
currentTranslate) and then clamp that computed height between the numeric half
and full height limits (use Math.max/Math.min or equivalent) before returning
the CSS calc string; ensure you handle units consistently for halfHeightValue
and resizableFullHeight and reference getResizableHeight, effectivePosition,
halfHeightValue, resizableFullHeight, and currentTranslate when making the
change.
- Around line 91-96: The sheet handle currently only spreads touch handlers from
useBottomSheet (handlers) and lacks pointer/mouse/keyboard support and ARIA
attributes; update useBottomSheet to return pointer and mouse handler aliases
(onPointerDown/onPointerMove/onPointerUp and onMouseDown/onMouseMove/onMouseUp)
or collapse them into unified pointer events, and in BottomSheet's handle div
(the element rendering the small rounded bar) add role="slider" or role="button"
as appropriate, aria-label (e.g., "Drag handle"), tabindex={0}, and keyboard
handlers (onKeyDown handling ArrowUp/ArrowDown/PageUp/PageDown/Home/End and
Space/Enter to start/finish moves by calling the same internal start/drag/end
functions exposed by useBottomSheet) plus pointer event handlers
(onPointerDown/onPointerMove/onPointerUp) that delegate to the existing touch
move logic; ensure handlers call preventDefault where needed and update
useBottomSheet's returned handler object name (handlers) so the component maps
pointer/mouse/keyboard events to the same internal drag logic.
🪄 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: f9bf98d1-1a79-4e4f-a7d1-a9e8b93c37ef
📒 Files selected for processing (3)
src/components/common/BottomSheet.tsxsrc/components/layout/bottomOverlay.tssrc/pages/Timer/components/RankingList.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/layout/bottomOverlay.ts
| const getResizableHeight = () => { | ||
| if (effectivePosition === 'half') { | ||
| return `calc(${halfHeightValue} - ${Math.min(0, currentTranslate)}px)`; | ||
| } | ||
|
|
||
| return `calc(${resizableFullHeight} - ${Math.max(0, currentTranslate)}px)`; | ||
| }; |
There was a problem hiding this comment.
드래그 높이는 half/full 범위로 clamp하는 게 안전합니다.
지금 계산은 currentTranslate를 그대로 반영해서, 위로 크게 당기면 full 높이를 넘고 아래로 크게 당기면 half보다 더 작아질 수 있습니다. 드래그 중 시트가 튀는 원인이라 범위를 고정해 두는 편이 좋습니다.
수정 예시
const getResizableHeight = () => {
if (effectivePosition === 'half') {
- return `calc(${halfHeightValue} - ${Math.min(0, currentTranslate)}px)`;
+ return `min(${resizableFullHeight}, calc(${halfHeightValue} - ${Math.min(0, currentTranslate)}px))`;
}
- return `calc(${resizableFullHeight} - ${Math.max(0, currentTranslate)}px)`;
+ return `max(${halfHeightValue}, calc(${resizableFullHeight} - ${Math.max(0, currentTranslate)}px))`;
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/common/BottomSheet.tsx` around lines 56 - 62, The
getResizableHeight function currently applies currentTranslate directly and can
produce heights beyond the intended half/full bounds; modify getResizableHeight
to compute the target height (based on effectivePosition using halfHeightValue
and resizableFullHeight adjusted by currentTranslate) and then clamp that
computed height between the numeric half and full height limits (use
Math.max/Math.min or equivalent) before returning the CSS calc string; ensure
you handle units consistently for halfHeightValue and resizableFullHeight and
reference getResizableHeight, effectivePosition, halfHeightValue,
resizableFullHeight, and currentTranslate when making the change.
| <div | ||
| className="flex h-6 shrink-0 cursor-grab touch-none items-center justify-center pt-3 select-none active:cursor-grabbing" | ||
| {...handlers} | ||
| > | ||
| <div className="bg-text-400 h-1 w-11 rounded-full" /> | ||
| </div> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -t f "BottomSheet.tsx" --type fRepository: BCSDLab/KONECT_FRONT_END
Length of output: 105
🏁 Script executed:
cat -n src/components/common/BottomSheet.tsxRepository: BCSDLab/KONECT_FRONT_END
Length of output: 4848
🏁 Script executed:
fd -t f "useBottomSheet*" src/Repository: BCSDLab/KONECT_FRONT_END
Length of output: 101
🏁 Script executed:
cat -n src/utils/hooks/useBottomSheet.tsRepository: BCSDLab/KONECT_FRONT_END
Length of output: 4509
핸들 요소가 터치 이벤트만 처리하며 마우스·키보드 입력과 접근성이 부족합니다.
useBottomSheet에서 반환하는 핸들러는 onTouchStart, onTouchMove, onTouchEnd만 포함하고 있어서 마우스 드래그와 키보드 네비게이션이 작동하지 않습니다. 또한 핸들 div에 role, aria-label 같은 접근성 속성이 없습니다. 포인터/마우스 이벤트와 키보드 지원을 추가하고 적절한 ARIA 속성을 붙여주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/common/BottomSheet.tsx` around lines 91 - 96, The sheet handle
currently only spreads touch handlers from useBottomSheet (handlers) and lacks
pointer/mouse/keyboard support and ARIA attributes; update useBottomSheet to
return pointer and mouse handler aliases
(onPointerDown/onPointerMove/onPointerUp and onMouseDown/onMouseMove/onMouseUp)
or collapse them into unified pointer events, and in BottomSheet's handle div
(the element rendering the small rounded bar) add role="slider" or role="button"
as appropriate, aria-label (e.g., "Drag handle"), tabindex={0}, and keyboard
handlers (onKeyDown handling ArrowUp/ArrowDown/PageUp/PageDown/Home/End and
Space/Enter to start/finish moves by calling the same internal start/drag/end
functions exposed by useBottomSheet) plus pointer event handlers
(onPointerDown/onPointerMove/onPointerUp) that delegate to the existing touch
move logic; ensure handlers call preventDefault where needed and update
useBottomSheet's returned handler object name (handlers) so the component maps
pointer/mouse/keyboard events to the same internal drag logic.
✨ 요약
😎 해결한 이슈
close #256
✅ 작업 내용
CLUB_GROUP채팅 타입을 지원하도록 채팅 엔티티를 확장했습니다.🧪 테스트
pnpm lintpnpm buildSummary by CodeRabbit
스타일
신규 기능
버그 수정
문서
제거