using Npgsql; using OpcUaManager.Models; namespace OpcUaManager.Services; /// /// PostgreSQL opc_history 테이블에 대한 읽기/쓰기를 담당합니다. /// 원본 SaveToDatabase 함수의 INSERT 패턴을 그대로 유지하며, /// OPC 태그 읽기(OpcSessionService)와 DB 저장을 조율합니다. /// public class DatabaseService { private readonly ILogger _logger; private readonly OpcSessionService _sessionSvc; public DatabaseService(ILogger logger, OpcSessionService sessionSvc) { _logger = logger; _sessionSvc = sessionSvc; } // ── 연결 문자열 빌더 ────────────────────────────────────────────── private static string BuildConnString(DbWriteRequest req) => $"Host={req.DbHost};Username={req.DbUser};Password={req.DbPassword};Database={req.DbName}"; // ── DB/테이블 초기화 (최초 한 번) ──────────────────────────────── /// /// opc_history 테이블이 없으면 자동 생성합니다. /// 원본 코드에는 없었지만, 프로젝트화 시 처음 실행 환경을 위해 추가합니다. /// public async Task<(bool Ok, string Msg)> EnsureTableAsync(DbWriteRequest req) { const string ddl = """ CREATE TABLE IF NOT EXISTS opc_history ( id BIGSERIAL PRIMARY KEY, tag_name TEXT NOT NULL, tag_value DOUBLE PRECISION NOT NULL, status_code TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); """; try { await using var conn = new NpgsqlConnection(BuildConnString(req)); await conn.OpenAsync(); await using var cmd = new NpgsqlCommand(ddl, conn); await cmd.ExecuteNonQueryAsync(); return (true, "테이블 확인/생성 완료"); } catch (Exception ex) { _logger.LogError(ex, "테이블 초기화 실패"); return (false, ex.Message); } } // ── OPC 읽기 + DB 저장 루프 ────────────────────────────────────── /// /// OPC 노드를 req.Count 회 읽고 각 결과를 DB에 저장합니다. /// 원본 for 루프 + SaveToDatabase 패턴을 그대로 유지합니다. /// public async Task WriteLoopAsync(DbWriteRequest req) { if (!_sessionSvc.IsConnected) return new DbWriteResult { Success = false, Message = "OPC 세션이 연결되어 있지 않습니다." }; var (tableOk, tableMsg) = await EnsureTableAsync(req); if (!tableOk) return new DbWriteResult { Success = false, Message = $"테이블 오류: {tableMsg}" }; var records = new List(); int saved = 0; for (int i = 1; i <= req.Count; i++) { // ── 1. OPC 태그 읽기 ──────────────────────────────────── double val = 0; string status = "Unknown"; bool dbSaved = false; try { var (rawVal, sc) = await _sessionSvc.ReadValueAsync(req.TagNodeId); val = Convert.ToDouble(rawVal); status = sc; _logger.LogInformation("[{I}/{N}] {Tag} = {Val} ({Status})", i, req.Count, req.TagName, val, status); } catch (Exception ex) { status = $"ReadError: {ex.Message}"; _logger.LogWarning("[{I}/{N}] OPC 읽기 실패: {Msg}", i, req.Count, ex.Message); } // ── 2. DB 저장 (원본 SaveToDatabase 로직) ─────────────── try { await SaveToDatabaseAsync(req, val, status); dbSaved = true; saved++; _logger.LogInformation("[{I}/{N}] DB 저장 완료.", i, req.Count); } catch (Exception ex) { _logger.LogError(ex, "[{I}/{N}] DB 저장 실패", i, req.Count); } records.Add(new DbWriteRecord { Seq = i, Timestamp = DateTime.UtcNow, TagName = req.TagName, Value = val, StatusCode = status, DbSaved = dbSaved }); // ── 3. 인터벌 대기 (마지막 회차는 생략) ───────────────── if (i < req.Count && req.IntervalMs > 0) await Task.Delay(req.IntervalMs); } return new DbWriteResult { Success = saved > 0, Message = $"{saved}/{req.Count}회 저장 완료", SavedCount = saved, Records = records }; } // ── 원본 SaveToDatabase (INSERT 패턴 동일) ──────────────────────── private async Task SaveToDatabaseAsync(DbWriteRequest req, double val, string status) { await using var conn = new NpgsqlConnection(BuildConnString(req)); await conn.OpenAsync(); // 원본 동일 INSERT const string sql = "INSERT INTO opc_history (tag_name, tag_value, status_code) " + "VALUES (@tag, @val, @status)"; await using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("tag", req.TagName); cmd.Parameters.AddWithValue("val", val); cmd.Parameters.AddWithValue("status", status); await cmd.ExecuteNonQueryAsync(); } // ── DB 조회 ─────────────────────────────────────────────────────── public async Task QueryRecentAsync(DbWriteRequest req, int limit = 100) { try { await using var conn = new NpgsqlConnection(BuildConnString(req)); await conn.OpenAsync(); string sql = $""" SELECT id, tag_name, tag_value, status_code, created_at FROM opc_history ORDER BY id DESC LIMIT {limit} """; await using var cmd = new NpgsqlCommand(sql, conn); await using var reader = await cmd.ExecuteReaderAsync(); var rows = new List(); while (await reader.ReadAsync()) { rows.Add(new OpcHistoryRow { Id = reader.GetInt64(0), TagName = reader.GetString(1), TagValue = reader.GetDouble(2), StatusCode = reader.GetString(3), CreatedAt = reader.GetDateTime(4) }); } // 전체 카운트 await reader.CloseAsync(); await using var cntCmd = new NpgsqlCommand("SELECT COUNT(*) FROM opc_history", conn); long total = (long)(await cntCmd.ExecuteScalarAsync() ?? 0L); return new DbQueryResult { Success = true, TotalCount = (int)total, Rows = rows }; } catch (Exception ex) { _logger.LogError(ex, "DB 조회 실패"); return new DbQueryResult { Success = false, Message = ex.Message }; } } // ── 연결 테스트 ─────────────────────────────────────────────────── public async Task<(bool Ok, string Msg)> TestConnectionAsync(DbWriteRequest req) { try { await using var conn = new NpgsqlConnection(BuildConnString(req)); await conn.OpenAsync(); await using var cmd = new NpgsqlCommand("SELECT version()", conn); var ver = await cmd.ExecuteScalarAsync(); return (true, $"연결 성공: {ver}"); } catch (Exception ex) { return (false, ex.Message); } } }