- 플랫폼팀님이 작성, 2026-05-12에 최종 변경
Page No. 150569101
작성자 : / 검수자 :
Launch Release No. 7.3.500.20250722 / Latest Release No.
개요
- 기존 데이터베이스는 데이터베이스 -> 스키마 -> 테이블 순의 3단계 구조이지만, Trino는 중간에 카탈로그가 추가된 데이터베이스 -> 카탈로그 -> 스키마 -> 테이블의 4단계 구조를 가집니다.
- Trino는 단일 쿼리로 모든 카탈로그 하위의 스키마 목록을 한 번에 불러오는 표준 명령을 제공하지 않아, 제품의 스키마 추가 화면에서 카탈로그 명을 자동으로 식별하기 어렵습니다.
- 따라서 어떤 카탈로그에 어떤 스키마가 속해 있는지에 대한 종속 관계는 시스템을 구축한 관리자(데이터 엔지니어)만이 명확히 알 수 있는 구조입니다.
- 이처럼 기존 제품의 데이터 구조 및 일반적인 DB 사용법과 확연히 다르기 때문에, 원활한 설정을 위한 별도의 가이드가 작성되었습니다.
1. 임베디드 서버 설정
1-1) application.yml 설정 (Vue, React 공통)
> 사전준비에서 발급받은 키 정보를 바탕으로, 프로젝트 내 설정 파일들을 사이트 환경에 맞게 수정합니다.
파일 위치: {소스경로}/src/main/resources/application.yml

< 그림 1-1. application.yml 설정 정보 캡쳐 >
- port: 사이트 정책에 맞는 포트 번호
- server-url: AUD7 서버 주소로 변경
- x-ap-update-addr: AUD7 주소 도메인으로 변경
- x-aud-ap-id: '1-0.공통 설정' 에서 입력한 어플리케이션 명으로 변경
- x-aud-ap-secret-key: '1-0.공통 설정' 에서 발급받은 Secret Key로 변경
- ssh-key-path: '1-0.공통 설정' 에서 발급받은 private_key.pem 파일의 절대경로로 변경
1-2) 프론트엔드 도메인 설정
> SHARE_DOMAIN 값을 임베디드 서버와 AUD7 서버가 공유하는 상위 도메인으로 설정합니다.
파일 위치: {소스경로}/frontend/src/main.[jsx|js] (React는 jsx, Vue는 js)

< 그림 1-2. main.jsx 설정 정보 캡쳐 >
- SHARE_DOMAIN: 임베디드 서버와 AUD7 서버가 공유하는 상위 도메인으로 설정합니다.
ex) AUD7 서버 주소: aud.example.com, 임베디드 서버 주소: embedded.example.com
→ SHARE_DOMAIN = 'example.com'
1-3) 보고서 코드 설정
> 임베디드 화면에서 호출할 보고서를 설정합니다.
파일 위치: {소스경로}/frontend/src/{DivViewer.[jsx|vue]} or {IframeViewer.[jsx|vue]} (React는 jsx, Vue는 js) (사용 방식(div / iframe)에 따라 수정할 jsx 파일을 선정)

< 그림 1-3. DivViewer.jsx 설정 정보 캡쳐 >
- name: 좌측 메뉴에 표시할 보고서명
- code: AUD7에 등록된 보고서 코드
- module: 보고서 모듈 코드
- isShow: 타이틀바 표시 여부
1-4) 서버 소스 설정
> 사용자 계정 정보와 관련된 서버 소스를 설정 합니다.(Vue, React 공통)
파일 위치: {소스경로}/src/main/java/com/aud/embedded/AudAction.java

< 그림 1-4. AudAction.java 설정 정보 캡쳐 >
- loginUserCode: 토큰을 발급 받아 로그인 처리할 유저 코드명
*공용 계정 사용 시: 임베디드 보고서의 권한을 가진 사용자 코드를 loginUserCode에 고정값으로 지정합니다.
*개별 사용자 인증 시: 외부 포탈의 로그인 세션에서 사용자 코드를 가져와 loginUserCode에 설정합니다.
(해당 사용자 코드는 AUD7 사용자 관리에도 동일하게 등록되어 있어야 합니다.)
2. 빌드
1) {소스경로}/frontend 경로에서 npm run build 빌드 명령어를 실행합니다.
2) 빌드가 완료되면 {소스경로}/frontend/dist/assets/ 폴더에 아래 파일이 생성되는데 해당 결과물을 Spring 정적 리소스 경로로 복사합니다.
-frontend/dist/assets/index.js → src/main/resources/static/assets/index.js
-frontend/dist/assets/index.css -> src/main/resources/static/assets/index.css
3. 서버 실행
> IntelliJ 등 IDE에서 SpringThymeleafTestApplication 클래스를 실행합니다.
파일 위치: {소스경로}/src/main/java/com/aud/embedded/SpringThymeleafTestApplication.java
4. 접속 확인
>서버가 정상 기동되면 브라우저에서 아래 URL로 접속합니다.
div 방식: http://{임베디드서버도메인}:{port}/aud/sitePortalAUD7EmAudDemo/div
iframe 방식: http://{임베디드서버도메인}:{port}/aud/sitePortalAUD7EmAudDemo/iframe

AUD인증 AP 토큰 발급 API
- AUD 인증 AP 토큰 발급 API는 AUD 플랫폼에 등록한 Application용 클라이언트 아이디와 클라이언트 시크릿를 인증 정보로 설정하여 Application 인증 JWT 토큰을 발급 합니다.
- 클라이언트에서 해당 인증 토큰 발급은 제한됩니다. 보안상 클라이언트 인증 정보가 확인될 소지 방지.
- 타 시스템 포탈에서 최초 1회 인증 토큰 발급 후 만료되었거나 유효하지 않은 토큰일 경우 클라이언트 아이디와 클라이언트 시크릿 정보를 이용하여 인증 토큰을 재발급 합니다.
- 발급한 토큰은 타 시스템 쿠키에 등록하여 사용합니다 . (쿠키 key = bimatrix_ap_accessToken)
| 요청 URL | 메서드 | Header 설정 | 응답 형식 | 설명 | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
{AUD서버 주소}/api/auth/sign/ap/token | POST | 서버 영역에서 API 호출 시에 Request Header로 설정하여 전달
| String | AUD 플랫폼에서 발급된 SSH Private.pem 인증서를 이용하여 Secret Key를 서명한 후에 Header에 Secret Key를 설정하여 전달하여 인증 후 전달된 사용자 또는 클라이언트 아이디로 AUD 플랫폼에서 사용 가능한 인증 토큰 발급. 해당 JWT 인증 토큰을 통해 AUD 플랫폼의 기능 연동을 지원 |
spring:
thymeleaf:
prefix: classpath:/templates/
check-template-location: true
suffix: .html
mode: HTML
cache: false # default true, 개발 시에는 false로 두는 것이 좋음
server:
port: 9995
#ssl:
# key-store: classpath:keystore.p12
# key-store-type: PKCS12
# key-store-password: 123456
# servlet:
# session:
# cookie:
# http-only: true
# secure: false
aud:
server-url: {AUD7 서버 주소로 변경}
x-ap-update-addr: {AUD7 주소 도메인으로 변경}
x-aud-ap-id: {'1-0.공통 설정'에서 입력한 어플리케이션 명으로 변경}
x-aud-ap-secret-key: {'1-0.공통 설정'에서 발급받은 Secret Key로 변경}
ssh-key-path: {'1-0.공통 설정'에서 발급받은 private_key.pem 파일의 절대경로로 변경}
import './assets/imatrix_header.css'
import { createRoot } from 'react-dom/client'
import DivViewer from './DivViewer.jsx'
import IframeViewer from './IframeViewer.jsx'
// ─────────────────────────────────────────────────────────────────
// 공통 초기화: 전역 변수 등록 & 토큰 쿠키 설정
// (mode에 관계없이 React 앱 로드 전에 선행 실행)
// ─────────────────────────────────────────────────────────────────
const audServerUrl = window.IIT_DATA.audServerUrl
const SHARE_DOMAIN = {임베디드 서버와 AUD7 서버가 공유하는 상위 도메인으로 설정}
const AUD7_PATH = audServerUrl + '/AUD/500'
window.AUD7_PATH = AUD7_PATH
window.AUD7_SETTING_PATH = AUD7_PATH
window.SHARE_DOMAIN = SHARE_DOMAIN
window.gvWebRootName = audServerUrl
// 인증 토큰 쿠키 등록
const exdate = new Date()
exdate.setDate(exdate.getDate() + 1)
document.cookie = `bimatrix_ap_accessToken=${window.IIT_DATA.token}; expires=${exdate.toUTCString()}; path=/; domain=${SHARE_DOMAIN}`
// ─────────────────────────────────────────────────────────────────
// mode에 따라 컴포넌트 분기
// div → App.jsx
// iframe → IframeViewer.jsx
// ─────────────────────────────────────────────────────────────────
const mode = window.IIT_DATA.mode || 'div'
const Root = mode === 'iframe' ? IframeViewer : DivViewer
// 주의: React 18 StrictMode는 useEffect를 개발 모드에서 두 번 실행시키므로,
// AUD 외부 스크립트가 중복 로드되는 부작용이 있어 본 임베디드 시나리오에서는 사용하지 않는다.
createRoot(document.getElementById('root')).render(<Root />)
import './assets/imatrix_header.css'
import { createApp } from 'vue'
import DivViewer from './DivViewer.vue'
import IframeViewer from './IframeViewer.vue'
// ─────────────────────────────────────────────────────────────────
// 공통 초기화: 전역 변수 등록 & 토큰 쿠키 설정
// (mode에 관계없이 Vue 앱 로드 전에 선행 실행)
// ─────────────────────────────────────────────────────────────────
const audServerUrl = window.IIT_DATA.audServerUrl
const SHARE_DOMAIN = 'bimatrix.com'
const AUD7_PATH = audServerUrl + '/AUD/500'
window.AUD7_PATH = AUD7_PATH
window.AUD7_SETTING_PATH = AUD7_PATH
window.SHARE_DOMAIN = SHARE_DOMAIN
window.gvWebRootName = audServerUrl
// 인증 토큰 쿠키 등록
const exdate = new Date()
exdate.setDate(exdate.getDate() + 1)
document.cookie = `bimatrix_ap_accessToken=${window.IIT_DATA.token}; expires=${exdate.toUTCString()}; path=/; domain=${SHARE_DOMAIN}`
// ─────────────────────────────────────────────────────────────────
// mode에 따라 컴포넌트 분기
// div → DivViewer.vue
// iframe → IframeViewer.vue
// ─────────────────────────────────────────────────────────────────
const mode = window.IIT_DATA.mode || 'div'
const Root = mode === 'iframe' ? IframeViewer : DivViewer
createApp(Root).mount('#app')
import { useEffect, useRef } from 'react'
import './layout.css'
// IIT_DATA 추출
const audServerUrl = window.IIT_DATA.audServerUrl
// 보고서 목록 (데모용 — 실제 보고서 코드로 교체)
const reportList = [
{ name: {좌측에 표시할 보고서 명}, code: {AUD7에 등록된 보고서 코드}, module: {보고서 모듈 코드}, isShow: {타이틀바 표시 여부} },
{ name: 'i-AUD 보고서 호출2', code: 'REP236AB97070714FA3AC22F9DFD00AFFF3', module: 'SD', isShow: false },
]
// 컴포넌트 외부 모듈 스코프에 두는 가변 상태
// (외부 AUD JS 가 직접 DOM 을 조작하므로 React 리렌더링이 필요 없어 useState 대신 plain object 사용)
const state = {
reportInfo: null,
rName: '',
folderCode: '',
description: '',
moduleCode: '',
TemplateCode: '',
reportId: '',
}
// React 18 StrictMode 미사용이지만, 혹시 모를 중복 마운트에 대비한 가드
let initialized = false
function DivViewer() {
const errorRef = useRef({ show: false, message: '' })
useEffect(() => {
if (initialized) return
initialized = true
try {
loadExternalResources(audServerUrl)
winResizer()
if (window.GFN_AUTHORITY && window.GFN_AUTHORITY.UpdateSession) {
window.GFN_AUTHORITY.UpdateSession()
}
const handleResize = () => {
winResizer()
if (window.AUD && window.AUD.GetMainViewer) {
const mainViewer = window.AUD.GetMainViewer()
if (mainViewer && mainViewer.ViewerSizeChanged) mainViewer.ViewerSizeChanged()
}
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
} catch (e) {
handleError('초기화 중 오류가 발생했습니다.', e)
}
}, [])
function handleError(message, error) {
errorRef.current = { show: true, message: `${message} (${error.message || error})` }
console.error(message, error)
}
return (
<>
<div className="top_panel"></div>
<div className="left_panel">
<ul>
{reportList.map(report => (
<li key={report.code}>
<div
className="rep_div"
onClick={() => openReport(report.code, report.isShow, report.module)}
>
{report.name}
</div>
</li>
))}
</ul>
</div>
<div className="main_group VisibleFrame">
<div className="titlebg" id="titlebg_main" style={{ display: 'none' }}>
<div className="title_area">
<ul>
<li><span id="dvReportName"></span></li>
</ul>
</div>
<div className="bookmark" id="bookmarkIcon" style={{ display: 'none' }}></div>
<div className="location" style={{ display: 'none' }}></div>
<div className="topbtn_group"></div>
</div>
<div id="AUDview" className="istudio-common-viewer"></div>
</div>
<div className="foot_panel"></div>
</>
)
}
// ─────────────────────────────────────────────────────────────────
// 외부 리소스 로드 (AUD 프레임워크 JS/CSS)
// ─────────────────────────────────────────────────────────────────
function loadExternalResources(serverUrl) {
const head = document.head
const scripts = [
'/AUD/500/js/lib/audframework/debug/bimatrix.lib.audframework.js',
'/AUD/500/js/lib/audframework/debug/bimatrix.module.audframework.js',
'/AUD/500/js/lib/rsa/jsbn.js',
'/AUD/500/js/lib/rsa/prng4.js',
'/AUD/500/js/lib/rsa/rng.js',
'/AUD/500/js/lib/rsa/rsa.js',
'/portal/js/jquery-3.6.0.min.js',
'/portal/js/Base64.js',
'/portal/js/jquery.portal.common.js',
'/portal/js/jquery.cookie.js',
'/portal/js/authorityCheck_em.jsp',
'/portal/js/portal.message.jsp',
'/portal/js/portal.option.data.jsp',
'/portal/js/portal.content.top.js',
'/portal/js/matrix.script.comm.js',
'/portal/js/matrix.script.content.em.js',
'/portal/js/portal.content.bookmark.js',
'/portal/js/portal.content.condition.js',
'/extention/AUD/customscript.jsp',
'/extention/portal/customscript.jsp',
]
const styles = [
'/AUD/500/theme/skin-default/ko/css/bimatrix.module.audframework.css',
'/AUD/500/theme/skin-default/ko/css/ion.rangeSlider.css',
'/extention/AUD/bimatrix.custom.audframework.css',
]
styles.forEach(path => {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = serverUrl + path
head.appendChild(link)
})
scripts.forEach(path => {
const script = document.createElement('script')
script.src = serverUrl + path
script.async = false
// GFN_OPTION을 정의하는 스크립트가 로드된 직후 portal 테마 CSS를 동적으로 추가
if (path.endsWith('portal.option.data.jsp')) {
script.onload = () => loadPortalThemeCss(serverUrl)
}
head.appendChild(script)
})
}
function loadPortalThemeCss(serverUrl) {
const opt = window.GFN_OPTION
if (!opt || !opt.PORTAL_THEME_CSS_PATH) return
const root = opt.WEB_ROOTNAME || ''
const baseUrl = (root && serverUrl.endsWith(root))
? serverUrl.slice(0, -root.length)
: serverUrl
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = `${baseUrl}${opt.PORTAL_THEME_CSS_PATH}/theme.css`
document.head.appendChild(link)
}
// ─────────────────────────────────────────────────────────────────
// 보고서 호출
// ─────────────────────────────────────────────────────────────────
function openReport(code, isTitle, module) {
state.reportId = code
state.moduleCode = module
const PARAM_ARR = [{ KEY: 'VS_TEST', VALUE: 'VS_TEST1_VAL' }]
if (window.AUD) window.AUD.SetCustomParams(PARAM_ARR)
if (window.GFN_AUTHORITY && window.GFN_AUTHORITY.searchReportInfo) {
state.reportInfo = window.GFN_AUTHORITY.searchReportInfo(code)
state.rName = state.reportInfo.name
state.folderCode = state.reportInfo.option.FolderCode
state.description = state.reportInfo.desc
}
if (window.GFN_AUTHORITY && window.GFN_AUTHORITY.USER_AUTH_INFO) window.GFN_AUTHORITY.USER_AUTH_INFO()
if (isTitle) {
const btnType = (window.GFN_OPTION && window.GFN_OPTION.OP04_VIEW_BTN === 'TEXT') ? 'text_type' : 'img_type'
if (window.$ && typeof window.$('.topbtn_group').option_top === 'function') {
window.$('.topbtn_group').option_top('view_btn', {
btn_type: btnType,
callbackFn: settingTitle,
embedded: true,
})
}
} else {
document.querySelector('.titlebg').style.display = 'none'
winResizer()
}
if (module === 'SD') {
if (window.AUD && window.AUD.Init) window.AUD.Init(AudOpenReport)
} else if (module === 'SX') {
window.AUD.ShellModuleCode = 'SX'
if (window.AUD && window.AUD.Init) window.AUD.Init(MetaOpenReport)
}
}
function AudOpenReport() {
if (window.AUD && window.AUD.SetFileDialogCallback) window.AUD.SetFileDialogCallback()
if (window.AUD) window.AUD.LoadDocument('AUDview', state.reportId, 2)
}
function MetaOpenReport() {
const metaInfo = {
META_CODE: state.reportId,
NAME: state.rName,
DESC: state.description,
PARENT: state.folderCode,
TYPE: state.moduleCode,
}
window.AUD.MetaViewManager.IsMetaFileView = true
window.AUD.mViewerId = 'AUDview'
window.AUD.LoadMetaDocument('AUDview', state.reportId, metaInfo)
}
// ─────────────────────────────────────────────────────────────────
// 타이틀 / 메뉴 버튼 셋업 (option_top 콜백)
// ─────────────────────────────────────────────────────────────────
function settingTitle() {
document.querySelector('.titlebg').style.display = ''
const info = state.reportInfo
if (!info) return
if (window.setReportInfo) window.setReportInfo(
info.code,
info.name,
info.desc,
info.module,
info.path,
'',
{ FolderCode: info.option.FolderCode }
)
if (window.menuVisible) window.menuVisible(
info.code,
info.module,
{ AuthNo: info.option.AuthNo },
false
)
const buttons = ['btnEdit', 'btnSaveAs', 'btnExport']
buttons.forEach(id => {
const el = document.getElementById(id)
if (el) el.style.display = 'none'
})
settingReportInfo(info.name)
winResizer()
}
function settingReportInfo(name) {
const el = document.querySelector('.titlebg #dvReportName')
if (!el) return
el.innerText = name
el.classList.add('title_bullet')
const opt = window.GFN_OPTION || {}
const root = opt.WEB_ROOTNAME || ''
const baseUrl = (root && audServerUrl.endsWith(root))
? audServerUrl.slice(0, -root.length)
: audServerUrl
const imgUrl = `${baseUrl}${opt.PORTAL_THEME_IMG_PATH}/tree/tree_iaud.png`
el.style.background = `url('${imgUrl}') 0px 0px no-repeat`
}
// ─────────────────────────────────────────────────────────────────
// 리사이즈 처리
// ─────────────────────────────────────────────────────────────────
function winResizer() {
const win_w = window.innerWidth
const win_h = window.innerHeight
const topPanel = document.querySelector('.top_panel')
const leftPanel = document.querySelector('.left_panel')
const footPanel = document.querySelector('.foot_panel')
const titleBg = document.querySelector('.titlebg')
const viewPanel = document.querySelector('.view_panel')
const audView = document.getElementById('AUDview')
const top_panel_height = (topPanel && topPanel.offsetHeight) || 0
const left_panel_width = (leftPanel && leftPanel.offsetWidth) || 0
const foot_panel_height = (footPanel && footPanel.offsetHeight) || 0
if (leftPanel) {
leftPanel.style.height = `${win_h - top_panel_height}px`
leftPanel.style.top = `${top_panel_height}px`
}
const mainGroup = document.querySelector('.main_group')
if (mainGroup) {
mainGroup.style.height = `${win_h - top_panel_height - foot_panel_height}px`
mainGroup.style.top = `${top_panel_height}px`
mainGroup.style.left = `${left_panel_width}px`
mainGroup.style.width = `${win_w - left_panel_width}px`
}
let title_panel_height = (titleBg && titleBg.offsetHeight) || 0
const isHidden = titleBg && getComputedStyle(titleBg).display === 'none'
if (isHidden) {
title_panel_height = 0
} else {
settingReportTitleWidth()
}
if (viewPanel) {
viewPanel.style.height = `${win_h}px`
viewPanel.style.width = `${win_w}px`
}
if (audView) {
audView.style.top = `${title_panel_height}px`
audView.style.height = `${win_h - top_panel_height - foot_panel_height - title_panel_height}px`
}
}
function settingReportTitleWidth() {
const titleEl = document.querySelector('#titlebg_main')
const topArea = titleEl ? titleEl.querySelector('.topbtn_area') : null
const bullet = titleEl ? titleEl.querySelector('.title_bullet') : null
if (!titleEl || !topArea || !bullet) return
const titlebg_w = titleEl.offsetWidth
const title_topbtn_w = topArea.offsetWidth
const title_reportName_w = titlebg_w - title_topbtn_w
bullet.style.width = `${title_reportName_w}px`
}
export default DivViewer
<script setup>
import { onMounted, onBeforeUnmount, ref } from 'vue'
import './layout.css'
// IIT_DATA 추출
const audServerUrl = window.IIT_DATA.audServerUrl
// 보고서 목록 (데모용 — 실제 보고서 코드로 교체)
const reportList = [
{ name: 'i-AUD 보고서 호출', code: 'REP4CE0125E54214D8396F788D30F335583', module: 'SD', isShow: true },
{ name: 'i-AUD 보고서 호출2', code: 'REP236AB97070714FA3AC22F9DFD00AFFF3', module: 'SD', isShow: false },
]
// 외부 AUD JS가 직접 DOM을 조작하므로 Vue 반응형이 필요 없어 일반 객체 사용
const state = {
reportInfo: null,
rName: '',
folderCode: '',
description: '',
moduleCode: '',
TemplateCode: '',
reportId: '',
}
const errorMessage = ref('')
const showError = ref(false)
let handleResize = null
onMounted(() => {
try {
loadExternalResources(audServerUrl)
winResizer()
if (window.GFN_AUTHORITY && window.GFN_AUTHORITY.UpdateSession) {
window.GFN_AUTHORITY.UpdateSession()
}
handleResize = () => {
winResizer()
if (window.AUD && window.AUD.GetMainViewer) {
const mainViewer = window.AUD.GetMainViewer()
if (mainViewer && mainViewer.ViewerSizeChanged) mainViewer.ViewerSizeChanged()
}
}
window.addEventListener('resize', handleResize)
} catch (e) {
handleError('초기화 중 오류가 발생했습니다.', e)
}
})
onBeforeUnmount(() => {
if (handleResize) window.removeEventListener('resize', handleResize)
})
function handleError(message, error) {
showError.value = true
errorMessage.value = `${message} (${error.message || error})`
console.error(message, error)
}
// ─────────────────────────────────────────────────────────────────
// 외부 리소스 로드 (AUD 프레임워크 JS/CSS)
// ─────────────────────────────────────────────────────────────────
function loadExternalResources(serverUrl) {
const head = document.head
const scripts = [
'/AUD/500/js/lib/audframework/debug/bimatrix.lib.audframework.js',
'/AUD/500/js/lib/audframework/debug/bimatrix.module.audframework.js',
'/AUD/500/js/lib/rsa/jsbn.js',
'/AUD/500/js/lib/rsa/prng4.js',
'/AUD/500/js/lib/rsa/rng.js',
'/AUD/500/js/lib/rsa/rsa.js',
'/portal/js/jquery-3.6.0.min.js',
'/portal/js/Base64.js',
'/portal/js/jquery.portal.common.js',
'/portal/js/jquery.cookie.js',
'/portal/js/authorityCheck_em.jsp',
'/portal/js/portal.message.jsp',
'/portal/js/portal.option.data.jsp',
'/portal/js/portal.content.top.js',
'/portal/js/matrix.script.comm.js',
'/portal/js/matrix.script.content.em.js',
'/portal/js/portal.content.bookmark.js',
'/portal/js/portal.content.condition.js',
'/extention/AUD/customscript.jsp',
'/extention/portal/customscript.jsp',
]
const styles = [
'/AUD/500/theme/skin-default/ko/css/bimatrix.module.audframework.css',
'/AUD/500/theme/skin-default/ko/css/ion.rangeSlider.css',
'/extention/AUD/bimatrix.custom.audframework.css',
]
styles.forEach(path => {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = serverUrl + path
head.appendChild(link)
})
scripts.forEach(path => {
const script = document.createElement('script')
script.src = serverUrl + path
script.async = false
// GFN_OPTION을 정의하는 스크립트가 로드된 직후 portal 테마 CSS를 동적으로 추가
if (path.endsWith('portal.option.data.jsp')) {
script.onload = () => loadPortalThemeCss(serverUrl)
}
head.appendChild(script)
})
}
function loadPortalThemeCss(serverUrl) {
const opt = window.GFN_OPTION
if (!opt || !opt.PORTAL_THEME_CSS_PATH) return
const root = opt.WEB_ROOTNAME || ''
const baseUrl = (root && serverUrl.endsWith(root))
? serverUrl.slice(0, -root.length)
: serverUrl
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = `${baseUrl}${opt.PORTAL_THEME_CSS_PATH}/theme.css`
document.head.appendChild(link)
}
// ─────────────────────────────────────────────────────────────────
// 보고서 호출
// ─────────────────────────────────────────────────────────────────
function openReport(code, isTitle, module) {
state.reportId = code
state.moduleCode = module
const PARAM_ARR = [{ KEY: 'VS_TEST', VALUE: 'VS_TEST1_VAL' }]
if (window.AUD) window.AUD.SetCustomParams(PARAM_ARR)
if (window.GFN_AUTHORITY && window.GFN_AUTHORITY.searchReportInfo) {
state.reportInfo = window.GFN_AUTHORITY.searchReportInfo(code)
state.rName = state.reportInfo.name
state.folderCode = state.reportInfo.option.FolderCode
state.description = state.reportInfo.desc
}
if (window.GFN_AUTHORITY && window.GFN_AUTHORITY.USER_AUTH_INFO) window.GFN_AUTHORITY.USER_AUTH_INFO()
if (isTitle) {
const btnType = (window.GFN_OPTION && window.GFN_OPTION.OP04_VIEW_BTN === 'TEXT') ? 'text_type' : 'img_type'
if (window.$ && typeof window.$('.topbtn_group').option_top === 'function') {
window.$('.topbtn_group').option_top('view_btn', {
btn_type: btnType,
callbackFn: settingTitle,
embedded: true,
})
}
} else {
document.querySelector('.titlebg').style.display = 'none'
winResizer()
}
if (module === 'SD') {
if (window.AUD && window.AUD.Init) window.AUD.Init(AudOpenReport)
} else if (module === 'SX') {
window.AUD.ShellModuleCode = 'SX'
if (window.AUD && window.AUD.Init) window.AUD.Init(MetaOpenReport)
}
}
function AudOpenReport() {
if (window.AUD && window.AUD.SetFileDialogCallback) window.AUD.SetFileDialogCallback()
if (window.AUD) window.AUD.LoadDocument('AUDview', state.reportId, 2)
}
function MetaOpenReport() {
const metaInfo = {
META_CODE: state.reportId,
NAME: state.rName,
DESC: state.description,
PARENT: state.folderCode,
TYPE: state.moduleCode,
}
window.AUD.MetaViewManager.IsMetaFileView = true
window.AUD.mViewerId = 'AUDview'
window.AUD.LoadMetaDocument('AUDview', state.reportId, metaInfo)
}
// ─────────────────────────────────────────────────────────────────
// 타이틀 / 메뉴 버튼 셋업 (option_top 콜백)
// ─────────────────────────────────────────────────────────────────
function settingTitle() {
document.querySelector('.titlebg').style.display = ''
const info = state.reportInfo
if (!info) return
if (window.setReportInfo) window.setReportInfo(
info.code,
info.name,
info.desc,
info.module,
info.path,
'',
{ FolderCode: info.option.FolderCode }
)
if (window.menuVisible) window.menuVisible(
info.code,
info.module,
{ AuthNo: info.option.AuthNo },
false
)
const buttons = ['btnEdit', 'btnSaveAs', 'btnExport']
buttons.forEach(id => {
const el = document.getElementById(id)
if (el) el.style.display = 'none'
})
settingReportInfo(info.name)
winResizer()
}
function settingReportInfo(name) {
const el = document.querySelector('.titlebg #dvReportName')
if (!el) return
el.innerText = name
el.classList.add('title_bullet')
const opt = window.GFN_OPTION || {}
const root = opt.WEB_ROOTNAME || ''
const baseUrl = (root && audServerUrl.endsWith(root))
? audServerUrl.slice(0, -root.length)
: audServerUrl
const imgUrl = `${baseUrl}${opt.PORTAL_THEME_IMG_PATH}/tree/tree_iaud.png`
el.style.background = `url('${imgUrl}') 0px 0px no-repeat`
}
// ─────────────────────────────────────────────────────────────────
// 리사이즈 처리
// ─────────────────────────────────────────────────────────────────
function winResizer() {
const win_w = window.innerWidth
const win_h = window.innerHeight
const topPanel = document.querySelector('.top_panel')
const leftPanel = document.querySelector('.left_panel')
const footPanel = document.querySelector('.foot_panel')
const titleBg = document.querySelector('.titlebg')
const viewPanel = document.querySelector('.view_panel')
const audView = document.getElementById('AUDview')
const top_panel_height = (topPanel && topPanel.offsetHeight) || 0
const left_panel_width = (leftPanel && leftPanel.offsetWidth) || 0
const foot_panel_height = (footPanel && footPanel.offsetHeight) || 0
if (leftPanel) {
leftPanel.style.height = `${win_h - top_panel_height}px`
leftPanel.style.top = `${top_panel_height}px`
}
const mainGroup = document.querySelector('.main_group')
if (mainGroup) {
mainGroup.style.height = `${win_h - top_panel_height - foot_panel_height}px`
mainGroup.style.top = `${top_panel_height}px`
mainGroup.style.left = `${left_panel_width}px`
mainGroup.style.width = `${win_w - left_panel_width}px`
}
let title_panel_height = (titleBg && titleBg.offsetHeight) || 0
const isHidden = titleBg && getComputedStyle(titleBg).display === 'none'
if (isHidden) {
title_panel_height = 0
} else {
settingReportTitleWidth()
}
if (viewPanel) {
viewPanel.style.height = `${win_h}px`
viewPanel.style.width = `${win_w}px`
}
if (audView) {
audView.style.top = `${title_panel_height}px`
audView.style.height = `${win_h - top_panel_height - foot_panel_height - title_panel_height}px`
}
}
function settingReportTitleWidth() {
const titleEl = document.querySelector('#titlebg_main')
const topArea = titleEl ? titleEl.querySelector('.topbtn_area') : null
const bullet = titleEl ? titleEl.querySelector('.title_bullet') : null
if (!titleEl || !topArea || !bullet) return
const titlebg_w = titleEl.offsetWidth
const title_topbtn_w = topArea.offsetWidth
const title_reportName_w = titlebg_w - title_topbtn_w
bullet.style.width = `${title_reportName_w}px`
}
</script>
<template>
<div class="top_panel"></div>
<div class="left_panel">
<ul>
<li v-for="report in reportList" :key="report.code">
<div
class="rep_div"
@click="openReport(report.code, report.isShow, report.module)"
>
{{ report.name }}
</div>
</li>
</ul>
</div>
<div class="main_group VisibleFrame">
<div class="titlebg" id="titlebg_main" style="display: none;">
<div class="title_area">
<ul>
<li><span id="dvReportName"></span></li>
</ul>
</div>
<div class="bookmark" id="bookmarkIcon" style="display: none;"></div>
<div class="location" style="display: none;"></div>
<div class="topbtn_group"></div>
</div>
<div id="AUDview" class="istudio-common-viewer"></div>
</div>
<div class="foot_panel"></div>
</template>
import { useEffect, useRef } from 'react'
import './layout.css'
const audServerUrl = window.IIT_DATA.audServerUrl
const webRoot = window.IIT_DATA.webRoot
// 보고서 목록 (데모용 — 실제 보고서 코드로 교체)
const reportList = [
{ name: {좌측에 표시할 보고서 명}, code: {AUD7에 등록된 보고서 코드}, module: {보고서 모듈 코드}, isShow: {타이틀바 표시 여부} },
{ name: 'i-AUD 보고서 호출2', code: 'REP236AB97070714FA3AC22F9DFD00AFFF3', module: 'SD', isShow: false },
]
let initialized = false
function IframeViewer() {
const iframeRef = useRef(null)
const formRef = useRef(null)
const loaded = useRef(false)
useEffect(() => {
if (initialized) return
initialized = true
winResizer()
const handleResize = () => {
winResizer()
sendResize()
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
// ── iframe 내부로 resize 메시지 전송 ──
function sendResize() {
try {
if (iframeRef.current && iframeRef.current.contentWindow) {
iframeRef.current.contentWindow.postMessage(JSON.stringify({ type: 'resize' }), '*')
}
} catch (e) { /* cross-origin 무시 */ }
}
// ── form POST로 iframe 최초 로드 ──
function submitForm(code, isTitle, module) {
const form = formRef.current
if (!form) return
form.innerHTML = ''
form.target = 'REPORT_AUD'
form.action = webRoot + '/iaud_main_view'
const fields = { audServerUrl, id: code, isTitle: String(isTitle), mCode: module }
Object.entries(fields).forEach(([name, value]) => {
const input = document.createElement('input')
input.type = 'hidden'
input.name = name
input.value = value
form.appendChild(input)
})
form.submit()
loaded.current = true
}
// ── 보고서 호출 (최초: form POST / 이후: fnOpen 재사용) ──
function openReport(code, isTitle, module) {
if (!iframeRef.current) return
if (loaded.current) {
try {
const w = iframeRef.current.contentWindow
const viewer = (w.AUD && w.AUD.GetMainViewer) ? w.AUD.GetMainViewer() : null
if (viewer && viewer.Dispose) try { viewer.Dispose() } catch (e) {}
if (w.AUD && w.AUD.SetCustomParams) w.AUD.SetCustomParams([{ KEY: 'VS_TEST', VALUE: 'VS_TEST1_VAL' }])
w.fnOpen(code, isTitle, module)
return
} catch (e) {
console.log('iframe reuse failed:', e.message)
}
}
submitForm(code, isTitle, module)
}
// ── 레이아웃 리사이즈 (titlebg는 iframe 내부에서 처리) ──
function winResizer() {
const win_w = window.innerWidth
const win_h = window.innerHeight
const topPanel = document.querySelector('.top_panel')
const leftPanel = document.querySelector('.left_panel')
const footPanel = document.querySelector('.foot_panel')
const audView = document.getElementById('AUDview')
const top_panel_height = (topPanel && topPanel.offsetHeight) || 0
const left_panel_width = (leftPanel && leftPanel.offsetWidth) || 0
const foot_panel_height = (footPanel && footPanel.offsetHeight) || 0
if (leftPanel) {
leftPanel.style.height = `${win_h - top_panel_height}px`
leftPanel.style.top = `${top_panel_height}px`
}
const mainGroup = document.querySelector('.main_group')
if (mainGroup) {
mainGroup.style.height = `${win_h - top_panel_height - foot_panel_height}px`
mainGroup.style.top = `${top_panel_height}px`
mainGroup.style.left = `${left_panel_width}px`
mainGroup.style.width = `${win_w - left_panel_width}px`
}
if (audView) {
audView.style.height = `${win_h - top_panel_height - foot_panel_height}px`
sendResize()
}
}
return (
<>
<div className="top_panel"></div>
<div className="left_panel">
<ul>
{reportList.map(report => (
<li key={report.code}>
<div
className="rep_div"
onClick={() => openReport(report.code, report.isShow, report.module)}
>
{report.name}
</div>
</li>
))}
</ul>
</div>
<div className="main_group VisibleFrame">
<iframe
ref={iframeRef}
id="AUDview"
name="REPORT_AUD"
className="istudio-common-viewer"
frameBorder="0"
scrolling="no"
style={{
border: 'none',
display: 'block',
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
}}
/>
<form ref={formRef} method="post" style={{ display: 'none' }} />
</div>
<div className="foot_panel"></div>
</>
)
}
export default IframeViewer
<script setup>
import { onMounted, onBeforeUnmount, ref } from 'vue'
import './layout.css'
const audServerUrl = window.IIT_DATA.audServerUrl
const webRoot = window.IIT_DATA.webRoot
// 보고서 목록 (데모용 — 실제 보고서 코드로 교체)
const reportList = [
{ name: 'i-AUD 보고서 호출', code: 'REP4CE0125E54214D8396F788D30F335583', module: 'SD', isShow: true },
{ name: 'i-AUD 보고서 호출2', code: 'REP236AB97070714FA3AC22F9DFD00AFFF3', module: 'SD', isShow: true },
]
const iframeRef = ref(null)
const formRef = ref(null)
const loaded = ref(false)
let handleResize = null
onMounted(() => {
winResizer()
handleResize = () => {
winResizer()
sendResize()
}
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
if (handleResize) window.removeEventListener('resize', handleResize)
})
// ── iframe 내부로 resize 메시지 전송 ──
function sendResize() {
try {
if (iframeRef.value && iframeRef.value.contentWindow) {
iframeRef.value.contentWindow.postMessage(JSON.stringify({ type: 'resize' }), '*')
}
} catch (e) { /* cross-origin 무시 */ }
}
// ── form POST로 iframe 최초 로드 ──
function submitForm(code, isTitle, module) {
const form = formRef.value
if (!form) return
form.innerHTML = ''
form.target = 'REPORT_AUD'
form.action = webRoot + '/iaud_main_view'
const fields = { audServerUrl, id: code, isTitle: String(isTitle), mCode: module }
Object.entries(fields).forEach(([name, value]) => {
const input = document.createElement('input')
input.type = 'hidden'
input.name = name
input.value = value
form.appendChild(input)
})
form.submit()
loaded.value = true
}
// ── 보고서 호출 (최초: form POST / 이후: fnOpen 재사용) ──
function openReport(code, isTitle, module) {
if (!iframeRef.value) return
if (loaded.value) {
try {
const w = iframeRef.value.contentWindow
const viewer = (w.AUD && w.AUD.GetMainViewer) ? w.AUD.GetMainViewer() : null
if (viewer && viewer.Dispose) try { viewer.Dispose() } catch (e) {}
if (w.AUD && w.AUD.SetCustomParams) w.AUD.SetCustomParams([{ KEY: 'VS_TEST', VALUE: 'VS_TEST1_VAL' }])
w.fnOpen(code, isTitle, module)
return
} catch (e) {
console.log('iframe reuse failed:', e.message)
}
}
submitForm(code, isTitle, module)
}
// ── 레이아웃 리사이즈 (titlebg는 iframe 내부에서 처리) ──
function winResizer() {
const win_w = window.innerWidth
const win_h = window.innerHeight
const topPanel = document.querySelector('.top_panel')
const leftPanel = document.querySelector('.left_panel')
const footPanel = document.querySelector('.foot_panel')
const audView = document.getElementById('AUDview')
const top_panel_height = (topPanel && topPanel.offsetHeight) || 0
const left_panel_width = (leftPanel && leftPanel.offsetWidth) || 0
const foot_panel_height = (footPanel && footPanel.offsetHeight) || 0
if (leftPanel) {
leftPanel.style.height = `${win_h - top_panel_height}px`
leftPanel.style.top = `${top_panel_height}px`
}
const mainGroup = document.querySelector('.main_group')
if (mainGroup) {
mainGroup.style.height = `${win_h - top_panel_height - foot_panel_height}px`
mainGroup.style.top = `${top_panel_height}px`
mainGroup.style.left = `${left_panel_width}px`
mainGroup.style.width = `${win_w - left_panel_width}px`
}
if (audView) {
audView.style.height = `${win_h - top_panel_height - foot_panel_height}px`
sendResize()
}
}
</script>
<template>
<div class="top_panel"></div>
<div class="left_panel">
<ul>
<li v-for="report in reportList" :key="report.code">
<div
class="rep_div"
@click="openReport(report.code, report.isShow, report.module)"
>
{{ report.name }}
</div>
</li>
</ul>
</div>
<div class="main_group VisibleFrame">
<iframe
ref="iframeRef"
id="AUDview"
name="REPORT_AUD"
class="istudio-common-viewer"
frameborder="0"
scrolling="no"
style="border: none; display: block; position: absolute; left: 0; top: 0; width: 100%; height: 100%;"
/>
<form ref="formRef" method="post" style="display: none;"></form>
</div>
<div class="foot_panel"></div>
</template>
package com.aud.embedded;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientRequestException;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import org.thymeleaf.util.StringUtils;
import java.security.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
@Service
public class AudAction {
private final WebClient audWebClient;
private final String audApId;
private final String audApSecretFilePath;
private final String apUpdateAddr;
private final String audSecretKey;
public AudAction(@Qualifier("audWebClient") WebClient audWebClient
, @Value("${aud.x-aud-ap-id}") String audApId
, @Value("${aud.ssh-key-path}") String audApSecretFilePath
, @Value("${aud.x-ap-update-addr}") String apUpdateAddr
, @Value("${aud.x-aud-ap-secret-key}") String audSecretKey) {
this.audWebClient = audWebClient;
this.audApId = audApId;
this.audApSecretFilePath = audApSecretFilePath;
this.apUpdateAddr = apUpdateAddr;
this.audSecretKey = audSecretKey;
}
public String getAccessToken() throws Exception {
AtomicReference<String> apToken = new AtomicReference<>("");
// Application 공통 계정으로 토큰 발급하여 공통으로 사용할 경우는 로그인 유저의 코드를 따로 전달하지 않아도 됩니다.
// 해당 부분은 실제 인증을 받고 나서 외부 포탈로 로그인한 사용자를 기준으로 토큰 발급을 원할 경우에 처리합니다.
// 로그인 유저는 백엔드에서 세션에서 확인 후 각 사이트별로 관리하는 방법으로 확인합니다.
String loginUserCode = {AUD7 유저 코드};
try {
// aud7 플랫폼에서 발급받은 secret key를 ssh의 private key로 서명하여 전달한다.
PrivateKey privateKey = loadPrivateKey(audApSecretFilePath);
// aud7 secret key 서명 생성
String signedMessage = signMessage(audSecretKey, privateKey);
ResponseEntity<String> res =
audWebClient.post()
.uri("/api/auth/sign/ap/token")
.headers(headers -> {
headers.set("X-AUD-AP-Id", audApId);
headers.set("X-AUD-AP-Secret-SSH", signedMessage);
headers.set("X-AP-UPDATE-ADDR", apUpdateAddr);
headers.set("X-AUD-USER", loginUserCode);
})
.retrieve()
.toEntity(String.class)
.block();
if (res == null) throw new Exception("");
res.getHeaders().get("bimatrix_ap_accessToken").stream().findFirst().ifPresent(apToken::set);
if (StringUtils.isEmpty(apToken.get()))
throw new Exception("token empty");
} catch (WebClientRequestException e) {
e.printStackTrace();
// throw
} catch (WebClientResponseException e) {
if (HttpStatus.UNAUTHORIZED == e.getStatusCode()) {
e.printStackTrace();
// 인증 실패
// throw
} else {
e.printStackTrace();
// throw
;
}
}catch (Exception e){
e.printStackTrace();
}
return apToken.get();
}
// 개인 키 로딩
private static PrivateKey loadPrivateKey(String path) throws Exception {
// 개인 키 로딩 로직을 구현 (파일 파싱 또는 다른 방법으로)
System.out.println("Private Key path: " + path);
String keyPEM = new String(Files.readAllBytes(Paths.get(path)))
.replaceAll("-----BEGIN PRIVATE KEY-----", "")
.replaceAll("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", ""); // 모든 공백 제거
byte[] keyBytes = Base64.getDecoder().decode(keyPEM);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}
// 메시지 서명
private static String signMessage(String message, PrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(message.getBytes());
byte[] signedBytes = signature.sign();
return Base64.getEncoder().encodeToString(signedBytes);
}
public String getAudMainJsonParam(Map<String, String[]> paramMap) {
final ObjectMapper mapper = new ObjectMapper();
String json = "";
List<Map<String, String>> paramList = new ArrayList<>();
for (String key : paramMap.keySet()) {
if(!key.startsWith("VS") && !key.startsWith("VN") && !key.startsWith("V_")) continue;
paramList.add(
new HashMap<>() {{
put("KEY", key);
Arrays.stream(paramMap.get(key)).findFirst().ifPresent(value -> {
put("VALUE", value);
});
}}
);
}
try {
json = mapper.writeValueAsString(paramList);
} catch (JsonProcessingException e) {
//throw new Exception(e.getMessage());
}
return json;
}
}
- 레이블 없음
