Skip to content

Commit 33be8c3

Browse files
authored
Merge pull request #280 from SWM16-ASAP/develop
Release: v2.6.3
2 parents 8689056 + acda20a commit 33be8c3

11 files changed

Lines changed: 507 additions & 129 deletions

File tree

.github/workflows/dev-deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Dev Deployment
22

33
on:
44
push:
5-
branches: [ develop, feat/streak ]
5+
branches: [ develop, fix/fcm-multi-404 ]
66

77
jobs:
88
build:

build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ springBoot {
1010
}
1111

1212
group = 'com.linglevel'
13-
version = '2.6.2-SNAPSHOT'
13+
version = '2.6.3-SNAPSHOT'
1414

1515
java {
1616
toolchain {
@@ -43,7 +43,7 @@ dependencies {
4343
exclude group: 'io.swagger.core.v3', module: 'swagger-annotations'
4444
}
4545
implementation 'software.amazon.awssdk:s3:2.31.78'
46-
implementation 'com.google.firebase:firebase-admin:8.1.0'
46+
implementation 'com.google.firebase:firebase-admin:9.7.0'
4747
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
4848
implementation 'com.sksamuel.scrimage:scrimage-core:4.3.5'
4949
implementation 'com.sksamuel.scrimage:scrimage-webp:4.3.5'

src/main/java/com/linglevel/api/admin/controller/PushCampaignController.java

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.linglevel.api.fcm.dto.PushCampaignSummary;
55
import com.linglevel.api.fcm.service.PushCampaignService;
66
import io.swagger.v3.oas.annotations.Operation;
7+
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
78
import io.swagger.v3.oas.annotations.tags.Tag;
89
import lombok.RequiredArgsConstructor;
910
import lombok.extern.slf4j.Slf4j;
@@ -19,14 +20,15 @@
1920
@RequiredArgsConstructor
2021
@Slf4j
2122
@Tag(name = "Admin - Push Campaigns", description = "어드민 전용 푸시 캠페인 통계 API")
23+
@SecurityRequirement(name = "adminApiKey")
2224
public class PushCampaignController {
2325

2426
private final PushCampaignService pushCampaignService;
2527

2628
@GetMapping
2729
@Operation(
28-
summary = "캠페인 목록 조회",
29-
description = "푸시 캠페인 목록을 조회합니다. 기간 필터링이 가능합니다."
30+
summary = "캠페인 그룹 목록 조회",
31+
description = "푸시 캠페인 그룹 목록을 조회합니다. 기간 필터링이 가능합니다."
3032
)
3133
public ResponseEntity<List<PushCampaignSummary>> getCampaigns(
3234
@RequestParam(required = false)
@@ -37,22 +39,22 @@ public ResponseEntity<List<PushCampaignSummary>> getCampaigns(
3739
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
3840
LocalDateTime endDate) {
3941

40-
log.debug("Get campaigns request - startDate: {}, endDate: {}", startDate, endDate);
42+
log.debug("Get campaign groups request - startDate: {}, endDate: {}", startDate, endDate);
4143

4244
List<PushCampaignSummary> summaries = pushCampaignService.getCampaignSummaries(startDate, endDate);
4345

4446
return ResponseEntity.ok(summaries);
4547
}
4648

47-
@GetMapping("/{campaignId}/stats")
49+
@GetMapping("/{campaignGroup}/stats")
4850
@Operation(
49-
summary = "캠페인 상세 통계 조회",
50-
description = "특정 캠페인의 상세 통계를 조회합니다."
51+
summary = "캠페인 그룹 상세 통계 조회",
52+
description = "특정 캠페인 그룹의 상세 통계를 조회합니다."
5153
)
52-
public ResponseEntity<PushCampaignStats> getCampaignStats(@PathVariable String campaignId) {
53-
log.debug("Get campaign stats request - campaignId: {}", campaignId);
54+
public ResponseEntity<PushCampaignStats> getCampaignStats(@PathVariable String campaignGroup) {
55+
log.debug("Get campaign group stats request - campaignGroup: {}", campaignGroup);
5456

55-
PushCampaignStats stats = pushCampaignService.getStats(campaignId);
57+
PushCampaignStats stats = pushCampaignService.getStats(campaignGroup);
5658

5759
return ResponseEntity.ok(stats);
5860
}

src/main/java/com/linglevel/api/fcm/entity/PushLog.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,13 @@ public class PushLog {
2020
@Id
2121
private String id;
2222

23+
@Indexed(unique = true)
24+
private String campaignId; // 각 메시지의 고유 ID (자체 UUID)
25+
26+
private String fcmMessageId; // FCM messageId (선택적, FCM 추적용)
27+
2328
@Indexed
24-
private String campaignId;
29+
private String campaignGroup; // 내부 그룹화용 (선택적)
2530

2631
@Indexed
2732
private String userId;

src/main/java/com/linglevel/api/fcm/repository/PushLogRepository.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
@Repository
1212
public interface PushLogRepository extends MongoRepository<PushLog, String> {
1313

14-
List<PushLog> findByCampaignId(String campaignId);
14+
Optional<PushLog> findByCampaignId(String campaignId);
1515

16-
List<PushLog> findByUserId(String userId);
16+
List<PushLog> findByCampaignGroup(String campaignGroup);
1717

18-
Optional<PushLog> findByCampaignIdAndUserId(String campaignId, String userId);
18+
List<PushLog> findByUserId(String userId);
1919

2020
List<PushLog> findBySentAtBetween(LocalDateTime startDate, LocalDateTime endDate);
2121
}

src/main/java/com/linglevel/api/fcm/service/FcmMessagingService.java

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.util.List;
1818
import java.util.Map;
1919
import java.util.Optional;
20+
import java.util.UUID;
2021
import java.util.stream.Collectors;
2122

2223
@Service
@@ -36,10 +37,12 @@ public class FcmMessagingService {
3637
*/
3738
public String sendMessage(String fcmToken, FcmMessageRequest messageRequest) {
3839
String userId = getUserIdFromToken(fcmToken);
39-
String campaignId = messageRequest.getCampaignId();
40+
String campaignGroup = messageRequest.getCampaignId(); // 원래의 campaignId를 그룹으로 사용
41+
String pushId = UUID.randomUUID().toString();
4042

4143
try {
4244
Map<String, String> data = buildDataWithUserId(messageRequest, userId);
45+
data.put("pushId", pushId);
4346

4447
Message.Builder messageBuilder = Message.builder()
4548
.setToken(fcmToken)
@@ -50,29 +53,27 @@ public String sendMessage(String fcmToken, FcmMessageRequest messageRequest) {
5053
.putAllData(data);
5154

5255
// Google Analytics 추적을 위한 FcmOptions 설정
53-
if (campaignId != null) {
54-
String analyticsLabel = ANALYTICS_LABEL_PREFIX + campaignId;
56+
if (campaignGroup != null) {
57+
String analyticsLabel = ANALYTICS_LABEL_PREFIX + campaignGroup;
5558
messageBuilder.setFcmOptions(FcmOptions.withAnalyticsLabel(analyticsLabel));
5659
log.debug("Analytics label set: {}", analyticsLabel);
5760
}
5861

5962
Message message = messageBuilder.build();
60-
String response = firebaseMessaging.send(message);
61-
log.debug("FCM message sent successfully: {}", response);
63+
String fcmMessageId = firebaseMessaging.send(message);
64+
log.debug("FCM message sent successfully - pushId: {}, fcmMessageId: {}", pushId, fcmMessageId);
6265

63-
// 송신 성공 로그 저장
64-
if (campaignId != null && userId != null) {
65-
pushLogService.logSent(campaignId, userId, true);
66+
if (userId != null) {
67+
pushLogService.logSent(pushId, userId, true, campaignGroup, fcmMessageId);
6668
}
6769

68-
return response;
70+
return pushId; // 자체 UUID 반환
6971

7072
} catch (FirebaseMessagingException e) {
71-
log.error("Failed to send FCM message: {}", e.getMessage());
73+
log.error("Failed to send FCM message - pushId: {}", pushId, e);
7274

73-
// 송신 실패 로그 저장
74-
if (campaignId != null && userId != null) {
75-
pushLogService.logSent(campaignId, userId, false);
75+
if (userId != null) {
76+
pushLogService.logSent(pushId, userId, false, campaignGroup, null);
7677
}
7778

7879
throw new FcmException(FcmErrorCode.MESSAGE_SEND_FAILED);
@@ -83,7 +84,7 @@ public String sendMessage(String fcmToken, FcmMessageRequest messageRequest) {
8384
* 여러 사용자에게 동시 알림 전송 (각 토큰마다 userId 포함)
8485
*/
8586
public BatchResponse sendMulticastMessage(List<String> fcmTokens, FcmMessageRequest messageRequest) {
86-
String campaignId = messageRequest.getCampaignId();
87+
String campaignGroup = messageRequest.getCampaignId(); // 원래의 campaignId를 그룹으로 사용
8788

8889
try {
8990
if (fcmTokens == null || fcmTokens.isEmpty()) {
@@ -93,18 +94,24 @@ public BatchResponse sendMulticastMessage(List<String> fcmTokens, FcmMessageRequ
9394
// 각 토큰마다 userId를 포함한 개별 메시지 생성
9495
Map<String, String> tokenToUserId = getTokenToUserIdMap(fcmTokens);
9596
List<Message> messages = new ArrayList<>();
97+
Map<Integer, String> indexToPushId = new java.util.HashMap<>(); // 인덱스별 pushId 매핑
9698

9799
// Google Analytics 추적을 위한 FcmOptions 설정
98100
String analyticsLabel = null;
99101
FcmOptions fcmOptions = null;
100-
if (campaignId != null) {
101-
analyticsLabel = ANALYTICS_LABEL_PREFIX + campaignId;
102+
if (campaignGroup != null) {
103+
analyticsLabel = ANALYTICS_LABEL_PREFIX + campaignGroup;
102104
fcmOptions = FcmOptions.withAnalyticsLabel(analyticsLabel);
103105
}
104106

107+
int index = 0;
105108
for (String fcmToken : fcmTokens) {
106109
String userId = tokenToUserId.get(fcmToken);
110+
String pushId = UUID.randomUUID().toString();
111+
indexToPushId.put(index, pushId);
112+
107113
Map<String, String> data = buildDataWithUserId(messageRequest, userId);
114+
data.put("pushId", pushId);
108115

109116
Message.Builder messageBuilder = Message.builder()
110117
.setToken(fcmToken)
@@ -119,27 +126,23 @@ public BatchResponse sendMulticastMessage(List<String> fcmTokens, FcmMessageRequ
119126
}
120127

121128
messages.add(messageBuilder.build());
129+
index++;
122130
}
123131

124-
BatchResponse response = firebaseMessaging.sendAll(messages);
132+
BatchResponse response = firebaseMessaging.sendEach(messages);
125133
log.info("Batch messages sent to {} tokens - Success: {}, Failed: {} (Analytics: {})",
126134
fcmTokens.size(), response.getSuccessCount(), response.getFailureCount(),
127135
analyticsLabel != null ? analyticsLabel : "N/A");
128136

129-
// 배치로 로그 저장
130-
if (campaignId != null) {
131-
savePushLogsBatch(fcmTokens, campaignId, response);
132-
}
137+
// 배치로 로그 저장 (자체 UUID와 FCM messageId 함께 저장)
138+
savePushLogsBatch(fcmTokens, campaignGroup, response, indexToPushId);
133139

134140
return response;
135141

136142
} catch (FirebaseMessagingException e) {
137143
log.error("Failed to send multicast FCM message: {}", e.getMessage());
138144

139-
// 전체 실패 로그 배치 저장
140-
if (campaignId != null) {
141-
savePushLogsAllFailed(fcmTokens, campaignId);
142-
}
145+
savePushLogsAllFailed(fcmTokens, campaignGroup);
143146

144147
throw new FcmException(FcmErrorCode.MESSAGE_SEND_FAILED);
145148
}
@@ -148,7 +151,7 @@ public BatchResponse sendMulticastMessage(List<String> fcmTokens, FcmMessageRequ
148151
/**
149152
* 푸시 로그를 배치로 저장 (성공/실패 혼합)
150153
*/
151-
private void savePushLogsBatch(List<String> fcmTokens, String campaignId, BatchResponse response) {
154+
private void savePushLogsBatch(List<String> fcmTokens, String campaignGroup, BatchResponse response, Map<Integer, String> indexToPushId) {
152155
try {
153156
Map<String, String> tokenToUserId = getTokenToUserIdMap(fcmTokens);
154157
List<PushLog> logsToSave = new ArrayList<>();
@@ -157,23 +160,26 @@ private void savePushLogsBatch(List<String> fcmTokens, String campaignId, BatchR
157160
for (int i = 0; i < response.getResponses().size(); i++) {
158161
String fcmToken = fcmTokens.get(i);
159162
String userId = tokenToUserId.get(fcmToken);
160-
boolean success = response.getResponses().get(i).isSuccessful();
163+
SendResponse sendResponse = response.getResponses().get(i);
164+
boolean success = sendResponse.isSuccessful();
165+
String pushId = indexToPushId.get(i); // 미리 생성된 UUID
161166

162-
if (userId != null) {
163-
logsToSave.add(createPushLog(campaignId, userId, success, now));
167+
if (userId != null && pushId != null) {
168+
String fcmMessageId = success ? sendResponse.getMessageId() : null;
169+
logsToSave.add(createPushLog(pushId, userId, success, campaignGroup, fcmMessageId, now));
164170
}
165171
}
166172

167-
savePushLogsIfNotEmpty(logsToSave, campaignId);
173+
savePushLogsIfNotEmpty(logsToSave, campaignGroup);
168174
} catch (Exception e) {
169-
log.error("Failed to batch save push logs for campaign: {}", campaignId, e);
175+
log.error("Failed to batch save push logs for campaignGroup: {}", campaignGroup, e);
170176
}
171177
}
172178

173179
/**
174180
* 전체 실패 시 푸시 로그를 배치로 저장
175181
*/
176-
private void savePushLogsAllFailed(List<String> fcmTokens, String campaignId) {
182+
private void savePushLogsAllFailed(List<String> fcmTokens, String campaignGroup) {
177183
try {
178184
Map<String, String> tokenToUserId = getTokenToUserIdMap(fcmTokens);
179185
List<PushLog> logsToSave = new ArrayList<>();
@@ -182,13 +188,14 @@ private void savePushLogsAllFailed(List<String> fcmTokens, String campaignId) {
182188
for (String fcmToken : fcmTokens) {
183189
String userId = tokenToUserId.get(fcmToken);
184190
if (userId != null) {
185-
logsToSave.add(createPushLog(campaignId, userId, false, now));
191+
String pushId = UUID.randomUUID().toString();
192+
logsToSave.add(createPushLog(pushId, userId, false, campaignGroup, null, now));
186193
}
187194
}
188195

189-
savePushLogsIfNotEmpty(logsToSave, campaignId);
196+
savePushLogsIfNotEmpty(logsToSave, campaignGroup);
190197
} catch (Exception e) {
191-
log.error("Failed to batch save failed push logs for campaign: {}", campaignId, e);
198+
log.error("Failed to batch save failed push logs for campaignGroup: {}", campaignGroup, e);
192199
}
193200
}
194201

@@ -208,9 +215,11 @@ private Map<String, String> getTokenToUserIdMap(List<String> fcmTokens) {
208215
/**
209216
* PushLog 객체 생성
210217
*/
211-
private PushLog createPushLog(String campaignId, String userId, boolean success, LocalDateTime now) {
218+
private PushLog createPushLog(String pushId, String userId, boolean success, String campaignGroup, String fcmMessageId, LocalDateTime now) {
212219
return PushLog.builder()
213-
.campaignId(campaignId)
220+
.campaignId(pushId) // 자체 UUID를 campaignId로 사용
221+
.fcmMessageId(fcmMessageId) // FCM messageId (선택적)
222+
.campaignGroup(campaignGroup) // 캠페인 그룹
214223
.userId(userId)
215224
.sentAt(now)
216225
.sentSuccess(success)
@@ -221,10 +230,10 @@ private PushLog createPushLog(String campaignId, String userId, boolean success,
221230
/**
222231
* PushLog 목록이 비어있지 않으면 배치 저장
223232
*/
224-
private void savePushLogsIfNotEmpty(List<PushLog> logsToSave, String campaignId) {
233+
private void savePushLogsIfNotEmpty(List<PushLog> logsToSave, String campaignGroup) {
225234
if (!logsToSave.isEmpty()) {
226235
pushLogRepository.saveAll(logsToSave);
227-
log.debug("Batch saved {} push logs for campaign: {}", logsToSave.size(), campaignId);
236+
log.debug("Batch saved {} push logs for campaignGroup: {}", logsToSave.size(), campaignGroup);
228237
}
229238
}
230239

0 commit comments

Comments
 (0)