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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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. 확인할 데이터

View File

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

View File

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

View File

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