Merge branch 'jenkins_with_weblogic' of ssh://git@192.168.240.178:18081/eapim/eapim-admin.git into jenkins_with_weblogic

This commit is contained in:
daekuk
2026-01-02 13:44:30 +09:00
23 changed files with 1110 additions and 156 deletions
+1
View File
@@ -140,4 +140,5 @@
<aop:aspectj-autoproxy />
<bean class="com.eactive.ext.kjb.web.ApiTransactionLogController"></bean>
</beans>
+334
View File
@@ -0,0 +1,334 @@
/**
* Editor Content Styles (editor-content.css)
*
* Summernote 에디터로 작성된 HTML 콘텐츠의 공통 스타일
* 관리자포탈/개발자포탈 양쪽에서 동일하게 사용
*
* 사용법:
* - 관리자포탈: Summernote .note-editable에 .editor-content 클래스 추가
* - 개발자포탈: 콘텐츠 wrapper에 .editor-content 클래스 추가
*
* @version 1.0.0
* @date 2025-12-30
*/
/* ==========================================================================
CSS Reset + 기본 설정
========================================================================== */
.editor-content {
all: revert;
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
font-size: 16px !important;
font-weight: 400 !important;
line-height: 1.8 !important;
color: #1A1A2E !important;
word-wrap: break-word;
text-align: left !important;
}
/* ==========================================================================
헤딩 (Headings)
========================================================================== */
.editor-content h1 {
font-size: 20px !important;
font-weight: 700 !important;
margin: 32px 0 16px !important;
line-height: 1.4 !important;
color: #1A1A2E !important;
}
.editor-content h1:first-child {
margin-top: 0 !important;
}
.editor-content h2 {
font-size: 18px !important;
font-weight: 700 !important;
margin: 24px 0 8px !important;
line-height: 1.4 !important;
color: #1A1A2E !important;
}
.editor-content h3 {
font-size: 16px !important;
font-weight: 600 !important;
margin: 16px 0 8px !important;
line-height: 1.4 !important;
color: #1A1A2E !important;
}
.editor-content h4,
.editor-content h5,
.editor-content h6 {
font-size: 16px !important;
font-weight: 600 !important;
margin: 16px 0 8px !important;
line-height: 1.4 !important;
color: #1A1A2E !important;
}
/* ==========================================================================
단락 (Paragraphs)
========================================================================== */
.editor-content p {
margin-bottom: 16px !important;
line-height: 1.8 !important;
font-size: 16px !important;
}
.editor-content p:last-child {
margin-bottom: 0 !important;
}
/* ==========================================================================
리스트 (Lists) - 글로벌 reset 대응
========================================================================== */
.editor-content ul {
list-style-type: disc !important;
padding-left: 32px !important;
margin: 16px 0 !important;
}
.editor-content ol {
list-style-type: decimal !important;
padding-left: 32px !important;
margin: 16px 0 !important;
}
.editor-content li {
margin-bottom: 8px !important;
line-height: 1.8 !important;
font-size: 16px !important;
}
.editor-content li:last-child {
margin-bottom: 0 !important;
}
/* 중첩 리스트 */
.editor-content ul ul {
list-style-type: circle !important;
margin: 8px 0 !important;
}
.editor-content ul ul ul {
list-style-type: square !important;
}
.editor-content ol ol {
list-style-type: lower-alpha !important;
margin: 8px 0 !important;
}
.editor-content ol ol ol {
list-style-type: lower-roman !important;
}
/* ==========================================================================
테이블 (Tables)
========================================================================== */
.editor-content table {
width: 100% !important;
border-collapse: collapse !important;
margin: 24px 0 !important;
border: 1px solid #E2E8F0 !important;
border-radius: 8px !important;
overflow: hidden !important;
font-size: 14px !important;
}
.editor-content th,
.editor-content td {
border: 1px solid #E2E8F0 !important;
padding: 8px 16px !important;
text-align: left !important;
vertical-align: top !important;
}
.editor-content th {
background: #EFF6FF !important;
font-weight: 600 !important;
color: #1A1A2E !important;
}
.editor-content tr:hover {
background: #F8FAFC !important;
}
/* ==========================================================================
링크 (Links)
========================================================================== */
.editor-content a {
color: #0049b4 !important;
text-decoration: underline !important;
transition: color 0.3s ease !important;
}
.editor-content a:hover {
color: #003080 !important;
}
/* ==========================================================================
인용문 (Blockquote)
========================================================================== */
.editor-content blockquote {
border-left: 4px solid #0049b4 !important;
padding: 16px 24px !important;
margin: 24px 0 !important;
background: #F8FAFC !important;
font-style: italic !important;
color: #64748B !important;
border-radius: 0 8px 8px 0 !important;
}
.editor-content blockquote p {
margin-bottom: 0 !important;
}
/* ==========================================================================
이미지 (Images)
========================================================================== */
.editor-content img {
max-width: 100% !important;
height: auto !important;
border-radius: 8px !important;
margin: 16px 0 !important;
display: block !important;
}
/* ==========================================================================
코드 (Code)
========================================================================== */
.editor-content code {
background: #F8FAFC !important;
padding: 2px 6px !important;
border-radius: 4px !important;
font-family: 'Fira Code', 'Courier New', monospace !important;
font-size: 0.9em !important;
color: #0049b4 !important;
}
.editor-content pre {
background: #F8FAFC !important;
border: 1px solid #E2E8F0 !important;
border-radius: 8px !important;
padding: 16px !important;
overflow-x: auto !important;
margin: 16px 0 !important;
}
.editor-content pre code {
background: transparent !important;
padding: 0 !important;
color: #1A1A2E !important;
}
/* ==========================================================================
구분선 (Horizontal Rule)
========================================================================== */
.editor-content hr {
border: none !important;
border-top: 1px solid #E2E8F0 !important;
margin: 32px 0 !important;
}
/* ==========================================================================
텍스트 강조 (Text Emphasis)
========================================================================== */
.editor-content strong,
.editor-content b {
font-weight: 700 !important;
}
.editor-content em,
.editor-content i {
font-style: italic !important;
}
.editor-content u {
text-decoration: underline !important;
}
.editor-content s,
.editor-content strike,
.editor-content del {
text-decoration: line-through !important;
}
.editor-content mark {
background-color: #FEF3C7 !important;
padding: 0 2px !important;
}
.editor-content sub {
vertical-align: sub !important;
font-size: 0.8em !important;
}
.editor-content sup {
vertical-align: super !important;
font-size: 0.8em !important;
}
/* ==========================================================================
반응형 (Responsive)
========================================================================== */
@media (max-width: 768px) {
.editor-content {
font-size: 14px !important;
}
.editor-content h1 {
font-size: 18px !important;
}
.editor-content h2 {
font-size: 16px !important;
}
.editor-content h3,
.editor-content h4,
.editor-content h5,
.editor-content h6 {
font-size: 14px !important;
}
.editor-content p,
.editor-content li {
font-size: 14px !important;
}
.editor-content table {
font-size: 12px !important;
}
.editor-content th,
.editor-content td {
padding: 4px 8px !important;
}
.editor-content ul,
.editor-content ol {
padding-left: 24px !important;
}
.editor-content blockquote {
padding: 8px 16px !important;
}
.editor-content pre {
padding: 8px !important;
}
}
+239
View File
@@ -60,8 +60,247 @@
<script src="<c:url value="/addon/summernote/summernote-cleaner.js"/>"></script>
<script src="<c:url value="/addon/summernote/lang/summernote-ko-KR.js"/>"></script>
<!-- Summernote 스타일 -->
<style>
.note-editable img {
max-width: 100%;
cursor: pointer;
}
.note-editable img.note-image-resizing {
outline: 2px solid #007bff;
}
/* 이미지 리사이즈 핸들 스타일 */
.note-control-sizing {
display: block !important;
}
.note-control-holder {
position: relative;
}
.note-control-holder .note-control-sizing {
position: absolute;
width: 10px;
height: 10px;
border: 1px solid #333;
background-color: #fff;
}
/* 코드뷰 모드 가로 사이즈 고정 */
.note-editor .note-codable {
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
resize: vertical !important;
white-space: pre-wrap !important;
word-wrap: break-word !important;
overflow-x: auto !important;
}
.note-editor.note-frame {
overflow: hidden;
}
</style>
<script language="javascript">
/**
* Summernote 에디터 공통 초기화 함수
* - 이미지 붙여넣기 시 data-uri(Base64) 방식으로 처리
* - 이미지 리사이징 지원
* @param {string} selector - 에디터 selector (예: '#contents')
* @param {object} customOptions - 추가 옵션 (선택사항)
*/
function initSummernote(selector, customOptions) {
customOptions = customOptions || {};
// 기본 설정
var defaultOptions = {
placeholder: customOptions.placeholder || '여기에 내용을 입력하세요.',
height: customOptions.height || 300,
lang: 'ko-KR',
toolbar: customOptions.toolbar || [
['style', ['style']],
['font', ['bold', 'underline', 'clear']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'picture']],
['view', ['fullscreen', 'codeview', 'help']]
],
// 이미지 팝오버 설정 - 리사이징 지원
popover: {
image: [
['resize', ['resizeFull', 'resizeHalf', 'resizeQuarter', 'resizeNone']],
['float', ['floatLeft', 'floatRight', 'floatNone']],
['remove', ['removeMedia']]
]
},
callbacks: {
onInit: function() {
var $editable = $(selector).next('.note-editor').find('.note-editable');
$editable.addClass('editor-content');
$editable.on('keydown', function(e) {
if (e.keyCode === 8) {
e.stopPropagation();
}
});
// 커스텀 onInit 콜백 호출
if (customOptions.callbacks && customOptions.callbacks.onInit) {
customOptions.callbacks.onInit.call(this);
}
},
// 이미지 업로드 처리 - data-uri(Base64) 방식
onImageUpload: function(files) {
for (var i = 0; i < files.length; i++) {
insertImageAsDataUri(files[i], $(selector));
}
},
// 붙여넣기 시 이미지 처리 (MS Office 등에서 복사한 이미지 포함)
onPaste: function(e) {
var clipboardData = e.originalEvent.clipboardData;
if (!clipboardData || !clipboardData.items) return;
var items = clipboardData.items;
var imageFile = null;
// 1. 먼저 순수 이미지 파일이 있는지 확인
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
imageFile = items[i].getAsFile();
break;
}
}
// 2. 이미지 파일이 있으면 data-uri로 변환하여 삽입
if (imageFile) {
e.preventDefault();
e.stopPropagation();
insertImageAsDataUri(imageFile, $(selector));
return;
}
// 3. HTML 붙여넣기인 경우 (MS Office 등) - 이미지 태그 정리
var htmlData = clipboardData.getData('text/html');
if (htmlData && htmlData.indexOf('<img') !== -1) {
e.preventDefault();
e.stopPropagation();
// HTML에서 이미지 src 추출 및 정리
var cleanedHtml = cleanPastedHtml(htmlData);
// 에디터에 포커스 후 HTML 삽입
$(selector).summernote('focus');
setTimeout(function() {
$(selector).summernote('pasteHTML', cleanedHtml);
}, 10);
return;
}
}
}
};
// 커스텀 옵션 병합 (callbacks는 위에서 별도 처리)
var mergedOptions = $.extend(true, {}, defaultOptions, customOptions);
// callbacks는 기본 콜백을 유지하면서 커스텀 콜백 추가
mergedOptions.callbacks = defaultOptions.callbacks;
if (customOptions.callbacks) {
// onInit은 위에서 이미 처리됨
// 다른 커스텀 콜백 추가
for (var key in customOptions.callbacks) {
if (key !== 'onInit' && key !== 'onImageUpload' && key !== 'onPaste') {
mergedOptions.callbacks[key] = customOptions.callbacks[key];
}
}
}
$(selector).summernote(mergedOptions);
}
/**
* 이미지를 Base64 data-uri로 변환하여 에디터에 삽입
* @param {File} file - 이미지 파일
* @param {jQuery} $editor - summernote 에디터 jQuery 객체
*/
function insertImageAsDataUri(file, $editor) {
// 파일 크기 제한 (5MB)
var maxSize = 5 * 1024 * 1024;
if (file.size > maxSize) {
alert('이미지 크기는 5MB를 초과할 수 없습니다.');
return;
}
// 이미지 타입 확인
if (!file.type.match(/image\/(jpeg|jpg|png|gif|bmp|webp)/i)) {
alert('지원하지 않는 이미지 형식입니다. (jpeg, png, gif, bmp, webp만 지원)');
return;
}
var reader = new FileReader();
reader.onload = function(e) {
var dataUri = e.target.result;
$editor.summernote('insertImage', dataUri, function($image) {
// 이미지 스타일 설정
$image.css({
'max-width': '100%',
'height': 'auto'
});
$image.attr('data-filename', file.name);
});
};
reader.onerror = function() {
alert('이미지 읽기에 실패했습니다.');
};
reader.readAsDataURL(file);
}
/**
* 붙여넣기된 HTML 정리 (MS Office 등에서 복사한 내용)
* - 불필요한 Office 속성 제거
* - 이미지 태그 정리 (alt, v:shapes 등 제거)
* @param {string} html - 원본 HTML
* @returns {string} 정리된 HTML
*/
function cleanPastedHtml(html) {
// 임시 div에서 HTML 파싱
var $temp = $('<div>').html(html);
// 이미지 태그 정리
$temp.find('img').each(function() {
var $img = $(this);
var src = $img.attr('src');
// src가 없거나 file:// 프로토콜이면 제거
if (!src || src.indexOf('file://') === 0) {
$img.remove();
return;
}
// 불필요한 속성 제거 (Office 관련)
var attributesToRemove = ['alt', 'v:shapes', 'o:title', 'style', 'class', 'id', 'name'];
attributesToRemove.forEach(function(attr) {
$img.removeAttr(attr);
});
// 깨끗한 스타일만 적용
$img.css({
'max-width': '100%',
'height': 'auto'
});
});
// VML 태그 제거 (v:, o:, w: 등)
var cleanedHtml = $temp.html();
cleanedHtml = cleanedHtml.replace(/<v:[^>]*>[\s\S]*?<\/v:[^>]*>/gi, '');
cleanedHtml = cleanedHtml.replace(/<o:[^>]*>[\s\S]*?<\/o:[^>]*>/gi, '');
cleanedHtml = cleanedHtml.replace(/<w:[^>]*>[\s\S]*?<\/w:[^>]*>/gi, '');
cleanedHtml = cleanedHtml.replace(/<!\[if[^>]*>[\s\S]*?<!\[endif\]>/gi, '');
// Office 네임스페이스 속성 제거
cleanedHtml = cleanedHtml.replace(/\s*v:[a-z]+="[^"]*"/gi, '');
cleanedHtml = cleanedHtml.replace(/\s*o:[a-z]+="[^"]*"/gi, '');
return cleanedHtml;
}
function buttonControl() {
if (arguments.length == 0) {
$("img[level], button[level]").hide();
@@ -13,6 +13,7 @@
<title>FAQ 상세</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<link rel="stylesheet" type="text/css" href="<c:url value="/css/editor-content.css"/>"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript">
var url = '<c:url value="/onl/apim/portalfaq/portalFaqMan.json" />';
@@ -66,27 +67,10 @@
buttonControl(false);
}
$('#faqAnswer').summernote({
// Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원)
initSummernote('#faqAnswer', {
placeholder: '여기에 답변을 입력하세요.',
height: 300,
toolbar: [
['style', ['style']],
['font', ['bold', 'underline', 'clear']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'picture']],
['view', ['fullscreen', 'codeview', 'help']]
],
callbacks: {
onInit: function() {
$('.note-editable').on('keydown', function(e) {
if (e.keyCode === 8) {
e.stopPropagation();
}
});
}
}
height: 300
});
if (key) {
@@ -109,26 +109,10 @@
var returnUrl = getReturnUrlForReturn();
var key = "${param.id}";
$('#responseDetail').summernote({
height: 200,
toolbar: [
['style', ['style']],
['font', ['bold', 'underline', 'clear']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link']],
['view', ['codeview', 'help']]
],
callbacks: {
onInit: function () {
$('.note-editable').on('keydown', function (e) {
if (e.keyCode === 8) {
e.stopPropagation();
}
});
}
}
// Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원)
initSummernote('#responseDetail', {
placeholder: '여기에 답변을 입력하세요.',
height: 200
});
if (key) {
@@ -13,6 +13,7 @@
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<link rel="stylesheet" type="text/css" href="<c:url value="/css/editor-content.css"/>"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript" >
@@ -136,27 +137,10 @@
init();
$('#contents').summernote({
// Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원)
initSummernote('#contents', {
placeholder: '여기에 내용을 입력하세요.',
height: 300,
toolbar: [
['style', ['style']],
['font', ['bold', 'underline', 'clear']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link']],
['view', ['codeview', 'help']]
],
callbacks: {
onInit: function() {
$('.note-editable').on('keydown', function(e) {
if (e.keyCode === 8) {
e.stopPropagation();
}
});
}
}
height: 300
});
if (key) {
@@ -18,6 +18,7 @@
<title>이용약관 상세</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<link rel="stylesheet" type="text/css" href="<c:url value="/css/editor-content.css"/>"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<jsp:include page="/jsp/common/include/portal.jsp"/>
<script language="javascript">
@@ -126,27 +127,10 @@
}
});
$('#contents').summernote({
placeholder: '여기에 답변을 입력하세요.',
height: 300,
toolbar: [
['style', ['style']],
['font', ['bold', 'underline', 'clear']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link']],
['view', ['fullscreen', 'codeview', 'help']]
],
callbacks: {
onInit: function() {
$('.note-editable').on('keydown', function(e) {
if (e.keyCode === 8) {
e.stopPropagation();
}
});
}
}
// Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원)
initSummernote('#contents', {
placeholder: '여기에 약관 내용을 입력하세요.',
height: 300
});
if (agreementsType && revision) {
@@ -54,7 +54,8 @@ function detail(key){
}
$(document).ready(function() {
$('#emailTemplate').summernote({
// Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원)
initSummernote('#emailTemplate', {
placeholder: '여기에 내용을 입력하세요.',
height: 300,
toolbar: [
@@ -65,16 +66,7 @@ $(document).ready(function() {
['table', ['table']],
['insert', ['link', 'picture', 'video']],
['view', ['codeview', 'help']]
],
callbacks: {
onInit: function() {
$('.note-editable').on('keydown', function(e) {
if (e.keyCode === 8) {
e.stopPropagation();
}
});
}
}
]
});
var key ="${param.id}";
@@ -69,6 +69,7 @@
}
function initializeSummernote() {
// Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원)
var commonOptions = {
height: 300,
toolbar: [
@@ -79,21 +80,12 @@
['table', ['table']],
['insert', ['link', 'picture', 'video']],
['view', ['codeview', 'help']]
],
callbacks: {
onInit: function () {
$('.note-editable').on('keydown', function (e) {
if (e.keyCode === 8) {
e.stopPropagation();
}
});
}
}
]
};
$('#description').summernote(commonOptions);
$('#apiRequestSpec').summernote(commonOptions);
$('#apiResponseSpec').summernote(commonOptions);
initSummernote('#description', commonOptions);
initSummernote('#apiRequestSpec', commonOptions);
initSummernote('#apiResponseSpec', commonOptions);
}
function detail(key) {
+64
View File
@@ -0,0 +1,64 @@
- url
- 요청 clientId
- 거래정보
- 기존 샘플
```
{
"clientId": "8cd49ec6bdf82502e053030011ac7064",
"header": {
"x-request-id": "22617829,22617831,22617833,22617835",
"traceId": "8d41a04141794275abc3691b525cf2a9",
"content-length": "75194",
"x-forwarded-for": "117.52.3.143",
"x-user-no": "29024601",
"accept": "application/json, application/cbor, application/*+json",
"x-partner-id": "KWANGJU_BANK",
"via": "1.1 localhost:20443",
"authorization": "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkODBiMDJmZDVmYmM0MjNhOWYyZTk5YzY1MzgwODY5YyIsInNpbXBsZXEiOiJkYjJmODVmNTQ4ZmY3Njk3MmMwZTA2YjBiMWUwNDBmYWIyNjczNTI2ODI0ZjE1M2RiODAzNDQ4MzRhMDIzNzk3IiwidHJhY2VfaWQiOiJiMGU3MjNkOTY2NTM0N2QxYmFiZmYzZTIwYzZiMjIxYSIsImdyYW50X3R5cGUiOiJjbGllbnRfY3JlZGVudGlhbHMiLCJyZXNvdXJjZSI6IjhjZDViYTczMzgzOTJkNTdlMDUzMDMwMDExYWM4YmUxIiwic2NvcGUiOiJvb2IiLCJzaW1wbGUiOiIxZjNmODYwY2U4ODkxNTVkNDAxYWZjNmE5YTIxZjQ3OTYwNTczNzVhZjI3NzdkY2Y2YTMyZjQ3ZTAxOTc0ZGFiYWY5MTM0ODEzOGFkMTBjMDg3N2FkYTRmZDhhZmVhMmZkMDIzMjVkYTIzYTBkZThlNjMyODRjYzBhMjNjY2VlZjU0NDAyZjdhYzU2NmE5NmM0MmFjZDg4YjI0ZmE2NWQ2NWRiMzcwZDEyMTg2ZjY5ZDkzNmNhYjM0MTcyZDk1NzdkMzNlNDk2MTYxMzczMTY0YjJmMTdjMTk0YWRkZjYyMWIwZmJjNDViNWMzNjUyNzU5MTY2NWQ2NDg1NGE0OWUyYWNmNzM4NDdmZDVkYWZkNDY1Zjg3ZDU5ZTU1N2ZiNWNlN2Y3Njk1NGU2YTg4YjE1ZWFhN2JlNDhmODk5NmVmN2QxYmRjMDg1MjMwOGM5NDc1MzdmMDc1Y2JhZTBmYTM5OTBhZmYzMTY0ZGZlOGVmYTg3YmFlZDgyYjQyYjcyMDA3ZTg4YTM3NmU5MjUyMzZiMGIwZjE5MzM2MDNlN2ExMzNkMzI4YzBkM2JhNWY0OTExNjMyYWFlZmRkZjI5OTk4N2FmMjY3MjIyZTM2ZWI5ZmNlMzg0OWIyODNmN2Q3Yzc4NTQ0MjRlZWE2YTBmYzQ0ZWFmOTNiNzY1ZGQ2YWE5ZWQzNGFlOWEyNTU5OWFmZDM1NTkwOWQ3YjViNDUxYjM4NTc4OTdiMTM2ZjQyNmQ4ODkwZTljZWFiZTQ3NWRjYTEiLCJleHAiOjE3NjU5NjEwMDksInRva2VuX3R5cGUiOiJCZWFyZXIiLCJleHBpcmVzX2luIjozNjAwfQ.G-KC8DPeBBM40NoD7yQs6Y552PAExlz-V_DhehmqMuc",
"x-loan-token": "HOkND9Wb2PGjcV/h1pdpeMyVia8",
"x-forwarded-server": "localhost",
"x-forwarded-host": "apit.kjbank.com:20443",
"key-event-id": "BSST5OEP7G",
"host": "192.168.246.16:28184",
"content-type": "application/json;charset=UTF-8",
"connection": "Keep-Alive",
"accept-encoding": "gzip,deflate",
"user-agent": "Apache-HttpClient/4.5.13 (Java/17.0.12)"
},
"queryString": "",
"body": {
"data": {
"agreeIdentifyInfo": true,
"joinDate": "20191217",
"corporateNumber": "1248100998",
"agreePersonalCreditInfo": true,
"ci": "CI85100722580288510072258028851007225802885100722580288510072258028851007225802885100722",
"name": "박주희",
"salary": 33000000,
"jobType": "01",
"rrn": "8510072037827"
},
"requestProducts": [
{
"loanReqNo": "T000022617829",
"loanProductId": "01535"
},
{
"loanReqNo": "T000022617831",
"loanProductId": "0531"
},
{
"loanReqNo": "T000022617833",
"loanProductId": "0607"
},
{
"loanReqNo": "T000022617835",
"loanProductId": "0608"
}
]
},
"exdata": "",
"txDate": "20251217"
}
```
@@ -15,12 +15,9 @@ class RequestWrapper extends HttpServletRequestWrapper {
// GS인증 크로스사이트스크립트 보안성 문제로 XSS 공격을 방지하기 위한 악성 스크립트 패턴 정의.
// 2025.05.27 패턴 추가
// 2025.12.30 src 패턴 수정 - 화이트리스트 방식 (http, https, data:image/ 만 허용)
private static final Pattern[] PATTERNS = new Pattern[] {
Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE),
Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("</script>", Pattern.CASE_INSENSITIVE),
Pattern.compile("<script(.*?)>",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
@@ -46,6 +43,16 @@ class RequestWrapper extends HttpServletRequestWrapper {
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL)
};
// src 속성 화이트리스트 패턴 - data:image/만 허용
private static final Pattern SRC_ATTR_PATTERN = Pattern.compile(
"(src\\s*=\\s*)([\"'])([^\"']*?)\\2",
Pattern.CASE_INSENSITIVE);
// 허용되는 src 값 패턴 (화이트리스트) - data:image/만 허용
private static final Pattern ALLOWED_SRC_PATTERN = Pattern.compile(
"^data:image/.*",
Pattern.CASE_INSENSITIVE);
public RequestWrapper(HttpServletRequest servletRequest) {
super(servletRequest);
@@ -123,6 +130,10 @@ class RequestWrapper extends HttpServletRequestWrapper {
// script 문자열을 완전히 제거
value = value.replaceAll("(?i)\\bscript\\b", "");
// src 속성 화이트리스트 필터링 - 허용된 프로토콜만 통과
value = filterSrcAttribute(value);
StringBuilder sb = new StringBuilder();
value = convertChars(value, sb);
@@ -133,6 +144,37 @@ class RequestWrapper extends HttpServletRequestWrapper {
return value;
}
/**
* src 속성을 화이트리스트 방식으로 필터링
* 허용: data:image/ (Base64 이미지)
* 비허용: 그 외 모든 프로토콜
*/
private String filterSrcAttribute(String value) {
if (value == null || !value.toLowerCase().contains("src")) {
return value;
}
Matcher matcher = SRC_ATTR_PATTERN.matcher(value);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String srcPrefix = matcher.group(1); // src=
String quote = matcher.group(2); // " 또는 '
String srcValue = matcher.group(3); // src 값
// 빈 값이거나 화이트리스트에 매칭되면 유지
if (srcValue.isEmpty() || ALLOWED_SRC_PATTERN.matcher(srcValue).matches()) {
matcher.appendReplacement(result, Matcher.quoteReplacement(srcPrefix + quote + srcValue + quote));
} else {
// 허용되지 않는 프로토콜은 src="" 로 대체
matcher.appendReplacement(result, Matcher.quoteReplacement(srcPrefix + quote + quote));
}
}
matcher.appendTail(result);
return result.toString();
}
// '(', ')', '"' 문자는 getMethod()가 "get"일 때만 변환됨.
// 이는 gridData에서 사용되는 큰따옴표(")와 변환 함수에서 사용하는 괄호('(', ')')의 처리를 위함.
private String convertChars(String value, StringBuilder sb) {
@@ -0,0 +1,17 @@
package com.eactive.eai.rms.data.entity.onl.logger;
import java.util.List;
import lombok.Data;
@Data
public class ApiLogDTO {
private String apiServiceNumber;
private String txDate;
private List<ApiMessageLogDTO> logs;
private Long totalProcessingTime;
}
@@ -0,0 +1,36 @@
package com.eactive.eai.rms.data.entity.onl.logger;
import java.util.List;
import java.util.Map;
import lombok.Data;
@Data
public class ApiMessageLogDTO {
private String apiServiceNumber;
private int processNumber;
private String txDate;
private String url;
private String queryString;
private String clientId;
private String method;
private Integer HttpStatus;
private String header;
private String body;
private String exdata;
private Long processingTime;
private String keymgtmsgctnt;
}
@@ -6,6 +6,7 @@ import org.springframework.data.domain.Pageable;
import com.eactive.eai.data.entity.onl.logger.EAILog;
import com.eactive.eai.data.entity.onl.logger.EAILogId;
import java.util.List;
import java.util.Map;
public interface EAILogRepository {
@@ -16,4 +17,6 @@ public interface EAILogRepository {
Map<String, Long> countDetailedTransactions(Class<? extends EAILog> clazz, EAILogSearch eaiLogSearch);
List<EAILog> getApiLog(Class<? extends EAILog> class1, String apiServiceNumber);
}
@@ -1,5 +1,19 @@
package com.eactive.eai.rms.data.entity.onl.logger;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import com.eactive.eai.data.entity.onl.logger.EAILog;
import com.eactive.eai.data.entity.onl.logger.EAILogId;
import com.eactive.eai.data.entity.onl.logger.QEAILog;
@@ -11,18 +25,6 @@ import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.EntityPathBase;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Repository
public class EAILogRepositoryImpl implements EAILogRepository {
@@ -217,4 +219,14 @@ public class EAILogRepositoryImpl implements EAILogRepository {
.fetchOne();
}
@Override
public List<EAILog> getApiLog(Class<? extends EAILog> clazz, String apiServiceNumber) {
QEAILog qeaiLog = QEAILog.eAILog;
EntityPathBase<?> entityPathBase = new EntityPathBase<>(clazz, qeaiLog.getMetadata().getName());
return new JPAQueryFactory(em).select(qeaiLog).from(entityPathBase)
.where(qeaiLog.id.eaisvcserno.trim().eq(apiServiceNumber)).orderBy(qeaiLog.id.logprcssserno.asc())
.fetch();
}
}
@@ -19,4 +19,6 @@ public interface EAILogService {
List<PortalStatisticsVO> selectLogList(HashMap<String, Object> paramMap);
Map<String, Long> getTransactionCounts(EAILogSearch eaiLogSearch);
ApiLogDTO getApiLog(String txDate, String apiServiceNumber);
}
@@ -1,55 +1,132 @@
package com.eactive.eai.rms.data.entity.onl.logger;
import com.eactive.eai.data.RollingTable;
import com.eactive.eai.data.entity.onl.logger.EAILog;
import com.eactive.eai.data.entity.onl.logger.EAILogId;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.data.entity.onl.apim.portalstatistics.PortalStatisticsVO;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.eactive.eai.data.RollingTable;
import com.eactive.eai.data.entity.onl.logger.EAILog;
import com.eactive.eai.data.entity.onl.logger.EAILogId;
import com.eactive.eai.data.entity.onl.logger.HttpAdapterExtraLog;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.data.entity.onl.apim.portalstatistics.PortalStatisticsVO;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class EAILogServiceImpl implements EAILogService {
private final EAILogRepository eaiLogRepository;
private final ApplicationContext applicationContext;
private final EAILogDAOForEMS eaiLogDAOForEMS;
private final EAILogRepository eaiLogRepository;
private final ApplicationContext applicationContext;
private final EAILogDAOForEMS eaiLogDAOForEMS;
private final HttpAdapterExtraLogService httpAdapterExtraLogService;
@Override
public Page<EAILogDto> findAll(Pageable pageable, EAILogSearch eaiLogSearch) {
long time = CommonUtil.getCalendar(eaiLogSearch.getSearchStartTime()).getTimeInMillis();
EAILog eaiLog = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time);
Page<EAILogDto> eaiLogDtos = eaiLogRepository.findAll(eaiLog.getClass(), pageable, eaiLogSearch);
return eaiLogDtos;
}
@Override
public Page<EAILogDto> findAll(Pageable pageable, EAILogSearch eaiLogSearch) {
long time = CommonUtil.getCalendar(eaiLogSearch.getSearchStartTime()).getTimeInMillis();
EAILog eaiLog = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time);
Page<EAILogDto> eaiLogDtos = eaiLogRepository.findAll(eaiLog.getClass(), pageable, eaiLogSearch);
return eaiLogDtos;
}
public Map<String, Long> getTransactionCounts(EAILogSearch eaiLogSearch){
long time = CommonUtil.getCalendar(eaiLogSearch.getSearchStartTime()).getTimeInMillis();
EAILog eaiLog = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time);
Map<String, Long> detailedCounts = eaiLogRepository.countDetailedTransactions(eaiLog.getClass(), eaiLogSearch);
return detailedCounts;
}
public Map<String, Long> getTransactionCounts(EAILogSearch eaiLogSearch) {
long time = CommonUtil.getCalendar(eaiLogSearch.getSearchStartTime()).getTimeInMillis();
EAILog eaiLog = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time);
Map<String, Long> detailedCounts = eaiLogRepository.countDetailedTransactions(eaiLog.getClass(), eaiLogSearch);
return detailedCounts;
}
@Override
public EAILogDto getById(EAILogId eaiLogId) {
long time = CommonUtil.getCalendar(eaiLogId.getMsgdpstyms()).getTimeInMillis();
EAILog eaiLog = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time);
return eaiLogRepository.getById(eaiLog.getClass(), eaiLogId);
}
@Override
public EAILogDto getById(EAILogId eaiLogId) {
long time = CommonUtil.getCalendar(eaiLogId.getMsgdpstyms()).getTimeInMillis();
EAILog eaiLog = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time);
return eaiLogRepository.getById(eaiLog.getClass(), eaiLogId);
}
public List<PortalStatisticsVO> selectLogList(HashMap<String, Object> paramMap) {
return eaiLogDAOForEMS.selectLogList(paramMap);
}
public List<PortalStatisticsVO> selectLogList(HashMap<String, Object> paramMap) {
return eaiLogDAOForEMS.selectLogList(paramMap);
}
@Override
public ApiLogDTO getApiLog(String txDate, String apiServiceNumber) {
long time = CommonUtil.getCalendar(txDate).getTimeInMillis();
EAILog eaiLogTargetObject = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time);
List<EAILog> eaiLogs = eaiLogRepository.getApiLog(eaiLogTargetObject.getClass(), apiServiceNumber);
eaiLogs.sort((l1, l2) -> l1.getId().getEaisvcserno().compareTo(l2.getId().getEaisvcserno()));
ApiLogDTO apiLogDTO = new ApiLogDTO();
apiLogDTO.setApiServiceNumber(apiServiceNumber);
eaiLogs.stream().findFirst().ifPresent(log -> apiLogDTO.setTxDate(log.getId().getMsgdpstyms()));
List<ApiMessageLogDTO> apiMessageLogDTOs = new LinkedList<>();
for (EAILog eaiLog : eaiLogs) {
ApiMessageLogDTO apiMessageLogDTO = new ApiMessageLogDTO();
apiMessageLogDTO.setApiServiceNumber(apiServiceNumber);
apiMessageLogDTO.setTxDate(eaiLog.getMsgprcssyms());
apiMessageLogDTO.setProcessNumber(Integer.parseInt(eaiLog.getId().getLogprcssserno()));
apiMessageLogDTO.setQueryString("");
apiMessageLogDTO.setClientId(eaiLog.getClientid());
apiMessageLogDTO.setKeymgtmsgctnt(eaiLog.getKeymgtmsgctnt());
String body = StringUtils.substringAfter(eaiLog.getBzwkdatactnt(), '|');
body = StringUtils.substringBeforeLast(body, "|");
apiMessageLogDTO.setBody(body);
apiMessageLogDTOs.add(apiMessageLogDTO);
}
Map<Integer, HttpAdapterExtraLog> extraLogs = httpAdapterExtraLogService.findByGuid(apiServiceNumber);
for (ApiMessageLogDTO apiMessageLogDTO : apiMessageLogDTOs) {
HttpAdapterExtraLog httpAdapterExtraLog = extraLogs.get(apiMessageLogDTO.getProcessNumber());
if (httpAdapterExtraLog != null) {
apiMessageLogDTO.setHeader(httpAdapterExtraLog.getHeaderContent());
apiMessageLogDTO.setMethod(httpAdapterExtraLog.getHttpMethod());
apiMessageLogDTO.setHttpStatus(httpAdapterExtraLog.getHttpStatus());
apiMessageLogDTO.setUrl(httpAdapterExtraLog.getUrl());
}
}
// 처리 시간 계산
for (int i = 0; i < apiMessageLogDTOs.size(); i++) {
ApiMessageLogDTO dto = apiMessageLogDTOs.get(i);
EAILog eaiLog = eaiLogs.get(i);
// msgprcssyms - msgdpstyms
long receivedTime = CommonUtil.getCalendar(eaiLog.getId().getMsgdpstyms()).getTimeInMillis();
long processedTime = CommonUtil.getCalendar(eaiLog.getMsgprcssyms()).getTimeInMillis();
long processingTime = processedTime - receivedTime;
dto.setProcessingTime(processingTime);
}
// 전체 처리 시간 계산 (첫 수신 시각 ~ 마지막 처리 시각)
if (!eaiLogs.isEmpty()) {
long firstReceivedTime = CommonUtil.getCalendar(eaiLogs.get(0).getId().getMsgdpstyms()).getTimeInMillis();
long lastProcessedTime = CommonUtil.getCalendar(eaiLogs.get(eaiLogs.size() - 1).getMsgprcssyms()).getTimeInMillis();
long totalProcessingTime = lastProcessedTime - firstReceivedTime;
apiLogDTO.setTotalProcessingTime(totalProcessingTime);
}
apiLogDTO.setLogs(apiMessageLogDTOs);
return apiLogDTO;
}
}
@@ -1,16 +1,28 @@
package com.eactive.eai.rms.data.entity.onl.logger;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import javax.transaction.Transactional;
import org.springframework.stereotype.Service;
import com.eactive.eai.data.entity.onl.logger.HttpAdapterExtraLog;
import com.eactive.eai.data.entity.onl.logger.HttpAdapterExtraLogId;
import com.eactive.eai.data.entity.onl.logger.QHttpAdapterExtraLog;
import com.eactive.eai.data.jpa.AbstractDataService;
import com.querydsl.core.types.dsl.BooleanExpression;
@Service
@Transactional
public class HttpAdapterExtraLogService
extends AbstractDataService<HttpAdapterExtraLog, HttpAdapterExtraLogId, HttpAdapterExtraLogRepository> {
public Map<Integer, HttpAdapterExtraLog> findByGuid(String guid) {
BooleanExpression predicate = QHttpAdapterExtraLog.httpAdapterExtraLog.id.guid.eq(guid);
return StreamSupport.stream(repository.findAll(predicate).spliterator(), false)
.collect(Collectors.toMap(log -> log.getId().getServiceProcessNumber(), log -> log));
}
}
@@ -553,7 +553,7 @@ public class ApiInterfaceService extends OnlBaseService {
restOption.setMethod(outboundHttpMethod);
restOption.setExtraPath(outboundRestPath);
String pathType = outboundRestPath.matches(".*\\{[a-zA-Z]+\\}.*") ? "variableUrlRequest" : "simpleRequest";
String pathType = outboundRestPath.matches(".*\\{[_a-zA-Z]+\\}.*") ? "variableUrlRequest" : "simpleRequest";
restOption.setType(pathType);
if (StringUtils.isNotBlank(contentType)) {
@@ -0,0 +1,17 @@
package com.eactive.ext.kjb.web;
import java.util.List;
import lombok.Data;
@Data
public class ApiLogUI {
private String apiServiceNumber;
private String txDate;
private List<ApiMessageLogUI> logs;
private Long totalProcessingTime;
}
@@ -0,0 +1,35 @@
package com.eactive.ext.kjb.web;
import java.util.Map;
import lombok.Data;
@Data
public class ApiMessageLogUI {
private String apiServiceNumber;
private int processNumber;
private String txDate;
private String url;
private String queryString;
private String clientId;
private String method;
private Integer HttpStatus;
private Map<String, String> header;
private Object body;
private String exdata;
private Long processingTime;
private String guid;
}
@@ -0,0 +1,142 @@
package com.eactive.ext.kjb.web;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.interceptor.InterceptorSkipController;
import com.eactive.eai.rms.data.entity.onl.logger.ApiLogDTO;
import com.eactive.eai.rms.data.entity.onl.logger.ApiMessageLogDTO;
import com.eactive.eai.rms.data.entity.onl.logger.EAILogService;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Controller
@RequiredArgsConstructor
public class ApiTransactionLogController implements InterceptorSkipController {
private final EAILogService eaiLogService;
private final ObjectMapper objectMapper = new ObjectMapper();
@GetMapping("/api/transaction/log.do")
@PostMapping("/api/transaction/log.do")
public ResponseEntity<ApiLogUI> getApiLog(String txDate, String apiServiceNumber) {
DataSourceContextHolder.setDataSourceType(DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
// txDate가 없으면 오늘 날짜 설정
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
LocalDate searchDate;
if (StringUtils.hasText(txDate)) {
searchDate = LocalDate.parse(txDate, formatter);
} else {
searchDate = LocalDate.now();
}
// 최대 10일 전까지 조회 시도
ApiLogDTO dto = null;
for (int i = 0; i < 10; i++) {
String searchDateStr = searchDate.format(formatter);
dto = eaiLogService.getApiLog(searchDateStr, apiServiceNumber);
// 결과가 있으면 종료
if (dto != null && dto.getLogs() != null && !dto.getLogs().isEmpty()) {
break;
}
// 하루 전으로 이동
searchDate = searchDate.minusDays(1);
}
return ResponseEntity.ok(convertToUI(dto));
}
private ApiLogUI convertToUI(ApiLogDTO dto) {
ApiLogUI ui = new ApiLogUI();
ui.setApiServiceNumber(dto.getApiServiceNumber());
ui.setTxDate(dto.getTxDate());
ui.setTotalProcessingTime(dto.getTotalProcessingTime());
if (dto.getLogs() != null) {
List<ApiMessageLogUI> uiLogs = dto.getLogs().stream().map(this::convertMessageToUI)
.collect(Collectors.toList());
ui.setLogs(uiLogs);
}
return ui;
}
private ApiMessageLogUI convertMessageToUI(ApiMessageLogDTO dto) {
ApiMessageLogUI ui = new ApiMessageLogUI();
ui.setApiServiceNumber(dto.getApiServiceNumber());
ui.setProcessNumber(dto.getProcessNumber());
ui.setTxDate(dto.getTxDate());
ui.setUrl(dto.getUrl());
ui.setQueryString(dto.getQueryString());
ui.setClientId(dto.getClientId());
ui.setMethod(dto.getMethod());
ui.setHttpStatus(dto.getHttpStatus());
ui.setExdata(dto.getExdata());
ui.setProcessingTime(dto.getProcessingTime());
ui.setGuid(dto.getKeymgtmsgctnt());
// header: JSON 문자열 Map<String, String>
if (dto.getHeader() != null) {
try {
List<Map<String, String>> headerList = objectMapper.readValue(dto.getHeader(),
new TypeReference<List<Map<String, String>>>() {
});
Map<String, String> headerMap = new HashMap<>();
for (Map<String, String> headerItem : headerList) {
String name = headerItem.get("name");
String value = headerItem.get("value");
if (name != null) {
headerMap.put(name, value);
}
}
// header에서 client*id 패턴 추출 (정규식, ignoreCase)
for (Map.Entry<String, String> entry : headerMap.entrySet()) {
if (entry.getKey() != null && entry.getKey().matches("(?i)client.*id")) {
ui.setClientId(entry.getValue());
break;
}
}
ui.setHeader(headerMap);
} catch (Exception e) {
log.error("Failed to parse header JSON: {}", dto.getHeader(), e);
}
}
// body: JSON 문자열 Object
if (dto.getBody() != null) {
try {
Object bodyObject = objectMapper.readValue(dto.getBody(), Object.class);
ui.setBody(bodyObject);
} catch (Exception e) {
// JSON 파싱 실패 원본 문자열 사용
ui.setBody(dto.getBody());
}
}
return ui;
}
}
@@ -0,0 +1 @@
package com.eactive.ext.kjb.web;