fix: T2S vLLM URL, first()/last() compatibility, PascalCase unification, event date parsing
- Fix vLLM base URL from port 8001 to 8000 (server.py) - Replace TimeScaleDB first()/last() with array_agg(ORDER BY) for standard PostgreSQL compatibility (TextToSqlService.cs) - Unify all backend response fields to PascalCase (TextToSqlController.cs) - Update frontend to match PascalCase response fields (t2s.js) - Change T2S date inputs to calendar popup (same as hist.html) (t2s.html) - Fix evt.js date parsing: empty string causes Invalid Date error (evt.js)
This commit is contained in:
@@ -42,7 +42,7 @@ def _kst_str(dt_iso: str | None) -> str:
|
||||
QDRANT_URL = os.environ.get("QDRANT_URL", "http://localhost:6333")
|
||||
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
||||
EMBED_MODEL = os.environ.get("EMBED_MODEL", "nomic-embed-text")
|
||||
VLLM_BASE_URL = os.environ.get("VLLM_BASE_URL", "http://localhost:8001/v1")
|
||||
VLLM_BASE_URL = os.environ.get("VLLM_BASE_URL", "http://localhost:8000/v1")
|
||||
from config import get_vllm_model
|
||||
VLLM_MODEL = get_vllm_model()
|
||||
|
||||
|
||||
@@ -203,11 +203,13 @@ public class TextToSqlService : ITextToSqlService
|
||||
}
|
||||
else
|
||||
{
|
||||
// first/last 쿼리 (TimeScaleDB 함수)
|
||||
var func = aggregate == "first" ? "first" : "last";
|
||||
// first/last 쿼리 — array_agg로 교체 (TimeScaleDB first()/last() 비호환)
|
||||
var func = aggregate == "first"
|
||||
? "(array_agg(value::double precision ORDER BY recorded_at))[1]"
|
||||
: "(array_agg(value::double precision ORDER BY recorded_at DESC))[1]";
|
||||
return (new List<string> {
|
||||
$"SELECT date_trunc('{timeBucket}', recorded_at) AS bucket, " +
|
||||
$"tagname, {func}(value::double precision, recorded_at) AS result " +
|
||||
$"tagname, {func} AS result " +
|
||||
$"FROM {HistoryTable} " +
|
||||
$"{whereTag} " +
|
||||
$"GROUP BY 1, 2 ORDER BY 1, 2"
|
||||
@@ -497,8 +499,8 @@ public class TextToSqlService : ITextToSqlService
|
||||
MIN(value::double precision) AS min_val,
|
||||
MAX(value::double precision) AS max_val,
|
||||
STDDEV(value::double precision) AS stddev_val,
|
||||
first(value::double precision, recorded_at) AS first_val,
|
||||
last(value::double precision, recorded_at) AS last_val,
|
||||
(array_agg(value::double precision ORDER BY recorded_at))[1] AS first_val,
|
||||
(array_agg(value::double precision ORDER BY recorded_at DESC))[1] AS last_val,
|
||||
COUNT(*) AS point_count
|
||||
FROM history_table
|
||||
WHERE tagname = @tagName
|
||||
|
||||
@@ -56,13 +56,13 @@ public class TextToSqlController : ControllerBase
|
||||
public async Task<IActionResult> QueryWithNl([FromBody] NaturalLanguageQueryDto dto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.Query))
|
||||
return BadRequest(new { success = false, error = "질문이 비어있음" });
|
||||
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 });
|
||||
return Ok(new { Success = false, Error = result.Error });
|
||||
|
||||
try
|
||||
{
|
||||
@@ -77,18 +77,18 @@ public class TextToSqlController : ControllerBase
|
||||
jsonData["data"] = parsedData!;
|
||||
}
|
||||
|
||||
return Ok(new { success = true, data = jsonData });
|
||||
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 });
|
||||
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 });
|
||||
return Ok(new { Success = false, Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,12 +101,12 @@ public class TextToSqlController : ControllerBase
|
||||
try
|
||||
{
|
||||
var tools = await _mcpService.ListToolsAsync();
|
||||
return Ok(new { success = true, tools });
|
||||
return Ok(new { Success = true, Tools = tools });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[TextToSql] 도구 목록 조회 실패");
|
||||
return Ok(new { success = false, error = ex.Message });
|
||||
return Ok(new { Success = false, Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ public class TextToSqlController : ControllerBase
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.Sql))
|
||||
{
|
||||
return BadRequest(new { success = false, error = "SQL이 비어있음" });
|
||||
return BadRequest(new { Success = false, Error = "SQL이 비어있음" });
|
||||
}
|
||||
|
||||
try
|
||||
@@ -131,8 +131,8 @@ public class TextToSqlController : ControllerBase
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
success = false,
|
||||
error = result.Error
|
||||
Success = false,
|
||||
Error = result.Error
|
||||
});
|
||||
}
|
||||
|
||||
@@ -140,17 +140,17 @@ public class TextToSqlController : ControllerBase
|
||||
try
|
||||
{
|
||||
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data!);
|
||||
return Ok(new { success = true, data = jsonData });
|
||||
return Ok(new { Success = true, Data = jsonData });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Ok(new { success = true, data = result.Data });
|
||||
return Ok(new { Success = true, Data = result.Data });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[TextToSql] MCP 실행 실패");
|
||||
return Ok(new { success = false, error = ex.Message });
|
||||
return Ok(new { Success = false, Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,8 +178,8 @@ public class TextToSqlController : ControllerBase
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
success = false,
|
||||
error = result.Error
|
||||
Success = false,
|
||||
Error = result.Error
|
||||
});
|
||||
}
|
||||
|
||||
@@ -188,23 +188,23 @@ public class TextToSqlController : ControllerBase
|
||||
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data!);
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
data = jsonData
|
||||
Success = true,
|
||||
Data = jsonData
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
data = result.Data
|
||||
Success = true,
|
||||
Data = result.Data
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[TextToSql] History 쿼리 실패");
|
||||
return Ok(new { success = false, error = ex.Message });
|
||||
return Ok(new { Success = false, Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,8 +223,8 @@ public class TextToSqlController : ControllerBase
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
success = false,
|
||||
error = result.Error
|
||||
Success = false,
|
||||
Error = result.Error
|
||||
});
|
||||
}
|
||||
|
||||
@@ -233,23 +233,23 @@ public class TextToSqlController : ControllerBase
|
||||
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data!);
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
data = jsonData
|
||||
Success = true,
|
||||
Data = jsonData
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
data = result.Data
|
||||
Success = true,
|
||||
Data = result.Data
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[TextToSql] 태그 검색 실패");
|
||||
return Ok(new { success = false, error = ex.Message });
|
||||
return Ok(new { Success = false, Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,8 +267,8 @@ public class TextToSqlController : ControllerBase
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
success = false,
|
||||
error = result.Error
|
||||
Success = false,
|
||||
Error = result.Error
|
||||
});
|
||||
}
|
||||
|
||||
@@ -277,23 +277,23 @@ public class TextToSqlController : ControllerBase
|
||||
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data!);
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
data = jsonData
|
||||
Success = true,
|
||||
Data = jsonData
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
data = result.Data
|
||||
Success = true,
|
||||
Data = result.Data
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[TextToSql] 도면 목록 조회 실패");
|
||||
return Ok(new { success = false, error = ex.Message });
|
||||
return Ok(new { Success = false, Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +304,7 @@ public class TextToSqlController : ControllerBase
|
||||
public async Task<IActionResult> Suggest([FromQuery] string input = "")
|
||||
{
|
||||
var suggestions = await _textToSqlService.SuggestQueriesAsync(input);
|
||||
return Ok(new { success = true, suggestions });
|
||||
return Ok(new { Success = true, Suggestions = suggestions });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -315,20 +315,20 @@ public class TextToSqlController : ControllerBase
|
||||
{
|
||||
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
|
||||
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()
|
||||
});
|
||||
}
|
||||
@@ -353,15 +353,15 @@ public class TextToSqlController : ControllerBase
|
||||
|
||||
var response = new
|
||||
{
|
||||
success = true,
|
||||
tagNames = result.TagNames.ToList(),
|
||||
rows = result.Rows.Select(r => new
|
||||
Success = true,
|
||||
TagNames = result.TagNames.ToList(),
|
||||
Rows = result.Rows.Select(r => new
|
||||
{
|
||||
timeBucket = r.TimeBucket,
|
||||
values = r.Values
|
||||
TimeBucket = r.TimeBucket,
|
||||
Values = r.Values
|
||||
}).ToList(),
|
||||
baseIntervalSeconds = result.BaseIntervalSeconds,
|
||||
queryInterval = result.QueryInterval
|
||||
BaseIntervalSeconds = result.BaseIntervalSeconds,
|
||||
QueryInterval = result.QueryInterval
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
@@ -369,7 +369,7 @@ public class TextToSqlController : ControllerBase
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[TextToSql] QueryHistoryInterval 실패");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { success = false, error = ex.Message });
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { Success = false, Error = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ async function evtQuery() {
|
||||
area: area || null,
|
||||
subArea: subArea || null,
|
||||
limit: parseInt(limit),
|
||||
from: fromRaw ? new Date(fromRaw).toISOString() : null,
|
||||
to: toRaw ? new Date(toRaw).toISOString() : null
|
||||
from: fromRaw && fromRaw.trim() ? new Date(fromRaw).toISOString() : null,
|
||||
to: toRaw && toRaw.trim() ? new Date(toRaw).toISOString() : null
|
||||
};
|
||||
|
||||
const infoEl = document.getElementById('evt-result-info');
|
||||
|
||||
@@ -78,24 +78,24 @@ async function t2sParse() {
|
||||
try {
|
||||
const res = await api('POST', '/api/text-to-sql/query-nl', { query: input });
|
||||
console.log('[t2sParse] MCP 응답:', res);
|
||||
if (res.success) {
|
||||
const d = (typeof res.data === 'object' && res.data !== null) ? res.data : {};
|
||||
if (res.Success) {
|
||||
const d = (typeof res.Data === 'object' && res.Data !== null) ? res.Data : {};
|
||||
console.log('[t2sParse] 데이터 파싱:', d, 'type:', typeof d);
|
||||
console.log('[t2sParse] d.data:', d.data, 'd.columns:', d.columns, 'd.count:', d.count);
|
||||
console.log('[t2sParse] d.Data:', d.Data, 'd.Columns:', d.Columns, 'd.Count:', d.Count);
|
||||
|
||||
// d.data가 object이지만 rows/columns 필드가 없을 수 있으므로 확인
|
||||
const dataObj = d.data || (Array.isArray(d) ? d : null);
|
||||
// d.Data가 object이지만 rows/columns 필드가 없을 수 있으므로 확인
|
||||
const dataObj = d.Data || (Array.isArray(d) ? d : null);
|
||||
console.log('[t2sParse] dataObj:', dataObj);
|
||||
|
||||
if (d.success === false) {
|
||||
sqlTextarea.value = `오류: ${d.error || 'SQL 생성 실패'}`;
|
||||
resultContainer.innerHTML = `<div class="t2s-error">MCP 오류: ${esc(d.error || 'SQL 생성 실패')}</div>`;
|
||||
if (d.Success === false) {
|
||||
sqlTextarea.value = `오류: ${d.Error || 'SQL 생성 실패'}`;
|
||||
resultContainer.innerHTML = `<div class="t2s-error">MCP 오류: ${esc(d.Error || 'SQL 생성 실패')}</div>`;
|
||||
} else {
|
||||
sqlTextarea.value = d.sql || '(SQL 없음)';
|
||||
// d.data가 object이지만 rows/columns 필드가 없을 수 있으므로 확인
|
||||
const rows = (dataObj && Array.isArray(dataObj.rows)) ? dataObj.rows : (Array.isArray(dataObj) ? dataObj : []);
|
||||
const columns = (dataObj && Array.isArray(dataObj.columns)) ? dataObj.columns : [];
|
||||
const totalCount = (dataObj && typeof dataObj.count === 'number') ? dataObj.count : (Array.isArray(dataObj) ? dataObj.length : 0);
|
||||
sqlTextarea.value = d.Sql || '(SQL 없음)';
|
||||
// d.Data가 object이지만 rows/columns 필드가 없을 수 있으므로 확인
|
||||
const rows = (dataObj && Array.isArray(dataObj.Rows)) ? dataObj.Rows : (Array.isArray(dataObj) ? dataObj : []);
|
||||
const columns = (dataObj && Array.isArray(dataObj.Columns)) ? dataObj.Columns : [];
|
||||
const totalCount = (dataObj && typeof dataObj.Count === 'number') ? dataObj.Count : (Array.isArray(dataObj) ? dataObj.length : 0);
|
||||
console.log('[t2sParse] 최종 rows.length:', rows.length, 'columns:', columns);
|
||||
|
||||
if (rows.length === 0) {
|
||||
@@ -105,7 +105,7 @@ async function t2sParse() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sqlTextarea.value = `오류: ${res.error || '알 수 없는 오류'}`;
|
||||
sqlTextarea.value = `오류: ${res.Error || '알 수 없는 오류'}`;
|
||||
resultContainer.innerHTML = '';
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -116,10 +116,10 @@ async function t2sParse() {
|
||||
sqlTextarea.value = '변환 중...';
|
||||
try {
|
||||
const res = await api('POST', '/api/text-to-sql/parse', { query: input });
|
||||
if (res.success) {
|
||||
sqlTextarea.value = res.sql || 'SQL 생성 실패';
|
||||
if (res.Success) {
|
||||
sqlTextarea.value = res.Sql || 'SQL 생성 실패';
|
||||
} else {
|
||||
sqlTextarea.value = `오류: ${res.error || '알 수 없는 오류'}`;
|
||||
sqlTextarea.value = `오류: ${res.Error || '알 수 없는 오류'}`;
|
||||
}
|
||||
} catch (err) {
|
||||
sqlTextarea.value = `연결 오류: ${err.message}`;
|
||||
@@ -171,23 +171,23 @@ async function t2sExecute() {
|
||||
if (t2sMode === 'mcp') {
|
||||
const res = await api('POST', '/api/text-to-sql/execute-mcp', { sql, limit });
|
||||
console.log('[t2sExecute] MCP execute 응답:', res);
|
||||
if (res.success) {
|
||||
const d = res.data || {};
|
||||
console.log('[t2sExecute] d.data:', d.data, 'd.columns:', d.columns);
|
||||
const { columns, rows } = t2sPivot(d.columns || [], d.data || []);
|
||||
console.log('[t2sExecute] pivot 후 rows.length:', rows.length);
|
||||
t2sRenderTable({ rows, columns, totalCount: rows.length });
|
||||
if (res.Success) {
|
||||
const d = res.Data || {};
|
||||
console.log('[t2sExecute] d.Data:', d.Data, 'd.Columns:', d.Columns);
|
||||
const { Columns, Rows } = t2sPivot(d.Columns || [], d.Data || []);
|
||||
console.log('[t2sExecute] pivot 후 rows.length:', Rows.length);
|
||||
t2sRenderTable({ rows: Rows, columns: Columns, totalCount: Rows.length });
|
||||
} else {
|
||||
resultContainer.innerHTML = `<div class="t2s-error">오류: ${res.error || '알 수 없는 오류'}</div>`;
|
||||
resultContainer.innerHTML = `<div class="t2s-error">오류: ${res.Error || '알 수 없는 오류'}</div>`;
|
||||
}
|
||||
} else {
|
||||
const res = await api('POST', '/api/text-to-sql/execute-mcp', { sql, limit });
|
||||
if (res.success) {
|
||||
const d = res.data || {};
|
||||
const { columns, rows } = t2sPivot(d.columns || [], d.data || []);
|
||||
t2sRenderTable({ rows, columns, totalCount: rows.length });
|
||||
if (res.Success) {
|
||||
const d = res.Data || {};
|
||||
const { Columns, Rows } = t2sPivot(d.Columns || [], d.Data || []);
|
||||
t2sRenderTable({ rows: Rows, columns: Columns, totalCount: Rows.length });
|
||||
} else {
|
||||
resultContainer.innerHTML = `<div class="t2s-error">오류: ${res.error || '알 수 없는 오류'}</div>`;
|
||||
resultContainer.innerHTML = `<div class="t2s-error">오류: ${res.Error || '알 수 없는 오류'}</div>`;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -203,10 +203,10 @@ function t2sRenderTable(result) {
|
||||
|
||||
console.log('[t2sRenderTable] 입력 결과:', result);
|
||||
|
||||
// 백엔드 응답: columns, rows, totalCount (소문자)
|
||||
const rows = result.rows || [];
|
||||
const columns = result.columns || [];
|
||||
const totalCount = result.totalCount || 0;
|
||||
// 백엔드 응답: Columns, Rows, TotalCount (대문자)
|
||||
const rows = result.Rows || [];
|
||||
const columns = result.Columns || [];
|
||||
const totalCount = result.TotalCount || 0;
|
||||
|
||||
console.log('[t2sRenderTable] rows.length:', rows.length, 'columns:', columns);
|
||||
|
||||
@@ -313,8 +313,8 @@ async function t2sAnalyze() {
|
||||
const interval = document.getElementById('t2s-interval').value;
|
||||
const limit = document.getElementById('t2s-limit-analyze').value || '100';
|
||||
|
||||
const dateFrom = document.getElementById('t2s-date-from').value;
|
||||
const dateTo = document.getElementById('t2s-date-to').value;
|
||||
const dateFrom = document.getElementById('hf-t2s-from').value;
|
||||
const dateTo = document.getElementById('hf-t2s-to').value;
|
||||
|
||||
const resultContainer = document.getElementById('t2s-analysis-results');
|
||||
resultContainer.innerHTML = '<div class="t2s-loading">분석 중...</div>';
|
||||
@@ -327,10 +327,10 @@ async function t2sAnalyze() {
|
||||
to: dateTo || undefined
|
||||
});
|
||||
|
||||
if (res.success) {
|
||||
if (res.Success) {
|
||||
t2sRenderAnalysis(res);
|
||||
} else {
|
||||
resultContainer.innerHTML = `<div class="t2s-error">오류: ${res.error || '알 수 없는 오류'}</div>`;
|
||||
resultContainer.innerHTML = `<div class="t2s-error">오류: ${res.Error || '알 수 없는 오류'}</div>`;
|
||||
}
|
||||
} catch (err) {
|
||||
resultContainer.innerHTML = `<div class="t2s-error">연결 오류: ${err.message}</div>`;
|
||||
@@ -343,22 +343,22 @@ async function t2sAnalyze() {
|
||||
function t2sRenderAnalysis(result) {
|
||||
const container = document.getElementById('t2s-analysis-results');
|
||||
|
||||
if (!result.tags || result.tags.length === 0) {
|
||||
if (!result.Tags || result.Tags.length === 0) {
|
||||
container.innerHTML = '<div class="t2s-empty">분석 결과가 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div class="t2s-result-info">총 <b>' + result.tags.length + '</b>개 태그 분석</div>';
|
||||
let html = '<div class="t2s-result-info">총 <b>' + result.Tags.length + '</b>개 태그 분석</div>';
|
||||
html += '<div class="t2s-analysis-grid">';
|
||||
|
||||
result.tags.forEach(tag => {
|
||||
result.Tags.forEach(tag => {
|
||||
html += '<div class="t2s-tag-card">';
|
||||
html += `<h4>${esc(tag.tagName.toUpperCase())}</h4>`;
|
||||
html += `<h4>${esc(tag.TagName.toUpperCase())}</h4>`;
|
||||
html += '<div class="t2s-tag-stats">';
|
||||
html += `<div class="t2s-stat-row"><span>평균:</span><span class="t2s-value">${tag.mean?.toFixed(2) || 'N/A'}</span></div>`;
|
||||
html += `<div class="t2s-stat-row"><span>최대:</span><span class="t2s-value t2s-max">${tag.max?.toFixed(2) || 'N/A'}</span></div>`;
|
||||
html += `<div class="t2s-stat-row"><span>최소:</span><span class="t2s-value t2s-min">${tag.min?.toFixed(2) || 'N/A'}</span></div>`;
|
||||
html += `<div class="t2s-stat-row"><span>표준편차:</span><span class="t2s-value">${tag.stdDev?.toFixed(2) || 'N/A'}</span></div>`;
|
||||
html += `<div class="t2s-stat-row"><span>평균:</span><span class="t2s-value">${tag.Mean?.toFixed(2) || 'N/A'}</span></div>`;
|
||||
html += `<div class="t2s-stat-row"><span>최대:</span><span class="t2s-value t2s-max">${tag.Max?.toFixed(2) || 'N/A'}</span></div>`;
|
||||
html += `<div class="t2s-stat-row"><span>최소:</span><span class="t2s-value t2s-min">${tag.Min?.toFixed(2) || 'N/A'}</span></div>`;
|
||||
html += `<div class="t2s-stat-row"><span>표준편차:</span><span class="t2s-value">${tag.StdDev?.toFixed(2) || 'N/A'}</span></div>`;
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
@@ -403,19 +403,19 @@ async function t2sChatSend() {
|
||||
const loadMsgs = document.querySelectorAll('[id^="t2s-chat-loading-"]');
|
||||
loadMsgs.forEach(el => el.remove());
|
||||
|
||||
if (!executeRes.success) {
|
||||
t2sAddChatMessage('system', `<span class="t2s-error">실패: ${executeRes.error || '알 수 없는 오류'}</span>`);
|
||||
if (!executeRes.Success) {
|
||||
t2sAddChatMessage('system', `<span class="t2s-error">실패: ${executeRes.Error || '알 수 없는 오류'}</span>`);
|
||||
} else {
|
||||
const d = executeRes.data || {};
|
||||
if (d.sql) {
|
||||
document.getElementById('t2s-sql').value = d.sql;
|
||||
t2sAddChatMessage('system', `✅ SQL 생성:<br><pre class="t2s-chat-sql">${esc(d.sql)}</pre>`);
|
||||
const d = executeRes.Data || {};
|
||||
if (d.Sql) {
|
||||
document.getElementById('t2s-sql').value = d.Sql;
|
||||
t2sAddChatMessage('system', `✅ SQL 생성:<br><pre class="t2s-chat-sql">${esc(d.Sql)}</pre>`);
|
||||
}
|
||||
if (d.success === false) {
|
||||
t2sAddChatMessage('system', `<span class="t2s-error">실행 오류: ${d.error || '알 수 없는 오류'}</span>`);
|
||||
if (d.Success === false) {
|
||||
t2sAddChatMessage('system', `<span class="t2s-error">실행 오류: ${d.Error || '알 수 없는 오류'}</span>`);
|
||||
} else {
|
||||
t2sRenderTable({ rows: d.data || [], columns: d.columns || [], totalCount: d.count || 0 });
|
||||
t2sAddChatMessage('system', `✅ <b>${d.count || 0}</b>개 결과 조회 완료`);
|
||||
t2sRenderTable({ rows: d.Data || [], columns: d.Columns || [], totalCount: d.Count || 0 });
|
||||
t2sAddChatMessage('system', `✅ <b>${d.Count || 0}</b>개 결과 조회 완료`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -429,8 +429,8 @@ async function t2sChatSend() {
|
||||
const loadingEl = document.getElementById(loadingId);
|
||||
if (loadingEl) loadingEl.remove();
|
||||
|
||||
if (!parseRes.success || !parseRes.sql) {
|
||||
t2sAddChatMessage('system', `<span class="t2s-error">SQL 변환 실패: ${parseRes.error || '알 수 없는 오류'}</span>`);
|
||||
if (!parseRes.Success || !parseRes.Sql) {
|
||||
t2sAddChatMessage('system', `<span class="t2s-error">SQL 변환 실패: ${parseRes.Error || '알 수 없는 오류'}</span>`);
|
||||
input.disabled = false;
|
||||
document.getElementById('t2s-chat-send-btn').disabled = false;
|
||||
input.focus();
|
||||
@@ -438,32 +438,32 @@ async function t2sChatSend() {
|
||||
}
|
||||
|
||||
// SQL 텍스트박스에도 반영
|
||||
document.getElementById('t2s-sql').value = parseRes.sql;
|
||||
document.getElementById('t2s-sql').value = parseRes.Sql;
|
||||
|
||||
// 시스템 메시지: 변환된 SQL 표시
|
||||
t2sAddChatMessage('system', `✅ SQL 변환 완료:<br><pre class="t2s-chat-sql">${esc(parseRes.sql)}</pre>`);
|
||||
t2sAddChatMessage('system', `✅ SQL 변환 완료:<br><pre class="t2s-chat-sql">${esc(parseRes.Sql)}</pre>`);
|
||||
|
||||
// 2. SQL 자동 실행
|
||||
t2sAddChatMessage('system', '<span class="t2s-typing">쿼리 실행 중...</span>');
|
||||
|
||||
const limitInput = document.getElementById('t2s-limit');
|
||||
const limit = limitInput.value ? parseInt(limitInput.value) : 1000;
|
||||
const executeRes = await api('POST', '/api/text-to-sql/execute-mcp', { sql: parseRes.sql, limit });
|
||||
const executeRes = await api('POST', '/api/text-to-sql/execute-mcp', { sql: parseRes.Sql, limit });
|
||||
|
||||
// 로딩 메시지 제거
|
||||
const loadMsgs = document.querySelectorAll('[id^="t2s-chat-loading-"]');
|
||||
loadMsgs.forEach(el => el.remove());
|
||||
|
||||
if (!executeRes.success) {
|
||||
t2sAddChatMessage('system', `<span class="t2s-error">쿼리 실행 실패: ${executeRes.error || '알 수 없는 오류'}</span>`);
|
||||
if (!executeRes.Success) {
|
||||
t2sAddChatMessage('system', `<span class="t2s-error">쿼리 실행 실패: ${executeRes.Error || '알 수 없는 오류'}</span>`);
|
||||
} else {
|
||||
// 결과 테이블 업데이트
|
||||
const d = executeRes.data || {};
|
||||
const { columns, rows } = t2sPivot(d.columns || [], d.data || []);
|
||||
t2sRenderTable({ rows, columns, totalCount: rows.length });
|
||||
const d = executeRes.Data || {};
|
||||
const { Columns, Rows } = t2sPivot(d.Columns || [], d.Data || []);
|
||||
t2sRenderTable({ rows: Rows, columns: Columns, totalCount: Rows.length });
|
||||
|
||||
// 결과 수 표시
|
||||
const totalCount = executeRes.totalCount || 0;
|
||||
const totalCount = executeRes.TotalCount || 0;
|
||||
t2sAddChatMessage('system', `✅ <b>${totalCount}</b>개 결과 조회 완료`);
|
||||
}
|
||||
}
|
||||
@@ -514,9 +514,9 @@ function t2sAddChatMessage(type, content, id) {
|
||||
async function loadMcpTools() {
|
||||
try {
|
||||
const res = await api('GET', '/api/text-to-sql/tools');
|
||||
if (res.success && res.tools && res.tools.length > 0) {
|
||||
renderToolsChips(res.tools);
|
||||
return res.tools;
|
||||
if (res.Success && res.Tools && res.Tools.length > 0) {
|
||||
renderToolsChips(res.Tools);
|
||||
return res.Tools;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('MCP 도구 로드 실패:', e);
|
||||
@@ -555,10 +555,10 @@ async function callTool(toolName) {
|
||||
|
||||
try {
|
||||
const res = await api('POST', '/api/text-to-sql/execute-mcp', { sql: input });
|
||||
if (res.success) {
|
||||
if (res.Success) {
|
||||
t2sRenderTable(res);
|
||||
} else {
|
||||
resultContainer.innerHTML = `<div class="t2s-error">오류: ${res.error || '알 수 없는 오류'}</div>`;
|
||||
resultContainer.innerHTML = `<div class="t2s-error">오류: ${res.Error || '알 수 없는 오류'}</div>`;
|
||||
}
|
||||
} catch (err) {
|
||||
resultContainer.innerHTML = `<div class="t2s-error">연결 오류: ${err.message}</div>`;
|
||||
@@ -595,8 +595,8 @@ async function apiChatSend() {
|
||||
const loadingEl = document.getElementById(loadingId);
|
||||
if (loadingEl) loadingEl.remove();
|
||||
|
||||
if (!parseRes.success || !parseRes.sql) {
|
||||
apiAddChatMessage('system', `<span class="t2s-error">SQL 변환 실패: ${parseRes.error || '알 수 없는 오류'}</span>`);
|
||||
if (!parseRes.Success || !parseRes.Sql) {
|
||||
apiAddChatMessage('system', `<span class="t2s-error">SQL 변환 실패: ${parseRes.Error || '알 수 없는 오류'}</span>`);
|
||||
input.disabled = false;
|
||||
document.getElementById('api-chat-send-btn').disabled = false;
|
||||
input.focus();
|
||||
@@ -604,26 +604,26 @@ async function apiChatSend() {
|
||||
}
|
||||
|
||||
// SQL 표시
|
||||
apiAddChatMessage('assistant', `📝 변환된 SQL:<br><pre class="api-chat-sql">${esc(parseRes.sql)}</pre>`);
|
||||
apiAddChatMessage('assistant', `📝 변환된 SQL:<br><pre class="api-chat-sql">${esc(parseRes.Sql)}</pre>`);
|
||||
|
||||
// 2. SQL 자동 실행
|
||||
apiAddChatMessage('assistant', '<span class="t2s-typing">쿼리 실행 중...</span>');
|
||||
|
||||
const executeRes = await api('POST', '/api/text-to-sql/execute-mcp', { sql: parseRes.sql, limit: 1000 });
|
||||
const executeRes = await api('POST', '/api/text-to-sql/execute-mcp', { sql: parseRes.Sql, limit: 1000 });
|
||||
|
||||
// 로딩 메시지 제거
|
||||
const loadMsgs = document.querySelectorAll('[id^="api-chat-loading-"]');
|
||||
loadMsgs.forEach(el => el.remove());
|
||||
|
||||
if (!executeRes.success) {
|
||||
apiAddChatMessage('system', `<span class="t2s-error">쿼리 실행 실패: ${executeRes.error || '알 수 없는 오류'}</span>`);
|
||||
if (!executeRes.Success) {
|
||||
apiAddChatMessage('system', `<span class="t2s-error">쿼리 실행 실패: ${executeRes.Error || '알 수 없는 오류'}</span>`);
|
||||
} else {
|
||||
// 결과를 응답 창에 표시
|
||||
const d = executeRes.data || {};
|
||||
apiRenderResponse({ rows: d.data || [], totalCount: d.count || 0 });
|
||||
const d = executeRes.Data || {};
|
||||
apiRenderResponse({ rows: d.Data || [], totalCount: d.Count || 0 });
|
||||
|
||||
// 결과 수 표시
|
||||
const totalCount = executeRes.totalCount || 0;
|
||||
const totalCount = executeRes.TotalCount || 0;
|
||||
apiAddChatMessage('assistant', `✅ <b>${totalCount}</b>개 결과 조회 완료`);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -708,7 +708,7 @@ function apiChatClear() {
|
||||
function apiRenderResponse(data) {
|
||||
const container = document.getElementById('api-response-content');
|
||||
|
||||
if (!data.rows || data.rows.length === 0) {
|
||||
if (!data.Rows || data.Rows.length === 0) {
|
||||
container.innerHTML = '<span class="placeholder">조회된 결과가 없습니다</span>';
|
||||
return;
|
||||
}
|
||||
@@ -717,14 +717,14 @@ function apiRenderResponse(data) {
|
||||
let html = '<table class="api-response-table"><thead><tr>';
|
||||
|
||||
// 헤더 생성
|
||||
const columns = Object.keys(data.rows[0]);
|
||||
const columns = Object.keys(data.Rows[0]);
|
||||
columns.forEach(col => {
|
||||
html += `<th>${esc(col)}</th>`;
|
||||
});
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
// 데이터 행 생성
|
||||
data.rows.forEach(row => {
|
||||
data.Rows.forEach(row => {
|
||||
html += '<tr>';
|
||||
columns.forEach(col => {
|
||||
const value = row[col];
|
||||
@@ -734,7 +734,7 @@ function apiRenderResponse(data) {
|
||||
});
|
||||
|
||||
html += '</tbody></table>';
|
||||
html += `<p style="margin-top:8px;font-size:12px;color:var(--t2)">총 ${data.totalCount || 0}개 결과</p>`;
|
||||
html += `<p style="margin-top:8px;font-size:12px;color:var(--t2)">총 ${data.TotalCount || 0}개 결과</p>`;
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
@@ -56,17 +56,19 @@
|
||||
<div class="cols-2" style="margin-top:12px">
|
||||
<div class="fg">
|
||||
<label>시작일 <em>(비우면 최근 24시간)</em></label>
|
||||
<input id="t2s-date-from" class="inp" type="datetime-local"/>
|
||||
<input type="hidden" id="hf-t2s-from"/>
|
||||
<div class="dt-display inp" id="dtp-t2s-from-display" onclick="dtOpen('t2s-from')">— 선택 안 함 —</div>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label>종료일 <em>(비우면 현재)</em></label>
|
||||
<input id="t2s-date-to" class="inp" type="datetime-local"/>
|
||||
<input type="hidden" id="hf-t2s-to"/>
|
||||
<div class="dt-display inp" id="dtp-t2s-to-display" onclick="dtOpen('t2s-to')">— 선택 안 함 —</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:12px">
|
||||
<div class="fg">
|
||||
<label>분석 데이터 제한</label>
|
||||
<input id="t2s-limit-analyze" class="inp" type="number" value="100"/>
|
||||
<input id="t2s-limit-analyze" class="inp" type="number" value="100" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user