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 _logger; public List TableList { get; set; } = new(); public string? SelectedTable { get; set; } public List ColumnNames { get; set; } = new(); public List> 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 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 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(); var parameters = new List(); 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 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(); var paramNames = new List(); var parameters = new List(); 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(); 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}"; } } } }