Files
ExperionCrawler/src/Web/Controllers/TextToSqlController.cs
2026-04-30 08:16:21 +09:00

376 lines
12 KiB
C#

using ExperionCrawler.Core.Application.DTOs;
using ExperionCrawler.Core.Application.Interfaces;
using ExperionCrawler.Infrastructure.Mcp;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace ExperionCrawler.Web.Controllers;
/// <summary>
/// Text-to-SQL API 컨트롤러
/// 자연어 질의를 파싱하고 시계열 데이터를 조회합니다.
/// MCP (Model Context Protocol) 통합을 위한 엔드포인트를 제공합니다.
/// </summary>
[ApiController]
[Route("api/text-to-sql")]
public class TextToSqlController : ControllerBase
{
private readonly ITextToSqlService _textToSqlService;
private readonly IExperionDbService _dbService;
private readonly IMcpService _mcpService;
private readonly ILogger<TextToSqlController> _logger;
public TextToSqlController(
ITextToSqlService textToSqlService,
IExperionDbService dbService,
IMcpService mcpService,
ILogger<TextToSqlController> logger)
{
_textToSqlService = textToSqlService;
_dbService = dbService;
_mcpService = mcpService;
_logger = logger;
}
/// <summary>
/// 자연어 질의를 SQL로 변환
/// </summary>
[HttpPost("parse")]
public async Task<IActionResult> Parse([FromBody] NaturalLanguageQueryDto dto)
{
try
{
var sql = await _textToSqlService.ParseNaturalLanguageAsync(dto.Query);
return Ok(new { success = true, sql });
}
catch (Exception ex)
{
return Ok(new { success = false, error = ex.Message });
}
}
/// <summary>
/// MCP query_with_nl 도구 호출 - 자연어 → LLM SQL 생성 → 실행
/// </summary>
[HttpPost("query-nl")]
public async Task<IActionResult> QueryWithNl([FromBody] NaturalLanguageQueryDto dto)
{
if (string.IsNullOrWhiteSpace(dto.Query))
return BadRequest(new { success = false, error = "질문이 비어있음" });
try
{
var result = await _mcpService.QueryWithNlAsync(dto.Query);
if (!result.Success)
return Ok(new { success = false, error = result.Error });
try
{
var jsonData = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>?>(result.Data!);
_logger.LogInformation("[TextToSql] query-nl 응답: success={Success}, data={Data}", jsonData?.GetType(), jsonData);
// data 필드가 JSON 문자열일 수 있으므로 다시 디시리얼라이즈
if (jsonData != null && jsonData.TryGetValue("data", out var dataObj) && dataObj is string dataString)
{
_logger.LogInformation("[TextToSql] data 필드가 문자열임: {DataString}", dataString);
var parsedData = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>?>(dataString);
jsonData["data"] = parsedData;
}
return Ok(new { success = true, data = jsonData });
}
catch (Exception ex)
{
_logger.LogError(ex, "[TextToSql] query-nl JSON 디시리얼라이즈 실패: {Data}", result.Data);
return Ok(new { success = true, data = result.Data });
}
}
catch (Exception ex)
{
_logger.LogError(ex, "[TextToSql] query-nl 실패");
return Ok(new { success = false, error = ex.Message });
}
}
/// <summary>
/// MCP 도구 목록 조회
/// </summary>
[HttpGet("tools")]
public async Task<IActionResult> ListTools()
{
try
{
var tools = await _mcpService.ListToolsAsync();
return Ok(new { success = true, tools });
}
catch (Exception ex)
{
_logger.LogError(ex, "[TextToSql] 도구 목록 조회 실패");
return Ok(new { success = false, error = ex.Message });
}
}
/// <summary>
/// MCP run_sql 도구 호출 - SQL 실행
/// Text-to-SQL 엔진으로 생성된 SQL을 안전하게 실행
/// </summary>
[HttpPost("execute-mcp")]
public async Task<IActionResult> ExecuteFromMcp([FromBody] SqlQueryDto dto)
{
if (string.IsNullOrWhiteSpace(dto.Sql))
{
return BadRequest(new { success = false, error = "SQL이 비어있음" });
}
try
{
// MCP run_sql 도구 호출
var result = await _mcpService.RunSqlAsync(dto.Sql);
if (!result.Success)
{
return Ok(new
{
success = false,
error = result.Error
});
}
// JSON 결과 반환 (쿼리 결과)
try
{
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data);
return Ok(new { success = true, data = jsonData });
}
catch
{
return Ok(new { success = true, data = result.Data });
}
}
catch (Exception ex)
{
_logger.LogError(ex, "[TextToSql] MCP 실행 실패");
return Ok(new { success = false, error = ex.Message });
}
}
/// <summary>
/// MCP query_pv_history 도구 호출 - 과거 값 히스토리 조회
/// </summary>
[HttpPost("query-history")]
public async Task<IActionResult> QueryHistory([FromBody] HistoryQueryRequestDto dto)
{
try
{
var tagNames = dto.TagNames ?? [];
var timeFrom = dto.From ?? DateTime.UtcNow.AddDays(-1).ToString("o");
var timeTo = dto.To ?? DateTime.UtcNow.ToString("o");
var limit = dto.Limit ?? 100;
var result = await _mcpService.QueryPvHistoryAsync(
tagNames,
timeFrom,
timeTo,
limit
);
if (!result.Success)
{
return Ok(new
{
success = false,
error = result.Error
});
}
try
{
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data);
return Ok(new
{
success = true,
data = jsonData
});
}
catch
{
return Ok(new
{
success = true,
data = result.Data
});
}
}
catch (Exception ex)
{
_logger.LogError(ex, "[TextToSql] History 쿼리 실패");
return Ok(new { success = false, error = ex.Message });
}
}
/// <summary>
/// MCP get_tag_metadata 도구 호출 - 태그 메타데이터 검색
/// </summary>
[HttpGet("tags/search")]
public async Task<IActionResult> SearchTags([FromQuery] string query, [FromQuery] int? limit)
{
try
{
var tagLimit = limit ?? 10;
var result = await _mcpService.GetTagMetadataAsync(query, tagLimit);
if (!result.Success)
{
return Ok(new
{
success = false,
error = result.Error
});
}
try
{
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data);
return Ok(new
{
success = true,
data = jsonData
});
}
catch
{
return Ok(new
{
success = true,
data = result.Data
});
}
}
catch (Exception ex)
{
_logger.LogError(ex, "[TextToSql] 태그 검색 실패");
return Ok(new { success = false, error = ex.Message });
}
}
/// <summary>
/// MCP list_drawings 도구 호출 - 도면 목록 조회
/// </summary>
[HttpGet("drawings")]
public async Task<IActionResult> ListDrawings([FromQuery] string? unitNo)
{
try
{
var result = await _mcpService.ListDrawingsAsync(unitNo);
if (!result.Success)
{
return Ok(new
{
success = false,
error = result.Error
});
}
try
{
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data);
return Ok(new
{
success = true,
data = jsonData
});
}
catch
{
return Ok(new
{
success = true,
data = result.Data
});
}
}
catch (Exception ex)
{
_logger.LogError(ex, "[TextToSql] 도면 목록 조회 실패");
return Ok(new { success = false, error = ex.Message });
}
}
/// <summary>
/// 쿼리 제안 (자동 완성)
/// </summary>
[HttpGet("suggest")]
public async Task<IActionResult> Suggest([FromQuery] string input = "")
{
var suggestions = await _textToSqlService.SuggestQueriesAsync(input);
return Ok(new { success = true, suggestions });
}
/// <summary>
/// 시계열 분석 (평균, 최대, 최소, 추세)
/// </summary>
[HttpPost("analyze")]
public async Task<IActionResult> Analyze([FromBody] AnalyzeRequestDto dto)
{
var result = await _textToSqlService.AnalyzeAsync(dto);
return Ok(new {
success = result.Success,
error = result.Error,
tags = result.Tags?.Select(t => new {
tagName = t.TagName,
avg = t.Avg,
mean = t.Mean,
min = t.Min,
max = t.Max,
first = t.First,
last = t.Last,
pointCount = t.PointCount,
stddev = t.StdDev,
from = t.From,
to = t.To
}).ToList()
});
}
/// <summary>
/// 사용자 지정 간격으로 history 이력 조회
/// history_table의 기본 저장 간격(60초)을 기반으로 사용자가 요청한 간격으로 데이터 집계
/// </summary>
[HttpPost("query-history-interval")]
public async Task<IActionResult> QueryHistoryInterval([FromBody] HistoryIntervalQueryRequestDto dto)
{
try
{
var request = new HistoryIntervalQueryRequest(
dto.TagNames,
dto.From,
dto.To,
dto.Interval,
dto.Limit);
var result = await _dbService.QueryHistoryWithIntervalAsync(request);
var response = new
{
success = true,
tagNames = result.TagNames.ToList(),
rows = result.Rows.Select(r => new
{
timeBucket = r.TimeBucket,
values = r.Values
}).ToList(),
baseIntervalSeconds = result.BaseIntervalSeconds,
queryInterval = result.QueryInterval
};
return Ok(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "[TextToSql] QueryHistoryInterval 실패");
return StatusCode(StatusCodes.Status500InternalServerError, new { success = false, error = ex.Message });
}
}
}