# STEP 6 — DB 서비스 구현: FastSession/FastRecord 메서드 추가 ## 사전 확인 (작업 전 반드시 수행) 1. `src/Infrastructure/Database/ExperionDbContext.cs` 파일을 열어 `ExperionDbService` 클래스를 찾는다. 2. 아래 항목을 확인하고 기록한다: - [x] STEP 5가 완료되어 `IExperionDbService`에 Fast 메서드가 선언되어 있는가? - [x] `ExperionDbService`가 `IExperionDbService`를 구현하는가? - [x] 파일 상단에 `using System.Text.Json;` import가 있는가? → 없으면 추가 - [x] `CreateFastSessionAsync` 구현이 이미 있는가? → 있으면 해당 메서드 건너뜀 - [x] `BatchInsertFastRecordsAsync` 구현이 이미 있는가? - [x] `ExportFastRecordsToCsvAsync` 구현이 이미 있는가? --- ## 작업 내용 **파일**: `src/Infrastructure/Database/ExperionDbContext.cs` **위치**: `ExperionDbService` 클래스 내부 마지막 메서드 아래 ```csharp // ── FastSession / FastRecord ───────────────────────────────────────────────── public async Task CreateFastSessionAsync(FastSessionCreateRequest request) { var session = new FastSession { Name = request.Name, SamplingMs = request.SamplingMs, DurationSec = request.DurationSec, TagList = JsonSerializer.Serialize(request.TagList), // string[] → JSONB StartedAt = DateTime.UtcNow, Status = "Pending", RowCount = 0, RetentionDays = request.RetentionDays, Pinned = false }; _ctx.FastSessions.Add(session); await _ctx.SaveChangesAsync(); return session; } public async Task UpdateFastSessionStatusAsync(int sessionId, string status) { var session = await _ctx.FastSessions.FindAsync(sessionId); if (session == null) return; session.Status = status; if (status is "Completed" or "Cancelled" or "Failed" or "RowLimitReached") session.EndedAt = DateTime.UtcNow; await _ctx.SaveChangesAsync(); } public async Task UpdateFastSessionRowCountAsync(int sessionId, int rowCount) { var session = await _ctx.FastSessions.FindAsync(sessionId); if (session == null) return; session.RowCount = rowCount; await _ctx.SaveChangesAsync(); } public async Task UpdateFastSessionPinnedAsync(int sessionId, bool pinned) { var session = await _ctx.FastSessions.FindAsync(sessionId); if (session == null) return; session.Pinned = pinned; await _ctx.SaveChangesAsync(); } public async Task GetFastSessionAsync(int sessionId) => await _ctx.FastSessions.FindAsync(sessionId); public async Task> GetFastSessionsAsync() => await _ctx.FastSessions.OrderBy(x => x.StartedAt).ToListAsync(); public async Task DeleteFastSessionAsync(int sessionId) { var session = await _ctx.FastSessions.FindAsync(sessionId); if (session == null) return; _ctx.FastSessions.Remove(session); await _ctx.SaveChangesAsync(); } public async Task> GetExpiredFastSessionsAsync() { var now = DateTime.UtcNow; return await _ctx.FastSessions .Where(x => x.EndedAt != null && !x.Pinned && x.RetentionDays.HasValue && x.EndedAt.Value.AddDays(x.RetentionDays.Value) < now) .OrderBy(x => x.EndedAt) .ToListAsync(); } public async Task GetFastRecordsAsync(int sessionId, DateTime? from, DateTime? to) { var query = _ctx.FastRecords.Where(x => x.SessionId == sessionId); if (from.HasValue) query = query.Where(x => x.RecordedAt >= from.Value); if (to.HasValue) query = query.Where(x => x.RecordedAt <= to.Value); var records = await query.OrderBy(x => x.RecordedAt).ToListAsync(); var tagNames = records.Select(x => x.TagName).Distinct().OrderBy(x => x).ToArray(); return new FastQueryResult( SessionId: sessionId, From: from ?? records.MinBy(x => x.RecordedAt)?.RecordedAt ?? DateTime.UtcNow, To: to ?? records.MaxBy(x => x.RecordedAt)?.RecordedAt ?? DateTime.UtcNow, TagNames: tagNames, Items: records, TotalCount: records.Count ); } public async Task BatchInsertFastRecordsAsync(IEnumerable records) { var list = records.ToList(); if (list.Count == 0) return; _ctx.FastRecords.AddRange(list); await _ctx.SaveChangesAsync(); } public async Task ExportFastRecordsToCsvAsync(int sessionId, Stream stream, DateTime? from, DateTime? to) { var result = await GetFastRecordsAsync(sessionId, from, to); using var writer = new StreamWriter(stream, leaveOpen: true); var header = "recorded_at," + string.Join(",", result.TagNames.Select(t => $"\"{t}\"")); await writer.WriteLineAsync(header); var grouped = result.Items .GroupBy(x => x.RecordedAt) .OrderBy(x => x.Key) .Select(g => new { Time = g.Key, Values = g.ToDictionary(r => r.TagName, r => r.Value) }); foreach (var g in grouped) { var row = g.Time.ToString("o") + "," + string.Join(",", result.TagNames.Select(t => g.Values.TryGetValue(t, out var v) ? $"\"{v}\"" : "")); await writer.WriteLineAsync(row); } await writer.FlushAsync(); } public async Task GetNodeIdByTagNameAsync(string tagName) => await _ctx.RealtimePoints .Where(x => x.TagName == tagName) .Select(x => x.NodeId) .FirstOrDefaultAsync(); ``` --- ## 사후 확인 (작업 후 반드시 수행) 1. `ExperionDbContext.cs` 파일을 다시 열어 추가된 메서드 목록을 읽는다. 2. 아래 항목을 하나씩 확인한다: - [x] `CreateFastSessionAsync` — `JsonSerializer.Serialize(request.TagList)` 사용하는가? - [x] `UpdateFastSessionStatusAsync` — `EndedAt` 자동 설정 로직이 있는가? - [x] `GetExpiredFastSessionsAsync` — `!x.Pinned` 조건이 있는가? - [x] `GetFastRecordsAsync` — 반환 타입이 `FastQueryResult`인가? - [x] `BatchInsertFastRecordsAsync` — 빈 리스트 early return이 있는가? - [x] `ExportFastRecordsToCsvAsync` — PIVOT 그룹핑 로직이 있는가? - [x] `GetNodeIdByTagNameAsync` — `_ctx.RealtimePoints` 에서 조회하는가? - [x] 파일 상단에 `using System.Text.Json;` 가 있는가? 3. `dotnet build src/Web` 실행 → 에러 2개 (ExperionOpcClient 구현 미완료, STEP 7에서 해결) 4. ExperionOpcClient 구현 에러는 예상된 결과 (인터페이스만 추가한 단계) --- ## 완료 조건 - `dotnet build src/Web` 결과: 에러 2개 (ExperionOpcClient 구현 미완료, STEP 7에서 해결) - Fast 관련 DB 메서드 12개 모두 구현됨