Skip to content

Commit b54dea4

Browse files
authored
Merge pull request #142 from SEMOSAN/feat/#141-semofeed-emoji-notification
[Feat] 세모피드 이모지 클릭 시 FCM 알림 추가
2 parents 3f325b1 + 1392d86 commit b54dea4

5 files changed

Lines changed: 178 additions & 0 deletions

File tree

src/main/java/com/semosan/api/domain/notification/enums/NotificationType.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ public enum NotificationType {
2929
false
3030
),
3131

32+
SEMOFEED_EMOJI(
33+
"세모피드에 반응이 달렸어요",
34+
"{actorName}님이 세모피드에 {emojiType} 반응을 남겼어요",
35+
Set.of("actorId", "actorName", "semoFeedId", "emojiType"),
36+
false
37+
),
38+
3239
/**
3340
* 트래킹 중 거리 마일스톤 도달 시 사진 촬영 유도.
3441
* 포그라운드 즉시 표시를 위해 FCM data-only 로 발송하고, 앱에서 로컬 알림을 생성한다.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.semosan.api.domain.semofeed.notification.service;
2+
3+
import com.semosan.api.domain.notification.enums.NotificationType;
4+
import com.semosan.api.domain.notification.service.NotificationService;
5+
import com.semosan.api.domain.semofeed.entity.SemoFeed;
6+
import com.semosan.api.domain.semofeed.enums.SemoFeedEmojiType;
7+
import com.semosan.api.domain.user.entity.User;
8+
import com.semosan.api.domain.user.repository.UserRepository;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.stereotype.Service;
11+
12+
import java.util.Map;
13+
14+
@Service
15+
@RequiredArgsConstructor
16+
public class SemoFeedNotificationService {
17+
18+
private final NotificationService notificationService;
19+
private final UserRepository userRepository;
20+
21+
// 세모피드에 새 이모지 반응이 등록되면 작성자에게 알림을 보냅니다.
22+
public void sendEmojiNotification(SemoFeed semoFeed, User reactor, SemoFeedEmojiType emojiType) {
23+
User receiver = semoFeed.getUser();
24+
sendIfEligible(
25+
receiver,
26+
reactor,
27+
Map.of(
28+
"actorId", reactor.getId(),
29+
"actorName", reactor.displayName(),
30+
"semoFeedId", semoFeed.getId(),
31+
"emojiType", emojiType.name()
32+
)
33+
);
34+
}
35+
36+
// 본인 반응과 탈퇴한 작성자는 알림 발송 대상에서 제외합니다.
37+
private void sendIfEligible(User receiver, User reactor, Map<String, Object> params) {
38+
if (receiver.getId().equals(reactor.getId())) {
39+
return;
40+
}
41+
if (!userRepository.existsByIdAndDeletedFalse(receiver.getId())) {
42+
return;
43+
}
44+
45+
notificationService.send(receiver.getId(), NotificationType.SEMOFEED_EMOJI, params);
46+
}
47+
}

src/main/java/com/semosan/api/domain/semofeed/service/SemoFeedEmojiService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.semosan.api.domain.semofeed.entity.SemoFeed;
77
import com.semosan.api.domain.semofeed.entity.SemoFeedEmoji;
88
import com.semosan.api.domain.semofeed.enums.SemoFeedEmojiType;
9+
import com.semosan.api.domain.semofeed.notification.service.SemoFeedNotificationService;
910
import com.semosan.api.domain.semofeed.repository.SemoFeedEmojiRepository;
1011
import com.semosan.api.domain.semofeed.repository.SemoFeedRepository;
1112
import com.semosan.api.domain.user.entity.User;
@@ -23,6 +24,7 @@ public class SemoFeedEmojiService {
2324

2425
private final SemoFeedEmojiRepository semoFeedEmojiRepository;
2526
private final SemoFeedRepository semoFeedRepository;
27+
private final SemoFeedNotificationService semoFeedNotificationService;
2628
private final UserReader userReader;
2729

2830
@Transactional
@@ -36,6 +38,10 @@ public SemoFeedEmojiToggleResponse toggleWithCount(
3638
User user = userReader.findActiveUserById(userId);
3739

3840
boolean reacted = toggle(semoFeed, user, emojiType);
41+
if (reacted) {
42+
// 새 반응 등록일 때만 작성자 알림을 발송합니다.
43+
semoFeedNotificationService.sendEmojiNotification(semoFeed, user, emojiType);
44+
}
3945
long count = semoFeedEmojiRepository.countBySemoFeedAndEmojiType(semoFeed, emojiType);
4046

4147
return new SemoFeedEmojiToggleResponse(emojiType, reacted, count);
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package com.semosan.api.domain.semofeed.notification.service;
2+
3+
import com.semosan.api.domain.notification.enums.NotificationType;
4+
import com.semosan.api.domain.notification.service.NotificationService;
5+
import com.semosan.api.domain.semofeed.entity.SemoFeed;
6+
import com.semosan.api.domain.semofeed.enums.SemoFeedEmojiType;
7+
import com.semosan.api.domain.user.entity.User;
8+
import com.semosan.api.domain.user.enums.user.DeviceType;
9+
import com.semosan.api.domain.user.repository.UserRepository;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.extension.ExtendWith;
12+
import org.mockito.InjectMocks;
13+
import org.mockito.Mock;
14+
import org.mockito.junit.jupiter.MockitoExtension;
15+
import org.springframework.test.util.ReflectionTestUtils;
16+
17+
import java.util.Map;
18+
19+
import static org.assertj.core.api.Assertions.assertThatCode;
20+
import static org.mockito.ArgumentMatchers.argThat;
21+
import static org.mockito.ArgumentMatchers.eq;
22+
import static org.mockito.Mockito.never;
23+
import static org.mockito.Mockito.verify;
24+
import static org.mockito.Mockito.when;
25+
26+
@ExtendWith(MockitoExtension.class)
27+
class SemoFeedNotificationServiceTest {
28+
29+
@Mock
30+
private NotificationService notificationService;
31+
32+
@Mock
33+
private UserRepository userRepository;
34+
35+
@InjectMocks
36+
private SemoFeedNotificationService semoFeedNotificationService;
37+
38+
@Test
39+
void semoFeedEmojiNotificationTypeRequiresNavigationPayload() {
40+
assertThatCode(() -> NotificationType.SEMOFEED_EMOJI.validate(Map.of(
41+
"actorId", 2L,
42+
"actorName", "reactor",
43+
"semoFeedId", 10L,
44+
"emojiType", "FIRE"
45+
))).doesNotThrowAnyException();
46+
}
47+
48+
@Test
49+
void sendEmojiNotificationSendsToSemoFeedAuthor() {
50+
User author = user(1L, "author");
51+
User reactor = user(2L, "reactor");
52+
SemoFeed semoFeed = semoFeed(10L, author);
53+
54+
when(userRepository.existsByIdAndDeletedFalse(1L)).thenReturn(true);
55+
56+
semoFeedNotificationService.sendEmojiNotification(semoFeed, reactor, SemoFeedEmojiType.FIRE);
57+
58+
verify(notificationService).send(
59+
eq(1L),
60+
eq(NotificationType.SEMOFEED_EMOJI),
61+
argThat(params -> params.get("actorId").equals(2L)
62+
&& params.get("actorName").equals("reactor")
63+
&& params.get("semoFeedId").equals(10L)
64+
&& params.get("emojiType").equals("FIRE"))
65+
);
66+
}
67+
68+
@Test
69+
void sendEmojiNotificationSkipsSelfReaction() {
70+
User author = user(1L, "author");
71+
SemoFeed semoFeed = semoFeed(10L, author);
72+
73+
semoFeedNotificationService.sendEmojiNotification(semoFeed, author, SemoFeedEmojiType.HEART);
74+
75+
verify(notificationService, never()).send(
76+
org.mockito.ArgumentMatchers.any(),
77+
org.mockito.ArgumentMatchers.any(),
78+
org.mockito.ArgumentMatchers.any()
79+
);
80+
}
81+
82+
@Test
83+
void sendEmojiNotificationSkipsInactiveAuthor() {
84+
User author = user(1L, "author");
85+
User reactor = user(2L, "reactor");
86+
SemoFeed semoFeed = semoFeed(10L, author);
87+
88+
when(userRepository.existsByIdAndDeletedFalse(1L)).thenReturn(false);
89+
90+
semoFeedNotificationService.sendEmojiNotification(semoFeed, reactor, SemoFeedEmojiType.LAUGH);
91+
92+
verify(notificationService, never()).send(
93+
org.mockito.ArgumentMatchers.any(),
94+
org.mockito.ArgumentMatchers.any(),
95+
org.mockito.ArgumentMatchers.any()
96+
);
97+
}
98+
99+
private User user(Long id, String nickname) {
100+
User user = User.createTestUser(nickname, DeviceType.IOS);
101+
ReflectionTestUtils.setField(user, "id", id);
102+
ReflectionTestUtils.setField(user, "nickname", nickname);
103+
return user;
104+
}
105+
106+
private SemoFeed semoFeed(Long id, User user) {
107+
SemoFeed semoFeed = SemoFeed.create(user, "https://example.com/semofeed.png");
108+
ReflectionTestUtils.setField(semoFeed, "id", id);
109+
return semoFeed;
110+
}
111+
}

src/test/java/com/semosan/api/domain/semofeed/service/SemoFeedEmojiServiceTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.semosan.api.domain.semofeed.entity.SemoFeed;
55
import com.semosan.api.domain.semofeed.entity.SemoFeedEmoji;
66
import com.semosan.api.domain.semofeed.enums.SemoFeedEmojiType;
7+
import com.semosan.api.domain.semofeed.notification.service.SemoFeedNotificationService;
78
import com.semosan.api.domain.semofeed.repository.SemoFeedEmojiRepository;
89
import com.semosan.api.domain.semofeed.repository.SemoFeedRepository;
910
import com.semosan.api.domain.user.entity.User;
@@ -20,6 +21,7 @@
2021

2122
import static org.assertj.core.api.Assertions.assertThat;
2223
import static org.mockito.ArgumentMatchers.any;
24+
import static org.mockito.Mockito.never;
2325
import static org.mockito.Mockito.verify;
2426
import static org.mockito.Mockito.when;
2527

@@ -32,6 +34,9 @@ class SemoFeedEmojiServiceTest {
3234
@Mock
3335
private SemoFeedRepository semoFeedRepository;
3436

37+
@Mock
38+
private SemoFeedNotificationService semoFeedNotificationService;
39+
3540
@Mock
3641
private UserReader userReader;
3742

@@ -63,6 +68,7 @@ void toggleWithCountCreatesEmojiReaction() {
6368
assertThat(result.reacted()).isTrue();
6469
assertThat(result.count()).isEqualTo(1L);
6570
verify(semoFeedEmojiRepository).save(any(SemoFeedEmoji.class));
71+
verify(semoFeedNotificationService).sendEmojiNotification(semoFeed, reactor, SemoFeedEmojiType.FIRE);
6672
}
6773

6874
@Test
@@ -90,6 +96,7 @@ void toggleWithCountDeletesExistingEmojiReaction() {
9096
assertThat(result.reacted()).isFalse();
9197
assertThat(result.count()).isZero();
9298
verify(semoFeedEmojiRepository).delete(existing);
99+
verify(semoFeedNotificationService, never()).sendEmojiNotification(any(), any(), any());
93100
}
94101

95102
private User user(Long id, String nickname) {

0 commit comments

Comments
 (0)