using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Domain.Entities.Stocks.ValueObject; using Microsoft.EntityFrameworkCore; namespace Application.Features.Api.Stocks.GetWorldIndices; /// /// 세계 주요국 증시지수 병합 — 국내(코스피)는 IndexDailyPrice 최신 거래일 실데이터, 해외는 WorldIndexSnapshot(나라당 1개). /// 국가 코드(ISO2)로 프론트가 세계지도 핀 좌표를 매핑한다. 해외 스냅샷에 KR 이 섞여도 중복 방지로 제외. /// internal sealed class Handler(IAppDbContext db) : IQueryHandler { public async Task Handle(Query request, CancellationToken ct) { var rows = new List(); // 국내(코스피) — IndexDailyPrice 최신 거래일. 한국 핀은 KRX 실데이터 사용. var latestKr = await db.IndexDailyPrice.AsNoTracking().OrderByDescending(c => c.TradeDate).Select(c => (DateOnly?)c.TradeDate).FirstOrDefaultAsync(ct); if (latestKr is not null) { var kospi = await db.IndexDailyPrice.AsNoTracking() .Where(c => c.TradeDate == latestKr.Value && c.Series == MarketIndexSeries.KOSPI && c.IndexName == "코스피") .Select(c => new { c.Close, c.ChangeVal, c.FlucRateBp, c.TradeDate }) .FirstOrDefaultAsync(ct); if (kospi is not null) { rows.Add(new Response.Row { CountryCode = "KR", Name = "코스피", ExchangeName = "KRX", Close = kospi.Close, ChangeVal = kospi.ChangeVal, FlucRateBp = kospi.FlucRateBp, TradeDate = kospi.TradeDate }); } } // 해외 — WorldIndexSnapshot. KR 은 위에서 병합했으므로 제외. 한 국가에 지수가 여러 개면(예: US ^gspc/^ixic) // 지도는 나라당 1핀이므로 심볼 오름차순 첫 행만 남긴다(US → ^gspc=S&P500). 요약 카드는 별도 엔드포인트에서 심볼로 조회. var foreignRaw = await db.WorldIndexSnapshot.AsNoTracking() .Where(c => c.CountryCode != "KR") .OrderBy(c => c.CountryCode).ThenBy(c => c.Symbol) .Select(c => new { c.CountryCode, c.Name, c.ExchangeName, c.Close, c.ChangeVal, c.FlucRateBp, c.TradeDate }) .ToListAsync(ct); var seenCountry = new HashSet(); foreach (var c in foreignRaw) { if (!seenCountry.Add(c.CountryCode)) { continue; } rows.Add(new Response.Row { CountryCode = c.CountryCode, Name = c.Name, ExchangeName = c.ExchangeName, Close = c.Close, ChangeVal = c.ChangeVal, FlucRateBp = c.FlucRateBp, TradeDate = c.TradeDate }); } return new Response { List = rows }; } }