using Hc900Crawler.Core.Application.Interfaces; using Hc900Crawler.Core.Domain.Entities; using Hc900Crawler.Infrastructure.Database; using Hc900Crawler.Infrastructure.Hc900; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace Hc900Crawler.Web.Controllers; // ── Gateway 상태 / 제어 ────────────────────────────────────────────────────── [ApiController] [Route("api/gateway")] public class GatewayController : ControllerBase { private readonly ControllerGrpcClientPool _clientPool; private readonly Hc900WriteService _write; private readonly Hc900RealtimeService _realtime; public GatewayController(ControllerGrpcClientPool clientPool, Hc900WriteService write, Hc900RealtimeService realtime) { _clientPool = clientPool; _write = write; _realtime = realtime; } /// gRPC Gateway 헬스체크 [HttpGet("health")] public async Task Health([FromQuery] string? controllerId = null) { try { var id = controllerId ?? _clientPool.EnabledControllerIds.FirstOrDefault() ?? "HC1"; var client = _clientPool.GetClient(id); var h = await client.HealthCheckAsync(new Hc900.Gateway.HealthCheckRequest()); return Ok(new { controllerId = id, status = h.Status.ToString(), controllerIp = h.ControllerIp, activeTags = h.ActiveTags, pollCount = h.PollCount, lastPollMs = h.LastPollMs, uptimeSec = h.UptimeSec }); } catch (Exception ex) { return Ok(new { status = "NOT_SERVING", error = ex.Message }); } } /// 실시간 수집 서비스 상태 [HttpGet("status")] public IActionResult Status() => Ok(new { isConnected = _realtime.IsConnected, pollCount = _realtime.PollCount, lastPollAt = _realtime.LastPollAt, controllers = _realtime.ControllerConnected, }); /// 레지스터 태그 목록 조회 [HttpGet("tags")] public async Task ListTags([FromQuery] string? controllerId = null, [FromQuery] string filter = "", [FromQuery] int limit = 100) { var id = controllerId ?? _clientPool.EnabledControllerIds.FirstOrDefault() ?? "HC1"; var client = _clientPool.GetClient(id); var resp = await client.ListTagsAsync(new Hc900.Gateway.ListTagsRequest { Filter = filter, Limit = limit }); return Ok(resp.Tags.Select(t => new { tagName = t.TagName, address = t.Address, count = t.Count, type = t.Type, access = t.Access, description = t.Description, eu = t.Eu })); } /// 태그 쓰기 [HttpPost("write")] public async Task Write([FromBody] WriteTagDto dto) { var (success, error) = await _write.WriteTagAsync(dto.ControllerId, dto.TagName, dto.Value); return Ok(new { success, error }); } } public class WriteTagDto { public string ControllerId { get; set; } = "HC1"; public string TagName { get; set; } = ""; public double Value { get; set; } } // ── Realtime Points ─────────────────────────────────────────────────────────── [ApiController] [Route("api/realtime")] public class RealtimeController : ControllerBase { private readonly IExperionDbService _db; public RealtimeController(IExperionDbService db) => _db = db; /// 전체 realtime 포인트 조회 (선택적 controllerId 필터) [HttpGet("points")] public async Task GetPoints([FromQuery] string? controllerId = null) { var points = await _db.GetRealtimePointsAsync(); if (!string.IsNullOrEmpty(controllerId)) points = points.Where(p => p.ControllerId == controllerId); return Ok(points); } [HttpDelete("points/{id}")] public async Task DeletePoint(int id, [FromQuery] bool purgeHistory = false) { var result = await _db.DeleteRealtimePointAsync(id, purgeHistory); return Ok(result); } } // ── History ─────────────────────────────────────────────────────────────────── [ApiController] [Route("api/history")] public class HistoryController : ControllerBase { private readonly IExperionDbService _db; public HistoryController(IExperionDbService db) => _db = db; [HttpPost("query")] public async Task Query([FromBody] HistoryQueryDto dto) { var result = await _db.QueryHistoryAsync(dto.TagNames, dto.From, dto.To, dto.Limit); return Ok(result); } [HttpPost("query-interval")] public async Task QueryInterval([FromBody] Hc900Crawler.Core.Application.Interfaces.HistoryIntervalQueryRequest req) { var result = await _db.QueryHistoryWithIntervalAsync(req); return Ok(result); } [HttpGet("tags")] public async Task GetTagNames() { var tags = await _db.GetTagNamesAsync(); return Ok(tags); } } public class HistoryQueryDto { public string? ControllerId { get; set; } public List TagNames { get; set; } = new(); public DateTime? From { get; set; } public DateTime? To { get; set; } public int Limit { get; set; } = 1000; } // ── Tag Metadata ────────────────────────────────────────────────────────────── [ApiController] [Route("api/metadata")] public class MetadataController : ControllerBase { private readonly IExperionDbService _db; public MetadataController(IExperionDbService db) => _db = db; [HttpGet] public async Task Get([FromQuery] string? baseTag = null) { using var scope = HttpContext.RequestServices.CreateScope(); var ctx = scope.ServiceProvider.GetRequiredService(); var q = ctx.TagMetadata.AsQueryable(); if (!string.IsNullOrEmpty(baseTag)) q = q.Where(x => x.BaseTag == baseTag); return Ok(await q.ToListAsync()); } } // ── Event History ───────────────────────────────────────────────────────────── [ApiController] [Route("api/events")] public class EventHistoryController : ControllerBase { private readonly IExperionDbService _db; public EventHistoryController(IExperionDbService db) => _db = db; [HttpPost("query")] public async Task Query([FromBody] EventQueryDto dto) { var result = await _db.QueryEventHistoryAsync( dto.TagName, dto.Area, dto.SubArea, dto.EventType, dto.From ?? DateTime.UtcNow.AddDays(-1), dto.To ?? DateTime.UtcNow, dto.Limit); return Ok(result); } } public class EventQueryDto { public string? ControllerId { get; set; } public string? TagName { get; set; } public string? Area { get; set; } public string? SubArea { get; set; } public string? EventType { get; set; } public DateTime? From { get; set; } public DateTime? To { get; set; } public int Limit { get; set; } = 500; } // ── HC900 Tag Manager ───────────────────────────────────────────────────────── [ApiController] [Route("api/hc900/tags")] public class Hc900TagManagerController : ControllerBase { private readonly Hc900DbContext _ctx; public Hc900TagManagerController(Hc900DbContext ctx) => _ctx = ctx; [HttpGet] public async Task Get( [FromQuery] string? controllerId = null, [FromQuery] string? paramType = null, [FromQuery] int? loopNo = null, [FromQuery] bool? active = null, [FromQuery] string? search = null) { var q = _ctx.Hc900MapEntries.AsQueryable(); if (!string.IsNullOrEmpty(controllerId)) q = q.Where(x => x.ControllerId == controllerId); if (active.HasValue) q = q.Where(x => x.IsActive == active.Value); if (!string.IsNullOrEmpty(paramType)) q = q.Where(x => x.ParamType == paramType); if (loopNo.HasValue) q = q.Where(x => x.LoopNo == loopNo.Value); if (!string.IsNullOrEmpty(search)) q = q.Where(x => x.TagName.Contains(search) || x.Hc900Tag.Contains(search)); var entries = await q.ToListAsync(); entries.Sort((a, b) => { var ca = char.ToUpper(a.TagName[0]); var cb = char.ToUpper(b.TagName[0]); if (ca != cb) return ca.CompareTo(cb); var dotA = a.TagName.LastIndexOf('.'); var dotB = b.TagName.LastIndexOf('.'); var baseA = dotA >= 0 ? a.TagName[..dotA] : a.TagName; var baseB = dotB >= 0 ? b.TagName[..dotB] : b.TagName; var propA = dotA >= 0 ? a.TagName[(dotA + 1)..] : ""; var propB = dotB >= 0 ? b.TagName[(dotB + 1)..] : ""; var cmp = string.Compare(baseA, baseB, StringComparison.OrdinalIgnoreCase); if (cmp != 0) return cmp; return string.Compare(propA, propB, StringComparison.OrdinalIgnoreCase); }); var tagNames = entries.Select(e => e.TagName).ToList(); var liveValues = await _ctx.RealtimePoints .Where(r => tagNames.Contains(r.TagName)) .ToDictionaryAsync(r => r.TagName, r => new { r.LiveValue, r.Timestamp }); var result = entries.Select(e => { liveValues.TryGetValue(e.TagName, out var live); return new { e.Id, e.TagName, e.Hc900Tag, e.ControllerId, ModbusAddrHex = $"0x{e.ModbusAddr:X4}", e.ModbusAddr, e.DataType, e.Access, e.LoopNo, e.ParamType, e.IsActive, LiveValue = live?.LiveValue, Timestamp = live?.Timestamp, }; }); return Ok(result); } [HttpPut("{id}/active")] public async Task SetActive(int id, [FromBody] bool active) { var entry = await _ctx.Hc900MapEntries.FindAsync(id); if (entry == null) return NotFound(); entry.IsActive = active; await _ctx.SaveChangesAsync(); return Ok(new { entry.Id, entry.TagName, entry.IsActive }); } [HttpPost("bulk-active")] public async Task BulkSetActive([FromBody] BulkActiveDto dto) { var q = _ctx.Hc900MapEntries.AsQueryable(); if (!string.IsNullOrEmpty(dto.ParamType)) q = q.Where(x => x.ParamType == dto.ParamType); if (dto.LoopNo.HasValue) q = q.Where(x => x.LoopNo == dto.LoopNo.Value); if (dto.Ids?.Any() == true) q = q.Where(x => dto.Ids.Contains(x.Id)); var count = await q.ExecuteUpdateAsync(s => s.SetProperty(x => x.IsActive, dto.Active)); return Ok(new { updated = count, active = dto.Active }); } [HttpGet("param-types")] public async Task GetParamTypes() { var types = await _ctx.Hc900MapEntries .Where(x => x.ParamType != null) .Select(x => x.ParamType!) .Distinct() .OrderBy(x => x) .ToListAsync(); return Ok(types); } [HttpPut("{id}/controller")] public async Task SetController(int id, [FromBody] string controllerId) { var entry = await _ctx.Hc900MapEntries.FindAsync(id); if (entry == null) return NotFound(); entry.ControllerId = controllerId; await _ctx.SaveChangesAsync(); return Ok(new { entry.Id, entry.ControllerId }); } [HttpGet("controller-ids")] public async Task GetControllerIds() { var ids = await _ctx.Hc900MapEntries .Select(x => x.ControllerId) .Distinct() .OrderBy(x => x) .ToListAsync(); return Ok(ids); } [HttpGet("summary")] public async Task GetSummary() { var total = await _ctx.Hc900MapEntries.CountAsync(); var active = await _ctx.Hc900MapEntries.CountAsync(x => x.IsActive); var byType = await _ctx.Hc900MapEntries .GroupBy(x => x.ParamType) .Select(g => new { paramType = g.Key, total = g.Count(), active = g.Count(x => x.IsActive) }) .OrderBy(x => x.paramType) .ToListAsync(); var liveCount = await _ctx.RealtimePoints.CountAsync(); return Ok(new { total, active, inactive = total - active, liveCount, byType }); } } public class BulkActiveDto { public bool Active { get; set; } public string? ParamType { get; set; } public int? LoopNo { get; set; } public List? Ids { get; set; } } // ── Sub-Area ────────────────────────────────────────────────────────────────── [ApiController] [Route("api/subarea")] public class SubAreaController : ControllerBase { private readonly IExperionDbService _db; public SubAreaController(IExperionDbService db) => _db = db; [HttpGet("{area}")] public async Task Get(string area, [FromQuery] int page = 1, [FromQuery] int pageSize = 50) { var (tags, total) = await _db.GetSubAreaListByAreaAsync(area, page, pageSize); return Ok(new { total, tags }); } [HttpPut("{baseTag}")] public async Task Update(string baseTag, [FromBody] string? subArea) { var ok = await _db.UpdateSubAreaAsync(baseTag, subArea); return Ok(new { success = ok }); } [HttpPost("seed")] public async Task Seed([FromQuery] bool dryRun = true) { var result = await _db.SeedSubAreaAsync(dryRun); return Ok(result); } }