Conversation
- 기존 Group 이벤트 기반 리스너 아키텍처 적용 - REST Controller (ChatRoomController) - /api/v1/chat 경로 - Request/Response DTOs - WebSocket 설정 (STOMP) - Service 레이어 (ChatRoomService, ChatMessageService) - yml 및 Security 설정 - Swagger 문서 (ChatRoomControllerDocs) - 아키텍처 문서 및 클라이언트 가이드 문서 작성
WalkthroughWebSocket 기반 실시간 채팅 기능을 추가합니다. 그룹 채팅과 1:1 DM을 지원하며, STOMP 프로토콜을 통한 양방향 통신, 메시지 읽음/읽지 않음 추적, 참여자 관리, 그룹 이벤트 연동을 포함합니다. 의존성 추가, 엔티티 정의, REST API 엔드포인트, 이벤트 리스너 구현, 설정 클래스를 도입합니다. Changes
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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes 변경 사항의 범위와 복잡도:
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
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/roomsLine 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을 사용합니다. 일관성을 위해ChatErrorCode에USER_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:joinedAt과updatedAt이 개별적으로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에서 더 명확하게 표시됩니다.
@ApiResponse에content속성을 추가하여 응답 스키마를 명시하면 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
📒 Files selected for processing (41)
build.gradledocs/채팅서비스_아키텍처.mddocs/채팅서비스_클라이언트_가이드문서.mdsrc/main/java/team/wego/wegobackend/chat/application/dto/request/CreateDmRequest.javasrc/main/java/team/wego/wegobackend/chat/application/dto/request/KickParticipantRequest.javasrc/main/java/team/wego/wegobackend/chat/application/dto/request/SendMessageRequest.javasrc/main/java/team/wego/wegobackend/chat/application/dto/response/ChatMessagePayload.javasrc/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomItemResponse.javasrc/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomListResponse.javasrc/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomResponse.javasrc/main/java/team/wego/wegobackend/chat/application/dto/response/KickNotificationPayload.javasrc/main/java/team/wego/wegobackend/chat/application/dto/response/LastMessageResponse.javasrc/main/java/team/wego/wegobackend/chat/application/dto/response/MessageListResponse.javasrc/main/java/team/wego/wegobackend/chat/application/dto/response/MessageResponse.javasrc/main/java/team/wego/wegobackend/chat/application/dto/response/ParticipantListResponse.javasrc/main/java/team/wego/wegobackend/chat/application/dto/response/ParticipantResponse.javasrc/main/java/team/wego/wegobackend/chat/application/dto/response/ReadStatusResponse.javasrc/main/java/team/wego/wegobackend/chat/application/listener/ChatEventListener.javasrc/main/java/team/wego/wegobackend/chat/application/service/ChatMessageService.javasrc/main/java/team/wego/wegobackend/chat/application/service/ChatRoomService.javasrc/main/java/team/wego/wegobackend/chat/config/ChatProperties.javasrc/main/java/team/wego/wegobackend/chat/config/StompChannelInterceptor.javasrc/main/java/team/wego/wegobackend/chat/config/WebSocketConfig.javasrc/main/java/team/wego/wegobackend/chat/domain/entity/ChatMessage.javasrc/main/java/team/wego/wegobackend/chat/domain/entity/ChatParticipant.javasrc/main/java/team/wego/wegobackend/chat/domain/entity/ChatRoom.javasrc/main/java/team/wego/wegobackend/chat/domain/entity/ChatType.javasrc/main/java/team/wego/wegobackend/chat/domain/entity/JoinType.javasrc/main/java/team/wego/wegobackend/chat/domain/entity/MessageType.javasrc/main/java/team/wego/wegobackend/chat/domain/entity/ParticipantStatus.javasrc/main/java/team/wego/wegobackend/chat/domain/exception/ChatErrorCode.javasrc/main/java/team/wego/wegobackend/chat/domain/exception/ChatException.javasrc/main/java/team/wego/wegobackend/chat/domain/repository/ChatMessageRepository.javasrc/main/java/team/wego/wegobackend/chat/domain/repository/ChatParticipantRepository.javasrc/main/java/team/wego/wegobackend/chat/domain/repository/ChatRoomRepository.javasrc/main/java/team/wego/wegobackend/chat/presentation/ChatMessageController.javasrc/main/java/team/wego/wegobackend/chat/presentation/ChatRoomController.javasrc/main/java/team/wego/wegobackend/chat/presentation/ChatRoomControllerDocs.javasrc/main/java/team/wego/wegobackend/common/security/SecurityEndpoints.javasrc/main/resources/application.ymlsrc/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!
StompPrincipalrecord가Principal인터페이스를 올바르게 구현하고 있습니다.userId를getName()으로 반환하여 메시지 핸들러에서 사용자 식별이 가능합니다.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로 적절한 구조입니다.
lastMessage와unreadCount필드를 통해 클라이언트에서 필요한 정보를 효율적으로 제공합니다.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: 자동 참여/퇴장 설정 일관성 확인
handleGroupJoined는chatProperties.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!
findByGroupId와findExpiredChatRooms는 적절하게 구현되어 있습니다. 특히findExpiredChatRooms에서 파라미터 바인딩을 올바르게 사용하고 있습니다.src/main/java/team/wego/wegobackend/chat/domain/entity/ChatRoom.java (2)
61-72: LGTM!정적 팩토리 메서드를 통한 생성 패턴이 잘 적용되어 있습니다.
createGroupChat과createDmChat으로 의도가 명확하게 드러납니다.
90-98: 양방향 연관관계 관리가 잘 구현되어 있습니다.
addParticipant와addMessage에서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()} |
There was a problem hiding this comment.
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.
| 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.
| // 이미 채팅방이 있는지 확인 | ||
| if (chatRoomRepository.findByGroupId(groupId).isPresent()) { | ||
| log.warn("이미 그룹 채팅방이 존재합니다 - groupId: {}", groupId); | ||
| return chatRoomRepository.findByGroupId(groupId).get(); | ||
| } |
There was a problem hiding this comment.
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.
| 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"); | ||
| } |
There was a problem hiding this comment.
토큰 검증 실패 시 연결이 차단되지 않는 보안 취약점
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.
| 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.
| @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); |
There was a problem hiding this comment.
🧩 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 -5Repository: 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 -50Repository: WeGo-Together/WeGo_BackEnd
Length of output: 574
🏁 Script executed:
cat -n build.gradle | head -60Repository: 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.javaRepository: 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.
| 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() | ||
| ); |
There was a problem hiding this comment.
인증 실패 처리 및 타입 캐스팅 개선 필요
-
Silent failure: 인증 실패 시 클라이언트에 아무런 피드백 없이 리턴합니다. 에러 메시지를 클라이언트에 전송하는 것이 좋습니다.
-
Unchecked cast:
StompPrincipal로의 캐스팅이 실패할 경우ClassCastException이 발생합니다. -
민감 정보 로깅: 메시지 내용(
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.
| @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)); | ||
| } |
There was a problem hiding this comment.
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.
| "email": "ttest@test.com", | ||
| "password": "Test1234!@#", | ||
| "nickName": "Beemo" | ||
| "nickName": "Beemoo" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat src/test/http/auth/auth-api.httpRepository: 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.
📝 Pull Request
📌 PR 종류
해당하는 항목에 체크해주세요.
✨ 변경 내용
기존 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
릴리스 노트
새로운 기능
문서
✏️ Tip: You can customize this high-level summary in your review settings.