Files
QuantEngineByItz/src/dotnet/QuantEngine.Web/Pages/Admin/Database/Index.cshtml.cs
T
kjh2064 1c192ecdea
Validators (Pushes and Pull Requests) / validate-ui-and-storage (push) Successful in 15s
Validators (Pushes and Pull Requests) / validate-core (push) Successful in 1m46s
feat(web): add DB Table Management admin page supporting dynamic view and CRUD row modification
2026-07-12 14:24:36 +09:00

271 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using Npgsql;
using QuantEngine.Infrastructure.Data;
namespace QuantEngine.Web.Pages.Admin.Database
{
public class IndexModel : PageModel
{
private readonly IDbConnectionFactory _connectionFactory;
private readonly ILogger<IndexModel> _logger;
public List<string> TableList { get; set; } = new();
public string? SelectedTable { get; set; }
public List<string> ColumnNames { get; set; } = new();
public List<Dictionary<string, object>> Rows { get; set; } = new();
public string? PrimaryKeyColumn { get; set; }
[BindProperty]
public string? ActionTableName { get; set; }
[BindProperty]
public string? ActionRowKey { get; set; }
public string? ErrorMessage { get; set; }
public string? SuccessMessage { get; set; }
public IndexModel(IDbConnectionFactory connectionFactory, ILogger<IndexModel> logger)
{
_connectionFactory = connectionFactory;
_logger = logger;
}
public async Task OnGetAsync(string? tableName)
{
await LoadTableListAsync();
if (!string.IsNullOrEmpty(tableName))
{
// Validate table name is in whitelist to prevent SQL Injection
if (TableList.Contains(tableName, StringComparer.OrdinalIgnoreCase))
{
SelectedTable = tableName;
await LoadTableDataAsync(tableName);
}
else
{
ErrorMessage = "허용되지 않은 테이블명입니다.";
}
}
}
public async Task<IActionResult> OnPostSaveRowAsync()
{
await LoadTableListAsync();
var tableName = Request.Form["tableName"].ToString();
var pkColumn = Request.Form["pkColumn"].ToString();
var pkValue = Request.Form["pkValue"].ToString();
if (string.IsNullOrEmpty(tableName) || !TableList.Contains(tableName, StringComparer.OrdinalIgnoreCase))
{
ErrorMessage = "유효하지 않은 테이블입니다.";
return Page();
}
try
{
using var conn = _connectionFactory.CreateConnection();
if (conn.State != ConnectionState.Open) conn.Open();
// Load target columns to update
var columns = new List<string>();
var parameters = new List<NpgsqlParameter>();
foreach (var key in Request.Form.Keys)
{
if (key == "tableName" || key == "pkColumn" || key == "pkValue" || key == "__RequestVerificationToken")
continue;
var val = Request.Form[key].ToString();
columns.Add($"\"{key}\" = @{key}");
var param = new NpgsqlParameter($"@{key}", NpgsqlTypes.NpgsqlDbType.Text);
param.Value = (object?)val ?? DBNull.Value;
parameters.Add(param);
}
if (columns.Count > 0 && !string.IsNullOrEmpty(pkColumn))
{
var sql = $"UPDATE {tableName} SET {string.Join(", ", columns)} WHERE \"{pkColumn}\" = @pk_val";
using var cmd = new NpgsqlCommand(sql, (NpgsqlConnection)conn);
foreach (var p in parameters) cmd.Parameters.Add(p);
var pkParam = new NpgsqlParameter("@pk_val", NpgsqlTypes.NpgsqlDbType.Text);
pkParam.Value = pkValue;
cmd.Parameters.Add(pkParam);
await cmd.ExecuteNonQueryAsync();
SuccessMessage = "행 데이터가 성공적으로 수정되었습니다.";
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update row for {Table}", tableName);
ErrorMessage = $"저장 실패: {ex.Message}";
}
return RedirectToPage(new { tableName });
}
public async Task<IActionResult> OnPostAddRowAsync()
{
await LoadTableListAsync();
var tableName = Request.Form["tableName"].ToString();
if (string.IsNullOrEmpty(tableName) || !TableList.Contains(tableName, StringComparer.OrdinalIgnoreCase))
{
ErrorMessage = "유효하지 않은 테이블입니다.";
return Page();
}
try
{
using var conn = _connectionFactory.CreateConnection();
if (conn.State != ConnectionState.Open) conn.Open();
var colNames = new List<string>();
var paramNames = new List<string>();
var parameters = new List<NpgsqlParameter>();
foreach (var key in Request.Form.Keys)
{
if (key == "tableName" || key == "__RequestVerificationToken")
continue;
var val = Request.Form[key].ToString();
colNames.Add($"\"{key}\"");
paramNames.Add($"@{key}");
var param = new NpgsqlParameter($"@{key}", NpgsqlTypes.NpgsqlDbType.Text);
param.Value = (object?)val ?? DBNull.Value;
parameters.Add(param);
}
if (colNames.Count > 0)
{
var sql = $"INSERT INTO {tableName} ({string.Join(", ", colNames)}) VALUES ({string.Join(", ", paramNames)})";
using var cmd = new NpgsqlCommand(sql, (NpgsqlConnection)conn);
foreach (var p in parameters) cmd.Parameters.Add(p);
await cmd.ExecuteNonQueryAsync();
SuccessMessage = "새 데이터 행이 성공적으로 추가되었습니다.";
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to insert row for {Table}", tableName);
ErrorMessage = $"추가 실패: {ex.Message}";
}
return RedirectToPage(new { tableName });
}
private async Task LoadTableListAsync()
{
TableList.Clear();
try
{
using var conn = _connectionFactory.CreateConnection();
if (conn.State != ConnectionState.Open) conn.Open();
var sql = @"
SELECT table_schema || '.' || table_name AS full_name
FROM information_schema.tables
WHERE table_schema IN ('quantengine', 'engine_history')
AND table_type = 'BASE TABLE'
ORDER BY table_schema, table_name;";
using var cmd = new NpgsqlCommand(sql, (NpgsqlConnection)conn);
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
TableList.Add(reader.GetString(0));
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load database table list.");
}
}
private async Task LoadTableDataAsync(string tableName)
{
ColumnNames.Clear();
Rows.Clear();
PrimaryKeyColumn = null;
try
{
using var conn = _connectionFactory.CreateConnection();
if (conn.State != ConnectionState.Open) conn.Open();
var parts = tableName.Split('.');
var schema = parts[0];
var tableOnly = parts[1];
var pkSql = @"
SELECT a.attname
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = @table_name::regclass
AND i.indisprimary;";
using (var pkCmd = new NpgsqlCommand(pkSql, (NpgsqlConnection)conn))
{
pkCmd.Parameters.AddWithValue("@table_name", tableName);
try
{
var pkResult = await pkCmd.ExecuteScalarAsync();
if (pkResult != null) PrimaryKeyColumn = pkResult.ToString();
}
catch { }
}
if (string.IsNullOrEmpty(PrimaryKeyColumn))
{
if (tableName.Contains("settings")) PrimaryKeyColumn = "key";
else if (tableName.Contains("workspace_account")) PrimaryKeyColumn = "username";
else if (tableName.Contains("collection_runs")) PrimaryKeyColumn = "run_id";
else if (tableName.Contains("workspace_meta")) PrimaryKeyColumn = "key";
}
var dataSql = $"SELECT * FROM {tableName} LIMIT 100;";
using var cmd = new NpgsqlCommand(dataSql, (NpgsqlConnection)conn);
using var reader = await cmd.ExecuteReaderAsync();
for (int i = 0; i < reader.FieldCount; i++)
{
ColumnNames.Add(reader.GetName(i));
}
while (await reader.ReadAsync())
{
var row = new Dictionary<string, object>();
for (int i = 0; i < reader.FieldCount; i++)
{
var val = reader.GetValue(i);
row[reader.GetName(i)] = val == DBNull.Value ? "null" : val;
}
Rows.Add(row);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load table data for {Table}", tableName);
ErrorMessage = $"테이블 데이터 조회 실패: {ex.Message}";
}
}
}
}