Files
ExperionCrawler/fastTable/step6.md

6.7 KiB

STEP 6 — DB 서비스 구현: FastSession/FastRecord 메서드 추가

사전 확인 (작업 전 반드시 수행)

  1. src/Infrastructure/Database/ExperionDbContext.cs 파일을 열어 ExperionDbService 클래스를 찾는다.
  2. 아래 항목을 확인하고 기록한다:
    • STEP 5가 완료되어 IExperionDbService에 Fast 메서드가 선언되어 있는가?
    • ExperionDbServiceIExperionDbService를 구현하는가?
    • 파일 상단에 using System.Text.Json; import가 있는가? → 없으면 추가
    • CreateFastSessionAsync 구현이 이미 있는가? → 있으면 해당 메서드 건너뜀
    • BatchInsertFastRecordsAsync 구현이 이미 있는가?
    • ExportFastRecordsToCsvAsync 구현이 이미 있는가?

작업 내용

파일: src/Infrastructure/Database/ExperionDbContext.cs
위치: ExperionDbService 클래스 내부 마지막 메서드 아래

// ── 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. 아래 항목을 하나씩 확인한다:
    • CreateFastSessionAsyncJsonSerializer.Serialize(request.TagList) 사용하는가?
    • UpdateFastSessionStatusAsyncEndedAt 자동 설정 로직이 있는가?
    • GetExpiredFastSessionsAsync!x.Pinned 조건이 있는가?
    • GetFastRecordsAsync — 반환 타입이 FastQueryResult인가?
    • BatchInsertFastRecordsAsync — 빈 리스트 early return이 있는가?
    • ExportFastRecordsToCsvAsync — PIVOT 그룹핑 로직이 있는가?
    • GetNodeIdByTagNameAsync_ctx.RealtimePoints 에서 조회하는가?
    • 파일 상단에 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개 모두 구현됨