commit 330105cb27bd9a7f54441fc436d9082a468216fd Author: koreafood Date: Fri May 29 17:49:25 2026 +0900 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07a8537 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ +/src/main/resources/public/ + diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..8b1952d --- /dev/null +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,118 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed 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 + * + * https://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. + */ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if (mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if (mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if (!outputFile.getParentFile().exists()) { + if (!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..2cc7d4a Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..d2b6f68 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ca1043e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,65 @@ +# CLAUDE.md + +Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed. + +**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. + +## 1. Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +## 2. Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## 3. Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## 4. Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. + +--- + +**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes. \ No newline at end of file diff --git a/build.groovy b/build.groovy new file mode 100644 index 0000000..da33f65 --- /dev/null +++ b/build.groovy @@ -0,0 +1,58 @@ +import java.text.SimpleDateFormat + +node { + def mvnHome + stage('Preparation') { // for display purposes + git([url: 'https://code.sdsdev.co.kr/SDL/Nonsan.git', credentialsId: 'sdlsupport', branch: '5.2.1']) + mvnHome = tool 'M3' + def dateFormat = new SimpleDateFormat("yyyy-MM-dd") + def date = new Date() + today = dateFormat.format(date) + pomVersion = bat(script: 'mvn help:evaluate -Dexpression=project.version -q -DforceStdout', returnStdout: true).trim() + pomVersion = pomVersion.readLines().drop(1).join(" ") + + echo 'project.version=' + pomVersion + echo 'today=' + today + } + environment { + DATE = today + } + stage('Directory Clean') { + bat("start cmd.exe /c rmdir /s/q %USERPROFILE%\\.m2\\repository\\sdl") + bat("start cmd.exe /c rmdir /s/q D:\\deploy\\sdl-5.0.0\\sdl-base") + bat("start cmd.exe /c rmdir /s/q D:\\deploy\\sdl-5.0.0\\sdl-sql") + bat('start cmd.exe /c xcopy sdl-base D:\\deploy\\sdl-5.0.0\\sdl-base\\ /s /e /d /y') + bat('start cmd.exe /c xcopy sdl-sql D:\\deploy\\sdl-5.0.0\\sdl-sql\\ /s /e /d /y') + } + + stage('lib deploy') { + + bat('mvn clean deploy -DaltDeploymentRepository=sdl-release::default::http://admin:admin123@70.118.68.230:8081/repository/sdl-release') + + } + + stage('Build') { + dir('D:\\deploy\\sdl-5.0.0\\sdl-base'){ + bat('mvn compile dependency:sources -DincludeGroupIds=sdl') + bat('start cmd.exe /c rmdir /s/q %USERPROFILE%\\.m2\\repository\\sdl\\sdl-base') + bat('start cmd.exe /c xcopy %USERPROFILE%\\.m2\\repository\\sdl D:\\deploy\\sdl-5.0.0\\sdl-base\\source\\sdl\\ /s /e /d /y') + bat('mvn clean compile dependency:sources -DincludeGroupIds=sdl') + bat("start cmd.exe /c rmdir /s/q D:\\deploy\\sdl-5.0.0\\sdl-base\\target") + } + } + stage('Packaging') { + dir('D:\\deploy\\sdl-5.0.0'){ + bat('start cmd.exe /c bc c D:\\deploy\\sdl-5.0.0\\sdl-base-'+pomVersion+'-'+today+'.zip sdl-base') + bat('start cmd.exe /c bc c D:\\deploy\\sdl-5.0.0\\sdl-ddl-'+pomVersion+'-'+today+'.zip sdl-sql') + } + } + stage('Maven Package') { + dir('D:\\deploy\\sdl-5.0.0\\sdl-base'){ + bat('mvn clean package') + } + } + stage('Success') { + echo 'project.version=' + pomVersion + echo 'today=' + today + } +} \ No newline at end of file diff --git a/doc/.asciidoctorconfig b/doc/.asciidoctorconfig new file mode 100644 index 0000000..eba2e3b --- /dev/null +++ b/doc/.asciidoctorconfig @@ -0,0 +1,21 @@ +// PDF +:pdf-fontsdir: {asciidoctorconfigdir}/fonts +:pdf-themesdir: {asciidoctorconfigdir}/themes +:pdf-theme: ko_KR + +// DOCTYPE, TOC +:doctype: book +:chapter-signifier: +:sectnums: +:sectanchors: +:toc: left +:toclevels: 3 + +:source-highlighter: highlight.js + +// ICON, IMAGE +:icons: font +:stylesheet: css/sdl-manual.css +:linkcss: +:imagesdir: {asciidoctorconfigdir}/img +:data-uri: diff --git a/doc/Appendix/Appendix.adoc b/doc/Appendix/Appendix.adoc new file mode 100644 index 0000000..bcdfec0 --- /dev/null +++ b/doc/Appendix/Appendix.adoc @@ -0,0 +1,13 @@ += Appendix + +:leveloffset: +1 + +include::QuartzClustering.adoc[leveloffset] + +include::Properties암호화툴사용방법.adoc[leveloffset] + +include::uiSDL.adoc[leveloffset] + +include::RedisConfig.adoc[leveloffset] + +include::D-KMS 암호화 적용.adoc[leveloffset] \ No newline at end of file diff --git a/doc/Appendix/D-KMS 암호화 적용.adoc b/doc/Appendix/D-KMS 암호화 적용.adoc new file mode 100644 index 0000000..971c801 --- /dev/null +++ b/doc/Appendix/D-KMS 암호화 적용.adoc @@ -0,0 +1,90 @@ += D-KMS 암호화 적용 + +SDL(표준개발라이브러리)에 D-KMS 암호화를 적용하는 방법에 대해 설명한다. + +== 사전 준비 + +=== KMS 포털에서 암호화 권한 신청 + +. KMS 포털에서 계정을 인증하고 암호화 권한 신청, 결재 +. 암호화 권한 결재 완료되면 D-KMS SDK 및 가이드 전송됨 +. 전달 받은 SDK 설치 및 코드 적용 + +== 코드 적용 + +=== Credential 생성 및 환경변수 추가 + +. D-KMS 가이드를 참고하여 Credential을 생성하고 시스템 환경변수에 추가한다. + +=== SDL 수정 + +. D-KMS 암호화 라이브러리 추가 + - https://code.sec.samsung.net/confluence/pages/viewpage.action?pageId=343332410[D-KMS 가이드]를 참고하여 라이브러리를 추가한다. + +. src/main/resources-{local|dev|prod} 에 dkms.properties 파일을 추가한다. (배포된 sdl-appendix 내 참조) + +IMPORTANT: - dkms.task-id 프로퍼티 값을 D-KMS 신청시 부여받은 값으로 수정해야한다. + + - 기타 다른 프로퍼티 값도 제대로 설정되었는지 확인한다. + +[start=3] +. mybatis typehandler 추가 + - 암호화 대상이 되는 컬럼에 대해 typehandler 를 적용한다. + - com.samsung.dkms.handler 패키지 생성 + - EncryptionTypeHandler.java, NameEncryptionTypeHandler.java, EmailEncryptionTypeHandler.java, PhoneEncryptionTypeHandler.java, AddressEncryptionTypeHandler.java, BirthdayEncryptionTypeHandler.java 파일을 복사/붙여넣기 한다. (배포된 sdl-appendix 내 참조) + +. mybatis-config.xml 수정 + - mapper xml 에 적용이 용이하도록 typehandler의 type alias 를 추가한다. ++ +image::dkms_typehandler_alias.png[] + +[start=5] +. mapper xml 에 mybatis typehandler 적용 + - 암호화가 필요한 컬럼에 해당하는 핸들러를 선택 적용한다. + - mapper xml에 typehandler 적용 방법은 샘플파일(mapper-mybatis-user.xml, 배포된 sdl-appendix 내 참조)을 참고한다. (`*""유무에 유의*`) + * select resultMap 적용 예 ++ +image::dkms_typehandler_select_resultmap.png[] + + * insert, update 적용 예 ++ +image::dkms_typehandler_insert.png[] +image::dkms_typehandler_update.png[] + + * 수정대상 mapper xml (배포판 기준) ++ +---- +mapper-mybatis-common.xml +mapper-mybatis-sample-approval-internal.xml +mapper-mybatis-sample-approval-knox.xml +mapper-mybatis-sample.xml +mapper-mybatis-sys-resource.xml +mapper-mybatis-user-sync.xml +mapper-mybatis-approval.xml +mapper-mybatis-user-role.xml +mapper-mybatis-user.xml +mapper-mybatis-workgroup-role.xml +mapper-mybatis-comment.xml +mapper-mybatis-post.xml +mapper-mybatis-email.xml +mapper-mybatis-mail-group.xml +mapper-mybatis-history.xml +mapper-mybatis-access-log.xml +mapper-mybatis-admin-address.xml +mapper-mybatis-knox-department-sync.xml +mapper-mybatis-knox-department.xml +mapper-mybatis-terms-use.xml +---- + +=== 테이블 수정 및 데이터 마이그레이션 +. 테이블 컬럼 사이즈 변경 + - 암호화 대상 컬럼의 사이즈를 최소 255byte 가 되도록 변경한다. +. 기존 데이터 암호화를 위한 마이그레이션은 D-KMS 가이드를 참고한다. + +CAUTION: *SQL기반의 Database의 경우, 개인정보가 암호화됨으로써 아래와 같은 SQL문에 영향이 발생* (D-KMS 소개자료 중 발췌) + + Nondeterministic Encryption으로 인해 Equality 비교 불가 + + : WHERE, GROUP BY, JOIN (ON), DISTINCT, HAVING, ORDER BY + + -> 일반적으로 Application Level에서 Filtering, Grouping, Joining 구현 필요 + + + + *대안: 별도 Column에 Hash 데이터를 추가 저장* + + Hash로 인해 영향 받는 SQL문: WHERE (비교에 사용하는 데이터 역시 Hash 필요. Equality 비교는 가능하나 LIKE 또는 대소비교 불가), ORDER BY + + -> 단, 모든 데이터가 Hash되어있지 않은 경우 GROUP BY에 영향을 줄 수 있음 \ No newline at end of file diff --git a/doc/Appendix/Properties암호화툴사용방법.adoc b/doc/Appendix/Properties암호화툴사용방법.adoc new file mode 100644 index 0000000..6df81e4 --- /dev/null +++ b/doc/Appendix/Properties암호화툴사용방법.adoc @@ -0,0 +1,170 @@ += Properties 암호화 툴 사용 방법 + +sdl-encrypt는 Spring Framework에서 사용하는 properties를 http://www.jasypt.org/[Jasypt]를 이용해 쉽게 암호화 하기 위한 모듈이다. + +== 사용 방법 + +=== 파일다운로드 + +sdl-encrypt-1.0.0.jar 파일을 다운로드 받아서 특정 디렉토리에 복사한다. (배포된 sdl-appendix 내 참조) + +sdl-encypt-1.0.0.jar 파일을 실행하기 위해서는 JDK 8 이상의 버전이 설치 되어 있어야 한다. + +---- +C:\encrypt>dir + C 드라이브의 볼륨에는 이름이 없습니다. + 볼륨 일련 번호: 1841-973A + + C:\encrypt 디렉터리 + +2022-08-09 오후 02:17 . +2022-08-09 오후 02:17 .. +2022-08-09 오후 02:15 9,114,144 sdl-encrypt-1.0.0.jar + +---- + +''' + +=== 암호화 key 파일 생성 + +같은 폴더에 암호화를 위한 key 파일을 sdl-encrypt-1.0.0.jar 파일을 있는 폴더에 생성한다. + +key파일의 이름은 *jasypt.key* 로 한다. jasypt.key 파일은 어플리케이션 실행 시 서버에서 읽어 복호화 할때 필요하다. + +*jasypt.key 파일의 내용이 유출 될 경우 문제가 생길 수 있으니 형상관리에 저장하지 않도록 한다.* + +*jasypt.key 샘플* + +---- +fhasiuk23dfjhasoijcnaoijfoase0342ruq0 + +---- + +jasypt.key 파일의 내용은 프로젝트에서 랜덤한 문자열로 생성한다. + +''' + +=== Properties 파일 생성 + +config.properties 파일에 암호화가 필요한 properties를 작성해 sdl-encrypt-1.0.0.jar 파일이 있는 폴더에 생성한다. + +*config.properties 파일 샘플* + +---- +# Datasource(Oracle) +datasource.driver=net.sf.log4jdbc.sql.jdbcapi.DriverSpy +datasource.url=jdbc:log4jdbc:oracle:thin:@10.127.72.226:1521:XE +datasource.username=sdl5 +datasource.password=dlatl#00 + +---- + +''' + +=== 암호화 하기 + +실행 전에 위의 모든 파일들이 같은 폴더에 위치하는지 확인한다. + +---- +C:\encrypt>dir + C 드라이브의 볼륨에는 이름이 없습니다. + 볼륨 일련 번호: 1841-973A + + C:\encrypt 디렉터리 + +2022-08-09 오후 02:29 . +2022-08-09 오후 02:29 .. +2022-08-08 오후 05:14 407 config.properties +2022-08-09 오후 02:29 37 jasypt.key +2022-08-09 오후 02:15 9,114,144 sdl-encrypt-1.0.0.jar + 3개 파일 9,114,588 바이트 + +---- + +*java -jar sdl-encrypt-1.0.0.jar* 실행 + +---- +C:\encrypt>java -jar sdl-encrypt-1.0.0.jar + + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v2.7.2) + +2022-08-09 14:30:33.152 INFO 47544 --- [ main] com.samsung.EncryptApplication : Starting EncryptApplication v1.0.0 using Java 1.8.0_341 on DESKTOP-BBNYDORY with PID 47544 (C:\encrypt\sdl-encrypt-1.0.0.jar started by bbnydory in C:\encrypt) +2022-08-09 14:30:33.157 INFO 47544 --- [ main] com.samsung.EncryptApplication : No active profile set, falling back to 1 default profile: "default" +2022-08-09 14:30:33.456 INFO 47544 --- [ main] ptablePropertiesBeanFactoryPostProcessor : Post-processing PropertySource instances +2022-08-09 14:30:33.458 INFO 47544 --- [ main] c.u.j.EncryptablePropertySourceConverter : Skipping PropertySource configurationProperties [class org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource +2022-08-09 14:30:33.461 INFO 47544 --- [ main] c.u.j.EncryptablePropertySourceConverter : Converting PropertySource systemProperties [org.springframework.core.env.PropertiesPropertySource] to EncryptableMapPropertySourceWrapper +2022-08-09 14:30:33.461 INFO 47544 --- [ main] c.u.j.EncryptablePropertySourceConverter : Converting PropertySource systemEnvironment [org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor$OriginAwareSystemEnvironmentPropertySource] to EncryptableSystemEnvironmentPropertySourceWrapper +2022-08-09 14:30:33.463 INFO 47544 --- [ main] c.u.j.EncryptablePropertySourceConverter : Converting PropertySource random [org.springframework.boot.env.RandomValuePropertySource] to EncryptablePropertySourceWrapper +2022-08-09 14:30:33.464 INFO 47544 --- [ main] c.u.j.EncryptablePropertySourceConverter : Converting PropertySource Config resource 'class path resource [application.properties]' via location 'optional:classpath:/' [org.springframework.boot.env.OriginTrackedMapPropertySource] to EncryptableMapPropertySourceWrapper +2022-08-09 14:30:33.500 INFO 47544 --- [ main] c.u.j.filter.DefaultLazyPropertyFilter : Property Filter custom Bean not found with name 'encryptablePropertyFilter'. Initializing Default Property Filter +2022-08-09 14:30:33.551 INFO 47544 --- [ main] c.u.j.r.DefaultLazyPropertyResolver : Property Resolver custom Bean not found with name 'encryptablePropertyResolver'. Initializing Default Property Resolver +2022-08-09 14:30:33.553 INFO 47544 --- [ main] c.u.j.d.DefaultLazyPropertyDetector : Property Detector custom Bean not found with name 'encryptablePropertyDetector'. Initializing Default Property Detector +2022-08-09 14:30:33.611 INFO 47544 --- [ main] com.samsung.EncryptApplication : Started EncryptApplication in 0.765 seconds (JVM running for 1.669) +2022-08-09 14:30:34.114 INFO 47544 --- [ main] com.samsung.EncryptService : datasource.username=ENC(fOdjJKVr6fkmdS46JlsCRw==) +2022-08-09 14:30:34.115 INFO 47544 --- [ main] com.samsung.EncryptService : datasource.password=ENC(xfWl/qeA7tIMQ10UyU7IInz+SyY6ovX9) +2022-08-09 14:30:34.117 INFO 47544 --- [ main] com.samsung.EncryptService : datasource.url=ENC(Ocp24DAK1cq0f8TezLyMpcedSu6nLdTACSIYyMoCnuanSJT5x76lw8yM0reAeu4qBrBF+yvItVyCBmtPlIv70A==) +2022-08-09 14:30:34.119 INFO 47544 --- [ main] com.samsung.EncryptService : datasource.driver=ENC(GJ527jBO/7Xjilg8WbQHk55GwVUAvc6fIa1Yai1o68WeimiE5BpHNj3e7YnjhtQR) + +---- + +''' + +=== 확인하기 + +암호화가 정상적으로 실행 됐다면 같은 폴더에 config-enc.properties 파일이 생성된 것을 확인 할 수 있다. + +---- +C:\encrypt>dir + C 드라이브의 볼륨에는 이름이 없습니다. + 볼륨 일련 번호: 1841-973A + + C:\encrypt 디렉터리 + +2022-08-09 오후 02:30 . +2022-08-09 오후 02:30 .. +2022-08-09 오후 02:30 305 config-enc.properties +2022-08-08 오후 05:14 407 config.properties +2022-08-09 오후 02:29 37 jasypt.key +2022-08-09 오후 02:15 9,114,144 sdl-encrypt-1.0.0.jar + 4개 파일 9,114,893 바이트 + +---- + +config-enc.properties 파일내용 + +---- +datasource.username=ENC(fOdjJKVr6fkmdS46JlsCRw==) +datasource.password=ENC(xfWl/qeA7tIMQ10UyU7IInz+SyY6ovX9) +datasource.url=ENC(Ocp24DAK1cq0f8TezLyMpcedSu6nLdTACSIYyMoCnuanSJT5x76lw8yM0reAeu4qBrBF+yvItVyCBmtPlIv70A==) +datasource.driver=ENC(GJ527jBO/7Xjilg8WbQHk55GwVUAvc6fIa1Yai1o68WeimiE5BpHNj3e7YnjhtQR) + +---- + +''' + +== 고급 사용 + +sdl-encrypt 모듈을 Spring Boot 프로젝트로 되어 있고 application.properties 파일에 암호화 key파일, Properties 파일, 출력파일에 대한 경로가 작성되어 있다. + +application.properties 내용 + +[source,properties] +---- +algorithm=PBEWithMD5AndDES +keyObtentionIterations=1000 +pollSize=1 +stringOutputType=base64 +keyFilePath=./jasypt.key +propertiesFilePath=./config.properties +outputPropertiesFilePath=./config-enc.properties +---- + +위 내용의 변경이 필요하면 + +sdl-encrypt-1.0.0.jar 파일이 있는 위치에 application.properties 파일을 새로 작성하고 위의 값들을 오버라이딩해서 사용할 수 있다. \ No newline at end of file diff --git a/doc/Appendix/QuartzClustering.adoc b/doc/Appendix/QuartzClustering.adoc new file mode 100644 index 0000000..f1e2f72 --- /dev/null +++ b/doc/Appendix/QuartzClustering.adoc @@ -0,0 +1,221 @@ += Quartz Clustering with JDBC-JobStore + +== JDBC-JobStore DB 초기화 +quartz db schema (배포된 sdl-appendix 내 참조) 를 사용하는 DBMS에 맞는 SQL을 실행해 JDBC-JobStore를 사용하기 위한 Table을 생성한다. + +CAUTION: Tibero DB는 미지원 + +== application.properties +properties 파일 내용 중 Quartz JDBC-JobStore 관련 설정이다. +[source, properties] +---- +# Spring Quartz 설정 +spring.quartz.job-store-type=jdbc +spring.quartz.jdbc.initialize-schema=always + +spring.quartz.datasource.driver-class-name=org.postgresql.Driver +spring.quartz.datasource.jdbcUrl=jdbc:postgresql://10.40.87.189:5445/SDL +spring.quartz.datasource.username=sdl5 +spring.quartz.datasource.password=dlatl#123 + +spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate <1> +spring.quartz.properties.org.quartz.jobStore.isClustered=true +spring.quartz.properties.org.quartz.jobStore.misfireThreshold=60000 +spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval=15000 +spring.quartz.properties.org.quartz.scheduler.instanceName=sdl-prod <2> +spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO <3> +spring.quartz.properties.org.quartz.scheduler.rmi.export=false +spring.quartz.properties.org.quartz.scheduler.rmi.proxy=false +spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool +spring.quartz.properties.org.quartz.threadPool.threadCount=10 <4> +spring.quartz.properties.org.quartz.threadPool.threadPriority=5 +spring.quartz.properties.org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true +spring.quartz.properties.org.quartz.scheduler-name=QuartzScheduler +---- + +<1> DB 에 따라 Delegate 를 변경한다. ++ +org.quartz.impl.jdbcjobstore.oracle.OracleDelegate, + +org.quartz.impl.jdbcjobstore.MSSQLDelegate, + +org.quartz.impl.jdbcjobstore.PostgreSQLDelegate +<2> Quartz Scheduler 를 구별하는 Property 값으로 Clustering 할 서버에서는 같은 값을 세팅한다. +<3> Unique 한 값이어야 하며 AUTO 일 경우 자동 생성된다. +<4> Job 을 실행하기 위한 Thread 수 + +기타 자세한 Property 값은 http://www.quartz-scheduler.org/documentation/quartz-2.3.0/configuration/ConfigJDBCJobStoreClustering.html[Quartz 메뉴얼, window=_blank]을 참고 한다. + +== QuartzClusteringConfig + +QuartzClusteringConfig 파일은 Spring 에 등록된 CronTriggerFactoryBean 을 실행 하도록 SchedulerFactoryBean이 정의되어 있다. + +[source, java] +---- +@Bean +public SchedulerFactoryBean setSchedulerFactoryBean(ApplicationContext applicationContext) { + SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); + + AutoWiringSpringBeanJobFactory jobFactory = new AutoWiringSpringBeanJobFactory(); + jobFactory.setApplicationContext(applicationContext); + + schedulerFactoryBean.setJobFactory(jobFactory); <1> + schedulerFactoryBean.setDataSource(quartzDataSource()); <2> + schedulerFactoryBean.setApplicationContext(applicationContext); + schedulerFactoryBean.setTransactionManager(quartzTransactionManager()); <3> + schedulerFactoryBean.setWaitForJobsToCompleteOnShutdown(true); + schedulerFactoryBean.setStartupDelay(60); + schedulerFactoryBean.setOverwriteExistingJobs(true); + + Properties properties = new Properties(); + properties.putAll(quartzProperties.getProperties()); <4> + schedulerFactoryBean.setQuartzProperties(properties); + + schedulerFactoryBean.setTriggers( + knoxSyncBatchConfig.knoxApprovalSyncTrigger().getObject(), + userBatchConfig.batchUserLongTermCheckTrigger().getObject(), + userBatchConfig.batchUserAuthExpiredTrigger().getObject(), + sysUseLogBatchConfig.batchSysUseLogTrigger().getObject(), + sysUseLogBatchConfig.batchMenuUseHistoryTrigger().getObject() + ); <5> + + return schedulerFactoryBean; +} + +@Bean +@ConfigurationProperties(prefix = "spring.quartz.datasource") +@QuartzDataSource <2> +public DataSource quartzDataSource() { + return DataSourceBuilder.create().type(HikariDataSource.class).build(); +} + +@Bean +@QuartzTransactionManager <3> +public DataSourceTransactionManager quartzTransactionManager() { + return new DataSourceTransactionManager(quartzDataSource()); +} +---- + +<1> Spring Bean을 AutoWiring +<2> Job 정보를 조회, 저회 저장할 때 사용하는 Datasource (resources-dev, resources-prod/application.properties 참조) +<3> Quartz 전용 TransactionManager +<4> Quartz 설정 파일 세팅 +<5> SchedulerFactoryBean 에서 실행 할 Trigger + +== JobDetail 및 Trigger Bean 등록 +아래는 KnoxSyncBatchConfig에 수행할 Job 클래스를 설정한 JobDetail Bean과 Trigger Bean을 등록한 예이다. + +.KnoxSyncBatchConfig.java +[source, java] +---- +@Value("${knox.approval.sync.cron}") +private String knoxApprovalSyncCron; + +@Bean +public JobDetail knoxApprovalSyncJobDetail() { + return JobBuilder + .newJob(KnoxSyncBatchExecutor.class) <1> + .withIdentity("knoxApprovalSyncJob") + .withDescription("Knox Approval Sync Batch") + .storeDurably(true) + .build(); +} + +@Bean +public CronTriggerFactoryBean knoxApprovalSyncTrigger() { + CronTriggerFactoryBean approvalTrigger = new CronTriggerFactoryBean(); + approvalTrigger.setJobDetail(knoxApprovalSyncJobDetail()); + approvalTrigger.setCronExpression(knoxApprovalSyncCron); + return approvalTrigger; +} +---- + +<1> 배치잡 실행시 QuartzJobBean을 상속 받은 KnoxSyncBatchExecutor의 executeInternal 메소드가 실행된다. + +== Job 구현 +QuartzJobBean은 Spring Bean을 DI Job의 구현체에서 사용할 수 있도록 Job을 구현한 추상 클래스이며, +QuartzJobBean을 상속 받아 executeInternal 메소드를 오버라이드하여 실행할 배치 Service의 메소드를 호출한다. + +.KnoxSyncBatchExecutor.java +[source,java] +---- +@Component +@Log4j2 +public class KnoxSyncBatchExecutor extends QuartzJobBean { + + @Value("${node-id}") + private String nodeId; + + @Autowired + private KnoxApprovalSyncService knoxApprovalSyncService; + + @Override + protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException { + JobDetail jobDetail = jobExecutionContext.getJobDetail(); + log.info(new StringBuilder() + .append("nodeId : ").append(nodeId).append(", ") + .append("jobName : ").append(jobDetail.getKey().getName()).append(", ") + .append("description : ").append(jobDetail.getDescription()) + ); + knoxApprovalSyncService.synchronizeKnox(); + } +} +---- + +CAUTION: UserBatchExecutor의 경우 JobKey(JobBuilder.withIdentity 설정 값)를 이용해 분기 처리 하고 있으므로 Job 등록시 Name 명 지정에 주의하도록 한다. + +.UserBatchConfig.java +[source, java] +---- +/** + * 장기 미사용자 관리 Job + */ +@Bean +public JobDetail batchUserLongTermCheckJob() { + return JobBuilder + .newJob(UserBatchExecutor.class) + .withIdentity("batchUserLongTermCheck") + .withDescription("User LongTerm Check Batch") + .storeDurably(true) + .build(); +} + +/** + * 만료 권한 삭제 Job + */ +@Bean +public JobDetail batchUserAuthExpiredJob() { + return JobBuilder + .newJob(UserBatchExecutor.class) + .withIdentity("batchUserAuthExpired") + .withDescription("User Auth Expired Batch") + .storeDurably(true) + .build(); +} +---- + +.UserBatchExecutor.java +[source, java] +---- +@Override +protected void executeInternal(JobExecutionContext jobExecutionContext) { + JobDetail jobDetail = jobExecutionContext.getJobDetail(); + String jobName = jobDetail.getKey().getName(); + log.info(new StringBuilder() + .append("nodeId : ").append(nodeId).append(", ") + .append("jobName : ").append(jobName).append(", ") + .append("description : ").append(jobDetail.getDescription()) + ); + switch (jobName) { + case "batchUserLongTermCheck" -> userBatchService.execUserLongTermCheck(); + case "batchUserAuthExpired" -> userBatchService.execUserAuthValidCheck(); + case "batchUserAuthExpiredMailing" -> { + try { + userBatchService.execUserAuthValidAlarmMail(); + } catch (IOException | MessagingException e) { + log.error(e.getMessage(), e); + } + } + default -> { + } + } +} +---- diff --git a/doc/Appendix/RedisConfig.adoc b/doc/Appendix/RedisConfig.adoc new file mode 100644 index 0000000..2257254 --- /dev/null +++ b/doc/Appendix/RedisConfig.adoc @@ -0,0 +1,46 @@ += Redis 설정 + +== RedisConfig +SDL은 어플리케이션 속도 향상을 위해 자주 사용하는 Data를 Cache하고 있다. +기본적으로 Local, Dev 환경에서는 Spring에서 제공하고 있는 ConcurrentMapCacheManager를 사용해 +Data를 Cache하고 있지만 Clustering 환경에서 Cache Replicated를 위해서 +Redis를 사용한다. + +IMPORTANT: Ehcache 3부터 RMI, JMS 방식이 Cache 동기화 지원이 중단 됐으니 Cache 복제가 필요한 경우 Redis를 반드시 사용해야 한다. + +application.properties +---- +spring.cache.type=redis +spring.data.redis.host=redis 서버 주소 +spring.data.redis.port=redis 포트 +---- + +RedisConfig +---- +@Bean +public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() { + return (builder) -> builder + .withCacheConfiguration("message-all", + RedisCacheConfiguration.defaultCacheConfig()) + .withCacheConfiguration("message", + RedisCacheConfiguration.defaultCacheConfig()) + .withCacheConfiguration("menu-all", + RedisCacheConfiguration.defaultCacheConfig()) + .withCacheConfiguration("menu-label-all", + RedisCacheConfiguration.defaultCacheConfig()) + .withCacheConfiguration("menu-full-path-all", + RedisCacheConfiguration.defaultCacheConfig()) + .withCacheConfiguration("page-full-path-all", + RedisCacheConfiguration.defaultCacheConfig()) + .withCacheConfiguration("api-full-path-all", + RedisCacheConfiguration.defaultCacheConfig()) + .withCacheConfiguration("api-user", + RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(300))) + .withCacheConfiguration("api-user-menu", + RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(300))) + .withCacheConfiguration("page-all-by-menu-auth", + RedisCacheConfiguration.defaultCacheConfig()); +} +---- + +각 Cache별 필요한 세팅은 redisCacheManagerBuilderCustomizer 메소드에서 할수 있으니 시스템 환경에 맞춰 적절하게 수정한다. \ No newline at end of file diff --git a/doc/Appendix/uiSDL.adoc b/doc/Appendix/uiSDL.adoc new file mode 100644 index 0000000..2ac73b2 --- /dev/null +++ b/doc/Appendix/uiSDL.adoc @@ -0,0 +1,272 @@ += UI 라이브러리(uiSDL) + +== UI 라이브러리(uiSDL)란? +UI라이브러리(uiSDL)은 웹 UI 개발 리드타임 단축을 위하여 실무에 유용한 컴포넌트들을 담은 UI라이브러리입니다. + +Vue2 및 Vue3 용 라이브러리가 나뉘어져 있습니다. + +NOTE: UI라이브러리 시스템 접속 + +vue2 : http://uisdl.scp.samsung.net/v2/[window=_blnak] + +vue3 : http://uisdl.scp.samsung.net/v3/[window=_blnak] + +IMPORTANT: 접속이 안되는 경우 window host 파일에 아래와 같이 추가해 주시기 바랍니다 + +10.195.53.147 uisdl.scp.samsung.net + + +== 컴포넌트 주요 특징 + +- 삼성전자 웹 시스템 개발 시 빠르게 웹 화면을 개발할 수 있도록 사용빈도가 높은 UI 환경 구성 요소들을 오픈 소스 Framework인 Vue.js와 Bootstrap 기반으로 제작하여 제공합니다. + +- 시스템의 특성에 맞게 제작이 가능하도록 화면의 구조인 레이아웃 6종과 단일 UI구성요소인 컴포넌트 37종으로 구성되어 있습니다 ++ +image::uisdl_1.png[] + +- 각 페이지 화면의 우측 상단에 개발과 디자인 탭으로 구분되어 있습니다. ++ +개발 탭에서는 개발 가이드와 간단한 기능 테스트를 해볼 수 있는 Code Demo 가 있고, 디자인 탭에서는 전사 UX 표준을 바탕으로 디자인 원칙과 구성요소들을 확인할 수 있습니다. ++ +image::uisdl_2.png[] + +== 컴포넌트 개발 환경 +* VueJS 버전 3 +* Used Open-source +** bootstrap: 5.3.0 or Higher +** vue-datepicker-next: 1.0.3 or Higher + +== 컴포넌트 적용 방법 + +. Install from Local file ++ +로컬 파일로부터 컴포넌트를 설치할 수 있습니다. 아래의 경로에서 파일을 다운로드 받습니다. ++ +http://uisdl.scp.samsung.net/v3/download/sdl-component-3.0.0.tgz[sdl-component-3.0.0.tgz 다운로드] ++ +해당 파일을 로컬의 임의의 위치로 복사한 다음, NPM 명령어로 컴포넌트를 설치합니다. 설치하실 프로젝트 루트 폴더로 이동한 다음, 아래 명령어를 커맨드 창에서 입력합니다. ++ +[source, shell] +---- +npm install --save ./{your-path}/sdl-component-{version}.tgz +---- + +. Import bootstrap ++ +'bootstrap' 관련 스타일과 스크립트를 Entry Point 파일에 Import 합니다. ++ +[source, javascript] +---- +// bootstrap css등록 및 popper가 포함된 bootstrap bundle js 임포트 +import 'bootstrap/dist/css/bootstrap.min.css'; +import 'bootstrap/dist/js/bootstrap.bundle.min'; +---- + +. Import sdl-component ++ +'sdl-component' 관련 스타일과 스크립트를 Entry Point 파일에 Import 하고 ++ +[source, javascript] +---- +import 'sdl-component/src/assets/css/custom.css'; +import SdlComponent from 'sdl-component'; +---- ++ +전역 Vue(app)에서 사용할 수 있도록 Plugin을 등록합니다. ++ +[source, javascript] +---- +app.use(SdlComponent); +---- + +. Complete ++ +이제 시스템 전역에서 'sdl-component'를 사용하실 수 있습니다 + +== UI 라이브러리 목록 +활용빈도, 사용성을 고려하여 사내시스템 개발에 필요한 Library를 제공합니다. + +* 레이아웃 ++ +화면의 구조를 정의하는 레이아웃 유형 ++ +[cols="7,33,60",options="header"] +|=== +|No |Library |설명 + +|1 +|GNB + Contents +|GNB와 콘텐츠로 구성된 화면 형태 + +|2 +|GNB + LNB + Contents +|GNB, LNB, 콘텐츠로 구성된 화면 형태 + +|3 +|GNB + LNB + Contents + Footer +|GNB, LNB, Footer, 콘텐츠로 구성된 화면 형태 + +|4 +|GNB + LNB + Aside + Contents + Footer +|GNB, LNB, Footer, Aside, 콘텐츠로 구성된 화면 형태 + +|5 +|Popup +|현재 화면에서 추가적으로 띄우는 레이어 형태의 창으로, 사용 유형에 따라 구별하여 사용 + +|6 +|Center Frame +|본 화면의 상하좌우 중앙에 콘텐츠 박스가 위치 +|=== + +* 컴포넌트 ++ +화면을 구성하는 요소 단위 ++ +[cols="7,33,60",options="header"] +|=== +|No |Library |설명 +|1 +|GNB +|(Global Navigation Bar) 시스템 명과 메뉴를 포함한 네비게이션으로 화면의 최 상단에 구성 + +|2 +|LNB +|(Left Navigation Bar) 서브메뉴라고 불리며 화면의 좌측에 구성 + +|3 +|Aside +|자주 사용하는 기능을 빠르게 접근(Quick Access)할 수 있도록 제공하는 영역 + +|4 +|Footer +|Copyright, 사이트 맵, 이용약관 등의 정보를 포함하며, 화면의 하단에 구성 + +|5 +|Card List +|한 레이아웃에서 여러 개의 카드를 함께 사용할 때 사용 + +|6 +|Search Grid +|검색조건을 입력/선택하는 조회 영역과 데이터결과를 호출하는 리스트영역으로 구성할 때 사용 + +|7 +|Form +|여러 개의 컨트롤(Input Box, Radio Button, Dropdown Box 등)를 활용하여 하나의 입력 폼으로 구성할 때 사용 + +|8 +|Form Detail +|폼 화면에서 입력한 정보를 화면에 표시할 때 사용 + +|9 +|Board Detail +|게시판 게시물의 기본적인 상세 화면  + +|10 +|Tree Detail +|계층 구조를 나타내는 트리 요소와 트리에서 선택한 정보를 페이지 전환 없이 즉각적으로 보여줄 때 사용 + +|11 +|Create Board +|게시판 게시물의 기본적인 입력 화면 + +|12 +|Approval +|시스템에서 Knox 결재나 시스템 내부 결재시 사용 + +|13 +|Reply +|간단하게 사용자 간의 의사소통을 할 수 있는 폼 형태로 구성 + +|14 +|Notice +|사용자에게 시스템 내 서비스 관련 정보들을 공지 시 사용 + +|15 +|Alert +|공지나 안내 사항 같은 부가적인 정보, 경고 상태 혹은 사용자의 행동에 대한 즉각적인 피드백이 필요할 때 사용 + +|16 +|Badge +|알림 목적으로 사용하거나, 데이터를 시각적으로 강조하고 싶을 때 사용 + +|17 +|Button +|사용자가 클릭하여 기능을 수행할 수 있는 그래픽 요소 + +|18 +|Card +|다양한 정보들을 그룹화하여 하나의 카드 모양 안에 구성할 때 사용 + +|19 +|Collapse(Toggle) +|사용자가 두개의 반대되는 상태 중에서 선택할 수 있는 온오프 스위치 + +|20 +|Tooltip +|페이지 요소 또는 기능에 대한 추가 정보를 제공하는 간단하고 유용한 메시지 + +|21 +|Text Input +|입력 폼 요소 중 한 줄의 비교적 짧은 데이터를 입력할 경우 사용 + +|22 +|Textarea +|입력 폼 요소 중 데이터의 내용이 일정하지 않거나, 여러 줄의 데이터를 입력할 경우 사용 + +|23 +|Radio +|두개 이상의 항목 중 하나를 선택할 때 사용 + +|24 +|Select +|사용자가 옵션 중 하나 또는 다중으로 선택할 수 있을 때 사용 + +|25 +|Checkbox +|여러 개의 항목 중 하나 이상을 다중 선택할 때 사용하거나 On/Off의 Toggle 경우에 사용 + +|26 +|Table +|데이터/정보를 열과 행으로 구분하여 사용자가 쉽게 읽고 이해할 수 있도록 제공 + +|27 +|Progress +|사용자 액션에 대한 즉각 데이터가 표시가 어려운 경우(데이터의 로딩 처리 시간이 긴 경우) 작업의 진행 상태에 대한 정확한 피드백을 제공하기 위해 사용 + +|28 +|Spinner +|사용자 액션에 대한 즉각 데이터가 표시가 어려운 경우(데이터의 로딩 처리 시간이 긴 경우) 작업의 진행 상태에 대한 정확한 피드백을 제공하지 못할 경우 사용 + +|29 +|Navbar +|시스템 명과 메뉴를 포함한 네비게이션 영역으로 주로 상단(GNB)에 제공 + +|30 +|File Attachment +|사용자가 첨부파일을 다운받거나 업로드할 경우에 사용 + +|31 +|Tree +|목록의 계층 구조를 표현하기 위해 사용하는 요소 + +|32 +|Datepicker +|날짜를 선택하고 그에 해당하는 데이터를 얻을 수 있도록 위젯 형태로 제공 + +|33 +|Breadcrumb +|시스템 내 사용자의 메뉴의 이동경로를 의미하며 현재 사용자의 위치를 보여줄 때 사용 + +|34 +|Pagination +|많은 양의 데이터를 여러 페이지로 나누고, 이전, 다음 페이지 혹은 특정 페이지로 이동할 수 있는 일련의 링크를 목록 하단에 배치하여 제공 + +|35 +|Tab(Basic, Progress) +|페이지 이동 및 시점 전환 없이 그룹화된 콘텐츠를 제공할 때 사용 + +|36 +|Jumbotron +|어떤 특별한 내용이나 정보를 눈에 띄게 보여주기 위한 박스 형태의 공간 + +|37 +|Carousel +|한 공간에 여러 개의 콘텐츠를 슬라이드 형태로 제공 +|=== \ No newline at end of file diff --git a/doc/css/sdl-manual.css b/doc/css/sdl-manual.css new file mode 100644 index 0000000..e547af2 --- /dev/null +++ b/doc/css/sdl-manual.css @@ -0,0 +1,460 @@ +/* @import "https://fonts.googleapis.com/css?family=Karla:400,700|Montserrat:400,700"; !normalize.css v2.1.2 | MIT License | git.io/normalize*/ +@font-face { + font-family: 'D2 coding'; + font-style: normal; + font-weight: 400; + src: url('../fonts/D2Coding.eot'); + src: local('※'), local('D2Coding'), + url('../fonts/D2Coding.eot?#iefix') format('embedded-opentype'), + url('../fonts/D2Coding.woff') format('woff'); +} +@font-face { + font-family: 'D2 coding'; + font-style: normal; + font-weight: 700; + src: url('../fonts/D2CodingBold.eot'); + src: local('※'), local('D2Coding Bold'), + url('../fonts/D2CodingBold.eot?#iefix') format('embedded-opentype'), + url('../fonts/D2CodingBold.woff') format('woff'); +} +article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section{display:block} +audio,video{display:inline-block} +audio:not([controls]){display:none;height:0} +html{font-family:"D2 coding";-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%} +a{background:none} +a:focus{outline:thin dotted} +a:active,a:hover{outline:0} +h1{font-size:2em;margin:.67em 0} +abbr[title]{border-bottom:1px dotted} +b,strong{font-weight:bold} +dfn{font-style:italic} +hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0} +mark{background:#ff0;color:#000} +code,kbd,pre,samp{font-family:"D2 coding";font-size:1em} +pre{white-space:pre-wrap} +q{quotes:"\201C" "\201D" "\2018" "\2019"} +small{font-size:80%} +sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} +sup{top:-.5em} +sub{bottom:-.25em} +img{border:0} +svg:not(:root){overflow:hidden} +figure{margin:0} +fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em} +legend{border:0;padding:0} +button,input,select,textarea{font-family:inherit;font-size:100%;margin:0} +button,input{line-height:normal} +button,select{text-transform:none} +button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer} +button[disabled],html input[disabled]{cursor:default} +input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0} +button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0} +textarea{overflow:auto;vertical-align:top} +table{border-collapse:collapse;border-spacing:0} +*,*::before,*::after{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box} +html,body{font-size:14px} +body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"D2 coding";font-weight:400;font-style:normal;line-height:1;position:relative;cursor:auto;tab-size:4;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased} +a:hover{cursor:pointer} +img,object,embed{max-width:100%;height:auto} +object,embed{height:100%} +img{-ms-interpolation-mode:bicubic} +.left{float:left!important} +.right{float:right!important} +.text-left{text-align:left!important} +.text-right{text-align:right!important} +.text-center{text-align:center!important} +.text-justify{text-align:justify!important} +.hide{display:none} +img,object,svg{display:inline-block;vertical-align:middle} +textarea{height:auto;min-height:50px} +select{width:100%} +.center{margin-left:auto;margin-right:auto} +.stretch{width:100%} +.subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;font-weight:800;margin-top:0;margin-bottom:.25em} +div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0;direction:ltr} +a{color:#2156a5;text-decoration:underline;line-height:inherit} +a:hover,a:focus{color:#1d4b8f} +a img{border:0} +p{font-family:inherit;font-weight:400;font-size:1em;line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility} +p aside{font-size:.875em;line-height:1.35;font-style:italic} +h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-weight:300;font-style:normal;color:inherit;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em} +h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0} +h1{font-size:2.125em} +h2{font-size:1.6875em} +h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em} +h4,h5{font-size:1.125em} +h6{font-size:1em} +hr{border:solid #dddddf;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em;height:0} +em,i{font-style:italic;line-height:inherit} +strong,b{font-weight:bold;line-height:inherit} +small{font-size:60%;line-height:inherit} +code{font-family:D2 coding,Monaco,Menlo,Consolas,courier new,monospace;font-weight:400;color:rgba(0,0,0,.9)} +ul,ol,dl{font-size:1em;line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit} +ul,ol{margin-left:1.5em} +ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0;font-size:1em} +ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit} +ul.square{list-style-type:square} +ul.circle{list-style-type:circle} +ul.disc{list-style-type:disc} +ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0} +dl dt{margin-bottom:.3125em;font-weight:bold} +dl dd{margin-bottom:1.25em} +abbr,acronym{text-transform:uppercase;font-size:90%;color:rgba(0,0,0,.8);border-bottom:1px dotted #ddd;cursor:help} +abbr{text-transform:none} +blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd} +blockquote cite{display:block;font-size:.9375em;color:rgba(0,0,0,.6)} +blockquote cite::before{content:"\2014 \0020"} +blockquote cite a,blockquote cite a:visited{color:rgba(0,0,0,.6)} +blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)} +table{background:#fff;margin-bottom:1.25em;border:solid 1px #cacaca;border-spacing:0} +table thead,table tfoot{background:#f7f8f7;font-weight:700} +table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:#000;text-align:left} +table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:#000} +table tr td{word-break:break-all} +table tr.even,table tr.alt{background:#fafafa} +table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{display:table-cell;line-height:1.6} +body{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;tab-size:4} +h1,h2,h3,#toctitle,.sidebarblock > .content > .title,h4,h5,h6{line-height:1.2;word-spacing:-.05em} +.clearfix:before,.clearfix:after,.float-group:before,.float-group:after{content:" ";display:table} +.clearfix:after,.float-group:after{clear:both} +:not(pre) > code{font-size:.8525em;font-style:normal!important;letter-spacing:0;padding:.1em .3em .2em;background-color:rgba(27,31,35,.05);border-radius:4px;text-rendering:optimizeSpeed} +pre,pre > code{line-height:1.85;color:rgba(0,0,0,.9);font-family:D2 coding,Monaco,Menlo,Consolas,courier new,monospace;font-weight:400;text-rendering:optimizeSpeed;word-break:normal} +pre{overflow:auto} +em em{font-style:normal} +strong strong{font-weight:400} +.keyseq{color:#6b625c} +kbd{font-family:D2 coding,Monaco,Menlo,Consolas,courier new,monospace;display:inline-block;color:#000;font-size:.65em;line-height:1.45;background-color:#f7f7f7;border:1px solid #ccc;-webkit-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em #fff inset;box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em #fff inset;margin:0 .15em;padding:.2em .5em;vertical-align:middle;position:relative;top:-.1em;white-space:nowrap} +.keyseq kbd:first-child{margin-left:0} +.keyseq kbd:last-child{margin-right:0} +.menuseq,.menu{color:#191715} +b.button:before,b.button:after{position:relative;top:-1px;font-weight:400} +b.button:before{content:"[";padding:0 3px 0 2px} +b.button:after{content:"]";padding:0 2px 0 3px} +p a > code:hover{color:rgba(0,0,0,.9)} +#toc{border-bottom:1px solid #ddddd8;padding-bottom:.5em} +#toc > ul{margin-left:.125em} +#toc ul.sectlevel0 > li > a{font-style:italic} +#toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0} +#toc ul{list-style-type:none} +#toc li{line-height:1.3334} +#toc a{text-decoration:none} +#toc a:active{text-decoration:underline} +#toctitle{color:#0b0a0a;font-size:1.2em} +body.toc2{padding-top:90px;text-rendering:optimizeLegibility} +#content #toc{border-style:solid;border-width:1px;border-color:#d7d7d7;margin-bottom:1.25em;padding:1.25em;background:#f1f1f1;-webkit-border-radius:4px;border-radius:4px} +#content #toc > :first-child{margin-top:0} +#content #toc > :last-child{margin-bottom:0} +#footer{padding-bottom:2rem} +#footer #footer-text{padding:2rem 0;border-top:1px solid #efefed} +#footer-text{color:rgba(0,0,0,.6);line-height:1.44} +.sect1{padding-bottom:.625em} +.sect1 + .sect1{border-top:1px solid #efefed} +#content h1 > a.anchor,h2 > a.anchor,h3 > a.anchor,#toctitle > a.anchor,.sidebarblock > .content > .title > a.anchor,h4 > a.anchor,h5 > a.anchor,h6 > a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;margin-top:.1rem;display:block;visibility:hidden;text-align:center;font-weight:400;color:rgba(0,0,0,.2)} +#content h1 > a.anchor:hover,h2 > a.anchor:hover,h3 > a.anchor:hover,#toctitle > a.anchor:hover,.sidebarblock > .content > .title > a.anchor:hover,h4 > a.anchor:hover,h5 > a.anchor:hover,h6 > a.anchor:hover{color:#097dff;text-decoration:none} +#content h1 > a.anchor:before,h2 > a.anchor:before,h3 > a.anchor:before,#toctitle > a.anchor:before,.sidebarblock > .content > .title > a.anchor:before,h4 > a.anchor:before,h5 > a.anchor:before,h6 > a.anchor:before{content:"\0023";font-size:.85em;display:block;padding-top:.1em} +#content h1:hover > a.anchor,#content h1 > a.anchor:hover,h2:hover > a.anchor,h2 > a.anchor:hover,h3:hover > a.anchor,#toctitle:hover > a.anchor,.sidebarblock > .content > .title:hover > a.anchor,h3 > a.anchor:hover,#toctitle > a.anchor:hover,.sidebarblock > .content > .title > a.anchor:hover,h4:hover > a.anchor,h4 > a.anchor:hover,h5:hover > a.anchor,h5 > a.anchor:hover,h6:hover > a.anchor,h6 > a.anchor:hover{visibility:visible} +#content h1 > a.link,h2 > a.link,h3 > a.link,#toctitle > a.link,.sidebarblock > .content > .title > a.link,h4 > a.link,h5 > a.link,h6 > a.link{color:#000;text-decoration:none} +#content h1 > a.link:hover,h2 > a.link:hover,h3 > a.link:hover,#toctitle > a.link:hover,.sidebarblock > .content > .title > a.link:hover,h4 > a.link:hover,h5 > a.link:hover,h6 > a.link:hover{color:#262321} +.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em} +.admonitionblock td.content > .title,.audioblock > .title,.exampleblock > .title,.imageblock > .title,.listingblock > .title,.literalblock > .title,.stemblock > .title,.openblock > .title,.paragraph > .title,.quoteblock > .title,table.tableblock > .title,.verseblock > .title,.videoblock > .title,.dlist > .title,.olist > .title,.ulist > .title,.qlist > .title,.hdlist > .title{text-rendering:optimizeLegibility;text-align:left;font-size:1rem} +table.tableblock > caption.title{white-space:nowrap;overflow:visible;max-width:0;padding:.6rem 0} +table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p{font-size:inherit} +.admonitionblock > table{border-collapse:separate;border:0;background:0 0;width:100%} +.admonitionblock > table td.icon{text-align:center;vertical-align:top;padding-top:.8em;width:80px} +.admonitionblock > table td.icon img{max-width:initial} +.admonitionblock > table td.icon .title{font-weight:700;text-transform:uppercase} +.admonitionblock > table td.content{padding-left:0;padding-right:1.25em;border-left:1px solid #ddddd8} +.admonitionblock > table td.content > :last-child > :last-child{margin-bottom:0} +.exampleblock > .content{border-style:solid;border-width:0;border-color:#e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#f1f1f1;border-radius:4px} +.exampleblock > .content > :first-child{margin-top:0} +.exampleblock > .content > :last-child{margin-bottom:0} +.sidebarblock{border-style:solid;border-width:0;border-color:#d7d7d7;margin-bottom:1.25em;padding:1.25em;background:#f1f1f1;border-radius:4px;overflow:scroll} +.sidebarblock > :first-child{margin-top:0} +.sidebarblock > :last-child{margin-bottom:0} +.sidebarblock > .content > .title{color:#0b0a0a;margin-top:0;text-align:center} +.exampleblock > .content > :last-child > :last-child,.exampleblock > .content .olist > ol > li:last-child > :last-child,.exampleblock > .content .ulist > ul > li:last-child > :last-child,.exampleblock > .content .qlist > ol > li:last-child > :last-child,.sidebarblock > .content > :last-child > :last-child,.sidebarblock > .content .olist > ol > li:last-child > :last-child,.sidebarblock > .content .ulist > ul > li:last-child > :last-child,.sidebarblock > .content .qlist > ol > li:last-child > :last-child{margin-bottom:0} +.literalblock pre,.listingblock pre:not(.highlight),.listingblock pre[class=highlight],.listingblock pre[class^="highlight "],.listingblock pre.CodeRay,.listingblock pre.prettyprint{background:#f2f2f2;color:#222;border-radius:4px} +.sidebarblock .literalblock pre,.sidebarblock .listingblock pre:not(.highlight),.sidebarblock .listingblock pre[class=highlight],.sidebarblock .listingblock pre[class^="highlight "],.sidebarblock .listingblock pre.CodeRay,.sidebarblock .listingblock pre.prettyprint{background:#f2f2f2;color:#222} +.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class],.listingblock pre:not(.highlight){padding:1em 1.5rem;font-size:.9285em} +.literalblock pre.nowrap,.literalblock pre[class].nowrap,.listingblock pre.nowrap,.listingblock pre[class].nowrap{overflow-x:auto} +.literalblock.output pre{color:#f5f5f5;background-color:rgba(0,0,0,.9)} +.listingblock{white-space:nowrap} +.listingblock pre.highlightjs{padding:.2rem 0} +.listingblock pre.highlightjs > code{padding:1em 1.5rem;border-radius:4px} +.listingblock > .content{position:relative} +.listingblock code[data-lang]:before{content:attr(data-lang);position:absolute;font-size:.8em;font-weight:700;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:#999;display:block} +.listingblock.terminal pre .command:before{content:attr(data-prompt);padding-right:.5em;color:#999} +.listingblock.terminal pre .command:not([data-prompt]):before{content:"$"} +table.pyhltable{border-collapse:separate;border:0;margin-bottom:0;background:0 0} +table.pyhltable td{vertical-align:top;padding-top:0;padding-bottom:0;line-height:1.45} +table.pyhltable td.code{padding-left:.75em;padding-right:0} +pre.pygments .lineno,table.pyhltable td:not(.code){color:#999;padding-left:0;padding-right:.5em;border-right:1px solid #ddddd8} +pre.pygments .lineno{display:block;margin-right:.25em} +table.pyhltable .linenodiv{background:0 0!important;padding-right:0!important} +.quoteblock{margin:0 1em 1.25em 1.5em;display:block;text-align:left;padding-left:20px} +.quoteblock blockquote,.quoteblock blockquote p{color:rgba(0,0,0,.85);line-height:1.75;letter-spacing:0} +.quoteblock blockquote{margin:0;padding:0;border:0;position:relative} +.quoteblock blockquote:before{content:"\201c";font-size:2.75em;font-weight:700;line-height:.6em;margin-left:0;margin-right:1rem;margin-top:.8rem;color:rgba(0,0,0,.1);position:absolute;top:0;left:-30px} +.quoteblock blockquote > .paragraph:last-child p{margin-bottom:0} +.quoteblock .attribution{margin-right:.5ex} +.quoteblock .quoteblock{margin-left:0;margin-right:0;padding:.5em 0;border-left:3px solid rgba(0,0,0,.6)} +.quoteblock .quoteblock blockquote{padding:0 0 0 .75em} +.quoteblock .quoteblock blockquote:before{display:none} +.verseblock{margin:0 1em 1.25em 0;background-color:#f1f1f1;padding:1rem 1.4rem;border-radius:4px} +.verseblock pre{font-family:D2 coding,Monaco,Menlo,Consolas,courier new,monospace;font-size:.9rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility} +.verseblock pre strong{font-weight:400} +.verseblock .attribution{margin-top:1.25rem;margin-left:.5ex} +.quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic} +.quoteblock .attribution br,.verseblock .attribution br{display:none} +.quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.025em;color:rgba(0,0,0,.6)} +.quoteblock.abstract{margin:0 0 1.25em;display:block} +.quoteblock.abstract blockquote,.quoteblock.abstract blockquote p{text-align:left;word-spacing:0} +.quoteblock.abstract blockquote:before,.quoteblock.abstract blockquote p:first-of-type:before{display:none} +table.tableblock{max-width:100%;border-collapse:separate;overflow-x:scroll} +table.tableblock td > .paragraph:last-child p > p:last-child,table.tableblock th > p:last-child,table.tableblock td > p:last-child{margin-bottom:0} +table.tableblock,th.tableblock,td.tableblock{border:0 solid #cacaca} +table.grid-all th.tableblock,table.grid-all td.tableblock{border-width:0 1px 1px 0} +table.grid-all tfoot > tr > th.tableblock,table.grid-all tfoot > tr > td.tableblock{border-width:1px 1px 0 0} +table.grid-cols th.tableblock,table.grid-cols td.tableblock{border-width:0 1px 0 0} +table.grid-all * > tr > .tableblock:last-child,table.grid-cols * > tr > .tableblock:last-child{border-right-width:0} +table.grid-rows th.tableblock,table.grid-rows td.tableblock{border-width:0 0 1px} +table.grid-all tbody > tr:last-child > th.tableblock,table.grid-all tbody > tr:last-child > td.tableblock,table.grid-all thead:last-child > tr > th.tableblock,table.grid-rows tbody > tr:last-child > th.tableblock,table.grid-rows tbody > tr:last-child > td.tableblock,table.grid-rows thead:last-child > tr > th.tableblock{border-bottom-width:0} +table.grid-rows tfoot > tr > th.tableblock,table.grid-rows tfoot > tr > td.tableblock{border-width:1px 0 0} +table.frame-all{border-width:1px} +table.frame-sides{border-width:0 1px} +table.frame-topbot{border-width:1px 0} +th.halign-left,td.halign-left{text-align:left} +th.halign-right,td.halign-right{text-align:right} +th.halign-center,td.halign-center{text-align:center} +th.valign-top,td.valign-top{vertical-align:top} +th.valign-bottom,td.valign-bottom{vertical-align:bottom} +th.valign-middle,td.valign-middle{vertical-align:middle} +table thead th,table tfoot th{font-weight:700} +tbody tr th{display:table-cell;line-height:1.6;background:#f7f8f7} +tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:#34302d;font-weight:700} +p.tableblock{font-size:1em} +td > div.verse{white-space:pre} +ol{margin-left:1.75em} +ul li ol{margin-left:1.5em} +dl dd{margin-left:1.125em} +dl dd:last-child,dl dd:last-child > :last-child{margin-bottom:0} +ol > li p,ul > li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em} +ul.unstyled,ol.unnumbered,ul.checklist,ul.none{list-style-type:none} +ul.unstyled,ol.unnumbered,ul.checklist{margin-left:.625em} +ul.checklist li > p:first-child > .fa-square-o:first-child,ul.checklist li > p:first-child > .fa-check-square-o:first-child{width:1em;font-size:.85em} +ul.checklist li > p:first-child > input[type=checkbox]:first-child{width:1em;position:relative;top:1px} +ul.inline{margin:0 auto .625em;margin-left:-1.375em;margin-right:0;padding:0;list-style:none;overflow:hidden} +ul.inline > li{list-style:none;float:left;margin-left:1.375em;display:block} +ul.inline > li > *{display:block} +.unstyled dl dt{font-weight:400;font-style:normal} +ol.arabic{list-style-type:decimal} +ol.decimal{list-style-type:decimal-leading-zero} +ol.loweralpha{list-style-type:lower-alpha} +ol.upperalpha{list-style-type:upper-alpha} +ol.lowerroman{list-style-type:lower-roman} +ol.upperroman{list-style-type:upper-roman} +ol.lowergreek{list-style-type:lower-greek} +.hdlist > table,.colist > table{border:0;background:0 0} +.hdlist > table > tbody > tr,.colist > table > tbody > tr{background:0 0} +td.hdlist1,td.hdlist2{vertical-align:top;padding:0 .625em} +td.hdlist1{font-weight:700;padding-bottom:1.25em} +.literalblock + .colist,.listingblock + .colist{margin-top:-.5em} +.colist > table tr > td:first-of-type{padding:0 .75em;line-height:1} +.colist > table tr > td:first-of-type img{max-width:initial} +.colist > table tr > td:last-of-type{padding:.25em 0} +.thumb,.th{line-height:0;display:inline-block;border:solid 4px #fff;-webkit-box-shadow:0 0 0 1px #ddd;box-shadow:0 0 0 1px #ddd} +.imageblock.left,.imageblock[style*="float: left"]{margin:.25em .625em 1.25em 0} +.imageblock.right,.imageblock[style*="float: right"]{margin:.25em 0 1.25em .625em} +.imageblock > .title{margin-bottom:0} +.imageblock.thumb,.imageblock.th{border-width:6px} +.imageblock.thumb > .title,.imageblock.th > .title{padding:0 .125em} +.image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0} +.image.left{margin-right:.625em} +.image.right{margin-left:.625em} +a.image{text-decoration:none;display:inline-block} +a.image object{pointer-events:none} +sup.footnote,sup.footnoteref{font-size:.875em;position:static;vertical-align:super} +sup.footnote a,sup.footnoteref a{text-decoration:none} +sup.footnote a:active,sup.footnoteref a:active{text-decoration:underline} +#footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em} +#footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em;border-width:1px 0 0} +#footnotes .footnote{padding:0 .375em 0 .225em;line-height:1.3334;font-size:.875em;margin-left:1.2em;text-indent:-1.05em;margin-bottom:.2em} +#footnotes .footnote a:first-of-type{font-weight:700;text-decoration:none} +#footnotes .footnote:last-of-type{margin-bottom:0} +#content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0} +.gist .file-data > table{border:0;background:#fff;width:100%;margin-bottom:0} +.gist .file-data > table td.line-data{width:99%} +div.unbreakable{page-break-inside:avoid} +.big{font-size:larger} +.small{font-size:smaller} +.underline{text-decoration:underline} +.overline{text-decoration:overline} +.line-through{text-decoration:line-through} +.aqua{color:#00bfbf} +.aqua-background{background-color:#00fafa} +.black{color:#000} +.black-background{background-color:#000} +.blue{color:#0000bf} +.blue-background{background-color:#0000fa} +.fuchsia{color:#bf00bf} +.fuchsia-background{background-color:#fa00fa} +.gray{color:#606060} +.gray-background{background-color:#7d7d7d} +.green{color:#006000} +.green-background{background-color:#007d00} +.lime{color:#00bf00} +.lime-background{background-color:#00fa00} +.maroon{color:#600000} +.maroon-background{background-color:#7d0000} +.navy{color:#000060} +.navy-background{background-color:#00007d} +.olive{color:#606000} +.olive-background{background-color:#7d7d00} +.purple{color:#600060} +.purple-background{background-color:#7d007d} +.red{color:#bf0000} +.red-background{background-color:#fa0000} +.silver{color:#909090} +.silver-background{background-color:#bcbcbc} +.teal{color:#006060} +.teal-background{background-color:#007d7d} +.white{color:#bfbfbf} +.white-background{background-color:#fafafa} +.yellow{color:#bfbf00} +.yellow-background{background-color:#fafa00} +span.icon > .fa{cursor:default} +.admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;cursor:default} +.admonitionblock td.icon .icon-note:before{content:"\f05a";color:#8979ca} +.admonitionblock td.icon .icon-tip:before{content:"\f0eb";color:#0077b9} +.admonitionblock td.icon .icon-warning:before{content:"\f071";color:#d88400} +.admonitionblock td.icon .icon-caution:before{content:"\f06d";color:#bf3400} +.admonitionblock td.icon .icon-important:before{content:"\f06a";color:#bf0000} +.conum[data-value]{display:inline-block;color:#000!important;background-color:#ffe157;-webkit-border-radius:100px;border-radius:100px;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-style:normal;font-weight:700} +.conum[data-value] *{color:#fff!important} +.conum[data-value] + b{display:none} +.conum[data-value]:after{content:attr(data-value)} +pre .conum[data-value]{position:relative;top:0;color:#000!important;background-color:#ffe157;font-size:12px} +b.conum *{color:inherit!important} +.conum:not([data-value]):empty{display:none} +.admonitionblock{background-color:#ebe7f8;padding:.8em 0;margin:30px 0;width:auto;border-radius:4px;overflow-x:auto} +.admonitionblock.important{border-left:0 solid #e20000;background-color:#f9ebeb} +.admonitionblock.warning{border-left:0 solid #d88400;background-color:#fff9e4} +.admonitionblock.tip{border-left:0 solid #0077b9;background-color:#e9f1f6} +.admonitionblock.caution{border-left:0 solid #e20000;background-color:#f9ebeb} +.admonitionblock .exampleblock > .content{border:0;background-color:#fff} +#toc a:hover{text-decoration:underline} +.admonitionblock > table{margin-bottom:0} +.admonitionblock > table td.content{border-left:none} +@media print { + #tocbot a.toc-link.node-name--H4{display:none} +} +.is-collapsible{max-height:1000px;overflow:hidden;transition:all 200ms ease-in-out} +.is-collapsed{max-height:0} +#index-link{display:none} +ul li > p > a > code{color:#097dff} +ul li > p > a:hover > code{color:#097dff} +div.back-action,#toc.toc2 div.back-action{padding:.8rem 0 0 5px} +div.back-action a,#toc.toc2 div.back-action a{position:relative;display:inline-block;padding:.6rem 1.2rem;padding-left:35px} +div.back-action a span,#toc.toc2 div.back-action a span{position:absolute;left:5px;top:5px;display:block;color:#333;height:26px;width:26px;border-radius:13px} +div.back-action a i,#toc.toc2 div.back-action a i{position:absolute;top:10px;left:5px} +div.back-action a:hover span,#toc.toc2 div.back-action a:hover span{color:#000} +#tocbot.desktop-toc{padding-top:.8rem} +#header-spring{position:absolute;text-rendering:optimizeLegibility;top:0;left:0;right:0;height:90px;margin:0 1rem;padding:0 1rem;border-bottom:1px solid #ddddd8;border-top:3px solid #9742ec} +#header-spring h1{margin:0;padding:0;font-size:22px;text-align:left;line-height:86px;padding-left:.6rem} +#header-spring h1 svg{width:200px} +#header-spring h1 svg .st0{fill:#6bb344} +#header-spring h1 svg .st2{fill:#444} +body.book #header-spring{position:relative;top:auto;left:auto;right:auto;margin:0} +body.book #header > h1:only-child{border:0;padding-bottom:1.2rem;font-size:1.8rem} +body.book #header,body.book #content,body.book #footnotes,body.book #footer{margin:0 auto} +body.toc2 #header-spring{position:absolute;left:0;right:0;top:0} +body.toc2 #header > h1:only-child{font-size:2.2rem} +body.toc2 #header,body.toc2 #content,body.toc2 #footnotes,body.toc2 #footer{margin:0 auto} +body.toc2 #content{padding-top:2rem} +#header,#content,#footnotes,#footer{width:100%;margin-right:auto;margin-top:0;margin-bottom:0;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em} +#header:before,#header:after,#content:before,#content:after,#footnotes:before,#footnotes:after,#footer:before,#footer:after{content:" ";display:table} +#header:after,#content:after,#footnotes:after,#footer:after{clear:both} +#content{margin-top:1.25em} +#content:before{content:none} +#header > h1:first-child{margin-top:2.55rem;margin-bottom:.5em} +#header > h1:first-child + #toc{margin-top:8px;border-top:0} +#header > h1:only-child,body.toc2 #header > h1:nth-last-child(2){border-bottom:1px solid #ddddd8;padding-bottom:8px;display:none} +#header .details{border-bottom:1px solid #ddddd8;line-height:1.45;padding-top:0;padding-bottom:2.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-flow:row wrap;-webkit-flex-flow:row wrap;flex-flow:row wrap} +#header .details span:first-child{margin-left:-.125em} +#header .details span.email a{color:rgba(0,0,0,.85)} +#header .details br{display:none} +#header .details br + span:before{content:"\00a0\2013\00a0"} +#header .details br + span.author:before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)} +#header .details br + span#revremark:before{content:"\00a0|\00a0"} +#header #revnumber{text-transform:capitalize} +#header #revnumber:after{content:"\00a0"} +#content > h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #ddddd8;padding-bottom:8px;margin-top:0;padding-top:1.5rem;margin-bottom:1.25rem} +h1{font-size:2.2rem;letter-spacing:-1px} +h1,h2,h3,h4,h5,h6{font-weight:700} +h1:focus,h2:focus,h3:focus,h4:focus,h5:focus,h6:focus{box-shadow:none;outline:none} +h2,h3,h4,h5,h6{padding:.8rem 0 .4rem} +h1{font-size:1.75em} +h2{font-size:1.6rem;letter-spacing:-1px} +h3{font-size:1.5rem} +h4{font-size:1.4rem} +h5{font-size:1.3rem} +h6{font-size:1.2rem} +pre.highlight{background:#f2f2f2;color:#666;border-radius:4px} +pre.highlight code{color:#222} +pre.highlight a,#toc.toc2 a{color:#000;font-size:1rem} +pre.highlight ul.sectlevel1,#toc.toc2 ul.sectlevel1{padding-left:.2rem} +pre.highlight ul.sectlevel1 li,#toc.toc2 ul.sectlevel1 li{line-height:1.4rem} +::selection{background-color:#faefff} +.literalblock pre::selection,.listingblock pre[class=highlight]::selection,.highlight::selection,pre::selection,.highlight code::selection,.highlight code span::selection{background:rgba(0,0,0,.2)!important} +body.book #header{margin-bottom:2rem} +body.toc2 #header{margin-bottom:0} +.desktop-toc{display:none} +.admonitionblock td.icon{display:none} +.admonitionblock > table td.content{padding-left:1.25em} +.brand-symbol{width:24px;height:24px;vertical-align:sub;margin-right:4px;} +@media only screen and (min-width:768px) { + #toctitle{font-size:1.375em} + .sect1{padding-bottom:1.25em} + .mobile-toc{display:none} + .desktop-toc{display:block} + .literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:.90625em} + .admonitionblock td.icon{display:table-cell} + .admonitionblock > table td.content{padding-left:0} + body.toc2{padding-right:0} + body.toc2 #toc.toc2{position:absolute;margin-top:0!important;width:15em;top:0;border-top-width:0!important;border-bottom-width:0!important;margin-left:-15.9375em;z-index:1000;padding:1.25em 1em 1.25em 1.25em;overflow:auto} + body.toc2 #toc.toc2 #toctitle{margin-top:0;margin-bottom:.8rem;font-size:1.2em} + body.toc2 #toc.toc2 > ul{font-size:.9em;margin-bottom:0} + body.toc2 #toc.toc2 ul ul{margin-left:0;padding-left:1em} + body.toc2 #toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em} + body.toc2 #header,body.toc2 #content,body.toc2 #footnotes,body.toc2 #footer{padding-left:15.9375em;padding-right:1.5rem;max-width:none} + body.book #header-spring h1{max-width:1400px;margin:0 auto} + body.book #header,body.book #content,body.book #footnotes,body.book #footer{max-width:1400px} + body.is-position-fixed #toc.toc2{position:fixed;height:100%} + h1,h2,h3,#toctitle,.sidebarblock > .content > .title,h4,h5,h6{line-height:1.2} + h1{font-size:1.75em} + h2{font-size:1.6em} + h3,#toctitle,.sidebarblock > .content > .title{font-size:1.5em} + h4{font-size:1.4em} + h5{font-size:1.2em} + h6{font-size:1.2em} + #tocbot a.toc-link.node-name--H1{font-style:italic} + #tocbot ol{margin:0;padding:0;padding-left:.6rem} + #tocbot ol li{list-style:none;padding:0;margin:0;display:block} + #tocbot{z-index:999} + #tocbot .toc-link{position:relative;display:block;z-index:999;padding-right:5px;padding-top:4px;padding-bottom:4px} + #tocbot .is-active-link{padding-right:3px;border-right:3px solid #9742ec} +} +@media only screen and (min-width:768px) { + #tocbot > ul.toc-list{margin-bottom:.5em;margin-left:.125em} + #tocbot ul.sectlevel0,#tocbot a.toc-link.node-name--H1 + ul{padding-left:0} + #tocbot a.toc-link{height:100%} + .is-collapsible{max-height:3000px;overflow:hidden} + .is-collapsed{max-height:0} + .is-active-link{font-weight:700} +} +@media only screen and (min-width:768px) { + body.toc2 #header,body.toc2 #content,body.toc2 #footer{background-repeat:repeat-y;background-position:14em 0;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAMAAAAoyzS7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQwIDc5LjE2MDQ1MSwgMjAxNy8wNS8wNi0wMTowODoyMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RDE0NUNENzNGMTVGMTFFODk5RjI5ODk3QURGRjcxMkEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RDE0NUNENzRGMTVGMTFFODk5RjI5ODk3QURGRjcxMkEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpEMTQ1Q0Q3MUYxNUYxMUU4OTlGMjk4OTdBREZGNzEyQSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpEMTQ1Q0Q3MkYxNUYxMUU4OTlGMjk4OTdBREZGNzEyQSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PjmGxxYAAAAGUExURd3d2AAAAJlCnKAAAAAMSURBVHjaYmAACDAAAAIAAU9tWeEAAAAASUVORK5CYII=)} +} +@media only screen and (min-width:1280px) { + body.toc2{padding-right:0}body.toc2 #toc.toc2{width:25em;left:auto;margin-left:-26.9375em}body.toc2 #toc.toc2 #toctitle{font-size:1.375em}body.toc2 #toc.toc2 > ul{font-size:.95em}body.toc2 #toc.toc2 ul ul{padding-left:1.25em}body.toc2 body.toc2.toc-right{padding-left:0;padding-right:20em}body.toc2 #header,body.toc2 #content,body.toc2 #footnotes,body.toc2 #footer{padding-left:26.9375em;padding-right:1.5rem;max-width:2000px}body.toc2 #header-spring h1{margin:0 auto;max-width:2000px}.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:.90625em}body.toc2 #header,body.toc2 #content,body.toc2 #footer{background-repeat:repeat-y;background-position:24em 0;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAMAAAAoyzS7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQwIDc5LjE2MDQ1MSwgMjAxNy8wNS8wNi0wMTowODoyMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RDE0NUNENzNGMTVGMTFFODk5RjI5ODk3QURGRjcxMkEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RDE0NUNENzRGMTVGMTFFODk5RjI5ODk3QURGRjcxMkEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpEMTQ1Q0Q3MUYxNUYxMUU4OTlGMjk4OTdBREZGNzEyQSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpEMTQ1Q0Q3MkYxNUYxMUU4OTlGMjk4OTdBREZGNzEyQSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PjmGxxYAAAAGUExURd3d2AAAAJlCnKAAAAAMSURBVHjaYmAACDAAAAIAAU9tWeEAAAAASUVORK5CYII=)} +} diff --git a/doc/fonts/D2Coding.eot b/doc/fonts/D2Coding.eot new file mode 100644 index 0000000..60bcd54 Binary files /dev/null and b/doc/fonts/D2Coding.eot differ diff --git a/doc/fonts/D2Coding.woff b/doc/fonts/D2Coding.woff new file mode 100644 index 0000000..fce24d2 Binary files /dev/null and b/doc/fonts/D2Coding.woff differ diff --git a/doc/fonts/D2CodingBold.eot b/doc/fonts/D2CodingBold.eot new file mode 100644 index 0000000..cb733d0 Binary files /dev/null and b/doc/fonts/D2CodingBold.eot differ diff --git a/doc/fonts/D2CodingBold.woff b/doc/fonts/D2CodingBold.woff new file mode 100644 index 0000000..0223d0e Binary files /dev/null and b/doc/fonts/D2CodingBold.woff differ diff --git a/doc/fonts/NanumGothic.ttf b/doc/fonts/NanumGothic.ttf new file mode 100644 index 0000000..009887a Binary files /dev/null and b/doc/fonts/NanumGothic.ttf differ diff --git a/doc/fonts/NanumGothicBold.ttf b/doc/fonts/NanumGothicBold.ttf new file mode 100644 index 0000000..f160c28 Binary files /dev/null and b/doc/fonts/NanumGothicBold.ttf differ diff --git a/doc/img/MyMenu.png b/doc/img/MyMenu.png new file mode 100644 index 0000000..b4b8fd0 Binary files /dev/null and b/doc/img/MyMenu.png differ diff --git a/doc/img/MyMenu_add.png b/doc/img/MyMenu_add.png new file mode 100644 index 0000000..c6b7153 Binary files /dev/null and b/doc/img/MyMenu_add.png differ diff --git a/doc/img/approvalSumit_01.png b/doc/img/approvalSumit_01.png new file mode 100644 index 0000000..d243e84 Binary files /dev/null and b/doc/img/approvalSumit_01.png differ diff --git a/doc/img/approvalSync_01.png b/doc/img/approvalSync_01.png new file mode 100644 index 0000000..21474cf Binary files /dev/null and b/doc/img/approvalSync_01.png differ diff --git a/doc/img/approval_01.png b/doc/img/approval_01.png new file mode 100644 index 0000000..51d16e3 Binary files /dev/null and b/doc/img/approval_01.png differ diff --git a/doc/img/approval_02.png b/doc/img/approval_02.png new file mode 100644 index 0000000..194a510 Binary files /dev/null and b/doc/img/approval_02.png differ diff --git a/doc/img/approverDelegate_01.png b/doc/img/approverDelegate_01.png new file mode 100644 index 0000000..16f4f1e Binary files /dev/null and b/doc/img/approverDelegate_01.png differ diff --git a/doc/img/batchJobLogList.png b/doc/img/batchJobLogList.png new file mode 100644 index 0000000..d7e4ac7 Binary files /dev/null and b/doc/img/batchJobLogList.png differ diff --git a/doc/img/batchJobMgmt.png b/doc/img/batchJobMgmt.png new file mode 100644 index 0000000..971e4e4 Binary files /dev/null and b/doc/img/batchJobMgmt.png differ diff --git a/doc/img/batchJobMgmtUpdate.png b/doc/img/batchJobMgmtUpdate.png new file mode 100644 index 0000000..2e4d474 Binary files /dev/null and b/doc/img/batchJobMgmtUpdate.png differ diff --git a/doc/img/boardManagement_01.png b/doc/img/boardManagement_01.png new file mode 100644 index 0000000..dad55e4 Binary files /dev/null and b/doc/img/boardManagement_01.png differ diff --git a/doc/img/boardManagement_02.png b/doc/img/boardManagement_02.png new file mode 100644 index 0000000..9c4018a Binary files /dev/null and b/doc/img/boardManagement_02.png differ diff --git a/doc/img/boardManagement_03.png b/doc/img/boardManagement_03.png new file mode 100644 index 0000000..b5f43fd Binary files /dev/null and b/doc/img/boardManagement_03.png differ diff --git a/doc/img/boardManagement_04.png b/doc/img/boardManagement_04.png new file mode 100644 index 0000000..e34fe2f Binary files /dev/null and b/doc/img/boardManagement_04.png differ diff --git a/doc/img/build_01.png b/doc/img/build_01.png new file mode 100644 index 0000000..1454c15 Binary files /dev/null and b/doc/img/build_01.png differ diff --git a/doc/img/build_02.png b/doc/img/build_02.png new file mode 100644 index 0000000..e09379f Binary files /dev/null and b/doc/img/build_02.png differ diff --git a/doc/img/build_03.png b/doc/img/build_03.png new file mode 100644 index 0000000..8d9fef2 Binary files /dev/null and b/doc/img/build_03.png differ diff --git a/doc/img/cacheMgmt.png b/doc/img/cacheMgmt.png new file mode 100644 index 0000000..7075f63 Binary files /dev/null and b/doc/img/cacheMgmt.png differ diff --git a/doc/img/commonCodeDetail.png b/doc/img/commonCodeDetail.png new file mode 100644 index 0000000..5a785c8 Binary files /dev/null and b/doc/img/commonCodeDetail.png differ diff --git a/doc/img/commonCodeDetail_Reg.png b/doc/img/commonCodeDetail_Reg.png new file mode 100644 index 0000000..11a1dbd Binary files /dev/null and b/doc/img/commonCodeDetail_Reg.png differ diff --git a/doc/img/commonCodeList.png b/doc/img/commonCodeList.png new file mode 100644 index 0000000..ec2a76c Binary files /dev/null and b/doc/img/commonCodeList.png differ diff --git a/doc/img/deptList.png b/doc/img/deptList.png new file mode 100644 index 0000000..cc62448 Binary files /dev/null and b/doc/img/deptList.png differ diff --git a/doc/img/deptMgmt(Knox).png b/doc/img/deptMgmt(Knox).png new file mode 100644 index 0000000..d720f1c Binary files /dev/null and b/doc/img/deptMgmt(Knox).png differ diff --git a/doc/img/dkms_typehandler_alias.png b/doc/img/dkms_typehandler_alias.png new file mode 100644 index 0000000..1c6e835 Binary files /dev/null and b/doc/img/dkms_typehandler_alias.png differ diff --git a/doc/img/dkms_typehandler_insert.png b/doc/img/dkms_typehandler_insert.png new file mode 100644 index 0000000..a82e073 Binary files /dev/null and b/doc/img/dkms_typehandler_insert.png differ diff --git a/doc/img/dkms_typehandler_select_resultmap.png b/doc/img/dkms_typehandler_select_resultmap.png new file mode 100644 index 0000000..5eff1ab Binary files /dev/null and b/doc/img/dkms_typehandler_select_resultmap.png differ diff --git a/doc/img/dkms_typehandler_update.png b/doc/img/dkms_typehandler_update.png new file mode 100644 index 0000000..c31f499 Binary files /dev/null and b/doc/img/dkms_typehandler_update.png differ diff --git a/doc/img/externalUserList.png b/doc/img/externalUserList.png new file mode 100644 index 0000000..1147cde Binary files /dev/null and b/doc/img/externalUserList.png differ diff --git a/doc/img/feature_01.png b/doc/img/feature_01.png new file mode 100644 index 0000000..b1c8354 Binary files /dev/null and b/doc/img/feature_01.png differ diff --git a/doc/img/fileDownloadHistory.png b/doc/img/fileDownloadHistory.png new file mode 100644 index 0000000..cb0a220 Binary files /dev/null and b/doc/img/fileDownloadHistory.png differ diff --git a/doc/img/front_01_01.png b/doc/img/front_01_01.png new file mode 100644 index 0000000..e42a471 Binary files /dev/null and b/doc/img/front_01_01.png differ diff --git a/doc/img/front_01_02.png b/doc/img/front_01_02.png new file mode 100644 index 0000000..6389e91 Binary files /dev/null and b/doc/img/front_01_02.png differ diff --git a/doc/img/front_01_03.png b/doc/img/front_01_03.png new file mode 100644 index 0000000..3c7beee Binary files /dev/null and b/doc/img/front_01_03.png differ diff --git a/doc/img/front_01_1.png b/doc/img/front_01_1.png new file mode 100644 index 0000000..8eec04e Binary files /dev/null and b/doc/img/front_01_1.png differ diff --git a/doc/img/front_01_2.png b/doc/img/front_01_2.png new file mode 100644 index 0000000..de66a99 Binary files /dev/null and b/doc/img/front_01_2.png differ diff --git a/doc/img/front_02_10.png b/doc/img/front_02_10.png new file mode 100644 index 0000000..32fd194 Binary files /dev/null and b/doc/img/front_02_10.png differ diff --git a/doc/img/front_02_11.png b/doc/img/front_02_11.png new file mode 100644 index 0000000..a301c27 Binary files /dev/null and b/doc/img/front_02_11.png differ diff --git a/doc/img/front_02_12.png b/doc/img/front_02_12.png new file mode 100644 index 0000000..ee9638f Binary files /dev/null and b/doc/img/front_02_12.png differ diff --git a/doc/img/front_02_13.png b/doc/img/front_02_13.png new file mode 100644 index 0000000..33485e7 Binary files /dev/null and b/doc/img/front_02_13.png differ diff --git a/doc/img/front_02_14.png b/doc/img/front_02_14.png new file mode 100644 index 0000000..ade0ec3 Binary files /dev/null and b/doc/img/front_02_14.png differ diff --git a/doc/img/front_02_15.png b/doc/img/front_02_15.png new file mode 100644 index 0000000..b25b1c8 Binary files /dev/null and b/doc/img/front_02_15.png differ diff --git a/doc/img/front_02_16.png b/doc/img/front_02_16.png new file mode 100644 index 0000000..2f68db8 Binary files /dev/null and b/doc/img/front_02_16.png differ diff --git a/doc/img/front_02_17.png b/doc/img/front_02_17.png new file mode 100644 index 0000000..6531011 Binary files /dev/null and b/doc/img/front_02_17.png differ diff --git a/doc/img/front_02_18.png b/doc/img/front_02_18.png new file mode 100644 index 0000000..dfe3115 Binary files /dev/null and b/doc/img/front_02_18.png differ diff --git a/doc/img/front_02_19.png b/doc/img/front_02_19.png new file mode 100644 index 0000000..68db949 Binary files /dev/null and b/doc/img/front_02_19.png differ diff --git a/doc/img/front_02_20.png b/doc/img/front_02_20.png new file mode 100644 index 0000000..36dfee5 Binary files /dev/null and b/doc/img/front_02_20.png differ diff --git a/doc/img/front_02_21.png b/doc/img/front_02_21.png new file mode 100644 index 0000000..841c3c3 Binary files /dev/null and b/doc/img/front_02_21.png differ diff --git a/doc/img/front_02_22.png b/doc/img/front_02_22.png new file mode 100644 index 0000000..c3e3892 Binary files /dev/null and b/doc/img/front_02_22.png differ diff --git a/doc/img/front_02_23.png b/doc/img/front_02_23.png new file mode 100644 index 0000000..a207023 Binary files /dev/null and b/doc/img/front_02_23.png differ diff --git a/doc/img/front_02_24.png b/doc/img/front_02_24.png new file mode 100644 index 0000000..8601aad Binary files /dev/null and b/doc/img/front_02_24.png differ diff --git a/doc/img/front_02_25.png b/doc/img/front_02_25.png new file mode 100644 index 0000000..ad9b52d Binary files /dev/null and b/doc/img/front_02_25.png differ diff --git a/doc/img/front_02_26.png b/doc/img/front_02_26.png new file mode 100644 index 0000000..762454a Binary files /dev/null and b/doc/img/front_02_26.png differ diff --git a/doc/img/front_02_27.png b/doc/img/front_02_27.png new file mode 100644 index 0000000..526ab52 Binary files /dev/null and b/doc/img/front_02_27.png differ diff --git a/doc/img/front_02_28.png b/doc/img/front_02_28.png new file mode 100644 index 0000000..124dcc4 Binary files /dev/null and b/doc/img/front_02_28.png differ diff --git a/doc/img/front_02_29.png b/doc/img/front_02_29.png new file mode 100644 index 0000000..272e6e5 Binary files /dev/null and b/doc/img/front_02_29.png differ diff --git a/doc/img/front_02_3.png b/doc/img/front_02_3.png new file mode 100644 index 0000000..e4d1fa5 Binary files /dev/null and b/doc/img/front_02_3.png differ diff --git a/doc/img/front_02_30.png b/doc/img/front_02_30.png new file mode 100644 index 0000000..36ccd51 Binary files /dev/null and b/doc/img/front_02_30.png differ diff --git a/doc/img/front_02_4.png b/doc/img/front_02_4.png new file mode 100644 index 0000000..03b8a8b Binary files /dev/null and b/doc/img/front_02_4.png differ diff --git a/doc/img/front_02_5.png b/doc/img/front_02_5.png new file mode 100644 index 0000000..7b320e1 Binary files /dev/null and b/doc/img/front_02_5.png differ diff --git a/doc/img/front_02_6.png b/doc/img/front_02_6.png new file mode 100644 index 0000000..6d2cb67 Binary files /dev/null and b/doc/img/front_02_6.png differ diff --git a/doc/img/front_02_7.png b/doc/img/front_02_7.png new file mode 100644 index 0000000..cc2e225 Binary files /dev/null and b/doc/img/front_02_7.png differ diff --git a/doc/img/front_02_8.png b/doc/img/front_02_8.png new file mode 100644 index 0000000..37e6e13 Binary files /dev/null and b/doc/img/front_02_8.png differ diff --git a/doc/img/front_02_9.png b/doc/img/front_02_9.png new file mode 100644 index 0000000..5fc44e7 Binary files /dev/null and b/doc/img/front_02_9.png differ diff --git a/doc/img/front_03_03.png b/doc/img/front_03_03.png new file mode 100644 index 0000000..2e43606 Binary files /dev/null and b/doc/img/front_03_03.png differ diff --git a/doc/img/front_03_04.png b/doc/img/front_03_04.png new file mode 100644 index 0000000..6255c7e Binary files /dev/null and b/doc/img/front_03_04.png differ diff --git a/doc/img/front_03_05.png b/doc/img/front_03_05.png new file mode 100644 index 0000000..da55686 Binary files /dev/null and b/doc/img/front_03_05.png differ diff --git a/doc/img/front_03_06.png b/doc/img/front_03_06.png new file mode 100644 index 0000000..dc62cac Binary files /dev/null and b/doc/img/front_03_06.png differ diff --git a/doc/img/front_04_01.png b/doc/img/front_04_01.png new file mode 100644 index 0000000..e8175be Binary files /dev/null and b/doc/img/front_04_01.png differ diff --git a/doc/img/front_04_02.png b/doc/img/front_04_02.png new file mode 100644 index 0000000..592f309 Binary files /dev/null and b/doc/img/front_04_02.png differ diff --git a/doc/img/front_04_03.png b/doc/img/front_04_03.png new file mode 100644 index 0000000..04d71b2 Binary files /dev/null and b/doc/img/front_04_03.png differ diff --git a/doc/img/front_05_01.png b/doc/img/front_05_01.png new file mode 100644 index 0000000..656da92 Binary files /dev/null and b/doc/img/front_05_01.png differ diff --git a/doc/img/front_06_01.png b/doc/img/front_06_01.png new file mode 100644 index 0000000..e6a9758 Binary files /dev/null and b/doc/img/front_06_01.png differ diff --git a/doc/img/front_06_02.png b/doc/img/front_06_02.png new file mode 100644 index 0000000..4d0e781 Binary files /dev/null and b/doc/img/front_06_02.png differ diff --git a/doc/img/front_06_02_01.png b/doc/img/front_06_02_01.png new file mode 100644 index 0000000..f9adc16 Binary files /dev/null and b/doc/img/front_06_02_01.png differ diff --git a/doc/img/front_06_03.png b/doc/img/front_06_03.png new file mode 100644 index 0000000..d093d95 Binary files /dev/null and b/doc/img/front_06_03.png differ diff --git a/doc/img/front_06_04.png b/doc/img/front_06_04.png new file mode 100644 index 0000000..9817e9c Binary files /dev/null and b/doc/img/front_06_04.png differ diff --git a/doc/img/front_07_01.png b/doc/img/front_07_01.png new file mode 100644 index 0000000..33b5e8c Binary files /dev/null and b/doc/img/front_07_01.png differ diff --git a/doc/img/front_07_04.png b/doc/img/front_07_04.png new file mode 100644 index 0000000..376ca5d Binary files /dev/null and b/doc/img/front_07_04.png differ diff --git a/doc/img/front_07_05.png b/doc/img/front_07_05.png new file mode 100644 index 0000000..9765e18 Binary files /dev/null and b/doc/img/front_07_05.png differ diff --git a/doc/img/front_07_06.png b/doc/img/front_07_06.png new file mode 100644 index 0000000..46985db Binary files /dev/null and b/doc/img/front_07_06.png differ diff --git a/doc/img/front_07_07.png b/doc/img/front_07_07.png new file mode 100644 index 0000000..f4ab22d Binary files /dev/null and b/doc/img/front_07_07.png differ diff --git a/doc/img/front_07_08.png b/doc/img/front_07_08.png new file mode 100644 index 0000000..39e3ca9 Binary files /dev/null and b/doc/img/front_07_08.png differ diff --git a/doc/img/install_09.png b/doc/img/install_09.png new file mode 100644 index 0000000..7a7a1f2 Binary files /dev/null and b/doc/img/install_09.png differ diff --git a/doc/img/install_1.png b/doc/img/install_1.png new file mode 100644 index 0000000..880aaa0 Binary files /dev/null and b/doc/img/install_1.png differ diff --git a/doc/img/install_10.png b/doc/img/install_10.png new file mode 100644 index 0000000..f3dc379 Binary files /dev/null and b/doc/img/install_10.png differ diff --git a/doc/img/install_11.png b/doc/img/install_11.png new file mode 100644 index 0000000..cb61e64 Binary files /dev/null and b/doc/img/install_11.png differ diff --git a/doc/img/install_12.png b/doc/img/install_12.png new file mode 100644 index 0000000..2e17416 Binary files /dev/null and b/doc/img/install_12.png differ diff --git a/doc/img/install_13.png b/doc/img/install_13.png new file mode 100644 index 0000000..b671641 Binary files /dev/null and b/doc/img/install_13.png differ diff --git a/doc/img/install_14.png b/doc/img/install_14.png new file mode 100644 index 0000000..05a4e57 Binary files /dev/null and b/doc/img/install_14.png differ diff --git a/doc/img/install_15.png b/doc/img/install_15.png new file mode 100644 index 0000000..52d67f3 Binary files /dev/null and b/doc/img/install_15.png differ diff --git a/doc/img/install_16.png b/doc/img/install_16.png new file mode 100644 index 0000000..01534f7 Binary files /dev/null and b/doc/img/install_16.png differ diff --git a/doc/img/install_17.png b/doc/img/install_17.png new file mode 100644 index 0000000..c7cea7c Binary files /dev/null and b/doc/img/install_17.png differ diff --git a/doc/img/install_18.png b/doc/img/install_18.png new file mode 100644 index 0000000..9981ac6 Binary files /dev/null and b/doc/img/install_18.png differ diff --git a/doc/img/install_19.png b/doc/img/install_19.png new file mode 100644 index 0000000..ecaae9a Binary files /dev/null and b/doc/img/install_19.png differ diff --git a/doc/img/install_2.png b/doc/img/install_2.png new file mode 100644 index 0000000..4a8d7e1 Binary files /dev/null and b/doc/img/install_2.png differ diff --git a/doc/img/install_20.png b/doc/img/install_20.png new file mode 100644 index 0000000..30da521 Binary files /dev/null and b/doc/img/install_20.png differ diff --git a/doc/img/install_21.png b/doc/img/install_21.png new file mode 100644 index 0000000..27c8bca Binary files /dev/null and b/doc/img/install_21.png differ diff --git a/doc/img/install_22.png b/doc/img/install_22.png new file mode 100644 index 0000000..238cff1 Binary files /dev/null and b/doc/img/install_22.png differ diff --git a/doc/img/install_23.png b/doc/img/install_23.png new file mode 100644 index 0000000..cefcc1b Binary files /dev/null and b/doc/img/install_23.png differ diff --git a/doc/img/install_24.png b/doc/img/install_24.png new file mode 100644 index 0000000..b061066 Binary files /dev/null and b/doc/img/install_24.png differ diff --git a/doc/img/install_25.png b/doc/img/install_25.png new file mode 100644 index 0000000..e9bc4c3 Binary files /dev/null and b/doc/img/install_25.png differ diff --git a/doc/img/install_26.png b/doc/img/install_26.png new file mode 100644 index 0000000..a7f55bf Binary files /dev/null and b/doc/img/install_26.png differ diff --git a/doc/img/install_27.png b/doc/img/install_27.png new file mode 100644 index 0000000..f06e239 Binary files /dev/null and b/doc/img/install_27.png differ diff --git a/doc/img/install_28.png b/doc/img/install_28.png new file mode 100644 index 0000000..b7f742a Binary files /dev/null and b/doc/img/install_28.png differ diff --git a/doc/img/install_29.png b/doc/img/install_29.png new file mode 100644 index 0000000..1f4f90c Binary files /dev/null and b/doc/img/install_29.png differ diff --git a/doc/img/install_30.png b/doc/img/install_30.png new file mode 100644 index 0000000..09e6449 Binary files /dev/null and b/doc/img/install_30.png differ diff --git a/doc/img/internalApproval_01.png b/doc/img/internalApproval_01.png new file mode 100644 index 0000000..e32e063 Binary files /dev/null and b/doc/img/internalApproval_01.png differ diff --git a/doc/img/internalApproval_02.png b/doc/img/internalApproval_02.png new file mode 100644 index 0000000..b16a803 Binary files /dev/null and b/doc/img/internalApproval_02.png differ diff --git a/doc/img/internalApproval_03.png b/doc/img/internalApproval_03.png new file mode 100644 index 0000000..3ce5b57 Binary files /dev/null and b/doc/img/internalApproval_03.png differ diff --git a/doc/img/internalApproval_04.png b/doc/img/internalApproval_04.png new file mode 100644 index 0000000..6c6fd1e Binary files /dev/null and b/doc/img/internalApproval_04.png differ diff --git a/doc/img/internalApproval_05.png b/doc/img/internalApproval_05.png new file mode 100644 index 0000000..586450f Binary files /dev/null and b/doc/img/internalApproval_05.png differ diff --git a/doc/img/internalApproval_06.png b/doc/img/internalApproval_06.png new file mode 100644 index 0000000..57feff4 Binary files /dev/null and b/doc/img/internalApproval_06.png differ diff --git a/doc/img/ipMgmt.png b/doc/img/ipMgmt.png new file mode 100644 index 0000000..b9701e2 Binary files /dev/null and b/doc/img/ipMgmt.png differ diff --git a/doc/img/language.png b/doc/img/language.png new file mode 100644 index 0000000..44189f2 Binary files /dev/null and b/doc/img/language.png differ diff --git a/doc/img/loggerMgmt.png b/doc/img/loggerMgmt.png new file mode 100644 index 0000000..a931dff Binary files /dev/null and b/doc/img/loggerMgmt.png differ diff --git a/doc/img/loginHistory.png b/doc/img/loginHistory.png new file mode 100644 index 0000000..cb73f8f Binary files /dev/null and b/doc/img/loginHistory.png differ diff --git a/doc/img/lombok_01.png b/doc/img/lombok_01.png new file mode 100644 index 0000000..7290e24 Binary files /dev/null and b/doc/img/lombok_01.png differ diff --git a/doc/img/lombok_02.png b/doc/img/lombok_02.png new file mode 100644 index 0000000..11a8ce9 Binary files /dev/null and b/doc/img/lombok_02.png differ diff --git a/doc/img/lombok_03.png b/doc/img/lombok_03.png new file mode 100644 index 0000000..bb416f8 Binary files /dev/null and b/doc/img/lombok_03.png differ diff --git a/doc/img/mailGroupList.png b/doc/img/mailGroupList.png new file mode 100644 index 0000000..24e12b4 Binary files /dev/null and b/doc/img/mailGroupList.png differ diff --git a/doc/img/mailGroupMappEdit.png b/doc/img/mailGroupMappEdit.png new file mode 100644 index 0000000..43b8d2e Binary files /dev/null and b/doc/img/mailGroupMappEdit.png differ diff --git a/doc/img/mailTemplate.png b/doc/img/mailTemplate.png new file mode 100644 index 0000000..b233479 Binary files /dev/null and b/doc/img/mailTemplate.png differ diff --git a/doc/img/mailTemplate1.png b/doc/img/mailTemplate1.png new file mode 100644 index 0000000..90d45cb Binary files /dev/null and b/doc/img/mailTemplate1.png differ diff --git a/doc/img/mainPage.png b/doc/img/mainPage.png new file mode 100644 index 0000000..20ec7a7 Binary files /dev/null and b/doc/img/mainPage.png differ diff --git a/doc/img/maven_01.png b/doc/img/maven_01.png new file mode 100644 index 0000000..108c52e Binary files /dev/null and b/doc/img/maven_01.png differ diff --git a/doc/img/maven_02.png b/doc/img/maven_02.png new file mode 100644 index 0000000..b3f2a7a Binary files /dev/null and b/doc/img/maven_02.png differ diff --git a/doc/img/maven_03.png b/doc/img/maven_03.png new file mode 100644 index 0000000..d070583 Binary files /dev/null and b/doc/img/maven_03.png differ diff --git a/doc/img/maven_04.png b/doc/img/maven_04.png new file mode 100644 index 0000000..d5772c1 Binary files /dev/null and b/doc/img/maven_04.png differ diff --git a/doc/img/menuManagement_01.png b/doc/img/menuManagement_01.png new file mode 100644 index 0000000..5305be7 Binary files /dev/null and b/doc/img/menuManagement_01.png differ diff --git a/doc/img/menuManagement_02.png b/doc/img/menuManagement_02.png new file mode 100644 index 0000000..233054d Binary files /dev/null and b/doc/img/menuManagement_02.png differ diff --git a/doc/img/menuManagement_03.png b/doc/img/menuManagement_03.png new file mode 100644 index 0000000..1c5bfd0 Binary files /dev/null and b/doc/img/menuManagement_03.png differ diff --git a/doc/img/menuManagement_04.png b/doc/img/menuManagement_04.png new file mode 100644 index 0000000..4d79b0a Binary files /dev/null and b/doc/img/menuManagement_04.png differ diff --git a/doc/img/menuUserHistoty.png b/doc/img/menuUserHistoty.png new file mode 100644 index 0000000..5500fa6 Binary files /dev/null and b/doc/img/menuUserHistoty.png differ diff --git a/doc/img/menuUtilHistory.png b/doc/img/menuUtilHistory.png new file mode 100644 index 0000000..9f4827f Binary files /dev/null and b/doc/img/menuUtilHistory.png differ diff --git a/doc/img/module_dependency_01.png b/doc/img/module_dependency_01.png new file mode 100644 index 0000000..cfa968a Binary files /dev/null and b/doc/img/module_dependency_01.png differ diff --git a/doc/img/module_hierarchy_01.png b/doc/img/module_hierarchy_01.png new file mode 100644 index 0000000..71440c9 Binary files /dev/null and b/doc/img/module_hierarchy_01.png differ diff --git a/doc/img/newEpTrayUtil.png b/doc/img/newEpTrayUtil.png new file mode 100644 index 0000000..f46f1fd Binary files /dev/null and b/doc/img/newEpTrayUtil.png differ diff --git a/doc/img/notice.png b/doc/img/notice.png new file mode 100644 index 0000000..a869f44 Binary files /dev/null and b/doc/img/notice.png differ diff --git a/doc/img/notice_alarm.png b/doc/img/notice_alarm.png new file mode 100644 index 0000000..aa61e91 Binary files /dev/null and b/doc/img/notice_alarm.png differ diff --git a/doc/img/npm_install_01.png b/doc/img/npm_install_01.png new file mode 100644 index 0000000..5974acf Binary files /dev/null and b/doc/img/npm_install_01.png differ diff --git a/doc/img/npm_run_local_01.png b/doc/img/npm_run_local_01.png new file mode 100644 index 0000000..20818ca Binary files /dev/null and b/doc/img/npm_run_local_01.png differ diff --git a/doc/img/pwdChange.png b/doc/img/pwdChange.png new file mode 100644 index 0000000..3059a16 Binary files /dev/null and b/doc/img/pwdChange.png differ diff --git a/doc/img/pwdReset.png b/doc/img/pwdReset.png new file mode 100644 index 0000000..afa8972 Binary files /dev/null and b/doc/img/pwdReset.png differ diff --git a/doc/img/pwdReset_01.png b/doc/img/pwdReset_01.png new file mode 100644 index 0000000..f65cb8b Binary files /dev/null and b/doc/img/pwdReset_01.png differ diff --git a/doc/img/pwdReset_02.png b/doc/img/pwdReset_02.png new file mode 100644 index 0000000..19289bd Binary files /dev/null and b/doc/img/pwdReset_02.png differ diff --git a/doc/img/quickMenu.png b/doc/img/quickMenu.png new file mode 100644 index 0000000..c32830c Binary files /dev/null and b/doc/img/quickMenu.png differ diff --git a/doc/img/quickMenu_Icon.png b/doc/img/quickMenu_Icon.png new file mode 100644 index 0000000..705cb07 Binary files /dev/null and b/doc/img/quickMenu_Icon.png differ diff --git a/doc/img/roleList.png b/doc/img/roleList.png new file mode 100644 index 0000000..e5c1990 Binary files /dev/null and b/doc/img/roleList.png differ diff --git a/doc/img/roleUserInfo.png b/doc/img/roleUserInfo.png new file mode 100644 index 0000000..8f5be0b Binary files /dev/null and b/doc/img/roleUserInfo.png differ diff --git a/doc/img/roleWorkgroupInfo.png b/doc/img/roleWorkgroupInfo.png new file mode 100644 index 0000000..8218678 Binary files /dev/null and b/doc/img/roleWorkgroupInfo.png differ diff --git a/doc/img/sdl_architecture.png b/doc/img/sdl_architecture.png new file mode 100644 index 0000000..14f0906 Binary files /dev/null and b/doc/img/sdl_architecture.png differ diff --git a/doc/img/sdl_introduction.png b/doc/img/sdl_introduction.png new file mode 100644 index 0000000..503d933 Binary files /dev/null and b/doc/img/sdl_introduction.png differ diff --git a/doc/img/sdl_structure.png b/doc/img/sdl_structure.png new file mode 100644 index 0000000..a2a0971 Binary files /dev/null and b/doc/img/sdl_structure.png differ diff --git a/doc/img/sentMailHistory.png b/doc/img/sentMailHistory.png new file mode 100644 index 0000000..7cd283d Binary files /dev/null and b/doc/img/sentMailHistory.png differ diff --git a/doc/img/sentMailHistoryInfo.png b/doc/img/sentMailHistoryInfo.png new file mode 100644 index 0000000..ee5f37c Binary files /dev/null and b/doc/img/sentMailHistoryInfo.png differ diff --git a/doc/img/setting.png b/doc/img/setting.png new file mode 100644 index 0000000..f7db7ec Binary files /dev/null and b/doc/img/setting.png differ diff --git a/doc/img/simpleApprovalDocument.png b/doc/img/simpleApprovalDocument.png new file mode 100644 index 0000000..3ee5776 Binary files /dev/null and b/doc/img/simpleApprovalDocument.png differ diff --git a/doc/img/siteMap.png b/doc/img/siteMap.png new file mode 100644 index 0000000..a4a7739 Binary files /dev/null and b/doc/img/siteMap.png differ diff --git a/doc/img/symbol.png b/doc/img/symbol.png new file mode 100644 index 0000000..c203c53 Binary files /dev/null and b/doc/img/symbol.png differ diff --git a/doc/img/termsCodeDetail.png b/doc/img/termsCodeDetail.png new file mode 100644 index 0000000..0ee0016 Binary files /dev/null and b/doc/img/termsCodeDetail.png differ diff --git a/doc/img/termsConditionDetail_Reg.png b/doc/img/termsConditionDetail_Reg.png new file mode 100644 index 0000000..8e9fdb2 Binary files /dev/null and b/doc/img/termsConditionDetail_Reg.png differ diff --git a/doc/img/termsConditionList.png b/doc/img/termsConditionList.png new file mode 100644 index 0000000..d079aba Binary files /dev/null and b/doc/img/termsConditionList.png differ diff --git a/doc/img/termsCondition_New.png b/doc/img/termsCondition_New.png new file mode 100644 index 0000000..4da4696 Binary files /dev/null and b/doc/img/termsCondition_New.png differ diff --git a/doc/img/timezone.png b/doc/img/timezone.png new file mode 100644 index 0000000..3e44d4d Binary files /dev/null and b/doc/img/timezone.png differ diff --git a/doc/img/uisdl_1.png b/doc/img/uisdl_1.png new file mode 100644 index 0000000..023e6f0 Binary files /dev/null and b/doc/img/uisdl_1.png differ diff --git a/doc/img/uisdl_2.png b/doc/img/uisdl_2.png new file mode 100644 index 0000000..4de5680 Binary files /dev/null and b/doc/img/uisdl_2.png differ diff --git a/doc/img/userInfo.png b/doc/img/userInfo.png new file mode 100644 index 0000000..1ff5053 Binary files /dev/null and b/doc/img/userInfo.png differ diff --git a/doc/img/userMgmt.png b/doc/img/userMgmt.png new file mode 100644 index 0000000..c815cda Binary files /dev/null and b/doc/img/userMgmt.png differ diff --git a/doc/img/utrans.png b/doc/img/utrans.png new file mode 100644 index 0000000..4890752 Binary files /dev/null and b/doc/img/utrans.png differ diff --git a/doc/img/vscode_02.png b/doc/img/vscode_02.png new file mode 100644 index 0000000..72f2bd9 Binary files /dev/null and b/doc/img/vscode_02.png differ diff --git a/doc/img/workgroupList.png b/doc/img/workgroupList.png new file mode 100644 index 0000000..f056226 Binary files /dev/null and b/doc/img/workgroupList.png differ diff --git a/doc/img/workgroupMenuInfo.png b/doc/img/workgroupMenuInfo.png new file mode 100644 index 0000000..62a7139 Binary files /dev/null and b/doc/img/workgroupMenuInfo.png differ diff --git a/doc/img/workgroupRoleInfo.png b/doc/img/workgroupRoleInfo.png new file mode 100644 index 0000000..2197a3d Binary files /dev/null and b/doc/img/workgroupRoleInfo.png differ diff --git a/doc/index.adoc b/doc/index.adoc new file mode 100644 index 0000000..8c9e2a8 --- /dev/null +++ b/doc/index.adoc @@ -0,0 +1,14 @@ + += 표준개발라이브러리 + +include::소개/Overview.adoc[leveloffset=+1] + +include::설치/설치.adoc[leveloffset=+1] + +include::아키텍처/아키텍처.adoc[leveloffset=+1] + +include::시스템공통/시스템공통.adoc[leveloffset=+1] + +include::공통기능/공통기능.adoc[leveloffset=+1] + +include::Appendix/Appendix.adoc[leveloffset=+1] diff --git a/doc/index.html b/doc/index.html new file mode 100644 index 0000000..17611bf --- /dev/null +++ b/doc/index.html @@ -0,0 +1,13777 @@ + + + + + + + +표준개발라이브러리 + + + + + + +
+

표준개발라이브러리 심볼표준개발라이브러리

+
+ +
+
+

1. Overview

+
+
+

삼성전자 내 정보시스템 개발을 위한 공통기능 및 아키텍처를 미리 만들어 제공함으로써, +프로젝트에서의 설계 및 개발 기간을 단축하고 유지보수를 용이하게 진행 할 수 있도록 지원한다.

+
+
+

1.1. 표준개발라이브러리란?

+
+

표준개발라이브러리(이하 SDL(Standard Development Library))는 웹 시스템 개발 시 재사용 가능한 공통 기능표준 개발 환경을 제공하는 통합 라이브러리다.

+
+
+
    +
  • +

    시스템 구축 시 자주 사용하는 공통 기능(웹 65개) 제공으로 개발 생산성 향산에 기여

    +
  • +
  • +

    웹 개발환경 표준화로 시스템 환경 구성 및 아키텍처 설계 기간 단축에 기여

    +
    +
      +
    • +

      적용 대상 : Java 기반의 신규 시스템 구축

      +
    • +
    +
    +
  • +
+
+
+
+sdl introduction +
+
+
+

1.1.1. 웹 부문

+
+
웹 공통기능 제공
+
+

전사 공통으로 사용하는 65개의 공통기능을 제공한다.

+
+
+
    +
  • +

    사용자 관리, 시스템 관리, 이력 관리, 보안 관리 등

    +
  • +
+
+
+
+
+
+

1.2. 주요특징

+
+

SDL 6.0의 주요 특징은 다음과 같다.

+
+
+
    +
  1. +

    Monolithic Architecture & Micro Service Architecture

    +
  2. +
  3. +

    Single Page Application

    +
  4. +
  5. +

    Spring Boot 3 (Spring Framework 6)

    +
  6. +
  7. +

    Javascript Framework 도입 Vue.js

    +
  8. +
  9. +

    CSS Framework 도입 Bootstrap 5

    +
  10. +
  11. +

    Front-end 빌드 : Vite (배포 타겟별 Profile 적용)

    +
  12. +
  13. +

    JDK baseline update 최소 요구 사항 JDK 17 이상

    +
  14. +
  15. +

    Back-end 빌드 : Maven (배포 타겟별 Profile 적용)

    +
  16. +
+
+ + +++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Table 1. SDL 4.5 vs 5.0, 6.0
구분4.55.06.0비고

공통기능

50개

65개

삭제 : Flex, MiPlatform, XPLATFORM 제외
+신규 : U-Trans, 결재경로관리, QuickMenu 등

아키텍처

Monolithic
+MSA 미지원

Monolithic
+MSA 지원

MSA 모델 중 서비스간 Database를 공유하는 모델 限

개발환경

JDK 6 이상
+Tomcat 7.0이상

JDK 8 이상
+Tomcat 9.0이상

JDK 17 이상
+Tomcat 10.1이상

Framework

Spring 4
+@Controller

Spring 5
+@RestController

Spring 6
+@RestController

Persistence Framework : MyBaits(동일)

UI

MPA
+JSP, jQuery
+CSS F/W 미제공
+ES5

SPA
+Vue.js 2
+Bootstrap 4
+ES6

SPA
+Vue.js 3
+Bootstrap 5
+ES6

Build

Ant
+UI 빌드 불필요

Maven
+Webpack

Maven
+Vite

3rd party 라이브러리 Maven Central Repo. 활용

+
+

1.2.1. Micro Service Architecture

+
+

시스템 내에 비즈니스 기능들 나누어 개발하고 다른 서버에서 서비스 됨 +→ 탄력적인 시스템 운영 가능 , 빌드/배포 시간 단축, 장애 영향도 최소, 기능 확장 용이

+
+
+
+

1.2.2. Single Page Application

+
+
    +
  • +

    페이지 이동 시 화면 깜빡임이 발생하지 않음

    +
  • +
  • +

    서버에서 필요한 데이터만 전달 받음

    +
  • +
  • +

    Java 개발은 Back-end(서버), Javascript 개발은 Front-end(UI)

    +
  • +
+
+
+
+
+

1.3. 지원환경

+
+

1.3.1. 웹 시스템 개발 환경

+
+

삼성전자 사내 시스템에 사용하는 인프라와 표준 WEB/WAS/DB 사용 지원

+
+ + ++++ + + + + + + + + + + + + + + + + + + + + +
Table 2. 시스템개발환경
구분환경

WAS

JBoss EAP 8.0
+Tomcat 10.1
+Spring Boot Embedded Tomcat

DBMS

MS SQL, MySQL, Oracle, EPAS, Tibero, PostgreSQL

JDK

JDK 17 이상

+
+
+
+

1.4. 기술지원범위

+
+

표준개발라이브러리 관련 기술지원 범위

+
+
+
    +
  • +

    SDL 공통기능 : SDL 공통기능과 관련된 Framework, REST API, UI, BUG Fix 관련 문의 및 개발지원

    +
  • +
  • +

    장애 및 오류 지원 : SDL 제공 공통기능과 관련된 장애 및 오류의 원인분석 및 개선지원 +(단, 증상의 재현이 가능하고 Error Log가 확보된 경우에 한함)

    +
  • +
+
+
+ + + + + +
+ + +기술지원 제외 대상
+다음과 같은 경우는 기술지원 대상에서 제외한다.
+1. SDL과 관련 없는 개발문의
+2. 개발 환경의 구축 및 설치, 네트워크 / HW / OS / WEB 서버 / WAS / DBMS / 패키지 SW 제품 관련 문제
+3. 개발 부서에서 자체 도입한 Open Source를 포함한 SW 관련 기능 +
+
+
+
+
+
+

2. 설치

+
+
+

2.1. 로컬 설치

+
+

2.1.1. Maven 설치

+
+

Apache Maven(https://maven.apache.org/download.cgi )에서 다운 받아 설치한다. +OS에 맞는 파일을 다운로드 한다. +Windows의 경우 Binary zip archive를 추천한다.

+
+
+
+maven_01 +
+
+
+

적당한 위치에 압축을 풀고 시스템 환경변수에서 %MAVEN_HOME%을 추가한다.

+
+
+
+maven_02 +
+
+
+

시스템 환경변수 path에 %MAVEN_HOME%\bin을 추가한다.

+
+
+
+maven_03 +
+
+
+

cmd창을 열어 mvn -version 명령어를 실행한다. +아래처럼 설치된 Maven 버전이 출력되면 된다.

+
+
+
+maven_04 +
+
+
+ + + + + +
+ + +사업장 Proxy 세팅을 위해 각 사업장 Proxy 정보를 Users/사용자명/.m2/settings.xml에 아래와 같이 추가한다. +(settings.xml 파일이 없을 경우 %MAVEN_HOME%\conf\settings.xml 파일을 해당 위치에 복사한다.) +
+
+
+
+
<settings>
+    <proxies>
+    <proxy>
+    	<id>proxy-http</id>
+    	<active>true</active>
+    	<protocol>http</protocol>
+    	<host>168.219.61.252</host> // 사업장 Proxy 정보
+    	<port>8080</port> // 사업장 Proxy 정보
+    	<nonProxyHosts>*.samsung.net|localhost|127.0.0.1</nonProxyHosts>
+    </proxy>
+    <proxy>
+    	<id>proxy-https</id>
+    	<active>true</active>
+    	<protocol>https</protocol>
+    	<host>168.219.61.252</host> // 사업장 Proxy 정보
+    	<port>8080</port> // 사업장 Proxy 정보
+    	<nonProxyHosts >*.samsung.net|localhost|127.0.0.1</nonProxyHosts>
+    </proxy>
+    </proxies>
+</settings>
+
+
+
+
+

2.1.2. IDE 설치

+
+

개발자 환경에 맞춰 필요한 IDE를 설치한다. +Eclipse의 경우 Spring Framework 개발에 필요한 Plugin이 포함된 Spring Tools Suite를 사용해도 된다. Front-end 개발을 위해서 VS Code 등을 추가로 설치 하는 것도 추천한다. 본 메뉴얼에서는 Eclipse나 IntelliJ 등 IDE 설치에 대해서는 다루지 않는다.

+
+
+
Visual Studio Code 설치
+
+ + + + + +
+ + +Visual Studio Code 외 다른 IDE를 사용하는 경우 건너뛴다. +
+
+
+

https://code.visualstudio.com/docs/?dv=win 에 접속해서 설치파일 다운로드

+
+
+
+front_02_10 +
+
+
+
+
+

2.1.3. Node.js 설치

+
+

링크(https://nodejs.org/en/ )의 페이지에서 사용중인 OS에 맞는 Node.js 설치파일을 다운로드 받아 설치 한다.

+
+
+ + + + + +
+ + +6.0.0 버전 기준 nodejs 버전 20.14.x 이상을 권장하고 있다. +
+
+
+
+front_01_1 +
+
+
+
설치 확인
+
+

Command 창에 node –v(version) 과 npm –v(version)을 입력합니다. 설치하신 node, npm정보가 조회되면 설치가 완료된 것이다.

+
+
+
+
C:\>node -v
+v20.14.0
+C:\>npm -v
+9.0.2
+
+
+
+
+
npm 설정 방법
+
+

Node library를 다운로드 받지 못하는 경우, NPM 설정이 필요하며, 설정에 필요한 명령어는 npm config set {환경변수명} {값} 이다. +현장의 proxy 서버에 맞게 명령어를 구성하면 되며, Proxy 설정 명령어는 아래와 같다. +npm 설정방법에 대한 자세한 설명은 npm-config를 참고 바란다.

+
+
+
+front 02 3 +
+
+
+
+
npm 설정 확인
+
+

npm 설정정보를 확인하기 위해 npm config list 명령을 수행한다. +아래 그림과 같이 설정한 값이 보이면 NPM 설정 완료.

+
+
+
+front 02 4 +
+
+
+
+
+

2.1.4. Eclipse (STS) Setting

+
+

다운받은 sdl-base-[version].zip 파일을 임의의 프로젝트 폴더 아래 압축을 푼다.

+
+
+
+install 1 +
+
+
+

Eclipse 실행 후 workspace 설정

+
+
+
+install_2 +
+
+
+

Eclipse 실행 후 완료

+
+
+
+install_09 +
+
+
+

Project Encoding 설정

+
+
+
+install_10 +
+
+
+

Validation Disable All

+
+
+
+install_11 +
+
+
+

Server 설정

+
+
+

본 문서에서는 로컬 WAS 설치 방법은 다루지 않는다.
+SDL은 스프링 부트가 기본 제공되므로 로컬 서버로는 스프링 부트 내장 Tomcat을 사용한다.

+
+
+
+

2.1.5. Eclipse Project Import

+
+

File → Import → Maven Project → Existing Maven Projects

+
+
+
+install 16 +
+
+
+
+install 17 +
+
+
+

sdl-base 프로젝트를 설치한 위치를 선택한다

+
+
+
+install 18 +
+
+
+

Import가 완료 되면 Package Explorer에서 아래처럼 프로젝트를 볼 수 있다.

+
+
+
+install 19 +
+
+
+ + + + + +
+ + +프로젝트를 처음 Import 할 경우 Build Path에서 Resource폴더가 Excluded 되어 있을 수 있으니 반드시 확인한다. +
+
+
+

Package Explorer에서 오른쪽 마우스 클릭 → Build Path → Configuration Build Path…​

+
+
+
+install 20 +
+
+
+

Source Tap에서 resources, resouces-local의 Excluded 가 None으로 되어 있나 확인한다.

+
+
+
+install 21 +
+
+
+
+install 22 +
+
+
+

Eclipse 프로젝트 Import가 완료되었다.

+
+
+ + + + + +
+ + +.properties의 한글이 깨진다면 아래처럼 인코딩 설정을 변경한다. +
+
+
+
+install 28 +
+
+
+
+

2.1.6. Lombok Plugin 설치

+
+

@Data @Log4j2 와 같은 Annotation에서 Compile 에러가 발생한다면 Lombok 플러그인을 설치한다.

+
+
+

https://projectlombok.org/download 에서 lombok jar 파일을 다운로드 받는다.

+
+
+
+lombok_01 +
+
+
+

java -jar lombok.jar 을 cmd창에서 실행한다.

+
+
+
+lombok_02 +
+
+
+ + + + + + + + + + + + + +
1Specify location..클릭
2Eclipse 설치 위치 선택
3Install/Update 클릭
+
+
+
+lombok_03 +
+
+
+

설치 완료

+
+
+
+

2.1.7. Server 실행

+
+

DB 접속 정보를 프로젝트 환경에 맞게 수정한다. (예. Postgresql 일 경우)

+
+
+
config.properties
+
+
# Datasource(Postgresql)
+datasource.driver=ENC(zg2s0lzTKz1OADpB2LmIk7pcJNLonCsW4WerSDJ9WTfmimam1Zi0Z8mH16+mWY+y)
+datasource.url=ENC(QaTWEyV020nKVDYneWGs8WNrezY8FGOKyleE2pkxlSs8m8nXE2/mqYMSaqAwUJghMJnLp20twDvdHh4csOKDzw==)
+datasource.username=ENC(Vz2UR99au+VBAaVJBDSdLw==)
+datasource.password=ENC(Wrer0/kHw+6UyGhtBT+bdSZNo83x+HNB)
+
+
+
+ + + + + +
+ + +해당 정보는 중요 정보로써 암호화를 통해 정보 탈취등의 보안 사고를 대비해야 한다. +표준개발라이브러리에서는 Jasypt(Java Simplified Encryption) 방식을 통해 해당 정보를 암호화 하고 있다. +
+
+
+
    +
  • +

    프로퍼티 값 암호화는 아래 링크의 내용을 참고하여 작성 및 입력한다. +(암호화가 필요한 정보만 따로 암호화하도록 한다.) +
    +Properties 암호화 툴 사용 방법 +

    +
  • +
  • +

    암호화에 사용된 키 값을 시스템 환경변수 또는 JVM 옵션을 사용하여 지정하면 +서버 구동 시 복호화 되어 DB에 접속 된다.(JasyptConfig.java)

    +
    +
      +
    • +

      시스템 환경변수 사용

      +
      +
        +
      • +

        Windows

        +
        +

        제어판 → 시스템 → 시스템 환경 변수 편집 → 환경 변수 → 시스템 변수 JASYPT_KEY 추가

        +
        +
        +
        +
        C:\Users\bbnydory>echo %JASYPT_KEY%
        +jfoadjvcoadr0j3402j
        +
        +
        +
      • +
      • +

        Linux

        +
        +
        +
        $ export JASYPT_KEY=jfoadjvcoadr0j3402j
        +
        +
        +
      • +
      • +

        Docker(Guide Jenkins Build)

        +
        +

        Docker Container로 어플리케이션을 실행 할 경우 Docker Build 변수를 Dockerfile에 전달해 설정해야 한다.

        +
        +
        +

        Jenkins Build Pipeline 환경 변수 설정 부분

        +
        +
        +
        +
        environment {
        +        PROJECT_ID = '...'
        +        GIT_CREDENTIAL_ID = '...'
        +        APP_IMAGE_REPO_CREDENTIAL_ID = '...'
        +        CA_CREDENTIAL_ID = '...'
        +        IMAGE_REPO = '....'
        +        JASYPT_KEY = 'jfoadjvcoadr0j3402j'
        +	}
        +
        +
        +
        +

        Jenkins Build Pipeline Doker 이미지 빌드 부분

        +
        +
        +
        +
        docker login -u $APP_IMAGE_REPO_USERNAME -p $APP_IMAGE_REPO_PASSWORD redii.secmis.sdspaas.io
        +docker build --pull --force-rm  --file=Dockerfile --build-arg JASYPT_KEY=$JASYPT_KEY --tag=$IMAGE_LOC .
        +
        +
        +
        +

        --build-arg JASYPT_KEY=$JASYPT_KEY 옵션으로 환경변수를 Dockerfile에 전달해야 한다.

        +
        +
        +

        Dockerfile 환경 변수 설정

        +
        +
        +
        +
        FROM redii.secmis.sdspaas.io/sdspaas-mw/xxxxxx
        +
        +ARG JASYPT_KEY
        +ENV JASYPT_KEY $JASYPT_KEY
        +
        +
        +
      • +
      +
      +
    • +
    • +

      JVM 옵션 사용

      +
      +

      WAS의 실행 명령어 또는 실행시 에 -DJASYPT_KEY=jfoadjvcoadr0j3402j 를 추가해서 어플리케이션을 실행한다.

      +
      +
      +
      +
      $ java -server -Xms8g -Xmx8g -XX:MaxMetaspaceSize=256m -Dspring.profiles.active=prod -DJASYPT_KEY=jfoadjvcoadr0j3402j
      +
      +
      +
    • +
    +
    +
  • +
+
+
+ + + + + +
+ + +Properties 암호화 툴에서 사용한 키 값(jasypt.key파일내용)과 Jasypt 암호화 키(JASYPT_KEY) 값은 일치해야 한다.
+환경변수와 key 값, value은 시스템 내에서 자유롭게 설정하도록 한다.
+위의 value값은 절대로 샘플의 값을 사용하지 않도록 한다. +
+
+
+
SpringConfig.java
+
+
@Bean
+public SqlSessionFactoryBean defaultSqlSessionFactory(DataSource dataSource,
+                                                      ApplicationContext applicationContext) throws IOException {
+    SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
+    factoryBean.setDataSource(dataSource);
+    factoryBean.setConfigLocation(applicationContext.getResource("classpath:sql/mybatis/postgresql/mybatis-config.xml"));
+    factoryBean.setMapperLocations(applicationContext.getResources("classpath*:sql/mybatis/postgresql/**/mapper-mybatis-*.xml"));
+    return factoryBean;
+}
+
+
+
+

pom.xml에서 프로젝트에서 사용할 DB의 jdbc driver dependency를 확인/설정한다.

+
+
+
pom.xml
+
+
    <dependency>
+        <groupId>org.postgresql</groupId>
+        <artifactId>postgresql</artifactId>
+        <version>42.7.3</version>
+        <exclusions>
+            <exclusion>
+                <groupId>org.checkerframework</groupId>
+                <artifactId>checker-qual</artifactId>
+            </exclusion>
+        </exclusions>
+    </dependency>
+
+
+
+
+

2.1.8. Spring Boot 실행

+
+

SDL 6.0은 Spring Boot로 되어 있기 때문에 별도의 WAS 없이 Embedded Tomcat를 이용해 서버를 실행 할 수 있다.

+
+
+
CMD 에서의 실행
+
+
+
mvn spring-boot:run
+
+
+
+
+
IntelliJ 에서 실행
+
+

프로젝트 창에서 com.samsung.SdlBaseBootApplication.java 파일 선택 후 Spring Boot Application 실행을 한다.

+
+
+
+
Eclipse 에서 실행
+
+

Spring Boot로 실행 할 com.samsung.SdlBaseBootApplication.java 파일 선택 후 Spring Boot Application을 실행 한다.

+
+
+
+
+

2.1.9. VS Code Setting

+
+ + + + + +
+ + +Visual Studio Code 외 다른 IDE를 사용하는 경우 건너뛴다. +
+
+
+

Open source +File > Open Folder > '\sdl-base\frontend' 선택

+
+
+
+front 02 11 +
+
+
+
+vscode 02 +
+
+
+

VS Code Plugin 설치 +코딩 컨벤션을 위한 Plugin 설치 - vscode 실행 후 좌측 아이콘 메뉴중 5번째 선택 후 검색어 입력

+
+
+

"eslint"

+
+
+
+front 02 12 +
+
+
+

"prettier"

+
+
+
+front 02 13 +
+
+
+

설치후 vscode 활성화 (저장시 자동 수정) +File > Preferences > Settings 메뉴 진입 하거나 +단축키 Ctrl + Shift + P 를 눌러 아래 검색어 user 입력

+
+
+
+front 02 14 +
+
+
+

설정검색란에 save 입력 후 settings.json에서 편집 클릭

+
+
+
+front 02 15 +
+
+
+

JSON 파일에 아래 코드 추가

+
+
+
+front 02 16 +
+
+
+ + + + + +
+ + +vscode 에서 eslint가 너무 늦게 적용될때 환경변수에 NO_UPDATE_NOTIFIER=1 추가하면 됨 +(참고링크 https://github.com/Microsoft/vscode-eslint/issues/440#issuecomment-380083518 ) +
+
+
+
+front 02 17 +
+
+
+
+

2.1.10. Frontend 설치 및 실행

+
+

Frontend 실행에 필요한 node module 설치

+
+
+

IDE에서 Terminal을 생성하여 npm install 명령어를 실행

+
+
+
npm-install
+
+
C:\~YOUR~PROJECT~PATH~\sdl-base\frontend>npm install
+
+
+
+

Vite dev-server 의 경우 host: 'localhost', port: 5173 이 default 값으로 설정되어있다.

+
+
+

SDL 은 dev-server 의 port 값을 '8081’로 사용하고 있으며, frontend/vite.config.js 파일에서 아래와 같이 서버 설정 +을 확인 및 수정 할 수 있다.

+
+
+
vite.config.js
+
+
server: {
+  // host: 'localhost' // default
+  port: '8081',
+}
+
+
+
+

로컬 환경 설정 파일 .env.test 파일을 열어 Back-end 정보를 입력한다.

+
+
+
.env.test
+
+
VITE_NODE_ENV=local
+VITE_API_URL=http://localhost:8080
+VITE_DIST_PATH=../src/main/resources/public
+VITE_WEB_CONTEXT_PATH=/
+
+
+
+

npm run local 명령어를 통해 +local 환경에서의 vite dev-server를 실행한다

+
+
+
+
C:\~YOUR~PROJECT~PATH~\sdl-base\frontend>npm run local
+
+
+
+
+

2.1.11. Local 접속 확인

+
+

Web browser(Chrome, Edge)에 http://localhost:8081 입력 후 실행

+
+
+
+front_02_20 +
+
+
+
+
+

2.2. 빌드&배포

+
+

2.2.1. Vite Bundling

+
+

SDL6 UI는 서비스 하기 위해 Bundling이 필요하다. +각 배포 대상에 따른 설정 파일은 .env.test , .env.development, .env.production 파일을 참고한다.

+
+
+
.env.production 예시
+
+
# default env
+VITE_NODE_ENV=production
+VITE_API_URL=http://sdl.sec.samsung.net/administrator
+VITE_DIST_PATH=../src/main/webapp/pp
+# CONTEXT_PATH value must be end with '/'
+VITE_WEB_CONTEXT_PATH=/
+
+
+
+

package.json 에 정의된 script에 따라 설정 파일이 적용된다.

+
+
+
package.json
+
+
  "scripts": {
+    "local": "vite --mode test",
+    "dev": "vite --mode development",
+    "prod": "vite --mode production",
+    "build": "vite build --mode production",
+    "build-test": "vite build --mode test",
+    "build-dev": "vite build --mode development",
+    "build-prod": "vite build --mode production",
+    "preview": "vite preview --mode production",
+    "preview-test": "vite preview --mode test",
+    "preview-dev": "vite preview --mode development",
+    "preview-prod": "vite preview --mode production",
+    "lint": "eslint src/**/*.{vue,js}"
+  },
+
+
+
+

npm run local 명령어를 실행할 경우 .env.test 파일이 적용되며, vite-dev-server가 실행된다. +vite-dev-server는 front-end 코드가 실시간으로 반영되므로 개발 시 편리하다.

+
+
+

npm run preview 명령어를 통해 빌드 된 결과물을 preview 서버를 통해 확인 할 수 있다. +반드시 VITE_DIST_PATH 에 Build 결과물이 존재해야 구동된다.

+
+
+
+

2.2.2. SonarQube Scanner 를 통한 소스코드 정적 분석

+
+
    +
  • +

    먼저 SonarQube Scanner 를 설치한다.

    +
  • +
+
+
+
+
npm install sonar-scanner -g
+
+
+
+
    +
  • +

    사용법 (Command Line)

    +
  • +
+
+
+
+
cd sdl-base (1)
+
+sonar-scanner -Dsonar.host.url=SonarQube서버URL -Dsonar.projectKey=프로젝트Key -Dsonar.projectName=프로젝트명 -Dsonar.projectVersion=프로젝트버전 -Dsonar.login=아이디 -Dsonar.password=패스워드 -Dsonar.sources=frontend/src (2)
+
+
+
+ + + + + + + + + +
1분석하고자 하는 프로젝트로 이동한다.
2설정한 파라미터값으로 실행한다.
+
+ + ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Table 3. 파라미터 설명
keyvalue(예)설명비고

sonar.host.url

http://guide.sonarqube.sec.samsung.net

SonarQube 서버 URL

필수

sonar.projectKey

my:project

분석하고자 하는 프로젝트의 고유한 키 값

필수

sonar.projectName

"my project for test"

프로젝트 이름

sonar.projectVersion

1.0

프로젝트 버전

sonar.login

user01

SonarQube 서버 로그인 ID

필수

sonar.password

passwd01

SonarQube 서버 로그인 패스워드

필수

sonar.sources

frontend/src

소스파일이 위치한 디렉토리

+
+ + + + + +
+ + +sonar-scanner 파라미터 관련 자세한 내용은 공식문서를 참조한다. +
+
+
+
+

2.2.3. Front-end 빌드

+
+

운영시스템에 배포 하기 위해서 cmd창에서 npm run-script build 명령어를 실행한다. +빌드 결과는 .env 파일의 VITE_DIST_PATH 경로에 생성된다.

+
+
+
+build_01 +
+
+
+
+build 02 +
+
+
+
+

2.2.4. Back-end 빌드

+
+

Back-end는 Maven을 이용해 Build 하고 있다. maven 명령어를 이용해 빌드한다. +패키징 시 배포 대상별로 Profile을 적용 할 수 있다.

+
+
+
+
mvn clean package -Pprod
+
+
+
+

-Pprod 옵션을 경우 src/main/resource-prod 아래의 설정파일들이 적용되어 패키징 된다.

+
+
+
+build_03 +
+
+
+ + + + + +
+ + +SDL 6.0은 Spring Profile이 적용되어 서버 실행시 -Dspring.profiles.active=local과 같은 옵션을 필요로 한다. +
+
+
+
+
+
+
+

3. 아키텍처

+
+
+

SDL은 Spring 6 기반의 Back-end, Vue.js 3 기반의 Front-end로 구성되어 있다. Back-end는 MVC 패턴을 기본으로 RESTful API 아키텍처 Front-end는 MVVM 패턴을 기본으로 SPA 아키텍처로 되어 있다.

+
+
+

3.1. SDL 구성

+
+

3.1.1. 모듈

+
+

SDL은 12개의 모듈로 구성되어 있다. base 프로젝트를 제외한 나머지 11개의 모듈은 패키징 되어 jar파일로 배포된다.

+
+
+
+module dependency 01 +
+
+
+
    +
  • +

    core : SDL 모듈 가장 상위에 위치하고 SDL 에서 annotation, exception등이 포함되어 있다.

    +
  • +
  • +

    common : 다른 모듈에서 사용되는 entity, service 등이 포함되어 있다.

    +
  • +
  • +

    resource : 파일 업/다운로드 관련한 기능이 포함되어 있다.

    +
  • +
  • +

    knox : Knox에서 제공하는 REST서비스를 연계하기 위한 기능들이 포함되어 있다.

    +
  • +
  • +

    email : 메일, 메일 그룹 관리 기능이 포함되어 있다.

    +
  • +
  • +

    auth : 사용자, 권한, 메뉴, 역할 등 인증, 인가관련 기능이 포함되어 있다.

    +
  • +
  • +

    support: 배치관리, 메뉴사용이력, 번역 서비스 등이 포함되어 있다.

    +
  • +
  • +

    approval : 결재, 결재자관리, 결재문서 관리 등 결재에 관련된 기능이 포함되어 있다.

    +
  • +
  • +

    board : 게시판 기능이 포함되어 있다.

    +
  • +
  • +

    excel : 엑셀 업/다운로드 기능이 포함되어 있다.

    +
  • +
  • +

    history : 메뉴 활용도, 메뉴 사용 이력 등 이력관리 기능이 포함되어 있다.

    +
  • +
+
+
+ + + + + +
+ + +sdl-base/source에 배포된 모든 모듈에 대한 원본 소스 파일이 함께 배포되니 분실하지 않도록 주의한다. +
+
+
+
+
+

3.2. Backend 구성

+
+

3.2.1. sdl-base

+
+

sdl-base은 시스템 개발을 위한 프로젝트로 sdl을 기반으로 시스템을 개발하기 위한 파일들로 구성되어 있다.

+
+
+
+
sdl-base
+ |- doc (1)
+ |- frontend (2)
+ |- src/main/java (3)
+ |- src/main/resource (4)
+ |- source (6)
+ |- pom.xml
+
+
+
+
    +
  1. +

    doc : SDL 개발자 문서 위치

    +
  2. +
  3. +

    frontend : SDL Frontend 프로젝트

    +
  4. +
  5. +

    src/main/java : Java file

    +
  6. +
  7. +

    resource : 프로젝트 실행을 위한 설정파일, xml 파일 등

    +
  8. +
  9. +

    source : SDL 모듈 jar파일과 source-jar파일

    +
  10. +
  11. +

    pom.xml

    +
  12. +
+
+
+
+

3.2.2. src/main/java

+
+

java source가 위치한 폴더로 maven compile 대상 프로젝트이다. SDL에서 배포되는 소스코드 외에 프로젝트에서 개발한 java class도 이곳에 작성하면 된다. SDL 기능중 시스템별로 수정이 많이 발생하거나 샘플로 제공되는 기능들은 sdl-base 모듈에 있다.

+
+
+
+
com
+|- onelogin.saml2 (1)
+|- samsung
+    |- aspect (2)
+    |- batch (3)
+    |- common (4)
+    |- config (5)
+    |- filter (6)
+    |- interceptor (7)
+    |- sample (8)
+    |- user.controller (9)
+
+
+
+
    +
  1. +

    onelogin.saml2 : AD 통합 인증 관련 Package

    +
  2. +
  3. +

    aspect : Aspect Package

    +
  4. +
  5. +

    batch : 사용자 동기화 배치 관련 Package

    +
  6. +
  7. +

    common : 시스템 공통으로 사용하는 Util Package

    +
  8. +
  9. +

    config : Spring config Package

    +
  10. +
  11. +

    filter : Servlet Filter Package

    +
  12. +
  13. +

    interceptor : HandlerInterceptor의 구현, 서비스 전,후처리 Package

    +
  14. +
  15. +

    sample : Sample Package

    +
  16. +
  17. +

    user.controller : 로그인 관련 Package

    +
  18. +
+
+
+
+

3.2.3. src/main/resources

+
+

resources는 어플리케이션이 실행될 때 필요한 설정파일, Mybatis Query Mapping 파일들을 포함하고 있다. +resource-{profile} 형태로 되어 있어 빌드 시 knox.properties, log4j2.xml, log4jdbc.log4j2.properties, onelogin.saml.properties +의 파일이 profile에 맞게 패키징된다.

+
+
+
    +
  • +

    resources : 모든 profile에 공통적으로 적용되는 파일(예: SQL, Excel 다운로드 양식)

    +
  • +
+
+
+
+
resources
+    |- excel :  excel up/download mapping xml 파일
+    |- message : spring message properties 파일
+    |- sql.mybatis : mybatis query mapper xml  파일
+    |- templates : 결재/메일 vm 파일
+
+
+
+
    +
  • +

    resources-local : 로컬 개발 환경에 필요한 설정 파일

    +
  • +
+
+
+
+
resources-local
+    |- application.properties : spring boot 설정 파일
+    |- config.properties : sdl의 설정 파일
+    |- knox.properties : knox 설정
+    |- log4j2.xml : log4j2 설정
+    |- onelogin.saml.properties : AD 통합 인증 설정 파일
+
+
+
+
    +
  • +

    resources-dev : 개발 환경에 필요한 설정 파일

    +
  • +
  • +

    resources-prod : 운영 환경에 필요한 설정 파일

    +
  • +
+
+
+ + + + + +
+ + +설정파일들은 반드시 프로젝트에 맞게 수정해서 사용하도록 한다. +
+
+
+
+
+

3.3. Frontend 구성

+
+

3.3.1. frontend 폴더 구조

+
+

sdl-base/frontend 폴더 아래 구성되어 있다.

+
+
+
+
frontend
+ |- public (1)
+ |- src (2)
+ |- static (3)
+ |- .env (4)
+ |- index.html (5)
+ |- package.json (6)
+ |- .prettierrc.json (7)
+ |- eslint.config.js (8)
+ |- vite.config.js (9)
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1build 시 파일명 hashing을 하지 않는 public 폴더
2Vue Source 폴더
3css, font 등 정적 자원을 포함한 폴더
4배포 대상에 따른 설정 파일(.env에 정의된 파일은 Vue에서 변수로 사용가능)
5프로젝트 index.html
6npm 모듈 관리를 위한 파일
7prettier 설정 json 파일
8eslint 설정 파일
9vite 설정 파일
+
+
+
+

3.3.2. src

+
+

src 폴더는 Vue의 Source 폴더이다. UI 구성을 위한 컴포넌트, 디렉티드, 필더, 라우터, 서비스 등이 있다.

+
+
+
+
src
+ |- components (1)
+ |- constants (2)
+ |- directives (3)
+ |- event (4)
+ |- filters (5)
+ |- i18n (6)
+ |- mixin (7)
+ |- router (8)
+ |- service (9)
+ |- utils (10)
+ |- vuex (11)
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1공통 및 화면단 컴포넌트 위치 +
+
    +
  • +

    components/common/control : SDL에서 제공하는 공통 컴포넌트 위치

    +
  • +
  • +

    components/common/popup : SDL에서 제공하는 공통 팝업 위치

    +
  • +
  • +

    components/view : 하위에 업무별로 패키지명을 구분해 폴더 구성

    +
  • +
+
2Vue 에서 사용하는 전역 상수 정의
3Vue 엘리먼트에서 사용되는 사용자 지정 속성을 정의
4Vue3에서 deprecated 된 event bus 모듈 폴더
5Vue 에서 텍스트 형식화를 적용할 수 있는 필터 정의
6vue-i18n에서 사용하는 다국어관련 정의
7Vue mixin 으로 사용하는 변수 및 메소드 정의
8Vue를 이용한 단일페이지에서 컴포넌트별 path를 정의
9Vue에서 사용하는 service를 정의
10SDL에서 제공하는 공통Util 위치
11Vue에서 제공하는 상태관리 + 패턴 라이브러리 위치
+
+
+
+

3.3.3. components

+
+

components 폴더는 SDL에서 제공하는 공통 컴포넌트와 공통 팝업, 어드민 기능 화면을 포함한다.

+
+
+
+
components
+ |- common
+    |- control (1)
+    |- popup (2)
+ |- view
+    |- admin
+        |- approval (3)
+        |- approvalTemplate (4)
+        |- batch (5)
+        |- board (6)
+        |- dept (7)
+        |- etc (8)
+        |- htmlTemplate (9)
+        |- log (10)
+        |- main (11)
+        |- menu (12)
+        |- role (13)
+        |- sample (14)
+        |- user (15)
+        |- workgroup (16)
+    |- sample (17)
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1SDL 공통컴포넌트
2SDL 공통팝업
3결재관리
4결재문서 양식 관리
5배치관리
6게시판관리
7부서관리
8기타관리
9HTML 양식 관리
10로그관리
11메인페이지
12메뉴관리
13역할관리
14결재샘플
15사용자관리
16업무그룹관리
17공통컴포넌트 사용 샘플
+
+
+ + + + + +
+ + +프로젝트에서 개발한 Vue 화면은 components/view 폴더 아래 있어야 한다.
+router 등록 시 디폴트 경로가 components/view 이다. +
+
+
+
+

3.3.4. css 위치 및 import 방법

+
+
+front 02 24 +
+
+
+

사용하고자 하는 css 를 static/css 밑에 넣어놓고 src/main.js에 import 에서 사용한다.

+
+
+
+

3.3.5. directive(사용자 지정속성)

+
+
/directives/custom.js
+
+
// v-directive 의 기본 구조
+export default {
+  beforeMount(el, binding, vnode) {
+    // console.log('bind');
+  },
+  mounted(el, binding, vndoe) {
+    // console.log('inserted');
+  },
+  updated(el, binding, vnode, oldVnode) {
+    // console.log('componentUpdated');
+  },
+};
+
+
+
+
    +
  • +

    Vue 엘리먼트에서 사용되는 사용자 지정 속성을 정의한다.

    +
  • +
  • +

    디렉티브에서 제공하는 훅 함수는 아래와 같다.

    +
    +
      +
    • +

      created : 이벤트 리스너가 적용되기 전에 호출된다.

      +
    • +
    • +

      beforeMount : 엘리먼트가 DOM에 삽입되기 직전에 호출된다.

      +
    • +
    • +

      mounted : 바인딩된 엘리먼트의 부모 컴포넌트 및 모든 자식 컴포넌트의 mounted 이후에 호출된다.

      +
    • +
    • +

      beforeUpdate : 부모 컴포넌트의 updated 전에 호출된다.

      +
    • +
    • +

      updated : 바인딩된 엘리먼트의 부모 컴포넌트 및 모든 자식 컴포넌트의 updated 이후에 호출됩니다.

      +
    • +
    • +

      beforeUnmount : 부모 컴포넌트의 beforeUnmount 이후에 호출된다.

      +
    • +
    • +

      unmounted : 부모 컴포넌트의 unmounted 전에 호출된다.

      +
    • +
    +
    +
  • +
  • +

    참고 : https://v3-docs.vuejs-korea.org/guide/reusability/custom-directives.html#directive-hooks

    +
  • +
+
+
+
directives/index.js
+
+
// directive install 정의
+import custom from './custom';
+import clickOutSide from './clickOutside';
+import regex from './regex';
+import authorization from './authorization';
+import validation from './validation';
+
+export default {
+  install(Vue) {
+    Vue.directive('custom', custom);
+    Vue.directive('click-outside', clickOutSide);
+    Vue.directive('regex', regex);
+    Vue.directive('authorization', authorization);
+    Vue.directive('validation', validation);
+  },
+};
+
+
+
+
main.js
+
+
// directive 사용
+// ... 생략 ...
+import SdlDirectives from './directives';
+
+app.use(SdlDirectives);
+// ... 생략 ...
+};
+
+
+
+
+

3.3.6. filter

+
+
    +
  • +

    Vue 에서 텍스트 형식화를 적용할 수 있는 필터를 정의한다.

    +
  • +
+
+
+ + + + + +
+ + +Vue3에선 전역 method 방식으로 사용한다. +
+
+
+
/filters/index.js
+
+
// filter install 정의
+import dateFormat from './dateFormat';
+import numberFormat from './numberFormat';
+import reverse from './reverse';
+import nl2br from './nl2br';
+import cutString from './cutString';
+
+export default {
+  install(Vue) {
+    Vue.config.globalProperties.$filters = {
+      reverse: reverse,
+      dateFormat: dateFormat,
+      numberFormat: numberFormat,
+      nl2br: nl2br,
+      cutString: cutString,
+    };
+  },
+};
+
+
+
+
예시
+
+
<span class="ui--text-total">
+    <strong>Total</strong> {{ $filters.numberFormat(total) }}
+</span>
+
+
+
+
+

3.3.7. 다국어 세팅 위치 및 사용 방법

+
+
+front 02 27 +
+
+
+
    +
  • +

    vue-i18n을 통한 다국어 지원

    +
  • +
  • +

    서버단의 모든 message.properties를 읽어 사용자 토큰이 없을 경우에는 브라우져의 언어셋을 사용자 토큰이 있을 경우에는 지정된 언어셋으로 지정된다.

    +
  • +
  • +

    vue-i18n에서 제공하는 $t를 이용하거나 SDL에서 제공하는 SDLUtil 유틸을 이용해서 다국어를 사용할 수 있다.

    +
  • +
+
+
+
+

3.3.8. 공통 component import 구조 및 사용 방법

+
+
+front 02 28 +
+
+
+

SDL에서 제공하는 공통 컴포넌트를 src/components/common/control/index.js에 컴포넌트로 리스트업 후 src/main.js에서 최종 plugin 한다.

+
+
+
+

3.3.9. 사용자 검색 팝업

+
+
+front 02 29 +
+
+
+

사용자 조회 function으로 아래와 같은 param을 정의해 사용한다.

+
+
+
+
{
+    searchColumn : '검색할 컬럼'(name : '이름', knoxId :'knoxId 아이디')
+    searchTxt : '검색할 text'('like로 검색됨')
+    rtnFunc : '검색한 내용을 return받을 function'
+ }
+
+
+
+
+
+
+
+

4. 시스템공통

+
+
+

4.1. 시스템 설정

+
+

SDL은 다양한 시스템 환경에 적용 할 수 있도록 여러가지 설정파일을 제공한다. sdl-base/src/resources-{profile} 폴더안에 profile 별로 다른 설정 파일들이 실행 될 수 있도록 구성되어 있다.

+
+
+

4.1.1. config.properties

+
+

SDL 가장 중요한 설정 파일로 시스템 전반에 영향을 준다. 로컬, 개발, 운영환경마다 내용이 달라질 수 있으니 패키징 시 설정값들이 맞는지 확인하고 배포 할 수 있도록 주의한다.

+
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
keyvalue(예)설명주의

node-id

localNode

서버 식별을 위한 ID

서버가 여러 대 있을 경우 ID값을 다르게 해야한다.

ssl-port

8443

http로 접근 했을 때 자동으로 redirection 하기 위한 https 포트

기본값은 443

cors.domain

*

CORS를 허용하기 위한 값으로 브라우저에서 다른 도메인으로 데이터 호출을 가능하게 한다.

*, sdl.sec.sasmsung.net

web-context-path

/

웹서버의 web-context-path

페이지 경로 앞에 참조

datasource.driver

ENC(txFyC35/92LBzew8iq2DjoFINRYg6e1UMcq7Du2mvs/nIqTSAU6e2r4VcyKrNc+K)

DMBS의 driver class name

중요 프로퍼티 정보 암호화. 자세한 내용은 프로퍼티 값 암호화 관련 부분 참조

datasource.url

ENC(N2SavucpTKhakXUzUGpVLh+Hy2UQ5EyQJKBMJfagFbTB0VR9m8KO7LmPM6GFRBPWgjEOYJx1hWtFrCsHctjYZQ==)

DMBS의 url

중요 프로퍼티 정보 암호화. 자세한 내용은 프로퍼티 값 암호화 관련 부분 참조

datasource.username

ENC(xS5l219WYApjTijLj8gwAw==)

DMBS의 사용자

중요 프로퍼티 정보 암호화. 자세한 내용은 프로퍼티 값 암호화 관련 부분 참조

datasource.password

ENC(WHYsCYza7ffqN8Wi7FtTwTBI0dP9wHy2)

DMBS의 패스워드

중요 프로퍼티 정보 암호화. 자세한 내용은 프로퍼티 값 암호화 관련 부분 참조

db.jndi

sdl_ds

WAS Datasource의 JNDI명

개발,운영 환경에서는 WAS의 Datasource를 사용한다.

web-content-root-path

http://localhost:8081

Web 서버의 Root의 경로

클라이언트로 redirect 할 경우 참조 (ex. AD인증 후)

static-resource-path

http://localhost:8081/static

css, image가 있는 경로

HTML Template (ex. Velocity) 에서 참조

base.package

com.samsung

클래스를 찾을 때 해당 패키지 아래에서만 찾는다.

결재 문서를 찾을 때 참조

entity.package-name

entity

결재 문서를 찾을 때 entity 패키지 아래의 클래스만 찾는다.

반드시 @ApprovalDocument가 선언되어 있어야 한다.

login.sso.knox-tray-private-key-path

/rsaprivkey8.pem

NewEpTray PrivateKey 경로

로그인 부분 참조

login.auto-sign-up

true

Knox 및 AD 사용자 자동 가입 허용

사용자 인증 후 시스템에 자동 가입된다.

user.auto-permission

true

사용자가 시스템 가입 신청시 자동 승인

false 시 승인대기 후 관리자 승인 절차

privacy-policy.check.enabled

true

시스템 이용 약관 동의 필터 사용 여부

false 시 약관 동의 여부 체크하지 않는다

privacy-policy.check.exclude-path

/privacypolicy/terms/valid

사용자가 약관을 동의 했는지 체크하는 필터 예외 path

email.limit-body

1048576

email 본문의 길이

발신 API에서 본문 사이즈는 최대 1mb까지 입력 가능 (Knox기준)

email.limit-recipients

100

최대 수신인

입력 가능한 최대 수신인 수는 100명 (Knox기준)

batch.user.sync.retire.enabled

false

퇴직자 처리 배치 사용여부

별도의 사용자 정보 데이터가 동기화 되어 있어야 한다.

batch.user.sync.cron

0 10 23 * * ?

사용자동기화 배치 스케줄

batch.user.long-term.month

3

장기미사용자 판단 기준(월)

batch.user.long-term-check.cron

0 10 00 * * ?

장기미사용자 처리 배치 스케줄

batch.user.auth-expired.alarm.before

1,7,14

권한만료 알림 메일 발송일

1일전,7일전,14일전

batch.user.auth-expired.cron

0 10 02 * * ?

권한만료 처리 배치 스케줄

batch.user.auth-expired-mailing.cron

0 30 02 * * ?

권한만료 알림 메일 배치 스케줄

batch.sys-use-log.menu-use-history.cron

0 10 01 * * ?

메뉴사용이력 배치 스케줄

batch.sys-use-log.menu-utilization.cron

0 30 01 * * ?

메뉴활용도 배치 스케줄

security.sql-injection.allowed-pattern

.*[^a-zA-Z0-9_\\s,].\*

SQL에 허용되는 문자

영문 대소문자, 숫자, 스페이스, 콤마만 허용

security.authentication.exclude-path

/**/noauth/**

권한,인증 체크에서 제외되는 url

security.jwt.secret-key

jwt를 암복호화 하기 위한 키 (256bits 이상)

시스템별로 상이해야 하며 키가 유출되지 않도록 주의해야 한다.

security.jwt.expiration-time

8

jwt 토큰 유지 시간

요청시 마다 체크. 유효하지 않을 경우 로그아웃

security.eptray.expiration-time

24

eptray 유지 시간

eptray 로그인시 체크. 이 시간이 경과되면 Knox 재로그인 필요

security.check.access.timeout

true

TimeoutCheckInterceptor 사용 여부

시스템을 일정시간 사용하지 않을 경우 로그아웃

security.access.limit.timeout

30

타임아웃 체크 기준 시간

security.check.duplicate.login

false

사용자 중복 로그인 체크 여부

access-log.store-type

db

Access Log 저장 방식

db, file

access-log.exclude-path

/**/noauth/**

Access Log에 기록 하지 않을 path

access-log.batch.enabled

true

시스템 사용 이력 통계 배치 실행 여부

access-log.file-path

/logs/access

Access Log 파일 저장 위치

Access Log 저장 방식이 file 일 때 참조

menu-utilization.retention-period

24

메뉴 활용도 보관 기간

common.upload-path

/NAS/upload

업로드 파일 저장 위치

common.upload.directory-name-len

2

업로드 파일의 디렉토리명 길이

common.download.zipfilename

compressed

모두 다운 받기 할때 zip 파일 이름 기본값

common.upload.allowed-extensions

xls,jpg

업로드 가능한 파일의 확장자

common.excel-upload-path

/excel

엑셀 업로드 임시 저장 폴더

common.upload.max-file-size

-1

업로드 가능한 개별 파일 크기 (바이트)

-1일 때 제한 없음

common.upload.max-request-size

-1

모든 파일 및 폼 데이터를 포함한 전체 멀티파트 요청 크기 (바이트)

-1일 때 제한 없음

common.upload.default-encoding

UTF-8

요청을 파싱할 때 사용할 캐릭터 인코딩

custom.upload-path.enabled

false

사용자지정 업로드 패스 설정 여부

custom.upload-path

notice=/nas/sdl/upload/notice,\
+faq=/nas/sdl/upload/faq

사용자지정 업로드 패스 설정 값

excel.mapping.locations

classpath*:/excel/*.xml

excel mapping file 위치

excel.mapping.reloadInterval

10000

excel mapping file 리로딩 시간

밀리 세컨, 0일 경우 리로딩 하지 않음

common.server-time-zone-id

Asia/Seoul

서버 time zone id

사용자 정보내 timezone 설정이 없을떄 기본으로 설정 되는 값

common.server-time-zone

GMT+9:00

서버 time zone

사용자 정보내 timezone 설정이 없을떄 기본으로 설정 되는 값

knox.approval.sync.cron

0 0/3 * * * ?

결재 문서 상태 동기화 주기

language-set

ko_KR,en_US

시스템 언어셋

메세지 프로퍼티 로케일 정보

api.utrans.url

utrans 서버 주소

api.utrans.type

utrans type

api.utrans.key

utrans key

admin.address.check

false

시스템 관리자의 ip 제한

system.email

시스템 대표 email 주소

Knox에 등록된 계정이어야 함

+
+
+

4.1.2. knox.properties

+
+

knox 서비스를 연계를 위해 필요한 설정들이다. Knox Staging, Production 에 따라 값이 다르니 반드시 환경에 맞는 값들이 설정됐는지 확인 후 배포한다.

+
+
+ + + + + +
+ + +1. Knox Stage 연계 시 Knox Stage 계정이 있어야 한다. 특히 메일, 결재 연계를 개발 할 때 Stage에 없는 사용자에게 메일을 보내거나 결재를 상신한다면 오류가 발생하니 반드시 사용자 계정이 있는지 확인 한다.
+2. Knox 연계 신청, 연계 오류 관련 문의는 Knox Support를 통해 문의 한다.
+3. Knox 운영 거점은 한국, 구주, 미주 3곳이 있으므로 연계 신청시 사용자 위치에 따라 거점 신청에 주의하도록 한다. 거점 연계가 안되어 있는 사용자는 결재, 메일 기능을 사용 할 수 없다. +
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
keyvalue설명

knox.system-id

Knox 연계 신청 시 받은 시스템 ID

knox.token

token1, token2, token3

Knox 연계 신청 시 받은 token

knox.address.prefix

openapi.samsung.net,openapi.w1.samsung.net,openapi.w2.samsung.net

한국,구주,미주 서비스 주소

+
+ + + + + +
+ + +token과 서비스 주소는 반드시 쌍으로 등록한다. +
+
+
+
+

4.1.3. 캐시 (Cache)

+
+

SDL은 빈번하게 요청되는 데이터 값을 저장하고 필요시 데이터를 빠르게 불러 올수 있도록 스프링 프레임워크에서 제공하는 캐시를 사용한다. 스프링 캐싱 서비스는 추상화로 제공되므로 캐시 Provider 별 CacheManager 를 구현하여 빈으로 등록을 해야한다.

+
+
+ + + + + +
+ + +SDL의 로컬 개발환경에서는 스프링 Simple Provider(ConcurrentHashMap)를 사용하고 있으며, 캐시 공유가 필요한 운영 환경에서는 Redis 라이브러리를 사용하고 있다. 자세한 설정은 스프링 부트 공식문서를 참조하도록 하고 본 문서에서는 SDL에서 사용하고 있는 캐시 데이터 중심으로 설명한다. +
+
+
+
    +
  • +

    message-all

    +
  • +
+
+
+

MessageBundleService getMessage의 결과를 저장한다. Spring MessageSource의 모든 언어에 대한 Key, Value를 리턴한다.

+
+
+
+
@Cacheable(value = "message-all")
+public Map<String, Map<String, String>> getMessage() {
+    Map<String, Map<String, String>> message = new HashMap<>();
+    for (String locale : languageSet) {
+        message.put(locale, getMessageByLang(new Locale(locale.split("_")[0], locale.split("_")[1])));
+    }
+    return message;
+}
+
+
+
+
    +
  • +

    message

    +
  • +
+
+
+

MessageBundleService getMessageByLang의 결과를 저장한다. locale일 별로 Spring MessageSource의 Key, Value를 리턴한다.

+
+
+
+
@Cacheable(value = "message", key = "#locale")
+public Map<String, String> getMessageByLang(Locale locale) {
+    Set<String> beanNames = messageSource.getBasenameSet();
+    Map<String, String> message = new HashMap<>();
+    for (String beanName : beanNames) {
+        ResourceBundle resourceBundle = ResourceBundle.getBundle(StringUtils.remove(beanName, "classpath:/"), locale);
+        Enumeration<String> keys = resourceBundle.getKeys();
+        while (keys.hasMoreElements()) {
+            String key = keys.nextElement();
+            String javaVersion = StringUtils.substringBeforeLast(System.getProperty("java.version"), ".");
+            if (Float.parseFloat(javaVersion) < 1.9) {
+                message.put(key, new String(resourceBundle.getString(key).getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8));
+            } else {
+                message.put(key, resourceBundle.getString(key));
+            }
+        }
+    }
+    return message;
+}
+
+
+
+
    +
  • +

    menu-all

    +
  • +
+
+
+

MenuService getAllMenuPagePaths 시스템의 모든 메뉴, 메뉴 패스를 저장한다.

+
+
+
+
@Cacheable(value = "menu-all")
+@Override
+public List<Map<String, String>> getAllMenuPagePaths() {
+    return menuDao.getAllMenuPagePaths();
+}
+
+
+
+
    +
  • +

    api-user

    +
  • +
+
+
+

ApiService getUserAuthApiListByUserId 사용자ID를 기준으로 접근할 수 있는 API 목록을 저장한다. 사용자가 늘어날 수록 캐시되는 데이터가 많으니 +캐시 관리에 주의하도록 한다.

+
+
+
+
@Override
+@Cacheable(value = "api-user", key = "#userId")
+public List<Api> getUserAuthApiListByUserId(String userId) {
+    return apiDao.getUserAuthApiListByUserId(userId);
+}
+
+
+
+
    +
  • +

    api-user-menu

    +
  • +
+
+
+

ApiService getUserAuthApiListByUserId 사용자ID와 메뉴ID를 기준으로 접근할 수 있는 API 목록을 저장한다. 사용자가 늘어날 수록 캐시되는 데이터가 많으니 +캐시 관리에 주의하도록 한다.

+
+
+
+
@Override
+@Cacheable(value = "api-user-menu", key = "#userId.concat(':').concat(#menuId)")
+public List<Api> getUserAuthApiListByUserIdAndMenuId(String userId, String menuId) {
+    return apiDao.getApiListByUserIdAndMenuId(userId, menuId);
+}
+
+
+
+
    +
  • +

    page-all-by-menu-auth

    +
  • +
+
+
+

ResourceCacheService getAllPageListByAuth 모든 메뉴의 페이지별 권한 타입을 저장한다.

+
+
+
+
@Cacheable(value = "page-all-by-menu-auth")
+public Map<String, Map<String, List<Page>>> getAllPageListByAuth() {
+    log.debug("PageService Start.");
+    Map<String, Map<String, List<Page>>> menuAuthMap = new HashMap<>();
+
+    Map<String, List<Page>> menuMap = getAllPageListByMenu();
+    for (Map.Entry<String, List<Page>> entry : menuMap.entrySet()) {
+
+        List<Page> authPageList = entry.getValue();
+        Map<String, List<Page>> authMap = new HashMap<>();
+
+        authMap.put("READ", authPageList.stream().filter(page -> "READ".equals(page.getAuthorizationType())).collect(Collectors.toList()));
+        authMap.put("UPDATE", authPageList.stream().filter(page -> "UPDATE".equals(page.getAuthorizationType())).collect(Collectors.toList()));
+        authMap.put("DOWNLOAD", authPageList.stream().filter(page -> "DOWNLOAD".equals(page.getAuthorizationType())).collect(Collectors.toList()));
+        authMap.put("EXECUTE", authPageList.stream().filter(page -> "EXECUTE".equals(page.getAuthorizationType())).collect(Collectors.toList()));
+        menuAuthMap.put(entry.getKey(), authMap);
+    }
+    log.debug("menuAuthMap : {}", menuAuthMap);
+    return menuAuthMap;
+}
+
+
+
+
    +
  • +

    page-full-path-all

    +
  • +
+
+
+

MenuService getPageFullPathList 모든 메뉴 full path를 저장한다.

+
+
+
+
@Cacheable(value="page-full-path-all")
+@Override
+public Map<String, String> getPageFullPathList() {
+
+    List<Map<String, String>> pageFullPathList = menuDao.getPageFullPathList();
+
+    Map<String, String> pageFullPathMap = new HashMap<>();
+
+    for(Map<String, String> pageFullPath : pageFullPathList) {
+        pageFullPathMap.put(pageFullPath.get("pageId"), pageFullPath.get("fullPath"));
+    }
+
+    return pageFullPathMap;
+}
+
+
+
+
+
+

4.2. SdlBaseBootApplication

+
+

SpringBoot 웹 애플리케이션을 배포할 때는 주로 embedded tomcat이 내장된 jar파일을 이용한다. 하지만 war 파일로 빌드, 배포를 진행해야 하는 경우를 위해 SpringBootServletInitializer를 상속받고 있다.

+
+ +
+

4.2.1. SpringBootServletInitializer 상속

+
+
SdlBaseBootApplication
+
+
@SpringBootApplication
+public class SdlBaseBootApplication extends SpringBootServletInitializer {
+    @Override
+    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
+        return application.sources(SdlBaseBootApplication.class);
+    }
+
+    public static void main(String[] args) {
+        SpringApplication.run(SdlBaseBootApplication.class, args);
+    }
+
+}
+
+
+
+
+

4.2.2. Profile 적용

+
+

@Profile 을 이용하여 Profile 별로 다른 설정이 가능하다. +아래는 spring.profiles.active(JAVA OPTS)에 따라서 변경되는 설정이다.

+
+
+
    +
  • +

    아래의 예는 local profile 에서만 적용된다.

    +
  • +
+
+
+
+
@Configuration
+@Profile({"local"})
+public class DbcpDataSourceConfig {
+
+
+
+ + + + + +
+ + +spring.profiles.active 는 runtime에서 매우 중요한 프로퍼티다. ServletContext에 등록되는 Filter, Servlet이 결정되고, +Spring Bean의 생성도 결정되니 반드시 JAVA OPTS에 설정해야 한다. +
+
+
+
+
+

4.3. Spring Config

+
+

SDL 6.0에서는 모든 Spring 설정이 Java Config로 되어 있다. com.samsung.config 패지키에 있다. +기본적인 설정파일들은 아래와 같다.

+
+
+
+
com.samsung.config
+    |- CacheConfig (1)
+    |- DbcpDataSourceConfig (2)
+    |- JasyptConfig (3)
+    |- JpaDatasourceConfig (4)
+    |- KnoxSyncBatchConfig (5)
+    |- QuartzClusteringConfig (6)
+    |- QuartzConfig (7)
+    |- RedisConfig (8)
+    |- SpringConfig (9)
+    |- SpringWebConfig (10)
+    |- SwaggerConfig (11)
+    |- SysUseLogBatchConfig (12)
+    |- TemplateConfig (13)
+    |- UserBatchConfig (14)
+    |- WebClientConfig (15)
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1캐시 설정 (Simple Provider)
2데이터 소스 설정
3프로퍼티 값 암호화를 위한 Jasypt 설정
4Jndi 데이터 소스 설정
5결재동기화 배치 쿼츠(Quartz) job/trigger 설정
6Quartz 클러스터링 설정 (JDBC Jobstore)
7Quartz 설정 (RAM Jobstore)
8캐시 설정 (Redis)
9Spring 설정
10Spring WebApplicationContext 설정
11Swagger 설정
12시스템 로그 배치 쿼츠(Quartz) job/trigger 설정
13Thymeleaf 템플릿 엔진 설정
14사용자 관련 배치 쿼츠(Quartz) job/trigger 설정
15WebClient 설정
+
+
+

4.3.1. SpringConfig

+
+

Spring 설정 중에 가장 기본이 된다. Transaction, MessageSource 를 사용 할수 있도록 +Spring Container 에 등록한다.

+
+
+
    +
  • +

    @Configuration

    +
  • +
+
+
+

Configuration Annotation은 Spring Container에게 해당 클래스가 Bean들을 등록하는 클래스라는 것을 알려주기 위한 Annotation이다. +프로젝트에서 Bean을 등록 할때는 클래스에 Configuration Annotation을 설정하도록 한다.

+
+
+
    +
  • +

    @EnableTransactionManagement

    +
  • +
+
+
+

EnableTransactionManagement annotation을 사용하면 Spring에서 @Transactional 을 사용해 Transaction을 관리 할 수 있다.

+
+
+
xml 설정
+
+
<tx:annotation-driven transaction-manager="txManager" />
+
+
+
+

@Transactional 에 대한 세부적인 내용은 Spring +Transactional Settings +를 참고 한다.

+
+
+
    +
  • +

    PropertySource

    +
  • +
+
+
+

property 파일을 읽기 위해 사용한다. SDL에서는 config.properties파일과 knox.properties 파일을 기본으로 로딩한다. +PropertySource에 등록된 값은 Spring Bean에서 사용할 수 있다.

+
+
+
    +
  • +

    Value Injection
    +@Value를 사용해 PropertySource의 값을 Injection 한다.

    +
  • +
  • +

    Environment Injection
    +org.springframework.core.env.Environment를 Injection 하고 getProperty("key")를 이용해 값을 얻는다.

    +
  • +
+
+
+
Value Injeciton
+
+
@Value("${security.access.limit.timeout:30}")
+private int limitTimeout;
+
+@Value("${security.check.access.timeout:false}")
+private boolean checkTimeout;
+
+
+
+
Environment Injection
+
+
String functionUrl = environment.getProperty(KNOX_EMP_SERVICE) + "/employees";
+
+
+
+
    +
  • +

    ComponentScan

    +
  • +
+
+
+

com.samsung 패키지에 속해 있는 @Service, @Repository, @Component 의 Bean만 찾아서 등록한다.

+
+
+
+
@ComponentScan(basePackages = "com.samsung", useDefaultFilters = false, includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Service.class), @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Repository.class), @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Component.class)})
+
+
+
+ + + + + +
+ + +@Controller Baen은 SpringConfig가 아닌 SpringWebConfig에서 Scan한다. +
+
+
+
    +
  • +

    MessageSource

    +
  • +
+
+
+

다국어 적용을 위해 MessageSource 를 사용한다. Backend 에서는 MessageSourceAccessor나 MessageSource를 이용해 다국어를 적용한다. +특히 velocity엔진 템플릿에서 다국어를 사용하기 위해서는 반드시 MessageSourceAccessor를 사용하도록 한다.

+
+
+ + + + + +
+ + +Frontend 가 처음 로딩 될 때 서버에서 "/noauth/messages/all" API를 호출해 시스템의 모든 메세지 리소스를 받는다. MessageBundleService에서는 config.properties 에 설정된 language-set에 해당하는 Message Properties 파일을 읽어 JSON으로 만들어 리턴한다. +
+
+
+
+

4.3.2. SpringWebConfig

+
+

Spring WebApplicationContext 설정을 위한 파일이다. WebMvcConfigurer를 구현하고 있으며, +Formatter, MessageConverter 등을 재정의 할 수 있다.

+
+
+
xml
+
+
<mvc:annotation-driven/>
+
+
+
+

@Configuration, @EnableWebMvc 를 선언하는 것으로 대체될 수 있다.

+
+
+
java
+
+
@Configuration
+@EnableWebMvc
+public class SpringWebConfig implements WebMvcConfigurer {
+}
+
+
+
+
    +
  • +

    ComponentScan

    +
  • +
+
+
+
+
@ComponentScan(basePackages = "com.samsung", useDefaultFilters = false, includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class), @ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableSwagger2.class)})
+
+
+
+

@Controller, @EnableSwagger2 로 선언된 Bean을 Scan한다.

+
+
+
    +
  • +

    addResourceHandlers

    +
  • +
+
+
+

정적 리소스를 관리하는 ResourceHandlerRegistry에 pathPatterns와 리소스 위치를 등록한다.

+
+
+
+
@Override
+public void addResourceHandlers(ResourceHandlerRegistry registry) {
+    registry.addResourceHandler("index.html").
+            addResourceLocations(webResourceRoot);
+    registry.addResourceHandler("favicon.ico").
+            addResourceLocations(webResourceRoot + "static/");
+    registry.addResourceHandler("static/**")
+            .addResourceLocations(webResourceRoot + "static/");
+    registry.addResourceHandler("swagger-ui.html")
+            .addResourceLocations("classpath:/META-INF/resources/");
+    registry.addResourceHandler("/webjars/**")
+            .addResourceLocations("classpath:/META-INF/resources/webjars/");
+}
+
+
+
+
    +
  • +

    MultipartResolver

    +
  • +
+
+
+

Multipart 요청에 대한 처리를 담당하는 Resolver다. 파일업로드의 최대 크기 등을 설정한다.

+
+
+
+
    @Value("${common.upload.max-request-size:-1}")
+    private long maxRequestSize;
+    @Value("${common.upload.max-file-size:-1}")
+    private long maxFileSize;
+
+
+    @Bean
+    public MultipartResolver multipartResolver() {
+        return new StandardServletMultipartResolver();
+    }
+
+    @Bean
+    public MultipartConfigElement multipartConfigElement() {
+        MultipartConfigFactory factory = new MultipartConfigFactory();
+        factory.setMaxRequestSize(DataSize.ofBytes(maxRequestSize));
+        factory.setMaxFileSize(DataSize.ofBytes(maxFileSize));
+
+        return factory.createMultipartConfig();
+    }
+
+
+
+

config.properties에 설정한 multipart/form-data 최대 사이즈, 전체 업로드 파일의 최대 용량을 참조한다.

+
+
+
+
+

4.4. HandlerInterceptor

+
+

org.springframework.web.servlet.HandlerInterceptor의 구현클래스에 대해서 설명한다. +HandlerInterceptor는 Controller 전후 에 실행되며 preHandle, postHandle, afterCompletion 메소드를 제공한다.

+
+
+
+
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+    return true;
+}
+
+default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
+}
+
+default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
+}
+
+
+
+

4.4.1. LoggingInterceptor

+
+

LoggingInterceptor는 시스템 이용 로그를 남기기 위한 Interceptor이다. 대부분의 로직은 postHandle에 구현되어 있으며 +TN_CF_SYS_USE_LOG 테이블에 데이터를 기록한다. +사용자정보(ID, IP, 브라우저), 요청시간, 응답시간, 요청 파라미터를 기록하며 이 데이터는 메뉴 사용 이력, +메뉴 활용도, 파일 다운로드 이력등에 사용된다.

+
+
+
+
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) {
+	String requestMethod = request.getMethod();
+    if(HttpMethod.OPTIONS.name().equals(requestMethod)) {
+    	return;
+    }
+    String requestURI = request.getRequestURI().substring(request.getContextPath().length());
+
+
+    SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd HHmmssSSS", Locale.getDefault());
+    String sTime = formatter.format(new Date());
+
+    long start = (long) request.getAttribute("start");
+    long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
+
+    String userId = "anonymous";
+    if (ObjectUtils.isNotEmpty(Account.currentUser())) {
+        userId = Account.currentUser().getUserId();
+    }
+
+    String getDecodedRequestURI = WebUtil.getDecodedRequestUrl(request, requestURI);
+    SysUseLog sysUseLog = new SysUseLog();
+    sysUseLog.setLogOccurId(idGenService.getNextStringId());
+
+    String menuId = request.getHeader("menu-Id");
+    String pageId = request.getHeader("page-id");
+    if(pageId != null) sysUseLog.setDescription(menuService.getPageFullPath(pageId));
+
+    sysUseLog.setNodeId(node);
+    sysUseLog.setUserId(userId);
+    sysUseLog.setUseFromDate(sTime.substring(0, 8));
+    sysUseLog.setUseFromHhmmss(sTime.substring(9, 15));
+    sysUseLog.setUseThruDatetime(new Date());
+    sysUseLog.setResponseTime(elapsedTime);
+    sysUseLog.setPath(getDecodedRequestURI);
+
+    String query = request.getQueryString();
+    if (query != null) {
+        try {
+            query = URLDecoder.decode(query, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            log.error(e.getMessage(), e);
+        }
+        String queryStr = StringUtils.replace(query, "'", "\\'");
+        sysUseLog.setParameter(queryStr);
+        sysUseLog.setUrl(requestURI + "?" + queryStr);
+    }
+    sysUseLog.setUseIp(WebUtil.getClientIp(request));
+    sysUseLog.setBrowserTypeName(request.getHeader("User-Agent"));
+    sysUseLog.setLogFlag(LogFlag.ACCESSLOG.getValue());
+    sysUseLog.setReqType(ReqType.WEB);
+    sysUseLog.setFirstRegDatetime(new Date());
+
+    sysUseLog.setMenuId(menuId);
+    sysUseLog.setPageId(pageId);
+    sysUseLog.setMethod(requestMethod);
+
+    // 대리 로그인 시 별도 이력 남김
+    String originalUserId = request.getHeader("original-user-id");
+    if(StringUtils.isNotEmpty(originalUserId)){
+        sysUseLog.setLogFlag(LogFlag.IMPERSONATION_ACCESS.getValue());
+        sysUseLog.setDescription("Original User Id : " + originalUserId);
+    }
+
+    try {
+        if ("db".equalsIgnoreCase(storeType)) {
+            accessLogService.insertAccessLog(sysUseLog);
+        } else {
+            accessLogService.logAccess(sysUseLog);
+        }
+    } catch (Exception e) {
+        log.error(e.getMessage());
+    }
+}
+
+
+
+

config.properties 파일의 access-log.store-type 값에 따라 db또는 file에 저장된다.

+
+
+
+

4.4.2. AuthenticationInterceptor

+
+

AuthenticationInterceptor는 사용자가 인증이 되어 있는지 체크하는 인터셉터다. +인증이 되어 있지 않을 경우 로그인 화면으로 이동한다.

+
+
+
+
String token = request.getHeader("x-auth-token"); (1)
+// Token 값 유무
+if (StringUtils.isEmpty(token)) {
+    throw new NotFoundTokenException("Not Found Token");
+}
+
+
+
+

인증이 된 사용자는 모든 요청 해더에 x-auth-token 값을 같이 보내야 한다. +Frontend에서 x-auth-token값을 공통으로 Set하는 코드는 main.js 를 참고한다.

+
+
+
+
axios.defaults.headers.common['x-auth-token'] = localStorage.getItem('userToken');
+
+
+
+

loginService.js에서 로그인이 완료되면 localStorage에 toekn값을 저장하고, axios 요청 시마다 +해더에 추가 한다.

+
+
+
+
String originalUserId = request.getHeader("original-user-id"); (1)
+
+
+
+ + + + + +
1대리로그인 시 원래 사용자의 ID가 헤더에 담겨있다.
+
+
+

대리로그인은 개발 시 기능, 권한 테스트를 하기 위한 용도로 관리자가 다른 사용자로 로그인하는 기능이다. 이 기능은 운영중인 시스템에 사용할 경우 보안에 문제가 발생하니 반드시 개발 시 테스트 용도로만 사용한다.

+
+
+ + + + + +
+ + +대리로그인은 개발 시 테스트를 위한 기능으로 운영 시 사용하지 않는다. +
+
+
+
+
Boolean jwtValid = jwtUtil.validateToken(token, user); (1)
+if (Boolean.TRUE.equals(jwtValid)) {
+    if (user.isActiveFlag()) {
+        user.setSystemAdminUser(roleService.isSystemAdmin(userId));
+        Account.updateCurrentAccount(user);
+        return true;
+    } else {
+        // 시스템 승인 대기중
+        throw new WaitingUserException("Waiting User"); (2)
+    }
+} else {
+    throw new InvalidTokenException("Token Expired");
+}
+
+
+
+ + + + + + + + + +
1유효한 Token인치 확인
2Active Flag가 false일 경우 시스템 승인 대기중 상태
+
+
+

로그인 Token 관련해서는 로그인을 참고한다.

+
+
+ + + + + +
+ + +중복로그인 방지를 위해 로그인시 발급된 jwt값을 db에 저장하고, 현재 jwt값을 비교한다. +config.properties파일 security.check.duplicate.login이 true일 때만 동작한다. +
+
+
+
+

4.4.3. AuthorizationInterceptor

+
+

AuthorizationInterceptor는 요청 URL(API)가 사용자에게 접근이 가능한지 판단하는 인터셉터다.

+
+
+
+
if (user == null) throw new NoSearchUserException("No Search User"); (1)
+if (roleService.isSystemAdmin(user.getUserId())) { (2)
+	if(adminAddressCheck) {
+		List<String> adminAddresses = adminAddressService.getAdminAddresses(null); (3)
+		String remoteAddr = WebUtil.getClientIp(request);
+		if(ObjectUtils.isNotEmpty(adminAddresses) && !adminAddresses.contains(remoteAddr)) {
+			throw new AuthorizationException("Remote address is not admin ip address.");
+		}
+	}
+	return true;
+}
+
+
+
+ + + + + + + + + + + + + +
1사용자가 null일 경우에는 Exception을 발생한다.
2현재 사용자가 System Admin일 경우
3접근 IP가 System Admin IP에 포함 되어 있는지 판단한다.
+
+
+ + + + + +
+ + +config.properties의 admin.address.check값이 true일 때만 System Admin IP를 체크 한다. +
+
+
+
+
for (Api api : authMenuList) {
+    if (api.getHttpMethod().equals(httpMethod) && checkUriMatch(api.getApiPath(), api.getApiParameters(), decodedUrl, queryString)) {
+        return true;
+    }
+}
+throw new AuthorizationException("API 권한 없음");
+
+
+
+ + + + + +
+ + +URL 권한 체크 시에는 파라미터 단위까지 체크한다. test?abc=123과 test?adb=123은 다른 권한이다. +
+
+
+
+

4.4.4. PrivacyPolicyInterceptor

+
+

PrivacyPolicyInterceptor는 이용 약관 동의 여부를 체크하는 Interceptor이다.

+
+
+
+
/**
+ * 약관동의 사용 여부
+ */
+@Value("${privacy-policy.check.enabled:true}")
+private boolean privacyPolicyCheck;
+
+/**
+ * 약관동의 인터셉터 체크 제외 URI
+ */
+@Value("${privacy-policy.check.exclude-path}")
+private String privacyPolicyCheckExcludePath;
+
+
+
+

약관 동의 체크 인터셉터 사용 여부와 제외 URI는 위 프로퍼티 값을 이용한다.

+
+
+
+
if (user != null && !user.isPrivacyPolicy()) {
+    throw new PrivacyPolicyException("Privacy policy agreement required.");
+} else {
+    return true;
+}
+
+
+
+

약관 동의가 완료되지 않은 경우 PrivacyPolicyException이 발생하고 약관 동의 페이지로 이동한다.

+
+
+
+

4.4.5. TimeoutCheckInterceptor

+
+

TimeoutCheckInterceptor는 일정시간 동안 사용하지 않을 경우 자동 로그아웃 하는 기능이다.

+
+
+

config.properties파일 security.check.access.timeout 값이 true 일때만 동작한다. security.access.limit.timeout(분) 동안 시스템을 사용하지 않을 경우 자동 로그아웃 한다.

+
+
+
+
@Value("${security.access.limit.timeout:30}")
+private int limitTimeout;
+
+static final long MILLISECONDS_PER_MINUTE = 60L*1000L;
+
+@Override
+public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
+    String lastAccessTimeHeader = request.getHeader("last-access-time");
+    String impersonate = request.getHeader("original-user-id");
+    if(StringUtils.isNotEmpty(lastAccessTimeHeader) && !StringUtils.equals("undefined", lastAccessTimeHeader) && !StringUtils.equals("NaN", lastAccessTimeHeader) && StringUtils.isEmpty(impersonate)){
+        long lastActivityTime = Long.parseLong(lastAccessTimeHeader);
+        long currentTime = System.currentTimeMillis();
+        long jwtRetentionTime = currentTime - lastActivityTime;
+        if (jwtRetentionTime > (limitTimeout * MILLISECONDS_PER_MINUTE)) {
+            //limitTimeout 동안 사용하지 않으면 자동 로그아웃
+            throw new TokenRetentionTimeoutException("Token Retention Timeout");
+        }
+    }
+    return true;
+}
+
+
+
+

동작 방식은 Backend 요청 시 Response Header에 마지막으로 요청한 시간을 세팅하고 다음 요청시 Request Header에 last-access-time에 설정된다. 현재 시간과 last-access-time을 비교해 로그아웃 여부를 결정한다.

+
+
+
+

4.4.6. UploadFileExtensionCheckInterceptor

+
+

UploadFileExtensionCheckInterceptor는 파일 업로드 시 파일 확장자를 검사한다.

+
+
+

config.properties파일 common.upload.allowed-extensions에 정의된 확장자를 가진 파일만 업로드 할 수 있다.

+
+
+ + + + + +
+ + +UI에서도 허용되는 파일 확장자를 정의해줘야한다. MultipleFileUploader.vue를 부모컴포넌트에서 사용할때 pros useExtList을 설정해야 한다. 자세한것은 File Component를 참고한다. +
+
+
+
+
+

4.5. 공통컴포넌트 & 유틸

+
+

4.5.1. DatePicker

+
+
DatePicker Component 사용방법
+
+

template에 component 추가

+
+
+
+
<template>
+    <sdl-datepicker
+        v-model="searchPeriod"
+        :period="searchPeriod">
+    </sdl-datepicker>
+</template>
+
+
+
+

props 설명

+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
props명설명

refKey

component Reference Key

period

type: Object, //기간 정보

minimumView

type: String, //최소 표현 단위 - ex) month로 설정 시 '월' 단위로 picker 선택 가능

+

default: 'day',

wrapClass

type: String, //class명

+

default: 'wd150',

disabled

type: Boolean, //달력 선택 가능여부

default: false,

+
+
+
+

4.5.2. Excel Download Button

+
+
Excel Download Component 사용방법
+
+

template에 component 추가

+
+
+
+
<template>
+    <sdl-exceldownloadbtn
+        :excelInfo="excelInfo"
+        :btnClass="btnClass"
+        :before-click="beforeClick"
+    />
+</template>
+
+
+
+

props 설명

+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
props명설명

btnLabel

type: String, //버튼 label 정의

+

default:'sdp.common.label.excelDownload'

excelInfo

type: Object

excelInfo: {

+

columnInfoFile:'externalUser',

+

fileName:'externalUser', //다운받을 엑셀파일명

+

queryId:'selectUser', //쿼리 아이디

+

sheetName:'외부 사용자', //sheet명

+

useNumberField:'Y', //엑셀 처음 컬럼에 넘버링 사용 여부

+

param: { deleted: '0', activeFlag: '1', externalUser: '1' },

+

}

beforeClick

엑셀 다운로드 전에 체크하고자 하는 로직이 있을 경우 사용

+

다운로드 가능할 경우 true 리턴

template :

+

<sdl-exceldownloadbtn :before-click="beforeClick"/>

+

method :

+

beforeClick() {

+

return true;

+

}, +default: false,

btnClass

type: String, //버튼 class 정의

+

default: 'btn btn-secondary'

+
+
+
+

4.5.3. Excel Upload Button

+
+
Excel Upload Component 사용방법
+
+

template에 component 추가

+
+
+
+
<template>
+    <sdl-exceluploadbtn
+        :excelInfo="excelUpInfo"
+        @after-upload="afterUpload"
+        :btnClass="btnClass"
+        :before-click="beforeClick"
+    />
+</template>
+
+
+
+

props 설명

+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
props명설명

btnLabel

type: String, //버튼 label 정의

+

default:'sdp.common.label.excelUpload'

excelInfo

type: Object

excelInfo:{

+

columnInfoFile:'largeExcelupload', //Column Info XML 파일

+

queryId:'selectUser', //실행할 query Id

+

returnPath:'', //엑셀 저장 후 이동할 페이지 경로('after-upload' event로 대체)

+

startRow:4, //저장할 데이터의 첫 Row

+

useObjectName:'com.samsung.sample.entity.ExcelTestObject', //데이터를 처리하기 위한 자바 객체

+

},

beforeClick

엑셀 업로드 전에 체크하고자 하는 로직이 있을 경우 사용

+

업로드 가능할 경우 true 리턴

template :

+

<sdl-exceluploadbtn :before-click="beforeClick"/>

+

method :

+

beforeClick() {

+

return true;

+

},

btnClass

type: String, //버튼 class 정의

+

default: 'btn btn-secondary'

+
+

event 설명

+
+ +++++ + + + + + + + + + + + + + + +
event명설명

after-upload

엑셀 업로드 후 진행할 로직을 추가

template :

+

<sdl-exceldownloadbtn @after-upload="afterUpload"/>

+

method :

+

afterUpload(rtn){

+

console.log(rtn);

+

SDLUtil.alert('excel upload 완료. 다음 작업 할것');

+

},

+
+
+
+

4.5.4. File Component

+
+
File Component 사용방법
+
+
    +
  • +

    template에 component 추가

    +
  • +
+
+
+
+
<template>
+    <sdl-fileuploader
+        style="width:1532px"
+        ref="uploader1"
+        :fileList="fileList"
+        @complete="rtnUploadList"
+        downloadType="A"
+        :maxTotalSize="uploadFileSize"
+        :modifyFlag="true"
+        useExtList="zip"
+        multiFileNm="정test"
+    ></sdl-fileuploader>
+</template>
+
+
+
+
    +
  • +

    선택 파일 다운로드 시 파일별다운로드(default) 및 압축파일다운로드 기능 제공

    +
  • +
  • +

    MultipleFileUploader.vue 파일 내 checkedDownload method 부분 참고

    +
  • +
+
+
+

props 설명

+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
props명설명

modifyFlag

type: Boolean, //true(등록/삭제 가능), false(다운로드만 가능)

+

default: true,

uploadURL

type: String, //업로드할 url 등록

+

default: '${SDLUtil.API_URL}/resource/attachments/multifile-upload' //SDL 사용시 default

multiFileNm(필수)

type: String //멀티 다운로드시 대표 이름

downloadType

type: String //file component를 한 vue에서 여러개 사용할 경우 downloadType으로 구분한다.

+

ex) 'A', 'B',…​ or 'F', 'I'

fileList

type: Array //파일 리스트를 component에 전달(SDL을 사용할 경우 API에서 넘어온 리스트 그대로 전달할것)

fileList: [

+

{

+

firstRegDatetime:1563412831556,

+

firstRegrId:'sdp-front',

+

lastModDatetime:1563412831556,

+

lastModrId:'sdp-front',

+

fileId:'AWwCqp1KAADw5f1I',

+

fileName:'정sdl_html_20190708.zip',

+

filePathName:'C:\\NAS\\SDL\\upload',

+

fileSize:14154292,

+

fileExtensionName:'b518546c12f1482a87ce44772fd56057',

+

fileMimeTypeName:'application/x-zip-compressed',

+

deleted:false,

+

downloadType:'A',

+

orderIdx:0,

+

},

useExtList

type: String, //업로드할 파일 확장자를 세팅

+

default: 'zip,xlsx,xls,ppt,pptx'

maxItems

type: Number, //최대 업로드 가능한 파일 갯수 세팅

+

default: 30

maxTotalSize

type: Number, //최대 첨부파일 용량을 세팅

+

default: 1073741824

+
+

event 설명

+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
event명설명

complete

업로드 후 업로드한 파일 리스트를 return 해준다.

template :

+

<sdl-fileuploader @complete="rtnUploadList"/>

+

method :

+

rtnUploadList(downloadType, fileList) {

+

console.log('downloadType:', downloadType);

+

console.log('rtnUploadList:', fileList);

+

SDLUtil.alert('받은 업로드 리스트 다음 작업 할것');

+

},

init

파일 컴퍼넌트를 init 처리한다.

template :

+

<sdl-fileuploader ref="uploader1"/>

+

method :

+

this.$refs.uploader1.init()

onUpload

추가한 첨부파일을 업로드 한다. 업로드 후 complete에 지정한 메소드로 리스트 return 해줌

template :

+

<sdl-fileuploader ref="uploader1"/>

+

method :

+

this.$refs.uploader1.onUpload()

+
+
+
+

4.5.5. Modal

+
+

Modal 팝업 띄우기

+
+
+
+front 01 01 +
+
+
+
    +
  • +

    형식 : SDLUtil.show(import된 컴포넌트, 컴포넌트에 전달할 인자, modal properties, events)

    +
  • +
+
+
+

props 설명

+
+ +++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
props명필수여부TypeDefault설명

name

true

[String, Number]

모달 명

resizable

false

Boolean

false

resize 가능여부

draggable

false

[Boolean, String]

false

drag 가능 여부

scrollable

false

Boolean

false

스크롤 가능 여부

width

false

[String, Number]

600

height

false

[String, Number]

300

minWidth

false

Number (px)

0

minHeight

false

Number (px)

0

maxWidth

false

Number (px)

Infinity

maxHeight

false

Number (px)

Infinity

+
+

event 설명

+
+ ++++ + + + + + + + + + + + + + + + + + + + + + + + + +
event 명설명

before-open

모달 오픈전 실행할 이벤트 정의

opened

모달 오픈후 실행할 이벤트 정의

before-close

모달 닫기전 실행할 이벤트 정의

closed

모달 닫기 후 실행할 이벤트 정의

+
+
+

4.5.6. Pagination

+
+
Pagination Component 사용방법
+
+

template에 component 추가

+
+
+
+
<template>
+    <sdl-pagination
+        :current-page="currentPage"
+        :total-rows="records"
+        :per-page="pageSize"
+        @change="onSearch"
+        align="center"
+    />
+</template>
+
+
+
+

props 설명

+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
props명설명비고

currentPage

type: Number //현재 페이지

totalRows

type: Number //총 데이터 갯수

perPage

type: Number //한 페이지에 보여질 데이터 갯수

limit

type: Number, //페이징 영역내에 보여질 페이지 숫자 갯수

+

default: 10

+
+
+
+

4.5.7. Tree

+
+
Tree Component 사용 방법
+
+

template에 component 추가

+
+
+
+
<template>
+    <sdl-tree
+        ref="tree"
+        :data="data"
+        :showCheckbox="false"
+        :item-events="itemEvents"
+        show-checkbox
+        :multiple="false"
+        allow-batch
+        whole-row
+        draggable
+        @item-click="itemClick"
+        @item-drag-start="itemDragStart"
+        @item-drag-end="itemDragEnd"
+        @item-drop-before="itemDropBefore"
+        @item-drop="itemDrop"
+    >
+    </sdl-tree>
+</template>
+
+
+
+

props 설명

+
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
props명TypeDefault설명

data

Array

set tree data

size

String

set tree item size , value : 'large' or '' or ''small'

show-checkbox

Boolean

false

set it show checkbox

allow-transition

Boolean

true

allow use transition animation

whole-row

Boolean

false

use whole row state

no-dots

Boolean

false

show or hide dots

collapse

Boolean

false

set all tree item collapse state

multiple

Boolean

false

set multiple selected tree item

allow-batch

Boolean

false

in multiple choices. allow batch select

text-field-name

String

'text'

set tree item display field

value-field-name

String

'value'

set tree item value field

children-field-name

String

'children'

set tree item children field

item-events

Object

{}

register any event to tree item, example

async

Function

async load callback function , if node is a leaf ,you can set 'isLeaf: true' in data

loading-text

String

'Loading'

set loading text

draggable

Boolean

false

set tree item can be dragged , selective drag and drop can set 'dragDisabled: true' and 'dropDisabled: true' , all default value is 'false'

drag-over-background-color

String

'#C9FDC9'

set drag over background color

klass

String

set append tree class

+
+

node.model 의 method

+
+ ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
method 명params

addChild

(object) newDataItem

addAfter

(object) newDataItem, (object) selectedNode

addBefore

(object) newDataItem, (object) selectedNode

openChildren

closeChildren

+
+

event 설명

+
+ ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
event 명설명

item-click

item-click(node, item, e)

item-toggle

item-toggle(node, item, e)

item-drag-start

item-drag-start(node, item, e)

item-drag-end

item-drag-end(node, item, e)

item-drop-before

item-drop-before(node, item, draggedItem, e)

item-drop

item-drop(node, item, draggedItem, e)

node

current node vue object

item

current node data item object

+
+

data item의 properties

+
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
properties명typedefault설명

icon

String

custom icon css class

opened

Boolean

false

set leaf opened

selected

Boolean

false

set node selected

disabled

Boolean

false

set node disabled

isLeaf

Boolean

false

if node is a leaf , set true can hide '+'

dragDisabled

Boolean

false

selective drag

dropDisabled

Boolean

false

selective drop

+
+
+
+

4.5.8. SDLUtil 사용 방법

+
+
SDL 공통 Util 사용방법
+
+
+
import SDLUtil from '@/utils/SDLUtil';
+// 또는
+import { SDLUtil, StringUtil } from '@/utils';
+
+// import 후 사용
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
method 명설명

getLoginedUserInfo

로그인한 사용자 정보

getMsgProp

메시지 프로퍼티에서 값 가져오기

SDLUtil.getMsgProp('sdl.user.label.work', ['하나', '둘'])

alert

레이어 alert

SDLUtil.alert({

+

msg:'I am a tiny dialog box.<br/>And I render <b>HTML!</b>',

+

title:'alert',

+

okLabel:'ok',

+

onOkEvt: () ⇒ console.log('ok'),

+

});

+

SDLUtil.alert("메시지"); //이 경우 나머지 입력값들은 default로 세팅되고 msg만 입력됨

confirm

레이어 confirm

SDLUtil.confirm({

+

msg: 'confirm body',

+

title: 'confirm title',

+

okLabel: 'ok label',

+

cancelLabel: 'cancel label',

+

onOkEvt: () ⇒ console.log('ok'),

+

onCancelEvt: () ⇒ console.log('cancel'),

+

});

showLoadingBar

default loading bar show/hide

SDLUtil.showLoadingBar(true);

getCommCodeList

로딩시 가져온 전체 코드리스트에서 특정 parentId에 대한 코드 리스트 return

const codelist = SDLUtil.getCommCodeList({ commCodeTypeId: 'MENUTYPE' });

+

console.log(codelist);

openUserPopup

사용자 검색 팝업

SDLUtil.openUserPopup({

+

searchColumn: 'userName',

+

searchTxt: this.searchUserNm,

+

rtnFunc: this.getUserList,

+

knoxSearch: !this.internalFlag,

+

});

isLogin

로그인 여부

getI18nLanguage

현재 로케일 정보

setI18nLanguage

로케일 변경

show

모달창 오픈

SDLUtil.show(

+

WorkgroupPopup,

+

{

+

rtnFunc: args ⇒ {

+

this.checkRoleWorkgroupList(args);

+

},

+

},

+

{ width: '850px', height: 'auto' },

+

);

loadAllCommCodeList

전체 공통코드 리스트 load

formParameters

object 를 formData로 변경

+
+
+
+

4.5.9. StringUtil 사용 방법

+
+
StringUtil 사용방법
+
+
+
import StringUtil from '@/utils/stringUtil';
+// 또는
+import { SDLUtil, StringUtil } from '@/utils';
+
+// import 후 사용
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + +
method 명설명

queryStringfy

url 쿼리 문자열로 변경

checkPatternUrl

입력된 문자열이 url 패턴인지 체크

+
+
+
+

4.5.10. DateUtil 사용 방법

+
+
DateUtil 사용방법
+
+
+
import DateUtil from '@/utils/DateUtil';
+// import 후 사용
+
+
+
+ + + + + +
+ + +DateUtil의 기본 데이터 포맷은 'YYYY-MM-DD' 형식이며, 국가별 언어에 따른 message.data-format.properties 는 사용자에게 보여줄 때의 형식이다. +
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
method 명설명

now

현재 날짜

addDate

현재 날짜 기준 이후 데이터

.addDate(1, 'M') 현재 날짜에서 한 달을 더한 날

subDate

날짜 기준 이전 데이터

.subDate(1, 'M') 현재 날짜에서 한 달을 뺀 날

stdFormat

표준 포맷으로 변환

Backend로 데이터를 보낼때의 표준 포맷(YYYY-MM-DD 순)

+
+
+
+

4.5.11. 공통 validation

+
+

필수 입력값 validation을 onBlur 시 그리고 저장/수정 시 하기 위해서는 아래와 같은 방법으로 처리한다.

+
+
+
+front 01 02 +
+
+
+

validation 하고자 하는 tag 에 v-validation(directive) 이용(errorMessage 를 지정하지 않을 경우 default로 표시 됨, default message의 경우 다국어 지원이 되지 않음)

+
+
+
    +
  • +

    팝업 없는 화면인 경우(directive 지정시 별도 내용 없이 처리)

    +
  • +
+
+
+
+
<div class="mb-2" style="position:relative;">
+<label for="title">{{ $t('sdl.approval.label.title') }} <span class="text-required">&#42;</span></label>
+<input
+  id="title"
+  type="text"
+  class="form-control"
+  v-model="apprDoc.title"
+  :readonly="apprDoc.docStatus !== ''"
+  v-validation=""
+  :errorMessage="$t('sdl.department.message.requiredValue')"
+/>
+</div>
+
+
+
+
    +
  • +

    등록 화면에 팝업 등록이 또 있는 경우(groupId 를 다르게 써서 체크)

    +
  • +
+
+
+
+
<tr>
+    <th class="text-nowrap" scope="row">
+        {{ $t('sdl.commonCode.label.order') }}
+        <span class="text-required">*</span>
+    </th>
+    <td>
+        <input type="number" min="0" max="99999" class="form-control" v-model.number="code.ord" v-validation="{ groupId: 'popup' }" />
+    </td>
+</tr>
+
+
+
+
    +
  • +

    값이 있고 없고 외에 별도 처리의 경우 함수와 에러메시지를 배열로 정의

    +
  • +
+
+
+
+
<tr>
+<th class="text-nowrap" scope="row">URL<span class="text-required">*</span></th>
+<td>
+<input
+    id="linkSiteUrl"
+    type="text"
+    class="form-control"
+    placeholder=""
+    v-model.trim="linkInfo.url"
+    v-validation="{
+        groupId: '',
+        valids: [
+            {
+                validFuncs: () => {
+                    return linkInfo.url.length <= 250;
+                },
+                errorMessage: 'test err',
+            },
+            {
+                validFuncs: () => {
+                    return !checkPatternUrl(linkInfo.url);
+                },
+                errorMessage: 'sdl.samsung.validate.url',
+            },
+        ],
+    }"
+    :errorMessage="'sdl.department.message.requiredValue'"
+/>
+</td>
+</tr>
+
+
+
+
    +
  • +

    저장/수정 버튼 클릭 시 directive로 지정한 tag들의 일괄 validation 처리(groupId가 있을 경우 넣고 없으면 안 넣어도 됨)

    +
  • +
+
+
+
+
if (SDLUtil.onSubmitValidation('popup')) return;
+
+
+
+ + + + + +
+ + +Bootstrap을 이용한 퍼블리싱이 아닐 경우 안 될 수 있음 +
+
+
+
+

4.5.12. 다국어 관련 날짜 포맷

+
+

SDL에서는 다국어 포맷을 Server로 부터 message properties를 통해 받아서 사용한다.

+
+
+
message.properties
+
+
 {
+  "ko_KR":{
+    "data-format.time.hm":"HH:mm",
+    "data-format.datetime.ymdhms":"YYYY-MM-DD HH:mm:ss",
+    "data-format.time.hms":"HH:mm:ss",
+    "data-format.date.ym":"YYYY-MM",
+    "data-format.date.yw":"YYYY-W",
+    "data-format.date.ymw":"YYYY-MM(W)",
+    "data-format.date.ymd":"YYYY-MM-DD"
+  },
+  "en_US":{
+    "data-format.time.hm":"HH:mm",
+    "data-format.datetime.ymdhms":"YYYY-MM-DD HH:mm:ss",
+    "data-format.time.hms":"HH:mm:ss",
+    "data-format.date.ym":"YYYY-MM",
+    "data-format.date.yw":"YYYY-W",
+    "data-format.date.ymw":"YYYY-MM(W)",
+    "data-format.date.ymd":"MM/DD/YYYY"
+  }
+}
+
+
+
+
moment() 를 이용한 현재 날짜의 언어별 포맷(SDLUtil.getMsgProp) 표기법
+
+
firstRegDatetime: moment().format(SDLUtil.getMsgProp('data-format.date.ymd')),
+
+
+
+
filter를 통해 template에서 사용가능
+
+
<!-- Vue2 -->
+<!-- default 'YYYY-MM-DD' -->
+<small class="float-right">{{ row.regDate || dateFormat }}</small>
+<!-- Vue3 -->
+<small class="float-right">{{ $filters.dateFormat(row.regDate) }}</small>
+
+<!-- Vue2 -->
+<!-- 포맷을 임의로 지정 -->
+<td class="text-center">{{ mail.sendDateTime || dateFormat($t('data-format.date.ymd')) }}</td>
+<!-- Vue3 -->
+<td class="text-center">{{ $filters.dateFormat(mail.sendDateTime, $t('data-format.date.ymd')) }}</td>
+
+
+
+
+

4.5.13. 화면별 권한처리

+
+

SDL에서는 역할관리에서 정의해놓은 역할별로 4개의 권한을 지정할 수 있다. +그리고 지정한 권한을 화면에서 버튼에 directive로 지정함으로써 show/hide 처리를 자동으로 해준다.

+
+
+
+front 01 03 +
+
+
+
    +
  • +

    vue 내에서 authorization directive를 통해 버튼 별 권한을 정의한다.(배열로 정의할 경우 여러개 중에 하나만 매칭되도 show처리 됨)

    +
    +
      +
    • +

      여러개의 권한을 둘 경우

      +
      +
      +
      <button
      +    type="button"
      +    class="btn btn-primary btn-sm"
      +    v-authorization="['UPDATE', 'EXECUTE']"
      +    @click="onApprWrite"
      +>
      +    {{ $t('sdl.approval.label.APPROVE') }}
      +</button>
      +
      +
      +
    • +
    • +

      하나의 권한을 둘 경우

      +
      +
      +
      <button type="button"
      +    class="btn btn-primary btn-sm"
      +    v-authorization="'UPDATE'"
      +    @click="onApprWrite"
      +>
      +    {{ $t('sdl.approval.label.APPROVE') }}
      +</button>
      +
      +
      +
    • +
    +
    +
  • +
  • +

    관리자일 경우는 directive 지정 상관 없이 무조건 show 처리된다.

    +
  • +
+
+
+
+

4.5.14. 목록 페이지와 상세 페이지 검색조건

+
+
목록 페이지
+
+

mixins 에 StoreParams 추가

+
+
+
+
 // 상단
+import StoreParams from '@/mixin/StoreParams';
+// script
+export default {
+mixins: [StoreParams],
+/*
+** 조회하는 method에 this.setStoreParameter(params); 호출하여 파라미터 저장
+*/
+ methods: {
+      getList(pageIndex = 1) {
+          const params = {
+            pageIndex,
+            pageSize: this.pageSize,
+            searchCondition: this.searchCondition,
+            searchKeyword: this.searchKeyword,
+          };
+        // 해당코드 추가
+        this.setStoreParameter(params);
+        // Loading bar 실행
+        SDLUtil.showLoadingBar(true);
+        // axios api 호출 로직 생략
+      }
+  }
+
+
+
+

created() 항목에 파라미터 복원 로직 추가

+
+
+
+
 created() {
+    const storeParams = this.getStoreParameter();
+    // 저장된 파라미터가 있으면 복원
+    if (storeParams) {
+      this.searchCondition = storeParams.searchCondition;
+      this.searchKeyword = storeParams.searchKeyword;
+      this.pageIndex = storeParams.pageIndex;
+      this.pageSize = storeParams.pageSize;
+
+      this.getList(this.pageIndex);
+    } else {
+      // 저장된 파라미터가 없이 들어왔을때 로직
+      this.getList();
+    }
+  },
+
+
+
+
+
상세 페이지
+
+

mixins에 StoreParams 추가 후 목록버튼에 goBackToList() 호출하여 목록으로 이동

+
+
+
+
 // 상단
+import StoreParams from '@/mixin/StoreParams';
+// script
+export default {
+  mixins: [StoreParams],
+  // ... 생략 ...
+  methods: {
+      // template 내에서는 goBackToList 바로 호출
+      // 로직상 필요한 경우도 호출
+      deleteDetail() {
+          // 해당페이지 상세 삭제후 목록이동
+          // 목록페이지로 돌아가기
+          this.goBackToList();
+      }
+  }
+
+
+
+
+
+

4.5.15. 기타 유용한 filter

+
+
cutString
+
+

원하는 길이만큼 문자열 컷 + '…​'

+
+
+
+
<!-- vue2 -->
+<td class="text-left">{{ grid.docTitle | cutString(100) }}</td>
+
+<!-- vue3 -->
+<td class="text-left">{{ $filters.cutString(grid.docTitle, 100) }}</td>
+
+
+
+
+
dateFormat
+
+

timestamp 형태의 값을 지정된 날짜 포맷으로 변경(default : 'YYYY-MM-DD')

+
+
+
+
<!-- vue2 -->
+<!-- default 'YYYY-MM-DD' -->
+<small class="float-right">{{ row.regDate | dateFormat }}</small>
+
+<!-- vue3 -->
+<small class="float-right">{{ $filters.dateFormat(row.regDate) }}</small>
+
+<!-- 포맷 임의 지정 -->
+<!-- vue2 -->
+<td class="text-center">{{ mail.sendDateTime | dateFormat($t('data-format.date.ymd')) }}</td>
+
+<!-- vue3 -->
+<td class="text-center">{{ $filters.dateFormat(mail.sendDateTime, $t('data-format.date.ymd')) }}</td>
+
+
+
+
+
numberFormat
+
+

숫자를 금액 단위로 표시(999,999,999)

+
+
+
+
<!-- vue2 -->
+ <td class="text-right">{{ log.hitCount | numberFormat }}</td>
+
+<!-- vue3 -->
+ <td class="text-right">{{ $filters.numberFormat(log.hitCount) }}</td>
+
+
+
+
+
nl2Br
+
+

\n 을 <br/> 로 변경(v-text 인 경우는 tag가 안 되므로 v-html 사용)

+
+
+
+
<!-- vue2 -->
+ <div
+  v-else
+  v-html="$options.filters.nl2br(post.postDetail)"
+  ref="postDetail"
+  class="form-control"
+  style="overflow:scroll; height:250px;max-height:250px"
+  contenteditable="true"
+></div>
+
+<!-- vue3 -->
+<div
+  v-else
+  v-html="$filters.nl2br(post.postDetail)"
+  ref="postDetail"
+  class="form-control"
+  style="overflow:scroll; height:250px;max-height:250px"
+  contenteditable="true"
+></div>
+
+
+
+
+
+
+
+
+

5. 공통기능

+
+
+

5.1. 사용자 관리

+
+

5.1.1. 사용자 관리

+
+
사용자 관리 화면
+
+

사용자 목록을 조회하며 시스템 사용 신청 대기자에 대한 사용 승인 및 취소가 가능하다.
+사용자 조회 목록의 Row를 클릭시 사용자 상세 정보 화면으로 이동할 수 있다.

+
+
+
+userMgmt +
+
+
+
기능별 설명
+
+
    +
  • +

    엑셀다운로드 : 조회 조건과 동일한 조회 결과 값을 담은 엑셀 파일을 다운로드 하는 기능.

    +
  • +
  • +

    삭제 : 선택된 사용자를 삭제 처리하는 기능.

    +
    +
      +
    • +

      삭제된 사용자의 row 데이터는 남아 있지만 사용자의 정보(EP ID, 사용자 이름 제외)는 모두 삭제 된다.

      +
    • +
    +
    +
  • +
  • +

    승인취소 : 선택된 사용자를 승인취소 처리하는 기능.

    +
  • +
  • +

    승인 : 선택된 사용자를 승인 처리하는 기능.

    +
    + +
    +
  • +
+
+
+
+
+
사용자 상세 조회 화면
+
+

사용자의 상세정보표시 및 조회한 사용자에 대한 역할 및 메뉴 추가/삭제가 가능하다.

+
+
+
+userInfo +
+
+
+
기능별 설명
+
+
    +
  • +

    사용자-역할 삭제 : 선택된 역할을 삭제

    +
  • +
  • +

    사용자-역할 추가 : 선택된 역할을 추가하는 기능. 역할 팝업이 호출 되며, 사용자에게 부여하고 싶은 역할을 선택하여 확인 버튼 클릭시 사용자-역할 Grid에 추가된다.

    +
  • +
  • +

    사용자-역할 저장 : 추가 또는 수정된 역할이 있을 경우, 저장 기능 수행.

    +
  • +
  • +

    사용자-메뉴 삭제 : 선택된 메뉴를 삭제

    +
  • +
  • +

    사용자-메뉴 추가 : 선택된 메뉴를 추가하는 기능. 메뉴 팝업이 호출 되며, 사용자에게 부여하고 싶은 메뉴을 선택하여 확인 버튼 클릭시 사용자-메뉴 Grid에 추가된다.

    +
    +
      +
    • +

      메뉴의 Page, API 권한 타입(READ, UPDATE, EXECUTE, DOWNLOAD) 중 필요한 권한을 check하여 등록한다.

      +
    • +
    • +

      권한 타입에 대한 설명은 메뉴관리 가이드를 참고한다. 메뉴 관리

      +
    • +
    +
    +
  • +
  • +

    사용자-메뉴 저장 : 추가 또는 수정된 메뉴이 있을 경우, 저장 기능 수행

    +
  • +
+
+
+
+
+
+

5.1.2. 부서관리(Knox)

+
+ + + + + +
+ + +해당 기능 사용 필요시 Knox API를 통해 연계 해야 한다. +
+
+
+
Table
+
+
    +
  • +

    부서(Knox) : TN_CF_DEPT_LDAP

    +
  • +
+
+
+
+
API
+
+
KnoxDepartmentController.java
+
    +
  • +

    부서의 데이터가 많으므로 목록에 1레벨만 불러오고 1레벨의 부서를 클릭시 "부서 관리(Knox)(하위 레벨 부서)" API를 실행하여 하위 부서를 조회한다.
    +부서 관리(사용자정의) 화면에서 부서매핑을 추가할 때도 사용한다.

    +
    +
      +
    1. +

      부서 관리(Knox)(1레벨 부서)
      +GET /admin/knox-department
      +Query ID : selectKnoxDepartmentList

      +
    2. +
    3. +

      부서 관리(Knox)(하위 레벨 부서)
      +GET /admin/knox-department/{upperDeptCode} +Query ID : selectKnoxDepartmentListByUpperCode

      +
    4. +
    +
    +
  • +
+
+
+
+
부서 Tree 구조
+
+

부서관리를 tree 구조로 보여지게 하기 위해, DB의 데이터가 parent 관계를 구성해야 한다. +LDAP 부서관리는 TN_CF_DEPT_LDAP 테이블의 dept_code(Child) 와 upper_dept_code(Parent) 관계로 tree 구조를 만들게 되는데 시스템의 최상위 dept code에는 ROOT를 입력해야 한다. +내부적으로 LDAP 부서 코드의 Root 밑의 1 level을 찾는 query가 ROOT 라는 코드를 찾도록 되어 있다.

+
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DEPT_CODEDEPT_NAMEDEPT_LEVELUPPER_DEPT_CODE

C00001

정보전략

1

ROOT

C000011

정보전략 부서1

2

C00001

C00002

마케팅

1

ROOT

C000021

마케팅 부서1

2

C00002

+
+

사용자정의 부서관리는 TN_CF_DEPT_SELF 테이블의 SELF_DEPT_CODE(Child) 와 UPPER_SELF_DEPT_CODE(Parent) +관계로 tree 구조를 이루며, 시스템의 최상위 SELF_DEPT_CODE는 DEPT를 입력해야 한다.

+
+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DEPT_CODEDEPT_NAMEDEPT_LEVELUPPER_DEPT_CODE

C00001

정보전략

1

DEPT

C000011

정보전략 부서1

2

C00001

C00002

마케팅

1

DEPT

C000021

마케팅 부서1

2

C00002

+
+
+
부서 목록
+
+

TN_CF_DEPT_LDAP 테이블에 저장되어 있는 부서정보를 트리형태로 보여준다.

+
+
+
+deptMgmt(Knox) +
+
+
+
+
+

5.1.3. 부서관리(사용자정의)

+
+
Table
+
+
    +
  • +

    부서(사용자정의) : TN_CF_DEPT_SELF

    +
  • +
  • +

    부서매핑 : TN_CF_DEPT_MAPPING

    +
  • +
+
+
+
+
API
+
+
CustomDepartmentController.java
+
    +
  1. +

    부서관리(사용자 정의) 목록 조회
    +GET /admin/department/custom
    +Query ID : selectCustomDepartmentList

    +
  2. +
  3. +

    부서 하위 레벨 조회
    +GET /admin/department/custom/dept-level-sub
    +Query ID : selectCustomDepartmentSubList

    +
    +
      +
    • +

      부서의 데이터가 많으므로 목록에 1레벨만 불러오고 1레벨의 부서를 클릭시 "부서 하위 레벨 조회" API를 실행하여 하위 부서를 조회한다.

      +
    • +
    +
    +
  4. +
  5. +

    부서 상세정보 조회
    +GET /admin/department/custom/dept-infos
    +Query ID : selectCustomDepartmentList, selectDeptMapping

    +
    +
      +
    • +

      부서 정보와 부서 매핑을 함께 불러온다.

      +
    • +
    +
    +
  6. +
  7. +

    부서 저장
    +POST /admin/department/custom
    +Query ID : insertCustomDepartment

    +
    +
      +
    • +

      등록된 부서가 있는지 중복체크하고 없으면 저장한다.

      +
    • +
    +
    +
  8. +
  9. +

    부서 수정
    +PUT /admin/department/custom/{selfDeptCode}
    +Query ID : updateCustomDepartment

    +
    +
      +
    • +

      부서명, 정렬순서, 설명만 수정 가능하다.

      +
    • +
    +
    +
  10. +
  11. +

    부서 이동
    +PUT /admin/department/custom
    +Query ID : updateDeptUpperDeptCode, updateDeptSequence

    +
    +
      +
    • +

      부서는 Drag & Drop 으로 원하는 곳에 이동하여 저장할 수 있다.

      +
    • +
    +
    +
  12. +
  13. +

    부서 매핑 조회
    +GET /admin/department/custom/mappings/{selfDeptCode}
    +Query ID : selectDeptMapping

    +
  14. +
  15. +

    부서 매핑 저장
    +POST /admin/department/custom/mappings/{selfDeptCode}
    +Query ID : insertDeptMapping

    +
    +
      +
    • +

      DB에 저장된 mapping과 화면에서 추가한 mapping을 비교하여 DB에 없는 mapping만 insert 한다.

      +
    • +
    +
    +
  16. +
  17. +

    부서 매핑 삭제
    +DELETE /admin/department/custom/mappings/{selfDeptCode}/{deptCode}
    +Query ID : deleteDeptMapping

    +
  18. +
+
+
+
+
부서관리(사용자정의) 기본 정보
+
+

사용자의 임의로 부서를 등록 및 해당되는 부서의 Knox 부서 매핑.

+
+
+
+deptList +
+
+
+
기본정보 필드 설명
+
+
    +
  • +

    부서코드 : 부서에 부여되는 코드(Unique)

    +
  • +
  • +

    부서명 : 부서명칭

    +
  • +
  • +

    부서레벨 : 부서에 부여되는 Level이며 최상위 Department 부서는 0 level이다.

    +
  • +
  • +

    정렬순서 : 같은 레벨 상 나오는 부서의 순서

    +
  • +
  • +

    설명 : 부서에 대한 설명

    +
  • +
+
+
+
+
기본정보 기능별 설명
+
+
    +
  • +

    삭제 : 부서목록의 부서를 선택 후 삭제 버튼 클릭시 선택된 부서 삭제(하위 부서 포함)

    +
  • +
  • +

    추가 : 추가하고자 하는 상위 부서를 선택 후 추가 버튼을 클릭시 Tree 구조에 New Document 생성

    +
  • +
  • +

    저장 : 추가로 생성된 또는 선택된 부서 정보를 저장

    +
  • +
+
+
+
+
+
부서 매핑
+
+

해당 부서에 대한 Knox 부서 매핑 정보.

+
+
+
부서 매핑 정보 필드 설명
+
+
    +
  • +

    부서코드 : Knox 부서에 부여되는 코드(Unique)

    +
  • +
  • +

    부서명 : Knox 부서명칭

    +
  • +
  • +

    삭제 : 삭제 실행 버튼

    +
  • +
+
+
+
+
기능별 설명
+
+
    +
  • +

    추가 : Knox 부서 Popup 호출

    +
  • +
  • +

    저장 : 호출된 Knox 부서 Popup 화면에서 추가하고자 하는 부서 매핑 정보에 저장.

    +
  • +
+
+
+
+
+
+

5.1.4. 장기미사용자

+
+
개요
+
+

일정 기간 시스템에 로그인(TN_CF_USER.RECENT_LOGIN_DATETIME)을 하지 않은 사용자는 장기 미사용자로 관리한다.

+
+
+
+
설명
+
+

장기 미사용자 관리 배치를 실행하면서 3개월간 로그인 하지 않은 사용자를 조회한다.

+
+
+
    +
  • +

    config.properties 파일에서 개월수(기본 3개월) 및 배치 실행시간을 변경 할 수 있다.

    +
  • +
+
+
+
+
batch.user.long-term.month=3
+batch.user.long-term-check.cron=0 10 00 * * ?
+
+
+
+
    +
  • +

    조회된 장기 미사용자에게 매핑된 메뉴, 업무그룹, 역할을 삭제하고 장기 미접속자로 상태를 변경한다. 사용자 관리 재직상태 컬럼(TN_CF_USER.EXPIRE_STATUS_CODE)에서 확인 할 수 있다.

    +
  • +
+
+
+
+
배치 설정
+
+
Spring 환경설정
+
+
    +
  • +

    QuartzConfig.java: Scheduler 등록

    +
  • +
  • +

    UserBatchConfig.java: 배치 Job, Trigger 등록

    +
  • +
  • +

    UserBatchExecutor.java: 서비스 호출

    +
  • +
+
+
+

SDL의 배치 작업은 Quartz 를 이용하여 구현하고 있다. 자세한 내용은 작업 스케쥴링Quartz Clustering with JDBC-JobStore 을 참고한다.

+
+
+
+
+
+

5.1.5. 사용권한 신청/승인

+
+
개요
+
+

사용자가 시스템에 가입시 시스템 사용 권한을 승인/승인 취소/삭제를 하여 시스템을 사용/미사용하게 한다.

+
+
+
+
Table
+
+
    +
  • +

    사용자 : TN_CF_USER

    +
  • +
+
+
+
+
API
+
+
UserController.java
+
    +
  1. +

    사용자 승인
    +PUT /auth/users/status/confirm
    +Query ID : updateUser

    +
  2. +
  3. +

    사용자 승인취소
    +PUT /auth/users/status/inactive
    +Query ID : updateUser

    +
  4. +
  5. +

    사용자 삭제
    +DELETE /auth/users
    +Query ID : updateUser

    +
    +
      +
    • +

      DeleteMapping이지만 DB 삭제가 아닌 ACTIVE_FLAG = 0, DELETED = 1 로 업데이트 한다.

      +
    • +
    +
    +
  6. +
+
+
+
+
+

5.1.6. 외부 사용자 관리

+
+
개요
+
+

ID/PW 회원가입을 통해서 등록한 사용자 목록을 조회하는 화면이다.

+
+
+
+
화면
+
+

외부 사용자 목록 조회 및 엑셀 다운로드 기능을 제공한다.

+
+
+ + + + + +
+ + +사용자 테이블(TN_CF_USER)의 외부 사용자 여부(EXTERNAL_FLAG) 정보를 통해서 확인할 수 있다.
+사용자 관리 메뉴에서 관리자 승인이 된 사용자 정보만 조회할 수 있다. +
+
+
+
+externalUserList +
+
+
+
기능별 설명
+
+
    +
  • +

    엑셀다운로드 : 조회 조건과 동일한 조회 결과 값을 담은 엑셀 파일을 다운로드 하는 기능.

    +
  • +
+
+
+
+
+
+

5.1.7. 권한관리 기간 배치

+
+
만료 권한 삭제
+
+

배치를 실행하여 사용자 및 역할에 매핑된 권한 중 만료된 권한을 삭제한다.

+
+
+
config.properties
+
+
batch.user.auth-expired.cron=0 10 02 * * ?
+
+
+
+
+
권한만료 예정 안내 메일 발송
+
+

배치를 실행하여 권한만료 예정인 사용자를 대상으로 안내메일을 발송한다.

+
+
+
config.properties
+
+
batch.user.auth-expired-mailing.cron=0 30 02 * * ?
+
+
+
+
+
Spring 환경설정
+
+
    +
  • +

    QuartzConfig.java: Scheduler 등록

    +
  • +
  • +

    UserBatchConfig.java: 배치 Job, Trigger 등록

    +
  • +
  • +

    UserBatchExecutor.java: 서비스 호출

    +
  • +
+
+
+
+
+

5.1.8. 역할 관리

+
+
개요
+
+

역할을 등록하고, 해당 역할에 사용자 및 업무그룹을 Mapping 한다.

+
+
+
+
UI Design & Function
+
+
역할 목록(RoleList.vue)
+
+

역할 등록, 수정 및 조회.
+조회된 목록의 사용자/업무그룹 컬럼을 클릭하여 역할-사용자, 역할-업무그룹 상세 화면으로 이동한다.

+
+
+
+역할 관리 +
+
+
+
    +
  • +

    기능 설명

    +
    +
      +
    1. +

      역할 목록 조회

      +
    2. +
    3. +

      역할 등록 : 역할 정보를 입력하는 Popup이 호출 되며 정보 기입 후 저장 버튼을 클릭하여 등록한다.

      +
    4. +
    5. +

      역할 수정 : 선택된(checkbox : true) 역할 정보를 수정하는 Popup이 호출 되며, 정보 기입 후 저장 버튼을 클릭하여 수정한다.

      +
    6. +
    7. +

      역할 삭제 : 선택된(checkbox : true) 역할 목록을 삭제한다.

      +
    8. +
    9. +

      엑셀 다운로드 : 조회 조건과 동일한 역할 목록을 엑셀 파일로 다운로드 한다.

      +
    10. +
    +
    +
  • +
+
+
+
+
역할-사용자 관리(RoleUserInfo.vue)
+
+

선택한 역할에 대한 사용자 추가.
+역할에 대한 사용자가 많을 경우를 대비하여 상단에 페이징 처리 되는 Grid를, 하단에는 사용자를 추가하는 Grid로 나누어 화면을 구성된다.

+
+
+
+역할-사용자 관리 +
+
+
+
    +
  • +

    기능 설명

    +
    +
      +
    1. +

      역할-사용자 목록 조회

      +
    2. +
    3. +

      역할-사용자 등록 : 추가 버튼 클릭시 사용자 조회 Popup이 호출 되며 사용자 선택 후 추가된 하단의 사용자 목록에서 저장한다.

      +
    4. +
    5. +

      역할-사용자 수정 : 기등록된 상단 사용자 목록에서 역할 적용 기간을 수정한다.

      +
    6. +
    7. +

      역할-사용자 삭제 : 기등록된 상단 사용자 목록에서 선택된(checkbox : true) 사용자 목록을 삭제한다.

      +
    8. +
    +
    +
  • +
+
+
+
+
역할-업무그룹 관리(RoleWorkgroupInfo.vue)
+
+

선택한 역할에 대한 업무그룹 추가.

+
+
+
+역할-업무그룹 관리 +
+
+
+
    +
  • +

    기능 설명

    +
    +
      +
    1. +

      역할-업무그룹 목록 조회

      +
    2. +
    3. +

      역할-업무그룹 등록 : 추가 버튼 클릭시 업무그룹 조회 Popup이 호출 되며 업무그룹을 선택 하여 등록 할 수 있다.

      +
    4. +
    5. +

      역할-업무그룹 삭제 : 기등록된 상단 업무그룹 목록에서 선택된(checkbox : true) 업무그룹 목록을 삭제한다.

      +
    6. +
    +
    +
  • +
+
+
+
+
+
API & Service
+
+
API
+
+
    +
  • +

    API : RoleController.java

    +
    +
      +
    1. +

      역할 목록 조회 : GET /roles-with-paging

      +
    2. +
    3. +

      역할 상세정보 조회 : GET /roles/{roleId}

      +
    4. +
    5. +

      역할 등록 : POST /roles

      +
    6. +
    7. +

      역할 수정 : PUT /roles

      +
    8. +
    9. +

      역할 삭제 : DELETE /roles

      +
    10. +
    11. +

      역할-사용자 목록 조회 : GET /roles/{roleId}/users-with-paging

      +
    12. +
    13. +

      역할-사용자 등록 : POST /roles/{roleId}/users

      +
    14. +
    15. +

      역할-사용자 수정 : PUT /roles/{roleId}/users

      +
    16. +
    17. +

      역할-사용자 삭제 : DELETE /roles/{roleId}/users

      +
    18. +
    19. +

      역할-업무그룹 목록 조회 : GET /roles/{roleId}/workgroups

      +
    20. +
    21. +

      역할-업무그룹 등록 : POST /roles/{roleId}/workgroups

      +
    22. +
    23. +

      역할-업무그룹 삭제 : DELETE /roles/{roleId}/workgroups

      +
    24. +
    +
    +
  • +
+
+
+
+
+
Entity Table & SQL
+
+
Entity Table
+
+
    +
  • +

    TN_CF_ROLE : 역할

    +
  • +
  • +

    TN_CF_USER_ROLE : 역할-사용자 맵핑

    +
  • +
  • +

    TN_CF_WORKGROUP_ROLE : 역할-업무그룹 맵핑

    +
  • +
+
+
+
+
SQL
+
+
    +
  1. +

    역할 목록 조회

    +
  2. +
+
+
+
+
<select id="selectRolePagingList" parameterType="java.util.HashMap" resultMap="roleResult">
+	SELECT *
+	FROM   (SELECT
+				ROW_NUMBER() OVER (	<if test="@com.samsung.SdlComparator@isNotEmptyForDynamicSql(orderBy)">ORDER BY ${orderBy}</if> ) ROWNUM,
+    --생략--
+
+
+
+
    +
  1. +

    역할 상세정보 조회

    +
  2. +
+
+
+
+
<select id="selectRole" parameterType="java.util.HashMap" resultMap="roleResult">
+	SELECT <include refid="columnsRole" />
+	FROM   <include refid="tableRole" />
+	--생략--
+
+
+
+
    +
  1. +

    역할 등록

    +
  2. +
+
+
+
+
<insert id="insertRole" parameterType="role">
+	INSERT INTO <include refid="tableRole" />
+	(<include refid="columnsRole" />)
+	--생략--
+
+
+
+
    +
  1. +

    역할 수정

    +
  2. +
+
+
+
+
<update id="updateRole"  parameterType="role" flushCache="true" >
+	UPDATE <include refid="tableRole" />
+	SET	   ROLE_NAME = #{roleName},
+	--생략--
+
+
+
+
    +
  1. +

    역할 삭제

    +
  2. +
+
+
+
+
<update id="deleteRole" parameterType="role" flushCache="true" >
+	UPDATE <include refid="tableRole" />
+	SET    DELETE_YN = 1,
+	--생략--
+
+
+
+
    +
  1. +

    역할-사용자 목록 조회

    +
  2. +
+
+
+
+
<select id="selectRoleUserPagingList" parameterType="java.util.HashMap" resultMap="roleUserResult">
+	SELECT *
+	FROM   (SELECT
+				ROW_NUMBER() OVER (	<if test="@com.samsung.SdlComparator@isNotEmptyForDynamicSql(orderBy)">ORDER BY ${orderBy}</if> ) ROWNUM,
+	--생략--
+
+
+
+
    +
  1. +

    역할-사용자 등록

    +
  2. +
+
+
+
+
<insert id="insertUserRole" parameterType="userRole">
+	INSERT INTO <include refid="tableUserRole" />
+		(ROLE_ID, USER_ID, FROM_DATE, THRU_DATE,
+	--생략--
+
+
+
+
    +
  1. +

    역할-사용자 수정

    +
  2. +
+
+
+
+
<update id="updateUserRole" parameterType="userRole">
+	UPDATE <include refid="tableUserRole" />
+	SET    THRU_DATE = #{thruDate},
+	--생략--
+
+
+
+
    +
  1. +

    역할-사용자 삭제

    +
  2. +
+
+
+
+
<delete id="deleteUserRole" parameterType="java.util.HashMap">
+	DELETE FROM <include refid="tableUserRole" />
+	WHERE  USER_ID = #{userId}
+	--생략--
+
+
+
+
    +
  1. +

    역할-업무그룹 목록 조회

    +
  2. +
+
+
+
+
<select id="selectRoleWorkgroupList" parameterType="java.util.HashMap" resultMap="workgroup.workgroupResult">
+	SELECT W.WORKGROUP_ID, W.WORKGROUP_NAME, W.DESCRIPTION,
+	       W.LABEL
+	--생략--
+
+
+
+
    +
  1. +

    역할-업무그룹 등록

    +
  2. +
+
+
+
+
<insert id="insertWorkgroupRole" parameterType="workgroupRole">
+	INSERT INTO <include refid="tableWorkgroupRole" />
+		(WORKGROUP_ID, USER_ROLE_ID, FROM_DATE, THRU_DATE, ROLE_ID, USER_ID,
+	--생략--
+
+
+
+
    +
  1. +

    역할-업무그룹 삭제

    +
  2. +
+
+
+
+
<delete id="deleteWorkgroupRole" parameterType="java.util.HashMap">
+	DELETE FROM <include refid="tableWorkgroupRole" />
+	WHERE  WORKGROUP_ID = #{workgroupId}
+	--생략--
+
+
+
+
+
+
+

5.1.9. 업무 그룹 관리

+
+
개요
+
+

메일 그룹 및 맵핑 정보 관리 기능 제공.

+
+
+
+
UI Design & Function
+
+
업무 그룹 목록(WorkgroupList.vue)
+
+

업무별로 업무그룹을 만들고 해당 업무그룹에서 사용가능한 메뉴를 선택, 해당 메뉴 사용 가능한 Role 또는 User를 추가한다.

+
+
+
+업무 그룹 목록 +
+
+
+
    +
  • +

    기능 설명

    +
    +
      +
    1. +

      업무그룹 목록 조회

      +
    2. +
    3. +

      업무그룹 상세정보 조회

      +
    4. +
    5. +

      업무그룹 등록 : 업무그룹 정보를 입력하는 Popup이 호출 되며, 정보 기입 후 저장 버튼을 클릭하여 등록한다.

      +
    6. +
    7. +

      업무그룹 수정 : 선택된(checkbox : true) 업무그룹 정보를 수정하는 Popup이 호출 되며 역할 정보 기입 후 저장 버튼을 클릭하여 수정한다.

      +
    8. +
    9. +

      업무그룹 삭제 : 선택된(checkbox : true) 역할 목록을 삭제한다.

      +
    10. +
    +
    +
  • +
+
+
+
+
업무그룹-메뉴(WorkgroupMenuInfo.vue)
+
+

업무그룹에서 사용 가능한 메뉴를 관리 하는 화면.
+메뉴 등록 및 삭제가 가능하며 메뉴별 권한 설정이 가능하다.

+
+
+
+업무그룹-메뉴 +
+
+
+
    +
  • +

    기능 설명

    +
    +
      +
    1. +

      업무그룹-메뉴 목록 조회

      +
    2. +
    3. +

      업무그룹-메뉴 등록 : 메뉴를 선택하는 Popup이 호출 되며 선택 후 저장 버튼을 클릭하여 등록한다.

      +
    4. +
    5. +

      업무그룹-메뉴 수정 : 권한을 선택(checkbox : true)하여 해당 메뉴에 대한 권한을 수정한다.

      +
    6. +
    7. +

      업무그룹-메뉴 삭제 : 선택된(checkbox : true) 메뉴 목록을 삭제한다.

      +
    8. +
    +
    +
  • +
+
+
+
+
업무그룹-역할(WorkgroupMenuInfo.vue)
+
+

선택한 업무그룹에 역할 또는 사용자를 관리하는 화면.
+역할과 사용자를 추가 또는 삭제할 수 있다.

+
+
+
+업무그룹-역할 +
+
+
+
    +
  • +

    기능 설명

    +
    +
      +
    1. +

      업무그룹-역할 목록 조회

      +
    2. +
    3. +

      업무그룹-역할 등록

      +
      +
        +
      1. +

        사용자 등록 : 사용자 추가 버튼 클릭 시 사용자를 선택할 수 있는 Popup이 호출 되며 선택 후 저장 버튼을 클릭하여 등록 한다.

        +
      2. +
      3. +

        역할 등록 : 역할 추가 버튼 클릭 시 역할을 선택할 수 있는 Popup이 호출 되며 선택 후 저장 버튼을 클릭하여 등록 한다.

        +
      4. +
      +
      +
    4. +
    5. +

      업무그룹-역할 수정 : 등록된 사용자의 업무그룹 만료일을 수정 한다.

      +
    6. +
    7. +

      업무그룹-역할 삭제 : 선택된(checkbox : true) 역할(사용자) 목록을 삭제한다.

      +
    8. +
    +
    +
  • +
+
+
+
+
+
API & Service
+
+
API
+
+
    +
  • +

    API : WorkgroupController.java

    +
    +
      +
    1. +

      업무그룹 목록 조회 : GET /auth/workgroups-with-paging

      +
    2. +
    3. +

      업무그룹 상세정보 조회 : GET /auth/workgroups/{workgroupId}

      +
    4. +
    5. +

      업무그룹 등록 : POST /auth/workgroups

      +
    6. +
    7. +

      업무그룹 수정 : PUT /auth/workgroups/{workgroupId}

      +
    8. +
    9. +

      업무그룹 삭제 : DELETE /auth/workgroups

      +
    10. +
    11. +

      업무그룹-메뉴 목록 조회 : GET /auth/workgroups/{workgroupId}/menus

      +
    12. +
    13. +

      업무그룹-메뉴 등록 : POST /auth/workgroups/{workgroupId}/menus

      +
    14. +
    15. +

      업무그룹-메뉴 수정 : PUT /auth/workgroups/{workgroupId}/menus

      +
    16. +
    17. +

      업무그룹-메뉴 삭제 : DELETE /auth/workgroups/{workgroupId}/menus

      +
    18. +
    19. +

      업무그룹-역할 목록 조회 : GET /auth/workgroups/{workgroupId}/roles

      +
    20. +
    21. +

      업무그룹-역할 등록 : POST /auth/workgroups/{workgroupId}/roles

      +
    22. +
    23. +

      업무그룹-역할 수정 : PUT /auth/workgroups/{workgroupId}/roles

      +
    24. +
    25. +

      업무그룹-역할 삭제 : DELETE /auth/workgroups/{workgroupId}/roles

      +
    26. +
    +
    +
  • +
  • +

    Service : WorkgroupServiceImpl.java

    +
    +
      +
    1. +

      업무그룹-메뉴 목록 조회.
      +메뉴의 전체 경로 셋팅 및 권한별 row 데이터를 메뉴 row 데이터로 가공한다.

      +
    2. +
    +
    +
  • +
+
+
+
+
@Override
+public List<WorkgroupMenuDto> getWorkgroupMenuList(String workgroupId) {
+
+    List<WorkgroupMenuDto> workgroupMenuDtoList = new ArrayList<>();
+    List<String> authorizationIdList;
+    String beforeMenuId = "";
+    -- 생략 --
+
+
+
+
+
+
Entity Table & SQL
+
+
Entity Table
+
+
    +
  • +

    TN_CF_WORKGROUP : 업무그룹

    +
  • +
  • +

    TN_CF_WORK_AUTHORIZATION : 업무그룹 메뉴 권한 맵핑

    +
  • +
  • +

    TN_CF_WORKGROUP_ROLE : 업무그룹 역할 맵핑

    +
  • +
+
+
+
+
SQL
+
+
    +
  1. +

    업무그룹 목록 조회

    +
  2. +
+
+
+
+
<select id="selectWorkgroupPagingList" parameterType="java.util.HashMap" resultMap="workgroupResult">
+    SELECT *
+    FROM   (SELECT ROW_NUMBER() OVER (	<if test="@com.samsung.SdlComparator@isNotEmptyForDynamicSql(orderBy)">ORDER BY ${orderBy}</if> ) ROWNUM,
+                   <include refid="columnsWorkgroup" />
+	--생략--
+
+
+
+
    +
  1. +

    업무그룹 상세정보 조회

    +
  2. +
+
+
+
+
<select id="selectWorkgroup" parameterType="java.util.HashMap" resultMap="workgroupResult">
+    SELECT <include refid="columnsWorkgroup" />
+    FROM   <include refid="tableWorkgroup" />
+    <include refid="conditionWorkgroup" />
+</select>
+
+
+
+
    +
  1. +

    업무그룹 등록

    +
  2. +
+
+
+
+
<insert id="insertWorkgroup" parameterType="workgroup">
+    INSERT INTO <include refid="tableWorkgroup" />
+        (<include refid="columnsWorkgroup"/>)
+    --생략--
+
+
+
+
    +
  1. +

    업무그룹 수정

    +
  2. +
+
+
+
+
<update id="updateWorkgroup" parameterType="workGroup">
+    UPDATE <include refid="tableWorkgroup" />
+    SET    WORKGROUP_NAME = #{workgroupName},
+    --생략--
+
+
+
+
    +
  1. +

    업무그룹 삭제

    +
  2. +
+
+
+
+
<delete id="deleteWorkgroup" parameterType="java.util.HashMap">
+    UPDATE <include refid="tableWorkgroup" />
+    SET    DELETE_YN = 1,
+    --생략--
+
+
+
+
    +
  1. +

    업무그룹-메뉴 목록 조회

    +
  2. +
+
+
+
+
<select id="selectWorkgroupMenuList" parameterType="java.util.HashMap" resultMap="workgroupMenuResult">
+    WITH TREE_TABLE AS (SELECT SR.UPPER_SYS_RESOURCE_ID, M.MENU_ID,
+                               CONVERT(NVARCHAR(100), M.LABEL) LABEL,
+                               CONVERT(NVARCHAR(100), M.LABEL_JSON) LABEL_JSON,
+    --생략--
+
+
+
+
    +
  1. +

    업무그룹-메뉴 등록

    +
  2. +
+
+
+
+
<insert id="insertWorkgroupMenu" parameterType="workgroupMenu">
+    INSERT INTO <include refid="tableWorkAuthorization" />
+        (WORKGROUP_ID, SYS_RESOURCE_ID, AUTHORIZATION_ID,
+    --생략--
+
+
+
+
    +
  1. +

    업무그룹-메뉴 삭제

    +
  2. +
+
+
+
+
<delete id="deleteWorkgroupMenu" parameterType="workgroupMenu">
+    DELETE FROM <include refid="tableWorkAuthorization" />
+    WHERE  SYS_RESOURCE_ID = #{sysResourceId}
+    --생략--
+
+
+
+
    +
  1. +

    업무그룹-역할 목록 조회

    +
  2. +
+
+
+
+
<select id="selectRoleWorkgroupList" parameterType="java.util.HashMap" resultMap="workgroup.workgroupResult">
+    SELECT W.WORKGROUP_ID, W.WORKGROUP_NAME, W.DESCRIPTION,
+           W.LABEL
+    --생략--
+
+
+
+
    +
  1. +

    업무그룹-역할 등록

    +
  2. +
+
+
+
+
<insert id="insertWorkgroupRole" parameterType="workgroupRole">
+    INSERT INTO <include refid="tableWorkgroupRole" />
+        (WORKGROUP_ID, USER_ROLE_ID, FROM_DATE, THRU_DATE, ROLE_ID, USER_ID,
+    --생략--
+
+
+
+
    +
  1. +

    업무그룹-역할 수정

    +
  2. +
+
+
+
+
<update id="updateWorkgroupRole" parameterType="workgroupRole">
+    UPDATE <include refid="tableWorkgroupRole" />
+    SET    THRU_DATE = #{thruDate},
+    --생략--
+
+
+
+
    +
  1. +

    업무그룹-역할 삭제

    +
  2. +
+
+
+
+
<delete id="deleteWorkgroupRole" parameterType="java.util.HashMap">
+    DELETE FROM <include refid="tableWorkgroupRole" />
+    WHERE  WORKGROUP_ID = #{workgroupId}
+    --생략--
+
+
+
+
+
+
+

5.1.10. 메뉴 관리

+
+
개요
+
+

메뉴와 하위 Page 및 API 목록을 관리하는 화면이다.

+
+
+ + + + + +
+ + +신규 메뉴를 등록 하거나 메뉴정보를 수정 또는 삭제할 경우 콜백함수를 통해서 메뉴관련 된 모든 캐시가 초기화 된다. +
+
+
+
+
UI Design & Function
+
+
메뉴 관리
+
+

메뉴를 등록, 수정 및 관리 할 수 있는 메뉴 트리를 제공한다.

+
+
+
+menuManagement 01 +
+
+
+
    +
  • +

    기능 설명

    +
    +
      +
    1. +

      메뉴 트리 목록 조회

      +
    2. +
    3. +

      메뉴 상세정보 조회 : 트리에서 메뉴 선택시 메뉴 상세정보를 조회 한다.

      +
    4. +
    5. +

      메뉴 등록 : 현재 선택된 메뉴 하위에 새로운 메뉴를 추가 한다.

      +
    6. +
    7. +

      메뉴 수정 / 삭제 : 현재 선택된 메뉴를 수정 또는 삭제 한다.

      +
    8. +
    9. +

      메뉴 이동 : 트리에서 Drag&Drop 기능으로 선택한 메뉴를 이동할 수 있다.

      +
    10. +
    +
    +
  • +
+
+
+
+
페이지 관리
+
+
+menuManagement 02 +
+
+
+
    +
  • +

    기능 설명

    +
    +
      +
    1. +

      Page 목록 조회 : 메뉴 트리에서 메뉴 선택 시 해당 메뉴에 맵핑되어 있는 페이지 목록 조회.

      +
    2. +
    3. +

      Page 등록, 수정, 삭제

      +
    4. +
    +
    +
  • +
+
+
+ + + + + +
+ + +권한 타입은 READ, UPDATE, EXECUTE, DOWNLOAD 로 구성되어 있으며 사용자가 가지고 있는 해당 메뉴에 대한 권한 종류로 UI의 권한 제어를 하고 있다.
+자세한 내용은 화면별 권한처리 매뉴얼을 참고한다. +
+
+
+
+
API 관리
+
+
+menuManagement 03 +
+
+
+
    +
  • +

    기능 설명

    +
    +
      +
    1. +

      API 목록 조회

      +
    2. +
    3. +

      API 등록

      +
    4. +
    5. +

      API 수정

      +
    6. +
    7. +

      API 삭제

      +
    8. +
    +
    +
  • +
+
+
+
+
SDL 게시판 Page, API 프리셋 추가
+
+
+menuManagement 04 +
+
+
+
    +
  • +

    기능 설명

    +
    +
      +
    1. +

      게시판 선택 및 Page, API 자동 추가

      +
    2. +
    +
    +
  • +
+
+
+
+
+
API & Service
+
+
API
+
+

MenuController.java
+Menu와 해당 메뉴에 Mapping 되어 있는 Page, API의 등록, 수정, 삭제 및 조회하는 API를 포함하고 있다.

+
+
+
    +
  • +

    주요 기능 API 목록

    +
    +
      +
    1. +

      메뉴 트리 목록 조회 : GET /auth/menus/menu-mgmt/level

      +
    2. +
    3. +

      메뉴 상세정보 조회 : GET /auth/menus/{menuId}

      +
    4. +
    5. +

      메뉴 등록 : POST /auth/menus

      +
    6. +
    7. +

      메뉴 수정 : PUT /auth/menus

      +
    8. +
    9. +

      메뉴 삭제 : DELETE /auth/menus/{menuId}

      +
    10. +
    11. +

      메뉴 이동 : PUT /auth/menus/move-menu

      +
    12. +
    13. +

      Page 목록 조회 : GET /auth/menus/{menuId}/pages

      +
    14. +
    15. +

      Page 등록 : POST /auth/menus/{menuId}/pages

      +
    16. +
    17. +

      Page 수정 : PUT /auth/menus/{menuId}/pages

      +
    18. +
    19. +

      Page 삭제 : DELETE /auth/menus/{menuId}/pages

      +
    20. +
    21. +

      API 목록 조회 : GET /auth/menus/{menuId}/apis

      +
    22. +
    23. +

      API 등록 : POST /auth/menus/{menuId}/apis

      +
    24. +
    25. +

      API 수정 : PUT /auth/menus/{menuId}/apis

      +
    26. +
    27. +

      API 삭제 : DELETE /auth/menus/{menuId}/apis

      +
    28. +
    +
    +
  • +
+
+
+
+
+
Entity Table & SQL
+
+
Entity Table
+
+
    +
  • +

    TN_CF_SYS_RESOURCE : 리소스

    +
  • +
  • +

    TN_CF_SYS_RESOURCE_MAPP : 리소스 맵핑

    +
  • +
  • +

    TN_CF_MENU : 메뉴

    +
  • +
  • +

    TN_CF_PAGE : Page

    +
  • +
  • +

    TN_CF_API : API

    +
  • +
+
+
+
+
SQL
+
+
    +
  1. +

    메뉴 트리 목록 조회

    +
  2. +
+
+
+
+
<select id="selectMenuLevel" parameterType="java.util.HashMap" resultMap="menuTreeInfo">
+    SELECT *
+    FROM (
+            <include refid="conditionMenuLevel"/>
+	--생략--
+
+
+
+
    +
  1. +

    메뉴 상세정보 조회

    +
  2. +
+
+
+
+
<select id="selectMenu" parameterType="java.util.HashMap" resultMap="menuResult">
+    SELECT M.MENU_ID, M.LABEL,
+           M.MENU_LEVEL, M.MENU_SEQUENCE,
+	--생략--
+
+
+
+
    +
  1. +

    메뉴 등록

    +
  2. +
+
+
+
+
<insert id="insertMenu" parameterType="menu">
+    INSERT INTO <include refid="tableMenu" />
+        (MENU_ID, LABEL,
+	--생략--
+
+
+
+
    +
  1. +

    메뉴 수정

    +
  2. +
+
+
+
+
<update id="updateMenu" parameterType="menu">
+    UPDATE <include refid="tableMenu" />
+    SET    LABEL = #{label},
+	--생략--
+
+
+
+
    +
  1. +

    메뉴 삭제

    +
  2. +
+
+
+
+
<delete id="deleteMenu" parameterType="java.util.HashMap">
+    UPDATE <include refid="tableMenu" />
+    SET    USE_YN = 0,
+           DELETE_YN = 1
+	--생략--
+
+
+
+
    +
  1. +

    메뉴 이동

    +
  2. +
+
+
+
+
<update id="updateParentMenuId" parameterType="java.util.HashMap">
+    UPDATE <include refid="sysResource.tableSysResource" />
+    SET    UPPER_SYS_RESOURCE_ID = #{parentMenuId}
+	--생략--
+
+
+
+
+
<update id="updateMenuSequence" parameterType="java.util.HashMap">
+    UPDATE <include refid="tableMenu" />
+    SET    MENU_SEQUENCE = #{moveToMenuSequence}
+	--생략--
+
+
+
+
    +
  1. +

    Page 목록 조회

    +
  2. +
+
+
+
+
<select id="selectPageList" parameterType="java.util.HashMap" resultMap="pageResult">
+    SELECT P.PAGE_ID, SA.AUTHORIZATION_ID, P.PAGE_NAME,
+           P.PAGE_PATH, P.COMPONENT_NAME, P.DEFAULTED, P.USE_YN
+	--생략--
+
+
+
+
    +
  1. +

    Page 등록

    +
  2. +
+
+
+
+
<insert id="insertPage" parameterType="page">
+    INSERT INTO <include refid="tablePage" />
+        (PAGE_ID, PAGE_NAME, PAGE_PATH,
+	--생략--
+
+
+
+
    +
  1. +

    Page 수정

    +
  2. +
+
+
+
+
    <update id="updatePage" parameterType="page">
+    UPDATE <include refid="tablePage" />
+    SET    PAGE_NAME = #{pageName},
+	--생략--
+
+
+
+
    +
  1. +

    Page 삭제

    +
  2. +
+
+
+
+
<delete id="deletePage" parameterType="java.util.HashMap">
+    UPDATE <include refid="tablePage" />
+    SET    USE_YN = 0,
+	--생략--
+
+
+
+
    +
  1. +

    API 목록 조회

    +
  2. +
+
+
+
+
<select id="selectApiList" parameterType="java.util.HashMap" resultMap="apiResult">
+    SELECT A.API_ID, SA.AUTHORIZATION_ID, A.API_NAME,
+           A.API_URL, A.HTTP_METHOD, A.SERVER_ID, A.USE_YN
+	--생략--
+
+
+
+
    +
  1. +

    API 등록

    +
  2. +
+
+
+
+
<insert id="insertApi" parameterType="api">
+    INSERT INTO <include refid="tableApi" />
+        (API_ID, API_NAME, API_URL, API_PATH, API_PARAMETERS, HTTP_METHOD, SERVER_ID, USE_YN, DELETE_YN)
+	--생략--
+
+
+
+
    +
  1. +

    API 수정

    +
  2. +
+
+
+
+
<update id="updateApi" parameterType="api">
+    UPDATE <include refid="tableApi" />
+    SET    API_NAME = #{apiName},
+	--생략--
+
+
+
+
    +
  1. +

    API 삭제

    +
  2. +
+
+
+
+
<delete id="deleteApi" parameterType="java.util.HashMap">
+    UPDATE <include refid="tableApi" />
+    SET    USE_YN = 0,
+	--생략--
+
+
+
+
+
+
+

5.1.11. My Menu 관리

+
+
My Menu 관리
+
+

사용자가 자주 사용하는 메뉴를 등록하여 사용자 임의로 만들어서 빠른 이동을 목적으로 하는 Menu 목록.

+
+
+

TopMenu - My Menu 에 구현되어 있다.

+
+
+
+MyMenu +
+
+
+
기능별 설명
+
+
+MyMenu add +
+
+
+

메뉴의 BreadCrumb 영역 내 메뉴명 우측 별 모양을 클릭하여 MyMenu에 등록 또는 해제

+
+
+ + + + + +
+ + +SideMenu(Right Side) 이용 시 MainOffsider.vue 내 'QuickMenu' 에 구현되어 있다. +
+
+
+
+
+
+

5.1.12. Sitemap

+
+
개요
+
+

로그인한 사용자의 권한에 맞는 전체메뉴를 볼 수 있다.

+
+
+

TopMenu - 사이트맵 부분에 구현되어 있다.

+
+
+
+
화면
+
+

권한에 따른 전체 메뉴 목록을 나열한 화면으로 메뉴 이동 가능

+
+
+
+siteMap +
+
+
+ + + + + +
+ + +SideMenu(Right Side) 이용 시 MainOffsider.vue 내 'sitemap' 부분에 구현되어 있다. +
+
+
+
+
+

5.1.13. 로그인

+
+

SDL은 Knox EpTray, ID/PW, ADFS 로그인을 지원한다.

+
+
+
EpTray
+
+

Knox EpTray 로그인은 Chrome, Edge 등 멀티 브라우저에서 가능하다. +사용자가 Knox에 로그인을 한 후 시스템에 접속을 하게 되면 EpTray값을 이용해 시스템 로그인을 한다.

+
+
+ + + + + +
+ + +Knox EpTray SSO 를 위한 연계 신청이 필요하다. (스테이지/운영)
+자세한 내용 안내 및 문의는 Knox Support (매뉴얼 > KnoxPortal NewEpTray 연계 가이드) 를 참조한다. +
+
+
+
EpTray 적용
+
+

연계신청 과정에서 생성되는 rsaprivkey8.pem 파일을
+config.properties의 login.sso.knox-tray-private-key-path 경로에 넣어준다.

+
+
+ + + + + +
+ + +rsaprivkey8.pem 파일이 없는 경우 아래와 같은 에러가 발생하므로 주의한다. +
+
+
+
+Can not load new EpTray Private Key.png +
+
+
+
+
LoginPage.vue
+
+

UI 에서 사용자 정보가 없을 경우 LoginPage.vue 페이지로 이동하게 되고 인증 절차를 시작한다.

+
+
+

EpTray 연계를 위한 메서드

+
+
+
+
async loginByNewEpTray() {
+  let loginResult = false;
+  SDLUtil.showLoadingBar(true);
+  await this.socConnect().then((data) => {
+    this.newEpTrayKey = data;
+    this.websocket.close();
+  }).catch((err) => {
+    SDLUtil.alert(err);
+  });
+  SDLUtil.showLoadingBar(false);
+
+  if (this.newEpTrayKey) {
+    if (this.newEpTrayKey.result === 'success') {
+      const { userInfo, key } = this.newEpTrayKey;
+      loginResult = await this.loginNewEpTray({ userInfo, key });
+    } else {
+      SDLUtil.alert(`sdl.epTray.error.${this.newEpTrayKey.errorCode}`);
+    }
+  }
+
+  if (!loginResult) {
+    if (this.$route.query.token) {
+      loginService.setToken(this.$route.query.token);
+      document.location = `${SDLUtil.WEB_CONTEXT_PATH}/`;
+    }
+  }
+  this.afterLoginProcess(loginResult);
+},
+
+
+
+
+
socConnect() {
+  return new Promise(((resolve, reject) => {
+    const server = new WebSocket('wss://localhost:29283'); (1)
+    this.websocket = server;
+    server.onopen = () => {
+      server.send('{"rqtype":"getknoxsso","token":"","data":"KCC60TRAY0072"}'); (2)
+      server.onmessage = (event) => {
+        const socketData = JSON.parse(event.data);
+        if (socketData.rpcode === 'RETURN_SUCCESS') {
+          resolve(JSON.parse(socketData.data));
+        } else {
+          this.websocket.close();
+          let err;
+          if (socketData.rpcode === 'EMPTY_BOX') err = 'sdl.epTray.rpCode.EMPTY_BOX';
+          else err = `sdl.epTray.rpCode.${socketData.detail}`;
+          reject(err);
+        }
+      };
+    };
+    server.onerror = () => {
+      const err = 'sdl.epTray.rpCode.CONNECTION_FAILED';
+      reject(err);
+    };
+  }));
+},
+
+
+
+ + + + + + + + + +
1WebSocket HOST 정보
2발급받은 연계용 system ID
+
+
+
+
LoginController.java
+
+

LoginController 에서는 EpTray값의 정보를 이용해 사용자를 로그인 한다. +이때 시스템 사용자가 아닐 경우, 등록 화면으로 이동하고, 정상적인 시스템 사용자일 경우 +Token을 발급한다.

+
+
+

GET /noauth/login/new-eptray

+
+
+
+
@PostMapping("/noauth/login/new-eptray")
+@Operation(value = "", description = "knox new eptray 로그인")
+public JwtResponse loginByNewEptray(HttpServletRequest request, HttpServletResponse response, @RequestParam String encodeUserInfo, @RequestParam String encodeAesKey ) throws AccountException {
+    loginInterceptorExecutor.applyPreLogin(request, response);
+    User user = authenticationService.loginByNewEpTray(encodeUserInfo, encodeAesKey);
+    user.setRecentLoginIp(webUtil.getClientIp(request));
+    JwtResponse jwtResponse = new JwtResponse(jwtUtil.generateToken(user), user.getUserId());
+    user.setJwt(jwtResponse.getJwtToken().split("\\.")[2]);
+    loginInterceptorExecutor.applyPostLogin(request, response, user);
+    return jwtResponse;
+}
+
+
+
+
+
+
ADFS 로그인
+
+ + + + + +
+ + +ADFS 통합인증 사이트 (https://adsso.sec.samsung.net) 에서 신청 후 사용해야 한다.
+자세한 내용은 해당 사이트를 참조하거나 전자통합인증3 (nextsso3@samsung.com) 으로 문의 한다. +
+
+
+

SDL에서는 ADFS 통합인증 사이트의 가이드를 참조하여 AD 로그인 샘플을 제공하고 있으며, 각 프로젝트 환경에 맞게 수정하여 사용한다.

+
+
+
참고할 샘플 파일
+
+
    +
  • +

    LoginPage.vue

    +
  • +
  • +

    AdLoginController.java

    +
  • +
  • +

    onelogin.saml.properties

    +
  • +
+
+
+
+
+
로그인/아웃 전,후 처리
+
+

로그인 전과 후, 로그아웃 전과 후 비즈니스 로직이 필요한 경우 LoginInterceptor를 상속받아 구현한다.

+
+
+

LoginInterceptor 클래스는 아래와 같은 Interface를 제공한다.

+
+
+
+
 /**
+ * 로그인 전
+ */
+default void preLogin(HttpServletRequest request, HttpServletResponse response) {
+}
+
+/**
+ * 로그인 후
+ */
+default void postLogin(HttpServletRequest request, HttpServletResponse response, User user) {
+}
+
+/**
+ * 로그아웃 전
+ */
+default void preLogout(HttpServletRequest request, HttpServletResponse response, User user) {
+}
+
+/**
+ * 로그아웃 후
+ */
+default void postLogout(HttpServletRequest request, HttpServletResponse response, User user) {
+}
+
+
+
+

LoginInterceptor를 구현한 클래스는 LoginInterceptorExecutor에 의해 실행되며 Spring Bean으로 등록하고 SpringWebCofig에 아래와 같이 LoginInterceptorExecutor에 등록한다.

+
+
+
+
@Bean
+public LoginInterceptorExecutor loginInterceptorExecutor() {
+    List<LoginInterceptor> loginInterceptorList = new ArrayList<>();
+    loginInterceptorList.add(userUpdateInterceptor());
+    loginInterceptorList.add(loginOutLogInterceptor());
+    return new LoginInterceptorExecutor(loginInterceptorList);
+}
+
+
+
+

아래는 로그인 후 사용자 정보를 update 하는 UserUpdateInterceptor의 일부분이다.

+
+
+
+
@Override
+    public void postLogin(HttpServletRequest request, HttpServletResponse response, User user) {
+
+        String recentLoginIp = user.getRecentLoginIp();
+        Date recentLoginDatetime = user.getRecentLoginDatetime();
+        user.setLastLoginDate(recentLoginDatetime);
+        user.setLastLoginIp(recentLoginIp);
+        user.setRecentLoginDatetime(new Date());
+        user.setRecentLoginIp(webUtil.getClientIp(request));
+        user.setLastActivityTime(System.currentTimeMillis());
+        userService.updateUser(user);
+    }
+
+
+
+
+
+
+

5.2. 시스템 관리

+
+

5.2.1. 개발툴/빌드 스크립드

+
+

SDL을 이용한 웹 어플리케이션 개발에 필요한 툴과 빌드, 배포 방법은 설치를 참고한다.

+
+
+
+

5.2.2. 게시판 관리

+
+
개요
+
+

SDL에서 제공하는 게시판 관리 기능으로 공지사항(메인화면 공지팝업 연동)이나 FAQ 게시판 등 Community 성격으로 게시글 등록 및 답변기능을 사용할 수 있는 게시판 기능이다.
+관리 기능에서 게시판 등록 및 관리를 할 수 있으며 사용자용과 관리자용 메뉴를 따로 만들어 권한별로 게시판 기능을 사용할 수 있도록 제공한다.

+
+
+
+
Table
+
+
    +
  • +

    게시판 : TN_CF_BOARD

    +
  • +
  • +

    게시판 분류 : TN_CF_BOARD_CLASSIFICATION

    +
  • +
  • +

    게시판 컬럼 : TN_CF_BOARD_COLUMN

    +
  • +
+
+
+
+
API
+
+
BoardController.java
+
    +
  1. +

    게시판 목록 조회(페이징)
    +GET /boards-with-paging
    +Query ID : selectBoardPagingList

    +
  2. +
  3. +

    게시판 상세정보 조회
    +GET /boards/{boardId}
    +Query ID : selectBoard

    +
  4. +
  5. +

    게시판 등록
    +POST /boards
    +Query ID : insertBoard, insertBoardClassification, insertBoardColumn

    +
    +
      +
    • +

      게시판 등록 시 등록화면에서 추가한 게시판 분류 목록과 default 컬럼 정보가 저장된다.

      +
    • +
    +
    +
  6. +
+
+
+
+
@Override
+@Transactional
+public void insertBoard(Board board) {
+
+    // 게시판 저장.
+    boardDao.insertBoard(board);
+
+    // 게시판 분류 저장.
+    if (!board.getClassifications().isEmpty()) {
+        for (BoardClassification boardClassification : board.getClassifications()) {
+            boardClassification.setBoardId(board.getBoardId());
+            boardClassification.setClassificationId(idGenService.getNextStringId());
+            boardDao.insertBoardClassification(boardClassification);
+        }
+    }
+
+    // 게시판 컬럼 저장.
+    boardDao.insertBoardColumn(board.getBoardId());
+}
+
+
+
+
    +
  1. +

    게시판 수정
    +PUT /boards/{boardId}
    +Query ID : updateBoard, updateBoardClassification, updateBoardColumn

    +
    +
      +
    • +

      게시판 수정시 변경된 게시판 분류 목록과 컬럼 정보가 수정된다.

      +
    • +
    +
    +
  2. +
+
+
+
+
@Override
+@Transactional
+public void updateBoard(Board board) {
+
+    // 게시판 수정.
+    boardDao.updateBoard(board);
+
+    // 게시판 분류 수정.
+    List<BoardClassification> beforeClassifications = boardDao.getBoardClassificationList(board.getBoardId());
+    List<String> beforeClassificationIdList = new ArrayList<>();
+    -- 생략 --
+}
+
+
+
+
    +
  1. +

    게시판 삭제
    +DELETE /boards/{boardId}
    +Query ID : deleteBoard

    +
  2. +
+
+
+
+
화면
+
+
    +
  1. +

    게시판 목록 화면

    +
  2. +
+
+
+
+boardManagement 01 +
+
+
+
    +
  • +

    게시판 목록을 확인할 수 있다.

    +
  • +
  • +

    해당 게시판의 등록된 게시글 등록건수와 현재 사용 여부등의 정보를 보여준다.

    +
  • +
  • +

    등록 버튼 클릭 시 등록화면으로 이동되며 게시판 명 클릭 시 게시판 수정화면으로 이동된다.

    +
  • +
+
+
+
    +
  1. +

    게시판 등록 화면

    +
  2. +
+
+
+
+boardManagement 02 +
+
+
+
    +
  • +

    게시판을 등록할 수 있다.

    +
  • +
  • +

    게시판 세부기능 속성

    +
    +
      +
    • +

      에디터 사용여부 : CafeNote 등 에디터를 사용할지 선택('미사용' 선택 시 textarea 태그로 구현된다)

      +
    • +
    • +

      이미지파일사용 : 이미지 파일 등록 시 본문에 이미지를 표시한다.(사진형 게시판일 경우 필수사용)

      +
    • +
    • +

      메인 공지팝업 : 게시글 등록 시 메인 공지사항 팝업창에 해당 기간동안 게시글이 노출된다.

      +
    • +
    • +

      게시글 '공지’라벨 표시 : 게시글 등록 시 게시글 목록 상단에 공지 게시글로 노출된다.

      +
    • +
    +
    +
  • +
+
+
+
    +
  1. +

    게시판 상세정보 수정 화면

    +
  2. +
+
+
+
+boardManagement 03 +
+
+
+
    +
  • +

    게시판 세부기능 속성 변경이 가능하다.

    +
  • +
  • +

    '공지팝업 제한' 기능은 해당 게시판의 공지게시글을 일시적으로 제한할 수 있는 기능이다.

    +
  • +
+
+
+
+boardManagement 04 +
+
+
+
    +
  • +

    게시판의 검색 조건을 선택할 수 있다.

    +
  • +
  • +

    게시글 목록화면에서 보여줄 정보를 선택 할수 있다.

    +
    +
      +
    • +

      각 컬럼의 사용 여부에 따라서 넓이가 합산 100%로 변경된다.

      +
    • +
    +
    +
  • +
  • +

    게시글 목록 정렬 기준을 선택 할 수 있으며 작성일 내림차순이 기본으로 선택된다.

    +
  • +
+
+
+
+
+

5.2.3. 공지사항 알림 뱃지

+
+
개요
+
+

관리자가 작성한 공지사항이 등록되면 메인페이지에 알림이 보인다.

+
+
+
+
Table
+
+
    +
  • +

    게시판 : TN_CF_BOARD

    +
  • +
  • +

    게시글 : TN_CF_POST

    +
  • +
  • +

    게시판 분류 : TN_CF_BOARD_CLASSIFICATION

    +
  • +
  • +

    사용자 : TN_CF_USER

    +
  • +
  • +

    공지사항 확인 : TN_CF_NOTICE_CHECKED

    +
  • +
+
+
+
+
API
+
+
BoardController.java
+
    +
  1. +

    공지사항 팝업 게시글 목록 조회(메인화면 용)
    +GET /notice-popup-posts
    +Query ID : selectNoticePopupPostList

    +
  2. +
  3. +

    공지사항 팝업 게시글 체크
    +POST /check-notice
    +Query ID : selectNoticeChecked, insertNoticeChecked

    +
  4. +
+
+
+
+
화면
+
+
    +
  • +

    MainTopMenu.vue

    +
  • +
+
+
+
+800 +
+
+
+
+800 +
+
+
+
+
+

5.2.4. 메인 페이지

+
+
구성
+
+
    +
  • +

    Header Section (TopMenu)

    +
    +
      +
    • +

      Navigation - My Menu(즐겨찾기) | navigation(결재함, 게시판, 샘플 페이지, 관리기능)

      +
    • +
    • +

      RightSide - 메인공지팝업 | 번역, 통합검색, 사이트맵 | 표준시간, 언어선택 | 프로필

      +
      + + + + + +
      + + +통합검색의 경우 검색솔루션 등을 통해 따로 구현해야함. +
      +
      +
    • +
    +
    +
  • +
  • +

    MainContainer Section - 메인 컨텐츠(공지사항, FAQ, 연락처 등)

    +
  • +
  • +

    Footer Section - 개인정보취급방침, 이용약관

    +
  • +
+
+
+
+mainPage +
+
+
+
+
+

5.2.5. 링크 사이트

+
+
개요
+
+

링크 사이트를 관리한다.

+
+
+
+
Table
+
+
    +
  • +

    링크사이트 : TN_CF_LINK_SITE

    +
  • +
+
+
+
+
API
+
+
LinkSiteController.java
+
    +
  1. +

    링크사이트 목록 조회(페이징)
    +GET /linksite/linksites-with-paging
    +Query ID : selectLinkSitePagingList

    +
  2. +
  3. +

    링크사이트 등록
    +POST /linksite/linksites
    +Query ID : insertLinkSite

    +
    +
      +
    • +

      LINK_SITE_ID는 IdGenService를 사용하여 유니크한 ID를 만들어서 등록한다.

      +
    • +
    +
    +
  4. +
  5. +

    링크사이트 수정
    +PUT /linksite/linksites
    +Query ID : updateLinkSite

    +
  6. +
  7. +

    링크사이트 삭제
    +DELETE /linksite/linksites
    +Query ID : deleteLinkSite

    +
  8. +
+
+
+
+
화면
+
+

외부 링크사이트를 관리기능 > 기타관리 > 링크사이트 관리를 통해 할 수 있다.

+
+
+
+front 05 01 +
+
+
+
    +
  • +

    구분 : 공통코드 type이 'LINKSITE’인 항목

    +
  • +
  • +

    사이트명(기본) : 기본 사이트명을 입력한다.

    +
  • +
  • +

    사이트명(locale) : 다국어별 사이트명을 입력한다.

    +
  • +
  • +

    설명 : 사이트에 대한 설명을 입력한다.

    +
  • +
  • +

    URL : 사이트의 URL을 등록한다.

    +
  • +
+
+
+
+
+

5.2.6. 일괄 작업 관리 및 이력 조회

+
+
일괄작업 관리
+
+
    +
  • +

    일괄작업 관리를 위해서는 구현 Service (com.samsung..Impl.) 의 메서드에 @BatchJob annotation이 설정되어 있어야 한다.

    +
  • +
  • +

    관리기능 > 일괄작업 관리> 일괄작업 관리 메뉴에서 아래와 같이 설정해야만 이력관리가 남게 된다.

    +
  • +
+
+
+
+batchJobMgmt +
+
+
+
+batchJobMgmtUpdate +
+
+
+
    +
  • +

    일괄작업 관리 화면에서 관리하고자 하는 Batch 정보를 위와 같이 입력한다.

    +
    +
      +
    • +

      구분 : 그룹코드 BATCHGUBUN 에 등록한 공통코드명을 입력한다.

      +
    • +
    • +

      작업명 : Batch 작업명을 입력한다.

      +
    • +
    • +

      작업클래스 : com.samsung.accesslog.impl.SysUseLogMngImpl.loadBatchData과 같이 Batch 실행시 실행되는 Package명을 포함한 클래스명 및 메소드명을 입력한다. (@BatchJob annotation에 설정한 값)

      +
    • +
    • +

      URL : Batch를 직접 실행하기 위한 URL을 입력한다. 여기에 입력한 URL은 일괄작업이력 화면에서 해당 배치실행 버튼을 클릭하였을때 Call 된다. +URL을 입력할 경우에는 해당 Request를 처리할 Controller를 구현해야 한다.

      +
    • +
    +
    +
  • +
+
+
+
+
일괄작업 이력
+
+

일괄작업 관리에 등록한 작업이 수행되면 일괄작업 이력에 아래와 같이 나타난다.

+
+
+
+batchJobLogList +
+
+
+
    +
  • +

    배치가 실행되는 시작 시간과 종료시간, 소요시간, 작업결과 등이 나타난다.

    +
  • +
  • +

    작업이 실패 했을경우 실행 할 수 있는 배치실행 버튼이 나타난다.(일괄작업관리 등록시 URL을 등록해야 나타남).

    +
  • +
+
+
+
+
+

5.2.7. 작업 스케쥴링

+
+
개요
+
+

SDL의 작업 스케쥴링은 Quartz를 이용하여 구현하고 있다. +수행할 작업(Job)을 등록하고 Trigger에 Job을 추가한 후 Scheduler에 Trigger(s)를 설정한다.

+
+
+
스케줄러 설정 예
+
+
config.properties
+
+
batch.user.long-term-check.cron=0 10 00 * * ?
+
+
+
+
UserBatchConfig
+
+
@Configuration
+public class UserBatchConfig {
+
+    @Value("${batch.user.long-term-check.cron}")
+    private String batchUserLongTermCheckCron;
+
+    /**
+     * 장기 미사용자 관리 Job
+     */
+    @Bean
+    public JobDetail batchUserLongTermCheckJob() {
+         return JobBuilder
+                .newJob(UserBatchExecutor.class)  (1)
+                .withIdentity("batchUserLongTermCheck") (2)
+                .withDescription("User LongTerm Check Batch")
+                .storeDurably(true)
+                .build();
+    }
+
+    /**
+     * 장기 미사용자 관리 Trigger
+     */
+    @Bean
+    public CronTriggerFactoryBean batchUserLongTermCheckTrigger() {
+        CronTriggerFactoryBean trigger = new CronTriggerFactoryBean();
+        trigger.setJobDetail(batchUserLongTermCheckJob());  (3)
+        trigger.setCronExpression(batchUserLongTermCheckCron);  (4)
+        return trigger;
+    }
+
+}
+
+
+
+ + + + + + + + + + + + + + + + + +
1서비스를 수행할 Job Class
2Job 구분 명
3Job 등록
4Cron 표현식 설정
+
+
+
+
+
+

5.2.8. 캐시 관리

+
+
개요
+
+

시스템 전체 캐시를 초기화 한다.

+
+
+
+캐시 관리 +
+
+
+
+
+

5.2.9. 코드 관리

+
+
개요
+
+

그룹 코드를 등록하고 그룹 코드의 공통 코드를 등록하여 추가, 수정 및 삭제를 관리한다.
+groupcodes는 그룹 코드를 나타내고, commcodes는 공통 코드를 나타낸다. 그룹 코드가 상위, 공통 코드가 그룹 코드의 하위 개념이다.

+
+
+
+
Table
+
+
    +
  • +

    그룹 코드 : TC_CF_COMM_CODE_TYPE

    +
  • +
  • +

    공통 코드 : TC_CF_COMM_CODE

    +
  • +
+
+
+
+
API
+
+
CommCodeController.java
+
    +
  1. +

    그룹 코드 목록 조회
    +GET /commcode/groupcodes-with-paging
    +Query ID : selectGroupCodePagingList

    +
    +
      +
    1. +

      검색조건에는 코드, 코드명, 설명이 있다.
      +"그룹 코드 목록 조회(페이징)"을 사용하며 각 조건에 맞게 쿼리를 실행한다.
      +특히 코드명은 한글, 영어, 중국어에 상관없이 입력한 값의 대소문자를 가리지 않고 1글자만 입력해도 검색이 된다.

      +
    2. +
    3. +

      orderBy는 기본이 COMM_CODE_TYPE_CODE(코드명) 이다.

      +
    4. +
    +
    +
  2. +
  3. +

    그룹 코드 등록
    +POST /commcode/groupcodes

    +
    +
      +
    • +

      그룹 코드는 20자 이내로 입력해야 한다. 영문자와 숫자, 특수문자만 사용할 수 있고 한글은 입력이 안 되게 정규식을 사용하여 프론트단에서 유효성 체크를 한다.

      +
    • +
    +
    +
  4. +
  5. +

    그룹 코드 삭제
    +DELETE /commcode/groupcodes/{groupCode}

    +
    +
      +
    1. +

      그룹 코드는 TC_CF_COMM_CODE_TYPE.DELETE_YN 컬럼 값을 true로 업데이트 하는 식으로 삭제한다.

      +
      +
        +
      1. +

        먼저 그룹 코드에 추가되어 있는 공통 코드의 하위를 전부 조회하여 찾은 후 공통 코드 전부 TC_CF_COMM_CODE.DELETE_YN 컬럼을 true로 업데이트 한다.

        +
        +
          +
        1. +

          Query ID : updateCommCodeDeleted

          +
        2. +
        +
        +
      2. +
      3. +

        그 후 그룹 코드의 DELETE_YN 컬럼을 true로 업데이트 하여 삭제한다.

        +
        +
          +
        1. +

          Query ID : updateGroupCode

          +
        2. +
        +
        +
      4. +
      +
      +
    2. +
    +
    +
  6. +
  7. +

    공통 코드 추가
    +POST /commcode/groupcodes/{groupCode}/commcodes

    +
    +
      +
    • +

      추가시에 중복체크하고 없으면 등록하는데 TC_CF_COMM_CODE.CODE_ID는 IdGenService를 사용하여 유니크한 ID를 만들어서 등록한다.

      +
    • +
    +
    +
  8. +
  9. +

    공통 코드 삭제
    +DELETE /commcode/groupcodes/{groupCode}/commcodes/{commCodeId}

    +
    +
      +
    • +

      삭제하려는 공통 코드의 자신과 자식들 코드를 조회하여 DELETE_YN 컬럼 값을 true로 업데이트 하는 식으로 삭제한다.

      +
      +
        +
      • +

        Query ID : updateCommCodeDeleted

        +
      • +
      +
      +
    • +
    +
    +
  10. +
+
+
+
+
화면
+
+

그룹 코드 및 그룹코드에 대한 공통 코드를 추가, 수정 및 삭제 기능을 수행하여 관리한다.

+
+
+
+commonCodeList +
+
+
+
기능별 설명
+
+
    +
  • +

    등록 : 코드 정보를 등록하는 화면으로 이동

    +
  • +
+
+
+
+
+
코드 등록
+
+

그룹 코드를 등록
+* 영문자와 숫자, '-', '_' 2개의 특수문자만 사용하여 등록 가능하다.

+
+
+
+commonCodeDetail Reg +
+
+
+
기능별 설명
+
+
    +
  • +

    목록 : 코드 관리 화면으로 이동

    +
  • +
  • +

    저장 : 그룹 코드 정보를 저장

    +
  • +
+
+
+
+
+
코드 상세 정보
+
+

그룹 코드의 상세 정보를 수정, 삭제하고 그에 대한 공통 코드를 추가, 수정, 삭제하여 관리

+
+
+
+commonCodeDetail +
+
+
+
기능별 설명
+
+그룹코드 정보 +
+
    +
  • +

    엑셀다운로드 : 조회 조건과 동일한 조회 결과 값을 담은 엑셀 파일을 다운로드 하는 기능

    +
  • +
  • +

    목록 : 코드 관리 화면으로 이동

    +
  • +
  • +

    삭제 : 그룹코드를 삭제

    +
  • +
  • +

    저장 : 수정된 그룹코드 상세 정보를 수정

    +
  • +
+
+
+
+공통코드 목록 +
+
    +
  • +

    추가 : 공통코드를 등록하는 Popup 호출

    +
  • +
  • +

    수정 : 공통코드를 수정하는 Popup 호출

    +
  • +
  • +

    삭제 : 공통코드를 삭제

    +
  • +
+
+
+
+
+
+
+

5.2.10. 업무 담당자 관리

+
+
개요
+
+

주요 업무 담당자 페이지를 관리한다.

+
+
+ + + + + +
+ + +정적 HTML 로 만든 페이지를 업로드 하여 그대로 보여주고 싶을 때 사용한다. (업무 담당자 관리, 주요 연락처 관리) +
+
+
+
+
화면
+
+
+front 06 01 +
+
+
+
    +
  • +

    등록된 업무 담당자 페이지 목록을 볼 수 있다.

    +
  • +
  • +

    목록의 Key 컬럼을 클릭하여 업무 담당자 페이지 정보를 수정할 수 있다.

    +
  • +
  • +

    팝업 미리보기를 클릭하여 등록된 업무 담당자 페이지를 확인 할 수 있다.

    +
  • +
+
+
+
+front 06 02 +
+
+
+
    +
  • +

    Key: Unique한 키를 지정(영문, 숫자만 가능)

    +
  • +
  • +

    제목: 업무 담당자 페이지 제목

    +
  • +
  • +

    설명: 업무 담당자 페이지 설명

    +
  • +
  • +

    첨부파일: 업무 담당자 HTML 페이지 (html, htm 확장자)

    +
  • +
+
+
+
+front 06 02 01 +
+
+
+
    +
  • +

    메뉴관리에서 메뉴를 등록하고 Page/API 를 추가한다.

    +
    +
      +
    • +

      Page Path에 업무 담당자 관리에서 등록한 Key를 포함한다.

      +
    • +
    • +

      Vue Component는 SampleStaff.vue 를 참조하여 생성 후 등록한다.

      +
    • +
    • +

      API URL에 업무 담당자 조회 API URL을 등록한다. (/html-templates/group/{templateGroupCode=STAFF}/key/{templateKey})

      +
    • +
    +
    +
  • +
+
+
+
+
+

5.2.11. 주요 전화 번호 관리

+
+
개요
+
+

주요 전화번호 페이지를 관리한다.

+
+
+
+
화면
+
+
+front 06 03 +
+
+
+
    +
  • +

    등록된 주요 전화번호 페이지 목록을 볼 수 있다.

    +
  • +
  • +

    리스트의 Key 컬럼을 클릭하여 주요 전화번호 페이지 정보를 수정할 수 있다.

    +
  • +
  • +

    팝업 미리보기를 클릭하여 등록된 주요 전화번호 페이지를 확인 할 수 있다.

    +
  • +
+
+
+
+front 06 04 +
+
+
+
    +
  • +

    Key : Unique한 키를 지정(영문, 숫자만 가능)

    +
  • +
  • +

    제목 : 주요 전화번호 페이지 제목

    +
  • +
  • +

    설명 : 주요 전화번호 페이지 설명

    +
  • +
  • +

    첨부파일 : 주요 전화번호 HTML 파일 (html, htm 확장자)

    +
  • +
+
+
+
    +
  • +

    메뉴관리에서 메뉴를 등록하고 Page/API 를 추가한다.

    +
    +
      +
    • +

      Page Path에 주요 전화번호 관리에서 등록한 Key를 포함한다.

      +
    • +
    • +

      API URL에 주요 전화번호 조회 API URL을 등록한다. (/html-templates/group/{templateGroupCode=CONTACT}/key/{templateKey})

      +
    • +
    +
    +
  • +
+
+
+
+
+
+

5.3. 이력 관리

+
+

5.3.1. 시스템 로깅

+
+
개요
+
+

로그인/아웃, 시스템 접속, 파일다운로드 등의 시스템 로그를 남기고 있다.

+
+
+
Login/out 로그
+
+

로그인 후와 로그아웃 전에 LoginOutLogInterceptor를 통해 로그를 남긴다.

+
+
+ + + + + +
+ + +로그인/아웃 전후에 추가할 로직이 있다면 LoginInterceptor 인터페이스를 상속받아 구현한다. +
+
+
+
+
관련 테이블
+
+

TN_CF_LOGIN_OUT

+
+
+
+
+
+
Access 로그
+
+

시스템 접속시 LoggingInterceptor를 통해 AccessLog를 남겨 db(또는 file)에 저장한다.
+일배치로 메뉴사용이력 및 메뉴활용도 데이터를 집계한다.

+
+
+
+
관련 테이블
+
+

TN_CF_SYS_USE_LOG

+
+
+
+
+
+
File Download 로그
+
+

첨부파일, 엑셀파일 등을 다운로드시 fileManagerService의 saveFileDownloadLog 메서드를 호출하여 파일다운로드 로그를 남긴다.

+
+
+
+
관련 테이블
+
+

TN_CF_SYS_USE_LOG

+
+
+
+
+
+
+
+

5.3.2. SQL Logging

+ +
+
+

5.3.3. 로거(Logger) 관리

+
+
개요
+
+

시스템 Logger 내역을 조회 및 log level을 변경할 수 있다.

+
+
+
+로거 관리 +
+
+
+ + + + + + + + + +
1Logger name별 설정된 configuration 내역(log4j2.xml)을 조회할 수 있다.
2Logger name별로 log level 을 변경할 수 있다.
+
+
+
+
+

5.3.4. 메뉴 활용도

+
+
개요
+
+

메뉴 별로 메뉴 사용 이력을 분석해서 월별, 일별로 활용률을 계산한다.

+
+
+
+
Table
+
+
    +
  • +

    공통코드 : TC_CF_COMM_CODE

    +
  • +
  • +

    메뉴사용주기 : TC_CF_MENU_USE_PERIOD

    +
  • +
  • +

    메뉴 : TN_CF_MENU

    +
  • +
  • +

    메뉴활용도 집계 : TS_CF_MENU_USE_MM

    +
  • +
+
+
+
+
API
+
+
MenuUtilizationController.java
+
    +
  1. +

    메뉴 활용도 목록 조회
    +GET /menuutilization/menu-utilizations
    +Query ID : selectMenuUtilizationList

    +
  2. +
+
+
+
+
화면
+
+

메뉴에 대한 사용 주기, Hit, 활용도를 조회

+
+
+
+menuUtilHistory +
+
+
+
+
+

5.3.5. 메뉴 사용 이력

+
+
개요
+
+

시스템에 접속한 사용자의 메뉴 사용 이력을 조회

+
+
+
+
Table
+
+
    +
  • +

    메뉴 사용 이력 : TN_CF_MENU_USE_HISTORY

    +
  • +
+
+
+
+
API
+
+
HistoryController.java
+
    +
  1. +

    일별 메뉴 사용 이력 조회
    +GET /history/menu-use-by-date
    +Query ID : selectMenuUseHistoryByDatePagingList

    +
  2. +
  3. +

    월별 메뉴 사용 이력 조회
    +GET /history/menu-use-by-month
    +Query ID : selectMenuUseHistoryByMonthPagingList

    +
  4. +
+
+
+
+
화면
+
+
+menuUserHistoty +
+
+
+
기능별 설명
+
+
    +
  • +

    엑셀다운로드 : 조회 조건과 동일한 조회 결과 값을 담은 엑셀 파일을 다운로드 하는 기능

    +
  • +
+
+
+
+
+
+

5.3.6. 파일 다운로드 이력

+
+
개요
+
+

사용자가 엑셀 다운로드 하거나 이미지 파일이 있는 게시판을 이용했거나 첨부파일을 다운로드 한 목록을 보여준다.

+
+
+
+
Table
+
+
    +
  • +

    시스템 사용 로그 : TN_CF_SYS_USE_LOG

    +
  • +
  • +

    사용자 : TN_CF_USER

    +
  • +
+
+
+
+
API
+
+
HistoryController.java
+
    +
  1. +

    파일 다운로드 이력 목록 조회
    +GET /history/file-download-logs
    +Query ID : selectFileDownloadLogPagingList

    +
    +
      +
    • +

      파일 다운로드 이력의 LOG_FLAG는 TN_CF_SYS_USE_LOG 테이블 LOG_FLAG 컬럼의 '5' 이다.

      +
    • +
    +
    +
  2. +
+
+
+
+
화면
+
+

엑셀 다운로드 기능이 있는 특정 화면에서 엑셀 다운로드 실행에 대한 이력을 조회.

+
+
+
+fileDownloadHistory +
+
+
+
기능별 설명
+
+
    +
  • +

    엑셀다운로드 : 조회 조건과 동일한 조회 결과 값을 담은 엑셀 파일을 다운로드 하는 기능

    +
  • +
+
+
+
+
+
+

5.3.7. 로그인 이력

+
+
개요
+
+

사용자가 시스템에 로그인한 이력을 조회

+
+
+
+
Table
+
+
    +
  • +

    로그인 이력 : TN_CF_LOGIN_OUT

    +
  • +
  • +

    사용자 : TN_CF_USER

    +
  • +
+
+
+
+
API
+
+
HistoryController.java
+
    +
  1. +

    로그인 이력 목록 조회
    +GET /history/login-out-logs +Query ID : selectListLoginOut

    +
  2. +
+
+
+
+
화면
+
+

사용자가 시스템에 로그인한 이력을 조회

+
+
+
+loginHistory +
+
+
+
+
LoginOutLogInterceptor
+
+

시스템에 사용자가 로그인/아웃을 하면 LoginOutLogInterceptor에 의해 사용자 정보가 저장된다.

+
+
+

LoginOutLogInterceptor는 LoginInterceptor의 구현클래스로 LoginInterceptor의 자세한 설명은 로그인로그인/아웃 전,후 처리를 참고한다.

+
+
+
+
@Override
+public void postLogin(HttpServletRequest request, HttpServletResponse response, User user) {
+    LoginOut loginOut = new LoginOut();
+
+    if (StringUtils.isNotEmpty(user.getJwt())) {
+        loginOut.setToken(user.getJwt().substring(user.getJwt().lastIndexOf('.') + 1));
+    }
+    loginOut.setUserId(user.getUserId());
+    loginOut.setLoginTime(new Date());
+    loginOut.setUseIp(user.getRecentLoginIp());
+    loginOut.setReqType("WEB");
+
+    loginOutLogService.insertLogin(loginOut);
+}
+
+
+
+

로그인 시 사용자 ID, 로그인 시간, IP 등을 TN_CF_LOGIN_OUT 테이블에 저장한다.

+
+
+
+
@Override
+public void preLogout(HttpServletRequest request, HttpServletResponse response, User user) {
+    LoginOut loginOut = new LoginOut();
+
+    if (StringUtils.isNotEmpty(user.getJwt())) {
+        loginOut.setToken(user.getJwt().substring(user.getJwt().lastIndexOf('.') + 1));
+    }
+    loginOut.setUserId(user.getUserId());
+    loginOut.setLoginTime(user.getRecentLoginDatetime());
+    loginOut.setLogoutTime(new Date());
+    loginOut.setUseIp(webUtil.getClientIp(request));
+    loginOut.setReqType("WEB");
+
+    loginOutLogService.updateLogout(loginOut);
+
+    userService.updateUserJwt(Map.of("userId", user.getUserId(), "lastLogoutDate", new Date(), "jwt", ""));
+}
+
+
+
+

로그아웃 전에는 TN_CF_LOGIN_OUT에 정보를 저장하는 것 외에 사용자의 jwt 값을 초기화 한다. +중복 로그인 방지를 위해 jwt값을 이용한다.

+
+
+
+
+
+

5.4. 결재/메일

+
+

5.4.1. Knox REST API 연계 서비스 신청

+
+
이 내용은 Knox결재외에 Knox메일 등 다른 Knox Portal REST API 연계 서비스 이용시에도 공통으로 해당되는 내용이다.
+
    +
  • +

    SDL은 Knox Rest API를 이용하여 결재/메일 등의 서비스를 제공한다. 따라서 우선 Knox 연계서비스를 신청하고 시스템 아이디와 토큰을 발급받아야 한다.

    +
  • +
  • +

    시스템 아이디와 Token 발급이 완료되면, API 샘플 테스트도 가능하다.

    +
  • +
+
+
+
    +
  1. +

    Knox Portal Support내 연계 신청(Knox Dev Center)을 통하여 연계 신청을 진행한다.

    +
  2. +
+
+
+ + + + + +
+ + +스테이지 연계 신청한 Knox Portal 계정에 한하여 운영 연계신청이 가능하므로, 중복신청 방지 및 일관된 관리를 위해 현업 담당자가 신청하는 것을 권장한다. +
+
+
+
    +
  1. +

    Knox Dev Center내 연계 신청 가이드 메뉴를 참고하여 스테이지 연계부터 신청을 진행한다.

    +
  2. +
  3. +

    스테이지 연계 신청 및 승인이 완료되면 API 연계 ID/Token 발급 및 API 구독이 완료되고, +신청자에게 메일로 관련 내용이 통보된다.

    +
  4. +
  5. +

    SDL에서 제공하는 Knox 연계 모듈을 이용한 서비스를 개발하여 연계 테스트를 진행한다.

    +
  6. +
  7. +

    스테이지 연계 테스트가 완료되면 운영도 스테이지와 동일하게 시스템을 통해 신청하여 테스트한다.

    +
  8. +
+
+
+
+

5.4.2. 결재

+
+
개요
+
+

SDL에서는 Knox결재와 동기화 되는 결재 모듈을 제공하고 있는데, 여기서는 Knox Portal REST API 연계 서비스 신청 및 Knox결재 서비스 연계 부분에 대하여 설명한다. 결재화면 및 기능구현 설명은 해당 가이드를 참조한다.

+
+
+
Knox결재 연계 설정
+
+
    +
  1. +

    Knox REST API 연계 서비스 신청이 되었다면, 발급받은 system-id, token 값을 설정한다.

    +
  2. +
+
+
+
knox.properties (스테이지)
+
+
knox.system-id=xxxxxxxxxxx
+knox.token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+knox.address.prefix=openapi.samsung.net
+knox.approval-service=/approval/api/v2.0/approvals
+
+
+
+
knox.properties (운영)
+
+
knox.system-id=xxxxxxxxxxx
+knox.token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx   (1)
+knox.address.prefix=openapi.samsung.net,openapi.w1.samsung.net,openapi.w2.samsung.net   (2)
+knox.approval-service=/approval/api/v2.0/approvals
+
+
+
+ + + + + + + + + +
1토큰 (comma(,)로 구분, 거점 순서와 동일)
2거점 (국내, 구주, 미주)
+
+
+
    +
  1. +

    결재화면 및 기능구현 설명은 해당 가이드를 참조한다.

    +
    + +
    +
  2. +
+
+
+ + + + + +
+ + +개발자의 IP도 반드시 Knox stage 방화벽에 등록하여야만 개발자 PC에서 상신이 된다. 또한 Knox 스테이지(http://www.stage.samsung.net) 에 개발자와 결재자의 계정도 생성 해야만 개발을 진행 할 수 있다. +
+
+
+
+
+
API
+
+

KnoxApprovalController는 Knox에서 제공하는 Approval API를 직접 연결하는 API를 제공한다. +시스템의 비즈니스 로직을 거치지 않고 Knox API를 직접 호출 하기 때문에 서비스 호출에 문제가 있는지 파악하는데 유용하다.

+
+
+ + + + + +
+ + +KnoxApprovalController에서 제공하는 API URI은 Knox REST Service의 API URI와 같다. +
+
+
+
    +
  1. +

    Knox 일반 결재 상신
    +POST /knox/approvals/submit

    +
    +
      +
    • +

      파라미터는 KnoxApproval 클래스를 참고한다.

      +
    • +
    • +

      attachments는 첨부파일, knoxApprovalStr는 KnoxApproval의 Json String 값이다.

      +
    • +
    +
    +
  2. +
+
+
+
+
@PostMapping("/submit")
+public KnoxApproval submitGeneral(@Parameter(value = "첨부파일", required = true) MultipartFile attachments,
+                            @Parameter(value = "상신정보", required = true) String knoxApprovalStr) throws IOException {
+
+    ObjectMapper objectMapper = new ObjectMapper();
+    KnoxApproval knoxApproval = objectMapper.readValue(knoxApprovalStr, KnoxApproval.class);
+
+    String serverLocation = Account.currentUser().getServerLocation();
+    if (StringUtils.isEmpty(serverLocation)) serverLocation = "KR";
+
+    String fileId = fileManagerService.store(attachments);
+    List<Resource> fileList = new ArrayList<>();
+    fileList.add(fileManagerService.getResource(fileId));
+
+    return knoxApprovalService.submit(knoxApproval, fileList, serverLocation);
+}
+
+
+
+
    +
  1. +

    Knox 보안 결재 상신
    +POST /knox/approvals/secu-submit

    +
    +
      +
    • +

      파라미터는 일반 결재 상신과 같지만 보안문서타입이 "CONFIDENTAIL" 이다.

      +
    • +
    +
    +
  2. +
+
+
+
+
knoxApproval.setDocSecuType("CONFIDENTAIL");
+
+
+
+
    +
  1. +

    Knox 결재 상세 상황 조회
    +GET /knox/approvals/{apInfId}/detail

    +
    +
      +
    • +

      결재 연계 ID로 결재 문서의 정보를 상세 조회한다.

      +
    • +
    +
    +
  2. +
  3. +

    Knox 결재 본문 조회
    +GET /knox/approvals/{apInfId}/content

    +
    +
      +
    • +

      결재 연계 ID로 결재 문서의 본문을 조회한다.

      +
    • +
    +
    +
  4. +
  5. +

    Knox 결재 상황 조회
    +POST /knox/approvals/status

    +
    +
      +
    • +

      결재문서의 진행 상태를 조회한다.

      +
    • +
    • +

      복수개의 결재 연계 ID를 요청하여 각각 해당하는 문서변경횟수와 결재상태정보를 응답받는다. 이를 이용하여 결재문서 동기화시 변경된 건에 대해 결재 상태를 업데이트 한다.

      +
    • +
    +
    +
  6. +
+
+
+
+
public List<KnoxApprovalStatus> getStatus(@RequestBody List<KnoxApprovalStatus> knoxApprovalStatusList) {
+
+
+
+
    +
  1. +

    Knox 결재 연계 ID 조회
    +POST /knox/approvals/apinfids

    +
    +
      +
    • +

      결재 ID로 결재 연계 ID를 조회한다.

      +
    • +
    +
    +
  2. +
+
+
+
+
public KnoxApproval getApInfIds(@RequestParam String apId) {
+
+
+
+
    +
  1. +

    Knox 상신함 리스트 조회
    +POST /knox/approvals/submission

    +
    +
      +
    • +

      상신자가 상신한 정보를 조회한다.

      +
    • +
    +
    +
  2. +
+
+
+
+
public List<KnoxApproval> getApInfIdInfos(@RequestParam String epId) {
+
+
+
+
    +
  1. +

    Knox 연계 이력 조회
    +GET /knox/approvals/apinfidinfos

    +
    +
      +
    • +

      요청 시스템에서 상신된 결재문서의 연계 이력을 조회한다.

      +
    • +
    +
    +
  2. +
  3. +

    Knox 상신 취소
    +POST /knox/approvals/{apInfId}/cancel

    +
    +
      +
    • +

      결재 문서를 상신취소한다.

      +
    • +
    +
    +
  4. +
  5. +

    Knox 완결 처리
    +POST /knox/approvals/{apInfId}/autoprogress

    +
    +
      +
    • +

      결재문서를 완결처리한다.

      +
    • +
    +
    +
  6. +
+
+
+
+
KnoxApprovalService
+
+

KnoxApprovalService는 시스템에서 결재 문서를 상신 할때 필요한 API들을 제공한다.

+
+
+
+
/**
+ * 일반 상신, 보안 상신
+ * @param knoxApproval
+ * @param attachments
+ * @param locale
+ * @return
+ */
+KnoxApproval submit(KnoxApproval knoxApproval, List<Resource> attachments, String locale);
+
+/**
+ * 취소
+ * @param apInfId
+ * @param locale
+ * @return
+ */
+KnoxApproval cancel(String apInfId,  String opinion, String locale);
+
+/**
+ * 완결 처리
+ * @param apInfId
+ * @param locale
+ * @return
+ */
+KnoxApproval autoProgress(String apInfId, String flag, String timeZone, String locale);
+
+/**
+ * 결재 상황 조회
+ * @param knoxApprovalStatusList
+ * @param locale
+ * @return
+ */
+List<KnoxApprovalStatus> getStatus(List<KnoxApprovalStatus> knoxApprovalStatusList, String locale);
+
+/**
+ * 결재상세상황조회
+ * @param apInfId
+ * @param locale
+ * @return
+ */
+KnoxApproval getDetail(String apInfId, String locale);
+
+
+/**
+ * 결재본문조회
+ * @param apInfId
+ * @param locale
+ * @return
+ */
+KnoxApproval getContent(String apInfId, String locale);
+
+/**
+ * 결재연계ID조회
+ * @param apId
+ * @return
+ */
+KnoxApproval getApInfIds(String apId, String locale);
+
+/**
+ * 상신함리스트조회
+ * @param epId
+ * @return
+ */
+List<KnoxApproval> getSubmission(String epId, String locale);
+
+/**
+ * 연계이력조회
+ * @param endDate yyyyMMddHHmm 형식
+ * @param page 페이지 처리
+ * @param duration 단위 : 분 / 최소 1분 ~ 최대 60분
+ * @return
+ */
+List<KnoxApproval> getApInfIdInfos(String endDate, String page, String duration, String locale);
+
+
+
+

각각의 메서드들은 Knox Rest 연계 서비스에서 요구하는 가이드대로 REST 형식을 갖추어 필요한 로직들을 수행한다.

+
+
+
+
+

5.4.3. Knox 상신

+
+
결재 Entity Class 생성 및 설정
+
+

결재 기능을 구현해야 하는 업무의 Entity Class를 생성하고 Annotation을 설정하고 기본 결재 Class를 상속 받는다.

+
+
+
+
@Data
+@EqualsAndHashCode(callSuper = true)
+@ApprovalDocument(docType = "knoxSample", description = "Sample Approval Doc. (Knox)", approvalClass = Approval.class, templateEngine = TemplateEngineType.VELOCITY,
+        templateFile = "/templates/approval/approval-sample.vm", docSecuType = ApprovalDocSecuType.PERSONAL,
+        isArbitrary = true, bodyType = ApprovalBodyType.MIME)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class KnoxApprovalSampleDocument extends DefaultApprovalDocument {
+
+	private static final long serialVersionUID = 1L;
+
+    private String docId;
+    private String contents;
+    private String creator;
+}
+
+
+
+

업무프로세스 Entity Class 에 @ApprovalDocument 을 달아 주고 DefaultApprovalDocument를 상속 받아야만 결재 Object 라는 것을 결재 모듈에서 알수 있다.

+
+
+
@ApprovalDocument
+
+

@ApprovalDocument를 이용해 결재 문서의 속정을 정의한다.

+
+ +++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

props명

필수여부

Type

Default

설명

docType

필수

String

사용자가 식별할수 있는 이름

description

필수

String

결재 문서에 대한 설명

approvalClass

필수

class

Approval.class

결재 문서를 DB에 저장할때 사용하는 클래스명

templateEngine

필수

TemplateEngineType

VELOCITY

결재 본문 파싱에 필요한 템플릿 엔진. THYMELEAF, VELOCITY 2개의 템플릿 엔진을 사용할 수 있다.

templateFile 또는 templateKey

필수

String

templateFile: 템플릿 파일 이름을 경로와 확장자 포함해서 설정한다.
+tempageKey: 결재양식 관리 메뉴에서 지정한 key

docSecuType

ApprovalDocSecuType

PERSONAL

결재 문서의 보안 형태

isBodyModify

boolean

true

문서의 기본 결재 본문 수정 true일 때만 UI에서 수정 가능

isRouteModify

boolean

true

문서의 기본 결재 경로 수정 true일 때만 UI에서 수정 가능

isArbitrary

boolean

false

문서의 기본 결재 전결 true일 때만 UI에서 수정 가능

bodyType

ApprovalBodyType

MIME

전송할 문서의 형태 TEXT, HTML, MIME

isInternalApproval

boolean

false

true: 내부결재, false: Knox결재

+
+
+
+
결재 본문 등록
+
+

결재 본문은 VELOCITY 또는 THYMELEAF 로 등록 할 수 있다.
+SDL에서 제공하는 결재 샘플에서는 VELOCITY 파일로 제공하고 있으며 vm 파일을 작성하고,
+위의 Entity Class 샘플에서와 같이 @ApprovalDocumenttemplateEngine=엔진타입templateFile="템플릿파일경로" 값을 등록한다.

+
+
+
+
결재 상신
+
+
결재 상신 UI
+
+

샘플 문서를 등록 하고나면 상세하면 하단에 결재 경로를 설정할 수 있는 결재 스텝을 입력 받는 Component가 표시된다.
+결재 문서 화면 개발시 이 Component를 붙여서 상신 기능을 구현한다.

+
+
+
+approvalSumit 01 +
+
+
+
+
+
+

5.4.4. Knox 결재 상태 동기화

+
+
결재 Batch 설정
+
+

Knox 결재 문서는 10분마다 배치가 실행되어 동기화 되고 있다.

+
+
+
    +
  • +

    config.properties : 동기화 배치 실행 주기 설정

    +
  • +
+
+
+
+
## Knox Approval Sync
+knox.approval.sync.cron=0 0/10 * * * ?
+
+
+
+
    +
  • +

    QuartzConfig.java: Scheduler 등록

    +
  • +
  • +

    KnoxSyncBatchConfig.java: 배치 Job, Trigger 등록

    +
  • +
  • +

    KnoxSyncBatchExecutor.java: 서비스 호출

    +
  • +
+
+
+
+
결재 동기화 로직
+
+

배치에서 실행되는 동기화 로직을 구현한 서비스는 knoxApprovalSyncServiceImpl.java 파일의 synchronizeKnox() 메소드 이며 동기화 로직은 간략하게 아래와 같다.

+
+
+
    +
  1. +

    SDL 결재 테이블에서 결재 진행중인 문서 목록 대상 조회.

    +
  2. +
  3. +

    대상 목록의 결재ID로 Knox REST를 조회하여 Revision이 변경되었는지 확인.

    +
  4. +
  5. +

    Revision 변경된 대상에 대해서 결재 정보 동기화 처리.

    +
  6. +
  7. +

    동기화 완료 후 결재 후처리 진행.

    +
  8. +
+
+
+

결재 동기화 시 오류가 발생한 결재 문건은 결재정보 테이블(TN_CF_APPROVAL)에 결재 동기화 상태값(APPROVAL_FAULT)이 false로 Update되며 다음 배치 실행시 대상으로 선정되지 않는다.
+이런 경우 이벤트 로그 테이블(TN_CF_APPROVAL_EVENT)의 로그를 확인하여 다시 동기화 해야 한다면 결재 관리 상세화면에서 개별 동기화가 가능하다.

+
+
+
+approvalSync 01 +
+
+
+
+
결재 전후처리
+
+

결재 동기화 처리전 또는 처리후 결재 문서 상태에 대한 비지니스를 처리할 수 있도록 인터페이스를 제공하고 있다.

+
+
+

ApprovalInterceptor 를 결재 문서별로 상속 받는 구현 클래스를 만들면 된다.

+
+
+
    +
  • +

    구현 예시(KnoxApprovalSampleInterceptor.java)

    +
  • +
+
+
+
+
@Log4j2
+@ApprovalDocumentType(names = {"knoxSample"})
+public class KnoxApprovalSampleInterceptor implements ApprovalInterceptor {
+
+    @Autowired
+    KnoxApprovalSampleService knoxApprovalSampleService;
+
+    @Override
+    public void afterSubmit(Approval approval, List<ApprovalStep> approvalStepList, Map<String, Object> attribute) {
+        log.debug("execute ApprovalSampleInterceptor afterSubmit");
+        //Knox 상신 후 문서 상태 업데이트
+        knoxApprovalSampleService.updateSampleApprovalDocument(approval.getDbDocId(), ApprovalDocStatus.INPROCESS);
+    }
+    -- 생략 --
+
+
+
+
+
+

5.4.5. 결재 관리

+
+
개요
+
+

결재 관리를 통해 내부/knox 결재 목록을 확인할 수 있다.

+
+
+
+
Table
+
+
    +
  • +

    결재 : TN_CF_APPROVAL

    +
  • +
  • +

    결재 이벤트 : TN_CF_APPROVAL_EVENT

    +
  • +
  • +

    결재 스텝 : TN_CF_APPROVAL_STEP

    +
  • +
+
+
+
+
API
+
+
ApprovalController.java
+
    +
  1. +

    시스템 전체 결재 문서 조회
    +GET /approval/approval-doc-types

    +
    +
      +
    • +

      결재경로 관리의 문서타입을 가져온다.

      +
    • +
    +
    +
  2. +
  3. +

    시스템 전체 결재 목록 조회(페이징)
    +GET /approval/approval-with-paging
    +Query ID : selectApprovalPagingList

    +
  4. +
  5. +

    결재 상세정보 조회
    +GET /approval/{approvalRequestId}
    +Query ID : selectApprovalStep, selectApprovalEventList

    +
  6. +
+
+
+
+
화면
+
+
+front 07 01 +
+
+
+
검색 조건
+
+
    +
  • +

    구분 : knox 결재/ 내부결재

    +
  • +
  • +

    상태 : 결재중/완료/후완결/반려/취소

    +
  • +
  • +

    문서명 : 결재양식 목록

    +
  • +
  • +

    상신자 : 결재 요청한 상신자

    +
  • +
  • +

    동기화 상태 : 성공/실패

    +
  • +
  • +

    기간 : 결재요청 기간

    +
  • +
+
+
+
+
+
+

5.4.6. 결재 경로 관리

+
+
개요
+
+

시스템의 결재 문서 샘플을 RUNTIME 동안 가지고 있다가 결재시에 사용한다.

+
+
+
    +
  • +

    ApprovalManager.java → ApprovalDocument.java → SampleApprovalDocument.java

    +
    +
      +
    1. +

      ApprovalManager : @ApprovalDocument라는 어노테이션이 달린 클래스를 찾는다.

      +
    2. +
    3. +

      SampleApprovalDocument : 결재경로 관리 목록에 결재 문서 샘플을 보여준다.

      +
    4. +
    +
    +
  • +
+
+
+
+
Table
+
+
    +
  • +

    결재 경로 : TN_CF_DYNAMIC_APPROVAL_PATH

    +
  • +
  • +

    필수 결재자 : TN_CF_REQUIRED_APPROVAL_USER

    +
  • +
+
+
+
+
API
+
+
ApprovalController.java
+
    +
  1. +

    시스템 전체 결재 문서 조회
    +GET /approval/approval-doc-types

    +
  2. +
  3. +

    결재 경로 조회
    +GET /approval/dynamic-approval-paths/book
    +Query ID : selectDynamicApprovalPath

    +
    +
      +
    • +

      기본결재 경로 목록을 보여준다.

      +
    • +
    +
    +
  4. +
  5. +

    결재 경로 저장
    +POST /approval/dynamic-approval-paths/book
    +Query ID : deleteDynamicApprovalPath, insertDynamicApprovalPath

    +
  6. +
  7. +

    필수 결재자 목록 조회
    +GET /approval/required-approval-users/book
    +Query ID : selectRequiredApprovalUserList

    +
    +
      +
    • +

      필수 결재자 목록을 보여준다.

      +
    • +
    +
    +
  8. +
  9. +

    필수 결재자 저장
    +POST /approval/required-approval-users/book
    +Query ID : deleteRequiredApprovalUser, insertRequiredApprovalUser

    +
  10. +
+
+
+
+
화면
+
+

지정된 문서타입에 따른 결재경로를 관리기능 > 결재/메일 관리 > 결재경로 관리를 통해 지정할 수 있다.

+
+
+
+front 07 04 +
+
+
+
    +
  • +

    지정된 문서타입 목록을 확인할 수 있다.

    +
  • +
+
+
+
+front 07 05 +
+
+
+
    +
  • +

    문서타입을 선택 후 해당 문서에 대한 기본 결재경로와 필수 결재자를 추가할 수 있다.

    +
  • +
  • +

    결재 상신시 결재자 목록에 지정된 기본결재 경로가 자동 추가되며, 지정된 필수 결재자가 있는 경우 추가하라는 알림을 준다. (다수중 1인 가능)

    +
  • +
+
+
+
+
+

5.4.7. 결재 양식 관리

+
+
개요
+
+

결재 양식을 관리한다.

+
+
+
+
Table
+
+
    +
  • +

    템플릿 : TN_CF_TEMPLATE

    +
  • +
+
+
+
+
API
+
+
TemplateController.java
+
    +
  1. +

    템플릿 목록 조회(페이징)
    +GET /templates-with-paging/group/{templateGroupCode}
    +Query ID : selectTemplatePagingList

    +
    +
      +
    • +

      TEMPLATE_GROUP_CODE 컬럼의 'APPROVAL’을 조회한다.

      +
    • +
    +
    +
  2. +
  3. +

    템플릿 상세 조회
    +GET /templates/group/{templateGroupCode}/key/{templateKey}
    +Query ID : selectTemplate

    +
  4. +
  5. +

    템플릿 상세 조회 (By ID)
    +GET /templates/{id}
    +Query ID : selectTemplate

    +
    +
      +
    • +

      양식의 상세 내용을 조회 하거나 팝업 미리보기를 할 수 있다.

      +
    • +
    +
    +
  6. +
  7. +

    템플릿 Key 중복 체크
    +GET /templates/dup-check/group/{templateGroupCode}/key/{templateKey}
    +Query ID : selectTemplate

    +
    +
      +
    • +

      저장 전 템플릿 Key 중복 여부를 검사한다.

      +
    • +
    +
    +
  8. +
  9. +

    템플릿 등록
    +POST /templates/group/{templateGroupCode}
    +Query ID : insertTemplate

    +
    +
      +
    • +

      양식을 저장한다.

      +
    • +
    +
    +
  10. +
  11. +

    템플릿 수정
    +POST /templates/{id}
    +Query ID : updateTemplate

    +
  12. +
  13. +

    템플릿 삭제
    +DELETE /templates/{id}
    +Query ID : deleteTemplate

    +
  14. +
+
+
+
+
화면
+
+

결재양식을 관리기능 > 결재/메일관리 > 결재양식 관리를 통해 볼 수 있다.

+
+
+
+front 07 06 +
+
+
+
    +
  • +

    등록된 결재양식 목록을 결재양식 관리를 통해 볼 수 있다.

    +
  • +
  • +

    목록의 Key 컬럼을 클릭하여 결재양식 정보를 수정할 수 있다.

    +
  • +
  • +

    팝업 미리보기 컬럼을 클릭하여 등록된 결재양식의 첨부파일을 확인 할 수 있다.

    +
  • +
+
+
+
+front 07 07 +
+
+
+
    +
  • +

    Key : 유니크한 결재양식 키를 지정(영문, 숫자만 가능)

    +
  • +
  • +

    제목 : 결재양식의 제목

    +
  • +
  • +

    설명 : 결재양식의 설명

    +
  • +
  • +

    첨부파일 : 결재양식 첨부파일

    +
  • +
+
+
+
+
+

5.4.8. 내부 결재

+
+
개요
+
+

SDL에서 제공하는 내부결재 기능.
+결재를 위한 문서 Entity등록과 내부결재 후처리를 위한 Interceptor 클래스 구현으로 간단하게 결재 기능을 구현할 수 있다.
+샘플로 제공하는 화면의 결재 스텝을 지정하는 Component를 필요한 결재 문서 화면에 적용하여 사용할 수 있으며 샘플 Controller를 참조하여 문서와 결재 스텝 목록을 저장하여 approvalService.submit 메소드를 호출하면 된다.

+
+
+
+
Table
+
+
    +
  • +

    결재 정보 : TN_CF_APPROVAL

    +
  • +
  • +

    결재 스텝 정보 : TN_CF_APPROVAL_STEP

    +
  • +
  • +

    Sample Document : TN_CF_SAMPLE_APPROVAL_INTERNAL_DOCUMENT

    +
  • +
+
+
+
+
API
+
+
ApprovalController.java
+
    +
  1. +

    상신함 목록 조회(내부 결재)
    +GET /internal-approvals/submit
    +Query ID : selectInternalApprovalPagingListBySubmit

    +
    +
      +
    • +

      사용자가 상신한 내부결재 문서를 조회한다.

      +
    • +
    +
    +
  2. +
  3. +

    미결함 목록 조회(내부 결재)
    +GET /internal-approvals/not-approve
    +Query ID : selectInternalApprovalPagingListByNotApprove

    +
    +
      +
    • +

      미결중인 내부결재 문서를 조회한다.

      +
    • +
    +
    +
  4. +
  5. +

    기결함 목록 조회(내부 결재)
    +GET /internal-approvals/approved
    +Query ID : selectInternalApprovalPagingListByApproved

    +
    +
      +
    • +

      사용자가 결재 완료한 내부결재 문서를 조회한다.

      +
    • +
    +
    +
  6. +
  7. +

    통보함 목록 조회(내부 결재)
    +GET /internal-approvals/notice
    +Query ID : selectInternalApprovalPagingListByNotice

    +
    +
      +
    • +

      사용자가 통보 대상인 결재문서를 조회한다.

      +
    • +
    +
    +
  8. +
  9. +

    내부결재 문서 상신 취소
    +POST /internal-approvals/{approvalRequestId}/cancel
    +Service : cancelInternalApproval +Query ID : updateInternalApprovalStep

    +
    +
      +
    • +

      결재 문서 상신을 취소한다.

      +
    • +
    +
    +
  10. +
  11. +

    내부결재 문서 결재 승인 또는 합의
    +POST /internal-approvals/{approvalRequestId}/confirm
    +Service : confirmInternalApproval +Query ID : updateInternalApprovalStep

    +
    +
      +
    • +

      결재 문서 승인 또는 합의 한다.

      +
    • +
    +
    +
  12. +
  13. +

    내부결재 문서 반려
    +POST /internal-approvals/{approvalRequestId}/reject
    +Service : rejectInternalApproval +Query ID : updateInternalApprovalStep

    +
    +
      +
    • +

      결재 문서를 반려 한다.

      +
    • +
    +
    +
  14. +
+
+
+
ApprovalSampleController.java
+
    +
  1. +

    내부결재 문서 목록 조회(페이징)
    +GET /internal-approval/sample-document-with-paging
    +Query ID : selectSampleApprovalDocumentPagingList

    +
    +
      +
    • +

      Sample Document 문서 목록을 조회한다.

      +
    • +
    +
    +
  2. +
  3. +

    내부 결재 문서 상신(Sample Document)
    +POST /internal-approval/submit/sample-document

    +
    +
      +
    • +

      Knox 결재 정보 동기화 로직을 제외한 다른 부분은 Knox 결재 상신과 동일하다.

      +
    • +
    • +

      동기화 배치 로직 대상에서 제외된다.

      +
    • +
    • +

      내부결재 전후 처리 로직은 문서 타입별로 Interceptor 클래스를 구현하여 처리한다.

      +
    • +
    +
    +
  4. +
+
+
+
+
@Log4j2
+@ApprovalDocumentType(names = {"internalSample"})
+public class InternalApprovalSampleInterceptor implements ApprovalInterceptor {
+
+    @Autowired
+    InternalApprovalSampleService internalApprovalSampleService;
+    @Override
+    public void afterSubmit(Approval approval, List<ApprovalStep> approvalStepList, Map<String, Object> attribute) {
+        log.debug("execute ApprovalSampleInterceptor afterSubmit");
+        //상신 후 문서 상태 업데이트
+        internalApprovalSampleService.updateSampleApprovalDocument(approval.getDbDocId(), ApprovalDocStatus.INPROCESS);
+    }
+    --생략--
+}
+
+
+
+
+
화면
+
+
    +
  1. +

    내부결재 문서 상신함

    +
  2. +
+
+
+
+internalApproval 01 +
+
+
+
+internalApproval 05 +
+
+
+
    +
  • +

    사용자가 상신한 내부결재 문서 목록을 조회한다.

    +
  • +
  • +

    상세화면에 진입하여 상신취소가 가능하다.

    +
  • +
+
+
+
    +
  1. +

    내부결재 문서 미결함

    +
  2. +
+
+
+
+internalApproval 02 +
+
+
+
+internalApproval 06 +
+
+
+
    +
  • +

    사용자의 결재 차순에 있는 내부결재 문서 목록을 조회한다.

    +
  • +
  • +

    상세화면에 진입하여 승인/합의 또는 반려가 가능하다.

    +
  • +
+
+
+
    +
  1. +

    내부결재 문서 기결함

    +
  2. +
+
+
+
+internalApproval 03 +
+
+
+
    +
  • +

    사용자의 결재 완료한 내부결재 문서 목록을 조회한다.

    +
  • +
+
+
+
    +
  1. +

    내부결재 문서 통보함

    +
  2. +
+
+
+
+internalApproval 04 +
+
+
+
    +
  • +

    결재 완료 후 사용자에게 통보된 내부결재 문서 목록을 조회한다.

    +
  • +
+
+
+
+
+

5.4.9. 대리 결재

+
+
개요
+
+

내부결재에 적용되는 대리결재자를 지정하는 기능을 제공한다.
+(Knox 결재 대리결재는 Knox portal에서 지정 가능함)

+
+
+
+
Table
+
+
    +
  • +

    결재 정보 : TN_CF_APPROVAL_DELEGATE

    +
  • +
+
+
+
+
API
+
+
ApprovalController.java
+
    +
  1. +

    대리결재자 조회
    +GET /approval/approver-delegate
    +Query ID : selectApproverDelegate

    +
    +
      +
    • +

      사용자의 대리결재자를 조회한다.

      +
    • +
    +
    +
  2. +
  3. +

    대리결재자 저장
    +POST /approval/approver-delegate
    +Query ID : updateApproverDelegate, insertApproverDelegate

    +
    +
      +
    • +

      사용자의 대리결재자를 저장한다.(등록 또는 변경)

      +
    • +
    +
    +
  4. +
  5. +

    대리결재자 삭제
    +DELETE /approval/approver-delegate
    +Query ID : deleteApproverDelegate

    +
    +
      +
    • +

      사용자의 대리결재자를 삭제한다.

      +
    • +
    +
    +
  6. +
+
+
+
+
화면
+
+
    +
  1. +

    대리결재자 조회 및 지정

    +
  2. +
+
+
+
+approverDelegate 01 +
+
+
+
    +
  • +

    사용자 정보 > 대리결재 메뉴를 통해서 대리결재자 등록이 가능하다.

    +
  • +
  • +

    기등록된 대리결재자가 존재한다면 대리결재자 팝업 상단에 표시되며 삭제하거나 다른 사용자를 선택하여 변경가능 하다.

    +
  • +
+
+
+
+
+

5.4.10. 메일

+
+
Knox REST API 연계 서비스 신청
+
+

Knox REST API 연계 서비스 신청은 Knox REST API 연계 서비스 신청 항목을 참조한다.

+
+
+
+
Knox메일 연계 설정
+
+

Knox REST API 연계 서비스 신청이 되었다면, 발급받은 system-id, token 값을 설정한다.

+
+
+
knox.properties (스테이지)
+
+
knox.system-id=xxxxxxxxxxx
+knox.token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+knox.address.prefix=openapi.samsung.net
+knox.mail-service=/mail/api/v2.0
+
+
+
+
knox.properties (운영)
+
+
knox.system-id=xxxxxxxxxxx
+knox.token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx   (1)
+knox.address.prefix=openapi.samsung.net,openapi.w1.samsung.net,openapi.w2.samsung.net   (2)
+knox.mail-service=/mail/api/v2.0
+
+
+
+ + + + + + + + + +
1토큰 (comma(,)로 구분, 거점 순서와 동일)
2거점 (국내, 구주, 미주)
+
+
+
+
KnoxMailService
+
+

Knox REST API 연계를 통해서 메일발신, 상태조회등을 하는 서비스로 주요 메서드는 KnoxMailService 인터페이스에 정의되어 있다.

+
+
+

KnoxMailService는 시스템에서 Knox 메일 서비스와 연계할 때 필요한 API들을 제공한다.

+
+
+
+
public interface KnoxMailService {
+    /**
+     * Knox 메일 발신
+     * @param sendMail 발신 메일 정보 (첨부파일정보(AttachFile) 포함)
+     * @return Knox 메일 아이디
+     */
+    String sendMail(SendMail sendMail);
+
+    /**
+     * Knox 메일 발신
+     * @param sendMail 발신 메일 정보
+     * @param attachResources 메일 첨부가 파일 리소스일 경우
+     * @return Knox 메일 아이디
+     */
+    String sendMail(SendMail sendMail, List<Resource> attachResources);
+
+    /**
+     * Knox 메일별 수신상태 조회
+     * @param mailIds Knox 메일 아이디 리스트
+     * @param sendMail 메일 정보
+     * @return Knox 메일 상태
+     */
+    MailStatus[] getDeliveryStatusCount(List<String> mailIds, SendMail sendMail);
+
+    /**
+     * Knox 메일 수신인별 수신상태 조회
+     * @param mailId Knox 메일 아이디
+     * @param sendMail 메일 정보
+     * @return Knox 메일 수신인별 수신상태 정보
+     */
+    Recipient[] getDeliveryStatus(String mailId, SendMail sendMail);
+}
+
+
+
+

각각의 구현 메서드들은 Knox Rest 연계 서비스에서 요구하는 가이드대로 REST 형식을 갖추어 필요한 로직들을 수행한다.

+
+
+

자세한 API 스펙은 Swagger API 문서의 knox-mail-controller 항목을 참고한다.
+메일 발송 샘플 코드는 MailSampleController 소스코드를 참고한다.

+
+
+ + + + + +
+ + +Knox 연계 메일 발송을 위해서는 발신자/수신자 모두 Knox 계정이 존재해야만 테스트가 가능하다. +
+
+
+
+
+

5.4.11. 보낸 메일 이력

+
+
개요
+
+

메일 발송 내역 및 수신 상태를 조회할 수 있다.

+
+
+
보낸 메일 목록
+
+
+sentMailHistory +
+
+
+ + + + + + + + + +
1기간, 제목별 검색 가능
2클릭시 상세 내역 조회
+
+
+
+
보낸 메일 상세 정보
+
+
+sentMailHistoryInfo +
+
+
+ + + + + + + + + + + + + +
1보낸 메일의 수신인별 개봉 여부
2보낸 메일 본문 확인 가능
3수신인과 수신상태 정보 목록
+
+
+
+
+
+

5.4.12. 메일 그룹 관리

+
+
개요
+
+

메일 그룹 및 맵핑 정보 관리 기능 제공.

+
+
+
+
UI Design & Function
+
+
메일 그룹 목록(MailGroupList.vue)
+
+

메일 그룹 등록, 수정 및 삭제가 가능하다.

+
+
+
+mailGroupList.png +
+
+
+
    +
  • +

    기능 설명

    +
    +
      +
    1. +

      메일 그룹 목록 조회

      +
    2. +
    3. +

      메일 그룹 상세정보 조회

      +
    4. +
    5. +

      메일 그룹 등록 : 등록 버튼 클릭 시 메일 그룹 입력 popup 호출.

      +
    6. +
    7. +

      메일 그룹 수정 : checkbox 선택 후 수정 버튼 클릭 시 변경을 위한 popup 호출.

      +
    8. +
    9. +

      메일 그룹 삭제 : checkbox 선택 후 삭제 버튼 클릭 시 삭제.

      +
    10. +
    +
    +
  • +
+
+
+
+
메일 그룹 맵핑 수정(MailGroupMappEdit.vue)
+
+

메일 그룹 맵핑 목록 등록 및 삭제가 가능하다.

+
+
+
+mailGroupMappList.png +
+
+
+
    +
  • +

    기능 설명

    +
    +
      +
    1. +

      메일 그룹 맵핑 목록 조회

      +
    2. +
    3. +

      메일 그룹 맵핑 저장

      +
      +
        +
      1. +

        사용자 추가 : 버튼 클릭 시 사용자 추가 popup 호출.

        +
      2. +
      3. +

        역할 추가 : 버튼 클릭 시 역할 추가 popup 호출.

        +
      4. +
      5. +

        업무그룹 추가 : 버튼 클릭 시 업무그룹 추가 popup 호출.

        +
      6. +
      +
      +
    4. +
    +
    +
  • +
+
+
+
+
+
API & Service
+
+
API
+
+
    +
  • +

    API : MailGroupController.java

    +
    +
      +
    1. +

      메일 그룹 목록 조회 : GET /mail-group-with-paging

      +
    2. +
    3. +

      메일 그룹 상세정보 조회 : GET /mail-group/{mailGroupId}

      +
    4. +
    5. +

      메일 그룹 등록 : POST /mail-group

      +
    6. +
    7. +

      메일 그룹 수정 : PUT /mail-group

      +
    8. +
    9. +

      메일 그룹 삭제 : DELETE /mail-group

      +
    10. +
    11. +

      메일 그룹 맵핑 목록 조회 : GET /mail-group-mapp-with-paging

      +
    12. +
    13. +

      메일 그룹 맵핑 저장 : POST /mail-group-mapp/{mailGroupId}

      +
    14. +
    +
    +
  • +
  • +

    Service : MailGroupServiceImpl.java

    +
    +
      +
    1. +

      메일 그룹 맵핑 저장
      +맵핑 정보 저장 시 기등록 되어 있는 맵핑 목록 삭제 후 +전체 목록을 다시 저장하도록 구현.

      +
    2. +
    +
    +
  • +
+
+
+
+
@Override
+@Transactional
+public void saveMailGroupMapp(String mailGroupId, List<MailGroupMapp> mailGroupMappList) {
+
+	mailGroupDao.deleteMailGroupMapp(mailGroupId);
+
+	for(MailGroupMapp mailGroupMapp : mailGroupMappList) {
+		mailGroupMapp.setMailGroupId(mailGroupId);
+		mailGroupDao.insertMailGroupMapp(mailGroupMapp);
+	}
+}
+
+
+
+
+
+
Entity Table & SQL
+
+
Entity Table
+
+
    +
  • +

    TN_CF_MAIL_GROUP : 메일 그룹

    +
  • +
  • +

    TN_CF_MAIL_GROUP_MAPP : 메일 그룹 맵핑

    +
  • +
+
+
+
+
SQL
+
+
    +
  1. +

    메일 그룹 목록 조회

    +
  2. +
+
+
+
+
<select id="selectMailGroupPagingList" parameterType="java.util.HashMap" resultMap="mailGroupResult">
+	SELECT T.*
+	FROM   (SELECT ROW_NUMBER() OVER(ORDER BY LABEL ASC) ROWNUM,
+	--생략--
+
+
+
+
    +
  1. +

    메일 그룹 상세정보 조회

    +
  2. +
+
+
+
+
<select id="selectMailGroup" parameterType="java.util.HashMap" resultMap="mailGroupResult">
+	SELECT <include refid="columnMailGroup" />,
+	       (SELECT USER_NAME
+	       --생략--
+
+
+
+
    +
  1. +

    메일그룹 등록

    +
  2. +
+
+
+
+
<insert id="insertMailGroup" parameterType="java.util.HashMap">
+	INSERT INTO <include refid="tableMailGroup" />
+	(<include refid="columnMailGroup" />)
+	--생략--
+
+
+
+
    +
  1. +

    메일그룹 수정

    +
  2. +
+
+
+
+
<update id="updateMailGroup" parameterType="java.util.HashMap">
+	UPDATE <include refid="tableMailGroup" />
+	SET    LABEL = #{label},
+	--생략--
+
+
+
+
    +
  1. +

    메일그룹 삭제

    +
  2. +
+
+
+
+
<delete id="deleteMailGroup" parameterType="java.util.HashMap">
+	UPDATE <include refid="tableMailGroup" />
+	SET    DELETED = '1',
+	--생략--
+
+
+
+
    +
  1. +

    메일그룹 맵핑 목록 조회

    +
  2. +
+
+
+
+
<delete id="deleteMailGroup" parameterType="java.util.HashMap">
+	UPDATE <include refid="tableMailGroup" />
+	SET    DELETED = '1',
+	--생략--
+
+
+
+
    +
  1. +

    메일그룹 맵핑 저장

    +
  2. +
+
+
+
+
<delete id="deleteMailGroupMapp" parameterType="java.util.HashMap">
+	DELETE FROM <include refid="tableMailGroupMapp" />
+	WHERE  MAIL_GROUP_ID = #{mailGroupId}
+</delete>
+
+<insert id="insertMailGroupMapp" parameterType="java.util.HashMap">
+	INSERT INTO <include refid="tableMailGroupMapp" />
+	(<include refid="columnMailGroupMapp" />)
+	--생략--
+
+
+
+
+
+
+

5.4.13. 메일 상태 조회

+
+
개요
+
+

Knox REST 메일수신상황조회 API와 연계하여 발신한 메일의 상태를 조회할 수 있다.

+
+
+
Knox메일 연계 설정
+
+

Knox REST 메일수신상황조회를 위해서는 Knox메일 연계 설정이 되어 있어야 한다.
+Knox메일 연계 설정은 메일 항목의 Knox메일 연계 설정 항목을 참조한다.

+
+
+
+
메일별 수신 상황 카운트 조회
+
+

발신한 메일의 메일 아이디값을 이용하여 발신한 메일을 수신한 수신자들의 개봉상태를 +요약한 카운트 정보조회

+
+
+
+
Service
+
+

KnoxMailService

+
+
Method
+
+
+
+
/**
+ * Knox 메일별 수신상태 조회
+ * @param mailIds Knox 메일 아이디 리스트
+ * @param sendMail 메일 정보
+ * @return Knox 메일 상태
+ */
+MailStatus[] getDeliveryStatusCount(List<String> mailIds, SendMail sendMail);
+
+
+
+
    +
  • +

    SendMail 객체에 senderId (발신자 EP ID) 값 설정 필수

    +
  • +
+
+
+
+
+
+
+
수신인 별 수신 상황 조회
+
+

발신한 메일의 메일 아이디값을 이용하여 발신한 메일의 수신자 별 수신 상태 정보를 조회

+
+
+
+
Service
+
+

KnoxMailService

+
+
Method
+
+
+
+
/**
+ * Knox 메일 수신인별 수신상태 조회
+ * @param mailId Knox 메일 아이디
+ * @param sendMail 메일 정보
+ * @return Knox 메일 수신인별 수신상태 정보
+ */
+Recipient[] getDeliveryStatus(String mailId, SendMail sendMail);
+
+
+
+
    +
  • +

    SendMail 객체에 senderId (발신자 EP ID) 값 설정 필수

    +
  • +
+
+
+
+
+
+
+
사용 예
+
+
SentMailHistoryServiceImpl
+
+
@Override
+public Map<String, Object> getSentMail(String mailId) {
+    Map<String, Object> rtnMap = new HashMap<>();
+    MailStatus mailStatus = new MailStatus();
+    List<Recipient> recipientList;
+
+    List<String> mailIds = new ArrayList<>();
+    mailIds.add(mailId);
+    SendMail sendMail = sentMailHistoryDao.getSentMail(mailId);
+
+    try {
+        // 14일 이전 문서는 Knox에서 조회
+        mailStatus = knoxMailService.getDeliveryStatusCount(mailIds, sendMail)[0];  (1)
+        recipientList = Arrays.asList(knoxMailService.getDeliveryStatus(mailId, sendMail));  (2)
+
+        if (ObjectUtils.isNotEmpty(recipientList)) {
+            // 메일 수신 상태 동기화
+            updateRecipient(mailId, recipientList);
+        } else {
+            recipientList = sentMailHistoryDao.getRecipientList(mailId);
+        }
+    } catch (Exception ex) {
+        log.error(ex.getMessage());
+        //Knox 에서 메일을 조회 할수 없을 경우, 14일이 경과 되었거나 Mail Box를 못찾을 경우
+        recipientList = sentMailHistoryDao.getRecipientList(mailId);
+    }
+
+    rtnMap.put("mailStatus", mailStatus);
+    rtnMap.put("recipientList", recipientList);
+    rtnMap.put("sendMail", sendMail);
+    return rtnMap;
+}
+
+
+
+ + + + + + + + + +
1Knox 메일별 수신자들의 개봉상태 카운트 조회
2Knox 수신인 별 메일 수신 상황 조회
+
+
+
+
+
+

5.4.14. 메일 양식 관리

+
+
개요
+
+

메일 양식을 관리한다.
+결재 양식 관리와 비교해서 'MAIL’로 조회하는 것 외에 동일하다.

+
+
+
+
Table
+
+
    +
  • +

    템플릿 : TN_CF_TEMPLATE

    +
  • +
+
+
+
+
API
+
+
TemplateController.java
+
    +
  1. +

    템플릿 목록 조회(페이징)
    +GET /templates-with-paging/group/{templateGroupCode}
    +Query ID : selectTemplatePagingList

    +
    +
      +
    • +

      TEMPLATE_GROUP_CODE 컬럼의 'MAIL’을 조회한다.

      +
    • +
    +
    +
  2. +
  3. +

    템플릿 상세 조회
    +GET /templates/group/{templateGroupCode}/key/{templateKey}
    +Query ID : selectTemplate

    +
  4. +
  5. +

    템플릿 상세 조회 (By ID)
    +GET /templates/{id}
    +Query ID : selectTemplate

    +
    +
      +
    • +

      양식의 상세 내용을 조회 하거나 팝업 미리보기를 할 수 있다.

      +
    • +
    +
    +
  6. +
  7. +

    템플릿 Key 중복 체크
    +GET /templates/dup-check/group/{templateGroupCode}/key/{templateKey}
    +Query ID : selectTemplate

    +
    +
      +
    • +

      저장 전 템플릿 Key 중복 여부를 검사한다.

      +
    • +
    +
    +
  8. +
  9. +

    템플릿 등록
    +POST /templates/group/{templateGroupCode}
    +Query ID : insertTemplate

    +
    +
      +
    • +

      양식을 저장한다.

      +
    • +
    +
    +
  10. +
  11. +

    템플릿 수정
    +POST /templates/{id}
    +Query ID : updateTemplate

    +
  12. +
  13. +

    템플릿 삭제
    +DELETE /templates/{id}
    +Query ID : deleteTemplate

    +
  14. +
+
+
+
+
화면
+
+

메일양식을 관리기능 > 결재/메일관리 > 메일양식 관리를 통해 할 수 있다.

+
+
+
+mailTemplate +
+
+
+
    +
  • +

    등록된 메일양식 목록을 메일양식 관리를 통해 볼 수 있다.

    +
  • +
  • +

    목록의 Key 컬럼을 클릭하여 메일양식 정보를 수정할 수 있다.

    +
  • +
  • +

    팝업 미리보기 컬럼을 클릭하여 등록된 메일양식의 첨부파일을 확인 할 수 있다.

    +
  • +
+
+
+
+mailTemplate1 +
+
+
+
    +
  • +

    Key : 유니크한 메일양식 키를 지정(영문, 숫자만 가능)

    +
  • +
  • +

    제목 : 메일양식의 제목

    +
  • +
  • +

    설명 : 메일양식의 설명

    +
  • +
  • +

    첨부파일 : 메일양식 첨부파일

    +
  • +
+
+
+
+
+

5.4.15. 임직원

+
+
개요
+
+

Knox Portal에서 제공하는 임직원 관련 Rest API 를 이용한 연계 서비스 제공

+
+
+
Knox REST API 연계 서비스 신청
+
+

Knox REST API 연계 서비스 신청은 Knox REST API 연계 서비스 신청 항목을 참조한다.

+
+
+
+
Knox임직원 연계 설정
+
+

Knox REST API 연계 서비스 신청이 되었다면, 발급받은 system-id, token 값을 설정한다.

+
+
+
knox.properties (스테이지)
+
+
knox.system-id=xxxxxxxxxxx
+knox.token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+knox.address.prefix=openapi.samsung.net
+knox.emp-service=/employee/api/v2.0
+
+
+
+
knox.properties (운영)
+
+
knox.system-id=xxxxxxxxxxx
+knox.token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx   (1)
+knox.address.prefix=openapi.samsung.net,openapi.w1.samsung.net,openapi.w2.samsung.net   (2)
+knox.emp-service=/employee/api/v2.0
+
+
+
+ + + + + + + + + +
1토큰 (comma(,)로 구분, 거점 순서와 동일)
2거점 (국내, 구주, 미주)
+
+
+
+
Knox임직원 API 연계 서비스
+
+

Knox REST API 연계를 통해서 임직원 및 조직 정보 조회 기능을 제공하는 서비스로 주요 메서드는 KnoxUserService 인터페이스에 정의되어 있다.

+
+
+

KnoxUserService는 시스템에서 임직원 및 조직 정보를 조회할때 필요한 API들을 제공한다.

+
+
+
+
public interface KnoxUserService {
+
+    /**
+     * Knox 임직원 조회(By EpId)
+     * @param epId EP ID
+     * @return Knox 사용자
+     */
+    Employee[] getKnoxEmployeesByEpId(String epId);
+
+    /**
+     * Knox 임직원 조회(By UserName)
+     * @param userName 사용자 이름
+     * @return Knox 사용자
+     */
+    Employee[] getKnoxEmployeesByUserName(String userName);
+
+    /**
+     * Knox 임직원 조회(By KnoxId)
+     * @param knoxId Knox ID
+     * @return Knox 사용자
+     */
+    Employee[] getKnoxEmployeesByKnoxId(String knoxId);
+
+    /**
+     * Knox 임직원 조회(By Email)
+     * @param email 이메일
+     * @return Knox 사용자
+     */
+    Employee[] getKnoxEmployeesByEmail(String email);
+
+    /**
+     * Knox 조직도 조회(By CompanyCode)
+     * @param companyCode 회사 코드
+     * @return Knox 조직도
+     */
+    Organization[] getKnoxOrganizationsByCompanyCode(String companyCode);
+
+    /**
+     * Knox 조직도 조회(By DepartmentCode)
+     * @param companyCode 회사 코드
+     * @param departmentCode 부서 코드
+     * @return Knox 조직도
+     */
+    Organization[] getKnoxOrganizationsByDepartmentCode(String companyCode, String departmentCode);
+
+    /**
+     * Knox 직급 조회
+     * @param companyCode 회사 코드
+     * @return Knox 직급
+     */
+    Title[] getKnoxTitles(String companyCode);
+}
+
+
+
+

자세한 API 스펙은 Swagger API 문서의 knox-user-controller 항목을 참고한다.

+
+
+
+
+
+

5.4.16. 주소록

+
+
개요
+
+

Knox Portal에서 제공하는 연락처 관련 Rest API 를 이용한 연계 서비스 제공

+
+
+
Knox REST API 연계 서비스 신청
+
+

Knox REST API 연계 서비스 신청은 Knox REST API 연계 서비스 신청 항목을 참조한다.

+
+
+
+
Knox Rest 연락처 연계 설정
+
+

메일, 결재 Knox Rest API 연계와 마찬가지로 연계를 위한 사전 준비가 되었다면, knox.properties 에 연락처 관련 설정이 되어 있는지 확인한다.

+
+
+
knox.properties
+
+
knox.pims-service=/pims/contacts/api/v2.0
+
+
+
+
+
Knox 연락처 연계 서비스
+
+

REST를 통해서 연락처를 연계하는 서비스로 주요 메서드는 KnoxContactService 인터페이스에 정의되어 있다.

+
+
+
+
public interface KnoxContactService {
+
+	/**
+	 * 연락처 그룹 생성
+	 * @param contactGroupDto
+	 * @param userId
+	 * @return
+	 */
+	ContactGroupDto createGroup(ContactGroupDto contactGroupDto, String userId);
+
+	/**
+	 * 연락처 그룹 수정
+	 * @param groupId
+	 * @param contactGroupDto
+	 * @param userId
+	 * @return
+	 */
+	ContactGroupDto updateGroup(String groupId, ContactGroupDto contactGroupDto, String userId);
+
+    /**
+     * 연락처 그룹 삭제
+     * @param groupId
+     * @param userId
+     * @return
+     */
+	String deleteGroup(String groupId, String userId);
+
+	/**
+	 * 연락처 그룹 조회
+	 * @param groupId
+	 * @param userId
+	 * @return
+	 */
+	ContactGroupDto getGroup(String groupId, String userId);
+
+    /**
+     * 연락처 그룹 목록 조회
+     * @param pubType
+     * @param userId
+     * @return
+     */
+	ContactGroupDto[] getGroups(String pubType, String userId);
+
+	/**
+	 * 연락처 생성
+	 * @param contactDto
+	 * @param userId
+	 * @return
+	 */
+	ContactDto createCard(ContactDto contactDto, String userId);
+
+	/**
+	 * 연락처 수정
+	 * @param contactId
+	 * @param contactDto
+	 * @param userId
+	 * @return
+	 */
+	ContactDto updateCard(String contactId, ContactDto contactDto, String userId);
+
+	/**
+	 * 연락처 삭제
+	 * @param contactId
+	 * @param userId
+	 * @return
+	 */
+	String deleteCard(String contactId, String userId);
+
+	/**
+	 * 연락처 조회
+	 * @param contactId
+	 * @param userId
+	 * @return
+	 */
+	ContactDto getCard(String contactId, String userId);
+
+	/**
+	 * 연락처 목록 조회
+	 * @param pubType
+	 * @param userId
+	 * @return
+	 */
+	ContactDto[] getCards(String pubType, String userId);
+
+	/**
+	 * KNOX REST GET MAPPING
+	 * @param <T>
+	 * @param methodName
+	 * @param params
+	 * @param paths
+	 * @param classType
+	 * @return
+	 */
+	<T> T contactsGet(String methodName, MultiValueMap<String, String> params, Map<String, String> paths, Class<T> classType);
+
+	/**
+	 * KNOX REST POST MAPPING
+	 * @param <T>
+	 * @param methodName
+	 * @param bodyMap
+	 * @param params
+	 * @param paths
+	 * @param classType
+	 * @return
+	 */
+	<T> T contactsPost(String methodName, Map<String, Object> bodyMap, MultiValueMap<String, String> params, Map<String, String> paths, Class<T> classType);
+}
+
+
+
+

자세한 API 스펙은 Swagger API 문서의 knox-contact-controller 항목을 참고한다.

+
+
+
+
+
+

5.4.17. 메신저

+
+
개요
+
+

Knox Portal에서 제공하는 메신저 관련 Rest API 를 이용한 연계 서비스 제공

+
+
+
Knox REST API 연계 서비스 신청
+
+

Knox REST API 연계 서비스 신청은 Knox REST API 연계 서비스 신청 항목을 참조한다.

+
+
+
+
Knox Rest 메신저 연계 설정
+
+

메일, 결재 Knox Rest API 연계와 마찬가지로 연계를 위한 사전 준비가 되었다면, knox.properties 에 메신저 관련 설정이 되어 있는지 확인한다.

+
+
+
knox.properties
+
+
knox.messenger.contact-service=/messenger/contact/api/v1.0
+knox.messenger.msgctx-service=/messenger/msgctx/api/v1.0
+knox.messenger.message-service=/messenger/message/api/v1.0
+
+
+
+
+
Knox 메신저 연계 서비스
+
+

REST를 통해서 메신저와 연계하는 서비스로 주요 메서드는 KnoxMessengerService 인터페이스에 정의되어 있다.

+
+
+
+
public interface KnoxMessengerService {
+
+	/**
+	 * 디바이스 ID 조회 : 사용자 ID 와 맵핑되는 단말의 ID 값
+	 * @return 디바이스 ID
+	 */
+	String getDeviceId();
+
+	/**
+	 * 메시지 암호화 키 조회 : 메시지를 암호화하기 위한 키 값
+	 * @param deviceId 디바이스 ID
+	 * @return 메시지 암호화 키
+	 */
+	String getKey(String deviceId);
+
+	/**
+	 * Knox Potal login ID를 이용하여 Knox Messenger 수신자들을 조회한다.
+	 * @param deviceId 디바이스 ID
+	 * @param singleIds 수신자 Knox ID 리스트
+	 * @return Knox Messenger 수신자 ID 리스트
+	 */
+	List<String> getUserIds(String deviceId, List<String> singleIds);
+
+	/**
+	 * 공지 메시지를 발신할 대화방이 없는 경우 신규 대화방 생성을 요청한다.
+	 * @param deviceId 디바이스 ID
+	 * @param key 암호화 키
+	 * @param userIds Knox Messenger 수신자 ID 리스트
+	 * @return 대화방 생성 요청 응답 결과
+	 */
+	Map<String, Object> createChatroom(String deviceId, String key, List<String> userIds);
+
+	/**
+	 * Message 서버 API 에서 필요한 암호화된 바디를 만들기 위한 function
+	 * @param key - msgCtx 를 통해 전달받은 key 값
+	 * @param body - 암호화 해야 될 String
+	 * @return 암호화된 String
+	 */
+	String encrypt(String key, String body);
+
+	/**
+	 * Response 로 전달된 암호화 body 를 복호화 하기 위한 function <br/>
+	 * 암호화의 역순으로 Base64 복호화 -> AES256 복호화
+	 * @param body - 암호화 되어 있는 body
+	 * @return 복호화된 response String
+	 */
+	String decrypt(String body);
+
+	/**
+	 * 신규 생성한 대화방 또는 기존에 사용중인 대화방에 공지 메시지를 발송한다.
+	 * @param deviceId 디바이스 ID
+	 * @param key 암호화 키
+	 * @param chatroomId 생성된 대화방 ID
+	 * @param chatMsg 메시지 내용
+	 * @return 메시지 발신 요청 응답 결과
+	 */
+	Map<String, Object> chat(String deviceId, String key, String chatroomId, String chatMsg);
+}
+
+
+
+

자세한 API 스펙은 Swagger API 문서의 knox-messenger-controller 항목을 참고한다.

+
+
+
+
+
+

5.4.18. MHTML 변환

+
+
개요
+
+

프로젝트에서 메일발송이나 결재상신한 후 방화벽 밖(모바일)에서 조회시 이미지, css가 적용되지 않는 문제를 해결하기 위해 MHT로 변환

+
+
+
MHT 변환 결과 예
+
+
Date: Fri, 9 Aug 2019 14:55:20 +0900 (KST)
+Message-ID: <963810424.1.1565330120404@DESKTOP-TUEGPQT>
+Subject: =?UTF-8?B?66mU64m0IOq2jO2VnCDrp4zro4wg7JiI7KCVIOyViOuCtA==?=
+MIME-Version: 1.0
+Content-Type: multipart/related;
+	boundary="----=_Part_0_398737318.1565330120140"
+
+------=_Part_0_398737318.1565330120140
+Content-Type: text/html; charset="utf-8"
+Content-Transfer-Encoding: base64
+
+PCFkb2N0eXBlIGh0bWw+DQo8aHRtbD4NCjxoZWFkPg0KPG1ldGEgY2hhcnNldD0idXRmLTgiPg0K
+PHRpdGxlPuuplOuJtCDqtoztlZwg66eM66OMIOyYiOyglSDslYjrgrQ8L3RpdGxlPg0KPC9oZWFk
+Pg0KDQo8Ym9keT4NCjxkaXYgc3R5bGU9IndpZHRoOjEwMCU7YmFja2dyb3VuZC1jb2xvcjojZmZm
+(중략)
+------=_Part_0_398737318.1565330120140
+Content-Type: application/octet-stream
+Content-Transfer-Encoding: base64
+Content-ID: <simbol.png>
+
+iVBORw0KGgoAAAANSUhEUgAAAQEAAAEBCAYAAAB47BD9AAAwaUlEQVR42u3deZxddX3/8QuC4kPt
+Rqu2iK222pYW2yogCNbyE81MVRSFIEuAewmBsGQl+zqZmexhExEV0AQCSSDrZLZkJjOZzGRmQhAM
+RXYqbrVaFZHJTOCe+/29v+fc7z3nnn35nnPP8v3j9XAeLWVScz/P7/ee73fu5AghuSD90+Pt5Trk
+(중략)
+------=_Part_0_398737318.1565330120140--
+
+
+
+
+
+
+

5.5. 보안관리

+
+

5.5.1. 약관 관리

+
+
이용약관동의 관리
+
+

이용약관동의관리 화면을 관리한다.

+
+
+
+termsConditionList +
+
+
+
+
이용약관동의 등록
+
+

등록 화면에서 새로운 약관 생성이 가능하다.

+
+
+

구분은 공통코드TERMS 정보를, 언어는 TERMS_LANG 정보를 참조한다.

+
+
+
+termsConditionDetail Reg +
+
+
+
+
이용약관동의 목록 추가
+
+

그룹코드 관리에서 약관동의 목록 추가가 가능하다.

+
+
+
+termsCodeDetail +
+
+
+

신규 사용자 로그인 시 화면에 아래와 같이 나타난다.

+
+
+
+
신규사용자 이용약관 동의
+
+

신규 사용자 로그인 시 화면에 아래와 같이 나타난다.

+
+
+
+termsCondition New +
+
+
+
+
+

5.5.2. 개인정보 사용 이력 관리

+
+
개요
+
+

사용자 정보를 조회한 이력을 남긴다. 관련 법에 따라 일정기간 동안 보관한다.

+
+
+
사용자 정보 조회 이력 관리
+
+
    +
  • +

    UserController에서 사용자 정보 관련 메서드 호출시, UserService의 writeUserHistoryLog 메서드를 호출하고 있다.

    +
  • +
  • +

    log4j2.xml에 설정한 파일에 이력이 남는다.

    +
  • +
+
+
+
UserController.class
+
+
    @Operation(summary = "사용자 목록 조회")
+    @GetMapping("/auth/users")
+    public PagingResult<User> getUserPagingList( @ModelAttribute UserSearchDto searchDto) {
+
+        PagingResult<User> resultPage = userService.getUserPagingList(searchDto);
+
+        // 개인정보조회 이력 남김
+        userService.writeUserHistoryLog(resultPage);
+
+        return resultPage;
+    }
+
+    @Operation(summary = "사용자 조회 (by EP ID)")
+    @GetMapping("/auth/users/{userId}")
+    public User userInfo(@Parameter(description = "EP ID", required = true) @PathVariable(required = true) String userId) {
+
+    	User userInfo = userService.getUserById(userId);
+
+    	// 개인정보조회 이력 남김
+        userService.writeUserHistoryLog(userInfo);
+
+        return userInfo;
+    }
+
+
+
+
UserServiceImpl.class
+
+
    @Override
+	public void writeUserHistoryLog(Object returnValue) {
+		HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
+		String requestUri = request.getRequestURI();
+		String requestMethod = request.getMethod();
+		User user = Account.currentUser();
+		if(ObjectUtils.isNotEmpty(user)) {	// 로그인된 사용자
+			try {
+				HistoryLog log = new HistoryLog();
+				log.setLogId(idGenService.getNextStringId());
+				log.setNodeId(nodeId);
+				if(ObjectUtils.isNotEmpty(user)) {
+					log.setWorkerId(user.getUserId());
+					log.setWorkerName(user.getUserName());
+				}
+				log.setWorkDatetime(DateTime.now().toString());
+				log.setRemoteAddr(webUtil.getClientIp(request));
+				log.setRequestMethod(requestMethod);
+				log.setRequestUri(requestUri);
+				log.setApiResult(returnValue);
+
+				String jsonVal = mapper.writeValueAsString(log);
+				USER_HISTORY_LOG.info(jsonVal);
+			} catch (JsonProcessingException e) {
+				log.warn(e.getMessage());
+			}
+		}
+	}
+
+
+
+
log4j2.xml
+
+
<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="INFO">
+    <Appenders>
+        <RollingFile name="UserHistoryAppender" fileName="/logs/history/user-history-${date:yyyy-MM-dd}-${hostName}.log"
+                     filePattern="/logs/history/user-history-%d{yyyy-MM-dd}-${hostName}.log">
+            <PatternLayout>
+                <Pattern>%d %-5p [%t] %-17c{2} \(%13F:%L\) - %m%n</Pattern>
+            </PatternLayout>
+            <Policies>
+                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
+            </Policies>
+        </RollingFile>
+    </Appenders>
+    <Loggers>
+        <Logger name="UserHistoryLog" level="INFO" additivity="false">
+            <AppenderRef ref="UserHistoryAppender"/>
+        </Logger>
+    </Loggers>
+</Configuration>
+
+
+
+
+
+
+

5.5.3. 개인정보 취급자 권한변경이력

+
+
개요
+
+

역할 및 업무그룹의 권한 정보를 변경한 이력을 남긴다.

+
+
+
역할 권한 변경 이력 로깅
+
+
    +
  • +

    AOP를 이용하여 RoleService의 사용자 역할 권한 추가/수정/삭제 메서드가 호출될때 이력을 남긴다.

    +
  • +
  • +

    log4j2.xml에 설정한 파일에 이력이 남는다.

    +
  • +
+
+
+
RoleHistoryLoggingAspect.class
+
+
@Aspect
+@Component
+@Log4j2
+public class RoleHistoryLoggingAspect extends HistoryLoggingSupport{
+
+	private static final Logger ROLE_HISTORY_LOG = LogManager.getLogger("RoleHistoryLog");
+
+	@Value("${node-id}")
+    private String nodeId;
+
+    private final IdGenService idGenService;
+
+	public RoleHistoryLoggingAspect(IdGenService idGenService) {
+		this.idGenService = idGenService;
+	}
+
+	@Pointcut("execution(* com.samsung.role.impl.RoleServiceImpl.insertUserRoleList(..))")
+	public void insertUserRolePointcut() {
+		// Do nothing because pointcut
+	}
+
+	@Pointcut("execution(* com.samsung.role.impl.RoleServiceImpl.updateUserRoleList(..))")
+	public void updateUserRolePointcut() {
+		// Do nothing because pointcut
+	}
+
+	@Pointcut("execution(* com.samsung.role.impl.RoleServiceImpl.deleteUserRoleList(..)) || execution(* com.samsung.role.impl.RoleServiceImpl.deleteUserRole(..))")
+	public void deleteUserRolePointcut() {
+		// Do nothing because pointcut
+	}
+
+	@After(value = "insertUserRolePointcut() || updateUserRolePointcut() || deleteUserRolePointcut()")
+	public void writeRoleHistoryLog() {
+		writeHistoryLog(ROLE_HISTORY_LOG, idGenService, nodeId);
+	}
+}
+
+
+
+
HistoryLoggingSupport.class
+
+
public class HistoryLoggingSupport {
+	private static final ObjectMapper mapper = new ObjectMapper();
+
+	@Autowired
+	protected WebUtil webUtil;
+
+	public void writeHistoryLog(Logger logger, IdGenService idGenService, String nodeId) {
+		HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
+		String requestUri = request.getRequestURI();
+		String requestMethod = request.getMethod();
+		User user = Account.currentUser();
+		if(ObjectUtils.isNotEmpty(user)) {	// 로그인된 사용자
+			try {
+				HistoryLog log = new HistoryLog();
+				log.setLogId(idGenService.getNextStringId());
+				log.setNodeId(nodeId);
+				if(ObjectUtils.isNotEmpty(user)) {
+					log.setWorkerId(user.getUserId());
+					log.setWorkerName(user.getUserName());
+				}
+				log.setWorkDatetime(DateTime.now().toString());
+				log.setRemoteAddr(webUtil.getClientIp(request));
+				log.setRequestMethod(requestMethod);
+				log.setRequestUri(requestUri);
+
+				String jsonVal = mapper.writeValueAsString(log);
+				logger.info(jsonVal);
+			} catch (JsonProcessingException e) {
+				logger.warn(e.getMessage());
+			}
+		}
+	}
+}
+
+
+
+
log4j2.xml
+
+
<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="INFO">
+    <Appenders>
+        <RollingFile name="RoleHistoryAppender" fileName="/logs/history/role-history-${date:yyyy-MM-dd}-${hostName}.log"
+                     filePattern="/logs/history/role-history-%d{yyyy-MM-dd}-${hostName}.log">
+            <PatternLayout>
+                <Pattern>%d %-5p [%t] %-17c{2} \(%13F:%L\) - %m%n</Pattern>
+            </PatternLayout>
+            <Policies>
+                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
+            </Policies>
+        </RollingFile>
+    </Appenders>
+    <Loggers>
+        <Logger name="RoleHistoryLog" level="INFO" additivity="false">
+            <AppenderRef ref="RoleHistoryAppender"/>
+        </Logger>
+    </Loggers>
+</Configuration>
+
+
+
+
+
업무그룹 권한 변경 이력 로깅
+
+
    +
  • +

    AOP를 이용하여 WorkGroupService서비스의 업무그룹 권한 추가/수정/삭제 메서드가 호출될때 이력을 남긴다.

    +
  • +
  • +

    log4j2.xml에 설정한 파일에 이력이 남는다.

    +
  • +
+
+
+
WorkgroupHistoryLoggingAspect.class
+
+
@Aspect
+@Component
+@Log4j2
+public class WorkgroupHistoryLoggingAspect extends HistoryLoggingSupport {
+
+	private static final Logger WORKGROUP_HISTORY_LOG = LogManager.getLogger("WorkgroupHistoryLog");
+
+	@Value("${node-id}")
+    private String nodeId;
+
+    private final IdGenService idGenService;
+
+	public WorkgroupHistoryLoggingAspect(IdGenService idGenService) {
+		this.idGenService = idGenService;
+	}
+
+	@Pointcut("execution(* com.samsung.workgroup.impl.WorkgroupServiceImpl.insertWorkgroupRoleList(..))")
+	public void insertWorkgroupRoleList() {
+		// Do nothing because pointcut
+	}
+
+	@Pointcut("execution(* com.samsung.workgroup.impl.WorkgroupServiceImpl.insertWorkgroupMenuList(..))")
+	public void insertWorkgroupMenuList() {
+		// Do nothing because pointcut
+	}
+
+	@Pointcut("execution(* com.samsung.workgroup.impl.WorkgroupServiceImpl.updateWorkgroupRoleList(..))")
+	public void updateWorkgroupRoleList() {
+		// Do nothing because pointcut
+	}
+
+	@Pointcut("execution(* com.samsung.workgroup.impl.WorkgroupServiceImpl.updateWorkgroupMenuList(..))")
+	public void updateWorkgroupMenuList() {
+		// Do nothing because pointcut
+	}
+
+	@Pointcut("execution(* com.samsung.workgroup.impl.WorkgroupServiceImpl.deleteWorkgroupRoleList(..))")
+	public void deleteWorkgroupRoleList() {
+		// Do nothing because pointcut
+	}
+
+	@Pointcut("execution(* com.samsung.workgroup.impl.WorkgroupServiceImpl.deleteWorkgroupMenuList(..))")
+	public void deleteWorkgroupMenuList() {
+		// Do nothing because pointcut
+	}
+
+	@After(value = "insertWorkgroupRoleList() || insertWorkgroupMenuList() || updateWorkgroupRoleList() || updateWorkgroupMenuList() || deleteWorkgroupRoleList() || deleteWorkgroupMenuList()")
+	public void writeWorkgroupHistoryLog() {
+		writeHistoryLog(WORKGROUP_HISTORY_LOG, idGenService, nodeId);
+	}
+}
+
+
+
+
log4j2.xml
+
+
<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="INFO">
+    <Appenders>
+        <RollingFile name="WorkgroupHistoryAppender"
+                     fileName="/logs/history/workgroup-history-${date:yyyy-MM-dd}-${hostName}.log"
+                     filePattern="/logs/history/workgroup-history-%d{yyyy-MM-dd}-${hostName}.log">
+            <PatternLayout>
+                <Pattern>%d %-5p [%t] %-17c{2} \(%13F:%L\) - %m%n</Pattern>
+            </PatternLayout>
+            <Policies>
+                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
+            </Policies>
+        </RollingFile>
+    </Appenders>
+    <Loggers>
+        <Logger name="WorkgroupHistoryLog" level="INFO" additivity="false">
+            <AppenderRef ref="WorkgroupHistoryAppender"/>
+        </Logger>
+    </Loggers>
+</Configuration>
+
+
+
+
+
+
+

5.5.4. EU GDPR

+
+
개요
+
+
    +
  1. +

    GDPR이란?
    +2018년 5월 25일부터 시행되는 EU(유럽연합)의 개인정보보호 법령이며, 동 법령 위반시 과징금 등 행정처분이 부과될 수 있어 EU와 거래하는 우리나라 기업도 이 법에 위반되지 않도록 주의할 필요가 있다.

    +
  2. +
  3. +

    EU와 거래하는 시스템은 이용약관동의 관리에서 '유럽연합 개인정보보호 규정’이라는 약관을 추가하여 사용자의 동의를 받아야 한다.

    +
    +
      +
    • +

      세부적인 내용은 약관 관리 매뉴얼을 참조한다.

      +
    • +
    +
    +
  4. +
+
+
+
+
+

5.5.5. 관리자 IP 관리

+
+
관리자 IP 관리
+
+

관리자용 계정의 IP를 관리

+
+
+
+ipMgmt +
+
+
+
기능별 설명
+
+
    +
  • +

    삭제 : 등록된 계정을 삭제

    +
  • +
  • +

    추가 : 관리자용 계정 정보를 등록하기 위해 Row를 추가

    +
  • +
  • +

    저장 : 추가된 계정 정보를 저장

    +
  • +
+
+
+
+
+
+

5.5.6. 문자열 암/복호화

+
+
개요
+
+

필요한 부분에 대하여 암/복호화를 적용하고 있다.

+
+
+
해쉬 알고리즘
+
+

ID/Password 로그인의 경우 Password에 대하여 해쉬 알고리즘(org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder)을 적용하고 있다.

+
+
+
+
전자서명
+
+

Knox Portal EpTray를 통한 로그인의 경우 전자서명된 ssoData를 시스템에 있는 개인키(Private Key)를 기반으로 검증한다.

+
+
+
+
+
+

5.5.7. SQL Injection

+
+
개요
+
+

표준개발라이브러리에서는 MyBatis의 PreparedStatement 를 사용한 value injection을 원칙으로 사용하기 때문에 java 혹은 jsp에서 sql을 만들지 않는다면 근본적으로 sql injection이 발생하지 않는다. +단, ${} 매핑을 사용 할 경우 SDLComparator를 사용해야 한다.

+
+
+
SQL Injeciton 공격 예
+
+

MyBatis에서는 #{}, ${} 두 변수 형태를 제공한다. #{}의 경우엔 SQL Injection 공격이 불가하지만 ${}는 값이 직접 매핑되기 때문에 SQL Injeciton에 노출되어 있다.

+
+
+

${}에 매핑될 값은 사용자가 입력값(파라미터를 변조할수 있는)을 사용할 경우 보안 취약점에 노출되게 된다.

+
+
+
+
<select id="getPerson" parameterType="string" resultType="org.application.vo.Person">
+SELECT * FROM PERSON WHERE NAME = #{name} AND PHONE LIKE '${phone}';
+</select>
+
+
+
+

위의 경우

+
+
+
+
SELECT * FROM PERSON WHERE NAME = ? and PHONE LIKE 'A%'; DELETE FROM PERSON; --'
+
+
+
+

실행이 가능하다.

+
+
+

따라서, ${}에 매핑될 값은 조작이 불가능 하도록 사용자 입력값을 코드로 입력 할 수 있도록 하고, 서버에서 코드에 맞는 스트링을 조합해서 실행 할 수 있도록 해야한다.

+
+
+
+
SDLComparator 적용
+
+

SDL에서는 개발자의 시큐어 코딩 실수로 인한 SQL Injection 실행 방지를 위해 MyBatis에서 변수 매핑 전에 허용된 문자만 사용 할 수 있도록 함수를 제공하고 있다.

+
+
+
사용방법
+
+
<if test="@com.samsung.SdlComparator@isNotEmptyForDynamicSql(orderBy)">
+	ORDER BY ${orderBy}
+</if>
+
+
+
+
+
+
+

5.5.8. XSS 방지

+
+
개요
+
+

XSS(Cross Site Scripting)는 JavaScript등으로 작성된 악성 스크립트 코드를 웹 게시판 등에 삽입해 세션을 가로채거나 공격자가 의도한대로 행동하도록 만드는 공격이다.

+
+
+
XSS 방지 적용
+
+

표준개발라이브러리에서는 HTML태그를 허용하는 게시판에 대해 화이트리스트를 선정하여 해당 태그만 허용되도록 하고 있다.

+
+
+
사용방법
+
+
    @Operation(summary = "게시글 상세조회")
+    @GetMapping("/posts/{postId}")
+    public Post getPost(
+            @Parameter("게시글 ID") @PathVariable String postId) {
+        Post post =  postService.getPost(postId);
+        post.setPostDetail(SdlHtmlPolicy.POLICY_DEFINITION.sanitize( post.getPostDetail())); (1)
+        return post;
+    }
+
+
+
+ + + + + +
1게시글 본문을 SdlHtmlPolicy.POLICY_DEFINITION에 정의된 정책을 기반으로 허용된 태그만 가능하도록 처리
+
+
+
+
+
+

5.5.9. 비밀번호 관리

+
+
개요
+
+

ID/PW 로그인시 사용하는 비밀번호의 변경 및 초기화가 가능하다.

+
+
+
비밀번호 변경
+
+

로그인된 상태에서 비밀번호 변경이 가능하다.

+
+
+
+pwdChange +
+
+
+
+
비밀번호 초기화
+
+

로그아웃된 상태에서 ID/PWD 로그인시 비밀번호 초기화를 할 수 있다.
+이메일로 임시 비밀번호가 발송된다.

+
+ ++++ + + + + + + +
+
+pwdReset 01 +
+
Figure 1. 로그인 화면
+
+
+pwdReset 02 +
+
Figure 2. 비밀번호 초기화 화면
+
+
+
+
+
+
+

5.6. 글로벌 지원

+
+

5.6.1. Timezone

+
+
개요
+
+

사용자의 Timezone을 관리한다.

+
+
+
+
Table
+
+
    +
  • +

    사용자 : TN_CF_USER

    +
    +
      +
    • +

      TIME_ZONE_CODE, TIME_ZONE_ID 컬럼 사용

      +
    • +
    +
    +
  • +
+
+
+
+
Timezone 저장
+
+
    +
  • +

    사용자가 시스템에 최초 등록시 저장.
    +그 이후에는 '타임존 저장' API를 사용하여 저장한다.

    +
    +
      +
    • +

      사용자가 시스템에 처음으로 SSO 로그인하여 사용자 등록시 epTray 연계된 타임존 정보를 가져와서 저장 (없을 경우 config.properties의 default 값)

      +
    • +
    +
    +
  • +
+
+
+
+
API
+
+
UserController.java
+
    +
  1. +

    타임존 목록 조회
    +GET /auth/users/timezone

    +
    +
      +
    • +

      타임존은 java.util.TimeZone 라이브러리를 사용하기 때문에 DB에 타임존 목록이 저장되어 있지 않으며,
      +서머타임(일광 절약 시간제, DST(Daylight Saving Time))을 따로 계산하지 않아도 자동으로 목록에서 보여준다.

      +
    • +
    +
    +
  2. +
  3. +

    타임존 저장
    +PUT /auth/users/timezone

    +
  4. +
+
+
+
+
화면
+
+

사용자의 Timezone을 설정하는 기능으로, Timezone을 설정하게 되면 Local Storage의 user.timeZoneId, user.timeZoneCode 에 저장된다.

+
+
+

TopMenu - 표준시간을 통해 접근 가능.

+
+
+
+timezone +
+
+
+ + + + + +
+ + +SideMenu(Right Side) 이용 시 MainOffsider.vue 내 'timezone' 부분에 구현되어 있다. +
+
+
+
+
+

5.6.2. 다국어 서비스

+
+
개요
+
+

message-common.properties를 사용하여 시스템에 다국어를 지원한다.
+한글, 영문을 기본으로 서비스한다.
+기본언어인 한글은 message-common_ko_KR.properties을 파일명으로 하고 영문은 '_en_US’를 붙여서 사용한다.

+
+
+
+
설정
+
+
프론트엔드 기본언어 값 설정
+
+
.env
+
+
# To set default Language for I18n (ko_KR, en_US, zh_CN, etc..)
+VITE_DEFAULT_LANG=ko_KR
+
+
+
+
+
백엔드 기본언어 및 다국어 설정
+
+
config.properties
+
+
## Language Set
+default-language=ko_KR
+language-set=ko_KR,en_US
+
+
+
+
    +
  • +

    예) 시스템에서 프랑스어를 추가하고자 할 때 방법

    +
    +
      +
    1. +

      config.properties의 language-set에 fr_FR을 추가

      +
      +
      +
      ## Language Set(프랑스어 추가)
      +language-set=ko_KR,en_US,fr_FR
      +
      +
      +
    2. +
    3. +

      message-common_fr_FR.properties 파일을 생성

      +
    4. +
    5. +

      메세지의 프랑스어 버전 작성

      +
    6. +
    +
    +
  • +
+
+
+ + + + + +
+ + +메세지들만 추가되므로 메뉴관리, 역할관리, 업무그룹관리 등 다국어 컬럼(LABEL_JSON)을 지원하는 table 데이터의 경우 직접 입력하여야 한다. +
+
+
+
+
+
API
+
+
MessageBundleController.java
+
    +
  1. +

    로케일별로 메세지를 조회
    +GET /noauth/messages

    +
  2. +
  3. +

    설정한 모든 언어의 메세지를 조회
    +GET /noauth/messages/all

    +
  4. +
+
+
+
+
화면
+
+

한국어,영어 중 원하는 언어로 변경하여 화면을 나타내는 기능으로, 언어를 설정하게 되면 User Token에 저장 되어 로그아웃을 하게 되더라도 마지막에 변경된 언어로 설정된다.

+
+
+

TopMenu - 언어선택을 통해 접근 가능.(단, 다국어 지원 되는 영역에 한해서만 지원).

+
+
+
+language +
+
+
+ + + + + +
+ + +SideMenu(Right Side) 이용 시 MainOffsider.vue 내 'language' 부분에 구현되어 있다. +
+
+
+
+
+

5.6.3. 번역(Utrans)

+
+
개요
+
+

Utrans API를 호출하여 한국어 및 영어 등 언어로 번역하는 기능.

+
+
+
+utrans +
+
+
+
지원 되는 언어방향
+
+
    +
  • +

    유럽어 : 러시아어,스페인어,독일어,프랑스어,이탈리아어,포르투갈어

    +
  • +
+
+ ++++ + + + + + + + + + + + + + + + + + + + + + + +

Source

Target

한국어

영어, 중국어, 베트남어, 일어, 유럽어

영어

유럽어

중국어

유럽어

유럽어

유럽어

+
+
+
기능별 설명
+
+
    +
  • +

    번역하기 : 번역된 내용을 오른쪽 창에 표시

    +
  • +
  • +

    복사하기 : 번역 결과 값을 클립보드에 복사

    +
  • +
+
+
+
+
+
+
+

5.7. 파일서비스

+
+

5.7.1. 엑셀 다운로드 및 업로드

+
+
개요
+
+

SDL 6.0은 사용자 관리, 외부 사용자 관리, 역할 관리, 메뉴 사용 이력, 파일 다운로드 이력, 코드 관리, 링크사이트 관리에서 엑셀 다운로드를 지원한다.
+그 외 메뉴에서 엑셀 다운로드 기능을 적용하려면 공통컴포넌트 & 유틸Excel Download Button, Excel Upload Button 매뉴얼을 참고한다.

+
+
+
+
API
+
+
ExcelController.java
+
    +
  1. +

    엑셀 다운로드
    +GET /excel/excel-download

    +
  2. +
  3. +

    엑셀 업로드
    +POST /excel/excel-upload

    +
  4. +
+
+
+
+
엑셀 다운로드
+
+
excel.xml 설정
+
+

sdl-base/src/main/resources/excel 폴더에 다운로드 할 기능의 xml 양식을 만든다.
+다국어 적용(message.properties)이 된다.

+
+
+
    +
  1. +

    대외비 표기여부, 시트보호 여부, 제목

    +
  2. +
+
+
+
+
<CONFIDENTIAL>true</CONFIDENTIAL>   <!--> 대외비 표기여부(false : 태그작성 x) <-->
+<PROTECTION>true</PROTECTION>       <!--> 시트보호 여부(false : 태그작성 x) <-->
+<PASSWORD>sdl</PASSWORD>            <!--> 시트보호 암호(PROTECTION = false : 태그작성 x) <-->
+<TITLE>sdl.excel.user.title</TITLE> <!--> 제목 <-->
+
+
+
+
    +
  1. +

    문서 Comment 출력

    +
    +
      +
    1. +

      코멘트를 여러개 작성 가능

      +
    2. +
    +
    +
  2. +
+
+
+
+
<COMMENTS>
+    <COMMENT>
+        <COMMENT_LABEL>sdl.excel.accessLog.comment</COMMENT_LABEL>  <!--> 코멘트 <-->
+        <COMMENT_FONT_COLOR>10</COMMENT_FONT_COLOR>                 <!--> 코멘트 글자색 <-->
+    </COMMENT>
+    <COMMENT>
+        <COMMENT_LABEL>2번째 코멘트</COMMENT_LABEL>
+        <COMMENT_FONT_COLOR>10</COMMENT_FONT_COLOR>
+    </COMMENT>
+</COMMENTS>
+
+
+
+
    +
  1. +

    헤더

    +
  2. +
+
+
+
+
<HEADERS>
+    <HEADER>
+        <HEADER_LABEL>NO.</HEADER_LABEL>                        <!--> 컬럼명 <-->
+        <ROWSPAN>3</ROWSPAN>                                    <!--> 열병합 <-->
+        <HEADER_FONT_COLOR>8</HEADER_FONT_COLOR>                <!--> 컬럼 글자색 <-->
+        <HEADER_BACKGROUND_COLOR>44</HEADER_BACKGROUND_COLOR>   <!--> 컬럼 배경색 <-->
+    </HEADER>
+    <HEADER>
+        <HEADER_LABEL>sdp.user.label.compName</HEADER_LABEL>
+        <COLSPAN>3</COLSPAN>                                    <!--> 행병합 <-->
+    </HEADER>
+</HEADERS>
+
+
+
+
    +
  1. +

    컬럼

    +
    +
      +
    1. +

      첫번째 Row 에 No. 필드 넣을 경우 No. 필드에 대한 <COLUMN>를 작성하지 않아도 된다.

      +
    2. +
    +
    +
  2. +
+
+
+
+
<COLUMNS>
+    <COLUMN>
+        <FIELD_NAME>compName</FIELD_NAME>           <!--> HEADER_LABEL에 대응하는 엔티티명 <-->
+        <COLUMN_WIDTH>15</COLUMN_WIDTH>             <!--> 컬럼 너비 <-->
+        <CELL_ALIGN>LEFT</CELL_ALIGN>               <!--> 셀 정렬 <-->
+        <COLUMN_LOCKING>true</COLUMN_LOCKING>       <!--> 셀 잠금 여부(false : 태그작성 x) <-->
+        <COLUMN_HIDDEN>true</COLUMN_HIDDEN>         <!--> 셀 숨김 여부(false : 태그작성 x) <-->
+        <COLUMN_TYPE>Number</COLUMN_TYPE>           <!--> 날짜 형식(Date), 숫자 형식(Number)으로 출력 <-->
+        <CELL_FONT_COLOR>10</CELL_FONT_COLOR>       <!--> 컬럼 글자색 <-->
+        <BACKGROUND_COLOR>13</BACKGROUND_COLOR>     <!--> 컬럼 배경색 <-->
+    </COLUMN>
+</COLUMNS>
+
+
+
+ + + + + +
+ + +간혹 다운로드 받은 엑셀에 초록색 경고가 뜨는데 이것은 DB에 문자로 저장된 숫자를 불러오기 때문이다.
+ 따라서 <COLUMN_TYPE>을 'Number’로 하면 엑셀에서도 숫자로 잘 나올 것이다. +
+
+
+
+
+
엑셀 업로드
+
+
excelUploadSample.xml 설정
+
+

SampleExcelUpload.vue 파일에 샘플로 엑셀 업로드가 구현되어 있다.

+
+
+
    +
  • +

    업로드할 데이터의 양식에 맞추어 excel.xml을 만들고 업로드 한다.

    +
    +
      +
    • +

      업로드 excel.xml 파일은 다운로드 excel.xml 파일과 같은 경로에 만든다.

      +
    • +
    • +

      날짜 형식 데이터가 업로드되지 않는 경우 yyyy-MM-dd 형태로 입력하거나 텍스트 서식으로 입력한다.

      +
    • +
    +
    +
  • +
+
+
+
+
+
+

5.7.2. 파일 업/다운로드

+
+
개요
+
+

클라이언트의 파일 업/다운로드 API 호출을 처리한다.

+
+
+
파일 업로드
+
+

단일 파일 업로드를 처리한다. 하나의 파일만 업로드하는 경우가 아니라면 보통은 멀티 파일 업로드를 이용한다.

+
+
+
+
멀티 파일 업로드
+
+
+
+

컨트롤러에서 파일 업로드 서비스를 호출한다.

+
+
+
FileManagerController
+
+
@PostMapping("/resource/attachments/multifile-upload")
+    public List<AttachFile> uploadMultiFile(
+            @Parameter(description = "MultipartFile[]", required = true) @RequestParam(required = true) MultipartFile[] files,
+            @Parameter(description = "다운로드 구분 (컴포넌트 한개 이상일 경우 구분)", required = true) @RequestParam(required = true) String downloadType) {
+
+        return fileManagerService.save(downloadType, Arrays.asList(files));
+    }
+
+
+
+
+
+
+
+

서비스에서 지정된 경로에 파일 리소스를 저장하고, 업로드된 파일 정보를 DB 에 저장한다.

+
+
+
FileManagerServiceImpl
+
+
/**
+ * 기본 업로드 패스 설정 값
+ */
+@Value("${common.upload-path}")
+private String fileUploadPath;
+
+/**
+ * 업로드 루트 하위 폴더 자릿수 설정 값
+ */
+@Value("${common.upload.directory-name-len}")
+private int directoryNameLen;
+
+/**
+ * 사용자지정 업로드 패스 설정 여부
+ */
+@Value("${custom.upload-path.enabled}")
+private boolean customUploadPathEnabled;
+
+/**
+ * 사용자지정 업로드 패스 설정 값
+ */
+@Value("${custom.upload-path}")
+private String customUploadPaths;
+
+@Override
+@Transactional
+public List<AttachFile> save(String downloadType, List<MultipartFile> files) {
+    return this.save(downloadType, null, files);
+}
+
+@Override
+@Transactional
+public List<AttachFile> save(String downloadType, String refId, List<MultipartFile> files) {
+    List<AttachFile> attachFileList = new ArrayList<>();
+
+    files.stream().forEach(file -> {
+        AttachFile attachFile = this.save(downloadType, refId, file);
+        attachFileList.add(attachFile);
+    });
+
+    return attachFileList;
+}
+
+@Override
+@Transactional
+public AttachFile save(String downloadType, String refId, MultipartFile file) {
+    String fileId = this.store(file, downloadType); (1)
+
+    AttachFile attachFile = new AttachFile();
+    attachFile.setDownloadType(downloadType);
+    attachFile.setFilePathName(this.rootUploadPath.toString());
+    attachFile.setFileExtensionName(fileId);
+    attachFile.setFileName(file.getOriginalFilename());
+    attachFile.setFileMimeTypeName(file.getContentType());
+    attachFile.setFileSize(file.getSize());
+    attachFile.setOwnerObjectPkId(refId);
+
+    return fileManagerDao.insertFileInfo(attachFile);   (2)
+}
+
+
+
+ + + + + + + + + +
1파일 리소스 저장
2업로드된 파일 정보 DB저장
+
+
+
+
+
+
+

파일 리소스 저장 경로는 기본 업로드 패스 값(common.upload-path)과 하위 디렉토리 길이 설정 값(common.upload.directory-name-len)에 따라 결정된다.
+기본 업로드 패스외에 사용자정의 업로드 패스 설정도 가능하다.

+
+
+
config.properties
+
+
## File Attach Configuration
+common.upload-path=/NAS/SDL/upload  (1)
+common.upload.directory-name-len=2  (2)
+# custom upload path 설정
+custom.upload-path.enabled=false    (3)
+custom.upload-path=\                (4)
+notice=/NAS/SDL/upload/notice,\
+faq=/NAS/SDL/upload/faq
+
+
+
+ + + + + + + + + + + + + + + + + +
1기본 업로드 패스 설정
2업로드 패스 하위 디렉토리 길이
+파일명(UUID)에서 이 길이 만큼 잘라서 기본 업로드 패스 하위 디렉토리가 생성된다.
3사용자정의 업로드 패스 사용 여부
4파일 컴포넌트별 사용자정의 업로드 패스 설정 (custom.upload-path.enabled=true 일 경우 적용됨)
+
+
+
+
+
+
파일 다운로드
+
+

컨트롤러에서 파일 다운로드 서비스를 호출한다.

+
+
+
FileManagerController
+
+
@GetMapping("/resource/attachments/file-download/{fileId}")
+public ResponseEntity<Resource> downloadFile(
+        @Parameter(description = "File ID", required = true) @PathVariable String fileId,
+        @Parameter(description = "다운로드 구분 (컴포넌트 한개 이상일 경우 구분)", required = true) @RequestParam(required = true) String downloadType,
+        HttpServletRequest request) {
+
+    long startTime = System.nanoTime();
+
+    if (StringUtils.isBlank(fileId)) {
+        throw new FileManagerException("No File ID");
+    }
+
+    AttachFile attachFile = fileManagerService.getAttachFile(fileId, downloadType);  (1)
+    if (attachFile == null) {
+        throw new FileManagerException("Cannot find file info: " + fileId);
+    }
+    Resource resource = fileManagerService.getResource(attachFile.getFileExtensionName(), attachFile.getFilePathName());  (2)
+
+    String contentType = attachFile.getFileMimeTypeName();
+    if (StringUtils.isBlank(contentType)) {
+        contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
+    }
+
+    String fileName = attachFile.getFileName(); (3)
+    String encodeFileName = null;
+    // 다운로드 파일명 UTF-8 인코딩
+    encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20");
+
+    // 다운로드 이력 로깅
+    fileManagerService.saveFileDownloadLog(fileName, startTime, request);  (4)
+
+    return ResponseEntity.ok()
+            .contentType(MediaType.parseMediaType(contentType))
+            .header("Content-Transfer-Encoding", "binary")
+            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + encodeFileName + "\"")
+            .body(resource);
+}
+
+
+
+ + + + + + + + + + + + + + + + + +
1파일 정보 조회
2파일 리소스 가져오기
3파일 업로드 시점의 파일명
4파일 다운로드 이력 로깅
+
+
+
+
멀티 파일 다운로드
+
+

여러 파일을 zip 파일 형태로 다운로드할 수 있도록 제공한다.

+
+
+
FileManagerController
+
+
    @Value("${common.download.zipfilename}")
+    private String zipFileName;
+
+    @GetMapping("/resource/attachments/multifile-download")
+    public ResponseEntity<ByteArrayResource> downloadMultiFile(
+            @Parameter(description = "File IDs", required = true) @RequestParam(required = true) String[] fileIds,
+            @Parameter(description = "다운로드 구분 (컴포넌트 한개 이상일 경우 구분)", required = true) @RequestParam(required = true) String downloadType,
+            @Parameter(description = "zip 파일명", required = false) @RequestParam(required = false) String zipFileName,
+            HttpServletRequest request) {
+
+    	long startTime = System.nanoTime();
+
+        if (fileIds == null || fileIds.length == 0) {
+            throw new FileManagerException("No File ID(s)");
+        }
+
+        if (StringUtils.isBlank(zipFileName)) {
+            zipFileName = this.zipFileName;
+        }
+
+        // 다운로드 파일명 UTF-8 인코딩
+		String fileName = null;
+        fileName = URLEncoder.encode(zipFileName, StandardCharsets.UTF_8).replace("+", "%20");
+
+        // 다운로드 이력 로깅
+        fileManagerService.saveFileDownloadLog(zipFileName, startTime, request);
+
+        ByteArrayResource baResource = fileManagerService.getByteArrayResource(fileIds, downloadType);
+
+        return ResponseEntity.ok()
+                .contentLength(baResource.contentLength())
+                .contentType(MediaType.parseMediaType("application/zip"))
+                .header("Content-Transfer-Encoding", "binary")
+                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + ".zip\";")
+                .body(baResource);
+    }
+
+
+
+
+
+
+
+
+
+

6. Appendix

+
+
+

6.1. Quartz Clustering with JDBC-JobStore

+
+

6.1.1. JDBC-JobStore DB 초기화

+
+

quartz db schema (배포된 sdl-appendix 내 참조) 를 사용하는 DBMS에 맞는 SQL을 실행해 JDBC-JobStore를 사용하기 위한 Table을 생성한다.

+
+
+ + + + + +
+ + +Tibero DB는 미지원 +
+
+
+
+

6.1.2. application.properties

+
+

properties 파일 내용 중 Quartz JDBC-JobStore 관련 설정이다.

+
+
+
+
# Spring Quartz 설정
+spring.quartz.job-store-type=jdbc
+spring.quartz.jdbc.initialize-schema=always
+
+spring.quartz.datasource.driver-class-name=org.postgresql.Driver
+spring.quartz.datasource.jdbcUrl=jdbc:postgresql://10.40.87.189:5445/SDL
+spring.quartz.datasource.username=sdl5
+spring.quartz.datasource.password=dlatl#123
+
+spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate (1)
+spring.quartz.properties.org.quartz.jobStore.isClustered=true
+spring.quartz.properties.org.quartz.jobStore.misfireThreshold=60000
+spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval=15000
+spring.quartz.properties.org.quartz.scheduler.instanceName=sdl-prod (2)
+spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO (3)
+spring.quartz.properties.org.quartz.scheduler.rmi.export=false
+spring.quartz.properties.org.quartz.scheduler.rmi.proxy=false
+spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
+spring.quartz.properties.org.quartz.threadPool.threadCount=10 (4)
+spring.quartz.properties.org.quartz.threadPool.threadPriority=5
+spring.quartz.properties.org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true
+spring.quartz.properties.org.quartz.scheduler-name=QuartzScheduler
+
+
+
+ + + + + + + + + + + + + + + + + +
1DB 에 따라 Delegate 를 변경한다. +
+

org.quartz.impl.jdbcjobstore.oracle.OracleDelegate,
+org.quartz.impl.jdbcjobstore.MSSQLDelegate,
+org.quartz.impl.jdbcjobstore.PostgreSQLDelegate

+
2Quartz Scheduler 를 구별하는 Property 값으로 Clustering 할 서버에서는 같은 값을 세팅한다.
3Unique 한 값이어야 하며 AUTO 일 경우 자동 생성된다.
4Job 을 실행하기 위한 Thread 수
+
+
+

기타 자세한 Property 값은 Quartz 메뉴얼을 참고 한다.

+
+
+
+

6.1.3. QuartzClusteringConfig

+
+

QuartzClusteringConfig 파일은 Spring 에 등록된 CronTriggerFactoryBean 을 실행 하도록 SchedulerFactoryBean이 정의되어 있다.

+
+
+
+
@Bean
+public SchedulerFactoryBean setSchedulerFactoryBean(ApplicationContext applicationContext) {
+    SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
+
+    AutoWiringSpringBeanJobFactory jobFactory = new AutoWiringSpringBeanJobFactory();
+    jobFactory.setApplicationContext(applicationContext);
+
+    schedulerFactoryBean.setJobFactory(jobFactory); (1)
+    schedulerFactoryBean.setDataSource(quartzDataSource()); (2)
+    schedulerFactoryBean.setApplicationContext(applicationContext);
+    schedulerFactoryBean.setTransactionManager(quartzTransactionManager()); (3)
+    schedulerFactoryBean.setWaitForJobsToCompleteOnShutdown(true);
+    schedulerFactoryBean.setStartupDelay(60);
+    schedulerFactoryBean.setOverwriteExistingJobs(true);
+
+    Properties properties = new Properties();
+    properties.putAll(quartzProperties.getProperties()); (4)
+    schedulerFactoryBean.setQuartzProperties(properties);
+
+    schedulerFactoryBean.setTriggers(
+            knoxSyncBatchConfig.knoxApprovalSyncTrigger().getObject(),
+            userBatchConfig.batchUserLongTermCheckTrigger().getObject(),
+            userBatchConfig.batchUserAuthExpiredTrigger().getObject(),
+            sysUseLogBatchConfig.batchSysUseLogTrigger().getObject(),
+            sysUseLogBatchConfig.batchMenuUseHistoryTrigger().getObject()
+    ); (5)
+
+    return schedulerFactoryBean;
+}
+
+@Bean
+@ConfigurationProperties(prefix = "spring.quartz.datasource")
+@QuartzDataSource (2)
+public DataSource quartzDataSource() {
+    return DataSourceBuilder.create().type(HikariDataSource.class).build();
+}
+
+@Bean
+@QuartzTransactionManager (3)
+public DataSourceTransactionManager quartzTransactionManager() {
+    return new DataSourceTransactionManager(quartzDataSource());
+}
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
1Spring Bean을 AutoWiring
2Job 정보를 조회, 저회 저장할 때 사용하는 Datasource (resources-dev, resources-prod/application.properties 참조)
3Quartz 전용 TransactionManager
4Quartz 설정 파일 세팅
5SchedulerFactoryBean 에서 실행 할 Trigger
+
+
+
+

6.1.4. JobDetail 및 Trigger Bean 등록

+
+

아래는 KnoxSyncBatchConfig에 수행할 Job 클래스를 설정한 JobDetail Bean과 Trigger Bean을 등록한 예이다.

+
+
+
KnoxSyncBatchConfig.java
+
+
@Value("${knox.approval.sync.cron}")
+private String knoxApprovalSyncCron;
+
+@Bean
+public JobDetail knoxApprovalSyncJobDetail() {
+    return JobBuilder
+            .newJob(KnoxSyncBatchExecutor.class) (1)
+            .withIdentity("knoxApprovalSyncJob")
+            .withDescription("Knox Approval Sync Batch")
+            .storeDurably(true)
+            .build();
+}
+
+@Bean
+public CronTriggerFactoryBean knoxApprovalSyncTrigger() {
+    CronTriggerFactoryBean approvalTrigger = new CronTriggerFactoryBean();
+    approvalTrigger.setJobDetail(knoxApprovalSyncJobDetail());
+    approvalTrigger.setCronExpression(knoxApprovalSyncCron);
+    return approvalTrigger;
+}
+
+
+
+ + + + + +
1배치잡 실행시 QuartzJobBean을 상속 받은 KnoxSyncBatchExecutor의 executeInternal 메소드가 실행된다.
+
+
+
+

6.1.5. Job 구현

+
+

QuartzJobBean은 Spring Bean을 DI Job의 구현체에서 사용할 수 있도록 Job을 구현한 추상 클래스이며, +QuartzJobBean을 상속 받아 executeInternal 메소드를 오버라이드하여 실행할 배치 Service의 메소드를 호출한다.

+
+
+
KnoxSyncBatchExecutor.java
+
+
@Component
+@Log4j2
+public class KnoxSyncBatchExecutor extends QuartzJobBean {
+
+    @Value("${node-id}")
+    private String nodeId;
+
+    @Autowired
+    private KnoxApprovalSyncService knoxApprovalSyncService;
+
+    @Override
+    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
+        JobDetail jobDetail = jobExecutionContext.getJobDetail();
+        log.info(new StringBuilder()
+                .append("nodeId : ").append(nodeId).append(", ")
+                .append("jobName : ").append(jobDetail.getKey().getName()).append(", ")
+                .append("description : ").append(jobDetail.getDescription())
+        );
+        knoxApprovalSyncService.synchronizeKnox();
+    }
+}
+
+
+
+ + + + + +
+ + +UserBatchExecutor의 경우 JobKey(JobBuilder.withIdentity 설정 값)를 이용해 분기 처리 하고 있으므로 Job 등록시 Name 명 지정에 주의하도록 한다. +
+
+
+
UserBatchConfig.java
+
+
/**
+ * 장기 미사용자 관리 Job
+ */
+@Bean
+public JobDetail batchUserLongTermCheckJob() {
+     return JobBuilder
+            .newJob(UserBatchExecutor.class)
+            .withIdentity("batchUserLongTermCheck")
+            .withDescription("User LongTerm Check Batch")
+            .storeDurably(true)
+            .build();
+}
+
+/**
+ * 만료 권한 삭제 Job
+ */
+@Bean
+public JobDetail batchUserAuthExpiredJob() {
+    return JobBuilder
+            .newJob(UserBatchExecutor.class)
+            .withIdentity("batchUserAuthExpired")
+            .withDescription("User Auth Expired Batch")
+            .storeDurably(true)
+            .build();
+}
+
+
+
+
UserBatchExecutor.java
+
+
@Override
+protected void executeInternal(JobExecutionContext jobExecutionContext) {
+    JobDetail jobDetail = jobExecutionContext.getJobDetail();
+    String jobName = jobDetail.getKey().getName();
+    log.info(new StringBuilder()
+            .append("nodeId : ").append(nodeId).append(", ")
+            .append("jobName : ").append(jobName).append(", ")
+            .append("description : ").append(jobDetail.getDescription())
+    );
+    switch (jobName) {
+        case "batchUserLongTermCheck" -> userBatchService.execUserLongTermCheck();
+        case "batchUserAuthExpired" -> userBatchService.execUserAuthValidCheck();
+        case "batchUserAuthExpiredMailing" -> {
+            try {
+                userBatchService.execUserAuthValidAlarmMail();
+            } catch (IOException | MessagingException e) {
+                log.error(e.getMessage(), e);
+            }
+        }
+        default -> {
+        }
+    }
+}
+
+
+
+
+
+

6.2. Properties 암호화 툴 사용 방법

+
+

sdl-encrypt는 Spring Framework에서 사용하는 properties를 Jasypt를 이용해 쉽게 암호화 하기 위한 모듈이다.

+
+
+

6.2.1. 사용 방법

+
+
파일다운로드
+
+

sdl-encrypt-1.0.0.jar 파일을 다운로드 받아서 특정 디렉토리에 복사한다. (배포된 sdl-appendix 내 참조)

+
+
+

sdl-encypt-1.0.0.jar 파일을 실행하기 위해서는 JDK 8 이상의 버전이 설치 되어 있어야 한다.

+
+
+
+
C:\encrypt>dir
+ C 드라이브의 볼륨에는 이름이 없습니다.
+ 볼륨 일련 번호: 1841-973A
+
+ C:\encrypt 디렉터리
+
+2022-08-09  오후 02:17    <DIR>          .
+2022-08-09  오후 02:17    <DIR>          ..
+2022-08-09  오후 02:15         9,114,144 sdl-encrypt-1.0.0.jar
+
+
+
+
+
+
암호화 key 파일 생성
+
+

같은 폴더에 암호화를 위한 key 파일을 sdl-encrypt-1.0.0.jar 파일을 있는 폴더에 생성한다.

+
+
+

key파일의 이름은 jasypt.key 로 한다. jasypt.key 파일은 어플리케이션 실행 시 서버에서 읽어 복호화 할때 필요하다.

+
+
+

jasypt.key 파일의 내용이 유출 될 경우 문제가 생길 수 있으니 형상관리에 저장하지 않도록 한다.

+
+
+

jasypt.key 샘플

+
+
+
+
fhasiuk23dfjhasoijcnaoijfoase0342ruq0
+
+
+
+

jasypt.key 파일의 내용은 프로젝트에서 랜덤한 문자열로 생성한다.

+
+
+
+
+
Properties 파일 생성
+
+

config.properties 파일에 암호화가 필요한 properties를 작성해 sdl-encrypt-1.0.0.jar 파일이 있는 폴더에 생성한다.

+
+
+

config.properties 파일 샘플

+
+
+
+
# Datasource(Oracle)
+datasource.driver=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
+datasource.url=jdbc:log4jdbc:oracle:thin:@10.127.72.226:1521:XE
+datasource.username=sdl5
+datasource.password=dlatl#00
+
+
+
+
+
+
암호화 하기
+
+

실행 전에 위의 모든 파일들이 같은 폴더에 위치하는지 확인한다.

+
+
+
+
C:\encrypt>dir
+ C 드라이브의 볼륨에는 이름이 없습니다.
+ 볼륨 일련 번호: 1841-973A
+
+ C:\encrypt 디렉터리
+
+2022-08-09  오후 02:29    <DIR>          .
+2022-08-09  오후 02:29    <DIR>          ..
+2022-08-08  오후 05:14               407 config.properties
+2022-08-09  오후 02:29                37 jasypt.key
+2022-08-09  오후 02:15         9,114,144 sdl-encrypt-1.0.0.jar
+               3개 파일           9,114,588 바이트
+
+
+
+

java -jar sdl-encrypt-1.0.0.jar 실행

+
+
+
+
C:\encrypt>java -jar sdl-encrypt-1.0.0.jar
+
+  .   ____          _            __ _ _
+ /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
+( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
+ \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
+  '  |____| .__|_| |_|_| |_\__, | / / / /
+ =========|_|==============|___/=/_/_/_/
+ :: Spring Boot ::                (v2.7.2)
+
+2022-08-09 14:30:33.152  INFO 47544 --- [           main] com.samsung.EncryptApplication           : Starting EncryptApplication v1.0.0 using Java 1.8.0_341 on DESKTOP-BBNYDORY with PID 47544 (C:\encrypt\sdl-encrypt-1.0.0.jar started by bbnydory in C:\encrypt)
+2022-08-09 14:30:33.157  INFO 47544 --- [           main] com.samsung.EncryptApplication           : No active profile set, falling back to 1 default profile: "default"
+2022-08-09 14:30:33.456  INFO 47544 --- [           main] ptablePropertiesBeanFactoryPostProcessor : Post-processing PropertySource instances
+2022-08-09 14:30:33.458  INFO 47544 --- [           main] c.u.j.EncryptablePropertySourceConverter : Skipping PropertySource configurationProperties [class org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource
+2022-08-09 14:30:33.461  INFO 47544 --- [           main] c.u.j.EncryptablePropertySourceConverter : Converting PropertySource systemProperties [org.springframework.core.env.PropertiesPropertySource] to EncryptableMapPropertySourceWrapper
+2022-08-09 14:30:33.461  INFO 47544 --- [           main] c.u.j.EncryptablePropertySourceConverter : Converting PropertySource systemEnvironment [org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor$OriginAwareSystemEnvironmentPropertySource] to EncryptableSystemEnvironmentPropertySourceWrapper
+2022-08-09 14:30:33.463  INFO 47544 --- [           main] c.u.j.EncryptablePropertySourceConverter : Converting PropertySource random [org.springframework.boot.env.RandomValuePropertySource] to EncryptablePropertySourceWrapper
+2022-08-09 14:30:33.464  INFO 47544 --- [           main] c.u.j.EncryptablePropertySourceConverter : Converting PropertySource Config resource 'class path resource [application.properties]' via location 'optional:classpath:/' [org.springframework.boot.env.OriginTrackedMapPropertySource] to EncryptableMapPropertySourceWrapper
+2022-08-09 14:30:33.500  INFO 47544 --- [           main] c.u.j.filter.DefaultLazyPropertyFilter   : Property Filter custom Bean not found with name 'encryptablePropertyFilter'. Initializing Default Property Filter
+2022-08-09 14:30:33.551  INFO 47544 --- [           main] c.u.j.r.DefaultLazyPropertyResolver      : Property Resolver custom Bean not found with name 'encryptablePropertyResolver'. Initializing Default Property Resolver
+2022-08-09 14:30:33.553  INFO 47544 --- [           main] c.u.j.d.DefaultLazyPropertyDetector      : Property Detector custom Bean not found with name 'encryptablePropertyDetector'. Initializing Default Property Detector
+2022-08-09 14:30:33.611  INFO 47544 --- [           main] com.samsung.EncryptApplication           : Started EncryptApplication in 0.765 seconds (JVM running for 1.669)
+2022-08-09 14:30:34.114  INFO 47544 --- [           main] com.samsung.EncryptService               : datasource.username=ENC(fOdjJKVr6fkmdS46JlsCRw==)
+2022-08-09 14:30:34.115  INFO 47544 --- [           main] com.samsung.EncryptService               : datasource.password=ENC(xfWl/qeA7tIMQ10UyU7IInz+SyY6ovX9)
+2022-08-09 14:30:34.117  INFO 47544 --- [           main] com.samsung.EncryptService               : datasource.url=ENC(Ocp24DAK1cq0f8TezLyMpcedSu6nLdTACSIYyMoCnuanSJT5x76lw8yM0reAeu4qBrBF+yvItVyCBmtPlIv70A==)
+2022-08-09 14:30:34.119  INFO 47544 --- [           main] com.samsung.EncryptService               : datasource.driver=ENC(GJ527jBO/7Xjilg8WbQHk55GwVUAvc6fIa1Yai1o68WeimiE5BpHNj3e7YnjhtQR)
+
+
+
+
+
+
확인하기
+
+

암호화가 정상적으로 실행 됐다면 같은 폴더에 config-enc.properties 파일이 생성된 것을 확인 할 수 있다.

+
+
+
+
C:\encrypt>dir
+ C 드라이브의 볼륨에는 이름이 없습니다.
+ 볼륨 일련 번호: 1841-973A
+
+ C:\encrypt 디렉터리
+
+2022-08-09  오후 02:30    <DIR>          .
+2022-08-09  오후 02:30    <DIR>          ..
+2022-08-09  오후 02:30               305 config-enc.properties
+2022-08-08  오후 05:14               407 config.properties
+2022-08-09  오후 02:29                37 jasypt.key
+2022-08-09  오후 02:15         9,114,144 sdl-encrypt-1.0.0.jar
+               4개 파일           9,114,893 바이트
+
+
+
+

config-enc.properties 파일내용

+
+
+
+
datasource.username=ENC(fOdjJKVr6fkmdS46JlsCRw==)
+datasource.password=ENC(xfWl/qeA7tIMQ10UyU7IInz+SyY6ovX9)
+datasource.url=ENC(Ocp24DAK1cq0f8TezLyMpcedSu6nLdTACSIYyMoCnuanSJT5x76lw8yM0reAeu4qBrBF+yvItVyCBmtPlIv70A==)
+datasource.driver=ENC(GJ527jBO/7Xjilg8WbQHk55GwVUAvc6fIa1Yai1o68WeimiE5BpHNj3e7YnjhtQR)
+
+
+
+
+
+
+

6.2.2. 고급 사용

+
+

sdl-encrypt 모듈을 Spring Boot 프로젝트로 되어 있고 application.properties 파일에 암호화 key파일, Properties 파일, 출력파일에 대한 경로가 작성되어 있다.

+
+
+

application.properties 내용

+
+
+
+
algorithm=PBEWithMD5AndDES
+keyObtentionIterations=1000
+pollSize=1
+stringOutputType=base64
+keyFilePath=./jasypt.key
+propertiesFilePath=./config.properties
+outputPropertiesFilePath=./config-enc.properties
+
+
+
+

위 내용의 변경이 필요하면

+
+
+

sdl-encrypt-1.0.0.jar 파일이 있는 위치에 application.properties 파일을 새로 작성하고 위의 값들을 오버라이딩해서 사용할 수 있다.

+
+
+
+
+

6.3. UI 라이브러리(uiSDL)

+
+

6.3.1. UI 라이브러리(uiSDL)란?

+
+

UI라이브러리(uiSDL)은 웹 UI 개발 리드타임 단축을 위하여 실무에 유용한 컴포넌트들을 담은 UI라이브러리입니다.
+Vue2 및 Vue3 용 라이브러리가 나뉘어져 있습니다.

+
+
+ + + + + +
+ + +UI라이브러리 시스템 접속
+vue2 : http://uisdl.scp.samsung.net/v2/
+vue3 : http://uisdl.scp.samsung.net/v3/ +
+
+
+ + + + + +
+ + +접속이 안되는 경우 window host 파일에 아래와 같이 추가해 주시기 바랍니다
+10.195.53.147 uisdl.scp.samsung.net +
+
+
+
+

6.3.2. 컴포넌트 주요 특징

+
+
    +
  • +

    삼성전자 웹 시스템 개발 시 빠르게 웹 화면을 개발할 수 있도록 사용빈도가 높은 UI 환경 구성 요소들을 오픈 소스 Framework인 Vue.js와 Bootstrap 기반으로 제작하여 제공합니다.

    +
  • +
  • +

    시스템의 특성에 맞게 제작이 가능하도록 화면의 구조인 레이아웃 6종과 단일 UI구성요소인 컴포넌트 37종으로 구성되어 있습니다

    +
    +
    +uisdl 1 +
    +
    +
  • +
  • +

    각 페이지 화면의 우측 상단에 개발과 디자인 탭으로 구분되어 있습니다.

    +
    +

    개발 탭에서는 개발 가이드와 간단한 기능 테스트를 해볼 수 있는 Code Demo 가 있고, 디자인 탭에서는 전사 UX 표준을 바탕으로 디자인 원칙과 구성요소들을 확인할 수 있습니다.

    +
    +
    +
    +uisdl 2 +
    +
    +
  • +
+
+
+
+

6.3.3. 컴포넌트 개발 환경

+
+
    +
  • +

    VueJS 버전 3

    +
  • +
  • +

    Used Open-source

    +
    +
      +
    • +

      bootstrap: 5.3.0 or Higher

      +
    • +
    • +

      vue-datepicker-next: 1.0.3 or Higher

      +
    • +
    +
    +
  • +
+
+
+
+

6.3.4. 컴포넌트 적용 방법

+
+
    +
  1. +

    Install from Local file

    +
    +

    로컬 파일로부터 컴포넌트를 설치할 수 있습니다. 아래의 경로에서 파일을 다운로드 받습니다.

    +
    + +
    +

    해당 파일을 로컬의 임의의 위치로 복사한 다음, NPM 명령어로 컴포넌트를 설치합니다. 설치하실 프로젝트 루트 폴더로 이동한 다음, 아래 명령어를 커맨드 창에서 입력합니다.

    +
    +
    +
    +
    npm install --save ./{your-path}/sdl-component-{version}.tgz
    +
    +
    +
  2. +
  3. +

    Import bootstrap

    +
    +

    'bootstrap' 관련 스타일과 스크립트를 Entry Point 파일에 Import 합니다.

    +
    +
    +
    +
    // bootstrap css등록 및 popper가 포함된 bootstrap bundle js 임포트
    +import 'bootstrap/dist/css/bootstrap.min.css';
    +import 'bootstrap/dist/js/bootstrap.bundle.min';
    +
    +
    +
  4. +
  5. +

    Import sdl-component

    +
    +

    'sdl-component' 관련 스타일과 스크립트를 Entry Point 파일에 Import 하고

    +
    +
    +
    +
    import 'sdl-component/src/assets/css/custom.css';
    +import SdlComponent from 'sdl-component';
    +
    +
    +
    +

    전역 Vue(app)에서 사용할 수 있도록 Plugin을 등록합니다.

    +
    +
    +
    +
    app.use(SdlComponent);
    +
    +
    +
  6. +
  7. +

    Complete

    +
    +

    이제 시스템 전역에서 'sdl-component’를 사용하실 수 있습니다

    +
    +
  8. +
+
+
+
+

6.3.5. UI 라이브러리 목록

+
+

활용빈도, 사용성을 고려하여 사내시스템 개발에 필요한 Library를 제공합니다.

+
+
+
    +
  • +

    레이아웃

    +
    +

    화면의 구조를 정의하는 레이아웃 유형

    +
    + +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NoLibrary설명

    1

    GNB + Contents

    GNB와 콘텐츠로 구성된 화면 형태

    2

    GNB + LNB + Contents

    GNB, LNB, 콘텐츠로 구성된 화면 형태

    3

    GNB + LNB + Contents + Footer

    GNB, LNB, Footer, 콘텐츠로 구성된 화면 형태

    4

    GNB + LNB + Aside + Contents + Footer

    GNB, LNB, Footer, Aside, 콘텐츠로 구성된 화면 형태

    5

    Popup

    현재 화면에서 추가적으로 띄우는 레이어 형태의 창으로, 사용 유형에 따라 구별하여 사용

    6

    Center Frame

    본 화면의 상하좌우 중앙에 콘텐츠 박스가 위치

    +
  • +
  • +

    컴포넌트

    +
    +

    화면을 구성하는 요소 단위

    +
    + +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NoLibrary설명

    1

    GNB

    (Global Navigation Bar) 시스템 명과 메뉴를 포함한 네비게이션으로 화면의 최 상단에 구성

    2

    LNB

    (Left Navigation Bar) 서브메뉴라고 불리며 화면의 좌측에 구성

    3

    Aside

    자주 사용하는 기능을 빠르게 접근(Quick Access)할 수 있도록 제공하는 영역

    4

    Footer

    Copyright, 사이트 맵, 이용약관 등의 정보를 포함하며, 화면의 하단에 구성

    5

    Card List

    한 레이아웃에서 여러 개의 카드를 함께 사용할 때 사용

    6

    Search Grid

    검색조건을 입력/선택하는 조회 영역과 데이터결과를 호출하는 리스트영역으로 구성할 때 사용

    7

    Form

    여러 개의 컨트롤(Input Box, Radio Button, Dropdown Box 등)를 활용하여 하나의 입력 폼으로 구성할 때 사용

    8

    Form Detail

    폼 화면에서 입력한 정보를 화면에 표시할 때 사용

    9

    Board Detail

    게시판 게시물의 기본적인 상세 화면 

    10

    Tree Detail

    계층 구조를 나타내는 트리 요소와 트리에서 선택한 정보를 페이지 전환 없이 즉각적으로 보여줄 때 사용

    11

    Create Board

    게시판 게시물의 기본적인 입력 화면

    12

    Approval

    시스템에서 Knox 결재나 시스템 내부 결재시 사용

    13

    Reply

    간단하게 사용자 간의 의사소통을 할 수 있는 폼 형태로 구성

    14

    Notice

    사용자에게 시스템 내 서비스 관련 정보들을 공지 시 사용

    15

    Alert

    공지나 안내 사항 같은 부가적인 정보, 경고 상태 혹은 사용자의 행동에 대한 즉각적인 피드백이 필요할 때 사용

    16

    Badge

    알림 목적으로 사용하거나, 데이터를 시각적으로 강조하고 싶을 때 사용

    17

    Button

    사용자가 클릭하여 기능을 수행할 수 있는 그래픽 요소

    18

    Card

    다양한 정보들을 그룹화하여 하나의 카드 모양 안에 구성할 때 사용

    19

    Collapse(Toggle)

    사용자가 두개의 반대되는 상태 중에서 선택할 수 있는 온오프 스위치

    20

    Tooltip

    페이지 요소 또는 기능에 대한 추가 정보를 제공하는 간단하고 유용한 메시지

    21

    Text Input

    입력 폼 요소 중 한 줄의 비교적 짧은 데이터를 입력할 경우 사용

    22

    Textarea

    입력 폼 요소 중 데이터의 내용이 일정하지 않거나, 여러 줄의 데이터를 입력할 경우 사용

    23

    Radio

    두개 이상의 항목 중 하나를 선택할 때 사용

    24

    Select

    사용자가 옵션 중 하나 또는 다중으로 선택할 수 있을 때 사용

    25

    Checkbox

    여러 개의 항목 중 하나 이상을 다중 선택할 때 사용하거나 On/Off의 Toggle 경우에 사용

    26

    Table

    데이터/정보를 열과 행으로 구분하여 사용자가 쉽게 읽고 이해할 수 있도록 제공

    27

    Progress

    사용자 액션에 대한 즉각 데이터가 표시가 어려운 경우(데이터의 로딩 처리 시간이 긴 경우) 작업의 진행 상태에 대한 정확한 피드백을 제공하기 위해 사용

    28

    Spinner

    사용자 액션에 대한 즉각 데이터가 표시가 어려운 경우(데이터의 로딩 처리 시간이 긴 경우) 작업의 진행 상태에 대한 정확한 피드백을 제공하지 못할 경우 사용

    29

    Navbar

    시스템 명과 메뉴를 포함한 네비게이션 영역으로 주로 상단(GNB)에 제공

    30

    File Attachment

    사용자가 첨부파일을 다운받거나 업로드할 경우에 사용

    31

    Tree

    목록의 계층 구조를 표현하기 위해 사용하는 요소

    32

    Datepicker

    날짜를 선택하고 그에 해당하는 데이터를 얻을 수 있도록 위젯 형태로 제공

    33

    Breadcrumb

    시스템 내 사용자의 메뉴의 이동경로를 의미하며 현재 사용자의 위치를 보여줄 때 사용

    34

    Pagination

    많은 양의 데이터를 여러 페이지로 나누고, 이전, 다음 페이지 혹은 특정 페이지로 이동할 수 있는 일련의 링크를 목록 하단에 배치하여 제공

    35

    Tab(Basic, Progress)

    페이지 이동 및 시점 전환 없이 그룹화된 콘텐츠를 제공할 때 사용

    36

    Jumbotron

    어떤 특별한 내용이나 정보를 눈에 띄게 보여주기 위한 박스 형태의 공간

    37

    Carousel

    한 공간에 여러 개의 콘텐츠를 슬라이드 형태로 제공

    +
  • +
+
+
+
+
+

6.4. Redis 설정

+
+

6.4.1. RedisConfig

+
+

SDL은 어플리케이션 속도 향상을 위해 자주 사용하는 Data를 Cache하고 있다. +기본적으로 Local, Dev 환경에서는 Spring에서 제공하고 있는 ConcurrentMapCacheManager를 사용해 +Data를 Cache하고 있지만 Clustering 환경에서 Cache Replicated를 위해서 +Redis를 사용한다.

+
+
+ + + + + +
+ + +Ehcache 3부터 RMI, JMS 방식이 Cache 동기화 지원이 중단 됐으니 Cache 복제가 필요한 경우 Redis를 반드시 사용해야 한다. +
+
+
+

application.properties

+
+
+
+
spring.cache.type=redis
+spring.data.redis.host=redis 서버 주소
+spring.data.redis.port=redis 포트
+
+
+
+

RedisConfig

+
+
+
+
@Bean
+public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
+    return (builder) -> builder
+            .withCacheConfiguration("message-all",
+                    RedisCacheConfiguration.defaultCacheConfig())
+            .withCacheConfiguration("message",
+                    RedisCacheConfiguration.defaultCacheConfig())
+            .withCacheConfiguration("menu-all",
+                    RedisCacheConfiguration.defaultCacheConfig())
+            .withCacheConfiguration("menu-label-all",
+                    RedisCacheConfiguration.defaultCacheConfig())
+            .withCacheConfiguration("menu-full-path-all",
+                    RedisCacheConfiguration.defaultCacheConfig())
+            .withCacheConfiguration("page-full-path-all",
+                    RedisCacheConfiguration.defaultCacheConfig())
+            .withCacheConfiguration("api-full-path-all",
+                    RedisCacheConfiguration.defaultCacheConfig())
+            .withCacheConfiguration("api-user",
+                    RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(300)))
+            .withCacheConfiguration("api-user-menu",
+                    RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(300)))
+            .withCacheConfiguration("page-all-by-menu-auth",
+                    RedisCacheConfiguration.defaultCacheConfig());
+}
+
+
+
+

각 Cache별 필요한 세팅은 redisCacheManagerBuilderCustomizer 메소드에서 할수 있으니 시스템 환경에 맞춰 적절하게 수정한다.

+
+
+
+
+

6.5. D-KMS 암호화 적용

+
+

SDL(표준개발라이브러리)에 D-KMS 암호화를 적용하는 방법에 대해 설명한다.

+
+
+

6.5.1. 사전 준비

+
+
KMS 포털에서 암호화 권한 신청
+
+
    +
  1. +

    KMS 포털에서 계정을 인증하고 암호화 권한 신청, 결재

    +
  2. +
  3. +

    암호화 권한 결재 완료되면 D-KMS SDK 및 가이드 전송됨

    +
  4. +
  5. +

    전달 받은 SDK 설치 및 코드 적용

    +
  6. +
+
+
+
+
+

6.5.2. 코드 적용

+
+
Credential 생성 및 환경변수 추가
+
+
    +
  1. +

    D-KMS 가이드를 참고하여 Credential을 생성하고 시스템 환경변수에 추가한다.

    +
  2. +
+
+
+
+
SDL 수정
+
+
    +
  1. +

    D-KMS 암호화 라이브러리 추가

    +
    + +
    +
  2. +
  3. +

    src/main/resources-{local|dev|prod} 에 dkms.properties 파일을 추가한다. (배포된 sdl-appendix 내 참조)

    +
  4. +
+
+
+ + + + + +
+ + +- dkms.task-id 프로퍼티 값을 D-KMS 신청시 부여받은 값으로 수정해야한다.
+ - 기타 다른 프로퍼티 값도 제대로 설정되었는지 확인한다. +
+
+
+
    +
  1. +

    mybatis typehandler 추가

    +
    +
      +
    • +

      암호화 대상이 되는 컬럼에 대해 typehandler 를 적용한다.

      +
    • +
    • +

      com.samsung.dkms.handler 패키지 생성

      +
    • +
    • +

      EncryptionTypeHandler.java, NameEncryptionTypeHandler.java, EmailEncryptionTypeHandler.java, PhoneEncryptionTypeHandler.java, AddressEncryptionTypeHandler.java, BirthdayEncryptionTypeHandler.java 파일을 복사/붙여넣기 한다. (배포된 sdl-appendix 내 참조)

      +
    • +
    +
    +
  2. +
  3. +

    mybatis-config.xml 수정

    +
    +
      +
    • +

      mapper xml 에 적용이 용이하도록 typehandler의 type alias 를 추가한다.

      +
      +
      +dkms typehandler alias +
      +
      +
    • +
    +
    +
  4. +
+
+
+
    +
  1. +

    mapper xml 에 mybatis typehandler 적용

    +
    +
      +
    • +

      암호화가 필요한 컬럼에 해당하는 핸들러를 선택 적용한다.

      +
    • +
    • +

      mapper xml에 typehandler 적용 방법은 샘플파일(mapper-mybatis-user.xml, 배포된 sdl-appendix 내 참조)을 참고한다. (""유무에 유의)

      +
      +
        +
      • +

        select resultMap 적용 예

        +
        +
        +dkms typehandler select resultmap +
        +
        +
      • +
      • +

        insert, update 적용 예

        +
        +
        +dkms typehandler insert +
        +
        +
        +
        +dkms typehandler update +
        +
        +
      • +
      • +

        수정대상 mapper xml (배포판 기준)

        +
        +
        +
        mapper-mybatis-common.xml
        +mapper-mybatis-sample-approval-internal.xml
        +mapper-mybatis-sample-approval-knox.xml
        +mapper-mybatis-sample.xml
        +mapper-mybatis-sys-resource.xml
        +mapper-mybatis-user-sync.xml
        +mapper-mybatis-approval.xml
        +mapper-mybatis-user-role.xml
        +mapper-mybatis-user.xml
        +mapper-mybatis-workgroup-role.xml
        +mapper-mybatis-comment.xml
        +mapper-mybatis-post.xml
        +mapper-mybatis-email.xml
        +mapper-mybatis-mail-group.xml
        +mapper-mybatis-history.xml
        +mapper-mybatis-access-log.xml
        +mapper-mybatis-admin-address.xml
        +mapper-mybatis-knox-department-sync.xml
        +mapper-mybatis-knox-department.xml
        +mapper-mybatis-terms-use.xml
        +
        +
        +
      • +
      +
      +
    • +
    +
    +
  2. +
+
+
+
+
테이블 수정 및 데이터 마이그레이션
+
+
    +
  1. +

    테이블 컬럼 사이즈 변경

    +
    +
      +
    • +

      암호화 대상 컬럼의 사이즈를 최소 255byte 가 되도록 변경한다.

      +
    • +
    +
    +
  2. +
  3. +

    기존 데이터 암호화를 위한 마이그레이션은 D-KMS 가이드를 참고한다.

    +
  4. +
+
+
+ + + + + +
+ + +SQL기반의 Database의 경우, 개인정보가 암호화됨으로써 아래와 같은 SQL문에 영향이 발생 (D-KMS 소개자료 중 발췌)
+ Nondeterministic Encryption으로 인해 Equality 비교 불가
+ : WHERE, GROUP BY, JOIN (ON), DISTINCT, HAVING, ORDER BY
+ → 일반적으로 Application Level에서 Filtering, Grouping, Joining 구현 필요
+
+ 대안: 별도 Column에 Hash 데이터를 추가 저장
+ Hash로 인해 영향 받는 SQL문: WHERE (비교에 사용하는 데이터 역시 Hash 필요. Equality 비교는 가능하나 LIKE 또는 대소비교 불가), ORDER BY
+ → 단, 모든 데이터가 Hash되어있지 않은 경우 GROUP BY에 영향을 줄 수 있음 +
+
+
+
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/doc/index_edit.html b/doc/index_edit.html new file mode 100644 index 0000000..472aa45 --- /dev/null +++ b/doc/index_edit.html @@ -0,0 +1,34 @@ + + + + + + + + 표준개발라이브러리 + + + + + + + +
+

표준개발라이브러리 심볼표준개발라이브러리

+
+ + + + + + + + + + + \ No newline at end of file diff --git a/doc/js/toc.js b/doc/js/toc.js new file mode 100644 index 0000000..33106ac --- /dev/null +++ b/doc/js/toc.js @@ -0,0 +1,40 @@ +var toctitle = document.getElementById('toctitle'); +var path = window.location.pathname; +if (toctitle != null) { + var oldtoc = toctitle.nextElementSibling; + var newtoc = document.createElement('div'); + newtoc.setAttribute('id', 'tocbot'); + newtoc.setAttribute('class', 'js-toc desktop-toc'); + oldtoc.setAttribute('class', 'mobile-toc'); + oldtoc.parentNode.appendChild(newtoc); + tocbot.init({ + contentSelector: '#content', + headingSelector: 'h1, h2, h3, h4', + positionFixedSelector: 'body', + fixedSidebarOffset: 90, + smoothScroll: false + }); + if (!path.endsWith("index.html") && !path.endsWith("/")) { + var link = document.createElement("a"); + if (document.getElementById('index-link')) { + indexLinkElement = document.querySelector('#index-link > p > a'); + linkHref = indexLinkElement.getAttribute("href"); + link.setAttribute("href", linkHref); + } else { + link.setAttribute("href", "index.html"); + } + link.innerHTML = " Back to index"; + var block = document.createElement("div"); + block.setAttribute('class', 'back-action'); + block.appendChild(link); + var toc = document.getElementById('toc'); + var next = document.getElementById('toctitle').nextElementSibling; + toc.insertBefore(block, next); + } +} + +var headerHtml = ''; + +var header = document.createElement("div"); +header.innerHTML = headerHtml; +document.body.insertBefore(header, document.body.firstChild); \ No newline at end of file diff --git a/doc/js/tocbot/tocbot.css b/doc/js/tocbot/tocbot.css new file mode 100644 index 0000000..a09517c --- /dev/null +++ b/doc/js/tocbot/tocbot.css @@ -0,0 +1 @@ +.toc{overflow-y:auto}.toc>.toc-list{overflow:hidden;position:relative}.toc>.toc-list li{list-style:none}.toc-list{margin:0;padding-left:10px}a.toc-link{color:currentColor;height:100%}.is-collapsible{max-height:1000px;overflow:hidden;transition:all 300ms ease-in-out}.is-collapsed{max-height:0}.is-position-fixed{position:fixed !important;top:0}.is-active-link{font-weight:700}.toc-link::before{background-color:#EEE;content:' ';display:inline-block;height:inherit;left:0;margin-top:-1px;position:absolute;width:2px}.is-active-link::before{background-color:#54BC4B} diff --git a/doc/js/tocbot/tocbot.min.js b/doc/js/tocbot/tocbot.min.js new file mode 100644 index 0000000..2aae837 --- /dev/null +++ b/doc/js/tocbot/tocbot.min.js @@ -0,0 +1 @@ +!function(e){function t(o){if(n[o])return n[o].exports;var l=n[o]={i:o,l:!1,exports:{}};return e[o].call(l.exports,l,l.exports,t),l.l=!0,l.exports}var n={};t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:o})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=0)}([function(e,t,n){(function(o){var l,r,i;!function(n,o){r=[],l=o(n),void 0!==(i="function"==typeof l?l.apply(t,r):l)&&(e.exports=i)}(void 0!==o?o:this.window||this.global,function(e){"use strict";function t(){for(var e={},t=0;te.fixedSidebarOffset?-1===n.className.indexOf(e.positionFixedClass)&&(n.className+=p+e.positionFixedClass):n.className=n.className.split(p+e.positionFixedClass).join("")}function i(t){var n=0;return t!=document.querySelector(e.contentSelector&&null!=t)&&(n=t.offsetTop,e.hasInnerContainers&&(n+=i(t.offsetParent))),n}function s(t){if(e.scrollContainer&&document.querySelector(e.scrollContainer))var n=document.querySelector(e.scrollContainer).scrollTop;else var n=document.documentElement.scrollTop||m.scrollTop;e.positionFixedSelector&&r();var o,l=t;if(h&&null!==document.querySelector(e.tocSelector)&&l.length>0){f.call(l,function(t,r){if(i(t)>n+e.headingsOffset+10){return o=l[0===r?r:r-1],!0}if(r===l.length-1)return o=l[l.length-1],!0});var s=document.querySelector(e.tocSelector).querySelectorAll("."+e.linkClass);d.call(s,function(t){t.className=t.className.split(p+e.activeLinkClass).join("")});var a=document.querySelector(e.tocSelector).querySelectorAll("."+e.listItemClass);d.call(a,function(t){t.className=t.className.split(p+e.activeListItemClass).join("")});var u=document.querySelector(e.tocSelector).querySelector("."+e.linkClass+".node-name--"+o.nodeName+'[href="#'+o.id+'"]');-1===u.className.indexOf(e.activeLinkClass)&&(u.className+=p+e.activeLinkClass);var C=u.parentNode;C&&-1===C.className.indexOf(e.activeListItemClass)&&(C.className+=p+e.activeListItemClass);var v=document.querySelector(e.tocSelector).querySelectorAll("."+e.listClass+"."+e.collapsibleClass);d.call(v,function(t){-1===t.className.indexOf(e.isCollapsedClass)&&(t.className+=p+e.isCollapsedClass)}),u.nextSibling&&-1!==u.nextSibling.className.indexOf(e.isCollapsedClass)&&(u.nextSibling.className=u.nextSibling.className.split(p+e.isCollapsedClass).join("")),c(u.parentNode.parentNode)}}function c(t){return-1!==t.className.indexOf(e.collapsibleClass)&&-1!==t.className.indexOf(e.isCollapsedClass)?(t.className=t.className.split(p+e.isCollapsedClass).join(""),c(t.parentNode.parentNode)):t}function a(t){var n=t.target||t.srcElement;"string"==typeof n.className&&-1!==n.className.indexOf(e.linkClass)&&(h=!1)}function u(){h=!0}var d=[].forEach,f=[].some,m=document.body,h=!0,p=" ";return{enableTocAnimation:u,disableTocAnimation:a,render:n,updateToc:s}}},function(e,t){e.exports=function(e){function t(e){return e[e.length-1]}function n(e){return+e.nodeName.split("H").join("")}function o(t){if(!(t instanceof HTMLElement))return t;if(e.ignoreHiddenElements&&(!t.offsetHeight||!t.offsetParent))return null;var o={id:t.id,children:[],nodeName:t.nodeName,headingLevel:n(t),textContent:e.headingLabelCallback?String(e.headingLabelCallback(t.textContent)):t.textContent.trim()};return e.includeHtml&&(o.childNodes=t.childNodes),e.headingObjectCallback?e.headingObjectCallback(o,t):o}function l(n,l){for(var r=o(n),i=r.headingLevel,s=l,c=t(s),a=c?c.headingLevel:0,u=i-a;u>0;)c=t(s),c&&void 0!==c.children&&(s=c.children),u--;return i>=e.collapseDepth&&(r.isCollapsed=!0),s.push(r),s}function r(t,n){var o=n;e.ignoreSelector&&(o=n.split(",").map(function(t){return t.trim()+":not("+e.ignoreSelector+")"}));try{return document.querySelector(t).querySelectorAll(o)}catch(e){return console.warn("Element not found: "+t),null}}function i(e){return s.call(e,function(e,t){var n=o(t);return n&&l(n,e.nest),e},{nest:[]})}var s=[].reduce;return{nestHeadingsArray:i,selectHeadings:r}}},function(e,t){function n(e){function t(e){return"a"===e.tagName.toLowerCase()&&(e.hash.length>0||"#"===e.href.charAt(e.href.length-1))&&(n(e.href)===s||n(e.href)+"#"===s)}function n(e){return e.slice(0,e.lastIndexOf("#"))}function l(e){var t=document.getElementById(e.substring(1));t&&(/^(?:a|select|input|button|textarea)$/i.test(t.tagName)||(t.tabIndex=-1),t.focus())}!function(){document.documentElement.style}();var r=e.duration,i=e.offset,s=location.hash?n(location.href):location.href;!function(){function n(n){!t(n.target)||n.target.className.indexOf("no-smooth-scroll")>-1||"#"===n.target.href.charAt(n.target.href.length-2)&&"!"===n.target.href.charAt(n.target.href.length-1)||-1===n.target.className.indexOf(e.linkClass)||o(n.target.hash,{duration:r,offset:i,callback:function(){l(n.target.hash)}})}document.body.addEventListener("click",n,!1)}()}function o(e,t){function n(e){i=e-r,window.scrollTo(0,c.easing(i,s,u,d)),i approvalStepList, Map attribute) { + log.debug("execute ApprovalSampleInterceptor afterSubmit"); + //Knox 상신 후 문서 상태 업데이트 + knoxApprovalSampleService.updateSampleApprovalDocument(approval.getDbDocId(), ApprovalDocStatus.INPROCESS); + } + -- 생략 -- +---- \ No newline at end of file diff --git a/doc/공통기능/결재메일/Knox상신.adoc b/doc/공통기능/결재메일/Knox상신.adoc new file mode 100644 index 0000000..e31715e --- /dev/null +++ b/doc/공통기능/결재메일/Knox상신.adoc @@ -0,0 +1,124 @@ += Knox 상신 + +== 결재 Entity Class 생성 및 설정 + +결재 기능을 구현해야 하는 업무의 Entity Class를 생성하고 Annotation을 설정하고 기본 결재 Class를 상속 받는다. + +[source,java] +---- +@Data +@EqualsAndHashCode(callSuper = true) +@ApprovalDocument(docType = "knoxSample", description = "Sample Approval Doc. (Knox)", approvalClass = Approval.class, templateEngine = TemplateEngineType.VELOCITY, + templateFile = "/templates/approval/approval-sample.vm", docSecuType = ApprovalDocSecuType.PERSONAL, + isArbitrary = true, bodyType = ApprovalBodyType.MIME) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class KnoxApprovalSampleDocument extends DefaultApprovalDocument { + + private static final long serialVersionUID = 1L; + + private String docId; + private String contents; + private String creator; +} +---- + +업무프로세스 Entity Class 에 @ApprovalDocument 을 달아 주고 DefaultApprovalDocument를 상속 받아야만 결재 Object 라는 것을 결재 모듈에서 알수 있다. + +=== @ApprovalDocument + +@ApprovalDocument를 이용해 결재 문서의 속정을 정의한다. + +[cols=5] +|==== +|props명 +|필수여부 +|Type +|Default +|설명 + +|docType +|필수 +|String +| +|사용자가 식별할수 있는 이름 + +|description +|필수 +|String +| +|결재 문서에 대한 설명 + +|approvalClass +|필수 +|class +|Approval.class +|결재 문서를 DB에 저장할때 사용하는 클래스명 + +|templateEngine +|필수 +|TemplateEngineType +|VELOCITY +|결재 본문 파싱에 필요한 템플릿 엔진. THYMELEAF, VELOCITY 2개의 템플릿 엔진을 사용할 수 있다. + +|templateFile 또는 templateKey +|필수 +|String +| +|templateFile: 템플릿 파일 이름을 경로와 확장자 포함해서 설정한다. + +tempageKey: 결재양식 관리 메뉴에서 지정한 key + +|docSecuType +| +|ApprovalDocSecuType +|PERSONAL +|결재 문서의 보안 형태 + +|isBodyModify +| +|boolean +|true +|문서의 기본 결재 본문 수정 true일 때만 UI에서 수정 가능 + + +|isRouteModify +| +|boolean +|true +|문서의 기본 결재 경로 수정 true일 때만 UI에서 수정 가능 + +|isArbitrary +| +|boolean +|false +|문서의 기본 결재 전결 true일 때만 UI에서 수정 가능 + +|bodyType +| +|ApprovalBodyType +|MIME +|전송할 문서의 형태 TEXT, HTML, MIME + +|isInternalApproval +| +|boolean +|false +|true: 내부결재, false: Knox결재 + +|==== + +== 결재 본문 등록 + +결재 본문은 VELOCITY 또는 THYMELEAF 로 등록 할 수 있다. + +SDL에서 제공하는 결재 샘플에서는 VELOCITY 파일로 제공하고 있으며 vm 파일을 작성하고, + +위의 Entity Class 샘플에서와 같이 `@ApprovalDocument` 의 `templateEngine=엔진타입` 과 `templateFile="템플릿파일경로"` 값을 등록한다. + + +== 결재 상신 + +=== 결재 상신 UI + +샘플 문서를 등록 하고나면 상세하면 하단에 결재 경로를 설정할 수 있는 결재 스텝을 입력 받는 Component가 표시된다. + +결재 문서 화면 개발시 이 Component를 붙여서 상신 기능을 구현한다. + +image::approvalSumit_01.png[] + diff --git a/doc/공통기능/결재메일/MHTML변환.adoc b/doc/공통기능/결재메일/MHTML변환.adoc new file mode 100644 index 0000000..edda8c3 --- /dev/null +++ b/doc/공통기능/결재메일/MHTML변환.adoc @@ -0,0 +1,34 @@ += MHTML 변환 + +== 개요 +프로젝트에서 메일발송이나 결재상신한 후 방화벽 밖(모바일)에서 조회시 이미지, css가 적용되지 않는 문제를 해결하기 위해 MHT로 변환 + +.MHT 변환 결과 예 +[source, text] +---- +Date: Fri, 9 Aug 2019 14:55:20 +0900 (KST) +Message-ID: <963810424.1.1565330120404@DESKTOP-TUEGPQT> +Subject: =?UTF-8?B?66mU64m0IOq2jO2VnCDrp4zro4wg7JiI7KCVIOyViOuCtA==?= +MIME-Version: 1.0 +Content-Type: multipart/related; + boundary="----=_Part_0_398737318.1565330120140" + +------=_Part_0_398737318.1565330120140 +Content-Type: text/html; charset="utf-8" +Content-Transfer-Encoding: base64 + +PCFkb2N0eXBlIGh0bWw+DQo8aHRtbD4NCjxoZWFkPg0KPG1ldGEgY2hhcnNldD0idXRmLTgiPg0K +PHRpdGxlPuuplOuJtCDqtoztlZwg66eM66OMIOyYiOyglSDslYjrgrQ8L3RpdGxlPg0KPC9oZWFk +Pg0KDQo8Ym9keT4NCjxkaXYgc3R5bGU9IndpZHRoOjEwMCU7YmFja2dyb3VuZC1jb2xvcjojZmZm +(중략) +------=_Part_0_398737318.1565330120140 +Content-Type: application/octet-stream +Content-Transfer-Encoding: base64 +Content-ID: + +iVBORw0KGgoAAAANSUhEUgAAAQEAAAEBCAYAAAB47BD9AAAwaUlEQVR42u3deZxddX3/8QuC4kPt +Rqu2iK222pYW2yogCNbyE81MVRSFIEuAewmBsGQl+zqZmexhExEV0AQCSSDrZLZkJjOZzGRmQhAM +RXYqbrVaFZHJTOCe+/29v+fc7z3nnn35nnPP8v3j9XAeLWVScz/P7/ee73fu5AghuSD90+Pt5Trk +(중략) +------=_Part_0_398737318.1565330120140-- +---- \ No newline at end of file diff --git a/doc/공통기능/결재메일/결재.adoc b/doc/공통기능/결재메일/결재.adoc new file mode 100644 index 0000000..272dd43 --- /dev/null +++ b/doc/공통기능/결재메일/결재.adoc @@ -0,0 +1,220 @@ += 결재 + +== 개요 +SDL에서는 Knox결재와 동기화 되는 결재 모듈을 제공하고 있는데, 여기서는 Knox Portal REST API 연계 서비스 신청 및 Knox결재 서비스 연계 부분에 대하여 설명한다. 결재화면 및 기능구현 설명은 해당 가이드를 참조한다. + +=== Knox결재 연계 설정 +. Knox REST API 연계 서비스 신청이 되었다면, 발급받은 `system-id`, `token` 값을 설정한다. + +.knox.properties (스테이지) +[source,properties] +---- +knox.system-id=xxxxxxxxxxx +knox.token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +knox.address.prefix=openapi.samsung.net +knox.approval-service=/approval/api/v2.0/approvals +---- + +.knox.properties (운영) +[source,properties] +---- +knox.system-id=xxxxxxxxxxx +knox.token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx // <1> +knox.address.prefix=openapi.samsung.net,openapi.w1.samsung.net,openapi.w2.samsung.net # <2> +knox.approval-service=/approval/api/v2.0/approvals +---- +<1> 토큰 (comma(,)로 구분, 거점 순서와 동일) +<2> 거점 (국내, 구주, 미주) + +[start=2] +. 결재화면 및 기능구현 설명은 해당 가이드를 참조한다. + +* <<_knox_상신,Knox상신>> + + +NOTE: 개발자의 IP도 반드시 Knox stage 방화벽에 등록하여야만 개발자 PC에서 상신이 된다. 또한 Knox 스테이지(http://www.stage.samsung.net) 에 개발자와 결재자의 계정도 생성 해야만 개발을 진행 할 수 있다. + +== API +KnoxApprovalController는 Knox에서 제공하는 Approval API를 직접 연결하는 API를 제공한다. +시스템의 비즈니스 로직을 거치지 않고 Knox API를 직접 호출 하기 때문에 서비스 호출에 문제가 있는지 파악하는데 유용하다. + +NOTE: KnoxApprovalController에서 제공하는 API URI은 Knox REST Service의 API URI와 같다. + +. Knox 일반 결재 상신 + +POST /knox/approvals/submit + +* 파라미터는 KnoxApproval 클래스를 참고한다. + +* attachments는 첨부파일, knoxApprovalStr는 KnoxApproval의 Json String 값이다. +[source,java] +---- +@PostMapping("/submit") +public KnoxApproval submitGeneral(@Parameter(value = "첨부파일", required = true) MultipartFile attachments, + @Parameter(value = "상신정보", required = true) String knoxApprovalStr) throws IOException { + + ObjectMapper objectMapper = new ObjectMapper(); + KnoxApproval knoxApproval = objectMapper.readValue(knoxApprovalStr, KnoxApproval.class); + + String serverLocation = Account.currentUser().getServerLocation(); + if (StringUtils.isEmpty(serverLocation)) serverLocation = "KR"; + + String fileId = fileManagerService.store(attachments); + List fileList = new ArrayList<>(); + fileList.add(fileManagerService.getResource(fileId)); + + return knoxApprovalService.submit(knoxApproval, fileList, serverLocation); +} +---- + +[start=2] +. Knox 보안 결재 상신 + +POST /knox/approvals/secu-submit + +* 파라미터는 일반 결재 상신과 같지만 보안문서타입이 "CONFIDENTAIL" 이다. +[source,java] +---- +knoxApproval.setDocSecuType("CONFIDENTAIL"); +---- + +[start=3] +. Knox 결재 상세 상황 조회 + +GET /knox/approvals/{apInfId}/detail + +* 결재 연계 ID로 결재 문서의 정보를 상세 조회한다. + +. Knox 결재 본문 조회 + +GET /knox/approvals/{apInfId}/content + +* 결재 연계 ID로 결재 문서의 본문을 조회한다. + +. Knox 결재 상황 조회 + +POST /knox/approvals/status + +* 결재문서의 진행 상태를 조회한다. +* 복수개의 결재 연계 ID를 요청하여 각각 해당하는 문서변경횟수와 결재상태정보를 응답받는다. 이를 이용하여 결재문서 동기화시 변경된 건에 대해 결재 상태를 업데이트 한다. +[source,java] +---- +public List getStatus(@RequestBody List knoxApprovalStatusList) { +---- + +[start=6] +. Knox 결재 연계 ID 조회 + +POST /knox/approvals/apinfids + +* 결재 ID로 결재 연계 ID를 조회한다. +[source,java] +---- +public KnoxApproval getApInfIds(@RequestParam String apId) { +---- + +[start=7] +. Knox 상신함 리스트 조회 + +POST /knox/approvals/submission + +* 상신자가 상신한 정보를 조회한다. +[source,java] +---- +public List getApInfIdInfos(@RequestParam String epId) { +---- + +[start=8] +. Knox 연계 이력 조회 + +GET /knox/approvals/apinfidinfos + +* 요청 시스템에서 상신된 결재문서의 연계 이력을 조회한다. + +. Knox 상신 취소 + +POST /knox/approvals/{apInfId}/cancel + +* 결재 문서를 상신취소한다. + +. Knox 완결 처리 + +POST /knox/approvals/{apInfId}/autoprogress + +* 결재문서를 완결처리한다. + + +== KnoxApprovalService + +KnoxApprovalService는 시스템에서 결재 문서를 상신 할때 필요한 API들을 제공한다. + +[source, java] +---- +/** + * 일반 상신, 보안 상신 + * @param knoxApproval + * @param attachments + * @param locale + * @return + */ +KnoxApproval submit(KnoxApproval knoxApproval, List attachments, String locale); + +/** + * 취소 + * @param apInfId + * @param locale + * @return + */ +KnoxApproval cancel(String apInfId, String opinion, String locale); + +/** + * 완결 처리 + * @param apInfId + * @param locale + * @return + */ +KnoxApproval autoProgress(String apInfId, String flag, String timeZone, String locale); + +/** + * 결재 상황 조회 + * @param knoxApprovalStatusList + * @param locale + * @return + */ +List getStatus(List knoxApprovalStatusList, String locale); + +/** + * 결재상세상황조회 + * @param apInfId + * @param locale + * @return + */ +KnoxApproval getDetail(String apInfId, String locale); + + +/** + * 결재본문조회 + * @param apInfId + * @param locale + * @return + */ +KnoxApproval getContent(String apInfId, String locale); + +/** + * 결재연계ID조회 + * @param apId + * @return + */ +KnoxApproval getApInfIds(String apId, String locale); + +/** + * 상신함리스트조회 + * @param epId + * @return + */ +List getSubmission(String epId, String locale); + +/** + * 연계이력조회 + * @param endDate yyyyMMddHHmm 형식 + * @param page 페이지 처리 + * @param duration 단위 : 분 / 최소 1분 ~ 최대 60분 + * @return + */ +List getApInfIdInfos(String endDate, String page, String duration, String locale); +---- + +각각의 메서드들은 Knox Rest 연계 서비스에서 요구하는 가이드대로 REST 형식을 갖추어 필요한 로직들을 수행한다. + + + + + + + + + + + + + diff --git a/doc/공통기능/결재메일/결재경로관리.adoc b/doc/공통기능/결재메일/결재경로관리.adoc new file mode 100644 index 0000000..f6d4483 --- /dev/null +++ b/doc/공통기능/결재메일/결재경로관리.adoc @@ -0,0 +1,46 @@ += 결재 경로 관리 + +== 개요 +시스템의 결재 문서 샘플을 RUNTIME 동안 가지고 있다가 결재시에 사용한다. + +* ApprovalManager.java -> ApprovalDocument.java -> SampleApprovalDocument.java +. ApprovalManager : @ApprovalDocument라는 어노테이션이 달린 클래스를 찾는다. +. SampleApprovalDocument : 결재경로 관리 목록에 결재 문서 샘플을 보여준다. + +== Table +* 결재 경로 : TN_CF_DYNAMIC_APPROVAL_PATH +* 필수 결재자 : TN_CF_REQUIRED_APPROVAL_USER + +== API +.ApprovalController.java + +. 시스템 전체 결재 문서 조회 + +GET /approval/approval-doc-types + +. 결재 경로 조회 + +GET /approval/dynamic-approval-paths/{docType} + +Query ID : selectDynamicApprovalPath +* 기본결재 경로 목록을 보여준다. + +. 결재 경로 저장 + +POST /approval/dynamic-approval-paths/{docType} + +Query ID : deleteDynamicApprovalPath, insertDynamicApprovalPath + +. 필수 결재자 목록 조회 + +GET /approval/required-approval-users/{docType} + +Query ID : selectRequiredApprovalUserList +* 필수 결재자 목록을 보여준다. + +. 필수 결재자 저장 + +POST /approval/required-approval-users/{docType} + +Query ID : deleteRequiredApprovalUser, insertRequiredApprovalUser + +== 화면 +지정된 문서타입에 따른 결재경로를 관리기능 > 결재/메일 관리 > 결재경로 관리를 통해 지정할 수 있다. + +image::front_07_04.png[] +* 지정된 문서타입 목록을 확인할 수 있다. + +image::front_07_05.png[] +* 문서타입을 선택 후 해당 문서에 대한 기본 결재경로와 필수 결재자를 추가할 수 있다. +* 결재 상신시 결재자 목록에 지정된 기본결재 경로가 자동 추가되며, 지정된 필수 결재자가 있는 경우 추가하라는 알림을 준다. (다수중 1인 가능) diff --git a/doc/공통기능/결재메일/결재관리.adoc b/doc/공통기능/결재메일/결재관리.adoc new file mode 100644 index 0000000..44daad9 --- /dev/null +++ b/doc/공통기능/결재메일/결재관리.adoc @@ -0,0 +1,35 @@ += 결재 관리 + +== 개요 +결재 관리를 통해 내부/knox 결재 목록을 확인할 수 있다. + +== Table +* 결재 : TN_CF_APPROVAL +* 결재 이벤트 : TN_CF_APPROVAL_EVENT +* 결재 스텝 : TN_CF_APPROVAL_STEP + +== API +.ApprovalController.java + +. 시스템 전체 결재 문서 조회 + +GET /approval/approval-doc-types + +* 결재경로 관리의 문서타입을 가져온다. + +. 시스템 전체 결재 목록 조회(페이징) + +GET /approval/approval-with-paging + +Query ID : selectApprovalPagingList + +. 결재 상세정보 조회 + +GET /approval/{approvalRequestId} + +Query ID : selectApprovalStep, selectApprovalEventList + +== 화면 +image::front_07_01.png[] + +=== 검색 조건 +** 구분 : knox 결재/ 내부결재 +** 상태 : 결재중/완료/후완결/반려/취소 +** 문서명 : 결재양식 목록 +** 상신자 : 결재 요청한 상신자 +** 동기화 상태 : 성공/실패 +** 기간 : 결재요청 기간 diff --git a/doc/공통기능/결재메일/결재양식관리.adoc b/doc/공통기능/결재메일/결재양식관리.adoc new file mode 100644 index 0000000..c62650b --- /dev/null +++ b/doc/공통기능/결재메일/결재양식관리.adoc @@ -0,0 +1,56 @@ += 결재 양식 관리 + +== 개요 +결재 양식을 관리한다. + +== Table +* 템플릿 : TN_CF_TEMPLATE + +== API +.TemplateController.java + +. 템플릿 목록 조회(페이징) + +GET /templates-with-paging/group/{templateGroupCode} + +Query ID : selectTemplatePagingList +* TEMPLATE_GROUP_CODE 컬럼의 'APPROVAL'을 조회한다. + +. 템플릿 상세 조회 + +GET /templates/group/{templateGroupCode}/key/{templateKey} + +Query ID : selectTemplate + +. 템플릿 상세 조회 (By ID) + +GET /templates/{id} + +Query ID : selectTemplate +* 양식의 상세 내용을 조회 하거나 팝업 미리보기를 할 수 있다. + +. 템플릿 Key 중복 체크 + +GET /templates/dup-check/group/{templateGroupCode}/key/{templateKey} + +Query ID : selectTemplate +* 저장 전 템플릿 Key 중복 여부를 검사한다. + +. 템플릿 등록 + +POST /templates/group/{templateGroupCode} + +Query ID : insertTemplate +* 양식을 저장한다. + +. 템플릿 수정 + +POST /templates/{id} + +Query ID : updateTemplate + +. 템플릿 삭제 + +DELETE /templates/{id} + +Query ID : deleteTemplate + +== 화면 +결재양식을 관리기능 > 결재/메일관리 > 결재양식 관리를 통해 볼 수 있다. + +image::front_07_06.png[] +* 등록된 결재양식 목록을 결재양식 관리를 통해 볼 수 있다. +* 목록의 Key 컬럼을 클릭하여 결재양식 정보를 수정할 수 있다. +* 팝업 미리보기 컬럼을 클릭하여 등록된 결재양식의 첨부파일을 확인 할 수 있다. + +image::front_07_07.png[] +* Key : 유니크한 결재양식 키를 지정(영문, 숫자만 가능) +* 제목 : 결재양식의 제목 +* 설명 : 결재양식의 설명 +* 첨부파일 : 결재양식 첨부파일 \ No newline at end of file diff --git a/doc/공통기능/결재메일/내부결재.adoc b/doc/공통기능/결재메일/내부결재.adoc new file mode 100644 index 0000000..5116c73 --- /dev/null +++ b/doc/공통기능/결재메일/내부결재.adoc @@ -0,0 +1,111 @@ += 내부 결재 + +== 개요 +SDL에서 제공하는 내부결재 기능. + +결재를 위한 문서 Entity등록과 내부결재 후처리를 위한 Interceptor 클래스 구현으로 간단하게 결재 기능을 구현할 수 있다. + +샘플로 제공하는 화면의 결재 스텝을 지정하는 Component를 필요한 결재 문서 화면에 적용하여 사용할 수 있으며 샘플 Controller를 참조하여 문서와 결재 스텝 목록을 저장하여 approvalService.submit 메소드를 호출하면 된다. + +== Table +* 결재 정보 : TN_CF_APPROVAL +* 결재 스텝 정보 : TN_CF_APPROVAL_STEP +* Sample Document : TN_CF_SAMPLE_APPROVAL_INTERNAL_DOCUMENT + +== API +.ApprovalController.java + +. 상신함 목록 조회(내부 결재) + +GET /internal-approvals/submit + +Query ID : selectInternalApprovalPagingListBySubmit +* 사용자가 상신한 내부결재 문서를 조회한다. + +. 미결함 목록 조회(내부 결재) + +GET /internal-approvals/not-approve + +Query ID : selectInternalApprovalPagingListByNotApprove +* 미결중인 내부결재 문서를 조회한다. + +. 기결함 목록 조회(내부 결재) + +GET /internal-approvals/approved + +Query ID : selectInternalApprovalPagingListByApproved +* 사용자가 결재 완료한 내부결재 문서를 조회한다. + +. 통보함 목록 조회(내부 결재) + +GET /internal-approvals/notice + +Query ID : selectInternalApprovalPagingListByNotice +* 사용자가 통보 대상인 결재문서를 조회한다. + +. 내부결재 문서 상신 취소 + +POST /internal-approvals/{approvalRequestId}/cancel + +Service : cancelInternalApproval +Query ID : updateInternalApprovalStep +* 결재 문서 상신을 취소한다. + +. 내부결재 문서 결재 승인 또는 합의 + +POST /internal-approvals/{approvalRequestId}/confirm + +Service : confirmInternalApproval +Query ID : updateInternalApprovalStep +* 결재 문서 승인 또는 합의 한다. + +. 내부결재 문서 반려 + +POST /internal-approvals/{approvalRequestId}/reject + +Service : rejectInternalApproval +Query ID : updateInternalApprovalStep +* 결재 문서를 반려 한다. + +.ApprovalSampleController.java + +. 내부결재 문서 목록 조회(페이징) + +GET /internal-approval/sample-document-with-paging + +Query ID : selectSampleApprovalDocumentPagingList +* Sample Document 문서 목록을 조회한다. + +. 내부 결재 문서 상신(Sample Document) + +POST /internal-approval/submit/sample-document +* Knox 결재 정보 동기화 로직을 제외한 다른 부분은 Knox 결재 상신과 동일하다. +* 동기화 배치 로직 대상에서 제외된다. +* 내부결재 전후 처리 로직은 문서 타입별로 Interceptor 클래스를 구현하여 처리한다. +[source,java] +---- +@Log4j2 +@ApprovalDocumentType(names = {"internalSample"}) +public class InternalApprovalSampleInterceptor implements ApprovalInterceptor { + + @Autowired + InternalApprovalSampleService internalApprovalSampleService; + @Override + public void afterSubmit(Approval approval, List approvalStepList, Map attribute) { + log.debug("execute ApprovalSampleInterceptor afterSubmit"); + //상신 후 문서 상태 업데이트 + internalApprovalSampleService.updateSampleApprovalDocument(approval.getDbDocId(), ApprovalDocStatus.INPROCESS); + } + --생략-- +} +---- + +== 화면 + +. 내부결재 문서 상신함 + +image::internalApproval_01.png[] +image::internalApproval_05.png[] +* 사용자가 상신한 내부결재 문서 목록을 조회한다. +* 상세화면에 진입하여 상신취소가 가능하다. + +[start=2] +. 내부결재 문서 미결함 + +image::internalApproval_02.png[] +image::internalApproval_06.png[] +* 사용자의 결재 차순에 있는 내부결재 문서 목록을 조회한다. +* 상세화면에 진입하여 승인/합의 또는 반려가 가능하다. + +[start=3] +. 내부결재 문서 기결함 + +image::internalApproval_03.png[] +* 사용자의 결재 완료한 내부결재 문서 목록을 조회한다. + +[start=4] +. 내부결재 문서 통보함 + +image::internalApproval_04.png[] +* 결재 완료 후 사용자에게 통보된 내부결재 문서 목록을 조회한다. \ No newline at end of file diff --git a/doc/공통기능/결재메일/대리결재.adoc b/doc/공통기능/결재메일/대리결재.adoc new file mode 100644 index 0000000..b8557bd --- /dev/null +++ b/doc/공통기능/결재메일/대리결재.adoc @@ -0,0 +1,35 @@ += 대리 결재 + +== 개요 +내부결재에 적용되는 대리결재자를 지정하는 기능을 제공한다. + +(Knox 결재 대리결재는 Knox portal에서 지정 가능함) + + +== Table +* 결재 정보 : TN_CF_APPROVAL_DELEGATE + +== API +.ApprovalController.java + +. 대리결재자 조회 + +GET /approval/approver-delegate + +Query ID : selectApproverDelegate +* 사용자의 대리결재자를 조회한다. + +. 대리결재자 저장 + +POST /approval/approver-delegate + +Query ID : updateApproverDelegate, insertApproverDelegate +* 사용자의 대리결재자를 저장한다.(등록 또는 변경) + +. 대리결재자 삭제 + +DELETE /approval/approver-delegate + +Query ID : deleteApproverDelegate +* 사용자의 대리결재자를 삭제한다. + +== 화면 + +. 대리결재자 조회 및 지정 + +image::approverDelegate_01.png[] + +* 사용자 정보 > 대리결재 메뉴를 통해서 대리결재자 등록이 가능하다. +* 기등록된 대리결재자가 존재한다면 대리결재자 팝업 상단에 표시되며 삭제하거나 다른 사용자를 선택하여 변경가능 하다. \ No newline at end of file diff --git a/doc/공통기능/결재메일/메신저.adoc b/doc/공통기능/결재메일/메신저.adoc new file mode 100644 index 0000000..a555727 --- /dev/null +++ b/doc/공통기능/결재메일/메신저.adoc @@ -0,0 +1,85 @@ += 메신저 + +== 개요 +Knox Portal에서 제공하는 메신저 관련 Rest API 를 이용한 연계 서비스 제공 + +=== Knox REST API 연계 서비스 신청 +Knox REST API 연계 서비스 신청은 <<_knox_rest_api_연계_서비스_신청,Knox REST API 연계 서비스 신청>> 항목을 참조한다. + +=== Knox Rest 메신저 연계 설정 +메일, 결재 Knox Rest API 연계와 마찬가지로 연계를 위한 사전 준비가 되었다면, knox.properties 에 메신저 관련 설정이 되어 있는지 확인한다. + +.knox.properties +[source,properties] +---- +knox.messenger.contact-service=/messenger/contact/api/v1.0 +knox.messenger.msgctx-service=/messenger/msgctx/api/v1.0 +knox.messenger.message-service=/messenger/message/api/v1.0 +---- + +=== Knox 메신저 연계 서비스 +REST를 통해서 메신저와 연계하는 서비스로 주요 메서드는 KnoxMessengerService 인터페이스에 정의되어 있다. + +[source, java] +---- +public interface KnoxMessengerService { + + /** + * 디바이스 ID 조회 : 사용자 ID 와 맵핑되는 단말의 ID 값 + * @return 디바이스 ID + */ + String getDeviceId(); + + /** + * 메시지 암호화 키 조회 : 메시지를 암호화하기 위한 키 값 + * @param deviceId 디바이스 ID + * @return 메시지 암호화 키 + */ + String getKey(String deviceId); + + /** + * Knox Potal login ID를 이용하여 Knox Messenger 수신자들을 조회한다. + * @param deviceId 디바이스 ID + * @param singleIds 수신자 Knox ID 리스트 + * @return Knox Messenger 수신자 ID 리스트 + */ + List getUserIds(String deviceId, List singleIds); + + /** + * 공지 메시지를 발신할 대화방이 없는 경우 신규 대화방 생성을 요청한다. + * @param deviceId 디바이스 ID + * @param key 암호화 키 + * @param userIds Knox Messenger 수신자 ID 리스트 + * @return 대화방 생성 요청 응답 결과 + */ + Map createChatroom(String deviceId, String key, List userIds); + + /** + * Message 서버 API 에서 필요한 암호화된 바디를 만들기 위한 function + * @param key - msgCtx 를 통해 전달받은 key 값 + * @param body - 암호화 해야 될 String + * @return 암호화된 String + */ + String encrypt(String key, String body); + + /** + * Response 로 전달된 암호화 body 를 복호화 하기 위한 function
+ * 암호화의 역순으로 Base64 복호화 -> AES256 복호화 + * @param body - 암호화 되어 있는 body + * @return 복호화된 response String + */ + String decrypt(String body); + + /** + * 신규 생성한 대화방 또는 기존에 사용중인 대화방에 공지 메시지를 발송한다. + * @param deviceId 디바이스 ID + * @param key 암호화 키 + * @param chatroomId 생성된 대화방 ID + * @param chatMsg 메시지 내용 + * @return 메시지 발신 요청 응답 결과 + */ + Map chat(String deviceId, String key, String chatroomId, String chatMsg); +} +---- + +자세한 API 스펙은 Swagger API 문서의 knox-messenger-controller 항목을 참고한다. diff --git a/doc/공통기능/결재메일/메일.adoc b/doc/공통기능/결재메일/메일.adoc new file mode 100644 index 0000000..83f5bb6 --- /dev/null +++ b/doc/공통기능/결재메일/메일.adoc @@ -0,0 +1,75 @@ += 메일 + +=== Knox REST API 연계 서비스 신청 +Knox REST API 연계 서비스 신청은 <<_knox_rest_api_연계_서비스_신청,Knox REST API 연계 서비스 신청>> 항목을 참조한다. + +=== Knox메일 연계 설정 +Knox REST API 연계 서비스 신청이 되었다면, 발급받은 `system-id`, `token` 값을 설정한다. + +.knox.properties (스테이지) +[source,properties] +---- +knox.system-id=xxxxxxxxxxx +knox.token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +knox.address.prefix=openapi.samsung.net +knox.mail-service=/mail/api/v2.0 +---- + +.knox.properties (운영) +[source,properties] +---- +knox.system-id=xxxxxxxxxxx +knox.token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx // <1> +knox.address.prefix=openapi.samsung.net,openapi.w1.samsung.net,openapi.w2.samsung.net # <2> +knox.mail-service=/mail/api/v2.0 +---- +<1> 토큰 (comma(,)로 구분, 거점 순서와 동일) +<2> 거점 (국내, 구주, 미주) + +=== KnoxMailService +Knox REST API 연계를 통해서 메일발신, 상태조회등을 하는 서비스로 주요 메서드는 KnoxMailService 인터페이스에 정의되어 있다. + +KnoxMailService는 시스템에서 Knox 메일 서비스와 연계할 때 필요한 API들을 제공한다. + +[source, java] +---- +public interface KnoxMailService { + /** + * Knox 메일 발신 + * @param sendMail 발신 메일 정보 (첨부파일정보(AttachFile) 포함) + * @return Knox 메일 아이디 + */ + String sendMail(SendMail sendMail); + + /** + * Knox 메일 발신 + * @param sendMail 발신 메일 정보 + * @param attachResources 메일 첨부가 파일 리소스일 경우 + * @return Knox 메일 아이디 + */ + String sendMail(SendMail sendMail, List attachResources); + + /** + * Knox 메일별 수신상태 조회 + * @param mailIds Knox 메일 아이디 리스트 + * @param sendMail 메일 정보 + * @return Knox 메일 상태 + */ + MailStatus[] getDeliveryStatusCount(List mailIds, SendMail sendMail); + + /** + * Knox 메일 수신인별 수신상태 조회 + * @param mailId Knox 메일 아이디 + * @param sendMail 메일 정보 + * @return Knox 메일 수신인별 수신상태 정보 + */ + Recipient[] getDeliveryStatus(String mailId, SendMail sendMail); +} +---- + +각각의 구현 메서드들은 Knox Rest 연계 서비스에서 요구하는 가이드대로 REST 형식을 갖추어 필요한 로직들을 수행한다. + +자세한 API 스펙은 Swagger API 문서의 knox-mail-controller 항목을 참고한다. + +메일 발송 샘플 코드는 MailSampleController 소스코드를 참고한다. + +IMPORTANT: Knox 연계 메일 발송을 위해서는 발신자/수신자 모두 Knox 계정이 존재해야만 테스트가 가능하다. diff --git a/doc/공통기능/결재메일/메일그룹관리.adoc b/doc/공통기능/결재메일/메일그룹관리.adoc new file mode 100644 index 0000000..0d5ffb4 --- /dev/null +++ b/doc/공통기능/결재메일/메일그룹관리.adoc @@ -0,0 +1,161 @@ += 메일 그룹 관리 + +== 개요 + +메일 그룹 및 맵핑 정보 관리 기능 제공. + +== UI Design & Function + +=== 메일 그룹 목록(MailGroupList.vue) + +메일 그룹 등록, 수정 및 삭제가 가능하다. + +image::mailGroupList.png[mailGroupList.png] + +* 기능 설명 +. 메일 그룹 목록 조회 +. 메일 그룹 상세정보 조회 +. 메일 그룹 등록 : 등록 버튼 클릭 시 메일 그룹 입력 popup 호출. +. 메일 그룹 수정 : checkbox 선택 후 수정 버튼 클릭 시 변경을 위한 popup 호출. +. 메일 그룹 삭제 : checkbox 선택 후 삭제 버튼 클릭 시 삭제. + + +=== 메일 그룹 맵핑 수정(MailGroupMappEdit.vue) + +메일 그룹 맵핑 목록 등록 및 삭제가 가능하다. + +image::mailGroupMappEdit.png[mailGroupMappList.png] + +* 기능 설명 +. 메일 그룹 맵핑 목록 조회 +. 메일 그룹 맵핑 저장 +.. 사용자 추가 : 버튼 클릭 시 사용자 추가 popup 호출. +.. 역할 추가 : 버튼 클릭 시 역할 추가 popup 호출. +.. 업무그룹 추가 : 버튼 클릭 시 업무그룹 추가 popup 호출. + +== API & Service + +=== API + +* API : MailGroupController.java + +. 메일 그룹 목록 조회 : GET /mail-group-with-paging +. 메일 그룹 상세정보 조회 : GET /mail-group/{mailGroupId} +. 메일 그룹 등록 : POST /mail-group +. 메일 그룹 수정 : PUT /mail-group +. 메일 그룹 삭제 : DELETE /mail-group +. 메일 그룹 맵핑 목록 조회 : GET /mail-group-mapp-with-paging +. 메일 그룹 맵핑 저장 : POST /mail-group-mapp/{mailGroupId} + +* Service : MailGroupServiceImpl.java + +. 메일 그룹 맵핑 저장 + +맵핑 정보 저장 시 기등록 되어 있는 맵핑 목록 삭제 후 +전체 목록을 다시 저장하도록 구현. + +[source,java] +---- +@Override +@Transactional +public void saveMailGroupMapp(String mailGroupId, List mailGroupMappList) { + + mailGroupDao.deleteMailGroupMapp(mailGroupId); + + for(MailGroupMapp mailGroupMapp : mailGroupMappList) { + mailGroupMapp.setMailGroupId(mailGroupId); + mailGroupDao.insertMailGroupMapp(mailGroupMapp); + } +} +---- + + + +== Entity Table & SQL + +=== Entity Table + +* TN_CF_MAIL_GROUP : 메일 그룹 +* TN_CF_MAIL_GROUP_MAPP : 메일 그룹 맵핑 + +=== SQL + +. 메일 그룹 목록 조회 + +[source,xml] +---- + + SELECT , + (SELECT USER_NAME + --생략-- +---- + +[start=3] +. 메일그룹 등록 + +[source,xml] +---- + + INSERT INTO + () + --생략-- +---- + +[start=4] +. 메일그룹 수정 + +[source,xml] +---- + + UPDATE + SET LABEL = #{label}, + --생략-- +---- + +[start=5] +. 메일그룹 삭제 + +[source,xml] +---- + + UPDATE + SET DELETED = '1', + --생략-- +---- + +[start=6] +. 메일그룹 맵핑 목록 조회 + +[source,xml] +---- + + UPDATE + SET DELETED = '1', + --생략-- +---- + +[start=7] +. 메일그룹 맵핑 저장 + +[source,xml] +---- + + DELETE FROM + WHERE MAIL_GROUP_ID = #{mailGroupId} + + + + INSERT INTO + () + --생략-- +---- \ No newline at end of file diff --git a/doc/공통기능/결재메일/메일상태조회.adoc b/doc/공통기능/결재메일/메일상태조회.adoc new file mode 100644 index 0000000..8cfd932 --- /dev/null +++ b/doc/공통기능/결재메일/메일상태조회.adoc @@ -0,0 +1,87 @@ += 메일 상태 조회 + +== 개요 +Knox REST 메일수신상황조회 API와 연계하여 발신한 메일의 상태를 조회할 수 있다. + +=== Knox메일 연계 설정 +Knox REST 메일수신상황조회를 위해서는 Knox메일 연계 설정이 되어 있어야 한다. + +Knox메일 연계 설정은 <<_메일,메일>> 항목의 <<_knox메일_연계_설정,Knox메일 연계 설정>> 항목을 참조한다. + +=== 메일별 수신 상황 카운트 조회 +발신한 메일의 메일 아이디값을 이용하여 발신한 메일을 수신한 수신자들의 개봉상태를 +요약한 카운트 정보조회 + +Service:: KnoxMailService + +Method:: ++ +[source,java] +---- +/** + * Knox 메일별 수신상태 조회 + * @param mailIds Knox 메일 아이디 리스트 + * @param sendMail 메일 정보 + * @return Knox 메일 상태 + */ +MailStatus[] getDeliveryStatusCount(List mailIds, SendMail sendMail); +---- +* SendMail 객체에 senderId (발신자 EP ID) 값 설정 필수 + +=== 수신인 별 수신 상황 조회 +발신한 메일의 메일 아이디값을 이용하여 발신한 메일의 수신자 별 수신 상태 정보를 조회 + +Service:: KnoxMailService + +Method:: ++ +[source,java] +---- +/** + * Knox 메일 수신인별 수신상태 조회 + * @param mailId Knox 메일 아이디 + * @param sendMail 메일 정보 + * @return Knox 메일 수신인별 수신상태 정보 + */ +Recipient[] getDeliveryStatus(String mailId, SendMail sendMail); +---- +* SendMail 객체에 senderId (발신자 EP ID) 값 설정 필수 + +=== 사용 예 +.SentMailHistoryServiceImpl +[source,java] +---- +@Override +public Map getSentMail(String mailId) { + Map rtnMap = new HashMap<>(); + MailStatus mailStatus = new MailStatus(); + List recipientList; + + List mailIds = new ArrayList<>(); + mailIds.add(mailId); + SendMail sendMail = sentMailHistoryDao.getSentMail(mailId); + + try { + // 14일 이전 문서는 Knox에서 조회 + mailStatus = knoxMailService.getDeliveryStatusCount(mailIds, sendMail)[0]; // <1> + recipientList = Arrays.asList(knoxMailService.getDeliveryStatus(mailId, sendMail)); // <2> + + if (ObjectUtils.isNotEmpty(recipientList)) { + // 메일 수신 상태 동기화 + updateRecipient(mailId, recipientList); + } else { + recipientList = sentMailHistoryDao.getRecipientList(mailId); + } + } catch (Exception ex) { + log.error(ex.getMessage()); + //Knox 에서 메일을 조회 할수 없을 경우, 14일이 경과 되었거나 Mail Box를 못찾을 경우 + recipientList = sentMailHistoryDao.getRecipientList(mailId); + } + + rtnMap.put("mailStatus", mailStatus); + rtnMap.put("recipientList", recipientList); + rtnMap.put("sendMail", sendMail); + return rtnMap; +} +---- +<1> Knox 메일별 수신자들의 개봉상태 카운트 조회 +<2> Knox 수신인 별 메일 수신 상황 조회 diff --git a/doc/공통기능/결재메일/메일양식관리.adoc b/doc/공통기능/결재메일/메일양식관리.adoc new file mode 100644 index 0000000..abfabd3 --- /dev/null +++ b/doc/공통기능/결재메일/메일양식관리.adoc @@ -0,0 +1,58 @@ += 메일 양식 관리 + +== 개요 +메일 양식을 관리한다. + +<<_결재_양식_관리,결재 양식 관리>>와 비교해서 'MAIL'로 조회하는 것 외에 동일하다. + +== Table +* 템플릿 : TN_CF_TEMPLATE + +== API +.TemplateController.java + +. 템플릿 목록 조회(페이징) + +GET /templates-with-paging/group/{templateGroupCode} + +Query ID : selectTemplatePagingList +* TEMPLATE_GROUP_CODE 컬럼의 'MAIL'을 조회한다. + +. 템플릿 상세 조회 + +GET /templates/group/{templateGroupCode}/key/{templateKey} + +Query ID : selectTemplate + +. 템플릿 상세 조회 (By ID) + +GET /templates/{id} + +Query ID : selectTemplate +* 양식의 상세 내용을 조회 하거나 팝업 미리보기를 할 수 있다. + +. 템플릿 Key 중복 체크 + +GET /templates/dup-check/group/{templateGroupCode}/key/{templateKey} + +Query ID : selectTemplate +* 저장 전 템플릿 Key 중복 여부를 검사한다. + +. 템플릿 등록 + +POST /templates/group/{templateGroupCode} + +Query ID : insertTemplate +* 양식을 저장한다. + +. 템플릿 수정 + +POST /templates/{id} + +Query ID : updateTemplate + +. 템플릿 삭제 + +DELETE /templates/{id} + +Query ID : deleteTemplate + +== 화면 + +메일양식을 관리기능 > 결재/메일관리 > 메일양식 관리를 통해 할 수 있다. + +image::mailTemplate.png[] +* 등록된 메일양식 목록을 메일양식 관리를 통해 볼 수 있다. +* 목록의 Key 컬럼을 클릭하여 메일양식 정보를 수정할 수 있다. +* 팝업 미리보기 컬럼을 클릭하여 등록된 메일양식의 첨부파일을 확인 할 수 있다. + +image::mailTemplate1.png[] +* Key : 유니크한 메일양식 키를 지정(영문, 숫자만 가능) +* 제목 : 메일양식의 제목 +* 설명 : 메일양식의 설명 +* 첨부파일 : 메일양식 첨부파일 \ No newline at end of file diff --git a/doc/공통기능/결재메일/보낸메일이력.adoc b/doc/공통기능/결재메일/보낸메일이력.adoc new file mode 100644 index 0000000..81f1d07 --- /dev/null +++ b/doc/공통기능/결재메일/보낸메일이력.adoc @@ -0,0 +1,19 @@ += 보낸 메일 이력 + +== 개요 +메일 발송 내역 및 수신 상태를 조회할 수 있다. + +=== 보낸 메일 목록 + +image::sentMailHistory.png[] + +<1> 기간, 제목별 검색 가능 +<2> 클릭시 상세 내역 조회 + +=== 보낸 메일 상세 정보 + +image::sentMailHistoryInfo.png[] + +<1> 보낸 메일의 수신인별 개봉 여부 +<2> 보낸 메일 본문 확인 가능 +<3> 수신인과 수신상태 정보 목록 diff --git a/doc/공통기능/결재메일/임직원.adoc b/doc/공통기능/결재메일/임직원.adoc new file mode 100644 index 0000000..f2c1e4c --- /dev/null +++ b/doc/공통기능/결재메일/임직원.adoc @@ -0,0 +1,93 @@ += 임직원 + +== 개요 +Knox Portal에서 제공하는 임직원 관련 Rest API 를 이용한 연계 서비스 제공 + +=== Knox REST API 연계 서비스 신청 +Knox REST API 연계 서비스 신청은 <<_knox_rest_api_연계_서비스_신청,Knox REST API 연계 서비스 신청>> 항목을 참조한다. + +=== Knox임직원 연계 설정 +Knox REST API 연계 서비스 신청이 되었다면, 발급받은 `system-id`, `token` 값을 설정한다. + +.knox.properties (스테이지) +[source,properties] +---- +knox.system-id=xxxxxxxxxxx +knox.token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +knox.address.prefix=openapi.samsung.net +knox.emp-service=/employee/api/v2.0 +---- + +.knox.properties (운영) +[source,properties] +---- +knox.system-id=xxxxxxxxxxx +knox.token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx // <1> +knox.address.prefix=openapi.samsung.net,openapi.w1.samsung.net,openapi.w2.samsung.net # <2> +knox.emp-service=/employee/api/v2.0 +---- +<1> 토큰 (comma(,)로 구분, 거점 순서와 동일) +<2> 거점 (국내, 구주, 미주) + +=== Knox임직원 API 연계 서비스 +Knox REST API 연계를 통해서 임직원 및 조직 정보 조회 기능을 제공하는 서비스로 주요 메서드는 KnoxUserService 인터페이스에 정의되어 있다. + +KnoxUserService는 시스템에서 임직원 및 조직 정보를 조회할때 필요한 API들을 제공한다. + +[source, java] +---- +public interface KnoxUserService { + + /** + * Knox 임직원 조회(By EpId) + * @param epId EP ID + * @return Knox 사용자 + */ + Employee[] getKnoxEmployeesByEpId(String epId); + + /** + * Knox 임직원 조회(By UserName) + * @param userName 사용자 이름 + * @return Knox 사용자 + */ + Employee[] getKnoxEmployeesByUserName(String userName); + + /** + * Knox 임직원 조회(By KnoxId) + * @param knoxId Knox ID + * @return Knox 사용자 + */ + Employee[] getKnoxEmployeesByKnoxId(String knoxId); + + /** + * Knox 임직원 조회(By Email) + * @param email 이메일 + * @return Knox 사용자 + */ + Employee[] getKnoxEmployeesByEmail(String email); + + /** + * Knox 조직도 조회(By CompanyCode) + * @param companyCode 회사 코드 + * @return Knox 조직도 + */ + Organization[] getKnoxOrganizationsByCompanyCode(String companyCode); + + /** + * Knox 조직도 조회(By DepartmentCode) + * @param companyCode 회사 코드 + * @param departmentCode 부서 코드 + * @return Knox 조직도 + */ + Organization[] getKnoxOrganizationsByDepartmentCode(String companyCode, String departmentCode); + + /** + * Knox 직급 조회 + * @param companyCode 회사 코드 + * @return Knox 직급 + */ + Title[] getKnoxTitles(String companyCode); +} +---- + +자세한 API 스펙은 Swagger API 문서의 knox-user-controller 항목을 참고한다. diff --git a/doc/공통기능/결재메일/주소록.adoc b/doc/공통기능/결재메일/주소록.adoc new file mode 100644 index 0000000..f64e186 --- /dev/null +++ b/doc/공통기능/결재메일/주소록.adoc @@ -0,0 +1,132 @@ += 주소록 + +== 개요 +Knox Portal에서 제공하는 연락처 관련 Rest API 를 이용한 연계 서비스 제공 + +=== Knox REST API 연계 서비스 신청 +Knox REST API 연계 서비스 신청은 <<_knox_rest_api_연계_서비스_신청,Knox REST API 연계 서비스 신청>> 항목을 참조한다. + +=== Knox Rest 연락처 연계 설정 +메일, 결재 Knox Rest API 연계와 마찬가지로 연계를 위한 사전 준비가 되었다면, knox.properties 에 연락처 관련 설정이 되어 있는지 확인한다. + +.knox.properties +[source,properties] +---- +knox.pims-service=/pims/contacts/api/v2.0 +---- + +=== Knox 연락처 연계 서비스 +REST를 통해서 연락처를 연계하는 서비스로 주요 메서드는 KnoxContactService 인터페이스에 정의되어 있다. + +[source, java] +---- +public interface KnoxContactService { + + /** + * 연락처 그룹 생성 + * @param contactGroupDto + * @param userId + * @return + */ + ContactGroupDto createGroup(ContactGroupDto contactGroupDto, String userId); + + /** + * 연락처 그룹 수정 + * @param groupId + * @param contactGroupDto + * @param userId + * @return + */ + ContactGroupDto updateGroup(String groupId, ContactGroupDto contactGroupDto, String userId); + + /** + * 연락처 그룹 삭제 + * @param groupId + * @param userId + * @return + */ + String deleteGroup(String groupId, String userId); + + /** + * 연락처 그룹 조회 + * @param groupId + * @param userId + * @return + */ + ContactGroupDto getGroup(String groupId, String userId); + + /** + * 연락처 그룹 목록 조회 + * @param pubType + * @param userId + * @return + */ + ContactGroupDto[] getGroups(String pubType, String userId); + + /** + * 연락처 생성 + * @param contactDto + * @param userId + * @return + */ + ContactDto createCard(ContactDto contactDto, String userId); + + /** + * 연락처 수정 + * @param contactId + * @param contactDto + * @param userId + * @return + */ + ContactDto updateCard(String contactId, ContactDto contactDto, String userId); + + /** + * 연락처 삭제 + * @param contactId + * @param userId + * @return + */ + String deleteCard(String contactId, String userId); + + /** + * 연락처 조회 + * @param contactId + * @param userId + * @return + */ + ContactDto getCard(String contactId, String userId); + + /** + * 연락처 목록 조회 + * @param pubType + * @param userId + * @return + */ + ContactDto[] getCards(String pubType, String userId); + + /** + * KNOX REST GET MAPPING + * @param + * @param methodName + * @param params + * @param paths + * @param classType + * @return + */ + T contactsGet(String methodName, MultiValueMap params, Map paths, Class classType); + + /** + * KNOX REST POST MAPPING + * @param + * @param methodName + * @param bodyMap + * @param params + * @param paths + * @param classType + * @return + */ + T contactsPost(String methodName, Map bodyMap, MultiValueMap params, Map paths, Class classType); +} +---- + +자세한 API 스펙은 Swagger API 문서의 knox-contact-controller 항목을 참고한다. diff --git a/doc/공통기능/공통기능.adoc b/doc/공통기능/공통기능.adoc new file mode 100644 index 0000000..cfff032 --- /dev/null +++ b/doc/공통기능/공통기능.adoc @@ -0,0 +1,15 @@ += 공통기능 + +include::사용자관리.adoc[leveloffset=+1] + +include::시스템관리.adoc[leveloffset=+1] + +include::이력관리.adoc[leveloffset=+1] + +include::결재메일.adoc[leveloffset=+1] + +include::보안관리.adoc[leveloffset=+1] + +include::글로벌지원.adoc[leveloffset=+1] + +include::파일서비스.adoc[leveloffset=+1] \ No newline at end of file diff --git a/doc/공통기능/글로벌지원.adoc b/doc/공통기능/글로벌지원.adoc new file mode 100644 index 0000000..3a4f3ef --- /dev/null +++ b/doc/공통기능/글로벌지원.adoc @@ -0,0 +1,7 @@ += 글로벌 지원 + +include::글로벌지원/Timezone.adoc[leveloffset=+1] + +include::글로벌지원/다국어서비스.adoc[leveloffset=+1] + +include::글로벌지원/번역(Utrans).adoc[leveloffset=+1] diff --git a/doc/공통기능/글로벌지원/Timezone.adoc b/doc/공통기능/글로벌지원/Timezone.adoc new file mode 100644 index 0000000..5463609 --- /dev/null +++ b/doc/공통기능/글로벌지원/Timezone.adoc @@ -0,0 +1,34 @@ += Timezone + +== 개요 +사용자의 Timezone을 관리한다. + +== Table +* 사용자 : TN_CF_USER +** TIME_ZONE_CODE, TIME_ZONE_ID 컬럼 사용 + +== Timezone 저장 + +* 사용자가 시스템에 최초 등록시 저장. + +그 이후에는 '타임존 저장' API를 사용하여 저장한다. +- 사용자가 시스템에 처음으로 SSO 로그인하여 사용자 등록시 epTray 연계된 타임존 정보를 가져와서 저장 (없을 경우 config.properties의 default 값) + +== API +.UserController.java + +. 타임존 목록 조회 + +GET /auth/users/timezone +* 타임존은 java.util.TimeZone 라이브러리를 사용하기 때문에 DB에 타임존 목록이 저장되어 있지 않으며, + +서머타임(일광 절약 시간제, DST(Daylight Saving Time))을 따로 계산하지 않아도 자동으로 목록에서 보여준다. + +. 타임존 저장 + +PUT /auth/users/timezone + +== 화면 +사용자의 Timezone을 설정하는 기능으로, Timezone을 설정하게 되면 Local Storage의 *user.timeZoneId, user.timeZoneCode* 에 저장된다. + + +TopMenu - 표준시간을 통해 접근 가능. + +image::timezone.png[] + +NOTE: SideMenu(Right Side) 이용 시 MainOffsider.vue 내 'timezone' 부분에 구현되어 있다. \ No newline at end of file diff --git a/doc/공통기능/글로벌지원/다국어서비스.adoc b/doc/공통기능/글로벌지원/다국어서비스.adoc new file mode 100644 index 0000000..69992b1 --- /dev/null +++ b/doc/공통기능/글로벌지원/다국어서비스.adoc @@ -0,0 +1,56 @@ += 다국어 서비스 + +== 개요 +message-common.properties를 사용하여 시스템에 다국어를 지원한다. + +한글, 영문을 기본으로 서비스한다. + +기본언어인 한글은 message-common_ko_KR.properties을 파일명으로 하고 영문은 '_en_US'를 붙여서 사용한다. + +== 설정 +=== 프론트엔드 기본언어 값 설정 +..env +[source,properties] +---- +# To set default Language for I18n (ko_KR, en_US, zh_CN, etc..) +VITE_DEFAULT_LANG=ko_KR +---- + +=== 백엔드 기본언어 및 다국어 설정 +.config.properties +[source,properties] +---- +## Language Set +default-language=ko_KR +language-set=ko_KR,en_US +---- + +* 예) 시스템에서 프랑스어를 추가하고자 할 때 방법 +. config.properties의 language-set에 fr_FR을 추가 ++ +[source,properties] +---- +## Language Set(프랑스어 추가) +language-set=ko_KR,en_US,fr_FR +---- + +. message-common_fr_FR.properties 파일을 생성 +. 메세지의 프랑스어 버전 작성 + +NOTE: 메세지들만 추가되므로 메뉴관리, 역할관리, 업무그룹관리 등 다국어 컬럼(LABEL_JSON)을 지원하는 table 데이터의 경우 직접 입력하여야 한다. + +== API +.MessageBundleController.java + +. 로케일별로 메세지를 조회 + +GET /noauth/messages + +. 설정한 모든 언어의 메세지를 조회 + +GET /noauth/messages/all + +== 화면 +한국어,영어 중 원하는 언어로 변경하여 화면을 나타내는 기능으로, 언어를 설정하게 되면 User Token에 저장 되어 로그아웃을 하게 되더라도 마지막에 변경된 언어로 설정된다. + +TopMenu - 언어선택을 통해 접근 가능.(단, 다국어 지원 되는 영역에 한해서만 지원). + +image::language.png[] + +NOTE: SideMenu(Right Side) 이용 시 MainOffsider.vue 내 'language' 부분에 구현되어 있다. \ No newline at end of file diff --git a/doc/공통기능/글로벌지원/번역(Utrans).adoc b/doc/공통기능/글로벌지원/번역(Utrans).adoc new file mode 100644 index 0000000..a662943 --- /dev/null +++ b/doc/공통기능/글로벌지원/번역(Utrans).adoc @@ -0,0 +1,20 @@ += 번역(Utrans) + +== 개요 +Utrans API를 호출하여 한국어 및 영어 등 언어로 번역하는 기능. + +image::utrans.png[] + +=== 지원 되는 언어방향 +- 유럽어 : 러시아어,스페인어,독일어,프랑스어,이탈리아어,포르투갈어 +|=== +|Source |Target +|한국어 | 영어, 중국어, 베트남어, 일어, 유럽어 +|영어 | 유럽어 +|중국어 | 유럽어 +|유럽어 | 유럽어 +|=== + +=== 기능별 설명 +- 번역하기 : 번역된 내용을 오른쪽 창에 표시 +- 복사하기 : 번역 결과 값을 클립보드에 복사 diff --git a/doc/공통기능/보안관리.adoc b/doc/공통기능/보안관리.adoc new file mode 100644 index 0000000..92b9536 --- /dev/null +++ b/doc/공통기능/보안관리.adoc @@ -0,0 +1,19 @@ += 보안관리 + +include::보안관리/약관관리.adoc[leveloffset=+1] + +include::보안관리/개인정보사용이력관리.adoc[leveloffset=+1] + +include::보안관리/개인정보취급자권한변경이력.adoc[leveloffset=+1] + +include::보안관리/EUGDPR.adoc[leveloffset=+1] + +include::보안관리/관리자IP관리.adoc[leveloffset=+1] + +include::보안관리/문자열암복호화.adoc[leveloffset=+1] + +include::보안관리/SQLInjection.adoc[leveloffset=+1] + +include::보안관리/XSS.adoc[leveloffset=+1] + +include::보안관리/비밀번호관리.adoc[leveloffset=+1] diff --git a/doc/공통기능/보안관리/EUGDPR.adoc b/doc/공통기능/보안관리/EUGDPR.adoc new file mode 100644 index 0000000..447bef5 --- /dev/null +++ b/doc/공통기능/보안관리/EUGDPR.adoc @@ -0,0 +1,7 @@ += EU GDPR + +== 개요 +. GDPR이란? + +2018년 5월 25일부터 시행되는 EU(유럽연합)의 개인정보보호 법령이며, 동 법령 위반시 과징금 등 행정처분이 부과될 수 있어 EU와 거래하는 우리나라 기업도 이 법에 위반되지 않도록 주의할 필요가 있다. +. EU와 거래하는 시스템은 이용약관동의 관리에서 '유럽연합 개인정보보호 규정'이라는 약관을 추가하여 사용자의 동의를 받아야 한다. +* 세부적인 내용은 <<_약관_관리,약관 관리>> 매뉴얼을 참조한다. \ No newline at end of file diff --git a/doc/공통기능/보안관리/SQLInjection.adoc b/doc/공통기능/보안관리/SQLInjection.adoc new file mode 100644 index 0000000..5fe83ce --- /dev/null +++ b/doc/공통기능/보안관리/SQLInjection.adoc @@ -0,0 +1,39 @@ += SQL Injection + +== 개요 +표준개발라이브러리에서는 MyBatis의 PreparedStatement 를 사용한 value injection을 원칙으로 사용하기 때문에 java 혹은 jsp에서 sql을 만들지 않는다면 근본적으로 sql injection이 발생하지 않는다. +단, ${} 매핑을 사용 할 경우 SDLComparator를 사용해야 한다. + +=== SQL Injeciton 공격 예 +MyBatis에서는 #{}, ${} 두 변수 형태를 제공한다. #{}의 경우엔 SQL Injection 공격이 불가하지만 ${}는 값이 직접 매핑되기 때문에 SQL Injeciton에 노출되어 있다. + +${}에 매핑될 값은 사용자가 입력값(파라미터를 변조할수 있는)을 사용할 경우 보안 취약점에 노출되게 된다. + +[source,xml] +---- + +---- + +위의 경우 + +[source,xml] +---- +SELECT * FROM PERSON WHERE NAME = ? and PHONE LIKE 'A%'; DELETE FROM PERSON; --' +---- + +실행이 가능하다. + +따라서, ${}에 매핑될 값은 조작이 불가능 하도록 사용자 입력값을 코드로 입력 할 수 있도록 하고, 서버에서 코드에 맞는 스트링을 조합해서 실행 할 수 있도록 해야한다. + +=== SDLComparator 적용 +SDL에서는 개발자의 시큐어 코딩 실수로 인한 SQL Injection 실행 방지를 위해 MyBatis에서 변수 매핑 전에 허용된 문자만 사용 할 수 있도록 함수를 제공하고 있다. + +.사용방법 +[source,xml] +---- + + ORDER BY ${orderBy} + +---- \ No newline at end of file diff --git a/doc/공통기능/보안관리/XSS.adoc b/doc/공통기능/보안관리/XSS.adoc new file mode 100644 index 0000000..6e40f4b --- /dev/null +++ b/doc/공통기능/보안관리/XSS.adoc @@ -0,0 +1,21 @@ += XSS 방지 + +== 개요 +XSS(Cross Site Scripting)는 JavaScript등으로 작성된 악성 스크립트 코드를 웹 게시판 등에 삽입해 세션을 가로채거나 공격자가 의도한대로 행동하도록 만드는 공격이다. + +=== XSS 방지 적용 +표준개발라이브러리에서는 HTML태그를 허용하는 게시판에 대해 화이트리스트를 선정하여 해당 태그만 허용되도록 하고 있다. + +.사용방법 +[source,java] +---- + @Operation(summary = "게시글 상세조회") + @GetMapping("/posts/{postId}") + public Post getPost( + @Parameter("게시글 ID") @PathVariable String postId) { + Post post = postService.getPost(postId); + post.setPostDetail(SdlHtmlPolicy.POLICY_DEFINITION.sanitize( post.getPostDetail())); // <1> + return post; + } +---- +<1> 게시글 본문을 SdlHtmlPolicy.POLICY_DEFINITION에 정의된 정책을 기반으로 허용된 태그만 가능하도록 처리 diff --git a/doc/공통기능/보안관리/개인정보사용이력관리.adoc b/doc/공통기능/보안관리/개인정보사용이력관리.adoc new file mode 100644 index 0000000..3d9daaf --- /dev/null +++ b/doc/공통기능/보안관리/개인정보사용이력관리.adoc @@ -0,0 +1,93 @@ += 개인정보 사용 이력 관리 + +== 개요 +사용자 정보를 조회한 이력을 남긴다. 관련 법에 따라 일정기간 동안 보관한다. + +=== 사용자 정보 조회 이력 관리 +* UserController에서 사용자 정보 관련 메서드 호출시, UserService의 writeUserHistoryLog 메서드를 호출하고 있다. +* log4j2.xml에 설정한 파일에 이력이 남는다. + +.UserController.class +[source,java] +---- + @Operation(summary = "사용자 목록 조회") + @GetMapping("/auth/users") + public PagingResult getUserPagingList( @ModelAttribute UserSearchDto searchDto) { + + PagingResult resultPage = userService.getUserPagingList(searchDto); + + // 개인정보조회 이력 남김 + userService.writeUserHistoryLog(resultPage); + + return resultPage; + } + + @Operation(summary = "사용자 조회 (by EP ID)") + @GetMapping("/auth/users/{userId}") + public User userInfo(@Parameter(description = "EP ID", required = true) @PathVariable(required = true) String userId) { + + User userInfo = userService.getUserById(userId); + + // 개인정보조회 이력 남김 + userService.writeUserHistoryLog(userInfo); + + return userInfo; + } +---- + +.UserServiceImpl.class +[source,java] +---- + @Override + public void writeUserHistoryLog(Object returnValue) { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); + String requestUri = request.getRequestURI(); + String requestMethod = request.getMethod(); + User user = Account.currentUser(); + if(ObjectUtils.isNotEmpty(user)) { // 로그인된 사용자 + try { + HistoryLog log = new HistoryLog(); + log.setLogId(idGenService.getNextStringId()); + log.setNodeId(nodeId); + if(ObjectUtils.isNotEmpty(user)) { + log.setWorkerId(user.getUserId()); + log.setWorkerName(user.getUserName()); + } + log.setWorkDatetime(DateTime.now().toString()); + log.setRemoteAddr(webUtil.getClientIp(request)); + log.setRequestMethod(requestMethod); + log.setRequestUri(requestUri); + log.setApiResult(returnValue); + + String jsonVal = mapper.writeValueAsString(log); + USER_HISTORY_LOG.info(jsonVal); + } catch (JsonProcessingException e) { + log.warn(e.getMessage()); + } + } + } +---- + +.log4j2.xml +[source,xml] +---- + + + + + + %d %-5p [%t] %-17c{2} \(%13F:%L\) - %m%n + + + + + + + + + + + + +---- \ No newline at end of file diff --git a/doc/공통기능/보안관리/개인정보취급자권한변경이력.adoc b/doc/공통기능/보안관리/개인정보취급자권한변경이력.adoc new file mode 100644 index 0000000..d342f41 --- /dev/null +++ b/doc/공통기능/보안관리/개인정보취급자권한변경이력.adoc @@ -0,0 +1,195 @@ += 개인정보 취급자 권한변경이력 + +== 개요 +역할 및 업무그룹의 권한 정보를 변경한 이력을 남긴다. + +=== 역할 권한 변경 이력 로깅 +* AOP를 이용하여 RoleService의 사용자 역할 권한 추가/수정/삭제 메서드가 호출될때 이력을 남긴다. +* log4j2.xml에 설정한 파일에 이력이 남는다. + +.RoleHistoryLoggingAspect.class +[source,java] +---- +@Aspect +@Component +@Log4j2 +public class RoleHistoryLoggingAspect extends HistoryLoggingSupport{ + + private static final Logger ROLE_HISTORY_LOG = LogManager.getLogger("RoleHistoryLog"); + + @Value("${node-id}") + private String nodeId; + + private final IdGenService idGenService; + + public RoleHistoryLoggingAspect(IdGenService idGenService) { + this.idGenService = idGenService; + } + + @Pointcut("execution(* com.samsung.role.impl.RoleServiceImpl.insertUserRoleList(..))") + public void insertUserRolePointcut() { + // Do nothing because pointcut + } + + @Pointcut("execution(* com.samsung.role.impl.RoleServiceImpl.updateUserRoleList(..))") + public void updateUserRolePointcut() { + // Do nothing because pointcut + } + + @Pointcut("execution(* com.samsung.role.impl.RoleServiceImpl.deleteUserRoleList(..)) || execution(* com.samsung.role.impl.RoleServiceImpl.deleteUserRole(..))") + public void deleteUserRolePointcut() { + // Do nothing because pointcut + } + + @After(value = "insertUserRolePointcut() || updateUserRolePointcut() || deleteUserRolePointcut()") + public void writeRoleHistoryLog() { + writeHistoryLog(ROLE_HISTORY_LOG, idGenService, nodeId); + } +} +---- +.HistoryLoggingSupport.class +[source,java] +---- +public class HistoryLoggingSupport { + private static final ObjectMapper mapper = new ObjectMapper(); + + @Autowired + protected WebUtil webUtil; + + public void writeHistoryLog(Logger logger, IdGenService idGenService, String nodeId) { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); + String requestUri = request.getRequestURI(); + String requestMethod = request.getMethod(); + User user = Account.currentUser(); + if(ObjectUtils.isNotEmpty(user)) { // 로그인된 사용자 + try { + HistoryLog log = new HistoryLog(); + log.setLogId(idGenService.getNextStringId()); + log.setNodeId(nodeId); + if(ObjectUtils.isNotEmpty(user)) { + log.setWorkerId(user.getUserId()); + log.setWorkerName(user.getUserName()); + } + log.setWorkDatetime(DateTime.now().toString()); + log.setRemoteAddr(webUtil.getClientIp(request)); + log.setRequestMethod(requestMethod); + log.setRequestUri(requestUri); + + String jsonVal = mapper.writeValueAsString(log); + logger.info(jsonVal); + } catch (JsonProcessingException e) { + logger.warn(e.getMessage()); + } + } + } +} +---- + +.log4j2.xml +[source,xml] +---- + + + + + + %d %-5p [%t] %-17c{2} \(%13F:%L\) - %m%n + + + + + + + + + + + + +---- + +=== 업무그룹 권한 변경 이력 로깅 +* AOP를 이용하여 WorkGroupService서비스의 업무그룹 권한 추가/수정/삭제 메서드가 호출될때 이력을 남긴다. +* log4j2.xml에 설정한 파일에 이력이 남는다. + +.WorkgroupHistoryLoggingAspect.class +[source,java] +---- +@Aspect +@Component +@Log4j2 +public class WorkgroupHistoryLoggingAspect extends HistoryLoggingSupport { + + private static final Logger WORKGROUP_HISTORY_LOG = LogManager.getLogger("WorkgroupHistoryLog"); + + @Value("${node-id}") + private String nodeId; + + private final IdGenService idGenService; + + public WorkgroupHistoryLoggingAspect(IdGenService idGenService) { + this.idGenService = idGenService; + } + + @Pointcut("execution(* com.samsung.workgroup.impl.WorkgroupServiceImpl.insertWorkgroupRoleList(..))") + public void insertWorkgroupRoleList() { + // Do nothing because pointcut + } + + @Pointcut("execution(* com.samsung.workgroup.impl.WorkgroupServiceImpl.insertWorkgroupMenuList(..))") + public void insertWorkgroupMenuList() { + // Do nothing because pointcut + } + + @Pointcut("execution(* com.samsung.workgroup.impl.WorkgroupServiceImpl.updateWorkgroupRoleList(..))") + public void updateWorkgroupRoleList() { + // Do nothing because pointcut + } + + @Pointcut("execution(* com.samsung.workgroup.impl.WorkgroupServiceImpl.updateWorkgroupMenuList(..))") + public void updateWorkgroupMenuList() { + // Do nothing because pointcut + } + + @Pointcut("execution(* com.samsung.workgroup.impl.WorkgroupServiceImpl.deleteWorkgroupRoleList(..))") + public void deleteWorkgroupRoleList() { + // Do nothing because pointcut + } + + @Pointcut("execution(* com.samsung.workgroup.impl.WorkgroupServiceImpl.deleteWorkgroupMenuList(..))") + public void deleteWorkgroupMenuList() { + // Do nothing because pointcut + } + + @After(value = "insertWorkgroupRoleList() || insertWorkgroupMenuList() || updateWorkgroupRoleList() || updateWorkgroupMenuList() || deleteWorkgroupRoleList() || deleteWorkgroupMenuList()") + public void writeWorkgroupHistoryLog() { + writeHistoryLog(WORKGROUP_HISTORY_LOG, idGenService, nodeId); + } +} +---- + +.log4j2.xml +[source,xml] +---- + + + + + + %d %-5p [%t] %-17c{2} \(%13F:%L\) - %m%n + + + + + + + + + + + + +---- \ No newline at end of file diff --git a/doc/공통기능/보안관리/관리자IP관리.adoc b/doc/공통기능/보안관리/관리자IP관리.adoc new file mode 100644 index 0000000..282121f --- /dev/null +++ b/doc/공통기능/보안관리/관리자IP관리.adoc @@ -0,0 +1,11 @@ += 관리자 IP 관리 + +== 관리자 IP 관리 +관리자용 계정의 IP를 관리 + +image::ipMgmt.png[] + +=== 기능별 설명 +- 삭제 : 등록된 계정을 삭제 +- 추가 : 관리자용 계정 정보를 등록하기 위해 Row를 추가 +- 저장 : 추가된 계정 정보를 저장 diff --git a/doc/공통기능/보안관리/리소스접근제어.adoc b/doc/공통기능/보안관리/리소스접근제어.adoc new file mode 100644 index 0000000..9a2493a --- /dev/null +++ b/doc/공통기능/보안관리/리소스접근제어.adoc @@ -0,0 +1 @@ += 리소스 접근 제어 diff --git a/doc/공통기능/보안관리/문자열암복호화.adoc b/doc/공통기능/보안관리/문자열암복호화.adoc new file mode 100644 index 0000000..fe3aa6b --- /dev/null +++ b/doc/공통기능/보안관리/문자열암복호화.adoc @@ -0,0 +1,10 @@ += 문자열 암/복호화 + +== 개요 +필요한 부분에 대하여 암/복호화를 적용하고 있다. + +=== 해쉬 알고리즘 +ID/Password 로그인의 경우 Password에 대하여 해쉬 알고리즘(org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder)을 적용하고 있다. + +=== 전자서명 +Knox Portal EpTray를 통한 로그인의 경우 전자서명된 ssoData를 시스템에 있는 개인키(Private Key)를 기반으로 검증한다. \ No newline at end of file diff --git a/doc/공통기능/보안관리/불법탈취방지.adoc b/doc/공통기능/보안관리/불법탈취방지.adoc new file mode 100644 index 0000000..18ef632 --- /dev/null +++ b/doc/공통기능/보안관리/불법탈취방지.adoc @@ -0,0 +1 @@ += 불법탈취(Hijacking) 방지 diff --git a/doc/공통기능/보안관리/비밀번호관리.adoc b/doc/공통기능/보안관리/비밀번호관리.adoc new file mode 100644 index 0000000..38be5f5 --- /dev/null +++ b/doc/공통기능/보안관리/비밀번호관리.adoc @@ -0,0 +1,23 @@ += 비밀번호 관리 + +== 개요 +ID/PW 로그인시 사용하는 비밀번호의 변경 및 초기화가 가능하다. + +=== 비밀번호 변경 +로그인된 상태에서 비밀번호 변경이 가능하다. + +image::pwdChange.png[] + +=== 비밀번호 초기화 +로그아웃된 상태에서 ID/PWD 로그인시 비밀번호 초기화를 할 수 있다. + +이메일로 임시 비밀번호가 발송된다. + +[cols=2*a] +|=== +| +.로그인 화면 +image::pwdReset_01.png[] +| +.비밀번호 초기화 화면 +image::pwdReset_02.png[] +|=== \ No newline at end of file diff --git a/doc/공통기능/보안관리/약관관리.adoc b/doc/공통기능/보안관리/약관관리.adoc new file mode 100644 index 0000000..226ec90 --- /dev/null +++ b/doc/공통기능/보안관리/약관관리.adoc @@ -0,0 +1,28 @@ += 약관 관리 + +== 이용약관동의 관리 +이용약관동의관리 화면을 관리한다. + +image::termsConditionList.png[] + +== 이용약관동의 등록 + +등록 화면에서 새로운 약관 생성이 가능하다. + +구분은 **공통코드**의 `TERMS` 정보를, 언어는 `TERMS_LANG` 정보를 참조한다. + +image::termsConditionDetail_Reg.png[] + +== 이용약관동의 목록 추가 + +그룹코드 관리에서 약관동의 목록 추가가 가능하다. + +image::termsCodeDetail.png[] + +신규 사용자 로그인 시 화면에 아래와 같이 나타난다. + +== 신규사용자 이용약관 동의 + +신규 사용자 로그인 시 화면에 아래와 같이 나타난다. + +image::termsCondition_New.png[] diff --git a/doc/공통기능/사용자관리.adoc b/doc/공통기능/사용자관리.adoc new file mode 100644 index 0000000..86bdbc2 --- /dev/null +++ b/doc/공통기능/사용자관리.adoc @@ -0,0 +1,27 @@ += 사용자 관리 + +include::사용자관리/사용자관리.adoc[leveloffset=+1] + +include::사용자관리/부서관리_Knox.adoc[leveloffset=+1] + +include::사용자관리/부서관리_사용자정의.adoc[leveloffset=+1] + +include::사용자관리/장기미사용자.adoc[leveloffset=+1] + +include::사용자관리/사용권한신청.adoc[leveloffset=+1] + +include::사용자관리/외부사용자관리.adoc[leveloffset=+1] + +include::사용자관리/권한관리기간배치.adoc[leveloffset=+1] + +include::사용자관리/역할관리.adoc[leveloffset=+1] + +include::사용자관리/업무그룹관리.adoc[leveloffset=+1] + +include::사용자관리/메뉴관리.adoc[leveloffset=+1] + +include::사용자관리/QuickMenu관리.adoc[leveloffset=+1] + +include::사용자관리/Sitemap.adoc[leveloffset=+1] + +include::사용자관리/로그인.adoc[leveloffset=+1] diff --git a/doc/공통기능/사용자관리/QuickMenu관리.adoc b/doc/공통기능/사용자관리/QuickMenu관리.adoc new file mode 100644 index 0000000..133ab7f --- /dev/null +++ b/doc/공통기능/사용자관리/QuickMenu관리.adoc @@ -0,0 +1,16 @@ += My Menu 관리 + +== My Menu 관리 +사용자가 자주 사용하는 메뉴를 등록하여 사용자 임의로 만들어서 빠른 이동을 목적으로 하는 Menu 목록. + +TopMenu - My Menu 에 구현되어 있다. + +image::MyMenu.png[] + +=== 기능별 설명 + +image::MyMenu_add.png[] +메뉴의 BreadCrumb 영역 내 메뉴명 우측 별 모양을 클릭하여 MyMenu에 등록 또는 해제 + + +NOTE: SideMenu(Right Side) 이용 시 MainOffsider.vue 내 'QuickMenu' 에 구현되어 있다. \ No newline at end of file diff --git a/doc/공통기능/사용자관리/Sitemap.adoc b/doc/공통기능/사용자관리/Sitemap.adoc new file mode 100644 index 0000000..27303d3 --- /dev/null +++ b/doc/공통기능/사용자관리/Sitemap.adoc @@ -0,0 +1,13 @@ += Sitemap + +== 개요 +로그인한 사용자의 권한에 맞는 전체메뉴를 볼 수 있다. + +TopMenu - 사이트맵 부분에 구현되어 있다. + +== 화면 +권한에 따른 전체 메뉴 목록을 나열한 화면으로 메뉴 이동 가능 + +image::siteMap.png[] + +NOTE: SideMenu(Right Side) 이용 시 MainOffsider.vue 내 'sitemap' 부분에 구현되어 있다. \ No newline at end of file diff --git a/doc/공통기능/사용자관리/권한관리.adoc b/doc/공통기능/사용자관리/권한관리.adoc new file mode 100644 index 0000000..1694912 --- /dev/null +++ b/doc/공통기능/사용자관리/권한관리.adoc @@ -0,0 +1,129 @@ += 권한 관리 + +== 권한 체크 로직 + +권한 체크는 AuthorizationInterceptor에서 사용자가 가지고 있는 메뉴의 권한 목록을 기반으로 URL Base 로 체크가 되고 있다. + +[source,java] +---- +@Override +public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + -- 생략 -- + String httpMethod = request.getMethod(); + String decodedUrl = WebUtil.getDecodedRequestUrl(request, request.getRequestURI().substring(request.getContextPath().length())); + + String queryString = ""; + if (decodedUrl.indexOf('?') > -1) { + String[] uri = decodedUrl.split("\\?"); + decodedUrl = uri[0]; + queryString = uri[1]; + } + + for (Api api : authMenuList) { + if (api.getHttpMethod().equals(httpMethod) && checkUriMatch(api.getApiPath(), api.getApiParameters(), decodedUrl, queryString)) { + return true; + } + } + throw new AuthorizationException("API 권한 없음"); +} +---- + +[source,java] +---- +public boolean checkUriMatch(String apiPath, String apiParameters, String decodedUrl, String queryString){ + Map requiredParamMap = new HashMap<>(); + if( StringUtils.isNotEmpty(apiParameters)){ + requiredParamMap = Splitter.on('&').trimResults().withKeyValueSeparator('=').split(apiParameters); + } + + UriTemplate uriTemplate = new UriTemplate(apiPath); + if ( uriTemplate.matches(decodedUrl) ){ + if( requiredParamMap.size() == 0 ) return true; + for (Map.Entry entry : requiredParamMap.entrySet()) { + log.debug("Required Param : " + entry.getKey() + " Value : " + entry.getValue()); + Map requestParamMap = getRequestParam(queryString); + if( !requestParamMap.containsKey(entry.getKey()) || !(entry.getValue()).equals(requestParamMap.get(entry.getKey()))){ + return false; + } + } + return true; + }else{ + return false; + } +} +---- + +== 권한별 메뉴 가져오기 + +SPA(Single Page Application) 방식으로 최초 메인화면 진입시에 사용자의 권한에 맞는 메뉴 목록을 조회하여 보여준다. + + +[source,java] +---- +@RestController +@RequestMapping("/auth") +public class MenuController { + --생략-- + @Operation(summary = "사용자의 권한이 있는 메뉴 조회") + @GetMapping("/menus-user-auth") + public List getMenuListByAuth() { + return menuService.getMenuListByAuth(Account.currentUser().getUserId()); + } + --생략-- +} + +---- + +일반 사용자와 시스템 어드민 사용자의 권한 메뉴 조회 서비스가 분리 되어 있다. + +[source,java] +---- +@Service +public class MenuServiceImpl implements MenuService { + --생략-- + @Override + public List getMenuListByAuth(String userId) { + if (Account.currentUser().isSystemAdminUser()) return this.getMenuListByAdminAuth(); + else return this.getMenuListByUserAuth(userId); + } + --생략-- +} +---- + +접속중인 상태에서 권한이 변경될 경우 화면 재진입(다시 로그인 또는 브라우저 새로고침)을 통해서 변경된 권한의 메뉴 목록을 다시 로드할 수 있다. + +== 권한에 따른 UI Control + +SDL에서는 역할관리에서 정의해놓은 역할별로 4개의 권한을 지정할 수 있다. + +최초 화면 진입시 조회한 사용자가 접근 가능한 메뉴 목록에 해당 메뉴에 대한 권한 목록을 함께 가지고 있으며 화면에서 버튼에 directive로 지정함으로써 show/hide 처리를 자동으로 처리한다. + +역할관리를 통한 메뉴별 권한 등록 + +image::front_01_03.png[] +vue 내에서 authorization directive를 통해 버튼 별 권한을 정의한다.(배열로 정의할 경우 여러개 중에 하나만 매칭되도 show처리 됨) + +* 여러개의 권한을 둘 경우 + +[source, html] +---- + +---- +* 하나의 권한을 둘 경우 + +[source, html] +---- + +---- +* 관리자일 경우는 directive 지정 상관 없이 무조건 show 처리된다. \ No newline at end of file diff --git a/doc/공통기능/사용자관리/권한관리기간배치.adoc b/doc/공통기능/사용자관리/권한관리기간배치.adoc new file mode 100644 index 0000000..7f800d7 --- /dev/null +++ b/doc/공통기능/사용자관리/권한관리기간배치.adoc @@ -0,0 +1,24 @@ += 권한관리 기간 배치 + +== 만료 권한 삭제 +배치를 실행하여 사용자 및 역할에 매핑된 권한 중 만료된 권한을 삭제한다. + +.config.properties +[source,properties] +---- +batch.user.auth-expired.cron=0 10 02 * * ? +---- + +== 권한만료 예정 안내 메일 발송 +배치를 실행하여 권한만료 예정인 사용자를 대상으로 안내메일을 발송한다. + +.config.properties +[source,properties] +---- +batch.user.auth-expired-mailing.cron=0 30 02 * * ? +---- + +== Spring 환경설정 +* QuartzConfig.java: Scheduler 등록 +* UserBatchConfig.java: 배치 Job, Trigger 등록 +* UserBatchExecutor.java: 서비스 호출 diff --git a/doc/공통기능/사용자관리/로그인.adoc b/doc/공통기능/사용자관리/로그인.adoc new file mode 100644 index 0000000..3c40559 --- /dev/null +++ b/doc/공통기능/사용자관리/로그인.adoc @@ -0,0 +1,185 @@ += 로그인 + +SDL은 Knox EpTray, ID/PW, ADFS 로그인을 지원한다. + +== EpTray +Knox EpTray 로그인은 Chrome, Edge 등 멀티 브라우저에서 가능하다. +사용자가 Knox에 로그인을 한 후 시스템에 접속을 하게 되면 EpTray값을 이용해 시스템 로그인을 한다. + +NOTE: Knox EpTray SSO 를 위한 연계 신청이 필요하다. (스테이지/운영) + +자세한 내용 안내 및 문의는 Knox Support (매뉴얼 > KnoxPortal NewEpTray 연계 가이드) 를 참조한다. + +=== EpTray 적용 +연계신청 과정에서 생성되는 *rsaprivkey8.pem* 파일을 + +config.properties의 login.sso.knox-tray-private-key-path 경로에 넣어준다. + +CAUTION: *rsaprivkey8.pem* 파일이 없는 경우 아래와 같은 에러가 발생하므로 주의한다. + +image::newEpTrayUtil.png[Can not load new EpTray Private Key.png] + +=== LoginPage.vue + +UI 에서 사용자 정보가 없을 경우 LoginPage.vue 페이지로 이동하게 되고 인증 절차를 시작한다. + +EpTray 연계를 위한 메서드 + +[source,javascript] +---- +async loginByNewEpTray() { + let loginResult = false; + SDLUtil.showLoadingBar(true); + await this.socConnect().then((data) => { + this.newEpTrayKey = data; + this.websocket.close(); + }).catch((err) => { + SDLUtil.alert(err); + }); + SDLUtil.showLoadingBar(false); + + if (this.newEpTrayKey) { + if (this.newEpTrayKey.result === 'success') { + const { userInfo, key } = this.newEpTrayKey; + loginResult = await this.loginNewEpTray({ userInfo, key }); + } else { + SDLUtil.alert(`sdl.epTray.error.${this.newEpTrayKey.errorCode}`); + } + } + + if (!loginResult) { + if (this.$route.query.token) { + loginService.setToken(this.$route.query.token); + document.location = `${SDLUtil.WEB_CONTEXT_PATH}/`; + } + } + this.afterLoginProcess(loginResult); +}, +---- + +[source,javascript] +---- +socConnect() { + return new Promise(((resolve, reject) => { + const server = new WebSocket('wss://localhost:29283'); // <1> + this.websocket = server; + server.onopen = () => { + server.send('{"rqtype":"getknoxsso","token":"","data":"KCC60TRAY0072"}'); // <2> + server.onmessage = (event) => { + const socketData = JSON.parse(event.data); + if (socketData.rpcode === 'RETURN_SUCCESS') { + resolve(JSON.parse(socketData.data)); + } else { + this.websocket.close(); + let err; + if (socketData.rpcode === 'EMPTY_BOX') err = 'sdl.epTray.rpCode.EMPTY_BOX'; + else err = `sdl.epTray.rpCode.${socketData.detail}`; + reject(err); + } + }; + }; + server.onerror = () => { + const err = 'sdl.epTray.rpCode.CONNECTION_FAILED'; + reject(err); + }; + })); +}, +---- +<1> WebSocket HOST 정보 +<2> 발급받은 연계용 system ID + +=== LoginController.java + +LoginController 에서는 EpTray값의 정보를 이용해 사용자를 로그인 한다. +이때 시스템 사용자가 아닐 경우, 등록 화면으로 이동하고, 정상적인 시스템 사용자일 경우 +Token을 발급한다. + +GET /noauth/login/new-eptray + +[source,java] +---- +@PostMapping("/noauth/login/new-eptray") +@Operation(value = "", description = "knox new eptray 로그인") +public JwtResponse loginByNewEptray(HttpServletRequest request, HttpServletResponse response, @RequestParam String encodeUserInfo, @RequestParam String encodeAesKey ) throws AccountException { + loginInterceptorExecutor.applyPreLogin(request, response); + User user = authenticationService.loginByNewEpTray(encodeUserInfo, encodeAesKey); + user.setRecentLoginIp(webUtil.getClientIp(request)); + JwtResponse jwtResponse = new JwtResponse(jwtUtil.generateToken(user), user.getUserId()); + user.setJwt(jwtResponse.getJwtToken().split("\\.")[2]); + loginInterceptorExecutor.applyPostLogin(request, response, user); + return jwtResponse; +} +---- + +== ADFS 로그인 + +NOTE: ADFS 통합인증 사이트 (https://adsso.sec.samsung.net) 에서 신청 후 사용해야 한다. + +자세한 내용은 해당 사이트를 참조하거나 전자통합인증3 (nextsso3@samsung.com) 으로 문의 한다. + +SDL에서는 ADFS 통합인증 사이트의 가이드를 참조하여 AD 로그인 샘플을 제공하고 있으며, 각 프로젝트 환경에 맞게 수정하여 사용한다. + +=== 참고할 샘플 파일 +* LoginPage.vue +* AdLoginController.java +* onelogin.saml.properties + +== 로그인/아웃 전,후 처리 + +로그인 전과 후, 로그아웃 전과 후 비즈니스 로직이 필요한 경우 LoginInterceptor를 상속받아 구현한다. + +LoginInterceptor 클래스는 아래와 같은 Interface를 제공한다. +[source, java] +---- + /** + * 로그인 전 + */ +default void preLogin(HttpServletRequest request, HttpServletResponse response) { +} + +/** + * 로그인 후 + */ +default void postLogin(HttpServletRequest request, HttpServletResponse response, User user) { +} + +/** + * 로그아웃 전 + */ +default void preLogout(HttpServletRequest request, HttpServletResponse response, User user) { +} + +/** + * 로그아웃 후 + */ +default void postLogout(HttpServletRequest request, HttpServletResponse response, User user) { +} +---- + +LoginInterceptor를 구현한 클래스는 LoginInterceptorExecutor에 의해 실행되며 Spring Bean으로 등록하고 SpringWebCofig에 아래와 같이 LoginInterceptorExecutor에 등록한다. + +[source, java] +---- +@Bean +public LoginInterceptorExecutor loginInterceptorExecutor() { + List loginInterceptorList = new ArrayList<>(); + loginInterceptorList.add(userUpdateInterceptor()); + loginInterceptorList.add(loginOutLogInterceptor()); + return new LoginInterceptorExecutor(loginInterceptorList); +} +---- + +아래는 로그인 후 사용자 정보를 update 하는 UserUpdateInterceptor의 일부분이다. + +[source, java] +---- +@Override + public void postLogin(HttpServletRequest request, HttpServletResponse response, User user) { + + String recentLoginIp = user.getRecentLoginIp(); + Date recentLoginDatetime = user.getRecentLoginDatetime(); + user.setLastLoginDate(recentLoginDatetime); + user.setLastLoginIp(recentLoginIp); + user.setRecentLoginDatetime(new Date()); + user.setRecentLoginIp(webUtil.getClientIp(request)); + user.setLastActivityTime(System.currentTimeMillis()); + userService.updateUser(user); + } +---- diff --git a/doc/공통기능/사용자관리/메뉴관리.adoc b/doc/공통기능/사용자관리/메뉴관리.adoc new file mode 100644 index 0000000..74e5a6a --- /dev/null +++ b/doc/공통기능/사용자관리/메뉴관리.adoc @@ -0,0 +1,248 @@ += 메뉴 관리 + +== 개요 + +메뉴와 하위 Page 및 API 목록을 관리하는 화면이다. + + +NOTE: 신규 메뉴를 등록 하거나 메뉴정보를 수정 또는 삭제할 경우 콜백함수를 통해서 메뉴관련 된 모든 캐시가 초기화 된다. + +== UI Design & Function + +=== 메뉴 관리 + +메뉴를 등록, 수정 및 관리 할 수 있는 메뉴 트리를 제공한다. + +image::menuManagement_01.png[] + +* 기능 설명 +. 메뉴 트리 목록 조회 +. 메뉴 상세정보 조회 : 트리에서 메뉴 선택시 메뉴 상세정보를 조회 한다. +. 메뉴 등록 : 현재 선택된 메뉴 하위에 새로운 메뉴를 추가 한다. +. 메뉴 수정 / 삭제 : 현재 선택된 메뉴를 수정 또는 삭제 한다. +. 메뉴 이동 : 트리에서 Drag&Drop 기능으로 선택한 메뉴를 이동할 수 있다. + +=== 페이지 관리 + +image::menuManagement_02.png[] + +* 기능 설명 +. Page 목록 조회 : 메뉴 트리에서 메뉴 선택 시 해당 메뉴에 맵핑되어 있는 페이지 목록 조회. +. Page 등록, 수정, 삭제 + +NOTE: 권한 타입은 READ, UPDATE, EXECUTE, DOWNLOAD 로 구성되어 있으며 사용자가 가지고 있는 해당 메뉴에 대한 권한 종류로 UI의 권한 제어를 하고 있다. + +자세한 내용은 <<_화면별_권한처리,화면별 권한처리>> 매뉴얼을 참고한다. + +=== API 관리 + +image::menuManagement_03.png[] + +* 기능 설명 +. API 목록 조회 +. API 등록 +. API 수정 +. API 삭제 + +=== SDL 게시판 Page, API 프리셋 추가 + +image::menuManagement_04.png[] + +* 기능 설명 +. 게시판 선택 및 Page, API 자동 추가 + +== API & Service + +=== API + +MenuController.java + +Menu와 해당 메뉴에 Mapping 되어 있는 Page, API의 등록, 수정, 삭제 및 조회하는 API를 포함하고 있다. + +* 주요 기능 API 목록 +. 메뉴 트리 목록 조회 : GET /auth/menus/menu-mgmt/level +. 메뉴 상세정보 조회 : GET /auth/menus/{menuId} +. 메뉴 등록 : POST /auth/menus +. 메뉴 수정 : PUT /auth/menus +. 메뉴 삭제 : DELETE /auth/menus/{menuId} +. 메뉴 이동 : PUT /auth/menus/move-menu +. Page 목록 조회 : GET /auth/menus/{menuId}/pages +. Page 등록 : POST /auth/menus/{menuId}/pages +. Page 수정 : PUT /auth/menus/{menuId}/pages +. Page 삭제 : DELETE /auth/menus/{menuId}/pages +. API 목록 조회 : GET /auth/menus/{menuId}/apis +. API 등록 : POST /auth/menus/{menuId}/apis +. API 수정 : PUT /auth/menus/{menuId}/apis +. API 삭제 : DELETE /auth/menus/{menuId}/apis + +== Entity Table & SQL + +=== Entity Table + +* TN_CF_SYS_RESOURCE : 리소스 +* TN_CF_SYS_RESOURCE_MAPP : 리소스 맵핑 +* TN_CF_MENU : 메뉴 +* TN_CF_PAGE : Page +* TN_CF_API : API + +=== SQL + +. 메뉴 트리 목록 조회 + +[source,xml] +---- + + SELECT M.MENU_ID, M.LABEL, + M.MENU_LEVEL, M.MENU_SEQUENCE, + --생략-- +---- + +[start=3] +. 메뉴 등록 + +[source,xml] +---- + + INSERT INTO + (MENU_ID, LABEL, + --생략-- +---- + +[start=4] +. 메뉴 수정 + +[source,xml] +---- + + UPDATE + SET LABEL = #{label}, + --생략-- +---- + +[start=5] +. 메뉴 삭제 + +[source,xml] +---- + + UPDATE + SET USE_YN = 0, + DELETE_YN = 1 + --생략-- +---- + +[start=6] +. 메뉴 이동 + +[source,xml] +---- + + UPDATE + SET UPPER_SYS_RESOURCE_ID = #{parentMenuId} + --생략-- +---- + +[source,xml] +---- + + UPDATE + SET MENU_SEQUENCE = #{moveToMenuSequence} + --생략-- +---- + +[start=7] +. Page 목록 조회 + +[source,xml] +---- + + SELECT A.API_ID, SA.AUTHORIZATION_ID, A.API_NAME, + A.API_URL, A.HTTP_METHOD, A.SERVER_ID, A.USE_YN + --생략-- +---- + +[start=12] +. API 등록 + +[source,xml] +---- + + INSERT INTO + (API_ID, API_NAME, API_URL, API_PATH, API_PARAMETERS, HTTP_METHOD, SERVER_ID, USE_YN, DELETE_YN) + --생략-- +---- + +[start=13] +. API 수정 + +[source,xml] +---- + + UPDATE + SET API_NAME = #{apiName}, + --생략-- +---- + +[start=14] +. API 삭제 + +[source,xml] +---- + + UPDATE + SET USE_YN = 0, + --생략-- +---- \ No newline at end of file diff --git a/doc/공통기능/사용자관리/부서관리_Knox.adoc b/doc/공통기능/사용자관리/부서관리_Knox.adoc new file mode 100644 index 0000000..543fb40 --- /dev/null +++ b/doc/공통기능/사용자관리/부서관리_Knox.adoc @@ -0,0 +1,83 @@ += 부서관리(Knox) + +NOTE: 해당 기능 사용 필요시 Knox API를 통해 연계 해야 한다. + +== Table +* 부서(Knox) : TN_CF_DEPT_LDAP + +== API +.KnoxDepartmentController.java + +* 부서의 데이터가 많으므로 목록에 1레벨만 불러오고 1레벨의 부서를 클릭시 "부서 관리(Knox)(하위 레벨 부서)" API를 실행하여 하위 부서를 조회한다. + +부서 관리(사용자정의) 화면에서 부서매핑을 추가할 때도 사용한다. + +. 부서 관리(Knox)(1레벨 부서) + +GET /admin/knox-department + +Query ID : selectKnoxDepartmentList + +. 부서 관리(Knox)(하위 레벨 부서) + +GET /admin/knox-department/{upperDeptCode} +Query ID : selectKnoxDepartmentListByUpperCode + +== 부서 Tree 구조 + +부서관리를 tree 구조로 보여지게 하기 위해, DB의 데이터가 parent 관계를 구성해야 한다. +LDAP 부서관리는 TN_CF_DEPT_LDAP 테이블의 dept_code(Child) 와 upper_dept_code(Parent) 관계로 tree 구조를 만들게 되는데 시스템의 최상위 dept code에는 ROOT를 입력해야 한다. +내부적으로 LDAP 부서 코드의 Root 밑의 1 level을 찾는 query가 *ROOT* 라는 코드를 찾도록 되어 있다. + +|=== +|DEPT_CODE |DEPT_NAME |DEPT_LEVEL |UPPER_DEPT_CODE + +|C00001 +|정보전략 +|1 +|ROOT + +|C000011 +|정보전략 부서1 +|2 +|C00001 + +|C00002 +|마케팅 +|1 +|ROOT + +|C000021 +|마케팅 부서1 +|2 +|C00002 +|=== + +사용자정의 부서관리는 TN_CF_DEPT_SELF 테이블의 SELF_DEPT_CODE(Child) 와 UPPER_SELF_DEPT_CODE(Parent) +관계로 tree 구조를 이루며, 시스템의 최상위 SELF_DEPT_CODE는 DEPT를 입력해야 한다. + +|=== +|DEPT_CODE |DEPT_NAME |DEPT_LEVEL |UPPER_DEPT_CODE + +|C00001 +|정보전략 +|1 +|DEPT + +|C000011 +|정보전략 부서1 +|2 +|C00001 + +|C00002 +|마케팅 +|1 +|DEPT + +|C000021 +|마케팅 부서1 +|2 +|C00002 +|=== + +== 부서 목록 + +TN_CF_DEPT_LDAP 테이블에 저장되어 있는 부서정보를 트리형태로 보여준다. + +image::deptMgmt(Knox).png[deptMgmt(Knox),300,400] \ No newline at end of file diff --git a/doc/공통기능/사용자관리/부서관리_사용자정의.adoc b/doc/공통기능/사용자관리/부서관리_사용자정의.adoc new file mode 100644 index 0000000..9483fa7 --- /dev/null +++ b/doc/공통기능/사용자관리/부서관리_사용자정의.adoc @@ -0,0 +1,79 @@ += 부서관리(사용자정의) + +== Table +* 부서(사용자정의) : TN_CF_DEPT_SELF +* 부서매핑 : TN_CF_DEPT_MAPPING + +== API +.CustomDepartmentController.java + +. 부서관리(사용자 정의) 목록 조회 + +GET /admin/department/custom + +Query ID : selectCustomDepartmentList + +. 부서 하위 레벨 조회 + +GET /admin/department/custom/dept-level-sub + +Query ID : selectCustomDepartmentSubList +* 부서의 데이터가 많으므로 목록에 1레벨만 불러오고 1레벨의 부서를 클릭시 "부서 하위 레벨 조회" API를 실행하여 하위 부서를 조회한다. + +. 부서 상세정보 조회 + +GET /admin/department/custom/dept-infos + +Query ID : selectCustomDepartmentList, selectDeptMapping +* 부서 정보와 부서 매핑을 함께 불러온다. + +. 부서 저장 + +POST /admin/department/custom + +Query ID : insertCustomDepartment +* 등록된 부서가 있는지 중복체크하고 없으면 저장한다. + +. 부서 수정 + +PUT /admin/department/custom/{selfDeptCode} + +Query ID : updateCustomDepartment +* 부서명, 정렬순서, 설명만 수정 가능하다. + +. 부서 이동 + +PUT /admin/department/custom + +Query ID : updateDeptUpperDeptCode, updateDeptSequence +* 부서는 Drag & Drop 으로 원하는 곳에 이동하여 저장할 수 있다. + +. 부서 매핑 조회 + +GET /admin/department/custom/mappings/{selfDeptCode} + +Query ID : selectDeptMapping + +. 부서 매핑 저장 + +POST /admin/department/custom/mappings/{selfDeptCode} + +Query ID : insertDeptMapping +* DB에 저장된 mapping과 화면에서 추가한 mapping을 비교하여 DB에 없는 mapping만 insert 한다. + +. 부서 매핑 삭제 + +DELETE /admin/department/custom/mappings/{selfDeptCode}/{deptCode} + +Query ID : deleteDeptMapping + +== 부서관리(사용자정의) 기본 정보 +사용자의 임의로 부서를 등록 및 해당되는 부서의 Knox 부서 매핑. + +image::deptList.png[] + +=== 기본정보 필드 설명 +- 부서코드 : 부서에 부여되는 코드(Unique) +- 부서명 : 부서명칭 +- 부서레벨 : 부서에 부여되는 Level이며 최상위 Department 부서는 0 level이다. +- 정렬순서 : 같은 레벨 상 나오는 부서의 순서 +- 설명 : 부서에 대한 설명 + +=== 기본정보 기능별 설명 +- 삭제 : 부서목록의 부서를 선택 후 삭제 버튼 클릭시 선택된 부서 삭제(하위 부서 포함) +- 추가 : 추가하고자 하는 상위 부서를 선택 후 추가 버튼을 클릭시 Tree 구조에 New Document 생성 +- 저장 : 추가로 생성된 또는 선택된 부서 정보를 저장 + +== 부서 매핑 +해당 부서에 대한 Knox 부서 매핑 정보. + +=== 부서 매핑 정보 필드 설명 +- 부서코드 : Knox 부서에 부여되는 코드(Unique) +- 부서명 : Knox 부서명칭 +- 삭제 : 삭제 실행 버튼 + +=== 기능별 설명 +- 추가 : Knox 부서 Popup 호출 +- 저장 : 호출된 Knox 부서 Popup 화면에서 추가하고자 하는 부서 매핑 정보에 저장. \ No newline at end of file diff --git a/doc/공통기능/사용자관리/사용권한신청.adoc b/doc/공통기능/사용자관리/사용권한신청.adoc new file mode 100644 index 0000000..5b5a432 --- /dev/null +++ b/doc/공통기능/사용자관리/사용권한신청.adoc @@ -0,0 +1,23 @@ += 사용권한 신청/승인 + +== 개요 +사용자가 시스템에 가입시 시스템 사용 권한을 승인/승인 취소/삭제를 하여 시스템을 사용/미사용하게 한다. + +== Table +* 사용자 : TN_CF_USER + +== API +.UserController.java + +. 사용자 승인 + +PUT /auth/users/status/confirm + +Query ID : updateUser + +. 사용자 승인취소 + +PUT /auth/users/status/inactive + +Query ID : updateUser + +. 사용자 삭제 + +DELETE /auth/users + +Query ID : updateUser +* DeleteMapping이지만 DB 삭제가 아닌 ACTIVE_FLAG = 0, DELETED = 1 로 업데이트 한다. \ No newline at end of file diff --git a/doc/공통기능/사용자관리/사용자관리.adoc b/doc/공통기능/사용자관리/사용자관리.adoc new file mode 100644 index 0000000..c8e657c --- /dev/null +++ b/doc/공통기능/사용자관리/사용자관리.adoc @@ -0,0 +1,30 @@ += 사용자 관리 + +== 사용자 관리 화면 +사용자 목록을 조회하며 시스템 사용 신청 대기자에 대한 사용 승인 및 취소가 가능하다. + +사용자 조회 목록의 Row를 클릭시 사용자 상세 정보 화면으로 이동할 수 있다. + +image::userMgmt.png[] + +=== 기능별 설명 +- 엑셀다운로드 : 조회 조건과 동일한 조회 결과 값을 담은 엑셀 파일을 다운로드 하는 기능. +- 삭제 : 선택된 사용자를 삭제 처리하는 기능. +* 삭제된 사용자의 row 데이터는 남아 있지만 사용자의 정보(EP ID, 사용자 이름 제외)는 모두 삭제 된다. +- 승인취소 : 선택된 사용자를 승인취소 처리하는 기능. +- 승인 : 선택된 사용자를 승인 처리하는 기능. +* 세부적인 내용은 <<_사용권한_신청승인,사용권한 신청/승인>> 매뉴얼을 참조한다. + +== 사용자 상세 조회 화면 +사용자의 상세정보표시 및 조회한 사용자에 대한 역할 및 메뉴 추가/삭제가 가능하다. + +image::userInfo.png[] + +=== 기능별 설명 +- 사용자-역할 삭제 : 선택된 역할을 삭제 +- 사용자-역할 추가 : 선택된 역할을 추가하는 기능. 역할 팝업이 호출 되며, 사용자에게 부여하고 싶은 역할을 선택하여 확인 버튼 클릭시 사용자-역할 Grid에 추가된다. +- 사용자-역할 저장 : 추가 또는 수정된 역할이 있을 경우, 저장 기능 수행. +- 사용자-메뉴 삭제 : 선택된 메뉴를 삭제 +- 사용자-메뉴 추가 : 선택된 메뉴를 추가하는 기능. 메뉴 팝업이 호출 되며, 사용자에게 부여하고 싶은 메뉴을 선택하여 확인 버튼 클릭시 사용자-메뉴 Grid에 추가된다. +* 메뉴의 Page, API 권한 타입(READ, UPDATE, EXECUTE, DOWNLOAD) 중 필요한 권한을 check하여 등록한다. +* 권한 타입에 대한 설명은 메뉴관리 가이드를 참고한다. <<_메뉴_관리,메뉴 관리>> +- 사용자-메뉴 저장 : 추가 또는 수정된 메뉴이 있을 경우, 저장 기능 수행 \ No newline at end of file diff --git a/doc/공통기능/사용자관리/업무그룹관리.adoc b/doc/공통기능/사용자관리/업무그룹관리.adoc new file mode 100644 index 0000000..3548c1f --- /dev/null +++ b/doc/공통기능/사용자관리/업무그룹관리.adoc @@ -0,0 +1,228 @@ += 업무 그룹 관리 + +== 개요 + +메일 그룹 및 맵핑 정보 관리 기능 제공. + +== UI Design & Function + +=== 업무 그룹 목록(WorkgroupList.vue) + +업무별로 업무그룹을 만들고 해당 업무그룹에서 사용가능한 메뉴를 선택, 해당 메뉴 사용 가능한 Role 또는 User를 추가한다. + +image::workgroupList.png[업무 그룹 목록] + +* 기능 설명 +. 업무그룹 목록 조회 +. 업무그룹 상세정보 조회 +. 업무그룹 등록 : 업무그룹 정보를 입력하는 Popup이 호출 되며, 정보 기입 후 저장 버튼을 클릭하여 등록한다. +. 업무그룹 수정 : 선택된(checkbox : true) 업무그룹 정보를 수정하는 Popup이 호출 되며 역할 정보 기입 후 저장 버튼을 클릭하여 수정한다. +. 업무그룹 삭제 : 선택된(checkbox : true) 역할 목록을 삭제한다. + +=== 업무그룹-메뉴(WorkgroupMenuInfo.vue) + +업무그룹에서 사용 가능한 메뉴를 관리 하는 화면. + +메뉴 등록 및 삭제가 가능하며 메뉴별 권한 설정이 가능하다. + +image::workgroupMenuInfo.png[업무그룹-메뉴] + +* 기능 설명 +. 업무그룹-메뉴 목록 조회 +. 업무그룹-메뉴 등록 : 메뉴를 선택하는 Popup이 호출 되며 선택 후 저장 버튼을 클릭하여 등록한다. +. 업무그룹-메뉴 수정 : 권한을 선택(checkbox : true)하여 해당 메뉴에 대한 권한을 수정한다. +. 업무그룹-메뉴 삭제 : 선택된(checkbox : true) 메뉴 목록을 삭제한다. + +=== 업무그룹-역할(WorkgroupMenuInfo.vue) + +선택한 업무그룹에 역할 또는 사용자를 관리하는 화면. + +역할과 사용자를 추가 또는 삭제할 수 있다. + +image::workgroupRoleInfo.png[업무그룹-역할] + +* 기능 설명 +. 업무그룹-역할 목록 조회 +. 업무그룹-역할 등록 +.. 사용자 등록 : 사용자 추가 버튼 클릭 시 사용자를 선택할 수 있는 Popup이 호출 되며 선택 후 저장 버튼을 클릭하여 등록 한다. +.. 역할 등록 : 역할 추가 버튼 클릭 시 역할을 선택할 수 있는 Popup이 호출 되며 선택 후 저장 버튼을 클릭하여 등록 한다. +. 업무그룹-역할 수정 : 등록된 사용자의 업무그룹 만료일을 수정 한다. +. 업무그룹-역할 삭제 : 선택된(checkbox : true) 역할(사용자) 목록을 삭제한다. + +== API & Service + +=== API + +* API : WorkgroupController.java + +. 업무그룹 목록 조회 : GET /auth/workgroups-with-paging +. 업무그룹 상세정보 조회 : GET /auth/workgroups/{workgroupId} +. 업무그룹 등록 : POST /auth/workgroups +. 업무그룹 수정 : PUT /auth/workgroups/{workgroupId} +. 업무그룹 삭제 : DELETE /auth/workgroups +. 업무그룹-메뉴 목록 조회 : GET /auth/workgroups/{workgroupId}/menus +. 업무그룹-메뉴 등록 : POST /auth/workgroups/{workgroupId}/menus +. 업무그룹-메뉴 수정 : PUT /auth/workgroups/{workgroupId}/menus +. 업무그룹-메뉴 삭제 : DELETE /auth/workgroups/{workgroupId}/menus +. 업무그룹-역할 목록 조회 : GET /auth/workgroups/{workgroupId}/roles +. 업무그룹-역할 등록 : POST /auth/workgroups/{workgroupId}/roles +. 업무그룹-역할 수정 : PUT /auth/workgroups/{workgroupId}/roles +. 업무그룹-역할 삭제 : DELETE /auth/workgroups/{workgroupId}/roles + +* Service : WorkgroupServiceImpl.java + +. 업무그룹-메뉴 목록 조회. + +메뉴의 전체 경로 셋팅 및 권한별 row 데이터를 메뉴 row 데이터로 가공한다. + +[source,java] +---- +@Override +public List getWorkgroupMenuList(String workgroupId) { + + List workgroupMenuDtoList = new ArrayList<>(); + List authorizationIdList; + String beforeMenuId = ""; + -- 생략 -- +---- + +== Entity Table & SQL + +=== Entity Table + +* TN_CF_WORKGROUP : 업무그룹 +* TN_CF_WORK_AUTHORIZATION : 업무그룹 메뉴 권한 맵핑 +* TN_CF_WORKGROUP_ROLE : 업무그룹 역할 맵핑 + +=== SQL + +. 업무그룹 목록 조회 + +[source,xml] +---- + + SELECT + FROM + + +---- + +[start=3] +. 업무그룹 등록 + +[source,xml] +---- + + INSERT INTO + () + --생략-- +---- + +[start=4] +. 업무그룹 수정 + +[source,xml] +---- + + UPDATE + SET WORKGROUP_NAME = #{workgroupName}, + --생략-- +---- + +[start=5] +. 업무그룹 삭제 + +[source,xml] +---- + + UPDATE + SET DELETE_YN = 1, + --생략-- +---- + +[start=6] +. 업무그룹-메뉴 목록 조회 + +[source,xml] +---- + + SELECT W.WORKGROUP_ID, W.WORKGROUP_NAME, W.DESCRIPTION, + W.LABEL + --생략-- +---- + +[start=10] +. 업무그룹-역할 등록 + +[source,xml] +---- + + INSERT INTO + (WORKGROUP_ID, USER_ROLE_ID, FROM_DATE, THRU_DATE, ROLE_ID, USER_ID, + --생략-- +---- + +[start=11] +. 업무그룹-역할 수정 + +[source,xml] +---- + + UPDATE + SET THRU_DATE = #{thruDate}, + --생략-- +---- + +[start=12] +. 업무그룹-역할 삭제 + +[source,xml] +---- + + DELETE FROM + WHERE WORKGROUP_ID = #{workgroupId} + --생략-- +---- \ No newline at end of file diff --git a/doc/공통기능/사용자관리/역할관리.adoc b/doc/공통기능/사용자관리/역할관리.adoc new file mode 100644 index 0000000..aabd43a --- /dev/null +++ b/doc/공통기능/사용자관리/역할관리.adoc @@ -0,0 +1,207 @@ += 역할 관리 + +== 개요 + +역할을 등록하고, 해당 역할에 사용자 및 업무그룹을 Mapping 한다. + +== UI Design & Function + +=== 역할 목록(RoleList.vue) + +역할 등록, 수정 및 조회. + +조회된 목록의 사용자/업무그룹 컬럼을 클릭하여 역할-사용자, 역할-업무그룹 상세 화면으로 이동한다. + +image::roleList.png[역할 관리] + +* 기능 설명 +. 역할 목록 조회 +. 역할 등록 : 역할 정보를 입력하는 Popup이 호출 되며 정보 기입 후 저장 버튼을 클릭하여 등록한다. +. 역할 수정 : 선택된(checkbox : true) 역할 정보를 수정하는 Popup이 호출 되며, 정보 기입 후 저장 버튼을 클릭하여 수정한다. +. 역할 삭제 : 선택된(checkbox : true) 역할 목록을 삭제한다. +. 엑셀 다운로드 : 조회 조건과 동일한 역할 목록을 엑셀 파일로 다운로드 한다. + +=== 역할-사용자 관리(RoleUserInfo.vue) + +선택한 역할에 대한 사용자 추가. + +역할에 대한 사용자가 많을 경우를 대비하여 상단에 페이징 처리 되는 Grid를, 하단에는 사용자를 추가하는 Grid로 나누어 화면을 구성된다. + +image::roleUserInfo.png[역할-사용자 관리] + +* 기능 설명 +. 역할-사용자 목록 조회 +. 역할-사용자 등록 : 추가 버튼 클릭시 사용자 조회 Popup이 호출 되며 사용자 선택 후 추가된 하단의 사용자 목록에서 저장한다. +. 역할-사용자 수정 : 기등록된 상단 사용자 목록에서 역할 적용 기간을 수정한다. +. 역할-사용자 삭제 : 기등록된 상단 사용자 목록에서 선택된(checkbox : true) 사용자 목록을 삭제한다. + +=== 역할-업무그룹 관리(RoleWorkgroupInfo.vue) + +선택한 역할에 대한 업무그룹 추가. + +image::roleWorkgroupInfo.png[역할-업무그룹 관리] + +* 기능 설명 +. 역할-업무그룹 목록 조회 +. 역할-업무그룹 등록 : 추가 버튼 클릭시 업무그룹 조회 Popup이 호출 되며 업무그룹을 선택 하여 등록 할 수 있다. +. 역할-업무그룹 삭제 : 기등록된 상단 업무그룹 목록에서 선택된(checkbox : true) 업무그룹 목록을 삭제한다. + +== API & Service + +=== API + +* API : RoleController.java + +. 역할 목록 조회 : GET /roles-with-paging +. 역할 상세정보 조회 : GET /roles/{roleId} +. 역할 등록 : POST /roles +. 역할 수정 : PUT /roles +. 역할 삭제 : DELETE /roles +. 역할-사용자 목록 조회 : GET /roles/{roleId}/users-with-paging +. 역할-사용자 등록 : POST /roles/{roleId}/users +. 역할-사용자 수정 : PUT /roles/{roleId}/users +. 역할-사용자 삭제 : DELETE /roles/{roleId}/users +. 역할-업무그룹 목록 조회 : GET /roles/{roleId}/workgroups +. 역할-업무그룹 등록 : POST /roles/{roleId}/workgroups +. 역할-업무그룹 삭제 : DELETE /roles/{roleId}/workgroups + +== Entity Table & SQL + +=== Entity Table + +* TN_CF_ROLE : 역할 +* TN_CF_USER_ROLE : 역할-사용자 맵핑 +* TN_CF_WORKGROUP_ROLE : 역할-업무그룹 맵핑 + +=== SQL + +. 역할 목록 조회 + +[source,xml] +---- + + SELECT + FROM + --생략-- +---- + +[start=3] +. 역할 등록 + +[source,xml] +---- + + INSERT INTO + () + --생략-- +---- + +[start=4] +. 역할 수정 + +[source,xml] +---- + + UPDATE + SET ROLE_NAME = #{roleName}, + --생략-- +---- + +[start=5] +. 역할 삭제 + +[source,xml] +---- + + UPDATE + SET DELETE_YN = 1, + --생략-- +---- + +[start=6] +. 역할-사용자 목록 조회 + +[source,xml] +---- + + SELECT W.WORKGROUP_ID, W.WORKGROUP_NAME, W.DESCRIPTION, + W.LABEL + --생략-- +---- + +[start=11] +. 역할-업무그룹 등록 + +[source,xml] +---- + + INSERT INTO + (WORKGROUP_ID, USER_ROLE_ID, FROM_DATE, THRU_DATE, ROLE_ID, USER_ID, + --생략-- +---- + +[start=12] +. 역할-업무그룹 삭제 + +[source,xml] +---- + + DELETE FROM + WHERE WORKGROUP_ID = #{workgroupId} + --생략-- +---- \ No newline at end of file diff --git a/doc/공통기능/사용자관리/외부사용자관리.adoc b/doc/공통기능/사용자관리/외부사용자관리.adoc new file mode 100644 index 0000000..4c94df2 --- /dev/null +++ b/doc/공통기능/사용자관리/외부사용자관리.adoc @@ -0,0 +1,15 @@ += 외부 사용자 관리 + +== 개요 +ID/PW 회원가입을 통해서 등록한 사용자 목록을 조회하는 화면이다. + +== 화면 +외부 사용자 목록 조회 및 엑셀 다운로드 기능을 제공한다. + +NOTE: 사용자 테이블(TN_CF_USER)의 외부 사용자 여부(EXTERNAL_FLAG) 정보를 통해서 확인할 수 있다. + +사용자 관리 메뉴에서 관리자 승인이 된 사용자 정보만 조회할 수 있다. + +image::externalUserList.png[] + +=== 기능별 설명 +- 엑셀다운로드 : 조회 조건과 동일한 조회 결과 값을 담은 엑셀 파일을 다운로드 하는 기능. \ No newline at end of file diff --git a/doc/공통기능/사용자관리/장기미사용자.adoc b/doc/공통기능/사용자관리/장기미사용자.adoc new file mode 100644 index 0000000..b0a49be --- /dev/null +++ b/doc/공통기능/사용자관리/장기미사용자.adoc @@ -0,0 +1,25 @@ += 장기미사용자 + +== 개요 +일정 기간 시스템에 로그인(TN_CF_USER.RECENT_LOGIN_DATETIME)을 하지 않은 사용자는 장기 미사용자로 관리한다. + +== 설명 +장기 미사용자 관리 배치를 실행하면서 3개월간 로그인 하지 않은 사용자를 조회한다. + + +* config.properties 파일에서 개월수(기본 3개월) 및 배치 실행시간을 변경 할 수 있다. +[source,properties] +---- +batch.user.long-term.month=3 +batch.user.long-term-check.cron=0 10 00 * * ? +---- + +* 조회된 장기 미사용자에게 매핑된 메뉴, 업무그룹, 역할을 삭제하고 장기 미접속자로 상태를 변경한다. 사용자 관리 재직상태 컬럼(TN_CF_USER.EXPIRE_STATUS_CODE)에서 확인 할 수 있다. + +== 배치 설정 + +=== Spring 환경설정 +* QuartzConfig.java: Scheduler 등록 +* UserBatchConfig.java: 배치 Job, Trigger 등록 +* UserBatchExecutor.java: 서비스 호출 + +SDL의 배치 작업은 Quartz 를 이용하여 구현하고 있다. 자세한 내용은 <<_작업_스케쥴링, 작업 스케쥴링>> 및 <<_quartz_clustering_with_jdbc_jobstore, Quartz Clustering with JDBC-JobStore>> 을 참고한다. \ No newline at end of file diff --git a/doc/공통기능/시스템관리.adoc b/doc/공통기능/시스템관리.adoc new file mode 100644 index 0000000..9a4f561 --- /dev/null +++ b/doc/공통기능/시스템관리.adoc @@ -0,0 +1,23 @@ += 시스템 관리 + +include::시스템관리/개발툴.adoc[leveloffset=+1] + +include::시스템관리/게시판관리.adoc[leveloffset=+1] + +include::시스템관리/공지사항알림.adoc[leveloffset=+1] + +include::시스템관리/메인페이지.adoc[leveloffset=+1] + +include::시스템관리/링크사이트.adoc[leveloffset=+1] + +include::시스템관리/일괄작업관리.adoc[leveloffset=+1] + +include::시스템관리/작업스케쥴링.adoc[leveloffset=+1] + +include::시스템관리/캐시관리.adoc[leveloffset=+1] + +include::시스템관리/코드관리.adoc[leveloffset=+1] + +include::시스템관리/업무담당자관리.adoc[leveloffset=+1] + +include::시스템관리/주요전화번호관리.adoc[leveloffset=+1] diff --git a/doc/공통기능/시스템관리/개발툴.adoc b/doc/공통기능/시스템관리/개발툴.adoc new file mode 100644 index 0000000..eac75f3 --- /dev/null +++ b/doc/공통기능/시스템관리/개발툴.adoc @@ -0,0 +1,3 @@ += 개발툴/빌드 스크립드 + +SDL을 이용한 웹 어플리케이션 개발에 필요한 툴과 빌드, 배포 방법은 <<_설치,설치>>를 참고한다. \ No newline at end of file diff --git a/doc/공통기능/시스템관리/게시판관리.adoc b/doc/공통기능/시스템관리/게시판관리.adoc new file mode 100644 index 0000000..0cc2f92 --- /dev/null +++ b/doc/공통기능/시스템관리/게시판관리.adoc @@ -0,0 +1,116 @@ += 게시판 관리 + +== 개요 + +SDL에서 제공하는 게시판 관리 기능으로 공지사항(메인화면 공지팝업 연동)이나 FAQ 게시판 등 Community 성격으로 게시글 등록 및 답변기능을 사용할 수 있는 게시판 기능이다. + +관리 기능에서 게시판 등록 및 관리를 할 수 있으며 사용자용과 관리자용 메뉴를 따로 만들어 권한별로 게시판 기능을 사용할 수 있도록 제공한다. + +== Table + +* 게시판 : TN_CF_BOARD +* 게시판 분류 : TN_CF_BOARD_CLASSIFICATION +* 게시판 컬럼 : TN_CF_BOARD_COLUMN + +== API + +.BoardController.java + +. 게시판 목록 조회(페이징) + +GET /boards-with-paging + +Query ID : selectBoardPagingList + +. 게시판 상세정보 조회 + +GET /boards/{boardId} + +Query ID : selectBoard + +. 게시판 등록 + +POST /boards + +Query ID : insertBoard, insertBoardClassification, insertBoardColumn +* 게시판 등록 시 등록화면에서 추가한 게시판 분류 목록과 default 컬럼 정보가 저장된다. + +[source,java] +---- +@Override +@Transactional +public void insertBoard(Board board) { + + // 게시판 저장. + boardDao.insertBoard(board); + + // 게시판 분류 저장. + if (!board.getClassifications().isEmpty()) { + for (BoardClassification boardClassification : board.getClassifications()) { + boardClassification.setBoardId(board.getBoardId()); + boardClassification.setClassificationId(idGenService.getNextStringId()); + boardDao.insertBoardClassification(boardClassification); + } + } + + // 게시판 컬럼 저장. + boardDao.insertBoardColumn(board.getBoardId()); +} +---- + +[start=5] +. 게시판 수정 + +PUT /boards/{boardId} + +Query ID : updateBoard, updateBoardClassification, updateBoardColumn +* 게시판 수정시 변경된 게시판 분류 목록과 컬럼 정보가 수정된다. + +[source,java] +---- +@Override +@Transactional +public void updateBoard(Board board) { + + // 게시판 수정. + boardDao.updateBoard(board); + + // 게시판 분류 수정. + List beforeClassifications = boardDao.getBoardClassificationList(board.getBoardId()); + List beforeClassificationIdList = new ArrayList<>(); + -- 생략 -- +} +---- + +[start=6] +. 게시판 삭제 + +DELETE /boards/{boardId} + +Query ID : deleteBoard + +== 화면 + +. 게시판 목록 화면 + +image::boardManagement_01.png[] + +* 게시판 목록을 확인할 수 있다. +* 해당 게시판의 등록된 게시글 등록건수와 현재 사용 여부등의 정보를 보여준다. +* 등록 버튼 클릭 시 등록화면으로 이동되며 게시판 명 클릭 시 게시판 수정화면으로 이동된다. + +[start=2] +. 게시판 등록 화면 + +image::boardManagement_02.png[] + +* 게시판을 등록할 수 있다. +* 게시판 세부기능 속성 +** 에디터 사용여부 : CafeNote 등 에디터를 사용할지 선택('미사용' 선택 시 textarea 태그로 구현된다) +** 이미지파일사용 : 이미지 파일 등록 시 본문에 이미지를 표시한다.(사진형 게시판일 경우 필수사용) +** 메인 공지팝업 : 게시글 등록 시 메인 공지사항 팝업창에 해당 기간동안 게시글이 노출된다. +** 게시글 '공지'라벨 표시 : 게시글 등록 시 게시글 목록 상단에 공지 게시글로 노출된다. + +[start=3] +. 게시판 상세정보 수정 화면 + +image::boardManagement_03.png[] + +* 게시판 세부기능 속성 변경이 가능하다. +* '공지팝업 제한' 기능은 해당 게시판의 공지게시글을 일시적으로 제한할 수 있는 기능이다. + +image::boardManagement_04.png[] + +* 게시판의 검색 조건을 선택할 수 있다. +* 게시글 목록화면에서 보여줄 정보를 선택 할수 있다. +** 각 컬럼의 사용 여부에 따라서 넓이가 합산 100%로 변경된다. +* 게시글 목록 정렬 기준을 선택 할 수 있으며 작성일 내림차순이 기본으로 선택된다. \ No newline at end of file diff --git a/doc/공통기능/시스템관리/공지사항알림.adoc b/doc/공통기능/시스템관리/공지사항알림.adoc new file mode 100644 index 0000000..1e87382 --- /dev/null +++ b/doc/공통기능/시스템관리/공지사항알림.adoc @@ -0,0 +1,29 @@ += 공지사항 알림 뱃지 + +== 개요 +관리자가 작성한 공지사항이 등록되면 메인페이지에 알림이 보인다. + +== Table +* 게시판 : TN_CF_BOARD +* 게시글 : TN_CF_POST +* 게시판 분류 : TN_CF_BOARD_CLASSIFICATION +* 사용자 : TN_CF_USER +* 공지사항 확인 : TN_CF_NOTICE_CHECKED + +== API +.BoardController.java + +. 공지사항 팝업 게시글 목록 조회(메인화면 용) + +GET /notice-popup-posts + +Query ID : selectNoticePopupPostList + +. 공지사항 팝업 게시글 체크 + +POST /check-notice + +Query ID : selectNoticeChecked, insertNoticeChecked + +== 화면 +* MainTopMenu.vue + +image::notice_alarm.png[800,600] + +image::notice.png[800,600] \ No newline at end of file diff --git a/doc/공통기능/시스템관리/공통페이지.adoc b/doc/공통기능/시스템관리/공통페이지.adoc new file mode 100644 index 0000000..a5c8a1d --- /dev/null +++ b/doc/공통기능/시스템관리/공통페이지.adoc @@ -0,0 +1 @@ += 공통 페이지 diff --git a/doc/공통기능/시스템관리/링크사이트.adoc b/doc/공통기능/시스템관리/링크사이트.adoc new file mode 100644 index 0000000..d805d5e --- /dev/null +++ b/doc/공통기능/시스템관리/링크사이트.adoc @@ -0,0 +1,39 @@ += 링크 사이트 + +== 개요 +링크 사이트를 관리한다. + +== Table +* 링크사이트 : TN_CF_LINK_SITE + +== API +.LinkSiteController.java + +. 링크사이트 목록 조회(페이징) + +GET /linksite/linksites-with-paging + +Query ID : selectLinkSitePagingList + +. 링크사이트 등록 + +POST /linksite/linksites + +Query ID : insertLinkSite +* LINK_SITE_ID는 IdGenService를 사용하여 유니크한 ID를 만들어서 등록한다. + +. 링크사이트 수정 + +PUT /linksite/linksites + +Query ID : updateLinkSite + +. 링크사이트 삭제 + +DELETE /linksite/linksites + +Query ID : deleteLinkSite + +== 화면 + +외부 링크사이트를 관리기능 > 기타관리 > 링크사이트 관리를 통해 할 수 있다. + +image::front_05_01.png[] + +* 구분 : 공통코드 type이 'LINKSITE'인 항목 +* 사이트명(기본) : 기본 사이트명을 입력한다. +* 사이트명(locale) : 다국어별 사이트명을 입력한다. +* 설명 : 사이트에 대한 설명을 입력한다. +* URL : 사이트의 URL을 등록한다. diff --git a/doc/공통기능/시스템관리/메인페이지.adoc b/doc/공통기능/시스템관리/메인페이지.adoc new file mode 100644 index 0000000..f8a60d6 --- /dev/null +++ b/doc/공통기능/시스템관리/메인페이지.adoc @@ -0,0 +1,12 @@ += 메인 페이지 + +== 구성 +* Header Section (TopMenu) +** Navigation - My Menu(즐겨찾기) | navigation(결재함, 게시판, 샘플 페이지, 관리기능) +** RightSide - 메인공지팝업 | 번역, 통합검색, 사이트맵 | 표준시간, 언어선택 | 프로필 ++ +NOTE: 통합검색의 경우 검색솔루션 등을 통해 따로 구현해야함. +* MainContainer Section - 메인 컨텐츠(공지사항, FAQ, 연락처 등) +* Footer Section - 개인정보취급방침, 이용약관 + +image::mainPage.png[] diff --git a/doc/공통기능/시스템관리/업무담당자관리.adoc b/doc/공통기능/시스템관리/업무담당자관리.adoc new file mode 100644 index 0000000..3198979 --- /dev/null +++ b/doc/공통기능/시스템관리/업무담당자관리.adoc @@ -0,0 +1,24 @@ += 업무 담당자 관리 + +== 개요 +주요 업무 담당자 페이지를 관리한다. + +NOTE: 정적 HTML 로 만든 페이지를 업로드 하여 그대로 보여주고 싶을 때 사용한다. (업무 담당자 관리, 주요 연락처 관리) + +== 화면 +image::front_06_01.png[] +* 등록된 업무 담당자 페이지 목록을 볼 수 있다. +* 목록의 Key 컬럼을 클릭하여 업무 담당자 페이지 정보를 수정할 수 있다. +* 팝업 미리보기를 클릭하여 등록된 업무 담당자 페이지를 확인 할 수 있다. + +image::front_06_02.png[] +* Key: Unique한 키를 지정(영문, 숫자만 가능) +* 제목: 업무 담당자 페이지 제목 +* 설명: 업무 담당자 페이지 설명 +* 첨부파일: 업무 담당자 HTML 페이지 (html, htm 확장자) + +image::front_06_02_01.png[] +* 메뉴관리에서 메뉴를 등록하고 Page/API 를 추가한다. + - Page Path에 업무 담당자 관리에서 등록한 Key를 포함한다. + - Vue Component는 SampleStaff.vue 를 참조하여 생성 후 등록한다. + - API URL에 업무 담당자 조회 API URL을 등록한다. (/html-templates/group/{templateGroupCode=STAFF}/key/{templateKey}) \ No newline at end of file diff --git a/doc/공통기능/시스템관리/일괄작업관리.adoc b/doc/공통기능/시스템관리/일괄작업관리.adoc new file mode 100644 index 0000000..c895d92 --- /dev/null +++ b/doc/공통기능/시스템관리/일괄작업관리.adoc @@ -0,0 +1,23 @@ += 일괄 작업 관리 및 이력 조회 + +== 일괄작업 관리 +* 일괄작업 관리를 위해서는 **구현 Service** (com.samsung..*Impl.*) 의 메서드에 `@BatchJob` annotation이 설정되어 있어야 한다. +* 관리기능 > 일괄작업 관리> 일괄작업 관리 메뉴에서 아래와 같이 설정해야만 이력관리가 남게 된다. + +image::batchJobMgmt.png[] + +image::batchJobMgmtUpdate.png[] + +* 일괄작업 관리 화면에서 관리하고자 하는 Batch 정보를 위와 같이 입력한다. +** 구분 : 그룹코드 BATCHGUBUN 에 등록한 공통코드명을 입력한다. +** 작업명 : Batch 작업명을 입력한다. +** 작업클래스 : com.samsung.accesslog.impl.SysUseLogMngImpl.loadBatchData과 같이 Batch 실행시 실행되는 Package명을 포함한 클래스명 및 메소드명을 입력한다. (`@BatchJob` annotation에 설정한 값) +** URL : Batch를 직접 실행하기 위한 URL을 입력한다. 여기에 입력한 URL은 일괄작업이력 화면에서 해당 배치실행 버튼을 클릭하였을때 Call 된다. +URL을 입력할 경우에는 해당 Request를 처리할 Controller를 구현해야 한다. + +== 일괄작업 이력 +일괄작업 관리에 등록한 작업이 수행되면 일괄작업 이력에 아래와 같이 나타난다. + +image::batchJobLogList.png[] +* 배치가 실행되는 시작 시간과 종료시간, 소요시간, 작업결과 등이 나타난다. +* 작업이 실패 했을경우 실행 할 수 있는 배치실행 버튼이 나타난다.(일괄작업관리 등록시 URL을 등록해야 나타남). diff --git a/doc/공통기능/시스템관리/작업스케쥴링.adoc b/doc/공통기능/시스템관리/작업스케쥴링.adoc new file mode 100644 index 0000000..4336204 --- /dev/null +++ b/doc/공통기능/시스템관리/작업스케쥴링.adoc @@ -0,0 +1,52 @@ += 작업 스케쥴링 + +== 개요 +SDL의 작업 스케쥴링은 http://www.quartz-scheduler.org/documentation/[Quartz]를 이용하여 구현하고 있다. +수행할 작업(Job)을 등록하고 Trigger에 Job을 추가한 후 Scheduler에 Trigger(s)를 설정한다. + +=== 스케줄러 설정 예 +.config.properties +[source,properties] +---- +batch.user.long-term-check.cron=0 10 00 * * ? +---- + +.UserBatchConfig +[source,java] +---- +@Configuration +public class UserBatchConfig { + + @Value("${batch.user.long-term-check.cron}") + private String batchUserLongTermCheckCron; + + /** + * 장기 미사용자 관리 Job + */ + @Bean + public JobDetail batchUserLongTermCheckJob() { + return JobBuilder + .newJob(UserBatchExecutor.class) // <1> + .withIdentity("batchUserLongTermCheck") // <2> + .withDescription("User LongTerm Check Batch") + .storeDurably(true) + .build(); + } + + /** + * 장기 미사용자 관리 Trigger + */ + @Bean + public CronTriggerFactoryBean batchUserLongTermCheckTrigger() { + CronTriggerFactoryBean trigger = new CronTriggerFactoryBean(); + trigger.setJobDetail(batchUserLongTermCheckJob()); // <3> + trigger.setCronExpression(batchUserLongTermCheckCron); // <4> + return trigger; + } + +} +---- +<1> 서비스를 수행할 Job Class +<2> Job 구분 명 +<3> Job 등록 +<4> Cron 표현식 설정 \ No newline at end of file diff --git a/doc/공통기능/시스템관리/주요전화번호관리.adoc b/doc/공통기능/시스템관리/주요전화번호관리.adoc new file mode 100644 index 0000000..a1ac59f --- /dev/null +++ b/doc/공통기능/시스템관리/주요전화번호관리.adoc @@ -0,0 +1,22 @@ += 주요 전화 번호 관리 + +== 개요 +주요 전화번호 페이지를 관리한다. + +== 화면 + +image::front_06_03.png[] +* 등록된 주요 전화번호 페이지 목록을 볼 수 있다. +* 리스트의 Key 컬럼을 클릭하여 주요 전화번호 페이지 정보를 수정할 수 있다. +* 팝업 미리보기를 클릭하여 등록된 주요 전화번호 페이지를 확인 할 수 있다. + +image::front_06_04.png[] +* Key : Unique한 키를 지정(영문, 숫자만 가능) +* 제목 : 주요 전화번호 페이지 제목 +* 설명 : 주요 전화번호 페이지 설명 +* 첨부파일 : 주요 전화번호 HTML 파일 (html, htm 확장자) + +[%hardbreaks] +* 메뉴관리에서 메뉴를 등록하고 Page/API 를 추가한다. +- Page Path에 주요 전화번호 관리에서 등록한 Key를 포함한다. +- API URL에 주요 전화번호 조회 API URL을 등록한다. (/html-templates/group/{templateGroupCode=CONTACT}/key/{templateKey}) \ No newline at end of file diff --git a/doc/공통기능/시스템관리/캐시관리.adoc b/doc/공통기능/시스템관리/캐시관리.adoc new file mode 100644 index 0000000..6d6b6b5 --- /dev/null +++ b/doc/공통기능/시스템관리/캐시관리.adoc @@ -0,0 +1,6 @@ += 캐시 관리 + +== 개요 +시스템 전체 캐시를 초기화 한다. + +image::cacheMgmt.png[캐시 관리] \ No newline at end of file diff --git a/doc/공통기능/시스템관리/코드관리.adoc b/doc/공통기능/시스템관리/코드관리.adoc new file mode 100644 index 0000000..ae029b5 --- /dev/null +++ b/doc/공통기능/시스템관리/코드관리.adoc @@ -0,0 +1,76 @@ += 코드 관리 + +== 개요 +그룹 코드를 등록하고 그룹 코드의 공통 코드를 등록하여 추가, 수정 및 삭제를 관리한다. + +groupcodes는 그룹 코드를 나타내고, commcodes는 공통 코드를 나타낸다. 그룹 코드가 상위, 공통 코드가 그룹 코드의 하위 개념이다. + +== Table +* 그룹 코드 : TC_CF_COMM_CODE_TYPE +* 공통 코드 : TC_CF_COMM_CODE + +== API +.CommCodeController.java + +. 그룹 코드 목록 조회 + +GET /commcode/groupcodes-with-paging + +Query ID : selectGroupCodePagingList +.. 검색조건에는 코드, 코드명, 설명이 있다. + +"그룹 코드 목록 조회(페이징)"을 사용하며 각 조건에 맞게 쿼리를 실행한다. + +특히 코드명은 한글, 영어, 중국어에 상관없이 입력한 값의 대소문자를 가리지 않고 1글자만 입력해도 검색이 된다. +.. orderBy는 기본이 COMM_CODE_TYPE_CODE(코드명) 이다. + +. 그룹 코드 등록 + +POST /commcode/groupcodes +** 그룹 코드는 20자 이내로 입력해야 한다. 영문자와 숫자, 특수문자만 사용할 수 있고 한글은 입력이 안 되게 정규식을 사용하여 프론트단에서 유효성 체크를 한다. + +. 그룹 코드 삭제 + +DELETE /commcode/groupcodes/{groupCode} +.. 그룹 코드는 TC_CF_COMM_CODE_TYPE.DELETE_YN 컬럼 값을 true로 업데이트 하는 식으로 삭제한다. +... 먼저 그룹 코드에 추가되어 있는 공통 코드의 하위를 전부 조회하여 찾은 후 공통 코드 전부 TC_CF_COMM_CODE.DELETE_YN 컬럼을 true로 업데이트 한다. +.... Query ID : updateCommCodeDeleted +... 그 후 그룹 코드의 DELETE_YN 컬럼을 true로 업데이트 하여 삭제한다. +.... Query ID : updateGroupCode + +. 공통 코드 추가 + +POST /commcode/groupcodes/{groupCode}/commcodes +** 추가시에 중복체크하고 없으면 등록하는데 TC_CF_COMM_CODE.CODE_ID는 IdGenService를 사용하여 유니크한 ID를 만들어서 등록한다. + +. 공통 코드 삭제 + +DELETE /commcode/groupcodes/{groupCode}/commcodes/{commCodeId} +** 삭제하려는 공통 코드의 자신과 자식들 코드를 조회하여 DELETE_YN 컬럼 값을 true로 업데이트 하는 식으로 삭제한다. +*** Query ID : updateCommCodeDeleted + +== 화면 +그룹 코드 및 그룹코드에 대한 공통 코드를 추가, 수정 및 삭제 기능을 수행하여 관리한다. + + +image::commonCodeList.png[] + +=== 기능별 설명 +- 등록 : 코드 정보를 등록하는 화면으로 이동 + +== 코드 등록 +그룹 코드를 등록 + +* 영문자와 숫자, '-', '_' 2개의 특수문자만 사용하여 등록 가능하다. + +image::commonCodeDetail_Reg.png[] + +=== 기능별 설명 +- 목록 : 코드 관리 화면으로 이동 +- 저장 : 그룹 코드 정보를 저장 + +== 코드 상세 정보 +그룹 코드의 상세 정보를 수정, 삭제하고 그에 대한 공통 코드를 추가, 수정, 삭제하여 관리 + +image::commonCodeDetail.png[] + +=== 기능별 설명 +==== 그룹코드 정보 +- 엑셀다운로드 : 조회 조건과 동일한 조회 결과 값을 담은 엑셀 파일을 다운로드 하는 기능 +- 목록 : 코드 관리 화면으로 이동 +- 삭제 : 그룹코드를 삭제 +- 저장 : 수정된 그룹코드 상세 정보를 수정 + +==== 공통코드 목록 +- 추가 : 공통코드를 등록하는 Popup 호출 +- 수정 : 공통코드를 수정하는 Popup 호출 +- 삭제 : 공통코드를 삭제 \ No newline at end of file diff --git a/doc/공통기능/이력관리.adoc b/doc/공통기능/이력관리.adoc new file mode 100644 index 0000000..cd5a10a --- /dev/null +++ b/doc/공통기능/이력관리.adoc @@ -0,0 +1,15 @@ += 이력 관리 + +include::이력관리/시스템로깅.adoc[leveloffset=+1] + +include::이력관리/SQLLogging.adoc[leveloffset=+1] + +include::이력관리/로거관리.adoc[leveloffset=+1] + +include::이력관리/메뉴활용도.adoc[leveloffset=+1] + +include::이력관리/메뉴사용이력.adoc[leveloffset=+1] + +include::이력관리/파일다운로드이력.adoc[leveloffset=+1] + +include::이력관리/로그인이력.adoc[leveloffset=+1] diff --git a/doc/공통기능/이력관리/SQLLogging.adoc b/doc/공통기능/이력관리/SQLLogging.adoc new file mode 100644 index 0000000..2cb5d95 --- /dev/null +++ b/doc/공통기능/이력관리/SQLLogging.adoc @@ -0,0 +1 @@ += SQL Logging diff --git a/doc/공통기능/이력관리/로거관리.adoc b/doc/공통기능/이력관리/로거관리.adoc new file mode 100644 index 0000000..4ca97d9 --- /dev/null +++ b/doc/공통기능/이력관리/로거관리.adoc @@ -0,0 +1,9 @@ += 로거(Logger) 관리 + +== 개요 +시스템 Logger 내역을 조회 및 log level을 변경할 수 있다. + +image::loggerMgmt.png[로거 관리] + +<1> Logger name별 설정된 configuration 내역(log4j2.xml)을 조회할 수 있다. +<2> Logger name별로 log level 을 변경할 수 있다. diff --git a/doc/공통기능/이력관리/로그인이력.adoc b/doc/공통기능/이력관리/로그인이력.adoc new file mode 100644 index 0000000..5164f46 --- /dev/null +++ b/doc/공통기능/이력관리/로그인이력.adoc @@ -0,0 +1,70 @@ += 로그인 이력 + +== 개요 +사용자가 시스템에 로그인한 이력을 조회 + +== Table +* 로그인 이력 : TN_CF_LOGIN_OUT +* 사용자 : TN_CF_USER + +== API +.HistoryController.java + +. 로그인 이력 목록 조회 + +GET /history/login-out-logs +Query ID : selectListLoginOut + +== 화면 +사용자가 시스템에 로그인한 이력을 조회 + +image::loginHistory.png[] + +== LoginOutLogInterceptor + +시스템에 사용자가 로그인/아웃을 하면 LoginOutLogInterceptor에 의해 사용자 정보가 저장된다. + +LoginOutLogInterceptor는 LoginInterceptor의 구현클래스로 LoginInterceptor의 자세한 설명은 <<_로그인, 로그인>>의 **로그인/아웃 전,후 처리**를 참고한다. + +[source, java] +---- +@Override +public void postLogin(HttpServletRequest request, HttpServletResponse response, User user) { + LoginOut loginOut = new LoginOut(); + + if (StringUtils.isNotEmpty(user.getJwt())) { + loginOut.setToken(user.getJwt().substring(user.getJwt().lastIndexOf('.') + 1)); + } + loginOut.setUserId(user.getUserId()); + loginOut.setLoginTime(new Date()); + loginOut.setUseIp(user.getRecentLoginIp()); + loginOut.setReqType("WEB"); + + loginOutLogService.insertLogin(loginOut); +} +---- + +로그인 시 사용자 ID, 로그인 시간, IP 등을 TN_CF_LOGIN_OUT 테이블에 저장한다. + + +[source, java] +---- +@Override +public void preLogout(HttpServletRequest request, HttpServletResponse response, User user) { + LoginOut loginOut = new LoginOut(); + + if (StringUtils.isNotEmpty(user.getJwt())) { + loginOut.setToken(user.getJwt().substring(user.getJwt().lastIndexOf('.') + 1)); + } + loginOut.setUserId(user.getUserId()); + loginOut.setLoginTime(user.getRecentLoginDatetime()); + loginOut.setLogoutTime(new Date()); + loginOut.setUseIp(webUtil.getClientIp(request)); + loginOut.setReqType("WEB"); + + loginOutLogService.updateLogout(loginOut); + + userService.updateUserJwt(Map.of("userId", user.getUserId(), "lastLogoutDate", new Date(), "jwt", "")); +} +---- +로그아웃 전에는 TN_CF_LOGIN_OUT에 정보를 저장하는 것 외에 사용자의 jwt 값을 초기화 한다. +중복 로그인 방지를 위해 jwt값을 이용한다. diff --git a/doc/공통기능/이력관리/메뉴사용이력.adoc b/doc/공통기능/이력관리/메뉴사용이력.adoc new file mode 100644 index 0000000..c5797a0 --- /dev/null +++ b/doc/공통기능/이력관리/메뉴사용이력.adoc @@ -0,0 +1,24 @@ += 메뉴 사용 이력 + +== 개요 +시스템에 접속한 사용자의 메뉴 사용 이력을 조회 + +== Table +* 메뉴 사용 이력 : TN_CF_MENU_USE_HISTORY + +== API +.HistoryController.java + +. 일별 메뉴 사용 이력 조회 + +GET /history/menu-use-by-date + +Query ID : selectMenuUseHistoryByDatePagingList + +. 월별 메뉴 사용 이력 조회 + +GET /history/menu-use-by-month + +Query ID : selectMenuUseHistoryByMonthPagingList + +== 화면 +image::menuUserHistoty.png[] + +=== 기능별 설명 +- 엑셀다운로드 : 조회 조건과 동일한 조회 결과 값을 담은 엑셀 파일을 다운로드 하는 기능 \ No newline at end of file diff --git a/doc/공통기능/이력관리/메뉴활용도.adoc b/doc/공통기능/이력관리/메뉴활용도.adoc new file mode 100644 index 0000000..44a9147 --- /dev/null +++ b/doc/공통기능/이력관리/메뉴활용도.adoc @@ -0,0 +1,22 @@ += 메뉴 활용도 + +== 개요 +메뉴 별로 메뉴 사용 이력을 분석해서 월별, 일별로 활용률을 계산한다. + +== Table +* 공통코드 : TC_CF_COMM_CODE +* 메뉴사용주기 : TC_CF_MENU_USE_PERIOD +* 메뉴 : TN_CF_MENU +* 메뉴활용도 집계 : TS_CF_MENU_USE_MM + +== API +.MenuUtilizationController.java + +. 메뉴 활용도 목록 조회 + +GET /menuutilization/menu-utilizations + +Query ID : selectMenuUtilizationList + +== 화면 +메뉴에 대한 사용 주기, Hit, 활용도를 조회 + +image::menuUtilHistory.png[] \ No newline at end of file diff --git a/doc/공통기능/이력관리/시스템로깅.adoc b/doc/공통기능/이력관리/시스템로깅.adoc new file mode 100644 index 0000000..6aa4115 --- /dev/null +++ b/doc/공통기능/이력관리/시스템로깅.adoc @@ -0,0 +1,22 @@ += 시스템 로깅 + +== 개요 +로그인/아웃, 시스템 접속, 파일다운로드 등의 시스템 로그를 남기고 있다. + +=== Login/out 로그 +로그인 후와 로그아웃 전에 LoginOutLogInterceptor를 통해 로그를 남긴다. + +NOTE: 로그인/아웃 전후에 추가할 로직이 있다면 LoginInterceptor 인터페이스를 상속받아 구현한다. + +관련 테이블:: TN_CF_LOGIN_OUT + +=== Access 로그 +시스템 접속시 LoggingInterceptor를 통해 AccessLog를 남겨 db(또는 file)에 저장한다. + +일배치로 메뉴사용이력 및 메뉴활용도 데이터를 집계한다. + +관련 테이블:: TN_CF_SYS_USE_LOG + +=== File Download 로그 +첨부파일, 엑셀파일 등을 다운로드시 fileManagerService의 saveFileDownloadLog 메서드를 호출하여 파일다운로드 로그를 남긴다. + +관련 테이블:: TN_CF_SYS_USE_LOG diff --git a/doc/공통기능/이력관리/파일다운로드이력.adoc b/doc/공통기능/이력관리/파일다운로드이력.adoc new file mode 100644 index 0000000..ca1597b --- /dev/null +++ b/doc/공통기능/이력관리/파일다운로드이력.adoc @@ -0,0 +1,24 @@ += 파일 다운로드 이력 + +== 개요 +사용자가 엑셀 다운로드 하거나 이미지 파일이 있는 게시판을 이용했거나 첨부파일을 다운로드 한 목록을 보여준다. + +== Table +* 시스템 사용 로그 : TN_CF_SYS_USE_LOG +* 사용자 : TN_CF_USER + +== API +.HistoryController.java + +. 파일 다운로드 이력 목록 조회 + +GET /history/file-download-logs + +Query ID : selectFileDownloadLogPagingList +* 파일 다운로드 이력의 LOG_FLAG는 TN_CF_SYS_USE_LOG 테이블 LOG_FLAG 컬럼의 '5' 이다. + +== 화면 +엑셀 다운로드 기능이 있는 특정 화면에서 엑셀 다운로드 실행에 대한 이력을 조회. + +image::fileDownloadHistory.png[] + +=== 기능별 설명 +- 엑셀다운로드 : 조회 조건과 동일한 조회 결과 값을 담은 엑셀 파일을 다운로드 하는 기능 \ No newline at end of file diff --git a/doc/공통기능/파일서비스.adoc b/doc/공통기능/파일서비스.adoc new file mode 100644 index 0000000..bd77ffd --- /dev/null +++ b/doc/공통기능/파일서비스.adoc @@ -0,0 +1,5 @@ += 파일서비스 + +include::파일서비스/엑셀다운로드및업로드.adoc[leveloffset=+1] + +include::파일서비스/파일업다운로드.adoc[leveloffset=+1] diff --git a/doc/공통기능/파일서비스/엑셀다운로드및업로드.adoc b/doc/공통기능/파일서비스/엑셀다운로드및업로드.adoc new file mode 100644 index 0000000..2b7a82e --- /dev/null +++ b/doc/공통기능/파일서비스/엑셀다운로드및업로드.adoc @@ -0,0 +1,99 @@ += 엑셀 다운로드 및 업로드 + +== 개요 + +SDL 6.0은 사용자 관리, 외부 사용자 관리, 역할 관리, 메뉴 사용 이력, 파일 다운로드 이력, 코드 관리, 링크사이트 관리에서 엑셀 다운로드를 지원한다. + +그 외 메뉴에서 엑셀 다운로드 기능을 적용하려면 <<_공통컴포넌트_유틸,공통컴포넌트 & 유틸>>의 <<_excel_download_button,Excel Download Button>>, <<_excel_upload_button,Excel Upload Button>> 매뉴얼을 참고한다. + +== API +.ExcelController.java + +. 엑셀 다운로드 + +GET /excel/excel-download + + +. 엑셀 업로드 + +POST /excel/excel-upload + +== 엑셀 다운로드 + +=== excel.xml 설정 +sdl-base/src/main/resources/excel 폴더에 다운로드 할 기능의 xml 양식을 만든다. + +다국어 적용(message.properties)이 된다. + +. 대외비 표기여부, 시트보호 여부, 제목 + +[source,xml] +---- +true 대외비 표기여부(false : 태그작성 x) <--> +true 시트보호 여부(false : 태그작성 x) <--> +sdl 시트보호 암호(PROTECTION = false : 태그작성 x) <--> +sdl.excel.user.title 제목 <--> +---- + +[start=2] +. 문서 Comment 출력 +.. 코멘트를 여러개 작성 가능 + +[source,xml] +---- + + + sdl.excel.accessLog.comment 코멘트 <--> + 10 코멘트 글자색 <--> + + + 2번째 코멘트 + 10 + + +---- + +[start=3] +. 헤더 + +[source,xml] +---- + +
+ NO. 컬럼명 <--> + 3 열병합 <--> + 8 컬럼 글자색 <--> + 44 컬럼 배경색 <--> +
+
+ sdp.user.label.compName + 3 행병합 <--> +
+
+---- + +[start=4] +. 컬럼 +.. 첫번째 Row 에 No. 필드 넣을 경우 No. 필드에 대한 를 작성하지 않아도 된다. + +[source,xml] +---- + + + compName HEADER_LABEL에 대응하는 엔티티명 <--> + 15 컬럼 너비 <--> + LEFT 셀 정렬 <--> + true 셀 잠금 여부(false : 태그작성 x) <--> + true 셀 숨김 여부(false : 태그작성 x) <--> + Number 날짜 형식(Date), 숫자 형식(Number)으로 출력 <--> + 10 컬럼 글자색 <--> + 13 컬럼 배경색 <--> + + +---- +IMPORTANT: 간혹 다운로드 받은 엑셀에 초록색 경고가 뜨는데 이것은 DB에 문자로 저장된 숫자를 불러오기 때문이다. + + 따라서 을 'Number'로 하면 엑셀에서도 숫자로 잘 나올 것이다. + +== 엑셀 업로드 + +=== excelUploadSample.xml 설정 +SampleExcelUpload.vue 파일에 샘플로 엑셀 업로드가 구현되어 있다. + +* 업로드할 데이터의 양식에 맞추어 excel.xml을 만들고 업로드 한다. +** 업로드 excel.xml 파일은 다운로드 excel.xml 파일과 같은 경로에 만든다. +** 날짜 형식 데이터가 업로드되지 않는 경우 `yyyy-MM-dd` 형태로 입력하거나 텍스트 서식으로 입력한다. diff --git a/doc/공통기능/파일서비스/파일업다운로드.adoc b/doc/공통기능/파일서비스/파일업다운로드.adoc new file mode 100644 index 0000000..6313e32 --- /dev/null +++ b/doc/공통기능/파일서비스/파일업다운로드.adoc @@ -0,0 +1,211 @@ += 파일 업/다운로드 + +== 개요 +클라이언트의 파일 업/다운로드 API 호출을 처리한다. + +=== 파일 업로드 +단일 파일 업로드를 처리한다. 하나의 파일만 업로드하는 경우가 아니라면 보통은 멀티 파일 업로드를 이용한다. + +=== 멀티 파일 업로드 + +==== +컨트롤러에서 파일 업로드 서비스를 호출한다. + +.FileManagerController +[source,java] +---- +@PostMapping("/resource/attachments/multifile-upload") + public List uploadMultiFile( + @Parameter(description = "MultipartFile[]", required = true) @RequestParam(required = true) MultipartFile[] files, + @Parameter(description = "다운로드 구분 (컴포넌트 한개 이상일 경우 구분)", required = true) @RequestParam(required = true) String downloadType) { + + return fileManagerService.save(downloadType, Arrays.asList(files)); + } +---- +==== + +==== +서비스에서 지정된 경로에 파일 리소스를 저장하고, 업로드된 파일 정보를 DB 에 저장한다. + +.FileManagerServiceImpl +[source,java] +---- +/** + * 기본 업로드 패스 설정 값 + */ +@Value("${common.upload-path}") +private String fileUploadPath; + +/** + * 업로드 루트 하위 폴더 자릿수 설정 값 + */ +@Value("${common.upload.directory-name-len}") +private int directoryNameLen; + +/** + * 사용자지정 업로드 패스 설정 여부 + */ +@Value("${custom.upload-path.enabled}") +private boolean customUploadPathEnabled; + +/** + * 사용자지정 업로드 패스 설정 값 + */ +@Value("${custom.upload-path}") +private String customUploadPaths; + +@Override +@Transactional +public List save(String downloadType, List files) { + return this.save(downloadType, null, files); +} + +@Override +@Transactional +public List save(String downloadType, String refId, List files) { + List attachFileList = new ArrayList<>(); + + files.stream().forEach(file -> { + AttachFile attachFile = this.save(downloadType, refId, file); + attachFileList.add(attachFile); + }); + + return attachFileList; +} + +@Override +@Transactional +public AttachFile save(String downloadType, String refId, MultipartFile file) { + String fileId = this.store(file, downloadType); // <1> + + AttachFile attachFile = new AttachFile(); + attachFile.setDownloadType(downloadType); + attachFile.setFilePathName(this.rootUploadPath.toString()); + attachFile.setFileExtensionName(fileId); + attachFile.setFileName(file.getOriginalFilename()); + attachFile.setFileMimeTypeName(file.getContentType()); + attachFile.setFileSize(file.getSize()); + attachFile.setOwnerObjectPkId(refId); + + return fileManagerDao.insertFileInfo(attachFile); // <2> +} +---- +<1> 파일 리소스 저장 +<2> 업로드된 파일 정보 DB저장 +==== + +==== +파일 리소스 저장 경로는 기본 업로드 패스 값(common.upload-path)과 하위 디렉토리 길이 설정 값(common.upload.directory-name-len)에 따라 결정된다. + +기본 업로드 패스외에 사용자정의 업로드 패스 설정도 가능하다. + +.config.properties +[source,properties] +---- +## File Attach Configuration +common.upload-path=/NAS/SDL/upload // <1> +common.upload.directory-name-len=2 // <2> +# custom upload path 설정 +custom.upload-path.enabled=false // <3> +custom.upload-path=\ // <4> +notice=/NAS/SDL/upload/notice,\ +faq=/NAS/SDL/upload/faq +---- +<1> 기본 업로드 패스 설정 +<2> 업로드 패스 하위 디렉토리 길이 + +파일명(UUID)에서 이 길이 만큼 잘라서 기본 업로드 패스 하위 디렉토리가 생성된다. +<3> 사용자정의 업로드 패스 사용 여부 +<4> 파일 컴포넌트별 사용자정의 업로드 패스 설정 (custom.upload-path.enabled=true 일 경우 적용됨) +==== + +=== 파일 다운로드 +컨트롤러에서 파일 다운로드 서비스를 호출한다. + +.FileManagerController +[source,java] +---- +@GetMapping("/resource/attachments/file-download/{fileId}") +public ResponseEntity downloadFile( + @Parameter(description = "File ID", required = true) @PathVariable String fileId, + @Parameter(description = "다운로드 구분 (컴포넌트 한개 이상일 경우 구분)", required = true) @RequestParam(required = true) String downloadType, + HttpServletRequest request) { + + long startTime = System.nanoTime(); + + if (StringUtils.isBlank(fileId)) { + throw new FileManagerException("No File ID"); + } + + AttachFile attachFile = fileManagerService.getAttachFile(fileId, downloadType); // <1> + if (attachFile == null) { + throw new FileManagerException("Cannot find file info: " + fileId); + } + Resource resource = fileManagerService.getResource(attachFile.getFileExtensionName(), attachFile.getFilePathName()); // <2> + + String contentType = attachFile.getFileMimeTypeName(); + if (StringUtils.isBlank(contentType)) { + contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE; + } + + String fileName = attachFile.getFileName(); // <3> + String encodeFileName = null; + // 다운로드 파일명 UTF-8 인코딩 + encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20"); + + // 다운로드 이력 로깅 + fileManagerService.saveFileDownloadLog(fileName, startTime, request); // <4> + + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType(contentType)) + .header("Content-Transfer-Encoding", "binary") + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + encodeFileName + "\"") + .body(resource); +} +---- +<1> 파일 정보 조회 +<2> 파일 리소스 가져오기 +<3> 파일 업로드 시점의 파일명 +<4> 파일 다운로드 이력 로깅 + +=== 멀티 파일 다운로드 +여러 파일을 zip 파일 형태로 다운로드할 수 있도록 제공한다. + +.FileManagerController +[source,java] +---- + @Value("${common.download.zipfilename}") + private String zipFileName; + + @GetMapping("/resource/attachments/multifile-download") + public ResponseEntity downloadMultiFile( + @Parameter(description = "File IDs", required = true) @RequestParam(required = true) String[] fileIds, + @Parameter(description = "다운로드 구분 (컴포넌트 한개 이상일 경우 구분)", required = true) @RequestParam(required = true) String downloadType, + @Parameter(description = "zip 파일명", required = false) @RequestParam(required = false) String zipFileName, + HttpServletRequest request) { + + long startTime = System.nanoTime(); + + if (fileIds == null || fileIds.length == 0) { + throw new FileManagerException("No File ID(s)"); + } + + if (StringUtils.isBlank(zipFileName)) { + zipFileName = this.zipFileName; + } + + // 다운로드 파일명 UTF-8 인코딩 + String fileName = null; + fileName = URLEncoder.encode(zipFileName, StandardCharsets.UTF_8).replace("+", "%20"); + + // 다운로드 이력 로깅 + fileManagerService.saveFileDownloadLog(zipFileName, startTime, request); + + ByteArrayResource baResource = fileManagerService.getByteArrayResource(fileIds, downloadType); + + return ResponseEntity.ok() + .contentLength(baResource.contentLength()) + .contentType(MediaType.parseMediaType("application/zip")) + .header("Content-Transfer-Encoding", "binary") + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + ".zip\";") + .body(baResource); + } +---- diff --git a/doc/설치/로컬설치.adoc b/doc/설치/로컬설치.adoc new file mode 100644 index 0000000..72ed08c --- /dev/null +++ b/doc/설치/로컬설치.adoc @@ -0,0 +1,392 @@ += 로컬 설치 + +== Maven 설치 + +Apache Maven(https://maven.apache.org/download.cgi )에서 다운 받아 설치한다. +OS에 맞는 파일을 다운로드 한다. +Windows의 경우 **Binary zip archive**를 추천한다. + +image::maven_01.png[maven_01] + +적당한 위치에 압축을 풀고 시스템 환경변수에서 %MAVEN_HOME%을 추가한다. + +image::maven_02.png[maven_02]jdbc: + +시스템 환경변수 path에 %MAVEN_HOME%\bin을 추가한다. + +image::maven_03.png[maven_03] + +cmd창을 열어 *mvn -version* 명령어를 실행한다. +아래처럼 설치된 Maven 버전이 출력되면 된다. + +image::maven_04.png[maven_04] + +IMPORTANT: 사업장 Proxy 세팅을 위해 각 사업장 Proxy 정보를 Users/사용자명/.m2/settings.xml에 아래와 같이 추가한다. +(settings.xml 파일이 없을 경우 %MAVEN_HOME%\conf\settings.xml 파일을 해당 위치에 복사한다.) + +---- + + + + proxy-http + true + http + 168.219.61.252 // 사업장 Proxy 정보 + 8080 // 사업장 Proxy 정보 + *.samsung.net|localhost|127.0.0.1 + + + proxy-https + true + https + 168.219.61.252 // 사업장 Proxy 정보 + 8080 // 사업장 Proxy 정보 + *.samsung.net|localhost|127.0.0.1 + + + +---- + +== IDE 설치 +개발자 환경에 맞춰 필요한 IDE를 설치한다. +Eclipse의 경우 Spring Framework 개발에 필요한 Plugin이 포함된 Spring Tools Suite를 사용해도 된다. Front-end 개발을 위해서 VS Code 등을 추가로 설치 하는 것도 추천한다. 본 메뉴얼에서는 Eclipse나 IntelliJ 등 IDE 설치에 대해서는 다루지 않는다. + +=== Visual Studio Code 설치 +NOTE: Visual Studio Code 외 다른 IDE를 사용하는 경우 건너뛴다. + +https://code.visualstudio.com/docs/?dv=win 에 접속해서 설치파일 다운로드 + +image::front_02_10.png[front_02_10,800,600] + +== Node.js 설치 + +링크(https://nodejs.org/en/ )의 페이지에서 사용중인 OS에 맞는 Node.js 설치파일을 다운로드 받아 설치 한다. + +IMPORTANT: 6.0.0 버전 기준 nodejs 버전 20.14.x 이상을 권장하고 있다. + +image::front_01_1.png[front_01_1,800,600] +=== 설치 확인 +Command 창에 node –v(version) 과 npm –v(version)을 입력합니다. 설치하신 node, npm정보가 조회되면 설치가 완료된 것이다. + +[source,bash] +---- +C:\>node -v +v20.14.0 +C:\>npm -v +9.0.2 +---- + +=== npm 설정 방법 +Node library를 다운로드 받지 못하는 경우, NPM 설정이 필요하며, 설정에 필요한 명령어는 npm config set {환경변수명} {값} 이다. +현장의 proxy 서버에 맞게 명령어를 구성하면 되며, Proxy 설정 명령어는 아래와 같다. +npm 설정방법에 대한 자세한 설명은 https://docs.npmjs.com/misc/config[npm-config]를 참고 바란다. + +image::front_02_3.png[] + +=== npm 설정 확인 +npm 설정정보를 확인하기 위해 npm config list 명령을 수행한다. +아래 그림과 같이 설정한 값이 보이면 NPM 설정 완료. + +image::front_02_4.png[] + +== Eclipse (STS) Setting + +다운받은 sdl-base-[version].zip 파일을 임의의 프로젝트 폴더 아래 압축을 푼다. + +image::install_1.png[] + +Eclipse 실행 후 workspace 설정 + +image::install_2.png[install_2,800,600] + +Eclipse 실행 후 완료 + +image::install_09.png[install_09,800,600] + +Project Encoding 설정 + +image::install_10.png[install_10,450,600] + +Validation Disable All + +image::install_11.png[install_11,450,600] + +Server 설정 + +본 문서에서는 로컬 WAS 설치 방법은 다루지 않는다. + +SDL은 스프링 부트가 기본 제공되므로 로컬 서버로는 스프링 부트 내장 Tomcat을 사용한다. + +== Eclipse Project Import + +File -> Import -> Maven Project -> Existing Maven Projects + +image::install_16.png[] + +image::install_17.png[] + +sdl-base 프로젝트를 설치한 위치를 선택한다 + +image::install_18.png[] + +Import가 완료 되면 Package Explorer에서 아래처럼 프로젝트를 볼 수 있다. + +image::install_19.png[] + +CAUTION: 프로젝트를 처음 Import 할 경우 Build Path에서 Resource폴더가 Excluded 되어 있을 수 있으니 반드시 확인한다. + +Package Explorer에서 오른쪽 마우스 클릭 -> Build Path -> Configuration Build Path... + +image::install_20.png[] + +Source Tap에서 resources, resouces-local의 Excluded 가 None으로 되어 있나 확인한다. + +image::install_21.png[] + +image::install_22.png[] + +Eclipse 프로젝트 Import가 완료되었다. + +CAUTION: .properties의 한글이 깨진다면 아래처럼 인코딩 설정을 변경한다. + +image::install_28.png[] + +== Lombok Plugin 설치 + +@Data @Log4j2 와 같은 Annotation에서 Compile 에러가 발생한다면 Lombok 플러그인을 설치한다. + +https://projectlombok.org/download 에서 lombok jar 파일을 다운로드 받는다. + +image::lombok_01.png[lombok_01,500,35] + +java -jar lombok.jar 을 cmd창에서 실행한다. + +image::lombok_02.png[lombok_02,600,450] + +<1> Specify location..클릭 +<2> Eclipse 설치 위치 선택 +<3> Install/Update 클릭 + +image::lombok_03.png[lombok_03,600,450] + +설치 완료 + +== Server 실행 + +DB 접속 정보를 프로젝트 환경에 맞게 수정한다. (예. Postgresql 일 경우) + +.config.properties +[source, properties] +---- +# Datasource(Postgresql) +datasource.driver=ENC(zg2s0lzTKz1OADpB2LmIk7pcJNLonCsW4WerSDJ9WTfmimam1Zi0Z8mH16+mWY+y) +datasource.url=ENC(QaTWEyV020nKVDYneWGs8WNrezY8FGOKyleE2pkxlSs8m8nXE2/mqYMSaqAwUJghMJnLp20twDvdHh4csOKDzw==) +datasource.username=ENC(Vz2UR99au+VBAaVJBDSdLw==) +datasource.password=ENC(Wrer0/kHw+6UyGhtBT+bdSZNo83x+HNB) +---- +IMPORTANT: 해당 정보는 중요 정보로써 암호화를 통해 정보 탈취등의 보안 사고를 대비해야 한다. +표준개발라이브러리에서는 Jasypt(Java Simplified Encryption) 방식을 통해 해당 정보를 암호화 하고 있다. + +* 프로퍼티 값 암호화는 아래 링크의 내용을 참고하여 작성 및 입력한다. +(암호화가 필요한 정보만 따로 암호화하도록 한다.) + + +<<_properties_암호화_툴_사용_방법, Properties 암호화 툴 사용 방법>> + + + +* 암호화에 사용된 키 값을 시스템 환경변수 또는 JVM 옵션을 사용하여 지정하면 +서버 구동 시 복호화 되어 DB에 접속 된다.(JasyptConfig.java) +** 시스템 환경변수 사용 +*** Windows ++ +제어판 -> 시스템 -> 시스템 환경 변수 편집 -> 환경 변수 -> 시스템 변수 JASYPT_KEY 추가 ++ +---- +C:\Users\bbnydory>echo %JASYPT_KEY% +jfoadjvcoadr0j3402j +---- ++ +*** Linux ++ +---- +$ export JASYPT_KEY=jfoadjvcoadr0j3402j +---- ++ +*** Docker(Guide Jenkins Build) ++ +Docker Container로 어플리케이션을 실행 할 경우 Docker Build 변수를 Dockerfile에 전달해 설정해야 한다. ++ +Jenkins Build Pipeline 환경 변수 설정 부분 ++ +---- +environment { + PROJECT_ID = '...' + GIT_CREDENTIAL_ID = '...' + APP_IMAGE_REPO_CREDENTIAL_ID = '...' + CA_CREDENTIAL_ID = '...' + IMAGE_REPO = '....' + JASYPT_KEY = 'jfoadjvcoadr0j3402j' + } +---- ++ +Jenkins Build Pipeline Doker 이미지 빌드 부분 ++ +---- +docker login -u $APP_IMAGE_REPO_USERNAME -p $APP_IMAGE_REPO_PASSWORD redii.secmis.sdspaas.io +docker build --pull --force-rm --file=Dockerfile --build-arg JASYPT_KEY=$JASYPT_KEY --tag=$IMAGE_LOC . +---- ++ +*--build-arg JASYPT_KEY=$JASYPT_KEY* 옵션으로 환경변수를 Dockerfile에 전달해야 한다. ++ +Dockerfile 환경 변수 설정 ++ +---- +FROM redii.secmis.sdspaas.io/sdspaas-mw/xxxxxx + +ARG JASYPT_KEY +ENV JASYPT_KEY $JASYPT_KEY +---- + +** JVM 옵션 사용 ++ +WAS의 실행 명령어 또는 실행시 에 *-DJASYPT_KEY=jfoadjvcoadr0j3402j* 를 추가해서 어플리케이션을 실행한다. ++ +---- +$ java -server -Xms8g -Xmx8g -XX:MaxMetaspaceSize=256m -Dspring.profiles.active=prod -DJASYPT_KEY=jfoadjvcoadr0j3402j +---- + +NOTE: Properties 암호화 툴에서 사용한 키 값(jasypt.key파일내용)과 Jasypt 암호화 키(JASYPT_KEY) 값은 일치해야 한다. + +환경변수와 key 값, value은 시스템 내에서 자유롭게 설정하도록 한다. + +위의 value값은 절대로 샘플의 값을 사용하지 않도록 한다. + +.SpringConfig.java +[source, java] +---- +@Bean +public SqlSessionFactoryBean defaultSqlSessionFactory(DataSource dataSource, + ApplicationContext applicationContext) throws IOException { + SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); + factoryBean.setDataSource(dataSource); + factoryBean.setConfigLocation(applicationContext.getResource("classpath:sql/mybatis/postgresql/mybatis-config.xml")); + factoryBean.setMapperLocations(applicationContext.getResources("classpath*:sql/mybatis/postgresql/**/mapper-mybatis-*.xml")); + return factoryBean; +} +---- + +pom.xml에서 프로젝트에서 사용할 DB의 jdbc driver dependency를 확인/설정한다. + +.pom.xml +[source, xml] +---- + + org.postgresql + postgresql + 42.7.3 + + + org.checkerframework + checker-qual + + + +---- + +== Spring Boot 실행 +SDL 6.0은 Spring Boot로 되어 있기 때문에 별도의 WAS 없이 Embedded Tomcat를 이용해 서버를 실행 할 수 있다. + +=== CMD 에서의 실행 +[source,bash] +---- +mvn spring-boot:run +---- + +=== IntelliJ 에서 실행 +프로젝트 창에서 com.samsung.SdlBaseBootApplication.java 파일 선택 후 Spring Boot Application 실행을 한다. + +=== Eclipse 에서 실행 +Spring Boot로 실행 할 com.samsung.SdlBaseBootApplication.java 파일 선택 후 Spring Boot Application을 실행 한다. + +== VS Code Setting +NOTE: Visual Studio Code 외 다른 IDE를 사용하는 경우 건너뛴다. + +Open source +File > Open Folder > '\sdl-base\frontend' 선택 + +image::front_02_11.png[] + +image::vscode_02.png[] + +VS Code Plugin 설치 +코딩 컨벤션을 위한 Plugin 설치 - vscode 실행 후 좌측 아이콘 메뉴중 5번째 선택 후 검색어 입력 + +"eslint" + +image::front_02_12.png[] +"prettier" + +image::front_02_13.png[] + +설치후 vscode 활성화 (저장시 자동 수정) +File > Preferences > Settings 메뉴 진입 하거나 +단축키 Ctrl + Shift + P 를 눌러 아래 검색어 user 입력 + +image::front_02_14.png[] +설정검색란에 save 입력 후 settings.json에서 편집 클릭 + +image::front_02_15.png[] +JSON 파일에 아래 코드 추가 + +image::front_02_16.png[] + +CAUTION: vscode 에서 eslint가 너무 늦게 적용될때 환경변수에 NO_UPDATE_NOTIFIER=1 추가하면 됨 +(참고링크 https://github.com/Microsoft/vscode-eslint/issues/440#issuecomment-380083518 ) + +image::front_02_17.png[] + +== Frontend 설치 및 실행 +[[npm-install]] +Frontend 실행에 필요한 node module 설치 + +IDE에서 Terminal을 생성하여 npm install 명령어를 실행 + +.npm-install +[source,shell] +---- +C:\~YOUR~PROJECT~PATH~\sdl-base\frontend>npm install +---- + +Vite dev-server 의 경우 host: 'localhost', port: 5173 이 default 값으로 설정되어있다. + +SDL 은 dev-server 의 port 값을 '8081'로 사용하고 있으며, frontend/vite.config.js 파일에서 아래와 같이 서버 설정 +을 확인 및 수정 할 수 있다. + +.vite.config.js +[source, json] +---- +server: { + // host: 'localhost' // default + port: '8081', +} +---- + +로컬 환경 설정 파일 .env.test 파일을 열어 Back-end 정보를 입력한다. + +..env.test +[source, properties] +---- +VITE_NODE_ENV=local +VITE_API_URL=http://localhost:8080 +VITE_DIST_PATH=../src/main/resources/public +VITE_WEB_CONTEXT_PATH=/ +---- + +npm run local 명령어를 통해 +local 환경에서의 vite dev-server를 실행한다 + +[source,shell] +---- +C:\~YOUR~PROJECT~PATH~\sdl-base\frontend>npm run local +---- + +== Local 접속 확인 +Web browser(Chrome, Edge)에 http://localhost:8081 입력 후 실행 + +image::front_02_20.png[front_02_20,450,600] \ No newline at end of file diff --git a/doc/설치/빌드&배포.adoc b/doc/설치/빌드&배포.adoc new file mode 100644 index 0000000..ec87ab4 --- /dev/null +++ b/doc/설치/빌드&배포.adoc @@ -0,0 +1,130 @@ += 빌드&배포 + +== Vite Bundling +SDL6 UI는 서비스 하기 위해 Bundling이 필요하다. +각 배포 대상에 따른 설정 파일은 .env.test , .env.development, .env.production 파일을 참고한다. + +..env.production 예시 +[source, properties] +---- +# default env +VITE_NODE_ENV=production +VITE_API_URL=http://sdl.sec.samsung.net/administrator +VITE_DIST_PATH=../src/main/webapp/pp +# CONTEXT_PATH value must be end with '/' +VITE_WEB_CONTEXT_PATH=/ +---- + +package.json 에 정의된 script에 따라 설정 파일이 적용된다. + +.package.json +[source, json] +---- + "scripts": { + "local": "vite --mode test", + "dev": "vite --mode development", + "prod": "vite --mode production", + "build": "vite build --mode production", + "build-test": "vite build --mode test", + "build-dev": "vite build --mode development", + "build-prod": "vite build --mode production", + "preview": "vite preview --mode production", + "preview-test": "vite preview --mode test", + "preview-dev": "vite preview --mode development", + "preview-prod": "vite preview --mode production", + "lint": "eslint src/**/*.{vue,js}" + }, +---- + +*npm run local* 명령어를 실행할 경우 .env.test 파일이 적용되며, vite-dev-server가 실행된다. +vite-dev-server는 front-end 코드가 실시간으로 반영되므로 개발 시 편리하다. + +*npm run preview* 명령어를 통해 빌드 된 결과물을 preview 서버를 통해 확인 할 수 있다. +반드시 *VITE_DIST_PATH* 에 Build 결과물이 존재해야 구동된다. + +== SonarQube Scanner 를 통한 소스코드 정적 분석 + +* 먼저 SonarQube Scanner 를 설치한다. + +---- +npm install sonar-scanner -g +---- + +* 사용법 (Command Line) + +---- +cd sdl-base <1> + +sonar-scanner -Dsonar.host.url=SonarQube서버URL -Dsonar.projectKey=프로젝트Key -Dsonar.projectName=프로젝트명 -Dsonar.projectVersion=프로젝트버전 -Dsonar.login=아이디 -Dsonar.password=패스워드 -Dsonar.sources=frontend/src <2> +---- + +<1> 분석하고자 하는 프로젝트로 이동한다. +<2> 설정한 파라미터값으로 실행한다. + +.파라미터 설명 +[width="100%",options="header"] +|===== +| key | value(예) | 설명 | 비고 + +| sonar.host.url +| \http://guide.sonarqube.sec.samsung.net +| SonarQube 서버 URL +| 필수 + +| sonar.projectKey +| my:project +| 분석하고자 하는 프로젝트의 고유한 키 값 +| 필수 + +| sonar.projectName +| "my project for test" +| 프로젝트 이름 +| + +| sonar.projectVersion +| 1.0 +| 프로젝트 버전 +| + +| sonar.login +| user01 +| SonarQube 서버 로그인 ID +| 필수 + +| sonar.password +| passwd01 +| SonarQube 서버 로그인 패스워드 +| 필수 + +| sonar.sources +| frontend/src +| 소스파일이 위치한 디렉토리 +| +|===== + +NOTE: sonar-scanner 파라미터 관련 자세한 내용은 https://docs.sonarqube.org/latest/analysis/analysis-parameters/[공식문서^]를 참조한다. + +== Front-end 빌드 + +운영시스템에 배포 하기 위해서 cmd창에서 *npm run-script build* 명령어를 실행한다. +빌드 결과는 .env 파일의 *VITE_DIST_PATH* 경로에 생성된다. + +image::build_01.png[build_01,800,600] + +image::build_02.png[] + +== Back-end 빌드 + +Back-end는 Maven을 이용해 Build 하고 있다. maven 명령어를 이용해 빌드한다. +패키징 시 배포 대상별로 Profile을 적용 할 수 있다. +---- +mvn clean package -Pprod +---- +-Pprod 옵션을 경우 src/main/resource-prod 아래의 설정파일들이 적용되어 패키징 된다. + +image::build_03.png[build_03,800,600] + +IMPORTANT: SDL 6.0은 Spring Profile이 적용되어 서버 실행시 -Dspring.profiles.active=local과 같은 옵션을 필요로 한다. + + + diff --git a/doc/설치/설치.adoc b/doc/설치/설치.adoc new file mode 100644 index 0000000..2f0f7f3 --- /dev/null +++ b/doc/설치/설치.adoc @@ -0,0 +1,5 @@ += 설치 + +include::로컬설치.adoc[leveloffset=+1] + +include::빌드&배포.adoc[leveloffset=+1] \ No newline at end of file diff --git a/doc/소개/Overview.adoc b/doc/소개/Overview.adoc new file mode 100644 index 0000000..6c3ed3e --- /dev/null +++ b/doc/소개/Overview.adoc @@ -0,0 +1,12 @@ += Overview + +삼성전자 내 정보시스템 개발을 위한 공통기능 및 아키텍처를 미리 만들어 제공함으로써, +프로젝트에서의 설계 및 개발 기간을 단축하고 유지보수를 용이하게 진행 할 수 있도록 지원한다. + +include::표준개발라이브러리란.adoc[leveloffset=+1] + +include::주요특징.adoc[leveloffset=+1] + +include::지원환경.adoc[leveloffset=+1] + +include::기술지원범위.adoc[leveloffset=+1] \ No newline at end of file diff --git a/doc/소개/기술지원범위.adoc b/doc/소개/기술지원범위.adoc new file mode 100644 index 0000000..f9be17b --- /dev/null +++ b/doc/소개/기술지원범위.adoc @@ -0,0 +1,14 @@ += 기술지원범위 + +표준개발라이브러리 관련 기술지원 범위 + +* SDL 공통기능 : SDL 공통기능과 관련된 Framework, REST API, UI, BUG Fix 관련 문의 및 개발지원 +* 장애 및 오류 지원 : SDL 제공 공통기능과 관련된 장애 및 오류의 원인분석 및 개선지원 +(단, 증상의 재현이 가능하고 Error Log가 확보된 경우에 한함) + +CAUTION: *기술지원 제외 대상* + +다음과 같은 경우는 기술지원 대상에서 제외한다. + +1. SDL과 관련 없는 개발문의 + +2. 개발 환경의 구축 및 설치, 네트워크 / HW / OS / WEB 서버 / WAS / DBMS / 패키지 SW 제품 관련 문제 + +3. 개발 부서에서 자체 도입한 Open Source를 포함한 SW 관련 기능 + diff --git a/doc/소개/주요특징.adoc b/doc/소개/주요특징.adoc new file mode 100644 index 0000000..7b467a6 --- /dev/null +++ b/doc/소개/주요특징.adoc @@ -0,0 +1,83 @@ += 주요특징 + +SDL 6.0의 주요 특징은 다음과 같다. + +. Monolithic Architecture & Micro Service Architecture +. Single Page Application +. Spring Boot 3 (Spring Framework 6) +. Javascript Framework 도입 Vue.js +. CSS Framework 도입 Bootstrap 5 +. Front-end 빌드 : Vite (배포 타겟별 Profile 적용) +. JDK baseline update 최소 요구 사항 JDK 17 이상 +. Back-end 빌드 : Maven (배포 타겟별 Profile 적용) + +.SDL 4.5 vs 5.0, 6.0 +[cols="2,2,2,2,5", options="header"] +|=== +^.^|구분 ^|4.5 ^|5.0 ^|6.0 ^| 비고 + +^.^|공통기능 +^.^|50개 +2+^.^|65개 +|삭제 : Flex, MiPlatform, XPLATFORM 제외 + +신규 : U-Trans, 결재경로관리, QuickMenu 등 + +^.^|아키텍처 +|Monolithic + +MSA 미지원 +2+^|Monolithic + +MSA 지원 +|MSA 모델 중 서비스간 Database를 공유하는 모델 限 + +^.^|개발환경 +|JDK 6 이상 + +Tomcat 7.0이상 +|JDK 8 이상 + +Tomcat 9.0이상 +|JDK 17 이상 + +Tomcat 10.1이상 | + +^.^|Framework +|Spring 4 + +@Controller +|Spring 5 + +@RestController +|Spring 6 + +@RestController +|Persistence Framework : MyBaits(동일) + +^.^|UI +|MPA + +JSP, jQuery + +CSS F/W 미제공 + +ES5 +|SPA + +Vue.js 2 + +Bootstrap 4 + +ES6 +|SPA + +Vue.js 3 + +Bootstrap 5 + +ES6 +| + +^.^|Build +|Ant + +UI 빌드 불필요 +|Maven + +Webpack +|Maven + +Vite +|3rd party 라이브러리 Maven Central Repo. 활용 +|=== + +== Micro Service Architecture + +시스템 내에 비즈니스 기능들 나누어 개발하고 다른 서버에서 서비스 됨 +-> 탄력적인 시스템 운영 가능 , 빌드/배포 시간 단축, 장애 영향도 최소, 기능 확장 용이 + +== Single Page Application + +- 페이지 이동 시 화면 깜빡임이 발생하지 않음 +- 서버에서 필요한 데이터만 전달 받음 +- Java 개발은 Back-end(서버), Javascript 개발은 Front-end(UI) diff --git a/doc/소개/지원환경.adoc b/doc/소개/지원환경.adoc new file mode 100644 index 0000000..2ca3bcc --- /dev/null +++ b/doc/소개/지원환경.adoc @@ -0,0 +1,17 @@ += 지원환경 + +== 웹 시스템 개발 환경 + +삼성전자 사내 시스템에 사용하는 인프라와 표준 WEB/WAS/DB 사용 지원 + +.시스템개발환경 +[cols="2,3", options="header"] +|==== +^.^|구분 ^| 환경 + +^| WAS ^| JBoss EAP 8.0 + +Tomcat 10.1 + +Spring Boot Embedded Tomcat +^| DBMS ^| MS SQL, MySQL, Oracle, EPAS, Tibero, PostgreSQL +^| JDK ^| JDK 17 이상 +|==== diff --git a/doc/소개/표준개발라이브러리란.adoc b/doc/소개/표준개발라이브러리란.adoc new file mode 100644 index 0000000..98a527e --- /dev/null +++ b/doc/소개/표준개발라이브러리란.adoc @@ -0,0 +1,16 @@ += 표준개발라이브러리란? + +표준개발라이브러리(이하 SDL(Standard Development Library))는 웹 시스템 개발 시 재사용 가능한 **공통 기능**과 **표준 개발 환경**을 제공하는 통합 라이브러리다. + +* 시스템 구축 시 자주 사용하는 공통 기능(웹 65개) 제공으로 개발 생산성 향산에 기여 +* 웹 개발환경 표준화로 시스템 환경 구성 및 아키텍처 설계 기간 단축에 기여 +** 적용 대상 : Java 기반의 신규 시스템 구축 + +image::sdl_introduction.png[] + +== 웹 부문 + +=== 웹 공통기능 제공 +전사 공통으로 사용하는 65개의 공통기능을 제공한다. + +* 사용자 관리, 시스템 관리, 이력 관리, 보안 관리 등 diff --git a/doc/시스템공통/HandlerInterceptor.adoc b/doc/시스템공통/HandlerInterceptor.adoc new file mode 100644 index 0000000..8eaafc3 --- /dev/null +++ b/doc/시스템공통/HandlerInterceptor.adoc @@ -0,0 +1,287 @@ += HandlerInterceptor + +org.springframework.web.servlet.HandlerInterceptor의 구현클래스에 대해서 설명한다. +HandlerInterceptor는 Controller 전후 에 실행되며 preHandle, postHandle, afterCompletion 메소드를 제공한다. + +[source, java] +---- + +default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + return true; +} + +default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception { +} + +default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { +} + +---- + + +== LoggingInterceptor + +LoggingInterceptor는 시스템 이용 로그를 남기기 위한 Interceptor이다. 대부분의 로직은 postHandle에 구현되어 있으며 +TN_CF_SYS_USE_LOG 테이블에 데이터를 기록한다. +사용자정보(ID, IP, 브라우저), 요청시간, 응답시간, 요청 파라미터를 기록하며 이 데이터는 메뉴 사용 이력, +메뉴 활용도, 파일 다운로드 이력등에 사용된다. + +[source, java] +---- + +public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) { + String requestMethod = request.getMethod(); + if(HttpMethod.OPTIONS.name().equals(requestMethod)) { + return; + } + String requestURI = request.getRequestURI().substring(request.getContextPath().length()); + + + SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd HHmmssSSS", Locale.getDefault()); + String sTime = formatter.format(new Date()); + + long start = (long) request.getAttribute("start"); + long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + + String userId = "anonymous"; + if (ObjectUtils.isNotEmpty(Account.currentUser())) { + userId = Account.currentUser().getUserId(); + } + + String getDecodedRequestURI = WebUtil.getDecodedRequestUrl(request, requestURI); + SysUseLog sysUseLog = new SysUseLog(); + sysUseLog.setLogOccurId(idGenService.getNextStringId()); + + String menuId = request.getHeader("menu-Id"); + String pageId = request.getHeader("page-id"); + if(pageId != null) sysUseLog.setDescription(menuService.getPageFullPath(pageId)); + + sysUseLog.setNodeId(node); + sysUseLog.setUserId(userId); + sysUseLog.setUseFromDate(sTime.substring(0, 8)); + sysUseLog.setUseFromHhmmss(sTime.substring(9, 15)); + sysUseLog.setUseThruDatetime(new Date()); + sysUseLog.setResponseTime(elapsedTime); + sysUseLog.setPath(getDecodedRequestURI); + + String query = request.getQueryString(); + if (query != null) { + try { + query = URLDecoder.decode(query, "UTF-8"); + } catch (UnsupportedEncodingException e) { + log.error(e.getMessage(), e); + } + String queryStr = StringUtils.replace(query, "'", "\\'"); + sysUseLog.setParameter(queryStr); + sysUseLog.setUrl(requestURI + "?" + queryStr); + } + sysUseLog.setUseIp(WebUtil.getClientIp(request)); + sysUseLog.setBrowserTypeName(request.getHeader("User-Agent")); + sysUseLog.setLogFlag(LogFlag.ACCESSLOG.getValue()); + sysUseLog.setReqType(ReqType.WEB); + sysUseLog.setFirstRegDatetime(new Date()); + + sysUseLog.setMenuId(menuId); + sysUseLog.setPageId(pageId); + sysUseLog.setMethod(requestMethod); + + // 대리 로그인 시 별도 이력 남김 + String originalUserId = request.getHeader("original-user-id"); + if(StringUtils.isNotEmpty(originalUserId)){ + sysUseLog.setLogFlag(LogFlag.IMPERSONATION_ACCESS.getValue()); + sysUseLog.setDescription("Original User Id : " + originalUserId); + } + + try { + if ("db".equalsIgnoreCase(storeType)) { + accessLogService.insertAccessLog(sysUseLog); + } else { + accessLogService.logAccess(sysUseLog); + } + } catch (Exception e) { + log.error(e.getMessage()); + } +} + +---- + +config.properties 파일의 access-log.store-type 값에 따라 db또는 file에 저장된다. + + +== AuthenticationInterceptor + +AuthenticationInterceptor는 사용자가 인증이 되어 있는지 체크하는 인터셉터다. +인증이 되어 있지 않을 경우 로그인 화면으로 이동한다. + +[source. java] +---- +String token = request.getHeader("x-auth-token"); <1> +// Token 값 유무 +if (StringUtils.isEmpty(token)) { + throw new NotFoundTokenException("Not Found Token"); +} +---- +인증이 된 사용자는 모든 요청 해더에 x-auth-token 값을 같이 보내야 한다. +Frontend에서 x-auth-token값을 공통으로 Set하는 코드는 main.js 를 참고한다. + +[source, javascript] +---- +axios.defaults.headers.common['x-auth-token'] = localStorage.getItem('userToken'); +---- + +loginService.js에서 로그인이 완료되면 localStorage에 toekn값을 저장하고, axios 요청 시마다 +해더에 추가 한다. + +[source, java] +---- +String originalUserId = request.getHeader("original-user-id"); <1> +---- + +<1> 대리로그인 시 원래 사용자의 ID가 헤더에 담겨있다. + +대리로그인은 개발 시 기능, 권한 테스트를 하기 위한 용도로 관리자가 다른 사용자로 로그인하는 기능이다. 이 기능은 운영중인 시스템에 사용할 경우 보안에 문제가 발생하니 반드시 개발 시 테스트 용도로만 사용한다. + + +CAUTION: 대리로그인은 개발 시 테스트를 위한 기능으로 운영 시 사용하지 않는다. + +[source, java] +---- +Boolean jwtValid = jwtUtil.validateToken(token, user); <1> +if (Boolean.TRUE.equals(jwtValid)) { + if (user.isActiveFlag()) { + user.setSystemAdminUser(roleService.isSystemAdmin(userId)); + Account.updateCurrentAccount(user); + return true; + } else { + // 시스템 승인 대기중 + throw new WaitingUserException("Waiting User"); <2> + } +} else { + throw new InvalidTokenException("Token Expired"); +} +---- + +<1> 유효한 Token인치 확인 +<2> Active Flag가 false일 경우 시스템 승인 대기중 상태 + +로그인 Token 관련해서는 <<_로그인>>을 참고한다. + +NOTE: 중복로그인 방지를 위해 로그인시 발급된 jwt값을 db에 저장하고, 현재 jwt값을 비교한다. +config.properties파일 security.check.duplicate.login이 true일 때만 동작한다. + + +== AuthorizationInterceptor +AuthorizationInterceptor는 요청 URL(API)가 사용자에게 접근이 가능한지 판단하는 인터셉터다. + +[source, java] +---- +if (user == null) throw new NoSearchUserException("No Search User"); <1> +if (roleService.isSystemAdmin(user.getUserId())) { <2> + if(adminAddressCheck) { + List adminAddresses = adminAddressService.getAdminAddresses(null); <3> + String remoteAddr = WebUtil.getClientIp(request); + if(ObjectUtils.isNotEmpty(adminAddresses) && !adminAddresses.contains(remoteAddr)) { + throw new AuthorizationException("Remote address is not admin ip address."); + } + } + return true; +} +---- + +<1> 사용자가 null일 경우에는 Exception을 발생한다. +<2> 현재 사용자가 System Admin일 경우 +<3> 접근 IP가 System Admin IP에 포함 되어 있는지 판단한다. + +NOTE: config.properties의 admin.address.check값이 true일 때만 System Admin IP를 체크 한다. + +[source, java] +---- +for (Api api : authMenuList) { + if (api.getHttpMethod().equals(httpMethod) && checkUriMatch(api.getApiPath(), api.getApiParameters(), decodedUrl, queryString)) { + return true; + } +} +throw new AuthorizationException("API 권한 없음"); +---- + +IMPORTANT: URL 권한 체크 시에는 파라미터 단위까지 체크한다. test?abc=123과 test?adb=123은 다른 권한이다. + + +== PrivacyPolicyInterceptor + +PrivacyPolicyInterceptor는 이용 약관 동의 여부를 체크하는 Interceptor이다. + +[source, java] +---- +/** + * 약관동의 사용 여부 + */ +@Value("${privacy-policy.check.enabled:true}") +private boolean privacyPolicyCheck; + +/** + * 약관동의 인터셉터 체크 제외 URI + */ +@Value("${privacy-policy.check.exclude-path}") +private String privacyPolicyCheckExcludePath; +---- + +약관 동의 체크 인터셉터 사용 여부와 제외 URI는 위 프로퍼티 값을 이용한다. + +[source, java] +---- +if (user != null && !user.isPrivacyPolicy()) { + throw new PrivacyPolicyException("Privacy policy agreement required."); +} else { + return true; +} +---- +약관 동의가 완료되지 않은 경우 PrivacyPolicyException이 발생하고 약관 동의 페이지로 이동한다. + + +== TimeoutCheckInterceptor + +TimeoutCheckInterceptor는 일정시간 동안 사용하지 않을 경우 자동 로그아웃 하는 기능이다. + +config.properties파일 security.check.access.timeout 값이 true 일때만 동작한다. security.access.limit.timeout(분) 동안 시스템을 사용하지 않을 경우 자동 로그아웃 한다. + + +[source, java] +---- +@Value("${security.access.limit.timeout:30}") +private int limitTimeout; + +static final long MILLISECONDS_PER_MINUTE = 60L*1000L; + +@Override +public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){ + String lastAccessTimeHeader = request.getHeader("last-access-time"); + String impersonate = request.getHeader("original-user-id"); + if(StringUtils.isNotEmpty(lastAccessTimeHeader) && !StringUtils.equals("undefined", lastAccessTimeHeader) && !StringUtils.equals("NaN", lastAccessTimeHeader) && StringUtils.isEmpty(impersonate)){ + long lastActivityTime = Long.parseLong(lastAccessTimeHeader); + long currentTime = System.currentTimeMillis(); + long jwtRetentionTime = currentTime - lastActivityTime; + if (jwtRetentionTime > (limitTimeout * MILLISECONDS_PER_MINUTE)) { + //limitTimeout 동안 사용하지 않으면 자동 로그아웃 + throw new TokenRetentionTimeoutException("Token Retention Timeout"); + } + } + return true; +} +---- + +동작 방식은 Backend 요청 시 Response Header에 마지막으로 요청한 시간을 세팅하고 다음 요청시 Request Header에 last-access-time에 설정된다. 현재 시간과 last-access-time을 비교해 로그아웃 여부를 결정한다. + + + +== UploadFileExtensionCheckInterceptor + +UploadFileExtensionCheckInterceptor는 파일 업로드 시 파일 확장자를 검사한다. + +config.properties파일 common.upload.allowed-extensions에 정의된 확장자를 가진 파일만 업로드 할 수 있다. + +NOTE: UI에서도 허용되는 파일 확장자를 정의해줘야한다. MultipleFileUploader.vue를 부모컴포넌트에서 사용할때 pros useExtList을 설정해야 한다. 자세한것은 <<_file_component>>를 참고한다. + + + + diff --git a/doc/시스템공통/SdlBaseBootApplication.adoc b/doc/시스템공통/SdlBaseBootApplication.adoc new file mode 100644 index 0000000..9c33cc5 --- /dev/null +++ b/doc/시스템공통/SdlBaseBootApplication.adoc @@ -0,0 +1,39 @@ += SdlBaseBootApplication + +SpringBoot 웹 애플리케이션을 배포할 때는 주로 embedded tomcat이 내장된 jar파일을 이용한다. 하지만 war 파일로 빌드, 배포를 진행해야 하는 경우를 위해 SpringBootServletInitializer를 상속받고 있다. + +NOTE: https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.traditional-deployment[Spring Boot supports traditional deployment] + +== SpringBootServletInitializer 상속 +.SdlBaseBootApplication +[source, java] +---- +@SpringBootApplication +public class SdlBaseBootApplication extends SpringBootServletInitializer { + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(SdlBaseBootApplication.class); + } + + public static void main(String[] args) { + SpringApplication.run(SdlBaseBootApplication.class, args); + } + +} +---- + +== Profile 적용 +@Profile 을 이용하여 Profile 별로 다른 설정이 가능하다. +아래는 spring.profiles.active(JAVA OPTS)에 따라서 변경되는 설정이다. + +* 아래의 예는 local profile 에서만 적용된다. + +[source, java] +---- +@Configuration +@Profile({"local"}) +public class DbcpDataSourceConfig { +---- + +IMPORTANT: spring.profiles.active 는 runtime에서 매우 중요한 프로퍼티다. ServletContext에 등록되는 Filter, Servlet이 결정되고, +Spring Bean의 생성도 결정되니 반드시 JAVA OPTS에 설정해야 한다. diff --git a/doc/시스템공통/SpringConfig.adoc b/doc/시스템공통/SpringConfig.adoc new file mode 100644 index 0000000..0230432 --- /dev/null +++ b/doc/시스템공통/SpringConfig.adoc @@ -0,0 +1,186 @@ += Spring Config + +SDL 6.0에서는 모든 Spring 설정이 Java Config로 되어 있다. com.samsung.config 패지키에 있다. +기본적인 설정파일들은 아래와 같다. +[source, text] +---- +com.samsung.config + |- CacheConfig <1> + |- DbcpDataSourceConfig <2> + |- JasyptConfig <3> + |- JpaDatasourceConfig <4> + |- KnoxSyncBatchConfig <5> + |- QuartzClusteringConfig <6> + |- QuartzConfig <7> + |- RedisConfig <8> + |- SpringConfig <9> + |- SpringWebConfig <10> + |- SwaggerConfig <11> + |- SysUseLogBatchConfig <12> + |- TemplateConfig <13> + |- UserBatchConfig <14> + |- WebClientConfig <15> +---- +<1> 캐시 설정 (Simple Provider) +<2> 데이터 소스 설정 +<3> 프로퍼티 값 암호화를 위한 Jasypt 설정 +<4> Jndi 데이터 소스 설정 +<5> 결재동기화 배치 쿼츠(Quartz) job/trigger 설정 +<6> Quartz 클러스터링 설정 (JDBC Jobstore) +<7> Quartz 설정 (RAM Jobstore) +<8> 캐시 설정 (Redis) +<9> Spring 설정 +<10> Spring WebApplicationContext 설정 +<11> Swagger 설정 +<12> 시스템 로그 배치 쿼츠(Quartz) job/trigger 설정 +<13> Thymeleaf 템플릿 엔진 설정 +<14> 사용자 관련 배치 쿼츠(Quartz) job/trigger 설정 +<15> WebClient 설정 + +== SpringConfig + +Spring 설정 중에 가장 기본이 된다. Transaction, MessageSource 를 사용 할수 있도록 +Spring Container 에 등록한다. + +* @Configuration + +Configuration Annotation은 Spring Container에게 해당 클래스가 Bean들을 등록하는 클래스라는 것을 알려주기 위한 Annotation이다. +프로젝트에서 Bean을 등록 할때는 클래스에 Configuration Annotation을 설정하도록 한다. + +* @EnableTransactionManagement + +EnableTransactionManagement annotation을 사용하면 Spring에서 @Transactional 을 사용해 Transaction을 관리 할 수 있다. + +.xml 설정 +[source, xml] +---- + +---- + +@Transactional 에 대한 세부적인 내용은 Spring +link:https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction-declarative-attransactional-settings[Transactional Settings] +를 참고 한다. + +* PropertySource + +property 파일을 읽기 위해 사용한다. SDL에서는 <<_config_properties, config.properties>>파일과 <<_knox_properties,knox.properties>> 파일을 기본으로 로딩한다. +PropertySource에 등록된 값은 Spring Bean에서 사용할 수 있다. + +** Value Injection + +@Value를 사용해 PropertySource의 값을 Injection 한다. + +** Environment Injection + +org.springframework.core.env.Environment를 Injection 하고 getProperty("key")를 이용해 값을 얻는다. + +.Value Injeciton +[source, java] +---- +@Value("${security.access.limit.timeout:30}") +private int limitTimeout; + +@Value("${security.check.access.timeout:false}") +private boolean checkTimeout; +---- +.Environment Injection +[source, java] +---- +String functionUrl = environment.getProperty(KNOX_EMP_SERVICE) + "/employees"; +---- + +* ComponentScan + +com.samsung 패키지에 속해 있는 @Service, @Repository, @Component 의 Bean만 찾아서 등록한다. + +[source, java] +---- +@ComponentScan(basePackages = "com.samsung", useDefaultFilters = false, includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Service.class), @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Repository.class), @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Component.class)}) +---- + +CAUTION: @Controller Baen은 SpringConfig가 아닌 SpringWebConfig에서 Scan한다. + +* MessageSource + +다국어 적용을 위해 MessageSource 를 사용한다. Backend 에서는 MessageSourceAccessor나 MessageSource를 이용해 다국어를 적용한다. +특히 velocity엔진 템플릿에서 다국어를 사용하기 위해서는 반드시 MessageSourceAccessor를 사용하도록 한다. + +IMPORTANT: Frontend 가 처음 로딩 될 때 서버에서 "/noauth/messages/all" API를 호출해 시스템의 모든 메세지 리소스를 받는다. MessageBundleService에서는 config.properties 에 설정된 language-set에 해당하는 Message Properties 파일을 읽어 JSON으로 만들어 리턴한다. + + +== SpringWebConfig +Spring WebApplicationContext 설정을 위한 파일이다. WebMvcConfigurer를 구현하고 있으며, +Formatter, MessageConverter 등을 재정의 할 수 있다. + +.xml +[source, xml] +---- + +---- + +@Configuration, @EnableWebMvc 를 선언하는 것으로 대체될 수 있다. + +.java +[source, xml] +---- +@Configuration +@EnableWebMvc +public class SpringWebConfig implements WebMvcConfigurer { +} +---- + +* ComponentScan + +[source, java] +---- +@ComponentScan(basePackages = "com.samsung", useDefaultFilters = false, includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class), @ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableSwagger2.class)}) +---- + +@Controller, @EnableSwagger2 로 선언된 Bean을 Scan한다. + +* addResourceHandlers + +정적 리소스를 관리하는 ResourceHandlerRegistry에 pathPatterns와 리소스 위치를 등록한다. + +[source, java] +---- +@Override +public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("index.html"). + addResourceLocations(webResourceRoot); + registry.addResourceHandler("favicon.ico"). + addResourceLocations(webResourceRoot + "static/"); + registry.addResourceHandler("static/**") + .addResourceLocations(webResourceRoot + "static/"); + registry.addResourceHandler("swagger-ui.html") + .addResourceLocations("classpath:/META-INF/resources/"); + registry.addResourceHandler("/webjars/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/"); +} +---- + +* MultipartResolver + +Multipart 요청에 대한 처리를 담당하는 Resolver다. 파일업로드의 최대 크기 등을 설정한다. +[source, java] +---- + @Value("${common.upload.max-request-size:-1}") + private long maxRequestSize; + @Value("${common.upload.max-file-size:-1}") + private long maxFileSize; + + + @Bean + public MultipartResolver multipartResolver() { + return new StandardServletMultipartResolver(); + } + + @Bean + public MultipartConfigElement multipartConfigElement() { + MultipartConfigFactory factory = new MultipartConfigFactory(); + factory.setMaxRequestSize(DataSize.ofBytes(maxRequestSize)); + factory.setMaxFileSize(DataSize.ofBytes(maxFileSize)); + + return factory.createMultipartConfig(); + } +---- + +config.properties에 설정한 multipart/form-data 최대 사이즈, 전체 업로드 파일의 최대 용량을 참조한다. diff --git a/doc/시스템공통/공통컴포넌트&유틸.adoc b/doc/시스템공통/공통컴포넌트&유틸.adoc new file mode 100644 index 0000000..7bdcd49 --- /dev/null +++ b/doc/시스템공통/공통컴포넌트&유틸.adoc @@ -0,0 +1,1216 @@ += 공통컴포넌트 & 유틸 + +== DatePicker +=== DatePicker Component 사용방법 +template에 component 추가 + +[source, html] +---- + +---- +props 설명 + +[%header,cols=3*] +|=== +|props명 +|설명 +|예 + +|refKey +|component Reference Key +| + +|period +|type: Object, //기간 정보 +| + +|minimumView +|type: String, //최소 표현 단위 - ex) month로 설정 시 '월' 단위로 picker 선택 가능 + +default: 'day', +| + +|wrapClass +|type: String, //class명 + +default: 'wd150', +| + +|disabled +|type: Boolean, //달력 선택 가능여부 +|default: false, +|=== + +== Excel Download Button +=== Excel Download Component 사용방법 +template에 component 추가 + +[source, html] +---- + +---- +props 설명 + +[%header,cols=3*] +|=== +|props명 +|설명 +|예 + +|btnLabel +|type: String, //버튼 label 정의 + +default:'sdp.common.label.excelDownload' +| + +|excelInfo +|type: Object +|excelInfo: { + +columnInfoFile:'externalUser', + +fileName:'externalUser', //다운받을 엑셀파일명 + +queryId:'selectUser', //쿼리 아이디 + +sheetName:'외부 사용자', //sheet명 + +useNumberField:'Y', //엑셀 처음 컬럼에 넘버링 사용 여부 + +param: { deleted: '0', activeFlag: '1', externalUser: '1' }, + +} + +|beforeClick +|엑셀 다운로드 전에 체크하고자 하는 로직이 있을 경우 사용 + +다운로드 가능할 경우 true 리턴 +|template : + + + +method : + +beforeClick() { + +return true; + +}, +default: false, +|btnClass +|type: String, //버튼 class 정의 + +default: 'btn btn-secondary' +| +|=== + +== Excel Upload Button +=== Excel Upload Component 사용방법 +template에 component 추가 + +[source, html] +---- + +---- +props 설명 + +[%header,cols=3*] +|=== +|props명 +|설명 +|예 + +|btnLabel +|type: String, //버튼 label 정의 + +default:'sdp.common.label.excelUpload' +| + +|excelInfo +|type: Object +|excelInfo:{ + +columnInfoFile:'largeExcelupload', //Column Info XML 파일 + +queryId:'selectUser', //실행할 query Id + +returnPath:'', //엑셀 저장 후 이동할 페이지 경로('after-upload' event로 대체) + +startRow:4, //저장할 데이터의 첫 Row + +useObjectName:'com.samsung.sample.entity.ExcelTestObject', //데이터를 처리하기 위한 자바 객체 + +}, + +|beforeClick +|엑셀 업로드 전에 체크하고자 하는 로직이 있을 경우 사용 + +업로드 가능할 경우 true 리턴 +|template : + + + +method : + +beforeClick() { + +return true; + +}, +|btnClass +|type: String, //버튼 class 정의 + +default: 'btn btn-secondary' +| +|=== +event 설명 + +[%header,cols=3*] +|=== +|event명 +|설명 +|예 + +|after-upload +|엑셀 업로드 후 진행할 로직을 추가 +|template : + + + +method : + +afterUpload(rtn){ + +console.log(rtn); + +SDLUtil.alert('excel upload 완료. 다음 작업 할것'); + +}, +|=== + +== File Component +=== File Component 사용방법 +- template에 component 추가 + +[source, html] +---- + +---- +- 선택 파일 다운로드 시 파일별다운로드(default) 및 압축파일다운로드 기능 제공 +- MultipleFileUploader.vue 파일 내 checkedDownload method 부분 참고 + +props 설명 + +[%header,cols=3*] +|=== +|props명 +|설명 +|예 + +|modifyFlag +|type: Boolean, //true(등록/삭제 가능), false(다운로드만 가능) + +default: true, +| + +|uploadURL +|type: String, //업로드할 url 등록 + +default: '${SDLUtil.API_URL}/resource/attachments/multifile-upload' //SDL 사용시 default +| + +|multiFileNm(필수) +|type: String //멀티 다운로드시 대표 이름 +| + +|downloadType +|type: String //file component를 한 vue에서 여러개 사용할 경우 downloadType으로 구분한다. + +ex) 'A', 'B',... or 'F', 'I' +| + +|fileList +|type: Array //파일 리스트를 component에 전달(SDL을 사용할 경우 API에서 넘어온 리스트 그대로 전달할것) +|fileList: [ + +{ + +firstRegDatetime:1563412831556, + +firstRegrId:'sdp-front', + +lastModDatetime:1563412831556, + +lastModrId:'sdp-front', + +fileId:'AWwCqp1KAADw5f1I', + +fileName:'정sdl_html_20190708.zip', + +filePathName:'C:\\NAS\\SDL\\upload', + +fileSize:14154292, + +fileExtensionName:'b518546c12f1482a87ce44772fd56057', + +fileMimeTypeName:'application/x-zip-compressed', + +deleted:false, + +downloadType:'A', + +orderIdx:0, + +}, + +|useExtList +|type: String, //업로드할 파일 확장자를 세팅 + +default: 'zip,xlsx,xls,ppt,pptx' +| + +|maxItems +|type: Number, //최대 업로드 가능한 파일 갯수 세팅 + +default: 30 +| + +|maxTotalSize +|type: Number, //최대 첨부파일 용량을 세팅 + +default: 1073741824 +| +|=== +event 설명 + +[%header,cols=3*] +|=== +|event명 +|설명 +|예 + +|complete +|업로드 후 업로드한 파일 리스트를 return 해준다. +|template : + + + +method : + +rtnUploadList(downloadType, fileList) { + +console.log('downloadType:', downloadType); + +console.log('rtnUploadList:', fileList); + +SDLUtil.alert('받은 업로드 리스트 다음 작업 할것'); + +}, + +|init +|파일 컴퍼넌트를 init 처리한다. +|template : + + + +method : + +this.$refs.uploader1.init() + +|onUpload +|추가한 첨부파일을 업로드 한다. 업로드 후 complete에 지정한 메소드로 리스트 return 해줌 +|template : + + + +method : + +this.$refs.uploader1.onUpload() +|=== + +== Modal +Modal 팝업 띄우기 + +image::front_01_01.png[] +* 형식 : SDLUtil.show(import된 컴포넌트, 컴포넌트에 전달할 인자, modal properties, events) + +props 설명 + +[%header,cols=5*] +|=== +|props명 +|필수여부 +|Type +|Default +|설명 + +|name +|true +|[String, Number] +| +|모달 명 + +|resizable +|false +|Boolean +|false +|resize 가능여부 + +|draggable +|false +|[Boolean, String] +|false +|drag 가능 여부 + +|scrollable +|false +|Boolean +|false +|스크롤 가능 여부 + +|width +|false +|[String, Number] +|600 +| + +|height +|false +|[String, Number] +|300 +| + +|minWidth +|false +|Number (px) +|0 +| + +|minHeight +|false +|Number (px) +|0 +| + +|maxWidth +|false +|Number (px) +|Infinity +| + +|maxHeight +|false +|Number (px) +|Infinity +| +|=== +event 설명 + +[%header,cols=2*] +|=== +|event 명 +|설명 + +|before-open +|모달 오픈전 실행할 이벤트 정의 + +|opened +|모달 오픈후 실행할 이벤트 정의 + +|before-close +|모달 닫기전 실행할 이벤트 정의 + +|closed +|모달 닫기 후 실행할 이벤트 정의 +|=== + +== Pagination +=== Pagination Component 사용방법 +template에 component 추가 + +[source, html] +---- + +---- +props 설명 + +[%header,cols=3*] +|=== +|props명 +|설명 +|비고 + +|currentPage +|type: Number //현재 페이지 +| + +|totalRows +|type: Number //총 데이터 갯수 +| + +|perPage +|type: Number //한 페이지에 보여질 데이터 갯수 +| + +|limit +|type: Number, //페이징 영역내에 보여질 페이지 숫자 갯수 + +default: 10 +| +|=== + +== Tree +=== Tree Component 사용 방법 +template에 component 추가 + +[source, html] +---- + +---- +props 설명 + +[%header,cols=4*] +|=== +|props명 +|Type +|Default +|설명 + +|data +|Array +| +|set tree data + +|size +|String +| +|set tree item size , value : 'large' or '' or ''small' + +|show-checkbox +|Boolean +|false +|set it show checkbox + +|allow-transition +|Boolean +|true +|allow use transition animation + +|whole-row +|Boolean +|false +|use whole row state + +|no-dots +|Boolean +|false +|show or hide dots + +|collapse +|Boolean +|false +|set all tree item collapse state + +|multiple +|Boolean +|false +|set multiple selected tree item + +|allow-batch +|Boolean +|false +|in multiple choices. allow batch select + +|text-field-name +|String +|'text' +|set tree item display field + +|value-field-name +|String +|'value' +|set tree item value field + +|children-field-name +|String +|'children' +|set tree item children field + +|item-events +|Object +|{} +|register any event to tree item, example + +|async +|Function +| +|async load callback function , if node is a leaf ,you can set 'isLeaf: true' in data + +|loading-text +|String +|'Loading' +|set loading text + +|draggable +|Boolean +|false +|set tree item can be dragged , selective drag and drop can set 'dragDisabled: true' and 'dropDisabled: true' , all default value is 'false' + +|drag-over-background-color +|String +|'#C9FDC9' +|set drag over background color + +|klass +|String +| +|set append tree class +|=== +node.model 의 method + +[%header,cols=2*] +|=== +|method 명 +|params + +|addChild +|(object) newDataItem + +|addAfter +|(object) newDataItem, (object) selectedNode + +|addBefore +|(object) newDataItem, (object) selectedNode + +|openChildren +| + +|closeChildren +| +|=== +event 설명 + +[%header,cols=2*] +|=== +|event 명 +|설명 + +|item-click +|item-click(node, item, e) + +|item-toggle +|item-toggle(node, item, e) + +|item-drag-start +|item-drag-start(node, item, e) + +|item-drag-end +|item-drag-end(node, item, e) + +|item-drop-before +|item-drop-before(node, item, draggedItem, e) + +|item-drop +|item-drop(node, item, draggedItem, e) + +|node +|current node vue object + +|item +|current node data item object +|=== +data item의 properties + +[%header,cols=4*] +|=== +|properties명 +|type +|default +|설명 + +|icon +|String +| +|custom icon css class + +|opened +|Boolean +|false +|set leaf opened + +|selected +|Boolean +|false +|set node selected + +|disabled +|Boolean +|false +|set node disabled + +|isLeaf +|Boolean +|false +|if node is a leaf , set true can hide '+' + +|dragDisabled +|Boolean +|false +|selective drag + +|dropDisabled +|Boolean +|false +|selective drop +|=== + +== SDLUtil 사용 방법 +=== SDL 공통 Util 사용방법 + +[source, javascript] +---- +import SDLUtil from '@/utils/SDLUtil'; +// 또는 +import { SDLUtil, StringUtil } from '@/utils'; + +// import 후 사용 +---- + +[%header,cols=3*] +|=== +|method 명 +|설명 +|예 + +|getLoginedUserInfo +|로그인한 사용자 정보 +| + +|getMsgProp +|메시지 프로퍼티에서 값 가져오기 +|SDLUtil.getMsgProp('sdl.user.label.work', ['하나', '둘']) + +|alert +|레이어 alert +|SDLUtil.alert({ + +msg:'I am a tiny dialog box.
And I render HTML!', + +title:'alert', + +okLabel:'ok', + +onOkEvt: () => console.log('ok'), + +}); + +SDLUtil.alert("메시지"); //이 경우 나머지 입력값들은 default로 세팅되고 msg만 입력됨 +|confirm +|레이어 confirm +|SDLUtil.confirm({ + +msg: 'confirm body', + +title: 'confirm title', + +okLabel: 'ok label', + +cancelLabel: 'cancel label', + +onOkEvt: () => console.log('ok'), + +onCancelEvt: () => console.log('cancel'), + +}); +|showLoadingBar +|default loading bar show/hide +|SDLUtil.showLoadingBar(true); + +|getCommCodeList +|로딩시 가져온 전체 코드리스트에서 특정 parentId에 대한 코드 리스트 return +|const codelist = SDLUtil.getCommCodeList({ commCodeTypeId: 'MENUTYPE' }); + +console.log(codelist); +|openUserPopup +|사용자 검색 팝업 +|SDLUtil.openUserPopup({ + +searchColumn: 'userName', + +searchTxt: this.searchUserNm, + +rtnFunc: this.getUserList, + +knoxSearch: !this.internalFlag, + +}); + +|isLogin +|로그인 여부 +| + +|getI18nLanguage +|현재 로케일 정보 +| + +|setI18nLanguage +|로케일 변경 +| + +|show +|모달창 오픈 +|SDLUtil.show( + +WorkgroupPopup, + +{ + +rtnFunc: args => { + +this.checkRoleWorkgroupList(args); + +}, + +}, + +{ width: '850px', height: 'auto' }, + +); + +|loadAllCommCodeList +|전체 공통코드 리스트 load +| + +|formParameters +|object 를 formData로 변경 +| +|=== + +== StringUtil 사용 방법 +=== StringUtil 사용방법 + +[source, javascript] +---- +import StringUtil from '@/utils/stringUtil'; +// 또는 +import { SDLUtil, StringUtil } from '@/utils'; + +// import 후 사용 +---- + +[%header,cols=3*] +|=== +|method 명 +|설명 +|예 + +|queryStringfy +|url 쿼리 문자열로 변경 +| + +|checkPatternUrl +|입력된 문자열이 url 패턴인지 체크 +| +|=== + +== DateUtil 사용 방법 +=== DateUtil 사용방법 + +[source, javascript] +---- +import DateUtil from '@/utils/DateUtil'; +// import 후 사용 +---- + +IMPORTANT: DateUtil의 기본 데이터 포맷은 'YYYY-MM-DD' 형식이며, 국가별 언어에 따른 message.data-format.properties 는 사용자에게 보여줄 때의 형식이다. + +[%header,cols=3*] +|=== +|method 명 +|설명 +|예 + +|now +|현재 날짜 +| + +|addDate +|현재 날짜 기준 이후 데이터 +|.addDate(1, 'M') 현재 날짜에서 한 달을 더한 날 + +|subDate +|날짜 기준 이전 데이터 +|.subDate(1, 'M') 현재 날짜에서 한 달을 뺀 날 + +|stdFormat +|표준 포맷으로 변환 +|Backend로 데이터를 보낼때의 표준 포맷(YYYY-MM-DD 순) +|=== + +== 공통 validation +필수 입력값 validation을 onBlur 시 그리고 저장/수정 시 하기 위해서는 아래와 같은 방법으로 처리한다. + +image::front_01_02.png[] +validation 하고자 하는 tag 에 v-validation(directive) 이용(errorMessage 를 지정하지 않을 경우 default로 표시 됨, default message의 경우 다국어 지원이 되지 않음) + +* 팝업 없는 화면인 경우(directive 지정시 별도 내용 없이 처리) + +[source, html] +---- +
+ + +
+---- +* 등록 화면에 팝업 등록이 또 있는 경우(groupId 를 다르게 써서 체크) + +[source, html] +---- + + + {{ $t('sdl.commonCode.label.order') }} + * + + + + + +---- +* 값이 있고 없고 외에 별도 처리의 경우 함수와 에러메시지를 배열로 정의 + +[source, html] +---- + +URL* + + + + +---- +* 저장/수정 버튼 클릭 시 directive로 지정한 tag들의 일괄 validation 처리(groupId가 있을 경우 넣고 없으면 안 넣어도 됨) + +[source, javascript] +---- +if (SDLUtil.onSubmitValidation('popup')) return; +---- + +IMPORTANT: Bootstrap을 이용한 퍼블리싱이 아닐 경우 안 될 수 있음 + +== 다국어 관련 날짜 포맷 +SDL에서는 다국어 포맷을 Server로 부터 message properties를 통해 받아서 사용한다. + +.message.properties +[source, json] +---- + { + "ko_KR":{ + "data-format.time.hm":"HH:mm", + "data-format.datetime.ymdhms":"YYYY-MM-DD HH:mm:ss", + "data-format.time.hms":"HH:mm:ss", + "data-format.date.ym":"YYYY-MM", + "data-format.date.yw":"YYYY-W", + "data-format.date.ymw":"YYYY-MM(W)", + "data-format.date.ymd":"YYYY-MM-DD" + }, + "en_US":{ + "data-format.time.hm":"HH:mm", + "data-format.datetime.ymdhms":"YYYY-MM-DD HH:mm:ss", + "data-format.time.hms":"HH:mm:ss", + "data-format.date.ym":"YYYY-MM", + "data-format.date.yw":"YYYY-W", + "data-format.date.ymw":"YYYY-MM(W)", + "data-format.date.ymd":"MM/DD/YYYY" + } +} +---- + +.moment() 를 이용한 현재 날짜의 언어별 포맷(SDLUtil.getMsgProp) 표기법 +[source, javascript] +---- +firstRegDatetime: moment().format(SDLUtil.getMsgProp('data-format.date.ymd')), +---- + +.filter를 통해 template에서 사용가능 +[source, html] +---- + + +{{ row.regDate || dateFormat }} + +{{ $filters.dateFormat(row.regDate) }} + + + +{{ mail.sendDateTime || dateFormat($t('data-format.date.ymd')) }} + +{{ $filters.dateFormat(mail.sendDateTime, $t('data-format.date.ymd')) }} +---- + +== 화면별 권한처리 +SDL에서는 역할관리에서 정의해놓은 역할별로 4개의 권한을 지정할 수 있다. +그리고 지정한 권한을 화면에서 버튼에 directive로 지정함으로써 show/hide 처리를 자동으로 해준다. + +image::front_01_03.png[] + +* vue 내에서 authorization directive를 통해 버튼 별 권한을 정의한다.(배열로 정의할 경우 여러개 중에 하나만 매칭되도 show처리 됨) + +** 여러개의 권한을 둘 경우 ++ +[source, html] +---- + +---- + +** 하나의 권한을 둘 경우 ++ +[source, javascript] +---- + +---- + +* 관리자일 경우는 directive 지정 상관 없이 무조건 show 처리된다. + +== 목록 페이지와 상세 페이지 검색조건 +=== 목록 페이지 +mixins 에 StoreParams 추가 + +[source, javascript] +---- + // 상단 +import StoreParams from '@/mixin/StoreParams'; +// script +export default { +mixins: [StoreParams], +/* +** 조회하는 method에 this.setStoreParameter(params); 호출하여 파라미터 저장 +*/ + methods: { + getList(pageIndex = 1) { + const params = { + pageIndex, + pageSize: this.pageSize, + searchCondition: this.searchCondition, + searchKeyword: this.searchKeyword, + }; + // 해당코드 추가 + this.setStoreParameter(params); + // Loading bar 실행 + SDLUtil.showLoadingBar(true); + // axios api 호출 로직 생략 + } + } +---- +created() 항목에 파라미터 복원 로직 추가 +[source, javascript] +---- + created() { + const storeParams = this.getStoreParameter(); + // 저장된 파라미터가 있으면 복원 + if (storeParams) { + this.searchCondition = storeParams.searchCondition; + this.searchKeyword = storeParams.searchKeyword; + this.pageIndex = storeParams.pageIndex; + this.pageSize = storeParams.pageSize; + + this.getList(this.pageIndex); + } else { + // 저장된 파라미터가 없이 들어왔을때 로직 + this.getList(); + } + }, +---- + +=== 상세 페이지 +mixins에 StoreParams 추가 후 목록버튼에 goBackToList() 호출하여 목록으로 이동 +[source, javascript] +---- + // 상단 +import StoreParams from '@/mixin/StoreParams'; +// script +export default { + mixins: [StoreParams], + // ... 생략 ... + methods: { + // template 내에서는 goBackToList 바로 호출 + // 로직상 필요한 경우도 호출 + deleteDetail() { + // 해당페이지 상세 삭제후 목록이동 + // 목록페이지로 돌아가기 + this.goBackToList(); + } + } +---- + +== 기타 유용한 filter +=== cutString + +원하는 길이만큼 문자열 컷 + '...' + +[source, html] +---- + +{{ grid.docTitle | cutString(100) }} + + +{{ $filters.cutString(grid.docTitle, 100) }} +---- + +=== dateFormat +timestamp 형태의 값을 지정된 날짜 포맷으로 변경(default : 'YYYY-MM-DD') + +[source, html] +---- + + +{{ row.regDate | dateFormat }} + + +{{ $filters.dateFormat(row.regDate) }} + + + +{{ mail.sendDateTime | dateFormat($t('data-format.date.ymd')) }} + + +{{ $filters.dateFormat(mail.sendDateTime, $t('data-format.date.ymd')) }} +---- + +=== numberFormat +숫자를 금액 단위로 표시(999,999,999) + +[source, html] +---- + + {{ log.hitCount | numberFormat }} + + + {{ $filters.numberFormat(log.hitCount) }} +---- + +=== nl2Br +\n 을
로 변경(v-text 인 경우는 tag가 안 되므로 v-html 사용) + +[source, html] +---- + +
+ + +
+---- + diff --git a/doc/시스템공통/시스템공통.adoc b/doc/시스템공통/시스템공통.adoc new file mode 100644 index 0000000..952b906 --- /dev/null +++ b/doc/시스템공통/시스템공통.adoc @@ -0,0 +1,11 @@ += 시스템공통 + +include::시스템설정.adoc[leveloffset=+1] + +include::SdlBaseBootApplication.adoc[leveloffset=+1] + +include::SpringConfig.adoc[leveloffset=+1] + +include::HandlerInterceptor.adoc[leveloffset=+1] + +include::공통컴포넌트&유틸.adoc[leveloffset=+1] diff --git a/doc/시스템공통/시스템설정.adoc b/doc/시스템공통/시스템설정.adoc new file mode 100644 index 0000000..868e328 --- /dev/null +++ b/doc/시스템공통/시스템설정.adoc @@ -0,0 +1,489 @@ += 시스템 설정 + +SDL은 다양한 시스템 환경에 적용 할 수 있도록 여러가지 설정파일을 제공한다. sdl-base/src/resources-{profile} 폴더안에 profile 별로 다른 설정 파일들이 실행 될 수 있도록 구성되어 있다. + +== config.properties + +SDL 가장 중요한 설정 파일로 시스템 전반에 영향을 준다. 로컬, 개발, 운영환경마다 내용이 달라질 수 있으니 패키징 시 설정값들이 맞는지 확인하고 배포 할 수 있도록 주의한다. + +[cols=".^1h,.^1,.^2,.^2",options="header"] +|==== +| key | value(예) | 설명 | 주의 + +| node-id +| localNode +| 서버 식별을 위한 ID +| 서버가 여러 대 있을 경우 ID값을 다르게 해야한다. + +| ssl-port +| 8443 +| http로 접근 했을 때 자동으로 redirection 하기 위한 https 포트 +| 기본값은 443 + +| cors.domain +| * +| CORS를 허용하기 위한 값으로 브라우저에서 다른 도메인으로 데이터 호출을 가능하게 한다. +| *, sdl.sec.sasmsung.net + +| web-context-path +| / +| 웹서버의 web-context-path +| 페이지 경로 앞에 참조 + +| datasource.driver +| ENC(txFyC35/92LBzew8iq2DjoFINRYg6e1UMcq7Du2mvs/nIqTSAU6e2r4VcyKrNc+K) +| DMBS의 driver class name +| 중요 프로퍼티 정보 암호화. 자세한 내용은 <<_server_실행, 프로퍼티 값 암호화>> 관련 부분 참조 + +| datasource.url +| ENC(N2SavucpTKhakXUzUGpVLh+Hy2UQ5EyQJKBMJfagFbTB0VR9m8KO7LmPM6GFRBPWgjEOYJx1hWtFrCsHctjYZQ==) +| DMBS의 url +| 중요 프로퍼티 정보 암호화. 자세한 내용은 <<_server_실행, 프로퍼티 값 암호화>> 관련 부분 참조 + +|datasource.username +| ENC(xS5l219WYApjTijLj8gwAw==) +| DMBS의 사용자 +| 중요 프로퍼티 정보 암호화. 자세한 내용은 <<_server_실행, 프로퍼티 값 암호화>> 관련 부분 참조 + +| datasource.password +| ENC(WHYsCYza7ffqN8Wi7FtTwTBI0dP9wHy2) +| DMBS의 패스워드 +| 중요 프로퍼티 정보 암호화. 자세한 내용은 <<_server_실행, 프로퍼티 값 암호화>> 관련 부분 참조 + +| db.jndi +| sdl_ds +| WAS Datasource의 JNDI명 +| 개발,운영 환경에서는 WAS의 Datasource를 사용한다. + +| web-content-root-path +| http://localhost:8081 +| Web 서버의 Root의 경로 +| 클라이언트로 redirect 할 경우 참조 (ex. AD인증 후) + +| static-resource-path +| http://localhost:8081/static +| css, image가 있는 경로 +| HTML Template (ex. Velocity) 에서 참조 + +| base.package +| com.samsung +| 클래스를 찾을 때 해당 패키지 아래에서만 찾는다. +| 결재 문서를 찾을 때 참조 + +| entity.package-name +| entity +| 결재 문서를 찾을 때 entity 패키지 아래의 클래스만 찾는다. +| 반드시 @ApprovalDocument가 선언되어 있어야 한다. + +| login.sso.knox-tray-private-key-path +| /rsaprivkey8.pem +| NewEpTray PrivateKey 경로 +| <<_로그인, 로그인>> 부분 참조 + +| login.auto-sign-up +| true +| Knox 및 AD 사용자 자동 가입 허용 +| 사용자 인증 후 시스템에 자동 가입된다. + +| user.auto-permission +| true +| 사용자가 시스템 가입 신청시 자동 승인 +| false 시 승인대기 후 관리자 승인 절차 + +| privacy-policy.check.enabled +| true +| 시스템 이용 약관 동의 필터 사용 여부 +| false 시 약관 동의 여부 체크하지 않는다 + +| privacy-policy.check.exclude-path +| /privacypolicy/terms/valid +| 사용자가 약관을 동의 했는지 체크하는 필터 예외 path +| + +| email.limit-body +| 1048576 +| email 본문의 길이 +| 발신 API에서 본문 사이즈는 최대 1mb까지 입력 가능 (Knox기준) + +| email.limit-recipients +| 100 +| 최대 수신인 +| 입력 가능한 최대 수신인 수는 100명 (Knox기준) + +| batch.user.sync.retire.enabled +| false +| 퇴직자 처리 배치 사용여부 +| 별도의 사용자 정보 데이터가 동기화 되어 있어야 한다. + +| batch.user.sync.cron +| 0 10 23 * * ? +| 사용자동기화 배치 스케줄 +| + +| batch.user.long-term.month +| 3 +| 장기미사용자 판단 기준(월) +| + +| batch.user.long-term-check.cron +| 0 10 00 * * ? +| 장기미사용자 처리 배치 스케줄 +| + +| batch.user.auth-expired.alarm.before +| 1,7,14 +| 권한만료 알림 메일 발송일 +| 1일전,7일전,14일전 + +| batch.user.auth-expired.cron +| 0 10 02 * * ? +| 권한만료 처리 배치 스케줄 +| + +| batch.user.auth-expired-mailing.cron +| 0 30 02 * * ? +| 권한만료 알림 메일 배치 스케줄 +| + +| batch.sys-use-log.menu-use-history.cron +| 0 10 01 * * ? +| 메뉴사용이력 배치 스케줄 +| + +| batch.sys-use-log.menu-utilization.cron +| 0 30 01 * * ? +| 메뉴활용도 배치 스케줄 +| + +| security.sql-injection.allowed-pattern +| .\*[^a-zA-Z0-9_\\s,].\* +| SQL에 허용되는 문자 +| 영문 대소문자, 숫자, 스페이스, 콤마만 허용 + +| security.authentication.exclude-path +| /\\**/noauth/** +| 권한,인증 체크에서 제외되는 url +| + +| security.jwt.secret-key +| +| jwt를 암복호화 하기 위한 키 (256bits 이상) +| 시스템별로 상이해야 하며 키가 유출되지 않도록 주의해야 한다. + +| security.jwt.expiration-time +| 8 +| jwt 토큰 유지 시간 +| 요청시 마다 체크. 유효하지 않을 경우 로그아웃 + +| security.eptray.expiration-time +| 24 +| eptray 유지 시간 +| eptray 로그인시 체크. 이 시간이 경과되면 Knox 재로그인 필요 + +| security.check.access.timeout +| true +| TimeoutCheckInterceptor 사용 여부 +| 시스템을 일정시간 사용하지 않을 경우 로그아웃 + +| security.access.limit.timeout +| 30 +| 타임아웃 체크 기준 시간 +| + +| security.check.duplicate.login +| false +| 사용자 중복 로그인 체크 여부 +| + +| access-log.store-type +| db +| Access Log 저장 방식 +| db, file + +| access-log.exclude-path +| /\\**/noauth/** +| Access Log에 기록 하지 않을 path +| + +| access-log.batch.enabled +| true +| 시스템 사용 이력 통계 배치 실행 여부 +| + +| access-log.file-path +| /logs/access +| Access Log 파일 저장 위치 +| Access Log 저장 방식이 file 일 때 참조 + +| menu-utilization.retention-period +| 24 +| 메뉴 활용도 보관 기간 +| + +| common.upload-path +| /NAS/upload +| 업로드 파일 저장 위치 +| + +| common.upload.directory-name-len +| 2 +| 업로드 파일의 디렉토리명 길이 +| + +| common.download.zipfilename +| compressed +| 모두 다운 받기 할때 zip 파일 이름 기본값 +| + +| common.upload.allowed-extensions +| xls,jpg +| 업로드 가능한 파일의 확장자 +| + +| common.excel-upload-path +| /excel +| 엑셀 업로드 임시 저장 폴더 +| + +| common.upload.max-file-size +| -1 +| 업로드 가능한 개별 파일 크기 (바이트) +| -1일 때 제한 없음 + +| common.upload.max-request-size +| -1 +| 모든 파일 및 폼 데이터를 포함한 전체 멀티파트 요청 크기 (바이트) +| -1일 때 제한 없음 + +| common.upload.default-encoding +| UTF-8 +| 요청을 파싱할 때 사용할 캐릭터 인코딩 +| + +| custom.upload-path.enabled +| false +| 사용자지정 업로드 패스 설정 여부 +| + +| custom.upload-path +| notice=/nas/sdl/upload/notice,\ + +faq=/nas/sdl/upload/faq +| 사용자지정 업로드 패스 설정 값 +| + +| excel.mapping.locations +| classpath*:/excel/*.xml +| excel mapping file 위치 +| + +| excel.mapping.reloadInterval +| 10000 +| excel mapping file 리로딩 시간 +| 밀리 세컨, 0일 경우 리로딩 하지 않음 + +| common.server-time-zone-id +| Asia/Seoul +| 서버 time zone id +| 사용자 정보내 timezone 설정이 없을떄 기본으로 설정 되는 값 + +| common.server-time-zone +| GMT+9:00 +| 서버 time zone +| 사용자 정보내 timezone 설정이 없을떄 기본으로 설정 되는 값 + +| knox.approval.sync.cron +| 0 0/3 * * * ? +| 결재 문서 상태 동기화 주기 +| + +| language-set +| ko_KR,en_US +| 시스템 언어셋 +| 메세지 프로퍼티 로케일 정보 + +| api.utrans.url +| +| utrans 서버 주소 +| + +| api.utrans.type +| +| utrans type +| + +| api.utrans.key +| +| utrans key +| + +| admin.address.check +| false +| 시스템 관리자의 ip 제한 +| + +| system.email +| +| 시스템 대표 email 주소 +| Knox에 등록된 계정이어야 함 +|==== + +== knox.properties + +knox 서비스를 연계를 위해 필요한 설정들이다. Knox Staging, Production 에 따라 값이 다르니 반드시 환경에 맞는 값들이 설정됐는지 확인 후 배포한다. + +IMPORTANT: 1. Knox Stage 연계 시 Knox Stage 계정이 있어야 한다. 특히 메일, 결재 연계를 개발 할 때 Stage에 없는 사용자에게 메일을 보내거나 결재를 상신한다면 오류가 발생하니 반드시 사용자 계정이 있는지 확인 한다. + +2. Knox 연계 신청, 연계 오류 관련 문의는 Knox Support를 통해 문의 한다. + +3. Knox 운영 거점은 한국, 구주, 미주 3곳이 있으므로 연계 신청시 사용자 위치에 따라 거점 신청에 주의하도록 한다. 거점 연계가 안되어 있는 사용자는 결재, 메일 기능을 사용 할 수 없다. + +[cols="3*",options="header"] +|==== +|key | value | 설명 + +| knox.system-id +| +| Knox 연계 신청 시 받은 시스템 ID + +| knox.token +| token1, token2, token3 +| Knox 연계 신청 시 받은 token + +| knox.address.prefix +| openapi.samsung.net,openapi.w1.samsung.net,openapi.w2.samsung.net +| 한국,구주,미주 서비스 주소 +|==== + +CAUTION: token과 서비스 주소는 반드시 쌍으로 등록한다. + +== 캐시 (Cache) + +SDL은 빈번하게 요청되는 데이터 값을 저장하고 필요시 데이터를 빠르게 불러 올수 있도록 스프링 프레임워크에서 제공하는 캐시를 사용한다. 스프링 캐싱 서비스는 추상화로 제공되므로 캐시 Provider 별 CacheManager 를 구현하여 빈으로 등록을 해야한다. + +NOTE: SDL의 로컬 개발환경에서는 스프링 Simple Provider(ConcurrentHashMap)를 사용하고 있으며, 캐시 공유가 필요한 운영 환경에서는 Redis 라이브러리를 사용하고 있다. 자세한 설정은 스프링 부트 공식문서를 참조하도록 하고 본 문서에서는 SDL에서 사용하고 있는 캐시 데이터 중심으로 설명한다. + +* message-all + +MessageBundleService getMessage의 결과를 저장한다. Spring MessageSource의 모든 언어에 대한 Key, Value를 리턴한다. +[source, java] +---- +@Cacheable(value = "message-all") +public Map> getMessage() { + Map> message = new HashMap<>(); + for (String locale : languageSet) { + message.put(locale, getMessageByLang(new Locale(locale.split("_")[0], locale.split("_")[1]))); + } + return message; +} +---- + +* message + +MessageBundleService getMessageByLang의 결과를 저장한다. locale일 별로 Spring MessageSource의 Key, Value를 리턴한다. +[source, java] +---- +@Cacheable(value = "message", key = "#locale") +public Map getMessageByLang(Locale locale) { + Set beanNames = messageSource.getBasenameSet(); + Map message = new HashMap<>(); + for (String beanName : beanNames) { + ResourceBundle resourceBundle = ResourceBundle.getBundle(StringUtils.remove(beanName, "classpath:/"), locale); + Enumeration keys = resourceBundle.getKeys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + String javaVersion = StringUtils.substringBeforeLast(System.getProperty("java.version"), "."); + if (Float.parseFloat(javaVersion) < 1.9) { + message.put(key, new String(resourceBundle.getString(key).getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8)); + } else { + message.put(key, resourceBundle.getString(key)); + } + } + } + return message; +} +---- + +* menu-all + +MenuService getAllMenuPagePaths 시스템의 모든 메뉴, 메뉴 패스를 저장한다. + +[source, java] +---- +@Cacheable(value = "menu-all") +@Override +public List> getAllMenuPagePaths() { + return menuDao.getAllMenuPagePaths(); +} +---- + +* api-user + +ApiService getUserAuthApiListByUserId 사용자ID를 기준으로 접근할 수 있는 API 목록을 저장한다. 사용자가 늘어날 수록 캐시되는 데이터가 많으니 +캐시 관리에 주의하도록 한다. +[source, java] +---- +@Override +@Cacheable(value = "api-user", key = "#userId") +public List getUserAuthApiListByUserId(String userId) { + return apiDao.getUserAuthApiListByUserId(userId); +} +---- + +* api-user-menu + +ApiService getUserAuthApiListByUserId 사용자ID와 메뉴ID를 기준으로 접근할 수 있는 API 목록을 저장한다. 사용자가 늘어날 수록 캐시되는 데이터가 많으니 +캐시 관리에 주의하도록 한다. +[source, java] +---- +@Override +@Cacheable(value = "api-user-menu", key = "#userId.concat(':').concat(#menuId)") +public List getUserAuthApiListByUserIdAndMenuId(String userId, String menuId) { + return apiDao.getApiListByUserIdAndMenuId(userId, menuId); +} +---- + +* page-all-by-menu-auth + +ResourceCacheService getAllPageListByAuth 모든 메뉴의 페이지별 권한 타입을 저장한다. +[source, java] +---- +@Cacheable(value = "page-all-by-menu-auth") +public Map>> getAllPageListByAuth() { + log.debug("PageService Start."); + Map>> menuAuthMap = new HashMap<>(); + + Map> menuMap = getAllPageListByMenu(); + for (Map.Entry> entry : menuMap.entrySet()) { + + List authPageList = entry.getValue(); + Map> authMap = new HashMap<>(); + + authMap.put("READ", authPageList.stream().filter(page -> "READ".equals(page.getAuthorizationType())).collect(Collectors.toList())); + authMap.put("UPDATE", authPageList.stream().filter(page -> "UPDATE".equals(page.getAuthorizationType())).collect(Collectors.toList())); + authMap.put("DOWNLOAD", authPageList.stream().filter(page -> "DOWNLOAD".equals(page.getAuthorizationType())).collect(Collectors.toList())); + authMap.put("EXECUTE", authPageList.stream().filter(page -> "EXECUTE".equals(page.getAuthorizationType())).collect(Collectors.toList())); + menuAuthMap.put(entry.getKey(), authMap); + } + log.debug("menuAuthMap : {}", menuAuthMap); + return menuAuthMap; +} +---- + +* page-full-path-all + +MenuService getPageFullPathList 모든 메뉴 full path를 저장한다. +[source, java] +---- +@Cacheable(value="page-full-path-all") +@Override +public Map getPageFullPathList() { + + List> pageFullPathList = menuDao.getPageFullPathList(); + + Map pageFullPathMap = new HashMap<>(); + + for(Map pageFullPath : pageFullPathList) { + pageFullPathMap.put(pageFullPath.get("pageId"), pageFullPath.get("fullPath")); + } + + return pageFullPathMap; +} +---- diff --git a/doc/아키텍처/Backend구성.adoc b/doc/아키텍처/Backend구성.adoc new file mode 100644 index 0000000..fec8321 --- /dev/null +++ b/doc/아키텍처/Backend구성.adoc @@ -0,0 +1,79 @@ += Backend 구성 + +== sdl-base + +sdl-base은 시스템 개발을 위한 프로젝트로 sdl을 기반으로 시스템을 개발하기 위한 파일들로 구성되어 있다. + +---- +sdl-base + |- doc <1> + |- frontend <2> + |- src/main/java <3> + |- src/main/resource <4> + |- source <6> + |- pom.xml +---- + +. doc : SDL 개발자 문서 위치 +. frontend : SDL Frontend 프로젝트 +. src/main/java : Java file +. resource : 프로젝트 실행을 위한 설정파일, xml 파일 등 +. source : SDL 모듈 jar파일과 source-jar파일 +. pom.xml + +== src/main/java + +java source가 위치한 폴더로 maven compile 대상 프로젝트이다. SDL에서 배포되는 소스코드 외에 프로젝트에서 개발한 java class도 이곳에 작성하면 된다. SDL 기능중 시스템별로 수정이 많이 발생하거나 샘플로 제공되는 기능들은 sdl-base 모듈에 있다. +---- +com +|- onelogin.saml2 <1> +|- samsung + |- aspect <2> + |- batch <3> + |- common <4> + |- config <5> + |- filter <6> + |- interceptor <7> + |- sample <8> + |- user.controller <9> +---- +1. onelogin.saml2 : AD 통합 인증 관련 Package +2. aspect : Aspect Package +3. batch : 사용자 동기화 배치 관련 Package +4. common : 시스템 공통으로 사용하는 Util Package +5. config : Spring config Package +6. filter : Servlet Filter Package +7. interceptor : HandlerInterceptor의 구현, 서비스 전,후처리 Package +8. sample : Sample Package +9. user.controller : 로그인 관련 Package + +== src/main/resources +resources는 어플리케이션이 실행될 때 필요한 설정파일, Mybatis Query Mapping 파일들을 포함하고 있다. +resource-{profile} 형태로 되어 있어 빌드 시 knox.properties, log4j2.xml, log4jdbc.log4j2.properties, onelogin.saml.properties +의 파일이 profile에 맞게 패키징된다. + +* resources : 모든 profile에 공통적으로 적용되는 파일(예: SQL, Excel 다운로드 양식) + +---- +resources + |- excel : excel up/download mapping xml 파일 + |- message : spring message properties 파일 + |- sql.mybatis : mybatis query mapper xml 파일 + |- templates : 결재/메일 vm 파일 + +---- +* resources-local : 로컬 개발 환경에 필요한 설정 파일 +---- +resources-local + |- application.properties : spring boot 설정 파일 + |- config.properties : sdl의 설정 파일 + |- knox.properties : knox 설정 + |- log4j2.xml : log4j2 설정 + |- onelogin.saml.properties : AD 통합 인증 설정 파일 +---- + +* resources-dev : 개발 환경에 필요한 설정 파일 +* resources-prod : 운영 환경에 필요한 설정 파일 + + +IMPORTANT: 설정파일들은 반드시 프로젝트에 맞게 수정해서 사용하도록 한다. diff --git a/doc/아키텍처/Frontend구성.adoc b/doc/아키텍처/Frontend구성.adoc new file mode 100644 index 0000000..d3f5613 --- /dev/null +++ b/doc/아키텍처/Frontend구성.adoc @@ -0,0 +1,247 @@ += Frontend 구성 + +== frontend 폴더 구조 + +sdl-base/frontend 폴더 아래 구성되어 있다. + +---- +frontend + |- public <1> + |- src <2> + |- static <3> + |- .env <4> + |- index.html <5> + |- package.json <6> + |- .prettierrc.json <7> + |- eslint.config.js <8> + |- vite.config.js <9> +---- + +<1> build 시 파일명 hashing을 하지 않는 public 폴더 +<2> Vue Source 폴더 +<3> css, font 등 정적 자원을 포함한 폴더 +<4> 배포 대상에 따른 설정 파일(.env에 정의된 파일은 Vue에서 변수로 사용가능) +<5> 프로젝트 index.html +<6> npm 모듈 관리를 위한 파일 +<7> prettier 설정 json 파일 +<8> eslint 설정 파일 +<9> vite 설정 파일 + +== src + +src 폴더는 Vue의 Source 폴더이다. UI 구성을 위한 컴포넌트, 디렉티드, 필더, 라우터, 서비스 등이 있다. + +---- +src + |- components <1> + |- constants <2> + |- directives <3> + |- event <4> + |- filters <5> + |- i18n <6> + |- mixin <7> + |- router <8> + |- service <9> + |- utils <10> + |- vuex <11> +---- + +<1> 공통 및 화면단 컴포넌트 위치 ++ +** components/common/control : SDL에서 제공하는 공통 컴포넌트 위치 ++ +** components/common/popup : SDL에서 제공하는 공통 팝업 위치 ++ +** components/view : 하위에 업무별로 패키지명을 구분해 폴더 구성 +<2> Vue 에서 사용하는 전역 상수 정의 +<3> Vue 엘리먼트에서 사용되는 사용자 지정 속성을 정의 +<4> Vue3에서 deprecated 된 event bus 모듈 폴더 +<5> Vue 에서 텍스트 형식화를 적용할 수 있는 필터 정의 +<6> vue-i18n에서 사용하는 다국어관련 정의 +<7> Vue mixin 으로 사용하는 변수 및 메소드 정의 +<8> Vue를 이용한 단일페이지에서 컴포넌트별 path를 정의 +<9> Vue에서 사용하는 service를 정의 +<10> SDL에서 제공하는 공통Util 위치 +<11> Vue에서 제공하는 상태관리 + 패턴 라이브러리 위치 + +== components + +components 폴더는 SDL에서 제공하는 공통 컴포넌트와 공통 팝업, 어드민 기능 화면을 포함한다. + +---- +components + |- common + |- control <1> + |- popup <2> + |- view + |- admin + |- approval <3> + |- approvalTemplate <4> + |- batch <5> + |- board <6> + |- dept <7> + |- etc <8> + |- htmlTemplate <9> + |- log <10> + |- main <11> + |- menu <12> + |- role <13> + |- sample <14> + |- user <15> + |- workgroup <16> + |- sample <17> +---- + +<1> SDL 공통컴포넌트 +<2> SDL 공통팝업 +<3> 결재관리 +<4> 결재문서 양식 관리 +<5> 배치관리 +<6> 게시판관리 +<7> 부서관리 +<8> 기타관리 +<9> HTML 양식 관리 +<10> 로그관리 +<11> 메인페이지 +<12> 메뉴관리 +<13> 역할관리 +<14> 결재샘플 +<15> 사용자관리 +<16> 업무그룹관리 +<17> 공통컴포넌트 사용 샘플 + +IMPORTANT: 프로젝트에서 개발한 Vue 화면은 components/view 폴더 아래 있어야 한다. + +router 등록 시 디폴트 경로가 components/view 이다. + +== css 위치 및 import 방법 +image::front_02_24.png[] +사용하고자 하는 css 를 static/css 밑에 넣어놓고 src/main.js에 import 에서 사용한다. + +== directive(사용자 지정속성) +./directives/custom.js +[source, javascript] +---- +// v-directive 의 기본 구조 +export default { + beforeMount(el, binding, vnode) { + // console.log('bind'); + }, + mounted(el, binding, vndoe) { + // console.log('inserted'); + }, + updated(el, binding, vnode, oldVnode) { + // console.log('componentUpdated'); + }, +}; +---- +* Vue 엘리먼트에서 사용되는 사용자 지정 속성을 정의한다. +* 디렉티브에서 제공하는 훅 함수는 아래와 같다. ++ +** created : 이벤트 리스너가 적용되기 전에 호출된다. ++ +** beforeMount : 엘리먼트가 DOM에 삽입되기 직전에 호출된다. ++ +** mounted : 바인딩된 엘리먼트의 부모 컴포넌트 및 모든 자식 컴포넌트의 mounted 이후에 호출된다. ++ +** beforeUpdate : 부모 컴포넌트의 updated 전에 호출된다. ++ +** updated : 바인딩된 엘리먼트의 부모 컴포넌트 및 모든 자식 컴포넌트의 updated 이후에 호출됩니다. ++ +** beforeUnmount : 부모 컴포넌트의 beforeUnmount 이후에 호출된다. ++ +** unmounted : 부모 컴포넌트의 unmounted 전에 호출된다. +* 참고 : https://v3-docs.vuejs-korea.org/guide/reusability/custom-directives.html#directive-hooks + + +.directives/index.js +[source, javascript] +---- +// directive install 정의 +import custom from './custom'; +import clickOutSide from './clickOutside'; +import regex from './regex'; +import authorization from './authorization'; +import validation from './validation'; + +export default { + install(Vue) { + Vue.directive('custom', custom); + Vue.directive('click-outside', clickOutSide); + Vue.directive('regex', regex); + Vue.directive('authorization', authorization); + Vue.directive('validation', validation); + }, +}; +---- + +.main.js +[source, javascript] +---- +// directive 사용 +// ... 생략 ... +import SdlDirectives from './directives'; + +app.use(SdlDirectives); +// ... 생략 ... +}; +---- + +== filter +* Vue 에서 텍스트 형식화를 적용할 수 있는 필터를 정의한다. + +NOTE: Vue3에선 전역 method 방식으로 사용한다. + +./filters/index.js +[source, javascript] +---- +// filter install 정의 +import dateFormat from './dateFormat'; +import numberFormat from './numberFormat'; +import reverse from './reverse'; +import nl2br from './nl2br'; +import cutString from './cutString'; + +export default { + install(Vue) { + Vue.config.globalProperties.$filters = { + reverse: reverse, + dateFormat: dateFormat, + numberFormat: numberFormat, + nl2br: nl2br, + cutString: cutString, + }; + }, +}; +---- + +.예시 +[source, html] +---- + + Total {{ $filters.numberFormat(total) }} + +---- + + + +== 다국어 세팅 위치 및 사용 방법 +image::front_02_27.png[] +* vue-i18n을 통한 다국어 지원 +* 서버단의 모든 message.properties를 읽어 사용자 토큰이 없을 경우에는 브라우져의 언어셋을 사용자 토큰이 있을 경우에는 지정된 언어셋으로 지정된다. +* vue-i18n에서 제공하는 $t를 이용하거나 SDL에서 제공하는 SDLUtil 유틸을 이용해서 다국어를 사용할 수 있다. + +== 공통 component import 구조 및 사용 방법 +image::front_02_28.png[] +SDL에서 제공하는 공통 컴포넌트를 src/components/common/control/index.js에 컴포넌트로 리스트업 후 src/main.js에서 최종 plugin 한다. + +== 사용자 검색 팝업 +image::front_02_29.png[] +사용자 조회 function으로 아래와 같은 param을 정의해 사용한다. +[source,javascript] +---- +{ + searchColumn : '검색할 컬럼'(name : '이름', knoxId :'knoxId 아이디') + searchTxt : '검색할 text'('like로 검색됨') + rtnFunc : '검색한 내용을 return받을 function' + } +---- diff --git a/doc/아키텍처/SDL구성.adoc b/doc/아키텍처/SDL구성.adoc new file mode 100644 index 0000000..47c8eeb --- /dev/null +++ b/doc/아키텍처/SDL구성.adoc @@ -0,0 +1,22 @@ += SDL 구성 + +== 모듈 + +SDL은 12개의 모듈로 구성되어 있다. base 프로젝트를 제외한 나머지 11개의 모듈은 패키징 되어 jar파일로 배포된다. + +image::module_dependency_01.png[] + +* core : SDL 모듈 가장 상위에 위치하고 SDL 에서 annotation, exception등이 포함되어 있다. +* common : 다른 모듈에서 사용되는 entity, service 등이 포함되어 있다. +* resource : 파일 업/다운로드 관련한 기능이 포함되어 있다. +* knox : Knox에서 제공하는 REST서비스를 연계하기 위한 기능들이 포함되어 있다. +* email : 메일, 메일 그룹 관리 기능이 포함되어 있다. +* auth : 사용자, 권한, 메뉴, 역할 등 인증, 인가관련 기능이 포함되어 있다. +* support: 배치관리, 메뉴사용이력, 번역 서비스 등이 포함되어 있다. +* approval : 결재, 결재자관리, 결재문서 관리 등 결재에 관련된 기능이 포함되어 있다. +* board : 게시판 기능이 포함되어 있다. +* excel : 엑셀 업/다운로드 기능이 포함되어 있다. +* history : 메뉴 활용도, 메뉴 사용 이력 등 이력관리 기능이 포함되어 있다. + +IMPORTANT: sdl-base/source에 배포된 모든 모듈에 대한 원본 소스 파일이 함께 배포되니 분실하지 않도록 주의한다. + diff --git a/doc/아키텍처/SPA설계가이드.adoc b/doc/아키텍처/SPA설계가이드.adoc new file mode 100644 index 0000000..d580dab --- /dev/null +++ b/doc/아키텍처/SPA설계가이드.adoc @@ -0,0 +1,198 @@ += SPA 설계 가이드 + +== 명명규칙 +JavaScript에 대한 명명규칙으로 UI 컴포넌트, Vue 화면 개발시 표준으로 활용한다. + +=== 변수 명명 규칙 +* 표준 용어사전(DD)에 있는 영문명 또는 영문약어를 사용하여 의미를 잘 파악할 수 있도록 작성한다. (권고사항) +* 한글 발음을 영어로 그대로 옮겨서 사용하거나 (예: GUBUN), 무의미한 변수명 (예: a, b, c 등)은 사용하지 않는다. +* 변수의 이름은 소문자로 시작하고, 복합어일 경우에는 두번째 시작하는 단어의 첫글자는 대문자로 표기한다. ++ +예) firstName, zipCode ++ +* 명사로 작성하면 각 단어의 첫글자는 대문자로 표기한다. +* Vue(JS)내 method 명명 규칙 +* Vue 사용되는 메소드 명명 규칙(한글명, 영문규칙)은 다음과 같으며, 아래 내용을 준수하여 작성하도록 한다. ++ +[cols=5*] +|=== +|파일 유형 +|Method 구분 +|명명규칙 +|사용 예 +|비고 + +.14+<|Vue, JS +|등록 +|insert + 오퍼레이션명 +|insertXxx() +| + +|수정 +|update + 오퍼레이션명 +|updateXxx() +| + +|삭제 +|delete + 오퍼레이션명 +|deleteXxx() +| + +|등록/수정/삭제 혼합 +|save + 오퍼레이션명 +|saveXxx() +|CUD동시 처리 + +|조회 (1건) +|get + 오퍼레이션명 +|getXxx() +| + +|다건 조회 +|list(get) + 오퍼레이션명 +|listXxx() +| + +|기능 실행 +|execute + 오퍼레이션명 +|executeXxx() +| + +|파일업로드 +|upload + 오퍼레이션명 +|uploadXxx() +| + +|파일다운로드 +|download + 오퍼레이션명 +|downloadXxx() +| + +|계산 +|calc + 오퍼레이션명 +|calcXxx() +| + +|출력 +|print + 오퍼레이션명 +|printXxx() +| + +|true/false +|is + 오퍼레이션명 +|isXxx() +| + +|확인 +|check + 오퍼레이션명 +|checkXxx() +| + +|개수 +|count + 오퍼레이션명 +|countXxx() +| + +.2+<|Getter, Setter +|getter +|get + 변수명 +| +| + +|setter +|set + 변수명 +|setXxx() +| +|=== + +=== 패키지 명명 규칙 +UI 개발 패키지 구조는 시스템 분류별로 구분되며 해당 패키지 아래로 UI 파일이 관리된다. + +[cols=7*] +|=== +|구분 +|1 +|2 +|3 +|4 +|5 +|비고 + +.2+<|패키지 구조 +.2+<|components +|common +|system(대분류) +|sub system(중분류) +|sub-sub system(소분류가 있을 경우) +|모듈별 vue, js 파일 + +|view +|admin(대분류) +|user(중분류) +|popup(소분류가 있을 경우) +|모듈별 vue, js 파일 +|=== +=== 그 외 공통 명명 규칙 +** 모든 파일은 UTF-8 포맷으로 작성한다. +** 모든 명명은 a-z, A-Z, 0-9의 영문 대소문자와 숫자의 조합으로 구성한다. +** 클래스(Class)명, 속성(Attribute)명 및 각종 오퍼레이션(Operation)명은 일관된 용어를 사용하도록 한다. +** 기본적으로 축약형을 사용하는 것을 원칙으로 하며, 풀 네임(Full Name) 사용은 지양하되 명시적으로 의미 식별이 필요한 경우 예외적으로 적용할 수 있다. +** 단어는 50자 이상 사용하는 것을 권장하지 않는다. +** 공통 naming 규칙 : 상수와 filename을 제외하고는 CamelCase를 표기법 사용한다. +** 유사한 이름을 사용함으로써 명명의 의미가 혼동되지 않도록 한다. ++ +예) exampleName vs exampleNames + +== 설계방법 +=== MVVM (Model - View - View Model) 중심설계 +image::front_02_21.png[] +=== MVVM 패턴의 ViewModel 레이어에 해당하는 View단 라이브러리 사용 +image::front_02_22.png[] +* 데이터 바인딩과 화면 단위를 컴포넌트 형태로 제공하며, 관련 API를 지원하는데에 궁극적인 목적이 있음 +* Angular 에서 지원하는 2 way data bindings 을 동일하게 제공 +* 하지만 Component 간 통신의 기본 골격을 React의 1 Way Data Flow (부모 -> 자식) 와 유사 +* 다른 Front-End FW (Angular, React) 와 비교했을 때 훨씬 가볍고 빠름 +* 간단한 Vue 를 적용하는데 있어서도 러닝커브가 낮고, 쉽게 접근 가능 + +=== REST API 호출 +* REST API 설계원칙에 따라 API를 호출한다. +* HTTP METHOD를 이용하여 행위를 선언한다. + +예) HTTP METHOD : POST(삽입), GET(조회), PUT(수정), DELETE(삭제) + +=== 데이터 바인딩 +* Vue 가 DOM 기반 HTML Template에 Vue 데이터를 바인딩 하는 방법은 아래와 같이 크게 3가지가 있다. +* Interpolation - 값 대입 : Vue 의 가장 기본적인 데이터 바인딩 체계는 Mustache {{ }} 를 따른다. + +[source, javascript] +---- +ex) Message: {{ msg }} +---- +* Binding Expressions - 값 연결 : "{{ }}" 를 이용한 데이터 바인딩을 할 떄 자바스크립트 표현식을 사용할 수 있다. + +[source, javascript] +---- +ex)
{{ message.split('').reverse().join('') }}
+---- +* Directive : Vue 에서 제공하는 특별한 Attributes 이며 -v 의 prefix(접두사)를 갖는다. + +[source, javascript] +---- +ex) +---- +== 화면 랜더링 +변경된 데이터를 중심으로 화면을 수정하는 방법으로 빠르게 화면을 갱신한다 + +=== 화면설계시 주의할 점 +* this.$parent 피하기 ++ +Vue 컴포넌트는 다른 모든 컴포넌트와 마찬가지로 독립적으로 작동해야 한다. ++ +컴포넌트가 부모에 접근하는 경우 다른 곳에서 재사용할 수 없다. +* this.$refs 주의하여 사용하기 ++ +Vue.js는 컴포넌트가 ref 어트리뷰트를 통해 다른 컴포넌트와 기본 HTML 엘리먼트에 접근 할 수 있도록 지원한다. ++ +하지만 Vue 컴포넌트에 잘못된 접근은 주의해야 한다 ++ +Vue 컴포넌트는 많은 API를 제공해야 하고 모든 접근을 지원하지 않으면 잘못 설계/구현된 것이다. diff --git a/doc/아키텍처/아키텍처.adoc b/doc/아키텍처/아키텍처.adoc new file mode 100644 index 0000000..2575ff9 --- /dev/null +++ b/doc/아키텍처/아키텍처.adoc @@ -0,0 +1,11 @@ += 아키텍처 + +SDL은 Spring 6 기반의 Back-end, Vue.js 3 기반의 Front-end로 구성되어 있다. Back-end는 MVC 패턴을 기본으로 RESTful API 아키텍처 Front-end는 MVVM 패턴을 기본으로 SPA 아키텍처로 되어 있다. + +include::SDL구성.adoc[leveloffset=+1] + +include::Backend구성.adoc[leveloffset=+1] + +include::Frontend구성.adoc[leveloffset=+1] + +//include::SPA설계가이드.adoc[leveloffset=+1] \ No newline at end of file diff --git a/frontend/.env.development b/frontend/.env.development new file mode 100644 index 0000000..8faaef4 --- /dev/null +++ b/frontend/.env.development @@ -0,0 +1,11 @@ +# 공통으로 적용하는 env 파일, .env.환경이름 과 merge +VITE_DESC=COMMON +# To set default Language for I18n (ko_KR, en_US, zh_CN, etc..) +VITE_DEFAULT_LANG=ko_KR + +# default env +VITE_NODE_ENV=development +VITE_API_URL=http://sdldev.misdev.sdspaas.io/ +VITE_DIST_PATH=../src/main/webapp/dd +# CONTEXT_PATH value must be end with '/' +VITE_WEB_CONTEXT_PATH=/ diff --git a/frontend/.env.production b/frontend/.env.production new file mode 100644 index 0000000..9b74e0b --- /dev/null +++ b/frontend/.env.production @@ -0,0 +1,11 @@ +# 공통으로 적용하는 env 파일, .env.환경이름 과 merge +VITE_DESC=COMMON +# To set default Language for I18n (ko_KR, en_US, zh_CN, etc..) +VITE_DEFAULT_LANG=ko_KR + +# default env +VITE_NODE_ENV=production +VITE_API_URL=http://sdl.sec.samsung.net/administrator +VITE_DIST_PATH=../src/main/webapp/pp +# CONTEXT_PATH value must be end with '/' +VITE_WEB_CONTEXT_PATH=/ diff --git a/frontend/.env.test b/frontend/.env.test new file mode 100644 index 0000000..9bec64b --- /dev/null +++ b/frontend/.env.test @@ -0,0 +1,11 @@ +# 공통으로 적용하는 env 파일, .env.환경이름 과 merge +VITE_DESC=COMMON +# To set default Language for I18n (ko_KR, en_US, zh_CN, etc..) +VITE_DEFAULT_LANG=ko_KR + +# default env +VITE_NODE_ENV=local +VITE_API_URL=http://localhost:8080 +VITE_DIST_PATH=../src/main/resources/public +# CONTEXT_PATH value must be end with '/' +VITE_WEB_CONTEXT_PATH=/ diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..b46ff4b --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json new file mode 100644 index 0000000..99689cf --- /dev/null +++ b/frontend/.prettierrc.json @@ -0,0 +1,11 @@ +{ + "endOfLine": "crlf", + "printWidth": 240, + "proseWrap": "never", + "arrowParens": "avoid", + "bracketSpacing": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "semi": true +} \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..6484976 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,35 @@ +# frontend + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). + +## Customize configuration + +See [Vite Configuration Reference](https://vitejs.dev/config/). + +## Project Setup + +```sh +npm install +``` + +### Compile and Hot-Reload for Development + +```sh +npm run dev +``` + +### Compile and Minify for Production + +```sh +npm run build +``` + +### Lint with [ESLint](https://eslint.org/) + +```sh +npm run lint +``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..86f8e62 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,17 @@ +import pluginVue from 'eslint-plugin-vue'; +import eslintConfigPrettier from 'eslint-config-prettier'; + +export default [ + // add more generic rulesets here, such as: + // js.configs.recommended, + ...pluginVue.configs['flat/strongly-recommended'], + eslintConfigPrettier, + // ...pluginVue.configs['flat/vue2-recommended'], // Use this if you are using Vue.js 2.x. + { + files: ['src/**/*.js'], + rules: { + // override/add rules settings here, such as: + // 'vue/no-unused-vars': 'error' + }, + }, +]; diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..e7af565 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + + SDL DEMO + + + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..5611d91 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3692 @@ +{ + "name": "frontend", + "version": "6.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "6.0.0", + "dependencies": { + "axios": "^1.7.7", + "jquery": "^3.7.1", + "jquery-ui": "^1.14.1", + "jquery.fancytree": "^2.38.4", + "lodash": "^4.17.21", + "moment": "^2.30.1", + "realgrid": "^2.8.5", + "tiny-emitter": "^2.1.0", + "vue": "^3.4.27", + "vue-flatpickr-component": "^11.0.5", + "vue-i18n": "^9.13.0", + "vue-multiselect": "^3.1.0", + "vue-router": "^4.3.1", + "vuedraggable": "^4.1.0", + "vuex": "^4.1.0" + }, + "devDependencies": { + "@rushstack/eslint-patch": "^1.10.0", + "@vitejs/plugin-vue": "^5.0.4", + "@vitejs/plugin-vue-jsx": "^3.1.0", + "@vue/eslint-config-prettier": "^9.0.0", + "dotenv": "^16.0.3", + "eslint": "^9.3.0", + "eslint-plugin-vue": "^9.26.0", + "less": "^4.1.3", + "prettier": "^3.2.5", + "vite": "^5.2.12" + }, + "engines": { + "node": ">= 20.14.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", + "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", + "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", + "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.7.tgz", + "integrity": "sha512-iLD3UNkgx2n/HrjBesVbYX6j0yqn/sJktvbtKKgcaLIQ4bTTQ8obAypc1VpyHPD2y4Phh9zHOaAt8e/L14wCpw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-typescript": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", + "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.16.0.tgz", + "integrity": "sha512-/jmuSd74i4Czf1XXn7wGRWZCuyaUZ330NH1Bek0Pplatt4Sy1S5haN21SCLLdbeKslQ+S0wEJ+++v5YibSi+Lg==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.5.0.tgz", + "integrity": "sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@intlify/core-base": { + "version": "9.13.1", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.13.1.tgz", + "integrity": "sha512-+bcQRkJO9pcX8d0gel9ZNfrzU22sZFSA0WVhfXrf5jdJOS24a+Bp8pozuS9sBI9Hk/tGz83pgKfmqcn/Ci7/8w==", + "dependencies": { + "@intlify/message-compiler": "9.13.1", + "@intlify/shared": "9.13.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "9.13.1", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.13.1.tgz", + "integrity": "sha512-SKsVa4ajYGBVm7sHMXd5qX70O2XXjm55zdZB3VeMFCvQyvLew/dLvq3MqnaIsTMF1VkkOb9Ttr6tHcMlyPDL9w==", + "dependencies": { + "@intlify/shared": "9.13.1", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "9.13.1", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.13.1.tgz", + "integrity": "sha512-u3b6BKGhE6j/JeRU6C/RL2FgyJfy6LakbtfeVF8fJXURpZZTzfh3e05J0bu0XPw447Q6/WUp3C4ajv4TMS4YsQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", + "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", + "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", + "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", + "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", + "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", + "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", + "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", + "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", + "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", + "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", + "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", + "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", + "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", + "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", + "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", + "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.5.tgz", + "integrity": "sha512-LOjm7XeIimLBZyzinBQ6OSm3UBCNVCpLkxGC0oWmm2YPzVZoxMsdvNVimLTBzpAnR9hl/yn1SHGuRfe6/Td9rQ==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitejs/plugin-vue-jsx": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-3.1.0.tgz", + "integrity": "sha512-w9M6F3LSEU5kszVb9An2/MmXNxocAnUb3WhRr8bHlimhDrXNt6n6D2nJQR3UXpGlZHh/EsgouOHCsM8V3Ln+WA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.3", + "@babel/plugin-transform-typescript": "^7.23.3", + "@vue/babel-plugin-jsx": "^1.1.5" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0 || ^5.0.0", + "vue": "^3.0.0" + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.2.2.tgz", + "integrity": "sha512-nOttamHUR3YzdEqdM/XXDyCSdxMA9VizUKoroLX6yTyRtggzQMHXcmwh8a7ZErcJttIBIc9s68a1B8GZ+Dmvsw==", + "dev": true + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.2.2.tgz", + "integrity": "sha512-nYTkZUVTu4nhP199UoORePsql0l+wj7v/oyQjtThUVhJl1U+6qHuoVhIvR3bf7eVKjbCK+Cs2AWd7mi9Mpz9rA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "~7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.23.3", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", + "@vue/babel-helper-vue-transform-on": "1.2.2", + "@vue/babel-plugin-resolve-type": "1.2.2", + "camelcase": "^6.3.0", + "html-tags": "^3.3.1", + "svg-tags": "^1.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-jsx/node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.2.2.tgz", + "integrity": "sha512-EntyroPwNg5IPVdUJupqs0CFzuf6lUrVvCspmv2J1FITLeGnUCuoGNNk78dgCusxEiYj6RMkTJflGSxk5aIC4A==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/helper-module-imports": "~7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/parser": "^7.23.9", + "@vue/compiler-sfc": "^3.4.15" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-plugin-resolve-type/node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.4.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.30.tgz", + "integrity": "sha512-ZL8y4Xxdh8O6PSwfdZ1IpQ24PjTAieOz3jXb/MDTfDtANcKBMxg1KLm6OX2jofsaQGYfIVzd3BAG22i56/cF1w==", + "dependencies": { + "@babel/parser": "^7.24.7", + "@vue/shared": "3.4.30", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.4.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.30.tgz", + "integrity": "sha512-+16Sd8lYr5j/owCbr9dowcNfrHd+pz+w2/b5Lt26Oz/kB90C9yNbxQ3bYOvt7rI2bxk0nqda39hVcwDFw85c2Q==", + "dependencies": { + "@vue/compiler-core": "3.4.30", + "@vue/shared": "3.4.30" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.4.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.30.tgz", + "integrity": "sha512-8vElKklHn/UY8+FgUFlQrYAPbtiSB2zcgeRKW7HkpSRn/JjMRmZvuOtwDx036D1aqKNSTtXkWRfqx53Qb+HmMg==", + "dependencies": { + "@babel/parser": "^7.24.7", + "@vue/compiler-core": "3.4.30", + "@vue/compiler-dom": "3.4.30", + "@vue/compiler-ssr": "3.4.30", + "@vue/shared": "3.4.30", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.10", + "postcss": "^8.4.38", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.4.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.30.tgz", + "integrity": "sha512-ZJ56YZGXJDd6jky4mmM0rNaNP6kIbQu9LTKZDhcpddGe/3QIalB1WHHmZ6iZfFNyj5mSypTa4+qDJa5VIuxMSg==", + "dependencies": { + "@vue/compiler-dom": "3.4.30", + "@vue/shared": "3.4.30" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.3.tgz", + "integrity": "sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==" + }, + "node_modules/@vue/eslint-config-prettier": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-z1ZIAAUS9pKzo/ANEfd2sO+v2IUalz7cM/cTLOZ7vRFOPk5/xuRKQteOu1DErFLAh/lYGXMVZ0IfYKlyInuDVg==", + "dev": true, + "dependencies": { + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0" + }, + "peerDependencies": { + "eslint": ">= 8.0.0", + "prettier": ">= 3.0.0" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.4.30", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.30.tgz", + "integrity": "sha512-bVJurnCe3LS0JII8PPoAA63Zd2MBzcKrEzwdQl92eHCcxtIbxD2fhNwJpa+KkM3Y/A4T5FUnmdhgKwOf6BfbcA==", + "dependencies": { + "@vue/shared": "3.4.30" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.4.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.30.tgz", + "integrity": "sha512-qaFEbnNpGz+tlnkaualomogzN8vBLkgzK55uuWjYXbYn039eOBZrWxyXWq/7qh9Bz2FPifZqGjVDl/FXiq9L2g==", + "dependencies": { + "@vue/reactivity": "3.4.30", + "@vue/shared": "3.4.30" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.4.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.30.tgz", + "integrity": "sha512-tV6B4YiZRj5QsaJgw2THCy5C1H+2UeywO9tqgWEc21tn85qHEERndHN/CxlyXvSBFrpmlexCIdnqPuR9RM9thw==", + "dependencies": { + "@vue/reactivity": "3.4.30", + "@vue/runtime-core": "3.4.30", + "@vue/shared": "3.4.30", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.4.30", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.30.tgz", + "integrity": "sha512-TBD3eqR1DeDc0cMrXS/vEs/PWzq1uXxnvjoqQuDGFIEHFIwuDTX/KWAQKIBjyMWLFHEeTDGYVsYci85z2UbTDg==", + "dependencies": { + "@vue/compiler-ssr": "3.4.30", + "@vue/shared": "3.4.30" + }, + "peerDependencies": { + "vue": "3.4.30" + } + }, + "node_modules/@vue/shared": { + "version": "3.4.30", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.30.tgz", + "integrity": "sha512-CLg+f8RQCHQnKvuHY9adMsMaQOcqclh6Z5V9TaoMgy0ut0tz848joZ7/CYFFyF/yZ5i2yaw7Fn498C+CNZVHIg==" + }, + "node_modules/acorn": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001636", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz", + "integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.811", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.811.tgz", + "integrity": "sha512-CDyzcJ5XW78SHzsIOdn27z8J4ist8eaFLhdto2hSMSJQgsiwvbv2fbizcKUICryw1Wii1TI/FEkvzvJsR3awrA==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.5.0.tgz", + "integrity": "sha512-+NAOZFrW/jFTS3dASCGBxX1pkFD0/fsO+hfAkJ4TyYKwgsXZbqzrw+seCYFCcPCYXvnD67tAnglU7GQTz6kcVw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/config-array": "^0.16.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.5.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.0.1", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.1", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.26.0.tgz", + "integrity": "sha512-eTvlxXgd4ijE1cdur850G6KalZqk65k1JKoOI2d1kT3hr8sPD07j1q98FRFdNnpxBELGPWxZmInxeHGF/GxtqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.0", + "vue-eslint-parser": "^9.4.2", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-vue/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-vue/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-scope": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", + "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatpickr": { + "version": "4.6.13", + "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz", + "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==", + "license": "MIT" + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "optional": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "license": "MIT" + }, + "node_modules/jquery-ui": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.14.1.tgz", + "integrity": "sha512-DhzsYH8VeIvOaxwi+B/2BCsFFT5EGjShdzOcm5DssWjtcpGWIMsn66rJciDA6jBruzNiLf1q0KvwMoX1uGNvnQ==", + "license": "MIT", + "dependencies": { + "jquery": ">=1.12.0 <5.0.0" + } + }, + "node_modules/jquery.fancytree": { + "version": "2.38.4", + "resolved": "https://registry.npmjs.org/jquery.fancytree/-/jquery.fancytree-2.38.4.tgz", + "integrity": "sha512-f4Fv5jZiZ6pBml/9txcJRAQDZpqQGGoJ8BUbicZKcO4CpgGbqBX9W7eQFwEaKQS0bxdVBLbqWQ9RoUK05ON2kQ==", + "license": "MIT", + "peerDependencies": { + "jquery": ">=1.9" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/less": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", + "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "dev": true, + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.10", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", + "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", + "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/realgrid": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/realgrid/-/realgrid-2.8.8.tgz", + "integrity": "sha512-BTICyug9txZIaXZ/t3PujX99ipbJmIaAYbBQCn8cq+IvRHkyUrDwnaxBj7Dc26Qg9WmLiR17mKdl6DOAXeFmgg==", + "license": "license.txt" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", + "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.18.0", + "@rollup/rollup-android-arm64": "4.18.0", + "@rollup/rollup-darwin-arm64": "4.18.0", + "@rollup/rollup-darwin-x64": "4.18.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", + "@rollup/rollup-linux-arm-musleabihf": "4.18.0", + "@rollup/rollup-linux-arm64-gnu": "4.18.0", + "@rollup/rollup-linux-arm64-musl": "4.18.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", + "@rollup/rollup-linux-riscv64-gnu": "4.18.0", + "@rollup/rollup-linux-s390x-gnu": "4.18.0", + "@rollup/rollup-linux-x64-gnu": "4.18.0", + "@rollup/rollup-linux-x64-musl": "4.18.0", + "@rollup/rollup-win32-arm64-msvc": "4.18.0", + "@rollup/rollup-win32-ia32-msvc": "4.18.0", + "@rollup/rollup-win32-x64-msvc": "4.18.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "optional": true + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "optional": true + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sortablejs": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz", + "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz", + "integrity": "sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.4.30", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.30.tgz", + "integrity": "sha512-NcxtKCwkdf1zPsr7Y8+QlDBCGqxvjLXF2EX+yi76rV5rrz90Y6gK1cq0olIhdWGgrlhs9ElHuhi9t3+W5sG5Xw==", + "dependencies": { + "@vue/compiler-dom": "3.4.30", + "@vue/compiler-sfc": "3.4.30", + "@vue/runtime-dom": "3.4.30", + "@vue/server-renderer": "3.4.30", + "@vue/shared": "3.4.30" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vue-flatpickr-component": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/vue-flatpickr-component/-/vue-flatpickr-component-11.0.5.tgz", + "integrity": "sha512-Vfwg5uVU+sanKkkLzUGC5BUlWd5wlqAMq/UpQ6lI2BCZq0DDrXhOMX7hrevt8bEgglIq2QUv0K2Nl84Me/VnlA==", + "license": "MIT", + "dependencies": { + "flatpickr": "^4.6.13" + }, + "engines": { + "node": ">=14.13.0" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-i18n": { + "version": "9.13.1", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.13.1.tgz", + "integrity": "sha512-mh0GIxx0wPtPlcB1q4k277y0iKgo25xmDPWioVVYanjPufDBpvu5ySTjP5wOrSvlYQ2m1xI+CFhGdauv/61uQg==", + "dependencies": { + "@intlify/core-base": "9.13.1", + "@intlify/shared": "9.13.1", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vue-multiselect": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-3.2.0.tgz", + "integrity": "sha512-ExI+IPvSbILbtaHrU0CgbBmfbD6yBpIWJKsGLPmuQMC7VWK8Nj1XSAI9eIt3n9/e+LSFYdt8VgfHxeS1O1OeVA==", + "license": "MIT", + "engines": { + "node": ">= 14.18.1", + "npm": ">= 6.14.15" + } + }, + "node_modules/vue-router": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.0.tgz", + "integrity": "sha512-HB+t2p611aIZraV2aPSRNXf0Z/oLZFrlygJm+sZbdJaW6lcFqEDQwnzUBXn+DApw+/QzDU/I9TeWx9izEjTmsA==", + "dependencies": { + "@vue/devtools-api": "^6.5.1" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vuedraggable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz", + "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==", + "dependencies": { + "sortablejs": "1.14.0" + }, + "peerDependencies": { + "vue": "^3.0.1" + } + }, + "node_modules/vuex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz", + "integrity": "sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==", + "dependencies": { + "@vue/devtools-api": "^6.0.0-beta.11" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..777e959 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,52 @@ +{ + "type": "module", + "name": "frontend", + "version": "6.0.0", + "private": true, + "scripts": { + "local": "vite --mode test", + "dev": "vite --mode development", + "prod": "vite --mode production", + "build": "vite build --mode production", + "build-test": "vite build --mode test", + "build-dev": "vite build --mode development", + "build-prod": "vite build --mode production", + "preview": "vite preview --mode production", + "preview-test": "vite preview --mode test", + "preview-dev": "vite preview --mode development", + "preview-prod": "vite preview --mode production", + "lint": "eslint src/**/*.{vue,js}" + }, + "engines": { + "node": ">= 20.14.0" + }, + "dependencies": { + "axios": "^1.7.7", + "jquery": "^3.7.1", + "jquery-ui": "^1.14.1", + "jquery.fancytree": "^2.38.4", + "lodash": "^4.17.21", + "moment": "^2.30.1", + "realgrid": "^2.8.5", + "tiny-emitter": "^2.1.0", + "vue": "^3.4.27", + "vue-flatpickr-component": "^11.0.5", + "vue-i18n": "^9.13.0", + "vue-multiselect": "^3.1.0", + "vue-router": "^4.3.1", + "vuedraggable": "^4.1.0", + "vuex": "^4.1.0" + }, + "devDependencies": { + "@rushstack/eslint-patch": "^1.10.0", + "@vitejs/plugin-vue": "^5.0.4", + "@vitejs/plugin-vue-jsx": "^3.1.0", + "@vue/eslint-config-prettier": "^9.0.0", + "dotenv": "^16.0.3", + "eslint": "^9.3.0", + "eslint-plugin-vue": "^9.26.0", + "less": "^4.1.3", + "prettier": "^3.2.5", + "vite": "^5.2.12" + } +} diff --git a/frontend/public/static/css/bootstrap-icons.css b/frontend/public/static/css/bootstrap-icons.css new file mode 100644 index 0000000..fb57622 --- /dev/null +++ b/frontend/public/static/css/bootstrap-icons.css @@ -0,0 +1,2020 @@ +@font-face { + font-display: block; + font-family: "bootstrap-icons"; + src: url("../fonts/bootstrap-icons.woff2") format("woff2"), +url("../fonts/bootstrap-icons.woff") format("woff"); +} + +.bi::before, +[class^="bi-"]::before, +[class*=" bi-"]::before { + display: inline-block; + font-family: bootstrap-icons !important; + font-style: normal; + font-weight: normal !important; + font-variant: normal; + text-transform: none; + line-height: 1; + vertical-align: -.125em; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.bi-123::before { content: "\f67f"; } +.bi-alarm-fill::before { content: "\f101"; } +.bi-alarm::before { content: "\f102"; } +.bi-align-bottom::before { content: "\f103"; } +.bi-align-center::before { content: "\f104"; } +.bi-align-end::before { content: "\f105"; } +.bi-align-middle::before { content: "\f106"; } +.bi-align-start::before { content: "\f107"; } +.bi-align-top::before { content: "\f108"; } +.bi-alt::before { content: "\f109"; } +.bi-app-indicator::before { content: "\f10a"; } +.bi-app::before { content: "\f10b"; } +.bi-archive-fill::before { content: "\f10c"; } +.bi-archive::before { content: "\f10d"; } +.bi-arrow-90deg-down::before { content: "\f10e"; } +.bi-arrow-90deg-left::before { content: "\f10f"; } +.bi-arrow-90deg-right::before { content: "\f110"; } +.bi-arrow-90deg-up::before { content: "\f111"; } +.bi-arrow-bar-down::before { content: "\f112"; } +.bi-arrow-bar-left::before { content: "\f113"; } +.bi-arrow-bar-right::before { content: "\f114"; } +.bi-arrow-bar-up::before { content: "\f115"; } +.bi-arrow-clockwise::before { content: "\f116"; } +.bi-arrow-counterclockwise::before { content: "\f117"; } +.bi-arrow-down-circle-fill::before { content: "\f118"; } +.bi-arrow-down-circle::before { content: "\f119"; } +.bi-arrow-down-left-circle-fill::before { content: "\f11a"; } +.bi-arrow-down-left-circle::before { content: "\f11b"; } +.bi-arrow-down-left-square-fill::before { content: "\f11c"; } +.bi-arrow-down-left-square::before { content: "\f11d"; } +.bi-arrow-down-left::before { content: "\f11e"; } +.bi-arrow-down-right-circle-fill::before { content: "\f11f"; } +.bi-arrow-down-right-circle::before { content: "\f120"; } +.bi-arrow-down-right-square-fill::before { content: "\f121"; } +.bi-arrow-down-right-square::before { content: "\f122"; } +.bi-arrow-down-right::before { content: "\f123"; } +.bi-arrow-down-short::before { content: "\f124"; } +.bi-arrow-down-square-fill::before { content: "\f125"; } +.bi-arrow-down-square::before { content: "\f126"; } +.bi-arrow-down-up::before { content: "\f127"; } +.bi-arrow-down::before { content: "\f128"; } +.bi-arrow-left-circle-fill::before { content: "\f129"; } +.bi-arrow-left-circle::before { content: "\f12a"; } +.bi-arrow-left-right::before { content: "\f12b"; } +.bi-arrow-left-short::before { content: "\f12c"; } +.bi-arrow-left-square-fill::before { content: "\f12d"; } +.bi-arrow-left-square::before { content: "\f12e"; } +.bi-arrow-left::before { content: "\f12f"; } +.bi-arrow-repeat::before { content: "\f130"; } +.bi-arrow-return-left::before { content: "\f131"; } +.bi-arrow-return-right::before { content: "\f132"; } +.bi-arrow-right-circle-fill::before { content: "\f133"; } +.bi-arrow-right-circle::before { content: "\f134"; } +.bi-arrow-right-short::before { content: "\f135"; } +.bi-arrow-right-square-fill::before { content: "\f136"; } +.bi-arrow-right-square::before { content: "\f137"; } +.bi-arrow-right::before { content: "\f138"; } +.bi-arrow-up-circle-fill::before { content: "\f139"; } +.bi-arrow-up-circle::before { content: "\f13a"; } +.bi-arrow-up-left-circle-fill::before { content: "\f13b"; } +.bi-arrow-up-left-circle::before { content: "\f13c"; } +.bi-arrow-up-left-square-fill::before { content: "\f13d"; } +.bi-arrow-up-left-square::before { content: "\f13e"; } +.bi-arrow-up-left::before { content: "\f13f"; } +.bi-arrow-up-right-circle-fill::before { content: "\f140"; } +.bi-arrow-up-right-circle::before { content: "\f141"; } +.bi-arrow-up-right-square-fill::before { content: "\f142"; } +.bi-arrow-up-right-square::before { content: "\f143"; } +.bi-arrow-up-right::before { content: "\f144"; } +.bi-arrow-up-short::before { content: "\f145"; } +.bi-arrow-up-square-fill::before { content: "\f146"; } +.bi-arrow-up-square::before { content: "\f147"; } +.bi-arrow-up::before { content: "\f148"; } +.bi-arrows-angle-contract::before { content: "\f149"; } +.bi-arrows-angle-expand::before { content: "\f14a"; } +.bi-arrows-collapse::before { content: "\f14b"; } +.bi-arrows-expand::before { content: "\f14c"; } +.bi-arrows-fullscreen::before { content: "\f14d"; } +.bi-arrows-move::before { content: "\f14e"; } +.bi-aspect-ratio-fill::before { content: "\f14f"; } +.bi-aspect-ratio::before { content: "\f150"; } +.bi-asterisk::before { content: "\f151"; } +.bi-at::before { content: "\f152"; } +.bi-award-fill::before { content: "\f153"; } +.bi-award::before { content: "\f154"; } +.bi-back::before { content: "\f155"; } +.bi-backspace-fill::before { content: "\f156"; } +.bi-backspace-reverse-fill::before { content: "\f157"; } +.bi-backspace-reverse::before { content: "\f158"; } +.bi-backspace::before { content: "\f159"; } +.bi-badge-3d-fill::before { content: "\f15a"; } +.bi-badge-3d::before { content: "\f15b"; } +.bi-badge-4k-fill::before { content: "\f15c"; } +.bi-badge-4k::before { content: "\f15d"; } +.bi-badge-8k-fill::before { content: "\f15e"; } +.bi-badge-8k::before { content: "\f15f"; } +.bi-badge-ad-fill::before { content: "\f160"; } +.bi-badge-ad::before { content: "\f161"; } +.bi-badge-ar-fill::before { content: "\f162"; } +.bi-badge-ar::before { content: "\f163"; } +.bi-badge-cc-fill::before { content: "\f164"; } +.bi-badge-cc::before { content: "\f165"; } +.bi-badge-hd-fill::before { content: "\f166"; } +.bi-badge-hd::before { content: "\f167"; } +.bi-badge-tm-fill::before { content: "\f168"; } +.bi-badge-tm::before { content: "\f169"; } +.bi-badge-vo-fill::before { content: "\f16a"; } +.bi-badge-vo::before { content: "\f16b"; } +.bi-badge-vr-fill::before { content: "\f16c"; } +.bi-badge-vr::before { content: "\f16d"; } +.bi-badge-wc-fill::before { content: "\f16e"; } +.bi-badge-wc::before { content: "\f16f"; } +.bi-bag-check-fill::before { content: "\f170"; } +.bi-bag-check::before { content: "\f171"; } +.bi-bag-dash-fill::before { content: "\f172"; } +.bi-bag-dash::before { content: "\f173"; } +.bi-bag-fill::before { content: "\f174"; } +.bi-bag-plus-fill::before { content: "\f175"; } +.bi-bag-plus::before { content: "\f176"; } +.bi-bag-x-fill::before { content: "\f177"; } +.bi-bag-x::before { content: "\f178"; } +.bi-bag::before { content: "\f179"; } +.bi-bar-chart-fill::before { content: "\f17a"; } +.bi-bar-chart-line-fill::before { content: "\f17b"; } +.bi-bar-chart-line::before { content: "\f17c"; } +.bi-bar-chart-steps::before { content: "\f17d"; } +.bi-bar-chart::before { content: "\f17e"; } +.bi-basket-fill::before { content: "\f17f"; } +.bi-basket::before { content: "\f180"; } +.bi-basket2-fill::before { content: "\f181"; } +.bi-basket2::before { content: "\f182"; } +.bi-basket3-fill::before { content: "\f183"; } +.bi-basket3::before { content: "\f184"; } +.bi-battery-charging::before { content: "\f185"; } +.bi-battery-full::before { content: "\f186"; } +.bi-battery-half::before { content: "\f187"; } +.bi-battery::before { content: "\f188"; } +.bi-bell-fill::before { content: "\f189"; } +.bi-bell::before { content: "\f18a"; } +.bi-bezier::before { content: "\f18b"; } +.bi-bezier2::before { content: "\f18c"; } +.bi-bicycle::before { content: "\f18d"; } +.bi-binoculars-fill::before { content: "\f18e"; } +.bi-binoculars::before { content: "\f18f"; } +.bi-blockquote-left::before { content: "\f190"; } +.bi-blockquote-right::before { content: "\f191"; } +.bi-book-fill::before { content: "\f192"; } +.bi-book-half::before { content: "\f193"; } +.bi-book::before { content: "\f194"; } +.bi-bookmark-check-fill::before { content: "\f195"; } +.bi-bookmark-check::before { content: "\f196"; } +.bi-bookmark-dash-fill::before { content: "\f197"; } +.bi-bookmark-dash::before { content: "\f198"; } +.bi-bookmark-fill::before { content: "\f199"; } +.bi-bookmark-heart-fill::before { content: "\f19a"; } +.bi-bookmark-heart::before { content: "\f19b"; } +.bi-bookmark-plus-fill::before { content: "\f19c"; } +.bi-bookmark-plus::before { content: "\f19d"; } +.bi-bookmark-star-fill::before { content: "\f19e"; } +.bi-bookmark-star::before { content: "\f19f"; } +.bi-bookmark-x-fill::before { content: "\f1a0"; } +.bi-bookmark-x::before { content: "\f1a1"; } +.bi-bookmark::before { content: "\f1a2"; } +.bi-bookmarks-fill::before { content: "\f1a3"; } +.bi-bookmarks::before { content: "\f1a4"; } +.bi-bookshelf::before { content: "\f1a5"; } +.bi-bootstrap-fill::before { content: "\f1a6"; } +.bi-bootstrap-reboot::before { content: "\f1a7"; } +.bi-bootstrap::before { content: "\f1a8"; } +.bi-border-all::before { content: "\f1a9"; } +.bi-border-bottom::before { content: "\f1aa"; } +.bi-border-center::before { content: "\f1ab"; } +.bi-border-inner::before { content: "\f1ac"; } +.bi-border-left::before { content: "\f1ad"; } +.bi-border-middle::before { content: "\f1ae"; } +.bi-border-outer::before { content: "\f1af"; } +.bi-border-right::before { content: "\f1b0"; } +.bi-border-style::before { content: "\f1b1"; } +.bi-border-top::before { content: "\f1b2"; } +.bi-border-width::before { content: "\f1b3"; } +.bi-border::before { content: "\f1b4"; } +.bi-bounding-box-circles::before { content: "\f1b5"; } +.bi-bounding-box::before { content: "\f1b6"; } +.bi-box-arrow-down-left::before { content: "\f1b7"; } +.bi-box-arrow-down-right::before { content: "\f1b8"; } +.bi-box-arrow-down::before { content: "\f1b9"; } +.bi-box-arrow-in-down-left::before { content: "\f1ba"; } +.bi-box-arrow-in-down-right::before { content: "\f1bb"; } +.bi-box-arrow-in-down::before { content: "\f1bc"; } +.bi-box-arrow-in-left::before { content: "\f1bd"; } +.bi-box-arrow-in-right::before { content: "\f1be"; } +.bi-box-arrow-in-up-left::before { content: "\f1bf"; } +.bi-box-arrow-in-up-right::before { content: "\f1c0"; } +.bi-box-arrow-in-up::before { content: "\f1c1"; } +.bi-box-arrow-left::before { content: "\f1c2"; } +.bi-box-arrow-right::before { content: "\f1c3"; } +.bi-box-arrow-up-left::before { content: "\f1c4"; } +.bi-box-arrow-up-right::before { content: "\f1c5"; } +.bi-box-arrow-up::before { content: "\f1c6"; } +.bi-box-seam::before { content: "\f1c7"; } +.bi-box::before { content: "\f1c8"; } +.bi-braces::before { content: "\f1c9"; } +.bi-bricks::before { content: "\f1ca"; } +.bi-briefcase-fill::before { content: "\f1cb"; } +.bi-briefcase::before { content: "\f1cc"; } +.bi-brightness-alt-high-fill::before { content: "\f1cd"; } +.bi-brightness-alt-high::before { content: "\f1ce"; } +.bi-brightness-alt-low-fill::before { content: "\f1cf"; } +.bi-brightness-alt-low::before { content: "\f1d0"; } +.bi-brightness-high-fill::before { content: "\f1d1"; } +.bi-brightness-high::before { content: "\f1d2"; } +.bi-brightness-low-fill::before { content: "\f1d3"; } +.bi-brightness-low::before { content: "\f1d4"; } +.bi-broadcast-pin::before { content: "\f1d5"; } +.bi-broadcast::before { content: "\f1d6"; } +.bi-brush-fill::before { content: "\f1d7"; } +.bi-brush::before { content: "\f1d8"; } +.bi-bucket-fill::before { content: "\f1d9"; } +.bi-bucket::before { content: "\f1da"; } +.bi-bug-fill::before { content: "\f1db"; } +.bi-bug::before { content: "\f1dc"; } +.bi-building::before { content: "\f1dd"; } +.bi-bullseye::before { content: "\f1de"; } +.bi-calculator-fill::before { content: "\f1df"; } +.bi-calculator::before { content: "\f1e0"; } +.bi-calendar-check-fill::before { content: "\f1e1"; } +.bi-calendar-check::before { content: "\f1e2"; } +.bi-calendar-date-fill::before { content: "\f1e3"; } +.bi-calendar-date::before { content: "\f1e4"; } +.bi-calendar-day-fill::before { content: "\f1e5"; } +.bi-calendar-day::before { content: "\f1e6"; } +.bi-calendar-event-fill::before { content: "\f1e7"; } +.bi-calendar-event::before { content: "\f1e8"; } +.bi-calendar-fill::before { content: "\f1e9"; } +.bi-calendar-minus-fill::before { content: "\f1ea"; } +.bi-calendar-minus::before { content: "\f1eb"; } +.bi-calendar-month-fill::before { content: "\f1ec"; } +.bi-calendar-month::before { content: "\f1ed"; } +.bi-calendar-plus-fill::before { content: "\f1ee"; } +.bi-calendar-plus::before { content: "\f1ef"; } +.bi-calendar-range-fill::before { content: "\f1f0"; } +.bi-calendar-range::before { content: "\f1f1"; } +.bi-calendar-week-fill::before { content: "\f1f2"; } +.bi-calendar-week::before { content: "\f1f3"; } +.bi-calendar-x-fill::before { content: "\f1f4"; } +.bi-calendar-x::before { content: "\f1f5"; } +.bi-calendar::before { content: "\f1f6"; } +.bi-calendar2-check-fill::before { content: "\f1f7"; } +.bi-calendar2-check::before { content: "\f1f8"; } +.bi-calendar2-date-fill::before { content: "\f1f9"; } +.bi-calendar2-date::before { content: "\f1fa"; } +.bi-calendar2-day-fill::before { content: "\f1fb"; } +.bi-calendar2-day::before { content: "\f1fc"; } +.bi-calendar2-event-fill::before { content: "\f1fd"; } +.bi-calendar2-event::before { content: "\f1fe"; } +.bi-calendar2-fill::before { content: "\f1ff"; } +.bi-calendar2-minus-fill::before { content: "\f200"; } +.bi-calendar2-minus::before { content: "\f201"; } +.bi-calendar2-month-fill::before { content: "\f202"; } +.bi-calendar2-month::before { content: "\f203"; } +.bi-calendar2-plus-fill::before { content: "\f204"; } +.bi-calendar2-plus::before { content: "\f205"; } +.bi-calendar2-range-fill::before { content: "\f206"; } +.bi-calendar2-range::before { content: "\f207"; } +.bi-calendar2-week-fill::before { content: "\f208"; } +.bi-calendar2-week::before { content: "\f209"; } +.bi-calendar2-x-fill::before { content: "\f20a"; } +.bi-calendar2-x::before { content: "\f20b"; } +.bi-calendar2::before { content: "\f20c"; } +.bi-calendar3-event-fill::before { content: "\f20d"; } +.bi-calendar3-event::before { content: "\f20e"; } +.bi-calendar3-fill::before { content: "\f20f"; } +.bi-calendar3-range-fill::before { content: "\f210"; } +.bi-calendar3-range::before { content: "\f211"; } +.bi-calendar3-week-fill::before { content: "\f212"; } +.bi-calendar3-week::before { content: "\f213"; } +.bi-calendar3::before { content: "\f214"; } +.bi-calendar4-event::before { content: "\f215"; } +.bi-calendar4-range::before { content: "\f216"; } +.bi-calendar4-week::before { content: "\f217"; } +.bi-calendar4::before { content: "\f218"; } +.bi-camera-fill::before { content: "\f219"; } +.bi-camera-reels-fill::before { content: "\f21a"; } +.bi-camera-reels::before { content: "\f21b"; } +.bi-camera-video-fill::before { content: "\f21c"; } +.bi-camera-video-off-fill::before { content: "\f21d"; } +.bi-camera-video-off::before { content: "\f21e"; } +.bi-camera-video::before { content: "\f21f"; } +.bi-camera::before { content: "\f220"; } +.bi-camera2::before { content: "\f221"; } +.bi-capslock-fill::before { content: "\f222"; } +.bi-capslock::before { content: "\f223"; } +.bi-card-checklist::before { content: "\f224"; } +.bi-card-heading::before { content: "\f225"; } +.bi-card-image::before { content: "\f226"; } +.bi-card-list::before { content: "\f227"; } +.bi-card-text::before { content: "\f228"; } +.bi-caret-down-fill::before { content: "\f229"; } +.bi-caret-down-square-fill::before { content: "\f22a"; } +.bi-caret-down-square::before { content: "\f22b"; } +.bi-caret-down::before { content: "\f22c"; } +.bi-caret-left-fill::before { content: "\f22d"; } +.bi-caret-left-square-fill::before { content: "\f22e"; } +.bi-caret-left-square::before { content: "\f22f"; } +.bi-caret-left::before { content: "\f230"; } +.bi-caret-right-fill::before { content: "\f231"; } +.bi-caret-right-square-fill::before { content: "\f232"; } +.bi-caret-right-square::before { content: "\f233"; } +.bi-caret-right::before { content: "\f234"; } +.bi-caret-up-fill::before { content: "\f235"; } +.bi-caret-up-square-fill::before { content: "\f236"; } +.bi-caret-up-square::before { content: "\f237"; } +.bi-caret-up::before { content: "\f238"; } +.bi-cart-check-fill::before { content: "\f239"; } +.bi-cart-check::before { content: "\f23a"; } +.bi-cart-dash-fill::before { content: "\f23b"; } +.bi-cart-dash::before { content: "\f23c"; } +.bi-cart-fill::before { content: "\f23d"; } +.bi-cart-plus-fill::before { content: "\f23e"; } +.bi-cart-plus::before { content: "\f23f"; } +.bi-cart-x-fill::before { content: "\f240"; } +.bi-cart-x::before { content: "\f241"; } +.bi-cart::before { content: "\f242"; } +.bi-cart2::before { content: "\f243"; } +.bi-cart3::before { content: "\f244"; } +.bi-cart4::before { content: "\f245"; } +.bi-cash-stack::before { content: "\f246"; } +.bi-cash::before { content: "\f247"; } +.bi-cast::before { content: "\f248"; } +.bi-chat-dots-fill::before { content: "\f249"; } +.bi-chat-dots::before { content: "\f24a"; } +.bi-chat-fill::before { content: "\f24b"; } +.bi-chat-left-dots-fill::before { content: "\f24c"; } +.bi-chat-left-dots::before { content: "\f24d"; } +.bi-chat-left-fill::before { content: "\f24e"; } +.bi-chat-left-quote-fill::before { content: "\f24f"; } +.bi-chat-left-quote::before { content: "\f250"; } +.bi-chat-left-text-fill::before { content: "\f251"; } +.bi-chat-left-text::before { content: "\f252"; } +.bi-chat-left::before { content: "\f253"; } +.bi-chat-quote-fill::before { content: "\f254"; } +.bi-chat-quote::before { content: "\f255"; } +.bi-chat-right-dots-fill::before { content: "\f256"; } +.bi-chat-right-dots::before { content: "\f257"; } +.bi-chat-right-fill::before { content: "\f258"; } +.bi-chat-right-quote-fill::before { content: "\f259"; } +.bi-chat-right-quote::before { content: "\f25a"; } +.bi-chat-right-text-fill::before { content: "\f25b"; } +.bi-chat-right-text::before { content: "\f25c"; } +.bi-chat-right::before { content: "\f25d"; } +.bi-chat-square-dots-fill::before { content: "\f25e"; } +.bi-chat-square-dots::before { content: "\f25f"; } +.bi-chat-square-fill::before { content: "\f260"; } +.bi-chat-square-quote-fill::before { content: "\f261"; } +.bi-chat-square-quote::before { content: "\f262"; } +.bi-chat-square-text-fill::before { content: "\f263"; } +.bi-chat-square-text::before { content: "\f264"; } +.bi-chat-square::before { content: "\f265"; } +.bi-chat-text-fill::before { content: "\f266"; } +.bi-chat-text::before { content: "\f267"; } +.bi-chat::before { content: "\f268"; } +.bi-check-all::before { content: "\f269"; } +.bi-check-circle-fill::before { content: "\f26a"; } +.bi-check-circle::before { content: "\f26b"; } +.bi-check-square-fill::before { content: "\f26c"; } +.bi-check-square::before { content: "\f26d"; } +.bi-check::before { content: "\f26e"; } +.bi-check2-all::before { content: "\f26f"; } +.bi-check2-circle::before { content: "\f270"; } +.bi-check2-square::before { content: "\f271"; } +.bi-check2::before { content: "\f272"; } +.bi-chevron-bar-contract::before { content: "\f273"; } +.bi-chevron-bar-down::before { content: "\f274"; } +.bi-chevron-bar-expand::before { content: "\f275"; } +.bi-chevron-bar-left::before { content: "\f276"; } +.bi-chevron-bar-right::before { content: "\f277"; } +.bi-chevron-bar-up::before { content: "\f278"; } +.bi-chevron-compact-down::before { content: "\f279"; } +.bi-chevron-compact-left::before { content: "\f27a"; } +.bi-chevron-compact-right::before { content: "\f27b"; } +.bi-chevron-compact-up::before { content: "\f27c"; } +.bi-chevron-contract::before { content: "\f27d"; } +.bi-chevron-double-down::before { content: "\f27e"; } +.bi-chevron-double-left::before { content: "\f27f"; } +.bi-chevron-double-right::before { content: "\f280"; } +.bi-chevron-double-up::before { content: "\f281"; } +.bi-chevron-down::before { content: "\f282"; } +.bi-chevron-expand::before { content: "\f283"; } +.bi-chevron-left::before { content: "\f284"; } +.bi-chevron-right::before { content: "\f285"; } +.bi-chevron-up::before { content: "\f286"; } +.bi-circle-fill::before { content: "\f287"; } +.bi-circle-half::before { content: "\f288"; } +.bi-circle-square::before { content: "\f289"; } +.bi-circle::before { content: "\f28a"; } +.bi-clipboard-check::before { content: "\f28b"; } +.bi-clipboard-data::before { content: "\f28c"; } +.bi-clipboard-minus::before { content: "\f28d"; } +.bi-clipboard-plus::before { content: "\f28e"; } +.bi-clipboard-x::before { content: "\f28f"; } +.bi-clipboard::before { content: "\f290"; } +.bi-clock-fill::before { content: "\f291"; } +.bi-clock-history::before { content: "\f292"; } +.bi-clock::before { content: "\f293"; } +.bi-cloud-arrow-down-fill::before { content: "\f294"; } +.bi-cloud-arrow-down::before { content: "\f295"; } +.bi-cloud-arrow-up-fill::before { content: "\f296"; } +.bi-cloud-arrow-up::before { content: "\f297"; } +.bi-cloud-check-fill::before { content: "\f298"; } +.bi-cloud-check::before { content: "\f299"; } +.bi-cloud-download-fill::before { content: "\f29a"; } +.bi-cloud-download::before { content: "\f29b"; } +.bi-cloud-drizzle-fill::before { content: "\f29c"; } +.bi-cloud-drizzle::before { content: "\f29d"; } +.bi-cloud-fill::before { content: "\f29e"; } +.bi-cloud-fog-fill::before { content: "\f29f"; } +.bi-cloud-fog::before { content: "\f2a0"; } +.bi-cloud-fog2-fill::before { content: "\f2a1"; } +.bi-cloud-fog2::before { content: "\f2a2"; } +.bi-cloud-hail-fill::before { content: "\f2a3"; } +.bi-cloud-hail::before { content: "\f2a4"; } +.bi-cloud-haze-1::before { content: "\f2a5"; } +.bi-cloud-haze-fill::before { content: "\f2a6"; } +.bi-cloud-haze::before { content: "\f2a7"; } +.bi-cloud-haze2-fill::before { content: "\f2a8"; } +.bi-cloud-lightning-fill::before { content: "\f2a9"; } +.bi-cloud-lightning-rain-fill::before { content: "\f2aa"; } +.bi-cloud-lightning-rain::before { content: "\f2ab"; } +.bi-cloud-lightning::before { content: "\f2ac"; } +.bi-cloud-minus-fill::before { content: "\f2ad"; } +.bi-cloud-minus::before { content: "\f2ae"; } +.bi-cloud-moon-fill::before { content: "\f2af"; } +.bi-cloud-moon::before { content: "\f2b0"; } +.bi-cloud-plus-fill::before { content: "\f2b1"; } +.bi-cloud-plus::before { content: "\f2b2"; } +.bi-cloud-rain-fill::before { content: "\f2b3"; } +.bi-cloud-rain-heavy-fill::before { content: "\f2b4"; } +.bi-cloud-rain-heavy::before { content: "\f2b5"; } +.bi-cloud-rain::before { content: "\f2b6"; } +.bi-cloud-slash-fill::before { content: "\f2b7"; } +.bi-cloud-slash::before { content: "\f2b8"; } +.bi-cloud-sleet-fill::before { content: "\f2b9"; } +.bi-cloud-sleet::before { content: "\f2ba"; } +.bi-cloud-snow-fill::before { content: "\f2bb"; } +.bi-cloud-snow::before { content: "\f2bc"; } +.bi-cloud-sun-fill::before { content: "\f2bd"; } +.bi-cloud-sun::before { content: "\f2be"; } +.bi-cloud-upload-fill::before { content: "\f2bf"; } +.bi-cloud-upload::before { content: "\f2c0"; } +.bi-cloud::before { content: "\f2c1"; } +.bi-clouds-fill::before { content: "\f2c2"; } +.bi-clouds::before { content: "\f2c3"; } +.bi-cloudy-fill::before { content: "\f2c4"; } +.bi-cloudy::before { content: "\f2c5"; } +.bi-code-slash::before { content: "\f2c6"; } +.bi-code-square::before { content: "\f2c7"; } +.bi-code::before { content: "\f2c8"; } +.bi-collection-fill::before { content: "\f2c9"; } +.bi-collection-play-fill::before { content: "\f2ca"; } +.bi-collection-play::before { content: "\f2cb"; } +.bi-collection::before { content: "\f2cc"; } +.bi-columns-gap::before { content: "\f2cd"; } +.bi-columns::before { content: "\f2ce"; } +.bi-command::before { content: "\f2cf"; } +.bi-compass-fill::before { content: "\f2d0"; } +.bi-compass::before { content: "\f2d1"; } +.bi-cone-striped::before { content: "\f2d2"; } +.bi-cone::before { content: "\f2d3"; } +.bi-controller::before { content: "\f2d4"; } +.bi-cpu-fill::before { content: "\f2d5"; } +.bi-cpu::before { content: "\f2d6"; } +.bi-credit-card-2-back-fill::before { content: "\f2d7"; } +.bi-credit-card-2-back::before { content: "\f2d8"; } +.bi-credit-card-2-front-fill::before { content: "\f2d9"; } +.bi-credit-card-2-front::before { content: "\f2da"; } +.bi-credit-card-fill::before { content: "\f2db"; } +.bi-credit-card::before { content: "\f2dc"; } +.bi-crop::before { content: "\f2dd"; } +.bi-cup-fill::before { content: "\f2de"; } +.bi-cup-straw::before { content: "\f2df"; } +.bi-cup::before { content: "\f2e0"; } +.bi-cursor-fill::before { content: "\f2e1"; } +.bi-cursor-text::before { content: "\f2e2"; } +.bi-cursor::before { content: "\f2e3"; } +.bi-dash-circle-dotted::before { content: "\f2e4"; } +.bi-dash-circle-fill::before { content: "\f2e5"; } +.bi-dash-circle::before { content: "\f2e6"; } +.bi-dash-square-dotted::before { content: "\f2e7"; } +.bi-dash-square-fill::before { content: "\f2e8"; } +.bi-dash-square::before { content: "\f2e9"; } +.bi-dash::before { content: "\f2ea"; } +.bi-diagram-2-fill::before { content: "\f2eb"; } +.bi-diagram-2::before { content: "\f2ec"; } +.bi-diagram-3-fill::before { content: "\f2ed"; } +.bi-diagram-3::before { content: "\f2ee"; } +.bi-diamond-fill::before { content: "\f2ef"; } +.bi-diamond-half::before { content: "\f2f0"; } +.bi-diamond::before { content: "\f2f1"; } +.bi-dice-1-fill::before { content: "\f2f2"; } +.bi-dice-1::before { content: "\f2f3"; } +.bi-dice-2-fill::before { content: "\f2f4"; } +.bi-dice-2::before { content: "\f2f5"; } +.bi-dice-3-fill::before { content: "\f2f6"; } +.bi-dice-3::before { content: "\f2f7"; } +.bi-dice-4-fill::before { content: "\f2f8"; } +.bi-dice-4::before { content: "\f2f9"; } +.bi-dice-5-fill::before { content: "\f2fa"; } +.bi-dice-5::before { content: "\f2fb"; } +.bi-dice-6-fill::before { content: "\f2fc"; } +.bi-dice-6::before { content: "\f2fd"; } +.bi-disc-fill::before { content: "\f2fe"; } +.bi-disc::before { content: "\f2ff"; } +.bi-discord::before { content: "\f300"; } +.bi-display-fill::before { content: "\f301"; } +.bi-display::before { content: "\f302"; } +.bi-distribute-horizontal::before { content: "\f303"; } +.bi-distribute-vertical::before { content: "\f304"; } +.bi-door-closed-fill::before { content: "\f305"; } +.bi-door-closed::before { content: "\f306"; } +.bi-door-open-fill::before { content: "\f307"; } +.bi-door-open::before { content: "\f308"; } +.bi-dot::before { content: "\f309"; } +.bi-download::before { content: "\f30a"; } +.bi-droplet-fill::before { content: "\f30b"; } +.bi-droplet-half::before { content: "\f30c"; } +.bi-droplet::before { content: "\f30d"; } +.bi-earbuds::before { content: "\f30e"; } +.bi-easel-fill::before { content: "\f30f"; } +.bi-easel::before { content: "\f310"; } +.bi-egg-fill::before { content: "\f311"; } +.bi-egg-fried::before { content: "\f312"; } +.bi-egg::before { content: "\f313"; } +.bi-eject-fill::before { content: "\f314"; } +.bi-eject::before { content: "\f315"; } +.bi-emoji-angry-fill::before { content: "\f316"; } +.bi-emoji-angry::before { content: "\f317"; } +.bi-emoji-dizzy-fill::before { content: "\f318"; } +.bi-emoji-dizzy::before { content: "\f319"; } +.bi-emoji-expressionless-fill::before { content: "\f31a"; } +.bi-emoji-expressionless::before { content: "\f31b"; } +.bi-emoji-frown-fill::before { content: "\f31c"; } +.bi-emoji-frown::before { content: "\f31d"; } +.bi-emoji-heart-eyes-fill::before { content: "\f31e"; } +.bi-emoji-heart-eyes::before { content: "\f31f"; } +.bi-emoji-laughing-fill::before { content: "\f320"; } +.bi-emoji-laughing::before { content: "\f321"; } +.bi-emoji-neutral-fill::before { content: "\f322"; } +.bi-emoji-neutral::before { content: "\f323"; } +.bi-emoji-smile-fill::before { content: "\f324"; } +.bi-emoji-smile-upside-down-fill::before { content: "\f325"; } +.bi-emoji-smile-upside-down::before { content: "\f326"; } +.bi-emoji-smile::before { content: "\f327"; } +.bi-emoji-sunglasses-fill::before { content: "\f328"; } +.bi-emoji-sunglasses::before { content: "\f329"; } +.bi-emoji-wink-fill::before { content: "\f32a"; } +.bi-emoji-wink::before { content: "\f32b"; } +.bi-envelope-fill::before { content: "\f32c"; } +.bi-envelope-open-fill::before { content: "\f32d"; } +.bi-envelope-open::before { content: "\f32e"; } +.bi-envelope::before { content: "\f32f"; } +.bi-eraser-fill::before { content: "\f330"; } +.bi-eraser::before { content: "\f331"; } +.bi-exclamation-circle-fill::before { content: "\f332"; } +.bi-exclamation-circle::before { content: "\f333"; } +.bi-exclamation-diamond-fill::before { content: "\f334"; } +.bi-exclamation-diamond::before { content: "\f335"; } +.bi-exclamation-octagon-fill::before { content: "\f336"; } +.bi-exclamation-octagon::before { content: "\f337"; } +.bi-exclamation-square-fill::before { content: "\f338"; } +.bi-exclamation-square::before { content: "\f339"; } +.bi-exclamation-triangle-fill::before { content: "\f33a"; } +.bi-exclamation-triangle::before { content: "\f33b"; } +.bi-exclamation::before { content: "\f33c"; } +.bi-exclude::before { content: "\f33d"; } +.bi-eye-fill::before { content: "\f33e"; } +.bi-eye-slash-fill::before { content: "\f33f"; } +.bi-eye-slash::before { content: "\f340"; } +.bi-eye::before { content: "\f341"; } +.bi-eyedropper::before { content: "\f342"; } +.bi-eyeglasses::before { content: "\f343"; } +.bi-facebook::before { content: "\f344"; } +.bi-file-arrow-down-fill::before { content: "\f345"; } +.bi-file-arrow-down::before { content: "\f346"; } +.bi-file-arrow-up-fill::before { content: "\f347"; } +.bi-file-arrow-up::before { content: "\f348"; } +.bi-file-bar-graph-fill::before { content: "\f349"; } +.bi-file-bar-graph::before { content: "\f34a"; } +.bi-file-binary-fill::before { content: "\f34b"; } +.bi-file-binary::before { content: "\f34c"; } +.bi-file-break-fill::before { content: "\f34d"; } +.bi-file-break::before { content: "\f34e"; } +.bi-file-check-fill::before { content: "\f34f"; } +.bi-file-check::before { content: "\f350"; } +.bi-file-code-fill::before { content: "\f351"; } +.bi-file-code::before { content: "\f352"; } +.bi-file-diff-fill::before { content: "\f353"; } +.bi-file-diff::before { content: "\f354"; } +.bi-file-earmark-arrow-down-fill::before { content: "\f355"; } +.bi-file-earmark-arrow-down::before { content: "\f356"; } +.bi-file-earmark-arrow-up-fill::before { content: "\f357"; } +.bi-file-earmark-arrow-up::before { content: "\f358"; } +.bi-file-earmark-bar-graph-fill::before { content: "\f359"; } +.bi-file-earmark-bar-graph::before { content: "\f35a"; } +.bi-file-earmark-binary-fill::before { content: "\f35b"; } +.bi-file-earmark-binary::before { content: "\f35c"; } +.bi-file-earmark-break-fill::before { content: "\f35d"; } +.bi-file-earmark-break::before { content: "\f35e"; } +.bi-file-earmark-check-fill::before { content: "\f35f"; } +.bi-file-earmark-check::before { content: "\f360"; } +.bi-file-earmark-code-fill::before { content: "\f361"; } +.bi-file-earmark-code::before { content: "\f362"; } +.bi-file-earmark-diff-fill::before { content: "\f363"; } +.bi-file-earmark-diff::before { content: "\f364"; } +.bi-file-earmark-easel-fill::before { content: "\f365"; } +.bi-file-earmark-easel::before { content: "\f366"; } +.bi-file-earmark-excel-fill::before { content: "\f367"; } +.bi-file-earmark-excel::before { content: "\f368"; } +.bi-file-earmark-fill::before { content: "\f369"; } +.bi-file-earmark-font-fill::before { content: "\f36a"; } +.bi-file-earmark-font::before { content: "\f36b"; } +.bi-file-earmark-image-fill::before { content: "\f36c"; } +.bi-file-earmark-image::before { content: "\f36d"; } +.bi-file-earmark-lock-fill::before { content: "\f36e"; } +.bi-file-earmark-lock::before { content: "\f36f"; } +.bi-file-earmark-lock2-fill::before { content: "\f370"; } +.bi-file-earmark-lock2::before { content: "\f371"; } +.bi-file-earmark-medical-fill::before { content: "\f372"; } +.bi-file-earmark-medical::before { content: "\f373"; } +.bi-file-earmark-minus-fill::before { content: "\f374"; } +.bi-file-earmark-minus::before { content: "\f375"; } +.bi-file-earmark-music-fill::before { content: "\f376"; } +.bi-file-earmark-music::before { content: "\f377"; } +.bi-file-earmark-person-fill::before { content: "\f378"; } +.bi-file-earmark-person::before { content: "\f379"; } +.bi-file-earmark-play-fill::before { content: "\f37a"; } +.bi-file-earmark-play::before { content: "\f37b"; } +.bi-file-earmark-plus-fill::before { content: "\f37c"; } +.bi-file-earmark-plus::before { content: "\f37d"; } +.bi-file-earmark-post-fill::before { content: "\f37e"; } +.bi-file-earmark-post::before { content: "\f37f"; } +.bi-file-earmark-ppt-fill::before { content: "\f380"; } +.bi-file-earmark-ppt::before { content: "\f381"; } +.bi-file-earmark-richtext-fill::before { content: "\f382"; } +.bi-file-earmark-richtext::before { content: "\f383"; } +.bi-file-earmark-ruled-fill::before { content: "\f384"; } +.bi-file-earmark-ruled::before { content: "\f385"; } +.bi-file-earmark-slides-fill::before { content: "\f386"; } +.bi-file-earmark-slides::before { content: "\f387"; } +.bi-file-earmark-spreadsheet-fill::before { content: "\f388"; } +.bi-file-earmark-spreadsheet::before { content: "\f389"; } +.bi-file-earmark-text-fill::before { content: "\f38a"; } +.bi-file-earmark-text::before { content: "\f38b"; } +.bi-file-earmark-word-fill::before { content: "\f38c"; } +.bi-file-earmark-word::before { content: "\f38d"; } +.bi-file-earmark-x-fill::before { content: "\f38e"; } +.bi-file-earmark-x::before { content: "\f38f"; } +.bi-file-earmark-zip-fill::before { content: "\f390"; } +.bi-file-earmark-zip::before { content: "\f391"; } +.bi-file-earmark::before { content: "\f392"; } +.bi-file-easel-fill::before { content: "\f393"; } +.bi-file-easel::before { content: "\f394"; } +.bi-file-excel-fill::before { content: "\f395"; } +.bi-file-excel::before { content: "\f396"; } +.bi-file-fill::before { content: "\f397"; } +.bi-file-font-fill::before { content: "\f398"; } +.bi-file-font::before { content: "\f399"; } +.bi-file-image-fill::before { content: "\f39a"; } +.bi-file-image::before { content: "\f39b"; } +.bi-file-lock-fill::before { content: "\f39c"; } +.bi-file-lock::before { content: "\f39d"; } +.bi-file-lock2-fill::before { content: "\f39e"; } +.bi-file-lock2::before { content: "\f39f"; } +.bi-file-medical-fill::before { content: "\f3a0"; } +.bi-file-medical::before { content: "\f3a1"; } +.bi-file-minus-fill::before { content: "\f3a2"; } +.bi-file-minus::before { content: "\f3a3"; } +.bi-file-music-fill::before { content: "\f3a4"; } +.bi-file-music::before { content: "\f3a5"; } +.bi-file-person-fill::before { content: "\f3a6"; } +.bi-file-person::before { content: "\f3a7"; } +.bi-file-play-fill::before { content: "\f3a8"; } +.bi-file-play::before { content: "\f3a9"; } +.bi-file-plus-fill::before { content: "\f3aa"; } +.bi-file-plus::before { content: "\f3ab"; } +.bi-file-post-fill::before { content: "\f3ac"; } +.bi-file-post::before { content: "\f3ad"; } +.bi-file-ppt-fill::before { content: "\f3ae"; } +.bi-file-ppt::before { content: "\f3af"; } +.bi-file-richtext-fill::before { content: "\f3b0"; } +.bi-file-richtext::before { content: "\f3b1"; } +.bi-file-ruled-fill::before { content: "\f3b2"; } +.bi-file-ruled::before { content: "\f3b3"; } +.bi-file-slides-fill::before { content: "\f3b4"; } +.bi-file-slides::before { content: "\f3b5"; } +.bi-file-spreadsheet-fill::before { content: "\f3b6"; } +.bi-file-spreadsheet::before { content: "\f3b7"; } +.bi-file-text-fill::before { content: "\f3b8"; } +.bi-file-text::before { content: "\f3b9"; } +.bi-file-word-fill::before { content: "\f3ba"; } +.bi-file-word::before { content: "\f3bb"; } +.bi-file-x-fill::before { content: "\f3bc"; } +.bi-file-x::before { content: "\f3bd"; } +.bi-file-zip-fill::before { content: "\f3be"; } +.bi-file-zip::before { content: "\f3bf"; } +.bi-file::before { content: "\f3c0"; } +.bi-files-alt::before { content: "\f3c1"; } +.bi-files::before { content: "\f3c2"; } +.bi-film::before { content: "\f3c3"; } +.bi-filter-circle-fill::before { content: "\f3c4"; } +.bi-filter-circle::before { content: "\f3c5"; } +.bi-filter-left::before { content: "\f3c6"; } +.bi-filter-right::before { content: "\f3c7"; } +.bi-filter-square-fill::before { content: "\f3c8"; } +.bi-filter-square::before { content: "\f3c9"; } +.bi-filter::before { content: "\f3ca"; } +.bi-flag-fill::before { content: "\f3cb"; } +.bi-flag::before { content: "\f3cc"; } +.bi-flower1::before { content: "\f3cd"; } +.bi-flower2::before { content: "\f3ce"; } +.bi-flower3::before { content: "\f3cf"; } +.bi-folder-check::before { content: "\f3d0"; } +.bi-folder-fill::before { content: "\f3d1"; } +.bi-folder-minus::before { content: "\f3d2"; } +.bi-folder-plus::before { content: "\f3d3"; } +.bi-folder-symlink-fill::before { content: "\f3d4"; } +.bi-folder-symlink::before { content: "\f3d5"; } +.bi-folder-x::before { content: "\f3d6"; } +.bi-folder::before { content: "\f3d7"; } +.bi-folder2-open::before { content: "\f3d8"; } +.bi-folder2::before { content: "\f3d9"; } +.bi-fonts::before { content: "\f3da"; } +.bi-forward-fill::before { content: "\f3db"; } +.bi-forward::before { content: "\f3dc"; } +.bi-front::before { content: "\f3dd"; } +.bi-fullscreen-exit::before { content: "\f3de"; } +.bi-fullscreen::before { content: "\f3df"; } +.bi-funnel-fill::before { content: "\f3e0"; } +.bi-funnel::before { content: "\f3e1"; } +.bi-gear-fill::before { content: "\f3e2"; } +.bi-gear-wide-connected::before { content: "\f3e3"; } +.bi-gear-wide::before { content: "\f3e4"; } +.bi-gear::before { content: "\f3e5"; } +.bi-gem::before { content: "\f3e6"; } +.bi-geo-alt-fill::before { content: "\f3e7"; } +.bi-geo-alt::before { content: "\f3e8"; } +.bi-geo-fill::before { content: "\f3e9"; } +.bi-geo::before { content: "\f3ea"; } +.bi-gift-fill::before { content: "\f3eb"; } +.bi-gift::before { content: "\f3ec"; } +.bi-github::before { content: "\f3ed"; } +.bi-globe::before { content: "\f3ee"; } +.bi-globe2::before { content: "\f3ef"; } +.bi-google::before { content: "\f3f0"; } +.bi-graph-down::before { content: "\f3f1"; } +.bi-graph-up::before { content: "\f3f2"; } +.bi-grid-1x2-fill::before { content: "\f3f3"; } +.bi-grid-1x2::before { content: "\f3f4"; } +.bi-grid-3x2-gap-fill::before { content: "\f3f5"; } +.bi-grid-3x2-gap::before { content: "\f3f6"; } +.bi-grid-3x2::before { content: "\f3f7"; } +.bi-grid-3x3-gap-fill::before { content: "\f3f8"; } +.bi-grid-3x3-gap::before { content: "\f3f9"; } +.bi-grid-3x3::before { content: "\f3fa"; } +.bi-grid-fill::before { content: "\f3fb"; } +.bi-grid::before { content: "\f3fc"; } +.bi-grip-horizontal::before { content: "\f3fd"; } +.bi-grip-vertical::before { content: "\f3fe"; } +.bi-hammer::before { content: "\f3ff"; } +.bi-hand-index-fill::before { content: "\f400"; } +.bi-hand-index-thumb-fill::before { content: "\f401"; } +.bi-hand-index-thumb::before { content: "\f402"; } +.bi-hand-index::before { content: "\f403"; } +.bi-hand-thumbs-down-fill::before { content: "\f404"; } +.bi-hand-thumbs-down::before { content: "\f405"; } +.bi-hand-thumbs-up-fill::before { content: "\f406"; } +.bi-hand-thumbs-up::before { content: "\f407"; } +.bi-handbag-fill::before { content: "\f408"; } +.bi-handbag::before { content: "\f409"; } +.bi-hash::before { content: "\f40a"; } +.bi-hdd-fill::before { content: "\f40b"; } +.bi-hdd-network-fill::before { content: "\f40c"; } +.bi-hdd-network::before { content: "\f40d"; } +.bi-hdd-rack-fill::before { content: "\f40e"; } +.bi-hdd-rack::before { content: "\f40f"; } +.bi-hdd-stack-fill::before { content: "\f410"; } +.bi-hdd-stack::before { content: "\f411"; } +.bi-hdd::before { content: "\f412"; } +.bi-headphones::before { content: "\f413"; } +.bi-headset::before { content: "\f414"; } +.bi-heart-fill::before { content: "\f415"; } +.bi-heart-half::before { content: "\f416"; } +.bi-heart::before { content: "\f417"; } +.bi-heptagon-fill::before { content: "\f418"; } +.bi-heptagon-half::before { content: "\f419"; } +.bi-heptagon::before { content: "\f41a"; } +.bi-hexagon-fill::before { content: "\f41b"; } +.bi-hexagon-half::before { content: "\f41c"; } +.bi-hexagon::before { content: "\f41d"; } +.bi-hourglass-bottom::before { content: "\f41e"; } +.bi-hourglass-split::before { content: "\f41f"; } +.bi-hourglass-top::before { content: "\f420"; } +.bi-hourglass::before { content: "\f421"; } +.bi-house-door-fill::before { content: "\f422"; } +.bi-house-door::before { content: "\f423"; } +.bi-house-fill::before { content: "\f424"; } +.bi-house::before { content: "\f425"; } +.bi-hr::before { content: "\f426"; } +.bi-hurricane::before { content: "\f427"; } +.bi-image-alt::before { content: "\f428"; } +.bi-image-fill::before { content: "\f429"; } +.bi-image::before { content: "\f42a"; } +.bi-images::before { content: "\f42b"; } +.bi-inbox-fill::before { content: "\f42c"; } +.bi-inbox::before { content: "\f42d"; } +.bi-inboxes-fill::before { content: "\f42e"; } +.bi-inboxes::before { content: "\f42f"; } +.bi-info-circle-fill::before { content: "\f430"; } +.bi-info-circle::before { content: "\f431"; } +.bi-info-square-fill::before { content: "\f432"; } +.bi-info-square::before { content: "\f433"; } +.bi-info::before { content: "\f434"; } +.bi-input-cursor-text::before { content: "\f435"; } +.bi-input-cursor::before { content: "\f436"; } +.bi-instagram::before { content: "\f437"; } +.bi-intersect::before { content: "\f438"; } +.bi-journal-album::before { content: "\f439"; } +.bi-journal-arrow-down::before { content: "\f43a"; } +.bi-journal-arrow-up::before { content: "\f43b"; } +.bi-journal-bookmark-fill::before { content: "\f43c"; } +.bi-journal-bookmark::before { content: "\f43d"; } +.bi-journal-check::before { content: "\f43e"; } +.bi-journal-code::before { content: "\f43f"; } +.bi-journal-medical::before { content: "\f440"; } +.bi-journal-minus::before { content: "\f441"; } +.bi-journal-plus::before { content: "\f442"; } +.bi-journal-richtext::before { content: "\f443"; } +.bi-journal-text::before { content: "\f444"; } +.bi-journal-x::before { content: "\f445"; } +.bi-journal::before { content: "\f446"; } +.bi-journals::before { content: "\f447"; } +.bi-joystick::before { content: "\f448"; } +.bi-justify-left::before { content: "\f449"; } +.bi-justify-right::before { content: "\f44a"; } +.bi-justify::before { content: "\f44b"; } +.bi-kanban-fill::before { content: "\f44c"; } +.bi-kanban::before { content: "\f44d"; } +.bi-key-fill::before { content: "\f44e"; } +.bi-key::before { content: "\f44f"; } +.bi-keyboard-fill::before { content: "\f450"; } +.bi-keyboard::before { content: "\f451"; } +.bi-ladder::before { content: "\f452"; } +.bi-lamp-fill::before { content: "\f453"; } +.bi-lamp::before { content: "\f454"; } +.bi-laptop-fill::before { content: "\f455"; } +.bi-laptop::before { content: "\f456"; } +.bi-layer-backward::before { content: "\f457"; } +.bi-layer-forward::before { content: "\f458"; } +.bi-layers-fill::before { content: "\f459"; } +.bi-layers-half::before { content: "\f45a"; } +.bi-layers::before { content: "\f45b"; } +.bi-layout-sidebar-inset-reverse::before { content: "\f45c"; } +.bi-layout-sidebar-inset::before { content: "\f45d"; } +.bi-layout-sidebar-reverse::before { content: "\f45e"; } +.bi-layout-sidebar::before { content: "\f45f"; } +.bi-layout-split::before { content: "\f460"; } +.bi-layout-text-sidebar-reverse::before { content: "\f461"; } +.bi-layout-text-sidebar::before { content: "\f462"; } +.bi-layout-text-window-reverse::before { content: "\f463"; } +.bi-layout-text-window::before { content: "\f464"; } +.bi-layout-three-columns::before { content: "\f465"; } +.bi-layout-wtf::before { content: "\f466"; } +.bi-life-preserver::before { content: "\f467"; } +.bi-lightbulb-fill::before { content: "\f468"; } +.bi-lightbulb-off-fill::before { content: "\f469"; } +.bi-lightbulb-off::before { content: "\f46a"; } +.bi-lightbulb::before { content: "\f46b"; } +.bi-lightning-charge-fill::before { content: "\f46c"; } +.bi-lightning-charge::before { content: "\f46d"; } +.bi-lightning-fill::before { content: "\f46e"; } +.bi-lightning::before { content: "\f46f"; } +.bi-link-45deg::before { content: "\f470"; } +.bi-link::before { content: "\f471"; } +.bi-linkedin::before { content: "\f472"; } +.bi-list-check::before { content: "\f473"; } +.bi-list-nested::before { content: "\f474"; } +.bi-list-ol::before { content: "\f475"; } +.bi-list-stars::before { content: "\f476"; } +.bi-list-task::before { content: "\f477"; } +.bi-list-ul::before { content: "\f478"; } +.bi-list::before { content: "\f479"; } +.bi-lock-fill::before { content: "\f47a"; } +.bi-lock::before { content: "\f47b"; } +.bi-mailbox::before { content: "\f47c"; } +.bi-mailbox2::before { content: "\f47d"; } +.bi-map-fill::before { content: "\f47e"; } +.bi-map::before { content: "\f47f"; } +.bi-markdown-fill::before { content: "\f480"; } +.bi-markdown::before { content: "\f481"; } +.bi-mask::before { content: "\f482"; } +.bi-megaphone-fill::before { content: "\f483"; } +.bi-megaphone::before { content: "\f484"; } +.bi-menu-app-fill::before { content: "\f485"; } +.bi-menu-app::before { content: "\f486"; } +.bi-menu-button-fill::before { content: "\f487"; } +.bi-menu-button-wide-fill::before { content: "\f488"; } +.bi-menu-button-wide::before { content: "\f489"; } +.bi-menu-button::before { content: "\f48a"; } +.bi-menu-down::before { content: "\f48b"; } +.bi-menu-up::before { content: "\f48c"; } +.bi-mic-fill::before { content: "\f48d"; } +.bi-mic-mute-fill::before { content: "\f48e"; } +.bi-mic-mute::before { content: "\f48f"; } +.bi-mic::before { content: "\f490"; } +.bi-minecart-loaded::before { content: "\f491"; } +.bi-minecart::before { content: "\f492"; } +.bi-moisture::before { content: "\f493"; } +.bi-moon-fill::before { content: "\f494"; } +.bi-moon-stars-fill::before { content: "\f495"; } +.bi-moon-stars::before { content: "\f496"; } +.bi-moon::before { content: "\f497"; } +.bi-mouse-fill::before { content: "\f498"; } +.bi-mouse::before { content: "\f499"; } +.bi-mouse2-fill::before { content: "\f49a"; } +.bi-mouse2::before { content: "\f49b"; } +.bi-mouse3-fill::before { content: "\f49c"; } +.bi-mouse3::before { content: "\f49d"; } +.bi-music-note-beamed::before { content: "\f49e"; } +.bi-music-note-list::before { content: "\f49f"; } +.bi-music-note::before { content: "\f4a0"; } +.bi-music-player-fill::before { content: "\f4a1"; } +.bi-music-player::before { content: "\f4a2"; } +.bi-newspaper::before { content: "\f4a3"; } +.bi-node-minus-fill::before { content: "\f4a4"; } +.bi-node-minus::before { content: "\f4a5"; } +.bi-node-plus-fill::before { content: "\f4a6"; } +.bi-node-plus::before { content: "\f4a7"; } +.bi-nut-fill::before { content: "\f4a8"; } +.bi-nut::before { content: "\f4a9"; } +.bi-octagon-fill::before { content: "\f4aa"; } +.bi-octagon-half::before { content: "\f4ab"; } +.bi-octagon::before { content: "\f4ac"; } +.bi-option::before { content: "\f4ad"; } +.bi-outlet::before { content: "\f4ae"; } +.bi-paint-bucket::before { content: "\f4af"; } +.bi-palette-fill::before { content: "\f4b0"; } +.bi-palette::before { content: "\f4b1"; } +.bi-palette2::before { content: "\f4b2"; } +.bi-paperclip::before { content: "\f4b3"; } +.bi-paragraph::before { content: "\f4b4"; } +.bi-patch-check-fill::before { content: "\f4b5"; } +.bi-patch-check::before { content: "\f4b6"; } +.bi-patch-exclamation-fill::before { content: "\f4b7"; } +.bi-patch-exclamation::before { content: "\f4b8"; } +.bi-patch-minus-fill::before { content: "\f4b9"; } +.bi-patch-minus::before { content: "\f4ba"; } +.bi-patch-plus-fill::before { content: "\f4bb"; } +.bi-patch-plus::before { content: "\f4bc"; } +.bi-patch-question-fill::before { content: "\f4bd"; } +.bi-patch-question::before { content: "\f4be"; } +.bi-pause-btn-fill::before { content: "\f4bf"; } +.bi-pause-btn::before { content: "\f4c0"; } +.bi-pause-circle-fill::before { content: "\f4c1"; } +.bi-pause-circle::before { content: "\f4c2"; } +.bi-pause-fill::before { content: "\f4c3"; } +.bi-pause::before { content: "\f4c4"; } +.bi-peace-fill::before { content: "\f4c5"; } +.bi-peace::before { content: "\f4c6"; } +.bi-pen-fill::before { content: "\f4c7"; } +.bi-pen::before { content: "\f4c8"; } +.bi-pencil-fill::before { content: "\f4c9"; } +.bi-pencil-square::before { content: "\f4ca"; } +.bi-pencil::before { content: "\f4cb"; } +.bi-pentagon-fill::before { content: "\f4cc"; } +.bi-pentagon-half::before { content: "\f4cd"; } +.bi-pentagon::before { content: "\f4ce"; } +.bi-people-fill::before { content: "\f4cf"; } +.bi-people::before { content: "\f4d0"; } +.bi-percent::before { content: "\f4d1"; } +.bi-person-badge-fill::before { content: "\f4d2"; } +.bi-person-badge::before { content: "\f4d3"; } +.bi-person-bounding-box::before { content: "\f4d4"; } +.bi-person-check-fill::before { content: "\f4d5"; } +.bi-person-check::before { content: "\f4d6"; } +.bi-person-circle::before { content: "\f4d7"; } +.bi-person-dash-fill::before { content: "\f4d8"; } +.bi-person-dash::before { content: "\f4d9"; } +.bi-person-fill::before { content: "\f4da"; } +.bi-person-lines-fill::before { content: "\f4db"; } + +.bi-person-plus-fill::before { content: "\f4dc"; } +.bi-person-plus::before { content: "\f4dd"; } +.bi-person-square::before { content: "\f4de"; } +.bi-person-x-fill::before { content: "\f4df"; } +.bi-person-x::before { content: "\f4e0"; } +.bi-person::before { content: "\f4e1"; } +.bi-phone-fill::before { content: "\f4e2"; } +.bi-phone-landscape-fill::before { content: "\f4e3"; } +.bi-phone-landscape::before { content: "\f4e4"; } +.bi-phone-vibrate-fill::before { content: "\f4e5"; } +.bi-phone-vibrate::before { content: "\f4e6"; } +.bi-phone::before { content: "\f4e7"; } +.bi-pie-chart-fill::before { content: "\f4e8"; } +.bi-pie-chart::before { content: "\f4e9"; } +.bi-pin-angle-fill::before { content: "\f4ea"; } +.bi-pin-angle::before { content: "\f4eb"; } +.bi-pin-fill::before { content: "\f4ec"; } +.bi-pin::before { content: "\f4ed"; } +.bi-pip-fill::before { content: "\f4ee"; } +.bi-pip::before { content: "\f4ef"; } +.bi-play-btn-fill::before { content: "\f4f0"; } +.bi-play-btn::before { content: "\f4f1"; } +.bi-play-circle-fill::before { content: "\f4f2"; } +.bi-play-circle::before { content: "\f4f3"; } +.bi-play-fill::before { content: "\f4f4"; } +.bi-play::before { content: "\f4f5"; } +.bi-plug-fill::before { content: "\f4f6"; } +.bi-plug::before { content: "\f4f7"; } +.bi-plus-circle-dotted::before { content: "\f4f8"; } +.bi-plus-circle-fill::before { content: "\f4f9"; } +.bi-plus-circle::before { content: "\f4fa"; } +.bi-plus-square-dotted::before { content: "\f4fb"; } +.bi-plus-square-fill::before { content: "\f4fc"; } +.bi-plus-square::before { content: "\f4fd"; } +.bi-plus::before { content: "\f4fe"; } +.bi-power::before { content: "\f4ff"; } +.bi-printer-fill::before { content: "\f500"; } +.bi-printer::before { content: "\f501"; } +.bi-puzzle-fill::before { content: "\f502"; } +.bi-puzzle::before { content: "\f503"; } +.bi-question-circle-fill::before { content: "\f504"; } +.bi-question-circle::before { content: "\f505"; } +.bi-question-diamond-fill::before { content: "\f506"; } +.bi-question-diamond::before { content: "\f507"; } +.bi-question-octagon-fill::before { content: "\f508"; } +.bi-question-octagon::before { content: "\f509"; } +.bi-question-square-fill::before { content: "\f50a"; } +.bi-question-square::before { content: "\f50b"; } +.bi-question::before { content: "\f50c"; } +.bi-rainbow::before { content: "\f50d"; } +.bi-receipt-cutoff::before { content: "\f50e"; } +.bi-receipt::before { content: "\f50f"; } +.bi-reception-0::before { content: "\f510"; } +.bi-reception-1::before { content: "\f511"; } +.bi-reception-2::before { content: "\f512"; } +.bi-reception-3::before { content: "\f513"; } +.bi-reception-4::before { content: "\f514"; } +.bi-record-btn-fill::before { content: "\f515"; } +.bi-record-btn::before { content: "\f516"; } +.bi-record-circle-fill::before { content: "\f517"; } +.bi-record-circle::before { content: "\f518"; } +.bi-record-fill::before { content: "\f519"; } +.bi-record::before { content: "\f51a"; } +.bi-record2-fill::before { content: "\f51b"; } +.bi-record2::before { content: "\f51c"; } +.bi-reply-all-fill::before { content: "\f51d"; } +.bi-reply-all::before { content: "\f51e"; } +.bi-reply-fill::before { content: "\f51f"; } +.bi-reply::before { content: "\f520"; } +.bi-rss-fill::before { content: "\f521"; } +.bi-rss::before { content: "\f522"; } +.bi-rulers::before { content: "\f523"; } +.bi-save-fill::before { content: "\f524"; } +.bi-save::before { content: "\f525"; } +.bi-save2-fill::before { content: "\f526"; } +.bi-save2::before { content: "\f527"; } +.bi-scissors::before { content: "\f528"; } +.bi-screwdriver::before { content: "\f529"; } +.bi-search::before { content: "\f52a"; } +.bi-segmented-nav::before { content: "\f52b"; } +.bi-server::before { content: "\f52c"; } +.bi-share-fill::before { content: "\f52d"; } +.bi-share::before { content: "\f52e"; } +.bi-shield-check::before { content: "\f52f"; } +.bi-shield-exclamation::before { content: "\f530"; } +.bi-shield-fill-check::before { content: "\f531"; } +.bi-shield-fill-exclamation::before { content: "\f532"; } + +.bi-shield-fill-minus::before { content: "\f533"; } +.bi-shield-fill-plus::before { content: "\f534"; } +.bi-shield-fill-x::before { content: "\f535"; } +.bi-shield-fill::before { content: "\f536"; } +.bi-shield-lock-fill::before { content: "\f537"; } +.bi-shield-lock::before { content: "\f538"; } +.bi-shield-minus::before { content: "\f539"; } +.bi-shield-plus::before { content: "\f53a"; } +.bi-shield-shaded::before { content: "\f53b"; } +.bi-shield-slash-fill::before { content: "\f53c"; } +.bi-shield-slash::before { content: "\f53d"; } +.bi-shield-x::before { content: "\f53e"; } +.bi-shield::before { content: "\f53f"; } +.bi-shift-fill::before { content: "\f540"; } +.bi-shift::before { content: "\f541"; } +.bi-shop-window::before { content: "\f542"; } +.bi-shop::before { content: "\f543"; } +.bi-shuffle::before { content: "\f544"; } +.bi-signpost-2-fill::before { content: "\f545"; } +.bi-signpost-2::before { content: "\f546"; } +.bi-signpost-fill::before { content: "\f547"; } +.bi-signpost-split-fill::before { content: "\f548"; } +.bi-signpost-split::before { content: "\f549"; } +.bi-signpost::before { content: "\f54a"; } +.bi-sim-fill::before { content: "\f54b"; } +.bi-sim::before { content: "\f54c"; } +.bi-skip-backward-btn-fill::before { content: "\f54d"; } +.bi-skip-backward-btn::before { content: "\f54e"; } +.bi-skip-backward-circle-fill::before { content: "\f54f"; } +.bi-skip-backward-circle::before { content: "\f550"; } +.bi-skip-backward-fill::before { content: "\f551"; } +.bi-skip-backward::before { content: "\f552"; } +.bi-skip-end-btn-fill::before { content: "\f553"; } +.bi-skip-end-btn::before { content: "\f554"; } +.bi-skip-end-circle-fill::before { content: "\f555"; } +.bi-skip-end-circle::before { content: "\f556"; } +.bi-skip-end-fill::before { content: "\f557"; } +.bi-skip-end::before { content: "\f558"; } +.bi-skip-forward-btn-fill::before { content: "\f559"; } +.bi-skip-forward-btn::before { content: "\f55a"; } +.bi-skip-forward-circle-fill::before { content: "\f55b"; } +.bi-skip-forward-circle::before { content: "\f55c"; } +.bi-skip-forward-fill::before { content: "\f55d"; } +.bi-skip-forward::before { content: "\f55e"; } +.bi-skip-start-btn-fill::before { content: "\f55f"; } +.bi-skip-start-btn::before { content: "\f560"; } +.bi-skip-start-circle-fill::before { content: "\f561"; } +.bi-skip-start-circle::before { content: "\f562"; } +.bi-skip-start-fill::before { content: "\f563"; } +.bi-skip-start::before { content: "\f564"; } +.bi-slack::before { content: "\f565"; } +.bi-slash-circle-fill::before { content: "\f566"; } +.bi-slash-circle::before { content: "\f567"; } +.bi-slash-square-fill::before { content: "\f568"; } +.bi-slash-square::before { content: "\f569"; } +.bi-slash::before { content: "\f56a"; } +.bi-sliders::before { content: "\f56b"; } +.bi-smartwatch::before { content: "\f56c"; } +.bi-snow::before { content: "\f56d"; } +.bi-snow2::before { content: "\f56e"; } +.bi-snow3::before { content: "\f56f"; } +.bi-sort-alpha-down-alt::before { content: "\f570"; } +.bi-sort-alpha-down::before { content: "\f571"; } +.bi-sort-alpha-up-alt::before { content: "\f572"; } +.bi-sort-alpha-up::before { content: "\f573"; } +.bi-sort-down-alt::before { content: "\f574"; } +.bi-sort-down::before { content: "\f575"; } +.bi-sort-numeric-down-alt::before { content: "\f576"; } +.bi-sort-numeric-down::before { content: "\f577"; } +.bi-sort-numeric-up-alt::before { content: "\f578"; } +.bi-sort-numeric-up::before { content: "\f579"; } +.bi-sort-up-alt::before { content: "\f57a"; } +.bi-sort-up::before { content: "\f57b"; } +.bi-soundwave::before { content: "\f57c"; } +.bi-speaker-fill::before { content: "\f57d"; } +.bi-speaker::before { content: "\f57e"; } +.bi-speedometer::before { content: "\f57f"; } +.bi-speedometer2::before { content: "\f580"; } +.bi-spellcheck::before { content: "\f581"; } +.bi-square-fill::before { content: "\f582"; } +.bi-square-half::before { content: "\f583"; } +.bi-square::before { content: "\f584"; } +.bi-stack::before { content: "\f585"; } +.bi-star-fill::before { content: "\f586"; } +.bi-star-half::before { content: "\f587"; } +.bi-star::before { content: "\f588"; } +.bi-stars::before { content: "\f589"; } +.bi-stickies-fill::before { content: "\f58a"; } +.bi-stickies::before { content: "\f58b"; } +.bi-sticky-fill::before { content: "\f58c"; } +.bi-sticky::before { content: "\f58d"; } +.bi-stop-btn-fill::before { content: "\f58e"; } +.bi-stop-btn::before { content: "\f58f"; } +.bi-stop-circle-fill::before { content: "\f590"; } +.bi-stop-circle::before { content: "\f591"; } +.bi-stop-fill::before { content: "\f592"; } +.bi-stop::before { content: "\f593"; } +.bi-stoplights-fill::before { content: "\f594"; } +.bi-stoplights::before { content: "\f595"; } +.bi-stopwatch-fill::before { content: "\f596"; } +.bi-stopwatch::before { content: "\f597"; } +.bi-subtract::before { content: "\f598"; } +.bi-suit-club-fill::before { content: "\f599"; } +.bi-suit-club::before { content: "\f59a"; } +.bi-suit-diamond-fill::before { content: "\f59b"; } +.bi-suit-diamond::before { content: "\f59c"; } +.bi-suit-heart-fill::before { content: "\f59d"; } +.bi-suit-heart::before { content: "\f59e"; } +.bi-suit-spade-fill::before { content: "\f59f"; } +.bi-suit-spade::before { content: "\f5a0"; } +.bi-sun-fill::before { content: "\f5a1"; } +.bi-sun::before { content: "\f5a2"; } +.bi-sunglasses::before { content: "\f5a3"; } +.bi-sunrise-fill::before { content: "\f5a4"; } +.bi-sunrise::before { content: "\f5a5"; } +.bi-sunset-fill::before { content: "\f5a6"; } +.bi-sunset::before { content: "\f5a7"; } +.bi-symmetry-horizontal::before { content: "\f5a8"; } +.bi-symmetry-vertical::before { content: "\f5a9"; } +.bi-table::before { content: "\f5aa"; } +.bi-tablet-fill::before { content: "\f5ab"; } +.bi-tablet-landscape-fill::before { content: "\f5ac"; } +.bi-tablet-landscape::before { content: "\f5ad"; } +.bi-tablet::before { content: "\f5ae"; } +.bi-tag-fill::before { content: "\f5af"; } +.bi-tag::before { content: "\f5b0"; } +.bi-tags-fill::before { content: "\f5b1"; } +.bi-tags::before { content: "\f5b2"; } +.bi-telegram::before { content: "\f5b3"; } +.bi-telephone-fill::before { content: "\f5b4"; } +.bi-telephone-forward-fill::before { content: "\f5b5"; } +.bi-telephone-forward::before { content: "\f5b6"; } +.bi-telephone-inbound-fill::before { content: "\f5b7"; } +.bi-telephone-inbound::before { content: "\f5b8"; } +.bi-telephone-minus-fill::before { content: "\f5b9"; } +.bi-telephone-minus::before { content: "\f5ba"; } +.bi-telephone-outbound-fill::before { content: "\f5bb"; } +.bi-telephone-outbound::before { content: "\f5bc"; } +.bi-telephone-plus-fill::before { content: "\f5bd"; } +.bi-telephone-plus::before { content: "\f5be"; } +.bi-telephone-x-fill::before { content: "\f5bf"; } +.bi-telephone-x::before { content: "\f5c0"; } +.bi-telephone::before { content: "\f5c1"; } +.bi-terminal-fill::before { content: "\f5c2"; } +.bi-terminal::before { content: "\f5c3"; } +.bi-text-center::before { content: "\f5c4"; } +.bi-text-indent-left::before { content: "\f5c5"; } +.bi-text-indent-right::before { content: "\f5c6"; } +.bi-text-left::before { content: "\f5c7"; } +.bi-text-paragraph::before { content: "\f5c8"; } +.bi-text-right::before { content: "\f5c9"; } +.bi-textarea-resize::before { content: "\f5ca"; } +.bi-textarea-t::before { content: "\f5cb"; } +.bi-textarea::before { content: "\f5cc"; } +.bi-thermometer-half::before { content: "\f5cd"; } +.bi-thermometer-high::before { content: "\f5ce"; } +.bi-thermometer-low::before { content: "\f5cf"; } +.bi-thermometer-snow::before { content: "\f5d0"; } +.bi-thermometer-sun::before { content: "\f5d1"; } +.bi-thermometer::before { content: "\f5d2"; } +.bi-three-dots-vertical::before { content: "\f5d3"; } +.bi-three-dots::before { content: "\f5d4"; } +.bi-toggle-off::before { content: "\f5d5"; } +.bi-toggle-on::before { content: "\f5d6"; } +.bi-toggle2-off::before { content: "\f5d7"; } +.bi-toggle2-on::before { content: "\f5d8"; } +.bi-toggles::before { content: "\f5d9"; } +.bi-toggles2::before { content: "\f5da"; } +.bi-tools::before { content: "\f5db"; } +.bi-tornado::before { content: "\f5dc"; } +.bi-trash-fill::before { content: "\f5dd"; } +.bi-trash::before { content: "\f5de"; } +.bi-trash2-fill::before { content: "\f5df"; } +.bi-trash2::before { content: "\f5e0"; } +.bi-tree-fill::before { content: "\f5e1"; } +.bi-tree::before { content: "\f5e2"; } +.bi-triangle-fill::before { content: "\f5e3"; } +.bi-triangle-half::before { content: "\f5e4"; } +.bi-triangle::before { content: "\f5e5"; } +.bi-trophy-fill::before { content: "\f5e6"; } +.bi-trophy::before { content: "\f5e7"; } +.bi-tropical-storm::before { content: "\f5e8"; } +.bi-truck-flatbed::before { content: "\f5e9"; } +.bi-truck::before { content: "\f5ea"; } +.bi-tsunami::before { content: "\f5eb"; } +.bi-tv-fill::before { content: "\f5ec"; } +.bi-tv::before { content: "\f5ed"; } +.bi-twitch::before { content: "\f5ee"; } +.bi-twitter::before { content: "\f5ef"; } +.bi-type-bold::before { content: "\f5f0"; } +.bi-type-h1::before { content: "\f5f1"; } +.bi-type-h2::before { content: "\f5f2"; } +.bi-type-h3::before { content: "\f5f3"; } +.bi-type-italic::before { content: "\f5f4"; } +.bi-type-strikethrough::before { content: "\f5f5"; } +.bi-type-underline::before { content: "\f5f6"; } +.bi-type::before { content: "\f5f7"; } +.bi-ui-checks-grid::before { content: "\f5f8"; } +.bi-ui-checks::before { content: "\f5f9"; } +.bi-ui-radios-grid::before { content: "\f5fa"; } +.bi-ui-radios::before { content: "\f5fb"; } +.bi-umbrella-fill::before { content: "\f5fc"; } +.bi-umbrella::before { content: "\f5fd"; } +.bi-union::before { content: "\f5fe"; } +.bi-unlock-fill::before { content: "\f5ff"; } +.bi-unlock::before { content: "\f600"; } +.bi-upc-scan::before { content: "\f601"; } +.bi-upc::before { content: "\f602"; } +.bi-upload::before { content: "\f603"; } +.bi-vector-pen::before { content: "\f604"; } +.bi-view-list::before { content: "\f605"; } +.bi-view-stacked::before { content: "\f606"; } +.bi-vinyl-fill::before { content: "\f607"; } +.bi-vinyl::before { content: "\f608"; } +.bi-voicemail::before { content: "\f609"; } +.bi-volume-down-fill::before { content: "\f60a"; } +.bi-volume-down::before { content: "\f60b"; } +.bi-volume-mute-fill::before { content: "\f60c"; } +.bi-volume-mute::before { content: "\f60d"; } +.bi-volume-off-fill::before { content: "\f60e"; } +.bi-volume-off::before { content: "\f60f"; } +.bi-volume-up-fill::before { content: "\f610"; } +.bi-volume-up::before { content: "\f611"; } +.bi-vr::before { content: "\f612"; } +.bi-wallet-fill::before { content: "\f613"; } +.bi-wallet::before { content: "\f614"; } +.bi-wallet2::before { content: "\f615"; } +.bi-watch::before { content: "\f616"; } +.bi-water::before { content: "\f617"; } +.bi-whatsapp::before { content: "\f618"; } +.bi-wifi-1::before { content: "\f619"; } +.bi-wifi-2::before { content: "\f61a"; } +.bi-wifi-off::before { content: "\f61b"; } +.bi-wifi::before { content: "\f61c"; } +.bi-wind::before { content: "\f61d"; } +.bi-window-dock::before { content: "\f61e"; } +.bi-window-sidebar::before { content: "\f61f"; } +.bi-window::before { content: "\f620"; } +.bi-wrench::before { content: "\f621"; } +.bi-x-circle-fill::before { content: "\f622"; } +.bi-x-circle::before { content: "\f623"; } +.bi-x-diamond-fill::before { content: "\f624"; } +.bi-x-diamond::before { content: "\f625"; } +.bi-x-octagon-fill::before { content: "\f626"; } +.bi-x-octagon::before { content: "\f627"; } +.bi-x-square-fill::before { content: "\f628"; } +.bi-x-square::before { content: "\f629"; } +.bi-x::before { content: "\f62a"; } +.bi-youtube::before { content: "\f62b"; } +.bi-zoom-in::before { content: "\f62c"; } +.bi-zoom-out::before { content: "\f62d"; } +.bi-bank::before { content: "\f62e"; } +.bi-bank2::before { content: "\f62f"; } +.bi-bell-slash-fill::before { content: "\f630"; } +.bi-bell-slash::before { content: "\f631"; } +.bi-cash-coin::before { content: "\f632"; } +.bi-check-lg::before { content: "\f633"; } +.bi-coin::before { content: "\f634"; } +.bi-currency-bitcoin::before { content: "\f635"; } +.bi-currency-dollar::before { content: "\f636"; } +.bi-currency-euro::before { content: "\f637"; } +.bi-currency-exchange::before { content: "\f638"; } +.bi-currency-pound::before { content: "\f639"; } +.bi-currency-yen::before { content: "\f63a"; } +.bi-dash-lg::before { content: "\f63b"; } +.bi-exclamation-lg::before { content: "\f63c"; } +.bi-file-earmark-pdf-fill::before { content: "\f63d"; } +.bi-file-earmark-pdf::before { content: "\f63e"; } +.bi-file-pdf-fill::before { content: "\f63f"; } +.bi-file-pdf::before { content: "\f640"; } +.bi-gender-ambiguous::before { content: "\f641"; } +.bi-gender-female::before { content: "\f642"; } +.bi-gender-male::before { content: "\f643"; } +.bi-gender-trans::before { content: "\f644"; } +.bi-headset-vr::before { content: "\f645"; } +.bi-info-lg::before { content: "\f646"; } +.bi-mastodon::before { content: "\f647"; } +.bi-messenger::before { content: "\f648"; } +.bi-piggy-bank-fill::before { content: "\f649"; } +.bi-piggy-bank::before { content: "\f64a"; } +.bi-pin-map-fill::before { content: "\f64b"; } +.bi-pin-map::before { content: "\f64c"; } +.bi-plus-lg::before { content: "\f64d"; } +.bi-question-lg::before { content: "\f64e"; } +.bi-recycle::before { content: "\f64f"; } +.bi-reddit::before { content: "\f650"; } +.bi-safe-fill::before { content: "\f651"; } +.bi-safe2-fill::before { content: "\f652"; } +.bi-safe2::before { content: "\f653"; } +.bi-sd-card-fill::before { content: "\f654"; } +.bi-sd-card::before { content: "\f655"; } +.bi-skype::before { content: "\f656"; } +.bi-slash-lg::before { content: "\f657"; } +.bi-translate::before { content: "\f658"; } +.bi-x-lg::before { content: "\f659"; } +.bi-safe::before { content: "\f65a"; } +.bi-apple::before { content: "\f65b"; } +.bi-microsoft::before { content: "\f65d"; } +.bi-windows::before { content: "\f65e"; } +.bi-behance::before { content: "\f65c"; } +.bi-dribbble::before { content: "\f65f"; } +.bi-line::before { content: "\f660"; } +.bi-medium::before { content: "\f661"; } +.bi-paypal::before { content: "\f662"; } +.bi-pinterest::before { content: "\f663"; } +.bi-signal::before { content: "\f664"; } +.bi-snapchat::before { content: "\f665"; } +.bi-spotify::before { content: "\f666"; } +.bi-stack-overflow::before { content: "\f667"; } +.bi-strava::before { content: "\f668"; } +.bi-wordpress::before { content: "\f669"; } +.bi-vimeo::before { content: "\f66a"; } +.bi-activity::before { content: "\f66b"; } +.bi-easel2-fill::before { content: "\f66c"; } +.bi-easel2::before { content: "\f66d"; } +.bi-easel3-fill::before { content: "\f66e"; } +.bi-easel3::before { content: "\f66f"; } +.bi-fan::before { content: "\f670"; } +.bi-fingerprint::before { content: "\f671"; } +.bi-graph-down-arrow::before { content: "\f672"; } +.bi-graph-up-arrow::before { content: "\f673"; } +.bi-hypnotize::before { content: "\f674"; } +.bi-magic::before { content: "\f675"; } +.bi-person-rolodex::before { content: "\f676"; } +.bi-person-video::before { content: "\f677"; } +.bi-person-video2::before { content: "\f678"; } +.bi-person-video3::before { content: "\f679"; } +.bi-person-workspace::before { content: "\f67a"; } +.bi-radioactive::before { content: "\f67b"; } +.bi-webcam-fill::before { content: "\f67c"; } +.bi-webcam::before { content: "\f67d"; } +.bi-yin-yang::before { content: "\f67e"; } +.bi-bandaid-fill::before { content: "\f680"; } +.bi-bandaid::before { content: "\f681"; } +.bi-bluetooth::before { content: "\f682"; } +.bi-body-text::before { content: "\f683"; } +.bi-boombox::before { content: "\f684"; } +.bi-boxes::before { content: "\f685"; } +.bi-dpad-fill::before { content: "\f686"; } +.bi-dpad::before { content: "\f687"; } +.bi-ear-fill::before { content: "\f688"; } +.bi-ear::before { content: "\f689"; } +.bi-envelope-check-1::before { content: "\f68a"; } +.bi-envelope-check-fill::before { content: "\f68b"; } +.bi-envelope-check::before { content: "\f68c"; } +.bi-envelope-dash-1::before { content: "\f68d"; } +.bi-envelope-dash-fill::before { content: "\f68e"; } +.bi-envelope-dash::before { content: "\f68f"; } +.bi-envelope-exclamation-1::before { content: "\f690"; } +.bi-envelope-exclamation-fill::before { content: "\f691"; } +.bi-envelope-exclamation::before { content: "\f692"; } +.bi-envelope-plus-fill::before { content: "\f693"; } +.bi-envelope-plus::before { content: "\f694"; } +.bi-envelope-slash-1::before { content: "\f695"; } +.bi-envelope-slash-fill::before { content: "\f696"; } +.bi-envelope-slash::before { content: "\f697"; } +.bi-envelope-x-1::before { content: "\f698"; } +.bi-envelope-x-fill::before { content: "\f699"; } +.bi-envelope-x::before { content: "\f69a"; } +.bi-explicit-fill::before { content: "\f69b"; } +.bi-explicit::before { content: "\f69c"; } +.bi-git::before { content: "\f69d"; } +.bi-infinity::before { content: "\f69e"; } +.bi-list-columns-reverse::before { content: "\f69f"; } +.bi-list-columns::before { content: "\f6a0"; } +.bi-meta::before { content: "\f6a1"; } +.bi-mortorboard-fill::before { content: "\f6a2"; } +.bi-mortorboard::before { content: "\f6a3"; } +.bi-nintendo-switch::before { content: "\f6a4"; } +.bi-pc-display-horizontal::before { content: "\f6a5"; } +.bi-pc-display::before { content: "\f6a6"; } +.bi-pc-horizontal::before { content: "\f6a7"; } +.bi-pc::before { content: "\f6a8"; } +.bi-playstation::before { content: "\f6a9"; } +.bi-plus-slash-minus::before { content: "\f6aa"; } +.bi-projector-fill::before { content: "\f6ab"; } +.bi-projector::before { content: "\f6ac"; } +.bi-qr-code-scan::before { content: "\f6ad"; } +.bi-qr-code::before { content: "\f6ae"; } +.bi-quora::before { content: "\f6af"; } +.bi-quote::before { content: "\f6b0"; } +.bi-robot::before { content: "\f6b1"; } +.bi-send-check-fill::before { content: "\f6b2"; } +.bi-send-check::before { content: "\f6b3"; } +.bi-send-dash-fill::before { content: "\f6b4"; } +.bi-send-dash::before { content: "\f6b5"; } +.bi-send-exclamation-1::before { content: "\f6b6"; } +.bi-send-exclamation-fill::before { content: "\f6b7"; } +.bi-send-exclamation::before { content: "\f6b8"; } +.bi-send-fill::before { content: "\f6b9"; } +.bi-send-plus-fill::before { content: "\f6ba"; } +.bi-send-plus::before { content: "\f6bb"; } +.bi-send-slash-fill::before { content: "\f6bc"; } +.bi-send-slash::before { content: "\f6bd"; } +.bi-send-x-fill::before { content: "\f6be"; } +.bi-send-x::before { content: "\f6bf"; } +.bi-send::before { content: "\f6c0"; } +.bi-steam::before { content: "\f6c1"; } +.bi-terminal-dash-1::before { content: "\f6c2"; } +.bi-terminal-dash::before { content: "\f6c3"; } +.bi-terminal-plus::before { content: "\f6c4"; } +.bi-terminal-split::before { content: "\f6c5"; } +.bi-ticket-detailed-fill::before { content: "\f6c6"; } +.bi-ticket-detailed::before { content: "\f6c7"; } +.bi-ticket-fill::before { content: "\f6c8"; } +.bi-ticket-perforated-fill::before { content: "\f6c9"; } +.bi-ticket-perforated::before { content: "\f6ca"; } +.bi-ticket::before { content: "\f6cb"; } +.bi-tiktok::before { content: "\f6cc"; } +.bi-window-dash::before { content: "\f6cd"; } +.bi-window-desktop::before { content: "\f6ce"; } +.bi-window-fullscreen::before { content: "\f6cf"; } +.bi-window-plus::before { content: "\f6d0"; } +.bi-window-split::before { content: "\f6d1"; } +.bi-window-stack::before { content: "\f6d2"; } +.bi-window-x::before { content: "\f6d3"; } +.bi-xbox::before { content: "\f6d4"; } +.bi-ethernet::before { content: "\f6d5"; } +.bi-hdmi-fill::before { content: "\f6d6"; } +.bi-hdmi::before { content: "\f6d7"; } +.bi-usb-c-fill::before { content: "\f6d8"; } +.bi-usb-c::before { content: "\f6d9"; } +.bi-usb-fill::before { content: "\f6da"; } +.bi-usb-plug-fill::before { content: "\f6db"; } +.bi-usb-plug::before { content: "\f6dc"; } +.bi-usb-symbol::before { content: "\f6dd"; } +.bi-usb::before { content: "\f6de"; } +.bi-boombox-fill::before { content: "\f6df"; } +.bi-displayport-1::before { content: "\f6e0"; } +.bi-displayport::before { content: "\f6e1"; } +.bi-gpu-card::before { content: "\f6e2"; } +.bi-memory::before { content: "\f6e3"; } +.bi-modem-fill::before { content: "\f6e4"; } +.bi-modem::before { content: "\f6e5"; } +.bi-motherboard-fill::before { content: "\f6e6"; } +.bi-motherboard::before { content: "\f6e7"; } +.bi-optical-audio-fill::before { content: "\f6e8"; } +.bi-optical-audio::before { content: "\f6e9"; } +.bi-pci-card::before { content: "\f6ea"; } +.bi-router-fill::before { content: "\f6eb"; } +.bi-router::before { content: "\f6ec"; } +.bi-ssd-fill::before { content: "\f6ed"; } +.bi-ssd::before { content: "\f6ee"; } +.bi-thunderbolt-fill::before { content: "\f6ef"; } +.bi-thunderbolt::before { content: "\f6f0"; } +.bi-usb-drive-fill::before { content: "\f6f1"; } +.bi-usb-drive::before { content: "\f6f2"; } +.bi-usb-micro-fill::before { content: "\f6f3"; } +.bi-usb-micro::before { content: "\f6f4"; } +.bi-usb-mini-fill::before { content: "\f6f5"; } +.bi-usb-mini::before { content: "\f6f6"; } +.bi-cloud-haze2::before { content: "\f6f7"; } +.bi-device-hdd-fill::before { content: "\f6f8"; } +.bi-device-hdd::before { content: "\f6f9"; } +.bi-device-ssd-fill::before { content: "\f6fa"; } +.bi-device-ssd::before { content: "\f6fb"; } +.bi-displayport-fill::before { content: "\f6fc"; } +.bi-mortarboard-fill::before { content: "\f6fd"; } +.bi-mortarboard::before { content: "\f6fe"; } +.bi-terminal-x::before { content: "\f6ff"; } +.bi-arrow-through-heart-fill::before { content: "\f700"; } +.bi-arrow-through-heart::before { content: "\f701"; } +.bi-badge-sd-fill::before { content: "\f702"; } +.bi-badge-sd::before { content: "\f703"; } +.bi-bag-heart-fill::before { content: "\f704"; } +.bi-bag-heart::before { content: "\f705"; } +.bi-balloon-fill::before { content: "\f706"; } +.bi-balloon-heart-fill::before { content: "\f707"; } +.bi-balloon-heart::before { content: "\f708"; } +.bi-balloon::before { content: "\f709"; } +.bi-box2-fill::before { content: "\f70a"; } +.bi-box2-heart-fill::before { content: "\f70b"; } +.bi-box2-heart::before { content: "\f70c"; } +.bi-box2::before { content: "\f70d"; } +.bi-braces-asterisk::before { content: "\f70e"; } +.bi-calendar-heart-fill::before { content: "\f70f"; } +.bi-calendar-heart::before { content: "\f710"; } +.bi-calendar2-heart-fill::before { content: "\f711"; } +.bi-calendar2-heart::before { content: "\f712"; } +.bi-chat-heart-fill::before { content: "\f713"; } +.bi-chat-heart::before { content: "\f714"; } +.bi-chat-left-heart-fill::before { content: "\f715"; } +.bi-chat-left-heart::before { content: "\f716"; } +.bi-chat-right-heart-fill::before { content: "\f717"; } +.bi-chat-right-heart::before { content: "\f718"; } +.bi-chat-square-heart-fill::before { content: "\f719"; } +.bi-chat-square-heart::before { content: "\f71a"; } +.bi-clipboard-check-fill::before { content: "\f71b"; } +.bi-clipboard-data-fill::before { content: "\f71c"; } +.bi-clipboard-fill::before { content: "\f71d"; } +.bi-clipboard-heart-fill::before { content: "\f71e"; } +.bi-clipboard-heart::before { content: "\f71f"; } +.bi-clipboard-minus-fill::before { content: "\f720"; } +.bi-clipboard-plus-fill::before { content: "\f721"; } +.bi-clipboard-pulse::before { content: "\f722"; } +.bi-clipboard-x-fill::before { content: "\f723"; } +.bi-clipboard2-check-fill::before { content: "\f724"; } +.bi-clipboard2-check::before { content: "\f725"; } +.bi-clipboard2-data-fill::before { content: "\f726"; } +.bi-clipboard2-data::before { content: "\f727"; } +.bi-clipboard2-fill::before { content: "\f728"; } +.bi-clipboard2-heart-fill::before { content: "\f729"; } +.bi-clipboard2-heart::before { content: "\f72a"; } +.bi-clipboard2-minus-fill::before { content: "\f72b"; } +.bi-clipboard2-minus::before { content: "\f72c"; } +.bi-clipboard2-plus-fill::before { content: "\f72d"; } +.bi-clipboard2-plus::before { content: "\f72e"; } +.bi-clipboard2-pulse-fill::before { content: "\f72f"; } +.bi-clipboard2-pulse::before { content: "\f730"; } +.bi-clipboard2-x-fill::before { content: "\f731"; } +.bi-clipboard2-x::before { content: "\f732"; } +.bi-clipboard2::before { content: "\f733"; } +.bi-emoji-kiss-fill::before { content: "\f734"; } +.bi-emoji-kiss::before { content: "\f735"; } +.bi-envelope-heart-fill::before { content: "\f736"; } +.bi-envelope-heart::before { content: "\f737"; } +.bi-envelope-open-heart-fill::before { content: "\f738"; } +.bi-envelope-open-heart::before { content: "\f739"; } +.bi-envelope-paper-fill::before { content: "\f73a"; } +.bi-envelope-paper-heart-fill::before { content: "\f73b"; } +.bi-envelope-paper-heart::before { content: "\f73c"; } +.bi-envelope-paper::before { content: "\f73d"; } +.bi-filetype-aac::before { content: "\f73e"; } +.bi-filetype-ai::before { content: "\f73f"; } +.bi-filetype-bmp::before { content: "\f740"; } +.bi-filetype-cs::before { content: "\f741"; } +.bi-filetype-css::before { content: "\f742"; } +.bi-filetype-csv::before { content: "\f743"; } +.bi-filetype-doc::before { content: "\f744"; } +.bi-filetype-docx::before { content: "\f745"; } +.bi-filetype-exe::before { content: "\f746"; } +.bi-filetype-gif::before { content: "\f747"; } +.bi-filetype-heic::before { content: "\f748"; } +.bi-filetype-html::before { content: "\f749"; } +.bi-filetype-java::before { content: "\f74a"; } +.bi-filetype-jpg::before { content: "\f74b"; } +.bi-filetype-js::before { content: "\f74c"; } +.bi-filetype-jsx::before { content: "\f74d"; } +.bi-filetype-key::before { content: "\f74e"; } +.bi-filetype-m4p::before { content: "\f74f"; } +.bi-filetype-md::before { content: "\f750"; } +.bi-filetype-mdx::before { content: "\f751"; } +.bi-filetype-mov::before { content: "\f752"; } +.bi-filetype-mp3::before { content: "\f753"; } +.bi-filetype-mp4::before { content: "\f754"; } +.bi-filetype-otf::before { content: "\f755"; } +.bi-filetype-pdf::before { content: "\f756"; } +.bi-filetype-php::before { content: "\f757"; } +.bi-filetype-png::before { content: "\f758"; } +.bi-filetype-ppt-1::before { content: "\f759"; } +.bi-filetype-ppt::before { content: "\f75a"; } +.bi-filetype-psd::before { content: "\f75b"; } +.bi-filetype-py::before { content: "\f75c"; } +.bi-filetype-raw::before { content: "\f75d"; } +.bi-filetype-rb::before { content: "\f75e"; } +.bi-filetype-sass::before { content: "\f75f"; } +.bi-filetype-scss::before { content: "\f760"; } +.bi-filetype-sh::before { content: "\f761"; } +.bi-filetype-svg::before { content: "\f762"; } +.bi-filetype-tiff::before { content: "\f763"; } +.bi-filetype-tsx::before { content: "\f764"; } +.bi-filetype-ttf::before { content: "\f765"; } +.bi-filetype-txt::before { content: "\f766"; } +.bi-filetype-wav::before { content: "\f767"; } +.bi-filetype-woff::before { content: "\f768"; } +.bi-filetype-xls-1::before { content: "\f769"; } +.bi-filetype-xls::before { content: "\f76a"; } +.bi-filetype-xml::before { content: "\f76b"; } +.bi-filetype-yml::before { content: "\f76c"; } +.bi-heart-arrow::before { content: "\f76d"; } +.bi-heart-pulse-fill::before { content: "\f76e"; } +.bi-heart-pulse::before { content: "\f76f"; } +.bi-heartbreak-fill::before { content: "\f770"; } +.bi-heartbreak::before { content: "\f771"; } +.bi-hearts::before { content: "\f772"; } +.bi-hospital-fill::before { content: "\f773"; } +.bi-hospital::before { content: "\f774"; } +.bi-house-heart-fill::before { content: "\f775"; } +.bi-house-heart::before { content: "\f776"; } +.bi-incognito::before { content: "\f777"; } +.bi-magnet-fill::before { content: "\f778"; } +.bi-magnet::before { content: "\f779"; } +.bi-person-heart::before { content: "\f77a"; } +.bi-person-hearts::before { content: "\f77b"; } +.bi-phone-flip::before { content: "\f77c"; } +.bi-plugin::before { content: "\f77d"; } +.bi-postage-fill::before { content: "\f77e"; } +.bi-postage-heart-fill::before { content: "\f77f"; } +.bi-postage-heart::before { content: "\f780"; } +.bi-postage::before { content: "\f781"; } +.bi-postcard-fill::before { content: "\f782"; } +.bi-postcard-heart-fill::before { content: "\f783"; } +.bi-postcard-heart::before { content: "\f784"; } +.bi-postcard::before { content: "\f785"; } +.bi-search-heart-fill::before { content: "\f786"; } +.bi-search-heart::before { content: "\f787"; } +.bi-sliders2-vertical::before { content: "\f788"; } +.bi-sliders2::before { content: "\f789"; } +.bi-trash3-fill::before { content: "\f78a"; } +.bi-trash3::before { content: "\f78b"; } +.bi-valentine::before { content: "\f78c"; } +.bi-valentine2::before { content: "\f78d"; } +.bi-wrench-adjustable-circle-fill::before { content: "\f78e"; } +.bi-wrench-adjustable-circle::before { content: "\f78f"; } +.bi-wrench-adjustable::before { content: "\f790"; } +.bi-filetype-json::before { content: "\f791"; } +.bi-filetype-pptx::before { content: "\f792"; } +.bi-filetype-xlsx::before { content: "\f793"; } +.bi-1-circle-1::before { content: "\f794"; } +.bi-1-circle-fill-1::before { content: "\f795"; } +.bi-1-circle-fill::before { content: "\f796"; } +.bi-1-circle::before { content: "\f797"; } +.bi-1-square-fill::before { content: "\f798"; } +.bi-1-square::before { content: "\f799"; } +.bi-2-circle-1::before { content: "\f79a"; } +.bi-2-circle-fill-1::before { content: "\f79b"; } +.bi-2-circle-fill::before { content: "\f79c"; } +.bi-2-circle::before { content: "\f79d"; } +.bi-2-square-fill::before { content: "\f79e"; } +.bi-2-square::before { content: "\f79f"; } +.bi-3-circle-1::before { content: "\f7a0"; } +.bi-3-circle-fill-1::before { content: "\f7a1"; } +.bi-3-circle-fill::before { content: "\f7a2"; } +.bi-3-circle::before { content: "\f7a3"; } +.bi-3-square-fill::before { content: "\f7a4"; } +.bi-3-square::before { content: "\f7a5"; } +.bi-4-circle-1::before { content: "\f7a6"; } +.bi-4-circle-fill-1::before { content: "\f7a7"; } +.bi-4-circle-fill::before { content: "\f7a8"; } +.bi-4-circle::before { content: "\f7a9"; } +.bi-4-square-fill::before { content: "\f7aa"; } +.bi-4-square::before { content: "\f7ab"; } +.bi-5-circle-1::before { content: "\f7ac"; } +.bi-5-circle-fill-1::before { content: "\f7ad"; } +.bi-5-circle-fill::before { content: "\f7ae"; } +.bi-5-circle::before { content: "\f7af"; } +.bi-5-square-fill::before { content: "\f7b0"; } +.bi-5-square::before { content: "\f7b1"; } +.bi-6-circle-1::before { content: "\f7b2"; } +.bi-6-circle-fill-1::before { content: "\f7b3"; } +.bi-6-circle-fill::before { content: "\f7b4"; } +.bi-6-circle::before { content: "\f7b5"; } +.bi-6-square-fill::before { content: "\f7b6"; } +.bi-6-square::before { content: "\f7b7"; } +.bi-7-circle-1::before { content: "\f7b8"; } +.bi-7-circle-fill-1::before { content: "\f7b9"; } +.bi-7-circle-fill::before { content: "\f7ba"; } +.bi-7-circle::before { content: "\f7bb"; } +.bi-7-square-fill::before { content: "\f7bc"; } +.bi-7-square::before { content: "\f7bd"; } +.bi-8-circle-1::before { content: "\f7be"; } +.bi-8-circle-fill-1::before { content: "\f7bf"; } +.bi-8-circle-fill::before { content: "\f7c0"; } +.bi-8-circle::before { content: "\f7c1"; } +.bi-8-square-fill::before { content: "\f7c2"; } +.bi-8-square::before { content: "\f7c3"; } +.bi-9-circle-1::before { content: "\f7c4"; } +.bi-9-circle-fill-1::before { content: "\f7c5"; } +.bi-9-circle-fill::before { content: "\f7c6"; } +.bi-9-circle::before { content: "\f7c7"; } +.bi-9-square-fill::before { content: "\f7c8"; } +.bi-9-square::before { content: "\f7c9"; } +.bi-airplane-engines-fill::before { content: "\f7ca"; } +.bi-airplane-engines::before { content: "\f7cb"; } +.bi-airplane-fill::before { content: "\f7cc"; } +.bi-airplane::before { content: "\f7cd"; } +.bi-alexa::before { content: "\f7ce"; } +.bi-alipay::before { content: "\f7cf"; } +.bi-android::before { content: "\f7d0"; } +.bi-android2::before { content: "\f7d1"; } +.bi-box-fill::before { content: "\f7d2"; } +.bi-box-seam-fill::before { content: "\f7d3"; } +.bi-browser-chrome::before { content: "\f7d4"; } +.bi-browser-edge::before { content: "\f7d5"; } +.bi-browser-firefox::before { content: "\f7d6"; } +.bi-browser-safari::before { content: "\f7d7"; } +.bi-c-circle-1::before { content: "\f7d8"; } +.bi-c-circle-fill-1::before { content: "\f7d9"; } +.bi-c-circle-fill::before { content: "\f7da"; } +.bi-c-circle::before { content: "\f7db"; } +.bi-c-square-fill::before { content: "\f7dc"; } +.bi-c-square::before { content: "\f7dd"; } +.bi-capsule-pill::before { content: "\f7de"; } +.bi-capsule::before { content: "\f7df"; } +.bi-car-front-fill::before { content: "\f7e0"; } +.bi-car-front::before { content: "\f7e1"; } +.bi-cassette-fill::before { content: "\f7e2"; } +.bi-cassette::before { content: "\f7e3"; } +.bi-cc-circle-1::before { content: "\f7e4"; } +.bi-cc-circle-fill-1::before { content: "\f7e5"; } +.bi-cc-circle-fill::before { content: "\f7e6"; } +.bi-cc-circle::before { content: "\f7e7"; } +.bi-cc-square-fill::before { content: "\f7e8"; } +.bi-cc-square::before { content: "\f7e9"; } +.bi-cup-hot-fill::before { content: "\f7ea"; } +.bi-cup-hot::before { content: "\f7eb"; } +.bi-currency-rupee::before { content: "\f7ec"; } +.bi-dropbox::before { content: "\f7ed"; } +.bi-escape::before { content: "\f7ee"; } +.bi-fast-forward-btn-fill::before { content: "\f7ef"; } +.bi-fast-forward-btn::before { content: "\f7f0"; } +.bi-fast-forward-circle-fill::before { content: "\f7f1"; } +.bi-fast-forward-circle::before { content: "\f7f2"; } +.bi-fast-forward-fill::before { content: "\f7f3"; } +.bi-fast-forward::before { content: "\f7f4"; } +.bi-filetype-sql::before { content: "\f7f5"; } +.bi-fire::before { content: "\f7f6"; } +.bi-google-play::before { content: "\f7f7"; } +.bi-h-circle-1::before { content: "\f7f8"; } +.bi-h-circle-fill-1::before { content: "\f7f9"; } +.bi-h-circle-fill::before { content: "\f7fa"; } +.bi-h-circle::before { content: "\f7fb"; } +.bi-h-square-fill::before { content: "\f7fc"; } +.bi-h-square::before { content: "\f7fd"; } +.bi-indent::before { content: "\f7fe"; } +.bi-lungs-fill::before { content: "\f7ff"; } +.bi-lungs::before { content: "\f800"; } +.bi-microsoft-teams::before { content: "\f801"; } +.bi-p-circle-1::before { content: "\f802"; } +.bi-p-circle-fill-1::before { content: "\f803"; } +.bi-p-circle-fill::before { content: "\f804"; } +.bi-p-circle::before { content: "\f805"; } +.bi-p-square-fill::before { content: "\f806"; } +.bi-p-square::before { content: "\f807"; } +.bi-pass-fill::before { content: "\f808"; } +.bi-pass::before { content: "\f809"; } +.bi-prescription::before { content: "\f80a"; } +.bi-prescription2::before { content: "\f80b"; } +.bi-r-circle-1::before { content: "\f80c"; } +.bi-r-circle-fill-1::before { content: "\f80d"; } +.bi-r-circle-fill::before { content: "\f80e"; } +.bi-r-circle::before { content: "\f80f"; } +.bi-r-square-fill::before { content: "\f810"; } +.bi-r-square::before { content: "\f811"; } +.bi-repeat-1::before { content: "\f812"; } +.bi-repeat::before { content: "\f813"; } +.bi-rewind-btn-fill::before { content: "\f814"; } +.bi-rewind-btn::before { content: "\f815"; } +.bi-rewind-circle-fill::before { content: "\f816"; } +.bi-rewind-circle::before { content: "\f817"; } +.bi-rewind-fill::before { content: "\f818"; } +.bi-rewind::before { content: "\f819"; } +.bi-train-freight-front-fill::before { content: "\f81a"; } +.bi-train-freight-front::before { content: "\f81b"; } +.bi-train-front-fill::before { content: "\f81c"; } +.bi-train-front::before { content: "\f81d"; } +.bi-train-lightrail-front-fill::before { content: "\f81e"; } +.bi-train-lightrail-front::before { content: "\f81f"; } +.bi-truck-front-fill::before { content: "\f820"; } +.bi-truck-front::before { content: "\f821"; } +.bi-ubuntu::before { content: "\f822"; } +.bi-unindent::before { content: "\f823"; } +.bi-unity::before { content: "\f824"; } +.bi-universal-access-circle::before { content: "\f825"; } +.bi-universal-access::before { content: "\f826"; } +.bi-virus::before { content: "\f827"; } +.bi-virus2::before { content: "\f828"; } +.bi-wechat::before { content: "\f829"; } +.bi-yelp::before { content: "\f82a"; } +.bi-sign-stop-fill::before { content: "\f82b"; } +.bi-sign-stop-lights-fill::before { content: "\f82c"; } +.bi-sign-stop-lights::before { content: "\f82d"; } +.bi-sign-stop::before { content: "\f82e"; } +.bi-sign-turn-left-fill::before { content: "\f82f"; } +.bi-sign-turn-left::before { content: "\f830"; } +.bi-sign-turn-right-fill::before { content: "\f831"; } +.bi-sign-turn-right::before { content: "\f832"; } +.bi-sign-turn-slight-left-fill::before { content: "\f833"; } +.bi-sign-turn-slight-left::before { content: "\f834"; } +.bi-sign-turn-slight-right-fill::before { content: "\f835"; } +.bi-sign-turn-slight-right::before { content: "\f836"; } +.bi-sign-yield-fill::before { content: "\f837"; } +.bi-sign-yield::before { content: "\f838"; } +.bi-ev-station-fill::before { content: "\f839"; } +.bi-ev-station::before { content: "\f83a"; } +.bi-fuel-pump-diesel-fill::before { content: "\f83b"; } +.bi-fuel-pump-diesel::before { content: "\f83c"; } +.bi-fuel-pump-fill::before { content: "\f83d"; } +.bi-fuel-pump::before { content: "\f83e"; } +.bi-0-circle-fill::before { content: "\f83f"; } +.bi-0-circle::before { content: "\f840"; } +.bi-0-square-fill::before { content: "\f841"; } +.bi-0-square::before { content: "\f842"; } +.bi-rocket-fill::before { content: "\f843"; } +.bi-rocket-takeoff-fill::before { content: "\f844"; } +.bi-rocket-takeoff::before { content: "\f845"; } +.bi-rocket::before { content: "\f846"; } +.bi-stripe::before { content: "\f847"; } +.bi-subscript::before { content: "\f848"; } +.bi-superscript::before { content: "\f849"; } +.bi-trello::before { content: "\f84a"; } +.bi-envelope-at-fill::before { content: "\f84b"; } +.bi-envelope-at::before { content: "\f84c"; } +.bi-regex::before { content: "\f84d"; } +.bi-text-wrap::before { content: "\f84e"; } +.bi-sign-dead-end-fill::before { content: "\f84f"; } +.bi-sign-dead-end::before { content: "\f850"; } +.bi-sign-do-not-enter-fill::before { content: "\f851"; } +.bi-sign-do-not-enter::before { content: "\f852"; } +.bi-sign-intersection-fill::before { content: "\f853"; } +.bi-sign-intersection-side-fill::before { content: "\f854"; } +.bi-sign-intersection-side::before { content: "\f855"; } +.bi-sign-intersection-t-fill::before { content: "\f856"; } +.bi-sign-intersection-t::before { content: "\f857"; } +.bi-sign-intersection-y-fill::before { content: "\f858"; } +.bi-sign-intersection-y::before { content: "\f859"; } +.bi-sign-intersection::before { content: "\f85a"; } +.bi-sign-merge-left-fill::before { content: "\f85b"; } +.bi-sign-merge-left::before { content: "\f85c"; } +.bi-sign-merge-right-fill::before { content: "\f85d"; } +.bi-sign-merge-right::before { content: "\f85e"; } +.bi-sign-no-left-turn-fill::before { content: "\f85f"; } +.bi-sign-no-left-turn::before { content: "\f860"; } +.bi-sign-no-parking-fill::before { content: "\f861"; } +.bi-sign-no-parking::before { content: "\f862"; } +.bi-sign-no-right-turn-fill::before { content: "\f863"; } +.bi-sign-no-right-turn::before { content: "\f864"; } +.bi-sign-railroad-fill::before { content: "\f865"; } +.bi-sign-railroad::before { content: "\f866"; } +.bi-building-add::before { content: "\f867"; } +.bi-building-check::before { content: "\f868"; } +.bi-building-dash::before { content: "\f869"; } +.bi-building-down::before { content: "\f86a"; } +.bi-building-exclamation::before { content: "\f86b"; } +.bi-building-fill-add::before { content: "\f86c"; } +.bi-building-fill-check::before { content: "\f86d"; } +.bi-building-fill-dash::before { content: "\f86e"; } +.bi-building-fill-down::before { content: "\f86f"; } +.bi-building-fill-exclamation::before { content: "\f870"; } +.bi-building-fill-gear::before { content: "\f871"; } +.bi-building-fill-lock::before { content: "\f872"; } +.bi-building-fill-slash::before { content: "\f873"; } +.bi-building-fill-up::before { content: "\f874"; } +.bi-building-fill-x::before { content: "\f875"; } +.bi-building-fill::before { content: "\f876"; } +.bi-building-gear::before { content: "\f877"; } +.bi-building-lock::before { content: "\f878"; } +.bi-building-slash::before { content: "\f879"; } +.bi-building-up::before { content: "\f87a"; } +.bi-building-x::before { content: "\f87b"; } +.bi-buildings-fill::before { content: "\f87c"; } +.bi-buildings::before { content: "\f87d"; } +.bi-bus-front-fill::before { content: "\f87e"; } +.bi-bus-front::before { content: "\f87f"; } +.bi-ev-front-fill::before { content: "\f880"; } +.bi-ev-front::before { content: "\f881"; } +.bi-globe-americas::before { content: "\f882"; } +.bi-globe-asia-australia::before { content: "\f883"; } +.bi-globe-central-south-asia::before { content: "\f884"; } +.bi-globe-europe-africa::before { content: "\f885"; } +.bi-house-add-fill::before { content: "\f886"; } +.bi-house-add::before { content: "\f887"; } +.bi-house-check-fill::before { content: "\f888"; } +.bi-house-check::before { content: "\f889"; } +.bi-house-dash-fill::before { content: "\f88a"; } +.bi-house-dash::before { content: "\f88b"; } +.bi-house-down-fill::before { content: "\f88c"; } +.bi-house-down::before { content: "\f88d"; } +.bi-house-exclamation-fill::before { content: "\f88e"; } +.bi-house-exclamation::before { content: "\f88f"; } +.bi-house-gear-fill::before { content: "\f890"; } +.bi-house-gear::before { content: "\f891"; } +.bi-house-lock-fill::before { content: "\f892"; } +.bi-house-lock::before { content: "\f893"; } +.bi-house-slash-fill::before { content: "\f894"; } +.bi-house-slash::before { content: "\f895"; } +.bi-house-up-fill::before { content: "\f896"; } +.bi-house-up::before { content: "\f897"; } +.bi-house-x-fill::before { content: "\f898"; } +.bi-house-x::before { content: "\f899"; } +.bi-person-add::before { content: "\f89a"; } +.bi-person-down::before { content: "\f89b"; } +.bi-person-exclamation::before { content: "\f89c"; } +.bi-person-fill-add::before { content: "\f89d"; } +.bi-person-fill-check::before { content: "\f89e"; } +.bi-person-fill-dash::before { content: "\f89f"; } +.bi-person-fill-down::before { content: "\f8a0"; } +.bi-person-fill-exclamation::before { content: "\f8a1"; } +.bi-person-fill-gear::before { content: "\f8a2"; } +.bi-person-fill-lock::before { content: "\f8a3"; } +.bi-person-fill-slash::before { content: "\f8a4"; } +.bi-person-fill-up::before { content: "\f8a5"; } +.bi-person-fill-x::before { content: "\f8a6"; } +.bi-person-gear::before { content: "\f8a7"; } +.bi-person-lock::before { content: "\f8a8"; } +.bi-person-slash::before { content: "\f8a9"; } +.bi-person-up::before { content: "\f8aa"; } +.bi-scooter::before { content: "\f8ab"; } +.bi-taxi-front-fill::before { content: "\f8ac"; } +.bi-taxi-front::before { content: "\f8ad"; } +.bi-amd::before { content: "\f8ae"; } +.bi-database-add::before { content: "\f8af"; } +.bi-database-check::before { content: "\f8b0"; } +.bi-database-dash::before { content: "\f8b1"; } +.bi-database-down::before { content: "\f8b2"; } +.bi-database-exclamation::before { content: "\f8b3"; } +.bi-database-fill-add::before { content: "\f8b4"; } +.bi-database-fill-check::before { content: "\f8b5"; } +.bi-database-fill-dash::before { content: "\f8b6"; } +.bi-database-fill-down::before { content: "\f8b7"; } +.bi-database-fill-exclamation::before { content: "\f8b8"; } +.bi-database-fill-gear::before { content: "\f8b9"; } +.bi-database-fill-lock::before { content: "\f8ba"; } +.bi-database-fill-slash::before { content: "\f8bb"; } +.bi-database-fill-up::before { content: "\f8bc"; } +.bi-database-fill-x::before { content: "\f8bd"; } +.bi-database-fill::before { content: "\f8be"; } +.bi-database-gear::before { content: "\f8bf"; } +.bi-database-lock::before { content: "\f8c0"; } +.bi-database-slash::before { content: "\f8c1"; } +.bi-database-up::before { content: "\f8c2"; } +.bi-database-x::before { content: "\f8c3"; } +.bi-database::before { content: "\f8c4"; } +.bi-houses-fill::before { content: "\f8c5"; } +.bi-houses::before { content: "\f8c6"; } +.bi-nvidia::before { content: "\f8c7"; } +.bi-person-vcard-fill::before { content: "\f8c8"; } +.bi-person-vcard::before { content: "\f8c9"; } +.bi-sina-weibo::before { content: "\f8ca"; } +.bi-tencent-qq::before { content: "\f8cb"; } +.bi-wikipedia::before { content: "\f8cc"; } \ No newline at end of file diff --git a/frontend/public/static/error.html b/frontend/public/static/error.html new file mode 100644 index 0000000..c7ed310 --- /dev/null +++ b/frontend/public/static/error.html @@ -0,0 +1,94 @@ + + + + + + + + SDL DEMO + + + + +
+
+
+ +
+
+

No response from server

+

try again

+
+
+
+
+
+ © Samsung +
+
+ + diff --git a/frontend/public/static/favicon.ico b/frontend/public/static/favicon.ico new file mode 100644 index 0000000..bca40d4 Binary files /dev/null and b/frontend/public/static/favicon.ico differ diff --git a/frontend/public/static/fonts/bootstrap-icons.woff b/frontend/public/static/fonts/bootstrap-icons.woff new file mode 100644 index 0000000..bfb8665 Binary files /dev/null and b/frontend/public/static/fonts/bootstrap-icons.woff differ diff --git a/frontend/public/static/fonts/bootstrap-icons.woff2 b/frontend/public/static/fonts/bootstrap-icons.woff2 new file mode 100644 index 0000000..4df0df2 Binary files /dev/null and b/frontend/public/static/fonts/bootstrap-icons.woff2 differ diff --git a/frontend/public/static/image/main/grainy-gradients.jpg b/frontend/public/static/image/main/grainy-gradients.jpg new file mode 100644 index 0000000..6f2b678 Binary files /dev/null and b/frontend/public/static/image/main/grainy-gradients.jpg differ diff --git a/frontend/public/static/image/main/logo_itvoc.png b/frontend/public/static/image/main/logo_itvoc.png new file mode 100644 index 0000000..12f535a Binary files /dev/null and b/frontend/public/static/image/main/logo_itvoc.png differ diff --git a/frontend/public/static/image/main/main_object1.png b/frontend/public/static/image/main/main_object1.png new file mode 100644 index 0000000..1a2ca47 Binary files /dev/null and b/frontend/public/static/image/main/main_object1.png differ diff --git a/frontend/public/static/image/main/simbol.png b/frontend/public/static/image/main/simbol.png new file mode 100644 index 0000000..f4ab958 Binary files /dev/null and b/frontend/public/static/image/main/simbol.png differ diff --git a/frontend/public/static/js/jszip.min.js b/frontend/public/static/js/jszip.min.js new file mode 100644 index 0000000..ff4cfd5 --- /dev/null +++ b/frontend/public/static/js/jszip.min.js @@ -0,0 +1,13 @@ +/*! + +JSZip v3.10.1 - A JavaScript class for generating and reading zip files + + +(c) 2009-2016 Stuart Knightley +Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/main/LICENSE.markdown. + +JSZip uses the library pako released under the MIT license : +https://github.com/nodeca/pako/blob/main/LICENSE +*/ + +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).JSZip=e()}}(function(){return function s(a,o,h){function u(r,e){if(!o[r]){if(!a[r]){var t="function"==typeof require&&require;if(!e&&t)return t(r,!0);if(l)return l(r,!0);var n=new Error("Cannot find module '"+r+"'");throw n.code="MODULE_NOT_FOUND",n}var i=o[r]={exports:{}};a[r][0].call(i.exports,function(e){var t=a[r][1][e];return u(t||e)},i,i.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,e=0;e>2,s=(3&t)<<4|r>>4,a=1>6:64,o=2>4,r=(15&i)<<4|(s=p.indexOf(e.charAt(o++)))>>2,n=(3&s)<<6|(a=p.indexOf(e.charAt(o++))),l[h++]=t,64!==s&&(l[h++]=r),64!==a&&(l[h++]=n);return l}},{"./support":30,"./utils":32}],2:[function(e,t,r){"use strict";var n=e("./external"),i=e("./stream/DataWorker"),s=e("./stream/Crc32Probe"),a=e("./stream/DataLengthProbe");function o(e,t,r,n,i){this.compressedSize=e,this.uncompressedSize=t,this.crc32=r,this.compression=n,this.compressedContent=i}o.prototype={getContentWorker:function(){var e=new i(n.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new a("data_length")),t=this;return e.on("end",function(){if(this.streamInfo.data_length!==t.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),e},getCompressedWorker:function(){return new i(n.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(e,t,r){return e.pipe(new s).pipe(new a("uncompressedSize")).pipe(t.compressWorker(r)).pipe(new a("compressedSize")).withStreamInfo("compression",t)},t.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(e,t,r){"use strict";var n=e("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(){return new n("STORE compression")},uncompressWorker:function(){return new n("STORE decompression")}},r.DEFLATE=e("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(e,t,r){"use strict";var n=e("./utils");var o=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t){return void 0!==e&&e.length?"string"!==n.getTypeOf(e)?function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t[a])];return-1^e}(0|t,e,e.length,0):function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t.charCodeAt(a))];return-1^e}(0|t,e,e.length,0):0}},{"./utils":32}],5:[function(e,t,r){"use strict";r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(e,t,r){"use strict";var n=null;n="undefined"!=typeof Promise?Promise:e("lie"),t.exports={Promise:n}},{lie:37}],7:[function(e,t,r){"use strict";var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,i=e("pako"),s=e("./utils"),a=e("./stream/GenericWorker"),o=n?"uint8array":"array";function h(e,t){a.call(this,"FlateWorker/"+e),this._pako=null,this._pakoAction=e,this._pakoOptions=t,this.meta={}}r.magic="\b\0",s.inherits(h,a),h.prototype.processChunk=function(e){this.meta=e.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,e.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new i[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var t=this;this._pako.onData=function(e){t.push({data:e,meta:t.meta})}},r.compressWorker=function(e){return new h("Deflate",e)},r.uncompressWorker=function(){return new h("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(e,t,r){"use strict";function A(e,t){var r,n="";for(r=0;r>>=8;return n}function n(e,t,r,n,i,s){var a,o,h=e.file,u=e.compression,l=s!==O.utf8encode,f=I.transformTo("string",s(h.name)),c=I.transformTo("string",O.utf8encode(h.name)),d=h.comment,p=I.transformTo("string",s(d)),m=I.transformTo("string",O.utf8encode(d)),_=c.length!==h.name.length,g=m.length!==d.length,b="",v="",y="",w=h.dir,k=h.date,x={crc32:0,compressedSize:0,uncompressedSize:0};t&&!r||(x.crc32=e.crc32,x.compressedSize=e.compressedSize,x.uncompressedSize=e.uncompressedSize);var S=0;t&&(S|=8),l||!_&&!g||(S|=2048);var z=0,C=0;w&&(z|=16),"UNIX"===i?(C=798,z|=function(e,t){var r=e;return e||(r=t?16893:33204),(65535&r)<<16}(h.unixPermissions,w)):(C=20,z|=function(e){return 63&(e||0)}(h.dosPermissions)),a=k.getUTCHours(),a<<=6,a|=k.getUTCMinutes(),a<<=5,a|=k.getUTCSeconds()/2,o=k.getUTCFullYear()-1980,o<<=4,o|=k.getUTCMonth()+1,o<<=5,o|=k.getUTCDate(),_&&(v=A(1,1)+A(B(f),4)+c,b+="up"+A(v.length,2)+v),g&&(y=A(1,1)+A(B(p),4)+m,b+="uc"+A(y.length,2)+y);var E="";return E+="\n\0",E+=A(S,2),E+=u.magic,E+=A(a,2),E+=A(o,2),E+=A(x.crc32,4),E+=A(x.compressedSize,4),E+=A(x.uncompressedSize,4),E+=A(f.length,2),E+=A(b.length,2),{fileRecord:R.LOCAL_FILE_HEADER+E+f+b,dirRecord:R.CENTRAL_FILE_HEADER+A(C,2)+E+A(p.length,2)+"\0\0\0\0"+A(z,4)+A(n,4)+f+b+p}}var I=e("../utils"),i=e("../stream/GenericWorker"),O=e("../utf8"),B=e("../crc32"),R=e("../signature");function s(e,t,r,n){i.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=t,this.zipPlatform=r,this.encodeFileName=n,this.streamFiles=e,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}I.inherits(s,i),s.prototype.push=function(e){var t=e.meta.percent||0,r=this.entriesCount,n=this._sources.length;this.accumulate?this.contentBuffer.push(e):(this.bytesWritten+=e.data.length,i.prototype.push.call(this,{data:e.data,meta:{currentFile:this.currentFile,percent:r?(t+100*(r-n-1))/r:100}}))},s.prototype.openedSource=function(e){this.currentSourceOffset=this.bytesWritten,this.currentFile=e.file.name;var t=this.streamFiles&&!e.file.dir;if(t){var r=n(e,t,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:r.fileRecord,meta:{percent:0}})}else this.accumulate=!0},s.prototype.closedSource=function(e){this.accumulate=!1;var t=this.streamFiles&&!e.file.dir,r=n(e,t,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(r.dirRecord),t)this.push({data:function(e){return R.DATA_DESCRIPTOR+A(e.crc32,4)+A(e.compressedSize,4)+A(e.uncompressedSize,4)}(e),meta:{percent:100}});else for(this.push({data:r.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},s.prototype.flush=function(){for(var e=this.bytesWritten,t=0;t=this.index;t--)r=(r<<8)+this.byteAt(t);return this.index+=e,r},readString:function(e){return n.transformTo("string",this.readData(e))},readData:function(){},lastIndexOfSignature:function(){},readAndCheckSignature:function(){},readDate:function(){var e=this.readInt(4);return new Date(Date.UTC(1980+(e>>25&127),(e>>21&15)-1,e>>16&31,e>>11&31,e>>5&63,(31&e)<<1))}},t.exports=i},{"../utils":32}],19:[function(e,t,r){"use strict";var n=e("./Uint8ArrayReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.readData=function(e){this.checkOffset(e);var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(e,t,r){"use strict";var n=e("./DataReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.byteAt=function(e){return this.data.charCodeAt(this.zero+e)},i.prototype.lastIndexOfSignature=function(e){return this.data.lastIndexOf(e)-this.zero},i.prototype.readAndCheckSignature=function(e){return e===this.readData(4)},i.prototype.readData=function(e){this.checkOffset(e);var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./DataReader":18}],21:[function(e,t,r){"use strict";var n=e("./ArrayReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.readData=function(e){if(this.checkOffset(e),0===e)return new Uint8Array(0);var t=this.data.subarray(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./ArrayReader":17}],22:[function(e,t,r){"use strict";var n=e("../utils"),i=e("../support"),s=e("./ArrayReader"),a=e("./StringReader"),o=e("./NodeBufferReader"),h=e("./Uint8ArrayReader");t.exports=function(e){var t=n.getTypeOf(e);return n.checkSupport(t),"string"!==t||i.uint8array?"nodebuffer"===t?new o(e):i.uint8array?new h(n.transformTo("uint8array",e)):new s(n.transformTo("array",e)):new a(e)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(e,t,r){"use strict";r.LOCAL_FILE_HEADER="PK",r.CENTRAL_FILE_HEADER="PK",r.CENTRAL_DIRECTORY_END="PK",r.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK",r.ZIP64_CENTRAL_DIRECTORY_END="PK",r.DATA_DESCRIPTOR="PK\b"},{}],24:[function(e,t,r){"use strict";var n=e("./GenericWorker"),i=e("../utils");function s(e){n.call(this,"ConvertWorker to "+e),this.destType=e}i.inherits(s,n),s.prototype.processChunk=function(e){this.push({data:i.transformTo(this.destType,e.data),meta:e.meta})},t.exports=s},{"../utils":32,"./GenericWorker":28}],25:[function(e,t,r){"use strict";var n=e("./GenericWorker"),i=e("../crc32");function s(){n.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}e("../utils").inherits(s,n),s.prototype.processChunk=function(e){this.streamInfo.crc32=i(e.data,this.streamInfo.crc32||0),this.push(e)},t.exports=s},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(e,t,r){"use strict";var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataLengthProbe for "+e),this.propName=e,this.withStreamInfo(e,0)}n.inherits(s,i),s.prototype.processChunk=function(e){if(e){var t=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=t+e.data.length}i.prototype.processChunk.call(this,e)},t.exports=s},{"../utils":32,"./GenericWorker":28}],27:[function(e,t,r){"use strict";var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataWorker");var t=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,e.then(function(e){t.dataIsReady=!0,t.data=e,t.max=e&&e.length||0,t.type=n.getTypeOf(e),t.isPaused||t._tickAndRepeat()},function(e){t.error(e)})}n.inherits(s,i),s.prototype.cleanUp=function(){i.prototype.cleanUp.call(this),this.data=null},s.prototype.resume=function(){return!!i.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,n.delay(this._tickAndRepeat,[],this)),!0)},s.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(n.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},s.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var e=null,t=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":e=this.data.substring(this.index,t);break;case"uint8array":e=this.data.subarray(this.index,t);break;case"array":case"nodebuffer":e=this.data.slice(this.index,t)}return this.index=t,this.push({data:e,meta:{percent:this.max?this.index/this.max*100:0}})},t.exports=s},{"../utils":32,"./GenericWorker":28}],28:[function(e,t,r){"use strict";function n(e){this.name=e||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}n.prototype={push:function(e){this.emit("data",e)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(e){this.emit("error",e)}return!0},error:function(e){return!this.isFinished&&(this.isPaused?this.generatedError=e:(this.isFinished=!0,this.emit("error",e),this.previous&&this.previous.error(e),this.cleanUp()),!0)},on:function(e,t){return this._listeners[e].push(t),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(e,t){if(this._listeners[e])for(var r=0;r "+e:e}},t.exports=n},{}],29:[function(e,t,r){"use strict";var h=e("../utils"),i=e("./ConvertWorker"),s=e("./GenericWorker"),u=e("../base64"),n=e("../support"),a=e("../external"),o=null;if(n.nodestream)try{o=e("../nodejs/NodejsStreamOutputAdapter")}catch(e){}function l(e,o){return new a.Promise(function(t,r){var n=[],i=e._internalType,s=e._outputType,a=e._mimeType;e.on("data",function(e,t){n.push(e),o&&o(t)}).on("error",function(e){n=[],r(e)}).on("end",function(){try{var e=function(e,t,r){switch(e){case"blob":return h.newBlob(h.transformTo("arraybuffer",t),r);case"base64":return u.encode(t);default:return h.transformTo(e,t)}}(s,function(e,t){var r,n=0,i=null,s=0;for(r=0;r>>6:(r<65536?t[s++]=224|r>>>12:(t[s++]=240|r>>>18,t[s++]=128|r>>>12&63),t[s++]=128|r>>>6&63),t[s++]=128|63&r);return t}(e)},s.utf8decode=function(e){return h.nodebuffer?o.transformTo("nodebuffer",e).toString("utf-8"):function(e){var t,r,n,i,s=e.length,a=new Array(2*s);for(t=r=0;t>10&1023,a[r++]=56320|1023&n)}return a.length!==r&&(a.subarray?a=a.subarray(0,r):a.length=r),o.applyFromCharCode(a)}(e=o.transformTo(h.uint8array?"uint8array":"array",e))},o.inherits(a,n),a.prototype.processChunk=function(e){var t=o.transformTo(h.uint8array?"uint8array":"array",e.data);if(this.leftOver&&this.leftOver.length){if(h.uint8array){var r=t;(t=new Uint8Array(r.length+this.leftOver.length)).set(this.leftOver,0),t.set(r,this.leftOver.length)}else t=this.leftOver.concat(t);this.leftOver=null}var n=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;0<=r&&128==(192&e[r]);)r--;return r<0?t:0===r?t:r+u[e[r]]>t?r:t}(t),i=t;n!==t.length&&(h.uint8array?(i=t.subarray(0,n),this.leftOver=t.subarray(n,t.length)):(i=t.slice(0,n),this.leftOver=t.slice(n,t.length))),this.push({data:s.utf8decode(i),meta:e.meta})},a.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:s.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},s.Utf8DecodeWorker=a,o.inherits(l,n),l.prototype.processChunk=function(e){this.push({data:s.utf8encode(e.data),meta:e.meta})},s.Utf8EncodeWorker=l},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(e,t,a){"use strict";var o=e("./support"),h=e("./base64"),r=e("./nodejsUtils"),u=e("./external");function n(e){return e}function l(e,t){for(var r=0;r>8;this.dir=!!(16&this.externalFileAttributes),0==e&&(this.dosPermissions=63&this.externalFileAttributes),3==e&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||"/"!==this.fileNameStr.slice(-1)||(this.dir=!0)},parseZIP64ExtraField:function(){if(this.extraFields[1]){var e=n(this.extraFields[1].value);this.uncompressedSize===s.MAX_VALUE_32BITS&&(this.uncompressedSize=e.readInt(8)),this.compressedSize===s.MAX_VALUE_32BITS&&(this.compressedSize=e.readInt(8)),this.localHeaderOffset===s.MAX_VALUE_32BITS&&(this.localHeaderOffset=e.readInt(8)),this.diskNumberStart===s.MAX_VALUE_32BITS&&(this.diskNumberStart=e.readInt(4))}},readExtraFields:function(e){var t,r,n,i=e.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});e.index+4>>6:(r<65536?t[s++]=224|r>>>12:(t[s++]=240|r>>>18,t[s++]=128|r>>>12&63),t[s++]=128|r>>>6&63),t[s++]=128|63&r);return t},r.buf2binstring=function(e){return l(e,e.length)},r.binstring2buf=function(e){for(var t=new h.Buf8(e.length),r=0,n=t.length;r>10&1023,o[n++]=56320|1023&i)}return l(o,n)},r.utf8border=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;0<=r&&128==(192&e[r]);)r--;return r<0?t:0===r?t:r+u[e[r]]>t?r:t}},{"./common":41}],43:[function(e,t,r){"use strict";t.exports=function(e,t,r,n){for(var i=65535&e|0,s=e>>>16&65535|0,a=0;0!==r;){for(r-=a=2e3>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t[a])];return-1^e}},{}],46:[function(e,t,r){"use strict";var h,c=e("../utils/common"),u=e("./trees"),d=e("./adler32"),p=e("./crc32"),n=e("./messages"),l=0,f=4,m=0,_=-2,g=-1,b=4,i=2,v=8,y=9,s=286,a=30,o=19,w=2*s+1,k=15,x=3,S=258,z=S+x+1,C=42,E=113,A=1,I=2,O=3,B=4;function R(e,t){return e.msg=n[t],t}function T(e){return(e<<1)-(4e.avail_out&&(r=e.avail_out),0!==r&&(c.arraySet(e.output,t.pending_buf,t.pending_out,r,e.next_out),e.next_out+=r,t.pending_out+=r,e.total_out+=r,e.avail_out-=r,t.pending-=r,0===t.pending&&(t.pending_out=0))}function N(e,t){u._tr_flush_block(e,0<=e.block_start?e.block_start:-1,e.strstart-e.block_start,t),e.block_start=e.strstart,F(e.strm)}function U(e,t){e.pending_buf[e.pending++]=t}function P(e,t){e.pending_buf[e.pending++]=t>>>8&255,e.pending_buf[e.pending++]=255&t}function L(e,t){var r,n,i=e.max_chain_length,s=e.strstart,a=e.prev_length,o=e.nice_match,h=e.strstart>e.w_size-z?e.strstart-(e.w_size-z):0,u=e.window,l=e.w_mask,f=e.prev,c=e.strstart+S,d=u[s+a-1],p=u[s+a];e.prev_length>=e.good_match&&(i>>=2),o>e.lookahead&&(o=e.lookahead);do{if(u[(r=t)+a]===p&&u[r+a-1]===d&&u[r]===u[s]&&u[++r]===u[s+1]){s+=2,r++;do{}while(u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&sh&&0!=--i);return a<=e.lookahead?a:e.lookahead}function j(e){var t,r,n,i,s,a,o,h,u,l,f=e.w_size;do{if(i=e.window_size-e.lookahead-e.strstart,e.strstart>=f+(f-z)){for(c.arraySet(e.window,e.window,f,f,0),e.match_start-=f,e.strstart-=f,e.block_start-=f,t=r=e.hash_size;n=e.head[--t],e.head[t]=f<=n?n-f:0,--r;);for(t=r=f;n=e.prev[--t],e.prev[t]=f<=n?n-f:0,--r;);i+=f}if(0===e.strm.avail_in)break;if(a=e.strm,o=e.window,h=e.strstart+e.lookahead,u=i,l=void 0,l=a.avail_in,u=x)for(s=e.strstart-e.insert,e.ins_h=e.window[s],e.ins_h=(e.ins_h<=x&&(e.ins_h=(e.ins_h<=x)if(n=u._tr_tally(e,e.strstart-e.match_start,e.match_length-x),e.lookahead-=e.match_length,e.match_length<=e.max_lazy_match&&e.lookahead>=x){for(e.match_length--;e.strstart++,e.ins_h=(e.ins_h<=x&&(e.ins_h=(e.ins_h<=x&&e.match_length<=e.prev_length){for(i=e.strstart+e.lookahead-x,n=u._tr_tally(e,e.strstart-1-e.prev_match,e.prev_length-x),e.lookahead-=e.prev_length-1,e.prev_length-=2;++e.strstart<=i&&(e.ins_h=(e.ins_h<e.pending_buf_size-5&&(r=e.pending_buf_size-5);;){if(e.lookahead<=1){if(j(e),0===e.lookahead&&t===l)return A;if(0===e.lookahead)break}e.strstart+=e.lookahead,e.lookahead=0;var n=e.block_start+r;if((0===e.strstart||e.strstart>=n)&&(e.lookahead=e.strstart-n,e.strstart=n,N(e,!1),0===e.strm.avail_out))return A;if(e.strstart-e.block_start>=e.w_size-z&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):(e.strstart>e.block_start&&(N(e,!1),e.strm.avail_out),A)}),new M(4,4,8,4,Z),new M(4,5,16,8,Z),new M(4,6,32,32,Z),new M(4,4,16,16,W),new M(8,16,32,32,W),new M(8,16,128,128,W),new M(8,32,128,256,W),new M(32,128,258,1024,W),new M(32,258,258,4096,W)],r.deflateInit=function(e,t){return Y(e,t,v,15,8,0)},r.deflateInit2=Y,r.deflateReset=K,r.deflateResetKeep=G,r.deflateSetHeader=function(e,t){return e&&e.state?2!==e.state.wrap?_:(e.state.gzhead=t,m):_},r.deflate=function(e,t){var r,n,i,s;if(!e||!e.state||5>8&255),U(n,n.gzhead.time>>16&255),U(n,n.gzhead.time>>24&255),U(n,9===n.level?2:2<=n.strategy||n.level<2?4:0),U(n,255&n.gzhead.os),n.gzhead.extra&&n.gzhead.extra.length&&(U(n,255&n.gzhead.extra.length),U(n,n.gzhead.extra.length>>8&255)),n.gzhead.hcrc&&(e.adler=p(e.adler,n.pending_buf,n.pending,0)),n.gzindex=0,n.status=69):(U(n,0),U(n,0),U(n,0),U(n,0),U(n,0),U(n,9===n.level?2:2<=n.strategy||n.level<2?4:0),U(n,3),n.status=E);else{var a=v+(n.w_bits-8<<4)<<8;a|=(2<=n.strategy||n.level<2?0:n.level<6?1:6===n.level?2:3)<<6,0!==n.strstart&&(a|=32),a+=31-a%31,n.status=E,P(n,a),0!==n.strstart&&(P(n,e.adler>>>16),P(n,65535&e.adler)),e.adler=1}if(69===n.status)if(n.gzhead.extra){for(i=n.pending;n.gzindex<(65535&n.gzhead.extra.length)&&(n.pending!==n.pending_buf_size||(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending!==n.pending_buf_size));)U(n,255&n.gzhead.extra[n.gzindex]),n.gzindex++;n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),n.gzindex===n.gzhead.extra.length&&(n.gzindex=0,n.status=73)}else n.status=73;if(73===n.status)if(n.gzhead.name){i=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending===n.pending_buf_size)){s=1;break}s=n.gzindexi&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),0===s&&(n.gzindex=0,n.status=91)}else n.status=91;if(91===n.status)if(n.gzhead.comment){i=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending===n.pending_buf_size)){s=1;break}s=n.gzindexi&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),0===s&&(n.status=103)}else n.status=103;if(103===n.status&&(n.gzhead.hcrc?(n.pending+2>n.pending_buf_size&&F(e),n.pending+2<=n.pending_buf_size&&(U(n,255&e.adler),U(n,e.adler>>8&255),e.adler=0,n.status=E)):n.status=E),0!==n.pending){if(F(e),0===e.avail_out)return n.last_flush=-1,m}else if(0===e.avail_in&&T(t)<=T(r)&&t!==f)return R(e,-5);if(666===n.status&&0!==e.avail_in)return R(e,-5);if(0!==e.avail_in||0!==n.lookahead||t!==l&&666!==n.status){var o=2===n.strategy?function(e,t){for(var r;;){if(0===e.lookahead&&(j(e),0===e.lookahead)){if(t===l)return A;break}if(e.match_length=0,r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++,r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):3===n.strategy?function(e,t){for(var r,n,i,s,a=e.window;;){if(e.lookahead<=S){if(j(e),e.lookahead<=S&&t===l)return A;if(0===e.lookahead)break}if(e.match_length=0,e.lookahead>=x&&0e.lookahead&&(e.match_length=e.lookahead)}if(e.match_length>=x?(r=u._tr_tally(e,1,e.match_length-x),e.lookahead-=e.match_length,e.strstart+=e.match_length,e.match_length=0):(r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++),r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):h[n.level].func(n,t);if(o!==O&&o!==B||(n.status=666),o===A||o===O)return 0===e.avail_out&&(n.last_flush=-1),m;if(o===I&&(1===t?u._tr_align(n):5!==t&&(u._tr_stored_block(n,0,0,!1),3===t&&(D(n.head),0===n.lookahead&&(n.strstart=0,n.block_start=0,n.insert=0))),F(e),0===e.avail_out))return n.last_flush=-1,m}return t!==f?m:n.wrap<=0?1:(2===n.wrap?(U(n,255&e.adler),U(n,e.adler>>8&255),U(n,e.adler>>16&255),U(n,e.adler>>24&255),U(n,255&e.total_in),U(n,e.total_in>>8&255),U(n,e.total_in>>16&255),U(n,e.total_in>>24&255)):(P(n,e.adler>>>16),P(n,65535&e.adler)),F(e),0=r.w_size&&(0===s&&(D(r.head),r.strstart=0,r.block_start=0,r.insert=0),u=new c.Buf8(r.w_size),c.arraySet(u,t,l-r.w_size,r.w_size,0),t=u,l=r.w_size),a=e.avail_in,o=e.next_in,h=e.input,e.avail_in=l,e.next_in=0,e.input=t,j(r);r.lookahead>=x;){for(n=r.strstart,i=r.lookahead-(x-1);r.ins_h=(r.ins_h<>>=y=v>>>24,p-=y,0===(y=v>>>16&255))C[s++]=65535&v;else{if(!(16&y)){if(0==(64&y)){v=m[(65535&v)+(d&(1<>>=y,p-=y),p<15&&(d+=z[n++]<>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(d&(1<>>=y,p-=y,(y=s-a)>3,d&=(1<<(p-=w<<3))-1,e.next_in=n,e.next_out=s,e.avail_in=n>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24)}function s(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new I.Buf16(320),this.work=new I.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function a(e){var t;return e&&e.state?(t=e.state,e.total_in=e.total_out=t.total=0,e.msg="",t.wrap&&(e.adler=1&t.wrap),t.mode=P,t.last=0,t.havedict=0,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new I.Buf32(n),t.distcode=t.distdyn=new I.Buf32(i),t.sane=1,t.back=-1,N):U}function o(e){var t;return e&&e.state?((t=e.state).wsize=0,t.whave=0,t.wnext=0,a(e)):U}function h(e,t){var r,n;return e&&e.state?(n=e.state,t<0?(r=0,t=-t):(r=1+(t>>4),t<48&&(t&=15)),t&&(t<8||15=s.wsize?(I.arraySet(s.window,t,r-s.wsize,s.wsize,0),s.wnext=0,s.whave=s.wsize):(n<(i=s.wsize-s.wnext)&&(i=n),I.arraySet(s.window,t,r-n,i,s.wnext),(n-=i)?(I.arraySet(s.window,t,r-n,n,0),s.wnext=n,s.whave=s.wsize):(s.wnext+=i,s.wnext===s.wsize&&(s.wnext=0),s.whave>>8&255,r.check=B(r.check,E,2,0),l=u=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&u)<<8)+(u>>8))%31){e.msg="incorrect header check",r.mode=30;break}if(8!=(15&u)){e.msg="unknown compression method",r.mode=30;break}if(l-=4,k=8+(15&(u>>>=4)),0===r.wbits)r.wbits=k;else if(k>r.wbits){e.msg="invalid window size",r.mode=30;break}r.dmax=1<>8&1),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=3;case 3:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<>>8&255,E[2]=u>>>16&255,E[3]=u>>>24&255,r.check=B(r.check,E,4,0)),l=u=0,r.mode=4;case 4:for(;l<16;){if(0===o)break e;o--,u+=n[s++]<>8),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=5;case 5:if(1024&r.flags){for(;l<16;){if(0===o)break e;o--,u+=n[s++]<>>8&255,r.check=B(r.check,E,2,0)),l=u=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&(o<(d=r.length)&&(d=o),d&&(r.head&&(k=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),I.arraySet(r.head.extra,n,s,d,k)),512&r.flags&&(r.check=B(r.check,n,d,s)),o-=d,s+=d,r.length-=d),r.length))break e;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===o)break e;for(d=0;k=n[s+d++],r.head&&k&&r.length<65536&&(r.head.name+=String.fromCharCode(k)),k&&d>9&1,r.head.done=!0),e.adler=r.check=0,r.mode=12;break;case 10:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break e;o--,u+=n[s++]<>>=1)){case 0:r.mode=14;break;case 1:if(j(r),r.mode=20,6!==t)break;u>>>=2,l-=2;break e;case 2:r.mode=17;break;case 3:e.msg="invalid block type",r.mode=30}u>>>=2,l-=2;break;case 14:for(u>>>=7&l,l-=7&l;l<32;){if(0===o)break e;o--,u+=n[s++]<>>16^65535)){e.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&u,l=u=0,r.mode=15,6===t)break e;case 15:r.mode=16;case 16:if(d=r.length){if(o>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286>>=3,l-=3}for(;r.have<19;)r.lens[A[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=T(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=_,l-=_,r.lens[r.have++]=b;else{if(16===b){for(z=_+2;l>>=_,l-=_,0===r.have){e.msg="invalid bit length repeat",r.mode=30;break}k=r.lens[r.have-1],d=3+(3&u),u>>>=2,l-=2}else if(17===b){for(z=_+3;l>>=_)),u>>>=3,l-=3}else{for(z=_+7;l>>=_)),u>>>=7,l-=7}if(r.have+d>r.nlen+r.ndist){e.msg="invalid bit length repeat",r.mode=30;break}for(;d--;)r.lens[r.have++]=k}}if(30===r.mode)break;if(0===r.lens[256]){e.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=T(D,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=T(F,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){e.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===t)break e;case 20:r.mode=21;case 21:if(6<=o&&258<=h){e.next_out=a,e.avail_out=h,e.next_in=s,e.avail_in=o,r.hold=u,r.bits=l,R(e,c),a=e.next_out,i=e.output,h=e.avail_out,s=e.next_in,n=e.input,o=e.avail_in,u=r.hold,l=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(C=r.lencode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,r.length=b,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){e.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;g=(C=r.distcode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,64&g){e.msg="invalid distance code",r.mode=30;break}r.offset=b,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){e.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break e;if(d=c-h,r.offset>d){if((d=r.offset-d)>r.whave&&r.sane){e.msg="invalid distance too far back",r.mode=30;break}p=d>r.wnext?(d-=r.wnext,r.wsize-d):r.wnext-d,d>r.length&&(d=r.length),m=r.window}else m=i,p=a-r.offset,d=r.length;for(hd?(m=R[T+a[v]],A[I+a[v]]):(m=96,0),h=1<>S)+(u-=h)]=p<<24|m<<16|_|0,0!==u;);for(h=1<>=1;if(0!==h?(E&=h-1,E+=h):E=0,v++,0==--O[b]){if(b===w)break;b=t[r+a[v]]}if(k>>7)]}function U(e,t){e.pending_buf[e.pending++]=255&t,e.pending_buf[e.pending++]=t>>>8&255}function P(e,t,r){e.bi_valid>d-r?(e.bi_buf|=t<>d-e.bi_valid,e.bi_valid+=r-d):(e.bi_buf|=t<>>=1,r<<=1,0<--t;);return r>>>1}function Z(e,t,r){var n,i,s=new Array(g+1),a=0;for(n=1;n<=g;n++)s[n]=a=a+r[n-1]<<1;for(i=0;i<=t;i++){var o=e[2*i+1];0!==o&&(e[2*i]=j(s[o]++,o))}}function W(e){var t;for(t=0;t>1;1<=r;r--)G(e,s,r);for(i=h;r=e.heap[1],e.heap[1]=e.heap[e.heap_len--],G(e,s,1),n=e.heap[1],e.heap[--e.heap_max]=r,e.heap[--e.heap_max]=n,s[2*i]=s[2*r]+s[2*n],e.depth[i]=(e.depth[r]>=e.depth[n]?e.depth[r]:e.depth[n])+1,s[2*r+1]=s[2*n+1]=i,e.heap[1]=i++,G(e,s,1),2<=e.heap_len;);e.heap[--e.heap_max]=e.heap[1],function(e,t){var r,n,i,s,a,o,h=t.dyn_tree,u=t.max_code,l=t.stat_desc.static_tree,f=t.stat_desc.has_stree,c=t.stat_desc.extra_bits,d=t.stat_desc.extra_base,p=t.stat_desc.max_length,m=0;for(s=0;s<=g;s++)e.bl_count[s]=0;for(h[2*e.heap[e.heap_max]+1]=0,r=e.heap_max+1;r<_;r++)p<(s=h[2*h[2*(n=e.heap[r])+1]+1]+1)&&(s=p,m++),h[2*n+1]=s,u>=7;n>>=1)if(1&r&&0!==e.dyn_ltree[2*t])return o;if(0!==e.dyn_ltree[18]||0!==e.dyn_ltree[20]||0!==e.dyn_ltree[26])return h;for(t=32;t>>3,(s=e.static_len+3+7>>>3)<=i&&(i=s)):i=s=r+5,r+4<=i&&-1!==t?J(e,t,r,n):4===e.strategy||s===i?(P(e,2+(n?1:0),3),K(e,z,C)):(P(e,4+(n?1:0),3),function(e,t,r,n){var i;for(P(e,t-257,5),P(e,r-1,5),P(e,n-4,4),i=0;i>>8&255,e.pending_buf[e.d_buf+2*e.last_lit+1]=255&t,e.pending_buf[e.l_buf+e.last_lit]=255&r,e.last_lit++,0===t?e.dyn_ltree[2*r]++:(e.matches++,t--,e.dyn_ltree[2*(A[r]+u+1)]++,e.dyn_dtree[2*N(t)]++),e.last_lit===e.lit_bufsize-1},r._tr_align=function(e){P(e,2,3),L(e,m,z),function(e){16===e.bi_valid?(U(e,e.bi_buf),e.bi_buf=0,e.bi_valid=0):8<=e.bi_valid&&(e.pending_buf[e.pending++]=255&e.bi_buf,e.bi_buf>>=8,e.bi_valid-=8)}(e)}},{"../utils/common":41}],53:[function(e,t,r){"use strict";t.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(e,t,r){(function(e){!function(r,n){"use strict";if(!r.setImmediate){var i,s,t,a,o=1,h={},u=!1,l=r.document,e=Object.getPrototypeOf&&Object.getPrototypeOf(r);e=e&&e.setTimeout?e:r,i="[object process]"==={}.toString.call(r.process)?function(e){process.nextTick(function(){c(e)})}:function(){if(r.postMessage&&!r.importScripts){var e=!0,t=r.onmessage;return r.onmessage=function(){e=!1},r.postMessage("","*"),r.onmessage=t,e}}()?(a="setImmediate$"+Math.random()+"$",r.addEventListener?r.addEventListener("message",d,!1):r.attachEvent("onmessage",d),function(e){r.postMessage(a+e,"*")}):r.MessageChannel?((t=new MessageChannel).port1.onmessage=function(e){c(e.data)},function(e){t.port2.postMessage(e)}):l&&"onreadystatechange"in l.createElement("script")?(s=l.documentElement,function(e){var t=l.createElement("script");t.onreadystatechange=function(){c(e),t.onreadystatechange=null,s.removeChild(t),t=null},s.appendChild(t)}):function(e){setTimeout(c,0,e)},e.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),r=0;r + + + + + + \ No newline at end of file diff --git a/frontend/src/components/common/control/approvalpath/ApprovalPath.vue b/frontend/src/components/common/control/approvalpath/ApprovalPath.vue new file mode 100644 index 0000000..dd5e0c7 --- /dev/null +++ b/frontend/src/components/common/control/approvalpath/ApprovalPath.vue @@ -0,0 +1,859 @@ + + + + diff --git a/frontend/src/components/common/control/breadcrumb/Breadcrumb.vue b/frontend/src/components/common/control/breadcrumb/Breadcrumb.vue new file mode 100644 index 0000000..6ddb0d8 --- /dev/null +++ b/frontend/src/components/common/control/breadcrumb/Breadcrumb.vue @@ -0,0 +1,162 @@ + + + + + + diff --git a/frontend/src/components/common/control/breadcrumb/index.js b/frontend/src/components/common/control/breadcrumb/index.js new file mode 100644 index 0000000..03824ae --- /dev/null +++ b/frontend/src/components/common/control/breadcrumb/index.js @@ -0,0 +1,3 @@ +import SdlBreadcrumb from './Breadcrumb.vue'; + +export default SdlBreadcrumb; diff --git a/frontend/src/components/common/control/datepicker/SdlDatePicker.vue b/frontend/src/components/common/control/datepicker/SdlDatePicker.vue new file mode 100644 index 0000000..5e66996 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/SdlDatePicker.vue @@ -0,0 +1,471 @@ + + + + + diff --git a/frontend/src/components/common/control/datepicker/components/DateInput.vue b/frontend/src/components/common/control/datepicker/components/DateInput.vue new file mode 100644 index 0000000..97bf36d --- /dev/null +++ b/frontend/src/components/common/control/datepicker/components/DateInput.vue @@ -0,0 +1,158 @@ + + diff --git a/frontend/src/components/common/control/datepicker/components/Datepicker.vue b/frontend/src/components/common/control/datepicker/components/Datepicker.vue new file mode 100644 index 0000000..07a052b --- /dev/null +++ b/frontend/src/components/common/control/datepicker/components/Datepicker.vue @@ -0,0 +1,479 @@ + + \ No newline at end of file diff --git a/frontend/src/components/common/control/datepicker/components/PickerDay.vue b/frontend/src/components/common/control/datepicker/components/PickerDay.vue new file mode 100644 index 0000000..d27ee3b --- /dev/null +++ b/frontend/src/components/common/control/datepicker/components/PickerDay.vue @@ -0,0 +1,376 @@ + + diff --git a/frontend/src/components/common/control/datepicker/components/PickerMonth.vue b/frontend/src/components/common/control/datepicker/components/PickerMonth.vue new file mode 100644 index 0000000..52e8f00 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/components/PickerMonth.vue @@ -0,0 +1,201 @@ + + diff --git a/frontend/src/components/common/control/datepicker/components/PickerYear.vue b/frontend/src/components/common/control/datepicker/components/PickerYear.vue new file mode 100644 index 0000000..0875370 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/components/PickerYear.vue @@ -0,0 +1,175 @@ + + diff --git a/frontend/src/components/common/control/datepicker/index.js b/frontend/src/components/common/control/datepicker/index.js new file mode 100644 index 0000000..ebc2cf2 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/index.js @@ -0,0 +1,3 @@ +import SdlDatePicker from './SdlDatePicker.vue'; + +export default SdlDatePicker; diff --git a/frontend/src/components/common/control/datepicker/locale/Language.js b/frontend/src/components/common/control/datepicker/locale/Language.js new file mode 100644 index 0000000..d414fc8 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/Language.js @@ -0,0 +1,57 @@ +export default class Language { + constructor (language, months, monthsAbbr, days) { + this.language = language + this.months = months + this.monthsAbbr = monthsAbbr + this.days = days + this.rtl = false + this.ymd = false + this.yearSuffix = '' + } + + get language () { + return this._language + } + + set language (language) { + if (typeof language !== 'string') { + throw new TypeError('Language must be a string') + } + this._language = language + } + + get months () { + return this._months + } + + set months (months) { + if (months.length !== 12) { + throw new RangeError(`There must be 12 months for ${this.language} language`) + } + this._months = months + } + + get monthsAbbr () { + return this._monthsAbbr + } + + set monthsAbbr (monthsAbbr) { + if (monthsAbbr.length !== 12) { + throw new RangeError(`There must be 12 abbreviated months for ${this.language} language`) + } + this._monthsAbbr = monthsAbbr + } + + get days () { + return this._days + } + + set days (days) { + if (days.length !== 7) { + throw new RangeError(`There must be 7 days for ${this.language} language`) + } + this._days = days + } +} +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/index.js b/frontend/src/components/common/control/datepicker/locale/index.js new file mode 100644 index 0000000..e1fa050 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/index.js @@ -0,0 +1,105 @@ +import af from './translations/af' +import ar from './translations/ar' +import bg from './translations/bg' +import bs from './translations/bs' +import ca from './translations/ca' +import cs from './translations/cs' +import da from './translations/da' +import de from './translations/de' +import ee from './translations/ee' +import el from './translations/el' +import en from './translations/en' +import es from './translations/es' +import fa from './translations/fa' +import fi from './translations/fi' +import fo from './translations/fo' +import fr from './translations/fr' +import ge from './translations/ge' +import gl from './translations/gl' +import he from './translations/he' +import hr from './translations/hr' +import hu from './translations/hu' +import id from './translations/id' +import is from './translations/is' +import it from './translations/it' +import ja from './translations/ja' +import kk from './translations/kk' +import ko from './translations/ko' +import lb from './translations/lb' +import lt from './translations/lt' +import lv from './translations/lv' +import mk from './translations/mk' +import mn from './translations/mn' +import nbNO from './translations/nb-NO' +import nl from './translations/nl' +import pl from './translations/pl' +import ptBR from './translations/pt-BR' +import ro from './translations/ro' +import ru from './translations/ru' +import sk from './translations/sk' +import slSI from './translations/sl-SI' +import srCYRL from './translations/sr-CYRL' +import sr from './translations/sr' +import sv from './translations/sv' +import th from './translations/th' +import tr from './translations/tr' +import uk from './translations/uk' +import ur from './translations/ur' +import vi from './translations/vi' +import zh from './translations/zh' +import zhHK from './translations/zh-HK' + +export { + af, + ar, + bg, + bs, + ca, + cs, + da, + de, + ee, + el, + en, + es, + fa, + fi, + fo, + fr, + ge, + gl, + he, + hr, + hu, + id, + is, + it, + ja, + kk, + ko, + lb, + lt, + lv, + mk, + mn, + nbNO, + nl, + pl, + ptBR, + ro, + ru, + sk, + slSI, + srCYRL, + sr, + sv, + th, + tr, + uk, + ur, + vi, + zh, + zhHK +} +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/af.js b/frontend/src/components/common/control/datepicker/locale/translations/af.js new file mode 100644 index 0000000..666695d --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/af.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Afrikaans', + ['Januarie', 'Februarie', 'Maart', 'April', 'Mei', 'Junie', 'Julie', 'Augustus', 'September', 'Oktober', 'November', 'Desember'], + ['Jan', 'Feb', 'Mrt', 'Apr', 'Mei', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Des'], + ['So.', 'Ma.', 'Di.', 'Wo.', 'Do.', 'Vr.', 'Sa.'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/ar.js b/frontend/src/components/common/control/datepicker/locale/translations/ar.js new file mode 100644 index 0000000..3e669da --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/ar.js @@ -0,0 +1,14 @@ +import Language from '../Language' + +const language = new Language( + 'Arabic', + ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوڤمبر', 'ديسمبر'], + ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوڤمبر', 'ديسمبر'], + ['أحد', 'إثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة', 'سبت'] +) + +language.rtl = true + +export default language +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/bg.js b/frontend/src/components/common/control/datepicker/locale/translations/bg.js new file mode 100644 index 0000000..ee963ba --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/bg.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Bulgarian', + ['Януари', 'Февруари', 'Март', 'Април', 'Май', 'Юни', 'Юли', 'Август', 'Септември', 'Октомври', 'Ноември', 'Декември'], + ['Ян', 'Фев', 'Мар', 'Апр', 'Май', 'Юни', 'Юли', 'Авг', 'Сеп', 'Окт', 'Ное', 'Дек'], + ['Нд', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/bs.js b/frontend/src/components/common/control/datepicker/locale/translations/bs.js new file mode 100644 index 0000000..eadb52b --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/bs.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Bosnian', + ['Januar', 'Februar', 'Mart', 'April', 'Maj', 'Juni', 'Juli', 'Avgust', 'Septembar', 'Oktobar', 'Novembar', 'Decembar'], + ['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Avg', 'Sep', 'Okt', 'Nov', 'Dec'], + ['Ned', 'Pon', 'Uto', 'Sri', 'Čet', 'Pet', 'Sub'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/ca.js b/frontend/src/components/common/control/datepicker/locale/translations/ca.js new file mode 100644 index 0000000..d137178 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/ca.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Catalan', + ['Gener', 'Febrer', 'Març', 'Abril', 'Maig', 'Juny', 'Juliol', 'Agost', 'Setembre', 'Octubre', 'Novembre', 'Desembre'], + ['Gen', 'Feb', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Oct', 'Nov', 'Des'], + ['Diu', 'Dil', 'Dmr', 'Dmc', 'Dij', 'Div', 'Dis'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/cs.js b/frontend/src/components/common/control/datepicker/locale/translations/cs.js new file mode 100644 index 0000000..21569df --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/cs.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Czech', + ['leden', 'únor', 'březen', 'duben', 'květen', 'červen', 'červenec', 'srpen', 'září', 'říjen', 'listopad', 'prosinec'], + ['led', 'úno', 'bře', 'dub', 'kvě', 'čer', 'čec', 'srp', 'zář', 'říj', 'lis', 'pro'], + ['ne', 'po', 'út', 'st', 'čt', 'pá', 'so'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/da.js b/frontend/src/components/common/control/datepicker/locale/translations/da.js new file mode 100644 index 0000000..bb4d2f5 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/da.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Danish', + ['Januar', 'Februar', 'Marts', 'April', 'Maj', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'December'], + ['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'], + ['Sø', 'Ma', 'Ti', 'On', 'To', 'Fr', 'Lø'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/de.js b/frontend/src/components/common/control/datepicker/locale/translations/de.js new file mode 100644 index 0000000..e31185c --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/de.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'German', + ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'], + ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'], + ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/ee.js b/frontend/src/components/common/control/datepicker/locale/translations/ee.js new file mode 100644 index 0000000..45f62c0 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/ee.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Estonian', + ['Jaanuar', 'Veebruar', 'Märts', 'Aprill', 'Mai', 'Juuni', 'Juuli', 'August', 'September', 'Oktoober', 'November', 'Detsember'], + ['Jaan', 'Veebr', 'Märts', 'Apr', 'Mai', 'Juuni', 'Juuli', 'Aug', 'Sept', 'Okt', 'Nov', 'Dets'], + ['P', 'E', 'T', 'K', 'N', 'R', 'L'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/el.js b/frontend/src/components/common/control/datepicker/locale/translations/el.js new file mode 100644 index 0000000..fb858e0 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/el.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Greek', + ['Ιανουάριος', 'Φεβρουάριος', 'Μάρτιος', 'Απρίλιος', 'Μάϊος', 'Ιούνιος', 'Ιούλιος', 'Αύγουστος', 'Σεπτέμβριος', 'Οκτώβριος', 'Νοέμβριος', 'Δεκέμβριος'], + ['Ιαν', 'Φεβ', 'Μαρ', 'Απρ', 'Μαι', 'Ιουν', 'Ιουλ', 'Αυγ', 'Σεπ', 'Οκτ', 'Νοε', 'Δεκ'], + ['Κυρ', 'Δευ', 'Τρι', 'Τετ', 'Πεμ', 'Παρ', 'Σαβ'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/en.js b/frontend/src/components/common/control/datepicker/locale/translations/en.js new file mode 100644 index 0000000..7959547 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/en.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'English', + ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], + ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/es.js b/frontend/src/components/common/control/datepicker/locale/translations/es.js new file mode 100644 index 0000000..600b4eb --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/es.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Spanish', + ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'], + ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'], + ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/fa.js b/frontend/src/components/common/control/datepicker/locale/translations/fa.js new file mode 100644 index 0000000..bf6b4e2 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/fa.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Persian', + ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند'], + ['فرو', 'ارد', 'خرد', 'تیر', 'مرد', 'شهر', 'مهر', 'آبا', 'آذر', 'دی', 'بهم', 'اسف'], + ['یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه', 'شنبه'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/fi.js b/frontend/src/components/common/control/datepicker/locale/translations/fi.js new file mode 100644 index 0000000..ffa2e7e --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/fi.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Finnish', + ['tammikuu', 'helmikuu', 'maaliskuu', 'huhtikuu', 'toukokuu', 'kesäkuu', 'heinäkuu', 'elokuu', 'syyskuu', 'lokakuu', 'marraskuu', 'joulukuu'], + ['tammi', 'helmi', 'maalis', 'huhti', 'touko', 'kesä', 'heinä', 'elo', 'syys', 'loka', 'marras', 'joulu'], + ['su', 'ma', 'ti', 'ke', 'to', 'pe', 'la'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/fo.js b/frontend/src/components/common/control/datepicker/locale/translations/fo.js new file mode 100644 index 0000000..ff33d19 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/fo.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Faroese', + ['Januar', 'Februar', 'Mars', 'Apríl', 'Mai', 'Juni', 'Juli', 'August', 'Septembur', 'Oktobur', 'Novembur', 'Desembur'], + ['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Des'], + ['Sun', 'Mán', 'Týs', 'Mik', 'Hós', 'Frí', 'Ley'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/fr.js b/frontend/src/components/common/control/datepicker/locale/translations/fr.js new file mode 100644 index 0000000..d988006 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/fr.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'French', + ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'], + ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin', 'Juil', 'Août', 'Sep', 'Oct', 'Nov', 'Déc'], + ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/ge.js b/frontend/src/components/common/control/datepicker/locale/translations/ge.js new file mode 100644 index 0000000..7c17a06 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/ge.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Georgia', + ['იანვარი', 'თებერვალი', 'მარტი', 'აპრილი', 'მაისი', 'ივნისი', 'ივლისი', 'აგვისტო', 'სექტემბერი', 'ოქტომბერი', 'ნოემბერი', 'დეკემბერი'], + ['იან', 'თებ', 'მარ', 'აპრ', 'მაი', 'ივნ', 'ივლ', 'აგვ', 'სექ', 'ოქტ', 'ნოე', 'დეკ'], + ['კვი', 'ორშ', 'სამ', 'ოთხ', 'ხუთ', 'პარ', 'შაბ'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/gl.js b/frontend/src/components/common/control/datepicker/locale/translations/gl.js new file mode 100644 index 0000000..c2f5d71 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/gl.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Galician', + ['Xaneiro', 'Febreiro', 'Marzo', 'Abril', 'Maio', 'Xuño', 'Xullo', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Decembro'], + ['Xan', 'Feb', 'Mar', 'Abr', 'Mai', 'Xuñ', 'Xul', 'Ago', 'Set', 'Out', 'Nov', 'Dec'], + ['Dom', 'Lun', 'Mar', 'Mér', 'Xov', 'Ven', 'Sáb'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/he.js b/frontend/src/components/common/control/datepicker/locale/translations/he.js new file mode 100644 index 0000000..c0ca09e --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/he.js @@ -0,0 +1,14 @@ +import Language from '../Language' + +const language = new Language( + 'Hebrew', + ['ינואר', 'פברואר', 'מרץ', 'אפריל', 'מאי', 'יוני', 'יולי', 'אוגוסט', 'ספטמבר', 'אוקטובר', 'נובמבר', 'דצמבר'], + ['ינו', 'פבר', 'מרץ', 'אפר', 'מאי', 'יונ', 'יול', 'אוג', 'ספט', 'אוק', 'נוב', 'דצמ'], + ['א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ש'] +) + +language.rtl = true + +export default language +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/hr.js b/frontend/src/components/common/control/datepicker/locale/translations/hr.js new file mode 100644 index 0000000..c2387fd --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/hr.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Croatian', + ['Siječanj', 'Veljača', 'Ožujak', 'Travanj', 'Svibanj', 'Lipanj', 'Srpanj', 'Kolovoz', 'Rujan', 'Listopad', 'Studeni', 'Prosinac'], + ['Sij', 'Velj', 'Ožu', 'Tra', 'Svi', 'Lip', 'Srp', 'Kol', 'Ruj', 'Lis', 'Stu', 'Pro'], + ['Ned', 'Pon', 'Uto', 'Sri', 'Čet', 'Pet', 'Sub'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/hu.js b/frontend/src/components/common/control/datepicker/locale/translations/hu.js new file mode 100644 index 0000000..a4db629 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/hu.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Hungarian', + ['Január', 'Február', 'Március', 'Április', 'Május', 'Június', 'Július', 'Augusztus', 'Szeptember', 'Október', 'November', 'December'], + ['Jan', 'Febr', 'Márc', 'Ápr', 'Máj', 'Jún', 'Júl', 'Aug', 'Szept', 'Okt', 'Nov', 'Dec'], + ['Vas', 'Hét', 'Ke', 'Sze', 'Csü', 'Pén', 'Szo'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/id.js b/frontend/src/components/common/control/datepicker/locale/translations/id.js new file mode 100644 index 0000000..7531b6d --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/id.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Indonesian', + ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'], + ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'], + ['Min', 'Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/is.js b/frontend/src/components/common/control/datepicker/locale/translations/is.js new file mode 100644 index 0000000..ae9e2fa --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/is.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Icelandic', + ['Janúar', 'Febrúar', 'Mars', 'Apríl', 'Maí', 'Júní', 'Júlí', 'Ágúst', 'September', 'Október', 'Nóvember', 'Desember'], + ['Jan', 'Feb', 'Mars', 'Apr', 'Maí', 'Jún', 'Júl', 'Ágú', 'Sep', 'Okt', 'Nóv', 'Des'], + ['Sun', 'Mán', 'Þri', 'Mið', 'Fim', 'Fös', 'Lau'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/it.js b/frontend/src/components/common/control/datepicker/locale/translations/it.js new file mode 100644 index 0000000..5d05deb --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/it.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Italian', + ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'], + ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'], + ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/ja.js b/frontend/src/components/common/control/datepicker/locale/translations/ja.js new file mode 100644 index 0000000..a1b6f75 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/ja.js @@ -0,0 +1,15 @@ +import Language from '../Language' + +const language = new Language( + 'Japanese', + ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'], + ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'], + ['日', '月', '火', '水', '木', '金', '土'] +) + +language.yearSuffix = '年' +language.ymd = true + +export default language +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/kk.js b/frontend/src/components/common/control/datepicker/locale/translations/kk.js new file mode 100644 index 0000000..c74cd91 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/kk.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Kazakh', + ['Қаңтар', 'Ақпан', 'Наурыз', 'Сәуір', 'Мамыр', 'Маусым', 'Шілде', 'Тамыз', 'Қыркүйек', 'Қазан', 'Қараша', 'Желтоқсан'], + ['Қаң', 'Ақп', 'Нау', 'Сәу', 'Мам', 'Мау', 'Шіл', 'Там', 'Қыр', 'Қаз', 'Қар', 'Жел'], + ['Жк', 'Дй', 'Сй', 'Ср', 'Бй', 'Жм', 'Сн'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/ko.js b/frontend/src/components/common/control/datepicker/locale/translations/ko.js new file mode 100644 index 0000000..98f7b6b --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/ko.js @@ -0,0 +1,14 @@ +import Language from '../Language' + +const language = new Language( + 'Korean', + ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'], + ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'], + ['일', '월', '화', '수', '목', '금', '토'] +) +language.yearSuffix = '년' +language.ymd = true + +export default language +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/lb.js b/frontend/src/components/common/control/datepicker/locale/translations/lb.js new file mode 100644 index 0000000..2dcbb0c --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/lb.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Luxembourgish', + ['Januar', 'Februar', 'Mäerz', 'Abrëll', 'Mee', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'], + ['Jan', 'Feb', 'Mäe', 'Abr', 'Mee', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'], + ['So.', 'Mé.', 'Dë.', 'Më.', 'Do.', 'Fr.', 'Sa.'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/lt.js b/frontend/src/components/common/control/datepicker/locale/translations/lt.js new file mode 100644 index 0000000..f066910 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/lt.js @@ -0,0 +1,14 @@ +import Language from '../Language' + +const language = new Language( + 'Lithuanian', + ['Sausis', 'Vasaris', 'Kovas', 'Balandis', 'Gegužė', 'Birželis', 'Liepa', 'Rugpjūtis', 'Rugsėjis', 'Spalis', 'Lapkritis', 'Gruodis'], + ['Sau', 'Vas', 'Kov', 'Bal', 'Geg', 'Bir', 'Lie', 'Rugp', 'Rugs', 'Spa', 'Lap', 'Gru'], + ['Sek', 'Pir', 'Ant', 'Tre', 'Ket', 'Pen', 'Šeš'] +) + +language.ymd = true + +export default language +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/lv.js b/frontend/src/components/common/control/datepicker/locale/translations/lv.js new file mode 100644 index 0000000..28a38b1 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/lv.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Latvian', + ['Janvāris', 'Februāris', 'Marts', 'Aprīlis', 'Maijs', 'Jūnijs', 'Jūlijs', 'Augusts', 'Septembris', 'Oktobris', 'Novembris', 'Decembris'], + ['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jūn', 'Jūl', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'], + ['Sv', 'Pr', 'Ot', 'Tr', 'Ce', 'Pk', 'Se'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/mk.js b/frontend/src/components/common/control/datepicker/locale/translations/mk.js new file mode 100644 index 0000000..413e864 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/mk.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Macedonian', + ['Јануари', 'Февруари', 'Март', 'Април', 'Мај', 'Јуни', 'Јули', 'Август', 'Септември', 'Октомври', 'Ноември', 'Декември'], + ['Јан', 'Фев', 'Мар', 'Апр', 'Мај', 'Јун', 'Јул', 'Авг', 'Сеп', 'Окт', 'Ное', 'Дек'], + ['Нед', 'Пон', 'Вто', 'Сре', 'Чет', 'Пет', 'Саб'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/mn.js b/frontend/src/components/common/control/datepicker/locale/translations/mn.js new file mode 100644 index 0000000..2370bf6 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/mn.js @@ -0,0 +1,14 @@ +import Language from '../Language' + +const language = new Language( + 'Mongolia', + ['1 дүгээр сар', '2 дугаар сар', '3 дугаар сар', '4 дүгээр сар', '5 дугаар сар', '6 дугаар сар', '7 дугаар сар', '8 дугаар сар', '9 дүгээр сар', '10 дугаар сар', '11 дүгээр сар', '12 дугаар сар'], + ['1-р сар', '2-р сар', '3-р сар', '4-р сар', '5-р сар', '6-р сар', '7-р сар', '8-р сар', '9-р сар', '10-р сар', '11-р сар', '12-р сар'], + ['Ня', 'Да', 'Мя', 'Лх', 'Пү', 'Ба', 'Бя'] +) + +language.ymd = true + +export default language +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/nb-NO.js b/frontend/src/components/common/control/datepicker/locale/translations/nb-NO.js new file mode 100644 index 0000000..af57014 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/nb-NO.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Norwegian Bokmål', + ['Januar', 'Februar', 'Mars', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Desember'], + ['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Des'], + ['Sø', 'Ma', 'Ti', 'On', 'To', 'Fr', 'Lø'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/nl.js b/frontend/src/components/common/control/datepicker/locale/translations/nl.js new file mode 100644 index 0000000..a5bed61 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/nl.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Dutch', + ['januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', 'augustus', 'september', 'oktober', 'november', 'december'], + ['jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'], + ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/pl.js b/frontend/src/components/common/control/datepicker/locale/translations/pl.js new file mode 100644 index 0000000..c1354f0 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/pl.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Polish', + ['Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec', 'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień'], + ['Sty', 'Lut', 'Mar', 'Kwi', 'Maj', 'Cze', 'Lip', 'Sie', 'Wrz', 'Paź', 'Lis', 'Gru'], + ['Nd', 'Pn', 'Wt', 'Śr', 'Czw', 'Pt', 'Sob'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/pt-BR.js b/frontend/src/components/common/control/datepicker/locale/translations/pt-BR.js new file mode 100644 index 0000000..a8bb671 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/pt-BR.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Brazilian', + ['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'], + ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'], + ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sab'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/ro.js b/frontend/src/components/common/control/datepicker/locale/translations/ro.js new file mode 100644 index 0000000..e5e8037 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/ro.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Romanian', + ['Ianuarie', 'Februarie', 'Martie', 'Aprilie', 'Mai', 'Iunie', 'Iulie', 'August', 'Septembrie', 'Octombrie', 'Noiembrie', 'Decembrie'], + ['Ian', 'Feb', 'Mar', 'Apr', 'Mai', 'Iun', 'Iul', 'Aug', 'Sep', 'Oct', 'Noi', 'Dec'], + ['D', 'L', 'Ma', 'Mi', 'J', 'V', 'S'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/ru.js b/frontend/src/components/common/control/datepicker/locale/translations/ru.js new file mode 100644 index 0000000..2d3380e --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/ru.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Russian', + ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'], + ['Янв', 'Февр', 'Март', 'Апр', 'Май', 'Июнь', 'Июль', 'Авг', 'Сент', 'Окт', 'Нояб', 'Дек'], + ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/sk.js b/frontend/src/components/common/control/datepicker/locale/translations/sk.js new file mode 100644 index 0000000..14a5274 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/sk.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Slovakian', + ['január', 'február', 'marec', 'apríl', 'máj', 'jún', 'júl', 'august', 'september', 'október', 'november', 'december'], + ['jan', 'feb', 'mar', 'apr', 'máj', 'jún', 'júl', 'aug', 'sep', 'okt', 'nov', 'dec'], + ['ne', 'po', 'ut', 'st', 'št', 'pi', 'so'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/sl-SI.js b/frontend/src/components/common/control/datepicker/locale/translations/sl-SI.js new file mode 100644 index 0000000..974ca65 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/sl-SI.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Sloveian', + ['Januar', 'Februar', 'Marec', 'April', 'Maj', 'Junij', 'Julij', 'Avgust', 'September', 'Oktober', 'November', 'December'], + ['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Avg', 'Sep', 'Okt', 'Nov', 'Dec'], + ['Ned', 'Pon', 'Tor', 'Sre', 'Čet', 'Pet', 'Sob'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/sr-CYRL.js b/frontend/src/components/common/control/datepicker/locale/translations/sr-CYRL.js new file mode 100644 index 0000000..4f4d65d --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/sr-CYRL.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Serbian in Cyrillic script', + ['Јануар', 'Фебруар', 'Март', 'Април', 'Мај', 'Јун', 'Јул', 'Август', 'Септембар', 'Октобар', 'Новембар', 'Децембар'], + ['Јан', 'Феб', 'Мар', 'Апр', 'Мај', 'Јун', 'Јул', 'Авг', 'Сеп', 'Окт', 'Нов', 'Дец'], + ['Нед', 'Пон', 'Уто', 'Сре', 'Чет', 'Пет', 'Суб'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/sr.js b/frontend/src/components/common/control/datepicker/locale/translations/sr.js new file mode 100644 index 0000000..4abf932 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/sr.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Serbian', + ['Januar', 'Februar', 'Mart', 'April', 'Maj', 'Jun', 'Jul', 'Avgust', 'Septembar', 'Oktobar', 'Novembar', 'Decembar'], + ['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Avg', 'Sep', 'Okt', 'Nov', 'Dec'], + ['Ned', 'Pon', 'Uto', 'Sre', 'Čet', 'Pet', 'Sub'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/sv.js b/frontend/src/components/common/control/datepicker/locale/translations/sv.js new file mode 100644 index 0000000..8ab7222 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/sv.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Swedish', + ['Januari', 'Februari', 'Mars', 'April', 'Maj', 'Juni', 'Juli', 'Augusti', 'September', 'Oktober', 'November', 'December'], + ['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'], + ['Sön', 'Mån', 'Tis', 'Ons', 'Tor', 'Fre', 'Lör'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/th.js b/frontend/src/components/common/control/datepicker/locale/translations/th.js new file mode 100644 index 0000000..a06c732 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/th.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Thai', + ['มกราคม', 'กุมภาพันธ์', 'มีนาคม', 'เมษายน', 'พฤษภาคม', 'มิถุนายน', 'กรกฎาคม', 'สิงหาคม', 'กันยายน', 'ตุลาคม', 'พฤศจิกายน', 'ธันวาคม'], + ['ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', 'พ.ย.', 'ธ.ค.'], + ['อา', 'จ', 'อ', 'พ', 'พฤ', 'ศ', 'ส'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/tr.js b/frontend/src/components/common/control/datepicker/locale/translations/tr.js new file mode 100644 index 0000000..ce0be2b --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/tr.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Turkish', + ['Ocak', 'Şubat', 'Mart', 'Nisan', 'Mayıs', 'Haziran', 'Temmuz', 'Ağustos', 'Eylül', 'Ekim', 'Kasım', 'Aralık'], + ['Oca', 'Şub', 'Mar', 'Nis', 'May', 'Haz', 'Tem', 'Ağu', 'Eyl', 'Eki', 'Kas', 'Ara'], + ['Paz', 'Pzt', 'Sal', 'Çar', 'Per', 'Cum', 'Cmt'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/uk.js b/frontend/src/components/common/control/datepicker/locale/translations/uk.js new file mode 100644 index 0000000..8487af0 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/uk.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Ukraine', + ['Січень', 'Лютий', 'Березень', 'Квітень', 'Травень', 'Червень', 'Липень', 'Серпень', 'Вересень', 'Жовтень', 'Листопад', 'Грудень'], + ['Січ', 'Лют', 'Бер', 'Квіт', 'Трав', 'Чер', 'Лип', 'Серп', 'Вер', 'Жовт', 'Лист', 'Груд'], + ['Нд', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/ur.js b/frontend/src/components/common/control/datepicker/locale/translations/ur.js new file mode 100644 index 0000000..bd8b9f3 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/ur.js @@ -0,0 +1,14 @@ +import Language from '../Language' + +const language = new Language( + 'Urdu', + ['جنوری', 'فروری', 'مارچ', 'اپریل', 'مئی', 'جون', 'جولائی', 'اگست', 'سپتمبر', 'اکتوبر', 'نومبر', 'دسمبر'], + ['جنوری', 'فروری', 'مارچ', 'اپریل', 'مئی', 'جون', 'جولائی', 'اگست', 'سپتمبر', 'اکتوبر', 'نومبر', 'دسمبر'], + ['اتوار', 'پیر', 'منگل', 'بدھ', 'جمعرات', 'جمعہ', 'ہفتہ'] +) + +language.rtl = true + +export default language +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/vi.js b/frontend/src/components/common/control/datepicker/locale/translations/vi.js new file mode 100644 index 0000000..44bfe1e --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/vi.js @@ -0,0 +1,10 @@ +import Language from '../Language' + +export default new Language( + 'Vietnamese', + ['Tháng 1', 'Tháng 2', 'Tháng 3', 'Tháng 4', 'Tháng 5', 'Tháng 6', 'Tháng 7', 'Tháng 8', 'Tháng 9', 'Tháng 10', 'Tháng 11', 'Tháng 12'], + ['T 01', 'T 02', 'T 03', 'T 04', 'T 05', 'T 06', 'T 07', 'T 08', 'T 09', 'T 10', 'T 11', 'T 12'], + ['CN', 'Thứ 2', 'Thứ 3', 'Thứ 4', 'Thứ 5', 'Thứ 6', 'Thứ 7'] +) +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/locale/translations/zh-HK.js b/frontend/src/components/common/control/datepicker/locale/translations/zh-HK.js new file mode 100644 index 0000000..7b9a042 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/zh-HK.js @@ -0,0 +1,11 @@ +import Language from '../Language' + +const language = new Language( + 'Chinese_HK', + ['壹月', '贰月', '叁月', '肆月', '伍月', '陆月', '柒月', '捌月', '玖月', '拾月', '拾壹月', '拾贰月'], + ['壹月', '贰月', '叁月', '肆月', '伍月', '陆月', '柒月', '捌月', '玖月', '拾月', '拾壹月', '拾贰月'], + ['日', '壹', '贰', '叁', '肆', '伍', '陆'] +) +language.yearSuffix = '年' + +export default language diff --git a/frontend/src/components/common/control/datepicker/locale/translations/zh.js b/frontend/src/components/common/control/datepicker/locale/translations/zh.js new file mode 100644 index 0000000..cbb52ef --- /dev/null +++ b/frontend/src/components/common/control/datepicker/locale/translations/zh.js @@ -0,0 +1,13 @@ +import Language from '../Language' + +const language = new Language( + 'Chinese', + ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'], + ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'], + ['日', '一', '二', '三', '四', '五', '六'] +) +language.yearSuffix = '年' + +export default language +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/datepicker/utils/DateUtils.js b/frontend/src/components/common/control/datepicker/utils/DateUtils.js new file mode 100644 index 0000000..5005cb3 --- /dev/null +++ b/frontend/src/components/common/control/datepicker/utils/DateUtils.js @@ -0,0 +1,252 @@ +import en from '../locale/translations/en' + +const utils = { + /** + * @type {Boolean} + */ + useUtc: false, + /** + * Returns the full year, using UTC or not + * @param {Date} date + */ + getFullYear (date) { + return this.useUtc ? date.getUTCFullYear() : date.getFullYear() + }, + + /** + * Returns the month, using UTC or not + * @param {Date} date + */ + getMonth (date) { + return this.useUtc ? date.getUTCMonth() : date.getMonth() + }, + + /** + * Returns the date, using UTC or not + * @param {Date} date + */ + getDate (date) { + return this.useUtc ? date.getUTCDate() : date.getDate() + }, + + /** + * Returns the day, using UTC or not + * @param {Date} date + */ + getDay (date) { + return this.useUtc ? date.getUTCDay() : date.getDay() + }, + + /** + * Returns the hours, using UTC or not + * @param {Date} date + */ + getHours (date) { + return this.useUtc ? date.getUTCHours() : date.getHours() + }, + + /** + * Returns the minutes, using UTC or not + * @param {Date} date + */ + getMinutes (date) { + return this.useUtc ? date.getUTCMinutes() : date.getMinutes() + }, + + /** + * Sets the full year, using UTC or not + * @param {Date} date + */ + setFullYear (date, value, useUtc) { + return this.useUtc ? date.setUTCFullYear(value) : date.setFullYear(value) + }, + + /** + * Sets the month, using UTC or not + * @param {Date} date + */ + setMonth (date, value, useUtc) { + return this.useUtc ? date.setUTCMonth(value) : date.setMonth(value) + }, + + /** + * Sets the date, using UTC or not + * @param {Date} date + * @param {Number} value + */ + setDate (date, value, useUtc) { + return this.useUtc ? date.setUTCDate(value) : date.setDate(value) + }, + + /** + * Check if date1 is equivalent to date2, without comparing the time + * @see https://stackoverflow.com/a/6202196/4455925 + * @param {Date} date1 + * @param {Date} date2 + */ + compareDates (date1, date2) { + const d1 = new Date(date1.getTime()) + const d2 = new Date(date2.getTime()) + + if (this.useUtc) { + d1.setUTCHours(0, 0, 0, 0) + d2.setUTCHours(0, 0, 0, 0) + } else { + d1.setHours(0, 0, 0, 0) + d2.setHours(0, 0, 0, 0) + } + return d1.getTime() === d2.getTime() + }, + + /** + * Validates a date object + * @param {Date} date - an object instantiated with the new Date constructor + * @return {Boolean} + */ + isValidDate (date) { + if (Object.prototype.toString.call(date) !== '[object Date]') { + return false + } + return !isNaN(date.getTime()) + }, + + /** + * Return abbreviated week day name + * @param {Date} + * @param {Array} + * @return {String} + */ + getDayNameAbbr (date, days) { + if (typeof date !== 'object') { + throw TypeError('Invalid Type') + } + return days[this.getDay(date)] + }, + + /** + * Return name of the month + * @param {Number|Date} + * @param {Array} + * @return {String} + */ + getMonthName (month, months) { + if (!months) { + throw Error('missing 2nd parameter Months array') + } + if (typeof month === 'object') { + return months[this.getMonth(month)] + } + if (typeof month === 'number') { + return months[month] + } + throw TypeError('Invalid type') + }, + + /** + * Return an abbreviated version of the month + * @param {Number|Date} + * @return {String} + */ + getMonthNameAbbr (month, monthsAbbr) { + if (!monthsAbbr) { + throw Error('missing 2nd paramter Months array') + } + if (typeof month === 'object') { + return monthsAbbr[this.getMonth(month)] + } + if (typeof month === 'number') { + return monthsAbbr[month] + } + throw TypeError('Invalid type') + }, + + /** + * Alternative get total number of days in month + * @param {Number} year + * @param {Number} m + * @return {Number} + */ + daysInMonth (year, month) { + return /8|3|5|10/.test(month) ? 30 : month === 1 ? (!(year % 4) && year % 100) || !(year % 400) ? 29 : 28 : 31 + }, + + /** + * Get nth suffix for date + * @param {Number} day + * @return {String} + */ + getNthSuffix (day) { + switch (day) { + case 1: + case 21: + case 31: + return 'st' + case 2: + case 22: + return 'nd' + case 3: + case 23: + return 'rd' + default: + return 'th' + } + }, + + /** + * Formats date object + * @param {Date} + * @param {String} + * @param {Object} + * @return {String} + */ + formatDate (date, format, translation) { + translation = (!translation) ? en : translation + let year = this.getFullYear(date) + let month = this.getMonth(date) + 1 + let day = this.getDate(date) + let str = format + .replace(/dd/, ('0' + day).slice(-2)) + .replace(/d/, day) + .replace(/yyyy/, year) + .replace(/yy/, String(year).slice(2)) + .replace(/MMMM/, this.getMonthName(this.getMonth(date), translation.months)) + .replace(/MMM/, this.getMonthNameAbbr(this.getMonth(date), translation.monthsAbbr)) + .replace(/MM/, ('0' + month).slice(-2)) + .replace(/M(?!a|ä|e)/, month) + .replace(/su/, this.getNthSuffix(this.getDate(date))) + .replace(/D(?!e|é|i)/, this.getDayNameAbbr(date, translation.days)) + return str + }, + + /** + * Creates an array of dates for each day in between two dates. + * @param {Date} start + * @param {Date} end + * @return {Array} + */ + createDateArray (start, end) { + let dates = [] + while (start <= end) { + dates.push(new Date(start)) + start = this.setDate(new Date(start), this.getDate(new Date(start)) + 1) + } + return dates + }, + + /** + * method used as a prop validator for input values + * @param {*} val + * @return {Boolean} + */ + validateDateInput (val) { + return val === null || val instanceof Date || typeof val === 'string' || typeof val === 'number' + } +} + +export const makeDateUtils = useUtc => ({...utils, useUtc}) + +export default { + ...utils +} +// eslint-disable-next-line +; diff --git a/frontend/src/components/common/control/editor/Editor.vue b/frontend/src/components/common/control/editor/Editor.vue new file mode 100644 index 0000000..bb17a89 --- /dev/null +++ b/frontend/src/components/common/control/editor/Editor.vue @@ -0,0 +1,127 @@ + + + + diff --git a/frontend/src/components/common/control/editor/index.js b/frontend/src/components/common/control/editor/index.js new file mode 100644 index 0000000..e3dd0aa --- /dev/null +++ b/frontend/src/components/common/control/editor/index.js @@ -0,0 +1,3 @@ +import Editor from './Editor.vue'; + +export default Editor; diff --git a/frontend/src/components/common/control/excel/ExcelDownload.vue b/frontend/src/components/common/control/excel/ExcelDownload.vue new file mode 100644 index 0000000..c64d98a --- /dev/null +++ b/frontend/src/components/common/control/excel/ExcelDownload.vue @@ -0,0 +1,129 @@ + + + + diff --git a/frontend/src/components/common/control/excel/ExcelUpload.vue b/frontend/src/components/common/control/excel/ExcelUpload.vue new file mode 100644 index 0000000..34f24a6 --- /dev/null +++ b/frontend/src/components/common/control/excel/ExcelUpload.vue @@ -0,0 +1,119 @@ + + + + diff --git a/frontend/src/components/common/control/excel/index.js b/frontend/src/components/common/control/excel/index.js new file mode 100644 index 0000000..9277cfb --- /dev/null +++ b/frontend/src/components/common/control/excel/index.js @@ -0,0 +1,4 @@ +import SdlExceldownload from './ExcelDownload.vue'; +import SdlExcelupload from './ExcelUpload.vue'; + +export { SdlExceldownload, SdlExcelupload }; diff --git a/frontend/src/components/common/control/fileuploader/MultipleFileUploader.vue b/frontend/src/components/common/control/fileuploader/MultipleFileUploader.vue new file mode 100644 index 0000000..455b5e5 --- /dev/null +++ b/frontend/src/components/common/control/fileuploader/MultipleFileUploader.vue @@ -0,0 +1,806 @@ + + + + + + diff --git a/frontend/src/components/common/control/index.js b/frontend/src/components/common/control/index.js new file mode 100644 index 0000000..9a99e88 --- /dev/null +++ b/frontend/src/components/common/control/index.js @@ -0,0 +1,43 @@ +/** + * Program: SDL Common Component + * Author: SDL + * Description: SDL Common Component + */ +import SdlBreadcrumb from './breadcrumb'; +import SdlLoadingbar from './loadingbar/LoadingBar.vue'; +import SdlModal from './modal'; +import SdlFileuploader from './fileuploader/MultipleFileUploader.vue'; +import SdlDatePicker from './datepicker'; +import SdlRegexInput from './input/RegexInput.vue'; +import sdlPagination from './pagination/Pagination.vue'; +import { SdlExceldownload, SdlExcelupload } from './excel'; +import SdlTree from './tree'; +import SdlApprovalpath from './approvalpath/ApprovalPath.vue'; +import SdlScrollToTop from './scrollToTop/ScrollToTop.vue'; + +const SdlControl = { + // eslint-disable-next-line no-unused-vars + install(Vue, options) { + Vue.use(SdlModal, { + dialog: true, + dynamic: true, + dynamicDefaults: { clickToClose: false }, + injectModalsContainer: true, + }); + Vue.component('sdl-breadcrumb', SdlBreadcrumb); + Vue.component('sdl-loadingbar', SdlLoadingbar); + Vue.component('sdl-fileuploader', SdlFileuploader); + Vue.component('sdl-datepicker', SdlDatePicker); + Vue.component('sdl-pagination', sdlPagination); + Vue.component('sdl-exceldownloadbtn', SdlExceldownload); + Vue.component('sdl-exceluploadbtn', SdlExcelupload); + Vue.component('sdl-tree', SdlTree); + Vue.component('sdl-approvalpath', SdlApprovalpath); + Vue.component('sdl-regex-input', SdlRegexInput); + // Exclude Editor from SDL common component related to license + // Vue.component('sdl-editor', SdlEditor); + Vue.component('sdl-scroll-to-top', SdlScrollToTop); + }, +}; + +export default SdlControl; diff --git a/frontend/src/components/common/control/input/RegexInput.vue b/frontend/src/components/common/control/input/RegexInput.vue new file mode 100644 index 0000000..cfa2504 --- /dev/null +++ b/frontend/src/components/common/control/input/RegexInput.vue @@ -0,0 +1,70 @@ + + + + diff --git a/frontend/src/components/common/control/loadingbar/LoadingBar.vue b/frontend/src/components/common/control/loadingbar/LoadingBar.vue new file mode 100644 index 0000000..395f238 --- /dev/null +++ b/frontend/src/components/common/control/loadingbar/LoadingBar.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/frontend/src/components/common/control/modal/Dialog.vue b/frontend/src/components/common/control/modal/Dialog.vue new file mode 100644 index 0000000..e203938 --- /dev/null +++ b/frontend/src/components/common/control/modal/Dialog.vue @@ -0,0 +1,210 @@ + + + + diff --git a/frontend/src/components/common/control/modal/Modal.vue b/frontend/src/components/common/control/modal/Modal.vue new file mode 100644 index 0000000..8fc1a94 --- /dev/null +++ b/frontend/src/components/common/control/modal/Modal.vue @@ -0,0 +1,734 @@ + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/common/control/modal/ModalsContainer.vue b/frontend/src/components/common/control/modal/ModalsContainer.vue new file mode 100644 index 0000000..77b2c12 --- /dev/null +++ b/frontend/src/components/common/control/modal/ModalsContainer.vue @@ -0,0 +1,66 @@ + + + diff --git a/frontend/src/components/common/control/modal/Resizer.vue b/frontend/src/components/common/control/modal/Resizer.vue new file mode 100644 index 0000000..972c46a --- /dev/null +++ b/frontend/src/components/common/control/modal/Resizer.vue @@ -0,0 +1,115 @@ + + + + diff --git a/frontend/src/components/common/control/modal/index.js b/frontend/src/components/common/control/modal/index.js new file mode 100644 index 0000000..75146fc --- /dev/null +++ b/frontend/src/components/common/control/modal/index.js @@ -0,0 +1,121 @@ +/** + * Program: Modal Plugin + * Author: SDL + * Description: Modal Plugin Common Component + */ +import emitter from '@/event/event'; +import Modal from './Modal.vue'; +import Dialog from './Dialog.vue'; +import ModalsContainer from './ModalsContainer.vue'; +import { createDivInBody } from './utils'; + +const DEFAULT_COMPONENT_NAME = 'Modal'; +const UNMOUNTED_ROOT_ERROR_MESSAGE = '[vue-js-modal] ' + 'In order to render dynamic modals, a ' + 'component must be present on the page.'; +const DYNAMIC_MODAL_DISABLED_ERROR = '[vue-js-modal] ' + '$modal() received object as a first argument, but dynamic modals are ' + 'switched off. https://github.com/euvl/vue-js-modal/#dynamic-modals'; +const UNSUPPORTED_ARGUMENT_ERROR = '[vue-js-modal] $modal() received an unsupported argument as a first argument.'; + +export const getModalsContainer = (Vue, options, root) => { + // eslint-disable-next-line no-underscore-dangle + if (!root._dynamicContainer && options.injectModalsContainer) { + // const container = createDivInBody(); + // createApp({ + // parent: root, + // render: h => h(ModalsContainer), + // }).mount(container); + } + + // eslint-disable-next-line no-underscore-dangle + return root._dynamicContainer; +}; + +const Plugin = { + install(Vue, options = {}) { + /** + * Makes sure that plugin can be installed only once + */ + if (this.installed) { + return; + } + + this.installed = true; + this.app = Vue; + this.event = emitter; + this.rootInstance = null; + + const componentName = options.componentName || DEFAULT_COMPONENT_NAME; + const dynamicDefaults = options.dynamicDefaults || {}; + /** + * Plugin API + */ + const showStaticModal = (modal, params) => { + emitter.$emit('toggle', modal, true, params); + }; + + const showDynamicModal = (modal, props, params, events) => { + /** + * Get root for dynamic modal + */ + const root = params && params.root ? params.root : Plugin.rootInstance; + const container = getModalsContainer(Vue, options, root); + /** + * Show dynamic modal + */ + if (container) { + container.add(modal, props, { ...dynamicDefaults, ...params }, events); + return; + } + + console.warn(UNMOUNTED_ROOT_ERROR_MESSAGE); + }; + + Vue.config.globalProperties.$modal = { + // eslint-disable-next-line consistent-return + show(modal, ...args) { + switch (typeof modal) { + case 'string': { + return showStaticModal(modal, ...args); + } + case 'object': + case 'function': { + return options.dynamic ? showDynamicModal(modal, ...args) : console.warn(DYNAMIC_MODAL_DISABLED_ERROR); + } + default: { + console.warn(UNSUPPORTED_ARGUMENT_ERROR, modal); + return false; + } + } + }, + hide(name, params) { + emitter.$emit('toggle', name, false, params); + }, + toggle(name, params) { + emitter.$emit('toggle', name, undefined, params); + }, + }; + /** + * Sets custom component name (if provided) + */ + Vue.component(componentName, Modal); + /** + * Registration of component + */ + if (options.dialog) { + Vue.component('sdl-dialog', Dialog); + } + /** + * Registration of component + */ + if (options.dynamic) { + Vue.component('sdl-modal-container', ModalsContainer); + Vue.mixin({ + beforeMount() { + if (Plugin.rootInstance === null) { + Plugin.rootInstance = this.$root; + } + }, + }); + } + }, +}; + +export default Plugin; diff --git a/frontend/src/components/common/control/modal/parser.js b/frontend/src/components/common/control/modal/parser.js new file mode 100644 index 0000000..26f7e96 --- /dev/null +++ b/frontend/src/components/common/control/modal/parser.js @@ -0,0 +1,70 @@ +/** + * Program: Modal parser + * Author: SDL + * Description: Modal parser util + */ +const floatRegexp = '[-+]?[0-9]*.?[0-9]+'; + +const types = [ + { + name: 'px', + regexp: new RegExp(`^${floatRegexp}px$`), + }, + { + name: '%', + regexp: new RegExp(`^${floatRegexp}%$`), + }, + /** + * Fallback optopn + * If no suffix specified, assigning "px" + */ + { + name: 'px', + regexp: new RegExp(`^${floatRegexp}$`), + }, +]; + +const getType = value => { + if (value === 'auto') { + return { + type: value, + value: 0, + }; + } + + for (let i = 0; i < types.length; i += 1) { + const type = types[i]; + + if (type.regexp.test(value)) { + return { + type: type.name, + value: parseFloat(value), + }; + } + } + + return { + type: '', + value, + }; +}; + +export const parseNumber = value => { + switch (typeof value) { + case 'number': + return { type: 'px', value }; + case 'string': + return getType(value); + default: + return { type: '', value }; + } +}; + +export const validateNumber = value => { + if (typeof value === 'string') { + const value2 = parseNumber(value); + return (value2.type === '%' || value2.type === 'px') && value2.value > 0; + } + + return value >= 0; +}; diff --git a/frontend/src/components/common/control/modal/utils/index.js b/frontend/src/components/common/control/modal/utils/index.js new file mode 100644 index 0000000..1b3c1c6 --- /dev/null +++ b/frontend/src/components/common/control/modal/utils/index.js @@ -0,0 +1,60 @@ +export const generateId = ((index = 0) => () => { + index += 1; + return index.toString(); +})(); +/** + * + * @param {Number} from Lower limit + * @param {Number} to Upper limit + * @param {Number} value Checked number value + * + * @return {Number} Either source value itself or limit value if range limits + * are exceeded + */ +// eslint-disable-next-line no-nested-ternary +export const inRange = (from, to, value) => (value < from ? from : value > to ? to : value); + +export const createModalEvent = (args = {}) => ({ + id: generateId(), + timestamp: Date.now(), + canceled: false, + ...args, +}); + +export const getMutationObserver = () => { + if (typeof window !== 'undefined') { + const prefixes = ['', 'WebKit', 'Moz', 'O', 'Ms']; + + for (let i = 0; i < prefixes.length; i += 1) { + const name = `${prefixes[i]}MutationObserver`; + + if (name in window) { + return window[name]; + } + } + } + + return false; +}; + +export const createDivInBody = () => { + const div = document.createElement('div'); + document.body.appendChild(div); + + return div; +}; + +export const blurActiveElement = () => { + if ( + typeof document !== 'undefined' + && document.activeElement + && document.activeElement.tagName !== 'BODY' + && document.activeElement.blur + ) { + document.activeElement.blur(); + + return true; + } + + return false; +}; diff --git a/frontend/src/components/common/control/pagination/Pagination.vue b/frontend/src/components/common/control/pagination/Pagination.vue new file mode 100644 index 0000000..76b3a1f --- /dev/null +++ b/frontend/src/components/common/control/pagination/Pagination.vue @@ -0,0 +1,113 @@ + + + + diff --git a/frontend/src/components/common/control/scrollToTop/ScrollToTop.vue b/frontend/src/components/common/control/scrollToTop/ScrollToTop.vue new file mode 100644 index 0000000..cbc2c35 --- /dev/null +++ b/frontend/src/components/common/control/scrollToTop/ScrollToTop.vue @@ -0,0 +1,39 @@ + + + diff --git a/frontend/src/components/common/control/tree/Tree.vue b/frontend/src/components/common/control/tree/Tree.vue new file mode 100644 index 0000000..31bc989 --- /dev/null +++ b/frontend/src/components/common/control/tree/Tree.vue @@ -0,0 +1,333 @@ + + + + diff --git a/frontend/src/components/common/control/tree/TreeItem.vue b/frontend/src/components/common/control/tree/TreeItem.vue new file mode 100644 index 0000000..c221c74 --- /dev/null +++ b/frontend/src/components/common/control/tree/TreeItem.vue @@ -0,0 +1,313 @@ + + + + + diff --git a/frontend/src/components/common/control/tree/index.js b/frontend/src/components/common/control/tree/index.js new file mode 100644 index 0000000..c835a8f --- /dev/null +++ b/frontend/src/components/common/control/tree/index.js @@ -0,0 +1,3 @@ +import Tree from './Tree.vue'; + +export default Tree; diff --git a/frontend/src/components/common/control/tree/less/base.less b/frontend/src/components/common/control/tree/less/base.less new file mode 100644 index 0000000..6a2a2b3 --- /dev/null +++ b/frontend/src/components/common/control/tree/less/base.less @@ -0,0 +1,33 @@ +// base tree +.tree-node, .tree-children, .tree-container-ul { display:block; margin:0; padding:0; list-style-type:none; list-style-image:none; } +.tree-children {overflow: hidden; } +.tree-node { white-space:nowrap; } +.tree-anchor { display:inline-block; color:black; white-space:nowrap; padding:0 4px 0 1px; margin:0; vertical-align:top; font-size: 14px; cursor: pointer;} +.tree-anchor:focus { outline:0; } +.tree-anchor, .tree-anchor:link, .tree-anchor:visited, .tree-anchor:hover, .tree-anchor:active { text-decoration:none; color:inherit; } +.tree-icon { display:inline-block; text-decoration:none; margin:0; padding:0; vertical-align:top; text-align:center; } +.tree-icon:empty { display:inline-block; text-decoration:none; margin:0; padding:0; vertical-align:top; text-align:center; } +.tree-ocl { cursor:pointer; } +.tree-leaf > .tree-ocl { cursor:default; } +.tree-anchor > .tree-themeicon { margin-right:2px; } +.tree-no-icons .tree-themeicon, +.tree-anchor > .tree-themeicon-hidden { display:none; } +.tree-hidden, .tree-node.tree-hidden { display:none; } + +// base tree rtl +.tree-rtl { + .tree-anchor { padding:0 1px 0 4px; } + .tree-anchor > .tree-themeicon { margin-left:2px; margin-right:0; } + .tree-node { margin-left:0; } + .tree-container-ul > .tree-node { margin-right:0; } +} + +// base tree wholerow +.tree-wholerow-ul { + position:relative; + display:inline-block; + min-width:100%; + .tree-leaf > .tree-ocl { cursor:pointer; } + .tree-anchor, .tree-icon { position:relative; } + .tree-wholerow { width:100%; cursor:pointer; z-index: -1; position:absolute; left:0; -webkit-user-select:none; -moz-user-select:none; -ms-user-select:none; user-select:none; } +} \ No newline at end of file diff --git a/frontend/src/components/common/control/tree/less/main.less b/frontend/src/components/common/control/tree/less/main.less new file mode 100644 index 0000000..8cec34a --- /dev/null +++ b/frontend/src/components/common/control/tree/less/main.less @@ -0,0 +1,60 @@ +.tree { + text-align: left; +} +.tree-@{theme-name} { + .tree-node, + .tree-icon { background-repeat:no-repeat; background-color:transparent; } + .tree-anchor, + .tree-animated, + .tree-wholerow { transition:background-color 0.15s, box-shadow 0.15s; } + .tree-hovered { background:@hovered-bg-color; border: 0px; box-shadow:none; } + .tree-context { background:@hovered-bg-color; border: 0px; box-shadow:none; } + .tree-selected { background:@clicked-bg-color; border: 0px; box-shadow:none; } + .tree-no-icons .tree-anchor > .tree-themeicon { display:none; } + .tree-disabled { + background:transparent; color:@disabled-color; + &.tree-hovered { background:transparent; box-shadow:none; } + &.tree-selected { background:@disabled-bg-color; } + > .tree-icon { opacity:0.8; filter: url("data:image/svg+xml;utf8,#tree-grayscale"); /* Firefox 10+ */ filter: gray; /* IE6-9 */ -webkit-filter: grayscale(100%); /* Chrome 19+ & Safari 6+ */ } + } + // search + .tree-search { font-style:italic; color:@search-result-color; font-weight:bold; } + // checkboxes + .tree-no-checkboxes .tree-checkbox { display:none !important; } + &.tree-checkbox-no-clicked { + .tree-selected { + background:transparent; + box-shadow:none; + &.tree-hovered { background:@hovered-bg-color; } + } + > .tree-wholerow-ul .tree-wholerow-clicked { + background:transparent; + &.tree-wholerow-hovered { background:@hovered-bg-color; } + } + } + // stripes + > .tree-striped { min-width:100%; display:inline-block; background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAkCAMAAAB/qqA+AAAABlBMVEUAAAAAAAClZ7nPAAAAAnRSTlMNAMM9s3UAAAAXSURBVHjajcEBAQAAAIKg/H/aCQZ70AUBjAATb6YPDgAAAABJRU5ErkJggg==") left top repeat; } + // wholerow + > .tree-wholerow-ul .tree-hovered, + > .tree-wholerow-ul .tree-selected { background:transparent; box-shadow:none; border-radius:0; } + .tree-wholerow { -moz-box-sizing:border-box; -webkit-box-sizing:border-box; box-sizing:border-box; } + .tree-wholerow-hovered { background:@hovered-bg-color; } + .tree-wholerow-clicked { background:@clicked-bg-color; } +} + +// theme variants +.tree-@{theme-name} { + .tree-theme(24px, "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAABgCAYAAABsS6soAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAACbBJREFUeNrsnX+IHFcBx9/szaZerrU/rmpCcmmLAVuFinjFhFLECmf+EEVrWlNThNo/iiBEaJSribW1EjSSNEY9/6iKxBZK8Q+10nSxAckfScmJtZAm1fzRJA2pUm1yOe9wd3bH9+Zuzunc3GV3frw3O+/zCY+5mbvL25l9893Pe/NuxvF9X4AQjuP0/DsjI2v8s2fPOaZecxXrpz2Wu82XCdX+zpx5I9NOODS4ajSGqkB7pM3rbC81mkG2TyDqhyp9+OgsZWh/GCAGiAHS5o0c+zzOOQwQA8MAgfMPA8QAMUDAADFADAwDBM4/DBADxAABA8QAMTAMEDj/MEAMsB8NsNFopG6kY2Njmd/YqtaPAQrhdvuDo6OjPdc0OTmZW6oUXX+aA7lu3drMM9E59l2fyGmCg/ozcupP96yXi+/L8vnYt34ny/j6jz/zqmEDzNTO3KIadZqTpuz1x9ERfpF97yWwKlc/GONpWW5L2P4ZWW6U5cOmXlgef4bp8v6mR5cBQnnoxqrS2FqJuW2Z790qDXGTtMCDVhjgUqaVd3enXzAdfsq0ejEzqD7j4+Pndu3atUZjlc/LEIwHkSfL72V5UIbjP8tsgFwFzmiAHAUoUfj9Wy7WGKjaSRCrz6kA1GCABKCtBggQCb9pubiuZC/r0SL/c2NjgPELDNF1Hd3h5S5w6OyOh2OAaZdpu71Lrce7wzrqjx37wusn6MaDpezmRrfNysWV8e1Vx9gYYDRk5sPISToBiiIp5EyMh4Uncdplyn3vep+Lrj/r8YFsQajCTi6bcnXQtvDLywDpAgP0ETHzU4sVNoZfxADNB6CyAtNXI7kaCjaGoK3hVxoDtHUKTBkg9AlBW8MvLwNkIjRAD5RpkrPN4ZeXAfYUgDr+vKzM9Rved6vrB2O8IcvalL+7Q4MB6rkKbLqra3NXm2Of740FqL8n7pflJ7IMX+bnrp1fvj2/VH8e97OyGyC3wwKwlH6/HRbPBQYAawMwj9fMPEAA6Eu4IzQAYIAYIABggBggAGg0MNNggACAAWKAAIABYoBaMX1XaNvrBwwQAwQADBADxMAwQMAAMUAAwAAxQAwMAwQMEAMEAAwQA8TAMEDAADFAAAwQAwQMDAMEDBCglDQaDU/M3fo8LJcVg7CMjY25OdSf+iSR9TtlrT8vA3zoG9/yRz9yqzj599fFqdPnxfobVgfbjx9/RTz7zFOFambW/Oq6caR5Hkeet1I3Xf9yBqTjQd+WH39H1t9LbyUISll/J8f9Fyn2X1Sl/uVY//4R8dwfjwTB981tD4jHHp97WNMVV75HhwHqeSZIrw26iAcYma4/CR3hl6ZBF/EQI4P1O5p/DxLY+pVtiefUi4cOz3+1emFbGH7qd3798ycKeR+0PxUOzBmg5ZQmALv5EKjyE/QOPLl30ba254kLF6fEj3/6pDh16uqFn7v7i1sLtcA8DDDzRRCbH1Wp2wBtO9mgdyYmJgqvY+Bjv31HWXH7H8Tw8HXa9zUPA+QqcA4GyFGAMrB//34xPT1tzf7mcRWYLnCfGyCAYt++faLZbFq1z8bGAOPd3ui6jiuvy3W709YfjuWlXeZhkt3+//Fub3Q97ZW/LPXHjj9pVCC7d+8Oltu3b1/YtnfvXuF53qLtlhigvqvASSEzH0aOzsafFHLqpMxSf3iSp13mZZLd/P/R/cy631nrB3NBqMJuz549ot1uWxd+Rg0QAMygQi60wHCpO/zaL302cfv09JQdBlhGI8BKwMYQ1Bl+8/P5lhx+WjU8FCz/+pdj4r4Hvh5MgQm3VdYAdYz5AcEPySGou9v73R3bFm3b+fgTQdDdvvHBYH3Llh+JV08eEJ+88w7xhbs2ix/+4HvVNkCAgknb0HOfolSmeZcmxvxuuunGRe/D5nu+5A+t3SRefvm4+M/sYTE0eIf44M33iRcPHRDXvPva6hig6UnPtk+6Nn3yGay/NAEIi9n6tS3i2V8ejHSFD4vTp18PusAXpt4urN48DJC7wUDp4W4w5b4bjNGuQcb8IgABLKXfA1AZYNZpaAQgAAForQHyt8AA0LcGmPlDAAMEwAAxQAAADBAAMEAMEAAAAwQADBADBADAAAEAA8QAAQAwQADAADFAAAAMEAAwQAwQAAADBAAMEAMEAMAA+4+64wxG11u+P6ut7qGB9z780I6vJn2v0Wj87ciRI0/zDgEGWNEA/O8ttwQv9ooTJ5wsP5Mp/FbWrvJmOteodXdl7YKY6VzSFYIbN268V4bcU/HtszOz4uLURTExMfGo5Duc2mBDAOZxR+hCusCjv3F8VYoOwm6354kKv7fe+tdrqohm7UOefB/iVqiTtueJmdlZIV+PkOH3iIQABCvI46lwuQdgkcEXtbp42EXXi7C/AGl/ruuuUV++8OeD4s03/3FIrZsMwZrrivoKN3hS0NSlKfHtnTsJQbCCPMYAcw3AaPhN3uUXEkJJIagl/GL86rlflCIE1c4OvWulGLlhRJw5c1acOPma2Lz57kfq9fonOEUAA1ye3B6MriP8oiFnMvyiIahQIbhq1fvuFCs6F2QIFn1hZNGzUJUFrhwcFCPrRoTX9AIjHB0dXc0pAhYYYKZzvusADAMuKdx0hl9SCJoIv6QQvP764Q/IA3qpMNtbYtRabR1w6+Lqq+rBmCCPA4dIm8EA8+wCx8f4TIRf3PyS1ivanH3R8TtLdYUVA9IGVQGwxAD1BGA03MLQK0P4LXdhRAdf/vT94lMf3SRUFziYFlMgbn1AHXA/7AeHZe4Lf3EaAlQY7QaYFIJlCD9TIRgNP8/zzhU9J9CR/3yVdNIB/XZb+LK7q0qn0xbtdmeu+zsHnWDAAIvoAsfDrgzmZyIE4+EnO51niwy/DRs2BJPWZcj57Y4X9ITbal0Wv6O8cK5nrELQxwEBAywmAKOhpzP8lgq7brbnhjS8wPQkOsNPcfTo0SDUW03PaTZbotXyhKxbeHLZbLVkkUu5XZWODElOD8AAL0/q0XIT4ddNwBUdgmqcT13tDb92PaHtT+GkAfqtTqtWi39uOfMCGI4QNjFAwAALDUAbUUGn5vktTHWZ6Wi9GUK73Vbz/ZyOGgOUiefLLxynFkx1GKjXAp9XLUJNiQGwxAD1zAOE/4egqbqPHTt2/uEdj6nAU77nB3O81BhgcFMLNR3QXxj/m5ycPM+7BRjg8nA7rPBA9MGEUflpFdyNpu66ouV5wnVdZ67/Oxd7XnTsT41XCjHLOwtVNsCsd4MhAPsoAAHgnXBHaACw1gDpAmOAABggBggAGCAGiAECYIAYIABggBggAEAC/xNgABT+eKeUWyLUAAAAAElFTkSuQmCC", 32px); + &.tree-rtl .tree-node { background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAACAQMAAAB49I5GAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMOBgAAGAAJMwQHdQAAAABJRU5ErkJggg=="); } + &.tree-rtl .tree-last { background:transparent; } +} +.tree-@{theme-name}-small { + .tree-theme(18px, "data:image/png;base64,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", 32px); + &.tree-rtl .tree-node { background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAACAQMAAABv1h6PAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMHBgAAiABBI4gz9AAAAABJRU5ErkJggg=="); } + &.tree-rtl .tree-last { background:transparent; } +} +.tree-@{theme-name}-large { + .tree-theme(32px, "data:image/png;base64,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", 32px); + &.tree-rtl .tree-node { background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAACAQMAAAAD0EyKAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjgIIGBgABCgCBvVLXcAAAAABJRU5ErkJggg=="); } + &.tree-rtl .tree-last { background:transparent; } +} \ No newline at end of file diff --git a/frontend/src/components/common/control/tree/less/mixins.less b/frontend/src/components/common/control/tree/less/mixins.less new file mode 100644 index 0000000..4d28042 --- /dev/null +++ b/frontend/src/components/common/control/tree/less/mixins.less @@ -0,0 +1,91 @@ +.tree-theme (@base-height, @image, @image-height) { + @correction: (@image-height - @base-height) / 2; + + .tree-node { min-height:@base-height; line-height:@base-height; margin-left:@base-height + 6; min-width:@base-height; } + .tree-anchor { line-height:@base-height; height:@base-height; } + .tree-icon { width:@base-height; height:@base-height; line-height:@base-height; } + .tree-icon:empty { width:@base-height; height:@base-height; line-height:@base-height; } + &.tree-rtl .tree-node { margin-right:@base-height; } + .tree-wholerow { height:@base-height; } + + .tree-node, + .tree-icon { background-image:url("@{image}"); } + .tree-node { background-position:-(@image-height * 9 + @correction) -(@correction); background-repeat:repeat-y; } + .tree-last { background:transparent; } + + .tree-open > .tree-ocl { background-position:-(@image-height * 4 + @correction) -(@correction); } + .tree-closed > .tree-ocl { background-position:-(@image-height * 3 + @correction) -(@correction); } + .tree-leaf > .tree-ocl { background-position:-(@image-height * 2 + @correction) -(@correction); } + + .tree-themeicon { background-position:-(@image-height * 8 + @correction) -(@correction); } + + > .tree-no-dots { + .tree-node, + .tree-leaf > .tree-ocl { background:transparent; } + .tree-open > .tree-ocl { background-position:-(@image-height * 1 + @correction) -(@correction); } + .tree-closed > .tree-ocl { background-position:-(@correction) -(@correction); } + } + + .tree-disabled { + background:transparent; + &.tree-hovered { + background:transparent; + } + &.tree-selected { + background:#efefef; + } + } + + .tree-checkbox { + background-position:-(@image-height * 5 + @correction) -(@correction); + &:hover { background-position:-(@image-height * 5 + @correction) -(@image-height * 1 + @correction); } + } + + &.tree-checkbox-selection .tree-selected, .tree-checked { + > .tree-checkbox { + background-position:-(@image-height * 7 + @correction) -(@correction); + &:hover { background-position:-(@image-height * 7 + @correction) -(@image-height * 1 + @correction); } + } + } + .tree-anchor { + > .tree-undetermined { + background-position:-(@image-height * 6 + @correction) -(@correction); + &:hover { + background-position:-(@image-height * 6 + @correction) -(@image-height * 1 + @correction); + } + } + } + .tree-checkbox-disabled { opacity:0.8; filter: url("data:image/svg+xml;utf8,#tree-grayscale"); /* Firefox 10+ */ filter: gray; /* IE6-9 */ -webkit-filter: grayscale(100%); /* Chrome 19+ & Safari 6+ */ } + + > .tree-striped { background-size:auto (@base-height * 2); } + + &.tree-rtl { + .tree-node { background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAACAQMAAAB49I5GAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMOBgAAGAAJMwQHdQAAAABJRU5ErkJggg=="); background-position: 100% 1px; background-repeat:repeat-y; } + .tree-last { background:transparent; } + .tree-open > .tree-ocl { background-position:-(@image-height * 4 + @correction) -(@image-height * 1 + @correction); } + .tree-closed > .tree-ocl { background-position:-(@image-height * 3 + @correction) -(@image-height * 1 + @correction); } + .tree-leaf > .tree-ocl { background-position:-(@image-height * 2 + @correction) -(@image-height * 1 + @correction); } + > .tree-no-dots { + .tree-node, + .tree-leaf > .tree-ocl { background:transparent; } + .tree-open > .tree-ocl { background-position:-(@image-height * 1 + @correction) -(@image-height * 1 + @correction); } + .tree-closed > .tree-ocl { background-position:-(@correction) -(@image-height * 1 + @correction); } + } + } + .tree-themeicon-custom { background-color:transparent; background-image:none; background-position:0 0; } + + .tree-node.tree-loading{background: none;} + + > .tree-container-ul .tree-loading > .tree-ocl { background:url("data:image/gif;base64,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") center center no-repeat; } + + .tree-file { background:url("@{image}") -(@image-height * 3 + @correction) -(@image-height * 2 + @correction) no-repeat; } + .tree-folder { background:url("@{image}") -(@image-height * 8 + @correction) -(@correction) no-repeat; } + + > .tree-container-ul > .tree-node { margin-left:0; margin-right:0; } + + // ellipsis + .tree-ellipsis { overflow: hidden; } + // base height + PADDINGS! + .tree-ellipsis .tree-anchor { width: calc(100% ~"-" - ( @base-height + 5px) ); text-overflow: ellipsis; overflow: hidden; } + .tree-ellipsis.tree-no-icons .tree-anchor { width: calc(100% ~"-" 5px); } +} \ No newline at end of file diff --git a/frontend/src/components/common/control/tree/less/style.less b/frontend/src/components/common/control/tree/less/style.less new file mode 100644 index 0000000..88abd0c --- /dev/null +++ b/frontend/src/components/common/control/tree/less/style.less @@ -0,0 +1,20 @@ +/* tree default theme */ +@theme-name: default; +@hovered-bg-color: #eee; +@hovered-shadow-color: #cccccc; +@disabled-color: #666666; +@disabled-bg-color: #efefef; +@clicked-bg-color: #e1e1e1; +@clicked-shadow-color: #999999; +@search-result-color: #8b0000; +@mobile-wholerow-bg-color: #ebebeb; +@mobile-wholerow-shadow: #666666; +@mobile-wholerow-bordert: rgba(255,255,255,0.7); +@mobile-wholerow-borderb: rgba(64,64,64,0.2); +@responsive: true; +@image-path: "./"; +@base-height: 40px; + +@import "./mixins.less"; +@import "./base.less"; +@import "./main.less"; \ No newline at end of file diff --git a/frontend/src/components/common/popup/ChangePasswordPopup.vue b/frontend/src/components/common/popup/ChangePasswordPopup.vue new file mode 100644 index 0000000..206d290 --- /dev/null +++ b/frontend/src/components/common/popup/ChangePasswordPopup.vue @@ -0,0 +1,132 @@ + + + + diff --git a/frontend/src/components/common/popup/DeptListPopup.vue b/frontend/src/components/common/popup/DeptListPopup.vue new file mode 100644 index 0000000..01b2e3a --- /dev/null +++ b/frontend/src/components/common/popup/DeptListPopup.vue @@ -0,0 +1,205 @@ + + + + diff --git a/frontend/src/components/common/popup/EntrustApprovalPopup.vue b/frontend/src/components/common/popup/EntrustApprovalPopup.vue new file mode 100644 index 0000000..5296ce5 --- /dev/null +++ b/frontend/src/components/common/popup/EntrustApprovalPopup.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/frontend/src/components/common/popup/MenuListPopup.vue b/frontend/src/components/common/popup/MenuListPopup.vue new file mode 100644 index 0000000..490d309 --- /dev/null +++ b/frontend/src/components/common/popup/MenuListPopup.vue @@ -0,0 +1,201 @@ + + + + diff --git a/frontend/src/components/common/popup/RoleFormPopup.vue b/frontend/src/components/common/popup/RoleFormPopup.vue new file mode 100644 index 0000000..49b6867 --- /dev/null +++ b/frontend/src/components/common/popup/RoleFormPopup.vue @@ -0,0 +1,186 @@ + + + + diff --git a/frontend/src/components/common/popup/RoleListPopup.vue b/frontend/src/components/common/popup/RoleListPopup.vue new file mode 100644 index 0000000..0494839 --- /dev/null +++ b/frontend/src/components/common/popup/RoleListPopup.vue @@ -0,0 +1,268 @@ + + + + + diff --git a/frontend/src/components/common/popup/SentMailPopup.vue b/frontend/src/components/common/popup/SentMailPopup.vue new file mode 100644 index 0000000..5e8608c --- /dev/null +++ b/frontend/src/components/common/popup/SentMailPopup.vue @@ -0,0 +1,47 @@ + + + + diff --git a/frontend/src/components/common/popup/TermPopup.vue b/frontend/src/components/common/popup/TermPopup.vue new file mode 100644 index 0000000..7bd3769 --- /dev/null +++ b/frontend/src/components/common/popup/TermPopup.vue @@ -0,0 +1,79 @@ + + + + diff --git a/frontend/src/components/common/popup/UserListPopup.vue b/frontend/src/components/common/popup/UserListPopup.vue new file mode 100644 index 0000000..df963a9 --- /dev/null +++ b/frontend/src/components/common/popup/UserListPopup.vue @@ -0,0 +1,421 @@ + + + + + diff --git a/frontend/src/components/common/popup/WorkgroupFormPopup.vue b/frontend/src/components/common/popup/WorkgroupFormPopup.vue new file mode 100644 index 0000000..5866284 --- /dev/null +++ b/frontend/src/components/common/popup/WorkgroupFormPopup.vue @@ -0,0 +1,187 @@ + + + + diff --git a/frontend/src/components/common/popup/WorkgroupListPopup.vue b/frontend/src/components/common/popup/WorkgroupListPopup.vue new file mode 100644 index 0000000..eab73cd --- /dev/null +++ b/frontend/src/components/common/popup/WorkgroupListPopup.vue @@ -0,0 +1,259 @@ + + + + + diff --git a/frontend/src/components/popup/SoldToCodePopup.vue b/frontend/src/components/popup/SoldToCodePopup.vue new file mode 100644 index 0000000..cdb4efa --- /dev/null +++ b/frontend/src/components/popup/SoldToCodePopup.vue @@ -0,0 +1,421 @@ + + + + + diff --git a/frontend/src/components/selectors/MarketingNameSelector.vue b/frontend/src/components/selectors/MarketingNameSelector.vue new file mode 100644 index 0000000..385d068 --- /dev/null +++ b/frontend/src/components/selectors/MarketingNameSelector.vue @@ -0,0 +1,32 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/selectors/MonthSelector.vue b/frontend/src/components/selectors/MonthSelector.vue new file mode 100644 index 0000000..5cacc85 --- /dev/null +++ b/frontend/src/components/selectors/MonthSelector.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/frontend/src/components/selectors/ProductGroupSelector.vue b/frontend/src/components/selectors/ProductGroupSelector.vue new file mode 100644 index 0000000..705dd47 --- /dev/null +++ b/frontend/src/components/selectors/ProductGroupSelector.vue @@ -0,0 +1,32 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/selectors/QuarterSelector.vue b/frontend/src/components/selectors/QuarterSelector.vue new file mode 100644 index 0000000..96d4549 --- /dev/null +++ b/frontend/src/components/selectors/QuarterSelector.vue @@ -0,0 +1,34 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/selectors/RangeWeekSelector copy.vue b/frontend/src/components/selectors/RangeWeekSelector copy.vue new file mode 100644 index 0000000..f124e29 --- /dev/null +++ b/frontend/src/components/selectors/RangeWeekSelector copy.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/frontend/src/components/selectors/RangeWeekSelector.vue b/frontend/src/components/selectors/RangeWeekSelector.vue new file mode 100644 index 0000000..151ac6b --- /dev/null +++ b/frontend/src/components/selectors/RangeWeekSelector.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/frontend/src/components/selectors/SkuSelector.vue b/frontend/src/components/selectors/SkuSelector.vue new file mode 100644 index 0000000..385d068 --- /dev/null +++ b/frontend/src/components/selectors/SkuSelector.vue @@ -0,0 +1,32 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/selectors/SoldToCodeSelector.vue b/frontend/src/components/selectors/SoldToCodeSelector.vue new file mode 100644 index 0000000..e0ec482 --- /dev/null +++ b/frontend/src/components/selectors/SoldToCodeSelector.vue @@ -0,0 +1,67 @@ + + + + diff --git a/frontend/src/components/selectors/WeekSelector.vue b/frontend/src/components/selectors/WeekSelector.vue new file mode 100644 index 0000000..dffd89c --- /dev/null +++ b/frontend/src/components/selectors/WeekSelector.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/frontend/src/components/selectors/YearSelector.vue b/frontend/src/components/selectors/YearSelector.vue new file mode 100644 index 0000000..6ad0584 --- /dev/null +++ b/frontend/src/components/selectors/YearSelector.vue @@ -0,0 +1,38 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/view/admin/approval/ApprMgmtDtl.vue b/frontend/src/components/view/admin/approval/ApprMgmtDtl.vue new file mode 100644 index 0000000..b40fdf9 --- /dev/null +++ b/frontend/src/components/view/admin/approval/ApprMgmtDtl.vue @@ -0,0 +1,330 @@ + + + + + + diff --git a/frontend/src/components/view/admin/approval/ApprMgmtList.vue b/frontend/src/components/view/admin/approval/ApprMgmtList.vue new file mode 100644 index 0000000..c42ea40 --- /dev/null +++ b/frontend/src/components/view/admin/approval/ApprMgmtList.vue @@ -0,0 +1,334 @@ + + + + + + diff --git a/frontend/src/components/view/admin/approval/ApprovalDetail.vue b/frontend/src/components/view/admin/approval/ApprovalDetail.vue new file mode 100644 index 0000000..84132ba --- /dev/null +++ b/frontend/src/components/view/admin/approval/ApprovalDetail.vue @@ -0,0 +1,288 @@ + + + + + + diff --git a/frontend/src/components/view/admin/approval/ApprovalList.vue b/frontend/src/components/view/admin/approval/ApprovalList.vue new file mode 100644 index 0000000..c1651b1 --- /dev/null +++ b/frontend/src/components/view/admin/approval/ApprovalList.vue @@ -0,0 +1,256 @@ + + + + + + diff --git a/frontend/src/components/view/admin/approval/DynamicApprPathDtl.vue b/frontend/src/components/view/admin/approval/DynamicApprPathDtl.vue new file mode 100644 index 0000000..786dc74 --- /dev/null +++ b/frontend/src/components/view/admin/approval/DynamicApprPathDtl.vue @@ -0,0 +1,1200 @@ + + + + + diff --git a/frontend/src/components/view/admin/approval/DynamicApprPathMgmt.vue b/frontend/src/components/view/admin/approval/DynamicApprPathMgmt.vue new file mode 100644 index 0000000..732d3a3 --- /dev/null +++ b/frontend/src/components/view/admin/approval/DynamicApprPathMgmt.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/frontend/src/components/view/admin/approval/popup/CommentPopup.vue b/frontend/src/components/view/admin/approval/popup/CommentPopup.vue new file mode 100644 index 0000000..434d06b --- /dev/null +++ b/frontend/src/components/view/admin/approval/popup/CommentPopup.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/frontend/src/components/view/admin/approval/popup/RequiredApprListPopup.vue b/frontend/src/components/view/admin/approval/popup/RequiredApprListPopup.vue new file mode 100644 index 0000000..9c57b5a --- /dev/null +++ b/frontend/src/components/view/admin/approval/popup/RequiredApprListPopup.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/frontend/src/components/view/admin/approvalTemplate/ApprTemplateEdit.vue b/frontend/src/components/view/admin/approvalTemplate/ApprTemplateEdit.vue new file mode 100644 index 0000000..b775172 --- /dev/null +++ b/frontend/src/components/view/admin/approvalTemplate/ApprTemplateEdit.vue @@ -0,0 +1,292 @@ + + + + + diff --git a/frontend/src/components/view/admin/approvalTemplate/ApprTemplateList.vue b/frontend/src/components/view/admin/approvalTemplate/ApprTemplateList.vue new file mode 100644 index 0000000..60fed6c --- /dev/null +++ b/frontend/src/components/view/admin/approvalTemplate/ApprTemplateList.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/frontend/src/components/view/admin/approvalTemplate/popup/ApprTemplatePreview.vue b/frontend/src/components/view/admin/approvalTemplate/popup/ApprTemplatePreview.vue new file mode 100644 index 0000000..eb7d46e --- /dev/null +++ b/frontend/src/components/view/admin/approvalTemplate/popup/ApprTemplatePreview.vue @@ -0,0 +1,58 @@ + + + + diff --git a/frontend/src/components/view/admin/batch/BatchJobLogList.vue b/frontend/src/components/view/admin/batch/BatchJobLogList.vue new file mode 100644 index 0000000..0b06614 --- /dev/null +++ b/frontend/src/components/view/admin/batch/BatchJobLogList.vue @@ -0,0 +1,226 @@ + + + + + + diff --git a/frontend/src/components/view/admin/batch/BatchJobMgmt.vue b/frontend/src/components/view/admin/batch/BatchJobMgmt.vue new file mode 100644 index 0000000..ae16ff8 --- /dev/null +++ b/frontend/src/components/view/admin/batch/BatchJobMgmt.vue @@ -0,0 +1,308 @@ + + + + + + diff --git a/frontend/src/components/view/admin/batch/popup/BatchJobPopup.vue b/frontend/src/components/view/admin/batch/popup/BatchJobPopup.vue new file mode 100644 index 0000000..7af873d --- /dev/null +++ b/frontend/src/components/view/admin/batch/popup/BatchJobPopup.vue @@ -0,0 +1,201 @@ + + + + + diff --git a/frontend/src/components/view/admin/board/BoardDetail.vue b/frontend/src/components/view/admin/board/BoardDetail.vue new file mode 100644 index 0000000..d5baa57 --- /dev/null +++ b/frontend/src/components/view/admin/board/BoardDetail.vue @@ -0,0 +1,697 @@ + + + + + + diff --git a/frontend/src/components/view/admin/board/BoardLink.vue b/frontend/src/components/view/admin/board/BoardLink.vue new file mode 100644 index 0000000..279d062 --- /dev/null +++ b/frontend/src/components/view/admin/board/BoardLink.vue @@ -0,0 +1,199 @@ + + + + + + diff --git a/frontend/src/components/view/admin/board/BoardList.vue b/frontend/src/components/view/admin/board/BoardList.vue new file mode 100644 index 0000000..5cb9e5e --- /dev/null +++ b/frontend/src/components/view/admin/board/BoardList.vue @@ -0,0 +1,164 @@ + + + + + + diff --git a/frontend/src/components/view/admin/board/PostEdit.vue b/frontend/src/components/view/admin/board/PostEdit.vue new file mode 100644 index 0000000..08523a5 --- /dev/null +++ b/frontend/src/components/view/admin/board/PostEdit.vue @@ -0,0 +1,457 @@ + + + + + + diff --git a/frontend/src/components/view/admin/board/PostList.vue b/frontend/src/components/view/admin/board/PostList.vue new file mode 100644 index 0000000..c04f8fa --- /dev/null +++ b/frontend/src/components/view/admin/board/PostList.vue @@ -0,0 +1,530 @@ + + + + + diff --git a/frontend/src/components/view/admin/board/PostPopup.vue b/frontend/src/components/view/admin/board/PostPopup.vue new file mode 100644 index 0000000..c29bbe5 --- /dev/null +++ b/frontend/src/components/view/admin/board/PostPopup.vue @@ -0,0 +1,43 @@ + + + + diff --git a/frontend/src/components/view/admin/board/PostView.vue b/frontend/src/components/view/admin/board/PostView.vue new file mode 100644 index 0000000..3fdb7b9 --- /dev/null +++ b/frontend/src/components/view/admin/board/PostView.vue @@ -0,0 +1,481 @@ + + + + + diff --git a/frontend/src/components/view/admin/dept/DeptList.vue b/frontend/src/components/view/admin/dept/DeptList.vue new file mode 100644 index 0000000..f3e5d2a --- /dev/null +++ b/frontend/src/components/view/admin/dept/DeptList.vue @@ -0,0 +1,918 @@ + + + + diff --git a/frontend/src/components/view/admin/dept/LdapDeptList.vue b/frontend/src/components/view/admin/dept/LdapDeptList.vue new file mode 100644 index 0000000..e678bed --- /dev/null +++ b/frontend/src/components/view/admin/dept/LdapDeptList.vue @@ -0,0 +1,268 @@ + + + + diff --git a/frontend/src/components/view/admin/etc/AttachMgmt.vue b/frontend/src/components/view/admin/etc/AttachMgmt.vue new file mode 100644 index 0000000..7da95b0 --- /dev/null +++ b/frontend/src/components/view/admin/etc/AttachMgmt.vue @@ -0,0 +1,245 @@ + + + + diff --git a/frontend/src/components/view/admin/etc/CacheConfigList.vue b/frontend/src/components/view/admin/etc/CacheConfigList.vue new file mode 100644 index 0000000..51a7d15 --- /dev/null +++ b/frontend/src/components/view/admin/etc/CacheConfigList.vue @@ -0,0 +1,55 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/view/admin/etc/CommcodeDetail.vue b/frontend/src/components/view/admin/etc/CommcodeDetail.vue new file mode 100644 index 0000000..31bd8d4 --- /dev/null +++ b/frontend/src/components/view/admin/etc/CommcodeDetail.vue @@ -0,0 +1,642 @@ + + + + + + diff --git a/frontend/src/components/view/admin/etc/CommcodeList.vue b/frontend/src/components/view/admin/etc/CommcodeList.vue new file mode 100644 index 0000000..b95fbac --- /dev/null +++ b/frontend/src/components/view/admin/etc/CommcodeList.vue @@ -0,0 +1,156 @@ + + + + diff --git a/frontend/src/components/view/admin/etc/IpMgmt.vue b/frontend/src/components/view/admin/etc/IpMgmt.vue new file mode 100644 index 0000000..7f32cfd --- /dev/null +++ b/frontend/src/components/view/admin/etc/IpMgmt.vue @@ -0,0 +1,331 @@ + + + + diff --git a/frontend/src/components/view/admin/etc/LinkSiteList.vue b/frontend/src/components/view/admin/etc/LinkSiteList.vue new file mode 100644 index 0000000..24c2eb7 --- /dev/null +++ b/frontend/src/components/view/admin/etc/LinkSiteList.vue @@ -0,0 +1,317 @@ + + + + + + diff --git a/frontend/src/components/view/admin/etc/LoggerConfigList.vue b/frontend/src/components/view/admin/etc/LoggerConfigList.vue new file mode 100644 index 0000000..c0dfe5c --- /dev/null +++ b/frontend/src/components/view/admin/etc/LoggerConfigList.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/frontend/src/components/view/admin/etc/MailGroupList.vue b/frontend/src/components/view/admin/etc/MailGroupList.vue new file mode 100644 index 0000000..2c167a1 --- /dev/null +++ b/frontend/src/components/view/admin/etc/MailGroupList.vue @@ -0,0 +1,315 @@ + + + + diff --git a/frontend/src/components/view/admin/etc/MailGroupMappEdit.vue b/frontend/src/components/view/admin/etc/MailGroupMappEdit.vue new file mode 100644 index 0000000..387594e --- /dev/null +++ b/frontend/src/components/view/admin/etc/MailGroupMappEdit.vue @@ -0,0 +1,553 @@ + + + + + diff --git a/frontend/src/components/view/admin/etc/TermsConditionDetail.vue b/frontend/src/components/view/admin/etc/TermsConditionDetail.vue new file mode 100644 index 0000000..1fb48f6 --- /dev/null +++ b/frontend/src/components/view/admin/etc/TermsConditionDetail.vue @@ -0,0 +1,408 @@ + + + + + + diff --git a/frontend/src/components/view/admin/etc/TermsConditionList.vue b/frontend/src/components/view/admin/etc/TermsConditionList.vue new file mode 100644 index 0000000..0f8ec45 --- /dev/null +++ b/frontend/src/components/view/admin/etc/TermsConditionList.vue @@ -0,0 +1,175 @@ + + + + diff --git a/frontend/src/components/view/admin/etc/TermsPreviewPopup.vue b/frontend/src/components/view/admin/etc/TermsPreviewPopup.vue new file mode 100644 index 0000000..e10f1d3 --- /dev/null +++ b/frontend/src/components/view/admin/etc/TermsPreviewPopup.vue @@ -0,0 +1,32 @@ + + + diff --git a/frontend/src/components/view/admin/etc/popup/CommcodePopup.vue b/frontend/src/components/view/admin/etc/popup/CommcodePopup.vue new file mode 100644 index 0000000..c09a4f0 --- /dev/null +++ b/frontend/src/components/view/admin/etc/popup/CommcodePopup.vue @@ -0,0 +1,227 @@ + + + + diff --git a/frontend/src/components/view/admin/etc/popup/LinkSitePopup.vue b/frontend/src/components/view/admin/etc/popup/LinkSitePopup.vue new file mode 100644 index 0000000..5cdcb40 --- /dev/null +++ b/frontend/src/components/view/admin/etc/popup/LinkSitePopup.vue @@ -0,0 +1,213 @@ + + + + + diff --git a/frontend/src/components/view/admin/etc/popup/MailGroupFormPopup.vue b/frontend/src/components/view/admin/etc/popup/MailGroupFormPopup.vue new file mode 100644 index 0000000..87be4d0 --- /dev/null +++ b/frontend/src/components/view/admin/etc/popup/MailGroupFormPopup.vue @@ -0,0 +1,153 @@ + + + diff --git a/frontend/src/components/view/admin/htmlTemplate/HtmlTemplateEdit.vue b/frontend/src/components/view/admin/htmlTemplate/HtmlTemplateEdit.vue new file mode 100644 index 0000000..2df97c8 --- /dev/null +++ b/frontend/src/components/view/admin/htmlTemplate/HtmlTemplateEdit.vue @@ -0,0 +1,293 @@ + + + + + diff --git a/frontend/src/components/view/admin/htmlTemplate/HtmlTemplateList.vue b/frontend/src/components/view/admin/htmlTemplate/HtmlTemplateList.vue new file mode 100644 index 0000000..4636486 --- /dev/null +++ b/frontend/src/components/view/admin/htmlTemplate/HtmlTemplateList.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/frontend/src/components/view/admin/htmlTemplate/popup/HtmlTemplatePreview.vue b/frontend/src/components/view/admin/htmlTemplate/popup/HtmlTemplatePreview.vue new file mode 100644 index 0000000..807f3f7 --- /dev/null +++ b/frontend/src/components/view/admin/htmlTemplate/popup/HtmlTemplatePreview.vue @@ -0,0 +1,58 @@ + + + + diff --git a/frontend/src/components/view/admin/log/FileDownloadHistory.vue b/frontend/src/components/view/admin/log/FileDownloadHistory.vue new file mode 100644 index 0000000..f79961c --- /dev/null +++ b/frontend/src/components/view/admin/log/FileDownloadHistory.vue @@ -0,0 +1,232 @@ + + + + + diff --git a/frontend/src/components/view/admin/log/LoginHistory.vue b/frontend/src/components/view/admin/log/LoginHistory.vue new file mode 100644 index 0000000..f668749 --- /dev/null +++ b/frontend/src/components/view/admin/log/LoginHistory.vue @@ -0,0 +1,208 @@ + + + + + diff --git a/frontend/src/components/view/admin/log/MenuUseHistory.vue b/frontend/src/components/view/admin/log/MenuUseHistory.vue new file mode 100644 index 0000000..5cc8d0f --- /dev/null +++ b/frontend/src/components/view/admin/log/MenuUseHistory.vue @@ -0,0 +1,259 @@ + + + + + diff --git a/frontend/src/components/view/admin/log/MenuUtilHistory.vue b/frontend/src/components/view/admin/log/MenuUtilHistory.vue new file mode 100644 index 0000000..c734561 --- /dev/null +++ b/frontend/src/components/view/admin/log/MenuUtilHistory.vue @@ -0,0 +1,196 @@ + + + + + diff --git a/frontend/src/components/view/admin/log/MenuUtilSolvHistory.vue b/frontend/src/components/view/admin/log/MenuUtilSolvHistory.vue new file mode 100644 index 0000000..d66bce4 --- /dev/null +++ b/frontend/src/components/view/admin/log/MenuUtilSolvHistory.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/frontend/src/components/view/admin/log/SentMailHistoryInfo.vue b/frontend/src/components/view/admin/log/SentMailHistoryInfo.vue new file mode 100644 index 0000000..7ddb529 --- /dev/null +++ b/frontend/src/components/view/admin/log/SentMailHistoryInfo.vue @@ -0,0 +1,155 @@ + + + + diff --git a/frontend/src/components/view/admin/log/SentMailHistoryList.vue b/frontend/src/components/view/admin/log/SentMailHistoryList.vue new file mode 100644 index 0000000..5631607 --- /dev/null +++ b/frontend/src/components/view/admin/log/SentMailHistoryList.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/frontend/src/components/view/admin/log/popup/ApiUseHistory.vue b/frontend/src/components/view/admin/log/popup/ApiUseHistory.vue new file mode 100644 index 0000000..41eb840 --- /dev/null +++ b/frontend/src/components/view/admin/log/popup/ApiUseHistory.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/frontend/src/components/view/admin/main/EmptyPage.vue b/frontend/src/components/view/admin/main/EmptyPage.vue new file mode 100644 index 0000000..66fef75 --- /dev/null +++ b/frontend/src/components/view/admin/main/EmptyPage.vue @@ -0,0 +1,138 @@ + + + + + + diff --git a/frontend/src/components/view/admin/main/InitPassword.vue b/frontend/src/components/view/admin/main/InitPassword.vue new file mode 100644 index 0000000..078baae --- /dev/null +++ b/frontend/src/components/view/admin/main/InitPassword.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/frontend/src/components/view/admin/main/LoginOut.vue b/frontend/src/components/view/admin/main/LoginOut.vue new file mode 100644 index 0000000..d82ce16 --- /dev/null +++ b/frontend/src/components/view/admin/main/LoginOut.vue @@ -0,0 +1,56 @@ + + + diff --git a/frontend/src/components/view/admin/main/LoginPage.vue b/frontend/src/components/view/admin/main/LoginPage.vue new file mode 100644 index 0000000..654abe5 --- /dev/null +++ b/frontend/src/components/view/admin/main/LoginPage.vue @@ -0,0 +1,311 @@ + + + diff --git a/frontend/src/components/view/admin/main/MainIndexPage.vue b/frontend/src/components/view/admin/main/MainIndexPage.vue new file mode 100644 index 0000000..bfdab58 --- /dev/null +++ b/frontend/src/components/view/admin/main/MainIndexPage.vue @@ -0,0 +1,552 @@ + + + + diff --git a/frontend/src/components/view/admin/main/MainOffsider.vue b/frontend/src/components/view/admin/main/MainOffsider.vue new file mode 100644 index 0000000..a080da6 --- /dev/null +++ b/frontend/src/components/view/admin/main/MainOffsider.vue @@ -0,0 +1,515 @@ + + + diff --git a/frontend/src/components/view/admin/main/MainPage.vue b/frontend/src/components/view/admin/main/MainPage.vue new file mode 100644 index 0000000..66a0cf4 --- /dev/null +++ b/frontend/src/components/view/admin/main/MainPage.vue @@ -0,0 +1,251 @@ + + + + + + diff --git a/frontend/src/components/view/admin/main/MainSideMenu.vue b/frontend/src/components/view/admin/main/MainSideMenu.vue new file mode 100644 index 0000000..d3a9565 --- /dev/null +++ b/frontend/src/components/view/admin/main/MainSideMenu.vue @@ -0,0 +1,145 @@ + + + diff --git a/frontend/src/components/view/admin/main/MainTopMenu.vue b/frontend/src/components/view/admin/main/MainTopMenu.vue new file mode 100644 index 0000000..fb5d3c8 --- /dev/null +++ b/frontend/src/components/view/admin/main/MainTopMenu.vue @@ -0,0 +1,582 @@ + + + + + + diff --git a/frontend/src/components/view/admin/main/NoAuthPage.vue b/frontend/src/components/view/admin/main/NoAuthPage.vue new file mode 100644 index 0000000..4e8b552 --- /dev/null +++ b/frontend/src/components/view/admin/main/NoAuthPage.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/frontend/src/components/view/admin/main/RegisterPage.vue b/frontend/src/components/view/admin/main/RegisterPage.vue new file mode 100644 index 0000000..5858957 --- /dev/null +++ b/frontend/src/components/view/admin/main/RegisterPage.vue @@ -0,0 +1,289 @@ + + + + + + diff --git a/frontend/src/components/view/admin/main/TermsAndConditionsPage.vue b/frontend/src/components/view/admin/main/TermsAndConditionsPage.vue new file mode 100644 index 0000000..d016155 --- /dev/null +++ b/frontend/src/components/view/admin/main/TermsAndConditionsPage.vue @@ -0,0 +1,134 @@ + + + + + + diff --git a/frontend/src/components/view/admin/main/TransPopup.vue b/frontend/src/components/view/admin/main/TransPopup.vue new file mode 100644 index 0000000..bca1989 --- /dev/null +++ b/frontend/src/components/view/admin/main/TransPopup.vue @@ -0,0 +1,339 @@ + + + + diff --git a/frontend/src/components/view/admin/main/popup/SearchPopup.vue b/frontend/src/components/view/admin/main/popup/SearchPopup.vue new file mode 100644 index 0000000..0fc66c2 --- /dev/null +++ b/frontend/src/components/view/admin/main/popup/SearchPopup.vue @@ -0,0 +1,37 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/view/admin/main/popup/SitemapPopup.vue b/frontend/src/components/view/admin/main/popup/SitemapPopup.vue new file mode 100644 index 0000000..8c2deec --- /dev/null +++ b/frontend/src/components/view/admin/main/popup/SitemapPopup.vue @@ -0,0 +1,106 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/view/admin/main/popup/TimeZonePopup.vue b/frontend/src/components/view/admin/main/popup/TimeZonePopup.vue new file mode 100644 index 0000000..5ac6312 --- /dev/null +++ b/frontend/src/components/view/admin/main/popup/TimeZonePopup.vue @@ -0,0 +1,119 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/view/admin/main/popup/TransPopup.vue b/frontend/src/components/view/admin/main/popup/TransPopup.vue new file mode 100644 index 0000000..ea8d477 --- /dev/null +++ b/frontend/src/components/view/admin/main/popup/TransPopup.vue @@ -0,0 +1,340 @@ + + + + diff --git a/frontend/src/components/view/admin/menu/MenuManagement.vue b/frontend/src/components/view/admin/menu/MenuManagement.vue new file mode 100644 index 0000000..728f9cf --- /dev/null +++ b/frontend/src/components/view/admin/menu/MenuManagement.vue @@ -0,0 +1,1407 @@ + + + + + + diff --git a/frontend/src/components/view/admin/menu/SelectBoardPopup.vue b/frontend/src/components/view/admin/menu/SelectBoardPopup.vue new file mode 100644 index 0000000..07c10df --- /dev/null +++ b/frontend/src/components/view/admin/menu/SelectBoardPopup.vue @@ -0,0 +1,110 @@ + + + + + + diff --git a/frontend/src/components/view/admin/role/RoleList.vue b/frontend/src/components/view/admin/role/RoleList.vue new file mode 100644 index 0000000..e014d11 --- /dev/null +++ b/frontend/src/components/view/admin/role/RoleList.vue @@ -0,0 +1,377 @@ + + + diff --git a/frontend/src/components/view/admin/role/RoleUserInfo.vue b/frontend/src/components/view/admin/role/RoleUserInfo.vue new file mode 100644 index 0000000..776d877 --- /dev/null +++ b/frontend/src/components/view/admin/role/RoleUserInfo.vue @@ -0,0 +1,578 @@ + + + diff --git a/frontend/src/components/view/admin/role/RoleWorkgroupInfo.vue b/frontend/src/components/view/admin/role/RoleWorkgroupInfo.vue new file mode 100644 index 0000000..4c37ae2 --- /dev/null +++ b/frontend/src/components/view/admin/role/RoleWorkgroupInfo.vue @@ -0,0 +1,381 @@ + + + diff --git a/frontend/src/components/view/admin/sample/ApprSampleEdit.vue b/frontend/src/components/view/admin/sample/ApprSampleEdit.vue new file mode 100644 index 0000000..cbe1000 --- /dev/null +++ b/frontend/src/components/view/admin/sample/ApprSampleEdit.vue @@ -0,0 +1,237 @@ + + + + + + diff --git a/frontend/src/components/view/admin/sample/ApprSampleList.vue b/frontend/src/components/view/admin/sample/ApprSampleList.vue new file mode 100644 index 0000000..a3d9bd9 --- /dev/null +++ b/frontend/src/components/view/admin/sample/ApprSampleList.vue @@ -0,0 +1,217 @@ + + + + + + diff --git a/frontend/src/components/view/admin/sample/ApprSampleWrite.vue b/frontend/src/components/view/admin/sample/ApprSampleWrite.vue new file mode 100644 index 0000000..dbc7105 --- /dev/null +++ b/frontend/src/components/view/admin/sample/ApprSampleWrite.vue @@ -0,0 +1,614 @@ + + + + + + diff --git a/frontend/src/components/view/admin/sample/SampleExcelUpload.vue b/frontend/src/components/view/admin/sample/SampleExcelUpload.vue new file mode 100644 index 0000000..71dff50 --- /dev/null +++ b/frontend/src/components/view/admin/sample/SampleExcelUpload.vue @@ -0,0 +1,62 @@ + + + + + + diff --git a/frontend/src/components/view/admin/sample/SampleStaff.vue b/frontend/src/components/view/admin/sample/SampleStaff.vue new file mode 100644 index 0000000..11a9141 --- /dev/null +++ b/frontend/src/components/view/admin/sample/SampleStaff.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/frontend/src/components/view/admin/sample/approval/internal/ApprSampleEdit.vue b/frontend/src/components/view/admin/sample/approval/internal/ApprSampleEdit.vue new file mode 100644 index 0000000..fae580f --- /dev/null +++ b/frontend/src/components/view/admin/sample/approval/internal/ApprSampleEdit.vue @@ -0,0 +1,224 @@ + + + + + + diff --git a/frontend/src/components/view/admin/sample/approval/internal/ApprSampleList.vue b/frontend/src/components/view/admin/sample/approval/internal/ApprSampleList.vue new file mode 100644 index 0000000..3e314b3 --- /dev/null +++ b/frontend/src/components/view/admin/sample/approval/internal/ApprSampleList.vue @@ -0,0 +1,198 @@ + + + + + + diff --git a/frontend/src/components/view/admin/sample/approval/internal/ApprSampleWrite.vue b/frontend/src/components/view/admin/sample/approval/internal/ApprSampleWrite.vue new file mode 100644 index 0000000..cbb0f45 --- /dev/null +++ b/frontend/src/components/view/admin/sample/approval/internal/ApprSampleWrite.vue @@ -0,0 +1,585 @@ + + + + + + diff --git a/frontend/src/components/view/admin/sample/approval/internal/popup/ApprPreview.vue b/frontend/src/components/view/admin/sample/approval/internal/popup/ApprPreview.vue new file mode 100644 index 0000000..fb5ec63 --- /dev/null +++ b/frontend/src/components/view/admin/sample/approval/internal/popup/ApprPreview.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/frontend/src/components/view/admin/sample/approval/knox/ApprSampleEdit.vue b/frontend/src/components/view/admin/sample/approval/knox/ApprSampleEdit.vue new file mode 100644 index 0000000..ec01ef3 --- /dev/null +++ b/frontend/src/components/view/admin/sample/approval/knox/ApprSampleEdit.vue @@ -0,0 +1,224 @@ + + + + + + diff --git a/frontend/src/components/view/admin/sample/approval/knox/ApprSampleList.vue b/frontend/src/components/view/admin/sample/approval/knox/ApprSampleList.vue new file mode 100644 index 0000000..99bea38 --- /dev/null +++ b/frontend/src/components/view/admin/sample/approval/knox/ApprSampleList.vue @@ -0,0 +1,201 @@ + + + + + + diff --git a/frontend/src/components/view/admin/sample/approval/knox/ApprSampleWrite.vue b/frontend/src/components/view/admin/sample/approval/knox/ApprSampleWrite.vue new file mode 100644 index 0000000..7bb7d7f --- /dev/null +++ b/frontend/src/components/view/admin/sample/approval/knox/ApprSampleWrite.vue @@ -0,0 +1,586 @@ + + + + + + diff --git a/frontend/src/components/view/admin/sample/approval/knox/popup/ApprPreview.vue b/frontend/src/components/view/admin/sample/approval/knox/popup/ApprPreview.vue new file mode 100644 index 0000000..1912294 --- /dev/null +++ b/frontend/src/components/view/admin/sample/approval/knox/popup/ApprPreview.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/frontend/src/components/view/admin/sample/popup/ApprPreview.vue b/frontend/src/components/view/admin/sample/popup/ApprPreview.vue new file mode 100644 index 0000000..9ed46d1 --- /dev/null +++ b/frontend/src/components/view/admin/sample/popup/ApprPreview.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/frontend/src/components/view/admin/user/ExternalUserList.vue b/frontend/src/components/view/admin/user/ExternalUserList.vue new file mode 100644 index 0000000..59d4fa3 --- /dev/null +++ b/frontend/src/components/view/admin/user/ExternalUserList.vue @@ -0,0 +1,251 @@ + + + + + + diff --git a/frontend/src/components/view/admin/user/UserInfo.vue b/frontend/src/components/view/admin/user/UserInfo.vue new file mode 100644 index 0000000..306f3ab --- /dev/null +++ b/frontend/src/components/view/admin/user/UserInfo.vue @@ -0,0 +1,1186 @@ + + + + diff --git a/frontend/src/components/view/admin/user/UserList.vue b/frontend/src/components/view/admin/user/UserList.vue new file mode 100644 index 0000000..786ff66 --- /dev/null +++ b/frontend/src/components/view/admin/user/UserList.vue @@ -0,0 +1,245 @@ + + + + diff --git a/frontend/src/components/view/admin/user/UserMgmtList.vue b/frontend/src/components/view/admin/user/UserMgmtList.vue new file mode 100644 index 0000000..998627c --- /dev/null +++ b/frontend/src/components/view/admin/user/UserMgmtList.vue @@ -0,0 +1,623 @@ + + + + diff --git a/frontend/src/components/view/admin/workgroup/WorkgroupList.vue b/frontend/src/components/view/admin/workgroup/WorkgroupList.vue new file mode 100644 index 0000000..fcdaa3b --- /dev/null +++ b/frontend/src/components/view/admin/workgroup/WorkgroupList.vue @@ -0,0 +1,379 @@ + + + diff --git a/frontend/src/components/view/admin/workgroup/WorkgroupMenuInfo.vue b/frontend/src/components/view/admin/workgroup/WorkgroupMenuInfo.vue new file mode 100644 index 0000000..4ddab9a --- /dev/null +++ b/frontend/src/components/view/admin/workgroup/WorkgroupMenuInfo.vue @@ -0,0 +1,584 @@ + + + diff --git a/frontend/src/components/view/admin/workgroup/WorkgroupRoleInfo.vue b/frontend/src/components/view/admin/workgroup/WorkgroupRoleInfo.vue new file mode 100644 index 0000000..a28d6d7 --- /dev/null +++ b/frontend/src/components/view/admin/workgroup/WorkgroupRoleInfo.vue @@ -0,0 +1,630 @@ + + + diff --git a/frontend/src/components/view/formula/fmDragTree.vue b/frontend/src/components/view/formula/fmDragTree.vue new file mode 100644 index 0000000..d58ff9d --- /dev/null +++ b/frontend/src/components/view/formula/fmDragTree.vue @@ -0,0 +1,221 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/view/formula/fmGrid.vue b/frontend/src/components/view/formula/fmGrid.vue new file mode 100644 index 0000000..4c86b04 --- /dev/null +++ b/frontend/src/components/view/formula/fmGrid.vue @@ -0,0 +1,188 @@ + + + + + diff --git a/frontend/src/components/view/formula/fmTree.vue b/frontend/src/components/view/formula/fmTree.vue new file mode 100644 index 0000000..46235ae --- /dev/null +++ b/frontend/src/components/view/formula/fmTree.vue @@ -0,0 +1,94 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/view/formula/formulaEdit.vue b/frontend/src/components/view/formula/formulaEdit.vue new file mode 100644 index 0000000..3314591 --- /dev/null +++ b/frontend/src/components/view/formula/formulaEdit.vue @@ -0,0 +1,219 @@ + + + + diff --git a/frontend/src/components/view/formula/formulaList.vue b/frontend/src/components/view/formula/formulaList.vue new file mode 100644 index 0000000..1b755ba --- /dev/null +++ b/frontend/src/components/view/formula/formulaList.vue @@ -0,0 +1,303 @@ + + + diff --git a/frontend/src/components/view/formula/formulaSim.vue b/frontend/src/components/view/formula/formulaSim.vue new file mode 100644 index 0000000..1c5ec67 --- /dev/null +++ b/frontend/src/components/view/formula/formulaSim.vue @@ -0,0 +1,396 @@ + + + + diff --git a/frontend/src/components/view/formula/formulaView.vue b/frontend/src/components/view/formula/formulaView.vue new file mode 100644 index 0000000..31295d4 --- /dev/null +++ b/frontend/src/components/view/formula/formulaView.vue @@ -0,0 +1,270 @@ + + + + diff --git a/frontend/src/components/view/kpis/setKPIs.vue b/frontend/src/components/view/kpis/setKPIs.vue new file mode 100644 index 0000000..08c303c --- /dev/null +++ b/frontend/src/components/view/kpis/setKPIs.vue @@ -0,0 +1,292 @@ + + + diff --git a/frontend/src/components/view/module/moduleEdit.vue b/frontend/src/components/view/module/moduleEdit.vue new file mode 100644 index 0000000..dd3c1cc --- /dev/null +++ b/frontend/src/components/view/module/moduleEdit.vue @@ -0,0 +1,201 @@ + + + + diff --git a/frontend/src/components/view/module/moduleList.vue b/frontend/src/components/view/module/moduleList.vue new file mode 100644 index 0000000..cec627c --- /dev/null +++ b/frontend/src/components/view/module/moduleList.vue @@ -0,0 +1,280 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/view/module/moduleView.vue b/frontend/src/components/view/module/moduleView.vue new file mode 100644 index 0000000..3d7e88d --- /dev/null +++ b/frontend/src/components/view/module/moduleView.vue @@ -0,0 +1,129 @@ + + + + diff --git a/frontend/src/components/view/parameter/parameterEdit.vue b/frontend/src/components/view/parameter/parameterEdit.vue new file mode 100644 index 0000000..7883036 --- /dev/null +++ b/frontend/src/components/view/parameter/parameterEdit.vue @@ -0,0 +1,197 @@ + + + + diff --git a/frontend/src/components/view/parameter/parameterList.vue b/frontend/src/components/view/parameter/parameterList.vue new file mode 100644 index 0000000..a9e7c77 --- /dev/null +++ b/frontend/src/components/view/parameter/parameterList.vue @@ -0,0 +1,254 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/view/parameter/parameterView.vue b/frontend/src/components/view/parameter/parameterView.vue new file mode 100644 index 0000000..6293704 --- /dev/null +++ b/frontend/src/components/view/parameter/parameterView.vue @@ -0,0 +1,130 @@ + + + + diff --git a/frontend/src/components/view/sample/Editor.vue b/frontend/src/components/view/sample/Editor.vue new file mode 100644 index 0000000..eee9c15 --- /dev/null +++ b/frontend/src/components/view/sample/Editor.vue @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/frontend/src/components/view/sample/Excel.vue b/frontend/src/components/view/sample/Excel.vue new file mode 100644 index 0000000..7c3882d --- /dev/null +++ b/frontend/src/components/view/sample/Excel.vue @@ -0,0 +1,47 @@ + + + diff --git a/frontend/src/components/view/sample/FileUploader.vue b/frontend/src/components/view/sample/FileUploader.vue new file mode 100644 index 0000000..0c84750 --- /dev/null +++ b/frontend/src/components/view/sample/FileUploader.vue @@ -0,0 +1,140 @@ + + + diff --git a/frontend/src/components/view/sample/RegexInput.vue b/frontend/src/components/view/sample/RegexInput.vue new file mode 100644 index 0000000..72ec1c8 --- /dev/null +++ b/frontend/src/components/view/sample/RegexInput.vue @@ -0,0 +1,41 @@ + + + diff --git a/frontend/src/components/view/sample/Tree.vue b/frontend/src/components/view/sample/Tree.vue new file mode 100644 index 0000000..5c60d16 --- /dev/null +++ b/frontend/src/components/view/sample/Tree.vue @@ -0,0 +1,624 @@ + + + diff --git a/frontend/src/constants/index.js b/frontend/src/constants/index.js new file mode 100644 index 0000000..b28d2a5 --- /dev/null +++ b/frontend/src/constants/index.js @@ -0,0 +1,8 @@ +// constant addition +const env = import.meta.env; +export default Object.freeze({ + ...env, + DEFAULT_LANG: env.VITE_DEFAULT_LANG, + API_URL: env.VITE_API_URL, + WEB_CONTEXT_PATH: env.VITE_WEB_CONTEXT_PATH, +}); diff --git a/frontend/src/directives/authorization.js b/frontend/src/directives/authorization.js new file mode 100644 index 0000000..d839bc6 --- /dev/null +++ b/frontend/src/directives/authorization.js @@ -0,0 +1,37 @@ +/** + * Program: Show/Hide according to authority + * Author: SDL + * Description: Show/Hide according to authority + */ + +/* eslint-disable no-unused-vars */ +import SDLUtil from '@/utils/SDLUtil'; +import router from '@/router'; + +export default { + beforeMount(el, binding) { + // permission list of page + const { menuUserAuthList } = router.currentRoute.value.meta; + + const userInfo = SDLUtil.getLoginedUserInfo(); + if (!userInfo.systemAdminUser) { + // If it is not an administrator + if (typeof binding.value === 'string') { + if (!menuUserAuthList.includes(binding.value.toUpperCase())) { + el.style.display = 'none'; + } + } else { + const perms = binding.value; + let authflag = false; + Object.keys(perms).forEach(i => { + if (menuUserAuthList.includes(perms[i].toUpperCase())) { + authflag = true; + } + }); + if (!authflag) { + el.style.display = 'none'; + } + } + } + }, +}; diff --git a/frontend/src/directives/clickOutside.js b/frontend/src/directives/clickOutside.js new file mode 100644 index 0000000..903473f --- /dev/null +++ b/frontend/src/directives/clickOutside.js @@ -0,0 +1,23 @@ +/** + * Program: Click outside + * Author: SDL + * Description: Manage mouse click events outside containers + */ +export default { + beforeMount(el, binding) { + const { bubble } = binding.modifiers; + const handler = e => { + if (bubble || (!el.contains(e.target) && el !== e.target)) { + binding.value({ source: el, evt: e }); + } + }; + el.vueClickOutside = handler; + // add Event Listeners + document.addEventListener('click', handler); + }, + unmounted(el) { + // Remove Event Listeners + document.removeEventListener('click', el.vueClickOutside); + el.vueClickOutside = null; + }, +}; diff --git a/frontend/src/directives/custom.js b/frontend/src/directives/custom.js new file mode 100644 index 0000000..07a356e --- /dev/null +++ b/frontend/src/directives/custom.js @@ -0,0 +1,12 @@ +/* eslint-disable no-unused-vars */ +export default { + beforeMount(el, binding, vnode) { + // console.log('bind'); + }, + mounted(el, binding, vndoe) { + // console.log('inserted'); + }, + updated(el, binding, vnode, oldVnode) { + // console.log('componentUpdated'); + }, +}; diff --git a/frontend/src/directives/index.js b/frontend/src/directives/index.js new file mode 100644 index 0000000..2c1643d --- /dev/null +++ b/frontend/src/directives/index.js @@ -0,0 +1,20 @@ +/** + * Program: SDL Common Directive + * Author: SDL + * Description: SDL Common Directive List + */ +import custom from './custom'; +import clickOutSide from './clickOutside'; +import regex from './regex'; +import authorization from './authorization'; +import validation from './validation'; + +export default { + install(Vue) { + Vue.directive('custom', custom); + Vue.directive('click-outside', clickOutSide); + Vue.directive('regex', regex); + Vue.directive('authorization', authorization); + Vue.directive('validation', validation); + }, +}; diff --git a/frontend/src/directives/regex.js b/frontend/src/directives/regex.js new file mode 100644 index 0000000..3bbbf89 --- /dev/null +++ b/frontend/src/directives/regex.js @@ -0,0 +1,16 @@ +/** + * Program: regex directive + * Author: SDL + * Description: Constraint through Regular Equation + */ +/* eslint-disable no-unused-vars */ +export default { + beforeMount(el, binding) { + el.addEventListener('keyup', () => { + const regex = new RegExp(binding.value); + if (!regex.test(el.value)) { + el.value = el.value.slice(0, -1); + } + }); + }, +}; diff --git a/frontend/src/directives/validation.js b/frontend/src/directives/validation.js new file mode 100644 index 0000000..d90192b --- /dev/null +++ b/frontend/src/directives/validation.js @@ -0,0 +1,74 @@ +/** + * Program: validation directive + * Author: SDL + * Description: validation directive + */ +/* eslint-disable prefer-destructuring */ +/* eslint-disable no-unused-vars */ +import SDLUtil from '@/utils/SDLUtil'; + +const showErrMsg = (field, msg) => { + const errDiv = document.createElement('div'); + + errDiv.innerHTML = SDLUtil.getMsgProp(msg); + errDiv.className = 'invalid-feedback'; + field.classList.add('is-invalid'); + field.insertAdjacentElement('afterend', errDiv); +}; +const validationObj = (field, valids) => { + Array.from(field.parentElement.querySelectorAll('.invalid-feedback')).forEach(el => { + try { + el.remove(); + } catch (e) { + el.parentNode.removeChild(el); + } + }); + + if (field.willValidate && !field.checkValidity()) { + let errMsg = field.validationMessage; + if (field.getAttribute('errorMessage')) errMsg = field.getAttribute('errorMessage'); + + showErrMsg(field, errMsg); + } else if (valids.length > 0) { + for (let i = 0; i < valids.length; i += 1) { + if (!valids[i].validFuncs()) { + showErrMsg(field, valids[i].errorMessage); + break; + } + } + } +}; + +export default { + beforeMount(el, binding) { + let groupId = ''; + let valids = []; + if (binding.value && Object.prototype.hasOwnProperty.call(binding.value, 'groupId')) { + groupId = binding.value.groupId; + } + if (binding.value && Object.prototype.hasOwnProperty.call(binding.value, 'valids')) { + valids = binding.value.valids; + } + SDLUtil.setValidList({ el, groupId }); + + el.setAttribute('required', 'required'); + el.addEventListener('blur', e => { + e.preventDefault(); + + Array.from(document.querySelectorAll('.is-invalid')).forEach(ele => ele.classList.remove('is-invalid')); + validationObj(el, valids); + }); + el.addEventListener('focus', e => { + e.preventDefault(); + + el.classList.remove('is-invalid'); + }); + }, + unmounted(el, binding) { + let groupId = ''; + if (binding.value && Object.prototype.hasOwnProperty.call(binding.value, 'groupId')) { + groupId = binding.value.groupId; + } + SDLUtil.deleteValidList(el, groupId); + }, +}; diff --git a/frontend/src/event/event.js b/frontend/src/event/event.js new file mode 100644 index 0000000..57078a4 --- /dev/null +++ b/frontend/src/event/event.js @@ -0,0 +1,9 @@ +// event.js +import emitter from 'tiny-emitter/instance'; + +export default { + $on: (...args) => emitter.on(...args), + $once: (...args) => emitter.once(...args), + $off: (...args) => emitter.off(...args), + $emit: (...args) => emitter.emit(...args), +} diff --git a/frontend/src/filters/cutString.js b/frontend/src/filters/cutString.js new file mode 100644 index 0000000..32e2d1a --- /dev/null +++ b/frontend/src/filters/cutString.js @@ -0,0 +1,17 @@ +/** + * Program: cut String by length + * Author: SDL + * Description: cut String by length + */ +import _ from 'lodash'; + +export default (str, len) => { + if (!_.isEmpty(str)) { + let s = 0; + for (let i = 0; i < str.length; i += 1) { + s += str.charCodeAt(i) > 128 ? 2 : 1; + if (s > len) return `${str.substring(0, i)}...`; + } + } + return str; +}; diff --git a/frontend/src/filters/dateFormat.js b/frontend/src/filters/dateFormat.js new file mode 100644 index 0000000..f2bce4d --- /dev/null +++ b/frontend/src/filters/dateFormat.js @@ -0,0 +1,14 @@ +/** + * Program: date filter + * Author: SDL + * Description: date filter + */ +import moment from 'moment'; +import { SDLUtil } from '@/utils'; + +export default (dateStr, formatStr = SDLUtil.getMsgProp('data-format.date.ymd')) => { + if (dateStr) { + return moment(dateStr).format(formatStr); + } + return dateStr; +}; diff --git a/frontend/src/filters/index.js b/frontend/src/filters/index.js new file mode 100644 index 0000000..2313049 --- /dev/null +++ b/frontend/src/filters/index.js @@ -0,0 +1,24 @@ +/** + * Program: SDL Common filters + * Author: SDL + * Description: SDL Common Filters + */ +import dateFormat from './dateFormat'; +import numberFormat from './numberFormat'; +import numberFormat_de from './numberFormat_de'; +import reverse from './reverse'; +import nl2br from './nl2br'; +import cutString from './cutString'; + +export default { + install(Vue) { + Vue.config.globalProperties.$filters = { + reverse: reverse, + dateFormat: dateFormat, + numberFormat: numberFormat, + numberFormat_de: numberFormat_de, + nl2br: nl2br, + cutString: cutString, + }; + }, +}; diff --git a/frontend/src/filters/nl2br.js b/frontend/src/filters/nl2br.js new file mode 100644 index 0000000..f5a3222 --- /dev/null +++ b/frontend/src/filters/nl2br.js @@ -0,0 +1,8 @@ +/** + * Program: newline to html br tag + * Author: SDL + * Description: Changed to open character html br tag + */ +import _ from 'lodash'; + +export default str => _.replace(str, /(\r?\n)/g, '
'); diff --git a/frontend/src/filters/numberFormat.js b/frontend/src/filters/numberFormat.js new file mode 100644 index 0000000..f2ed50f --- /dev/null +++ b/frontend/src/filters/numberFormat.js @@ -0,0 +1,6 @@ +/** + * Program: numberFormat + * Author: SDL + * Description: comma per thousand + */ +export default str => String(str).replace(/(\d)(?=(?:\d{3})+(?!\d))/g, '$1,'); diff --git a/frontend/src/filters/numberFormat_de.js b/frontend/src/filters/numberFormat_de.js new file mode 100644 index 0000000..ad19f2a --- /dev/null +++ b/frontend/src/filters/numberFormat_de.js @@ -0,0 +1,6 @@ +/** + * Program: numberFormat + * Author: SDL + * Description: comma per thousand + */ +export default str => new Intl.NumberFormat('de-DE').format(str); diff --git a/frontend/src/filters/reverse.js b/frontend/src/filters/reverse.js new file mode 100644 index 0000000..4d3ff38 --- /dev/null +++ b/frontend/src/filters/reverse.js @@ -0,0 +1,9 @@ +/** + * Program: reverse + * Author: SDL + * Description: character string inverted display + */ +export default val => val + .split('') + .reverse() + .join(''); diff --git a/frontend/src/i18n/index.js b/frontend/src/i18n/index.js new file mode 100644 index 0000000..909912b --- /dev/null +++ b/frontend/src/i18n/index.js @@ -0,0 +1,56 @@ +/** + * Program: i18n + * Author: SDL + * Description: i18n language setting file + */ +// https://kazupon.github.io/vue-i18n/guide/lazy-loading.html +import { createI18n } from 'vue-i18n'; +import axios from 'axios'; +import isEmpty from 'lodash/isEmpty'; +import store from '@/vuex/store'; +import SDLUtil from '@/utils/SDLUtil'; + +const defaultLanguage = (window.navigator.language || SDLUtil.DEFAULT_LANG).replace('-', '_'); + +export const i18n = createI18n({ + locale: defaultLanguage, // set locale + fallbackLocale: { + 'ko': ['ko_KR'], + 'en': ['en_US'], + 'default': ['en_US'], + }, // fallback lang + messages: {}, // set locale messages + silentFallbackWarn: true, + silentTranslationWarn: true, + warnHtmlInMessage: 'off', // disable of the Detected HTML in message +}); + +let loadedLanguages = []; // our default language that is preloaded + +function setI18nLanguage(lang) { + i18n.global.locale = lang; + store.commit('LOCALE', lang); + axios.defaults.headers.common['Accept-Language'] = lang; + document.querySelector('html').setAttribute('lang', lang); + return lang; +} + +export async function loadLanguageAsync(lang) { + let language; + if (isEmpty(loadedLanguages)) { + await axios.get(`${SDLUtil.API_URL}/noauth/messages/all`).then(msgs => { + loadedLanguages = Object.keys(msgs.data); + loadedLanguages.forEach(country => { + i18n.global.setLocaleMessage(country, msgs.data[country]); + }); + }); + } + try { + language = loadedLanguages.find(find => find === lang) || loadedLanguages.find(find => find.includes(lang.split('_')[0])) || SDLUtil.DEFAULT_LANG; + } catch (e) { + language = SDLUtil.DEFAULT_LANG; + } + return setI18nLanguage(language); +} + +export default i18n; diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..c1411c6 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,129 @@ +import { createApp } from 'vue'; +import axios from 'axios'; +import moment from 'moment'; +import SdlControl from './components/common/control'; +import SdlFilters from './filters'; +import SdlDirectives from './directives'; +import App from './App.vue'; +import { addEntryPoint, router } from './router'; +import Store from './vuex/store'; +import { i18n } from './i18n'; +import '../static/bootstrap/css/bootstrap.min.css'; +import '../static/css/custom.css'; +import '../static/css/sdl.css'; +import '../static/css/realgrid-style.css'; +import '../static/bootstrap/js/bootstrap.bundle.min'; + +import api from './service'; +import SDLUtil from '@/utils/SDLUtil'; +import RealGrid from 'realgrid'; + +const app = createApp(App); + +app.use(SdlControl); // Additional components. common sdl +app.use(SdlFilters); // Additional filters. common sdl +app.use(SdlDirectives); // Additional directives. common sdl +app.use(Store); + +// Additional context +app.config.globalProperties.$moment = moment; +app.config.globalProperties.$CONTEXT = SDLUtil.WEB_CONTEXT_PATH; + +axios.defaults.headers.common['x-auth-token'] = localStorage.getItem('userToken'); +if (localStorage.getItem('adminToken') !== null && localStorage.getItem('adminToken') !== '') { + axios.defaults.headers.common['original-user-id'] = SDLUtil.parseJwt(localStorage.getItem('adminToken')).userId; +} + +axios.interceptors.request.use( + (config) => { + axios.defaults.headers.common['last-access-time'] = localStorage.getItem('lastAccessTime'); + // console.log('request:', localStorage.getItem('lastAccessTime')); + // Do something before request is sent + if (Object.prototype.hasOwnProperty.call(config, 'data')) { + if (Object.prototype.hasOwnProperty.call(config.data, 'labelJson')) { + config.data.labelJson = JSON.stringify(config.data.labelJson); + } + } + return config; + }, + (error) => Promise.reject(error), +); + +axios.interceptors.response.use( + (response) => { + if (response.headers.date !== null && response.headers.date !== '') { + localStorage.setItem('lastAccessTime', new Date(response.headers.date).getTime()); + // console.log('resonse:', localStorage.getItem('lastAccessTime')); + } + + if (Object.prototype.hasOwnProperty.call(response.data, 'error')) { + /* + * Add code number below if error message is passed to login after error message during certain error code + */ + const loginErrCodes = ['187', '190', '191', '192', '193', '194', '195', '199', '200']; + + // 500 is excluded for error checking when localhost. + if (window.location.hostname !== 'localhost') { + loginErrCodes.push('500'); + } + if (loginErrCodes.includes(String(response.data.error.code)) && router.currentRoute.value.fullPath.indexOf('/login') === -1) { + // Do not process logout when there is a general error + if (String(response.data.error.code) !== '500') api.logout(); + document.location = `${SDLUtil.WEB_CONTEXT_PATH}/error?code=${response.data.error.code}&redirect=${router.currentRoute.value.fullPath}`; + } + return Promise.reject(Object.assign(response, new Error(response.data.error.message))); + } + return response; + }, + (error) => Promise.reject(error), +); + +// axios.get('/static/dummy/allMenuPagePaths.json') +axios + .get(`${SDLUtil.API_URL}/auth/noauth/all-menu-page-paths`) + .then(({ data }) => { + addEntryPoint(data).then(() => { + app.use(i18n); + app.use(router); + app.mount('#app'); + }); + }) + .catch((err) => { + // TODO network error processing + console.error(err); + window.location = `${SDLUtil.WEB_CONTEXT_PATH}/static/error.html`; + }); + + RealGrid.setLicenseKey('upVcPE+wPOmtLjqyBIh9RkM/nBOseBrflwxYpzGZyYm9cY8amGDkiMnVeQKUHJDjW2y71jtk+wteqHQ1mRMIXzEcGIrzZpzzNTakk0yR9UcO/hzNodVsIiqQNVtxmmYt'); + + //https://docs.realgrid.com/refs/Interface/GridOptions + RealGrid.setDefault({ + edit: { + editable: false, + commitByCell: true, + commitWhenLeave: true, + columnEditableFirst: true + }, + copy: {enabled: true}, + paste: {enabled: false}, + + + checkBar: { visible: false, showAll: false }, + stateBar: { + visible: false + }, + header: { + height: 30 + }, + footer: { + visible: false + }, + display: { + rowHeight: 30, + rowResizable: true, + eachRowResizable: true, + columnMovable: false, + showEmptyMessage:true, + emptyMessage: "NO DATA", + } + }); \ No newline at end of file diff --git a/frontend/src/mixin/GridCommon.js b/frontend/src/mixin/GridCommon.js new file mode 100644 index 0000000..1c31d33 --- /dev/null +++ b/frontend/src/mixin/GridCommon.js @@ -0,0 +1,178 @@ +import { SDLUtil } from '@/utils'; + +export default { + methods: { + //main.js RealGrid.setDefault 이외 페이지 별도 설정 + grid_setGridOptions(gv, pParam) { + var param = pParam || {}; + param.options = param.options || {}; + param.displayOptions = param.displayOptions || {}; + + gv.setOptions(param.options); + /* + param.options = { + checkBar: { visible: true, showAll: true }, + stateBar: { visible: false }, + panel: { visible: false }, + footer: { visible: false }, + edit: { updatable: false, insertable: false, appendable: false, deletable: false }, + header: { minHeight: 20, resizable: true }, + };*/ + + /*param.providerOptions = { + softDeleting: true, // true:rowstate만 변경, false:바로 삭제 + deleteCreated: true, // true:[softDeleting=true]인 경우라도 create는 바로 삭제 + };*/ + + gv.setDisplayOptions(param.displayOptions); + + // checkable + //gv.setCheckableExpression("state = 'c'", true); + + //gv.setCopyOptions({ enabled: false }); + + return param; + }, + grid_getCheckedRowArray(_grid) { + var list = []; + + var checkedList = _grid.getCheckedRows(); + for (var i = 0; i < checkedList.length; i++) { + if (_grid.getDataSource().getRowState(checkedList[i]) == 'created') { + _grid.getDataSource().removeRow(checkedList[i]); + } else { + list.push(_grid.getDataSource().getJsonRow(checkedList[i])); + } + } + + return list; + }, + grid_getGridExcel(_grid,param) { + + let pageMeta = this.$route.meta; + let iUserObj = SDLUtil.getLoginedUserInfo(); + iUserObj.commonMessage = + 'SAMSUNG SECRET\n' + + '삼성반도체 임직원을 위한 시스템으로서 인가된분만 사용할수 있습니다.\n(As a system for employees of Samsung, only authorized persons can use it.)\n' + + '불법으로 사용시에는 법적제재를 받을수 있습니다.\n(If used illegally, you may be subject to legal sanctions.)'; + + let date = new Date().toLocaleDateString('de-DE'); + + var systemIdent = 'Confidential\n\nUSER NAME : ' + iUserObj.userName + '\nUSER DEPT : ' + iUserObj.deptNameEn + '\nMENU NAME : ' + pageMeta.pageName + '\nDATE TIME : ' + date + '\n' + iUserObj.commonMessage; + // var systemIdent ="aa" + _grid.exportGrid({ + type: 'excel', + target: 'local', + fileName: (param?.fileName||pageMeta.pageName.replace(/\s+/g, ''))+this.$moment().format('_YYYYMMDDHHMMSS')+'.xlsx', + showProgress: true, + progressMessage: 'Excel Download....', + indicator: 'default', + //header: header, + //footer: footer, + //compatibility: excelType, + documentTitle: { + //부제 + message: systemIdent, + visible: true, + styles: { + textAlignment: 'near', + foreground: '#800000', + fontBold: true, + }, + height: 130, + }, + done: function () { + //내보내기 완료 후 실행되는 함수 + alert('done excel export'); + }, + }); + }, + }, +}; + +/** + * SDLUtil.getLoginedUserInfo() + * + * + * userInfo:{ + "deptName": "클라우드전환실행그룹(통합Managed Appl.서비스)", + "timeZoneId": "Asia/Seoul", + "mobile": "+82-10-2526-7743", + "language": "ko_KR", + "empNo": "BP351", + "userName": "진문영", + "grdNameEn": "", + "userId": "M221216045308C318832", + "compName": "삼성SDS", + "grdCode": "B5C", + "compCode": "C60", + "userNameEn": "MoonYoung Jin", + "compNameEn": "SAMSUNG SDS", + "deptNameEn": "Cloud Appl. Transformation Implementation Group", + "grdName": "현장관리자", + "systemAdminUser": false, + "timeZoneCode": "GMT+9:00", + "clientIp": "0:0:0:0:0:0:0:1", + "deptCode": "C60AJ826", + "email": "my03.jin@partner.samsung.com", + "knoxId": "my03.jin", + "iat": 1733105054, + "exp": 1733119454, + "sub": "M221216045308C318832" +} + +$route.meta + + $route.meta::{ + "deleted": false, + "parentId": "AZNSNJLMASF_waHz", + "menuId": "AZNSNNn8ASl_waHz", + "label": "Formula Mgmt", + "labelJson": { + "en_US": "Formula Mgmt", + "ko_KR": "Formula Mgmt" + }, + "menuLevel": 2, + "menuSequence": 1, + "externalUrlUsed": false, + "externalUrl": "", + "used": true, + "checked": false, + "pages": [ + { + "authorizationType": "READ", + "deleted": false, + "menuId": "AZNSNNn8ASl_waHz", + "pageId": "AZNsDXZsAGx_waGY", + "pageName": "Formula List", + "pagePath": "/embo/formula/list", + "componentName": "formula/formulaList", + "defaulted": true, + "used": true + } + ], + "menuUserAuthList": [ + "READ" + ], + "menuUrl": "/embo/formula/list", + "componentName": "formula/formulaList", + "defaultPage": { + "authorizationType": "READ", + "deleted": false, + "menuId": "AZNSNNn8ASl_waHz", + "pageId": "AZNsDXZsAGx_waGY", + "pageName": "Formula List", + "pagePath": "/embo/formula/list", + "componentName": "formula/formulaList", + "defaulted": true, + "used": true + }, + "authorizationType": "READ", + "pageId": "AZNsDXZsAGx_waGY", + "pageName": "Formula List", + "pagePath": "/embo/formula/list", + "defaulted": true +} + * + * + */ diff --git a/frontend/src/mixin/SDLLocale.js b/frontend/src/mixin/SDLLocale.js new file mode 100644 index 0000000..81a37cc --- /dev/null +++ b/frontend/src/mixin/SDLLocale.js @@ -0,0 +1,26 @@ +/** + * Program: Mixin for Multilingual Processing + * Author: SDL + * Description: Mixin for Multilingual Processing + */ +import { mapGetters } from 'vuex'; + +export default { + computed: { + ...mapGetters({ + locale: 'getLocale', + }), + }, + methods: { + getLabel({ labelJson, label }) { + try { + if (labelJson && labelJson[this.locale] !== undefined && labelJson[this.locale].trim() !== '') { + return labelJson[this.locale]; + } + return label; + } catch (e) { + return label; + } + }, + }, +}; diff --git a/frontend/src/mixin/StoreParams.js b/frontend/src/mixin/StoreParams.js new file mode 100644 index 0000000..5f1d405 --- /dev/null +++ b/frontend/src/mixin/StoreParams.js @@ -0,0 +1,42 @@ +/** + * Program: Mixed in for parameters and menus + * Author: SDL + * Description: Mixed in for parameters and menus + */ +import { mapGetters, mapActions } from 'vuex'; +import _ from 'lodash'; + +export default { + computed: { + ...mapGetters({ + vueStoreParameter: 'getParams', + menus: 'getMenus', + }), + pageId() { + return this.$route.meta.pageId; + }, + }, + methods: { + ...mapActions({ + saveVueStoreParameter: 'setParams', + }), + getStoreParameter() { + return this.vueStoreParameter[this.pageId]; + }, + setStoreParameter(params) { + // console.info('setStoreParameter', this.pageId, params); + const saveParams = { + ...this.getStoreParameter(), + }; + saveParams[this.pageId] = params; + this.saveVueStoreParameter(saveParams); + }, + goBackToList() { + const parentMenu = this.menus.find(menu => menu.menuId === this.$route.meta.menuId); + const defaultPage = _.find(parentMenu.pages, ['defaulted', true]); + if (defaultPage) { + this.$router.push({ name: defaultPage.pageId, state: { restoreList: true } }); + } + }, + }, +}; diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..4dfcbac --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,288 @@ +/** + * Program: Router + * Author: SDL + * Description: router setting file + */ +import { createRouter, createWebHistory } from 'vue-router'; +import _ from 'lodash'; +import axios from 'axios'; +import store from '@/vuex/store'; +import { fetchMenuAll } from '@/service/menuService'; +import SDLUtil from '@/utils/SDLUtil'; + +import { i18n, loadLanguageAsync } from '@/i18n'; +// 일반 +import LoginPage from '@/components/view/admin/main/LoginPage.vue'; +import MainPage from '@/components/view/admin/main/MainPage.vue'; +import MainIndexPage from '@/components/view/admin/main/MainIndexPage.vue'; +import EmptyPage from '@/components/view/admin/main/EmptyPage.vue'; + +export const router = createRouter({ + history: createWebHistory(), + routes: [ + // login + { + name: 'login', + path: `${SDLUtil.WEB_CONTEXT_PATH}/login`, + component: LoginPage, + }, + // logout + { + name: 'logout', + path: `${SDLUtil.WEB_CONTEXT_PATH}/logout`, + component: () => import(/* webpackChunkName: '[request]' */ '@/components/view/admin/main/LoginOut.vue'), + }, + // system registration + { + name: 'systemUserRegister', + path: `${SDLUtil.WEB_CONTEXT_PATH}/register`, + component: () => import(/* webpackChunkName: '[request]' */ '@/components/view/admin/main/RegisterPage.vue'), + }, + // system subscription clause + { + name: 'terms', + path: `${SDLUtil.WEB_CONTEXT_PATH}/terms-conditions`, + component: () => import(/* webpackChunkName: '[request]' */ '@/components/view/admin/main/TermsAndConditionsPage.vue'), + }, + // Password initialization + { + name: 'initPassword', + path: `${SDLUtil.WEB_CONTEXT_PATH}/initPassword`, + component: () => import(/* webpackChunkName: '[request]' */ '@/components/view/admin/main/InitPassword.vue'), + }, + // sample test + { + path: `${SDLUtil.WEB_CONTEXT_PATH}/sample/tree`, + component: () => import('@/components/view/sample/Tree.vue'), + }, + { + path: `${SDLUtil.WEB_CONTEXT_PATH}/sample/fileuploader`, + component: () => import('@/components/view/sample/FileUploader.vue'), + }, + { + path: `${SDLUtil.WEB_CONTEXT_PATH}/sample/excel`, + component: () => import('@/components/view/sample/Excel.vue'), + }, + { + path: `${SDLUtil.WEB_CONTEXT_PATH}/sample/regexinput`, + component: () => import('@/components/view/sample/RegexInput.vue'), + }, + // popup + { + name: 'noticePopup', + path: `${SDLUtil.WEB_CONTEXT_PATH}/noticePopup/:boardId/:postId`, + component: () => import(/* webpackChunkName: '[request]' */ '@/components/view/admin/board/PostPopup.vue'), + }, + // popup + { + name: 'transPopup', + path: `${SDLUtil.WEB_CONTEXT_PATH}/transPopup`, + component: () => import('@/components/view/admin/main/TransPopup.vue'), + }, + // No page registration in menu management => Unconditional 404 processing of user criteria regardless of component registration + { + name: 'emptyPage', + path: `${SDLUtil.WEB_CONTEXT_PATH}/emptypage`, + component: EmptyPage, + }, + { + name: '404', + path: `${SDLUtil.WEB_CONTEXT_PATH}/404`, + component: EmptyPage, + }, + { + name: 'error', + path: `${SDLUtil.WEB_CONTEXT_PATH}/error`, + component: EmptyPage, + }, + // Page if not authenticated + { + path: `${SDLUtil.WEB_CONTEXT_PATH}/noauth`, + component: () => import(/* webpackChunkName: 'no_auth' */ '@/components/view/admin/main/NoAuthPage.vue'), + }, + { + name: 'main', + path: `${SDLUtil.WEB_CONTEXT_PATH}/`, + alias: '/', + component: MainPage, + children: [ + { + path: '', + name: 'mainIndexPage', + component: MainIndexPage, + }, + ], // mock + }, + // otherwise redirect to home + { + path: '/:pathMatch(.*)*', + name: 'not-found', + redirect: `${SDLUtil.WEB_CONTEXT_PATH}/404`, + }, + ], +}); + +router.beforeEach((to, from, next) => { + // redirect to login page if not logged in and trying to access a restricted page + const publicPages = ['/login', '/logout', '/register', '/initPassword', '/error'].map(page => SDLUtil.WEB_CONTEXT_PATH + page); + const authRequired = !publicPages.includes(to.path); + const loggedIn = SDLUtil.isLogin(); + + if (authRequired && !loggedIn) { + if (to.fullPath.indexOf(`${SDLUtil.WEB_CONTEXT_PATH}/404`) > -1 || to.fullPath === `${SDLUtil.WEB_CONTEXT_PATH}/`) { + return next(`${SDLUtil.WEB_CONTEXT_PATH}/login`); + } + if (to.fullPath.indexOf(`${SDLUtil.WEB_CONTEXT_PATH}/noauth`) > -1) { + return next({ path: `${SDLUtil.WEB_CONTEXT_PATH}/login`, query: { redirect: to.redirectedFrom.fullPath } }); + } + return next({ path: `${SDLUtil.WEB_CONTEXT_PATH}/login`, query: { redirect: to.fullPath } }); + } + return next(); +}); + +router.beforeEach((to, from, next) => { + // Import locale information from logged in user information + let userPreferLang; + const userInfo = SDLUtil.getLoginedUserInfo(); + if (userInfo && userInfo.language) { + userPreferLang = userInfo.language; + } + + const lang = (userPreferLang || window.navigator.language || SDLUtil.DEFAULT_LANG).replace('-', '_'); + loadLanguageAsync(lang).then(() => next()); +}); + +// Add to menu ID, page ID header when changing router +router.beforeEach((to, from, next) => { + if (to.meta.menuId) { + axios.defaults.headers.common['menu-id'] = to.meta.menuId; + } else { + delete axios.defaults.headers.common['menu-id']; + } + if (to.meta.pageId) { + axios.defaults.headers.common['page-id'] = to.meta.pageId; + } else { + delete axios.defaults.headers.common['page-id']; + } + + return next(); +}); + +// Language Settings When Changing Router +router.beforeEach((to, from, next) => { + if (store.getters.getLocale !== '') { + i18n.locale = store.getters.getLocale; + } + return next(); +}); + +// Initialize storing the stored parameter setting value when changing the router +router.beforeEach((to, from, next) => { + if (history.state.restoreList !== true && to.meta.menuId !== from.meta.menuId) { + store.commit('PARAMS', {}); + } + // console.info('파라미터값', store.getters.getParams, to.params.restoreList); + return next(); +}); + +router.afterEach(to => { + let title = SDLUtil.getMsgProp('sdl.common.projectName'); + if (to.meta.labelJson && to.meta.labelJson[i18n.locale]) { + title += ` - ${to.meta.labelJson[i18n.locale]}`; + } else if (to.meta.label) { + title += ` - ${to.meta.label}`; + } + document.title = title; + if (to.meta.menuId !== undefined) { + axios.post(`${SDLUtil.API_URL}/history/menuUseHistory/${to.meta.menuId}`).then(); + } +}); + +export async function addEntryPoint(routeData = []) { + const entryChildren = routeData + .filter(menu => menu.pagePath) + .map(menu => ({ + name: menu.pageId, + path: menu.pagePath, + redirect: `${SDLUtil.WEB_CONTEXT_PATH}/noauth`, + meta: { + ...menu, + }, + })); + + // const existComponents = componentList.map(r => r.path); + + // If you are logged in + if (SDLUtil.isLogin()) { + // Import common code + // await SDLUtil.loadAllCommCodeList(); + // await store.dispatch('getMenus'); + const termsUrl = `${SDLUtil.WEB_CONTEXT_PATH}/terms-conditions`; + const isTermsPage = window.location.pathname === termsUrl; + const menuResponse = await fetchMenuAll() + .then(res => res) + .catch(err => { + // We need agreement. + if (err.data.error.code === 201 && !isTermsPage) { + console.info('change', err); + window.location.href = termsUrl; + } + }); + + // skip the terms and conditions page + if (isTermsPage) { + return; + } + + await Promise.all([SDLUtil.loadAllCommCodeList(), store.dispatch('getMenus'), store.dispatch('getMyMenus')]); + + let authPages = []; + const globComponents = import.meta.glob('../components/view/**/**.vue'); + menuResponse.data.forEach(menu => { + const pages = menu.pages.map(page => { + + const routeItem = { + name: page.pageId, + path: page.pagePath, + component: EmptyPage, + meta: { + ...menu, + ...page, + tabMenuObj:menu, + componentName: page.componentName, + }, + }; + if (page.componentName !== undefined && page.componentName !== '') { + routeItem.component = globComponents[`../components/view/${page.componentName}.vue`]; + } + if(page.pagePath.startsWith('/embo/formula/')){ + console.log(routeItem); + } + + return routeItem; + }); + + authPages = authPages.concat(pages); + }); + + authPages.forEach(page => { + const idx = _.findIndex(entryChildren, ['path', page.path]); + if (idx > -1) { + entryChildren[idx] = page; + } + }); + } + + [...entryChildren].forEach(child => { + router.addRoute('main', child); + // console.log(child.path); + }); +} + +router.onError(error => { + if (/loading chunk \d* failed./i.test(error.message)) { + window.location.reload(); + } +}); + +export default router; diff --git a/frontend/src/service/cacheService.js b/frontend/src/service/cacheService.js new file mode 100644 index 0000000..d7822fb --- /dev/null +++ b/frontend/src/service/cacheService.js @@ -0,0 +1,13 @@ +/** + * Program: Clear Cache service + * Author: SDL + * Description: Clear Cache service + */ +import axios from 'axios'; +import SDLUtil from '@/utils/SDLUtil'; + +export default { + async clearCache() { + await axios.get(`${SDLUtil.API_URL}/caches/clear-all`); + }, +}; diff --git a/frontend/src/service/index.js b/frontend/src/service/index.js new file mode 100644 index 0000000..3a347a4 --- /dev/null +++ b/frontend/src/service/index.js @@ -0,0 +1,30 @@ +import loginService from './loginService'; +import menuService from './menuService'; +import cacheService from './cacheService'; + +export default { + async login(userId, password) { + try { + return await loginService.login(userId, password); + } catch (err) { + console.error(err); + } + }, + async loginNewEpTray(userInfo, key) { + try { + return await loginService.loginNewEpTray(userInfo, key); + } catch (err) { + console.error(err); + } + }, + logout() { + loginService.logout(); + }, + async getMenus(forceRemote = false) { + return menuService.getMenus(forceRemote); + }, + clearCache() { + cacheService.clearCache(); + }, + loginService, menuService, cacheService +}; diff --git a/frontend/src/service/loginService.js b/frontend/src/service/loginService.js new file mode 100644 index 0000000..58a3b03 --- /dev/null +++ b/frontend/src/service/loginService.js @@ -0,0 +1,66 @@ +import axios from 'axios'; +import SDLUtil from '@/utils/SDLUtil'; + +const getUserInfo = (formData) => axios.post(`${SDLUtil.API_URL}/noauth/login/id`, formData); +const getNewEpTrayUserInfo = formData => axios.post(`${SDLUtil.API_URL}/noauth/login/new-eptray`, formData); + +const setHeaderToken = token => { + axios.defaults.headers.common['x-auth-token'] = token; +}; + +export default { + async login(userId, password) { + try { + const formData = new FormData(); + formData.append('userId', userId); + formData.append('password', password); + const userInfoResponse = await getUserInfo(formData); + this.setToken(userInfoResponse.data.jwtToken); + + return Object.assign(userInfoResponse.data, { code: 'authOk' }); + } catch (err) { + return err.data.error; + } + }, + async loginNewEpTray(userInfo, key) { + try { + const formData = new FormData(); + formData.append('encodeUserInfo', userInfo); + formData.append('encodeAesKey', key); + const userInfoResponse = await getNewEpTrayUserInfo(formData); + this.setToken(userInfoResponse.data.jwtToken); + + return Object.assign(userInfoResponse.data, { code: 'authOk' }); + } catch (err) { + // console.error('login:', err); + return err.data.error; + } + }, + logout() { + this.setToken(''); + }, + setToken(token) { + if (token === '' || token === null) { + // impersonator check + if (localStorage.getItem('adminToken') !== null && localStorage.getItem('adminToken') !== '') { + // call axios + axios.put(`${SDLUtil.API_URL}/impersonation/logout`).then(() => {}); + this.setToken(localStorage.getItem('adminToken')); + localStorage.setItem('adminToken', ''); + axios.defaults.headers.common['original-user-id'] = ''; + document.location = `${SDLUtil.WEB_CONTEXT_PATH}/`; + } else { + setHeaderToken(''); + localStorage.setItem('user', ''); + localStorage.setItem('userToken', ''); + } + } else { + setHeaderToken(token); + localStorage.setItem('userToken', `${token}`); + localStorage.setItem('user', JSON.stringify(SDLUtil.parseJwt(token))); + + const defaultLang = (window.navigator.userLanguage || window.navigator.language || SDLUtil.DEFAULT_LANG).replace('-', '_'); + SDLUtil.setI18nLanguage(SDLUtil.getLoginedUserInfo().language || defaultLang); // Change to language setting of login user + } + }, +}; diff --git a/frontend/src/service/menuService.js b/frontend/src/service/menuService.js new file mode 100644 index 0000000..691101f --- /dev/null +++ b/frontend/src/service/menuService.js @@ -0,0 +1,55 @@ +import axios from 'axios'; +import _ from 'lodash'; +import SDLUtil from '@/utils/SDLUtil'; + +let storeFetch = null; +export const fetchMenuAll = async (forceRemote = true) => { + if (storeFetch == null || forceRemote) { + storeFetch = await axios.get(`${SDLUtil.API_URL}/auth/menus-user-auth`); + } + return storeFetch; +}; + +let storeMenus = []; + +export default { + async getMenus(forceRemote = false) { + if (storeMenus.length === 0 || forceRemote) { + try { + const menuResponse = await fetchMenuAll(forceRemote); + storeMenus = menuResponse.data.map(menu => { + const defaultPage = _.find(menu.pages, ['defaulted', true]); + if (defaultPage) { + menu.menuUrl = defaultPage.pagePath; + menu.componentName = defaultPage.componentName; + menu.defaultPage = defaultPage; + } else { + menu.menuUrl = '/emptypage'; + menu.componentName = 'admin/main/EmptyPage'; + } + // menu.isGroup = menu.pages.length > 0; // TODO 메뉴쪽 후에 외부링크 추가되면 로직추가 필요 + return menu; + }); + return storeMenus; + } catch (error) { + console.error(error); + return []; + } + } else { + return Promise.resolve(storeMenus); + } + }, + async getMyMenus() { + let myMenus = []; + await axios.get(`${SDLUtil.API_URL}/auth/quick-menus`).then(({ data }) => { + myMenus = data; + }); + return myMenus; + }, + async addMyMenu(menuIds) { + return axios.post(`${SDLUtil.API_URL}/auth/quick-menus`, menuIds); + }, + async deleteMyMenu(menuId) { + return axios.delete(`${SDLUtil.API_URL}/auth/quick-menus/${menuId}`); + }, +}; diff --git a/frontend/src/utils/DateUtil.js b/frontend/src/utils/DateUtil.js new file mode 100644 index 0000000..be4226e --- /dev/null +++ b/frontend/src/utils/DateUtil.js @@ -0,0 +1,57 @@ +// date 관련 유틸리티 +import moment from 'moment'; +import _ from 'lodash'; + +/** + * 현재 날짜 (locale 별 format) + * @returns {*} + */ +const now = () => moment().format('YYYY-MM-DD'); + +/** + * 날짜 기준 이후 데이터 + * @param num 더해지는 수치 + * @param unit 더해지는 항목 ex) 년: Y, 월: M, 일: D + * @param date 기준 날짜 (기본값 - 현재) + * @returns {string} + */ +const addDate = (num, unit, date = moment()) => moment(date) + .add(num, unit) + .format('YYYY-MM-DD'); + +/** + * 날짜 기준 이전 데이터 + * @param num 빼지는 수치 + * @param unit 빼지는 항목 ex) 년: Y, 월: M, 일: D + * @param date 기준 날짜 (기본값 - 현재) + * @returns {string} + */ +const subDate = (num, unit, date = moment()) => moment(date) + .subtract(num, unit) + .format('YYYY-MM-DD'); + +/** + * 표준 포맷으로 변환 + * @param param 날짜 데이터 (String / Object) + * @param format 날짜 포맷 (ex. YYYY-MM-DD) + * @returns {string|{endDate: string, startDate: string}} + */ +const stdFormat = (param, format = 'YYYY-MM-DD') => { + let result = null; + if (_.has(param, 'startDate')) { + result = { + startDate: moment(param.startDate).format(format), + endDate: moment(param.endDate).format(format), + }; + } else { + result = moment(param).format(format); + } + return result; +}; + +export default { + now, + addDate, + subDate, + stdFormat, +}; diff --git a/frontend/src/utils/GridUtil.js b/frontend/src/utils/GridUtil.js new file mode 100644 index 0000000..4688e05 --- /dev/null +++ b/frontend/src/utils/GridUtil.js @@ -0,0 +1,44 @@ +import moment from 'moment'; + +// +export function generateWeekList(startYear, endYear, startWeek, endWeek) { + const weeks = []; + const start = moment(startYear+ String(startWeek).padStart(2, '0'), "GGGGWW", true).startOf('isoWeek'); + const end = moment(endYear+ String(endWeek).padStart(2, '0'), "GGGGWW", true).startOf('isoWeek'); + + + let current = start.clone(); + let lastMonth = ''; + let monthIdx = 0; + while (current.isSameOrBefore(end)) { + let cur3 = current.clone().add(3, 'days'); //목요일기준 + + if (lastMonth == cur3.format('MM')) { + monthIdx++; + } else { + monthIdx = 0; + } + lastMonth = cur3.format('MM'); + + weeks.push({ + start: current.format('YYYYMMDD'), + end: current.clone().endOf('isoWeek').format('YYYYMMDD'), + week: current.isoWeek(), + year: current.isoWeekYear(), + formatted: current.format('GGGG[W]WW'),//`${current.isoWeekYear()}W${current.isoWeek().toString().padStart(2, '0')}`, + month: `${current.isoWeekYear()}M${cur3.format('MM')}`, + monthIdx: monthIdx, + lastMonth: lastMonth, + quarter: `${current.isoWeekYear()}Q${Math.ceil((cur3.month() + 1) / 3)}`, // Adding 1 because moment months are 0-based + half: `${current.isoWeekYear()}H${Math.ceil((cur3.month() + 1) / 6)}`, + }); + + current.add(1, 'week'); + } + + return weeks; +} + +export default { + generateWeekList, +}; diff --git a/frontend/src/utils/SDLUtil.js b/frontend/src/utils/SDLUtil.js new file mode 100644 index 0000000..73eb538 --- /dev/null +++ b/frontend/src/utils/SDLUtil.js @@ -0,0 +1,601 @@ +/** + * Program: SDLUtil.js + * Author: SDL + * Description: SDL Common Util + */ +import axios from 'axios'; +import _ from 'lodash'; +import SdlModal from '@/components/common/control/modal'; +import UserPopup from '@/components/common/popup/UserListPopup.vue'; +import Vuei18n from '@/i18n'; +import constants from '@/constants'; + +const API_URL = `${constants.API_URL}`; +const WEB_CONTEXT_PATH = `${constants.WEB_CONTEXT_PATH.replace(/\/$/, '')}`; +const DEFAULT_LANG = `${constants.DEFAULT_LANG}`; +let codeList = []; + +/** + * param: + * return: boolean + * exception: + * Description : Check whether you are logged in or not + */ +const isLogin = () => localStorage.getItem('user') !== null && localStorage.getItem('user') !== '' && localStorage.getItem('userToken') !== null && localStorage.getItem('userToken') !== ''; + +/** + * param: + * return: json userinfo + * exception: + * Description : Logged in User Information + */ +const getLoginedUserInfo = () => { + if (isLogin()) { + return JSON.parse(localStorage.getItem('user')); + } + return undefined; +}; + +/** + * param: key : message key(String) + * param: replaces : param(Array) ex) properties text : {0} {1} three => param ["one", "two"] + * return: string + * exception: + * Description : get message properties + */ +const getMsgProp = (key, replaces) => Vuei18n.global.t(key, replaces); + +/** + * param: + * return: string + * exception: + * Description : the language currently set + */ +const getI18nLanguage = () => Vuei18n.locale; + +/** + * param: lang(String) + * return: + * exception: + * Description : Language Settings + */ +const setI18nLanguage = lang => { + Vuei18n.locale = lang; +}; + +/** + * + * @param param + * Description : alert, confirm button handler + */ +const okHandler = param => { + SdlModal.app.config.globalProperties.$modal.hide('dialog'); + if (param.onOkEvt !== null && typeof param.onOkEvt === 'function') { + param.onOkEvt(); + } +}; +const cancelHandler = param => { + SdlModal.app.config.globalProperties.$modal.hide('dialog'); + if (param.onCancelEvt !== null && typeof param.onCancelEvt === 'function') { + param.onCancelEvt(); + } +}; + +/** + * param: msg -> message(String) + * param: title -> title(String) + * param: okLabel -> ok button text(String) + * param: onOkEvt -> ok click event(Function) + * return: + * exception: + * Description : alert + */ +const alert = (params, replaces) => { + let param = { + msg: '', + title: '', + okLabel: '', + onOkEvt: () => {}, + }; + if (typeof params === 'string') { + param = Object.assign(param, { msg: params }); + } else { + param = Object.assign(param, params); + } + SdlModal.app.config.globalProperties.$modal.show('dialog', { + title: getMsgProp(param.title) || getMsgProp('sdl.common.label.confirm'), + text: getMsgProp(param.msg, replaces), + buttons: [ + { + title: getMsgProp(param.okLabel) || getMsgProp('sdl.common.label.confirm'), + default: false, + handler: () => { + okHandler(param); + }, + }, + ], + }); +}; + +/** + * param: error message(json) + * return: + * exception: + * Description : error alert + */ +const errorAlert = err => { + let msg = 'sdl.commonError'; // default error 메세지 + if (err && err.data && err.data.error) { + if (err.data.error.reason && err.data.error.reason !== '') { + msg = err.data.error.reason; + } + } + alert(msg); +}; + +/** + * param: msg -> message(String) + * param: title -> title(String) + * param: okLabel -> ok button text(String) + * param: cancelLabel -> cancel button text(String) + * param: onOkEvt -> ok click event(Function) + * param: onCancelEvt -> cancel click event(Function) + * return: + * exception: + * Description : confirm + */ +const confirm = params => { + let param = { + msg: '', + title: '', + okLabel: '', + cancelLabel: '', + onOkEvt: () => {}, + onCancelEvt: () => {}, + }; + param = Object.assign(param, params); + SdlModal.app.config.globalProperties.$modal.show('dialog', { + title: getMsgProp(param.title) || getMsgProp('sdl.common.label.confirm'), + text: getMsgProp(param.msg), + buttons: [ + { + title: getMsgProp(param.cancelLabel) || getMsgProp('sdl.common.label.cancel'), + default: true, + handler: () => { + cancelHandler(param); + }, + }, + { + title: getMsgProp(param.okLabel) || getMsgProp('sdl.common.label.confirm'), + default: false, + handler: () => { + okHandler(param); + }, + }, + ], + }); +}; + +/** + * param: modal (import vue) + * param: Please refer to http://devops.sdsdev.co.kr/confluence/display/SDL/Modal + * return: + * exception: + * Description : modal popup + */ +const show = (modal, ...args) => { + SdlModal.app.config.globalProperties.$modal.show(modal, ...args); +}; + +/** + * param: true/false (Boolean) + * return: + * exception: + * Description : default loading bar show/hide + */ +const showLoadingBar = (flag, args) => { + if (flag) { + SdlModal.app.config.globalProperties.$modal.show('loadingbar', args); + } else { + SdlModal.app.config.globalProperties.$modal.hide('loadingbar'); + } +}; + +/** + * param: + * return: array + * exception: + * Description : Import a complete list of codes. + */ +const loadAllCommCodeList = () => + axios + .get(`${API_URL}/commcode/groupcodes/all-commcodes`) + .then(response => { + codeList = response.data; + }) + .catch(error => { + // handle error + console.log(error); + }); +/* eslint-enable */ + +/** + * param: commCodeTypeId -> groupcode(String) + * param: sort -> {sortColumn : String, sortType : asc/desc} + * return: array + * exception: + * Description : Code list return for a specific parentId in the entire code list + */ +const getCommCodeList = params => { + let param = { + commCodeTypeId: '', + sort: { sortColumn: 'ord', sortType: 'asc' }, + }; + param = Object.assign(param, params); + + return _.orderBy( + codeList.filter(code => code.commCodeTypeId === param.commCodeTypeId), + param.sort.sortColumn, + param.sort.sortType, + ); +}; + +/** + * param: searchColumn(userName, knoxId, epId, email) + * param: searchTxt -> search text(String) + * param: rtnFunc -> Function to receive a list of discovered users + * param: knoxSearch -> true(knox search), false(db search(default)) + * return: + * exception: + * Description : user search popup + */ +const openUserPopup = param => { + show( + UserPopup, + param, // component에서 props 로 선언한 인자 전달 + { + width: 850, + height: 'auto', + }, // 모달로 전달할 Properties + ); +}; + +/** + * param: url -> script url path + * return: + * exception: + * Description : es6 can not be imported from es legacy script. + */ +const importJS = url => + new Promise((resolve, reject) => { + // Skip if it already exists + if (document.querySelectorAll(`script[src="${url}"]`).length !== 0) { + setTimeout(() => { + if (window.cafe) resolve(window); + else { + setTimeout(() => { + if (window.cafe) resolve(window); + else reject({ data: { error: { reason: 'Script is not loaded' } } }); + }, 700); + } + }, 300); + return; + } + const script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = url; + script.addEventListener('load', () => resolve(window), false); + script.addEventListener('error', () => reject(script), false); + document.body.appendChild(script); + }); + +/** + * param: boardId(String) + * return: Array + * exception: + * Description : Information generated. list in menu management boards page/api + */ +const getBoardPageApiList = (boardId, boardType, roleType) => ({ + page: [ + { + authorizationType: 'READ', + pageName: '게시글 목록', + pagePath: `/board/${boardId}/${roleType}/list`, + componentName: 'admin/board/PostList', + isEdited: true, + }, + { + authorizationType: 'EXECUTE', + pageName: '게시글 등록', + pagePath: `/board/${boardId}/${roleType}/regist`, + componentName: 'admin/board/PostEdit', + isEdited: true, + }, + { + authorizationType: 'READ', + pageName: '게시글 상세', + pagePath: `/board/${boardId}/${roleType}/view/:postId/:viewType`, + componentName: 'admin/board/PostView', + isEdited: true, + }, + { + authorizationType: 'UPDATE', + pageName: '게시글 수정', + pagePath: `/board/${boardId}/${roleType}/edit/:postId`, + componentName: 'admin/board/PostEdit', + isEdited: true, + }, + { + authorizationType: 'EXECUTE', + pageName: '답글 등록', + pagePath: `/board/${boardId}/${roleType}/reply/:postId`, + componentName: 'admin/board/PostEdit', + isEdited: true, + }, + ], + api: [ + { + authorizationType: 'READ', + apiName: '게시판 상세 정보 조회', + httpMethod: 'GET', + serverId: 'AWwHn0OqABHw5fDS', + apiUrl: `/boards/${boardId}`, + used: true, + }, + { + authorizationType: 'READ', + apiName: '공지사항 목록 조회', + httpMethod: 'GET', + serverId: 'AWwHn0OqABHw5fDS', + apiUrl: `/boards/${boardId}/notice-label-posts`, + used: true, + }, + { + authorizationType: 'READ', + apiName: '게시글 목록 조회(페이징)', + httpMethod: 'GET', + serverId: 'AWwHn0OqABHw5fDS', + apiUrl: `/boards/${boardId}/posts-with-paging`, + used: true, + }, + { + authorizationType: 'READ', + apiName: '게시글 상세조회', + httpMethod: 'GET', + serverId: 'AWwHn0OqABHw5fDS', + apiUrl: `/boards/${boardId}/posts/{postId}`, + used: true, + }, + { + authorizationType: 'EXECUTE', + apiName: '게시글 등록', + httpMethod: 'POST', + serverId: 'AWwHn0OqABHw5fDS', + apiUrl: `/boards/${boardId}/posts`, + used: true, + }, + { + authorizationType: 'UPDATE', + apiName: '게시글 수정', + httpMethod: 'PUT', + serverId: 'AWwHn0OqABHw5fDS', + apiUrl: `/boards/${boardId}/posts/{postId}`, + used: true, + }, + { + authorizationType: 'EXECUTE', + apiName: '게시글 삭제', + httpMethod: 'DELETE', + serverId: 'AWwHn0OqABHw5fDS', + apiUrl: `/boards/${boardId}/posts/{postId}`, + used: true, + }, + { + authorizationType: 'EXECUTE', + apiName: '게시글 추천', + httpMethod: 'POST', + serverId: 'AWwHn0OqABHw5fDS', + apiUrl: `/boards/${boardId}/posts-recommend/{postId}`, + used: true, + }, + { + authorizationType: 'READ', + apiName: '댓글 목록 조회', + httpMethod: 'GET', + serverId: 'AWwHn0OqABHw5fDS', + apiUrl: `/boards/${boardId}/posts/{postId}/comments`, + used: true, + }, + { + authorizationType: 'EXECUTE', + apiName: '댓글 등록', + httpMethod: 'POST', + serverId: 'AWwHn0OqABHw5fDS', + apiUrl: `/boards/${boardId}/posts/{postId}/comments`, + used: true, + }, + { + authorizationType: 'UPDATE', + apiName: '댓글 수정', + httpMethod: 'PUT', + serverId: 'AWwHn0OqABHw5fDS', + apiUrl: `/boards/${boardId}/posts/{postId}/comments/{commentId}`, + used: true, + }, + { + authorizationType: 'EXECUTE', + apiName: '댓글 삭제', + httpMethod: 'DELETE', + serverId: 'AWwHn0OqABHw5fDS', + apiUrl: `/boards/${boardId}/posts/{postId}/comments/{commentId}`, + used: true, + }, + { + authorizationType: 'EXECUTE', + apiName: '댓글 평가(추천/반대)', + httpMethod: 'POST', + serverId: 'AWwHn0OqABHw5fDS', + apiUrl: `/boards/${boardId}/posts/{postId}/comment-consent/{commentId}`, + used: true, + }, + { + authorizationType: 'EXECUTE', + apiName: '파일 업로드', + httpMethod: 'POST', + serverId: 'AWwHn0OqABHw5fDS', + apiUrl: '/resource/attachments/multifile-upload', + used: true, + }, + { + authorizationType: 'DOWNLOAD', + apiName: '파일 다운로드', + httpMethod: 'GET', + serverId: 'AWwHn0OqABHw5fDS', + apiUrl: `/resource/attachments/file-download/{fileId}?downloadType=${boardId}`, + used: true, + }, + { + authorizationType: 'DOWNLOAD', + apiName: '파일 다운로드(Multi)', + httpMethod: 'GET', + serverId: 'AWwHn0OqABHw5fDS', + apiUrl: `/resource/attachments/multifile-download?downloadType=${boardId}${_.eq(boardType, 'BASIC') ? '' : '_IMAGE'}`, + used: true, + }, + { + authorizationType: 'READ', + apiName: '이미지 다운로드', + httpMethod: 'GET', + serverId: 'AWwHn0OqABHw5fDS', + apiUrl: `/resource/attachments/file-download/{fileId}?downloadType=${boardId}_IMAGE`, + used: true, + }, + ], +}); + +/** + * param: Object + * return: formData + * exception: + * Description : change object to formData + */ +const formParameters = obj => { + const formData = new FormData(); + Object.keys(obj).forEach(key => { + if (obj[key]) { + formData.append(key, obj[key]); + } + }); + return formData; +}; + +const validList = []; + +/** + * param: Object + * return: + * exception: + * Description : Save Validation on Page as a list + */ +const setValidList = el => { + validList.push(el); +}; + +/** + * param: groupId => String + * return: + * exception: + * Description : Delete Validation list corresponding to groupId + */ +const deleteValidList = (el, groupId) => { + for (let i = 0; i < validList.length; i += 1) { + if (validList[i].groupId === groupId && validList[i].el === el) { + validList.splice(i, 1); + // i -= -1; + break; + } + } +}; + +/** + * param: groupId => String + * return: + * exception: + * Description : Call all onBlur events on page + */ +const onSubmitValidation = groupId => { + if (!groupId) { + groupId = ''; + } + let flag = false; + for (let i = 0; i < validList.length; i += 1) { + if (validList[i].groupId === groupId) { + validList[i].el.focus(); + validList[i].el.blur(); + if (validList[i].el.classList.contains('is-invalid')) { + // validList[i].focus(); + flag = true; + break; + } + } + } + return flag; +}; + +/** + * param: + * return: + * exception: + * Description : delete all is-invalid css + */ +const clearAllIsInvalidCss = () => { + Array.from(document.querySelectorAll('.is-invalid')).forEach(ele => ele.classList.remove('is-invalid')); +}; + +/** + * param: + * return: + * exception: + * Description : parse jwttoken + */ +const parseJwt = token => { + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent( + atob(base64) + .split('') + .map(c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`) + .join(''), + ); + + return JSON.parse(jsonPayload); +}; + +export default { + API_URL, + WEB_CONTEXT_PATH, + DEFAULT_LANG, + isLogin, + getLoginedUserInfo, + getMsgProp, + getI18nLanguage, + setI18nLanguage, + alert, + errorAlert, + confirm, + show, + showLoadingBar, + loadAllCommCodeList, + getCommCodeList, + openUserPopup, + importJS, + getBoardPageApiList, + formParameters, + // onBlurValidation, + onSubmitValidation, + // etcValidationObj, + setValidList, + deleteValidList, + clearAllIsInvalidCss, + parseJwt, +}; diff --git a/frontend/src/utils/StringUtil.js b/frontend/src/utils/StringUtil.js new file mode 100644 index 0000000..d50592c --- /dev/null +++ b/frontend/src/utils/StringUtil.js @@ -0,0 +1,31 @@ +// string 관련 유틸리티 + +/* + * desc : url 쿼리 문자열로 변경 + * param : Object + */ +export const queryStringfy = obj => { + const str = []; + Object.keys(obj).forEach(key => { + if (obj[key]) { + str.push(`${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`); + } + }); + + return str.join('&'); +}; + +/* + * desc : 입력된 문자열이 url 패턴인지 체크 + * param : { + * str : String + * } + */ +export const checkPatternUrl = str => { + const expUrl = /^(http\:\/\/)?((\w+)[.])+(asia|biz|cc|cn|com|de|eu|in|info|jobs|jp|kr|mobi|mx|name|net|nz|org|travel|tv|tw|uk|us)(\/(\w*))*$/i; + return expUrl.test(str); +}; +export default { + queryStringfy, + checkPatternUrl, +}; diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js new file mode 100644 index 0000000..1e8a2a5 --- /dev/null +++ b/frontend/src/utils/index.js @@ -0,0 +1,7 @@ +import SDLUtil from './SDLUtil'; + +export { SDLUtil }; +export { default as StringUtil } from './StringUtil'; +export { default as DateUtil } from './DateUtil'; +export { default as GridUtil } from './GridUtil'; +// export default { SDLUtil }; diff --git a/frontend/src/vuex/actions.js b/frontend/src/vuex/actions.js new file mode 100644 index 0000000..c6bff7e --- /dev/null +++ b/frontend/src/vuex/actions.js @@ -0,0 +1,133 @@ +import { UID, IS_AUTH, ERROR_STATE, MENUS, LOCALE, PARAMS, AVAILABLE_MY_MENUS, MY_MENUS } from './mutation_types'; + +import api from '@/service'; +import { find } from 'lodash'; + +const setUID = ({ commit }, data) => { + commit(UID, data); +}; + +const setErrorState = ({ commit }, data) => { + commit(ERROR_STATE, data); +}; + +const setIsAuth = ({ commit }, data) => { + commit(IS_AUTH, data); +}; + +const setMenus = ({ commit }, data) => { + commit(MENUS, data); +}; + +const setAvailableMyMenus = ({ commit }, data) => { + commit(AVAILABLE_MY_MENUS, data); +}; + +const setMyMenus = ({ commit }, data) => { + commit(MY_MENUS, data); +}; + +const setLocale = ({ commit }, data) => { + commit(LOCALE, data); +}; + +const setParams = ({ commit }, data) => { + commit(PARAMS, data); +}; + +const processResponse = (store, loginResponse) => { + // http://devops.sdsdev.co.kr/confluence/pages/viewpage.action?pageId=76315514 code표 참고 + // console.log('processResponse:', loginResponse); + switch (loginResponse.code) { + case 'authOk': + setUID(store, loginResponse.uid); + setErrorState(store, { code: loginResponse.code }); + setIsAuth(store, true); + break; + case 'exception': + setErrorState(store, { code: loginResponse.code, message: 'System error' }); + setIsAuth(store, false); + break; + default: + // console.log(typeof loginResponse.code); + if (typeof loginResponse.code === 'number') { + loginResponse.code = String(loginResponse.code); + } + setErrorState(store, loginResponse); + setIsAuth(store, false); + break; + } +}; + +const myMenus = (store, menu) => { + let myMenus = []; + menu.map(item => { + myMenus.push(find(store.getters.getAvailableMyMenus, ['menuId', item])); + }); + return myMenus; +} + +export default { + async login(store, { userId, password }) { + const loginResponse = await api.login(userId, password); + processResponse(store, loginResponse); + return store.getters.getIsAuth; + }, + async loginNewEpTray(store, { userInfo, key }) { + const loginResponse = await api.loginNewEpTray(userInfo, key); + processResponse(store, loginResponse); + return store.getters.getIsAuth; + }, + logout() { + api.logout(); + }, + async getMenus(store, forceRemote = false) { + const menuResponse = await api.getMenus(forceRemote); + setMenus(store, menuResponse); + const availableMyMenu = menuResponse.filter(menu => { + // 외부 링크 사용 + if (menu.externalUrlUsed) { + return true; + } + // default Page 가 있는 메뉴 + return menu.pages.length > 0 && find(menu.pages, ['defaulted', true]); + }).map(menu => { + const defaultPage = find(menu.pages, ['defaulted', true]); + return { + ...menu, + ...defaultPage, + selected: false, + }; + }); + setAvailableMyMenus(store, availableMyMenu); + }, + async getMyMenus(store) { + const menuResponse = await api.menuService.getMyMenus(); + setMyMenus(store, myMenus(store, menuResponse)); + }, + addMyMenu(store, menuIds) { + api.menuService.getMyMenus().then(menuResponse => { + menuResponse.push(menuIds); + api.menuService.addMyMenu(menuResponse).then(() => { + setMyMenus(store, myMenus(store, menuResponse)); + }); + }); + }, + deleteMyMenu(store, menuId) { + api.menuService.deleteMyMenu(menuId).then(() => { + api.menuService.getMyMenus().then((menuResponse) => { + setMyMenus(store, myMenus(store, menuResponse)); + }); + }); + }, + moveMyMenu(store, menuIds) { + api.menuService.addMyMenu(menuIds).then(() => { + setMyMenus(store, myMenus(store, menuIds)); + }); + }, + clearCache() { + api.clearCache(); + }, + setLocale, + setParams, +}; diff --git a/frontend/src/vuex/getters.js b/frontend/src/vuex/getters.js new file mode 100644 index 0000000..5d6d665 --- /dev/null +++ b/frontend/src/vuex/getters.js @@ -0,0 +1,10 @@ +export default { + getUid: state => state.uid, + getErrorState: state => state.errorState, + getIsAuth: state => state.isAuth, + getMenus: state => state.menus, + getAvailableMyMenus: state => state.availableMyMenus, + getMyMenus: state => state.myMenus, + getLocale: state => state.locale, + getParams: state => state.params, +}; diff --git a/frontend/src/vuex/mutation_types.js b/frontend/src/vuex/mutation_types.js new file mode 100644 index 0000000..5e495ad --- /dev/null +++ b/frontend/src/vuex/mutation_types.js @@ -0,0 +1,8 @@ +export const UID = 'UID'; +export const ERROR_STATE = 'ERROR_STATE'; +export const IS_AUTH = 'IS_AUTH'; +export const MENUS = 'MENUS'; +export const AVAILABLE_MY_MENUS = 'AVAILABLE_MY_MENUS'; +export const MY_MENUS = 'MY_MENUS'; +export const LOCALE = 'LOCALE'; +export const PARAMS = 'PARAMS'; diff --git a/frontend/src/vuex/mutations.js b/frontend/src/vuex/mutations.js new file mode 100644 index 0000000..bfc780a --- /dev/null +++ b/frontend/src/vuex/mutations.js @@ -0,0 +1,28 @@ +import * as types from './mutation_types'; + +export default { + [types.UID](state, uid) { + state.uid = uid; + }, + [types.ERROR_STATE](state, errorState) { + state.errorState = errorState; + }, + [types.IS_AUTH](state, isAuth) { + state.isAuth = isAuth; + }, + [types.MENUS](state, menus) { + state.menus = menus; + }, + [types.AVAILABLE_MY_MENUS](state, availableMyMenus) { + state.availableMyMenus = availableMyMenus; + }, + [types.MY_MENUS](state, myMenus) { + state.myMenus = myMenus; + }, + [types.LOCALE](state, locale) { + state.locale = locale; + }, + [types.PARAMS](state, params) { + state.params = params; + }, +}; diff --git a/frontend/src/vuex/store.js b/frontend/src/vuex/store.js new file mode 100644 index 0000000..0e8ae16 --- /dev/null +++ b/frontend/src/vuex/store.js @@ -0,0 +1,22 @@ +import { createStore } from 'vuex' +import getters from './getters'; +import actions from './actions'; +import mutations from './mutations'; + +const state = { + uid: '', + errorState: '', + isAuth: false, + menus: [], + availableMenus: [], + myMenus: [], + locale: '', + params: {}, +}; + +export default createStore({ + state, + mutations, + getters, + actions, +}); diff --git a/frontend/static/.gitkeep b/frontend/static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/static/bootstrap/css/bootstrap.min.css b/frontend/static/bootstrap/css/bootstrap.min.css new file mode 100644 index 0000000..0213271 --- /dev/null +++ b/frontend/static/bootstrap/css/bootstrap.min.css @@ -0,0 +1,6 @@ +@charset "UTF-8";/*! + * Bootstrap v5.3.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2022 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text:#0a58ca;--bs-secondary-text:#6c757d;--bs-success-text:#146c43;--bs-info-text:#087990;--bs-warning-text:#997404;--bs-danger-text:#b02a37;--bs-light-text:#6c757d;--bs-dark-text:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#f8f9fa;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#e9ecef;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-2xl:2rem;--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(var(--bs-body-color-rgb), 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(var(--bs-body-color-rgb), 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(var(--bs-body-color-rgb), 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(var(--bs-body-color-rgb), 0.075);--bs-emphasis-color:#000;--bs-form-control-bg:var(--bs-body-bg);--bs-form-control-disabled-bg:var(--bs-secondary-bg);--bs-highlight-bg:#fff3cd;--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}[data-bs-theme=dark]{--bs-body-color:#adb5bd;--bs-body-color-rgb:173,181,189;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#f8f9fa;--bs-emphasis-color-rgb:248,249,250;--bs-secondary-color:rgba(173, 181, 189, 0.75);--bs-secondary-color-rgb:173,181,189;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(173, 181, 189, 0.5);--bs-tertiary-color-rgb:173,181,189;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-emphasis-color:#fff;--bs-primary-text:#6ea8fe;--bs-secondary-text:#dee2e6;--bs-success-text:#75b798;--bs-info-text:#6edff6;--bs-warning-text:#ffda6a;--bs-danger-text:#ea868f;--bs-light-text:#f8f9fa;--bs-dark-text:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#212529;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#495057;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#055160;--bs-warning-border-subtle:#664d03;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:#fff;--bs-link-color:#6ea8fe;--bs-link-hover-color:#9ec5fe;--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:158,197,254;--bs-code-color:#e685b5;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15)}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color,inherit)}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color:var(--bs-body-color);--bs-table-bg:transparent;--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-body-color);--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:var(--bs-body-color);--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:var(--bs-body-color);--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:var(--bs-table-color);vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width) * 2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-color:#000;--bs-table-bg:#cfe2ff;--bs-table-border-color:#bacbe6;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:#e2e3e5;--bs-table-border-color:#cbccce;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000;--bs-table-bg:#d1e7dd;--bs-table-border-color:#bcd0c7;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000;--bs-table-bg:#cff4fc;--bs-table-border-color:#badce3;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000;--bs-table-bg:#fff3cd;--bs-table-border-color:#e6dbb9;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:#f8d7da;--bs-table-border-color:#dfc2c4;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:#dfe0e1;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#373b3e;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + var(--bs-border-width));padding-bottom:calc(.375rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + var(--bs-border-width));padding-bottom:calc(.5rem + var(--bs-border-width));font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + var(--bs-border-width));padding-bottom:calc(.25rem + var(--bs-border-width));font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);background-color:var(--bs-form-control-bg);background-clip:padding-box;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:var(--bs-form-control-bg);border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:var(--bs-secondary-color);opacity:1}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-form-control-disabled-bg);opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:var(--bs-secondary-bg)}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:transparent;border:solid transparent;border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:.5rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:.375rem}.form-control-color::-webkit-color-swatch{border-radius:.375rem}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);background-color:var(--bs-form-control-bg);background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:var(--bs-form-control-disabled-bg)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:.25rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:.5rem}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23adb5bd' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg:var(--bs-form-control-bg);width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-tertiary-bg);border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-tertiary-bg);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating::before:not(.form-control:disabled){position:absolute;top:var(--bs-border-width);left:var(--bs-border-width);width:calc(100% - (calc(calc(.375em + .1875rem) + calc(.75em + .375rem))));height:1.875em;content:"";background-color:var(--bs-form-control-bg);border-radius:.375rem}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;width:100%;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>.form-control:disabled~label{color:#6c757d}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:var(--bs-tertiary-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:.375rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.5rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(var(--bs-border-width) * -1);border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-success-text)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-success);border-radius:var(--bs-border-radius)}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:var(--bs-success);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:var(--bs-success);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:var(--bs-success)}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:var(--bs-success);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-success)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-success-text)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-success-text)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-danger-text)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-danger);border-radius:var(--bs-border-radius)}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:var(--bs-danger);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:var(--bs-danger);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:var(--bs-danger)}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:var(--bs-danger);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-danger)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-danger-text)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-danger-text)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:#212529;--bs-btn-bg:transparent;--bs-btn-border-width:var(--bs-border-width);--bs-btn-border-color:transparent;--bs-btn-border-radius:0.375rem;--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5c636a;--bs-btn-hover-border-color:#565e64;--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:#565e64;--bs-btn-active-border-color:#51585e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#157347;--bs-btn-hover-border-color:#146c43;--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:#146c43;--bs-btn-active-border-color:#13653f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#31d2f2;--bs-btn-hover-border-color:#25cff2;--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:#3dd5f3;--bs-btn-active-border-color:#25cff2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffca2c;--bs-btn-hover-border-color:#ffc720;--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffcd39;--bs-btn-active-border-color:#ffc720;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#bb2d3b;--bs-btn-hover-border-color:#b02a37;--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b02a37;--bs-btn-active-border-color:#a52834;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#d3d4d5;--bs-btn-hover-border-color:#c6c7c8;--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:#c6c7c8;--bs-btn-active-border-color:#babbbc;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424649;--bs-btn-hover-border-color:#373b3e;--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5154;--bs-btn-active-border-color:#373b3e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0d6efd;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6c757d;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#198754;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0dcaf0;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f8f9fa;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:none;--bs-btn-focus-shadow-rgb:49,132,253;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:0.5rem}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:0.25rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:var(--bs-body-bg);--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:0.375rem;--bs-dropdown-border-width:var(--bs-border-width);--bs-dropdown-inner-border-radius:calc(0.375rem - var(--bs-border-width));--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:0 0.5rem 1rem rgba(var(--bs-body-color-rgb), 0.15);--bs-dropdown-link-color:var(--bs-body-color);--bs-dropdown-link-hover-color:var(--bs-body-color);--bs-dropdown-link-hover-bg:var(--bs-tertiary-bg);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:.375rem}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(var(--bs-border-width) * -1)}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:calc(var(--bs-border-width) * -1)}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link.disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:var(--bs-border-width);--bs-nav-tabs-border-color:var(--bs-border-color);--bs-nav-tabs-border-radius:var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color:var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color:var(--bs-emphasis-color);--bs-nav-tabs-link-active-bg:var(--bs-body-bg);--bs-nav-tabs-link-active-border-color:var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));background:0 0;border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.disabled,.nav-tabs .nav-link:disabled{color:var(--bs-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:0.375rem;--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0d6efd}.nav-pills .nav-link{background:0 0;border:0;border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link:disabled{color:var(--bs-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius:0.375rem;--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .show>.nav-link{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar{--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(var(--bs-body-color-rgb), 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:var(--bs-body-bg);--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:var(--bs-body-bg);--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:var(--bs-border-width);--bs-accordion-border-radius:var(--bs-border-radius);--bs-accordion-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230a58ca'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-focus-border-color:#86b7fe;--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:var(--bs-primary-text);--bs-accordion-active-bg:var(--bs-primary-bg-subtle)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:var(--bs-accordion-btn-focus-border-color);outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button,.accordion-flush .accordion-item .accordion-button.collapsed{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:var(--bs-secondary-color);--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:var(--bs-body-bg);--bs-pagination-border-width:var(--bs-border-width);--bs-pagination-border-color:var(--bs-border-color);--bs-pagination-border-radius:var(--bs-border-radius);--bs-pagination-hover-color:var(--bs-link-hover-color);--bs-pagination-hover-bg:var(--bs-tertiary-bg);--bs-pagination-hover-border-color:var(--bs-border-color);--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:var(--bs-secondary-bg);--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-disabled-color:var(--bs-secondary-color);--bs-pagination-disabled-bg:var(--bs-secondary-bg);--bs-pagination-disabled-border-color:var(--bs-border-color);display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(var(--bs-border-width) * -1)}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:0.5rem}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:0.25rem}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:0.375rem;display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:0.375rem;--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:var(--bs-primary-text);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text)}.alert-success{--bs-alert-color:var(--bs-success-text);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text)}.alert-info{--bs-alert-color:var(--bs-info-text);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text)}.alert-warning{--bs-alert-color:var(--bs-warning-text);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text)}.alert-danger{--bs-alert-color:var(--bs-danger-text);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text)}.alert-light{--bs-alert-color:var(--bs-light-text);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text)}.alert-dark{--bs-alert-color:var(--bs-dark-text);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text)}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:var(--bs-secondary-bg);--bs-progress-border-radius:var(--bs-border-radius);--bs-progress-box-shadow:var(--bs-box-shadow-inset);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#0d6efd;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:var(--bs-body-color);--bs-list-group-bg:var(--bs-body-bg);--bs-list-group-border-color:var(--bs-border-color);--bs-list-group-border-width:var(--bs-border-width);--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:var(--bs-secondary-color);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-tertiary-bg);--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:var(--bs-secondary-bg);--bs-list-group-disabled-color:var(--bs-secondary-color);--bs-list-group-disabled-bg:var(--bs-body-bg);--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle)}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle)}.list-group-item-primary.list-group-item-action:active{--bs-list-group-active-color:var(--bs-emphasis-color);--bs-list-group-active-bg:var(--bs-primary-text);--bs-list-group-active-border-color:var(--bs-primary-text)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle)}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle)}.list-group-item-secondary.list-group-item-action:active{--bs-list-group-active-color:var(--bs-emphasis-color);--bs-list-group-active-bg:var(--bs-secondary-text);--bs-list-group-active-border-color:var(--bs-secondary-text)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle)}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle)}.list-group-item-success.list-group-item-action:active{--bs-list-group-active-color:var(--bs-emphasis-color);--bs-list-group-active-bg:var(--bs-success-text);--bs-list-group-active-border-color:var(--bs-success-text)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle)}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle)}.list-group-item-info.list-group-item-action:active{--bs-list-group-active-color:var(--bs-emphasis-color);--bs-list-group-active-bg:var(--bs-info-text);--bs-list-group-active-border-color:var(--bs-info-text)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle)}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle)}.list-group-item-warning.list-group-item-action:active{--bs-list-group-active-color:var(--bs-emphasis-color);--bs-list-group-active-bg:var(--bs-warning-text);--bs-list-group-active-border-color:var(--bs-warning-text)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle)}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle)}.list-group-item-danger.list-group-item-action:active{--bs-list-group-active-color:var(--bs-emphasis-color);--bs-list-group-active-bg:var(--bs-danger-text);--bs-list-group-active-border-color:var(--bs-danger-text)}.list-group-item-light{--bs-list-group-color:var(--bs-light-text);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle)}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle)}.list-group-item-light.list-group-item-action:active{--bs-list-group-active-color:var(--bs-emphasis-color);--bs-list-group-active-bg:var(--bs-light-text);--bs-list-group-active-border-color:var(--bs-light-text)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle)}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle)}.list-group-item-dark.list-group-item-action:active{--bs-list-group-active-color:var(--bs-emphasis-color);--bs-list-group-active-bg:var(--bs-dark-text);--bs-list-group-active-border-color:var(--bs-dark-text)}.btn-close{--bs-btn-close-color:#000;--bs-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");--bs-btn-close-opacity:0.5;--bs-btn-close-hover-opacity:0.75;--bs-btn-close-focus-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;--bs-btn-close-white-filter:invert(1) grayscale(100%) brightness(200%);box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;border:0;border-radius:.375rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{filter:var(--bs-btn-close-white-filter)}[data-bs-theme=dark] .btn-close{filter:var(--bs-btn-close-white-filter)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-border-width:var(--bs-border-width);--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:var(--bs-secondary-color);--bs-toast-header-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-header-border-color:var(--bs-border-color-translucent);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color: ;--bs-modal-bg:var(--bs-body-bg);--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:var(--bs-border-width);--bs-modal-border-radius:var(--bs-border-radius-lg);--bs-modal-box-shadow:0 0.125rem 0.25rem rgba(var(--bs-body-color-rgb), 0.075);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:var(--bs-border-width);--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(-.5 * var(--bs-modal-header-padding-y)) calc(-.5 * var(--bs-modal-header-padding-x)) calc(-.5 * var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:0 0.5rem 1rem rgba(var(--bs-body-color-rgb), 0.15)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:var(--bs-body-bg);--bs-tooltip-bg:var(--bs-emphasis-color);--bs-tooltip-border-radius:var(--bs-border-radius);--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;padding:var(--bs-tooltip-arrow-height);margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:var(--bs-body-bg);--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:var(--bs-border-radius-lg);--bs-popover-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow:0 0.5rem 1rem rgba(var(--bs-body-color-rgb), 0.15);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color: ;--bs-popover-header-bg:var(--bs-secondary-bg);--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:var(--bs-body-color);--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}[data-bs-theme=dark] .carousel .carousel-control-next-icon,[data-bs-theme=dark] .carousel .carousel-control-prev-icon{filter:invert(1) grayscale(100)}[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target]{background-color:#000}[data-bs-theme=dark] .carousel .carousel-caption{color:#000}.spinner-border,.spinner-grow{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:var(--bs-border-width);--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:0 0.125rem 0.25rem rgba(var(--bs-body-color-rgb), 0.075);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:575.98px){.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}}@media (max-width:575.98px){.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:767.98px){.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}}@media (max-width:767.98px){.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:991.98px){.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}}@media (max-width:991.98px){.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:1199.98px){.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}}@media (max-width:1199.98px){.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:1399.98px){.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}}@media (max-width:1399.98px){.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin-top:calc(-.5 * var(--bs-offcanvas-padding-y));margin-right:calc(-.5 * var(--bs-offcanvas-padding-x));margin-bottom:calc(-.5 * var(--bs-offcanvas-padding-y))}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(13,110,253,var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(108,117,125,var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(25,135,84,var(--bs-bg-opacity,1))!important}.text-bg-info{color:#000!important;background-color:RGBA(13,202,240,var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(255,193,7,var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(220,53,69,var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(248,249,250,var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(33,37,41,var(--bs-bg-opacity,1))!important}.link-primary{color:#0d6efd!important}.link-primary:focus,.link-primary:hover{color:#0a58ca!important}.link-secondary{color:#6c757d!important}.link-secondary:focus,.link-secondary:hover{color:#565e64!important}.link-success{color:#198754!important}.link-success:focus,.link-success:hover{color:#146c43!important}.link-info{color:#0dcaf0!important}.link-info:focus,.link-info:hover{color:#3dd5f3!important}.link-warning{color:#ffc107!important}.link-warning:focus,.link-warning:hover{color:#ffcd39!important}.link-danger{color:#dc3545!important}.link-danger:focus,.link-danger:hover{color:#b02a37!important}.link-light{color:#f8f9fa!important}.link-light:focus,.link-light:hover{color:#f9fafb!important}.link-dark{color:#212529!important}.link-dark:focus,.link-dark:hover{color:#1a1e21!important}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(var(--bs-body-color-rgb),.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(var(--bs-body-color-rgb),.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(var(--bs-body-color-rgb),.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!important}.border-1{--bs-border-width:1px}.border-2{--bs-border-width:2px}.border-3{--bs-border-width:3px}.border-4{--bs-border-width:4px}.border-5{--bs-border-width:5px}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.25rem!important}.row-gap-2{row-gap:.5rem!important}.row-gap-3{row-gap:1rem!important}.row-gap-4{row-gap:1.5rem!important}.row-gap-5{row-gap:3rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text)!important}.text-secondary-emphasis{color:var(--bs-secondary-text)!important}.text-success-emphasis{color:var(--bs-success-text)!important}.text-info-emphasis{color:var(--bs-info-text)!important}.text-warning-emphasis{color:var(--bs-warning-text)!important}.text-danger-emphasis{color:var(--bs-danger-text)!important}.text-light-emphasis{color:var(--bs-light-text)!important}.text-dark-emphasis{color:var(--bs-dark-text)!important}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-emphasis{--bs-bg-opacity:1;background-color:rgba(var(--bs-emphasis-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-2xl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-2xl)!important;border-top-right-radius:var(--bs-border-radius-2xl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-2xl)!important;border-bottom-right-radius:var(--bs-border-radius-2xl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-2xl)!important;border-bottom-left-radius:var(--bs-border-radius-2xl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-2xl)!important;border-top-left-radius:var(--bs-border-radius-2xl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.25rem!important}.row-gap-sm-2{row-gap:.5rem!important}.row-gap-sm-3{row-gap:1rem!important}.row-gap-sm-4{row-gap:1.5rem!important}.row-gap-sm-5{row-gap:3rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-sm-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-sm-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-sm-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.25rem!important}.row-gap-md-2{row-gap:.5rem!important}.row-gap-md-3{row-gap:1rem!important}.row-gap-md-4{row-gap:1.5rem!important}.row-gap-md-5{row-gap:3rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-md-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-md-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-md-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.25rem!important}.row-gap-lg-2{row-gap:.5rem!important}.row-gap-lg-3{row-gap:1rem!important}.row-gap-lg-4{row-gap:1.5rem!important}.row-gap-lg-5{row-gap:3rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-lg-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-lg-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-lg-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.25rem!important}.row-gap-xl-2{row-gap:.5rem!important}.row-gap-xl-3{row-gap:1rem!important}.row-gap-xl-4{row-gap:1.5rem!important}.row-gap-xl-5{row-gap:3rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.25rem!important}.row-gap-xxl-2{row-gap:.5rem!important}.row-gap-xxl-3{row-gap:1rem!important}.row-gap-xxl-4{row-gap:1.5rem!important}.row-gap-xxl-5{row-gap:3rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xxl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xxl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xxl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/frontend/static/bootstrap/css/bootstrap.min.css.map b/frontend/static/bootstrap/css/bootstrap.min.css.map new file mode 100644 index 0000000..3477bc5 --- /dev/null +++ b/frontend/static/bootstrap/css/bootstrap.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/mixins/_banner.scss","../../scss/_root.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","../../scss/mixins/_color-mode.scss","../../scss/_reboot.scss","../../scss/mixins/_border-radius.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/_tables.scss","../../scss/mixins/_table-variants.scss","../../scss/forms/_labels.scss","../../scss/forms/_form-text.scss","../../scss/forms/_form-control.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_gradients.scss","../../scss/forms/_form-select.scss","../../scss/forms/_form-check.scss","../../scss/forms/_form-range.scss","../../scss/forms/_floating-labels.scss","../../scss/forms/_input-group.scss","../../scss/mixins/_forms.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/_button-group.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_accordion.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/_alert.scss","../../scss/_progress.scss","../../scss/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/mixins/_backdrop.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/_offcanvas.scss","../../scss/_placeholders.scss","../../scss/helpers/_color-bg.scss","../../scss/helpers/_colored-links.scss","../../scss/helpers/_ratio.scss","../../scss/helpers/_position.scss","../../scss/helpers/_stacks.scss","../../scss/helpers/_visually-hidden.scss","../../scss/mixins/_visually-hidden.scss","../../scss/helpers/_stretched-link.scss","../../scss/helpers/_text-truncation.scss","../../scss/mixins/_text-truncate.scss","../../scss/helpers/_vr.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"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","sourcesContent":["@mixin bsBanner($file) {\n /*!\n * Bootstrap #{$file} v5.3.0-alpha1 (https://getbootstrap.com/)\n * Copyright 2011-2022 The Bootstrap Authors\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n}\n",":root,\n[data-bs-theme=\"light\"] {\n // Note: Custom variable values only support SassScript inside `#{}`.\n\n // Colors\n //\n // Generate palettes for full colors, grays, and theme colors.\n\n @each $color, $value in $colors {\n --#{$prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $grays {\n --#{$prefix}gray-#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors-rgb {\n --#{$prefix}#{$color}-rgb: #{$value};\n }\n\n @each $color, $value in $theme-colors-text {\n --#{$prefix}#{$color}-text: #{$value};\n }\n\n @each $color, $value in $theme-colors-bg-subtle {\n --#{$prefix}#{$color}-bg-subtle: #{$value};\n }\n\n @each $color, $value in $theme-colors-border-subtle {\n --#{$prefix}#{$color}-border-subtle: #{$value};\n }\n\n --#{$prefix}white-rgb: #{to-rgb($white)};\n --#{$prefix}black-rgb: #{to-rgb($black)};\n --#{$prefix}body-color-rgb: #{to-rgb($body-color)};\n --#{$prefix}body-bg-rgb: #{to-rgb($body-bg)};\n\n // Fonts\n\n // Note: Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --#{$prefix}font-sans-serif: #{inspect($font-family-sans-serif)};\n --#{$prefix}font-monospace: #{inspect($font-family-monospace)};\n --#{$prefix}gradient: #{$gradient};\n\n // Root and body\n // scss-docs-start root-body-variables\n @if $font-size-root != null {\n --#{$prefix}root-font-size: #{$font-size-root};\n }\n --#{$prefix}body-font-family: #{inspect($font-family-base)};\n @include rfs($font-size-base, --#{$prefix}body-font-size);\n --#{$prefix}body-font-weight: #{$font-weight-base};\n --#{$prefix}body-line-height: #{$line-height-base};\n --#{$prefix}body-color: #{$body-color};\n\n --#{$prefix}emphasis-color: #{$body-emphasis-color};\n --#{$prefix}emphasis-color-rgb: #{to-rgb($body-emphasis-color)};\n\n --#{$prefix}secondary-color: #{$body-secondary-color};\n --#{$prefix}secondary-color-rgb: #{to-rgb($body-secondary-color)};\n --#{$prefix}secondary-bg: #{$body-secondary-bg};\n --#{$prefix}secondary-bg-rgb: #{to-rgb($body-secondary-bg)};\n\n --#{$prefix}tertiary-color: #{$body-tertiary-color};\n --#{$prefix}tertiary-color-rgb: #{to-rgb($body-tertiary-color)};\n --#{$prefix}tertiary-bg: #{$body-tertiary-bg};\n --#{$prefix}tertiary-bg-rgb: #{to-rgb($body-tertiary-bg)};\n\n @if $body-text-align != null {\n --#{$prefix}body-text-align: #{$body-text-align};\n }\n --#{$prefix}body-bg: #{$body-bg};\n --#{$prefix}body-bg-rgb: #{to-rgb($body-bg)};\n // scss-docs-end root-body-variables\n\n @if $headings-color != null {\n --#{$prefix}heading-color: #{$headings-color};\n }\n\n --#{$prefix}link-color: #{$link-color};\n --#{$prefix}link-color-rgb: #{to-rgb($link-color)};\n --#{$prefix}link-decoration: #{$link-decoration};\n\n --#{$prefix}link-hover-color: #{$link-hover-color};\n --#{$prefix}link-hover-color-rgb: #{to-rgb($link-hover-color)};\n\n @if $link-hover-decoration != null {\n --#{$prefix}link-hover-decoration: #{$link-hover-decoration};\n }\n\n --#{$prefix}code-color: #{$code-color};\n --#{$prefix}highlight-bg: #{$mark-bg};\n\n // scss-docs-start root-border-var\n --#{$prefix}border-width: #{$border-width};\n --#{$prefix}border-style: #{$border-style};\n --#{$prefix}border-color: #{$border-color};\n --#{$prefix}border-color-translucent: #{$border-color-translucent};\n\n --#{$prefix}border-radius: #{$border-radius};\n --#{$prefix}border-radius-sm: #{$border-radius-sm};\n --#{$prefix}border-radius-lg: #{$border-radius-lg};\n --#{$prefix}border-radius-xl: #{$border-radius-xl};\n --#{$prefix}border-radius-2xl: #{$border-radius-2xl};\n --#{$prefix}border-radius-pill: #{$border-radius-pill};\n // scss-docs-end root-border-var\n\n --#{$prefix}box-shadow: #{$box-shadow};\n --#{$prefix}box-shadow-sm: #{$box-shadow-sm};\n --#{$prefix}box-shadow-lg: #{$box-shadow-lg};\n --#{$prefix}box-shadow-inset: #{$box-shadow-inset};\n\n --#{$prefix}emphasis-color: #{$emphasis-color};\n\n // scss-docs-start form-control-vars\n --#{$prefix}form-control-bg: var(--#{$prefix}body-bg);\n --#{$prefix}form-control-disabled-bg: var(--#{$prefix}secondary-bg);\n // scss-docs-end form-control-vars\n\n --#{$prefix}highlight-bg: #{$mark-bg};\n\n @each $name, $value in $grid-breakpoints {\n --#{$prefix}breakpoint-#{$name}: #{$value};\n }\n}\n\n@if $enable-dark-mode {\n @include color-mode(dark, true) {\n // scss-docs-start root-dark-mode-vars\n --#{$prefix}body-color: #{$body-color-dark};\n --#{$prefix}body-color-rgb: #{to-rgb($body-color-dark)};\n --#{$prefix}body-bg: #{$body-bg-dark};\n --#{$prefix}body-bg-rgb: #{to-rgb($body-bg-dark)};\n\n --#{$prefix}emphasis-color: #{$body-emphasis-color-dark};\n --#{$prefix}emphasis-color-rgb: #{to-rgb($body-emphasis-color-dark)};\n\n --#{$prefix}secondary-color: #{$body-secondary-color-dark};\n --#{$prefix}secondary-color-rgb: #{to-rgb($body-secondary-color-dark)};\n --#{$prefix}secondary-bg: #{$body-secondary-bg-dark};\n --#{$prefix}secondary-bg-rgb: #{to-rgb($body-secondary-bg-dark)};\n\n --#{$prefix}tertiary-color: #{$body-tertiary-color-dark};\n --#{$prefix}tertiary-color-rgb: #{to-rgb($body-tertiary-color-dark)};\n --#{$prefix}tertiary-bg: #{$body-tertiary-bg-dark};\n --#{$prefix}tertiary-bg-rgb: #{to-rgb($body-tertiary-bg-dark)};\n\n --#{$prefix}emphasis-color: #{$emphasis-color-dark};\n\n --#{$prefix}primary-text: #{$primary-text-dark};\n --#{$prefix}secondary-text: #{$secondary-text-dark};\n --#{$prefix}success-text: #{$success-text-dark};\n --#{$prefix}info-text: #{$info-text-dark};\n --#{$prefix}warning-text: #{$warning-text-dark};\n --#{$prefix}danger-text: #{$danger-text-dark};\n --#{$prefix}light-text: #{$light-text-dark};\n --#{$prefix}dark-text: #{$dark-text-dark};\n\n --#{$prefix}primary-bg-subtle: #{$primary-bg-subtle-dark};\n --#{$prefix}secondary-bg-subtle: #{$secondary-bg-subtle-dark};\n --#{$prefix}success-bg-subtle: #{$success-bg-subtle-dark};\n --#{$prefix}info-bg-subtle: #{$info-bg-subtle-dark};\n --#{$prefix}warning-bg-subtle: #{$warning-bg-subtle-dark};\n --#{$prefix}danger-bg-subtle: #{$danger-bg-subtle-dark};\n --#{$prefix}light-bg-subtle: #{$light-bg-subtle-dark};\n --#{$prefix}dark-bg-subtle: #{$dark-bg-subtle-dark};\n\n --#{$prefix}primary-border-subtle: #{$primary-border-subtle-dark};\n --#{$prefix}secondary-border-subtle: #{$secondary-border-subtle-dark};\n --#{$prefix}success-border-subtle: #{$success-border-subtle-dark};\n --#{$prefix}info-border-subtle: #{$info-border-subtle-dark};\n --#{$prefix}warning-border-subtle: #{$warning-border-subtle-dark};\n --#{$prefix}danger-border-subtle: #{$danger-border-subtle-dark};\n --#{$prefix}light-border-subtle: #{$light-border-subtle-dark};\n --#{$prefix}dark-border-subtle: #{$dark-border-subtle-dark};\n\n --#{$prefix}heading-color: #{$headings-color-dark};\n\n --#{$prefix}link-color: #{$link-color-dark};\n --#{$prefix}link-hover-color: #{$link-hover-color-dark};\n --#{$prefix}link-color-rgb: #{to-rgb($link-color-dark)};\n --#{$prefix}link-hover-color-rgb: #{to-rgb($link-hover-color-dark)};\n\n --#{$prefix}code-color: #{$code-color-dark};\n\n --#{$prefix}border-color: #{$border-color-dark};\n --#{$prefix}border-color-translucent: #{$border-color-translucent-dark};\n // scss-docs-end root-dark-mode-vars\n }\n}\n","@charset \"UTF-8\";\n/*!\n * Bootstrap v5.3.0-alpha1 (https://getbootstrap.com/)\n * Copyright 2011-2022 The Bootstrap Authors\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n:root,\n[data-bs-theme=light] {\n --bs-blue: #0d6efd;\n --bs-indigo: #6610f2;\n --bs-purple: #6f42c1;\n --bs-pink: #d63384;\n --bs-red: #dc3545;\n --bs-orange: #fd7e14;\n --bs-yellow: #ffc107;\n --bs-green: #198754;\n --bs-teal: #20c997;\n --bs-cyan: #0dcaf0;\n --bs-black: #000;\n --bs-white: #fff;\n --bs-gray: #6c757d;\n --bs-gray-dark: #343a40;\n --bs-gray-100: #f8f9fa;\n --bs-gray-200: #e9ecef;\n --bs-gray-300: #dee2e6;\n --bs-gray-400: #ced4da;\n --bs-gray-500: #adb5bd;\n --bs-gray-600: #6c757d;\n --bs-gray-700: #495057;\n --bs-gray-800: #343a40;\n --bs-gray-900: #212529;\n --bs-primary: #0d6efd;\n --bs-secondary: #6c757d;\n --bs-success: #198754;\n --bs-info: #0dcaf0;\n --bs-warning: #ffc107;\n --bs-danger: #dc3545;\n --bs-light: #f8f9fa;\n --bs-dark: #212529;\n --bs-primary-rgb: 13, 110, 253;\n --bs-secondary-rgb: 108, 117, 125;\n --bs-success-rgb: 25, 135, 84;\n --bs-info-rgb: 13, 202, 240;\n --bs-warning-rgb: 255, 193, 7;\n --bs-danger-rgb: 220, 53, 69;\n --bs-light-rgb: 248, 249, 250;\n --bs-dark-rgb: 33, 37, 41;\n --bs-primary-text: #0a58ca;\n --bs-secondary-text: #6c757d;\n --bs-success-text: #146c43;\n --bs-info-text: #087990;\n --bs-warning-text: #997404;\n --bs-danger-text: #b02a37;\n --bs-light-text: #6c757d;\n --bs-dark-text: #495057;\n --bs-primary-bg-subtle: #cfe2ff;\n --bs-secondary-bg-subtle: #f8f9fa;\n --bs-success-bg-subtle: #d1e7dd;\n --bs-info-bg-subtle: #cff4fc;\n --bs-warning-bg-subtle: #fff3cd;\n --bs-danger-bg-subtle: #f8d7da;\n --bs-light-bg-subtle: #fcfcfd;\n --bs-dark-bg-subtle: #ced4da;\n --bs-primary-border-subtle: #9ec5fe;\n --bs-secondary-border-subtle: #e9ecef;\n --bs-success-border-subtle: #a3cfbb;\n --bs-info-border-subtle: #9eeaf9;\n --bs-warning-border-subtle: #ffe69c;\n --bs-danger-border-subtle: #f1aeb5;\n --bs-light-border-subtle: #e9ecef;\n --bs-dark-border-subtle: #adb5bd;\n --bs-white-rgb: 255, 255, 255;\n --bs-black-rgb: 0, 0, 0;\n --bs-body-color-rgb: 33, 37, 41;\n --bs-body-bg-rgb: 255, 255, 255;\n --bs-font-sans-serif: system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", \"Noto Sans\", \"Liberation Sans\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));\n --bs-body-font-family: var(--bs-font-sans-serif);\n --bs-body-font-size: 1rem;\n --bs-body-font-weight: 400;\n --bs-body-line-height: 1.5;\n --bs-body-color: #212529;\n --bs-emphasis-color: #000;\n --bs-emphasis-color-rgb: 0, 0, 0;\n --bs-secondary-color: rgba(33, 37, 41, 0.75);\n --bs-secondary-color-rgb: 33, 37, 41;\n --bs-secondary-bg: #e9ecef;\n --bs-secondary-bg-rgb: 233, 236, 239;\n --bs-tertiary-color: rgba(33, 37, 41, 0.5);\n --bs-tertiary-color-rgb: 33, 37, 41;\n --bs-tertiary-bg: #f8f9fa;\n --bs-tertiary-bg-rgb: 248, 249, 250;\n --bs-body-bg: #fff;\n --bs-body-bg-rgb: 255, 255, 255;\n --bs-link-color: #0d6efd;\n --bs-link-color-rgb: 13, 110, 253;\n --bs-link-decoration: underline;\n --bs-link-hover-color: #0a58ca;\n --bs-link-hover-color-rgb: 10, 88, 202;\n --bs-code-color: #d63384;\n --bs-highlight-bg: #fff3cd;\n --bs-border-width: 1px;\n --bs-border-style: solid;\n --bs-border-color: #dee2e6;\n --bs-border-color-translucent: rgba(0, 0, 0, 0.175);\n --bs-border-radius: 0.375rem;\n --bs-border-radius-sm: 0.25rem;\n --bs-border-radius-lg: 0.5rem;\n --bs-border-radius-xl: 1rem;\n --bs-border-radius-2xl: 2rem;\n --bs-border-radius-pill: 50rem;\n --bs-box-shadow: 0 0.5rem 1rem rgba(var(--bs-body-color-rgb), 0.15);\n --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(var(--bs-body-color-rgb), 0.075);\n --bs-box-shadow-lg: 0 1rem 3rem rgba(var(--bs-body-color-rgb), 0.175);\n --bs-box-shadow-inset: inset 0 1px 2px rgba(var(--bs-body-color-rgb), 0.075);\n --bs-emphasis-color: #000;\n --bs-form-control-bg: var(--bs-body-bg);\n --bs-form-control-disabled-bg: var(--bs-secondary-bg);\n --bs-highlight-bg: #fff3cd;\n --bs-breakpoint-xs: 0;\n --bs-breakpoint-sm: 576px;\n --bs-breakpoint-md: 768px;\n --bs-breakpoint-lg: 992px;\n --bs-breakpoint-xl: 1200px;\n --bs-breakpoint-xxl: 1400px;\n}\n\n[data-bs-theme=dark] {\n --bs-body-color: #adb5bd;\n --bs-body-color-rgb: 173, 181, 189;\n --bs-body-bg: #212529;\n --bs-body-bg-rgb: 33, 37, 41;\n --bs-emphasis-color: #f8f9fa;\n --bs-emphasis-color-rgb: 248, 249, 250;\n --bs-secondary-color: rgba(173, 181, 189, 0.75);\n --bs-secondary-color-rgb: 173, 181, 189;\n --bs-secondary-bg: #343a40;\n --bs-secondary-bg-rgb: 52, 58, 64;\n --bs-tertiary-color: rgba(173, 181, 189, 0.5);\n --bs-tertiary-color-rgb: 173, 181, 189;\n --bs-tertiary-bg: #2b3035;\n --bs-tertiary-bg-rgb: 43, 48, 53;\n --bs-emphasis-color: #fff;\n --bs-primary-text: #6ea8fe;\n --bs-secondary-text: #dee2e6;\n --bs-success-text: #75b798;\n --bs-info-text: #6edff6;\n --bs-warning-text: #ffda6a;\n --bs-danger-text: #ea868f;\n --bs-light-text: #f8f9fa;\n --bs-dark-text: #dee2e6;\n --bs-primary-bg-subtle: #031633;\n --bs-secondary-bg-subtle: #212529;\n --bs-success-bg-subtle: #051b11;\n --bs-info-bg-subtle: #032830;\n --bs-warning-bg-subtle: #332701;\n --bs-danger-bg-subtle: #2c0b0e;\n --bs-light-bg-subtle: #343a40;\n --bs-dark-bg-subtle: #1a1d20;\n --bs-primary-border-subtle: #084298;\n --bs-secondary-border-subtle: #495057;\n --bs-success-border-subtle: #0f5132;\n --bs-info-border-subtle: #055160;\n --bs-warning-border-subtle: #664d03;\n --bs-danger-border-subtle: #842029;\n --bs-light-border-subtle: #495057;\n --bs-dark-border-subtle: #343a40;\n --bs-heading-color: #fff;\n --bs-link-color: #6ea8fe;\n --bs-link-hover-color: #9ec5fe;\n --bs-link-color-rgb: 110, 168, 254;\n --bs-link-hover-color-rgb: 158, 197, 254;\n --bs-code-color: #e685b5;\n --bs-border-color: #495057;\n --bs-border-color-translucent: rgba(255, 255, 255, 0.15);\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n :root {\n scroll-behavior: smooth;\n }\n}\n\nbody {\n margin: 0;\n font-family: var(--bs-body-font-family);\n font-size: var(--bs-body-font-size);\n font-weight: var(--bs-body-font-weight);\n line-height: var(--bs-body-line-height);\n color: var(--bs-body-color);\n text-align: var(--bs-body-text-align);\n background-color: var(--bs-body-bg);\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\nhr {\n margin: 1rem 0;\n color: inherit;\n border: 0;\n border-top: var(--bs-border-width) solid;\n opacity: 0.25;\n}\n\nh6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n font-weight: 500;\n line-height: 1.2;\n color: var(--bs-heading-color, inherit);\n}\n\nh1, .h1 {\n font-size: calc(1.375rem + 1.5vw);\n}\n@media (min-width: 1200px) {\n h1, .h1 {\n font-size: 2.5rem;\n }\n}\n\nh2, .h2 {\n font-size: calc(1.325rem + 0.9vw);\n}\n@media (min-width: 1200px) {\n h2, .h2 {\n font-size: 2rem;\n }\n}\n\nh3, .h3 {\n font-size: calc(1.3rem + 0.6vw);\n}\n@media (min-width: 1200px) {\n h3, .h3 {\n font-size: 1.75rem;\n }\n}\n\nh4, .h4 {\n font-size: calc(1.275rem + 0.3vw);\n}\n@media (min-width: 1200px) {\n h4, .h4 {\n font-size: 1.5rem;\n }\n}\n\nh5, .h5 {\n font-size: 1.25rem;\n}\n\nh6, .h6 {\n font-size: 1rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title] {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n cursor: help;\n -webkit-text-decoration-skip-ink: none;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: 0.5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall, .small {\n font-size: 0.875em;\n}\n\nmark, .mark {\n padding: 0.1875em;\n background-color: var(--bs-highlight-bg);\n}\n\nsub,\nsup {\n position: relative;\n font-size: 0.75em;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -0.25em;\n}\n\nsup {\n top: -0.5em;\n}\n\na {\n color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));\n text-decoration: underline;\n}\na:hover {\n --bs-link-color-rgb: var(--bs-link-hover-color-rgb);\n}\n\na:not([href]):not([class]), a:not([href]):not([class]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: var(--bs-font-monospace);\n font-size: 1em;\n}\n\npre {\n display: block;\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n font-size: 0.875em;\n}\npre code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n}\n\ncode {\n font-size: 0.875em;\n color: var(--bs-code-color);\n word-wrap: break-word;\n}\na > code {\n color: inherit;\n}\n\nkbd {\n padding: 0.1875rem 0.375rem;\n font-size: 0.875em;\n color: var(--bs-body-bg);\n background-color: var(--bs-body-color);\n border-radius: 0.25rem;\n}\nkbd kbd {\n padding: 0;\n font-size: 1em;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n color: var(--bs-secondary-color);\n text-align: left;\n}\n\nth {\n text-align: inherit;\n text-align: -webkit-match-parent;\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\nlabel {\n display: inline-block;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\n[role=button] {\n cursor: pointer;\n}\n\nselect {\n word-wrap: normal;\n}\nselect:disabled {\n opacity: 1;\n}\n\n[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {\n display: none !important;\n}\n\nbutton,\n[type=button],\n[type=reset],\n[type=submit] {\n -webkit-appearance: button;\n}\nbutton:not(:disabled),\n[type=button]:not(:disabled),\n[type=reset]:not(:disabled),\n[type=submit]:not(:disabled) {\n cursor: pointer;\n}\n\n::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ntextarea {\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n float: left;\n width: 100%;\n padding: 0;\n margin-bottom: 0.5rem;\n font-size: calc(1.275rem + 0.3vw);\n line-height: inherit;\n}\n@media (min-width: 1200px) {\n legend {\n font-size: 1.5rem;\n }\n}\nlegend + * {\n clear: left;\n}\n\n::-webkit-datetime-edit-fields-wrapper,\n::-webkit-datetime-edit-text,\n::-webkit-datetime-edit-minute,\n::-webkit-datetime-edit-hour-field,\n::-webkit-datetime-edit-day-field,\n::-webkit-datetime-edit-month-field,\n::-webkit-datetime-edit-year-field {\n padding: 0;\n}\n\n::-webkit-inner-spin-button {\n height: auto;\n}\n\n[type=search] {\n outline-offset: -2px;\n -webkit-appearance: textfield;\n}\n\n/* rtl:raw:\n[type=\"tel\"],\n[type=\"url\"],\n[type=\"email\"],\n[type=\"number\"] {\n direction: ltr;\n}\n*/\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-color-swatch-wrapper {\n padding: 0;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\n::file-selector-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\niframe {\n border: 0;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[hidden] {\n display: none !important;\n}\n\n.lead {\n font-size: 1.25rem;\n font-weight: 300;\n}\n\n.display-1 {\n font-size: calc(1.625rem + 4.5vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-1 {\n font-size: 5rem;\n }\n}\n\n.display-2 {\n font-size: calc(1.575rem + 3.9vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-2 {\n font-size: 4.5rem;\n }\n}\n\n.display-3 {\n font-size: calc(1.525rem + 3.3vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-3 {\n font-size: 4rem;\n }\n}\n\n.display-4 {\n font-size: calc(1.475rem + 2.7vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-4 {\n font-size: 3.5rem;\n }\n}\n\n.display-5 {\n font-size: calc(1.425rem + 2.1vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-5 {\n font-size: 3rem;\n }\n}\n\n.display-6 {\n font-size: calc(1.375rem + 1.5vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-6 {\n font-size: 2.5rem;\n }\n}\n\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline-item {\n display: inline-block;\n}\n.list-inline-item:not(:last-child) {\n margin-right: 0.5rem;\n}\n\n.initialism {\n font-size: 0.875em;\n text-transform: uppercase;\n}\n\n.blockquote {\n margin-bottom: 1rem;\n font-size: 1.25rem;\n}\n.blockquote > :last-child {\n margin-bottom: 0;\n}\n\n.blockquote-footer {\n margin-top: -1rem;\n margin-bottom: 1rem;\n font-size: 0.875em;\n color: #6c757d;\n}\n.blockquote-footer::before {\n content: \"— \";\n}\n\n.img-fluid {\n max-width: 100%;\n height: auto;\n}\n\n.img-thumbnail {\n padding: 0.25rem;\n background-color: var(--bs-body-bg);\n border: var(--bs-border-width) solid var(--bs-border-color);\n border-radius: var(--bs-border-radius);\n max-width: 100%;\n height: auto;\n}\n\n.figure {\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: 0.5rem;\n line-height: 1;\n}\n\n.figure-caption {\n font-size: 0.875em;\n color: var(--bs-secondary-color);\n}\n\n.container,\n.container-fluid,\n.container-xxl,\n.container-xl,\n.container-lg,\n.container-md,\n.container-sm {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n width: 100%;\n padding-right: calc(var(--bs-gutter-x) * 0.5);\n padding-left: calc(var(--bs-gutter-x) * 0.5);\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container-sm, .container {\n max-width: 540px;\n }\n}\n@media (min-width: 768px) {\n .container-md, .container-sm, .container {\n max-width: 720px;\n }\n}\n@media (min-width: 992px) {\n .container-lg, .container-md, .container-sm, .container {\n max-width: 960px;\n }\n}\n@media (min-width: 1200px) {\n .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1140px;\n }\n}\n@media (min-width: 1400px) {\n .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1320px;\n }\n}\n.row {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n margin-top: calc(-1 * var(--bs-gutter-y));\n margin-right: calc(-0.5 * var(--bs-gutter-x));\n margin-left: calc(-0.5 * var(--bs-gutter-x));\n}\n.row > * {\n flex-shrink: 0;\n width: 100%;\n max-width: 100%;\n padding-right: calc(var(--bs-gutter-x) * 0.5);\n padding-left: calc(var(--bs-gutter-x) * 0.5);\n margin-top: var(--bs-gutter-y);\n}\n\n.col {\n flex: 1 0 0%;\n}\n\n.row-cols-auto > * {\n flex: 0 0 auto;\n width: auto;\n}\n\n.row-cols-1 > * {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 auto;\n width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n}\n\n.col-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n}\n\n.col-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-3 {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.col-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.col-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n}\n\n.col-6 {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.col-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n}\n\n.col-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n}\n\n.col-9 {\n flex: 0 0 auto;\n width: 75%;\n}\n\n.col-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n}\n\n.col-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n}\n\n.col-12 {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.offset-1 {\n margin-left: 8.33333333%;\n}\n\n.offset-2 {\n margin-left: 16.66666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.33333333%;\n}\n\n.offset-5 {\n margin-left: 41.66666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.33333333%;\n}\n\n.offset-8 {\n margin-left: 66.66666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.33333333%;\n}\n\n.offset-11 {\n margin-left: 91.66666667%;\n}\n\n.g-0,\n.gx-0 {\n --bs-gutter-x: 0;\n}\n\n.g-0,\n.gy-0 {\n --bs-gutter-y: 0;\n}\n\n.g-1,\n.gx-1 {\n --bs-gutter-x: 0.25rem;\n}\n\n.g-1,\n.gy-1 {\n --bs-gutter-y: 0.25rem;\n}\n\n.g-2,\n.gx-2 {\n --bs-gutter-x: 0.5rem;\n}\n\n.g-2,\n.gy-2 {\n --bs-gutter-y: 0.5rem;\n}\n\n.g-3,\n.gx-3 {\n --bs-gutter-x: 1rem;\n}\n\n.g-3,\n.gy-3 {\n --bs-gutter-y: 1rem;\n}\n\n.g-4,\n.gx-4 {\n --bs-gutter-x: 1.5rem;\n}\n\n.g-4,\n.gy-4 {\n --bs-gutter-y: 1.5rem;\n}\n\n.g-5,\n.gx-5 {\n --bs-gutter-x: 3rem;\n}\n\n.g-5,\n.gy-5 {\n --bs-gutter-y: 3rem;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex: 1 0 0%;\n }\n .row-cols-sm-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-sm-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-sm-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-sm-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n .row-cols-sm-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-sm-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-sm-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-sm-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-sm-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-sm-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-sm-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-sm-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-sm-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-sm-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-sm-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-sm-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-sm-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-sm-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-sm-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.33333333%;\n }\n .offset-sm-2 {\n margin-left: 16.66666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.33333333%;\n }\n .offset-sm-5 {\n margin-left: 41.66666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.33333333%;\n }\n .offset-sm-8 {\n margin-left: 66.66666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.33333333%;\n }\n .offset-sm-11 {\n margin-left: 91.66666667%;\n }\n .g-sm-0,\n .gx-sm-0 {\n --bs-gutter-x: 0;\n }\n .g-sm-0,\n .gy-sm-0 {\n --bs-gutter-y: 0;\n }\n .g-sm-1,\n .gx-sm-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-sm-1,\n .gy-sm-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-sm-2,\n .gx-sm-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-sm-2,\n .gy-sm-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-sm-3,\n .gx-sm-3 {\n --bs-gutter-x: 1rem;\n }\n .g-sm-3,\n .gy-sm-3 {\n --bs-gutter-y: 1rem;\n }\n .g-sm-4,\n .gx-sm-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-sm-4,\n .gy-sm-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-sm-5,\n .gx-sm-5 {\n --bs-gutter-x: 3rem;\n }\n .g-sm-5,\n .gy-sm-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 768px) {\n .col-md {\n flex: 1 0 0%;\n }\n .row-cols-md-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-md-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-md-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-md-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n .row-cols-md-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-md-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-md-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-md-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-md-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-md-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-md-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-md-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-md-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-md-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-md-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-md-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-md-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-md-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-md-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.33333333%;\n }\n .offset-md-2 {\n margin-left: 16.66666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.33333333%;\n }\n .offset-md-5 {\n margin-left: 41.66666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.33333333%;\n }\n .offset-md-8 {\n margin-left: 66.66666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.33333333%;\n }\n .offset-md-11 {\n margin-left: 91.66666667%;\n }\n .g-md-0,\n .gx-md-0 {\n --bs-gutter-x: 0;\n }\n .g-md-0,\n .gy-md-0 {\n --bs-gutter-y: 0;\n }\n .g-md-1,\n .gx-md-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-md-1,\n .gy-md-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-md-2,\n .gx-md-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-md-2,\n .gy-md-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-md-3,\n .gx-md-3 {\n --bs-gutter-x: 1rem;\n }\n .g-md-3,\n .gy-md-3 {\n --bs-gutter-y: 1rem;\n }\n .g-md-4,\n .gx-md-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-md-4,\n .gy-md-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-md-5,\n .gx-md-5 {\n --bs-gutter-x: 3rem;\n }\n .g-md-5,\n .gy-md-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 992px) {\n .col-lg {\n flex: 1 0 0%;\n }\n .row-cols-lg-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-lg-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-lg-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-lg-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n .row-cols-lg-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-lg-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-lg-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-lg-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-lg-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-lg-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-lg-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-lg-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-lg-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-lg-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-lg-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-lg-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-lg-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-lg-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-lg-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.33333333%;\n }\n .offset-lg-2 {\n margin-left: 16.66666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.33333333%;\n }\n .offset-lg-5 {\n margin-left: 41.66666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.33333333%;\n }\n .offset-lg-8 {\n margin-left: 66.66666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.33333333%;\n }\n .offset-lg-11 {\n margin-left: 91.66666667%;\n }\n .g-lg-0,\n .gx-lg-0 {\n --bs-gutter-x: 0;\n }\n .g-lg-0,\n .gy-lg-0 {\n --bs-gutter-y: 0;\n }\n .g-lg-1,\n .gx-lg-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-lg-1,\n .gy-lg-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-lg-2,\n .gx-lg-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-lg-2,\n .gy-lg-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-lg-3,\n .gx-lg-3 {\n --bs-gutter-x: 1rem;\n }\n .g-lg-3,\n .gy-lg-3 {\n --bs-gutter-y: 1rem;\n }\n .g-lg-4,\n .gx-lg-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-lg-4,\n .gy-lg-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-lg-5,\n .gx-lg-5 {\n --bs-gutter-x: 3rem;\n }\n .g-lg-5,\n .gy-lg-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1200px) {\n .col-xl {\n flex: 1 0 0%;\n }\n .row-cols-xl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-xl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-xl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-xl-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n .row-cols-xl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-xl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-xl-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-xl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-xl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-xl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-xl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-xl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-xl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-xl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-xl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-xl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-xl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-xl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.33333333%;\n }\n .offset-xl-2 {\n margin-left: 16.66666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.33333333%;\n }\n .offset-xl-5 {\n margin-left: 41.66666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.33333333%;\n }\n .offset-xl-8 {\n margin-left: 66.66666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.33333333%;\n }\n .offset-xl-11 {\n margin-left: 91.66666667%;\n }\n .g-xl-0,\n .gx-xl-0 {\n --bs-gutter-x: 0;\n }\n .g-xl-0,\n .gy-xl-0 {\n --bs-gutter-y: 0;\n }\n .g-xl-1,\n .gx-xl-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-xl-1,\n .gy-xl-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-xl-2,\n .gx-xl-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-xl-2,\n .gy-xl-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-xl-3,\n .gx-xl-3 {\n --bs-gutter-x: 1rem;\n }\n .g-xl-3,\n .gy-xl-3 {\n --bs-gutter-y: 1rem;\n }\n .g-xl-4,\n .gx-xl-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-xl-4,\n .gy-xl-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-xl-5,\n .gx-xl-5 {\n --bs-gutter-x: 3rem;\n }\n .g-xl-5,\n .gy-xl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1400px) {\n .col-xxl {\n flex: 1 0 0%;\n }\n .row-cols-xxl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-xxl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-xxl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-xxl-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n .row-cols-xxl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-xxl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-xxl-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n .col-xxl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-xxl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-xxl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xxl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-xxl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-xxl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-xxl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-xxl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-xxl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-xxl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-xxl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-xxl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-xxl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-xxl-0 {\n margin-left: 0;\n }\n .offset-xxl-1 {\n margin-left: 8.33333333%;\n }\n .offset-xxl-2 {\n margin-left: 16.66666667%;\n }\n .offset-xxl-3 {\n margin-left: 25%;\n }\n .offset-xxl-4 {\n margin-left: 33.33333333%;\n }\n .offset-xxl-5 {\n margin-left: 41.66666667%;\n }\n .offset-xxl-6 {\n margin-left: 50%;\n }\n .offset-xxl-7 {\n margin-left: 58.33333333%;\n }\n .offset-xxl-8 {\n margin-left: 66.66666667%;\n }\n .offset-xxl-9 {\n margin-left: 75%;\n }\n .offset-xxl-10 {\n margin-left: 83.33333333%;\n }\n .offset-xxl-11 {\n margin-left: 91.66666667%;\n }\n .g-xxl-0,\n .gx-xxl-0 {\n --bs-gutter-x: 0;\n }\n .g-xxl-0,\n .gy-xxl-0 {\n --bs-gutter-y: 0;\n }\n .g-xxl-1,\n .gx-xxl-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-xxl-1,\n .gy-xxl-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-xxl-2,\n .gx-xxl-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-xxl-2,\n .gy-xxl-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-xxl-3,\n .gx-xxl-3 {\n --bs-gutter-x: 1rem;\n }\n .g-xxl-3,\n .gy-xxl-3 {\n --bs-gutter-y: 1rem;\n }\n .g-xxl-4,\n .gx-xxl-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-xxl-4,\n .gy-xxl-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-xxl-5,\n .gx-xxl-5 {\n --bs-gutter-x: 3rem;\n }\n .g-xxl-5,\n .gy-xxl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n.table {\n --bs-table-color: var(--bs-body-color);\n --bs-table-bg: transparent;\n --bs-table-border-color: var(--bs-border-color);\n --bs-table-accent-bg: transparent;\n --bs-table-striped-color: var(--bs-body-color);\n --bs-table-striped-bg: rgba(0, 0, 0, 0.05);\n --bs-table-active-color: var(--bs-body-color);\n --bs-table-active-bg: rgba(0, 0, 0, 0.1);\n --bs-table-hover-color: var(--bs-body-color);\n --bs-table-hover-bg: rgba(0, 0, 0, 0.075);\n width: 100%;\n margin-bottom: 1rem;\n color: var(--bs-table-color);\n vertical-align: top;\n border-color: var(--bs-table-border-color);\n}\n.table > :not(caption) > * > * {\n padding: 0.5rem 0.5rem;\n background-color: var(--bs-table-bg);\n border-bottom-width: var(--bs-border-width);\n box-shadow: inset 0 0 0 9999px var(--bs-table-accent-bg);\n}\n.table > tbody {\n vertical-align: inherit;\n}\n.table > thead {\n vertical-align: bottom;\n}\n\n.table-group-divider {\n border-top: calc(var(--bs-border-width) * 2) solid currentcolor;\n}\n\n.caption-top {\n caption-side: top;\n}\n\n.table-sm > :not(caption) > * > * {\n padding: 0.25rem 0.25rem;\n}\n\n.table-bordered > :not(caption) > * {\n border-width: var(--bs-border-width) 0;\n}\n.table-bordered > :not(caption) > * > * {\n border-width: 0 var(--bs-border-width);\n}\n\n.table-borderless > :not(caption) > * > * {\n border-bottom-width: 0;\n}\n.table-borderless > :not(:first-child) {\n border-top-width: 0;\n}\n\n.table-striped > tbody > tr:nth-of-type(odd) > * {\n --bs-table-accent-bg: var(--bs-table-striped-bg);\n color: var(--bs-table-striped-color);\n}\n\n.table-striped-columns > :not(caption) > tr > :nth-child(even) {\n --bs-table-accent-bg: var(--bs-table-striped-bg);\n color: var(--bs-table-striped-color);\n}\n\n.table-active {\n --bs-table-accent-bg: var(--bs-table-active-bg);\n color: var(--bs-table-active-color);\n}\n\n.table-hover > tbody > tr:hover > * {\n --bs-table-accent-bg: var(--bs-table-hover-bg);\n color: var(--bs-table-hover-color);\n}\n\n.table-primary {\n --bs-table-color: #000;\n --bs-table-bg: #cfe2ff;\n --bs-table-border-color: #bacbe6;\n --bs-table-striped-bg: #c5d7f2;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #bacbe6;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #bfd1ec;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-secondary {\n --bs-table-color: #000;\n --bs-table-bg: #e2e3e5;\n --bs-table-border-color: #cbccce;\n --bs-table-striped-bg: #d7d8da;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #cbccce;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #d1d2d4;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-success {\n --bs-table-color: #000;\n --bs-table-bg: #d1e7dd;\n --bs-table-border-color: #bcd0c7;\n --bs-table-striped-bg: #c7dbd2;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #bcd0c7;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #c1d6cc;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-info {\n --bs-table-color: #000;\n --bs-table-bg: #cff4fc;\n --bs-table-border-color: #badce3;\n --bs-table-striped-bg: #c5e8ef;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #badce3;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #bfe2e9;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-warning {\n --bs-table-color: #000;\n --bs-table-bg: #fff3cd;\n --bs-table-border-color: #e6dbb9;\n --bs-table-striped-bg: #f2e7c3;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #e6dbb9;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #ece1be;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-danger {\n --bs-table-color: #000;\n --bs-table-bg: #f8d7da;\n --bs-table-border-color: #dfc2c4;\n --bs-table-striped-bg: #eccccf;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #dfc2c4;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #e5c7ca;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-light {\n --bs-table-color: #000;\n --bs-table-bg: #f8f9fa;\n --bs-table-border-color: #dfe0e1;\n --bs-table-striped-bg: #ecedee;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #dfe0e1;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #e5e6e7;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-dark {\n --bs-table-color: #fff;\n --bs-table-bg: #212529;\n --bs-table-border-color: #373b3e;\n --bs-table-striped-bg: #2c3034;\n --bs-table-striped-color: #fff;\n --bs-table-active-bg: #373b3e;\n --bs-table-active-color: #fff;\n --bs-table-hover-bg: #323539;\n --bs-table-hover-color: #fff;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-responsive {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n}\n\n@media (max-width: 575.98px) {\n .table-responsive-sm {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 767.98px) {\n .table-responsive-md {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 991.98px) {\n .table-responsive-lg {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 1199.98px) {\n .table-responsive-xl {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 1399.98px) {\n .table-responsive-xxl {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n.form-label {\n margin-bottom: 0.5rem;\n}\n\n.col-form-label {\n padding-top: calc(0.375rem + var(--bs-border-width));\n padding-bottom: calc(0.375rem + var(--bs-border-width));\n margin-bottom: 0;\n font-size: inherit;\n line-height: 1.5;\n}\n\n.col-form-label-lg {\n padding-top: calc(0.5rem + var(--bs-border-width));\n padding-bottom: calc(0.5rem + var(--bs-border-width));\n font-size: 1.25rem;\n}\n\n.col-form-label-sm {\n padding-top: calc(0.25rem + var(--bs-border-width));\n padding-bottom: calc(0.25rem + var(--bs-border-width));\n font-size: 0.875rem;\n}\n\n.form-text {\n margin-top: 0.25rem;\n font-size: 0.875em;\n color: var(--bs-secondary-color);\n}\n\n.form-control {\n display: block;\n width: 100%;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: var(--bs-body-color);\n background-color: var(--bs-form-control-bg);\n background-clip: padding-box;\n border: var(--bs-border-width) solid var(--bs-border-color);\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n border-radius: 0.375rem;\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-control {\n transition: none;\n }\n}\n.form-control[type=file] {\n overflow: hidden;\n}\n.form-control[type=file]:not(:disabled):not([readonly]) {\n cursor: pointer;\n}\n.form-control:focus {\n color: var(--bs-body-color);\n background-color: var(--bs-form-control-bg);\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-control::-webkit-date-and-time-value {\n height: 1.5em;\n}\n.form-control::-webkit-datetime-edit {\n display: block;\n padding: 0;\n}\n.form-control::-moz-placeholder {\n color: var(--bs-secondary-color);\n opacity: 1;\n}\n.form-control::placeholder {\n color: var(--bs-secondary-color);\n opacity: 1;\n}\n.form-control:disabled {\n background-color: var(--bs-form-control-disabled-bg);\n opacity: 1;\n}\n.form-control::-webkit-file-upload-button {\n padding: 0.375rem 0.75rem;\n margin: -0.375rem -0.75rem;\n -webkit-margin-end: 0.75rem;\n margin-inline-end: 0.75rem;\n color: var(--bs-body-color);\n background-color: var(--bs-tertiary-bg);\n pointer-events: none;\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n border-inline-end-width: var(--bs-border-width);\n border-radius: 0;\n -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n.form-control::file-selector-button {\n padding: 0.375rem 0.75rem;\n margin: -0.375rem -0.75rem;\n -webkit-margin-end: 0.75rem;\n margin-inline-end: 0.75rem;\n color: var(--bs-body-color);\n background-color: var(--bs-tertiary-bg);\n pointer-events: none;\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n border-inline-end-width: var(--bs-border-width);\n border-radius: 0;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-control::-webkit-file-upload-button {\n -webkit-transition: none;\n transition: none;\n }\n .form-control::file-selector-button {\n transition: none;\n }\n}\n.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button {\n background-color: var(--bs-secondary-bg);\n}\n.form-control:hover:not(:disabled):not([readonly])::file-selector-button {\n background-color: var(--bs-secondary-bg);\n}\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding: 0.375rem 0;\n margin-bottom: 0;\n line-height: 1.5;\n color: var(--bs-body-color);\n background-color: transparent;\n border: solid transparent;\n border-width: var(--bs-border-width) 0;\n}\n.form-control-plaintext:focus {\n outline: 0;\n}\n.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {\n padding-right: 0;\n padding-left: 0;\n}\n\n.form-control-sm {\n min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n border-radius: 0.25rem;\n}\n.form-control-sm::-webkit-file-upload-button {\n padding: 0.25rem 0.5rem;\n margin: -0.25rem -0.5rem;\n -webkit-margin-end: 0.5rem;\n margin-inline-end: 0.5rem;\n}\n.form-control-sm::file-selector-button {\n padding: 0.25rem 0.5rem;\n margin: -0.25rem -0.5rem;\n -webkit-margin-end: 0.5rem;\n margin-inline-end: 0.5rem;\n}\n\n.form-control-lg {\n min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n border-radius: 0.5rem;\n}\n.form-control-lg::-webkit-file-upload-button {\n padding: 0.5rem 1rem;\n margin: -0.5rem -1rem;\n -webkit-margin-end: 1rem;\n margin-inline-end: 1rem;\n}\n.form-control-lg::file-selector-button {\n padding: 0.5rem 1rem;\n margin: -0.5rem -1rem;\n -webkit-margin-end: 1rem;\n margin-inline-end: 1rem;\n}\n\ntextarea.form-control {\n min-height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2));\n}\ntextarea.form-control-sm {\n min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));\n}\ntextarea.form-control-lg {\n min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));\n}\n\n.form-control-color {\n width: 3rem;\n height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2));\n padding: 0.375rem;\n}\n.form-control-color:not(:disabled):not([readonly]) {\n cursor: pointer;\n}\n.form-control-color::-moz-color-swatch {\n border: 0 !important;\n border-radius: 0.375rem;\n}\n.form-control-color::-webkit-color-swatch {\n border-radius: 0.375rem;\n}\n.form-control-color.form-control-sm {\n height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));\n}\n.form-control-color.form-control-lg {\n height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));\n}\n\n.form-select {\n --bs-form-select-bg-img: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e\");\n display: block;\n width: 100%;\n padding: 0.375rem 2.25rem 0.375rem 0.75rem;\n -moz-padding-start: calc(0.75rem - 3px);\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: var(--bs-body-color);\n background-color: var(--bs-form-control-bg);\n background-image: var(--bs-form-select-bg-img), var(--bs-form-select-bg-icon, none);\n background-repeat: no-repeat;\n background-position: right 0.75rem center;\n background-size: 16px 12px;\n border: var(--bs-border-width) solid var(--bs-border-color);\n border-radius: 0.375rem;\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-select {\n transition: none;\n }\n}\n.form-select:focus {\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-select[multiple], .form-select[size]:not([size=\"1\"]) {\n padding-right: 0.75rem;\n background-image: none;\n}\n.form-select:disabled {\n background-color: var(--bs-form-control-disabled-bg);\n}\n.form-select:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 var(--bs-body-color);\n}\n\n.form-select-sm {\n padding-top: 0.25rem;\n padding-bottom: 0.25rem;\n padding-left: 0.5rem;\n font-size: 0.875rem;\n border-radius: 0.25rem;\n}\n\n.form-select-lg {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n padding-left: 1rem;\n font-size: 1.25rem;\n border-radius: 0.5rem;\n}\n\n[data-bs-theme=dark] .form-select {\n --bs-form-select-bg-img: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23adb5bd' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e\");\n}\n\n.form-check {\n display: block;\n min-height: 1.5rem;\n padding-left: 1.5em;\n margin-bottom: 0.125rem;\n}\n.form-check .form-check-input {\n float: left;\n margin-left: -1.5em;\n}\n\n.form-check-reverse {\n padding-right: 1.5em;\n padding-left: 0;\n text-align: right;\n}\n.form-check-reverse .form-check-input {\n float: right;\n margin-right: -1.5em;\n margin-left: 0;\n}\n\n.form-check-input {\n --bs-form-check-bg: var(--bs-form-control-bg);\n width: 1em;\n height: 1em;\n margin-top: 0.25em;\n vertical-align: top;\n background-color: var(--bs-form-check-bg);\n background-image: var(--bs-form-check-bg-image);\n background-repeat: no-repeat;\n background-position: center;\n background-size: contain;\n border: var(--bs-border-width) solid var(--bs-border-color);\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n -webkit-print-color-adjust: exact;\n color-adjust: exact;\n print-color-adjust: exact;\n}\n.form-check-input[type=checkbox] {\n border-radius: 0.25em;\n}\n.form-check-input[type=radio] {\n border-radius: 50%;\n}\n.form-check-input:active {\n filter: brightness(90%);\n}\n.form-check-input:focus {\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-check-input:checked {\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.form-check-input:checked[type=checkbox] {\n --bs-form-check-bg-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e\");\n}\n.form-check-input:checked[type=radio] {\n --bs-form-check-bg-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e\");\n}\n.form-check-input[type=checkbox]:indeterminate {\n background-color: #0d6efd;\n border-color: #0d6efd;\n --bs-form-check-bg-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e\");\n}\n.form-check-input:disabled {\n pointer-events: none;\n filter: none;\n opacity: 0.5;\n}\n.form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label {\n cursor: default;\n opacity: 0.5;\n}\n\n.form-switch {\n padding-left: 2.5em;\n}\n.form-switch .form-check-input {\n --bs-form-switch-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e\");\n width: 2em;\n margin-left: -2.5em;\n background-image: var(--bs-form-switch-bg);\n background-position: left center;\n border-radius: 2em;\n transition: background-position 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-switch .form-check-input {\n transition: none;\n }\n}\n.form-switch .form-check-input:focus {\n --bs-form-switch-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e\");\n}\n.form-switch .form-check-input:checked {\n background-position: right center;\n --bs-form-switch-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e\");\n}\n.form-switch.form-check-reverse {\n padding-right: 2.5em;\n padding-left: 0;\n}\n.form-switch.form-check-reverse .form-check-input {\n margin-right: -2.5em;\n margin-left: 0;\n}\n\n.form-check-inline {\n display: inline-block;\n margin-right: 1rem;\n}\n\n.btn-check {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.btn-check[disabled] + .btn, .btn-check:disabled + .btn {\n pointer-events: none;\n filter: none;\n opacity: 0.65;\n}\n\n[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus) {\n --bs-form-switch-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e\");\n}\n\n.form-range {\n width: 100%;\n height: 1.5rem;\n padding: 0;\n background-color: transparent;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n.form-range:focus {\n outline: 0;\n}\n.form-range:focus::-webkit-slider-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-range:focus::-moz-range-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-range::-moz-focus-outer {\n border: 0;\n}\n.form-range::-webkit-slider-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: -0.25rem;\n background-color: #0d6efd;\n border: 0;\n border-radius: 1rem;\n -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n -webkit-appearance: none;\n appearance: none;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-range::-webkit-slider-thumb {\n -webkit-transition: none;\n transition: none;\n }\n}\n.form-range::-webkit-slider-thumb:active {\n background-color: #b6d4fe;\n}\n.form-range::-webkit-slider-runnable-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: var(--bs-tertiary-bg);\n border-color: transparent;\n border-radius: 1rem;\n}\n.form-range::-moz-range-thumb {\n width: 1rem;\n height: 1rem;\n background-color: #0d6efd;\n border: 0;\n border-radius: 1rem;\n -moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n -moz-appearance: none;\n appearance: none;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-range::-moz-range-thumb {\n -moz-transition: none;\n transition: none;\n }\n}\n.form-range::-moz-range-thumb:active {\n background-color: #b6d4fe;\n}\n.form-range::-moz-range-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: var(--bs-tertiary-bg);\n border-color: transparent;\n border-radius: 1rem;\n}\n.form-range:disabled {\n pointer-events: none;\n}\n.form-range:disabled::-webkit-slider-thumb {\n background-color: var(--bs-secondary-color);\n}\n.form-range:disabled::-moz-range-thumb {\n background-color: var(--bs-secondary-color);\n}\n\n.form-floating {\n position: relative;\n}\n.form-floating::before:not(.form-control:disabled) {\n position: absolute;\n top: var(--bs-border-width);\n left: var(--bs-border-width);\n width: calc(100% - (calc(calc(0.375em + 0.1875rem) + calc(0.75em + 0.375rem))));\n height: 1.875em;\n content: \"\";\n background-color: var(--bs-form-control-bg);\n border-radius: 0.375rem;\n}\n.form-floating > .form-control,\n.form-floating > .form-control-plaintext,\n.form-floating > .form-select {\n height: calc(3.5rem + calc(var(--bs-border-width) * 2));\n line-height: 1.25;\n}\n.form-floating > label {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n padding: 1rem 0.75rem;\n overflow: hidden;\n text-align: start;\n text-overflow: ellipsis;\n white-space: nowrap;\n pointer-events: none;\n border: var(--bs-border-width) solid transparent;\n transform-origin: 0 0;\n transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-floating > label {\n transition: none;\n }\n}\n.form-floating > .form-control,\n.form-floating > .form-control-plaintext {\n padding: 1rem 0.75rem;\n}\n.form-floating > .form-control::-moz-placeholder, .form-floating > .form-control-plaintext::-moz-placeholder {\n color: transparent;\n}\n.form-floating > .form-control::placeholder,\n.form-floating > .form-control-plaintext::placeholder {\n color: transparent;\n}\n.form-floating > .form-control:not(:-moz-placeholder-shown), .form-floating > .form-control-plaintext:not(:-moz-placeholder-shown) {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown),\n.form-floating > .form-control-plaintext:focus,\n.form-floating > .form-control-plaintext:not(:placeholder-shown) {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-control:-webkit-autofill,\n.form-floating > .form-control-plaintext:-webkit-autofill {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-select {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label {\n opacity: 0.65;\n transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);\n}\n.form-floating > .form-control:focus ~ label,\n.form-floating > .form-control:not(:placeholder-shown) ~ label,\n.form-floating > .form-control-plaintext ~ label,\n.form-floating > .form-select ~ label {\n opacity: 0.65;\n transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);\n}\n.form-floating > .form-control:-webkit-autofill ~ label {\n opacity: 0.65;\n transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);\n}\n.form-floating > .form-control-plaintext ~ label {\n border-width: var(--bs-border-width) 0;\n}\n.form-floating > .form-control:disabled ~ label {\n color: #6c757d;\n}\n\n.input-group {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: stretch;\n width: 100%;\n}\n.input-group > .form-control,\n.input-group > .form-select,\n.input-group > .form-floating {\n position: relative;\n flex: 1 1 auto;\n width: 1%;\n min-width: 0;\n}\n.input-group > .form-control:focus,\n.input-group > .form-select:focus,\n.input-group > .form-floating:focus-within {\n z-index: 5;\n}\n.input-group .btn {\n position: relative;\n z-index: 2;\n}\n.input-group .btn:focus {\n z-index: 5;\n}\n\n.input-group-text {\n display: flex;\n align-items: center;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: var(--bs-body-color);\n text-align: center;\n white-space: nowrap;\n background-color: var(--bs-tertiary-bg);\n border: var(--bs-border-width) solid var(--bs-border-color);\n border-radius: 0.375rem;\n}\n\n.input-group-lg > .form-control,\n.input-group-lg > .form-select,\n.input-group-lg > .input-group-text,\n.input-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n border-radius: 0.5rem;\n}\n\n.input-group-sm > .form-control,\n.input-group-sm > .form-select,\n.input-group-sm > .input-group-text,\n.input-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n border-radius: 0.25rem;\n}\n\n.input-group-lg > .form-select,\n.input-group-sm > .form-select {\n padding-right: 3rem;\n}\n\n.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),\n.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3),\n.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-control,\n.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-select {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),\n.input-group.has-validation > .dropdown-toggle:nth-last-child(n+4),\n.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-control,\n.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-select {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {\n margin-left: calc(var(--bs-border-width) * -1);\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.input-group > .form-floating:not(:first-child) > .form-control,\n.input-group > .form-floating:not(:first-child) > .form-select {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.valid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 0.875em;\n color: var(--bs-success-text);\n}\n\n.valid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: 0.1rem;\n font-size: 0.875rem;\n color: #fff;\n background-color: var(--bs-success);\n border-radius: var(--bs-border-radius);\n}\n\n.was-validated :valid ~ .valid-feedback,\n.was-validated :valid ~ .valid-tooltip,\n.is-valid ~ .valid-feedback,\n.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:valid, .form-control.is-valid {\n border-color: var(--bs-success);\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-control:valid:focus, .form-control.is-valid:focus {\n border-color: var(--bs-success);\n box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);\n}\n\n.was-validated textarea.form-control:valid, textarea.form-control.is-valid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .form-select:valid, .form-select.is-valid {\n border-color: var(--bs-success);\n}\n.was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size=\"1\"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size=\"1\"] {\n --bs-form-select-bg-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");\n padding-right: 4.125rem;\n background-position: right 0.75rem center, center right 2.25rem;\n background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-select:valid:focus, .form-select.is-valid:focus {\n border-color: var(--bs-success);\n box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);\n}\n\n.was-validated .form-control-color:valid, .form-control-color.is-valid {\n width: calc(3rem + calc(1.5em + 0.75rem));\n}\n\n.was-validated .form-check-input:valid, .form-check-input.is-valid {\n border-color: var(--bs-success);\n}\n.was-validated .form-check-input:valid:checked, .form-check-input.is-valid:checked {\n background-color: var(--bs-success-text);\n}\n.was-validated .form-check-input:valid:focus, .form-check-input.is-valid:focus {\n box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);\n}\n.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {\n color: var(--bs-success-text);\n}\n\n.form-check-inline .form-check-input ~ .valid-feedback {\n margin-left: 0.5em;\n}\n\n.was-validated .input-group > .form-control:not(:focus):valid, .input-group > .form-control:not(:focus).is-valid,\n.was-validated .input-group > .form-select:not(:focus):valid,\n.input-group > .form-select:not(:focus).is-valid,\n.was-validated .input-group > .form-floating:not(:focus-within):valid,\n.input-group > .form-floating:not(:focus-within).is-valid {\n z-index: 3;\n}\n\n.invalid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 0.875em;\n color: var(--bs-danger-text);\n}\n\n.invalid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: 0.1rem;\n font-size: 0.875rem;\n color: #fff;\n background-color: var(--bs-danger);\n border-radius: var(--bs-border-radius);\n}\n\n.was-validated :invalid ~ .invalid-feedback,\n.was-validated :invalid ~ .invalid-tooltip,\n.is-invalid ~ .invalid-feedback,\n.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:invalid, .form-control.is-invalid {\n border-color: var(--bs-danger);\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus {\n border-color: var(--bs-danger);\n box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25);\n}\n\n.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .form-select:invalid, .form-select.is-invalid {\n border-color: var(--bs-danger);\n}\n.was-validated .form-select:invalid:not([multiple]):not([size]), .was-validated .form-select:invalid:not([multiple])[size=\"1\"], .form-select.is-invalid:not([multiple]):not([size]), .form-select.is-invalid:not([multiple])[size=\"1\"] {\n --bs-form-select-bg-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\");\n padding-right: 4.125rem;\n background-position: right 0.75rem center, center right 2.25rem;\n background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-select:invalid:focus, .form-select.is-invalid:focus {\n border-color: var(--bs-danger);\n box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25);\n}\n\n.was-validated .form-control-color:invalid, .form-control-color.is-invalid {\n width: calc(3rem + calc(1.5em + 0.75rem));\n}\n\n.was-validated .form-check-input:invalid, .form-check-input.is-invalid {\n border-color: var(--bs-danger);\n}\n.was-validated .form-check-input:invalid:checked, .form-check-input.is-invalid:checked {\n background-color: var(--bs-danger-text);\n}\n.was-validated .form-check-input:invalid:focus, .form-check-input.is-invalid:focus {\n box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25);\n}\n.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {\n color: var(--bs-danger-text);\n}\n\n.form-check-inline .form-check-input ~ .invalid-feedback {\n margin-left: 0.5em;\n}\n\n.was-validated .input-group > .form-control:not(:focus):invalid, .input-group > .form-control:not(:focus).is-invalid,\n.was-validated .input-group > .form-select:not(:focus):invalid,\n.input-group > .form-select:not(:focus).is-invalid,\n.was-validated .input-group > .form-floating:not(:focus-within):invalid,\n.input-group > .form-floating:not(:focus-within).is-invalid {\n z-index: 4;\n}\n\n.btn {\n --bs-btn-padding-x: 0.75rem;\n --bs-btn-padding-y: 0.375rem;\n --bs-btn-font-family: ;\n --bs-btn-font-size: 1rem;\n --bs-btn-font-weight: 400;\n --bs-btn-line-height: 1.5;\n --bs-btn-color: #212529;\n --bs-btn-bg: transparent;\n --bs-btn-border-width: var(--bs-border-width);\n --bs-btn-border-color: transparent;\n --bs-btn-border-radius: 0.375rem;\n --bs-btn-hover-border-color: transparent;\n --bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n --bs-btn-disabled-opacity: 0.65;\n --bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);\n display: inline-block;\n padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x);\n font-family: var(--bs-btn-font-family);\n font-size: var(--bs-btn-font-size);\n font-weight: var(--bs-btn-font-weight);\n line-height: var(--bs-btn-line-height);\n color: var(--bs-btn-color);\n text-align: center;\n text-decoration: none;\n vertical-align: middle;\n cursor: pointer;\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n border: var(--bs-btn-border-width) solid var(--bs-btn-border-color);\n border-radius: var(--bs-btn-border-radius);\n background-color: var(--bs-btn-bg);\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .btn {\n transition: none;\n }\n}\n.btn:hover {\n color: var(--bs-btn-hover-color);\n background-color: var(--bs-btn-hover-bg);\n border-color: var(--bs-btn-hover-border-color);\n}\n.btn-check + .btn:hover {\n color: var(--bs-btn-color);\n background-color: var(--bs-btn-bg);\n border-color: var(--bs-btn-border-color);\n}\n.btn:focus-visible {\n color: var(--bs-btn-hover-color);\n background-color: var(--bs-btn-hover-bg);\n border-color: var(--bs-btn-hover-border-color);\n outline: 0;\n box-shadow: var(--bs-btn-focus-box-shadow);\n}\n.btn-check:focus-visible + .btn {\n border-color: var(--bs-btn-hover-border-color);\n outline: 0;\n box-shadow: var(--bs-btn-focus-box-shadow);\n}\n.btn-check:checked + .btn, :not(.btn-check) + .btn:active, .btn:first-child:active, .btn.active, .btn.show {\n color: var(--bs-btn-active-color);\n background-color: var(--bs-btn-active-bg);\n border-color: var(--bs-btn-active-border-color);\n}\n.btn-check:checked + .btn:focus-visible, :not(.btn-check) + .btn:active:focus-visible, .btn:first-child:active:focus-visible, .btn.active:focus-visible, .btn.show:focus-visible {\n box-shadow: var(--bs-btn-focus-box-shadow);\n}\n.btn:disabled, .btn.disabled, fieldset:disabled .btn {\n color: var(--bs-btn-disabled-color);\n pointer-events: none;\n background-color: var(--bs-btn-disabled-bg);\n border-color: var(--bs-btn-disabled-border-color);\n opacity: var(--bs-btn-disabled-opacity);\n}\n\n.btn-primary {\n --bs-btn-color: #fff;\n --bs-btn-bg: #0d6efd;\n --bs-btn-border-color: #0d6efd;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #0b5ed7;\n --bs-btn-hover-border-color: #0a58ca;\n --bs-btn-focus-shadow-rgb: 49, 132, 253;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #0a58ca;\n --bs-btn-active-border-color: #0a53be;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #fff;\n --bs-btn-disabled-bg: #0d6efd;\n --bs-btn-disabled-border-color: #0d6efd;\n}\n\n.btn-secondary {\n --bs-btn-color: #fff;\n --bs-btn-bg: #6c757d;\n --bs-btn-border-color: #6c757d;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #5c636a;\n --bs-btn-hover-border-color: #565e64;\n --bs-btn-focus-shadow-rgb: 130, 138, 145;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #565e64;\n --bs-btn-active-border-color: #51585e;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #fff;\n --bs-btn-disabled-bg: #6c757d;\n --bs-btn-disabled-border-color: #6c757d;\n}\n\n.btn-success {\n --bs-btn-color: #fff;\n --bs-btn-bg: #198754;\n --bs-btn-border-color: #198754;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #157347;\n --bs-btn-hover-border-color: #146c43;\n --bs-btn-focus-shadow-rgb: 60, 153, 110;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #146c43;\n --bs-btn-active-border-color: #13653f;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #fff;\n --bs-btn-disabled-bg: #198754;\n --bs-btn-disabled-border-color: #198754;\n}\n\n.btn-info {\n --bs-btn-color: #000;\n --bs-btn-bg: #0dcaf0;\n --bs-btn-border-color: #0dcaf0;\n --bs-btn-hover-color: #000;\n --bs-btn-hover-bg: #31d2f2;\n --bs-btn-hover-border-color: #25cff2;\n --bs-btn-focus-shadow-rgb: 11, 172, 204;\n --bs-btn-active-color: #000;\n --bs-btn-active-bg: #3dd5f3;\n --bs-btn-active-border-color: #25cff2;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #000;\n --bs-btn-disabled-bg: #0dcaf0;\n --bs-btn-disabled-border-color: #0dcaf0;\n}\n\n.btn-warning {\n --bs-btn-color: #000;\n --bs-btn-bg: #ffc107;\n --bs-btn-border-color: #ffc107;\n --bs-btn-hover-color: #000;\n --bs-btn-hover-bg: #ffca2c;\n --bs-btn-hover-border-color: #ffc720;\n --bs-btn-focus-shadow-rgb: 217, 164, 6;\n --bs-btn-active-color: #000;\n --bs-btn-active-bg: #ffcd39;\n --bs-btn-active-border-color: #ffc720;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #000;\n --bs-btn-disabled-bg: #ffc107;\n --bs-btn-disabled-border-color: #ffc107;\n}\n\n.btn-danger {\n --bs-btn-color: #fff;\n --bs-btn-bg: #dc3545;\n --bs-btn-border-color: #dc3545;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #bb2d3b;\n --bs-btn-hover-border-color: #b02a37;\n --bs-btn-focus-shadow-rgb: 225, 83, 97;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #b02a37;\n --bs-btn-active-border-color: #a52834;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #fff;\n --bs-btn-disabled-bg: #dc3545;\n --bs-btn-disabled-border-color: #dc3545;\n}\n\n.btn-light {\n --bs-btn-color: #000;\n --bs-btn-bg: #f8f9fa;\n --bs-btn-border-color: #f8f9fa;\n --bs-btn-hover-color: #000;\n --bs-btn-hover-bg: #d3d4d5;\n --bs-btn-hover-border-color: #c6c7c8;\n --bs-btn-focus-shadow-rgb: 211, 212, 213;\n --bs-btn-active-color: #000;\n --bs-btn-active-bg: #c6c7c8;\n --bs-btn-active-border-color: #babbbc;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #000;\n --bs-btn-disabled-bg: #f8f9fa;\n --bs-btn-disabled-border-color: #f8f9fa;\n}\n\n.btn-dark {\n --bs-btn-color: #fff;\n --bs-btn-bg: #212529;\n --bs-btn-border-color: #212529;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #424649;\n --bs-btn-hover-border-color: #373b3e;\n --bs-btn-focus-shadow-rgb: 66, 70, 73;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #4d5154;\n --bs-btn-active-border-color: #373b3e;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #fff;\n --bs-btn-disabled-bg: #212529;\n --bs-btn-disabled-border-color: #212529;\n}\n\n.btn-outline-primary {\n --bs-btn-color: #0d6efd;\n --bs-btn-border-color: #0d6efd;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #0d6efd;\n --bs-btn-hover-border-color: #0d6efd;\n --bs-btn-focus-shadow-rgb: 13, 110, 253;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #0d6efd;\n --bs-btn-active-border-color: #0d6efd;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #0d6efd;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #0d6efd;\n --bs-gradient: none;\n}\n\n.btn-outline-secondary {\n --bs-btn-color: #6c757d;\n --bs-btn-border-color: #6c757d;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #6c757d;\n --bs-btn-hover-border-color: #6c757d;\n --bs-btn-focus-shadow-rgb: 108, 117, 125;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #6c757d;\n --bs-btn-active-border-color: #6c757d;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #6c757d;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #6c757d;\n --bs-gradient: none;\n}\n\n.btn-outline-success {\n --bs-btn-color: #198754;\n --bs-btn-border-color: #198754;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #198754;\n --bs-btn-hover-border-color: #198754;\n --bs-btn-focus-shadow-rgb: 25, 135, 84;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #198754;\n --bs-btn-active-border-color: #198754;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #198754;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #198754;\n --bs-gradient: none;\n}\n\n.btn-outline-info {\n --bs-btn-color: #0dcaf0;\n --bs-btn-border-color: #0dcaf0;\n --bs-btn-hover-color: #000;\n --bs-btn-hover-bg: #0dcaf0;\n --bs-btn-hover-border-color: #0dcaf0;\n --bs-btn-focus-shadow-rgb: 13, 202, 240;\n --bs-btn-active-color: #000;\n --bs-btn-active-bg: #0dcaf0;\n --bs-btn-active-border-color: #0dcaf0;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #0dcaf0;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #0dcaf0;\n --bs-gradient: none;\n}\n\n.btn-outline-warning {\n --bs-btn-color: #ffc107;\n --bs-btn-border-color: #ffc107;\n --bs-btn-hover-color: #000;\n --bs-btn-hover-bg: #ffc107;\n --bs-btn-hover-border-color: #ffc107;\n --bs-btn-focus-shadow-rgb: 255, 193, 7;\n --bs-btn-active-color: #000;\n --bs-btn-active-bg: #ffc107;\n --bs-btn-active-border-color: #ffc107;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #ffc107;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #ffc107;\n --bs-gradient: none;\n}\n\n.btn-outline-danger {\n --bs-btn-color: #dc3545;\n --bs-btn-border-color: #dc3545;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #dc3545;\n --bs-btn-hover-border-color: #dc3545;\n --bs-btn-focus-shadow-rgb: 220, 53, 69;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #dc3545;\n --bs-btn-active-border-color: #dc3545;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #dc3545;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #dc3545;\n --bs-gradient: none;\n}\n\n.btn-outline-light {\n --bs-btn-color: #f8f9fa;\n --bs-btn-border-color: #f8f9fa;\n --bs-btn-hover-color: #000;\n --bs-btn-hover-bg: #f8f9fa;\n --bs-btn-hover-border-color: #f8f9fa;\n --bs-btn-focus-shadow-rgb: 248, 249, 250;\n --bs-btn-active-color: #000;\n --bs-btn-active-bg: #f8f9fa;\n --bs-btn-active-border-color: #f8f9fa;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #f8f9fa;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #f8f9fa;\n --bs-gradient: none;\n}\n\n.btn-outline-dark {\n --bs-btn-color: #212529;\n --bs-btn-border-color: #212529;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #212529;\n --bs-btn-hover-border-color: #212529;\n --bs-btn-focus-shadow-rgb: 33, 37, 41;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #212529;\n --bs-btn-active-border-color: #212529;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #212529;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #212529;\n --bs-gradient: none;\n}\n\n.btn-link {\n --bs-btn-font-weight: 400;\n --bs-btn-color: var(--bs-link-color);\n --bs-btn-bg: transparent;\n --bs-btn-border-color: transparent;\n --bs-btn-hover-color: var(--bs-link-hover-color);\n --bs-btn-hover-border-color: transparent;\n --bs-btn-active-color: var(--bs-link-hover-color);\n --bs-btn-active-border-color: transparent;\n --bs-btn-disabled-color: #6c757d;\n --bs-btn-disabled-border-color: transparent;\n --bs-btn-box-shadow: none;\n --bs-btn-focus-shadow-rgb: 49, 132, 253;\n text-decoration: underline;\n}\n.btn-link:focus-visible {\n color: var(--bs-btn-color);\n}\n.btn-link:hover {\n color: var(--bs-btn-hover-color);\n}\n\n.btn-lg, .btn-group-lg > .btn {\n --bs-btn-padding-y: 0.5rem;\n --bs-btn-padding-x: 1rem;\n --bs-btn-font-size: 1.25rem;\n --bs-btn-border-radius: 0.5rem;\n}\n\n.btn-sm, .btn-group-sm > .btn {\n --bs-btn-padding-y: 0.25rem;\n --bs-btn-padding-x: 0.5rem;\n --bs-btn-font-size: 0.875rem;\n --bs-btn-border-radius: 0.25rem;\n}\n\n.fade {\n transition: opacity 0.15s linear;\n}\n@media (prefers-reduced-motion: reduce) {\n .fade {\n transition: none;\n }\n}\n.fade:not(.show) {\n opacity: 0;\n}\n\n.collapse:not(.show) {\n display: none;\n}\n\n.collapsing {\n height: 0;\n overflow: hidden;\n transition: height 0.35s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .collapsing {\n transition: none;\n }\n}\n.collapsing.collapse-horizontal {\n width: 0;\n height: auto;\n transition: width 0.35s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .collapsing.collapse-horizontal {\n transition: none;\n }\n}\n\n.dropup,\n.dropend,\n.dropdown,\n.dropstart,\n.dropup-center,\n.dropdown-center {\n position: relative;\n}\n\n.dropdown-toggle {\n white-space: nowrap;\n}\n.dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-bottom: 0;\n border-left: 0.3em solid transparent;\n}\n.dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropdown-menu {\n --bs-dropdown-zindex: 1000;\n --bs-dropdown-min-width: 10rem;\n --bs-dropdown-padding-x: 0;\n --bs-dropdown-padding-y: 0.5rem;\n --bs-dropdown-spacer: 0.125rem;\n --bs-dropdown-font-size: 1rem;\n --bs-dropdown-color: var(--bs-body-color);\n --bs-dropdown-bg: var(--bs-body-bg);\n --bs-dropdown-border-color: var(--bs-border-color-translucent);\n --bs-dropdown-border-radius: 0.375rem;\n --bs-dropdown-border-width: var(--bs-border-width);\n --bs-dropdown-inner-border-radius: calc(0.375rem - var(--bs-border-width));\n --bs-dropdown-divider-bg: var(--bs-border-color-translucent);\n --bs-dropdown-divider-margin-y: 0.5rem;\n --bs-dropdown-box-shadow: 0 0.5rem 1rem rgba(var(--bs-body-color-rgb), 0.15);\n --bs-dropdown-link-color: var(--bs-body-color);\n --bs-dropdown-link-hover-color: var(--bs-body-color);\n --bs-dropdown-link-hover-bg: var(--bs-tertiary-bg);\n --bs-dropdown-link-active-color: #fff;\n --bs-dropdown-link-active-bg: #0d6efd;\n --bs-dropdown-link-disabled-color: #adb5bd;\n --bs-dropdown-item-padding-x: 1rem;\n --bs-dropdown-item-padding-y: 0.25rem;\n --bs-dropdown-header-color: #6c757d;\n --bs-dropdown-header-padding-x: 1rem;\n --bs-dropdown-header-padding-y: 0.5rem;\n position: absolute;\n z-index: var(--bs-dropdown-zindex);\n display: none;\n min-width: var(--bs-dropdown-min-width);\n padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);\n margin: 0;\n font-size: var(--bs-dropdown-font-size);\n color: var(--bs-dropdown-color);\n text-align: left;\n list-style: none;\n background-color: var(--bs-dropdown-bg);\n background-clip: padding-box;\n border: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);\n border-radius: var(--bs-dropdown-border-radius);\n}\n.dropdown-menu[data-bs-popper] {\n top: 100%;\n left: 0;\n margin-top: var(--bs-dropdown-spacer);\n}\n\n.dropdown-menu-start {\n --bs-position: start;\n}\n.dropdown-menu-start[data-bs-popper] {\n right: auto;\n left: 0;\n}\n\n.dropdown-menu-end {\n --bs-position: end;\n}\n.dropdown-menu-end[data-bs-popper] {\n right: 0;\n left: auto;\n}\n\n@media (min-width: 576px) {\n .dropdown-menu-sm-start {\n --bs-position: start;\n }\n .dropdown-menu-sm-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n .dropdown-menu-sm-end {\n --bs-position: end;\n }\n .dropdown-menu-sm-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 768px) {\n .dropdown-menu-md-start {\n --bs-position: start;\n }\n .dropdown-menu-md-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n .dropdown-menu-md-end {\n --bs-position: end;\n }\n .dropdown-menu-md-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 992px) {\n .dropdown-menu-lg-start {\n --bs-position: start;\n }\n .dropdown-menu-lg-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n .dropdown-menu-lg-end {\n --bs-position: end;\n }\n .dropdown-menu-lg-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 1200px) {\n .dropdown-menu-xl-start {\n --bs-position: start;\n }\n .dropdown-menu-xl-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n .dropdown-menu-xl-end {\n --bs-position: end;\n }\n .dropdown-menu-xl-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 1400px) {\n .dropdown-menu-xxl-start {\n --bs-position: start;\n }\n .dropdown-menu-xxl-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n .dropdown-menu-xxl-end {\n --bs-position: end;\n }\n .dropdown-menu-xxl-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n.dropup .dropdown-menu[data-bs-popper] {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: var(--bs-dropdown-spacer);\n}\n.dropup .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0;\n border-right: 0.3em solid transparent;\n border-bottom: 0.3em solid;\n border-left: 0.3em solid transparent;\n}\n.dropup .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropend .dropdown-menu[data-bs-popper] {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: var(--bs-dropdown-spacer);\n}\n.dropend .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0;\n border-bottom: 0.3em solid transparent;\n border-left: 0.3em solid;\n}\n.dropend .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n.dropend .dropdown-toggle::after {\n vertical-align: 0;\n}\n\n.dropstart .dropdown-menu[data-bs-popper] {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: var(--bs-dropdown-spacer);\n}\n.dropstart .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n}\n.dropstart .dropdown-toggle::after {\n display: none;\n}\n.dropstart .dropdown-toggle::before {\n display: inline-block;\n margin-right: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0.3em solid;\n border-bottom: 0.3em solid transparent;\n}\n.dropstart .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n.dropstart .dropdown-toggle::before {\n vertical-align: 0;\n}\n\n.dropdown-divider {\n height: 0;\n margin: var(--bs-dropdown-divider-margin-y) 0;\n overflow: hidden;\n border-top: 1px solid var(--bs-dropdown-divider-bg);\n opacity: 1;\n}\n\n.dropdown-item {\n display: block;\n width: 100%;\n padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);\n clear: both;\n font-weight: 400;\n color: var(--bs-dropdown-link-color);\n text-align: inherit;\n text-decoration: none;\n white-space: nowrap;\n background-color: transparent;\n border: 0;\n border-radius: var(--bs-dropdown-item-border-radius, 0);\n}\n.dropdown-item:hover, .dropdown-item:focus {\n color: var(--bs-dropdown-link-hover-color);\n background-color: var(--bs-dropdown-link-hover-bg);\n}\n.dropdown-item.active, .dropdown-item:active {\n color: var(--bs-dropdown-link-active-color);\n text-decoration: none;\n background-color: var(--bs-dropdown-link-active-bg);\n}\n.dropdown-item.disabled, .dropdown-item:disabled {\n color: var(--bs-dropdown-link-disabled-color);\n pointer-events: none;\n background-color: transparent;\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n.dropdown-header {\n display: block;\n padding: var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);\n margin-bottom: 0;\n font-size: 0.875rem;\n color: var(--bs-dropdown-header-color);\n white-space: nowrap;\n}\n\n.dropdown-item-text {\n display: block;\n padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);\n color: var(--bs-dropdown-link-color);\n}\n\n.dropdown-menu-dark {\n --bs-dropdown-color: #dee2e6;\n --bs-dropdown-bg: #343a40;\n --bs-dropdown-border-color: var(--bs-border-color-translucent);\n --bs-dropdown-box-shadow: ;\n --bs-dropdown-link-color: #dee2e6;\n --bs-dropdown-link-hover-color: #fff;\n --bs-dropdown-divider-bg: var(--bs-border-color-translucent);\n --bs-dropdown-link-hover-bg: rgba(255, 255, 255, 0.15);\n --bs-dropdown-link-active-color: #fff;\n --bs-dropdown-link-active-bg: #0d6efd;\n --bs-dropdown-link-disabled-color: #adb5bd;\n --bs-dropdown-header-color: #adb5bd;\n}\n\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-flex;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n flex: 1 1 auto;\n}\n.btn-group > .btn-check:checked + .btn,\n.btn-group > .btn-check:focus + .btn,\n.btn-group > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn-check:checked + .btn,\n.btn-group-vertical > .btn-check:focus + .btn,\n.btn-group-vertical > .btn:hover,\n.btn-group-vertical > .btn:focus,\n.btn-group-vertical > .btn:active,\n.btn-group-vertical > .btn.active {\n z-index: 1;\n}\n\n.btn-toolbar {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n}\n.btn-toolbar .input-group {\n width: auto;\n}\n\n.btn-group {\n border-radius: 0.375rem;\n}\n.btn-group > :not(.btn-check:first-child) + .btn,\n.btn-group > .btn-group:not(:first-child) {\n margin-left: calc(var(--bs-border-width) * -1);\n}\n.btn-group > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group > .btn.dropdown-toggle-split:first-child,\n.btn-group > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn:nth-child(n+3),\n.btn-group > :not(.btn-check) + .btn,\n.btn-group > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.dropdown-toggle-split {\n padding-right: 0.5625rem;\n padding-left: 0.5625rem;\n}\n.dropdown-toggle-split::after, .dropup .dropdown-toggle-split::after, .dropend .dropdown-toggle-split::after {\n margin-left: 0;\n}\n.dropstart .dropdown-toggle-split::before {\n margin-right: 0;\n}\n\n.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {\n padding-right: 0.375rem;\n padding-left: 0.375rem;\n}\n\n.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {\n padding-right: 0.75rem;\n padding-left: 0.75rem;\n}\n\n.btn-group-vertical {\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group {\n width: 100%;\n}\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) {\n margin-top: calc(var(--bs-border-width) * -1);\n}\n.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group-vertical > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn ~ .btn,\n.btn-group-vertical > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav {\n --bs-nav-link-padding-x: 1rem;\n --bs-nav-link-padding-y: 0.5rem;\n --bs-nav-link-font-weight: ;\n --bs-nav-link-color: var(--bs-link-color);\n --bs-nav-link-hover-color: var(--bs-link-hover-color);\n --bs-nav-link-disabled-color: var(--bs-secondary-color);\n display: flex;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);\n font-size: var(--bs-nav-link-font-size);\n font-weight: var(--bs-nav-link-font-weight);\n color: var(--bs-nav-link-color);\n text-decoration: none;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .nav-link {\n transition: none;\n }\n}\n.nav-link:hover, .nav-link:focus {\n color: var(--bs-nav-link-hover-color);\n}\n.nav-link.disabled {\n color: var(--bs-nav-link-disabled-color);\n pointer-events: none;\n cursor: default;\n}\n\n.nav-tabs {\n --bs-nav-tabs-border-width: var(--bs-border-width);\n --bs-nav-tabs-border-color: var(--bs-border-color);\n --bs-nav-tabs-border-radius: var(--bs-border-radius);\n --bs-nav-tabs-link-hover-border-color: var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);\n --bs-nav-tabs-link-active-color: var(--bs-emphasis-color);\n --bs-nav-tabs-link-active-bg: var(--bs-body-bg);\n --bs-nav-tabs-link-active-border-color: var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);\n border-bottom: var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color);\n}\n.nav-tabs .nav-link {\n margin-bottom: calc(-1 * var(--bs-nav-tabs-border-width));\n background: none;\n border: var(--bs-nav-tabs-border-width) solid transparent;\n border-top-left-radius: var(--bs-nav-tabs-border-radius);\n border-top-right-radius: var(--bs-nav-tabs-border-radius);\n}\n.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {\n isolation: isolate;\n border-color: var(--bs-nav-tabs-link-hover-border-color);\n}\n.nav-tabs .nav-link.disabled, .nav-tabs .nav-link:disabled {\n color: var(--bs-nav-link-disabled-color);\n background-color: transparent;\n border-color: transparent;\n}\n.nav-tabs .nav-link.active,\n.nav-tabs .nav-item.show .nav-link {\n color: var(--bs-nav-tabs-link-active-color);\n background-color: var(--bs-nav-tabs-link-active-bg);\n border-color: var(--bs-nav-tabs-link-active-border-color);\n}\n.nav-tabs .dropdown-menu {\n margin-top: calc(-1 * var(--bs-nav-tabs-border-width));\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav-pills {\n --bs-nav-pills-border-radius: 0.375rem;\n --bs-nav-pills-link-active-color: #fff;\n --bs-nav-pills-link-active-bg: #0d6efd;\n}\n.nav-pills .nav-link {\n background: none;\n border: 0;\n border-radius: var(--bs-nav-pills-border-radius);\n}\n.nav-pills .nav-link:disabled {\n color: var(--bs-nav-link-disabled-color);\n background-color: transparent;\n border-color: transparent;\n}\n.nav-pills .nav-link.active,\n.nav-pills .show > .nav-link {\n color: var(--bs-nav-pills-link-active-color);\n background-color: var(--bs-nav-pills-link-active-bg);\n}\n\n.nav-fill > .nav-link,\n.nav-fill .nav-item {\n flex: 1 1 auto;\n text-align: center;\n}\n\n.nav-justified > .nav-link,\n.nav-justified .nav-item {\n flex-basis: 0;\n flex-grow: 1;\n text-align: center;\n}\n\n.nav-fill .nav-item .nav-link,\n.nav-justified .nav-item .nav-link {\n width: 100%;\n}\n\n.tab-content > .tab-pane {\n display: none;\n}\n.tab-content > .active {\n display: block;\n}\n\n.navbar {\n --bs-navbar-padding-x: 0;\n --bs-navbar-padding-y: 0.5rem;\n --bs-navbar-color: rgba(var(--bs-emphasis-color-rgb), 0.65);\n --bs-navbar-hover-color: rgba(var(--bs-emphasis-color-rgb), 0.8);\n --bs-navbar-disabled-color: rgba(var(--bs-emphasis-color-rgb), 0.3);\n --bs-navbar-active-color: rgba(var(--bs-emphasis-color-rgb), 1);\n --bs-navbar-brand-padding-y: 0.3125rem;\n --bs-navbar-brand-margin-end: 1rem;\n --bs-navbar-brand-font-size: 1.25rem;\n --bs-navbar-brand-color: rgba(var(--bs-emphasis-color-rgb), 1);\n --bs-navbar-brand-hover-color: rgba(var(--bs-emphasis-color-rgb), 1);\n --bs-navbar-nav-link-padding-x: 0.5rem;\n --bs-navbar-toggler-padding-y: 0.25rem;\n --bs-navbar-toggler-padding-x: 0.75rem;\n --bs-navbar-toggler-font-size: 1.25rem;\n --bs-navbar-toggler-icon-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n --bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15);\n --bs-navbar-toggler-border-radius: 0.375rem;\n --bs-navbar-toggler-focus-width: 0.25rem;\n --bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n padding: var(--bs-navbar-padding-y) var(--bs-navbar-padding-x);\n}\n.navbar > .container,\n.navbar > .container-fluid,\n.navbar > .container-sm,\n.navbar > .container-md,\n.navbar > .container-lg,\n.navbar > .container-xl,\n.navbar > .container-xxl {\n display: flex;\n flex-wrap: inherit;\n align-items: center;\n justify-content: space-between;\n}\n.navbar-brand {\n padding-top: var(--bs-navbar-brand-padding-y);\n padding-bottom: var(--bs-navbar-brand-padding-y);\n margin-right: var(--bs-navbar-brand-margin-end);\n font-size: var(--bs-navbar-brand-font-size);\n color: var(--bs-navbar-brand-color);\n text-decoration: none;\n white-space: nowrap;\n}\n.navbar-brand:hover, .navbar-brand:focus {\n color: var(--bs-navbar-brand-hover-color);\n}\n\n.navbar-nav {\n --bs-nav-link-padding-x: 0;\n --bs-nav-link-padding-y: 0.5rem;\n --bs-nav-link-font-weight: ;\n --bs-nav-link-color: var(--bs-navbar-color);\n --bs-nav-link-hover-color: var(--bs-navbar-hover-color);\n --bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n.navbar-nav .show > .nav-link,\n.navbar-nav .nav-link.active {\n color: var(--bs-navbar-active-color);\n}\n.navbar-nav .dropdown-menu {\n position: static;\n}\n\n.navbar-text {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n color: var(--bs-navbar-color);\n}\n.navbar-text a,\n.navbar-text a:hover,\n.navbar-text a:focus {\n color: var(--bs-navbar-active-color);\n}\n\n.navbar-collapse {\n flex-basis: 100%;\n flex-grow: 1;\n align-items: center;\n}\n\n.navbar-toggler {\n padding: var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);\n font-size: var(--bs-navbar-toggler-font-size);\n line-height: 1;\n color: var(--bs-navbar-color);\n background-color: transparent;\n border: var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);\n border-radius: var(--bs-navbar-toggler-border-radius);\n transition: var(--bs-navbar-toggler-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n .navbar-toggler {\n transition: none;\n }\n}\n.navbar-toggler:hover {\n text-decoration: none;\n}\n.navbar-toggler:focus {\n text-decoration: none;\n outline: 0;\n box-shadow: 0 0 0 var(--bs-navbar-toggler-focus-width);\n}\n\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n background-image: var(--bs-navbar-toggler-icon-bg);\n background-repeat: no-repeat;\n background-position: center;\n background-size: 100%;\n}\n\n.navbar-nav-scroll {\n max-height: var(--bs-scroll-height, 75vh);\n overflow-y: auto;\n}\n\n@media (min-width: 576px) {\n .navbar-expand-sm {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-sm .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-sm .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-sm .navbar-nav .nav-link {\n padding-right: var(--bs-navbar-nav-link-padding-x);\n padding-left: var(--bs-navbar-nav-link-padding-x);\n }\n .navbar-expand-sm .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-sm .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-sm .navbar-toggler {\n display: none;\n }\n .navbar-expand-sm .offcanvas {\n position: static;\n z-index: auto;\n flex-grow: 1;\n width: auto !important;\n height: auto !important;\n visibility: visible !important;\n background-color: transparent !important;\n border: 0 !important;\n transform: none !important;\n transition: none;\n }\n .navbar-expand-sm .offcanvas .offcanvas-header {\n display: none;\n }\n .navbar-expand-sm .offcanvas .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 768px) {\n .navbar-expand-md {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-md .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-md .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-md .navbar-nav .nav-link {\n padding-right: var(--bs-navbar-nav-link-padding-x);\n padding-left: var(--bs-navbar-nav-link-padding-x);\n }\n .navbar-expand-md .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-md .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-md .navbar-toggler {\n display: none;\n }\n .navbar-expand-md .offcanvas {\n position: static;\n z-index: auto;\n flex-grow: 1;\n width: auto !important;\n height: auto !important;\n visibility: visible !important;\n background-color: transparent !important;\n border: 0 !important;\n transform: none !important;\n transition: none;\n }\n .navbar-expand-md .offcanvas .offcanvas-header {\n display: none;\n }\n .navbar-expand-md .offcanvas .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 992px) {\n .navbar-expand-lg {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-lg .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-lg .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-lg .navbar-nav .nav-link {\n padding-right: var(--bs-navbar-nav-link-padding-x);\n padding-left: var(--bs-navbar-nav-link-padding-x);\n }\n .navbar-expand-lg .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-lg .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-lg .navbar-toggler {\n display: none;\n }\n .navbar-expand-lg .offcanvas {\n position: static;\n z-index: auto;\n flex-grow: 1;\n width: auto !important;\n height: auto !important;\n visibility: visible !important;\n background-color: transparent !important;\n border: 0 !important;\n transform: none !important;\n transition: none;\n }\n .navbar-expand-lg .offcanvas .offcanvas-header {\n display: none;\n }\n .navbar-expand-lg .offcanvas .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 1200px) {\n .navbar-expand-xl {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-xl .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-xl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xl .navbar-nav .nav-link {\n padding-right: var(--bs-navbar-nav-link-padding-x);\n padding-left: var(--bs-navbar-nav-link-padding-x);\n }\n .navbar-expand-xl .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-xl .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-xl .navbar-toggler {\n display: none;\n }\n .navbar-expand-xl .offcanvas {\n position: static;\n z-index: auto;\n flex-grow: 1;\n width: auto !important;\n height: auto !important;\n visibility: visible !important;\n background-color: transparent !important;\n border: 0 !important;\n transform: none !important;\n transition: none;\n }\n .navbar-expand-xl .offcanvas .offcanvas-header {\n display: none;\n }\n .navbar-expand-xl .offcanvas .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 1400px) {\n .navbar-expand-xxl {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-xxl .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-xxl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xxl .navbar-nav .nav-link {\n padding-right: var(--bs-navbar-nav-link-padding-x);\n padding-left: var(--bs-navbar-nav-link-padding-x);\n }\n .navbar-expand-xxl .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-xxl .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-xxl .navbar-toggler {\n display: none;\n }\n .navbar-expand-xxl .offcanvas {\n position: static;\n z-index: auto;\n flex-grow: 1;\n width: auto !important;\n height: auto !important;\n visibility: visible !important;\n background-color: transparent !important;\n border: 0 !important;\n transform: none !important;\n transition: none;\n }\n .navbar-expand-xxl .offcanvas .offcanvas-header {\n display: none;\n }\n .navbar-expand-xxl .offcanvas .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n.navbar-expand {\n flex-wrap: nowrap;\n justify-content: flex-start;\n}\n.navbar-expand .navbar-nav {\n flex-direction: row;\n}\n.navbar-expand .navbar-nav .dropdown-menu {\n position: absolute;\n}\n.navbar-expand .navbar-nav .nav-link {\n padding-right: var(--bs-navbar-nav-link-padding-x);\n padding-left: var(--bs-navbar-nav-link-padding-x);\n}\n.navbar-expand .navbar-nav-scroll {\n overflow: visible;\n}\n.navbar-expand .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n}\n.navbar-expand .navbar-toggler {\n display: none;\n}\n.navbar-expand .offcanvas {\n position: static;\n z-index: auto;\n flex-grow: 1;\n width: auto !important;\n height: auto !important;\n visibility: visible !important;\n background-color: transparent !important;\n border: 0 !important;\n transform: none !important;\n transition: none;\n}\n.navbar-expand .offcanvas .offcanvas-header {\n display: none;\n}\n.navbar-expand .offcanvas .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n}\n\n.navbar-dark {\n --bs-navbar-color: rgba(255, 255, 255, 0.55);\n --bs-navbar-hover-color: rgba(255, 255, 255, 0.75);\n --bs-navbar-disabled-color: rgba(255, 255, 255, 0.25);\n --bs-navbar-active-color: #fff;\n --bs-navbar-brand-color: #fff;\n --bs-navbar-brand-hover-color: #fff;\n --bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1);\n --bs-navbar-toggler-icon-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n[data-bs-theme=dark] .navbar {\n --bs-navbar-toggler-icon-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n.card {\n --bs-card-spacer-y: 1rem;\n --bs-card-spacer-x: 1rem;\n --bs-card-title-spacer-y: 0.5rem;\n --bs-card-title-color: ;\n --bs-card-subtitle-color: ;\n --bs-card-border-width: var(--bs-border-width);\n --bs-card-border-color: var(--bs-border-color-translucent);\n --bs-card-border-radius: var(--bs-border-radius);\n --bs-card-box-shadow: ;\n --bs-card-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width)));\n --bs-card-cap-padding-y: 0.5rem;\n --bs-card-cap-padding-x: 1rem;\n --bs-card-cap-bg: rgba(var(--bs-body-color-rgb), 0.03);\n --bs-card-cap-color: ;\n --bs-card-height: ;\n --bs-card-color: ;\n --bs-card-bg: var(--bs-body-bg);\n --bs-card-img-overlay-padding: 1rem;\n --bs-card-group-margin: 0.75rem;\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n height: var(--bs-card-height);\n word-wrap: break-word;\n background-color: var(--bs-card-bg);\n background-clip: border-box;\n border: var(--bs-card-border-width) solid var(--bs-card-border-color);\n border-radius: var(--bs-card-border-radius);\n}\n.card > hr {\n margin-right: 0;\n margin-left: 0;\n}\n.card > .list-group {\n border-top: inherit;\n border-bottom: inherit;\n}\n.card > .list-group:first-child {\n border-top-width: 0;\n border-top-left-radius: var(--bs-card-inner-border-radius);\n border-top-right-radius: var(--bs-card-inner-border-radius);\n}\n.card > .list-group:last-child {\n border-bottom-width: 0;\n border-bottom-right-radius: var(--bs-card-inner-border-radius);\n border-bottom-left-radius: var(--bs-card-inner-border-radius);\n}\n.card > .card-header + .list-group,\n.card > .list-group + .card-footer {\n border-top: 0;\n}\n\n.card-body {\n flex: 1 1 auto;\n padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x);\n color: var(--bs-card-color);\n}\n\n.card-title {\n margin-bottom: var(--bs-card-title-spacer-y);\n color: var(--bs-card-title-color);\n}\n\n.card-subtitle {\n margin-top: calc(-0.5 * var(--bs-card-title-spacer-y));\n margin-bottom: 0;\n color: var(--bs-card-subtitle-color);\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link + .card-link {\n margin-left: var(--bs-card-spacer-x);\n}\n\n.card-header {\n padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);\n margin-bottom: 0;\n color: var(--bs-card-cap-color);\n background-color: var(--bs-card-cap-bg);\n border-bottom: var(--bs-card-border-width) solid var(--bs-card-border-color);\n}\n.card-header:first-child {\n border-radius: var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0;\n}\n\n.card-footer {\n padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);\n color: var(--bs-card-cap-color);\n background-color: var(--bs-card-cap-bg);\n border-top: var(--bs-card-border-width) solid var(--bs-card-border-color);\n}\n.card-footer:last-child {\n border-radius: 0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius);\n}\n\n.card-header-tabs {\n margin-right: calc(-0.5 * var(--bs-card-cap-padding-x));\n margin-bottom: calc(-1 * var(--bs-card-cap-padding-y));\n margin-left: calc(-0.5 * var(--bs-card-cap-padding-x));\n border-bottom: 0;\n}\n.card-header-tabs .nav-link.active {\n background-color: var(--bs-card-bg);\n border-bottom-color: var(--bs-card-bg);\n}\n\n.card-header-pills {\n margin-right: calc(-0.5 * var(--bs-card-cap-padding-x));\n margin-left: calc(-0.5 * var(--bs-card-cap-padding-x));\n}\n\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: var(--bs-card-img-overlay-padding);\n border-radius: var(--bs-card-inner-border-radius);\n}\n\n.card-img,\n.card-img-top,\n.card-img-bottom {\n width: 100%;\n}\n\n.card-img,\n.card-img-top {\n border-top-left-radius: var(--bs-card-inner-border-radius);\n border-top-right-radius: var(--bs-card-inner-border-radius);\n}\n\n.card-img,\n.card-img-bottom {\n border-bottom-right-radius: var(--bs-card-inner-border-radius);\n border-bottom-left-radius: var(--bs-card-inner-border-radius);\n}\n\n.card-group > .card {\n margin-bottom: var(--bs-card-group-margin);\n}\n@media (min-width: 576px) {\n .card-group {\n display: flex;\n flex-flow: row wrap;\n }\n .card-group > .card {\n flex: 1 0 0%;\n margin-bottom: 0;\n }\n .card-group > .card + .card {\n margin-left: 0;\n border-left: 0;\n }\n .card-group > .card:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-top,\n .card-group > .card:not(:last-child) .card-header {\n border-top-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-bottom,\n .card-group > .card:not(:last-child) .card-footer {\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-top,\n .card-group > .card:not(:first-child) .card-header {\n border-top-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-bottom,\n .card-group > .card:not(:first-child) .card-footer {\n border-bottom-left-radius: 0;\n }\n}\n\n.accordion {\n --bs-accordion-color: var(--bs-body-color);\n --bs-accordion-bg: var(--bs-body-bg);\n --bs-accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;\n --bs-accordion-border-color: var(--bs-border-color);\n --bs-accordion-border-width: var(--bs-border-width);\n --bs-accordion-border-radius: var(--bs-border-radius);\n --bs-accordion-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width)));\n --bs-accordion-btn-padding-x: 1.25rem;\n --bs-accordion-btn-padding-y: 1rem;\n --bs-accordion-btn-color: var(--bs-body-color);\n --bs-accordion-btn-bg: var(--bs-accordion-bg);\n --bs-accordion-btn-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\");\n --bs-accordion-btn-icon-width: 1.25rem;\n --bs-accordion-btn-icon-transform: rotate(-180deg);\n --bs-accordion-btn-icon-transition: transform 0.2s ease-in-out;\n --bs-accordion-btn-active-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230a58ca'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\");\n --bs-accordion-btn-focus-border-color: #86b7fe;\n --bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n --bs-accordion-body-padding-x: 1.25rem;\n --bs-accordion-body-padding-y: 1rem;\n --bs-accordion-active-color: var(--bs-primary-text);\n --bs-accordion-active-bg: var(--bs-primary-bg-subtle);\n}\n\n.accordion-button {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n padding: var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);\n font-size: 1rem;\n color: var(--bs-accordion-btn-color);\n text-align: left;\n background-color: var(--bs-accordion-btn-bg);\n border: 0;\n border-radius: 0;\n overflow-anchor: none;\n transition: var(--bs-accordion-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n .accordion-button {\n transition: none;\n }\n}\n.accordion-button:not(.collapsed) {\n color: var(--bs-accordion-active-color);\n background-color: var(--bs-accordion-active-bg);\n box-shadow: inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color);\n}\n.accordion-button:not(.collapsed)::after {\n background-image: var(--bs-accordion-btn-active-icon);\n transform: var(--bs-accordion-btn-icon-transform);\n}\n.accordion-button::after {\n flex-shrink: 0;\n width: var(--bs-accordion-btn-icon-width);\n height: var(--bs-accordion-btn-icon-width);\n margin-left: auto;\n content: \"\";\n background-image: var(--bs-accordion-btn-icon);\n background-repeat: no-repeat;\n background-size: var(--bs-accordion-btn-icon-width);\n transition: var(--bs-accordion-btn-icon-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n .accordion-button::after {\n transition: none;\n }\n}\n.accordion-button:hover {\n z-index: 2;\n}\n.accordion-button:focus {\n z-index: 3;\n border-color: var(--bs-accordion-btn-focus-border-color);\n outline: 0;\n box-shadow: var(--bs-accordion-btn-focus-box-shadow);\n}\n\n.accordion-header {\n margin-bottom: 0;\n}\n\n.accordion-item {\n color: var(--bs-accordion-color);\n background-color: var(--bs-accordion-bg);\n border: var(--bs-accordion-border-width) solid var(--bs-accordion-border-color);\n}\n.accordion-item:first-of-type {\n border-top-left-radius: var(--bs-accordion-border-radius);\n border-top-right-radius: var(--bs-accordion-border-radius);\n}\n.accordion-item:first-of-type .accordion-button {\n border-top-left-radius: var(--bs-accordion-inner-border-radius);\n border-top-right-radius: var(--bs-accordion-inner-border-radius);\n}\n.accordion-item:not(:first-of-type) {\n border-top: 0;\n}\n.accordion-item:last-of-type {\n border-bottom-right-radius: var(--bs-accordion-border-radius);\n border-bottom-left-radius: var(--bs-accordion-border-radius);\n}\n.accordion-item:last-of-type .accordion-button.collapsed {\n border-bottom-right-radius: var(--bs-accordion-inner-border-radius);\n border-bottom-left-radius: var(--bs-accordion-inner-border-radius);\n}\n.accordion-item:last-of-type .accordion-collapse {\n border-bottom-right-radius: var(--bs-accordion-border-radius);\n border-bottom-left-radius: var(--bs-accordion-border-radius);\n}\n\n.accordion-body {\n padding: var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x);\n}\n\n.accordion-flush .accordion-collapse {\n border-width: 0;\n}\n.accordion-flush .accordion-item {\n border-right: 0;\n border-left: 0;\n border-radius: 0;\n}\n.accordion-flush .accordion-item:first-child {\n border-top: 0;\n}\n.accordion-flush .accordion-item:last-child {\n border-bottom: 0;\n}\n.accordion-flush .accordion-item .accordion-button, .accordion-flush .accordion-item .accordion-button.collapsed {\n border-radius: 0;\n}\n\n[data-bs-theme=dark] .accordion-button::after {\n --bs-accordion-btn-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\");\n --bs-accordion-btn-active-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\");\n}\n\n.breadcrumb {\n --bs-breadcrumb-padding-x: 0;\n --bs-breadcrumb-padding-y: 0;\n --bs-breadcrumb-margin-bottom: 1rem;\n --bs-breadcrumb-bg: ;\n --bs-breadcrumb-border-radius: ;\n --bs-breadcrumb-divider-color: var(--bs-secondary-color);\n --bs-breadcrumb-item-padding-x: 0.5rem;\n --bs-breadcrumb-item-active-color: var(--bs-secondary-color);\n display: flex;\n flex-wrap: wrap;\n padding: var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);\n margin-bottom: var(--bs-breadcrumb-margin-bottom);\n font-size: var(--bs-breadcrumb-font-size);\n list-style: none;\n background-color: var(--bs-breadcrumb-bg);\n border-radius: var(--bs-breadcrumb-border-radius);\n}\n\n.breadcrumb-item + .breadcrumb-item {\n padding-left: var(--bs-breadcrumb-item-padding-x);\n}\n.breadcrumb-item + .breadcrumb-item::before {\n float: left;\n padding-right: var(--bs-breadcrumb-item-padding-x);\n color: var(--bs-breadcrumb-divider-color);\n content: var(--bs-breadcrumb-divider, \"/\") /* rtl: var(--bs-breadcrumb-divider, \"/\") */;\n}\n.breadcrumb-item.active {\n color: var(--bs-breadcrumb-item-active-color);\n}\n\n.pagination {\n --bs-pagination-padding-x: 0.75rem;\n --bs-pagination-padding-y: 0.375rem;\n --bs-pagination-font-size: 1rem;\n --bs-pagination-color: var(--bs-link-color);\n --bs-pagination-bg: var(--bs-body-bg);\n --bs-pagination-border-width: var(--bs-border-width);\n --bs-pagination-border-color: var(--bs-border-color);\n --bs-pagination-border-radius: var(--bs-border-radius);\n --bs-pagination-hover-color: var(--bs-link-hover-color);\n --bs-pagination-hover-bg: var(--bs-tertiary-bg);\n --bs-pagination-hover-border-color: var(--bs-border-color);\n --bs-pagination-focus-color: var(--bs-link-hover-color);\n --bs-pagination-focus-bg: var(--bs-secondary-bg);\n --bs-pagination-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n --bs-pagination-active-color: #fff;\n --bs-pagination-active-bg: #0d6efd;\n --bs-pagination-active-border-color: #0d6efd;\n --bs-pagination-disabled-color: var(--bs-secondary-color);\n --bs-pagination-disabled-bg: var(--bs-secondary-bg);\n --bs-pagination-disabled-border-color: var(--bs-border-color);\n display: flex;\n padding-left: 0;\n list-style: none;\n}\n\n.page-link {\n position: relative;\n display: block;\n padding: var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);\n font-size: var(--bs-pagination-font-size);\n color: var(--bs-pagination-color);\n text-decoration: none;\n background-color: var(--bs-pagination-bg);\n border: var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .page-link {\n transition: none;\n }\n}\n.page-link:hover {\n z-index: 2;\n color: var(--bs-pagination-hover-color);\n background-color: var(--bs-pagination-hover-bg);\n border-color: var(--bs-pagination-hover-border-color);\n}\n.page-link:focus {\n z-index: 3;\n color: var(--bs-pagination-focus-color);\n background-color: var(--bs-pagination-focus-bg);\n outline: 0;\n box-shadow: var(--bs-pagination-focus-box-shadow);\n}\n.page-link.active, .active > .page-link {\n z-index: 3;\n color: var(--bs-pagination-active-color);\n background-color: var(--bs-pagination-active-bg);\n border-color: var(--bs-pagination-active-border-color);\n}\n.page-link.disabled, .disabled > .page-link {\n color: var(--bs-pagination-disabled-color);\n pointer-events: none;\n background-color: var(--bs-pagination-disabled-bg);\n border-color: var(--bs-pagination-disabled-border-color);\n}\n\n.page-item:not(:first-child) .page-link {\n margin-left: calc(var(--bs-border-width) * -1);\n}\n.page-item:first-child .page-link {\n border-top-left-radius: var(--bs-pagination-border-radius);\n border-bottom-left-radius: var(--bs-pagination-border-radius);\n}\n.page-item:last-child .page-link {\n border-top-right-radius: var(--bs-pagination-border-radius);\n border-bottom-right-radius: var(--bs-pagination-border-radius);\n}\n\n.pagination-lg {\n --bs-pagination-padding-x: 1.5rem;\n --bs-pagination-padding-y: 0.75rem;\n --bs-pagination-font-size: 1.25rem;\n --bs-pagination-border-radius: 0.5rem;\n}\n\n.pagination-sm {\n --bs-pagination-padding-x: 0.5rem;\n --bs-pagination-padding-y: 0.25rem;\n --bs-pagination-font-size: 0.875rem;\n --bs-pagination-border-radius: 0.25rem;\n}\n\n.badge {\n --bs-badge-padding-x: 0.65em;\n --bs-badge-padding-y: 0.35em;\n --bs-badge-font-size: 0.75em;\n --bs-badge-font-weight: 700;\n --bs-badge-color: #fff;\n --bs-badge-border-radius: 0.375rem;\n display: inline-block;\n padding: var(--bs-badge-padding-y) var(--bs-badge-padding-x);\n font-size: var(--bs-badge-font-size);\n font-weight: var(--bs-badge-font-weight);\n line-height: 1;\n color: var(--bs-badge-color);\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: var(--bs-badge-border-radius);\n}\n.badge:empty {\n display: none;\n}\n\n.btn .badge {\n position: relative;\n top: -1px;\n}\n\n.alert {\n --bs-alert-bg: transparent;\n --bs-alert-padding-x: 1rem;\n --bs-alert-padding-y: 1rem;\n --bs-alert-margin-bottom: 1rem;\n --bs-alert-color: inherit;\n --bs-alert-border-color: transparent;\n --bs-alert-border: var(--bs-border-width) solid var(--bs-alert-border-color);\n --bs-alert-border-radius: 0.375rem;\n --bs-alert-link-color: inherit;\n position: relative;\n padding: var(--bs-alert-padding-y) var(--bs-alert-padding-x);\n margin-bottom: var(--bs-alert-margin-bottom);\n color: var(--bs-alert-color);\n background-color: var(--bs-alert-bg);\n border: var(--bs-alert-border);\n border-radius: var(--bs-alert-border-radius);\n}\n\n.alert-heading {\n color: inherit;\n}\n\n.alert-link {\n font-weight: 700;\n color: var(--bs-alert-link-color);\n}\n\n.alert-dismissible {\n padding-right: 3rem;\n}\n.alert-dismissible .btn-close {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n padding: 1.25rem 1rem;\n}\n\n.alert-primary {\n --bs-alert-color: var(--bs-primary-text);\n --bs-alert-bg: var(--bs-primary-bg-subtle);\n --bs-alert-border-color: var(--bs-primary-border-subtle);\n --bs-alert-link-color: var(--bs-primary-text);\n}\n\n.alert-secondary {\n --bs-alert-color: var(--bs-secondary-text);\n --bs-alert-bg: var(--bs-secondary-bg-subtle);\n --bs-alert-border-color: var(--bs-secondary-border-subtle);\n --bs-alert-link-color: var(--bs-secondary-text);\n}\n\n.alert-success {\n --bs-alert-color: var(--bs-success-text);\n --bs-alert-bg: var(--bs-success-bg-subtle);\n --bs-alert-border-color: var(--bs-success-border-subtle);\n --bs-alert-link-color: var(--bs-success-text);\n}\n\n.alert-info {\n --bs-alert-color: var(--bs-info-text);\n --bs-alert-bg: var(--bs-info-bg-subtle);\n --bs-alert-border-color: var(--bs-info-border-subtle);\n --bs-alert-link-color: var(--bs-info-text);\n}\n\n.alert-warning {\n --bs-alert-color: var(--bs-warning-text);\n --bs-alert-bg: var(--bs-warning-bg-subtle);\n --bs-alert-border-color: var(--bs-warning-border-subtle);\n --bs-alert-link-color: var(--bs-warning-text);\n}\n\n.alert-danger {\n --bs-alert-color: var(--bs-danger-text);\n --bs-alert-bg: var(--bs-danger-bg-subtle);\n --bs-alert-border-color: var(--bs-danger-border-subtle);\n --bs-alert-link-color: var(--bs-danger-text);\n}\n\n.alert-light {\n --bs-alert-color: var(--bs-light-text);\n --bs-alert-bg: var(--bs-light-bg-subtle);\n --bs-alert-border-color: var(--bs-light-border-subtle);\n --bs-alert-link-color: var(--bs-light-text);\n}\n\n.alert-dark {\n --bs-alert-color: var(--bs-dark-text);\n --bs-alert-bg: var(--bs-dark-bg-subtle);\n --bs-alert-border-color: var(--bs-dark-border-subtle);\n --bs-alert-link-color: var(--bs-dark-text);\n}\n\n@keyframes progress-bar-stripes {\n 0% {\n background-position-x: 1rem;\n }\n}\n.progress,\n.progress-stacked {\n --bs-progress-height: 1rem;\n --bs-progress-font-size: 0.75rem;\n --bs-progress-bg: var(--bs-secondary-bg);\n --bs-progress-border-radius: var(--bs-border-radius);\n --bs-progress-box-shadow: var(--bs-box-shadow-inset);\n --bs-progress-bar-color: #fff;\n --bs-progress-bar-bg: #0d6efd;\n --bs-progress-bar-transition: width 0.6s ease;\n display: flex;\n height: var(--bs-progress-height);\n overflow: hidden;\n font-size: var(--bs-progress-font-size);\n background-color: var(--bs-progress-bg);\n border-radius: var(--bs-progress-border-radius);\n}\n\n.progress-bar {\n display: flex;\n flex-direction: column;\n justify-content: center;\n overflow: hidden;\n color: var(--bs-progress-bar-color);\n text-align: center;\n white-space: nowrap;\n background-color: var(--bs-progress-bar-bg);\n transition: var(--bs-progress-bar-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n .progress-bar {\n transition: none;\n }\n}\n\n.progress-bar-striped {\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: var(--bs-progress-height) var(--bs-progress-height);\n}\n\n.progress-stacked > .progress {\n overflow: visible;\n}\n\n.progress-stacked > .progress > .progress-bar {\n width: 100%;\n}\n\n.progress-bar-animated {\n animation: 1s linear infinite progress-bar-stripes;\n}\n@media (prefers-reduced-motion: reduce) {\n .progress-bar-animated {\n animation: none;\n }\n}\n\n.list-group {\n --bs-list-group-color: var(--bs-body-color);\n --bs-list-group-bg: var(--bs-body-bg);\n --bs-list-group-border-color: var(--bs-border-color);\n --bs-list-group-border-width: var(--bs-border-width);\n --bs-list-group-border-radius: var(--bs-border-radius);\n --bs-list-group-item-padding-x: 1rem;\n --bs-list-group-item-padding-y: 0.5rem;\n --bs-list-group-action-color: var(--bs-secondary-color);\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-tertiary-bg);\n --bs-list-group-action-active-color: var(--bs-body-color);\n --bs-list-group-action-active-bg: var(--bs-secondary-bg);\n --bs-list-group-disabled-color: var(--bs-secondary-color);\n --bs-list-group-disabled-bg: var(--bs-body-bg);\n --bs-list-group-active-color: #fff;\n --bs-list-group-active-bg: #0d6efd;\n --bs-list-group-active-border-color: #0d6efd;\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n border-radius: var(--bs-list-group-border-radius);\n}\n\n.list-group-numbered {\n list-style-type: none;\n counter-reset: section;\n}\n.list-group-numbered > .list-group-item::before {\n content: counters(section, \".\") \". \";\n counter-increment: section;\n}\n\n.list-group-item-action {\n width: 100%;\n color: var(--bs-list-group-action-color);\n text-align: inherit;\n}\n.list-group-item-action:hover, .list-group-item-action:focus {\n z-index: 1;\n color: var(--bs-list-group-action-hover-color);\n text-decoration: none;\n background-color: var(--bs-list-group-action-hover-bg);\n}\n.list-group-item-action:active {\n color: var(--bs-list-group-action-active-color);\n background-color: var(--bs-list-group-action-active-bg);\n}\n\n.list-group-item {\n position: relative;\n display: block;\n padding: var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);\n color: var(--bs-list-group-color);\n text-decoration: none;\n background-color: var(--bs-list-group-bg);\n border: var(--bs-list-group-border-width) solid var(--bs-list-group-border-color);\n}\n.list-group-item:first-child {\n border-top-left-radius: inherit;\n border-top-right-radius: inherit;\n}\n.list-group-item:last-child {\n border-bottom-right-radius: inherit;\n border-bottom-left-radius: inherit;\n}\n.list-group-item.disabled, .list-group-item:disabled {\n color: var(--bs-list-group-disabled-color);\n pointer-events: none;\n background-color: var(--bs-list-group-disabled-bg);\n}\n.list-group-item.active {\n z-index: 2;\n color: var(--bs-list-group-active-color);\n background-color: var(--bs-list-group-active-bg);\n border-color: var(--bs-list-group-active-border-color);\n}\n.list-group-item + .list-group-item {\n border-top-width: 0;\n}\n.list-group-item + .list-group-item.active {\n margin-top: calc(-1 * var(--bs-list-group-border-width));\n border-top-width: var(--bs-list-group-border-width);\n}\n\n.list-group-horizontal {\n flex-direction: row;\n}\n.list-group-horizontal > .list-group-item:first-child:not(:last-child) {\n border-bottom-left-radius: var(--bs-list-group-border-radius);\n border-top-right-radius: 0;\n}\n.list-group-horizontal > .list-group-item:last-child:not(:first-child) {\n border-top-right-radius: var(--bs-list-group-border-radius);\n border-bottom-left-radius: 0;\n}\n.list-group-horizontal > .list-group-item.active {\n margin-top: 0;\n}\n.list-group-horizontal > .list-group-item + .list-group-item {\n border-top-width: var(--bs-list-group-border-width);\n border-left-width: 0;\n}\n.list-group-horizontal > .list-group-item + .list-group-item.active {\n margin-left: calc(-1 * var(--bs-list-group-border-width));\n border-left-width: var(--bs-list-group-border-width);\n}\n\n@media (min-width: 576px) {\n .list-group-horizontal-sm {\n flex-direction: row;\n }\n .list-group-horizontal-sm > .list-group-item:first-child:not(:last-child) {\n border-bottom-left-radius: var(--bs-list-group-border-radius);\n border-top-right-radius: 0;\n }\n .list-group-horizontal-sm > .list-group-item:last-child:not(:first-child) {\n border-top-right-radius: var(--bs-list-group-border-radius);\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-sm > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-sm > .list-group-item + .list-group-item {\n border-top-width: var(--bs-list-group-border-width);\n border-left-width: 0;\n }\n .list-group-horizontal-sm > .list-group-item + .list-group-item.active {\n margin-left: calc(-1 * var(--bs-list-group-border-width));\n border-left-width: var(--bs-list-group-border-width);\n }\n}\n@media (min-width: 768px) {\n .list-group-horizontal-md {\n flex-direction: row;\n }\n .list-group-horizontal-md > .list-group-item:first-child:not(:last-child) {\n border-bottom-left-radius: var(--bs-list-group-border-radius);\n border-top-right-radius: 0;\n }\n .list-group-horizontal-md > .list-group-item:last-child:not(:first-child) {\n border-top-right-radius: var(--bs-list-group-border-radius);\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-md > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-md > .list-group-item + .list-group-item {\n border-top-width: var(--bs-list-group-border-width);\n border-left-width: 0;\n }\n .list-group-horizontal-md > .list-group-item + .list-group-item.active {\n margin-left: calc(-1 * var(--bs-list-group-border-width));\n border-left-width: var(--bs-list-group-border-width);\n }\n}\n@media (min-width: 992px) {\n .list-group-horizontal-lg {\n flex-direction: row;\n }\n .list-group-horizontal-lg > .list-group-item:first-child:not(:last-child) {\n border-bottom-left-radius: var(--bs-list-group-border-radius);\n border-top-right-radius: 0;\n }\n .list-group-horizontal-lg > .list-group-item:last-child:not(:first-child) {\n border-top-right-radius: var(--bs-list-group-border-radius);\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-lg > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-lg > .list-group-item + .list-group-item {\n border-top-width: var(--bs-list-group-border-width);\n border-left-width: 0;\n }\n .list-group-horizontal-lg > .list-group-item + .list-group-item.active {\n margin-left: calc(-1 * var(--bs-list-group-border-width));\n border-left-width: var(--bs-list-group-border-width);\n }\n}\n@media (min-width: 1200px) {\n .list-group-horizontal-xl {\n flex-direction: row;\n }\n .list-group-horizontal-xl > .list-group-item:first-child:not(:last-child) {\n border-bottom-left-radius: var(--bs-list-group-border-radius);\n border-top-right-radius: 0;\n }\n .list-group-horizontal-xl > .list-group-item:last-child:not(:first-child) {\n border-top-right-radius: var(--bs-list-group-border-radius);\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-xl > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-xl > .list-group-item + .list-group-item {\n border-top-width: var(--bs-list-group-border-width);\n border-left-width: 0;\n }\n .list-group-horizontal-xl > .list-group-item + .list-group-item.active {\n margin-left: calc(-1 * var(--bs-list-group-border-width));\n border-left-width: var(--bs-list-group-border-width);\n }\n}\n@media (min-width: 1400px) {\n .list-group-horizontal-xxl {\n flex-direction: row;\n }\n .list-group-horizontal-xxl > .list-group-item:first-child:not(:last-child) {\n border-bottom-left-radius: var(--bs-list-group-border-radius);\n border-top-right-radius: 0;\n }\n .list-group-horizontal-xxl > .list-group-item:last-child:not(:first-child) {\n border-top-right-radius: var(--bs-list-group-border-radius);\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-xxl > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-xxl > .list-group-item + .list-group-item {\n border-top-width: var(--bs-list-group-border-width);\n border-left-width: 0;\n }\n .list-group-horizontal-xxl > .list-group-item + .list-group-item.active {\n margin-left: calc(-1 * var(--bs-list-group-border-width));\n border-left-width: var(--bs-list-group-border-width);\n }\n}\n.list-group-flush {\n border-radius: 0;\n}\n.list-group-flush > .list-group-item {\n border-width: 0 0 var(--bs-list-group-border-width);\n}\n.list-group-flush > .list-group-item:last-child {\n border-bottom-width: 0;\n}\n\n.list-group-item-primary {\n --bs-list-group-color: var(--bs-primary-text);\n --bs-list-group-bg: var(--bs-primary-bg-subtle);\n --bs-list-group-border-color: var(--bs-primary-border-subtle);\n}\n.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus {\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-primary-border-subtle);\n}\n.list-group-item-primary.list-group-item-action:active {\n --bs-list-group-active-color: var(--bs-emphasis-color);\n --bs-list-group-active-bg: var(--bs-primary-text);\n --bs-list-group-active-border-color: var(--bs-primary-text);\n}\n\n.list-group-item-secondary {\n --bs-list-group-color: var(--bs-secondary-text);\n --bs-list-group-bg: var(--bs-secondary-bg-subtle);\n --bs-list-group-border-color: var(--bs-secondary-border-subtle);\n}\n.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus {\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-secondary-border-subtle);\n}\n.list-group-item-secondary.list-group-item-action:active {\n --bs-list-group-active-color: var(--bs-emphasis-color);\n --bs-list-group-active-bg: var(--bs-secondary-text);\n --bs-list-group-active-border-color: var(--bs-secondary-text);\n}\n\n.list-group-item-success {\n --bs-list-group-color: var(--bs-success-text);\n --bs-list-group-bg: var(--bs-success-bg-subtle);\n --bs-list-group-border-color: var(--bs-success-border-subtle);\n}\n.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus {\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-success-border-subtle);\n}\n.list-group-item-success.list-group-item-action:active {\n --bs-list-group-active-color: var(--bs-emphasis-color);\n --bs-list-group-active-bg: var(--bs-success-text);\n --bs-list-group-active-border-color: var(--bs-success-text);\n}\n\n.list-group-item-info {\n --bs-list-group-color: var(--bs-info-text);\n --bs-list-group-bg: var(--bs-info-bg-subtle);\n --bs-list-group-border-color: var(--bs-info-border-subtle);\n}\n.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-info-border-subtle);\n}\n.list-group-item-info.list-group-item-action:active {\n --bs-list-group-active-color: var(--bs-emphasis-color);\n --bs-list-group-active-bg: var(--bs-info-text);\n --bs-list-group-active-border-color: var(--bs-info-text);\n}\n\n.list-group-item-warning {\n --bs-list-group-color: var(--bs-warning-text);\n --bs-list-group-bg: var(--bs-warning-bg-subtle);\n --bs-list-group-border-color: var(--bs-warning-border-subtle);\n}\n.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus {\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-warning-border-subtle);\n}\n.list-group-item-warning.list-group-item-action:active {\n --bs-list-group-active-color: var(--bs-emphasis-color);\n --bs-list-group-active-bg: var(--bs-warning-text);\n --bs-list-group-active-border-color: var(--bs-warning-text);\n}\n\n.list-group-item-danger {\n --bs-list-group-color: var(--bs-danger-text);\n --bs-list-group-bg: var(--bs-danger-bg-subtle);\n --bs-list-group-border-color: var(--bs-danger-border-subtle);\n}\n.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus {\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-danger-border-subtle);\n}\n.list-group-item-danger.list-group-item-action:active {\n --bs-list-group-active-color: var(--bs-emphasis-color);\n --bs-list-group-active-bg: var(--bs-danger-text);\n --bs-list-group-active-border-color: var(--bs-danger-text);\n}\n\n.list-group-item-light {\n --bs-list-group-color: var(--bs-light-text);\n --bs-list-group-bg: var(--bs-light-bg-subtle);\n --bs-list-group-border-color: var(--bs-light-border-subtle);\n}\n.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus {\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-light-border-subtle);\n}\n.list-group-item-light.list-group-item-action:active {\n --bs-list-group-active-color: var(--bs-emphasis-color);\n --bs-list-group-active-bg: var(--bs-light-text);\n --bs-list-group-active-border-color: var(--bs-light-text);\n}\n\n.list-group-item-dark {\n --bs-list-group-color: var(--bs-dark-text);\n --bs-list-group-bg: var(--bs-dark-bg-subtle);\n --bs-list-group-border-color: var(--bs-dark-border-subtle);\n}\n.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus {\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-dark-border-subtle);\n}\n.list-group-item-dark.list-group-item-action:active {\n --bs-list-group-active-color: var(--bs-emphasis-color);\n --bs-list-group-active-bg: var(--bs-dark-text);\n --bs-list-group-active-border-color: var(--bs-dark-text);\n}\n\n.btn-close {\n --bs-btn-close-color: #000;\n --bs-btn-close-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e\");\n --bs-btn-close-opacity: 0.5;\n --bs-btn-close-hover-opacity: 0.75;\n --bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n --bs-btn-close-focus-opacity: 1;\n --bs-btn-close-disabled-opacity: 0.25;\n --bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);\n box-sizing: content-box;\n width: 1em;\n height: 1em;\n padding: 0.25em 0.25em;\n color: var(--bs-btn-close-color);\n background: transparent var(--bs-btn-close-bg) center/1em auto no-repeat;\n border: 0;\n border-radius: 0.375rem;\n opacity: var(--bs-btn-close-opacity);\n}\n.btn-close:hover {\n color: var(--bs-btn-close-color);\n text-decoration: none;\n opacity: var(--bs-btn-close-hover-opacity);\n}\n.btn-close:focus {\n outline: 0;\n box-shadow: var(--bs-btn-close-focus-shadow);\n opacity: var(--bs-btn-close-focus-opacity);\n}\n.btn-close:disabled, .btn-close.disabled {\n pointer-events: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n opacity: var(--bs-btn-close-disabled-opacity);\n}\n\n.btn-close-white {\n filter: var(--bs-btn-close-white-filter);\n}\n\n[data-bs-theme=dark] .btn-close {\n filter: var(--bs-btn-close-white-filter);\n}\n\n.toast {\n --bs-toast-zindex: 1090;\n --bs-toast-padding-x: 0.75rem;\n --bs-toast-padding-y: 0.5rem;\n --bs-toast-spacing: 1.5rem;\n --bs-toast-max-width: 350px;\n --bs-toast-font-size: 0.875rem;\n --bs-toast-color: ;\n --bs-toast-bg: rgba(var(--bs-body-bg-rgb), 0.85);\n --bs-toast-border-width: var(--bs-border-width);\n --bs-toast-border-color: var(--bs-border-color-translucent);\n --bs-toast-border-radius: var(--bs-border-radius);\n --bs-toast-box-shadow: var(--bs-box-shadow);\n --bs-toast-header-color: var(--bs-secondary-color);\n --bs-toast-header-bg: rgba(var(--bs-body-bg-rgb), 0.85);\n --bs-toast-header-border-color: var(--bs-border-color-translucent);\n width: var(--bs-toast-max-width);\n max-width: 100%;\n font-size: var(--bs-toast-font-size);\n color: var(--bs-toast-color);\n pointer-events: auto;\n background-color: var(--bs-toast-bg);\n background-clip: padding-box;\n border: var(--bs-toast-border-width) solid var(--bs-toast-border-color);\n box-shadow: var(--bs-toast-box-shadow);\n border-radius: var(--bs-toast-border-radius);\n}\n.toast.showing {\n opacity: 0;\n}\n.toast:not(.show) {\n display: none;\n}\n\n.toast-container {\n --bs-toast-zindex: 1090;\n position: absolute;\n z-index: var(--bs-toast-zindex);\n width: -webkit-max-content;\n width: -moz-max-content;\n width: max-content;\n max-width: 100%;\n pointer-events: none;\n}\n.toast-container > :not(:last-child) {\n margin-bottom: var(--bs-toast-spacing);\n}\n\n.toast-header {\n display: flex;\n align-items: center;\n padding: var(--bs-toast-padding-y) var(--bs-toast-padding-x);\n color: var(--bs-toast-header-color);\n background-color: var(--bs-toast-header-bg);\n background-clip: padding-box;\n border-bottom: var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);\n border-top-left-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));\n border-top-right-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));\n}\n.toast-header .btn-close {\n margin-right: calc(-0.5 * var(--bs-toast-padding-x));\n margin-left: var(--bs-toast-padding-x);\n}\n\n.toast-body {\n padding: var(--bs-toast-padding-x);\n word-wrap: break-word;\n}\n\n.modal {\n --bs-modal-zindex: 1055;\n --bs-modal-width: 500px;\n --bs-modal-padding: 1rem;\n --bs-modal-margin: 0.5rem;\n --bs-modal-color: ;\n --bs-modal-bg: var(--bs-body-bg);\n --bs-modal-border-color: var(--bs-border-color-translucent);\n --bs-modal-border-width: var(--bs-border-width);\n --bs-modal-border-radius: var(--bs-border-radius-lg);\n --bs-modal-box-shadow: 0 0.125rem 0.25rem rgba(var(--bs-body-color-rgb), 0.075);\n --bs-modal-inner-border-radius: calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));\n --bs-modal-header-padding-x: 1rem;\n --bs-modal-header-padding-y: 1rem;\n --bs-modal-header-padding: 1rem 1rem;\n --bs-modal-header-border-color: var(--bs-border-color);\n --bs-modal-header-border-width: var(--bs-border-width);\n --bs-modal-title-line-height: 1.5;\n --bs-modal-footer-gap: 0.5rem;\n --bs-modal-footer-bg: ;\n --bs-modal-footer-border-color: var(--bs-border-color);\n --bs-modal-footer-border-width: var(--bs-border-width);\n position: fixed;\n top: 0;\n left: 0;\n z-index: var(--bs-modal-zindex);\n display: none;\n width: 100%;\n height: 100%;\n overflow-x: hidden;\n overflow-y: auto;\n outline: 0;\n}\n\n.modal-dialog {\n position: relative;\n width: auto;\n margin: var(--bs-modal-margin);\n pointer-events: none;\n}\n.modal.fade .modal-dialog {\n transition: transform 0.3s ease-out;\n transform: translate(0, -50px);\n}\n@media (prefers-reduced-motion: reduce) {\n .modal.fade .modal-dialog {\n transition: none;\n }\n}\n.modal.show .modal-dialog {\n transform: none;\n}\n.modal.modal-static .modal-dialog {\n transform: scale(1.02);\n}\n\n.modal-dialog-scrollable {\n height: calc(100% - var(--bs-modal-margin) * 2);\n}\n.modal-dialog-scrollable .modal-content {\n max-height: 100%;\n overflow: hidden;\n}\n.modal-dialog-scrollable .modal-body {\n overflow-y: auto;\n}\n\n.modal-dialog-centered {\n display: flex;\n align-items: center;\n min-height: calc(100% - var(--bs-modal-margin) * 2);\n}\n\n.modal-content {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n color: var(--bs-modal-color);\n pointer-events: auto;\n background-color: var(--bs-modal-bg);\n background-clip: padding-box;\n border: var(--bs-modal-border-width) solid var(--bs-modal-border-color);\n border-radius: var(--bs-modal-border-radius);\n outline: 0;\n}\n\n.modal-backdrop {\n --bs-backdrop-zindex: 1050;\n --bs-backdrop-bg: #000;\n --bs-backdrop-opacity: 0.5;\n position: fixed;\n top: 0;\n left: 0;\n z-index: var(--bs-backdrop-zindex);\n width: 100vw;\n height: 100vh;\n background-color: var(--bs-backdrop-bg);\n}\n.modal-backdrop.fade {\n opacity: 0;\n}\n.modal-backdrop.show {\n opacity: var(--bs-backdrop-opacity);\n}\n\n.modal-header {\n display: flex;\n flex-shrink: 0;\n align-items: center;\n justify-content: space-between;\n padding: var(--bs-modal-header-padding);\n border-bottom: var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);\n border-top-left-radius: var(--bs-modal-inner-border-radius);\n border-top-right-radius: var(--bs-modal-inner-border-radius);\n}\n.modal-header .btn-close {\n padding: calc(var(--bs-modal-header-padding-y) * 0.5) calc(var(--bs-modal-header-padding-x) * 0.5);\n margin: calc(-0.5 * var(--bs-modal-header-padding-y)) calc(-0.5 * var(--bs-modal-header-padding-x)) calc(-0.5 * var(--bs-modal-header-padding-y)) auto;\n}\n\n.modal-title {\n margin-bottom: 0;\n line-height: var(--bs-modal-title-line-height);\n}\n\n.modal-body {\n position: relative;\n flex: 1 1 auto;\n padding: var(--bs-modal-padding);\n}\n\n.modal-footer {\n display: flex;\n flex-shrink: 0;\n flex-wrap: wrap;\n align-items: center;\n justify-content: flex-end;\n padding: calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * 0.5);\n background-color: var(--bs-modal-footer-bg);\n border-top: var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);\n border-bottom-right-radius: var(--bs-modal-inner-border-radius);\n border-bottom-left-radius: var(--bs-modal-inner-border-radius);\n}\n.modal-footer > * {\n margin: calc(var(--bs-modal-footer-gap) * 0.5);\n}\n\n@media (min-width: 576px) {\n .modal {\n --bs-modal-margin: 1.75rem;\n --bs-modal-box-shadow: 0 0.5rem 1rem rgba(var(--bs-body-color-rgb), 0.15);\n }\n .modal-dialog {\n max-width: var(--bs-modal-width);\n margin-right: auto;\n margin-left: auto;\n }\n .modal-sm {\n --bs-modal-width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg,\n .modal-xl {\n --bs-modal-width: 800px;\n }\n}\n@media (min-width: 1200px) {\n .modal-xl {\n --bs-modal-width: 1140px;\n }\n}\n.modal-fullscreen {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n}\n.modal-fullscreen .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n}\n.modal-fullscreen .modal-header,\n.modal-fullscreen .modal-footer {\n border-radius: 0;\n}\n.modal-fullscreen .modal-body {\n overflow-y: auto;\n}\n\n@media (max-width: 575.98px) {\n .modal-fullscreen-sm-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-sm-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-sm-down .modal-header,\n .modal-fullscreen-sm-down .modal-footer {\n border-radius: 0;\n }\n .modal-fullscreen-sm-down .modal-body {\n overflow-y: auto;\n }\n}\n@media (max-width: 767.98px) {\n .modal-fullscreen-md-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-md-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-md-down .modal-header,\n .modal-fullscreen-md-down .modal-footer {\n border-radius: 0;\n }\n .modal-fullscreen-md-down .modal-body {\n overflow-y: auto;\n }\n}\n@media (max-width: 991.98px) {\n .modal-fullscreen-lg-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-lg-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-lg-down .modal-header,\n .modal-fullscreen-lg-down .modal-footer {\n border-radius: 0;\n }\n .modal-fullscreen-lg-down .modal-body {\n overflow-y: auto;\n }\n}\n@media (max-width: 1199.98px) {\n .modal-fullscreen-xl-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-xl-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-xl-down .modal-header,\n .modal-fullscreen-xl-down .modal-footer {\n border-radius: 0;\n }\n .modal-fullscreen-xl-down .modal-body {\n overflow-y: auto;\n }\n}\n@media (max-width: 1399.98px) {\n .modal-fullscreen-xxl-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-xxl-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-xxl-down .modal-header,\n .modal-fullscreen-xxl-down .modal-footer {\n border-radius: 0;\n }\n .modal-fullscreen-xxl-down .modal-body {\n overflow-y: auto;\n }\n}\n.tooltip {\n --bs-tooltip-zindex: 1080;\n --bs-tooltip-max-width: 200px;\n --bs-tooltip-padding-x: 0.5rem;\n --bs-tooltip-padding-y: 0.25rem;\n --bs-tooltip-margin: ;\n --bs-tooltip-font-size: 0.875rem;\n --bs-tooltip-color: var(--bs-body-bg);\n --bs-tooltip-bg: var(--bs-emphasis-color);\n --bs-tooltip-border-radius: var(--bs-border-radius);\n --bs-tooltip-opacity: 0.9;\n --bs-tooltip-arrow-width: 0.8rem;\n --bs-tooltip-arrow-height: 0.4rem;\n z-index: var(--bs-tooltip-zindex);\n display: block;\n padding: var(--bs-tooltip-arrow-height);\n margin: var(--bs-tooltip-margin);\n font-family: var(--bs-font-sans-serif);\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n white-space: normal;\n word-spacing: normal;\n line-break: auto;\n font-size: var(--bs-tooltip-font-size);\n word-wrap: break-word;\n opacity: 0;\n}\n.tooltip.show {\n opacity: var(--bs-tooltip-opacity);\n}\n.tooltip .tooltip-arrow {\n display: block;\n width: var(--bs-tooltip-arrow-width);\n height: var(--bs-tooltip-arrow-height);\n}\n.tooltip .tooltip-arrow::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-tooltip-top .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow {\n bottom: 0;\n}\n.bs-tooltip-top .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before {\n top: -1px;\n border-width: var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * 0.5) 0;\n border-top-color: var(--bs-tooltip-bg);\n}\n\n/* rtl:begin:ignore */\n.bs-tooltip-end .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow {\n left: 0;\n width: var(--bs-tooltip-arrow-height);\n height: var(--bs-tooltip-arrow-width);\n}\n.bs-tooltip-end .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before {\n right: -1px;\n border-width: calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * 0.5) 0;\n border-right-color: var(--bs-tooltip-bg);\n}\n\n/* rtl:end:ignore */\n.bs-tooltip-bottom .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow {\n top: 0;\n}\n.bs-tooltip-bottom .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before {\n bottom: -1px;\n border-width: 0 calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height);\n border-bottom-color: var(--bs-tooltip-bg);\n}\n\n/* rtl:begin:ignore */\n.bs-tooltip-start .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow {\n right: 0;\n width: var(--bs-tooltip-arrow-height);\n height: var(--bs-tooltip-arrow-width);\n}\n.bs-tooltip-start .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before {\n left: -1px;\n border-width: calc(var(--bs-tooltip-arrow-width) * 0.5) 0 calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height);\n border-left-color: var(--bs-tooltip-bg);\n}\n\n/* rtl:end:ignore */\n.tooltip-inner {\n max-width: var(--bs-tooltip-max-width);\n padding: var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);\n color: var(--bs-tooltip-color);\n text-align: center;\n background-color: var(--bs-tooltip-bg);\n border-radius: var(--bs-tooltip-border-radius);\n}\n\n.popover {\n --bs-popover-zindex: 1070;\n --bs-popover-max-width: 276px;\n --bs-popover-font-size: 0.875rem;\n --bs-popover-bg: var(--bs-body-bg);\n --bs-popover-border-width: var(--bs-border-width);\n --bs-popover-border-color: var(--bs-border-color-translucent);\n --bs-popover-border-radius: var(--bs-border-radius-lg);\n --bs-popover-inner-border-radius: calc(var(--bs-border-radius-lg) - var(--bs-border-width));\n --bs-popover-box-shadow: 0 0.5rem 1rem rgba(var(--bs-body-color-rgb), 0.15);\n --bs-popover-header-padding-x: 1rem;\n --bs-popover-header-padding-y: 0.5rem;\n --bs-popover-header-font-size: 1rem;\n --bs-popover-header-color: ;\n --bs-popover-header-bg: var(--bs-secondary-bg);\n --bs-popover-body-padding-x: 1rem;\n --bs-popover-body-padding-y: 1rem;\n --bs-popover-body-color: var(--bs-body-color);\n --bs-popover-arrow-width: 1rem;\n --bs-popover-arrow-height: 0.5rem;\n --bs-popover-arrow-border: var(--bs-popover-border-color);\n z-index: var(--bs-popover-zindex);\n display: block;\n max-width: var(--bs-popover-max-width);\n font-family: var(--bs-font-sans-serif);\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n white-space: normal;\n word-spacing: normal;\n line-break: auto;\n font-size: var(--bs-popover-font-size);\n word-wrap: break-word;\n background-color: var(--bs-popover-bg);\n background-clip: padding-box;\n border: var(--bs-popover-border-width) solid var(--bs-popover-border-color);\n border-radius: var(--bs-popover-border-radius);\n}\n.popover .popover-arrow {\n display: block;\n width: var(--bs-popover-arrow-width);\n height: var(--bs-popover-arrow-height);\n}\n.popover .popover-arrow::before, .popover .popover-arrow::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n border-width: 0;\n}\n\n.bs-popover-top > .popover-arrow, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow {\n bottom: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));\n}\n.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before, .bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after {\n border-width: var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0;\n}\n.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before {\n bottom: 0;\n border-top-color: var(--bs-popover-arrow-border);\n}\n.bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after {\n bottom: var(--bs-popover-border-width);\n border-top-color: var(--bs-popover-bg);\n}\n\n/* rtl:begin:ignore */\n.bs-popover-end > .popover-arrow, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow {\n left: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));\n width: var(--bs-popover-arrow-height);\n height: var(--bs-popover-arrow-width);\n}\n.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before, .bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after {\n border-width: calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0;\n}\n.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before {\n left: 0;\n border-right-color: var(--bs-popover-arrow-border);\n}\n.bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after {\n left: var(--bs-popover-border-width);\n border-right-color: var(--bs-popover-bg);\n}\n\n/* rtl:end:ignore */\n.bs-popover-bottom > .popover-arrow, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow {\n top: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));\n}\n.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before, .bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after {\n border-width: 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height);\n}\n.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before {\n top: 0;\n border-bottom-color: var(--bs-popover-arrow-border);\n}\n.bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after {\n top: var(--bs-popover-border-width);\n border-bottom-color: var(--bs-popover-bg);\n}\n.bs-popover-bottom .popover-header::before, .bs-popover-auto[data-popper-placement^=bottom] .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: var(--bs-popover-arrow-width);\n margin-left: calc(-0.5 * var(--bs-popover-arrow-width));\n content: \"\";\n border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-header-bg);\n}\n\n/* rtl:begin:ignore */\n.bs-popover-start > .popover-arrow, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow {\n right: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));\n width: var(--bs-popover-arrow-height);\n height: var(--bs-popover-arrow-width);\n}\n.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before, .bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after {\n border-width: calc(var(--bs-popover-arrow-width) * 0.5) 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height);\n}\n.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before {\n right: 0;\n border-left-color: var(--bs-popover-arrow-border);\n}\n.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after {\n right: var(--bs-popover-border-width);\n border-left-color: var(--bs-popover-bg);\n}\n\n/* rtl:end:ignore */\n.popover-header {\n padding: var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);\n margin-bottom: 0;\n font-size: var(--bs-popover-header-font-size);\n color: var(--bs-popover-header-color);\n background-color: var(--bs-popover-header-bg);\n border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-border-color);\n border-top-left-radius: var(--bs-popover-inner-border-radius);\n border-top-right-radius: var(--bs-popover-inner-border-radius);\n}\n.popover-header:empty {\n display: none;\n}\n\n.popover-body {\n padding: var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);\n color: var(--bs-popover-body-color);\n}\n\n.carousel {\n position: relative;\n}\n\n.carousel.pointer-event {\n touch-action: pan-y;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n.carousel-inner::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.carousel-item {\n position: relative;\n display: none;\n float: left;\n width: 100%;\n margin-right: -100%;\n -webkit-backface-visibility: hidden;\n backface-visibility: hidden;\n transition: transform 0.6s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-item {\n transition: none;\n }\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n.carousel-item-next:not(.carousel-item-start),\n.active.carousel-item-end {\n transform: translateX(100%);\n}\n\n.carousel-item-prev:not(.carousel-item-end),\n.active.carousel-item-start {\n transform: translateX(-100%);\n}\n\n.carousel-fade .carousel-item {\n opacity: 0;\n transition-property: opacity;\n transform: none;\n}\n.carousel-fade .carousel-item.active,\n.carousel-fade .carousel-item-next.carousel-item-start,\n.carousel-fade .carousel-item-prev.carousel-item-end {\n z-index: 1;\n opacity: 1;\n}\n.carousel-fade .active.carousel-item-start,\n.carousel-fade .active.carousel-item-end {\n z-index: 0;\n opacity: 0;\n transition: opacity 0s 0.6s;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-fade .active.carousel-item-start,\n .carousel-fade .active.carousel-item-end {\n transition: none;\n }\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 15%;\n padding: 0;\n color: #fff;\n text-align: center;\n background: none;\n border: 0;\n opacity: 0.5;\n transition: opacity 0.15s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-control-prev,\n .carousel-control-next {\n transition: none;\n }\n}\n.carousel-control-prev:hover, .carousel-control-prev:focus,\n.carousel-control-next:hover,\n.carousel-control-next:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n opacity: 0.9;\n}\n\n.carousel-control-prev {\n left: 0;\n}\n\n.carousel-control-next {\n right: 0;\n}\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n background-repeat: no-repeat;\n background-position: 50%;\n background-size: 100% 100%;\n}\n\n/* rtl:options: {\n \"autoRename\": true,\n \"stringMap\":[ {\n \"name\" : \"prev-next\",\n \"search\" : \"prev\",\n \"replace\" : \"next\"\n } ]\n} */\n.carousel-control-prev-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e\");\n}\n\n.carousel-control-next-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\");\n}\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 2;\n display: flex;\n justify-content: center;\n padding: 0;\n margin-right: 15%;\n margin-bottom: 1rem;\n margin-left: 15%;\n list-style: none;\n}\n.carousel-indicators [data-bs-target] {\n box-sizing: content-box;\n flex: 0 1 auto;\n width: 30px;\n height: 3px;\n padding: 0;\n margin-right: 3px;\n margin-left: 3px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #fff;\n background-clip: padding-box;\n border: 0;\n border-top: 10px solid transparent;\n border-bottom: 10px solid transparent;\n opacity: 0.5;\n transition: opacity 0.6s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-indicators [data-bs-target] {\n transition: none;\n }\n}\n.carousel-indicators .active {\n opacity: 1;\n}\n\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 1.25rem;\n left: 15%;\n padding-top: 1.25rem;\n padding-bottom: 1.25rem;\n color: #fff;\n text-align: center;\n}\n\n.carousel-dark .carousel-control-prev-icon,\n.carousel-dark .carousel-control-next-icon {\n filter: invert(1) grayscale(100);\n}\n.carousel-dark .carousel-indicators [data-bs-target] {\n background-color: #000;\n}\n.carousel-dark .carousel-caption {\n color: #000;\n}\n\n[data-bs-theme=dark] .carousel .carousel-control-prev-icon,\n[data-bs-theme=dark] .carousel .carousel-control-next-icon {\n filter: invert(1) grayscale(100);\n}\n[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target] {\n background-color: #000;\n}\n[data-bs-theme=dark] .carousel .carousel-caption {\n color: #000;\n}\n\n.spinner-grow,\n.spinner-border {\n display: inline-block;\n width: var(--bs-spinner-width);\n height: var(--bs-spinner-height);\n vertical-align: var(--bs-spinner-vertical-align);\n border-radius: 50%;\n animation: var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name);\n}\n\n@keyframes spinner-border {\n to {\n transform: rotate(360deg) /* rtl:ignore */;\n }\n}\n.spinner-border {\n --bs-spinner-width: 2rem;\n --bs-spinner-height: 2rem;\n --bs-spinner-vertical-align: -0.125em;\n --bs-spinner-border-width: 0.25em;\n --bs-spinner-animation-speed: 0.75s;\n --bs-spinner-animation-name: spinner-border;\n border: var(--bs-spinner-border-width) solid currentcolor;\n border-right-color: transparent;\n}\n\n.spinner-border-sm {\n --bs-spinner-width: 1rem;\n --bs-spinner-height: 1rem;\n --bs-spinner-border-width: 0.2em;\n}\n\n@keyframes spinner-grow {\n 0% {\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n transform: none;\n }\n}\n.spinner-grow {\n --bs-spinner-width: 2rem;\n --bs-spinner-height: 2rem;\n --bs-spinner-vertical-align: -0.125em;\n --bs-spinner-animation-speed: 0.75s;\n --bs-spinner-animation-name: spinner-grow;\n background-color: currentcolor;\n opacity: 0;\n}\n\n.spinner-grow-sm {\n --bs-spinner-width: 1rem;\n --bs-spinner-height: 1rem;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .spinner-border,\n .spinner-grow {\n --bs-spinner-animation-speed: 1.5s;\n }\n}\n.offcanvas, .offcanvas-xxl, .offcanvas-xl, .offcanvas-lg, .offcanvas-md, .offcanvas-sm {\n --bs-offcanvas-zindex: 1045;\n --bs-offcanvas-width: 400px;\n --bs-offcanvas-height: 30vh;\n --bs-offcanvas-padding-x: 1rem;\n --bs-offcanvas-padding-y: 1rem;\n --bs-offcanvas-color: var(--bs-body-color);\n --bs-offcanvas-bg: var(--bs-body-bg);\n --bs-offcanvas-border-width: var(--bs-border-width);\n --bs-offcanvas-border-color: var(--bs-border-color-translucent);\n --bs-offcanvas-box-shadow: 0 0.125rem 0.25rem rgba(var(--bs-body-color-rgb), 0.075);\n --bs-offcanvas-transition: transform 0.3s ease-in-out;\n --bs-offcanvas-title-line-height: 1.5;\n}\n\n@media (max-width: 575.98px) {\n .offcanvas-sm {\n position: fixed;\n bottom: 0;\n z-index: var(--bs-offcanvas-zindex);\n display: flex;\n flex-direction: column;\n max-width: 100%;\n color: var(--bs-offcanvas-color);\n visibility: hidden;\n background-color: var(--bs-offcanvas-bg);\n background-clip: padding-box;\n outline: 0;\n transition: var(--bs-offcanvas-transition);\n }\n}\n@media (max-width: 575.98px) and (prefers-reduced-motion: reduce) {\n .offcanvas-sm {\n transition: none;\n }\n}\n@media (max-width: 575.98px) {\n .offcanvas-sm.offcanvas-start {\n top: 0;\n left: 0;\n width: var(--bs-offcanvas-width);\n border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(-100%);\n }\n}\n@media (max-width: 575.98px) {\n .offcanvas-sm.offcanvas-end {\n top: 0;\n right: 0;\n width: var(--bs-offcanvas-width);\n border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(100%);\n }\n}\n@media (max-width: 575.98px) {\n .offcanvas-sm.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(-100%);\n }\n}\n@media (max-width: 575.98px) {\n .offcanvas-sm.offcanvas-bottom {\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(100%);\n }\n}\n@media (max-width: 575.98px) {\n .offcanvas-sm.showing, .offcanvas-sm.show:not(.hiding) {\n transform: none;\n }\n}\n@media (max-width: 575.98px) {\n .offcanvas-sm.showing, .offcanvas-sm.hiding, .offcanvas-sm.show {\n visibility: visible;\n }\n}\n@media (min-width: 576px) {\n .offcanvas-sm {\n --bs-offcanvas-height: auto;\n --bs-offcanvas-border-width: 0;\n background-color: transparent !important;\n }\n .offcanvas-sm .offcanvas-header {\n display: none;\n }\n .offcanvas-sm .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n background-color: transparent !important;\n }\n}\n\n@media (max-width: 767.98px) {\n .offcanvas-md {\n position: fixed;\n bottom: 0;\n z-index: var(--bs-offcanvas-zindex);\n display: flex;\n flex-direction: column;\n max-width: 100%;\n color: var(--bs-offcanvas-color);\n visibility: hidden;\n background-color: var(--bs-offcanvas-bg);\n background-clip: padding-box;\n outline: 0;\n transition: var(--bs-offcanvas-transition);\n }\n}\n@media (max-width: 767.98px) and (prefers-reduced-motion: reduce) {\n .offcanvas-md {\n transition: none;\n }\n}\n@media (max-width: 767.98px) {\n .offcanvas-md.offcanvas-start {\n top: 0;\n left: 0;\n width: var(--bs-offcanvas-width);\n border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(-100%);\n }\n}\n@media (max-width: 767.98px) {\n .offcanvas-md.offcanvas-end {\n top: 0;\n right: 0;\n width: var(--bs-offcanvas-width);\n border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(100%);\n }\n}\n@media (max-width: 767.98px) {\n .offcanvas-md.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(-100%);\n }\n}\n@media (max-width: 767.98px) {\n .offcanvas-md.offcanvas-bottom {\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(100%);\n }\n}\n@media (max-width: 767.98px) {\n .offcanvas-md.showing, .offcanvas-md.show:not(.hiding) {\n transform: none;\n }\n}\n@media (max-width: 767.98px) {\n .offcanvas-md.showing, .offcanvas-md.hiding, .offcanvas-md.show {\n visibility: visible;\n }\n}\n@media (min-width: 768px) {\n .offcanvas-md {\n --bs-offcanvas-height: auto;\n --bs-offcanvas-border-width: 0;\n background-color: transparent !important;\n }\n .offcanvas-md .offcanvas-header {\n display: none;\n }\n .offcanvas-md .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n background-color: transparent !important;\n }\n}\n\n@media (max-width: 991.98px) {\n .offcanvas-lg {\n position: fixed;\n bottom: 0;\n z-index: var(--bs-offcanvas-zindex);\n display: flex;\n flex-direction: column;\n max-width: 100%;\n color: var(--bs-offcanvas-color);\n visibility: hidden;\n background-color: var(--bs-offcanvas-bg);\n background-clip: padding-box;\n outline: 0;\n transition: var(--bs-offcanvas-transition);\n }\n}\n@media (max-width: 991.98px) and (prefers-reduced-motion: reduce) {\n .offcanvas-lg {\n transition: none;\n }\n}\n@media (max-width: 991.98px) {\n .offcanvas-lg.offcanvas-start {\n top: 0;\n left: 0;\n width: var(--bs-offcanvas-width);\n border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(-100%);\n }\n}\n@media (max-width: 991.98px) {\n .offcanvas-lg.offcanvas-end {\n top: 0;\n right: 0;\n width: var(--bs-offcanvas-width);\n border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(100%);\n }\n}\n@media (max-width: 991.98px) {\n .offcanvas-lg.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(-100%);\n }\n}\n@media (max-width: 991.98px) {\n .offcanvas-lg.offcanvas-bottom {\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(100%);\n }\n}\n@media (max-width: 991.98px) {\n .offcanvas-lg.showing, .offcanvas-lg.show:not(.hiding) {\n transform: none;\n }\n}\n@media (max-width: 991.98px) {\n .offcanvas-lg.showing, .offcanvas-lg.hiding, .offcanvas-lg.show {\n visibility: visible;\n }\n}\n@media (min-width: 992px) {\n .offcanvas-lg {\n --bs-offcanvas-height: auto;\n --bs-offcanvas-border-width: 0;\n background-color: transparent !important;\n }\n .offcanvas-lg .offcanvas-header {\n display: none;\n }\n .offcanvas-lg .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n background-color: transparent !important;\n }\n}\n\n@media (max-width: 1199.98px) {\n .offcanvas-xl {\n position: fixed;\n bottom: 0;\n z-index: var(--bs-offcanvas-zindex);\n display: flex;\n flex-direction: column;\n max-width: 100%;\n color: var(--bs-offcanvas-color);\n visibility: hidden;\n background-color: var(--bs-offcanvas-bg);\n background-clip: padding-box;\n outline: 0;\n transition: var(--bs-offcanvas-transition);\n }\n}\n@media (max-width: 1199.98px) and (prefers-reduced-motion: reduce) {\n .offcanvas-xl {\n transition: none;\n }\n}\n@media (max-width: 1199.98px) {\n .offcanvas-xl.offcanvas-start {\n top: 0;\n left: 0;\n width: var(--bs-offcanvas-width);\n border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(-100%);\n }\n}\n@media (max-width: 1199.98px) {\n .offcanvas-xl.offcanvas-end {\n top: 0;\n right: 0;\n width: var(--bs-offcanvas-width);\n border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(100%);\n }\n}\n@media (max-width: 1199.98px) {\n .offcanvas-xl.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(-100%);\n }\n}\n@media (max-width: 1199.98px) {\n .offcanvas-xl.offcanvas-bottom {\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(100%);\n }\n}\n@media (max-width: 1199.98px) {\n .offcanvas-xl.showing, .offcanvas-xl.show:not(.hiding) {\n transform: none;\n }\n}\n@media (max-width: 1199.98px) {\n .offcanvas-xl.showing, .offcanvas-xl.hiding, .offcanvas-xl.show {\n visibility: visible;\n }\n}\n@media (min-width: 1200px) {\n .offcanvas-xl {\n --bs-offcanvas-height: auto;\n --bs-offcanvas-border-width: 0;\n background-color: transparent !important;\n }\n .offcanvas-xl .offcanvas-header {\n display: none;\n }\n .offcanvas-xl .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n background-color: transparent !important;\n }\n}\n\n@media (max-width: 1399.98px) {\n .offcanvas-xxl {\n position: fixed;\n bottom: 0;\n z-index: var(--bs-offcanvas-zindex);\n display: flex;\n flex-direction: column;\n max-width: 100%;\n color: var(--bs-offcanvas-color);\n visibility: hidden;\n background-color: var(--bs-offcanvas-bg);\n background-clip: padding-box;\n outline: 0;\n transition: var(--bs-offcanvas-transition);\n }\n}\n@media (max-width: 1399.98px) and (prefers-reduced-motion: reduce) {\n .offcanvas-xxl {\n transition: none;\n }\n}\n@media (max-width: 1399.98px) {\n .offcanvas-xxl.offcanvas-start {\n top: 0;\n left: 0;\n width: var(--bs-offcanvas-width);\n border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(-100%);\n }\n}\n@media (max-width: 1399.98px) {\n .offcanvas-xxl.offcanvas-end {\n top: 0;\n right: 0;\n width: var(--bs-offcanvas-width);\n border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(100%);\n }\n}\n@media (max-width: 1399.98px) {\n .offcanvas-xxl.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(-100%);\n }\n}\n@media (max-width: 1399.98px) {\n .offcanvas-xxl.offcanvas-bottom {\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(100%);\n }\n}\n@media (max-width: 1399.98px) {\n .offcanvas-xxl.showing, .offcanvas-xxl.show:not(.hiding) {\n transform: none;\n }\n}\n@media (max-width: 1399.98px) {\n .offcanvas-xxl.showing, .offcanvas-xxl.hiding, .offcanvas-xxl.show {\n visibility: visible;\n }\n}\n@media (min-width: 1400px) {\n .offcanvas-xxl {\n --bs-offcanvas-height: auto;\n --bs-offcanvas-border-width: 0;\n background-color: transparent !important;\n }\n .offcanvas-xxl .offcanvas-header {\n display: none;\n }\n .offcanvas-xxl .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n background-color: transparent !important;\n }\n}\n\n.offcanvas {\n position: fixed;\n bottom: 0;\n z-index: var(--bs-offcanvas-zindex);\n display: flex;\n flex-direction: column;\n max-width: 100%;\n color: var(--bs-offcanvas-color);\n visibility: hidden;\n background-color: var(--bs-offcanvas-bg);\n background-clip: padding-box;\n outline: 0;\n transition: var(--bs-offcanvas-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n .offcanvas {\n transition: none;\n }\n}\n.offcanvas.offcanvas-start {\n top: 0;\n left: 0;\n width: var(--bs-offcanvas-width);\n border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(-100%);\n}\n.offcanvas.offcanvas-end {\n top: 0;\n right: 0;\n width: var(--bs-offcanvas-width);\n border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(100%);\n}\n.offcanvas.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(-100%);\n}\n.offcanvas.offcanvas-bottom {\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(100%);\n}\n.offcanvas.showing, .offcanvas.show:not(.hiding) {\n transform: none;\n}\n.offcanvas.showing, .offcanvas.hiding, .offcanvas.show {\n visibility: visible;\n}\n\n.offcanvas-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1040;\n width: 100vw;\n height: 100vh;\n background-color: #000;\n}\n.offcanvas-backdrop.fade {\n opacity: 0;\n}\n.offcanvas-backdrop.show {\n opacity: 0.5;\n}\n\n.offcanvas-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);\n}\n.offcanvas-header .btn-close {\n padding: calc(var(--bs-offcanvas-padding-y) * 0.5) calc(var(--bs-offcanvas-padding-x) * 0.5);\n margin-top: calc(-0.5 * var(--bs-offcanvas-padding-y));\n margin-right: calc(-0.5 * var(--bs-offcanvas-padding-x));\n margin-bottom: calc(-0.5 * var(--bs-offcanvas-padding-y));\n}\n\n.offcanvas-title {\n margin-bottom: 0;\n line-height: var(--bs-offcanvas-title-line-height);\n}\n\n.offcanvas-body {\n flex-grow: 1;\n padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);\n overflow-y: auto;\n}\n\n.placeholder {\n display: inline-block;\n min-height: 1em;\n vertical-align: middle;\n cursor: wait;\n background-color: currentcolor;\n opacity: 0.5;\n}\n.placeholder.btn::before {\n display: inline-block;\n content: \"\";\n}\n\n.placeholder-xs {\n min-height: 0.6em;\n}\n\n.placeholder-sm {\n min-height: 0.8em;\n}\n\n.placeholder-lg {\n min-height: 1.2em;\n}\n\n.placeholder-glow .placeholder {\n animation: placeholder-glow 2s ease-in-out infinite;\n}\n\n@keyframes placeholder-glow {\n 50% {\n opacity: 0.2;\n }\n}\n.placeholder-wave {\n -webkit-mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);\n mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);\n -webkit-mask-size: 200% 100%;\n mask-size: 200% 100%;\n animation: placeholder-wave 2s linear infinite;\n}\n\n@keyframes placeholder-wave {\n 100% {\n -webkit-mask-position: -200% 0%;\n mask-position: -200% 0%;\n }\n}\n.clearfix::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.text-bg-primary {\n color: #fff !important;\n background-color: RGBA(13, 110, 253, var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-secondary {\n color: #fff !important;\n background-color: RGBA(108, 117, 125, var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-success {\n color: #fff !important;\n background-color: RGBA(25, 135, 84, var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-info {\n color: #000 !important;\n background-color: RGBA(13, 202, 240, var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-warning {\n color: #000 !important;\n background-color: RGBA(255, 193, 7, var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-danger {\n color: #fff !important;\n background-color: RGBA(220, 53, 69, var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-light {\n color: #000 !important;\n background-color: RGBA(248, 249, 250, var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-dark {\n color: #fff !important;\n background-color: RGBA(33, 37, 41, var(--bs-bg-opacity, 1)) !important;\n}\n\n.link-primary {\n color: #0d6efd !important;\n}\n.link-primary:hover, .link-primary:focus {\n color: #0a58ca !important;\n}\n\n.link-secondary {\n color: #6c757d !important;\n}\n.link-secondary:hover, .link-secondary:focus {\n color: #565e64 !important;\n}\n\n.link-success {\n color: #198754 !important;\n}\n.link-success:hover, .link-success:focus {\n color: #146c43 !important;\n}\n\n.link-info {\n color: #0dcaf0 !important;\n}\n.link-info:hover, .link-info:focus {\n color: #3dd5f3 !important;\n}\n\n.link-warning {\n color: #ffc107 !important;\n}\n.link-warning:hover, .link-warning:focus {\n color: #ffcd39 !important;\n}\n\n.link-danger {\n color: #dc3545 !important;\n}\n.link-danger:hover, .link-danger:focus {\n color: #b02a37 !important;\n}\n\n.link-light {\n color: #f8f9fa !important;\n}\n.link-light:hover, .link-light:focus {\n color: #f9fafb !important;\n}\n\n.link-dark {\n color: #212529 !important;\n}\n.link-dark:hover, .link-dark:focus {\n color: #1a1e21 !important;\n}\n\n.ratio {\n position: relative;\n width: 100%;\n}\n.ratio::before {\n display: block;\n padding-top: var(--bs-aspect-ratio);\n content: \"\";\n}\n.ratio > * {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n}\n\n.ratio-1x1 {\n --bs-aspect-ratio: 100%;\n}\n\n.ratio-4x3 {\n --bs-aspect-ratio: 75%;\n}\n\n.ratio-16x9 {\n --bs-aspect-ratio: 56.25%;\n}\n\n.ratio-21x9 {\n --bs-aspect-ratio: 42.8571428571%;\n}\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1030;\n}\n\n.sticky-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n}\n\n.sticky-bottom {\n position: -webkit-sticky;\n position: sticky;\n bottom: 0;\n z-index: 1020;\n}\n\n@media (min-width: 576px) {\n .sticky-sm-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n .sticky-sm-bottom {\n position: -webkit-sticky;\n position: sticky;\n bottom: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 768px) {\n .sticky-md-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n .sticky-md-bottom {\n position: -webkit-sticky;\n position: sticky;\n bottom: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 992px) {\n .sticky-lg-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n .sticky-lg-bottom {\n position: -webkit-sticky;\n position: sticky;\n bottom: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 1200px) {\n .sticky-xl-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n .sticky-xl-bottom {\n position: -webkit-sticky;\n position: sticky;\n bottom: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 1400px) {\n .sticky-xxl-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n .sticky-xxl-bottom {\n position: -webkit-sticky;\n position: sticky;\n bottom: 0;\n z-index: 1020;\n }\n}\n.hstack {\n display: flex;\n flex-direction: row;\n align-items: center;\n align-self: stretch;\n}\n\n.vstack {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n align-self: stretch;\n}\n\n.visually-hidden,\n.visually-hidden-focusable:not(:focus):not(:focus-within) {\n position: absolute !important;\n width: 1px !important;\n height: 1px !important;\n padding: 0 !important;\n margin: -1px !important;\n overflow: hidden !important;\n clip: rect(0, 0, 0, 0) !important;\n white-space: nowrap !important;\n border: 0 !important;\n}\n\n.stretched-link::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1;\n content: \"\";\n}\n\n.text-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.vr {\n display: inline-block;\n align-self: stretch;\n width: 1px;\n min-height: 1em;\n background-color: currentcolor;\n opacity: 0.25;\n}\n\n.align-baseline {\n vertical-align: baseline !important;\n}\n\n.align-top {\n vertical-align: top !important;\n}\n\n.align-middle {\n vertical-align: middle !important;\n}\n\n.align-bottom {\n vertical-align: bottom !important;\n}\n\n.align-text-bottom {\n vertical-align: text-bottom !important;\n}\n\n.align-text-top {\n vertical-align: text-top !important;\n}\n\n.float-start {\n float: left !important;\n}\n\n.float-end {\n float: right !important;\n}\n\n.float-none {\n float: none !important;\n}\n\n.object-fit-contain {\n -o-object-fit: contain !important;\n object-fit: contain !important;\n}\n\n.object-fit-cover {\n -o-object-fit: cover !important;\n object-fit: cover !important;\n}\n\n.object-fit-fill {\n -o-object-fit: fill !important;\n object-fit: fill !important;\n}\n\n.object-fit-scale {\n -o-object-fit: scale-down !important;\n object-fit: scale-down !important;\n}\n\n.object-fit-none {\n -o-object-fit: none !important;\n object-fit: none !important;\n}\n\n.opacity-0 {\n opacity: 0 !important;\n}\n\n.opacity-25 {\n opacity: 0.25 !important;\n}\n\n.opacity-50 {\n opacity: 0.5 !important;\n}\n\n.opacity-75 {\n opacity: 0.75 !important;\n}\n\n.opacity-100 {\n opacity: 1 !important;\n}\n\n.overflow-auto {\n overflow: auto !important;\n}\n\n.overflow-hidden {\n overflow: hidden !important;\n}\n\n.overflow-visible {\n overflow: visible !important;\n}\n\n.overflow-scroll {\n overflow: scroll !important;\n}\n\n.overflow-x-auto {\n overflow-x: auto !important;\n}\n\n.overflow-x-hidden {\n overflow-x: hidden !important;\n}\n\n.overflow-x-visible {\n overflow-x: visible !important;\n}\n\n.overflow-x-scroll {\n overflow-x: scroll !important;\n}\n\n.overflow-y-auto {\n overflow-y: auto !important;\n}\n\n.overflow-y-hidden {\n overflow-y: hidden !important;\n}\n\n.overflow-y-visible {\n overflow-y: visible !important;\n}\n\n.overflow-y-scroll {\n overflow-y: scroll !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-grid {\n display: grid !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n.d-none {\n display: none !important;\n}\n\n.shadow {\n box-shadow: 0 0.5rem 1rem rgba(var(--bs-body-color-rgb), 0.15) !important;\n}\n\n.shadow-sm {\n box-shadow: 0 0.125rem 0.25rem rgba(var(--bs-body-color-rgb), 0.075) !important;\n}\n\n.shadow-lg {\n box-shadow: 0 1rem 3rem rgba(var(--bs-body-color-rgb), 0.175) !important;\n}\n\n.shadow-none {\n box-shadow: none !important;\n}\n\n.position-static {\n position: static !important;\n}\n\n.position-relative {\n position: relative !important;\n}\n\n.position-absolute {\n position: absolute !important;\n}\n\n.position-fixed {\n position: fixed !important;\n}\n\n.position-sticky {\n position: -webkit-sticky !important;\n position: sticky !important;\n}\n\n.top-0 {\n top: 0 !important;\n}\n\n.top-50 {\n top: 50% !important;\n}\n\n.top-100 {\n top: 100% !important;\n}\n\n.bottom-0 {\n bottom: 0 !important;\n}\n\n.bottom-50 {\n bottom: 50% !important;\n}\n\n.bottom-100 {\n bottom: 100% !important;\n}\n\n.start-0 {\n left: 0 !important;\n}\n\n.start-50 {\n left: 50% !important;\n}\n\n.start-100 {\n left: 100% !important;\n}\n\n.end-0 {\n right: 0 !important;\n}\n\n.end-50 {\n right: 50% !important;\n}\n\n.end-100 {\n right: 100% !important;\n}\n\n.translate-middle {\n transform: translate(-50%, -50%) !important;\n}\n\n.translate-middle-x {\n transform: translateX(-50%) !important;\n}\n\n.translate-middle-y {\n transform: translateY(-50%) !important;\n}\n\n.border {\n border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-0 {\n border: 0 !important;\n}\n\n.border-top {\n border-top: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-top-0 {\n border-top: 0 !important;\n}\n\n.border-end {\n border-right: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-end-0 {\n border-right: 0 !important;\n}\n\n.border-bottom {\n border-bottom: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-bottom-0 {\n border-bottom: 0 !important;\n}\n\n.border-start {\n border-left: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-start-0 {\n border-left: 0 !important;\n}\n\n.border-primary {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-primary-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-secondary {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-secondary-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-success {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-success-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-info {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-info-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-warning {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-warning-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-danger {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-danger-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-light {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-light-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-dark {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-dark-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-white {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-white-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-primary-subtle {\n border-color: var(--bs-primary-border-subtle) !important;\n}\n\n.border-secondary-subtle {\n border-color: var(--bs-secondary-border-subtle) !important;\n}\n\n.border-success-subtle {\n border-color: var(--bs-success-border-subtle) !important;\n}\n\n.border-info-subtle {\n border-color: var(--bs-info-border-subtle) !important;\n}\n\n.border-warning-subtle {\n border-color: var(--bs-warning-border-subtle) !important;\n}\n\n.border-danger-subtle {\n border-color: var(--bs-danger-border-subtle) !important;\n}\n\n.border-light-subtle {\n border-color: var(--bs-light-border-subtle) !important;\n}\n\n.border-dark-subtle {\n border-color: var(--bs-dark-border-subtle) !important;\n}\n\n.border-1 {\n --bs-border-width: 1px;\n}\n\n.border-2 {\n --bs-border-width: 2px;\n}\n\n.border-3 {\n --bs-border-width: 3px;\n}\n\n.border-4 {\n --bs-border-width: 4px;\n}\n\n.border-5 {\n --bs-border-width: 5px;\n}\n\n.border-opacity-10 {\n --bs-border-opacity: 0.1;\n}\n\n.border-opacity-25 {\n --bs-border-opacity: 0.25;\n}\n\n.border-opacity-50 {\n --bs-border-opacity: 0.5;\n}\n\n.border-opacity-75 {\n --bs-border-opacity: 0.75;\n}\n\n.border-opacity-100 {\n --bs-border-opacity: 1;\n}\n\n.w-25 {\n width: 25% !important;\n}\n\n.w-50 {\n width: 50% !important;\n}\n\n.w-75 {\n width: 75% !important;\n}\n\n.w-100 {\n width: 100% !important;\n}\n\n.w-auto {\n width: auto !important;\n}\n\n.mw-100 {\n max-width: 100% !important;\n}\n\n.vw-100 {\n width: 100vw !important;\n}\n\n.min-vw-100 {\n min-width: 100vw !important;\n}\n\n.h-25 {\n height: 25% !important;\n}\n\n.h-50 {\n height: 50% !important;\n}\n\n.h-75 {\n height: 75% !important;\n}\n\n.h-100 {\n height: 100% !important;\n}\n\n.h-auto {\n height: auto !important;\n}\n\n.mh-100 {\n max-height: 100% !important;\n}\n\n.vh-100 {\n height: 100vh !important;\n}\n\n.min-vh-100 {\n min-height: 100vh !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.justify-content-evenly {\n justify-content: space-evenly !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n.order-first {\n order: -1 !important;\n}\n\n.order-0 {\n order: 0 !important;\n}\n\n.order-1 {\n order: 1 !important;\n}\n\n.order-2 {\n order: 2 !important;\n}\n\n.order-3 {\n order: 3 !important;\n}\n\n.order-4 {\n order: 4 !important;\n}\n\n.order-5 {\n order: 5 !important;\n}\n\n.order-last {\n order: 6 !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mx-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n}\n\n.mx-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n}\n\n.mx-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n}\n\n.mx-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n}\n\n.mx-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n}\n\n.mx-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n}\n\n.mx-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n}\n\n.my-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n}\n\n.my-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n}\n\n.my-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n}\n\n.my-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n}\n\n.my-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n}\n\n.my-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n}\n\n.my-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n}\n\n.mt-0 {\n margin-top: 0 !important;\n}\n\n.mt-1 {\n margin-top: 0.25rem !important;\n}\n\n.mt-2 {\n margin-top: 0.5rem !important;\n}\n\n.mt-3 {\n margin-top: 1rem !important;\n}\n\n.mt-4 {\n margin-top: 1.5rem !important;\n}\n\n.mt-5 {\n margin-top: 3rem !important;\n}\n\n.mt-auto {\n margin-top: auto !important;\n}\n\n.me-0 {\n margin-right: 0 !important;\n}\n\n.me-1 {\n margin-right: 0.25rem !important;\n}\n\n.me-2 {\n margin-right: 0.5rem !important;\n}\n\n.me-3 {\n margin-right: 1rem !important;\n}\n\n.me-4 {\n margin-right: 1.5rem !important;\n}\n\n.me-5 {\n margin-right: 3rem !important;\n}\n\n.me-auto {\n margin-right: auto !important;\n}\n\n.mb-0 {\n margin-bottom: 0 !important;\n}\n\n.mb-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.mb-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.mb-3 {\n margin-bottom: 1rem !important;\n}\n\n.mb-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.mb-5 {\n margin-bottom: 3rem !important;\n}\n\n.mb-auto {\n margin-bottom: auto !important;\n}\n\n.ms-0 {\n margin-left: 0 !important;\n}\n\n.ms-1 {\n margin-left: 0.25rem !important;\n}\n\n.ms-2 {\n margin-left: 0.5rem !important;\n}\n\n.ms-3 {\n margin-left: 1rem !important;\n}\n\n.ms-4 {\n margin-left: 1.5rem !important;\n}\n\n.ms-5 {\n margin-left: 3rem !important;\n}\n\n.ms-auto {\n margin-left: auto !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.px-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n}\n\n.px-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n}\n\n.px-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n}\n\n.px-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n}\n\n.px-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n}\n\n.px-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n}\n\n.py-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n}\n\n.py-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n}\n\n.py-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n}\n\n.py-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n}\n\n.py-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n}\n\n.py-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n}\n\n.pt-0 {\n padding-top: 0 !important;\n}\n\n.pt-1 {\n padding-top: 0.25rem !important;\n}\n\n.pt-2 {\n padding-top: 0.5rem !important;\n}\n\n.pt-3 {\n padding-top: 1rem !important;\n}\n\n.pt-4 {\n padding-top: 1.5rem !important;\n}\n\n.pt-5 {\n padding-top: 3rem !important;\n}\n\n.pe-0 {\n padding-right: 0 !important;\n}\n\n.pe-1 {\n padding-right: 0.25rem !important;\n}\n\n.pe-2 {\n padding-right: 0.5rem !important;\n}\n\n.pe-3 {\n padding-right: 1rem !important;\n}\n\n.pe-4 {\n padding-right: 1.5rem !important;\n}\n\n.pe-5 {\n padding-right: 3rem !important;\n}\n\n.pb-0 {\n padding-bottom: 0 !important;\n}\n\n.pb-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pb-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pb-3 {\n padding-bottom: 1rem !important;\n}\n\n.pb-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pb-5 {\n padding-bottom: 3rem !important;\n}\n\n.ps-0 {\n padding-left: 0 !important;\n}\n\n.ps-1 {\n padding-left: 0.25rem !important;\n}\n\n.ps-2 {\n padding-left: 0.5rem !important;\n}\n\n.ps-3 {\n padding-left: 1rem !important;\n}\n\n.ps-4 {\n padding-left: 1.5rem !important;\n}\n\n.ps-5 {\n padding-left: 3rem !important;\n}\n\n.gap-0 {\n gap: 0 !important;\n}\n\n.gap-1 {\n gap: 0.25rem !important;\n}\n\n.gap-2 {\n gap: 0.5rem !important;\n}\n\n.gap-3 {\n gap: 1rem !important;\n}\n\n.gap-4 {\n gap: 1.5rem !important;\n}\n\n.gap-5 {\n gap: 3rem !important;\n}\n\n.row-gap-0 {\n row-gap: 0 !important;\n}\n\n.row-gap-1 {\n row-gap: 0.25rem !important;\n}\n\n.row-gap-2 {\n row-gap: 0.5rem !important;\n}\n\n.row-gap-3 {\n row-gap: 1rem !important;\n}\n\n.row-gap-4 {\n row-gap: 1.5rem !important;\n}\n\n.row-gap-5 {\n row-gap: 3rem !important;\n}\n\n.column-gap-0 {\n -moz-column-gap: 0 !important;\n column-gap: 0 !important;\n}\n\n.column-gap-1 {\n -moz-column-gap: 0.25rem !important;\n column-gap: 0.25rem !important;\n}\n\n.column-gap-2 {\n -moz-column-gap: 0.5rem !important;\n column-gap: 0.5rem !important;\n}\n\n.column-gap-3 {\n -moz-column-gap: 1rem !important;\n column-gap: 1rem !important;\n}\n\n.column-gap-4 {\n -moz-column-gap: 1.5rem !important;\n column-gap: 1.5rem !important;\n}\n\n.column-gap-5 {\n -moz-column-gap: 3rem !important;\n column-gap: 3rem !important;\n}\n\n.font-monospace {\n font-family: var(--bs-font-monospace) !important;\n}\n\n.fs-1 {\n font-size: calc(1.375rem + 1.5vw) !important;\n}\n\n.fs-2 {\n font-size: calc(1.325rem + 0.9vw) !important;\n}\n\n.fs-3 {\n font-size: calc(1.3rem + 0.6vw) !important;\n}\n\n.fs-4 {\n font-size: calc(1.275rem + 0.3vw) !important;\n}\n\n.fs-5 {\n font-size: 1.25rem !important;\n}\n\n.fs-6 {\n font-size: 1rem !important;\n}\n\n.fst-italic {\n font-style: italic !important;\n}\n\n.fst-normal {\n font-style: normal !important;\n}\n\n.fw-lighter {\n font-weight: lighter !important;\n}\n\n.fw-light {\n font-weight: 300 !important;\n}\n\n.fw-normal {\n font-weight: 400 !important;\n}\n\n.fw-medium {\n font-weight: 500 !important;\n}\n\n.fw-semibold {\n font-weight: 600 !important;\n}\n\n.fw-bold {\n font-weight: 700 !important;\n}\n\n.fw-bolder {\n font-weight: bolder !important;\n}\n\n.lh-1 {\n line-height: 1 !important;\n}\n\n.lh-sm {\n line-height: 1.25 !important;\n}\n\n.lh-base {\n line-height: 1.5 !important;\n}\n\n.lh-lg {\n line-height: 2 !important;\n}\n\n.text-start {\n text-align: left !important;\n}\n\n.text-end {\n text-align: right !important;\n}\n\n.text-center {\n text-align: center !important;\n}\n\n.text-decoration-none {\n text-decoration: none !important;\n}\n\n.text-decoration-underline {\n text-decoration: underline !important;\n}\n\n.text-decoration-line-through {\n text-decoration: line-through !important;\n}\n\n.text-lowercase {\n text-transform: lowercase !important;\n}\n\n.text-uppercase {\n text-transform: uppercase !important;\n}\n\n.text-capitalize {\n text-transform: capitalize !important;\n}\n\n.text-wrap {\n white-space: normal !important;\n}\n\n.text-nowrap {\n white-space: nowrap !important;\n}\n\n/* rtl:begin:remove */\n.text-break {\n word-wrap: break-word !important;\n word-break: break-word !important;\n}\n\n/* rtl:end:remove */\n.text-primary {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-secondary {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-success {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-info {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-warning {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-danger {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-light {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-dark {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-black {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-white {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-body {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-muted {\n --bs-text-opacity: 1;\n color: var(--bs-secondary-color) !important;\n}\n\n.text-black-50 {\n --bs-text-opacity: 1;\n color: rgba(0, 0, 0, 0.5) !important;\n}\n\n.text-white-50 {\n --bs-text-opacity: 1;\n color: rgba(255, 255, 255, 0.5) !important;\n}\n\n.text-body-secondary {\n --bs-text-opacity: 1;\n color: var(--bs-secondary-color) !important;\n}\n\n.text-body-tertiary {\n --bs-text-opacity: 1;\n color: var(--bs-tertiary-color) !important;\n}\n\n.text-body-emphasis {\n --bs-text-opacity: 1;\n color: var(--bs-emphasis-color) !important;\n}\n\n.text-reset {\n --bs-text-opacity: 1;\n color: inherit !important;\n}\n\n.text-opacity-25 {\n --bs-text-opacity: 0.25;\n}\n\n.text-opacity-50 {\n --bs-text-opacity: 0.5;\n}\n\n.text-opacity-75 {\n --bs-text-opacity: 0.75;\n}\n\n.text-opacity-100 {\n --bs-text-opacity: 1;\n}\n\n.text-primary-emphasis {\n color: var(--bs-primary-text) !important;\n}\n\n.text-secondary-emphasis {\n color: var(--bs-secondary-text) !important;\n}\n\n.text-success-emphasis {\n color: var(--bs-success-text) !important;\n}\n\n.text-info-emphasis {\n color: var(--bs-info-text) !important;\n}\n\n.text-warning-emphasis {\n color: var(--bs-warning-text) !important;\n}\n\n.text-danger-emphasis {\n color: var(--bs-danger-text) !important;\n}\n\n.text-light-emphasis {\n color: var(--bs-light-text) !important;\n}\n\n.text-dark-emphasis {\n color: var(--bs-dark-text) !important;\n}\n\n.bg-primary {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-secondary {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-success {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-info {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-warning {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-danger {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-light {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-dark {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-black {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-white {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-body {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-transparent {\n --bs-bg-opacity: 1;\n background-color: transparent !important;\n}\n\n.bg-body-secondary {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-secondary-bg-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-body-tertiary {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-tertiary-bg-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-body-emphasis {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-emphasis-bg-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-opacity-10 {\n --bs-bg-opacity: 0.1;\n}\n\n.bg-opacity-25 {\n --bs-bg-opacity: 0.25;\n}\n\n.bg-opacity-50 {\n --bs-bg-opacity: 0.5;\n}\n\n.bg-opacity-75 {\n --bs-bg-opacity: 0.75;\n}\n\n.bg-opacity-100 {\n --bs-bg-opacity: 1;\n}\n\n.bg-primary-subtle {\n background-color: var(--bs-primary-bg-subtle) !important;\n}\n\n.bg-secondary-subtle {\n background-color: var(--bs-secondary-bg-subtle) !important;\n}\n\n.bg-success-subtle {\n background-color: var(--bs-success-bg-subtle) !important;\n}\n\n.bg-info-subtle {\n background-color: var(--bs-info-bg-subtle) !important;\n}\n\n.bg-warning-subtle {\n background-color: var(--bs-warning-bg-subtle) !important;\n}\n\n.bg-danger-subtle {\n background-color: var(--bs-danger-bg-subtle) !important;\n}\n\n.bg-light-subtle {\n background-color: var(--bs-light-bg-subtle) !important;\n}\n\n.bg-dark-subtle {\n background-color: var(--bs-dark-bg-subtle) !important;\n}\n\n.bg-gradient {\n background-image: var(--bs-gradient) !important;\n}\n\n.user-select-all {\n -webkit-user-select: all !important;\n -moz-user-select: all !important;\n user-select: all !important;\n}\n\n.user-select-auto {\n -webkit-user-select: auto !important;\n -moz-user-select: auto !important;\n user-select: auto !important;\n}\n\n.user-select-none {\n -webkit-user-select: none !important;\n -moz-user-select: none !important;\n user-select: none !important;\n}\n\n.pe-none {\n pointer-events: none !important;\n}\n\n.pe-auto {\n pointer-events: auto !important;\n}\n\n.rounded {\n border-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-0 {\n border-radius: 0 !important;\n}\n\n.rounded-1 {\n border-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-2 {\n border-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-3 {\n border-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-4 {\n border-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-5 {\n border-radius: var(--bs-border-radius-2xl) !important;\n}\n\n.rounded-circle {\n border-radius: 50% !important;\n}\n\n.rounded-pill {\n border-radius: var(--bs-border-radius-pill) !important;\n}\n\n.rounded-top {\n border-top-left-radius: var(--bs-border-radius) !important;\n border-top-right-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-top-0 {\n border-top-left-radius: 0 !important;\n border-top-right-radius: 0 !important;\n}\n\n.rounded-top-1 {\n border-top-left-radius: var(--bs-border-radius-sm) !important;\n border-top-right-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-top-2 {\n border-top-left-radius: var(--bs-border-radius) !important;\n border-top-right-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-top-3 {\n border-top-left-radius: var(--bs-border-radius-lg) !important;\n border-top-right-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-top-4 {\n border-top-left-radius: var(--bs-border-radius-xl) !important;\n border-top-right-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-top-5 {\n border-top-left-radius: var(--bs-border-radius-2xl) !important;\n border-top-right-radius: var(--bs-border-radius-2xl) !important;\n}\n\n.rounded-top-circle {\n border-top-left-radius: 50% !important;\n border-top-right-radius: 50% !important;\n}\n\n.rounded-top-pill {\n border-top-left-radius: var(--bs-border-radius-pill) !important;\n border-top-right-radius: var(--bs-border-radius-pill) !important;\n}\n\n.rounded-end {\n border-top-right-radius: var(--bs-border-radius) !important;\n border-bottom-right-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-end-0 {\n border-top-right-radius: 0 !important;\n border-bottom-right-radius: 0 !important;\n}\n\n.rounded-end-1 {\n border-top-right-radius: var(--bs-border-radius-sm) !important;\n border-bottom-right-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-end-2 {\n border-top-right-radius: var(--bs-border-radius) !important;\n border-bottom-right-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-end-3 {\n border-top-right-radius: var(--bs-border-radius-lg) !important;\n border-bottom-right-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-end-4 {\n border-top-right-radius: var(--bs-border-radius-xl) !important;\n border-bottom-right-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-end-5 {\n border-top-right-radius: var(--bs-border-radius-2xl) !important;\n border-bottom-right-radius: var(--bs-border-radius-2xl) !important;\n}\n\n.rounded-end-circle {\n border-top-right-radius: 50% !important;\n border-bottom-right-radius: 50% !important;\n}\n\n.rounded-end-pill {\n border-top-right-radius: var(--bs-border-radius-pill) !important;\n border-bottom-right-radius: var(--bs-border-radius-pill) !important;\n}\n\n.rounded-bottom {\n border-bottom-right-radius: var(--bs-border-radius) !important;\n border-bottom-left-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-bottom-0 {\n border-bottom-right-radius: 0 !important;\n border-bottom-left-radius: 0 !important;\n}\n\n.rounded-bottom-1 {\n border-bottom-right-radius: var(--bs-border-radius-sm) !important;\n border-bottom-left-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-bottom-2 {\n border-bottom-right-radius: var(--bs-border-radius) !important;\n border-bottom-left-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-bottom-3 {\n border-bottom-right-radius: var(--bs-border-radius-lg) !important;\n border-bottom-left-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-bottom-4 {\n border-bottom-right-radius: var(--bs-border-radius-xl) !important;\n border-bottom-left-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-bottom-5 {\n border-bottom-right-radius: var(--bs-border-radius-2xl) !important;\n border-bottom-left-radius: var(--bs-border-radius-2xl) !important;\n}\n\n.rounded-bottom-circle {\n border-bottom-right-radius: 50% !important;\n border-bottom-left-radius: 50% !important;\n}\n\n.rounded-bottom-pill {\n border-bottom-right-radius: var(--bs-border-radius-pill) !important;\n border-bottom-left-radius: var(--bs-border-radius-pill) !important;\n}\n\n.rounded-start {\n border-bottom-left-radius: var(--bs-border-radius) !important;\n border-top-left-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-start-0 {\n border-bottom-left-radius: 0 !important;\n border-top-left-radius: 0 !important;\n}\n\n.rounded-start-1 {\n border-bottom-left-radius: var(--bs-border-radius-sm) !important;\n border-top-left-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-start-2 {\n border-bottom-left-radius: var(--bs-border-radius) !important;\n border-top-left-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-start-3 {\n border-bottom-left-radius: var(--bs-border-radius-lg) !important;\n border-top-left-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-start-4 {\n border-bottom-left-radius: var(--bs-border-radius-xl) !important;\n border-top-left-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-start-5 {\n border-bottom-left-radius: var(--bs-border-radius-2xl) !important;\n border-top-left-radius: var(--bs-border-radius-2xl) !important;\n}\n\n.rounded-start-circle {\n border-bottom-left-radius: 50% !important;\n border-top-left-radius: 50% !important;\n}\n\n.rounded-start-pill {\n border-bottom-left-radius: var(--bs-border-radius-pill) !important;\n border-top-left-radius: var(--bs-border-radius-pill) !important;\n}\n\n.visible {\n visibility: visible !important;\n}\n\n.invisible {\n visibility: hidden !important;\n}\n\n.z-n1 {\n z-index: -1 !important;\n}\n\n.z-0 {\n z-index: 0 !important;\n}\n\n.z-1 {\n z-index: 1 !important;\n}\n\n.z-2 {\n z-index: 2 !important;\n}\n\n.z-3 {\n z-index: 3 !important;\n}\n\n@media (min-width: 576px) {\n .float-sm-start {\n float: left !important;\n }\n .float-sm-end {\n float: right !important;\n }\n .float-sm-none {\n float: none !important;\n }\n .object-fit-sm-contain {\n -o-object-fit: contain !important;\n object-fit: contain !important;\n }\n .object-fit-sm-cover {\n -o-object-fit: cover !important;\n object-fit: cover !important;\n }\n .object-fit-sm-fill {\n -o-object-fit: fill !important;\n object-fit: fill !important;\n }\n .object-fit-sm-scale {\n -o-object-fit: scale-down !important;\n object-fit: scale-down !important;\n }\n .object-fit-sm-none {\n -o-object-fit: none !important;\n object-fit: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-grid {\n display: grid !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n .d-sm-none {\n display: none !important;\n }\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n .flex-sm-row {\n flex-direction: row !important;\n }\n .flex-sm-column {\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n justify-content: center !important;\n }\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n .justify-content-sm-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n align-items: center !important;\n }\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n align-content: center !important;\n }\n .align-content-sm-between {\n align-content: space-between !important;\n }\n .align-content-sm-around {\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n align-self: auto !important;\n }\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n align-self: center !important;\n }\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n .order-sm-first {\n order: -1 !important;\n }\n .order-sm-0 {\n order: 0 !important;\n }\n .order-sm-1 {\n order: 1 !important;\n }\n .order-sm-2 {\n order: 2 !important;\n }\n .order-sm-3 {\n order: 3 !important;\n }\n .order-sm-4 {\n order: 4 !important;\n }\n .order-sm-5 {\n order: 5 !important;\n }\n .order-sm-last {\n order: 6 !important;\n }\n .m-sm-0 {\n margin: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mx-sm-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-sm-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-sm-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-sm-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-sm-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-sm-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-sm-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-sm-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-sm-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-sm-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-sm-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-sm-0 {\n margin-top: 0 !important;\n }\n .mt-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mt-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mt-sm-3 {\n margin-top: 1rem !important;\n }\n .mt-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mt-sm-5 {\n margin-top: 3rem !important;\n }\n .mt-sm-auto {\n margin-top: auto !important;\n }\n .me-sm-0 {\n margin-right: 0 !important;\n }\n .me-sm-1 {\n margin-right: 0.25rem !important;\n }\n .me-sm-2 {\n margin-right: 0.5rem !important;\n }\n .me-sm-3 {\n margin-right: 1rem !important;\n }\n .me-sm-4 {\n margin-right: 1.5rem !important;\n }\n .me-sm-5 {\n margin-right: 3rem !important;\n }\n .me-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-0 {\n margin-bottom: 0 !important;\n }\n .mb-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-sm-3 {\n margin-bottom: 1rem !important;\n }\n .mb-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-sm-5 {\n margin-bottom: 3rem !important;\n }\n .mb-sm-auto {\n margin-bottom: auto !important;\n }\n .ms-sm-0 {\n margin-left: 0 !important;\n }\n .ms-sm-1 {\n margin-left: 0.25rem !important;\n }\n .ms-sm-2 {\n margin-left: 0.5rem !important;\n }\n .ms-sm-3 {\n margin-left: 1rem !important;\n }\n .ms-sm-4 {\n margin-left: 1.5rem !important;\n }\n .ms-sm-5 {\n margin-left: 3rem !important;\n }\n .ms-sm-auto {\n margin-left: auto !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .px-sm-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-sm-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-sm-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-sm-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-sm-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-sm-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-sm-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-sm-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-sm-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-sm-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-sm-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-sm-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-sm-0 {\n padding-top: 0 !important;\n }\n .pt-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pt-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pt-sm-3 {\n padding-top: 1rem !important;\n }\n .pt-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pt-sm-5 {\n padding-top: 3rem !important;\n }\n .pe-sm-0 {\n padding-right: 0 !important;\n }\n .pe-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pe-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pe-sm-3 {\n padding-right: 1rem !important;\n }\n .pe-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pe-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-0 {\n padding-bottom: 0 !important;\n }\n .pb-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pb-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-sm-5 {\n padding-bottom: 3rem !important;\n }\n .ps-sm-0 {\n padding-left: 0 !important;\n }\n .ps-sm-1 {\n padding-left: 0.25rem !important;\n }\n .ps-sm-2 {\n padding-left: 0.5rem !important;\n }\n .ps-sm-3 {\n padding-left: 1rem !important;\n }\n .ps-sm-4 {\n padding-left: 1.5rem !important;\n }\n .ps-sm-5 {\n padding-left: 3rem !important;\n }\n .gap-sm-0 {\n gap: 0 !important;\n }\n .gap-sm-1 {\n gap: 0.25rem !important;\n }\n .gap-sm-2 {\n gap: 0.5rem !important;\n }\n .gap-sm-3 {\n gap: 1rem !important;\n }\n .gap-sm-4 {\n gap: 1.5rem !important;\n }\n .gap-sm-5 {\n gap: 3rem !important;\n }\n .row-gap-sm-0 {\n row-gap: 0 !important;\n }\n .row-gap-sm-1 {\n row-gap: 0.25rem !important;\n }\n .row-gap-sm-2 {\n row-gap: 0.5rem !important;\n }\n .row-gap-sm-3 {\n row-gap: 1rem !important;\n }\n .row-gap-sm-4 {\n row-gap: 1.5rem !important;\n }\n .row-gap-sm-5 {\n row-gap: 3rem !important;\n }\n .column-gap-sm-0 {\n -moz-column-gap: 0 !important;\n column-gap: 0 !important;\n }\n .column-gap-sm-1 {\n -moz-column-gap: 0.25rem !important;\n column-gap: 0.25rem !important;\n }\n .column-gap-sm-2 {\n -moz-column-gap: 0.5rem !important;\n column-gap: 0.5rem !important;\n }\n .column-gap-sm-3 {\n -moz-column-gap: 1rem !important;\n column-gap: 1rem !important;\n }\n .column-gap-sm-4 {\n -moz-column-gap: 1.5rem !important;\n column-gap: 1.5rem !important;\n }\n .column-gap-sm-5 {\n -moz-column-gap: 3rem !important;\n column-gap: 3rem !important;\n }\n .text-sm-start {\n text-align: left !important;\n }\n .text-sm-end {\n text-align: right !important;\n }\n .text-sm-center {\n text-align: center !important;\n }\n}\n@media (min-width: 768px) {\n .float-md-start {\n float: left !important;\n }\n .float-md-end {\n float: right !important;\n }\n .float-md-none {\n float: none !important;\n }\n .object-fit-md-contain {\n -o-object-fit: contain !important;\n object-fit: contain !important;\n }\n .object-fit-md-cover {\n -o-object-fit: cover !important;\n object-fit: cover !important;\n }\n .object-fit-md-fill {\n -o-object-fit: fill !important;\n object-fit: fill !important;\n }\n .object-fit-md-scale {\n -o-object-fit: scale-down !important;\n object-fit: scale-down !important;\n }\n .object-fit-md-none {\n -o-object-fit: none !important;\n object-fit: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-grid {\n display: grid !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: flex !important;\n }\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n .d-md-none {\n display: none !important;\n }\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n .flex-md-row {\n flex-direction: row !important;\n }\n .flex-md-column {\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n justify-content: center !important;\n }\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n .justify-content-md-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-md-start {\n align-items: flex-start !important;\n }\n .align-items-md-end {\n align-items: flex-end !important;\n }\n .align-items-md-center {\n align-items: center !important;\n }\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n .align-content-md-start {\n align-content: flex-start !important;\n }\n .align-content-md-end {\n align-content: flex-end !important;\n }\n .align-content-md-center {\n align-content: center !important;\n }\n .align-content-md-between {\n align-content: space-between !important;\n }\n .align-content-md-around {\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n .align-self-md-auto {\n align-self: auto !important;\n }\n .align-self-md-start {\n align-self: flex-start !important;\n }\n .align-self-md-end {\n align-self: flex-end !important;\n }\n .align-self-md-center {\n align-self: center !important;\n }\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n .order-md-first {\n order: -1 !important;\n }\n .order-md-0 {\n order: 0 !important;\n }\n .order-md-1 {\n order: 1 !important;\n }\n .order-md-2 {\n order: 2 !important;\n }\n .order-md-3 {\n order: 3 !important;\n }\n .order-md-4 {\n order: 4 !important;\n }\n .order-md-5 {\n order: 5 !important;\n }\n .order-md-last {\n order: 6 !important;\n }\n .m-md-0 {\n margin: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mx-md-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-md-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-md-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-md-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-md-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-md-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-md-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-md-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-md-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-md-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-md-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-md-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-md-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-md-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-md-0 {\n margin-top: 0 !important;\n }\n .mt-md-1 {\n margin-top: 0.25rem !important;\n }\n .mt-md-2 {\n margin-top: 0.5rem !important;\n }\n .mt-md-3 {\n margin-top: 1rem !important;\n }\n .mt-md-4 {\n margin-top: 1.5rem !important;\n }\n .mt-md-5 {\n margin-top: 3rem !important;\n }\n .mt-md-auto {\n margin-top: auto !important;\n }\n .me-md-0 {\n margin-right: 0 !important;\n }\n .me-md-1 {\n margin-right: 0.25rem !important;\n }\n .me-md-2 {\n margin-right: 0.5rem !important;\n }\n .me-md-3 {\n margin-right: 1rem !important;\n }\n .me-md-4 {\n margin-right: 1.5rem !important;\n }\n .me-md-5 {\n margin-right: 3rem !important;\n }\n .me-md-auto {\n margin-right: auto !important;\n }\n .mb-md-0 {\n margin-bottom: 0 !important;\n }\n .mb-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-md-3 {\n margin-bottom: 1rem !important;\n }\n .mb-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-md-5 {\n margin-bottom: 3rem !important;\n }\n .mb-md-auto {\n margin-bottom: auto !important;\n }\n .ms-md-0 {\n margin-left: 0 !important;\n }\n .ms-md-1 {\n margin-left: 0.25rem !important;\n }\n .ms-md-2 {\n margin-left: 0.5rem !important;\n }\n .ms-md-3 {\n margin-left: 1rem !important;\n }\n .ms-md-4 {\n margin-left: 1.5rem !important;\n }\n .ms-md-5 {\n margin-left: 3rem !important;\n }\n .ms-md-auto {\n margin-left: auto !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .px-md-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-md-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-md-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-md-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-md-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-md-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-md-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-md-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-md-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-md-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-md-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-md-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-md-0 {\n padding-top: 0 !important;\n }\n .pt-md-1 {\n padding-top: 0.25rem !important;\n }\n .pt-md-2 {\n padding-top: 0.5rem !important;\n }\n .pt-md-3 {\n padding-top: 1rem !important;\n }\n .pt-md-4 {\n padding-top: 1.5rem !important;\n }\n .pt-md-5 {\n padding-top: 3rem !important;\n }\n .pe-md-0 {\n padding-right: 0 !important;\n }\n .pe-md-1 {\n padding-right: 0.25rem !important;\n }\n .pe-md-2 {\n padding-right: 0.5rem !important;\n }\n .pe-md-3 {\n padding-right: 1rem !important;\n }\n .pe-md-4 {\n padding-right: 1.5rem !important;\n }\n .pe-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-0 {\n padding-bottom: 0 !important;\n }\n .pb-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-md-3 {\n padding-bottom: 1rem !important;\n }\n .pb-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-md-5 {\n padding-bottom: 3rem !important;\n }\n .ps-md-0 {\n padding-left: 0 !important;\n }\n .ps-md-1 {\n padding-left: 0.25rem !important;\n }\n .ps-md-2 {\n padding-left: 0.5rem !important;\n }\n .ps-md-3 {\n padding-left: 1rem !important;\n }\n .ps-md-4 {\n padding-left: 1.5rem !important;\n }\n .ps-md-5 {\n padding-left: 3rem !important;\n }\n .gap-md-0 {\n gap: 0 !important;\n }\n .gap-md-1 {\n gap: 0.25rem !important;\n }\n .gap-md-2 {\n gap: 0.5rem !important;\n }\n .gap-md-3 {\n gap: 1rem !important;\n }\n .gap-md-4 {\n gap: 1.5rem !important;\n }\n .gap-md-5 {\n gap: 3rem !important;\n }\n .row-gap-md-0 {\n row-gap: 0 !important;\n }\n .row-gap-md-1 {\n row-gap: 0.25rem !important;\n }\n .row-gap-md-2 {\n row-gap: 0.5rem !important;\n }\n .row-gap-md-3 {\n row-gap: 1rem !important;\n }\n .row-gap-md-4 {\n row-gap: 1.5rem !important;\n }\n .row-gap-md-5 {\n row-gap: 3rem !important;\n }\n .column-gap-md-0 {\n -moz-column-gap: 0 !important;\n column-gap: 0 !important;\n }\n .column-gap-md-1 {\n -moz-column-gap: 0.25rem !important;\n column-gap: 0.25rem !important;\n }\n .column-gap-md-2 {\n -moz-column-gap: 0.5rem !important;\n column-gap: 0.5rem !important;\n }\n .column-gap-md-3 {\n -moz-column-gap: 1rem !important;\n column-gap: 1rem !important;\n }\n .column-gap-md-4 {\n -moz-column-gap: 1.5rem !important;\n column-gap: 1.5rem !important;\n }\n .column-gap-md-5 {\n -moz-column-gap: 3rem !important;\n column-gap: 3rem !important;\n }\n .text-md-start {\n text-align: left !important;\n }\n .text-md-end {\n text-align: right !important;\n }\n .text-md-center {\n text-align: center !important;\n }\n}\n@media (min-width: 992px) {\n .float-lg-start {\n float: left !important;\n }\n .float-lg-end {\n float: right !important;\n }\n .float-lg-none {\n float: none !important;\n }\n .object-fit-lg-contain {\n -o-object-fit: contain !important;\n object-fit: contain !important;\n }\n .object-fit-lg-cover {\n -o-object-fit: cover !important;\n object-fit: cover !important;\n }\n .object-fit-lg-fill {\n -o-object-fit: fill !important;\n object-fit: fill !important;\n }\n .object-fit-lg-scale {\n -o-object-fit: scale-down !important;\n object-fit: scale-down !important;\n }\n .object-fit-lg-none {\n -o-object-fit: none !important;\n object-fit: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-grid {\n display: grid !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n .d-lg-none {\n display: none !important;\n }\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n .flex-lg-row {\n flex-direction: row !important;\n }\n .flex-lg-column {\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n justify-content: center !important;\n }\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n .justify-content-lg-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n align-items: center !important;\n }\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n align-content: center !important;\n }\n .align-content-lg-between {\n align-content: space-between !important;\n }\n .align-content-lg-around {\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n align-self: auto !important;\n }\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n align-self: center !important;\n }\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n .order-lg-first {\n order: -1 !important;\n }\n .order-lg-0 {\n order: 0 !important;\n }\n .order-lg-1 {\n order: 1 !important;\n }\n .order-lg-2 {\n order: 2 !important;\n }\n .order-lg-3 {\n order: 3 !important;\n }\n .order-lg-4 {\n order: 4 !important;\n }\n .order-lg-5 {\n order: 5 !important;\n }\n .order-lg-last {\n order: 6 !important;\n }\n .m-lg-0 {\n margin: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mx-lg-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-lg-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-lg-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-lg-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-lg-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-lg-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-lg-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-lg-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-lg-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-lg-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-lg-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-lg-0 {\n margin-top: 0 !important;\n }\n .mt-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mt-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mt-lg-3 {\n margin-top: 1rem !important;\n }\n .mt-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mt-lg-5 {\n margin-top: 3rem !important;\n }\n .mt-lg-auto {\n margin-top: auto !important;\n }\n .me-lg-0 {\n margin-right: 0 !important;\n }\n .me-lg-1 {\n margin-right: 0.25rem !important;\n }\n .me-lg-2 {\n margin-right: 0.5rem !important;\n }\n .me-lg-3 {\n margin-right: 1rem !important;\n }\n .me-lg-4 {\n margin-right: 1.5rem !important;\n }\n .me-lg-5 {\n margin-right: 3rem !important;\n }\n .me-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-0 {\n margin-bottom: 0 !important;\n }\n .mb-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-lg-3 {\n margin-bottom: 1rem !important;\n }\n .mb-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-lg-5 {\n margin-bottom: 3rem !important;\n }\n .mb-lg-auto {\n margin-bottom: auto !important;\n }\n .ms-lg-0 {\n margin-left: 0 !important;\n }\n .ms-lg-1 {\n margin-left: 0.25rem !important;\n }\n .ms-lg-2 {\n margin-left: 0.5rem !important;\n }\n .ms-lg-3 {\n margin-left: 1rem !important;\n }\n .ms-lg-4 {\n margin-left: 1.5rem !important;\n }\n .ms-lg-5 {\n margin-left: 3rem !important;\n }\n .ms-lg-auto {\n margin-left: auto !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .px-lg-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-lg-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-lg-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-lg-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-lg-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-lg-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-lg-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-lg-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-lg-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-lg-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-lg-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-lg-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-lg-0 {\n padding-top: 0 !important;\n }\n .pt-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pt-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pt-lg-3 {\n padding-top: 1rem !important;\n }\n .pt-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pt-lg-5 {\n padding-top: 3rem !important;\n }\n .pe-lg-0 {\n padding-right: 0 !important;\n }\n .pe-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pe-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pe-lg-3 {\n padding-right: 1rem !important;\n }\n .pe-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pe-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-0 {\n padding-bottom: 0 !important;\n }\n .pb-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pb-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-lg-5 {\n padding-bottom: 3rem !important;\n }\n .ps-lg-0 {\n padding-left: 0 !important;\n }\n .ps-lg-1 {\n padding-left: 0.25rem !important;\n }\n .ps-lg-2 {\n padding-left: 0.5rem !important;\n }\n .ps-lg-3 {\n padding-left: 1rem !important;\n }\n .ps-lg-4 {\n padding-left: 1.5rem !important;\n }\n .ps-lg-5 {\n padding-left: 3rem !important;\n }\n .gap-lg-0 {\n gap: 0 !important;\n }\n .gap-lg-1 {\n gap: 0.25rem !important;\n }\n .gap-lg-2 {\n gap: 0.5rem !important;\n }\n .gap-lg-3 {\n gap: 1rem !important;\n }\n .gap-lg-4 {\n gap: 1.5rem !important;\n }\n .gap-lg-5 {\n gap: 3rem !important;\n }\n .row-gap-lg-0 {\n row-gap: 0 !important;\n }\n .row-gap-lg-1 {\n row-gap: 0.25rem !important;\n }\n .row-gap-lg-2 {\n row-gap: 0.5rem !important;\n }\n .row-gap-lg-3 {\n row-gap: 1rem !important;\n }\n .row-gap-lg-4 {\n row-gap: 1.5rem !important;\n }\n .row-gap-lg-5 {\n row-gap: 3rem !important;\n }\n .column-gap-lg-0 {\n -moz-column-gap: 0 !important;\n column-gap: 0 !important;\n }\n .column-gap-lg-1 {\n -moz-column-gap: 0.25rem !important;\n column-gap: 0.25rem !important;\n }\n .column-gap-lg-2 {\n -moz-column-gap: 0.5rem !important;\n column-gap: 0.5rem !important;\n }\n .column-gap-lg-3 {\n -moz-column-gap: 1rem !important;\n column-gap: 1rem !important;\n }\n .column-gap-lg-4 {\n -moz-column-gap: 1.5rem !important;\n column-gap: 1.5rem !important;\n }\n .column-gap-lg-5 {\n -moz-column-gap: 3rem !important;\n column-gap: 3rem !important;\n }\n .text-lg-start {\n text-align: left !important;\n }\n .text-lg-end {\n text-align: right !important;\n }\n .text-lg-center {\n text-align: center !important;\n }\n}\n@media (min-width: 1200px) {\n .float-xl-start {\n float: left !important;\n }\n .float-xl-end {\n float: right !important;\n }\n .float-xl-none {\n float: none !important;\n }\n .object-fit-xl-contain {\n -o-object-fit: contain !important;\n object-fit: contain !important;\n }\n .object-fit-xl-cover {\n -o-object-fit: cover !important;\n object-fit: cover !important;\n }\n .object-fit-xl-fill {\n -o-object-fit: fill !important;\n object-fit: fill !important;\n }\n .object-fit-xl-scale {\n -o-object-fit: scale-down !important;\n object-fit: scale-down !important;\n }\n .object-fit-xl-none {\n -o-object-fit: none !important;\n object-fit: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-grid {\n display: grid !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n .d-xl-none {\n display: none !important;\n }\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xl-row {\n flex-direction: row !important;\n }\n .flex-xl-column {\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n justify-content: center !important;\n }\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n .justify-content-xl-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n align-items: center !important;\n }\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n align-content: center !important;\n }\n .align-content-xl-between {\n align-content: space-between !important;\n }\n .align-content-xl-around {\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n align-self: auto !important;\n }\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n align-self: center !important;\n }\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n .order-xl-first {\n order: -1 !important;\n }\n .order-xl-0 {\n order: 0 !important;\n }\n .order-xl-1 {\n order: 1 !important;\n }\n .order-xl-2 {\n order: 2 !important;\n }\n .order-xl-3 {\n order: 3 !important;\n }\n .order-xl-4 {\n order: 4 !important;\n }\n .order-xl-5 {\n order: 5 !important;\n }\n .order-xl-last {\n order: 6 !important;\n }\n .m-xl-0 {\n margin: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mx-xl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-xl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-xl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-xl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-xl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-xl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-xl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-xl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-xl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-xl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-xl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-xl-0 {\n margin-top: 0 !important;\n }\n .mt-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mt-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mt-xl-3 {\n margin-top: 1rem !important;\n }\n .mt-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mt-xl-5 {\n margin-top: 3rem !important;\n }\n .mt-xl-auto {\n margin-top: auto !important;\n }\n .me-xl-0 {\n margin-right: 0 !important;\n }\n .me-xl-1 {\n margin-right: 0.25rem !important;\n }\n .me-xl-2 {\n margin-right: 0.5rem !important;\n }\n .me-xl-3 {\n margin-right: 1rem !important;\n }\n .me-xl-4 {\n margin-right: 1.5rem !important;\n }\n .me-xl-5 {\n margin-right: 3rem !important;\n }\n .me-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-0 {\n margin-bottom: 0 !important;\n }\n .mb-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-xl-3 {\n margin-bottom: 1rem !important;\n }\n .mb-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-xl-5 {\n margin-bottom: 3rem !important;\n }\n .mb-xl-auto {\n margin-bottom: auto !important;\n }\n .ms-xl-0 {\n margin-left: 0 !important;\n }\n .ms-xl-1 {\n margin-left: 0.25rem !important;\n }\n .ms-xl-2 {\n margin-left: 0.5rem !important;\n }\n .ms-xl-3 {\n margin-left: 1rem !important;\n }\n .ms-xl-4 {\n margin-left: 1.5rem !important;\n }\n .ms-xl-5 {\n margin-left: 3rem !important;\n }\n .ms-xl-auto {\n margin-left: auto !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .px-xl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-xl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-xl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-xl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-xl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-xl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-xl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-xl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-xl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-xl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-xl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-xl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-xl-0 {\n padding-top: 0 !important;\n }\n .pt-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pt-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pt-xl-3 {\n padding-top: 1rem !important;\n }\n .pt-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pt-xl-5 {\n padding-top: 3rem !important;\n }\n .pe-xl-0 {\n padding-right: 0 !important;\n }\n .pe-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pe-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pe-xl-3 {\n padding-right: 1rem !important;\n }\n .pe-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pe-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-0 {\n padding-bottom: 0 !important;\n }\n .pb-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pb-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-xl-5 {\n padding-bottom: 3rem !important;\n }\n .ps-xl-0 {\n padding-left: 0 !important;\n }\n .ps-xl-1 {\n padding-left: 0.25rem !important;\n }\n .ps-xl-2 {\n padding-left: 0.5rem !important;\n }\n .ps-xl-3 {\n padding-left: 1rem !important;\n }\n .ps-xl-4 {\n padding-left: 1.5rem !important;\n }\n .ps-xl-5 {\n padding-left: 3rem !important;\n }\n .gap-xl-0 {\n gap: 0 !important;\n }\n .gap-xl-1 {\n gap: 0.25rem !important;\n }\n .gap-xl-2 {\n gap: 0.5rem !important;\n }\n .gap-xl-3 {\n gap: 1rem !important;\n }\n .gap-xl-4 {\n gap: 1.5rem !important;\n }\n .gap-xl-5 {\n gap: 3rem !important;\n }\n .row-gap-xl-0 {\n row-gap: 0 !important;\n }\n .row-gap-xl-1 {\n row-gap: 0.25rem !important;\n }\n .row-gap-xl-2 {\n row-gap: 0.5rem !important;\n }\n .row-gap-xl-3 {\n row-gap: 1rem !important;\n }\n .row-gap-xl-4 {\n row-gap: 1.5rem !important;\n }\n .row-gap-xl-5 {\n row-gap: 3rem !important;\n }\n .column-gap-xl-0 {\n -moz-column-gap: 0 !important;\n column-gap: 0 !important;\n }\n .column-gap-xl-1 {\n -moz-column-gap: 0.25rem !important;\n column-gap: 0.25rem !important;\n }\n .column-gap-xl-2 {\n -moz-column-gap: 0.5rem !important;\n column-gap: 0.5rem !important;\n }\n .column-gap-xl-3 {\n -moz-column-gap: 1rem !important;\n column-gap: 1rem !important;\n }\n .column-gap-xl-4 {\n -moz-column-gap: 1.5rem !important;\n column-gap: 1.5rem !important;\n }\n .column-gap-xl-5 {\n -moz-column-gap: 3rem !important;\n column-gap: 3rem !important;\n }\n .text-xl-start {\n text-align: left !important;\n }\n .text-xl-end {\n text-align: right !important;\n }\n .text-xl-center {\n text-align: center !important;\n }\n}\n@media (min-width: 1400px) {\n .float-xxl-start {\n float: left !important;\n }\n .float-xxl-end {\n float: right !important;\n }\n .float-xxl-none {\n float: none !important;\n }\n .object-fit-xxl-contain {\n -o-object-fit: contain !important;\n object-fit: contain !important;\n }\n .object-fit-xxl-cover {\n -o-object-fit: cover !important;\n object-fit: cover !important;\n }\n .object-fit-xxl-fill {\n -o-object-fit: fill !important;\n object-fit: fill !important;\n }\n .object-fit-xxl-scale {\n -o-object-fit: scale-down !important;\n object-fit: scale-down !important;\n }\n .object-fit-xxl-none {\n -o-object-fit: none !important;\n object-fit: none !important;\n }\n .d-xxl-inline {\n display: inline !important;\n }\n .d-xxl-inline-block {\n display: inline-block !important;\n }\n .d-xxl-block {\n display: block !important;\n }\n .d-xxl-grid {\n display: grid !important;\n }\n .d-xxl-table {\n display: table !important;\n }\n .d-xxl-table-row {\n display: table-row !important;\n }\n .d-xxl-table-cell {\n display: table-cell !important;\n }\n .d-xxl-flex {\n display: flex !important;\n }\n .d-xxl-inline-flex {\n display: inline-flex !important;\n }\n .d-xxl-none {\n display: none !important;\n }\n .flex-xxl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xxl-row {\n flex-direction: row !important;\n }\n .flex-xxl-column {\n flex-direction: column !important;\n }\n .flex-xxl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xxl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xxl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xxl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xxl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xxl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-xxl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xxl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xxl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-xxl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xxl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xxl-center {\n justify-content: center !important;\n }\n .justify-content-xxl-between {\n justify-content: space-between !important;\n }\n .justify-content-xxl-around {\n justify-content: space-around !important;\n }\n .justify-content-xxl-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-xxl-start {\n align-items: flex-start !important;\n }\n .align-items-xxl-end {\n align-items: flex-end !important;\n }\n .align-items-xxl-center {\n align-items: center !important;\n }\n .align-items-xxl-baseline {\n align-items: baseline !important;\n }\n .align-items-xxl-stretch {\n align-items: stretch !important;\n }\n .align-content-xxl-start {\n align-content: flex-start !important;\n }\n .align-content-xxl-end {\n align-content: flex-end !important;\n }\n .align-content-xxl-center {\n align-content: center !important;\n }\n .align-content-xxl-between {\n align-content: space-between !important;\n }\n .align-content-xxl-around {\n align-content: space-around !important;\n }\n .align-content-xxl-stretch {\n align-content: stretch !important;\n }\n .align-self-xxl-auto {\n align-self: auto !important;\n }\n .align-self-xxl-start {\n align-self: flex-start !important;\n }\n .align-self-xxl-end {\n align-self: flex-end !important;\n }\n .align-self-xxl-center {\n align-self: center !important;\n }\n .align-self-xxl-baseline {\n align-self: baseline !important;\n }\n .align-self-xxl-stretch {\n align-self: stretch !important;\n }\n .order-xxl-first {\n order: -1 !important;\n }\n .order-xxl-0 {\n order: 0 !important;\n }\n .order-xxl-1 {\n order: 1 !important;\n }\n .order-xxl-2 {\n order: 2 !important;\n }\n .order-xxl-3 {\n order: 3 !important;\n }\n .order-xxl-4 {\n order: 4 !important;\n }\n .order-xxl-5 {\n order: 5 !important;\n }\n .order-xxl-last {\n order: 6 !important;\n }\n .m-xxl-0 {\n margin: 0 !important;\n }\n .m-xxl-1 {\n margin: 0.25rem !important;\n }\n .m-xxl-2 {\n margin: 0.5rem !important;\n }\n .m-xxl-3 {\n margin: 1rem !important;\n }\n .m-xxl-4 {\n margin: 1.5rem !important;\n }\n .m-xxl-5 {\n margin: 3rem !important;\n }\n .m-xxl-auto {\n margin: auto !important;\n }\n .mx-xxl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-xxl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-xxl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-xxl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-xxl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-xxl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-xxl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-xxl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-xxl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-xxl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-xxl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-xxl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-xxl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-xxl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-xxl-0 {\n margin-top: 0 !important;\n }\n .mt-xxl-1 {\n margin-top: 0.25rem !important;\n }\n .mt-xxl-2 {\n margin-top: 0.5rem !important;\n }\n .mt-xxl-3 {\n margin-top: 1rem !important;\n }\n .mt-xxl-4 {\n margin-top: 1.5rem !important;\n }\n .mt-xxl-5 {\n margin-top: 3rem !important;\n }\n .mt-xxl-auto {\n margin-top: auto !important;\n }\n .me-xxl-0 {\n margin-right: 0 !important;\n }\n .me-xxl-1 {\n margin-right: 0.25rem !important;\n }\n .me-xxl-2 {\n margin-right: 0.5rem !important;\n }\n .me-xxl-3 {\n margin-right: 1rem !important;\n }\n .me-xxl-4 {\n margin-right: 1.5rem !important;\n }\n .me-xxl-5 {\n margin-right: 3rem !important;\n }\n .me-xxl-auto {\n margin-right: auto !important;\n }\n .mb-xxl-0 {\n margin-bottom: 0 !important;\n }\n .mb-xxl-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-xxl-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-xxl-3 {\n margin-bottom: 1rem !important;\n }\n .mb-xxl-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-xxl-5 {\n margin-bottom: 3rem !important;\n }\n .mb-xxl-auto {\n margin-bottom: auto !important;\n }\n .ms-xxl-0 {\n margin-left: 0 !important;\n }\n .ms-xxl-1 {\n margin-left: 0.25rem !important;\n }\n .ms-xxl-2 {\n margin-left: 0.5rem !important;\n }\n .ms-xxl-3 {\n margin-left: 1rem !important;\n }\n .ms-xxl-4 {\n margin-left: 1.5rem !important;\n }\n .ms-xxl-5 {\n margin-left: 3rem !important;\n }\n .ms-xxl-auto {\n margin-left: auto !important;\n }\n .p-xxl-0 {\n padding: 0 !important;\n }\n .p-xxl-1 {\n padding: 0.25rem !important;\n }\n .p-xxl-2 {\n padding: 0.5rem !important;\n }\n .p-xxl-3 {\n padding: 1rem !important;\n }\n .p-xxl-4 {\n padding: 1.5rem !important;\n }\n .p-xxl-5 {\n padding: 3rem !important;\n }\n .px-xxl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-xxl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-xxl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-xxl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-xxl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-xxl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-xxl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-xxl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-xxl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-xxl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-xxl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-xxl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-xxl-0 {\n padding-top: 0 !important;\n }\n .pt-xxl-1 {\n padding-top: 0.25rem !important;\n }\n .pt-xxl-2 {\n padding-top: 0.5rem !important;\n }\n .pt-xxl-3 {\n padding-top: 1rem !important;\n }\n .pt-xxl-4 {\n padding-top: 1.5rem !important;\n }\n .pt-xxl-5 {\n padding-top: 3rem !important;\n }\n .pe-xxl-0 {\n padding-right: 0 !important;\n }\n .pe-xxl-1 {\n padding-right: 0.25rem !important;\n }\n .pe-xxl-2 {\n padding-right: 0.5rem !important;\n }\n .pe-xxl-3 {\n padding-right: 1rem !important;\n }\n .pe-xxl-4 {\n padding-right: 1.5rem !important;\n }\n .pe-xxl-5 {\n padding-right: 3rem !important;\n }\n .pb-xxl-0 {\n padding-bottom: 0 !important;\n }\n .pb-xxl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-xxl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-xxl-3 {\n padding-bottom: 1rem !important;\n }\n .pb-xxl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-xxl-5 {\n padding-bottom: 3rem !important;\n }\n .ps-xxl-0 {\n padding-left: 0 !important;\n }\n .ps-xxl-1 {\n padding-left: 0.25rem !important;\n }\n .ps-xxl-2 {\n padding-left: 0.5rem !important;\n }\n .ps-xxl-3 {\n padding-left: 1rem !important;\n }\n .ps-xxl-4 {\n padding-left: 1.5rem !important;\n }\n .ps-xxl-5 {\n padding-left: 3rem !important;\n }\n .gap-xxl-0 {\n gap: 0 !important;\n }\n .gap-xxl-1 {\n gap: 0.25rem !important;\n }\n .gap-xxl-2 {\n gap: 0.5rem !important;\n }\n .gap-xxl-3 {\n gap: 1rem !important;\n }\n .gap-xxl-4 {\n gap: 1.5rem !important;\n }\n .gap-xxl-5 {\n gap: 3rem !important;\n }\n .row-gap-xxl-0 {\n row-gap: 0 !important;\n }\n .row-gap-xxl-1 {\n row-gap: 0.25rem !important;\n }\n .row-gap-xxl-2 {\n row-gap: 0.5rem !important;\n }\n .row-gap-xxl-3 {\n row-gap: 1rem !important;\n }\n .row-gap-xxl-4 {\n row-gap: 1.5rem !important;\n }\n .row-gap-xxl-5 {\n row-gap: 3rem !important;\n }\n .column-gap-xxl-0 {\n -moz-column-gap: 0 !important;\n column-gap: 0 !important;\n }\n .column-gap-xxl-1 {\n -moz-column-gap: 0.25rem !important;\n column-gap: 0.25rem !important;\n }\n .column-gap-xxl-2 {\n -moz-column-gap: 0.5rem !important;\n column-gap: 0.5rem !important;\n }\n .column-gap-xxl-3 {\n -moz-column-gap: 1rem !important;\n column-gap: 1rem !important;\n }\n .column-gap-xxl-4 {\n -moz-column-gap: 1.5rem !important;\n column-gap: 1.5rem !important;\n }\n .column-gap-xxl-5 {\n -moz-column-gap: 3rem !important;\n column-gap: 3rem !important;\n }\n .text-xxl-start {\n text-align: left !important;\n }\n .text-xxl-end {\n text-align: right !important;\n }\n .text-xxl-center {\n text-align: center !important;\n }\n}\n@media (min-width: 1200px) {\n .fs-1 {\n font-size: 2.5rem !important;\n }\n .fs-2 {\n font-size: 2rem !important;\n }\n .fs-3 {\n font-size: 1.75rem !important;\n }\n .fs-4 {\n font-size: 1.5rem !important;\n }\n}\n@media print {\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-grid {\n display: grid !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: flex !important;\n }\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n .d-print-none {\n display: none !important;\n }\n}\n\n/*# sourceMappingURL=bootstrap.css.map */","// stylelint-disable property-blacklist, scss/dollar-variable-default\n\n// SCSS RFS mixin\n//\n// Automated responsive values for font sizes, paddings, margins and much more\n//\n// Licensed under MIT (https://github.com/twbs/rfs/blob/main/LICENSE)\n\n// Configuration\n\n// Base value\n$rfs-base-value: 1.25rem !default;\n$rfs-unit: rem !default;\n\n@if $rfs-unit != rem and $rfs-unit != px {\n @error \"`#{$rfs-unit}` is not a valid unit for $rfs-unit. Use `px` or `rem`.\";\n}\n\n// Breakpoint at where values start decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n@if $rfs-breakpoint-unit != px and $rfs-breakpoint-unit != em and $rfs-breakpoint-unit != rem {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n}\n\n// Resize values based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != number or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Mode. Possibilities: \"min-media-query\", \"max-media-query\"\n$rfs-mode: min-media-query !default;\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-rfs to false\n$enable-rfs: true !default;\n\n// Cache $rfs-base-value unit\n$rfs-base-value-unit: unit($rfs-base-value);\n\n@function divide($dividend, $divisor, $precision: 10) {\n $sign: if($dividend > 0 and $divisor > 0 or $dividend < 0 and $divisor < 0, 1, -1);\n $dividend: abs($dividend);\n $divisor: abs($divisor);\n @if $dividend == 0 {\n @return 0;\n }\n @if $divisor == 0 {\n @error \"Cannot divide by 0\";\n }\n $remainder: $dividend;\n $result: 0;\n $factor: 10;\n @while ($remainder > 0 and $precision >= 0) {\n $quotient: 0;\n @while ($remainder >= $divisor) {\n $remainder: $remainder - $divisor;\n $quotient: $quotient + 1;\n }\n $result: $result * 10 + $quotient;\n $factor: $factor * .1;\n $remainder: $remainder * 10;\n $precision: $precision - 1;\n @if ($precision < 0 and $remainder >= $divisor * 5) {\n $result: $result + 1;\n }\n }\n $result: $result * $factor * $sign;\n $dividend-unit: unit($dividend);\n $divisor-unit: unit($divisor);\n $unit-map: (\n \"px\": 1px,\n \"rem\": 1rem,\n \"em\": 1em,\n \"%\": 1%\n );\n @if ($dividend-unit != $divisor-unit and map-has-key($unit-map, $dividend-unit)) {\n $result: $result * map-get($unit-map, $dividend-unit);\n }\n @return $result;\n}\n\n// Remove px-unit from $rfs-base-value for calculations\n@if $rfs-base-value-unit == px {\n $rfs-base-value: divide($rfs-base-value, $rfs-base-value * 0 + 1);\n}\n@else if $rfs-base-value-unit == rem {\n $rfs-base-value: divide($rfs-base-value, divide($rfs-base-value * 0 + 1, $rfs-rem-value));\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == px {\n $rfs-breakpoint: divide($rfs-breakpoint, $rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == rem or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: divide($rfs-breakpoint, divide($rfs-breakpoint * 0 + 1, $rfs-rem-value));\n}\n\n// Calculate the media query value\n$rfs-mq-value: if($rfs-breakpoint-unit == px, #{$rfs-breakpoint}px, #{divide($rfs-breakpoint, $rfs-rem-value)}#{$rfs-breakpoint-unit});\n$rfs-mq-property-width: if($rfs-mode == max-media-query, max-width, min-width);\n$rfs-mq-property-height: if($rfs-mode == max-media-query, max-height, min-height);\n\n// Internal mixin used to determine which media query needs to be used\n@mixin _rfs-media-query {\n @if $rfs-two-dimensional {\n @if $rfs-mode == max-media-query {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}), (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {\n @content;\n }\n }\n @else {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) and (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {\n @content;\n }\n }\n }\n @else {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) {\n @content;\n }\n }\n}\n\n// Internal mixin that adds disable classes to the selector if needed.\n@mixin _rfs-rule {\n @if $rfs-class == disable and $rfs-mode == max-media-query {\n // Adding an extra class increases specificity, which prevents the media query to override the property\n &,\n .disable-rfs &,\n &.disable-rfs {\n @content;\n }\n }\n @else if $rfs-class == enable and $rfs-mode == min-media-query {\n .enable-rfs &,\n &.enable-rfs {\n @content;\n }\n }\n @else {\n @content;\n }\n}\n\n// Internal mixin that adds enable classes to the selector if needed.\n@mixin _rfs-media-query-rule {\n\n @if $rfs-class == enable {\n @if $rfs-mode == min-media-query {\n @content;\n }\n\n @include _rfs-media-query {\n .enable-rfs &,\n &.enable-rfs {\n @content;\n }\n }\n }\n @else {\n @if $rfs-class == disable and $rfs-mode == min-media-query {\n .disable-rfs &,\n &.disable-rfs {\n @content;\n }\n }\n @include _rfs-media-query {\n @content;\n }\n }\n}\n\n// Helper function to get the formatted non-responsive value\n@function rfs-value($values) {\n // Convert to list\n $values: if(type-of($values) != list, ($values,), $values);\n\n $val: '';\n\n // Loop over each value and calculate value\n @each $value in $values {\n @if $value == 0 {\n $val: $val + ' 0';\n }\n @else {\n // Cache $value unit\n $unit: if(type-of($value) == \"number\", unit($value), false);\n\n @if $unit == px {\n // Convert to rem if needed\n $val: $val + ' ' + if($rfs-unit == rem, #{divide($value, $value * 0 + $rfs-rem-value)}rem, $value);\n }\n @else if $unit == rem {\n // Convert to px if needed\n $val: $val + ' ' + if($rfs-unit == px, #{divide($value, $value * 0 + 1) * $rfs-rem-value}px, $value);\n }\n @else {\n // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n $val: $val + ' ' + $value;\n }\n }\n }\n\n // Remove first space\n @return unquote(str-slice($val, 2));\n}\n\n// Helper function to get the responsive value calculated by RFS\n@function rfs-fluid-value($values) {\n // Convert to list\n $values: if(type-of($values) != list, ($values,), $values);\n\n $val: '';\n\n // Loop over each value and calculate value\n @each $value in $values {\n @if $value == 0 {\n $val: $val + ' 0';\n }\n\n @else {\n // Cache $value unit\n $unit: if(type-of($value) == \"number\", unit($value), false);\n\n // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $unit or $unit != px and $unit != rem {\n $val: $val + ' ' + $value;\n }\n\n @else {\n // Remove unit from $value for calculations\n $value: divide($value, $value * 0 + if($unit == px, 1, divide(1, $rfs-rem-value)));\n\n // Only add the media query if the value is greater than the minimum value\n @if abs($value) <= $rfs-base-value or not $enable-rfs {\n $val: $val + ' ' + if($rfs-unit == rem, #{divide($value, $rfs-rem-value)}rem, #{$value}px);\n }\n @else {\n // Calculate the minimum value\n $value-min: $rfs-base-value + divide(abs($value) - $rfs-base-value, $rfs-factor);\n\n // Calculate difference between $value and the minimum value\n $value-diff: abs($value) - $value-min;\n\n // Base value formatting\n $min-width: if($rfs-unit == rem, #{divide($value-min, $rfs-rem-value)}rem, #{$value-min}px);\n\n // Use negative value if needed\n $min-width: if($value < 0, -$min-width, $min-width);\n\n // Use `vmin` if two-dimensional is enabled\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{divide($value-diff * 100, $rfs-breakpoint)}#{$variable-unit};\n\n // Return the calculated value\n $val: $val + ' calc(' + $min-width + if($value < 0, ' - ', ' + ') + $variable-width + ')';\n }\n }\n }\n }\n\n // Remove first space\n @return unquote(str-slice($val, 2));\n}\n\n// RFS mixin\n@mixin rfs($values, $property: font-size) {\n @if $values != null {\n $val: rfs-value($values);\n $fluidVal: rfs-fluid-value($values);\n\n // Do not print the media query if responsive & non-responsive values are the same\n @if $val == $fluidVal {\n #{$property}: $val;\n }\n @else {\n @include _rfs-rule {\n #{$property}: if($rfs-mode == max-media-query, $val, $fluidVal);\n\n // Include safari iframe resize fix if needed\n min-width: if($rfs-safari-iframe-resize-bug-fix, (0 * 1vw), null);\n }\n\n @include _rfs-media-query-rule {\n #{$property}: if($rfs-mode == max-media-query, $fluidVal, $val);\n }\n }\n }\n}\n\n// Shorthand helper mixins\n@mixin font-size($value) {\n @include rfs($value);\n}\n\n@mixin padding($value) {\n @include rfs($value, padding);\n}\n\n@mixin padding-top($value) {\n @include rfs($value, padding-top);\n}\n\n@mixin padding-right($value) {\n @include rfs($value, padding-right);\n}\n\n@mixin padding-bottom($value) {\n @include rfs($value, padding-bottom);\n}\n\n@mixin padding-left($value) {\n @include rfs($value, padding-left);\n}\n\n@mixin margin($value) {\n @include rfs($value, margin);\n}\n\n@mixin margin-top($value) {\n @include rfs($value, margin-top);\n}\n\n@mixin margin-right($value) {\n @include rfs($value, margin-right);\n}\n\n@mixin margin-bottom($value) {\n @include rfs($value, margin-bottom);\n}\n\n@mixin margin-left($value) {\n @include rfs($value, margin-left);\n}\n","// scss-docs-start color-mode-mixin\n@mixin color-mode($mode: light, $root: false) {\n @if $color-mode-type == \"media-query\" {\n @if $root == true {\n @media (prefers-color-scheme: $mode) {\n :root {\n @content;\n }\n }\n } @else {\n @media (prefers-color-scheme: $mode) {\n @content;\n }\n }\n } @else {\n [data-bs-theme=\"#{$mode}\"] {\n @content;\n }\n }\n}\n// scss-docs-end color-mode-mixin\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n @if $font-size-root != null {\n @include font-size(var(--#{$prefix}root-font-size));\n }\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\n// scss-docs-start reboot-body-rules\nbody {\n margin: 0; // 1\n font-family: var(--#{$prefix}body-font-family);\n @include font-size(var(--#{$prefix}body-font-size));\n font-weight: var(--#{$prefix}body-font-weight);\n line-height: var(--#{$prefix}body-line-height);\n color: var(--#{$prefix}body-color);\n text-align: var(--#{$prefix}body-text-align);\n background-color: var(--#{$prefix}body-bg); // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n// scss-docs-end reboot-body-rules\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n border: 0;\n border-top: $hr-border-width solid $hr-border-color;\n opacity: $hr-opacity;\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: var(--#{$prefix}heading-color, inherit);\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 2. Add explicit cursor to indicate changed behavior.\n// 3. Prevent the text-decoration to be skipped.\n\nabbr[title] {\n text-decoration: underline dotted; // 1\n cursor: help; // 2\n text-decoration-skip-ink: none; // 3\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n background-color: var(--#{$prefix}highlight-bg);\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: rgba(var(--#{$prefix}link-color-rgb), var(--#{$prefix}link-opacity, 1));\n text-decoration: $link-decoration;\n\n &:hover {\n --#{$prefix}link-color-rgb: var(--#{$prefix}link-hover-color-rgb);\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: var(--#{$prefix}code-color);\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`