Skip to content

UI update#21

Merged
aryu1217 merged 12 commits into
mainfrom
ui-update
May 6, 2026
Merged

UI update#21
aryu1217 merged 12 commits into
mainfrom
ui-update

Conversation

@aryu1217

@aryu1217 aryu1217 commented May 6, 2026

Copy link
Copy Markdown
Member

• ## 새 기능

  • 사용자 간 연결을 위한 팔로우 시스템 추가
  • 비밀번호가 설정된 방을 구분할 수 있는 잠금 아이콘 표시 추가
  • 방 탐색 화면에서 방장 정보 표시 개선
  • 팔로잉, 방 생성, 비밀번호 입력 전용 모달 추가

개선 사항

  • 검색 및 방 탐색 인터페이스 리디자인
  • 방 이동 및 탐색 경험 개선
  • 앱 전반의 시각 스타일과 레이아웃 정리

@vercel

vercel Bot commented May 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
queuing Ready Ready Preview, Comment May 6, 2026 1:51pm

@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@aryu1217 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 25 minutes and 41 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ad4568e5-2f79-4f93-8f3b-df897e6a2983

📥 Commits

Reviewing files that changed from the base of the PR and between f76b3ec and 7fc87eb.

📒 Files selected for processing (10)
  • .agents/skills/queuing-ui-flow/SKILL.md
  • docs/harness/queuing/incidents/2026-05-06-react-wheel-passive-listener.md
  • docs/harness/queuing/incidents/2026-05-06-search-room-card-inline-handler.md
  • src/entities/room/ui/SearchPageRoomCard.tsx
  • src/features/room/create/ui/RoomFormModal.module.css
  • src/features/room/create/ui/RoomFormModal.tsx
  • src/features/room/join/model/useRoomEntry.ts
  • src/features/room/list/ui/HomeRoomStage.tsx
  • src/features/room/search/ui/SearchPageRoomList.tsx
  • src/shared/lib/useRoomWheelNavigation.ts
📝 Walkthrough

코드 리뷰 분석

Walkthrough

친구 기능을 팔로우 기능으로 마이그레이션하고, 홈 화면 UI를 RadialControl 기반에서 HomeSearchControlDock 기반으로 리팩토링했으며, 방 입장 시 비밀번호 지속성, 현재 요청자 카드, 휠 네비게이션 등을 추가했습니다.

Changes

Follow 시스템 마이그레이션

레이어 / 파일(들) 요약
데이터 타입
src/entities/follow/model/types.ts, src/features/follow/*/model/types.ts
FriendFollowingUser, FriendsListResponseFollowingListResponse 타입 이름 변경. SendFollowRequestPayload, ReceivedFollowRequest, FetchReceivedFollowRequestsParams, AcceptFollowRequestParams 추가.
API 및 데이터 페칭
src/features/follow/following/api/fetchFollowing.ts, src/features/follow/following/hooks/useFollowingList.ts, src/features/follow/requests/api/*
새로운 팔로우 API 함수들 추가: fetchFollowing, 팔로우 요청 관련 API 함수들 (acceptFollowRequest, fetchReceivedFollowRequests, sendFollowRequest); 언팔로우 API 추가 (unfollow).
리액트 훅
src/features/follow/*/hooks/*
useFollowingList, useAcceptFollowRequest, useFetchReceivedFollowRequest, useFollowRequestTargetStatus, useSendFollowRequest, useUnfollow 훅 추가.
UI 컴포넌트
src/features/follow/*/ui/*
FollowingCard, FollowingList, FollowingPanel, FollowersPanel, BlockedUsersPanel, ReceivedFollowRequestList, FollowRequestCard, AcceptFollowRequestButton, FollowRequestButton, UnfollowButton, FollowModal 컴포넌트 추가/리팩토링.
기존 친구 기능 제거
src/features/friend/*
FriendsList, FriendsReceivedRequestList, RemoveFriendButton, AcceptFriendRequestButton 및 관련 API/훅 제거.
UI 통합
src/app/test/page.tsx, src/features/user/search/ui/UserSearchCard.tsx, src/features/room/profile/ui/RoomProfilePanel.tsx
친구 컴포넌트를 팔로우 컴포넌트로 교체; 훅/타입 참조 업데이트.

홈 화면 및 방 네비게이션 UI 리팩토링

레이어 / 파일(들) 요약
네비게이션 기초
src/shared/lib/useRoomWheelNavigation.ts
마우스 휠 기반 방 네비게이션을 위한 훅 추가; 임계값과 쿨다운을 지원하는 이벤트 핸들러 반환.
방 스테이지 컴포넌트
src/features/room/list/ui/HomeRoomStage.module.css, src/features/room/list/ui/HomeRoomStage.tsx
CSS 변수 기반 3D 스테이지 효과, 드래그 인터랙션, 로딩 상태 스켈레톤 애니메이션 추가; 휠 네비게이션 훅 통합; onRequestRoomEntry 콜백 추가.
방 카드 강화
src/entities/room/ui/RoomStageCard.tsx, src/entities/room/ui/RoomStageCard.module.css
useRoomMeta 훅 추가로 소유자 정보 표시; 선택 시 소유자 바 렌더링.
검색 페이지 방 카드
src/entities/room/ui/SearchPageRoomCard.tsx, src/entities/room/ui/SearchPageRoomCard.module.css
수평 레이아웃으로 리팩토링; onClick 콜백, 이미지 렌더링, 비밀번호 락 아이콘 지원 추가.
검색 페이지 방 리스트
src/features/room/search/ui/SearchPageRoomList.tsx, src/features/room/search/ui/SearchPageRoomList.module.css
휠 네비게이션 훅 통합; 높이/translate 기반 렌더링에서 훅 기반 렌더링으로 전환; onSelectRoom, onRequestRoomEntry 콜백 추가.
제어 패널
src/widgets/home/ui/HomeControlPanelShell.tsx, src/widgets/home/ui/HomeControlPanelShell.module.css
메뉴 항목 "QUE" → "RANDOM"으로 변경; activeMenuItem 제거; 버튼 기반 렌더링으로 단순화; 스타일 업데이트 (반올림된 칩, 호버 효과).
검색 제어 독
src/widgets/home/ui/HomeSearchControlDock.tsx, src/widgets/home/ui/HomeSearchControlDock.module.css
새로운 제어 도구 컴포넌트 추가; RadialControl 통합; 메뉴/필터 패널 토글; 네비게이션, 필터, 생성, 팔로우 콜백 지원.
홈 화면 리팩토링
src/widgets/home/ui/HomeScreen.tsx, src/widgets/home/ui/HomeScreen.module.css
이전 RadialControl 기반 레이아웃을 HomeSearchControlDock 기반으로 전환; 비밀번호 모달, 팔로우 모달 추가; useRoomEntry 통합; 백그라운드 색상 업데이트.
홈 상단 바
src/widgets/home/ui/MainLogo.tsx, src/widgets/home/ui/MainLogo.module.css
로고 텍스트 "QUEUING.COM" → "QUEUING.CC" 변경; 글꼴, 크기, 색상 업데이트.

방 입장 및 비밀번호 관리

레이어 / 파일(들) 요약
비밀번호 저장소
src/features/room/join/lib/roomJoinPasswordStorage.ts
sessionStorage 기반 방 입장 비밀번호 읽기/쓰기/삭제 유틸리티 추가; SSR 가드 포함.
입장 흐름 훅
src/features/room/join/model/useRoomEntry.ts
방 입장 흐름 관리 훅 추가; 비밀번호 모달 상태, 입장 요청, 완료 콜백 관리.
비밀번호 모달
src/features/room/join/ui/RoomJoinPasswordModal.tsx, src/features/room/join/ui/RoomJoinPasswordModal.module.css
새로운 비밀번호 입장 모달 컴포넌트; 입력, 제출, 에러 처리, 비밀번호 지속성 포함.
비밀번호 입력
src/features/room/join/ui/roomPasswordInput.tsx
라벨 제거; 버튼 텍스트 "입장하기" → "확인" 변경.
현재 요청자 카드
src/features/room/current-requester/ui/CurrentRequesterCard.tsx, src/features/room/current-requester/ui/CurrentRequesterCard.module.css
요청자 정보, 스킵 버튼, 아바타, 트랙 제목, 지속 시간 표시 컴포넌트 추가.
방 페이지 통합
src/app/room/[slug]/RoomPageSongInfo.tsx, src/app/room/[slug]/RoomPageSongInfo.module.css
RoomPageClientRoomPageSongInfo 이름 변경; CurrentRequesterCard 통합; YouTubePlayer 추가; 비밀번호 저장소 활용; UpdateRoomButton 추가.

검색 페이지 및 스타일링 개선

레이어 / 파일(들) 요약
검색 페이지 UI
src/app/search/page.tsx, src/app/search/page.module.css
새로운 헤더, RoomSearchInput, 동적 방 리스트, 미리보기 썸네일, 제어 독 추가; 모달 통합 (RoomFormModal, RoomJoinPasswordModal, FollowModal).
방 검색 입력
src/features/room/search/ui/RoomSearchInput.tsx, src/features/room/search/ui/RoomSearchInput.module.css
상태 관리 추가; 초기화 버튼 추가; 스타일 업데이트 (높이 감소, 배경색 변경, 텍스트 색상 변경).
글꼴 및 전역 스타일
src/app/globals.css, src/app/layout.tsx
CSS 변수 --fw-light: 300 추가; BebasNeue 로컬 글꼴 추가.
UI 컴포넌트 스타일
src/entities/room/ui/RoomInfo.tsx, src/features/auth/login-with-google/ui/*, src/features/playlist/player/ui/YouTubePlayer.tsx, src/shared/ui/radial-control/RadialControl.module.css
비밀번호 락 아이콘, Google 로그인 버튼, YouTubePlayer CSS 모듈 전환, RadialControl 스타일 업데이트 등 다양한 UI 개선.

Sequence Diagram

sequenceDiagram
    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: 방 페이지로 이동
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120분

Possibly related PRs

  • Queuing-org/frontend#12: 친구 기능을 팔로우 기능으로 직접 마이그레이션하는 PR—동일한 타입, API, 훅, UI 컴포넌트를 수정합니다.
  • Queuing-org/frontend#16: 동일한 app 레벨 레이아웃 및 검색 UI 파일들(src/app/globals.css, src/app/layout.tsx, src/app/search/page*.tsx/css)을 수정하므로 관련이 있습니다.
  • Queuing-org/frontend#14: HomeScreen/홈 방 선택 UI (HomeRoomStage, RoomStageCard, HomeScreen/HomeTopBar)를 모두 수정하고 공유 레이아웃 글꼴을 로드하므로 관련이 있습니다.

Poem

🐰 친구들이 팔로워가 되고,
휠로 방들이 춤을 추며,
비밀은 안전히 저장되고,
새로운 홈 UI가 빛난다!
큐잉의 변신, 완벽해! ✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ui-update

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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만 존재하고 기능이 없는 상태입니다.

해소 방향 두 가지 중 하나를 선택하세요:

  1. 콜백 prop 노출onQueryChange 혹은 onSearch prop을 추가하여 부모가 상태를 제어하도록 합니다.
  2. URL 동기화useRouter / useSearchParamsquery를 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 win

hover 시 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

.descriptionfont-weight가 CSS 변수 대신 하드코딩됨

이 PR에서 --fw-light: 300globals.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 win

Stylelint 오류: @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 win

Stylelint 오류: 커스텀 프로퍼티 뒤 일반 선언 전에 빈 줄 필요 (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 win

Stylelint 오류: 선언 사이의 불필요한 빈 줄 제거 (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 win

Stylelint 오류: 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 win

Stylelint 오류: 커스텀 프로퍼티 뒤에 빈 줄 추가 필요 (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 win

Stylelint 오류 2건 → CI 블로킹 가능성

두 곳에서 실제 Stylelint 오류가 발생합니다.

  1. Line 15 (declaration-empty-line-before): CSS 커스텀 프로퍼티 블록(--panel-*) 바로 뒤에 일반 선언이 오면 Stylelint가 빈 줄을 요구합니다.
  2. Line 200 (value-keyword-case): currentColorcurrentcolor(소문자)
🔧 수정 제안
 .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/... 주석을 새 메뉴 라벨로 갱신.

menuItemsQUERANDOM으로 바뀌었지만 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.svgfollower의 오타입니다. 현재 이 파일명이 실제로 저장되어 있지만, 코드와 자산명 모두 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.cssFollowersPanel.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을 사용하세요.

FetchReceivedFollowRequestsParamsReceivedFollowRequestsResponse는 런타임에 값으로 쓰이지 않으므로 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에 파라미터를 개별 필드로 펼치면 향후 필드 추가 시 캐시 키 누락 위험이 있습니다.

현재 lastIdlimit만 개별 원소로 나열하고 있어, 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 win

Escape 키로 패널 닫기 추가 권장.

외부 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

.panelcolor: #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 제거.

roomListRoomsrooms를 단순 재할당만 하고 있어 의미 없는 가독성 저해 요소입니다. 위 필터링 적용을 안 하더라도 한쪽으로 통일하는 것을 추천합니다.

-  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); } }는 매 렌더마다 새로 만들어지고, currentRoomnull인 경우(빈 방 목록 / 로딩 직후) 사용자가 진입 버튼을 눌러도 조용히 무시되는 흐름이 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} 패턴은 초기 로딩뿐 아니라 useRoomsQuery refetch(예: 윈도우 포커스 / 무효화 / 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

📥 Commits

Reviewing files that changed from the base of the PR and between 581e635 and f76b3ec.

⛔ Files ignored due to path filters (17)
  • public/icons/block.svg is excluded by !**/*.svg
  • public/icons/follow_add.svg is excluded by !**/*.svg
  • public/icons/following.svg is excluded by !**/*.svg
  • public/icons/follwer.svg is excluded by !**/*.svg
  • public/icons/hover_left_arrow.svg is excluded by !**/*.svg
  • public/icons/left_arrow.svg is excluded by !**/*.svg
  • public/icons/lock2.svg is excluded by !**/*.svg
  • public/icons/onwer_black.svg is excluded by !**/*.svg
  • public/icons/right_arrow.svg is excluded by !**/*.svg
  • public/icons/room_left_arrow.svg is excluded by !**/*.svg
  • public/icons/round_arrow.svg is excluded by !**/*.svg
  • public/icons/search2.svg is excluded by !**/*.svg
  • public/icons/search_arrow_g.svg is excluded by !**/*.svg
  • public/icons/search_arrow_w.svg is excluded by !**/*.svg
  • public/icons/search_left_arrow.svg is excluded by !**/*.svg
  • public/qlofile_white.png is excluded by !**/*.png
  • src/app/fonts/BebasNeue-Regular.ttf is excluded by !**/*.ttf
📒 Files selected for processing (100)
  • src/app/globals.css
  • src/app/layout.tsx
  • src/app/room/[slug]/RoomPageSongInfo.module.css
  • src/app/room/[slug]/RoomPageSongInfo.tsx
  • src/app/room/[slug]/page.tsx
  • src/app/search/page.module.css
  • src/app/search/page.tsx
  • src/app/test/page.tsx
  • src/entities/follow/model/types.ts
  • src/entities/room/ui/RoomInfo.module.css
  • src/entities/room/ui/RoomInfo.tsx
  • src/entities/room/ui/RoomStageCard.module.css
  • src/entities/room/ui/RoomStageCard.tsx
  • src/entities/room/ui/SearchPageRoomCard.module.css
  • src/entities/room/ui/SearchPageRoomCard.tsx
  • src/features/auth/login-with-google/ui/GoogleLoginButton.module.css
  • src/features/auth/login-with-google/ui/GoogleLoginButton.tsx
  • src/features/auth/login-with-google/ui/LoginModal.module.css
  • src/features/auth/login-with-google/ui/LoginModal.tsx
  • src/features/auth/login-with-google/ui/SignUpButton.module.css
  • src/features/follow/blocked/ui/BlockedUsersPanel.module.css
  • src/features/follow/blocked/ui/BlockedUsersPanel.tsx
  • src/features/follow/followers/ui/FollowersPanel.module.css
  • src/features/follow/followers/ui/FollowersPanel.tsx
  • src/features/follow/following/api/fetchFollowing.ts
  • src/features/follow/following/hooks/useFollowingList.ts
  • src/features/follow/following/model/types.ts
  • src/features/follow/following/ui/FollowingCard.module.css
  • src/features/follow/following/ui/FollowingCard.tsx
  • src/features/follow/following/ui/FollowingList.module.css
  • src/features/follow/following/ui/FollowingList.tsx
  • src/features/follow/following/ui/FollowingPanel.tsx
  • src/features/follow/requests/api/acceptFollowRequest.ts
  • src/features/follow/requests/api/fetchReceivedFollowRequests.ts
  • src/features/follow/requests/api/sendFollowRequest.ts
  • src/features/follow/requests/hooks/useAcceptFollowRequest.ts
  • src/features/follow/requests/hooks/useFetchReceivedFollowRequest.ts
  • src/features/follow/requests/hooks/useFollowRequestTargetStatus.ts
  • src/features/follow/requests/hooks/useSendFollowRequest.ts
  • src/features/follow/requests/model/types.ts
  • src/features/follow/requests/ui/AcceptFollowRequestButton.module.css
  • src/features/follow/requests/ui/AcceptFollowRequestButton.tsx
  • src/features/follow/requests/ui/FollowRequestButton.tsx
  • src/features/follow/requests/ui/FollowRequestCard.module.css
  • src/features/follow/requests/ui/FollowRequestCard.tsx
  • src/features/follow/requests/ui/ReceivedFollowRequestList.module.css
  • src/features/follow/requests/ui/ReceivedFollowRequestList.tsx
  • src/features/follow/ui/FollowModal.module.css
  • src/features/follow/ui/FollowModal.tsx
  • src/features/follow/unfollow/api/unfollow.ts
  • src/features/follow/unfollow/hooks/useUnfollow.ts
  • src/features/follow/unfollow/model/types.ts
  • src/features/follow/unfollow/ui/UnfollowButton.module.css
  • src/features/follow/unfollow/ui/UnfollowButton.tsx
  • src/features/friend/list/api/fetchFriends.ts
  • src/features/friend/list/hooks/useFriendsList.ts
  • src/features/friend/list/ui/FriendCard.tsx
  • src/features/friend/list/ui/FriendsList.tsx
  • src/features/friend/remove/api/removeFriend.ts
  • src/features/friend/remove/hooks/useRemoveFriend.ts
  • src/features/friend/remove/model/types.ts
  • src/features/friend/remove/ui/RemoveFriendButton.tsx
  • src/features/friend/requests/hooks/useAcceptFriendRequest.ts
  • src/features/friend/requests/hooks/useFetchReceivedFriendRequest.ts
  • src/features/friend/requests/hooks/useFriendRequestTargetStatus.ts
  • src/features/friend/requests/hooks/useSendFriendRequest.ts
  • src/features/friend/requests/ui/AcceptFriendRequestButton.tsx
  • src/features/friend/requests/ui/FriendRequestCard.tsx
  • src/features/friend/requests/ui/FriendsReceivedRequestList.tsx
  • src/features/playlist/player/ui/YouTubePlayer.module.css
  • src/features/playlist/player/ui/YouTubePlayer.tsx
  • src/features/playlist/skip-track/ui/SkipTrackButton.module.css
  • src/features/playlist/skip-track/ui/SkipTrackButton.tsx
  • src/features/room/create/ui/RoomFormModal.tsx
  • src/features/room/current-requester/ui/CurrentRequesterCard.module.css
  • src/features/room/current-requester/ui/CurrentRequesterCard.tsx
  • src/features/room/join/lib/roomJoinPasswordStorage.ts
  • src/features/room/join/model/useRoomEntry.ts
  • src/features/room/join/ui/RoomJoinPasswordModal.module.css
  • src/features/room/join/ui/RoomJoinPasswordModal.tsx
  • src/features/room/join/ui/roomPasswordInput.tsx
  • src/features/room/list/ui/HomeRoomStage.module.css
  • src/features/room/list/ui/HomeRoomStage.tsx
  • src/features/room/profile/ui/RoomProfilePanel.tsx
  • src/features/room/search/ui/RoomSearchInput.module.css
  • src/features/room/search/ui/RoomSearchInput.tsx
  • src/features/room/search/ui/SearchPageRoomList.module.css
  • src/features/room/search/ui/SearchPageRoomList.tsx
  • src/features/room/update/ui/UpdateRoomButton.module.css
  • src/features/user/search/ui/UserSearchCard.tsx
  • src/shared/lib/useRoomWheelNavigation.ts
  • src/shared/ui/radial-control/RadialControl.module.css
  • src/widgets/home/ui/HomeControlPanelShell.module.css
  • src/widgets/home/ui/HomeControlPanelShell.tsx
  • src/widgets/home/ui/HomeScreen.module.css
  • src/widgets/home/ui/HomeScreen.tsx
  • src/widgets/home/ui/HomeSearchControlDock.module.css
  • src/widgets/home/ui/HomeSearchControlDock.tsx
  • src/widgets/home/ui/MainLogo.module.css
  • src/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

Comment thread src/app/search/page.tsx
Comment on lines +28 to 33
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

roomListFilters가 실제 방 목록 렌더링에 반영되지 않습니다.

roomListFiltersHomeSearchControlDockactiveFilters로만 전달될 뿐, SearchPageRoomList에 넘기는 roomListRoomsdata?.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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

온라인 상태 하드코딩 — 실제 데이터와 무관하게 항상 "온라인" 표시

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.

Suggested change
<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.

Comment on lines +29 to +34
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

유저 검색 입력에 디바운스 미적용으로 매 입력마다 새 쿼리가 발사됩니다.

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.

Comment on lines +146 to +170
<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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

탭 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.

Suggested change
<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.

Comment on lines +8 to +10
const res = await axiosInstance.delete<ApiResponse<boolean>>(
`/api/v1/friends/${targetSlug}`,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines 62 to 65
const [maxUsers, setMaxUsers] = useState(() => DEFAULT_MAX_USERS);
const [trackLimitMinutes, setTrackLimitMinutes] = useState(() =>
DEFAULT_TRACK_LIMIT_MINUTES,
const [trackLimitMinutes, setTrackLimitMinutes] = useState(
() => DEFAULT_TRACK_LIMIT_MINUTES,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

선택한 인원/곡 제한 값이 제출 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.

Suggested change
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.

Comment on lines +59 to +126
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>
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

접근성 이슈: 모달 포커스 트랩(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.

Comment thread src/features/room/search/ui/SearchPageRoomList.tsx Outdated
Comment thread src/features/room/search/ui/SearchPageRoomList.tsx
Comment on lines +77 to +88
const selectMenuItem = (menuItem: HomeMenuItem) => {
setOpenPanel(null);

if (menuItem === "CREATE") {
onCreateRoom();
return;
}

if (menuItem === "FRIEND") {
onOpenFollow();
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

RANDOM/SETTING 메뉴 클릭이 무동작으로 패널만 닫힙니다.

menuItems에 노출된 4개 중 selectMenuItemCREATE/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.

@aryu1217 aryu1217 merged commit 9392262 into main May 6, 2026
1 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant