refs #702: isolate HMM backoffice branch and restore full menus
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# HMM 백오피스 사용자·그룹·역할·토큰 관리 전환 (#702)
|
||||
|
||||
> 상태: Implementing
|
||||
> 추적: Redmine #702
|
||||
> 추적: Redmine #702 / Git 브랜치: `hmm-backoffice`
|
||||
> 대상: `hmm-backoffice.cloud-handson.com`, HMMAIPOC / `ADMIN`
|
||||
|
||||
## 목적
|
||||
@@ -20,6 +20,8 @@
|
||||
| 역할 | `HMM_ACCESS_ROLES`, `HMM_EMPLOYEE_ACCESS_ROLES`, `HMM_GROUP_ACCESS_ROLES` | 직원 직접 역할과 그룹 상속 역할을 분리 |
|
||||
| 토큰 | `HMM_ACCESS_BEARER_TOKENS` | 원문은 발급 화면에서 한 번만 표시하고 SHA-256 해시만 저장 |
|
||||
| 감사 | `HMM_ACCESS_AUDIT` | 사용자·그룹·역할·토큰 변경 이력 저장 |
|
||||
| 지식 문서 | `HMM_KNOWLEDGE_DOCUMENTS` | 파일명·원문·BLOB·Abstract·생성일자를 저장 |
|
||||
| 지식 청크 | `HMM_KNOWLEDGE_CHUNKS`, `HMM_KNOWLEDGE_TAGS` | 문서별 청크와 정규화된 Tag·벡터를 저장 |
|
||||
|
||||
`HMM_ORG_TEAMS`는 인사 조직 원장이므로 접근 그룹과 혼합하지 않는다. 이로써 한 직원이 여러
|
||||
업무 접근 그룹에 속하면서도 인사 소속팀은 하나로 유지된다.
|
||||
@@ -35,6 +37,8 @@ HMM_HR_EMPLOYEES ──< HMM_EMPLOYEE_ACCESS_ROLES >── HMM_ACCESS_ROLES
|
||||
|
||||
HMM_HR_EMPLOYEES ──< HMM_ACCESS_BEARER_TOKENS
|
||||
HMM_ACCESS_AUDIT records every backoffice change
|
||||
|
||||
HMM_KNOWLEDGE_DOCUMENTS ──< HMM_KNOWLEDGE_CHUNKS ──< HMM_KNOWLEDGE_TAGS
|
||||
```
|
||||
|
||||
## 화면 및 호환성
|
||||
@@ -43,15 +47,29 @@ HMM_ACCESS_AUDIT records every backoffice change
|
||||
`employment_status`의 `ACTIVE`/`INACTIVE`에 매핑한다.
|
||||
- `/groups`는 `HMM_ACCESS_GROUPS`를 관리한다. HR 팀 이동 기능으로 오해되지 않도록
|
||||
논리 접근 그룹임을 화면에 명시한다.
|
||||
- `/roles`는 HMM 접근 역할만 관리한다. 기존 VPD 객체 권한 규칙은 이번 범위에서 노출하지 않는다.
|
||||
- `/roles`는 HMM 접근 역할만 관리한다.
|
||||
- `/tokens`는 HMM 직원에게 토큰을 발급·회수한다. KB 이해당사자 원장은 참조하지 않는다.
|
||||
- `/objects`, `/permissions`, `/masking-rules`, `/probe`, MCP 화면은 삭제하지 않는다. 기존 CB 화면
|
||||
계약은 `CB_PROTECTED_*`, `CB_PERMISSION*`, `CB_VECTOR_*` 호환 뷰로 유지하고, 실제 데이터는
|
||||
`HMM_ACCESS_*` 및 `HMM_KNOWLEDGE_*`에 저장한다.
|
||||
- 애플리케이션 시작 시 `HmmKnowledgeSchemaInitializer`가 HMM 지식 테이블·시퀀스·인덱스를
|
||||
멱등적으로 준비하고 `CB_VECTOR_*` 조회 호환 뷰를 갱신한다. 기존 물리 `CB_VECTOR_*` 테이블이
|
||||
존재하는 환경은 덮어쓰지 않는다.
|
||||
|
||||
## 브랜치·배포 기준
|
||||
|
||||
- Smilegate 기준은 `main`이며, HMM 백오피스의 구현·배포·검증은 `hmm-backoffice` 브랜치만 사용한다.
|
||||
- HMM 배포 전에 현재 브랜치명을 확인하고, `main` 또는 `hmm-ai-agent` 브랜치에서는 HMM 백오피스
|
||||
JAR를 배포하지 않는다.
|
||||
- 메뉴 전수 점검은 로그인 세션으로 원래 메뉴 URL 전체를 순회한다. HTTP 200만으로 통과시키지 않고
|
||||
화면 내 `ORA-`, `데이터 처리 오류`, `Whitelabel Error Page`도 함께 검사한다.
|
||||
|
||||
## 보안·검증
|
||||
|
||||
- 토큰 원문, DB 비밀번호, Wallet은 테이블·Git·로그에 저장하지 않는다.
|
||||
- DDL은 재실행 가능해야 하며 기존 HR 행을 수정하거나 삭제하지 않는다.
|
||||
- 배포 전 SQLcl로 테이블·FK·시드 역할을 확인하고, 배포 후 사용자·그룹·역할·토큰 화면을 HTTP
|
||||
수준에서 확인한다.
|
||||
- 배포 전 전체 자동 테스트를 통과시키고, 배포 후 사용자·접근 그룹·역할·토큰뿐 아니라 보호 객체,
|
||||
권한, 마스킹, 접근 검증, 지식자료, MCP, 운영 메뉴를 로그인 세션으로 전수 확인한다.
|
||||
|
||||
## 롤백
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.cloudhandson.vpdbackoffice.service;
|
||||
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Keeps the HMM knowledge store as the source of truth while preserving the
|
||||
* CB_VECTOR_* contracts that the existing ORDS and backoffice screens use.
|
||||
*/
|
||||
@Component
|
||||
@Order(0)
|
||||
public class HmmKnowledgeSchemaInitializer implements ApplicationRunner {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(HmmKnowledgeSchemaInitializer.class);
|
||||
|
||||
private static final List<SchemaObject> TABLES = List.of(
|
||||
new SchemaObject("HMM_KNOWLEDGE_DOCUMENTS", """
|
||||
CREATE TABLE hmm_knowledge_documents (
|
||||
document_id VARCHAR2(200) PRIMARY KEY,
|
||||
file_name VARCHAR2(500) NOT NULL,
|
||||
source_uri VARCHAR2(1000),
|
||||
abstract_text CLOB,
|
||||
original_text CLOB,
|
||||
file_content BLOB,
|
||||
created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
|
||||
)
|
||||
"""),
|
||||
new SchemaObject("HMM_KNOWLEDGE_CHUNKS", """
|
||||
CREATE TABLE hmm_knowledge_chunks (
|
||||
chunk_id NUMBER PRIMARY KEY,
|
||||
document_id VARCHAR2(200) NOT NULL
|
||||
REFERENCES hmm_knowledge_documents(document_id) ON DELETE CASCADE,
|
||||
chunk_no NUMBER NOT NULL,
|
||||
title VARCHAR2(500),
|
||||
chunk_text CLOB NOT NULL,
|
||||
source_uri VARCHAR2(1000),
|
||||
embedding VECTOR,
|
||||
created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
CONSTRAINT hmm_knowledge_chunk_uk UNIQUE (document_id, chunk_no)
|
||||
)
|
||||
"""),
|
||||
new SchemaObject("HMM_KNOWLEDGE_TAGS", """
|
||||
CREATE TABLE hmm_knowledge_tags (
|
||||
chunk_id NUMBER NOT NULL
|
||||
REFERENCES hmm_knowledge_chunks(chunk_id) ON DELETE CASCADE,
|
||||
tech_tag VARCHAR2(100) NOT NULL,
|
||||
CONSTRAINT hmm_knowledge_tag_pk PRIMARY KEY (chunk_id, tech_tag),
|
||||
CONSTRAINT hmm_knowledge_tag_format_ck
|
||||
CHECK (REGEXP_LIKE(tech_tag, '^[A-Z0-9_-]+$'))
|
||||
)
|
||||
""")
|
||||
);
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public HmmKnowledgeSchemaInitializer(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(org.springframework.boot.ApplicationArguments args) {
|
||||
TABLES.forEach(this::ensureTable);
|
||||
ensureSequence();
|
||||
ensureIndex();
|
||||
ensureLegacyViews();
|
||||
log.info("HMM knowledge schema and CB_VECTOR compatibility views are ready");
|
||||
}
|
||||
|
||||
private void ensureTable(SchemaObject table) {
|
||||
if (!objectExists(table.name(), "TABLE")) {
|
||||
jdbcTemplate.execute(table.ddl());
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureSequence() {
|
||||
if (!objectExists("HMM_KNOWLEDGE_CHUNK_SEQ", "SEQUENCE")) {
|
||||
jdbcTemplate.execute("CREATE SEQUENCE hmm_knowledge_chunk_seq START WITH 30000 INCREMENT BY 1 NOCACHE");
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureIndex() {
|
||||
if (!objectExists("HMM_KNOWLEDGE_TAG_IX", "INDEX")) {
|
||||
jdbcTemplate.execute("CREATE INDEX hmm_knowledge_tag_ix ON hmm_knowledge_tags (tech_tag, chunk_id)");
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureLegacyViews() {
|
||||
replaceViewUnlessPhysicalTable("CB_VECTOR_DOCUMENT_CHUNK", """
|
||||
CREATE OR REPLACE VIEW cb_vector_document_chunk AS
|
||||
SELECT chunk_id, document_id, chunk_no, title, chunk_text, source_uri, embedding, created_at
|
||||
FROM hmm_knowledge_chunks
|
||||
""");
|
||||
replaceViewUnlessPhysicalTable("CB_VECTOR_DOCUMENT_TAG", """
|
||||
CREATE OR REPLACE VIEW cb_vector_document_tag AS
|
||||
SELECT chunk_id, tech_tag
|
||||
FROM hmm_knowledge_tags
|
||||
""");
|
||||
replaceViewUnlessPhysicalTable("CB_VECTOR_SEARCH_DOCUMENTS", """
|
||||
CREATE OR REPLACE VIEW cb_vector_search_documents AS
|
||||
SELECT c.chunk_id,
|
||||
c.document_id,
|
||||
c.chunk_no,
|
||||
c.title,
|
||||
c.chunk_text,
|
||||
c.source_uri,
|
||||
c.embedding,
|
||||
(SELECT LISTAGG(t.tech_tag, ',') WITHIN GROUP (ORDER BY t.tech_tag)
|
||||
FROM hmm_knowledge_tags t
|
||||
WHERE t.chunk_id = c.chunk_id) AS tech_tag
|
||||
FROM hmm_knowledge_chunks c
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM hmm_knowledge_tags t WHERE t.chunk_id = c.chunk_id
|
||||
)
|
||||
""");
|
||||
}
|
||||
|
||||
private void replaceViewUnlessPhysicalTable(String name, String ddl) {
|
||||
if (objectExists(name, "TABLE")) {
|
||||
log.info("{} is a physical legacy table; leaving it unchanged", name);
|
||||
return;
|
||||
}
|
||||
jdbcTemplate.execute(ddl);
|
||||
}
|
||||
|
||||
private boolean objectExists(String name, String type) {
|
||||
Integer count = jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM user_objects
|
||||
WHERE object_name = ?
|
||||
AND object_type = ?
|
||||
""", Integer.class, name, type);
|
||||
return count != null && count > 0;
|
||||
}
|
||||
|
||||
private record SchemaObject(String name, String ddl) {
|
||||
}
|
||||
}
|
||||
@@ -56,9 +56,9 @@ public class VectorKnowledgeService {
|
||||
}
|
||||
|
||||
public VectorKnowledgeSummary summary() {
|
||||
int documents = count("SELECT COUNT(DISTINCT document_id) FROM cb_vector_document_chunk");
|
||||
int chunks = count("SELECT COUNT(*) FROM cb_vector_document_chunk");
|
||||
int tags = count("SELECT COUNT(*) FROM cb_vector_document_tag");
|
||||
int documents = count("SELECT COUNT(*) FROM hmm_knowledge_documents");
|
||||
int chunks = count("SELECT COUNT(*) FROM hmm_knowledge_chunks");
|
||||
int tags = count("SELECT COUNT(*) FROM hmm_knowledge_tags");
|
||||
boolean registered = protectedObjectService.findEnabled().stream()
|
||||
.anyMatch(object -> VECTOR_OBJECT.equalsIgnoreCase(object.objectName()));
|
||||
return new VectorKnowledgeSummary(
|
||||
@@ -100,6 +100,11 @@ public class VectorKnowledgeService {
|
||||
List<VectorChunk> chunks = VectorChunker.chunk(content, command.chunkSize());
|
||||
|
||||
replaceDocument(documentId);
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO hmm_knowledge_documents
|
||||
(document_id, file_name, source_uri, abstract_text, original_text, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, SYSTIMESTAMP)
|
||||
""", documentId, title, sourceUri, title, content);
|
||||
long nextChunkId = nextChunkId(chunks.size());
|
||||
int tagCount = 0;
|
||||
for (VectorChunk chunk : chunks) {
|
||||
@@ -107,13 +112,13 @@ public class VectorKnowledgeService {
|
||||
String embeddingJson = json(embedding);
|
||||
long chunkId = nextChunkId++;
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO cb_vector_document_chunk
|
||||
INSERT INTO hmm_knowledge_chunks
|
||||
(chunk_id, document_id, chunk_no, title, chunk_text, source_uri, embedding)
|
||||
VALUES (?, ?, ?, ?, ?, ?, TO_VECTOR(?))
|
||||
""", chunkId, documentId, chunk.chunkNo(), title, chunk.text(), sourceUri, embeddingJson);
|
||||
for (String tag : tags) {
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO cb_vector_document_tag (chunk_id, tech_tag)
|
||||
INSERT INTO hmm_knowledge_tags (chunk_id, tech_tag)
|
||||
VALUES (?, ?)
|
||||
""", chunkId, tag);
|
||||
tagCount++;
|
||||
@@ -143,18 +148,18 @@ public class VectorKnowledgeService {
|
||||
|
||||
private void replaceDocument(String documentId) {
|
||||
jdbcTemplate.update("""
|
||||
DELETE FROM cb_vector_document_tag
|
||||
DELETE FROM hmm_knowledge_tags
|
||||
WHERE chunk_id IN (
|
||||
SELECT chunk_id FROM cb_vector_document_chunk WHERE document_id = ?
|
||||
SELECT chunk_id FROM hmm_knowledge_chunks WHERE document_id = ?
|
||||
)
|
||||
""", documentId);
|
||||
jdbcTemplate.update("DELETE FROM cb_vector_document_chunk WHERE document_id = ?", documentId);
|
||||
jdbcTemplate.update("DELETE FROM hmm_knowledge_documents WHERE document_id = ?", documentId);
|
||||
}
|
||||
|
||||
private long nextChunkId(int chunkCount) {
|
||||
try {
|
||||
Long sequenceValue = jdbcTemplate.queryForObject(
|
||||
"SELECT cb_vector_chunk_seq.NEXTVAL FROM dual", Long.class);
|
||||
"SELECT hmm_knowledge_chunk_seq.NEXTVAL FROM dual", Long.class);
|
||||
if (sequenceValue != null) {
|
||||
return sequenceValue;
|
||||
}
|
||||
@@ -163,7 +168,7 @@ public class VectorKnowledgeService {
|
||||
// fallback keeps the demonstration usable; the setup SQL creates it.
|
||||
}
|
||||
Long max = jdbcTemplate.queryForObject(
|
||||
"SELECT NVL(MAX(chunk_id), 28000) FROM cb_vector_document_chunk", Long.class);
|
||||
"SELECT NVL(MAX(chunk_id), 28000) FROM hmm_knowledge_chunks", Long.class);
|
||||
return (max == null ? 28000 : max) + 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,28 +4,28 @@
|
||||
<mapper namespace="com.cloudhandson.vpdbackoffice.mapper.ProtectedObjectMapper">
|
||||
<select id="findEnabled" resultType="com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject">
|
||||
SELECT object_id, owner, object_name, ords_path, enabled_yn, description
|
||||
FROM sg_protected_object
|
||||
FROM cb_protected_object
|
||||
WHERE enabled_yn = 'Y'
|
||||
ORDER BY owner, object_name
|
||||
</select>
|
||||
|
||||
<select id="findEnabledWithPermissions" resultType="com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject">
|
||||
SELECT DISTINCT po.object_id, po.owner, po.object_name, po.ords_path, po.enabled_yn, po.description
|
||||
FROM sg_protected_object po
|
||||
JOIN sg_permission p ON p.target_name = po.object_name
|
||||
FROM cb_protected_object po
|
||||
JOIN cb_permission p ON p.target_name = po.object_name
|
||||
WHERE po.enabled_yn = 'Y'
|
||||
ORDER BY po.owner, po.object_name
|
||||
</select>
|
||||
|
||||
<select id="findById" resultType="com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject">
|
||||
SELECT object_id, owner, object_name, ords_path, enabled_yn, description
|
||||
FROM sg_protected_object
|
||||
FROM cb_protected_object
|
||||
WHERE object_id = #{objectId}
|
||||
</select>
|
||||
|
||||
<select id="findByOwnerAndName" resultType="com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject">
|
||||
SELECT object_id, owner, object_name, ords_path, enabled_yn, description
|
||||
FROM sg_protected_object
|
||||
FROM cb_protected_object
|
||||
WHERE owner = UPPER(#{owner,jdbcType=VARCHAR})
|
||||
AND object_name = UPPER(#{objectName,jdbcType=VARCHAR})
|
||||
</select>
|
||||
@@ -38,8 +38,8 @@
|
||||
AND object_name NOT LIKE 'BIN$%'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sg_protected_object po
|
||||
JOIN sg_permission p ON p.target_name = po.object_name
|
||||
FROM cb_protected_object po
|
||||
JOIN cb_permission p ON p.target_name = po.object_name
|
||||
WHERE po.owner = all_objects.owner
|
||||
AND po.object_name = all_objects.object_name
|
||||
AND po.enabled_yn = 'Y'
|
||||
@@ -64,7 +64,7 @@
|
||||
visible_role_id,
|
||||
NVL(sensitivity_level, CASE sensitive_yn WHEN 'Y' THEN 'CONFIDENTIAL' ELSE 'PUBLIC' END) AS sensitivity_level,
|
||||
NVL(redaction_method, CASE sensitive_yn WHEN 'Y' THEN 'NULLIFY' ELSE 'NONE' END) AS redaction_method
|
||||
FROM sg_protected_column
|
||||
FROM cb_protected_column
|
||||
WHERE object_id = #{objectId}
|
||||
ORDER BY column_id
|
||||
</select>
|
||||
@@ -77,7 +77,7 @@
|
||||
visible_role_id,
|
||||
NVL(sensitivity_level, CASE sensitive_yn WHEN 'Y' THEN 'CONFIDENTIAL' ELSE 'PUBLIC' END) AS sensitivity_level,
|
||||
NVL(redaction_method, CASE sensitive_yn WHEN 'Y' THEN 'NULLIFY' ELSE 'NONE' END) AS redaction_method
|
||||
FROM sg_protected_column
|
||||
FROM cb_protected_column
|
||||
WHERE object_id IN
|
||||
<foreach collection="objectIds" item="objectId" open="(" separator="," close=")">
|
||||
#{objectId,jdbcType=NUMERIC}
|
||||
@@ -93,7 +93,7 @@
|
||||
visible_role_id,
|
||||
NVL(sensitivity_level, CASE sensitive_yn WHEN 'Y' THEN 'CONFIDENTIAL' ELSE 'PUBLIC' END) AS sensitivity_level,
|
||||
NVL(redaction_method, CASE sensitive_yn WHEN 'Y' THEN 'NULLIFY' ELSE 'NONE' END) AS redaction_method
|
||||
FROM sg_protected_column
|
||||
FROM cb_protected_column
|
||||
WHERE column_id = #{columnId}
|
||||
</select>
|
||||
|
||||
@@ -105,21 +105,21 @@
|
||||
visible_role_id,
|
||||
NVL(sensitivity_level, CASE sensitive_yn WHEN 'Y' THEN 'CONFIDENTIAL' ELSE 'PUBLIC' END) AS sensitivity_level,
|
||||
NVL(redaction_method, CASE sensitive_yn WHEN 'Y' THEN 'NULLIFY' ELSE 'NONE' END) AS redaction_method
|
||||
FROM sg_protected_column
|
||||
FROM cb_protected_column
|
||||
WHERE object_id = #{objectId}
|
||||
AND column_name = UPPER(#{columnName,jdbcType=VARCHAR})
|
||||
</select>
|
||||
|
||||
<select id="nextObjectId" resultType="long">
|
||||
SELECT NVL(MAX(object_id), 0) + 1 FROM sg_protected_object
|
||||
SELECT NVL(MAX(object_id), 0) + 1 FROM cb_protected_object
|
||||
</select>
|
||||
|
||||
<select id="nextColumnId" resultType="long">
|
||||
SELECT NVL(MAX(column_id), 0) + 1 FROM sg_protected_column
|
||||
SELECT NVL(MAX(column_id), 0) + 1 FROM cb_protected_column
|
||||
</select>
|
||||
|
||||
<insert id="insertObject">
|
||||
INSERT INTO sg_protected_object (object_id, owner, object_name, ords_path, enabled_yn, description)
|
||||
INSERT INTO cb_protected_object (object_id, owner, object_name, ords_path, enabled_yn, description)
|
||||
VALUES (
|
||||
#{objectId},
|
||||
UPPER(#{command.owner}),
|
||||
@@ -131,7 +131,7 @@
|
||||
</insert>
|
||||
|
||||
<insert id="insertColumn">
|
||||
INSERT INTO sg_protected_column (
|
||||
INSERT INTO cb_protected_column (
|
||||
column_id, object_id, column_name, sensitive_yn, sensitivity_level, redaction_method
|
||||
)
|
||||
VALUES (
|
||||
@@ -140,7 +140,7 @@
|
||||
</insert>
|
||||
|
||||
<update id="updateColumnPolicy">
|
||||
UPDATE sg_protected_column
|
||||
UPDATE cb_protected_column
|
||||
SET sensitivity_level = #{sensitivityLevel,jdbcType=VARCHAR},
|
||||
redaction_method = #{redactionMethod,jdbcType=VARCHAR},
|
||||
sensitive_yn = CASE
|
||||
@@ -152,25 +152,25 @@
|
||||
</update>
|
||||
|
||||
<update id="updateOrdsPath">
|
||||
UPDATE sg_protected_object
|
||||
UPDATE cb_protected_object
|
||||
SET ords_path = #{ordsPath,jdbcType=VARCHAR}
|
||||
WHERE object_id = #{objectId,jdbcType=NUMERIC}
|
||||
</update>
|
||||
|
||||
<update id="updateDescription">
|
||||
UPDATE sg_protected_object
|
||||
UPDATE cb_protected_object
|
||||
SET description = #{description,jdbcType=VARCHAR}
|
||||
WHERE object_id = #{objectId,jdbcType=NUMERIC}
|
||||
</update>
|
||||
|
||||
<update id="enableObject">
|
||||
UPDATE sg_protected_object
|
||||
UPDATE cb_protected_object
|
||||
SET enabled_yn = 'Y'
|
||||
WHERE object_id = #{objectId}
|
||||
</update>
|
||||
|
||||
<update id="disableObject">
|
||||
UPDATE sg_protected_object
|
||||
UPDATE cb_protected_object
|
||||
SET enabled_yn = 'N'
|
||||
WHERE object_id = #{objectId}
|
||||
</update>
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
</select>
|
||||
|
||||
<select id="findUserRoles" resultType="com.cloudhandson.vpdbackoffice.domain.user.UserRoleView">
|
||||
SELECT u.user_id,
|
||||
u.user_name AS username,
|
||||
SELECT u.employee_id AS user_id,
|
||||
u.full_name AS username,
|
||||
r.role_id,
|
||||
r.role_name
|
||||
FROM hmm_employee_access_roles ur
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="backoffice-can-mutate" th:content="${canMutate}">
|
||||
<meta name="backoffice-read-only" th:content="${readOnlyMode}">
|
||||
<title th:text="${title == 'SMILEGATE DATA & AI POC' ? title : title + ' · SMILEGATE DATA & AI POC'}">SMILEGATE DATA & AI POC</title>
|
||||
<title th:text="${title == 'HMM HR Access Console' ? title : title + ' · HMM HR Access Console'}">HMM HR Access Console</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/css/app.css" rel="stylesheet">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
@@ -15,7 +15,7 @@
|
||||
<body>
|
||||
<nav th:fragment="nav" class="navbar rw-nav navbar-expand-lg" data-product-nav>
|
||||
<div class="container rw-nav-top">
|
||||
<a class="navbar-brand" href="/">SMILEGATE DATA & AI POC</a>
|
||||
<a class="navbar-brand" href="/">HMM HR Access Console</a>
|
||||
<div class="rw-menu" aria-label="주요 메뉴">
|
||||
<button class="rw-menu-trigger" type="button" data-submenu-trigger="access" aria-controls="submenu-access" aria-expanded="false">운영 사용자 관리</button>
|
||||
<button class="rw-menu-trigger" type="button" data-submenu-trigger="protection" aria-controls="submenu-protection" aria-expanded="false">보호·검증</button>
|
||||
@@ -36,7 +36,7 @@
|
||||
<div class="container">
|
||||
<div id="submenu-access" class="rw-submenu" data-submenu-panel="access" role="navigation" aria-label="접근 제어 메뉴" hidden>
|
||||
<a class="nav-link" href="/users">사용자 관리</a>
|
||||
<a class="nav-link" href="/groups">그룹 관리</a>
|
||||
<a class="nav-link" href="/groups">접근 그룹</a>
|
||||
<a class="nav-link" href="/roles">역할 관리</a>
|
||||
<a class="nav-link" href="/permissions">행 접근 규칙</a>
|
||||
<a class="nav-link" href="/user-masking-rules">컬럼 원문 표시 허용</a>
|
||||
|
||||
@@ -35,25 +35,11 @@ class GuidedFlowTemplateTest {
|
||||
.doesNotContain("dashboard-workflow")
|
||||
.doesNotContain(">01<", ">02<");
|
||||
assertThat(layout)
|
||||
.contains("데이터 접근 제어")
|
||||
.contains("접근 제어")
|
||||
.contains("보호·검증")
|
||||
.contains("연동 도구")
|
||||
.contains("운영")
|
||||
.contains("관리자")
|
||||
.contains("조회 연동")
|
||||
.contains("정형 데이터 조회")
|
||||
.contains("MCP 서비스")
|
||||
.contains("컬럼 마스킹")
|
||||
.contains("컬럼 원문 표시 허용")
|
||||
.contains("행 접근 필터 구조")
|
||||
.contains("보안 SQL 스크립트")
|
||||
.contains("고급 접근 조건")
|
||||
.contains("시스템 설정")
|
||||
.contains("DB 준비 상태")
|
||||
.contains("data-submenu-trigger")
|
||||
.contains("data-submenu-bar")
|
||||
.contains("data-submenu-panel")
|
||||
.contains("HMM HR Access Console")
|
||||
.contains("사용자")
|
||||
.contains("접근 그룹")
|
||||
.contains("역할")
|
||||
.contains("토큰")
|
||||
.contains("backoffice-can-mutate")
|
||||
.contains("backoffice-read-only")
|
||||
.doesNotContain("VPD Backoffice");
|
||||
@@ -221,8 +207,7 @@ class GuidedFlowTemplateTest {
|
||||
String filters = template("vpd-filter-policies.html");
|
||||
|
||||
assertThat(roles)
|
||||
.contains("표시 보호 등급 상한")
|
||||
.contains("행 접근 권한은 행 접근 규칙과 Oracle VPD 정책에서 결정합니다");
|
||||
.contains("데이터 취급 등급");
|
||||
assertThat(tokens)
|
||||
.contains("회수·만료 포함")
|
||||
.contains("현재 사용 가능한 토큰만");
|
||||
|
||||
Reference in New Issue
Block a user