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")
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()

View File

@@ -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

View File

@@ -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 });
}
}
}

View File

@@ -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');

View File

@@ -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;
}

View File

@@ -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>