Files
dap-was-dapms/potal/agent-test-backend/bin/main/static/index.html

626 lines
22 KiB
HTML

<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AX HUB Portal PoC</title>
<style>
:root {
--bg: #f4f6f9;
--surface: #ffffff;
--line: #d7dde8;
--text: #172033;
--muted: #667085;
--primary: #1d5fd1;
--primary-dark: #164aa5;
--soft: #eef3fb;
--good: #087443;
--bad: #b42318;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: "Segoe UI", "Noto Sans KR", Arial, sans-serif;
}
header {
background: #162033;
color: #fff;
padding: 18px 28px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
}
h1 {
margin: 0;
font-size: 22px;
letter-spacing: 0;
}
.header-meta {
color: #cbd5e1;
font-size: 13px;
text-align: right;
line-height: 1.5;
}
main {
width: min(1320px, calc(100% - 32px));
margin: 18px auto 28px;
display: grid;
grid-template-columns: 360px minmax(0, 1fr);
gap: 16px;
}
section {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 8px;
padding: 16px;
}
h2 {
margin: 0 0 12px;
font-size: 17px;
letter-spacing: 0;
}
h3 {
margin: 16px 0 8px;
font-size: 14px;
color: var(--muted);
letter-spacing: 0;
}
.stack {
display: grid;
gap: 12px;
}
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.item {
border: 1px solid var(--line);
border-radius: 8px;
padding: 12px;
background: #fff;
}
.item-title {
font-weight: 700;
margin-bottom: 6px;
}
.kv {
display: grid;
grid-template-columns: 110px minmax(0, 1fr);
gap: 5px 10px;
font-size: 13px;
color: var(--muted);
word-break: break-word;
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
background: #e7f6ee;
color: var(--good);
font-size: 12px;
font-weight: 700;
}
label {
display: block;
margin: 10px 0 6px;
color: var(--muted);
font-size: 13px;
font-weight: 700;
}
input,
textarea {
width: 100%;
border: 1px solid var(--line);
border-radius: 6px;
padding: 10px;
font: inherit;
background: #fff;
color: var(--text);
}
textarea {
resize: vertical;
min-height: 92px;
font-family: Consolas, "Courier New", monospace;
font-size: 13px;
}
.chat-box {
min-height: 104px;
font-family: "Segoe UI", "Noto Sans KR", Arial, sans-serif;
font-size: 15px;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
button {
border: 0;
border-radius: 6px;
padding: 10px 12px;
min-height: 38px;
background: var(--primary);
color: #fff;
font-weight: 700;
cursor: pointer;
}
button:hover {
background: var(--primary-dark);
}
button.secondary {
background: #344054;
}
button.secondary:hover {
background: #202939;
}
button.soft {
background: var(--soft);
color: #1f2937;
}
button.soft:hover {
background: #dfe8f6;
}
pre {
margin: 0;
min-height: 300px;
padding: 14px;
overflow: auto;
border-radius: 8px;
background: #101828;
color: #e5e7eb;
font-size: 13px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.answer {
margin-top: 12px;
padding: 14px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fbfcff;
min-height: 80px;
line-height: 1.6;
}
.manual-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
@media (max-width: 960px) {
header,
main,
.two-col,
.manual-grid {
grid-template-columns: 1fr;
}
header {
display: grid;
}
.header-meta {
text-align: left;
}
}
</style>
</head>
<body>
<header>
<div>
<h1>AX HUB Portal PoC</h1>
<div class="header-meta" style="text-align:left">Hardcoded Registry + Agent Backend + MCP Tool Call</div>
</div>
<div class="header-meta" id="config">Loading configuration...</div>
</header>
<main>
<div class="stack">
<section>
<h2>Portal Registry</h2>
<p class="header-meta" style="text-align:left;color:var(--muted)">필요할 때 MCP가 Registry를 다시 확인하도록 revision을 갱신합니다.</p>
<input id="routeKey" value="cus" type="hidden">
<div class="actions">
<button onclick="bumpRevision()">Portal Revision 올리기</button>
<button onclick="openNewBundle()">+ Tool Server 추가</button>
</div>
<details id="bundleEditor" style="margin-top:14px">
<summary><strong>Tool Server 설정</strong></summary>
<div style="margin-top:12px">
<label for="bundleId">Bundle ID</label>
<input id="bundleId" placeholder="business-tools">
<label for="toolServerName">Tool Server 이름</label>
<input id="toolServerName" placeholder="Business Tool Server">
<label for="manifestUrl">Manifest URL</label>
<input id="manifestUrl" placeholder="http://localhost:9090/tool-manifest">
<label for="baseUrl">실행 Base URL</label>
<input id="baseUrl" placeholder="http://localhost:9090">
<label for="namePrefix">Tool name prefix</label>
<input id="namePrefix" placeholder="business.">
<label><input id="bundleEnabled" type="checkbox" checked style="width:auto"> 활성화</label>
<details style="margin-top:12px">
<summary>고급 설정</summary>
<label for="pollInterval">Manifest 조회 주기(초)</label>
<input id="pollInterval" type="number" min="5" value="60">
<label for="apiKey">Tool Server API Key (입력 전용)</label>
<input id="apiKey" type="password" autocomplete="new-password" placeholder="기존 Key를 유지하려면 비워두세요">
<label for="toolEndpoints">Tool 실행 경로 매핑(JSON)</label>
<textarea id="toolEndpoints">{}</textarea>
</details>
<div class="actions">
<button onclick="saveBundle()">저장</button>
<button class="soft" onclick="closeBundleEditor()">취소</button>
</div>
</div>
</details>
</section>
<section>
<details>
<summary><strong>MCP 연결 테스트</strong></summary>
<label for="mcpToolServerFilter">확인할 Tool Server</label>
<select id="mcpToolServerFilter" style="width:100%;padding:10px;border:1px solid var(--line);border-radius:6px">
<option value="external.">External Tool Server</option>
<option value="business.">Business Tool Server</option>
<option value="cus">DAP WAS CUS Tool Server</option>
</select>
<div class="actions">
<button onclick="callApi('POST', mcpTestAction('initialize'))">Initialize</button>
<button class="secondary" onclick="callApi('POST', mcpTestAction('initialized'))">Initialized</button>
<button onclick="loadMcpTools()">Tools/List</button>
</div>
<div id="mcpToolList" class="stack" style="margin-top:12px"></div>
</details>
</section>
</div>
<div class="stack">
<section>
<details>
<summary><strong>Agent 테스트</strong></summary>
<label for="message">사용자 요청</label>
<textarea id="message" class="chat-box">서울 날씨 알려줘</textarea>
<div class="actions">
<button onclick="sendChat()">Agent 실행</button>
<button class="soft" onclick="setMessage('서울 날씨 알려줘')">서울 날씨</button>
<button class="soft" onclick="setMessage('부산 날씨 알려줘')">부산 날씨</button>
<button class="soft" onclick="setMessage('달러 환율 알려줘')">달러 환율</button>
<button class="soft" onclick="setMessage('대한민국 공휴일 조회해줘')">공휴일 조회</button>
<button class="soft" onclick="setMessage('서울 좌표 조회해줘')">서울 좌표</button>
<button class="soft" onclick="setMessage('대한민국 국가 정보 조회해줘')">국가 정보</button>
<button class="soft" onclick="setMessage('고객 조회해줘')">고객 조회</button>
<button class="soft" onclick="setMessage('주문 상태 조회해줘')">주문 상태</button>
<button class="soft" onclick="setMessage('고객 문의 티켓 생성해줘')">티켓 생성</button>
<button class="soft" onclick="setMessage('메타 공통코드 조회해줘')">메타 공통코드</button>
<button class="soft" onclick="setMessage('메타 테이블 조회해줘')">메타 테이블</button>
<button class="soft" onclick="setMessage('템플릿 다운로드 URL 알려줘')">템플릿 URL</button>
<button class="soft" onclick="setMessage('SOL 의뢰서 목록 조회해줘')">SOL 목록</button>
<button class="soft" onclick="setMessage('SOL 의뢰서 상세 조회해줘')">SOL 상세</button>
<button class="soft" onclick="setMessage('보험금 청구 처리해줘')">보험금 청구</button>
<button class="soft" onclick="setMessage('가입설계 한도 조회해줘')">가입설계 한도</button>
<button class="soft" onclick="setMessage('CUS 달러 환율 조회해줘')">CUS 환율</button>
<button class="soft" onclick="setMessage('CUS 서울 날씨 조회해줘')">CUS 날씨</button>
<button class="soft" onclick="setMessage('오늘의 명언 알려줘')">오늘의 명언</button>
<button class="soft" onclick="setMessage('TOOL 파트 구성원 조회해줘')">TOOL 구성원</button>
</div>
<div class="answer" id="answer">Agent 응답 대기 중</div>
</details>
</section>
<section>
<details>
<summary><strong>Tool 직접 호출</strong></summary>
<label for="toolPreset">호출할 Tool</label>
<select id="toolPreset" onchange="setToolPreset(this.value)" style="width:100%;padding:10px;border:1px solid var(--line);border-radius:6px">
<option value="external.weather_lookup">날씨 조회</option>
<option value="external.exchange_rate">환율 조회</option>
<option value="external.public_holiday_lookup">공휴일 조회</option>
<option value="external.geocoding_lookup">도시 좌표 조회</option>
<option value="external.country_info_lookup">국가 정보 조회</option>
<option value="business.customer_search">고객 검색</option>
<option value="business.order_status">주문 상태 조회</option>
<option value="business.ticket_create">지원 티켓 생성(승인 필요)</option>
<option value="cmm_comcode_lookup">메타 공통코드 조회</option>
<option value="cmm_customer_tool">고객 통합 안내이력 조회</option>
<option value="cmm_meta_table">메타 테이블 조회</option>
<option value="cmm_template_url">템플릿 URL 조회</option>
<option value="sol_request_list">SOL 의뢰서 목록</option>
<option value="sol_request_detail">SOL 의뢰서 상세</option>
<option value="ins_insurance_processor">보험금 청구 처리</option>
<option value="oth_onnba3011_call">가입설계 한도 조회</option>
<option value="smp_exchange_inquiry">CUS 환율 조회</option>
<option value="smp_weather_inquiry">CUS 날씨 조회</option>
<option value="smp_quote_daily">오늘의 명언</option>
<option value="smp_team_list">TOOL 파트 구성원</option>
</select>
<div class="manual-grid">
<div>
<label for="toolName">Tool Name</label>
<input id="toolName" value="external.weather_lookup">
</div>
<div>
<label for="arguments">Arguments JSON</label>
<textarea id="arguments">{
"city": "Seoul",
"timezone": "Asia/Seoul"
}</textarea>
</div>
</div>
<div class="actions">
<button onclick="callTool()">Tools/Call</button>
<button class="soft" onclick="setWeather()">Weather Args</button>
<button class="soft" onclick="setExchange()">Exchange Args</button>
</div>
</details>
</section>
<section>
<details>
<summary><strong>상세 응답 보기</strong></summary>
<pre id="output">Waiting...</pre>
</details>
</section>
</div>
</main>
<script>
const output = document.getElementById("output");
const config = document.getElementById("config");
const answer = document.getElementById("answer");
async function callApi(method, url, body) {
output.textContent = "Requesting...";
try {
const response = await fetch(url, {
method,
headers: {"Content-Type": "application/json"},
body: body ? JSON.stringify(body) : undefined
});
const text = await response.text();
const data = text ? JSON.parse(text) : {};
output.textContent = JSON.stringify(data, null, 2);
if (!response.ok) {
throw new Error(data.detail || data.message || data.error || `HTTP ${response.status}`);
}
return data;
} catch (error) {
output.textContent = JSON.stringify({error: error.message}, null, 2);
throw error;
}
}
async function loadConfig() {
const data = await callApi("GET", "/api/config");
document.getElementById("routeKey").value = data.defaultRouteKey || "cus";
config.innerHTML = `MCP ${data.mcpEndpointUrl}<br>Route MCP ${data.defaultRoutedMcpEndpointUrl}<br>Manifest ${data.toolManifestUrl}`;
}
async function loadRegistry() {
const bundles = await callApi("GET", "/api/portal/bundles");
window.portalBundles = bundles;
}
function openNewBundle() {
clearBundleForm();
document.getElementById("bundleEditor").open = true;
}
function closeBundleEditor() {
document.getElementById("bundleEditor").open = false;
}
function clearBundleForm() {
["bundleId", "toolServerName", "manifestUrl", "baseUrl", "namePrefix", "apiKey"]
.forEach(id => document.getElementById(id).value = "");
document.getElementById("bundleId").readOnly = false;
document.getElementById("pollInterval").value = 60;
document.getElementById("toolEndpoints").value = "{}";
document.getElementById("bundleEnabled").checked = true;
}
async function saveBundle() {
const idInput = document.getElementById("bundleId");
const payload = {
bundleId: idInput.value.trim(),
toolServerName: document.getElementById("toolServerName").value.trim(),
manifestUrl: document.getElementById("manifestUrl").value.trim(),
baseUrl: document.getElementById("baseUrl").value.trim(),
namePrefix: document.getElementById("namePrefix").value.trim(),
enabled: document.getElementById("bundleEnabled").checked,
manifestPollIntervalSeconds: Number(document.getElementById("pollInterval").value),
apiKey: document.getElementById("apiKey").value,
toolEndpoints: JSON.parse(document.getElementById("toolEndpoints").value || "{}")
};
const url = idInput.readOnly
? `/api/portal/bundles/${encodeURIComponent(payload.bundleId)}`
: "/api/portal/bundles";
await callApi(idInput.readOnly ? "PUT" : "POST", url, payload);
await loadRegistry();
closeBundleEditor();
}
function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, char => ({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"})[char]);
}
function escapeJs(value) {
return String(value).replace(/[\\']/g, "\\$&");
}
async function bumpRevision() {
const data = await callApi("POST", `/api/portal/registry/${selectedRoute()}/revision`);
await loadRegistry();
output.textContent = JSON.stringify({
message: "Portal registry revision bumped",
routeKey: data.routeKey,
registryRevision: data.registryRevision
}, null, 2);
}
function renderGroup(title, rows) {
return `<div><h3>${title}</h3>${rows.map(renderItem).join("")}</div>`;
}
function renderItem(row) {
const title = row.displayName || row.serviceKey || row.routeKey;
const entries = Object.entries(row)
.map(([key, value]) => `<div>${key}</div><div>${value}</div>`)
.join("");
return `<div class="item"><div class="item-title">${title} <span class="badge">${row.status || row.source}</span></div><div class="kv">${entries}</div></div>`;
}
async function sendChat() {
const message = document.getElementById("message").value.trim();
answer.textContent = "Agent is selecting a tool...";
const data = await callApi("POST", "/api/agent/chat", {message});
answer.innerHTML = `<strong>${data.answer}</strong><br><br>Agent route 결정: ${data.routeKey}<br>판단: ${data.routeDecision}<br>MCP: ${data.mcpEndpointUrl}<br>Tool: ${data.selectedTool}<br>인자: ${JSON.stringify(data.arguments)}`;
}
async function callTool() {
const name = document.getElementById("toolName").value.trim();
const args = JSON.parse(document.getElementById("arguments").value);
const routeKey = name.startsWith("business.") ? "business"
: /^(cmm|ins|oth|smp|sol)_/.test(name) ? "cus" : "external";
await callApi("POST", `/api/mcp/${routeKey}/tools/call`, {name, arguments: args});
}
function setToolPreset(name) {
const presets = {
"external.weather_lookup": {city: "Seoul", timezone: "Asia/Seoul"},
"external.exchange_rate": {from: "USD", to: "KRW"},
"external.public_holiday_lookup": {countryCode: "KR", year: new Date().getFullYear()},
"external.geocoding_lookup": {city: "Seoul", language: "ko"},
"external.country_info_lookup": {countryCode: "KR"},
"business.customer_search": {keyword: "C-1001"},
"business.order_status": {orderId: "O-9001"},
"business.ticket_create": {
title: "Portal test ticket",
priority: "normal",
description: "Created from the Portal Tool test screen"
},
"cmm_comcode_lookup": {groupCode: "GRP_COMM_CD", useYn: "Y"},
"cmm_customer_tool": {csNo: "000000000001"},
"cmm_meta_table": {tableName: "TB_CUST_BAS", owner: "DAPADM"},
"cmm_template_url": {templateId: "TPL_001"},
"sol_request_list": {status: "진행중", period: "1개월", target: "나의 업무"},
"sol_request_detail": {srId: "SR-001"},
"ins_insurance_processor": {claimNumber: "CLM20230001", claimAmount: 1500000, claimDate: "2026-08-12"},
"oth_onnba3011_call": {dalScCd: "1", cstSucoRltyCd: "01", csNo: "000000000001"},
"smp_exchange_inquiry": {currencyCode: "USD"},
"smp_weather_inquiry": {city: "서울"},
"smp_quote_daily": {category: "속담"},
"smp_team_list": {teamName: "TOOL"}
};
document.getElementById("toolName").value = name;
document.getElementById("arguments").value = JSON.stringify(presets[name] || {}, null, 2);
}
function selectedRoute() {
const route = document.getElementById("routeKey").value.trim();
return route || "cus";
}
function mcpAction(action) {
return `/api/mcp/${selectedRoute()}/${action}`;
}
async function loadMcpTools() {
const data = await callApi("POST", mcpTestAction("tools/list"));
const prefix = document.getElementById("mcpToolServerFilter").value;
const tools = (((data || {}).body || {}).result || {}).tools || [];
const filtered = prefix === "all" || prefix === "cus"
? tools : tools.filter(tool => tool.name.startsWith(prefix));
const target = document.getElementById("mcpToolList");
target.innerHTML = filtered.length ? filtered.map(tool => `
<div class="item">
<div class="item-title">${escapeHtml(tool.title || tool.name)}</div>
<div class="kv">
<div>name</div><div>${escapeHtml(tool.name)}</div>
<div>설명</div><div>${escapeHtml(tool.description || "-")}</div>
<div>유형</div><div>${tool.annotations && tool.annotations.readOnlyHint ? "READ" : "WRITE"}</div>
</div>
<div class="actions">
<button class="soft" onclick="prepareToolCall('${escapeJs(tool.name)}')">이 Tool 호출</button>
</div>
</div>`).join("") : `<div class="header-meta" style="text-align:left;color:var(--muted)">선택한 서버의 Tool이 없습니다.</div>`;
}
function mcpTestAction(action) {
const filter = document.getElementById("mcpToolServerFilter").value;
const routeKey = filter === "business." ? "business"
: filter === "cus" ? "cus" : "external";
return `/api/mcp/${routeKey}/${action}`;
}
function prepareToolCall(name) {
setToolPreset(name);
document.getElementById("toolPreset").value = name;
document.getElementById("toolPreset").closest("details").open = true;
document.getElementById("toolPreset").scrollIntoView({behavior: "smooth", block: "center"});
}
function setMessage(value) {
document.getElementById("message").value = value;
}
function setWeather() {
document.getElementById("toolName").value = "external.weather_lookup";
document.getElementById("arguments").value = JSON.stringify({
city: "Seoul",
timezone: "Asia/Seoul"
}, null, 2);
}
function setExchange() {
document.getElementById("toolName").value = "external.exchange_rate";
document.getElementById("arguments").value = JSON.stringify({
from: "USD",
to: "KRW"
}, null, 2);
}
loadConfig().then(loadRegistry).catch(error => {
config.textContent = "Configuration load failed";
output.textContent = JSON.stringify({error: error.message}, null, 2);
});
</script>
</body>
</html>