using Application.Abstractions.Data; using Domain.Entities.Stocks; using Microsoft.EntityFrameworkCore; namespace Infrastructure.StockData; /// /// SEIBro 수집 마커(SeibroFetchMarker rolling · SeibroSweptDate date-sweep) 조회/upsert 헬퍼 — 리뷰 결함 #1 해소 배선의 공용 경로. /// 각 SyncService 가 완료 판정을 "데이터 행 존재"에서 "조회 시도(0행 포함) 마커 존재"로 바꾸는 데 쓴다. /// • rolling: LoadFetchMarkers 로 (키→마지막조회시각) 사전을 만들어 rolling 대상을 정렬(미조회 최우선→오래된 순), fetch 후 TouchFetchMarker. /// • date-sweep: IsSwept 로 "데이터 존재 OR 마커 존재" 판정(기존 적재분 재조회도 회피), fetch(0행 포함) 후 MarkSwept. /// EntityKey 는 문자열 — 복합키는 배치가 구분자로 결합해 전달한다. JobKey 는 job 상수. /// public static class SeibroMarkers { /// 해당 job 의 (EntityKey → LastCheckedAt) 사전 로드 — rolling 대상 정렬 기준. public static async Task> LoadFetchMarkersAsync(IAppDbContext db, string jobKey, CancellationToken ct) { return await db.SeibroFetchMarker.AsNoTracking().Where(c => c.JobKey == jobKey).ToDictionaryAsync(c => c.EntityKey, c => c.LastCheckedAt, ct); } /// 조회 시도(0행 포함) 후 rolling 마커 upsert — 있으면 Touch, 없으면 Create. 즉시 SaveChanges. public static async Task TouchFetchMarkerAsync(IAppDbContext db, string jobKey, string entityKey, CancellationToken ct) { var marker = await db.SeibroFetchMarker.FirstOrDefaultAsync(c => c.JobKey == jobKey && c.EntityKey == entityKey, ct); if (marker is null) { await db.SeibroFetchMarker.AddAsync(SeibroFetchMarker.Create(jobKey, entityKey), ct); } else { marker.Touch(); } await db.SaveChangesAsync(ct); } /// 해당 (job, 날짜[, 축])이 이미 조회 완료(0행 포함)로 기록됐는지. public static async Task IsSweptAsync(IAppDbContext db, string jobKey, DateOnly date, string? discriminator, CancellationToken ct) { var disc = string.IsNullOrWhiteSpace(discriminator) ? string.Empty : discriminator.Trim(); return await db.SeibroSweptDate.AsNoTracking().AnyAsync(c => c.JobKey == jobKey && c.Date == date && c.Discriminator == disc, ct); } /// 조회 완료(0행 포함) 후 날짜 스윕 마커 기록 — 이미 있으면 no-op. 즉시 SaveChanges. public static async Task MarkSweptAsync(IAppDbContext db, string jobKey, DateOnly date, string? discriminator, CancellationToken ct) { var disc = string.IsNullOrWhiteSpace(discriminator) ? string.Empty : discriminator.Trim(); if (await db.SeibroSweptDate.AnyAsync(c => c.JobKey == jobKey && c.Date == date && c.Discriminator == disc, ct)) { return; } await db.SeibroSweptDate.AddAsync(SeibroSweptDate.Create(jobKey, date, disc), ct); await db.SaveChangesAsync(ct); } }