페이지 트리

버전 비교

  • 이 줄이 추가되었습니다.
  • 이 줄이 삭제되었습니다.
  • 서식이 변경되었습니다.

Page No. 

Showpageid

작성자 :손성준     / 검수자 :  


 Launch Release No. 7.3.500.20250722 / Latest Release No. 


개요

- 기존 데이터베이스는 데이터베이스 -> 스키마 -> 테이블 순의 3단계 구조이지만, Trino는 중간에 카탈로그가 추가된 데이터베이스 -> 카탈로그 -> 스키마 -> 테이블의 4단계 구조를 가집니다.
Trino는 단일 쿼리로 모든 카탈로그 하위의 스키마 목록을 한 번에 불러오는 표준 명령을 제공하지 않아, 제품의 스키마 추가 화면에서 카탈로그 명을 자동으로 식별하기 어렵습니다.
따라서 어떤 카탈로그에 어떤 스키마가 속해 있는지에 대한 종속 관계는 시스템을 구축한 관리자(데이터 엔지니어)만이 명확히 알 수 있는 구조입니다.
이처럼 기존 제품의 데이터 구조 및 일반적인 DB 사용법과 확연히 다르기 때문에, 원활한 설정을 위한 별도의 가이드가 작성되었습니다.



Easy Heading Macro
navigationExpandOptionexpand-all-by-default

1.

임베디드 서버 설정

Connection 등록

1-1)

application.yml 설정 (Vue, React 공통)
> 사전준비에서 발급받은 키 정보를 바탕으로, 프로젝트 내 설정 파일들을 사이트 환경에 맞게 수정합니다.
    파일 위치: {소스경로}/src/main/resources/application.yml

Image Removed

   < 그림 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)

Image Removed

   < 그림 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 파일을 선정)

Image Removed

   < 그림 1-3. DivViewer.jsx 설정 정보 캡쳐 >

- name: 좌측 메뉴에 표시할 보고서명

- code: AUD7에 등록된 보고서 코드

- module: 보고서 모듈 코드

- isShow: 타이틀바 표시 여부

1-4) 서버 소스 설정

> 사용자 계정 정보와 관련된 서버 소스를 설정 합니다.(Vue, React 공통)
파일 위치: {소스경로}/src/main/java/com/aud/embedded/AudAction.java

Image Removed

   < 그림 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

Image Removed

정보
titleAUD인증 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로 설정하여 전달

Key설명
X-AUD-AP-Id애플리케이션 클라이언트 아이디값
X-AUD-AP-Secret-SSH

애플리케이션 클라이언트 시크릿값

      • AUD7 플랫폼 발급한 ssh private pem 파일을 이용하여 전달받은 시크릿 Key에 서명을 한 후에 전달하여 토큰 발급 요청
X-AUD-USER
      • Application용 아이디가 아닌 타 시스템에서 로그인 한 사용자로 인증 토큰 발급받아 사용 시에 세션 사용자 아이디 설정
      • 해당 사용자도 AUD 플랫폼 사용자에 등록된 id만 가능
X-AP-UPDATE-ADDR
      • 설정 도메인 (쿠키에 토큰 공유를 위한 도메인 정보)
      • 서브 도메인 , 포트는 달라도 무방함
String

AUD 플랫폼에서 발급된 SSH Private.pem 인증서를 이용하여 Secret Key를 서명한 후에 Header에 Secret Key를 설정하여 전달하여 인증 후 전달된 사용자 또는 클라이언트 아이디로 AUD 플랫폼에서 사용 가능한 인증 토큰 발급.

해당 JWT 인증 토큰을 통해 AUD 플랫폼의 기능 연동을 지원

  
코드 블럭
languageyml
themeMidnight
firstline1
titleapplication.yml (Vue, React 공통)
linenumberstrue
collapsetrue
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 파일의 절대경로로 변경}



코드 블럭
languagejs
themeMidnight
firstline1
titlemain.jsx (React)
linenumberstrue
collapsetrue
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 />)   
코드 블럭
languagejs
themeMidnight
firstline1
titlemain.js (Vue)
linenumberstrue
collapsetrue
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')
코드 블럭
themeMidnight
firstline1
titleDivViewer.jsx (React)
linenumberstrue
collapsetrue
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
코드 블럭
themeMidnight
firstline1
titleDivViewer.vue (Vue)
linenumberstrue
collapsetrue
<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>
코드 블럭
themeMidnight
firstline1
titleIframeViewer.jsx (React)
linenumberstrue
collapsetrue
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
코드 블럭
themeMidnight
firstline1
titleIframeViewer.vue (Vue)
linenumberstrue
collapsetrue
<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>
코드 블럭
languagejava
themeMidnight
firstline1
titleAudAction.java (Vue, React 공통)
linenumberstrue
collapsetrue
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;
    }
}
정보
titleiFrame 샘플 소스

View file
namespring-embedded-react.zip
height250
View file
namespring-embedded-vue.zip
height250

AUD Platform Admin > 자원관리 > [서버관리] 화면에 진입하여 Trino DB 서버 정보를 등록합니다.

1-2) AUD Platform Admin > 자원관리 > [데이터베이스 관리] 화면에 진입하여 Connection을 신규 등록 합니다.

1-2-1) 드라이버 명을 [Java(OCI)]으로 선택하고 필수 입력 값 [DB 별칭] [서버명] [DB 서비스 아이디] [DB 소유자 아이디] 를 입력합니다.
    [DB 서비스 아이디] [DB 소유자 아이디]는 아무 값이나 입력합니다. (드라이버를 oci로 설정 시 url정보로 Conncetion을 생성한다는 의미이기 때문)
1-2-2) [DB JNDI] 값을 다음과 같은 양식으로 기입합니다.
    ex) jdbc:trino://<ip>:<port>?<연결에 필요한 파라미터>
Image Added

   < 그림 1-2. main.jsx 설정 정보 캡쳐 >

1-2-3) [DB 설명] 값에 다음과 같은 값을 입력합니다
   ex) [[DRIVER=io.trino.jdbc.TrinoDriver]]
Image Added
※ Trino JDBC 연결 URL에 대한 자세한 정보는 공식 문서에서 확인하실 수 있습니다.
    https://trino.io/docs/current/client/jdbc.html


2. 쿼리 작성 및 실행

1-1) i-AUD Designer의 Script Editor에서 [Data source] 진입 후 DB Connection을 [Trino]로 선택 후 쿼리 조회 
    > Trino쿼리에서 테이블 조회 시 [카탈로그.스키마.테이블명] 까지 입력하여 조회 해야 함.
Image Added

3. 메타 보고서 생성

3-1) i-META Designer의 [스키마 관리자]에서 Trino 데이터 베이스 선택 후 스키마 추가에서 추가할 스키마 선택

3-2) [파생 테이블 추가]를 사용하여 메타로 생성하고자 하는 테이블을 직접 생성
    > Trino는 카탈로그정보 depth가 하나 더 존재하므로 메타 데이터를 생성하려면 파생테이블로 직접 카탈로그 정보까지 입력하여 생성해야 함

Image Added