瀏覽代碼

국내/국제 시세 페이지 상단 미니 차트 추가

- 공용 ChartData 모델 + Chart.cshtml 파셜 + price-chart.js (Chart.js 4.x CDN)
- 국제(천연가스/커모디티): 캐시된 전체 시계열 재사용, 최신 60개 포인트 단일 라인
- 국내(금/석유/배출권): 같은 검색조건 차트용 1회 추가 조회, 종목/유종별 다중 라인
- 데이터 없거나 API 실패 시 차트 영역 미표시 (표 동작 무변화)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
KIM-JINO5 1 天之前
父節點
當前提交
aeabb09770

+ 35 - 0
Controllers/Price/Domestic/EmissionController.cs

@@ -1,6 +1,7 @@
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.Filters;
 using Microsoft.AspNetCore.WebUtilities;
+using System.Globalization;
 using economy.Helpers;
 using economy.Models;
 using economy.Models.Price.Domestic;
@@ -62,10 +63,44 @@ namespace economy.Controllers.Price.Domestic
                 ViewBag.errorMessage = "시세 정보를 가져오지 못했습니다. (외부 API 오류)";
             }
 
+            // 차트용 데이터 조회 (같은 검색조건, 최대 300행)
+            ChartData? chart = null;
+
+            var chartRequest = new Request
+            {
+                PageNo = 1,
+                NumOfRows = 300,
+                StartDate = request.StartDate,
+                EndDate = request.EndDate,
+                LikeSrtnCd = request.LikeSrtnCd
+            };
+            Response chartList = await domesticModel.GetEmissionPriceInfo(chartRequest);
+
+            if (chartList.Body?.Items?.ItemList is not null)
+            {
+                var rows = chartList.Body.Items.ItemList;
+                var labels = rows.Select(r => r.BasDt).Distinct().OrderBy(d => d).ToList();
+
+                chart = new ChartData
+                {
+                    Labels = labels.Select(d => Common.StringToDateFormat(d)).ToList(),
+                    Series = rows.GroupBy(r => r.ItmsNm).Select(g => new ChartSeries
+                    {
+                        Name = g.Key,
+                        Values = labels.Select(d =>
+                        {
+                            var row = g.FirstOrDefault(r => r.BasDt == d);
+                            return (row is not null && decimal.TryParse(row.Clpr, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal value)) ? value : (decimal?)null;
+                        }).ToList()
+                    }).ToList()
+                };
+            }
+
             var viewModel = new View<Request, Response>();
             viewModel.SelectedListPerPage = request.NumOfRows;
             viewModel.Request = request;
             viewModel.Response = itemList;
+            viewModel.Chart = chart;
 
             var queryString = new
             {

+ 35 - 0
Controllers/Price/Domestic/GoldController.cs

@@ -1,6 +1,7 @@
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.Filters;
 using Microsoft.AspNetCore.WebUtilities;
+using System.Globalization;
 using economy.Helpers;
 using economy.Models;
 using economy.Models.Price.Domestic;
@@ -62,10 +63,44 @@ namespace economy.Controllers.Price.Domestic
                 ViewBag.errorMessage = "시세 정보를 가져오지 못했습니다. (외부 API 오류)";
             }
 
+            // 차트용 데이터 조회 (같은 검색조건, 최대 300행)
+            ChartData? chart = null;
+
+            var chartRequest = new Request
+            {
+                PageNo = 1,
+                NumOfRows = 300,
+                StartDate = request.StartDate,
+                EndDate = request.EndDate,
+                LikeSrtnCd = request.LikeSrtnCd
+            };
+            Response chartList = await domesticModel.GetGoldPriceInfo(chartRequest);
+
+            if (chartList.Body?.Items?.ItemList is not null)
+            {
+                var rows = chartList.Body.Items.ItemList;
+                var labels = rows.Select(r => r.BasDt).Distinct().OrderBy(d => d).ToList();
+
+                chart = new ChartData
+                {
+                    Labels = labels.Select(d => Common.StringToDateFormat(d)).ToList(),
+                    Series = rows.GroupBy(r => r.ItmsNm).Select(g => new ChartSeries
+                    {
+                        Name = g.Key,
+                        Values = labels.Select(d =>
+                        {
+                            var row = g.FirstOrDefault(r => r.BasDt == d);
+                            return (row is not null && decimal.TryParse(row.Clpr, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal value)) ? value : (decimal?)null;
+                        }).ToList()
+                    }).ToList()
+                };
+            }
+
             var viewModel = new View<Request, Response>();
             viewModel.SelectedListPerPage = request.NumOfRows;
             viewModel.Request = request;
             viewModel.Response = itemList;
+            viewModel.Chart = chart;
 
             var queryString = new
             {

+ 34 - 0
Controllers/Price/Domestic/OilController.cs

@@ -1,6 +1,7 @@
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.Filters;
 using Microsoft.AspNetCore.WebUtilities;
+using System.Globalization;
 using economy.Helpers;
 using economy.Models;
 using economy.Models.Price.Domestic;
@@ -59,10 +60,43 @@ namespace economy.Controllers.Price.Domestic
                 ViewBag.errorMessage = "시세 정보를 가져오지 못했습니다. (외부 API 오류)";
             }
 
+            // 차트용 데이터 조회 (같은 검색조건, 최대 300행)
+            ChartData? chart = null;
+
+            var chartRequest = new Request
+            {
+                PageNo = 1,
+                NumOfRows = 300,
+                StartDate = request.StartDate,
+                EndDate = request.EndDate
+            };
+            Response chartList = await domesticModel.GetOilPriceInfo(chartRequest);
+
+            if (chartList.Body?.Items?.ItemList is not null)
+            {
+                var rows = chartList.Body.Items.ItemList;
+                var labels = rows.Select(r => r.BasDt).Distinct().OrderBy(d => d).ToList();
+
+                chart = new ChartData
+                {
+                    Labels = labels.Select(d => Common.StringToDateFormat(d)).ToList(),
+                    Series = rows.GroupBy(r => r.OilCtg).Select(g => new ChartSeries
+                    {
+                        Name = g.Key,
+                        Values = labels.Select(d =>
+                        {
+                            var row = g.FirstOrDefault(r => r.BasDt == d);
+                            return (row is not null && decimal.TryParse(row.WtAvgPrcCptn, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal value)) ? value : (decimal?)null;
+                        }).ToList()
+                    }).ToList()
+                };
+            }
+
             var viewModel = new View<Request, Response>();
             viewModel.SelectedListPerPage = request.NumOfRows;
             viewModel.Request = request;
             viewModel.Response = itemList;
+            viewModel.Chart = chart;
 
             var queryString = new
             {

+ 30 - 0
Controllers/Price/GlobalController.cs

@@ -75,6 +75,28 @@ namespace economy.Controllers.Price.Global
             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)
         {
@@ -93,10 +115,13 @@ namespace economy.Controllers.Price.Global
             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);
 
@@ -126,6 +151,7 @@ namespace economy.Controllers.Price.Global
             viewModel.SelectedListPerPage = request.NumOfRows;
             viewModel.Request = request;
             viewModel.Response = itemList;
+            viewModel.Chart = chart;
 
             var queryString = new
             {
@@ -163,10 +189,13 @@ namespace economy.Controllers.Price.Global
             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);
 
@@ -201,6 +230,7 @@ namespace economy.Controllers.Price.Global
             viewModel.SelectedListPerPage = request.NumOfRows;
             viewModel.Request = request;
             viewModel.Response = itemList;
+            viewModel.Chart = chart;
 
             var queryString = new
             {

+ 17 - 0
Models/ChartData.cs

@@ -0,0 +1,17 @@
+namespace economy.Models
+{
+    // 시세 페이지 상단 미니 차트 데이터
+    public class ChartData
+    {
+        public List<string> Labels { get; set; } = []; // X축 날짜 (오름차순, "yyyy-MM-dd")
+
+        public List<ChartSeries> Series { get; set; } = [];
+    }
+
+    public class ChartSeries
+    {
+        public string Name { get; set; } = ""; // 범례 이름 (종목명/유종)
+
+        public List<decimal?> Values { get; set; } = []; // Labels와 같은 길이, 결측은 null
+    }
+}

+ 2 - 0
Models/View.cs

@@ -75,6 +75,8 @@ namespace economy.Models
             new SelectListItem { Value = "300", Text = "상위 300" }
         };
 
+        public ChartData? Chart { get; set; }
+
         public Pagination Pagination { get; set; }
     }
 }

+ 11 - 0
Views/Component/Chart.cshtml

@@ -0,0 +1,11 @@
+@model economy.Models.ChartData
+
+@if (Model != null && Model.Labels.Count > 1 && Model.Series.Count > 0)
+{
+    <div class="mt-3" style="position: relative; height: 240px;">
+        <canvas id="priceChart"></canvas>
+    </div>
+    <script type="application/json" id="priceChartData">@Json.Serialize(Model)</script>
+    <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
+    <script src="~/js/price-chart.js" asp-append-version="true"></script>
+}

+ 2 - 0
Views/Price/Domestic/Emission.cshtml

@@ -26,6 +26,8 @@
         </div>
     </div>
 
+    @await Html.PartialAsync("~/Views/Component/Chart.cshtml", Model.Chart)
+
     <div class="table-responsive">
         <table id="emissionPriceInfo" class="table table-bordered table-hover">
             <caption class="caption-top">

+ 2 - 0
Views/Price/Domestic/Gold.cshtml

@@ -26,6 +26,8 @@
         </div>
     </div>
 
+    @await Html.PartialAsync("~/Views/Component/Chart.cshtml", Model.Chart)
+
     <div class="table-responsive">
         <table id="goldPriceInfo" class="table table-bordered table-hover">
             <caption class="caption-top">

+ 2 - 0
Views/Price/Domestic/Oil.cshtml

@@ -26,6 +26,8 @@
         </div>
     </div>
 
+    @await Html.PartialAsync("~/Views/Component/Chart.cshtml", Model.Chart)
+
     <div class="table-responsive">
         <table id="oilPriceInfo" class="table table-bordered table-hover">
             <caption class="caption-top">

+ 2 - 0
Views/Price/Global/Commodity.cshtml

@@ -27,6 +27,8 @@
         </div>
     </div>
 
+    @await Html.PartialAsync("~/Views/Component/Chart.cshtml", Model.Chart)
+
     <div class="table-responsive">
         <table id="globalPriceList" class="table table-bordered table-hover">
             <caption class="caption-top">

+ 2 - 0
Views/Price/Global/NaturalGas.cshtml

@@ -20,6 +20,8 @@
         </div>
     </div>
 
+    @await Html.PartialAsync("~/Views/Component/Chart.cshtml", Model.Chart)
+
     <div class="table-responsive">
         <table id="globalPriceList" class="table table-bordered table-hover">
             <caption class="caption-top">

+ 37 - 0
wwwroot/js/price-chart.js

@@ -0,0 +1,37 @@
+class PriceChart {
+    constructor() {
+        const el = document.getElementById("priceChartData");
+        if (!el || typeof Chart === "undefined") {
+            return;
+        }
+
+        const data = JSON.parse(el.textContent);
+
+        new Chart(document.getElementById("priceChart"), {
+            type: "line",
+            data: {
+                labels: data.labels,
+                datasets: data.series.map((s) => ({
+                    label: s.name,
+                    data: s.values,
+                    borderWidth: 2,
+                    pointRadius: 2,
+                    tension: 0.2,
+                    spanGaps: true
+                }))
+            },
+            options: {
+                responsive: true,
+                maintainAspectRatio: false,
+                plugins: {
+                    legend: { display: data.series.length > 1 }
+                },
+                scales: {
+                    y: { beginAtZero: false }
+                }
+            }
+        });
+    }
+}
+
+const priceChart = new PriceChart();