📖 Adit 서비스 가이드에 오신 것을 환영합니다!
system
스케줄러

Adit 스케줄러(배경 작업) 시스템

최종 업데이트: 2026-05-31 (코드 기준 재검증: src/scheduler/*.scheduler.ts)


1. 개요

Adit 백엔드는 setInterval 기반의 클래스형 스케줄러를 사용해 배경 작업을 주기적으로 실행합니다.

  • 진입점: src/scheduler/index.tsstartSchedulers() / stopSchedulers() 함수가 모든 스케줄러 인스턴스를 관리합니다.
  • 서버 기동 시 자동 시작: src/server.ts에서 앱 초기화 완료 후 startSchedulers()를 호출합니다.
  • 중복 실행 방지: 각 스케줄러는 isRunning 플래그를 사용해 이전 실행이 끝나지 않으면 다음 tick을 건너뜁니다.
  • 즉시 1회 실행: 대부분의 스케줄러는 start() 시 인터벌 등록 전에 즉시 한 번 실행됩니다.
⚠️

주기(interval)는 코드 상수(INTERVAL_MS)로 하드코딩되어 있습니다. 런타임 환경 변수나 DB 설정으로 변경할 수 없습니다. 주기를 바꾸려면 해당 스케줄러 파일의 INTERVAL_MS 상수를 수정하고 재배포해야 합니다.


2. 스케줄러 목록

2.1 핵심 스케줄러 (ADIT 플랫폼)

파일주기실행 조건역할
instagram-sync.scheduler.ts6시간매 tick 실행연동된 파트너 채널의 미디어/인사이트 동기화
instagram-token-refresh.scheduler.ts12시간매 tick 실행만료 임박 인스타그램 장기 액세스 토큰 갱신
campaign-reminder.scheduler.ts1시간 (체크)KST 09:00, 하루 1회업로드 리마인더 이메일 + 연체 알림 Slack 발송
recurring-payment.scheduler.ts1시간 (체크)KST 00:00~01:00, 하루 1회광고주 구독 정기결제(Portone 빌링키) 처리
revoked-token-cleanup.scheduler.ts24시간매 tick 실행만료된 RevokedToken 레코드 DB 정리

2.2 CRM/리드 관련 스케줄러

별도 제품 기능에 속하는 스케줄러입니다. 동일한 index.ts에서 함께 시작됩니다.

파일주기역할
email-sequence.scheduler.ts5분이메일 드립 시퀀스 발송 (PitchrEmailSequenceLog 기반)
lead-inactivity.scheduler.ts1시간 (체크, KST 10:00)장기 미연락 리드 비활성 알림 (3일/7일 기준)

3. 핵심 스케줄러 상세

3.1 Instagram 미디어/인사이트 동기화

파일: src/scheduler/instagram-sync.scheduler.ts
주기: 6시간 (INTERVAL_MS = 6 * 60 * 60 * 1000)

Instagram API와 연동된 파트너 채널의 데이터를 주기적으로 최신화합니다. 두 단계로 나뉘어 실행됩니다.

단계호출설명
1instagramService.syncAllActiveConnections()프로필/미디어 기본 정보 동기화
2instagramService.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-syncinstagram-sync.scheduler.ts:12
instagram-token-refreshinstagram-token-refresh.scheduler.ts:12
campaign-remindercampaign-reminder.scheduler.ts:17
recurring-paymentrecurring-payment.scheduler.ts:14
revoked-token-cleanuprevoked-token-cleanup.scheduler.ts:12

5.2 KST 시간 기반 실행 스케줄러

campaign-reminderrecurring-payment는 매시간 tick을 체크하되 특정 KST 시각에만 실제 작업을 수행합니다. 서버 재기동 타이밍에 따라 해당 시각의 tick을 놓칠 수 있습니다. 두 스케줄러 모두 lastRunDate 또는 pre-claim 패턴으로 하루 1회 중복 실행을 방지합니다.

5.3 헬스체크

src/utils/scheduler-health.tsrecordSchedulerRun(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)