페이지 트리

버전 비교

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



Page No. 

Showpageid

작성자 :   / 검수자 :  


 Launch Release No. 7.3.500.20250722 / Latest Release No. 

※ 주의 : 외부 포탈 임베디드(Project)방식의 SSO연동을 적용하기 위해 '1-0.공통 설정' 과정을 선행으로 설정해야 함


− 개요

> 외부 포탈 사이트에서 AUD7 보고서를 임베디드 방식으로 연동하기 위한 샘플 프로젝트
> Spring Boot(백엔드)에서 AUD7 서버와 인증을 처리하고,
React 또는 Vue(프론트엔드)가 보고서 뷰어 UI를 제공
> 단 , 일부 기능이 크로스 도메인 block으로 처리 불가할 수 있음

   (top , parent 접근 불가하여 postMessage 형식 등으로 커스텀 개발이 AUD플랫폼 내 적용되어야 함)


− 제약 사항

> 기본적으로 AP 토큰이 쿠키에 공유되어 사용되기 때문에 외부 PORTAL (타 사이트 포탈) 과 AUD7 플랫폼 PORTAL의 Domain name이 동일해야 된다.
> 외부 POTAL에서는 발급된 AP 인증 토큰이 정상적으로 브라우저 Cookie bimatrix_ap_accessToken으로 설정되어 있는지 확인이 필요하다.



Easy Heading Macro
navigationExpandOptionexpand-all-by-default

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



정보
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


...