Skip to content

✨Feat: 동호회 가입 신청, 실시간 알람 기능 구현#50

Merged
imjuyongp merged 3 commits intodevelopfrom
feat/notification
Feb 4, 2026
Merged

✨Feat: 동호회 가입 신청, 실시간 알람 기능 구현#50
imjuyongp merged 3 commits intodevelopfrom
feat/notification

Conversation

@imjuyongp
Copy link
Copy Markdown
Member

@imjuyongp imjuyongp commented Feb 4, 2026

#️⃣ 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() 메서드 추가

플로우

  1. 사용자 A가 동호회 B에 가입 신청
    POST /api/clubs/{clubId}/join

  2. JoinClubRequest 생성 (status=PENDING)

  3. Notification 생성 (receiver=동호회장, type=JOIN_REQUEST)

  4. 웹소켓으로 동호회장에게 실시간 전송
    /sub/notifications/{leaderId}

  5. 동호회장이 승인/거절
    POST /api/clubs/join-requests/{id}/approve
    POST /api/clubs/join-requests/{id}/reject

  6. 승인 시: ClubMember 생성, 신청자에게 승인 알림
    거절 시: 신청자에게 거절 알림

🛠️ PR 유형

어떤 변경 사항이 있나요?

  • 새로운 기능 추가
  • 버그 수정
  • 코드 리팩토링
  • 주석 추가 및 수정

📸스크린샷 (선택)

💬 공유사항 to 리뷰어

  • erd 수정사항 있습니다.
  • 알림, 가입신청 테이블 리뷰

✅ PR Checklist

PR이 다음 요구 사항을 충족하는지 확인하세요.

  • 커밋 메시지 컨벤션에 맞게 작성했습니다.
  • 변경 사항에 대한 테스트를 했습니다.(버그 수정/기능에 대한 테스트).

Summary by CodeRabbit

  • 새로운 기능

    • 동호회 가입 신청 흐름 추가: 사용자 신청, 신청 취소, 리더의 승인/거절 및 대기 목록 조회 가능
    • 실시간 알림 추가: 가입 신청, 승인, 거절 알림 전송 및 수신자별 알림 조회·읽음 처리·미확인 수량 제공
    • 가입 요청 관련 오류 코드 및 검증 로직 추가
  • 문서

    • 동호회 관리 API 설명 문구 업데이트

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 4, 2026

Warning

Rate limit exceeded

@imjuyongp has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 17 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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

Cohort / File(s) Summary
가입 요청 컨트롤러
src/main/java/com/be/sportizebe/domain/notification/controller/JoinClubRequestController.java
동호회 가입 신청 관련 6개 REST 엔드포인트 추가(요청, 취소, 승인, 거부, 대기 조회, 내 요청 조회).
알림 컨트롤러
src/main/java/com/be/sportizebe/domain/notification/controller/NotificationController.java
알림 조회(전체/미읽음), 미읽음 개수 조회, 읽음 표시 엔드포인트 추가; User 조회 보조 로직 포함.
가입 요청 서비스
src/main/java/com/be/sportizebe/domain/notification/service/JoinClubRequestService.java, src/main/java/com/be/sportizebe/domain/notification/service/JoinClubRequestServiceImpl.java
가입 요청 생성/취소/승인/거부/조회 구현. 권한·상태·정원·중복 검증, ClubMember 추가, 알림 생성 호출 등 비즈니스 로직 포함.
알림 서비스
src/main/java/com/be/sportizebe/domain/notification/service/NotificationService.java, src/main/java/com/be/sportizebe/domain/notification/service/NotificationServiceImpl.java
가입 관련 알림 생성(요청/승인/거부), DB 저장, SimpMessagingTemplate을 통한 WebSocket 전송, 알림 조회/미읽음/읽음 처리 구현.
저장소 (JoinRequest / Notification)
src/main/java/com/be/sportizebe/domain/notification/repository/JoinClubRequestRepository.java, src/main/java/com/be/sportizebe/domain/notification/repository/NotificationRepository.java
JoinClubRequest 및 Notification 조회/검색/카운트용 JPA 메서드 추가(사용자·동호회별 검색, 상태별 조회, 정렬 등).
DTO / 예외 / 에러 코드
src/main/java/com/be/sportizebe/domain/notification/dto/response/JoinClubRequestResponse.java, src/main/java/com/be/sportizebe/domain/notification/dto/response/NotificationResponse.java, src/main/java/com/be/sportizebe/domain/notification/exception/JoinClubRequestErrorCode.java
응답 DTO(JoinClubRequestResponse, NotificationResponse) 추가 및 JoinClubRequest 전용 에러 코드(enum) 추가.
동호회 관련 변경
src/main/java/com/be/sportizebe/domain/club/repository/ClubMemberRepository.java, src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java
existsByClubAndUser(Club, User) 메서드 추가 및 Swagger Tag 설명 텍스트 수정.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45분

Possibly related issues

  • ✨Feat: 실시간 알림기능 #38: 본 PR은 가입 요청 알림 흐름(JoinClubRequest + Notification + WebSocket 전송)을 구현하므로 이 이슈의 목표와 직접적으로 일치합니다.

Possibly related PRs

개요

이 PR은 동호회 가입 신청 및 알림 기능을 구현합니다. 새로운 컨트롤러(JoinClubRequestController, NotificationController), 서비스(JoinClubRequestServiceImpl, NotificationServiceImpl), 저장소, DTO, 예외 코드를 추가하여 사용자가 동호회에 가입을 신청하고 관리자가 승인/거부할 수 있도록 하며, WebSocket을 통한 실시간 알림 전달을 지원합니다.

축시

🐰 새로 온 요청에 깡총 뛰며,
알림은 톡톡, 실시간으로 날아가고,
승인은 문을 열고, 거절은 조용히 남겨두며,
커뮤니티의 하루가 또 반짝이네. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 동호회 가입 신청과 실시간 알람 기능 구현이라는 주요 변경사항을 명확하게 요약하고 있으며, 변경 세트의 핵심 내용과 완벽하게 일치합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/notification

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
Copy Markdown

@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: 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: 알림 조회 시 페이지네이션 고려

현재 findByReceiverOrderByCreatedAtDescfindByReceiverAndIsReadFalseOrderByCreatedAtDesc 메서드는 사용자의 모든 알림을 반환합니다. 알림이 누적될 경우 성능 저하 및 메모리 문제가 발생할 수 있습니다.

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 쿼리를 발생시킬 수 있습니다. ClubMemberRepositorycountByClub(Club club) 메서드를 추가하여 COUNT 쿼리를 사용하는 것이 성능상 효율적입니다.

♻️ 제안된 수정

ClubMemberRepository에 추가:

long countByClub(Club club);

서비스에서 사용:

-  if (club.getMembers().size() >= club.getMaxMembers()) {
+  if (clubMemberRepository.countByClub(club) >= club.getMaxMembers()) {

Line 114의 approveRequest 메서드에서도 동일한 패턴이 사용되므로 함께 수정하세요.

Comment on lines +27 to +28
private final NotificationServiceImpl notificationService;
private final UserRepository userRepository;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

인터페이스 주입 권장 및 계층 분리

  1. NotificationServiceImpl 대신 NotificationService 인터페이스를 주입하여 의존성 역전 원칙(DIP)을 준수하세요.
  2. 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.

@imjuyongp imjuyongp merged commit 30e1665 into develop Feb 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants