@@ -51,6 +51,13 @@ export BACKOFFICE_ORDS_DB_URL="${BACKOFFICE_DB_URL}"
|
|||||||
export BACKOFFICE_ORDS_DB_USERNAME="CB_ORDS"
|
export BACKOFFICE_ORDS_DB_USERNAME="CB_ORDS"
|
||||||
export BACKOFFICE_ORDS_DB_PASSWORD=""
|
export BACKOFFICE_ORDS_DB_PASSWORD=""
|
||||||
|
|
||||||
|
# Smilegate Select AI는 프로파일 소유 스키마(SGMP_POC)로 별도 접속합니다.
|
||||||
|
# 원문 비밀번호는 .env 또는 배포 환경 secret에만 두며 Git에 올리지 않습니다.
|
||||||
|
export BACKOFFICE_SELECT_AI_DB_URL="${BACKOFFICE_DB_URL}"
|
||||||
|
export BACKOFFICE_SELECT_AI_DB_USERNAME="SGMP_POC"
|
||||||
|
export BACKOFFICE_SELECT_AI_DB_PASSWORD=""
|
||||||
|
export BACKOFFICE_SELECT_AI_PROFILE="SGMP_POC_HAIKU45"
|
||||||
|
|
||||||
# --- (2c) OpenAI 호환 AI 호출 (MCP-style Reasoning 탭) ---
|
# --- (2c) OpenAI 호환 AI 호출 (MCP-style Reasoning 탭) ---
|
||||||
export BACKOFFICE_AI_ENABLED="false"
|
export BACKOFFICE_AI_ENABLED="false"
|
||||||
export BACKOFFICE_AI_PROVIDER="openai" # openai | oci
|
export BACKOFFICE_AI_PROVIDER="openai" # openai | oci
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -17,6 +17,10 @@ logs/
|
|||||||
# Java / Maven
|
# Java / Maven
|
||||||
target/
|
target/
|
||||||
|
|
||||||
|
# Python / Streamlit
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
|
||||||
# Locally downloaded development tools (for example SQLcl)
|
# Locally downloaded development tools (for example SQLcl)
|
||||||
.tools/
|
.tools/
|
||||||
|
|
||||||
|
|||||||
76
docs/design/703-smilegate-backoffice-finalization/README.md
Normal file
76
docs/design/703-smilegate-backoffice-finalization/README.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# 설계서: 스마일게이트 백오피스 잔여 UI 전환 및 운영 검증
|
||||||
|
|
||||||
|
## 추적성
|
||||||
|
|
||||||
|
- Redmine: #703 `[Smilegate] 백오피스 잔여 UI 전환 및 운영 검증`
|
||||||
|
- 관련 설계: `docs/design/smilegate-demo-rebranding/README.md`, `docs/design/smilegate-identity-administration/README.md`
|
||||||
|
- 구현 대상: `src/main/resources/templates/`, `src/main/resources/static/js/app.js`, `src/main/java/com/cloudhandson/vpdbackoffice/`, `poc4_active_source_20260714/`
|
||||||
|
- 검증 대상: Maven·Streamlit 설정 테스트, 인증 후 핵심 메뉴 HTTP 응답, 화면의 잔여 고객사 문구 검사
|
||||||
|
- 상태: Implemented / deployment pending
|
||||||
|
|
||||||
|
## 프로젝트 개요
|
||||||
|
|
||||||
|
이 저장소의 Spring Boot 백오피스는 Oracle VPD, Data Redaction, FGA, ORDS 및 Select AI PoC의 운영 설정을 확인하고 관리한다. 공개 데모의 고객·업무 대상은 스마일게이트 게임 로그와 서비스 데이터 분석이다.
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
|
||||||
|
백오피스에 남은 KB손해보험/보험 업무 예시를 스마일게이트 게임 데이터 기준으로 전환한다. 화면 문구만 바꾸지 않고, 실제 MCP Select AI 안내·행 접근 규칙 요약·마스킹 동기화 대상도 `SGMP_POC` 게임 데이터와 모순되지 않게 맞춘다.
|
||||||
|
|
||||||
|
## 범위
|
||||||
|
|
||||||
|
1. 웹 화면과 브라우저에서 실행되는 JavaScript에 노출된 기존 보험 업무 예시를 게임 사용자·게임 서비스·판매/환불 데이터 예시로 교체한다.
|
||||||
|
2. 권한 규칙의 표시명과 미리보기는 기존 조건 코드의 저장 형식을 보존하면서 게임 데이터 의미로 설명한다.
|
||||||
|
3. MCP 데모의 tool 식별자·설명·질의 예시를 `SGMP_POC` Select AI 프로파일 기반으로 전환한다. 행 접근 토큰을 전제로 하는 기존 KB ORDS endpoint를 게임 데이터 endpoint인 것처럼 표시하지 않는다.
|
||||||
|
4. Data Redaction 동기화는 `SGMP_POC`의 실제 게임 사용자·판매 데이터 컬럼만 관리 대상으로 삼는다.
|
||||||
|
5. 내부 호환용 `CB_*` 뷰와 과거 SQL 이력은 실행 경로에서 제외한다. Smilegate 공개 화면·MCP 설정은 이력의 고객 데이터나 endpoint를 참조하지 않는다.
|
||||||
|
6. Streamlit 외피는 Smilegate 프로필·게임 데이터 시나리오·`oracle.select_ai.smilegate_game_text2sql` MCP 하나만 노출한다. 이전 고객용 토큰 프리셋 및 감사·보안관리 탭은 기본 실행 경로에서 제외한다.
|
||||||
|
|
||||||
|
## 설계 결정
|
||||||
|
|
||||||
|
### 1. 업무 용어는 데이터 모델의 사실에 맞춘다
|
||||||
|
|
||||||
|
- 사용자 식별자: `CZN_COMN_USER_MST.GUID`/`AUID`, `COMN_SALES_USER_MST.USER_KEY_VAL`
|
||||||
|
- 게임 서비스 식별: `COMN_GAME_ALIAS_BAS`의 `GAME_ID`, `GAME_PREFIX`, `GAME_NM`, `GAME_ALIAS_NM`
|
||||||
|
- 거래/서비스 데이터: `COMN_SALES_TXN`, `COMN_REFUND_TXN`, `CZN_CUSTOM_*`
|
||||||
|
|
||||||
|
화면 예시는 위 객체를 사용하되, 실제로 존재하지 않는 담당자·채널 컬럼을 SQL 예시로 만들지 않는다.
|
||||||
|
|
||||||
|
### 2. 조건 코드의 호환성과 표시 의미를 분리한다
|
||||||
|
|
||||||
|
`OWN_CONTRACT`, `CHANNEL_CONTRACT`, `OWN_CUSTOMER`, `CHANNEL_CUSTOMER` 같은 과거 코드값은 저장값 호환을 위해 유지한다. 화면에는 각각 `담당 게임 서비스`, `토큰 채널 게임 서비스`, `담당 게임 사용자 데이터`, `토큰 채널 게임 사용자 데이터`로 표시한다. VPD 구현이 게임 데이터에 대한 실제 관계를 갖지 않는 조건은 설명에서 일반적인 보안 범위 조건으로만 제시하고, 존재하지 않는 조인 SQL을 제안하지 않는다.
|
||||||
|
|
||||||
|
### 3. MCP/Select AI는 현재 실행 경계를 정직하게 표시한다
|
||||||
|
|
||||||
|
MCP tool은 `SGMP_POC_HAIKU45` 프로파일을 기준으로 게임 데이터의 읽기 전용 `SELECT`/`WITH` 질의를 **생성**하는 용도로 안내한다. 생성 단계는 `SHOWSQL`만 사용하며 모델이 만든 SQL을 백오피스가 자동 실행하지 않는다. 운영자는 Database Actions 또는 검증된 실행 경로에서 SQL을 검토·실행한다.
|
||||||
|
|
||||||
|
프로파일은 `SGMP_POC` 소유이므로 일반 백오피스 관리 DB 연결(ADMIN)에서 사용할 수 없다. MCP Text2SQL 서비스는 별도 `BACKOFFICE_SELECT_AI_DB_URL`, `BACKOFFICE_SELECT_AI_DB_USERNAME`, `BACKOFFICE_SELECT_AI_DB_PASSWORD` 환경 변수로 `SGMP_POC` 연결을 만들고, 설정이 없을 때는 명확한 설정 오류만 반환한다. 비밀 값은 Git·화면·로그에 저장하지 않는다.
|
||||||
|
|
||||||
|
호출 전에 백오피스의 Bearer 토큰 해시를 검증하고 활성 사용자 토큰에만 Text2SQL 요청을 허용한다. 현재 PoC의 두 데모 운영 사용자는 게임 데이터 전체 권한을 갖지만, 후속 권한 세분화 시 이 지점에 역할별 데이터 범위 검증을 추가한다.
|
||||||
|
|
||||||
|
### 4. 마스킹 대상은 관리 가능한 실제 객체로 제한한다
|
||||||
|
|
||||||
|
마스킹 동기화 대상 owner는 `SGMP_POC`다. 관리 정책은 실제 컬럼 존재 여부를 검증한 뒤 사용자 식별자와 거래 사용자 식별자에만 적용한다. 대상에 없는 규칙은 DBMS_REDACT 호출 전에 화면 설정 오류로 처리한다.
|
||||||
|
|
||||||
|
## 변경 파일과 책임
|
||||||
|
|
||||||
|
| 영역 | 파일 | 변경 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 행 접근 화면 | `templates/permissions.html`, `static/js/app.js`, `PermissionView.java` | 보험 용어와 존재하지 않는 KB SQL 예시 제거 |
|
||||||
|
| 마스킹 화면 | `templates/masking-rules.html`, `templates/user-masking-rules.html`, `MaskingPolicySynchronizer.java` | 게임 데이터 예시 및 실제 `SGMP_POC` 관리 대상 사용 |
|
||||||
|
| VPD/운영 화면 | `templates/vpd-filter-runtime.html`, `templates/operation-status.html` | 게임 데이터 상태 표시 예시 적용 |
|
||||||
|
| MCP 화면 | `templates/mcp-sse.html`, `McpSseService.java`, `SmilegateSelectAiService.java` | 게임 데이터 Select AI 도구, 토큰 검증 및 SHOWSQL 생성 |
|
||||||
|
| 보안 스크립트 화면 | `SecuritySqlScriptService.java` | UI에 노출되는 KB 설명을 게임 데이터 설명으로 교체 |
|
||||||
|
| Streamlit 외피 | `poc4_active_source_20260714/config/`, `apps/poc4/mcp_discovery_ui.py` | Smilegate 로그인/헤더/시나리오와 단일 게임 Text2SQL MCP 계약 적용 |
|
||||||
|
|
||||||
|
## 완료 기준
|
||||||
|
|
||||||
|
1. Smilegate 공개 화면·활성 MCP 설정에서 기존 고객사명·보험 원장·기존 endpoint가 검색되지 않는다. 과거 SQL 이력 및 미실행 호환 코드는 제외한다.
|
||||||
|
2. `SGMP_POC` 게임 데이터 객체만 마스킹 동기화 대상으로 선택된다.
|
||||||
|
3. `mvn test`가 통과한다.
|
||||||
|
4. 인증된 `admin`으로 주요 메뉴가 오류 배너 없이 200 응답을 반환하고, Streamlit의 MCP는 Text2SQL 생성 결과를 정상 표기한다.
|
||||||
|
5. 변경 사항은 #703을 참조하는 Git 커밋과 Redmine 작업 로그로 남긴다.
|
||||||
|
|
||||||
|
## 위험 및 완화
|
||||||
|
|
||||||
|
- 과거 KB ORDS API는 게임 데이터 정책을 보장하지 않는다. endpoint 이름만 치환해 기존 API를 재사용하지 않는다.
|
||||||
|
- 운영 VM SSH 키 인증이 거부될 수 있다. 로컬 빌드·공개 URL 확인을 먼저 수행하고, 배포 시에는 승인된 운영 접속 경로를 사용한다.
|
||||||
43
docs/design/704-smilegate-branch-isolation/README.md
Normal file
43
docs/design/704-smilegate-branch-isolation/README.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# 설계서: Smilegate 전용 브랜치 분리
|
||||||
|
|
||||||
|
## 추적성
|
||||||
|
|
||||||
|
- Redmine: #704 `[Release] Smilegate 전용 브랜치 분리`
|
||||||
|
- 관련 이슈: #703 `[Smilegate] 백오피스 잔여 UI 전환 및 운영 검증`
|
||||||
|
- 작업 경로: `/Users/joungminko/claude-workspace/vpd-smilegate-rebrand`
|
||||||
|
- 원격: `https://gittea.cloud-handson.com/joungmin/vpd-permission-poc.git`
|
||||||
|
- 상태: Draft
|
||||||
|
|
||||||
|
## 프로젝트 개요
|
||||||
|
|
||||||
|
`vpd-permission-poc`은 Spring Boot VPD 관리 백오피스를 포함한다. HMM과 Smilegate 데모는 현재 같은 원격 저장소를 사용하지만, 고객별 화면·데이터 모델·배포 기준은 분리돼야 한다.
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
|
||||||
|
Smilegate 작업본을 원격 `smilegate` 브랜치로 분리한다. HMM은 기존 `main` 브랜치를 그대로 사용하고, Smilegate 변경은 `smilegate` 브랜치만 기준으로 커밋·푸시·배포한다.
|
||||||
|
|
||||||
|
## 범위
|
||||||
|
|
||||||
|
1. detached HEAD 상태의 Smilegate worktree에서 `smilegate` 브랜치를 생성한다.
|
||||||
|
2. `origin/smilegate`를 생성하고 현재 worktree의 upstream으로 설정한다.
|
||||||
|
3. #703의 Smilegate 전용 설계서와 UI 변경만 `smilegate` 브랜치에 기록한다.
|
||||||
|
4. HMM 작업본, `origin/main`, 다른 worktree의 파일과 HEAD를 변경하지 않는다.
|
||||||
|
|
||||||
|
## 비범위
|
||||||
|
|
||||||
|
- HMM의 로컬 수정·브랜치·배포 변경
|
||||||
|
- 기존 `main`의 이력 재작성 또는 강제 푸시
|
||||||
|
- 원격 저장소를 새로 생성하거나 삭제하는 작업
|
||||||
|
|
||||||
|
## 검증 기준
|
||||||
|
|
||||||
|
1. `git branch --show-current`은 Smilegate worktree에서 `smilegate`를 반환한다.
|
||||||
|
2. `git rev-parse --abbrev-ref @{u}`는 `origin/smilegate`를 반환한다.
|
||||||
|
3. `origin/main`의 커밋 ID는 분리 전후 동일하다.
|
||||||
|
4. HMM 작업본의 status와 HEAD는 분리 작업으로 변경되지 않는다.
|
||||||
|
|
||||||
|
## 운영 규칙
|
||||||
|
|
||||||
|
- Smilegate 배포는 `origin/smilegate`의 검증된 커밋만 사용한다.
|
||||||
|
- HMM 변경은 `main` 또는 HMM 전용 작업 경로에서만 수행한다.
|
||||||
|
- 공통 기반을 변경해야 하면 두 고객 브랜치에 적용하기 전에 영향 범위를 별도 이슈로 검토한다.
|
||||||
@@ -62,7 +62,7 @@ from src.poc4.scenarios import ScenarioConfigError, load_demo_scenarios
|
|||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
MCP_PROTOCOL_VERSION = "2025-11-25"
|
MCP_PROTOCOL_VERSION = "2025-11-25"
|
||||||
PREFERRED_TOOL = "ords.query.kb_select_ai_vpd"
|
PREFERRED_TOOL = "oracle.select_ai.smilegate_game_text2sql"
|
||||||
DEFAULT_QUESTION = ""
|
DEFAULT_QUESTION = ""
|
||||||
MAX_RESPONSE_BYTES = 1_000_000
|
MAX_RESPONSE_BYTES = 1_000_000
|
||||||
MAX_CONVERSATION_MESSAGES = 8
|
MAX_CONVERSATION_MESSAGES = 8
|
||||||
@@ -77,17 +77,21 @@ DEFAULT_QUERY_MODEL_PROFILE = "gpt54_mini_oci"
|
|||||||
ENV_FILE = ROOT / ".env"
|
ENV_FILE = ROOT / ".env"
|
||||||
MCP_SERVERS_FILE = ROOT / "config" / "mcp_servers.json"
|
MCP_SERVERS_FILE = ROOT / "config" / "mcp_servers.json"
|
||||||
VPD_TOKEN_PRESETS_FILE = ROOT / "config" / "vpd_token_presets.json"
|
VPD_TOKEN_PRESETS_FILE = ROOT / "config" / "vpd_token_presets.json"
|
||||||
DEMO_SCENARIOS_FILE = ROOT / "config" / "hmm_demo_scenarios.json"
|
DEMO_SCENARIOS_FILE = ROOT / "config" / "smilegate_demo_scenarios.json"
|
||||||
APP_PROFILE_FILE = ROOT / "config" / "app_profile.json"
|
APP_PROFILE_FILE = ROOT / "config" / "app_profile.json"
|
||||||
CHAT_DB_FILE = ROOT / "data" / "poc4_mcp_chat.sqlite3"
|
CHAT_DB_FILE = ROOT / "data" / "poc4_mcp_chat.sqlite3"
|
||||||
DEFAULT_VPD_USER_ID = "FC00789"
|
DEFAULT_VPD_USER_ID = "sg-teamlead"
|
||||||
VPD_OPERATIONS_URL = "https://kb.cloud-handson.com/"
|
VPD_OPERATIONS_URL = "https://smilegate-backoffice.cloud-handson.com/"
|
||||||
PORTAL_AUTHENTICATED_KEY = "poc4_portal_authenticated"
|
PORTAL_AUTHENTICATED_KEY = "poc4_portal_authenticated"
|
||||||
PORTAL_AUTH_USER_KEY = "poc4_portal_auth_user"
|
PORTAL_AUTH_USER_KEY = "poc4_portal_auth_user"
|
||||||
PORTAL_LOGIN_FAILURE_KEY = "poc4_portal_login_failed"
|
PORTAL_LOGIN_FAILURE_KEY = "poc4_portal_login_failed"
|
||||||
PORTAL_REMEMBER_TOKEN_PARAM = "poc4_remember"
|
PORTAL_REMEMBER_TOKEN_PARAM = "poc4_remember"
|
||||||
PORTAL_REMEMBER_MAX_AGE_SECONDS = 7 * 24 * 60 * 60
|
PORTAL_REMEMBER_MAX_AGE_SECONDS = 7 * 24 * 60 * 60
|
||||||
AUDIT_SCHEMA = "POC_2"
|
AUDIT_SCHEMA = "SGMP_POC"
|
||||||
|
REFERENCE_EVIDENCE_ENABLED = (
|
||||||
|
os.environ.get("POC4_REFERENCE_EVIDENCE_ENABLED", "").strip().lower()
|
||||||
|
in {"1", "true", "yes"}
|
||||||
|
)
|
||||||
AUDIT_DB_ENV_FILE = Path(
|
AUDIT_DB_ENV_FILE = Path(
|
||||||
os.environ.get("POC4_AUDIT_DB_ENV_FILE", "/home/opc/kbmcp/.env")
|
os.environ.get("POC4_AUDIT_DB_ENV_FILE", "/home/opc/kbmcp/.env")
|
||||||
).expanduser()
|
).expanduser()
|
||||||
@@ -2374,6 +2378,8 @@ def collect_security_evidence(
|
|||||||
) -> Mapping[str, Any]:
|
) -> Mapping[str, Any]:
|
||||||
"""Collect predefined, token-scoped security evidence without exposing secrets."""
|
"""Collect predefined, token-scoped security evidence without exposing secrets."""
|
||||||
|
|
||||||
|
if not REFERENCE_EVIDENCE_ENABLED:
|
||||||
|
return {}
|
||||||
kind = _security_evidence_kind(question)
|
kind = _security_evidence_kind(question)
|
||||||
if not kind or token_preset is None:
|
if not kind or token_preset is None:
|
||||||
return {}
|
return {}
|
||||||
@@ -2938,6 +2944,8 @@ def collect_business_evidence(
|
|||||||
) -> Mapping[str, Any]:
|
) -> Mapping[str, Any]:
|
||||||
"""Collect token-validated, explicitly scoped business and catalog evidence."""
|
"""Collect token-validated, explicitly scoped business and catalog evidence."""
|
||||||
|
|
||||||
|
if not REFERENCE_EVIDENCE_ENABLED:
|
||||||
|
return {}
|
||||||
kind = _business_evidence_kind(question)
|
kind = _business_evidence_kind(question)
|
||||||
if not kind or token_preset is None:
|
if not kind or token_preset is None:
|
||||||
return {}
|
return {}
|
||||||
@@ -7069,8 +7077,8 @@ def main() -> None:
|
|||||||
else:
|
else:
|
||||||
selected_token_preset = None
|
selected_token_preset = None
|
||||||
if configured_mcp_bearer:
|
if configured_mcp_bearer:
|
||||||
# The HMM MCP gateway uses its own server-side credential. VPD user
|
# The Smilegate MCP gateway uses a server-side credential. UI-only
|
||||||
# tokens remain UI context only and must never be forwarded to it.
|
# context must never be forwarded as a second bearer token.
|
||||||
st.caption("MCP 인증: 서버 관리 토큰 적용")
|
st.caption("MCP 인증: 서버 관리 토큰 적용")
|
||||||
manual_bearer_token = ""
|
manual_bearer_token = ""
|
||||||
elif selected_token_preset is not None:
|
elif selected_token_preset is not None:
|
||||||
@@ -7256,9 +7264,7 @@ def main() -> None:
|
|||||||
'<div id="kb-main-tabs-anchor" style="scroll-margin-top: 0.75rem;"></div>',
|
'<div id="kb-main-tabs-anchor" style="scroll-margin-top: 0.75rem;"></div>',
|
||||||
unsafe_allow_html=True,
|
unsafe_allow_html=True,
|
||||||
)
|
)
|
||||||
architecture_tab, scenario_tab, audit_tab, operations_tab = st.tabs(
|
architecture_tab, scenario_tab = st.tabs(["아키텍처", "시나리오"])
|
||||||
["아키텍처", "시나리오", "감사로그", "보안관리"]
|
|
||||||
)
|
|
||||||
with architecture_tab:
|
with architecture_tab:
|
||||||
_render_architecture_tab()
|
_render_architecture_tab()
|
||||||
|
|
||||||
@@ -7359,12 +7365,6 @@ def main() -> None:
|
|||||||
height=0,
|
height=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
with audit_tab:
|
|
||||||
_render_fga_audit_tab()
|
|
||||||
|
|
||||||
with operations_tab:
|
|
||||||
_render_vpd_operations_tab()
|
|
||||||
|
|
||||||
if not submitted:
|
if not submitted:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"product": {
|
"product": {
|
||||||
"name": "AI 업무 에이전트",
|
"name": "SMILEGATE DATA & AI POC",
|
||||||
"short_name": "AGENT",
|
"short_name": "SMILEGATE",
|
||||||
"page_title": "AI 업무 에이전트",
|
"page_title": "SMILEGATE DATA & AI POC",
|
||||||
"page_icon": "🤖",
|
"page_icon": "🤖",
|
||||||
"header_title": "AI 업무 에이전트",
|
"header_title": "스마일게이트 게임 데이터 AI 에이전트",
|
||||||
"header_description": "사용자 권한에 맞는 업무 질의와 보안 관리 기능을 제공합니다.",
|
"header_description": "게임 로그·서비스 데이터를 기반으로 AI 업무 효율화와 데이터 플랫폼 활용 방식을 검증합니다.",
|
||||||
"login_kicker": "DATA & AI DEMO",
|
"login_kicker": "SMILEGATE DATA & AI POC",
|
||||||
"login_title": "AI 업무 에이전트",
|
"login_title": "스마일게이트 게임 데이터 AI 에이전트",
|
||||||
"login_description": "사용자 인증 후 업무 질의와 보안 관리 기능을 이용할 수 있습니다.",
|
"login_description": "사용자 인증 후 게임 데이터 AI 질의와 보안 관리 기능을 이용할 수 있습니다.",
|
||||||
"login_footer": "인증된 DEMO 사용자만 접근할 수 있습니다."
|
"login_footer": "승인된 Data & AI PoC 사용자만 접근할 수 있습니다."
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"primary_color": "#003b70",
|
"primary_color": "#113F67",
|
||||||
"text_color": "#172b3a",
|
"text_color": "#15283B",
|
||||||
"muted_color": "#667785",
|
"muted_color": "#5D6C7C",
|
||||||
"border_color": "#dfe7ed"
|
"border_color": "#D7E0E8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
{
|
{
|
||||||
"default_server_id": "hmm_hr_mcp",
|
"default_server_id": "smilegate_game_data_mcp",
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
"id": "hmm_hr_mcp",
|
"id": "smilegate_game_data_mcp",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"provider": "hmm_compat_mcp",
|
"provider": "smilegate_select_ai_mcp",
|
||||||
"transport": "http",
|
"transport": "http",
|
||||||
"endpoint_url": "https://hmm-mcp.cloud-handson.com/mcp",
|
"endpoint_url": "https://smilegate-backoffice.cloud-handson.com/mcp",
|
||||||
"auth_token_env": "HMM_MCP_BEARER_TOKEN",
|
"auth_token_env": "SMILEGATE_MCP_BEARER_TOKEN",
|
||||||
"timeout_seconds_env": "POC3_MCP_TIMEOUT_SECONDS",
|
"timeout_seconds_env": "POC3_MCP_TIMEOUT_SECONDS",
|
||||||
"default_tool": "search_hr_data",
|
"default_tool": "oracle.select_ai.smilegate_game_text2sql",
|
||||||
"router_model_profile": "gpt54_mini_oci",
|
"router_model_profile": "gpt54_mini_oci",
|
||||||
"tool_allowlist": [
|
"tool_allowlist": [
|
||||||
"search_hr_data",
|
"oracle.select_ai.smilegate_game_text2sql"
|
||||||
"resolve_hr_term",
|
|
||||||
"search_hr_policy"
|
|
||||||
],
|
],
|
||||||
"description": "HMM HR knowledge and data MCP server"
|
"description": "Smilegate game-data Text2SQL MCP server"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"description": "Smilegate Data & AI PoC 화면에 표시할 게임 데이터 질의 샘플입니다.",
|
||||||
|
"scenarios": [
|
||||||
|
{
|
||||||
|
"id": "GAME-01",
|
||||||
|
"enabled": true,
|
||||||
|
"category": "활성 사용자",
|
||||||
|
"title": "카제나 최신 AU",
|
||||||
|
"question": "카제나 최신 기준 활성 사용자 수(AU)를 알려줘"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "GAME-02",
|
||||||
|
"enabled": true,
|
||||||
|
"category": "매출",
|
||||||
|
"title": "게임별 판매 현황",
|
||||||
|
"question": "최신 기준 게임별 판매 건수와 판매 금액을 보여줘"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "GAME-03",
|
||||||
|
"enabled": true,
|
||||||
|
"category": "환불",
|
||||||
|
"title": "최근 환불 현황",
|
||||||
|
"question": "최신 기준 게임별 환불 건수와 환불 금액을 보여줘"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "GAME-04",
|
||||||
|
"enabled": true,
|
||||||
|
"category": "게임·서버",
|
||||||
|
"title": "게임 서버 구성",
|
||||||
|
"question": "등록된 게임과 게임 서버 정보를 보여줘"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "GAME-05",
|
||||||
|
"enabled": true,
|
||||||
|
"category": "사용자 분석",
|
||||||
|
"title": "신규 사용자 현황",
|
||||||
|
"question": "최신 월 기준 게임별 신규 사용자 수를 보여줘"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"presets": [
|
"presets": [
|
||||||
{
|
{
|
||||||
"enabled": true,
|
"enabled": false,
|
||||||
"default": true,
|
"default": true,
|
||||||
"token": "vpd_live_REPLACE_WITH_USER_TOKEN",
|
"token": "vpd_live_REPLACE_WITH_USER_TOKEN",
|
||||||
"user_id": "FC00789",
|
"user_id": "FC00789",
|
||||||
|
|||||||
@@ -16,29 +16,29 @@ class DemoScenarioConfigTest(unittest.TestCase):
|
|||||||
with patch.dict(
|
with patch.dict(
|
||||||
"os.environ",
|
"os.environ",
|
||||||
{
|
{
|
||||||
"AGENT_CONSOLE_SHORT_NAME": "HMM",
|
"AGENT_CONSOLE_SHORT_NAME": "SMILEGATE",
|
||||||
"AGENT_CONSOLE_PAGE_TITLE": "HMM AI 업무 에이전트",
|
"AGENT_CONSOLE_PAGE_TITLE": "SMILEGATE DATA & AI POC",
|
||||||
"AGENT_CONSOLE_PRIMARY_COLOR": "#003b70",
|
"AGENT_CONSOLE_PRIMARY_COLOR": "#113F67",
|
||||||
},
|
},
|
||||||
clear=False,
|
clear=False,
|
||||||
):
|
):
|
||||||
profile = load_app_profile(path)
|
profile = load_app_profile(path)
|
||||||
|
|
||||||
self.assertEqual(profile.short_name, "HMM")
|
self.assertEqual(profile.short_name, "SMILEGATE")
|
||||||
self.assertEqual(profile.page_title, "HMM AI 업무 에이전트")
|
self.assertEqual(profile.page_title, "SMILEGATE DATA & AI POC")
|
||||||
self.assertEqual(profile.primary_color, "#003b70")
|
self.assertEqual(profile.primary_color, "#113F67")
|
||||||
|
|
||||||
def test_profile_reads_dotenv_values(self) -> None:
|
def test_profile_reads_dotenv_values(self) -> None:
|
||||||
path = Path(__file__).parents[1] / "config" / "app_profile.json"
|
path = Path(__file__).parents[1] / "config" / "app_profile.json"
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
env_file = Path(temp_dir) / ".env"
|
env_file = Path(temp_dir) / ".env"
|
||||||
env_file.write_text("AGENT_CONSOLE_SHORT_NAME=HMM\n", encoding="utf-8")
|
env_file.write_text("AGENT_CONSOLE_SHORT_NAME=SMILEGATE\n", encoding="utf-8")
|
||||||
profile = load_app_profile(path, env_file)
|
profile = load_app_profile(path, env_file)
|
||||||
|
|
||||||
self.assertEqual(profile.short_name, "HMM")
|
self.assertEqual(profile.short_name, "SMILEGATE")
|
||||||
|
|
||||||
def test_hmm_scenarios_are_enabled_and_unique(self) -> None:
|
def test_smilegate_scenarios_are_enabled_and_unique(self) -> None:
|
||||||
path = Path(__file__).parents[1] / "config" / "hmm_demo_scenarios.json"
|
path = Path(__file__).parents[1] / "config" / "smilegate_demo_scenarios.json"
|
||||||
scenarios = load_demo_scenarios(path)
|
scenarios = load_demo_scenarios(path)
|
||||||
|
|
||||||
self.assertGreaterEqual(len(scenarios), 3)
|
self.assertGreaterEqual(len(scenarios), 3)
|
||||||
|
|||||||
7
pom.xml
7
pom.xml
@@ -98,11 +98,8 @@
|
|||||||
<directory>sql/adb</directory>
|
<directory>sql/adb</directory>
|
||||||
<targetPath>sql/adb</targetPath>
|
<targetPath>sql/adb</targetPath>
|
||||||
<includes>
|
<includes>
|
||||||
<include>62_kb_aso_masking_backoffice_metadata.sql</include>
|
<include>70_sg_tool_user.sql</include>
|
||||||
<include>63_kb_aso_masking_rule_runtime.sql</include>
|
<include>71_sg_identity_administration.sql</include>
|
||||||
<include>64_kb_aso_masking_default_column_rules.sql</include>
|
|
||||||
<include>65_kb_select_ai_vpd_query_api.sql</include>
|
|
||||||
<include>66_kb_select_ai_vpd_query_ords.sql</include>
|
|
||||||
</includes>
|
</includes>
|
||||||
</resource>
|
</resource>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ public record BackofficeProperties(
|
|||||||
Security security,
|
Security security,
|
||||||
Token token,
|
Token token,
|
||||||
Ords ords,
|
Ords ords,
|
||||||
Ai ai
|
Ai ai,
|
||||||
|
SelectAi selectAi
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public record Security(
|
public record Security(
|
||||||
@@ -72,4 +73,20 @@ public record BackofficeProperties(
|
|||||||
this(enabled, "openai", baseUrl, model, apiKey, timeout, "", "", "", "", "");
|
this(enabled, "openai", baseUrl, model, apiKey, timeout, "", "", "", "", "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Separate ADB connection because Select AI profiles are owned by SGMP_POC. */
|
||||||
|
public record SelectAi(
|
||||||
|
String dbUrl,
|
||||||
|
String dbUsername,
|
||||||
|
String dbPassword,
|
||||||
|
String profile
|
||||||
|
) {
|
||||||
|
|
||||||
|
public boolean configured() {
|
||||||
|
return dbUrl != null && !dbUrl.isBlank()
|
||||||
|
&& dbUsername != null && !dbUsername.isBlank()
|
||||||
|
&& dbPassword != null && !dbPassword.isBlank()
|
||||||
|
&& profile != null && !profile.isBlank();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,6 @@ public class DbPoolWarmup {
|
|||||||
groupService.findGroupRoles();
|
groupService.findGroupRoles();
|
||||||
permissionService.findRoles();
|
permissionService.findRoles();
|
||||||
permissionService.findPermissionViews();
|
permissionService.findPermissionViews();
|
||||||
log.info("HMM identity catalog cache warmed up in {}ms", (System.nanoTime() - started) / 1_000_000);
|
log.info("Smilegate identity catalog cache warmed up in {}ms", (System.nanoTime() - started) / 1_000_000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ public enum MaskingTemplate {
|
|||||||
"A******** (예시)"),
|
"A******** (예시)"),
|
||||||
RRN_PARTIAL(
|
RRN_PARTIAL(
|
||||||
"RRN_PARTIAL",
|
"RRN_PARTIAL",
|
||||||
"주민등록번호 부분 마스킹",
|
"식별번호 부분 마스킹",
|
||||||
"앞 6자리만 표시하고 나머지는 가리는 사전 정의 식별번호 규칙입니다.",
|
"앞 6자리만 표시하고 나머지를 가리는 사전 정의 식별번호 규칙입니다.",
|
||||||
"DBMS_REDACT.REGEXP",
|
"DBMS_REDACT.REGEXP",
|
||||||
"900101-******* (예시)");
|
"123456-******* (예시)");
|
||||||
|
|
||||||
private final String code;
|
private final String code;
|
||||||
private final String label;
|
private final String label;
|
||||||
|
|||||||
@@ -50,16 +50,16 @@ public record PermissionView(
|
|||||||
return "토큰으로 식별된 이해관계자 본인 행";
|
return "토큰으로 식별된 이해관계자 본인 행";
|
||||||
}
|
}
|
||||||
if (upper.contains(" OWN_CONTRACT")) {
|
if (upper.contains(" OWN_CONTRACT")) {
|
||||||
return "담당 설계사 본인 계약";
|
return "담당 게임 서비스 범위";
|
||||||
}
|
}
|
||||||
if (upper.contains(" CHANNEL_CONTRACT")) {
|
if (upper.contains(" CHANNEL_CONTRACT")) {
|
||||||
return "토큰 사용자의 채널 계약";
|
return "토큰 채널 게임 서비스 범위";
|
||||||
}
|
}
|
||||||
if (upper.contains(" OWN_CUSTOMER")) {
|
if (upper.contains(" OWN_CUSTOMER")) {
|
||||||
return "담당 설계사 본인 계약에 연결된 고객/청구/외부보유";
|
return "담당 게임 사용자·거래 데이터 범위";
|
||||||
}
|
}
|
||||||
if (upper.contains(" CHANNEL_CUSTOMER")) {
|
if (upper.contains(" CHANNEL_CUSTOMER")) {
|
||||||
return "토큰 사용자 채널 계약에 연결된 고객/청구/외부보유";
|
return "토큰 채널 게임 사용자·거래 데이터 범위";
|
||||||
}
|
}
|
||||||
if (upper.contains(" STATIC_SQL ")) {
|
if (upper.contains(" STATIC_SQL ")) {
|
||||||
return "정적 SQL 조건: " + rawRule.replaceFirst("(?i)^\\s*STATIC_SQL\\s+", "");
|
return "정적 SQL 조건: " + rawRule.replaceFirst("(?i)^\\s*STATIC_SQL\\s+", "");
|
||||||
|
|||||||
@@ -258,8 +258,8 @@ public class BackofficeSchemaService {
|
|||||||
"문자형은 공백, 숫자형은 0으로 반환하는 전체 마스킹 방식"),
|
"문자형은 공백, 숫자형은 0으로 반환하는 전체 마스킹 방식"),
|
||||||
new MaskingRuleSeed("MASK_TEXT_PARTIAL", "문자열 일부 마스킹", "TEXT_PARTIAL",
|
new MaskingRuleSeed("MASK_TEXT_PARTIAL", "문자열 일부 마스킹", "TEXT_PARTIAL",
|
||||||
"첫 글자만 보이고 나머지는 가리는 문자열 마스킹 방식"),
|
"첫 글자만 보이고 나머지는 가리는 문자열 마스킹 방식"),
|
||||||
new MaskingRuleSeed("MASK_RRN_PARTIAL", "주민등록번호 부분 마스킹", "RRN_PARTIAL",
|
new MaskingRuleSeed("MASK_IDENTIFIER_PARTIAL", "식별번호 부분 마스킹", "RRN_PARTIAL",
|
||||||
"앞 6자리만 보이고 나머지는 가리는 식별번호 마스킹 방식")
|
"앞 6자리만 보이고 나머지는 가리는 게임 사용자 식별번호 마스킹 방식")
|
||||||
);
|
);
|
||||||
|
|
||||||
private static final String MASKING_RULE_SEED_SQL = """
|
private static final String MASKING_RULE_SEED_SQL = """
|
||||||
@@ -705,7 +705,7 @@ public class BackofficeSchemaService {
|
|||||||
@sql/adb/17_agent_ords_security_local_vpd_setup.sql
|
@sql/adb/17_agent_ords_security_local_vpd_setup.sql
|
||||||
@sql/adb/25_agent_ords_security_backoffice_support.sql
|
@sql/adb/25_agent_ords_security_backoffice_support.sql
|
||||||
@sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql
|
@sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql
|
||||||
@sql/adb/62_kb_aso_masking_backoffice_metadata.sql
|
@sql/adb/71_sg_identity_administration.sql
|
||||||
@sql/adb/21_agent_ords_security_ords_enable_schema.sql
|
@sql/adb/21_agent_ords_security_ords_enable_schema.sql
|
||||||
|
|
||||||
-- 2. ORDS parsing schema로 접속
|
-- 2. ORDS parsing schema로 접속
|
||||||
@@ -717,9 +717,8 @@ public class BackofficeSchemaService {
|
|||||||
GRANT EXECUTE ON cb_agent_ctx_pkg TO cb_ords;
|
GRANT EXECUTE ON cb_agent_ctx_pkg TO cb_ords;
|
||||||
GRANT SELECT ON <owner>.<table_or_view> TO cb_ords;
|
GRANT SELECT ON <owner>.<table_or_view> TO cb_ords;
|
||||||
|
|
||||||
-- 4. 마스킹 규칙을 UI에서 컬럼에 연결한 뒤 실행
|
-- 4. 마스킹 규칙을 UI에서 게임 데이터 컬럼에 연결
|
||||||
@sql/adb/64_kb_aso_masking_default_column_rules.sql
|
-- DBMS_REDACT 정책은 백오피스가 SGMP_POC 대상에 자동 동기화합니다.
|
||||||
@sql/adb/63_kb_aso_masking_rule_runtime.sql
|
|
||||||
""".formatted(owner.toLowerCase());
|
""".formatted(owner.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import org.springframework.stereotype.Service;
|
|||||||
@Service
|
@Service
|
||||||
public class MaskingPolicySynchronizer {
|
public class MaskingPolicySynchronizer {
|
||||||
|
|
||||||
private static final String OWNER = "POC_2";
|
private static final String OWNER = "SGMP_POC";
|
||||||
private static final Pattern COLUMN_NAME = Pattern.compile("[A-Z][A-Z0-9_$#]{0,127}");
|
private static final Pattern COLUMN_NAME = Pattern.compile("[A-Z][A-Z0-9_$#]{0,127}");
|
||||||
private static final Map<String, String> MANAGED_POLICIES = managedPolicyMap();
|
private static final Map<String, String> MANAGED_POLICIES = managedPolicyMap();
|
||||||
|
|
||||||
@@ -38,10 +38,10 @@ public class MaskingPolicySynchronizer {
|
|||||||
|
|
||||||
private static Map<String, String> managedPolicyMap() {
|
private static Map<String, String> managedPolicyMap() {
|
||||||
Map<String, String> policies = new LinkedHashMap<>();
|
Map<String, String> policies = new LinkedHashMap<>();
|
||||||
policies.put("KB_CUSTOMERS", "KB_CUSTOMER_PII_REDACT");
|
policies.put("CZN_COMN_USER_MST", "SG_CZN_USER_REDACT");
|
||||||
policies.put("KB_CLAIMS", "KB_CLAIM_AMOUNT_REDACT");
|
policies.put("COMN_SALES_USER_MST", "SG_SALES_USER_REDACT");
|
||||||
policies.put("KB_CONTRACTS", "KB_CONTRACT_PREMIUM_REDACT");
|
policies.put("COMN_SALES_TXN", "SG_SALES_TXN_REDACT");
|
||||||
policies.put("KB_EXTERNAL_HOLDINGS", "KB_EXT_HOLDING_REDACT");
|
policies.put("COMN_REFUND_TXN", "SG_REFUND_TXN_REDACT");
|
||||||
return Collections.unmodifiableMap(policies);
|
return Collections.unmodifiableMap(policies);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public class MaskingRuleService {
|
|||||||
return mapper.findColumnRules();
|
return mapper.findColumnRules();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reads the actual Oracle Data Redaction state for the three managed KB objects. */
|
/** Reads the actual Oracle Data Redaction state for the managed Smilegate game-data objects. */
|
||||||
public List<MaskingPolicyStatus> findPolicyStatuses() {
|
public List<MaskingPolicyStatus> findPolicyStatuses() {
|
||||||
return mapper.findPolicyStatuses();
|
return mapper.findPolicyStatuses();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,30 +8,29 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
/** MCP boundary exposing only the row-access-aware GPT-5.4-mini Select AI query tool. */
|
/** MCP boundary exposing the Smilegate game-data Select AI SHOWSQL tool. */
|
||||||
@Service
|
@Service
|
||||||
public class McpSseService {
|
public class McpSseService {
|
||||||
|
|
||||||
private static final String SELECT_AI_VPD_QUERY_TOOL = "ords.query.kb_select_ai_vpd";
|
private static final String SELECT_AI_VPD_QUERY_TOOL = "oracle.select_ai.smilegate_game_text2sql";
|
||||||
private static final String SELECT_AI_VPD_QUERY_PATH = "cb-ords/kb-select-ai-vpd/query";
|
private static final String SELECT_AI_VPD_QUERY_PATH = "/mcp (tools/call)";
|
||||||
private static final String SELECT_AI_VPD_QUERY_PROFILE =
|
private static final String SELECT_AI_VPD_QUERY_PROFILE = "SGMP_POC_HAIKU45";
|
||||||
"KB_AIDP_SELECTAI_GPT54_MINI_FULLMETA_PROFILE_V1";
|
|
||||||
private static final McpToolView SELECT_AI_VPD_QUERY_VIEW = new McpToolView(
|
private static final McpToolView SELECT_AI_VPD_QUERY_VIEW = new McpToolView(
|
||||||
SELECT_AI_VPD_QUERY_TOOL,
|
SELECT_AI_VPD_QUERY_TOOL,
|
||||||
"GPT-5.4-mini Select AI 자연어 질의를 행 접근 컨텍스트로 실행합니다. 테이블/컬럼 comment, annotation, constraint 메타데이터를 사용하고 생성 SQL은 KB 업무 테이블의 읽기 전용 SELECT/WITH만 허용합니다.",
|
"SGMP_POC_HAIKU45 프로파일로 게임 로그·서비스 데이터용 읽기 전용 SELECT/WITH SQL을 생성합니다. 생성 SQL은 자동 실행하지 않으며 테이블·컬럼 comment, annotation, constraint를 참고합니다.",
|
||||||
-1L,
|
-1L,
|
||||||
"KB Select AI 행 접근 자연어 조회",
|
"Smilegate 게임 데이터 Text2SQL",
|
||||||
SELECT_AI_VPD_QUERY_PATH
|
SELECT_AI_VPD_QUERY_PATH
|
||||||
);
|
);
|
||||||
|
|
||||||
private final SelectAiAgentOrdsService selectAiAgentOrdsService;
|
private final SmilegateSelectAiService smilegateSelectAiService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public McpSseService(
|
public McpSseService(
|
||||||
SelectAiAgentOrdsService selectAiAgentOrdsService,
|
SmilegateSelectAiService smilegateSelectAiService,
|
||||||
ObjectMapper objectMapper
|
ObjectMapper objectMapper
|
||||||
) {
|
) {
|
||||||
this.selectAiAgentOrdsService = selectAiAgentOrdsService;
|
this.smilegateSelectAiService = smilegateSelectAiService;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,17 +105,10 @@ public class McpSseService {
|
|||||||
|
|
||||||
ObjectNode prompt = objectMapper.createObjectNode();
|
ObjectNode prompt = objectMapper.createObjectNode();
|
||||||
prompt.put("type", "string");
|
prompt.put("type", "string");
|
||||||
prompt.put("description", "KB 업무 원장에 대해 조회할 내용을 자연어로 입력합니다.");
|
prompt.put("description", "Smilegate 게임 로그·서비스 데이터에 대해 조회할 내용을 자연어로 입력합니다.");
|
||||||
prompt.put("maxLength", 4000);
|
prompt.put("maxLength", 4000);
|
||||||
properties.set("prompt", prompt);
|
properties.set("prompt", prompt);
|
||||||
|
|
||||||
ObjectNode limit = objectMapper.createObjectNode();
|
|
||||||
limit.put("type", "integer");
|
|
||||||
limit.put("description", "최대 반환 행 수. 1부터 100까지 허용하며 기본값은 50입니다.");
|
|
||||||
limit.put("minimum", 1);
|
|
||||||
limit.put("maximum", 100);
|
|
||||||
properties.set("limit", limit);
|
|
||||||
|
|
||||||
schema.set("properties", properties);
|
schema.set("properties", properties);
|
||||||
ArrayNode required = objectMapper.createArrayNode();
|
ArrayNode required = objectMapper.createArrayNode();
|
||||||
required.add("prompt");
|
required.add("prompt");
|
||||||
@@ -139,11 +131,7 @@ public class McpSseService {
|
|||||||
}
|
}
|
||||||
JsonNode response;
|
JsonNode response;
|
||||||
try {
|
try {
|
||||||
response = selectAiAgentOrdsService.run(
|
response = smilegateSelectAiService.generateShowSql(token, arguments.path("prompt").asText(""));
|
||||||
token,
|
|
||||||
arguments.path("prompt").asText(""),
|
|
||||||
normalizeLimit(arguments.path("limit").asInt(50))
|
|
||||||
);
|
|
||||||
} catch (VpdTokenAccessDeniedException ignored) {
|
} catch (VpdTokenAccessDeniedException ignored) {
|
||||||
return tokenAccessDeniedResult();
|
return tokenAccessDeniedResult();
|
||||||
}
|
}
|
||||||
@@ -181,13 +169,6 @@ public class McpSseService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int normalizeLimit(int limit) {
|
|
||||||
if (limit < 1) {
|
|
||||||
return 50;
|
|
||||||
}
|
|
||||||
return Math.min(limit, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String pretty(Object value) {
|
private String pretty(Object value) {
|
||||||
try {
|
try {
|
||||||
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);
|
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);
|
||||||
|
|||||||
@@ -643,7 +643,7 @@ public class OrdsProbeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// APEX_JSON omits a ref-cursor column whose value is NULL. The KB
|
// APEX_JSON omits a ref-cursor column whose value is NULL. The
|
||||||
// object handlers always select every registered protected column, so
|
// object handlers always select every registered protected column, so
|
||||||
// a sensitive column missing from every JSON row is also a NULL result
|
// a sensitive column missing from every JSON row is also a NULL result
|
||||||
// and must be reported as masked to the MCP/UI caller.
|
// and must be reported as masked to the MCP/UI caller.
|
||||||
|
|||||||
@@ -19,39 +19,18 @@ public class SecuritySqlScriptService {
|
|||||||
|
|
||||||
private static final List<ScriptDefinition> CURATED_SCRIPTS = List.of(
|
private static final List<ScriptDefinition> CURATED_SCRIPTS = List.of(
|
||||||
new ScriptDefinition(
|
new ScriptDefinition(
|
||||||
"aso-masking-metadata",
|
"smilegate-tool-users",
|
||||||
"ASO / 마스킹",
|
"Smilegate 사용자",
|
||||||
"62_kb_aso_masking_backoffice_metadata.sql",
|
"70_sg_tool_user.sql",
|
||||||
"컬럼 마스킹 규칙 메타데이터",
|
"PoC 도구 사용자 초기 데이터",
|
||||||
"ASO 컬럼 마스킹 규칙·컬럼 연결·사용자 예외를 관리하는 백오피스 메타데이터를 생성합니다."
|
"Data & AI TF 팀장·팀원 데모 사용자와 역할을 생성합니다. 게임 서비스 사용자가 아닌 PoC 도구 운영 사용자입니다."
|
||||||
),
|
),
|
||||||
new ScriptDefinition(
|
new ScriptDefinition(
|
||||||
"aso-masking-runtime",
|
"smilegate-identity-administration",
|
||||||
"ASO / 마스킹",
|
"Smilegate 권한",
|
||||||
"63_kb_aso_masking_rule_runtime.sql",
|
"71_sg_identity_administration.sql",
|
||||||
"ASO 마스킹 런타임 적용",
|
"사용자·그룹·역할 관리 모델",
|
||||||
"백오피스 컬럼 마스킹 규칙을 Oracle Data Redaction 정책과 신뢰 컨텍스트에 반영합니다."
|
"Smilegate PoC 운영 사용자, 그룹, 역할, 권한 메타데이터와 백오피스 호환 뷰를 생성합니다."
|
||||||
),
|
|
||||||
new ScriptDefinition(
|
|
||||||
"aso-masking-default-columns",
|
|
||||||
"ASO / 마스킹",
|
|
||||||
"64_kb_aso_masking_default_column_rules.sql",
|
|
||||||
"ASO 기본 대상 컬럼",
|
|
||||||
"주민번호·청구/지급금·타사보유 컬럼의 마스킹 블랙리스트 초기값을 연결합니다."
|
|
||||||
),
|
|
||||||
new ScriptDefinition(
|
|
||||||
"select-ai-vpd-api",
|
|
||||||
"Select AI / 행 접근",
|
|
||||||
"65_kb_select_ai_vpd_query_api.sql",
|
|
||||||
"행 접근 적용 Select AI 조회 API",
|
|
||||||
"생성 SQL을 KB 업무 테이블의 단일 읽기 전용 SELECT/WITH로 검증해 행 접근 컨텍스트에서 실행합니다."
|
|
||||||
),
|
|
||||||
new ScriptDefinition(
|
|
||||||
"select-ai-vpd-ords",
|
|
||||||
"ORDS / Select AI",
|
|
||||||
"66_kb_select_ai_vpd_query_ords.sql",
|
|
||||||
"Select AI 행 접근 ORDS Endpoint",
|
|
||||||
"Bearer 토큰을 검증해 행 접근 컨텍스트를 설정한 뒤 Select AI 조회 API를 노출합니다."
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
package com.cloudhandson.vpdbackoffice.service;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.util.UUID;
|
|
||||||
import org.springframework.http.HttpEntity;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpMethod;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.web.client.HttpStatusCodeException;
|
|
||||||
import org.springframework.web.client.ResourceAccessException;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
|
||||||
|
|
||||||
/** Calls the ORDS boundary; the database endpoint owns bearer-to-row-access-context mapping. */
|
|
||||||
@Service
|
|
||||||
public class SelectAiAgentOrdsService {
|
|
||||||
|
|
||||||
static final String ORDS_PATH = "/cb-ords/kb-select-ai-vpd/query";
|
|
||||||
private static final int MAX_PROMPT_LENGTH = 4_000;
|
|
||||||
private static final int MAX_LIMIT = 100;
|
|
||||||
|
|
||||||
private final SettingService settingService;
|
|
||||||
private final RestTemplate restTemplate;
|
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
public SelectAiAgentOrdsService(
|
|
||||||
SettingService settingService,
|
|
||||||
RestTemplate ordsAgentRestTemplate,
|
|
||||||
ObjectMapper objectMapper
|
|
||||||
) {
|
|
||||||
this.settingService = settingService;
|
|
||||||
this.restTemplate = ordsAgentRestTemplate;
|
|
||||||
this.objectMapper = objectMapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compatibility overload for callers of the former SQL-generation tool.
|
|
||||||
* The row-access query endpoint is stateless and therefore ignores conversationId.
|
|
||||||
*/
|
|
||||||
public JsonNode run(String bearerToken, String prompt, String conversationId) {
|
|
||||||
return run(bearerToken, prompt, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsonNode run(String bearerToken, String prompt, int limit) {
|
|
||||||
String normalizedToken = required(bearerToken, "bearerToken");
|
|
||||||
String normalizedPrompt = required(prompt, "prompt");
|
|
||||||
if (normalizedPrompt.length() > MAX_PROMPT_LENGTH) {
|
|
||||||
throw new AppException("prompt는 " + MAX_PROMPT_LENGTH + "자 이하여야 합니다.");
|
|
||||||
}
|
|
||||||
int normalizedLimit = normalizeLimit(limit);
|
|
||||||
String baseUrl = settingService.ordsBaseUrl();
|
|
||||||
if (baseUrl == null || baseUrl.isBlank()) {
|
|
||||||
throw new AppException("ORDS base URL이 설정되지 않았습니다.");
|
|
||||||
}
|
|
||||||
|
|
||||||
ObjectNode requestBody = objectMapper.createObjectNode();
|
|
||||||
requestBody.put("prompt", normalizedPrompt);
|
|
||||||
requestBody.put("limit", normalizedLimit);
|
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.setBearerAuth(normalizedToken);
|
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
|
||||||
headers.set("X-VPD-Probe-Id", UUID.randomUUID().toString());
|
|
||||||
|
|
||||||
try {
|
|
||||||
ResponseEntity<String> response = restTemplate.exchange(
|
|
||||||
endpoint(baseUrl),
|
|
||||||
HttpMethod.POST,
|
|
||||||
new HttpEntity<>(requestBody.toString(), headers),
|
|
||||||
String.class
|
|
||||||
);
|
|
||||||
JsonNode body = parse(response.getBody());
|
|
||||||
if (body.hasNonNull("error")) {
|
|
||||||
throw new AppException("Select AI 행 접근 ORDS 오류: " + body.path("error").asText());
|
|
||||||
}
|
|
||||||
return body;
|
|
||||||
} catch (HttpStatusCodeException e) {
|
|
||||||
if (e.getStatusCode().isSameCodeAs(HttpStatus.UNAUTHORIZED)
|
|
||||||
|| e.getStatusCode().isSameCodeAs(HttpStatus.FORBIDDEN)) {
|
|
||||||
throw new VpdTokenAccessDeniedException();
|
|
||||||
}
|
|
||||||
throw new AppException("Select AI 행 접근 ORDS HTTP " + e.getStatusCode().value()
|
|
||||||
+ ": " + responseError(e.getResponseBodyAsString()));
|
|
||||||
} catch (ResourceAccessException e) {
|
|
||||||
throw new AppException("Select AI 행 접근 ORDS 연결 또는 응답 시간 초과: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private URI endpoint(String baseUrl) {
|
|
||||||
return UriComponentsBuilder.fromUriString(baseUrl)
|
|
||||||
.path(ORDS_PATH)
|
|
||||||
.build()
|
|
||||||
.toUri();
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonNode parse(String value) {
|
|
||||||
try {
|
|
||||||
if (value == null || value.isBlank()) {
|
|
||||||
throw new AppException("Select AI 행 접근 ORDS 응답 본문이 비어 있습니다.");
|
|
||||||
}
|
|
||||||
return objectMapper.readTree(value);
|
|
||||||
} catch (AppException e) {
|
|
||||||
throw e;
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new AppException("Select AI 행 접근 ORDS 응답 JSON 파싱 실패: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String responseError(String body) {
|
|
||||||
try {
|
|
||||||
JsonNode parsed = objectMapper.readTree(body);
|
|
||||||
return parsed.path("error").asText(body == null ? "" : body);
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
return body == null ? "" : body;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String required(String value, String name) {
|
|
||||||
if (value == null || value.isBlank()) {
|
|
||||||
throw new AppException(name + "은(는) 필수입니다.");
|
|
||||||
}
|
|
||||||
return value.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int normalizeLimit(int value) {
|
|
||||||
if (value < 1) {
|
|
||||||
return 50;
|
|
||||||
}
|
|
||||||
return Math.min(value, MAX_LIMIT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package com.cloudhandson.vpdbackoffice.service;
|
||||||
|
|
||||||
|
import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
|
||||||
|
import com.cloudhandson.vpdbackoffice.domain.token.BearerTokenRecord;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates reviewed read-only SQL through the schema-owned Smilegate Select AI profile.
|
||||||
|
* The generated SQL is never executed by this service.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class SmilegateSelectAiService {
|
||||||
|
|
||||||
|
private static final int MAX_PROMPT_LENGTH = 4_000;
|
||||||
|
private static final String ACTION_SHOWSQL = "showsql";
|
||||||
|
|
||||||
|
private final BackofficeProperties properties;
|
||||||
|
private final BearerTokenService bearerTokenService;
|
||||||
|
private final Clock clock;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public SmilegateSelectAiService(
|
||||||
|
BackofficeProperties properties,
|
||||||
|
BearerTokenService bearerTokenService,
|
||||||
|
Clock clock,
|
||||||
|
ObjectMapper objectMapper
|
||||||
|
) {
|
||||||
|
this.properties = properties;
|
||||||
|
this.bearerTokenService = bearerTokenService;
|
||||||
|
this.clock = clock;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonNode generateShowSql(String bearerToken, String prompt) {
|
||||||
|
requireActiveToken(bearerToken);
|
||||||
|
String normalizedPrompt = requiredPrompt(prompt);
|
||||||
|
BackofficeProperties.SelectAi selectAi = properties == null ? null : properties.selectAi();
|
||||||
|
if (selectAi == null || !selectAi.configured()) {
|
||||||
|
throw new AppException("Smilegate Select AI 연결 설정이 필요합니다. "
|
||||||
|
+ "BACKOFFICE_SELECT_AI_DB_URL, BACKOFFICE_SELECT_AI_DB_USERNAME, "
|
||||||
|
+ "BACKOFFICE_SELECT_AI_DB_PASSWORD를 확인하세요.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String generatedSql = generate(selectAi, normalizedPrompt);
|
||||||
|
String normalizedSql = validateReadOnlySql(generatedSql);
|
||||||
|
ObjectNode response = objectMapper.createObjectNode();
|
||||||
|
response.put("status", "SHOWSQL");
|
||||||
|
response.put("profile", selectAi.profile());
|
||||||
|
response.put("generatedSql", normalizedSql);
|
||||||
|
response.put("execution", "NOT_EXECUTED");
|
||||||
|
response.put("nextStep", "생성 SQL을 검토한 뒤 Database Actions 또는 승인된 실행 경로에서 실행하세요.");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireActiveToken(String bearerToken) {
|
||||||
|
if (bearerToken == null || bearerToken.isBlank()) {
|
||||||
|
throw new VpdTokenAccessDeniedException();
|
||||||
|
}
|
||||||
|
BearerTokenRecord token = bearerTokenService.findByPlainToken(bearerToken.trim());
|
||||||
|
LocalDateTime now = LocalDateTime.now(clock.withZone(ZoneId.systemDefault()));
|
||||||
|
if (token == null || !token.active(now)) {
|
||||||
|
throw new VpdTokenAccessDeniedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String requiredPrompt(String prompt) {
|
||||||
|
String normalized = prompt == null ? "" : prompt.trim();
|
||||||
|
if (normalized.isEmpty()) {
|
||||||
|
throw new AppException("prompt는 필수입니다.");
|
||||||
|
}
|
||||||
|
if (normalized.length() > MAX_PROMPT_LENGTH) {
|
||||||
|
throw new AppException("prompt는 " + MAX_PROMPT_LENGTH + "자 이하여야 합니다.");
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generate(BackofficeProperties.SelectAi selectAi, String prompt) {
|
||||||
|
String sql = "SELECT DBMS_CLOUD_AI.GENERATE(?, ?, 'showsql') FROM dual";
|
||||||
|
try (Connection connection = DriverManager.getConnection(
|
||||||
|
selectAi.dbUrl(), selectAi.dbUsername(), selectAi.dbPassword());
|
||||||
|
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||||
|
statement.setString(1, prompt);
|
||||||
|
statement.setString(2, selectAi.profile());
|
||||||
|
try (ResultSet resultSet = statement.executeQuery()) {
|
||||||
|
if (!resultSet.next() || resultSet.getString(1) == null) {
|
||||||
|
throw new AppException("Select AI가 생성 SQL을 반환하지 않았습니다.");
|
||||||
|
}
|
||||||
|
return resultSet.getString(1);
|
||||||
|
}
|
||||||
|
} catch (AppException exception) {
|
||||||
|
throw exception;
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new AppException("Smilegate Select AI SHOWSQL 생성 실패: " + exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String validateReadOnlySql(String generatedSql) {
|
||||||
|
String normalized = generatedSql == null ? "" : generatedSql.trim();
|
||||||
|
if (normalized.startsWith("```")) {
|
||||||
|
int firstLineEnd = normalized.indexOf('\n');
|
||||||
|
int closingFence = normalized.lastIndexOf("```");
|
||||||
|
if (firstLineEnd >= 0 && closingFence > firstLineEnd) {
|
||||||
|
normalized = normalized.substring(firstLineEnd + 1, closingFence).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normalized = normalized.replaceFirst(";\\s*$", "").trim();
|
||||||
|
if (!normalized.matches("(?is)^(select|with)\\b.*")) {
|
||||||
|
throw new AppException("Select AI가 읽기 전용 SELECT/WITH SQL을 반환하지 않았습니다.");
|
||||||
|
}
|
||||||
|
if (normalized.contains(";")) {
|
||||||
|
throw new AppException("Select AI 결과에 여러 SQL 문장이 포함되어 있어 반환하지 않습니다.");
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,3 +68,10 @@ backoffice:
|
|||||||
oci-profile: ${BACKOFFICE_AI_OCI_PROFILE:${OCI_PROFILE:DEFAULT}}
|
oci-profile: ${BACKOFFICE_AI_OCI_PROFILE:${OCI_PROFILE:DEFAULT}}
|
||||||
oci-region: ${BACKOFFICE_AI_OCI_REGION:${POC3_LLM_GPT55_OCI_REGION:}}
|
oci-region: ${BACKOFFICE_AI_OCI_REGION:${POC3_LLM_GPT55_OCI_REGION:}}
|
||||||
oci-compartment-id: ${BACKOFFICE_AI_OCI_COMPARTMENT_ID:${OCI_GENAI_COMPARTMENT_ID:}}
|
oci-compartment-id: ${BACKOFFICE_AI_OCI_COMPARTMENT_ID:${OCI_GENAI_COMPARTMENT_ID:}}
|
||||||
|
select-ai:
|
||||||
|
# Cloud AI profiles are schema-owned. This connection must use SGMP_POC,
|
||||||
|
# not the ADMIN connection used by the backoffice control plane.
|
||||||
|
db-url: ${BACKOFFICE_SELECT_AI_DB_URL:}
|
||||||
|
db-username: ${BACKOFFICE_SELECT_AI_DB_USERNAME:}
|
||||||
|
db-password: ${BACKOFFICE_SELECT_AI_DB_PASSWORD:}
|
||||||
|
profile: ${BACKOFFICE_SELECT_AI_PROFILE:SGMP_POC_HAIKU45}
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.cloudhandson.vpdbackoffice.mapper.AuditMapper">
|
<mapper namespace="com.cloudhandson.vpdbackoffice.mapper.AuditMapper">
|
||||||
<insert id="insert" parameterType="com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent">
|
<insert id="insert" parameterType="com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent">
|
||||||
INSERT INTO hmm_access_audit (
|
INSERT INTO sg_audit_event (
|
||||||
event_type, key_id, object_id, status, row_count, error_code, message, created_at
|
audit_id, event_type, key_id, object_id, status, row_count, error_code, message, created_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
|
sg_audit_event_seq.NEXTVAL,
|
||||||
#{eventType,jdbcType=VARCHAR},
|
#{eventType,jdbcType=VARCHAR},
|
||||||
#{keyId,jdbcType=NUMERIC},
|
#{keyId,jdbcType=NUMERIC},
|
||||||
#{objectId,jdbcType=NUMERIC},
|
#{objectId,jdbcType=NUMERIC},
|
||||||
|
|||||||
@@ -68,10 +68,10 @@
|
|||||||
-->
|
-->
|
||||||
<select id="findPolicyStatuses" resultType="com.cloudhandson.vpdbackoffice.domain.masking.MaskingPolicyStatus">
|
<select id="findPolicyStatuses" resultType="com.cloudhandson.vpdbackoffice.domain.masking.MaskingPolicyStatus">
|
||||||
WITH managed_policy AS (
|
WITH managed_policy AS (
|
||||||
SELECT 'KB_CUSTOMERS' AS object_name, 'KB_CUSTOMER_PII_REDACT' AS policy_name FROM dual
|
SELECT 'CZN_COMN_USER_MST' AS object_name, 'SG_CZN_USER_REDACT' AS policy_name FROM dual
|
||||||
UNION ALL SELECT 'KB_CLAIMS', 'KB_CLAIM_AMOUNT_REDACT' FROM dual
|
UNION ALL SELECT 'COMN_SALES_USER_MST', 'SG_SALES_USER_REDACT' FROM dual
|
||||||
UNION ALL SELECT 'KB_CONTRACTS', 'KB_CONTRACT_PREMIUM_REDACT' FROM dual
|
UNION ALL SELECT 'COMN_SALES_TXN', 'SG_SALES_TXN_REDACT' FROM dual
|
||||||
UNION ALL SELECT 'KB_EXTERNAL_HOLDINGS', 'KB_EXT_HOLDING_REDACT' FROM dual
|
UNION ALL SELECT 'COMN_REFUND_TXN', 'SG_REFUND_TXN_REDACT' FROM dual
|
||||||
),
|
),
|
||||||
configured AS (
|
configured AS (
|
||||||
SELECT protected_object.object_name,
|
SELECT protected_object.object_name,
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
JOIN sg_masking_rule rule ON rule.rule_id = link.rule_id
|
JOIN sg_masking_rule rule ON rule.rule_id = link.rule_id
|
||||||
JOIN sg_protected_column protected_column ON protected_column.column_id = link.column_id
|
JOIN sg_protected_column protected_column ON protected_column.column_id = link.column_id
|
||||||
JOIN sg_protected_object protected_object ON protected_object.object_id = protected_column.object_id
|
JOIN sg_protected_object protected_object ON protected_object.object_id = protected_column.object_id
|
||||||
WHERE protected_object.owner = 'POC_2'
|
WHERE protected_object.owner = 'SGMP_POC'
|
||||||
AND rule.enabled_yn = 'Y'
|
AND rule.enabled_yn = 'Y'
|
||||||
GROUP BY protected_object.object_name
|
GROUP BY protected_object.object_name
|
||||||
),
|
),
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
LEFT JOIN redaction_columns policy_column
|
LEFT JOIN redaction_columns policy_column
|
||||||
ON policy_column.object_owner = policy.object_owner
|
ON policy_column.object_owner = policy.object_owner
|
||||||
AND policy_column.object_name = policy.object_name
|
AND policy_column.object_name = policy.object_name
|
||||||
WHERE policy.object_owner = 'POC_2'
|
WHERE policy.object_owner = 'SGMP_POC'
|
||||||
GROUP BY policy.object_name, policy.policy_name, policy.enable
|
GROUP BY policy.object_name, policy.policy_name, policy.enable
|
||||||
),
|
),
|
||||||
missing_columns AS (
|
missing_columns AS (
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
ON policy_column.object_owner = protected_object.owner
|
ON policy_column.object_owner = protected_object.owner
|
||||||
AND policy_column.object_name = protected_object.object_name
|
AND policy_column.object_name = protected_object.object_name
|
||||||
AND policy_column.column_name = protected_column.column_name
|
AND policy_column.column_name = protected_column.column_name
|
||||||
WHERE protected_object.owner = 'POC_2'
|
WHERE protected_object.owner = 'SGMP_POC'
|
||||||
AND rule.enabled_yn = 'Y'
|
AND rule.enabled_yn = 'Y'
|
||||||
AND policy_column.column_name IS NULL
|
AND policy_column.column_name IS NULL
|
||||||
GROUP BY protected_object.object_name
|
GROUP BY protected_object.object_name
|
||||||
@@ -120,14 +120,14 @@
|
|||||||
COUNT(*) AS extra_column_count
|
COUNT(*) AS extra_column_count
|
||||||
FROM redaction_columns policy_column
|
FROM redaction_columns policy_column
|
||||||
JOIN managed_policy managed ON managed.object_name = policy_column.object_name
|
JOIN managed_policy managed ON managed.object_name = policy_column.object_name
|
||||||
WHERE policy_column.object_owner = 'POC_2'
|
WHERE policy_column.object_owner = 'SGMP_POC'
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM sg_column_masking_rule link
|
FROM sg_column_masking_rule link
|
||||||
JOIN sg_masking_rule rule ON rule.rule_id = link.rule_id
|
JOIN sg_masking_rule rule ON rule.rule_id = link.rule_id
|
||||||
JOIN sg_protected_column protected_column ON protected_column.column_id = link.column_id
|
JOIN sg_protected_column protected_column ON protected_column.column_id = link.column_id
|
||||||
JOIN sg_protected_object protected_object ON protected_object.object_id = protected_column.object_id
|
JOIN sg_protected_object protected_object ON protected_object.object_id = protected_column.object_id
|
||||||
WHERE protected_object.owner = 'POC_2'
|
WHERE protected_object.owner = 'SGMP_POC'
|
||||||
AND protected_object.object_name = policy_column.object_name
|
AND protected_object.object_name = policy_column.object_name
|
||||||
AND protected_column.column_name = policy_column.column_name
|
AND protected_column.column_name = policy_column.column_name
|
||||||
AND rule.enabled_yn = 'Y'
|
AND rule.enabled_yn = 'Y'
|
||||||
@@ -138,18 +138,15 @@
|
|||||||
SELECT policy.object_name,
|
SELECT policy.object_name,
|
||||||
COUNT(*) AS legacy_vpd_column_policy_count
|
COUNT(*) AS legacy_vpd_column_policy_count
|
||||||
FROM all_policies policy
|
FROM all_policies policy
|
||||||
WHERE policy.object_owner = 'POC_2'
|
WHERE policy.object_owner = 'SGMP_POC'
|
||||||
AND policy.enable = 'YES'
|
AND policy.enable = 'YES'
|
||||||
AND policy.policy_name IN (
|
AND policy.policy_name IN (
|
||||||
'KB_CUST_NM_CLS_POLICY', 'KB_RRN_CLS_POLICY',
|
'SG_CZN_USER_REDACT', 'SG_SALES_USER_REDACT',
|
||||||
'KB_PREMIUM_CLS_POLICY',
|
'SG_SALES_TXN_REDACT', 'SG_REFUND_TXN_REDACT'
|
||||||
'KB_CLAIM_AMT_CLS_POLICY', 'KB_PAID_AMT_CLS_POLICY',
|
|
||||||
'KB_EXT_INSURER_CLS_POLICY', 'KB_EXT_PRODUCT_GRP_CLS_POLICY',
|
|
||||||
'KB_EXT_PRODUCT_TYPE_CLS_POLICY'
|
|
||||||
)
|
)
|
||||||
GROUP BY policy.object_name
|
GROUP BY policy.object_name
|
||||||
)
|
)
|
||||||
SELECT 'POC_2' AS owner,
|
SELECT 'SGMP_POC' AS owner,
|
||||||
managed.object_name,
|
managed.object_name,
|
||||||
managed.policy_name,
|
managed.policy_name,
|
||||||
database_policy.enable AS enabled,
|
database_policy.enable AS enabled,
|
||||||
|
|||||||
@@ -3,31 +3,36 @@
|
|||||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.cloudhandson.vpdbackoffice.mapper.StakeholderMapper">
|
<mapper namespace="com.cloudhandson.vpdbackoffice.mapper.StakeholderMapper">
|
||||||
<select id="findTokenSubjects" resultType="com.cloudhandson.vpdbackoffice.domain.stakeholder.StakeholderTokenSubject">
|
<select id="findTokenSubjects" resultType="com.cloudhandson.vpdbackoffice.domain.stakeholder.StakeholderTokenSubject">
|
||||||
SELECT s.user_id AS stakeholder_user_id,
|
SELECT TO_CHAR(u.user_id) AS stakeholder_user_id,
|
||||||
s.user_nm AS username,
|
u.user_name AS username,
|
||||||
s.role,
|
COALESCE((
|
||||||
s.channel,
|
SELECT MIN(r.role_name)
|
||||||
s.access_scope,
|
FROM sg_user_role user_role
|
||||||
|
JOIN sg_app_role r ON r.role_id = user_role.role_id
|
||||||
|
WHERE user_role.user_id = u.user_id
|
||||||
|
), 'DATA_AI_OPERATOR') AS role,
|
||||||
|
'BACKOFFICE' AS channel,
|
||||||
|
u.dept_code AS access_scope,
|
||||||
u.user_id AS app_user_id
|
u.user_id AS app_user_id
|
||||||
FROM poc_2.kb_stakeholders s
|
FROM sg_app_user u
|
||||||
JOIN cb_app_user u ON u.stakeholder_user_id = s.user_id
|
|
||||||
WHERE u.active = 'Y'
|
WHERE u.active = 'Y'
|
||||||
ORDER BY CASE s.role WHEN '지점장' THEN 1 WHEN '설계사' THEN 2 ELSE 3 END,
|
ORDER BY role, u.dept_code, u.user_name, u.user_id
|
||||||
s.channel,
|
|
||||||
s.user_nm,
|
|
||||||
s.user_id
|
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="findTokenSubject" resultType="com.cloudhandson.vpdbackoffice.domain.stakeholder.StakeholderTokenSubject">
|
<select id="findTokenSubject" resultType="com.cloudhandson.vpdbackoffice.domain.stakeholder.StakeholderTokenSubject">
|
||||||
SELECT s.user_id AS stakeholder_user_id,
|
SELECT TO_CHAR(u.user_id) AS stakeholder_user_id,
|
||||||
s.user_nm AS username,
|
u.user_name AS username,
|
||||||
s.role,
|
COALESCE((
|
||||||
s.channel,
|
SELECT MIN(r.role_name)
|
||||||
s.access_scope,
|
FROM sg_user_role user_role
|
||||||
|
JOIN sg_app_role r ON r.role_id = user_role.role_id
|
||||||
|
WHERE user_role.user_id = u.user_id
|
||||||
|
), 'DATA_AI_OPERATOR') AS role,
|
||||||
|
'BACKOFFICE' AS channel,
|
||||||
|
u.dept_code AS access_scope,
|
||||||
u.user_id AS app_user_id
|
u.user_id AS app_user_id
|
||||||
FROM poc_2.kb_stakeholders s
|
FROM sg_app_user u
|
||||||
JOIN cb_app_user u ON u.stakeholder_user_id = s.user_id
|
WHERE TO_CHAR(u.user_id) = #{stakeholderUserId,jdbcType=VARCHAR}
|
||||||
WHERE s.user_id = #{stakeholderUserId,jdbcType=VARCHAR}
|
|
||||||
AND u.active = 'Y'
|
AND u.active = 'Y'
|
||||||
</select>
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -728,16 +728,16 @@ function permissionRuleBusinessLabel(type, column, value) {
|
|||||||
return `토큰 이해관계자 본인 행 (${displayColumn})`;
|
return `토큰 이해관계자 본인 행 (${displayColumn})`;
|
||||||
}
|
}
|
||||||
if (type === 'OWN_CONTRACT') {
|
if (type === 'OWN_CONTRACT') {
|
||||||
return `담당 설계사 본인 계약 (${displayColumn})`;
|
return `담당 게임 서비스 범위 (${displayColumn})`;
|
||||||
}
|
}
|
||||||
if (type === 'CHANNEL_CONTRACT') {
|
if (type === 'CHANNEL_CONTRACT') {
|
||||||
return `토큰 사용자 채널 계약 (${displayColumn})`;
|
return `토큰 채널 게임 서비스 범위 (${displayColumn})`;
|
||||||
}
|
}
|
||||||
if (type === 'OWN_CUSTOMER') {
|
if (type === 'OWN_CUSTOMER') {
|
||||||
return `담당 설계사 본인 계약에 연결된 고객/청구/외부보유 (${displayColumn})`;
|
return `담당 게임 사용자·거래 데이터 범위 (${displayColumn})`;
|
||||||
}
|
}
|
||||||
if (type === 'CHANNEL_CUSTOMER') {
|
if (type === 'CHANNEL_CUSTOMER') {
|
||||||
return `토큰 사용자 채널 계약에 연결된 고객/청구/외부보유 (${displayColumn})`;
|
return `토큰 채널 게임 사용자·거래 데이터 범위 (${displayColumn})`;
|
||||||
}
|
}
|
||||||
if (type === 'STATIC_SQL') {
|
if (type === 'STATIC_SQL') {
|
||||||
return `정적 SQL 조건: ${value || '조건식 미입력'}`;
|
return `정적 SQL 조건: ${value || '조건식 미입력'}`;
|
||||||
@@ -813,10 +813,10 @@ function collectWizardPredicates(root) {
|
|||||||
return `${displayColumn} = SYS_CONTEXT('CB_AGENT_CTX', 'EMP_NO')`;
|
return `${displayColumn} = SYS_CONTEXT('CB_AGENT_CTX', 'EMP_NO')`;
|
||||||
}
|
}
|
||||||
if (type === 'STAKEHOLDER_SELF') {
|
if (type === 'STAKEHOLDER_SELF') {
|
||||||
return `EXISTS (KB_CONTRACTS c: c.${displayColumn} = <현재행>.${displayColumn} AND TOKEN_ROLE = '${value}' AND c.FC_ID = TOKEN_STAKEHOLDER_ID)`;
|
return '조건 코드 STAKEHOLDER_SELF: DB 행 접근 함수가 토큰 컨텍스트와 등록된 업무 관계를 기준으로 조건을 생성';
|
||||||
}
|
}
|
||||||
if (type === 'STAKEHOLDER_CHANNEL') {
|
if (type === 'STAKEHOLDER_CHANNEL') {
|
||||||
return `EXISTS (KB_CONTRACTS c: c.${displayColumn} = <현재행>.${displayColumn} AND TOKEN_ROLE = '${value}' AND c.FC_CHANNEL = TOKEN_CHANNEL)`;
|
return '조건 코드 STAKEHOLDER_CHANNEL: DB 행 접근 함수가 토큰 채널과 등록된 업무 관계를 기준으로 조건을 생성';
|
||||||
}
|
}
|
||||||
if (type === 'TOKEN_SUBJECT') {
|
if (type === 'TOKEN_SUBJECT') {
|
||||||
return `${displayColumn} = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID')`;
|
return `${displayColumn} = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID')`;
|
||||||
@@ -828,10 +828,10 @@ function collectWizardPredicates(root) {
|
|||||||
return `${displayColumn} = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_CHANNEL')`;
|
return `${displayColumn} = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_CHANNEL')`;
|
||||||
}
|
}
|
||||||
if (type === 'OWN_CUSTOMER') {
|
if (type === 'OWN_CUSTOMER') {
|
||||||
return `${displayColumn} IN (SELECT CUST_ID FROM KB_CONTRACTS WHERE FC_ID = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID'))`;
|
return '조건 코드 OWN_CUSTOMER: DB 행 접근 함수가 담당 사용자와 게임 데이터 관계를 기준으로 조건을 생성';
|
||||||
}
|
}
|
||||||
if (type === 'CHANNEL_CUSTOMER') {
|
if (type === 'CHANNEL_CUSTOMER') {
|
||||||
return `${displayColumn} IN (SELECT CUST_ID FROM KB_CONTRACTS WHERE FC_CHANNEL = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_CHANNEL'))`;
|
return '조건 코드 CHANNEL_CUSTOMER: DB 행 접근 함수가 토큰 채널과 게임 데이터 관계를 기준으로 조건을 생성';
|
||||||
}
|
}
|
||||||
if (type === 'DEPT' || type === 'EMP_NO') {
|
if (type === 'DEPT' || type === 'EMP_NO') {
|
||||||
return `${displayColumn} = ${sqlLiteral(value)}`;
|
return `${displayColumn} = ${sqlLiteral(value)}`;
|
||||||
|
|||||||
@@ -108,8 +108,8 @@
|
|||||||
<thead><tr><th>보호 객체</th><th>DB 정책</th><th>백오피스 활성 컬럼</th><th>DB Redaction 컬럼</th><th>레거시 VPD 컬럼 제어</th><th>DB 정책 활성</th><th>상태 판단</th></tr></thead>
|
<thead><tr><th>보호 객체</th><th>DB 정책</th><th>백오피스 활성 컬럼</th><th>DB Redaction 컬럼</th><th>레거시 VPD 컬럼 제어</th><th>DB 정책 활성</th><th>상태 판단</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="status : ${policyStatuses}">
|
<tr th:each="status : ${policyStatuses}">
|
||||||
<td><code th:text="${status.targetLabel()}">POC_2.KB_CUSTOMERS</code></td>
|
<td><code th:text="${status.targetLabel()}">SGMP_POC.CZN_COMN_USER_MST</code></td>
|
||||||
<td><code th:text="${status.policyName()}">KB_CUSTOMER_PII_REDACT</code></td>
|
<td><code th:text="${status.policyName()}">SG_GAME_USER_REDACT</code></td>
|
||||||
<td th:text="${status.configuredColumnCount()}">0</td>
|
<td th:text="${status.configuredColumnCount()}">0</td>
|
||||||
<td th:text="${status.appliedColumnCount()}">0</td>
|
<td th:text="${status.appliedColumnCount()}">0</td>
|
||||||
<td><span class="badge" th:classappend="${status.legacyVpdColumnPolicyCount() == 0} ? ' text-bg-secondary' : ' text-bg-danger'" th:text="${status.legacyVpdColumnPolicyCount() == 0} ? '없음' : ${status.legacyVpdColumnPolicyCount() + '건 활성'}">없음</span></td>
|
<td><span class="badge" th:classappend="${status.legacyVpdColumnPolicyCount() == 0} ? ' text-bg-secondary' : ' text-bg-danger'" th:text="${status.legacyVpdColumnPolicyCount() == 0} ? '없음' : ${status.legacyVpdColumnPolicyCount() + '건 활성'}">없음</span></td>
|
||||||
@@ -142,17 +142,17 @@
|
|||||||
|
|
||||||
<section class="content-band">
|
<section class="content-band">
|
||||||
<h2>컬럼 마스킹 규칙 등록</h2>
|
<h2>컬럼 마스킹 규칙 등록</h2>
|
||||||
<p class="section-subtitle">업무용 이름을 붙여 템플릿을 재사용합니다. 예: <code>KB_RRN_STANDARD</code>.</p>
|
<p class="section-subtitle">게임 데이터 기준의 업무용 이름을 붙여 템플릿을 재사용합니다. 예: <code>GAME_USER_ID_MASK</code>.</p>
|
||||||
<form method="post" action="/masking-rules" class="form-grid">
|
<form method="post" action="/masking-rules" class="form-grid">
|
||||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
<label>
|
<label>
|
||||||
규칙 코드
|
규칙 코드
|
||||||
<input class="form-control" name="ruleCode" maxlength="64" pattern="[A-Za-z][A-Za-z0-9_]{2,63}" required placeholder="KB_RRN_STANDARD">
|
<input class="form-control" name="ruleCode" maxlength="64" pattern="[A-Za-z][A-Za-z0-9_]{2,63}" required placeholder="GAME_USER_ID_MASK">
|
||||||
<span class="form-hint">영문·숫자·밑줄만 사용합니다.</span>
|
<span class="form-hint">영문·숫자·밑줄만 사용합니다.</span>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
규칙명
|
규칙명
|
||||||
<input class="form-control" name="ruleName" maxlength="100" required placeholder="주민번호 기본 마스킹">
|
<input class="form-control" name="ruleName" maxlength="100" required placeholder="게임 사용자 식별자 기본 마스킹">
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
마스킹 템플릿
|
마스킹 템플릿
|
||||||
@@ -176,8 +176,8 @@
|
|||||||
<thead><tr><th>코드</th><th>규칙명</th><th>템플릿</th><th>설명</th><th>상태</th><th></th></tr></thead>
|
<thead><tr><th>코드</th><th>규칙명</th><th>템플릿</th><th>설명</th><th>상태</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="rule : ${rules}">
|
<tr th:each="rule : ${rules}">
|
||||||
<td><code th:text="${rule.ruleCode()}">KB_RRN_STANDARD</code></td>
|
<td><code th:text="${rule.ruleCode()}">GAME_USER_ID_MASK</code></td>
|
||||||
<td th:text="${rule.ruleName()}">주민번호 기본 마스킹</td>
|
<td th:text="${rule.ruleName()}">게임 사용자 식별자 기본 마스킹</td>
|
||||||
<td th:text="${rule.templateLabel()}">값 숨김(NULL)</td>
|
<td th:text="${rule.templateLabel()}">값 숨김(NULL)</td>
|
||||||
<td th:text="${rule.description() ?: '-'}">설명</td>
|
<td th:text="${rule.description() ?: '-'}">설명</td>
|
||||||
<td><span class="badge" th:classappend="${rule.enabled()} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${rule.enabledYn()}">Y</span></td>
|
<td><span class="badge" th:classappend="${rule.enabled()} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${rule.enabledYn()}">Y</span></td>
|
||||||
@@ -209,11 +209,11 @@
|
|||||||
<optgroup th:label="${object.displayName()}" th:if="${!#lists.isEmpty(availableMaskingColumnsByObject[object.objectId()])}">
|
<optgroup th:label="${object.displayName()}" th:if="${!#lists.isEmpty(availableMaskingColumnsByObject[object.objectId()])}">
|
||||||
<option th:each="columnName : ${availableMaskingColumnsByObject[object.objectId()]}"
|
<option th:each="columnName : ${availableMaskingColumnsByObject[object.objectId()]}"
|
||||||
th:value="|${object.objectId()}:${columnName}|"
|
th:value="|${object.objectId()}:${columnName}|"
|
||||||
th:text="${object.displayName() + '.' + columnName}">POC_2.KB_CONTRACTS.PREMIUM</option>
|
th:text="${object.displayName() + '.' + columnName}">SGMP_POC.COMN_SALES_TXN.GUID</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
</th:block>
|
</th:block>
|
||||||
</select>
|
</select>
|
||||||
<span class="form-hint">현재 관리 대상 ASO 정책이 있는 객체만 표시됩니다. 예: <code>POC_2.KB_CONTRACTS.PREMIUM</code>.</span>
|
<span class="form-hint">현재 관리 대상 ASO 정책이 있는 게임 데이터 객체만 표시됩니다. 예: <code>SGMP_POC.COMN_SALES_TXN.GUID</code>.</span>
|
||||||
</label>
|
</label>
|
||||||
<button class="btn btn-outline-primary" type="submit">대상 컬럼 추가</button>
|
<button class="btn btn-outline-primary" type="submit">대상 컬럼 추가</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -253,9 +253,9 @@
|
|||||||
<thead><tr><th>대상 컬럼</th><th>규칙</th><th>템플릿</th><th>백오피스 설정</th><th>DB ASO 적용 상태</th><th></th></tr></thead>
|
<thead><tr><th>대상 컬럼</th><th>규칙</th><th>템플릿</th><th>백오피스 설정</th><th>DB ASO 적용 상태</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="columnRule : ${columnRules}">
|
<tr th:each="columnRule : ${columnRules}">
|
||||||
<td><code th:text="${columnRule.targetLabel()}">POC_2.KB_CUSTOMERS.RRN_MASKED</code></td>
|
<td><code th:text="${columnRule.targetLabel()}">SGMP_POC.CZN_COMN_USER_MST.USER_ID</code></td>
|
||||||
<td th:text="${columnRule.ruleName()}">주민번호 기본 마스킹</td>
|
<td th:text="${columnRule.ruleName()}">게임 사용자 식별자 기본 마스킹</td>
|
||||||
<td th:text="${columnRule.template().label()}">주민등록번호 부분 마스킹</td>
|
<td th:text="${columnRule.template().label()}">식별자 부분 마스킹</td>
|
||||||
<td><span class="badge" th:classappend="${columnRule.ruleEnabled()} ? ' text-bg-success' : ' text-bg-warning'" th:text="${columnRule.ruleEnabled()} ? '기본 규칙 연결됨' : '규칙 비활성'">기본 규칙 연결됨</span></td>
|
<td><span class="badge" th:classappend="${columnRule.ruleEnabled()} ? ' text-bg-success' : ' text-bg-warning'" th:text="${columnRule.ruleEnabled()} ? '기본 규칙 연결됨' : '규칙 비활성'">기본 규칙 연결됨</span></td>
|
||||||
<td th:with="policyStatus=${policyStatusByObjectName[columnRule.objectName()]}">
|
<td th:with="policyStatus=${policyStatusByObjectName[columnRule.objectName()]}">
|
||||||
<span class="badge"
|
<span class="badge"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<h1>MCP 연동</h1>
|
<h1>MCP 연동</h1>
|
||||||
<details class="explanation-details">
|
<details class="explanation-details">
|
||||||
<summary>도움말</summary>
|
<summary>도움말</summary>
|
||||||
<p>사용자 Bearer Token을 전달해 GPT-5.4-mini Select AI 자연어 조회를 실행하는 단일 MCP tool을 제공합니다. Select AI는 comment, annotation, constraint 메타데이터를 함께 사용하고, DB에서는 토큰 기준 행 접근 context가 적용됩니다.</p>
|
<p>사용자 Bearer Token을 검증한 뒤 <code>SGMP_POC_HAIKU45</code> Select AI 프로파일로 게임 데이터 Text2SQL을 생성하는 단일 MCP tool을 제공합니다. Select AI는 comment, annotation, constraint 메타데이터를 함께 사용하며, 생성 SQL은 자동 실행하지 않습니다.</p>
|
||||||
</details>
|
</details>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<div th:text="${tool.description()}">ORDS 행 접근 조회 도구 설명</div>
|
<div th:text="${tool.description()}">ORDS 행 접근 조회 도구 설명</div>
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
HTTP <code>Authorization</code> → 행 접근 컨텍스트 · <code>prompt</code> → GPT-5.4-mini Select AI 자연어 질의 · <code>limit</code> → 최대 반환 행 수
|
HTTP <code>Authorization</code> → 활성 PoC 사용자 토큰 검증 · <code>prompt</code> → 게임 데이터 Text2SQL 생성 · 결과 SQL은 검토 후 별도 실행
|
||||||
</small>
|
</small>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -116,10 +116,9 @@
|
|||||||
<summary>tools/call parameter 예시 보기</summary>
|
<summary>tools/call parameter 예시 보기</summary>
|
||||||
<h2>tools/call Arguments</h2>
|
<h2>tools/call Arguments</h2>
|
||||||
<pre class="code-block">{
|
<pre class="code-block">{
|
||||||
"prompt": "KB_CLAIMS의 전체 청구 건수를 조회해 줘.",
|
"prompt": "카제나의 최신 BASE_DT 기준 AU(활성 사용자 수)를 조회하는 SQL을 만들어줘."
|
||||||
"limit": 50
|
|
||||||
}</pre>
|
}</pre>
|
||||||
<p class="form-hint">등록 tool은 <code>ords.query.kb_select_ai_vpd</code> 하나입니다. HTTP Authorization의 사용자 Bearer Token으로 컨텍스트를 설정한 뒤 GPT-5.4-mini가 comment, annotation, constraint를 참고해 생성한 검증된 읽기 전용 KB 원장 SQL만 실행합니다.</p>
|
<p class="form-hint">등록 tool은 <code>oracle.select_ai.smilegate_game_text2sql</code> 하나입니다. 활성 사용자 Bearer Token을 확인한 뒤 <code>SGMP_POC_HAIKU45</code>가 comment, annotation, constraint를 참고해 읽기 전용 게임 데이터 SQL을 생성합니다. 실행은 자동으로 수행하지 않습니다.</p>
|
||||||
</details>
|
</details>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -153,10 +152,9 @@
|
|||||||
"id": 3,
|
"id": 3,
|
||||||
"method": "tools/call",
|
"method": "tools/call",
|
||||||
"params": {
|
"params": {
|
||||||
"name": "ords.query.kb_select_ai_vpd",
|
"name": "oracle.select_ai.smilegate_game_text2sql",
|
||||||
"arguments": {
|
"arguments": {
|
||||||
"prompt": "KB_CLAIMS의 전체 청구 건수를 조회해 줘.",
|
"prompt": "카제나의 최신 BASE_DT 기준 AU(활성 사용자 수)를 조회하는 SQL을 만들어줘."
|
||||||
"limit": 50
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}</pre>
|
}</pre>
|
||||||
|
|||||||
@@ -159,8 +159,8 @@
|
|||||||
<thead><tr><th>보호 객체</th><th>DB 정책</th><th>백오피스 활성 컬럼</th><th>DB Redaction 컬럼</th><th>상태</th><th>확인 결과</th></tr></thead>
|
<thead><tr><th>보호 객체</th><th>DB 정책</th><th>백오피스 활성 컬럼</th><th>DB Redaction 컬럼</th><th>상태</th><th>확인 결과</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="status : ${maskingPolicyStatuses}">
|
<tr th:each="status : ${maskingPolicyStatuses}">
|
||||||
<td><code th:text="${status.targetLabel()}">POC_2.KB_CLAIMS</code></td>
|
<td><code th:text="${status.targetLabel()}">SGMP_POC.COMN_SALES_TXN</code></td>
|
||||||
<td><code th:text="${status.policyName()}">KB_CLAIM_AMOUNT_REDACT</code></td>
|
<td><code th:text="${status.policyName()}">SG_SALES_TXN_REDACT</code></td>
|
||||||
<td th:text="${status.configuredColumnCount()}">0</td>
|
<td th:text="${status.configuredColumnCount()}">0</td>
|
||||||
<td th:text="${status.appliedColumnCount()}">0</td>
|
<td th:text="${status.appliedColumnCount()}">0</td>
|
||||||
<td><span class="badge" th:classappend="${' ' + status.badgeClass()}" th:text="${status.statusLabel()}">적용됨</span></td>
|
<td><span class="badge" th:classappend="${' ' + status.badgeClass()}" th:text="${status.statusLabel()}">적용됨</span></td>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<details class="explanation-details">
|
<details class="explanation-details">
|
||||||
<summary>도움말</summary>
|
<summary>도움말</summary>
|
||||||
<p>이 화면은 <strong>행 접근 규칙</strong>만 저장합니다. 저장값은 최종 SQL이 아니라 <code>CB_AGENT_DOC_VPD_FILTER</code>가 읽어서 대상 테이블의 WHERE predicate로 바꾸는 매핑 데이터입니다.</p>
|
<p>이 화면은 <strong>행 접근 규칙</strong>만 저장합니다. 저장값은 최종 SQL이 아니라 <code>CB_AGENT_DOC_VPD_FILTER</code>가 읽어서 대상 테이블의 WHERE predicate로 바꾸는 매핑 데이터입니다.</p>
|
||||||
<p>컬럼 원문/마스킹은 이 화면에서 처리하지 않습니다. 주민번호, 보험료, 청구금액 같은 컬럼 표시는 <a href="/masking-rules">컬럼 마스킹</a>의 ASO/Data Redaction 정책에서 관리합니다.</p>
|
<p>컬럼 원문/마스킹은 이 화면에서 처리하지 않습니다. 게임 사용자 식별자, 결제금액 같은 민감 컬럼 표시는 <a href="/masking-rules">컬럼 마스킹</a>의 ASO/Data Redaction 정책에서 관리합니다.</p>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -158,15 +158,15 @@
|
|||||||
</select>
|
</select>
|
||||||
<select class="form-select rule-type-select" name="ruleType">
|
<select class="form-select rule-type-select" name="ruleType">
|
||||||
<option value="ALL">ALL</option>
|
<option value="ALL">ALL</option>
|
||||||
<optgroup label="KB · 본인 범위">
|
<optgroup label="게임 데이터 · 담당자 범위">
|
||||||
<option value="OWN_CONTRACT">본인 담당 계약</option>
|
<option value="OWN_CONTRACT">담당 게임 서비스</option>
|
||||||
<option value="OWN_CUSTOMER">본인 담당 고객</option>
|
<option value="OWN_CUSTOMER">담당 게임 사용자</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
<optgroup label="KB · 채널 범위">
|
<optgroup label="게임 데이터 · 채널 범위">
|
||||||
<option value="CHANNEL_CONTRACT">토큰 채널 계약</option>
|
<option value="CHANNEL_CONTRACT">토큰 채널 게임 서비스</option>
|
||||||
<option value="CHANNEL_CUSTOMER">토큰 채널 고객</option>
|
<option value="CHANNEL_CUSTOMER">토큰 채널 게임 사용자</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
<optgroup label="KB · 토큰 식별">
|
<optgroup label="게임 데이터 · 토큰 식별">
|
||||||
<option value="TOKEN_SUBJECT">토큰 사용자 본인</option>
|
<option value="TOKEN_SUBJECT">토큰 사용자 본인</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
<optgroup label="SQL · 정적 조건">
|
<optgroup label="SQL · 정적 조건">
|
||||||
@@ -179,7 +179,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<details class="explanation-details">
|
<details class="explanation-details">
|
||||||
<summary>행 규칙의 두 가지 적용 방식 보기</summary>
|
<summary>행 규칙의 두 가지 적용 방식 보기</summary>
|
||||||
<p class="wizard-hint"><strong>조건 코드</strong>는 토큰 context로 치환됩니다. 예를 들어 <code>본인 담당 고객 / CUST_ID</code>는 계약원장에서 토큰 사용자 ID의 담당 고객을 찾아 현재 객체의 <code>CUST_ID</code>에 적용합니다. <strong>정적 SQL 조건식</strong>은 <code>CONTRACT_STATUS = '정상'</code>처럼 현재 객체 컬럼을 사용한 WHERE 절을 그대로 추가합니다. 한 권한 안의 규칙은 모두 AND로 좁혀지고, 서로 다른 역할의 ALLOW 권한은 OR로 합쳐집니다. 역할명은 최종 WHERE 절에 직접 들어가지 않습니다.</p>
|
<p class="wizard-hint"><strong>조건 코드</strong>는 토큰 context와 DB에 등록된 업무 관계를 바탕으로 행 접근 함수가 해석합니다. 관계가 없는 게임 데이터 객체에는 조건 코드를 억지로 적용하지 말고, <strong>정적 SQL 조건식</strong>으로 <code>GAME_ID = 'CZN'</code>처럼 실제 컬럼을 사용하세요. 한 권한 안의 규칙은 모두 AND로 좁혀지고, 서로 다른 역할의 ALLOW 권한은 OR로 합쳐집니다.</p>
|
||||||
<p class="wizard-hint"><strong>컬럼 원문/마스킹은 제외했습니다.</strong> 행 접근 필터는 행만 남기고, ASO/Data Redaction이 허용된 행 안에서 컬럼을 원문 또는 마스킹으로 반환합니다.</p>
|
<p class="wizard-hint"><strong>컬럼 원문/마스킹은 제외했습니다.</strong> 행 접근 필터는 행만 남기고, ASO/Data Redaction이 허용된 행 안에서 컬럼을 원문 또는 마스킹으로 반환합니다.</p>
|
||||||
</details>
|
</details>
|
||||||
<details class="explanation-details">
|
<details class="explanation-details">
|
||||||
@@ -196,28 +196,28 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>TOKEN_SUBJECT</code></td>
|
<td><code>TOKEN_SUBJECT</code></td>
|
||||||
<td>토큰으로 식별된 이해관계자 본인 행</td>
|
<td>토큰으로 식별된 운영 사용자 범위</td>
|
||||||
<td><code>USER_ID = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID')</code></td>
|
<td>대상 객체가 토큰 주체 식별 컬럼을 가질 때만 DB 행 접근 함수가 생성</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>OWN_CONTRACT</code></td>
|
<td><code>OWN_CONTRACT</code></td>
|
||||||
<td>토큰 사용자가 담당 설계사인 계약</td>
|
<td>토큰 사용자가 담당하는 게임 서비스 범위</td>
|
||||||
<td><code>FC_ID = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID')</code></td>
|
<td>등록된 업무 관계가 있을 때만 DB 행 접근 함수가 생성</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>CHANNEL_CONTRACT</code></td>
|
<td><code>CHANNEL_CONTRACT</code></td>
|
||||||
<td>토큰 사용자의 채널에 속한 계약</td>
|
<td>토큰 채널에 속한 게임 서비스 범위</td>
|
||||||
<td><code>FC_CHANNEL = SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_CHANNEL')</code></td>
|
<td>등록된 업무 관계가 있을 때만 DB 행 접근 함수가 생성</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>OWN_CUSTOMER</code> / <code>CHANNEL_CUSTOMER</code></td>
|
<td><code>OWN_CUSTOMER</code> / <code>CHANNEL_CUSTOMER</code></td>
|
||||||
<td>계약원장에서 담당자 또는 채널 기준으로 연결되는 고객·청구·담보·외부보유 행</td>
|
<td>게임 서비스·사용자 기준으로 연결되는 게임 로그·판매·환불 데이터 범위</td>
|
||||||
<td><code>EXISTS (SELECT 1 FROM POC_2.KB_CONTRACTS ...)</code></td>
|
<td>대상 테이블의 실제 키 관계를 DB 행 접근 함수가 검증한 뒤 생성</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>STATIC_SQL</code></td>
|
<td><code>STATIC_SQL</code></td>
|
||||||
<td>현재 객체 컬럼으로 표현한 고정 조건</td>
|
<td>현재 객체 컬럼으로 표현한 고정 조건</td>
|
||||||
<td><code>CONTRACT_STATUS = '정상'</code>처럼 검증된 현재 객체 컬럼 조건</td>
|
<td><code>GAME_ID = 'CZN'</code>처럼 검증된 현재 객체 컬럼 조건</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -29,9 +29,9 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="item : ${scripts}" th:classappend="${selectedScript != null and item.scriptId() == selectedScript.scriptId()} ? ' table-primary'">
|
<tr th:each="item : ${scripts}" th:classappend="${selectedScript != null and item.scriptId() == selectedScript.scriptId()} ? ' table-primary'">
|
||||||
<td><span class="badge text-bg-light" th:text="${item.category()}">ASO / 마스킹</span></td>
|
<td><span class="badge text-bg-light" th:text="${item.category()}">ASO / 마스킹</span></td>
|
||||||
<td><code th:text="${item.fileName()}">62_kb_aso_masking_backoffice_metadata.sql</code></td>
|
<td><code th:text="${item.fileName()}">71_sg_identity_administration.sql</code></td>
|
||||||
<td>
|
<td>
|
||||||
<strong th:text="${item.title()}">컬럼 마스킹 규칙 메타데이터</strong>
|
<strong th:text="${item.title()}">사용자·그룹·역할 관리 모델</strong>
|
||||||
<div class="form-hint" th:text="${item.description()}">설명</div>
|
<div class="form-hint" th:text="${item.description()}">설명</div>
|
||||||
</td>
|
</td>
|
||||||
<td><a class="btn btn-sm rw-btn-secondary" th:href="@{/security-sql-scripts(script=${item.scriptId()})}">원문 보기</a></td>
|
<td><a class="btn btn-sm rw-btn-secondary" th:href="@{/security-sql-scripts(script=${item.scriptId()})}">원문 보기</a></td>
|
||||||
@@ -45,13 +45,13 @@
|
|||||||
<section class="content-band" th:if="${selectedScript}">
|
<section class="content-band" th:if="${selectedScript}">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<div>
|
<div>
|
||||||
<span class="badge text-bg-secondary" th:text="${selectedScript.category()}">ASO / 마스킹</span>
|
<span class="badge text-bg-secondary" th:text="${selectedScript.category()}">Smilegate 권한</span>
|
||||||
<h2 class="mt-2" th:text="${selectedScript.title()}">컬럼 마스킹 규칙 메타데이터</h2>
|
<h2 class="mt-2" th:text="${selectedScript.title()}">사용자·그룹·역할 관리 모델</h2>
|
||||||
<p class="section-subtitle" th:text="${selectedScript.description()}">설명</p>
|
<p class="section-subtitle" th:text="${selectedScript.description()}">설명</p>
|
||||||
</div>
|
</div>
|
||||||
<code th:text="${selectedScript.fileName()}">62_kb_aso_masking_backoffice_metadata.sql</code>
|
<code th:text="${selectedScript.fileName()}">71_sg_identity_administration.sql</code>
|
||||||
</div>
|
</div>
|
||||||
<p class="form-hint">Git source: <code th:text="${'sql/adb/' + selectedScript.fileName()}">sql/adb/62_kb_aso_masking_backoffice_metadata.sql</code>. 실제 DB 배포본은 <a href="/vpd-filter-runtime">행 접근 필터 구조</a> 및 DB 배포 이력과 함께 확인하세요.</p>
|
<p class="form-hint">Git source: <code th:text="${'sql/adb/' + selectedScript.fileName()}">sql/adb/71_sg_identity_administration.sql</code>. 실제 DB 배포본은 <a href="/vpd-filter-runtime">행 접근 필터 구조</a> 및 DB 배포 이력과 함께 확인하세요.</p>
|
||||||
<form hx-post="/security-sql-scripts/explanation" hx-target="#security-sql-explanation" hx-swap="innerHTML" class="mb-3">
|
<form hx-post="/security-sql-scripts/explanation" hx-target="#security-sql-explanation" hx-swap="innerHTML" class="mb-3">
|
||||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
<input type="hidden" name="script" th:value="${selectedScript.scriptId()}">
|
<input type="hidden" name="script" th:value="${selectedScript.scriptId()}">
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||||
<head th:replace="~{fragments/layout :: head('HMM 액세스 토큰')}"></head>
|
<head th:replace="~{fragments/layout :: head('Smilegate 액세스 토큰')}"></head>
|
||||||
<body>
|
<body>
|
||||||
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
||||||
<main class="container py-4">
|
<main class="container py-4">
|
||||||
<div class="page-title">
|
<div class="page-title">
|
||||||
<h1>HMM 액세스 토큰</h1>
|
<h1>Smilegate 액세스 토큰</h1>
|
||||||
<details class="explanation-details">
|
<details class="explanation-details">
|
||||||
<summary>도움말</summary>
|
<summary>도움말</summary>
|
||||||
<p>HMM HR 직원에게 접근 토큰을 발급합니다. 토큰 원문은 한 번만 표시하며, DB에는 SHA-256 해시와 식별용 prefix만 보관합니다.</p>
|
<p>Data & AI PoC 도구 사용자에게 접근 토큰을 발급합니다. 토큰 원문은 한 번만 표시하며, DB에는 SHA-256 해시와 식별용 prefix만 보관합니다.</p>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -27,14 +27,14 @@
|
|||||||
<section class="content-band">
|
<section class="content-band">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<div>
|
<div>
|
||||||
<h2>직원 토큰 발급</h2>
|
<h2>도구 사용자 토큰 발급</h2>
|
||||||
<p class="section-subtitle">활성 HMM 직원에게 토큰을 발급합니다. 직접 역할과 접근 그룹 역할은 토큰 재발급 없이 조회 시점에 반영됩니다.</p>
|
<p class="section-subtitle">활성 Data & AI PoC 사용자에게 토큰을 발급합니다. 직접 역할과 접근 그룹 역할은 토큰 재발급 없이 조회 시점에 반영됩니다.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="/tokens" class="form-grid">
|
<form method="post" action="/tokens" class="form-grid">
|
||||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
<label>
|
<label>
|
||||||
HMM 직원
|
도구 사용자
|
||||||
<select class="form-select" name="userId" required>
|
<select class="form-select" name="userId" required>
|
||||||
<option value="" selected disabled>토큰을 발급할 직원 선택</option>
|
<option value="" selected disabled>토큰을 발급할 직원 선택</option>
|
||||||
<option th:each="user : ${users}"
|
<option th:each="user : ${users}"
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
용도 메모
|
용도 메모
|
||||||
<input class="form-control" name="description" maxlength="200" placeholder="예: HR 권한 확인">
|
<input class="form-control" name="description" maxlength="200" placeholder="예: 게임 데이터 AI 질의 검증">
|
||||||
</label>
|
</label>
|
||||||
<button class="btn rw-btn-primary" type="submit">검증 세션 발급</button>
|
<button class="btn rw-btn-primary" type="submit">검증 세션 발급</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<select class="form-select" name="userId" required>
|
<select class="form-select" name="userId" required>
|
||||||
<option value="">선택하세요</option>
|
<option value="">선택하세요</option>
|
||||||
<option th:each="user : ${users}" th:value="${user.userId()}" th:attr="data-user-label=${user.username()}"
|
<option th:each="user : ${users}" th:value="${user.userId()}" th:attr="data-user-label=${user.username()}"
|
||||||
th:text="${user.username() + ' (' + user.empNo() + ')'}">KB_VPD_ADMIN</option>
|
th:text="${user.username() + ' (' + user.empNo() + ')'}">sg-teamlead (SG-001)</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
data-result=${columnRule.template().previewResult()},
|
data-result=${columnRule.template().previewResult()},
|
||||||
data-aso-function=${columnRule.template().asoFunction()},
|
data-aso-function=${columnRule.template().asoFunction()},
|
||||||
data-context=${'MR_' + columnRule.columnId()}"
|
data-context=${'MR_' + columnRule.columnId()}"
|
||||||
th:text="${columnRule.targetLabel() + ' · ' + columnRule.ruleLabel()}">KB_CUSTOMERS.RRN_MASKED</option>
|
th:text="${columnRule.targetLabel() + ' · ' + columnRule.ruleLabel()}">SGMP_POC.CZN_COMN_USER_MST.GUID</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<div>
|
<div>
|
||||||
@@ -85,9 +85,9 @@
|
|||||||
<thead><tr><th>사용자</th><th>대상 컬럼</th><th>기본 규칙</th><th>원문 표시 상태</th><th>상태</th><th></th></tr></thead>
|
<thead><tr><th>사용자</th><th>대상 컬럼</th><th>기본 규칙</th><th>원문 표시 상태</th><th>상태</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="userRule : ${userRules}">
|
<tr th:each="userRule : ${userRules}">
|
||||||
<td th:text="${userRule.username()}">KB_VPD_ADMIN</td>
|
<td th:text="${userRule.username()}">sg-teamlead</td>
|
||||||
<td><code th:text="${userRule.targetLabel()}">POC_2.KB_CUSTOMERS.RRN_MASKED</code></td>
|
<td><code th:text="${userRule.targetLabel()}">SGMP_POC.CZN_COMN_USER_MST.GUID</code></td>
|
||||||
<td th:text="${userRule.ruleLabel()}">주민번호 기본 마스킹 · 주민등록번호 부분 마스킹</td>
|
<td th:text="${userRule.ruleLabel()}">게임 사용자 식별자 기본 마스킹 · 식별번호 부분 마스킹</td>
|
||||||
<td><span class="badge" th:classappend="${userRule.unmasked()} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${userRule.decisionLabel()}">원문 표시 예외</span></td>
|
<td><span class="badge" th:classappend="${userRule.unmasked()} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${userRule.decisionLabel()}">원문 표시 예외</span></td>
|
||||||
<td th:text="${userRule.activeYn()}">Y</td>
|
<td th:text="${userRule.activeYn()}">Y</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -82,8 +82,8 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="policy : ${policies}">
|
<tr th:each="policy : ${policies}">
|
||||||
<td><code th:text="${policy.objectDisplayName()}">POC_2.KB_CONTRACTS</code></td>
|
<td><code th:text="${policy.objectDisplayName()}">SGMP_POC.CZN_COMN_USER_MST</code></td>
|
||||||
<td><code th:text="${policy.policyName()}">KB_KB_CONTRACTS_ROW_POLICY</code></td>
|
<td><code th:text="${policy.policyName()}">SG_CZN_USER_ROW_POLICY</code></td>
|
||||||
<td th:text="${policy.statementTypes()}">SELECT</td>
|
<td th:text="${policy.statementTypes()}">SELECT</td>
|
||||||
<td><span class="badge" th:classappend="${policy.enabled() == 'YES'} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${policy.enabled() == 'YES'} ? '적용됨' : '중지됨'">적용됨</span></td>
|
<td><span class="badge" th:classappend="${policy.enabled() == 'YES'} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${policy.enabled() == 'YES'} ? '적용됨' : '중지됨'">적용됨</span></td>
|
||||||
<td><code th:text="${policy.functionDisplayName()}">ADMIN.CB_AGENT_DOC_VPD_FILTER</code></td>
|
<td><code th:text="${policy.functionDisplayName()}">ADMIN.CB_AGENT_DOC_VPD_FILTER</code></td>
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
<tr><td>토큰 신뢰 경계</td><td>Bearer Token은 ORDS에서 검증하고 요청 종료 시 컨텍스트를 정리합니다.</td><td>ORDS Handler / <code>CB_AGENT_CTX_PKG</code></td></tr>
|
<tr><td>토큰 신뢰 경계</td><td>Bearer Token은 ORDS에서 검증하고 요청 종료 시 컨텍스트를 정리합니다.</td><td>ORDS Handler / <code>CB_AGENT_CTX_PKG</code></td></tr>
|
||||||
<tr><td>권한 결합</td><td>권한 내부 규칙은 AND, ALLOW 권한은 OR, DENY 조건은 최종적으로 제외합니다.</td><td><a href="/permissions">행 접근 규칙</a></td></tr>
|
<tr><td>권한 결합</td><td>권한 내부 규칙은 AND, ALLOW 권한은 OR, DENY 조건은 최종적으로 제외합니다.</td><td><a href="/permissions">행 접근 규칙</a></td></tr>
|
||||||
<tr><td>오류·미권한</td><td>유효한 컨텍스트나 ALLOW 권한이 없으면 행 접근은 차단돼야 합니다.</td><td><a href="/probe">접근 검증</a></td></tr>
|
<tr><td>오류·미권한</td><td>유효한 컨텍스트나 ALLOW 권한이 없으면 행 접근은 차단돼야 합니다.</td><td><a href="/probe">접근 검증</a></td></tr>
|
||||||
<tr><td>컬럼 보호</td><td>행 필터(VPD)와 보험료·주민번호 등의 ASO/Data Redaction 마스킹을 분리해 확인합니다.</td><td><a href="/masking-rules">컬럼 마스킹</a></td></tr>
|
<tr><td>컬럼 보호</td><td>행 필터(VPD)와 게임 사용자 식별자·거래 식별자의 ASO/Data Redaction 마스킹을 분리해 확인합니다.</td><td><a href="/masking-rules">컬럼 마스킹</a></td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ class MaskingPolicySynchronizerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void kbContractsPremiumPolicyIsManagedByBackoffice() {
|
void smilegateSalesTransactionPolicyIsManagedByBackoffice() {
|
||||||
assertThat(MaskingPolicySynchronizer.managedPolicyName("KB_CONTRACTS"))
|
assertThat(MaskingPolicySynchronizer.managedPolicyName("COMN_SALES_TXN"))
|
||||||
.isEqualTo("KB_CONTRACT_PREMIUM_REDACT");
|
.isEqualTo("SG_SALES_TXN_REDACT");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import org.junit.jupiter.api.Test;
|
|||||||
class McpSseServiceTest {
|
class McpSseServiceTest {
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
private final SelectAiAgentOrdsService selectAiAgentOrdsService = new CapturingSelectAiAgentOrdsService();
|
private final SmilegateSelectAiService selectAiService = new CapturingSmilegateSelectAiService();
|
||||||
private final McpSseService service = new McpSseService(
|
private final McpSseService service = new McpSseService(
|
||||||
selectAiAgentOrdsService,
|
selectAiService,
|
||||||
objectMapper
|
objectMapper
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -22,12 +22,12 @@ class McpSseServiceTest {
|
|||||||
var tools = response.path("result").path("tools");
|
var tools = response.path("result").path("tools");
|
||||||
assertThat(tools).hasSize(1);
|
assertThat(tools).hasSize(1);
|
||||||
var selectAi = tools.get(0);
|
var selectAi = tools.get(0);
|
||||||
assertThat(selectAi.path("name").asText()).isEqualTo("ords.query.kb_select_ai_vpd");
|
assertThat(selectAi.path("name").asText()).isEqualTo("oracle.select_ai.smilegate_game_text2sql");
|
||||||
assertThat(selectAi.path("inputSchema").path("required"))
|
assertThat(selectAi.path("inputSchema").path("required"))
|
||||||
.extracting(node -> node.asText())
|
.extracting(node -> node.asText())
|
||||||
.contains("prompt");
|
.contains("prompt");
|
||||||
assertThat(selectAi.path("inputSchema").path("properties").has("bearerToken")).isFalse();
|
assertThat(selectAi.path("inputSchema").path("properties").has("bearerToken")).isFalse();
|
||||||
assertThat(selectAi.path("inputSchema").path("properties").has("limit")).isTrue();
|
assertThat(selectAi.path("inputSchema").path("properties").has("limit")).isFalse();
|
||||||
assertThat(selectAi.path("inputSchema").path("properties").has("conversationId")).isFalse();
|
assertThat(selectAi.path("inputSchema").path("properties").has("conversationId")).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,31 +35,29 @@ class McpSseServiceTest {
|
|||||||
void callsVpdSelectAiThroughOrdsService() {
|
void callsVpdSelectAiThroughOrdsService() {
|
||||||
ObjectNode request = request(2, "tools/call");
|
ObjectNode request = request(2, "tools/call");
|
||||||
ObjectNode params = (ObjectNode) request.putObject("params");
|
ObjectNode params = (ObjectNode) request.putObject("params");
|
||||||
params.put("name", "ords.query.kb_select_ai_vpd");
|
params.put("name", "oracle.select_ai.smilegate_game_text2sql");
|
||||||
ObjectNode arguments = params.putObject("arguments");
|
ObjectNode arguments = params.putObject("arguments");
|
||||||
arguments.put("prompt", "고객 수를 조회해 줘");
|
arguments.put("prompt", "카제나 AU를 조회해 줘");
|
||||||
arguments.put("limit", 25);
|
|
||||||
|
|
||||||
ObjectNode response = service.handle("default", request, "user-bearer");
|
ObjectNode response = service.handle("default", request, "user-bearer");
|
||||||
|
|
||||||
CapturingSelectAiAgentOrdsService agentService =
|
CapturingSmilegateSelectAiService agentService =
|
||||||
(CapturingSelectAiAgentOrdsService) selectAiAgentOrdsService;
|
(CapturingSmilegateSelectAiService) selectAiService;
|
||||||
assertThat(agentService.bearerToken).isEqualTo("user-bearer");
|
assertThat(agentService.bearerToken).isEqualTo("user-bearer");
|
||||||
assertThat(agentService.prompt).isEqualTo("고객 수를 조회해 줘");
|
assertThat(agentService.prompt).isEqualTo("카제나 AU를 조회해 줘");
|
||||||
assertThat(agentService.limit).isEqualTo(25);
|
|
||||||
assertThat(response.path("error").isMissingNode()).isTrue();
|
assertThat(response.path("error").isMissingNode()).isTrue();
|
||||||
assertThat(response.path("result").path("isError").asBoolean()).isFalse();
|
assertThat(response.path("result").path("isError").asBoolean()).isFalse();
|
||||||
assertThat(response.path("result").path("content").get(0).path("text").asText())
|
assertThat(response.path("result").path("content").get(0).path("text").asText())
|
||||||
.contains("KB_AIDP_SELECTAI_GPT54_MINI_FULLMETA_PROFILE_V1")
|
.contains("SGMP_POC_HAIKU45")
|
||||||
.contains("SELECT COUNT(*) FROM KB_CUSTOMERS");
|
.contains("SELECT 1 FROM DUAL");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void returnsToolLevelDeniedResultWhenVpdTokenIsMissing() {
|
void returnsToolLevelDeniedResultWhenVpdTokenIsMissing() {
|
||||||
ObjectNode request = request(3, "tools/call");
|
ObjectNode request = request(3, "tools/call");
|
||||||
ObjectNode params = (ObjectNode) request.putObject("params");
|
ObjectNode params = (ObjectNode) request.putObject("params");
|
||||||
params.put("name", "ords.query.kb_select_ai_vpd");
|
params.put("name", "oracle.select_ai.smilegate_game_text2sql");
|
||||||
params.putObject("arguments").put("prompt", "고객 수를 조회해 줘");
|
params.putObject("arguments").put("prompt", "카제나 AU를 조회해 줘");
|
||||||
|
|
||||||
ObjectNode response = service.handle("default", request, "");
|
ObjectNode response = service.handle("default", request, "");
|
||||||
|
|
||||||
@@ -78,24 +76,22 @@ class McpSseServiceTest {
|
|||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class CapturingSelectAiAgentOrdsService extends SelectAiAgentOrdsService {
|
private static final class CapturingSmilegateSelectAiService extends SmilegateSelectAiService {
|
||||||
|
|
||||||
private String bearerToken;
|
private String bearerToken;
|
||||||
private String prompt;
|
private String prompt;
|
||||||
private int limit;
|
|
||||||
|
|
||||||
private CapturingSelectAiAgentOrdsService() {
|
private CapturingSmilegateSelectAiService() {
|
||||||
super(null, null, new ObjectMapper());
|
super(null, null, null, new ObjectMapper());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JsonNode run(String bearerToken, String prompt, int limit) {
|
public JsonNode generateShowSql(String bearerToken, String prompt) {
|
||||||
this.bearerToken = bearerToken;
|
this.bearerToken = bearerToken;
|
||||||
this.prompt = prompt;
|
this.prompt = prompt;
|
||||||
this.limit = limit;
|
|
||||||
return new ObjectMapper().createObjectNode()
|
return new ObjectMapper().createObjectNode()
|
||||||
.put("profile", "KB_AIDP_SELECTAI_GPT54_MINI_FULLMETA_PROFILE_V1")
|
.put("profile", "SGMP_POC_HAIKU45")
|
||||||
.put("generatedSql", "SELECT COUNT(*) FROM KB_CUSTOMERS");
|
.put("generatedSql", "SELECT 1 FROM DUAL");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,16 +14,13 @@ class SecuritySqlScriptServiceTest {
|
|||||||
assertThat(service.list())
|
assertThat(service.list())
|
||||||
.extracting(item -> item.fileName())
|
.extracting(item -> item.fileName())
|
||||||
.containsExactly(
|
.containsExactly(
|
||||||
"62_kb_aso_masking_backoffice_metadata.sql",
|
"70_sg_tool_user.sql",
|
||||||
"63_kb_aso_masking_rule_runtime.sql",
|
"71_sg_identity_administration.sql"
|
||||||
"64_kb_aso_masking_default_column_rules.sql",
|
|
||||||
"65_kb_select_ai_vpd_query_api.sql",
|
|
||||||
"66_kb_select_ai_vpd_query_ords.sql"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(service.find("select-ai-vpd-ords").source())
|
assertThat(service.find("smilegate-identity-administration").source())
|
||||||
.contains("POST /ords/cb-ords/kb-select-ai-vpd/query")
|
.contains("create table sg_app_user")
|
||||||
.contains("Authorization: Bearer <VPD token>");
|
.contains("DATA_AI_TF");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -12,22 +12,22 @@ class StructuredDataServiceTest {
|
|||||||
private final StructuredDataService service = new StructuredDataService(mock(JdbcTemplate.class));
|
private final StructuredDataService service = new StructuredDataService(mock(JdbcTemplate.class));
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void exposesOnlyTheSevenApprovedStructuredTables() {
|
void exposesOnlyTheSevenApprovedSmilegateStructuredTables() {
|
||||||
assertThat(service.tables())
|
assertThat(service.tables())
|
||||||
.extracting(table -> table.tableName())
|
.extracting(table -> table.tableName())
|
||||||
.containsExactly(
|
.containsExactly(
|
||||||
"KB_CUSTOMERS",
|
"CZN_COMN_USER_MST",
|
||||||
"KB_PRODUCTS",
|
"CZN_COMN_CHARACTER_MST",
|
||||||
"KB_CONTRACTS",
|
"COMN_SALES_TXN",
|
||||||
"KB_COVERAGES",
|
"COMN_REFUND_TXN",
|
||||||
"KB_CLAIMS",
|
"COMN_SALES_PRODUCT_DISP_BAS",
|
||||||
"KB_EXTERNAL_HOLDINGS",
|
"COMN_GAME_SERVER_BAS",
|
||||||
"KB_STAKEHOLDERS");
|
"COMN_GAME_ALIAS_BAS");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rejectsAnyTableOutsideTheServerSideAllowlist() {
|
void rejectsAnyTableOutsideTheServerSideAllowlist() {
|
||||||
assertThatThrownBy(() -> service.requireTable("KB_SECURITY_AUDIT_LOG"))
|
assertThatThrownBy(() -> service.requireTable("security-audit-log"))
|
||||||
.isInstanceOf(AppException.class)
|
.isInstanceOf(AppException.class)
|
||||||
.hasMessage("선택할 수 없는 정형 데이터 테이블입니다.");
|
.hasMessage("선택할 수 없는 정형 데이터 테이블입니다.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class GuidedFlowTemplateTest {
|
|||||||
.doesNotContain("dashboard-workflow")
|
.doesNotContain("dashboard-workflow")
|
||||||
.doesNotContain(">01<", ">02<");
|
.doesNotContain(">01<", ">02<");
|
||||||
assertThat(layout)
|
assertThat(layout)
|
||||||
.contains("데이터 접근 제어")
|
.contains("운영 사용자 관리")
|
||||||
.contains("접근 제어")
|
.contains("접근 제어")
|
||||||
.contains("보호·검증")
|
.contains("보호·검증")
|
||||||
.contains("연동 도구")
|
.contains("연동 도구")
|
||||||
@@ -122,7 +122,7 @@ class GuidedFlowTemplateTest {
|
|||||||
.contains("data-wizard-target=\"2\">대상")
|
.contains("data-wizard-target=\"2\">대상")
|
||||||
.doesNotContain(">1 역할<", ">2 객체<", "wizard-step-number");
|
.doesNotContain(">1 역할<", ">2 객체<", "wizard-step-number");
|
||||||
assertThat(tokens)
|
assertThat(tokens)
|
||||||
.contains("직원 토큰 발급", "검증 세션 발급")
|
.contains("도구 사용자 토큰 발급", "검증 세션 발급")
|
||||||
.doesNotContain("1. 검증할 사용자 선택", "2. 검증 세션 발급");
|
.doesNotContain("1. 검증할 사용자 선택", "2. 검증 세션 발급");
|
||||||
assertThat(probe)
|
assertThat(probe)
|
||||||
.contains("접근 검증 실행", "<strong>검증 결과</strong>")
|
.contains("접근 검증 실행", "<strong>검증 결과</strong>")
|
||||||
@@ -198,7 +198,7 @@ class GuidedFlowTemplateTest {
|
|||||||
String javascript = Files.readString(Path.of("src/main/resources/static/js/app.js"));
|
String javascript = Files.readString(Path.of("src/main/resources/static/js/app.js"));
|
||||||
|
|
||||||
assertThat(html)
|
assertThat(html)
|
||||||
.contains("value=\"OWN_CUSTOMER\">본인 담당 고객")
|
.contains("value=\"OWN_CUSTOMER\">담당 게임 사용자")
|
||||||
.contains("value=\"STATIC_SQL\">정적 SQL 조건식")
|
.contains("value=\"STATIC_SQL\">정적 SQL 조건식")
|
||||||
.contains("행 규칙의 두 가지 적용 방식 보기")
|
.contains("행 규칙의 두 가지 적용 방식 보기")
|
||||||
.contains("한 권한 안의 규칙은 모두 AND")
|
.contains("한 권한 안의 규칙은 모두 AND")
|
||||||
@@ -221,8 +221,8 @@ class GuidedFlowTemplateTest {
|
|||||||
String filters = template("vpd-filter-policies.html");
|
String filters = template("vpd-filter-policies.html");
|
||||||
|
|
||||||
assertThat(roles)
|
assertThat(roles)
|
||||||
.contains("표시 보호 등급 상한")
|
.contains("스마일게이트 Data & AI PoC 운영 역할")
|
||||||
.contains("행 접근 권한은 행 접근 규칙과 Oracle VPD 정책에서 결정합니다");
|
.contains("데이터 취급 등급");
|
||||||
assertThat(tokens)
|
assertThat(tokens)
|
||||||
.contains("회수·만료 포함")
|
.contains("회수·만료 포함")
|
||||||
.contains("현재 사용 가능한 토큰만");
|
.contains("현재 사용 가능한 토큰만");
|
||||||
|
|||||||
@@ -31,26 +31,26 @@ class MaskingRuleTemplateRenderTest {
|
|||||||
var engine = new SpringTemplateEngine();
|
var engine = new SpringTemplateEngine();
|
||||||
engine.setTemplateResolver(resolver);
|
engine.setTemplateResolver(resolver);
|
||||||
|
|
||||||
var object = new ProtectedObject(7L, "POC_2", "KB_CUSTOMERS", "kb/customers", "Y");
|
var object = new ProtectedObject(7L, "SGMP_POC", "CZN_COMN_USER_MST", "sgmp-poc/czn-users", "Y");
|
||||||
var contractObject = new ProtectedObject(8L, "POC_2", "KB_CONTRACTS", "kb/contracts", "Y");
|
var transactionObject = new ProtectedObject(8L, "SGMP_POC", "COMN_SALES_TXN", "sgmp-poc/sales", "Y");
|
||||||
var column = new ProtectedColumn(11L, 7L, "RRN_MASKED", "Y", null, "RESTRICTED", "NULLIFY");
|
var column = new ProtectedColumn(11L, 7L, "GUID", "Y", null, "RESTRICTED", "NULLIFY");
|
||||||
var rule = new MaskingRule(1L, "KB_RRN_STANDARD", "주민번호 기본 마스킹", "RRN_PARTIAL", "테스트", "Y");
|
var rule = new MaskingRule(1L, "GAME_GUID_MASK", "게임 사용자 식별자 기본 마스킹", "TEXT_PARTIAL", "테스트", "Y");
|
||||||
var columnRule = new ColumnMaskingRule(11L, 7L, "POC_2", "KB_CUSTOMERS", "RRN_MASKED",
|
var columnRule = new ColumnMaskingRule(11L, 7L, "SGMP_POC", "CZN_COMN_USER_MST", "GUID",
|
||||||
1L, "KB_RRN_STANDARD", "주민번호 기본 마스킹", "RRN_PARTIAL", "Y");
|
1L, "GAME_GUID_MASK", "게임 사용자 식별자 기본 마스킹", "TEXT_PARTIAL", "Y");
|
||||||
var context = new Context(Locale.KOREAN);
|
var context = new Context(Locale.KOREAN);
|
||||||
context.setVariable("_csrf", new CsrfFixture("_csrf", "test-token"));
|
context.setVariable("_csrf", new CsrfFixture("_csrf", "test-token"));
|
||||||
context.setVariable("templates", List.of(MaskingTemplate.values()));
|
context.setVariable("templates", List.of(MaskingTemplate.values()));
|
||||||
context.setVariable("rules", List.of(rule));
|
context.setVariable("rules", List.of(rule));
|
||||||
context.setVariable("columnRules", List.of(columnRule));
|
context.setVariable("columnRules", List.of(columnRule));
|
||||||
var status = new MaskingPolicyStatus(
|
var status = new MaskingPolicyStatus(
|
||||||
"POC_2", "KB_CUSTOMERS", "KB_CUSTOMER_PII_REDACT", "YES", 1, 1, 0, 0
|
"SGMP_POC", "CZN_COMN_USER_MST", "SG_CZN_USER_REDACT", "YES", 1, 1, 0, 0
|
||||||
);
|
);
|
||||||
context.setVariable("policyStatuses", List.of(status));
|
context.setVariable("policyStatuses", List.of(status));
|
||||||
context.setVariable("policyStatusByObjectName", Map.of("KB_CUSTOMERS", status));
|
context.setVariable("policyStatusByObjectName", Map.of("CZN_COMN_USER_MST", status));
|
||||||
context.setVariable("objects", List.of(object, contractObject));
|
context.setVariable("objects", List.of(object, transactionObject));
|
||||||
context.setVariable("maskingTargetObjects", List.of(object, contractObject));
|
context.setVariable("maskingTargetObjects", List.of(object, transactionObject));
|
||||||
context.setVariable("sensitiveColumnsByObject", Map.of(7L, List.of(column), 8L, List.of()));
|
context.setVariable("sensitiveColumnsByObject", Map.of(7L, List.of(column), 8L, List.of()));
|
||||||
context.setVariable("availableMaskingColumnsByObject", Map.of(7L, List.of("CUST_NM"), 8L, List.of("PREMIUM")));
|
context.setVariable("availableMaskingColumnsByObject", Map.of(7L, List.of("GUID"), 8L, List.of("GUID")));
|
||||||
|
|
||||||
String rulesPage = engine.process("masking-rules", context);
|
String rulesPage = engine.process("masking-rules", context);
|
||||||
|
|
||||||
@@ -64,13 +64,13 @@ class MaskingRuleTemplateRenderTest {
|
|||||||
.contains("DB Redaction 컬럼")
|
.contains("DB Redaction 컬럼")
|
||||||
.contains("레거시 VPD 컬럼 제어")
|
.contains("레거시 VPD 컬럼 제어")
|
||||||
.contains("마스킹 대상 컬럼 추가")
|
.contains("마스킹 대상 컬럼 추가")
|
||||||
.contains("POC_2.KB_CONTRACTS.PREMIUM")
|
.contains("SGMP_POC.COMN_SALES_TXN.GUID")
|
||||||
.contains("KB_CUSTOMER_PII_REDACT")
|
.contains("SG_CZN_USER_REDACT")
|
||||||
.contains("적용됨");
|
.contains("적용됨");
|
||||||
|
|
||||||
context.setVariable("users", List.of(new AppUser(2L, "KB_VPD_ADMIN", "ADMIN", "SEC", "N", "Y")));
|
context.setVariable("users", List.of(new AppUser(2L, "sg-teamlead", "SG-001", "DATA_AI", "N", "Y")));
|
||||||
context.setVariable("userRules", List.of(new UserMaskingRule(2L, "KB_VPD_ADMIN", 11L,
|
context.setVariable("userRules", List.of(new UserMaskingRule(2L, "sg-teamlead", 11L,
|
||||||
"POC_2", "KB_CUSTOMERS", "RRN_MASKED", "주민번호 기본 마스킹", "RRN_PARTIAL", "UNMASK", "Y")));
|
"SGMP_POC", "CZN_COMN_USER_MST", "GUID", "게임 사용자 식별자 기본 마스킹", "TEXT_PARTIAL", "UNMASK", "Y")));
|
||||||
String userPage = engine.process("user-masking-rules", context);
|
String userPage = engine.process("user-masking-rules", context);
|
||||||
|
|
||||||
assertThat(userPage)
|
assertThat(userPage)
|
||||||
|
|||||||
Reference in New Issue
Block a user