Skip to content

[FEAT] 채팅 서비스 V1 개발#179

Merged
Be-HinD merged 1 commit intomainfrom
feat/chat
Dec 29, 2025
Merged

[FEAT] 채팅 서비스 V1 개발#179
Be-HinD merged 1 commit intomainfrom
feat/chat

Conversation

@Be-HinD
Copy link
Member

@Be-HinD Be-HinD commented Dec 29, 2025

📝 Pull Request

📌 PR 종류

해당하는 항목에 체크해주세요.

  • 기능 추가 (Feature)
  • 버그 수정 (Fix)
  • 문서 수정 (Docs)
  • 코드 리팩터링 (Refactor)
  • 테스트 추가 (Test)
  • 기타 변경 (Chore)

✨ 변경 내용

  • 기존 Group 이벤트 기반 리스너 아키텍처 적용

  • REST Controller (ChatRoomController) - /api/v1/chat 경로

  • Request/Response DTOs

  • WebSocket 설정 (STOMP)

  • Service 레이어 (ChatRoomService, ChatMessageService)

  • yml 및 Security 설정

  • Swagger 문서 (ChatRoomControllerDocs)

  • 아키텍처 문서 및 클라이언트 가이드 문서 작성


🔍 관련 이슈

해당 PR이 해결하는 이슈가 있다면 연결해주세요.
#166 #167


🧪 테스트

변경된 기능에 대한 테스트 범위 또는 테스트 결과를 작성해주세요.

  • 유닛 테스트 추가 / 수정
  • 통합 테스트 검증
  • 수동 테스트 완료

클라이언트측 테스트 필요


🚨 확인해야 할 사항 (Checklist)

PR을 제출하기 전에 아래 항목들을 확인해주세요.

  • 코드 포매팅 완료
  • 불필요한 파일/코드 제거
  • 로직 검증 완료
  • 프로젝트 빌드 성공
  • 린트/정적 분석 통과 (해당 시)

🙋 기타 참고 사항

현재 초기 설계단계이며, 추후 수정 가능성이 큼.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 실시간 WebSocket 기반 채팅 서비스 추가 (그룹 채팅, 1:1 다이렉트 메시지)
    • 메시지 이력 조회 및 커서 기반 페이징 지원
    • 읽음/읽지 않음 상태 추적 기능
    • 참여자 관리 (강제 퇴장, 채팅방 나가기)
    • 그룹 생성 시 자동 채팅방 생성 및 멤버 자동 참여 옵션
  • 문서

    • 채팅 서비스 아키텍처 상세 문서 추가
    • 클라이언트 통합 및 테스트 가이드 문서 추가

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

- 기존 Group 이벤트 기반 리스너 아키텍처 적용
- REST Controller (ChatRoomController) - /api/v1/chat 경로
- Request/Response DTOs
- WebSocket 설정 (STOMP)
- Service 레이어 (ChatRoomService, ChatMessageService)
- yml 및 Security 설정
- Swagger 문서 (ChatRoomControllerDocs)
- 아키텍처 문서 및 클라이언트 가이드 문서 작성
@Be-HinD Be-HinD self-assigned this Dec 29, 2025
@Be-HinD Be-HinD added 📝documentation Improvements or additions to documentation ✨enhancement New feature or request labels Dec 29, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 29, 2025

Walkthrough

WebSocket 기반 실시간 채팅 기능을 추가합니다. 그룹 채팅과 1:1 DM을 지원하며, STOMP 프로토콜을 통한 양방향 통신, 메시지 읽음/읽지 않음 추적, 참여자 관리, 그룹 이벤트 연동을 포함합니다. 의존성 추가, 엔티티 정의, REST API 엔드포인트, 이벤트 리스너 구현, 설정 클래스를 도입합니다.

Changes

코호트 / 파일 변경 요약
의존성 및 보안 설정
build.gradle, src/main/java/team/wego/wegobackend/common/security/SecurityEndpoints.java, src/main/resources/application.yml
WebSocket 스타터 의존성 추가; /ws-chat/** 공개 엔드포인트 등록; 채팅 설정 블록 추가 (자동참여, DM 정책, WebSocket 엔드포인트, 메시지 길이, 배치 작업)
WebSocket 설정
src/main/java/team/wego/wegobackend/chat/config/WebSocketConfig.java, src/main/java/team/wego/wegobackend/chat/config/StompChannelInterceptor.java, src/main/java/team/wego/wegobackend/chat/config/ChatProperties.java
STOMP 메시지 브로커 활성화 및 엔드포인트 등록; JWT 기반 WebSocket 인증 처리; 채팅 기능별 구성 속성 정의
도메인 엔티티
src/main/java/team/wego/wegobackend/chat/domain/entity/ChatRoom.java, src/main/java/team/wego/wegobackend/chat/domain/entity/ChatMessage.java, src/main/java/team/wego/wegobackend/chat/domain/entity/ChatParticipant.java, src/main/java/team/wego/wegobackend/chat/domain/entity/ChatType.java, src/main/java/team/wego/wegobackend/chat/domain/entity/MessageType.java, src/main/java/team/wego/wegobackend/chat/domain/entity/JoinType.java, src/main/java/team/wego/wegobackend/chat/domain/entity/ParticipantStatus.java
채팅방, 메시지, 참여자 엔티티 및 관련 열거형 정의; 상태 관리 및 생명주기 메서드 포함
에러 처리
src/main/java/team/wego/wegobackend/chat/domain/exception/ChatException.java, src/main/java/team/wego/wegobackend/chat/domain/exception/ChatErrorCode.java
채팅 도메인 예외 및 에러 코드 정의 (채팅방, 참여자, 메시지 관련)
리포지토리
src/main/java/team/wego/wegobackend/chat/domain/repository/ChatRoomRepository.java, src/main/java/team/wego/wegobackend/chat/domain/repository/ChatMessageRepository.java, src/main/java/team/wego/wegobackend/chat/domain/repository/ChatParticipantRepository.java
채팅방, 메시지, 참여자 쿼리 메서드 정의 (페이징, 상태 필터링, 읽지 않은 메시지 카운트 등)
응답 DTO
src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomResponse.java, src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomItemResponse.java, src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomListResponse.java, src/main/java/team/wego/wegobackend/chat/application/dto/response/MessageResponse.java, src/main/java/team/wego/wegobackend/chat/application/dto/response/MessageListResponse.java, src/main/java/team/wego/wegobackend/chat/application/dto/response/ParticipantResponse.java, src/main/java/team/wego/wegobackend/chat/application/dto/response/ParticipantListResponse.java, src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatMessagePayload.java, src/main/java/team/wego/wegobackend/chat/application/dto/response/KickNotificationPayload.java, src/main/java/team/wego/wegobackend/chat/application/dto/response/LastMessageResponse.java, src/main/java/team/wego/wegobackend/chat/application/dto/response/ReadStatusResponse.java
채팅방 조회, 메시지 목록, 참여자 정보, WebSocket 페이로드 응답 DTO
요청 DTO
src/main/java/team/wego/wegobackend/chat/application/dto/request/SendMessageRequest.java, src/main/java/team/wego/wegobackend/chat/application/dto/request/CreateDmRequest.java, src/main/java/team/wego/wegobackend/chat/application/dto/request/KickParticipantRequest.java
메시지 전송, DM 생성, 참여자 추방 요청 DTO
서비스
src/main/java/team/wego/wegobackend/chat/application/service/ChatRoomService.java, src/main/java/team/wego/wegobackend/chat/application/service/ChatMessageService.java
채팅방 CRUD, 참여자 관리, 메시지 송수신, 읽음 상태 처리, 그룹 기반 채팅방 생성
이벤트 리스너
src/main/java/team/wego/wegobackend/chat/application/listener/ChatEventListener.java
그룹 생성/참여/탈퇴 이벤트 처리 및 채팅방 자동 생성/참여/탈퇴
프레젠테이션
src/main/java/team/wego/wegobackend/chat/presentation/ChatRoomController.java, src/main/java/team/wego/wegobackend/chat/presentation/ChatRoomControllerDocs.java, src/main/java/team/wego/wegobackend/chat/presentation/ChatMessageController.java
REST API 엔드포인트 (채팅방 조회, 참여자 관리, DM 생성); WebSocket 메시지 핸들러; Swagger 문서화
문서 및 테스트
docs/채팅서비스_아키텍처.md, docs/채팅서비스_클라이언트_가이드문서.md, src/test/http/auth/auth-api.http
채팅 서비스 아키텍처 및 클라이언트 통합 가이드; 테스트 데이터 업데이트

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant WebSocket as WebSocket<br/>(SockJS+STOMP)
    participant ChatMessageCtrl as ChatMessageController
    participant ChatMsgService as ChatMessageService
    participant MessageRepo as ChatMessageRepository
    participant SimpMessaging as SimpMessageSendingOperations

    Client->>WebSocket: 1. CONNECT (JWT Header)
    WebSocket->>StompChannelInterceptor: 2. Authenticate
    StompChannelInterceptor-->>WebSocket: 3. StompPrincipal (userId)
    
    Client->>WebSocket: 4. SEND /pub/chat/message<br/>(SendMessageRequest)
    
    WebSocket->>ChatMessageCtrl: 5. `@MessageMapping`<br/>sendMessage()
    ChatMessageCtrl->>ChatMsgService: 6. sendMessage(userId, roomId, content)
    
    ChatMsgService->>ChatMsgService: 7. Validate & Find ChatRoom/User
    ChatMsgService->>MessageRepo: 8. Save ChatMessage
    MessageRepo-->>ChatMsgService: 9. ChatMessage persisted
    ChatMsgService-->>ChatMessageCtrl: 10. ChatMessagePayload
    
    ChatMessageCtrl->>SimpMessaging: 11. convertAndSend<br/>to /sub/chat/room/{roomId}
    SimpMessaging->>Client: 12. Deliver message<br/>to all subscribers
    
    Note over Client,SimpMessaging: Group Chat Real-time Flow

    rect rgb(100, 150, 200)
    Note over ChatMessageCtrl: Message received &<br/>persisted
    end
    
    rect rgb(100, 200, 150)
    Note over SimpMessaging: Broadcasted to<br/>all room subscribers
    end
Loading
sequenceDiagram
    participant GroupEvent as GroupCreatedEvent
    participant ChatEventListn as ChatEventListener
    participant ChatRoomSvc as ChatRoomService
    participant ChatRoomRepo as ChatRoomRepository
    participant ParticipantRepo as ChatParticipantRepository

    GroupEvent->>ChatEventListn: 1. `@EventListener`<br/>handleGroupCreated(event)
    
    ChatEventListn->>ChatRoomSvc: 2. createGroupChatRoomForMeeting<br/>(groupId, hostUserId)
    
    ChatRoomSvc->>ChatRoomRepo: 3. Create ChatRoom<br/>(GROUP type, groupId)
    ChatRoomRepo-->>ChatRoomSvc: 4. ChatRoom saved
    
    ChatRoomSvc->>ParticipantRepo: 5. Add ChatParticipant<br/>(host, MANUAL join)
    ParticipantRepo-->>ChatRoomSvc: 6. Participant added
    
    ChatRoomSvc-->>ChatEventListn: 7. Return ChatRoom
    ChatEventListn-->>GroupEvent: 8. Log success
    
    Note over GroupEvent,ChatEventListn: Synchronize Group & Chat State

    rect rgb(200, 150, 100)
    Note over ChatRoomSvc: Transactional group<br/>chat room creation
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

변경 사항의 범위와 복잡도:

  • 파일 수: 약 35개의 새로운 파일 (엔티티, DTO, 서비스, 컨트롤러, 설정)
  • 로직 밀도: 고밀도의 비즈니스 로직 (WebSocket 인증, 트랜잭션, 이벤트 처리, 쿼리 최적화)
  • 다양성: 엔티티 설계, 저장소 쿼리, 서비스 레이어 오케스트레이션, 프레젠테이션 계층, 이벤트 처리 등 이질적인 변경
  • 상호 의존성: 여러 계층에 걸친 복잡한 통합 및 의존성 체인
  • 검토 포인트: JWT 기반 WebSocket 인증, 트랜잭션 경계 설정, 동시성 제어, 이벤트 기반 상태 동기화, 커서 기반 페이징, 참여자 상태 관리

Possibly related PRs

  • [FEAT] 모임 V2 엔티티 재설계 #125: 채팅 기능이 GroupV2 엔티티를 직접 참조하며 (ChatRoom.createGroupChat, ChatRoomService, ChatEventListener), 해당 PR에서 GroupV2 및 관련 v2 그룹 엔티티를 변경하므로 코드 수준의 직접적인 관련성이 있습니다.

  • [FEAT] V2 모임 알림 설정 #170: 본 PR의 ChatEventListenerGroupCreated/GroupJoined/GroupLeft 이벤트를 소비하며, 해당 PR에서 이러한 그룹 이벤트 타입과 이벤트 발행 로직을 정의하므로 동일한 이벤트 클래스 및 이벤트 흐름을 공유합니다.

  • [FEAT] 스프링 인증/인가 개발 #32: 본 PR이 SecurityEndpoints.PUBLIC_PATTERNS"/ws-chat/**"를 추가하고, 해당 PR에서 SecurityEndpoints 클래스 및 상수 목록을 초기 도입하므로 보안 설정 수준에서 직접 관련되어 있습니다.

Poem

🐰 채팅방이 활짝 열리고,
웹소켓을 타고 메시지 날아오르고,
참여자들의 상태 추적하며,
실시간 대화가 피어나네요!
그룹도, DM도, 모두 환영합니다. 🌟

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.04% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed Pull request 제목은 채팅 서비스 V1 개발이라는 주요 변경사항을 명확하고 간결하게 요약하고 있습니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/chat

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.

Copy link

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

🧹 Nitpick comments (19)
docs/채팅서비스_클라이언트_가이드문서.md (1)

177-202: 코드 블록에 언어 지정 누락

STOMP 프레임 예제 코드 블록에 언어가 지정되지 않았습니다. text 또는 적절한 언어 식별자를 추가하면 마크다운 린트 경고를 해결할 수 있습니다.

🔎 제안하는 수정 사항
 **STOMP CONNECT 프레임:**
-```
+```text
 CONNECT
 Authorization:Bearer YOUR_ACCESS_TOKEN
 accept-version:1.1,1.0
 heart-beat:10000,10000

 ^@
 ```

 **채팅방 구독:**
-```
+```text
 SUBSCRIBE
 id:sub-0
 destination:/sub/chat/room/1

 ^@
 ```

 **메시지 전송:**
-```
+```text
 SEND
 destination:/pub/chat/message
 content-type:application/json

 {"chatRoomId":1,"content":"Hello from Postman!"}^@
 ```
src/main/java/team/wego/wegobackend/chat/application/dto/response/MessageResponse.java (1)

16-26: sender null 체크 반복을 줄이는 것을 고려해 보세요.

message.getSender() null 체크가 3번 반복되고 있습니다. 로컬 변수로 추출하면 가독성이 향상됩니다.

🔎 제안하는 리팩토링
    public static MessageResponse from(ChatMessage message) {
+       var sender = message.getSender();
        return new MessageResponse(
                message.getId(),
-               message.getSender() != null ? message.getSender().getId() : null,
-               message.getSender() != null ? message.getSender().getNickName() : null,
-               message.getSender() != null ? message.getSender().getProfileImage() : null,
+               sender != null ? sender.getId() : null,
+               sender != null ? sender.getNickName() : null,
+               sender != null ? sender.getProfileImage() : null,
                message.getContent(),
                message.getMessageType(),
                message.getCreatedAt()
        );
    }
docs/채팅서비스_아키텍처.md (2)

19-19: 코드 블록에 언어 지정자를 추가하세요.

여러 fenced code block에 언어 지정자가 누락되어 있어 Markdown 렌더링 시 구문 강조가 적용되지 않습니다.

🔎 수정 예시

Line 110-111:

-```
-GET /api/v1/chat/rooms
+```http
+GET /api/v1/chat/rooms

Line 226-228:

-```
-Authorization: Bearer {accessToken}
-```
+```http
+Authorization: Bearer {accessToken}
+```

Line 295-298 (이벤트 플로우):
```diff
-```
-GroupCreatedEvent → ChatEventListener.handleGroupCreated()
+```text
+GroupCreatedEvent → ChatEventListener.handleGroupCreated()

Also applies to: 110-111, 142-143, 148-149, 177-178, 183-184, 194-195, 200-201, 211-212, 226-227, 231-232, 242-243, 247-248, 295-298, 301-304, 307-310


74-82: 테이블 주변에 빈 줄을 추가하세요.

Markdown 테이블 앞뒤로 빈 줄이 없어 일부 렌더러에서 표시가 깨질 수 있습니다.

🔎 수정 예시
 ### chat_room (채팅방)
+
 | 컬럼명 | 타입 | 설명 |
 |--------|------|------|
 ...
+
 ### chat_participant (참여자)

Also applies to: 84-93, 96-103

src/main/java/team/wego/wegobackend/chat/domain/repository/ChatParticipantRepository.java (1)

17-27: JPQL에서 enum 값을 문자열 리터럴로 직접 비교하고 있습니다.

cp.status = 'ACTIVE' 형태의 하드코딩된 문자열 비교는 enum 값이 변경될 경우 런타임 오류가 발생할 수 있습니다. Line 15의 findAllByChatRoomIdAndStatus처럼 파라미터 바인딩을 사용하는 것이 더 안전합니다.

🔎 권장 수정 사항
     @Query("""
             SELECT cp FROM ChatParticipant cp
-            WHERE cp.chatRoom.id = :chatRoomId AND cp.status = 'ACTIVE'
+            WHERE cp.chatRoom.id = :chatRoomId AND cp.status = :status
             """)
-    List<ChatParticipant> findActiveParticipants(@Param("chatRoomId") Long chatRoomId);
+    List<ChatParticipant> findActiveParticipants(@Param("chatRoomId") Long chatRoomId, @Param("status") ParticipantStatus status);

     @Query("""
             SELECT COUNT(cp) FROM ChatParticipant cp
-            WHERE cp.chatRoom.id = :chatRoomId AND cp.status = 'ACTIVE'
+            WHERE cp.chatRoom.id = :chatRoomId AND cp.status = :status
             """)
-    int countActiveParticipants(@Param("chatRoomId") Long chatRoomId);
+    int countActiveParticipants(@Param("chatRoomId") Long chatRoomId, @Param("status") ParticipantStatus status);

또는 Spring Data JPA 쿼리 메서드 명명 규칙을 활용할 수 있습니다:

List<ChatParticipant> findByChatRoomIdAndStatus(Long chatRoomId, ParticipantStatus status);
int countByChatRoomIdAndStatus(Long chatRoomId, ParticipantStatus status);
src/main/java/team/wego/wegobackend/chat/application/listener/ChatEventListener.java (1)

28-41: 비동기 이벤트 처리 시 실패 복구 전략 검토 필요

채팅방 생성 실패 시 로그만 기록하고 예외를 삼킵니다. 그룹은 생성되었지만 채팅방이 없는 불일치 상태가 발생할 수 있습니다.

고려 사항:

  • 재시도 메커니즘 (Spring Retry, exponential backoff)
  • 실패 이벤트 저장 및 수동/자동 복구 처리
  • 모니터링/알림 연동
src/main/java/team/wego/wegobackend/chat/config/WebSocketConfig.java (1)

19-29: Simple Broker 사용 시 수평 확장 제한

enableSimpleBroker("/sub")는 단일 인스턴스 환경에서는 잘 동작하지만, 여러 서버 인스턴스로 확장 시 서버 간 메시지 공유가 되지 않습니다.

향후 수평 확장이 필요하다면 외부 메시지 브로커(Redis, RabbitMQ 등) 도입을 고려해주세요.

src/main/java/team/wego/wegobackend/chat/domain/repository/ChatRoomRepository.java (1)

22-41: JPQL에서 enum 값을 문자열 리터럴로 비교하고 있습니다.

'ACTIVE', 'DM' 등 하드코딩된 문자열이 enum 값과 일치하지 않으면 런타임 오류가 발생할 수 있습니다. 파라미터 바인딩을 사용하면 타입 안전성이 향상됩니다.

🔎 권장 수정 사항
     @Query("""
             SELECT cr FROM ChatRoom cr
             JOIN cr.participants cp
-            WHERE cp.user.id = :userId AND cp.status = 'ACTIVE'
+            WHERE cp.user.id = :userId AND cp.status = :status
             ORDER BY cr.updatedAt DESC
             """)
-    List<ChatRoom> findAllByUserIdAndActiveStatus(@Param("userId") Long userId);
+    List<ChatRoom> findAllByUserIdAndActiveStatus(@Param("userId") Long userId, @Param("status") ParticipantStatus status);

     @Query("""
             SELECT cr FROM ChatRoom cr
             JOIN cr.participants cp1
             JOIN cr.participants cp2
-            WHERE cr.chatType = 'DM'
-            AND cp1.user.id = :userId1 AND cp1.status = 'ACTIVE'
-            AND cp2.user.id = :userId2 AND cp2.status = 'ACTIVE'
+            WHERE cr.chatType = :chatType
+            AND cp1.user.id = :userId1 AND cp1.status = :status
+            AND cp2.user.id = :userId2 AND cp2.status = :status
             """)
     Optional<ChatRoom> findDmChatRoom(
             @Param("userId1") Long userId1,
-            @Param("userId2") Long userId2
+            @Param("userId2") Long userId2,
+            @Param("chatType") ChatType chatType,
+            @Param("status") ParticipantStatus status
     );
src/main/java/team/wego/wegobackend/chat/domain/entity/ChatRoom.java (2)

51-52: messages 컬렉션을 엔티티에 로드하면 메모리 문제가 발생할 수 있습니다.

채팅방에 수천 개의 메시지가 쌓이면 messages 컬렉션을 통한 접근 시 OOM이 발생할 수 있습니다. 현재 코드에서는 addMessage로만 사용되고 조회는 Repository를 통해 하고 있어 실제 문제는 적지만, 실수로 getMessages()를 호출하면 전체 메시지를 로드하게 됩니다.

필요하다면 컬렉션 매핑을 제거하고 단방향 관계로 변경하는 것을 고려해 보세요.


78-80: LocalDateTime.now() 직접 사용은 테스트를 어렵게 만듭니다.

시간 기반 로직 테스트를 위해 Clock을 주입받거나, 메서드 파라미터로 현재 시간을 받는 방식을 고려해 보세요.

src/main/java/team/wego/wegobackend/chat/domain/repository/ChatMessageRepository.java (1)

13-22: 커서 기반 페이징이 잘 구현되어 있습니다.

대용량 데이터에서 offset 기반보다 효율적입니다. 성능을 위해 (chat_room_id, created_at DESC, id) 복합 인덱스 추가를 고려하세요.

src/main/java/team/wego/wegobackend/chat/application/service/ChatMessageService.java (2)

116-119: 예외 처리가 일관되지 않습니다.

사용자를 찾을 수 없는 경우 IllegalArgumentException을 사용하고, 다른 경우에는 ChatException을 사용합니다. 일관성을 위해 ChatErrorCodeUSER_NOT_FOUND를 추가하고 ChatException을 사용하는 것을 권장합니다.

🔎 제안하는 수정
 private User findUserById(Long userId) {
     return userRepository.findById(userId)
-            .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다: " + userId));
+            .orElseThrow(() -> new ChatException(ChatErrorCode.USER_NOT_FOUND));
 }

109-128: ChatRoomService와 헬퍼 메서드가 중복됩니다.

findChatRoomById, findUserById, validateParticipant 메서드가 ChatRoomService에도 동일하게 존재합니다. 향후 공통 헬퍼 클래스로 추출하는 것을 고려해 보세요.

src/main/java/team/wego/wegobackend/chat/config/ChatProperties.java (1)

8-12: @Configuration 대신 @ConfigurationPropertiesScan을 사용하는 것이 권장됩니다.

@ConfigurationProperties 클래스에 @Configuration을 사용하면 동작하지만, Spring Boot 2.2+에서는 메인 애플리케이션 클래스에 @ConfigurationPropertiesScan을 추가하거나 @EnableConfigurationProperties(ChatProperties.class)를 사용하는 것이 표준 패턴입니다.

src/main/java/team/wego/wegobackend/chat/presentation/ChatRoomController.java (1)

115-125: DM 생성/조회 시 HTTP 상태 코드 고려가 필요합니다.

현재 항상 201 CREATED를 반환하지만, 기존 DM 채팅방을 반환하는 경우에는 200 OK가 더 적절할 수 있습니다. 서비스에서 새로 생성되었는지 여부를 반환하여 상태 코드를 분기하는 것을 고려해 보세요.

src/main/java/team/wego/wegobackend/chat/domain/entity/ChatParticipant.java (1)

64-72: joinedAtupdatedAt이 개별적으로 now()를 호출합니다.

두 필드가 미세하게 다른 시간값을 가질 수 있습니다. 일관성을 위해 하나의 변수에 저장 후 사용하는 것을 고려하세요.

🔎 제안하는 수정
 @Builder
 private ChatParticipant(ChatRoom chatRoom, User user, JoinType joinType) {
+    LocalDateTime now = LocalDateTime.now();
     this.chatRoom = chatRoom;
     this.user = user;
     this.joinType = joinType != null ? joinType : JoinType.AUTO;
-    this.joinedAt = LocalDateTime.now();
+    this.joinedAt = now;
     this.status = ParticipantStatus.ACTIVE;
-    this.updatedAt = LocalDateTime.now();
+    this.updatedAt = now;
 }
src/main/java/team/wego/wegobackend/chat/presentation/ChatRoomControllerDocs.java (1)

33-41: 응답 스키마를 명시적으로 정의하면 Swagger UI에서 더 명확하게 표시됩니다.

@ApiResponsecontent 속성을 추가하여 응답 스키마를 명시하면 API 문서가 더 완전해집니다.

🔎 예시
@io.swagger.v3.oas.annotations.responses.ApiResponse(
        responseCode = "200",
        description = "조회 성공",
        content = @Content(schema = @Schema(implementation = ChatRoomListResponse.class))
)
src/main/java/team/wego/wegobackend/chat/application/service/ChatRoomService.java (2)

47-55: getMyChatRooms에서 N+1 쿼리 문제가 발생할 수 있습니다.

각 채팅방마다 buildChatRoomItem에서 여러 쿼리(참여자 수, 마지막 메시지, 안읽은 수 등)가 실행됩니다. 채팅방이 많아지면 성능 저하가 발생할 수 있습니다.

향후 개선 시 @EntityGraph, batch fetching, 또는 단일 DTO 프로젝션 쿼리로 최적화하는 것을 고려하세요.


222-229: leaveChatRoomByGroup가 채팅방/참여자가 없을 때 조용히 무시합니다.

이벤트 리스너에서 호출되므로 의도된 동작일 수 있지만, 예상치 못한 상태를 감지하기 위해 최소한 warn 레벨 로깅을 추가하는 것을 고려해 보세요.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b8e6655 and 79e633e.

📒 Files selected for processing (41)
  • build.gradle
  • docs/채팅서비스_아키텍처.md
  • docs/채팅서비스_클라이언트_가이드문서.md
  • src/main/java/team/wego/wegobackend/chat/application/dto/request/CreateDmRequest.java
  • src/main/java/team/wego/wegobackend/chat/application/dto/request/KickParticipantRequest.java
  • src/main/java/team/wego/wegobackend/chat/application/dto/request/SendMessageRequest.java
  • src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatMessagePayload.java
  • src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomItemResponse.java
  • src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomListResponse.java
  • src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomResponse.java
  • src/main/java/team/wego/wegobackend/chat/application/dto/response/KickNotificationPayload.java
  • src/main/java/team/wego/wegobackend/chat/application/dto/response/LastMessageResponse.java
  • src/main/java/team/wego/wegobackend/chat/application/dto/response/MessageListResponse.java
  • src/main/java/team/wego/wegobackend/chat/application/dto/response/MessageResponse.java
  • src/main/java/team/wego/wegobackend/chat/application/dto/response/ParticipantListResponse.java
  • src/main/java/team/wego/wegobackend/chat/application/dto/response/ParticipantResponse.java
  • src/main/java/team/wego/wegobackend/chat/application/dto/response/ReadStatusResponse.java
  • src/main/java/team/wego/wegobackend/chat/application/listener/ChatEventListener.java
  • src/main/java/team/wego/wegobackend/chat/application/service/ChatMessageService.java
  • src/main/java/team/wego/wegobackend/chat/application/service/ChatRoomService.java
  • src/main/java/team/wego/wegobackend/chat/config/ChatProperties.java
  • src/main/java/team/wego/wegobackend/chat/config/StompChannelInterceptor.java
  • src/main/java/team/wego/wegobackend/chat/config/WebSocketConfig.java
  • src/main/java/team/wego/wegobackend/chat/domain/entity/ChatMessage.java
  • src/main/java/team/wego/wegobackend/chat/domain/entity/ChatParticipant.java
  • src/main/java/team/wego/wegobackend/chat/domain/entity/ChatRoom.java
  • src/main/java/team/wego/wegobackend/chat/domain/entity/ChatType.java
  • src/main/java/team/wego/wegobackend/chat/domain/entity/JoinType.java
  • src/main/java/team/wego/wegobackend/chat/domain/entity/MessageType.java
  • src/main/java/team/wego/wegobackend/chat/domain/entity/ParticipantStatus.java
  • src/main/java/team/wego/wegobackend/chat/domain/exception/ChatErrorCode.java
  • src/main/java/team/wego/wegobackend/chat/domain/exception/ChatException.java
  • src/main/java/team/wego/wegobackend/chat/domain/repository/ChatMessageRepository.java
  • src/main/java/team/wego/wegobackend/chat/domain/repository/ChatParticipantRepository.java
  • src/main/java/team/wego/wegobackend/chat/domain/repository/ChatRoomRepository.java
  • src/main/java/team/wego/wegobackend/chat/presentation/ChatMessageController.java
  • src/main/java/team/wego/wegobackend/chat/presentation/ChatRoomController.java
  • src/main/java/team/wego/wegobackend/chat/presentation/ChatRoomControllerDocs.java
  • src/main/java/team/wego/wegobackend/common/security/SecurityEndpoints.java
  • src/main/resources/application.yml
  • src/test/http/auth/auth-api.http
🧰 Additional context used
🧬 Code graph analysis (6)
src/main/java/team/wego/wegobackend/chat/domain/entity/ChatMessage.java (2)
src/main/java/team/wego/wegobackend/chat/domain/entity/ChatParticipant.java (1)
  • Entity (22-118)
src/main/java/team/wego/wegobackend/chat/domain/entity/ChatRoom.java (1)
  • Entity (26-99)
src/main/java/team/wego/wegobackend/chat/domain/entity/ChatParticipant.java (2)
src/main/java/team/wego/wegobackend/chat/domain/entity/ChatMessage.java (1)
  • Entity (21-86)
src/main/java/team/wego/wegobackend/chat/domain/entity/ChatRoom.java (1)
  • Entity (26-99)
src/main/java/team/wego/wegobackend/chat/config/ChatProperties.java (1)
src/main/java/team/wego/wegobackend/chat/config/WebSocketConfig.java (1)
  • Configuration (11-47)
src/main/java/team/wego/wegobackend/chat/config/WebSocketConfig.java (1)
src/main/java/team/wego/wegobackend/chat/config/ChatProperties.java (1)
  • Configuration (8-62)
src/main/java/team/wego/wegobackend/chat/application/service/ChatMessageService.java (2)
src/main/java/team/wego/wegobackend/chat/application/service/ChatRoomService.java (1)
  • Service (32-339)
src/main/java/team/wego/wegobackend/chat/domain/exception/ChatException.java (1)
  • ChatException (6-15)
src/main/java/team/wego/wegobackend/chat/application/service/ChatRoomService.java (2)
src/main/java/team/wego/wegobackend/chat/application/service/ChatMessageService.java (1)
  • Service (26-140)
src/main/java/team/wego/wegobackend/chat/domain/exception/ChatException.java (1)
  • ChatException (6-15)
🪛 Gitleaks (8.30.0)
docs/채팅서비스_클라이언트_가이드문서.md

[high] 28-28: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 29-29: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 markdownlint-cli2 (0.18.1)
docs/채팅서비스_아키텍처.md

19-19: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


74-74: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


84-84: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


96-96: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


110-110: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


142-142: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


148-148: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


177-177: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


183-183: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


194-194: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


200-200: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


211-211: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


226-226: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


231-231: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


242-242: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


247-247: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


295-295: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


301-301: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


307-307: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

docs/채팅서비스_클라이언트_가이드문서.md

177-177: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


187-187: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


196-196: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (42)
build.gradle (1)

26-26: LGTM!

WebSocket 스타터 의존성이 올바르게 추가되었습니다. Spring Boot BOM을 통한 버전 관리가 적절합니다.

src/main/java/team/wego/wegobackend/chat/domain/entity/MessageType.java (1)

1-6: LGTM!

메시지 타입 enum이 명확하게 정의되었습니다. TEXT와 SYSTEM 구분이 적절합니다.

src/main/java/team/wego/wegobackend/chat/application/dto/response/KickNotificationPayload.java (1)

8-21: LGTM!

추방 알림 페이로드가 record로 적절하게 구현되었습니다. 정적 팩토리 메서드 패턴이 잘 적용되었습니다.

src/main/java/team/wego/wegobackend/chat/config/StompChannelInterceptor.java (1)

62-67: LGTM!

StompPrincipal record가 Principal 인터페이스를 올바르게 구현하고 있습니다. userIdgetName()으로 반환하여 메시지 핸들러에서 사용자 식별이 가능합니다.

docs/채팅서비스_클라이언트_가이드문서.md (1)

23-33: 예제 토큰은 보안 위험이 아님

정적 분석 도구(Gitleaks)가 이 예제 토큰들을 API 키로 감지했으나, 이는 문서화 목적의 플레이스홀더 예제이므로 무시해도 됩니다.

src/main/java/team/wego/wegobackend/chat/domain/entity/ChatMessage.java (1)

21-86: LGTM!

ChatMessage 엔티티가 잘 설계되었습니다:

  • Lazy 로딩이 올바르게 적용됨
  • 정적 팩토리 메서드(createTextMessage, createSystemMessage)가 명확함
  • assignToChatRoom이 package-private으로 캡슐화가 잘 유지됨
  • ChatRoom.addMessage()를 통한 양방향 관계 설정 패턴이 ChatParticipant와 일관됨
src/main/java/team/wego/wegobackend/chat/domain/entity/JoinType.java (1)

1-6: LGTM!

참여 유형 enum이 간결하게 정의되었습니다. AUTO(자동 입장)와 MANUAL(수동 입장) 구분이 모임 연동 로직에 적합합니다.

src/main/java/team/wego/wegobackend/chat/domain/entity/ParticipantStatus.java (1)

1-7: LGTM!

참여자 상태 enum이 명확하게 정의되었습니다. ACTIVE, LEFT, KICKED 상태가 채팅방 참여자 생명주기를 잘 표현합니다.

src/main/java/team/wego/wegobackend/common/security/SecurityEndpoints.java (1)

18-20: WebSocket 엔드포인트를 공개 패턴에 추가하는 것은 적절합니다.

STOMP CONNECT 단계에서 인증을 처리하는 것은 WebSocket의 일반적인 패턴입니다. 다만 StompChannelInterceptor가 JWT 인증을 올바르게 강제하고 있는지, 인증 실패 시 연결이 적절히 거부되는지 확인해 주세요.

src/main/resources/application.yml (1)

92-110: 채팅 설정 블록이 잘 구성되어 있습니다.

설정 구조가 명확하고 각 옵션에 대한 설명이 주석으로 제공되어 있습니다. allowed-origins 목록이 개발 및 프로덕션 환경을 모두 커버하고 있으며, 배치 작업의 cron 표현식도 올바릅니다.

src/main/java/team/wego/wegobackend/chat/domain/entity/ChatType.java (1)

1-6: LGTM!

간단하고 명확한 enum 정의입니다.

src/main/java/team/wego/wegobackend/chat/application/dto/request/KickParticipantRequest.java (1)

1-9: LGTM!

record를 사용한 깔끔한 DTO 설계이며, 필수 필드에 대한 유효성 검증이 적절히 적용되어 있습니다.

src/main/java/team/wego/wegobackend/chat/application/dto/request/CreateDmRequest.java (1)

1-9: LGTM!

KickParticipantRequest와 일관된 패턴을 따르는 깔끔한 DTO입니다.

src/main/java/team/wego/wegobackend/chat/application/dto/response/ParticipantResponse.java (1)

15-24: participant.getUser()에 대한 null 체크가 누락되어 있습니다.

MessageResponse.from()과 달리 이 메서드는 getUser() 호출에 대한 null 체크가 없습니다. ChatParticipant가 항상 유효한 User를 갖는 것이 도메인 불변 조건이라면 괜찮지만, 그렇지 않다면 NPE가 발생할 수 있습니다.

도메인 불변 조건을 확인하거나, 일관성을 위해 null-safe 처리를 추가하는 것을 고려해 주세요.

src/main/java/team/wego/wegobackend/chat/application/dto/response/MessageListResponse.java (1)

1-17: LGTM!

커서 기반 페이지네이션을 위한 깔끔한 응답 DTO입니다. 구조가 명확하고 사용하기 쉽습니다.

src/main/java/team/wego/wegobackend/chat/application/dto/response/ReadStatusResponse.java (1)

3-11: 잘 구현된 응답 DTO입니다.

읽음 상태를 나타내는 필드들이 명확하게 정의되어 있고, 정적 팩토리 메서드를 통한 일관된 생성 패턴을 따르고 있습니다.

src/main/java/team/wego/wegobackend/chat/domain/exception/ChatException.java (1)

6-15: 표준 예외 패턴을 잘 따르고 있습니다.

AppException을 확장하여 채팅 도메인의 일관된 예외 처리를 제공하며, 가변 인자를 지원하는 생성자로 유연한 에러 메시지 구성이 가능합니다.

src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomListResponse.java (1)

5-11: 깔끔한 리스트 응답 래퍼입니다.

채팅방 목록을 감싸는 단순하고 명확한 구조로, 프로젝트의 다른 리스트 응답 DTO들과 일관성을 유지하고 있습니다.

src/main/java/team/wego/wegobackend/chat/application/dto/request/SendMessageRequest.java (1)

6-13: 검증 로직이 적절하게 구현되어 있습니다.

필수 필드에 대한 @NotNull@NotBlank 애노테이션이 올바르게 적용되어 있으며, 한국어 검증 메시지로 사용자 친화적인 에러 응답을 제공합니다.

src/main/java/team/wego/wegobackend/chat/application/dto/response/ParticipantListResponse.java (1)

5-16: 참여자 수를 안전하게 계산하는 좋은 설계입니다.

totalCount를 팩토리 메서드 내부에서 participants.size()로 계산함으로써, 수동으로 전달받을 때 발생할 수 있는 불일치 문제를 방지합니다.

src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatMessagePayload.java (2)

20-31: 안전한 null 처리가 잘 되어 있습니다.

시스템 메시지의 경우 sender가 null일 수 있는 상황을 삼항 연산자로 적절하게 처리하여 NPE를 방지하고 있습니다.


33-44: 시스템 메시지 생성 로직이 명확합니다.

시스템 메시지의 특성(sender 정보 없음, MessageType.SYSTEM)을 명시적으로 표현한 팩토리 메서드로 코드 가독성이 높습니다.

src/main/java/team/wego/wegobackend/chat/application/dto/response/LastMessageResponse.java (1)

6-18: 깔끔한 팩토리 메서드 설계입니다.

senderName을 별도 파라미터로 받아 메시지 엔티티의 연관 관계 로딩에 의존하지 않는 유연한 구조입니다.

src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomResponse.java (1)

8-34: LGTM!

레코드 기반 DTO 구현이 깔끔하고, 팩토리 메서드 패턴을 통해 엔티티에서 응답 객체로의 변환이 잘 캡슐화되어 있습니다. getGroup() null 체크도 적절합니다.

src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomItemResponse.java (1)

7-35: LGTM!

목록 조회용 DTO로 적절한 구조입니다. lastMessageunreadCount 필드를 통해 클라이언트에서 필요한 정보를 효율적으로 제공합니다.

src/main/java/team/wego/wegobackend/chat/domain/exception/ChatErrorCode.java (1)

10-34: 잘 구성된 에러 코드 체계입니다.

에러 코드가 채팅방, 참여자, 메시지, DM 카테고리로 잘 분류되어 있고 HTTP 상태 코드 매핑도 적절합니다.

MESSAGE_TOO_LONG의 경우 %d 포맷 문자열을 사용하고 있으니, 실제 사용 시 String.format()으로 최대 길이 값을 주입하는지 확인해주세요.

src/main/java/team/wego/wegobackend/chat/presentation/ChatMessageController.java (1)

55-90: LGTM!

시스템 메시지 및 개인 알림 전송을 위한 헬퍼 메서드들이 적절하게 구현되어 있습니다. 다른 서비스에서 호출하여 WebSocket 메시지를 브로드캐스트할 수 있는 구조입니다.

src/main/java/team/wego/wegobackend/chat/application/listener/ChatEventListener.java (1)

46-91: 자동 참여/퇴장 설정 일관성 확인

handleGroupJoinedchatProperties.getAutoJoin().isEnabled() 설정을 확인하지만, handleGroupLeft는 항상 실행됩니다. 의도된 설계라면 괜찮지만, 일관성을 위해 auto-leave 설정도 추가할지 검토해주세요.

src/main/java/team/wego/wegobackend/chat/config/WebSocketConfig.java (1)

31-46: LGTM!

SockJS 폴백과 네이티브 WebSocket 모두 지원하도록 엔드포인트가 잘 구성되어 있습니다. 설정값을 ChatProperties로 외부화한 것도 좋은 접근입니다.

src/main/java/team/wego/wegobackend/chat/domain/repository/ChatRoomRepository.java (1)

12-20: LGTM!

findByGroupIdfindExpiredChatRooms는 적절하게 구현되어 있습니다. 특히 findExpiredChatRooms에서 파라미터 바인딩을 올바르게 사용하고 있습니다.

src/main/java/team/wego/wegobackend/chat/domain/entity/ChatRoom.java (2)

61-72: LGTM!

정적 팩토리 메서드를 통한 생성 패턴이 잘 적용되어 있습니다. createGroupChatcreateDmChat으로 의도가 명확하게 드러납니다.


90-98: 양방향 연관관계 관리가 잘 구현되어 있습니다.

addParticipantaddMessage에서 assignToChatRoom을 호출하여 양방향 관계를 올바르게 동기화하고 있습니다.

src/main/java/team/wego/wegobackend/chat/application/service/ChatMessageService.java (2)

41-61: LGTM!

메시지 전송 로직이 검증(내용, 채팅방, 참여자) → 생성 → 저장 순서로 잘 구성되어 있습니다.


82-107: 커서 기반 페이징 로직이 잘 구현되어 있습니다.

첫 페이지와 이후 페이지를 구분하여 처리하고, nextCursor 계산도 정확합니다.

src/main/java/team/wego/wegobackend/chat/config/ChatProperties.java (1)

20-61: LGTM!

설정 클래스가 논리적으로 잘 그룹화되어 있고, 기본값들이 적절하게 설정되어 있습니다.

src/main/java/team/wego/wegobackend/chat/presentation/ChatRoomController.java (1)

28-45: LGTM!

컨트롤러 구조가 깔끔하고, Swagger 문서화를 위한 인터페이스 분리 패턴이 잘 적용되어 있습니다.

src/main/java/team/wego/wegobackend/chat/domain/entity/ChatParticipant.java (2)

90-117: LGTM!

상태 관리 메서드(leave, kick, rejoin)가 명확하게 구현되어 있고, 상태 변경 시 updatedAt도 함께 업데이트됩니다.


22-29: 유니크 제약조건이 적절하게 정의되어 있습니다.

(chat_room_id, user_id) 복합 유니크 키로 동일 사용자가 같은 채팅방에 중복 참여하는 것을 방지합니다.

src/main/java/team/wego/wegobackend/chat/presentation/ChatRoomControllerDocs.java (1)

25-189: LGTM!

API 문서화가 체계적으로 잘 되어 있습니다. 각 엔드포인트의 성공/실패 케이스가 명확하게 정의되어 있습니다.

src/main/java/team/wego/wegobackend/chat/application/service/ChatRoomService.java (3)

258-267: DM 채팅방에서 추방 권한 검증이 누락되었습니다.

validateHost에서 group이 null이면 바로 리턴하므로, DM 채팅방에서는 누구나 상대방을 추방할 수 있습니다. 이것이 의도된 동작인지 확인이 필요합니다.


194-216: LGTM!

ifPresentOrElse를 활용하여 기존 참여자 재참여와 신규 참여를 깔끔하게 처리하고 있습니다.


324-338: LGTM!

DM 채팅방 생성 로직이 명확하고, 두 참여자를 올바르게 추가합니다.

<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

onKeyPress는 React에서 deprecated됨

onKeyPress는 React 17 이후로 deprecated되었습니다. onKeyDown을 사용하는 것이 권장됩니다.

🔎 제안하는 수정 사항
-          onKeyPress={(e) => e.key === 'Enter' && handleSend()}
+          onKeyDown={(e) => e.key === 'Enter' && handleSend()}
📝 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
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
🤖 Prompt for AI Agents
In docs/채팅서비스_클라이언트_가이드문서.md around line 385, replace the deprecated React
onKeyPress usage with onKeyDown to handle Enter submissions; update the handler
to use onKeyDown={(e) => e.key === 'Enter' && handleSend()} (or call
e.preventDefault() inside if needed) so the Enter-key behavior remains identical
while avoiding the deprecated API.

Comment on lines +173 to +177
// 이미 채팅방이 있는지 확인
if (chatRoomRepository.findByGroupId(groupId).isPresent()) {
log.warn("이미 그룹 채팅방이 존재합니다 - groupId: {}", groupId);
return chatRoomRepository.findByGroupId(groupId).get();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

findByGroupId가 중복 호출됩니다.

채팅방 존재 여부 확인 후 같은 쿼리를 다시 실행합니다. 첫 번째 결과를 재사용하세요.

🔎 제안하는 수정
-        // 이미 채팅방이 있는지 확인
-        if (chatRoomRepository.findByGroupId(groupId).isPresent()) {
-            log.warn("이미 그룹 채팅방이 존재합니다 - groupId: {}", groupId);
-            return chatRoomRepository.findByGroupId(groupId).get();
-        }
+        // 이미 채팅방이 있는지 확인
+        Optional<ChatRoom> existingRoom = chatRoomRepository.findByGroupId(groupId);
+        if (existingRoom.isPresent()) {
+            log.warn("이미 그룹 채팅방이 존재합니다 - groupId: {}", groupId);
+            return existingRoom.get();
+        }
🤖 Prompt for AI Agents
In
src/main/java/team/wego/wegobackend/chat/application/service/ChatRoomService.java
around lines 173 to 177, the call to chatRoomRepository.findByGroupId(groupId)
is executed twice; store the Optional result in a local variable, check
isPresent() (or use ifPresent/orElse), log and return the contained ChatRoom
from that variable to avoid repeating the query.

Comment on lines +35 to +49
try {
if (jwtTokenProvider.validateAccessToken(token)) {
Long userId = jwtTokenProvider.getTokenUserId(token);
String email = jwtTokenProvider.getEmailFromToken(token);

// Principal로 사용자 정보 설정
StompPrincipal principal = new StompPrincipal(userId, email);
accessor.setUser(principal);

log.debug("WebSocket 연결 인증 성공 - userId: {}", userId);
}
} catch (Exception e) {
log.error("WebSocket 연결 인증 실패", e);
throw new IllegalArgumentException("Invalid token");
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

토큰 검증 실패 시 연결이 차단되지 않는 보안 취약점

validateAccessToken(token)false를 반환하는 경우(예: 만료되었거나 유효하지 않은 토큰), 예외가 발생하지 않고 principal이 설정되지 않은 채로 메시지가 계속 전달됩니다. 이로 인해 인증되지 않은 WebSocket 연결이 허용될 수 있습니다.

🔎 제안하는 수정 사항
 try {
     if (jwtTokenProvider.validateAccessToken(token)) {
         Long userId = jwtTokenProvider.getTokenUserId(token);
         String email = jwtTokenProvider.getEmailFromToken(token);

         // Principal로 사용자 정보 설정
         StompPrincipal principal = new StompPrincipal(userId, email);
         accessor.setUser(principal);

         log.debug("WebSocket 연결 인증 성공 - userId: {}", userId);
+    } else {
+        log.warn("WebSocket 연결 인증 실패 - 유효하지 않은 토큰");
+        throw new IllegalArgumentException("Invalid token");
     }
 } catch (Exception e) {
     log.error("WebSocket 연결 인증 실패", e);
     throw new IllegalArgumentException("Invalid token");
 }
📝 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
try {
if (jwtTokenProvider.validateAccessToken(token)) {
Long userId = jwtTokenProvider.getTokenUserId(token);
String email = jwtTokenProvider.getEmailFromToken(token);
// Principal로 사용자 정보 설정
StompPrincipal principal = new StompPrincipal(userId, email);
accessor.setUser(principal);
log.debug("WebSocket 연결 인증 성공 - userId: {}", userId);
}
} catch (Exception e) {
log.error("WebSocket 연결 인증 실패", e);
throw new IllegalArgumentException("Invalid token");
}
try {
if (jwtTokenProvider.validateAccessToken(token)) {
Long userId = jwtTokenProvider.getTokenUserId(token);
String email = jwtTokenProvider.getEmailFromToken(token);
// Principal로 사용자 정보 설정
StompPrincipal principal = new StompPrincipal(userId, email);
accessor.setUser(principal);
log.debug("WebSocket 연결 인증 성공 - userId: {}", userId);
} else {
log.warn("WebSocket 연결 인증 실패 - 유효하지 않은 토큰");
throw new IllegalArgumentException("Invalid token");
}
} catch (Exception e) {
log.error("WebSocket 연결 인증 실패", e);
throw new IllegalArgumentException("Invalid token");
}
🤖 Prompt for AI Agents
In src/main/java/team/wego/wegobackend/chat/config/StompChannelInterceptor.java
around lines 35 to 49, currently when
jwtTokenProvider.validateAccessToken(token) returns false the code continues
without setting a principal and allows the message to proceed; change the logic
so that when validateAccessToken(token) is false you log the failure and
immediately block the connection by throwing an exception (e.g.,
IllegalArgumentException or an AuthenticationException) or returning null from
preSend, and only call accessor.setUser(...) when the token is valid; ensure no
principal is set for invalid tokens and include a clear error log message before
throwing to aid debugging.

Comment on lines +34 to +40
@Query("""
SELECT cm FROM ChatMessage cm
WHERE cm.chatRoom.id = :chatRoomId
ORDER BY cm.createdAt DESC
LIMIT 1
""")
Optional<ChatMessage> findLatestByChatRoomId(@Param("chatRoomId") Long chatRoomId);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

JPQL LIMIT clause support Hibernate version compatibility JPA standard

💡 Result:

Short answer

  • JPQL (the JPA query language) does NOT include a LIMIT/OFFSET clause in the JPA specification; pagination is provided by the Query API (setFirstResult / setMaxResults). [1][2]
  • Hibernate HQL historically did not support a LIMIT token (users relied on setMaxResults). Starting with Hibernate ORM 6, HQL gained SQL-style pagination syntax (LIMIT / OFFSET and FETCH … ROWS) in addition to the API methods. If you need portable JPA code, keep using setFirstResult/setMaxResults; if you use Hibernate-specific HQL you can use the new clauses on Hibernate 6+. [1][3]

Sources

  • JPQL / JPA pagination recommendation (use setFirstResult / setMaxResults). [1]
  • Hibernate ORM HQL docs showing LIMIT/OFFSET/FETCH support (Hibernate 6.x). [3]
  • Community history/notes that LIMIT was never a JPQL/HQL standard clause and setMaxResults was the supported approach. [2]

References
[1] Thorben Janssen — “Pagination with JPA and Hibernate” (explains JPQL lacks LIMIT/OFFSET; use setFirstResult/setMaxResults).
[2] StackOverflow / historical answers — LIMIT was not a supported HQL/JPQL clause; use setMaxResults.
[3] Hibernate ORM docs (HQL / Query Language — limits and offsets; Hibernate 6 adds limit/offset/fetch syntax).


🏁 Script executed:

# Check for Hibernate version in project configuration
find . -name "pom.xml" -o -name "build.gradle" -o -name "build.gradle.kts" | head -5

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 84


🏁 Script executed:

# Look for Hibernate dependency version
rg -i "hibernate|jpa" --type xml --type gradle -A 2 | head -50

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 574


🏁 Script executed:

cat -n build.gradle | head -60

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 2544


🏁 Script executed:

# Check the specific ChatMessageRepository file to confirm @Query usage
cat -n src/main/java/team/wego/wegobackend/chat/domain/repository/ChatMessageRepository.java

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 2214


JPQL에서 LIMIT 절은 JPA 표준이 아닙니다.

@query 애노테이션은 JPQL(JPA 표준 쿼리 언어)을 사용하는데, JPQL은 LIMIT 절을 지원하지 않습니다. 이 코드는 현재 Hibernate 6.x에서 동작하지만, 다른 JPA 구현체로 마이그레이션할 경우 호환성 문제가 발생할 수 있습니다. 표준 JPA 방식으로 Pageable을 사용하세요.

🔎 대안: Pageable 사용
-    @Query("""
-            SELECT cm FROM ChatMessage cm
-            WHERE cm.chatRoom.id = :chatRoomId
-            ORDER BY cm.createdAt DESC
-            LIMIT 1
-            """)
-    Optional<ChatMessage> findLatestByChatRoomId(@Param("chatRoomId") Long chatRoomId);
+    @Query("""
+            SELECT cm FROM ChatMessage cm
+            WHERE cm.chatRoom.id = :chatRoomId
+            ORDER BY cm.createdAt DESC
+            """)
+    Optional<ChatMessage> findLatestByChatRoomId(@Param("chatRoomId") Long chatRoomId, Pageable pageable);

호출 시 PageRequest.of(0, 1)을 전달하면 됩니다.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +29 to +43
public void sendMessage(@Payload SendMessageRequest request, Principal principal) {
if (principal == null) {
log.warn("인증되지 않은 사용자의 메시지 전송 시도");
return;
}

StompPrincipal stompPrincipal = (StompPrincipal) principal;
Long userId = stompPrincipal.userId();

log.debug("메시지 수신 - roomId: {}, userId: {}, content: {}",
request.chatRoomId(), userId, request.content());

ChatMessagePayload payload = chatMessageService.sendMessage(
userId, request.chatRoomId(), request.content()
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

인증 실패 처리 및 타입 캐스팅 개선 필요

  1. Silent failure: 인증 실패 시 클라이언트에 아무런 피드백 없이 리턴합니다. 에러 메시지를 클라이언트에 전송하는 것이 좋습니다.

  2. Unchecked cast: StompPrincipal로의 캐스팅이 실패할 경우 ClassCastException이 발생합니다.

  3. 민감 정보 로깅: 메시지 내용(content)을 로그에 기록하면 민감한 사용자 데이터가 노출될 수 있습니다.

🔎 권장 수정 사항
     @MessageMapping("/chat/message")
     public void sendMessage(@Payload SendMessageRequest request, Principal principal) {
-        if (principal == null) {
+        if (principal == null || !(principal instanceof StompPrincipal)) {
             log.warn("인증되지 않은 사용자의 메시지 전송 시도");
+            // 클라이언트에 에러 전송 고려
             return;
         }

-        StompPrincipal stompPrincipal = (StompPrincipal) principal;
+        StompPrincipal stompPrincipal = (StompPrincipal) principal;
         Long userId = stompPrincipal.userId();

-        log.debug("메시지 수신 - roomId: {}, userId: {}, content: {}",
-                request.chatRoomId(), userId, request.content());
+        log.debug("메시지 수신 - roomId: {}, userId: {}", request.chatRoomId(), userId);
🤖 Prompt for AI Agents
In
src/main/java/team/wego/wegobackend/chat/presentation/ChatMessageController.java
around lines 29–43, handle unauthenticated and invalid-principal cases and stop
logging sensitive content: return an error response to the client instead of
silently returning (e.g., send a Stomp error or a user-targeted error message),
guard the cast by checking principal instanceof StompPrincipal before casting
and send an error to the client if it is not, and remove or redact
request.content() from logs (log only chatRoomId and userId or a
content-length/placeholder). Ensure all error paths send a clear client-facing
error and do not let ClassCastException propagate.

Comment on lines +62 to +73
@GetMapping("/rooms/{roomId}/messages")
public ResponseEntity<ApiResponse<MessageListResponse>> getMessages(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long roomId,
@RequestParam(required = false) Long cursor,
@RequestParam(defaultValue = "50") int size
) {
MessageListResponse response = chatMessageService.getMessages(
userDetails.getId(), roomId, cursor, size
);
return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK.value(), response));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

size 파라미터에 상한값 검증이 필요합니다.

클라이언트가 매우 큰 값(예: 1000000)을 전달하면 서버에 과부하가 걸릴 수 있습니다. @Max 어노테이션을 추가하거나 서비스 레이어에서 최대값을 제한하세요.

🔎 제안하는 수정
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import org.springframework.validation.annotation.Validated;

+@Validated
 @RestController
 @RequestMapping("/api/v1/chat")
 @RequiredArgsConstructor
 public class ChatRoomController implements ChatRoomControllerDocs {
     ...
     @GetMapping("/rooms/{roomId}/messages")
     public ResponseEntity<ApiResponse<MessageListResponse>> getMessages(
             @AuthenticationPrincipal CustomUserDetails userDetails,
             @PathVariable Long roomId,
             @RequestParam(required = false) Long cursor,
-            @RequestParam(defaultValue = "50") int size
+            @RequestParam(defaultValue = "50") @Min(1) @Max(100) int size
     ) {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/team/wego/wegobackend/chat/presentation/ChatRoomController.java
around lines 62 to 73, the request parameter 'size' lacks an upper-bound check
which allows clients to request extremely large page sizes; add validation to
limit 'size' (e.g., annotate with @Max(100) and @Min(1) on the controller
parameter or explicitly clamp it before calling the service) and ensure the
service also enforces a hard cap (e.g., Math.min(size, MAX_SIZE)) to prevent
overloads and malicious requests.

Comment on lines +6 to +8
"email": "ttest@test.com",
"password": "Test1234!@#",
"nickName": "Beemo"
"nickName": "Beemoo"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat src/test/http/auth/auth-api.http

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 1071


회원가입 요청의 이메일과 로그인 요청의 이메일이 일치하지 않습니다.

회원가입에서 "ttest@test.com"으로 가입하지만, 로그인 요청은 "test@example.com"을 사용하고 있어 테스트 플로우가 실패합니다. 로그인 요청의 이메일을 "ttest@test.com"으로 업데이트하세요.

{
-  "email": "test@example.com",
+  "email": "ttest@test.com",
   "password": "Test1234!@#"
}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/test/http/auth/auth-api.http around lines 6 to 8, the signup uses
"ttest@test.com" but the subsequent login uses a different email; update the
login request email to "ttest@test.com" so both requests use the same email and
the test flow succeeds.

@Be-HinD Be-HinD merged commit 99be4df into main Dec 29, 2025
1 check passed
@Be-HinD Be-HinD deleted the feat/chat branch December 29, 2025 14:59
@github-project-automation github-project-automation bot moved this from Backlog to Done in WeGo-Together Backend Dec 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📝documentation Improvements or additions to documentation ✨enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant