주민번호 CI 변환 시스템 기능구현 완료

design
시온파파 3 years ago
parent a87653a857
commit 23da9cda15

@ -21,10 +21,23 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-devtools'
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
/* =================================================================================== */
/* External Jar.. */
/* =================================================================================== */
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.google.code.gson:gson:2.9.0'
}
tasks.named('test') {

Binary file not shown.

@ -0,0 +1,21 @@
package cokr.xit.ci;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Slf4j
@Controller
@RequiredArgsConstructor
public class IndexController {
@GetMapping("/")
public String index(){
log.info("Welcome to the CI system");
return "index";
}
}

@ -0,0 +1,58 @@
package cokr.xit.ci.api.code;
public enum ErrCd {
OK("정상")
/* =======================================================================
* (4xx: Client, 5xx: Server, 6xx: LinkService, 9xx: Etc..)
======================================================================= */
//클라이언트 요청 오류
, ERR401("필수 파라미터가 없습니다.")
, ERR402("파라미터 유효성 검증 오류")
, ERR403("잘못된 파라미터 입니다.")
, ERR404("일치하는 자료가 없습니다.")
, ERR405("잘못된 요청 값.")
, ERR411("잘못된 JSON 포맷 문자열")
//서버 오류
, ERR501("HttpServer 오류")
, ERR502("HttpClient 오류")
, ERR503("RestClient 오류")
, ERR504("요청 데이터 Json 파싱 오류")
, ERR505("응답 데이터 Json 파싱 오류")
, ERR506("Hash 생성 오류")
, ERR511("JSON 형식으로 변환 실패")
, ERR520("통신오류")
, ERR521("방화벽 설정 오류")
//외부서비스 오류
, ERR600("API서버 요청 오류")
, ERR601("API서버 응답 오류")
, ERR602("API서버 내부 오류")
, ERR603("유효하지 않은 토큰(OTT) 값")
, ERR610("응답 데이터에 필수값이 없음")
, ERR620("API Response Error")
//기타오류
, ERR999("기타 오류")
, ERR901("권한 없음")
, ERR902("유효하지 않은 데이터")
, ERR903("처리 완료된 데이터")
;
private final String code; //코드
private final String codeNm; //코드명
ErrCd(String codeNm) {
this.code = this.name();
this.codeNm = codeNm;
}
public String getCode() {
return this.code;
}
public String getCodeNm() {
return this.codeNm;
}
}

@ -0,0 +1,15 @@
package cokr.xit.ci.api.model;
import cokr.xit.ci.api.code.ErrCd;
import lombok.*;
@Builder
@Getter
@ToString
@EqualsAndHashCode
public class ResponseVO {
private ErrCd errCode;
private String errMsg;
private Object resultInfo;
}

@ -0,0 +1,48 @@
package cokr.xit.ci.api.presentation;
import cokr.xit.ci.api.code.ErrCd;
import cokr.xit.ci.api.model.ResponseVO;
import cokr.xit.ci.api.service.NiceCiService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.google.gson.Gson;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.ModelMap;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@RequiredArgsConstructor
public class NiceCiController {
private final NiceCiService diCiService;
@Value("${nice.api.ci.site-code}")
private String SITE_CODE;
@Value("${nice.api.ci.site-pw}")
private String SITE_PW;
@SuppressWarnings("deprecation")
@PostMapping("/nice/ci")
// public ResponseEntity<ResponseVO> ci(@RequestBody Map<String, Object> mParam) {
public ResponseEntity<ResponseVO> ci(@RequestBody String param) {
Gson gson = new Gson();
List<Map<String, String>> params = gson.fromJson(param, ArrayList.class);
List<String> jids = params.stream()
.map(row -> row.get("jid"))
.collect(Collectors.toList());
ResponseVO respVO = diCiService.findAllByJid(SITE_CODE, SITE_PW, jids);
return new ResponseEntity<ResponseVO>(respVO, HttpStatus.OK);
}
}

@ -0,0 +1,111 @@
package cokr.xit.ci.api.service;
import cokr.xit.ci.api.code.ErrCd;
import cokr.xit.ci.api.model.ResponseVO;
import cokr.xit.ci.api.service.suport.Interop;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Component
@RequiredArgsConstructor
public class NiceCiService {
/**
* CI .
* -.CI: (Connecting Information)
* -. .
* -. .
* -. -> -> CI (88 byte)
* @param siteCode
* @param sitePw
* @param jids
* @return
*/
public ResponseVO findAllByJid(String siteCode, String sitePw, List<String> jids) {
return ResponseVO.builder()
.errCode(ErrCd.OK)
.errMsg(ErrCd.OK.getCodeNm())
.resultInfo(
jids.stream()
// jids.parallelStream()
.map(jid -> {
ResponseVO responseVO = null;
try {
/* ========================
* validate
======================== */
if(StringUtils.isEmpty(siteCode)){
responseVO = ResponseVO.builder().errCode(ErrCd.ERR401).errMsg("사이트코드(은)는 필수조건 입니다.").build();
throw new RuntimeException(responseVO.getErrMsg());
}
if(StringUtils.isEmpty(sitePw)){
responseVO = ResponseVO.builder().errCode(ErrCd.ERR401).errMsg("사이트 패스워드(은)는 필수조건 입니다.").build();
throw new RuntimeException(responseVO.getErrMsg());
}
if(StringUtils.isEmpty(jid)){
responseVO = ResponseVO.builder().errCode(ErrCd.ERR401).errMsg("서비스 구분값(주민번호:JID)(은)는 필수조건 입니다.").build();
throw new RuntimeException(responseVO.getErrMsg());
}
/* ========================
* api call
======================== */
responseVO = Interop.getCI(siteCode, sitePw, jid);
} catch (Exception e){
log.error(e.getMessage());
} finally {
/* ========================
* result set
======================== */
Map<String, Object> m = new HashMap<>();
m.put("key", jid);
m.put("value", responseVO);
return m;
}
})
.collect(Collectors.toMap(m -> String.valueOf(m.get("key")), m -> m.get("value"), (k1, k2)->k1)))
.build();
}
/**
* sha256
* @param text
* @return
* @throws IOException
* @throws NoSuchAlgorithmException
*/
public static String hexSha256(String text) throws IOException, NoSuchAlgorithmException{
StringBuffer sbuf = new StringBuffer();
MessageDigest mDigest = MessageDigest.getInstance("SHA-256");
mDigest.update(text.getBytes());
byte[] msgStr = mDigest.digest() ;
for(int i=0; i < msgStr.length; i++){
byte tmpStrByte = msgStr[i];
String tmpEncTxt = Integer.toString((tmpStrByte & 0xff) + 0x100, 16).substring(1);
sbuf.append(tmpEncTxt) ;
}
return sbuf.toString();
}
}

@ -0,0 +1,97 @@
package cokr.xit.ci.api.service.suport;
import KISINFO.VNO.VNOInterop;
import cokr.xit.ci.api.code.ErrCd;
import cokr.xit.ci.api.model.ResponseVO;
public class Interop
{
public Interop()
{
}
public static ResponseVO getCI(String siteCode, String sitePw, String jumin) {
final String sSiteCode = siteCode; // NICE평가정보에서 발급한 서비스 사이트코드
final String sSitePw = sitePw; // NICE평가정보에서 발급한 서비스 사이트패스워드
final String sJumin = jumin.replaceAll("[^0-9]", ""); // 주민등록번호 13자리
final String sFlag = "JID"; // 서비스 구분값 (JID:주민번호 이용)
int iRtnCI = -1;
ErrCd errCode = null;
String errMsg = null;
String ci = null;
try {
// 모듈 객체 생성
VNOInterop vnoInterop = new VNOInterop();
/* ──── CI 값을 추출하기 위한 부분 Start */
// 인증요청처리
iRtnCI = vnoInterop.fnRequestConnInfo(sSiteCode, sSitePw, sJumin, sFlag);
System.out.println("=======================================================================");
System.out.println("JID=" + sJumin);
System.out.println("iRtnCI=" + iRtnCI);
// 인증결과코드에 따른 처리
if (iRtnCI == 1) {
// CI 값 추출 (연계정보 확인값, 88Byte)
String sConnInfo = vnoInterop.getConnInfo();
System.out.println("CONNINFO=[" + sConnInfo + "]");
// 결과설정
errCode = ErrCd.OK;
errMsg = String.format("[%s]\n(응답코드 %s)", ErrCd.OK.getCodeNm(), iRtnCI);
ci = sConnInfo;
} else if (iRtnCI == 3) {
System.out.println("[사용자 정보와 서비스 구분값 매핑 오류]");
System.out.println("사용자 정보와 서비스 구분값이 서로 일치하도록 매핑하여 주시기 바랍니다.");
// 결과설정
errCode = ErrCd.ERR405;
errMsg = String.format("[사용자 정보와 서비스 구분값 매핑 오류]\n사용자 정보와 서비스 구분값이 서로 일치하도록 매핑하여 주시기 바랍니다.\n(응답코드 %s)", iRtnCI);
} else if (iRtnCI == -9) {
System.out.println("[입력값 오류]");
System.out.println("fnRequestConnInfo 함수 처리시, 필요한 4개의 파라미터값의 정보를 정확하게 입력해 주시기 바랍니다.");
// 결과설정
errCode = ErrCd.ERR403;
errMsg = String.format("[입력값 오류]\nfnRequestConnInfo 함수 처리시, 필요한 4개의 파라미터값의 정보를 정확하게 입력해 주시기 바랍니다.\n(응답코드 %s)", iRtnCI);
} else if (iRtnCI == -21 || iRtnCI == -31 || iRtnCI == -34) {
System.out.println("[통신오류]");
System.out.println("방화벽 이용 시 아래 IP와 Port(총 5개)를 등록해주셔야 합니다.");
System.out.println("IP : 203.234.219.72 / Port : 81, 82, 83, 84, 85");
// 결과설정
errCode = ErrCd.ERR521;
errMsg = String.format("[통신오류]\n방화벽 이용 시 아래 IP와 Port(총 5개)를 등록해주셔야 합니다.\nIP : 203.234.219.72 / Port : 81, 82, 83, 84, 85.\n(응답코드 %s)", iRtnCI);
} else {
System.out.println("[기타오류]");
System.out.println("iRtnCI 값 확인 후 NICE평가정보 전산 담당자에게 문의");
// 결과설정
errCode = ErrCd.ERR999;
errMsg = String.format("[기타오류]\niRtnCI 값 확인 후 NICE평가정보 전산 담당자에게 문의.\n(응답코드 %s)", iRtnCI);
}
/* ──── CI 값을 추출하기 위한 부분 End */
} catch (Exception e) {
// 결과설정
errCode = ErrCd.ERR602;
errMsg = String.format("[나이스API 오류]\n%s\n(응답코드 %s)", e.getMessage(), iRtnCI);
} finally {
return ResponseVO.builder()
.errCode(errCode)
.errMsg(errMsg)
.resultInfo(ci)
.build();
}
}
}

@ -0,0 +1,36 @@
spring:
profiles:
active: prod
mvc:
view:
prefix: /WEB-INF/jsp/
suffix: .jsp
devtools:
livereload:
enabled: true #JSP ?? ? ?? ??? ?? ?? ??
pid:
file: joa.pid
logging:
file:
name: ./logs/logback.log
logback:
rollingpolicy:
file-name-pattern: ${LOG_FILE}.%d{yyyy-MM-dd}-%i.log
max-history: 30 #30??? ??
max-file-size:
100MB #????(100MB)
level:
root: info
'[org.hibernate.sql]': info
# =====================================================
# NICE ????
# =====================================================
nice:
api:
ci:
site-code:
site-pw:

@ -0,0 +1,326 @@
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>CI 변환 시스템</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.1/xlsx.full.min.js"></script>
<script src="https://uicdn.toast.com/grid/latest/tui-grid.js"></script>
<link rel="stylesheet" href="https://uicdn.toast.com/grid/latest/tui-grid.css" />
<!-- 공통 -->
<link rel="stylesheet" href="/resource/css/style.css" />
<script defer src="/resource/js/common.js"></script>
<!-- Drag&Drop -->
<link rel="stylesheet" href="/resource/css/file-drag-and-drop.css" />
<script defer src="/resource/js/file-drag-and-drop.js"></script>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script type="text/javascript">
window.onload = function(){
document.getElementById('fExcel').addEventListener('change', excelExport);
document.querySelector('#findBtn').addEventListener('click', findData);
stateDragAndDrop.callback = excelExport; //File Drag&Drop callback 이벤트 초기화
}
let readExcel = ()=>{
let input = event.target;
let reader = new FileReader();
reader.onload = function () {
let data = reader.result;
let workBook = XLSX.read(data, { type: 'binary' });
workBook.SheetNames.forEach(function (sheetName) {
let rows = XLSX.utils.sheet_to_json(workBook.Sheets[sheetName]);
})
};
reader.readAsBinaryString(input.files[0]);
}
let excelExport = (event)=>{
state.init();
excelExportCommon(event, handleExcelDataAll);
}
function excelExportCommon(event, callback){
let input = event.target;
let reader = new FileReader();
reader.onload = function(){
let fileData = reader.result;
let wb = XLSX.read(fileData, {type : 'binary'});
let sheetNameList = wb.SheetNames; // 시트 이름 목록 가져오기
sheetNameList.forEach(function(sheetName){
let sheet = wb.Sheets[sheetName]; // 첫번째 시트
callback(sheet, sheetName);
});
state.feature(); //기능함수 호출
instance.resetData(state.resultInfo[0].data); //데이터출력
};
reader.readAsBinaryString(input.files[0]);
}
function handleExcelDataAll(sheet, sheetName){
handleExcelDataHeader(sheet); // header 정보
handleExcelDataJson(sheet); // json 형태
handleExcelDataCsv(sheet); // csv 형태
handleExcelDataHtml(sheet); // html 형태
handleExcelDataGrid(sheet, sheetName); // grid 형태
}
function handleExcelDataHeader(sheet){
let headers = get_header_row(sheet);
$("#displayHeaders").html(JSON.stringify(headers));
}
function handleExcelDataJson(sheet){
$("#displayExcelJson").html(JSON.stringify(XLSX.utils.sheet_to_json (sheet)));
}
function handleExcelDataCsv(sheet){
$("#displayExcelCsv").html(XLSX.utils.sheet_to_csv (sheet));
}
function handleExcelDataHtml(sheet){
$("#displayExcelHtml").html(XLSX.utils.sheet_to_html (sheet));
}
let state = {
init: function(){
this.currentSheetIdx=0;
this.resultInfo = [];
},
currentSheetIdx : 0,
defColumn: [
//첫번째 시트 컬럼 정의..
{
name: '성명',
jid: '주민번호'
}
],
resultInfo: [],
feature: function(){
let sheet1 = this.resultInfo[0].data;
}
}
function handleExcelDataGrid(sheet, sheetName){
let mSheet = {
sheetName: sheetName,
title: '',
col: state.defColumn[state.currentSheetIdx],
data: []
};
//HeaderName..
let headers = get_header_row(sheet);
mSheet.title = getHeaderName(headers);
//Header..
const dataset = XLSX.utils.sheet_to_json(sheet);
for(let col in dataset[0]){
for(key in mSheet.col){
if(mSheet.col[key]==dataset[0][col]){ //컬럼명칭이 일치하면...
mSheet.col[key]=col; //col 값으로 replace
break;
}
}
}
//Body..
for(let i=0; i<dataset.length; i++){
let row = dataset[i];
let data = {};
for(let key in mSheet.col){
data[key] = row[mSheet.col[key]];
}
mSheet.data.push(data);
}
//Push Dataset..
state.resultInfo.push(mSheet);
function getHeaderName(headers){
headers.forEach(function(text, idx){
headers[idx] = text.replace(/UNKNOWN+ [0-9]/gi,'');
});
return headers.join('').trim();
}
}
// 출처 : https://github.com/SheetJS/js-xlsx/issues/214
function get_header_row(sheet) {
let headers = [];
let range = XLSX.utils.decode_range(sheet['!ref']);
let C, R = range.s.r; /* start in the first row */
/* walk every column in the range */
for(C = range.s.c; C <= range.e.c; ++C) {
let cell = sheet[XLSX.utils.encode_cell({c:C, r:R})] /* find the cell in the first row */
let hdr = "UNKNOWN " + C; // <-- replace with your desired default
if(cell && cell.t) hdr = XLSX.utils.format_cell(cell);
headers.push(hdr);
}
return headers;
}
let findData = ()=>{
const jsonParam = JSON.stringify(instance.getData()
.map(row => {
let obj = {};
obj.name = row.name;
obj.jid = row.jid;
return obj;
}));
$.ajax({
type : "POST",
url : "/nice/ci",
dataType: 'json',
contentType: 'application/json; charset=utf-8',
data : jsonParam,
success : function(resp){
let data = instance.getData()
.map(row => {
row.ci = getMatchedCi(resp.resultInfo, row.jid)
return row;
});
instance.resetData(data);
},
error : function(XMLHttpRequest, textStatus, errorThrown){ // 비동기 통신이 실패할경우 error 콜백으로 들어옵니다.
alert("통신 실패.")
}
});
function getMatchedCi(mJidInfo, jid){
let jidInfo = mJidInfo[jid];
if(jidInfo.errCode == 'OK')
return jidInfo['resultInfo'];
else
return jidInfo['errMsg'];
}
}
</script>
<style type="text/css">
div.content{
margin: 10px;
}
</style>
</head>
<body>
<div class="app-container">
<div class="app-item nav">
<jsp:include page="nav.jsp"></jsp:include>
</div>
<div class="app-item article">
<div class="app-container" style="align-items: center">
<h1>CI 변환</h1>
<details>
<summary>메뉴 세부정보</summary>
<ul>
<li>주민번호에 대한 CI 를 취득 합니다.</li>
<li>첨부파일은 엑셀문서(xls,xlsx)만 가능하며, 시트에는 `A열:성명, B:주민번호` 가 작성되어 있어야 합니다.</li>
<span>시트 작성</span>
<ul>
<li>1행: 컬럼명(A열:성명, B열:주민번호)</li>
<li>2~xxx행: 데이터</li>
</ul>
</ul>
</details>
</div>
<div class="content">
<input type="file" id="fExcel" name="fExcel" style="display: none;"/>
<div class="file-drag-and-drop">
<p>첨부파일(xls,xlsx)을 이곳에 올려주세요</p>
</div>
<!-- <h1>Header 정보 보기</h1> -->
<!-- <div id="displayHeaders"></div> -->
<!-- <h1>JSON 형태로 보기</h1> -->
<!-- <div id="displayExcelJson"></div> -->
<!-- <h1>CSV 형태로 보기</h1> -->
<!-- <div id="displayExcelCsv"></div> -->
<!-- <h1>HTML 형태로 보기</h1> -->
<!-- <div id="displayExcelHtml"></div> -->
</div>
<div>
<input type="button" id="findBtn" value="CI 조회 하기"/>
</div>
<div id="grid" class="tuigrid"></div>
</div>
</div>
</body>
<script type="text/javascript">
//import Grid from 'tui-grid'; /* ES6 */
const Grid = tui.Grid;
const instance = new Grid({
el: document.getElementById('grid'), // Container element
// data: {
// initialRequest: false,
// api: {
// readData: { url: 'nice/ci', method: 'POST'},
// modifyData: { url: 'nice/ci', method: 'POST'}
// },
// contentType: 'application/json',
// serializer(params){
// return JSON.stringify(instance.getData()
// .map(row => {
// let obj = {};
// obj.name = row.name;
// obj.jid = row.jid;
// return obj;
// }));
// }
// },
rowHeaders: ['rowNum'],
bodyHeight: 450,
columns: [
{
header: '성명',
name: 'name',
minWidth: 100,
filter: 'select',
sortingType: 'desc',
sortable: true
},
{
header: '주민번호',
name: 'jid',
minWidth: 100,
filter: 'select',
sortingType: 'desc',
sortable: true
},
{
header: 'CI',
name: 'ci',
minWidth: 100,
filter: 'select',
sortingType: 'desc',
sortable: true
}
]
});
// instance.resetData(newData); // Call API of instance's public method
Grid.applyTheme('striped'); // Call API of static method
</script>
</html>

@ -0,0 +1,24 @@
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="UTF-8">
<head>
<meta charset="UTF-8">
</head>
<body>
<div>
<ul class="nav">
<li class="dp1">
<a href="/">CI 변환</a>
</li>
</ul>
</div>
</body>
</html>

@ -0,0 +1,18 @@
.file-drag-and-drop{
outline: 2px dashed #92b0b3 ;
outline-offset:-10px;
text-align: center;
transition: all .15s ease-in-out;
width: 300px;
height: 100px;
background-color: gray;
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: center;
align-content: center;
flex-wrap: wrap;
}

@ -0,0 +1,31 @@
html{
font-size: 15px;
}
div.app-container{
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
div.app-item.nav{
padding: 10px;
background-color: burlywood;
width: 14rem;
flex-grow: 0;
}
div.app-item.article{
padding: 10px;
background-color: whitesmoke;
width: 50%;
flex-grow: 1;
}
details {
margin: 10px;
}
details > summary:hover {
cursor: pointer;
color: blue;
}

@ -0,0 +1,73 @@
const stateDragAndDrop = {
callback: ()=>{},
attchFileCnt: 1
}
$('.file-drag-and-drop')
.on("dragover", dragOver)
.on("dragleave", dragOver)
.on("drop", uploadFiles);
// function dragOver(e){
// e.stopPropagation();
// e.preventDefault();
// }
function dragOver(e) {
e.stopPropagation();
e.preventDefault();
if (e.type == "dragover") {
$(e.target).css({
"background-color": "black",
"outline-offset": "-20px"
});
} else {
$(e.target).css({
"background-color": "gray",
"outline-offset": "-10px"
});
}
}
// function uploadFiles(e){
// e.stopPropagation();
// e.preventDefault();
// }
function uploadFiles(e) {
e.stopPropagation();
e.preventDefault();
dragOver(e); //1
e.dataTransfer = e.originalEvent.dataTransfer; //2
let files = e.target.files || e.dataTransfer.files;
if (files.length > stateDragAndDrop.attchFileCnt) {
alert('파일은 '+stateDragAndDrop.attchFileCnt+'개만 올리세요.');
return;
}
if (files[0].type.match(/image.*/)) {
$(e.target).css({
"background-image": "url(" + window.URL.createObjectURL(files[0]) + ")",
"outline": "none",
"background-size": "100% 100%"
});
}else{
// alert('이미지가 아닙니다.');
// return;
// debugger;
let event = {
target: {
files: files
}
}
// excelExportCommon(event, handleExcelDataAll);
stateDragAndDrop.callback(event);
}
}
Loading…
Cancel
Save