271 lines
10 KiB
C#
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}";
|
|
}
|
|
}
|
|
}
|
|
}
|