페이지 트리
메타 데이터의 끝으로 건너뛰기
메타 데이터의 시작으로 이동

이 페이지의 이전 버전을 보고 있습니다. 현재 버전 보기.

현재와 비교 페이지 이력 보기

« 이전 버전 26 다음 »



Page No.  150569035

작성자 :   / 검수자 :  


 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으로 설정되어 있는지 확인이 필요하다.



1. 임베디드 서버 설정

1-1) application.yml 설정 (Vue, React 공통)

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

   < 그림 1-1. application.yml 설정 정보 캡쳐 >
- port사이트 정책에 맞는 포트 번호

- server-url: AUD7 서버 주소로 변경

- x-ap-update-addr: AUD7 주소 도메인으로 변경

- x-aud-ap-id: '1-0.공통 설정' 에서 입력한 어플리케이션 명으로 변경

- x-aud-ap-secret-key: '1-0.공통 설정'  에서 발급받은 Secret Key로 변경

- ssh-key-path: '1-0.공통 설정' 에서 발급받은 private_key.pem 파일의 절대경로로 변경

1-2) 프론트엔드 도메인 설정

> SHARE_DOMAIN 값을 임베디드 서버와 AUD7 서버가 공유하는 상위 도메인으로 설정합니다.
    파일 위치: {소스경로}/frontend/src/main.[jsx|js] (React는 jsx, Vue는 js)

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

- SHARE_DOMAIN: 임베디드 서버와 AUD7 서버가 공유하는 상위 도메인으로 설정합니다.
ex) AUD7 서버 주소: aud.example.com, 임베디드 서버 주소: embedded.example.com
     → SHARE_DOMAIN = 'example.com'

1-3) 보고서 코드 설정

> 임베디드 화면에서 호출할 보고서를 설정합니다.
    파일 위치: {소스경로}/frontend/src/{DivViewer.[jsx|vue]} or {IframeViewer.[jsx|vue]} (React는 jsx, Vue는 js) (사용 방식(div / iframe)에 따라 수정할 jsx 파일을 선정)

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

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

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

- module: 보고서 모듈 코드

- isShow: 타이틀바 표시 여부


1-4) 서버 소스 설정

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

   < 그림 1-4. AudAction.java 설정 정보 캡쳐 >

- loginUserCode: 토큰을 발급 받아 로그인 처리할 유저 코드명
공용 계정 사용 시: 임베디드 보고서의 권한을 가진 사용자 코드를 loginUserCode에 고정값으로 지정합니다.
개별 사용자 인증 시: 외부 포탈의 로그인 세션에서 사용자 코드를 가져와 loginUserCode에 설정합니다.
    (해당 사용자 코드는 AUD7 사용자 관리에도 동일하게 등록되어 있어야 합니다.)


2. 빌드

1) {소스경로}/frontend 경로에서 npm run build 빌드 명령어를 실행합니다.
2) 빌드가 완료되면 {소스경로}/frontend/dist/assets/ 폴더에 아래 파일이 생성되는데 해당 결과물을 Spring 정적 리소스 경로로 복사합니다.
   -frontend/dist/assets/index.js → src/main/resources/static/assets/index.js
   -frontend/dist/assets/index.css -> src/main/resources/static/assets/index.css


3. 서버 실행 

> IntelliJ 등 IDE에서 SpringThymeleafTestApplication 클래스를 실행합니다.
    파일 위치: {소스경로}/src/main/java/com/aud/embedded/SpringThymeleafTestApplication.java


4. 접속 확인

>서버가 정상 기동되면 브라우저에서 아래 URL로 접속합니다.
   div 방식: http://{임베디드서버도메인}:{port}/aud/sitePortalAUD7EmAudDemo/div
   iframe 방식: http://{임베디드서버도메인}:{port}/aud/sitePortalAUD7EmAudDemo/iframe



AUD인증 AP 토큰 발급 API

- AUD 인증 AP 토큰 발급 API는 AUD 플랫폼에 등록한 Application용 클라이언트 아이디와 클라이언트 시크릿를 인증 정보로 설정하여 Application 인증 JWT 토큰을 발급 합니다.

      • 클라이언트에서 해당 인증 토큰 발급은 제한됩니다. 보안상 클라이언트 인증 정보가 확인될 소지 방지.
      • 타 시스템 포탈에서 최초 1회 인증 토큰 발급 후 만료되었거나 유효하지 않은 토큰일 경우 클라이언트 아이디와 클라이언트 시크릿 정보를 이용하여 인증 토큰을 재발급 합니다.
      • 발급한 토큰은 타 시스템 쿠키에 등록하여 사용합니다 . (쿠키 key = bimatrix_ap_accessToken)
요청 URL메서드Header 설정응답 형식설명

{AUD서버 주소}/api/auth/sign/ap/token

POST

서버 영역에서 API 호출 시에 Request Header로 설정하여 전달

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 플랫폼의 기능 연동을 지원


  

application.yml
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 파일의 절대경로로 변경}



main.jsx
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 />)   
DivViewer.jsx
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
IframeViewer.jsx
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
AudAction.java
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;
    }
}








 



  • 레이블 없음