namespace Application.Helpers; /// /// KRX 일별 수집 배치의 공용 백필 오케스트레이션 (순수 로직, DB 비의존 — 테스트 가능). /// endDate 에서 startDate 방향(과거)으로 영업일을 훑으며, 아직 적재되지 않은 날만 수집한다: /// • 주말/휴장일(holidays) 은 건너뛴다. /// • existsForDate(day) == true (이미 적재됨) 이면 건너뛴다 → resumable. /// • 그 외에는 fetchAndUpsertForDate(day) 를 호출하고, quota 보호를 위해 호출 사이에 delayMs 만큼 쉰다. /// • fetch 횟수가 maxPerRun 에 도달하면 즉시 멈춘다 → 하루 배치가 quota 상한 안에서 조금씩 채운다. /// 최신일부터 채우므로(recent-first) 여러 번의 일일 실행에 걸쳐 3년치를 메우고 이후엔 최신만 유지한다. /// 반환값은 이번 실행에서 실제 fetch 한 날 수. /// public static class KrxBackfill { /// 해당 날짜가 이미 적재되어 있는지 (true = skip) /// 해당 날짜를 수집·upsert (부작용 담당) /// 백필 하한(포함) — 보통 today.AddYears(-BackfillYears) /// 백필 상한(포함) — 보통 직전 영업일. 여기서부터 과거로 훑는다 /// 휴장일 집합 (주말은 자동 제외) /// 이번 실행 최대 fetch 수 (quota 상한). 0 이하면 fetch 안 함 /// fetch 호출 사이 지연(ms). 0 이하면 지연 없음 public static async Task RunAsync( Func> existsForDate, Func fetchAndUpsertForDate, DateOnly startDate, DateOnly endDate, IReadOnlySet holidays, int maxPerRun, int delayMs, CancellationToken ct) { if (maxPerRun <= 0) { return 0; } var fetched = 0; for (var day = endDate; day >= startDate; day = day.AddDays(-1)) { ct.ThrowIfCancellationRequested(); if (day.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday || holidays.Contains(day)) { continue; } if (await existsForDate(day, ct)) { continue; } if (fetched > 0 && delayMs > 0) { await Task.Delay(delayMs, ct); } await fetchAndUpsertForDate(day, ct); fetched++; if (fetched >= maxPerRun) { break; } } return fetched; } }