Skip to content

✨ Feat: 1:1 쪽지 채팅 기능 구현 + WebSocketStompBroker 설정 리펙토링 + 주석 추가#20

Merged
imjuyongp merged 8 commits intodevelopfrom
refactor/chat
Jan 26, 2026
Merged

✨ Feat: 1:1 쪽지 채팅 기능 구현 + WebSocketStompBroker 설정 리펙토링 + 주석 추가#20
imjuyongp merged 8 commits intodevelopfrom
refactor/chat

Conversation

@imjuyongp
Copy link
Copy Markdown
Member

@imjuyongp imjuyongp commented Jan 24, 2026

#️⃣ Issue Number

📝 요약(Summary)

  • ws stomp broker 설정 관련 주석 추가 + 수정 (WebSocketConfig -> WebSocketStompBrokerConfig)
  • broker prefix 수정 : topic -> sub / app -> pub (발행 구독 과정에서 가독성을 높이기 위해)
    • stomp-test.html의 prefix도 수정완료
  • simple broker 사용 (spring 내장 브로커)
  • 1:1 쪽지 기능 -> 채팅처럼 구현 (기존 웹소켓 로직 재사용)
  • chatRooms entity에 필드 추가함
  • 1:1 채팅방 chatRooms 엔티티 생성시 post 엔티티와 user 엔티티 매핑하여 메타정보 저장
  • 1:1 채팅방 생성 http api까지 개발 완료

🛠️ PR 유형

어떤 변경 사항이 있나요?

  • 새로운 기능 추가
  • 버그 수정
  • 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경)
  • 코드 리팩토링
  • 주석 추가 및 수정
  • 테스트 추가, 테스트 리팩토링
  • 파일 혹은 폴더명 수정

📸스크린샷 (선택)

💬 공유사항 to 리뷰어

  • erd 수정사항 있습니다.
  • 단체채팅 관련 http api 구현해주세요. (채팅방 crud)
  • 사용자 인증 로직 적용 필요해 보입니다.

✅ PR Checklist

PR이 다음 요구 사항을 충족하는지 확인하세요.

  • 커밋 메시지 컨벤션에 맞게 작성했습니다.
  • 변경 사항에 대한 테스트를 했습니다.(버그 수정/기능에 대한 테스트).

Summary by CodeRabbit

릴리스 노트

  • 새 기능

    • 게시물별 일대일 채팅(노트) 룸 생성 기능 추가
    • 사용자의 노트(일대일) 채팅 룸 목록 조회 기능 추가
  • 개선사항

    • 채팅에 일대일 노트 채팅과 그룹 채팅 유형 도입
    • 채팅 룸에 유형, 최대참여자, 게시물 및 참여자 메타데이터 추가
  • 기타

    • STOMP/WebSocket 엔드포인트 및 메시지 경로가 새 네임스페이스(/ws-stomp, /pub, /sub)로 갱신됨

✏️ Tip: You can customize this high-level summary in your review settings.

imjuyongp and others added 7 commits January 24, 2026 18:53
- /topic → /sub, /app → /pub prefix 통일
- /ws → /ws-stomp 엔드포인트 변경
- WebSocketConfig → WebSocketStompBrokerConfig 이름 변경 및 chat 도메인으로 이동
- ChatStompController 메시지 발송 경로 수정

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- NoteController 추가: 채팅방 생성/목록 조회 API
- ChatRoom 엔티티에 1대1 채팅용 필드 추가 (post, hostUser, guestUser)
- ChatRoomRepository에 중복 채팅방 체크 쿼리 추가
- ChatRoomService에 createNote, findMyNoteRooms 메서드 추가
- ChatRoomResponse에 1대1 채팅방 정보 필드 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 24, 2026

Walkthrough

Note(1:1) 채팅 기능과 관련 엔드포인트 및 도메인 모델이 추가되었고, WebSocket STOMP 설정이 기존 클래스에서 새로운 설정 클래스로 대체되며 STOMP 목적지 접두사가 변경되었습니다.

Changes

Cohort / File(s) 변경 요약
NoteController / 엔드포인트
src/main/java/com/be/sportizebe/domain/chat/controller/NoteController.java
신규 컨트롤러 추가: POST /api/note/rooms/{postId} (1:1 채팅방 생성/조회, 테스트 유저 사용), GET /api/note/rooms (사용자 관련 노트 목록 조회)
채팅 DTO (응답/요청)
src/main/java/com/be/sportizebe/domain/chat/dto/response/ChatRoomResponse.java, src/main/java/com/be/sportizebe/domain/chat/dto/request/ChatPresenceRequest.java
ChatRoomResponse에 chatRoomType, maxMembers, postId, hostUserId/hostUsername, guestUserId/guestUsername 필드 추가. ChatPresenceRequest 패키지 경로 변경
ChatRoom 엔티티 / 리포지토리
src/main/java/com/be/sportizebe/domain/chat/entity/ChatRoom.java, src/main/java/com/be/sportizebe/domain/chat/repository/ChatRoomRepository.java
ChatRoom에 ChatRoomType(enum NOTE,GROUP), maxMembers, post, hostUser, guestUser 필드 및 연관관계 추가. Repository에 findByPostAndHostUserAndGuestUserfindByHostUserOrGuestUser 메서드 추가
서비스 로직
src/main/java/com/be/sportizebe/domain/chat/service/ChatRoomService.java
createGroup(String), createNote(Post, User) (멱등적 1:1 노트 생성, 본인 게시물 차단), findMyNoteRooms(User) 추가
WebSocket 설정 교체
src/main/java/com/be/sportizebe/domain/chat/websocket/config/WebSocketStompBrokerConfig.java, 삭제: src/main/java/com/be/sportizebe/global/config/WebSocketConfig.java
기존 WebSocketConfig 삭제. 신규 WebSocketStompBrokerConfig 등록: 엔드포인트 /ws-stomp, 브로커 구독 접두사 /sub, 애플리케이션 발행 접두사 /pub
WebSocket 핸들러 및 테스트
src/main/java/com/be/sportizebe/domain/chat/websocket/handler/ChatStompController.java, test/stomp-test.html
STOMP 목적지 변경: /topic/sub, /app/pub. 메시지 브로드캐스트 경로 및 테스트 HTML 동기화

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant NoteController
    participant UserRepository
    participant PostRepository
    participant ChatRoomService
    participant ChatRoomRepository
    participant Database

    Client->>NoteController: POST /api/note/rooms/{postId}
    NoteController->>UserRepository: 테스트 사용자 조회 (id=1)
    UserRepository-->>NoteController: User 반환
    NoteController->>PostRepository: post 조회 (postId)
    PostRepository-->>NoteController: Post 반환
    NoteController->>ChatRoomService: createNote(post, guestUser)
    ChatRoomService->>ChatRoomRepository: findByPostAndHostUserAndGuestUser(post, host, guest)
    ChatRoomRepository-->>ChatRoomService: Optional<ChatRoom>
    alt 없음
        ChatRoomService->>ChatRoomRepository: save(new NOTE ChatRoom)
        ChatRoomRepository->>Database: 저장
        Database-->>ChatRoomRepository: 저장 완료
    end
    ChatRoomRepository-->>ChatRoomService: ChatRoom 반환
    ChatRoomService-->>NoteController: ChatRoom 반환
    NoteController-->>Client: 201 Created + ChatRoomResponse

    Client->>NoteController: GET /api/note/rooms
    NoteController->>UserRepository: 테스트 사용자 조회 (id=1)
    UserRepository-->>NoteController: User 반환
    NoteController->>ChatRoomService: findMyNoteRooms(user)
    ChatRoomService->>ChatRoomRepository: findByHostUserOrGuestUser(host, guest)
    ChatRoomRepository->>Database: 쿼리
    Database-->>ChatRoomRepository: List<ChatRoom>
    ChatRoomRepository-->>ChatRoomService: List<ChatRoom>
    ChatRoomService-->>NoteController: List<ChatRoom>
    NoteController-->>Client: 200 OK + List<ChatRoomResponse>
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 분

Possibly related PRs

Poem

🐰
새 노트방 만들었네, 살며시 톡을 켜고,
NOTE와 GROUP 나뉘어 춤추는 길,
/ws-stomp로 뛰어가고 /sub로 속삭여,
작은 당근 하나로 연결된 우리들,
홀이 홀이, 채팅은 살랑살랑 🎋

🚥 Pre-merge checks | ✅ 5 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 1:1 쪽지 채팅 기능 구현, WebSocketStompBroker 설정 리펙토링, 주석 추가라는 핵심 변경 사항을 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed PR이 #15의 1:1 채팅 기능 구현과 채팅 도메인 리펙토링, #19의 채팅방 정원 정보 추가를 모두 충족합니다.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 1:1 쪽지 채팅 기능, WebSocket 리펙토링, 채팅 도메인 개선과 관련된 범위 내의 작업입니다.
Description check ✅ Passed PR이 변경 사항의 목적, 주요 수정 내역, 리뷰 담당자를 위한 추가 주석 등을 명확하게 설명하고 있습니다.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

@imjuyongp imjuyongp changed the title ✨ Feat: 1:1 쪽지 채팅 기능 + WebSocketStompBroker 설정 리펙토링 + 주석 추가 ✨ Feat: 1:1 쪽지 채팅 기능 구현 + WebSocketStompBroker 설정 리펙토링 + 주석 추가 Jan 24, 2026
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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/be/sportizebe/domain/chat/dto/request/ChatPresenceRequest.java (1)

9-11: JOIN/LEAVE 요청 필드명 불일치로 null 바인딩 발생

ChatPresenceRequestuserId/nickname을 정의하고 있으나, 테스트 클라이언트와 ChatSendRequestsenderUserId/senderNickname을 사용 중입니다. 클라이언트가 {senderUserId: 1, senderNickname: "hoon"}을 전송할 때 DTO의 해당 필드는 null로 바인딩되어, ChatStompControllerjoin()leave() 메서드에서 null 값이 그대로 사용됩니다. 그 결과 "null 님이 입장했습니다" 같은 잘못된 메시지가 생성되고 세션 레지스트리에 null 값이 저장됩니다.

🔧 제안 수정 (JsonAlias로 하위호환)
+import com.fasterxml.jackson.annotation.JsonAlias;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 
 `@Getter`
 `@NoArgsConstructor`
 public class ChatPresenceRequest {
     private Long roomId;
+    `@JsonAlias`("senderUserId")
     private Long userId;
+    `@JsonAlias`("senderNickname")
     private String nickname;
 }
🤖 Fix all issues with AI agents
In `@src/main/java/com/be/sportizebe/domain/chat/controller/NoteController.java`:
- Around line 36-38: Replace the hardcoded test user lookup in NoteController
with authentication-based user resolution: modify the controller methods (e.g.,
createChatRoom and any other methods referencing guestUser at the other
occurrence) to accept `@AuthenticationPrincipal` CustomUserDetails userDetails as
a parameter and obtain the User via userDetails.getUser() (or via
userRepository.findById(userDetails.getId()) if necessary), then remove the User
guestUser = userRepository.findById(1L)... hardcoded lines and use the
authenticated user instead.

In
`@src/main/java/com/be/sportizebe/domain/chat/dto/response/ChatRoomResponse.java`:
- Around line 32-42: ChatRoomResponse.from() accesses lazy associations (post,
hostUser, guestUser) outside a transactional context when called from
NoteController.createChatRoom() and NoteController.getMyChatRooms(), causing
LazyInitializationException; fix by ensuring these associations are initialized
before mapping: either add `@Transactional`(readOnly = true) to the controller
methods createChatRoom() and getMyChatRooms(), or modify the repository query
that loads ChatRoom (use JOIN FETCH or `@EntityGraph` on the repository method
used by the service) so post/hostUser/guestUser are eagerly fetched, or
initialize the lazy fields inside the service method (e.g., touch
getPost()/getHostUser()/getGuestUser() or use Hibernate.initialize) before
calling ChatRoomResponse.from().

In `@src/main/java/com/be/sportizebe/domain/chat/entity/ChatRoom.java`:
- Around line 31-33: The createGroup() flow currently saves a ChatRoom without
setting the non-nullable field maxMembers, causing runtime constraint
violations; update the createGroup() method to assign a sensible maxMembers
value (e.g., same logic used in createNote() or a defined default) before
calling save() and ensure every ChatRoom creation path sets maxMembers. Also
annotate the chatRoomType enum field in ChatRoom with
`@Enumerated`(EnumType.STRING) to persist enum names instead of ordinals to avoid
future data corruption when enums are reordered.

In `@src/main/java/com/be/sportizebe/domain/chat/service/ChatRoomService.java`:
- Around line 32-35: Replace the reference-equality check of Long IDs in
ChatRoomService (the hostUser.getId() == guestUser.getId() block) with a
value-equality check to avoid incorrect results and caching pitfalls; use a
null-safe comparison such as Objects.equals(hostUser.getId(), guestUser.getId())
(or hostUser.getId().equals(guestUser.getId()) after null checks) and keep the
existing IllegalArgumentException throw when they match.

In
`@src/main/java/com/be/sportizebe/domain/chat/websocket/config/WebSocketStompBrokerConfig.java`:
- Around line 17-40: The WebSocket config currently ignores the injected
allowedOrigins and hardcodes "*" in registerStompEndpoints; update the
StompEndpointRegistry registration in registerStompEndpoints (the
registry.addEndpoint("/ws-stomp") call) to use the allowedOrigins property
instead of the literal "*" by passing the parsed allowedOrigins values to
setAllowedOriginPatterns (e.g., split comma-separated allowedOrigins, trim
entries, and supply as varargs) so the endpoint honors the same CORS policy as
CorsConfig and avoid allowing a global wildcard.
🧹 Nitpick comments (3)
src/main/java/com/be/sportizebe/domain/chat/entity/ChatRoom.java (1)

34-35: Enum은 STRING 저장 권장 (ordinal 위험)

기본값(ORDINAL) 저장은 enum 순서 변경 시 데이터가 깨질 수 있습니다. 안정성을 위해 STRING 저장을 권장합니다.

♻️ 제안 수정
+    `@Enumerated`(EnumType.STRING)
     private ChatRoomType chatRoomType; // 채팅방 타입 (단체, 1:1)
src/main/java/com/be/sportizebe/domain/chat/service/ChatRoomService.java (1)

52-59: findAll() 메서드의 용도 명확화 고려

findAll()이 NOTE와 GROUP 타입을 모두 반환합니다. 만약 그룹 채팅방만 조회하는 용도라면 findAllGroupRooms() 같은 명칭이나 타입 필터링을 추가하면 의도가 더 명확해집니다. 현재 설계가 의도적이라면 무시해도 됩니다.

src/main/java/com/be/sportizebe/domain/chat/controller/NoteController.java (1)

40-41: 예외 처리 일관성 개선 권장

postRepository.findById에서 IllegalArgumentException을 사용하고 있습니다. 프로젝트 전반에 커스텀 예외(예: PostNotFoundException)나 일관된 예외 처리 전략이 있다면 이에 맞춰 수정하는 것이 좋습니다. 현재 구현도 동작에는 문제가 없습니다.

Comment on lines +36 to +38
// TODO: 인증 로직 개발 후 @AuthenticationPrincipal User user로 변경
User guestUser = userRepository.findById(1L)
.orElseThrow(() -> new RuntimeException("테스트 유저가 없습니다."));
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

하드코딩된 테스트 유저 ID - 프로덕션 배포 전 반드시 제거 필요

TODO 주석으로 인지하고 계시지만, 하드코딩된 유저 ID(1L)는 보안상 심각한 문제가 될 수 있습니다. 프로덕션 배포 전에 반드시 @AuthenticationPrincipal을 사용한 인증 로직으로 교체해야 합니다.

제공된 코드 스니펫에서 CustomUserDetailServiceCustomUserDetails가 이미 구현되어 있으므로, 아래와 같이 적용 가능합니다:

🔒 인증 로직 적용 예시
`@PostMapping`("/rooms/{postId}")
public ResponseEntity<BaseResponse<ChatRoomResponse>> createChatRoom(
        `@Parameter`(description = "게시글 ID") `@PathVariable` Long postId,
        `@AuthenticationPrincipal` CustomUserDetails userDetails) {
    
    User guestUser = userDetails.getUser(); // 또는 userRepository.findById(userDetails.getId())
    // ...
}

Also applies to: 53-55

🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/chat/controller/NoteController.java`
around lines 36 - 38, Replace the hardcoded test user lookup in NoteController
with authentication-based user resolution: modify the controller methods (e.g.,
createChatRoom and any other methods referencing guestUser at the other
occurrence) to accept `@AuthenticationPrincipal` CustomUserDetails userDetails as
a parameter and obtain the User via userDetails.getUser() (or via
userRepository.findById(userDetails.getId()) if necessary), then remove the User
guestUser = userRepository.findById(1L)... hardcoded lines and use the
authenticated user instead.

Comment on lines +32 to +42
if (r.getPost() != null) {
builder.postId(r.getPost().getId());
}
if (r.getHostUser() != null) {
builder.hostUserId(r.getHostUser().getId())
.hostUsername(r.getHostUser().getUsername());
}
if (r.getGuestUser() != null) {
builder.guestUserId(r.getGuestUser().getId())
.guestUsername(r.getGuestUser().getUsername());
}
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find ChatRoom entity file
find . -type f -name "ChatRoom.java" | grep -E "(domain|entity)"

Repository: Sportize/be

Length of output: 121


🏁 Script executed:

# Search for ChatRoom entity definition to check fetch types
rg -n "@ManyToOne|@OneToOne|@OneToMany|FetchType" --type=java -A3 | grep -A10 "class ChatRoom"

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Look for the from() method in ChatRoomResponse
rg -n "from\(" --type=java -B2 -A5 src/main/java/com/be/sportizebe/domain/chat/dto/response/ChatRoomResponse.java

Repository: Sportize/be

Length of output: 439


🏁 Script executed:

# Search for service layer usage of ChatRoomResponse
rg -n "ChatRoomResponse\.from|toDTO|\.from\(" --type=java | grep -i service

Repository: Sportize/be

Length of output: 555


🏁 Script executed:

# Check for `@Transactional` annotations in chat service
find . -type f -name "*ChatService*.java" -o -name "*ChatRoomService*.java" | head -3

Repository: Sportize/be

Length of output: 129


🏁 Script executed:

# Read ChatRoom entity to check fetch types
cat src/main/java/com/be/sportizebe/domain/chat/entity/ChatRoom.java

Repository: Sportize/be

Length of output: 1552


🏁 Script executed:

# Read ChatRoomService to verify transaction handling
cat src/main/java/com/be/sportizebe/domain/chat/service/ChatRoomService.java

Repository: Sportize/be

Length of output: 2328


🏁 Script executed:

# Search for all usages of ChatRoomResponse.from() in the codebase
rg -n "ChatRoomResponse\.from|ChatRoomResponse\.builder" --type=java -B3 -A3

Repository: Sportize/be

Length of output: 1690


🏁 Script executed:

# Find controller classes that might use ChatRoomResponse
find . -type f -name "*ChatController*.java" -o -name "*ChatRoomController*.java"

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Check if there are any tests or other files that use ChatRoomResponse
rg -n "ChatRoomResponse" --type=java | head -20

Repository: Sportize/be

Length of output: 1364


🏁 Script executed:

# Read NoteController to check for `@Transactional` annotations
cat src/main/java/com/be/sportizebe/domain/chat/controller/NoteController.java

Repository: Sportize/be

Length of output: 2767


트랜잭션 컨텍스트 외부에서 호출되는 Lazy Loading 엔티티 접근 - 중대한 문제

ChatRoomResponse.from() 메서드가 NoteController의 createChatRoom() (라인 46)과 getMyChatRooms() (라인 57-58)에서 호출되는데, 컨트롤러에 @Transactional 어노테이션이 없어서 서비스 메서드의 트랜잭션이 종료된 후에 실행됩니다.

ChatRoom 엔티티의 post, hostUser, guestUser 모두 FetchType.LAZY로 설정되어 있으므로, from() 메서드의 라인 33-41에서 이들 엔티티에 접근할 때 LazyInitializationException이 발생합니다.

해결 방안:

  1. 컨트롤러 메서드에 @Transactional(readOnly = true) 추가
  2. 저장소 쿼리에 JOIN FETCH 또는 @EntityGraph 적용으로 eager loading 사용
  3. 서비스 레이어 내에서 lazy-loaded 엔티티를 먼저 초기화한 후 반환
🤖 Prompt for AI Agents
In
`@src/main/java/com/be/sportizebe/domain/chat/dto/response/ChatRoomResponse.java`
around lines 32 - 42, ChatRoomResponse.from() accesses lazy associations (post,
hostUser, guestUser) outside a transactional context when called from
NoteController.createChatRoom() and NoteController.getMyChatRooms(), causing
LazyInitializationException; fix by ensuring these associations are initialized
before mapping: either add `@Transactional`(readOnly = true) to the controller
methods createChatRoom() and getMyChatRooms(), or modify the repository query
that loads ChatRoom (use JOIN FETCH or `@EntityGraph` on the repository method
used by the service) so post/hostUser/guestUser are eagerly fetched, or
initialize the lazy fields inside the service method (e.g., touch
getPost()/getHostUser()/getGuestUser() or use Hibernate.initialize) before
calling ChatRoomResponse.from().

Comment on lines +31 to +33
@Column(nullable = false)
private Integer maxMembers; // 채팅방 최대 정원

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

🧩 Analysis chain

🏁 Script executed:

cat -n src/main/java/com/be/sportizebe/domain/chat/entity/ChatRoom.java

Repository: Sportize/be

Length of output: 2007


🏁 Script executed:

find . -type f -name "*.sql" -o -name "*Migration*" | grep -i chat | head -20

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

rg "ChatRoom" -t java --max-count=50 | grep -E "(new ChatRoom|ChatRoom\.)" | head -30

Repository: Sportize/be

Length of output: 882


🏁 Script executed:

rg "maxMembers" -t java -A 3 -B 3

Repository: Sportize/be

Length of output: 3105


🏁 Script executed:

rg "chatRoomType" -t java -A 2 -B 2

Repository: Sportize/be

Length of output: 2862


🏁 Script executed:

find . -type f \( -name "*.sql" -o -name "*.xml" \) -path "*/migration*" -o -path "*/db/*" 2>/dev/null | head -20

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

find . -type d -name "db" -o -name "migration" -o -name "flyway" -o -name "liquibase" 2>/dev/null

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

cat -n src/main/java/com/be/sportizebe/domain/chat/service/ChatRoomService.java | head -80

Repository: Sportize/be

Length of output: 2783


createGroup() 메서드에서 maxMembers를 설정해야 합니다.

createGroup() 메서드 (line 20-26)에서 maxMembers를 설정하지 않은 채로 save()를 호출하면, NOT NULL 제약으로 인해 런타임 예외가 발생합니다. createNote()처럼 maxMembers에 적절한 값을 설정하거나, 모든 생성 경로에서 반드시 설정되도록 보장해 주세요.

추가로, chatRoomType enum에 @Enumerated(EnumType.STRING) 어노테이션을 추가하여 ordinal 저장 방식을 문자열 저장으로 변경하는 것이 좋습니다. 현재 방식은 향후 enum 순서를 변경할 때 기존 데이터가 손상될 위험이 있습니다.

🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/chat/entity/ChatRoom.java` around
lines 31 - 33, The createGroup() flow currently saves a ChatRoom without setting
the non-nullable field maxMembers, causing runtime constraint violations; update
the createGroup() method to assign a sensible maxMembers value (e.g., same logic
used in createNote() or a defined default) before calling save() and ensure
every ChatRoom creation path sets maxMembers. Also annotate the chatRoomType
enum field in ChatRoom with `@Enumerated`(EnumType.STRING) to persist enum names
instead of ordinals to avoid future data corruption when enums are reordered.

Comment on lines +32 to +35
// 자기 자신에게 채팅 불가
if (hostUser.getId() == guestUser.getId()) {
throw new IllegalArgumentException("자신의 게시글에는 채팅을 시작할 수 없습니다.");
}
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 | 🔴 Critical

Long 객체 비교 시 == 대신 equals() 사용 필요

Long 타입에 == 연산자를 사용하면 객체 참조를 비교합니다. Java의 Long 캐싱은 -128 ~ 127 범위만 지원하므로, ID가 이 범위를 벗어나면 동일한 값이어도 false를 반환할 수 있습니다.

🐛 수정 제안
         // 자기 자신에게 채팅 불가
-        if (hostUser.getId() == guestUser.getId()) {
+        if (hostUser.getId().equals(guestUser.getId())) {
             throw new IllegalArgumentException("자신의 게시글에는 채팅을 시작할 수 없습니다.");
         }
🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/chat/service/ChatRoomService.java`
around lines 32 - 35, Replace the reference-equality check of Long IDs in
ChatRoomService (the hostUser.getId() == guestUser.getId() block) with a
value-equality check to avoid incorrect results and caching pitfalls; use a
null-safe comparison such as Objects.equals(hostUser.getId(), guestUser.getId())
(or hostUser.getId().equals(guestUser.getId()) after null checks) and keep the
existing IllegalArgumentException throw when they match.

Comment on lines +17 to +40
@Value("${cors.allowed-origins}")
private String allowedOrigins;

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 구독(sub) : 접두사로 시작하는 메시지를 브로커가 처리하도록 설정합니다. 클라이언트는 이 접두사로 시작하는 주제를 구독하여 메시지를 받을 수 있습니다.
// 예를 들어, 소켓 통신에서 사용자가 특정 메시지를 받기위해 "/sub"이라는 prefix 기반 메시지 수신을 위해 Subscribe합니다.
// 구독(브로드캐스트) prefix
registry.enableSimpleBroker("/sub"); // /sub/** 패턴의 모든 구독을 SimpleBroker가 자동 처리

// 발행(pub) : 접두사로 시작하는 메시지는 @MessageMapping이 달린 메서드로 라우팅됩니다. 클라이언트가 서버로 메시지를 보낼 때 이 접두사를 사용합니다.
// 예를 들어, 소켓 통신에서 사용자가 특정 메시지를 전송하기 위해 "/pub"라는 prefix 기반 메시지 전송을 위해 Publish 합니다.
// 클라이언트가 서버로 메시지 보낼 떄 prefix
registry.setApplicationDestinationPrefixes("/pub");
}

// 각각 특정 URL에 매핑되는 STOMP 엔드포인트를 등록하고, 선택적으로 SockJS 폴백 옵션을 활성화하고 구성합니다.
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// ws endpoint
// < 일단 이건 테스트/호환성 때문에 주석처리 해놓음 > 순수 WebSocket임
registry.addEndpoint("/ws-stomp")
.setAllowedOriginPatterns("*")
.withSockJS();
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

🧩 Analysis chain

🏁 Script executed:

find . -name "WebSocketStompBrokerConfig.java" -type f

Repository: Sportize/be

Length of output: 149


🏁 Script executed:

cat -n "src/main/java/com/be/sportizebe/domain/chat/websocket/config/WebSocketStompBrokerConfig.java"

Repository: Sportize/be

Length of output: 2201


🏁 Script executed:

rg "allowedOrigins" --type java -B 2 -A 2

Repository: Sportize/be

Length of output: 1477


프로퍼티를 주입했으나 하드코딩된 와일드카드로 보안 정책 우회

allowedOrigins 프로퍼티를 주입해 놓고 registerStompEndpoints() 메서드에서 실제로는 "*"를 허용하고 있습니다. 같은 프로퍼티를 사용하는 CorsConfig에서는 올바르게 처리하고 있으므로, WebSocket 설정도 일관되게 수정해야 합니다.

💡 제안 수정
         registry.addEndpoint("/ws-stomp")
-                .setAllowedOriginPatterns("*")
+                .setAllowedOriginPatterns(allowedOrigins.split("\\s*,\\s*"))
                 .withSockJS();
🤖 Prompt for AI Agents
In
`@src/main/java/com/be/sportizebe/domain/chat/websocket/config/WebSocketStompBrokerConfig.java`
around lines 17 - 40, The WebSocket config currently ignores the injected
allowedOrigins and hardcodes "*" in registerStompEndpoints; update the
StompEndpointRegistry registration in registerStompEndpoints (the
registry.addEndpoint("/ws-stomp") call) to use the allowedOrigins property
instead of the literal "*" by passing the parsed allowedOrigins values to
setAllowedOriginPatterns (e.g., split comma-separated allowedOrigins, trim
entries, and supply as varargs) so the endpoint honors the same CORS policy as
CorsConfig and avoid allowing a global wildcard.

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: 1

🤖 Fix all issues with AI agents
In `@src/main/java/com/be/sportizebe/domain/chat/controller/NoteController.java`:
- Around line 40-41: The current orElseThrow in NoteController uses
IllegalArgumentException which leads to a 500 from the global handler; change
the exception to one that yields 404 by throwing
ResponseStatusException(HttpStatus.NOT_FOUND, "게시글을 찾을 수 없습니다: " + postId) or
your project’s CustomException (e.g., ResourceNotFoundException) instead of
IllegalArgumentException when resolving Post via postRepository.findById so the
controller returns HTTP 404 for missing posts.
♻️ Duplicate comments (1)
src/main/java/com/be/sportizebe/domain/chat/controller/NoteController.java (1)

36-38: 테스트 유저 ID 하드코딩은 프로덕션 보안 리스크

이 부분은 이전 리뷰에서 이미 지적된 사항과 동일합니다. 인증 사용자 기반으로 교체가 필요합니다.

Also applies to: 53-55

Comment on lines +40 to +41
Post post = postRepository.findById(postId)
.orElseThrow(() -> new IllegalArgumentException("게시글을 찾을 수 없습니다: " + postId));
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

🧩 Analysis chain

🏁 Script executed:

# Search for global exception handling (`@ControllerAdvice`, `@ExceptionHandler`, ResponseStatusException)
rg -n --type=java '@ControllerAdvice|@ExceptionHandler|ResponseStatusException' -A 3

Repository: Sportize/be

Length of output: 1676


🏁 Script executed:

# View the NoteController.java file to understand context
fd -t f 'NoteController.java' --exec cat -n {} \;

Repository: Sportize/be

Length of output: 3211


🏁 Script executed:

# Check if there's any specific mapping for IllegalArgumentException
rg -n --type=java 'IllegalArgumentException' -B 2 -A 2

Repository: Sportize/be

Length of output: 3641


🏁 Script executed:

# Get the full GlobalExceptionHandler to see how Exception.class is handled
cat -n src/main/java/com/be/sportizebe/global/exception/GlobalExceptionHandler.java

Repository: Sportize/be

Length of output: 2250


🏁 Script executed:

# Check if there's a CustomException being used elsewhere and understand the error handling pattern
rg -n --type=java 'class CustomException|CustomException(' -A 2 | head -30

Repository: Sportize/be

Length of output: 181


🏁 Script executed:

# Check what BaseResponse looks like and the error code pattern
fd -t f 'BaseErrorCode.java|CustomException.java' --exec cat -n {} \;

Repository: Sportize/be

Length of output: 793


IllegalArgumentException은 500 응답을 반환합니다. 404로 명시적으로 처리하세요.

전역 예외 핸들러의 @ExceptionHandler(Exception.class)IllegalArgumentException을 가로채서 500 응답을 반환합니다. 프로젝트에서 이미 확립된 CustomException 패턴을 활용하거나 ResponseStatusException(HttpStatus.NOT_FOUND, ...)로 변경하여 정확한 404 상태 코드를 반환해야 합니다.

현재 코드
Post post = postRepository.findById(postId)
        .orElseThrow(() -> new IllegalArgumentException("게시글을 찾을 수 없습니다: " + postId));
🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/chat/controller/NoteController.java`
around lines 40 - 41, The current orElseThrow in NoteController uses
IllegalArgumentException which leads to a 500 from the global handler; change
the exception to one that yields 404 by throwing
ResponseStatusException(HttpStatus.NOT_FOUND, "게시글을 찾을 수 없습니다: " + postId) or
your project’s CustomException (e.g., ResourceNotFoundException) instead of
IllegalArgumentException when resolving Post via postRepository.findById so the
controller returns HTTP 404 for missing posts.

@imjuyongp imjuyongp merged commit 0ea8b91 into develop Jan 26, 2026
1 check passed
@imjuyongp imjuyongp deleted the refactor/chat branch January 26, 2026 11:04
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.

♻️Refactor: 단체 채팅 리펙토링 ♻️Refactor: 1:1 채팅 기능 + refactoring

1 participant