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:
windpacer
2026-06-11 06:11:28 +09:00
parent f1c11931fe
commit dbad4a54cf
6 changed files with 156 additions and 152 deletions

View File

@@ -42,7 +42,7 @@ def _kst_str(dt_iso: str | None) -> str:
QDRANT_URL = os.environ.get("QDRANT_URL", "http://localhost:6333") QDRANT_URL = os.environ.get("QDRANT_URL", "http://localhost:6333")
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434") OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
EMBED_MODEL = os.environ.get("EMBED_MODEL", "nomic-embed-text") 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 from config import get_vllm_model
VLLM_MODEL = get_vllm_model() VLLM_MODEL = get_vllm_model()

View File

@@ -203,11 +203,13 @@ public class TextToSqlService : ITextToSqlService
} }
else else
{ {
// first/last 쿼리 (TimeScaleDB 함수) // first/last 쿼리 — array_agg로 교체 (TimeScaleDB first()/last() 비호환)
var func = aggregate == "first" ? "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> { return (new List<string> {
$"SELECT date_trunc('{timeBucket}', recorded_at) AS bucket, " + $"SELECT date_trunc('{timeBucket}', recorded_at) AS bucket, " +
$"tagname, {func}(value::double precision, recorded_at) AS result " + $"tagname, {func} AS result " +
$"FROM {HistoryTable} " + $"FROM {HistoryTable} " +
$"{whereTag} " + $"{whereTag} " +
$"GROUP BY 1, 2 ORDER BY 1, 2" $"GROUP BY 1, 2 ORDER BY 1, 2"
@@ -497,8 +499,8 @@ public class TextToSqlService : ITextToSqlService
MIN(value::double precision) AS min_val, MIN(value::double precision) AS min_val,
MAX(value::double precision) AS max_val, MAX(value::double precision) AS max_val,
STDDEV(value::double precision) AS stddev_val, STDDEV(value::double precision) AS stddev_val,
first(value::double precision, recorded_at) AS first_val, (array_agg(value::double precision ORDER BY recorded_at))[1] AS first_val,
last(value::double precision, recorded_at) AS last_val, (array_agg(value::double precision ORDER BY recorded_at DESC))[1] AS last_val,
COUNT(*) AS point_count COUNT(*) AS point_count
FROM history_table FROM history_table
WHERE tagname = @tagName WHERE tagname = @tagName

View File

@@ -56,13 +56,13 @@ public class TextToSqlController : ControllerBase
public async Task<IActionResult> QueryWithNl([FromBody] NaturalLanguageQueryDto dto) public async Task<IActionResult> QueryWithNl([FromBody] NaturalLanguageQueryDto dto)
{ {
if (string.IsNullOrWhiteSpace(dto.Query)) if (string.IsNullOrWhiteSpace(dto.Query))
return BadRequest(new { success = false, error = "질문이 비어있음" }); return BadRequest(new { Success = false, Error = "질문이 비어있음" });
try try
{ {
var result = await _mcpService.QueryWithNlAsync(dto.Query); var result = await _mcpService.QueryWithNlAsync(dto.Query);
if (!result.Success) if (!result.Success)
return Ok(new { success = false, error = result.Error }); return Ok(new { Success = false, Error = result.Error });
try try
{ {
@@ -77,18 +77,18 @@ public class TextToSqlController : ControllerBase
jsonData["data"] = parsedData!; jsonData["data"] = parsedData!;
} }
return Ok(new { success = true, data = jsonData }); return Ok(new { Success = true, Data = jsonData });
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "[TextToSql] query-nl JSON 디시리얼라이즈 실패: {Data}", result.Data); _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) catch (Exception ex)
{ {
_logger.LogError(ex, "[TextToSql] query-nl 실패"); _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 try
{ {
var tools = await _mcpService.ListToolsAsync(); var tools = await _mcpService.ListToolsAsync();
return Ok(new { success = true, tools }); return Ok(new { Success = true, Tools = tools });
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "[TextToSql] 도구 목록 조회 실패"); _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)) if (string.IsNullOrWhiteSpace(dto.Sql))
{ {
return BadRequest(new { success = false, error = "SQL이 비어있음" }); return BadRequest(new { Success = false, Error = "SQL이 비어있음" });
} }
try try
@@ -131,8 +131,8 @@ public class TextToSqlController : ControllerBase
{ {
return Ok(new return Ok(new
{ {
success = false, Success = false,
error = result.Error Error = result.Error
}); });
} }
@@ -140,17 +140,17 @@ public class TextToSqlController : ControllerBase
try try
{ {
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data!); 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 catch
{ {
return Ok(new { success = true, data = result.Data }); return Ok(new { Success = true, Data = result.Data });
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "[TextToSql] MCP 실행 실패"); _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 return Ok(new
{ {
success = false, Success = false,
error = result.Error Error = result.Error
}); });
} }
@@ -188,23 +188,23 @@ public class TextToSqlController : ControllerBase
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data!); var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data!);
return Ok(new return Ok(new
{ {
success = true, Success = true,
data = jsonData Data = jsonData
}); });
} }
catch catch
{ {
return Ok(new return Ok(new
{ {
success = true, Success = true,
data = result.Data Data = result.Data
}); });
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "[TextToSql] History 쿼리 실패"); _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 return Ok(new
{ {
success = false, Success = false,
error = result.Error Error = result.Error
}); });
} }
@@ -233,23 +233,23 @@ public class TextToSqlController : ControllerBase
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data!); var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data!);
return Ok(new return Ok(new
{ {
success = true, Success = true,
data = jsonData Data = jsonData
}); });
} }
catch catch
{ {
return Ok(new return Ok(new
{ {
success = true, Success = true,
data = result.Data Data = result.Data
}); });
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "[TextToSql] 태그 검색 실패"); _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 return Ok(new
{ {
success = false, Success = false,
error = result.Error Error = result.Error
}); });
} }
@@ -277,23 +277,23 @@ public class TextToSqlController : ControllerBase
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data!); var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data!);
return Ok(new return Ok(new
{ {
success = true, Success = true,
data = jsonData Data = jsonData
}); });
} }
catch catch
{ {
return Ok(new return Ok(new
{ {
success = true, Success = true,
data = result.Data Data = result.Data
}); });
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "[TextToSql] 도면 목록 조회 실패"); _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 = "") public async Task<IActionResult> Suggest([FromQuery] string input = "")
{ {
var suggestions = await _textToSqlService.SuggestQueriesAsync(input); var suggestions = await _textToSqlService.SuggestQueriesAsync(input);
return Ok(new { success = true, suggestions }); return Ok(new { Success = true, Suggestions = suggestions });
} }
/// <summary> /// <summary>
@@ -315,20 +315,20 @@ public class TextToSqlController : ControllerBase
{ {
var result = await _textToSqlService.AnalyzeAsync(dto); var result = await _textToSqlService.AnalyzeAsync(dto);
return Ok(new { return Ok(new {
success = result.Success, Success = result.Success,
error = result.Error, Error = result.Error,
tags = result.Tags?.Select(t => new { Tags = result.Tags?.Select(t => new {
tagName = t.TagName, TagName = t.TagName,
avg = t.Avg, Avg = t.Avg,
mean = t.Mean, Mean = t.Mean,
min = t.Min, Min = t.Min,
max = t.Max, Max = t.Max,
first = t.First, First = t.First,
last = t.Last, Last = t.Last,
pointCount = t.PointCount, PointCount = t.PointCount,
stddev = t.StdDev, StdDev = t.StdDev,
from = t.From, From = t.From,
to = t.To To = t.To
}).ToList() }).ToList()
}); });
} }
@@ -353,15 +353,15 @@ public class TextToSqlController : ControllerBase
var response = new var response = new
{ {
success = true, Success = true,
tagNames = result.TagNames.ToList(), TagNames = result.TagNames.ToList(),
rows = result.Rows.Select(r => new Rows = result.Rows.Select(r => new
{ {
timeBucket = r.TimeBucket, TimeBucket = r.TimeBucket,
values = r.Values Values = r.Values
}).ToList(), }).ToList(),
baseIntervalSeconds = result.BaseIntervalSeconds, BaseIntervalSeconds = result.BaseIntervalSeconds,
queryInterval = result.QueryInterval QueryInterval = result.QueryInterval
}; };
return Ok(response); return Ok(response);
@@ -369,7 +369,7 @@ public class TextToSqlController : ControllerBase
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "[TextToSql] QueryHistoryInterval 실패"); _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 });
} }
} }
} }

View File

@@ -32,8 +32,8 @@ async function evtQuery() {
area: area || null, area: area || null,
subArea: subArea || null, subArea: subArea || null,
limit: parseInt(limit), limit: parseInt(limit),
from: fromRaw ? new Date(fromRaw).toISOString() : null, from: fromRaw && fromRaw.trim() ? new Date(fromRaw).toISOString() : null,
to: toRaw ? new Date(toRaw).toISOString() : null to: toRaw && toRaw.trim() ? new Date(toRaw).toISOString() : null
}; };
const infoEl = document.getElementById('evt-result-info'); const infoEl = document.getElementById('evt-result-info');

View File

@@ -78,24 +78,24 @@ async function t2sParse() {
try { try {
const res = await api('POST', '/api/text-to-sql/query-nl', { query: input }); const res = await api('POST', '/api/text-to-sql/query-nl', { query: input });
console.log('[t2sParse] MCP 응답:', res); console.log('[t2sParse] MCP 응답:', res);
if (res.success) { if (res.Success) {
const d = (typeof res.data === 'object' && res.data !== null) ? res.data : {}; const d = (typeof res.Data === 'object' && res.Data !== null) ? res.Data : {};
console.log('[t2sParse] 데이터 파싱:', d, 'type:', typeof d); 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 필드가 없을 수 있으므로 확인 // d.Data가 object이지만 rows/columns 필드가 없을 수 있으므로 확인
const dataObj = d.data || (Array.isArray(d) ? d : null); const dataObj = d.Data || (Array.isArray(d) ? d : null);
console.log('[t2sParse] dataObj:', dataObj); console.log('[t2sParse] dataObj:', dataObj);
if (d.success === false) { if (d.Success === false) {
sqlTextarea.value = `오류: ${d.error || 'SQL 생성 실패'}`; sqlTextarea.value = `오류: ${d.Error || 'SQL 생성 실패'}`;
resultContainer.innerHTML = `<div class="t2s-error">MCP 오류: ${esc(d.error || 'SQL 생성 실패')}</div>`; resultContainer.innerHTML = `<div class="t2s-error">MCP 오류: ${esc(d.Error || 'SQL 생성 실패')}</div>`;
} else { } else {
sqlTextarea.value = d.sql || '(SQL 없음)'; sqlTextarea.value = d.Sql || '(SQL 없음)';
// d.data가 object이지만 rows/columns 필드가 없을 수 있으므로 확인 // d.Data가 object이지만 rows/columns 필드가 없을 수 있으므로 확인
const rows = (dataObj && Array.isArray(dataObj.rows)) ? dataObj.rows : (Array.isArray(dataObj) ? dataObj : []); const rows = (dataObj && Array.isArray(dataObj.Rows)) ? dataObj.Rows : (Array.isArray(dataObj) ? dataObj : []);
const columns = (dataObj && Array.isArray(dataObj.columns)) ? dataObj.columns : []; const columns = (dataObj && Array.isArray(dataObj.Columns)) ? dataObj.Columns : [];
const totalCount = (dataObj && typeof dataObj.count === 'number') ? dataObj.count : (Array.isArray(dataObj) ? dataObj.length : 0); const totalCount = (dataObj && typeof dataObj.Count === 'number') ? dataObj.Count : (Array.isArray(dataObj) ? dataObj.length : 0);
console.log('[t2sParse] 최종 rows.length:', rows.length, 'columns:', columns); console.log('[t2sParse] 최종 rows.length:', rows.length, 'columns:', columns);
if (rows.length === 0) { if (rows.length === 0) {
@@ -105,7 +105,7 @@ async function t2sParse() {
} }
} }
} else { } else {
sqlTextarea.value = `오류: ${res.error || '알 수 없는 오류'}`; sqlTextarea.value = `오류: ${res.Error || '알 수 없는 오류'}`;
resultContainer.innerHTML = ''; resultContainer.innerHTML = '';
} }
} catch (err) { } catch (err) {
@@ -116,10 +116,10 @@ async function t2sParse() {
sqlTextarea.value = '변환 중...'; sqlTextarea.value = '변환 중...';
try { try {
const res = await api('POST', '/api/text-to-sql/parse', { query: input }); const res = await api('POST', '/api/text-to-sql/parse', { query: input });
if (res.success) { if (res.Success) {
sqlTextarea.value = res.sql || 'SQL 생성 실패'; sqlTextarea.value = res.Sql || 'SQL 생성 실패';
} else { } else {
sqlTextarea.value = `오류: ${res.error || '알 수 없는 오류'}`; sqlTextarea.value = `오류: ${res.Error || '알 수 없는 오류'}`;
} }
} catch (err) { } catch (err) {
sqlTextarea.value = `연결 오류: ${err.message}`; sqlTextarea.value = `연결 오류: ${err.message}`;
@@ -171,23 +171,23 @@ async function t2sExecute() {
if (t2sMode === 'mcp') { if (t2sMode === 'mcp') {
const res = await api('POST', '/api/text-to-sql/execute-mcp', { sql, limit }); const res = await api('POST', '/api/text-to-sql/execute-mcp', { sql, limit });
console.log('[t2sExecute] MCP execute 응답:', res); console.log('[t2sExecute] MCP execute 응답:', res);
if (res.success) { if (res.Success) {
const d = res.data || {}; const d = res.Data || {};
console.log('[t2sExecute] d.data:', d.data, 'd.columns:', d.columns); console.log('[t2sExecute] d.Data:', d.Data, 'd.Columns:', d.Columns);
const { columns, rows } = t2sPivot(d.columns || [], d.data || []); const { Columns, Rows } = t2sPivot(d.Columns || [], d.Data || []);
console.log('[t2sExecute] pivot 후 rows.length:', rows.length); console.log('[t2sExecute] pivot 후 rows.length:', Rows.length);
t2sRenderTable({ rows, columns, totalCount: rows.length }); t2sRenderTable({ rows: Rows, columns: Columns, totalCount: Rows.length });
} else { } else {
resultContainer.innerHTML = `<div class="t2s-error">오류: ${res.error || '알 수 없는 오류'}</div>`; resultContainer.innerHTML = `<div class="t2s-error">오류: ${res.Error || '알 수 없는 오류'}</div>`;
} }
} else { } else {
const res = await api('POST', '/api/text-to-sql/execute-mcp', { sql, limit }); const res = await api('POST', '/api/text-to-sql/execute-mcp', { sql, limit });
if (res.success) { if (res.Success) {
const d = res.data || {}; const d = res.Data || {};
const { columns, rows } = t2sPivot(d.columns || [], d.data || []); const { Columns, Rows } = t2sPivot(d.Columns || [], d.Data || []);
t2sRenderTable({ rows, columns, totalCount: rows.length }); t2sRenderTable({ rows: Rows, columns: Columns, totalCount: Rows.length });
} else { } else {
resultContainer.innerHTML = `<div class="t2s-error">오류: ${res.error || '알 수 없는 오류'}</div>`; resultContainer.innerHTML = `<div class="t2s-error">오류: ${res.Error || '알 수 없는 오류'}</div>`;
} }
} }
} catch (err) { } catch (err) {
@@ -203,10 +203,10 @@ function t2sRenderTable(result) {
console.log('[t2sRenderTable] 입력 결과:', result); console.log('[t2sRenderTable] 입력 결과:', result);
// 백엔드 응답: columns, rows, totalCount (문자) // 백엔드 응답: Columns, Rows, TotalCount (문자)
const rows = result.rows || []; const rows = result.Rows || [];
const columns = result.columns || []; const columns = result.Columns || [];
const totalCount = result.totalCount || 0; const totalCount = result.TotalCount || 0;
console.log('[t2sRenderTable] rows.length:', rows.length, 'columns:', columns); console.log('[t2sRenderTable] rows.length:', rows.length, 'columns:', columns);
@@ -313,8 +313,8 @@ async function t2sAnalyze() {
const interval = document.getElementById('t2s-interval').value; const interval = document.getElementById('t2s-interval').value;
const limit = document.getElementById('t2s-limit-analyze').value || '100'; const limit = document.getElementById('t2s-limit-analyze').value || '100';
const dateFrom = document.getElementById('t2s-date-from').value; const dateFrom = document.getElementById('hf-t2s-from').value;
const dateTo = document.getElementById('t2s-date-to').value; const dateTo = document.getElementById('hf-t2s-to').value;
const resultContainer = document.getElementById('t2s-analysis-results'); const resultContainer = document.getElementById('t2s-analysis-results');
resultContainer.innerHTML = '<div class="t2s-loading">분석 중...</div>'; resultContainer.innerHTML = '<div class="t2s-loading">분석 중...</div>';
@@ -327,10 +327,10 @@ async function t2sAnalyze() {
to: dateTo || undefined to: dateTo || undefined
}); });
if (res.success) { if (res.Success) {
t2sRenderAnalysis(res); t2sRenderAnalysis(res);
} else { } else {
resultContainer.innerHTML = `<div class="t2s-error">오류: ${res.error || '알 수 없는 오류'}</div>`; resultContainer.innerHTML = `<div class="t2s-error">오류: ${res.Error || '알 수 없는 오류'}</div>`;
} }
} catch (err) { } catch (err) {
resultContainer.innerHTML = `<div class="t2s-error">연결 오류: ${err.message}</div>`; resultContainer.innerHTML = `<div class="t2s-error">연결 오류: ${err.message}</div>`;
@@ -343,22 +343,22 @@ async function t2sAnalyze() {
function t2sRenderAnalysis(result) { function t2sRenderAnalysis(result) {
const container = document.getElementById('t2s-analysis-results'); 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>'; container.innerHTML = '<div class="t2s-empty">분석 결과가 없습니다.</div>';
return; 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">'; html += '<div class="t2s-analysis-grid">';
result.tags.forEach(tag => { result.Tags.forEach(tag => {
html += '<div class="t2s-tag-card">'; 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-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">${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-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 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.StdDev?.toFixed(2) || 'N/A'}</span></div>`;
html += '</div>'; html += '</div>';
html += '</div>'; html += '</div>';
}); });
@@ -403,19 +403,19 @@ async function t2sChatSend() {
const loadMsgs = document.querySelectorAll('[id^="t2s-chat-loading-"]'); const loadMsgs = document.querySelectorAll('[id^="t2s-chat-loading-"]');
loadMsgs.forEach(el => el.remove()); loadMsgs.forEach(el => el.remove());
if (!executeRes.success) { if (!executeRes.Success) {
t2sAddChatMessage('system', `<span class="t2s-error">실패: ${executeRes.error || '알 수 없는 오류'}</span>`); t2sAddChatMessage('system', `<span class="t2s-error">실패: ${executeRes.Error || '알 수 없는 오류'}</span>`);
} else { } else {
const d = executeRes.data || {}; const d = executeRes.Data || {};
if (d.sql) { if (d.Sql) {
document.getElementById('t2s-sql').value = d.sql; document.getElementById('t2s-sql').value = d.Sql;
t2sAddChatMessage('system', `✅ SQL 생성:<br><pre class="t2s-chat-sql">${esc(d.sql)}</pre>`); t2sAddChatMessage('system', `✅ SQL 생성:<br><pre class="t2s-chat-sql">${esc(d.Sql)}</pre>`);
} }
if (d.success === false) { if (d.Success === false) {
t2sAddChatMessage('system', `<span class="t2s-error">실행 오류: ${d.error || '알 수 없는 오류'}</span>`); t2sAddChatMessage('system', `<span class="t2s-error">실행 오류: ${d.Error || '알 수 없는 오류'}</span>`);
} else { } else {
t2sRenderTable({ rows: d.data || [], columns: d.columns || [], totalCount: d.count || 0 }); t2sRenderTable({ rows: d.Data || [], columns: d.Columns || [], totalCount: d.Count || 0 });
t2sAddChatMessage('system', `✅ <b>${d.count || 0}</b>개 결과 조회 완료`); t2sAddChatMessage('system', `✅ <b>${d.Count || 0}</b>개 결과 조회 완료`);
} }
} }
} else { } else {
@@ -429,8 +429,8 @@ async function t2sChatSend() {
const loadingEl = document.getElementById(loadingId); const loadingEl = document.getElementById(loadingId);
if (loadingEl) loadingEl.remove(); if (loadingEl) loadingEl.remove();
if (!parseRes.success || !parseRes.sql) { if (!parseRes.Success || !parseRes.Sql) {
t2sAddChatMessage('system', `<span class="t2s-error">SQL 변환 실패: ${parseRes.error || '알 수 없는 오류'}</span>`); t2sAddChatMessage('system', `<span class="t2s-error">SQL 변환 실패: ${parseRes.Error || '알 수 없는 오류'}</span>`);
input.disabled = false; input.disabled = false;
document.getElementById('t2s-chat-send-btn').disabled = false; document.getElementById('t2s-chat-send-btn').disabled = false;
input.focus(); input.focus();
@@ -438,32 +438,32 @@ async function t2sChatSend() {
} }
// SQL 텍스트박스에도 반영 // SQL 텍스트박스에도 반영
document.getElementById('t2s-sql').value = parseRes.sql; document.getElementById('t2s-sql').value = parseRes.Sql;
// 시스템 메시지: 변환된 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 자동 실행 // 2. SQL 자동 실행
t2sAddChatMessage('system', '<span class="t2s-typing">쿼리 실행 중...</span>'); t2sAddChatMessage('system', '<span class="t2s-typing">쿼리 실행 중...</span>');
const limitInput = document.getElementById('t2s-limit'); const limitInput = document.getElementById('t2s-limit');
const limit = limitInput.value ? parseInt(limitInput.value) : 1000; 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-"]'); const loadMsgs = document.querySelectorAll('[id^="t2s-chat-loading-"]');
loadMsgs.forEach(el => el.remove()); loadMsgs.forEach(el => el.remove());
if (!executeRes.success) { if (!executeRes.Success) {
t2sAddChatMessage('system', `<span class="t2s-error">쿼리 실행 실패: ${executeRes.error || '알 수 없는 오류'}</span>`); t2sAddChatMessage('system', `<span class="t2s-error">쿼리 실행 실패: ${executeRes.Error || '알 수 없는 오류'}</span>`);
} else { } else {
// 결과 테이블 업데이트 // 결과 테이블 업데이트
const d = executeRes.data || {}; const d = executeRes.Data || {};
const { columns, rows } = t2sPivot(d.columns || [], d.data || []); const { Columns, Rows } = t2sPivot(d.Columns || [], d.Data || []);
t2sRenderTable({ rows, columns, totalCount: rows.length }); t2sRenderTable({ rows: Rows, columns: Columns, totalCount: Rows.length });
// 결과 수 표시 // 결과 수 표시
const totalCount = executeRes.totalCount || 0; const totalCount = executeRes.TotalCount || 0;
t2sAddChatMessage('system', `✅ <b>${totalCount}</b>개 결과 조회 완료`); t2sAddChatMessage('system', `✅ <b>${totalCount}</b>개 결과 조회 완료`);
} }
} }
@@ -514,9 +514,9 @@ function t2sAddChatMessage(type, content, id) {
async function loadMcpTools() { async function loadMcpTools() {
try { try {
const res = await api('GET', '/api/text-to-sql/tools'); const res = await api('GET', '/api/text-to-sql/tools');
if (res.success && res.tools && res.tools.length > 0) { if (res.Success && res.Tools && res.Tools.length > 0) {
renderToolsChips(res.tools); renderToolsChips(res.Tools);
return res.tools; return res.Tools;
} }
} catch (e) { } catch (e) {
console.error('MCP 도구 로드 실패:', e); console.error('MCP 도구 로드 실패:', e);
@@ -555,10 +555,10 @@ async function callTool(toolName) {
try { try {
const res = await api('POST', '/api/text-to-sql/execute-mcp', { sql: input }); const res = await api('POST', '/api/text-to-sql/execute-mcp', { sql: input });
if (res.success) { if (res.Success) {
t2sRenderTable(res); t2sRenderTable(res);
} else { } else {
resultContainer.innerHTML = `<div class="t2s-error">오류: ${res.error || '알 수 없는 오류'}</div>`; resultContainer.innerHTML = `<div class="t2s-error">오류: ${res.Error || '알 수 없는 오류'}</div>`;
} }
} catch (err) { } catch (err) {
resultContainer.innerHTML = `<div class="t2s-error">연결 오류: ${err.message}</div>`; resultContainer.innerHTML = `<div class="t2s-error">연결 오류: ${err.message}</div>`;
@@ -595,8 +595,8 @@ async function apiChatSend() {
const loadingEl = document.getElementById(loadingId); const loadingEl = document.getElementById(loadingId);
if (loadingEl) loadingEl.remove(); if (loadingEl) loadingEl.remove();
if (!parseRes.success || !parseRes.sql) { if (!parseRes.Success || !parseRes.Sql) {
apiAddChatMessage('system', `<span class="t2s-error">SQL 변환 실패: ${parseRes.error || '알 수 없는 오류'}</span>`); apiAddChatMessage('system', `<span class="t2s-error">SQL 변환 실패: ${parseRes.Error || '알 수 없는 오류'}</span>`);
input.disabled = false; input.disabled = false;
document.getElementById('api-chat-send-btn').disabled = false; document.getElementById('api-chat-send-btn').disabled = false;
input.focus(); input.focus();
@@ -604,26 +604,26 @@ async function apiChatSend() {
} }
// SQL 표시 // 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 자동 실행 // 2. SQL 자동 실행
apiAddChatMessage('assistant', '<span class="t2s-typing">쿼리 실행 중...</span>'); 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-"]'); const loadMsgs = document.querySelectorAll('[id^="api-chat-loading-"]');
loadMsgs.forEach(el => el.remove()); loadMsgs.forEach(el => el.remove());
if (!executeRes.success) { if (!executeRes.Success) {
apiAddChatMessage('system', `<span class="t2s-error">쿼리 실행 실패: ${executeRes.error || '알 수 없는 오류'}</span>`); apiAddChatMessage('system', `<span class="t2s-error">쿼리 실행 실패: ${executeRes.Error || '알 수 없는 오류'}</span>`);
} else { } else {
// 결과를 응답 창에 표시 // 결과를 응답 창에 표시
const d = executeRes.data || {}; const d = executeRes.Data || {};
apiRenderResponse({ rows: d.data || [], totalCount: d.count || 0 }); apiRenderResponse({ rows: d.Data || [], totalCount: d.Count || 0 });
// 결과 수 표시 // 결과 수 표시
const totalCount = executeRes.totalCount || 0; const totalCount = executeRes.TotalCount || 0;
apiAddChatMessage('assistant', `✅ <b>${totalCount}</b>개 결과 조회 완료`); apiAddChatMessage('assistant', `✅ <b>${totalCount}</b>개 결과 조회 완료`);
} }
} catch (err) { } catch (err) {
@@ -708,7 +708,7 @@ function apiChatClear() {
function apiRenderResponse(data) { function apiRenderResponse(data) {
const container = document.getElementById('api-response-content'); 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>'; container.innerHTML = '<span class="placeholder">조회된 결과가 없습니다</span>';
return; return;
} }
@@ -717,14 +717,14 @@ function apiRenderResponse(data) {
let html = '<table class="api-response-table"><thead><tr>'; 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 => { columns.forEach(col => {
html += `<th>${esc(col)}</th>`; html += `<th>${esc(col)}</th>`;
}); });
html += '</tr></thead><tbody>'; html += '</tr></thead><tbody>';
// 데이터 행 생성 // 데이터 행 생성
data.rows.forEach(row => { data.Rows.forEach(row => {
html += '<tr>'; html += '<tr>';
columns.forEach(col => { columns.forEach(col => {
const value = row[col]; const value = row[col];
@@ -734,7 +734,7 @@ function apiRenderResponse(data) {
}); });
html += '</tbody></table>'; 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; container.innerHTML = html;
} }

View File

@@ -56,17 +56,19 @@
<div class="cols-2" style="margin-top:12px"> <div class="cols-2" style="margin-top:12px">
<div class="fg"> <div class="fg">
<label>시작일 <em>(비우면 최근 24시간)</em></label> <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>
<div class="fg"> <div class="fg">
<label>종료일 <em>(비우면 현재)</em></label> <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> </div>
<div style="margin-top:12px"> <div style="margin-top:12px">
<div class="fg"> <div class="fg">
<label>분석 데이터 제한</label> <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> </div>
</div> </div>