-
Notifications
You must be signed in to change notification settings - Fork 19
refactor: 좋아요 알림 재발송 문제 해결 #1440
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 5 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
b5438bb
refactor: 좋아요 알림 마일스톤 정책 분리
KEEKE132 4ce7209
feat: 게시글 좋아요 마일스톤 이력 추가
KEEKE132 cb13e2a
refactor: 좋아요 마일스톤 이벤트 흐름 분리
KEEKE132 5f5b5e3
refactor: 좋아요 알림 이벤트 흐름 단순화
KEEKE132 1a70f07
test: 스키마 검증 대상 패키지 수정
KEEKE132 b324e0a
fix: 좋아요 마일스톤 이력 조회 예외 통일
KEEKE132 6d10fac
test: 좋아요 마일스톤 대상 불가 경계 보완
KEEKE132 e875bf8
test: 소비된 마일스톤 알림 호출 검증 강화
KEEKE132 a4c18a7
feat: 알림 정책 수정(1~5)
KEEKE132 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
90 changes: 90 additions & 0 deletions
90
.../causw/app/main/domain/notification/notification/entity/PostLikeMilestoneAchievement.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,90 @@ | ||
| package net.causw.app.main.domain.notification.notification.entity; | ||
|
|
||
| import net.causw.app.main.domain.community.post.entity.Post; | ||
| import net.causw.app.main.domain.notification.notification.enums.PostLikeMilestoneAchievementStatus; | ||
| import net.causw.app.main.domain.notification.notification.enums.PostLikeMilestoneSuppressionReason; | ||
| import net.causw.app.main.domain.user.account.entity.user.User; | ||
| import net.causw.app.main.shared.entity.BaseEntity; | ||
|
|
||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.EnumType; | ||
| import jakarta.persistence.Enumerated; | ||
| import jakarta.persistence.FetchType; | ||
| import jakarta.persistence.Index; | ||
| import jakarta.persistence.JoinColumn; | ||
| import jakarta.persistence.ManyToOne; | ||
| import jakarta.persistence.OneToOne; | ||
| import jakarta.persistence.Table; | ||
| import jakarta.persistence.UniqueConstraint; | ||
| import lombok.AccessLevel; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @Entity | ||
| @Builder(access = AccessLevel.PROTECTED) | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
| @Table(name = "tb_post_like_milestone_achievement", uniqueConstraints = { | ||
| @UniqueConstraint(name = "uk_post_like_milestone_achievement_post_milestone", columnNames = {"post_id", | ||
| "milestone_count"}) | ||
| }, indexes = { | ||
| @Index(name = "idx_post_like_milestone_achievement_trigger_user", columnList = "trigger_user_id") | ||
| }) | ||
| public class PostLikeMilestoneAchievement extends BaseEntity { | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "post_id", nullable = false, updatable = false) | ||
| private Post post; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "trigger_user_id", updatable = false) | ||
| private User triggerUser; | ||
|
|
||
| @Column(name = "milestone_count", nullable = false, updatable = false) | ||
| private long milestoneCount; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| @Column(name = "status", nullable = false, length = 32) | ||
| private PostLikeMilestoneAchievementStatus status; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| @Column(name = "suppression_reason", length = 32) | ||
| private PostLikeMilestoneSuppressionReason suppressionReason; | ||
|
|
||
| @OneToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "notification_id", unique = true) | ||
| private Notification notification; | ||
|
|
||
| public static PostLikeMilestoneAchievement pending(Post post, User triggerUser, long milestoneCount) { | ||
| return PostLikeMilestoneAchievement.builder() | ||
| .post(post) | ||
| .triggerUser(triggerUser) | ||
| .milestoneCount(milestoneCount) | ||
| .status(PostLikeMilestoneAchievementStatus.PENDING) | ||
| .build(); | ||
| } | ||
|
|
||
| public static PostLikeMilestoneAchievement baselined(Post post, long milestoneCount) { | ||
| return PostLikeMilestoneAchievement.builder() | ||
| .post(post) | ||
| .milestoneCount(milestoneCount) | ||
| .status(PostLikeMilestoneAchievementStatus.BASELINED) | ||
| .build(); | ||
| } | ||
|
|
||
| public void markNotificationCreated(Notification notification) { | ||
| this.notification = notification; | ||
| this.suppressionReason = null; | ||
| this.status = PostLikeMilestoneAchievementStatus.NOTIFICATION_CREATED; | ||
| } | ||
|
|
||
| public void suppress(PostLikeMilestoneSuppressionReason suppressionReason) { | ||
| this.notification = null; | ||
| this.suppressionReason = suppressionReason; | ||
| this.status = PostLikeMilestoneAchievementStatus.SUPPRESSED; | ||
| } | ||
| } |
8 changes: 8 additions & 0 deletions
8
...w/app/main/domain/notification/notification/enums/PostLikeMilestoneAchievementStatus.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,8 @@ | ||
| package net.causw.app.main.domain.notification.notification.enums; | ||
|
|
||
| public enum PostLikeMilestoneAchievementStatus { | ||
| PENDING, | ||
| BASELINED, | ||
| NOTIFICATION_CREATED, | ||
| SUPPRESSED | ||
| } |
8 changes: 8 additions & 0 deletions
8
...w/app/main/domain/notification/notification/enums/PostLikeMilestoneSuppressionReason.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,8 @@ | ||
| package net.causw.app.main.domain.notification.notification.enums; | ||
|
|
||
| public enum PostLikeMilestoneSuppressionReason { | ||
| SELF_LIKE, | ||
| SETTING_DISABLED, | ||
| BLOCKED, | ||
| TARGET_UNAVAILABLE | ||
| } |
10 changes: 10 additions & 0 deletions
10
...net/causw/app/main/domain/notification/notification/event/PostLikeMilestonePushEvent.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,10 @@ | ||
| package net.causw.app.main.domain.notification.notification.event; | ||
|
|
||
| import net.causw.app.main.domain.notification.notification.service.dto.PushNotificationData; | ||
|
|
||
| public record PostLikeMilestonePushEvent( | ||
| String recipientUserId, | ||
| String pushTitle, | ||
| String pushBody, | ||
| PushNotificationData pushData) { | ||
| } |
4 changes: 4 additions & 0 deletions
4
.../causw/app/main/domain/notification/notification/event/PostLikeMilestoneReachedEvent.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,4 @@ | ||
| package net.causw.app.main.domain.notification.notification.event; | ||
|
|
||
| public record PostLikeMilestoneReachedEvent(String postId, String likerId, long milestoneCount) { | ||
| } |
4 changes: 0 additions & 4 deletions
4
...c/main/java/net/causw/app/main/domain/notification/notification/event/PostLikedEvent.java
This file was deleted.
Oops, something went wrong.
13 changes: 13 additions & 0 deletions
13
...n/domain/notification/notification/repository/PostLikeMilestoneAchievementRepository.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,13 @@ | ||
| package net.causw.app.main.domain.notification.notification.repository; | ||
|
|
||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import net.causw.app.main.domain.notification.notification.entity.PostLikeMilestoneAchievement; | ||
|
|
||
| @Repository | ||
| public interface PostLikeMilestoneAchievementRepository | ||
| extends JpaRepository<PostLikeMilestoneAchievement, String> { | ||
|
|
||
| boolean existsByPostIdAndMilestoneCount(String postId, long milestoneCount); | ||
| } |
23 changes: 23 additions & 0 deletions
23
.../notification/notification/service/implementation/PostLikeMilestoneAchievementReader.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,23 @@ | ||
| package net.causw.app.main.domain.notification.notification.service.implementation; | ||
|
|
||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import net.causw.app.main.domain.notification.notification.entity.PostLikeMilestoneAchievement; | ||
| import net.causw.app.main.domain.notification.notification.repository.PostLikeMilestoneAchievementRepository; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| @Transactional(readOnly = true) | ||
| public class PostLikeMilestoneAchievementReader { | ||
|
|
||
| private final PostLikeMilestoneAchievementRepository achievementRepository; | ||
|
|
||
| public PostLikeMilestoneAchievement findById(String achievementId) { | ||
| return achievementRepository.findById(achievementId) | ||
| .orElseThrow(() -> new IllegalStateException( | ||
| "게시글 좋아요 마일스톤 이력을 찾을 수 없습니다: " + achievementId)); | ||
| } | ||
| } | ||
33 changes: 33 additions & 0 deletions
33
...otification/notification/service/implementation/PostLikeMilestoneAchievementRecorder.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 net.causw.app.main.domain.notification.notification.service.implementation; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Propagation; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import net.causw.app.main.domain.community.post.entity.Post; | ||
| import net.causw.app.main.domain.community.post.service.implementation.PostReader; | ||
| import net.causw.app.main.domain.notification.notification.event.PostLikeMilestoneReachedEvent; | ||
| import net.causw.app.main.domain.user.account.entity.user.User; | ||
| import net.causw.app.main.domain.user.account.service.implementation.UserReader; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class PostLikeMilestoneAchievementRecorder { | ||
|
|
||
| private final PostReader postReader; | ||
| private final UserReader userReader; | ||
| private final PostLikeMilestoneAchievementWriter achievementWriter; | ||
|
|
||
| @Transactional(propagation = Propagation.REQUIRES_NEW) | ||
| public Optional<String> record(PostLikeMilestoneReachedEvent event) { | ||
| Post post = postReader.findById(event.postId()); | ||
| User liker = userReader.findUserById(event.likerId()); | ||
|
|
||
| return achievementWriter.savePendingIfAbsent(post, liker, event.milestoneCount()) | ||
| .map(achievement -> achievement.getId()); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
52 changes: 52 additions & 0 deletions
52
.../notification/notification/service/implementation/PostLikeMilestoneAchievementWriter.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,52 @@ | ||
| package net.causw.app.main.domain.notification.notification.service.implementation; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import net.causw.app.main.domain.community.post.entity.Post; | ||
| import net.causw.app.main.domain.notification.notification.entity.Notification; | ||
| import net.causw.app.main.domain.notification.notification.entity.PostLikeMilestoneAchievement; | ||
| import net.causw.app.main.domain.notification.notification.enums.PostLikeMilestoneSuppressionReason; | ||
| import net.causw.app.main.domain.notification.notification.repository.PostLikeMilestoneAchievementRepository; | ||
| import net.causw.app.main.domain.user.account.entity.user.User; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| @Transactional | ||
| public class PostLikeMilestoneAchievementWriter { | ||
|
|
||
| private final PostLikeMilestoneAchievementRepository achievementRepository; | ||
|
|
||
| public Optional<PostLikeMilestoneAchievement> savePendingIfAbsent( | ||
| Post post, | ||
| User triggerUser, | ||
| long milestoneCount) { | ||
| if (achievementRepository.existsByPostIdAndMilestoneCount(post.getId(), milestoneCount)) { | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| PostLikeMilestoneAchievement achievement = PostLikeMilestoneAchievement.pending( | ||
| post, | ||
| triggerUser, | ||
| milestoneCount); | ||
| return Optional.of(achievementRepository.saveAndFlush(achievement)); | ||
| } | ||
|
KEEKE132 marked this conversation as resolved.
|
||
|
|
||
| public PostLikeMilestoneAchievement suppress( | ||
| PostLikeMilestoneAchievement achievement, | ||
| PostLikeMilestoneSuppressionReason suppressionReason) { | ||
| achievement.suppress(suppressionReason); | ||
| return achievementRepository.save(achievement); | ||
| } | ||
|
|
||
| public PostLikeMilestoneAchievement markNotificationCreated( | ||
| PostLikeMilestoneAchievement achievement, | ||
| Notification notification) { | ||
| achievement.markNotificationCreated(notification); | ||
| return achievementRepository.save(achievement); | ||
| } | ||
| } | ||
93 changes: 93 additions & 0 deletions
93
...ification/notification/service/implementation/PostLikeMilestoneNotificationProcessor.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,93 @@ | ||
| package net.causw.app.main.domain.notification.notification.service.implementation; | ||
|
|
||
| import org.springframework.context.ApplicationEventPublisher; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Propagation; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import net.causw.app.main.domain.community.common.service.CommunityPermissionPolicy; | ||
| import net.causw.app.main.domain.community.post.entity.Post; | ||
| import net.causw.app.main.domain.notification.notification.entity.Notification; | ||
| import net.causw.app.main.domain.notification.notification.entity.PostLikeMilestoneAchievement; | ||
| import net.causw.app.main.domain.notification.notification.enums.NoticeType; | ||
| import net.causw.app.main.domain.notification.notification.enums.PostLikeMilestoneAchievementStatus; | ||
| import net.causw.app.main.domain.notification.notification.enums.PostLikeMilestoneSuppressionReason; | ||
| import net.causw.app.main.domain.notification.notification.enums.UserNotificationSettingKey; | ||
| import net.causw.app.main.domain.notification.notification.event.PostLikeMilestonePushEvent; | ||
| import net.causw.app.main.domain.notification.notification.service.dto.PushNotificationData; | ||
| import net.causw.app.main.domain.notification.notification.service.dto.UserNotificationSettingMap; | ||
| import net.causw.app.main.domain.user.account.entity.user.User; | ||
| import net.causw.app.main.domain.user.relation.service.implementation.BlockReader; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class PostLikeMilestoneNotificationProcessor { | ||
|
|
||
| private final PostLikeMilestoneAchievementReader achievementReader; | ||
| private final PostLikeMilestoneAchievementWriter achievementWriter; | ||
| private final NotificationWriter notificationWriter; | ||
| private final NotificationSettingReader notificationSettingReader; | ||
| private final BlockReader blockReader; | ||
| private final ApplicationEventPublisher eventPublisher; | ||
|
|
||
| @Transactional(propagation = Propagation.REQUIRES_NEW) | ||
| public void process(String achievementId) { | ||
| PostLikeMilestoneAchievement achievement = achievementReader.findById(achievementId); | ||
| if (achievement.getStatus() != PostLikeMilestoneAchievementStatus.PENDING) { | ||
| return; | ||
| } | ||
|
|
||
| Post post = achievement.getPost(); | ||
| User liker = achievement.getTriggerUser(); | ||
| User postWriter = post == null ? null : post.getWriter(); | ||
|
|
||
| if (liker == null || isTargetUnavailable(post, postWriter)) { | ||
| achievementWriter.suppress(achievement, PostLikeMilestoneSuppressionReason.TARGET_UNAVAILABLE); | ||
| return; | ||
| } | ||
|
|
||
| if (liker.getId().equals(postWriter.getId())) { | ||
| achievementWriter.suppress(achievement, PostLikeMilestoneSuppressionReason.SELF_LIKE); | ||
| return; | ||
| } | ||
|
|
||
| UserNotificationSettingMap settingMap = notificationSettingReader.findSettingMap(postWriter.getId()); | ||
| if (!settingMap.get(UserNotificationSettingKey.COMMUNITY_LIKE_ON_MY_POST)) { | ||
| achievementWriter.suppress(achievement, PostLikeMilestoneSuppressionReason.SETTING_DISABLED); | ||
| return; | ||
| } | ||
|
|
||
| if (blockReader.existsByBlockerAndBlocked(postWriter, liker)) { | ||
| achievementWriter.suppress(achievement, PostLikeMilestoneSuppressionReason.BLOCKED); | ||
| return; | ||
| } | ||
|
|
||
| long likeCount = achievement.getMilestoneCount(); | ||
| String serviceTitle = String.format("게시물이 좋아요 %d개를 달성했습니다!", likeCount); | ||
| String serviceBody = String.format("내 게시글에 좋아요가 %d개 달렸어요.", likeCount); | ||
| String pushTitle = String.format("게시물 좋아요 %d개 달성", likeCount); | ||
| PushNotificationData pushData = new PushNotificationData(NoticeType.COMMUNITY, post.getId(), | ||
| post.getBoard().getId()); | ||
|
|
||
| Notification notification = notificationWriter.save( | ||
| Notification.of(postWriter, serviceTitle, serviceBody, NoticeType.COMMUNITY, post.getId(), | ||
| post.getBoard().getId())); | ||
| notificationWriter.saveLog(postWriter, notification); | ||
| achievementWriter.markNotificationCreated(achievement, notification); | ||
|
|
||
| eventPublisher.publishEvent(new PostLikeMilestonePushEvent( | ||
| postWriter.getId(), | ||
| pushTitle, | ||
| serviceBody, | ||
| pushData)); | ||
| } | ||
|
|
||
| private boolean isTargetUnavailable(Post post, User postWriter) { | ||
| return !CommunityPermissionPolicy.isAlive(post) | ||
| || postWriter == null | ||
| || postWriter.isInactive() | ||
| || postWriter.isDropped(); | ||
| } | ||
| } |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.