182 lines
8.1 KiB
HTML
182 lines
8.1 KiB
HTML
<!doctype html>
|
|
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
|
<head th:replace="~{fragments/layout :: head('MCP Reasoning')}"></head>
|
|
<body>
|
|
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
|
<main class="container py-4">
|
|
<div class="page-title">
|
|
<h1>MCP Reasoning</h1>
|
|
<p>Bearer Token으로 보호 객체를 조회한 뒤, 반환된 행과 NULL 처리 결과를 모델이 설명합니다.</p>
|
|
</div>
|
|
|
|
<div class="alert alert-warning" th:if="${runtimeError}">
|
|
<strong th:text="${runtimeError.title()}">DB 연결 설정이 필요합니다.</strong>
|
|
<span th:text="${runtimeError.message()}">message</span>
|
|
<div th:if="${runtimeError.showSupportCommand()}">
|
|
<code>./run.sh backoffice-support</code>
|
|
</div>
|
|
</div>
|
|
|
|
<section class="content-band">
|
|
<div class="section-heading">
|
|
<h2>권한 결과 해석</h2>
|
|
<a class="btn btn-sm btn-outline-secondary" href="/mcp/tools" target="_blank" rel="noreferrer">도구 JSON</a>
|
|
</div>
|
|
<form hx-post="/mcp-reasoning" hx-target="#mcp-result" hx-swap="innerHTML" class="form-grid token-form">
|
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
|
<label>
|
|
등록 토큰
|
|
<select class="form-select" name="tokenKeyId" data-token-context-select>
|
|
<option value="">원문 직접 입력</option>
|
|
<option th:each="token : ${tokens}"
|
|
th:value="${token.keyId()}"
|
|
th:text="${token.displayLabel()}"
|
|
th:attr="data-username=${token.username()},
|
|
data-prefix=${token.maskedToken()},
|
|
data-status=${token.statusLabel()},
|
|
data-expires-at=${token.expiresAt()},
|
|
data-description=${token.description()},
|
|
data-direct-roles=${#strings.listJoin(token.directRoles(), '|')},
|
|
data-groups=${#strings.listJoin(token.groups(), '|')},
|
|
data-inherited-roles=${#strings.listJoin(token.inheritedRoles(), '|')}"></option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Bearer Token 원문
|
|
<input class="form-control" name="bearerToken" type="password" autocomplete="off" required>
|
|
<span class="form-hint">선택 토큰은 컨텍스트 확인용입니다. ORDS 호출에는 원문 입력이 필요합니다.</span>
|
|
</label>
|
|
<aside class="token-context-preview effective-preview span-2" data-token-context-preview>
|
|
<div class="section-heading compact-heading">
|
|
<h3>선택 토큰 컨텍스트</h3>
|
|
<span class="badge text-bg-secondary" data-token-preview="status">미선택</span>
|
|
</div>
|
|
<dl>
|
|
<div>
|
|
<dt>사용자</dt>
|
|
<dd data-token-preview="username">원문 직접 입력</dd>
|
|
</div>
|
|
<div>
|
|
<dt>Prefix</dt>
|
|
<dd><code data-token-preview="prefix">-</code></dd>
|
|
</div>
|
|
<div>
|
|
<dt>만료</dt>
|
|
<dd data-token-preview="expiresAt">-</dd>
|
|
</div>
|
|
<div>
|
|
<dt>직접 역할</dt>
|
|
<dd data-token-preview="directRoles">-</dd>
|
|
</div>
|
|
<div>
|
|
<dt>그룹</dt>
|
|
<dd data-token-preview="groups">-</dd>
|
|
</div>
|
|
<div>
|
|
<dt>그룹 상속 역할</dt>
|
|
<dd data-token-preview="inheritedRoles">-</dd>
|
|
</div>
|
|
</dl>
|
|
<p class="form-hint" data-token-preview="description">질문 전에 이 토큰이 어느 사용자/역할 컨텍스트인지 확인합니다.</p>
|
|
</aside>
|
|
<label>
|
|
조회 대상
|
|
<select class="form-select" id="mcp-reasoning-object" name="objectId" required>
|
|
<option th:each="tool : ${tools}"
|
|
th:value="${tool.objectId()}"
|
|
th:text="${tool.displayName() + ' / ' + tool.ordsPath()}"></option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Limit
|
|
<input class="form-control" name="limit" type="number" min="1" max="500" value="50">
|
|
</label>
|
|
<label class="span-2">
|
|
질문
|
|
<textarea class="form-control" id="mcp-reasoning-question" name="question" rows="3"
|
|
placeholder="비워도 조회 행, NULL 처리, 권한 범위, 다음 조치를 요약합니다."></textarea>
|
|
</label>
|
|
<div class="question-presets span-2" aria-label="질문 예시">
|
|
<button class="btn rw-btn-secondary question-preset" type="button"
|
|
data-question="요약부터 작성해줘. 이 토큰으로 조회 가능한 행 수, 주요 식별자, NULL 처리 여부, 권한 범위를 표로 정리하고 상세 근거를 이어서 설명해줘.">
|
|
기본 분석
|
|
</button>
|
|
<button class="btn rw-btn-secondary question-preset" type="button"
|
|
data-question="반환된 컬럼과 maskedColumns를 기준으로 민감 컬럼이 NULL 처리 또는 미노출됐는지 먼저 요약하고, role/permission 설정에서 확인할 항목을 정리해줘.">
|
|
민감 컬럼 점검
|
|
</button>
|
|
<button class="btn rw-btn-secondary question-preset" type="button"
|
|
data-question="반환된 행 수와 행의 부서/사번 값을 근거로 이 토큰이 전체 조회, 부서 제한, 본인 제한, 조건 제한 중 어디에 가까운지 판단해줘. 추정이면 추정이라고 표시해줘.">
|
|
권한 범위 판단
|
|
</button>
|
|
<button class="btn rw-btn-secondary question-preset" type="button"
|
|
data-question="운영자가 확인해야 할 이상 징후를 먼저 bullet로 요약하고, ORDS path, VPD policy, permission rule, token 상태 중 어디를 봐야 하는지 제시해줘.">
|
|
운영 점검 요약
|
|
</button>
|
|
</div>
|
|
<button class="btn rw-btn-primary" type="submit">Reasoning 실행</button>
|
|
</form>
|
|
</section>
|
|
|
|
<section class="content-band">
|
|
<div class="section-heading">
|
|
<h2>조회 가능 대상</h2>
|
|
<span class="badge text-bg-secondary" th:text="${#lists.size(tools)}">0</span>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>선택</th>
|
|
<th>Object</th>
|
|
<th>ORDS Path</th>
|
|
<th>MCP Tool</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr th:each="tool : ${tools}">
|
|
<td>
|
|
<button class="btn btn-sm rw-btn-secondary reasoning-object-preset"
|
|
type="button"
|
|
th:attr="data-object-id=${tool.objectId()},data-question=${tool.displayName() + '에서 이 토큰으로 조회 가능한 행, NULL 처리 컬럼, 권한 범위를 요약 먼저 표로 정리해줘.'}">
|
|
선택
|
|
</button>
|
|
</td>
|
|
<td th:text="${tool.displayName()}">ADMIN.TABLE</td>
|
|
<td><code th:text="${tool.ordsPath()}">path</code></td>
|
|
<td><code th:text="${tool.name()}">tool</code></td>
|
|
</tr>
|
|
<tr th:if="${#lists.isEmpty(tools)}">
|
|
<td colspan="4" class="text-muted">등록된 보호 객체 도구가 없습니다.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="mcp-result" class="content-band">
|
|
<div class="text-muted">실행하면 ORDS 호출 결과, 모델 요약, request/response 증거가 한 번에 표시됩니다.</div>
|
|
</section>
|
|
</main>
|
|
<script>
|
|
document.querySelectorAll('.question-preset').forEach((button) => {
|
|
button.addEventListener('click', () => {
|
|
const question = document.getElementById('mcp-reasoning-question');
|
|
question.value = button.dataset.question || '';
|
|
question.focus();
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.reasoning-object-preset').forEach((button) => {
|
|
button.addEventListener('click', () => {
|
|
const objectSelect = document.getElementById('mcp-reasoning-object');
|
|
const question = document.getElementById('mcp-reasoning-question');
|
|
objectSelect.value = button.dataset.objectId || objectSelect.value;
|
|
question.value = button.dataset.question || question.value;
|
|
question.focus();
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|