Skip to content

Commit 2a5e030

Browse files
authored
Feat: 어드민 기능- 전체 사용자 토픽 구독 API 추가 (#314)
* feat: User.getFcmToken getter 추가 * feat: 사용자 모든 구독 정보 가져오는 Repository Method 추가 * remove: 불필요 메서드 삭제 * remove: 모든 사용자 토픽 구독 처리 서비스 로직 추가 * remove: 모든 사용자 토픽 구독 처리 API추가 * remove: 모든 사용자 토픽 구독 처리 인수테스트 추가 * fix: 재구독 시 트랜잭션 어노테이션 제거 * fix: 재구독 성공/실패 개수 로깅 * feat: 학사일정 카테고리 컬럼 사이즈 30으로 변경
1 parent 485d77a commit 2a5e030

13 files changed

Lines changed: 135 additions & 34 deletions

File tree

src/main/java/com/kustacks/kuring/admin/adapter/in/web/AdminCommandApiV2.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import com.kustacks.kuring.common.utils.converter.StringToDateTimeConverter;
1818
import com.kustacks.kuring.common.utils.validator.BadWordInitProcessor;
1919
import com.kustacks.kuring.common.utils.validator.WhitelistWordInitProcessor;
20-
import io.swagger.v3.oas.annotations.Hidden;
2120
import io.swagger.v3.oas.annotations.Operation;
2221
import io.swagger.v3.oas.annotations.Parameter;
2322
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@@ -28,7 +27,6 @@
2827
import org.springframework.http.ResponseEntity;
2928
import org.springframework.validation.annotation.Validated;
3029
import org.springframework.web.bind.annotation.DeleteMapping;
31-
import org.springframework.web.bind.annotation.GetMapping;
3230
import org.springframework.web.bind.annotation.PathVariable;
3331
import org.springframework.web.bind.annotation.PostMapping;
3432
import org.springframework.web.bind.annotation.RequestBody;
@@ -136,11 +134,12 @@ public ResponseEntity<BaseResponse<String>> refreshWhitelistWords() {
136134
return ResponseEntity.ok().body(new BaseResponse<>(ResponseCodeAndMessages.ADMIN_LOAD_WHITELIST_WORDS, null));
137135
}
138136

139-
@Hidden
137+
@Operation(summary = "사용자 토픽 재구독", description = "모든 사용자의 구독 정보를 기반으로 Firebase 토픽을 재구독합니다")
138+
@SecurityRequirement(name = "JWT")
140139
@Secured(AdminRole.ROLE_ROOT)
141-
@GetMapping("/subscribe/all")
142-
public ResponseEntity<Void> subscribe() {
143-
adminCommandUseCase.subscribeAllUserSameTopic();
144-
return ResponseEntity.ok().build();
140+
@PostMapping("/users/subscriptions/all")
141+
public ResponseEntity<BaseResponse<String>> resubscribeAllUsersToTopics() {
142+
adminCommandUseCase.resubscribeAllUsersToTopics();
143+
return ResponseEntity.ok().body(new BaseResponse<>(ResponseCodeAndMessages.ADMIN_USER_SUBSCRIPTION_UPDATE_SUCCESS, null));
145144
}
146145
}

src/main/java/com/kustacks/kuring/admin/application/port/in/AdminCommandUseCase.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ public interface AdminCommandUseCase {
1111

1212
void createRealNoticeForAllUser(RealNotificationCommand command);
1313

14-
void subscribeAllUserSameTopic();
15-
1614
void addAlertSchedule(AlertCreateCommand command);
1715

1816
void cancelAlertSchedule(Long id);
1917

2018
void embeddingCustomData(DataEmbeddingCommand command);
19+
20+
void resubscribeAllUsersToTopics();
2121
}

src/main/java/com/kustacks/kuring/admin/application/port/out/AdminUserFeedbackPort.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@
44
import org.springframework.data.domain.Page;
55
import org.springframework.data.domain.Pageable;
66

7-
import java.util.List;
8-
97
public interface AdminUserFeedbackPort {
10-
List<String> findAllToken();
11-
128
Page<FeedbackDto> findAllFeedbackByPageRequest(Pageable pageable);
139
}

src/main/java/com/kustacks/kuring/admin/application/service/AdminCommandService.java

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
package com.kustacks.kuring.admin.application.service;
22

3-
import com.google.firebase.messaging.FirebaseMessaging;
43
import com.kustacks.kuring.admin.application.port.in.AdminCommandUseCase;
54
import com.kustacks.kuring.admin.application.port.in.dto.RealNotificationCommand;
65
import com.kustacks.kuring.admin.application.port.in.dto.TestNotificationCommand;
76
import com.kustacks.kuring.admin.application.port.out.AdminAlertEventPort;
87
import com.kustacks.kuring.admin.application.port.out.AdminEventPort;
9-
import com.kustacks.kuring.admin.application.port.out.AdminUserFeedbackPort;
108
import com.kustacks.kuring.admin.application.port.out.AiEventPort;
119
import com.kustacks.kuring.admin.domain.Admin;
1210
import com.kustacks.kuring.alert.application.port.in.dto.AlertCreateCommand;
1311
import com.kustacks.kuring.alert.application.port.in.dto.DataEmbeddingCommand;
1412
import com.kustacks.kuring.auth.userdetails.UserDetailsServicePort;
1513
import com.kustacks.kuring.common.annotation.UseCase;
1614
import com.kustacks.kuring.common.properties.ServerProperties;
15+
import com.kustacks.kuring.message.application.port.out.FirebaseSubscribePort;
1716
import com.kustacks.kuring.notice.domain.CategoryName;
17+
import com.kustacks.kuring.user.application.port.out.UserQueryPort;
18+
import com.kustacks.kuring.user.domain.User;
1819
import lombok.RequiredArgsConstructor;
1920
import lombok.extern.slf4j.Slf4j;
2021
import org.springframework.core.io.InputStreamResource;
@@ -26,8 +27,12 @@
2627
import java.io.IOException;
2728
import java.time.LocalDateTime;
2829
import java.time.format.DateTimeFormatter;
30+
import java.util.HashMap;
31+
import java.util.LinkedList;
2932
import java.util.List;
33+
import java.util.Map;
3034

35+
import static com.kustacks.kuring.message.application.service.FirebaseSubscribeService.ACADEMIC_EVENT_TOPIC;
3136
import static com.kustacks.kuring.message.application.service.FirebaseSubscribeService.ALL_DEVICE_SUBSCRIBED_TOPIC;
3237

3338
@Slf4j
@@ -36,10 +41,11 @@
3641
public class AdminCommandService implements AdminCommandUseCase {
3742

3843
private final UserDetailsServicePort userDetailsServicePort;
39-
private final AdminUserFeedbackPort adminUserFeedbackPort;
4044
private final AdminAlertEventPort adminAlertEventPort;
4145
private final AdminEventPort adminEventPort;
4246
private final AiEventPort aiEventPort;
47+
private final UserQueryPort userQueryPort;
48+
private final FirebaseSubscribePort firebaseSubscribePort;
4349
private final NoticeProperties noticeProperties;
4450
private final ServerProperties serverProperties;
4551
private final PasswordEncoder passwordEncoder;
@@ -107,21 +113,61 @@ public void embeddingCustomData(DataEmbeddingCommand command) {
107113
}
108114
}
109115

110-
/**
111-
* TODO : 1회성 API - client v2 배포 후, 단 한번 모든 사용자를 공통 topic에 구독시킨 후 제거 예정
112-
*/
113-
@Transactional
114116
@Override
115-
public void subscribeAllUserSameTopic() {
116-
String topic = serverProperties.ifDevThenAddSuffix(ALL_DEVICE_SUBSCRIBED_TOPIC);
117+
public void resubscribeAllUsersToTopics() {
118+
List<User> allUsers = userQueryPort.findAllWithSubscriptions();
119+
Map<String, List<String>> topicSubscriptions = new HashMap<>();
120+
121+
//User 당 (1 + 학사일정 알림 구독여부 + 유저별 카테고리 구독 수 + 유저별 학과 구독 수) 반복
122+
//User 당 최대 2 + 카테고리 수 + 학과 수 => 넉넉 잡아 80
123+
//ex. User 5000명 * 80 => 400,000번 반복
124+
for (User user : allUsers) {
125+
String fcmToken = user.getFcmToken();
126+
127+
// allDevice 토픽 - 모든 사용자
128+
String allDeviceTopic = serverProperties.ifDevThenAddSuffix(ALL_DEVICE_SUBSCRIBED_TOPIC);
129+
topicSubscriptions.computeIfAbsent(allDeviceTopic, k -> new LinkedList<>()).add(fcmToken);
130+
131+
// academicEvent 토픽 - 학사일정 알림이 활성화된 사용자
132+
if (user.isAcademicEventNotificationEnabled()) {
133+
String academicEventTopic = serverProperties.ifDevThenAddSuffix(ACADEMIC_EVENT_TOPIC);
134+
topicSubscriptions.computeIfAbsent(academicEventTopic, k -> new LinkedList<>()).add(fcmToken);
135+
}
136+
137+
// 카테고리별 토픽
138+
user.getSubscribedCategoryList().forEach(category -> {
139+
String categoryTopic = serverProperties.ifDevThenAddSuffix(category.getName());
140+
topicSubscriptions.computeIfAbsent(categoryTopic, k -> new LinkedList<>()).add(fcmToken);
141+
});
142+
143+
// 학과별 토픽
144+
user.getSubscribedDepartmentList().forEach(department -> {
145+
String departmentTopic = serverProperties.ifDevThenAddSuffix(department.getName());
146+
topicSubscriptions.computeIfAbsent(departmentTopic, k -> new LinkedList<>()).add(fcmToken);
147+
});
148+
}
149+
150+
// 토픽별로 500개씩 나누어 구독 처리
151+
for (Map.Entry<String, List<String>> entry : topicSubscriptions.entrySet()) {
152+
String topic = entry.getKey();
153+
List<String> tokens = entry.getValue();
154+
155+
log.info("Resubscribing {} users to topic: {}", tokens.size(), topic);
156+
157+
int successCount = 0;
158+
int failureCount = 0;
117159

118-
FirebaseMessaging instance = FirebaseMessaging.getInstance();
119-
List<String> allToken = adminUserFeedbackPort.findAllToken();
160+
for (int i = 0; i < tokens.size(); i += 500) {
161+
List<String> batch = tokens.subList(i, Math.min(i + 500, tokens.size()));
162+
try {
163+
firebaseSubscribePort.subscribeToTopic(batch, topic);
164+
successCount += batch.size();
165+
} catch (Exception e) {
166+
failureCount += batch.size();
167+
}
168+
}
120169

121-
int size = allToken.size();
122-
for (int i = 0; i < size; i += 500) {
123-
List<String> subList = allToken.subList(i, Math.min(i + 500, size));
124-
instance.subscribeToTopicAsync(subList, topic);
170+
log.info("Resubscribed {} users to topic: {}. {} users failed.", successCount, topic, failureCount);
125171
}
126172
}
127173

src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public enum ResponseCodeAndMessages {
2929
ADMIN_EMBEDDING_NOTICE_SUCCESS(HttpStatus.OK.value(), "데이터 임베딩에 생성에 성공하였습니다"),
3030
ADMIN_LOAD_BAD_WORDS(HttpStatus.OK.value(), "금칙어 로드에 성공했습니다."),
3131
ADMIN_LOAD_WHITELIST_WORDS(HttpStatus.OK.value(), "허용 단어 로드에 성공했습니다."),
32+
ADMIN_USER_SUBSCRIPTION_UPDATE_SUCCESS(HttpStatus.OK.value(), "사용자들의 구독 정보를 성공적으로 재설정했습니다."),
3233

3334
/* User */
3435
USER_REGISTER_SUCCESS(HttpStatus.OK.value(), "회원가입에 성공하였습니다"),

src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserPersistenceAdapter.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,6 @@ public Page<FeedbackDto> findAllFeedbackByPageRequest(Pageable pageable) {
3030
return userRepository.findAllFeedbackByPageRequest(pageable);
3131
}
3232

33-
@Override
34-
public List<String> findAllToken() {
35-
return userRepository.findAllFcmTokens();
36-
}
37-
3833
@Override
3934
public Optional<User> findByToken(String token) {
4035
if (token == null || token.isBlank()) {
@@ -67,6 +62,11 @@ public List<User> findAll() {
6762
return userRepository.findAll();
6863
}
6964

65+
@Override
66+
public List<User> findAllWithSubscriptions() {
67+
return userRepository.findAllWithSubscriptions();
68+
}
69+
7070
@Override
7171
public List<User> findByPageRequest(Pageable pageable) {
7272
return userRepository.findByPageRequest(pageable);

src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,7 @@ interface UserQueryRepository {
1313

1414
List<User> findByPageRequest(Pageable pageable);
1515

16+
List<User> findAllWithSubscriptions();
17+
1618
void resetAllUserQuestionCount();
1719
}

src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepositoryImpl.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ public List<User> findByPageRequest(Pageable pageable) {
4545
.fetch();
4646
}
4747

48+
@Override
49+
public List<User> findAllWithSubscriptions() {
50+
return queryFactory
51+
.selectFrom(user)
52+
.leftJoin(user.categories.categoryNamesSet).fetchJoin()
53+
.leftJoin(user.departments.departmentNamesSet).fetchJoin()
54+
.distinct()
55+
.fetch();
56+
}
57+
4858
@Transactional
4959
@Override
5060
public void resetAllUserQuestionCount() {

src/main/java/com/kustacks/kuring/user/application/port/out/UserQueryPort.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public interface UserQueryPort {
1111
Optional<User> findByToken(String token);
1212

1313
List<User> findAll();
14+
15+
List<User> findAllWithSubscriptions();
1416
List<User> findByPageRequest(Pageable pageable);
1517
List<User> findByLoggedInUserId(Long id);
1618

src/main/java/com/kustacks/kuring/user/domain/User.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public class User implements Serializable {
3535
@Column(name = "id", nullable = false)
3636
private Long id;
3737

38+
@Getter(AccessLevel.PUBLIC)
3839
@Column(name = "fcm_token", unique = true, nullable = false)
3940
private String fcmToken;
4041

0 commit comments

Comments
 (0)