국가별 영업일 발송 게이트

최적 설계 시퀀스 다이어그램 — enrollment 시점 SSOT 해소 + 발송시점 영업일 게이트

N0.0 SSOT 통합 N1.0 발송 게이트 N1.1 80k 재동기 N2.0 공휴일
── 흐름 SSOT country→IANA 단일 매핑 fail-open tz 미상 시 미지연 $0 무상태 산술, LLM/Redis Lua 불요
국가별 영업일 발송 게이트 — 최적 설계 시퀀스 (enrollment 해소 + 발송시점 게이트)Enrollmentcountry-timezone.tsWorkerdecideSendWindowbusiness-calendarSESLoaderEnrollmentcountry-timezone.tsPostgresBullMQWorkerdecideSendWindowbusiness-calendarSESLoader(producer)Loader(producer)EnrollmentServiceEnrollmentServicecountry-timezone.ts(SSOT, pure)country-timezone.ts(SSOT, pure)Postgres(leads / executions)Postgres(leads / executions)BullMQ(sequence-email)BullMQ(sequence-email)Workerpre-checkWorkerpre-checkdecideSendWindow(domain, pure)decideSendWindow(domain, pure)business-calendar(IANA+luxon+공휴일)business-calendar(IANA+luxon+공휴일)SESSESEnrollmentcountry-timezone.tsWorkerdecideSendWindowbusiness-calendarSESN0.0 / N0.1 · enrollment 시점 timezone 해소 (SSOT)bulkEnroll(leads, timezoneMode)resolveLeadTimezone(lead.timezone, country)cascade:lead.timezone|| countryToTimezone(country) ← 별칭 USA·미국·Türkiye|| "Asia/Seoul"IANA tz (예: America/New_York)INSERT step_executions(timezone=resolved, scheduled_at)addJobs(delay=scheduled_at, payload{timezone})N1.1 · 기존 buyer 80,274건 targeted 재동기 (1회)UPDATE execution.timezone = leads.timezoneWHERE buyer + pending + country기지(sender 988k 무영향, bounded)N1.0 / N2.0 · 발송시점 영업일 게이트 ($0, claim 전)job 발사 (payload.timezone)decideSendWindow(now, execution.timezone)businessCalendarFor(tz)IANA+luxon (DST 정확)workDays(걸프권 일~목)+ holidays set (N2.0){ workDays, hours, holidays }alt[주말 · 공휴일 · 업무외 시간]{ deferUntil: 다음 영업슬롯 }moveToDelayed(deferUntil)throw DelayedError (재예약)발송 보류 → 자동응답 최소화[영업일 · 업무시간 / tz 미상(fail-open)]{ sendNow: true }claim → resolve → verifysend()delivered
핵심 설계 요약