Adit 스케줄러(배경 작업) 시스템
최종 업데이트: 2026-05-31 (코드 기준 재검증: src/scheduler/*.scheduler.ts)
1. 개요
Adit 백엔드는 setInterval 기반의 클래스형 스케줄러를 사용해 배경 작업을 주기적으로 실행합니다.
- 진입점:
src/scheduler/index.ts—startSchedulers()/stopSchedulers()함수가 모든 스케줄러 인스턴스를 관리합니다. - 서버 기동 시 자동 시작:
src/server.ts에서 앱 초기화 완료 후startSchedulers()를 호출합니다. - 중복 실행 방지: 각 스케줄러는
isRunning플래그를 사용해 이전 실행이 끝나지 않으면 다음 tick을 건너뜁니다. - 즉시 1회 실행: 대부분의 스케줄러는
start()시 인터벌 등록 전에 즉시 한 번 실행됩니다.
주기(interval)는 코드 상수(INTERVAL_MS)로 하드코딩되어 있습니다. 런타임 환경 변수나 DB 설정으로 변경할 수 없습니다. 주기를 바꾸려면 해당 스케줄러 파일의 INTERVAL_MS 상수를 수정하고 재배포해야 합니다.
2. 스케줄러 목록
2.1 핵심 스케줄러 (ADIT 플랫폼)
| 파일 | 주기 | 실행 조건 | 역할 |
|---|---|---|---|
instagram-sync.scheduler.ts | 6시간 | 매 tick 실행 | 연동된 파트너 채널의 미디어/인사이트 동기화 |
instagram-token-refresh.scheduler.ts | 12시간 | 매 tick 실행 | 만료 임박 인스타그램 장기 액세스 토큰 갱신 |
campaign-reminder.scheduler.ts | 1시간 (체크) | KST 09:00, 하루 1회 | 업로드 리마인더 이메일 + 연체 알림 Slack 발송 |
recurring-payment.scheduler.ts | 1시간 (체크) | KST 00:00~01:00, 하루 1회 | 광고주 구독 정기결제(Portone 빌링키) 처리 |
revoked-token-cleanup.scheduler.ts | 24시간 | 매 tick 실행 | 만료된 RevokedToken 레코드 DB 정리 |
2.2 CRM/리드 관련 스케줄러
별도 제품 기능에 속하는 스케줄러입니다. 동일한 index.ts에서 함께 시작됩니다.
| 파일 | 주기 | 역할 |
|---|---|---|
email-sequence.scheduler.ts | 5분 | 이메일 드립 시퀀스 발송 (PitchrEmailSequenceLog 기반) |
lead-inactivity.scheduler.ts | 1시간 (체크, KST 10:00) | 장기 미연락 리드 비활성 알림 (3일/7일 기준) |
3. 핵심 스케줄러 상세
3.1 Instagram 미디어/인사이트 동기화
파일: src/scheduler/instagram-sync.scheduler.ts
주기: 6시간 (INTERVAL_MS = 6 * 60 * 60 * 1000)
Instagram API와 연동된 파트너 채널의 데이터를 주기적으로 최신화합니다. 두 단계로 나뉘어 실행됩니다.
| 단계 | 호출 | 설명 |
|---|---|---|
| 1 | instagramService.syncAllActiveConnections() | 프로필/미디어 기본 정보 동기화 |
| 2 | instagramService.syncAllInsightsForActiveConnections() | 미디어 인사이트(도달/노출/참여) 동기화 — 실패해도 1단계 결과 보존 |
- 동기화 중 발생한 채널별 오류는 로그에 기록되고 다음 채널로 진행합니다.
recordSchedulerRun('instagram-sync')호출로 헬스체크 기록을 남깁니다.
3.2 Instagram 액세스 토큰 갱신
파일: src/scheduler/instagram-token-refresh.scheduler.ts
주기: 12시간 (INTERVAL_MS = 12 * 60 * 60 * 1000)
Instagram 장기 액세스 토큰(Long-Lived Access Token)은 60일 유효합니다. 만료 임박 토큰을 사전에 자동 갱신해 채널 연동 단절을 방지합니다.
instagramService.refreshExpiringTokens()— 만료 임박 기준 필터링 후 Graph API로 갱신- 토큰은
AES-256-GCM으로 암호화해 DB에 저장됩니다 (src/utils/crypto.ts) - 갱신 실패 채널은 로그에
connectionId,username,error정보를 남깁니다
3.3 캠페인 업로드 리마인더 / 연체 알림
파일: src/scheduler/campaign-reminder.scheduler.ts
주기: 1시간 체크, KST 09:00에 하루 1회 실행 (lastRunDate로 중복 방지)
IN_PROGRESS 상태 캠페인의 uploadDate를 기준으로 두 가지 알림을 발송합니다.
대상: uploadDate가 오늘 또는 내일인 IN_PROGRESS 캠페인
- 이벤트 타입:
CAMPAIGN_UPLOAD_REMINDER - 채널: 파트너 이메일
- 발송 주체:
notificationService.sendCampaignNotification()
3.4 광고주 구독 정기결제
파일: src/scheduler/recurring-payment.scheduler.ts
주기: 1시간 체크, KST 00:00~01:00에 하루 1회 실행
광고주 구독(AdvertiserSubscription)의 nextBillingDate가 오늘인 활성 구독에 대해 Portone 빌링키 결제를 처리합니다.
멱등성 보장 (pre-claim 패턴):
1. nextBillingDate가 오늘인 ACTIVE 구독 조회
2. updateMany로 nextBillingDate를 내일로 임시 이동 (atomic pre-claim)
→ count=0이면 다른 인스턴스가 처리 중 → skip
3. processRecurringPayment() → Portone 결제 요청
4. 성공: 서비스 내부에서 nextBillingDate 정확한 다음 결제일로 재설정
5. 실패/예외: nextBillingDate 원복 (재시도 가능 상태 유지)
6. 타임아웃: 결제 미확정 → status = 'PAST_DUE' (재시도 안 함)파트너 구독(prisma.subscription) 정기결제는 현재 미구현(v2 예정)입니다. 현재는 광고주 구독(advertiserSubscription)만 처리합니다.
3.5 만료된 리프레시 토큰 정리
파일: src/scheduler/revoked-token-cleanup.scheduler.ts
주기: 24시간 (INTERVAL_MS = 24 * 60 * 60 * 1000)
RevokedToken 테이블에서 expiresAt < now인 레코드를 deleteMany로 일괄 삭제합니다. 로그아웃/토큰 갱신 시 폐기된 리프레시 토큰이 누적되는 것을 방지합니다.
4. 구조 패턴
모든 스케줄러는 동일한 클래스 패턴을 따릅니다.
class FooScheduler {
private intervalId: NodeJS.Timeout | null = null;
private isRunning = false;
private readonly INTERVAL_MS = N * 60 * 60 * 1000; // 주기 (하드코딩)
start() {
// 중복 시작 방지
if (this.intervalId) return;
this.runTask(); // 즉시 1회 실행
this.intervalId = setInterval(() => {
this.runTask();
}, this.INTERVAL_MS);
}
stop() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
private async runTask() {
if (this.isRunning) return; // 중복 실행 방지
this.isRunning = true;
try { /* ... */ }
finally { this.isRunning = false; }
}
async runNow() { await this.runTask(); } // 수동 실행 (테스트/디버그)
}runNow() 메서드는 테스트·디버그 목적으로 제공됩니다. isRunning 플래그 상태에 따라 실제 실행이 건너뛰어질 수 있습니다.
5. 운영 주의사항
5.1 주기 변경
각 스케줄러의 실행 주기는 파일 내 INTERVAL_MS 상수로만 제어됩니다. 환경 변수나 런타임 설정으로는 변경 불가합니다.
| 스케줄러 | 상수 위치 |
|---|---|
| instagram-sync | instagram-sync.scheduler.ts:12 |
| instagram-token-refresh | instagram-token-refresh.scheduler.ts:12 |
| campaign-reminder | campaign-reminder.scheduler.ts:17 |
| recurring-payment | recurring-payment.scheduler.ts:14 |
| revoked-token-cleanup | revoked-token-cleanup.scheduler.ts:12 |
5.2 KST 시간 기반 실행 스케줄러
campaign-reminder와 recurring-payment는 매시간 tick을 체크하되 특정 KST 시각에만 실제 작업을 수행합니다. 서버 재기동 타이밍에 따라 해당 시각의 tick을 놓칠 수 있습니다. 두 스케줄러 모두 lastRunDate 또는 pre-claim 패턴으로 하루 1회 중복 실행을 방지합니다.
5.3 헬스체크
src/utils/scheduler-health.ts의 recordSchedulerRun(name) 함수가 마지막 실행 시각을 기록합니다. Instagram 관련 스케줄러와 campaign-reminder, recurring-payment에서 사용합니다.
5.4 서버 재기동 시 동작
- 모든 스케줄러는
start()호출 즉시 첫 실행을 수행합니다 (단, KST 시간 기반 스케줄러는 조건 불충족 시 skip). - 재기동 빈도가 높으면 6시간/12시간 스케줄러가 예정보다 자주 실행될 수 있으나,
isRunning플래그로 동시 실행은 방지됩니다.
부록
A. 관련 파일 위치
adit-backend/
└── src/scheduler/
├── index.ts # startSchedulers() / stopSchedulers() 오케스트레이터
├── instagram-sync.scheduler.ts # 미디어/인사이트 동기화 (6h)
├── instagram-token-refresh.scheduler.ts # 토큰 갱신 (12h)
├── campaign-reminder.scheduler.ts # 업로드 리마인더/연체 알림 (1h, KST 09:00)
├── recurring-payment.scheduler.ts # 정기결제 (1h, KST 00:00)
├── revoked-token-cleanup.scheduler.ts # 만료 토큰 정리 (24h)
├── email-sequence.scheduler.ts # 이메일 드립 시퀀스 (5m)
└── lead-inactivity.scheduler.ts # 리드 비활성 알림 (1h, KST 10:00)