Skip to content

Conversation

@uni-j-uni
Copy link
Contributor

@uni-j-uni uni-j-uni commented Dec 15, 2025

✨ 새로운 기능

  • WebSocket(STOMP) 기반 채팅 인증 처리 안정화
  • SockJS 엔드포인트(/ws-chat-sockjs)에 JWT 인증 연동 복구
  • 로그인 사용자만 채팅 메시지 송·수신 가능
  • 채팅 중 사용자 식별 오류(principal=null)로 인한 메시지 실패 제거
  • 채팅방 진입 후 메시지 누락/무반응 현상 해결

🛠 개발 상세(구현 방식 요약)

  • WebSocketConfig에 WebSocket 인증 관련 설정 복구
  • STOMP 연결 시 JWT를 Principal로 변환
  • 메시지 수신 시 인증 정보 유지
  • Handshake 단계: WebSocketAuthInterceptor
  • STOMP 메시지 단계: StompJwtChannelInterceptor

🧪 테스트 방법

  • WebSocket 연결 테스트 (SockJS + STOMP)
  • 로그인 사용자 채팅 메시지 송·수신 확인
  • Principal 정상 주입 여부 로그 확인
  • 다중 사용자 채팅방 메시지 정상 동작 확인

🧩 추가 고려사항

  • 버전 관리 고려 필요 여부 → 없음 (설정 복구 및 인증 연동 수정)
  • 배포 시 별도 주의사항
    • 프론트엔드에서 STOMP CONNECT 시 Authorization 헤더 전달 필수
    • /ws-chat-sockjs 엔드포인트 변경 없음

🔗 관련 문서 / 이슈

Summary by CodeRabbit

  • 새로운 기능
    • 실시간 채팅 지원 추가: 채팅방 생성, 메시지 전송/수신 및 웹소켓(STOMP) 기반 통신
  • 개선 사항
    • 채팅방 목록·메시지 무한 스크롤 페이징 제공
    • 채팅 읽음 처리 및 동기화 개선
    • 교환 게시글에 카테고리 필터 추가 및 카드에 게시글 ID 표시
  • 버그 수정
    • 게시글 조회 시 조회수 중복 증가 문제 수정

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

@uni-j-uni uni-j-uni self-assigned this Dec 15, 2025
@uni-j-uni uni-j-uni added ✨ feature 새로운 기능 요청 🚧 in progress 작업 진행 중 labels Dec 15, 2025
@uni-j-uni uni-j-uni linked an issue Dec 15, 2025 that may be closed by this pull request
1 task
@coderabbitai
Copy link

coderabbitai bot commented Dec 15, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

WebSocket/STOMP 기반 실시간 채팅(엔드포인트, 서비스, 엔티티, 리포지토리, DTO, 매퍼)과 JWT 기반 WebSocket 인증(핸드셰이크 및 STOMP 채널 인터셉터)을 추가하고, 교환 게시글의 exchangeCategory 필터 및 관련 DTO/리포지토리/서비스 시그니처를 도입했습니다. build.gradle에 WebSocket 의존성도 추가되었습니다.

Changes

Cohort / File(s) Summary
빌드 설정
build.gradle
Spring WebSocket 스타터(org.springframework.boot:spring-boot-starter-websocket) 의존성 추가.
채팅 컨트롤러
src/main/java/com/sku/refit/domain/chat/controller/ChatController.java, src/main/java/com/sku/refit/domain/chat/controller/ChatControllerImpl.java, src/main/java/com/sku/refit/domain/chat/controller/ChatMessageController.java
REST 및 STOMP 엔드포인트(채팅방 생성/조회, 메시지 조회/읽음, STOMP 메시지 수신) 인터페이스 및 구현 추가. Swagger/OpenAPI 주석 포함.
채팅 서비스
src/main/java/com/sku/refit/domain/chat/service/ChatService.java, src/main/java/com/sku/refit/domain/chat/service/ChatServiceImpl.java
채팅방 생성, 메시지 전송(저장 및 SimpMessagingTemplate 브로드캐스트), 채팅 목록/메시지 페이징, 읽음 처리 등 비즈니스 로직 추가. 트랜잭션 경계 및 예외 처리 포함.
엔티티 / 예외
src/main/java/com/sku/refit/domain/chat/entity/ChatRoom.java, src/main/java/com/sku/refit/domain/chat/entity/ChatMessage.java, src/main/java/com/sku/refit/domain/chat/exception/ChatErrorCode.java
JPA 엔티티 ChatRoom, ChatMessage 추가(관계·제약·읽음/삭제 플래그 등) 및 채팅 전용 에러 코드 추가.
DTO / 매퍼
src/main/java/com/sku/refit/domain/chat/dto/request/ChatMessageRequest.java, src/main/java/com/sku/refit/domain/chat/dto/response/ChatRoomResponse.java, src/main/java/com/sku/refit/domain/chat/dto/response/ChatMessageResponse.java, src/main/java/com/sku/refit/domain/chat/mapper/ChatMapper.java
요청/응답 DTO 추가 및 엔티티↔DTO 매핑 유틸리티 구현(Builder·Schema 주석 포함).
리포지토리
src/main/java/com/sku/refit/domain/chat/repository/ChatRoomRepository.java, src/main/java/com/sku/refit/domain/chat/repository/ChatMessageRepository.java
채팅방/메시지 조회용 Spring Data JPA 리포지토리 추가. 사용자별/무한스크롤 조회용 커스텀 JPQL 쿼리 포함.
WebSocket 구성 및 인터셉터
src/main/java/com/sku/refit/global/config/WebSocketConfig.java, src/main/java/com/sku/refit/global/interceptor/WebSocketAuthInterceptor.java, src/main/java/com/sku/refit/global/interceptor/StompJwtChannelInterceptor.java
STOMP 엔드포인트(/ws-chat-sockjs) 및 브로커(/sub, /pub) 설정, 핸드셰이크 시 토큰 전달, CONNECT 프레임에서 JWT 검증 후 Authentication 주입 로직 추가.
교환 게시글: 카테고리 필터링 및 DTO 변경
src/main/java/com/sku/refit/domain/exchange/controller/ExchangeController.java, src/main/java/com/sku/refit/domain/exchange/controller/ExchangeControllerImpl.java, src/main/java/com/sku/refit/domain/exchange/dto/response/ExchangePostCardResponse.java, src/main/java/com/sku/refit/domain/exchange/mapper/ExchangeMapper.java, src/main/java/com/sku/refit/domain/exchange/repository/ExchangeRepository.java, src/main/java/com/sku/refit/domain/exchange/service/ExchangeService.java, src/main/java/com/sku/refit/domain/exchange/service/ExchangeServiceImpl.java
위치 기반 조회에 exchangeCategory 파라미터 추가(컨트롤러·서비스 시그니처 변경, 리포지토리 쿼리 추가). ExchangePostCardResponseexchangePostId 필드 추가 및 매퍼 반영.
게시글 카테고리 처리 개선
src/main/java/com/sku/refit/domain/post/controller/PostController.java, src/main/java/com/sku/refit/domain/post/repository/PostRepository.java, src/main/java/com/sku/refit/domain/post/service/PostServiceImpl.java
컨트롤러 파라미터 주석 보강, 리포지토리에서 카테고리 타입을 PostCategory로 변경, 서비스에서 문자열→enum 변환 및 유효성 검사 추가, 중복 뷰 증가 제거.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client (Browser)
    participant WS as WebSocket Server\n(STOMP)
    participant HS as WebSocketAuthInterceptor
    participant CH as StompJwtChannelInterceptor
    participant Auth as JwtProvider / UserDetailsService
    participant Service as ChatServiceImpl
    participant DB as Database
    participant Broker as SimpleBroker (/sub)

    Client->>WS: WS 핸드셰이크 (Authorization 헤더)
    WS->>HS: beforeHandshake (Authorization 저장)
    HS-->>WS: token 속성 저장

    Client->>WS: STOMP CONNECT
    WS->>CH: preSend (CONNECT)
    CH->>Auth: JWT 검증 → UserDetails 조회
    Auth-->>CH: UserDetails
    CH->>WS: Authentication/Principal 설정
    WS-->>Client: CONNECTED

    Client->>WS: SEND /pub/chat/send (ChatMessageRequest)
    WS->>Service: ChatMessageController.sendMessage
    Service->>DB: ChatMessage 저장 / ChatRoom 업데이트
    Service->>Broker: convertAndSend(/sub/chat/{roomId}, ChatMessageResponse)
    Broker-->>Client: 구독자에게 메시지 브로드캐스트
Loading
sequenceDiagram
    participant Client as Client (REST)
    participant Ctrl as ChatControllerImpl
    participant Service as ChatServiceImpl
    participant PostRepo as ExchangeRepository
    participant RoomRepo as ChatRoomRepository
    participant MsgRepo as ChatMessageRepository
    participant DB as Database

    Client->>Ctrl: POST /api/chats/exchange/{postId}
    Ctrl->>Service: createChatRoom(postId)
    Service->>PostRepo: ExchangePost 조회
    Service->>RoomRepo: findByExchangePostIdAndUsers(...)
    alt 기존 방 존재
        RoomRepo-->>Service: ChatRoom 반환
    else 새 방 생성
        Service->>DB: ChatRoom 저장
        DB-->>Service: ChatRoom
    end
    Service-->>Ctrl: ChatRoomResponse
    Ctrl-->>Client: 200 OK

    Client->>Ctrl: GET /api/chats/rooms/{roomId}/messages
    Ctrl->>Service: getMessages(roomId, lastChatId, size)
    Service->>MsgRepo: findMessages(roomId, lastChatId, pageable)
    MsgRepo-->>Service: 메시지 리스트
    Service-->>Ctrl: InfiniteResponse<ChatMessageResponse>
    Ctrl-->>Client: 200 OK + 메시지 목록
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45분

  • 추가로 주의할 파일/영역:
    • StompJwtChannelInterceptor, WebSocketAuthInterceptor의 토큰 추출·인증 및 예외 처리 흐름.
    • ChatServiceImpl.sendMessage의 트랜잭션·동시성(채팅룸 존재 확인, 메시지 저장·브로드캐스트) 경계.
    • JPQL 쿼리 및 페이징 로직 (ChatRoomRepository, ChatMessageRepository, ExchangeRepository)의 성능·정확성.
    • ExchangeServiceImpl의 exchangeCategory 파싱·검증 경로 및 쿼리 선택 로직.

Possibly related PRs

Poem

🐰 채팅 길 닦았어요, 털구름 발자국 쿵쿵,
톡 하나가 날아와 방을 채우고,
토큰은 문지기, STOMP는 다리,
메시지 춤추며 구독자에게 닿네 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% 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 PR 제목은 웹소켓 기반 채팅 기능 구현이라는 주요 변경사항을 명확하게 요약하고 있으며, 변경 사항의 핵심을 정확히 반영합니다.
Description check ✅ Passed PR 설명은 제공된 템플릿의 모든 주요 섹션을 포함하고 있으며, 새로운 기능, 개발 상세, 테스트 방법, 고려사항, 관련 이슈가 구체적으로 기술되어 있습니다.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 59f12f0 and 93e6daf.

📒 Files selected for processing (4)
  • src/main/java/com/sku/refit/domain/chat/controller/ChatControllerImpl.java (1 hunks)
  • src/main/java/com/sku/refit/domain/chat/controller/ChatMessageController.java (1 hunks)
  • src/main/java/com/sku/refit/domain/chat/entity/ChatRoom.java (1 hunks)
  • src/main/java/com/sku/refit/domain/exchange/controller/ExchangeController.java (2 hunks)

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

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/sku/refit/domain/exchange/mapper/ExchangeMapper.java (1)

66-74: getFirst() 호출 시 빈 리스트에 대한 예외 발생 가능

imageUrlList가 비어있을 경우 getFirst() 호출 시 NoSuchElementException이 발생합니다. ExchangeServiceImpl.createExchangePost에서 이미지 없이 게시글 생성이 가능하므로 방어 코드가 필요합니다.

   public ExchangePostCardResponse toCardResponse(ExchangePost exchangePost) {
+    List<String> imageUrlList = exchangePost.getImageUrlList();
+    String thumbnailUrl = (imageUrlList != null && !imageUrlList.isEmpty())
+        ? imageUrlList.getFirst()
+        : null;
+
     return ExchangePostCardResponse.builder()
         .exchangePostId(exchangePost.getId())
-        .thumbnailImageUrl(exchangePost.getImageUrlList().getFirst())
+        .thumbnailImageUrl(thumbnailUrl)
         .category(exchangePost.getExchangeCategory())
         .title(exchangePost.getTitle())
         .exchangeSpot(exchangePost.getExchangeSpot())
         .build();
   }
🧹 Nitpick comments (10)
src/main/java/com/sku/refit/domain/exchange/service/ExchangeService.java (1)

28-37: Javadoc에 exchangeCategory 파라미터 설명 누락

새로 추가된 exchangeCategory 파라미터에 대한 @param 문서가 Javadoc에 누락되어 있습니다.

   /**
    * 교환 게시글 위치 기반 조회 (페이지네이션)
    *
    * @param pageable 페이지 정보
+   * @param exchangeCategory 교환 카테고리 (선택, null 또는 빈 문자열일 경우 전체 조회)
    * @param latitude 사용자 위도
    * @param longitude 사용자 경도
    * @return 교환 게시글 카드 페이지 응답
    */
src/main/java/com/sku/refit/domain/post/controller/PostController.java (1)

30-72: 카테고리 파라미터에 대한 Swagger 메타데이터 추가는 적절하며, enum 값과의 동기화만 주의하면 좋겠습니다.

  • @Schema(allowableValues = {"FREE", "REPAIR", "INFO"}, example = "FREE") 추가로 API 문서 관점에서 카테고리 입력 제약이 명확해져서 좋습니다.
  • 서비스 계층에서 PostCategory.valueOf(category) 를 사용하고 있으므로, PostCategory enum 정의와 여기 allowableValues 값이 항상 1:1 로 일치하도록 관리해 주셔야 합니다. 추후 enum 값이 추가/변경될 경우 이 부분도 함께 업데이트해야 문서와 실제 동작이 어긋나지 않습니다.

그 외 시그니처나 동작 변화는 없고, 변경 사항은 문서 품질 향상 수준이라 그대로 유지해도 무방해 보입니다.

src/main/java/com/sku/refit/domain/chat/dto/request/ChatMessageRequest.java (1)

13-17: 입력 유효성 검사 어노테이션 추가를 권장합니다.

WebSocket 메시지에서도 유효성 검사가 중요합니다. roomIdcontent에 검증 어노테이션을 추가하면 잘못된 요청을 조기에 차단할 수 있습니다.

+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+
 @Getter
 @NoArgsConstructor
 @AllArgsConstructor
 public class ChatMessageRequest {

+  @NotNull
   private Long roomId;
+
+  @NotBlank
+  @Size(max = 1000)
   private String content;
 }

참고: STOMP 메시지 핸들러에서 @Validated를 사용하려면 추가 설정이 필요할 수 있습니다.

src/main/java/com/sku/refit/global/config/WebSocketConfig.java (1)

27-44: 와일드카드 Origin은 운영 환경에서 도메인 제한을 고려해 주세요

setAllowedOriginPatterns("*")로 모든 Origin을 허용하고 있어서 개발 단계에서는 편하지만, 운영 환경에서는 불필요하게 표면적이 넓어질 수 있습니다.
실제 배포 시에는 프론트엔드 도메인(들)로 한정하거나, 설정 값으로 주입해서 프로파일별로 다르게 가져가는 형태도 고려해 보시면 좋겠습니다.

-        .setAllowedOriginPatterns("*")
+        .setAllowedOriginPatterns("https://your-frontend.example.com")

또는 허용 Origin을 설정 파일에서 읽어오는 방향도 좋습니다.

src/main/java/com/sku/refit/domain/chat/entity/ChatMessage.java (1)

25-54: 엔티티 매핑은 적절하고, 도메인 메서드는 이후 확장 포인트로 두면 좋겠습니다

연관관계, NOT NULL 제약, 기본값 설정 모두 자연스럽고, 현재 요구사항 기준으로는 충분해 보입니다.
추후 읽음 처리(isRead)나 소프트 삭제(isDeleted) 관련 도메인 규칙이 복잡해지면, 플래그를 외부에서 직접 세팅하기보다는 markAsRead, softDelete 같은 도메인 메서드로 캡슐화하는 방향도 고려해 볼 수 있을 것 같습니다.

src/main/java/com/sku/refit/domain/chat/service/ChatServiceImpl.java (1)

78-79: @Override 어노테이션 누락

sendMessage 메서드에 @Override 어노테이션이 누락되었습니다. 인터페이스 메서드 구현임을 명시해 주세요.

   @Transactional
+  @Override
   public void sendMessage(ChatMessageRequest request, Principal principal) {
src/main/java/com/sku/refit/global/interceptor/StompJwtChannelInterceptor.java (1)

37-37: NPE 대신 graceful 처리 고려

Objects.requireNonNull(accessor)는 accessor가 null일 경우 NPE를 발생시킵니다. STOMP 프레임워크에서 accessor가 null인 경우는 드물지만, 방어적으로 null 체크 후 메시지를 그대로 반환하는 것이 더 안전할 수 있습니다.

-    if (StompCommand.CONNECT.equals(Objects.requireNonNull(accessor).getCommand())) {
+    if (accessor == null) {
+      return message;
+    }
+
+    if (StompCommand.CONNECT.equals(accessor.getCommand())) {
src/main/java/com/sku/refit/domain/chat/controller/ChatController.java (2)

1-8: 중복된 저작권 주석

저작권 주석이 두 번 포함되어 있습니다. 하나를 제거해 주세요.

 /* 
  * Copyright (c) SKU 다시입을Lab 
  */
 package com.sku.refit.domain.chat.controller;
 
-/*
- * Copyright (c) SKU 다시입을Lab
- */
-
 import org.springframework.http.ResponseEntity;

45-52: @Operation 어노테이션 누락

getMessages 엔드포인트에 @Operation 어노테이션이 누락되어 Swagger 문서에 설명이 표시되지 않습니다.

   @GetMapping("/rooms/{roomId}/messages")
+  @Operation(summary = "채팅 메시지 조회", description = "특정 채팅방의 메시지 내역을 조회합니다.")
   ResponseEntity<BaseResponse<InfiniteResponse<ChatMessageResponse>>> getMessages(
src/main/java/com/sku/refit/domain/chat/repository/ChatRoomRepository.java (1)

20-22: findByExchangePostIdAndSenderIdAndReceiverId 메서드 제거 검토

해당 메서드가 코드베이스 어디서도 사용되지 않고 있습니다. ChatServiceImpl에서는 sender/receiver 순서와 무관하게 양방향 조회를 수행하는 findByExchangePostIdAndUsers 메서드만 사용하고 있으므로, 불필요한 findByExchangePostIdAndSenderIdAndReceiverId 메서드는 제거해 주세요.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 63ed0cf and 67b5775.

📒 Files selected for processing (28)
  • build.gradle (1 hunks)
  • src/main/java/com/sku/refit/domain/chat/controller/ChatController.java (1 hunks)
  • src/main/java/com/sku/refit/domain/chat/controller/ChatControllerImpl.java (1 hunks)
  • src/main/java/com/sku/refit/domain/chat/controller/ChatMessageController.java (1 hunks)
  • src/main/java/com/sku/refit/domain/chat/dto/request/ChatMessageRequest.java (1 hunks)
  • src/main/java/com/sku/refit/domain/chat/dto/response/ChatMessageResponse.java (1 hunks)
  • src/main/java/com/sku/refit/domain/chat/dto/response/ChatRoomResponse.java (1 hunks)
  • src/main/java/com/sku/refit/domain/chat/entity/ChatMessage.java (1 hunks)
  • src/main/java/com/sku/refit/domain/chat/entity/ChatRoom.java (1 hunks)
  • src/main/java/com/sku/refit/domain/chat/exception/ChatErrorCode.java (1 hunks)
  • src/main/java/com/sku/refit/domain/chat/mapper/ChatMapper.java (1 hunks)
  • src/main/java/com/sku/refit/domain/chat/repository/ChatMessageRepository.java (1 hunks)
  • src/main/java/com/sku/refit/domain/chat/repository/ChatRoomRepository.java (1 hunks)
  • src/main/java/com/sku/refit/domain/chat/service/ChatService.java (1 hunks)
  • src/main/java/com/sku/refit/domain/chat/service/ChatServiceImpl.java (1 hunks)
  • src/main/java/com/sku/refit/domain/exchange/controller/ExchangeController.java (2 hunks)
  • src/main/java/com/sku/refit/domain/exchange/controller/ExchangeControllerImpl.java (2 hunks)
  • src/main/java/com/sku/refit/domain/exchange/dto/response/ExchangePostCardResponse.java (1 hunks)
  • src/main/java/com/sku/refit/domain/exchange/mapper/ExchangeMapper.java (1 hunks)
  • src/main/java/com/sku/refit/domain/exchange/repository/ExchangeRepository.java (2 hunks)
  • src/main/java/com/sku/refit/domain/exchange/service/ExchangeService.java (1 hunks)
  • src/main/java/com/sku/refit/domain/exchange/service/ExchangeServiceImpl.java (2 hunks)
  • src/main/java/com/sku/refit/domain/post/controller/PostController.java (2 hunks)
  • src/main/java/com/sku/refit/domain/post/repository/PostRepository.java (1 hunks)
  • src/main/java/com/sku/refit/domain/post/service/PostServiceImpl.java (1 hunks)
  • src/main/java/com/sku/refit/global/config/WebSocketConfig.java (1 hunks)
  • src/main/java/com/sku/refit/global/interceptor/StompJwtChannelInterceptor.java (1 hunks)
  • src/main/java/com/sku/refit/global/interceptor/WebSocketAuthInterceptor.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/main/java/com/sku/refit/global/interceptor/WebSocketAuthInterceptor.java (1)
src/main/java/com/sku/refit/global/interceptor/StompJwtChannelInterceptor.java (1)
  • Component (24-55)
src/main/java/com/sku/refit/domain/chat/entity/ChatMessage.java (4)
src/main/java/com/sku/refit/domain/chat/entity/ChatRoom.java (1)
  • Entity (29-74)
src/main/java/com/sku/refit/domain/chat/dto/request/ChatMessageRequest.java (1)
  • Getter (10-17)
src/main/java/com/sku/refit/domain/chat/dto/response/ChatMessageResponse.java (1)
  • Getter (12-31)
src/main/java/com/sku/refit/domain/chat/dto/response/ChatRoomResponse.java (1)
  • Getter (12-31)
src/main/java/com/sku/refit/domain/post/controller/PostController.java (1)
src/main/java/com/sku/refit/domain/event/dto/response/EventResponse.java (1)
  • Schema (14-196)
src/main/java/com/sku/refit/domain/chat/dto/request/ChatMessageRequest.java (2)
src/main/java/com/sku/refit/domain/chat/dto/response/ChatMessageResponse.java (1)
  • Getter (12-31)
src/main/java/com/sku/refit/domain/chat/dto/response/ChatRoomResponse.java (1)
  • Getter (12-31)
src/main/java/com/sku/refit/global/interceptor/StompJwtChannelInterceptor.java (1)
src/main/java/com/sku/refit/global/interceptor/WebSocketAuthInterceptor.java (1)
  • Component (15-46)
🪛 GitHub Actions: CI
src/main/java/com/sku/refit/domain/post/service/PostServiceImpl.java

[error] 170-170: cannot find symbol
.findByPostCategoryAndIdLessThan(postCategory, lastPostId, pageable)
^
symbol: method findByPostCategoryAndIdLessThan(PostCategory,Long,Pageable)
location: variable postRepository of type PostRepository

🔇 Additional comments (18)
src/main/java/com/sku/refit/domain/exchange/repository/ExchangeRepository.java (1)

41-58: LGTM!

카테고리 필터링을 위한 새로운 쿼리 메서드가 기존 findByDistanceAndStatus와 일관된 패턴으로 잘 구현되었습니다. ST_Distance_Sphere 함수를 사용한 거리 기반 정렬과 카테고리 필터 조합이 올바르게 구성되어 있습니다.

src/main/java/com/sku/refit/domain/exchange/dto/response/ExchangePostCardResponse.java (1)

17-18: LGTM!

교환 게시글 식별자 필드가 적절한 Swagger 문서화와 함께 추가되었습니다.

src/main/java/com/sku/refit/domain/exchange/service/ExchangeServiceImpl.java (2)

84-103: LGTM!

카테고리 필터링 로직이 깔끔하게 구현되었습니다. null/빈 문자열 체크와 IllegalArgumentException에 대한 예외 처리가 적절합니다.


107-110: 로깅 추가 승인

읽기 작업에 대한 로깅이 추가되어 접근 패턴 추적에 유용합니다.

Also applies to: 125-126

src/main/java/com/sku/refit/domain/exchange/controller/ExchangeControllerImpl.java (1)

45-49: 카테고리 파라미터 추가 및 서비스 연동은 구현상 문제 없어 보입니다

컨트롤러 구현이 인터페이스 시그니처를 잘 따라가고 있고, exchangeCategory를 그대로 서비스로 전달하는 흐름도 자연스럽습니다. 페이지 번호/크기 검증 로직에도 영향이 없어서 이 파일 내에서는 별도 이슈는 없어 보입니다.

Also applies to: 61-62

src/main/java/com/sku/refit/domain/exchange/controller/ExchangeController.java (1)

31-31: Schema import 추가는 현재 사용과 잘 맞습니다

exchangeCategory 파라미터에 @Schema를 사용하고 있어 해당 import 추가는 필요하고, 불필요한 의존성도 아닙니다.

build.gradle (1)

64-66: LGTM!

WebSocket/STOMP 지원을 위한 올바른 의존성이 추가되었습니다. Spring Boot BOM에서 버전을 관리하므로 명시적 버전 지정이 필요 없습니다.

src/main/java/com/sku/refit/domain/chat/exception/ChatErrorCode.java (1)

13-22: LGTM!

에러 코드 구조가 올바르게 구현되었습니다. 현재는 CHAT_NOT_FOUND만 정의되어 있는데, 채팅 기능이 확장됨에 따라 CHAT_ROOM_NOT_FOUND, CHAT_UNAUTHORIZED, CHAT_MESSAGE_TOO_LONG 등 추가 에러 코드가 필요할 수 있습니다.

src/main/java/com/sku/refit/domain/chat/repository/ChatMessageRepository.java (1)

16-28: LGTM! 커서 기반 페이지네이션이 잘 구현되었습니다.

lastId를 사용한 keyset 페이지네이션은 대용량 채팅 메시지에서 offset 기반보다 성능이 좋습니다.

참고: @Repository 어노테이션은 JpaRepository를 확장하는 인터페이스에서는 자동 감지되므로 생략해도 됩니다. 유지해도 문제는 없습니다.

src/main/java/com/sku/refit/domain/chat/mapper/ChatMapper.java (1)

18-40: 매퍼 구현 깔끔합니다

엔티티 → DTO 필드 매핑이 도메인/DTO 정의와 잘 맞고, 불필요한 로직 없이 단순 매핑만 수행해서 유지보수하기 좋아 보입니다. 상위 서비스에서 chatRoom, chatMessage 연관 관계를 적절히 로딩해준다는 전제라면 별도 수정 필요 없어 보입니다.

src/main/java/com/sku/refit/domain/chat/service/ChatService.java (1)

13-23: 서비스 인터페이스 구성 적절합니다

채팅방 생성, 메시지 전송, 무한 스크롤 조회, 읽음 처리까지 핵심 유즈케이스가 잘 정리되어 있고, Principal을 send 쪽에만 받는 것도 책임 분리가 명확해서 좋습니다. size, lastChatRoomId/lastChatId에 대한 최소/최대 값 검증은 컨트롤러나 구현체 레벨에서만 보완해 주면 될 것 같습니다.

src/main/java/com/sku/refit/global/interceptor/WebSocketAuthInterceptor.java (1)

18-45: 핸드셰이크 인터셉터 역할이 명확합니다

HTTP 핸드셰이크 단계에서 Authorization 헤더만 세션 attribute(token)로 보존하고, 연결 자체는 항상 허용한 뒤 STOMP 레벨에서 JWT 인증을 거는 구조라 의도와 잘 맞습니다. 현재처럼 복잡한 검증 없이 토큰만 넘겨두고, 실제 인증은 StompJwtChannelInterceptor가 담당하도록 분리한 점이 깔끔합니다.

src/main/java/com/sku/refit/domain/chat/dto/response/ChatRoomResponse.java (1)

12-30: 채팅방 응답 DTO 정의가 명확합니다

필드 구성이 엔티티/매퍼와 잘 맞고, Swagger 메타데이터도 충분해서 API 문서 측면에서 바로 활용 가능해 보입니다. 현재 요구사항 기준으로는 추가 수정 없이 사용 가능해 보입니다.

src/main/java/com/sku/refit/domain/chat/dto/response/ChatMessageResponse.java (1)

12-30: 채팅 메시지 응답 DTO 구성 적절합니다

메시지 식별자, 방 ID, 발신자 닉네임, 내용, 생성 시각까지 기본적으로 필요한 정보가 모두 포함되어 있고, 빌더 패턴과 Swagger 스키마도 잘 붙어 있어서 사용성과 문서화 측면 모두 무난해 보입니다.

src/main/java/com/sku/refit/domain/chat/repository/ChatRoomRepository.java (2)

25-33: 커서 기반 페이지네이션 구현 적절

JPQL 쿼리가 lastId 파라미터를 사용한 커서 기반 페이지네이션을 올바르게 구현하고 있으며, lastMessageAt DESC NULLS LAST 정렬로 최신 메시지 기준 정렬을 지원합니다.


35-48: 양방향 사용자 조회 쿼리 적절

sender/receiver 순서와 무관하게 채팅방을 조회할 수 있도록 OR 조건을 사용한 구현이 적절합니다. 이를 통해 채팅방 중복 생성을 효과적으로 방지할 수 있습니다.

src/main/java/com/sku/refit/domain/chat/controller/ChatControllerImpl.java (1)

23-28: 채팅방 생성 구현 적절

서비스 계층에 위임하고 BaseResponse로 일관되게 래핑하는 구현이 적절합니다.

src/main/java/com/sku/refit/domain/chat/controller/ChatController.java (1)

27-57: API 설계 적절

커서 기반 페이지네이션(lastChatRoomId, lastChatId)과 RESTful 엔드포인트 구조가 잘 설계되어 있습니다. Swagger 어노테이션을 통한 API 문서화도 적절합니다.

Comment on lines +12 to 20
import com.sku.refit.domain.post.entity.PostCategory;

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {

Page<Post> findByPostCategoryContaining(String category, Pageable pageable);
Page<Post> findByPostCategory(PostCategory category, Pageable pageable);

Page<Post> findByPostCategoryContainingAndIdLessThan(
String category, Long lastPostId, Pageable pageable);
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

🧩 Analysis chain

🏁 Script executed:

# 1. Find all usages of the String-based method
rg -n "findByPostCategoryContainingAndIdLessThan" --type=java -C2

# 2. Also search for any usage of the PostRepository methods
echo "=== Searching for PostRepository method calls ==="
rg -n "findByPostCategory" --type=java -C2

Repository: refit-lab/refit-be

Length of output: 2401


🏁 Script executed:

# 3. Find the PostServiceImpl to check actual method calls
fd -n "PostServiceImpl.java" -x cat {}

Repository: refit-lab/refit-be

Length of output: 292


🏁 Script executed:

# 4. Check if findByPostCategoryAndIdLessThan is being called anywhere
rg -n "findByPostCategoryAndIdLessThan" --type=java -C2

Repository: refit-lab/refit-be

Length of output: 583


PostCategory 기반 조회 메서드 시그니처가 Service와 불일치합니다 (컴파일 에러 원인).

PostServiceImpl의 170줄에서
postRepository.findByPostCategoryAndIdLessThan(postCategory, lastPostId, pageable)
를 호출하지만, PostRepository에는 해당 메서드가 없고 대신 다음 메서드만 존재합니다:

Page<Post> findByPostCategoryContainingAndIdLessThan(
    String category, Long lastPostId, Pageable pageable);

메서드명(AndIdLessThan vs ContainingAndIdLessThan)과 매개변수 타입(PostCategory vs String)이 모두 불일치하여 컴파일 에러가 발생합니다.

PostCategory enum으로 통일하려는 의도가 명확하고 기존 String 기반 메서드는 사용되지 않으므로, 아래와 같이 수정하는 것을 권장합니다:

-  Page<Post> findByPostCategoryContainingAndIdLessThan(
-      String category, Long lastPostId, Pageable pageable);
+  Page<Post> findByPostCategoryAndIdLessThan(
+      PostCategory category, Long lastPostId, Pageable pageable);
🤖 Prompt for AI Agents
In src/main/java/com/sku/refit/domain/post/repository/PostRepository.java around
lines 12 to 20, the repository method signatures disagree with PostServiceImpl:
the service calls findByPostCategoryAndIdLessThan(PostCategory, Long, Pageable)
but the repository currently declares
findByPostCategoryContainingAndIdLessThan(String, Long, Pageable). Replace or
add a repository method that uses the PostCategory enum and the exact name used
by the service: declare Page<Post> findByPostCategoryAndIdLessThan(PostCategory
category, Long lastPostId, Pageable pageable); and remove the unused
String-based Containing variant to restore compile-time compatibility.

Comment on lines 158 to 171
PostCategory postCategory;
try {
postCategory = PostCategory.valueOf(category);
} catch (IllegalArgumentException e) {
throw new CustomException(PostErrorCode.INVALID_CATEGORY);
}

if (lastPostId == null) {
posts = postRepository.findByPostCategoryContaining(category, pageable).getContent();
posts = postRepository.findByPostCategory(postCategory, pageable).getContent();
} else {
posts =
postRepository
.findByPostCategoryContainingAndIdLessThan(category, lastPostId, pageable)
.findByPostCategoryAndIdLessThan(postCategory, lastPostId, pageable)
.getContent();
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

카테고리 enum 파싱은 적절하지만, 레포지토리 시그니처와의 정합성 정리가 필요합니다.

  • PostCategory.valueOf(category)try-catch 로 감싸서 INVALID_CATEGORY 로 매핑하는 흐름은 createPost 와도 일관되고, 유효성 처리 측면에서 적절해 보입니다.
  • 다만 아래 호출부에서 사용하는 메서드는 현재 PostRepository 에 선언되어 있지 않아 CI 가 실패 중입니다.
posts = postRepository.findByPostCategory(postCategory, pageable).getContent();
// ...
posts =
    postRepository
        .findByPostCategoryAndIdLessThan(postCategory, lastPostId, pageable)
        .getContent();

앞선 PostRepository 리뷰에서 제안드린 것처럼 레포지토리 시그니처를

Page<Post> findByPostCategoryAndIdLessThan(PostCategory category, Long lastPostId, Pageable pageable);

형태로 맞추면 이 서비스 코드는 그대로 컴파일·동작할 수 있을 것 같습니다.
레포지토리 변경 후에도 추가적인 컴파일 에러가 없는지 한 번 전체 빌드를 돌려서 확인 부탁드립니다.

🧰 Tools
🪛 GitHub Actions: CI

[error] 170-170: cannot find symbol
.findByPostCategoryAndIdLessThan(postCategory, lastPostId, pageable)
^
symbol: method findByPostCategoryAndIdLessThan(PostCategory,Long,Pageable)
location: variable postRepository of type PostRepository

🤖 Prompt for AI Agents
In src/main/java/com/sku/refit/domain/post/service/PostServiceImpl.java around
lines 158-171 the category parsing is fine but the repository methods used do
not exist; update PostRepository to declare the needed query signatures so this
service compiles: ensure there is a Page<Post> findByPostCategory(PostCategory
category, Pageable pageable) and a Page<Post>
findByPostCategoryAndIdLessThan(PostCategory category, Long lastPostId, Pageable
pageable) (or the equivalent custom query methods), then run a full build to
catch any remaining compile errors.

Comment on lines +41 to +51
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);

String username = jwtProvider.getUsernameFromToken(token);
UserDetails user = userDetailsService.loadUserByUsername(username);

Authentication authentication =
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());

accessor.setUser(authentication); // ★ 핵심
}
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

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "JwtProvider.java" | head -20

Repository: refit-lab/refit-be

Length of output: 119


🏁 Script executed:

rg -l "class JwtProvider" --type=java

Repository: refit-lab/refit-be

Length of output: 117


🏁 Script executed:

rg -n "getUsernameFromToken|validateToken" --type=java -B 2 -A 10 | head -100

Repository: refit-lab/refit-be

Length of output: 10078


🏁 Script executed:

cat -n src/main/java/com/sku/refit/global/jwt/JwtProvider.java | head -250

Repository: refit-lab/refit-be

Length of output: 8680


🏁 Script executed:

rg -n "validateToken" src/main/java/com/sku/refit/global/jwt/JwtProvider.java -A 8

Repository: refit-lab/refit-be

Length of output: 656


🏁 Script executed:

cat -n src/main/java/com/sku/refit/global/interceptor/StompJwtChannelInterceptor.java

Repository: refit-lab/refit-be

Length of output: 2387


JWT 토큰 검증 및 예외 처리 누락

getUsernameFromToken() 호출 시 토큰 유효성 검증(서명, 만료, 블랙리스트 등)이 명시적으로 수행되지 않습니다. 내부적으로 parseClaimsJws()가 유효성을 검사하지만, 예외가 처리되지 않아 무효한 토큰이 들어오면 WebSocket 연결이 적절한 에러 메시지 없이 실패합니다.

      if (authHeader != null && authHeader.startsWith("Bearer ")) {
        String token = authHeader.substring(7);

+       if (jwtProvider.validateToken(token)) {
          String username = jwtProvider.getUsernameFromToken(token);
          UserDetails user = userDetailsService.loadUserByUsername(username);

          Authentication authentication =
              new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());

          accessor.setUser(authentication);
+       }
      }

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

🤖 Prompt for AI Agents
In
src/main/java/com/sku/refit/global/interceptor/StompJwtChannelInterceptor.java
around lines 41 to 51, the code calls jwtProvider.getUsernameFromToken(token)
without explicit validation or exception handling; update this block to
explicitly validate the JWT and handle parsing/validation exceptions: call the
provider’s parse/validate method (or wrap getUsernameFromToken) inside a
try/catch that catches signature/expired/malformed JWT exceptions and other
runtime errors, log the failure with token context (avoid logging raw token in
production), and prevent authentication from being set when validation fails
(e.g., do not call accessor.setUser(...), optionally send an error STOMP frame
or close the connection/throw a suitable AuthenticationException so the client
receives a clear error). Ensure successful validation continues to
loadUserByUsername and set the Authentication as before.

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

♻️ Duplicate comments (1)
src/main/java/com/sku/refit/domain/post/service/PostServiceImpl.java (1)

165-171: 카테고리 기반 조회 분기에서 enum/문자열 기준이 혼재되어 있어 시그니처와 동작이 어긋날 수 있습니다.

현재 분기별 repo 호출이 다음과 같이 서로 다른 기준을 사용하고 있습니다.

  • lastPostId == null: findByPostCategory(postCategory, pageable)PostCategory enum 기반
  • lastPostId != null: findByPostCategoryContainingAndIdLessThan(category, lastPostId, pageable)String 기반 + Containing

이전 리뷰에서도 지적되었듯이, 카테고리 필터링을 enum 로 통일하는 리팩터링을 진행 중이라면, else 분기 역시 enum 기반 메서드 시그니처로 맞추는 편이 타입 안정성과 유지보수성 측면에서 좋습니다. 또한 레포지토리에서 findByPostCategoryContainingAndIdLessThan 를 제거하거나 시그니처를 변경했다면 현재 코드는 컴파일 에러를 유발할 수 있습니다.

서비스/레포지토리 양쪽에서 최종적으로 아래와 같은 형태로 통일하는 것을 권장합니다.

-      posts =
-          postRepository
-              .findByPostCategoryContainingAndIdLessThan(category, lastPostId, pageable)
-              .getContent();
+      posts =
+          postRepository
+              .findByPostCategoryAndIdLessThan(postCategory, lastPostId, pageable)
+              .getContent();

그리고 PostRepository 에도 이에 맞는 시그니처를 선언해 두셨는지 한 번 더 확인해 주세요.

🧹 Nitpick comments (1)
src/main/java/com/sku/refit/domain/post/service/PostServiceImpl.java (1)

158-163: 카테고리 enum 파싱은 적절하지만 null 처리까지 고려해 볼 수 있습니다.

PostCategory.valueOf(category)try-catch 로 감싸 INVALID_CATEGORY 로 매핑하는 흐름은 좋습니다. 다만 categorynull 인 경우에는 IllegalArgumentException 이 아니라 NullPointerException 이 발생해 현재 블록에서 처리되지 않습니다. 컨트롤러 레벨에서 @NotNull/@NotBlank 로 이미 방어하고 있다면 그대로 가도 되지만, 서비스 레벨에서 한 번 더 방어(Objects.requireNonNull 또는 null/blank 체크)해 두면 예외 메시지가 더 명확해질 수 있습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 67b5775 and 59f12f0.

📒 Files selected for processing (1)
  • src/main/java/com/sku/refit/domain/post/service/PostServiceImpl.java (1 hunks)

@uni-j-uni uni-j-uni merged commit 3582598 into main Dec 15, 2025
2 of 3 checks passed
@uni-j-uni uni-j-uni deleted the feature/chat branch December 15, 2025 06:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feature 새로운 기능 요청 🚧 in progress 작업 진행 중

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨Feat: 교환 채팅 API 작업

2 participants