using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Caching.Memory; using System.Globalization; using economy.Helpers; using economy.Models; using economy.Models.Financial; using economy.Models.Price.Global; using NaturalGas = economy.Models.Price.Global.NaturalGas; using Commodity = economy.Models.Price.Global.Commodity; namespace economy.Controllers.Price.Global { public class GlobalController : Controller { private readonly Alpha_API _alphaAPI; private readonly KoreaEximGoKR _koreaEximGoKR; private readonly IMemoryCache _cache; private Dictionary _queryString; // 커모디티 화이트리스트 (Alpha Vantage 함수, 화면 제목, 원화 표시 여부) private static readonly Dictionary CommodityMap = new(StringComparer.OrdinalIgnoreCase) { ["copper"] = ("COPPER", "구리 시세", true), ["aluminum"] = ("ALUMINUM", "알루미늄 시세", true), ["wheat"] = ("WHEAT", "밀 시세", true), ["cotton"] = ("COTTON", "섬유(면화) 시세", true), ["sugar"] = ("SUGAR", "설탕 시세", true), ["coffee"] = ("COFFEE", "커피 시세", true), ["allcommodities"] = ("ALL_COMMODITIES", "원자재 종합지수", false), // 지수(2016=100)라 원화 환산 없음 }; public GlobalController(Alpha_API alphaAPI, KoreaEximGoKR koreaEximGoKR, IMemoryCache cache) { _alphaAPI = alphaAPI; _koreaEximGoKR = koreaEximGoKR; _cache = cache; _queryString = []; } public override void OnActionExecuting(ActionExecutingContext context) { _queryString = QueryHelpers.ParseQuery(HttpContext.Request.QueryString.Value).ToDictionary(k => k.Key, v => string.Join(",", v.Value)); ViewBag.QueryString = _queryString; base.OnActionExecuting(context); } // 환율 조회 (1시간 캐시 — 실패값 0은 캐시하지 않음) private async Task GetExchangeValueAsync() { if (_cache.TryGetValue("ExchangeValue_USD", out int cachedValue)) { return cachedValue; } // 환율 API가 느리거나 장애일 때 시세 페이지까지 지연되지 않도록 최대 3초만 대기 (실패 시 원화 미표시) var exchangeTask = new FinancialModel(_koreaEximGoKR).GetExchangeValue(); var completedTask = await Task.WhenAny(exchangeTask, Task.Delay(TimeSpan.FromSeconds(3))); int exchangeValue = (completedTask == exchangeTask) ? await exchangeTask : 0; if (exchangeValue > 0) { _cache.Set("ExchangeValue_USD", exchangeValue, TimeSpan.FromHours(1)); } else { // 환율 API 장애 중 매 요청마다 3초씩 대기하지 않도록 실패값은 5분간만 유지 _cache.Set("ExchangeValue_USD", 0, TimeSpan.FromMinutes(5)); } return exchangeValue; } [HttpGet("Price/Global/NaturalGas")] public async Task NaturalGas(NaturalGas.Request request) { if (!ModelState.IsValid) { return BadRequest(ModelState); } if (!new[] { "daily", "weekly", "monthly" }.Contains(request.Interval)) { return BadRequest("Invalid input provided."); } int exchangeValue = await GetExchangeValueAsync(); GlobalModel globalModel = new GlobalModel(_alphaAPI, _cache); NaturalGas.Response itemList = await globalModel.GetNaturalGasPriceInfo(request); int total = 0; if (itemList.Data != null && itemList.Data.Any()) { total = itemList.Data.Count; int listNum = Common.CalcListNumber(total, request.PageNo, request.NumOfRows); // 페이징 처리 int skip = (request.PageNo - 1) * request.NumOfRows; int take = request.NumOfRows; itemList.Data = itemList.Data.Skip(skip).Take(take).Select((row, index) => { row.Num = (listNum - index); if (decimal.TryParse(row.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal value) && value > 0) { row.KRW = (int)Math.Round(value * exchangeValue); } return row; }).ToList(); } else { ViewBag.isError = true; ViewBag.errorMessage = "국제 시세 데이터를 가져오지 못했습니다. (API 한도 초과 또는 일시 장애)"; } var viewModel = new View(); viewModel.SelectedListPerPage = request.NumOfRows; viewModel.Request = request; viewModel.Response = itemList; var queryString = new { interval = request.Interval }; var pagination = new Pagination(total, request.PageNo, request.NumOfRows, queryString); viewModel.Pagination = pagination; return View("/Views/Price/Global/NaturalGas.cshtml", viewModel); } [HttpGet("Price/Global/{commodity}")] public async Task Commodity(Commodity.Request request) { if (!ModelState.IsValid) { return BadRequest(ModelState); } // 화이트리스트에 없는 상품은 404 if (request.Commodity is null || !CommodityMap.TryGetValue(request.Commodity, out var meta)) { return NotFound(); } // 원자재 계열은 monthly/quarterly/annual만 지원 (잘못된 값은 Alpha Vantage가 조용히 monthly로 대체하므로 사전 차단) if (!new[] { "monthly", "quarterly", "annual" }.Contains(request.Interval)) { return BadRequest("Invalid input provided."); } int exchangeValue = meta.HasKrw ? await GetExchangeValueAsync() : 0; GlobalModel globalModel = new GlobalModel(_alphaAPI, _cache); Commodity.Response itemList = await globalModel.GetCommodityPriceInfo(meta.Function, request.Interval); int total = 0; if (itemList.Data != null && itemList.Data.Any()) { total = itemList.Data.Count; int listNum = Common.CalcListNumber(total, request.PageNo, request.NumOfRows); // 페이징 처리 int skip = (request.PageNo - 1) * request.NumOfRows; int take = request.NumOfRows; itemList.Data = itemList.Data.Skip(skip).Take(take).Select((row, index) => { row.Num = (listNum - index); if (decimal.TryParse(row.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal value) && value > 0) { row.KRW = Math.Round(value * exchangeValue); } row.Value = Common.NumberFormat(row.Value, "#,##0.##"); return row; }).ToList(); } else { ViewBag.isError = true; ViewBag.errorMessage = "국제 시세 데이터를 가져오지 못했습니다. (API 한도 초과 또는 일시 장애)"; } ViewBag.Title = meta.Title; ViewBag.HasKrw = meta.HasKrw; var viewModel = new View(); viewModel.SelectedListPerPage = request.NumOfRows; viewModel.Request = request; viewModel.Response = itemList; var queryString = new { interval = request.Interval }; var pagination = new Pagination(total, request.PageNo, request.NumOfRows, queryString); viewModel.Pagination = pagination; return View("/Views/Price/Global/Commodity.cshtml", viewModel); } } }