-
Notifications
You must be signed in to change notification settings - Fork 0
✨Feat: 웹소켓 기반 채팅 기능 구현 #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Caution Review failedThe pull request is closed. WalkthroughWebSocket/STOMP 기반 실시간 채팅(엔드포인트, 서비스, 엔티티, 리포지토리, DTO, 매퍼)과 JWT 기반 WebSocket 인증(핸드셰이크 및 STOMP 채널 인터셉터)을 추가하고, 교환 게시글의 exchangeCategory 필터 및 관련 DTO/리포지토리/서비스 시그니처를 도입했습니다. build.gradle에 WebSocket 의존성도 추가되었습니다. Changes
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: 구독자에게 메시지 브로드캐스트
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 + 메시지 목록
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45분
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (4)
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.
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)를 사용하고 있으므로,PostCategoryenum 정의와 여기allowableValues값이 항상 1:1 로 일치하도록 관리해 주셔야 합니다. 추후 enum 값이 추가/변경될 경우 이 부분도 함께 업데이트해야 문서와 실제 동작이 어긋나지 않습니다.그 외 시그니처나 동작 변화는 없고, 변경 사항은 문서 품질 향상 수준이라 그대로 유지해도 무방해 보입니다.
src/main/java/com/sku/refit/domain/chat/dto/request/ChatMessageRequest.java (1)
13-17: 입력 유효성 검사 어노테이션 추가를 권장합니다.WebSocket 메시지에서도 유효성 검사가 중요합니다.
roomId와content에 검증 어노테이션을 추가하면 잘못된 요청을 조기에 차단할 수 있습니다.+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
📒 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:Schemaimport 추가는 현재 사용과 잘 맞습니다
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 문서화도 적절합니다.
src/main/java/com/sku/refit/domain/chat/controller/ChatControllerImpl.java
Show resolved
Hide resolved
src/main/java/com/sku/refit/domain/chat/controller/ChatMessageController.java
Show resolved
Hide resolved
src/main/java/com/sku/refit/domain/exchange/controller/ExchangeController.java
Show resolved
Hide resolved
| 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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -C2Repository: 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 -C2Repository: 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.
| 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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
카테고리 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.
| 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); // ★ 핵심 | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "JwtProvider.java" | head -20Repository: refit-lab/refit-be
Length of output: 119
🏁 Script executed:
rg -l "class JwtProvider" --type=javaRepository: refit-lab/refit-be
Length of output: 117
🏁 Script executed:
rg -n "getUsernameFromToken|validateToken" --type=java -B 2 -A 10 | head -100Repository: refit-lab/refit-be
Length of output: 10078
🏁 Script executed:
cat -n src/main/java/com/sku/refit/global/jwt/JwtProvider.java | head -250Repository: refit-lab/refit-be
Length of output: 8680
🏁 Script executed:
rg -n "validateToken" src/main/java/com/sku/refit/global/jwt/JwtProvider.java -A 8Repository: refit-lab/refit-be
Length of output: 656
🏁 Script executed:
cat -n src/main/java/com/sku/refit/global/interceptor/StompJwtChannelInterceptor.javaRepository: 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.
There was a problem hiding this 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)—PostCategoryenum 기반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로 매핑하는 흐름은 좋습니다. 다만category가null인 경우에는IllegalArgumentException이 아니라NullPointerException이 발생해 현재 블록에서 처리되지 않습니다. 컨트롤러 레벨에서@NotNull/@NotBlank로 이미 방어하고 있다면 그대로 가도 되지만, 서비스 레벨에서 한 번 더 방어(Objects.requireNonNull또는 null/blank 체크)해 두면 예외 메시지가 더 명확해질 수 있습니다.
✨ 새로운 기능
🛠 개발 상세(구현 방식 요약)
🧪 테스트 방법
🧩 추가 고려사항
🔗 관련 문서 / 이슈
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.