feat(wbs-ux): implement real-world CRUD templates and refactor QuantDataGrid to AG Grid
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FastEndpoints;
|
||||
using Npgsql;
|
||||
using ExcelDataReader;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace QuantEngine.Web.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// BFF FastEndpoints for streaming Excel file upload and importing to PostgreSQL using COPY binary protocol.
|
||||
/// SOLID: Single Responsibility for streaming large files to prevent OOM.
|
||||
/// </summary>
|
||||
public class BulkInsertMarketExcelEndpoint : EndpointWithoutRequest
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
|
||||
public BulkInsertMarketExcelEndpoint(IConfiguration config)
|
||||
{
|
||||
_connectionString = config.GetConnectionString("DefaultConnection")
|
||||
?? throw new ArgumentNullException("ConnectionStrings__DefaultConnection");
|
||||
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/admin/market/upload-excel-stream");
|
||||
AllowFileUploads();
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
if (Files.Count == 0)
|
||||
{
|
||||
await SendAsync(new { success = false, message = "업로드된 파일이 없습니다." }, 400, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
var file = Files[0];
|
||||
using var fileStream = file.OpenReadStream();
|
||||
using var reader = ExcelReaderFactory.CreateReader(fileStream);
|
||||
|
||||
using var conn = new NpgsqlConnection(_connectionString);
|
||||
await conn.OpenAsync(ct);
|
||||
|
||||
using var writer = await conn.BeginBinaryImportAsync(
|
||||
"COPY quantengine.market_raw_history (ticker, as_of_date, close_price, nav_price, disparate_ratio, raw_payload, provenance) FROM STDIN (FORMAT BINARY)",
|
||||
ct
|
||||
);
|
||||
|
||||
bool isHeader = true;
|
||||
int processedRows = 0;
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
if (isHeader)
|
||||
{
|
||||
isHeader = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
string ticker = reader.GetValue(0)?.ToString() ?? string.Empty;
|
||||
string asOfDate = reader.GetValue(1)?.ToString() ?? string.Empty;
|
||||
decimal closePrice = Convert.ToDecimal(reader.GetValue(2) ?? 0);
|
||||
decimal navPrice = Convert.ToDecimal(reader.GetValue(3) ?? 0);
|
||||
decimal disparateRatio = navPrice > 0 ? (closePrice - navPrice) / navPrice : 0;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ticker) || ticker.Length < 6) continue;
|
||||
|
||||
await writer.StartRowAsync(ct);
|
||||
await writer.WriteAsync(ticker, ct);
|
||||
await writer.WriteAsync(asOfDate, ct);
|
||||
await writer.WriteAsync(closePrice, ct);
|
||||
await writer.WriteAsync(navPrice, ct);
|
||||
await writer.WriteAsync(disparateRatio, ct);
|
||||
await writer.WriteAsync("{}", ct);
|
||||
await writer.WriteAsync("{\"source\": \"excel_stream_uploader\"}", ct);
|
||||
|
||||
processedRows++;
|
||||
}
|
||||
|
||||
await writer.CompleteAsync(ct);
|
||||
await SendAsync(new { success = true, count = processedRows, message = "성공적으로 스트리밍 적재 완료되었습니다." }, cancellation: ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FastEndpoints;
|
||||
using Npgsql;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace QuantEngine.Web.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// BFF FastEndpoints for downloading large factor output data using sequential data reader streams.
|
||||
/// SOLID: Single Responsibility for streaming CSV reports.
|
||||
/// </summary>
|
||||
public class ExportStreamingFactorOlapEndpoint : EndpointWithoutRequest
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
|
||||
public ExportStreamingFactorOlapEndpoint(IConfiguration config)
|
||||
{
|
||||
_connectionString = config.GetConnectionString("DefaultConnection")
|
||||
?? throw new ArgumentNullException("ConnectionStrings__DefaultConnection");
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/admin/reports/export-factor-olap-stream");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
HttpContext.Response.ContentType = "text/csv";
|
||||
// ASP0019 대응: Headers.Append 또는 인덱서 사용
|
||||
HttpContext.Response.Headers.Append("Content-Disposition", $"attachment; filename=Streaming_Factor_Report_{DateTime.Now:yyyyMMdd}.csv");
|
||||
|
||||
using var conn = new NpgsqlConnection(_connectionString);
|
||||
await conn.OpenAsync(ct);
|
||||
|
||||
using var cmd = new NpgsqlCommand(@"
|
||||
SELECT ticker, as_of_date, factor_id, score, calculation_state
|
||||
FROM quantengine.factor_output_history
|
||||
ORDER BY as_of_date DESC;", conn);
|
||||
|
||||
using var reader = await cmd.ExecuteReaderAsync(System.Data.CommandBehavior.SequentialAccess, ct);
|
||||
using var writer = new StreamWriter(HttpContext.Response.Body, System.Text.Encoding.UTF8);
|
||||
|
||||
await writer.WriteLineAsync("Ticker,AsOfDate,FactorId,Score,State");
|
||||
|
||||
while (await reader.ReadAsync(ct))
|
||||
{
|
||||
string ticker = reader.GetString(0);
|
||||
string asOfDate = reader.GetString(1);
|
||||
string factorId = reader.GetString(2);
|
||||
decimal score = reader.GetDecimal(3);
|
||||
string state = reader.GetString(4);
|
||||
|
||||
await writer.WriteLineAsync($"{ticker},{asOfDate},{factorId},{score},{state}");
|
||||
}
|
||||
|
||||
await writer.FlushAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FastEndpoints;
|
||||
using Dapper;
|
||||
using Npgsql;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace QuantEngine.Web.Endpoints;
|
||||
|
||||
public record UpdateThresholdRequest(string FactorId, string CalibrationState, string ThresholdParamsJson);
|
||||
public record UpdateThresholdResponse(bool Success, string Message);
|
||||
|
||||
/// <summary>
|
||||
/// BFF FastEndpoints for updating factor threshold parameter logic.
|
||||
/// SOLID: Single Responsibility for updating factor settings.
|
||||
/// </summary>
|
||||
public class UpdateFactorThresholdEndpoint : Endpoint<UpdateThresholdRequest, UpdateThresholdResponse>
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
|
||||
public UpdateFactorThresholdEndpoint(IConfiguration config)
|
||||
{
|
||||
_connectionString = config.GetConnectionString("DefaultConnection")
|
||||
?? throw new ArgumentNullException("ConnectionStrings__DefaultConnection");
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/admin/factors/update-threshold");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(UpdateThresholdRequest req, CancellationToken ct)
|
||||
{
|
||||
using var conn = new NpgsqlConnection(_connectionString);
|
||||
await conn.OpenAsync(ct);
|
||||
|
||||
const string sql = @"
|
||||
UPDATE quantengine.factor_version_history
|
||||
SET calibration_state = @CalibrationState,
|
||||
threshold_params = @ThresholdParams::jsonb,
|
||||
updated_at = NOW()
|
||||
WHERE factor_id = @FactorId;";
|
||||
|
||||
int affectedRows = await conn.ExecuteAsync(sql, new {
|
||||
req.FactorId,
|
||||
req.CalibrationState,
|
||||
ThresholdParams = req.ThresholdParamsJson
|
||||
});
|
||||
|
||||
if (affectedRows > 0)
|
||||
{
|
||||
await SendAsync(new UpdateThresholdResponse(true, "성공적으로 반영되었습니다."), cancellation: ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SendAsync(new UpdateThresholdResponse(false, "해당 Factor ID를 찾을 수 없습니다."), statusCode: 404, cancellation: ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EPPlus" Version="8.6.3" />
|
||||
<PackageReference Include="ExcelDataReader" Version="3.9.0" />
|
||||
<PackageReference Include="FastEndpoints" Version="5.34.0" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.23" />
|
||||
<PackageReference Include="Hangfire.Core" Version="1.8.23" />
|
||||
|
||||
Reference in New Issue
Block a user