namespace Application.Helpers; /// /// DART(OpenDART list.json) 공시 수집의 페이징/백필 판단 순수 로직 (DB/HTTP 비의존 — 테스트 가능). /// KRX 배치(날짜별 fetch)와 달리 DART 는 접수일 범위 페이징이라 별도 규칙이 필요하다: /// • 스펙 제한: corp_code 미지정 검색은 최근 3개월 접수분만 제공 → 요청 창을 3개월로 클램프. /// • 5만 행 상한: page_count=100 × MaxPages=500. 초기 백필처럼 창 전체가 미적재면 주 단위로 쪼개 /// 창당 행 수를 낮춰 상한(정렬 desc 라 오래된 날짜부터 유실)을 회피한다. /// • 조기 종료: 일상 증분 수집에선 "한 페이지 전체가 기존 건"이면 그보다 오래된 페이지도 기수집으로 /// 간주하고 종료 — 단 1페이지에서는 종료하지 않는다 (같은 날 재실행/부분 적재 방어 보수 조건). /// public static class DartBackfill { /// corp_code 미지정 list.json 의 스펙상 조회 한도 — 최근 3개월 public const int SpecWindowMonths = 3; /// /// 요청 창 시작일을 스펙 한도(오늘-3개월)로 클램프. BackfillDays 가 90 초과로 설정돼도 요청 창이 3개월을 넘지 않는다. /// /// 클램프된 시작일과 클램프 발생 여부 (발생 시 호출부가 경고 로그) public static (DateOnly Begin, bool Clamped) ClampToSpecWindow(DateOnly begin, DateOnly today) { var floor = today.AddMonths(-SpecWindowMonths); return begin < floor ? (floor, true) : (begin, false); } /// /// [begin, end] 포함 범위를 7일 단위 (bgn_de, end_de) 청크로 분할 — 오래된 쪽부터 순서대로. /// begin > end 면 빈 목록. 마지막 청크는 end 에서 잘린다. /// public static IReadOnlyList<(DateOnly Begin, DateOnly End)> SplitWeekly(DateOnly begin, DateOnly end) { var chunks = new List<(DateOnly Begin, DateOnly End)>(); for (var chunkBegin = begin; chunkBegin <= end; chunkBegin = chunkBegin.AddDays(7)) { var chunkEnd = chunkBegin.AddDays(6); if (chunkEnd > end) { chunkEnd = end; } chunks.Add((chunkBegin, chunkEnd)); } return chunks; } /// /// 증분 수집 조기 종료 판정 — 해당 페이지의 모든 행이 이미 DB 에 존재하고(신규 0), 1페이지가 아닐 때만 true. /// 1페이지 제외 이유: 같은 날 재실행이면 1페이지도 전부 기존이지만, 과거에 부분 적재된 창일 가능성을 /// 배제할 수 없어 최소 2페이지까지는 확인한다 (비용: 페이지 1콜). /// public static bool ShouldStopEarly(int pageNo, int rowCount, int newCount) { return pageNo > 1 && rowCount > 0 && newCount == 0; } }