Conversation
📝 WalkthroughWalkthroughWebSocket 기반 실시간 채팅방 참여 기능이 추가됩니다. Changes
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()
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment Tip You can make CodeRabbit's review stricter and more nitpicky using the `assertive` profile, if that's what you prefer.Change the |
Summary of ChangesHello, 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 Highlights
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
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.
| } catch { | ||
| return; | ||
| } |
There was a problem hiding this comment.
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;
}
| event = JSON.parse(body) as RoomJoinEvent; | ||
| } catch { | ||
| return; |
There was a problem hiding this comment.
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;
}There was a problem hiding this comment.
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:handlePasswordSubmit을useCallback으로 감싸면 불필요한 리렌더링을 방지할 수 있습니다.현재 함수가 매 렌더마다 재생성되어
RoomPasswordInput에 새 참조가 전달됩니다.RoomPasswordInput이React.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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (14)
package.jsonsrc/app/room/[slug]/page.tsxsrc/entities/room/api/joinRoom.tssrc/entities/room/api/joinRoom.types.tssrc/entities/room/api/websocket/publishJoinRequest.tssrc/entities/room/api/websocket/subscribeRoomEvents.tssrc/entities/room/api/websocket/subscribeUserJoinEvents.tssrc/entities/room/model/types.tssrc/entities/room/ui/RoomCard.tsxsrc/features/room/join/ui/JoinRoomButton.tsxsrc/features/room/join/ui/roomPasswordInput.tsxsrc/features/room/list/ui/RoomListTest.tsxsrc/features/user/search/ui/UserSearchCard.tsxsrc/shared/api/websocket/stompConnection.ts
💤 Files with no reviewable changes (1)
- src/features/user/search/ui/UserSearchCard.tsx
| let event: WsEvent; | ||
| try { | ||
| event = JSON.parse(body) as WsEvent; | ||
| } catch { | ||
| return; | ||
| } |
There was a problem hiding this comment.
타입 검증 없는 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.
| } catch (error) { | ||
| const err = error as ApiError; | ||
| setMessage(err.message ?? "join failed"); | ||
| setErrorCode(err.code ?? "join failed"); |
There was a problem hiding this comment.
error as ApiError 캐스팅에 타입 가드가 필요합니다.
joinRoom이 ApiError가 아닌 다른 예외를 던질 경우 err.code나 err.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.
| } 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.
| 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, | ||
| }), | ||
| ); | ||
| } |
There was a problem hiding this comment.
event.data를 WsErrorData로 캐스팅 시 방어적 검증 권장.
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.
| 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.
| <div className="text-sm"> | ||
| <span className="font-semibold">isPrivate: </span> | ||
| {isPrivate ? "true" : "false"} | ||
| </div> |
There was a problem hiding this comment.
불리언 리터럴(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.
| async function handleSubmit(event: FormEvent<HTMLFormElement>) { | ||
| event.preventDefault(); | ||
|
|
||
| const trimmedPassword = password.trim(); | ||
| if (!trimmedPassword) { | ||
| setValidationMessage("비밀번호를 입력해주세요."); | ||
| return; | ||
| } | ||
|
|
||
| setValidationMessage(""); | ||
| await onSubmit(trimmedPassword); | ||
| } |
There was a problem hiding this comment.
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.
| const client = new Client({ | ||
| brokerURL: process.env.NEXT_PUBLIC_WS_URL, | ||
| reconnectDelay: 5000, | ||
| heartbeatIncoming: 4000, | ||
| heartbeatOutgoing: 4000, | ||
| debug: (message) => { | ||
| console.log("[STOMP]", message); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
NEXT_PUBLIC_WS_URL 환경 변수 미설정 시 방어 처리 필요.
process.env.NEXT_PUBLIC_WS_URL이 undefined인 경우 @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.
Summary by CodeRabbit
릴리스 노트