From d03e6a33386442e5d38aaa89e8a036dc60b24540 Mon Sep 17 00:00:00 2001 From: dungbik Date: Mon, 18 Aug 2025 00:25:12 +0900 Subject: [PATCH 1/8] =?UTF-8?q?Feat:=20=EA=B7=B8=EB=A3=B9=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=A0=EC=B2=AD=20=EC=95=8C=EB=A6=BC=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/event/GroupJoinRequestedEvent.java | 10 ++ .../repository/GroupMemberRepository.java | 3 + .../GroupRolePermissionRepository.java | 4 + .../groupjoin/service/GroupJoinService.java | 114 ++++++++++++------ .../notification/entity/Notification.java | 13 +- .../notification/entity/NotificationType.java | 3 +- ...JoinNotificationDispatchEventListener.java | 25 ++++ .../GroupJoinRequestedEventListener.java | 40 ++++++ .../GroupJoinNotificationDispatchEvent.java | 10 ++ .../model/NotificationResponse.java | 6 +- .../service/NotificationService.java | 56 ++++++++- .../repository/UserProfileRepository.java | 4 + .../flipnote/user/service/UserService.java | 5 + src/main/resources/application.yml | 5 + src/main/resources/messages.properties | 1 + 15 files changed, 254 insertions(+), 45 deletions(-) create mode 100644 src/main/java/project/flipnote/common/model/event/GroupJoinRequestedEvent.java create mode 100644 src/main/java/project/flipnote/notification/listener/GroupJoinNotificationDispatchEventListener.java create mode 100644 src/main/java/project/flipnote/notification/listener/GroupJoinRequestedEventListener.java create mode 100644 src/main/java/project/flipnote/notification/model/GroupJoinNotificationDispatchEvent.java diff --git a/src/main/java/project/flipnote/common/model/event/GroupJoinRequestedEvent.java b/src/main/java/project/flipnote/common/model/event/GroupJoinRequestedEvent.java new file mode 100644 index 00000000..83fb8f17 --- /dev/null +++ b/src/main/java/project/flipnote/common/model/event/GroupJoinRequestedEvent.java @@ -0,0 +1,10 @@ +package project.flipnote.common.model.event; + +import java.util.List; + +public record GroupJoinRequestedEvent( + Long groupId, + List receiverIds, + Long requesterId +) { +} diff --git a/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java b/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java index c5001b75..4e3badce 100644 --- a/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java @@ -6,8 +6,10 @@ import org.springframework.stereotype.Repository; import project.flipnote.group.entity.GroupMember; +import project.flipnote.group.entity.GroupMemberRole; import project.flipnote.user.entity.UserProfile; +import java.util.List; import java.util.Optional; @Repository @@ -20,4 +22,5 @@ public interface GroupMemberRepository extends JpaRepository Optional findByGroup_IdAndUser_Id(Long groupId, Long userId); + List findByGroupAndRoleIn(Group group, List roles); } diff --git a/src/main/java/project/flipnote/group/repository/GroupRolePermissionRepository.java b/src/main/java/project/flipnote/group/repository/GroupRolePermissionRepository.java index f61756b0..e2ec5117 100644 --- a/src/main/java/project/flipnote/group/repository/GroupRolePermissionRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupRolePermissionRepository.java @@ -1,5 +1,7 @@ package project.flipnote.group.repository; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; import project.flipnote.group.entity.Group; @@ -11,4 +13,6 @@ @Repository public interface GroupRolePermissionRepository extends JpaRepository { boolean existsByGroupAndRoleAndGroupPermission(Group group, GroupMemberRole role, GroupPermission groupPermission); + + List findByGroupAndGroupPermission(Group group, GroupPermission groupPermission); } diff --git a/src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java b/src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java index 71e78665..cb8d990b 100644 --- a/src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java +++ b/src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java @@ -1,15 +1,23 @@ package project.flipnote.groupjoin.service; +import java.util.List; -import jakarta.validation.Valid; -import lombok.extern.slf4j.Slf4j; - +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; -import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Transactional; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import project.flipnote.common.exception.BizException; +import project.flipnote.common.model.event.GroupJoinRequestedEvent; import project.flipnote.common.security.dto.AuthPrinciple; -import project.flipnote.group.entity.*; +import project.flipnote.group.entity.Group; +import project.flipnote.group.entity.GroupMember; +import project.flipnote.group.entity.GroupMemberRole; +import project.flipnote.group.entity.GroupPermission; +import project.flipnote.group.entity.GroupPermissionStatus; +import project.flipnote.group.entity.GroupRolePermission; import project.flipnote.group.exception.GroupErrorCode; import project.flipnote.group.repository.GroupMemberRepository; import project.flipnote.group.repository.GroupPermissionRepository; @@ -18,15 +26,18 @@ import project.flipnote.groupjoin.entity.GroupJoin; import project.flipnote.groupjoin.entity.GroupJoinStatus; import project.flipnote.groupjoin.exception.GroupJoinErrorCode; -import project.flipnote.groupjoin.model.*; +import project.flipnote.groupjoin.model.FindGroupJoinListMeResponse; +import project.flipnote.groupjoin.model.GroupJoinListResponse; +import project.flipnote.groupjoin.model.GroupJoinRequest; +import project.flipnote.groupjoin.model.GroupJoinRespondRequest; +import project.flipnote.groupjoin.model.GroupJoinRespondResponse; +import project.flipnote.groupjoin.model.GroupJoinResponse; import project.flipnote.groupjoin.repository.GroupJoinRepository; import project.flipnote.user.entity.UserProfile; import project.flipnote.user.entity.UserStatus; import project.flipnote.user.exception.UserErrorCode; import project.flipnote.user.repository.UserProfileRepository; -import java.util.List; - @Slf4j @Service @RequiredArgsConstructor @@ -38,21 +49,22 @@ public class GroupJoinService { private final GroupMemberRepository groupMemberRepository; private final GroupRolePermissionRepository groupRolePermissionRepository; private final GroupPermissionRepository groupPermissionRepository; + private final ApplicationEventPublisher eventPublisher; //유저 정보 조회 private UserProfile findUser(AuthPrinciple authPrinciple) { return userProfileRepository.findByIdAndStatus(authPrinciple.userId(), UserStatus.ACTIVE).orElseThrow( - () -> new BizException(UserErrorCode.USER_NOT_FOUND) + () -> new BizException(UserErrorCode.USER_NOT_FOUND) ); } //그룹 정보 조회 private Group findGroup(Long groupId) { return groupRepository.findById(groupId).orElseThrow( - () -> new BizException(GroupErrorCode.GROUP_NOT_FOUND) + () -> new BizException(GroupErrorCode.GROUP_NOT_FOUND) ); } - + //중복조회 private Boolean existGroupJoin(Group group, UserProfile userProfile) { return groupJoinRepository.existsByGroup_idAndUser_id(group.getId(), userProfile.getId()); @@ -69,33 +81,34 @@ private void checkMaxMember(Group group) { throw new BizException(GroupJoinErrorCode.GROUP_IS_ALREADY_MAX_MEMBER); } } - + //그룹 내 권한 정보 조회 - private Boolean hasPermission(Group group, UserProfile userProfile) { + private Boolean hasPermission(Group group, UserProfile userProfile) { GroupMember groupMember = groupMemberRepository.findByGroupAndUser(group, userProfile).orElseThrow( - () -> new BizException(GroupJoinErrorCode.USER_NOT_IN_GROUP) + () -> new BizException(GroupJoinErrorCode.USER_NOT_IN_GROUP) ); - GroupPermission groupPermission = groupPermissionRepository.findByName(GroupPermissionStatus.JOIN_REQUEST_MANAGE); + GroupPermission groupPermission = groupPermissionRepository.findByName( + GroupPermissionStatus.JOIN_REQUEST_MANAGE); - return groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission( - group, - groupMember.getRole(), - groupPermission); + return groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission( + group, + groupMember.getRole(), + groupPermission); } - + //그룹 내 모든 가입신청 요청 조회 private List findGroupJoins(Group group) { - return groupJoinRepository.findAllByGroup(group); + return groupJoinRepository.findAllByGroup(group); } //가입 신청 조회 private GroupJoin findGroupJoin(Long joinId) { return groupJoinRepository.findById(joinId).orElseThrow( - () -> new BizException(GroupJoinErrorCode.NOT_EXIST_JOIN) + () -> new BizException(GroupJoinErrorCode.NOT_EXIST_JOIN) ); } - + //가입 신청 요청 @Transactional public GroupJoinResponse joinRequest(AuthPrinciple authPrinciple, Long groupId, GroupJoinRequest req) { @@ -107,15 +120,15 @@ public GroupJoinResponse joinRequest(AuthPrinciple authPrinciple, Long groupId, if (existGroupJoin(group, user)) { throw new BizException(GroupJoinErrorCode.ALREADY_JOINED_GROUP); } - + //비공개 그룹일 경우 if (!group.getPublicVisible()) { throw new BizException(GroupJoinErrorCode.GROUP_IS_NOT_PUBLIC); } - + //그룹이 최대인원인 경우 checkMaxMember(group); - + GroupJoinStatus status = GroupJoinStatus.ACCEPT; //가입 신청이 필수이면 pending 아니면 바로 가입 if (group.getApplicationRequired()) { @@ -123,17 +136,43 @@ public GroupJoinResponse joinRequest(AuthPrinciple authPrinciple, Long groupId, } GroupJoin groupJoin = GroupJoin.builder() - .group(group) - .user(user) - .joinIntro(req.joinIntro()) - .status(status) - .build(); + .group(group) + .user(user) + .joinIntro(req.joinIntro()) + .status(status) + .build(); groupJoinRepository.save(groupJoin); + sendJoinRequestNotification(group, user); + return GroupJoinResponse.from(groupJoin.getId(), groupJoin.getStatus()); } + /** + * 그룹 가입 신청 알림 전송 + * + * @param group 가입 신청 대상 그룹 + * @param requester 가입 신청자 + * @author 윤정환 + */ + private void sendJoinRequestNotification(Group group, UserProfile requester) { + // TODO: GroupPermission 캐시 혹은 name 자체를 pk로 써서 쿼리 조회 없이 사용할 수 있도록 수정 필요 + GroupPermission groupPermission = groupPermissionRepository + .findByName(GroupPermissionStatus.JOIN_REQUEST_MANAGE); + List memberRoles = groupRolePermissionRepository + .findByGroupAndGroupPermission(group, groupPermission) + .stream() + .map(GroupRolePermission::getRole) + .toList(); + List receiverIds = groupMemberRepository.findByGroupAndRoleIn(group, memberRoles) + .stream() + .map(GroupMember::getId) + .toList(); + + eventPublisher.publishEvent(new GroupJoinRequestedEvent(group.getId(), receiverIds, requester.getId())); + } + //그룹 가입 신청 리스트 조회 public GroupJoinListResponse findGroupJoinList(AuthPrinciple authPrinciple, Long groupId) { //유저 조회 @@ -141,10 +180,10 @@ public GroupJoinListResponse findGroupJoinList(AuthPrinciple authPrinciple, Long //그룹 조회 Group group = findGroup(groupId); - + //그룹 내 권한 조회 Boolean isExistPermission = hasPermission(group, userProfile); - + //권한 존재하지 않으면 에러 if (!isExistPermission) { throw new BizException(GroupJoinErrorCode.USER_NOT_PERMISSION); @@ -152,14 +191,15 @@ public GroupJoinListResponse findGroupJoinList(AuthPrinciple authPrinciple, Long //그룹 내 가입 신청 리스트 조회 List groupJoins = findGroupJoins(group); - + //반환 return GroupJoinListResponse.from(groupJoins); } //가입 신청 응답 @Transactional - public GroupJoinRespondResponse respondToJoinRequest(AuthPrinciple authPrinciple, Long groupId, Long joinId, @Valid GroupJoinRespondRequest req) { + public GroupJoinRespondResponse respondToJoinRequest(AuthPrinciple authPrinciple, Long groupId, Long joinId, + @Valid GroupJoinRespondRequest req) { //유저 조회 UserProfile user = findUser(authPrinciple); @@ -176,7 +216,7 @@ public GroupJoinRespondResponse respondToJoinRequest(AuthPrinciple authPrinciple //그룹 가입 신청 조회 GroupJoin groupJoin = findGroupJoin(joinId); - + //최대 인원 조회 if (req.status() == GroupJoinStatus.ACCEPT) { checkMaxMember(group); @@ -184,7 +224,7 @@ public GroupJoinRespondResponse respondToJoinRequest(AuthPrinciple authPrinciple groupJoin.updateStatus(req.status()); groupJoinRepository.save(groupJoin); - + //그룹 멤버 추가 GroupMember groupMember = GroupMember.builder() .group(group) diff --git a/src/main/java/project/flipnote/notification/entity/Notification.java b/src/main/java/project/flipnote/notification/entity/Notification.java index fc0cc6e4..217b9e19 100644 --- a/src/main/java/project/flipnote/notification/entity/Notification.java +++ b/src/main/java/project/flipnote/notification/entity/Notification.java @@ -33,6 +33,8 @@ public class Notification extends BaseEntity { @Column(nullable = false) private Long receiverId; + private Long groupId; + @Enumerated(EnumType.STRING) @Column(nullable = false, length = 20) private NotificationType type; @@ -41,7 +43,7 @@ public class Notification extends BaseEntity { private Map variables; @Convert(converter = MapToJsonConverter.class) - private Map additionalData; + private Map metadata; @Column(name = "is_read", nullable = false) boolean read; @@ -50,12 +52,17 @@ public class Notification extends BaseEntity { @Builder public Notification( - Long receiverId, NotificationType type, Map variables, Map additionalData + Long receiverId, + Long groupId, + NotificationType type, + Map variables, + Map metadata ) { this.receiverId = receiverId; + this.groupId = groupId; this.type = type; this.variables = variables == null ? new HashMap<>() : variables; - this.additionalData = additionalData == null ? new HashMap<>() : additionalData; + this.metadata = metadata == null ? new HashMap<>() : metadata; this.read = false; } diff --git a/src/main/java/project/flipnote/notification/entity/NotificationType.java b/src/main/java/project/flipnote/notification/entity/NotificationType.java index bcc2368e..da58acbe 100644 --- a/src/main/java/project/flipnote/notification/entity/NotificationType.java +++ b/src/main/java/project/flipnote/notification/entity/NotificationType.java @@ -6,7 +6,8 @@ @Getter @RequiredArgsConstructor public enum NotificationType { - GROUP_INVITE("notification.group.invite"); + GROUP_INVITE("notification.group.invite"), + GROUP_JOIN_REQUEST("notification.group.join.request"); private final String messageKey; } diff --git a/src/main/java/project/flipnote/notification/listener/GroupJoinNotificationDispatchEventListener.java b/src/main/java/project/flipnote/notification/listener/GroupJoinNotificationDispatchEventListener.java new file mode 100644 index 00000000..62ef3ac2 --- /dev/null +++ b/src/main/java/project/flipnote/notification/listener/GroupJoinNotificationDispatchEventListener.java @@ -0,0 +1,25 @@ +package project.flipnote.notification.listener; + +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import project.flipnote.notification.model.GroupJoinNotificationDispatchEvent; +import project.flipnote.notification.service.NotificationService; + +@Slf4j +@RequiredArgsConstructor +@Component +public class GroupJoinNotificationDispatchEventListener { + + private final NotificationService notificationService; + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleGroupJoinNotificationDispatchEvent(GroupJoinNotificationDispatchEvent event) { + notificationService.sendGroupJoinRequestNotifications(event.notifications()); + } +} diff --git a/src/main/java/project/flipnote/notification/listener/GroupJoinRequestedEventListener.java b/src/main/java/project/flipnote/notification/listener/GroupJoinRequestedEventListener.java new file mode 100644 index 00000000..77257be3 --- /dev/null +++ b/src/main/java/project/flipnote/notification/listener/GroupJoinRequestedEventListener.java @@ -0,0 +1,40 @@ +package project.flipnote.notification.listener; + +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Recover; +import org.springframework.retry.annotation.Retryable; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import project.flipnote.common.model.event.GroupJoinRequestedEvent; +import project.flipnote.notification.service.NotificationService; + +@Slf4j +@RequiredArgsConstructor +@Component +public class GroupJoinRequestedEventListener { + + private final NotificationService notificationService; + + @Async + @Retryable( + maxAttempts = 3, + backoff = @Backoff(delay = 2000, multiplier = 2) + ) + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleGroupJoinRequestedEvent(GroupJoinRequestedEvent event) { + notificationService.sendGroupJoinRequest(event.groupId(), event.receiverIds(), event.requesterId()); + } + + @Recover + public void recover(Exception ex, GroupJoinRequestedEvent event) { + log.error( + "그룹 가입 신청 후속 처리 예외 발생: groupId={}, receiverIds={}, requesterId={}" + , event.groupId(), event.receiverIds(), event.requesterId(), ex + ); + } +} diff --git a/src/main/java/project/flipnote/notification/model/GroupJoinNotificationDispatchEvent.java b/src/main/java/project/flipnote/notification/model/GroupJoinNotificationDispatchEvent.java new file mode 100644 index 00000000..7878d318 --- /dev/null +++ b/src/main/java/project/flipnote/notification/model/GroupJoinNotificationDispatchEvent.java @@ -0,0 +1,10 @@ +package project.flipnote.notification.model; + +import java.util.List; + +import project.flipnote.notification.entity.Notification; + +public record GroupJoinNotificationDispatchEvent( + List notifications +) { +} diff --git a/src/main/java/project/flipnote/notification/model/NotificationResponse.java b/src/main/java/project/flipnote/notification/model/NotificationResponse.java index 5764061d..abf631ca 100644 --- a/src/main/java/project/flipnote/notification/model/NotificationResponse.java +++ b/src/main/java/project/flipnote/notification/model/NotificationResponse.java @@ -9,8 +9,9 @@ public record NotificationResponse( Long notificationId, + Long groupId, String message, - Map additionalData, + Map metadata, boolean isRead, @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @@ -23,8 +24,9 @@ public record NotificationResponse( public static NotificationResponse of(Notification notification, String message) { return new NotificationResponse( notification.getId(), + notification.getGroupId(), message, - notification.getAdditionalData(), + notification.getMetadata(), notification.isRead(), notification.getReadAt(), notification.getCreatedAt() diff --git a/src/main/java/project/flipnote/notification/service/NotificationService.java b/src/main/java/project/flipnote/notification/service/NotificationService.java index e292c4d7..40f5677a 100644 --- a/src/main/java/project/flipnote/notification/service/NotificationService.java +++ b/src/main/java/project/flipnote/notification/service/NotificationService.java @@ -9,6 +9,7 @@ import java.util.Optional; import org.apache.commons.text.StringSubstitutor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.MessageSource; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -30,12 +31,14 @@ import project.flipnote.notification.entity.Notification; import project.flipnote.notification.entity.NotificationType; import project.flipnote.notification.exception.NotificationErrorCode; +import project.flipnote.notification.model.GroupJoinNotificationDispatchEvent; import project.flipnote.notification.model.MarkNotificationsAsReadRequest; import project.flipnote.notification.model.NotificationListRequest; import project.flipnote.notification.model.NotificationResponse; import project.flipnote.notification.model.TokenRegisterRequest; import project.flipnote.notification.repository.FcmTokenRepository; import project.flipnote.notification.repository.NotificationRepository; +import project.flipnote.user.service.UserService; @Slf4j @RequiredArgsConstructor @@ -48,6 +51,8 @@ public class NotificationService { private final GroupService groupService; private final FcmTokenRepository fcmTokenRepository; private final FirebaseService firebaseService; + private final UserService userService; + private final ApplicationEventPublisher eventPublisher; /** * 알림 목록 커서 기반 페이징으로 조회 @@ -93,9 +98,9 @@ public void sendGroupInvite(Long groupId, Long inviteeId) { Notification notification = Notification.builder() .receiverId(inviteeId) + .groupId(groupId) .type(type) .variables(Map.of("groupName", groupName)) - .additionalData(Map.of("groupId", groupId)) .build(); notificationRepository.save(notification); @@ -139,6 +144,54 @@ public void markNotificationsAsRead(Long userId, MarkNotificationsAsReadRequest notificationRepository.bulkMarkAsRead(userId, req.notificationIds(), LocalDateTime.now()); } + /** + * 그룹 가입 신청 알림 전송 + * + * @param groupId 가입 신청 대상 그룹 ID + * @param receiverIds 알림 받는 회원 ID 목록 + * @param requesterId 가입 신청 회원 ID + * @author 윤정환 + */ + @Transactional + public void sendGroupJoinRequest(Long groupId, List receiverIds, Long requesterId) { + NotificationType type = NotificationType.GROUP_JOIN_REQUEST; + String requesterNickname = userService.getNickname(requesterId); + + List notifications = receiverIds.stream() + .map((receiverId) -> Notification.builder() + .receiverId(receiverId) + .groupId(groupId) + .type(type) + .variables(Map.of("requesterNickname", requesterNickname)) + .metadata(Map.of("requesterId", requesterId)) + .build()) + .toList(); + notificationRepository.saveAll(notifications); + + eventPublisher.publishEvent(new GroupJoinNotificationDispatchEvent(notifications)); + } + + /** + * 그룹 가입 신청 알림 회원들에게 전송 + * + * @param notifications 전송할 알림 데이터 목록 + * @author 윤정환 + */ + public void sendGroupJoinRequestNotifications(List notifications) { + for (Notification notification : notifications) { + try { + // TODO: 전송 실패시 재처리 + String message = buildMessage(notification, Locale.KOREA); + sendNotification(notification.getReceiverId(), message); + } catch (Exception ex) { + log.error( + "Failed to send group join request notification to receiverId={}, notificationId={}", + notification.getReceiverId(), notification.getId(), ex + ); + } + } + } + /** * FCM을 통해 실제 알림 전송 *

@@ -182,7 +235,6 @@ private void sendNotification(Long userId, String body) { fcmTokenRepository.bulkUpdateLastUsedAt(validTokens, LocalDateTime.now()); } } catch (FirebaseMessagingException e) { - log.error("FCM 전송 실패 userId:{}", userId, e); String errorName = e.getMessagingErrorCode() != null ? e.getMessagingErrorCode().name() : "INTERNAL"; FcmErrorCode code = FcmErrorCode.from(errorName); if (code == FcmErrorCode.UNAVAILABLE) { diff --git a/src/main/java/project/flipnote/user/repository/UserProfileRepository.java b/src/main/java/project/flipnote/user/repository/UserProfileRepository.java index 7c6370c3..858f28ad 100644 --- a/src/main/java/project/flipnote/user/repository/UserProfileRepository.java +++ b/src/main/java/project/flipnote/user/repository/UserProfileRepository.java @@ -4,6 +4,7 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import project.flipnote.user.model.UserIdNickname; import project.flipnote.user.entity.UserProfile; @@ -20,4 +21,7 @@ public interface UserProfileRepository extends JpaRepository Optional findByEmailAndStatus(String email, UserStatus status); List findIdAndNicknameByIdIn(List ids); + + @Query("SELECT up.nickname FROM UserProfile up WHERE up.id = :userId") + Optional findNicknameById(Long userId); } diff --git a/src/main/java/project/flipnote/user/service/UserService.java b/src/main/java/project/flipnote/user/service/UserService.java index f8b4d8ea..b00eb043 100644 --- a/src/main/java/project/flipnote/user/service/UserService.java +++ b/src/main/java/project/flipnote/user/service/UserService.java @@ -123,4 +123,9 @@ public Map getIdAndNicknames(List inviteeUserIds) { (a, b) -> a )); } + + public String getNickname(Long userId) { + return userProfileRepository.findNicknameById(userId) + .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND)); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index fd33dbab..7a8051c8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,6 +16,11 @@ spring: properties: hibernate: dialect: org.hibernate.dialect.MySQLDialect + order_inserts: true + order_updates: true + jdbc: + batch_size: 50 + batch_versioned_data: true thymeleaf: prefix: classpath:/templates/ diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 03554880..f8f13a17 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -1 +1,2 @@ notification.group.invite=${groupName} 그룹에 초대되셨습니다. +notification.group.join.request=${requesterNickname}님이 그룹 가입을 신청했습니다. From 64f75940f3a72179717adc56d4247706d3020aec Mon Sep 17 00:00:00 2001 From: dungbik Date: Mon, 18 Aug 2025 00:35:49 +0900 Subject: [PATCH 2/8] =?UTF-8?q?Feat:=20=EC=95=8C=EB=A6=BC=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/NotificationController.java | 11 +++++++++++ .../docs/NotificationControllerDocs.java | 3 +++ .../exception/NotificationErrorCode.java | 4 +++- .../repository/NotificationRepository.java | 3 +++ .../service/NotificationService.java | 19 +++++++++++++++++++ 5 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/main/java/project/flipnote/notification/controller/NotificationController.java b/src/main/java/project/flipnote/notification/controller/NotificationController.java index 8cf0211c..9ebe7d4a 100644 --- a/src/main/java/project/flipnote/notification/controller/NotificationController.java +++ b/src/main/java/project/flipnote/notification/controller/NotificationController.java @@ -5,6 +5,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -58,4 +59,14 @@ public ResponseEntity markNotificationsAsRead( return ResponseEntity.ok().build(); } + + @PostMapping("/{notificationId}/read") + public ResponseEntity markNotificationAsRead( + @PathVariable("notificationId") Long notificationId, + @AuthenticationPrincipal AuthPrinciple authPrinciple + ) { + notificationService.markNotificationAsRead(authPrinciple.userId(), notificationId); + + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/project/flipnote/notification/controller/docs/NotificationControllerDocs.java b/src/main/java/project/flipnote/notification/controller/docs/NotificationControllerDocs.java index 805c4d29..b00f36b8 100644 --- a/src/main/java/project/flipnote/notification/controller/docs/NotificationControllerDocs.java +++ b/src/main/java/project/flipnote/notification/controller/docs/NotificationControllerDocs.java @@ -25,4 +25,7 @@ ResponseEntity> getNotifications( @Operation(summary = "여러 알림을 읽음 처리") ResponseEntity markNotificationsAsRead(MarkNotificationsAsReadRequest req, AuthPrinciple authPrinciple); + + @Operation(summary = "알림 읽음 처리") + ResponseEntity markNotificationAsRead(Long notificationId, AuthPrinciple authPrinciple); } diff --git a/src/main/java/project/flipnote/notification/exception/NotificationErrorCode.java b/src/main/java/project/flipnote/notification/exception/NotificationErrorCode.java index 820dd441..970d139b 100644 --- a/src/main/java/project/flipnote/notification/exception/NotificationErrorCode.java +++ b/src/main/java/project/flipnote/notification/exception/NotificationErrorCode.java @@ -10,7 +10,9 @@ @RequiredArgsConstructor public enum NotificationErrorCode implements ErrorCode { FCM_INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "NOTIFICATION_001", "FCM 내부 오류가 발생했습니다"), - FCM_SERVER_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "NOTIFICATION_002", "FCM 서버를 사용할 수 없습니다."); + FCM_SERVER_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "NOTIFICATION_002", "FCM 서버를 사용할 수 없습니다."), + NOTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "NOTIFICATION_003", "알림이 존재하지 않습니다."), + ALREADY_READ_NOTIFICATION(HttpStatus.CONFLICT, "NOTIFICATION_004", "이미 읽은 알림입니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/project/flipnote/notification/repository/NotificationRepository.java b/src/main/java/project/flipnote/notification/repository/NotificationRepository.java index 8bb8bdf9..44e16f64 100644 --- a/src/main/java/project/flipnote/notification/repository/NotificationRepository.java +++ b/src/main/java/project/flipnote/notification/repository/NotificationRepository.java @@ -2,6 +2,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -33,4 +34,6 @@ int bulkMarkAsRead( @Param("ids") List ids, @Param("now") LocalDateTime now ); + + Optional findByIdAndReceiverId(Long id, Long receiverId); } diff --git a/src/main/java/project/flipnote/notification/service/NotificationService.java b/src/main/java/project/flipnote/notification/service/NotificationService.java index 40f5677a..6579d342 100644 --- a/src/main/java/project/flipnote/notification/service/NotificationService.java +++ b/src/main/java/project/flipnote/notification/service/NotificationService.java @@ -192,6 +192,25 @@ public void sendGroupJoinRequestNotifications(List notifications) } } + /** + * 알림 읽음 처리 + * + * @param userId 알림 읽음 처리를 시도하는 회원 ID + * @param notificationId 읽음 처리할 알림 ID + * @author 윤정환 + */ + @Transactional + public void markNotificationAsRead(Long userId, Long notificationId) { + Notification notification = notificationRepository.findByIdAndReceiverId(notificationId, userId) + .orElseThrow(() -> new BizException(NotificationErrorCode.NOTIFICATION_NOT_FOUND)); + + if (notification.isRead()) { + throw new BizException(NotificationErrorCode.ALREADY_READ_NOTIFICATION); + } + + notification.markAsRead(); + } + /** * FCM을 통해 실제 알림 전송 *

From c1cbc0adaecb7037624456a90c58aa1f05129c6c Mon Sep 17 00:00:00 2001 From: dungbik Date: Mon, 18 Aug 2025 00:40:13 +0900 Subject: [PATCH 3/8] =?UTF-8?q?Feat:=20=EB=AA=A8=EB=93=A0=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=9D=BD=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/NotificationController.java | 8 +++----- .../controller/docs/NotificationControllerDocs.java | 5 ++--- .../model/MarkNotificationsAsReadRequest.java | 11 ----------- .../repository/NotificationRepository.java | 2 -- .../notification/service/NotificationService.java | 10 ++++------ 5 files changed, 9 insertions(+), 27 deletions(-) delete mode 100644 src/main/java/project/flipnote/notification/model/MarkNotificationsAsReadRequest.java diff --git a/src/main/java/project/flipnote/notification/controller/NotificationController.java b/src/main/java/project/flipnote/notification/controller/NotificationController.java index 9ebe7d4a..7aaa3e96 100644 --- a/src/main/java/project/flipnote/notification/controller/NotificationController.java +++ b/src/main/java/project/flipnote/notification/controller/NotificationController.java @@ -16,7 +16,6 @@ import project.flipnote.common.model.response.CursorPageResponse; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.notification.controller.docs.NotificationControllerDocs; -import project.flipnote.notification.model.MarkNotificationsAsReadRequest; import project.flipnote.notification.model.NotificationListRequest; import project.flipnote.notification.model.NotificationResponse; import project.flipnote.notification.model.TokenRegisterRequest; @@ -50,12 +49,11 @@ public ResponseEntity registerFcmToken( return ResponseEntity.status(HttpStatus.CREATED).build(); } - @PostMapping("/read") - public ResponseEntity markNotificationsAsRead( - @Valid @RequestBody MarkNotificationsAsReadRequest req, + @PostMapping("/read-all") + public ResponseEntity markAllNotificationsAsRead( @AuthenticationPrincipal AuthPrinciple authPrinciple ) { - notificationService.markNotificationsAsRead(authPrinciple.userId(), req); + notificationService.markAllNotificationsAsRead(authPrinciple.userId()); return ResponseEntity.ok().build(); } diff --git a/src/main/java/project/flipnote/notification/controller/docs/NotificationControllerDocs.java b/src/main/java/project/flipnote/notification/controller/docs/NotificationControllerDocs.java index b00f36b8..232e1860 100644 --- a/src/main/java/project/flipnote/notification/controller/docs/NotificationControllerDocs.java +++ b/src/main/java/project/flipnote/notification/controller/docs/NotificationControllerDocs.java @@ -6,7 +6,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import project.flipnote.common.model.response.CursorPageResponse; import project.flipnote.common.security.dto.AuthPrinciple; -import project.flipnote.notification.model.MarkNotificationsAsReadRequest; import project.flipnote.notification.model.NotificationListRequest; import project.flipnote.notification.model.NotificationResponse; import project.flipnote.notification.model.TokenRegisterRequest; @@ -23,8 +22,8 @@ ResponseEntity> getNotifications( @Operation(summary = "FCM 토큰 등록") ResponseEntity registerFcmToken(TokenRegisterRequest req, AuthPrinciple authPrinciple); - @Operation(summary = "여러 알림을 읽음 처리") - ResponseEntity markNotificationsAsRead(MarkNotificationsAsReadRequest req, AuthPrinciple authPrinciple); + @Operation(summary = "모든 알림 읽음 처리") + ResponseEntity markAllNotificationsAsRead(AuthPrinciple authPrinciple); @Operation(summary = "알림 읽음 처리") ResponseEntity markNotificationAsRead(Long notificationId, AuthPrinciple authPrinciple); diff --git a/src/main/java/project/flipnote/notification/model/MarkNotificationsAsReadRequest.java b/src/main/java/project/flipnote/notification/model/MarkNotificationsAsReadRequest.java deleted file mode 100644 index c8189a80..00000000 --- a/src/main/java/project/flipnote/notification/model/MarkNotificationsAsReadRequest.java +++ /dev/null @@ -1,11 +0,0 @@ -package project.flipnote.notification.model; - -import java.util.List; - -import jakarta.validation.constraints.NotEmpty; - -public record MarkNotificationsAsReadRequest( - @NotEmpty - List notificationIds -) { -} diff --git a/src/main/java/project/flipnote/notification/repository/NotificationRepository.java b/src/main/java/project/flipnote/notification/repository/NotificationRepository.java index 44e16f64..795e3c32 100644 --- a/src/main/java/project/flipnote/notification/repository/NotificationRepository.java +++ b/src/main/java/project/flipnote/notification/repository/NotificationRepository.java @@ -26,12 +26,10 @@ List findNotificationsByReceiverIdAndCursor( UPDATE Notification n SET n.read = TRUE, n.readAt = :now WHERE n.receiverId = :userId - AND n.id IN :ids AND n.read is FALSE """) int bulkMarkAsRead( @Param("userId") Long userId, - @Param("ids") List ids, @Param("now") LocalDateTime now ); diff --git a/src/main/java/project/flipnote/notification/service/NotificationService.java b/src/main/java/project/flipnote/notification/service/NotificationService.java index 6579d342..9b776401 100644 --- a/src/main/java/project/flipnote/notification/service/NotificationService.java +++ b/src/main/java/project/flipnote/notification/service/NotificationService.java @@ -32,7 +32,6 @@ import project.flipnote.notification.entity.NotificationType; import project.flipnote.notification.exception.NotificationErrorCode; import project.flipnote.notification.model.GroupJoinNotificationDispatchEvent; -import project.flipnote.notification.model.MarkNotificationsAsReadRequest; import project.flipnote.notification.model.NotificationListRequest; import project.flipnote.notification.model.NotificationResponse; import project.flipnote.notification.model.TokenRegisterRequest; @@ -133,15 +132,14 @@ public void registerFcmToken(Long userId, TokenRegisterRequest req) { } /** - * 여러 알림을 읽음 처리 + * 모든 알림을 읽음 처리 * - * @param userId 알림 읽음 처리를 사용하는 회원 ID - * @param req 알림 읽음 처리를 위한 정보 + * @param userId 모든 알림 읽음 처리를 사용하는 회원 ID * @author 윤정환 */ @Transactional - public void markNotificationsAsRead(Long userId, MarkNotificationsAsReadRequest req) { - notificationRepository.bulkMarkAsRead(userId, req.notificationIds(), LocalDateTime.now()); + public void markAllNotificationsAsRead(Long userId) { + notificationRepository.bulkMarkAsRead(userId, LocalDateTime.now()); } /** From f04185cad1d6cac63bbd270f4736ff140263d5c1 Mon Sep 17 00:00:00 2001 From: dungbik Date: Mon, 18 Aug 2025 01:04:08 +0900 Subject: [PATCH 4/8] =?UTF-8?q?Feat:=20=EC=95=8C=EB=A6=BC=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=EC=97=90=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=EB=A7=81=20=EC=B6=94=EA=B0=80=20(groupId,=20read)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/model/request/CursorPageRequest.java | 6 +++++- .../notification/model/NotificationListRequest.java | 10 ++++++++++ .../repository/NotificationRepository.java | 10 +++++++++- .../notification/service/NotificationService.java | 8 +++++--- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/main/java/project/flipnote/common/model/request/CursorPageRequest.java b/src/main/java/project/flipnote/common/model/request/CursorPageRequest.java index cb49ba91..182786bb 100644 --- a/src/main/java/project/flipnote/common/model/request/CursorPageRequest.java +++ b/src/main/java/project/flipnote/common/model/request/CursorPageRequest.java @@ -29,6 +29,10 @@ public Long getCursorId() { return null; } - return Long.valueOf(normalized); + try { + return Long.valueOf(normalized); + } catch (NumberFormatException ex) { + return null; + } } } diff --git a/src/main/java/project/flipnote/notification/model/NotificationListRequest.java b/src/main/java/project/flipnote/notification/model/NotificationListRequest.java index 86d3f410..490d93cf 100644 --- a/src/main/java/project/flipnote/notification/model/NotificationListRequest.java +++ b/src/main/java/project/flipnote/notification/model/NotificationListRequest.java @@ -1,6 +1,16 @@ package project.flipnote.notification.model; +import jakarta.validation.constraints.Min; +import lombok.Getter; +import lombok.Setter; import project.flipnote.common.model.request.CursorPageRequest; +@Getter +@Setter public class NotificationListRequest extends CursorPageRequest { + + @Min(1) + private Long groupId; + + private Boolean read; } diff --git a/src/main/java/project/flipnote/notification/repository/NotificationRepository.java b/src/main/java/project/flipnote/notification/repository/NotificationRepository.java index 795e3c32..c2b09378 100644 --- a/src/main/java/project/flipnote/notification/repository/NotificationRepository.java +++ b/src/main/java/project/flipnote/notification/repository/NotificationRepository.java @@ -14,10 +14,18 @@ public interface NotificationRepository extends JpaRepository { - @Query("SELECT n FROM Notification n WHERE (:cursor IS NULL OR n.id < :cursor) AND n.receiverId = :receiverId ORDER BY n.id DESC") + @Query(""" + SELECT n FROM Notification n + WHERE (:cursor IS NULL OR n.id < :cursor) + AND (:groupId IS NULL OR n.groupId = :groupId) + AND (:read IS NULL OR n.read = :read) + AND n.receiverId = :receiverId + """) List findNotificationsByReceiverIdAndCursor( @Param("receiverId") Long receiverId, @Param("cursor") Long cursor, + @Param("groupId") Long groupId, + @Param("read") Boolean read, Pageable pageable ); diff --git a/src/main/java/project/flipnote/notification/service/NotificationService.java b/src/main/java/project/flipnote/notification/service/NotificationService.java index 9b776401..417fcf5f 100644 --- a/src/main/java/project/flipnote/notification/service/NotificationService.java +++ b/src/main/java/project/flipnote/notification/service/NotificationService.java @@ -13,6 +13,7 @@ import org.springframework.context.MessageSource; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -62,9 +63,10 @@ public class NotificationService { * @author 윤정환 */ public CursorPageResponse getNotifications(Long userId, NotificationListRequest req) { - Pageable pageable = PageRequest.of(0, req.getSize() + 1); - List notifications - = notificationRepository.findNotificationsByReceiverIdAndCursor(userId, req.getCursorId(), pageable); + Pageable pageable = PageRequest.of(0, req.getSize() + 1, Sort.by("id").descending()); + List notifications = notificationRepository.findNotificationsByReceiverIdAndCursor( + userId, req.getCursorId(), req.getGroupId(), req.getRead(), pageable + ); boolean hasNext = notifications.size() > req.getSize(); Long nextCursor = null; From a5d7d68173b36da3923c1ef31dc553d790653f44 Mon Sep 17 00:00:00 2001 From: dungbik Date: Mon, 18 Aug 2025 01:05:33 +0900 Subject: [PATCH 5/8] =?UTF-8?q?Feat:=20=EA=B0=80=EC=9E=85=20=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=EC=9D=B4=20=ED=95=84=EC=88=98=EC=9D=BC=20=EB=95=8C?= =?UTF-8?q?=EB=A7=8C=20=EC=95=8C=EB=A6=BC=EC=9D=84=20=EB=B3=B4=EB=82=B4?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/flipnote/groupjoin/service/GroupJoinService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java b/src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java index cb8d990b..c61b7a0e 100644 --- a/src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java +++ b/src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java @@ -144,7 +144,9 @@ public GroupJoinResponse joinRequest(AuthPrinciple authPrinciple, Long groupId, groupJoinRepository.save(groupJoin); - sendJoinRequestNotification(group, user); + if (group.getApplicationRequired()) { + sendJoinRequestNotification(group, user); + } return GroupJoinResponse.from(groupJoin.getId(), groupJoin.getStatus()); } From a3cf814ac017d2f08594b22723ab606af105caf3 Mon Sep 17 00:00:00 2001 From: dungbik Date: Mon, 18 Aug 2025 01:07:29 +0900 Subject: [PATCH 6/8] =?UTF-8?q?Fix:=20findNicknameById=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=EC=97=90=20@Param=20=EB=88=84=EB=9D=BD=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flipnote/user/repository/UserProfileRepository.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/project/flipnote/user/repository/UserProfileRepository.java b/src/main/java/project/flipnote/user/repository/UserProfileRepository.java index 858f28ad..ce3d8ad7 100644 --- a/src/main/java/project/flipnote/user/repository/UserProfileRepository.java +++ b/src/main/java/project/flipnote/user/repository/UserProfileRepository.java @@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import project.flipnote.user.model.UserIdNickname; import project.flipnote.user.entity.UserProfile; @@ -23,5 +24,5 @@ public interface UserProfileRepository extends JpaRepository List findIdAndNicknameByIdIn(List ids); @Query("SELECT up.nickname FROM UserProfile up WHERE up.id = :userId") - Optional findNicknameById(Long userId); + Optional findNicknameById(@Param("userId") Long userId); } From 193d6c25a8749bb64b056c4332f235e11c5c2a19 Mon Sep 17 00:00:00 2001 From: dungbik Date: Mon, 18 Aug 2025 01:08:52 +0900 Subject: [PATCH 7/8] =?UTF-8?q?Fix:=20sendJoinRequestNotification=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=EC=97=90=EC=84=9C=20=EC=8B=A4?= =?UTF-8?q?=EC=88=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/flipnote/groupjoin/service/GroupJoinService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java b/src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java index c61b7a0e..c66dcbde 100644 --- a/src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java +++ b/src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java @@ -169,7 +169,7 @@ private void sendJoinRequestNotification(Group group, UserProfile requester) { .toList(); List receiverIds = groupMemberRepository.findByGroupAndRoleIn(group, memberRoles) .stream() - .map(GroupMember::getId) + .map((groupMember -> groupMember.getGroup().getId())) .toList(); eventPublisher.publishEvent(new GroupJoinRequestedEvent(group.getId(), receiverIds, requester.getId())); From 2de323be931ccd844b996e5bc7e9a9b17e061f16 Mon Sep 17 00:00:00 2001 From: dungbik Date: Mon, 18 Aug 2025 01:14:19 +0900 Subject: [PATCH 8/8] =?UTF-8?q?Style:=20=EC=BD=94=EB=93=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EC=95=88=EB=A7=9E=EB=8A=94=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 2 +- .../controller/docs/AuthControllerDocs.java | 2 +- .../flipnote/auth/service/AuthService.java | 6 +-- .../common/exception/CommonErrorCode.java | 4 +- .../security/config/SecurityConfig.java | 3 +- .../repository/GroupInvitationRepository.java | 10 +++-- .../repository/GroupMemberRepository.java | 11 +++-- .../flipnote/group/service/GroupService.java | 45 +++++++++---------- .../repository/GroupJoinRepository.java | 10 ++--- .../infra/firebase/FirebaseService.java | 6 ++- .../GroupJoinRequestedEventListener.java | 4 +- .../repository/NotificationRepository.java | 16 +++---- .../service/NotificationService.java | 6 +-- .../flipnote/user/model/UserIdNickname.java | 1 + .../repository/UserProfileRepository.java | 2 +- .../flipnote/user/service/UserService.java | 6 +-- 16 files changed, 71 insertions(+), 63 deletions(-) diff --git a/src/main/java/project/flipnote/auth/controller/AuthController.java b/src/main/java/project/flipnote/auth/controller/AuthController.java index 0f6499e1..228366d9 100644 --- a/src/main/java/project/flipnote/auth/controller/AuthController.java +++ b/src/main/java/project/flipnote/auth/controller/AuthController.java @@ -19,8 +19,8 @@ import lombok.RequiredArgsConstructor; import project.flipnote.auth.controller.docs.AuthControllerDocs; import project.flipnote.auth.model.ChangePasswordRequest; -import project.flipnote.auth.model.EmailVerifyRequest; import project.flipnote.auth.model.EmailVerificationRequest; +import project.flipnote.auth.model.EmailVerifyRequest; import project.flipnote.auth.model.PasswordResetCreateRequest; import project.flipnote.auth.model.PasswordResetRequest; import project.flipnote.auth.model.TokenPair; diff --git a/src/main/java/project/flipnote/auth/controller/docs/AuthControllerDocs.java b/src/main/java/project/flipnote/auth/controller/docs/AuthControllerDocs.java index cca41108..2d7cc421 100644 --- a/src/main/java/project/flipnote/auth/controller/docs/AuthControllerDocs.java +++ b/src/main/java/project/flipnote/auth/controller/docs/AuthControllerDocs.java @@ -5,8 +5,8 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import project.flipnote.auth.model.ChangePasswordRequest; -import project.flipnote.auth.model.EmailVerifyRequest; import project.flipnote.auth.model.EmailVerificationRequest; +import project.flipnote.auth.model.EmailVerifyRequest; import project.flipnote.auth.model.PasswordResetCreateRequest; import project.flipnote.auth.model.PasswordResetRequest; import project.flipnote.auth.model.UserLoginRequest; diff --git a/src/main/java/project/flipnote/auth/service/AuthService.java b/src/main/java/project/flipnote/auth/service/AuthService.java index 920393c2..a25e5049 100644 --- a/src/main/java/project/flipnote/auth/service/AuthService.java +++ b/src/main/java/project/flipnote/auth/service/AuthService.java @@ -18,8 +18,8 @@ import project.flipnote.auth.event.PasswordResetCreateEvent; import project.flipnote.auth.exception.AuthErrorCode; import project.flipnote.auth.model.ChangePasswordRequest; -import project.flipnote.auth.model.EmailVerifyRequest; import project.flipnote.auth.model.EmailVerificationRequest; +import project.flipnote.auth.model.EmailVerifyRequest; import project.flipnote.auth.model.PasswordResetCreateRequest; import project.flipnote.auth.model.PasswordResetRequest; import project.flipnote.auth.model.TokenPair; @@ -34,9 +34,9 @@ import project.flipnote.auth.util.PasswordResetTokenGenerator; import project.flipnote.auth.util.VerificationCodeGenerator; import project.flipnote.common.config.ClientProperties; -import project.flipnote.common.model.request.UserCreateCommand; -import project.flipnote.common.model.event.UserRegisteredEvent; import project.flipnote.common.exception.BizException; +import project.flipnote.common.model.event.UserRegisteredEvent; +import project.flipnote.common.model.request.UserCreateCommand; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.common.security.jwt.JwtComponent; import project.flipnote.user.model.SocialLinksResponse; diff --git a/src/main/java/project/flipnote/common/exception/CommonErrorCode.java b/src/main/java/project/flipnote/common/exception/CommonErrorCode.java index c0df7645..1ed934e0 100644 --- a/src/main/java/project/flipnote/common/exception/CommonErrorCode.java +++ b/src/main/java/project/flipnote/common/exception/CommonErrorCode.java @@ -10,7 +10,9 @@ public enum CommonErrorCode implements ErrorCode { INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR.value(), "COMMON_001", "예기치 않은 오류가 발생했습니다."), INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST.value(), "COMMON_002", "입력값이 올바르지 않습니다."), - SERVICE_TEMPORARILY_UNAVAILABLE(HttpStatus.TOO_MANY_REQUESTS.value(), "COMMON_003", "요청이 많아 처리되지 않았습니다. 잠시 후 다시 시도해주세요."); + SERVICE_TEMPORARILY_UNAVAILABLE( + HttpStatus.TOO_MANY_REQUESTS.value(), "COMMON_003", "요청이 많아 처리되지 않았습니다. 잠시 후 다시 시도해주세요." + ); private final int status; private final String code; diff --git a/src/main/java/project/flipnote/common/security/config/SecurityConfig.java b/src/main/java/project/flipnote/common/security/config/SecurityConfig.java index bd516e0d..c4c8e97e 100644 --- a/src/main/java/project/flipnote/common/security/config/SecurityConfig.java +++ b/src/main/java/project/flipnote/common/security/config/SecurityConfig.java @@ -58,7 +58,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .authorizeHttpRequests(auth -> auth .requestMatchers( HttpMethod.POST, - "/*/users", "/*/auth/token/refresh", "/*/auth/password-resets", "/*/auth/register", "/*/images/upload" + "/*/users", "/*/auth/token/refresh", "/*/auth/password-resets", "/*/auth/register", + "/*/images/upload" ).permitAll() .requestMatchers(HttpMethod.PATCH, "/*/auth/password-resets").permitAll() .requestMatchers( diff --git a/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java index 4cf2e292..e4bff22c 100644 --- a/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java @@ -33,9 +33,11 @@ Optional findByIdAndGroup_IdAndInviteeUserIdAndStatus( boolean existsByGroup_IdAndInviteeEmailAndStatus(Long groupId, String inviteeEmail, GroupInvitationStatus status); @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query("UPDATE GroupInvitation gi " + - "SET gi.status = project.flipnote.group.entity.GroupInvitationStatus.EXPIRED " + - "WHERE gi.status = project.flipnote.group.entity.GroupInvitationStatus.PENDING " + - "AND gi.expiredAt < :now") + @Query(""" + UPDATE GroupInvitation gi + SET gi.status = project.flipnote.group.entity.GroupInvitationStatus.EXPIRED + WHERE gi.status = project.flipnote.group.entity.GroupInvitationStatus.PENDING + AND gi.expiredAt < :now + """) int bulkExpire(@Param("now") LocalDateTime now); } diff --git a/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java b/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java index 4e3badce..dd61aa18 100644 --- a/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java @@ -1,20 +1,19 @@ package project.flipnote.group.repository; +import java.util.List; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; import project.flipnote.group.entity.Group; - -import org.springframework.stereotype.Repository; import project.flipnote.group.entity.GroupMember; import project.flipnote.group.entity.GroupMemberRole; import project.flipnote.user.entity.UserProfile; -import java.util.List; -import java.util.Optional; - @Repository public interface GroupMemberRepository extends JpaRepository { - Optional findByGroupAndUser(Group group, UserProfile userProfile); + Optional findByGroupAndUser(Group group, UserProfile userProfile); long countByGroup_Id(Long groupId); diff --git a/src/main/java/project/flipnote/group/service/GroupService.java b/src/main/java/project/flipnote/group/service/GroupService.java index c45638c4..96f28b99 100644 --- a/src/main/java/project/flipnote/group/service/GroupService.java +++ b/src/main/java/project/flipnote/group/service/GroupService.java @@ -50,14 +50,14 @@ public UserProfile validateUser(AuthPrinciple authPrinciple) { () -> new BizException(UserErrorCode.USER_NOT_FOUND) ); } - + /* 그룹 내 유저 조회 */ public boolean validateGroupInUser(UserProfile user, Long groupId) { return groupMemberRepository.existsByGroup_idAndUser_id(groupId, user.getId()); } - + /* 그룹 조회 */ @@ -67,7 +67,6 @@ public Group validateGroup(Long groupId) { ); } - /* 그룹 생성 */ @@ -113,13 +112,13 @@ private void initializeGroupPermissions(Group group) { List groupPermissions = groupPermissionRepository.findAll(); List groupRolePermissions = Arrays.stream(GroupMemberRole.values()) - .flatMap(role -> groupPermissions.stream() - .map(permission -> GroupRolePermission.builder() - .group(group) - .groupPermission(permission) - .role(role) - .build())) - .toList(); + .flatMap(role -> groupPermissions.stream() + .map(permission -> GroupRolePermission.builder() + .group(group) + .groupPermission(permission) + .role(role) + .build())) + .toList(); groupRolePermissionRepository.saveAll(groupRolePermissions); } @@ -127,16 +126,16 @@ private void initializeGroupPermissions(Group group) { /* 그룹 생성 메서드 */ - private Group createGroup(GroupCreateRequest req) { + private Group createGroup(GroupCreateRequest req) { Group group = Group.builder() - .name(req.name()) - .category(req.category()) - .description(req.description()) - .applicationRequired(req.applicationRequired()) - .publicVisible(req.publicVisible()) - .maxMember(req.maxMember()) - .imageUrl(req.image()) - .build(); + .name(req.name()) + .category(req.category()) + .description(req.description()) + .applicationRequired(req.applicationRequired()) + .publicVisible(req.publicVisible()) + .maxMember(req.maxMember()) + .imageUrl(req.image()) + .build(); Group saveGroup = groupRepository.save(group); @@ -150,10 +149,10 @@ private Group createGroup(GroupCreateRequest req) { */ private void saveGroupOwner(Group group, UserProfile user) { GroupMember groupMember = GroupMember.builder() - .group(group) - .user(user) - .role(GroupMemberRole.OWNER) - .build(); + .group(group) + .user(user) + .role(GroupMemberRole.OWNER) + .build(); groupMemberRepository.save(groupMember); } diff --git a/src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java b/src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java index 54780ead..a6c5d55a 100644 --- a/src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java +++ b/src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java @@ -1,5 +1,7 @@ package project.flipnote.groupjoin.repository; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -7,13 +9,11 @@ import project.flipnote.groupjoin.entity.GroupJoin; import project.flipnote.user.entity.UserProfile; -import java.util.List; - @Repository public interface GroupJoinRepository extends JpaRepository { - List findAllByGroup(Group group); + List findAllByGroup(Group group); - List findAllByUser(UserProfile userProfile); + List findAllByUser(UserProfile userProfile); - boolean existsByGroup_idAndUser_id(Long groupId, Long userId); + boolean existsByGroup_idAndUser_id(Long groupId, Long userId); } diff --git a/src/main/java/project/flipnote/infra/firebase/FirebaseService.java b/src/main/java/project/flipnote/infra/firebase/FirebaseService.java index 10580eda..2a99b475 100644 --- a/src/main/java/project/flipnote/infra/firebase/FirebaseService.java +++ b/src/main/java/project/flipnote/infra/firebase/FirebaseService.java @@ -46,7 +46,11 @@ public void initialize() throws IOException { } } - public BatchResponse sendEachForMulticast(List tokens, String title, String body) throws FirebaseMessagingException { + public BatchResponse sendEachForMulticast( + List tokens, + String title, + String body + ) throws FirebaseMessagingException { Notification notification = Notification.builder() .setTitle(title) .setBody(body) diff --git a/src/main/java/project/flipnote/notification/listener/GroupJoinRequestedEventListener.java b/src/main/java/project/flipnote/notification/listener/GroupJoinRequestedEventListener.java index 77257be3..b1ee84c1 100644 --- a/src/main/java/project/flipnote/notification/listener/GroupJoinRequestedEventListener.java +++ b/src/main/java/project/flipnote/notification/listener/GroupJoinRequestedEventListener.java @@ -33,8 +33,8 @@ public void handleGroupJoinRequestedEvent(GroupJoinRequestedEvent event) { @Recover public void recover(Exception ex, GroupJoinRequestedEvent event) { log.error( - "그룹 가입 신청 후속 처리 예외 발생: groupId={}, receiverIds={}, requesterId={}" - , event.groupId(), event.receiverIds(), event.requesterId(), ex + "그룹 가입 신청 후속 처리 예외 발생: groupId={}, receiverIds={}, requesterId={}", + event.groupId(), event.receiverIds(), event.requesterId(), ex ); } } diff --git a/src/main/java/project/flipnote/notification/repository/NotificationRepository.java b/src/main/java/project/flipnote/notification/repository/NotificationRepository.java index c2b09378..002bc08a 100644 --- a/src/main/java/project/flipnote/notification/repository/NotificationRepository.java +++ b/src/main/java/project/flipnote/notification/repository/NotificationRepository.java @@ -15,11 +15,11 @@ public interface NotificationRepository extends JpaRepository { @Query(""" - SELECT n FROM Notification n - WHERE (:cursor IS NULL OR n.id < :cursor) - AND (:groupId IS NULL OR n.groupId = :groupId) - AND (:read IS NULL OR n.read = :read) - AND n.receiverId = :receiverId + SELECT n FROM Notification n + WHERE (:cursor IS NULL OR n.id < :cursor) + AND (:groupId IS NULL OR n.groupId = :groupId) + AND (:read IS NULL OR n.read = :read) + AND n.receiverId = :receiverId """) List findNotificationsByReceiverIdAndCursor( @Param("receiverId") Long receiverId, @@ -32,9 +32,9 @@ List findNotificationsByReceiverIdAndCursor( @Modifying(clearAutomatically = true, flushAutomatically = true) @Query(""" UPDATE Notification n - SET n.read = TRUE, n.readAt = :now - WHERE n.receiverId = :userId - AND n.read is FALSE + SET n.read = TRUE, n.readAt = :now + WHERE n.receiverId = :userId + AND n.read is FALSE """) int bulkMarkAsRead( @Param("userId") Long userId, diff --git a/src/main/java/project/flipnote/notification/service/NotificationService.java b/src/main/java/project/flipnote/notification/service/NotificationService.java index 417fcf5f..e7819d58 100644 --- a/src/main/java/project/flipnote/notification/service/NotificationService.java +++ b/src/main/java/project/flipnote/notification/service/NotificationService.java @@ -235,11 +235,11 @@ private void sendNotification(Long userId, String body) { List validTokens = new ArrayList<>(); List invalidTokens = new ArrayList<>(); for (int i = 0; i < response.getResponses().size(); i++) { - SendResponse r = response.getResponses().get(i); - if (r.isSuccessful()) { + SendResponse res = response.getResponses().get(i); + if (res.isSuccessful()) { validTokens.add(tokens.get(i)); } else { - String errorName = r.getException().getMessagingErrorCode().name(); + String errorName = res.getException().getMessagingErrorCode().name(); FcmErrorCode code = FcmErrorCode.from(errorName); if (code == FcmErrorCode.UNREGISTERED || code == FcmErrorCode.INVALID_ARGUMENT) { invalidTokens.add(tokens.get(i)); diff --git a/src/main/java/project/flipnote/user/model/UserIdNickname.java b/src/main/java/project/flipnote/user/model/UserIdNickname.java index d7bf56a3..049428ae 100644 --- a/src/main/java/project/flipnote/user/model/UserIdNickname.java +++ b/src/main/java/project/flipnote/user/model/UserIdNickname.java @@ -2,5 +2,6 @@ public interface UserIdNickname { Long getId(); + String getNickname(); } diff --git a/src/main/java/project/flipnote/user/repository/UserProfileRepository.java b/src/main/java/project/flipnote/user/repository/UserProfileRepository.java index ce3d8ad7..3fa1f30a 100644 --- a/src/main/java/project/flipnote/user/repository/UserProfileRepository.java +++ b/src/main/java/project/flipnote/user/repository/UserProfileRepository.java @@ -7,9 +7,9 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import project.flipnote.user.model.UserIdNickname; import project.flipnote.user.entity.UserProfile; import project.flipnote.user.entity.UserStatus; +import project.flipnote.user.model.UserIdNickname; public interface UserProfileRepository extends JpaRepository { diff --git a/src/main/java/project/flipnote/user/service/UserService.java b/src/main/java/project/flipnote/user/service/UserService.java index b00eb043..7aebb948 100644 --- a/src/main/java/project/flipnote/user/service/UserService.java +++ b/src/main/java/project/flipnote/user/service/UserService.java @@ -11,14 +11,14 @@ import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; -import project.flipnote.common.model.request.UserCreateCommand; -import project.flipnote.common.model.event.UserWithdrawnEvent; import project.flipnote.common.exception.BizException; -import project.flipnote.user.model.UserIdNickname; +import project.flipnote.common.model.event.UserWithdrawnEvent; +import project.flipnote.common.model.request.UserCreateCommand; import project.flipnote.user.entity.UserProfile; import project.flipnote.user.entity.UserStatus; import project.flipnote.user.exception.UserErrorCode; import project.flipnote.user.model.MyInfoResponse; +import project.flipnote.user.model.UserIdNickname; import project.flipnote.user.model.UserInfoResponse; import project.flipnote.user.model.UserUpdateRequest; import project.flipnote.user.model.UserUpdateResponse;