From f97be981a437c2ffd13e433b9507b310f1e1b35a Mon Sep 17 00:00:00 2001 From: windpacer Date: Fri, 12 Jun 2026 12:31:14 +0900 Subject: [PATCH] =?UTF-8?q?style(api):=20JSON=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20PascalCase=20=ED=86=B5=EC=9D=BC=20(?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=C2=B7DTO=C2=B7JS)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C# 익명객체 응답 속성을 camelCase→PascalCase로 통일(key→Key, success→Success, error→Error 등)하고, 프런트 JS의 응답 접근도 맞춰 변경(ramp.jobs→ramp.Jobs, data.columns→data.Columns 등). Kb/Pid/Steam/Feedforward/Ollama/PointBuilder 등 전 컨트롤러와 대응 JS, ExperionDtos·TrendDtos, McpService 반영. Co-Authored-By: Claude Opus 4.8 --- src/Core/Application/DTOs/ExperionDtos.cs | 24 +-- src/Core/Application/DTOs/TrendDtos.cs | 70 +++---- .../Application/Interfaces/IMcpService.cs | 1 + .../Controllers/DocsController.cs | 50 ++--- .../Controllers/FastController.cs | 14 +- .../Controllers/FeedforwardController.cs | 102 ++++----- .../Controllers/Hc900Controllers.cs | 22 +- .../Controllers/HypertableController.cs | 2 +- .../Controllers/KbAuthController.cs | 16 +- src/Hc900Crawler/Controllers/KbController.cs | 150 +++++++------- .../Controllers/OllamaController.cs | 137 ++++++++++--- src/Hc900Crawler/Controllers/PidController.cs | 188 ++++++++--------- .../Controllers/PidGraphController.cs | 42 ++-- .../Controllers/PointBuilderController.cs | 64 +++--- .../Controllers/SetupController.cs | 20 +- .../Controllers/SteamAdvisorController.cs | 194 +++++++++--------- .../Controllers/TextToSqlController.cs | 4 +- .../Controllers/TrendController.cs | 28 +-- src/Hc900Crawler/wwwroot/js/docs.js | 34 +-- src/Hc900Crawler/wwwroot/js/fast.js | 22 +- src/Hc900Crawler/wwwroot/js/ff.js | 18 +- src/Hc900Crawler/wwwroot/js/hist.js | 10 +- src/Hc900Crawler/wwwroot/js/kbadmin.js | 74 +++---- src/Hc900Crawler/wwwroot/js/llmchat.js | 63 +++--- src/Hc900Crawler/wwwroot/js/pb.js | 64 +++--- src/Hc900Crawler/wwwroot/js/pid-viewer.js | 28 +-- src/Hc900Crawler/wwwroot/js/pid.js | 122 +++++------ src/Hc900Crawler/wwwroot/js/setup.js | 26 +-- src/Hc900Crawler/wwwroot/js/steam.js | 160 +++++++-------- src/Hc900Crawler/wwwroot/js/trend.js | 26 +-- src/Hc900Crawler/wwwroot/js/write.js | 6 +- src/Infrastructure/Mcp/McpService.cs | 2 +- 32 files changed, 935 insertions(+), 848 deletions(-) diff --git a/src/Core/Application/DTOs/ExperionDtos.cs b/src/Core/Application/DTOs/ExperionDtos.cs index 4de68e8..b18e888 100644 --- a/src/Core/Application/DTOs/ExperionDtos.cs +++ b/src/Core/Application/DTOs/ExperionDtos.cs @@ -114,22 +114,22 @@ public class Hc900PointBuilderBuildDto public class Hc900PointBuilderPreviewItem { - [JsonPropertyName("tagName")] public string TagName { get; set; } = ""; - [JsonPropertyName("hc900Tag")] public string Hc900Tag { get; set; } = ""; - [JsonPropertyName("modbusAddr")] public int ModbusAddr { get; set; } - [JsonPropertyName("paramType")] public string ParamType { get; set; } = ""; - [JsonPropertyName("dataType")] public string DataType { get; set; } = ""; - [JsonPropertyName("loopNo")] public int? LoopNo { get; set; } - [JsonPropertyName("access")] public string Access { get; set; } = "R"; - [JsonPropertyName("controllerId")]public string ControllerId { get; set; } = "HC1"; - [JsonPropertyName("group")] public string Group { get; set; } = ""; - [JsonPropertyName("isActive")] public bool IsActive { get; set; } + public string TagName { get; set; } = ""; + public string Hc900Tag { get; set; } = ""; + public int ModbusAddr { get; set; } + public string ParamType { get; set; } = ""; + public string DataType { get; set; } = ""; + public int? LoopNo { get; set; } + public string Access { get; set; } = "R"; + public string ControllerId { get; set; } = "HC1"; + public string Group { get; set; } = ""; + public bool IsActive { get; set; } } public class Hc900PointBuilderPreviewResult { - [JsonPropertyName("count")] public int Count { get; set; } - [JsonPropertyName("items")] public List Items { get; set; } = new(); + public int Count { get; set; } + public List Items { get; set; } = new(); } public class Hc900PointBuilderApplyDto diff --git a/src/Core/Application/DTOs/TrendDtos.cs b/src/Core/Application/DTOs/TrendDtos.cs index a3c42ad..d421701 100644 --- a/src/Core/Application/DTOs/TrendDtos.cs +++ b/src/Core/Application/DTOs/TrendDtos.cs @@ -2,74 +2,70 @@ using System.Text.Json.Serialization; namespace Hc900Crawler.Core.Application.DTOs; -// 이 프로젝트의 기본 JSON 직렬화는 PascalCase 패스스루다(기존 컨트롤러는 익명객체로 직접 camelCase 명명). -// 트렌드 DTO는 프론트(camelCase) 계약에 맞추기 위해 [JsonPropertyName]으로 camelCase 고정. -// (입력 바인딩은 MVC 기본 대소문자 무시라 무관, 출력 일관성 목적) - /// 트렌드 그룹 멤버 — 태그 + 색상 + Y축(좌/우) public class TrendMemberDto { - [JsonPropertyName("tag")] public string Tag { get; set; } = ""; - [JsonPropertyName("color")] public string Color { get; set; } = ""; // #rrggbb - [JsonPropertyName("axis")] public string Axis { get; set; } = "left"; // left | right + public string Tag { get; set; } = ""; + public string Color { get; set; } = ""; // #rrggbb + public string Axis { get; set; } = "left"; // left | right } /// 트렌드 그룹 (멤버는 JSONB 저장) public class TrendGroupDto { - [JsonPropertyName("id")] public int Id { get; set; } - [JsonPropertyName("name")] public string Name { get; set; } = ""; - [JsonPropertyName("description")] public string? Description { get; set; } - [JsonPropertyName("members")] public List Members { get; set; } = new(); - [JsonPropertyName("createdAt")] public DateTime CreatedAt { get; set; } - [JsonPropertyName("updatedAt")] public DateTime UpdatedAt { get; set; } + public int Id { get; set; } + public string Name { get; set; } = ""; + public string? Description { get; set; } + public List Members { get; set; } = new(); + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } } /// 그룹 생성/수정 요청 public class TrendGroupUpsertDto { - [JsonPropertyName("name")] public string Name { get; set; } = ""; - [JsonPropertyName("description")] public string? Description { get; set; } - [JsonPropertyName("members")] public List Members { get; set; } = new(); + public string Name { get; set; } = ""; + public string? Description { get; set; } + public List Members { get; set; } = new(); } /// 아날로그 포인트 (그룹 빌더용 — 숫자 livevalue 한정) public class AnalogPointDto { - [JsonPropertyName("tagName")] public string TagName { get; set; } = ""; - [JsonPropertyName("baseTag")] public string BaseTag { get; set; } = ""; - [JsonPropertyName("value")] public double? Value { get; set; } - [JsonPropertyName("timestamp")] public DateTime Timestamp { get; set; } - [JsonPropertyName("unit")] public string? Unit { get; set; } - [JsonPropertyName("euLo")] public double? EuLo { get; set; } - [JsonPropertyName("euHi")] public double? EuHi { get; set; } - [JsonPropertyName("description")] public string? Description { get; set; } - [JsonPropertyName("area")] public string? Area { get; set; } + public string TagName { get; set; } = ""; + public string BaseTag { get; set; } = ""; + public double? Value { get; set; } + public DateTime Timestamp { get; set; } + public string? Unit { get; set; } + public double? EuLo { get; set; } + public double? EuHi { get; set; } + public string? Description { get; set; } + public string? Area { get; set; } } /// 실시간 tail 포인트 (현재값) public class TrendLivePointDto { - [JsonPropertyName("tag")] public string Tag { get; set; } = ""; - [JsonPropertyName("value")] public double? Value { get; set; } - [JsonPropertyName("ts")] public DateTime Ts { get; set; } + public string Tag { get; set; } = ""; + public double? Value { get; set; } + public DateTime Ts { get; set; } } /// 알람 한계선 (HI/LO/SP) public class TrendLimitDto { - [JsonPropertyName("tag")] public string Tag { get; set; } = ""; - [JsonPropertyName("hi")] public double? Hi { get; set; } - [JsonPropertyName("lo")] public double? Lo { get; set; } - [JsonPropertyName("sp")] public double? Sp { get; set; } - [JsonPropertyName("unit")] public string? Unit { get; set; } + public string Tag { get; set; } = ""; + public double? Hi { get; set; } + public double? Lo { get; set; } + public double? Sp { get; set; } + public string? Unit { get; set; } } /// 운전상태 밴드 (RUN/TRIP 구간) public class TrendBandDto { - [JsonPropertyName("tag")] public string Tag { get; set; } = ""; - [JsonPropertyName("t0")] public DateTime T0 { get; set; } - [JsonPropertyName("t1")] public DateTime T1 { get; set; } - [JsonPropertyName("state")] public string State { get; set; } = ""; + public string Tag { get; set; } = ""; + public DateTime T0 { get; set; } + public DateTime T1 { get; set; } + public string State { get; set; } = ""; } diff --git a/src/Core/Application/Interfaces/IMcpService.cs b/src/Core/Application/Interfaces/IMcpService.cs index 0ed11e0..69aa475 100644 --- a/src/Core/Application/Interfaces/IMcpService.cs +++ b/src/Core/Application/Interfaces/IMcpService.cs @@ -55,6 +55,7 @@ public class McpToolDto { public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; + public System.Text.Json.JsonElement? InputSchema { get; set; } } /// diff --git a/src/Hc900Crawler/Controllers/DocsController.cs b/src/Hc900Crawler/Controllers/DocsController.cs index 9f233c2..4530970 100644 --- a/src/Hc900Crawler/Controllers/DocsController.cs +++ b/src/Hc900Crawler/Controllers/DocsController.cs @@ -26,18 +26,18 @@ public class DocsController : ControllerBase private Task IsAdminAsync(CancellationToken ct) => _auth.ValidateAsync(Request.Headers["X-Kb-Token"].ToString(), ct); - private IActionResult Fail(string error) => Ok(new { success = false, error }); + private IActionResult Fail(string error) => Ok(new { Success = false, Error = error }); // ── 메타/설정 ─────────────────────────────────────────────── [HttpGet("config")] public async Task Config(CancellationToken ct) => Ok(new { - success = true, - root = _docs.Root, - canManage = await IsAdminAsync(ct), - maxTextBytes = _docs.MaxTextBytes, - maxUploadBytes = _docs.MaxUploadBytes, + Success = true, + Root = _docs.Root, + CanManage = await IsAdminAsync(ct), + MaxTextBytes = _docs.MaxTextBytes, + MaxUploadBytes = _docs.MaxUploadBytes, }); // ── 디렉토리 목록 ─────────────────────────────────────────── @@ -48,14 +48,14 @@ public class DocsController : ControllerBase { var entries = _docs.List(path).Select(e => new { - name = e.Name, - path = e.RelPath, - type = e.IsDir ? "dir" : "file", - size = e.Size, - mtime = e.ModifiedUtc, - ext = e.Ext, + Name = e.Name, + Path = e.RelPath, + Type = e.IsDir ? "dir" : "file", + Size = e.Size, + Mtime = e.ModifiedUtc, + Ext = e.Ext, }); - return Ok(new { success = true, path = path ?? "", entries }); + return Ok(new { Success = true, Path = path ?? "", Entries = entries }); } catch (DocBrowserException ex) { return Fail(ex.Message); } } @@ -67,7 +67,7 @@ public class DocsController : ControllerBase try { var r = _docs.ReadText(path); - return Ok(new { success = true, text = r.Text, truncated = r.Truncated, size = r.Size, ext = r.Ext }); + return Ok(new { Success = true, Text = r.Text, Truncated = r.Truncated, Size = r.Size, Ext = r.Ext }); } catch (DocBrowserException ex) { return Fail(ex.Message); } } @@ -83,7 +83,7 @@ public class DocsController : ControllerBase return File(r.Stream, "application/octet-stream", r.FileName); return File(r.Stream, r.ContentType); // inline (pdf 등) } - catch (DocBrowserException ex) { return NotFound(new { success = false, error = ex.Message }); } + catch (DocBrowserException ex) { return NotFound(new { Success = false, Error = ex.Message }); } } // ── 텍스트 저장 (admin) ───────────────────────────────────── @@ -93,13 +93,13 @@ public class DocsController : ControllerBase [RequestSizeLimit(8_000_000)] public async Task WriteText([FromBody] WriteRequest req, CancellationToken ct) { - if (!await IsAdminAsync(ct)) return Unauthorized(new { success = false, error = "unauthorized" }); + if (!await IsAdminAsync(ct)) return Unauthorized(new { Success = false, Error = "unauthorized" }); if (req == null || string.IsNullOrEmpty(req.Path)) return Fail("path required"); try { _docs.WriteText(req.Path, req.Content ?? ""); _logger.LogInformation("[Docs] 저장 {Path}", req.Path); - return Ok(new { success = true }); + return Ok(new { Success = true }); } catch (DocBrowserException ex) { return Fail(ex.Message); } } @@ -110,14 +110,14 @@ public class DocsController : ControllerBase [HttpPost("rename")] public async Task Rename([FromBody] RenameRequest req, CancellationToken ct) { - if (!await IsAdminAsync(ct)) return Unauthorized(new { success = false, error = "unauthorized" }); + if (!await IsAdminAsync(ct)) return Unauthorized(new { Success = false, Error = "unauthorized" }); if (req == null || string.IsNullOrEmpty(req.From) || string.IsNullOrEmpty(req.To)) return Fail("from/to required"); try { var newRel = _docs.Rename(req.From, req.To); _logger.LogInformation("[Docs] 이름변경 {From} → {To}", req.From, newRel); - return Ok(new { success = true, path = newRel }); + return Ok(new { Success = true, Path = newRel }); } catch (DocBrowserException ex) { return Fail(ex.Message); } } @@ -128,12 +128,12 @@ public class DocsController : ControllerBase [HttpPost("mkdir")] public async Task Mkdir([FromBody] MkdirRequest req, CancellationToken ct) { - if (!await IsAdminAsync(ct)) return Unauthorized(new { success = false, error = "unauthorized" }); + if (!await IsAdminAsync(ct)) return Unauthorized(new { Success = false, Error = "unauthorized" }); if (req == null || string.IsNullOrEmpty(req.Path)) return Fail("path required"); try { var rel = _docs.MakeDir(req.Path); - return Ok(new { success = true, path = rel }); + return Ok(new { Success = true, Path = rel }); } catch (DocBrowserException ex) { return Fail(ex.Message); } } @@ -142,13 +142,13 @@ public class DocsController : ControllerBase [HttpDelete] public async Task Delete([FromQuery] string path, [FromQuery] bool recursive, CancellationToken ct) { - if (!await IsAdminAsync(ct)) return Unauthorized(new { success = false, error = "unauthorized" }); + if (!await IsAdminAsync(ct)) return Unauthorized(new { Success = false, Error = "unauthorized" }); if (string.IsNullOrEmpty(path)) return Fail("path required"); try { _docs.Delete(path, recursive); _logger.LogInformation("[Docs] 삭제 {Path} (recursive={R})", path, recursive); - return Ok(new { success = true }); + return Ok(new { Success = true }); } catch (DocBrowserException ex) { return Fail(ex.Message); } } @@ -158,14 +158,14 @@ public class DocsController : ControllerBase [RequestSizeLimit(100_000_000)] public async Task Upload([FromForm] IFormFile file, [FromForm] string? path, CancellationToken ct) { - if (!await IsAdminAsync(ct)) return Unauthorized(new { success = false, error = "unauthorized" }); + if (!await IsAdminAsync(ct)) return Unauthorized(new { Success = false, Error = "unauthorized" }); if (file == null || file.Length == 0) return Fail("file required"); try { await using var stream = file.OpenReadStream(); var rel = await _docs.SaveUploadAsync(path, file.FileName, stream, ct); _logger.LogInformation("[Docs] 업로드 {Path} ({Size} bytes)", rel, file.Length); - return Ok(new { success = true, path = rel }); + return Ok(new { Success = true, Path = rel }); } catch (DocBrowserException ex) { return Fail(ex.Message); } } diff --git a/src/Hc900Crawler/Controllers/FastController.cs b/src/Hc900Crawler/Controllers/FastController.cs index 4cf6bdc..9645bcc 100644 --- a/src/Hc900Crawler/Controllers/FastController.cs +++ b/src/Hc900Crawler/Controllers/FastController.cs @@ -31,7 +31,7 @@ public class FastController : ControllerBase ? Array.Empty() : System.Text.Json.JsonSerializer.Deserialize(s.TagList) ?? Array.Empty() }); - return Ok(new { items }); + return Ok(new { Items = items }); } [HttpGet("{id}")] @@ -58,7 +58,7 @@ public class FastController : ControllerBase var cts = new CancellationTokenSource(); lock (_sessions) { _sessions[session.Id] = cts; } _ = Task.Run(() => RunSessionAsync(session.Id, req, cts.Token)); - return Ok(new { session.Id, status = "Running" }); + return Ok(new { Id = session.Id, Status = "Running" }); } // 백그라운드 수집 루프. 요청 스코프는 응답 후 dispose되므로 매 반복 자체 스코프에서 DbContext를 새로 받는다. @@ -114,7 +114,7 @@ public class FastController : ControllerBase { lock (_sessions) { if (_sessions.TryGetValue(id, out var cts)) { cts.Cancel(); _sessions.Remove(id); } } await _db.UpdateFastSessionStatusAsync(id, "Stopped"); - return Ok(new { success = true }); + return Ok(new { Success = true }); } [HttpDelete("{id}")] @@ -122,14 +122,14 @@ public class FastController : ControllerBase { lock (_sessions) { if (_sessions.TryGetValue(id, out var cts)) { cts.Cancel(); _sessions.Remove(id); } } await _db.DeleteFastSessionAsync(id); - return Ok(new { success = true }); + return Ok(new { Success = true }); } [HttpPost("{id}/pin")] public async Task Pin(int id, [FromBody] bool pinned) { await _db.UpdateFastSessionPinnedAsync(id, pinned); - return Ok(new { success = true }); + return Ok(new { Success = true }); } [HttpGet("{id}/records")] @@ -139,8 +139,8 @@ public class FastController : ControllerBase return Ok(new { result.SessionId, result.From, result.To, - tagNames = result.TagNames, - items = result.Items.Select(r => new + TagNames = result.TagNames, + Items = result.Items.Select(r => new { r.SessionId, r.RecordedAt, r.TagName, r.Value }), diff --git a/src/Hc900Crawler/Controllers/FeedforwardController.cs b/src/Hc900Crawler/Controllers/FeedforwardController.cs index 16bfa00..e0c12e4 100644 --- a/src/Hc900Crawler/Controllers/FeedforwardController.cs +++ b/src/Hc900Crawler/Controllers/FeedforwardController.cs @@ -72,58 +72,58 @@ public sealed class FeedforwardController : ControllerBase public async Task GetConfig(CancellationToken ct) { var cols = await _config.LoadAllAsync(ct); - return Ok(new { columns = cols.Select(MapConfig) }); + return Ok(new { Columns = cols.Select(MapConfig) }); } [HttpPost("config")] public async Task SaveConfig([FromBody] ColumnConfig body, CancellationToken ct) { - if (!await AuthAsync(ct)) return Unauthorized(new { error = "X-Kb-Token 인증 필요" }); + if (!await AuthAsync(ct)) return Unauthorized(new { Error = "X-Kb-Token 인증 필요" }); var id = await _config.SaveColumnAsync(body, ct); - return Ok(new { success = true, id }); + return Ok(new { Success = true, Id = id }); } [HttpDelete("config/{id:int}")] public async Task DeleteConfig(int id, CancellationToken ct) { - if (!await AuthAsync(ct)) return Unauthorized(new { error = "X-Kb-Token 인증 필요" }); + if (!await AuthAsync(ct)) return Unauthorized(new { Error = "X-Kb-Token 인증 필요" }); await _config.DeleteColumnAsync(id, ct); - return Ok(new { success = true }); + return Ok(new { Success = true }); } // ── WO-6 전환류 ARM/취소 ── [HttpPost("recovery/{id:int}/arm")] - public IActionResult ArmRecovery(int id) => Ok(new { success = _supervisor.Arm(id) }); + public IActionResult ArmRecovery(int id) => Ok(new { Success = _supervisor.Arm(id) }); [HttpPost("recovery/{id:int}/cancel")] - public IActionResult CancelRecovery(int id) => Ok(new { success = _supervisor.Cancel(id) }); + public IActionResult CancelRecovery(int id) => Ok(new { Success = _supervisor.Cancel(id) }); // ── Phase II: 수동 SP 쓰기 ── [HttpPost("write/{columnId:int}/{streamKey}")] public async Task WriteSp(int columnId, string streamKey, [FromBody] WriteSpBody body, CancellationToken ct) { - if (!await AuthAsync(ct)) return Unauthorized(new { error = "X-Kb-Token 인증 필요" }); + if (!await AuthAsync(ct)) return Unauthorized(new { Error = "X-Kb-Token 인증 필요" }); var advisory = _store.Get(columnId); - if (advisory is null) return NotFound(new { error = "advisory 없음" }); + if (advisory is null) return NotFound(new { Error = "advisory 없음" }); var cfg = (await _config.LoadAllAsync(ct)).FirstOrDefault(c => c.Id == columnId); - if (cfg is null) return NotFound(new { error = "config 없음" }); + if (cfg is null) return NotFound(new { Error = "config 없음" }); var sc = cfg.Streams.FirstOrDefault(s => s.Key == streamKey); - if (sc is null) return NotFound(new { error = "stream 없음" }); + if (sc is null) return NotFound(new { Error = "stream 없음" }); // SP 쓰기 대상 = flow 태그에서 WSP(.SP, offset 0x04 RW) 자동 파생. SpNodeId는 선택적 override. var spTag = FfSpTag.Resolve(sc.FlowTag, sc.SpNodeId); if (string.IsNullOrWhiteSpace(spTag)) - return BadRequest(new { error = "flow 태그가 없어 SP 대상 산출 불가" }); + return BadRequest(new { Error = "flow 태그가 없어 SP 대상 산출 불가" }); var adv = advisory.Streams.FirstOrDefault(a => a.Key == streamKey); - if (adv is null) return NotFound(new { error = "stream advisory 없음" }); + if (adv is null) return NotFound(new { Error = "stream advisory 없음" }); double spVal = body.value ?? (adv.RecommendedSp ?? double.NaN); - if (double.IsNaN(spVal)) return BadRequest(new { error = "SP 값 없음" }); + if (double.IsNaN(spVal)) return BadRequest(new { Error = "SP 값 없음" }); // 범위 클램프(§3.3) 후 WriteGuard 검증 — manualOverride=true(AdvisoryOnly만 우회, 나머지 가드 유지) spVal = Math.Clamp(spVal, sc.SpMin, sc.SpMax); var check = _writeGuard.Check(cfg, adv, sc, advisory, manualOverride: true); if (!check.Allowed) - return BadRequest(new { error = $"WriteGuard 차단: {check.Reason}" }); + return BadRequest(new { Error = $"WriteGuard 차단: {check.Reason}" }); // 되돌리기용: 쓰기 전 현재 WSP 값을 캡처(realtime_table) double? prevSp = await TryReadCurrentAsync(spTag, ct); @@ -131,7 +131,7 @@ public sealed class FeedforwardController : ControllerBase // 컨트롤러는 태그→controller_id 매핑에서 해석(DB). 컬럼 ControllerId에 의존하지 않음. var ctrlId = await _db.GetControllerIdForTagAsync(spTag); if (string.IsNullOrWhiteSpace(ctrlId)) - return BadRequest(new { error = $"태그 {spTag}의 컨트롤러를 DB에서 찾지 못함(realtime 폴링 확인)" }); + return BadRequest(new { Error = $"태그 {spTag}의 컨트롤러를 DB에서 찾지 못함(realtime 폴링 확인)" }); // HC900 gRPC 쓰기 (WSP 태그, 해석된 컨트롤러로 라우팅) var (success, error) = await _writeClient.WriteTagAsync(ctrlId, spTag, spVal); @@ -143,9 +143,9 @@ public sealed class FeedforwardController : ControllerBase OperatorName: "manual"), ct); if (!success) - return StatusCode(502, new { error = $"HC900 쓰기 실패: {error}" }); + return StatusCode(502, new { Error = $"HC900 쓰기 실패: {error}" }); - return Ok(new { success = true, streamKey, nodeId = spTag, value = spVal, previousSp = prevSp }); + return Ok(new { Success = true, StreamKey = streamKey, NodeId = spTag, Value = spVal, PreviousSp = prevSp }); } // ── 측류 추종 ON/OFF/원복 ─────────────────────────────────────── @@ -154,12 +154,12 @@ public sealed class FeedforwardController : ControllerBase public async Task TrackOn(int columnId, string streamKey, CancellationToken ct) { var cfg = (await _config.LoadAllAsync(ct)).FirstOrDefault(c => c.Id == columnId); - if (cfg is null) return NotFound(new { error = "config 없음" }); + if (cfg is null) return NotFound(new { Error = "config 없음" }); var sc = cfg.Streams.FirstOrDefault(s => s.Key == streamKey); - if (sc is null) return NotFound(new { error = "stream 없음" }); - if (sc.Role == StreamRole.Monitor) return BadRequest(new { error = "Monitor 스트림은 추종 대상 아님" }); + if (sc is null) return NotFound(new { Error = "stream 없음" }); + if (sc.Role == StreamRole.Monitor) return BadRequest(new { Error = "Monitor 스트림은 추종 대상 아님" }); var spTag = FfSpTag.Resolve(sc.FlowTag, sc.SpNodeId); - if (string.IsNullOrWhiteSpace(spTag)) return BadRequest(new { error = "flow 태그 없음 — SP 대상 산출 불가" }); + if (string.IsNullOrWhiteSpace(spTag)) return BadRequest(new { Error = "flow 태그 없음 — SP 대상 산출 불가" }); _tracking.Set(new FfTrackingState { @@ -168,7 +168,7 @@ public sealed class FeedforwardController : ControllerBase }); await _audit.LogAsync(new FfActionLogEntry(columnId, "track_on", StreamKey: streamKey, NodeId: spTag, Result: "started", OperatorName: "manual"), ct); - return Ok(new { success = true, streamKey }); + return Ok(new { Success = true, StreamKey = streamKey }); } // OFF(=취소): 추종 중지. 컨트롤러는 마지막 값 유지. @@ -183,7 +183,7 @@ public sealed class FeedforwardController : ControllerBase }); await _audit.LogAsync(new FfActionLogEntry(columnId, "track_off", StreamKey: streamKey, Result: "stopped", OperatorName: "manual"), ct); - return Ok(new { success = true }); + return Ok(new { Success = true }); } // ── 모듈1 shadow 검증: 최근 shadow 데이터 조회 ── @@ -191,21 +191,21 @@ public sealed class FeedforwardController : ControllerBase public IActionResult GetShadow(int columnId, [FromQuery] int count = 100) { var entries = _shadow.GetRecent(columnId, count); - return Ok(new { columnId, count = entries.Count, entries }); + return Ok(new { ColumnId = columnId, Count = entries.Count, Entries = entries }); } [HttpPost("shadow/{columnId:int}/clear")] public IActionResult ClearShadow(int columnId) { _shadow.Clear(columnId); - return Ok(new { success = true }); + return Ok(new { Success = true }); } // ── Phase II: 감사 로그 조회 ── [HttpGet("audit")] public async Task GetAudit([FromQuery] int? columnId, [FromQuery] int limit = 50, CancellationToken ct = default) { - if (!await AuthAsync(ct)) return Unauthorized(new { error = "X-Kb-Token 인증 필요" }); + if (!await AuthAsync(ct)) return Unauthorized(new { Error = "X-Kb-Token 인증 필요" }); var rows = await _audit.QueryAsync(columnId, limit, ct); return Ok(new { rows }); } @@ -257,7 +257,7 @@ public sealed class FeedforwardController : ControllerBase [FromQuery] double n = 1.8, CancellationToken ct = default) { var a = await _ramp.ComputeAsync(columnId, targetFeed, deltaIAllow, sensibleGain, feedTempRef, floodLimit, n, ct); - if (a is null) return NotFound(new { error = "config 없음" }); + if (a is null) return NotFound(new { Error = "config 없음" }); return Ok(MapRamp(a)); } @@ -268,17 +268,17 @@ public sealed class FeedforwardController : ControllerBase public async Task StartFeedRamp(int columnId, [FromBody] FeedRampStartBody body, CancellationToken ct) { if (body is null || double.IsNaN(body.targetFeed) || double.IsInfinity(body.targetFeed)) - return BadRequest(new { error = "targetFeed 값 필요" }); + return BadRequest(new { Error = "targetFeed 값 필요" }); var cfg = (await _config.LoadAllAsync(ct)).FirstOrDefault(c => c.Id == columnId); - if (cfg is null) return NotFound(new { error = "config 없음" }); + if (cfg is null) return NotFound(new { Error = "config 없음" }); if (string.IsNullOrWhiteSpace(FfSpTag.Resolve(cfg.FeedTag, cfg.FeedSpNodeId))) - return BadRequest(new { error = "Feed 태그 없음 — FEED SP 대상 산출 불가" }); + return BadRequest(new { Error = "Feed 태그 없음 — FEED SP 대상 산출 불가" }); // 현재 피드 확인 + 업램프만 허용 var adv = await _ramp.ComputeAsync(columnId, body.targetFeed, 50, double.NaN, double.NaN, double.NaN, 1.8, ct); - if (adv is null) return NotFound(new { error = "config 없음" }); - if (adv.Hold) return BadRequest(new { error = $"피드 불량 — 시작 불가: {string.Join(", ", adv.Warnings)}" }); + if (adv is null) return NotFound(new { Error = "config 없음" }); + if (adv.Hold) return BadRequest(new { Error = $"피드 불량 — 시작 불가: {string.Join(", ", adv.Warnings)}" }); bool dryRun = RampDryRun() || _sim.Enabled; var job = _rampJobs.Start(columnId, body.targetFeed, "manual", dryRun); @@ -287,7 +287,7 @@ public sealed class FeedforwardController : ControllerBase SpValue: body.targetFeed, NodeId: cfg.FeedSpNodeId, Result: dryRun ? "started(dry-run)" : "started", OperatorName: "manual"), ct); - return Ok(new { success = true, dryRun, job = MapRampJob(job) }); + return Ok(new { Success = true, DryRun = dryRun, Job = MapRampJob(job) }); } [HttpPost("feed-ramp/{columnId:int}/cancel")] @@ -297,19 +297,19 @@ public sealed class FeedforwardController : ControllerBase if (ok) await _audit.LogAsync(new FfActionLogEntry(columnId, "feed_ramp_cancel", Result: "canceled", OperatorName: "manual"), ct); - return Ok(new { success = ok }); + return Ok(new { Success = ok }); } [HttpGet("feed-ramp/{columnId:int}")] public IActionResult GetFeedRamp(int columnId) { var job = _rampJobs.Get(columnId); - return Ok(new { dryRun = RampDryRun() || _sim.Enabled, job = job is null ? null : MapRampJob(job) }); + return Ok(new { DryRun = RampDryRun() || _sim.Enabled, Job = job is null ? null : MapRampJob(job) }); } [HttpGet("feed-ramp")] public IActionResult GetAllFeedRamp() - => Ok(new { dryRun = RampDryRun() || _sim.Enabled, jobs = _rampJobs.GetAll().Select(MapRampJob) }); + => Ok(new { DryRun = RampDryRun() || _sim.Enabled, Jobs = _rampJobs.GetAll().Select(MapRampJob) }); private static object MapRampJob(FeedRampJob j) => new { @@ -333,57 +333,57 @@ public sealed class FeedforwardController : ControllerBase [HttpGet("sim/override")] public IActionResult GetSimOverride() { - if (!SimEnabled()) return StatusCode(403, new { error = "SimOverride 비활성(Feedforward:SimOverrideEnabled=false)" }); - return Ok(new { enabled = _sim.Enabled, values = _sim.Snapshot() }); + if (!SimEnabled()) return StatusCode(403, new { Error = "SimOverride 비활성(Feedforward:SimOverrideEnabled=false)" }); + return Ok(new { Enabled = _sim.Enabled, Values = _sim.Snapshot() }); } [HttpPost("sim/override")] public IActionResult SetSimOverride([FromBody] SimOverrideBody body) { - if (!SimEnabled()) return StatusCode(403, new { error = "SimOverride 비활성" }); + if (!SimEnabled()) return StatusCode(403, new { Error = "SimOverride 비활성" }); _sim.SetMany(body.enabled, body.values ?? new Dictionary()); - return Ok(new { enabled = _sim.Enabled, values = _sim.Snapshot() }); + return Ok(new { Enabled = _sim.Enabled, Values = _sim.Snapshot() }); } [HttpDelete("sim/override")] public IActionResult ClearSimOverride() { - if (!SimEnabled()) return StatusCode(403, new { error = "SimOverride 비활성" }); + if (!SimEnabled()) return StatusCode(403, new { Error = "SimOverride 비활성" }); _sim.Clear(); - return Ok(new { enabled = _sim.Enabled }); + return Ok(new { Enabled = _sim.Enabled }); } // ── WP5 3단계: 조성 분율 수동입력(랩) ── [HttpGet("composition")] - public IActionResult GetComposition() => Ok(new { fractions = _composition.Snapshot() }); + public IActionResult GetComposition() => Ok(new { Fractions = _composition.Snapshot() }); [HttpPost("composition")] public IActionResult SetComposition([FromBody] CompositionBody b) { - if (b is null || string.IsNullOrWhiteSpace(b.streamKey)) return BadRequest(new { error = "streamKey 필요" }); + if (b is null || string.IsNullOrWhiteSpace(b.streamKey)) return BadRequest(new { Error = "streamKey 필요" }); _composition.Set(b.columnId, b.streamKey, b.fraction); - return Ok(new { fractions = _composition.Snapshot() }); + return Ok(new { Fractions = _composition.Snapshot() }); } [HttpDelete("composition/{columnId:int}/{streamKey}")] public IActionResult ClearComposition(int columnId, string streamKey) - { _composition.Clear(columnId, streamKey); return Ok(new { fractions = _composition.Snapshot() }); } + { _composition.Clear(columnId, streamKey); return Ok(new { Fractions = _composition.Snapshot() }); } private static double? Fin(double v) => (double.IsNaN(v) || double.IsInfinity(v)) ? (double?)null : v; private static object MapRamp(FeedRampAdvisory a) => new { columnId = a.ColumnId, currentFeed = Fin(a.CurrentFeed), targetFeed = Fin(a.TargetFeed), clampedTarget = Fin(a.ClampedTarget), - ceiling = new { value = Fin(a.Ceiling.Value), binding = a.Ceiling.Binding }, - rampRate = new { value = Fin(a.RampRate.Value), binding = a.RampRate.Binding }, + ceiling = new { Value = Fin(a.Ceiling.Value), Binding = a.Ceiling.Binding }, + rampRate = new { Value = Fin(a.RampRate.Value), Binding = a.RampRate.Binding }, rampTimeMin = Fin(a.RampTimeMin), - steam = new { fiq6115From = a.Steam.Fiq6115From, fiq6115To = a.Steam.Fiq6115To, startOpPct = a.Steam.StartOpPct }, + steam = new { Fiq6115From = a.Steam.Fiq6115From, Fiq6115To = a.Steam.Fiq6115To, StartOpPct = a.Steam.StartOpPct }, simOverrideActive = a.SimOverrideActive, hold = a.Hold, warnings = a.Warnings }; // ── Advisory (공개 읽기) ─────────────────────────────────────── [HttpGet("advisory")] - public IActionResult GetAll() => Ok(new { columns = _store.GetAll().Select(MapColumn) }); + public IActionResult GetAll() => Ok(new { Columns = _store.GetAll().Select(MapColumn) }); [HttpGet("advisory/{columnId:int}")] public IActionResult Get(int columnId) diff --git a/src/Hc900Crawler/Controllers/Hc900Controllers.cs b/src/Hc900Crawler/Controllers/Hc900Controllers.cs index 02ce935..0063dd6 100644 --- a/src/Hc900Crawler/Controllers/Hc900Controllers.cs +++ b/src/Hc900Crawler/Controllers/Hc900Controllers.cs @@ -46,7 +46,7 @@ public class GatewayController : ControllerBase } catch (Exception ex) { - return Ok(new { status = "NOT_SERVING", error = ex.Message }); + return Ok(new { Status = "NOT_SERVING", Error = ex.Message }); } } @@ -84,7 +84,7 @@ public class GatewayController : ControllerBase public async Task Write([FromBody] WriteTagDto dto) { var (success, error) = await _write.WriteTagAsync(dto.ControllerId, dto.TagName, dto.Value); - return Ok(new { success, error }); + return Ok(new { Success = success, Error = error }); } } @@ -305,7 +305,7 @@ public class Hc900TagManagerController : ControllerBase if (entry == null) return NotFound(); entry.IsActive = active; await _ctx.SaveChangesAsync(); - return Ok(new { entry.Id, entry.TagName, entry.IsActive }); + return Ok(new { Id = entry.Id, TagName = entry.TagName, IsActive = entry.IsActive }); } [HttpPost("bulk-active")] @@ -318,7 +318,7 @@ public class Hc900TagManagerController : ControllerBase if (!string.IsNullOrEmpty(dto.ControllerId)) q = q.Where(x => x.ControllerId == dto.ControllerId); var count = await q.ExecuteUpdateAsync(s => s.SetProperty(x => x.IsActive, dto.Active)); - return Ok(new { updated = count, active = dto.Active }); + return Ok(new { Updated = count, Active = dto.Active }); } [HttpGet("param-types")] @@ -370,22 +370,22 @@ public class Hc900TagManagerController : ControllerBase var byType = await _ctx.Hc900MapEntries .Where(x => x.ControllerId == controllerId) .GroupBy(x => x.ParamType) - .Select(g => new { paramType = g.Key, total = g.Count(), active = g.Count(x => x.IsActive) }) - .OrderBy(x => x.paramType) + .Select(g => new { ParamType = g.Key, Total = g.Count(), Active = g.Count(x => x.IsActive) }) + .OrderBy(x => x.ParamType) .ToListAsync(); - return Ok(new { controllerId, catalog, assigned, live, config, unassigned, byType }); + return Ok(new { ControllerId = controllerId, Catalog = catalog, Assigned = assigned, Live = live, Config = config, Unassigned = unassigned, ByType = byType }); } var total = await _ctx.Hc900MapEntries.CountAsync(); var active = await _ctx.Hc900MapEntries.CountAsync(x => x.IsActive); var byTypeAll = 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) + .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 = byTypeAll }); + return Ok(new { Total = total, Active = active, Inactive = total - active, LiveCount = liveCount, ByType = byTypeAll }); } } @@ -419,7 +419,7 @@ public class SubAreaController : ControllerBase public async Task Update(string baseTag, [FromBody] string? subArea) { var ok = await _db.UpdateSubAreaAsync(baseTag, subArea); - return Ok(new { success = ok }); + return Ok(new { Success = ok }); } [HttpPost("seed")] diff --git a/src/Hc900Crawler/Controllers/HypertableController.cs b/src/Hc900Crawler/Controllers/HypertableController.cs index cd5766b..75d8ba4 100644 --- a/src/Hc900Crawler/Controllers/HypertableController.cs +++ b/src/Hc900Crawler/Controllers/HypertableController.cs @@ -43,7 +43,7 @@ public class HypertableController : ControllerBase CreateContinuousAggregate = dto.CreateContinuousAggregate }; var result = await _db.CreateHypertableAsync(request); - return Ok(new { success = result.Success, message = result.Message }); + return Ok(new { Success = result.Success, Message = result.Message }); } } diff --git a/src/Hc900Crawler/Controllers/KbAuthController.cs b/src/Hc900Crawler/Controllers/KbAuthController.cs index 74acd91..bb70d73 100644 --- a/src/Hc900Crawler/Controllers/KbAuthController.cs +++ b/src/Hc900Crawler/Controllers/KbAuthController.cs @@ -23,14 +23,14 @@ public class KbAuthController : ControllerBase public async Task Login([FromBody] LoginRequest req, CancellationToken ct) { if (req == null || string.IsNullOrWhiteSpace(req.Password)) - return Ok(new { success = false, error = "password is required" }); + return Ok(new { Success = false, Error = "password is required" }); var ip = HttpContext.Connection.RemoteIpAddress?.ToString(); var result = await _auth.LoginAsync(req.Password, ip, ct); if (!result.Success) - return Ok(new { success = false, error = result.Error ?? "login failed" }); + return Ok(new { Success = false, Error = result.Error ?? "login failed" }); - return Ok(new { success = true, token = result.Token, expiresAt = result.ExpiresAt }); + return Ok(new { Success = true, Token = result.Token, ExpiresAt = result.ExpiresAt }); } [HttpPost("logout")] @@ -39,7 +39,7 @@ public class KbAuthController : ControllerBase var token = Request.Headers["X-Kb-Token"].ToString(); if (!string.IsNullOrEmpty(token)) await _auth.LogoutAsync(token, ct); - return Ok(new { success = true }); + return Ok(new { Success = true }); } [HttpGet("status")] @@ -55,14 +55,14 @@ public class KbAuthController : ControllerBase { var token = Request.Headers["X-Kb-Token"].ToString(); if (!await _auth.ValidateAsync(token, ct)) - return Unauthorized(new { success = false, error = "invalid token" }); + return Unauthorized(new { Success = false, Error = "invalid token" }); if (req == null || string.IsNullOrWhiteSpace(req.OldPassword) || string.IsNullOrWhiteSpace(req.NewPassword)) - return Ok(new { success = false, error = "passwords required" }); + return Ok(new { Success = false, Error = "passwords required" }); if (req.NewPassword.Length < 6) - return Ok(new { success = false, error = "new password must be at least 6 chars" }); + return Ok(new { Success = false, Error = "new password must be at least 6 chars" }); var ok = await _auth.ChangePasswordAsync(req.OldPassword, req.NewPassword, ct); - return Ok(new { success = ok }); + return Ok(new { Success = ok }); } } diff --git a/src/Hc900Crawler/Controllers/KbController.cs b/src/Hc900Crawler/Controllers/KbController.cs index dae783b..f20fd97 100644 --- a/src/Hc900Crawler/Controllers/KbController.cs +++ b/src/Hc900Crawler/Controllers/KbController.cs @@ -48,10 +48,10 @@ public class KbController : ControllerBase .OrderBy(c => c.CollectionKey) .Select(c => new { - key = c.CollectionKey, - name = c.DisplayName, - qdrant = c.QdrantName, - description = c.Description + Key = c.CollectionKey, + Name = c.DisplayName, + Qdrant = c.QdrantName, + Description = c.Description }) .ToListAsync(ct); @@ -64,10 +64,10 @@ public class KbController : ControllerBase var byKey = docCounts.ToDictionary(x => x.Key, x => (x.Count, x.Chunks)); var result = items.Select(c => { - byKey.TryGetValue(c.key, out var counts); - return new { c.key, c.name, c.qdrant, c.description, docCount = counts.Count, chunkCount = counts.Chunks }; + byKey.TryGetValue(c.Key, out var counts); + return new { c.Key, c.Name, c.Qdrant, c.Description, DocCount = counts.Count, ChunkCount = counts.Chunks }; }); - return Ok(new { success = true, items = result }); + return Ok(new { Success = true, Items = result }); } [HttpPost("upload")] @@ -80,15 +80,15 @@ public class KbController : ControllerBase CancellationToken ct) { if (!await RequireAdminAsync(ct)) - return Unauthorized(new { success = false, error = "unauthorized" }); + return Unauthorized(new { Success = false, Error = "unauthorized" }); if (file == null || file.Length == 0) - return BadRequest(new { success = false, error = "file required" }); + return BadRequest(new { Success = false, Error = "file required" }); if (string.IsNullOrWhiteSpace(collectionKey)) - return BadRequest(new { success = false, error = "collectionKey required" }); + return BadRequest(new { Success = false, Error = "collectionKey required" }); var coll = await _db.KbCollections.FirstOrDefaultAsync(c => c.CollectionKey == collectionKey, ct); - if (coll == null) return BadRequest(new { success = false, error = "unknown collectionKey" }); + if (coll == null) return BadRequest(new { Success = false, Error = "unknown collectionKey" }); await using var stream = file.OpenReadStream(); var stored = await _storage.SaveAsync(stream, file.FileName, ct); @@ -123,7 +123,7 @@ public class KbController : ControllerBase await _db.SaveChangesAsync(ct); _logger.LogInformation("[Kb] 업로드 {Id} {Title} ({Size} bytes)", doc.Id, doc.Title, doc.FileSize); - return Ok(new { success = true, docId = doc.Id, status = doc.Status }); + return Ok(new { Success = true, DocId = doc.Id, Status = doc.Status }); } [HttpGet("documents")] @@ -154,45 +154,45 @@ public class KbController : ControllerBase .Take(pageSize) .Select(d => new { - id = d.Id, - title = d.Title, - collection = d.CollectionKey, - tags = d.Tags, - status = d.Status, - chunkCount = d.ChunkCount, - fileSize = d.FileSize, - uploadedAt = d.UploadedAt, - indexedAt = d.IndexedAt, - errorMessage = d.ErrorMessage + Id = d.Id, + Title = d.Title, + Collection = d.CollectionKey, + Tags = d.Tags, + Status = d.Status, + ChunkCount = d.ChunkCount, + FileSize = d.FileSize, + UploadedAt = d.UploadedAt, + IndexedAt = d.IndexedAt, + ErrorMessage = d.ErrorMessage }) .ToListAsync(ct); - return Ok(new { success = true, total, page, pageSize, items }); + return Ok(new { Success = true, Total = total, Page = page, PageSize = pageSize, Items = items }); } [HttpGet("documents/{id:guid}")] public async Task GetDocument(Guid id, CancellationToken ct) { var d = await _db.KbDocuments.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct); - if (d == null) return NotFound(new { success = false }); + if (d == null) return NotFound(new { Success = false }); return Ok(new { - success = true, - item = new + Success = true, + Item = new { - id = d.Id, - title = d.Title, - collection = d.CollectionKey, - tags = d.Tags, - status = d.Status, - chunkCount = d.ChunkCount, - fileSize = d.FileSize, - mimeType = d.MimeType, - uploadedAt = d.UploadedAt, - indexedAt = d.IndexedAt, - disabledAt = d.DisabledAt, - originalPath = d.OriginalPath, - fileSha256 = d.FileSha256, - errorMessage = d.ErrorMessage + Id = d.Id, + Title = d.Title, + Collection = d.CollectionKey, + Tags = d.Tags, + Status = d.Status, + ChunkCount = d.ChunkCount, + FileSize = d.FileSize, + MimeType = d.MimeType, + UploadedAt = d.UploadedAt, + IndexedAt = d.IndexedAt, + DisabledAt = d.DisabledAt, + OriginalPath = d.OriginalPath, + FileSha256 = d.FileSha256, + ErrorMessage = d.ErrorMessage } }); } @@ -201,14 +201,14 @@ public class KbController : ControllerBase public async Task GetChunks(Guid id, [FromQuery] int limit = 200, CancellationToken ct = default) { if (!await RequireAdminAsync(ct)) - return Unauthorized(new { success = false, error = "unauthorized" }); + return Unauthorized(new { Success = false, Error = "unauthorized" }); var d = await _db.KbDocuments.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct); - if (d == null) return NotFound(new { success = false }); + if (d == null) return NotFound(new { Success = false }); var coll = await _db.KbCollections.AsNoTracking() .FirstOrDefaultAsync(c => c.CollectionKey == d.CollectionKey, ct); - if (coll == null) return BadRequest(new { success = false, error = "collection not found" }); + if (coll == null) return BadRequest(new { Success = false, Error = "collection not found" }); limit = Math.Clamp(limit, 1, 500); var rows = await _qdrant.GetChunksByDocIdAsync(coll.QdrantName, d.Id, limit, ct); @@ -231,11 +231,11 @@ public class KbController : ControllerBase return Ok(new { - success = true, - docId = d.Id, - collection = d.CollectionKey, - count = chunks.Count, - chunks + Success = true, + DocId = d.Id, + Collection = d.CollectionKey, + Count = chunks.Count, + Chunks = chunks }); } @@ -262,7 +262,7 @@ public class KbController : ControllerBase startedAt = j.StartedAt, finishedAt = j.FinishedAt }).ToListAsync(ct); - return Ok(new { success = true, items }); + return Ok(new { Success = true, Items = items }); } [HttpGet("download/{id:guid}")] @@ -283,7 +283,7 @@ public class KbController : ControllerBase public IActionResult DownloadInstrumentsDraft([FromQuery] string path) { var abs = _storage.Resolve(path); - if (!System.IO.File.Exists(abs)) return NotFound(new { success = false, error = "file not found" }); + if (!System.IO.File.Exists(abs)) return NotFound(new { Success = false, Error = "file not found" }); var stream = new FileStream(abs, FileMode.Open, FileAccess.Read, FileShare.Read, 64 * 1024, true); var fileName = Path.GetFileName(abs); @@ -294,10 +294,10 @@ public class KbController : ControllerBase public async Task Delete(Guid id, CancellationToken ct) { if (!await RequireAdminAsync(ct)) - return Unauthorized(new { success = false, error = "unauthorized" }); + return Unauthorized(new { Success = false, Error = "unauthorized" }); var d = await _db.KbDocuments.FirstOrDefaultAsync(x => x.Id == id, ct); - if (d == null) return NotFound(new { success = false }); + if (d == null) return NotFound(new { Success = false }); var coll = await _db.KbCollections.FirstOrDefaultAsync(c => c.CollectionKey == d.CollectionKey, ct); if (coll != null) @@ -307,17 +307,17 @@ public class KbController : ControllerBase _db.KbDocuments.Remove(d); await _db.SaveChangesAsync(ct); - return Ok(new { success = true }); + return Ok(new { Success = true }); } [HttpPost("documents/{id:guid}/reindex")] public async Task Reindex(Guid id, CancellationToken ct) { if (!await RequireAdminAsync(ct)) - return Unauthorized(new { success = false, error = "unauthorized" }); + return Unauthorized(new { Success = false, Error = "unauthorized" }); var d = await _db.KbDocuments.FirstOrDefaultAsync(x => x.Id == id, ct); - if (d == null) return NotFound(new { success = false }); + if (d == null) return NotFound(new { Success = false }); var coll = await _db.KbCollections.FirstOrDefaultAsync(c => c.CollectionKey == d.CollectionKey, ct); if (coll != null) @@ -330,21 +330,21 @@ public class KbController : ControllerBase _db.KbIngestJobs.Add(new KbIngestJob { DocId = d.Id, Stage = "parse" }); await _db.SaveChangesAsync(ct); - return Ok(new { success = true }); + return Ok(new { Success = true }); } [HttpPost("documents/{id:guid}/disable")] public async Task Disable(Guid id, CancellationToken ct) { if (!await RequireAdminAsync(ct)) - return Unauthorized(new { success = false, error = "unauthorized" }); + return Unauthorized(new { Success = false, Error = "unauthorized" }); var d = await _db.KbDocuments.FirstOrDefaultAsync(x => x.Id == id, ct); - if (d == null) return NotFound(new { success = false }); + if (d == null) return NotFound(new { Success = false }); d.Status = "disabled"; d.DisabledAt = DateTime.UtcNow; await _db.SaveChangesAsync(ct); - return Ok(new { success = true }); + return Ok(new { Success = true }); } public sealed record BulkDisableRequest(string Title); @@ -353,16 +353,16 @@ public class KbController : ControllerBase public async Task BulkDisable([FromBody] BulkDisableRequest req, CancellationToken ct) { if (!await RequireAdminAsync(ct)) - return Unauthorized(new { success = false, error = "unauthorized" }); + return Unauthorized(new { Success = false, Error = "unauthorized" }); if (req == null || string.IsNullOrWhiteSpace(req.Title)) - return BadRequest(new { success = false, error = "title required" }); + return BadRequest(new { Success = false, Error = "title required" }); var rows = await _db.KbDocuments .Where(d => d.Title == req.Title && d.Status != "disabled") .ExecuteUpdateAsync(set => set .SetProperty(x => x.Status, _ => "disabled") .SetProperty(x => x.DisabledAt, _ => DateTime.UtcNow), ct); - return Ok(new { success = true, affected = rows }); + return Ok(new { Success = true, Affected = rows }); } public sealed record PurgeRequest(int? OlderThanDays); @@ -371,7 +371,7 @@ public class KbController : ControllerBase public async Task PurgeDisabled([FromBody] PurgeRequest req, CancellationToken ct) { if (!await RequireAdminAsync(ct)) - return Unauthorized(new { success = false, error = "unauthorized" }); + return Unauthorized(new { Success = false, Error = "unauthorized" }); var cutoff = req?.OlderThanDays is int days && days > 0 ? DateTime.UtcNow.AddDays(-days) @@ -389,7 +389,7 @@ public class KbController : ControllerBase } _db.KbDocuments.RemoveRange(victims); await _db.SaveChangesAsync(ct); - return Ok(new { success = true, deleted = victims.Count }); + return Ok(new { Success = true, Deleted = victims.Count }); } public sealed record InferInstrumentsRequest(bool UseLlm = false, string? SeedDocId = null); @@ -398,7 +398,7 @@ public class KbController : ControllerBase public async Task InferInstruments([FromBody] InferInstrumentsRequest? req, CancellationToken ct) { if (!await RequireAdminAsync(ct)) - return Unauthorized(new { success = false, error = "unauthorized" }); + return Unauthorized(new { Success = false, Error = "unauthorized" }); try { @@ -426,7 +426,7 @@ public class KbController : ControllerBase var raw = await _mcp.CallToolAsync("infer_field_instruments", args, ct); if (string.IsNullOrWhiteSpace(raw)) - return BadRequest(new { success = false, error = "MCP 응답 없음" }); + return BadRequest(new { Success = false, Error = "MCP 응답 없음" }); // [H-2 수정] McpClient.CallToolAsync는 실패 시 평문 문자열 반환 try @@ -435,7 +435,7 @@ public class KbController : ControllerBase if (jdoc.RootElement.TryGetProperty("success", out var s) && s.ValueKind == System.Text.Json.JsonValueKind.False) { var err = jdoc.RootElement.TryGetProperty("message", out var e) ? e.GetString() : "unknown"; - return BadRequest(new { success = false, error = err }); + return BadRequest(new { Success = false, Error = err }); } var docPath = jdoc.RootElement.GetProperty("doc_path").GetString() ?? ""; @@ -457,25 +457,25 @@ public class KbController : ControllerBase return Ok(new { - success = true, - downloadPath = stored.RelativePath, - instrumentCount, - powerEquipmentCount, - unmatchedCount, - message = jdoc.RootElement.TryGetProperty("message", out var m) ? m.GetString() : "" + Success = true, + DownloadPath = stored.RelativePath, + InstrumentCount = instrumentCount, + PowerEquipmentCount = powerEquipmentCount, + UnmatchedCount = unmatchedCount, + Message = jdoc.RootElement.TryGetProperty("message", out var m) ? m.GetString() : "" }); } catch (System.Text.Json.JsonException) { // [H-2 수정] 평문 응답("도구 호출 실패: ...")일 경우 _logger.LogWarning("[Kb][Infer] MCP 평문 응답: {Raw}", raw); - return BadRequest(new { success = false, error = "MCP 도구 실패: " + raw.TrimEnd('\r', '\n', ' ') }); + return BadRequest(new { Success = false, Error = "MCP 도구 실패: " + raw.TrimEnd('\r', '\n', ' ') }); } } catch (Exception ex) { _logger.LogError(ex, "[Kb][Infer] 실패"); - return BadRequest(new { success = false, error = ex.Message }); + return BadRequest(new { Success = false, Error = ex.Message }); } } } diff --git a/src/Hc900Crawler/Controllers/OllamaController.cs b/src/Hc900Crawler/Controllers/OllamaController.cs index 0651aef..9b9a407 100644 --- a/src/Hc900Crawler/Controllers/OllamaController.cs +++ b/src/Hc900Crawler/Controllers/OllamaController.cs @@ -8,6 +8,7 @@ using System.Net.Http; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.RegularExpressions; using System.Threading; namespace Hc900Crawler.Web.Controllers; @@ -287,7 +288,7 @@ public class OllamaController : ControllerBase var cfg = LoadConfig(); var res = await _httpClient.GetAsync($"{cfg.BaseUrl}/api/tags"); if (!res.IsSuccessStatusCode) - return Ok(new { success = false, error = $"Ollama HTTP {(int)res.StatusCode}", models = Array.Empty() }); + return Ok(new { Success = false, Error = $"Ollama HTTP {(int)res.StatusCode}", Models = Array.Empty() }); var body = await res.Content.ReadAsStringAsync(); var doc = JsonDocument.Parse(body); @@ -328,16 +329,16 @@ public class OllamaController : ControllerBase return Ok(new { - success = true, - models = chatModels.OrderBy(x => x).ToArray(), + Success = true, + Models = chatModels.OrderBy(x => x).ToArray(), // 진단/안내용 — UI에서 셀렉터 옆에 회색 안내로 노출 가능 - excluded = embeddingModels.OrderBy(x => x).ToArray() + Excluded = embeddingModels.OrderBy(x => x).ToArray() }); } catch (Exception ex) { _logger.LogError(ex, "[OllamaController] 모델 조회 실패"); - return Ok(new { success = false, error = ex.Message, models = Array.Empty() }); + return Ok(new { Success = false, Error = ex.Message, Models = Array.Empty() }); } } @@ -389,7 +390,7 @@ public class OllamaController : ControllerBase if (!res.IsSuccessStatusCode) { var err = await res.Content.ReadAsStringAsync(); - return Ok(new { success = false, error = err }); + return Ok(new { Success = false, Error = err }); } var body = await res.Content.ReadAsStringAsync(); @@ -400,12 +401,12 @@ public class OllamaController : ControllerBase { reply = cnt.GetString(); } - return Ok(new { success = true, reply }); + return Ok(new { Success = true, Reply = reply }); } catch (Exception ex) { _logger.LogError(ex, "[OllamaController] 채팅 실패"); - return Ok(new { success = false, error = ex.Message, reply = (string?)null }); + return Ok(new { Success = false, Error = ex.Message, Reply = (string?)null }); } } @@ -489,7 +490,7 @@ public class OllamaController : ControllerBase public IActionResult GetConfig() { var cfg = LoadConfig(); - return Ok(new { success = true, host = cfg.Host, port = cfg.Port, baseUrl = cfg.BaseUrl, vllmModel = LoadVllmModel() }); + return Ok(new { Success = true, Host = cfg.Host, Port = cfg.Port, BaseUrl = cfg.BaseUrl, VllmModel = LoadVllmModel() }); } [HttpPost("config")] @@ -509,12 +510,12 @@ public class OllamaController : ControllerBase }, new JsonSerializerOptions { WriteIndented = true }); System.IO.File.WriteAllText(path, json); _logger.LogInformation("[OllamaController] 설정 저장: {Host}:{Port}", cfg.Host, cfg.Port); - return Ok(new { success = true, host = cfg.Host, port = cfg.Port, baseUrl = cfg.BaseUrl }); + return Ok(new { Success = true, Host = cfg.Host, Port = cfg.Port, BaseUrl = cfg.BaseUrl }); } catch (Exception ex) { _logger.LogError(ex, "[OllamaController] 설정 저장 실패"); - return Ok(new { success = false, error = ex.Message }); + return Ok(new { Success = false, Error = ex.Message }); } } @@ -525,11 +526,11 @@ public class OllamaController : ControllerBase { var cfg = LoadConfig(); var res = await _httpClient.GetAsync($"{cfg.BaseUrl}/api/tags"); - return Ok(new { success = res.IsSuccessStatusCode, host = cfg.Host, port = cfg.Port }); + return Ok(new { Success = res.IsSuccessStatusCode, Host = cfg.Host, Port = cfg.Port }); } catch (Exception ex) { - return Ok(new { success = false, error = ex.Message }); + return Ok(new { Success = false, Error = ex.Message }); } } @@ -572,7 +573,7 @@ public class OllamaController : ControllerBase { var res = await _vllmClient.GetAsync("/v1/models"); if (!res.IsSuccessStatusCode) - return Ok(new { success = false, error = $"vLLM HTTP {(int)res.StatusCode}", models = Array.Empty() }); + return Ok(new { Success = false, Error = $"vLLM HTTP {(int)res.StatusCode}", Models = Array.Empty() }); var body = await res.Content.ReadAsStringAsync(); var doc = JsonDocument.Parse(body); @@ -585,12 +586,12 @@ public class OllamaController : ControllerBase models.Add(name.GetString() ?? ""); } } - return Ok(new { success = true, models = models.OrderBy(x => x).ToArray() }); + return Ok(new { Success = true, Models = models.OrderBy(x => x).ToArray() }); } catch (Exception ex) { _logger.LogError(ex, "[OllamaController] vLLM 모델 조회 실패"); - return Ok(new { success = false, error = ex.Message, models = Array.Empty() }); + return Ok(new { Success = false, Error = ex.Message, Models = Array.Empty() }); } } @@ -619,7 +620,7 @@ public class OllamaController : ControllerBase if (!res.IsSuccessStatusCode) { var err = await res.Content.ReadAsStringAsync(); - return Ok(new { success = false, error = err }); + return Ok(new { Success = false, Error = err }); } var body = await res.Content.ReadAsStringAsync(); @@ -631,12 +632,12 @@ public class OllamaController : ControllerBase if (first.TryGetProperty("message", out var msg) && msg.TryGetProperty("content", out var cnt)) reply = cnt.GetString(); } - return Ok(new { success = true, reply }); + return Ok(new { Success = true, Reply = reply }); } catch (Exception ex) { _logger.LogError(ex, "[OllamaController] vLLM 채팅 실패"); - return Ok(new { success = false, error = ex.Message, reply = (string?)null }); + return Ok(new { Success = false, Error = ex.Message, Reply = (string?)null }); } } @@ -647,7 +648,7 @@ public class OllamaController : ControllerBase try { if (req?.Messages == null || req.Messages.Length == 0) - return Ok(new { success = false, error = "messages required", summary = "" }); + return Ok(new { Success = false, Error = "messages required", Summary = "" }); // 대화를 평문으로 직렬화 var convo = new StringBuilder(); @@ -689,7 +690,7 @@ public class OllamaController : ControllerBase if (!res.IsSuccessStatusCode) { var err = await res.Content.ReadAsStringAsync(); - return Ok(new { success = false, error = err, summary = "" }); + return Ok(new { Success = false, Error = err, Summary = "" }); } var body = await res.Content.ReadAsStringAsync(); var doc = JsonDocument.Parse(body); @@ -700,12 +701,12 @@ public class OllamaController : ControllerBase if (first.TryGetProperty("message", out var msg) && msg.TryGetProperty("content", out var cnt)) summary = cnt.GetString() ?? ""; } - return Ok(new { success = true, summary = summary.Trim() }); + return Ok(new { Success = true, Summary = summary.Trim() }); } catch (Exception ex) { _logger.LogError(ex, "[OllamaController] 대화 요약 실패"); - return Ok(new { success = false, error = ex.Message, summary = "" }); + return Ok(new { Success = false, Error = ex.Message, Summary = "" }); } } @@ -1087,6 +1088,49 @@ public class OllamaController : ControllerBase } } + // Python 스타일 도구 호출 감지: tool_name(key="val", ...) — JSON 감지 실패 후 시도 + if (!string.IsNullOrEmpty(stopContent)) + { + var knownTools = req.Tools?.Select(t => t.Function.Name) + .ToHashSet(StringComparer.OrdinalIgnoreCase) ?? new HashSet(StringComparer.OrdinalIgnoreCase); + var pythonCalls = ExtractPythonStyleToolCalls(stopContent, knownTools); + if (pythonCalls.Count > 0) + { + bool pyExecuted = false; + var pyResults = new List(); + + foreach (var (callName, callArgs) in pythonCalls) + { + var pseudoId = $"pytc_{toolRound}_{Guid.NewGuid():N}"; + var argsJson = JsonSerializer.Serialize(callArgs); + await EmitToolStart(pseudoId, callName, argsJson); + try + { + var toolResult = await _mcpClient.CallToolAsync( + callName, callArgs, HttpContext.RequestAborted, + onTimeoutExtended: info => EmitToolExtending(pseudoId, info.ToolName, info.SoftTimeoutSec, info.ExtendedTimeoutSec)); + await EmitToolResult(pseudoId, callName, ok: true, payload: toolResult); + pyResults.Add($"[{callName} 결과]\n{toolResult}"); + pyExecuted = true; + } + catch (Exception ex) + { + await EmitToolResult(pseudoId, callName, ok: false, payload: ex.Message); + _logger.LogWarning(ex, "[OllamaController] Python형 도구 호출 실패: {Tool}", callName); + } + if (HttpContext.RequestAborted.IsCancellationRequested) return; + } + + if (pyExecuted) + { + var combined = string.Join("\n\n", pyResults); + messages.Add(new { role = "assistant", content = stopContent }); + messages.Add(new { role = "user", content = $"{combined}\n\n위 결과를 바탕으로 사용자의 질문에 자연어로 답변해주세요." }); + continue; + } + } + } + // 첫 번째 비스트리밍 응답의 content를 직접 전달 (두 번째 LLM 호출 없이) if (!string.IsNullOrEmpty(stopContent)) { @@ -1170,14 +1214,57 @@ public class OllamaController : ControllerBase try { var res = await _vllmClient.GetAsync("/v1/models"); - return Ok(new { success = res.IsSuccessStatusCode, model = LoadVllmModel() }); + return Ok(new { Success = res.IsSuccessStatusCode, Model = LoadVllmModel() }); } catch (Exception ex) { - return Ok(new { success = false, error = ex.Message }); + return Ok(new { Success = false, Error = ex.Message }); } } + // Python 스타일 도구 호출 감지: funcname(key="val", key2="val2", ...) + // knownTools 에 있는 이름만 매칭 (SQL 함수 등 false positive 방지) + private static List<(string name, Dictionary args)> ExtractPythonStyleToolCalls( + string content, HashSet knownTools) + { + var result = new List<(string, Dictionary)>(); + if (string.IsNullOrWhiteSpace(content) || knownTools.Count == 0) return result; + + var matches = Regex.Matches(content.Trim(), @"(\w+)\s*\(([^)]*)\)"); + foreach (Match m in matches) + { + var name = m.Groups[1].Value; + if (!knownTools.Contains(name)) continue; + var args = ParsePythonCallArgs(m.Groups[2].Value); + if (args.Count > 0) + result.Add((name, args)); + } + return result; + } + + private static Dictionary ParsePythonCallArgs(string argsStr) + { + var args = new Dictionary(); + if (string.IsNullOrWhiteSpace(argsStr)) return args; + + // 키=값 쌍 추출: key="val" | key='val' | key=bareword + foreach (Match m in Regex.Matches(argsStr, @"(\w+)\s*=\s*(?:""([^""]*)""|'([^']*)'|(\S+?))(?:\s*,|$)")) + { + var key = m.Groups[1].Value; + var raw = m.Groups[2].Success ? m.Groups[2].Value + : m.Groups[3].Success ? m.Groups[3].Value + : m.Groups[4].Value.TrimEnd(',').Trim(); + // 타입 추론 + if (int.TryParse(raw, out var i)) args[key] = (object)i; + else if (double.TryParse(raw, System.Globalization.NumberStyles.Any, + System.Globalization.CultureInfo.InvariantCulture, out var d)) args[key] = d; + else if (raw.Equals("true", StringComparison.OrdinalIgnoreCase)) args[key] = true; + else if (raw.Equals("false", StringComparison.OrdinalIgnoreCase)) args[key] = false; + else args[key] = raw; + } + return args; + } + // content 안에서 첫 번째 완전한 JSON 객체를 추출 (앞뒤 텍스트 무시, 모델 무관) private static string ExtractFirstJsonObject(string content) { diff --git a/src/Hc900Crawler/Controllers/PidController.cs b/src/Hc900Crawler/Controllers/PidController.cs index 19b29f5..05a55f0 100644 --- a/src/Hc900Crawler/Controllers/PidController.cs +++ b/src/Hc900Crawler/Controllers/PidController.cs @@ -36,11 +36,11 @@ public class PidController : ControllerBase public async Task UploadFile(IFormFile file) { if (file == null || file.Length == 0) - return BadRequest(new { error = "파일이 없습니다." }); + return BadRequest(new { Error = "파일이 없습니다." }); var safeName = Path.GetFileName(file.FileName); if (!IsValidFileName(safeName)) - return BadRequest(new { error = "지원 형식: .dxf, .pdf" }); + return BadRequest(new { Error = "지원 형식: .dxf, .pdf" }); Directory.CreateDirectory(UploadDir); var filePath = Path.Combine(UploadDir, safeName); @@ -50,44 +50,44 @@ public class PidController : ControllerBase _logger.LogInformation("[PID] 파일 저장 완료: {FileName} ({Bytes:N0} bytes)", safeName, file.Length); - return Ok(new { fileName = safeName, fileSize = file.Length }); + return Ok(new { FileName = safeName, FileSize = file.Length }); } [HttpGet("server-files")] public IActionResult GetServerFiles() { if (!Directory.Exists(UploadDir)) - return Ok(new { files = Array.Empty() }); + return Ok(new { Files = Array.Empty() }); var files = Directory.GetFiles(UploadDir) .Where(f => IsValidFileName(Path.GetFileName(f))) .Select(f => new FileInfo(f)) .OrderByDescending(f => f.LastWriteTimeUtc) - .Select(f => new { fileName = f.Name, fileSize = f.Length, uploadedAt = f.LastWriteTimeUtc }) + .Select(f => new { FileName = f.Name, FileSize = f.Length, UploadedAt = f.LastWriteTimeUtc }) .ToArray(); - return Ok(new { files }); + return Ok(new { Files = files }); } [HttpPost("extract")] public async Task Extract([FromBody] ExtractRequest request) { if (!IsValidFileName(request.FileName)) - return BadRequest(new { error = "파일명이 유효하지 않습니다." }); + return BadRequest(new { Error = "파일명이 유효하지 않습니다." }); var filePath = Path.Combine(UploadDir, request.FileName); if (!System.IO.File.Exists(filePath)) - return NotFound(new { error = $"서버에 파일이 없습니다: {request.FileName}" }); + return NotFound(new { Error = $"서버에 파일이 없습니다: {request.FileName}" }); _logger.LogInformation("[PID] 추출 시작: {FileName}, imageMode={ImageMode}", request.FileName, request.UseImageMode); var result = await _pidExtractor.ExtractFromFileAsync(filePath, request.UseImageMode); return Ok(new { - totalCount = result.TotalCount, - confidenceItems = result.ConfidenceItems, - lowConfidenceItems = result.LowConfidenceItems, - skippedDuplicates = result.SkippedDuplicates + TotalCount = result.TotalCount, + ConfidenceItems = result.ConfidenceItems, + LowConfidenceItems = result.LowConfidenceItems, + SkippedDuplicates = result.SkippedDuplicates }); } @@ -98,12 +98,12 @@ public class PidController : ControllerBase public async Task AnalyzeConnections([FromBody] ConnectionsRequest request) { if (!IsValidFileName(request.FileName)) - return BadRequest(new { error = "파일명이 유효하지 않습니다." }); + return BadRequest(new { Error = "파일명이 유효하지 않습니다." }); _logger.LogInformation("[PID] 연결 분석 시작: {FileName}", request.FileName); var count = await _pidExtractor.AnalyzeConnectionsAsync(request.FileName); - return Ok(new { connectionCount = count }); + return Ok(new { ConnectionCount = count }); } [HttpGet("equipment")] @@ -114,39 +114,39 @@ public class PidController : ControllerBase var equipmentDtos = itemList.Select(e => new { - id = e.Id, - tagName = e.TagNo, - equipmentName = e.EquipmentName, - instrumentType = e.InstrumentType, - lineNumber = e.LineNumber, - pidDrawingNo = e.PidDrawingNo, - confidence = e.Confidence, - isActive = e.IsActive, - extractedAt = e.ExtractedAt, - updatedAt = e.UpdatedAt, - experionTagId = e.ExperionTagId, - experionTagName = e.ExperionTag?.TagName, - category = e.Category, - tagClass = e.TagClass, - tagDcs = e.TagDcs, - role = e.Role, - fromTag = e.FromTag, - fromAt = e.FromAt, - toTag = e.ToTag, - toAt = e.ToAt, - subArea = e.SubArea, - posX = e.PosX, - posY = e.PosY, - drawingFile = e.DrawingFile + Id = e.Id, + TagNo = e.TagNo, + EquipmentName = e.EquipmentName, + InstrumentType = e.InstrumentType, + LineNumber = e.LineNumber, + PidDrawingNo = e.PidDrawingNo, + Confidence = e.Confidence, + IsActive = e.IsActive, + ExtractedAt = e.ExtractedAt, + UpdatedAt = e.UpdatedAt, + ExperionTagId = e.ExperionTagId, + ExperionTagName = e.ExperionTag?.TagName, + Category = e.Category, + TagClass = e.TagClass, + TagDcs = e.TagDcs, + Role = e.Role, + FromTag = e.FromTag, + FromAt = e.FromAt, + ToTag = e.ToTag, + ToAt = e.ToAt, + SubArea = e.SubArea, + PosX = e.PosX, + PosY = e.PosY, + DrawingFile = e.DrawingFile }); return Ok(new { - total, - page, - pageSize, - confidenceRate = itemList.Count > 0 ? itemList.Average(e => e.Confidence) : 0, - items = equipmentDtos + Total = total, + Page = page, + PageSize = pageSize, + ConfidenceRate = itemList.Count > 0 ? itemList.Average(e => e.Confidence) : 0, + Items = equipmentDtos }); } @@ -154,15 +154,15 @@ public class PidController : ControllerBase public async Task AutoMap(long id) { var (found, tagName) = await _pidExtractor.AutoMapTagAsync(id); - if (!found) return NotFound(new { error = "매칭되는 Experion 태그가 없습니다." }); - return Ok(new { tagName }); + if (!found) return NotFound(new { Error = "매칭되는 Experion 태그가 없습니다." }); + return Ok(new { TagName = tagName }); } [HttpPut("{id}")] public async Task UpdateEquipment(long id, [FromBody] UpdateEquipmentRequest request) { var ok = await _pidExtractor.UpdateEquipmentAsync(id, request); - if (!ok) return NotFound(new { error = "레코드를 찾을 수 없습니다." }); + if (!ok) return NotFound(new { Error = "레코드를 찾을 수 없습니다." }); return NoContent(); } @@ -170,18 +170,18 @@ public class PidController : ControllerBase public async Task CreateEquipment([FromBody] CreateEquipmentRequest request) { var e = await _pidExtractor.CreateEquipmentAsync(request); - return CreatedAtAction(nameof(CreateEquipment), new { id = e.Id }, new + return CreatedAtAction(nameof(CreateEquipment), new { Id = e.Id }, new { - id = e.Id, - tagName = e.TagNo, - equipmentName = e.EquipmentName, - instrumentType = e.InstrumentType, - category = e.Category, - tagDcs = e.TagDcs, - tagClass = e.TagClass, - role = e.Role, - fromTag = e.FromTag, - toTag = e.ToTag + Id = e.Id, + TagName = e.TagNo, + EquipmentName = e.EquipmentName, + InstrumentType = e.InstrumentType, + Category = e.Category, + TagDcs = e.TagDcs, + TagClass = e.TagClass, + Role = e.Role, + FromTag = e.FromTag, + ToTag = e.ToTag }); } @@ -197,12 +197,12 @@ public class PidController : ControllerBase return Ok(new { - confidenceItems, - lowConfidenceItems, - confidenceRange, - drawingCount, - unmappedCount, - mappedCount + ConfidenceItems = confidenceItems, + LowConfidenceItems = lowConfidenceItems, + ConfidenceRange = confidenceRange, + DrawingCount = drawingCount, + UnmappedCount = unmappedCount, + MappedCount = mappedCount }); } @@ -210,7 +210,7 @@ public class PidController : ControllerBase public async Task UpdateConfidence(long id, [FromBody] double confidence) { if (confidence < 0 || confidence > 1) - return BadRequest(new { error = "신뢰도는 0~1 사이여야 합니다." }); + return BadRequest(new { Error = "신뢰도는 0~1 사이여야 합니다." }); await _pidExtractor.UpdateConfidenceAsync(id, confidence); return NoContent(); @@ -234,7 +234,7 @@ public class PidController : ControllerBase public async Task Delete(long id) { var ok = await _pidExtractor.DeleteAsync(id); - if (!ok) return NotFound(new { error = "레코드를 찾을 수 없습니다." }); + if (!ok) return NotFound(new { Error = "레코드를 찾을 수 없습니다." }); return NoContent(); } @@ -242,7 +242,7 @@ public class PidController : ControllerBase public async Task DeleteAll() { var count = await _pidExtractor.DeleteAllEquipmentAsync(); - return Ok(new { deletedCount = count, message = $"{count}건 삭제됨" }); + return Ok(new { DeletedCount = count, Message = $"{count}건 삭제됨" }); } [HttpGet("mappings")] @@ -252,10 +252,10 @@ public class PidController : ControllerBase return Ok(new { - total, - page, - pageSize, - items = items + Total = total, + Page = page, + PageSize = pageSize, + Items = items }); } @@ -263,7 +263,7 @@ public class PidController : ControllerBase public async Task CreateMapping([FromBody] CreateMappingRequest request) { var result = await _tagMapping.CreateMappingAsync(request); - return Ok(new { result = result }); + return Ok(new { Result = result }); } [HttpPut("mappings/{id}")] @@ -284,21 +284,21 @@ public class PidController : ControllerBase public async Task GetUnmappedCount() { var count = await _tagMapping.GetUnmappedCountAsync(); - return Ok(new { count }); + return Ok(new { Count = count }); } [HttpGet("mappings/mapped")] public async Task GetMappedCount() { var count = await _tagMapping.GetMappedCountAsync(); - return Ok(new { count }); + return Ok(new { Count = count }); } [HttpGet("mappings/available-tags")] public async Task GetAvailableTags() { var tags = await _tagMapping.GetAvailableTagsAsync(); - return Ok(new { tags }); + return Ok(new { Tags = tags }); } // ── Prefix 규칙 CRUD ────────────────────────────────────────────────── @@ -309,15 +309,15 @@ public class PidController : ControllerBase var rules = await _pidExtractor.GetPrefixRulesAsync(); return Ok(new { - items = rules.Select(r => new + Items = rules.Select(r => new { - id = r.Id, - prefix = r.Prefix, - category = r.Category, - tagDcs = r.TagDcs, - description = r.Description, - sortOrder = r.SortOrder, - createdAt = r.CreatedAt + Id = r.Id, + Prefix = r.Prefix, + Category = r.Category, + TagDcs = r.TagDcs, + Description = r.Description, + SortOrder = r.SortOrder, + CreatedAt = r.CreatedAt }) }); } @@ -326,32 +326,32 @@ public class PidController : ControllerBase public async Task CreatePrefixRule([FromBody] CreatePidPrefixRuleRequest request) { if (string.IsNullOrWhiteSpace(request.Prefix)) - return BadRequest(new { error = "prefix는 필수입니다." }); + return BadRequest(new { Error = "prefix는 필수입니다." }); if (!PidEquipment.AllCategories.Contains(request.Category)) - return BadRequest(new { error = $"category는 {string.Join(", ", PidEquipment.AllCategories)} 중 하나여야 합니다." }); + return BadRequest(new { Error = $"category는 {string.Join(", ", PidEquipment.AllCategories)} 중 하나여야 합니다." }); var rule = await _pidExtractor.CreatePrefixRuleAsync(request); - return Ok(new { id = rule.Id, prefix = rule.Prefix, category = rule.Category }); + return Ok(new { Id = rule.Id, Prefix = rule.Prefix, Category = rule.Category }); } [HttpPut("prefix-rules/{id}")] public async Task UpdatePrefixRule(int id, [FromBody] UpdatePidPrefixRuleRequest request) { if (string.IsNullOrWhiteSpace(request.Prefix)) - return BadRequest(new { error = "prefix는 필수입니다." }); + return BadRequest(new { Error = "prefix는 필수입니다." }); if (!PidEquipment.AllCategories.Contains(request.Category)) - return BadRequest(new { error = $"category는 {string.Join(", ", PidEquipment.AllCategories)} 중 하나여야 합니다." }); + return BadRequest(new { Error = $"category는 {string.Join(", ", PidEquipment.AllCategories)} 중 하나여야 합니다." }); var rule = await _pidExtractor.UpdatePrefixRuleAsync(id, request); - if (rule == null) return NotFound(new { error = "규칙을 찾을 수 없습니다." }); - return Ok(new { id = rule.Id, prefix = rule.Prefix, category = rule.Category }); + if (rule == null) return NotFound(new { Error = "규칙을 찾을 수 없습니다." }); + return Ok(new { Id = rule.Id, Prefix = rule.Prefix, Category = rule.Category }); } [HttpDelete("prefix-rules/{id}")] public async Task DeletePrefixRule(int id) { var ok = await _pidExtractor.DeletePrefixRuleAsync(id); - if (!ok) return NotFound(new { error = "규칙을 찾을 수 없습니다." }); + if (!ok) return NotFound(new { Error = "규칙을 찾을 수 없습니다." }); return NoContent(); } @@ -359,7 +359,7 @@ public class PidController : ControllerBase public async Task ApplyCategories() { var count = await _pidExtractor.ApplyCategoriesToExistingAsync(); - return Ok(new { applied = count }); + return Ok(new { Applied = count }); } /// 편집한 export 엑셀(.xlsx)을 업로드하여 pid_equipment 를 UPSERT. @@ -368,9 +368,9 @@ public class PidController : ControllerBase public async Task ImportExcel(IFormFile file) { if (file == null || file.Length == 0) - return BadRequest(new { error = "파일이 없습니다." }); + return BadRequest(new { Error = "파일이 없습니다." }); if (!file.FileName.EndsWith(".xlsx", StringComparison.OrdinalIgnoreCase)) - return BadRequest(new { error = "지원 형식: .xlsx (export 엑셀)" }); + return BadRequest(new { Error = "지원 형식: .xlsx (export 엑셀)" }); try { @@ -384,7 +384,7 @@ public class PidController : ControllerBase catch (Exception ex) { _logger.LogError(ex, "[PID] 엑셀 import 실패: {File}", file.FileName); - return BadRequest(new { error = $"import 실패: {ex.Message}" }); + return BadRequest(new { Error = $"import 실패: {ex.Message}" }); } } } diff --git a/src/Hc900Crawler/Controllers/PidGraphController.cs b/src/Hc900Crawler/Controllers/PidGraphController.cs index 621d5d1..27fc5aa 100644 --- a/src/Hc900Crawler/Controllers/PidGraphController.cs +++ b/src/Hc900Crawler/Controllers/PidGraphController.cs @@ -42,20 +42,20 @@ public class PidGraphController : ControllerBase // 프론트엔드 camelCase 규칙 준수 및 PidResponse 래핑 return Ok(new { - success = true, - data = new + Success = true, + Data = new { - startNode = result.StartNode, - impactedNodes = result.ImpactedNodes, - paths = result.Paths + StartNode = result.StartNode, + ImpactedNodes = result.ImpactedNodes, + Paths = result.Paths }, - message = "분석 완료" + Message = "분석 완료" }); } catch (Exception ex) { _logger.LogError(ex, "Unexpected error during impact analysis"); - return StatusCode(500, new { error = "Internal server error", details = ex.Message }); + return StatusCode(500, new { Error = "Internal server error", Details = ex.Message }); } } @@ -67,13 +67,13 @@ public class PidGraphController : ControllerBase { return Ok(new { - success = true, - data = new + Success = true, + Data = new { - taskId = status.TaskId, - progress = status.Progress, - status = status.Status, - message = status.Message + TaskId = status.TaskId, + Progress = status.Progress, + Status = status.Status, + Message = status.Message } }); } @@ -95,10 +95,10 @@ public class PidGraphController : ControllerBase { var data = System.Text.Json.JsonSerializer.Serialize(new { - taskId = status.TaskId, - progress = status.Progress, - status = status.Status, - message = status.Message + TaskId = status.TaskId, + Progress = status.Progress, + Status = status.Status, + Message = status.Message }); await Response.WriteAsync($"data: {data}\n\n", ct); @@ -120,7 +120,7 @@ public class PidGraphController : ControllerBase public async Task BuildGraph([FromBody] BuildGraphRequest request) { if (string.IsNullOrEmpty(request.Filepath)) - return BadRequest(new { error = "Filepath is required" }); + return BadRequest(new { Error = "Filepath is required" }); var taskId = Guid.NewGuid().ToString(); await _statusStore.UpdateStatusAsync(new AnalysisStatus(taskId, 0, "Starting", "추출 준비 중...")); @@ -181,9 +181,9 @@ public class PidGraphController : ControllerBase return Ok(new { - success = true, - data = new { taskId = taskId }, - message = "작업이 시작되었습니다." + Success = true, + Data = new { TaskId = taskId }, + Message = "작업이 시작되었습니다." }); } diff --git a/src/Hc900Crawler/Controllers/PointBuilderController.cs b/src/Hc900Crawler/Controllers/PointBuilderController.cs index a0098ce..2a7e303 100644 --- a/src/Hc900Crawler/Controllers/PointBuilderController.cs +++ b/src/Hc900Crawler/Controllers/PointBuilderController.cs @@ -202,7 +202,7 @@ public class PointBuilderController : ControllerBase public IActionResult ListSinamFiles() { if (!Directory.Exists(SinamUploadDir)) - return Ok(new { success = true, files = Array.Empty() }); + return Ok(new { Success = true, Files = Array.Empty() }); var files = Directory.GetFiles(SinamUploadDir, "*.xlsx") .Select(f => new @@ -215,7 +215,7 @@ public class PointBuilderController : ControllerBase .OrderByDescending(f => f.modified) .ToList(); - return Ok(new { success = true, files }); + return Ok(new { Success = true, Files = files }); } [HttpPost("sinam/upload")] @@ -223,11 +223,11 @@ public class PointBuilderController : ControllerBase public async Task UploadSinam(IFormFile file, CancellationToken ct) { if (file == null || file.Length == 0) - return BadRequest(new { success = false, error = "file required" }); + return BadRequest(new { Success = false, Error = "file required" }); var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); if (ext != ".xlsx") - return BadRequest(new { success = false, error = ".xlsx 파일만 업로드 가능" }); + return BadRequest(new { Success = false, Error = ".xlsx 파일만 업로드 가능" }); Directory.CreateDirectory(SinamUploadDir); var savePath = Path.Combine(SinamUploadDir, $"Sinam_{DateTime.UtcNow:yyyyMMdd-HHmmss}.xlsx"); @@ -236,33 +236,33 @@ public class PointBuilderController : ControllerBase await file.CopyToAsync(stream, ct); _log.LogInformation("[Sinam] 업로드: {Path} ({Size} bytes)", savePath, file.Length); - return Ok(new { success = true, file = savePath }); + return Ok(new { Success = true, File = savePath }); } [HttpPost("sinam/parse")] public async Task ParseSinam([FromBody] SinamParseRequest dto, CancellationToken ct) { if (string.IsNullOrWhiteSpace(dto.File)) - return BadRequest(new { success = false, error = "file required" }); + return BadRequest(new { Success = false, Error = "file required" }); if (!System.IO.File.Exists(dto.File)) - return BadRequest(new { success = false, error = "file not found" }); + return BadRequest(new { Success = false, Error = "file not found" }); var validControllers = new[] { "C1", "C2", "C3", "C4" }; if (!validControllers.Contains(dto.Controller)) - return BadRequest(new { success = false, error = $"controller must be one of: {string.Join(", ", validControllers)}" }); + return BadRequest(new { Success = false, Error = $"controller must be one of: {string.Join(", ", validControllers)}" }); var result = await RunSinamScriptAsync(dto.File, dto.Controller, dto.ApplyDb, ct); if (!result.Success) - return StatusCode(500, new { success = false, error = result.Error }); + return StatusCode(500, new { Success = false, Error = result.Error }); return Ok(new { - success = true, - registers = result.Registers, - archive = result.Archive, - loops = result.Loops, - stdout = result.Stdout, + Success = true, + Registers = result.Registers, + Archive = result.Archive, + Loops = result.Loops, + Stdout = result.Stdout, }); } @@ -303,33 +303,33 @@ public class PointBuilderController : ControllerBase var count = await _db.Hc900BuildRealtimeTableAsync(groups); _log.LogInformation("[PointBuilder] Build 완료: {Count}개 활성화", count); - return Ok(new { success = true, count, message = $"{count}개 포인트 활성화 완료" }); + return Ok(new { Success = true, Count = count, Message = $"{count}개 포인트 활성화 완료" }); } [HttpPost("apply")] public async Task Apply([FromBody] Hc900PointBuilderApplyDto dto) { if (dto.SelectedTagNames == null || dto.SelectedTagNames.Count == 0) - return BadRequest(new { success = false, error = "selectedTagNames는 최소 1개 이상 필요합니다" }); + return BadRequest(new { Success = false, Error = "selectedTagNames는 최소 1개 이상 필요합니다" }); if (string.IsNullOrEmpty(dto.ControllerId)) - return BadRequest(new { success = false, error = "controllerId는 필수입니다" }); + return BadRequest(new { Success = false, Error = "controllerId는 필수입니다" }); var count = await _db.Hc900ApplySelectedPointsAsync(dto.ControllerId, dto.SelectedTagNames); _log.LogInformation("[PointBuilder] Apply 완료: {Count}개 활성화 (controller={Ctrl})", count, dto.ControllerId); - return Ok(new { success = true, count, message = $"{count}개 포인트 활성화 완료" }); + return Ok(new { Success = true, Count = count, Message = $"{count}개 포인트 활성화 완료" }); } [HttpPost("append")] public async Task Append([FromBody] Hc900PointBuilderApplyDto dto) { if (dto.SelectedTagNames == null || dto.SelectedTagNames.Count == 0) - return BadRequest(new { success = false, error = "selectedTagNames는 최소 1개 이상 필요합니다" }); + return BadRequest(new { Success = false, Error = "selectedTagNames는 최소 1개 이상 필요합니다" }); if (string.IsNullOrEmpty(dto.ControllerId)) - return BadRequest(new { success = false, error = "controllerId는 필수입니다" }); + return BadRequest(new { Success = false, Error = "controllerId는 필수입니다" }); var count = await _db.Hc900AppendPointsAsync(dto.ControllerId, dto.SelectedTagNames); _log.LogInformation("[PointBuilder] Append 완료: {Count}개 추가 (controller={Ctrl})", count, dto.ControllerId); - return Ok(new { success = true, count, message = $"{count}개 포인트 추가 완료" }); + return Ok(new { Success = true, Count = count, Message = $"{count}개 포인트 추가 완료" }); } public sealed record RealtimeBulkDto(List? Ids, string? ControllerId); @@ -339,13 +339,13 @@ public class PointBuilderController : ControllerBase public async Task RealtimeAdd([FromBody] RealtimeBulkDto dto) { if (dto.Ids == null || dto.Ids.Count == 0) - return BadRequest(new { success = false, error = "ids는 최소 1개 이상 필요합니다" }); + return BadRequest(new { Success = false, Error = "ids는 최소 1개 이상 필요합니다" }); if (string.IsNullOrEmpty(dto.ControllerId)) - return BadRequest(new { success = false, error = "controllerId는 필수입니다" }); + return BadRequest(new { Success = false, Error = "controllerId는 필수입니다" }); var count = await _db.Hc900RealtimeAddByIdsAsync(dto.ControllerId, dto.Ids); _log.LogInformation("[PointBuilder] 실시간 추가: {Count}개 (controller={Ctrl})", count, dto.ControllerId); - return Ok(new { success = true, count, message = $"{count}개 실시간 편입 완료" }); + return Ok(new { Success = true, Count = count, Message = $"{count}개 실시간 편입 완료" }); } // 선택(id)한 태그를 realtime_table에서 제거 @@ -353,13 +353,13 @@ public class PointBuilderController : ControllerBase public async Task RealtimeRemove([FromBody] RealtimeBulkDto dto) { if (dto.Ids == null || dto.Ids.Count == 0) - return BadRequest(new { success = false, error = "ids는 최소 1개 이상 필요합니다" }); + return BadRequest(new { Success = false, Error = "ids는 최소 1개 이상 필요합니다" }); if (string.IsNullOrEmpty(dto.ControllerId)) - return BadRequest(new { success = false, error = "controllerId는 필수입니다" }); + return BadRequest(new { Success = false, Error = "controllerId는 필수입니다" }); var count = await _db.Hc900RealtimeRemoveByIdsAsync(dto.ControllerId, dto.Ids); _log.LogInformation("[PointBuilder] 실시간 제거: {Count}개 (controller={Ctrl})", count, dto.ControllerId); - return Ok(new { success = true, count, message = $"{count}개 실시간 제거 완료" }); + return Ok(new { Success = true, Count = count, Message = $"{count}개 실시간 제거 완료" }); } [HttpGet("points")] @@ -448,18 +448,18 @@ public class PointBuilderController : ControllerBase public async Task Add([FromBody] Hc900PointBuilderAddDto dto) { if (string.IsNullOrWhiteSpace(dto.TagName)) - return BadRequest(new { success = false, error = "tagName은 필수입니다" }); + return BadRequest(new { Success = false, Error = "tagName은 필수입니다" }); try { var point = await _db.Hc900AddRealtimePointAsync( dto.TagName, dto.Hc900Tag, dto.ModbusAddr, dto.DataType, dto.ParamType, dto.Access, dto.ControllerId); - return Ok(new { success = true, point }); + return Ok(new { Success = true, Point = point }); } catch (InvalidOperationException ex) { - return Conflict(new { success = false, error = ex.Message }); + return Conflict(new { Success = false, Error = ex.Message }); } } @@ -468,7 +468,7 @@ public class PointBuilderController : ControllerBase { var entry = await _db.Hc900MapEntries.FindAsync(id); if (entry == null) - return NotFound(new { success = false, error = "지정한 id의 태그가 존재하지 않습니다" }); + return NotFound(new { Success = false, Error = "지정한 id의 태그가 존재하지 않습니다" }); entry.IsActive = false; await _db.SaveChangesAsync(); @@ -500,6 +500,6 @@ public class PointBuilderController : ControllerBase } _log.LogInformation("[PointBuilder] 삭제: {TagName} (purgeHistory={Purge})", entry.TagName, purgeHistory); - return Ok(new { success = true, tagName = entry.TagName }); + return Ok(new { Success = true, TagName = entry.TagName }); } } diff --git a/src/Hc900Crawler/Controllers/SetupController.cs b/src/Hc900Crawler/Controllers/SetupController.cs index 76f3198..0eefb8f 100644 --- a/src/Hc900Crawler/Controllers/SetupController.cs +++ b/src/Hc900Crawler/Controllers/SetupController.cs @@ -88,11 +88,11 @@ public class SetupController : ControllerBase { ControllerProcessManager.SaveConfig(cfg); _procMgr.ReloadConfig(); - return Ok(new { success = true, message = "설정 저장 완료" }); + return Ok(new { Success = true, Message = "설정 저장 완료" }); } catch (Exception ex) { - return StatusCode(500, new { success = false, message = ex.Message }); + return StatusCode(500, new { Success = false, Message = ex.Message }); } } @@ -103,13 +103,13 @@ public class SetupController : ControllerBase if (id != null) { var (ok, msg) = await _procMgr.StartAsync(id); - return Ok(new { success = ok, message = msg }); + return Ok(new { Success = ok, Message = msg }); } var results = new List(); foreach (var ctrl in _procMgr.Config.Controllers.Where(c => c.Enabled)) { var (ok, msg) = await _procMgr.StartAsync(ctrl.Id); - results.Add(new { ctrl.Id, ok, msg }); + results.Add(new { ctrl.Id, Ok = ok, Msg = msg }); } return Ok(results); } @@ -121,13 +121,13 @@ public class SetupController : ControllerBase if (id != null) { var (ok, msg) = await _procMgr.StopAsync(id); - return Ok(new { success = ok, message = msg }); + return Ok(new { Success = ok, Message = msg }); } var results = new List(); foreach (var ctrl in _procMgr.Config.Controllers) { var (ok, msg) = await _procMgr.StopAsync(ctrl.Id); - results.Add(new { ctrl.Id, ok, msg }); + results.Add(new { ctrl.Id, Ok = ok, Msg = msg }); } return Ok(results); } @@ -142,7 +142,7 @@ public class SetupController : ControllerBase await _procMgr.StopAsync(id); await Task.Delay(500); var (ok, msg) = await _procMgr.StartAsync(id); - return Ok(new { success = ok, message = msg }); + return Ok(new { Success = ok, Message = msg }); } foreach (var ctrl in _procMgr.Config.Controllers) await _procMgr.StopAsync(ctrl.Id); @@ -151,7 +151,7 @@ public class SetupController : ControllerBase foreach (var ctrl in _procMgr.Config.Controllers.Where(c => c.Enabled)) { var (ok, msg) = await _procMgr.StartAsync(ctrl.Id); - results.Add(new { ctrl.Id, ok, msg }); + results.Add(new { ctrl.Id, Ok = ok, Msg = msg }); } return Ok(results); } @@ -161,8 +161,8 @@ public class SetupController : ControllerBase public IActionResult GetLog([FromQuery] string? id = null, [FromQuery] int lines = 50) { var target = id ?? _procMgr.Config.Controllers.FirstOrDefault()?.Id; - if (target == null) return Ok(new { log = Array.Empty() }); + if (target == null) return Ok(new { Log = Array.Empty() }); var status = _procMgr.GetStatus(target); - return Ok(new { log = status.RecentLog.TakeLast(lines) }); + return Ok(new { Log = status.RecentLog.TakeLast(lines) }); } } diff --git a/src/Hc900Crawler/Controllers/SteamAdvisorController.cs b/src/Hc900Crawler/Controllers/SteamAdvisorController.cs index 4ab9242..3b97bca 100644 --- a/src/Hc900Crawler/Controllers/SteamAdvisorController.cs +++ b/src/Hc900Crawler/Controllers/SteamAdvisorController.cs @@ -54,7 +54,7 @@ public sealed class SteamAdvisorController : ControllerBase [HttpGet("health")] public IActionResult Health() { - return Ok(new { loaded = _advisor.IsLoaded }); + return Ok(new { Loaded = _advisor.IsLoaded }); } [HttpGet("predict")] @@ -80,7 +80,7 @@ public sealed class SteamAdvisorController : ControllerBase var modelDir = _config.GetValue("SteamAdvisor:ModelDir") ?? "/home/windpacer/projects/hc900_ax/scripts/analysis"; if (!Directory.Exists(modelDir)) - return Ok(new { columns = Array.Empty() }); + return Ok(new { Columns = Array.Empty() }); var columns = Directory.GetFiles(modelDir, "*_model.json") .Select(Path.GetFileNameWithoutExtension) @@ -94,7 +94,7 @@ public sealed class SteamAdvisorController : ControllerBase .ToHashSet(); var defaultCol = _config.GetValue("SteamAdvisor:DefaultColumn") ?? "C-6111"; - return Ok(new { columns, configured = configured.OrderBy(x => x).ToList(), defaultColumn = defaultCol }); + return Ok(new { Columns = columns, Configured = configured.OrderBy(x => x).ToList(), DefaultColumn = defaultCol }); } [HttpGet("backtest/{col}")] @@ -104,7 +104,7 @@ public sealed class SteamAdvisorController : ControllerBase ?? "/home/windpacer/projects/hc900_ax/scripts/analysis"; var path = Path.Combine(plotDir, $"{col}_plotdata.json"); if (!System.IO.File.Exists(path)) - return NotFound(new { error = $"플롯데이터 없음: {col}" }); + return NotFound(new { Error = $"플롯데이터 없음: {col}" }); try { @@ -113,7 +113,7 @@ public sealed class SteamAdvisorController : ControllerBase } catch (Exception ex) { - return StatusCode(500, new { error = $"플롯데이터 읽기 실패: {ex.Message}" }); + return StatusCode(500, new { Error = $"플롯데이터 읽기 실패: {ex.Message}" }); } } @@ -129,7 +129,7 @@ public sealed class SteamAdvisorController : ControllerBase var steamFlowTag = tags["SteamFlow"]; if (string.IsNullOrEmpty(feedTag) || string.IsNullOrEmpty(productTag) || string.IsNullOrEmpty(tcTag)) - return BadRequest(new { error = $"컬럼 {col} 태그 매핑 불완전" }); + return BadRequest(new { Error = $"컬럼 {col} 태그 매핑 불완전" }); var tagNames = new[] { feedTag, productTag, tcTag }; if (!string.IsNullOrEmpty(steamOpTag)) tagNames = tagNames.Append(steamOpTag).ToArray(); @@ -145,8 +145,8 @@ public sealed class SteamAdvisorController : ControllerBase var missing = new[] { feedTag, productTag, tcTag } .Where(t => !live.ContainsKey(t)) .ToList(); - return Ok(new { col, status = "missing_tags", missing, - message = "일부 태그 실시간값 없음 — 게이트웨이 폴링 확인" }); + return Ok(new { Col = col, Status = "missing_tags", Missing = missing, + Message = "일부 태그 실시간값 없음 — 게이트웨이 폴링 확인" }); } var feed = double.TryParse(live[feedTag], out var f) ? f : double.NaN; @@ -164,7 +164,7 @@ public sealed class SteamAdvisorController : ControllerBase return Ok(new { - col, + Col = col, result.RecOp, result.RecSteam, result.Confidence, result.Mode, result.Ood, result.InEnv, result.Message, Feed = result.Feed, @@ -172,8 +172,8 @@ public sealed class SteamAdvisorController : ControllerBase TC = result.TC, ActualOp = steamOp, ActualSteamFlow = steamFlow, - Tags = new { feed = feedTag, product = productTag, tC = tcTag, steamOp = steamOpTag, steamFlow = steamFlowTag }, - Descs = new { feed = GetDesc(feedTag), product = GetDesc(productTag), tC = GetDesc(tcTag), steamOp = GetDesc(steamOpTag), steamFlow = GetDesc(steamFlowTag) }, + Tags = new { Feed = feedTag, Product = productTag, TC = tcTag, SteamOp = steamOpTag, SteamFlow = steamFlowTag }, + Descs = new { Feed = GetDesc(feedTag), Product = GetDesc(productTag), TC = GetDesc(tcTag), SteamOp = GetDesc(steamOpTag), SteamFlow = GetDesc(steamFlowTag) }, Timestamp = DateTime.UtcNow, }); } @@ -187,7 +187,7 @@ public sealed class SteamAdvisorController : ControllerBase [FromQuery] string? product = null) { var tref = await LoadTempRef(col); - if (tref is null) return NotFound(new { error = $"기준 프로파일 없음: {col}_tempref.json" }); + if (tref is null) return NotFound(new { Error = $"기준 프로파일 없음: {col}_tempref.json" }); tref = MergeProductLabels(col, tref); @@ -204,38 +204,38 @@ public sealed class SteamAdvisorController : ControllerBase return Ok(new { - column = tref.Column, - period = tref.Period, - matchedProduct = prod?.Name ?? prod?.Label, // 미선택이면 null - nProducts = tref.NProducts, - stages, - vacuum, - spanAD, - spanRef = prod?.SpanAD, + Column = tref.Column, + Period = tref.Period, + MatchedProduct = prod?.Name ?? prod?.Label, // 미선택이면 null + NProducts = tref.NProducts, + Stages = stages, + Vacuum = vacuum, + SpanAD = spanAD, + SpanRef = prod?.SpanAD, // 소문자 키로 projection (TempProduct 레코드는 PascalCase 직렬화 → 프론트가 못 읽음) - products = tref.Products.Select(p => new { - label = p.Label, - name = p.Name, - rebMedian = p.Stages.GetValueOrDefault("reb_temp")?.Median, - vacMedian = p.Vacuum.Median, - nRows = p.NRows, + Products = tref.Products.Select(p => new { + Label = p.Label, + Name = p.Name, + RebMedian = p.Stages.GetValueOrDefault("reb_temp")?.Median, + VacMedian = p.Vacuum.Median, + NRows = p.NRows, }).ToList(), - timestamp = DateTime.UtcNow, - flow = new { - feed = flow["feed"], - reflux = flow["reflux"], - overhead = flow["overhead"], - bottom = flow["bottom"], - product = flow["product"], - steam = flow["steam"], + Timestamp = DateTime.UtcNow, + Flow = new { + Feed = flow["feed"], + Reflux = flow["reflux"], + Overhead = flow["overhead"], + Bottom = flow["bottom"], + Product = flow["product"], + Steam = flow["steam"], }, - flowTags = new { - feed = $"{bases["feed"]}.PV", - reflux = $"{bases["reflux"]}.PV", - overhead = $"{bases["overhead"]}.PV", - bottom = $"{bases["bottom"]}.PV", - product = $"{bases["product"]}.PV", - steam = $"FIQ-{ToSuffix(col).Substring(0, ToSuffix(col).Length - 2)}15.PV", + FlowTags = new { + Feed = $"{bases["feed"]}.PV", + Reflux = $"{bases["reflux"]}.PV", + Overhead = $"{bases["overhead"]}.PV", + Bottom = $"{bases["bottom"]}.PV", + Product = $"{bases["product"]}.PV", + Steam = $"FIQ-{ToSuffix(col).Substring(0, ToSuffix(col).Length - 2)}15.PV", }, }); } @@ -253,7 +253,7 @@ public sealed class SteamAdvisorController : ControllerBase try { var tref = await LoadTempRef(col); - if (tref is null) return NotFound(new { error = $"기준 프로파일 없음: {col}_tempref.json" }); + if (tref is null) return NotFound(new { Error = $"기준 프로파일 없음: {col}_tempref.json" }); tref = MergeProductLabels(col, tref); @@ -296,48 +296,48 @@ public sealed class SteamAdvisorController : ControllerBase var (prod, stages, vacuum, spanAD) = ComputeStages(cur, tref, selected); object FlowRole(string baseTag, string role) - => new { pv = cur.GetValueOrDefault(role), sp = V(row.Values, $"{baseTag}.SP"), op = V(row.Values, $"{baseTag}.OP") }; + => new { Pv = cur.GetValueOrDefault(role), Sp = V(row.Values, $"{baseTag}.SP"), Op = V(row.Values, $"{baseTag}.OP") }; snapshots.Add(new { - ts = row.TimeBucket, - matchedProduct = prod?.Name ?? prod?.Label, // 미선택이면 null - stages, - vacuum, - spanAD, - flow = new { - feed = FlowRole(flowBases["feed"], "feed"), - reflux = FlowRole(flowBases["reflux"], "reflux"), - overhead = FlowRole(flowBases["overhead"], "overhead"), - bottom = FlowRole(flowBases["bottom"], "bottom"), - product = FlowRole(flowBases["product"], "product"), - steam = new { pv = cur.GetValueOrDefault("steam") }, + Ts = row.TimeBucket, + MatchedProduct = prod?.Name ?? prod?.Label, // 미선택이면 null + Stages = stages, + Vacuum = vacuum, + SpanAD = spanAD, + Flow = new { + Feed = FlowRole(flowBases["feed"], "feed"), + Reflux = FlowRole(flowBases["reflux"], "reflux"), + Overhead = FlowRole(flowBases["overhead"], "overhead"), + Bottom = FlowRole(flowBases["bottom"], "bottom"), + Product = FlowRole(flowBases["product"], "product"), + Steam = new { Pv = cur.GetValueOrDefault("steam") }, }, - flowTags = new { - feed = $"{flowBases["feed"]}.PV", - reflux = $"{flowBases["reflux"]}.PV", - overhead = $"{flowBases["overhead"]}.PV", - bottom = $"{flowBases["bottom"]}.PV", - product = $"{flowBases["product"]}.PV", - steam = $"FIQ-{ToSuffix(col).Substring(0, ToSuffix(col).Length - 2)}15.PV", + FlowTags = new { + Feed = $"{flowBases["feed"]}.PV", + Reflux = $"{flowBases["reflux"]}.PV", + Overhead = $"{flowBases["overhead"]}.PV", + Bottom = $"{flowBases["bottom"]}.PV", + Product = $"{flowBases["product"]}.PV", + Steam = $"FIQ-{ToSuffix(col).Substring(0, ToSuffix(col).Length - 2)}15.PV", }, }); } return Ok(new { - column = tref.Column, - period = tref.Period, - from, - to, - interval = "1 minute", - n = snapshots.Count, - snapshots, - timestamp = DateTime.UtcNow, + Column = tref.Column, + Period = tref.Period, + From = from, + To = to, + Interval = "1 minute", + N = snapshots.Count, + Snapshots = snapshots, + Timestamp = DateTime.UtcNow, }); } catch (Exception ex) { - return StatusCode(500, new { error = ex.Message, stack = ex.ToString() }); + return StatusCode(500, new { Error = ex.Message, Stack = ex.ToString() }); } } @@ -345,9 +345,9 @@ public sealed class SteamAdvisorController : ControllerBase // MSDS 후보 (docs/MSDS_PMA_PGMEA_EL.md). 자유 입력도 허용하되 기본 제안 목록 제공. private static readonly object[] _productSuggestions = { - new { name = "PMA", casNo = "108-65-6" }, - new { name = "PGMEA", casNo = "108-65-6" }, - new { name = "EL", casNo = "97-64-3" }, + new { Name = "PMA", CasNo = "108-65-6" }, + new { Name = "PGMEA", CasNo = "108-65-6" }, + new { Name = "EL", CasNo = "97-64-3" }, }; private string ProductLabelsPath() @@ -362,20 +362,20 @@ public sealed class SteamAdvisorController : ControllerBase public async Task GetProductLabels(string col) { var tref = await LoadTempRef(col); - if (tref is null) return NotFound(new { error = $"기준 프로파일 없음: {col}_tempref.json" }); + if (tref is null) return NotFound(new { Error = $"기준 프로파일 없음: {col}_tempref.json" }); tref = MergeProductLabels(col, tref); var clusters = tref.Products.Select(p => new { - cluster = p.Label, - rebMedian = p.Stages.GetValueOrDefault("reb_temp")?.Median, - vacMedian = p.Vacuum.Median, - nRows = p.NRows, + Cluster = p.Label, + RebMedian = p.Stages.GetValueOrDefault("reb_temp")?.Median, + VacMedian = p.Vacuum.Median, + NRows = p.NRows, // 현재 지정명: 매핑이 있으면 Name, 없으면 빈값(=미지정) - name = string.IsNullOrEmpty(p.Name) || p.Name == p.Label ? "" : p.Name, + Name = string.IsNullOrEmpty(p.Name) || p.Name == p.Label ? "" : p.Name, }).ToList(); - return Ok(new { column = col, clusters, suggestions = _productSuggestions }); + return Ok(new { Column = col, Clusters = clusters, Suggestions = _productSuggestions }); } public sealed record ProductLabelEntry @@ -389,7 +389,7 @@ public sealed class SteamAdvisorController : ControllerBase [HttpPost("productlabels/{col}")] public IActionResult SaveProductLabels(string col, [FromBody] List entries) { - if (entries is null) return BadRequest(new { error = "본문(entries) 누락" }); + if (entries is null) return BadRequest(new { Error = "본문(entries) 누락" }); var path = ProductLabelsPath(); try @@ -422,11 +422,11 @@ public sealed class SteamAdvisorController : ControllerBase Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, }); System.IO.File.WriteAllText(path, json); - return Ok(new { success = true, saved = clean.Count, path }); + return Ok(new { Success = true, Saved = clean.Count, Path = path }); } catch (Exception ex) { - return StatusCode(500, new { success = false, error = ex.Message }); + return StatusCode(500, new { Success = false, Error = ex.Message }); } } @@ -519,8 +519,8 @@ public sealed class SteamAdvisorController : ControllerBase var result = new Dictionary(); foreach (var (role, baseTag) in ficqBases) - result[role] = new { pv = P(live, $"{baseTag}.PV"), sp = P(live, $"{baseTag}.SP"), op = P(live, $"{baseTag}.OP") }; - result["steam"] = new { pv = P(live, steamTag) }; + result[role] = new { Pv = P(live, $"{baseTag}.PV"), Sp = P(live, $"{baseTag}.SP"), Op = P(live, $"{baseTag}.OP") }; + result["steam"] = new { Pv = P(live, steamTag) }; return result; } @@ -554,12 +554,12 @@ public sealed class SteamAdvisorController : ControllerBase var z = Z(c, rs); return (object)new { - stage = s, - current = c, - refMedian = rs?.Median, - refStd = rs?.Std, - z, - deviated = z is double zz && Math.Abs(zz) > 2 + Stage = s, + Current = c, + RefMedian = rs?.Median, + RefStd = rs?.Std, + Z = z, + Deviated = z is double zz && Math.Abs(zz) > 2 }; }).ToList(); @@ -569,11 +569,11 @@ public sealed class SteamAdvisorController : ControllerBase return (prod, stages, new { - current = vac, - refMedian = prod?.Vacuum.Median, - refStd = prod?.Vacuum.Std, - z = vz, - deviated = vz is double v && Math.Abs(v) > 2 + Current = vac, + RefMedian = prod?.Vacuum.Median, + RefStd = prod?.Vacuum.Std, + Z = vz, + Deviated = vz is double v && Math.Abs(v) > 2 }, spanAD); } diff --git a/src/Hc900Crawler/Controllers/TextToSqlController.cs b/src/Hc900Crawler/Controllers/TextToSqlController.cs index 897573b..7d84f89 100644 --- a/src/Hc900Crawler/Controllers/TextToSqlController.cs +++ b/src/Hc900Crawler/Controllers/TextToSqlController.cs @@ -41,11 +41,11 @@ public class TextToSqlController : ControllerBase try { var sql = await _textToSqlService.ParseNaturalLanguageAsync(dto.Query); - return Ok(new { success = true, sql }); + return Ok(new { Success = true, Sql = sql }); } catch (Exception ex) { - return Ok(new { success = false, error = ex.Message }); + return Ok(new { Success = false, Error = ex.Message }); } } diff --git a/src/Hc900Crawler/Controllers/TrendController.cs b/src/Hc900Crawler/Controllers/TrendController.cs index 392729c..d42989b 100644 --- a/src/Hc900Crawler/Controllers/TrendController.cs +++ b/src/Hc900Crawler/Controllers/TrendController.cs @@ -22,15 +22,15 @@ public class TrendController : ControllerBase [HttpGet("analog-points")] public async Task AnalogPoints([FromQuery] string? q, CancellationToken ct) { - try { return Ok(new { items = await _svc.GetAnalogPointsAsync(q ?? "", ct) }); } - catch (Exception ex) { _logger.LogError(ex, "[Trend] analog-points 실패"); return StatusCode(500, new { error = ex.Message }); } + try { return Ok(new { Items = await _svc.GetAnalogPointsAsync(q ?? "", ct) }); } + catch (Exception ex) { _logger.LogError(ex, "[Trend] analog-points 실패"); return StatusCode(500, new { Error = ex.Message }); } } [HttpGet("groups")] public async Task Groups(CancellationToken ct) { - try { return Ok(new { items = await _svc.GetGroupsAsync(ct) }); } - catch (Exception ex) { _logger.LogError(ex, "[Trend] groups 조회 실패"); return StatusCode(500, new { error = ex.Message }); } + try { return Ok(new { Items = await _svc.GetGroupsAsync(ct) }); } + catch (Exception ex) { _logger.LogError(ex, "[Trend] groups 조회 실패"); return StatusCode(500, new { Error = ex.Message }); } } [HttpGet("groups/{id:int}")] @@ -44,31 +44,31 @@ public class TrendController : ControllerBase public async Task Create([FromBody] TrendGroupUpsertDto dto, CancellationToken ct) { if (string.IsNullOrWhiteSpace(dto.Name)) - return BadRequest(new { error = "name 은 필수입니다." }); + return BadRequest(new { Error = "name 은 필수입니다." }); try { return Ok(await _svc.CreateGroupAsync(dto, ct)); } - catch (Exception ex) { _logger.LogError(ex, "[Trend] 그룹 생성 실패"); return StatusCode(500, new { error = ex.Message }); } + catch (Exception ex) { _logger.LogError(ex, "[Trend] 그룹 생성 실패"); return StatusCode(500, new { Error = ex.Message }); } } [HttpPut("groups/{id:int}")] public async Task Update(int id, [FromBody] TrendGroupUpsertDto dto, CancellationToken ct) { if (string.IsNullOrWhiteSpace(dto.Name)) - return BadRequest(new { error = "name 은 필수입니다." }); + return BadRequest(new { Error = "name 은 필수입니다." }); var g = await _svc.UpdateGroupAsync(id, dto, ct); return g is null ? NotFound() : Ok(g); } [HttpDelete("groups/{id:int}")] public async Task Delete(int id, CancellationToken ct) - => await _svc.DeleteGroupAsync(id, ct) ? Ok(new { success = true }) : NotFound(); + => await _svc.DeleteGroupAsync(id, ct) ? Ok(new { Success = true }) : NotFound(); /// 실시간 tail — 그룹 멤버 현재값(아날로그) [HttpGet("live")] public async Task Live([FromQuery] string? tags, CancellationToken ct) { var list = (tags ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - try { return Ok(new { items = await _svc.GetLiveAsync(list, ct) }); } - catch (Exception ex) { _logger.LogError(ex, "[Trend] live 실패"); return StatusCode(500, new { error = ex.Message }); } + try { return Ok(new { Items = await _svc.GetLiveAsync(list, ct) }); } + catch (Exception ex) { _logger.LogError(ex, "[Trend] live 실패"); return StatusCode(500, new { Error = ex.Message }); } } /// 알람 한계선 (HI/LO/SP 수평선용) @@ -76,15 +76,15 @@ public class TrendController : ControllerBase public async Task Limits([FromQuery] string? tags, CancellationToken ct) { var list = (tags ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - try { return Ok(new { items = await _svc.GetLimitsAsync(list, ct) }); } - catch (Exception ex) { _logger.LogError(ex, "[Trend] limits 실패"); return StatusCode(500, new { error = ex.Message }); } + try { return Ok(new { Items = await _svc.GetLimitsAsync(list, ct) }); } + catch (Exception ex) { _logger.LogError(ex, "[Trend] limits 실패"); return StatusCode(500, new { Error = ex.Message }); } } /// 운전상태 밴드 (RUN/TRIP 구간) [HttpGet("runbands")] public async Task RunBands([FromQuery] DateTime from, [FromQuery] DateTime to, [FromQuery] string? area, CancellationToken ct) { - try { return Ok(new { items = await _svc.GetRunBandsAsync(from, to, area, ct) }); } - catch (Exception ex) { _logger.LogError(ex, "[Trend] runbands 실패"); return StatusCode(500, new { error = ex.Message }); } + try { return Ok(new { Items = await _svc.GetRunBandsAsync(from, to, area, ct) }); } + catch (Exception ex) { _logger.LogError(ex, "[Trend] runbands 실패"); return StatusCode(500, new { Error = ex.Message }); } } } diff --git a/src/Hc900Crawler/wwwroot/js/docs.js b/src/Hc900Crawler/wwwroot/js/docs.js index ad576e4..ddf06a2 100644 --- a/src/Hc900Crawler/wwwroot/js/docs.js +++ b/src/Hc900Crawler/wwwroot/js/docs.js @@ -73,9 +73,9 @@ async function docsUnlock() { body: JSON.stringify({ password: pw }), }); const d = await r.json(); - if (!d.success) { alert('로그인 실패: ' + (d.error || '')); return; } - sessionStorage.setItem('kbToken', d.token); - try { kbToken = d.token; } catch (e) {} + if (!d.Success) { alert('로그인 실패: ' + (d.Error || '')); return; } + sessionStorage.setItem('kbToken', d.Token); + try { kbToken = d.Token; } catch (e) {} await docsLoadConfig(); docsToast('관리 잠금해제됨'); } catch (e) { alert('로그인 오류: ' + e.message); } @@ -85,8 +85,8 @@ async function docsUnlock() { async function docsFetchTree(path) { const r = await fetch('/api/docs/tree?path=' + encodeURIComponent(path || ''), { headers: docsHeaders() }); const d = await r.json(); - if (!d.success) throw new Error(d.error || '목록 조회 실패'); - return d.entries; + if (!d.Success) throw new Error(d.Error || '목록 조회 실패'); + return d.Entries; } async function docsRefresh() { @@ -200,7 +200,7 @@ function docsOpenPath(path) { async function docsFetchText(path) { const r = await fetch('/api/docs/text?path=' + encodeURIComponent(path), { headers: docsHeaders() }); const d = await r.json(); - if (!d.success) throw new Error(d.error || '파일 읽기 실패'); + if (!d.Success) throw new Error(d.Error || '파일 읽기 실패'); return d; } @@ -223,10 +223,10 @@ async function docsViewText(path) { try { const d = await docsFetchText(path); v.innerHTML = ''; - if (d.truncated) v.appendChild(docsTruncBanner(d.size)); + if (d.Truncated) v.appendChild(docsTruncBanner(d.Size)); const pre = document.createElement('pre'); pre.className = 'docs-text-pre'; - pre.textContent = d.text; + pre.textContent = d.Text; v.appendChild(pre); } catch (e) { docsViewerError(e); } } @@ -246,11 +246,11 @@ async function docsViewMarkdown(path) { await docsEnsureMdLibs(); const d = await docsFetchText(path); v.innerHTML = ''; - if (d.truncated) v.appendChild(docsTruncBanner(d.size)); + if (d.Truncated) v.appendChild(docsTruncBanner(d.Size)); const body = document.createElement('div'); body.className = 'md-body'; v.appendChild(body); - docsRenderMarkdownInto(body, d.text); + docsRenderMarkdownInto(body, d.Text); } catch (e) { docsViewerError(e); } } @@ -549,7 +549,7 @@ async function docsEditStart() { ${isMd ? '
' : ''} `; const ta = document.getElementById('docs-edit-ta'); - ta.value = d.text; + ta.value = d.Text; if (isMd) { await docsEnsureMdLibs(); const prev = document.getElementById('docs-edit-prev'); @@ -570,7 +570,7 @@ async function docsEditSave() { body: JSON.stringify({ path: f.path, content: ta.value }), }); const d = await r.json(); - if (!d.success) { alert('저장 실패: ' + (d.error || r.status)); return; } + if (!d.Success) { alert('저장 실패: ' + (d.Error || r.status)); return; } docsState.editing = false; docsOpenPath(f.path); docsToast('저장됨'); @@ -592,7 +592,7 @@ async function docsMkdirPrompt() { body: JSON.stringify({ path }), }); const d = await r.json(); - if (!d.success) { alert('실패: ' + (d.error || '')); return; } + if (!d.Success) { alert('실패: ' + (d.Error || '')); return; } await docsRefresh(); docsToast('폴더 생성됨'); } catch (e) { alert(e.message); } @@ -614,9 +614,9 @@ async function docsUploadDo() { const r = await fetch('/api/docs/upload', { method: 'POST', headers: docsHeaders(), body: fd }); const d = await r.json(); input.value = ''; - if (!d.success) { alert('업로드 실패: ' + (d.error || '')); return; } + if (!d.Success) { alert('업로드 실패: ' + (d.Error || '')); return; } await docsRefresh(); - docsToast('업로드됨: ' + d.path); + docsToast('업로드됨: ' + d.Path); } catch (e) { input.value = ''; alert(e.message); } } @@ -630,7 +630,7 @@ async function docsRenamePrompt(path) { body: JSON.stringify({ from: path, to: next }), }); const d = await r.json(); - if (!d.success) { alert('실패: ' + (d.error || '')); return; } + if (!d.Success) { alert('실패: ' + (d.Error || '')); return; } await docsRefresh(); docsToast('이름 변경됨'); } catch (e) { alert(e.message); } @@ -647,7 +647,7 @@ async function docsDeletePrompt(path, type) { const url = '/api/docs?path=' + encodeURIComponent(path) + (isDir ? '&recursive=true' : ''); const r = await fetch(url, { method: 'DELETE', headers: docsHeaders() }); const d = await r.json(); - if (!d.success) { alert('삭제 실패: ' + (d.error || '')); return; } + if (!d.Success) { alert('삭제 실패: ' + (d.Error || '')); return; } if (docsState.curFile && docsState.curFile.path === path) { docsState.curFile = null; document.getElementById('docs-cur-path').textContent = '파일을 선택하세요'; diff --git a/src/Hc900Crawler/wwwroot/js/fast.js b/src/Hc900Crawler/wwwroot/js/fast.js index 1571969..fc33d2b 100644 --- a/src/Hc900Crawler/wwwroot/js/fast.js +++ b/src/Hc900Crawler/wwwroot/js/fast.js @@ -65,7 +65,7 @@ async function fastSessionsLoad() { const list = document.getElementById('fast-session-list'); list.innerHTML = ''; - if (!data.items || data.items.length === 0) { + if (!data.Items || data.Items.length === 0) { list.innerHTML = '세션이 없습니다. + 신규를 눌러 시작하세요.'; return; } @@ -80,7 +80,7 @@ async function fastSessionsLoad() { Failed: '실패', RowLimitReached: '행제한', Pending: '대기' }; - data.items.forEach(s => { + data.Items.forEach(s => { const isActive = s.Id === fastCurrentSessionId; const dot = statusColor[s.Status] ?? '#aaa'; const label = statusLabel[s.Status] ?? s.Status; @@ -137,7 +137,7 @@ async function fastStart() { if (!res.ok) { const err = await res.json(); - alert('오류: ' + (err.error ?? '알 수 없는 오류')); + alert('오류: ' + (err.Error ?? '알 수 없는 오류')); return; } @@ -228,14 +228,14 @@ async function fastRenderChart() { const container = document.getElementById('fast-chart-container'); - if (!data.items || data.items.length === 0) { + if (!data.Items || data.Items.length === 0) { container.innerHTML = '
수집된 데이터가 없습니다.
'; return; } // Long 포맷 → PIVOT (recorded_at 기준 그룹화) const grouped = {}; - for (const r of data.items) { + for (const r of data.Items) { if (!grouped[r.RecordedAt]) grouped[r.RecordedAt] = {}; grouped[r.RecordedAt][r.TagName] = parseFloat(r.Value) || null; } @@ -244,10 +244,10 @@ async function fastRenderChart() { const timesNum = times.map(t => new Date(t).getTime() / 1000); // uPlot: Unix seconds // uPlot data: [[x...], [y1...], [y2...], ...] - const uData = [timesNum, ...data.tagNames.map(tag => times.map(t => grouped[t][tag] ?? null))]; + const uData = [timesNum, ...data.TagNames.map(tag => times.map(t => grouped[t][tag] ?? null))]; // 동일 태그 구성이면 setData()로 인플레이스 업데이트 → zoom/pan 상태 유지 - const tagsKey = data.tagNames.join('\0'); + const tagsKey = data.TagNames.join('\0'); if (fastChart && fastChartTagNames === tagsKey) { fastChart.setData(uData, false); // false = 스케일 유지 (zoom 보존) return; @@ -272,7 +272,7 @@ async function fastRenderChart() { ], series: [ {}, - ...data.tagNames.map(tag => ({ + ...data.TagNames.map(tag => ({ label: tag, stroke: fastTagColor(tag), width: 2 @@ -458,15 +458,15 @@ paneInit.fast = function() { // Long → Wide (배열의 배열 형식으로 XLSX.utils.aoa_to_sheet에 전달) const timeMap = {}; - for (const r of data.items) { + for (const r of data.Items) { if (!timeMap[r.RecordedAt]) timeMap[r.RecordedAt] = {}; timeMap[r.RecordedAt][r.TagName] = r.Value; } - const rows = [['recorded_at', ...data.tagNames]]; + const rows = [['recorded_at', ...data.TagNames]]; for (const t of Object.keys(timeMap).sort()) { rows.push([new Date(t).toLocaleString('ko-KR'), - ...data.tagNames.map(tag => timeMap[t][tag] ?? '')]); + ...data.TagNames.map(tag => timeMap[t][tag] ?? '')]); } const ws = XLSX.utils.aoa_to_sheet(rows); diff --git a/src/Hc900Crawler/wwwroot/js/ff.js b/src/Hc900Crawler/wwwroot/js/ff.js index ba13bc7..53d32e6 100644 --- a/src/Hc900Crawler/wwwroot/js/ff.js +++ b/src/Hc900Crawler/wwwroot/js/ff.js @@ -175,8 +175,8 @@ async function ffLoadDash() { catch (e) { return; } try { const ramp = await api('GET', '/api/ff/feed-ramp'); - ffRampJobs = {}; (ramp.jobs || []).forEach(j => ffRampJobs[j.columnId] = j); - ffRampDryRun = !!ramp.dryRun; + ffRampJobs = {}; (ramp.Jobs || []).forEach(j => ffRampJobs[j.columnId] = j); + ffRampDryRun = !!ramp.DryRun; const modeEl = document.getElementById('ff-ramp-mode'); if (modeEl) modeEl.textContent = ffRampDryRun ? '모의(DryRun)' : '실쓰기 모드'; } catch (e) { /* 무시 */ } @@ -185,7 +185,7 @@ async function ffLoadDash() { // FEED Target SP 입력 중이면 재렌더 보류(타이핑/포커스 유지) const ae = document.activeElement; if (ae && ae.classList && ae.classList.contains('ff-rt')) return; - const cols = data.columns || []; + const cols = data.Columns || []; if (!cols.length) { host.innerHTML = '
활성 컬럼 없음
'; return; } host.innerHTML = cols.map(ffCard).join(''); } @@ -468,16 +468,16 @@ function ffMsg(m, err) { const e=document.getElementById('ff-cfg-msg'); e.textCo async function ffLoadConfig() { const data = await ffApi('GET', '/api/ff/config'); const host = document.getElementById('ff-cfg-list'); - host.innerHTML = (data.columns||[]).map(ffCfgRow).join('') || '
설정 없음
'; + host.innerHTML = (data.Columns||[]).map(ffCfgRow).join('') || '
설정 없음
'; host.querySelectorAll('[data-edit]').forEach(b => b.onclick = () => - ffEditColumn(data.columns.find(c => c.id == b.dataset.edit))); + ffEditColumn(data.Columns.find(c => c.id == b.dataset.edit))); host.querySelectorAll('[data-del]').forEach(b => b.onclick = () => ffDelete(b.dataset.del)); } function ffCfgRow(c) { - return `
${esc(c.name)} (id ${c.id}) — feed ${esc(c.feedTag)}, - 스트림 ${c.streams.length}개, ${c.enabled?'활성':'비활성'} - -
`; + return `
${esc(c.Name)} (id ${c.Id}) — feed ${esc(c.FeedTag)}, + 스트림 ${c.Streams.length}개, ${c.Enabled?'활성':'비활성'} + +
`; } async function ffDelete(id) { if (!confirm(`컬럼 ${id} 삭제?`)) return; diff --git a/src/Hc900Crawler/wwwroot/js/hist.js b/src/Hc900Crawler/wwwroot/js/hist.js index 5b02638..2c7fb14 100644 --- a/src/Hc900Crawler/wwwroot/js/hist.js +++ b/src/Hc900Crawler/wwwroot/js/hist.js @@ -119,14 +119,14 @@ async function histQuery() { try { const d = await api('POST', '/api/text-to-sql/query-history-interval', body); - if (!d.success) { - throw new Error(d.error || '조회 실패'); + if (!d.Success) { + throw new Error(d.Error || '조회 실패'); } - const rows = d.rows || []; - const tNames = d.tagNames || []; + const rows = d.Rows || []; + const tNames = d.TagNames || []; - renderHistoryTable(rows, tNames, interval, d.baseIntervalSeconds, d.queryInterval); + renderHistoryTable(rows, tNames, interval, d.BaseIntervalSeconds, d.QueryInterval); } catch (e) { histShowStatus('err', '❌', '조회 실패', ` ${e.message}\n\n컨솔에서 상세 오류를 확인하세요.`); setGlobal('err', '조회 실패'); diff --git a/src/Hc900Crawler/wwwroot/js/kbadmin.js b/src/Hc900Crawler/wwwroot/js/kbadmin.js index d65735d..ac177fa 100644 --- a/src/Hc900Crawler/wwwroot/js/kbadmin.js +++ b/src/Hc900Crawler/wwwroot/js/kbadmin.js @@ -26,7 +26,7 @@ async function kbFetch(method, path, body, opt) { paneInit.kbadmin = async function() { if (kbToken) { const r = await kbFetch('GET', '/api/kb/auth/status'); - if (r.ok && r.data && r.data.valid) { + if (r.ok && r.data && r.data.Valid) { kbShowMain(); } else { kbShowLogin('세션이 만료되었습니다. 다시 로그인하세요.'); @@ -76,12 +76,12 @@ async function kbLogin() { body: JSON.stringify({ password: pw }) }); const data = await r.json().catch(() => ({})); - if (!r.ok || !data.success) { - msg.textContent = '❌ ' + (data.error || '로그인 실패'); + if (!r.ok || !data.Success) { + msg.textContent = '❌ ' + (data.Error || '로그인 실패'); return; } - kbToken = data.token; - kbExpiresAt = data.expiresAt; + kbToken = data.Token; + kbExpiresAt = data.ExpiresAt; sessionStorage.setItem('kbToken', kbToken); sessionStorage.setItem('kbExpiresAt', kbExpiresAt || ''); document.getElementById('kb-pw').value = ''; @@ -97,14 +97,14 @@ async function kbLogout() { // ── 컬렉션 ───────────────────────────────────────────── async function kbLoadCollections() { const r = await kbFetch('GET', '/api/kb/collections'); - if (!r.ok || !r.data || !r.data.success) return; - kbCollections = r.data.items || []; + if (!r.ok || !r.data || !r.data.Success) return; + kbCollections = r.data.Items || []; const fSel = document.getElementById('kb-f-coll'); const uSel = document.getElementById('kb-up-coll'); fSel.innerHTML = '' + - kbCollections.map(c => ``).join(''); + kbCollections.map(c => ``).join(''); uSel.innerHTML = '' + - kbCollections.map(c => ``).join(''); + kbCollections.map(c => ``).join(''); } // ── 목록 ─────────────────────────────────────────────── @@ -119,11 +119,11 @@ async function kbRefresh() { qs.set('pageSize', '200'); const r = await kbFetch('GET', '/api/kb/documents?' + qs.toString()); - if (!r.ok || !r.data || !r.data.success) { + if (!r.ok || !r.data || !r.data.Success) { document.getElementById('kb-doc-table').innerHTML = '
조회 실패
'; return; } - kbRenderDocs(r.data.items, r.data.total); + kbRenderDocs(r.data.Items, r.data.Total); } function kbRenderDocs(items, total) { @@ -134,26 +134,26 @@ function kbRenderDocs(items, total) { tbl.innerHTML = '
문서 없음
'; return; } - const collMap = Object.fromEntries(kbCollections.map(c => [c.key, c.name])); + const collMap = Object.fromEntries(kbCollections.map(c => [c.Key, c.Name])); const rows = items.map(d => { - const tags = (d.tags || []).map(t => `${t}`).join(' '); - const dt = d.uploadedAt ? new Date(d.uploadedAt).toLocaleString('ko-KR') : ''; - const size = d.fileSize ? kbFmtSize(d.fileSize) : ''; + const tags = (d.Tags || []).map(t => `${t}`).join(' '); + const dt = d.UploadedAt ? new Date(d.UploadedAt).toLocaleString('ko-KR') : ''; + const size = d.FileSize ? kbFmtSize(d.FileSize) : ''; return ` - ${kbShortId(d.id)} - ${kbEscape(d.title)} - ${collMap[d.collection] || d.collection} + ${kbShortId(d.Id)} + ${kbEscape(d.Title)} + ${collMap[d.Collection] || d.Collection} ${tags} ${size} - ${d.status}${d.errorMessage ? `
${kbEscape(d.errorMessage.slice(0,60))}…
`:''} - ${d.chunkCount || 0} + ${d.Status}${d.ErrorMessage ? `
${kbEscape(d.ErrorMessage.slice(0,60))}…
`:''} + ${d.ChunkCount || 0} ${dt} - - ${(d.chunkCount || 0) > 0 ? `` : ''} - - - + + ${(d.ChunkCount || 0) > 0 ? `` : ''} + + + `; }).join(''); @@ -199,8 +199,8 @@ async function kbUploadSubmit() { msg.textContent = '업로드 중...'; const r = await fetch('/api/kb/upload', { method: 'POST', headers: kbHeaders(), body: fd }); const data = await r.json().catch(() => ({})); - if (!r.ok || !data.success) { - msg.textContent = '❌ ' + (data.error || ('HTTP ' + r.status)); + if (!r.ok || !data.Success) { + msg.textContent = '❌ ' + (data.Error || ('HTTP ' + r.status)); return; } msg.textContent = '✅ 업로드 완료. 인덱싱 진행 중...'; @@ -218,8 +218,8 @@ async function kbShowChunks(id, title) { modal.classList.remove('hidden'); const r = await kbFetch('GET', '/api/kb/documents/' + id + '/chunks?limit=200'); - if (!r.ok || !r.data || !r.data.success) { - body.innerHTML = '
조회 실패: ' + ((r.data && r.data.error) || r.status) + '
'; + if (!r.ok || !r.data || !r.data.Success) { + body.innerHTML = '
조회 실패: ' + ((r.data && r.data.Error) || r.status) + '
'; return; } kbRenderChunks(r.data.chunks || []); @@ -262,7 +262,7 @@ function kbDownload(id) { async function kbReindex(id) { if (!confirm('재인덱싱하시겠습니까? (Qdrant 기존 청크 삭제 후 다시 처리)')) return; const r = await kbFetch('POST', '/api/kb/documents/' + id + '/reindex'); - if (!r.ok) alert('실패: ' + (r.data && r.data.error ? r.data.error : r.status)); + if (!r.ok) alert('실패: ' + (r.data && r.data.Error ? r.data.Error : r.status)); kbRefresh(); } @@ -284,7 +284,7 @@ async function kbBulkDisable() { const title = prompt('일괄 비활성화할 제목을 정확히 입력하세요:'); if (!title) return; const r = await kbFetch('POST', '/api/kb/documents/bulk-disable', { title }); - if (r.ok && r.data && r.data.success) alert(`${r.data.affected}건 비활성화 완료`); + if (r.ok && r.data && r.data.Success) alert(`${r.data.Affected}건 비활성화 완료`); else alert('실패'); kbRefresh(); } @@ -299,7 +299,7 @@ async function kbPurgeDisabled() { } if (!confirm('정말 영구삭제하시겠습니까? (되돌릴 수 없습니다)')) return; const r = await kbFetch('POST', '/api/kb/documents/purge-disabled', body); - if (r.ok && r.data && r.data.success) alert(`${r.data.deleted}건 영구삭제 완료`); + if (r.ok && r.data && r.data.Success) alert(`${r.data.Deleted}건 영구삭제 완료`); else alert('실패'); kbRefresh(); } @@ -321,11 +321,11 @@ async function kbChangePwSubmit() { if (!oldPw || !newPw) { msg.textContent = '❌ 비밀번호를 입력하세요.'; return; } if (newPw.length < 6) { msg.textContent = '❌ 새 비밀번호는 6자 이상.'; return; } const r = await kbFetch('POST', '/api/kb/auth/change-password', { oldPassword: oldPw, newPassword: newPw }); - if (r.ok && r.data && r.data.success) { + if (r.ok && r.data && r.data.Success) { msg.textContent = '✅ 변경 완료. 다시 로그인해 주세요.'; setTimeout(() => { kbChangePwClose(); kbLogout(); }, 800); } else { - msg.textContent = '❌ ' + (r.data && r.data.error ? r.data.error : '변경 실패'); + msg.textContent = '❌ ' + (r.data && r.data.Error ? r.data.Error : '변경 실패'); } } @@ -338,7 +338,7 @@ function kbStartPoll() { const r = await kbFetch('GET', '/api/kb/documents?status=parsing&pageSize=1'); const r2 = await kbFetch('GET', '/api/kb/documents?status=embedding&pageSize=1'); const r3 = await kbFetch('GET', '/api/kb/documents?status=pending&pageSize=1'); - const active = [r, r2, r3].some(x => x.ok && x.data && x.data.total > 0); + const active = [r, r2, r3].some(x => x.ok && x.data && x.data.Total > 0); if (active) kbRefresh(); }, 1500); } @@ -369,8 +369,8 @@ async function kbInferStart() { seedDocId: '' }); - if (!r.ok || !r.data || !r.data.success) { - msg.textContent = '❌ ' + (r.data && r.data.error ? r.data.error : 'HTTP ' + r.status); + if (!r.ok || !r.data || !r.data.Success) { + msg.textContent = '❌ ' + (r.data && r.data.Error ? r.data.Error : 'HTTP ' + r.status); return; } diff --git a/src/Hc900Crawler/wwwroot/js/llmchat.js b/src/Hc900Crawler/wwwroot/js/llmchat.js index 5c032c4..2d9c8c0 100644 --- a/src/Hc900Crawler/wwwroot/js/llmchat.js +++ b/src/Hc900Crawler/wwwroot/js/llmchat.js @@ -359,13 +359,13 @@ function llmRenderToolPayload(name, ok, preview, payload) { // search_kb 형태 if (Array.isArray(j.hits)) return llmRenderKbHits(j.hits); // run_sql/query_with_nl 형태: {success, columns:[], data:[{...}, ...]} - if (j.success && Array.isArray(j.columns) && Array.isArray(j.data)) { + if (j.Success && Array.isArray(j.Columns) && Array.isArray(j.data)) { const ts = llmDetectTimeSeries(j.data); const sparkHtml = ts ? llmBuildSparklineHtml(j.data, ts) : ''; - return sparkHtml + llmRenderTable(j.columns, j.data); + return sparkHtml + llmRenderTable(j.Columns, j.data); } - // query_pv_history 형태: {success, data:[{tag_name, timestamp, value}, ...]} - if (j.success && Array.isArray(j.data) && j.data.length > 0 && typeof j.data[0] === 'object') { + // query_pv_history 형태: {Success, data:[{tag_name, timestamp, value}, ...]} + if (j.Success && Array.isArray(j.data) && j.data.length > 0 && typeof j.data[0] === 'object') { const cols = Object.keys(j.data[0]); const ts = llmDetectTimeSeries(j.data); const sparkHtml = ts ? llmBuildSparklineHtml(j.data, ts) : ''; @@ -502,8 +502,8 @@ async function llmLoadModels() { const d = await api('GET', `${prefix}/models`); sel.innerHTML = ''; - if (d.success && d.models) { - d.models.forEach(m => { + if (d.Success && d.Models) { + d.Models.forEach(m => { const opt = document.createElement('option'); opt.value = m; opt.textContent = m; @@ -515,14 +515,14 @@ async function llmLoadModels() { if (llmType === 'vllm') { try { const cfg = await api('GET', '/api/ollama/config'); - if (cfg.success && cfg.vllmModel) { - if (![...sel.options].some(o => o.value === cfg.vllmModel)) { + if (cfg.Success && cfg.VllmModel) { + if (![...sel.options].some(o => o.value === cfg.VllmModel)) { const opt = document.createElement('option'); - opt.value = cfg.vllmModel; - opt.textContent = cfg.vllmModel; + opt.value = cfg.VllmModel; + opt.textContent = cfg.VllmModel; sel.appendChild(opt); } - sel.value = cfg.vllmModel; + sel.value = cfg.VllmModel; } } catch (_) {} } @@ -534,8 +534,8 @@ async function llmLoadModels() { const dot = document.getElementById('llm-conn-status'); if (dot) { - dot.className = d.success ? 'llm-conn-dot connected' : 'llm-conn-dot error'; - dot.title = d.success ? `${label} 연결됨` : `${label} 연결 실패`; + dot.className = d.Success ? 'llm-conn-dot connected' : 'llm-conn-dot error'; + dot.title = d.Success ? `${label} 연결됨` : `${label} 연결 실패`; } } catch (e) { const dot = document.getElementById('llm-conn-status'); @@ -578,8 +578,11 @@ async function llmLoadMcpTools() { if (!llmUseTools) { llmMcpTools = []; return; } try { const d = await api('GET', '/api/text-to-sql/tools'); - if (d.success && d.tools) { - llmMcpTools = d.tools.map(t => ({ + // 백엔드 응답 필드는 PascalCase(Success/Tools) — camelCase 폴백 병행 + const ok = d.Success ?? d.success; + const toolList = d.Tools ?? d.tools; + if (ok && toolList) { + llmMcpTools = toolList.map(t => ({ type: 'function', function: { name: t.Name || t.name, @@ -638,8 +641,8 @@ async function llmEnsureSummary(sess, model) { try { const r = await api('POST', '/api/ollama/summarize', { model, messages: merged }); - if (r && r.success && r.summary) { - sess.summary = r.summary; + if (r && r.Success && r.Summary) { + sess.summary = r.Summary; sess.summarizedUpTo = targetEnd; llmSaveSessions(); } @@ -948,10 +951,10 @@ async function llmSaveConfig() { const port = parseInt(document.getElementById('llm-port').value) || 11434; try { const d = await api('POST', '/api/ollama/config', { host, port }); - if (d.success) { + if (d.Success) { alert('설정 저장 완료. 변경 사항 적용을 위해 페이지를 새로고침하세요.'); } else { - alert('설정 저장 실패: ' + (d.error || '알 수 없는 오류')); + alert('설정 저장 실패: ' + (d.Error || '알 수 없는 오류')); } } catch (e) { alert('설정 저장 실패: ' + e.message); @@ -965,10 +968,10 @@ async function llmTestConnection() { const d = await api('GET', `${prefix}/ping`); const dot = document.getElementById('llm-conn-status'); if (dot) { - dot.className = d.success ? 'llm-conn-dot connected' : 'llm-conn-dot error'; - dot.title = d.success ? `${label} 연결됨` : `${label} 연결 실패: ${d.error || ''}`; + dot.className = d.Success ? 'llm-conn-dot connected' : 'llm-conn-dot error'; + dot.title = d.Success ? `${label} 연결됨` : `${label} 연결 실패: ${d.Error || ''}`; } - alert(d.success ? `${label} 연결 성공!` : `${label} 연결 실패: ${d.error || ''}`); + alert(d.Success ? `${label} 연결 성공!` : `${label} 연결 실패: ${d.Error || ''}`); } catch (e) { alert(`${label} 연결 테스트 실패: ` + e.message); } @@ -976,25 +979,25 @@ async function llmTestConnection() { function llmLoadConfigToUI() { api('GET', '/api/ollama/config').then(d => { - if (d.success) { + if (d.Success) { const hostEl = document.getElementById('llm-host'); const portEl = document.getElementById('llm-port'); - if (hostEl && d.host) hostEl.value = d.host; - if (portEl && d.port) portEl.value = d.port; + if (hostEl && d.Host) hostEl.value = d.Host; + if (portEl && d.Port) portEl.value = d.Port; } }).catch(() => {}); api('GET', '/api/ollama/config').then(d => { - if (d.success && d.vllmModel) { + if (d.Success && d.VllmModel) { const sel = document.getElementById('llm-model-select'); if (!sel) return; - if (![...sel.options].some(o => o.value === d.vllmModel)) { + if (![...sel.options].some(o => o.value === d.VllmModel)) { const opt = document.createElement('option'); - opt.value = d.vllmModel; - opt.textContent = d.vllmModel; + opt.value = d.VllmModel; + opt.textContent = d.VllmModel; sel.appendChild(opt); } - sel.value = d.vllmModel; + sel.value = d.VllmModel; } }).catch(() => {}); } diff --git a/src/Hc900Crawler/wwwroot/js/pb.js b/src/Hc900Crawler/wwwroot/js/pb.js index bad234b..7463c58 100644 --- a/src/Hc900Crawler/wwwroot/js/pb.js +++ b/src/Hc900Crawler/wwwroot/js/pb.js @@ -81,7 +81,7 @@ async function pbBuild() { if (!confirm('모든 활성 태그를 해제하고 조건에 맞는 태그만 활성화합니다. 계속하시겠습니까?')) return; const payload = pbCollectAllGroups(); const res = await pbApi('/api/pointbuilder/build', 'POST', payload); - alert(res.message || `${res.count}개 활성화됨`); + alert(res.Message || `${res.Count}개 활성화됨`); pbRefresh(); pbLoadSummary(); } @@ -97,7 +97,7 @@ async function pbApply() { if (!ctrl) { alert('컨트롤러를 선택해주세요.'); return; } if (!confirm(`[${ctrl}] 기존 활성화를 모두 해제하고 선택한 ${selected.length}개만 활성화합니다. 계속하시겠습니까?`)) return; const res = await pbApi('/api/pointbuilder/apply', 'POST', { selectedTagNames: selected, controllerId: ctrl }); - alert(res.message || `${res.count}개 활성화됨`); + alert(res.Message || `${res.Count}개 활성화됨`); pbRefresh(); pbLoadSummary(); } @@ -108,8 +108,8 @@ async function pbAppend() { if (selected.length === 0) { alert('미리보기에서 추가할 태그를 선택해주세요.'); return; } if (!ctrl) { alert('컨트롤러를 선택해주세요.'); return; } const res = await pbApi('/api/pointbuilder/append', 'POST', { selectedTagNames: selected, controllerId: ctrl }); - if (res.count > 0) { - alert(`${res.count}개 추가됨`); + if (res.Count > 0) { + alert(`${res.Count}개 추가됨`); } else { alert('추가할 새 태그가 없습니다 (이미 모두 활성화되어 있음)'); } @@ -132,7 +132,7 @@ async function pbAddManual() { }; const res = await pbApi('/api/pointbuilder/add', 'POST', payload); - if (res.success) { + if (res.Success) { alert(`등록 완료: ${tagName}`); document.getElementById('pb-add-tag').value = ''; document.getElementById('pb-add-addr').value = ''; @@ -140,7 +140,7 @@ async function pbAddManual() { pbRefresh(); pbLoadSummary(); } else { - alert(`오류: ${res.error}`); + alert(`오류: ${res.Error}`); } } @@ -227,7 +227,7 @@ function pbRenderPreview(res) { const tbody = document.getElementById('pb-preview-tbody'); const count = document.getElementById('pb-preview-count'); - if (!res.items || res.items.length === 0) { + if (!res.Items || res.Items.length === 0) { area.style.display = 'block'; count.textContent = '(0건)'; tbody.innerHTML = '조건에 맞는 태그가 없습니다'; @@ -235,20 +235,20 @@ function pbRenderPreview(res) { } area.style.display = 'block'; - count.textContent = `(${res.count}건)`; + count.textContent = `(${res.Count}건)`; - tbody.innerHTML = res.items.map(item => { - const badgeClass = `param-badge p-${item.paramType}`.replace(/[^a-zA-Z0-9\s-]/g, ''); + tbody.innerHTML = res.Items.map(item => { + const badgeClass = `param-badge p-${item.ParamType}`.replace(/[^a-zA-Z0-9\s-]/g, ''); return ` - - ${item.tagName} - ${item.paramType || '—'} - ${item.dataType} - 0x${(item.modbusAddr ?? 0).toString(16).toUpperCase().padStart(4,'0')} - ${item.loopNo ?? '—'} - ${item.access} - ${item.group} - ${item.isActive ? '✓' : ''} + + ${item.TagName} + ${item.ParamType || '—'} + ${item.DataType} + 0x${(item.ModbusAddr ?? 0).toString(16).toUpperCase().padStart(4,'0')} + ${item.LoopNo ?? '—'} + ${item.Access} + ${item.Group} + ${item.IsActive ? '✓' : ''} `; }).join(''); } @@ -346,7 +346,7 @@ async function pbRealtimeAdd() { if (!ctrl) { alert('컨트롤러를 선택해주세요.'); return; } if (!confirm(`[${ctrl}] 선택한 ${ids.length}개를 실시간(realtime_table)에 추가합니다. 계속하시겠습니까?`)) return; const res = await pbApi('/api/pointbuilder/realtime-add', 'POST', { ids, controllerId: ctrl }); - alert(res.message || `${res.count}개 실시간 편입`); + alert(res.Message || `${res.Count}개 실시간 편입`); pbRefresh(); pbLoadSummary(); } @@ -359,7 +359,7 @@ async function pbRealtimeRemove() { if (!ctrl) { alert('컨트롤러를 선택해주세요.'); return; } if (!confirm(`[${ctrl}] 선택한 ${ids.length}개를 실시간(realtime_table)에서 제거합니다. 계속하시겠습니까?`)) return; const res = await pbApi('/api/pointbuilder/realtime-remove', 'POST', { ids, controllerId: ctrl }); - alert(res.message || `${res.count}개 실시간 제거`); + alert(res.Message || `${res.Count}개 실시간 제거`); pbRefresh(); pbLoadSummary(); } @@ -367,7 +367,7 @@ async function pbRealtimeRemove() { async function pbDeleteOne(id) { if (!confirm('이 태그를 비활성화하고 realtime_table에서 제거합니다. 계속하시겠습니까?')) return; const res = await pbApi(`/api/pointbuilder/${id}`, 'DELETE'); - if (res.success) { + if (res.Success) { pbRefresh(); pbLoadSummary(); } @@ -431,10 +431,10 @@ async function pbSinamUploadBtn() { fd.append('file', file); const res = await fetch('/api/pointbuilder/sinam/upload', { method: 'POST', body: fd }); const data = await res.json(); - if (!data.success) throw new Error(data.error || 'upload failed'); + if (!data.Success) throw new Error(data.Error || 'upload failed'); await pbSinamLoadExisting(); - _pbSinamSetFile(data.file); + _pbSinamSetFile(data.File); fileInput.value = ''; } catch (e) { alert('업로드 오류: ' + e.message); @@ -466,17 +466,17 @@ async function pbSinamParse() { try { const res = await pbApi('/api/pointbuilder/sinam/parse', 'POST', { file: filePath, controller: ctrl, applyDb }); - if (!res.success) { - resultEl.innerHTML = `오류: ${res.error || '알 수 없는 오류'}`; + if (!res.Success) { + resultEl.innerHTML = `오류: ${res.Error || '알 수 없는 오류'}`; return; } const sel = document.getElementById('pb-sinam-existing'); const fileName = sel?.options[sel.selectedIndex]?.text || ''; const lines = [`✓ 완료`]; - if (res.registers !== undefined) lines.push(`레지스터: ${res.registers}개`); - if (res.archive !== undefined) lines.push(`archive: ${res.archive}개`); - if (res.loops !== undefined) lines.push(`루프: ${res.loops}개`); + if (res.Registers !== undefined) lines.push(`레지스터: ${res.Registers}개`); + if (res.Archive !== undefined) lines.push(`archive: ${res.Archive}개`); + if (res.Loops !== undefined) lines.push(`루프: ${res.Loops}개`); lines.push(`${fileName}`); resultEl.innerHTML = lines.join('
'); @@ -500,9 +500,9 @@ async function pbSinamGwRestart() { resultEl.innerHTML = '재시작 중...'; try { const res = await pbApi(`/api/setup/gateway/restart?id=${ctrl}`, 'POST'); - resultEl.innerHTML = res.success - ? `✓ ${res.message || '재시작 완료'}` - : `${res.message || '재시작 실패'}`; + resultEl.innerHTML = res.Success + ? `✓ ${res.Message || '재시작 완료'}` + : `${res.Message || '재시작 실패'}`; } catch (e) { resultEl.innerHTML = `오류: ${e.message}`; } diff --git a/src/Hc900Crawler/wwwroot/js/pid-viewer.js b/src/Hc900Crawler/wwwroot/js/pid-viewer.js index e280edf..d4827d9 100644 --- a/src/Hc900Crawler/wwwroot/js/pid-viewer.js +++ b/src/Hc900Crawler/wwwroot/js/pid-viewer.js @@ -17,7 +17,7 @@ function pidHandleError(error, context = '작업') { let msg = '알 수 없는 오류가 발생했습니다.'; if (error && typeof error === 'object') { - msg = error.message || error.error || error.details || (error instanceof Error ? error.message : msg); + msg = error.message || error.Error || error.Details || (error instanceof Error ? error.message : msg); } else if (typeof error === 'string') { msg = error; } @@ -66,7 +66,7 @@ async function pidRestoreBuildStatus() { try { const statusRes = await api('GET', `/api/pidgraph/status/${savedTaskId}`); - if (statusRes.status === 'Processing' || statusRes.status === 'Starting') { + if (statusRes.Status === 'Processing' || statusRes.Status === 'Starting') { progWrap.classList.remove('hidden'); const startTime = parseInt(savedStartTime); @@ -90,18 +90,18 @@ async function pidRestoreBuildStatus() { eventSource.onmessage = (event) => { const res = JSON.parse(event.data); - progBar.style.width = `${res.progress}%`; - statusTxt.textContent = res.message; + progBar.style.width = `${res.Progress}%`; + statusTxt.textContent = res.Message; updateTimer(); - if (res.status === 'Completed') { + if (res.Status === 'Completed') { statusTxt.textContent = '추출이 완료되었습니다.'; progWrap.classList.add('hidden'); localStorage.removeItem('pid_build_task_id'); localStorage.removeItem('pid_build_start_time'); eventSource.close(); - } else if (res.status === 'Failed') { - statusTxt.textContent = `오류 발생: ${res.message}`; + } else if (res.Status === 'Failed') { + statusTxt.textContent = `오류 발생: ${res.Message}`; progWrap.classList.add('hidden'); localStorage.removeItem('pid_build_task_id'); localStorage.removeItem('pid_build_start_time'); @@ -119,7 +119,7 @@ async function pidRestoreBuildStatus() { setInterval(updateTimer, 1000); startSse(savedTaskId); - } else if (statusRes.status === 'Completed') { + } else if (statusRes.Status === 'Completed') { localStorage.removeItem('pid_build_task_id'); localStorage.removeItem('pid_build_start_time'); } @@ -193,7 +193,7 @@ async function pidBuildGraph(filepath) { // 1. 빌드 요청 statusTxt.textContent = '추출 요청 중...'; const res = await api('POST', '/api/pidgraph/build', { filepath }); - const taskId = res.taskId; + const taskId = res.Data.TaskId; // 상태 복구를 위해 localStorage에 저장 localStorage.setItem('pid_build_task_id', taskId); @@ -205,17 +205,17 @@ async function pidBuildGraph(filepath) { eventSource.onmessage = (event) => { const statusRes = JSON.parse(event.data); - progBar.style.width = `${statusRes.progress}%`; - statusTxt.textContent = statusRes.message; + progBar.style.width = `${statusRes.Progress}%`; + statusTxt.textContent = statusRes.Message; updateTimer(); - if (statusRes.status === 'Completed') { + if (statusRes.Status === 'Completed') { eventSource.close(); setGlobal('ok', '추출 완료'); resolve(); - } else if (statusRes.status === 'Failed') { + } else if (statusRes.Status === 'Failed') { eventSource.close(); - reject(new Error(statusRes.message)); + reject(new Error(statusRes.Message)); } }; diff --git a/src/Hc900Crawler/wwwroot/js/pid.js b/src/Hc900Crawler/wwwroot/js/pid.js index 5f62b21..821e16c 100644 --- a/src/Hc900Crawler/wwwroot/js/pid.js +++ b/src/Hc900Crawler/wwwroot/js/pid.js @@ -31,13 +31,13 @@ async function pidUpload() { const res = await fetch('/api/pid/upload', { method: 'POST', body: formData }); if (!res.ok) { - const err = await res.json().catch(() => ({ error: res.statusText })); - throw new Error(err.error || res.statusText); + const err = await res.json().catch(() => ({ Error: res.statusText })); + throw new Error(err.Error || res.statusText); } const data = await res.json(); - if (statusEl) statusEl.textContent = `✅ 전송 완료: ${data.fileName} (${(data.fileSize / 1024).toFixed(0)} KB)`; + if (statusEl) statusEl.textContent = `✅ 전송 완료: ${data.FileName} (${(data.FileSize / 1024).toFixed(0)} KB)`; - await pidLoadServerFiles(data.fileName); + await pidLoadServerFiles(data.FileName); } catch (e) { if (statusEl) statusEl.textContent = `❌ 전송 실패: ${e.message}`; } @@ -64,12 +64,12 @@ async function pidLoadServerFiles(selectFileName) { const data = await res.json(); - if (!data.files || data.files.length === 0) { + if (!data.Files || data.Files.length === 0) { sel.innerHTML = ''; return; } - sel.innerHTML = data.files.map(f => - `` + sel.innerHTML = data.Files.map(f => + `` ).join(''); if (selectFileName) sel.value = selectFileName; @@ -120,19 +120,19 @@ async function pidExtract() { }); if (!res.ok) { - const err = await res.json().catch(() => ({ error: res.statusText })); - throw new Error(err.error || res.statusText); + const err = await res.json().catch(() => ({ Error: res.statusText })); + throw new Error(err.Error || res.statusText); } const data = await res.json(); pidExtracting = false; - const skipMsg = data.skippedDuplicates > 0 ? ` (${data.skippedDuplicates}건 중복 제외)` : ''; - if (statusEl) statusEl.textContent = `✅ 추출 완료: ${data.totalCount}건${skipMsg}`; + const skipMsg = data.SkippedDuplicates > 0 ? ` (${data.SkippedDuplicates}건 중복 제외)` : ''; + if (statusEl) statusEl.textContent = `✅ 추출 완료: ${data.TotalCount}건${skipMsg}`; log('pid-log', [ - { c: 'ok', t: `✅ 추출 완료: ${data.totalCount}건${skipMsg}` }, - { c: 'inf', t: ` 신뢰도 70%+: ${data.confidenceItems}건` }, - { c: 'inf', t: ` 신뢰도 50%미만: ${data.lowConfidenceItems}건` }, - ...(data.skippedDuplicates > 0 ? [{ c: 'warn', t: ` 중복 스킵: ${data.skippedDuplicates}건` }] : []) + { c: 'ok', t: `✅ 추출 완료: ${data.TotalCount}건${skipMsg}` }, + { c: 'inf', t: ` 신뢰도 70%+: ${data.ConfidenceItems}건` }, + { c: 'inf', t: ` 신뢰도 50%미만: ${data.LowConfidenceItems}건` }, + ...(data.SkippedDuplicates > 0 ? [{ c: 'warn', t: ` 중복 스킵: ${data.SkippedDuplicates}건` }] : []) ]); pidCurrentPage = 1; @@ -189,7 +189,7 @@ async function pidLoadTable(page = 1) { const data = await res.json(); pidLastResult = data; - if (!data.items || data.items.length === 0) { + if (!data.Items || data.Items.length === 0) { tbody.innerHTML = '데이터가 없습니다'; document.getElementById('pid-pagination').innerHTML = ''; return; @@ -200,48 +200,48 @@ async function pidLoadTable(page = 1) { // 현재 카테고리 → 가상 키 변환 (instrument + tagDcs 조합) function pidVcat(item) { - if (item.category === 'instrument') return item.tagDcs ? 'instrument_dcs' : 'instrument_field'; - return item.category || ''; + if (item.Category === 'instrument') return item.TagDcs ? 'instrument_dcs' : 'instrument_field'; + return item.Category || ''; } - tbody.innerHTML = data.items.map(item => { + tbody.innerHTML = data.Items.map(item => { const vcat = pidVcat(item); const catOpts = CATEGORY_ORDER.map(k => `` ).join(''); return ` - ${item.id} - - - + ${item.Id} + + + - ${catOpts} - - - - - - + + + + + + - ${item.experionTagId - ? `` - : `` + ${item.ExperionTagId + ? `` + : `` } - + - + `; }).join(''); - pidRenderPagination(data.total, page); + pidRenderPagination(data.Total, page); } catch (e) { tbody.innerHTML = `오류: ${e.message}`; } @@ -331,8 +331,8 @@ async function pidCreateRow(btn) { body: JSON.stringify(body) }); if (!res.ok) { - const err = await res.json().catch(() => ({ error: res.statusText })); - throw new Error(err.error || res.statusText); + const err = await res.json().catch(() => ({ Error: res.statusText })); + throw new Error(err.Error || res.statusText); } row.remove(); await pidLoadTable(pidCurrentPage); @@ -371,7 +371,7 @@ async function pidDeleteRow(id) { const res = await fetch(`/api/pid/${id}`, { method: 'DELETE' }); if (!res.ok) { const err = await res.json().catch(() => ({})); - throw new Error(err.error || res.statusText); + throw new Error(err.Error || res.statusText); } await pidLoadTable(pidCurrentPage); pidUpdateStats(); @@ -381,11 +381,11 @@ async function pidDeleteRow(id) { } function pidUpdateStats() { - if (!pidLastResult || !pidLastResult.items) return; + if (!pidLastResult || !pidLastResult.Items) return; - const total = pidLastResult.total || 0; - const highConf = pidLastResult.items.filter(i => i.confidence >= 0.7).length; - const mapped = pidLastResult.items.filter(i => i.experionTagId).length; + const total = pidLastResult.Total || 0; + const highConf = pidLastResult.Items.filter(i => i.Confidence >= 0.7).length; + const mapped = pidLastResult.Items.filter(i => i.ExperionTagId).length; const elTotal = document.getElementById('pid-stat-total'); const elHigh = document.getElementById('pid-stat-high'); @@ -424,13 +424,13 @@ async function pidAnalyzeConnections() { body: JSON.stringify({ fileName }) }); if (!res.ok) { - const err = await res.json().catch(() => ({ error: res.statusText })); - throw new Error(err.error || res.statusText); + const err = await res.json().catch(() => ({ Error: res.statusText })); + throw new Error(err.Error || res.statusText); } const data = await res.json(); - if (statusEl) statusEl.textContent = `✅ 연결 분석 완료: ${data.connectionCount}건`; + if (statusEl) statusEl.textContent = `✅ 연결 분석 완료: ${data.ConnectionCount}건`; log('pid-log', [ - { c: 'ok', t: `✅ 연결 분석 완료: ${data.connectionCount}개 from→to 연결` } + { c: 'ok', t: `✅ 연결 분석 완료: ${data.ConnectionCount}개 from→to 연결` } ]); pidCurrentPage = 1; await pidLoadTable(); @@ -457,8 +457,8 @@ async function pidDeleteAll() { const res = await fetch('/api/pid/all', { method: 'DELETE' }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); - if (statusEl) statusEl.textContent = `🗑 ${data.message}`; - log('pid-log', [{ c: 'warn', t: `🗑 전체 삭제 완료: ${data.deletedCount}건` }]); + if (statusEl) statusEl.textContent = `🗑 ${data.Message}`; + log('pid-log', [{ c: 'warn', t: `🗑 전체 삭제 완료: ${data.DeletedCount}건` }]); pidCurrentPage = 1; await pidLoadTable(); pidUpdateStats(); @@ -490,7 +490,7 @@ const CATEGORY_META = { process_equipment:{ label: 'Process Equipment', badge: '', dbCat: 'process_equipment', tagDcs: false }, utility_equipment:{ label: 'Utility Equipment', badge: 'warn', dbCat: 'utility_equipment', tagDcs: false }, pipings: { label: 'Pipings', badge: 'ok', dbCat: 'pipings', tagDcs: false }, - // ── 장비 테이블 배지용 (equipment 목록의 r.category 값과 매핑) ── + // ── 장비 테이블 배지용 (equipment 목록의 r.Category 값과 매핑) ── instrument: { label: 'Instrument', badge: 'ok' }, }; const CATEGORY_ORDER = [ @@ -511,7 +511,7 @@ async function pidRefreshPrefixRules() { const res = await fetch('/api/pid/prefix-rules'); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); - const items = data.items || []; + const items = data.Items || []; if (items.length === 0) { container.innerHTML = '
규칙이 없습니다. 아래에서 추가하세요.
'; @@ -521,8 +521,8 @@ async function pidRefreshPrefixRules() { // instrument → tag_dcs 기준으로 가상 분할 const grouped = {}; for (const r of items) { - let key = r.category; - if (r.category === 'instrument') { + let key = r.Category; + if (r.Category === 'instrument') { key = r.tagDcs ? 'instrument_dcs' : 'instrument_field'; } if (!grouped[key]) grouped[key] = []; @@ -550,11 +550,11 @@ async function pidRefreshPrefixRules() { if (rules) { for (const r of rules) { html += `
- + - - + +
`; } @@ -594,8 +594,8 @@ async function pidAddPrefixRule(vcat) { body: JSON.stringify({ prefix, category, tagDcs, description: desc, sortOrder: order }) }); if (!res.ok) { - const err = await res.json().catch(() => ({ error: res.statusText })); - throw new Error(err.error || res.statusText); + const err = await res.json().catch(() => ({ Error: res.statusText })); + throw new Error(err.Error || res.statusText); } row.querySelector('.pid-cat-prefix-input').value = ''; row.querySelector('.pid-cat-desc-input').value = ''; @@ -626,8 +626,8 @@ async function pidUpdatePrefixRule(id, btn) { body: JSON.stringify({ prefix, category, tagDcs, description: desc, sortOrder: order }) }); if (!res.ok) { - const err = await res.json().catch(() => ({ error: res.statusText })); - throw new Error(err.error || res.statusText); + const err = await res.json().catch(() => ({ Error: res.statusText })); + throw new Error(err.Error || res.statusText); } await pidRefreshPrefixRules(); } catch (e) { @@ -652,7 +652,7 @@ async function pidApplyCategories() { const res = await fetch('/api/pid/apply-categories', { method: 'POST' }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); - alert(`${data.applied}건 category 적용 완료`); + alert(`${data.Applied}건 category 적용 완료`); await pidLoadTable(pidCurrentPage); pidUpdateStats(); } catch (e) { diff --git a/src/Hc900Crawler/wwwroot/js/setup.js b/src/Hc900Crawler/wwwroot/js/setup.js index e9633d9..a1de829 100644 --- a/src/Hc900Crawler/wwwroot/js/setup.js +++ b/src/Hc900Crawler/wwwroot/js/setup.js @@ -161,19 +161,19 @@ async function refreshAll() { async function startCtrl(id) { const r = await _setupApi('POST', '/gateway/start?id=' + encodeURIComponent(id)); - _setupMsg('msg-' + id, r.success ?? r.ok, r.message ?? (r.ok ? '시작됨' : '실패')); + _setupMsg('msg-' + id, r.Success ?? r.ok, r.Message ?? (r.ok ? '시작됨' : '실패')); setTimeout(refreshAll, 2500); } async function stopCtrl(id) { const r = await _setupApi('POST', '/gateway/stop?id=' + encodeURIComponent(id)); - _setupMsg('msg-' + id, r.success ?? r.ok, r.message ?? (r.ok ? '중지됨' : '실패')); + _setupMsg('msg-' + id, r.Success ?? r.ok, r.Message ?? (r.ok ? '중지됨' : '실패')); setTimeout(refreshAll, 1000); } async function restartCtrl(id) { const r = await _setupApi('POST', '/gateway/restart?id=' + encodeURIComponent(id)); - _setupMsg('msg-' + id, r.success ?? r.ok, r.message ?? (r.ok ? '재시작됨' : '실패')); + _setupMsg('msg-' + id, r.Success ?? r.ok, r.Message ?? (r.ok ? '재시작됨' : '실패')); setTimeout(refreshAll, 3000); } @@ -194,7 +194,7 @@ async function saveCtrl(id) { const enEl = document.getElementById('enabled-' + id); if (enEl) ctrl.enabled = enEl.checked; const r = await _setupApi('POST', '/config', cfg); - _setupMsg('msg-' + id, r.success, r.message); + _setupMsg('msg-' + id, r.Success, r.Message); } async function deleteCtrl(id) { @@ -204,8 +204,8 @@ async function deleteCtrl(id) { cfg.controllers = cfg.controllers.filter(c => (c.Id ?? c.id) !== id); if (cfg.controllers.length === before) return; const r = await _setupApi('POST', '/config', cfg); - _setupMsg('add-msg', r.success, r.message); - if (r.success) loadAll(); + _setupMsg('add-msg', r.Success, r.Message); + if (r.Success) loadAll(); } async function saveSharedConfig() { @@ -214,7 +214,7 @@ async function saveSharedConfig() { cfg.shared.ldLibraryPath = document.getElementById('cfg-ld').value.trim(); cfg.shared.logDir = document.getElementById('cfg-logdir').value.trim(); const r = await _setupApi('POST', '/config', cfg); - _setupMsg('shared-msg', r.success, r.message); + _setupMsg('shared-msg', r.Success, r.Message); } async function addController() { @@ -230,8 +230,8 @@ async function addController() { enabled: true, }); const r = await _setupApi('POST', '/config', cfg); - _setupMsg('add-msg', r.success, r.message); - if (r.success) { + _setupMsg('add-msg', r.Success, r.Message); + if (r.Success) { loadAll(); document.getElementById('new-id').value = ''; document.getElementById('new-name').value = ''; @@ -247,26 +247,26 @@ async function refreshLog() { const d = await _setupApi('GET', '/gateway/log?id=' + encodeURIComponent(id) + '&lines=80'); const lb = document.getElementById('log-box'); if (lb) { - lb.textContent = (d.log || []).join('\n') || '로그 없음'; + lb.textContent = (d.Log || []).join('\n') || '로그 없음'; lb.scrollTop = 9999; } } async function startAll() { const r = await _setupApi('POST', '/gateway/start'); - _setupMsg('global-msg', true, Array.isArray(r) ? r.map(x => `${x.Id ?? x.id}:${(x.ok ?? x.success) ? 'OK' : 'FAIL'}`).join(', ') : (r.success ? '전체 시작됨' : '실패')); + _setupMsg('global-msg', true, Array.isArray(r) ? r.map(x => `${x.Id ?? x.id}:${(x.Ok ?? x.success) ? 'OK' : 'FAIL'}`).join(', ') : (r.Success ? '전체 시작됨' : '실패')); setTimeout(refreshAll, 2500); } async function stopAll() { const r = await _setupApi('POST', '/gateway/stop'); - _setupMsg('global-msg', true, Array.isArray(r) ? r.map(x => `${x.Id ?? x.id}:${(x.ok ?? x.success) ? 'OK' : 'FAIL'}`).join(', ') : (r.success ? '전체 중지됨' : '실패')); + _setupMsg('global-msg', true, Array.isArray(r) ? r.map(x => `${x.Id ?? x.id}:${(x.Ok ?? x.success) ? 'OK' : 'FAIL'}`).join(', ') : (r.Success ? '전체 중지됨' : '실패')); setTimeout(refreshAll, 1000); } async function restartAll() { const r = await _setupApi('POST', '/gateway/restart'); - _setupMsg('global-msg', true, Array.isArray(r) ? r.map(x => `${x.Id ?? x.id}:${(x.ok ?? x.success) ? 'OK' : 'FAIL'}`).join(', ') : (r.success ? '전체 재시작됨' : '실패')); + _setupMsg('global-msg', true, Array.isArray(r) ? r.map(x => `${x.Id ?? x.id}:${(x.Ok ?? x.success) ? 'OK' : 'FAIL'}`).join(', ') : (r.Success ? '전체 재시작됨' : '실패')); setTimeout(refreshAll, 3000); } diff --git a/src/Hc900Crawler/wwwroot/js/steam.js b/src/Hc900Crawler/wwwroot/js/steam.js index 15143f7..e6bf478 100644 --- a/src/Hc900Crawler/wwwroot/js/steam.js +++ b/src/Hc900Crawler/wwwroot/js/steam.js @@ -62,8 +62,8 @@ async function stLoadColumns() { const d = await api('GET', '/api/steam/models'); const sel1 = document.getElementById('st-col'); const sel2 = document.getElementById('st-bt-col'); - const cols = d.configured || d.columns || []; - const defaultCol = d.defaultColumn || 'C-6111'; + const cols = d.Configured || d.Columns || []; + const defaultCol = d.DefaultColumn || 'C-6111'; [sel1, sel2].forEach(sel => { sel.innerHTML = cols.map(c => ``).join(''); }); @@ -100,8 +100,8 @@ async function stLoadProdOptions(col) { if (!sel) return; let data; try { data = await api('GET', `/api/steam/productlabels/${col}`); } catch (_) { return; } - const opts = (data.clusters || []).map(c => { - const v = c.name || c.cluster; // 매핑된 이름 우선, 없으면 클러스터 라벨(P0…) + const opts = (data.Clusters || []).map(c => { + const v = c.Name || c.Cluster; // 매핑된 이름 우선, 없으면 클러스터 라벨(P0…) return ``; }).join(''); const saved = stGetSavedProduct(col); @@ -185,18 +185,18 @@ async function stOpenProdModal() { // 제안 목록(datalist) const dl = document.getElementById('st-prod-suggest'); - dl.innerHTML = (data.suggestions || []).map(s => ``; }).join(''); sel.innerHTML = `` + opts; @@ -298,7 +298,7 @@ async function stTempHistLoad() { try { const h = await api('GET', url); console.log('[steam] hist response:', JSON.stringify(h).slice(0, 300)); - stTempSnapshots = h && h.snapshots || []; + stTempSnapshots = h && h.Snapshots || []; } catch (e) { console.warn('[steam] hist load fail:', e); stTempSnapshots = []; @@ -308,11 +308,11 @@ async function stTempHistLoad() { if (n >= 2) { slider.max = (n - 1).toString(); slider.value = (n - 1).toString(); - document.getElementById('st-scrub-from').textContent = stFmtDT(stTempSnapshots[0].ts); - document.getElementById('st-scrub-to').textContent = stFmtDT(stTempSnapshots[n - 1].ts); + document.getElementById('st-scrub-from').textContent = stFmtDT(stTempSnapshots[0].Ts); + document.getElementById('st-scrub-to').textContent = stFmtDT(stTempSnapshots[n - 1].Ts); } else { slider.max = '0'; slider.value = '0'; - document.getElementById('st-scrub-ts').textContent = n === 0 ? '히스토리 없음' : stFmtDT(stTempSnapshots[0].ts); + document.getElementById('st-scrub-ts').textContent = n === 0 ? '히스토리 없음' : stFmtDT(stTempSnapshots[0].Ts); document.getElementById('st-scrub-from').textContent = '—'; document.getElementById('st-scrub-to').textContent = '—'; } @@ -332,7 +332,7 @@ function stTempScrub() { return; } stTempIsLive = false; - document.getElementById('st-scrub-ts').textContent = stFmtDT(snap.ts); + document.getElementById('st-scrub-ts').textContent = stFmtDT(snap.Ts); stRenderTemp(snap); } @@ -343,9 +343,9 @@ function stTempUpdateBadges(src) { // 제품명은 드롭다운이 표시 — 별도 배지 없음 (중복 제거) const vb = document.getElementById('st-temp-vac'); - vb.textContent = `진공 ${stFmt(s.vacuum.current)} (기준 ${stFmt(s.vacuum.refMedian)})` + (s.vacuum.deviated ? ' ⚠이격' : ''); - vb.style.background = s.vacuum.deviated ? '#3a1a1a' : '#1a1a3a'; - vb.style.color = s.vacuum.deviated ? '#f66' : '#66f'; + vb.textContent = `진공 ${stFmt(s.Vacuum.Current)} (기준 ${stFmt(s.Vacuum.RefMedian)})` + (s.Vacuum.Deviated ? ' ⚠이격' : ''); + vb.style.background = s.Vacuum.Deviated ? '#3a1a1a' : '#1a1a3a'; + vb.style.color = s.Vacuum.Deviated ? '#f66' : '#66f'; } let stRenderTempCol = ''; @@ -363,13 +363,13 @@ function stRenderTemp(snap) { stTempUpdateBadges(snapSrc); stRenderTempCol = document.getElementById('st-temp-col').value; - const stages = stTempLive.stages; + const stages = stTempLive.Stages; if (!stages || !stages.length) return; - const cats = stages.map(s => ST_STAGE_LABEL[s.stage] || s.stage); - const lo = stages.map(s => (s.refMedian != null && s.refStd != null) ? +(s.refMedian - 2 * s.refStd).toFixed(2) : null); - const band = stages.map(s => s.refStd != null ? +(4 * s.refStd).toFixed(2) : null); - const med = stages.map(s => s.refMedian); + const cats = stages.map(s => ST_STAGE_LABEL[s.Stage] || s.Stage); + const lo = stages.map(s => (s.RefMedian != null && s.RefStd != null) ? +(s.RefMedian - 2 * s.RefStd).toFixed(2) : null); + const band = stages.map(s => s.RefStd != null ? +(4 * s.RefStd).toFixed(2) : null); + const med = stages.map(s => s.RefMedian); const col = stRenderTempCol; const el = document.getElementById('st-chart-temp'); @@ -385,13 +385,13 @@ function stRenderTemp(snap) { ]; if (snap) { - const curD = stTempLive.stages.map(s => ({ value: s.current, itemStyle: { color: s.deviated ? '#f66' : '#6cf' } })); - const snapD = snapSrc.stages.map(s => ({ value: s.current, itemStyle: { color: s.deviated ? '#f66' : '#fa3' } })); + const curD = stTempLive.Stages.map(s => ({ value: s.Current, itemStyle: { color: s.Deviated ? '#f66' : '#6cf' } })); + const snapD = snapSrc.Stages.map(s => ({ value: s.Current, itemStyle: { color: s.Deviated ? '#f66' : '#fa3' } })); series.push({ name: '실시간참조', type: 'line', data: curD, lineStyle: { color: '#6cf', width: 1, type: 'dashed' }, symbol: 'none', silent: true }); series.push({ name: '선택시점', type: 'line', data: snapD, lineStyle: { color: '#fa3', width: 2.5 }, symbol: 'circle', symbolSize: 9 }); leg.push('실시간참조', '선택시점'); } else { - const curD = stTempLive.stages.map(s => ({ value: s.current, itemStyle: { color: s.deviated ? '#f66' : '#6cf' } })); + const curD = stTempLive.Stages.map(s => ({ value: s.Current, itemStyle: { color: s.Deviated ? '#f66' : '#6cf' } })); series.push({ name: '실시간', type: 'line', data: curD, lineStyle: { color: '#6cf', width: 2.5 }, symbol: 'circle', symbolSize: 9 }); leg.push('실시간'); } @@ -399,12 +399,12 @@ function stRenderTemp(snap) { chart.setOption({ backgroundColor: 'transparent', title: { - text: snap ? `${col} · ${stFmtDT(snap.ts)}` : `${col} 단면 온도 프로파일`, + text: snap ? `${col} · ${stFmtDT(snap.Ts)}` : `${col} 단면 온도 프로파일`, subtext: snap - ? `기준제품 ${snap.matchedProduct || '미선택'} · 진공 ${stFmt(snap.vacuum?.current)} (기준 ${stFmt(snap.vacuum?.refMedian)})${snap.vacuum?.deviated ? ' ⚠' : ''}` - : `기준제품 ${stTempLive.matchedProduct || '미선택'} · 진공 ${stFmt(stTempLive.vacuum.current)} (기준 ${stFmt(stTempLive.vacuum.refMedian)})${stTempLive.vacuum.deviated ? ' ⚠' : ''}`, + ? `기준제품 ${snap.MatchedProduct || '미선택'} · 진공 ${stFmt(snap.Vacuum?.Current)} (기준 ${stFmt(snap.Vacuum?.RefMedian)})${snap.Vacuum?.Deviated ? ' ⚠' : ''}` + : `기준제품 ${stTempLive.MatchedProduct || '미선택'} · 진공 ${stFmt(stTempLive.Vacuum.Current)} (기준 ${stFmt(stTempLive.Vacuum.RefMedian)})${stTempLive.Vacuum.Deviated ? ' ⚠' : ''}`, textStyle: { color: '#ccc', fontSize: 13 }, - subtextStyle: { color: snap ? (snap.vacuum?.deviated ? '#f66' : '#888') : (stTempLive.vacuum.deviated ? '#f66' : '#888'), fontSize: 11 } }, + subtextStyle: { color: snap ? (snap.Vacuum?.Deviated ? '#f66' : '#888') : (stTempLive.Vacuum.Deviated ? '#f66' : '#888'), fontSize: 11 } }, tooltip: { trigger: 'axis' }, legend: { data: leg, textStyle: { color: '#888' }, top: 6, right: 10 }, grid: { top: 64, left: 48, right: 20, bottom: 28 }, @@ -420,41 +420,41 @@ function stRenderTemp(snap) { // 왼쪽 표: 온도 // 표는 D(탑)→C→B→A(보텀) 순서로 표시 (차트는 A→B→C→D 유지) - let stgRows = snapSrc.stages.map(s => { - const label = ST_STAGE_LABEL[s.stage] || s.stage; - const cur = stFmt(s.current) + '℃'; - const ref = s.refMedian != null ? `${stFmt(s.refMedian)}±${stFmt(s.refStd)}` : '—'; + let stgRows = snapSrc.Stages.map(s => { + const label = ST_STAGE_LABEL[s.Stage] || s.Stage; + const cur = stFmt(s.Current) + '℃'; + const ref = s.RefMedian != null ? `${stFmt(s.RefMedian)}±${stFmt(s.RefStd)}` : '—'; return `${label}${cur}${ref}`; }).reverse(); - if (snapSrc.spanAD != null) { - stgRows.push(`ΔT(A-D)${stFmt(snapSrc.spanAD)}℃—`); + if (snapSrc.SpanAD != null) { + stgRows.push(`ΔT(A-D)${stFmt(snapSrc.SpanAD)}℃—`); } - if (snapSrc.vacuum) { - const v = snapSrc.vacuum; - const cur = stFmt(v.current); - const ref = v.refMedian != null ? `${stFmt(v.refMedian)}±${stFmt(v.refStd)}` : '—'; + if (snapSrc.Vacuum) { + const v = snapSrc.Vacuum; + const cur = stFmt(v.Current); + const ref = v.RefMedian != null ? `${stFmt(v.RefMedian)}±${stFmt(v.RefStd)}` : '—'; stgRows.push(`진공${cur}${ref}`); } // 오른쪽 표: 유량 let flowHtml = ''; - if (snapSrc.flow) { - const f = snapSrc.flow; - const ft = snapSrc.flowTags || {}; + if (snapSrc.Flow) { + const f = snapSrc.Flow; + const ft = snapSrc.FlowTags || {}; const flowHeaders = [ '', - `FEED
${escHtml((ft.feed || 'FICQ-??01.PV').replace('.PV',''))}`, - `REFLUX
${escHtml((ft.reflux || 'FICQ-??13.PV').replace('.PV',''))}`, - `제품추출
${escHtml((ft.product || 'FICQ-??18.PV').replace('.PV',''))}`, - `경비물
${escHtml((ft.overhead || 'FICQ-??14.PV').replace('.PV',''))}`, - `중비물
${escHtml((ft.bottom || 'FICQ-??16.PV').replace('.PV',''))}`, - `스팀
${escHtml((ft.steam || 'FIQ-???15.PV').replace('.PV',''))}`, + `FEED
${escHtml((ft.Feed || 'FICQ-??01.PV').replace('.PV',''))}`, + `REFLUX
${escHtml((ft.Reflux || 'FICQ-??13.PV').replace('.PV',''))}`, + `제품추출
${escHtml((ft.Product || 'FICQ-??18.PV').replace('.PV',''))}`, + `경비물
${escHtml((ft.Overhead || 'FICQ-??14.PV').replace('.PV',''))}`, + `중비물
${escHtml((ft.Bottom || 'FICQ-??16.PV').replace('.PV',''))}`, + `스팀
${escHtml((ft.Steam || 'FIQ-???15.PV').replace('.PV',''))}`, ].join(''); - const stPv = stFmt(f.steam?.pv); + const stPv = stFmt(f.Steam?.Pv); const flowRows = [ - `PV${stFmt(f.feed?.pv)}${stFmt(f.reflux?.pv)}${stFmt(f.product?.pv)}${stFmt(f.overhead?.pv)}${stFmt(f.bottom?.pv)}${stPv}`, - `SP${stFmt(f.feed?.sp)}${stFmt(f.reflux?.sp)}${stFmt(f.product?.sp)}${stFmt(f.overhead?.sp)}${stFmt(f.bottom?.sp)}—`, - `OP${stFmt(f.feed?.op)}${stFmt(f.reflux?.op)}${stFmt(f.product?.op)}${stFmt(f.overhead?.op)}${stFmt(f.bottom?.op)}—`, + `PV${stFmt(f.Feed?.Pv)}${stFmt(f.Reflux?.Pv)}${stFmt(f.Product?.Pv)}${stFmt(f.Overhead?.Pv)}${stFmt(f.Bottom?.Pv)}${stPv}`, + `SP${stFmt(f.Feed?.Sp)}${stFmt(f.Reflux?.Sp)}${stFmt(f.Product?.Sp)}${stFmt(f.Overhead?.Sp)}${stFmt(f.Bottom?.Sp)}—`, + `OP${stFmt(f.Feed?.Op)}${stFmt(f.Reflux?.Op)}${stFmt(f.Product?.Op)}${stFmt(f.Overhead?.Op)}${stFmt(f.Bottom?.Op)}—`, ].join(''); flowHtml = `${flowHeaders}${flowRows}
`; } @@ -481,8 +481,8 @@ async function stLiveTick() { const col = document.getElementById('st-col').value; try { const d = await api('GET', `/api/steam/live?col=${col}`); - if (d.status === 'missing_tags') { - document.getElementById('st-live-msg').textContent = `⚠ 태그 없음: ${d.missing?.join(', ') || '—'} (${d.message || '게이트웨이 폴링 확인'})`; + if (d.Status === 'missing_tags') { + document.getElementById('st-live-msg').textContent = `⚠ 태그 없음: ${d.Missing?.join(', ') || '—'} (${d.Message || '게이트웨이 폴링 확인'})`; return; } stUpdateLive(d); @@ -492,35 +492,35 @@ async function stLiveTick() { } function stUpdateLive(d) { - document.getElementById('st-mode').textContent = d.mode || '—'; - document.getElementById('st-conf').textContent = d.confidence || '—'; + document.getElementById('st-mode').textContent = d.Mode || '—'; + document.getElementById('st-conf').textContent = d.Confidence || '—'; const oodEl = document.getElementById('st-ood'); - if (d.ood) { oodEl.style.display = 'inline-block'; } else { oodEl.style.display = 'none'; } + if (d.Ood) { oodEl.style.display = 'inline-block'; } else { oodEl.style.display = 'none'; } - document.getElementById('st-op-rec').textContent = d.recOp != null ? d.recOp.toFixed(1) : '—'; - document.getElementById('st-op-act').textContent = d.actualOp != null ? d.actualOp.toFixed(1) : '—'; + document.getElementById('st-op-rec').textContent = d.RecOp != null ? d.RecOp.toFixed(1) : '—'; + document.getElementById('st-op-act').textContent = d.ActualOp != null ? d.ActualOp.toFixed(1) : '—'; const opTag = document.getElementById('st-op-act-tag'); - if (opTag) opTag.textContent = (d.Tags?.steamOp || '') + (d.Descs?.steamOp ? ' · ' + d.Descs.steamOp : ''); + if (opTag) opTag.textContent = (d.Tags?.SteamOp || '') + (d.Descs?.SteamOp ? ' · ' + d.Descs.SteamOp : ''); // 게이지 const fill = document.getElementById('st-gauge-fill'); - if (d.recOp != null) fill.style.width = Math.min(100, Math.max(0, d.recOp)) + '%'; + if (d.RecOp != null) fill.style.width = Math.min(100, Math.max(0, d.RecOp)) + '%'; // 입력값 + 태그명 + 설명 - document.getElementById('st-live-feed').textContent = d.feed != null ? d.feed.toFixed(1) : '—'; - document.getElementById('st-live-feed-tag').textContent = (d.Tags?.feed || '') + (d.Descs?.feed ? ' · ' + d.Descs.feed : ''); - document.getElementById('st-live-product').textContent = d.product != null ? d.product.toFixed(1) : '—'; - document.getElementById('st-live-product-tag').textContent = (d.Tags?.product || '') + (d.Descs?.product ? ' · ' + d.Descs.product : ''); - document.getElementById('st-live-tc').textContent = d.tC != null ? d.tC.toFixed(1) : '—'; - document.getElementById('st-live-tc-tag').textContent = (d.Tags?.tC || '') + (d.Descs?.tC ? ' · ' + d.Descs.tC : ''); - document.getElementById('st-live-sf').textContent = d.actualSteamFlow != null ? d.actualSteamFlow.toFixed(1) : '—'; - document.getElementById('st-live-sf-tag').textContent = (d.Tags?.steamFlow || '') + (d.Descs?.steamFlow ? ' · ' + d.Descs.steamFlow : ''); + document.getElementById('st-live-feed').textContent = d.Feed != null ? d.Feed.toFixed(1) : '—'; + document.getElementById('st-live-feed-tag').textContent = (d.Tags?.Feed || '') + (d.Descs?.Feed ? ' · ' + d.Descs.Feed : ''); + document.getElementById('st-live-product').textContent = d.Product != null ? d.Product.toFixed(1) : '—'; + document.getElementById('st-live-product-tag').textContent = (d.Tags?.Product || '') + (d.Descs?.Product ? ' · ' + d.Descs.Product : ''); + document.getElementById('st-live-tc').textContent = d.TC != null ? d.TC.toFixed(1) : '—'; + document.getElementById('st-live-tc-tag').textContent = (d.Tags?.TC || '') + (d.Descs?.TC ? ' · ' + d.Descs.TC : ''); + document.getElementById('st-live-sf').textContent = d.ActualSteamFlow != null ? d.ActualSteamFlow.toFixed(1) : '—'; + document.getElementById('st-live-sf-tag').textContent = (d.Tags?.SteamFlow || '') + (d.Descs?.SteamFlow ? ' · ' + d.Descs.SteamFlow : ''); - document.getElementById('st-live-msg').textContent = d.message || ''; + document.getElementById('st-live-msg').textContent = d.Message || ''; // uPlot data const now = Date.now() / 1000; - stLiveData.push({ ts: now, recOp: d.recOp, actOp: d.actualOp }); + stLiveData.push({ ts: now, recOp: d.RecOp, actOp: d.ActualOp }); if (stLiveData.length > ST_MAX_POINTS) stLiveData.splice(0, stLiveData.length - ST_MAX_POINTS); stUpdateUplot(); } diff --git a/src/Hc900Crawler/wwwroot/js/trend.js b/src/Hc900Crawler/wwwroot/js/trend.js index 1302d67..5358b51 100644 --- a/src/Hc900Crawler/wwwroot/js/trend.js +++ b/src/Hc900Crawler/wwwroot/js/trend.js @@ -170,7 +170,7 @@ async function trLoadLimits() { try { const d = await api('GET', `/api/trend/limits?tags=${encodeURIComponent(tags.join(','))}`); trState.cache.limits = {}; - for (const l of (d.items || [])) trState.cache.limits[l.tag] = l; + for (const l of (d.Items || [])) trState.cache.limits[l.Tag] = l; } catch (e) { console.error('trLoadLimits:', e); trState.cache.limits = {}; } } @@ -183,8 +183,8 @@ async function trLoadRunbands() { p.set('from', new Date(from).toISOString()); p.set('to', new Date(to).toISOString()); const d = await api('GET', `/api/trend/runbands?${p}`); - trState.cache.runbands = (d.items || []).map(b => ({ - tag: b.tag, t0: new Date(b.t0).getTime(), t1: new Date(b.t1).getTime(), state: b.state + trState.cache.runbands = (d.Items || []).map(b => ({ + Tag: b.Tag, T0: new Date(b.T0).getTime(), T1: new Date(b.T1).getTime(), State: b.State })); } catch (e) { console.error('trLoadRunbands:', e); trState.cache.runbands = []; } } @@ -337,7 +337,7 @@ async function trQuery() { if (from) body.from = new Date(from).toISOString(); if (to) body.to = new Date(to).toISOString(); const d = await api('POST', '/api/history/query', body); - const raw = d.rows || d.Rows || []; + const raw = d.Rows || []; const rows = raw.map(r => ({ recordedAt: r.recordedAt ?? r.RecordedAt, values: r.values ?? r.Values @@ -355,7 +355,7 @@ async function trQuery() { to: to ? new Date(to).toISOString() : null, interval, limit: 5000 }); - rows = (d && d.success !== false) ? (d.rows || []) : []; + rows = (d && d.Success !== false) ? d.Rows : []; tk = rows[0]?.timeBucket != null ? 'timeBucket' : 'RecordedAt'; // 집계가 0행이면(엔드포인트 이슈 등) 원시로 폴백 → 차트 공백 방지 if (!rows.length) { const r = await rawQuery(); rows = r.rows; tk = r.tk; } @@ -650,11 +650,11 @@ async function trLiveTick() { try { const d = await api('GET', `/api/trend/live?tags=${encodeURIComponent(tags.join(','))}`); let changed = false; - for (const pt of (d.items || [])) { - if (pt.value == null) continue; - trState.liveNow[pt.tag] = +pt.value; // 현재값은 매 틱 갱신(타임스탬프 동일해도) - const arr = trState.seriesData[pt.tag]; if (!arr) continue; - const ms = new Date(pt.ts).getTime(); + for (const pt of (d.Items || [])) { + if (pt.Value == null) continue; + trState.liveNow[pt.Tag] = +pt.Value; + const arr = trState.seriesData[pt.Tag]; if (!arr) continue; + const ms = new Date(pt.Ts).getTime(); if (!isFinite(ms)) continue; if (!arr.length || arr[arr.length - 1][0] !== ms) { arr.push([ms, +pt.value]); changed = true; } } @@ -671,9 +671,9 @@ function trAnalyze() { document.getElementById('tr-ins-body').textContent = 'P3 async function trEnsureAnalogMap() { if (trAnalogMap) return trAnalogMap; const d = await api('GET', '/api/trend/analog-points'); - trAnalogAll = d.items || []; + trAnalogAll = d.Items || []; trAnalogMap = {}; - for (const p of trAnalogAll) trAnalogMap[p.tagName] = { desc: p.description, unit: p.unit, euLo: p.euLo, euHi: p.euHi }; + for (const p of trAnalogAll) trAnalogMap[p.TagName] = { Desc: p.Desc, Unit: p.Unit, EuLo: p.EuLo, EuHi: p.EuHi }; return trAnalogMap; } @@ -740,7 +740,7 @@ async function trLoadGroupList() { const sel = document.getElementById('tr-group-select'); const cur = sel.value; sel.innerHTML = '' + - (d.items || []).map(g => ``).join(''); + (d.Items || []).map(g => ``).join(''); if (cur) sel.value = cur; } catch (e) { console.error('trLoadGroupList:', e); } } diff --git a/src/Hc900Crawler/wwwroot/js/write.js b/src/Hc900Crawler/wwwroot/js/write.js index 1818243..c47ad44 100644 --- a/src/Hc900Crawler/wwwroot/js/write.js +++ b/src/Hc900Crawler/wwwroot/js/write.js @@ -30,7 +30,7 @@ async function wrWriteTag() { try { const d = await api('POST', '/api/gateway/write', { controllerId, tagName, value }); log('wr-log', [ - { c: d.success ? 'ok' : 'err', t: (d.success ? '✅ ' : '❌ ') + 'Write ' + esc(tagName) + ' = ' + value + (d.error ? ' → ' + esc(d.error) : '') }, + { c: d.Success ? 'ok' : 'err', t: (d.Success ? '✅ ' : '❌ ') + 'Write ' + esc(tagName) + ' = ' + value + (d.Error ? ' → ' + esc(d.Error) : '') }, ]); } catch (e) { log('wr-log', [{ c: 'err', t: '❌ ' + e.message }]); @@ -53,7 +53,7 @@ async function wrSetMode() { try { const d = await api('POST', '/api/gateway/write', { controllerId, tagName: tagName + '.' + modeTag, value: writeValue }); log('wr-log', [ - { c: d.success ? 'ok' : 'err', t: (d.success ? '✅ ' : '❌ ') + 'Mode ' + esc(tagName) + ' → ' + mode + ' (' + modeTag + ')' + (d.error ? ' → ' + esc(d.error) : '') }, + { c: d.Success ? 'ok' : 'err', t: (d.Success ? '✅ ' : '❌ ') + 'Mode ' + esc(tagName) + ' → ' + mode + ' (' + modeTag + ')' + (d.Error ? ' → ' + esc(d.Error) : '') }, ]); } catch (e) { log('wr-log', [{ c: 'err', t: '❌ ' + e.message }]); @@ -71,7 +71,7 @@ async function wrControlOp() { try { const d = await api('POST', '/api/gateway/write', { controllerId, tagName, value: opValue }); log('wr-log', [ - { c: d.success ? 'ok' : 'err', t: (d.success ? '✅ ' : '❌ ') + '통합 제어 ' + esc(tagName) + ' OP=' + opValue + (d.error ? ' — ' + esc(d.error) : '') }, + { c: d.Success ? 'ok' : 'err', t: (d.Success ? '✅ ' : '❌ ') + '통합 제어 ' + esc(tagName) + ' OP=' + opValue + (d.Error ? ' — ' + esc(d.Error) : '') }, ]); } catch (e) { log('wr-log', [{ c: 'err', t: '❌ ' + e.message }]); diff --git a/src/Infrastructure/Mcp/McpService.cs b/src/Infrastructure/Mcp/McpService.cs index 46774cd..8ca216d 100644 --- a/src/Infrastructure/Mcp/McpService.cs +++ b/src/Infrastructure/Mcp/McpService.cs @@ -21,7 +21,7 @@ public class McpService : IMcpService public async Task> ListToolsAsync() { var tools = await _client.ListToolsAsync(); - return tools.Select(t => new McpToolDto { Name = t.Name, Description = t.Description }).ToList(); + return tools.Select(t => new McpToolDto { Name = t.Name, Description = t.Description, InputSchema = t.InputSchema }).ToList(); } public async Task RunSqlAsync(string sql)