14 Commits

Author SHA1 Message Date
Rinjae dfb37c18b9 commons-lang3 3.13 → 3.13.1 업그레이드와 코드 최적화
- Strings API로 대체 및 StringUtils 불필요 메서드 제거
- CellPhoneValidator 시작 번호 체크 로직 개선
2026-08-18 17:48:58 +09:00
Rinjae 5f5b23026f JSON 및 파일 처리 라이브러리 상향 회귀 테스트 추가
- commons-lang3 3.20.0 업그레이드로 문자열 처리 유지 검증
- 파트 헤더 설정 증가(CVE-2025-48976) 회귀 확인 테스트 추가
- jackson-bom 2.18.10 업그레이드 후 날짜 직렬화 검증
2026-08-18 17:07:21 +09:00
Rinjae 5c61fc3f24 멀티파트 업로드 및 의존성 보안 업데이트
- MultipartConfig: partHeaderSizeMax 설정 추가(CVE-2025-48976 대응)
- build.gradle: commons-fileupload 1.6.0 및 commons-beanutils 1.11.0 업그레이드
2026-08-18 16:56:26 +09:00
Rinjae a7aa5bc98c Spring Boot/Framework/Security 관련 CVE 억제 규칙 추가 및 구성 수정
- dependency-check-suppressions.xml: 오탐 및 비사용 기능 CVE 5건 억제 추가
- PortalConfigSecurity.java: CVE-2026-22732 대응 우회 로직 추가
- build.gradle: Spring 의존성 버전 정리 및 중복 핀 제거
2026-08-18 15:18:08 +09:00
Rinjae bf63701e68 리다이렉트 경로 상수화 및 중복 제거
- "redirect:/", "redirect:/login?reason=auth" 상수로 치환
- 중복된 문자열 정의 제거 및 가독성 개선
2026-08-18 14:50:08 +09:00
Rinjae f26aae296a ECharts 테마 파일 및 ChartEditor.js 제거 - 불필요 리소스 삭제 2026-08-18 14:44:06 +09:00
Rinjae 8b4bc425ec - build.gradle: damo-manager 의존성 scope 변경(compileOnly → implementation)
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- application.yml: 로그 경로 수정(/logs/eapim → /logs/prod/eapim)
2026-08-14 19:05:29 +09:00
Rinjae 06dc148515 Lodash 업데이트 및 보안 강화
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- Lodash 버전 4.17.21 → 4.18.1로 업데이트
- prototype pollution 방지 로직 추가
- `_.template` 관련 보안 경고 및 문서 갱신
2026-08-14 17:48:45 +09:00
Rinjae 3cfd6b0b04 Atomikos JTA 제거: 로컬 트랜잭션 전환 및 관련 설정 정리
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- JTA/XA 미사용으로 변경: JpaTransactionManager로 전환
- Atomikos 의존성 및 설정 제거, 관련 문서 및 주석 업데이트
2026-08-14 17:11:54 +09:00
Rinjae 7a0f88bfb1 Atomikos 설정 변경: max-actives 속성 추가
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-08-14 16:50:10 +09:00
Rinjae 6d7b292b8a 운영 인증번호 테스트 종료일 판정 클래스 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- TestNoticeWindow: 운영 환경에서 인증번호 노출 종료일 유효성 검증
- TwoFactorProperties/AuthNoticeProperties에 종료일 기반 처리 로직 통합
2026-08-14 13:13:20 +09:00
Rinjae 20ad6f8394 Thymeleaf 템플릿 JS 분석 제외 설정 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- sonar.javascript.exclusions 속성에 템플릿 경로 추가
- XSS 룰 유지 위해 HTML 분석은 제외하지 않음
2026-08-13 18:18:09 +09:00
Rinjae 2bae0e350c OWASP Dependency-Check 통합 추가
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
- Jenkinsfile.security: 라이브러리 취약점 검사용 파이프라인 작성 (SAST/SCA 병행)
- dependency-check-classpath.gradle: 스캔 대상 의존성 jar 분리 로직 작성
- dependency-check-suppressions.xml: 오탐 억제 규칙 파일 추가
2026-08-13 17:53:14 +09:00
Rinjae 45e8fa2903 레거시 암호화 컨트롤러 제거 - 내부 API 토큰 초기화 로직 추가 - 정규식 성능 최적화
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled
2026-08-13 16:52:04 +09:00
89 changed files with 1750 additions and 12247 deletions
+10 -3
View File
@@ -185,8 +185,9 @@ com.eactive.apim.portal/
설정: `config/PortalDatasourceConfiguration.java`
- 각 데이터베이스별 별도 EntityManager
- Atomikos JTA를 통한 분산 트랜잭션
- 트랜잭션 로그: `/Log/eapim/portal/`
- **JTA/XA 미사용**. EntityManagerFactory 별 로컬 트랜잭션 (`config/PortalConfigTransaction.java`)
- `transactionManager` (@Primary) → EMS
- `gatewayTransactionManager` → Gateway
### 설정 프로파일
@@ -359,7 +360,13 @@ return queryFactory.selectFrom(user)
적절한 propagation과 함께 `@Transactional` 사용:
- 기본값: `REQUIRED` (기존 트랜잭션에 참여하거나 새로 생성)
- 읽기 전용 작업: 최적화를 위해 `@Transactional(readOnly = true)`
- 다중 데이터베이스: Atomikos JTA가 자동으로 분산 트랜잭션 처리
- **다중 데이터베이스: 분산 트랜잭션(JTA/XA) 없음.** EMS·Gateway 각각 독립 로컬 트랜잭션이다.
- 무지정 `@Transactional` = EMS(`transactionManager`)
- **Gateway 엔티티(`com.eactive.apim.gateway.*`, `com.eactive.eai.data.entity.onl.*`)를 다루는 서비스는
`@Transactional("gatewayTransactionManager")` 를 명시**한다. 안 하면 게이트웨이 EntityManager 가
리포지토리 호출 단위로 닫혀 지연 로딩에서 `LazyInitializationException` 이 난다
(예: `ApiServiceService` — `ApiGroup.apiGroupApiList`).
- 두 DB 를 한 원자 단위로 묶어야 하는 작업은 만들지 않는다. 현재 Gateway 는 조회 전용이다.
### 에러 처리
+501
View File
@@ -0,0 +1,501 @@
// 보안 검사 통합 파이프라인 — OWASP Dependency-Check(SCA) + SonarQube(SAST).
//
// 두 검사는 보는 대상이 다르다. 하나만 돌리면 절반만 본다.
// SonarQube : 우리가 쓴 코드의 결함 (SQL Injection, XSS, 하드코딩 시크릿 …)
// Dependency-Check : 우리가 쓰는 라이브러리의 알려진 취약점 (CVE)
//
// 전제 (Jenkins 쪽 설정 이름이 다르면 environment 블록만 고치면 된다)
// 1) Manage Jenkins > System > SonarQube servers 에 서버 등록 (이름: SONAR_ENV_NAME)
// 2) Manage Jenkins > Tools > SonarQube Scanner 에 스캐너 등록 (없으면 PATH 탐색)
// 3) 노드에 Dependency-Check CLI 설치 (DC_HOME) — 설치 방법은 17-보안 가이드 문서 참고
// 4) NVD API Key 를 Secret text credential 로 등록 (기본 ID: nvd-api-key)
// 키가 없으면 NVD 갱신이 극심하게 느려진다(수 시간). 폐쇄망은 UPDATE_NVD=false 로 운영.
// 5) Quality Gate 대기를 쓰려면 SonarQube 에 <JENKINS_URL>/sonarqube-webhook/ 웹훅 등록
//
// JDK 분리: 빌드는 JDK 8, 스캐너/Dependency-Check 실행은 JDK 17.
// (SonarScanner CLI 5.x 는 JRE 17 필수, Dependency-Check 9.x 이상은 JRE 11 이상 필수)
pipeline {
agent { label 'djb-vm' }
triggers {
// 코드 변경 감지
pollSCM('H/30 * * * *')
// 코드가 그대로여도 새 CVE 는 계속 공개된다 → 야간 정기 재검사
cron('H 3 * * *')
}
options {
timestamps()
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '20'))
}
parameters {
booleanParam(
name: 'RUN_DEPENDENCY_CHECK',
defaultValue: true,
description: 'OWASP Dependency-Check(SCA) 실행'
)
booleanParam(
name: 'RUN_SONAR',
defaultValue: true,
description: 'SonarQube 정적 분석(SAST) 실행'
)
booleanParam(
name: 'RUN_TESTS',
defaultValue: false,
description: '단위 테스트를 함께 실행해 JUnit 결과를 Sonar 로 전송한다(분석 시간 증가).'
)
booleanParam(
name: 'UPDATE_NVD',
defaultValue: true,
description: 'NVD 취약점 DB 갱신. 폐쇄망(외부 인터넷 불가)이면 반드시 끈다 — 캐시된 DB 로만 검사한다.'
)
string(
name: 'FAIL_ON_CVSS',
defaultValue: '11',
description: '이 CVSS 점수 이상이면 검사 실패로 표시. 11 = 실패시키지 않음(리포트만). 예: 7 = High 이상'
)
booleanParam(
name: 'FAIL_BUILD_ON_FINDING',
defaultValue: false,
description: 'FAIL_ON_CVSS 위반 시 빌드를 FAILURE 로 만든다. 끄면 UNSTABLE 로만 표시.'
)
booleanParam(
name: 'SCAN_JS',
defaultValue: true,
description: '정적 JS 라이브러리(static/js, static/plugins)도 RetireJS 로 검사. 폐쇄망에서 오류나면 끈다.'
)
}
environment {
// --- Jenkins 설정 이름 (환경에 맞게 수정) ---
SONAR_ENV_NAME = 'SonarQube-Community'
SONAR_SCANNER_TOOL = ''
// NVD API Key 를 담은 Secret text credential ID. 없으면 키 없이 진행한다(느림).
NVD_API_KEY_CRED = 'nvd-api-key'
// --- 툴체인 ---
JAVA_HOME = '/apps/opts/jdk8' // gradle 컴파일용 (프로젝트는 Java 8)
JAVA_HOME_SCANNER = '/apps/opts/jdk17' // sonar-scanner / dependency-check 실행용
GRADLE_HOME = '/apps/opts/gradle-8.7'
GRADLE_USER_HOME = '/apps/opts/gradle-home'
PATH = "/apps/opts/jdk8/bin:/apps/opts/gradle-8.7/bin:/apps/opts/bin:${env.PATH}"
GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new'
// --- Dependency-Check ---
// CLI 설치 경로. 비어 있거나 없으면 Jenkins Tools 등록분 > PATH 순으로 탐색한다.
DC_HOME = '/apps/opts/dependency-check'
// NVD 캐시(H2 DB). 워크스페이스 밖에 두어야 빌드마다 1GB 이상을 다시 받지 않는다.
// 여러 Job 이 공유하므로 disableConcurrentBuilds 를 켠 채로 쓴다.
DC_DATA = '/apps/opts/dependency-check-data'
DC_TOOL_NAME = ''
DC_REPORT_DIR = 'build/reports/dependency-check'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
// eapim-portal 은 elink-portal-common / elink-online-core-jpa 를 subproject 로 참조한다.
// 이 둘이 없으면 컴파일도 의존성 해석도 불가능하다.
stage('Checkout dependencies') {
steps {
sh '''
set -eu
cd "$WORKSPACE/.."
if [ ! -d elink-portal-common/.git ]; then
rm -rf elink-portal-common
git clone --depth=1 --branch master \
ssh://git@172.30.1.50:2222/djb-eapim/elink-portal-common.git \
elink-portal-common
else
git -C elink-portal-common fetch --depth=1 origin master
git -C elink-portal-common reset --hard origin/master
git -C elink-portal-common clean -fdx
fi
mkdir -p eapim-online
if [ ! -d eapim-online/elink-online-core-jpa/.git ]; then
rm -rf eapim-online/elink-online-core-jpa
git clone --depth=1 --branch master \
ssh://git@172.30.1.50:2222/djb-eapim/elink-online-core-jpa.git \
eapim-online/elink-online-core-jpa
else
git -C eapim-online/elink-online-core-jpa fetch --depth=1 origin master
git -C eapim-online/elink-online-core-jpa reset --hard origin/master
git -C eapim-online/elink-online-core-jpa clean -fdx
fi
'''
}
}
stage('Verify toolchain') {
steps {
sh '''
set -eu
echo "--- build JDK ---"
java -version
gradle --version
echo "--- scanner/dependency-check JDK ---"
if [ ! -x "$JAVA_HOME_SCANNER/bin/java" ]; then
echo "JDK 17 이 없다: $JAVA_HOME_SCANNER"
echo "SonarScanner 는 JRE 17, Dependency-Check 는 JRE 11 이상을 요구한다."
exit 1
fi
"$JAVA_HOME_SCANNER/bin/java" -version
'''
}
}
// build/generated 잔여 산출물이 남으면 MapStruct Impl / QueryDSL Q클래스가
// 중복 생성되어 컴파일이 깨진다. clean 이 파일 잠금 등으로 실패하는 경우가 있어
// 생성 소스 디렉터리는 별도로 먼저 지운다.
stage('Compile') {
steps {
sh '''
set -eu
rm -rf build/generated build/classes
gradle clean classes testClasses --no-daemon
'''
}
}
stage('Test') {
when { expression { return params.RUN_TESTS } }
steps {
sh 'gradle test --no-daemon'
}
post {
always {
junit allowEmptyResults: true, testResults: 'build/test-results/test/*.xml'
}
}
}
// 배포본에 실리는 것과 같은 목록(runtimeClasspath)만 모은다.
// 이게 없으면 Dependency-Check 가 스캔할 jar 가 없어 "취약점 0건" 이라는 거짓 안심을 준다.
stage('Export dependency jars') {
when { expression { return params.RUN_DEPENDENCY_CHECK } }
steps {
sh 'gradle -I ci/dependency-check-classpath.gradle exportDependencyJars --no-daemon'
sh '''
set -eu
COUNT=$(find build/dependency-check/libs -name '*.jar' -type f | wc -l | tr -d ' ')
echo "scan target jars: $COUNT"
if [ "$COUNT" -eq 0 ]; then
echo "수집된 jar 가 0개다. 의존성 해석이 실패했는지 확인할 것."
exit 1
fi
'''
}
}
stage('OWASP Dependency-Check') {
when { expression { return params.RUN_DEPENDENCY_CHECK } }
steps {
script {
// CLI 위치 결정: DC_HOME > Jenkins Tools 등록분 > PATH
def dcHome = null
if (env.DC_HOME?.trim() && fileExists("${env.DC_HOME}/bin/dependency-check.sh")) {
dcHome = env.DC_HOME.trim()
echo "Dependency-Check: ${dcHome} (DC_HOME)"
} else {
def candidates = env.DC_TOOL_NAME?.trim()
? [env.DC_TOOL_NAME.trim()]
: ['dependency-check', 'Dependency-Check', 'OWASP Dependency-Check', 'dependency-check-cli']
for (name in candidates) {
try {
dcHome = tool name: name,
type: 'org.jenkinsci.plugins.DependencyCheck.tools.DependencyCheckInstallation'
echo "Dependency-Check tool: '${name}' -> ${dcHome}"
break
} catch (ignored) {
// 등록되지 않은 이름은 건너뛴다
}
}
if (dcHome == null) {
echo 'Jenkins Tools 에 등록된 Dependency-Check 가 없다. PATH 의 dependency-check.sh 로 진행한다.'
}
}
// NVD API Key credential 이 있는지 먼저 확인한다.
// (없는 credential 로 withCredentials 를 감싸면 파이프라인 자체가 죽는다)
def hasNvdKey = true
try {
withCredentials([string(credentialsId: env.NVD_API_KEY_CRED, variable: 'NVD_PROBE')]) {
// 존재 확인만 한다
}
} catch (ignored) {
hasNvdKey = false
}
if (!hasNvdKey) {
echo "NVD API Key credential('${env.NVD_API_KEY_CRED}') 이 없다. 키 없이 진행한다."
echo 'NVD 갱신이 매우 느려진다(수 시간). https://nvd.nist.gov/developers/request-an-api-key 에서 발급 권장.'
}
def dcScript = '''
set -eu
# Dependency-Check 는 JRE 11 이상 필요 (빌드용 JDK 8 과 분리)
export JAVA_HOME="$JAVA_HOME_SCANNER"
export PATH="$JAVA_HOME/bin:$PATH"
mkdir -p "$DC_DATA" "$DC_REPORT_DIR"
if [ -n "${DC_RESOLVED_HOME:-}" ] && [ -x "$DC_RESOLVED_HOME/bin/dependency-check.sh" ]; then
DC="$DC_RESOLVED_HOME/bin/dependency-check.sh"
elif command -v dependency-check.sh >/dev/null 2>&1; then
DC="$(command -v dependency-check.sh)"
else
echo "dependency-check.sh 를 찾지 못했다."
echo "노드에 CLI 를 설치하고 DC_HOME 을 맞추거나, Manage Jenkins > Tools 에 등록할 것."
exit 1
fi
echo "dependency-check: $DC"
ARGS=""
# 폐쇄망/오프라인: 캐시된 NVD DB 로만 검사한다.
if [ "${UPDATE_NVD_FLAG}" != "true" ]; then
ARGS="$ARGS --noupdate"
if [ ! -d "$DC_DATA" ] || [ -z "$(ls -A "$DC_DATA" 2>/dev/null)" ]; then
echo "NVD 캐시가 비어 있는데 갱신이 꺼져 있다: $DC_DATA"
echo "인터넷 되는 곳에서 1회 적재한 data 디렉터리를 복사해 둘 것."
exit 1
fi
fi
if [ -n "${NVD_API_KEY:-}" ]; then
ARGS="$ARGS --nvdApiKey ${NVD_API_KEY}"
fi
# OSS Index 분석기는 외부 서비스(ossindex.sonatype.org)를 호출한다.
# 갱신을 끈 환경(=외부 통신 불가)에서는 같이 끈다.
if [ "${UPDATE_NVD_FLAG}" != "true" ]; then
ARGS="$ARGS --disableOssIndex"
fi
# 자바 프로젝트에 불필요한 분석기 — 실행 시간만 늘린다.
ARGS="$ARGS --disableAssembly --disableNodeAudit --disableNodeJS"
SCAN_ARGS="--scan build/dependency-check/libs"
if [ "${SCAN_JS_FLAG}" = "true" ]; then
# 번들된 JS 라이브러리(jQuery, summernote 등)의 알려진 취약점 — RetireJS
[ -d src/main/resources/static/js ] && SCAN_ARGS="$SCAN_ARGS --scan src/main/resources/static/js"
[ -d src/main/resources/static/plugins ] && SCAN_ARGS="$SCAN_ARGS --scan src/main/resources/static/plugins"
else
ARGS="$ARGS --disableRetireJS"
fi
SUPPRESSION=""
if [ -f ci/dependency-check-suppressions.xml ]; then
SUPPRESSION="--suppression ci/dependency-check-suppressions.xml"
fi
# 토큰/키가 콘솔에 남지 않도록 실행 명령 자체는 출력하지 않는다.
set +x
"$DC" \
--project "DJB eAPIM Portal" \
$SCAN_ARGS \
--out "$DC_REPORT_DIR" \
--format HTML --format XML --format JSON \
--data "$DC_DATA" \
--failOnCVSS "${FAIL_ON_CVSS_VALUE}" \
$SUPPRESSION \
$ARGS
'''
def dcEnv = [
"DC_RESOLVED_HOME=${dcHome ?: ''}",
"UPDATE_NVD_FLAG=${params.UPDATE_NVD}",
"SCAN_JS_FLAG=${params.SCAN_JS}",
"FAIL_ON_CVSS_VALUE=${params.FAIL_ON_CVSS}"
]
// 정책: 기본은 UNSTABLE 로만 표시하고 뒤의 Sonar 분석까지 마친다.
// FAIL_BUILD_ON_FINDING 을 켜면 임계치 위반이 빌드 실패가 된다.
def onFinding = params.FAIL_BUILD_ON_FINDING ? 'FAILURE' : 'UNSTABLE'
catchError(buildResult: onFinding, stageResult: 'FAILURE') {
withEnv(dcEnv) {
if (hasNvdKey) {
withCredentials([string(credentialsId: env.NVD_API_KEY_CRED, variable: 'NVD_API_KEY')]) {
sh dcScript
}
} else {
sh dcScript
}
}
}
}
}
post {
always {
archiveArtifacts allowEmptyArchive: true,
artifacts: 'build/reports/dependency-check/dependency-check-report.*'
script {
// OWASP Dependency-Check 플러그인이 설치돼 있으면 추이 그래프/경고를 남긴다.
// 없어도 리포트는 위에서 아카이브되므로 실패로 보지 않는다.
try {
dependencyCheckPublisher pattern: 'build/reports/dependency-check/dependency-check-report.xml'
} catch (ignored) {
echo 'OWASP Dependency-Check 플러그인 미설치 — 아카이브된 HTML 리포트로 확인할 것.'
}
}
}
}
}
// sonar.java.libraries 용 클래스패스 덤프.
// 없으면 타입 해석이 안 돼 보안 룰 다수가 침묵하므로 실패 시 UNSTABLE 로만 넘기고 분석은 계속한다.
stage('Export analysis classpath') {
when { expression { return params.RUN_SONAR } }
steps {
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
sh 'gradle -I ci/sonar-classpath.gradle exportSonarClasspath --no-daemon'
}
}
}
stage('SonarQube analysis') {
when { expression { return params.RUN_SONAR } }
steps {
script {
// 스캐너 위치 결정: 지정 이름 > 흔한 등록 이름 후보 > PATH 의 sonar-scanner
def candidates = env.SONAR_SCANNER_TOOL?.trim()
? [env.SONAR_SCANNER_TOOL.trim()]
: ['SonarScanner', 'SonarQube Scanner', 'sonar-scanner', 'SonarScanner CLI', 'sonarqube-scanner']
def scannerHome = null
for (name in candidates) {
try {
scannerHome = tool name: name, type: 'hudson.plugins.sonar.SonarRunnerInstallation'
echo "SonarScanner tool: '${name}' -> ${scannerHome}"
break
} catch (ignored) {
// 등록되지 않은 이름은 건너뛴다
}
}
if (scannerHome == null) {
echo 'Jenkins Tools 에 등록된 SonarQube Scanner 를 찾지 못했다. PATH 의 sonar-scanner 로 진행한다.'
echo '(Manage Jenkins > Tools 에 등록한 뒤 environment 의 SONAR_SCANNER_TOOL 에 그 이름을 넣으면 확실하다.)'
}
withSonarQubeEnv(env.SONAR_ENV_NAME) {
withEnv(["SONAR_SCANNER_HOME=${scannerHome ?: ''}"]) {
sh '''
set -eu
# 스캐너는 JDK 17 로 실행한다 (빌드용 JDK 8 과 분리)
export JAVA_HOME="$JAVA_HOME_SCANNER"
export PATH="$JAVA_HOME/bin:$PATH"
# 토큰은 커맨드라인(-Dsonar.token)에 노출시키지 않고 환경변수로만 넘긴다.
if [ -z "${SONAR_TOKEN:-}" ] && [ -n "${SONAR_AUTH_TOKEN:-}" ]; then
export SONAR_TOKEN="$SONAR_AUTH_TOKEN"
fi
LIBS=""
if [ -f build/sonar/java-libraries.txt ]; then
LIBS=$(cat build/sonar/java-libraries.txt)
else
echo "WARN: build/sonar/java-libraries.txt 없음 - 타입 해석 정확도 저하"
fi
TEST_LIBS=""
if [ -f build/sonar/java-test-libraries.txt ]; then
TEST_LIBS=$(cat build/sonar/java-test-libraries.txt)
fi
JUNIT_ARG=""
if [ -d build/test-results/test ]; then
JUNIT_ARG="-Dsonar.junit.reportPaths=build/test-results/test"
fi
# SonarQube 에 Dependency-Check 플러그인이 설치돼 있으면 CVE 결과도 함께 올린다.
# 플러그인이 없으면 스캐너가 모르는 속성으로 무시한다(경고만).
DC_ARG=""
if [ -f "$DC_REPORT_DIR/dependency-check-report.json" ]; then
DC_ARG="-Dsonar.dependencyCheck.jsonReportPath=$DC_REPORT_DIR/dependency-check-report.json"
if [ -f "$DC_REPORT_DIR/dependency-check-report.html" ]; then
DC_ARG="$DC_ARG -Dsonar.dependencyCheck.htmlReportPath=$DC_REPORT_DIR/dependency-check-report.html"
fi
fi
if [ -n "${SONAR_SCANNER_HOME:-}" ] && [ -x "$SONAR_SCANNER_HOME/bin/sonar-scanner" ]; then
SCANNER="$SONAR_SCANNER_HOME/bin/sonar-scanner"
elif command -v sonar-scanner >/dev/null 2>&1; then
SCANNER="$(command -v sonar-scanner)"
else
echo "sonar-scanner 실행 파일을 찾지 못했다."
echo "Manage Jenkins > Tools > SonarQube Scanner 에 등록하거나 노드 PATH 에 설치할 것."
exit 1
fi
echo "scanner: $SCANNER"
"$SCANNER" \
-Dsonar.projectVersion="${BUILD_NUMBER}" \
-Dsonar.java.libraries="$LIBS" \
-Dsonar.java.test.libraries="$TEST_LIBS" \
$JUNIT_ARG \
$DC_ARG
'''
}
}
}
}
}
// SonarQube 웹훅으로 게이트 결과를 받아 판정한다.
// 정책: 게이트 실패는 UNSTABLE 로만 표시하고 빌드는 실패시키지 않는다.
stage('Quality Gate') {
when { expression { return params.RUN_SONAR } }
steps {
script {
def qg = null
try {
timeout(time: 15, unit: 'MINUTES') {
qg = waitForQualityGate abortPipeline: false
}
} catch (err) {
unstable("Quality Gate 결과 대기 실패/타임아웃: ${err}")
return
}
if (qg == null) {
unstable('Quality Gate 결과를 받지 못했다. SonarQube 웹훅 설정을 확인할 것.')
} else if (qg.status != 'OK') {
unstable("Quality Gate ${qg.status}")
} else {
echo 'Quality Gate OK'
}
}
}
}
}
post {
always {
archiveArtifacts allowEmptyArchive: true, artifacts: '.scannerwork/report-task.txt'
script {
def report = '.scannerwork/report-task.txt'
if (fileExists(report)) {
def url = readFile(report).readLines().find { it.startsWith('dashboardUrl=') }
if (url) {
echo "SonarQube: ${url.substring('dashboardUrl='.length())}"
}
}
if (fileExists('build/reports/dependency-check/dependency-check-report.html')) {
echo "Dependency-Check 리포트: ${env.BUILD_URL}artifact/build/reports/dependency-check/dependency-check-report.html"
}
}
}
}
}
+67 -12
View File
@@ -60,7 +60,6 @@ dependencies {
// implementation project(':kjb-safedb')
implementation('org.springframework.boot:spring-boot-starter')
implementation 'org.springframework.boot:spring-boot-starter-jta-atomikos'
implementation('org.springframework.boot:spring-boot-starter-web')
implementation('org.springframework.boot:spring-boot-starter-validation')
@@ -84,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'
@@ -92,33 +94,40 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
implementation 'org.springframework:spring-expression:5.3.30'
// spring-expression 개별 pin 제거: 아래 ext 의 spring-framework.version 이 전 모듈을 일괄 관리한다.
// 개별 pin 이 남아 있으면 다른 spring-* 모듈보다 낮은 버전으로 고정되어 버전이 어긋난다.
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'
implementation 'org.jasypt:jasypt:1.9.3'
implementation 'xerces:xercesImpl:2.12.2'
implementation 'org.apache.commons:commons-lang3:3.12.0'
// Uncontrolled recursion in ClassUtils.getClass(...) on very long inputs. 3.18.0+ 에서 수정.
implementation 'org.apache.commons:commons-lang3:3.20.0'
implementation 'org.apache.commons:commons-collections4:4.4'
implementation 'commons-net:commons-net:3.9.0'
implementation('commons-beanutils:commons-beanutils:1.9.4') {
// CVE-2025-48734 (PropertyUtilsBean 이 enum 의 declaredClass 프로퍼티 노출 → ClassLoader 접근/RCE).
// 1.11.0 부터 SuppressPropertiesBeanIntrospector 가 기본 활성이라 declaredClass 접근이 차단된다.
// 이 앱의 호출부(PasswordMatchValidator / PasswordRuleValidator / AuthNumberValidator)는
// 어노테이션에 박힌 고정 프로퍼티명만 넘기므로 외부 입력 경로는 없지만 버전은 올려 둔다.
// 1.11.0 = Java 8 바이트코드(major 52), PropertyUtils.getProperty/getNestedProperty API 동일.
implementation('commons-beanutils:commons-beanutils:1.11.0') {
// exclude group: 'commons-collections', module: 'commons-collections'
}
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
// WS-2026-0003 (jackson-core async parser DoS, CVSS 7.5) — 2.18.6 에서 수정. JDK8 호환.
implementation 'com.fasterxml.jackson.core:jackson-core:2.18.6'
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.18.6'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.6'
// jackson 개별 pin 제거: 아래 ext 의 jackson-bom.version 이 전 모듈을 일괄 관리한다.
// 개별 pin 은 BOM 보다 우선하므로 남겨 두면 BOM 만 올렸을 때 core/annotations/databind 가
// 옛 버전에 고정돼 버전이 어긋난다(실제로 그런 상태였다).
implementation group: 'org.apache.velocity', name: 'velocity-engine-core', version: '2.3'
@@ -128,7 +137,10 @@ dependencies {
implementation 'net.bytebuddy:byte-buddy:1.14.5'
// Commons FileUpload (WAS 독립적인 multipart 처리)
implementation 'commons-fileupload:commons-fileupload:1.5'
// CVE-2025-48976 (멀티파트 파트 헤더 크기 제한 부재 → DoS). 1.6.0 에서 partHeaderSizeMax 도입.
// 주의: 1.6 부터 파트 헤더 총량 기본 상한이 10240 → 512 바이트로 줄었다(DEFAULT_PART_HEADER_SIZE_MAX).
// 한글 파일명은 UTF-8 로 3바이트/자라 Content-Disposition 이 길어질 수 있어 실측으로 여유를 확인했다.
implementation 'commons-fileupload:commons-fileupload:1.6.0'
implementation 'commons-io:commons-io:2.15.1'
@@ -146,6 +158,49 @@ 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')
// jackson 전 모듈 버전 통일(Boot 2.7.18 BOM 기본 2.13.5). 2.18.x 는 JDK8 호환 라인이다.
// 이유 3가지
// 1) snakeyaml 2.x 는 ParserImpl(StreamReader) 를 제거했고 jackson-dataformat-yaml 은
// 2.15+ 부터 ParserImpl(StreamReader, LoaderOptions) 를 쓴다 — 위 snakeyaml 상향의 전제.
// 2) WS-2026-0003 (jackson-core async parser DoS, CVSS 7.5) — 2.18.6 에서 수정.
// 3) jackson-databind PolymorphicTypeValidator 우회(제네릭 타입 인자 미검증) — 2.18.8 에서 수정.
// 이 앱은 다형성 역직렬화(activateDefaultTyping/@JsonTypeInfo)를 쓰지 않아 노출 경로는 없다.
// 2.18.x 마지막 패치를 쓴다.
set('jackson-bom.version', '2.18.10')
// 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')
// Spring Framework 5.3.x OSS 마지막 릴리스로 통일(Boot 2.7.18 BOM 기본 5.3.31, 일부 5.3.30 혼재였음).
// 5.3.32~5.3.39 구간 CVE 정리용. 5.3.x 는 OSS EOL 이라 이 위로는 상용(Enterprise) 빌드뿐이다.
// 남는 5.3.x 지적(CVE-2026-41855 JMS Jackson 역직렬화, CVE-2016-1000027 HttpInvoker)은
// 5.3 계열에 수정본이 없고 앱이 JMS/HttpInvoker 를 쓰지 않으므로 억제 항목으로 따로 관리한다.
set('spring-framework.version', '5.3.39')
// Spring Security 5.7.x OSS 마지막 릴리스(Boot 2.7.18 BOM 기본 5.7.11).
// CVE-2026-22732(응답 커밋 후 보안 헤더 미기록)는 수정본이 5.7.22/5.8.24 = Enterprise 전용이라
// OSS 로는 올릴 수 없다. 우회책(HeaderWriterFilter.shouldWriteHeadersEagerly=true)은 동작 변경이라 분리 검토.
set('spring-security.version', '5.7.14')
}
+58
View File
@@ -0,0 +1,58 @@
// OWASP Dependency-Check 가 스캔할 서드파티 jar 를 한 디렉터리로 모은다.
//
// build.gradle 을 건드리지 않기 위해 init script(-I) 로만 주입한다.
// gradle -I ci/dependency-check-classpath.gradle exportDependencyJars --no-daemon
//
// 출력:
// build/dependency-check/libs/*.jar runtimeClasspath 의 외부 의존 jar (원본 파일명 유지)
// build/dependency-check/jars.txt 수집 목록(경로 1줄씩) — 진단용
//
// 왜 디렉터리로 모으는가
// Dependency-Check CLI 는 --scan <경로> 로 파일/디렉터리를 받는다. Gradle 캐시를 통째로 스캔하면
// 이 프로젝트가 쓰지 않는 버전까지 잡히고, 프로젝트 디렉터리만 스캔하면 의존 jar 가 아예 안 잡힌다.
// 실제 배포본에 실리는 것과 같은 목록(runtimeClasspath)만 모아야 결과가 배포 산출물과 일치한다.
//
// 파일명을 유지해야 하는 이유
// Dependency-Check 는 jar 안의 POM/MANIFEST 외에 파일명에서도 CPE(제품/버전)를 추론한다.
// 이름을 바꾸면 탐지 정확도가 떨어진다.
rootProject { project ->
project.plugins.withId('java') {
project.tasks.register('exportDependencyJars') {
group = 'verification'
description = 'Dependency-Check 스캔용 런타임 의존 jar 를 build/dependency-check/libs 에 모은다'
doLast {
def outRoot = new File(project.layout.buildDirectory.get().asFile, 'dependency-check')
def outDir = new File(outRoot, 'libs')
project.delete(outDir)
outDir.mkdirs()
// 컴포지트/서브프로젝트의 build 디렉터리 산출물(우리가 만든 jar)은 제외한다.
// 자체 코드는 SonarQube 가 보는 영역이고, SCA 대상은 외부 라이브러리다.
def buildDirs = project.allprojects.collect {
it.layout.buildDirectory.get().asFile.absolutePath
}
def isOwnArtifact = { File f ->
buildDirs.any { f.absolutePath.startsWith(it + File.separator) }
}
def jars = project.configurations.runtimeClasspath.files
.findAll { it.isFile() && it.name.endsWith('.jar') && !isOwnArtifact(it) }
.unique()
.sort { it.name }
project.copy {
from jars
into outDir
}
new File(outRoot, 'jars.txt').text =
jars.collect { it.absolutePath }.join(System.lineSeparator()) + System.lineSeparator()
def totalMb = (jars.sum { it.length() } ?: 0L) / (1024 * 1024)
logger.lifecycle("dependency-check scan target: ${jars.size()} jars, ${totalMb as int} MB -> ${outDir}")
}
}
}
}
+121
View File
@@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
OWASP Dependency-Check 오탐(false positive) 억제 목록.
규칙
1. 억제는 "오탐"에만 쓴다. 진짜 취약점을 조용히 숨기는 용도로 쓰지 않는다.
실제 취약하지만 당장 못 올리는 경우는 억제 대신 만료일(until)을 넣어 재검토를 강제한다.
2. 항목마다 <notes> 에 판단 근거와 판단자/일자를 남긴다. 근거 없는 억제는 리뷰에서 거절한다.
3. 범위를 좁게 잡는다. cve 단건 + 특정 파일(sha1/packageUrl)이 기본이고,
cpe 나 정규식 filePath 로 넓게 억제하지 않는다.
작성법
Dependency-Check HTML 리포트의 각 취약점 옆 "Suppress" 버튼을 누르면 해당 항목의
<suppress> 블록이 그대로 생성된다. 그것을 이 파일에 붙여넣고 <notes> 만 채우면 된다.
참고: https://dependency-check.github.io/DependencyCheck/general/suppression.html
-->
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
<!-- 예시 (실제 억제 시 주석을 풀고 값 교체)
<suppress until="2026-12-31Z">
<notes><![CDATA[
오탐 근거: 해당 CVE 는 X 기능을 사용할 때만 성립하는데 이 앱은 해당 API 를 호출하지 않음.
확인: 홍길동 / 2026-08-13 / 호출부 grep 결과 0건.
]]></notes>
<packageUrl regex="true">^pkg:maven/org\.example/example-lib@.*$</packageUrl>
<cve>CVE-2026-00000</cve>
</suppress>
-->
<!-- ================================================================================
Spring 계열 Critical 5건 (2026-08-18 리포트 기준).
공통 배경: Spring Framework 5.3.x / Spring Boot 2.7.x / Spring Security 5.7.x 는 모두 OSS EOL 이라
남은 수정본이 상용(Enterprise/Tanzu)뿐이다. JDK 8 유지 제약상 Boot 3 + Spring 6 이관 전까지는
버전 상향으로 못 없앤다. 아래는 "코드에 트리거 경로가 없음"을 근거로 한 억제이며 만료일을 둔다.
버전 범위를 5.3.x / 2.7.x / 5.7.x 로 고정해 두었으므로, 이관 후에는 억제가 자동으로 풀린다.
재검토 시 확인할 것: (1) 각 근거 grep 이 여전히 0건인지 (2) OSS 수정본이 나왔는지.
================================================================================ -->
<!-- 1. Spring Boot: Cassandra SSL 호스트명 미검증 -->
<suppress until="2027-02-28Z">
<notes><![CDATA[
CVE-2026-40974 는 Spring Boot 의 Cassandra SSL 자동설정이 SSL 번들의 호스트명 검증 설정을
드라이버에 전달하지 않는 문제다. 이 앱은 Cassandra 를 쓰지 않는다
(runtimeClasspath 에 cassandra/datastax 계열 jar 0건, spring-boot-starter-data-cassandra 미선언).
수정본 2.7.33 은 상용(Enterprise) 릴리스라 OSS 로는 올릴 수 없다.
devtools/actuator/spring-boot-admin jar 는 스캔 대상(runtimeClasspath)에는 있으나
배포 산출물에서는 build.gradle 의 localOnlyLibPrefixes 로 제외되어 WAR 에 실리지 않는다.
확인: Rinjae / 2026-08-18 / grep -ri cassandra 0건, WAR 내 devtools/actuator jar 0건.
]]></notes>
<packageUrl regex="true">^pkg:maven/org\.springframework\.boot/.*@2\.7\..*$</packageUrl>
<cve>CVE-2026-40974</cve>
</suppress>
<!-- 2. Thymeleaf SSTI 2건: starter jar 이름 기준 오탐 -->
<suppress>
<notes><![CDATA[
오탐. CVE-2026-40477 / CVE-2026-40478 은 thymeleaf 본체 3.1.3.RELEASE 이하의 표현식 샌드박스
우회 문제이고 3.1.4.RELEASE 에서 수정됐다. 이 프로젝트는 build.gradle 의 ext 에서
thymeleaf.version=3.1.4.RELEASE / thymeleaf-extras-springsecurity.version=3.1.5.RELEASE 로
올려 두었으므로 실제 실리는 jar 는 thymeleaf-3.1.4.RELEASE.jar 다.
Dependency-Check 가 spring-boot-starter-thymeleaf-2.7.18.jar(의존만 선언한 빈 starter)에
thymeleaf CPE 를 잘못 매칭한 결과다. thymeleaf 본체 jar 에 대한 탐지는 억제하지 않는다.
확인: Rinjae / 2026-08-18 / WAR 내 thymeleaf-3.1.4.RELEASE.jar, thymeleaf-spring5-3.1.4.RELEASE.jar.
]]></notes>
<packageUrl regex="true">^pkg:maven/org\.springframework\.boot/spring-boot-starter-thymeleaf@.*$</packageUrl>
<cve>CVE-2026-40477</cve>
<cve>CVE-2026-40478</cve>
</suppress>
<!-- 3. Spring Framework: JMS Jackson 역직렬화 -->
<suppress until="2027-02-28Z">
<notes><![CDATA[
CVE-2026-41855 는 org.springframework.jms.support.converter.MappingJackson2MessageConverter /
JacksonJsonMessageConverter 가 임의 클래스 인스턴스화를 허용하는 문제다(신뢰할 수 없는 JMS 환경 전제).
이 앱은 JMS 를 쓰지 않는다: runtimeClasspath 에 spring-jms 0건, 소스에 javax.jms /
JmsTemplate / MappingJackson2MessageConverter 참조 0건.
5.3.x 는 OSS 수정본이 없다(6.2.19 / 7.0.8 에서만 수정). 이미 5.3.x OSS 마지막인 5.3.39 로 올려 둔 상태다.
확인: Rinjae / 2026-08-18 / grep -rn "javax.jms|JmsTemplate|MappingJackson2MessageConverter" src 0건.
]]></notes>
<packageUrl regex="true">^pkg:maven/org\.springframework/spring-.*@5\.3\..*$</packageUrl>
<cve>CVE-2026-41855</cve>
</suppress>
<!-- 4. spring-web: HttpInvoker 역직렬화 -->
<suppress until="2027-02-28Z">
<notes><![CDATA[
CVE-2016-1000027 은 HttpInvokerServiceExporter 를 노출했을 때만 성립한다. 해당 클래스는
Spring 6.0 에서 제거됐고 5.3.x 에는 수정본이 없다(= 5.3.x 를 쓰는 한 계속 탐지된다).
이 앱은 HttpInvoker 계열을 쓰지 않는다: 소스에 HttpInvoker 참조 0건이고 외부 호출은
RestTemplate / HttpClient 기반이다.
근본 해결은 Spring 6(Boot 3, JDK 17) 이관. 이관 전까지 억제한다.
확인: Rinjae / 2026-08-18 / grep -rn HttpInvoker src 0건.
]]></notes>
<packageUrl regex="true">^pkg:maven/org\.springframework/spring-web@5\.3\..*$</packageUrl>
<cve>CVE-2016-1000027</cve>
</suppress>
<!-- 5. Spring Security: 응답 커밋 시 보안 헤더 미기록 -->
<suppress until="2027-02-28Z">
<notes><![CDATA[
CVE-2026-22732 는 응답이 커밋된 뒤 Spring Security 가 보안 헤더를 기록하지 못하는 문제다.
트리거는 Content-Length 를 setHeader / setIntHeader / addIntHeader 로 지정하는 경로다
(OnCommittedResponseWrapper 는 setContentLength / setContentLengthLong / addHeader 만 추적한다).
(1) 이 코드베이스에는 트리거가 없다: portal / elink-portal-common / elink-online-core-jpa 전체에
setIntHeader, addIntHeader, setHeader("Content-Length") 0건. 파일 다운로드는
response.setContentLength(int) 를 쓰며 실측상 보안 헤더가 정상 기록된다.
(2) 그럼에도 안전망으로 PortalConfigSecurity 에서
HeaderWriterFilter.setShouldWriteHeadersEagerly(true) 를 적용해(spring.io 권고 우회책)
요청 시작 시점에 헤더를 기록하도록 했다.
수정본 5.7.22 / 5.8.24 는 Enterprise 전용이라 OSS 로는 올릴 수 없다. 이미 5.7.x OSS 마지막인
5.7.14 로 올려 둔 상태다.
확인: Rinjae / 2026-08-18 / 동일 스택(Boot 2.7.18 + Security 5.7.14 + Tomcat 9.0.120) 프로브 앱 실측 —
우회책 미적용 시 setHeader/setIntHeader/addIntHeader 경로에서 헤더 누락 재현, 적용 후 정상 기록.
]]></notes>
<packageUrl regex="true">^pkg:maven/org\.springframework\.security/spring-security-.*@5\.7\..*$</packageUrl>
<cve>CVE-2026-22732</cve>
</suppress>
</suppressions>
+8
View File
@@ -51,6 +51,14 @@ sonar.exclusions=\
# 자동 생성 코드(MapStruct Impl, QueryDSL Q클래스)는 build/ 아래라 이미 제외된다.
# Thymeleaf 인라인 표현식([[${...}]], /*[[${...}]]*/)은 JS 파서가 읽지 못해
# "Failed to parse file ... Unexpected token" 으로 분석이 중단된다.
# JS 분석에서만 빼고 HTML(web) 센서는 그대로 두어 템플릿 XSS 룰은 유지한다.
# 기본값이 **/node_modules/** 이므로 재정의 시 함께 명시해야 한다.
sonar.javascript.exclusions=\
**/node_modules/**,\
src/main/resources/templates/**
# --- SCM -------------------------------------------------------------------
# blame 기반 "새 코드" 판정을 위해 Jenkins Job 에서 shallow clone 을 쓰지 않는다.
sonar.scm.provider=git
@@ -22,8 +22,19 @@ import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* API 그룹(전시) 조회 서비스.
*
* <p>주 조회 대상 {@link ApiGroup} 은 <b>AGW(게이트웨이) 데이터소스</b> 엔티티라
* 트랜잭션 매니저를 {@code gatewayTransactionManager} 로 지정한다. 기본
* {@code transactionManager}(EMS) 를 쓰면 게이트웨이 EntityManager 가 리포지토리 호출 단위로
* 닫혀 {@code ApiGroup.apiGroupApiList} 지연 로딩에서 LazyInitializationException 이 난다.</p>
*
* <p>내부에서 호출하는 {@code ApiSpecInfoService} 는 EMS 쪽이며 자체 {@code @Transactional}
* (기본 매니저)로 독립 트랜잭션을 연다. {@code ApiSpecInfo} 에는 지연 연관이 없어 문제되지 않는다.</p>
*/
@Service("apiServiceService")
@Transactional
@Transactional("gatewayTransactionManager")
@RequiredArgsConstructor
@Slf4j
public class ApiServiceService {
@@ -36,6 +36,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.springframework.stereotype.Service;
@Service
@@ -155,10 +156,10 @@ public class ApiSpecService {
updateReferences(pathItem, apiId);
String tmpFullPath = StringUtils.isNotEmpty(basePath)
? StringUtils.join(basePath, "/", StringUtils.removeStart(path, "/"))
? StringUtils.join(basePath, "/", Strings.CS.removeStart(path, "/"))
: path;
final String fullPath = StringUtils.replacePattern(tmpFullPath, "//+", "/");
final String fullPath = tmpFullPath.replaceAll("//+", "/");
// HTTP 메소드별로 중복 체크
pathItem.readOperationsMap().forEach((httpMethod, operation) -> {
@@ -294,7 +295,7 @@ public class ApiSpecService {
return StringUtils.EMPTY;
}
Server server = api.getServers().get(0);
return StringUtils.removeEnd(server.getUrl(), "/");
return Strings.CS.removeEnd(server.getUrl(), "/");
}
private void mergeComponents(OpenAPI currentAPI, String apiId, String apiName, Map<String, Object> mergedComponents, ObjectMapper objectMapper) {
@@ -37,7 +37,7 @@ public class AdminGatewayClient {
String baseUrl = portalPropertyService.getOrCreateProperty(
PROP_GROUP, PROP_ADMIN_BASE_URL, DEFAULT_ADMIN_BASE_URL, "admin(관리자포털) 내부 API base URL");
String url = baseUrl.replaceAll("/+$", "") + CLIENT_BLOCK_PATH;
String url = stripTrailingSlashes(baseUrl) + CLIENT_BLOCK_PATH;
// 네트워크/HTTP 오류는 RestTemplate 이 예외로 던진다.
ResponseEntity<Map> response = restTemplate.postForEntity(url, null, Map.class, clientId);
@@ -51,4 +51,18 @@ public class AdminGatewayClient {
log.info("admin GW 차단/리로드 위임 성공 - clientId={}", clientId);
}
/**
* base URL 끝의 {@code '/'} 를 모두 걷어낸다.
*
* <p>{@code replaceAll("/+$", "")} 은 백트래킹으로 super-linear 가 될 수 있어(Sonar S5852)
* 선형 스캔으로 대체했다. 동작은 동일하다.</p>
*/
private static String stripTrailingSlashes(String url) {
int end = url.length();
while (end > 0 && url.charAt(end - 1) == '/') {
end--;
}
return url.substring(0, end);
}
}
@@ -2,8 +2,6 @@ package com.eactive.apim.portal.apps.auth;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import lombok.RequiredArgsConstructor;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.stereotype.Component;
/**
@@ -12,10 +10,18 @@ import org.springframework.stereotype.Component;
* <p>그룹 {@code Portal}, 키 {@code auth.test-notice.enabled}(true/false). 값이 참이면 인증 요청
* 응답에 인증번호를 실어 화면에 노출한다(실제 발송 대신 테스트 확인 용도). 기존 application.yml
* {@code portal.test-auth-notice-enabled} 설정을 DB PTL_PROPERTY 로 이전한 것으로,
* {@link TwoFactorProperties} 와 동일한 {@code getOrCreateProperty} 패턴을 따른다.</p>
* {@link com.eactive.apim.portal.apps.auth.twofactor.TwoFactorProperties} 와 동일한
* {@code getOrCreateProperty} 패턴을 따른다.</p>
*
* <p><b>prod 프로파일에서 DB 값과 무관하게 항상 false</b> 를 반환한다(운영 환경 인증번호 노출 금지).
* 세션 keepalive 등 다른 비운영 전용 스위치와 동일한 정책이다.</p>
* <p><b>운영(prod)에서 DB 값만으로 켤 수 있다.</b> 대신 종료일
* {@code auth.test-notice.prod-until}({@code yyyy-MM-dd})을 함께 지정해야 하며, 날짜가 지나면
* {@link TestNoticeWindow} 가 자동으로 닫는다. 종료일 미설정/형식 오류면 운영에서는 열리지 않는다.
* 운영 오픈 전 UMS 실발송 미연계 기간을 위한 한시 스위치다.</p>
*
* <p>이 판정은 {@link com.eactive.apim.portal.common.breadcrumb.GlobalControllerAdvice} 의
* {@code @ModelAttribute} 에서 <b>모든 페이지 요청마다</b> 호출된다. PortalPropertyService 에 캐시가
* 없어 호출마다 DB 조회가 발생하므로 {@value #CACHE_TTL_MILLIS}ms 짧은 캐시를 둔다.
* PTL_PROPERTY 를 바꾸면 최대 그 시간만큼 반영이 늦다.</p>
*/
@Component
@RequiredArgsConstructor
@@ -23,21 +29,45 @@ public class AuthNoticeProperties {
public static final String GROUP = "Portal";
public static final String KEY_TEST_NOTICE_ENABLED = "auth.test-notice.enabled";
public static final String KEY_TEST_NOTICE_PROD_UNTIL = "auth.test-notice.prod-until";
/** 판정 결과 캐시 유효시간(ms) */
static final long CACHE_TTL_MILLIS = 30_000L;
private final PortalPropertyService portalPropertyService;
private final Environment environment;
private final TestNoticeWindow testNoticeWindow;
private volatile boolean cachedEnabled;
/** 캐시 갱신 시각(ms). 0 이면 미조회 */
private volatile long cachedAt;
/**
* 인증 요청 응답에 인증번호를 실어 UI 에 노출할지 여부(개발/테스트 전용).
* prod 환경에서는 property 값과 무관하게 항상 false.
* 운영에서는 종료일({@link #KEY_TEST_NOTICE_PROD_UNTIL})까지만 참이 된다.
*/
public boolean isTestNoticeEnabled() {
if (environment.acceptsProfiles(Profiles.of("prod"))) {
long now = System.currentTimeMillis();
long at = cachedAt;
if (at != 0L && now - at < CACHE_TTL_MILLIS) {
return cachedEnabled;
}
boolean enabled = resolveEnabled();
cachedEnabled = enabled;
cachedAt = now;
return enabled;
}
private boolean resolveEnabled() {
// 기본값 false - row 가 없는 환경(신규 운영 DB 등)에서 자동으로 켜지지 않도록 한다.
String value = portalPropertyService.getOrCreateProperty(
GROUP, KEY_TEST_NOTICE_ENABLED, "false",
"인증(이메일/SMS) 요청 시 인증번호를 화면에 표시할지 여부 (true/false, 테스트 전용)");
if (value == null || !"true".equalsIgnoreCase(value.trim())) {
return false;
}
String value = portalPropertyService.getOrCreateProperty(
GROUP, KEY_TEST_NOTICE_ENABLED, "true",
"인증(이메일/SMS) 요청 시 인증번호 화면 표시할지 여부 (true/false, 테스트 전용)");
return value != null && "true".equalsIgnoreCase(value.trim());
String until = portalPropertyService.getOrCreateProperty(
GROUP, KEY_TEST_NOTICE_PROD_UNTIL, TestNoticeWindow.UNSET,
"운영(prod)에서 인증번호 화면 표시를 허용할 종료일 (yyyy-MM-dd, 미사용은 none). 경과 시 자동 차단");
return testNoticeWindow.isOpen(until, KEY_TEST_NOTICE_ENABLED);
}
}
@@ -0,0 +1,74 @@
package com.eactive.apim.portal.apps.auth;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
/**
* 인증번호 화면 노출(테스트 안내) 스위치의 <b>운영 환경 사용 기한</b> 판정기.
*
* <p>운영 오픈 전에는 UMS/EAI 실발송 연계가 끝나지 않아 인증번호가 실제로 도달하지 않을 수 있다.
* 이 기간에는 prod 에서도 인증번호를 화면에 노출해야 테스트가 가능하다. 다만 스위치 끄기를 잊으면
* 인증번호가 상시 노출되므로, prod 에서는 <b>종료일을 명시한 경우에만</b> 열어 주고 그 날짜가 지나면
* PTL_PROPERTY 값과 무관하게 자동으로 닫는다.</p>
*
* <ul>
* <li>비운영(!prod) — 항상 열림. 종료일을 보지 않는다.</li>
* <li>운영(prod) — 종료일이 없거나 형식이 잘못되면 <b>닫힘</b>(fail-safe). 종료일 당일까지 열림.</li>
* </ul>
*
* <p>이메일/휴대폰 인증({@link AuthNoticeProperties})과 2차 인증
* ({@code TwoFactorProperties})이 같은 규칙을 쓰도록 판정을 이 클래스로 모은다.
* 종료일 프로퍼티 값은 {@code yyyy-MM-dd} 형식이며, 미설정을 뜻하는 기본값은 {@value #UNSET}
* ({@code PTL_PROPERTY.property_value} 가 NOT NULL 이라 빈 문자열을 기본값으로 쓸 수 없다).</p>
*/
@Component
@Slf4j
public class TestNoticeWindow {
/** 종료일 미설정을 뜻하는 기본값. prod 에서는 이 값이면 닫힘 */
public static final String UNSET = "none";
private final Environment environment;
public TestNoticeWindow(Environment environment) {
this.environment = environment;
}
/**
* @param untilValue 종료일 문자열({@code yyyy-MM-dd}). PTL_PROPERTY 값 원본
* @param subject 로그 식별용 스위치 이름
* @return 지금 인증번호를 화면에 노출해도 되는 기간인지 여부
*/
public boolean isOpen(String untilValue, String subject) {
if (!environment.acceptsProfiles(Profiles.of("prod"))) {
return true;
}
LocalDate until = parseDate(untilValue);
if (until == null) {
return false;
}
if (LocalDate.now().isAfter(until)) {
return false;
}
// 운영에서 인증번호가 화면에 노출되는 상태 - 흔적을 남긴다.
log.warn("운영 환경 인증번호 노출 활성 상태 - {} (종료일 {})", subject, until);
return true;
}
private static LocalDate parseDate(String value) {
if (value == null || value.trim().isEmpty()) {
return null;
}
try {
return LocalDate.parse(value.trim());
} catch (DateTimeParseException e) {
log.warn("인증번호 노출 종료일 형식 오류(yyyy-MM-dd 필요) - {}", value);
return null;
}
}
}
@@ -1,5 +1,6 @@
package com.eactive.apim.portal.apps.auth.twofactor;
import com.eactive.apim.portal.apps.auth.TestNoticeWindow;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode;
import lombok.RequiredArgsConstructor;
@@ -25,9 +26,11 @@ public class TwoFactorProperties {
public static final String KEY_TTL_SECONDS = "two-factor.ttl.seconds";
public static final String KEY_ATTEMPT_LIMIT = "two-factor.attempt.limit";
public static final String KEY_TEST_NOTICE_ENABLED = "two-factor.test-notice.enabled";
public static final String KEY_TEST_NOTICE_PROD_UNTIL = "two-factor.test-notice.prod-until";
public static final String KEY_STEPUP_ENABLED = "two-factor.stepup.enabled";
private final PortalPropertyService portalPropertyService;
private final TestNoticeWindow testNoticeWindow;
/** 로그인 2FA 활성화 여부 */
public boolean isLoginEnabled() {
@@ -91,9 +94,21 @@ public class TwoFactorProperties {
return parseInt(resolve(KEY_ATTEMPT_LIMIT, "5", "2차 인증번호 검증 시도 한도"), 5);
}
/** 팝업에 테스트용 인증번호를 노출할지 여부(개발/테스트 전용) */
/**
* 팝업에 테스트용 인증번호를 노출할지 여부(개발/테스트 전용).
*
* <p>운영(prod)에서는 종료일 {@link #KEY_TEST_NOTICE_PROD_UNTIL}({@code yyyy-MM-dd})을 함께
* 지정한 경우에만 참이 되고, 날짜가 지나면 {@link TestNoticeWindow} 가 자동으로 닫는다.
* 종료일 미설정/형식 오류면 운영에서는 열리지 않는다. 이메일/휴대폰 인증
* ({@code auth.test-notice.*})과 동일한 규칙이다.</p>
*/
public boolean isTestNoticeEnabled() {
return parseBool(resolve(KEY_TEST_NOTICE_ENABLED, "false", "2차 인증 팝업에 테스트용 인증번호 표시 여부 (true/false)"));
if (!parseBool(resolve(KEY_TEST_NOTICE_ENABLED, "false", "2차 인증 팝업에 테스트용 인증번호 표시 여부 (true/false)"))) {
return false;
}
String until = resolve(KEY_TEST_NOTICE_PROD_UNTIL, TestNoticeWindow.UNSET,
"운영(prod)에서 2차 인증번호 화면 표시를 허용할 종료일 (yyyy-MM-dd, 미사용은 none). 경과 시 자동 차단");
return testNoticeWindow.isOpen(until, KEY_TEST_NOTICE_ENABLED);
}
private String resolve(String key, String defaultValue, String description) {
@@ -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;
@@ -36,23 +37,27 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@RequiredArgsConstructor
public class PortalGlobalExceptionHandler {
private static final String REDIRECT_PREFIX = "redirect:";
private static final String REDIRECT_HOME = REDIRECT_PREFIX + "/";
private static final String REDIRECT_LOGIN_AUTH = REDIRECT_PREFIX + "/login?reason=auth";
private final Logger log = LoggerFactory.getLogger(getClass());
private final PortalProperties portalProperties;
private final Environment environment;
@ExceptionHandler(value = NotFoundException.class)
public ModelAndView handleINotFoundException(HttpServletRequest request, NotFoundException ex) {
return new ModelAndView("redirect:/");
return new ModelAndView(REDIRECT_HOME);
}
@ExceptionHandler(value = MethodArgumentTypeMismatchException.class)
public ModelAndView handleMethodArgumentTypeMismatchException(HttpServletRequest request, MethodArgumentTypeMismatchException ex) {
return new ModelAndView("redirect:/");
return new ModelAndView(REDIRECT_HOME);
}
@ExceptionHandler(value = UserNotLoginException.class)
public ModelAndView handleUserNotLoginException(HttpServletRequest request, UserNotLoginException ex) {
return new ModelAndView("redirect:/login?reason=auth");
return new ModelAndView(REDIRECT_LOGIN_AUTH);
}
@ExceptionHandler(value = AccessDeniedException.class)
@@ -61,7 +66,7 @@ public class PortalGlobalExceptionHandler {
if (!SecurityUtil.isAuthenticated()) {
// 원래 요청 페이지를 세션에 저장 → 로그인+2FA 완료 후 LoginFinalizer 가 복귀시킨다.
savePostLoginRedirect(request);
return new ModelAndView("redirect:/login?reason=auth");
return new ModelAndView(REDIRECT_LOGIN_AUTH);
}
log.warn("접근 권한 없음: loginId={}, uri={}", StringMaskingUtil.maskLoginId(SecurityUtil.getCurrentLoginId()), request.getRequestURI());
ModelAndView modelAndView = new ModelAndView("error");
@@ -122,9 +127,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_HOME);
}
return new ModelAndView(target);
}
/** 내부 경로/뷰 이름만 허용(외부 URL·프로토콜 상대 URL·표현식 문자 차단) */
private boolean isSafeRedirectTarget(String target) {
if (target == null) {
return false;
}
String path = target;
if (path.startsWith(REDIRECT_PREFIX)) {
path = path.substring(REDIRECT_PREFIX.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})
@@ -140,7 +176,7 @@ public class PortalGlobalExceptionHandler {
public ModelAndView handleHttpRequestMethodNotSupportedException(HttpServletRequest request, HttpRequestMethodNotSupportedException ex) {
log.error(ex.getMessage());
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("redirect:/");
modelAndView.setViewName(REDIRECT_HOME);
return modelAndView;
}
@@ -184,7 +220,7 @@ public class PortalGlobalExceptionHandler {
public ModelAndView handleInvalidFileException(HttpServletRequest request, RedirectAttributes redirectAttributes, InvalidFileException ex) {
ModelAndView modelAndView = new ModelAndView();
redirectAttributes.addFlashAttribute("error", ex.getMessage());
modelAndView.setViewName("redirect:" + request.getRequestURI());
modelAndView.setViewName(REDIRECT_PREFIX + request.getRequestURI());
return modelAndView;
}
@@ -195,7 +231,7 @@ public class PortalGlobalExceptionHandler {
String errorMessage = "파일 크기가 허용된 최대 용량(" + maxSize + ")을 초과했습니다.";
redirectAttributes.addFlashAttribute("error", errorMessage);
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("redirect:" + request.getRequestURI());
modelAndView.setViewName(REDIRECT_PREFIX + request.getRequestURI());
return modelAndView;
}
}
@@ -1,258 +0,0 @@
package com.eactive.apim.portal.common.migration;
import com.eactive.apim.portal.common.util.StringMaskingUtil;
import com.eactive.apim.portal.jpa.PersonalDataEncryptConverter;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import javax.servlet.http.HttpServletRequest;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* [임시] 레거시 평문 데이터를 {@link PersonalDataEncryptConverter} 규칙으로 일괄 정규화(암호화)하는 운영 도구.
*
* <p>배경: {@code @Convert} 컬럼이 평문으로 저장된 레거시 행은, derived query가 검색값을 암호화하면서
* 평문 DB값과 불일치해 검색/로그인이 실패한다. 컨버터는 읽기에서 평문/암호문을 자동 구분하고
* 쓰기에서 무조건 인코딩하므로, {@code convertToDatabaseColumn(convertToEntityAttribute(x))}는
* 평문→인코딩, 인코딩→동일값(멱등)으로 정규화된다. 이 값이 기존과 다를 때만 UPDATE 한다.</p>
*
* <p>보안: PTL_PROPERTY {@code Portal / migration.internal.allow-ips} 허용 IP 목록(콤마 구분,
* 기본 loopback)에 포함된 IP 의 직접 호출만 허용한다 ({@code MenuInternalController} 모델).
* 운영 서버는 bind IP 가 NIC IP 라 loopback 호출이 불가하므로, 실행 전 property 에 호출자 IP 를
* 추가하고 작업 완료 후 원복한다. 프록시 경유(X-Forwarded-For 존재) 요청은 거부한다.
* 기본은 dry-run(미변경)이며, 실제 실행은 {@code dryRun=false}를 명시해야 한다.
* 작업 완료 후 이 클래스는 제거한다.</p>
*
* <pre>
* # 미리보기(변경 안 함)
* curl -X POST 'http://127.0.0.1:39130/internal/migration/encrypt-legacy'
* # 실제 실행 (PII 컬럼)
* curl -X POST 'http://127.0.0.1:39130/internal/migration/encrypt-legacy?dryRun=false'
* # audit 컬럼(created_by/last_modified_by, 19개 테이블)까지 포함
* curl -X POST 'http://127.0.0.1:39130/internal/migration/encrypt-legacy?dryRun=false&includeAudit=true'
* </pre>
*/
@Slf4j
@RestController
@RequestMapping("/internal/migration")
public class LegacyEncryptionMigrationController {
/** PII 직접 컬럼 (로그인/검색에 직접 영향). ofctelno 는 admin(UnifbwkManService)이 컨버터를 수동 호출해 암호화하는 컬럼 */
private static final List<TargetTable> PII_TARGETS = Arrays.asList(
new TargetTable("PTL_USER", Arrays.asList("login_id", "email_addr", "phone_number", "mobile_number")),
new TargetTable("PTL_MESSAGE_REQUEST", Arrays.asList("email", "phone")),
new TargetTable("tseairm02", Arrays.asList("cphnno", "emad", "ofctelno")),
new TargetTable("PTL_USER_LOG", Arrays.asList("login_id")),
new TargetTable("PTL_TWO_FACTOR_AUTH", Arrays.asList("recipient")),
new TargetTable("PTL_USER_INVITATION", Arrays.asList("INVITATION_MOBILE"))
);
/** Auditable(@MappedSuperclass) 상속 테이블의 감사 컬럼 (옵션) */
private static final List<String> AUDIT_TABLES = Arrays.asList(
"PTL_USER", "ptl_faq", "ptl_org",
"DJB_APISTATUS_INCIDENT", "DJB_APISTATUS_INCIDENT_TIMELINE", "DJB_APISTATUS_INCIDENT_API",
"ptl_file", "PTL_MESSAGE_TEMPLATE", "ptl_notice", "ptl_terms",
"ptl_user_privacy_policy_agreement", "ptl_approval_line",
"PTL_INQUIRY_COMMENT", "ptl_inquiry", "ptl_partnership_application",
"PTL_MENU_ITEM", "PTL_MENU_PLACEMENT", "PTL_ROLE", "PTL_ROLE_AUTHORITY"
);
private static final List<String> AUDIT_COLUMNS = Arrays.asList("created_by", "last_modified_by");
static final String PROP_GROUP = "Portal";
static final String PROP_ALLOW_IPS = "migration.internal.allow-ips";
static final String DEFAULT_ALLOW_IPS = "127.0.0.1,::1";
private final JdbcTemplate jdbcTemplate;
private final PortalPropertyService portalPropertyService;
private final PersonalDataEncryptConverter converter = new PersonalDataEncryptConverter();
public LegacyEncryptionMigrationController(@Qualifier("portalDataSource") DataSource emsDataSource,
PortalPropertyService portalPropertyService) {
// EMS(EMSAPP) 스키마 데이터소스. 컨버터 적용 테이블은 모두 EMS에 존재한다.
this.jdbcTemplate = new JdbcTemplate(emsDataSource);
this.portalPropertyService = portalPropertyService;
}
@PostMapping("/encrypt-legacy")
@Transactional("transactionManager")
public Map<String, Object> encryptLegacy(HttpServletRequest request,
@RequestParam(defaultValue = "true") boolean dryRun,
@RequestParam(defaultValue = "false") boolean includeAudit) {
assertAllowedIp(request);
assertNotBypass();
List<TargetTable> targets = new ArrayList<>(PII_TARGETS);
if (includeAudit) {
for (String table : AUDIT_TABLES) {
targets.add(new TargetTable(table, AUDIT_COLUMNS));
}
}
List<Map<String, Object>> results = new ArrayList<>();
int totalChanged = 0;
int totalSkipped = 0;
for (TargetTable target : targets) {
for (String column : target.columns) {
Map<String, Object> r = processColumn(target.table, column, dryRun);
results.add(r);
totalChanged += (int) r.get("changed");
totalSkipped += (int) r.get("skipped");
}
}
Map<String, Object> response = new LinkedHashMap<>();
response.put("mode", dryRun ? "dry-run (변경 없음)" : "executed");
response.put("damoMode", resolveDamoMode());
response.put("includeAudit", includeAudit);
response.put("totalChanged", totalChanged);
// 정규화 결과가 빈 값이라 UPDATE 를 생략한 건수. 0 이 아니면 원인 조사 후 진행할 것.
response.put("totalSkipped", totalSkipped);
response.put("results", results);
log.info("[레거시 암호화 마이그레이션] mode={} includeAudit={} totalChanged={} totalSkipped={}",
dryRun ? "dry-run" : "executed", includeAudit, totalChanged, totalSkipped);
return response;
}
private String resolveDamoMode() {
if (converter.isBypassMode()) {
return "BYPASS";
}
return converter.isFakeMode() ? "FAKE" : "REAL";
}
/**
* 단일 (테이블, 컬럼)의 고유값을 정규화하고, 값이 바뀌는 경우에만 UPDATE.
*/
private Map<String, Object> processColumn(String table, String column, boolean dryRun) {
Map<String, Object> r = new LinkedHashMap<>();
r.put("table", table);
r.put("column", column);
List<String> values;
try {
values = jdbcTemplate.queryForList(
"SELECT DISTINCT " + column + " FROM " + table + " WHERE " + column + " IS NOT NULL",
String.class);
} catch (Exception e) {
log.warn("[마이그레이션] 조회 실패 table={} column={} : {}", table, column, e.toString());
r.put("distinct", 0);
r.put("changed", 0);
r.put("skipped", 0);
r.put("error", e.getMessage());
return r;
}
int changed = 0;
int skipped = 0;
for (String value : values) {
String normalized;
try {
// 평문 → 인코딩, 이미 인코딩 → 동일값 (멱등)
normalized = converter.convertToDatabaseColumn(converter.convertToEntityAttribute(value));
} catch (Exception e) {
log.warn("[마이그레이션] 정규화 실패 table={} column={} : {}", table, column, e.toString());
continue;
}
if ((normalized == null || normalized.isEmpty()) && !value.isEmpty()) {
// 방어: 원본이 비어있지 않은데 정규화 결과가 빈 값 → 절대 UPDATE 하지 않음 (데이터 소실 방지)
skipped++;
log.warn("[마이그레이션] 정규화 결과가 빈 값 — UPDATE 생략 table={} column={} valueLen={}",
table, column, value.length());
continue;
}
if (normalized != null && !normalized.equals(value)) {
if (!dryRun) {
jdbcTemplate.update(
"UPDATE " + table + " SET " + column + " = ? WHERE " + column + " = ?",
normalized, value);
}
changed++;
}
}
r.put("distinct", values.size());
r.put("changed", changed);
r.put("skipped", skipped);
return r;
}
/**
* damo-manager 가 bypass 모드면 실행 자체를 거부한다.
* bypass 에서는 {@code encrypt} 가 무변환(원문 그대로)이라 정규화가 평문→평문 no-op 이 되어
* 마이그레이션이 의미가 없고, "완료"로 오인될 위험이 있다. real/fake 모드로 기동 후 실행해야 한다.
*/
private void assertNotBypass() {
if (converter.isBypassMode()) {
log.warn("[마이그레이션] bypass 모드 실행 거부 — 암복호화가 무변환이라 마이그레이션이 무의미함");
throw new ResponseStatusException(HttpStatus.CONFLICT,
"damo-manager 가 bypass 모드입니다. 암복호화가 무변환(원문 그대로)이라 마이그레이션이 무의미하므로 거부합니다. "
+ "real/fake 모드(-Ddamo-manager.enabled=true)로 기동한 뒤 실행하세요.");
}
}
/**
* PTL_PROPERTY({@code Portal / migration.internal.allow-ips}) 허용 IP 목록 검사.
* 프록시 경유(X-Forwarded-For 존재) 요청은 IP 신뢰 불가로 거부한다. ({@code MenuInternalController} 모델)
* 운영 서버는 bind IP 가 NIC IP 라 loopback 기본값으로는 호출 불가 — 실행 전 property 에
* 호출자 IP 를 추가하고 완료 후 원복한다.
*/
private void assertAllowedIp(HttpServletRequest request) {
String remote = canonicalize(request.getRemoteAddr());
boolean viaProxy = request.getHeader("X-Forwarded-For") != null;
Set<String> allowed = Arrays.stream(resolveAllowIps().split(","))
.map(String::trim)
.filter(ip -> !ip.isEmpty())
.map(LegacyEncryptionMigrationController::canonicalize)
.collect(Collectors.toSet());
if (viaProxy || !allowed.contains(remote)) {
log.warn("[마이그레이션] 비허용 접근 차단 remoteAddr={} viaProxy={} xff={}",
StringMaskingUtil.maskIpAddress(remote), viaProxy,
StringMaskingUtil.maskIpAddress(request.getHeader("X-Forwarded-For")));
throw new ResponseStatusException(HttpStatus.FORBIDDEN,
"허용되지 않은 접근입니다. (PTL_PROPERTY " + PROP_GROUP + "/" + PROP_ALLOW_IPS + " 확인)");
}
}
private String resolveAllowIps() {
try {
return portalPropertyService.getOrCreateProperty(PROP_GROUP, PROP_ALLOW_IPS,
DEFAULT_ALLOW_IPS, "레거시 암호화 마이그레이션 내부 API 허용 IP 목록(콤마 구분)");
} catch (Exception e) {
log.warn("[마이그레이션] 허용 IP 목록 조회 실패 - 기본값({}) 사용", DEFAULT_ALLOW_IPS, e);
return DEFAULT_ALLOW_IPS;
}
}
/** IPv6 loopback 표기 통일 */
private static String canonicalize(String ip) {
return "0:0:0:0:0:0:0:1".equals(ip) ? "::1" : ip;
}
private static final class TargetTable {
final String table;
final List<String> columns;
TargetTable(String table, List<String> columns) {
this.table = table;
this.columns = columns;
}
}
}
@@ -1,6 +1,7 @@
package com.eactive.apim.portal.common.validator;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.springframework.stereotype.Component;
import javax.validation.ConstraintValidator;
@@ -39,14 +40,8 @@ public class CellPhoneValidator implements ConstraintValidator<CellPhone, String
}
// 기존의 prefix 체크 로직 유지
return StringUtils.startsWith(cellPhone, "+821") ||
StringUtils.startsWith(cellPhone, "821") ||
StringUtils.startsWith(cellPhone, "010") ||
StringUtils.startsWith(cellPhone, "011") ||
StringUtils.startsWith(cellPhone, "016") ||
StringUtils.startsWith(cellPhone, "017") ||
StringUtils.startsWith(cellPhone, "018") ||
StringUtils.startsWith(cellPhone, "019");
return Strings.CS.startsWithAny(cellPhone,
"+821", "821", "010", "011", "016", "017", "018", "019");
}
private boolean isValidPartLength(String prefix, String middle, String last) {
@@ -2,6 +2,7 @@ package com.eactive.apim.portal.config;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.HashMap;
import javax.naming.NamingException;
import javax.sql.DataSource;
@@ -58,11 +59,11 @@ public class BaseDatasourceConfiguration {
//public interface AvailableSettings extends org.hibernate.jpa.AvailableSettings 참고
properties.put("hibernate.dialect", prop.getHibernateDialect());
properties.put("hibernate.connection.handling_mode", "DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION");
properties.put("hibernate.transaction.jta.platform", "org.hibernate.engine.transaction.jta.platform.internal.AtomikosJtaPlatform");
properties.put("hibernate.physical_naming_strategy", prop.getHibernatePhysicalNamingStrategy());
properties.put("hibernate.default_schema", prop.getSchema());
properties.put("javax.persistence.validation.mode", "none");
properties.put("javax.persistence.transactionType", "JTA");
// JTA/XA 미사용. EMF 별 로컬 트랜잭션(JpaTransactionManager)으로 처리한다 - PortalConfigTransaction 참고
properties.put("javax.persistence.transactionType", "RESOURCE_LOCAL");
properties.put("hibernate.format_sql", "true");
if (prop instanceof EmsDatasourceProperty) {
@@ -110,9 +111,22 @@ public class BaseDatasourceConfiguration {
return builder
.dataSource(dataSource)
// entity-package 는 콤마로 복수 지정 가능 (gateway 는 online-core + 포털 통계 엔티티 패키지)
.packages(prop.getEntityPackage().split("\\s*,\\s*"))
.packages(splitPackages(prop.getEntityPackage()))
.persistenceUnit(persistenceUnit)
.properties(properties)
.build();
}
/**
* 콤마로 나열된 엔티티 패키지를 분리한다. 앞뒤 공백은 걷어내고 빈 토큰은 버린다.
*
* <p>구분자 정규식 {@code "\\s*,\\s*"} 은 백트래킹으로 super-linear 가 될 수 있어(Sonar S5852)
* 단순 분리 + trim 으로 대체했다.</p>
*/
private static String[] splitPackages(String entityPackage) {
return Arrays.stream(entityPackage.split(","))
.map(String::trim)
.filter(pkg -> !pkg.isEmpty())
.toArray(String[]::new);
}
}
@@ -18,7 +18,8 @@ import javax.sql.DataSource;
@Configuration
@EntityScan(basePackages = {"com.eactive.apim.gateway"})
@EnableJpaRepositories(basePackages = "com.eactive.apim.gateway", repositoryBaseClass = BaseRepositoryImpl.class, entityManagerFactoryRef = "gatewayEntityManagerFactory")
@EnableJpaRepositories(basePackages = "com.eactive.apim.gateway", repositoryBaseClass = BaseRepositoryImpl.class,
entityManagerFactoryRef = "gatewayEntityManagerFactory", transactionManagerRef = "gatewayTransactionManager")
@Slf4j
public class GatewayDatasourceConfiguration extends BaseDatasourceConfiguration {
private final Environment env;
@@ -0,0 +1,31 @@
package com.eactive.apim.portal.config;
import com.eactive.apim.portal.common.internal.InternalApiTokenService;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* 내부 API 공유 토큰 초기화 — 포탈 기동 시 1회.
*
* <p>PTL_PROPERTY {@code Portal / internal.api.header-name}, {@code internal.api.token} 이 없으면
* 이 시점에 생성된다(토큰은 SecureRandom 난수). eapim-admin 은 이 값을 <b>읽기만</b> 하므로,
* 토큰을 처음 만드는 주체는 포탈 한 곳으로 고정된다.</p>
*
* <p>{@link ApplicationReadyEvent} 를 쓰는 이유: 데이터소스/JTA 초기화가 끝난 뒤여야 프로퍼티를 저장할 수 있다.
* WAR 배포(WebLogic)에서도 동일하게 발생한다.</p>
*
* @see com.eactive.apim.portal.djb.menu.MenuInternalController
*/
@Component
@RequiredArgsConstructor
public class InternalApiTokenInitializer {
private final InternalApiTokenService internalApiTokenService;
@EventListener(ApplicationReadyEvent.class)
public void initialize() {
internalApiTokenService.initialize();
}
}
@@ -2,6 +2,8 @@ package com.eactive.apim.portal.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUpload;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
@@ -18,6 +20,18 @@ import org.springframework.web.multipart.commons.CommonsMultipartResolver;
@RequiredArgsConstructor
public class MultipartConfig {
/**
* 멀티파트 파트 1개의 헤더 구간 최대 바이트.
* <p>
* commons-fileupload 1.6.0(CVE-2025-48976 수정)부터 이 상한이 생겼고 기본값이 512 바이트다.
* 한글 파일명은 UTF-8 로 3바이트/자라 Content-Disposition 이 금방 커진다 — 실측상 기본값 512 로는
* 한글 약 137자 이상 파일명에서 업로드가 실패하고, 예외도 "Maximum upload size exceeded" 로 감싸져
* 원인 파악이 어렵다. OS 파일명 상한(255자)이 전부 한글이어도 통과하도록 2048 로 둔다.
* (1.5 이전에는 사실상 10240 이었으므로 이 값도 그보다 5배 엄격하다.)
* </p>
*/
private static final int PART_HEADER_SIZE_MAX = 2048;
private final PortalProperties portalProperties;
/**
@@ -31,7 +45,14 @@ public class MultipartConfig {
*/
@Bean(name = "filterMultipartResolver")
public CommonsMultipartResolver filterMultipartResolver() {
CommonsMultipartResolver resolver = new CommonsMultipartResolver();
CommonsMultipartResolver resolver = new CommonsMultipartResolver() {
@Override
protected FileUpload newFileUpload(FileItemFactory fileItemFactory) {
FileUpload fileUpload = super.newFileUpload(fileItemFactory);
fileUpload.setPartHeaderSizeMax(PART_HEADER_SIZE_MAX);
return fileUpload;
}
};
long maxSizeBytes = portalProperties.getFile().getMaxSizeBytes();
resolver.setMaxUploadSize(maxSizeBytes);
@@ -39,8 +60,8 @@ public class MultipartConfig {
resolver.setMaxInMemorySize((int) Math.min(maxSizeBytes, Integer.MAX_VALUE));
resolver.setDefaultEncoding("UTF-8");
log.info("CommonsMultipartResolver configured with maxUploadSize: {} bytes ({})",
maxSizeBytes, portalProperties.getFile().getMaxSize());
log.info("CommonsMultipartResolver configured with maxUploadSize: {} bytes ({}), partHeaderSizeMax: {} bytes",
maxSizeBytes, portalProperties.getFile().getMaxSize(), PART_HEADER_SIZE_MAX);
return resolver;
}
@@ -33,15 +33,13 @@ import org.springframework.web.filter.ForwardedHeaderFilter;
* → getHeader("X-Forwarded-For") = null (필터가 제거 → viaProxy 판정이 false)
* </pre>
*
* 즉 {@code MenuInternalController#isAllowed} / {@code LegacyEncryptionMigrationController#assertAllowedIp}
* 의 "프록시 경유 거부 + loopback 허용" 가드가 무력화된다. 필터를 제외해 두면 이 경로만은 <b>원 소켓 IP</b> 로
* 검사되므로 기존 가드가 설계대로 동작한다.
* 즉 {@code MenuInternalController#isAllowedIp} 의 "프록시 경유 거부 + loopback 허용" 가드가 무력화된다.
* 필터를 제외해 두면 이 경로만은 <b>원 소켓 IP</b> 로 검사되므로 기존 가드가 설계대로 동작한다.
*
* <p><b>전제</b>: {@code framework} 는 신뢰 프록시 목록이 없어 헤더를 무조건 신뢰한다. 앞단(OHS)에서 인바운드
* {@code X-Forwarded-*} / {@code Forwarded} 를 제거한 뒤 재설정해야 하며, WAS 포트로의 직접 접근 경로도 차단해야 한다.
*
* @see com.eactive.apim.portal.djb.menu.MenuInternalController
* @see com.eactive.apim.portal.common.migration.LegacyEncryptionMigrationController
*/
@Slf4j
@Configuration
@@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -16,6 +17,7 @@ import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CsrfException;
import org.springframework.security.web.header.HeaderWriterFilter;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@@ -92,6 +94,23 @@ public class PortalConfigSecurity {
http
.authenticationManager(portalAuthenticationManager)
// CVE-2026-22732 우회책. Spring Security 는 기본적으로 보안 헤더를 "응답 커밋 시점"에
// 지연 기록하는데, 응답 래퍼가 Content-Length 를 setHeader/setIntHeader/addIntHeader 로
// 지정하는 경로를 추적하지 못해 그 경우 헤더가 통째로 누락된다
// (X-Content-Type-Options, X-Frame-Options, Cache-Control, Pragma, Expires, X-XSS-Protection).
// 수정본은 5.7.22/5.8.24(Enterprise 전용)뿐이라 OSS 로는 올릴 수 없어 우회책을 적용한다.
// 요청 시작 시점에 헤더를 기록하게 만든다. 앱이 나중에 같은 헤더를 지정하면 앱 값이 남는다
// (실측: FileDownloadController#viewImage 의 Cache-Control: public, max-age=86400 유지됨).
// 현재 앱은 response.setContentLength(int) 만 쓰므로 노출 경로는 없지만,
// 새 코드가 위 메서드를 쓰더라도 헤더가 빠지지 않도록 두는 안전망이다.
// Spring Security 를 수정본(6.5.9+/7.0.4+ 또는 Enterprise 5.7.22+)으로 올리면 제거 가능.
.headers(headers -> headers.addObjectPostProcessor(new ObjectPostProcessor<HeaderWriterFilter>() {
@Override
public <O extends HeaderWriterFilter> O postProcess(O filter) {
filter.setShouldWriteHeadersEagerly(true);
return filter;
}
}))
.formLogin(form -> form
.loginPage("/login")
.usernameParameter("id")
@@ -107,8 +126,12 @@ public class PortalConfigSecurity {
.logoutSuccessHandler(logoutSuccessHandler))
.csrf(csrf -> csrf
.csrfTokenRepository(csrfTokenRepository)
.ignoringRequestMatchers(new AntPathRequestMatcher("/_proxy/**/*"))
.ignoringRequestMatchers(new AntPathRequestMatcher("/internal/migration/**"))
// /_proxy 는 대응 핸들러가 없어 예외를 해제했다. 경로가 부활하면 아래를 되살릴 것.
// .ignoringRequestMatchers(new AntPathRequestMatcher("/_proxy/**/*"))
// /internal/migration 은 LegacyEncryptionMigrationController 제거와 함께 삭제됨.
// /internal/menu 는 브라우저 세션이 없는 서버간 호출(admin → portal)이라 CSRF 토큰을 실을 수 없다.
// 대신 InternalApiTokenService 의 공유 토큰 헤더 + 허용 IP 목록으로 통제한다.
// (커스텀 헤더는 cross-site form POST 로 위조할 수 없어 CSRF 경로가 차단된다)
.ignoringRequestMatchers(new AntPathRequestMatcher("/internal/menu/**"))
)
// 로그인 페이지에 오래 머물러 세션(=CSRF 토큰 저장소)이 타임아웃되면
@@ -1,39 +1,44 @@
package com.eactive.apim.portal.config;
import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import javax.persistence.EntityManagerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.jta.JtaTransactionManager;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;
/**
* 트랜잭션 매니저 설정.
*
* <p>EMS(포털)·AGW(게이트웨이) 두 데이터소스를 쓰지만 <b>JTA/XA 는 사용하지 않는다</b>.
* 두 DB 를 한 트랜잭션으로 묶어 쓰는 지점이 없고(게이트웨이 측은 조회 전용),
* 이전 Atomikos 구성도 {@code AtomikosDataSourceBean} 없이 JNDI/Hikari 데이터소스를 그대로 넘겨
* XA 리소스가 하나도 enlist 되지 않는 형태였다. 즉 2PC 는 실제로 동작한 적이 없고
* {@code com.atomikos.icatch.max_actives} 상한(기본 50)만 병목으로 남았다.</p>
*
* <p>따라서 EntityManagerFactory 별로 로컬 {@link JpaTransactionManager} 를 둔다.
* 게이트웨이 리포지토리는 {@code GatewayDatasourceConfiguration} 의
* {@code transactionManagerRef = "gatewayTransactionManager"} 로 연결된다.</p>
*/
@Configuration
@EnableTransactionManagement
public class PortalConfigTransaction {
@Bean(name = "userTransaction")
public UserTransaction userTransaction() throws SystemException {
UserTransactionImp userTransactionImp = new UserTransactionImp();
userTransactionImp.setTransactionTimeout(30000);
return userTransactionImp;
}
@Bean(name = "atomikosTransactionManager", initMethod = "init", destroyMethod = "close")
public UserTransactionManager atomikosTransactionManager() {
UserTransactionManager userTransactionManager = new UserTransactionManager();
userTransactionManager.setForceShutdown(false);
return userTransactionManager;
}
/** EMS(포털) 기본 트랜잭션 매니저. {@code @Transactional} 무지정 시 이 매니저가 쓰인다. */
@Primary
@Bean(name = "transactionManager")
@DependsOn({"userTransaction", "atomikosTransactionManager"})
public PlatformTransactionManager transactionManager(UserTransactionManager atomikosTransactionManager) throws SystemException {
UserTransaction userTransaction = userTransaction();
return new JtaTransactionManager(userTransaction, atomikosTransactionManager);
public PlatformTransactionManager transactionManager(
@Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
/** AGW(게이트웨이) 트랜잭션 매니저. {@code @Transactional("gatewayTransactionManager")} 로 지정해 쓴다. */
@Bean(name = "gatewayTransactionManager")
public PlatformTransactionManager gatewayTransactionManager(
@Qualifier("gatewayEntityManagerFactory") EntityManagerFactory gatewayEntityManagerFactory) {
return new JpaTransactionManager(gatewayEntityManagerFactory);
}
}
@@ -1,5 +1,6 @@
package com.eactive.apim.portal.djb.menu;
import com.eactive.apim.portal.common.internal.InternalApiTokenService;
import com.eactive.apim.portal.common.util.IpAddressMatcher;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import lombok.RequiredArgsConstructor;
@@ -19,14 +20,20 @@ import java.util.Map;
/**
* 메뉴 캐시 내부 API — eapim-admin 의 reload 명령 수신용.
*
* <p>가드: PTL_PROPERTY {@code Portal / menu.internal.allow-ips} 허용 IP 목록
* (기본 loopback) + X-Forwarded-For 동반 요청 거부
* ({@code LegacyEncryptionMigrationController.assertLocalOnly} 모델).
* 허용 목록은 {@link IpAddressMatcher} 규칙을 따라 정확 일치 외에
* IPv4 CIDR({@code 172.30.1.0/24}) 과 옥텟 와일드카드({@code 172.30.*.*}) 를 지원한다.
* CSRF 는 PortalConfigSecurity 에서 {@code /internal/menu/**} 예외 처리.</p>
* <p>가드는 두 겹이다.</p>
* <ol>
* <li><b>공유 토큰 헤더</b> — {@link InternalApiTokenService}. 헤더명/토큰은 PTL_PROPERTY
* {@code Portal / internal.api.header-name}, {@code internal.api.token} 으로 관리하며
* 토큰은 포탈 최초 기동 시 자동 생성된다. 이 경로는 CSRF 예외 대상
* (PortalConfigSecurity {@code /internal/menu/**})이라, 커스텀 헤더 요구가 CSRF 를 대신한다 —
* 브라우저의 cross-site form POST 는 커스텀 헤더를 붙일 수 없다.</li>
* <li><b>허용 IP 목록</b> — PTL_PROPERTY {@code Portal / menu.internal.allow-ips}(기본 loopback)
* + X-Forwarded-For 동반 요청 거부. 허용 목록은 {@link IpAddressMatcher} 규칙을 따라 정확 일치 외에
* IPv4 CIDR({@code 172.30.1.0/24}) 과 옥텟 와일드카드({@code 172.30.*.*}) 를 지원한다.</li>
* </ol>
*
* <pre>curl -X POST http://127.0.0.1:39130/internal/menu/reload</pre>
* <pre>curl -X POST -H 'X-Internal-Token: &lt;PTL_PROPERTY Portal/internal.api.token&gt;' \
* http://127.0.0.1:39130/internal/menu/reload</pre>
*/
@Slf4j
@RestController
@@ -43,14 +50,18 @@ public class MenuInternalController {
private final MenuService menuService;
private final PortalPropertyService portalPropertyService;
private final InternalApiTokenService internalApiTokenService;
@PostMapping("/reload")
public ResponseEntity<Map<String, Object>> reload(HttpServletRequest request) {
if (!isAllowed(request)) {
Map<String, Object> denied = new LinkedHashMap<>();
denied.put("result", "DENIED");
denied.put("message", "허용되지 않은 접근입니다. (menu.internal.allow-ips 확인)");
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(denied);
if (!isAllowedIp(request)) {
return denied(HttpStatus.FORBIDDEN,
"허용되지 않은 접근입니다. (" + PROP_GROUP + "/" + PROP_ALLOW_IPS + " 확인)");
}
if (!hasValidToken(request)) {
return denied(HttpStatus.UNAUTHORIZED,
"내부 API 토큰이 유효하지 않습니다. (" + InternalApiTokenService.PROP_GROUP + "/"
+ InternalApiTokenService.PROP_TOKEN + " 확인)");
}
MenuService.MenuSnapshot snapshot = menuService.reload();
@@ -63,12 +74,31 @@ public class MenuInternalController {
return ResponseEntity.ok(body);
}
private ResponseEntity<Map<String, Object>> denied(HttpStatus status, String message) {
Map<String, Object> denied = new LinkedHashMap<>();
denied.put("result", "DENIED");
denied.put("message", message);
return ResponseEntity.status(status).body(denied);
}
/**
* 공유 토큰 헤더 검사. 헤더명은 PTL_PROPERTY 로 바뀔 수 있으므로 매 요청 조회한다(호출 빈도가 낮다).
*/
private boolean hasValidToken(HttpServletRequest request) {
String headerName = internalApiTokenService.ensureHeaderName();
if (!internalApiTokenService.matches(request.getHeader(headerName))) {
log.warn("메뉴 내부 API 차단 - 토큰 불일치, remote: {}, header: {}", request.getRemoteAddr(), headerName);
return false;
}
return true;
}
/**
* 허용 IP 검사. 프록시 경유(X-Forwarded-For 존재) 요청은 IP 신뢰 불가로 거부한다.
* (embedded Tomcat 은 forward-headers-strategy: native 로 XFF 가 remoteAddr 에 반영될 수 있으나
* 그 경우에도 allowlist 검사로 차단된다. WebLogic WAR 배포에서는 원 소켓 IP 로 검사된다.)
*/
private boolean isAllowed(HttpServletRequest request) {
private boolean isAllowedIp(HttpServletRequest request) {
String remote = IpAddressMatcher.canonicalize(request.getRemoteAddr());
boolean viaProxy = request.getHeader("X-Forwarded-For") != null;
-2
View File
@@ -1,6 +1,4 @@
spring:
jta:
enabled: false
config:
activate:
on-profile: dev
-2
View File
@@ -1,6 +1,4 @@
spring:
jta:
enabled: false
config:
activate:
on-profile: prod
-2
View File
@@ -1,6 +1,4 @@
spring:
jta:
enabled: false
web:
resources:
cache:
+2 -8
View File
@@ -58,13 +58,7 @@ spring:
# 설정으로는 적용되지 않는다. 실제 버전닝은 PortalConfigWebDispatcherServlet 가
# 아래 app.resource-versioning.enabled 토글을 읽어 직접 수행한다.
jta:
enabled: false
atomikos:
properties:
# log-base-dir: /logs/eapim/${inst.Name:devSvr11}/atomikos
log-base-dir: /dev/null
log-base-name: adp_tx
# JTA/Atomikos 제거됨. EMS·AGW 각각 로컬 트랜잭션(JpaTransactionManager) 사용 - PortalConfigTransaction 참고
thymeleaf:
prefix: 'classpath:/templates/views/'
@@ -125,7 +119,7 @@ portal:
allowed-extensions: pdf,doc,docx,xls,xlsx,ppt,pptx,hwp,gif,jpg,jpeg,png
logging:
log-path: /logs/eapim
log-path: /logs/prod/eapim
# 내부 사용자(운영자) 판별 설정
# 로그인 계정 이메일의 호스트가 아래 도메인이면 내부 사용자로 분류한다.
-3
View File
@@ -1,3 +0,0 @@
# application.yml ? ??
# com.atomikos.icatch.log_base_dir=/Log/eapim/portal/
# com.atomikos.icatch.log_base_name=adp_tx
-3
View File
@@ -87,9 +87,6 @@
<logger name="org.springframework.orm.jpa" level="DEBUG" additivity="false">
<appender-ref ref="FILE_HIBERNATE" />
</logger>
<logger name="com.atomikos" level="DEBUG" additivity="false">
<appender-ref ref="FILE_HIBERNATE" />
</logger>
<logger name="eapim.portal.session" level="INFO" additivity="false">
<appender-ref ref="HTTP_SESSION" />
-491
View File
@@ -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 = `
<div class="modal fade" id="chart_editor_modal" tabindex="-1" aria-labelledby="chart_editor_modal" aria-hidden="true">
<div class="modal-dialog modal-fullscreen modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">차트 정보</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="editor_container">
<input type="hidden" name="type" value="" />
<div class="left-panel default-info">
${LayoutUtil.createForm(this.chartEditorModel)}
</div>
<div class="editor-right-panel">
<div class="series-config d-flex flex-column">
<h6>Data Source</h6>
<div class="d-flex flex-row">
<div class="form-floating me-2" style="flex: 1">
<select class="form-select" name="seriesConfig[0].queryType">
<option value="elasticsearch">Elasticsearch</option>
</select>
<label for="queryType">Query Type</label>
</div>
<div class="form-floating me-2" style="flex: 1">
<input class="form-control" type="text" name="seriesConfig[0].index" value="" />
<label for="index">Elasticsearch Index</label>
</div>
</div>
<div class="d-flex flex-row mt-2" style="flex: 1">
<div style="flex: 1">
<div class="form-floating me-2" style="height: 100%">
<textarea class="form-control" type="text" name="seriesConfig[0].query" id="seriesConfigQuery" placeholder="Code goes here..." style="height: 400px;">
</textarea>
</div>
</div>
<div style="flex: 1">
<div class="form-floating me-2" style="height: 100%">
<textarea class="form-control" type="text" name="seriesConfig[0].queryResult" style="height: 400px;" readonly>
</textarea>
<label for="query">요청 결과</label>
</div>
</div>
</div>
<div class="d-flex flex-row mt-2" style="flex: 1">
<div style="flex: 1">
<div class="form-floating me-2" style="height: 100%">
<textarea class="form-control" type="text" name="seriesConfig[0].mapperY" style="height: 400px;">
</textarea>
<label for="mapper">데이터 Mapper</label>
</div>
</div>
<div style="flex: 1">
<div class="form-floating me-2" style="height: 100%">
<textarea class="form-control" type="text" name="seriesConfig[0].mapperX" style="height: 400px;">
</textarea>
<label for="mapper">X축 Mapper</label>
</div>
</div>
</div>
<div class="d-flex flex-row" style="height: 400px">
<div style="flex: 1">
<div class="form-floating me-2" style="height: 100%">
<textarea class="form-control" type="text" id="echartOption" style="width: 100%; height: 400px;" readonly>
</textarea>
<label for="mapper">생성된 Option</label>
</div>
</div>
<div style="flex: 1">
<div class="chart-preview form-control" id="chart-preview" style="height: 400px;">
chart_preview
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary btn-sm" data-bs-dismiss="modal">닫기</button>
<button type="button" class="btn btn-primary btn-sm add-chart">추가</button>
<button type="button" class="btn btn-primary btn-sm save-chart">저장</button>
</div>
</div>
</div>
</div>`;
$(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();
});
}
}
-344
View File
@@ -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 `
<div class="input-group mb-3">
<div class="form-floating">
<input id="${id}Input" type="text" class="form-control" data-td-target="#${id}"/>
<label for="${id}Input">${label}</label>
</div>
<span class="input-group-text" data-td-target="#${id}" data-td-toggle="datetimepicker">
<span class="fa-solid fa-calendar"></span>
</span>
</div>
`;
}
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 = `<script>
function get(obj, path, defaultValue = undefined) {
const keys = Array.isArray(path) ? path : path.replace(/\\[(\\d+)\\]/g, '.$1').split('.');
let result = obj;
for (let key of keys) {
result = result?.[key];
if (result === undefined) {
return defaultValue;
}
}
return result;
}
function 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}\`;
}
function 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\`;
}
function formatDateTime(isoString) {
return formatDate(isoString) + 'T' + formatTime(isoString);
}
function startOfDay(isoString) {
return formatDate(isoString) + 'T00:00:00';
}
window.result = window.parent.result;
try {
const executeFunction = ${script};
const data = executeFunction(window.result);
window.parent.preRequestComplete(data, null);
} catch (error) {
window.parent.preRequestComplete(null, error);
}
</script>`;
});
} 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
}
}
}
@@ -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 = `
<div class="modal fade" id="container_editor_modal" tabindex="-1" aria-labelledby="container_editor_modal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">컨테이너 정보</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="containerOption">
${LayoutUtil.createForm(this.containerEditorModel)}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary btn-sm" data-bs-dismiss="modal">닫기</button>
<button type="button" class="btn btn-primary btn-sm add-container">추가</button>
<button type="button" class="btn btn-primary btn-sm save-container">저장</button>
</div>
</div>
`;
$(this.rootContainer).parent().append(containerEditorPopup);
}
initDashboardEditor() {
const containerEditorPopup = `
<div class="modal fade" id="dashboard_editor_modal" tabindex="-1" aria-labelledby="dashboard_editor_modal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">대시보드 정보</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="containerOption">
${LayoutUtil.createForm(this.dashboardEditorModel)}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary btn-sm" data-bs-dismiss="modal">닫기</button>
<button type="button" class="btn btn-primary btn-sm save-dashboard">적용</button>
</div>
</div>
`;
$(this.rootContainer).parent().append(containerEditorPopup);
}
resetLayout() {
console.log('resetLayout');
$(this.rootContainer).empty();
this.renderRoot(this.layoutData, this.rootContainer);
this.dashboard.initCharts();
this.dashboard.layoutData = this.layoutData;
this.dashboard.drawCharts();
this.editContainerIndex = -1;
this.bindEvents();
}
addNewChart(containerIndex) {
this.editContainerIndex = containerIndex;
let tempChart = {
'contentId': '',
'type': 'chart',
'styles': {
width: '100%', height: '100%'
},
'config': {
'tooltip': {
'trigger': 'axis',
'show': true
},
'legend': {
'show': true
},
'grid': {
'show': true,
'top': 0,
'bottom': 0,
'left': 0,
'right': 0,
'containLabel': true
},
'title': {},
'xAxis': {
'show': true,
'type': 'category'
},
'yAxis': [
{
'show': true,
'name': '',
'type': 'value',
'position': 'left'
}],
'toolbox': {
'show': true
}
},
'seriesConfig': [
{
'type': 'line',
'index': '',
'queryType': 'elasticsearch',
'query': '',
'mapperX': `
function(result) {
let data = [];
return data;
}
`,
'mapperY': `
function(result) {
let data = [{
type : 'line',
data: []
}];
return data;
}
`,
'queryResult': ''
}
]
};
this.chartEditor.showChartEditor(true, tempChart, containerIndex);
}
editChart(editContainerIndex, editColumnIndex) {
console.log('edit-chart');
let tempChart = this.layoutData.children[editContainerIndex].children[editColumnIndex];
this.chartEditor.showChartEditor(false, JSON.parse(JSON.stringify(tempChart, LayoutUtil.echartReplacer)), editContainerIndex, editColumnIndex);
}
renderRoot(layoutData, parent) {
let classList = 'dashboard-root mt-2' + (!this.viewMode ? ' edit' : '');
let children = '';
let style = '';
if (layoutData.styles) {
style = Object.keys(layoutData.styles).map(key => `${key}: ${layoutData.styles[key]};`).join(' ');
}
console.log({style});
if (layoutData.children) {
layoutData.children.forEach((child) => {
if (child.type === 'container') {
children += this.renderContainer(child);
} else if (child.type === 'chart') {
children += this.renderChart(child);
}
});
}
let layoutItem = `<div class="${classList}" style="${style}"> ${children}</div>`;
$(parent).append(layoutItem);
// if (!this.viewMode) {
// this.traverse(this.layoutData);
// }
};
renderChart(layoutData) {
let style = '';
if (layoutData.styles) {
style = Object.keys(layoutData.styles).map(key => `${key}: ${layoutData.styles[key]};`).join(' ');
}
let editButton = this.viewMode ? '' : `<div class="chart-menu-icon" data-content-id="chart_${layoutData.contentId}">
<div class="dropdown">
<span class="" data-bs-toggle="dropdown" aria-expanded="false">⋮</span>
<ul class="dropdown-menu">
<li><span class="dropdown-item edit-chart">차트 편집</span></li>
<li><span class="dropdown-item remove-chart">차트 제거</span></li>
</ul>
</div>
</div>`;
return `
<div class="dashboard-chart" data-content-id="chart_${layoutData.contentId}" style="${style}">
${editButton}
<div id="chart_${layoutData.contentId}" style="width: 100%; height: 100%"></div>
</div>
`;
}
renderContainer(layoutData) {
console.log('renderContainer');
let children = '';
if (layoutData.children) {
layoutData.children.forEach(child => {
if (child.type === 'container') {
children += this.renderContainer(child);
} else if (child.type === 'chart') {
children += this.renderChart(child);
}
});
}
let width = layoutData.styles.width || '100%';
let height = layoutData.styles.height || '100%';
let editButton = this.viewMode ? '' : `
<div class="container-menu-icon">
<div class="dropdown">
<span><i class="fa-solid fa-arrows-up-down-left-right"></i></span>
<span class="" data-bs-toggle="dropdown" aria-expanded="false">⋮</span>
<ul class="dropdown-menu">
<li><span class="dropdown-item edit-container">컨테이너 편집</span></li>
<li><span class="dropdown-item remove-container">컨테이너 제거</span></li>
<li><span class="dropdown-item add-new-chart">차트 추가</span></li>
</ul>
</div>
</div>`;
let style = '';
if (layoutData.styles) {
style = Object.keys(layoutData.styles).map(key =>{
if (key === 'width' || key === 'height') return `${key}: 100%;`;
return `${key}: ${layoutData.styles[key]};`;
} ).join(' ');
}
return `<div class="dashboard-container" style="width: ${width}; height: ${height};">
<div class="handle">
${editButton}
</div>
<div class="dashboard-container-children" style="${style}">
${children}
</div>
</div>`;
}
addNewContainer(event) {
this.tempContainer = {
type: 'container',
styles: {
display: 'flex',
'flex-direction': 'row',
width: '100%',
height: '300px'
},
children: []
};
this.showContainerEditor(true);
}
editContainer(editContainerIndex) {
console.log('edit-container: ' + editContainerIndex);
this.tempContainer = JSON.parse(JSON.stringify(this.layoutData.children[editContainerIndex], LayoutUtil.echartReplacer));
this.showContainerEditor(false);
this.editContainerIndex = editContainerIndex;
}
removeContainer(containerIndex) {
console.log('edit-container: ' + containerIndex);
this.layoutData.children.splice(containerIndex, 1);
this.resetLayout();
}
addContainer(event) {
$('#container_editor_modal').find('input, select, textarea').each((index, element) => {
const fieldName = $(element).attr('name');
const newValue = LayoutUtil.getFieldValue(element);
if (fieldName !== '') {
_.set(this.tempContainer, fieldName, newValue);
}
});
this.layoutData.children.push(JSON.parse(JSON.stringify(this.tempContainer)));
this.tempContainer = null;
this.resetLayout();
$('#container_editor_modal').modal('hide');
}
saveContainer(event) {
console.log('save-container: ' + this.editContainerIndex);
$('#container_editor_modal').find('input, select, textarea').each((index, element) => {
const fieldName = $(element).attr('name');
const newValue = LayoutUtil.getFieldValue(element);
if (fieldName !== '') {
_.set(this.tempContainer, fieldName, newValue);
}
});
this.layoutData.children[this.editContainerIndex] = JSON.parse(JSON.stringify(this.tempContainer));
this.tempContainer = null;
this.resetLayout();
$('#container_editor_modal').modal('hide');
}
removeChart(containerIndex, columnIndex) {
this.layoutData.children[containerIndex].children.splice(columnIndex, 1);
this.resetLayout();
}
onRowsSorted(ui) {
let originalContainerIndex = ui.item.data('original_row_index');
this.updateLayoutDataRows(originalContainerIndex, ui);
}
onColumnsSorted(ui) {
let containerIndex = $('.dashboard-root').children('.dashboard-container').index(ui.item.closest('.dashboard-container'));
let originalContainerIndex = ui.item.data('original_row_index');
if (originalContainerIndex) {
if (originalContainerIndex === containerIndex) { // Check if the column moved from another row
this.updateLayoutDataWithinRow(ui.item);
} else { // The column was sorted within the same row
this.updateLayoutDataColumns(ui.item);
}
this.resetLayout();
}
}
updateLayoutDataRows(originalContainerIndex, ui) {
let targetIndex = $('.dashboard-root').children('.dashboard-container').index(ui.item.closest('.dashboard-container'));
if (targetIndex !== originalContainerIndex) {
this.layoutData.children.splice(targetIndex, 0, this.layoutData.children.splice(originalContainerIndex, 1)[0]);
}
console.log(this.layoutData);
}
updateLayoutDataWithinRow(movedItem) {
let oldContainerIndex = movedItem.data('original_row_index');
let oldIndex = movedItem.data('original_column_index');
if (oldContainerIndex !== -1 && oldIndex !== -1) {
let $row = movedItem.closest('.dashboard-container');
let newIndex = $row.children('.dashboard-chart').index(movedItem);
this.layoutData.children[oldContainerIndex].children.splice(newIndex, 0, this.layoutData.children[oldContainerIndex].children.splice(oldIndex, 1)[0]);
}
}
save() {
const jsonString = JSON.stringify(this.layoutData, LayoutUtil.echartReplacer);
return jsonString;
}
updateLayoutDataColumns(movedItem) {
// 새로운 행과 이전 행의 jQuery 요소를 가져옵니다
let $newRow = movedItem.closest('.dashboard-container');
// 이전 행의 레이아웃 데이터에서 이동된 행과 열의 인덱스를 찾습니다
let oldContainerIndex = movedItem.data('original_row_index');
let movedColumnIndex = movedItem.data('original_column_index');
if (oldContainerIndex !== -1 && movedColumnIndex !== undefined && movedColumnIndex !== -1) {
movedItem.data('original_row_index', -1);
movedItem.data('original_column_index', -1);
// 새 행의 인덱스를 찾습니다
let newContainerIndex = $('.dashboard-root').children('.dashboard-container').index($newRow);
// 이전 행에서 열 데이터를 추출합니다
let movedColumnData = this.layoutData.children[oldContainerIndex].children.splice(movedColumnIndex, 1)[0];
// 아이템이 드롭된 새 열 인덱스를 찾습니다
let newColumnIndex = movedItem.closest('.dashboard-container-children').children('.dashboard-chart').index(movedItem);
// 이동된 열 데이터를 새 행의 열에 새 인덱스에 삽입합니다
// 아이템이 마지막 위치로 이동된 경우 인덱스는 -1이 될 것이므로 이 경우를 처리합니다
if (newColumnIndex === -1) {
this.layoutData.children[newContainerIndex].children.push(movedColumnData);
} else {
this.layoutData.children[newContainerIndex].children.splice(newColumnIndex, 0, movedColumnData);
}
} else {
console.log('called twice on update');
}
}
showContainerEditor(isNew) {
if (isNew) {
$('.add-container').show();
$('.save-container').hide();
} else {
$('.add-container').hide();
$('.save-container').show();
}
let flatModel = LayoutUtil.flattenObject(JSON.parse(JSON.stringify(this.tempContainer)));
$('#container_editor_modal').find('input, select, textarea').each((index, element) => {
if ($(element).attr('type') === 'checkbox') {
$(element).prop('checked', false);
}
$(element).val('');
});
_.forEach(flatModel, (value, key) => {
if ($('#container_editor_modal').find(`input[name="${key}"]`).attr('type') === 'checkbox') {
$('#container_editor_modal').find(`input[name="${key}"]`).prop('checked', value);
} else {
$('#container_editor_modal').find(`input[name="${key}"], select[name="${key}"], textarea[name="${key}"]`).val(value);
}
});
$('#container_editor_modal').modal('show');
}
resize() {
console.log('resize');
this.dashboard.resizeAllCharts();
}
editDashboard(event) {
this.showDashboardEditor();
}
showDashboardEditor() {
let flatModel = LayoutUtil.flattenObject(JSON.parse(JSON.stringify(this.layoutData, LayoutUtil.echartReplacer)));
$('#dashboard_editor_modal').find('input, select, textarea').each((index, element) => {
if ($(element).attr('type') === 'checkbox') {
$(element).prop('checked', false);
}
$(element).val('');
});
_.forEach(flatModel, (value, key) => {
if ($('#dashboard_editor_modal').find(`input[name="${key}"]`).attr('type') === 'checkbox') {
$('#dashboard_editor_modal').find(`input[name="${key}"]`).prop('checked', value);
} else {
$('#dashboard_editor_modal').find(`input[name="${key}"], select[name="${key}"], textarea[name="${key}"]`).val(value);
}
});
$('#dashboard_editor_modal').modal('show');
}
saveDashboard(event) {
$('#dashboard_editor_modal').find('input, select, textarea').each((index, element) => {
const fieldName = $(element).attr('name');
const newValue = LayoutUtil.getFieldValue(element);
if (fieldName !== '') {
_.set(this.layoutData, fieldName, newValue);
}
});
console.log(this.layoutData);
this.resetLayout();
$('#dashboard_editor_modal').modal('hide');
}
bindEvents() {
$('.add-new-container').off('click').on('click', (event) => {
this.addNewContainer(event);
});
$('.edit-dashboard').off('click').on('click', (event) => {
this.editDashboard(event);
});
$('.save-dashboard').off('click').on('click', (event) => {
this.saveDashboard(event);
});
$('.remove-container').off('click').on('click', (event) => {
let containerIndex = $('.dashboard-root').children('.dashboard-container').index(event.target.closest('.dashboard-container'));
this.removeContainer(containerIndex);
});
$('.add-container').off('click').on('click', (event) => {
this.addContainer(event);
});
$('.save-container').off('click').on('click', (event) => {
this.saveContainer(event);
});
$('.edit-container').off('click').on('click', (event) => {
let editContainerIndex = $('.dashboard-root').children('.dashboard-container').index(event.target.closest('.dashboard-container'));
this.editContainer(editContainerIndex);
});
$('.edit-chart').off('click').on('click', (event) => {
let editContainerIndex = $('.dashboard-root').children('.dashboard-container').index(event.target.closest('.dashboard-container'));
let column = event.target.closest('.dashboard-chart');
let editColumnIndex = $(event.target.closest('.dashboard-container-children')).children('.dashboard-chart').index(column);
this.editChart(editContainerIndex, editColumnIndex);
});
$('.add-new-chart').off('click').on('click', (event) => {
let containerIndex = $('.dashboard-root').children('.dashboard-container').index(event.target.closest('.dashboard-container'));
this.addNewChart(containerIndex);
});
$('.remove-chart').off('click').on('click', (event) => {
let column = event.target.closest('.dashboard-chart');
let containerIndex = $('.dashboard-root').children('.dashboard-container').index(event.target.closest('.dashboard-container'));
let columnIndex = $(event.target.closest('.dashboard-container-children')).children('.dashboard-chart').index(column);
this.removeChart(containerIndex, columnIndex);
});
if ($('.dashboard-root').hasClass('ui-sortable')) {
$('.dashboard-root').sortable('destroy');
}
$('.dashboard-root').sortable({
handle: '.handle',
items: '.dashboard-container',
placeholder: 'ui-state-highlight',
start: (event, ui) => {
let originalRowIdx = $('.dashboard-root').children('.dashboard-container').index(ui.item.closest('.dashboard-container'));
ui.item.data('original_row_index', originalRowIdx);
}, update: (event, ui) => {
this.onRowsSorted(ui);
}
});
if ($('.dashboard-container').hasClass('ui-sortable')) {
$('.dashboard-container').sortable('destroy');
}
$('.dashboard-container-children').sortable({
// handle: ".handle",
placeholder: 'ui-state-highlight',
items: '.dashboard-chart',
connectWith: '.dashboard-container-children',
update: (event, ui) => {
this.onColumnsSorted(ui);
},
start: (event, ui) => {
let originalRowIdx = $('.dashboard-root').children('.dashboard-container').index(ui.item.closest('.dashboard-container'));
let originalColumnIdx = ui.item.closest('.dashboard-container-children').children('.dashboard-chart').index(ui.item);
ui.item.data('original_row_index', originalRowIdx);
ui.item.data('original_column_index', originalColumnIdx);
}
}).disableSelection();
}
}
+16
View File
@@ -321,6 +321,14 @@ function insertImageAsDataUri(file, $editor) {
reader.readAsDataURL(file);
}
/**
* 정규식 기반 정리를 적용할 HTML 최대 길이(문자).
* 아래 VML/네임스페이스 정리 정규식은 백트래킹으로 super-linear 가 될 수 있어(Sonar S5852)
* 비정상적으로 큰 입력에서는 정규식 단계를 건너뛴다. data-uri 이미지를 포함한 일반적인
* Office 붙여넣기는 이 한도 아래다.
*/
var MAX_REGEX_CLEAN_LENGTH = 500000;
/**
* 붙여넣기된 HTML 정리 (MS Office 등에서 복사한 내용)
* - 불필요한 Office 속성 제거
@@ -358,6 +366,14 @@ function cleanPastedHtml(html) {
// VML 태그 제거 (v:, o:, w: 등)
var cleanedHtml = $temp.html();
// 한도 초과 시 정규식 정리를 건너뛴다 — DOM 정리(이미지 속성 제거)는 이미 끝났으므로
// 붙여넣기 자체는 동작하고, Office 네임스페이스 잔여물만 남는다. (브라우저 멈춤 방지)
if (cleanedHtml.length > MAX_REGEX_CLEAN_LENGTH) {
console.warn('붙여넣기 HTML이 너무 커서 Office 태그 정리를 건너뜁니다. length=' + cleanedHtml.length);
return cleanedHtml;
}
cleanedHtml = cleanedHtml.replace(/<v:[^>]*>[\s\S]*?<\/v:[^>]*>/gi, '');
cleanedHtml = cleanedHtml.replace(/<o:[^>]*>[\s\S]*?<\/o:[^>]*>/gi, '');
cleanedHtml = cleanedHtml.replace(/<w:[^>]*>[\s\S]*?<\/w:[^>]*>/gi, '');
+60 -10
View File
@@ -12,7 +12,7 @@
var undefined;
/** Used as the semantic version number. */
var VERSION = '4.17.21';
var VERSION = '4.18.1';
/** Used as the size to enable large array optimizations. */
var LARGE_ARRAY_SIZE = 200;
@@ -20,7 +20,8 @@
/** Error message constants. */
var CORE_ERROR_TEXT = 'Unsupported core-js use. Try https://npms.io/search?q=ponyfill.',
FUNC_ERROR_TEXT = 'Expected a function',
INVALID_TEMPL_VAR_ERROR_TEXT = 'Invalid `variable` option passed into `_.template`';
INVALID_TEMPL_VAR_ERROR_TEXT = 'Invalid `variable` option passed into `_.template`',
INVALID_TEMPL_IMPORTS_ERROR_TEXT = 'Invalid `imports` option passed into `_.template`';
/** Used to stand-in for `undefined` hash values. */
var HASH_UNDEFINED = '__lodash_hash_undefined__';
@@ -1752,6 +1753,10 @@
* embedded Ruby (ERB) as well as ES2015 template strings. Change the
* following template settings to use alternative delimiters.
*
* **Security:** See
* [threat model](https://github.com/lodash/lodash/blob/main/threat-model.md)
* `_.template` is insecure and will be removed in v5.
*
* @static
* @memberOf _
* @type {Object}
@@ -2300,7 +2305,7 @@
* @name has
* @memberOf SetCache
* @param {*} value The value to search for.
* @returns {number} Returns `true` if `value` is found, else `false`.
* @returns {boolean} Returns `true` if `value` is found, else `false`.
*/
function setCacheHas(value) {
return this.__data__.has(value);
@@ -3766,7 +3771,7 @@
if (isArray(iteratee)) {
return function(value) {
return baseGet(value, iteratee.length === 1 ? iteratee[0] : iteratee);
}
};
}
return iteratee;
});
@@ -4370,8 +4375,34 @@
*/
function baseUnset(object, path) {
path = castPath(path, object);
object = parent(object, path);
return object == null || delete object[toKey(last(path))];
// Prevent prototype pollution:
// https://github.com/lodash/lodash/security/advisories/GHSA-xxjr-mmjv-4gpg
// https://github.com/lodash/lodash/security/advisories/GHSA-f23m-r3pf-42rh
var index = -1,
length = path.length;
if (!length) {
return true;
}
while (++index < length) {
var key = toKey(path[index]);
// Always block "__proto__" anywhere in the path if it's not expected
if (key === '__proto__' && !hasOwnProperty.call(object, '__proto__')) {
return false;
}
// Block constructor/prototype as non-terminal traversal keys to prevent
// escaping the object graph into built-in constructors and prototypes.
if ((key === 'constructor' || key === 'prototype') && index < length - 1) {
return false;
}
}
var obj = parent(object, path);
return obj == null || delete obj[toKey(last(path))];
}
/**
@@ -6922,7 +6953,7 @@
/**
* Creates an array with all falsey values removed. The values `false`, `null`,
* `0`, `""`, `undefined`, and `NaN` are falsey.
* `0`, `-0`, `0n`, `""`, `undefined`, and `NaN` are falsy.
*
* @static
* @memberOf _
@@ -7461,7 +7492,7 @@
while (++index < length) {
var pair = pairs[index];
result[pair[0]] = pair[1];
baseAssignValue(result, pair[0], pair[1]);
}
return result;
}
@@ -14121,6 +14152,8 @@
* **Note:** JavaScript follows the IEEE-754 standard for resolving
* floating-point values which can produce unexpected results.
*
* **Note:** If `lower` is greater than `upper`, the values are swapped.
*
* @static
* @memberOf _
* @since 0.7.0
@@ -14134,9 +14167,16 @@
* _.random(0, 5);
* // => an integer between 0 and 5
*
* // when lower is greater than upper the values are swapped
* _.random(5, 0);
* // => an integer between 0 and 5
*
* _.random(5);
* // => also an integer between 0 and 5
*
* _.random(-5);
* // => an integer between -5 and 0
*
* _.random(5, true);
* // => a floating-point number between 0 and 5
*
@@ -14738,6 +14778,10 @@
* properties may be accessed as free variables in the template. If a setting
* object is given, it takes precedence over `_.templateSettings` values.
*
* **Security:** `_.template` is insecure and should not be used. It will be
* removed in Lodash v5. Avoid untrusted input. See
* [threat model](https://github.com/lodash/lodash/blob/main/threat-model.md).
*
* **Note:** In the development build `_.template` utilizes
* [sourceURLs](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl)
* for easier debugging.
@@ -14845,12 +14889,18 @@
options = undefined;
}
string = toString(string);
options = assignInWith({}, options, settings, customDefaultsAssignIn);
options = assignWith({}, options, settings, customDefaultsAssignIn);
var imports = assignInWith({}, options.imports, settings.imports, customDefaultsAssignIn),
var imports = assignWith({}, options.imports, settings.imports, customDefaultsAssignIn),
importsKeys = keys(imports),
importsValues = baseValues(imports, importsKeys);
arrayEach(importsKeys, function(key) {
if (reForbiddenIdentifierChars.test(key)) {
throw new Error(INVALID_TEMPL_IMPORTS_ERROR_TEXT);
}
});
var isEscaping,
isEvaluating,
index = 0,
File diff suppressed because one or more lines are too long
@@ -1,173 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* AUTO-GENERATED FILE. DO NOT MODIFY.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports);
} else {
// Browser globals
factory({});
}
})(this, function(exports) {
/**
* Language: English.
*/
var localeObj = {
time: {
month: [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
],
monthAbbr: [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
],
dayOfWeek: [
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
],
dayOfWeekAbbr: [
'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'
]
},
legend: {
selector: {
all: 'All',
inverse: 'Inv'
}
},
toolbox: {
brush: {
title: {
rect: 'Box Select',
polygon: 'Lasso Select',
lineX: 'Horizontally Select',
lineY: 'Vertically Select',
keep: 'Keep Selections',
clear: 'Clear Selections'
}
},
dataView: {
title: 'Data View',
lang: ['Data View', 'Close', 'Refresh']
},
dataZoom: {
title: {
zoom: 'Zoom',
back: 'Zoom Reset'
}
},
magicType: {
title: {
line: 'Switch to Line Chart',
bar: 'Switch to Bar Chart',
stack: 'Stack',
tiled: 'Tile'
}
},
restore: {
title: 'Restore'
},
saveAsImage: {
title: 'Save as Image',
lang: ['Right Click to Save Image']
}
},
series: {
typeNames: {
pie: 'Pie chart',
bar: 'Bar chart',
line: 'Line chart',
scatter: 'Scatter plot',
effectScatter: 'Ripple scatter plot',
radar: 'Radar chart',
tree: 'Tree',
treemap: 'Treemap',
boxplot: 'Boxplot',
candlestick: 'Candlestick',
k: 'K line chart',
heatmap: 'Heat map',
map: 'Map',
parallel: 'Parallel coordinate map',
lines: 'Line graph',
graph: 'Relationship graph',
sankey: 'Sankey diagram',
funnel: 'Funnel chart',
gauge: 'Gauge',
pictorialBar: 'Pictorial bar',
themeRiver: 'Theme River Map',
sunburst: 'Sunburst'
}
},
aria: {
general: {
withTitle: 'This is a chart about "{title}"',
withoutTitle: 'This is a chart'
},
series: {
single: {
prefix: '',
withName: ' with type {seriesType} named {seriesName}.',
withoutName: ' with type {seriesType}.'
},
multiple: {
prefix: '. It consists of {seriesCount} series count.',
withName: ' The {seriesId} series is a {seriesType} representing {seriesName}.',
withoutName: ' The {seriesId} series is a {seriesType}.',
separator: {
middle: '',
end: ''
}
}
},
data: {
allData: 'The data is as follows: ',
partialData: 'The first {displayCnt} items are: ',
withName: 'the data for {name} is {value}',
withoutName: '{value}',
separator: {
middle: ', ',
end: '. '
}
}
}
};
for (var key in localeObj) {
if (localeObj.hasOwnProperty(key)) {
exports[key] = localeObj[key];
}
}
});
@@ -1,169 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* AUTO-GENERATED FILE. DO NOT MODIFY.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
/**
* Language: English.
*/
var localeObj = {
time: {
month: [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
],
monthAbbr: [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
],
dayOfWeek: [
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
],
dayOfWeekAbbr: [
'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'
]
},
legend: {
selector: {
all: 'All',
inverse: 'Inv'
}
},
toolbox: {
brush: {
title: {
rect: 'Box Select',
polygon: 'Lasso Select',
lineX: 'Horizontally Select',
lineY: 'Vertically Select',
keep: 'Keep Selections',
clear: 'Clear Selections'
}
},
dataView: {
title: 'Data View',
lang: ['Data View', 'Close', 'Refresh']
},
dataZoom: {
title: {
zoom: 'Zoom',
back: 'Zoom Reset'
}
},
magicType: {
title: {
line: 'Switch to Line Chart',
bar: 'Switch to Bar Chart',
stack: 'Stack',
tiled: 'Tile'
}
},
restore: {
title: 'Restore'
},
saveAsImage: {
title: 'Save as Image',
lang: ['Right Click to Save Image']
}
},
series: {
typeNames: {
pie: 'Pie chart',
bar: 'Bar chart',
line: 'Line chart',
scatter: 'Scatter plot',
effectScatter: 'Ripple scatter plot',
radar: 'Radar chart',
tree: 'Tree',
treemap: 'Treemap',
boxplot: 'Boxplot',
candlestick: 'Candlestick',
k: 'K line chart',
heatmap: 'Heat map',
map: 'Map',
parallel: 'Parallel coordinate map',
lines: 'Line graph',
graph: 'Relationship graph',
sankey: 'Sankey diagram',
funnel: 'Funnel chart',
gauge: 'Gauge',
pictorialBar: 'Pictorial bar',
themeRiver: 'Theme River Map',
sunburst: 'Sunburst'
}
},
aria: {
general: {
withTitle: 'This is a chart about "{title}"',
withoutTitle: 'This is a chart'
},
series: {
single: {
prefix: '',
withName: ' with type {seriesType} named {seriesName}.',
withoutName: ' with type {seriesType}.'
},
multiple: {
prefix: '. It consists of {seriesCount} series count.',
withName: ' The {seriesId} series is a {seriesType} representing {seriesName}.',
withoutName: ' The {seriesId} series is a {seriesType}.',
separator: {
middle: '',
end: ''
}
}
},
data: {
allData: 'The data is as follows: ',
partialData: 'The first {displayCnt} items are: ',
withName: 'the data for {name} is {value}',
withoutName: '{value}',
separator: {
middle: ', ',
end: '. '
}
}
}
};
echarts.registerLocale('EN', localeObj);
});
@@ -1,173 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* AUTO-GENERATED FILE. DO NOT MODIFY.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports);
} else {
// Browser globals
factory({});
}
})(this, function(exports) {
/**
* Language: Korean.
*/
var localeObj = {
time: {
month: [
'1월', '2월', '3월', '4월', '5월', '6월',
'7월', '8월', '9월', '10월', '11월', '12월'
],
monthAbbr: [
'1월', '2월', '3월', '4월', '5월', '6월',
'7월', '8월', '9월', '10월', '11월', '12월'
],
dayOfWeek: [
'일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'
],
dayOfWeekAbbr: [
'일', '월', '화', '수', '목', '금', '토'
]
},
legend: {
selector: {
all: '모두 선택',
inverse: '선택 범위 반전'
}
},
toolbox: {
brush: {
title: {
rect: '사각형 선택',
polygon: '올가미 선택',
lineX: '수평 선택',
lineY: '수직 선택',
keep: '선택 유지',
clear: '선택 지우기'
}
},
dataView: {
title: '날짜 보기',
lang: ['날짜 보기', '닫기', '새로 고침']
},
dataZoom: {
title: {
zoom: '확대/축소',
back: '확대/축소 초기화'
}
},
magicType: {
title: {
line: '꺽은선 그래프로 변경',
bar: '막대 그래프로 변경',
stack: '스택',
tiled: '타일'
}
},
restore: {
title: '복구'
},
saveAsImage: {
title: '이미지로 저장',
lang: ['이미지를 저장하려면 마우스 오른쪽 버튼을 클릭하세요.']
}
},
series: {
typeNames: {
pie: '원 그래프',
bar: '막대 그래프',
line: '꺽은선 그래프',
scatter: '산점도',
effectScatter: '물결 효과 산점도',
radar: '방사형 그래프',
tree: '트리',
treemap: '트리맵',
boxplot: '상자 수염 그래프',
candlestick: '캔들스틱 차트',
k: 'K 라인 차트',
heatmap: '히트 맵',
map: '지도',
parallel: '평행 좌표 맵',
lines: '선',
graph: '관계 그래프',
sankey: '산키 다이어그램',
funnel: '깔때기형 그래프',
gauge: '계기',
pictorialBar: '픽토그램 차트',
themeRiver: '스트림 그래프',
sunburst: '선버스트 차트'
}
},
aria: {
general: {
withTitle: '"{title}"에 대한 차트입니다.',
withoutTitle: '차트입니다.'
},
series: {
single: {
prefix: '',
withName: ' 차트 유형은 {seriesType}이며 {seriesName}을 표시합니다.',
withoutName: ' 차트 유형은 {seriesType}입니다.'
},
multiple: {
prefix: '. {seriesCount} 하나의 차트 시리즈로 구성됩니다.',
withName: ' {seriesId}번째 시리즈는 {seriesName}을 나타내는 {seriesType} representing.',
withoutName: ' {seriesId}번째 시리즈는 {seriesType}입니다.',
separator: {
middle: '',
end: ''
}
}
},
data: {
allData: '데이터: ',
partialData: '첫번째 {displayCnt} 아이템: ',
withName: '{name}의 데이터는 {value}',
withoutName: '{value}',
separator: {
middle: ', ',
end: '. '
}
}
}
};
for (var key in localeObj) {
if (localeObj.hasOwnProperty(key)) {
exports[key] = localeObj[key];
}
}
});
@@ -1,169 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* AUTO-GENERATED FILE. DO NOT MODIFY.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
/**
* Language: Korean.
*/
var localeObj = {
time: {
month: [
'1월', '2월', '3월', '4월', '5월', '6월',
'7월', '8월', '9월', '10월', '11월', '12월'
],
monthAbbr: [
'1월', '2월', '3월', '4월', '5월', '6월',
'7월', '8월', '9월', '10월', '11월', '12월'
],
dayOfWeek: [
'일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'
],
dayOfWeekAbbr: [
'일', '월', '화', '수', '목', '금', '토'
]
},
legend: {
selector: {
all: '모두 선택',
inverse: '선택 범위 반전'
}
},
toolbox: {
brush: {
title: {
rect: '사각형 선택',
polygon: '올가미 선택',
lineX: '수평 선택',
lineY: '수직 선택',
keep: '선택 유지',
clear: '선택 지우기'
}
},
dataView: {
title: '날짜 보기',
lang: ['날짜 보기', '닫기', '새로 고침']
},
dataZoom: {
title: {
zoom: '확대/축소',
back: '확대/축소 초기화'
}
},
magicType: {
title: {
line: '꺽은선 그래프로 변경',
bar: '막대 그래프로 변경',
stack: '스택',
tiled: '타일'
}
},
restore: {
title: '복구'
},
saveAsImage: {
title: '이미지로 저장',
lang: ['이미지를 저장하려면 마우스 오른쪽 버튼을 클릭하세요.']
}
},
series: {
typeNames: {
pie: '원 그래프',
bar: '막대 그래프',
line: '꺽은선 그래프',
scatter: '산점도',
effectScatter: '물결 효과 산점도',
radar: '방사형 그래프',
tree: '트리',
treemap: '트리맵',
boxplot: '상자 수염 그래프',
candlestick: '캔들스틱 차트',
k: 'K 라인 차트',
heatmap: '히트 맵',
map: '지도',
parallel: '평행 좌표 맵',
lines: '선',
graph: '관계 그래프',
sankey: '산키 다이어그램',
funnel: '깔때기형 그래프',
gauge: '계기',
pictorialBar: '픽토그램 차트',
themeRiver: '스트림 그래프',
sunburst: '선버스트 차트'
}
},
aria: {
general: {
withTitle: '"{title}"에 대한 차트입니다.',
withoutTitle: '차트입니다.'
},
series: {
single: {
prefix: '',
withName: ' 차트 유형은 {seriesType}이며 {seriesName}을 표시합니다.',
withoutName: ' 차트 유형은 {seriesType}입니다.'
},
multiple: {
prefix: '. {seriesCount} 하나의 차트 시리즈로 구성됩니다.',
withName: ' {seriesId}번째 시리즈는 {seriesName}을 나타내는 {seriesType} representing.',
withoutName: ' {seriesId}번째 시리즈는 {seriesType}입니다.',
separator: {
middle: '',
end: ''
}
}
},
data: {
allData: '데이터: ',
partialData: '첫번째 {displayCnt} 아이템: ',
withName: '{name}의 데이터는 {value}',
withoutName: '{value}',
separator: {
middle: ', ',
end: '. '
}
}
}
};
echarts.registerLocale('KO', localeObj);
});
@@ -1,163 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#f2385a',
'#f5a503',
'#4ad9d9',
'#f7879c',
'#c1d7a8',
'#4dffd2',
'#fccfd7',
'#d5f6f6'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#f2385a'
}
},
visualMap: {
color: ['#f2385a', '#f5a503']
},
toolbox: {
color: ['#f2385a', '#f2385a', '#f2385a', '#f2385a']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#f2385a',
type: 'dashed'
},
crossStyle: {
color: '#f2385a'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#f2385a' // Handle color
},
timeline: {
lineStyle: {
color: '#f2385a'
},
controlStyle: {
color: '#f2385a',
borderColor: '#f2385a'
}
},
candlestick: {
itemStyle: {
color: '#f2385a',
color0: '#f5a503'
},
lineStyle: {
width: 1,
color: '#f2385a',
color0: '#f5a503'
},
areaStyle: {
color: '#c1d7a8',
color0: '#4ad9d9'
}
},
map: {
itemStyle: {
color: '#f2385a'
},
areaStyle: {
color: '#ddd'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#f2385a'
},
linkStyle: {
color: '#f2385a'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#f5a503'],
[0.8, '#f2385a'],
[1, '#c1d7a8']
],
width: 8
}
}
}
};
echarts.registerTheme('azul', theme);
});
@@ -1,178 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#001727',
'#805500',
'#ffff00',
'#ffd11a',
'#f2d71f',
'#f2be19',
'#f3a81a',
'#fff5cc'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#001727'
}
},
visualMap: {
color: ['#001727', '#805500']
},
toolbox: {
color: ['#001727', '#001727', '#001727', '#001727']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#001727',
type: 'dashed'
},
crossStyle: {
color: '#001727'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#001727' // Handle color
},
timeline: {
lineStyle: {
color: '#001727'
},
controlStyle: {
color: '#001727',
borderColor: '#001727'
}
},
candlestick: {
itemStyle: {
color: '#f3a81a',
color0: '#ffff00'
},
lineStyle: {
width: 1,
color: '#ffff00',
color0: '#f3a81a'
},
areaStyle: {
color: '#805500',
color0: '#ffff00'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#f3a81a',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#805500'
}
},
map: {
itemStyle: {
color: '#ffd11a'
},
areaStyle: {
color: '#f2be19'
},
label: {
color: '#ffd11a'
}
},
graph: {
itemStyle: {
color: '#001727'
},
linkStyle: {
color: '#001727'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#f2d71f'],
[0.8, '#001727'],
[1, '#ffff00']
],
width: 8
}
}
}
};
echarts.registerTheme('bee-inspired', theme);
});
@@ -1,178 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#1790cf',
'#1bb2d8',
'#99d2dd',
'#88b0bb',
'#1c7099',
'#038cc4',
'#75abd0',
'#afd6dd'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#1790cf'
}
},
visualMap: {
color: ['#1790cf', '#a2d4e6']
},
toolbox: {
color: ['#1790cf', '#1790cf', '#1790cf', '#1790cf']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#1790cf',
type: 'dashed'
},
crossStyle: {
color: '#1790cf'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(144,197,237,0.2)', // Fill the color
handleColor: '#1790cf' // Handle color
},
timeline: {
lineStyle: {
color: '#1790cfa'
},
controlStyle: {
color: '#1790cf',
borderColor: '#1790cf'
}
},
candlestick: {
itemStyle: {
color: '#1bb2d8',
color0: '#99d2dd'
},
lineStyle: {
width: 1,
color: '#1c7099',
color0: '#88b0bb'
},
areaStyle: {
color: '#1790cf',
color0: '#1bb2d8'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#1bb2d8',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#1790cf'
}
},
graph: {
itemStyle: {
color: '#1bb2d8'
},
linkStyle: {
color: '#88b0bb'
}
},
map: {
itemStyle: {
color: '#ddd'
},
areaStyle: {
color: '99d2dd'
},
label: {
color: '#c12e34'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#1bb2d8'],
[0.8, '#1790cf'],
[1, '#1c7099']
],
width: 8
}
}
}
};
echarts.registerTheme('blue', theme);
});
@@ -1,178 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#fad089',
'#ff9c5b',
'#f5634a',
'#ed303c',
'#3b8183',
'#f7826e',
'#faac9e',
'#fcd5cf'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#fad089'
}
},
visualMap: {
color: ['#fad089', '#a2d4e6']
},
toolbox: {
color: ['#fad089', '#fad089', '#fad089', '#fad089']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#fad089',
type: 'dashed'
},
crossStyle: {
color: '#fad089'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(144,197,237,0.2)', // Fill the color
handleColor: '#fad089' // Handle color
},
timeline: {
lineStyle: {
color: '#fad089'
},
controlStyle: {
color: '#fad089',
borderColor: '#fad089'
}
},
candlestick: {
itemStyle: {
color: '#ff9c5b',
color0: '#f5634a'
},
lineStyle: {
width: 1,
color: '#3b8183',
color0: '#ed303c'
},
areaStyle: {
color: '#fad089',
color0: '#ed303c'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#fad089',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#ed303c'
}
},
map: {
itemStyle: {
color: '#ddd'
},
areaStyle: {
color: '#f5634a'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#f5634a'
},
linkStyle: {
color: '#fad089'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#ff9c5b'],
[0.8, '#fad089'],
[1, '#3b8183']
],
width: 8
}
}
}
};
echarts.registerTheme('caravan', theme);
});
@@ -1,163 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#f0d8A8',
'#3d1c00',
'#86b8b1',
'#f2d694',
'#fa2a00',
'#ff8066',
'#ffd5cc',
'#f9edd2'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#f0d8A8'
}
},
visualMap: {
color: ['#f0d8A8', '#3d1c00']
},
toolbox: {
color: ['#f0d8A8', '#f0d8A8', '#f0d8A8', '#f0d8A8']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#f0d8A8',
type: 'dashed'
},
crossStyle: {
color: '#f0d8A8'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#f0d8A8' // Handle color
},
timeline: {
lineStyle: {
color: '#f0dba8'
},
controlStyle: {
color: '#f0dba8',
borderColor: '#f0dba8'
}
},
candlestick: {
itemStyle: {
color: '#3d1c00',
color0: '#86b8b1'
},
lineStyle: {
width: 1,
color: '#fa2a00',
color0: '#f2d694'
},
areaStyle: {
color: '#f0d8A8',
color0: '#86b8b1'
}
},
map: {
itemStyle: {
color: '#ddd'
},
areaStyle: {
color: '#86b8b1'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#3d1c00'
},
linkStyle: {
color: '#f0d8A8'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#3d1c00'],
[0.8, '#f0d8A8'],
[1, '#fa2a00']
],
width: 8
}
}
}
};
echarts.registerTheme('carp', theme);
});
@@ -1,180 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#b21ab4',
'#6f0099',
'#2a2073',
'#0b5ea8',
'#17aecc',
'#b3b3ff',
'#eb99ff',
'#fae6ff',
'#e6f2ff',
'#eeeeee'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#00aecd'
}
},
visualMap: {
color: ['#00aecd', '#a2d4e6']
},
toolbox: {
color: ['#00aecd', '#00aecd', '#00aecd', '#00aecd']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#00aecd',
type: 'dashed'
},
crossStyle: {
color: '#00aecd'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(144,197,237,0.2)', // Fill the color
handleColor: '#00aecd' // Handle color
},
timeline: {
lineStyle: {
color: '#00aecd'
},
controlStyle: {
color: '#00aecd',
borderColor: '00aecd'
}
},
candlestick: {
itemStyle: {
color: '#00aecd',
color0: '#a2d4e6'
},
lineStyle: {
width: 1,
color: '#00aecd',
color0: '#a2d4e6'
},
areaStyle: {
color: '#b21ab4',
color0: '#0b5ea8'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#b21ab4',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#0b5ea8'
}
},
graph: {
itemStyle: {
color: '#b21ab4'
},
linkStyle: {
color: '#2a2073'
}
},
map: {
itemStyle: {
color: '#c12e34'
},
areaStyle: {
color: '#ddd'
},
label: {
color: '#c12e34'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#dddddd'],
[0.8, '#00aecd'],
[1, '#f5ccff']
],
width: 8
}
}
}
};
echarts.registerTheme('cool', theme);
});
@@ -1,164 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var contrastColor = '#eee';
var axisCommon = function() {
return {
axisLine: {
lineStyle: {
color: contrastColor
}
},
axisTick: {
lineStyle: {
color: contrastColor
}
},
axisLabel: {
color: contrastColor
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#aaa'
}
},
splitArea: {
areaStyle: {
color: contrastColor
}
}
};
};
var colorPalette = [
'#00305a',
'#004b8d',
'#0074d9',
'#4192d9',
'#7abaf2',
'#99cce6',
'#d6ebf5',
'#eeeeee'
];
var theme = {
color: colorPalette,
backgroundColor: '#333',
tooltip: {
axisPointer: {
lineStyle: {
color: contrastColor
},
crossStyle: {
color: contrastColor
}
}
},
legend: {
textStyle: {
color: contrastColor
}
},
title: {
textStyle: {
color: contrastColor
}
},
toolbox: {
iconStyle: {
borderColor: contrastColor
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#00305a' // Handle color
},
timeline: {
itemStyle: {
color: colorPalette[1]
},
lineStyle: {
color: contrastColor
},
controlStyle: {
color: contrastColor,
borderColor: contrastColor
},
label: {
color: contrastColor
}
},
timeAxis: axisCommon(),
logAxis: axisCommon(),
valueAxis: axisCommon(),
categoryAxis: axisCommon(),
line: {
symbol: 'circle'
},
graph: {
color: colorPalette
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#004b8d'],
[0.8, '#00305a'],
[1, '#7abaf2']
],
width: 8
}
}
}
};
theme.categoryAxis.splitLine.show = false;
echarts.registerTheme('dark-blue', theme);
});
@@ -1,164 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var contrastColor = '#eee';
var axisCommon = function() {
return {
axisLine: {
lineStyle: {
color: contrastColor
}
},
axisTick: {
lineStyle: {
color: contrastColor
}
},
axisLabel: {
color: contrastColor
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#aaa'
}
},
splitArea: {
areaStyle: {
color: contrastColor
}
}
};
};
var colorPalette = [
'#458c6b',
'#f2da87',
'#d9a86c',
'#d94436',
'#a62424',
'#76bc9b',
'#cce6da',
'#eeeeee'
];
var theme = {
color: colorPalette,
backgroundColor: '#333',
tooltip: {
axisPointer: {
lineStyle: {
color: contrastColor
},
crossStyle: {
color: contrastColor
}
}
},
legend: {
textStyle: {
color: contrastColor
}
},
title: {
textStyle: {
color: contrastColor
}
},
toolbox: {
iconStyle: {
borderColor: contrastColor
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#458c6b' // Handle color
},
timeline: {
itemStyle: {
color: colorPalette[1]
},
lineStyle: {
color: contrastColor
},
controlStyle: {
color: contrastColor,
borderColor: contrastColor
},
label: {
color: contrastColor
}
},
timeAxis: axisCommon(),
logAxis: axisCommon(),
valueAxis: axisCommon(),
categoryAxis: axisCommon(),
line: {
symbol: 'circle'
},
graph: {
color: colorPalette
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#f2da87'],
[0.8, '#458c6b'],
[1, '#a62424']
],
width: 8
}
}
}
};
theme.categoryAxis.splitLine.show = false;
echarts.registerTheme('dark-bold', theme);
});
@@ -1,164 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var contrastColor = '#eee';
var axisCommon = function() {
return {
axisLine: {
lineStyle: {
color: contrastColor
}
},
axisTick: {
lineStyle: {
color: contrastColor
}
},
axisLabel: {
color: contrastColor
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#aaa'
}
},
splitArea: {
areaStyle: {
color: contrastColor
}
}
};
};
var colorPalette = [
'#52656b',
'#ff3b77',
'#a3cc00',
'#ffffff',
'#b8b89f',
'#ffccdb',
'#e5ff80',
'#f4f4f0'
];
var theme = {
color: colorPalette,
backgroundColor: '#333',
tooltip: {
axisPointer: {
lineStyle: {
color: contrastColor
},
crossStyle: {
color: contrastColor
}
}
},
legend: {
textStyle: {
color: contrastColor
}
},
title: {
textStyle: {
color: contrastColor
}
},
toolbox: {
iconStyle: {
borderColor: contrastColor
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#52656b' // Handle color
},
timeline: {
itemStyle: {
color: colorPalette[1]
},
lineStyle: {
color: contrastColor
},
controlStyle: {
color: contrastColor,
borderColor: contrastColor
},
label: {
color: contrastColor
}
},
timeAxis: axisCommon(),
logAxis: axisCommon(),
valueAxis: axisCommon(),
categoryAxis: axisCommon(),
line: {
symbol: 'circle'
},
graph: {
color: colorPalette
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#ff3b77'],
[0.8, '#52656b'],
[1, '#b8b89f']
],
width: 8
}
}
}
};
theme.categoryAxis.splitLine.show = false;
echarts.registerTheme('dark-digerati', theme);
});
@@ -1,164 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var contrastColor = '#eee';
var axisCommon = function() {
return {
axisLine: {
lineStyle: {
color: contrastColor
}
},
axisTick: {
lineStyle: {
color: contrastColor
}
},
axisLabel: {
color: contrastColor
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#aaa'
}
},
splitArea: {
areaStyle: {
color: contrastColor
}
}
};
};
var colorPalette = [
'#00a8c6',
'#40c0cb',
'#ebd3ad',
'#aee239',
'#8fbe00',
'#33e0ff',
'#b3f4ff',
'#e6ff99'
];
var theme = {
color: colorPalette,
backgroundColor: '#333',
tooltip: {
axisPointer: {
lineStyle: {
color: contrastColor
},
crossStyle: {
color: contrastColor
}
}
},
legend: {
textStyle: {
color: contrastColor
}
},
title: {
textStyle: {
color: contrastColor
}
},
toolbox: {
iconStyle: {
borderColor: contrastColor
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#00a8c6' // Handle color
},
timeline: {
itemStyle: {
color: colorPalette[1]
},
lineStyle: {
color: contrastColor
},
controlStyle: {
color: contrastColor,
borderColor: contrastColor
},
label: {
color: contrastColor
}
},
timeAxis: axisCommon(),
logAxis: axisCommon(),
valueAxis: axisCommon(),
categoryAxis: axisCommon(),
line: {
symbol: 'circle'
},
graph: {
color: colorPalette
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#40c0cb'],
[0.8, '#00a8c6'],
[1, '#8fbe00']
],
width: 8
}
}
}
};
theme.categoryAxis.splitLine.show = false;
echarts.registerTheme('dark-fresh-cut', theme);
});
@@ -1,164 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var contrastColor = '#eee';
var axisCommon = function() {
return {
axisLine: {
lineStyle: {
color: contrastColor
}
},
axisTick: {
lineStyle: {
color: contrastColor
}
},
axisLabel: {
color: contrastColor
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#aaa'
}
},
splitArea: {
areaStyle: {
color: contrastColor
}
}
};
};
var colorPalette = [
'#cc0e00',
'#ff1a0a',
'#ff8880',
'#ffc180',
'#ffc2b0',
'#ffffff',
'#ff8880',
'#ffe6e6'
];
var theme = {
color: colorPalette,
backgroundColor: '#333',
tooltip: {
axisPointer: {
lineStyle: {
color: contrastColor
},
crossStyle: {
color: contrastColor
}
}
},
legend: {
textStyle: {
color: contrastColor
}
},
title: {
textStyle: {
color: contrastColor
}
},
toolbox: {
iconStyle: {
borderColor: contrastColor
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#cc0e00' // Handle color
},
timeline: {
itemStyle: {
color: colorPalette[1]
},
lineStyle: {
color: contrastColor
},
controlStyle: {
color: contrastColor,
borderColor: contrastColor
},
label: {
color: contrastColor
}
},
timeAxis: axisCommon(),
logAxis: axisCommon(),
valueAxis: axisCommon(),
categoryAxis: axisCommon(),
line: {
symbol: 'circle'
},
graph: {
color: colorPalette
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#ff1a0a'],
[0.8, '#cc0e00'],
[1, '#ffc2b0']
],
width: 8
}
}
}
};
theme.categoryAxis.splitLine.show = false;
echarts.registerTheme('dark-mushroom', theme);
});
@@ -1,224 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var contrastColor = '#B9B8CE';
var backgroundColor = '#100C2A';
var axisCommon = function () {
return {
axisLine: {
lineStyle: {
color: contrastColor
}
},
splitLine: {
lineStyle: {
color: '#484753'
}
},
splitArea: {
areaStyle: {
color: ['rgba(255,255,255,0.02)', 'rgba(255,255,255,0.05)']
}
},
minorSplitLine: {
lineStyle: {
color: '#20203B'
}
}
};
};
var colorPalette = [
'#4992ff',
'#7cffb2',
'#fddd60',
'#ff6e76',
'#58d9f9',
'#05c091',
'#ff8a45',
'#8d48e3',
'#dd79ff'
];
var theme = {
darkMode: true,
color: colorPalette,
backgroundColor: backgroundColor,
axisPointer: {
lineStyle: {
color: '#817f91'
},
crossStyle: {
color: '#817f91'
},
label: {
// TODO Contrast of label backgorundColor
color: '#fff'
}
},
legend: {
textStyle: {
color: contrastColor
}
},
textStyle: {
color: contrastColor
},
title: {
textStyle: {
color: '#EEF1FA'
},
subtextStyle: {
color: '#B9B8CE'
}
},
toolbox: {
iconStyle: {
borderColor: contrastColor
}
},
dataZoom: {
borderColor: '#71708A',
textStyle: {
color: contrastColor
},
brushStyle: {
color: 'rgba(135,163,206,0.3)'
},
handleStyle: {
color: '#353450',
borderColor: '#C5CBE3'
},
moveHandleStyle: {
color: '#B0B6C3',
opacity: 0.3
},
fillerColor: 'rgba(135,163,206,0.2)',
emphasis: {
handleStyle: {
borderColor: '#91B7F2',
color: '#4D587D'
},
moveHandleStyle: {
color: '#636D9A',
opacity: 0.7
}
},
dataBackground: {
lineStyle: {
color: '#71708A',
width: 1
},
areaStyle: {
color: '#71708A'
}
},
selectedDataBackground: {
lineStyle: {
color: '#87A3CE'
},
areaStyle: {
color: '#87A3CE'
}
}
},
visualMap: {
textStyle: {
color: contrastColor
}
},
timeline: {
lineStyle: {
color: contrastColor
},
label: {
color: contrastColor
},
controlStyle: {
color: contrastColor,
borderColor: contrastColor
}
},
calendar: {
itemStyle: {
color: backgroundColor
},
dayLabel: {
color: contrastColor
},
monthLabel: {
color: contrastColor
},
yearLabel: {
color: contrastColor
}
},
timeAxis: axisCommon(),
logAxis: axisCommon(),
valueAxis: axisCommon(),
categoryAxis: axisCommon(),
line: {
symbol: 'circle'
},
graph: {
color: colorPalette
},
gauge: {
title: {
color: contrastColor
}
},
candlestick: {
itemStyle: {
color: '#FD1050',
color0: '#0CF49B',
borderColor: '#FD1050',
borderColor0: '#0CF49B'
}
}
};
theme.categoryAxis.splitLine.show = false;
echarts.registerTheme('dark', theme);
});
@@ -1,178 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#59535e',
'#e7dcef',
'#f1baf3',
'#5d4970',
'#372049',
'#c0b2cd',
'#ffccff',
'#f2f0f5'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#59535e'
}
},
visualMap: {
color: ['#59535e', '#e7dcef']
},
toolbox: {
color: ['#59535e', '#59535e', '#59535e', '#59535e']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#59535e',
type: 'dashed'
},
crossStyle: {
color: '#59535e'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#59535e' // Handle color
},
timeline: {
lineStyle: {
color: '#59535e'
},
controlStyle: {
color: '#59535e',
borderColor: '#59535e'
}
},
candlestick: {
itemStyle: {
color: '#e7dcef',
color0: '#f1baf3'
},
lineStyle: {
width: 1,
color: '#372049',
color0: '#5d4970'
},
areaStyle: {
color: '#59535e',
color0: '#e7dcef'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#59535e',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#e7dcef'
}
},
map: {
itemStyle: {
color: '#ddd'
},
areaStyle: {
color: '#f1baf3'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#59535e'
},
linkStyle: {
color: '#59535e'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#e7dcef'],
[0.8, '#59535e'],
[1, '#372049']
],
width: 8
}
}
}
};
echarts.registerTheme('eduardo', theme);
});
@@ -1,163 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#313b23',
'#494f2b',
'#606233',
'#d6b77b',
'#0e0e0e',
'#076278',
'#808080',
'#e7d5b1'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#313b23'
}
},
visualMap: {
color: ['#313b23', '#494f2b']
},
toolbox: {
color: ['#313b23', '#313b23', '#313b23', '#313b23']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#313b23',
type: 'dashed'
},
crossStyle: {
color: '#313b23'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#313b23' // Handle color
},
timeline: {
lineStyle: {
color: '#313b23'
},
controlStyle: {
color: '#313b23',
borderColor: '#313b23'
}
},
candlestick: {
itemStyle: {
color: '#494f2b',
color0: '#606233'
},
lineStyle: {
width: 1,
color: '#0e0e0e',
color0: '#d6b77b'
},
areaStyle: {
color: '#494f2b',
color0: '#d6b77b'
}
},
map: {
itemStyle: {
color: '#606233'
},
areaStyle: {
color: '#ddd'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#494f2b'
},
linkStyle: {
color: '#313b23'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#494f2b'],
[0.8, '#313b23'],
[1, '0e0e0e']
],
width: 8
}
}
}
};
echarts.registerTheme('forest', theme);
});
@@ -1,163 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#00a8c6',
'#40c0cb',
'#f0dec2',
'#aee239',
'#8fbe00',
'#33e0ff',
'#b3f4ff',
'#e6ff99'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#00a8c6'
}
},
visualMap: {
color: ['#00a8c6', '#a2d4e6']
},
toolbox: {
color: ['#00a8c6', '#00a8c6', '#00a8c6', '#00a8c6']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#00a8c6',
type: 'dashed'
},
crossStyle: {
color: '#00a8c6'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(144,197,237,0.2)', // Fill the color
handleColor: '#00a8c6' // Handle color
},
timeline: {
lineStyle: {
color: '#00a8c6'
},
controlStyle: {
color: '#00a8c6',
borderColor: '#00a8c6'
}
},
candlestick: {
itemStyle: {
color: '#40c0cb',
color0: '#f0dec2'
},
lineStyle: {
width: 1,
color: '#8fbe00',
color0: '#aee239'
},
areaStyle: {
color: '#00a8c6',
color0: '#aee239'
}
},
map: {
itemStyle: {
color: '#ddd'
},
areaStyle: {
color: '#f0dec2'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#f0dec2'
},
linkStyle: {
color: '#00a8c6'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#40c0cb'],
[0.8, '#00a8c6'],
[1, '#8fbe00']
],
width: 8
}
}
}
};
echarts.registerTheme('fresh-cut', theme);
});
@@ -1,178 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#ffcb6a',
'#ffa850',
'#ffe2c4',
'#e5834e',
'#ffb081',
'#f7826e',
'#faac9e',
'#fcd5cf'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#ffcb6a'
}
},
visualMap: {
color: ['#ffcb6a', '#ffa850']
},
toolbox: {
color: ['#ffcb6a', '#ffcb6a', '#ffcb6a', '#ffcb6a']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#ffcb6a',
type: 'dashed'
},
crossStyle: {
color: '#ffcb6a'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#ffcb6a' // Handle color
},
timeline: {
lineStyle: {
color: '#ffcb6a'
},
controlStyle: {
color: '#ffcb6a',
borderColor: '#ffcb6a'
}
},
candlestick: {
itemStyle: {
color: '#ffa850',
color0: '#ffe2c4'
},
lineStyle: {
width: 1,
color: '#ffb081',
color0: '#e5834e'
},
areaStyle: {
color: '#e5834e',
color0: '#fcd5cf'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#fcd5cf',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#e5834e'
}
},
map: {
itemStyle: {
color: '#ffe2c4'
},
areaStyle: {
color: '#ddd'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#f2385a'
},
linkStyle: {
color: '#ffcb6a'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#ffa850'],
[0.8, '#ffcb6a'],
[1, '#ffb081']
],
width: 8
}
}
}
};
echarts.registerTheme('fruit', theme);
});
@@ -1,220 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#757575',
'#c7c7c7',
'#dadada',
'#8b8b8b',
'#b5b5b5',
'#e9e9e9'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#757575'
}
},
dataRange: {
color: ['#636363', '#dcdcdc']
},
toolbox: {
color: ['#757575', '#757575', '#757575', '#757575']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#757575',
type: 'dashed'
},
crossStyle: {
color: '#757575'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(117,117,117,0.2)', // Fill the color
handleColor: '#757575' // Handle color
},
grid: {
borderWidth: 0
},
categoryAxis: {
axisLine: {
// Coordinate axis
lineStyle: {
// Property 'lineStyle' controls line styles
color: '#757575'
}
},
splitLine: {
// Separation line
lineStyle: {
// Property 'lineStyle' (see lineStyle) controls line styles
color: ['#eee']
}
}
},
valueAxis: {
axisLine: {
// Coordinate axis
lineStyle: {
// Property 'lineStyle' controls line styles
color: '#757575'
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(250,250,250,0.1)', 'rgba(200,200,200,0.1)']
}
},
splitLine: {
// Separation line
lineStyle: {
// Property 'lineStyle' (see lineStyle) controls line styles
color: ['#eee']
}
}
},
timeline: {
lineStyle: {
color: '#757575'
},
controlStyle: {
color: '#757575',
borderColor: '#757575'
}
},
candlestick: {
itemStyle: {
color: '#8b8b8b',
color0: '#dadada'
},
lineStyle: {
width: 1,
color: '#757575',
color0: '#c7c7c7'
},
areaStyle: {
color: '#757575',
color0: '#e9e9e9'
}
},
map: {
itemStyle: {
color: '#c7c7c7'
},
areaStyle: {
color: 'ddd'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#e9e9e9'
},
linkStyle: {
color: '#757575'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#e9e9e9',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#757575'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#b5b5b5'],
[0.8, '#757575'],
[1, '#5c5c5c']
],
width: 8
}
}
}
};
echarts.registerTheme('gray', theme);
});
@@ -1,222 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#408829',
'#68a54a',
'#a9cba2',
'#86b379',
'#397b29',
'#8abb6f',
'#759c6a',
'#bfd3b7'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#408829'
}
},
visualMap: {
color: ['408829', '#a9cba2']
},
toolbox: {
color: ['#408829', '#408829', '#408829', '#408829']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#408829',
type: 'dashed'
},
crossStyle: {
color: '#408829'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(64,136,41,0.2)', // Fill the color
handleColor: '#408829' // Handle color
},
grid: {
borderWidth: 0
},
categoryAxis: {
axisLine: {
// Coordinate axis
lineStyle: {
// Property 'lineStyle' controls line styles
color: '#408829'
}
},
splitLine: {
// Separation line
lineStyle: {
// Property 'lineStyle' (see lineStyle) controls line styles
color: ['#eee']
}
}
},
valueAxis: {
axisLine: {
// Coordinate axis
lineStyle: {
// Property 'lineStyle' controls line styles
color: '#408829'
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(250,250,250,0.1)', 'rgba(200,200,200,0.1)']
}
},
splitLine: {
// Separation line
lineStyle: {
// Property 'lineStyle' (see lineStyle) controls line styles
color: ['#eee']
}
}
},
timeline: {
lineStyle: {
color: '#408829'
},
controlStyle: {
color: '#408829',
borderColor: '#408829'
}
},
candlestick: {
itemStyle: {
color: '#68a54a',
color0: '#a9cba2'
},
lineStyle: {
width: 1,
color: '#408829',
color0: '#86b379'
},
areaStyle: {
color: '#408829',
color0: '#bfd3b7'
}
},
graph: {
itemStyle: {
color: '#bfd3b7'
},
linkStyle: {
color: '#408829'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#bfd3b7',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#408829'
}
},
map: {
itemStyle: {
color: '#ddd'
},
areaStyle: {
color: '#408829'
},
label: {
color: '#000'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#86b379'],
[0.8, '#68a54a'],
[1, '#408829']
],
width: 8
}
}
}
};
echarts.registerTheme('green', theme);
});
@@ -1,263 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#44B7D3',
'#E42B6D',
'#F4E24E',
'#FE9616',
'#8AED35',
'#ff69b4',
'#ba55d3',
'#cd5c5c',
'#ffa500',
'#40e0d0',
'#E95569',
'#ff6347',
'#7b68ee',
'#00fa9a',
'#ffd700',
'#6699FF',
'#ff6666',
'#3cb371',
'#b8860b',
'#30e0e0'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#8A826D'
}
},
dataRange: {
x: 'right',
y: 'center',
itemWidth: 5,
itemHeight: 25,
color: ['#E42B6D', '#F9AD96'],
text: ['High', 'Low'], // Text, default is numeric text
textStyle: {
color: '#8A826D' // Range text color
}
},
toolbox: {
color: ['#E95569', '#E95569', '#E95569', '#E95569'],
effectiveColor: '#ff4500',
itemGap: 8
},
tooltip: {
backgroundColor: 'rgba(138,130,109,0.7)', // Prompt background color, default is black with a transparency of 0.7
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#6B6455',
type: 'dashed'
},
crossStyle: {
color: '#A6A299'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: 'rgba(130,197,209,0.6)', // Data background color
fillerColor: 'rgba(233,84,105,0.1)', // Fill the color
handleColor: 'rgba(107,99,84,0.8)' // Handle color
},
grid: {
borderWidth: 0
},
categoryAxis: {
axisLine: {
// Coordinate axis
lineStyle: {
// Property 'lineStyle' controls line styles
color: '#6B6455'
}
},
splitLine: {
// separate line
show: false
}
},
valueAxis: {
axisLine: {
// Coordinate axis
show: true
},
splitArea: {
show: false
},
splitLine: {
// separate line
lineStyle: {
// Property 'lineStyle' controls line styles
color: ['#FFF'],
type: 'dashed'
}
}
},
polar: {
axisLine: {
// Coordinate axis
lineStyle: {
// // Property 'lineStyle' controls line styles
color: '#ddd'
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(250,250,250,0.2)', 'rgba(200,200,200,0.2)']
}
},
splitLine: {
lineStyle: {
color: '#ddd'
}
}
},
timeline: {
lineStyle: {
color: '#6B6455'
},
controlStyle: {
color: '#6B6455',
borderColor: '#6B6455'
}
},
line: {
smooth: true,
symbol: 'emptyCircle', // Inflection point graphic type
symbolSize: 3 // Inflection point graphic size
},
candlestick: {
itemStyle: {
color: '#e42B6d',
color0: '#44B7d3'
},
lineStyle: {
width: 1,
color: '#e42B6d',
color0: '#44B7d3'
},
areaStyle: {
color: '#fe994e',
color0: '#e42B6d'
}
},
map: {
itemStyle: {
color: '#6b6455'
},
areaStyle: {
color: '#ddd'
},
label: {
color: '#e42B6d'
}
},
graph: {
itemStyle: {
color: '#e42B6d'
},
linkStyle: {
color: '#6b6455'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#e42B6d',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#6b6455'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#44B7D3'],
[0.8, '#6B6455'],
[1, '#E42B6D']
],
width: 8
}
}
}
};
echarts.registerTheme('helianthus', theme);
});
@@ -1,236 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#C1232B',
'#27727B',
'#FCCE10',
'#E87C25',
'#B5C334',
'#FE8463',
'#9BCA63',
'#FAD860',
'#F3A43B',
'#60C0DD',
'#D7504B',
'#C6E579',
'#F4E001',
'#F0805A',
'#26C0C0'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#27727B'
}
},
visualMap: {
color: ['#C1232B', '#FCCE10']
},
toolbox: {
iconStyle: {
borderColor: colorPalette[0]
}
},
tooltip: {
backgroundColor: 'rgba(50,50,50,0.5)',
axisPointer: {
type: 'line',
lineStyle: {
color: '#27727B',
type: 'dashed'
},
crossStyle: {
color: '#27727B'
},
shadowStyle: {
color: 'rgba(200,200,200,0.3)'
}
}
},
dataZoom: {
dataBackgroundColor: 'rgba(181,195,52,0.3)',
fillerColor: 'rgba(181,195,52,0.2)',
handleColor: '#27727B'
},
categoryAxis: {
axisLine: {
lineStyle: {
color: '#27727B'
}
},
splitLine: {
show: false
}
},
valueAxis: {
axisLine: {
show: false
},
splitArea: {
show: false
},
splitLine: {
lineStyle: {
color: ['#ccc'],
type: 'dashed'
}
}
},
timeline: {
itemStyle: {
color: '#27727B'
},
lineStyle: {
color: '#27727B'
},
controlStyle: {
color: '#27727B',
borderColor: '#27727B'
},
symbol: 'emptyCircle',
symbolSize: 3
},
line: {
itemStyle: {
borderWidth: 2,
borderColor: '#fff',
lineStyle: {
width: 3
}
},
emphasis: {
itemStyle: {
borderWidth: 0
}
},
symbol: 'circle',
symbolSize: 3.5
},
candlestick: {
itemStyle: {
color: '#c1232b',
color0: '#b5c334'
},
lineStyle: {
width: 1,
color: '#c1232b',
color0: '#b5c334'
},
areaStyle: {
color: '#c1232b',
color0: '#27727b'
}
},
graph: {
itemStyle: {
color: '#c1232b'
},
linkStyle: {
color: '#b5c334'
}
},
map: {
itemStyle: {
color: '#f2385a',
areaColor: '#ddd',
borderColor: '#eee'
},
areaStyle: {
color: '#fe994e'
},
label: {
color: '#c1232b'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#B5C334'],
[0.8, '#27727B'],
[1, '#C1232B']
]
}
},
axisTick: {
splitNumber: 2,
length: 5,
lineStyle: {
color: '#fff'
}
},
axisLabel: {
color: '#fff'
},
splitLine: {
length: '5%',
lineStyle: {
color: '#fff'
}
},
title: {
offsetCenter: [0, -20]
}
}
};
echarts.registerTheme('infographic', theme);
});
@@ -1,163 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#cc0000',
'#002266',
'#ff9900',
'#006600',
'#8a150f',
'#076278',
'#808080',
'#f07b75'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#cc0000'
}
},
visualMap: {
color: ['#cc0000', '#002266']
},
toolbox: {
color: ['#cc0000', '#cc0000', '#cc0000', '#cc0000']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#cc0000',
type: 'dashed'
},
crossStyle: {
color: '#cc0000'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#cc0000' // Handle color
},
timeline: {
lineStyle: {
color: '#cc0000'
},
controlStyle: {
color: '#cc0000',
borderColor: '#cc0000'
}
},
candlestick: {
itemStyle: {
color: '#002266',
color0: '#ff9900'
},
lineStyle: {
width: 1,
color: '#8a150f',
color0: '#006600'
},
areaStyle: {
color: '#cc0000',
color0: '#ff9900'
}
},
map: {
itemStyle: {
color: '#ff9900'
},
areaStyle: {
color: '#ddd'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#ff9900'
},
linkStyle: {
color: '#cc0000'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#002266'],
[0.8, '#cc0000'],
[1, '8a150f']
],
width: 8
}
}
}
};
echarts.registerTheme('inspired', theme);
});
@@ -1,163 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#e9e0d1',
'#91a398',
'#33605a',
'#070001',
'#68462b',
'#58a79c',
'#abd3ce',
'#eef6f5'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#e9e0d1'
}
},
visualMap: {
color: ['#e9e0d1', '#91a398']
},
toolbox: {
color: ['#e9e0d1', '#e9e0d1', '#e9e0d1', '#e9e0d1']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#e9e0d1',
type: 'dashed'
},
crossStyle: {
color: '#e9e0d1'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#e9e0d1' // Handle color
},
timeline: {
lineStyle: {
color: '#e9e0d1'
},
controlStyle: {
color: '#e9e0d1',
borderColor: '#e9e0d1'
}
},
candlestick: {
itemStyle: {
color: '#91a398',
color0: '#33605a'
},
lineStyle: {
width: 1,
color: '#68462b',
color0: '#070001'
},
areaStyle: {
color: '#91a398',
color0: '#abd3ce'
}
},
map: {
itemStyle: {
color: '#c12e34'
},
areaStyle: {
color: '#ddd'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#33605a'
},
linkStyle: {
color: '#e9e0d1'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#91a398'],
[0.8, '#e9e0d1'],
[1, '#68462b']
],
width: 8
}
}
}
};
echarts.registerTheme('jazz', theme);
});
@@ -1,163 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#02151a',
'#043a47',
'#087891',
'#c8c8c8',
'#b31d14',
'#0b9cc1',
'#f2f2f2',
'#f07b75'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#02151a'
}
},
visualMap: {
color: ['#02151a', '#a2d4e6']
},
toolbox: {
color: ['#02151a', '#02151a', '#02151a', '#02151a']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#02151a',
type: 'dashed'
},
crossStyle: {
color: '#02151a'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(144,197,237,0.2)', // Fill the color
handleColor: '#02151a' // Handle color
},
timeline: {
lineStyle: {
color: '#02151a'
},
controlStyle: {
color: '#02151a',
borderColor: '#02151a'
}
},
candlestick: {
itemStyle: {
color: '#043a47',
color0: '#087891'
},
lineStyle: {
width: 1,
color: '#b31d14',
color0: '#c8c8c8'
},
areaStyle: {
color: '#087891',
color0: '#c8c8c8'
}
},
map: {
itemStyle: {
color: '#ddd'
},
areaStyle: {
color: '#087891'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#c12e34'
},
linkStyle: {
color: '#02151a'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#043a47'],
[0.8, '#02151a'],
[1, '#b31d14']
],
width: 8
}
}
}
};
echarts.registerTheme('london', theme);
});
@@ -1,242 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#2ec7c9',
'#b6a2de',
'#5ab1ef',
'#ffb980',
'#d87a80',
'#8d98b3',
'#e5cf0d',
'#97b552',
'#95706d',
'#dc69aa',
'#07a2a4',
'#9a7fd1',
'#588dd5',
'#f5994e',
'#c05050',
'#59678c',
'#c9ab00',
'#7eb00a',
'#6f5553',
'#c14089'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#008acd'
}
},
visualMap: {
itemWidth: 15,
color: ['#5ab1ef', '#e0ffff']
},
toolbox: {
iconStyle: {
borderColor: colorPalette[0]
}
},
tooltip: {
borderWidth: 0,
backgroundColor: 'rgba(50,50,50,0.5)',
textStyle: {
color: '#FFF'
},
axisPointer: {
type: 'line',
lineStyle: {
color: '#008acd'
},
crossStyle: {
color: '#008acd'
},
shadowStyle: {
color: 'rgba(200,200,200,0.2)'
}
}
},
dataZoom: {
dataBackgroundColor: '#efefff',
fillerColor: 'rgba(182,162,222,0.2)',
handleColor: '#008acd'
},
grid: {
borderColor: '#eee'
},
categoryAxis: {
axisLine: {
lineStyle: {
color: '#008acd'
}
},
splitLine: {
lineStyle: {
color: ['#eee']
}
}
},
valueAxis: {
axisLine: {
lineStyle: {
color: '#008acd'
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(250,250,250,0.1)', 'rgba(200,200,200,0.1)']
}
},
splitLine: {
lineStyle: {
color: ['#eee']
}
}
},
timeline: {
lineStyle: {
color: '#008acd'
},
controlStyle: {
color: '#008acd',
borderColor: '#008acd'
},
symbol: 'emptyCircle',
symbolSize: 3
},
line: {
smooth: true,
symbol: 'emptyCircle',
symbolSize: 3
},
candlestick: {
itemStyle: {
color: '#d87a80',
color0: '#2ec7c9'
},
lineStyle: {
width: 1,
color: '#d87a80',
color0: '#2ec7c9'
},
areaStyle: {
color: '#2ec7c9',
color0: '#b6a2de'
}
},
scatter: {
symbol: 'circle',
symbolSize: 4
},
map: {
itemStyle: {
color: '#ddd'
},
areaStyle: {
color: '#fe994e'
},
label: {
color: '#d87a80'
}
},
graph: {
itemStyle: {
color: '#d87a80'
},
linkStyle: {
color: '#2ec7c9'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#2ec7c9'],
[0.8, '#5ab1ef'],
[1, '#d87a80']
],
width: 10
}
},
axisTick: {
splitNumber: 10,
length: 15,
lineStyle: {
color: 'auto'
}
},
splitLine: {
length: 22,
lineStyle: {
color: 'auto'
}
},
pointer: {
width: 5
}
}
};
echarts.registerTheme('macarons', theme);
});
@@ -1,246 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#ed9678',
'#e7dac9',
'#cb8e85',
'#f3f39d',
'#c8e49c',
'#f16d7a',
'#f3d999',
'#d3758f',
'#dcc392',
'#2e4783',
'#82b6e9',
'#ff6347',
'#a092f1',
'#0a915d',
'#eaf889',
'#6699FF',
'#ff6666',
'#3cb371',
'#d5b158',
'#38b6b6'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#cb8e85'
}
},
dataRange: {
color: ['#cb8e85', '#e7dac9'], //颜色
//text:['高','低'], // 文本,默认为数值文本
textStyle: {
color: '#333' // 值域文字颜色
}
},
bar: {
barMinHeight: 0, // 最小高度改为0
// barWidth: null, // 默认自适应
barGap: '30%', // 柱间距离,默认为柱形宽度的30%,可设固定值
barCategoryGap: '20%', // 类目间柱形距离,默认为类目间距的20%,可设固定值
label: {
show: false
// position: 默认自适应,水平布局为'top',垂直布局为'right',可选为
// 'inside'|'left'|'right'|'top'|'bottom'
// textStyle: null // 默认使用全局文本样式,详见TEXTSTYLE
},
itemStyle: {
// color: '各异',
barBorderColor: '#fff', // 柱条边线
barBorderRadius: 0, // 柱条边线圆角,单位px,默认为0
barBorderWidth: 1 // 柱条边线线宽,单位px,默认为1
},
emphasis: {
itemStyle: {
// color: '各异',
barBorderColor: 'rgba(0,0,0,0)', // 柱条边线
barBorderRadius: 0, // 柱条边线圆角,单位px,默认为0
barBorderWidth: 1, // 柱条边线线宽,单位px,默认为1
},
label: {
show: false
// position: 默认自适应,水平布局为'top',垂直布局为'right',可选为
// 'inside'|'left'|'right'|'top'|'bottom'
// textStyle: null // 默认使用全局文本样式,详见TEXTSTYLE
}
}
},
line: {
label: {
show: false
// position: 默认自适应,水平布局为'top',垂直布局为'right',可选为
// 'inside'|'left'|'right'|'top'|'bottom'
// textStyle: null // 默认使用全局文本样式,详见TEXTSTYLE
},
itemStyle: {
// color: 各异,
},
emphasis: {
// color: 各异,
label: {
show: false
// position: 默认自适应,水平布局为'top',垂直布局为'right',可选为
// 'inside'|'left'|'right'|'top'|'bottom'
// textStyle: null // 默认使用全局文本样式,详见TEXTSTYLE
}
},
lineStyle: {
width: 2,
type: 'solid',
shadowColor: 'rgba(0,0,0,0)', //默认透明
shadowBlur: 5,
shadowOffsetX: 3,
shadowOffsetY: 3
},
//smooth : false,
//symbol: null, // 拐点图形类型
symbolSize: 2, // 拐点图形大小
//symbolRotate : null, // 拐点图形旋转控制
showAllSymbol: false // 标志图形默认只有主轴显示(随主轴标签间隔隐藏策略)
},
candlestick: {
itemStyle: {
color: '#fe9778',
color0: '#e7dac9'
},
lineStyle: {
width: 1,
color: '#f78766',
color0: '#f1ccb8'
},
areaStyle: {
color: '#e7dac9',
color0: '#c8e49c'
}
},
// 饼图默认参数
pie: {
center: ['50%', '50%'], // 默认全局居中
radius: [0, '75%'],
clockWise: false, // 默认逆时针
startAngle: 90,
minAngle: 0, // 最小角度改为0
selectedOffset: 10, // 选中是扇区偏移量
label: {
show: true,
position: 'outer',
color: '#1b1b1b',
lineStyle: { color: '#1b1b1b' }
// textStyle: null // 默认使用全局文本样式,详见TEXTSTYLE
},
itemStyle: {
// color: 各异,
borderColor: '#fff',
borderWidth: 1
},
labelLine: {
show: true,
length: 20,
lineStyle: {
// color: 各异,
width: 1,
type: 'solid'
}
}
},
map: {
itemStyle: {
color: '#ddd',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
color: '#f3f39d'
},
label: {
show: false,
color: 'rgba(139,69,19,1)'
},
showLegendSymbol: true
},
graph: {
itemStyle: {
color: '#d87a80'
},
linkStyle: {
strokeColor: '#a17e6e'
},
nodeStyle: {
brushType: 'both',
strokeColor: '#a17e6e'
},
label: {
show: false
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#ed9678'],
[0.8, '#e7dac9'],
[1, '#cb8e85']
],
width: 8
}
}
}
};
echarts.registerTheme('macarons2', theme);
});
@@ -1,155 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#8aedd5',
'#93bc9e',
'#cef1db',
'#7fe579',
'#a6d7c2',
'#bef0bb',
'#99e2vb',
'#94f8a8',
'#7de5b8',
'#4dfb70'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#8aedd5'
}
},
toolbox: {
color: ['#8aedd5', '#8aedd5', '#8aedd5', '#8aedd5']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#8aedd5',
type: 'dashed'
},
crossStyle: {
color: '#8aedd5'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(64,136,41,0.2)', // Fill the color
handleColor: '#408829' // Handle color
},
dataRange: {
color: ['#93bc92', '#bef0bb']
},
candlestick: {
itemStyle: {
color: '#8aedd5',
color0: '#7fe579'
},
lineStyle: {
width: 1,
color: '#8aedd5',
color0: '#7fe579'
},
areaStyle: {
color: '#8aedd5',
color0: '#93bc9e'
}
},
graph: {
itemStyle: {
color: '#8aedd5'
},
linkStyle: {
color: '#93bc9e'
}
},
map: {
itemStyle: {
color: '#8aedd5'
},
areaStyle: {
color: '#93bc9e'
},
label: {
color: '#cef1db'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#93bc9e'],
[0.8, '#8aedd5'],
[1, '#a6d7c2']
],
width: 8
}
}
}
};
echarts.registerTheme('mint', theme);
});
@@ -1,163 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#8b1a2d',
'#a7314b',
'#e6004c',
'#ff8066',
'#8e5c4e',
'#ff1a66',
'#d6c582',
'#f0d4af'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#8b1a2d'
}
},
visualMap: {
color: ['#8b1a2d', '#a7314b']
},
toolbox: {
color: ['#8b1a2d', '#8b1a2d', '#8b1a2d', '#8b1a2d']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#8b1a2d',
type: 'dashed'
},
crossStyle: {
color: '#8b1a2d'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#8b1a2d' // Handle color
},
timeline: {
lineStyle: {
color: '#8b1a2d'
},
controlStyle: {
color: '#8b1a2d',
borderColor: '#8b1a2d'
}
},
candlestick: {
itemStyle: {
color: '#a7314b',
color0: '#d6c582'
},
lineStyle: {
width: 1,
color: '#8e5c4e',
color0: '#f0d4af'
},
areaStyle: {
color: '#8b1a2d',
color0: '#ff8066'
}
},
map: {
itemStyle: {
color: '#8b1a2d'
},
areaStyle: {
color: '#ff8066'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#ff8066'
},
linkStyle: {
color: '#8b1a2d'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#a7314b'],
[0.8, '#8b1a2d'],
[1, '#8e5c4e']
],
width: 8
}
}
}
};
echarts.registerTheme('red-velvet', theme);
});
@@ -1,225 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#d8361b',
'#f16b4c',
'#f7b4a9',
'#d26666',
'#99311c',
'#c42703',
'#d07e75'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#d8361b'
}
},
visualMap: {
color: ['#d8361b', '#ffd2d2']
},
dataRange: {
color: ['#bd0707', '#ffd2d2']
},
toolbox: {
color: ['#d8361b', '#d8361b', '#d8361b', '#d8361b']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#d8361b',
type: 'dashed'
},
crossStyle: {
color: '#d8361b'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(216,54,27,0.2)', // Fill the color
handleColor: '#d8361b' // Handle color
},
grid: {
borderWidth: 0
},
categoryAxis: {
axisLine: {
// Coordinate axis
lineStyle: {
// Property 'lineStyle' controls line styles
color: '#d8361b'
}
},
splitLine: {
// Separation line
lineStyle: {
// Property 'lineStyle' (see lineStyle) controls line styles
color: ['#eee']
}
}
},
valueAxis: {
axisLine: {
// Coordinate axis
lineStyle: {
// Property 'lineStyle' controls line styles
color: '#d8361b'
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(250,250,250,0.1)', 'rgba(200,200,200,0.1)']
}
},
splitLine: {
// Separation line
lineStyle: {
// Property 'lineStyle' (see lineStyle) controls line styles
color: ['#eee']
}
}
},
timeline: {
lineStyle: {
color: '#d8361b'
},
controlStyle: {
color: '#d8361b',
borderColor: '#d8361b'
}
},
candlestick: {
itemStyle: {
color: '#f16b4c',
color0: '#f7b4a9'
},
lineStyle: {
width: 1,
color: '#d8361b',
color0: '#d26666'
},
areaStyle: {
color: '#d8361b',
color0: '#d07e75'
}
},
graph: {
itemStyle: {
color: '#d07e75'
},
linkStyle: {
color: '#d8361b'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#d07e75',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#d8361b'
}
},
map: {
itemStyle: {
color: '#d8361b'
},
areaStyle: {
color: '#d07e75'
},
label: {
color: '#c12e34'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#f16b4c'],
[0.8, '#d8361b'],
[1, '#99311c']
],
width: 8
}
}
}
};
echarts.registerTheme('red', theme);
});
@@ -1,119 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#E01F54',
'#001852',
'#f5e8c8',
'#b8d2c7',
'#c6b38e',
'#a4d8c2',
'#f3d999',
'#d3758f',
'#dcc392',
'#2e4783',
'#82b6e9',
'#ff6347',
'#a092f1',
'#0a915d',
'#eaf889',
'#6699FF',
'#ff6666',
'#3cb371',
'#d5b158',
'#38b6b6'
];
var theme = {
color: colorPalette,
visualMap: {
color: ['#e01f54', '#e7dbc3'],
textStyle: {
color: '#333'
}
},
candlestick: {
itemStyle: {
color: '#e01f54',
color0: '#001852'
},
lineStyle: {
width: 1,
color: '#f5e8c8',
color0: '#b8d2c7'
},
areaStyle: {
color: '#a4d8c2',
color0: '#f3d999'
}
},
graph: {
itemStyle: {
color: '#a4d8c2'
},
linkStyle: {
color: '#f3d999'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#E01F54'],
[0.8, '#b8d2c7'],
[1, '#001852']
],
width: 8
}
}
}
};
echarts.registerTheme('roma', theme);
});
@@ -1,163 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#3f7ea6',
'#993366',
'#408000',
'#8c6f56',
'#a65149',
'#731f17',
'#adc2eb',
'#d9c3b0'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#3f7ea6'
}
},
visualMap: {
color: ['#3f7ea6', '#993366']
},
toolbox: {
color: ['#3f7ea6', '#3f7ea6', '#3f7ea6', '#3f7ea6']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#3f7ea6',
type: 'dashed'
},
crossStyle: {
color: '#3f7ea6'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(200,200,200,0.2)', // Fill the color
handleColor: '#3f7ea6' // Handle color
},
timeline: {
lineStyle: {
color: '#3f7ea6'
},
controlStyle: {
color: '#3f7ea6',
borderColor: '#3f7ea6'
}
},
candlestick: {
itemStyle: {
color: '#d9c3b0',
color0: '#8c6f56'
},
lineStyle: {
width: 1,
color: '#731f17',
color0: '#a65149'
},
areaStyle: {
color: '#3f7ea6',
color0: '#993366'
}
},
map: {
itemStyle: {
color: '#d9c3b0'
},
areaStyle: {
color: '#ddd'
},
label: {
color: '#c12e34'
}
},
graph: {
itemStyle: {
color: '#993366'
},
linkStyle: {
color: '#3f7ea6'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#d9c3b0'],
[0.8, '#3f7ea6'],
[1, '#731f17']
],
width: 8
}
}
}
};
echarts.registerTheme('royal', theme);
});
@@ -1,140 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#e52c3c',
'#f7b1ab',
'#fa506c',
'#f59288',
'#f8c4d8',
'#e54f5c',
'#f06d5c',
'#e54f80',
'#f29c9f',
'#eeb5b7'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#e52c3c'
}
},
visualMap: {
color: ['#e52c3c', '#f7b1ab']
},
dataRange: {
color: ['#e52c3c', '#f7b1ab']
},
candlestick: {
itemStyle: {
color: '#e52c3c',
color0: '#f59288'
},
lineStyle: {
width: 1,
color: '#e52c3c',
color0: '#f59288'
},
areaStyle: {
color: '#fa506c',
color0: '#f8c4d8'
}
},
map: {
itemStyle: {
color: '#e52c3c',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
color: '#ccc'
},
label: {
color: 'rgba(139,69,19,1)',
show: false
}
},
graph: {
itemStyle: {
color: '#f2385a'
},
nodeStyle: {
brushType: 'both',
strokeColor: '#e54f5c'
},
linkStyle: {
color: '#f2385a',
strokeColor: '#e54f5c'
},
label: {
color: '#f2385a',
show: false
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#e52c3c'],
[0.8, '#f7b1ab'],
[1, '#fa506c']
],
width: 8
}
}
}
};
echarts.registerTheme('sakura', theme);
});
@@ -1,175 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#c12e34',
'#e6b600',
'#0098d9',
'#2b821d',
'#005eaa',
'#339ca8',
'#cda819',
'#32a487'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal'
}
},
visualMap: {
color: ['#1790cf', '#a2d4e6']
},
toolbox: {
iconStyle: {
borderColor: '#06467c'
}
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.6)'
},
dataZoom: {
dataBackgroundColor: '#dedede',
fillerColor: 'rgba(154,217,247,0.2)',
handleColor: '#005eaa'
},
timeline: {
lineStyle: {
color: '#005eaa'
},
controlStyle: {
color: '#005eaa',
borderColor: '#005eaa'
}
},
candlestick: {
itemStyle: {
color: '#c12e34',
color0: '#2b821d'
},
lineStyle: {
width: 1,
color: '#c12e34',
color0: '#2b821d'
},
areaStyle: {
color: '#e6b600',
color0: '#005eaa'
}
},
graph: {
itemStyle: {
color: '#e6b600'
},
linkStyle: {
color: '#005eaa'
}
},
map: {
itemStyle: {
color: '#f2385a',
borderColor: '#eee',
areaColor: '#ddd'
},
areaStyle: {
color: '#ddd'
},
label: {
color: '#c12e34'
}
},
gauge: {
axisLine: {
show: true,
lineStyle: {
color: [
[0.2, '#2b821d'],
[0.8, '#005eaa'],
[1, '#c12e34']
],
width: 5
}
},
axisTick: {
splitNumber: 10,
length: 8,
lineStyle: {
color: 'auto'
}
},
axisLabel: {
color: 'auto'
},
splitLine: {
length: 12,
lineStyle: {
color: 'auto'
}
},
pointer: {
length: '90%',
width: 3,
color: 'auto'
},
title: {
color: '#333'
},
detail: {
color: 'auto'
}
}
};
echarts.registerTheme('shine', theme);
});
@@ -1,184 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#4d4d4d',
'#3a5897',
'#007bb6',
'#7094db',
'#0080ff',
'#b3b3ff',
'#00bdec',
'#33ccff',
'#ccddff',
'#eeeeee'
];
var theme = {
color: colorPalette,
title: {
textStyle: {
fontWeight: 'normal',
color: '#00aecd'
}
},
visualMap: {
color: ['#00aecd', '#a2d4e6']
},
toolbox: {
color: ['#00aecd', '#00aecd', '#00aecd', '#00aecd']
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.5)',
axisPointer: {
// Axis indicator, coordinate trigger effective
type: 'line', // The default is a straight line 'line' | 'shadow'
lineStyle: {
// Straight line indicator style settings
color: '#00aecd',
type: 'dashed'
},
crossStyle: {
color: '#00aecd'
},
shadowStyle: {
// Shadow indicator style settings
color: 'rgba(200,200,200,0.3)'
}
}
},
// Area scaling controller
dataZoom: {
dataBackgroundColor: '#eee', // Data background color
fillerColor: 'rgba(144,197,237,0.2)', // Fill the color
handleColor: '#00aecd' // Handle color
},
timeline: {
lineStyle: {
color: '#00aecd'
},
controlStyle: {
color: '#00aecd',
},
emphasis: {
controlStyle: {
color: '#00aecd'
}
}
},
candlestick: {
itemStyle: {
color: '#ddd',
color0: '#eee'
},
lineStyle: {
width: 1,
color: '#33ccff',
color0: '#1bb4cf'
},
areaStyle: {
color: '#7094db',
color0: '#33ccff'
}
},
chord: {
padding: 4,
itemStyle: {
color: '#7094db',
borderWidth: 1,
borderColor: 'rgba(128, 128, 128, 0.5)'
},
lineStyle: {
color: 'rgba(128, 128, 128, 0.5)'
},
areaStyle: {
color: '#33ccff'
}
},
graph: {
itemStyle: {
color: '#7094db'
},
linkStyle: {
color: '#33ccff'
}
},
map: {
itemStyle: {
color: '#7094db'
},
areaStyle: {
color: '#33ccff'
},
label: {
color: '#ddd'
}
},
gauge: {
axisLine: {
lineStyle: {
color: [
[0.2, '#dddddd'],
[0.8, '#00aecd'],
[1, '#33ccff']
],
width: 8
}
}
}
};
echarts.registerTheme('tech-blue', theme);
});
@@ -1,88 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export default {
title: {
text: 'Area Chart',
left: 'center',
top: '3%',
textStyle: {
fontWeight: 'normal'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '12%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday','Sunday']
},
yAxis: {
type: 'value',
splitNumber: 3
},
dataZoom: {
},
series: [
{
name:'Email',
type:'line',
stack: '总量',
areaStyle: {},
data:[120, 132, 101, 134, 90, 230, 210]
},
{
name:'联盟广告',
type:'line',
stack: '总量',
areaStyle: {},
data:[220, 182, 191, 234, 290, 330, 310]
},
{
name:'视频广告',
type:'line',
stack: '总量',
areaStyle: {},
data:[150, 232, 201, 154, 190, 330, 410]
},
{
name:'直接访问',
type:'line',
stack: '总量',
areaStyle: {},
data:[320, 332, 301, 334, 390, 330, 320]
},
{
name:'搜索引擎',
type:'line',
stack: '总量',
label: {
show: true,
position: 'top'
},
areaStyle: {},
data:[820, 932, 901, 934, 1290, 1330, 1320]
}
]
};
@@ -1,107 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export default {
title: {
text: 'Bar Chart',
left: 'center',
top: '3%',
textStyle: {
fontWeight: 'normal'
}
},
toolbox: {
top: '3%',
feature: {
magicType: {
type: ['line', 'bar', 'stack', 'tiled']
},
restore: {},
dataZoom: {},
saveAsImage: {}
}
},
grid: {
left: '13%',
right: '5%',
bottom: '5%',
textStyle: {
fontWeight: 'normal'
}
},
xAxis: {
type: 'value'
},
yAxis: {
type: 'category',
data: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday','Sunday']
},
series: [
{
name:'直接访问',
type:'bar',
stack: '总量',
label: {
show: true,
position: 'insideRight'
},
data:[320, 302, 301, 334, 390, 330, 320]
},
{
name:'邮件营销',
type:'bar',
stack: '总量',
label: {
show: true,
position: 'insideRight'
},
data:[120, 132, 101, 134, 90, 230, 210]
},
{
name:'联盟广告',
type:'bar',
stack: '总量',
label: {
show: true,
position: 'insideRight'
},
data:[220, 182, 191, 234, 290, 330, 310]
},
{
name:'视频广告',
type:'bar',
stack: '总量',
label: {
show: true,
position: 'insideRight'
},
data:[150, 212, 201, 154, 190, 330, 410]
},
{
name:'搜索引擎',
type:'bar',
stack: '总量',
label: {
show: true,
position: 'insideRight'
},
data:[820, 832, 901, 934, 1290, 1330, 1320]
}
]
};
File diff suppressed because it is too large Load Diff
@@ -1,141 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export default {
visualMap: {
show: true,
min: 0,
max: 1500,
right: 50,
top: 'middle',
text:['高','低']
// orient: 'horizontal'
},
selectedMode: 'single',
series : [
{
name: 'iphone3',
type: 'map',
map: 'china',
showLegendSymbol: true,
label: {
show: false,
},
emphasis: {
label: {
show: false,
}
},
data:[
{name: '北京',value: 500},
{name: '天津',value: 500},
{name: '上海',value: 500},
{name: '重庆',value: 500},
{name: '河北',value: 500},
{name: '河南',value: 500},
{name: '云南',value: 500},
{name: '辽宁',value: 500},
{name: '黑龙江',value: 500},
{name: '湖南',value: 500},
{name: '安徽',value: 500},
{name: '山东',value: 500},
{name: '新疆',value: 500},
{name: '江苏',value: 500},
{name: '浙江',value: 500},
{name: '江西',value: 500},
{name: '湖北',value: 500},
{name: '广西',value: 500},
{name: '甘肃',value: 500},
{name: '山西',value: 500},
{name: '内蒙古',value: 500},
{name: '陕西',value: 500},
{name: '吉林',value: 500},
{name: '福建',value: 500},
{name: '贵州',value: 500},
{name: '广东',value: 500},
{name: '青海',value: 500},
{name: '西藏',value: 500},
{name: '四川',value: 500},
{name: '宁夏',value: 500},
{name: '海南',value: 500},
{name: '台湾',value: 500},
{name: '香港',value: 500},
{name: '澳门',value: 500}
]
},
{
name: 'iphone4',
type: 'map',
mapType: 'china',
showLegendSymbol: true,
label: {
show: false,
},
emphasis: {
label: {
show: false
}
},
data:[
{name: '北京',value: 500},
{name: '天津',value: 500},
{name: '上海',value: 500},
{name: '重庆',value: 500},
{name: '河北',value: 500},
{name: '安徽',value: 500},
{name: '新疆',value: 500},
{name: '浙江',value: 500},
{name: '江西',value: 500},
{name: '山西',value: 500},
{name: '内蒙古',value: 500},
{name: '吉林',value: 500},
{name: '福建',value: 500},
{name: '广东',value: 500},
{name: '西藏',value: 500},
{name: '四川',value: 500},
{name: '宁夏',value: 500},
{name: '香港',value: 500},
{name: '澳门',value: 500}
]
},
{
name: 'iphone5',
type: 'map',
mapType: 'china',
showLegendSymbol: true,
label: {
show: false,
},
emphasis: {
label: {
show: false
}
},
data:[
{name: '北京',value: 500},
{name: '天津',value: 500},
{name: '上海',value: 500},
{name: '广东',value: 500},
{name: '台湾',value: 500},
{name: '香港',value: 500},
{name: '澳门',value: 500}
]
}
]
};
@@ -1,87 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export default {
legend: {
bottom: '5%',
data: ['rose1', 'rose2', 'rose3', 'rose4']
},
series : [
{
name:'半径模式',
type:'pie',
radius : [20, 80],
center : ['25%', 110],
label: {
show: false,
},
lableLine: {
show: false,
},
emphasis: {
label: {
show: true
},
lableLine: {
show: true
}
},
data:[
{value:10, name:'rose1'},
{value:5, name:'rose2'},
{value:15, name:'rose3'},
{value:25, name:'rose4'},
{value:20, name:'rose5'},
{value:35, name:'rose6'},
{value:30, name:'rose7'},
{value:40, name:'rose8'}
]
},
{
name:'面积模式',
type:'pie',
radius : [30, 80],
center : ['75%', 110],
roseType : 'area',
labelLine: {
length: 5
},
data:[
{value:10, name:'rose1'},
{value:5, name:'rose2'},
{value:15, name:'rose3'},
{value:25, name:'rose4'},
{value:20, name:'rose5'},
{value:35, name:'rose6'},
{value:30, name:'rose7'},
{value:40, name:'rose8'}
]
},
{
name:'仪表盘',
type:'gauge',
radius : 100,
center : ['50%', 280],
detail : {formatter:'{value}%'},
data:[
{value:50, name:'Gauge'}
]
}
]
};
@@ -1,201 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export default {
timeline: {
left: '2%',
right: '2%',
data: [
'2002-01-01','2003-01-01','2004-01-01',
{
value: '2005-01-01',
symbol: 'diamond',
symbolSize: 16
},
'2006-01-01', '2007-01-01','2008-01-01','2009-01-01','2010-01-01',
{
value: '2011-01-01',
symbol: 'diamond',
symbolSize: 18
}
],
label: {
formatter : function(s) {
return (new Date(s)).getFullYear();
}
}
},
options: [{
grid: {
left: '13%',
right: '5%',
bottom: '20%'
},
xAxis: {
type : 'value',
scale:true,
axisLabel : {
formatter: '{value} cm'
}
},
yAxis: {
type : 'value',
scale:true,
axisLabel : {
formatter: '{value} kg'
}
},
series : [
{
name:'女性',
type:'scatter',
data: [[161.2, 51.6], [167.5, 59.0], [159.5, 49.2], [157.0, 63.0], [155.8, 53.6],
[170.0, 59.0], [159.1, 47.6], [166.0, 69.8], [176.2, 66.8], [160.2, 75.2],
[172.5, 55.2], [170.9, 54.2], [172.9, 62.5], [153.4, 42.0], [160.0, 50.0],
[147.2, 49.8], [168.2, 49.2], [175.0, 73.2], [157.0, 47.8], [167.6, 68.8],
[159.5, 50.6], [175.0, 82.5], [166.8, 57.2], [176.5, 87.8], [170.2, 72.8],
[174.0, 54.5], [173.0, 59.8], [179.9, 67.3], [170.5, 67.8], [160.0, 47.0],
[154.4, 46.2], [162.0, 55.0], [176.5, 83.0], [160.0, 54.4], [152.0, 45.8],
[162.1, 53.6], [170.0, 73.2], [160.2, 52.1], [161.3, 67.9], [166.4, 56.6],
[168.9, 62.3], [163.8, 58.5], [167.6, 54.5], [160.0, 50.2], [161.3, 60.3],
[167.6, 58.3], [165.1, 56.2], [160.0, 50.2], [170.0, 72.9], [157.5, 59.8],
[167.6, 61.0], [160.7, 69.1], [163.2, 55.9], [152.4, 46.5], [157.5, 54.3],
[168.3, 54.8], [180.3, 60.7], [165.5, 60.0], [165.0, 62.0], [164.5, 60.3],
[156.0, 52.7], [160.0, 74.3], [163.0, 62.0], [165.7, 73.1], [161.0, 80.0],
[162.0, 54.7], [166.0, 53.2], [174.0, 75.7], [172.7, 61.1], [167.6, 55.7],
[151.1, 48.7], [164.5, 52.3], [163.5, 50.0], [152.0, 59.3], [169.0, 62.5],
[164.0, 55.7], [161.2, 54.8], [155.0, 45.9], [170.0, 70.6], [176.2, 67.2],
[170.0, 69.4], [162.5, 58.2], [170.3, 64.8], [164.1, 71.6], [169.5, 52.8],
[163.2, 59.8], [154.5, 49.0], [159.8, 50.0], [173.2, 69.2], [170.0, 55.9],
[161.4, 63.4], [169.0, 58.2], [166.2, 58.6], [159.4, 45.7], [162.5, 52.2],
[159.0, 48.6], [162.8, 57.8], [159.0, 55.6], [179.8, 66.8], [162.9, 59.4],
[161.0, 53.6], [151.1, 73.2], [168.2, 53.4], [168.9, 69.0], [173.2, 58.4],
[171.8, 56.2], [178.0, 70.6], [164.3, 59.8], [163.0, 72.0], [168.5, 65.2],
[166.8, 56.6], [172.7, 105.2], [163.5, 51.8], [169.4, 63.4], [167.8, 59.0],
[159.5, 47.6], [167.6, 63.0], [161.2, 55.2], [160.0, 45.0], [163.2, 54.0],
[162.2, 50.2], [161.3, 60.2], [149.5, 44.8], [157.5, 58.8], [163.2, 56.4],
[172.7, 62.0], [155.0, 49.2], [156.5, 67.2], [164.0, 53.8], [160.9, 54.4],
[162.8, 58.0], [167.0, 59.8], [160.0, 54.8], [160.0, 43.2], [168.9, 60.5],
[158.2, 46.4], [156.0, 64.4], [160.0, 48.8], [167.1, 62.2], [158.0, 55.5],
[167.6, 57.8], [156.0, 54.6], [162.1, 59.2], [173.4, 52.7], [159.8, 53.2],
[170.5, 64.5], [159.2, 51.8], [157.5, 56.0], [161.3, 63.6], [162.6, 63.2],
[160.0, 59.5], [168.9, 56.8], [165.1, 64.1], [162.6, 50.0], [165.1, 72.3],
[166.4, 55.0], [160.0, 55.9], [152.4, 60.4], [170.2, 69.1], [162.6, 84.5],
[170.2, 55.9], [158.8, 55.5], [172.7, 69.5], [167.6, 76.4], [162.6, 61.4],
[167.6, 65.9], [156.2, 58.6], [175.2, 66.8], [172.1, 56.6], [162.6, 58.6],
[160.0, 55.9], [165.1, 59.1], [182.9, 81.8], [166.4, 70.7], [165.1, 56.8],
[177.8, 60.0], [165.1, 58.2], [175.3, 72.7], [154.9, 54.1], [158.8, 49.1],
[172.7, 75.9], [168.9, 55.0], [161.3, 57.3], [167.6, 55.0], [165.1, 65.5],
[175.3, 65.5], [157.5, 48.6], [163.8, 58.6], [167.6, 63.6], [165.1, 55.2],
[165.1, 62.7], [168.9, 56.6], [162.6, 53.9], [164.5, 63.2], [176.5, 73.6],
[168.9, 62.0], [175.3, 63.6], [159.4, 53.2], [160.0, 53.4], [170.2, 55.0],
[162.6, 70.5], [167.6, 54.5], [162.6, 54.5], [160.7, 55.9], [160.0, 59.0],
[157.5, 63.6], [162.6, 54.5], [152.4, 47.3], [170.2, 67.7], [165.1, 80.9],
[172.7, 70.5], [165.1, 60.9], [170.2, 63.6], [170.2, 54.5], [170.2, 59.1],
[161.3, 70.5], [167.6, 52.7], [167.6, 62.7], [165.1, 86.3], [162.6, 66.4],
[152.4, 67.3], [168.9, 63.0], [170.2, 73.6], [175.2, 62.3], [175.2, 57.7],
[160.0, 55.4], [165.1, 104.1], [174.0, 55.5], [170.2, 77.3], [160.0, 80.5],
[167.6, 64.5], [167.6, 72.3], [167.6, 61.4], [154.9, 58.2], [162.6, 81.8],
[175.3, 63.6], [171.4, 53.4], [157.5, 54.5], [165.1, 53.6], [160.0, 60.0],
[174.0, 73.6], [162.6, 61.4], [174.0, 55.5], [162.6, 63.6], [161.3, 60.9],
[156.2, 60.0], [149.9, 46.8], [169.5, 57.3], [160.0, 64.1], [175.3, 63.6],
[169.5, 67.3], [160.0, 75.5], [172.7, 68.2], [162.6, 61.4], [157.5, 76.8],
[176.5, 71.8], [164.4, 55.5], [160.7, 48.6], [174.0, 66.4], [163.8, 67.3]
],
markPoint : {
data : [
{type : 'max', name: '最大值'},
{type : 'min', name: '最小值'}
]
},
markLine : {
data : [
{type : 'average', name: '平均值'}
]
}
},
{
name:'男性',
type:'scatter',
data: [[174.0, 65.6], [175.3, 71.8], [193.5, 80.7], [186.5, 72.6], [187.2, 78.8],
[181.5, 74.8], [184.0, 86.4], [184.5, 78.4], [175.0, 62.0], [184.0, 81.6],
[180.0, 76.6], [177.8, 83.6], [192.0, 90.0], [176.0, 74.6], [174.0, 71.0],
[184.0, 79.6], [192.7, 93.8], [171.5, 70.0], [173.0, 72.4], [176.0, 85.9],
[176.0, 78.8], [180.5, 77.8], [172.7, 66.2], [176.0, 86.4], [173.5, 81.8],
[178.0, 89.6], [180.3, 82.8], [180.3, 76.4], [164.5, 63.2], [173.0, 60.9],
[183.5, 74.8], [175.5, 70.0], [188.0, 72.4], [189.2, 84.1], [172.8, 69.1],
[170.0, 59.5], [182.0, 67.2], [170.0, 61.3], [177.8, 68.6], [184.2, 80.1],
[186.7, 87.8], [171.4, 84.7], [172.7, 73.4], [175.3, 72.1], [180.3, 82.6],
[182.9, 88.7], [188.0, 84.1], [177.2, 94.1], [172.1, 74.9], [167.0, 59.1],
[169.5, 75.6], [174.0, 86.2], [172.7, 75.3], [182.2, 87.1], [164.1, 55.2],
[163.0, 57.0], [171.5, 61.4], [184.2, 76.8], [174.0, 86.8], [174.0, 72.2],
[177.0, 71.6], [186.0, 84.8], [167.0, 68.2], [171.8, 66.1], [182.0, 72.0],
[167.0, 64.6], [177.8, 74.8], [164.5, 70.0], [192.0, 101.6], [175.5, 63.2],
[171.2, 79.1], [181.6, 78.9], [167.4, 67.7], [181.1, 66.0], [177.0, 68.2],
[174.5, 63.9], [177.5, 72.0], [170.5, 56.8], [182.4, 74.5], [197.1, 90.9],
[180.1, 93.0], [175.5, 80.9], [180.6, 72.7], [184.4, 68.0], [175.5, 70.9],
[180.6, 72.5], [177.0, 72.5], [177.1, 83.4], [181.6, 75.5], [176.5, 73.0],
[175.0, 70.2], [174.0, 73.4], [165.1, 70.5], [177.0, 68.9], [192.0, 102.3],
[176.5, 68.4], [169.4, 65.9], [182.1, 75.7], [179.8, 84.5], [175.3, 87.7],
[184.9, 86.4], [177.3, 73.2], [167.4, 53.9], [178.1, 72.0], [168.9, 55.5],
[157.2, 58.4], [180.3, 83.2], [170.2, 72.7], [177.8, 64.1], [172.7, 72.3],
[165.1, 65.0], [186.7, 86.4], [165.1, 65.0], [174.0, 88.6], [175.3, 84.1],
[185.4, 66.8], [177.8, 75.5], [180.3, 93.2], [180.3, 82.7], [177.8, 58.0],
[177.8, 79.5], [177.8, 78.6], [177.8, 71.8], [177.8, 116.4], [163.8, 72.2],
[188.0, 83.6], [198.1, 85.5], [175.3, 90.9], [166.4, 85.9], [190.5, 89.1],
[166.4, 75.0], [177.8, 77.7], [179.7, 86.4], [172.7, 90.9], [190.5, 73.6],
[185.4, 76.4], [168.9, 69.1], [167.6, 84.5], [175.3, 64.5], [170.2, 69.1],
[190.5, 108.6], [177.8, 86.4], [190.5, 80.9], [177.8, 87.7], [184.2, 94.5],
[176.5, 80.2], [177.8, 72.0], [180.3, 71.4], [171.4, 72.7], [172.7, 84.1],
[172.7, 76.8], [177.8, 63.6], [177.8, 80.9], [182.9, 80.9], [170.2, 85.5],
[167.6, 68.6], [175.3, 67.7], [165.1, 66.4], [185.4, 102.3], [181.6, 70.5],
[172.7, 95.9], [190.5, 84.1], [179.1, 87.3], [175.3, 71.8], [170.2, 65.9],
[193.0, 95.9], [171.4, 91.4], [177.8, 81.8], [177.8, 96.8], [167.6, 69.1],
[167.6, 82.7], [180.3, 75.5], [182.9, 79.5], [176.5, 73.6], [186.7, 91.8],
[188.0, 84.1], [188.0, 85.9], [177.8, 81.8], [174.0, 82.5], [177.8, 80.5],
[171.4, 70.0], [185.4, 81.8], [185.4, 84.1], [188.0, 90.5], [188.0, 91.4],
[182.9, 89.1], [176.5, 85.0], [175.3, 69.1], [175.3, 73.6], [188.0, 80.5],
[188.0, 82.7], [175.3, 86.4], [170.5, 67.7], [179.1, 92.7], [177.8, 93.6],
[175.3, 70.9], [182.9, 75.0], [170.8, 93.2], [188.0, 93.2], [180.3, 77.7],
[177.8, 61.4], [185.4, 94.1], [168.9, 75.0], [185.4, 83.6], [180.3, 85.5],
[174.0, 73.9], [167.6, 66.8], [182.9, 87.3], [160.0, 72.3], [180.3, 88.6],
[167.6, 75.5], [186.7, 101.4], [175.3, 91.1], [175.3, 67.3], [175.9, 77.7],
[175.3, 81.8], [179.1, 75.5], [181.6, 84.5], [177.8, 76.6], [182.9, 85.0],
[177.8, 102.5], [184.2, 77.3], [179.1, 71.8], [176.5, 87.9], [188.0, 94.3],
[174.0, 70.9], [167.6, 64.5], [170.2, 77.3], [167.6, 72.3], [188.0, 87.3],
[174.0, 80.0], [176.5, 82.3], [180.3, 73.6], [167.6, 74.1], [188.0, 85.9],
[180.3, 73.2], [167.6, 76.3], [183.0, 65.9], [183.0, 90.9], [179.1, 89.1],
[170.2, 62.3], [177.8, 82.7], [179.1, 79.1], [190.5, 98.2], [177.8, 84.1],
[180.3, 83.2], [180.3, 83.2]
],
markPoint : {
data : [
{type : 'max', name: '最大值'},
{type : 'min', name: '最小值'}
]
},
markLine : {
data : [
{type : 'average', name: '平均值'}
]
}
}
]
}]
};
@@ -1,65 +0,0 @@
<!DOCTYPE html>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script src="../../dist/echarts.js"></script>
<script src="../../map/js/china.js"></script>
</head>
<body>
<style>
body {
margin: 0;
}
</style>
<div id="main"></div>
<script type="module">
import bar from './option/bar';
import area from './option/area';
import scatter from './option/scatter';
import pie from './option/pie';
import graph from './option/graph';
import map from './option/map';
let options = [bar, area, scatter, pie, graph, map];
let mainDiv = document.querySelector('#main');
let theme = window.location.hash.slice(1);
let scriptTag = document.createElement('script');
scriptTag.src = '../' + theme + '.js';
scriptTag.onload = function () {
options.forEach(option => {
let div = document.createElement('div');
mainDiv.appendChild(div);
div.style.cssText = 'float:left;width:50%;height:400px';
let chart = echarts.init(div, theme);
option.animation = false;
chart.setOption(option);
});
}
document.body.appendChild(scriptTag);
</script>
</body>
</html>
@@ -1,63 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
const glob = require('glob');
const puppeteer = require('puppeteer');
const fs = require('fs');
const path = require('path');
async function wait(time) {
return new Promise(resolve => {
setTimeout(resolve, time);
});
}
async function snapshot(browser, themePath) {
let themeName = path.basename(themePath, '.js');
let code = fs.readFileSync(themePath, 'utf-8');
let page = await browser.newPage();
await page.evaluateOnNewDocument(code);
await page.setViewport({ width: 1200, height: 1200 });
try {
await page.goto('http://localhost/echarts/theme/tool/thumb.html#' + themeName);
await wait(200);
await page.screenshot({ path: __dirname + '/../thumb/' + themeName + '.png' });
}
catch (e) {
console.log(e);
}
await page.close();
console.log('Updated ' + themeName);
}
glob('../*.js', async function (err, themePathList) {
let browser = await puppeteer.launch();
for (let themePath of themePathList) {
try {
await snapshot(browser, themePath);
}
catch(e) {
console.log(e);
}
}
await browser.close();
});
@@ -1,63 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'echarts'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports, require('echarts/lib/echarts'));
} else {
// Browser globals
factory({}, root.echarts);
}
})(this, function(exports, echarts) {
var log = function(msg) {
if (typeof console !== 'undefined') {
console && console.error && console.error(msg);
}
};
if (!echarts) {
log('ECharts is not Loaded');
return;
}
var colorPalette = [
'#d87c7c',
'#919e8b',
'#d7ab82',
'#6e7074',
'#61a0a8',
'#efa18d',
'#787464',
'#cc7e63',
'#724e58',
'#4b565b'
];
echarts.registerTheme('vintage', {
color: colorPalette,
backgroundColor: '#fef8ef',
graph: {
color: colorPalette
}
});
});
@@ -20,14 +20,14 @@
<nav class="service-nav">
<!-- All APIs -->
<a class="service-nav__item"
th:classappend="${selected == '-1' || #request.getParameter('selected') == '-1'} ? 'service-nav__item--active' : ''"
th:classappend="${selected == '-1' or (param.selected != null and param.selected[0] == '-1')} ? 'service-nav__item--active' : ''"
th:href="@{/apis}">
전체
</a>
<!-- Service Categories -->
<a class="service-nav__item" th:each="service : ${services}"
th:classappend="${(#request.getParameter('selected') != null ? #request.getParameter('selected') : (selected != null ? selected : apiSpecInfo.service)) == service.id} ? 'service-nav__item--active' : ''"
th:classappend="${(param.selected != null ? param.selected[0] : (selected != null ? selected : apiSpecInfo.service)) == service.id} ? 'service-nav__item--active' : ''"
th:href="@{/apis(groupIds=${service.id})}">
[[${service.groupName}]]
</a>
@@ -122,8 +122,14 @@
<!--/* LiveReload (개발 전용): DevTools 가 정적 리소스/템플릿 변경을 감지하면 브라우저를 자동 새로고침한다.
localhost 외 IP(예: 172.30.1.14)로 접속해도 동작하도록 접속 호스트 기준으로 livereload.js 를 로드한다.
prod/stage 에는 DevTools 자체가 없으므로(developmentOnly) 개발 프로파일에서만 주입한다. */-->
<script th:if="${@environment.acceptsProfiles('local_rinjaemac','local')}"
th:src="|//${#request.serverName}:35729/livereload.js|"></script>
<script th:if="${@environment.acceptsProfiles('local_rinjaemac','local')}">
// Thymeleaf 3.1 은 #request 표현식 객체를 제거했으므로 호스트는 브라우저에서 읽는다.
(function () {
var s = document.createElement('script');
s.src = '//' + location.hostname + ':35729/livereload.js';
document.head.appendChild(s);
})();
</script>
</head>
</html>
@@ -0,0 +1,107 @@
package com.eactive.apim.portal.common.compatibility;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Arrays;
import com.eactive.apim.portal.common.util.UserTypeUtil;
import com.eactive.apim.portal.common.validator.CellPhoneValidator;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* Commons Lang 업그레이드 시 포털에서 사용하는 문자열 처리 결과가 유지되는지 검증한다.
*/
class CommonsLangUpgradeCompatibilityTest {
@AfterEach
void resetUserTypeConfiguration() {
UserTypeUtil.setInternalEmailDomains(null);
}
@Test
@DisplayName("빈 문자열과 공백 판정이 기존 분기 조건을 유지한다")
void keepsEmptyAndBlankBranchingBehavior() {
assertAll(
() -> assertTrue(StringUtils.isEmpty(null)),
() -> assertTrue(StringUtils.isEmpty("")),
() -> assertFalse(StringUtils.isEmpty(" ")),
() -> assertTrue(StringUtils.isBlank(" \t")),
() -> assertFalse(StringUtils.isNotBlank(" \t")),
() -> assertTrue(StringUtils.isNotEmpty(" "))
);
}
@Test
@DisplayName("폼 입력과 상태 설정의 trim 및 기본값 처리가 유지된다")
void keepsTrimmingAndDefaultingBehavior() {
assertAll(
() -> assertEquals("서비스명", StringUtils.trimToEmpty(" 서비스명 ")),
() -> assertEquals("", StringUtils.trimToEmpty(null)),
() -> assertEquals("api-001", StringUtils.defaultIfBlank(" ", "api-001")),
() -> assertEquals("API 이름", StringUtils.defaultIfBlank("API 이름", "api-001")),
() -> assertTrue(Strings.CI.equalsAny(" Y ".trim(), "true", "y", "1"))
);
}
@Test
@DisplayName("API 경로와 메시지 리소스명 문자열 처리가 유지된다")
void keepsPathAndResourceNameBehavior() {
String fullPath = StringUtils.join("/openapi/", "/", Strings.CS.removeStart("/users", "/"));
String normalizedPath = fullPath.replaceAll("//+", "/");
String resource = "/WEB-INF/messages/messages_ko.properties";
String baseName = StringUtils.substringBeforeLast(resource, ".properties");
assertAll(
() -> assertEquals("/openapi/users", normalizedPath),
() -> assertEquals("/openapi", Strings.CS.removeEnd("/openapi/", "/")),
() -> assertEquals("/WEB-INF/messages", StringUtils.substringBeforeLast(baseName, "/")),
() -> assertEquals("messages_ko", StringUtils.substringAfterLast(baseName, "/")),
() -> assertEquals("messages", StringUtils.substringBeforeLast("messages_ko", "_"))
);
}
@SuppressWarnings("deprecation")
@Test
@DisplayName("API와 약관 본문의 HTML 엔티티 디코딩 결과가 유지된다")
void keepsHtmlUnescapeBehavior() {
assertEquals("<p>이용약관 & 안내</p>",
StringEscapeUtils.unescapeHtml4("&lt;p&gt;이용약관 &amp; 안내&lt;/p&gt;"));
}
@Test
@DisplayName("휴대전화 검증의 기존 허용 및 거부 결과가 유지된다")
void keepsCellPhoneValidationBehavior() {
CellPhoneValidator validator = new CellPhoneValidator();
assertAll(
() -> assertTrue(validator.isValid("010-1234-5678", null)),
() -> assertTrue(validator.isValid("821012345678", null)),
() -> assertFalse(validator.isValid("", null)),
() -> assertFalse(validator.isValid("02-1234-5678", null)),
() -> assertFalse(validator.isValid("010-12-5678", null))
);
}
@Test
@DisplayName("내부 사용자 이메일 도메인 정규화와 판별 결과가 유지된다")
void keepsInternalUserDomainBehavior() {
UserTypeUtil.setInternalEmailDomains(Arrays.asList(" DJBANK.com ", "@Partner.COM", " "));
assertAll(
() -> assertEquals(Arrays.asList("@djbank.com", "@partner.com"),
UserTypeUtil.getInternalEmailDomains()),
() -> assertTrue(UserTypeUtil.isInternalEmail(" USER@DJBANK.COM ")),
() -> assertTrue(UserTypeUtil.isInternalEmail("user@partner.com")),
() -> assertFalse(UserTypeUtil.isInternalEmail("user@example.com")),
() -> assertFalse(UserTypeUtil.isInternalEmail(null))
);
}
}
@@ -0,0 +1,128 @@
package com.eactive.apim.portal.common.json;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.LocalDate;
import java.time.LocalDateTime;
import com.eactive.apim.portal.djb.apistatus.dto.DailyStatDTO;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
/**
* jackson 2.13.5 → 2.18.x 상향에 따른 JSON 직렬화 회귀 테스트.
* <p>
* Spring Boot 의 {@link JacksonAutoConfiguration} 이 만들어 주는 ObjectMapper(= MVC 응답에 실제로 쓰이는 것)로
* 화면/AJAX 응답의 날짜 표현이 그대로인지 고정한다. 직접 new 한 ObjectMapper 나 Jackson2ObjectMapperBuilder 는
* WRITE_DATES_AS_TIMESTAMPS 가 켜져 있어 실제 응답과 다르므로 쓰지 않는다.
* </p>
*/
class JacksonDateSerializationTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class));
/** Boot 가 구성한 ObjectMapper 로 검증 로직을 실행한다. */
private void withBootObjectMapper(MapperAssertion assertion) {
contextRunner.run(context -> assertion.accept(context.getBean(ObjectMapper.class)));
}
@FunctionalInterface
interface MapperAssertion {
void accept(ObjectMapper objectMapper) throws Exception;
}
/** 테스트 소스에는 lombok annotation processor 가 걸려 있지 않아 접근자를 직접 둔다. */
public static class SampleDTO {
private LocalDateTime createdAt;
private LocalDate baseDate;
private String name;
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDate getBaseDate() {
return baseDate;
}
public void setBaseDate(LocalDate baseDate) {
this.baseDate = baseDate;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@Test
@DisplayName("@JsonFormat 지정 필드는 지정 패턴 문자열로 직렬화된다")
void jsonFormatPatternIsHonored() {
withBootObjectMapper(objectMapper -> {
DailyStatDTO dto = new DailyStatDTO();
dto.setStatDate(LocalDate.of(2026, 8, 18));
dto.setUptimeRatio(0.9987);
dto.setStatus("NORMAL");
String json = objectMapper.writeValueAsString(dto);
assertTrue(json.contains("\"statDate\":\"2026-08-18\""), json);
assertTrue(json.contains("\"status\":\"NORMAL\""), json);
});
}
@Test
@DisplayName("어노테이션 없는 날짜 필드는 타임스탬프 숫자가 아니라 ISO-8601 문자열이다")
void plainDatesAreIsoStrings() {
withBootObjectMapper(objectMapper -> {
SampleDTO dto = new SampleDTO();
dto.setCreatedAt(LocalDateTime.of(2026, 8, 18, 9, 30, 0));
dto.setBaseDate(LocalDate.of(2026, 8, 18));
dto.setName("포털");
String json = objectMapper.writeValueAsString(dto);
assertTrue(json.contains("\"createdAt\":\"2026-08-18T09:30:00\""), json);
assertTrue(json.contains("\"baseDate\":\"2026-08-18\""), json);
assertTrue(json.contains("\"name\":\"포털\""), json);
});
}
@Test
@DisplayName("ISO-8601 문자열은 다시 날짜 타입으로 역직렬화된다")
void isoStringsDeserializeBack() {
withBootObjectMapper(objectMapper -> {
String json = "{\"createdAt\":\"2026-08-18T09:30:00\",\"baseDate\":\"2026-08-18\",\"name\":\"포털\"}";
SampleDTO dto = objectMapper.readValue(json, SampleDTO.class);
assertEquals(LocalDateTime.of(2026, 8, 18, 9, 30, 0), dto.getCreatedAt());
assertEquals(LocalDate.of(2026, 8, 18), dto.getBaseDate());
});
}
@Test
@DisplayName("모르는 필드가 있어도 역직렬화가 실패하지 않는다 - Boot 기본 동작 유지")
void unknownPropertiesAreIgnored() {
withBootObjectMapper(objectMapper -> {
String json = "{\"name\":\"포털\",\"unknownField\":123}";
SampleDTO dto = objectMapper.readValue(json, SampleDTO.class);
assertEquals("포털", dto.getName());
});
}
}
@@ -0,0 +1,56 @@
package com.eactive.apim.portal.common.security;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.apache.commons.beanutils.PropertyUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* commons-beanutils 1.11.0(CVE-2025-48734) 상향 회귀 테스트.
* <p>
* 1.11.0 부터 SuppressPropertiesBeanIntrospector 가 기본 활성이라 enum 의 declaredClass 를 통해
* ClassLoader 로 내려가는 경로가 막힌다. 검증기들이 쓰는 일반 프로퍼티 접근은 그대로 동작해야 한다.
* </p>
*/
class BeanUtilsPropertyAccessTest {
enum Kind {
A, B
}
public static class Form {
private String password = "pw1234";
private Kind kind = Kind.A;
public String getPassword() {
return password;
}
public Kind getKind() {
return kind;
}
}
@Test
@DisplayName("일반 프로퍼티 접근은 정상 - PasswordRuleValidator 등이 쓰는 경로")
void plainPropertyAccessWorks() throws Exception {
assertEquals("pw1234", PropertyUtils.getProperty(new Form(), "password"));
assertEquals(Kind.A, PropertyUtils.getProperty(new Form(), "kind"));
}
@Test
@DisplayName("enum 의 declaredClass 접근은 차단된다")
void declaredClassIsSuppressed() {
assertThrows(NoSuchMethodException.class,
() -> PropertyUtils.getProperty(new Form(), "kind.declaredClass"));
}
@Test
@DisplayName("declaredClass 를 거쳐 ClassLoader 로 내려가는 경로도 차단된다")
void classLoaderIsUnreachable() {
assertThrows(NoSuchMethodException.class,
() -> PropertyUtils.getNestedProperty(new Form(), "kind.declaredClass.classLoader"));
}
}
@@ -0,0 +1,51 @@
package com.eactive.apim.portal.common.security;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.ByteArrayResource;
/**
* snakeyaml 2.6(CVE-2022-1471) 상향 회귀 테스트.
* <p>
* application.yml 로딩 경로가 정상 동작하는지, 그리고 임의 타입을 지정하는 글로벌 태그(!!java...)가
* 거부되는지 확인한다.
* </p>
*/
class YamlLoaderTagTest {
private final YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
private List<PropertySource<?>> load(String yaml) throws IOException {
return loader.load("test", new ByteArrayResource(yaml.getBytes("UTF-8")));
}
@Test
@DisplayName("일반 yml 은 그대로 로딩된다")
void plainYamlLoads() throws Exception {
List<PropertySource<?>> sources = load("server:\n port: 39130\nportal:\n auth-ttl: 300\n");
assertEquals(1, sources.size());
assertEquals(39130, sources.get(0).getProperty("server.port"));
assertEquals(300, sources.get(0).getProperty("portal.auth-ttl"));
}
@Test
@DisplayName("임의 타입을 지정하는 글로벌 태그는 거부된다")
void globalTagIsRejected() {
String payload = "key: !!javax.script.ScriptEngineManager [!!java.net.URL [\"http://127.0.0.1/\"]]\n";
Exception thrown = assertThrows(Exception.class, () -> load(payload));
assertNotNull(thrown.getMessage());
assertEquals(true, thrown.getMessage().contains("Global tag is not allowed"));
}
}
@@ -0,0 +1,100 @@
package com.eactive.apim.portal.config;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
/**
* commons-fileupload 1.6.0(CVE-2025-48976) 상향에 따른 파트 헤더 상한 회귀 테스트.
* <p>
* 1.6 부터 파트 헤더 총량 기본 상한이 512 바이트라 한글 파일명(UTF-8 3바이트/자)이 길면 업로드가 깨진다.
* {@link MultipartConfig} 가 상한을 2048 로 올려 두는지 파싱 결과로 확인한다.
* </p>
*/
class MultipartConfigTest {
private static final String BOUNDARY = "----portalTestBoundary";
private CommonsMultipartResolver resolver() {
PortalProperties properties = new PortalProperties();
properties.getFile().setMaxSize("8MB");
return new MultipartConfig(properties).filterMultipartResolver();
}
private MockHttpServletRequest multipartRequest(String filename) throws IOException {
String head = "--" + BOUNDARY + "\r\n"
+ "Content-Disposition: form-data; name=\"file\"; filename=\"" + filename + "\"\r\n"
+ "Content-Type: application/octet-stream\r\n\r\n";
ByteArrayOutputStream body = new ByteArrayOutputStream();
body.write(head.getBytes("UTF-8"));
body.write("hello".getBytes("UTF-8"));
body.write(("\r\n--" + BOUNDARY + "--\r\n").getBytes("UTF-8"));
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/file/upload");
request.setContentType("multipart/form-data; boundary=" + BOUNDARY);
request.setCharacterEncoding("UTF-8");
request.setContent(body.toByteArray());
return request;
}
private String korean(int length) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append('한');
}
return sb.append(".pdf").toString();
}
@Test
@DisplayName("영문 파일명 업로드는 그대로 파싱된다")
void asciiFilename() throws Exception {
MultipartHttpServletRequest parsed = resolver().resolveMultipart(multipartRequest("report.pdf"));
MultipartFile file = parsed.getFile("file");
assertNotNull(file);
assertEquals("report.pdf", file.getOriginalFilename());
assertEquals(5L, file.getSize());
}
@Test
@DisplayName("한글 파일명 137자 이상도 파싱된다 - fileupload 1.6 기본 상한 512 였다면 실패할 길이")
void longKoreanFilenameOverDefaultLimit() throws Exception {
String filename = korean(150);
MultipartHttpServletRequest parsed = resolver().resolveMultipart(multipartRequest(filename));
MultipartFile file = parsed.getFile("file");
assertNotNull(file);
assertEquals(filename, file.getOriginalFilename());
}
@Test
@DisplayName("OS 파일명 상한(255자)이 전부 한글이어도 파싱된다")
void koreanFilenameAtOsLimit() throws Exception {
String filename = korean(255);
MultipartHttpServletRequest parsed = resolver().resolveMultipart(multipartRequest(filename));
MultipartFile file = parsed.getFile("file");
assertNotNull(file);
assertEquals(filename, file.getOriginalFilename());
}
@Test
@DisplayName("파트 헤더가 상한(2048바이트)을 넘으면 거부한다 - DoS 방어가 살아 있음")
void partHeaderOverConfiguredLimitIsRejected() {
assertThrows(MultipartException.class,
() -> resolver().resolveMultipart(multipartRequest(korean(700))));
}
}
@@ -0,0 +1,70 @@
package com.eactive.apim.portal.config;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.web.header.HeaderWriterFilter;
import org.springframework.security.web.header.writers.CacheControlHeadersWriter;
import org.springframework.security.web.header.writers.XContentTypeOptionsHeaderWriter;
/**
* CVE-2026-22732 우회책 회귀 테스트.
* <p>
* {@code PortalConfigSecurity} 는 HeaderWriterFilter 를 eager 모드로 후처리한다. eager 모드에서는
* 컨트롤러가 실행되기 전에 보안 헤더가 이미 기록돼 있어야 하며, 응답이 먼저 커밋되더라도 헤더가 누락되지 않는다.
* </p>
*/
class SecurityHeaderEagerWriteTest {
private HeaderWriterFilter filter(boolean eager) {
HeaderWriterFilter filter = new HeaderWriterFilter(Arrays.asList(
new XContentTypeOptionsHeaderWriter(),
new CacheControlHeadersWriter()));
filter.setShouldWriteHeadersEagerly(eager);
return filter;
}
@Test
@DisplayName("eager 모드에서는 체인(컨트롤러) 실행 시점에 이미 보안 헤더가 기록돼 있다")
void eagerWritesHeadersBeforeChain() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/file/download");
MockHttpServletResponse response = new MockHttpServletResponse();
AtomicReference<String> seenInChain = new AtomicReference<>();
filter(true).doFilter(request, response, (req, res) -> {
seenInChain.set(((HttpServletResponse) res).getHeader("X-Content-Type-Options"));
new MockFilterChain().doFilter(req, res);
});
assertEquals("nosniff", seenInChain.get(), "체인 진입 시점에 헤더가 있어야 한다");
assertEquals("nosniff", response.getHeader("X-Content-Type-Options"));
assertNotNull(response.getHeader("Cache-Control"));
}
@Test
@DisplayName("기본(lazy) 모드에서는 체인 실행 시점에 헤더가 아직 없다 - eager 설정이 실제로 의미 있음을 고정")
void lazyDoesNotWriteHeadersBeforeChain() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/file/download");
MockHttpServletResponse response = new MockHttpServletResponse();
AtomicReference<String> seenInChain = new AtomicReference<>();
filter(false).doFilter(request, response, (req, res) -> {
seenInChain.set(((HttpServletResponse) res).getHeader("X-Content-Type-Options"));
new MockFilterChain().doFilter(req, res);
});
assertNull(seenInChain.get(), "lazy 모드는 체인 이후에 헤더를 쓴다");
assertEquals("nosniff", response.getHeader("X-Content-Type-Options"));
}
}