diff --git a/src/Core/Domain/Entities/ExperionEntities.cs b/src/Core/Domain/Entities/ExperionEntities.cs
index d172c4c..7d28fca 100644
--- a/src/Core/Domain/Entities/ExperionEntities.cs
+++ b/src/Core/Domain/Entities/ExperionEntities.cs
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations.Schema;
+using Microsoft.EntityFrameworkCore;
namespace ExperionCrawler.Core.Domain.Entities;
@@ -7,6 +8,7 @@ public class ExperionTag
{
public int Id { get; set; }
public string NodeId { get; set; } = string.Empty;
+ public string TagName { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string? Value { get; set; }
public string? DataType { get; set; }
@@ -94,3 +96,32 @@ public class ExperionStatusCodeInfo
public ulong Decimal { get; set; }
public string Description { get; set; } = string.Empty;
}
+
+/// fastSession — 데이터 수집 세션 메타
+[Table("fast_session")]
+public class FastSession
+{
+ [Column("id")] public int Id { get; set; }
+ [Column("name")] public string Name { get; set; } = string.Empty;
+ [Column("started_at")] public DateTime StartedAt { get; set; }
+ [Column("ended_at")] public DateTime? EndedAt { get; set; }
+ [Column("status")] public string Status { get; set; } = "Pending";
+ // Status 허용값: Pending / Running / Completed / Cancelled / Failed / RowLimitReached
+ [Column("sampling_ms")] public int SamplingMs { get; set; }
+ [Column("duration_sec")] public int DurationSec { get; set; }
+ [Column("tag_list")] public string TagList { get; set; } = "[]"; // JSONB → string[] 직렬화
+ [Column("row_count")] public int RowCount { get; set; }
+ [Column("retention_days")] public int? RetentionDays { get; set; } // null = 무한 보관
+ [Column("pinned")] public bool Pinned { get; set; }
+}
+
+/// fastRecord — 시계열 데이터 (Long 포맷: 태그 1행/시점)
+[Table("fast_record")]
+public class FastRecord
+{
+ [NotMapped] public int Id { get; set; }
+ [Column("session_id")] public int SessionId { get; set; }
+ [Column("recorded_at")] public DateTime RecordedAt { get; set; }
+ [Column("tagname")] public string TagName { get; set; } = string.Empty;
+ [Column("value")] public string? Value { get; set; }
+}
diff --git a/src/Infrastructure/Database/ExperionDbContext.cs b/src/Infrastructure/Database/ExperionDbContext.cs
index 557b0f8..f353dd6 100644
--- a/src/Infrastructure/Database/ExperionDbContext.cs
+++ b/src/Infrastructure/Database/ExperionDbContext.cs
@@ -68,9 +68,8 @@ public class ExperionDbContext : DbContext
modelBuilder.Entity(e =>
{
- e.HasKey(x => x.Id);
+ e.HasKey(x => new { x.SessionId, x.RecordedAt, x.TagName });
e.HasIndex(x => x.SessionId);
- e.HasIndex(x => new { x.SessionId, x.TagName, x.RecordedAt });
});
}
}
@@ -113,17 +112,17 @@ public class ExperionDbService : IExperionDbService
await _ctx.Database.ExecuteSqlRawAsync("""
CREATE TABLE IF NOT EXISTS fast_record (
- id SERIAL PRIMARY KEY,
session_id INTEGER NOT NULL REFERENCES fast_session(id) ON DELETE CASCADE,
recorded_at TIMESTAMPTZ NOT NULL,
tagname TEXT NOT NULL,
- value TEXT
+ value TEXT,
+ PRIMARY KEY (session_id, recorded_at, tagname)
)
""");
// TimescaleDB hypertable 생성 (recorded_at 기준, chunk_interval = 1 day)
await _ctx.Database.ExecuteSqlRawAsync("""
- SELECT create_hypertable('fast_record', 'recorded_at', if_not_exists => TRUE)
+ SELECT create_hypertable('fast_record', 'recorded_at', if_not_exists => TRUE, migrate_data => TRUE)
""");
await _ctx.Database.ExecuteSqlRawAsync("""