GlobalController.cs 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. using Microsoft.AspNetCore.Mvc;
  2. using Microsoft.AspNetCore.Mvc.Filters;
  3. using Microsoft.AspNetCore.WebUtilities;
  4. using Microsoft.Extensions.Caching.Memory;
  5. using System.Globalization;
  6. using economy.Helpers;
  7. using economy.Models;
  8. using economy.Models.Financial;
  9. using economy.Models.Price.Global;
  10. using NaturalGas = economy.Models.Price.Global.NaturalGas;
  11. using Commodity = economy.Models.Price.Global.Commodity;
  12. namespace economy.Controllers.Price.Global
  13. {
  14. public class GlobalController : Controller
  15. {
  16. private readonly Alpha_API _alphaAPI;
  17. private readonly KoreaEximGoKR _koreaEximGoKR;
  18. private readonly IMemoryCache _cache;
  19. private Dictionary<string, string> _queryString;
  20. // 커모디티 화이트리스트 (Alpha Vantage 함수, 화면 제목, 원화 표시 여부)
  21. private static readonly Dictionary<string, (string Function, string Title, bool HasKrw)> CommodityMap = new(StringComparer.OrdinalIgnoreCase)
  22. {
  23. ["copper"] = ("COPPER", "구리 시세", true),
  24. ["aluminum"] = ("ALUMINUM", "알루미늄 시세", true),
  25. ["wheat"] = ("WHEAT", "밀 시세", true),
  26. ["cotton"] = ("COTTON", "섬유(면화) 시세", true),
  27. ["sugar"] = ("SUGAR", "설탕 시세", true),
  28. ["coffee"] = ("COFFEE", "커피 시세", true),
  29. ["allcommodities"] = ("ALL_COMMODITIES", "원자재 종합지수", false), // 지수(2016=100)라 원화 환산 없음
  30. };
  31. public GlobalController(Alpha_API alphaAPI, KoreaEximGoKR koreaEximGoKR, IMemoryCache cache)
  32. {
  33. _alphaAPI = alphaAPI;
  34. _koreaEximGoKR = koreaEximGoKR;
  35. _cache = cache;
  36. _queryString = [];
  37. }
  38. public override void OnActionExecuting(ActionExecutingContext context)
  39. {
  40. _queryString = QueryHelpers.ParseQuery(HttpContext.Request.QueryString.Value).ToDictionary(k => k.Key, v => string.Join(",", v.Value));
  41. ViewBag.QueryString = _queryString;
  42. base.OnActionExecuting(context);
  43. }
  44. // 환율 조회 (1시간 캐시 — 실패값 0은 캐시하지 않음)
  45. private async Task<int> GetExchangeValueAsync()
  46. {
  47. if (_cache.TryGetValue("ExchangeValue_USD", out int cachedValue))
  48. {
  49. return cachedValue;
  50. }
  51. // 환율 API가 느리거나 장애일 때 시세 페이지까지 지연되지 않도록 최대 3초만 대기 (실패 시 원화 미표시)
  52. var exchangeTask = new FinancialModel(_koreaEximGoKR).GetExchangeValue();
  53. var completedTask = await Task.WhenAny(exchangeTask, Task.Delay(TimeSpan.FromSeconds(3)));
  54. int exchangeValue = (completedTask == exchangeTask) ? await exchangeTask : 0;
  55. if (exchangeValue > 0)
  56. {
  57. _cache.Set("ExchangeValue_USD", exchangeValue, TimeSpan.FromHours(1));
  58. }
  59. else
  60. {
  61. // 환율 API 장애 중 매 요청마다 3초씩 대기하지 않도록 실패값은 5분간만 유지
  62. _cache.Set("ExchangeValue_USD", 0, TimeSpan.FromMinutes(5));
  63. }
  64. return exchangeValue;
  65. }
  66. [HttpGet("Price/Global/NaturalGas")]
  67. public async Task<IActionResult> NaturalGas(NaturalGas.Request request)
  68. {
  69. if (!ModelState.IsValid)
  70. {
  71. return BadRequest(ModelState);
  72. }
  73. if (!new[] { "daily", "weekly", "monthly" }.Contains(request.Interval))
  74. {
  75. return BadRequest("Invalid input provided.");
  76. }
  77. int exchangeValue = await GetExchangeValueAsync();
  78. GlobalModel globalModel = new GlobalModel(_alphaAPI, _cache);
  79. NaturalGas.Response itemList = await globalModel.GetNaturalGasPriceInfo(request);
  80. int total = 0;
  81. if (itemList.Data != null && itemList.Data.Any())
  82. {
  83. total = itemList.Data.Count;
  84. int listNum = Common.CalcListNumber(total, request.PageNo, request.NumOfRows);
  85. // 페이징 처리
  86. int skip = (request.PageNo - 1) * request.NumOfRows;
  87. int take = request.NumOfRows;
  88. itemList.Data = itemList.Data.Skip(skip).Take(take).Select((row, index) =>
  89. {
  90. row.Num = (listNum - index);
  91. if (decimal.TryParse(row.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal value) && value > 0)
  92. {
  93. row.KRW = (int)Math.Round(value * exchangeValue);
  94. }
  95. return row;
  96. }).ToList();
  97. }
  98. else
  99. {
  100. ViewBag.isError = true;
  101. ViewBag.errorMessage = "국제 시세 데이터를 가져오지 못했습니다. (API 한도 초과 또는 일시 장애)";
  102. }
  103. var viewModel = new View<NaturalGas.Request, NaturalGas.Response>();
  104. viewModel.SelectedListPerPage = request.NumOfRows;
  105. viewModel.Request = request;
  106. viewModel.Response = itemList;
  107. var queryString = new
  108. {
  109. interval = request.Interval
  110. };
  111. var pagination = new Pagination(total, request.PageNo, request.NumOfRows, queryString);
  112. viewModel.Pagination = pagination;
  113. return View("/Views/Price/Global/NaturalGas.cshtml", viewModel);
  114. }
  115. [HttpGet("Price/Global/{commodity}")]
  116. public async Task<IActionResult> Commodity(Commodity.Request request)
  117. {
  118. if (!ModelState.IsValid)
  119. {
  120. return BadRequest(ModelState);
  121. }
  122. // 화이트리스트에 없는 상품은 404
  123. if (request.Commodity is null || !CommodityMap.TryGetValue(request.Commodity, out var meta))
  124. {
  125. return NotFound();
  126. }
  127. // 원자재 계열은 monthly/quarterly/annual만 지원 (잘못된 값은 Alpha Vantage가 조용히 monthly로 대체하므로 사전 차단)
  128. if (!new[] { "monthly", "quarterly", "annual" }.Contains(request.Interval))
  129. {
  130. return BadRequest("Invalid input provided.");
  131. }
  132. int exchangeValue = meta.HasKrw ? await GetExchangeValueAsync() : 0;
  133. GlobalModel globalModel = new GlobalModel(_alphaAPI, _cache);
  134. Commodity.Response itemList = await globalModel.GetCommodityPriceInfo(meta.Function, request.Interval);
  135. int total = 0;
  136. if (itemList.Data != null && itemList.Data.Any())
  137. {
  138. total = itemList.Data.Count;
  139. int listNum = Common.CalcListNumber(total, request.PageNo, request.NumOfRows);
  140. // 페이징 처리
  141. int skip = (request.PageNo - 1) * request.NumOfRows;
  142. int take = request.NumOfRows;
  143. itemList.Data = itemList.Data.Skip(skip).Take(take).Select((row, index) =>
  144. {
  145. row.Num = (listNum - index);
  146. if (decimal.TryParse(row.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal value) && value > 0)
  147. {
  148. row.KRW = Math.Round(value * exchangeValue);
  149. }
  150. row.Value = Common.NumberFormat(row.Value, "#,##0.##");
  151. return row;
  152. }).ToList();
  153. }
  154. else
  155. {
  156. ViewBag.isError = true;
  157. ViewBag.errorMessage = "국제 시세 데이터를 가져오지 못했습니다. (API 한도 초과 또는 일시 장애)";
  158. }
  159. ViewBag.Title = meta.Title;
  160. ViewBag.HasKrw = meta.HasKrw;
  161. var viewModel = new View<Commodity.Request, Commodity.Response>();
  162. viewModel.SelectedListPerPage = request.NumOfRows;
  163. viewModel.Request = request;
  164. viewModel.Response = itemList;
  165. var queryString = new
  166. {
  167. interval = request.Interval
  168. };
  169. var pagination = new Pagination(total, request.PageNo, request.NumOfRows, queryString);
  170. viewModel.Pagination = pagination;
  171. return View("/Views/Price/Global/Commodity.cshtml", viewModel);
  172. }
  173. }
  174. }