diff --git a/docs/README.md b/docs/README.md
index 0544232..64a1314 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -17,6 +17,7 @@ docs/
adr/ ← Architecture Decision Records: 가로지르는 결정 기록
_TEMPLATE.md
NNNN-
.md
+ architecture/ ← 현행 시스템 아키텍처 Draw.io 원본과 검토용 SVG/PNG
reference/ ← 레퍼런스: 구현된 모듈/함수/설정 사양 (구현 "후" 동기화)
guides/ ← How-to / 사용 가이드 / 튜토리얼 (사용자·운영자 대상)
pipeline/ ← 개발 프로세스 문서 (큐 프로토콜·런북)
diff --git a/docs/architecture/README.md b/docs/architecture/README.md
new file mode 100644
index 0000000..c9c95a8
--- /dev/null
+++ b/docs/architecture/README.md
@@ -0,0 +1,76 @@
+# HMM AI 데이터 접근 아키텍처
+
+OCI Draw.io Style Guide의 공식 서비스 도형과 Oracle 색상 규칙으로 정리한 HMM 데모
+아키텍처다. Draw.io 원본은 두 페이지로 구성한다.
+
+| 페이지 | 설명 | 미리보기 |
+|---|---|---|
+| `01 · 전체 구성` | HMM 사용자, Compute, Backoffice MCP, ADB, GenAI, Object Storage, AWS RDS federation | [SVG](hmm-ai-data-access-architecture-overview.svg) · [PNG](hmm-ai-data-access-architecture-overview.png) |
+| `02 · MCP 요청과 VPD` | Tool 광고 시작 검증, 사용자 Bearer 인증, Agent Tool과 Select AI 분기, `CB_ORDS` VPD 실행 | [SVG](hmm-ai-data-access-architecture-security-flow.svg) · [PNG](hmm-ai-data-access-architecture-security-flow.png) |
+
+편집 원본: [hmm-ai-data-access-architecture.drawio](hmm-ai-data-access-architecture.drawio)
+
+## 현행 기준
+
+- 사용자별 MCP: `https://hmm-backoffice.cloud-handson.com/mcp`
+- 공개 Tool 계약: `BACKOFFICE_MCP_TOOLS`
+- ADB Agent Tool: `resolve_hr_term`, `search_hr_policy`
+- Java Select AI Tool: `search_hr_data`
+- Select AI SQL 생성: ADMIN 세션
+- 보호 SQL 실행: `CB_ORDS` 비면제 읽기 전용 세션
+- VPD 문맥: `HMM_ACCESS_CTX`
+- 휴가 정책: `HMM_LEAVE_SCOPE_POLICY`
+- 정책 문서: Object Storage PDF → Abstract/Tag/Chunk/Embed 4 Vector
+- 선사 실적: AWS RDS PostgreSQL → `HMM_RDS_PG_LINK` → ADB federation View
+- 공용 호환 게이트웨이 `hmm-mcp.cloud-handson.com`은 사용자별 VPD MCP 주소가 아니다.
+
+## 흐름을 읽는 방법
+
+- 파란 실선: 사용자 요청 또는 읽기 전용 데이터 실행
+- Oracle red 실선: 인증, 보안 Context, VPD 적용
+- 주황 점선: 모델 호출 또는 문서 적재
+- 회색 실선: 외부 PostgreSQL federation
+- 녹색 실선: 정상 결과와 세션 정리
+
+`AGENT_TOOL`과 `SELECT_AI`의 DB 세션은 구분한다.
+
+- `AGENT_TOOL`: 기본 datasource의 같은 connection에서
+ `SET_USER_BY_BEARER → DBMS_CLOUD_AI_AGENT.RUN_TOOL → CLEAR_USER`
+- `search_hr_data`: ADMIN이 SQL만 생성하고, `CB_ORDS`의 같은 connection에서
+ `SET_VPD_CONTEXT → SELECT → ROLLBACK → CLEAR_VPD_CONTEXT`
+
+## 재생성
+
+로컬 OCI 라이브러리 경로를 명시한다. 비밀번호, Token, OCID는 입력하지 않는다.
+
+```bash
+node tools/architecture/generate-hmm-oci-architecture.mjs \
+ "/Users/joungminko/Downloads/OCI Style Guide for Drawio/OCI Library.xml" \
+ docs/architecture
+```
+
+SVG와 PNG는 diagrams.net의 embed exporter를 Playwright로 호출해 실제 Draw.io 렌더링과
+동일하게 만든다. 인터넷 연결과 Playwright Chromium이 필요하다.
+
+```bash
+node tools/architecture/export-drawio.mjs \
+ docs/architecture/hmm-ai-data-access-architecture.drawio \
+ docs/architecture
+```
+
+구조, 필수 문구, OCI stencil, 비밀값 패턴, 렌더링 크기를 검사한다.
+
+```bash
+node tools/architecture/validate-hmm-architecture.mjs docs/architecture
+```
+
+## 스타일 기준
+
+- 기준 파일: `OCI Library.xml`, `Read-ME.drawio`
+- 글꼴: Oracle Sans
+- 본문: `#312D2A`
+- OCI 경계: `#F5F4F2` / `#9E9892`
+- OCI 강조: `#AE562C`
+- 보안 강조: `#C74634`
+- 외부 Cloud: 흰 배경과 회색 점선
+- Style Guide의 안내용 pink와 Courier New는 사용하지 않는다.
diff --git a/docs/architecture/hmm-ai-data-access-architecture-overview.png b/docs/architecture/hmm-ai-data-access-architecture-overview.png
new file mode 100644
index 0000000..9db347e
Binary files /dev/null and b/docs/architecture/hmm-ai-data-access-architecture-overview.png differ
diff --git a/docs/architecture/hmm-ai-data-access-architecture-overview.svg b/docs/architecture/hmm-ai-data-access-architecture-overview.svg
new file mode 100644
index 0000000..b69368f
--- /dev/null
+++ b/docs/architecture/hmm-ai-data-access-architecture-overview.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/architecture/hmm-ai-data-access-architecture-security-flow.png b/docs/architecture/hmm-ai-data-access-architecture-security-flow.png
new file mode 100644
index 0000000..ed22042
Binary files /dev/null and b/docs/architecture/hmm-ai-data-access-architecture-security-flow.png differ
diff --git a/docs/architecture/hmm-ai-data-access-architecture-security-flow.svg b/docs/architecture/hmm-ai-data-access-architecture-security-flow.svg
new file mode 100644
index 0000000..3817318
--- /dev/null
+++ b/docs/architecture/hmm-ai-data-access-architecture-security-flow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/architecture/hmm-ai-data-access-architecture.drawio b/docs/architecture/hmm-ai-data-access-architecture.drawio
new file mode 100644
index 0000000..78cf895
--- /dev/null
+++ b/docs/architecture/hmm-ai-data-access-architecture.drawio
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/design/745-hmm-oci-architecture/README.md b/docs/design/745-hmm-oci-architecture/README.md
new file mode 100644
index 0000000..29170af
--- /dev/null
+++ b/docs/design/745-hmm-oci-architecture/README.md
@@ -0,0 +1,128 @@
+# #745 OCI 스타일 HMM AI 데이터 접근 아키텍처
+
+> 상태: Approved
+> Redmine: #745
+> 구현 산출물: `docs/architecture/hmm-ai-data-access-architecture.*`
+> 생성 도구: `tools/architecture/generate-hmm-oci-architecture.mjs`
+
+## 목적
+
+HMM 데모의 애플리케이션, MCP, ADB 보안 실행 경계, AI 모델, 문서 지식과 외부
+PostgreSQL federation을 한 장에서 설명할 수 있는 아키텍처 그림을 만든다. 세부 요청 흐름은
+두 번째 페이지에 분리해 사용자 Bearer Token이 실제 VPD 행 필터로 연결되는 지점을 명확히 한다.
+
+기준 스타일은 `/Users/joungminko/Downloads/OCI Style Guide for Drawio`의
+`OCI Library.xml`과 `Read-ME.drawio`다. OCI 서비스는 공식 라이브러리 도형을 사용하고,
+외부 서비스는 `3rd Party Cloud` 경계와 중립 색상으로 구분한다.
+
+## 산출물
+
+| 파일 | 용도 |
+|---|---|
+| `docs/architecture/hmm-ai-data-access-architecture.drawio` | 편집 가능한 2페이지 원본 |
+| `docs/architecture/hmm-ai-data-access-architecture-overview.svg` | 전체 구성 검토·문서 삽입용 |
+| `docs/architecture/hmm-ai-data-access-architecture-security-flow.svg` | 요청·보안 흐름 검토용 |
+| 같은 이름의 `.png` | 일반 문서·메신저 공유용 |
+| `docs/architecture/README.md` | 범례, 현행 기준, 편집·재생성 방법 |
+
+## 페이지 1: 전체 구성
+
+다음 경계를 왼쪽에서 오른쪽으로 배치한다.
+
+1. HMM 사용자: 팀장, 팀원, 관리자
+2. 인터넷 공개 HTTPS: `hmm.cloud-handson.com`, `hmm-backoffice.cloud-handson.com`
+3. OCI 서울 리전
+ - Compute VM + Nginx
+ - AI Web Agent Console
+ - VPD Backoffice + 사용자 Bearer MCP `/mcp`
+ - OCI Object Storage의 HR PDF
+ - Autonomous Database HMMAIPOC
+ - OCI Generative AI
+4. 3rd Party Cloud: AWS RDS for PostgreSQL
+
+ADB 내부에는 아래 실행 요소를 표시한다.
+
+- MCP Tool catalog와 시작 검증: `BACKOFFICE_MCP_TOOLS` → `USER_AI_AGENT_TOOLS.ENABLED`
+- ADB Agent Tool: `resolve_hr_term`, `search_hr_policy`
+- Java `SELECT_AI`: `search_hr_data`
+- 비면제 런타임 사용자 `CB_ORDS`
+- `HMM_ACCESS_CTX`, `HMM_LEAVE_SCOPE_POLICY`, HR 정형 테이블
+- 문서 메타데이터, Abstract, Tag, Chunk, Embed 4 Vector
+- `HMM_RDS_PG_LINK`와 `HMM_RDS_*_V`, 선사 배정 View
+
+모델 연결은 용도를 구분한다.
+
+- GPT-5.4 Mini: 질문 계획, Select AI SQL 생성, 답변 합성
+- Cohere Embed 4: HR 용어와 정책 문서 임베딩
+
+## 페이지 2: MCP 요청과 VPD 적용 흐름
+
+다음 순서를 번호로 표시한다.
+
+1. Agent가 인증된 `initialize`/`tools/list`를 호출한다.
+2. 백오피스가 사용자 Bearer Token의 해시, 만료, 회수와 재직 상태를 확인한다.
+3. `tools/call`에서 Tool 유형을 분기한다.
+4. `AGENT_TOOL`은 `DBMS_CLOUD_AI_AGENT.RUN_TOOL`을 호출한다.
+5. `search_hr_data`는 ADMIN 세션에서 `DBMS_CLOUD_AI.GENERATE(..., 'showsql')`로
+ 승인 객체만 사용하는 읽기 전용 SQL을 만든다.
+6. 같은 `CB_ORDS` JDBC 세션에서 `CB_ORDS_HANDLER_PKG.SET_VPD_CONTEXT`를 호출한다.
+7. `HMM_ACCESS_CTX_PKG.SET_USER_BY_BEARER`가 `HMM_ACCESS_CTX`를 설정한다.
+8. `CB_ORDS`가 SQL을 실행하면 `HMM_LEAVE_SCOPE_POLICY`가 SELF/MANAGED_TEAM/ALL
+ predicate를 자동 적용한다.
+9. 트랜잭션을 rollback하고 `CLEAR_VPD_CONTEXT`로 세션 문맥을 제거한다.
+10. 필터링된 결과와 근거만 Agent에 반환한다.
+
+서버 기동 흐름은 요청 흐름과 분리한다. `AGENT_TOOL targetName`이
+`USER_AI_AGENT_TOOLS`에 없거나 `ENABLED`가 아니면 서버는 준비 상태가 되지 않으며
+불완전한 `tools/list`를 광고하지 않는다.
+
+## 시각 규칙
+
+- 기본 글꼴: `Oracle Sans`, 대체 글꼴 `Arial`, `sans-serif`
+- 본문 색: `#312D2A`
+- OCI 경계 배경/선: `#F5F4F2` / `#9E9892`
+- OCI 강조선: `#AE562C`
+- Oracle red 강조: `#C74634`
+- 외부 Cloud 경계: 흰색 배경, `#6B7280` 점선
+- 정상 데이터 흐름: 실선, 보안·컨텍스트 적용: Oracle red 실선, 관리/적재: 점선
+- 선 교차를 최소화하고 연결선에는 동작 이름을 직접 표기한다.
+- 스타일 가이드의 안내 전용 pink와 Courier New는 사용하지 않는다.
+
+## 생성기 계약
+
+`generate-hmm-oci-architecture.mjs`는 OCI Library 경로와 출력 디렉터리를 인자로 받는다.
+라이브러리에서 아래 공식 도형을 제목으로 찾아 원본 Draw.io에 포함한다.
+
+- `Identity and Security - User`
+- `Compute - Virtual Machine VM`
+- `Database - Autonomous DB`
+- `Analytics and AI - Artificial Intelligence`
+- `Storage - Object Storage`
+- `Identity and Security - Vault`
+
+필수 도형이 없으면 불완전한 그림을 만들지 않고 실패한다. 생성 결과는 비밀번호, Token,
+OCID를 포함하지 않는다.
+
+## 검증
+
+1. `.drawio`가 XML로 파싱되고 페이지가 정확히 2개다.
+2. 두 페이지 모두 필수 구성요소와 흐름 라벨을 포함한다.
+3. 공식 OCI 라이브러리 stencil이 원본에 포함된다.
+4. SVG를 PNG로 렌더링해 잘림, 겹침, 읽기 어려운 글자, 불필요한 선 교차를 확인한다.
+5. `git diff --check`와 비밀값 패턴 검사를 통과한다.
+
+## 롤백
+
+`docs/architecture/hmm-ai-data-access-architecture*`와 생성기를 제거하면 된다. 애플리케이션,
+DB, OCI 리소스와 운영 설정은 이 작업에서 변경하지 않는다.
+
+## 검증 결과
+
+- Draw.io XML 파싱: PASS
+- 페이지 수와 이름: `2` / `01 · 전체 구성`, `02 · MCP 요청과 VPD`
+- OCI 공식 stencil 포함: PASS
+- 필수 구성요소·보안 흐름 검사: PASS
+- 비밀값 패턴 검사: PASS
+- 동일 입력 재생성 byte 비교: PASS
+- diagrams.net 실제 렌더링: `1840×1120`, `1840×1090`
+- PNG 육안 검사: 흰 배경, 제목 대비, 구성요소 잘림 없음, 주요 연결선과 설명 식별 가능
diff --git a/tools/architecture/export-drawio.mjs b/tools/architecture/export-drawio.mjs
new file mode 100644
index 0000000..7bde93f
--- /dev/null
+++ b/tools/architecture/export-drawio.mjs
@@ -0,0 +1,118 @@
+#!/usr/bin/env node
+
+import fs from "node:fs";
+import path from "node:path";
+import { chromium } from "playwright";
+
+const sourcePath = path.resolve(
+ process.argv[2] || "docs/architecture/hmm-ai-data-access-architecture.drawio",
+);
+const outputDir = path.resolve(process.argv[3] || path.dirname(sourcePath));
+const source = fs.readFileSync(sourcePath, "utf8");
+const diagrams = [...source.matchAll(
+ /]*name="([^"]+)"[^>]*>[\s\S]*?<\/diagram>/g,
+)];
+
+if (diagrams.length !== 2) {
+ throw new Error(`Expected 2 Draw.io pages, found ${diagrams.length}`);
+}
+
+const outputs = [
+ "hmm-ai-data-access-architecture-overview",
+ "hmm-ai-data-access-architecture-security-flow",
+];
+const embedUrl = "https://embed.diagrams.net/?embed=1&ui=min&spin=1&proto=json";
+
+function singlePageMxfile(diagram) {
+ return [
+ '',
+ '',
+ diagram,
+ "",
+ ].join("");
+}
+
+function decodeDataUrl(data, mimeType) {
+ const base64Prefix = `data:${mimeType};base64,`;
+ if (data.startsWith(base64Prefix)) {
+ return Buffer.from(data.slice(base64Prefix.length), "base64");
+ }
+ const utf8Prefix = `data:${mimeType},`;
+ if (data.startsWith(utf8Prefix)) {
+ return Buffer.from(decodeURIComponent(data.slice(utf8Prefix.length)), "utf8");
+ }
+ throw new Error(`Unexpected diagrams.net export prefix: ${data.slice(0, 80)}`);
+}
+
+async function exportPage(browser, xml, format) {
+ const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } });
+ const html = `
+
+
+`;
+ await page.setContent(html, { waitUntil: "domcontentloaded" });
+ await page.waitForFunction(
+ () => window.exportResult || window.exportError,
+ null,
+ { timeout: 120000 },
+ );
+ const result = await page.evaluate(() => ({
+ data: window.exportResult,
+ error: window.exportError,
+ }));
+ await page.close();
+ if (result.error) {
+ throw new Error(result.error);
+ }
+ return result.data;
+}
+
+fs.mkdirSync(outputDir, { recursive: true });
+const browser = await chromium.launch({ headless: true });
+try {
+ for (let index = 0; index < diagrams.length; index += 1) {
+ const xml = singlePageMxfile(diagrams[index][0]);
+ for (const format of ["svg", "png"]) {
+ const data = await exportPage(browser, xml, format);
+ const mimeType = format === "svg" ? "image/svg+xml" : "image/png";
+ const output = path.join(outputDir, `${outputs[index]}.${format}`);
+ fs.writeFileSync(output, decodeDataUrl(data, mimeType));
+ console.log(output);
+ }
+ }
+} finally {
+ await browser.close();
+}
diff --git a/tools/architecture/generate-hmm-oci-architecture.mjs b/tools/architecture/generate-hmm-oci-architecture.mjs
new file mode 100644
index 0000000..eae5979
--- /dev/null
+++ b/tools/architecture/generate-hmm-oci-architecture.mjs
@@ -0,0 +1,801 @@
+#!/usr/bin/env node
+
+import fs from "node:fs";
+import path from "node:path";
+import zlib from "node:zlib";
+
+const DEFAULT_OUTPUT_DIR = "docs/architecture";
+const libraryInput = process.argv[2] || process.env.OCI_DRAWIO_LIBRARY;
+if (!libraryInput) {
+ throw new Error(
+ "OCI Draw.io library path is required as the first argument or OCI_DRAWIO_LIBRARY",
+ );
+}
+const libraryPath = path.resolve(libraryInput);
+const outputDir = path.resolve(process.argv[3] || DEFAULT_OUTPUT_DIR);
+
+const COLORS = {
+ ink: "#312D2A",
+ muted: "#5B5652",
+ oracleRed: "#C74634",
+ oracleOrange: "#AE562C",
+ regionFill: "#F5F4F2",
+ regionStroke: "#9E9892",
+ panel: "#FFFFFF",
+ panelSoft: "#FCFBFA",
+ thirdParty: "#6B7280",
+ success: "#2F6B4F",
+ blue: "#2F6F8F",
+};
+
+const REQUIRED_ICONS = [
+ "Identity and Security - User",
+ "Compute - Virtual Machine VM",
+ "Database - Autonomous DB",
+ "Analytics and AI - Artificial Intelligence",
+ "Storage - Object Storage",
+ "Identity and Security - Vault",
+];
+
+function escapeXml(value) {
+ return String(value)
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """);
+}
+
+function readLibrary(file) {
+ const raw = fs.readFileSync(file, "utf8").trim();
+ const prefix = "";
+ const suffix = "";
+ if (!raw.startsWith(prefix) || !raw.endsWith(suffix)) {
+ throw new Error(`Invalid Draw.io library: ${file}`);
+ }
+ const entries = JSON.parse(raw.slice(prefix.length, -suffix.length));
+ const byTitle = new Map(entries.map((entry) => [entry.title, entry]));
+ const missing = REQUIRED_ICONS.filter((title) => !byTitle.has(title));
+ if (missing.length) {
+ throw new Error(`Required OCI library shapes not found: ${missing.join(", ")}`);
+ }
+ return byTitle;
+}
+
+function decodeLibraryEntry(entry) {
+ const encoded = Buffer.from(entry.xml, "base64");
+ return decodeURIComponent(zlib.inflateRawSync(encoded).toString("utf8"));
+}
+
+function style(parts) {
+ return Object.entries(parts)
+ .filter(([, value]) => value !== undefined && value !== null)
+ .map(([key, value]) => `${key}=${value}`)
+ .join(";") + ";";
+}
+
+function createPage(name, id, width, height) {
+ let sequence = 1;
+ const cells = [
+ ``
+ + ``
+ + ``,
+ ];
+ const nextId = (prefix = "c") => `${id}_${prefix}_${sequence++}`;
+
+ function vertex({
+ x,
+ y,
+ w,
+ h,
+ value = "",
+ cellStyle,
+ parent = "1",
+ cellId = nextId("v"),
+ connectable,
+ }) {
+ const connectableAttr = connectable === false ? ' connectable="0"' : "";
+ cells.push(
+ ``
+ + ``
+ + ``,
+ );
+ return cellId;
+ }
+
+ function edge({
+ source,
+ target,
+ value = "",
+ cellStyle,
+ points = [],
+ cellId = nextId("e"),
+ }) {
+ const pointXml = points.length
+ ? `${points.map(([x, y]) => ``).join("")}`
+ : "";
+ cells.push(
+ ``
+ + `${pointXml}`
+ + ``,
+ );
+ return cellId;
+ }
+
+ function officialIcon(library, title, x, y, parent = "1") {
+ const entry = library.get(title);
+ const decoded = decodeLibraryEntry(entry);
+ const sourceCells = [
+ ...decoded.matchAll(/]*\/>|]*>[\s\S]*?<\/mxCell>/g),
+ ].map((match) => match[0]);
+ const parsed = sourceCells
+ .map((xml) => ({
+ xml,
+ id: xml.match(/\bid="([^"]+)"/)?.[1],
+ parent: xml.match(/\bparent="([^"]+)"/)?.[1],
+ value: xml.match(/\bvalue="([^"]*)"/)?.[1] ?? "",
+ }))
+ .filter((cell) => cell.id && !["0", "1"].includes(cell.id));
+ const included = parsed.filter((cell) => !cell.value.trim());
+ const wrapperId = nextId("oci");
+ cells.push(
+ ``
+ + ``
+ + ``,
+ );
+ const idMap = new Map(included.map((cell) => [cell.id, nextId("ociPart")]));
+ for (const cell of included) {
+ let xml = cell.xml;
+ xml = xml.replace(/\bid="([^"]+)"/, `id="${idMap.get(cell.id)}"`);
+ xml = xml.replace(/\bparent="([^"]+)"/, (_match, oldParent) => {
+ if (oldParent === "1") {
+ return `parent="${wrapperId}"`;
+ }
+ return `parent="${idMap.get(oldParent) || wrapperId}"`;
+ });
+ xml = xml.replace(/\b(source|target)="([^"]+)"/g, (_match, attr, oldId) =>
+ `${attr}="${idMap.get(oldId) || oldId}"`);
+ cells.push(xml);
+ }
+ return wrapperId;
+ }
+
+ function result() {
+ const model = [
+ ``,
+ "",
+ '',
+ '',
+ ...cells,
+ "",
+ "",
+ ].join("");
+ return `${model}`;
+ }
+
+ return { vertex, edge, officialIcon, result };
+}
+
+const labelStyle = (options = {}) => style({
+ rounded: 0,
+ whiteSpace: "wrap",
+ html: 1,
+ strokeColor: "none",
+ fillColor: "none",
+ align: options.align || "left",
+ verticalAlign: options.verticalAlign || "middle",
+ fontFamily: "Oracle Sans",
+ fontColor: options.fontColor || COLORS.ink,
+ fontSize: options.fontSize || 12,
+ fontStyle: options.bold ? 1 : 0,
+ spacingLeft: options.spacingLeft || 0,
+ spacingRight: options.spacingRight || 0,
+});
+
+const panelStyle = (options = {}) => style({
+ rounded: 1,
+ arcSize: options.arcSize || 8,
+ whiteSpace: "wrap",
+ html: 1,
+ fillColor: options.fillColor || COLORS.panel,
+ strokeColor: options.strokeColor || COLORS.regionStroke,
+ strokeWidth: options.strokeWidth || 1,
+ dashed: options.dashed ? 1 : 0,
+ fontFamily: "Oracle Sans",
+ fontColor: options.fontColor || COLORS.ink,
+ fontSize: options.fontSize || 12,
+ align: options.align || "center",
+ verticalAlign: options.verticalAlign || "middle",
+ spacing: options.spacing || 6,
+ spacingLeft: options.spacingLeft,
+ spacingRight: options.spacingRight,
+});
+
+const edgeStyle = (options = {}) => style({
+ edgeStyle: "orthogonalEdgeStyle",
+ rounded: 1,
+ orthogonalLoop: 1,
+ jettySize: "auto",
+ html: 1,
+ strokeColor: options.strokeColor || COLORS.ink,
+ strokeWidth: options.strokeWidth || 2,
+ dashed: options.dashed ? 1 : 0,
+ endArrow: options.endArrow || "block",
+ endFill: options.endFill === false ? 0 : 1,
+ fontFamily: "Oracle Sans",
+ fontColor: options.fontColor || COLORS.ink,
+ fontSize: options.fontSize || 11,
+ labelBackgroundColor: "#FFFFFF",
+ exitX: options.exitX,
+ exitY: options.exitY,
+ entryX: options.entryX,
+ entryY: options.entryY,
+});
+
+function title(page, heading, subtitle, width) {
+ page.vertex({
+ x: 36,
+ y: 22,
+ w: width - 72,
+ h: 38,
+ value: heading,
+ cellStyle: labelStyle({ fontSize: 25, bold: true }),
+ });
+ page.vertex({
+ x: 38,
+ y: 60,
+ w: width - 76,
+ h: 28,
+ value: subtitle,
+ cellStyle: labelStyle({ fontSize: 12, fontColor: COLORS.muted }),
+ });
+ page.vertex({
+ x: 36,
+ y: 92,
+ w: width - 72,
+ h: 3,
+ cellStyle: panelStyle({
+ fillColor: COLORS.oracleRed,
+ strokeColor: COLORS.oracleRed,
+ arcSize: 0,
+ }),
+ });
+}
+
+function boundary(page, x, y, w, h, heading, options = {}) {
+ const box = page.vertex({
+ x,
+ y,
+ w,
+ h,
+ value: "",
+ cellStyle: panelStyle({
+ fillColor: options.fillColor || COLORS.regionFill,
+ strokeColor: options.strokeColor || COLORS.regionStroke,
+ strokeWidth: options.strokeWidth || 1,
+ dashed: options.dashed,
+ arcSize: options.arcSize || 7,
+ }),
+ });
+ page.vertex({
+ x: x + 14,
+ y: y + 8,
+ w: w - 28,
+ h: 26,
+ value: `${heading}`,
+ cellStyle: labelStyle({
+ fontSize: options.fontSize || 13,
+ fontColor: options.headingColor || COLORS.ink,
+ }),
+ });
+ return box;
+}
+
+function addOverviewPage(library) {
+ const page = createPage("01 · 전체 구성", "hmm-overview", 1800, 1080);
+ title(
+ page,
+ "HMM AI 데이터 접근 아키텍처",
+ "사용자 Bearer MCP · Select AI · VPD · HR 정책 벡터 검색 · AWS RDS PostgreSQL Federation",
+ 1800,
+ );
+
+ boundary(page, 30, 120, 190, 790, "HMM 사용자", {
+ fillColor: COLORS.panelSoft,
+ });
+ page.officialIcon(library, "Identity and Security - User", 80, 165);
+ const users = page.vertex({
+ x: 54,
+ y: 270,
+ w: 142,
+ h: 150,
+ value: "팀장 · E1001
팀원 · E1002~E1007
HR 관리자
같은 질문이라도 역할과 보고 관계에 따라 다른 행을 조회",
+ cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: "#D4CFCA" }),
+ });
+ const externalAgent = page.vertex({
+ x: 54,
+ y: 465,
+ w: 142,
+ h: 115,
+ value: "외부 Agent
AI Database Private
Agent Factory
MCP client",
+ cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: "#D4CFCA" }),
+ });
+ page.vertex({
+ x: 54,
+ y: 625,
+ w: 142,
+ h: 210,
+ value: "공개 HTTPS
hmm.cloud-handson.com
hmm-backoffice.cloud-handson.com/mcp
사용자 Bearer Token
TLS · HttpOnly portal cookie",
+ cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: COLORS.oracleOrange }),
+ });
+
+ boundary(page, 250, 120, 1260, 900, "Oracle Cloud Infrastructure", {
+ fillColor: "#FBFAF9",
+ });
+ boundary(page, 280, 165, 930, 815, "OCI Region · ap-seoul-1", {
+ fillColor: COLORS.regionFill,
+ });
+ boundary(page, 315, 210, 480, 320, "VCN · Public application subnet", {
+ fillColor: "#FFFFFF",
+ strokeColor: COLORS.oracleOrange,
+ dashed: true,
+ headingColor: COLORS.oracleOrange,
+ });
+
+ page.officialIcon(library, "Compute - Virtual Machine VM", 340, 265);
+ const compute = page.vertex({
+ x: 445,
+ y: 260,
+ w: 320,
+ h: 235,
+ value: "",
+ cellStyle: panelStyle({ fillColor: "#FCFBFA", strokeColor: "#D4CFCA" }),
+ });
+ page.vertex({
+ x: 462,
+ y: 274,
+ w: 286,
+ h: 34,
+ value: "Compute VM · 132.226.232.69",
+ cellStyle: labelStyle({ fontSize: 13, bold: true }),
+ });
+ const nginx = page.vertex({
+ x: 470,
+ y: 320,
+ w: 270,
+ h: 42,
+ value: "Nginx · TLS / reverse proxy",
+ cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: "#D4CFCA" }),
+ });
+ const consoleApp = page.vertex({
+ x: 470,
+ y: 376,
+ w: 270,
+ h: 46,
+ value: "AI Web Agent Console
Streamlit · 질문/근거/감사 UI",
+ cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: COLORS.blue }),
+ });
+ const backoffice = page.vertex({
+ x: 470,
+ y: 436,
+ w: 270,
+ h: 46,
+ value: "VPD Backoffice
Spring Boot · `/mcp` · 권한 관리",
+ cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: COLORS.oracleRed }),
+ });
+
+ page.officialIcon(library, "Identity and Security - Vault", 330, 635);
+ const vault = page.vertex({
+ x: 420,
+ y: 642,
+ w: 160,
+ h: 76,
+ value: "Vault / Secrets
DB·OCI 자격증명
Git 외부 관리",
+ cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: "#D4CFCA" }),
+ });
+
+ page.officialIcon(library, "Storage - Object Storage", 330, 800);
+ const objectStorage = page.vertex({
+ x: 445,
+ y: 804,
+ w: 180,
+ h: 82,
+ value: "Object Storage
HR 규정 PDF 원본
파일명·생성일 메타데이터",
+ cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: "#D4CFCA" }),
+ });
+
+ page.officialIcon(library, "Database - Autonomous DB", 805, 225);
+ const adb = page.vertex({
+ x: 905,
+ y: 220,
+ w: 270,
+ h: 690,
+ value: "",
+ cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: COLORS.oracleRed, strokeWidth: 2 }),
+ });
+ page.vertex({
+ x: 920,
+ y: 238,
+ w: 238,
+ h: 40,
+ value: "Autonomous Database · HMMAIPOC",
+ cellStyle: labelStyle({ fontSize: 14, bold: true }),
+ });
+ const startupGate = page.vertex({
+ x: 925,
+ y: 294,
+ w: 230,
+ h: 72,
+ value: "MCP Tool 계약
BACKOFFICE_MCP_TOOLS
시작 시 USER_AI_AGENT_TOOLS.ENABLED 검증",
+ cellStyle: panelStyle({ fillColor: "#FFF7F2", strokeColor: COLORS.oracleOrange }),
+ });
+ const agentTools = page.vertex({
+ x: 925,
+ y: 382,
+ w: 230,
+ h: 78,
+ value: "ADB Agent Tools
resolve_hr_term
search_hr_policy
DBMS_CLOUD_AI_AGENT.RUN_TOOL",
+ cellStyle: panelStyle({ fillColor: "#FCFBFA", strokeColor: "#D4CFCA" }),
+ });
+ const selectAi = page.vertex({
+ x: 925,
+ y: 475,
+ w: 230,
+ h: 80,
+ value: "search_hr_data · SELECT_AI
ADMIN: SHOWSQL 생성/검증
CB_ORDS: 읽기 전용 SQL 실행",
+ cellStyle: panelStyle({ fillColor: "#F4F8FA", strokeColor: COLORS.blue }),
+ });
+ const vpd = page.vertex({
+ x: 925,
+ y: 570,
+ w: 230,
+ h: 92,
+ value: "Bearer → Secure Context → VPD
HMM_ACCESS_CTX
HMM_LEAVE_SCOPE_POLICY
SELF · MANAGED_TEAM · ALL",
+ cellStyle: panelStyle({ fillColor: "#FFF7F2", strokeColor: COLORS.oracleRed }),
+ });
+ const hrData = page.vertex({
+ x: 925,
+ y: 678,
+ w: 230,
+ h: 72,
+ value: "HMM HR 정형 데이터
직원·팀·휴가·근태
역할·그룹·권한·Token hash",
+ cellStyle: panelStyle({ fillColor: "#FCFBFA", strokeColor: "#D4CFCA" }),
+ });
+ const vectorData = page.vertex({
+ x: 925,
+ y: 765,
+ w: 230,
+ h: 62,
+ value: "정책 지식
Abstract · Tag · Chunk · Embed 4 Vector",
+ cellStyle: panelStyle({ fillColor: "#FCFBFA", strokeColor: "#D4CFCA" }),
+ });
+ const federation = page.vertex({
+ x: 925,
+ y: 842,
+ w: 230,
+ h: 52,
+ value: "Federation View
HMM_RDS_*_V · 선사 배정 View",
+ cellStyle: panelStyle({ fillColor: "#FCFBFA", strokeColor: "#D4CFCA" }),
+ });
+
+ boundary(page, 1235, 165, 240, 300, "OCI Region · us-chicago-1", {
+ fillColor: COLORS.regionFill,
+ });
+ page.officialIcon(library, "Analytics and AI - Artificial Intelligence", 1260, 220);
+ const genai = page.vertex({
+ x: 1260,
+ y: 320,
+ w: 190,
+ h: 110,
+ value: "OCI Generative AI
GPT-5.4 Mini
질문 계획 · SQL 생성 · 답변
Cohere Embed 4
용어·문서 Vector",
+ cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: COLORS.oracleOrange }),
+ });
+
+ boundary(page, 1535, 120, 235, 900, "3rd Party Cloud · AWS", {
+ fillColor: "#FFFFFF",
+ strokeColor: COLORS.thirdParty,
+ dashed: true,
+ headingColor: COLORS.thirdParty,
+ });
+ const rds = page.vertex({
+ x: 1570,
+ y: 530,
+ w: 165,
+ h: 180,
+ value: "AWS RDS PostgreSQL
hmm_demo.carriers
carrier_monthly_performance
가상 선사 8개
월간 실적 144행",
+ cellStyle: style({
+ shape: "cylinder3",
+ boundedLbl: 1,
+ backgroundOutline: 1,
+ size: 15,
+ whiteSpace: "wrap",
+ html: 1,
+ fillColor: "#F9FAFB",
+ strokeColor: COLORS.thirdParty,
+ fontFamily: "Oracle Sans",
+ fontColor: COLORS.ink,
+ fontSize: 12,
+ spacingTop: 12,
+ }),
+ });
+
+ page.edge({
+ source: users,
+ target: nginx,
+ value: "HTTPS · 질문/관리",
+ cellStyle: edgeStyle({ strokeColor: COLORS.blue, exitX: 1, exitY: 0.35, entryX: 0, entryY: 0.5 }),
+ });
+ page.edge({
+ source: externalAgent,
+ target: backoffice,
+ value: "MCP initialize · tools/list · tools/call",
+ cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed, exitX: 1, exitY: 0.5, entryX: 0, entryY: 0.5 }),
+ });
+ page.edge({
+ source: nginx,
+ target: consoleApp,
+ value: "",
+ cellStyle: edgeStyle({ strokeColor: COLORS.blue, exitX: 0.5, exitY: 1, entryX: 0.5, entryY: 0 }),
+ });
+ page.edge({
+ source: consoleApp,
+ target: backoffice,
+ value: "MCP",
+ cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed, exitX: 1, exitY: 0.5, entryX: 1, entryY: 0.5 }),
+ points: [[780, 399], [780, 459]],
+ });
+ page.edge({
+ source: backoffice,
+ target: startupGate,
+ value: "JDBC · Tool 호출",
+ cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed, exitX: 1, exitY: 0.5, entryX: 0, entryY: 0.5 }),
+ points: [[805, 459], [885, 459], [885, 330]],
+ });
+ page.edge({
+ source: startupGate,
+ target: agentTools,
+ value: "ENABLED",
+ cellStyle: edgeStyle({ strokeColor: COLORS.success, exitX: 0.5, exitY: 1, entryX: 0.5, entryY: 0 }),
+ });
+ page.edge({
+ source: startupGate,
+ target: selectAi,
+ value: "Java Tool",
+ cellStyle: edgeStyle({ strokeColor: COLORS.blue, exitX: 1, exitY: 0.5, entryX: 1, entryY: 0.5 }),
+ points: [[1170, 330], [1170, 515]],
+ });
+ page.edge({
+ source: selectAi,
+ target: vpd,
+ value: "동일 CB_ORDS session",
+ cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed, exitX: 0.5, exitY: 1, entryX: 0.5, entryY: 0 }),
+ });
+ page.edge({
+ source: vpd,
+ target: hrData,
+ value: "predicate 자동 적용",
+ cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed, exitX: 0.5, exitY: 1, entryX: 0.5, entryY: 0 }),
+ });
+ page.edge({
+ source: objectStorage,
+ target: vectorData,
+ value: "PDF ingest · 청킹",
+ cellStyle: edgeStyle({ dashed: true, strokeColor: COLORS.oracleOrange, exitX: 1, exitY: 0.5, entryX: 0, entryY: 0.5 }),
+ });
+ page.edge({
+ source: vault,
+ target: compute,
+ value: "runtime secret",
+ cellStyle: edgeStyle({ dashed: true, strokeColor: COLORS.regionStroke, exitX: 0.5, exitY: 0, entryX: 0, entryY: 1 }),
+ });
+ page.edge({
+ source: adb,
+ target: genai,
+ value: "모델 호출",
+ cellStyle: edgeStyle({ dashed: true, strokeColor: COLORS.oracleOrange, exitX: 1, exitY: 0.28, entryX: 0, entryY: 0.55 }),
+ });
+ page.edge({
+ source: federation,
+ target: rds,
+ value: "HMM_RDS_PG_LINK · TLS 5432",
+ cellStyle: edgeStyle({ strokeColor: COLORS.thirdParty, exitX: 1, exitY: 0.5, entryX: 0, entryY: 0.5 }),
+ });
+
+ page.vertex({
+ x: 1545,
+ y: 830,
+ w: 215,
+ h: 105,
+ value: "읽기 책임 분리
RDS: 선사 실적 원장
ADB: 직원·배정·권한·VPD
MCP: 허용된 View만 자연어 조회",
+ cellStyle: panelStyle({ fillColor: "#F9FAFB", strokeColor: COLORS.thirdParty }),
+ });
+ page.vertex({
+ x: 280,
+ y: 985,
+ w: 1195,
+ h: 24,
+ value: "※ hmm-mcp.cloud-handson.com은 공용 호환 게이트웨이입니다. 사용자별 VPD MCP의 기준 주소는 hmm-backoffice.cloud-handson.com/mcp입니다.",
+ cellStyle: labelStyle({ fontSize: 11, fontColor: COLORS.muted }),
+ });
+ return page.result();
+}
+
+function numberedBox(page, number, x, y, w, h, heading, body, options = {}) {
+ const box = page.vertex({
+ x,
+ y,
+ w,
+ h,
+ value: `${heading}
${body}`,
+ cellStyle: panelStyle({
+ fillColor: options.fillColor || "#FFFFFF",
+ strokeColor: options.strokeColor || "#D4CFCA",
+ align: "left",
+ verticalAlign: "middle",
+ spacing: 10,
+ spacingLeft: 22,
+ spacingRight: 8,
+ fontSize: options.fontSize || 11,
+ }),
+ });
+ page.vertex({
+ x: x - 12,
+ y: y + 10,
+ w: 30,
+ h: 30,
+ value: `${number}`,
+ cellStyle: style({
+ ellipse: 1,
+ shape: "ellipse",
+ whiteSpace: "wrap",
+ html: 1,
+ fillColor: options.badgeColor || COLORS.oracleRed,
+ strokeColor: options.badgeColor || COLORS.oracleRed,
+ fontFamily: "Oracle Sans",
+ fontSize: 13,
+ align: "center",
+ verticalAlign: "middle",
+ }),
+ });
+ return box;
+}
+
+function addSecurityFlowPage(library) {
+ const page = createPage("02 · MCP 요청과 VPD", "hmm-security-flow", 1800, 1050);
+ title(
+ page,
+ "MCP Tool Discovery와 사용자별 VPD 실행 흐름",
+ "광고 계약 검증은 시작 시 1회 · 사용자 인증과 Context 설정·SQL 실행·정리는 요청마다 같은 세션에서 수행",
+ 1800,
+ );
+
+ boundary(page, 35, 120, 1730, 185, "서버 시작 · Tool 광고 전 Fail-Closed Gate", {
+ fillColor: "#FFF7F2",
+ strokeColor: COLORS.oracleOrange,
+ headingColor: COLORS.oracleOrange,
+ });
+ const config = numberedBox(page, "A", 95, 185, 300, 78, "BACKOFFICE_MCP_TOOLS", "공개 이름 · 설명 · 입력 schema
AGENT_TOOL targetName", {
+ strokeColor: COLORS.oracleOrange,
+ badgeColor: COLORS.oracleOrange,
+ });
+ const validator = numberedBox(page, "B", 510, 185, 330, 78, "McpAgentToolStartupValidator", "AGENT_TOOL targetName 중복 제거
SELECT_AI는 DB Tool 검증에서 제외", {
+ strokeColor: COLORS.oracleOrange,
+ badgeColor: COLORS.oracleOrange,
+ });
+ const metadata = numberedBox(page, "C", 955, 185, 330, 78, "USER_AI_AGENT_TOOLS", "Tool 존재 + STATUS=ENABLED 확인", {
+ strokeColor: COLORS.oracleOrange,
+ badgeColor: COLORS.oracleOrange,
+ });
+ const ready = numberedBox(page, "D", 1400, 185, 285, 78, "Endpoint Ready", "모두 정상일 때만 tools/list 광고
누락·비활성·조회 오류는 기동 실패", {
+ strokeColor: COLORS.success,
+ badgeColor: COLORS.success,
+ });
+ page.edge({ source: config, target: validator, value: "구성 로드", cellStyle: edgeStyle({ strokeColor: COLORS.oracleOrange }) });
+ page.edge({ source: validator, target: metadata, value: "시작 시 1회 조회", cellStyle: edgeStyle({ strokeColor: COLORS.oracleOrange }) });
+ page.edge({ source: metadata, target: ready, value: "모두 ENABLED", cellStyle: edgeStyle({ strokeColor: COLORS.success }) });
+
+ boundary(page, 35, 330, 225, 665, "MCP Client", { fillColor: "#F9FAFB" });
+ page.officialIcon(library, "Identity and Security - User", 95, 385);
+ const client = numberedBox(page, 1, 70, 500, 160, 100, "Agent 요청", "initialize
tools/list
tools/call", {
+ strokeColor: COLORS.blue,
+ badgeColor: COLORS.blue,
+ });
+ const response = numberedBox(page, 10, 70, 800, 160, 115, "필터링 결과", "허용 행 + 근거만 반환
Token·내부 SQL은 미노출", {
+ strokeColor: COLORS.success,
+ badgeColor: COLORS.success,
+ });
+
+ boundary(page, 285, 330, 650, 665, "VPD Backoffice · Spring Boot", {
+ fillColor: "#FCFBFA",
+ });
+ const auth = numberedBox(page, 2, 330, 400, 250, 110, "Bearer 인증", "Token SHA-256 hash
만료·회수·재직 상태 검증
실패: HTTP 401", {
+ strokeColor: COLORS.oracleRed,
+ });
+ const router = numberedBox(page, 3, 650, 400, 235, 110, "MCP Tool Router", "catalog에서 이름·인자 검증
AGENT_TOOL / SELECT_AI 분기", {
+ strokeColor: COLORS.oracleRed,
+ });
+ const agentTool = numberedBox(page, 4, 350, 590, 250, 130, "ADB AGENT_TOOL", "resolve_hr_term · search_hr_policy
동일 기본 JDBC session
SET_USER_BY_BEARER
→ RUN_TOOL → CLEAR_USER", {
+ strokeColor: COLORS.oracleOrange,
+ badgeColor: COLORS.oracleOrange,
+ fontSize: 10,
+ });
+ const showSql = numberedBox(page, 5, 650, 590, 235, 125, "search_hr_data", "ADMIN 생성 세션
DBMS_CLOUD_AI.GENERATE
action=showsql
SELECT/WITH + 승인 객체 검증", {
+ strokeColor: COLORS.blue,
+ badgeColor: COLORS.blue,
+ });
+ page.edge({ source: client, target: auth, value: "Authorization: Bearer …", cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed }) });
+ page.edge({ source: auth, target: router, value: "인증된 employee", cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed }) });
+ page.edge({ source: router, target: agentTool, value: "AGENT_TOOL", cellStyle: edgeStyle({ strokeColor: COLORS.oracleOrange, exitX: 0.25, exitY: 1, entryX: 0.5, entryY: 0 }) });
+ page.edge({ source: router, target: showSql, value: "SELECT_AI", cellStyle: edgeStyle({ strokeColor: COLORS.blue, exitX: 0.75, exitY: 1, entryX: 0.5, entryY: 0 }) });
+
+ boundary(page, 960, 330, 805, 665, "Autonomous Database · 보안 실행 경계", {
+ fillColor: COLORS.regionFill,
+ strokeColor: COLORS.oracleRed,
+ });
+ page.officialIcon(library, "Database - Autonomous DB", 995, 370);
+ const setContext = numberedBox(page, 6, 1100, 390, 285, 110, "CB_ORDS 동일 JDBC 세션", "CB_ORDS_HANDLER_PKG
SET_VPD_CONTEXT
Runtime principal·context 일치 검증", {
+ strokeColor: COLORS.oracleRed,
+ });
+ const context = numberedBox(page, 7, 1445, 390, 270, 110, "Secure Application Context", "HMM_ACCESS_CTX_PKG
SET_USER_BY_BEARER
EMPLOYEE_ID · CODE · TEAM_ID", {
+ strokeColor: COLORS.oracleRed,
+ });
+ const execute = numberedBox(page, 8, 1100, 585, 285, 125, "읽기 전용 SQL 실행", "CB_ORDS · SET TRANSACTION READ ONLY
EXEMPT ACCESS POLICY 없음
승인된 HR Table/View만 조회", {
+ strokeColor: COLORS.blue,
+ badgeColor: COLORS.blue,
+ });
+ const policy = numberedBox(page, "8a", 1445, 585, 270, 125, "HMM_LEAVE_SCOPE_POLICY", "SYS_CONTEXT 기반 predicate 자동 적용
VIEWER → SELF
MANAGER → MANAGED_TEAM
ADMIN role → ALL", {
+ strokeColor: COLORS.oracleRed,
+ });
+ const cleanup = numberedBox(page, 9, 1100, 805, 615, 95, "항상 정리", "결과 읽기 → ROLLBACK → CB_ORDS_HANDLER_PKG · CLEAR_VPD_CONTEXT
성공·실패와 무관하게 session context와 client identifier 제거", {
+ strokeColor: COLORS.success,
+ badgeColor: COLORS.success,
+ });
+ page.edge({ source: showSql, target: setContext, value: "검증된 SQL + 원문 Bearer", cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed }) });
+ page.edge({
+ source: agentTool,
+ target: response,
+ value: "Agent Tool 결과 · CLEAR_USER",
+ cellStyle: edgeStyle({ dashed: true, strokeColor: COLORS.success, exitX: 1, exitY: 0.5, entryX: 1, entryY: 0.5 }),
+ points: [[635, 655], [635, 760], [280, 760], [280, 855]],
+ });
+ page.edge({ source: setContext, target: context, value: "SET_USER_BY_BEARER", cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed }) });
+ page.edge({ source: setContext, target: execute, value: "context 확인 후 실행", cellStyle: edgeStyle({ strokeColor: COLORS.blue, exitX: 0.5, exitY: 1, entryX: 0.5, entryY: 0 }) });
+ page.edge({ source: context, target: policy, value: "SYS_CONTEXT", cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed, exitX: 0.5, exitY: 1, entryX: 0.5, entryY: 0 }) });
+ page.edge({ source: execute, target: policy, value: "SELECT → predicate 주입", cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed }) });
+ page.edge({ source: policy, target: cleanup, value: "허용 행", cellStyle: edgeStyle({ strokeColor: COLORS.success, exitX: 0.5, exitY: 1, entryX: 0.8, entryY: 0 }) });
+ page.edge({ source: execute, target: cleanup, value: "result set", cellStyle: edgeStyle({ strokeColor: COLORS.success, exitX: 0.5, exitY: 1, entryX: 0.2, entryY: 0 }) });
+ page.edge({
+ source: cleanup,
+ target: response,
+ value: "정제된 Tool 결과",
+ cellStyle: edgeStyle({ strokeColor: COLORS.success, exitX: 0, exitY: 0.5, entryX: 1, entryY: 0.5 }),
+ points: [[950, 945], [270, 945]],
+ });
+
+ page.vertex({
+ x: 330,
+ y: 790,
+ w: 555,
+ h: 110,
+ value: "보안 불변조건
• Token 원문은 DB·로그·응답·Git에 저장하지 않음
• SQL 생성 ADMIN과 실행 CB_ORDS를 분리
• 컨텍스트 설정과 보호 객체 SELECT는 반드시 같은 connection
• 권한 또는 메타데이터가 불완전하면 결과를 축소하는 대신 요청/기동 실패",
+ cellStyle: panelStyle({ fillColor: "#FFF7F2", strokeColor: COLORS.oracleRed, align: "left", verticalAlign: "middle", spacing: 10 }),
+ });
+ return page.result();
+}
+
+function writeDrawio(library) {
+ const diagrams = [addOverviewPage(library), addSecurityFlowPage(library)];
+ const mxfile = [
+ '',
+ '',
+ ...diagrams,
+ "",
+ ].join("\n");
+ fs.mkdirSync(outputDir, { recursive: true });
+ const output = path.join(outputDir, "hmm-ai-data-access-architecture.drawio");
+ fs.writeFileSync(output, mxfile, "utf8");
+ return output;
+}
+
+const library = readLibrary(libraryPath);
+const output = writeDrawio(library);
+console.log(output);
diff --git a/tools/architecture/validate-hmm-architecture.mjs b/tools/architecture/validate-hmm-architecture.mjs
new file mode 100644
index 0000000..a2b139d
--- /dev/null
+++ b/tools/architecture/validate-hmm-architecture.mjs
@@ -0,0 +1,82 @@
+#!/usr/bin/env node
+
+import fs from "node:fs";
+import path from "node:path";
+
+const architectureDir = path.resolve(process.argv[2] || "docs/architecture");
+const drawioPath = path.join(
+ architectureDir,
+ "hmm-ai-data-access-architecture.drawio",
+);
+const drawio = fs.readFileSync(drawioPath, "utf8");
+const diagrams = [...drawio.matchAll(
+ /]*name="([^"]+)"[^>]*>([\s\S]*?)<\/diagram>/g,
+)];
+
+function assert(condition, message) {
+ if (!condition) {
+ throw new Error(message);
+ }
+}
+
+assert(diagrams.length === 2, `Expected 2 pages, found ${diagrams.length}`);
+assert(
+ diagrams.map((match) => match[1]).join("|") ===
+ "01 · 전체 구성|02 · MCP 요청과 VPD",
+ "Unexpected Draw.io page names",
+);
+
+for (const [name, page] of diagrams.map((match) => [match[1], match[2]])) {
+ const ids = [...page.matchAll(/]*\bid="([^"]+)"/g)]
+ .map((match) => match[1]);
+ assert(ids.length === new Set(ids).size, `Duplicate mxCell id in ${name}`);
+ assert(page.includes("shape=stencil("), `OCI stencil missing in ${name}`);
+}
+
+const requiredText = [
+ "Autonomous Database",
+ "OCI Generative AI",
+ "Object Storage",
+ "AWS RDS PostgreSQL",
+ "BACKOFFICE_MCP_TOOLS",
+ "USER_AI_AGENT_TOOLS.ENABLED",
+ "CB_ORDS",
+ "HMM_ACCESS_CTX",
+ "HMM_LEAVE_SCOPE_POLICY",
+ "HMM_RDS_PG_LINK",
+ "SET_USER_BY_BEARER",
+ "CLEAR_VPD_CONTEXT",
+];
+for (const text of requiredText) {
+ assert(drawio.includes(text), `Required architecture text missing: ${text}`);
+}
+
+const forbidden = [
+ /vpd_live_[A-Za-z0-9_-]{8,}/,
+ /Bearer\s+[A-Za-z0-9_-]{20,}/,
+ /BEGIN (?:RSA |OPENSSH )?PRIVATE KEY/,
+ /Dhfkzmf#/,
+];
+for (const pattern of forbidden) {
+ assert(!pattern.test(drawio), `Sensitive value pattern found: ${pattern}`);
+}
+
+const renderNames = [
+ "hmm-ai-data-access-architecture-overview",
+ "hmm-ai-data-access-architecture-security-flow",
+];
+for (const name of renderNames) {
+ const svg = fs.readFileSync(path.join(architectureDir, `${name}.svg`), "utf8");
+ assert(svg.includes("