|
|
@@ -1,11 +1,14 @@
|
|
|
-using Microsoft.AspNetCore.Mvc;
|
|
|
+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
|
|
|
{
|
|
|
@@ -13,22 +16,27 @@ namespace economy.Controllers.Price.Global
|
|
|
{
|
|
|
private readonly Alpha_API _alphaAPI;
|
|
|
private readonly KoreaEximGoKR _koreaEximGoKR;
|
|
|
+ private readonly IMemoryCache _cache;
|
|
|
private Dictionary<string, string> _queryString;
|
|
|
- private int _exchangeValue = 0;
|
|
|
|
|
|
- public GlobalController(Alpha_API alphaAPI, KoreaEximGoKR koreaEximGoKR)
|
|
|
+ // 커모디티 화이트리스트 (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;
|
|
|
-
|
|
|
- InitializeAsync();
|
|
|
- }
|
|
|
-
|
|
|
- // 초기화 메서드
|
|
|
- public async Task InitializeAsync()
|
|
|
- {
|
|
|
- // 환율 조회
|
|
|
- _exchangeValue = await new FinancialModel(_koreaEximGoKR).GetExchangeValue();
|
|
|
+ _cache = cache;
|
|
|
+ _queryString = [];
|
|
|
}
|
|
|
|
|
|
public override void OnActionExecuting(ActionExecutingContext context)
|
|
|
@@ -40,6 +48,33 @@ namespace economy.Controllers.Price.Global
|
|
|
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;
|
|
|
+ }
|
|
|
+
|
|
|
[HttpGet("Price/Global/NaturalGas")]
|
|
|
public async Task<IActionResult> NaturalGas(NaturalGas.Request request)
|
|
|
{
|
|
|
@@ -53,12 +88,14 @@ namespace economy.Controllers.Price.Global
|
|
|
return BadRequest("Invalid input provided.");
|
|
|
}
|
|
|
|
|
|
- GlobalModel globalModel = new GlobalModel(_alphaAPI);
|
|
|
+ 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())
|
|
|
+ if (itemList.Data != null && itemList.Data.Any())
|
|
|
{
|
|
|
total = itemList.Data.Count;
|
|
|
int listNum = Common.CalcListNumber(total, request.PageNo, request.NumOfRows);
|
|
|
@@ -71,14 +108,19 @@ namespace economy.Controllers.Price.Global
|
|
|
{
|
|
|
row.Num = (listNum - index);
|
|
|
|
|
|
- if (int.TryParse(row.Value, out int value) && value > 0)
|
|
|
+ if (decimal.TryParse(row.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal value) && value > 0)
|
|
|
{
|
|
|
- row.KRW = (value * _exchangeValue);
|
|
|
+ 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;
|
|
|
@@ -87,7 +129,7 @@ namespace economy.Controllers.Price.Global
|
|
|
|
|
|
var queryString = new
|
|
|
{
|
|
|
- date = request.Interval
|
|
|
+ interval = request.Interval
|
|
|
};
|
|
|
|
|
|
var pagination = new Pagination(total, request.PageNo, request.NumOfRows, queryString);
|
|
|
@@ -95,5 +137,80 @@ namespace economy.Controllers.Price.Global
|
|
|
|
|
|
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);
|
|
|
+
|
|
|
+ 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<Commodity.Request, Commodity.Response>();
|
|
|
+ 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);
|
|
|
+ }
|
|
|
}
|
|
|
}
|