diff --git a/dds-backoffice/src/main/java/com/cloudhandson/ddsbackoffice/web/DdsVectorKnowledgeController.java b/dds-backoffice/src/main/java/com/cloudhandson/ddsbackoffice/web/DdsVectorKnowledgeController.java index 22dcb3b..0ef1176 100644 --- a/dds-backoffice/src/main/java/com/cloudhandson/ddsbackoffice/web/DdsVectorKnowledgeController.java +++ b/dds-backoffice/src/main/java/com/cloudhandson/ddsbackoffice/web/DdsVectorKnowledgeController.java @@ -12,6 +12,8 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import java.util.UUID; + @Controller public class DdsVectorKnowledgeController { @@ -34,28 +36,38 @@ public class DdsVectorKnowledgeController { @PostMapping("/vector-knowledge/ingest") public String ingest( - @RequestParam String documentId, + @RequestParam(required = false, defaultValue = "") String documentId, @RequestParam String title, @RequestParam(required = false, defaultValue = "") String sourceUri, @RequestParam String content, @RequestParam String techTags, @RequestParam(defaultValue = "600") int chunkSize, @RequestParam(defaultValue = "DEMO") String embeddingMode, + @RequestParam(defaultValue = "false") boolean replaceExisting, RedirectAttributes redirectAttributes ) { try { var result = service.ingest(new VectorIngestCommand( - documentId, title, sourceUri, content, techTags, chunkSize, embeddingMode)); + resolveDocumentId(documentId, replaceExisting), title, sourceUri, content, techTags, chunkSize, embeddingMode)); redirectAttributes.addFlashAttribute("ingestResult", result); redirectAttributes.addFlashAttribute("message", - result.chunkCount() + "개 청크를 저장했습니다. DDS 권한 평가용 태그 " - + result.tagCount() + "개가 연결되었습니다."); + "지식자료를 등록하고 검색 준비를 완료했습니다."); } catch (Exception exception) { redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage()); } return "redirect:/vector-knowledge"; } + private String resolveDocumentId(String documentId, boolean replaceExisting) { + if (documentId == null || documentId.isBlank()) { + return "knowledge-" + UUID.randomUUID(); + } + if (!replaceExisting) { + throw new AppException("기존 자료를 교체하려면 교체 확인을 선택하세요."); + } + return documentId.trim(); + } + @PostMapping("/vector-knowledge/search") public String search( @RequestParam String userKey, diff --git a/dds-backoffice/src/main/resources/static/css/dds.css b/dds-backoffice/src/main/resources/static/css/dds.css index a101783..d430e2a 100644 --- a/dds-backoffice/src/main/resources/static/css/dds.css +++ b/dds-backoffice/src/main/resources/static/css/dds.css @@ -75,6 +75,23 @@ body { margin: 0; background: var(--dds-bg); color: var(--dds-ink); font-family: .page-title .context-summary { font-size: .95rem; max-width: 700px; } .explanation-details > summary { color: #43516a; font-size: .86rem; } .explanation-details > summary::marker { color: var(--dds-accent); } +.registration-advanced-options { background: #f8faff; border: 1px solid var(--dds-line); border-radius: 8px; padding: .85rem 1rem; } +.registration-advanced-options > summary { color: #43516a; cursor: pointer; font-size: .875rem; font-weight: 700; } +.registration-advanced-options > p { color: var(--dds-muted); font-size: .82rem; margin: .7rem 0; } +.registration-advanced-grid { align-items: end; } +.registration-checkbox { align-items: center; color: var(--dds-ink) !important; display: flex; gap: .5rem; } +.registration-checkbox input { height: 1rem; width: 1rem; } +.registration-actions { border-top: 1px solid var(--dds-line); padding-top: 1rem; } +.registration-actions .btn { min-width: 7rem; } +.registration-result { align-items: flex-start; background: #f3fbf7; border: 1px solid #bfe0cf; border-radius: 9px; display: flex; gap: 1.5rem; justify-content: space-between; padding: 1rem; } +.registration-result strong, .registration-result span { display: block; } +.registration-result span { color: var(--dds-muted); font-size: .86rem; margin-top: .25rem; } +.registration-result dl { display: flex; flex-wrap: wrap; gap: 1.2rem; margin: 0; } +.registration-result dl div { min-width: 7rem; } +.registration-result dt { color: var(--dds-muted); font-size: .72rem; font-weight: 700; } +.registration-result dd { font-size: .9rem; font-weight: 700; margin: .2rem 0 0; } +.advanced-workflow > details > summary { color: var(--dds-ink); cursor: pointer; font-size: 1rem; font-weight: 700; } +.advanced-workflow .section-subtitle { margin: .4rem 0 0; } .protection-summary { border-top: 3px solid #3157d5; } .protection-status-card { background: #fbfcff; border: 1px solid #dbe3f4; border-radius: 14px; margin-top: 1rem; padding: 1.25rem; } .protection-status-card.status-danger { background: #fffafa; border-color: #f2c8c8; } @@ -112,5 +129,6 @@ body { margin: 0; background: var(--dds-bg); color: var(--dds-ink); font-family: .panel { padding: 19px; } .hero { padding-top: 30px; } .protection-status-topline, .protection-status-footer { align-items: flex-start; flex-direction: column; } + .registration-result { flex-direction: column; } .dds-product-nav { border-radius: 8px; } } diff --git a/dds-backoffice/src/main/resources/templates/fragments/layout.html b/dds-backoffice/src/main/resources/templates/fragments/layout.html index cc6ac4c..d2341ab 100644 --- a/dds-backoffice/src/main/resources/templates/fragments/layout.html +++ b/dds-backoffice/src/main/resources/templates/fragments/layout.html @@ -45,7 +45,7 @@
ORDS 조회 대상 ORDS 핸들러 - 지식 검색 관리 + 지식 검색 관리 Chatbot Reasoning SSE 서비스 diff --git a/dds-backoffice/src/main/resources/templates/vector-knowledge.html b/dds-backoffice/src/main/resources/templates/vector-knowledge.html index 9bf3091..4440fe4 100644 --- a/dds-backoffice/src/main/resources/templates/vector-knowledge.html +++ b/dds-backoffice/src/main/resources/templates/vector-knowledge.html @@ -7,7 +7,7 @@
DDS · KNOWLEDGE ACCESS

지식자료 관리

-

등록 · 접근 규칙 · 업무 사용자 검색 · 고급 검증

+

자료 등록 · 권한 검색

도움말

문서는 검색 단위로 나뉘고 각 단위에 접근 분류값(TECH_TAG)이 연결됩니다. 역할별 허용 값을 등록하면 보호 규칙이 허용된 분류값만 검색 대상으로 남깁니다.

@@ -26,87 +26,89 @@
- 지식자료 -

지식자료 등록

-

문서 ID가 같으면 기존 검색 단위를 교체합니다.

+

자료 등록

+

제목, 원문, 접근 태그를 입력합니다.

- 0개 검색 단위
도움말 -
    -
  • 문단을 우선 묶고 길이가 길면 문장·공백 기준으로 청크를 나눕니다.
  • -
  • 기본 로컬 임베딩은 외부 API 없이 흐름을 확인하는 개발용 4차원 벡터입니다.
  • -
  • 운영에서는 등록과 검색에 같은 모델·차원을 사용해야 합니다.
  • -
+

접근 태그는 이 자료를 볼 수 있는 권한 범위입니다. 권한 관리에 등록된 TAG 값과 일치하는 태그를 입력하세요. 등록 후 문서는 검색 단위로 나뉘고 임베딩이 생성됩니다.

- - - - - -
- 접근 규칙 -

권한 규칙은 한 곳에서 관리합니다.

-

사용자·그룹·역할·접근 분류 규칙을 한 곳에서 관리합니다.

+

접근 태그 정책

+

자료에 입력한 접근 태그의 허용 범위를 관리합니다.

-
-

접근 규칙

예: 세일즈 역할에 SALES 접근 분류값을 허용하면 SALES 자료만 검색됩니다. 거부 규칙은 허용 대상에서 제외됩니다.

-

보호 방식

ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS의 보호 규칙이 업무 사용자와 접근 분류값을 비교합니다.

-
-
+
도움말 -

VPD는 요청마다 권한 테이블을 읽어 predicate를 계산합니다. DDS의 직접 비교 경로는 보호 VIEW에 선언한 DATA GRANT와 END USER 매핑을 사용합니다. 토큰 경로에서는 토큰이 공통 사용자 Context를 만들고, 보호 VIEW의 객체별 DATA GRANT predicate가 같은 권한 테이블을 다시 평가합니다.

+

예를 들어 SALES 태그를 허용한 역할은 SALES 태그 자료를 검색할 수 있습니다. 여러 태그가 허용되면 그중 하나와 일치하는 자료를 검색합니다. 태그 변경 뒤에는 권한 게시를 실행하세요.

- 업무 사용자 검색 -

업무 사용자 검색

-

하나의 기술 사용자 연결에서 Bearer 토큰으로 업무 사용자를 식별합니다.

+

권한 검색

+

임시 Bearer 토큰으로 허용된 자료만 검색합니다.

- 객체별 DATA GRANT
도움말 -
    -
  1. 서버가 설정한 하나의 DDS 기술 사용자로 연결합니다.
  2. -
  3. Bearer 토큰을 CB_AGENT_CTX_PKG.SET_USER_BY_BEARER에 전달합니다.
  4. -
  5. CB_DDS_VECTOR_SEARCH_DOCUMENTS의 객체별 DATA GRANT predicate가 CB_APP_USER, 역할·그룹, CB_PERMISSION, CB_PERMISSION_RULE을 조회합니다.
  6. -
  7. ALLOW TAG는 OR로 합치고 DENY TAG는 최종 결과에서 제외합니다.
  8. -
-

토큰은 보호 규칙의 입력값이 아니라 업무 사용자 Context를 만드는 값이며 저장하지 않습니다.

+

토큰은 검색하는 사용자를 확인하기 위한 임시 값이며 저장하지 않습니다. 회수되었거나 만료된 토큰은 사용할 수 없습니다.

@@ -114,65 +116,61 @@ 요청 처리 후 저장하지 않습니다. 회수·만료된 토큰은 거부됩니다. - - - +
+ 검색 옵션 +
+ + +
+
+
-
Bearer 토큰과 질문을 입력하면 허용된 지식자료만 표시합니다.
+
토큰과 검색 질문을 입력하면 허용된 자료만 표시합니다.
-
-
-
- 고급 검증 -

고급: 직접 접근 비교

-

DDS 선언형 보호 규칙 점검용 고급 검증

-
- DDS view -
-
- 도움말 -
    -
  1. 검색어를 등록 때와 같은 방식으로 임베딩합니다.
  2. -
  3. DDS END USER로 보호 VIEW에 연결합니다.
  4. -
  5. DATA ROLE/DATA GRANT가 허용하지 않은 태그 청크는 객체 권한 단계에서 제외합니다.
  6. -
  7. 남은 청크를 벡터 관련도 순으로 반환합니다.
  8. -
+
+
+ 직접 접근 검증 +

DDS 보호 정책을 점검하는 관리자용 기능

+
+ 도움말 +

업무 사용자 토큰 경로와 별도로, DDS END USER와 DATA GRANT가 직접 적용되는 결과를 비교합니다. 운영 검색에는 권한 검색을 사용하세요.

+
+
+ + + + + +
+
+
+
검증 사용자와 검색 질문을 입력하면 보호 결과를 표시합니다.
+
-
- - - - - - -
-
-
검증 프로필과 질문을 선택하면 허용된 검색 단위만 표시합니다.
-