Skip to content

Feat/websocket dev#13

Merged
aryu1217 merged 5 commits intomainfrom
feat/websocket-dev
Mar 20, 2026
Merged

Feat/websocket dev#13
aryu1217 merged 5 commits intomainfrom
feat/websocket-dev

Conversation

@aryu1217
Copy link
Copy Markdown
Member

@aryu1217 aryu1217 commented Mar 20, 2026

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능
    • 방 참여 기능 추가
    • 비밀번호 보호된 방 지원
    • 실시간 방 이벤트 업데이트 (WebSocket 기반)
    • 방 목록에서 비공개 상태 표시

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

WebSocket 기반 실시간 채팅방 참여 기능이 추가됩니다. RoomPage가 비동기 서버 컴포넌트에서 클라이언트 컴포넌트로 변환되고, STOMP 프로토콜을 통한 방 입장 API 및 이벤트 구독 모듈이 새로 작성되었으며, 비밀번호 입력 UI와 WebSocket 연결 관리 인프라가 도입됩니다.

Changes

Cohort / File(s) Summary
의존성 추가
package.json
@stomp/stompjs@^7.2.1 의존성 추가
WebSocket 연결 기반 시설
src/shared/api/websocket/stompConnection.ts
STOMPJS 클라이언트 초기화, 자동 재연결, 생명주기 이벤트 관리 및 리스너 등록/해제 기능 제공
방 입장 API
src/entities/room/api/joinRoom.ts, src/entities/room/api/joinRoom.types.ts
방 입장 함수: 웹소켓 연결 대기, 구독 및 발행을 통한 join 요청 처리, 타임아웃 및 오류 관리
WebSocket 이벤트 구독
src/entities/room/api/websocket/publishJoinRequest.ts, src/entities/room/api/websocket/subscribeRoomEvents.ts, src/entities/room/api/websocket/subscribeUserJoinEvents.ts
Join 요청 발행, 방 이벤트 구독, 사용자 join 이벤트 수신 및 JSON 파싱 처리
타입 정의
src/entities/room/model/types.ts
WebSocket 이벤트(WsEvent, WsErrorData) 및 미디어 재생 동기화(PlaybackStatus, PlaybackSyncData) 타입 추가
UI 컴포넌트 업데이트
src/entities/room/ui/RoomCard.tsx
isPrivate 필수 prop 추가 및 렌더링
비밀번호 입력 컴포넌트
src/features/room/join/ui/roomPasswordInput.tsx
방 입장 시 비밀번호 입력 폼: 유효성 검사, 제출 상태 관리
클라이언트 컴포넌트 변환
src/app/room/[slug]/page.tsx
서버 컴포넌트에서 클라이언트 컴포넌트로 변환, 입장 상태 관리, WebSocket 구독 생명주기 추가
리스트 및 검색 UI
src/features/room/list/ui/RoomsListTest.tsx, src/features/user/search/ui/UserSearchCard.tsx
isPrivate prop 전달, 임포트 경로 업데이트, 주석 제거

Sequence Diagram(s)

sequenceDiagram
    participant Client as 클라이언트<br/>(RoomPage)
    participant WS as WebSocket<br/>(stompConnection)
    participant Subscribe as 구독<br/>(subscribeUserJoinEvents)
    participant Server as 서버 백엔드

    Client->>WS: connectSocket()
    WS-->>Client: 연결 완료
    
    Client->>Subscribe: subscribeUserJoinEvents(slug, handlers)
    Subscribe->>WS: getSocketClient()
    WS-->>Subscribe: STOMP 클라이언트
    Subscribe->>Server: /user/playlist/events 구독
    Subscribe-->>Client: StompSubscription 반환
    
    Client->>WS: publishJoinRequest(slug, payload)
    WS->>Server: /app/room/{slug}/join 발행
    
    Server->>Server: 방 입장 로직 처리
    
    alt 성공
        Server->>Subscribe: ROOM_JOINED 이벤트
        Subscribe->>Client: handlers.onJoined(result)
        Client->>Client: 상태 업데이트: "joined"
    else 비밀번호 필요
        Server->>Subscribe: ERROR 이벤트<br/>(room.password-required)
        Subscribe->>Client: handlers.onError(error)
        Client->>Client: 상태 업데이트: "needs-password"
    else 오류 발생
        Server->>Subscribe: ERROR/ROOM_JOIN_FAILED
        Subscribe->>Client: handlers.onError(error)
        Client->>Client: 상태 업데이트: "error"
    end
    
    Client->>Client: cleanup & unsubscribe (unmount 시)
    Client->>Subscribe: subscription.unsubscribe()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #12: RoomCard.tsx 수정 — 기존 PR에서 RoomCard 컴포넌트 추가, 현재 PR에서 isPrivate 필수 prop 추가로 직접 관련됨
  • PR #11: src/app/room/[slug]/page.tsx 수정 — 같은 파일의 컴포넌트 서명 및 동작 변경(서버 ↔ 클라이언트)으로 직접 관련됨

Poem

🐰 웹소켓 연결 꾸준히,
방에 들어가고 구독하고,
JSON 파싱 신나게,
상태 관리 깔끔하게—
실시간 방 입장, 완성! ✨

🚥 Pre-merge checks | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning PR 설명이 완전히 작성되지 않았습니다. 필수 섹션인 Title, Summary, Linked Issue, Checklist이 모두 누락되었습니다. 저장소의 PR 템플릿에 따라 설명을 작성하세요. Summary, Linked Issue, Checklist을 포함하여 작성해주시기 바랍니다.
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive 제목이 모호하고 구체적이지 않습니다. 'Feat/websocket dev'는 WebSocket 개발이라는 일반적인 표현이며, PR의 주요 변경사항(room join flow, STOMP 통신, password 입력)을 명확히 나타내지 않습니다. 제목을 더 구체적으로 수정하세요. 예: 'Feat: Add STOMP WebSocket integration for room join flow' 또는 'Feat: Implement room join with password support via WebSocket'

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/websocket-dev
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can make CodeRabbit's review stricter and more nitpicky using the `assertive` profile, if that's what you prefer.

Change the reviews.profile setting to assertive to make CodeRabbit's nitpick more issues in your PRs.

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the application's real-time capabilities by integrating WebSocket functionality. It establishes a robust framework for connecting to a STOMP WebSocket server, managing room joining processes, and subscribing to dynamic room events. The RoomPage has been thoroughly refactored to leverage this new infrastructure, providing users with immediate feedback on room status and event updates, including a new mechanism for handling password-protected rooms.

Highlights

  • WebSocket Integration: Integrated @stomp/stompjs for WebSocket communication, establishing a foundational layer for real-time features.
  • Room Joining Logic: Implemented a comprehensive joinRoom function that handles WebSocket connection, subscription to user-specific join events, publishing join requests, and robust error management, including handling password-protected rooms.
  • Real-time Room Events: Developed utilities to subscribe to general room events and user-specific join events, enabling dynamic updates based on WebSocket messages.
  • Password-Protected Room Handling: Introduced UI and logic to prompt users for a password when attempting to join private rooms, enhancing security and access control.
  • UI Updates: Refactored the RoomPage to display real-time room status and event information, and updated RoomCard to show room privacy status.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

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

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 introduces WebSocket functionality using the STOMP protocol for room joining and event handling. The changes are well-structured, leveraging React hooks effectively for managing state, effects, and references. Error handling is generally robust, and the separation of concerns across multiple API files is a good design choice. The addition of the @stomp/stompjs dependency and the new components for room interaction are clearly implemented.

Comment on lines +66 to +68
} catch {
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The JSON.parse(body) operation can fail if the WebSocket message body is not valid JSON. While the try-catch prevents a crash, silently returning without logging the error might make debugging difficult if malformed messages are received. Consider logging the parsing error for better observability.

        let event: WsEvent;
        try {
          event = JSON.parse(body) as WsEvent;
        } catch (error) {
          console.error("Failed to parse WebSocket event body:", error, body);
          return;
        }

Comment on lines +28 to +30
event = JSON.parse(body) as RoomJoinEvent;
} catch {
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Similar to the page.tsx file, if JSON.parse(body) fails here, the error is silently ignored. Logging this error would be beneficial for diagnosing issues with malformed user join event messages from the WebSocket server.

    let event: RoomJoinEvent;
    try {
      event = JSON.parse(body) as RoomJoinEvent;
    } catch (error) {
      console.error("Failed to parse user join event body:", error, body);
      return;
    }

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (8)
src/entities/room/ui/RoomCard.tsx (1)

4-10: Props를 도메인 타입(Room) 기반으로 묶어 타입 드리프트를 줄여주세요.

현재 Room의 일부 필드를 수동으로 재선언하고 있어 모델 변경 시 누락/불일치가 생기기 쉽습니다. Pick<Room, ...>로 선언하면 변경 추적 지점이 단일화됩니다.

제안 diff
 import type { ReactNode } from "react";
-import type { RoomTag } from "@/src/entities/room/model/types";
+import type { Room } from "@/src/entities/room/model/types";

-type Props = {
-  title: string;
-  slug: string;
-  tags?: RoomTag[];
-  actions?: ReactNode;
-  isPrivate: boolean;
-};
+type Props = Pick<Room, "title" | "slug" | "tags" | "isPrivate"> & {
+  actions?: ReactNode;
+};

As per coding guidelines, "유지보수/확장성 관점에서 모듈 경계(의존성 방향, 책임 분리)가 적절한지 최우선으로 확인."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/entities/room/ui/RoomCard.tsx` around lines 4 - 10, Replace the
hand-typed Props with a domain-backed type to avoid drift: import the Room type
and redefine Props as Pick<Room, 'title' | 'slug' | 'tags' | 'isPrivate'> plus
keep the existing actions?: ReactNode; update the Props usage in the RoomCard
component signature (Props symbol) so fields are sourced from Room instead of
being re-declared.
src/shared/api/websocket/stompConnection.ts (1)

21-49: onConnect 이벤트도 리스너에 전파 고려.

현재 onStompError, onWebSocketError, onWebSocketClose만 리스너에 전파하고 있습니다. 연결 성공 시점을 알아야 하는 소비자가 있다면 onConnect도 리스너 패턴에 포함하는 것이 일관성 있습니다.

현재 사용처(joinRoom.ts)에서는 client.connected 폴링으로 연결 상태를 확인하고 있어 당장 필요하지 않을 수 있지만, 향후 확장성을 위해 고려해 볼 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/api/websocket/stompConnection.ts` around lines 21 - 49, Add
propagation for successful connections: when client.onConnect fires, call each
listener's onConnect handler (e.g., listener.onConnect?.()) just like you do in
client.onStompError, client.onWebSocketError, and client.onWebSocketClose;
update the client.onConnect block to log the event and iterate over
socketListeners invoking listener.onConnect if present so consumers can react to
connection establishment without polling client.connected.
src/entities/room/api/joinRoom.types.ts (1)

5-9: JoinRoomResult.data 타입 명세화 고려.

현재 data: unknown으로 정의되어 있습니다. ROOM_JOINED 이벤트의 응답 구조가 명확하다면, 도메인 타입(예: RoomJoinedData)으로 구체화하면 소비자 측에서 타입 가드 없이 안전하게 사용할 수 있습니다.

현재 구조가 유동적이라면 unknown이 적절합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/entities/room/api/joinRoom.types.ts` around lines 5 - 9, The
JoinRoomResult currently types data as unknown—if the ROOM_JOINED event response
shape is known, define a specific interface/type (e.g., RoomJoinedData)
describing fields returned by that event and replace JoinRoomResult.data:
unknown with data: RoomJoinedData (export the new type and update any callers to
import it); if the response truly varies, leave unknown but add a comment
explaining why and consider a union or generics (e.g., JoinRoomResult<T>) for
future specificity to make consumers safer when handling ROOM_JOINED payloads.
src/features/room/join/ui/roomPasswordInput.tsx (1)

33-38: 접근성(Accessibility) 개선 권장.

<input>id<label>htmlFor 연결이 없어 스크린 리더 사용자가 폼 필드를 인식하기 어렵습니다.

♿ 접근성 개선 제안
-      <label className="block text-sm font-medium">비밀번호를 입력하세요</label>
+      <label className="block text-sm font-medium" htmlFor="room-password">
+        비밀번호를 입력하세요
+      </label>
       <input
+        id="room-password"
         className="w-full border px-3 py-2"
         type="password"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/room/join/ui/roomPasswordInput.tsx` around lines 33 - 38, The
password input lacks an id and a corresponding label connection, harming
screen-reader accessibility; update the RoomPasswordInput component to add a
stable id on the <input> (e.g., "room-password-input" or a generated prop-based
id) and ensure there is a matching <label htmlFor="..."> associated with that id
(or update the existing label to use htmlFor). Locate the input using the
identifiers password, setPassword, and submitting to make the change; keep the
input's disabled, value, and onChange logic intact while only adding the id and
linking the label.
src/entities/room/api/websocket/subscribeUserJoinEvents.ts (1)

23-31: JSON 파싱 실패 시 silent return 의도 확인.

JSON.parse 실패 시 로깅 없이 조용히 반환합니다. 디버깅 시 문제 파악이 어려울 수 있으므로, 개발 환경에서는 경고 로그를 남기는 것을 고려해 보세요.

🔍 개발 환경 로깅 추가 제안
     try {
       event = JSON.parse(body) as RoomJoinEvent;
     } catch {
+      if (process.env.NODE_ENV === "development") {
+        console.warn("[subscribeUserJoinEvents] Failed to parse message body:", body);
+      }
       return;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/entities/room/api/websocket/subscribeUserJoinEvents.ts` around lines 23 -
31, In subscribeUserJoinEvents, the JSON.parse try/catch currently swallows
parse errors silently; update the catch to log a warning in development so parse
failures are visible during debugging (e.g., check NODE_ENV or an isDev flag)
and include the raw body and error message in the log; keep returning after
logging to preserve existing behavior.
src/entities/room/api/joinRoom.ts (1)

103-111: 타임아웃 값을 상수 또는 설정으로 분리하면 유지보수에 도움이 됩니다.

8000ms (join timeout)와 5000ms (socket connect timeout)가 하드코딩되어 있습니다. 환경에 따라 조정이 필요할 수 있으므로 상수로 분리하는 것을 권장합니다.

+const SOCKET_CONNECT_TIMEOUT_MS = 5000;
+const JOIN_RESPONSE_TIMEOUT_MS = 8000;
+
 // waitForSocketConnected 내부:
-async function waitForSocketConnected(timeoutMs = 5000) {
+async function waitForSocketConnected(timeoutMs = SOCKET_CONNECT_TIMEOUT_MS) {

 // joinRoom 내부:
     const timeoutId = setTimeout(() => {
       // ...
-    }, 8000);
+    }, JOIN_RESPONSE_TIMEOUT_MS);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/entities/room/api/joinRoom.ts` around lines 103 - 111, Extract the
hardcoded timeouts into named constants (e.g., JOIN_TIMEOUT_MS and
SOCKET_CONNECT_TIMEOUT_MS) or configuration values and replace the numeric
literals (8000 and 5000) with those constants where used in joinRoom.ts; update
the setTimeout that creates timeoutId (and any socket connect timeout logic) to
use JOIN_TIMEOUT_MS/SOCKET_CONNECT_TIMEOUT_MS, ensure the constants are exported
or read from environment/config so they can be tuned, and keep the existing use
of finishReject and ApiError untouched except for using the constant value in
the timeout setup.
src/app/room/[slug]/page.tsx (2)

78-106: handlePasswordSubmituseCallback으로 감싸면 불필요한 리렌더링을 방지할 수 있습니다.

현재 함수가 매 렌더마다 재생성되어 RoomPasswordInput에 새 참조가 전달됩니다. RoomPasswordInputReact.memo를 사용하거나 내부에서 onSubmit을 의존성으로 쓸 경우 불필요한 리렌더링이 발생합니다.

♻️ useCallback 적용 제안
-  async function handlePasswordSubmit(password: string) {
+  const handlePasswordSubmit = useCallback(async (password: string) => {
     if (!slug) return;
     // ... 기존 로직 동일
-  }
+  }, [slug, ensureRoomSubscription]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/room/`[slug]/page.tsx around lines 78 - 106, Wrap the
handlePasswordSubmit function in useCallback to avoid recreating it each render
and prevent unnecessary re-renders of RoomPasswordInput; import useCallback and
change the declaration to useCallback(async (password: string) => { ... },
[slug, joinRoom, ensureRoomSubscription, setStatus, setMessage, setErrorCode,
setIsSubmittingPassword]); ensure you keep the same body (including calls to
joinRoom and ensureRoomSubscription and the try/catch/finally logic) and include
all state setters and external functions used inside as dependencies so the
memoized callback updates correctly.

29-34: 초기 상태값 개선을 권장합니다.

code 상태의 초기값이 "joining..."인데, 이는 에러 코드가 아닌 상태 메시지입니다. 또한 status"joining"일 때 에러 코드는 아직 없으므로 빈 문자열("")이 더 적절해 보입니다.

-  const [code, setErrorCode] = useState("joining...");
+  const [code, setErrorCode] = useState("");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/room/`[slug]/page.tsx around lines 29 - 34, The initial state for the
error code is incorrect: change the useState for `code`/`setErrorCode` in
page.tsx so its initial value is an empty string (`""`) instead of
`"joining..."`; ensure `status` remains `"joining"` while `message` can keep
`"joining..."` (or be adjusted separately) so error code only contains an actual
error when one occurs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/room/`[slug]/page.tsx:
- Around line 63-68: The JSON.parse result is being unguardedly cast to WsEvent
which risks runtime errors when properties like event.type or event.timestamp
are missing; add a type-guard function (e.g., isWsEvent(obj): obj is WsEvent)
that checks required fields and their types, use it to validate the parsed value
before assigning to event in the try block (or log/return on failure), and
update the code paths that access event.type/event.timestamp to rely on the
validated event instance.
- Around line 92-95: The catch block in the join flow casts errors directly to
ApiError before checking; add a safe type guard in the catch handlers (the block
using setMessage and setErrorCode after calling joinRoom) to verify the thrown
value is an ApiError (e.g., instanceof ApiError or checking for .message/.code
properties) before reading err.message or err.code, and fall back to a generic
message/code when it is not an ApiError; apply the same pattern to the similar
catch at the other location (the block around lines 136-138) so setMessage and
setErrorCode never assume the error shape.

In `@src/entities/room/api/websocket/subscribeUserJoinEvents.ts`:
- Around line 47-56: Validate event.data before casting to WsErrorData in the
block handling event.type === "ERROR" || "ROOM_JOIN_FAILED": check that
event.data exists and has the expected properties (statusCode, code, message)
and types, and if any are missing or invalid, construct a safe fallback ApiError
(e.g., with a default status, generic code/message) or call handlers.onError
with a generic ApiError; update the logic around the WsErrorData cast and the
handlers.onError(new ApiError(...)) call so it never accesses undefined
properties and always passes a well-formed ApiError to handlers.onError.

In `@src/entities/room/ui/RoomCard.tsx`:
- Around line 38-41: Replace the raw boolean literal display in the RoomCard UI
with a human-readable domain label: in the RoomCard component where it currently
renders {isPrivate ? "true" : "false"}, change that to render a descriptive
string (e.g., when isPrivate is true render "비공개" or "Private", otherwise render
"공개" or "Public"); update any adjacent text like the <span> label if needed to
read nicely (e.g., "Visibility:" or "공개 여부:"), and keep the logic tied to the
existing isPrivate prop so the rendered text reflects the same boolean state.

In `@src/features/room/join/ui/roomPasswordInput.tsx`:
- Around line 17-28: handleSubmit currently awaits onSubmit(trimmedPassword)
without catching errors which can cause unhandled rejections; wrap the await
call in a try/catch inside handleSubmit, call setValidationMessage with a
user-friendly error (e.g., error.message or a generic "오류가 발생했습니다.") on failure,
optionally log the error, and keep the existing validation flow (clear message
before try or in finally) so the UI can respond safely to thrown errors from
onSubmit.

In `@src/shared/api/websocket/stompConnection.ts`:
- Around line 11-19: The code constructs a new Client with brokerURL set
directly from process.env.NEXT_PUBLIC_WS_URL which can be undefined; add
defensive validation before instantiating Client: check NEXT_PUBLIC_WS_URL, and
if it's falsy log or throw a clear error and avoid creating the Client (or set a
safe disabled/placeholder path) so `@stomp/stompjs` doesn't receive an
empty/undefined brokerURL. Apply this check around the Client creation
(reference the Client constant and its brokerURL option) and ensure any
consumers handle the "no WS URL" case.

---

Nitpick comments:
In `@src/app/room/`[slug]/page.tsx:
- Around line 78-106: Wrap the handlePasswordSubmit function in useCallback to
avoid recreating it each render and prevent unnecessary re-renders of
RoomPasswordInput; import useCallback and change the declaration to
useCallback(async (password: string) => { ... }, [slug, joinRoom,
ensureRoomSubscription, setStatus, setMessage, setErrorCode,
setIsSubmittingPassword]); ensure you keep the same body (including calls to
joinRoom and ensureRoomSubscription and the try/catch/finally logic) and include
all state setters and external functions used inside as dependencies so the
memoized callback updates correctly.
- Around line 29-34: The initial state for the error code is incorrect: change
the useState for `code`/`setErrorCode` in page.tsx so its initial value is an
empty string (`""`) instead of `"joining..."`; ensure `status` remains
`"joining"` while `message` can keep `"joining..."` (or be adjusted separately)
so error code only contains an actual error when one occurs.

In `@src/entities/room/api/joinRoom.ts`:
- Around line 103-111: Extract the hardcoded timeouts into named constants
(e.g., JOIN_TIMEOUT_MS and SOCKET_CONNECT_TIMEOUT_MS) or configuration values
and replace the numeric literals (8000 and 5000) with those constants where used
in joinRoom.ts; update the setTimeout that creates timeoutId (and any socket
connect timeout logic) to use JOIN_TIMEOUT_MS/SOCKET_CONNECT_TIMEOUT_MS, ensure
the constants are exported or read from environment/config so they can be tuned,
and keep the existing use of finishReject and ApiError untouched except for
using the constant value in the timeout setup.

In `@src/entities/room/api/joinRoom.types.ts`:
- Around line 5-9: The JoinRoomResult currently types data as unknown—if the
ROOM_JOINED event response shape is known, define a specific interface/type
(e.g., RoomJoinedData) describing fields returned by that event and replace
JoinRoomResult.data: unknown with data: RoomJoinedData (export the new type and
update any callers to import it); if the response truly varies, leave unknown
but add a comment explaining why and consider a union or generics (e.g.,
JoinRoomResult<T>) for future specificity to make consumers safer when handling
ROOM_JOINED payloads.

In `@src/entities/room/api/websocket/subscribeUserJoinEvents.ts`:
- Around line 23-31: In subscribeUserJoinEvents, the JSON.parse try/catch
currently swallows parse errors silently; update the catch to log a warning in
development so parse failures are visible during debugging (e.g., check NODE_ENV
or an isDev flag) and include the raw body and error message in the log; keep
returning after logging to preserve existing behavior.

In `@src/entities/room/ui/RoomCard.tsx`:
- Around line 4-10: Replace the hand-typed Props with a domain-backed type to
avoid drift: import the Room type and redefine Props as Pick<Room, 'title' |
'slug' | 'tags' | 'isPrivate'> plus keep the existing actions?: ReactNode;
update the Props usage in the RoomCard component signature (Props symbol) so
fields are sourced from Room instead of being re-declared.

In `@src/features/room/join/ui/roomPasswordInput.tsx`:
- Around line 33-38: The password input lacks an id and a corresponding label
connection, harming screen-reader accessibility; update the RoomPasswordInput
component to add a stable id on the <input> (e.g., "room-password-input" or a
generated prop-based id) and ensure there is a matching <label htmlFor="...">
associated with that id (or update the existing label to use htmlFor). Locate
the input using the identifiers password, setPassword, and submitting to make
the change; keep the input's disabled, value, and onChange logic intact while
only adding the id and linking the label.

In `@src/shared/api/websocket/stompConnection.ts`:
- Around line 21-49: Add propagation for successful connections: when
client.onConnect fires, call each listener's onConnect handler (e.g.,
listener.onConnect?.()) just like you do in client.onStompError,
client.onWebSocketError, and client.onWebSocketClose; update the
client.onConnect block to log the event and iterate over socketListeners
invoking listener.onConnect if present so consumers can react to connection
establishment without polling client.connected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f819f7d0-d1e5-4234-ac6f-4596437dd2c8

📥 Commits

Reviewing files that changed from the base of the PR and between 7e1f3c7 and c7e369a.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • package.json
  • src/app/room/[slug]/page.tsx
  • src/entities/room/api/joinRoom.ts
  • src/entities/room/api/joinRoom.types.ts
  • src/entities/room/api/websocket/publishJoinRequest.ts
  • src/entities/room/api/websocket/subscribeRoomEvents.ts
  • src/entities/room/api/websocket/subscribeUserJoinEvents.ts
  • src/entities/room/model/types.ts
  • src/entities/room/ui/RoomCard.tsx
  • src/features/room/join/ui/JoinRoomButton.tsx
  • src/features/room/join/ui/roomPasswordInput.tsx
  • src/features/room/list/ui/RoomListTest.tsx
  • src/features/user/search/ui/UserSearchCard.tsx
  • src/shared/api/websocket/stompConnection.ts
💤 Files with no reviewable changes (1)
  • src/features/user/search/ui/UserSearchCard.tsx

Comment on lines +63 to +68
let event: WsEvent;
try {
event = JSON.parse(body) as WsEvent;
} catch {
return;
}
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 | 🟡 Minor

타입 검증 없는 as WsEvent 캐스팅은 런타임 오류 위험이 있습니다.

JSON.parse 결과를 검증 없이 WsEvent로 캐스팅하면 서버에서 예상과 다른 형태의 메시지가 올 때 event.type이나 event.timestamp 접근 시 undefined 관련 오류가 발생할 수 있습니다.

🛡️ 타입 가드 함수를 통한 검증 제안
+function isWsEvent(data: unknown): data is WsEvent {
+  return (
+    typeof data === "object" &&
+    data !== null &&
+    "type" in data &&
+    "timestamp" in data
+  );
+}
+
 // ensureRoomSubscription 내부에서:
-        let event: WsEvent;
         try {
-          event = JSON.parse(body) as WsEvent;
+          const parsed: unknown = JSON.parse(body);
+          if (!isWsEvent(parsed)) return;
+          setLastRoomEventType(parsed.type);
+          setLastRoomEventTime(new Date(parsed.timestamp).toLocaleTimeString());
         } catch {
           return;
         }
-
-        setLastRoomEventType(event.type);
-        setLastRoomEventTime(new Date(event.timestamp).toLocaleTimeString());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/room/`[slug]/page.tsx around lines 63 - 68, The JSON.parse result is
being unguardedly cast to WsEvent which risks runtime errors when properties
like event.type or event.timestamp are missing; add a type-guard function (e.g.,
isWsEvent(obj): obj is WsEvent) that checks required fields and their types, use
it to validate the parsed value before assigning to event in the try block (or
log/return on failure), and update the code paths that access
event.type/event.timestamp to rely on the validated event instance.

Comment on lines +92 to +95
} catch (error) {
const err = error as ApiError;
setMessage(err.message ?? "join failed");
setErrorCode(err.code ?? "join failed");
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 | 🟡 Minor

error as ApiError 캐스팅에 타입 가드가 필요합니다.

joinRoomApiError가 아닌 다른 예외를 던질 경우 err.codeerr.message 접근이 예상대로 동작하지 않을 수 있습니다. 동일한 패턴이 Lines 136-138에도 있습니다.

🛡️ instanceof 검사 추가 제안
     } catch (error) {
-      const err = error as ApiError;
-      setMessage(err.message ?? "join failed");
-      setErrorCode(err.code ?? "join failed");
+      const err = error instanceof ApiError ? error : null;
+      setMessage(err?.message ?? "join failed");
+      setErrorCode(err?.code ?? "UNKNOWN");

-      if (err.code === "room.password-required") {
+      if (err?.code === "room.password-required") {
📝 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
} catch (error) {
const err = error as ApiError;
setMessage(err.message ?? "join failed");
setErrorCode(err.code ?? "join failed");
} catch (error) {
const err = error instanceof ApiError ? error : null;
setMessage(err?.message ?? "join failed");
setErrorCode(err?.code ?? "UNKNOWN");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/room/`[slug]/page.tsx around lines 92 - 95, The catch block in the
join flow casts errors directly to ApiError before checking; add a safe type
guard in the catch handlers (the block using setMessage and setErrorCode after
calling joinRoom) to verify the thrown value is an ApiError (e.g., instanceof
ApiError or checking for .message/.code properties) before reading err.message
or err.code, and fall back to a generic message/code when it is not an ApiError;
apply the same pattern to the similar catch at the other location (the block
around lines 136-138) so setMessage and setErrorCode never assume the error
shape.

Comment on lines +47 to +56
if (event.type === "ERROR" || event.type === "ROOM_JOIN_FAILED") {
const errorData = event.data as WsErrorData;
handlers.onError(
new ApiError({
status: errorData.statusCode,
code: errorData.code,
message: errorData.message,
}),
);
}
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 | 🟡 Minor

event.dataWsErrorData로 캐스팅 시 방어적 검증 권장.

event.data as WsErrorData 캐스팅은 서버 응답이 예상 형태와 다를 경우 undefined 프로퍼티 접근으로 이어질 수 있습니다. statusCode, code, message 중 하나라도 누락되면 ApiError 생성 시 문제가 발생할 수 있습니다.

🛡️ 방어적 검증 추가 제안
     if (event.type === "ERROR" || event.type === "ROOM_JOIN_FAILED") {
       const errorData = event.data as WsErrorData;
+      const statusCode = errorData?.statusCode ?? 500;
+      const code = errorData?.code ?? "UNKNOWN_ERROR";
+      const message = errorData?.message ?? "알 수 없는 오류가 발생했습니다.";
       handlers.onError(
         new ApiError({
-          status: errorData.statusCode,
-          code: errorData.code,
-          message: errorData.message,
+          status: statusCode,
+          code,
+          message,
         }),
       );
     }
📝 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
if (event.type === "ERROR" || event.type === "ROOM_JOIN_FAILED") {
const errorData = event.data as WsErrorData;
handlers.onError(
new ApiError({
status: errorData.statusCode,
code: errorData.code,
message: errorData.message,
}),
);
}
if (event.type === "ERROR" || event.type === "ROOM_JOIN_FAILED") {
const errorData = event.data as WsErrorData;
const statusCode = errorData?.statusCode ?? 500;
const code = errorData?.code ?? "UNKNOWN_ERROR";
const message = errorData?.message ?? "알 수 없는 오류가 발생했습니다.";
handlers.onError(
new ApiError({
status: statusCode,
code,
message,
}),
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/entities/room/api/websocket/subscribeUserJoinEvents.ts` around lines 47 -
56, Validate event.data before casting to WsErrorData in the block handling
event.type === "ERROR" || "ROOM_JOIN_FAILED": check that event.data exists and
has the expected properties (statusCode, code, message) and types, and if any
are missing or invalid, construct a safe fallback ApiError (e.g., with a default
status, generic code/message) or call handlers.onError with a generic ApiError;
update the logic around the WsErrorData cast and the handlers.onError(new
ApiError(...)) call so it never accesses undefined properties and always passes
a well-formed ApiError to handlers.onError.

Comment on lines +38 to +41
<div className="text-sm">
<span className="font-semibold">isPrivate: </span>
{isPrivate ? "true" : "false"}
</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 | 🟡 Minor

불리언 리터럴(true/false) 대신 도메인 의미(공개/비공개)로 표시하는 게 좋습니다.

사용자 노출 텍스트로는 의미 전달력이 낮아 UI 해석성이 떨어집니다.

제안 diff
       <div className="text-sm">
-        <span className="font-semibold">isPrivate: </span>
-        {isPrivate ? "true" : "false"}
+        <span className="font-semibold">방 유형:</span>{" "}
+        {isPrivate ? "비공개" : "공개"}
       </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/entities/room/ui/RoomCard.tsx` around lines 38 - 41, Replace the raw
boolean literal display in the RoomCard UI with a human-readable domain label:
in the RoomCard component where it currently renders {isPrivate ? "true" :
"false"}, change that to render a descriptive string (e.g., when isPrivate is
true render "비공개" or "Private", otherwise render "공개" or "Public"); update any
adjacent text like the <span> label if needed to read nicely (e.g.,
"Visibility:" or "공개 여부:"), and keep the logic tied to the existing isPrivate
prop so the rendered text reflects the same boolean state.

Comment on lines +17 to +28
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();

const trimmedPassword = password.trim();
if (!trimmedPassword) {
setValidationMessage("비밀번호를 입력해주세요.");
return;
}

setValidationMessage("");
await onSubmit(trimmedPassword);
}
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 | 🟡 Minor

onSubmit 호출 시 에러 처리 누락.

await onSubmit(trimmedPassword) 호출 시 에러가 발생하면 unhandled promise rejection이 발생합니다. 상위 컴포넌트에서 에러를 throw하는 경우 UI가 적절히 대응하지 못할 수 있습니다.

🛡️ 에러 처리 추가 제안
   async function handleSubmit(event: FormEvent<HTMLFormElement>) {
     event.preventDefault();

     const trimmedPassword = password.trim();
     if (!trimmedPassword) {
       setValidationMessage("비밀번호를 입력해주세요.");
       return;
     }

     setValidationMessage("");
-    await onSubmit(trimmedPassword);
+    try {
+      await onSubmit(trimmedPassword);
+    } catch {
+      // 에러는 상위 컴포넌트에서 message prop으로 전달받아 표시
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/room/join/ui/roomPasswordInput.tsx` around lines 17 - 28,
handleSubmit currently awaits onSubmit(trimmedPassword) without catching errors
which can cause unhandled rejections; wrap the await call in a try/catch inside
handleSubmit, call setValidationMessage with a user-friendly error (e.g.,
error.message or a generic "오류가 발생했습니다.") on failure, optionally log the error,
and keep the existing validation flow (clear message before try or in finally)
so the UI can respond safely to thrown errors from onSubmit.

Comment on lines +11 to +19
const client = new Client({
brokerURL: process.env.NEXT_PUBLIC_WS_URL,
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
debug: (message) => {
console.log("[STOMP]", message);
},
});
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 | 🟡 Minor

NEXT_PUBLIC_WS_URL 환경 변수 미설정 시 방어 처리 필요.

process.env.NEXT_PUBLIC_WS_URLundefined인 경우 @stomp/stompjs가 빈 URL로 연결을 시도하여 런타임 에러가 발생할 수 있습니다.

🛡️ 환경 변수 검증 추가 제안
+const WS_URL = process.env.NEXT_PUBLIC_WS_URL;
+if (!WS_URL) {
+  throw new Error("NEXT_PUBLIC_WS_URL 환경 변수가 설정되지 않았습니다.");
+}
+
 const client = new Client({
-  brokerURL: process.env.NEXT_PUBLIC_WS_URL,
+  brokerURL: WS_URL,
   reconnectDelay: 5000,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/api/websocket/stompConnection.ts` around lines 11 - 19, The code
constructs a new Client with brokerURL set directly from
process.env.NEXT_PUBLIC_WS_URL which can be undefined; add defensive validation
before instantiating Client: check NEXT_PUBLIC_WS_URL, and if it's falsy log or
throw a clear error and avoid creating the Client (or set a safe
disabled/placeholder path) so `@stomp/stompjs` doesn't receive an empty/undefined
brokerURL. Apply this check around the Client creation (reference the Client
constant and its brokerURL option) and ensure any consumers handle the "no WS
URL" case.

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