using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Domain.Entities.Stocks.ValueObject; using Microsoft.EntityFrameworkCore; namespace Application.Features.Api.Stocks.GetExchangeRates; /// /// 환율 목록 — Yahoo Finance 수집분(MarketQuoteSnapshot, Category=Fx·GroupCode="macro")에서 통화별 최신 스냅샷을 반환 (익명). /// koreaexim AP01 은 봇차단 WAF(TLS 리셋+쿠키챌린지)로 이 서버망에서 수집 불가 → 이미 수집 중인 Yahoo 매크로 시세로 소스 전환 (2026-07-14). /// Yahoo 는 단일가(regularMarketPrice)만 제공하므로 DealBasRate=Close, Ttb/Tts=null. USDKRW=X 가 KRW=X 로 정규화돼 중복 저장되는 /// 케이스는 통화코드 파생 후 통화별 최신 TradeDate 로 dedup 한다. Date 파라미터는 스냅샷 특성상 무시(항상 최신)한다. /// internal sealed class Handler(IAppDbContext db) : IQueryHandler { public async Task Handle(Query request, CancellationToken ct) { var rows = await db.MarketQuoteSnapshot.AsNoTracking() .Where(c => c.Category == QuoteCategory.Fx && c.GroupCode == "macro") .Select(c => new { c.Symbol, c.Name, c.Close, c.TradeDate }) .ToListAsync(ct); if (rows.Count == 0) { return new Response { Total = 0, TradeDate = null, List = [] }; } // 통화코드 파생 + 통화별 최신 TradeDate 로 dedup (USDKRW=X ↔ KRW=X 정규화 중복 방어) var list = rows .GroupBy(c => ToCurrencyUnit(c.Symbol)) .Select(g => g.OrderByDescending(x => x.TradeDate).First()) .OrderBy(c => ToCurrencyUnit(c.Symbol)) .Select(c => new Response.Row { CurrencyUnit = ToCurrencyUnit(c.Symbol), CurrencyName = c.Name, DealBasRate = c.Close, Ttb = null, Tts = null }) .ToList(); return new Response { Total = list.Count, TradeDate = rows.Max(c => c.TradeDate), List = list }; } /// Yahoo FX 심볼 → 통화코드. USDKRW=X 는 Yahoo 가 KRW=X 로 정규화하므로 둘 다 USD 로 매핑. private static string ToCurrencyUnit(string symbol) { var s = symbol.Trim().ToUpperInvariant(); if (s is "KRW=X" or "USDKRW=X") { return "USD"; } var idx = s.IndexOf("KRW=X", StringComparison.Ordinal); return idx > 0 ? s[..idx] : s.Replace("=X", ""); } }