# 표준개발라이브러리

## 1. Overview

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

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

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

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

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

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

#### 1.1.1. 웹 부문

##### 웹 공통기능 제공

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

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

### 1.2. 주요특징

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

1. Monolithic Architecture & Micro Service Architecture

2. Single Page Application

3. Spring Boot 3 (Spring Framework 6)

4. Javascript Framework 도입 Vue.js

5. CSS Framework 도입 Bootstrap 5

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

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

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

Table 1. SDL 4.5 vs 5.0, 6.0
구분 | 4.5 | 5.0 | 6.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_HOME%을 추가한다.

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

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

| 사업장 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 에 접속해서 설치파일 다운로드

#### 2.1.3. Node.js 설치

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

| 6.0.0 버전 기준 nodejs 버전 20.14.x 이상을 권장하고 있다.

##### 설치 확인

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를 참고 바란다.

##### npm 설정 확인

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

#### 2.1.4. Eclipse (STS) Setting

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

Eclipse 실행 후 workspace 설정

Eclipse 실행 후 완료

Project Encoding 설정

Validation Disable All

Server 설정

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

#### 2.1.5. Eclipse Project Import

File → Import → Maven Project → Existing Maven Projects

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

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

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

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

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

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

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

#### 2.1.6. Lombok Plugin 설치

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

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

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

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

설치 완료

#### 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' 선택

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

"eslint"

"prettier"

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

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

JSON 파일에 아래 코드 추가

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

#### 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 입력 후 실행

### 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. 파라미터 설명
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 | 소스파일이 위치한 디렉토리 |

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

#### 2.2.3. Front-end 빌드

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

#### 2.2.4. Back-end 빌드

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

    mvn clean package -Pprod

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

| 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파일로 배포된다.

- 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. frontend : SDL Frontend 프로젝트

3. src/main/java : Java file

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

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

6. pom.xml

#### 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. 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

#### 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)

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 설정 파일

#### 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 : 하위에 업무별로 패키지명을 구분해 폴더 구성

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에서 제공하는 상태관리 + 패턴 라이브러리 위치

#### 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)

1 | SDL 공통컴포넌트
2 | SDL 공통팝업
3 | 결재관리
4 | 결재문서 양식 관리
5 | 배치관리
6 | 게시판관리
7 | 부서관리
8 | 기타관리
9 | HTML 양식 관리
10 | 로그관리
11 | 메인페이지
12 | 메뉴관리
13 | 역할관리
14 | 결재샘플
15 | 사용자관리
16 | 업무그룹관리
17 | 공통컴포넌트 사용 샘플

| 프로젝트에서 개발한 Vue 화면은 components/view 폴더 아래 있어야 한다. router 등록 시 디폴트 경로가 components/view 이다.

#### 3.3.4. css 위치 및 import 방법

사용하고자 하는 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. 다국어 세팅 위치 및 사용 방법

- vue-i18n을 통한 다국어 지원

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

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

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

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

#### 3.3.9. 사용자 검색 팝업

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

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 | 중요 프로퍼티 정보 암호화. 자세한 내용은 프로퍼티 값 암호화 관련 부분 참조
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곳이 있으므로 연계 신청시 사용자 위치에 따라 거점 신청에 주의하도록 한다. 거점 연계가 안되어 있는 사용자는 결재, 메일 기능을 사용 할 수 없다.

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 | 한국,구주,미주 서비스 주소

| 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를 상속받고 있다.

| Spring Boot supports traditional deployment

#### 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 설정
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 설정

#### 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인치 확인
2 | Active 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 팝업 띄우기

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

props 설명

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 설명

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명 | 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

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명 | 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

#### 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 시 그리고 저장/수정 시 하기 위해서는 아래와 같은 방법으로 처리한다.

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 처리를 자동으로 해준다.

- 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를 클릭시 사용자 상세 정보 화면으로 이동할 수 있다.

###### 기능별 설명

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

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

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

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

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

  - 세부적인 내용은 사용권한 신청/승인 매뉴얼을 참조한다.

##### 사용자 상세 조회 화면

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

###### 기능별 설명

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

- 사용자-역할 추가 : 선택된 역할을 추가하는 기능. 역할 팝업이 호출 되며, 사용자에게 부여하고 싶은 역할을 선택하여 확인 버튼 클릭시 사용자-역할 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. 부서 관리(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 테이블에 저장되어 있는 부서정보를 트리형태로 보여준다.

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

##### Table

- 부서(사용자정의) : TN_CF_DEPT_SELF

- 부서매핑 : TN_CF_DEPT_MAPPING

##### API

CustomDepartmentController.java

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

##### 부서관리(사용자정의) 기본 정보

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

###### 기본정보 필드 설명

- 부서코드 : 부서에 부여되는 코드(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. 사용자 승인취소
   PUT /auth/users/status/inactive
   Query ID : updateUser

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

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

#### 5.1.6. 외부 사용자 관리

##### 개요

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

##### 화면

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

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

###### 기능별 설명

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

#### 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. 역할 등록 : 역할 정보를 입력하는 Popup이 호출 되며 정보 기입 후 저장 버튼을 클릭하여 등록한다.

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

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

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

###### 역할-사용자 관리(RoleUserInfo.vue)

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

- 기능 설명

  1. 역할-사용자 목록 조회

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

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

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

###### 역할-업무그룹 관리(RoleWorkgroupInfo.vue)

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

- 기능 설명

  1. 역할-업무그룹 목록 조회

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

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

##### API & Service

###### API

- API : RoleController.java

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

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

  3. 역할 등록 : POST /roles

  4. 역할 수정 : PUT /roles

  5. 역할 삭제 : DELETE /roles

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

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

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

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

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

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

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

##### Entity Table & SQL

###### Entity Table

- TN_CF_ROLE : 역할

- TN_CF_USER_ROLE : 역할-사용자 맵핑

- TN_CF_WORKGROUP_ROLE : 역할-업무그룹 맵핑

###### SQL

1. 역할 목록 조회

    <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,
        --생략--

2. 역할 상세정보 조회

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

3. 역할 등록

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

4. 역할 수정

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

5. 역할 삭제

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

6. 역할-사용자 목록 조회

    <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,
    	--생략--

7. 역할-사용자 등록

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

8. 역할-사용자 수정

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

9. 역할-사용자 삭제

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

10. 역할-업무그룹 목록 조회

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

11. 역할-업무그룹 등록

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

12. 역할-업무그룹 삭제

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

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

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

###### 업무그룹-메뉴(WorkgroupMenuInfo.vue)

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

- 기능 설명

  1. 업무그룹-메뉴 목록 조회

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

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

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

###### 업무그룹-역할(WorkgroupMenuInfo.vue)

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

- 기능 설명

  1. 업무그룹-역할 목록 조회

  2. 업무그룹-역할 등록

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

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

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

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

##### API & Service

###### API

- API : WorkgroupController.java

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

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

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

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

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

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

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

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

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

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

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

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

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

- Service : WorkgroupServiceImpl.java

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

    @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. 업무그룹 목록 조회

    <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" />
    	--생략--

2. 업무그룹 상세정보 조회

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

3. 업무그룹 등록

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

4. 업무그룹 수정

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

5. 업무그룹 삭제

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

6. 업무그룹-메뉴 목록 조회

    <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,
        --생략--

7. 업무그룹-메뉴 등록

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

8. 업무그룹-메뉴 삭제

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

9. 업무그룹-역할 목록 조회

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

10. 업무그룹-역할 등록

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

11. 업무그룹-역할 수정

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

12. 업무그룹-역할 삭제

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

#### 5.1.10. 메뉴 관리

##### 개요

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

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

##### UI Design & Function

###### 메뉴 관리

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

- 기능 설명

  1. 메뉴 트리 목록 조회

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

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

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

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

###### 페이지 관리

- 기능 설명

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

  2. Page 등록, 수정, 삭제

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

###### API 관리

- 기능 설명

  1. API 목록 조회

  2. API 등록

  3. API 수정

  4. API 삭제

###### SDL 게시판 Page, API 프리셋 추가

- 기능 설명

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

##### API & Service

###### API

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

- 주요 기능 API 목록

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

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

  3. 메뉴 등록 : POST /auth/menus

  4. 메뉴 수정 : PUT /auth/menus

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

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

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

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

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

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

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

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

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

  14. 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

1. 메뉴 트리 목록 조회

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

2. 메뉴 상세정보 조회

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

3. 메뉴 등록

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

4. 메뉴 수정

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

5. 메뉴 삭제

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

6. 메뉴 이동

    <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}
    	--생략--

7. Page 목록 조회

    <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
    	--생략--

8. Page 등록

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

9. Page 수정

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

10. Page 삭제

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

11. API 목록 조회

    <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
    	--생략--

12. API 등록

    <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)
    	--생략--

13. API 수정

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

14. API 삭제

    <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 에 구현되어 있다.

###### 기능별 설명

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

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

#### 5.1.12. Sitemap

##### 개요

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

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

##### 화면

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

| 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 파일이 없는 경우 아래와 같은 에러가 발생하므로 주의한다.

###### 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);
        };
      }));
    },

1 | WebSocket 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. 게시판 상세정보 조회
   GET /boards/{boardId}
   Query ID : selectBoard

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

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

    @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());
    }

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

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

    @Override
    @Transactional
    public void updateBoard(Board board) {

        // 게시판 수정.
        boardDao.updateBoard(board);

        // 게시판 분류 수정.
        List<BoardClassification> beforeClassifications = boardDao.getBoardClassificationList(board.getBoardId());
        List<String> beforeClassificationIdList = new ArrayList<>();
        -- 생략 --
    }

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

##### 화면

1. 게시판 목록 화면

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

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

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

2. 게시판 등록 화면

- 게시판을 등록할 수 있다.

- 게시판 세부기능 속성

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

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

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

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

3. 게시판 상세정보 수정 화면

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

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

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

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

  - 각 컬럼의 사용 여부에 따라서 넓이가 합산 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. 공지사항 팝업 게시글 체크
   POST /check-notice
   Query ID : selectNoticeChecked, insertNoticeChecked

##### 화면

- MainTopMenu.vue

#### 5.2.4. 메인 페이지

##### 구성

- Header Section (TopMenu)

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

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

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

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

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

#### 5.2.5. 링크 사이트

##### 개요

링크 사이트를 관리한다.

##### Table

- 링크사이트 : TN_CF_LINK_SITE

##### API

LinkSiteController.java

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

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

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

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

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

##### 화면

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

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

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

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

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

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

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

##### 일괄작업 관리

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

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

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

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

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

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

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

##### 일괄작업 이력

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

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

- 작업이 실패 했을경우 실행 할 수 있는 배치실행 버튼이 나타난다.(일괄작업관리 등록시 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
2 | Job 구분 명
3 | Job 등록
4 | Cron 표현식 설정

#### 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. orderBy는 기본이 COMM_CODE_TYPE_CODE(코드명) 이다.

2. 그룹 코드 등록
   POST /commcode/groupcodes

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

3. 그룹 코드 삭제
   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. 그 후 그룹 코드의 DELETE_YN 컬럼을 true로 업데이트 하여 삭제한다.

         1. Query ID : updateGroupCode

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

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

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

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

     - Query ID : updateCommCodeDeleted

##### 화면

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

###### 기능별 설명

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

##### 코드 등록

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

###### 기능별 설명

- 목록 : 코드 관리 화면으로 이동

- 저장 : 그룹 코드 정보를 저장

##### 코드 상세 정보

그룹 코드의 상세 정보를 수정, 삭제하고 그에 대한 공통 코드를 추가, 수정, 삭제하여 관리

###### 기능별 설명

그룹코드 정보

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

- 목록 : 코드 관리 화면으로 이동

- 삭제 : 그룹코드를 삭제

- 저장 : 수정된 그룹코드 상세 정보를 수정

공통코드 목록

- 추가 : 공통코드를 등록하는 Popup 호출

- 수정 : 공통코드를 수정하는 Popup 호출

- 삭제 : 공통코드를 삭제

#### 5.2.10. 업무 담당자 관리

##### 개요

주요 업무 담당자 페이지를 관리한다.

| 정적 HTML 로 만든 페이지를 업로드 하여 그대로 보여주고 싶을 때 사용한다. (업무 담당자 관리, 주요 연락처 관리)

##### 화면

- 등록된 업무 담당자 페이지 목록을 볼 수 있다.

- 목록의 Key 컬럼을 클릭하여 업무 담당자 페이지 정보를 수정할 수 있다.

- 팝업 미리보기를 클릭하여 등록된 업무 담당자 페이지를 확인 할 수 있다.

- Key: Unique한 키를 지정(영문, 숫자만 가능)

- 제목: 업무 담당자 페이지 제목

- 설명: 업무 담당자 페이지 설명

- 첨부파일: 업무 담당자 HTML 페이지 (html, htm 확장자)

- 메뉴관리에서 메뉴를 등록하고 Page/API 를 추가한다.

  - Page Path에 업무 담당자 관리에서 등록한 Key를 포함한다.

  - Vue Component는 SampleStaff.vue 를 참조하여 생성 후 등록한다.

  - API URL에 업무 담당자 조회 API URL을 등록한다. (/html-templates/group/{templateGroupCode=STAFF}/key/{templateKey})

#### 5.2.11. 주요 전화 번호 관리

##### 개요

주요 전화번호 페이지를 관리한다.

##### 화면

- 등록된 주요 전화번호 페이지 목록을 볼 수 있다.

- 리스트의 Key 컬럼을 클릭하여 주요 전화번호 페이지 정보를 수정할 수 있다.

- 팝업 미리보기를 클릭하여 등록된 주요 전화번호 페이지를 확인 할 수 있다.

- 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을 변경할 수 있다.

1 | Logger name별 설정된 configuration 내역(log4j2.xml)을 조회할 수 있다.
2 | Logger 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

##### 화면

메뉴에 대한 사용 주기, Hit, 활용도를 조회

#### 5.3.5. 메뉴 사용 이력

##### 개요

시스템에 접속한 사용자의 메뉴 사용 이력을 조회

##### Table

- 메뉴 사용 이력 : TN_CF_MENU_USE_HISTORY

##### API

HistoryController.java

1. 일별 메뉴 사용 이력 조회
   GET /history/menu-use-by-date
   Query ID : selectMenuUseHistoryByDatePagingList

2. 월별 메뉴 사용 이력 조회
   GET /history/menu-use-by-month
   Query ID : selectMenuUseHistoryByMonthPagingList

##### 화면

###### 기능별 설명

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

#### 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' 이다.

##### 화면

엑셀 다운로드 기능이 있는 특정 화면에서 엑셀 다운로드 실행에 대한 이력을 조회.

###### 기능별 설명

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

#### 5.3.7. 로그인 이력

##### 개요

사용자가 시스템에 로그인한 이력을 조회

##### Table

- 로그인 이력 : TN_CF_LOGIN_OUT

- 사용자 : TN_CF_USER

##### API

HistoryController.java

1. 로그인 이력 목록 조회
   GET /history/login-out-logs Query ID : selectListLoginOut

##### 화면

사용자가 시스템에 로그인한 이력을 조회

##### 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)을 통하여 연계 신청을 진행한다.

| 스테이지 연계 신청한 Knox Portal 계정에 한하여 운영 연계신청이 가능하므로, 중복신청 방지 및 일관된 관리를 위해 현업 담당자가 신청하는 것을 권장한다.

2. Knox Dev Center내 연계 신청 가이드 메뉴를 참고하여 스테이지 연계부터 신청을 진행한다.

3. 스테이지 연계 신청 및 승인이 완료되면 API 연계 ID/Token 발급 및 API 구독이 완료되고, 신청자에게 메일로 관련 내용이 통보된다.

4. SDL에서 제공하는 Knox 연계 모듈을 이용한 서비스를 개발하여 연계 테스트를 진행한다.

5. 스테이지 연계 테스트가 완료되면 운영도 스테이지와 동일하게 시스템을 통해 신청하여 테스트한다.

#### 5.4.2. 결재

##### 개요

SDL에서는 Knox결재와 동기화 되는 결재 모듈을 제공하고 있는데, 여기서는 Knox Portal REST API 연계 서비스 신청 및 Knox결재 서비스 연계 부분에 대하여 설명한다. 결재화면 및 기능구현 설명은 해당 가이드를 참조한다.

###### Knox결재 연계 설정

1. Knox REST API 연계 서비스 신청이 되었다면, 발급받은 `system-id`, `token` 값을 설정한다.

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 | 거점 (국내, 구주, 미주)

2. 결재화면 및 기능구현 설명은 해당 가이드를 참조한다.

   - Knox상신

| 개발자의 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 값이다.

    @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);
    }

2. Knox 보안 결재 상신
   POST /knox/approvals/secu-submit

   - 파라미터는 일반 결재 상신과 같지만 보안문서타입이 "CONFIDENTAIL" 이다.

    knoxApproval.setDocSecuType("CONFIDENTAIL");

3. Knox 결재 상세 상황 조회
   GET /knox/approvals/{apInfId}/detail

   - 결재 연계 ID로 결재 문서의 정보를 상세 조회한다.

4. Knox 결재 본문 조회
   GET /knox/approvals/{apInfId}/content

   - 결재 연계 ID로 결재 문서의 본문을 조회한다.

5. Knox 결재 상황 조회
   POST /knox/approvals/status

   - 결재문서의 진행 상태를 조회한다.

   - 복수개의 결재 연계 ID를 요청하여 각각 해당하는 문서변경횟수와 결재상태정보를 응답받는다. 이를 이용하여 결재문서 동기화시 변경된 건에 대해 결재 상태를 업데이트 한다.

    public List<KnoxApprovalStatus> getStatus(@RequestBody List<KnoxApprovalStatus> knoxApprovalStatusList) {

6. Knox 결재 연계 ID 조회
   POST /knox/approvals/apinfids

   - 결재 ID로 결재 연계 ID를 조회한다.

    public KnoxApproval getApInfIds(@RequestParam String apId) {

7. Knox 상신함 리스트 조회
   POST /knox/approvals/submission

   - 상신자가 상신한 정보를 조회한다.

    public List<KnoxApproval> getApInfIdInfos(@RequestParam String epId) {

8. Knox 연계 이력 조회
   GET /knox/approvals/apinfidinfos

   - 요청 시스템에서 상신된 결재문서의 연계 이력을 조회한다.

9. Knox 상신 취소
   POST /knox/approvals/{apInfId}/cancel

   - 결재 문서를 상신취소한다.

10. Knox 완결 처리
    POST /knox/approvals/{apInfId}/autoprogress

    - 결재문서를 완결처리한다.

##### 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 샘플에서와 같이 `@ApprovalDocument` 의 `templateEngine=엔진타입` 과 `templateFile="템플릿파일경로"` 값을 등록한다.

##### 결재 상신

###### 결재 상신 UI

샘플 문서를 등록 하고나면 상세하면 하단에 결재 경로를 설정할 수 있는 결재 스텝을 입력 받는 Component가 표시된다.
결재 문서 화면 개발시 이 Component를 붙여서 상신 기능을 구현한다.

#### 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. 대상 목록의 결재ID로 Knox REST를 조회하여 Revision이 변경되었는지 확인.

3. Revision 변경된 대상에 대해서 결재 정보 동기화 처리.

4. 동기화 완료 후 결재 후처리 진행.

결재 동기화 시 오류가 발생한 결재 문건은 결재정보 테이블(TN_CF_APPROVAL)에 결재 동기화 상태값(APPROVAL_FAULT)이 false로 Update되며 다음 배치 실행시 대상으로 선정되지 않는다.
이런 경우 이벤트 로그 테이블(TN_CF_APPROVAL_EVENT)의 로그를 확인하여 다시 동기화 해야 한다면 결재 관리 상세화면에서 개별 동기화가 가능하다.

##### 결재 전후처리

결재 동기화 처리전 또는 처리후 결재 문서 상태에 대한 비지니스를 처리할 수 있도록 인터페이스를 제공하고 있다.

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. 시스템 전체 결재 목록 조회(페이징)
   GET /approval/approval-with-paging
   Query ID : selectApprovalPagingList

3. 결재 상세정보 조회
   GET /approval/{approvalRequestId}
   Query ID : selectApprovalStep, selectApprovalEventList

##### 화면

###### 검색 조건

- 구분 : knox 결재/ 내부결재

- 상태 : 결재중/완료/후완결/반려/취소

- 문서명 : 결재양식 목록

- 상신자 : 결재 요청한 상신자

- 동기화 상태 : 성공/실패

- 기간 : 결재요청 기간

#### 5.4.6. 결재 경로 관리

##### 개요

시스템의 결재 문서 샘플을 RUNTIME 동안 가지고 있다가 결재시에 사용한다.

- ApprovalManager.java → ApprovalDocument.java → SampleApprovalDocument.java

  1. ApprovalManager : @ApprovalDocument라는 어노테이션이 달린 클래스를 찾는다.

  2. SampleApprovalDocument : 결재경로 관리 목록에 결재 문서 샘플을 보여준다.

##### Table

- 결재 경로 : TN_CF_DYNAMIC_APPROVAL_PATH

- 필수 결재자 : TN_CF_REQUIRED_APPROVAL_USER

##### API

ApprovalController.java

1. 시스템 전체 결재 문서 조회
   GET /approval/approval-doc-types

2. 결재 경로 조회
   GET /approval/dynamic-approval-paths/book
   Query ID : selectDynamicApprovalPath

   - 기본결재 경로 목록을 보여준다.

3. 결재 경로 저장
   POST /approval/dynamic-approval-paths/book
   Query ID : deleteDynamicApprovalPath, insertDynamicApprovalPath

4. 필수 결재자 목록 조회
   GET /approval/required-approval-users/book
   Query ID : selectRequiredApprovalUserList

   - 필수 결재자 목록을 보여준다.

5. 필수 결재자 저장
   POST /approval/required-approval-users/book
   Query ID : deleteRequiredApprovalUser, insertRequiredApprovalUser

##### 화면

지정된 문서타입에 따른 결재경로를 관리기능 > 결재/메일 관리 > 결재경로 관리를 통해 지정할 수 있다.

- 지정된 문서타입 목록을 확인할 수 있다.

- 문서타입을 선택 후 해당 문서에 대한 기본 결재경로와 필수 결재자를 추가할 수 있다.

- 결재 상신시 결재자 목록에 지정된 기본결재 경로가 자동 추가되며, 지정된 필수 결재자가 있는 경우 추가하라는 알림을 준다. (다수중 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. 템플릿 상세 조회
   GET /templates/group/{templateGroupCode}/key/{templateKey}
   Query ID : selectTemplate

3. 템플릿 상세 조회 (By ID)
   GET /templates/{id}
   Query ID : selectTemplate

   - 양식의 상세 내용을 조회 하거나 팝업 미리보기를 할 수 있다.

4. 템플릿 Key 중복 체크
   GET /templates/dup-check/group/{templateGroupCode}/key/{templateKey}
   Query ID : selectTemplate

   - 저장 전 템플릿 Key 중복 여부를 검사한다.

5. 템플릿 등록
   POST /templates/group/{templateGroupCode}
   Query ID : insertTemplate

   - 양식을 저장한다.

6. 템플릿 수정
   POST /templates/{id}
   Query ID : updateTemplate

7. 템플릿 삭제
   DELETE /templates/{id}
   Query ID : deleteTemplate

##### 화면

결재양식을 관리기능 > 결재/메일관리 > 결재양식 관리를 통해 볼 수 있다.

- 등록된 결재양식 목록을 결재양식 관리를 통해 볼 수 있다.

- 목록의 Key 컬럼을 클릭하여 결재양식 정보를 수정할 수 있다.

- 팝업 미리보기 컬럼을 클릭하여 등록된 결재양식의 첨부파일을 확인 할 수 있다.

- 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. 미결함 목록 조회(내부 결재)
   GET /internal-approvals/not-approve
   Query ID : selectInternalApprovalPagingListByNotApprove

   - 미결중인 내부결재 문서를 조회한다.

3. 기결함 목록 조회(내부 결재)
   GET /internal-approvals/approved
   Query ID : selectInternalApprovalPagingListByApproved

   - 사용자가 결재 완료한 내부결재 문서를 조회한다.

4. 통보함 목록 조회(내부 결재)
   GET /internal-approvals/notice
   Query ID : selectInternalApprovalPagingListByNotice

   - 사용자가 통보 대상인 결재문서를 조회한다.

5. 내부결재 문서 상신 취소
   POST /internal-approvals/{approvalRequestId}/cancel
   Service : cancelInternalApproval Query ID : updateInternalApprovalStep

   - 결재 문서 상신을 취소한다.

6. 내부결재 문서 결재 승인 또는 합의
   POST /internal-approvals/{approvalRequestId}/confirm
   Service : confirmInternalApproval Query ID : updateInternalApprovalStep

   - 결재 문서 승인 또는 합의 한다.

7. 내부결재 문서 반려
   POST /internal-approvals/{approvalRequestId}/reject
   Service : rejectInternalApproval Query ID : updateInternalApprovalStep

   - 결재 문서를 반려 한다.

ApprovalSampleController.java

1. 내부결재 문서 목록 조회(페이징)
   GET /internal-approval/sample-document-with-paging
   Query ID : selectSampleApprovalDocumentPagingList

   - Sample Document 문서 목록을 조회한다.

2. 내부 결재 문서 상신(Sample Document)
   POST /internal-approval/submit/sample-document

   - Knox 결재 정보 동기화 로직을 제외한 다른 부분은 Knox 결재 상신과 동일하다.

   - 동기화 배치 로직 대상에서 제외된다.

   - 내부결재 전후 처리 로직은 문서 타입별로 Interceptor 클래스를 구현하여 처리한다.

    @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. 내부결재 문서 미결함

- 사용자의 결재 차순에 있는 내부결재 문서 목록을 조회한다.

- 상세화면에 진입하여 승인/합의 또는 반려가 가능하다.

3. 내부결재 문서 기결함

- 사용자의 결재 완료한 내부결재 문서 목록을 조회한다.

4. 내부결재 문서 통보함

- 결재 완료 후 사용자에게 통보된 내부결재 문서 목록을 조회한다.

#### 5.4.9. 대리 결재

##### 개요

내부결재에 적용되는 대리결재자를 지정하는 기능을 제공한다.
(Knox 결재 대리결재는 Knox portal에서 지정 가능함)

##### Table

- 결재 정보 : TN_CF_APPROVAL_DELEGATE

##### API

ApprovalController.java

1. 대리결재자 조회
   GET /approval/approver-delegate
   Query ID : selectApproverDelegate

   - 사용자의 대리결재자를 조회한다.

2. 대리결재자 저장
   POST /approval/approver-delegate
   Query ID : updateApproverDelegate, insertApproverDelegate

   - 사용자의 대리결재자를 저장한다.(등록 또는 변경)

3. 대리결재자 삭제
   DELETE /approval/approver-delegate
   Query ID : deleteApproverDelegate

   - 사용자의 대리결재자를 삭제한다.

##### 화면

1. 대리결재자 조회 및 지정

- 사용자 정보 > 대리결재 메뉴를 통해서 대리결재자 등록이 가능하다.

- 기등록된 대리결재자가 존재한다면 대리결재자 팝업 상단에 표시되며 삭제하거나 다른 사용자를 선택하여 변경가능 하다.

#### 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. 보낸 메일 이력

##### 개요

메일 발송 내역 및 수신 상태를 조회할 수 있다.

###### 보낸 메일 목록

1 | 기간, 제목별 검색 가능
2 | 클릭시 상세 내역 조회

###### 보낸 메일 상세 정보

1 | 보낸 메일의 수신인별 개봉 여부
2 | 보낸 메일 본문 확인 가능
3 | 수신인과 수신상태 정보 목록

#### 5.4.12. 메일 그룹 관리

##### 개요

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

##### UI Design & Function

###### 메일 그룹 목록(MailGroupList.vue)

메일 그룹 등록, 수정 및 삭제가 가능하다.

- 기능 설명

  1. 메일 그룹 목록 조회

  2. 메일 그룹 상세정보 조회

  3. 메일 그룹 등록 : 등록 버튼 클릭 시 메일 그룹 입력 popup 호출.

  4. 메일 그룹 수정 : checkbox 선택 후 수정 버튼 클릭 시 변경을 위한 popup 호출.

  5. 메일 그룹 삭제 : checkbox 선택 후 삭제 버튼 클릭 시 삭제.

###### 메일 그룹 맵핑 수정(MailGroupMappEdit.vue)

메일 그룹 맵핑 목록 등록 및 삭제가 가능하다.

- 기능 설명

  1. 메일 그룹 맵핑 목록 조회

  2. 메일 그룹 맵핑 저장

     1. 사용자 추가 : 버튼 클릭 시 사용자 추가 popup 호출.

     2. 역할 추가 : 버튼 클릭 시 역할 추가 popup 호출.

     3. 업무그룹 추가 : 버튼 클릭 시 업무그룹 추가 popup 호출.

##### API & Service

###### API

- API : MailGroupController.java

  1. 메일 그룹 목록 조회 : GET /mail-group-with-paging

  2. 메일 그룹 상세정보 조회 : GET /mail-group/{mailGroupId}

  3. 메일 그룹 등록 : POST /mail-group

  4. 메일 그룹 수정 : PUT /mail-group

  5. 메일 그룹 삭제 : DELETE /mail-group

  6. 메일 그룹 맵핑 목록 조회 : GET /mail-group-mapp-with-paging

  7. 메일 그룹 맵핑 저장 : POST /mail-group-mapp/{mailGroupId}

- Service : MailGroupServiceImpl.java

  1. 메일 그룹 맵핑 저장
     맵핑 정보 저장 시 기등록 되어 있는 맵핑 목록 삭제 후 전체 목록을 다시 저장하도록 구현.

    @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. 메일 그룹 목록 조회

    <select id="selectMailGroupPagingList" parameterType="java.util.HashMap" resultMap="mailGroupResult">
    	SELECT T.*
    	FROM   (SELECT ROW_NUMBER() OVER(ORDER BY LABEL ASC) ROWNUM,
    	--생략--

2. 메일 그룹 상세정보 조회

    <select id="selectMailGroup" parameterType="java.util.HashMap" resultMap="mailGroupResult">
    	SELECT <include refid="columnMailGroup" />,
    	       (SELECT USER_NAME
    	       --생략--

3. 메일그룹 등록

    <insert id="insertMailGroup" parameterType="java.util.HashMap">
    	INSERT INTO <include refid="tableMailGroup" />
    	(<include refid="columnMailGroup" />)
    	--생략--

4. 메일그룹 수정

    <update id="updateMailGroup" parameterType="java.util.HashMap">
    	UPDATE <include refid="tableMailGroup" />
    	SET    LABEL = #{label},
    	--생략--

5. 메일그룹 삭제

    <delete id="deleteMailGroup" parameterType="java.util.HashMap">
    	UPDATE <include refid="tableMailGroup" />
    	SET    DELETED = '1',
    	--생략--

6. 메일그룹 맵핑 목록 조회

    <delete id="deleteMailGroup" parameterType="java.util.HashMap">
    	UPDATE <include refid="tableMailGroup" />
    	SET    DELETED = '1',
    	--생략--

7. 메일그룹 맵핑 저장

    <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;
    }

1 | Knox 메일별 수신자들의 개봉상태 카운트 조회
2 | Knox 수신인 별 메일 수신 상황 조회

#### 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. 템플릿 상세 조회
   GET /templates/group/{templateGroupCode}/key/{templateKey}
   Query ID : selectTemplate

3. 템플릿 상세 조회 (By ID)
   GET /templates/{id}
   Query ID : selectTemplate

   - 양식의 상세 내용을 조회 하거나 팝업 미리보기를 할 수 있다.

4. 템플릿 Key 중복 체크
   GET /templates/dup-check/group/{templateGroupCode}/key/{templateKey}
   Query ID : selectTemplate

   - 저장 전 템플릿 Key 중복 여부를 검사한다.

5. 템플릿 등록
   POST /templates/group/{templateGroupCode}
   Query ID : insertTemplate

   - 양식을 저장한다.

6. 템플릿 수정
   POST /templates/{id}
   Query ID : updateTemplate

7. 템플릿 삭제
   DELETE /templates/{id}
   Query ID : deleteTemplate

##### 화면

메일양식을 관리기능 > 결재/메일관리 > 메일양식 관리를 통해 할 수 있다.

- 등록된 메일양식 목록을 메일양식 관리를 통해 볼 수 있다.

- 목록의 Key 컬럼을 클릭하여 메일양식 정보를 수정할 수 있다.

- 팝업 미리보기 컬럼을 클릭하여 등록된 메일양식의 첨부파일을 확인 할 수 있다.

- 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. 약관 관리

##### 이용약관동의 관리

이용약관동의관리 화면을 관리한다.

##### 이용약관동의 등록

등록 화면에서 새로운 약관 생성이 가능하다.

구분은 공통코드의 `TERMS` 정보를, 언어는 `TERMS_LANG` 정보를 참조한다.

##### 이용약관동의 목록 추가

그룹코드 관리에서 약관동의 목록 추가가 가능하다.

신규 사용자 로그인 시 화면에 아래와 같이 나타난다.

##### 신규사용자 이용약관 동의

신규 사용자 로그인 시 화면에 아래와 같이 나타난다.

#### 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. EU와 거래하는 시스템은 이용약관동의 관리에서 '유럽연합 개인정보보호 규정’이라는 약관을 추가하여 사용자의 동의를 받아야 한다.

   - 세부적인 내용은 약관 관리 매뉴얼을 참조한다.

#### 5.5.5. 관리자 IP 관리

##### 관리자 IP 관리

관리자용 계정의 IP를 관리

###### 기능별 설명

- 삭제 : 등록된 계정을 삭제

- 추가 : 관리자용 계정 정보를 등록하기 위해 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 로그인시 사용하는 비밀번호의 변경 및 초기화가 가능하다.

###### 비밀번호 변경

로그인된 상태에서 비밀번호 변경이 가능하다.

###### 비밀번호 초기화

로그아웃된 상태에서 ID/PWD 로그인시 비밀번호 초기화를 할 수 있다.
이메일로 임시 비밀번호가 발송된다.

Figure 1. 로그인 화면 | 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. 타임존 저장
   PUT /auth/users/timezone

##### 화면

사용자의 Timezone을 설정하는 기능으로, Timezone을 설정하게 되면 Local Storage의 user.timeZoneId, user.timeZoneCode 에 저장된다.

TopMenu - 표준시간을 통해 접근 가능.

| 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. message-common_fr_FR.properties 파일을 생성

  3. 메세지의 프랑스어 버전 작성

| 메세지들만 추가되므로 메뉴관리, 역할관리, 업무그룹관리 등 다국어 컬럼(LABEL_JSON)을 지원하는 table 데이터의 경우 직접 입력하여야 한다.

##### API

MessageBundleController.java

1. 로케일별로 메세지를 조회
   GET /noauth/messages

2. 설정한 모든 언어의 메세지를 조회
   GET /noauth/messages/all

##### 화면

한국어,영어 중 원하는 언어로 변경하여 화면을 나타내는 기능으로, 언어를 설정하게 되면 User Token에 저장 되어 로그아웃을 하게 되더라도 마지막에 변경된 언어로 설정된다.

TopMenu - 언어선택을 통해 접근 가능.(단, 다국어 지원 되는 영역에 한해서만 지원).

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

#### 5.6.3. 번역(Utrans)

##### 개요

Utrans API를 호출하여 한국어 및 영어 등 언어로 번역하는 기능.

###### 지원 되는 언어방향

- 유럽어 : 러시아어,스페인어,독일어,프랑스어,이탈리아어,포르투갈어

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. 엑셀 업로드
   POST /excel/excel-upload

##### 엑셀 다운로드

###### excel.xml 설정

sdl-base/src/main/resources/excel 폴더에 다운로드 할 기능의 xml 양식을 만든다.
다국어 적용(message.properties)이 된다.

1. 대외비 표기여부, 시트보호 여부, 제목

    <CONFIDENTIAL>true</CONFIDENTIAL>   <!--> 대외비 표기여부(false : 태그작성 x) <-->
    <PROTECTION>true</PROTECTION>       <!--> 시트보호 여부(false : 태그작성 x) <-->
    <PASSWORD>sdl</PASSWORD>            <!--> 시트보호 암호(PROTECTION = false : 태그작성 x) <-->
    <TITLE>sdl.excel.user.title</TITLE> <!--> 제목 <-->

2. 문서 Comment 출력

   1. 코멘트를 여러개 작성 가능

    <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>

3. 헤더

    <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>

4. 컬럼

   1. 첫번째 Row 에 No. 필드 넣을 경우 No. 필드에 대한 <COLUMN>를 작성하지 않아도 된다.

    <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

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 값은 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());
    }

1 | Spring Bean을 AutoWiring
2 | Job 정보를 조회, 저회 저장할 때 사용하는 Datasource (resources-dev, resources-prod/application.properties 참조)
3 | Quartz 전용 TransactionManager
4 | Quartz 설정 파일 세팅
5 | SchedulerFactoryBean 에서 실행 할 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종으로 구성되어 있습니다

- 각 페이지 화면의 우측 상단에 개발과 디자인 탭으로 구분되어 있습니다.

  개발 탭에서는 개발 가이드와 간단한 기능 테스트를 해볼 수 있는 Code Demo 가 있고, 디자인 탭에서는 전사 UX 표준을 바탕으로 디자인 원칙과 구성요소들을 확인할 수 있습니다.

#### 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

   로컬 파일로부터 컴포넌트를 설치할 수 있습니다. 아래의 경로에서 파일을 다운로드 받습니다.

   sdl-component-3.0.0.tgz 다운로드

   해당 파일을 로컬의 임의의 위치로 복사한 다음, NPM 명령어로 컴포넌트를 설치합니다. 설치하실 프로젝트 루트 폴더로 이동한 다음, 아래 명령어를 커맨드 창에서 입력합니다.

       npm install --save ./{your-path}/sdl-component-{version}.tgz

2. 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';

3. 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);

4. Complete

   이제 시스템 전역에서 'sdl-component’를 사용하실 수 있습니다

#### 6.3.5. UI 라이브러리 목록

활용빈도, 사용성을 고려하여 사내시스템 개발에 필요한 Library를 제공합니다.

- 레이아웃

  화면의 구조를 정의하는 레이아웃 유형

  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 | 본 화면의 상하좌우 중앙에 콘텐츠 박스가 위치

- 컴포넌트

  화면을 구성하는 요소 단위

  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 | 한 공간에 여러 개의 콘텐츠를 슬라이드 형태로 제공

### 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. 암호화 권한 결재 완료되면 D-KMS SDK 및 가이드 전송됨

3. 전달 받은 SDK 설치 및 코드 적용

#### 6.5.2. 코드 적용

##### Credential 생성 및 환경변수 추가

1. D-KMS 가이드를 참고하여 Credential을 생성하고 시스템 환경변수에 추가한다.

##### SDL 수정

1. D-KMS 암호화 라이브러리 추가

   - D-KMS 가이드를 참고하여 라이브러리를 추가한다.

2. src/main/resources-{local|dev|prod} 에 dkms.properties 파일을 추가한다. (배포된 sdl-appendix 내 참조)

| - dkms.task-id 프로퍼티 값을 D-KMS 신청시 부여받은 값으로 수정해야한다. - 기타 다른 프로퍼티 값도 제대로 설정되었는지 확인한다.

3. mybatis typehandler 추가

   - 암호화 대상이 되는 컬럼에 대해 typehandler 를 적용한다.

   - com.samsung.dkms.handler 패키지 생성

   - EncryptionTypeHandler.java, NameEncryptionTypeHandler.java, EmailEncryptionTypeHandler.java, PhoneEncryptionTypeHandler.java, AddressEncryptionTypeHandler.java, BirthdayEncryptionTypeHandler.java 파일을 복사/붙여넣기 한다. (배포된 sdl-appendix 내 참조)

4. mybatis-config.xml 수정

   - mapper xml 에 적용이 용이하도록 typehandler의 type alias 를 추가한다.

5. mapper xml 에 mybatis typehandler 적용

   - 암호화가 필요한 컬럼에 해당하는 핸들러를 선택 적용한다.

   - mapper xml에 typehandler 적용 방법은 샘플파일(mapper-mybatis-user.xml, 배포된 sdl-appendix 내 참조)을 참고한다. (`""유무에 유의`)

     - select resultMap 적용 예

     - insert, 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

##### 테이블 수정 및 데이터 마이그레이션

1. 테이블 컬럼 사이즈 변경

   - 암호화 대상 컬럼의 사이즈를 최소 255byte 가 되도록 변경한다.

2. 기존 데이터 암호화를 위한 마이그레이션은 D-KMS 가이드를 참고한다.

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에 영향을 줄 수 있음