Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
To continue reviewing without waiting, purchase usage credits in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (10)
📝 Walkthrough코드 리뷰 분석Walkthrough친구 기능을 팔로우 기능으로 마이그레이션하고, 홈 화면 UI를 RadialControl 기반에서 HomeSearchControlDock 기반으로 리팩토링했으며, 방 입장 시 비밀번호 지속성, 현재 요청자 카드, 휠 네비게이션 등을 추가했습니다. ChangesFollow 시스템 마이그레이션
홈 화면 및 방 네비게이션 UI 리팩토링
방 입장 및 비밀번호 관리
검색 페이지 및 스타일링 개선
Sequence DiagramsequenceDiagram
actor User
participant HomeScreen as 홈 화면
participant HomeSearchControlDock as 제어 독
participant HomeRoomStage as 방 스테이지
participant RoomJoinPasswordModal as 비밀번호 모달
participant RoomEntry as 입장 흐름 관리
participant API as 서버 API
User->>HomeScreen: 앱 로드
activate HomeScreen
HomeScreen->>RoomEntry: useRoomEntry 초기화
HomeScreen->>HomeSearchControlDock: 제어 독 렌더링
HomeScreen->>HomeRoomStage: 방 스테이지 렌더링
deactivate HomeScreen
User->>HomeRoomStage: 휠로 방 선택/터치
activate HomeRoomStage
HomeRoomStage->>RoomEntry: onRequestRoomEntry 호출
deactivate HomeRoomStage
activate RoomEntry
alt 비밀번호 보호 방
RoomEntry->>RoomJoinPasswordModal: 모달 열기
activate RoomJoinPasswordModal
User->>RoomJoinPasswordModal: 비밀번호 입력 및 제출
RoomJoinPasswordModal->>API: joinRoom (비밀번호 포함)
API-->>RoomJoinPasswordModal: 성공/실패
alt 성공
RoomJoinPasswordModal->>RoomJoinPasswordModal: 비밀번호 저장
RoomJoinPasswordModal->>RoomEntry: onJoined 콜백
end
deactivate RoomJoinPasswordModal
else 공개 방
RoomEntry->>API: 직접 입장
end
deactivate RoomEntry
User->>HomeSearchControlDock: 필터/메뉴 선택
activate HomeSearchControlDock
HomeSearchControlDock->>HomeScreen: onSelectFilter/onCreateRoom 호출
deactivate HomeSearchControlDock
User->>HomeScreen: 방 입장 완료
HomeScreen->>API: 방 페이지로 이동
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120분 Possibly related PRs
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Code Review
This pull request implements a significant UI refactor, replacing the radial navigation control with a new dock-based navigation system for rooms and introducing follow/following management features to replace the previous friend-based terminology. Additionally, it updates various UI components, such as the YouTube player, login modal, and room list, to align with the new design system. I have no specific feedback to provide as the review comments were purely validation-based and did not identify actionable issues.
There was a problem hiding this comment.
Actionable comments posted: 10
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
src/features/room/join/ui/roomPasswordInput.tsx (1)
32-38:⚠️ Potential issue | 🟠 Major | ⚡ Quick win접근성 이슈:
<label>제거 후aria-label이 누락되었습니다.
<label>요소가 제거되면서 비밀번호 입력 필드에 접근 가능한 이름이 없어졌습니다. 스크린 리더 사용자는 이 필드의 용도를 알 수 없습니다 (WCAG 4.1.2 위반).🛠️ 수정 제안
<input className="w-full border px-3 py-2" type="password" + aria-label="방 비밀번호" value={password} onChange={(event) => setPassword(event.target.value)} disabled={submitting} />🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/room/join/ui/roomPasswordInput.tsx` around lines 32 - 38, The password input lost its accessible name when the <label> was removed; add an accessible name so screen readers can identify it—for example, in the RoomPasswordInput component update the input that uses value={password}, onChange={(event) => setPassword(event.target.value)} and disabled={submitting} to include an aria-label (e.g., aria-label="Room password" or a localized equivalent) or reintroduce a visually-hidden <label> tied to the input via id to satisfy WCAG 4.1.2; ensure the label text matches the field purpose and works with the existing password, setPassword, and submitting props/state.src/features/follow/requests/ui/FollowRequestButton.tsx (1)
36-36:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win버튼 레이블 "친구추가"가 follow 의미와 불일치
이 PR은 friend→follow 마이그레이션인데, 버튼 레이블만 여전히 "친구추가"로 남아 있습니다.
hasRequested("요청됨"),isRequesting("요청중...") 텍스트는 이미 팔로우 컨텍스트로 변경되었으므로 기본 상태 레이블도 동일하게 수정해야 합니다.✏️ 수정 제안
- {hasRequested ? "요청됨" : isRequesting ? "요청중..." : "친구추가"} + {hasRequested ? "요청됨" : isRequesting ? "요청중..." : "팔로우"}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/follow/requests/ui/FollowRequestButton.tsx` at line 36, The default button label is still "친구추가" which misaligns with the follow migration; update the label used in FollowRequestButton (the JSX conditional that references hasRequested and isRequesting) so the fallback text matches follow terminology (e.g., change "친구추가" to the appropriate follow label like "팔로우" or another agreed word), keeping the existing hasRequested ("요청됨") and isRequesting ("요청중...") branches untouched.src/features/follow/requests/api/fetchReceivedFollowRequests.ts (1)
8-16:⚠️ Potential issue | 🟠 Major | ⚡ Quick win엔드포인트 경로가 함수명과 불일치 — 잘못된 API를 호출할 수 있습니다.
함수명은
fetchReceivedFollowRequests로 변경되었지만 호출 경로는 여전히/api/v1/friend-requests를 가리킵니다. 백엔드에서 팔로우 요청 전용 경로(/api/v1/follow-requests등)가 별도로 존재한다면 지금 코드는 엉뚱한 엔드포인트를 호출하게 됩니다.의도적으로 이전 경로를 그대로 재사용하는 것이라면, 하위 호환 목적임을 주석으로 명시해 주세요.
🐛 경로가 변경된 경우 적용할 수정안
- >("/api/v1/friend-requests", { params }); + >("/api/v1/follow-requests", { params });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/follow/requests/api/fetchReceivedFollowRequests.ts` around lines 8 - 16, The function fetchReceivedFollowRequests currently calls "/api/v1/friend-requests" which doesn't match its name; either update the GET path in the axiosInstance.get call to the correct follow-requests endpoint (e.g., "/api/v1/follow-requests") or, if keeping the old friend-requests endpoint intentionally for backward compatibility, add a clear comment above fetchReceivedFollowRequests explaining that the legacy "/api/v1/friend-requests" route is being reused on purpose; locate the axiosInstance.get invocation in fetchReceivedFollowRequests to make the change or add the comment.src/features/room/search/ui/RoomSearchInput.tsx (1)
7-46:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
query상태가 컴포넌트 외부로 전달되지 않아 실제 검색 기능이 동작하지 않습니다.
query상태는 로컬에만 머물고 있어, 사용자가 텍스트를 입력해도 검색 API 호출이나 URL 동기화 등 어떤 동작도 발생하지 않습니다. 검색 UI만 존재하고 기능이 없는 상태입니다.해소 방향 두 가지 중 하나를 선택하세요:
- 콜백 prop 노출 —
onQueryChange혹은onSearchprop을 추가하여 부모가 상태를 제어하도록 합니다.- URL 동기화 —
useRouter/useSearchParams로query를 URL 쿼리 파라미터에 바인딩하여 페이지 단위 검색이 가능하게 합니다.또한 Line 41의 버튼 레이블
RESET이 한국어 UI 맥락과 어울리지 않습니다 (초기화또는지우기등으로 변경을 권장합니다).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/room/search/ui/RoomSearchInput.tsx` around lines 7 - 46, RoomSearchInput's local query state (query, setQuery) never leaves the component so typing doesn't trigger search; update the component to either (A) accept a callback prop (e.g., onQueryChange or onSearch) and call it from the input onChange and the reset button so the parent can perform API calls/URL updates, or (B) sync query with the URL using Next.js router hooks (useRouter or useSearchParams) so changes update the query param and trigger page-level search; also replace the reset button label "RESET" with a Korean label like "초기화" or "지우기" and ensure the reset button clears state and invokes the chosen callback or updates the URL accordingly (refer to RoomSearchInput, query, setQuery, onQueryChange/onSearch, and useRouter/useSearchParams).src/features/playlist/skip-track/ui/SkipTrackButton.module.css (1)
1-21:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
:focus-visible제거로 인한 접근성 회귀
:focus-visible스타일이 삭제되면서 키보드 포커스 표시자가 사라졌습니다. 스킵 트랙은 핵심 인터랙션이므로 키보드/보조 기술 사용자가 현재 포커스된 버튼을 시각적으로 구분할 수 없게 됩니다.♿ :focus-visible 복구 제안
.button:disabled { cursor: default; opacity: 0.55; } + +.button:focus-visible { + outline: 3px solid `#5b8cff`; + outline-offset: 2px; +}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/playlist/skip-track/ui/SkipTrackButton.module.css` around lines 1 - 21, The .button CSS removed :focus-visible causing keyboard users to lose the visible focus indicator; restore a focus-visible rule by adding a .button:focus-visible selector that applies a clear, high-contrast visual (e.g., outline or box-shadow using an accessible color variable) and ensure disabled buttons are not styled (e.g., .button:disabled:focus-visible { outline: none }). Also ensure the style pairs with existing transitions and does not show for mouse focus (use :focus-visible rather than :focus).src/app/room/[slug]/RoomPageSongInfo.tsx (1)
296-337:⚠️ Potential issue | 🟠 Major | ⚡ Quick win저장된 비밀번호가 비-비밀번호 에러(네트워크/서버 오류 등)에도 무차별로 폐기됩니다.
if (storedPassword) { clearStoredRoomJoinPassword(slug); setStatus("needs-password"); return; }이 분기는
storedPassword가 존재하기만 하면 모든 종류의 join 실패에 대해 저장된 비밀번호를 삭제합니다. 즉, 일시적 네트워크 오류나 5xx 서버 오류로 join이 실패해도 정상이었던 비밀번호가 사라져 사용자에게 다시 입력을 강요하게 됩니다(영속화의 본 취지를 무력화). 비밀번호 관련 에러 코드일 때만 폐기하도록 좁혀 주세요.🐛 제안 수정
- if (storedPassword) { - clearStoredRoomJoinPassword(slug); - setStatus("needs-password"); - return; - } - - if (err.code === "room.password-required") { - setStatus("needs-password"); - return; - } + const isPasswordError = + err.code === "room.password-required" || + err.code === "room.password-incorrect"; + + if (isPasswordError) { + if (storedPassword) { + clearStoredRoomJoinPassword(slug); + } + setStatus("needs-password"); + return; + }실제 서버 측 비밀번호 오류 코드 명세에 맞춰 코드 상수만 조정해 주세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/room/`[slug]/RoomPageSongInfo.tsx around lines 296 - 337, The current catch block indiscriminately clears storedPassword whenever storedPassword is present, which removes valid passwords on transient failures; change the logic in the catch of the async join flow (where joinRequestRef.current.promise is awaited) to only call clearStoredRoomJoinPassword(slug) and setStatus("needs-password") when the API error code indicates a password problem (e.g., err.code === "room.password-required" or the server's specific password-related error code(s) such as "room.password-invalid"); otherwise keep the stored password and set status to "error" (or preserve current behavior for network/server errors). Update the conditionals around err (the ApiError handling) so storedPassword is cleared only for those password-specific codes and leave other errors untouched.
🟡 Minor comments (14)
src/shared/ui/radial-control/RadialControl.module.css-87-92 (1)
87-92:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winhover 시
opacity: 0.82적용이 아이콘까지 흐리게 만들어 UX를 저하시킵니다.
opacity는 요소 전체(배경 + 아이콘 아이콘 콘텐츠)에 적용됩니다. hover 시 배경만 약하게 표시하고 싶다면background의 알파값으로만 처리하고opacity속성을 제거하는 것이 의도에 맞습니다..leftSlot > *:not(:disabled):hover, .rightSlot > *:not(:disabled):hover { border-radius: 999px; background: rgba(176, 176, 181, 0.14); - opacity: 0.82; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/shared/ui/radial-control/RadialControl.module.css` around lines 87 - 92, The hover rule on .leftSlot and .rightSlot applies opacity: 0.82 which dims the entire element including the icon; remove the opacity property from the hover selectors and rely on the existing background rgba alpha (or adjust the rgba alpha value) so only the background is visually softened while the icon remains fully opaque; update the CSS rule for .leftSlot > *:not(:disabled):hover and .rightSlot > *:not(:disabled):hover to delete the opacity line and, if necessary, tweak the background rgba alpha to achieve the desired visual strength.src/features/room/join/ui/RoomJoinPasswordModal.module.css-43-43 (1)
43-43:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
.description의font-weight가 CSS 변수 대신 하드코딩됨이 PR에서
--fw-light: 300이globals.css에 추가되었는데, 동일한 PR에서font-weight: 300을 리터럴로 사용하고 있습니다. 같은 파일의 다른 모든font-weight값(lines 35, 58, 80, 99)은var(--fw-*)변수를 사용하므로 일관성 유지가 필요합니다.🐛 수정 제안
.description { color: `#b0b0b5`; font-size: 16px; margin-top: 3px; - font-weight: 300; + font-weight: var(--fw-light); line-height: 1.4; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/room/join/ui/RoomJoinPasswordModal.module.css` at line 43, The .description rule currently uses a hardcoded font-weight (300); update it to use the CSS variable --fw-light like the other rules (e.g., var(--fw-*) usage elsewhere) so font-weight is consistent—locate the .description selector in RoomJoinPasswordModal.module.css and replace the literal 300 with var(--fw-light) ensuring consistency with other font-weight declarations.src/entities/room/ui/RoomInfo.module.css-65-90 (1)
65-90:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win일부 CSS 값이 유효하지 않아 스타일이 누락될 수 있습니다.
Line [76], Line [89]의padding: 5 14px;는 유효하지 않아 무시됩니다. 또한 Line [72]의--fw-semiold는 오타로 보이며 의도한 두께가 적용되지 않을 수 있습니다.수정 제안
.tagItem { - font-weight: var(--fw-semiold); + font-weight: var(--fw-semibold); line-height: 1; background-color: white; border: 0.5px solid `#b0b0b5`; - padding: 5 14px; + padding: 5px 14px; border-radius: 999px; } .usersCount { @@ - padding: 5 14px; + padding: 5px 14px; font-size: 14px; font-weight: var(--fw-semibold);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/entities/room/ui/RoomInfo.module.css` around lines 65 - 90, The CSS in .tagItem and .usersCount uses an invalid shorthand padding "padding: 5 14px;" and a likely misspelled custom property "--fw-semiold"; update both selectors to use a valid padding shorthand (e.g., "padding: 5px 14px;") and correct the font-weight variable reference in .tagItem to the intended name (e.g., replace "--fw-semiold" with the proper variable like "--fw-semibold" or substitute a numeric weight such as "600") so the styles apply as expected.src/features/room/list/ui/HomeRoomStage.module.css-158-158 (1)
158-158:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winStylelint 오류:
@keyframes이름을 kebab-case로 변경해야 합니다.
keyframes-name-pattern규칙 위반으로 CI lint가 실패합니다. 애니메이션 참조(Line 158)와@keyframes선언(Line 222) 모두 함께 수정해야 합니다.🔧 수정 제안
- animation: skeletonShimmer 1300ms ease-in-out infinite; + animation: skeleton-shimmer 1300ms ease-in-out infinite;-@keyframes skeletonShimmer { +@keyframes skeleton-shimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } }Also applies to: 222-230
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/room/list/ui/HomeRoomStage.module.css` at line 158, The animation name skeletonShimmer violates the stylelint keyframes-name-pattern (should be kebab-case); rename the keyframes declaration and all references to kebab-case (for example change skeletonShimmer to skeleton-shimmer) by updating the animation property using "skeleton-shimmer" and the corresponding `@keyframes` rule (the current `@keyframes` skeletonShimmer declaration) so both the usage and the keyframes block match the new kebab-case identifier.src/features/room/list/ui/HomeRoomStage.module.css-121-127 (1)
121-127:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winStylelint 오류: 커스텀 프로퍼티 뒤 일반 선언 전에 빈 줄 필요 (
declaration-empty-line-before).Lines 125, 165, 173, 181, 189 모두 같은 패턴입니다 —
--room-stage-image-opacity(커스텀 프로퍼티) 바로 뒤에opacity(일반 프로퍼티)가 오는 경우 빈 줄이 필요합니다.🔧 수정 예시 (동일 패턴을 5곳 모두에 적용)
.slot[data-slot="current"] .slotCard { --card-width: var(--card-current-size); --card-height: var(--card-current-size); --room-stage-image-opacity: 1; + opacity: 1; filter: none; }Also applies to: 161-175, 177-191
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/room/list/ui/HomeRoomStage.module.css` around lines 121 - 127, The Stylelint rule requires a blank line between custom properties and subsequent regular declarations; in each affected selector (e.g., .slot[data-slot="current"] .slotCard) insert a single blank line between the custom property --room-stage-image-opacity and the following regular property (opacity) — apply the same change for the other four occurrences with the same pattern (lines around the other selectors where --room-stage-image-opacity is immediately followed by opacity) so every custom property block is separated from normal declarations by one empty line.src/entities/room/ui/SearchPageRoomCard.module.css-3-5 (1)
3-5:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winStylelint 오류: 선언 사이의 불필요한 빈 줄 제거 (
declaration-empty-line-before).Line 3(
height)과 Line 5(padding-inline) 사이의 빈 줄(Line 4)을 제거해야 합니다.🔧 수정 제안
height: var(--room-card-height, 67px); - padding-inline: 30px;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/entities/room/ui/SearchPageRoomCard.module.css` around lines 3 - 5, Remove the unnecessary blank line between the CSS declarations in SearchPageRoomCard.module.css: in the rule that contains the height declaration (height: var(--room-card-height, 67px);) and the padding-inline declaration (padding-inline: 30px;), delete the empty line so the two declarations are adjacent to satisfy the stylelint declaration-empty-line-before rule.src/features/room/list/ui/HomeRoomStage.module.css-16-23 (1)
16-23:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winStylelint 오류:
calc()내+연산자 뒤 줄바꿈 금지 (scss/operator-no-newline-after).Lines 17, 21에서
+다음에 줄바꿈이 있어 lint 오류가 발생합니다. 연산자를 다음 줄 앞으로 이동시켜야 합니다.🔧 수정 제안
--near-offset: calc( - (var(--card-current-size) / 2) + (var(--card-near-size) / 2) + - var(--gap-current-near) + (var(--card-current-size) / 2) + (var(--card-near-size) / 2) + + var(--gap-current-near) ); --far-offset: calc( - var(--near-offset) + (var(--card-near-size) / 2) + - (var(--card-far-size) / 2) + var(--gap-near-far) + var(--near-offset) + (var(--card-near-size) / 2) + + (var(--card-far-size) / 2) + var(--gap-near-far) );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/room/list/ui/HomeRoomStage.module.css` around lines 16 - 23, 스타일린트 오류는 calc() 식에서 `+` 연산자 뒤에 줄바꿈이 있어 발생하므로 `--near-offset`와 `--far-offset` 계산식의 줄바꿈 위치를 바꿔 `+` 연산자가 다음 줄 맨 앞에 오지 않도록 수정하세요; 즉 함수 calc(...) 내부에서 `+` 연산자를 해당 연산자 왼쪽 항 끝에 붙이거나 연산자를 각 줄의 끝으로 이동시켜 `--near-offset`와 `--far-offset` 선언에서 모든 `+` 연산자 뒤에 줄바꿈이 없도록 정렬하면 됩니다.src/features/room/search/ui/SearchPageRoomList.module.css-14-15 (1)
14-15:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winStylelint 오류: 커스텀 프로퍼티 뒤에 빈 줄 추가 필요 (
declaration-empty-line-before).
--room-card-height커스텀 프로퍼티(Line 14)와width일반 프로퍼티(Line 15) 사이에 빈 줄이 있어야 합니다.🔧 수정 제안
.track { --room-card-height: 67px; + width: 100%;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/room/search/ui/SearchPageRoomList.module.css` around lines 14 - 15, The Stylelint rule requires an empty line before non-custom declarations when a custom property is present; in SearchPageRoomList.module.css add a blank line between the custom property --room-card-height and the following width declaration so the block looks like: define --room-card-height: 67px; then a single empty line, then width: 100%; this satisfies declaration-empty-line-before.src/entities/room/ui/RoomStageCard.tsx-38-38 (1)
38-38:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
aria-label이 선택 상태에 관계없이 항상 "방 선택"으로 고정되어 있습니다.
SearchPageRoomCard는 선택 상태(isSelected)에 따라 "방 선택"과 "방 입장"을 동적으로 구분하는 반면, 이 컴포넌트는 항상 "방 선택"을 반환합니다. 카드가 이미 선택된 상태에서는 스크린 리더 사용자에게 "방 입장" 레이블이 더 정확한 액션을 전달합니다.♿ 제안 수정
- aria-label={ariaLabel ?? `${title} 방 선택`} + aria-label={ariaLabel ?? (isSelected ? `${title} 방 입장` : `${title} 방 선택`)}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/entities/room/ui/RoomStageCard.tsx` at line 38, The aria-label is always set to `${title} 방 선택` regardless of selection state; update RoomStageCard (in src/entities/room/ui/RoomStageCard.tsx) to compute the default aria-label using the isSelected prop so that when ariaLabel is not provided it uses `${title} 방 입장` if isSelected is true and `${title} 방 선택` if false (preserve any explicit ariaLabel prop if passed); mirror the dynamic behavior used by SearchPageRoomCard to improve accessibility for screen-reader users.src/features/follow/requests/ui/FollowRequestCard.tsx-19-26 (1)
19-26:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win프로필 이미지에 의미 있는
alt텍스트 사용 필요프로필 사진은 장식 이미지가 아니므로
alt=""는 접근성 지침에 맞지 않습니다. Next.js 공식 문서에 따르면alt속성은 스크린 리더와 검색 엔진을 위해 이미지를 설명하는 대체 텍스트이며, 이미지 로딩 실패 시 폴백 텍스트로도 사용됩니다. 이미지의 의미를 바꾸지 않고 이미지를 대체할 수 있는 텍스트를 포함해야 합니다. 이미지가 순전히 장식용이거나 사용자에게 의도된 것이 아닌 경우에만alt=""로 설정해야 합니다.🛡️ 수정 제안
- <Image - src={profileImageSrc} - alt="" - fill - sizes="40px" - unoptimized={Boolean(item.requesterProfileImageUrl)} - className={styles.avatar} - /> + <Image + src={profileImageSrc} + alt={`${item.requesterNickname}의 프로필 이미지`} + fill + sizes="40px" + unoptimized={Boolean(item.requesterProfileImageUrl)} + className={styles.avatar} + />🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/follow/requests/ui/FollowRequestCard.tsx` around lines 19 - 26, The Image in FollowRequestCard.tsx currently uses alt="" which is not accessible; update the Image alt to a meaningful description using the requester's data (e.g., derive from item.requesterDisplayName or item.requesterProfileName) such as "Profile photo of {requesterDisplayName}" and fall back to a generic label like "Profile image" when the name is missing; ensure this change references the same props used now (profileImageSrc, item.requesterProfileImageUrl, styles.avatar) so screen readers and image-fallback behavior get the descriptive text.src/features/follow/ui/FollowModal.module.css-13-15 (1)
13-15:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winStylelint 오류 2건 → CI 블로킹 가능성
두 곳에서 실제 Stylelint 오류가 발생합니다.
- Line 15 (
declaration-empty-line-before): CSS 커스텀 프로퍼티 블록(--panel-*) 바로 뒤에 일반 선언이 오면 Stylelint가 빈 줄을 요구합니다.- Line 200 (
value-keyword-case):currentColor→currentcolor(소문자)🔧 수정 제안
.modal { --panel-horizontal-padding: 16px; --panel-content-width: 1060px; + position: relative;- background-color: currentColor; + background-color: currentcolor;추가로,
:global(...)선택자는 CSS Modules 유효 문법이나 Stylelint가 이를 인식하지 못하고 있습니다. Stylelint 설정에 아래를 추가해 false positive를 억제하세요."selector-pseudo-class-no-unknown": [true, { "ignorePseudoClasses": ["global", "local"] }]🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/follow/ui/FollowModal.module.css` around lines 13 - 15, Add a blank line after the CSS custom property block (the --panel-horizontal-padding and --panel-content-width declarations) before the next regular declaration to satisfy declaration-empty-line-before, change any occurrences of "currentColor" to lowercase "currentcolor" to satisfy value-keyword-case, and update Stylelint config to ignore :global and :local pseudo-classes (so the CSS Modules :global(...) selector won't produce false positives).src/widgets/home/ui/HomeControlPanelShell.module.css-12-12 (1)
12-12:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win오래된
QUE/...주석을 새 메뉴 라벨로 갱신.
menuItems가QUE→RANDOM으로 바뀌었지만 CSS 주석은 여전히QUE/CREATE/FRIEND/SETTING,QUE/CREATE표기를 유지하고 있어 추후 검색/유지보수에 혼선을 줄 수 있습니다.-/* QUE/CREATE/FRIEND/SETTING 메뉴 패널 */ +/* RANDOM/CREATE/FRIEND/SETTING 메뉴 패널 */ .menuPanel { /* ... */ } -/* QUE/CREATE 메뉴 옵션 배치 */ +/* 메뉴 옵션 배치 */ .menuRow { /* ... */ } -/* QUE/CREATE 메뉴 옵션 버튼 영역 */ +/* 메뉴 옵션 버튼 영역 */ .menuItem { /* ... */ }Also applies to: 22-22, 58-58
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/widgets/home/ui/HomeControlPanelShell.module.css` at line 12, Update the outdated CSS comments that still reference "QUE/CREATE/FRIEND/SETTING" and "QUE/CREATE": locate the comment occurrences in HomeControlPanelShell.module.css (the top panel comment and the other comment blocks) and replace those strings with the new menu labels "RANDOM/CREATE/FRIEND/SETTING" and "RANDOM/CREATE" respectively so the comments match the updated menuItems naming.src/features/follow/ui/FollowModal.tsx-23-25 (1)
23-25:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win아이콘 파일명 오타 수정 필요.
/icons/follwer.svg는follower의 오타입니다. 현재 이 파일명이 실제로 저장되어 있지만, 코드와 자산명 모두follower.svg로 통일하여 가독성과 유지보수성을 개선해 주세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/follow/ui/FollowModal.tsx` around lines 23 - 25, Update the icon file name typo for the follow tabs: change the iconSrc values currently using "/icons/follwer.svg" to the correct "/icons/follower.svg" for the entries with key "following" and key "followers" in FollowModal.tsx, and rename the actual asset file to follower.svg so code and assets match.src/app/search/page.module.css-2-4 (1)
2-4:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
min-height: 480px;이 의도대로 동작하지 않을 가능성CSS 캐스케이드 특성상
100dvh를 지원하는 브라우저(현재 거의 모든 모던 브라우저)에서는 바로 위 라인의min-height: 480px;선언이 즉시 덮어써집니다. 결과적으로480px선언은dvh를 지원하지 않는 구형 브라우저용 fallback으로만 동작하므로, 화면 높이가 480px보다 작은 모바일 환경(예: 가로모드 작은 폰)에서 컨텐츠가 잘리지 않도록 "최소 480px 보장"이 의도였다면 아래처럼max()로 합쳐야 합니다.🛠 제안 수정
background-color: `#f5f5f7`; - min-height: 480px; - min-height: 100dvh; + min-height: 480px; /* dvh 미지원 브라우저용 fallback */ + min-height: max(480px, 100dvh);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/search/page.module.css` around lines 2 - 4, 현재 CSS에 동일한 선택자에 대해 두 개의 min-height 선언이 있어 최신 브라우저에서 `min-height: 100dvh;`가 `min-height: 480px;`를 덮어써 의도한 "최소 480px 보장"이 되지 않습니다; src/app/search/page.module.css에서 중복된 min-height 선언을 하나로 합쳐 `min-height: max(480px, 100dvh)`처럼 작성해 구형 브라우저용 폴백과 최신 브라우저의 dvh 값을 함께 보장하도록 바꾸세요.
🧹 Nitpick comments (18)
src/shared/ui/radial-control/RadialControl.module.css (1)
3-7: ⚡ Quick win하드코딩된 색상 값을 CSS 변수(디자인 토큰)로 교체하는 것을 권장합니다.
#b0b0b5가 line 21, 90, 99, 104에 반복 등장하고,#d9d9d9,#7c7c84,#f5f5f7,#ffffff도 분산되어 있습니다. 동일한 색상이 여러 곳에 흩어져 있으면, 향후 테마 변경이나 다크 모드 대응 시 모든 위치를 일일이 수정해야 합니다. CSS 변수로 통일하면 단일 지점에서 관리 가능합니다.+:root { + --color-control-border: `#d9d9d9`; + --color-control-bg: `#ffffff`; + --color-slot-default: `#b0b0b5`; + --color-slot-hover: `#7c7c84`; + --color-center-bg: `#f5f5f7`; +} .control { - border: 1px solid `#d9d9d9`; - background: `#ffffff`; + border: 1px solid var(--color-control-border); + background: var(--color-control-bg); } .slot > * { - color: `#b0b0b5`; + color: var(--color-slot-default); } .topSlot > *:hover, .bottomSlot > *:hover { - color: `#7c7c84`; + color: var(--color-slot-hover); } .leftSlot > *:not(:disabled):hover, .rightSlot > *:not(:disabled):hover { - background: rgba(176, 176, 181, 0.14); + background: rgba(from var(--color-slot-default) r g b / 0.14); } .centerSlot > * { - border: 1px solid `#b0b0b5`; + border: 1px solid var(--color-slot-default); background: linear-gradient( 180deg, - rgba(255, 255, 255, 0.2) 0%, - rgba(176, 176, 181, 0.2) 100% + rgba(from var(--color-control-bg) r g b / 0.2) 0%, + rgba(from var(--color-slot-default) r g b / 0.2) 100% ), - `#f5f5f7`; + var(--color-center-bg); }Also applies to: 21-21, 95-95, 99-106
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/shared/ui/radial-control/RadialControl.module.css` around lines 3 - 7, In src/shared/ui/radial-control/RadialControl.module.css replace all hardcoded hex colors (`#b0b0b5`, `#d9d9d9`, `#7c7c84`, `#f5f5f7`, `#ffffff`) with design-token CSS variables (e.g. --color-border, --color-muted, --color-foreground, --color-bg, --color-surface) and use those variables in the rules that set width/aspect-ratio/border/border-radius/background; add the variable definitions in a central place (:root or your design tokens file) and update every occurrence of `#b0b0b5` (currently repeated at multiple sites including the instances noted) to the chosen token so theming/dark-mode changes are handled from one source.src/features/follow/blocked/ui/BlockedUsersPanel.module.css (1)
1-10: ⚡ Quick win
FollowersPanel.module.css와 동일한.state규칙 중복이 파일의
.state클래스는src/features/follow/followers/ui/FollowersPanel.module.css의.state와 모든 속성이 완전히 동일합니다.FollowingList.module.css에도 유사한 패턴이 있어 앞으로 계속 복사될 가능성이 높습니다. 공유 CSS 모듈(예:src/shared/ui/emptyState.module.css)이나 공통 Tailwind 유틸리티로 추출하면 이후 스타일 불일치를 방지할 수 있습니다.♻️ 리팩토링 제안
src/shared/ui/emptyState.module.css생성:+.state { + display: flex; + min-height: 100%; + align-items: center; + justify-content: center; + color: `#8d8d8d`; + font-size: 16px; + font-weight: var(--fw-medium); + line-height: 1.4; +}
BlockedUsersPanel.module.css와FollowersPanel.module.css에서 각자.state를 제거하고 공유 모듈을 임포트하여 사용.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/follow/blocked/ui/BlockedUsersPanel.module.css` around lines 1 - 10, The .state CSS rule in BlockedUsersPanel.module.css is duplicated across FollowersPanel.module.css (and appears in FollowingList.module.css); extract this shared rule into a new module (e.g., src/shared/ui/emptyState.module.css) containing the single .state definition, remove the .state blocks from BlockedUsersPanel.module.css and FollowersPanel.module.css (and any other duplicates), and update the corresponding components to import and use the shared .state class instead (or replace usage with a common Tailwind utility if you prefer) so all components reference the centralized .state definition.src/features/follow/requests/hooks/useFollowRequestTargetStatus.ts (1)
13-24: ⚖️ Poor tradeoff
useQuery를 클라이언트 전용 상태 저장소로 사용하는 것은 안티패턴입니다.
queryFn: () => "idle"은 네트워크 호출이 없고, 실제 상태 변경은 외부에서queryClient.setQueryData()로만 이루어집니다. 코딩 가이드라인에서 TanStack Query는 서버 상태 관리 용도로 사용하도록 명시하고 있습니다.추가로
gcTime: Infinity설정은 방문한 모든targetSlug의 상태를 세션 내내 메모리에 누적하여, 장기 세션에서 메모리가 무한히 증가할 수 있습니다.권장 대안:
// Zustand store (예시) type FollowRequestStatusStore = { statuses: Map<string, FollowRequestTargetStatus>; setStatus: (slug: string, status: FollowRequestTargetStatus) => void; };또는 Jotai
atomFamily를 사용하면 슬러그별로 자동 스코핑되면서 메모리 관리도 됩니다.As per coding guidelines, "
src/**/*.{ts,tsx}: Use TanStack Query for server state".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/follow/requests/hooks/useFollowRequestTargetStatus.ts` around lines 13 - 24, The hook useFollowRequestTargetStatus currently misuses useQuery as a client-only state store (queryFn: () => "idle", relying on queryClient.setQueryData and gcTime: Infinity), so replace this implementation with a proper client-state store (e.g., a Zustand store or Jotai atomFamily) that holds a Map of slug -> FollowRequestTargetStatus and exposes setStatus/getStatus; update useFollowRequestTargetStatus to read from that store (and not call useQuery with a dummy queryFn or gcTime: Infinity), remove reliance on followRequestTargetStatusQueryKey/queryClient.setQueryData for local updates, and ensure memory is bounded by scoping state per-slug (atomFamily) or cleaning entries when no longer needed.src/features/follow/requests/hooks/useFetchReceivedFollowRequest.ts (2)
5-8: 💤 Low value타입 전용 임포트에는
import type을 사용하세요.
FetchReceivedFollowRequestsParams와ReceivedFollowRequestsResponse는 런타임에 값으로 쓰이지 않으므로import type으로 명시하면 번들러 트리셰이킹이 보장되고 의도가 명확해집니다.♻️ 제안
-import { - FetchReceivedFollowRequestsParams, - ReceivedFollowRequestsResponse, -} from "../model/types"; +import type { + FetchReceivedFollowRequestsParams, + ReceivedFollowRequestsResponse, +} from "../model/types";🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/follow/requests/hooks/useFetchReceivedFollowRequest.ts` around lines 5 - 8, The imports FetchReceivedFollowRequestsParams and ReceivedFollowRequestsResponse are types only, so change the import to a type-only import by using "import type" for those symbols in useFetchReceivedFollowRequest.ts; update the import statement to "import type { FetchReceivedFollowRequestsParams, ReceivedFollowRequestsResponse } from '../model/types'" so the bundler can tree-shake and the intent is explicit, leaving all runtime code unchanged.
14-22: ⚡ Quick win
queryKey에 파라미터를 개별 필드로 펼치면 향후 필드 추가 시 캐시 키 누락 위험이 있습니다.현재
lastId와limit만 개별 원소로 나열하고 있어,FetchReceivedFollowRequestsParams에 새 필드가 추가될 때queryKey를 업데이트하지 않으면 잘못된 캐시가 반환됩니다.params객체를 그대로 키에 포함하면 이 문제가 자동으로 해소됩니다.♻️ queryKey 구조 개선안
- return useQuery<ReceivedFollowRequestsResponse, ApiError>({ - queryKey: [ - "followRequests", - "received", - params?.lastId ?? null, - params?.limit ?? null, - ], - queryFn: () => fetchReceivedFollowRequests(params), - }); + return useQuery<ReceivedFollowRequestsResponse, ApiError>({ + queryKey: ["followRequests", "received", params ?? null], + queryFn: () => fetchReceivedFollowRequests(params), + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/follow/requests/hooks/useFetchReceivedFollowRequest.ts` around lines 14 - 22, The queryKey in useFetchReceivedFollowRequest currently spreads individual params fields ("lastId", "limit") which risks missing new fields later; update the queryKey to include the whole params object (e.g., ["followRequests","received", params ?? null]) so any future additions to FetchReceivedFollowRequestsParams are automatically part of the cache key, and keep queryFn as () => fetchReceivedFollowRequests(params) to preserve behavior for ReceivedFollowRequestsResponse/ApiError handling.src/features/room/current-requester/ui/CurrentRequesterCard.tsx (1)
74-74: 하드코딩된 플레이스홀더 "사연이 나올 영역입니다."사연(story) 표시 영역이 고정 문자열로 처리되어 있습니다. 실제 데이터 연동이 필요합니다.
새 이슈를 열어
story필드를CurrentRequesterProfile에 추가하고 이 영역에 렌더링하는 작업을 추적하시겠습니까?🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/room/current-requester/ui/CurrentRequesterCard.tsx` at line 74, The span with className styles.story in CurrentRequesterCard.tsx is using a hardcoded placeholder string; update the data model and rendering by adding a story field to the CurrentRequesterProfile type/prop and pass the real story into CurrentRequesterCard instead of the fixed text, then replace the hardcoded span content with that prop (reference symbols: CurrentRequesterProfile, CurrentRequesterCard, styles.story). Also open a tracking issue to add/propagate the story field through the upstream data fetching/props pipeline so tests and consumers are updated accordingly.src/features/follow/ui/FollowModal.module.css (1)
146-152: ⚡ Quick win
:global(.border)/:global(.cursor-pointer)— Tailwind 클래스명에 의존하는 취약한 결합Tailwind 유틸리티 클래스명(
.border,.cursor-pointer)을 CSS 모듈 내:global로 직접 타겟팅하면, 자식 컴포넌트가 해당 Tailwind 클래스를 제거·변경할 경우 스타일이 조용히 깨집니다.
addAction내부에 렌더링되는 컴포넌트가 CSS 모듈className을 prop으로 수용하거나,.addAction의 직접 자식에 전용 CSS 모듈 클래스를 부여하는 방식으로 교체하세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/follow/ui/FollowModal.module.css` around lines 146 - 152, The CSS module currently targets Tailwind utility selectors via :global(.border) and :global(.cursor-pointer) under .addAction, creating a fragile coupling; replace those global targets with a local CSS module class (e.g., addAction__child) and apply the styling there, and update the child component(s) that render inside .addAction to accept a className prop or to receive that dedicated CSS module class instead of relying on Tailwind utilities; specifically modify the selectors referencing :global(.border) and :global(.cursor-pointer) in FollowModal.module.css and change the child component usages to pass the new module class (or support className) so styles remain encapsulated.src/widgets/home/ui/HomeSearchControlDock.tsx (1)
51-75: ⚡ Quick winEscape 키로 패널 닫기 추가 권장.
외부 pointerdown으로만 닫히고 키보드 사용자가 패널을 닫을 수단이 없습니다.
keydown리스너로Escape를 함께 처리하면 접근성이 개선됩니다.useEffect(() => { if (!openPanel) { return; } function closePanelOnOutsideClick(event: PointerEvent) { /* ... */ } + function closePanelOnEscape(event: KeyboardEvent) { + if (event.key === "Escape") setOpenPanel(null); + } document.addEventListener("pointerdown", closePanelOnOutsideClick); + document.addEventListener("keydown", closePanelOnEscape); return () => { document.removeEventListener("pointerdown", closePanelOnOutsideClick); + document.removeEventListener("keydown", closePanelOnEscape); }; }, [openPanel]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/widgets/home/ui/HomeSearchControlDock.tsx` around lines 51 - 75, The effect that currently adds a pointerdown listener (useEffect containing closePanelOnOutsideClick) should also register a keydown listener to close the panel when Escape is pressed; add a handler (e.g. handleKeyDown) that checks for event.key === "Escape" and calls setOpenPanel(null), attach it alongside document.addEventListener(...), and ensure you remove the keydown listener in the effect cleanup; keep useEffect dependencies (openPanel, dockRef, setOpenPanel) consistent so the listener lifecycle matches the existing closePanelOnOutsideClick behavior.src/shared/lib/useRoomWheelNavigation.ts (2)
3-4: ⚡ Quick win기본 cooldown/threshold 값 재검토 권장.
DEFAULT_WHEEL_COOLDOWN_MS=50은 트랙패드의 관성 휠 스트림에서 의도와 달리 여러 번 navigate가 트리거될 가능성이 큽니다(50ms × 평균 휠 이벤트 간격이 비슷한 수준). 또한DEFAULT_WHEEL_SELECT_THRESHOLD=24는 디바이스별 delta 단위 편차를 고려할 때 마우스 휠 한 틱(보통 100120)에 비해 낮아 작은 스크롤도 navigate로 잡힐 수 있습니다. 실제 사용 환경(맥 트랙패드/일반 마우스)에서 한 번의 “의도된 스와이프”당 한 칸만 이동되는지 확인하고, 필요 시 cooldown은 200300ms, threshold는 40~80 정도로 상향을 검토해 주세요.Also applies to: 33-38
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/shared/lib/useRoomWheelNavigation.ts` around lines 3 - 4, The default wheel constants DEFAULT_WHEEL_COOLDOWN_MS and DEFAULT_WHEEL_SELECT_THRESHOLD in useRoomWheelNavigation.ts are too low for real devices (trackpads/mice) and can cause multiple unintended navigations; increase DEFAULT_WHEEL_COOLDOWN_MS to around 200–300 and DEFAULT_WHEEL_SELECT_THRESHOLD to around 40–80 and re-run manual tests on Mac trackpad and mouse to ensure one intentional swipe triggers a single navigation; update the definitions of DEFAULT_WHEEL_COOLDOWN_MS and DEFAULT_WHEEL_SELECT_THRESHOLD (and any other occurrences noted in the diff) accordingly and adjust related logic in useRoomWheelNavigation to rely on the new values.
23-52: 💤 Low value휠 이벤트 동축 판정 시 부호 보존 확인.
Math.abs(deltaX) > Math.abs(deltaY)로 우세 축의 raw delta를 그대로 사용하는 로직은 OK입니다. 다만 두 축이 동시에 큰 값을 갖는 사선 스와이프 상황에서 우연히 X축이 미세하게 우세하면 가로 휠로 해석되어 의도와 다른 방향이 선택될 수 있습니다. 가능하면 “수평이 일정 비율(예: 1.5×) 이상 우세할 때만 X축 사용” 같은 가드를 두는 편이 사용자 체감이 안정적입니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/shared/lib/useRoomWheelNavigation.ts` around lines 23 - 52, The wheel-axis selection in handleWheel can misclassify diagonal swipes because it uses Math.abs(deltaX) > Math.abs(deltaY) unguarded; change the decision to require a dominance ratio (e.g., const dominanceRatio = 1.5) and use deltaX only when Math.abs(deltaX) > dominanceRatio * Math.abs(deltaY), otherwise use deltaY, preserving the original sign; update the primaryDelta computation in handleWheel (and introduce a dominanceRatio constant near threshold/cooldownMs) so horizontal movement is only chosen when sufficiently dominant, then keep the existing cooldown, preventDefault, lastWheelAtRef, and onSelectRoom/nextRoomSlug/previousRoomSlug logic unchanged.src/widgets/home/ui/HomeControlPanelShell.module.css (1)
2-10: 💤 Low value
.panel의color:#f6f6f6`` + 흰 배경 조합 점검.
background:#ffffff에 `color: `#f6f6f6은 자식 요소가 색을 override하지 않을 경우 사실상 보이지 않는 텍스트가 됩니다. 현재 모든 칩이 자체color를 갖지만, 추후 자식이 추가될 때 사고를 줄이려면 패널 기본 색상을 더 어두운 값(예: 본문 텍스트 컬러)으로 두는 편이 안전합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/widgets/home/ui/HomeControlPanelShell.module.css` around lines 2 - 10, .panel의 기본 텍스트 색상이 배경(`#ffffff`)과 너무 유사하여 자식 요소가 색을 지정하지 않을 경우 가독성이 없으니, .panel 선택자에서 color: `#f6f6f6을` 더 어두운 본문용 색상으로 변경하세요 (예: color: `#111827` 또는 프로젝트의 본문 텍스트 변수 like var(--text-color)). 이렇게 하면 현재의 칩 색상에는 영향이 없으면서 향후 추가되는 자식 요소의 가독성 사고를 방지할 수 있습니다.src/app/search/page.tsx (2)
32-33: 💤 Low value불필요한 alias 제거.
roomListRooms는rooms를 단순 재할당만 하고 있어 의미 없는 가독성 저해 요소입니다. 위 필터링 적용을 안 하더라도 한쪽으로 통일하는 것을 추천합니다.- const rooms = data?.rooms ?? []; - const roomListRooms = rooms; + const roomListRooms = data?.rooms ?? [];🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/search/page.tsx` around lines 32 - 33, Remove the unnecessary alias roomListRooms which merely reassigns rooms; keep a single source of truth by using rooms everywhere instead. Replace any usages of roomListRooms with rooms (e.g., in rendering or filtering code that references roomListRooms) and delete the declaration const roomListRooms = rooms; ensure imports/types for rooms remain unchanged and run type checks to confirm no references were missed.
51-58: 💤 Low value선택된 방 메타가 캐시되지 않은 짧은 순간 썸네일/오너 정보가 깜박일 수 있습니다.
useRoomMeta(selectedRoom?.slug ?? null)로 별도 fetch가 발생하기 때문에, 사용자가 휠/드래그로 빠르게 방을 넘길 때 owner bar가 한 박자 늦게 등장/사라지는 플리커가 보일 수 있습니다. 이미useRoomsQuery로 받아온Room에 owner 요약(닉네임/프로필 이미지)이 포함되어 있다면 그 값을 우선 표시하고 메타가 도착했을 때 갱신하는 방식이 사용자 경험상 더 자연스럽습니다(없다면 스켈레톤/플레이스홀더 권장).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/search/page.tsx` around lines 51 - 58, The owner thumbnail and name flicker because useRoomMeta(selectedRoom?.slug ?? null) fetches separately; change the selection logic so you first read the owner summary from the already-fetched Room (from useRoomsQuery / selectedRoom.ownerSummary or similar on the selectedRoom object) and use that to populate selectedOwnerName and selectedOwnerImageSrc immediately, then overwrite/update them when selectedRoomMeta (from useRoomMeta) arrives; if neither source has data, render a consistent skeleton/placeholder instead of empty strings to avoid flicker. Ensure you update the variables referenced (selectedOwner, selectedOwnerName, selectedOwnerImageSrc, and the useRoomMeta usage) so that selectedRoom-derived values take precedence and selectedRoomMeta is used only to refresh/augment them when available.src/features/follow/ui/FollowModal.tsx (2)
48-60: ⚡ Quick win모달 키보드 접근성 보강 권장 (Escape, 포커스 트랩).
role="dialog"/aria-modal="true"만 부여되어 있고 Escape 키로 닫기와 포커스 트랩이 없어 키보드/스크린리더 사용자가 갇힐 수 있습니다. Escape 핸들러를 추가하고, 가능하면 첫 포커스 가능한 엘리먼트(검색 input)에 자동 포커스 + 모달 외부 포커스 이동을 막아 주세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/follow/ui/FollowModal.tsx` around lines 48 - 60, FollowModal currently returns a modal with role="dialog" but lacks keyboard accessibility; update the FollowModal component to add an Escape key handler that calls closeModal (attach in a useEffect and clean up on unmount), ensure the search input (or first focusable element) receives autofocus when open (use ref and focus when open), and implement a simple focus trap inside the modal so tab/shift+tab cycles between focusable elements (prevent focus from moving to elements outside styles.modal / styles.overlay); reference the FollowModal component, closeModal, styles.overlay, styles.modal, and the search input ref when making these changes.
20-26: TODO: 친구↔팔로우 도메인 임시 매핑.추후 팔로잉/팔로워 API가 분리될 때 라벨/패널 연결을 동시 교체해야 합니다. 누락 방지를 위해 이슈를 만들어 두는 것을 권장합니다.
원하시면 본 PR과 별개로 “팔로우 도메인 매핑 정리” 이슈를 열고 체크리스트(라벨/아이콘/패널 컴포넌트/쿼리 키)를 정리해 두겠습니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/follow/ui/FollowModal.tsx` around lines 20 - 26, The followTabs constant currently uses temporary friend↔follow domain mappings (followTabs, FollowTab with keys "following","followers","blocked"); create a dedicated issue titled e.g. "팔로우 도메인 매핑 정리" and in it list a clear checklist to update the mappings when follow/follower APIs are separated: (1) update followTabs labels and iconSrc values, (2) remap tab keys in FollowModal to the canonical follow domain keys, (3) replace/verify the panel components that each tab renders, and (4) update any associated query keys and hook usages that rely on the temporary mapping; when implementing the fix, modify the followTabs definition and any consumers of FollowTab to use the new canonical keys and labels and ensure icons and panels are corrected accordingly.src/widgets/home/ui/HomeScreen.tsx (2)
78-84: ⚡ Quick win
onEnterSelectedRoom인라인 콜백을 정리해 의도 표현 + 참조 안정성 확보
() => { if (currentRoom) { roomEntry.requestRoomEntry(currentRoom); } }는 매 렌더마다 새로 만들어지고,currentRoom이null인 경우(빈 방 목록 / 로딩 직후) 사용자가 진입 버튼을 눌러도 조용히 무시되는 흐름이 prop 안에 숨어 있습니다.useCallback+currentRoom의존성으로 추출하거나, 진입 가능 여부를 명시적인 prop(예:canEnter)으로 함께 노려두면 도크 측에서 버튼을 disabled 처리하기도 쉬워집니다.♻️ 제안 변경
+ const handleEnterSelectedRoom = useCallback(() => { + if (!currentRoom) return; + roomEntry.requestRoomEntry(currentRoom); + }, [currentRoom, roomEntry]); ... - onEnterSelectedRoom={() => { - if (currentRoom) { - roomEntry.requestRoomEntry(currentRoom); - } - }} + onEnterSelectedRoom={handleEnterSelectedRoom} + canEnterSelectedRoom={Boolean(currentRoom)}As per coding guidelines: "리스트 key 안정성, 불필요한 re-render 유발 props(익명 함수/객체) 지적".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/widgets/home/ui/HomeScreen.tsx` around lines 78 - 84, 현재 onEnterSelectedRoom에 전달된 익명 콜백 (() => { if (currentRoom) { roomEntry.requestRoomEntry(currentRoom); } })가 매 렌더마다 새로 생성되고 currentRoom이 없을 때 호출이 조용히 무시되는 문제가 있습니다; 이를 고치려면 HomeScreen에서 onEnterSelectedRoom 핸들러를 useCallback으로 추출하고 currentRoom을 의존성으로 넣어 안정적인 참조를 확보한 뒤, 동일 컴포넌트나 도크에 canEnter 같은 불리언 prop을 추가해 버튼을 disabled 처리할 수 있도록 하세요 (핸들러 참조는 onEnterSelectedRoom, 내부 호출은 roomEntry.requestRoomEntry(currentRoom)을 사용).
68-86: ⚡ Quick win
isLoading변화마다HomeSearchControlDock이 unmount/remount 됨
{!isLoading ? <HomeSearchControlDock .../> : null}패턴은 초기 로딩뿐 아니라useRoomsQueryrefetch(예: 윈도우 포커스 / 무효화 / mutation 후 invalidate)마다 도크 전체를 마운트 해제했다가 다시 만듭니다. 이로 인해
- 도크 내부 로컬 상태(드롭다운 열림, 호버, 포커스, 애니메이션 진행 상태 등)가 매번 초기화되고
- 사용자에게 컨트롤이 잠깐 사라지는 깜빡임이 보일 수 있습니다.
DOM에서 들어내기보다 prop으로 비활성/스켈레톤 상태를 내려주는 편이 안전합니다.
♻️ 제안 변경
- {!isLoading ? ( - <HomeSearchControlDock - ariaLabel="홈 하단 컨트롤" - selectedRoomSlug={selectedRoomSlug} - ... - /> - ) : null} + <HomeSearchControlDock + ariaLabel="홈 하단 컨트롤" + isLoading={isLoading} + selectedRoomSlug={selectedRoomSlug} + ... + />
HomeSearchControlDock측에서isLoading시 내부 버튼들을 disabled 처리하거나 placeholder 렌더로 분기하면 됩니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/widgets/home/ui/HomeScreen.tsx` around lines 68 - 86, Currently HomeSearchControlDock is unmounted whenever isLoading toggles because of the conditional rendering; always render <HomeSearchControlDock /> and pass the loading state as a prop (e.g., isLoading) instead of using {!isLoading ? ... : null}, then update HomeSearchControlDock to handle that prop by disabling interactive elements, rendering skeletons/placeholders, and short-circuiting side-effectful callbacks (e.g., onGoPrevious/onGoNext/onEnterSelectedRoom) while isLoading is true; this preserves internal component state (dropdowns, focus, animations) across refetches while visually showing a disabled/skeleton UI.src/app/search/page.module.css (1)
37-145: 💤 Low value클래스 네이밍 컨벤션 혼용
같은 파일 안에서 snake_case(
list_container,search_input,thumbnail_container,room_list)와 camelCase(searchHeader,backButton,contentGrid,listContent,thumbnailImageFrame,thumbnailOwnerBar)가 섞여 있어 컴포넌트 측에서styles.searchHeader/styles.search_input처럼 케이싱을 일일이 기억해야 합니다. 이번 PR에서 신규로 추가된 클래스들이 대부분 camelCase인 점을 감안해, 기존 snake_case 항목도 함께 camelCase로 정리하면 사용처(page.tsx)에서 일관된 접근 패턴을 유지할 수 있습니다 — 다만 사용처가 많을 경우 별도 후속 PR로 빼도 됩니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/search/page.module.css` around lines 37 - 145, Convert the snake_case CSS class names to camelCase to match the existing camelCase styles: rename search_input → searchInput, thumbnail_container → thumbnailContainer (and any other snake_case names like list_container or room_list if present) in this CSS module (selectors .search_input, .thumbnail_container, etc.), and update all references in the component code (e.g., page.tsx) from styles.search_input to styles.searchInput (and similarly for thumbnailContainer, listContainer, roomList) so usage is consistent with the existing camelCase classes like searchHeader, backButton, contentGrid, listContent, thumbnailImageFrame, thumbnailOwnerBar.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: f8f4df17-110c-4768-903b-8d78fd4b277e
⛔ Files ignored due to path filters (17)
public/icons/block.svgis excluded by!**/*.svgpublic/icons/follow_add.svgis excluded by!**/*.svgpublic/icons/following.svgis excluded by!**/*.svgpublic/icons/follwer.svgis excluded by!**/*.svgpublic/icons/hover_left_arrow.svgis excluded by!**/*.svgpublic/icons/left_arrow.svgis excluded by!**/*.svgpublic/icons/lock2.svgis excluded by!**/*.svgpublic/icons/onwer_black.svgis excluded by!**/*.svgpublic/icons/right_arrow.svgis excluded by!**/*.svgpublic/icons/room_left_arrow.svgis excluded by!**/*.svgpublic/icons/round_arrow.svgis excluded by!**/*.svgpublic/icons/search2.svgis excluded by!**/*.svgpublic/icons/search_arrow_g.svgis excluded by!**/*.svgpublic/icons/search_arrow_w.svgis excluded by!**/*.svgpublic/icons/search_left_arrow.svgis excluded by!**/*.svgpublic/qlofile_white.pngis excluded by!**/*.pngsrc/app/fonts/BebasNeue-Regular.ttfis excluded by!**/*.ttf
📒 Files selected for processing (100)
src/app/globals.csssrc/app/layout.tsxsrc/app/room/[slug]/RoomPageSongInfo.module.csssrc/app/room/[slug]/RoomPageSongInfo.tsxsrc/app/room/[slug]/page.tsxsrc/app/search/page.module.csssrc/app/search/page.tsxsrc/app/test/page.tsxsrc/entities/follow/model/types.tssrc/entities/room/ui/RoomInfo.module.csssrc/entities/room/ui/RoomInfo.tsxsrc/entities/room/ui/RoomStageCard.module.csssrc/entities/room/ui/RoomStageCard.tsxsrc/entities/room/ui/SearchPageRoomCard.module.csssrc/entities/room/ui/SearchPageRoomCard.tsxsrc/features/auth/login-with-google/ui/GoogleLoginButton.module.csssrc/features/auth/login-with-google/ui/GoogleLoginButton.tsxsrc/features/auth/login-with-google/ui/LoginModal.module.csssrc/features/auth/login-with-google/ui/LoginModal.tsxsrc/features/auth/login-with-google/ui/SignUpButton.module.csssrc/features/follow/blocked/ui/BlockedUsersPanel.module.csssrc/features/follow/blocked/ui/BlockedUsersPanel.tsxsrc/features/follow/followers/ui/FollowersPanel.module.csssrc/features/follow/followers/ui/FollowersPanel.tsxsrc/features/follow/following/api/fetchFollowing.tssrc/features/follow/following/hooks/useFollowingList.tssrc/features/follow/following/model/types.tssrc/features/follow/following/ui/FollowingCard.module.csssrc/features/follow/following/ui/FollowingCard.tsxsrc/features/follow/following/ui/FollowingList.module.csssrc/features/follow/following/ui/FollowingList.tsxsrc/features/follow/following/ui/FollowingPanel.tsxsrc/features/follow/requests/api/acceptFollowRequest.tssrc/features/follow/requests/api/fetchReceivedFollowRequests.tssrc/features/follow/requests/api/sendFollowRequest.tssrc/features/follow/requests/hooks/useAcceptFollowRequest.tssrc/features/follow/requests/hooks/useFetchReceivedFollowRequest.tssrc/features/follow/requests/hooks/useFollowRequestTargetStatus.tssrc/features/follow/requests/hooks/useSendFollowRequest.tssrc/features/follow/requests/model/types.tssrc/features/follow/requests/ui/AcceptFollowRequestButton.module.csssrc/features/follow/requests/ui/AcceptFollowRequestButton.tsxsrc/features/follow/requests/ui/FollowRequestButton.tsxsrc/features/follow/requests/ui/FollowRequestCard.module.csssrc/features/follow/requests/ui/FollowRequestCard.tsxsrc/features/follow/requests/ui/ReceivedFollowRequestList.module.csssrc/features/follow/requests/ui/ReceivedFollowRequestList.tsxsrc/features/follow/ui/FollowModal.module.csssrc/features/follow/ui/FollowModal.tsxsrc/features/follow/unfollow/api/unfollow.tssrc/features/follow/unfollow/hooks/useUnfollow.tssrc/features/follow/unfollow/model/types.tssrc/features/follow/unfollow/ui/UnfollowButton.module.csssrc/features/follow/unfollow/ui/UnfollowButton.tsxsrc/features/friend/list/api/fetchFriends.tssrc/features/friend/list/hooks/useFriendsList.tssrc/features/friend/list/ui/FriendCard.tsxsrc/features/friend/list/ui/FriendsList.tsxsrc/features/friend/remove/api/removeFriend.tssrc/features/friend/remove/hooks/useRemoveFriend.tssrc/features/friend/remove/model/types.tssrc/features/friend/remove/ui/RemoveFriendButton.tsxsrc/features/friend/requests/hooks/useAcceptFriendRequest.tssrc/features/friend/requests/hooks/useFetchReceivedFriendRequest.tssrc/features/friend/requests/hooks/useFriendRequestTargetStatus.tssrc/features/friend/requests/hooks/useSendFriendRequest.tssrc/features/friend/requests/ui/AcceptFriendRequestButton.tsxsrc/features/friend/requests/ui/FriendRequestCard.tsxsrc/features/friend/requests/ui/FriendsReceivedRequestList.tsxsrc/features/playlist/player/ui/YouTubePlayer.module.csssrc/features/playlist/player/ui/YouTubePlayer.tsxsrc/features/playlist/skip-track/ui/SkipTrackButton.module.csssrc/features/playlist/skip-track/ui/SkipTrackButton.tsxsrc/features/room/create/ui/RoomFormModal.tsxsrc/features/room/current-requester/ui/CurrentRequesterCard.module.csssrc/features/room/current-requester/ui/CurrentRequesterCard.tsxsrc/features/room/join/lib/roomJoinPasswordStorage.tssrc/features/room/join/model/useRoomEntry.tssrc/features/room/join/ui/RoomJoinPasswordModal.module.csssrc/features/room/join/ui/RoomJoinPasswordModal.tsxsrc/features/room/join/ui/roomPasswordInput.tsxsrc/features/room/list/ui/HomeRoomStage.module.csssrc/features/room/list/ui/HomeRoomStage.tsxsrc/features/room/profile/ui/RoomProfilePanel.tsxsrc/features/room/search/ui/RoomSearchInput.module.csssrc/features/room/search/ui/RoomSearchInput.tsxsrc/features/room/search/ui/SearchPageRoomList.module.csssrc/features/room/search/ui/SearchPageRoomList.tsxsrc/features/room/update/ui/UpdateRoomButton.module.csssrc/features/user/search/ui/UserSearchCard.tsxsrc/shared/lib/useRoomWheelNavigation.tssrc/shared/ui/radial-control/RadialControl.module.csssrc/widgets/home/ui/HomeControlPanelShell.module.csssrc/widgets/home/ui/HomeControlPanelShell.tsxsrc/widgets/home/ui/HomeScreen.module.csssrc/widgets/home/ui/HomeScreen.tsxsrc/widgets/home/ui/HomeSearchControlDock.module.csssrc/widgets/home/ui/HomeSearchControlDock.tsxsrc/widgets/home/ui/MainLogo.module.csssrc/widgets/home/ui/MainLogo.tsx
💤 Files with no reviewable changes (15)
- src/features/friend/list/api/fetchFriends.ts
- src/features/friend/requests/hooks/useSendFriendRequest.ts
- src/features/friend/remove/api/removeFriend.ts
- src/features/friend/remove/model/types.ts
- src/features/friend/list/ui/FriendCard.tsx
- src/features/friend/requests/hooks/useFriendRequestTargetStatus.ts
- src/features/friend/list/hooks/useFriendsList.ts
- src/features/friend/remove/ui/RemoveFriendButton.tsx
- src/features/friend/requests/hooks/useFetchReceivedFriendRequest.ts
- src/features/friend/requests/hooks/useAcceptFriendRequest.ts
- src/features/friend/requests/ui/FriendRequestCard.tsx
- src/features/friend/requests/ui/AcceptFriendRequestButton.tsx
- src/features/friend/remove/hooks/useRemoveFriend.ts
- src/features/friend/requests/ui/FriendsReceivedRequestList.tsx
- src/features/friend/list/ui/FriendsList.tsx
| const [roomListFilters, setRoomListFilters] = useState(DEFAULT_HOME_FILTERS); | ||
| const [isCreateRoomModalOpen, setIsCreateRoomModalOpen] = useState(false); | ||
| const [isFollowModalOpen, setIsFollowModalOpen] = useState(false); | ||
| const { data, isLoading, isError } = useRoomsQuery(); | ||
| const rooms = data?.rooms ?? []; | ||
| const roomListRooms = rooms; |
There was a problem hiding this comment.
roomListFilters가 실제 방 목록 렌더링에 반영되지 않습니다.
roomListFilters는 HomeSearchControlDock의 activeFilters로만 전달될 뿐, SearchPageRoomList에 넘기는 roomListRooms는 data?.rooms ?? []를 그대로 사용합니다. 즉 사용자가 Genre/Date/Participants 필터를 토글해도 목록은 변하지 않아 컨트롤이 죽은 UI가 됩니다. 서버 쿼리 파라미터로 내려보내거나 클라이언트에서 roomListFilters 기반으로 useMemo로 필터링한 결과를 SearchPageRoomList에 전달해 주세요.
♻️ 적용 예시 (클라이언트 필터링 케이스)
- const rooms = data?.rooms ?? [];
- const roomListRooms = rooms;
+ const rooms = data?.rooms ?? [];
+ const roomListRooms = useMemo(
+ () => applyHomeFilters(rooms, roomListFilters),
+ [rooms, roomListFilters],
+ );As per coding guidelines, "파생 값은 state로 두지 말고 계산(useMemo는 필요할 때만)" 원칙에 따라 필터 결과는 useMemo로 도출하시면 됩니다.
Also applies to: 60-67, 114-120
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/search/page.tsx` around lines 28 - 33, roomListFilters is never
applied to the rendered list—useRoomsQuery() returns data?.rooms but
roomListRooms is left unfiltered; update the component to derive a filteredRooms
value (using useMemo) from rooms and roomListFilters (e.g., filter by
genre/date/participants) and pass that filteredRooms into SearchPageRoomList
instead of roomListRooms, or alternatively wire roomListFilters into the server
query by passing it as params to useRoomsQuery; touch symbols: roomListFilters,
setRoomListFilters, useRoomsQuery, rooms, roomListRooms, HomeSearchControlDock,
SearchPageRoomList, and ensure not to store derived state but compute it with
useMemo.
|
|
||
| <div className={styles.meta}> | ||
| <div className={styles.nickname}>{user.nickname}</div> | ||
| <div className={styles.status}>온라인</div> |
There was a problem hiding this comment.
온라인 상태 하드코딩 — 실제 데이터와 무관하게 항상 "온라인" 표시
FollowingUser 타입에 온라인 상태 필드가 없는 상태에서 모든 유저를 무조건 "온라인"으로 표시하면 사용자에게 잘못된 정보를 제공합니다. API에서 상태 데이터를 받을 수 없는 경우라면 해당 텍스트를 제거하거나, 실제 상태가 구현될 때까지 표시하지 않는 것이 바람직합니다.
🐛 수정 제안 — 상태 필드가 준비될 때까지 렌더링 제거
- <div className={styles.status}>온라인</div>
+ {/* TODO: FollowingUser에 onlineStatus 필드 추가 후 실제 상태 표시 */}📝 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={styles.status}>온라인</div> | |
| {/* TODO: FollowingUser에 onlineStatus 필드 추가 후 실제 상태 표시 */} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/follow/following/ui/FollowingCard.tsx` at line 26, The
FollowingCard component currently renders a hardcoded status div (<div
className={styles.status}>온라인</div>) which misreports users as always online;
remove this hardcoded element (or change it to conditional render only when a
real status property exists on the FollowingUser model) so the component no
longer shows "온라인" by default; locate the FollowingCard component and the
styles.status usage and either delete the div or gate it behind a check for a
status field on the FollowingUser prop until the API/status field is
implemented.
| const [query, setQuery] = useState(""); | ||
| const [selectedUser, setSelectedUser] = useState<SearchUser | null>(null); | ||
| const [activeTab, setActiveTab] = useState<FollowTab>("following"); | ||
| const { data, isLoading, isError } = useSearchUsers({ query }); | ||
| const users = data?.items ?? []; | ||
| const hasSearchQuery = query.trim().length > 0; |
There was a problem hiding this comment.
유저 검색 입력에 디바운스 미적용으로 매 입력마다 새 쿼리가 발사됩니다.
useSearchUsers({ query })의 queryKey가 ["searchUsers", query, ...]라 한 글자 입력마다 별개의 캐시 엔트리/네트워크 요청이 생성됩니다. 사용자가 “friend”를 입력하면 6번의 요청이 누적되고, 결과 도착 순서에 따라 드롭다운이 깜박일 수 있습니다. 입력값을 200~300ms 디바운스한 뒤 쿼리 키에 사용하도록 변경해 주세요.
♻️ 디바운싱 적용 예시
- const [query, setQuery] = useState("");
+ const [query, setQuery] = useState("");
+ const [debouncedQuery, setDebouncedQuery] = useState("");
+ useEffect(() => {
+ const timer = window.setTimeout(() => setDebouncedQuery(query), 250);
+ return () => window.clearTimeout(timer);
+ }, [query]);
/* ... */
- const { data, isLoading, isError } = useSearchUsers({ query });
+ const { data, isLoading, isError } = useSearchUsers({ query: debouncedQuery });Also applies to: 70-80
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/follow/ui/FollowModal.tsx` around lines 29 - 34, The search
input currently calls useSearchUsers with the raw query on every keystroke,
creating a new queryKey (["searchUsers", query, ...]) and firing many requests;
debounce the query state (200–300ms) and pass the debounced value to
useSearchUsers instead. Add or reuse a debounce hook/utility (e.g., useDebounce
or lodash debounce) to derive debouncedQuery from query, update FollowModal to
call useSearchUsers({ query: debouncedQuery }) and keep setQuery as the
immediate input setter so the UI stays responsive while network calls are
rate-limited.
| <div className={styles.content}> | ||
| <aside className={styles.sidebar} aria-label="팔로우 탭"> | ||
| {followTabs.map((tab) => ( | ||
| <button | ||
| key={tab.key} | ||
| type="button" | ||
| className={styles.tabButton} | ||
| data-active={activeTab === tab.key} | ||
| onClick={() => setActiveTab(tab.key)} | ||
| > | ||
| <span | ||
| className={styles.tabIcon} | ||
| style={ | ||
| { | ||
| "--tab-icon-src": `url(${tab.iconSrc})`, | ||
| } as CSSProperties | ||
| } | ||
| aria-hidden="true" | ||
| /> | ||
| {tab.label} | ||
| </button> | ||
| ))} | ||
| </aside> | ||
| <div className={styles.tabPanel}>{tabPanels[activeTab]}</div> | ||
| </div> |
There was a problem hiding this comment.
탭 UI에 ARIA tablist 시맨틱 누락.
현재 탭은 data-active만으로 활성 상태를 표시하고 role="tablist"/role="tab"/aria-selected/aria-controls이 없어 스크린리더 사용자에게 탭 그룹임이 전달되지 않습니다. 또한 좌우 화살표 키보드 탐색도 동작하지 않습니다.
- <aside className={styles.sidebar} aria-label="팔로우 탭">
+ <aside className={styles.sidebar} aria-label="팔로우 탭" role="tablist">
{followTabs.map((tab) => (
<button
key={tab.key}
type="button"
+ role="tab"
+ id={`follow-tab-${tab.key}`}
+ aria-selected={activeTab === tab.key}
+ aria-controls={`follow-tabpanel-${tab.key}`}
className={styles.tabButton}
data-active={activeTab === tab.key}
onClick={() => setActiveTab(tab.key)}
>
{/* ... */}
</button>
))}
</aside>
- <div className={styles.tabPanel}>{tabPanels[activeTab]}</div>
+ <div
+ className={styles.tabPanel}
+ role="tabpanel"
+ id={`follow-tabpanel-${activeTab}`}
+ aria-labelledby={`follow-tab-${activeTab}`}
+ >
+ {tabPanels[activeTab]}
+ </div>📝 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={styles.content}> | |
| <aside className={styles.sidebar} aria-label="팔로우 탭"> | |
| {followTabs.map((tab) => ( | |
| <button | |
| key={tab.key} | |
| type="button" | |
| className={styles.tabButton} | |
| data-active={activeTab === tab.key} | |
| onClick={() => setActiveTab(tab.key)} | |
| > | |
| <span | |
| className={styles.tabIcon} | |
| style={ | |
| { | |
| "--tab-icon-src": `url(${tab.iconSrc})`, | |
| } as CSSProperties | |
| } | |
| aria-hidden="true" | |
| /> | |
| {tab.label} | |
| </button> | |
| ))} | |
| </aside> | |
| <div className={styles.tabPanel}>{tabPanels[activeTab]}</div> | |
| </div> | |
| <div className={styles.content}> | |
| <aside className={styles.sidebar} aria-label="팔로우 탭" role="tablist"> | |
| {followTabs.map((tab) => ( | |
| <button | |
| key={tab.key} | |
| type="button" | |
| role="tab" | |
| id={`follow-tab-${tab.key}`} | |
| aria-selected={activeTab === tab.key} | |
| aria-controls={`follow-tabpanel-${tab.key}`} | |
| className={styles.tabButton} | |
| data-active={activeTab === tab.key} | |
| onClick={() => setActiveTab(tab.key)} | |
| > | |
| <span | |
| className={styles.tabIcon} | |
| style={ | |
| { | |
| "--tab-icon-src": `url(${tab.iconSrc})`, | |
| } as CSSProperties | |
| } | |
| aria-hidden="true" | |
| /> | |
| {tab.label} | |
| </button> | |
| ))} | |
| </aside> | |
| <div | |
| className={styles.tabPanel} | |
| role="tabpanel" | |
| id={`follow-tabpanel-${activeTab}`} | |
| aria-labelledby={`follow-tab-${activeTab}`} | |
| > | |
| {tabPanels[activeTab]} | |
| </div> | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/follow/ui/FollowModal.tsx` around lines 146 - 170, The tab UI
lacks ARIA tab semantics and keyboard navigation: wrap the tab buttons container
(aside) with role="tablist", give each button role="tab" plus
aria-selected={activeTab === tab.key} and aria-controls={`panel-${tab.key}`},
ensure each tab panel in tabPanels has id={`panel-${key}`} and role="tabpanel"
and aria-labelledby={`tab-${key}`}, and add ids on buttons like
id={`tab-${tab.key}`}; implement keyboard handling (onKeyDown on the tab buttons
or tablist) to move focus and call setActiveTab on ArrowLeft/ArrowRight (using
the followTabs array order and activeTab) so left/right arrow navigation updates
focus and selection while keeping activeTab state in sync.
| const res = await axiosInstance.delete<ApiResponse<boolean>>( | ||
| `/api/v1/friends/${targetSlug}`, | ||
| ); |
There was a problem hiding this comment.
URL 경로 파라미터는 인코딩해서 요청해야 합니다.
targetSlug를 그대로 path에 넣으면 예약문자 포함 시 잘못된 엔드포인트로 요청될 수 있습니다. encodeURIComponent로 segment 인코딩을 적용해주세요.
제안 패치
export async function unfollow({
targetSlug,
}: UnfollowParams): Promise<boolean> {
const res = await axiosInstance.delete<ApiResponse<boolean>>(
- `/api/v1/friends/${targetSlug}`,
+ `/api/v1/friends/${encodeURIComponent(targetSlug)}`,
);
return res.data.result;
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/follow/unfollow/api/unfollow.ts` around lines 8 - 10, The
request path uses targetSlug directly in
axiosInstance.delete<ApiResponse<boolean>>(`/api/v1/friends/${targetSlug}`),
which can break when targetSlug contains reserved characters; replace the raw
segment with an encoded one by applying encodeURIComponent(targetSlug) before
interpolating into the path so the DELETE goes to the correct endpoint.
| const [maxUsers, setMaxUsers] = useState(() => DEFAULT_MAX_USERS); | ||
| const [trackLimitMinutes, setTrackLimitMinutes] = useState(() => | ||
| DEFAULT_TRACK_LIMIT_MINUTES, | ||
| const [trackLimitMinutes, setTrackLimitMinutes] = useState( | ||
| () => DEFAULT_TRACK_LIMIT_MINUTES, | ||
| ); |
There was a problem hiding this comment.
선택한 인원/곡 제한 값이 제출 payload에 반영되지 않습니다.
Line [62]-Line [65]에서 상태를 유지하고 Line [279]-Line [306]에서 사용자가 값을 선택하지만, Line [119]-Line [124]의 생성 요청에는 포함되지 않아 설정이 유실됩니다. 생성/수정에서 둘 다 실제 반영되도록 연결하거나, 지원 전까지 UI를 비활성화해야 합니다.
예시 수정안
if (isCreateMode) {
createRoomMutation.mutate({
title: trimmedTitle,
password:
isPasswordEnabled && trimmedPassword ? trimmedPassword : undefined,
tags: selectedTagSlugs,
+ maxUsers: Number(maxUsers),
+ trackLimitMinutes: Number(trackLimitMinutes),
});
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.
| const [maxUsers, setMaxUsers] = useState(() => DEFAULT_MAX_USERS); | |
| const [trackLimitMinutes, setTrackLimitMinutes] = useState(() => | |
| DEFAULT_TRACK_LIMIT_MINUTES, | |
| const [trackLimitMinutes, setTrackLimitMinutes] = useState( | |
| () => DEFAULT_TRACK_LIMIT_MINUTES, | |
| ); | |
| if (isCreateMode) { | |
| createRoomMutation.mutate({ | |
| title: trimmedTitle, | |
| password: | |
| isPasswordEnabled && trimmedPassword ? trimmedPassword : undefined, | |
| tags: selectedTagSlugs, | |
| maxUsers: Number(maxUsers), | |
| trackLimitMinutes: Number(trackLimitMinutes), | |
| }); | |
| return; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/room/create/ui/RoomFormModal.tsx` around lines 62 - 65, The
selected maxUsers/trackLimitMinutes state (maxUsers, setMaxUsers,
trackLimitMinutes, setTrackLimitMinutes) is not being included in the room
create/update payload; update the submit handler (e.g., the function that sends
the create/update request such as the createRoom or onSubmit handler referenced
around the existing request code) to include maxUsers and trackLimitMinutes
values in the request body so they persist, or alternatively disable the UI
controls until backend support exists; ensure the same fields are added to both
create and update request payloads so selections are applied for both flows.
| return ( | ||
| <div | ||
| className={styles.overlay} | ||
| onClick={() => { | ||
| if (!isSubmitting) { | ||
| onClose(); | ||
| } | ||
| }} | ||
| role="presentation" | ||
| > | ||
| <form | ||
| className={styles.modal} | ||
| onClick={(event) => event.stopPropagation()} | ||
| onSubmit={handleSubmit} | ||
| role="dialog" | ||
| aria-modal="true" | ||
| aria-labelledby="room-join-password-title" | ||
| aria-describedby="room-join-password-description" | ||
| > | ||
| <div className={styles.header}> | ||
| <h2 id="room-join-password-title" className={styles.title}> | ||
| 방 비밀번호 | ||
| </h2> | ||
| <p | ||
| id="room-join-password-description" | ||
| className={styles.description} | ||
| > | ||
| 비밀번호 입력이 필요한 방입니다. 방장이 알려준 비밀번호를 | ||
| 입력해주세요. | ||
| </p> | ||
| </div> | ||
|
|
||
| <div className={styles.field}> | ||
| <input | ||
| className={styles.input} | ||
| type="password" | ||
| value={password} | ||
| onChange={(event) => setPassword(event.target.value)} | ||
| placeholder="비밀번호 입력" | ||
| disabled={isSubmitting} | ||
| aria-label="방 비밀번호" | ||
| autoFocus | ||
| /> | ||
| {errorMessage ? ( | ||
| <p className={styles.errorText}>{errorMessage}</p> | ||
| ) : null} | ||
| </div> | ||
|
|
||
| <div className={styles.actions}> | ||
| <button | ||
| type="button" | ||
| className={styles.cancelButton} | ||
| onClick={onClose} | ||
| disabled={isSubmitting} | ||
| > | ||
| 취소 | ||
| </button> | ||
| <button | ||
| type="submit" | ||
| className={styles.confirmButton} | ||
| disabled={!canSubmit} | ||
| > | ||
| {isSubmitting ? "확인 중" : "확인"} | ||
| </button> | ||
| </div> | ||
| </form> | ||
| </div> | ||
| ); |
There was a problem hiding this comment.
접근성 이슈: 모달 포커스 트랩(Focus Trap)이 구현되지 않았습니다.
모달이 열려 있는 상태에서 Tab 키를 사용하면 포커스가 배경 컨텐츠로 이동할 수 있습니다. 모달 다이얼로그가 열리면 포커스는 모달 내 첫 번째 인터랙티브 요소로 이동해야 하며, 모달 내부에서 프로그래밍적으로 트랩되어야 합니다(사용자가 배경 탭으로 Tab 이동하는 것 방지). 또한 모달은 키보드를 통해 닫을 수 있는 명확한 메커니즘(예: Escape 키, 접근 가능한 닫기 버튼)을 제공해야 합니다.
아울러 Escape 키 핸들러가 없으면 키보드 사용자가 모달에서 빠져나갈 수 없습니다.
🛠️ 수정 제안: 포커스 트랩 + Escape 키 처리
+import { useEffect, useRef, useState, type FormEvent } from "react";
import { useEffect, useState, type FormEvent } from "react";
export default function RoomJoinPasswordModal({ room, onClose, onJoined }: Props) {
+ const formRef = useRef<HTMLFormElement>(null);
// ... 기존 코드 ...
+ useEffect(() => {
+ if (!room) return;
+ const form = formRef.current;
+ if (!form) return;
+
+ function handleKeyDown(event: KeyboardEvent) {
+ if (event.key === "Escape" && !isSubmitting) {
+ onClose();
+ return;
+ }
+ if (event.key !== "Tab" || !form) return;
+
+ const focusable = Array.from(
+ form.querySelectorAll<HTMLElement>(
+ 'input:not([disabled]), button:not([disabled])'
+ )
+ );
+ const first = focusable[0];
+ const last = focusable[focusable.length - 1];
+
+ if (event.shiftKey && document.activeElement === first) {
+ event.preventDefault();
+ last.focus();
+ } else if (!event.shiftKey && document.activeElement === last) {
+ event.preventDefault();
+ first.focus();
+ }
+ }
+
+ document.addEventListener("keydown", handleKeyDown);
+ return () => document.removeEventListener("keydown", handleKeyDown);
+ }, [room, isSubmitting, onClose]);
return (
<div ...>
<form
+ ref={formRef}
...
>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/room/join/ui/RoomJoinPasswordModal.tsx` around lines 59 - 126,
Implement a keyboard-accessible focus trap and Escape-key close in the
RoomJoinPasswordModal component: store the previously focused element on open,
move focus to the first interactive element (the password input ref) when
mounted, and trap Tab/Shift+Tab within the modal by handling keydown and cycling
focus among interactive elements inside the form (use a ref for the form/modal
and query selector for focusable elements). Add an Escape key handler (keydown
listener) that calls onClose (respecting isSubmitting) and remove listeners on
unmount; ensure onClose restores focus to the previously focused element. Also
add an accessible close control (e.g., a visually-hidden or icon button that
calls onClose and is keyboard-focusable) so keyboard users have a clear close
mechanism in addition to Escape.
| const selectMenuItem = (menuItem: HomeMenuItem) => { | ||
| setOpenPanel(null); | ||
|
|
||
| if (menuItem === "CREATE") { | ||
| onCreateRoom(); | ||
| return; | ||
| } | ||
|
|
||
| if (menuItem === "FRIEND") { | ||
| onOpenFollow(); | ||
| } | ||
| }; |
There was a problem hiding this comment.
RANDOM/SETTING 메뉴 클릭이 무동작으로 패널만 닫힙니다.
menuItems에 노출된 4개 중 selectMenuItem은 CREATE/FRIEND만 처리하고 나머지(RANDOM, SETTING)는 단지 setOpenPanel(null) 효과만 발생시킵니다. 사용자에게 클릭은 가능한데 아무 일도 안 일어나는 “죽은 메뉴”로 보이므로,
- 아직 미구현이면
menuItems에서 임시로 제외하거나 disabled 처리, 또는 - 라우팅/모달 등 실제 동작을 연결
해 주세요. HomeMenuItem 유니언이 selectMenuItem 분기를 컴파일 타임에 강제하지 않으므로, exhaustive switch 패턴으로 만들면 추후 항목 추가 시 누락도 방지됩니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/widgets/home/ui/HomeSearchControlDock.tsx` around lines 77 - 88,
selectMenuItem only handles "CREATE" and "FRIEND", leaving "RANDOM" and
"SETTING" as no-ops (only setOpenPanel(null)); change selectMenuItem to an
exhaustive switch over HomeMenuItem so the compiler forces handling of all
cases, and then either implement behavior for "RANDOM" and "SETTING" (e.g., call
onRandomRoom()/openSettingsModal() or navigate to settings) or explicitly
disable those menu entries (e.g., return a disabled action or call a provided
onDisabledClick handler) instead of silently doing nothing; update or add helper
functions (onCreateRoom, onOpenFollow, new onRandomRoom/onOpenSettings) and keep
setOpenPanel usage consistent.
• ## 새 기능
개선 사항