diff --git a/src/main/java/site/coach_coach/coach_coach_server/coach/service/CoachService.java b/src/main/java/site/coach_coach/coach_coach_server/coach/service/CoachService.java index ec697286..099c4996 100644 --- a/src/main/java/site/coach_coach/coach_coach_server/coach/service/CoachService.java +++ b/src/main/java/site/coach_coach/coach_coach_server/coach/service/CoachService.java @@ -148,7 +148,9 @@ public void contactCoach(User user, Long coachId) { newMatching.getUserCoachMatchingId()); Long chatRoomId = chatRoomService.createChatRoom(chatRoomRequest); - notificationService.createNotification(user.getUserId(), coachId, RelationFunctionEnum.ask); + notificationService.createNotification(user.getUserId(), coach.getUser().getUserId(), + RelationFunctionEnum.request); + notificationService.createNotification(user.getUserId(), coach.getUser().getUserId(), RelationFunctionEnum.ask); } @Transactional @@ -165,9 +167,9 @@ public void deleteMatching(Long coachUserId, Long userId) { boolean isMatching = matching.getIsMatching(); matchingRepository.delete(matching); if (isMatching) { - notificationService.createNotification(userId, coach.getCoachId(), RelationFunctionEnum.cancel); + notificationService.createNotification(userId, coach.getUser().getUserId(), RelationFunctionEnum.cancel); } else { - notificationService.createNotification(userId, coach.getCoachId(), RelationFunctionEnum.refusal); + notificationService.createNotification(userId, coach.getUser().getUserId(), RelationFunctionEnum.refusal); } } @@ -274,6 +276,7 @@ public void addCoachToFavorites(Long userId, Long coachId) { if (!userCoachLikeRepository.existsByUser_UserIdAndCoach_CoachId(userId, coachId)) { userCoachLikeRepository.save(new UserCoachLike(null, user, coach)); } + notificationService.createNotification(userId, coach.getUser().getUserId(), RelationFunctionEnum.like); } @Transactional @@ -286,7 +289,6 @@ public void deleteCoachToFavorites(Long userId, Long coachId) { } } - public List getMatchingUsersByCoachId(Long coachUserId) { Long coachId = coachRepository.findCoachIdByUserId(coachUserId) .orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COACH)); @@ -321,7 +323,6 @@ private MatchingUserResponseDto buildMatchingUserResponseDto(Matching matching) ); } - @Transactional public void addReview(Long userId, Long coachId, ReviewRequestDto requestDto) { User user = getUserById(userId); @@ -342,7 +343,8 @@ public void addReview(Long userId, Long coachId, ReviewRequestDto requestDto) { Review review = new Review(null, coach, user, requestDto.contents(), requestDto.stars()); reviewRepository.save(review); - notificationService.createNotification(user.getUserId(), coachId, RelationFunctionEnum.review); + notificationService.createNotification(user.getUserId(), coach.getUser().getUserId(), + RelationFunctionEnum.review); } @Transactional @@ -567,6 +569,6 @@ public void updateMatchingStatus(Long coachUserId, Long userId) { matching.markAsMatched(); coach.increaseTotalUserCount(); - notificationService.createNotification(userId, coach.getCoachId(), RelationFunctionEnum.match); + notificationService.createNotification(userId, coachUserId, RelationFunctionEnum.match); } } diff --git a/src/main/java/site/coach_coach/coach_coach_server/common/domain/RelationFunctionEnum.java b/src/main/java/site/coach_coach/coach_coach_server/common/domain/RelationFunctionEnum.java index ac46f34e..91f18301 100644 --- a/src/main/java/site/coach_coach/coach_coach_server/common/domain/RelationFunctionEnum.java +++ b/src/main/java/site/coach_coach/coach_coach_server/common/domain/RelationFunctionEnum.java @@ -1,5 +1,5 @@ package site.coach_coach.coach_coach_server.common.domain; public enum RelationFunctionEnum { - review, ask, like, match, refusal, cancel + review, ask, like, match, refusal, cancel, routine, request } diff --git a/src/main/java/site/coach_coach/coach_coach_server/notification/constants/NotificationMessage.java b/src/main/java/site/coach_coach/coach_coach_server/notification/constants/NotificationMessage.java index 8141e1fb..50a6929b 100644 --- a/src/main/java/site/coach_coach/coach_coach_server/notification/constants/NotificationMessage.java +++ b/src/main/java/site/coach_coach/coach_coach_server/notification/constants/NotificationMessage.java @@ -4,13 +4,15 @@ @Getter public enum NotificationMessage { - REVIEW_MESSAGE("새로운 리뷰가 작성되었습니다."), - ASK_MESSAGE("매칭을 신청하였습니다."), - LIKE_MESSAGE("회원님을 관심 코치로 등록하였습니다."), - MATCH_MESSAGE("회원님의 매칭 신청을 수락하셨습니다."), - REFUSAL_MESSAGE("매칭 신청을 거절하였습니다."), + REVIEW_MESSAGE("새로운 리뷰가 작성되었습니다!"), + ASK_MESSAGE("코치 매칭을 요청했어요!"), + LIKE_MESSAGE("회원님을 관심 코치로 등록했어요!"), + MATCH_MESSAGE("코치 매칭을 수락했어요!"), + MATCH_REQUEST_MESSAGE(" 님께 코치 매칭을 요청했어요! 답변이 오면 빠르게 알려드릴게요 :)"), + REFUSAL_MESSAGE("매칭 요청을 거절하였습니다."), CANCEL_MESSAGE("매칭을 취소하였습니다."), - USER_MESSAGE("님이 "); + ROUTINE_MESSAGE("새로운 루틴을 등록했어요."), + USER_MESSAGE(" 님이 "); private final String message; diff --git a/src/main/java/site/coach_coach/coach_coach_server/notification/controller/NotificationController.java b/src/main/java/site/coach_coach/coach_coach_server/notification/controller/NotificationController.java index b72928ba..68b502da 100644 --- a/src/main/java/site/coach_coach/coach_coach_server/notification/controller/NotificationController.java +++ b/src/main/java/site/coach_coach/coach_coach_server/notification/controller/NotificationController.java @@ -7,6 +7,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -52,4 +53,23 @@ public ResponseEntity deleteNotification( new SuccessResponse(HttpStatus.OK.value(), SuccessMessage.DELETE_NOTIFICATION_SUCCESS.getMessage()) ); } + + @PatchMapping("/v1/notifications/{notificationId}") + public ResponseEntity markNotificationAsRead( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable(name = "notificationId") Long notificationId + ) { + Long userId = userDetails.getUserId(); + notificationService.markNotificationAsRead(userId, notificationId); + return ResponseEntity.noContent().build(); + } + + @PatchMapping("/v1/notifications") + public ResponseEntity markAllNotificationsAsRead( + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + Long userId = userDetails.getUserId(); + notificationService.markAllNotificationsAsRead(userId); + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/site/coach_coach/coach_coach_server/notification/domain/Notification.java b/src/main/java/site/coach_coach/coach_coach_server/notification/domain/Notification.java index bc27758d..994b878d 100644 --- a/src/main/java/site/coach_coach/coach_coach_server/notification/domain/Notification.java +++ b/src/main/java/site/coach_coach/coach_coach_server/notification/domain/Notification.java @@ -39,6 +39,10 @@ public class Notification extends DateEntity { @JoinColumn(name = "user_id") private User user; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "coach_id") + private User coach; + @NotBlank @Column(name = "message", nullable = false, length = 100) @Size(max = 100) @@ -48,4 +52,12 @@ public class Notification extends DateEntity { @Column(name = "relation_function") @Enumerated(EnumType.STRING) private RelationFunctionEnum relationFunction; + + @NotNull + @Column(name = "is_read") + private boolean isRead; + + public void markAsRead() { + this.isRead = true; + } } diff --git a/src/main/java/site/coach_coach/coach_coach_server/notification/dto/NotificationListResponse.java b/src/main/java/site/coach_coach/coach_coach_server/notification/dto/NotificationListResponse.java index 7b640b7a..369f51a1 100644 --- a/src/main/java/site/coach_coach/coach_coach_server/notification/dto/NotificationListResponse.java +++ b/src/main/java/site/coach_coach/coach_coach_server/notification/dto/NotificationListResponse.java @@ -4,21 +4,15 @@ import lombok.Builder; import site.coach_coach.coach_coach_server.common.domain.RelationFunctionEnum; -import site.coach_coach.coach_coach_server.notification.domain.Notification; @Builder public record NotificationListResponse( Long notificationId, + String nickname, + String profileImageUrl, String message, RelationFunctionEnum relationFunction, + boolean isRead, LocalDateTime createdAt ) { - public static NotificationListResponse from(Notification notification) { - return new NotificationListResponse( - notification.getNotificationId(), - notification.getMessage(), - notification.getRelationFunction(), - notification.getCreatedAt() - ); - } } diff --git a/src/main/java/site/coach_coach/coach_coach_server/notification/repository/NotificationRepository.java b/src/main/java/site/coach_coach/coach_coach_server/notification/repository/NotificationRepository.java index d311c118..b305ec64 100644 --- a/src/main/java/site/coach_coach/coach_coach_server/notification/repository/NotificationRepository.java +++ b/src/main/java/site/coach_coach/coach_coach_server/notification/repository/NotificationRepository.java @@ -1,9 +1,16 @@ package site.coach_coach.coach_coach_server.notification.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import site.coach_coach.coach_coach_server.notification.domain.Notification; public interface NotificationRepository extends JpaRepository { - int countByUser_UserId(Long userId); + int countByUser_UserIdAndIsReadFalse(Long userId); + + @Modifying + @Query("UPDATE Notification n SET n.isRead = TRUE WHERE n.user.userId = :userId AND n.isRead = FALSE") + void updateIsReadByUserID(@Param("userId") Long userId); } diff --git a/src/main/java/site/coach_coach/coach_coach_server/notification/service/NotificationService.java b/src/main/java/site/coach_coach/coach_coach_server/notification/service/NotificationService.java index 84261b59..60271fcd 100644 --- a/src/main/java/site/coach_coach/coach_coach_server/notification/service/NotificationService.java +++ b/src/main/java/site/coach_coach/coach_coach_server/notification/service/NotificationService.java @@ -1,6 +1,5 @@ package site.coach_coach.coach_coach_server.notification.service; -import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Service; @@ -37,7 +36,7 @@ public List getAllNotifications(Long userId) { return user.getNotifications() .stream() - .map(NotificationListResponse::from) + .map(this::buildNotificationResponse) .toList(); } @@ -53,23 +52,17 @@ public void createNotification(Long userId, Long coachId, RelationFunctionEnum r throw new InvalidInputException(ErrorMessage.INVALID_REQUEST); } - User receiver; - - if (relationFunction == RelationFunctionEnum.match || relationFunction == RelationFunctionEnum.refusal - || relationFunction == RelationFunctionEnum.cancel) { - receiver = user; - } else { - receiver = coach; - } - Notification notification = Notification.builder() - .user(receiver) + .user(user) + .coach(coach) .message(message) .relationFunction(relationFunction) + .isRead(false) .build(); notificationRepository.save(notification); } + @Transactional public void deleteNotification(Long userId, Long notificationId) { Notification notification = notificationRepository.findById(notificationId) .orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_NOTIFICATION)); @@ -82,25 +75,79 @@ public void deleteNotification(Long userId, Long notificationId) { public void deleteAllNotifications(Long userId) { User user = userRepository.findById(userId).orElseThrow(UserNotFoundException::new); - List notifications = new ArrayList<>(user.getNotifications()); - if (!notifications.isEmpty()) { - notificationRepository.deleteAll(notifications); + notificationRepository.deleteAll(user.getNotifications()); + } + + @Transactional + public void markNotificationAsRead(Long userId, Long notificationId) { + Notification notification = notificationRepository.findById(notificationId) + .orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_NOTIFICATION)); + + if (!notification.getUser().getUserId().equals(userId)) { + throw new AccessDeniedException(); } + notification.markAsRead(); + notificationRepository.save(notification); + } + + @Transactional + public void markAllNotificationsAsRead(Long userId) { + notificationRepository.updateIsReadByUserID(userId); } private String createMessage(User user, User coach, RelationFunctionEnum relationFunction) { return switch (relationFunction) { - case ask -> user.getNickname() + NotificationMessage.USER_MESSAGE.getMessage() - + NotificationMessage.ASK_MESSAGE.getMessage(); - case like -> user.getNickname() + NotificationMessage.USER_MESSAGE.getMessage() - + NotificationMessage.LIKE_MESSAGE.getMessage(); + case ask -> String.format( + "%s%s%s", user.getNickname(), NotificationMessage.USER_MESSAGE.getMessage(), + NotificationMessage.ASK_MESSAGE.getMessage() + ); + case like -> String.format( + "%s%s%s", user.getNickname(), NotificationMessage.USER_MESSAGE.getMessage(), + NotificationMessage.LIKE_MESSAGE.getMessage() + ); case review -> NotificationMessage.REVIEW_MESSAGE.getMessage(); - case match -> coach.getNickname() + NotificationMessage.USER_MESSAGE.getMessage() - + NotificationMessage.MATCH_MESSAGE.getMessage(); - case refusal -> coach.getNickname() + NotificationMessage.USER_MESSAGE.getMessage() - + NotificationMessage.REFUSAL_MESSAGE.getMessage(); - case cancel -> coach.getNickname() + NotificationMessage.USER_MESSAGE.getMessage() - + NotificationMessage.CANCEL_MESSAGE.getMessage(); + case match -> String.format( + "%s%s%s", coach.getNickname(), NotificationMessage.USER_MESSAGE.getMessage(), + NotificationMessage.MATCH_MESSAGE.getMessage() + ); + case refusal -> String.format( + "%s%s%s", coach.getNickname(), NotificationMessage.USER_MESSAGE.getMessage(), + NotificationMessage.REFUSAL_MESSAGE.getMessage() + ); + case cancel -> String.format( + "%s%s%s", coach.getNickname(), NotificationMessage.USER_MESSAGE.getMessage(), + NotificationMessage.CANCEL_MESSAGE.getMessage() + ); + case routine -> String.format( + "%s%s%s", coach.getNickname(), NotificationMessage.USER_MESSAGE.getMessage(), + NotificationMessage.ROUTINE_MESSAGE.getMessage() + ); + case request -> coach.getNickname() + NotificationMessage.MATCH_REQUEST_MESSAGE.getMessage(); + }; + } + + private NotificationListResponse buildNotificationResponse(Notification notification) { + User sender; + if (isCoachSender(notification.getRelationFunction())) { + sender = notification.getCoach(); + } else { + sender = notification.getUser(); + } + return NotificationListResponse.builder() + .notificationId(notification.getNotificationId()) + .nickname(sender.getNickname()) + .profileImageUrl(sender.getProfileImageUrl()) + .message(notification.getMessage()) + .relationFunction(notification.getRelationFunction()) + .isRead(notification.isRead()) + .createdAt(notification.getCreatedAt()) + .build(); + } + + private boolean isCoachSender(RelationFunctionEnum relationFunction) { + return switch (relationFunction) { + case match, refusal, cancel, request, routine -> true; + default -> false; }; } } diff --git a/src/main/java/site/coach_coach/coach_coach_server/routine/service/RoutineService.java b/src/main/java/site/coach_coach/coach_coach_server/routine/service/RoutineService.java index 94f5badb..2dbd3a1d 100644 --- a/src/main/java/site/coach_coach/coach_coach_server/routine/service/RoutineService.java +++ b/src/main/java/site/coach_coach/coach_coach_server/routine/service/RoutineService.java @@ -13,10 +13,12 @@ import site.coach_coach.coach_coach_server.coach.domain.Coach; import site.coach_coach.coach_coach_server.coach.repository.CoachRepository; import site.coach_coach.coach_coach_server.common.constants.ErrorMessage; +import site.coach_coach.coach_coach_server.common.domain.RelationFunctionEnum; import site.coach_coach.coach_coach_server.common.exception.AccessDeniedException; import site.coach_coach.coach_coach_server.common.exception.NotFoundException; import site.coach_coach.coach_coach_server.matching.domain.Matching; import site.coach_coach.coach_coach_server.matching.repository.MatchingRepository; +import site.coach_coach.coach_coach_server.notification.service.NotificationService; import site.coach_coach.coach_coach_server.routine.domain.Routine; import site.coach_coach.coach_coach_server.routine.dto.CreateRoutineRequest; import site.coach_coach.coach_coach_server.routine.dto.RoutineCreatorDto; @@ -38,6 +40,7 @@ public class RoutineService { private final CoachRepository coachRepository; private final UserRepository userRepository; private final SportRepository sportRepository; + private final NotificationService notificationService; private int numberOfCompletedRoutine; public void checkIsMatching(Long userId, Long coachId) { @@ -130,6 +133,8 @@ public Routine createRoutine(CreateRoutineRequest createRoutineRequest, Long use Coach coach = coachRepository.findById(coachId) .orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COACH)); routineBuilder.coach(coach); + notificationService.createNotification(createRoutineRequest.userId(), userIdByJwt, + RelationFunctionEnum.routine); } return routineRepository.save(routineBuilder.build()); diff --git a/src/main/java/site/coach_coach/coach_coach_server/user/service/UserService.java b/src/main/java/site/coach_coach/coach_coach_server/user/service/UserService.java index 4abc3810..171ab0d3 100644 --- a/src/main/java/site/coach_coach/coach_coach_server/user/service/UserService.java +++ b/src/main/java/site/coach_coach/coach_coach_server/user/service/UserService.java @@ -115,7 +115,7 @@ private AuthResponse getLoggedInUserAuthStatus(User user) { GenderEnum gender = user.getGender(); String profileImageUrl = user.getProfileImageUrl(); boolean isCoach = user.getIsCoach(); - int countOfNotifications = notificationRepository.countByUser_UserId(user.getUserId()); + int countOfNotifications = notificationRepository.countByUser_UserIdAndIsReadFalse(user.getUserId()); return AuthResponse.builder() .isLogin(true) .nickname(nickname) diff --git a/src/main/resources/db/migration/V23__modify_notification_table.sql b/src/main/resources/db/migration/V23__modify_notification_table.sql new file mode 100644 index 00000000..5bc707f1 --- /dev/null +++ b/src/main/resources/db/migration/V23__modify_notification_table.sql @@ -0,0 +1,13 @@ +ALTER TABLE `coachcoach`.`notifications` + ADD COLUMN `is_read` BIT(1) NOT NULL DEFAULT b'0' AFTER `relation_function`, +CHANGE COLUMN `message` `message` VARCHAR(200) NOT NULL , +CHANGE COLUMN `relation_function` `relation_function` ENUM('ask', 'like', 'review', 'match', 'refusal', 'cancel', 'routine', 'request') NOT NULL ; + +ALTER TABLE `coachcoach`.`notifications` + ADD COLUMN `coach_id` BIGINT NOT NULL AFTER `user_id`; +ALTER TABLE `coachcoach`.`notifications` + ADD INDEX `fk_notifications_coach_id_idx` (`coach_id` ASC) VISIBLE; +ALTER TABLE `coachcoach`.`notifications` + ADD CONSTRAINT `fk_notifications_coach_id` + FOREIGN KEY (`coach_id`) + REFERENCES `coachcoach`.`users` (`user_id`);