ui: simplify DDS knowledge registration

This commit is contained in:
devmrko
2026-07-01 09:43:52 +09:00
parent 7067941ddd
commit 8830f07be1
4 changed files with 131 additions and 103 deletions

View File

@@ -7,7 +7,7 @@
<div class="page-title">
<span class="architecture-kicker">DDS · KNOWLEDGE ACCESS</span>
<h1>지식자료 관리</h1>
<p class="context-summary">등록 · 접근 규칙 · 업무 사용자 검색 · 고급 검증</p>
<p class="context-summary">자료 등록 · 권한 검색</p>
<details class="explanation-details">
<summary>도움말</summary>
<p>문서는 검색 단위로 나뉘고 각 단위에 접근 분류값(<code>TECH_TAG</code>)이 연결됩니다. 역할별 허용 값을 등록하면 보호 규칙이 허용된 분류값만 검색 대상으로 남깁니다.</p>
@@ -26,87 +26,89 @@
<section class="content-band">
<div class="section-heading">
<div>
<span class="architecture-kicker">지식자료</span>
<h2>지식자료 등록</h2>
<p class="section-subtitle">문서 ID가 같으면 기존 검색 단위를 교체합니다.</p>
<h2>자료 등록</h2>
<p class="section-subtitle">제목, 원문, 접근 태그를 입력합니다.</p>
</div>
<span class="badge text-bg-secondary" th:text="${summary == null ? '저장소 확인 필요' : summary.chunkCount() + '개 검색 단위'}">0개 검색 단위</span>
</div>
<details class="explanation-details">
<summary>도움말</summary>
<ul>
<li>문단을 우선 묶고 길이가 길면 문장·공백 기준으로 청크를 나눕니다.</li>
<li>기본 로컬 임베딩은 외부 API 없이 흐름을 확인하는 개발용 4차원 벡터입니다.</li>
<li>운영에서는 등록과 검색에 같은 모델·차원을 사용해야 합니다.</li>
</ul>
<p class="mb-0">접근 태그는 이 자료를 볼 수 있는 권한 범위입니다. 권한 관리에 등록된 TAG 값과 일치하는 태그를 입력하세요. 등록 후 문서는 검색 단위로 나뉘고 임베딩이 생성됩니다.</p>
</details>
<form method="post" action="/vector-knowledge/ingest" class="form-grid mt-3">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<label>문서 ID<input class="form-control" name="documentId" placeholder="knowledge-security-001" required></label>
<label>문서 제목<input class="form-control" name="title" placeholder="DDS 기술 태그 권한 설계" required></label>
<label>원문 주소 (선택)<input class="form-control" name="sourceUri" placeholder="kb://security/dds-vector"></label>
<label>지식 접근 분류값 (쉼표 또는 공백)
<input class="form-control" name="techTags" placeholder="SALES INTERNAL" required>
<span class="form-hint">예: SALES. 값은 대문자로 정규화되고 권한 규칙의 TAG 값과 일치해야 합니다.</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">로컬 임베딩(개발용)</option>
<option value="AI" th:disabled="${!aiEmbeddingConfigured}">AI 임베딩 (설정 필요)</option>
</select>
<label>자료 제목<input class="form-control" name="title" placeholder="예: DDS 기술 태그 권한 설계" required></label>
<label>원문 주소 <span class="text-muted fw-normal">(선택)</span><input class="form-control" name="sourceUri" placeholder="예: https://kb.example.com/dds-vector"></label>
<label class="span-2">접근 태그
<input class="form-control" name="techTags" placeholder="예: SALES, INTERNAL" required>
<span class="form-hint">태그가 하나라도 허용되면 자료를 검색할 수 있습니다.</span>
</label>
<label class="span-2">문서 본문
<textarea class="form-control" name="content" rows="8" placeholder="문서 내용을 붙여 넣으세요." required></textarea>
</label>
<button class="btn rw-btn-primary" type="submit">지식자료 저장</button>
<details class="span-2 registration-advanced-options">
<summary>고급 설정</summary>
<p>기본 처리 방식을 변경하거나 기존 자료를 교체할 때만 사용합니다.</p>
<div class="form-grid registration-advanced-grid">
<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">기본 처리</option>
<option value="AI" th:disabled="${!aiEmbeddingConfigured}">AI 임베딩</option>
</select>
</label>
<label class="span-2">교체할 자료 ID
<input class="form-control" name="documentId" placeholder="기존 등록 결과에 표시된 자료 ID">
<span class="form-hint">입력한 자료의 검색 단위를 새 내용으로 교체합니다.</span>
</label>
<label class="span-2 registration-checkbox">
<input type="checkbox" name="replaceExisting" value="true"> 기존 자료를 교체합니다.
</label>
</div>
</details>
<div class="span-2 registration-actions"><button class="btn rw-btn-primary" type="submit">등록</button></div>
</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>
</div>
<section class="registration-result mt-3" th:if="${ingestResult}" aria-live="polite">
<div>
<strong>등록 완료</strong>
<span>검색할 수 있도록 자료를 처리했습니다.</span>
</div>
<dl>
<div><dt>자료 ID</dt><dd><code th:text="${ingestResult.documentId()}">document</code></dd></div>
<div><dt>검색 단위</dt><dd th:text="${ingestResult.chunkCount() + '개'}">0개</dd></div>
<div><dt>접근 태그</dt><dd th:text="${ingestResult.tagCount() + '개 연결'}">0개 연결</dd></div>
</dl>
</section>
</section>
<section class="content-band">
<div class="section-heading">
<div>
<span class="architecture-kicker">접근 규칙</span>
<h2>권한 규칙은 한 곳에서 관리합니다.</h2>
<p class="section-subtitle">사용자·그룹·역할·접근 분류 규칙을 한 곳에서 관리합니다.</p>
<h2>접근 태그 정책</h2>
<p class="section-subtitle">자료에 입력한 접근 태그의 허용 범위를 관리합니다.</p>
</div>
<div class="d-flex gap-2">
<a class="btn btn-sm rw-btn-secondary" href="/permissions">권한 관리 열기</a>
<a class="btn btn-sm rw-btn-primary" href="/dds-provision">권한 게시</a>
</div>
</div>
<div class="macro-micro-grid">
<div><h3>접근 규칙</h3><p>예: 세일즈 역할에 SALES 접근 분류값을 허용하면 SALES 자료만 검색됩니다. 거부 규칙은 허용 대상에서 제외됩니다.</p></div>
<div><h3>보호 방식</h3><p><code th:text="${ddsVectorObject}">ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS</code>의 보호 규칙이 업무 사용자와 접근 분류값을 비교합니다.</p></div>
</div>
<details class="explanation-details mt-3">
<details class="explanation-details mb-0">
<summary>도움말</summary>
<p>VPD는 요청마다 권한 테이블을 읽어 predicate를 계산합니다. DDS의 직접 비교 경로는 보호 VIEW에 선언한 DATA GRANT와 END USER 매핑을 사용합니다. 토큰 경로에서는 토큰이 공통 사용자 Context를 만들고, 보호 VIEW의 객체별 DATA GRANT predicate가 같은 권한 테이블을 다시 평가합니다.</p>
<p class="mb-0">예를 들어 SALES 태그를 허용한 역할은 SALES 태그 자료를 검색할 수 있습니다. 여러 태그가 허용되면 그중 하나와 일치하는 자료를 검색합니다. 태그 변경 뒤에는 권한 게시를 실행하세요.</p>
</details>
</section>
<section class="content-band">
<div class="section-heading">
<div>
<span class="architecture-kicker">업무 사용자 검색</span>
<h2>업무 사용자 검색</h2>
<p class="section-subtitle">하나의 기술 사용자 연결에서 Bearer 토큰으로 업무 사용자를 식별합니다.</p>
<h2>권한 검색</h2>
<p class="section-subtitle">임시 Bearer 토큰으로 허용된 자료만 검색합니다.</p>
</div>
<span class="badge text-bg-secondary">객체별 DATA GRANT</span>
</div>
<details class="explanation-details">
<summary>도움말</summary>
<ol>
<li>서버가 설정한 하나의 DDS 기술 사용자로 연결합니다.</li>
<li>Bearer 토큰을 <code>CB_AGENT_CTX_PKG.SET_USER_BY_BEARER</code>에 전달합니다.</li>
<li><code>CB_DDS_VECTOR_SEARCH_DOCUMENTS</code>의 객체별 DATA GRANT predicate가 <code>CB_APP_USER</code>, 역할·그룹, <code>CB_PERMISSION</code>, <code>CB_PERMISSION_RULE</code>을 조회합니다.</li>
<li>ALLOW TAG는 OR로 합치고 DENY TAG는 최종 결과에서 제외합니다.</li>
</ol>
<p class="mb-0">토큰은 보호 규칙의 입력값이 아니라 업무 사용자 Context를 만드는 값이며 저장하지 않습니다.</p>
<p class="mb-0">토큰은 검색하는 사용자를 확인하기 위한 임시 값이며 저장하지 않습니다. 회수되었거나 만료된 토큰은 사용할 수 없습니다.</p>
</details>
<form hx-post="/vector-knowledge/token-search" hx-target="#vector-token-search-result" hx-swap="innerHTML" class="form-grid mt-3">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
@@ -114,65 +116,61 @@
<input class="form-control" name="bearerToken" type="password" autocomplete="off" placeholder="발급된 임시 토큰을 붙여 넣으세요." required>
<span class="form-hint">요청 처리 후 저장하지 않습니다. 회수·만료된 토큰은 거부됩니다.</span>
</label>
<label>결과 수<input class="form-control" name="limit" type="number" min="1" max="100" value="10"></label>
<label>임베딩 방식
<select class="form-select" name="embeddingMode">
<option value="DEMO">로컬 임베딩(개발용)</option>
<option value="AI" th:disabled="${!aiEmbeddingConfigured}">AI 임베딩</option>
</select>
</label>
<label class="span-2">검색 질문
<textarea class="form-control" name="query" rows="3" placeholder="예: 세일즈 파이프라인 후속 조치 기준" required></textarea>
</label>
<button class="btn rw-btn-primary" type="submit">권한 검색</button>
<details class="span-2 registration-advanced-options">
<summary>검색 옵션</summary>
<div class="form-grid registration-advanced-grid mt-3">
<label>결과 수<input class="form-control" name="limit" type="number" min="1" max="100" value="10"></label>
<label>임베딩 방식
<select class="form-select" name="embeddingMode">
<option value="DEMO">기본 처리</option>
<option value="AI" th:disabled="${!aiEmbeddingConfigured}">AI 임베딩</option>
</select>
</label>
</div>
</details>
<div class="span-2 registration-actions"><button class="btn rw-btn-primary" type="submit">검색</button></div>
</form>
<section id="vector-token-search-result" class="mt-3" aria-live="polite">
<div class="empty-result-guide">Bearer 토큰과 질문을 입력하면 허용된 지식자료만 표시합니다.</div>
<div class="empty-result-guide">토큰과 검색 질문을 입력하면 허용된 자료만 표시합니다.</div>
</section>
</section>
<section class="content-band">
<div class="section-heading">
<div>
<span class="architecture-kicker">고급 검증</span>
<h2>고급: 직접 접근 비교</h2>
<p class="section-subtitle">DDS 선언형 보호 규칙 점검용 고급 검증</p>
</div>
<span class="badge text-bg-primary" th:text="${ddsVectorObject}">DDS view</span>
</div>
<details class="explanation-details">
<summary>도움말</summary>
<ol>
<li>검색어를 등록 때와 같은 방식으로 임베딩합니다.</li>
<li>DDS END USER로 보호 VIEW에 연결합니다.</li>
<li>DATA ROLE/DATA GRANT가 허용하지 않은 태그 청크는 객체 권한 단계에서 제외합니다.</li>
<li>남은 청크를 벡터 관련도 순으로 반환합니다.</li>
</ol>
<section class="content-band advanced-workflow">
<details>
<summary>직접 접근 검증</summary>
<p class="section-subtitle">DDS 보호 정책을 점검하는 관리자용 기능</p>
<details class="explanation-details mt-3">
<summary>도움말</summary>
<p class="mb-0">업무 사용자 토큰 경로와 별도로, DDS END USER와 DATA GRANT가 직접 적용되는 결과를 비교합니다. 운영 검색에는 권한 검색을 사용하세요.</p>
</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="userKey" required>
<option th:each="user : ${ddsUsers}" th:value="${user.key()}" th:selected="${user.key() == 'both'}"
th:text="${user.label() + ' · ' + user.username()}"></option>
</select>
<span class="form-hint">자격증명은 서버 환경 변수에서만 관리합니다.</span>
</label>
<label>결과 수<input class="form-control" name="limit" type="number" min="1" max="100" value="10"></label>
<label>임베딩 방식
<select class="form-select" name="embeddingMode">
<option value="DEMO">기본 처리</option>
<option value="AI" th:disabled="${!aiEmbeddingConfigured}">AI 임베딩</option>
</select>
</label>
<label class="span-2">검색 질문
<textarea class="form-control" name="query" rows="3" placeholder="예: 기술 태그로 지식자료 검색 권한을 제한하는 방법" required></textarea>
</label>
<div class="span-2 registration-actions"><button class="btn rw-btn-secondary" type="submit">검증 실행</button></div>
</form>
<section id="vector-search-result" class="mt-3" aria-live="polite">
<div class="empty-result-guide">검증 사용자와 검색 질문을 입력하면 보호 결과를 표시합니다.</div>
</section>
</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="userKey" required>
<option th:each="user : ${ddsUsers}" th:value="${user.key()}" th:selected="${user.key() == 'both'}"
th:text="${user.label() + ' · ' + user.username()}"></option>
</select>
<span class="form-hint">비밀번호는 서버 환경 변수에서만 읽고 화면에 표시하지 않습니다.</span>
</label>
<label>결과 수<input class="form-control" name="limit" type="number" min="1" max="100" value="10"></label>
<label>임베딩 방식
<select class="form-select" name="embeddingMode">
<option value="DEMO">로컬 임베딩(개발용)</option>
<option value="AI" th:disabled="${!aiEmbeddingConfigured}">AI 임베딩</option>
</select>
</label>
<label class="span-2">검색 질문
<textarea class="form-control" name="query" rows="3" placeholder="예: 기술 태그로 지식자료 검색 권한을 제한하는 방법" required></textarea>
</label>
<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>
</section>
</section>
<section class="content-band">