refactor #565: productize knowledge search language

This commit is contained in:
devmrko
2026-06-29 20:51:42 +09:00
parent b674d301db
commit 5b98c87da1
17 changed files with 102 additions and 101 deletions

View File

@@ -103,7 +103,7 @@ public class McpSseService {
if (isVectorTool(tool)) { if (isVectorTool(tool)) {
ObjectNode embedding = objectMapper.createObjectNode(); ObjectNode embedding = objectMapper.createObjectNode();
embedding.put("type", "array"); embedding.put("type", "array");
embedding.put("description", "외부 임베딩 모델이 만든 검색 벡터. 이 예제 fixture는 4차원입니다."); embedding.put("description", "외부 임베딩 모델이 만든 검색 벡터. 개발 환경에서는 4차원 벡터를 사용합니다.");
ObjectNode items = objectMapper.createObjectNode(); ObjectNode items = objectMapper.createObjectNode();
items.put("type", "number"); items.put("type", "number");
embedding.set("items", items); embedding.set("items", items);

View File

@@ -64,7 +64,7 @@ public class OpenAiCompatibleClient {
public List<Double> embedding(String input) { public List<Double> embedding(String input) {
if (!embeddingConfigured()) { if (!embeddingConfigured()) {
throw new AppException("AI 임베딩 설정이 없습니다. BACKOFFICE_AI_EMBEDDING_MODEL을 설정하거나 데모 임베딩을 선택하세요."); throw new AppException("AI 임베딩 설정이 없습니다. BACKOFFICE_AI_EMBEDDING_MODEL을 설정하거나 로컬 임베딩(개발용)을 선택하세요.");
} }
BackofficeProperties.Ai ai = properties.ai(); BackofficeProperties.Ai ai = properties.ai();

View File

@@ -138,7 +138,7 @@ public class ProtectedObjectService {
defaultOrdsPath(normalizedOwner, normalizedObjectName), defaultOrdsPath(normalizedOwner, normalizedObjectName),
String.join(",", columns), String.join(",", columns),
"", "",
"DB 객체를 한 개의 ORDS 조회 Handler로 노출하는 예제 대상" "DB 객체를 한 개의 ORDS 조회 Handler로 노출하는 기본 구성"
); );
long objectId = mapper.nextObjectId(); long objectId = mapper.nextObjectId();
mapper.insertObject(objectId, command); mapper.insertObject(objectId, command);

View File

@@ -65,7 +65,7 @@ public class VectorKnowledgeService {
chunks, chunks,
tags, tags,
registered, registered,
embeddingClient.embeddingConfigured() ? embeddingClient.embeddingModelName() : "DEMO-4D" embeddingClient.embeddingConfigured() ? embeddingClient.embeddingModelName() : "로컬 임베딩(개발용)"
); );
} }
@@ -120,7 +120,7 @@ public class VectorKnowledgeService {
"CB_VECTOR_SEARCH_DOCUMENTS 보호 객체가 없습니다. 28_agent_ords_vector_tag_vpd_setup.sql을 먼저 실행하세요.")); "CB_VECTOR_SEARCH_DOCUMENTS 보호 객체가 없습니다. 28_agent_ords_vector_tag_vpd_setup.sql을 먼저 실행하세요."));
String mode = normalizeMode(embeddingMode); String mode = normalizeMode(embeddingMode);
String requestBody = "{\"embedding\":" + json(embed(normalizedQuery, mode)) + "}"; String requestBody = "{\"embedding\":" + json(embed(normalizedQuery, mode)) + "}";
IssuedToken temporary = tokenService.issueTemporaryToken(userId, "벡터 지식자료 검색 임시 실행"); IssuedToken temporary = tokenService.issueTemporaryToken(userId, "지식 검색 검증 세션");
try { try {
ProbeResult probe = ordsProbeService.runProbe(new ProbeCommand( ProbeResult probe = ordsProbeService.runProbe(new ProbeCommand(
temporary.keyId(), vectorObject.objectId(), temporary.plainToken(), normalizeLimit(limit), requestBody)); temporary.keyId(), vectorObject.objectId(), temporary.plainToken(), normalizeLimit(limit), requestBody));
@@ -223,16 +223,16 @@ public class VectorKnowledgeService {
private String normalizeMode(String mode) { private String normalizeMode(String mode) {
String normalized = mode == null ? DEMO_MODE : mode.trim().toUpperCase(Locale.ROOT); String normalized = mode == null ? DEMO_MODE : mode.trim().toUpperCase(Locale.ROOT);
if (!DEMO_MODE.equals(normalized) && !AI_MODE.equals(normalized)) { if (!DEMO_MODE.equals(normalized) && !AI_MODE.equals(normalized)) {
throw new AppException("임베딩 방식은 DEMO 또는 AI만 사용할 수 있습니다."); throw new AppException("임베딩 방식은 로컬 임베딩 또는 AI 임베딩만 사용할 수 있습니다.");
} }
if (AI_MODE.equals(normalized) && !embeddingClient.embeddingConfigured()) { if (AI_MODE.equals(normalized) && !embeddingClient.embeddingConfigured()) {
throw new AppException("AI 임베딩이 설정되지 않았습니다. 설정에서 embedding model/API key를 추가하거나 DEMO 임베딩을 선택하세요."); throw new AppException("AI 임베딩이 설정되지 않았습니다. 설정에서 embedding model/API key를 추가하거나 로컬 임베딩(개발용)을 선택하세요.");
} }
return normalized; return normalized;
} }
private String embeddingModel(String mode) { private String embeddingModel(String mode) {
return AI_MODE.equals(mode) ? embeddingClient.embeddingModelName() : "DEMO-4D"; return AI_MODE.equals(mode) ? embeddingClient.embeddingModelName() : "로컬 임베딩(개발용)";
} }
private String json(List<Double> values) { private String json(List<Double> values) {

View File

@@ -7,7 +7,7 @@
<header class="page-title guided-hero"> <header class="page-title guided-hero">
<span class="architecture-kicker">권한을 정하면 DB가 그대로 지킵니다</span> <span class="architecture-kicker">권한을 정하면 DB가 그대로 지킵니다</span>
<h1>누가 어떤 데이터를 볼 수 있는지 설계하고, 실제 결과까지 확인하세요.</h1> <h1>누가 어떤 데이터를 볼 수 있는지 설계하고, 실제 결과까지 확인하세요.</h1>
<p class="context-summary">권한 설계 → DB 보호 연결 → 토큰 발급 → 실제 결과 확인</p> <p class="context-summary">권한 설계 → DB 보호 연결 → 검증 세션 → 실제 결과 확인</p>
<details class="explanation-details"> <details class="explanation-details">
<summary>전체 흐름 설명 보기</summary> <summary>전체 흐름 설명 보기</summary>
<p>사용자·그룹·역할에 권한을 연결하면 Oracle VPD가 요청할 때마다 그 규칙을 읽어 허용된 행만 반환합니다. 별도 SQL 필터를 만드는 일은 예외적인 고급 작업입니다.</p> <p>사용자·그룹·역할에 권한을 연결하면 Oracle VPD가 요청할 때마다 그 규칙을 읽어 허용된 행만 반환합니다. 별도 SQL 필터를 만드는 일은 예외적인 고급 작업입니다.</p>
@@ -40,9 +40,9 @@
<a class="journey-card" href="/tokens"> <a class="journey-card" href="/tokens">
<span class="journey-number">3</span> <span class="journey-number">3</span>
<div> <div>
<h2>3. 토큰 발급</h2> <h2>3. 검증 세션</h2>
<p>검증할 사용자를 선택해 토큰을 발급하고, 한 번만 보이는 원문을 복사합니다.</p> <p>검증할 사용자를 선택해 토큰을 발급하고, 한 번만 보이는 원문을 복사합니다.</p>
<strong>테스트 토큰 발급하기 →</strong> <strong>검증 세션 발급하기 →</strong>
</div> </div>
</a> </a>
<a class="journey-card" href="/probe"> <a class="journey-card" href="/probe">

View File

@@ -29,7 +29,7 @@
<button class="rw-menu-trigger" type="button" aria-expanded="false">2. 보호·검증</button> <button class="rw-menu-trigger" type="button" aria-expanded="false">2. 보호·검증</button>
<div class="rw-menu-panel"> <div class="rw-menu-panel">
<a class="nav-link" href="/vpd-policies">DB 보호 연결</a> <a class="nav-link" href="/vpd-policies">DB 보호 연결</a>
<a class="nav-link" href="/tokens">테스트 토큰 발급</a> <a class="nav-link" href="/tokens">검증 세션 발급</a>
<a class="nav-link" href="/probe">권한 결과 확인</a> <a class="nav-link" href="/probe">권한 결과 확인</a>
</div> </div>
</div> </div>
@@ -38,11 +38,11 @@
<div class="rw-menu-panel"> <div class="rw-menu-panel">
<a class="nav-link" href="/objects">ORDS 조회 대상</a> <a class="nav-link" href="/objects">ORDS 조회 대상</a>
<a class="nav-link" href="/ords-handlers">ORDS 핸들러</a> <a class="nav-link" href="/ords-handlers">ORDS 핸들러</a>
<a class="nav-link" href="/vector-knowledge">벡터 지식자료 데모</a> <a class="nav-link" href="/vector-knowledge">지식 검색 관리</a>
<a class="nav-link" href="/mcp-chatbot">Chatbot</a> <a class="nav-link" href="/mcp-chatbot">Chatbot</a>
<a class="nav-link" href="/mcp-reasoning">Reasoning</a> <a class="nav-link" href="/mcp-reasoning">Reasoning</a>
<a class="nav-link" href="/mcp-sse">SSE 서비스</a> <a class="nav-link" href="/mcp-sse">SSE 서비스</a>
<a class="nav-link" href="/mcp-client-demo">Client Demo</a> <a class="nav-link" href="/mcp-client-demo">MCP Client</a>
</div> </div>
</div> </div>
<div class="rw-menu-group"> <div class="rw-menu-group">
@@ -76,8 +76,8 @@
<div class="architecture-arrow"></div> <div class="architecture-arrow"></div>
<div class="architecture-step" th:classappend="${activeLayer == 'token'} ? ' active'"> <div class="architecture-step" th:classappend="${activeLayer == 'token'} ? ' active'">
<span class="architecture-kicker">3 · IDENTITY</span> <span class="architecture-kicker">3 · IDENTITY</span>
<strong>토큰 발급</strong> <strong>검증 세션</strong>
<p>검증할 사용자를 나타내는 일회성 원문 토큰을 준비합니다.</p> <p>확인할 사용자를 나타내는 일회성 토큰을 준비합니다.</p>
</div> </div>
<div class="architecture-arrow"></div> <div class="architecture-arrow"></div>
<div class="architecture-step" th:classappend="${activeLayer == 'ords'} ? ' active'"> <div class="architecture-step" th:classappend="${activeLayer == 'ords'} ? ' active'">

View File

@@ -8,7 +8,7 @@
<span th:text="${object.owner() + '.' + object.objectName()}">ADMIN.TABLE_NAME</span> <span th:text="${object.owner() + '.' + object.objectName()}">ADMIN.TABLE_NAME</span>
<span class="text-muted"> / 기본 ORDS Handler PL/SQL</span> <span class="text-muted"> / 기본 ORDS Handler PL/SQL</span>
</h3> </h3>
<p class="form-hint">이 소스는 VPD context를 설정한 뒤 한 테이블을 SELECT하는 시작 예제입니다. 실제 수정·저장은 <a href="/ords-handlers">ORDS 핸들러</a> 화면에서 parsing schema 권한으로 진행하세요.</p> <p class="form-hint">이 소스는 VPD context를 설정한 뒤 한 테이블을 SELECT하는 기본 구성입니다. 실제 수정·저장은 <a href="/ords-handlers">ORDS 핸들러</a> 화면에서 parsing schema 권한으로 진행하세요.</p>
<pre th:text="${source}">source</pre> <pre th:text="${source}">source</pre>
</section> </section>
</div> </div>

View File

@@ -2,7 +2,7 @@
<div class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}"></div> <div class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}"></div>
<div th:if="${searchResult}"> <div th:if="${searchResult}">
<div class="result-metrics"> <div class="result-metrics">
<div><span>임베딩</span><strong th:text="${searchResult.embeddingModel()}">DEMO-4D</strong></div> <div><span>임베딩</span><strong th:text="${searchResult.embeddingModel() == 'DEMO-4D' ? '로컬 임베딩(개발용)' : searchResult.embeddingModel()}">로컬 임베딩(개발용)</strong></div>
<div><span>검색 결과</span><strong th:text="${searchResult.probe().rowCount() + '건 · ' + searchResult.probe().status()}">0건</strong></div> <div><span>검색 결과</span><strong th:text="${searchResult.probe().rowCount() + '건 · ' + searchResult.probe().status()}">0건</strong></div>
</div> </div>
<div class="next-action-card mt-3"> <div class="next-action-card mt-3">
@@ -14,11 +14,11 @@
<table class="table table-sm align-middle"> <table class="table table-sm align-middle">
<thead> <thead>
<tr> <tr>
<th>Chunk</th> <th>검색 단위 ID</th>
<th>Document</th> <th>자료 ID</th>
<th>Title</th> <th>제목</th>
<th>TECH_TAG</th> <th>기술 태그</th>
<th>Score</th> <th>관련도</th>
<th>본문</th> <th>본문</th>
</tr> </tr>
</thead> </thead>

View File

@@ -21,10 +21,10 @@
<section class="content-band chat-shell"> <section class="content-band chat-shell">
<div class="chat-message assistant-message"> <div class="chat-message assistant-message">
<strong>질문 실행</strong> <strong>질문 실행</strong>
<p>예시를 선택하거나 질문을 직접 입력하세요.</p> <p>추천 질문을 선택하거나 질문을 직접 입력하세요.</p>
<details class="explanation-details"> <details class="explanation-details">
<summary>토큰과 조회 결과 설명 보기</summary> <summary>세션과 조회 결과 설명 보기</summary>
<p>Bearer Token 원문을 붙여 넣거나 사용자를 선택해 10분 임시 토큰으로 실제 ORDS/VPD 결과를 조회할 수 있습니다.</p> <p>Bearer Token 원문을 붙여 넣거나 사용자를 선택해 10분 검증 세션으로 실제 ORDS/VPD 결과를 조회할 수 있습니다.</p>
</details> </details>
</div> </div>
@@ -47,12 +47,12 @@
placeholder="비우면 라우팅만 확인합니다."> placeholder="비우면 라우팅만 확인합니다.">
</label> </label>
<label> <label>
임시 토큰 사용자 (선택) 검증 세션 사용자 (선택)
<select class="form-select" name="tempUserId"> <select class="form-select" name="tempUserId">
<option value="">원문 토큰 사용</option> <option value="">원문 토큰 사용</option>
<option th:each="user : ${users}" th:value="${user.userId()}" th:text="${user.username()}"></option> <option th:each="user : ${users}" th:value="${user.userId()}" th:text="${user.username()}"></option>
</select> </select>
<span class="form-hint">선택하면 질문 처리 중에만 임시 토큰을 발하고 완료 후 즉시 회수합니다.</span> <span class="form-hint">선택하면 질문 처리 중에만 검증 세션을 발하고 완료 후 즉시 폐기합니다.</span>
</label> </label>
<label> <label>
질문 질문
@@ -69,7 +69,7 @@
민감 컬럼 확인 민감 컬럼 확인
</button> </button>
<button class="btn rw-btn-secondary question-preset" type="button" <button class="btn rw-btn-secondary question-preset" type="button"
data-question="CB_VECTOR_SEARCH_DOCUMENTS에서 SPRING_BOOT 또는 ORACLE_VPD 태그 권한으로 검색 가능한 청크와 VPD 결과를 요약해줘."> data-question="CB_VECTOR_SEARCH_DOCUMENTS에서 SPRING_BOOT 또는 ORACLE_VPD 태그 권한으로 검색 가능한 검색 단위와 VPD 결과를 요약해줘.">
태그 벡터 검색 태그 벡터 검색
</button> </button>
<button class="btn rw-btn-secondary question-preset" type="button" <button class="btn rw-btn-secondary question-preset" type="button"

View File

@@ -1,14 +1,14 @@
<!doctype html> <!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"> <html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/layout :: head('MCP Client Demo')}"></head> <head th:replace="~{fragments/layout :: head('MCP Client')}"></head>
<body> <body>
<nav th:replace="~{fragments/layout :: nav}"></nav> <nav th:replace="~{fragments/layout :: nav}"></nav>
<main class="container page-shell"> <main class="container page-shell">
<section class="page-heading"> <section class="page-heading">
<h1>MCP Client Demo</h1> <h1>MCP Client</h1>
<p class="context-summary">Java client가 MCP 연결과 도구 목록 계약 확인합니다.</p> <p class="context-summary">Java client가 MCP 연결과 사용 가능한 도구 계약 확인합니다.</p>
<details class="explanation-details"> <details class="explanation-details">
<summary>Client Demo의 범위 보기</summary> <summary>Client 연동 범위 보기</summary>
<p>백오피스 Java client가 MCP message endpoint의 initialize/tools/list 계약을 확인합니다. 실제 tool 선택과 호출은 reasoning 결과를 받은 MCP client가 담당합니다.</p> <p>백오피스 Java client가 MCP message endpoint의 initialize/tools/list 계약을 확인합니다. 실제 tool 선택과 호출은 reasoning 결과를 받은 MCP client가 담당합니다.</p>
</details> </details>
</section> </section>
@@ -23,7 +23,7 @@
<form hx-post="/mcp-client-demo" hx-target="#mcp-client-demo-result" hx-swap="innerHTML" class="form-grid"> <form hx-post="/mcp-client-demo" hx-target="#mcp-client-demo-result" hx-swap="innerHTML" class="form-grid">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<details class="explanation-details span-2"> <details class="explanation-details span-2">
<summary>이번 테스트에서 확인하는 계약 보기</summary> <summary>확인하는 계약 보기</summary>
<p>이 화면은 고정된 기본 MCP endpoint에서 initialize와 tools/list만 확인합니다. 질문 기반 tool 선택·Bearer Token 전달·tools/call은 MCP Reasoning 또는 외부 MCP client 흐름에서 수행합니다.</p> <p>이 화면은 고정된 기본 MCP endpoint에서 initialize와 tools/list만 확인합니다. 질문 기반 tool 선택·Bearer Token 전달·tools/call은 MCP Reasoning 또는 외부 MCP client 흐름에서 수행합니다.</p>
</details> </details>
<button class="btn rw-btn-primary" type="submit">MCP 계약 확인</button> <button class="btn rw-btn-primary" type="submit">MCP 계약 확인</button>
@@ -55,7 +55,7 @@
</section> </section>
<section id="mcp-client-demo-result" class="content-band"> <section id="mcp-client-demo-result" class="content-band">
<div class="text-muted">Java MCP client 실행 결과가 여기에 표시됩니다.</div> <div class="text-muted">Java MCP client 확인 결과가 여기에 표시됩니다.</div>
</section> </section>
</main> </main>
</body> </body>

View File

@@ -31,22 +31,22 @@
<label> <label>
Bearer Token 원문 Bearer Token 원문
<input class="form-control" name="bearerToken" type="password" autocomplete="off"> <input class="form-control" name="bearerToken" type="password" autocomplete="off">
<span class="form-hint">원문을 붙여 넣거나 임시 토큰 사용자를 선택하세요.</span> <span class="form-hint">원문을 붙여 넣거나 검증 세션 사용자를 선택하세요.</span>
<details class="explanation-details"> <details class="explanation-details">
<summary>토큰 처리 방식 보기</summary> <summary>토큰 처리 방식 보기</summary>
<p>등록 토큰을 목록에서 선택하지 않습니다. 원문은 저장하지 않고 요청에만 사용합니다.</p> <p>등록 토큰을 목록에서 선택하지 않습니다. 원문은 저장하지 않고 요청에만 사용합니다.</p>
</details> </details>
</label> </label>
<label> <label>
임시 토큰 사용자 (선택) 검증 세션 사용자 (선택)
<select class="form-select" name="tempUserId"> <select class="form-select" name="tempUserId">
<option value="">원문 토큰 사용</option> <option value="">원문 토큰 사용</option>
<option th:each="user : ${users}" th:value="${user.userId()}" th:text="${user.username()}"></option> <option th:each="user : ${users}" th:value="${user.userId()}" th:text="${user.username()}"></option>
</select> </select>
<span class="form-hint">실행 중 발하고 완료 즉시 회수합니다.</span> <span class="form-hint">실행 중 발하고 완료 즉시 폐기합니다.</span>
<details class="explanation-details"> <details class="explanation-details">
<summary>임시 토큰 상세 보기</summary> <summary>검증 세션 상세 보기</summary>
<p>선택하면 reasoning 실행 중 10분 토큰을 발하고 완료 즉시 회수합니다.</p> <p>선택하면 reasoning 실행 중 10분 세션을 발하고 완료 즉시 폐기합니다.</p>
</details> </details>
</label> </label>
<label> <label>
@@ -60,7 +60,7 @@
</label> </label>
<div class="question-presets span-2" aria-label="질문 예시"> <div class="question-presets span-2" aria-label="질문 예시">
<button class="btn rw-btn-secondary question-preset" type="button" <button class="btn rw-btn-secondary question-preset" type="button"
data-question="CB_VECTOR_SEARCH_DOCUMENTS에서 이 토큰이 볼 수 있는 기술 태그 청크 수와 VPD가 제외한 범위를 요약해줘."> data-question="CB_VECTOR_SEARCH_DOCUMENTS에서 이 토큰이 볼 수 있는 기술 태그 검색 단위 수와 VPD가 제외한 범위를 요약해줘.">
기본 분석 기본 분석
</button> </button>
<button class="btn rw-btn-secondary question-preset" type="button" <button class="btn rw-btn-secondary question-preset" type="button"
@@ -68,7 +68,7 @@
민감 컬럼 점검 민감 컬럼 점검
</button> </button>
<button class="btn rw-btn-secondary question-preset" type="button" <button class="btn rw-btn-secondary question-preset" type="button"
data-question="반환된 벡터 청크의 기술 태그와 권한 규칙을 근거로 어떤 지식 범위를 조회할 수 있는지 판단해줘. 추정이면 추정이라고 표시해줘."> data-question="반환된 검색 단위의 기술 태그와 권한 규칙을 근거로 어떤 지식 범위를 조회할 수 있는지 판단해줘. 추정이면 추정이라고 표시해줘.">
태그 권한 범위 판단 태그 권한 범위 판단
</button> </button>
<button class="btn rw-btn-secondary question-preset" type="button" <button class="btn rw-btn-secondary question-preset" type="button"

View File

@@ -14,10 +14,10 @@
</div> </div>
<div class="alert alert-info"> <div class="alert alert-info">
<strong>벡터 검색 예제:</strong> 문서 청크와 기술 태그를 등록한 뒤 전용 ORDS 검색을 연결합니다. <strong>지식 검색 연결:</strong> 문서 검색 단위와 기술 태그를 등록한 뒤 전용 ORDS 검색을 연결합니다.
<details class="explanation-details mt-2"> <details class="explanation-details mt-2">
<summary>벡터 지식자료 검색 흐름 설명 보기</summary> <summary>지식 검색 연결 흐름 보기</summary>
<p>문서를 청크로 나누고 기술 태그를 붙인 뒤 전용 ORDS 검색을 등록합니다. 그 다음 <a href="/permissions">권한 관리</a>에서 <code>특정 기술 태그</code>를 여러 개 추가하면 태그 중 하나라도 맞는 청크만 검색됩니다.</p> <p>문서를 검색 단위로 나누고 기술 태그를 붙인 뒤 전용 ORDS 검색을 등록합니다. 그 다음 <a href="/permissions">권한 관리</a>에서 <code>특정 기술 태그</code>를 여러 개 추가하면 태그 중 하나라도 맞는 검색 단위만 검색됩니다.</p>
</details> </details>
</div> </div>
@@ -27,8 +27,8 @@
<div class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}"></div> <div class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}"></div>
<section class="content-band"> <section class="content-band">
<h2>조회 대상 추가 (예제 Handler)</h2> <h2>조회 대상 추가</h2>
<p class="section-subtitle">DB 객체 하나를 하나의 조회 Handler로 연결하는 시작 예제입니다.</p> <p class="section-subtitle">DB 객체 하나를 하나의 조회 Handler로 연결하는 기본 구성입니다.</p>
<details class="explanation-details"> <details class="explanation-details">
<summary>대상 추가 후 생성되는 내용 보기</summary> <summary>대상 추가 후 생성되는 내용 보기</summary>
<p>대상 추가 후 Handler를 생성하면 VPD context 설정과 기본 SELECT가 포함된 PL/SQL이 만들어집니다. 소스 보기에서 실제 업무에 맞게 수정할 수 있습니다.</p> <p>대상 추가 후 Handler를 생성하면 VPD context 설정과 기본 SELECT가 포함된 PL/SQL이 만들어집니다. 소스 보기에서 실제 업무에 맞게 수정할 수 있습니다.</p>
@@ -53,7 +53,7 @@
<input class="form-control" name="ordsPath" data-ords-path-input <input class="form-control" name="ordsPath" data-ords-path-input
placeholder="DB 객체 선택 시 자동 입력" readonly required> placeholder="DB 객체 선택 시 자동 입력" readonly required>
<span class="form-hint"> <span class="form-hint">
객체를 선택하면 예제용 module/template 경로가 자동으로 입력됩니다. 비워 제출해도 서버가 같은 규칙으로 채웁니다. 객체를 선택하면 기본 module/template 경로가 자동으로 입력됩니다. 비워 제출해도 서버가 같은 규칙으로 채웁니다.
<label class="form-check form-check-inline ms-2"> <label class="form-check form-check-inline ms-2">
<input class="form-check-input" type="checkbox" data-ords-path-edit> <input class="form-check-input" type="checkbox" data-ords-path-edit>
<span class="form-check-label">고급: 직접 수정</span> <span class="form-check-label">고급: 직접 수정</span>
@@ -68,7 +68,7 @@
<button class="btn btn-primary" type="submit">대상 추가</button> <button class="btn btn-primary" type="submit">대상 추가</button>
</form> </form>
<details class="explanation-details explanation-warning mt-3"> <details class="explanation-details explanation-warning mt-3">
<summary>기본 Handler 예제와 수정 가이드 보기</summary> <summary>기본 Handler 구성과 수정 가이드 보기</summary>
<p><code>cb_ords_handler_pkg.set_vpd_context(:auth_header)</code>로 토큰의 사용자·역할 컨텍스트를 넣고, 선택한 한 테이블에 <code>SELECT ... FROM OWNER.TABLE</code>을 실행한 뒤 JSON으로 반환합니다. 이것은 유일한 사용 방식이 아니라 시작점이며, Handler 소스와 ORDS 메타데이터에서 수정할 수 있습니다. 컬럼 민감도·마스킹은 이 화면에서 다루지 않고 <a href="/permissions">권한 관리의 원문 표시 허용 컬럼</a>에서 별도로 설정합니다.</p> <p><code>cb_ords_handler_pkg.set_vpd_context(:auth_header)</code>로 토큰의 사용자·역할 컨텍스트를 넣고, 선택한 한 테이블에 <code>SELECT ... FROM OWNER.TABLE</code>을 실행한 뒤 JSON으로 반환합니다. 이것은 유일한 사용 방식이 아니라 시작점이며, Handler 소스와 ORDS 메타데이터에서 수정할 수 있습니다. 컬럼 민감도·마스킹은 이 화면에서 다루지 않고 <a href="/permissions">권한 관리의 원문 표시 허용 컬럼</a>에서 별도로 설정합니다.</p>
</details> </details>
</section> </section>

View File

@@ -25,7 +25,7 @@
<p>토큰은 사용자를 찾는 열쇠입니다. 서버가 직접 역할과 그룹 상속 역할을 합치고, VPD가 저장된 행·열 규칙을 적용한 결과를 보여줍니다.</p> <p>토큰은 사용자를 찾는 열쇠입니다. 서버가 직접 역할과 그룹 상속 역할을 합치고, VPD가 저장된 행·열 규칙을 적용한 결과를 보여줍니다.</p>
</details> </details>
</div> </div>
<a class="btn rw-btn-secondary" href="/tokens">테스트 토큰이 없나요? 먼저 발급하기</a> <a class="btn rw-btn-secondary" href="/tokens">검증 세션이 없나요? 먼저 발급하기</a>
</section> </section>
<section class="content-band"> <section class="content-band">
@@ -41,15 +41,15 @@
1. 발급받은 토큰 원문 1. 발급받은 토큰 원문
<input class="form-control" name="bearerToken" type="password" autocomplete="off" <input class="form-control" name="bearerToken" type="password" autocomplete="off"
placeholder="토큰 발급 직후 복사한 값을 붙여 넣으세요"> placeholder="토큰 발급 직후 복사한 값을 붙여 넣으세요">
<span class="form-hint">원문은 DB에 저장되지 않습니다. 아래 임시 토큰 사용자를 선택하면 이 입력은 무시되고 실행 중 발급·완료 즉시 회수됩니다. 둘 다 비우면 invalid token 결과를 확인할 수 있습니다.</span> <span class="form-hint">원문은 DB에 저장되지 않습니다. 아래 검증 세션 사용자를 선택하면 이 입력은 무시되고 실행 중 발급·완료 즉시 폐기됩니다. 둘 다 비우면 invalid token 결과를 확인할 수 있습니다.</span>
</label> </label>
<label> <label>
임시 토큰 사용자 (선택) 검증 세션 사용자 (선택)
<select class="form-select" name="tempUserId"> <select class="form-select" name="tempUserId">
<option value="">원문 토큰 사용</option> <option value="">원문 토큰 사용</option>
<option th:each="user : ${users}" th:value="${user.userId()}" th:text="${user.username()}"></option> <option th:each="user : ${users}" th:value="${user.userId()}" th:text="${user.username()}"></option>
</select> </select>
<span class="form-hint">ORDS 호출에 사용할 10분 토큰을 만들고 테스트가 끝나면 바로 회수합니다.</span> <span class="form-hint">ORDS 호출에 사용할 10분 검증 세션을 만들고 완료 후 바로 폐기합니다.</span>
</label> </label>
<label> <label>
2. 확인할 데이터 2. 확인할 데이터

View File

@@ -1,12 +1,12 @@
<!doctype html> <!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"> <html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/layout :: head('테스트 토큰 발급')}"></head> <head th:replace="~{fragments/layout :: head('검증 세션 발급')}"></head>
<body> <body>
<nav th:replace="~{fragments/layout :: nav}"></nav> <nav th:replace="~{fragments/layout :: nav}"></nav>
<main class="container py-4"> <main class="container py-4">
<div class="page-title"> <div class="page-title">
<h1>테스트 토큰 발급</h1> <h1>검증 세션 발급</h1>
<p class="context-summary">검증할 사용자의 일회성 Bearer Token을 발급합니다.</p> <p class="context-summary">선택한 사용자의 접근 권한을 확인할 일회성 Bearer Token을 발급합니다.</p>
<details class="explanation-details"> <details class="explanation-details">
<summary>토큰 보관 방식 설명 보기</summary> <summary>토큰 보관 방식 설명 보기</summary>
<p>검증할 사용자를 나타내는 토큰을 만들고, 한 번만 표시되는 원문을 다음 단계에서 사용합니다. 원문은 저장하지 않고 해시와 식별용 prefix만 보관합니다.</p> <p>검증할 사용자를 나타내는 토큰을 만들고, 한 번만 표시되는 원문을 다음 단계에서 사용합니다. 원문은 저장하지 않고 해시와 식별용 prefix만 보관합니다.</p>
@@ -50,7 +50,7 @@
용도 메모 용도 메모
<input class="form-control" name="description" maxlength="200" placeholder="예: HR 권한 확인"> <input class="form-control" name="description" maxlength="200" placeholder="예: HR 권한 확인">
</label> </label>
<button class="btn rw-btn-primary" type="submit">2. 토큰 발급</button> <button class="btn rw-btn-primary" type="submit">2. 검증 세션 발급</button>
</form> </form>
</section> </section>

View File

@@ -1,15 +1,15 @@
<!doctype html> <!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"> <html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/layout :: head('벡터 지식자료 데모')}"></head> <head th:replace="~{fragments/layout :: head('지식 검색 관리')}"></head>
<body> <body>
<nav th:replace="~{fragments/layout :: nav}"></nav> <nav th:replace="~{fragments/layout :: nav}"></nav>
<main class="container py-4"> <main class="container py-4">
<div class="page-title"> <div class="page-title">
<h1>벡터 지식자료 데모</h1> <h1>지식 검색 관리</h1>
<p class="context-summary">문서를 청크로 나누고 임베딩·기술 태그를 저장한 뒤, 사용자 권한에 맞는 청크만 검색합니다.</p> <p class="context-summary">지식자료를 등록하고 접근 정책에 따라 검색 결과를 제공합니다.</p>
<details class="explanation-details"> <details class="explanation-details">
<summary>전체 시나리오 설명 보기</summary> <summary>검색 정책과 처리 방식</summary>
<p>문서 본문을 청크로 나누고 각 청크를 벡터로 변환합니다. 청크마다 기술 태그를 붙이면 권한 규칙<code>TAG</code> 조건이 같은 태그가 붙은 행만 허용합니다. 마지막 검색은 Bearer Token으로 VPD를 통과한 청크만 벡터 거리순으로 반환합니다.</p> <p>문서는 검색 단위로 나누고 각 단위를 임베딩으로 변환합니다. 기술 태그는 접근 정책<code>TAG</code> 조건으로 사용되며, 검색 시 DB 보호 정책이 허용된 지식자료만 남깁니다. <code>Bearer Token</code>은 검증 세션에만 사용되고 검색이 끝나면 폐기됩니다.</p>
</details> </details>
</div> </div>
@@ -25,19 +25,19 @@
<section class="content-band"> <section class="content-band">
<div class="section-heading"> <div class="section-heading">
<div> <div>
<span class="architecture-kicker">1 · INGEST</span> <span class="architecture-kicker">1 · CONTENT</span>
<h2>문서 등록 → 청킹 → 임베딩 → 태그 저장</h2> <h2>지식자료 등록</h2>
<p class="section-subtitle">같은 문서 ID로 다시 저장하면 기존 청크를 교체합니다.</p> <p class="section-subtitle">같은 문서 ID로 저장하면 최신 내용으로 갱신됩니다.</p>
</div> </div>
<span class="badge text-bg-secondary" th:text="${summary == null ? 'DB 확인 필요' : summary.chunkCount() + ' chunks'}">0 chunks</span> <span class="badge text-bg-secondary" th:text="${summary == null ? '저장소 확인 필요' : summary.chunkCount() + '개 검색 단위'}">0개 검색 단위</span>
</div> </div>
<details class="explanation-details"> <details class="explanation-details">
<summary>청킹과 임베딩 방식 보기</summary> <summary>분할·임베딩 설정 보기</summary>
<ul> <ul>
<li>문단을 우선 묶고 길이가 길면 문장·공백 기준으로 나눕니다.</li> <li>문단을 우선 묶고 길이가 길면 문장·공백 기준으로 나눕니다.</li>
<li><strong>DEMO-4D</strong> 외부 API 없이 동일한 4차원 예제를 재현하는 모드입니다. 의미 검색 품질을 제공하는 실제 임베딩은 아닙니다.</li> <li><strong>로컬 임베딩(개발용)</strong> 외부 API 없이 검색 흐름을 확인하는 4차원 처리 방식입니다. 운영 검색 품질을 위한 모델은 아닙니다.</li>
<li><strong>AI</strong><code>BACKOFFICE_AI_EMBEDDING_MODEL</code>과 API key가 설정된 OpenAI 호환 <code>/v1/embeddings</code>를 사용합니다.</li> <li><strong>AI</strong><code>BACKOFFICE_AI_EMBEDDING_MODEL</code>과 API key가 설정된 OpenAI 호환 <code>/v1/embeddings</code>를 사용합니다.</li>
<li>실제 운영에서는 문서 내용과 같은 임베딩 모델로 검색어도 임베딩해야 합니다.</li> <li>운영 환경에서는 자료 등록과 검색에 같은 임베딩 모델과 차원을 사용해야 합니다.</li>
</ul> </ul>
</details> </details>
<form method="post" action="/vector-knowledge/ingest" class="form-grid mt-3"> <form method="post" action="/vector-knowledge/ingest" class="form-grid mt-3">
@@ -60,13 +60,13 @@
<span class="form-hint">태그는 대문자로 정규화됩니다. 예: <code>SPRING_BOOT</code>, <code>ORACLE_VPD</code></span> <span class="form-hint">태그는 대문자로 정규화됩니다. 예: <code>SPRING_BOOT</code>, <code>ORACLE_VPD</code></span>
</label> </label>
<label> <label>
청크 길이 검색 단위 길이
<input class="form-control" name="chunkSize" type="number" min="80" max="2000" value="600"> <input class="form-control" name="chunkSize" type="number" min="80" max="2000" value="600">
</label> </label>
<label> <label>
임베딩 방식 임베딩 방식
<select class="form-select" name="embeddingMode"> <select class="form-select" name="embeddingMode">
<option value="DEMO">DEMO-4D (로컬 재현)</option> <option value="DEMO">로컬 임베딩(개발용)</option>
<option value="AI" th:disabled="${!aiEmbeddingConfigured}">AI 임베딩 (설정 필요)</option> <option value="AI" th:disabled="${!aiEmbeddingConfigured}">AI 임베딩 (설정 필요)</option>
</select> </select>
</label> </label>
@@ -75,37 +75,37 @@
<textarea class="form-control" name="content" rows="8" <textarea class="form-control" name="content" rows="8"
placeholder="문서 내용을 붙여 넣으세요. 빈 줄은 문단 경계로 사용됩니다." required></textarea> placeholder="문서 내용을 붙여 넣으세요. 빈 줄은 문단 경계로 사용됩니다." required></textarea>
</label> </label>
<button class="btn rw-btn-primary" type="submit">청크·임베딩·태그 저장</button> <button class="btn rw-btn-primary" type="submit">지식자료 저장</button>
</form> </form>
<div class="alert alert-info mt-3 mb-0" th:if="${ingestResult}"> <div class="alert alert-info mt-3 mb-0" th:if="${ingestResult}">
<strong th:text="${ingestResult.documentId()}">document</strong> <strong th:text="${ingestResult.documentId()}">document</strong>
<span th:text="${ingestResult.chunkCount() + '개 청크 / ' + ingestResult.tagCount() + '개 태그 연결 / ' + ingestResult.embeddingModel()}">저장 완료</span> <span th:text="${ingestResult.chunkCount() + '개 검색 단위 / ' + ingestResult.tagCount() + '개 태그 연결 / ' + ingestResult.embeddingModel()}">저장 완료</span>
</div> </div>
</section> </section>
<section class="content-band"> <section class="content-band">
<div class="section-heading"> <div class="section-heading">
<div> <div>
<span class="architecture-kicker">2 · AUTHORIZE</span> <span class="architecture-kicker">2 · ACCESS POLICY</span>
<h2>태그 권한 설정</h2> <h2>접근 정책 설정</h2>
<p class="section-subtitle">태그 권한은 기존 권한 관리에서 역할별 행 규칙으로 저장합니다.</p> <p class="section-subtitle">역할별 기술 태그 접근 규칙은 권한 관리에서 관리합니다.</p>
</div> </div>
<a class="btn btn-sm rw-btn-primary" href="/permissions">권한 관리 열기</a> <a class="btn btn-sm rw-btn-primary" href="/permissions">권한 관리 열기</a>
</div> </div>
<div class="macro-micro-grid"> <div class="macro-micro-grid">
<div> <div>
<h3>예: 백엔드 역할</h3> <h3>백엔드 지식 접근 정책</h3>
<p><code>ALLOW TAG SPRING_BOOT</code><br><code>ALLOW TAG ORACLE_VPD</code></p> <p><code>ALLOW TAG SPRING_BOOT</code><br><code>ALLOW TAG ORACLE_VPD</code></p>
<small class="text-muted">두 태그 중 하나가 붙은 청크를 허용합니다.</small> <small class="text-muted">두 태그 중 하나가 붙은 검색 단위를 허용합니다.</small>
</div> </div>
<div> <div>
<h3>예: ORDS 제외 역할</h3> <h3>ORDS 제외 정책</h3>
<p><code>ALLOW TAG SPRING_BOOT</code><br><code>DENY TAG ORDS</code></p> <p><code>ALLOW TAG SPRING_BOOT</code><br><code>DENY TAG ORDS</code></p>
<small class="text-muted">허용 후보 중 ORDS 태그 청크를 다시 제외합니다.</small> <small class="text-muted">허용 후보 중 ORDS 태그 검색 단위를 다시 제외합니다.</small>
</div> </div>
</div> </div>
<details class="explanation-details mt-3"> <details class="explanation-details mt-3">
<summary>VPD predicate가 계산하는 식 보기</summary> <summary>접근 정책이 적용되는 방식</summary>
<pre class="code-block">(ALLOW TAG A OR ALLOW TAG B) <pre class="code-block">(ALLOW TAG A OR ALLOW TAG B)
AND NOT (DENY TAG C OR DENY TAG D)</pre> AND NOT (DENY TAG C OR DENY TAG D)</pre>
<p>허용 태그가 없거나 토큰 컨텍스트가 없으면 <code>1=0</code>으로 닫힙니다. 태그 권한은 행 접근 기준이고, 본문·임베딩 표시 보호는 컬럼 정책으로 별도 관리합니다.</p> <p>허용 태그가 없거나 토큰 컨텍스트가 없으면 <code>1=0</code>으로 닫힙니다. 태그 권한은 행 접근 기준이고, 본문·임베딩 표시 보호는 컬럼 정책으로 별도 관리합니다.</p>
@@ -116,25 +116,25 @@ AND NOT (DENY TAG C OR DENY TAG D)</pre>
<div class="section-heading"> <div class="section-heading">
<div> <div>
<span class="architecture-kicker">3 · SEARCH</span> <span class="architecture-kicker">3 · SEARCH</span>
<h2>권한 적용 벡터 검색</h2> <h2>권한 기반 지식 검색</h2>
<p class="section-subtitle">선택한 사용자에게 임시 토큰을 발급하고 검색 완료 즉시 회수합니다.</p> <p class="section-subtitle">검색 권한 주체에 대한 검증용 세션을 사용하고 완료 후 폐기합니다.</p>
</div> </div>
<span class="badge" th:classappend="${summary != null && summary.vectorObjectRegistered()} ? ' text-bg-success' : ' text-bg-warning'" <span class="badge" th:classappend="${summary != null && summary.vectorObjectRegistered()} ? ' text-bg-success' : ' text-bg-warning'"
th:text="${summary != null && summary.vectorObjectRegistered()} ? 'Vector ORDS 연결됨' : 'Vector ORDS 필요'">Vector ORDS</span> th:text="${summary != null && summary.vectorObjectRegistered()} ? '검색 API 연결됨' : '검색 API 필요'">검색 API</span>
</div> </div>
<details class="explanation-details"> <details class="explanation-details">
<summary>검색 실행 순서 보기</summary> <summary>검색 처리 방식 보기</summary>
<ol> <ol>
<li>검색어를 같은 임베딩 방식으로 벡터화합니다.</li> <li>검색어를 같은 임베딩 방식으로 벡터화합니다.</li>
<li>임시 Bearer Token으로 전용 ORDS Handler를 호출합니다.</li> <li>임시 Bearer Token으로 전용 ORDS Handler를 호출합니다.</li>
<li>VPD가 TECH_TAG 권한에 맞지 않는 청크를 먼저 제거합니다.</li> <li>VPD가 TECH_TAG 권한에 맞지 않는 검색 단위를 먼저 제거합니다.</li>
<li>남은 청크를 벡터 거리순으로 반환합니다.</li> <li>남은 검색 단위를 관련도 순으로 반환합니다.</li>
</ol> </ol>
</details> </details>
<form hx-post="/vector-knowledge/search" hx-target="#vector-search-result" hx-swap="innerHTML" class="form-grid mt-3"> <form hx-post="/vector-knowledge/search" hx-target="#vector-search-result" hx-swap="innerHTML" class="form-grid mt-3">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<label> <label>
테스트 사용자 검색 권한 주체
<select class="form-select" name="userId" required> <select class="form-select" name="userId" required>
<option th:each="user : ${users}" th:value="${user.userId()}" th:text="${user.username() + ' / ' + user.deptCode()}"></option> <option th:each="user : ${users}" th:value="${user.userId()}" th:text="${user.username() + ' / ' + user.deptCode()}"></option>
</select> </select>
@@ -146,7 +146,7 @@ AND NOT (DENY TAG C OR DENY TAG D)</pre>
<label> <label>
임베딩 방식 임베딩 방식
<select class="form-select" name="embeddingMode"> <select class="form-select" name="embeddingMode">
<option value="DEMO">DEMO-4D (샘플과 동일)</option> <option value="DEMO">로컬 임베딩(개발용)</option>
<option value="AI" th:disabled="${!aiEmbeddingConfigured}">AI 임베딩</option> <option value="AI" th:disabled="${!aiEmbeddingConfigured}">AI 임베딩</option>
</select> </select>
</label> </label>
@@ -154,19 +154,19 @@ AND NOT (DENY TAG C OR DENY TAG D)</pre>
검색 질문 검색 질문
<textarea class="form-control" name="query" rows="3" placeholder="예: Oracle VPD에서 ORDS 권한을 적용하는 방법" required></textarea> <textarea class="form-control" name="query" rows="3" placeholder="예: Oracle VPD에서 ORDS 권한을 적용하는 방법" required></textarea>
</label> </label>
<button class="btn rw-btn-primary" type="submit">권한 적용 검색</button> <button class="btn rw-btn-primary" type="submit">검색 실행</button>
</form> </form>
<section id="vector-search-result" class="mt-3" aria-live="polite"> <section id="vector-search-result" class="mt-3" aria-live="polite">
<div class="empty-result-guide">검색하면 선택한 사용자에게 허용된 청크만 여기에 표시됩니다.</div> <div class="empty-result-guide">검색 권한에 따라 허용된 지식자료만 여기에 표시됩니다.</div>
</section> </section>
</section> </section>
<section class="content-band"> <section class="content-band">
<details class="explanation-details"> <details class="explanation-details">
<summary>현재 저장 현황 보기</summary> <summary>저장 현황 보기</summary>
<div class="summary-grid mt-3"> <div class="summary-grid mt-3">
<div class="summary-tile"><span class="label">문서</span><strong th:text="${summary == null ? '-' : summary.documentCount()}">0</strong></div> <div class="summary-tile"><span class="label">문서</span><strong th:text="${summary == null ? '-' : summary.documentCount()}">0</strong></div>
<div class="summary-tile"><span class="label">청크</span><strong th:text="${summary == null ? '-' : summary.chunkCount()}">0</strong></div> <div class="summary-tile"><span class="label">검색 단위</span><strong th:text="${summary == null ? '-' : summary.chunkCount()}">0</strong></div>
<div class="summary-tile"><span class="label">태그 연결</span><strong th:text="${summary == null ? '-' : summary.tagCount()}">0</strong></div> <div class="summary-tile"><span class="label">태그 연결</span><strong th:text="${summary == null ? '-' : summary.tagCount()}">0</strong></div>
</div> </div>
</details> </details>

View File

@@ -88,7 +88,7 @@
<label class="span-2"> <label class="span-2">
이 Filter가 보호하는 내용 이 Filter가 보호하는 내용
<input class="form-control" name="description" maxlength="500" <input class="form-control" name="description" maxlength="500"
placeholder="예: 지식자료 TECH_TAG가 권한에 포함된 청크만 허용" required> placeholder="예: 지식자료 TECH_TAG가 권한에 포함된 검색 단위만 허용" required>
</label> </label>
<div class="question-presets span-2" aria-label="Filter predicate 안전 예시"> <div class="question-presets span-2" aria-label="Filter predicate 안전 예시">
<button class="btn rw-btn-secondary question-preset" type="button" data-target="filter-only-predicate" data-question="1=0">안전한 기본 차단</button> <button class="btn rw-btn-secondary question-preset" type="button" data-target="filter-only-predicate" data-question="1=0">안전한 기본 차단</button>

View File

@@ -16,7 +16,7 @@ class GuidedFlowTemplateTest {
assertThat(html) assertThat(html)
.contains("1. 권한 설계") .contains("1. 권한 설계")
.contains("2. DB 보호 연결") .contains("2. DB 보호 연결")
.contains("3. 토큰 발급") .contains("3. 검증 세션")
.contains("4. 결과 확인"); .contains("4. 결과 확인");
} }
@@ -27,7 +27,7 @@ class GuidedFlowTemplateTest {
assertThat(probe) assertThat(probe)
.contains("name=\"bearerToken\"") .contains("name=\"bearerToken\"")
.contains("임시 토큰 사용자") .contains("검증 세션 사용자")
.doesNotContain("name=\"tokenKeyId\""); .doesNotContain("name=\"tokenKeyId\"");
assertThat(result) assertThat(result)
.contains("적용된 사용자와 권한") .contains("적용된 사용자와 권한")
@@ -58,7 +58,7 @@ class GuidedFlowTemplateTest {
assertThat(html) assertThat(html)
.contains("누가 어떤 행을 볼 수 있는지는 권한 규칙과 VPD에서 결정") .contains("누가 어떤 행을 볼 수 있는지는 권한 규칙과 VPD에서 결정")
.contains("벡터 지식자료 검색 흐름") .contains("지식 검색 연결 흐름")
.contains("VPD context 설정과 기본 SELECT") .contains("VPD context 설정과 기본 SELECT")
.contains("전용 벡터 Handler 사용") .contains("전용 벡터 Handler 사용")
.contains("원문 표시 허용 컬럼") .contains("원문 표시 허용 컬럼")
@@ -103,8 +103,8 @@ class GuidedFlowTemplateTest {
String client = template("mcp-client-demo.html"); String client = template("mcp-client-demo.html");
String sse = template("mcp-sse.html"); String sse = template("mcp-sse.html");
assertThat(chatbot).contains("임시 토큰 사용자").contains("태그 벡터 검색"); assertThat(chatbot).contains("검증 세션 사용자").contains("태그 벡터 검색");
assertThat(reasoning).contains("MCP tool을 고르고").contains("임시 토큰 사용자").contains("MCP Tool"); assertThat(reasoning).contains("MCP tool을 고르고").contains("검증 세션 사용자").contains("MCP Tool");
assertThat(client).contains("tool 선택과 호출은 reasoning 결과").doesNotContain("Context Path"); assertThat(client).contains("tool 선택과 호출은 reasoning 결과").doesNotContain("Context Path");
assertThat(sse).contains("Instruction / parameter mapping").contains("bearerToken"); assertThat(sse).contains("Instruction / parameter mapping").contains("bearerToken");
} }
@@ -115,10 +115,11 @@ class GuidedFlowTemplateTest {
String result = template("fragments/vector-search-result.html"); String result = template("fragments/vector-search-result.html");
assertThat(vector) assertThat(vector)
.contains("문서 등록 → 청킹 → 임베딩 → 태그 저장") .contains("지식자료 등록")
.contains("ALLOW TAG SPRING_BOOT") .contains("ALLOW TAG SPRING_BOOT")
.contains("DENY TAG ORDS") .contains("DENY TAG ORDS")
.contains("권한 적용 벡터 검색") .contains("권한 기반 지식 검색")
.contains("로컬 임베딩(개발용)")
.contains("/vector-knowledge/ingest") .contains("/vector-knowledge/ingest")
.contains("/vector-knowledge/search"); .contains("/vector-knowledge/search");
assertThat(result) assertThat(result)