| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 |
- 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<string, string> _queryString;
- // 커모디티 화이트리스트 (Alpha Vantage 함수, 화면 제목, 원화 표시 여부)
- private static readonly Dictionary<string, (string Function, string Title, bool HasKrw)> 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<int> 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;
- }
- // 국제 시세 응답을 차트 데이터로 변환 (최신 60개, 오름차순)
- private static ChartData BuildChart(string name, IEnumerable<(string Date, string Value)> rows)
- {
- var points = rows
- .Select(r => (r.Date, Parsed: decimal.TryParse(r.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal value) ? value : (decimal?)null))
- .Where(p => p.Parsed.HasValue)
- .Take(60)
- .Reverse()
- .ToList();
- if (points.Count < 2)
- {
- return new ChartData();
- }
- return new ChartData
- {
- Labels = points.Select(p => p.Date).ToList(),
- Series = [new ChartSeries { Name = name, Values = points.Select(p => p.Parsed).ToList() }]
- };
- }
- [HttpGet("Price/Global/NaturalGas")]
- public async Task<IActionResult> 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);
- ChartData? chart = null;
- int total = 0;
- if (itemList.Data != null && itemList.Data.Any())
- {
- chart = BuildChart("천연가스", itemList.Data.Select(d => (d.Date, d.Value)));
- 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<NaturalGas.Request, NaturalGas.Response>();
- viewModel.SelectedListPerPage = request.NumOfRows;
- viewModel.Request = request;
- viewModel.Response = itemList;
- viewModel.Chart = chart;
- 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<IActionResult> 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);
- ChartData? chart = null;
- int total = 0;
- if (itemList.Data != null && itemList.Data.Any())
- {
- chart = BuildChart(meta.Title, itemList.Data.Select(d => (d.Date, d.Value)));
- 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<Commodity.Request, Commodity.Response>();
- viewModel.SelectedListPerPage = request.NumOfRows;
- viewModel.Request = request;
- viewModel.Response = itemList;
- viewModel.Chart = chart;
- 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);
- }
- }
- }
|