Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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,55 @@
package com.blackcompany.eeos.notification.application.scheduler;

import com.blackcompany.eeos.notification.application.repository.MemberPushTokenRepository;
import com.blackcompany.eeos.notification.application.service.SlackNotificationService;
import java.time.LocalDateTime;
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;
import org.springframework.transaction.annotation.Transactional;

@Component
@Slf4j
@RequiredArgsConstructor
public class PushTokenCleanScheduler {

private final MemberPushTokenRepository memberPushTokenRepository;
private final SlackNotificationService slackNotificationService;

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")
@Transactional
@Retryable(
maxAttempts = 3,
backoff = @Backoff(delay = 2000),
recover = "recoverDeleteInactiveTokens")
public void deleteInactiveTokens() {
log.info("{} 시작", SCHEDULER_NAME);
LocalDateTime limitDate = LocalDateTime.now().minusDays(INACTIVE_DAYS_THRESHOLD);
int deleteCount = memberPushTokenRepository.deleteByLastActiveAtBefore(limitDate);
log.info("{}개의 비활성 푸시 토큰 삭제 완료 (기준일: {})", deleteCount, limitDate);
}

@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
@@ -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}