ExperionCrawler First Commit

This commit is contained in:
windpacer
2026-04-14 04:02:43 +00:00
commit 323aec34af
158 changed files with 539535 additions and 0 deletions

View File

@@ -0,0 +1,187 @@
using ExperionCrawler.Core.Application.Interfaces;
using ExperionCrawler.Core.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace ExperionCrawler.Infrastructure.Database;
// ── DbContext ────────────────────────────────────────────────────────────────
public class ExperionDbContext : DbContext
{
public ExperionDbContext(DbContextOptions<ExperionDbContext> options) : base(options) { }
public DbSet<ExperionRecord> ExperionRecords => Set<ExperionRecord>();
public DbSet<RawNodeMap> RawNodeMaps => Set<RawNodeMap>();
public DbSet<NodeMapMaster> NodeMapMasters => Set<NodeMapMaster>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ExperionRecord>(e =>
{
e.HasKey(x => x.Id);
e.HasIndex(x => x.CollectedAt);
e.HasIndex(x => x.NodeId);
e.HasIndex(x => x.SessionId);
});
modelBuilder.Entity<RawNodeMap>(e =>
{
e.HasKey(x => x.Id);
e.HasIndex(x => x.NodeId);
});
modelBuilder.Entity<NodeMapMaster>(e =>
{
e.HasKey(x => x.Id);
e.HasIndex(x => x.NodeId);
e.HasIndex(x => x.Level);
});
}
}
// ── Service ──────────────────────────────────────────────────────────────────
public class ExperionDbService : IExperionDbService
{
private readonly ExperionDbContext _ctx;
private readonly ILogger<ExperionDbService> _logger;
public ExperionDbService(ExperionDbContext ctx, ILogger<ExperionDbService> logger)
{
_ctx = ctx;
_logger = logger;
}
public async Task<bool> InitializeAsync()
{
try
{
await _ctx.Database.EnsureCreatedAsync();
// EnsureCreatedAsync는 기존 DB에 새 테이블을 추가하지 않으므로
// raw_node_map / node_map_master 는 DDL로 직접 보장
await _ctx.Database.ExecuteSqlRawAsync("""
CREATE TABLE IF NOT EXISTS raw_node_map (
id SERIAL PRIMARY KEY,
level INTEGER NOT NULL,
class TEXT NOT NULL,
name TEXT NOT NULL,
node_id TEXT NOT NULL,
data_type TEXT NOT NULL
)
""");
await _ctx.Database.ExecuteSqlRawAsync("""
CREATE TABLE IF NOT EXISTS node_map_master (
id SERIAL PRIMARY KEY,
level INTEGER NOT NULL,
class TEXT NOT NULL,
name TEXT NOT NULL,
node_id TEXT NOT NULL,
data_type TEXT NOT NULL
)
""");
_logger.LogInformation("[ExperionDb] 데이터베이스 초기화 완료");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "[ExperionDb] 초기화 실패");
return false;
}
}
public async Task<int> SaveRecordsAsync(IEnumerable<ExperionRecord> records)
{
var list = records.ToList();
await _ctx.ExperionRecords.AddRangeAsync(list);
var saved = await _ctx.SaveChangesAsync();
_logger.LogInformation("[ExperionDb] {Count}건 저장", saved);
return saved;
}
public async Task<int> ClearRecordsAsync()
{
var deleted = await _ctx.ExperionRecords.ExecuteDeleteAsync();
_logger.LogInformation("[ExperionDb] {Count}건 삭제 (초기화)", deleted);
return deleted;
}
public async Task<int> BuildMasterFromRawAsync(bool truncate = false)
{
if (truncate)
{
await _ctx.Database.ExecuteSqlRawAsync(
"TRUNCATE TABLE node_map_master RESTART IDENTITY");
_logger.LogInformation("[ExperionDb] node_map_master 초기화 완료");
}
var inserted = await _ctx.Database.ExecuteSqlRawAsync(
"INSERT INTO node_map_master (level, class, name, node_id, data_type) " +
"SELECT level, class, name, node_id, data_type FROM raw_node_map");
_logger.LogInformation("[ExperionDb] node_map_master 빌드 완료: {Count}건", inserted);
return inserted;
}
public async Task<IEnumerable<ExperionRecord>> GetRecordsAsync(
DateTime? from = null, DateTime? to = null, int limit = 1000)
{
var q = _ctx.ExperionRecords.AsQueryable();
if (from.HasValue) q = q.Where(r => r.CollectedAt >= from.Value);
if (to.HasValue) q = q.Where(r => r.CollectedAt <= to.Value);
return await q.OrderByDescending(r => r.CollectedAt).Take(limit).ToListAsync();
}
public async Task<int> GetTotalCountAsync()
=> await _ctx.ExperionRecords.CountAsync();
public async Task<IEnumerable<string>> GetNameListAsync()
{
return await _ctx.NodeMapMasters
.Select(x => x.Name).Distinct()
.OrderBy(x => x).ToListAsync();
}
public async Task<NodeMapStats> GetMasterStatsAsync()
{
if (!await _ctx.NodeMapMasters.AnyAsync())
return new NodeMapStats(0, 0, 0, 0, Enumerable.Empty<string>());
var total = await _ctx.NodeMapMasters.CountAsync();
var objectCount = await _ctx.NodeMapMasters.CountAsync(x => x.Class == "Object");
var variableCount = await _ctx.NodeMapMasters.CountAsync(x => x.Class == "Variable");
var maxLevel = await _ctx.NodeMapMasters.MaxAsync(x => (int?)x.Level) ?? 0;
var dataTypes = await _ctx.NodeMapMasters
.Select(x => x.DataType).Distinct()
.OrderBy(x => x).ToListAsync();
_logger.LogInformation("[ExperionDb] 노드맵 통계: total={Total}", total);
return new NodeMapStats(total, objectCount, variableCount, maxLevel, dataTypes);
}
public async Task<NodeMapQueryResult> QueryMasterAsync(
int? minLevel, int? maxLevel, string? nodeClass,
IEnumerable<string>? names, string? nodeId, string? dataType,
int limit, int offset)
{
var q = _ctx.NodeMapMasters.AsQueryable();
if (minLevel.HasValue) q = q.Where(x => x.Level >= minLevel.Value);
if (maxLevel.HasValue) q = q.Where(x => x.Level <= maxLevel.Value);
if (!string.IsNullOrEmpty(nodeClass)) q = q.Where(x => x.Class == nodeClass);
var nameList = names?.Where(n => !string.IsNullOrEmpty(n)).ToList();
if (nameList?.Count > 0) q = q.Where(x => nameList.Contains(x.Name));
if (!string.IsNullOrEmpty(nodeId)) q = q.Where(x => x.NodeId.Contains(nodeId));
if (!string.IsNullOrEmpty(dataType)) q = q.Where(x => x.DataType == dataType);
var total = await q.CountAsync();
var items = await q.OrderBy(x => x.Level).ThenBy(x => x.Name)
.Skip(offset).Take(Math.Min(limit, 500))
.ToListAsync();
return new NodeMapQueryResult(total, items);
}
}