Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions eeos/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ dependencies {
// Firebase Admin SDK
implementation(libs.firebase.admin)

implementation(libs.spring.retry)
implementation(libs.spring.aspects)



}

dependencyManagement {
Expand Down
9 changes: 9 additions & 0 deletions eeos/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ spring-security-test="6.5.2"
# Database
flyway = "10.7.1"

# Retry
spring-retry = "2.0.5"


[libraries]
# Spring Boot
spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" }
Expand Down Expand Up @@ -61,6 +65,11 @@ lombok = { group = "org.projectlombok", name = "lombok" }
# Firebase Admin SDK
firebase-admin = { group = "com.google.firebase", name = "firebase-admin", version.ref = "firebase-admin" }

# Retry
spring-retry = { group = "org.springframework.retry", name = "spring-retry", version.ref = "spring-retry" }
spring-aspects = { group = "org.springframework", name = "spring-aspects" }


[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }
spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "spring-dependency-management" }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.blackcompany.eeos.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;

@Configuration
@EnableRetry
public class RetryConfig {}
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ List<MemberPushTokenModel> findByMemberIdAndProvider(
int deleteByUpdatedDateBefore(LocalDateTime updatedDate);

MemberPushTokenModel save(MemberPushTokenModel memberPushToken);

int deleteByLastActiveAtBefore(LocalDateTime limitDate);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.blackcompany.eeos.notification.application.scheduler;

import com.blackcompany.eeos.notification.application.service.NotificationTokenService;
import com.blackcompany.eeos.notification.application.service.SlackNotificationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
@Slf4j
@RequiredArgsConstructor
public class PushTokenCleanScheduler {

private final SlackNotificationService slackNotificationService;
private final NotificationTokenService notificationTokenService;

private static final int INACTIVE_DAYS_THRESHOLD = 90;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

사용되지 않는 상수 제거

INACTIVE_DAYS_THRESHOLD 상수가 이 클래스에서 사용되지 않습니다. 실제 삭제 로직은 NotificationTokenService.deleteInactiveTokens()에서 자체 상수를 사용하고 있습니다.

🔧 제안된 수정
-	private static final int INACTIVE_DAYS_THRESHOLD = 90;
 	private static final String SCHEDULER_NAME = "비활성화 토큰 삭제 스케줄러";
🤖 Prompt for AI Agents
In
`@eeos/src/main/java/com/blackcompany/eeos/notification/application/scheduler/PushTokenCleanScheduler.java`
at line 21, PushTokenCleanScheduler contains an unused constant
INACTIVE_DAYS_THRESHOLD — remove the declaration of INACTIVE_DAYS_THRESHOLD from
PushTokenCleanScheduler and rely on the existing deletion logic in
NotificationTokenService.deleteInactiveTokens(); ensure no other code in
PushTokenCleanScheduler references INACTIVE_DAYS_THRESHOLD and remove any
now-unneeded imports or comments related to that constant.

private static final String SCHEDULER_NAME = "비활성화 토큰 삭제 스케줄러";

/*
* 매주 토요일 새벽 3시 90일 이상 비활성화된 푸시 토큰 삭제
* cron : 초 분 시 일 월 요일(6=토요일)
* */

@Scheduled(cron = "0 0 3 * * 6")
@Retryable(
maxAttempts = 3,
backoff = @Backoff(delay = 2000),
recover = "recoverDeleteInactiveTokens")
public void deleteInactiveTokens() {
log.info("{} 시작", SCHEDULER_NAME);
int deleteCount = notificationTokenService.deleteInactiveTokens();
log.info("{}개의 비활성화 토큰 삭제 완료", deleteCount);
}

@Recover
public void recoverDeleteInactiveTokens(Exception e) {
log.error("{} 실행 실패 - 모든 재시도 소진", SCHEDULER_NAME, e);

String errorMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();

if (errorMessage.length() > 300) {
errorMessage = errorMessage.substring(0, 300) + "...생략";
}
slackNotificationService.sendSchedulerFailureMessage(SCHEDULER_NAME, errorMessage);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class NotificationTokenService
DeleteMemberPushTokenUsecase {

private final MemberPushTokenRepository memberPushTokenRepository;
private static final int INACTIVE_DAYS_THRESHOLD = 90;

@Override
@Transactional
Expand Down Expand Up @@ -64,4 +65,10 @@ public void delete(Long memberId, DeleteMemberPushTokenRequest request) {
}
memberPushTokenRepository.deleteByPushToken(request.getPushToken());
}

@Transactional
public int deleteInactiveTokens() {
LocalDateTime limitDate = LocalDateTime.now().minusDays(INACTIVE_DAYS_THRESHOLD);
return memberPushTokenRepository.deleteByLastActiveAtBefore(limitDate);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.blackcompany.eeos.notification.application.service;

import com.blackcompany.eeos.program.infra.api.slack.chat.client.SlackChatApiClient;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class SlackNotificationService {

private final SlackChatApiClient slackChatApiClient;
private final ObjectMapper objectMapper;

@Value("${slack.bot.black-company.eeos}")
private String botToken;

@Value("${slack.channel.black-company.error-report}")
private String errorReportChannel;

public void sendSchedulerFailureMessage(String schedulerName, String errorMessage) {
String message =
String.format(
":rotating_light: *스케줄러 실행 실패 알림*\n\n"
+ "*Scheduler*\n `%s`\n\n"
+ "*Failed At*\n`%s`\n\n"
+ "*Error Message*\n`%s`\n\n",
schedulerName,
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
errorMessage);
sendMessage(message);
}

private void sendMessage(String text) {
try {
String blocks =
objectMapper.writeValueAsString(
List.of(Map.of("type", "section", "text", Map.of("type", "mrkdwn", "text", text))));

slackChatApiClient.post(
"Bearer " + botToken, errorReportChannel, blocks, "EEOS Scheduler Bot");

} catch (Exception e) {
log.error("Slack 알림 전송 실패", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.blackcompany.eeos.notification.application.model.NotificationProvider;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
Expand All @@ -27,4 +28,8 @@ List<MemberPushTokenEntity> findByMemberIdAndProvider(
void deleteByPushToken(String token);

int deleteByUpdatedDateBefore(Timestamp updatedDate);

@Modifying
@Query("DELETE FROM MemberPushTokenEntity t where t.lastActiveAt < :limitDate")
int deleteByLastActiveAtBefore(@Param("limitDate") LocalDateTime limitDate);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@ToString
@SuperBuilder(toBuilder = true)
@Table(name = "member_push_token")
@Table(name = "Notification_token")
public class MemberPushTokenEntity extends BaseEntity {

public static final String ENTITY_PREFIX = "member_push_token";
public static final String ENTITY_PREFIX = "Notification_token";

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,9 @@ public MemberPushTokenModel save(MemberPushTokenModel memberPushToken) {
MemberPushTokenEntity saved = jpaRepository.save(memberPushTokenEntity);
return converter.from(saved);
}

@Override
public int deleteByLastActiveAtBefore(LocalDateTime limitDate) {
return jpaRepository.deleteByLastActiveAtBefore(limitDate);
}
}
2 changes: 2 additions & 0 deletions eeos/src/main/resources/application-slack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ slack:
black-company:
slack-message-test: ${BLACK_COMPANY_TEST_CHANNEL_ID}
notification : ${BLACK_COMPANY_NOTIFICATION_CHANNEL_ID}
error-report : ${BLACK_COMPANY_ERROR_REPORT_CHANNEL_ID}

econovation:
notification: ${ECONOVATION_NOTIFICATION_CHANNEL_ID}
small_talk: ${ECONOVATION_SMALL_TALK_CHANNEL_ID}
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,3 @@ CREATE TABLE `restrict_team_building` (