Conversation
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. Walkthrough동호회 가입 신청 및 알림 기능을 추가합니다: join-request 흐름(요청/취소/승인/거부/조회), 알림 생성/조회/읽음 처리, WebSocket 실시간 전송, 관련 저장소/DTO/예외 코드, Swagger 태그 수정이 포함됩니다. Changes
Sequence Diagram(s)sequenceDiagram
actor User as 사용자
participant Controller as JoinClubRequestController
participant Service as JoinClubRequestServiceImpl
participant JoinRepo as JoinClubRequestRepository
participant ClubRepo as ClubRepository
participant MemberRepo as ClubMemberRepository
participant NotifService as NotificationServiceImpl
participant NotifRepo as NotificationRepository
participant WS as WebSocket(SimpMessagingTemplate)
participant DB as 데이터베이스
User->>Controller: POST /api/clubs/{clubId}/join
Controller->>Service: requestJoin(clubId, userId)
Service->>ClubRepo: findById(clubId)
Service->>JoinRepo: existsByUserAndClub(user, club)
Service->>MemberRepo: existsByClubAndUser(club, user)
Service->>JoinRepo: save(JoinClubRequest)
JoinRepo->>DB: INSERT JoinClubRequest
Service->>NotifService: createJoinRequestNotification(joinRequest, leader)
NotifService->>NotifRepo: save(Notification)
NotifRepo->>DB: INSERT Notification
NotifService->>WS: convertAndSend(/sub/notifications/{leaderId}, payload)
WS-->>User: 실시간 알림 전달
Service-->>Controller: JoinClubRequestResponse
Controller-->>User: 201 Created
sequenceDiagram
actor Leader as 리더
participant Controller as JoinClubRequestController
participant Service as JoinClubRequestServiceImpl
participant JoinRepo as JoinClubRequestRepository
participant MemberRepo as ClubMemberRepository
participant NotifService as NotificationServiceImpl
participant NotifRepo as NotificationRepository
participant WS as WebSocket(SimpMessagingTemplate)
participant DB as 데이터베이스
Leader->>Controller: POST /api/clubs/join-requests/{id}/approve
Controller->>Service: approveRequest(requestId, leaderId)
Service->>JoinRepo: findById(requestId)
Service->>MemberRepo: save(ClubMember)
MemberRepo->>DB: INSERT ClubMember
Service->>JoinRepo: save(joinRequest status = APPROVED)
JoinRepo->>DB: UPDATE JoinClubRequest
Service->>NotifService: createJoinApprovedNotification(joinRequest)
NotifService->>NotifRepo: save(Notification)
NotifRepo->>DB: INSERT Notification
NotifService->>WS: convertAndSend(/sub/notifications/{userId}, payload)
WS-->>User: 승인 알림 전달
Service-->>Controller: JoinClubRequestResponse
Controller-->>Leader: 200 OK
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45분 Possibly related issues
Possibly related PRs
개요이 PR은 동호회 가입 신청 및 알림 기능을 구현합니다. 새로운 컨트롤러(JoinClubRequestController, NotificationController), 서비스(JoinClubRequestServiceImpl, NotificationServiceImpl), 저장소, DTO, 예외 코드를 추가하여 사용자가 동호회에 가입을 신청하고 관리자가 승인/거부할 수 있도록 하며, WebSocket을 통한 실시간 알림 전달을 지원합니다. 축시
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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: 6
🤖 Fix all issues with AI agents
In
`@src/main/java/com/be/sportizebe/domain/notification/controller/JoinClubRequestController.java`:
- Line 24: 필드와 생성자에서 구체 구현인 JoinClubRequestServiceImpl 대신 인터페이스
JoinClubRequestService를 주입하도록 변경하세요: JoinClubRequestController 클래스의 private
final joinClubRequestService 필드 타입을 JoinClubRequestService로 바꾸고 생성자 매개변수 및 할당도
JoinClubRequestService로 수정한 뒤 필요한 import를 조정해 의존성 역전 원칙(DIP)을 준수하도록 합니다.
In
`@src/main/java/com/be/sportizebe/domain/notification/controller/NotificationController.java`:
- Around line 57-64: The markAsRead endpoint in NotificationController currently
ignores userAuthInfo and calls notificationService.markAsRead(notificationId)
allowing users to mark others' notifications; update
NotificationController.markAsRead to pass the authenticated user (e.g.,
userAuthInfo or userAuthInfo.getId()) into the service call, change the
NotificationService.markAsRead signature to accept the user identifier, and
implement an ownership check inside NotificationService.markAsRead (compare the
notification's recipient/user id to the provided user id and throw an
appropriate exception like AccessDeniedException or a domain-specific exception
if they don't match) to enforce authorization.
- Around line 27-28: Replace the concrete NotificationServiceImpl field in
NotificationController with the NotificationService interface and remove direct
use of UserRepository from the controller; instead update the service API (e.g.,
change methods like sendNotification(User user, ...) to sendNotification(Long
userId, ...)) and move the user lookup logic into NotificationServiceImpl so the
controller only passes the userId to NotificationService and delegates
repository access to the service layer.
In
`@src/main/java/com/be/sportizebe/domain/notification/service/JoinClubRequestServiceImpl.java`:
- Around line 84-86: The comparison in JoinClubRequestServiceImpl using
`request.getUser().getId() != userId` compares Long references and can fail for
values outside the cached range; change this to a value-safe comparison such as
using Objects.equals(request.getUser().getId(), userId) (or
`!request.getUser().getId().equals(userId)` with a prior null check) and throw
ClubErrorCode.CLUB_UPDATE_DENIED when they are not equal; update the conditional
that currently uses `request.getUser().getId() != userId` accordingly.
- Line 33: Update JoinClubRequestServiceImpl to depend on the
NotificationService interface instead of the concrete NotificationServiceImpl:
replace the private final NotificationServiceImpl notificationService field with
a private final NotificationService notificationService, update the constructor
parameter to accept NotificationService, and adjust any imports/usages inside
JoinClubRequestServiceImpl accordingly so the class uses the interface type
(NotificationService) rather than the concrete implementation.
In
`@src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java`:
- Around line 106-111: The markAsRead method in NotificationServiceImpl lacks
ownership checks allowing any user to mark any notification; update the API so
ownership is enforced: change the NotificationService.markAsRead signature to
accept the current user identifier (e.g., userId or Principal) or retrieve the
current user inside markAsRead, then in NotificationServiceImpl.markAsRead load
the notification via notificationRepository.findById(notificationId), verify
notification.getRecipientId() (or notification.getUser()) matches the
authenticated user, and only then call Notification.markAsRead(); if the
notification is missing or owned by another user throw an appropriate
access/NotFound exception. Also update NotificationController to pass the
authenticated user (from SecurityContext/Principal) into the service call and
adjust any callers to the new signature.
🧹 Nitpick comments (5)
src/main/java/com/be/sportizebe/domain/notification/repository/NotificationRepository.java (1)
11-18: 알림 조회 시 페이지네이션 고려현재
findByReceiverOrderByCreatedAtDesc와findByReceiverAndIsReadFalseOrderByCreatedAtDesc메서드는 사용자의 모든 알림을 반환합니다. 알림이 누적될 경우 성능 저하 및 메모리 문제가 발생할 수 있습니다.
Pageable파라미터를 추가하여 페이지네이션을 지원하는 것을 권장합니다.♻️ 페이지네이션 적용 예시
+import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; - List<Notification> findByReceiverOrderByCreatedAtDesc(User receiver); + Page<Notification> findByReceiverOrderByCreatedAtDesc(User receiver, Pageable pageable); - List<Notification> findByReceiverAndIsReadFalseOrderByCreatedAtDesc(User receiver); + Page<Notification> findByReceiverAndIsReadFalseOrderByCreatedAtDesc(User receiver, Pageable pageable);src/main/java/com/be/sportizebe/domain/notification/dto/response/JoinClubRequestResponse.java (1)
21-32:from()메서드에서 N+1 쿼리 문제 개선 권장현재 모든 서비스 메서드가
@Transactional로 선언되어 있어LazyInitializationException이 발생하지 않으나,request.getUser()와request.getClub()에 접근할 때 각각 추가 쿼리가 실행됩니다. 특히getPendingRequests()에서 여러 건의 요청을 처리할 때 N+1 쿼리 문제가 발생합니다. 성능 최적화를 위해findByClubAndStatus()와findByUser()메서드에@EntityGraph또는@Query를 사용하여 연관 엔티티를 함께 로딩하기를 권장합니다.src/main/java/com/be/sportizebe/domain/notification/dto/response/NotificationResponse.java (1)
11-19: @Schema 어노테이션에 example 값 추가 권장다른 DTO들(예:
LikeResponse,LoginResponse)과 일관성을 위해@Schema어노테이션에example값을 추가하면 Swagger 문서가 더 명확해집니다.♻️ 제안된 수정
public record NotificationResponse( - `@Schema`(description = "알림 ID") Long id, - `@Schema`(description = "알림 타입") Notification.NotificationType type, - `@Schema`(description = "알림 메시지") String message, - `@Schema`(description = "읽음 여부") Boolean isRead, - `@Schema`(description = "관련 가입 신청 ID") Long joinRequestId, - `@Schema`(description = "관련 대상 ID (댓글, 채팅 등)") Long targetId, - `@Schema`(description = "알림 생성 일시") LocalDateTime createdAt + `@Schema`(description = "알림 ID", example = "1") Long id, + `@Schema`(description = "알림 타입", example = "JOIN_REQUEST") Notification.NotificationType type, + `@Schema`(description = "알림 메시지", example = "홍길동님이 축구 동호회에 가입을 신청했습니다.") String message, + `@Schema`(description = "읽음 여부", example = "false") Boolean isRead, + `@Schema`(description = "관련 가입 신청 ID", example = "1") Long joinRequestId, + `@Schema`(description = "관련 대상 ID (댓글, 채팅 등)", example = "1") Long targetId, + `@Schema`(description = "알림 생성 일시", example = "2026-02-04T14:30:00") LocalDateTime createdAt )src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java (1)
39-44: WebSocket 전송이 트랜잭션 내부에서 실행됨
sendNotificationToUser가 DB 저장과 동일한 트랜잭션 내에서 호출됩니다. 트랜잭션 롤백 시 WebSocket 메시지는 이미 전송되어 불일치가 발생할 수 있습니다.트랜잭션 커밋 후 WebSocket 전송을 수행하려면
@TransactionalEventListener(phase = AFTER_COMMIT)또는TransactionSynchronizationManager를 사용하는 것을 고려해 보세요.src/main/java/com/be/sportizebe/domain/notification/service/JoinClubRequestServiceImpl.java (1)
56-59: 정원 확인 시 N+1 쿼리 또는 전체 컬렉션 로딩 가능성
club.getMembers().size()는 JPA에서 전체 멤버 컬렉션을 로딩하거나 N+1 쿼리를 발생시킬 수 있습니다.ClubMemberRepository에countByClub(Club club)메서드를 추가하여 COUNT 쿼리를 사용하는 것이 성능상 효율적입니다.♻️ 제안된 수정
ClubMemberRepository에 추가:long countByClub(Club club);서비스에서 사용:
- if (club.getMembers().size() >= club.getMaxMembers()) { + if (clubMemberRepository.countByClub(club) >= club.getMaxMembers()) {Line 114의
approveRequest메서드에서도 동일한 패턴이 사용되므로 함께 수정하세요.
| private final NotificationServiceImpl notificationService; | ||
| private final UserRepository userRepository; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
인터페이스 주입 권장 및 계층 분리
NotificationServiceImpl대신NotificationService인터페이스를 주입하여 의존성 역전 원칙(DIP)을 준수하세요.UserRepository를 컨트롤러에서 직접 사용하는 것은 계층 분리 원칙에 어긋납니다. 사용자 조회 로직은 서비스 레이어에서 처리하는 것이 좋습니다.
♻️ 제안된 수정
- private final NotificationServiceImpl notificationService;
- private final UserRepository userRepository;
+ private final NotificationService notificationService;서비스 메서드 시그니처를 User 대신 Long userId를 받도록 변경하고, 서비스 내부에서 사용자 조회를 처리하세요.
🤖 Prompt for AI Agents
In
`@src/main/java/com/be/sportizebe/domain/notification/controller/NotificationController.java`
around lines 27 - 28, Replace the concrete NotificationServiceImpl field in
NotificationController with the NotificationService interface and remove direct
use of UserRepository from the controller; instead update the service API (e.g.,
change methods like sendNotification(User user, ...) to sendNotification(Long
userId, ...)) and move the user lookup logic into NotificationServiceImpl so the
controller only passes the userId to NotificationService and delegates
repository access to the service layer.
#️⃣ Issue Number
📝 요약(Summary)
생성된 파일
생성된 파일 (9개)
src/main/java/com/be/sportizebe/domain/notification/
├── controller/
│ ├── JoinClubRequestController.java # 가입 신청 API
│ └── NotificationController.java # 알림 API
├── dto/response/
│ ├── JoinClubRequestResponse.java # 가입 신청 응답 DTO
│ └── NotificationResponse.java # 알림 응답 DTO
├── exception/
│ └── JoinClubRequestErrorCode.java # 에러 코드
├── repository/
│ ├── JoinClubRequestRepository.java # 가입 신청 Repository
│ └── NotificationRepository.java # 알림 Repository
└── service/
├── JoinClubRequestService.java # 인터페이스
├── JoinClubRequestServiceImpl.java # 구현체
└── NotificationService.java # 알림 + 웹소켓 서비스
수정된 파일 (1개)
ClubMemberRepository.java - existsByClubAndUser() 메서드 추가
플로우
사용자 A가 동호회 B에 가입 신청
POST /api/clubs/{clubId}/join
JoinClubRequest 생성 (status=PENDING)
Notification 생성 (receiver=동호회장, type=JOIN_REQUEST)
웹소켓으로 동호회장에게 실시간 전송
/sub/notifications/{leaderId}
동호회장이 승인/거절
POST /api/clubs/join-requests/{id}/approve
POST /api/clubs/join-requests/{id}/reject
승인 시: ClubMember 생성, 신청자에게 승인 알림
거절 시: 신청자에게 거절 알림
🛠️ PR 유형
어떤 변경 사항이 있나요?
📸스크린샷 (선택)
💬 공유사항 to 리뷰어
✅ PR Checklist
PR이 다음 요구 사항을 충족하는지 확인하세요.
Summary by CodeRabbit
새로운 기능
문서