From f26aae296a50e1f3b0137c65dfa782d48d4cd572 Mon Sep 17 00:00:00 2001 From: Rinjae Date: Tue, 18 Aug 2026 14:44:06 +0900 Subject: [PATCH] =?UTF-8?q?ECharts=20=ED=85=8C=EB=A7=88=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=B0=8F=20ChartEditor.js=20=EC=A0=9C=EA=B1=B0=20-?= =?UTF-8?q?=20=EB=B6=88=ED=95=84=EC=9A=94=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 37 +- .../PortalGlobalExceptionHandler.java | 34 +- src/main/resources/static/js/ChartEditor.js | 491 --- src/main/resources/static/js/Dashboard.js | 344 -- src/main/resources/static/js/LayoutManager.js | 643 ---- .../static/plugins/echarts/echarts.min.js | 45 - .../static/plugins/echarts/i18n/langEN-obj.js | 173 - .../static/plugins/echarts/i18n/langEN.js | 169 - .../static/plugins/echarts/i18n/langKO-obj.js | 173 - .../static/plugins/echarts/i18n/langKO.js | 169 - .../static/plugins/echarts/theme/azul.js | 163 - .../plugins/echarts/theme/bee-inspired.js | 178 -- .../static/plugins/echarts/theme/blue.js | 178 -- .../static/plugins/echarts/theme/caravan.js | 178 -- .../static/plugins/echarts/theme/carp.js | 163 - .../static/plugins/echarts/theme/cool.js | 180 -- .../static/plugins/echarts/theme/dark-blue.js | 164 - .../static/plugins/echarts/theme/dark-bold.js | 164 - .../plugins/echarts/theme/dark-digerati.js | 164 - .../plugins/echarts/theme/dark-fresh-cut.js | 164 - .../plugins/echarts/theme/dark-mushroom.js | 164 - .../static/plugins/echarts/theme/dark.js | 224 -- .../static/plugins/echarts/theme/eduardo.js | 178 -- .../static/plugins/echarts/theme/forest.js | 163 - .../static/plugins/echarts/theme/fresh-cut.js | 163 - .../static/plugins/echarts/theme/fruit.js | 178 -- .../static/plugins/echarts/theme/gray.js | 220 -- .../static/plugins/echarts/theme/green.js | 222 -- .../plugins/echarts/theme/helianthus.js | 263 -- .../plugins/echarts/theme/infographic.js | 236 -- .../static/plugins/echarts/theme/inspired.js | 163 - .../static/plugins/echarts/theme/jazz.js | 163 - .../static/plugins/echarts/theme/london.js | 163 - .../static/plugins/echarts/theme/macarons.js | 242 -- .../static/plugins/echarts/theme/macarons2.js | 246 -- .../static/plugins/echarts/theme/mint.js | 155 - .../plugins/echarts/theme/red-velvet.js | 163 - .../static/plugins/echarts/theme/red.js | 225 -- .../static/plugins/echarts/theme/roma.js | 119 - .../static/plugins/echarts/theme/royal.js | 163 - .../static/plugins/echarts/theme/sakura.js | 140 - .../static/plugins/echarts/theme/shine.js | 175 - .../static/plugins/echarts/theme/tech-blue.js | 184 -- .../plugins/echarts/theme/tool/option/area.js | 88 - .../plugins/echarts/theme/tool/option/bar.js | 107 - .../echarts/theme/tool/option/graph.js | 2824 ----------------- .../plugins/echarts/theme/tool/option/map.js | 141 - .../plugins/echarts/theme/tool/option/pie.js | 87 - .../echarts/theme/tool/option/scatter.js | 201 -- .../plugins/echarts/theme/tool/thumb.html | 65 - .../plugins/echarts/theme/tool/thumb.js | 63 - .../static/plugins/echarts/theme/vintage.js | 63 - .../views/apps/apis/mainApiDetail.html | 4 +- .../templates/views/fragment/djbank/head.html | 10 +- 54 files changed, 77 insertions(+), 11862 deletions(-) delete mode 100644 src/main/resources/static/js/ChartEditor.js delete mode 100644 src/main/resources/static/js/Dashboard.js delete mode 100644 src/main/resources/static/js/LayoutManager.js delete mode 100644 src/main/resources/static/plugins/echarts/echarts.min.js delete mode 100644 src/main/resources/static/plugins/echarts/i18n/langEN-obj.js delete mode 100644 src/main/resources/static/plugins/echarts/i18n/langEN.js delete mode 100644 src/main/resources/static/plugins/echarts/i18n/langKO-obj.js delete mode 100644 src/main/resources/static/plugins/echarts/i18n/langKO.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/azul.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/bee-inspired.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/blue.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/caravan.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/carp.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/cool.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/dark-blue.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/dark-bold.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/dark-digerati.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/dark-fresh-cut.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/dark-mushroom.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/dark.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/eduardo.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/forest.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/fresh-cut.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/fruit.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/gray.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/green.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/helianthus.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/infographic.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/inspired.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/jazz.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/london.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/macarons.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/macarons2.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/mint.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/red-velvet.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/red.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/roma.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/royal.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/sakura.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/shine.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/tech-blue.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/tool/option/area.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/tool/option/bar.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/tool/option/graph.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/tool/option/map.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/tool/option/pie.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/tool/option/scatter.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/tool/thumb.html delete mode 100644 src/main/resources/static/plugins/echarts/theme/tool/thumb.js delete mode 100644 src/main/resources/static/plugins/echarts/theme/vintage.js diff --git a/build.gradle b/build.gradle index 59457ed..b03f8ba 100644 --- a/build.gradle +++ b/build.gradle @@ -83,7 +83,10 @@ dependencies { implementation group: 'com.fasterxml.woodstox', name: 'woodstox-core', version: '6.5.1' - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + // Thymeleaf 3.1 코어가 #temporals 를 내장 제공 → java8time extras 를 함께 두면 표현식 객체가 중복 등록된다. + implementation('org.springframework.boot:spring-boot-starter-thymeleaf') { + exclude group: 'org.thymeleaf.extras', module: 'thymeleaf-extras-java8time' + } implementation 'org.springframework.boot:spring-boot-starter-security' implementation('org.springframework.boot:spring-boot-starter-cache') implementation 'org.springframework.boot:spring-boot-starter-data-jpa' @@ -96,10 +99,11 @@ dependencies { implementation group: 'xalan', name: 'xalan', version: '2.7.3' implementation 'org.hibernate:hibernate-envers:5.6.15.Final' - implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.0.0' + implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.4.0' // 3.0.0 은 thymeleaf 3.0 전용 implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5' - implementation 'com.github.ua-parser:uap-java:1.5.3' + // uap-java 제거: 소스 전체에 ua_parser 참조 0건이고, 1.5.3 은 snakeyaml 2.x 에서 삭제된 + // SafeConstructor() no-arg 를 호출해 NoSuchMethodError 를 낸다. 되살릴 경우 1.6.1 이상. implementation 'org.apache.httpcomponents:httpclient:4.5.14' implementation 'com.navercorp.lucy:lucy-xss-servlet:2.0.1' implementation 'javax.servlet:javax.servlet-api:4.0.0' @@ -145,6 +149,33 @@ ext { springMavenArtifactVersion = '5.3.30' encoding = 'UTF-8' profile = 'local' + + // 내장 Tomcat 버전 상향 (Spring Boot 2.7.18 기본값 9.0.83 → 9.0.120). + // Boot BOM 의 tomcat.version 프로퍼티를 덮어써서 tomcat-embed-core/-el/-websocket 이 함께 올라간다. + // 9.0.x 계열 유지 = Servlet 4.0 / javax.* 네임스페이스 그대로, JDK8 호환. + set('tomcat.version', '9.0.120') + + // CVE-2022-1471 (snakeyaml Constructor 임의 타입 역직렬화 → RCE). Boot 2.7.18 BOM 기본값 1.30 → 2.6. + // 앱/프레임워크 실경로는 이미 SafeConstructor 계열(OriginTrackedYamlLoader, spring-beans + // FilteringConstructor, swagger-parser DeserializationUtils)이라 익스플로잇 경로는 없었으나 + // SCA 는 버전으로 판정하므로 2.x 로 올린다. snakeyaml 2.6 = Java 8 바이트코드(major 52). + // 2.6 인 이유: swagger-core 2.2.52 / swagger-parser-v3 2.1.45 가 요구하는 버전이라 강등이 없다. + // 부수 효과: swagger-parser 가 호출하는 LoaderOptions.setCodePointLimit(1.32+ API) 도 해소. + set('snakeyaml.version', '2.6') + + // 위 상향의 전제. snakeyaml 2.x 는 ParserImpl(StreamReader) 를 제거했고, + // jackson-dataformat-yaml 은 2.15+ 부터 ParserImpl(StreamReader, LoaderOptions) 를 쓴다. + // BOM 을 통째로 올려 jackson 모듈 버전도 통일한다(기존엔 core/databind 만 2.18.6, 나머지는 2.13.5). + set('jackson-bom.version', '2.18.6') + + // Thymeleaf SSTI (≤3.1.3.RELEASE: 표현식 접근 객체 제한 우회 → 템플릿 인젝션). 3.0.x 는 EOL 이라 + // 백포트가 없어 3.1.4 로 올린다. JDK8/Spring5 유지: thymeleaf 3.1.4 / thymeleaf-spring5 3.1.4 / + // extras-springsecurity5 3.1.5 / layout-dialect 3.4.0 모두 Java8 바이트코드(major 52), 패키지도 + // org.thymeleaf.spring5 + javax.servlet 그대로다. + // Boot 2.7 ThymeleafAutoConfiguration 이 호출하는 setter 는 3.1.4 에 전부 존재함(확인함). + // 주의: 3.1 은 #request/#session/#response/#servletContext 표현식 객체를 제거했다(IllegalArgumentException). + set('thymeleaf.version', '3.1.4.RELEASE') + set('thymeleaf-extras-springsecurity.version', '3.1.5.RELEASE') } diff --git a/src/main/java/com/eactive/apim/portal/common/exception/PortalGlobalExceptionHandler.java b/src/main/java/com/eactive/apim/portal/common/exception/PortalGlobalExceptionHandler.java index 8afd94e..c39c024 100644 --- a/src/main/java/com/eactive/apim/portal/common/exception/PortalGlobalExceptionHandler.java +++ b/src/main/java/com/eactive/apim/portal/common/exception/PortalGlobalExceptionHandler.java @@ -2,6 +2,7 @@ package com.eactive.apim.portal.common.exception; import java.util.Arrays; +import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; @@ -122,9 +123,40 @@ public class PortalGlobalExceptionHandler { || path.startsWith("/favicon")); } + /** + * 리다이렉트/뷰 이름으로 허용할 문자. 스킴 구분자(:)·중괄호·달러를 막아 + * 외부 URL 이나 Thymeleaf 표현식이 뷰 이름으로 흘러드는 것을 차단한다. + */ + private static final Pattern SAFE_REDIRECT_TARGET = Pattern.compile("[A-Za-z0-9._/?=&%-]+"); + @ExceptionHandler(value = PortalRedirectException.class) public ModelAndView handlePortalRedirectException(HttpServletRequest request, PortalRedirectException ex) { - return new ModelAndView(ex.getMessage()); + // PortalRedirectException 은 super(message) 를 호출하지 않으므로 getMessage() 는 항상 null 이다. + // 이전 구현은 그 null 을 뷰 이름으로 넘겨(= 뷰 미지정) 요청 URL 기준으로 뷰가 추론되게 만들었다. + // 실제 대상은 redirectPage 필드다. + String target = ex.getRedirectPage(); + if (!isSafeRedirectTarget(target)) { + log.warn("허용되지 않은 리다이렉트 대상 - uri={}, target={}", request.getRequestURI(), target); + return new ModelAndView("redirect:/"); + } + return new ModelAndView(target); + } + + /** 내부 경로/뷰 이름만 허용(외부 URL·프로토콜 상대 URL·표현식 문자 차단) */ + private boolean isSafeRedirectTarget(String target) { + if (target == null) { + return false; + } + String path = target; + if (path.startsWith("redirect:")) { + path = path.substring("redirect:".length()); + } else if (path.startsWith("forward:")) { + path = path.substring("forward:".length()); + } + if (path.isEmpty() || path.startsWith("//")) { // //evil.example 형태의 외부 리다이렉트 차단 + return false; + } + return SAFE_REDIRECT_TARGET.matcher(path).matches(); } @ExceptionHandler(value = {IllegalArgumentException.class}) diff --git a/src/main/resources/static/js/ChartEditor.js b/src/main/resources/static/js/ChartEditor.js deleted file mode 100644 index 26d0f20..0000000 --- a/src/main/resources/static/js/ChartEditor.js +++ /dev/null @@ -1,491 +0,0 @@ -class ChartEditor { - constructor(container, layoutManager, dashboard) { - this.rootContainer = container; - this.layoutManager = layoutManager; - this.dashboard = dashboard; - this.targetContainerIndex = -1; - this.targetColumnIndex = -1; - this.tempChart = null; - this.chartPreview = null; - this.containerHeight = 0; - - this.chartEditorModel = [ - { - 'groupName': 'default', - 'groupTitle': '기본 정보', - 'fields': [ - { - 'path': 'contentId', - 'type': 'text', - 'label': 'Chart ID', - 'placeholder': 'chartId를 입력하세요' - }, - { - 'path': 'styles.width', - 'type': 'text', - 'label': '길이(px, %)', - 'placeholder': 'px, % 등 단위를 포함한 길이를 입력하세요' - }, - { - 'path': 'styles.height', - 'type': 'text', - 'label': '높이(px, %)', - 'placeholder': 'px, % 등 단위를 포함한 높이를 입력하세요' - }, - { - 'path': 'config.animation', - 'type': 'boolean', - 'label': 'Animation 사용' - } - ] - }, - { - 'groupName': 'title', - 'groupTitle': '제목', - 'fields': [ - { - 'path': 'config.title.text', - 'type': 'text', - 'label': '차트 제목', - 'placeholder': '차트 제목을 입력하세요' - } - ] - }, - { - 'groupName': 'tooltip', - 'groupTitle': 'Tooltip', - 'fields': [ - { - 'path': 'config.tooltip.show', - 'type': 'boolean', - 'label': 'Tooltip 표시' - }, - { - 'path': 'config.tooltip.trigger', - 'type': 'select', - 'label': 'Tooltip Trigger', - 'placeholder': 'Enter the trigger type', - 'options': ['item', 'axis', 'none'] - } - ] - }, - { - 'groupName': 'legend', - 'groupTitle': 'Legend', - 'fields': [ - { - 'path': 'config.legend.show', - 'type': 'boolean', - 'label': 'Legend 표시' - } - ] - }, - { - 'groupName': 'grid', - 'groupTitle': 'Grid', - 'fields': [ - { - 'path': 'config.grid.show', - 'type': 'boolean', - 'label': 'Grid 표시' - }, - { - 'path': 'config.grid.left', - 'type': 'text', - 'label': 'Grid Left', - 'placeholder': '좌측 여백' - }, - { - 'path': 'config.grid.right', - 'type': 'text', - 'label': 'Grid Right', - 'placeholder': '우측 여백' - }, - { - 'path': 'config.grid.top', - 'type': 'text', - 'label': 'Grid Top', - 'placeholder': '위쪽 여백' - }, - { - 'path': 'config.grid.bottom', - 'type': 'text', - 'label': 'Grid Bottom', - 'placeholder': '아래쪽 여백' - }, - { - 'path': 'config.grid.containLabel', - 'type': 'boolean', - 'label': 'Contain Label' - } - ] - }, - { - 'groupName': 'xAxis', - 'groupTitle': 'X축', - 'fields': [ - { - 'path': 'config.xAxis.show', - 'type': 'boolean', - 'label': 'X축 표시' - }, - { - 'path': 'config.xAxis.type', - 'type': 'select', - 'label': 'X축 유형', - 'placeholder': 'X축 유형을 선택하세요', - 'options': ['category', 'value', 'time'] - } - ] - }, - { - 'groupName': 'yAxis', - 'groupTitle': 'Y축', - 'fields': [ - { - 'path': 'config.yAxis[0].show', - 'type': 'boolean', - 'label': 'Y축 표시' - }, - { - 'path': 'config.yAxis[0].name', - 'type': 'text', - 'label': 'Y축 이름' - }, - { - 'path': 'config.yAxis[0].type', - 'type': 'select', - 'label': 'Y축 유형', - 'placeholder': 'Y축 유형을 선택하세요', - 'options': ['category', 'value', 'time'] - }, - { - 'path': 'config.yAxis[0].position', - 'type': 'select', - 'label': 'Y축 위치', - 'placeholder': 'Y축 위치를 선택하세요', - 'options': ['left', 'right'] - } - ] - }, - { - 'groupName': 'toolbox', - 'groupTitle': 'Toolbox', - 'fields': [ - { - 'path': 'config.toolbox.show', - 'type': 'boolean', - 'label': 'Toolbox 표시', - 'placeholder': 'Check to show toolbox' - }, - { - 'path': 'config.toolbox.feature.saveAsImage.show', - 'type': 'boolean', - 'label': 'Save as Image 사용' - } - ] - } - ]; - this.seriesConfigModel = [ - { - 'groupName': 'default', - 'groupTitle': '시리즈 정보', - 'fields': [ - { - 'path': 'seriesConfig[0].queryType', - 'type': 'select', - 'label': 'Query Type', - 'options': ['elasticsearch'] - }, - // { - // 'path': 'seriesConfig[0].type', - // 'type': 'select', - // 'label': '차트 유형', - // 'placeholder': '차트 유형을 선택하세요', - // 'options': ['bar', 'line', 'pie'] - // }, - { - 'path': 'seriesConfig[0].index', - 'type': 'text', - 'label': 'Elasticsearch Index', - 'placeholder': 'Elasticsearch Index를 입력하세요' - }, - { - 'path': 'seriesConfig[0].query', - 'type': 'textarea', - 'label': 'Elasticsearch Query' - }, - { - 'path': 'seriesConfig[0].queryResult', - 'type': 'textarea', - 'label': 'Elasticsearch Query Result' - }, - { - 'path': 'seriesConfig[0].dataPath', - 'type': 'text', - 'label': '데이터 경로', - 'placeholder': '데이터 경로를 입력하세요' - } - ] - } - ]; - } - - initChartEditor() { - - const chartEditorPopup = ` - `; - - $(this.rootContainer).parent().append(chartEditorPopup); - - //add event to modal show - $('#chart_editor_modal').on('shown.bs.modal', (event) => { - let chartContainer = document.getElementById('chart-preview'); - let rect = chartContainer.getBoundingClientRect(); - let width = rect.width; // Precise width - let height = rect.height; // Precise height - this.chartPreview = echarts.init(document.getElementById('chart-preview'), 'dark', { - width: width, height: height - }); - this.drawCharts(); - }); - } - - showChartEditor(isNew, tempChart, containerIndex, columnIndex) { - this.targetContainerIndex = containerIndex; - this.targetColumnIndex = columnIndex; - this.tempChart = tempChart; - if (isNew) { - $('.add-chart').show(); - $('.save-chart').hide(); - } else { - $('.add-chart').hide(); - $('.save-chart').show(); - } - - let flatModel = LayoutUtil.flattenObject(tempChart); - - $('#chart_editor_modal').find('input, select, textarea').each((index, element) => { - if ($(element).attr('type') === 'checkbox') { - $(element).prop('checked', false); - } - $(element).val(''); - }); - - _.forEach(flatModel, (value, key) => { - - const inputSelector = `input[name="${key}"]`; - const selectSelector = `select[name="${key}"]`; - const textareaSelector = `textarea[name="${key}"]`; - - const $input = $('#chart_editor_modal').find(inputSelector); - const $select = $('#chart_editor_modal').find(selectSelector); - const $textarea = $('#chart_editor_modal').find(textareaSelector); - - if ($input.length > 0 && $input.attr('type') === 'checkbox') { - // If it's a checkbox, update its 'checked' property - $input.prop('checked', value); - } else if ($input.length > 0 || $select.length > 0 || $textarea.length > 0) { - // If any input, select, or textarea is found, update its value - $($input.add($select).add($textarea)).val(value); - } else { - // If no elements are found, log to the console - console.log(`Element not found for name: ${key}`); - } - }); - - $('#chart_editor_modal').modal('show'); - console.log('chart_editor_modal show'); - - this.bindEvents(); - } - - addChart() { - $('#chart_editor_modal').find('input, select, textarea').each((index, element) => { - const fieldName = $(element).attr('name'); - const newValue = LayoutUtil.getFieldValue(element); - if (fieldName !== '') { - _.set(this.tempChart, fieldName, newValue); - } - }); - this.layoutManager.layoutData.children[this.targetContainerIndex].children.push(JSON.parse(JSON.stringify(this.tempChart))); - this.tempChart = null; - this.targetContainerIndex = -1; - this.targetColumnIndex = -1; - - this.layoutManager.resetLayout(); - $('#chart_editor_modal').modal('hide'); - } - - cleanUpEvents() { - console.log('cleanUpEvents'); - $('.add-chart').off('click'); - $('.save-chart').off('click'); - } - - saveChart() { - console.log('saveChart'); - - $('#chart_editor_modal').find('input, select, textarea').each((index, element) => { - const fieldName = $(element).attr('name'); - const newValue = LayoutUtil.getFieldValue(element); - if (fieldName !== undefined && fieldName !== '') { - _.set(this.tempChart, fieldName, newValue); - } - }); - const json = JSON.stringify(this.tempChart, LayoutUtil.echartReplacer, 2); - console.log(json); - this.layoutManager.layoutData.children[this.targetContainerIndex].children[this.targetColumnIndex] = JSON.parse(json); - this.targetContainerIndex = -1; - this.targetColumnIndex = -1; - this.layoutManager.resetLayout(); - $('#chart_editor_modal').modal('hide'); - } - - refreshChart(chartInfo) { - console.log('refreshChart'); - - return new Promise((resolve, reject) => { - let option = JSON.parse(JSON.stringify(chartInfo.config)); - option.series = []; - - let self = this; - Promise.all(this.tempChart.seriesConfig.map(config => this.dashboard.requestQuery(config))).then(async (results) => { - await self.dashboard.updateChartSeries(option, results, chartInfo, (index, result) => { - $('#chart_editor_modal').find(`textarea[name='seriesConfig[${index}].queryResult']`).val('result = ' + JSON.stringify(result, null, 2)); - }); - - document.getElementById('echartOption').value = JSON.stringify(option, null, 2); - self.chartPreview.setOption(option); - resolve(); - }).catch((error) => { - console.error('Error fetching data for preview: ', error); - reject(error); - }); - - }); - } - - drawCharts() { - console.log('drawCharts'); - - const promises = [this.refreshChart(this.tempChart)]; - - Promise.all(promises).then(() => console.log('Chart Preview is completed')).catch(error => console.error('An error occurred:', error)); - } - - bindEvents() { - $('.add-chart').off('click').on('click', (event) => { - this.addChart(); - }); - - $('.save-chart').off('click').on('click', (event) => { - this.saveChart(); - }); - - $('#chart_editor_modal').find('input, select, textarea').each((index, element) => { - $(element).off('change').on('change', (event) => { - console.log('change event'); - const fieldName = $(event.currentTarget).attr('name'); - const newValue = LayoutUtil.getFieldValue(event.currentTarget); - if (fieldName !== '') { - _.set(this.tempChart, fieldName, newValue); - } - this.refreshChart(this.tempChart); - }); - }); - - $('#chart_editor_modal').on('hidden.bs.modal', (event) => { - if (this.chartPreview != null && this.chartPreview.dispose) { - this.chartPreview.dispose(); - this.chartPreview = null; - } - - this.cleanUpEvents(); - }); - } -} diff --git a/src/main/resources/static/js/Dashboard.js b/src/main/resources/static/js/Dashboard.js deleted file mode 100644 index 43f6235..0000000 --- a/src/main/resources/static/js/Dashboard.js +++ /dev/null @@ -1,344 +0,0 @@ -class Dashboard { - - constructor(layoutData) { - this.layoutData = layoutData || {}; - this.fromDateTarget = 'fromDate'; - this.toDateTarget = 'toDate'; - this.charts = []; - this.theme = 'dark'; - this.requestUrl = '/mgmt/dashboard/search.do'; - - } - - bindEvents() { - const events = [{selector: '.refresh', action: this.refresh.bind(this)}]; - - for (let event of events) { - $(document).off('click', event.selector).on('click', event.selector, event.action); - } - } - - getDateTemplate(id, label) { - return ` -
-
- - -
- - - -
- `; - } - - init(chartAreaId) { - console.log('Dashboard init'); - this.chartAreaId = chartAreaId; - this.initDate(); - this.initCharts(); - this.drawCharts(); - - window.addEventListener('resize', this.resizeAllCharts.bind(this)); - } - - initCharts() { - console.log('initCharts'); - this.charts = []; - if (this.layoutData.children) { - this.layoutData.children.forEach((row) => { - row.children.forEach((chartInfo) => { - this.charts.push(chartInfo); - }); - }); - } - } - - formatDate(isoString) { - const date = new Date(isoString); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); // +1 because getMonth() returns 0-11 - const day = String(date.getDate()).padStart(2, '0'); - - return `${year}-${month}-${day}`; - } - - formatTime(isoString) { - const date = new Date(isoString); - const hour = String(date.getHours()).padStart(2, '0'); - const minute = String(date.getMinutes()).padStart(2, '0'); - return `${hour}:${minute}:00`; - } - - formatDateTime(isoString) { - return this.formatDate(isoString) + 'T' + this.formatTime(isoString); - } - - startOfDay(isoString) { - return this.formatDate(isoString) + 'T00:00:00'; - } - - initDate() { - const ko = tempusDominus.locales.ko.localization; - const fromDateElement = document.getElementById(this.fromDateTarget); - $(fromDateElement).data('td-target-input', 'nearest'); - $(fromDateElement).data('td-target-toggle', 'nearest'); - - const toDateElement = document.getElementById(this.toDateTarget); - $(toDateElement).data('td-target-input', 'nearest'); - $(toDateElement).data('td-target-toggle', 'nearest'); - - $(fromDateElement).append(this.getDateTemplate(this.fromDateTarget, 'From')); - $(toDateElement).append(this.getDateTemplate(this.toDateTarget, 'To')); - - this.fromDate = new tempusDominus.TempusDominus(fromDateElement, { - localization: ko, defaultDate: this.startOfDay(new Date()) - }); - this.toDate = new tempusDominus.TempusDominus(toDateElement, { - useCurrent: false, localization: ko, defaultDate: new Date() - }); - - fromDateElement.addEventListener(tempusDominus.Namespace.events.change, (e) => { - this.toDate.updateOptions({ - restrictions: { - minDate: e.detail.date - } - }); - }); - - const subscription = this.toDate.subscribe(tempusDominus.Namespace.events.change, (e) => { - this.fromDate.updateOptions({ - restrictions: { - maxDate: e.date - } - }); - }); - } - - requestPost(url, data, hds) { - var deferred = $.Deferred(); - - var headers = $.extend({ - 'Content-Type': 'application/json; charset=utf-8' - }, hds); - - if (headers['Content-Type'].startsWith('application/json')) { - data = JSON.stringify(data); - } - - $.ajax({ - url: url, async: true, method: 'POST', data: data, headers: headers - }).done((response) => { - deferred.resolve(response); - }).fail((response, textStatus, errorThrown) => { - deferred.reject(response, textStatus, errorThrown); - }); - return deferred.promise(); - }; - - requestQuery(seriesConfig) { - let request = null; - try { - if (seriesConfig.queryType === 'elasticsearch') { - if (seriesConfig.query && seriesConfig.index !== '') { - let requestQuery = JSON.parse(seriesConfig.query); - requestQuery.query = requestQuery.query || {}; - requestQuery.query.bool = requestQuery.query.bool || {}; - requestQuery.query.bool.filter = requestQuery.query.bool.filter || []; - requestQuery.query.bool.filter.push({ - 'range': { - '@timestamp': { - 'gte': this.formatDateTime(this.fromDate.dates.picked[0]), - 'lte': this.formatDateTime(this.toDate.dates.picked[0]), - 'time_zone': 'Asia/Seoul' - } - } - }); - - request = { - query: JSON.stringify(requestQuery), index: seriesConfig.index - }; - return this.requestPost(this.requestUrl, request, {}); - } else { - console.log('query is empty'); - return Promise.resolve(); - } - } - } catch (error) { - console.error('Error fetching data for ' + seriesConfig.contentId + ': ', error); - return Promise.reject(error); - } - } - - resizeAllCharts() { - this.charts.forEach(chartInfo => { - let chartContainer = document.getElementById('chart_' + chartInfo.contentId); - let rect = chartContainer.getBoundingClientRect(); - let width = rect.width; // Precise width - let height = rect.height; // Precise height - if (chartInfo.echart) { - chartInfo.echart.resize({width: width, height: height}); - } - }); - } - - initializeChart(chartInfo) { - console.log('initializeChart: ' + chartInfo.contentId); - let chartContainer = document.getElementById('chart_' + chartInfo.contentId); - let rect = chartContainer.getBoundingClientRect(); - let width = rect.width; // Precise width - let height = rect.height; // Precise height - - chartInfo.echart = echarts.init(document.getElementById('chart_' + chartInfo.contentId), this.theme, { - width: width, height: height - }); - } - - executeScript(result, script) { - console.log('executeScript'); - if (script) { - return new Promise((resolve, reject) => { - let resultFrame = document.getElementById('mapperScript'); - window.preRequestComplete = (result, error) => { - if (error) { - console.log('Error executing script: ', error); - reject(error); - } else { - resolve(result); - } - }; - window.result = result; - resultFrame.srcdoc = ``; - }); - } else { - console.log('No script to execute'); - return Promise.resolve(); - } - - } - - async refresh(event) { - console.log('refresh'); - - for (let i = 0; i < this.charts.length; i++) { - const chartInfo = this.charts[i]; - try { - await this.refreshChart(chartInfo); - } catch (error) { - console.error(`Error refreshing chart ${chartInfo.contentId}:`, error); - } - } - console.log('All Charts requests are complete'); - // const promises = this.charts.map(info => this.refreshChart(info)); - // Promise.all(promises).then(() => console.log('All Charts requests are complete')).catch(error => console.error('An error occurred:', error)); - } - - drawCharts() { - console.log('drawCharts'); - this.charts.forEach(info => this.initializeChart(info)); - - this.refresh(); - this.bindEvents(); - } - - async updateChartSeries(option, results, chartInfo, callback) { - console.log('updateChartSeries'); - for (let i = 0; i < results.length; i++) { - const result = results[i]; - if (result){ - let config = chartInfo.seriesConfig[i]; - let parsedResult = JSON.parse(result); - - if (callback) { - callback(i, parsedResult); - } - try { - console.log('start executeScript'); - const dataY = await this.executeScript(parsedResult, config.mapperY); - - if (Array.isArray(dataY)) { - dataY.forEach(item => { - option.series.push(item); - }); - } else { - option.series.push(dataY); - } - const dataX = await this.executeScript(parsedResult, config.mapperX); - option.xAxis.data = dataX; - - console.log('stop executeScript'); - } catch (error) { - console.error(`Error updating series for chart ${chartInfo.contentId}:`, error); - } - } - } - } - - async refreshChart(chartInfo) { - - try { - console.log('refreshChart'); - - let option = JSON.parse(JSON.stringify(chartInfo.config)); - option.series = []; - - const results = await Promise.all(chartInfo.seriesConfig.map(config => this.requestQuery(config))); - await this.updateChartSeries(option, results, chartInfo); - - // Clear the chart and set the new option - chartInfo.echart.clear(); - chartInfo.echart.setOption(option); - - console.log('Chart refreshed successfully'); - - } catch (error) { - console.error('Error fetching data for ' + chartInfo.contentId + ': ', error); - throw error; // Rethrow the error if you need to catch it higher up in the call stack - } - } - -} diff --git a/src/main/resources/static/js/LayoutManager.js b/src/main/resources/static/js/LayoutManager.js deleted file mode 100644 index 0634f88..0000000 --- a/src/main/resources/static/js/LayoutManager.js +++ /dev/null @@ -1,643 +0,0 @@ -class LayoutManager { - constructor(container, layoutData) { - this.rootContainer = container; - this.tempContainer = null; - this.editContainerIndex = -1; - this.layoutData = layoutData || { - type: 'root', - styles: { - 'display': 'flex', - 'flex-direction': 'column', - 'justify-content': 'start', - 'align-items': 'flex-start', - 'gap': '10px', - 'width': '100%', - 'height': '100vh', - 'position': 'relative', - 'overflow': 'auto', - 'background-color': '#100C2A' - }, - children: [] - }; - - this.containerEditorModel = [ - { - 'groupName': 'default', - 'groupTitle': '기본', - 'fields': [ - { - 'path': 'styles.display', - 'type': 'select', - 'label': 'Display', - 'options': ['flex'] - }, - { - 'path': 'styles.flex-direction', - 'type': 'select', - 'label': 'Flex 방향', - 'options': ['row', 'column', 'row-reverse', 'column-reverse'] - }, - { - 'path': 'styles.width', - 'type': 'text', - 'label': '길이(px, %)', - 'placeholder': 'px, % 등 단위를 포함한 길이를 입력하세요' - }, - { - 'path': 'styles.height', - 'type': 'text', - 'label': '높이(px, %)', - 'placeholder': 'px, % 등 단위를 포함한 높이를 입력하세요' - } - ] - } - ]; - - this.dashboardEditorModel = [ - { - 'groupName': 'default', - 'groupTitle': '기본', - 'fields': [ - { - 'path': 'styles.background-color', - 'type': 'text', - 'label': '색상', - 'placeholder': '#로 시작하는 값을 입력하세요' - }, - { - 'path': 'styles.width', - 'type': 'text', - 'label': '길이(px)', - 'placeholder': 'px 단위를 포함한 길이를 입력하세요' - }, - { - 'path': 'styles.height', - 'type': 'text', - 'label': '높이(px)', - 'placeholder': 'px 단위를 포함한 높이를 입력하세요' - } - ] - } - ] - - } - - // traverse(obj) { - // if (obj.type === 'chart') { - // $(`#chart_${obj.contentId}`).closest('.dashboard-chart').resizable({ - // grid: 10, - // ghost: true, - // stop: (event, ui) => { - // const {width, height} = ui.size; - // obj.styles.width = `${width}px`; - // obj.styles.height = `${height}px`; - // this.resetLayout(); - // } - // }); - // } - // - // if (obj.children) { - // obj.children.forEach(child => { - // this.traverse(child); - // }); - // } - // } - - init(Dashboard, viewerMode) { - this.viewMode = viewerMode || false; - this.renderRoot(this.layoutData, this.rootContainer); - - if (Dashboard) { - this.dashboard = new Dashboard(this.layoutData); - this.dashboard.init('chart_area'); - } - - if (!this.viewMode) { - this.initContainerEditor(); - this.initDashboardEditor(); - this.chartEditor = new ChartEditor(this.rootContainer, this, this.dashboard); - this.chartEditor.initChartEditor(); - this.bindEvents(); - } - } - - initContainerEditor() { - const containerEditorPopup = ` -