출결관리

출결 관리는 자가진단이 성공적으로 제출된 후 진행할 수 있으며 ‘출결 관리' 메뉴로 들어가면 QR코드가 생성됩니다.
(QR 코드는 관리자가 입실, 퇴실에 사용하는 각 개인의 QR 코드입니다.)

출결 관리 버튼을 클릭하시면 체크인 화면으로 넘어갑니다. 
체크인은 입실과 퇴실을 체크인하며 입실과 퇴실에 대한 표시는 메인화면 출결관리 버튼에 표시합니다.

체크인은 교육 참석 방법에 따라 'QR 체크인'과 '비대면 체크인' 방식으로 입실과 퇴실을 체크합니다.

QR체크인

'QR 체크인'은 대면 교육 시 교육장에 방문하여 비치된 QR 리더기에 생성된 QR코드를 인식 시켜 체크인하는 방법입니다.

화면구성

소스참고
EduHub\mobile\Mobile_frmSeedQrCode.xfdl (Tabpage1)

Mobile_frmSeedQrCode.xfdl (Tabpage1)

1 ImageViewer
이미지로 생성된 QR코드를 출력합니다.

웹브라우저 QR코드 문제 이미지 표시로 해결

처음 개발에서 QR코드를 웹브라우저로 생성하여 보여주는 방식으로 구현하였는데, 이 방식의 문제는 모바일에서 웹 브라우저 인식의 차이로 인해 이미지 크기가 제각각으로 출력합니다. 해결 방법은 서버에서 이미지 파일을 Write하는 방식으로 변경해서 이미지를 서버에서 만들고 넥사크로에서는 ImageViewer를 이용하여 표현해서 문제를 해결했습니다.

주요 기능 구현 설명

  1. 화면 로드시 QR코드 생성에 필요한 파라미터값을 구성합니다.

var sParameter = "";
this.frmQrCode_onload = function(obj:nexacro.Form,e:nexacro.LoadEventInfo)
{  
	this.gfn_eduformOnLoad(this);
    sParameter = this.getOwnerFrame().pEDU_PRODUCT_CODE + ":" 
    + this.getOwnerFrame().pES_SEQ + ":" + this.getOwnerFrame().pESL_SEQ;
};
  1. QR코드는 화면에서 생성하지 않고 서버에서 생성합니다. QR코드 생성에 필요한 정보들을 가지고 QR코드를 생성하는 makeSeedQrCode.jsp 를 호출합니다.

this.fn_qrCode = function(arg) 
{    
	this.gfn_transCOMMClear();     
	// input , output,args,select  
	var args = "QRCODE=" + arg;    
 	  
	this.transaction("qrCodeView",nexacro.getApplication().GV_QRCODE_PATH 
    + "makeSeedQrCode.jsp","","ds_QrImage=output",args,"fnCallback");
};
  1. QR코드는 makeSeedQrCode.jsp의 소스입니다.

소스참고
makeSeedQrCode.jsp
<%@ page import = "java.awt.image.BufferedImage, javax.imageio.ImageIO" %>
<%@ page import = "com.nexacro17.xapi.data.*" %>
<%@ page import = "com.nexacro17.xapi.tx.*" %>
<%@ page import = "com.google.zxing.BarcodeFormat"%>
<%@ page import = "com.google.zxing.client.j2se.MatrixToImageWriter"%>
<%@ page import = "com.google.zxing.common.BitMatrix"%>
<%@ page import = "com.google.zxing.qrcode.QRCodeWriter"%>
<%@ page import = "sun.misc.BASE64Decoder"%>
<%@ page import = "sun.misc.BASE64Encoder"%>
<%
HttpPlatformRequest pReq = new HttpPlatformRequest(request);
pReq.receiveData();
PlatformData in_pData = pReq.getData();

VariableList in_varList = in_pData.getVariableList();
String qrString = in_varList.getString("QRCODE");

qrString = encrypt(padString(qrString));
  
PlatformData out_pData = new PlatformData();
int    nErrorCode  = 0;
String sErrorMsg = "SUCC";

QRCodeWriter q = new QRCodeWriter();  

qrString = new String(qrString.getBytes("UTF-8"), "ISO-8859-1");

QRCodeWriter writer = new QRCodeWriter();    
BitMatrix qrCode = writer.encode(qrString, BarcodeFormat.QR_CODE, 512, 512);   
 
BufferedImage qrImage = MatrixToImageWriter.toBufferedImage(qrCode);   

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 
ImageIO.write(qrImage, "png", byteArrayOutputStream); // 파일 포맷 일치 필요 


DataSet ds = new DataSet("output");
ds.addColumn("QrImage", DataTypes.BLOB,255);

int row = ds.newRow();
ds.set(row ,"QrImage", byteArrayOutputStream.toByteArray());

out_pData.addDataSet(ds);

%>

오픈소스 : zxing모듈 적용

암호 알고리즘 : 한국인터넷진흥원(KISA) SEED

QR코드 생성시 보안은 매우 중요 하므로 개인정보가 들어가지 않도록 설계하고, 암호화를 반드시 진행합니다.

보안을 위해 QR코드는 파일로 생성하지 말고 일회성으로 생성하고 끝내는 방식으로 구현하거나, 실행 시 생성하는 것을 추천합니다.

QR코드를 읽는 방법은 전면 카메라를 이용하여 인식하는 방법과 카메라 셀카 모드로 변경하여 읽는 방법이 있다. 이때 셀카모드를 적용하게 되면 사용자가 움직이는 방향과 카메라가 인식하는 방향이 반대로 인식되어 사용에 불편함이 발생한다. 반드시 셀카모드에서는 카메라 인식방향과 사용자 방향을 일치해 주어야 한다.

비대면 체크인

'비대면 체크인'은 온라인 교육 시 강사가 보여주는 숫자를 빈칸에 입력 한 후 체크인하는 방식입니다.

화면구성

소스참고
EduHub\mobile\Mobile_frmSeedQrCode.xfdl (Tabpage2)

Mobile_frmSeedQrCode.xfdl (Tabpage2)

1 MaskEdit
숫자를 입력하거나 출력합니다. 5개의 숫자가 강사가 보여주는 숫자와 동일한지를 체크합니다.

QR 리더

QR코드 체크인에서 생성된 QR코드를 인식하는 화면으로 QR코드 Reader기능을 가지고 있는 관리자 화면입니다.

GettyImages-1140838525-e1606979514948

QR코드 리더기는 교육장 입/퇴실 시 입구에 비치되어 있으며, 로그인시 화면에 생성된 QR코드로 스캔합니다.

화면구성

소스참고
EduHub\mobile\Mobile_frmQrCodeReader.xfdl

Mobile_frmQrCodeReader.xfdl

1 Grid
Dataset 'ds_data'의 정보를 이용하여 입실/퇴실 등의 정보를 표시합니다.

2 WebBrowser
QR코드 Reader 하는 부분으로 생성된 QR코드를 이곳에 맞춰야 체크인 작업이 완료됩니다.

3 VideoPlayer
정상처리, 비정상처리(오류),이미 처리된 상황을 음성메세지로 표현합니다.

주요 기능 구현 설명

암호화 풀기

WebBrowser의 onusernotify 이벤트는 WebBrowser 에 로드된 웹페이지에서 nexacro 쪽으로 정보를 전달할 때 발생하는 이벤트입니다.

this.webRrCode_onusernotify = function(obj:nexacro.WebBrowser,e:nexacro.WebUserNotifyEventInfo)
{      
	var key = CryptoJS.enc.Utf8.parse(gdsUser.getColumn(gdsUser.rowposition,"TOBE_YEAR"));
	var iv  = CryptoJS.enc.Utf8.parse(gdsUser.getColumn(gdsUser.rowposition,"TOBE_IV"));
}

crypto-js


표준 및 보안 암호화 알고리즘의 JavaScript 구현

-----------------------------------------------------

CryptoJS는 모범 사례와 패턴을 사용하여 JavaScript로 구현 된 표준 및 보안 암호화 알고리즘 모음입니다. 빠르고 일관되고 간단한 인터페이스를 가지고 있습니다

QR코드 리드기능 ? 오픈소스 : jsQR모듈

this.frmQrCodeReader_onload = function(obj:nexacro.Form,e:nexacro.LoadEventInfo)
{

	this.webRrCode.set_url(nexacro.getApplication().GV_QRREAD_PATH + "QrCode3.jsp?facingMode=" + flag + "&time=" + objDate.getTime());

};
이번 프로젝트에서 가장 핵심적인 기술을 뽑으라면 ‘QR코드의 인식률’을 높이는 것이었다. 관련 자료를 찾던 중 별도의 설치모듈이 없어도 사용이 가능한 오픈소스를 발견하여 의외로 쉽게 적용했다. 이때 주의사항이 있는데, jsQR모듈은 HTML5환경에서 사용을 해야 하기 때문에 통신방식은 반드시 HTTPS를 적용해야 한다.

jsQR모듈은 HTML5환경에서 사용을 해야 하기 때문에 통신방식은 반드시 HTTPS를 적용해야 한다.

전면 카메라 vs. 셀카모드

소스참고
QrCode3.js
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
	String facingMode = request.getParameter("facingMode");
	if(facingMode == null)
	{
		facingMode = "environment";
	}	
%>

<html>
<head>
<meta charset="utf-8">
<title>QR Code</title>
<script src="./jsQR.js"></script>
<link href="https://fonts.googleapis.com/css?family=Ropa+Sans" rel="stylesheet">
<style>
  body {
    font-family: 'Ropa Sans', sans-serif;
    color: #333;
    max-width: 640px;
    margin: 0 auto;
    position: relative;
  }

  #githubLink {
    position: absolute;
    right: 0;
    top: 12px;
    color: #2D99FF;
  }

  h1 { 
    margin: 10px 0;
    font-size: 40px;
  }

  #loadingMessage {
    text-align: center;
    padding: 40px;
    background-color: #eee;
  }

  #canvas {
    width: 100%;
		height: 80%;
		max-width: 400px;  		
		background-color: #666666;
		
		<%
			if(facingMode == "environment")
			{
			} else {
		%>	
					
		    /*Mirror code starts*/
		    transform: rotateY(180deg);
		    -webkit-transform:rotateY(180deg); /* Safari and Chrome */
		    -moz-transform:rotateY(180deg); /* Firefox */
		    /*Mirror code ends*/
		<%
			}
		%>	
    
  }         
   
  #output {
    margin-top: 12px;
    background: #f3f3f3;
    padding: 10px;
    padding-bottom: 0;
    text-align: center; 
  } 

  #output div {
    padding-bottom: 10px;
    word-wrap: break-word;
  }

  #noQRFound {
    text-align: center;
  }
</style> 
 <script type="text/javascript">
if (! window.NEXACROHTML) {
	window.NEXACROHTML = {};
}    
window.NEXACROHTML.FireUserNotify = function(userdata) {
	
	if (window.NEXACROWEBBROWSER) {
		  window.NEXACROWEBBROWSER.on_fire_onusernotify(window.NEXACROWEBBROWSER, userdata);
	} else {
		
		  window.document.title = userdata;
	  }			
}

  function setContent(str)
  {       
  	var d = new Date(); 
  	window.NEXACROHTML.FireUserNotify(d.getTime() + "QrCode:" + str);    	
  } 
  
  </script>
</head>
<body>
<div id="loadingMessage">Unable to access video stream (please make sure you have a webcam enabled)</div>
<canvas id="canvas" hidden></canvas>
  <div id="output" hidden>
  <div id="outputMessage">QR코드를 카메라에 위치시켜 주세요.</div>
  <div hidden><b></b> <span id="outputData"></span></div>
</div>  
<script>
	isScanning=false; /* variable to stop or start scanning loop */	
	var mytimerId;
  var video = document.createElement("video");
  var canvasElement = document.getElementById("canvas");
  var canvas = canvasElement.getContext("2d");
  var loadingMessage = document.getElementById("loadingMessage");
  var outputContainer = document.getElementById("output");
  var outputMessage = document.getElementById("outputMessage");
  var outputData = document.getElementById("outputData");
  var strTemp = "";  
  streamTemp      = null;
  
  function fn_init()
  {
  	isScanning=true;

  }
  function drawLine(begin, end, color) {
    canvas.beginPath();
    canvas.moveTo(begin.x, begin.y);
    canvas.lineTo(end.x, end.y);
    canvas.lineWidth = 5;
    canvas.strokeStyle = color;
    canvas.stroke();   
  }
 
  // Use facingMode: environment to attemt to get the front camera on phones  environment /user
  navigator.mediaDevices.getUserMedia({ video: { 
  	facingMode: "<%=facingMode%>", 
  	width: { exact: 400 },
		height: { exact: 400 },
    focusDistance: 1,
    qrbox : 250,
  	} }).then(function(stream) {
    video.srcObject = stream;  
    video.setAttribute("playsinline", true); 
    video.play();
    
    isScanning = true;
    streamTemp = stream;
    requestAnimationFrame(tick);
  });

  function tick() {
    loadingMessage.innerText = "⌛ Loading video..."
    if (video.readyState === video.HAVE_ENOUGH_DATA) {
      loadingMessage.hidden = true;
      canvasElement.hidden = false;
      outputContainer.hidden = false;

      canvasElement.height = video.videoHeight;
      canvasElement.width = video.videoWidth;
      canvas.drawImage(video, 0, 0, canvasElement.width, canvasElement.height);
      var imageData = canvas.getImageData(0, 0, canvasElement.width, canvasElement.height);
      var code = jsQR(imageData.data, imageData.width, imageData.height, {
        inversionAttempts: "dontInvert",
      });   
      if (code) {
       	drawLine(code.location.topLeftCorner, code.location.topRightCorner, "#FF3B58");
        drawLine(code.location.topRightCorner, code.location.bottomRightCorner, "#FF3B58");
        drawLine(code.location.bottomRightCorner, code.location.bottomLeftCorner, "#FF3B58");
        drawLine(code.location.bottomLeftCorner, code.location.topLeftCorner, "#FF3B58");
        outputMessage.hidden = true;
        outputData.parentElement.hidden = false;
        //outputData.innerText = code.data;		      
        outputData.innerText = "QR코드 인식성공";		      
        
        isScanning = false;
        
	  		if(strTemp != code.data)
	  		{
	  			setContent(code.data);   
	  			strTemp = code.data; 
	  		}	     
	  		
      } else {  
        outputMessage.hidden = false;
        outputData.parentElement.hidden = true;
      }
    }
    
    if(isScanning){
          /* Continue scanning ... */
    		requestAnimationFrame(tick);
    }else{
          /* Stopping scan ... */
//          streamTemp.getTracks()[0].stop()
			mytimerId = setInterval(myStopFunction, 1000);
    }	
    
  }


function myStopFunction() {
  isScanning = true;
  clearInterval(mytimerId);
  requestAnimationFrame(tick);
}

function sleep(num){	//[1/1000초]
    var now = new Date();
	var stop = now.getTime() + num;
	while(true){
	    now = new Date();
		if(now.getTime() > stop)return;
	}
}

  function clearData()
  {
  			strTemp = "";
	}	

</script>    

</body>
</html>

카메라 모드 설정 (전면카메라/셀카모드)

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
	String facingMode = request.getParameter("facingMode");
	if(facingMode == null)
	{
		facingMode = "environment";
	}	
%>

facingMode

environment : 전면

user : 셀카

셀마코드 (미러링)

/*Mirror code starts*/

transform: rotateY(180deg);
-webkit-transform:rotateY(180deg); /* Safari and Chrome */
-moz-transform:rotateY(180deg); /* Firefox */
/*Mirror code ends*/

QR코드를 읽는 방법은 전면 카메라를 이용하여 인식하는 방법과 카메라 셀카 모드로 변경하여 읽는 방법이 있다.


이때 셀카모드를 적용하게 되면 사용자가 움직이는 방향과 카메라가 인식하는 방향이 반대로 인식되어 사용에 불편함이 발생한다.


반드시 셀카모드에서는 카메라 인식방향과 사용자 방향을 일치해 주어야 한다.

참고사항

QR체크인과 비대면체크인의 차이를 알고 싶습니다.

교육 출결체크 방법은 크게 QR체크인 방식과 비대면 체크인 방식을 제공합니다. 
둘의 차이는 다음과 같습니다.

교육 참석방법에 따라 한 가지 선택이 가능합니다.

QR체크인

대면교육 즉 교육장에 방문하여 교육을 참석하는 경우 사용하는 방식이며, 
교육장에 비치된 QR리더기를 통해서 출결처리를 할 때 사용합니다.

비대면체크인

온라인으로 교육을 진행하는 경우 사용하는 방식입니다. 
체크인 방식은 강사가 첫 교육 시작시 보여주는 숫자를 확인하여 비어있는 숫자를 입력 후 입실할 수 있으며, 퇴실의 경우 종료 1시간 전 또는 종료시간에 보여주는 숫자를 입력 후 퇴실 처리를 할 수 있습니다.

출결처리 전 자가진단은 반드시 진행을 해야 하나요?

교육 참석을 위해서는 자가진단이 필수로 진행이 되어야 하며, 이는 참가 교육생의 건강과 
감염병 확산방지를 사전에 차단하기 위함이니 적극적인 참여를 부탁합니다.
교육 당일 교육장 입장하기 전 본인의 건강 상태를 확인하고, 비치된 체온계를 이용하여 자가 진단 문진표 항목에 기입합니다. 자가 문진표 작성 후 출입 가능 메시지 화면이 나오면 입장이 가능합니다. 만약, 문진표 결과 출입이 불가한 경우 교육담당자에게 관련 내용을 즉시 알려주십시오. 담당자의 안내에 따라 이후 진행방안을 안내받게 됩니다.

자가 진단 결과는 별도 DB에 관리하지 않습니다.

네이버, 카카오톡에서 제공하는 QR코드로 인증할 수 있나요?

'투비교육포털'은 자체 제공하는 QR코드를 사용해야 인증이 가능합니다. 
간혹 네이버, 카카오톡 QR코드로 인증을 시도하는 분들이 있으나, 이는 사용이 불가능합니다.

네이버, 카카오의 QR코드 ❌ 아니죠~! ‘투비교육포털’에서는 앱내 생성된 자체 QR코드를 사용합니다

비대면 체크인시 현재 등록 가능한 입/퇴실 숫자 정보가 없습니다 관리자에게 문의하세요. 라는 메시지가 나와 입력이 불가능합니다.

비대면 체크인의 경우 관리자 또는 교육을 진행하는 강사가 사전 입실에 필요한 숫자(key) 정보를 생성해야 진행이 가능합니다.  등록 가능한 숫자가 정보가 없는 경우 해당 교육은 온라인 교육을 허용하지 않는 교육일 수 있으니 참고하시고, 또는 해당 교육 담당자에게 문의하시기 바랍니다.

QR코드 생성시 사용된 컴포넌트는 어떤 종류인지 알 수 있나요?

QR코드시 사용한 컴포넌트는 ImageViewer를 사용하였습니다.

처음 개발에서 QR코드를 웹브라우저로 생성하여 보여주는 방식으로 구현하였는데, 이 방식의 문제는 모바일에서 웹 브라우저 인식의 차이로 인해 이미지 크기가 제각각으로 보여졌다. 해결 방법은 서버에서 이미지 파일을 Write하는 방식으로 변경하는 것이다. 즉 이미지를 서버에서 만들고 넥사크로에서는 ImageViewer를 이용하여 표현하도록 변경하여 문제를 해결했다.

QR코드 생성은 어떻게 개발 했으며, 개발 시 주의사항을 알 수 있을까요?

최근 코로나19로 인해 많은 분이 QR코드를 사용하고 있어서 출입 시 어려움은 없을 것으로 예상했지만, 개발 측면에서 개인정보가 들어가는 부분이라 보안에 신경을 많이 썼다.

오픈소스 : zxing모듈 적용
암호 알고리즘 : 한국인터넷진흥원(KISA) SEED
QR코드 생성시 보안은 매우 중요. 대부분의 QR코드는 QR리더기에서 다 읽히는데 개인정보가 들어가지 않도록 설계하고, 암호화를 반드시 진행
QR코드는 파일로 생성하지 말고 일회성으로 생성하고 끝내는 방식으로 구현하거나, 실행 시 생성되어야 함.
≫ 웹브라우저 QR코드 이미지 표시 문제

처음 개발에서 QR코드를 웹브라우저로 생성하여 보여주는 방식으로 구현하였는데, 이 방식의 문제는 모바일에서 웹 브라우저 인식의 차이로 인해 이미지 크기가 제각각으로 보여졌다. 해결 방법은 서버에서 이미지 파일을 Write하는 방식으로 변경하는 것이다. 즉 이미지를 서버에서 만들고 넥사크로에서는 ImageViewer를 이용하여 표현하도록 변경하여 문제를 해결했다.

QR코드 리더기는 어떻게 개발 했으며, 개발 시 주의사항을 알고 싶습니다.

GettyImages-1140838525-e1606979514948

QR코드 리더기는 교육장 입/퇴실 시 입구에 비치되어 있으며, 로그인시 화면에 생성된 QR코드로 스캔합니다.

QR코드 리드기능 ? 오픈소스 : jsQR모듈

이번 프로젝트에서 가장 핵심적인 기술을 뽑으라면 ‘QR코드의 인식률’을 높이는 것이었다. 관련 자료를 찾던 중 별도의 설치모듈이 없어도 사용이 가능한 오픈소스를 발견하여 의외로 쉽게 적용했다. 이때 주의사항이 있는데, jsQR모듈은 HTML5환경에서 사용을 해야 하기 때문에 통신방식은 반드시 HTTPS를 적용해야 한다.

jsQR모듈은 HTML5환경에서 사용을 해야 하기 때문에 통신방식은 반드시 HTTPS를 적용해야 한다.

전면 카메라 vs. 셀카모드

QR코드를 읽는 방법은 전면 카메라를 이용하여 인식하는 방법과 카메라 셀카 모드로 변경하여 읽는 방법이 있다. 이때 셀카모드를 적용하게 되면 사용자가 움직이는 방향과 카메라가 인식하는 방향이 반대로 인식되어 사용에 불편함이 발생한다. 반드시 셀카모드에서는 카메라 인식방향과 사용자 방향을 일치해 주어야 한다.

개발하면서 참고했던 정보들

QR코드 생성에 필요한 오픈 소스

https://github.com/zxing/zxing

QR코드 리더기에 필요한 오픈 소스

https://github.com/cozmo/jsQR

암호 알고리즘 소스코드

한국인터넷진흥원(KISA) 제공
https://seed.kisa.or.kr/kisa/Board/17/detailView.do

투비소프트 앱빌더(App Builder)

http://docs.tobesoft.com/deployment_guide_nexacro_17_ko