refactor #565: productize knowledge search language
This commit is contained in:
@@ -103,7 +103,7 @@ public class McpSseService {
|
||||
if (isVectorTool(tool)) {
|
||||
ObjectNode embedding = objectMapper.createObjectNode();
|
||||
embedding.put("type", "array");
|
||||
embedding.put("description", "외부 임베딩 모델이 만든 검색 벡터. 이 예제 fixture는 4차원입니다.");
|
||||
embedding.put("description", "외부 임베딩 모델이 만든 검색 벡터. 개발 환경에서는 4차원 벡터를 사용합니다.");
|
||||
ObjectNode items = objectMapper.createObjectNode();
|
||||
items.put("type", "number");
|
||||
embedding.set("items", items);
|
||||
|
||||
@@ -64,7 +64,7 @@ public class OpenAiCompatibleClient {
|
||||
|
||||
public List<Double> embedding(String input) {
|
||||
if (!embeddingConfigured()) {
|
||||
throw new AppException("AI 임베딩 설정이 없습니다. BACKOFFICE_AI_EMBEDDING_MODEL을 설정하거나 데모 임베딩을 선택하세요.");
|
||||
throw new AppException("AI 임베딩 설정이 없습니다. BACKOFFICE_AI_EMBEDDING_MODEL을 설정하거나 로컬 임베딩(개발용)을 선택하세요.");
|
||||
}
|
||||
|
||||
BackofficeProperties.Ai ai = properties.ai();
|
||||
|
||||
@@ -138,7 +138,7 @@ public class ProtectedObjectService {
|
||||
defaultOrdsPath(normalizedOwner, normalizedObjectName),
|
||||
String.join(",", columns),
|
||||
"",
|
||||
"DB 객체를 한 개의 ORDS 조회 Handler로 노출하는 예제 대상"
|
||||
"DB 객체를 한 개의 ORDS 조회 Handler로 노출하는 기본 구성"
|
||||
);
|
||||
long objectId = mapper.nextObjectId();
|
||||
mapper.insertObject(objectId, command);
|
||||
|
||||
@@ -65,7 +65,7 @@ public class VectorKnowledgeService {
|
||||
chunks,
|
||||
tags,
|
||||
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을 먼저 실행하세요."));
|
||||
String mode = normalizeMode(embeddingMode);
|
||||
String requestBody = "{\"embedding\":" + json(embed(normalizedQuery, mode)) + "}";
|
||||
IssuedToken temporary = tokenService.issueTemporaryToken(userId, "벡터 지식자료 검색 임시 실행");
|
||||
IssuedToken temporary = tokenService.issueTemporaryToken(userId, "지식 검색 검증 세션");
|
||||
try {
|
||||
ProbeResult probe = ordsProbeService.runProbe(new ProbeCommand(
|
||||
temporary.keyId(), vectorObject.objectId(), temporary.plainToken(), normalizeLimit(limit), requestBody));
|
||||
@@ -223,16 +223,16 @@ public class VectorKnowledgeService {
|
||||
private String normalizeMode(String mode) {
|
||||
String normalized = mode == null ? DEMO_MODE : mode.trim().toUpperCase(Locale.ROOT);
|
||||
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()) {
|
||||
throw new AppException("AI 임베딩이 설정되지 않았습니다. 설정에서 embedding model/API key를 추가하거나 DEMO 임베딩을 선택하세요.");
|
||||
throw new AppException("AI 임베딩이 설정되지 않았습니다. 설정에서 embedding model/API key를 추가하거나 로컬 임베딩(개발용)을 선택하세요.");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<header class="page-title guided-hero">
|
||||
<span class="architecture-kicker">권한을 정하면 DB가 그대로 지킵니다</span>
|
||||
<h1>누가 어떤 데이터를 볼 수 있는지 설계하고, 실제 결과까지 확인하세요.</h1>
|
||||
<p class="context-summary">권한 설계 → DB 보호 연결 → 토큰 발급 → 실제 결과 확인</p>
|
||||
<p class="context-summary">권한 설계 → DB 보호 연결 → 검증 세션 → 실제 결과 확인</p>
|
||||
<details class="explanation-details">
|
||||
<summary>전체 흐름 설명 보기</summary>
|
||||
<p>사용자·그룹·역할에 권한을 연결하면 Oracle VPD가 요청할 때마다 그 규칙을 읽어 허용된 행만 반환합니다. 별도 SQL 필터를 만드는 일은 예외적인 고급 작업입니다.</p>
|
||||
@@ -40,9 +40,9 @@
|
||||
<a class="journey-card" href="/tokens">
|
||||
<span class="journey-number">3</span>
|
||||
<div>
|
||||
<h2>3. 토큰 발급</h2>
|
||||
<h2>3. 검증 세션</h2>
|
||||
<p>검증할 사용자를 선택해 토큰을 발급하고, 한 번만 보이는 원문을 복사합니다.</p>
|
||||
<strong>테스트 토큰 발급하기 →</strong>
|
||||
<strong>검증 세션 발급하기 →</strong>
|
||||
</div>
|
||||
</a>
|
||||
<a class="journey-card" href="/probe">
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<button class="rw-menu-trigger" type="button" aria-expanded="false">2. 보호·검증</button>
|
||||
<div class="rw-menu-panel">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,11 +38,11 @@
|
||||
<div class="rw-menu-panel">
|
||||
<a class="nav-link" href="/objects">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-reasoning">Reasoning</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 class="rw-menu-group">
|
||||
@@ -76,8 +76,8 @@
|
||||
<div class="architecture-arrow">→</div>
|
||||
<div class="architecture-step" th:classappend="${activeLayer == 'token'} ? ' active'">
|
||||
<span class="architecture-kicker">3 · IDENTITY</span>
|
||||
<strong>토큰 발급</strong>
|
||||
<p>검증할 사용자를 나타내는 일회성 원문 토큰을 준비합니다.</p>
|
||||
<strong>검증 세션</strong>
|
||||
<p>확인할 사용자를 나타내는 일회성 토큰을 준비합니다.</p>
|
||||
</div>
|
||||
<div class="architecture-arrow">→</div>
|
||||
<div class="architecture-step" th:classappend="${activeLayer == 'ords'} ? ' active'">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<span th:text="${object.owner() + '.' + object.objectName()}">ADMIN.TABLE_NAME</span>
|
||||
<span class="text-muted"> / 기본 ORDS Handler PL/SQL</span>
|
||||
</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>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}"></div>
|
||||
<div th:if="${searchResult}">
|
||||
<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>
|
||||
<div class="next-action-card mt-3">
|
||||
@@ -14,11 +14,11 @@
|
||||
<table class="table table-sm align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Chunk</th>
|
||||
<th>Document</th>
|
||||
<th>Title</th>
|
||||
<th>TECH_TAG</th>
|
||||
<th>Score</th>
|
||||
<th>검색 단위 ID</th>
|
||||
<th>자료 ID</th>
|
||||
<th>제목</th>
|
||||
<th>기술 태그</th>
|
||||
<th>관련도</th>
|
||||
<th>본문</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
<section class="content-band chat-shell">
|
||||
<div class="chat-message assistant-message">
|
||||
<strong>질문 실행</strong>
|
||||
<p>예시를 선택하거나 질문을 직접 입력하세요.</p>
|
||||
<p>추천 질문을 선택하거나 질문을 직접 입력하세요.</p>
|
||||
<details class="explanation-details">
|
||||
<summary>토큰과 조회 결과 설명 보기</summary>
|
||||
<p>Bearer Token 원문을 붙여 넣거나 사용자를 선택해 10분 임시 토큰으로 실제 ORDS/VPD 결과를 조회할 수 있습니다.</p>
|
||||
<summary>세션과 조회 결과 설명 보기</summary>
|
||||
<p>Bearer Token 원문을 붙여 넣거나 사용자를 선택해 10분 검증 세션으로 실제 ORDS/VPD 결과를 조회할 수 있습니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -47,12 +47,12 @@
|
||||
placeholder="비우면 라우팅만 확인합니다.">
|
||||
</label>
|
||||
<label>
|
||||
임시 토큰 사용자 (선택)
|
||||
검증 세션 사용자 (선택)
|
||||
<select class="form-select" name="tempUserId">
|
||||
<option value="">원문 토큰 사용</option>
|
||||
<option th:each="user : ${users}" th:value="${user.userId()}" th:text="${user.username()}"></option>
|
||||
</select>
|
||||
<span class="form-hint">선택하면 질문 처리 중에만 임시 토큰을 발행하고 완료 후 즉시 회수합니다.</span>
|
||||
<span class="form-hint">선택하면 질문 처리 중에만 검증 세션을 발급하고 완료 후 즉시 폐기합니다.</span>
|
||||
</label>
|
||||
<label>
|
||||
질문
|
||||
@@ -69,7 +69,7 @@
|
||||
민감 컬럼 확인
|
||||
</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 class="btn rw-btn-secondary question-preset" type="button"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<!doctype html>
|
||||
<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>
|
||||
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
||||
<main class="container page-shell">
|
||||
<section class="page-heading">
|
||||
<h1>MCP Client Demo</h1>
|
||||
<p class="context-summary">Java client가 MCP 연결과 도구 목록 계약만 확인합니다.</p>
|
||||
<h1>MCP Client</h1>
|
||||
<p class="context-summary">Java client가 MCP 연결과 사용 가능한 도구 계약을 확인합니다.</p>
|
||||
<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>
|
||||
</details>
|
||||
</section>
|
||||
@@ -23,7 +23,7 @@
|
||||
<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}">
|
||||
<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>
|
||||
</details>
|
||||
<button class="btn rw-btn-primary" type="submit">MCP 계약 확인</button>
|
||||
@@ -55,7 +55,7 @@
|
||||
</section>
|
||||
|
||||
<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>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
@@ -31,22 +31,22 @@
|
||||
<label>
|
||||
Bearer Token 원문
|
||||
<input class="form-control" name="bearerToken" type="password" autocomplete="off">
|
||||
<span class="form-hint">원문을 붙여 넣거나 임시 토큰 사용자를 선택하세요.</span>
|
||||
<span class="form-hint">원문을 붙여 넣거나 검증 세션 사용자를 선택하세요.</span>
|
||||
<details class="explanation-details">
|
||||
<summary>토큰 처리 방식 보기</summary>
|
||||
<p>등록 토큰을 목록에서 선택하지 않습니다. 원문은 저장하지 않고 요청에만 사용합니다.</p>
|
||||
</details>
|
||||
</label>
|
||||
<label>
|
||||
임시 토큰 사용자 (선택)
|
||||
검증 세션 사용자 (선택)
|
||||
<select class="form-select" name="tempUserId">
|
||||
<option value="">원문 토큰 사용</option>
|
||||
<option th:each="user : ${users}" th:value="${user.userId()}" th:text="${user.username()}"></option>
|
||||
</select>
|
||||
<span class="form-hint">실행 중 발행하고 완료 즉시 회수합니다.</span>
|
||||
<span class="form-hint">실행 중 발급하고 완료 즉시 폐기합니다.</span>
|
||||
<details class="explanation-details">
|
||||
<summary>임시 토큰 상세 보기</summary>
|
||||
<p>선택하면 reasoning 실행 중 10분 토큰을 발행하고 완료 즉시 회수합니다.</p>
|
||||
<summary>검증 세션 상세 보기</summary>
|
||||
<p>선택하면 reasoning 실행 중 10분 세션을 발급하고 완료 즉시 폐기합니다.</p>
|
||||
</details>
|
||||
</label>
|
||||
<label>
|
||||
@@ -60,7 +60,7 @@
|
||||
</label>
|
||||
<div class="question-presets span-2" aria-label="질문 예시">
|
||||
<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 class="btn rw-btn-secondary question-preset" type="button"
|
||||
@@ -68,7 +68,7 @@
|
||||
민감 컬럼 점검
|
||||
</button>
|
||||
<button class="btn rw-btn-secondary question-preset" type="button"
|
||||
data-question="반환된 벡터 청크의 기술 태그와 권한 규칙을 근거로 어떤 지식 범위를 조회할 수 있는지 판단해줘. 추정이면 추정이라고 표시해줘.">
|
||||
data-question="반환된 검색 단위의 기술 태그와 권한 규칙을 근거로 어떤 지식 범위를 조회할 수 있는지 판단해줘. 추정이면 추정이라고 표시해줘.">
|
||||
태그 권한 범위 판단
|
||||
</button>
|
||||
<button class="btn rw-btn-secondary question-preset" type="button"
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>벡터 검색 예제:</strong> 문서 청크와 기술 태그를 등록한 뒤 전용 ORDS 검색을 연결합니다.
|
||||
<strong>지식 검색 연결:</strong> 문서 검색 단위와 기술 태그를 등록한 뒤 전용 ORDS 검색을 연결합니다.
|
||||
<details class="explanation-details mt-2">
|
||||
<summary>벡터 지식자료 검색 흐름 설명 보기</summary>
|
||||
<p>문서를 청크로 나누고 기술 태그를 붙인 뒤 전용 ORDS 검색을 등록합니다. 그 다음 <a href="/permissions">권한 관리</a>에서 <code>특정 기술 태그</code>를 여러 개 추가하면 태그 중 하나라도 맞는 청크만 검색됩니다.</p>
|
||||
<summary>지식 검색 연결 흐름 보기</summary>
|
||||
<p>문서를 검색 단위로 나누고 기술 태그를 붙인 뒤 전용 ORDS 검색을 등록합니다. 그 다음 <a href="/permissions">권한 관리</a>에서 <code>특정 기술 태그</code>를 여러 개 추가하면 태그 중 하나라도 맞는 검색 단위만 검색됩니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
<div class="alert alert-danger" th:if="${errorMessage}" th:text="${errorMessage}"></div>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>조회 대상 추가 (예제 Handler)</h2>
|
||||
<p class="section-subtitle">DB 객체 하나를 하나의 조회 Handler로 연결하는 시작 예제입니다.</p>
|
||||
<h2>조회 대상 추가</h2>
|
||||
<p class="section-subtitle">DB 객체 하나를 하나의 조회 Handler로 연결하는 기본 구성입니다.</p>
|
||||
<details class="explanation-details">
|
||||
<summary>대상 추가 후 생성되는 내용 보기</summary>
|
||||
<p>대상 추가 후 Handler를 생성하면 VPD context 설정과 기본 SELECT가 포함된 PL/SQL이 만들어집니다. 소스 보기에서 실제 업무에 맞게 수정할 수 있습니다.</p>
|
||||
@@ -53,7 +53,7 @@
|
||||
<input class="form-control" name="ordsPath" data-ords-path-input
|
||||
placeholder="DB 객체 선택 시 자동 입력" readonly required>
|
||||
<span class="form-hint">
|
||||
객체를 선택하면 예제용 module/template 경로가 자동으로 입력됩니다. 비워 제출해도 서버가 같은 규칙으로 채웁니다.
|
||||
객체를 선택하면 기본 module/template 경로가 자동으로 입력됩니다. 비워 제출해도 서버가 같은 규칙으로 채웁니다.
|
||||
<label class="form-check form-check-inline ms-2">
|
||||
<input class="form-check-input" type="checkbox" data-ords-path-edit>
|
||||
<span class="form-check-label">고급: 직접 수정</span>
|
||||
@@ -68,7 +68,7 @@
|
||||
<button class="btn btn-primary" type="submit">대상 추가</button>
|
||||
</form>
|
||||
<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>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<p>토큰은 사용자를 찾는 열쇠입니다. 서버가 직접 역할과 그룹 상속 역할을 합치고, VPD가 저장된 행·열 규칙을 적용한 결과를 보여줍니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
<a class="btn rw-btn-secondary" href="/tokens">테스트 토큰이 없나요? 먼저 발급하기</a>
|
||||
<a class="btn rw-btn-secondary" href="/tokens">검증 세션이 없나요? 먼저 발급하기</a>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
@@ -41,15 +41,15 @@
|
||||
1. 발급받은 토큰 원문
|
||||
<input class="form-control" name="bearerToken" type="password" autocomplete="off"
|
||||
placeholder="토큰 발급 직후 복사한 값을 붙여 넣으세요">
|
||||
<span class="form-hint">원문은 DB에 저장되지 않습니다. 아래 임시 토큰 사용자를 선택하면 이 입력은 무시되고 실행 중 발급·완료 즉시 회수됩니다. 둘 다 비우면 invalid token 결과를 확인할 수 있습니다.</span>
|
||||
<span class="form-hint">원문은 DB에 저장되지 않습니다. 아래 검증 세션 사용자를 선택하면 이 입력은 무시되고 실행 중 발급·완료 즉시 폐기됩니다. 둘 다 비우면 invalid token 결과를 확인할 수 있습니다.</span>
|
||||
</label>
|
||||
<label>
|
||||
임시 토큰 사용자 (선택)
|
||||
검증 세션 사용자 (선택)
|
||||
<select class="form-select" name="tempUserId">
|
||||
<option value="">원문 토큰 사용</option>
|
||||
<option th:each="user : ${users}" th:value="${user.userId()}" th:text="${user.username()}"></option>
|
||||
</select>
|
||||
<span class="form-hint">ORDS 호출에만 사용할 10분 토큰을 만들고 테스트가 끝나면 바로 회수합니다.</span>
|
||||
<span class="form-hint">ORDS 호출에 사용할 10분 검증 세션을 만들고 완료 후 바로 폐기합니다.</span>
|
||||
</label>
|
||||
<label>
|
||||
2. 확인할 데이터
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:replace="~{fragments/layout :: head('테스트 토큰 발급')}"></head>
|
||||
<head th:replace="~{fragments/layout :: head('검증 세션 발급')}"></head>
|
||||
<body>
|
||||
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
||||
<main class="container py-4">
|
||||
<div class="page-title">
|
||||
<h1>테스트 토큰 발급</h1>
|
||||
<p class="context-summary">검증할 사용자의 일회성 Bearer Token을 발급합니다.</p>
|
||||
<h1>검증 세션 발급</h1>
|
||||
<p class="context-summary">선택한 사용자의 접근 권한을 확인할 일회성 Bearer Token을 발급합니다.</p>
|
||||
<details class="explanation-details">
|
||||
<summary>토큰 보관 방식 설명 보기</summary>
|
||||
<p>검증할 사용자를 나타내는 토큰을 만들고, 한 번만 표시되는 원문을 다음 단계에서 사용합니다. 원문은 저장하지 않고 해시와 식별용 prefix만 보관합니다.</p>
|
||||
@@ -50,7 +50,7 @@
|
||||
용도 메모
|
||||
<input class="form-control" name="description" maxlength="200" placeholder="예: HR 권한 확인">
|
||||
</label>
|
||||
<button class="btn rw-btn-primary" type="submit">2. 토큰 발급</button>
|
||||
<button class="btn rw-btn-primary" type="submit">2. 검증 세션 발급</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:replace="~{fragments/layout :: head('벡터 지식자료 데모')}"></head>
|
||||
<head th:replace="~{fragments/layout :: head('지식 검색 관리')}"></head>
|
||||
<body>
|
||||
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
||||
<main class="container py-4">
|
||||
<div class="page-title">
|
||||
<h1>벡터 지식자료 데모</h1>
|
||||
<p class="context-summary">문서를 청크로 나누고 임베딩·기술 태그를 저장한 뒤, 사용자 권한에 맞는 청크만 검색합니다.</p>
|
||||
<h1>지식 검색 관리</h1>
|
||||
<p class="context-summary">지식자료를 등록하고 접근 정책에 따라 검색 결과를 제공합니다.</p>
|
||||
<details class="explanation-details">
|
||||
<summary>전체 시나리오 설명 보기</summary>
|
||||
<p>문서 본문을 청크로 나누고 각 청크를 벡터로 변환합니다. 청크마다 기술 태그를 붙이면 권한 규칙의 <code>TAG</code> 조건이 같은 태그가 붙은 행만 허용합니다. 마지막 검색은 Bearer Token으로 VPD를 통과한 청크만 벡터 거리순으로 반환합니다.</p>
|
||||
<summary>검색 정책과 처리 방식</summary>
|
||||
<p>문서는 검색 단위로 나누고 각 단위를 임베딩으로 변환합니다. 기술 태그는 접근 정책의 <code>TAG</code> 조건으로 사용되며, 검색 시 DB 보호 정책이 허용된 지식자료만 남깁니다. <code>Bearer Token</code>은 검증 세션에만 사용되고 검색이 끝나면 폐기됩니다.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -25,19 +25,19 @@
|
||||
<section class="content-band">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<span class="architecture-kicker">1 · INGEST</span>
|
||||
<h2>문서 등록 → 청킹 → 임베딩 → 태그 저장</h2>
|
||||
<p class="section-subtitle">같은 문서 ID로 다시 저장하면 기존 청크를 교체합니다.</p>
|
||||
<span class="architecture-kicker">1 · CONTENT</span>
|
||||
<h2>지식자료 등록</h2>
|
||||
<p class="section-subtitle">같은 문서 ID로 저장하면 최신 내용으로 갱신됩니다.</p>
|
||||
</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>
|
||||
<details class="explanation-details">
|
||||
<summary>청킹과 임베딩 방식 보기</summary>
|
||||
<summary>분할·임베딩 설정 보기</summary>
|
||||
<ul>
|
||||
<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>실제 운영에서는 문서 내용과 같은 임베딩 모델로 검색어도 임베딩해야 합니다.</li>
|
||||
<li>운영 환경에서는 자료 등록과 검색에 같은 임베딩 모델과 차원을 사용해야 합니다.</li>
|
||||
</ul>
|
||||
</details>
|
||||
<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>
|
||||
</label>
|
||||
<label>
|
||||
청크 길이
|
||||
검색 단위 길이
|
||||
<input class="form-control" name="chunkSize" type="number" min="80" max="2000" value="600">
|
||||
</label>
|
||||
<label>
|
||||
임베딩 방식
|
||||
<select class="form-select" name="embeddingMode">
|
||||
<option value="DEMO">DEMO-4D (로컬 재현)</option>
|
||||
<option value="DEMO">로컬 임베딩(개발용)</option>
|
||||
<option value="AI" th:disabled="${!aiEmbeddingConfigured}">AI 임베딩 (설정 필요)</option>
|
||||
</select>
|
||||
</label>
|
||||
@@ -75,37 +75,37 @@
|
||||
<textarea class="form-control" name="content" rows="8"
|
||||
placeholder="문서 내용을 붙여 넣으세요. 빈 줄은 문단 경계로 사용됩니다." required></textarea>
|
||||
</label>
|
||||
<button class="btn rw-btn-primary" type="submit">청크·임베딩·태그 저장</button>
|
||||
<button class="btn rw-btn-primary" type="submit">지식자료 저장</button>
|
||||
</form>
|
||||
<div class="alert alert-info mt-3 mb-0" th:if="${ingestResult}">
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<span class="architecture-kicker">2 · AUTHORIZE</span>
|
||||
<h2>태그 권한 설정</h2>
|
||||
<p class="section-subtitle">태그 권한은 기존 권한 관리에서 역할별 행 규칙으로 저장합니다.</p>
|
||||
<span class="architecture-kicker">2 · ACCESS POLICY</span>
|
||||
<h2>접근 정책 설정</h2>
|
||||
<p class="section-subtitle">역할별 기술 태그 접근 규칙은 권한 관리에서 관리합니다.</p>
|
||||
</div>
|
||||
<a class="btn btn-sm rw-btn-primary" href="/permissions">권한 관리 열기</a>
|
||||
</div>
|
||||
<div class="macro-micro-grid">
|
||||
<div>
|
||||
<h3>예: 백엔드 역할</h3>
|
||||
<h3>백엔드 지식 접근 정책</h3>
|
||||
<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>
|
||||
<h3>예: ORDS 제외 역할</h3>
|
||||
<h3>ORDS 제외 정책</h3>
|
||||
<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>
|
||||
<details class="explanation-details mt-3">
|
||||
<summary>VPD predicate가 계산하는 식 보기</summary>
|
||||
<summary>접근 정책이 적용되는 방식</summary>
|
||||
<pre class="code-block">(ALLOW TAG A OR ALLOW TAG B)
|
||||
AND NOT (DENY TAG C OR DENY TAG D)</pre>
|
||||
<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>
|
||||
<span class="architecture-kicker">3 · SEARCH</span>
|
||||
<h2>권한 적용 벡터 검색</h2>
|
||||
<p class="section-subtitle">선택한 사용자에게 임시 토큰을 발급하고 검색 완료 즉시 회수합니다.</p>
|
||||
<h2>권한 기반 지식 검색</h2>
|
||||
<p class="section-subtitle">검색 권한 주체에 대한 검증용 세션을 사용하고 완료 후 폐기합니다.</p>
|
||||
</div>
|
||||
<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>
|
||||
<details class="explanation-details">
|
||||
<summary>검색 실행 순서 보기</summary>
|
||||
<summary>검색 처리 방식 보기</summary>
|
||||
<ol>
|
||||
<li>검색어를 같은 임베딩 방식으로 벡터화합니다.</li>
|
||||
<li>임시 Bearer Token으로 전용 ORDS Handler를 호출합니다.</li>
|
||||
<li>VPD가 TECH_TAG 권한에 맞지 않는 청크를 먼저 제거합니다.</li>
|
||||
<li>남은 청크를 벡터 거리순으로 반환합니다.</li>
|
||||
<li>VPD가 TECH_TAG 권한에 맞지 않는 검색 단위를 먼저 제거합니다.</li>
|
||||
<li>남은 검색 단위를 관련도 순으로 반환합니다.</li>
|
||||
</ol>
|
||||
</details>
|
||||
<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}">
|
||||
<label>
|
||||
테스트 사용자
|
||||
검색 권한 주체
|
||||
<select class="form-select" name="userId" required>
|
||||
<option th:each="user : ${users}" th:value="${user.userId()}" th:text="${user.username() + ' / ' + user.deptCode()}"></option>
|
||||
</select>
|
||||
@@ -146,7 +146,7 @@ AND NOT (DENY TAG C OR DENY TAG D)</pre>
|
||||
<label>
|
||||
임베딩 방식
|
||||
<select class="form-select" name="embeddingMode">
|
||||
<option value="DEMO">DEMO-4D (샘플과 동일)</option>
|
||||
<option value="DEMO">로컬 임베딩(개발용)</option>
|
||||
<option value="AI" th:disabled="${!aiEmbeddingConfigured}">AI 임베딩</option>
|
||||
</select>
|
||||
</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>
|
||||
</label>
|
||||
<button class="btn rw-btn-primary" type="submit">권한 적용 검색</button>
|
||||
<button class="btn rw-btn-primary" type="submit">검색 실행</button>
|
||||
</form>
|
||||
<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 class="content-band">
|
||||
<details class="explanation-details">
|
||||
<summary>현재 저장 현황 보기</summary>
|
||||
<summary>저장 현황 보기</summary>
|
||||
<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.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>
|
||||
</details>
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
<label class="span-2">
|
||||
이 Filter가 보호하는 내용
|
||||
<input class="form-control" name="description" maxlength="500"
|
||||
placeholder="예: 지식자료 TECH_TAG가 권한에 포함된 청크만 허용" required>
|
||||
placeholder="예: 지식자료 TECH_TAG가 권한에 포함된 검색 단위만 허용" required>
|
||||
</label>
|
||||
<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>
|
||||
|
||||
@@ -16,7 +16,7 @@ class GuidedFlowTemplateTest {
|
||||
assertThat(html)
|
||||
.contains("1. 권한 설계")
|
||||
.contains("2. DB 보호 연결")
|
||||
.contains("3. 토큰 발급")
|
||||
.contains("3. 검증 세션")
|
||||
.contains("4. 결과 확인");
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class GuidedFlowTemplateTest {
|
||||
|
||||
assertThat(probe)
|
||||
.contains("name=\"bearerToken\"")
|
||||
.contains("임시 토큰 사용자")
|
||||
.contains("검증 세션 사용자")
|
||||
.doesNotContain("name=\"tokenKeyId\"");
|
||||
assertThat(result)
|
||||
.contains("적용된 사용자와 권한")
|
||||
@@ -58,7 +58,7 @@ class GuidedFlowTemplateTest {
|
||||
|
||||
assertThat(html)
|
||||
.contains("누가 어떤 행을 볼 수 있는지는 권한 규칙과 VPD에서 결정")
|
||||
.contains("벡터 지식자료 검색 흐름")
|
||||
.contains("지식 검색 연결 흐름")
|
||||
.contains("VPD context 설정과 기본 SELECT")
|
||||
.contains("전용 벡터 Handler 사용")
|
||||
.contains("원문 표시 허용 컬럼")
|
||||
@@ -103,8 +103,8 @@ class GuidedFlowTemplateTest {
|
||||
String client = template("mcp-client-demo.html");
|
||||
String sse = template("mcp-sse.html");
|
||||
|
||||
assertThat(chatbot).contains("임시 토큰 사용자").contains("태그 벡터 검색");
|
||||
assertThat(reasoning).contains("MCP tool을 고르고").contains("임시 토큰 사용자").contains("MCP Tool");
|
||||
assertThat(chatbot).contains("검증 세션 사용자").contains("태그 벡터 검색");
|
||||
assertThat(reasoning).contains("MCP tool을 고르고").contains("검증 세션 사용자").contains("MCP Tool");
|
||||
assertThat(client).contains("tool 선택과 호출은 reasoning 결과").doesNotContain("Context Path");
|
||||
assertThat(sse).contains("Instruction / parameter mapping").contains("bearerToken");
|
||||
}
|
||||
@@ -115,10 +115,11 @@ class GuidedFlowTemplateTest {
|
||||
String result = template("fragments/vector-search-result.html");
|
||||
|
||||
assertThat(vector)
|
||||
.contains("문서 등록 → 청킹 → 임베딩 → 태그 저장")
|
||||
.contains("지식자료 등록")
|
||||
.contains("ALLOW TAG SPRING_BOOT")
|
||||
.contains("DENY TAG ORDS")
|
||||
.contains("권한 적용 벡터 검색")
|
||||
.contains("권한 기반 지식 검색")
|
||||
.contains("로컬 임베딩(개발용)")
|
||||
.contains("/vector-knowledge/ingest")
|
||||
.contains("/vector-knowledge/search");
|
||||
assertThat(result)
|
||||
|
||||
Reference in New Issue
Block a user