Initial commit

This commit is contained in:
2026-05-29 17:49:25 +09:00
commit 330105cb27
1081 changed files with 148694 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
= 결재/메일
== Knox REST API 연계 서비스 신청
.이 내용은 Knox결재외에 Knox메일 등 다른 Knox Portal REST API 연계 서비스 이용시에도 공통으로 해당되는 내용이다.
* SDL은 Knox Rest API를 이용하여 결재/메일 등의 서비스를 제공한다. 따라서 우선 Knox 연계서비스를 신청하고 시스템 아이디와 토큰을 발급받아야 한다.
* 시스템 아이디와 Token 발급이 완료되면, API 샘플 테스트도 가능하다.
//-
. Knox Portal Support내 연계 신청(Knox Dev Center)을 통하여 연계 신청을 진행한다.
NOTE: 스테이지 연계 신청한 Knox Portal 계정에 한하여 운영 연계신청이 가능하므로, 중복신청 방지 및 일관된 관리를 위해 현업 담당자가 신청하는 것을 권장한다.
[start=2]
. Knox Dev Center내 연계 신청 가이드 메뉴를 참고하여 스테이지 연계부터 신청을 진행한다.
. 스테이지 연계 신청 및 승인이 완료되면 API 연계 ID/Token 발급 및 API 구독이 완료되고,
신청자에게 메일로 관련 내용이 통보된다.
. SDL에서 제공하는 Knox 연계 모듈을 이용한 서비스를 개발하여 연계 테스트를 진행한다.
. 스테이지 연계 테스트가 완료되면 운영도 스테이지와 동일하게 시스템을 통해 신청하여 테스트한다.
:leveloffset: +1
include::결재메일/결재.adoc[leveloffset]
include::결재메일/Knox상신.adoc[leveloffset]
include::결재메일/Knox결재상태동기화.adoc[leveloffset]
include::결재메일/결재관리.adoc[leveloffset]
include::결재메일/결재경로관리.adoc[leveloffset]
include::결재메일/결재양식관리.adoc[leveloffset]
include::결재메일/내부결재.adoc[leveloffset]
include::결재메일/대리결재.adoc[leveloffset]
include::결재메일/메일.adoc[leveloffset]
include::결재메일/보낸메일이력.adoc[leveloffset]
include::결재메일/메일그룹관리.adoc[leveloffset]
include::결재메일/메일상태조회.adoc[leveloffset]
include::결재메일/메일양식관리.adoc[leveloffset]
include::결재메일/임직원.adoc[leveloffset]
include::결재메일/주소록.adoc[leveloffset]
include::결재메일/메신저.adoc[leveloffset]
include::결재메일/MHTML변환.adoc[leveloffset]
@@ -0,0 +1,57 @@
= Knox 결재 상태 동기화
== 결재 Batch 설정
Knox 결재 문서는 10분마다 배치가 실행되어 동기화 되고 있다.
* config.properties : 동기화 배치 실행 주기 설정
[source,properties]
----
## Knox Approval Sync
knox.approval.sync.cron=0 0/10 * * * ?
----
* QuartzConfig.java: Scheduler 등록
* KnoxSyncBatchConfig.java: 배치 Job, Trigger 등록
* KnoxSyncBatchExecutor.java: 서비스 호출
== 결재 동기화 로직
배치에서 실행되는 동기화 로직을 구현한 서비스는 knoxApprovalSyncServiceImpl.java 파일의 synchronizeKnox() 메소드 이며 동기화 로직은 간략하게 아래와 같다.
. SDL 결재 테이블에서 결재 진행중인 문서 목록 대상 조회.
. 대상 목록의 결재ID로 Knox REST를 조회하여 Revision이 변경되었는지 확인.
. Revision 변경된 대상에 대해서 결재 정보 동기화 처리.
. 동기화 완료 후 결재 후처리 진행.
결재 동기화 시 오류가 발생한 결재 문건은 결재정보 테이블(TN_CF_APPROVAL)에 결재 동기화 상태값(APPROVAL_FAULT)이 false로 Update되며 다음 배치 실행시 대상으로 선정되지 않는다. +
이런 경우 이벤트 로그 테이블(TN_CF_APPROVAL_EVENT)의 로그를 확인하여 다시 동기화 해야 한다면 결재 관리 상세화면에서 개별 동기화가 가능하다.
image::approvalSync_01.png[]
== 결재 전후처리
결재 동기화 처리전 또는 처리후 결재 문서 상태에 대한 비지니스를 처리할 수 있도록 인터페이스를 제공하고 있다.
ApprovalInterceptor 를 결재 문서별로 상속 받는 구현 클래스를 만들면 된다.
* 구현 예시(KnoxApprovalSampleInterceptor.java)
[source,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);
}
-- 생략 --
----
@@ -0,0 +1,124 @@
= Knox 상신
== 결재 Entity Class 생성 및 설정
결재 기능을 구현해야 하는 업무의 Entity Class를 생성하고 Annotation을 설정하고 기본 결재 Class를 상속 받는다.
[source,java]
----
@Data
@EqualsAndHashCode(callSuper = true)
@ApprovalDocument(docType = "knoxSample", description = "Sample Approval Doc. (Knox)", approvalClass = Approval.class, templateEngine = TemplateEngineType.VELOCITY,
templateFile = "/templates/approval/approval-sample.vm", docSecuType = ApprovalDocSecuType.PERSONAL,
isArbitrary = true, bodyType = ApprovalBodyType.MIME)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class KnoxApprovalSampleDocument extends DefaultApprovalDocument {
private static final long serialVersionUID = 1L;
private String docId;
private String contents;
private String creator;
}
----
업무프로세스 Entity Class 에 @ApprovalDocument 을 달아 주고 DefaultApprovalDocument를 상속 받아야만 결재 Object 라는 것을 결재 모듈에서 알수 있다.
=== @ApprovalDocument
@ApprovalDocument를 이용해 결재 문서의 속정을 정의한다.
[cols=5]
|====
|props명
|필수여부
|Type
|Default
|설명
|docType
|필수
|String
|
|사용자가 식별할수 있는 이름
|description
|필수
|String
|
|결재 문서에 대한 설명
|approvalClass
|필수
|class
|Approval.class
|결재 문서를 DB에 저장할때 사용하는 클래스명
|templateEngine
|필수
|TemplateEngineType
|VELOCITY
|결재 본문 파싱에 필요한 템플릿 엔진. THYMELEAF, VELOCITY 2개의 템플릿 엔진을 사용할 수 있다.
|templateFile 또는 templateKey
|필수
|String
|
|templateFile: 템플릿 파일 이름을 경로와 확장자 포함해서 설정한다. +
tempageKey: 결재양식 관리 메뉴에서 지정한 key
|docSecuType
|
|ApprovalDocSecuType
|PERSONAL
|결재 문서의 보안 형태
|isBodyModify
|
|boolean
|true
|문서의 기본 결재 본문 수정 true일 때만 UI에서 수정 가능
|isRouteModify
|
|boolean
|true
|문서의 기본 결재 경로 수정 true일 때만 UI에서 수정 가능
|isArbitrary
|
|boolean
|false
|문서의 기본 결재 전결 true일 때만 UI에서 수정 가능
|bodyType
|
|ApprovalBodyType
|MIME
|전송할 문서의 형태 TEXT, HTML, MIME
|isInternalApproval
|
|boolean
|false
|true: 내부결재, false: Knox결재
|====
== 결재 본문 등록
결재 본문은 VELOCITY 또는 THYMELEAF 로 등록 할 수 있다. +
SDL에서 제공하는 결재 샘플에서는 VELOCITY 파일로 제공하고 있으며 vm 파일을 작성하고, +
위의 Entity Class 샘플에서와 같이 `@ApprovalDocument` 의 `templateEngine=엔진타입` 과 `templateFile="템플릿파일경로"` 값을 등록한다.
== 결재 상신
=== 결재 상신 UI
샘플 문서를 등록 하고나면 상세하면 하단에 결재 경로를 설정할 수 있는 결재 스텝을 입력 받는 Component가 표시된다. +
결재 문서 화면 개발시 이 Component를 붙여서 상신 기능을 구현한다.
image::approvalSumit_01.png[]
@@ -0,0 +1,34 @@
= MHTML 변환
== 개요
프로젝트에서 메일발송이나 결재상신한 후 방화벽 밖(모바일)에서 조회시 이미지, css가 적용되지 않는 문제를 해결하기 위해 MHT로 변환
.MHT 변환 결과 예
[source, text]
----
Date: Fri, 9 Aug 2019 14:55:20 +0900 (KST)
Message-ID: <963810424.1.1565330120404@DESKTOP-TUEGPQT>
Subject: =?UTF-8?B?66mU64m0IOq2jO2VnCDrp4zro4wg7JiI7KCVIOyViOuCtA==?=
MIME-Version: 1.0
Content-Type: multipart/related;
boundary="----=_Part_0_398737318.1565330120140"
------=_Part_0_398737318.1565330120140
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: base64
PCFkb2N0eXBlIGh0bWw+DQo8aHRtbD4NCjxoZWFkPg0KPG1ldGEgY2hhcnNldD0idXRmLTgiPg0K
PHRpdGxlPuuplOuJtCDqtoztlZwg66eM66OMIOyYiOyglSDslYjrgrQ8L3RpdGxlPg0KPC9oZWFk
Pg0KDQo8Ym9keT4NCjxkaXYgc3R5bGU9IndpZHRoOjEwMCU7YmFja2dyb3VuZC1jb2xvcjojZmZm
(중략)
------=_Part_0_398737318.1565330120140
Content-Type: application/octet-stream
Content-Transfer-Encoding: base64
Content-ID: <simbol.png>
iVBORw0KGgoAAAANSUhEUgAAAQEAAAEBCAYAAAB47BD9AAAwaUlEQVR42u3deZxddX3/8QuC4kPt
Rqu2iK222pYW2yogCNbyE81MVRSFIEuAewmBsGQl+zqZmexhExEV0AQCSSDrZLZkJjOZzGRmQhAM
RXYqbrVaFZHJTOCe+/29v+fc7z3nnn35nnPP8v3j9XAeLWVScz/P7/ee73fu5AghuSD90+Pt5Trk
(중략)
------=_Part_0_398737318.1565330120140--
----
+220
View File
@@ -0,0 +1,220 @@
= 결재
== 개요
SDL에서는 Knox결재와 동기화 되는 결재 모듈을 제공하고 있는데, 여기서는 Knox Portal REST API 연계 서비스 신청 및 Knox결재 서비스 연계 부분에 대하여 설명한다. 결재화면 및 기능구현 설명은 해당 가이드를 참조한다.
=== Knox결재 연계 설정
. Knox REST API 연계 서비스 신청이 되었다면, 발급받은 `system-id`, `token` 값을 설정한다.
.knox.properties (스테이지)
[source,properties]
----
knox.system-id=xxxxxxxxxxx
knox.token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
knox.address.prefix=openapi.samsung.net
knox.approval-service=/approval/api/v2.0/approvals
----
.knox.properties (운영)
[source,properties]
----
knox.system-id=xxxxxxxxxxx
knox.token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx // <1>
knox.address.prefix=openapi.samsung.net,openapi.w1.samsung.net,openapi.w2.samsung.net # <2>
knox.approval-service=/approval/api/v2.0/approvals
----
<1> 토큰 (comma(,)로 구분, 거점 순서와 동일)
<2> 거점 (국내, 구주, 미주)
[start=2]
. 결재화면 및 기능구현 설명은 해당 가이드를 참조한다.
* <<_knox_상신,Knox상신>>
NOTE: 개발자의 IP도 반드시 Knox stage 방화벽에 등록하여야만 개발자 PC에서 상신이 된다. 또한 Knox 스테이지(http://www.stage.samsung.net) 에 개발자와 결재자의 계정도 생성 해야만 개발을 진행 할 수 있다.
== API
KnoxApprovalController는 Knox에서 제공하는 Approval API를 직접 연결하는 API를 제공한다.
시스템의 비즈니스 로직을 거치지 않고 Knox API를 직접 호출 하기 때문에 서비스 호출에 문제가 있는지 파악하는데 유용하다.
NOTE: KnoxApprovalController에서 제공하는 API URI은 Knox REST Service의 API URI와 같다.
. Knox 일반 결재 상신 +
POST /knox/approvals/submit +
* 파라미터는 KnoxApproval 클래스를 참고한다.
* attachments는 첨부파일, knoxApprovalStr는 KnoxApproval의 Json String 값이다.
[source,java]
----
@PostMapping("/submit")
public KnoxApproval submitGeneral(@Parameter(value = "첨부파일", required = true) MultipartFile attachments,
@Parameter(value = "상신정보", required = true) String knoxApprovalStr) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
KnoxApproval knoxApproval = objectMapper.readValue(knoxApprovalStr, KnoxApproval.class);
String serverLocation = Account.currentUser().getServerLocation();
if (StringUtils.isEmpty(serverLocation)) serverLocation = "KR";
String fileId = fileManagerService.store(attachments);
List<Resource> fileList = new ArrayList<>();
fileList.add(fileManagerService.getResource(fileId));
return knoxApprovalService.submit(knoxApproval, fileList, serverLocation);
}
----
[start=2]
. Knox 보안 결재 상신 +
POST /knox/approvals/secu-submit +
* 파라미터는 일반 결재 상신과 같지만 보안문서타입이 "CONFIDENTAIL" 이다.
[source,java]
----
knoxApproval.setDocSecuType("CONFIDENTAIL");
----
[start=3]
. Knox 결재 상세 상황 조회 +
GET /knox/approvals/{apInfId}/detail +
* 결재 연계 ID로 결재 문서의 정보를 상세 조회한다.
. Knox 결재 본문 조회 +
GET /knox/approvals/{apInfId}/content +
* 결재 연계 ID로 결재 문서의 본문을 조회한다.
. Knox 결재 상황 조회 +
POST /knox/approvals/status +
* 결재문서의 진행 상태를 조회한다.
* 복수개의 결재 연계 ID를 요청하여 각각 해당하는 문서변경횟수와 결재상태정보를 응답받는다. 이를 이용하여 결재문서 동기화시 변경된 건에 대해 결재 상태를 업데이트 한다.
[source,java]
----
public List<KnoxApprovalStatus> getStatus(@RequestBody List<KnoxApprovalStatus> knoxApprovalStatusList) {
----
[start=6]
. Knox 결재 연계 ID 조회 +
POST /knox/approvals/apinfids +
* 결재 ID로 결재 연계 ID를 조회한다.
[source,java]
----
public KnoxApproval getApInfIds(@RequestParam String apId) {
----
[start=7]
. Knox 상신함 리스트 조회 +
POST /knox/approvals/submission +
* 상신자가 상신한 정보를 조회한다.
[source,java]
----
public List<KnoxApproval> getApInfIdInfos(@RequestParam String epId) {
----
[start=8]
. Knox 연계 이력 조회 +
GET /knox/approvals/apinfidinfos +
* 요청 시스템에서 상신된 결재문서의 연계 이력을 조회한다.
. Knox 상신 취소 +
POST /knox/approvals/{apInfId}/cancel +
* 결재 문서를 상신취소한다.
. Knox 완결 처리 +
POST /knox/approvals/{apInfId}/autoprogress +
* 결재문서를 완결처리한다.
== KnoxApprovalService
KnoxApprovalService는 시스템에서 결재 문서를 상신 할때 필요한 API들을 제공한다.
[source, java]
----
/**
* 일반 상신, 보안 상신
* @param knoxApproval
* @param attachments
* @param locale
* @return
*/
KnoxApproval submit(KnoxApproval knoxApproval, List<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 형식을 갖추어 필요한 로직들을 수행한다.
@@ -0,0 +1,46 @@
= 결재 경로 관리
== 개요
시스템의 결재 문서 샘플을 RUNTIME 동안 가지고 있다가 결재시에 사용한다.
* ApprovalManager.java -> ApprovalDocument.java -> SampleApprovalDocument.java
. ApprovalManager : @ApprovalDocument라는 어노테이션이 달린 클래스를 찾는다.
. SampleApprovalDocument : 결재경로 관리 목록에 결재 문서 샘플을 보여준다.
== Table
* 결재 경로 : TN_CF_DYNAMIC_APPROVAL_PATH
* 필수 결재자 : TN_CF_REQUIRED_APPROVAL_USER
== API
.ApprovalController.java
. 시스템 전체 결재 문서 조회 +
GET /approval/approval-doc-types
. 결재 경로 조회 +
GET /approval/dynamic-approval-paths/{docType} +
Query ID : selectDynamicApprovalPath
* 기본결재 경로 목록을 보여준다.
. 결재 경로 저장 +
POST /approval/dynamic-approval-paths/{docType} +
Query ID : deleteDynamicApprovalPath, insertDynamicApprovalPath
. 필수 결재자 목록 조회 +
GET /approval/required-approval-users/{docType} +
Query ID : selectRequiredApprovalUserList
* 필수 결재자 목록을 보여준다.
. 필수 결재자 저장 +
POST /approval/required-approval-users/{docType} +
Query ID : deleteRequiredApprovalUser, insertRequiredApprovalUser
== 화면
지정된 문서타입에 따른 결재경로를 관리기능 > 결재/메일 관리 > 결재경로 관리를 통해 지정할 수 있다.
image::front_07_04.png[]
* 지정된 문서타입 목록을 확인할 수 있다.
image::front_07_05.png[]
* 문서타입을 선택 후 해당 문서에 대한 기본 결재경로와 필수 결재자를 추가할 수 있다.
* 결재 상신시 결재자 목록에 지정된 기본결재 경로가 자동 추가되며, 지정된 필수 결재자가 있는 경우 추가하라는 알림을 준다. (다수중 1인 가능)
@@ -0,0 +1,35 @@
= 결재 관리
== 개요
결재 관리를 통해 내부/knox 결재 목록을 확인할 수 있다.
== Table
* 결재 : TN_CF_APPROVAL
* 결재 이벤트 : TN_CF_APPROVAL_EVENT
* 결재 스텝 : TN_CF_APPROVAL_STEP
== API
.ApprovalController.java
. 시스템 전체 결재 문서 조회 +
GET /approval/approval-doc-types +
* 결재경로 관리의 문서타입을 가져온다.
. 시스템 전체 결재 목록 조회(페이징) +
GET /approval/approval-with-paging +
Query ID : selectApprovalPagingList
. 결재 상세정보 조회 +
GET /approval/{approvalRequestId} +
Query ID : selectApprovalStep, selectApprovalEventList
== 화면
image::front_07_01.png[]
=== 검색 조건
** 구분 : knox 결재/ 내부결재
** 상태 : 결재중/완료/후완결/반려/취소
** 문서명 : 결재양식 목록
** 상신자 : 결재 요청한 상신자
** 동기화 상태 : 성공/실패
** 기간 : 결재요청 기간
@@ -0,0 +1,56 @@
= 결재 양식 관리
== 개요
결재 양식을 관리한다.
== Table
* 템플릿 : TN_CF_TEMPLATE
== API
.TemplateController.java
. 템플릿 목록 조회(페이징) +
GET /templates-with-paging/group/{templateGroupCode} +
Query ID : selectTemplatePagingList
* TEMPLATE_GROUP_CODE 컬럼의 'APPROVAL'을 조회한다.
. 템플릿 상세 조회 +
GET /templates/group/{templateGroupCode}/key/{templateKey} +
Query ID : selectTemplate
. 템플릿 상세 조회 (By ID) +
GET /templates/{id} +
Query ID : selectTemplate
* 양식의 상세 내용을 조회 하거나 팝업 미리보기를 할 수 있다.
. 템플릿 Key 중복 체크 +
GET /templates/dup-check/group/{templateGroupCode}/key/{templateKey} +
Query ID : selectTemplate
* 저장 전 템플릿 Key 중복 여부를 검사한다.
. 템플릿 등록 +
POST /templates/group/{templateGroupCode} +
Query ID : insertTemplate
* 양식을 저장한다.
. 템플릿 수정 +
POST /templates/{id} +
Query ID : updateTemplate
. 템플릿 삭제 +
DELETE /templates/{id} +
Query ID : deleteTemplate
== 화면
결재양식을 관리기능 > 결재/메일관리 > 결재양식 관리를 통해 볼 수 있다.
image::front_07_06.png[]
* 등록된 결재양식 목록을 결재양식 관리를 통해 볼 수 있다.
* 목록의 Key 컬럼을 클릭하여 결재양식 정보를 수정할 수 있다.
* 팝업 미리보기 컬럼을 클릭하여 등록된 결재양식의 첨부파일을 확인 할 수 있다.
image::front_07_07.png[]
* Key : 유니크한 결재양식 키를 지정(영문, 숫자만 가능)
* 제목 : 결재양식의 제목
* 설명 : 결재양식의 설명
* 첨부파일 : 결재양식 첨부파일
@@ -0,0 +1,111 @@
= 내부 결재
== 개요
SDL에서 제공하는 내부결재 기능. +
결재를 위한 문서 Entity등록과 내부결재 후처리를 위한 Interceptor 클래스 구현으로 간단하게 결재 기능을 구현할 수 있다. +
샘플로 제공하는 화면의 결재 스텝을 지정하는 Component를 필요한 결재 문서 화면에 적용하여 사용할 수 있으며 샘플 Controller를 참조하여 문서와 결재 스텝 목록을 저장하여 approvalService.submit 메소드를 호출하면 된다.
== Table
* 결재 정보 : TN_CF_APPROVAL
* 결재 스텝 정보 : TN_CF_APPROVAL_STEP
* Sample Document : TN_CF_SAMPLE_APPROVAL_INTERNAL_DOCUMENT
== API
.ApprovalController.java
. 상신함 목록 조회(내부 결재) +
GET /internal-approvals/submit +
Query ID : selectInternalApprovalPagingListBySubmit
* 사용자가 상신한 내부결재 문서를 조회한다.
. 미결함 목록 조회(내부 결재) +
GET /internal-approvals/not-approve +
Query ID : selectInternalApprovalPagingListByNotApprove
* 미결중인 내부결재 문서를 조회한다.
. 기결함 목록 조회(내부 결재) +
GET /internal-approvals/approved +
Query ID : selectInternalApprovalPagingListByApproved
* 사용자가 결재 완료한 내부결재 문서를 조회한다.
. 통보함 목록 조회(내부 결재) +
GET /internal-approvals/notice +
Query ID : selectInternalApprovalPagingListByNotice
* 사용자가 통보 대상인 결재문서를 조회한다.
. 내부결재 문서 상신 취소 +
POST /internal-approvals/{approvalRequestId}/cancel +
Service : cancelInternalApproval
Query ID : updateInternalApprovalStep
* 결재 문서 상신을 취소한다.
. 내부결재 문서 결재 승인 또는 합의 +
POST /internal-approvals/{approvalRequestId}/confirm +
Service : confirmInternalApproval
Query ID : updateInternalApprovalStep
* 결재 문서 승인 또는 합의 한다.
. 내부결재 문서 반려 +
POST /internal-approvals/{approvalRequestId}/reject +
Service : rejectInternalApproval
Query ID : updateInternalApprovalStep
* 결재 문서를 반려 한다.
.ApprovalSampleController.java
. 내부결재 문서 목록 조회(페이징) +
GET /internal-approval/sample-document-with-paging +
Query ID : selectSampleApprovalDocumentPagingList
* Sample Document 문서 목록을 조회한다.
. 내부 결재 문서 상신(Sample Document) +
POST /internal-approval/submit/sample-document
* Knox 결재 정보 동기화 로직을 제외한 다른 부분은 Knox 결재 상신과 동일하다.
* 동기화 배치 로직 대상에서 제외된다.
* 내부결재 전후 처리 로직은 문서 타입별로 Interceptor 클래스를 구현하여 처리한다.
[source,java]
----
@Log4j2
@ApprovalDocumentType(names = {"internalSample"})
public class InternalApprovalSampleInterceptor implements ApprovalInterceptor {
@Autowired
InternalApprovalSampleService internalApprovalSampleService;
@Override
public void afterSubmit(Approval approval, List<ApprovalStep> approvalStepList, Map<String, Object> attribute) {
log.debug("execute ApprovalSampleInterceptor afterSubmit");
//상신 후 문서 상태 업데이트
internalApprovalSampleService.updateSampleApprovalDocument(approval.getDbDocId(), ApprovalDocStatus.INPROCESS);
}
--생략--
}
----
== 화면
. 내부결재 문서 상신함
image::internalApproval_01.png[]
image::internalApproval_05.png[]
* 사용자가 상신한 내부결재 문서 목록을 조회한다.
* 상세화면에 진입하여 상신취소가 가능하다.
[start=2]
. 내부결재 문서 미결함
image::internalApproval_02.png[]
image::internalApproval_06.png[]
* 사용자의 결재 차순에 있는 내부결재 문서 목록을 조회한다.
* 상세화면에 진입하여 승인/합의 또는 반려가 가능하다.
[start=3]
. 내부결재 문서 기결함
image::internalApproval_03.png[]
* 사용자의 결재 완료한 내부결재 문서 목록을 조회한다.
[start=4]
. 내부결재 문서 통보함
image::internalApproval_04.png[]
* 결재 완료 후 사용자에게 통보된 내부결재 문서 목록을 조회한다.
@@ -0,0 +1,35 @@
= 대리 결재
== 개요
내부결재에 적용되는 대리결재자를 지정하는 기능을 제공한다. +
(Knox 결재 대리결재는 Knox portal에서 지정 가능함) +
== Table
* 결재 정보 : TN_CF_APPROVAL_DELEGATE
== API
.ApprovalController.java
. 대리결재자 조회 +
GET /approval/approver-delegate +
Query ID : selectApproverDelegate
* 사용자의 대리결재자를 조회한다.
. 대리결재자 저장 +
POST /approval/approver-delegate +
Query ID : updateApproverDelegate, insertApproverDelegate
* 사용자의 대리결재자를 저장한다.(등록 또는 변경)
. 대리결재자 삭제 +
DELETE /approval/approver-delegate +
Query ID : deleteApproverDelegate
* 사용자의 대리결재자를 삭제한다.
== 화면
. 대리결재자 조회 및 지정
image::approverDelegate_01.png[]
* 사용자 정보 > 대리결재 메뉴를 통해서 대리결재자 등록이 가능하다.
* 기등록된 대리결재자가 존재한다면 대리결재자 팝업 상단에 표시되며 삭제하거나 다른 사용자를 선택하여 변경가능 하다.
@@ -0,0 +1,85 @@
= 메신저
== 개요
Knox Portal에서 제공하는 메신저 관련 Rest API 를 이용한 연계 서비스 제공
=== Knox REST API 연계 서비스 신청
Knox REST API 연계 서비스 신청은 <<_knox_rest_api_연계_서비스_신청,Knox REST API 연계 서비스 신청>> 항목을 참조한다.
=== Knox Rest 메신저 연계 설정
메일, 결재 Knox Rest API 연계와 마찬가지로 연계를 위한 사전 준비가 되었다면, knox.properties 에 메신저 관련 설정이 되어 있는지 확인한다.
.knox.properties
[source,properties]
----
knox.messenger.contact-service=/messenger/contact/api/v1.0
knox.messenger.msgctx-service=/messenger/msgctx/api/v1.0
knox.messenger.message-service=/messenger/message/api/v1.0
----
=== Knox 메신저 연계 서비스
REST를 통해서 메신저와 연계하는 서비스로 주요 메서드는 KnoxMessengerService 인터페이스에 정의되어 있다.
[source, java]
----
public interface KnoxMessengerService {
/**
* 디바이스 ID 조회 : 사용자 ID 와 맵핑되는 단말의 ID 값
* @return 디바이스 ID
*/
String getDeviceId();
/**
* 메시지 암호화 키 조회 : 메시지를 암호화하기 위한 키 값
* @param deviceId 디바이스 ID
* @return 메시지 암호화 키
*/
String getKey(String deviceId);
/**
* Knox Potal login ID를 이용하여 Knox Messenger 수신자들을 조회한다.
* @param deviceId 디바이스 ID
* @param singleIds 수신자 Knox ID 리스트
* @return Knox Messenger 수신자 ID 리스트
*/
List<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 항목을 참고한다.
+75
View File
@@ -0,0 +1,75 @@
= 메일
=== Knox REST API 연계 서비스 신청
Knox REST API 연계 서비스 신청은 <<_knox_rest_api_연계_서비스_신청,Knox REST API 연계 서비스 신청>> 항목을 참조한다.
=== Knox메일 연계 설정
Knox REST API 연계 서비스 신청이 되었다면, 발급받은 `system-id`, `token` 값을 설정한다.
.knox.properties (스테이지)
[source,properties]
----
knox.system-id=xxxxxxxxxxx
knox.token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
knox.address.prefix=openapi.samsung.net
knox.mail-service=/mail/api/v2.0
----
.knox.properties (운영)
[source,properties]
----
knox.system-id=xxxxxxxxxxx
knox.token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx // <1>
knox.address.prefix=openapi.samsung.net,openapi.w1.samsung.net,openapi.w2.samsung.net # <2>
knox.mail-service=/mail/api/v2.0
----
<1> 토큰 (comma(,)로 구분, 거점 순서와 동일)
<2> 거점 (국내, 구주, 미주)
=== KnoxMailService
Knox REST API 연계를 통해서 메일발신, 상태조회등을 하는 서비스로 주요 메서드는 KnoxMailService 인터페이스에 정의되어 있다.
KnoxMailService는 시스템에서 Knox 메일 서비스와 연계할 때 필요한 API들을 제공한다.
[source, java]
----
public interface KnoxMailService {
/**
* Knox 메일 발신
* @param sendMail 발신 메일 정보 (첨부파일정보(AttachFile) 포함)
* @return Knox 메일 아이디
*/
String sendMail(SendMail sendMail);
/**
* Knox 메일 발신
* @param sendMail 발신 메일 정보
* @param attachResources 메일 첨부가 파일 리소스일 경우
* @return Knox 메일 아이디
*/
String sendMail(SendMail sendMail, List<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 소스코드를 참고한다.
IMPORTANT: Knox 연계 메일 발송을 위해서는 발신자/수신자 모두 Knox 계정이 존재해야만 테스트가 가능하다.
@@ -0,0 +1,161 @@
= 메일 그룹 관리
== 개요
메일 그룹 및 맵핑 정보 관리 기능 제공.
== UI Design & Function
=== 메일 그룹 목록(MailGroupList.vue)
메일 그룹 등록, 수정 및 삭제가 가능하다.
image::mailGroupList.png[mailGroupList.png]
* 기능 설명
. 메일 그룹 목록 조회
. 메일 그룹 상세정보 조회
. 메일 그룹 등록 : 등록 버튼 클릭 시 메일 그룹 입력 popup 호출.
. 메일 그룹 수정 : checkbox 선택 후 수정 버튼 클릭 시 변경을 위한 popup 호출.
. 메일 그룹 삭제 : checkbox 선택 후 삭제 버튼 클릭 시 삭제.
=== 메일 그룹 맵핑 수정(MailGroupMappEdit.vue)
메일 그룹 맵핑 목록 등록 및 삭제가 가능하다.
image::mailGroupMappEdit.png[mailGroupMappList.png]
* 기능 설명
. 메일 그룹 맵핑 목록 조회
. 메일 그룹 맵핑 저장
.. 사용자 추가 : 버튼 클릭 시 사용자 추가 popup 호출.
.. 역할 추가 : 버튼 클릭 시 역할 추가 popup 호출.
.. 업무그룹 추가 : 버튼 클릭 시 업무그룹 추가 popup 호출.
== API & Service
=== API
* API : MailGroupController.java
. 메일 그룹 목록 조회 : GET /mail-group-with-paging
. 메일 그룹 상세정보 조회 : GET /mail-group/{mailGroupId}
. 메일 그룹 등록 : POST /mail-group
. 메일 그룹 수정 : PUT /mail-group
. 메일 그룹 삭제 : DELETE /mail-group
. 메일 그룹 맵핑 목록 조회 : GET /mail-group-mapp-with-paging
. 메일 그룹 맵핑 저장 : POST /mail-group-mapp/{mailGroupId}
* Service : MailGroupServiceImpl.java
. 메일 그룹 맵핑 저장 +
맵핑 정보 저장 시 기등록 되어 있는 맵핑 목록 삭제 후
전체 목록을 다시 저장하도록 구현.
[source,java]
----
@Override
@Transactional
public void saveMailGroupMapp(String mailGroupId, List<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
. 메일 그룹 목록 조회
[source,xml]
----
<select id="selectMailGroupPagingList" parameterType="java.util.HashMap" resultMap="mailGroupResult">
SELECT T.*
FROM (SELECT ROW_NUMBER() OVER(ORDER BY LABEL ASC) ROWNUM,
--생략--
----
[start=2]
. 메일 그룹 상세정보 조회
[source,xml]
----
<select id="selectMailGroup" parameterType="java.util.HashMap" resultMap="mailGroupResult">
SELECT <include refid="columnMailGroup" />,
(SELECT USER_NAME
--생략--
----
[start=3]
. 메일그룹 등록
[source,xml]
----
<insert id="insertMailGroup" parameterType="java.util.HashMap">
INSERT INTO <include refid="tableMailGroup" />
(<include refid="columnMailGroup" />)
--생략--
----
[start=4]
. 메일그룹 수정
[source,xml]
----
<update id="updateMailGroup" parameterType="java.util.HashMap">
UPDATE <include refid="tableMailGroup" />
SET LABEL = #{label},
--생략--
----
[start=5]
. 메일그룹 삭제
[source,xml]
----
<delete id="deleteMailGroup" parameterType="java.util.HashMap">
UPDATE <include refid="tableMailGroup" />
SET DELETED = '1',
--생략--
----
[start=6]
. 메일그룹 맵핑 목록 조회
[source,xml]
----
<delete id="deleteMailGroup" parameterType="java.util.HashMap">
UPDATE <include refid="tableMailGroup" />
SET DELETED = '1',
--생략--
----
[start=7]
. 메일그룹 맵핑 저장
[source,xml]
----
<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" />)
--생략--
----
@@ -0,0 +1,87 @@
= 메일 상태 조회
== 개요
Knox REST 메일수신상황조회 API와 연계하여 발신한 메일의 상태를 조회할 수 있다.
=== Knox메일 연계 설정
Knox REST 메일수신상황조회를 위해서는 Knox메일 연계 설정이 되어 있어야 한다. +
Knox메일 연계 설정은 <<_메일,메일>> 항목의 <<_knox메일_연계_설정,Knox메일 연계 설정>> 항목을 참조한다.
=== 메일별 수신 상황 카운트 조회
발신한 메일의 메일 아이디값을 이용하여 발신한 메일을 수신한 수신자들의 개봉상태를
요약한 카운트 정보조회
Service:: KnoxMailService
Method::
+
[source,java]
----
/**
* Knox 메일별 수신상태 조회
* @param mailIds Knox 메일 아이디 리스트
* @param sendMail 메일 정보
* @return Knox 메일 상태
*/
MailStatus[] getDeliveryStatusCount(List<String> mailIds, SendMail sendMail);
----
* SendMail 객체에 senderId (발신자 EP ID) 값 설정 필수
=== 수신인 별 수신 상황 조회
발신한 메일의 메일 아이디값을 이용하여 발신한 메일의 수신자 별 수신 상태 정보를 조회
Service:: KnoxMailService
Method::
+
[source,java]
----
/**
* Knox 메일 수신인별 수신상태 조회
* @param mailId Knox 메일 아이디
* @param sendMail 메일 정보
* @return Knox 메일 수신인별 수신상태 정보
*/
Recipient[] getDeliveryStatus(String mailId, SendMail sendMail);
----
* SendMail 객체에 senderId (발신자 EP ID) 값 설정 필수
=== 사용 예
.SentMailHistoryServiceImpl
[source,java]
----
@Override
public Map<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 수신인 별 메일 수신 상황 조회
@@ -0,0 +1,58 @@
= 메일 양식 관리
== 개요
메일 양식을 관리한다. +
<<_결재_양식_관리,결재 양식 관리>>와 비교해서 'MAIL'로 조회하는 것 외에 동일하다.
== Table
* 템플릿 : TN_CF_TEMPLATE
== API
.TemplateController.java
. 템플릿 목록 조회(페이징) +
GET /templates-with-paging/group/{templateGroupCode} +
Query ID : selectTemplatePagingList
* TEMPLATE_GROUP_CODE 컬럼의 'MAIL'을 조회한다.
. 템플릿 상세 조회 +
GET /templates/group/{templateGroupCode}/key/{templateKey} +
Query ID : selectTemplate
. 템플릿 상세 조회 (By ID) +
GET /templates/{id} +
Query ID : selectTemplate
* 양식의 상세 내용을 조회 하거나 팝업 미리보기를 할 수 있다.
. 템플릿 Key 중복 체크 +
GET /templates/dup-check/group/{templateGroupCode}/key/{templateKey} +
Query ID : selectTemplate
* 저장 전 템플릿 Key 중복 여부를 검사한다.
. 템플릿 등록 +
POST /templates/group/{templateGroupCode} +
Query ID : insertTemplate
* 양식을 저장한다.
. 템플릿 수정 +
POST /templates/{id} +
Query ID : updateTemplate
. 템플릿 삭제 +
DELETE /templates/{id} +
Query ID : deleteTemplate
== 화면
메일양식을 관리기능 > 결재/메일관리 > 메일양식 관리를 통해 할 수 있다.
image::mailTemplate.png[]
* 등록된 메일양식 목록을 메일양식 관리를 통해 볼 수 있다.
* 목록의 Key 컬럼을 클릭하여 메일양식 정보를 수정할 수 있다.
* 팝업 미리보기 컬럼을 클릭하여 등록된 메일양식의 첨부파일을 확인 할 수 있다.
image::mailTemplate1.png[]
* Key : 유니크한 메일양식 키를 지정(영문, 숫자만 가능)
* 제목 : 메일양식의 제목
* 설명 : 메일양식의 설명
* 첨부파일 : 메일양식 첨부파일
@@ -0,0 +1,19 @@
= 보낸 메일 이력
== 개요
메일 발송 내역 및 수신 상태를 조회할 수 있다.
=== 보낸 메일 목록
image::sentMailHistory.png[]
<1> 기간, 제목별 검색 가능
<2> 클릭시 상세 내역 조회
=== 보낸 메일 상세 정보
image::sentMailHistoryInfo.png[]
<1> 보낸 메일의 수신인별 개봉 여부
<2> 보낸 메일 본문 확인 가능
<3> 수신인과 수신상태 정보 목록
@@ -0,0 +1,93 @@
= 임직원
== 개요
Knox Portal에서 제공하는 임직원 관련 Rest API 를 이용한 연계 서비스 제공
=== Knox REST API 연계 서비스 신청
Knox REST API 연계 서비스 신청은 <<_knox_rest_api_연계_서비스_신청,Knox REST API 연계 서비스 신청>> 항목을 참조한다.
=== Knox임직원 연계 설정
Knox REST API 연계 서비스 신청이 되었다면, 발급받은 `system-id`, `token` 값을 설정한다.
.knox.properties (스테이지)
[source,properties]
----
knox.system-id=xxxxxxxxxxx
knox.token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
knox.address.prefix=openapi.samsung.net
knox.emp-service=/employee/api/v2.0
----
.knox.properties (운영)
[source,properties]
----
knox.system-id=xxxxxxxxxxx
knox.token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx // <1>
knox.address.prefix=openapi.samsung.net,openapi.w1.samsung.net,openapi.w2.samsung.net # <2>
knox.emp-service=/employee/api/v2.0
----
<1> 토큰 (comma(,)로 구분, 거점 순서와 동일)
<2> 거점 (국내, 구주, 미주)
=== Knox임직원 API 연계 서비스
Knox REST API 연계를 통해서 임직원 및 조직 정보 조회 기능을 제공하는 서비스로 주요 메서드는 KnoxUserService 인터페이스에 정의되어 있다.
KnoxUserService는 시스템에서 임직원 및 조직 정보를 조회할때 필요한 API들을 제공한다.
[source, java]
----
public interface KnoxUserService {
/**
* Knox 임직원 조회(By EpId)
* @param epId EP ID
* @return Knox 사용자
*/
Employee[] getKnoxEmployeesByEpId(String epId);
/**
* Knox 임직원 조회(By UserName)
* @param userName 사용자 이름
* @return Knox 사용자
*/
Employee[] getKnoxEmployeesByUserName(String userName);
/**
* Knox 임직원 조회(By KnoxId)
* @param knoxId Knox ID
* @return Knox 사용자
*/
Employee[] getKnoxEmployeesByKnoxId(String knoxId);
/**
* Knox 임직원 조회(By Email)
* @param email 이메일
* @return Knox 사용자
*/
Employee[] getKnoxEmployeesByEmail(String email);
/**
* Knox 조직도 조회(By CompanyCode)
* @param companyCode 회사 코드
* @return Knox 조직도
*/
Organization[] getKnoxOrganizationsByCompanyCode(String companyCode);
/**
* Knox 조직도 조회(By DepartmentCode)
* @param companyCode 회사 코드
* @param departmentCode 부서 코드
* @return Knox 조직도
*/
Organization[] getKnoxOrganizationsByDepartmentCode(String companyCode, String departmentCode);
/**
* Knox 직급 조회
* @param companyCode 회사 코드
* @return Knox 직급
*/
Title[] getKnoxTitles(String companyCode);
}
----
자세한 API 스펙은 Swagger API 문서의 knox-user-controller 항목을 참고한다.
@@ -0,0 +1,132 @@
= 주소록
== 개요
Knox Portal에서 제공하는 연락처 관련 Rest API 를 이용한 연계 서비스 제공
=== Knox REST API 연계 서비스 신청
Knox REST API 연계 서비스 신청은 <<_knox_rest_api_연계_서비스_신청,Knox REST API 연계 서비스 신청>> 항목을 참조한다.
=== Knox Rest 연락처 연계 설정
메일, 결재 Knox Rest API 연계와 마찬가지로 연계를 위한 사전 준비가 되었다면, knox.properties 에 연락처 관련 설정이 되어 있는지 확인한다.
.knox.properties
[source,properties]
----
knox.pims-service=/pims/contacts/api/v2.0
----
=== Knox 연락처 연계 서비스
REST를 통해서 연락처를 연계하는 서비스로 주요 메서드는 KnoxContactService 인터페이스에 정의되어 있다.
[source, java]
----
public interface KnoxContactService {
/**
* 연락처 그룹 생성
* @param contactGroupDto
* @param userId
* @return
*/
ContactGroupDto createGroup(ContactGroupDto contactGroupDto, String userId);
/**
* 연락처 그룹 수정
* @param groupId
* @param contactGroupDto
* @param userId
* @return
*/
ContactGroupDto updateGroup(String groupId, ContactGroupDto contactGroupDto, String userId);
/**
* 연락처 그룹 삭제
* @param groupId
* @param userId
* @return
*/
String deleteGroup(String groupId, String userId);
/**
* 연락처 그룹 조회
* @param groupId
* @param userId
* @return
*/
ContactGroupDto getGroup(String groupId, String userId);
/**
* 연락처 그룹 목록 조회
* @param pubType
* @param userId
* @return
*/
ContactGroupDto[] getGroups(String pubType, String userId);
/**
* 연락처 생성
* @param contactDto
* @param userId
* @return
*/
ContactDto createCard(ContactDto contactDto, String userId);
/**
* 연락처 수정
* @param contactId
* @param contactDto
* @param userId
* @return
*/
ContactDto updateCard(String contactId, ContactDto contactDto, String userId);
/**
* 연락처 삭제
* @param contactId
* @param userId
* @return
*/
String deleteCard(String contactId, String userId);
/**
* 연락처 조회
* @param contactId
* @param userId
* @return
*/
ContactDto getCard(String contactId, String userId);
/**
* 연락처 목록 조회
* @param pubType
* @param userId
* @return
*/
ContactDto[] getCards(String pubType, String userId);
/**
* KNOX REST GET MAPPING
* @param <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 항목을 참고한다.
+15
View File
@@ -0,0 +1,15 @@
= 공통기능
include::사용자관리.adoc[leveloffset=+1]
include::시스템관리.adoc[leveloffset=+1]
include::이력관리.adoc[leveloffset=+1]
include::결재메일.adoc[leveloffset=+1]
include::보안관리.adoc[leveloffset=+1]
include::글로벌지원.adoc[leveloffset=+1]
include::파일서비스.adoc[leveloffset=+1]
+7
View File
@@ -0,0 +1,7 @@
= 글로벌 지원
include::글로벌지원/Timezone.adoc[leveloffset=+1]
include::글로벌지원/다국어서비스.adoc[leveloffset=+1]
include::글로벌지원/번역(Utrans).adoc[leveloffset=+1]
@@ -0,0 +1,34 @@
= Timezone
== 개요
사용자의 Timezone을 관리한다.
== Table
* 사용자 : TN_CF_USER
** TIME_ZONE_CODE, TIME_ZONE_ID 컬럼 사용
== Timezone 저장
* 사용자가 시스템에 최초 등록시 저장. +
그 이후에는 '타임존 저장' API를 사용하여 저장한다.
- 사용자가 시스템에 처음으로 SSO 로그인하여 사용자 등록시 epTray 연계된 타임존 정보를 가져와서 저장 (없을 경우 config.properties의 default 값)
== API
.UserController.java
. 타임존 목록 조회 +
GET /auth/users/timezone
* 타임존은 java.util.TimeZone 라이브러리를 사용하기 때문에 DB에 타임존 목록이 저장되어 있지 않으며, +
서머타임(일광 절약 시간제, DST(Daylight Saving Time))을 따로 계산하지 않아도 자동으로 목록에서 보여준다.
. 타임존 저장 +
PUT /auth/users/timezone
== 화면
사용자의 Timezone을 설정하는 기능으로, Timezone을 설정하게 되면 Local Storage의 *user.timeZoneId, user.timeZoneCode* 에 저장된다. +
TopMenu - 표준시간을 통해 접근 가능.
image::timezone.png[]
NOTE: SideMenu(Right Side) 이용 시 MainOffsider.vue 내 'timezone' 부분에 구현되어 있다.
@@ -0,0 +1,56 @@
= 다국어 서비스
== 개요
message-common.properties를 사용하여 시스템에 다국어를 지원한다. +
한글, 영문을 기본으로 서비스한다. +
기본언어인 한글은 message-common_ko_KR.properties을 파일명으로 하고 영문은 '_en_US'를 붙여서 사용한다.
== 설정
=== 프론트엔드 기본언어 값 설정
..env
[source,properties]
----
# To set default Language for I18n (ko_KR, en_US, zh_CN, etc..)
VITE_DEFAULT_LANG=ko_KR
----
=== 백엔드 기본언어 및 다국어 설정
.config.properties
[source,properties]
----
## Language Set
default-language=ko_KR
language-set=ko_KR,en_US
----
* 예) 시스템에서 프랑스어를 추가하고자 할 때 방법
. config.properties의 language-set에 fr_FR을 추가
+
[source,properties]
----
## Language Set(프랑스어 추가)
language-set=ko_KR,en_US,fr_FR
----
. message-common_fr_FR.properties 파일을 생성
. 메세지의 프랑스어 버전 작성
NOTE: 메세지들만 추가되므로 메뉴관리, 역할관리, 업무그룹관리 등 다국어 컬럼(LABEL_JSON)을 지원하는 table 데이터의 경우 직접 입력하여야 한다.
== API
.MessageBundleController.java
. 로케일별로 메세지를 조회 +
GET /noauth/messages
. 설정한 모든 언어의 메세지를 조회 +
GET /noauth/messages/all
== 화면
한국어,영어 중 원하는 언어로 변경하여 화면을 나타내는 기능으로, 언어를 설정하게 되면 User Token에 저장 되어 로그아웃을 하게 되더라도 마지막에 변경된 언어로 설정된다.
TopMenu - 언어선택을 통해 접근 가능.(단, 다국어 지원 되는 영역에 한해서만 지원).
image::language.png[]
NOTE: SideMenu(Right Side) 이용 시 MainOffsider.vue 내 'language' 부분에 구현되어 있다.
@@ -0,0 +1,20 @@
= 번역(Utrans)
== 개요
Utrans API를 호출하여 한국어 및 영어 등 언어로 번역하는 기능.
image::utrans.png[]
=== 지원 되는 언어방향
- 유럽어 : 러시아어,스페인어,독일어,프랑스어,이탈리아어,포르투갈어
|===
|Source |Target
|한국어 | 영어, 중국어, 베트남어, 일어, 유럽어
|영어 | 유럽어
|중국어 | 유럽어
|유럽어 | 유럽어
|===
=== 기능별 설명
- 번역하기 : 번역된 내용을 오른쪽 창에 표시
- 복사하기 : 번역 결과 값을 클립보드에 복사
+19
View File
@@ -0,0 +1,19 @@
= 보안관리
include::보안관리/약관관리.adoc[leveloffset=+1]
include::보안관리/개인정보사용이력관리.adoc[leveloffset=+1]
include::보안관리/개인정보취급자권한변경이력.adoc[leveloffset=+1]
include::보안관리/EUGDPR.adoc[leveloffset=+1]
include::보안관리/관리자IP관리.adoc[leveloffset=+1]
include::보안관리/문자열암복호화.adoc[leveloffset=+1]
include::보안관리/SQLInjection.adoc[leveloffset=+1]
include::보안관리/XSS.adoc[leveloffset=+1]
include::보안관리/비밀번호관리.adoc[leveloffset=+1]
@@ -0,0 +1,7 @@
= EU GDPR
== 개요
. GDPR이란? +
2018년 5월 25일부터 시행되는 EU(유럽연합)의 개인정보보호 법령이며, 동 법령 위반시 과징금 등 행정처분이 부과될 수 있어 EU와 거래하는 우리나라 기업도 이 법에 위반되지 않도록 주의할 필요가 있다.
. EU와 거래하는 시스템은 이용약관동의 관리에서 '유럽연합 개인정보보호 규정'이라는 약관을 추가하여 사용자의 동의를 받아야 한다.
* 세부적인 내용은 <<_약관_관리,약관 관리>> 매뉴얼을 참조한다.
@@ -0,0 +1,39 @@
= SQL Injection
== 개요
표준개발라이브러리에서는 MyBatis의 PreparedStatement 를 사용한 value injection을 원칙으로 사용하기 때문에 java 혹은 jsp에서 sql을 만들지 않는다면 근본적으로 sql injection이 발생하지 않는다.
단, ${} 매핑을 사용 할 경우 SDLComparator를 사용해야 한다.
=== SQL Injeciton 공격 예
MyBatis에서는 #{}, ${} 두 변수 형태를 제공한다. #{}의 경우엔 SQL Injection 공격이 불가하지만 ${}는 값이 직접 매핑되기 때문에 SQL Injeciton에 노출되어 있다.
${}에 매핑될 값은 사용자가 입력값(파라미터를 변조할수 있는)을 사용할 경우 보안 취약점에 노출되게 된다.
[source,xml]
----
<select id="getPerson" parameterType="string" resultType="org.application.vo.Person">
SELECT * FROM PERSON WHERE NAME = #{name} AND PHONE LIKE '${phone}';
</select>
----
위의 경우
[source,xml]
----
SELECT * FROM PERSON WHERE NAME = ? and PHONE LIKE 'A%'; DELETE FROM PERSON; --'
----
실행이 가능하다.
따라서, ${}에 매핑될 값은 조작이 불가능 하도록 사용자 입력값을 코드로 입력 할 수 있도록 하고, 서버에서 코드에 맞는 스트링을 조합해서 실행 할 수 있도록 해야한다.
=== SDLComparator 적용
SDL에서는 개발자의 시큐어 코딩 실수로 인한 SQL Injection 실행 방지를 위해 MyBatis에서 변수 매핑 전에 허용된 문자만 사용 할 수 있도록 함수를 제공하고 있다.
.사용방법
[source,xml]
----
<if test="@com.samsung.SdlComparator@isNotEmptyForDynamicSql(orderBy)">
ORDER BY ${orderBy}
</if>
----
+21
View File
@@ -0,0 +1,21 @@
= XSS 방지
== 개요
XSS(Cross Site Scripting)는 JavaScript등으로 작성된 악성 스크립트 코드를 웹 게시판 등에 삽입해 세션을 가로채거나 공격자가 의도한대로 행동하도록 만드는 공격이다.
=== XSS 방지 적용
표준개발라이브러리에서는 HTML태그를 허용하는 게시판에 대해 화이트리스트를 선정하여 해당 태그만 허용되도록 하고 있다.
.사용방법
[source,java]
----
@Operation(summary = "게시글 상세조회")
@GetMapping("/posts/{postId}")
public Post getPost(
@Parameter("게시글 ID") @PathVariable String postId) {
Post post = postService.getPost(postId);
post.setPostDetail(SdlHtmlPolicy.POLICY_DEFINITION.sanitize( post.getPostDetail())); // <1>
return post;
}
----
<1> 게시글 본문을 SdlHtmlPolicy.POLICY_DEFINITION에 정의된 정책을 기반으로 허용된 태그만 가능하도록 처리
@@ -0,0 +1,93 @@
= 개인정보 사용 이력 관리
== 개요
사용자 정보를 조회한 이력을 남긴다. 관련 법에 따라 일정기간 동안 보관한다.
=== 사용자 정보 조회 이력 관리
* UserController에서 사용자 정보 관련 메서드 호출시, UserService의 writeUserHistoryLog 메서드를 호출하고 있다.
* log4j2.xml에 설정한 파일에 이력이 남는다.
.UserController.class
[source,java]
----
@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
[source,java]
----
@Override
public void writeUserHistoryLog(Object returnValue) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String requestUri = request.getRequestURI();
String requestMethod = request.getMethod();
User user = Account.currentUser();
if(ObjectUtils.isNotEmpty(user)) { // 로그인된 사용자
try {
HistoryLog log = new HistoryLog();
log.setLogId(idGenService.getNextStringId());
log.setNodeId(nodeId);
if(ObjectUtils.isNotEmpty(user)) {
log.setWorkerId(user.getUserId());
log.setWorkerName(user.getUserName());
}
log.setWorkDatetime(DateTime.now().toString());
log.setRemoteAddr(webUtil.getClientIp(request));
log.setRequestMethod(requestMethod);
log.setRequestUri(requestUri);
log.setApiResult(returnValue);
String jsonVal = mapper.writeValueAsString(log);
USER_HISTORY_LOG.info(jsonVal);
} catch (JsonProcessingException e) {
log.warn(e.getMessage());
}
}
}
----
.log4j2.xml
[source,xml]
----
<?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>
----
@@ -0,0 +1,195 @@
= 개인정보 취급자 권한변경이력
== 개요
역할 및 업무그룹의 권한 정보를 변경한 이력을 남긴다.
=== 역할 권한 변경 이력 로깅
* AOP를 이용하여 RoleService의 사용자 역할 권한 추가/수정/삭제 메서드가 호출될때 이력을 남긴다.
* log4j2.xml에 설정한 파일에 이력이 남는다.
.RoleHistoryLoggingAspect.class
[source,java]
----
@Aspect
@Component
@Log4j2
public class RoleHistoryLoggingAspect extends HistoryLoggingSupport{
private static final Logger ROLE_HISTORY_LOG = LogManager.getLogger("RoleHistoryLog");
@Value("${node-id}")
private String nodeId;
private final IdGenService idGenService;
public RoleHistoryLoggingAspect(IdGenService idGenService) {
this.idGenService = idGenService;
}
@Pointcut("execution(* com.samsung.role.impl.RoleServiceImpl.insertUserRoleList(..))")
public void insertUserRolePointcut() {
// Do nothing because pointcut
}
@Pointcut("execution(* com.samsung.role.impl.RoleServiceImpl.updateUserRoleList(..))")
public void updateUserRolePointcut() {
// Do nothing because pointcut
}
@Pointcut("execution(* com.samsung.role.impl.RoleServiceImpl.deleteUserRoleList(..)) || execution(* com.samsung.role.impl.RoleServiceImpl.deleteUserRole(..))")
public void deleteUserRolePointcut() {
// Do nothing because pointcut
}
@After(value = "insertUserRolePointcut() || updateUserRolePointcut() || deleteUserRolePointcut()")
public void writeRoleHistoryLog() {
writeHistoryLog(ROLE_HISTORY_LOG, idGenService, nodeId);
}
}
----
.HistoryLoggingSupport.class
[source,java]
----
public class HistoryLoggingSupport {
private static final ObjectMapper mapper = new ObjectMapper();
@Autowired
protected WebUtil webUtil;
public void writeHistoryLog(Logger logger, IdGenService idGenService, String nodeId) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String requestUri = request.getRequestURI();
String requestMethod = request.getMethod();
User user = Account.currentUser();
if(ObjectUtils.isNotEmpty(user)) { // 로그인된 사용자
try {
HistoryLog log = new HistoryLog();
log.setLogId(idGenService.getNextStringId());
log.setNodeId(nodeId);
if(ObjectUtils.isNotEmpty(user)) {
log.setWorkerId(user.getUserId());
log.setWorkerName(user.getUserName());
}
log.setWorkDatetime(DateTime.now().toString());
log.setRemoteAddr(webUtil.getClientIp(request));
log.setRequestMethod(requestMethod);
log.setRequestUri(requestUri);
String jsonVal = mapper.writeValueAsString(log);
logger.info(jsonVal);
} catch (JsonProcessingException e) {
logger.warn(e.getMessage());
}
}
}
}
----
.log4j2.xml
[source,xml]
----
<?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
[source,java]
----
@Aspect
@Component
@Log4j2
public class WorkgroupHistoryLoggingAspect extends HistoryLoggingSupport {
private static final Logger WORKGROUP_HISTORY_LOG = LogManager.getLogger("WorkgroupHistoryLog");
@Value("${node-id}")
private String nodeId;
private final IdGenService idGenService;
public WorkgroupHistoryLoggingAspect(IdGenService idGenService) {
this.idGenService = idGenService;
}
@Pointcut("execution(* com.samsung.workgroup.impl.WorkgroupServiceImpl.insertWorkgroupRoleList(..))")
public void insertWorkgroupRoleList() {
// Do nothing because pointcut
}
@Pointcut("execution(* com.samsung.workgroup.impl.WorkgroupServiceImpl.insertWorkgroupMenuList(..))")
public void insertWorkgroupMenuList() {
// Do nothing because pointcut
}
@Pointcut("execution(* com.samsung.workgroup.impl.WorkgroupServiceImpl.updateWorkgroupRoleList(..))")
public void updateWorkgroupRoleList() {
// Do nothing because pointcut
}
@Pointcut("execution(* com.samsung.workgroup.impl.WorkgroupServiceImpl.updateWorkgroupMenuList(..))")
public void updateWorkgroupMenuList() {
// Do nothing because pointcut
}
@Pointcut("execution(* com.samsung.workgroup.impl.WorkgroupServiceImpl.deleteWorkgroupRoleList(..))")
public void deleteWorkgroupRoleList() {
// Do nothing because pointcut
}
@Pointcut("execution(* com.samsung.workgroup.impl.WorkgroupServiceImpl.deleteWorkgroupMenuList(..))")
public void deleteWorkgroupMenuList() {
// Do nothing because pointcut
}
@After(value = "insertWorkgroupRoleList() || insertWorkgroupMenuList() || updateWorkgroupRoleList() || updateWorkgroupMenuList() || deleteWorkgroupRoleList() || deleteWorkgroupMenuList()")
public void writeWorkgroupHistoryLog() {
writeHistoryLog(WORKGROUP_HISTORY_LOG, idGenService, nodeId);
}
}
----
.log4j2.xml
[source,xml]
----
<?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>
----
@@ -0,0 +1,11 @@
= 관리자 IP 관리
== 관리자 IP 관리
관리자용 계정의 IP를 관리
image::ipMgmt.png[]
=== 기능별 설명
- 삭제 : 등록된 계정을 삭제
- 추가 : 관리자용 계정 정보를 등록하기 위해 Row를 추가
- 저장 : 추가된 계정 정보를 저장
@@ -0,0 +1 @@
= 리소스 접근 제어
@@ -0,0 +1,10 @@
= 문자열 암/복호화
== 개요
필요한 부분에 대하여 암/복호화를 적용하고 있다.
=== 해쉬 알고리즘
ID/Password 로그인의 경우 Password에 대하여 해쉬 알고리즘(org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder)을 적용하고 있다.
=== 전자서명
Knox Portal EpTray를 통한 로그인의 경우 전자서명된 ssoData를 시스템에 있는 개인키(Private Key)를 기반으로 검증한다.
@@ -0,0 +1 @@
= 불법탈취(Hijacking) 방지
@@ -0,0 +1,23 @@
= 비밀번호 관리
== 개요
ID/PW 로그인시 사용하는 비밀번호의 변경 및 초기화가 가능하다.
=== 비밀번호 변경
로그인된 상태에서 비밀번호 변경이 가능하다.
image::pwdChange.png[]
=== 비밀번호 초기화
로그아웃된 상태에서 ID/PWD 로그인시 비밀번호 초기화를 할 수 있다. +
이메일로 임시 비밀번호가 발송된다.
[cols=2*a]
|===
|
.로그인 화면
image::pwdReset_01.png[]
|
.비밀번호 초기화 화면
image::pwdReset_02.png[]
|===
@@ -0,0 +1,28 @@
= 약관 관리
== 이용약관동의 관리
이용약관동의관리 화면을 관리한다.
image::termsConditionList.png[]
== 이용약관동의 등록
등록 화면에서 새로운 약관 생성이 가능하다.
구분은 **공통코드**의 `TERMS` 정보를, 언어는 `TERMS_LANG` 정보를 참조한다.
image::termsConditionDetail_Reg.png[]
== 이용약관동의 목록 추가
그룹코드 관리에서 약관동의 목록 추가가 가능하다.
image::termsCodeDetail.png[]
신규 사용자 로그인 시 화면에 아래와 같이 나타난다.
== 신규사용자 이용약관 동의
신규 사용자 로그인 시 화면에 아래와 같이 나타난다.
image::termsCondition_New.png[]
+27
View File
@@ -0,0 +1,27 @@
= 사용자 관리
include::사용자관리/사용자관리.adoc[leveloffset=+1]
include::사용자관리/부서관리_Knox.adoc[leveloffset=+1]
include::사용자관리/부서관리_사용자정의.adoc[leveloffset=+1]
include::사용자관리/장기미사용자.adoc[leveloffset=+1]
include::사용자관리/사용권한신청.adoc[leveloffset=+1]
include::사용자관리/외부사용자관리.adoc[leveloffset=+1]
include::사용자관리/권한관리기간배치.adoc[leveloffset=+1]
include::사용자관리/역할관리.adoc[leveloffset=+1]
include::사용자관리/업무그룹관리.adoc[leveloffset=+1]
include::사용자관리/메뉴관리.adoc[leveloffset=+1]
include::사용자관리/QuickMenu관리.adoc[leveloffset=+1]
include::사용자관리/Sitemap.adoc[leveloffset=+1]
include::사용자관리/로그인.adoc[leveloffset=+1]
@@ -0,0 +1,16 @@
= My Menu 관리
== My Menu 관리
사용자가 자주 사용하는 메뉴를 등록하여 사용자 임의로 만들어서 빠른 이동을 목적으로 하는 Menu 목록.
TopMenu - My Menu 에 구현되어 있다.
image::MyMenu.png[]
=== 기능별 설명
image::MyMenu_add.png[]
메뉴의 BreadCrumb 영역 내 메뉴명 우측 별 모양을 클릭하여 MyMenu에 등록 또는 해제
NOTE: SideMenu(Right Side) 이용 시 MainOffsider.vue 내 'QuickMenu' 에 구현되어 있다.
@@ -0,0 +1,13 @@
= Sitemap
== 개요
로그인한 사용자의 권한에 맞는 전체메뉴를 볼 수 있다.
TopMenu - 사이트맵 부분에 구현되어 있다.
== 화면
권한에 따른 전체 메뉴 목록을 나열한 화면으로 메뉴 이동 가능
image::siteMap.png[]
NOTE: SideMenu(Right Side) 이용 시 MainOffsider.vue 내 'sitemap' 부분에 구현되어 있다.
@@ -0,0 +1,129 @@
= 권한 관리
== 권한 체크 로직
권한 체크는 AuthorizationInterceptor에서 사용자가 가지고 있는 메뉴의 권한 목록을 기반으로 URL Base 로 체크가 되고 있다.
[source,java]
----
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
-- 생략 --
String httpMethod = request.getMethod();
String decodedUrl = WebUtil.getDecodedRequestUrl(request, request.getRequestURI().substring(request.getContextPath().length()));
String queryString = "";
if (decodedUrl.indexOf('?') > -1) {
String[] uri = decodedUrl.split("\\?");
decodedUrl = uri[0];
queryString = uri[1];
}
for (Api api : authMenuList) {
if (api.getHttpMethod().equals(httpMethod) && checkUriMatch(api.getApiPath(), api.getApiParameters(), decodedUrl, queryString)) {
return true;
}
}
throw new AuthorizationException("API 권한 없음");
}
----
[source,java]
----
public boolean checkUriMatch(String apiPath, String apiParameters, String decodedUrl, String queryString){
Map<String, String> requiredParamMap = new HashMap<>();
if( StringUtils.isNotEmpty(apiParameters)){
requiredParamMap = Splitter.on('&').trimResults().withKeyValueSeparator('=').split(apiParameters);
}
UriTemplate uriTemplate = new UriTemplate(apiPath);
if ( uriTemplate.matches(decodedUrl) ){
if( requiredParamMap.size() == 0 ) return true;
for (Map.Entry<String, String> entry : requiredParamMap.entrySet()) {
log.debug("Required Param : " + entry.getKey() + " Value : " + entry.getValue());
Map<String, String> requestParamMap = getRequestParam(queryString);
if( !requestParamMap.containsKey(entry.getKey()) || !(entry.getValue()).equals(requestParamMap.get(entry.getKey()))){
return false;
}
}
return true;
}else{
return false;
}
}
----
== 권한별 메뉴 가져오기
SPA(Single Page Application) 방식으로 최초 메인화면 진입시에 사용자의 권한에 맞는 메뉴 목록을 조회하여 보여준다. +
[source,java]
----
@RestController
@RequestMapping("/auth")
public class MenuController {
--생략--
@Operation(summary = "사용자의 권한이 있는 메뉴 조회")
@GetMapping("/menus-user-auth")
public List<Menu> getMenuListByAuth() {
return menuService.getMenuListByAuth(Account.currentUser().getUserId());
}
--생략--
}
----
일반 사용자와 시스템 어드민 사용자의 권한 메뉴 조회 서비스가 분리 되어 있다.
[source,java]
----
@Service
public class MenuServiceImpl implements MenuService {
--생략--
@Override
public List<Menu> getMenuListByAuth(String userId) {
if (Account.currentUser().isSystemAdminUser()) return this.getMenuListByAdminAuth();
else return this.getMenuListByUserAuth(userId);
}
--생략--
}
----
접속중인 상태에서 권한이 변경될 경우 화면 재진입(다시 로그인 또는 브라우저 새로고침)을 통해서 변경된 권한의 메뉴 목록을 다시 로드할 수 있다.
== 권한에 따른 UI Control
SDL에서는 역할관리에서 정의해놓은 역할별로 4개의 권한을 지정할 수 있다. +
최초 화면 진입시 조회한 사용자가 접근 가능한 메뉴 목록에 해당 메뉴에 대한 권한 목록을 함께 가지고 있으며 화면에서 버튼에 directive로 지정함으로써 show/hide 처리를 자동으로 처리한다.
역할관리를 통한 메뉴별 권한 등록
image::front_01_03.png[]
vue 내에서 authorization directive를 통해 버튼 별 권한을 정의한다.(배열로 정의할 경우 여러개 중에 하나만 매칭되도 show처리 됨)
* 여러개의 권한을 둘 경우
[source, html]
----
<button
type="button"
class="btn btn-primary btn-sm"
v-authorization="['UPDATE', 'EXECUTE']"
@click="onApprWrite"
>
{{ $t('sdl.approval.label.APPROVE') }}
</button>
----
* 하나의 권한을 둘 경우
[source, html]
----
<button type="button"
class="btn btn-primary btn-sm"
v-authorization="'UPDATE'"
@click="onApprWrite"
>
{{ $t('sdl.approval.label.APPROVE') }}
</button>
----
* 관리자일 경우는 directive 지정 상관 없이 무조건 show 처리된다.
@@ -0,0 +1,24 @@
= 권한관리 기간 배치
== 만료 권한 삭제
배치를 실행하여 사용자 및 역할에 매핑된 권한 중 만료된 권한을 삭제한다.
.config.properties
[source,properties]
----
batch.user.auth-expired.cron=0 10 02 * * ?
----
== 권한만료 예정 안내 메일 발송
배치를 실행하여 권한만료 예정인 사용자를 대상으로 안내메일을 발송한다.
.config.properties
[source,properties]
----
batch.user.auth-expired-mailing.cron=0 30 02 * * ?
----
== Spring 환경설정
* QuartzConfig.java: Scheduler 등록
* UserBatchConfig.java: 배치 Job, Trigger 등록
* UserBatchExecutor.java: 서비스 호출
@@ -0,0 +1,185 @@
= 로그인
SDL은 Knox EpTray, ID/PW, ADFS 로그인을 지원한다.
== EpTray
Knox EpTray 로그인은 Chrome, Edge 등 멀티 브라우저에서 가능하다.
사용자가 Knox에 로그인을 한 후 시스템에 접속을 하게 되면 EpTray값을 이용해 시스템 로그인을 한다.
NOTE: Knox EpTray SSO 를 위한 연계 신청이 필요하다. (스테이지/운영) +
자세한 내용 안내 및 문의는 Knox Support (매뉴얼 > KnoxPortal NewEpTray 연계 가이드) 를 참조한다.
=== EpTray 적용
연계신청 과정에서 생성되는 *rsaprivkey8.pem* 파일을 +
config.properties의 login.sso.knox-tray-private-key-path 경로에 넣어준다.
CAUTION: *rsaprivkey8.pem* 파일이 없는 경우 아래와 같은 에러가 발생하므로 주의한다.
image::newEpTrayUtil.png[Can not load new EpTray Private Key.png]
=== LoginPage.vue
UI 에서 사용자 정보가 없을 경우 LoginPage.vue 페이지로 이동하게 되고 인증 절차를 시작한다.
EpTray 연계를 위한 메서드
[source,javascript]
----
async loginByNewEpTray() {
let loginResult = false;
SDLUtil.showLoadingBar(true);
await this.socConnect().then((data) => {
this.newEpTrayKey = data;
this.websocket.close();
}).catch((err) => {
SDLUtil.alert(err);
});
SDLUtil.showLoadingBar(false);
if (this.newEpTrayKey) {
if (this.newEpTrayKey.result === 'success') {
const { userInfo, key } = this.newEpTrayKey;
loginResult = await this.loginNewEpTray({ userInfo, key });
} else {
SDLUtil.alert(`sdl.epTray.error.${this.newEpTrayKey.errorCode}`);
}
}
if (!loginResult) {
if (this.$route.query.token) {
loginService.setToken(this.$route.query.token);
document.location = `${SDLUtil.WEB_CONTEXT_PATH}/`;
}
}
this.afterLoginProcess(loginResult);
},
----
[source,javascript]
----
socConnect() {
return new Promise(((resolve, reject) => {
const server = new WebSocket('wss://localhost:29283'); // <1>
this.websocket = server;
server.onopen = () => {
server.send('{"rqtype":"getknoxsso","token":"","data":"KCC60TRAY0072"}'); // <2>
server.onmessage = (event) => {
const socketData = JSON.parse(event.data);
if (socketData.rpcode === 'RETURN_SUCCESS') {
resolve(JSON.parse(socketData.data));
} else {
this.websocket.close();
let err;
if (socketData.rpcode === 'EMPTY_BOX') err = 'sdl.epTray.rpCode.EMPTY_BOX';
else err = `sdl.epTray.rpCode.${socketData.detail}`;
reject(err);
}
};
};
server.onerror = () => {
const err = 'sdl.epTray.rpCode.CONNECTION_FAILED';
reject(err);
};
}));
},
----
<1> WebSocket HOST 정보
<2> 발급받은 연계용 system ID
=== LoginController.java
LoginController 에서는 EpTray값의 정보를 이용해 사용자를 로그인 한다.
이때 시스템 사용자가 아닐 경우, 등록 화면으로 이동하고, 정상적인 시스템 사용자일 경우
Token을 발급한다.
GET /noauth/login/new-eptray
[source,java]
----
@PostMapping("/noauth/login/new-eptray")
@Operation(value = "", description = "knox new eptray 로그인")
public JwtResponse loginByNewEptray(HttpServletRequest request, HttpServletResponse response, @RequestParam String encodeUserInfo, @RequestParam String encodeAesKey ) throws AccountException {
loginInterceptorExecutor.applyPreLogin(request, response);
User user = authenticationService.loginByNewEpTray(encodeUserInfo, encodeAesKey);
user.setRecentLoginIp(webUtil.getClientIp(request));
JwtResponse jwtResponse = new JwtResponse(jwtUtil.generateToken(user), user.getUserId());
user.setJwt(jwtResponse.getJwtToken().split("\\.")[2]);
loginInterceptorExecutor.applyPostLogin(request, response, user);
return jwtResponse;
}
----
== ADFS 로그인
NOTE: ADFS 통합인증 사이트 (https://adsso.sec.samsung.net) 에서 신청 후 사용해야 한다. +
자세한 내용은 해당 사이트를 참조하거나 전자통합인증3 (nextsso3@samsung.com) 으로 문의 한다.
SDL에서는 ADFS 통합인증 사이트의 가이드를 참조하여 AD 로그인 샘플을 제공하고 있으며, 각 프로젝트 환경에 맞게 수정하여 사용한다.
=== 참고할 샘플 파일
* LoginPage.vue
* AdLoginController.java
* onelogin.saml.properties
== 로그인/아웃 전,후 처리
로그인 전과 후, 로그아웃 전과 후 비즈니스 로직이 필요한 경우 LoginInterceptor를 상속받아 구현한다.
LoginInterceptor 클래스는 아래와 같은 Interface를 제공한다.
[source, java]
----
/**
* 로그인 전
*/
default void preLogin(HttpServletRequest request, HttpServletResponse response) {
}
/**
* 로그인 후
*/
default void postLogin(HttpServletRequest request, HttpServletResponse response, User user) {
}
/**
* 로그아웃 전
*/
default void preLogout(HttpServletRequest request, HttpServletResponse response, User user) {
}
/**
* 로그아웃 후
*/
default void postLogout(HttpServletRequest request, HttpServletResponse response, User user) {
}
----
LoginInterceptor를 구현한 클래스는 LoginInterceptorExecutor에 의해 실행되며 Spring Bean으로 등록하고 SpringWebCofig에 아래와 같이 LoginInterceptorExecutor에 등록한다.
[source, java]
----
@Bean
public LoginInterceptorExecutor loginInterceptorExecutor() {
List<LoginInterceptor> loginInterceptorList = new ArrayList<>();
loginInterceptorList.add(userUpdateInterceptor());
loginInterceptorList.add(loginOutLogInterceptor());
return new LoginInterceptorExecutor(loginInterceptorList);
}
----
아래는 로그인 후 사용자 정보를 update 하는 UserUpdateInterceptor의 일부분이다.
[source, java]
----
@Override
public void postLogin(HttpServletRequest request, HttpServletResponse response, User user) {
String recentLoginIp = user.getRecentLoginIp();
Date recentLoginDatetime = user.getRecentLoginDatetime();
user.setLastLoginDate(recentLoginDatetime);
user.setLastLoginIp(recentLoginIp);
user.setRecentLoginDatetime(new Date());
user.setRecentLoginIp(webUtil.getClientIp(request));
user.setLastActivityTime(System.currentTimeMillis());
userService.updateUser(user);
}
----
@@ -0,0 +1,248 @@
= 메뉴 관리
== 개요
메뉴와 하위 Page 및 API 목록을 관리하는 화면이다. +
NOTE: 신규 메뉴를 등록 하거나 메뉴정보를 수정 또는 삭제할 경우 콜백함수를 통해서 메뉴관련 된 모든 캐시가 초기화 된다.
== UI Design & Function
=== 메뉴 관리
메뉴를 등록, 수정 및 관리 할 수 있는 메뉴 트리를 제공한다.
image::menuManagement_01.png[]
* 기능 설명
. 메뉴 트리 목록 조회
. 메뉴 상세정보 조회 : 트리에서 메뉴 선택시 메뉴 상세정보를 조회 한다.
. 메뉴 등록 : 현재 선택된 메뉴 하위에 새로운 메뉴를 추가 한다.
. 메뉴 수정 / 삭제 : 현재 선택된 메뉴를 수정 또는 삭제 한다.
. 메뉴 이동 : 트리에서 Drag&Drop 기능으로 선택한 메뉴를 이동할 수 있다.
=== 페이지 관리
image::menuManagement_02.png[]
* 기능 설명
. Page 목록 조회 : 메뉴 트리에서 메뉴 선택 시 해당 메뉴에 맵핑되어 있는 페이지 목록 조회.
. Page 등록, 수정, 삭제
NOTE: 권한 타입은 READ, UPDATE, EXECUTE, DOWNLOAD 로 구성되어 있으며 사용자가 가지고 있는 해당 메뉴에 대한 권한 종류로 UI의 권한 제어를 하고 있다. +
자세한 내용은 <<_화면별_권한처리,화면별 권한처리>> 매뉴얼을 참고한다.
=== API 관리
image::menuManagement_03.png[]
* 기능 설명
. API 목록 조회
. API 등록
. API 수정
. API 삭제
=== SDL 게시판 Page, API 프리셋 추가
image::menuManagement_04.png[]
* 기능 설명
. 게시판 선택 및 Page, API 자동 추가
== API & Service
=== API
MenuController.java +
Menu와 해당 메뉴에 Mapping 되어 있는 Page, API의 등록, 수정, 삭제 및 조회하는 API를 포함하고 있다.
* 주요 기능 API 목록
. 메뉴 트리 목록 조회 : GET /auth/menus/menu-mgmt/level
. 메뉴 상세정보 조회 : GET /auth/menus/{menuId}
. 메뉴 등록 : POST /auth/menus
. 메뉴 수정 : PUT /auth/menus
. 메뉴 삭제 : DELETE /auth/menus/{menuId}
. 메뉴 이동 : PUT /auth/menus/move-menu
. Page 목록 조회 : GET /auth/menus/{menuId}/pages
. Page 등록 : POST /auth/menus/{menuId}/pages
. Page 수정 : PUT /auth/menus/{menuId}/pages
. Page 삭제 : DELETE /auth/menus/{menuId}/pages
. API 목록 조회 : GET /auth/menus/{menuId}/apis
. API 등록 : POST /auth/menus/{menuId}/apis
. API 수정 : PUT /auth/menus/{menuId}/apis
. API 삭제 : DELETE /auth/menus/{menuId}/apis
== Entity Table & SQL
=== Entity Table
* TN_CF_SYS_RESOURCE : 리소스
* TN_CF_SYS_RESOURCE_MAPP : 리소스 맵핑
* TN_CF_MENU : 메뉴
* TN_CF_PAGE : Page
* TN_CF_API : API
=== SQL
. 메뉴 트리 목록 조회
[source,xml]
----
<select id="selectMenuLevel" parameterType="java.util.HashMap" resultMap="menuTreeInfo">
SELECT *
FROM (
<include refid="conditionMenuLevel"/>
--생략--
----
[start=2]
. 메뉴 상세정보 조회
[source,xml]
----
<select id="selectMenu" parameterType="java.util.HashMap" resultMap="menuResult">
SELECT M.MENU_ID, M.LABEL,
M.MENU_LEVEL, M.MENU_SEQUENCE,
--생략--
----
[start=3]
. 메뉴 등록
[source,xml]
----
<insert id="insertMenu" parameterType="menu">
INSERT INTO <include refid="tableMenu" />
(MENU_ID, LABEL,
--생략--
----
[start=4]
. 메뉴 수정
[source,xml]
----
<update id="updateMenu" parameterType="menu">
UPDATE <include refid="tableMenu" />
SET LABEL = #{label},
--생략--
----
[start=5]
. 메뉴 삭제
[source,xml]
----
<delete id="deleteMenu" parameterType="java.util.HashMap">
UPDATE <include refid="tableMenu" />
SET USE_YN = 0,
DELETE_YN = 1
--생략--
----
[start=6]
. 메뉴 이동
[source,xml]
----
<update id="updateParentMenuId" parameterType="java.util.HashMap">
UPDATE <include refid="sysResource.tableSysResource" />
SET UPPER_SYS_RESOURCE_ID = #{parentMenuId}
--생략--
----
[source,xml]
----
<update id="updateMenuSequence" parameterType="java.util.HashMap">
UPDATE <include refid="tableMenu" />
SET MENU_SEQUENCE = #{moveToMenuSequence}
--생략--
----
[start=7]
. Page 목록 조회
[source,xml]
----
<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
--생략--
----
[start=8]
. Page 등록
[source,xml]
----
<insert id="insertPage" parameterType="page">
INSERT INTO <include refid="tablePage" />
(PAGE_ID, PAGE_NAME, PAGE_PATH,
--생략--
----
[start=9]
. Page 수정
[source,xml]
----
<update id="updatePage" parameterType="page">
UPDATE <include refid="tablePage" />
SET PAGE_NAME = #{pageName},
--생략--
----
[start=10]
. Page 삭제
[source,xml]
----
<delete id="deletePage" parameterType="java.util.HashMap">
UPDATE <include refid="tablePage" />
SET USE_YN = 0,
--생략--
----
[start=11]
. API 목록 조회
[source,xml]
----
<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
--생략--
----
[start=12]
. API 등록
[source,xml]
----
<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)
--생략--
----
[start=13]
. API 수정
[source,xml]
----
<update id="updateApi" parameterType="api">
UPDATE <include refid="tableApi" />
SET API_NAME = #{apiName},
--생략--
----
[start=14]
. API 삭제
[source,xml]
----
<delete id="deleteApi" parameterType="java.util.HashMap">
UPDATE <include refid="tableApi" />
SET USE_YN = 0,
--생략--
----
@@ -0,0 +1,83 @@
= 부서관리(Knox)
NOTE: 해당 기능 사용 필요시 Knox API를 통해 연계 해야 한다.
== Table
* 부서(Knox) : TN_CF_DEPT_LDAP
== API
.KnoxDepartmentController.java
* 부서의 데이터가 많으므로 목록에 1레벨만 불러오고 1레벨의 부서를 클릭시 "부서 관리(Knox)(하위 레벨 부서)" API를 실행하여 하위 부서를 조회한다. +
부서 관리(사용자정의) 화면에서 부서매핑을 추가할 때도 사용한다.
. 부서 관리(Knox)(1레벨 부서) +
GET /admin/knox-department +
Query ID : selectKnoxDepartmentList
. 부서 관리(Knox)(하위 레벨 부서) +
GET /admin/knox-department/{upperDeptCode}
Query ID : selectKnoxDepartmentListByUpperCode
== 부서 Tree 구조
부서관리를 tree 구조로 보여지게 하기 위해, DB의 데이터가 parent 관계를 구성해야 한다.
LDAP 부서관리는 TN_CF_DEPT_LDAP 테이블의 dept_code(Child) 와 upper_dept_code(Parent) 관계로 tree 구조를 만들게 되는데 시스템의 최상위 dept code에는 ROOT를 입력해야 한다.
내부적으로 LDAP 부서 코드의 Root 밑의 1 level을 찾는 query가 *ROOT* 라는 코드를 찾도록 되어 있다.
|===
|DEPT_CODE |DEPT_NAME |DEPT_LEVEL |UPPER_DEPT_CODE
|C00001
|정보전략
|1
|ROOT
|C000011
|정보전략 부서1
|2
|C00001
|C00002
|마케팅
|1
|ROOT
|C000021
|마케팅 부서1
|2
|C00002
|===
사용자정의 부서관리는 TN_CF_DEPT_SELF 테이블의 SELF_DEPT_CODE(Child) 와 UPPER_SELF_DEPT_CODE(Parent)
관계로 tree 구조를 이루며, 시스템의 최상위 SELF_DEPT_CODE는 DEPT를 입력해야 한다.
|===
|DEPT_CODE |DEPT_NAME |DEPT_LEVEL |UPPER_DEPT_CODE
|C00001
|정보전략
|1
|DEPT
|C000011
|정보전략 부서1
|2
|C00001
|C00002
|마케팅
|1
|DEPT
|C000021
|마케팅 부서1
|2
|C00002
|===
== 부서 목록
TN_CF_DEPT_LDAP 테이블에 저장되어 있는 부서정보를 트리형태로 보여준다.
image::deptMgmt(Knox).png[deptMgmt(Knox),300,400]
@@ -0,0 +1,79 @@
= 부서관리(사용자정의)
== Table
* 부서(사용자정의) : TN_CF_DEPT_SELF
* 부서매핑 : TN_CF_DEPT_MAPPING
== API
.CustomDepartmentController.java
. 부서관리(사용자 정의) 목록 조회 +
GET /admin/department/custom +
Query ID : selectCustomDepartmentList
. 부서 하위 레벨 조회 +
GET /admin/department/custom/dept-level-sub +
Query ID : selectCustomDepartmentSubList
* 부서의 데이터가 많으므로 목록에 1레벨만 불러오고 1레벨의 부서를 클릭시 "부서 하위 레벨 조회" API를 실행하여 하위 부서를 조회한다.
. 부서 상세정보 조회 +
GET /admin/department/custom/dept-infos +
Query ID : selectCustomDepartmentList, selectDeptMapping
* 부서 정보와 부서 매핑을 함께 불러온다.
. 부서 저장 +
POST /admin/department/custom +
Query ID : insertCustomDepartment
* 등록된 부서가 있는지 중복체크하고 없으면 저장한다.
. 부서 수정 +
PUT /admin/department/custom/{selfDeptCode} +
Query ID : updateCustomDepartment
* 부서명, 정렬순서, 설명만 수정 가능하다.
. 부서 이동 +
PUT /admin/department/custom +
Query ID : updateDeptUpperDeptCode, updateDeptSequence
* 부서는 Drag & Drop 으로 원하는 곳에 이동하여 저장할 수 있다.
. 부서 매핑 조회 +
GET /admin/department/custom/mappings/{selfDeptCode} +
Query ID : selectDeptMapping
. 부서 매핑 저장 +
POST /admin/department/custom/mappings/{selfDeptCode} +
Query ID : insertDeptMapping
* DB에 저장된 mapping과 화면에서 추가한 mapping을 비교하여 DB에 없는 mapping만 insert 한다.
. 부서 매핑 삭제 +
DELETE /admin/department/custom/mappings/{selfDeptCode}/{deptCode} +
Query ID : deleteDeptMapping
== 부서관리(사용자정의) 기본 정보
사용자의 임의로 부서를 등록 및 해당되는 부서의 Knox 부서 매핑.
image::deptList.png[]
=== 기본정보 필드 설명
- 부서코드 : 부서에 부여되는 코드(Unique)
- 부서명 : 부서명칭
- 부서레벨 : 부서에 부여되는 Level이며 최상위 Department 부서는 0 level이다.
- 정렬순서 : 같은 레벨 상 나오는 부서의 순서
- 설명 : 부서에 대한 설명
=== 기본정보 기능별 설명
- 삭제 : 부서목록의 부서를 선택 후 삭제 버튼 클릭시 선택된 부서 삭제(하위 부서 포함)
- 추가 : 추가하고자 하는 상위 부서를 선택 후 추가 버튼을 클릭시 Tree 구조에 New Document 생성
- 저장 : 추가로 생성된 또는 선택된 부서 정보를 저장
== 부서 매핑
해당 부서에 대한 Knox 부서 매핑 정보.
=== 부서 매핑 정보 필드 설명
- 부서코드 : Knox 부서에 부여되는 코드(Unique)
- 부서명 : Knox 부서명칭
- 삭제 : 삭제 실행 버튼
=== 기능별 설명
- 추가 : Knox 부서 Popup 호출
- 저장 : 호출된 Knox 부서 Popup 화면에서 추가하고자 하는 부서 매핑 정보에 저장.
@@ -0,0 +1,23 @@
= 사용권한 신청/승인
== 개요
사용자가 시스템에 가입시 시스템 사용 권한을 승인/승인 취소/삭제를 하여 시스템을 사용/미사용하게 한다.
== Table
* 사용자 : TN_CF_USER
== API
.UserController.java
. 사용자 승인 +
PUT /auth/users/status/confirm +
Query ID : updateUser
. 사용자 승인취소 +
PUT /auth/users/status/inactive +
Query ID : updateUser
. 사용자 삭제 +
DELETE /auth/users +
Query ID : updateUser
* DeleteMapping이지만 DB 삭제가 아닌 ACTIVE_FLAG = 0, DELETED = 1 로 업데이트 한다.
@@ -0,0 +1,30 @@
= 사용자 관리
== 사용자 관리 화면
사용자 목록을 조회하며 시스템 사용 신청 대기자에 대한 사용 승인 및 취소가 가능하다. +
사용자 조회 목록의 Row를 클릭시 사용자 상세 정보 화면으로 이동할 수 있다.
image::userMgmt.png[]
=== 기능별 설명
- 엑셀다운로드 : 조회 조건과 동일한 조회 결과 값을 담은 엑셀 파일을 다운로드 하는 기능.
- 삭제 : 선택된 사용자를 삭제 처리하는 기능.
* 삭제된 사용자의 row 데이터는 남아 있지만 사용자의 정보(EP ID, 사용자 이름 제외)는 모두 삭제 된다.
- 승인취소 : 선택된 사용자를 승인취소 처리하는 기능.
- 승인 : 선택된 사용자를 승인 처리하는 기능.
* 세부적인 내용은 <<_사용권한_신청승인,사용권한 신청/승인>> 매뉴얼을 참조한다.
== 사용자 상세 조회 화면
사용자의 상세정보표시 및 조회한 사용자에 대한 역할 및 메뉴 추가/삭제가 가능하다.
image::userInfo.png[]
=== 기능별 설명
- 사용자-역할 삭제 : 선택된 역할을 삭제
- 사용자-역할 추가 : 선택된 역할을 추가하는 기능. 역할 팝업이 호출 되며, 사용자에게 부여하고 싶은 역할을 선택하여 확인 버튼 클릭시 사용자-역할 Grid에 추가된다.
- 사용자-역할 저장 : 추가 또는 수정된 역할이 있을 경우, 저장 기능 수행.
- 사용자-메뉴 삭제 : 선택된 메뉴를 삭제
- 사용자-메뉴 추가 : 선택된 메뉴를 추가하는 기능. 메뉴 팝업이 호출 되며, 사용자에게 부여하고 싶은 메뉴을 선택하여 확인 버튼 클릭시 사용자-메뉴 Grid에 추가된다.
* 메뉴의 Page, API 권한 타입(READ, UPDATE, EXECUTE, DOWNLOAD) 중 필요한 권한을 check하여 등록한다.
* 권한 타입에 대한 설명은 메뉴관리 가이드를 참고한다. <<_메뉴_관리,메뉴 관리>>
- 사용자-메뉴 저장 : 추가 또는 수정된 메뉴이 있을 경우, 저장 기능 수행
@@ -0,0 +1,228 @@
= 업무 그룹 관리
== 개요
메일 그룹 및 맵핑 정보 관리 기능 제공.
== UI Design & Function
=== 업무 그룹 목록(WorkgroupList.vue)
업무별로 업무그룹을 만들고 해당 업무그룹에서 사용가능한 메뉴를 선택, 해당 메뉴 사용 가능한 Role 또는 User를 추가한다.
image::workgroupList.png[업무 그룹 목록]
* 기능 설명
. 업무그룹 목록 조회
. 업무그룹 상세정보 조회
. 업무그룹 등록 : 업무그룹 정보를 입력하는 Popup이 호출 되며, 정보 기입 후 저장 버튼을 클릭하여 등록한다.
. 업무그룹 수정 : 선택된(checkbox : true) 업무그룹 정보를 수정하는 Popup이 호출 되며 역할 정보 기입 후 저장 버튼을 클릭하여 수정한다.
. 업무그룹 삭제 : 선택된(checkbox : true) 역할 목록을 삭제한다.
=== 업무그룹-메뉴(WorkgroupMenuInfo.vue)
업무그룹에서 사용 가능한 메뉴를 관리 하는 화면. +
메뉴 등록 및 삭제가 가능하며 메뉴별 권한 설정이 가능하다.
image::workgroupMenuInfo.png[업무그룹-메뉴]
* 기능 설명
. 업무그룹-메뉴 목록 조회
. 업무그룹-메뉴 등록 : 메뉴를 선택하는 Popup이 호출 되며 선택 후 저장 버튼을 클릭하여 등록한다.
. 업무그룹-메뉴 수정 : 권한을 선택(checkbox : true)하여 해당 메뉴에 대한 권한을 수정한다.
. 업무그룹-메뉴 삭제 : 선택된(checkbox : true) 메뉴 목록을 삭제한다.
=== 업무그룹-역할(WorkgroupMenuInfo.vue)
선택한 업무그룹에 역할 또는 사용자를 관리하는 화면. +
역할과 사용자를 추가 또는 삭제할 수 있다.
image::workgroupRoleInfo.png[업무그룹-역할]
* 기능 설명
. 업무그룹-역할 목록 조회
. 업무그룹-역할 등록
.. 사용자 등록 : 사용자 추가 버튼 클릭 시 사용자를 선택할 수 있는 Popup이 호출 되며 선택 후 저장 버튼을 클릭하여 등록 한다.
.. 역할 등록 : 역할 추가 버튼 클릭 시 역할을 선택할 수 있는 Popup이 호출 되며 선택 후 저장 버튼을 클릭하여 등록 한다.
. 업무그룹-역할 수정 : 등록된 사용자의 업무그룹 만료일을 수정 한다.
. 업무그룹-역할 삭제 : 선택된(checkbox : true) 역할(사용자) 목록을 삭제한다.
== API & Service
=== API
* API : WorkgroupController.java
. 업무그룹 목록 조회 : GET /auth/workgroups-with-paging
. 업무그룹 상세정보 조회 : GET /auth/workgroups/{workgroupId}
. 업무그룹 등록 : POST /auth/workgroups
. 업무그룹 수정 : PUT /auth/workgroups/{workgroupId}
. 업무그룹 삭제 : DELETE /auth/workgroups
. 업무그룹-메뉴 목록 조회 : GET /auth/workgroups/{workgroupId}/menus
. 업무그룹-메뉴 등록 : POST /auth/workgroups/{workgroupId}/menus
. 업무그룹-메뉴 수정 : PUT /auth/workgroups/{workgroupId}/menus
. 업무그룹-메뉴 삭제 : DELETE /auth/workgroups/{workgroupId}/menus
. 업무그룹-역할 목록 조회 : GET /auth/workgroups/{workgroupId}/roles
. 업무그룹-역할 등록 : POST /auth/workgroups/{workgroupId}/roles
. 업무그룹-역할 수정 : PUT /auth/workgroups/{workgroupId}/roles
. 업무그룹-역할 삭제 : DELETE /auth/workgroups/{workgroupId}/roles
* Service : WorkgroupServiceImpl.java
. 업무그룹-메뉴 목록 조회. +
메뉴의 전체 경로 셋팅 및 권한별 row 데이터를 메뉴 row 데이터로 가공한다.
[source,java]
----
@Override
public List<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
. 업무그룹 목록 조회
[source,xml]
----
<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" />
--생략--
----
[start=2]
. 업무그룹 상세정보 조회
[source,xml]
----
<select id="selectWorkgroup" parameterType="java.util.HashMap" resultMap="workgroupResult">
SELECT <include refid="columnsWorkgroup" />
FROM <include refid="tableWorkgroup" />
<include refid="conditionWorkgroup" />
</select>
----
[start=3]
. 업무그룹 등록
[source,xml]
----
<insert id="insertWorkgroup" parameterType="workgroup">
INSERT INTO <include refid="tableWorkgroup" />
(<include refid="columnsWorkgroup"/>)
--생략--
----
[start=4]
. 업무그룹 수정
[source,xml]
----
<update id="updateWorkgroup" parameterType="workGroup">
UPDATE <include refid="tableWorkgroup" />
SET WORKGROUP_NAME = #{workgroupName},
--생략--
----
[start=5]
. 업무그룹 삭제
[source,xml]
----
<delete id="deleteWorkgroup" parameterType="java.util.HashMap">
UPDATE <include refid="tableWorkgroup" />
SET DELETE_YN = 1,
--생략--
----
[start=6]
. 업무그룹-메뉴 목록 조회
[source,xml]
----
<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,
--생략--
----
[start=7]
. 업무그룹-메뉴 등록
[source,xml]
----
<insert id="insertWorkgroupMenu" parameterType="workgroupMenu">
INSERT INTO <include refid="tableWorkAuthorization" />
(WORKGROUP_ID, SYS_RESOURCE_ID, AUTHORIZATION_ID,
--생략--
----
[start=8]
. 업무그룹-메뉴 삭제
[source,xml]
----
<delete id="deleteWorkgroupMenu" parameterType="workgroupMenu">
DELETE FROM <include refid="tableWorkAuthorization" />
WHERE SYS_RESOURCE_ID = #{sysResourceId}
--생략--
----
[start=9]
. 업무그룹-역할 목록 조회
[source,xml]
----
<select id="selectRoleWorkgroupList" parameterType="java.util.HashMap" resultMap="workgroup.workgroupResult">
SELECT W.WORKGROUP_ID, W.WORKGROUP_NAME, W.DESCRIPTION,
W.LABEL
--생략--
----
[start=10]
. 업무그룹-역할 등록
[source,xml]
----
<insert id="insertWorkgroupRole" parameterType="workgroupRole">
INSERT INTO <include refid="tableWorkgroupRole" />
(WORKGROUP_ID, USER_ROLE_ID, FROM_DATE, THRU_DATE, ROLE_ID, USER_ID,
--생략--
----
[start=11]
. 업무그룹-역할 수정
[source,xml]
----
<update id="updateWorkgroupRole" parameterType="workgroupRole">
UPDATE <include refid="tableWorkgroupRole" />
SET THRU_DATE = #{thruDate},
--생략--
----
[start=12]
. 업무그룹-역할 삭제
[source,xml]
----
<delete id="deleteWorkgroupRole" parameterType="java.util.HashMap">
DELETE FROM <include refid="tableWorkgroupRole" />
WHERE WORKGROUP_ID = #{workgroupId}
--생략--
----
@@ -0,0 +1,207 @@
= 역할 관리
== 개요
역할을 등록하고, 해당 역할에 사용자 및 업무그룹을 Mapping 한다.
== UI Design & Function
=== 역할 목록(RoleList.vue)
역할 등록, 수정 및 조회. +
조회된 목록의 사용자/업무그룹 컬럼을 클릭하여 역할-사용자, 역할-업무그룹 상세 화면으로 이동한다.
image::roleList.png[역할 관리]
* 기능 설명
. 역할 목록 조회
. 역할 등록 : 역할 정보를 입력하는 Popup이 호출 되며 정보 기입 후 저장 버튼을 클릭하여 등록한다.
. 역할 수정 : 선택된(checkbox : true) 역할 정보를 수정하는 Popup이 호출 되며, 정보 기입 후 저장 버튼을 클릭하여 수정한다.
. 역할 삭제 : 선택된(checkbox : true) 역할 목록을 삭제한다.
. 엑셀 다운로드 : 조회 조건과 동일한 역할 목록을 엑셀 파일로 다운로드 한다.
=== 역할-사용자 관리(RoleUserInfo.vue)
선택한 역할에 대한 사용자 추가. +
역할에 대한 사용자가 많을 경우를 대비하여 상단에 페이징 처리 되는 Grid를, 하단에는 사용자를 추가하는 Grid로 나누어 화면을 구성된다.
image::roleUserInfo.png[역할-사용자 관리]
* 기능 설명
. 역할-사용자 목록 조회
. 역할-사용자 등록 : 추가 버튼 클릭시 사용자 조회 Popup이 호출 되며 사용자 선택 후 추가된 하단의 사용자 목록에서 저장한다.
. 역할-사용자 수정 : 기등록된 상단 사용자 목록에서 역할 적용 기간을 수정한다.
. 역할-사용자 삭제 : 기등록된 상단 사용자 목록에서 선택된(checkbox : true) 사용자 목록을 삭제한다.
=== 역할-업무그룹 관리(RoleWorkgroupInfo.vue)
선택한 역할에 대한 업무그룹 추가.
image::roleWorkgroupInfo.png[역할-업무그룹 관리]
* 기능 설명
. 역할-업무그룹 목록 조회
. 역할-업무그룹 등록 : 추가 버튼 클릭시 업무그룹 조회 Popup이 호출 되며 업무그룹을 선택 하여 등록 할 수 있다.
. 역할-업무그룹 삭제 : 기등록된 상단 업무그룹 목록에서 선택된(checkbox : true) 업무그룹 목록을 삭제한다.
== API & Service
=== API
* API : RoleController.java
. 역할 목록 조회 : GET /roles-with-paging
. 역할 상세정보 조회 : GET /roles/{roleId}
. 역할 등록 : POST /roles
. 역할 수정 : PUT /roles
. 역할 삭제 : DELETE /roles
. 역할-사용자 목록 조회 : GET /roles/{roleId}/users-with-paging
. 역할-사용자 등록 : POST /roles/{roleId}/users
. 역할-사용자 수정 : PUT /roles/{roleId}/users
. 역할-사용자 삭제 : DELETE /roles/{roleId}/users
. 역할-업무그룹 목록 조회 : GET /roles/{roleId}/workgroups
. 역할-업무그룹 등록 : POST /roles/{roleId}/workgroups
. 역할-업무그룹 삭제 : DELETE /roles/{roleId}/workgroups
== Entity Table & SQL
=== Entity Table
* TN_CF_ROLE : 역할
* TN_CF_USER_ROLE : 역할-사용자 맵핑
* TN_CF_WORKGROUP_ROLE : 역할-업무그룹 맵핑
=== SQL
. 역할 목록 조회
[source,xml]
----
<select 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,
--생략--
----
[start=2]
. 역할 상세정보 조회
[source,xml]
----
<select id="selectRole" parameterType="java.util.HashMap" resultMap="roleResult">
SELECT <include refid="columnsRole" />
FROM <include refid="tableRole" />
--생략--
----
[start=3]
. 역할 등록
[source,xml]
----
<insert id="insertRole" parameterType="role">
INSERT INTO <include refid="tableRole" />
(<include refid="columnsRole" />)
--생략--
----
[start=4]
. 역할 수정
[source,xml]
----
<update id="updateRole" parameterType="role" flushCache="true" >
UPDATE <include refid="tableRole" />
SET ROLE_NAME = #{roleName},
--생략--
----
[start=5]
. 역할 삭제
[source,xml]
----
<update id="deleteRole" parameterType="role" flushCache="true" >
UPDATE <include refid="tableRole" />
SET DELETE_YN = 1,
--생략--
----
[start=6]
. 역할-사용자 목록 조회
[source,xml]
----
<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,
--생략--
----
[start=7]
. 역할-사용자 등록
[source,xml]
----
<insert id="insertUserRole" parameterType="userRole">
INSERT INTO <include refid="tableUserRole" />
(ROLE_ID, USER_ID, FROM_DATE, THRU_DATE,
--생략--
----
[start=8]
. 역할-사용자 수정
[source,xml]
----
<update id="updateUserRole" parameterType="userRole">
UPDATE <include refid="tableUserRole" />
SET THRU_DATE = #{thruDate},
--생략--
----
[start=9]
. 역할-사용자 삭제
[source,xml]
----
<delete id="deleteUserRole" parameterType="java.util.HashMap">
DELETE FROM <include refid="tableUserRole" />
WHERE USER_ID = #{userId}
--생략--
----
[start=10]
. 역할-업무그룹 목록 조회
[source,xml]
----
<select id="selectRoleWorkgroupList" parameterType="java.util.HashMap" resultMap="workgroup.workgroupResult">
SELECT W.WORKGROUP_ID, W.WORKGROUP_NAME, W.DESCRIPTION,
W.LABEL
--생략--
----
[start=11]
. 역할-업무그룹 등록
[source,xml]
----
<insert id="insertWorkgroupRole" parameterType="workgroupRole">
INSERT INTO <include refid="tableWorkgroupRole" />
(WORKGROUP_ID, USER_ROLE_ID, FROM_DATE, THRU_DATE, ROLE_ID, USER_ID,
--생략--
----
[start=12]
. 역할-업무그룹 삭제
[source,xml]
----
<delete id="deleteWorkgroupRole" parameterType="java.util.HashMap">
DELETE FROM <include refid="tableWorkgroupRole" />
WHERE WORKGROUP_ID = #{workgroupId}
--생략--
----
@@ -0,0 +1,15 @@
= 외부 사용자 관리
== 개요
ID/PW 회원가입을 통해서 등록한 사용자 목록을 조회하는 화면이다.
== 화면
외부 사용자 목록 조회 및 엑셀 다운로드 기능을 제공한다.
NOTE: 사용자 테이블(TN_CF_USER)의 외부 사용자 여부(EXTERNAL_FLAG) 정보를 통해서 확인할 수 있다. +
사용자 관리 메뉴에서 관리자 승인이 된 사용자 정보만 조회할 수 있다.
image::externalUserList.png[]
=== 기능별 설명
- 엑셀다운로드 : 조회 조건과 동일한 조회 결과 값을 담은 엑셀 파일을 다운로드 하는 기능.
@@ -0,0 +1,25 @@
= 장기미사용자
== 개요
일정 기간 시스템에 로그인(TN_CF_USER.RECENT_LOGIN_DATETIME)을 하지 않은 사용자는 장기 미사용자로 관리한다.
== 설명
장기 미사용자 관리 배치를 실행하면서 3개월간 로그인 하지 않은 사용자를 조회한다. +
* config.properties 파일에서 개월수(기본 3개월) 및 배치 실행시간을 변경 할 수 있다.
[source,properties]
----
batch.user.long-term.month=3
batch.user.long-term-check.cron=0 10 00 * * ?
----
* 조회된 장기 미사용자에게 매핑된 메뉴, 업무그룹, 역할을 삭제하고 장기 미접속자로 상태를 변경한다. 사용자 관리 재직상태 컬럼(TN_CF_USER.EXPIRE_STATUS_CODE)에서 확인 할 수 있다.
== 배치 설정
=== Spring 환경설정
* QuartzConfig.java: Scheduler 등록
* UserBatchConfig.java: 배치 Job, Trigger 등록
* UserBatchExecutor.java: 서비스 호출
SDL의 배치 작업은 Quartz 를 이용하여 구현하고 있다. 자세한 내용은 <<_작업_스케쥴링, 작업 스케쥴링>> 및 <<_quartz_clustering_with_jdbc_jobstore, Quartz Clustering with JDBC-JobStore>> 을 참고한다.
+23
View File
@@ -0,0 +1,23 @@
= 시스템 관리
include::시스템관리/개발툴.adoc[leveloffset=+1]
include::시스템관리/게시판관리.adoc[leveloffset=+1]
include::시스템관리/공지사항알림.adoc[leveloffset=+1]
include::시스템관리/메인페이지.adoc[leveloffset=+1]
include::시스템관리/링크사이트.adoc[leveloffset=+1]
include::시스템관리/일괄작업관리.adoc[leveloffset=+1]
include::시스템관리/작업스케쥴링.adoc[leveloffset=+1]
include::시스템관리/캐시관리.adoc[leveloffset=+1]
include::시스템관리/코드관리.adoc[leveloffset=+1]
include::시스템관리/업무담당자관리.adoc[leveloffset=+1]
include::시스템관리/주요전화번호관리.adoc[leveloffset=+1]
@@ -0,0 +1,3 @@
= 개발툴/빌드 스크립드
SDL을 이용한 웹 어플리케이션 개발에 필요한 툴과 빌드, 배포 방법은 <<_설치,설치>>를 참고한다.
@@ -0,0 +1,116 @@
= 게시판 관리
== 개요
SDL에서 제공하는 게시판 관리 기능으로 공지사항(메인화면 공지팝업 연동)이나 FAQ 게시판 등 Community 성격으로 게시글 등록 및 답변기능을 사용할 수 있는 게시판 기능이다. +
관리 기능에서 게시판 등록 및 관리를 할 수 있으며 사용자용과 관리자용 메뉴를 따로 만들어 권한별로 게시판 기능을 사용할 수 있도록 제공한다.
== Table
* 게시판 : TN_CF_BOARD
* 게시판 분류 : TN_CF_BOARD_CLASSIFICATION
* 게시판 컬럼 : TN_CF_BOARD_COLUMN
== API
.BoardController.java
. 게시판 목록 조회(페이징) +
GET /boards-with-paging +
Query ID : selectBoardPagingList
. 게시판 상세정보 조회 +
GET /boards/{boardId} +
Query ID : selectBoard
. 게시판 등록 +
POST /boards +
Query ID : insertBoard, insertBoardClassification, insertBoardColumn
* 게시판 등록 시 등록화면에서 추가한 게시판 분류 목록과 default 컬럼 정보가 저장된다.
[source,java]
----
@Override
@Transactional
public void insertBoard(Board board) {
// 게시판 저장.
boardDao.insertBoard(board);
// 게시판 분류 저장.
if (!board.getClassifications().isEmpty()) {
for (BoardClassification boardClassification : board.getClassifications()) {
boardClassification.setBoardId(board.getBoardId());
boardClassification.setClassificationId(idGenService.getNextStringId());
boardDao.insertBoardClassification(boardClassification);
}
}
// 게시판 컬럼 저장.
boardDao.insertBoardColumn(board.getBoardId());
}
----
[start=5]
. 게시판 수정 +
PUT /boards/{boardId} +
Query ID : updateBoard, updateBoardClassification, updateBoardColumn
* 게시판 수정시 변경된 게시판 분류 목록과 컬럼 정보가 수정된다.
[source,java]
----
@Override
@Transactional
public void updateBoard(Board board) {
// 게시판 수정.
boardDao.updateBoard(board);
// 게시판 분류 수정.
List<BoardClassification> beforeClassifications = boardDao.getBoardClassificationList(board.getBoardId());
List<String> beforeClassificationIdList = new ArrayList<>();
-- 생략 --
}
----
[start=6]
. 게시판 삭제 +
DELETE /boards/{boardId} +
Query ID : deleteBoard
== 화면
. 게시판 목록 화면
image::boardManagement_01.png[]
* 게시판 목록을 확인할 수 있다.
* 해당 게시판의 등록된 게시글 등록건수와 현재 사용 여부등의 정보를 보여준다.
* 등록 버튼 클릭 시 등록화면으로 이동되며 게시판 명 클릭 시 게시판 수정화면으로 이동된다.
[start=2]
. 게시판 등록 화면
image::boardManagement_02.png[]
* 게시판을 등록할 수 있다.
* 게시판 세부기능 속성
** 에디터 사용여부 : CafeNote 등 에디터를 사용할지 선택('미사용' 선택 시 textarea 태그로 구현된다)
** 이미지파일사용 : 이미지 파일 등록 시 본문에 이미지를 표시한다.(사진형 게시판일 경우 필수사용)
** 메인 공지팝업 : 게시글 등록 시 메인 공지사항 팝업창에 해당 기간동안 게시글이 노출된다.
** 게시글 '공지'라벨 표시 : 게시글 등록 시 게시글 목록 상단에 공지 게시글로 노출된다.
[start=3]
. 게시판 상세정보 수정 화면
image::boardManagement_03.png[]
* 게시판 세부기능 속성 변경이 가능하다.
* '공지팝업 제한' 기능은 해당 게시판의 공지게시글을 일시적으로 제한할 수 있는 기능이다.
image::boardManagement_04.png[]
* 게시판의 검색 조건을 선택할 수 있다.
* 게시글 목록화면에서 보여줄 정보를 선택 할수 있다.
** 각 컬럼의 사용 여부에 따라서 넓이가 합산 100%로 변경된다.
* 게시글 목록 정렬 기준을 선택 할 수 있으며 작성일 내림차순이 기본으로 선택된다.
@@ -0,0 +1,29 @@
= 공지사항 알림 뱃지
== 개요
관리자가 작성한 공지사항이 등록되면 메인페이지에 알림이 보인다.
== Table
* 게시판 : TN_CF_BOARD
* 게시글 : TN_CF_POST
* 게시판 분류 : TN_CF_BOARD_CLASSIFICATION
* 사용자 : TN_CF_USER
* 공지사항 확인 : TN_CF_NOTICE_CHECKED
== API
.BoardController.java
. 공지사항 팝업 게시글 목록 조회(메인화면 용) +
GET /notice-popup-posts +
Query ID : selectNoticePopupPostList
. 공지사항 팝업 게시글 체크 +
POST /check-notice +
Query ID : selectNoticeChecked, insertNoticeChecked
== 화면
* MainTopMenu.vue
image::notice_alarm.png[800,600]
image::notice.png[800,600]
@@ -0,0 +1 @@
= 공통 페이지
@@ -0,0 +1,39 @@
= 링크 사이트
== 개요
링크 사이트를 관리한다.
== Table
* 링크사이트 : TN_CF_LINK_SITE
== API
.LinkSiteController.java
. 링크사이트 목록 조회(페이징) +
GET /linksite/linksites-with-paging +
Query ID : selectLinkSitePagingList
. 링크사이트 등록 +
POST /linksite/linksites +
Query ID : insertLinkSite
* LINK_SITE_ID는 IdGenService를 사용하여 유니크한 ID를 만들어서 등록한다.
. 링크사이트 수정 +
PUT /linksite/linksites +
Query ID : updateLinkSite
. 링크사이트 삭제 +
DELETE /linksite/linksites +
Query ID : deleteLinkSite
== 화면
외부 링크사이트를 관리기능 > 기타관리 > 링크사이트 관리를 통해 할 수 있다.
image::front_05_01.png[]
* 구분 : 공통코드 type이 'LINKSITE'인 항목
* 사이트명(기본) : 기본 사이트명을 입력한다.
* 사이트명(locale) : 다국어별 사이트명을 입력한다.
* 설명 : 사이트에 대한 설명을 입력한다.
* URL : 사이트의 URL을 등록한다.
@@ -0,0 +1,12 @@
= 메인 페이지
== 구성
* Header Section (TopMenu)
** Navigation - My Menu(즐겨찾기) | navigation(결재함, 게시판, 샘플 페이지, 관리기능)
** RightSide - 메인공지팝업 | 번역, 통합검색, 사이트맵 | 표준시간, 언어선택 | 프로필
+
NOTE: 통합검색의 경우 검색솔루션 등을 통해 따로 구현해야함.
* MainContainer Section - 메인 컨텐츠(공지사항, FAQ, 연락처 등)
* Footer Section - 개인정보취급방침, 이용약관
image::mainPage.png[]
@@ -0,0 +1,24 @@
= 업무 담당자 관리
== 개요
주요 업무 담당자 페이지를 관리한다.
NOTE: 정적 HTML 로 만든 페이지를 업로드 하여 그대로 보여주고 싶을 때 사용한다. (업무 담당자 관리, 주요 연락처 관리)
== 화면
image::front_06_01.png[]
* 등록된 업무 담당자 페이지 목록을 볼 수 있다.
* 목록의 Key 컬럼을 클릭하여 업무 담당자 페이지 정보를 수정할 수 있다.
* 팝업 미리보기를 클릭하여 등록된 업무 담당자 페이지를 확인 할 수 있다.
image::front_06_02.png[]
* Key: Unique한 키를 지정(영문, 숫자만 가능)
* 제목: 업무 담당자 페이지 제목
* 설명: 업무 담당자 페이지 설명
* 첨부파일: 업무 담당자 HTML 페이지 (html, htm 확장자)
image::front_06_02_01.png[]
* 메뉴관리에서 메뉴를 등록하고 Page/API 를 추가한다.
- Page Path에 업무 담당자 관리에서 등록한 Key를 포함한다.
- Vue Component는 SampleStaff.vue 를 참조하여 생성 후 등록한다.
- API URL에 업무 담당자 조회 API URL을 등록한다. (/html-templates/group/{templateGroupCode=STAFF}/key/{templateKey})
@@ -0,0 +1,23 @@
= 일괄 작업 관리 및 이력 조회
== 일괄작업 관리
* 일괄작업 관리를 위해서는 **구현 Service** (com.samsung..*Impl.*) 의 메서드에 `@BatchJob` annotation이 설정되어 있어야 한다.
* 관리기능 > 일괄작업 관리> 일괄작업 관리 메뉴에서 아래와 같이 설정해야만 이력관리가 남게 된다.
image::batchJobMgmt.png[]
image::batchJobMgmtUpdate.png[]
* 일괄작업 관리 화면에서 관리하고자 하는 Batch 정보를 위와 같이 입력한다.
** 구분 : 그룹코드 BATCHGUBUN 에 등록한 공통코드명을 입력한다.
** 작업명 : Batch 작업명을 입력한다.
** 작업클래스 : com.samsung.accesslog.impl.SysUseLogMngImpl.loadBatchData과 같이 Batch 실행시 실행되는 Package명을 포함한 클래스명 및 메소드명을 입력한다. (`@BatchJob` annotation에 설정한 값)
** URL : Batch를 직접 실행하기 위한 URL을 입력한다. 여기에 입력한 URL은 일괄작업이력 화면에서 해당 배치실행 버튼을 클릭하였을때 Call 된다.
URL을 입력할 경우에는 해당 Request를 처리할 Controller를 구현해야 한다.
== 일괄작업 이력
일괄작업 관리에 등록한 작업이 수행되면 일괄작업 이력에 아래와 같이 나타난다.
image::batchJobLogList.png[]
* 배치가 실행되는 시작 시간과 종료시간, 소요시간, 작업결과 등이 나타난다.
* 작업이 실패 했을경우 실행 할 수 있는 배치실행 버튼이 나타난다.(일괄작업관리 등록시 URL을 등록해야 나타남).
@@ -0,0 +1,52 @@
= 작업 스케쥴링
== 개요
SDL의 작업 스케쥴링은 http://www.quartz-scheduler.org/documentation/[Quartz]를 이용하여 구현하고 있다.
수행할 작업(Job)을 등록하고 Trigger에 Job을 추가한 후 Scheduler에 Trigger(s)를 설정한다.
=== 스케줄러 설정 예
.config.properties
[source,properties]
----
batch.user.long-term-check.cron=0 10 00 * * ?
----
.UserBatchConfig
[source,java]
----
@Configuration
public class UserBatchConfig {
@Value("${batch.user.long-term-check.cron}")
private String batchUserLongTermCheckCron;
/**
* 장기 미사용자 관리 Job
*/
@Bean
public JobDetail batchUserLongTermCheckJob() {
return JobBuilder
.newJob(UserBatchExecutor.class) // <1>
.withIdentity("batchUserLongTermCheck") // <2>
.withDescription("User LongTerm Check Batch")
.storeDurably(true)
.build();
}
/**
* 장기 미사용자 관리 Trigger
*/
@Bean
public CronTriggerFactoryBean batchUserLongTermCheckTrigger() {
CronTriggerFactoryBean trigger = new CronTriggerFactoryBean();
trigger.setJobDetail(batchUserLongTermCheckJob()); // <3>
trigger.setCronExpression(batchUserLongTermCheckCron); // <4>
return trigger;
}
}
----
<1> 서비스를 수행할 Job Class
<2> Job 구분 명
<3> Job 등록
<4> Cron 표현식 설정
@@ -0,0 +1,22 @@
= 주요 전화 번호 관리
== 개요
주요 전화번호 페이지를 관리한다.
== 화면
image::front_06_03.png[]
* 등록된 주요 전화번호 페이지 목록을 볼 수 있다.
* 리스트의 Key 컬럼을 클릭하여 주요 전화번호 페이지 정보를 수정할 수 있다.
* 팝업 미리보기를 클릭하여 등록된 주요 전화번호 페이지를 확인 할 수 있다.
image::front_06_04.png[]
* Key : Unique한 키를 지정(영문, 숫자만 가능)
* 제목 : 주요 전화번호 페이지 제목
* 설명 : 주요 전화번호 페이지 설명
* 첨부파일 : 주요 전화번호 HTML 파일 (html, htm 확장자)
[%hardbreaks]
* 메뉴관리에서 메뉴를 등록하고 Page/API 를 추가한다.
- Page Path에 주요 전화번호 관리에서 등록한 Key를 포함한다.
- API URL에 주요 전화번호 조회 API URL을 등록한다. (/html-templates/group/{templateGroupCode=CONTACT}/key/{templateKey})
@@ -0,0 +1,6 @@
= 캐시 관리
== 개요
시스템 전체 캐시를 초기화 한다.
image::cacheMgmt.png[캐시 관리]
@@ -0,0 +1,76 @@
= 코드 관리
== 개요
그룹 코드를 등록하고 그룹 코드의 공통 코드를 등록하여 추가, 수정 및 삭제를 관리한다. +
groupcodes는 그룹 코드를 나타내고, commcodes는 공통 코드를 나타낸다. 그룹 코드가 상위, 공통 코드가 그룹 코드의 하위 개념이다.
== Table
* 그룹 코드 : TC_CF_COMM_CODE_TYPE
* 공통 코드 : TC_CF_COMM_CODE
== API
.CommCodeController.java
. 그룹 코드 목록 조회 +
GET /commcode/groupcodes-with-paging +
Query ID : selectGroupCodePagingList
.. 검색조건에는 코드, 코드명, 설명이 있다. +
"그룹 코드 목록 조회(페이징)"을 사용하며 각 조건에 맞게 쿼리를 실행한다. +
특히 코드명은 한글, 영어, 중국어에 상관없이 입력한 값의 대소문자를 가리지 않고 1글자만 입력해도 검색이 된다.
.. orderBy는 기본이 COMM_CODE_TYPE_CODE(코드명) 이다.
. 그룹 코드 등록 +
POST /commcode/groupcodes
** 그룹 코드는 20자 이내로 입력해야 한다. 영문자와 숫자, 특수문자만 사용할 수 있고 한글은 입력이 안 되게 정규식을 사용하여 프론트단에서 유효성 체크를 한다.
. 그룹 코드 삭제 +
DELETE /commcode/groupcodes/{groupCode}
.. 그룹 코드는 TC_CF_COMM_CODE_TYPE.DELETE_YN 컬럼 값을 true로 업데이트 하는 식으로 삭제한다.
... 먼저 그룹 코드에 추가되어 있는 공통 코드의 하위를 전부 조회하여 찾은 후 공통 코드 전부 TC_CF_COMM_CODE.DELETE_YN 컬럼을 true로 업데이트 한다.
.... Query ID : updateCommCodeDeleted
... 그 후 그룹 코드의 DELETE_YN 컬럼을 true로 업데이트 하여 삭제한다.
.... Query ID : updateGroupCode
. 공통 코드 추가 +
POST /commcode/groupcodes/{groupCode}/commcodes
** 추가시에 중복체크하고 없으면 등록하는데 TC_CF_COMM_CODE.CODE_ID는 IdGenService를 사용하여 유니크한 ID를 만들어서 등록한다.
. 공통 코드 삭제 +
DELETE /commcode/groupcodes/{groupCode}/commcodes/{commCodeId}
** 삭제하려는 공통 코드의 자신과 자식들 코드를 조회하여 DELETE_YN 컬럼 값을 true로 업데이트 하는 식으로 삭제한다.
*** Query ID : updateCommCodeDeleted
== 화면
그룹 코드 및 그룹코드에 대한 공통 코드를 추가, 수정 및 삭제 기능을 수행하여 관리한다. +
image::commonCodeList.png[]
=== 기능별 설명
- 등록 : 코드 정보를 등록하는 화면으로 이동
== 코드 등록
그룹 코드를 등록 +
* 영문자와 숫자, '-', '_' 2개의 특수문자만 사용하여 등록 가능하다.
image::commonCodeDetail_Reg.png[]
=== 기능별 설명
- 목록 : 코드 관리 화면으로 이동
- 저장 : 그룹 코드 정보를 저장
== 코드 상세 정보
그룹 코드의 상세 정보를 수정, 삭제하고 그에 대한 공통 코드를 추가, 수정, 삭제하여 관리
image::commonCodeDetail.png[]
=== 기능별 설명
==== 그룹코드 정보
- 엑셀다운로드 : 조회 조건과 동일한 조회 결과 값을 담은 엑셀 파일을 다운로드 하는 기능
- 목록 : 코드 관리 화면으로 이동
- 삭제 : 그룹코드를 삭제
- 저장 : 수정된 그룹코드 상세 정보를 수정
==== 공통코드 목록
- 추가 : 공통코드를 등록하는 Popup 호출
- 수정 : 공통코드를 수정하는 Popup 호출
- 삭제 : 공통코드를 삭제
+15
View File
@@ -0,0 +1,15 @@
= 이력 관리
include::이력관리/시스템로깅.adoc[leveloffset=+1]
include::이력관리/SQLLogging.adoc[leveloffset=+1]
include::이력관리/로거관리.adoc[leveloffset=+1]
include::이력관리/메뉴활용도.adoc[leveloffset=+1]
include::이력관리/메뉴사용이력.adoc[leveloffset=+1]
include::이력관리/파일다운로드이력.adoc[leveloffset=+1]
include::이력관리/로그인이력.adoc[leveloffset=+1]
@@ -0,0 +1 @@
= SQL Logging
@@ -0,0 +1,9 @@
= 로거(Logger) 관리
== 개요
시스템 Logger 내역을 조회 및 log level을 변경할 수 있다.
image::loggerMgmt.png[로거 관리]
<1> Logger name별 설정된 configuration 내역(log4j2.xml)을 조회할 수 있다.
<2> Logger name별로 log level 을 변경할 수 있다.
@@ -0,0 +1,70 @@
= 로그인 이력
== 개요
사용자가 시스템에 로그인한 이력을 조회
== Table
* 로그인 이력 : TN_CF_LOGIN_OUT
* 사용자 : TN_CF_USER
== API
.HistoryController.java
. 로그인 이력 목록 조회 +
GET /history/login-out-logs
Query ID : selectListLoginOut
== 화면
사용자가 시스템에 로그인한 이력을 조회
image::loginHistory.png[]
== LoginOutLogInterceptor
시스템에 사용자가 로그인/아웃을 하면 LoginOutLogInterceptor에 의해 사용자 정보가 저장된다.
LoginOutLogInterceptor는 LoginInterceptor의 구현클래스로 LoginInterceptor의 자세한 설명은 <<_로그인, 로그인>>의 **로그인/아웃 전,후 처리**를 참고한다.
[source, java]
----
@Override
public void postLogin(HttpServletRequest request, HttpServletResponse response, User user) {
LoginOut loginOut = new LoginOut();
if (StringUtils.isNotEmpty(user.getJwt())) {
loginOut.setToken(user.getJwt().substring(user.getJwt().lastIndexOf('.') + 1));
}
loginOut.setUserId(user.getUserId());
loginOut.setLoginTime(new Date());
loginOut.setUseIp(user.getRecentLoginIp());
loginOut.setReqType("WEB");
loginOutLogService.insertLogin(loginOut);
}
----
로그인 시 사용자 ID, 로그인 시간, IP 등을 TN_CF_LOGIN_OUT 테이블에 저장한다.
[source, java]
----
@Override
public void preLogout(HttpServletRequest request, HttpServletResponse response, User user) {
LoginOut loginOut = new LoginOut();
if (StringUtils.isNotEmpty(user.getJwt())) {
loginOut.setToken(user.getJwt().substring(user.getJwt().lastIndexOf('.') + 1));
}
loginOut.setUserId(user.getUserId());
loginOut.setLoginTime(user.getRecentLoginDatetime());
loginOut.setLogoutTime(new Date());
loginOut.setUseIp(webUtil.getClientIp(request));
loginOut.setReqType("WEB");
loginOutLogService.updateLogout(loginOut);
userService.updateUserJwt(Map.of("userId", user.getUserId(), "lastLogoutDate", new Date(), "jwt", ""));
}
----
로그아웃 전에는 TN_CF_LOGIN_OUT에 정보를 저장하는 것 외에 사용자의 jwt 값을 초기화 한다.
중복 로그인 방지를 위해 jwt값을 이용한다.
@@ -0,0 +1,24 @@
= 메뉴 사용 이력
== 개요
시스템에 접속한 사용자의 메뉴 사용 이력을 조회
== Table
* 메뉴 사용 이력 : TN_CF_MENU_USE_HISTORY
== API
.HistoryController.java
. 일별 메뉴 사용 이력 조회 +
GET /history/menu-use-by-date +
Query ID : selectMenuUseHistoryByDatePagingList
. 월별 메뉴 사용 이력 조회 +
GET /history/menu-use-by-month +
Query ID : selectMenuUseHistoryByMonthPagingList
== 화면
image::menuUserHistoty.png[]
=== 기능별 설명
- 엑셀다운로드 : 조회 조건과 동일한 조회 결과 값을 담은 엑셀 파일을 다운로드 하는 기능
@@ -0,0 +1,22 @@
= 메뉴 활용도
== 개요
메뉴 별로 메뉴 사용 이력을 분석해서 월별, 일별로 활용률을 계산한다.
== Table
* 공통코드 : TC_CF_COMM_CODE
* 메뉴사용주기 : TC_CF_MENU_USE_PERIOD
* 메뉴 : TN_CF_MENU
* 메뉴활용도 집계 : TS_CF_MENU_USE_MM
== API
.MenuUtilizationController.java
. 메뉴 활용도 목록 조회 +
GET /menuutilization/menu-utilizations +
Query ID : selectMenuUtilizationList
== 화면
메뉴에 대한 사용 주기, Hit, 활용도를 조회
image::menuUtilHistory.png[]
@@ -0,0 +1,22 @@
= 시스템 로깅
== 개요
로그인/아웃, 시스템 접속, 파일다운로드 등의 시스템 로그를 남기고 있다.
=== Login/out 로그
로그인 후와 로그아웃 전에 LoginOutLogInterceptor를 통해 로그를 남긴다.
NOTE: 로그인/아웃 전후에 추가할 로직이 있다면 LoginInterceptor 인터페이스를 상속받아 구현한다.
관련 테이블:: TN_CF_LOGIN_OUT
=== Access 로그
시스템 접속시 LoggingInterceptor를 통해 AccessLog를 남겨 db(또는 file)에 저장한다. +
일배치로 메뉴사용이력 및 메뉴활용도 데이터를 집계한다.
관련 테이블:: TN_CF_SYS_USE_LOG
=== File Download 로그
첨부파일, 엑셀파일 등을 다운로드시 fileManagerService의 saveFileDownloadLog 메서드를 호출하여 파일다운로드 로그를 남긴다.
관련 테이블:: TN_CF_SYS_USE_LOG
@@ -0,0 +1,24 @@
= 파일 다운로드 이력
== 개요
사용자가 엑셀 다운로드 하거나 이미지 파일이 있는 게시판을 이용했거나 첨부파일을 다운로드 한 목록을 보여준다.
== Table
* 시스템 사용 로그 : TN_CF_SYS_USE_LOG
* 사용자 : TN_CF_USER
== API
.HistoryController.java
. 파일 다운로드 이력 목록 조회 +
GET /history/file-download-logs +
Query ID : selectFileDownloadLogPagingList
* 파일 다운로드 이력의 LOG_FLAG는 TN_CF_SYS_USE_LOG 테이블 LOG_FLAG 컬럼의 '5' 이다.
== 화면
엑셀 다운로드 기능이 있는 특정 화면에서 엑셀 다운로드 실행에 대한 이력을 조회.
image::fileDownloadHistory.png[]
=== 기능별 설명
- 엑셀다운로드 : 조회 조건과 동일한 조회 결과 값을 담은 엑셀 파일을 다운로드 하는 기능
+5
View File
@@ -0,0 +1,5 @@
= 파일서비스
include::파일서비스/엑셀다운로드및업로드.adoc[leveloffset=+1]
include::파일서비스/파일업다운로드.adoc[leveloffset=+1]
@@ -0,0 +1,99 @@
= 엑셀 다운로드 및 업로드
== 개요
SDL 6.0은 사용자 관리, 외부 사용자 관리, 역할 관리, 메뉴 사용 이력, 파일 다운로드 이력, 코드 관리, 링크사이트 관리에서 엑셀 다운로드를 지원한다. +
그 외 메뉴에서 엑셀 다운로드 기능을 적용하려면 <<_공통컴포넌트_유틸,공통컴포넌트 & 유틸>>의 <<_excel_download_button,Excel Download Button>>, <<_excel_upload_button,Excel Upload Button>> 매뉴얼을 참고한다.
== API
.ExcelController.java
. 엑셀 다운로드 +
GET /excel/excel-download +
. 엑셀 업로드 +
POST /excel/excel-upload
== 엑셀 다운로드
=== excel.xml 설정
sdl-base/src/main/resources/excel 폴더에 다운로드 할 기능의 xml 양식을 만든다. +
다국어 적용(message.properties)이 된다.
. 대외비 표기여부, 시트보호 여부, 제목
[source,xml]
----
<CONFIDENTIAL>true</CONFIDENTIAL> <!--> 대외비 표기여부(false : 태그작성 x) <-->
<PROTECTION>true</PROTECTION> <!--> 시트보호 여부(false : 태그작성 x) <-->
<PASSWORD>sdl</PASSWORD> <!--> 시트보호 암호(PROTECTION = false : 태그작성 x) <-->
<TITLE>sdl.excel.user.title</TITLE> <!--> 제목 <-->
----
[start=2]
. 문서 Comment 출력
.. 코멘트를 여러개 작성 가능
[source,xml]
----
<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>
----
[start=3]
. 헤더
[source,xml]
----
<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>
----
[start=4]
. 컬럼
.. 첫번째 Row 에 No. 필드 넣을 경우 No. 필드에 대한 <COLUMN>를 작성하지 않아도 된다.
[source,xml]
----
<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>
----
IMPORTANT: 간혹 다운로드 받은 엑셀에 초록색 경고가 뜨는데 이것은 DB에 문자로 저장된 숫자를 불러오기 때문이다. +
따라서 <COLUMN_TYPE>을 'Number'로 하면 엑셀에서도 숫자로 잘 나올 것이다.
== 엑셀 업로드
=== excelUploadSample.xml 설정
SampleExcelUpload.vue 파일에 샘플로 엑셀 업로드가 구현되어 있다.
* 업로드할 데이터의 양식에 맞추어 excel.xml을 만들고 업로드 한다.
** 업로드 excel.xml 파일은 다운로드 excel.xml 파일과 같은 경로에 만든다.
** 날짜 형식 데이터가 업로드되지 않는 경우 `yyyy-MM-dd` 형태로 입력하거나 텍스트 서식으로 입력한다.
@@ -0,0 +1,211 @@
= 파일 업/다운로드
== 개요
클라이언트의 파일 업/다운로드 API 호출을 처리한다.
=== 파일 업로드
단일 파일 업로드를 처리한다. 하나의 파일만 업로드하는 경우가 아니라면 보통은 멀티 파일 업로드를 이용한다.
=== 멀티 파일 업로드
====
컨트롤러에서 파일 업로드 서비스를 호출한다.
.FileManagerController
[source,java]
----
@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
[source,java]
----
/**
* 기본 업로드 패스 설정 값
*/
@Value("${common.upload-path}")
private String fileUploadPath;
/**
* 업로드 루트 하위 폴더 자릿수 설정 값
*/
@Value("${common.upload.directory-name-len}")
private int directoryNameLen;
/**
* 사용자지정 업로드 패스 설정 여부
*/
@Value("${custom.upload-path.enabled}")
private boolean customUploadPathEnabled;
/**
* 사용자지정 업로드 패스 설정 값
*/
@Value("${custom.upload-path}")
private String customUploadPaths;
@Override
@Transactional
public List<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
[source,properties]
----
## File Attach Configuration
common.upload-path=/NAS/SDL/upload // <1>
common.upload.directory-name-len=2 // <2>
# custom upload path 설정
custom.upload-path.enabled=false // <3>
custom.upload-path=\ // <4>
notice=/NAS/SDL/upload/notice,\
faq=/NAS/SDL/upload/faq
----
<1> 기본 업로드 패스 설정
<2> 업로드 패스 하위 디렉토리 길이 +
파일명(UUID)에서 이 길이 만큼 잘라서 기본 업로드 패스 하위 디렉토리가 생성된다.
<3> 사용자정의 업로드 패스 사용 여부
<4> 파일 컴포넌트별 사용자정의 업로드 패스 설정 (custom.upload-path.enabled=true 일 경우 적용됨)
====
=== 파일 다운로드
컨트롤러에서 파일 다운로드 서비스를 호출한다.
.FileManagerController
[source,java]
----
@GetMapping("/resource/attachments/file-download/{fileId}")
public ResponseEntity<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
[source,java]
----
@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);
}
----