KosisImport.cs 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. using Application.Abstractions.Data;
  2. using Domain.Entities.Stocks;
  3. using Microsoft.EntityFrameworkCore;
  4. using SharedKernel;
  5. namespace Infrastructure.StockData;
  6. /// <summary>
  7. /// KOSIS 파싱 결과 → MacroIndicator upsert (UQ = Code+Period). HTTP 와 분리된 순수 DB 반영 로직 (Seibro *Import 방식 미러링) —
  8. /// 통합 테스트가 파서 fixture 입력으로 직접 호출한다. 호출부(KosisSyncService)는 지표 하나의 응답 rows 를 넘긴다.
  9. /// 계열 특정: config ClassCode(C1) 가 지정되면 그 C1 행만, 비면 응답 첫 계열(첫 등장 C1)로 자동 고정해 여러 분류 혼입을 배제한다.
  10. /// </summary>
  11. public static class KosisImport
  12. {
  13. /// <summary>
  14. /// 지표 하나의 파싱 rows 를 (Code, Period) upsert. 반환 = (inserted, updated). SaveChanges 는 호출부가 수행하지 않으므로 여기서 저장한다.
  15. /// </summary>
  16. public static async Task<(int Inserted, int Updated)> UpsertAsync(
  17. IAppDbContext db,
  18. AppSettings.KosisSection.KosisIndicator indicator,
  19. IReadOnlyList<KosisParser.Row> rows,
  20. CancellationToken ct
  21. ) {
  22. var selected = SelectSeries(indicator.ClassCode, rows);
  23. if (selected.Count == 0)
  24. {
  25. return (0, 0);
  26. }
  27. var code = indicator.Code.Trim();
  28. var periods = selected.Select(c => c.Period).ToHashSet();
  29. var existing = await db.MacroIndicator.Where(c => c.Code == code && periods.Contains(c.Period)).ToListAsync(ct);
  30. var existingByPeriod = existing.ToDictionary(c => c.Period);
  31. var inserted = 0;
  32. var updated = 0;
  33. foreach (var row in selected)
  34. {
  35. if (existingByPeriod.TryGetValue(row.Period, out var entity))
  36. {
  37. var before = (entity.Value, entity.Unit, entity.Name);
  38. entity.Update(indicator.Name, row.Value, row.Unit);
  39. if (before != (entity.Value, entity.Unit, entity.Name))
  40. {
  41. updated++;
  42. }
  43. }
  44. else
  45. {
  46. var created = MacroIndicator.Create(code, indicator.Name, row.Period, row.Value, row.Unit);
  47. await db.MacroIndicator.AddAsync(created, ct);
  48. existingByPeriod[row.Period] = created;
  49. inserted++;
  50. }
  51. }
  52. if (inserted > 0 || updated > 0)
  53. {
  54. await db.SaveChangesAsync(ct);
  55. }
  56. return (inserted, updated);
  57. }
  58. /// <summary>
  59. /// 여러 분류가 섞인 rows 에서 원하는 한 계열만 고른다. classCode 지정 시 그 C1 행만,
  60. /// 비면 첫 등장 C1 값으로 고정 (KOSIS 통계표가 단일 계열이면 그대로 전부 채택). 시점(Period)당 1행이 되도록 첫 매치만 취한다.
  61. /// </summary>
  62. private static List<KosisParser.Row> SelectSeries(string classCode, IReadOnlyList<KosisParser.Row> rows)
  63. {
  64. var result = new List<KosisParser.Row>();
  65. if (rows.Count == 0)
  66. {
  67. return result;
  68. }
  69. var target = classCode.Trim();
  70. if (target.Length == 0)
  71. {
  72. target = rows[0].ClassCode;
  73. }
  74. var seenPeriods = new HashSet<string>();
  75. foreach (var row in rows)
  76. {
  77. if (row.ClassCode != target)
  78. {
  79. continue;
  80. }
  81. if (seenPeriods.Add(row.Period))
  82. {
  83. result.Add(row);
  84. }
  85. }
  86. return result;
  87. }
  88. }