-
Notifications
You must be signed in to change notification settings - Fork 0
✨Feat: 동호회 가입 신청, 실시간 알람 기능 구현 #50
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
5 changes: 5 additions & 0 deletions
5
src/main/java/com/be/sportizebe/domain/club/repository/ClubMemberRepository.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,12 @@ | ||
| package com.be.sportizebe.domain.club.repository; | ||
|
|
||
| import com.be.sportizebe.domain.club.entity.Club; | ||
| import com.be.sportizebe.domain.club.entity.ClubMember; | ||
| import com.be.sportizebe.domain.user.entity.User; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface ClubMemberRepository extends JpaRepository<ClubMember, Long> { | ||
|
|
||
| // 특정 사용자가 특정 동호회에 이미 가입했는지 확인 | ||
| boolean existsByClubAndUser(Club club, User user); | ||
| } |
79 changes: 79 additions & 0 deletions
79
...main/java/com/be/sportizebe/domain/notification/controller/JoinClubRequestController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| package com.be.sportizebe.domain.notification.controller; | ||
|
|
||
| import com.be.sportizebe.domain.notification.dto.response.JoinClubRequestResponse; | ||
| import com.be.sportizebe.domain.notification.service.JoinClubRequestService; | ||
| import com.be.sportizebe.global.cache.dto.UserAuthInfo; | ||
| import com.be.sportizebe.global.response.BaseResponse; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.Parameter; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/api/clubs") | ||
| @Tag(name = "join-request", description = "동호회 가입 관련 API") | ||
| public class JoinClubRequestController { | ||
|
|
||
| private final JoinClubRequestService joinClubRequestService; | ||
|
|
||
| @PostMapping("/{clubId}/join") | ||
| @Operation(summary = "가입 신청", description = "동호회에 가입을 신청합니다. 동호회장에게 알림이 전송됩니다.") | ||
| public ResponseEntity<BaseResponse<JoinClubRequestResponse>> requestJoin( | ||
| @Parameter(description = "동호회 ID") @PathVariable Long clubId, | ||
| @AuthenticationPrincipal UserAuthInfo userAuthInfo) { | ||
| JoinClubRequestResponse response = joinClubRequestService.requestJoin(clubId, userAuthInfo.getId()); | ||
| return ResponseEntity.status(HttpStatus.CREATED) | ||
| .body(BaseResponse.success("가입 신청 완료", response)); | ||
| } | ||
|
|
||
| @DeleteMapping("/join-requests/{requestId}") | ||
| @Operation(summary = "가입 신청 취소", description = "본인의 가입 신청을 취소합니다.") | ||
| public ResponseEntity<BaseResponse<Void>> cancelRequest( | ||
| @Parameter(description = "가입 신청 ID") @PathVariable Long requestId, | ||
| @AuthenticationPrincipal UserAuthInfo userAuthInfo) { | ||
| joinClubRequestService.cancelRequest(requestId, userAuthInfo.getId()); | ||
| return ResponseEntity.ok(BaseResponse.success("가입 신청 취소 완료", null)); | ||
| } | ||
|
|
||
| @PostMapping("/join-requests/{requestId}/approve") | ||
| @Operation(summary = "가입 승인", description = "가입 신청을 승인합니다. 동호회장만 가능합니다.") | ||
| public ResponseEntity<BaseResponse<JoinClubRequestResponse>> approveRequest( | ||
| @Parameter(description = "가입 신청 ID") @PathVariable Long requestId, | ||
| @AuthenticationPrincipal UserAuthInfo userAuthInfo) { | ||
| JoinClubRequestResponse response = joinClubRequestService.approveRequest(requestId, userAuthInfo.getId()); | ||
| return ResponseEntity.ok(BaseResponse.success("가입 승인 완료", response)); | ||
| } | ||
|
|
||
| @PostMapping("/join-requests/{requestId}/reject") | ||
| @Operation(summary = "가입 거절", description = "가입 신청을 거절합니다. 동호회장만 가능합니다.") | ||
| public ResponseEntity<BaseResponse<JoinClubRequestResponse>> rejectRequest( | ||
| @Parameter(description = "가입 신청 ID") @PathVariable Long requestId, | ||
| @AuthenticationPrincipal UserAuthInfo userAuthInfo) { | ||
| JoinClubRequestResponse response = joinClubRequestService.rejectRequest(requestId, userAuthInfo.getId()); | ||
| return ResponseEntity.ok(BaseResponse.success("가입 거절 완료", response)); | ||
| } | ||
|
|
||
| @GetMapping("/{clubId}/join-requests") | ||
| @Operation(summary = "대기 중인 가입 신청 목록", description = "동호회의 대기 중인 가입 신청 목록을 조회합니다. 동호회장만 가능합니다.") | ||
| public ResponseEntity<BaseResponse<List<JoinClubRequestResponse>>> getPendingRequests( | ||
| @Parameter(description = "동호회 ID") @PathVariable Long clubId, | ||
| @AuthenticationPrincipal UserAuthInfo userAuthInfo) { | ||
| List<JoinClubRequestResponse> response = joinClubRequestService.getPendingRequests(clubId, userAuthInfo.getId()); | ||
| return ResponseEntity.ok(BaseResponse.success("가입 신청 목록 조회 성공", response)); | ||
| } | ||
|
|
||
| @GetMapping("/my-join-requests") | ||
| @Operation(summary = "내 가입 신청 목록", description = "내가 신청한 가입 신청 목록을 조회합니다.") | ||
| public ResponseEntity<BaseResponse<List<JoinClubRequestResponse>>> getMyRequests( | ||
| @AuthenticationPrincipal UserAuthInfo userAuthInfo) { | ||
| List<JoinClubRequestResponse> response = joinClubRequestService.getMyRequests(userAuthInfo.getId()); | ||
| return ResponseEntity.ok(BaseResponse.success("내 가입 신청 목록 조회 성공", response)); | ||
| } | ||
| } |
70 changes: 70 additions & 0 deletions
70
src/main/java/com/be/sportizebe/domain/notification/controller/NotificationController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| package com.be.sportizebe.domain.notification.controller; | ||
|
|
||
| import com.be.sportizebe.domain.notification.dto.response.NotificationResponse; | ||
| import com.be.sportizebe.domain.notification.service.NotificationServiceImpl; | ||
| import com.be.sportizebe.domain.user.entity.User; | ||
| import com.be.sportizebe.domain.user.exception.UserErrorCode; | ||
| import com.be.sportizebe.domain.user.repository.UserRepository; | ||
| import com.be.sportizebe.global.cache.dto.UserAuthInfo; | ||
| import com.be.sportizebe.global.exception.CustomException; | ||
| import com.be.sportizebe.global.response.BaseResponse; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.Parameter; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/api/notifications") | ||
| @Tag(name = "notification", description = "알림 API") | ||
| public class NotificationController { | ||
|
|
||
| private final NotificationServiceImpl notificationService; | ||
| private final UserRepository userRepository; | ||
|
|
||
| @GetMapping | ||
| @Operation(summary = "알림 목록 조회", description = "나의 모든 알림을 조회합니다.") | ||
| public ResponseEntity<BaseResponse<List<NotificationResponse>>> getNotifications( | ||
| @AuthenticationPrincipal UserAuthInfo userAuthInfo) { | ||
| User user = findUserById(userAuthInfo.getId()); | ||
| List<NotificationResponse> response = notificationService.getNotifications(user); | ||
| return ResponseEntity.ok(BaseResponse.success("알림 목록 조회 성공", response)); | ||
| } | ||
|
|
||
| @GetMapping("/unread") | ||
| @Operation(summary = "읽지 않은 알림 조회", description = "읽지 않은 알림 목록을 조회합니다.") | ||
| public ResponseEntity<BaseResponse<List<NotificationResponse>>> getUnreadNotifications( | ||
| @AuthenticationPrincipal UserAuthInfo userAuthInfo) { | ||
| User user = findUserById(userAuthInfo.getId()); | ||
| List<NotificationResponse> response = notificationService.getUnreadNotifications(user); | ||
| return ResponseEntity.ok(BaseResponse.success("읽지 않은 알림 조회 성공", response)); | ||
| } | ||
|
|
||
| @GetMapping("/unread/count") | ||
| @Operation(summary = "읽지 않은 알림 개수", description = "읽지 않은 알림 개수를 조회합니다.") | ||
| public ResponseEntity<BaseResponse<Long>> getUnreadCount( | ||
| @AuthenticationPrincipal UserAuthInfo userAuthInfo) { | ||
| User user = findUserById(userAuthInfo.getId()); | ||
| long count = notificationService.getUnreadCount(user); | ||
| return ResponseEntity.ok(BaseResponse.success("읽지 않은 알림 개수 조회 성공", count)); | ||
| } | ||
|
|
||
| @PatchMapping("/{notificationId}/read") | ||
| @Operation(summary = "알림 읽음 처리", description = "알림을 읽음 처리합니다.") | ||
| public ResponseEntity<BaseResponse<Void>> markAsRead( | ||
| @Parameter(description = "알림 ID") @PathVariable Long notificationId, | ||
| @AuthenticationPrincipal UserAuthInfo userAuthInfo) { | ||
| notificationService.markAsRead(notificationId, userAuthInfo.getId()); | ||
| return ResponseEntity.ok(BaseResponse.success("알림 읽음 처리 완료", null)); | ||
| } | ||
|
imjuyongp marked this conversation as resolved.
|
||
|
|
||
| private User findUserById(Long userId) { | ||
| return userRepository.findById(userId) | ||
| .orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND)); | ||
| } | ||
| } | ||
33 changes: 33 additions & 0 deletions
33
...main/java/com/be/sportizebe/domain/notification/dto/response/JoinClubRequestResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.be.sportizebe.domain.notification.dto.response; | ||
|
|
||
| import com.be.sportizebe.domain.notification.entity.JoinClubRequest; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import lombok.Builder; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Builder | ||
| @Schema(title = "JoinClubRequestResponse DTO", description = "가입 신청 응답") | ||
| public record JoinClubRequestResponse( | ||
| @Schema(description = "가입 신청 ID") Long id, | ||
| @Schema(description = "신청자 ID") Long userId, | ||
| @Schema(description = "신청자 닉네임") String userNickname, | ||
| @Schema(description = "신청자 프로필 이미지") String userProfileImage, | ||
| @Schema(description = "동호회 ID") Long clubId, | ||
| @Schema(description = "동호회 이름") String clubName, | ||
| @Schema(description = "신청 상태") JoinClubRequest.JoinClubRequestStatus status, | ||
| @Schema(description = "신청 일시") LocalDateTime createdAt | ||
| ) { | ||
| public static JoinClubRequestResponse from(JoinClubRequest request) { | ||
| return JoinClubRequestResponse.builder() | ||
| .id(request.getId()) | ||
| .userId(request.getUser().getId()) | ||
| .userNickname(request.getUser().getNickname()) | ||
| .userProfileImage(request.getUser().getProfileImage()) | ||
| .clubId(request.getClub().getId()) | ||
| .clubName(request.getClub().getName()) | ||
| .status(request.getStatus()) | ||
| .createdAt(request.getCreatedAt()) | ||
| .build(); | ||
| } | ||
| } |
32 changes: 32 additions & 0 deletions
32
src/main/java/com/be/sportizebe/domain/notification/dto/response/NotificationResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package com.be.sportizebe.domain.notification.dto.response; | ||
|
|
||
| import com.be.sportizebe.domain.notification.entity.Notification; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import lombok.Builder; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Builder | ||
| @Schema(title = "NotificationResponse DTO", description = "알림 응답") | ||
| 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 | ||
| ) { | ||
| public static NotificationResponse from(Notification notification) { | ||
| return NotificationResponse.builder() | ||
| .id(notification.getId()) | ||
| .type(notification.getType()) | ||
| .message(notification.getMessage()) | ||
| .isRead(notification.getIsRead()) | ||
| .joinRequestId(notification.getJoinClubRequest() != null | ||
| ? notification.getJoinClubRequest().getId() : null) | ||
| .targetId(notification.getTargetId()) | ||
| .createdAt(notification.getCreatedAt()) | ||
| .build(); | ||
| } | ||
| } |
21 changes: 21 additions & 0 deletions
21
src/main/java/com/be/sportizebe/domain/notification/exception/JoinClubRequestErrorCode.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package com.be.sportizebe.domain.notification.exception; | ||
|
|
||
| import com.be.sportizebe.global.exception.model.BaseErrorCode; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import org.springframework.http.HttpStatus; | ||
|
|
||
| @Getter | ||
| @AllArgsConstructor | ||
| public enum JoinClubRequestErrorCode implements BaseErrorCode { | ||
| JOIN_REQUEST_NOT_FOUND("JOIN_001", "가입 신청을 찾을 수 없습니다.", HttpStatus.NOT_FOUND), | ||
| JOIN_REQUEST_ALREADY_EXISTS("JOIN_002", "이미 가입 신청한 동호회입니다.", HttpStatus.CONFLICT), | ||
| ALREADY_CLUB_MEMBER("JOIN_003", "이미 가입된 동호회입니다.", HttpStatus.CONFLICT), | ||
| JOIN_REQUEST_NOT_PENDING("JOIN_004", "대기 중인 가입 신청이 아닙니다.", HttpStatus.BAD_REQUEST), | ||
| CANNOT_JOIN_OWN_CLUB("JOIN_005", "자신이 만든 동호회에는 가입 신청할 수 없습니다.", HttpStatus.BAD_REQUEST), | ||
| CLUB_FULL("JOIN_006", "동호회 정원이 가득 찼습니다.", HttpStatus.BAD_REQUEST); | ||
|
|
||
| private final String code; | ||
| private final String message; | ||
| private final HttpStatus status; | ||
| } |
24 changes: 24 additions & 0 deletions
24
...main/java/com/be/sportizebe/domain/notification/repository/JoinClubRequestRepository.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.be.sportizebe.domain.notification.repository; | ||
|
|
||
| import com.be.sportizebe.domain.club.entity.Club; | ||
| import com.be.sportizebe.domain.notification.entity.JoinClubRequest; | ||
| import com.be.sportizebe.domain.user.entity.User; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| public interface JoinClubRequestRepository extends JpaRepository<JoinClubRequest, Long> { | ||
|
|
||
| // 특정 사용자의 특정 동호회 가입 신청 조회 | ||
| Optional<JoinClubRequest> findByUserAndClub(User user, Club club); | ||
|
|
||
| // 특정 사용자가 특정 동호회에 이미 신청했는지 확인 | ||
| boolean existsByUserAndClub(User user, Club club); | ||
|
|
||
| // 특정 동호회의 대기 중인 가입 신청 목록 | ||
| List<JoinClubRequest> findByClubAndStatus(Club club, JoinClubRequest.JoinClubRequestStatus status); | ||
|
|
||
| // 특정 사용자의 가입 신청 목록 | ||
| List<JoinClubRequest> findByUser(User user); | ||
| } |
19 changes: 19 additions & 0 deletions
19
src/main/java/com/be/sportizebe/domain/notification/repository/NotificationRepository.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package com.be.sportizebe.domain.notification.repository; | ||
|
|
||
| import com.be.sportizebe.domain.notification.entity.Notification; | ||
| import com.be.sportizebe.domain.user.entity.User; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public interface NotificationRepository extends JpaRepository<Notification, Long> { | ||
|
|
||
| // 특정 사용자의 알림 목록 (최신순) | ||
| List<Notification> findByReceiverOrderByCreatedAtDesc(User receiver); | ||
|
|
||
| // 특정 사용자의 읽지 않은 알림 목록 | ||
| List<Notification> findByReceiverAndIsReadFalseOrderByCreatedAtDesc(User receiver); | ||
|
|
||
| // 특정 사용자의 읽지 않은 알림 개수 | ||
| long countByReceiverAndIsReadFalse(User receiver); | ||
| } |
26 changes: 26 additions & 0 deletions
26
src/main/java/com/be/sportizebe/domain/notification/service/JoinClubRequestService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.be.sportizebe.domain.notification.service; | ||
|
|
||
| import com.be.sportizebe.domain.notification.dto.response.JoinClubRequestResponse; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public interface JoinClubRequestService { | ||
|
|
||
| // 가입 신청 | ||
| JoinClubRequestResponse requestJoin(Long clubId, Long userId); | ||
|
|
||
| // 가입 신청 취소 | ||
| void cancelRequest(Long requestId, Long userId); | ||
|
|
||
| // 가입 승인 (동호회장만) | ||
| JoinClubRequestResponse approveRequest(Long requestId, Long leaderId); | ||
|
|
||
| // 가입 거절 (동호회장만) | ||
| JoinClubRequestResponse rejectRequest(Long requestId, Long leaderId); | ||
|
|
||
| // 동호회의 대기 중인 가입 신청 목록 조회 (동호회장만) | ||
| List<JoinClubRequestResponse> getPendingRequests(Long clubId, Long leaderId); | ||
|
|
||
| // 내 가입 신청 목록 조회 | ||
| List<JoinClubRequestResponse> getMyRequests(Long userId); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
🛠️ Refactor suggestion | 🟠 Major
인터페이스 주입 권장 및 계층 분리
NotificationServiceImpl대신NotificationService인터페이스를 주입하여 의존성 역전 원칙(DIP)을 준수하세요.UserRepository를 컨트롤러에서 직접 사용하는 것은 계층 분리 원칙에 어긋납니다. 사용자 조회 로직은 서비스 레이어에서 처리하는 것이 좋습니다.♻️ 제안된 수정
서비스 메서드 시그니처를
User대신Long userId를 받도록 변경하고, 서비스 내부에서 사용자 조회를 처리하세요.🤖 Prompt for AI Agents