Files
ExperionCrawler/fastTable/step6.md
2026-04-30 08:16:21 +09:00

173 lines
6.7 KiB
Markdown

# 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<FastSession> 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<FastSession?> GetFastSessionAsync(int sessionId)
=> await _ctx.FastSessions.FindAsync(sessionId);
public async Task<IEnumerable<FastSession>> 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<IEnumerable<FastSession>> 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<FastQueryResult> 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<FastRecord> 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<string?> 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개 모두 구현됨