[Java, JS] SpringBoot 프로젝트 : 잔여시간 계산 및 차감 Ajax 처리
목표
- 시간권 구매 회원이 좌석 입실 시 실시간 잔여시간 차감 기능 구현
- setInterval 사용하지 않아야 함.
- 페이지 이동 간 시간 계산이 지속적으로 이루어져야 함.
- 페이지 이동, 새로고침 등에 의해 시간 누수가 발생하지 않아야 함.
[기본 로직]
- 모든 페이지에서 지속적으로 시간 계산이 이루어져야 하므로, 모든 페이지에 따라다니는 메뉴바에 JS파일 링크
- JS세션에 입실했을 때 시간을 저장
- JS의 Date 객체를 통해 현재 시간을 구함.
- 입실시간 - 현재시간 결과가 60이 되면, 즉 60초가 지나면 DB와 화면을 업데이트하는 function을 만들어 실행
- 퇴실 시 세션에 저장된 입실시간 제거
[문제점]
- JS는 유니티 update함수처럼 프레임마다 실행시켜주지 않아서 실시간 처리가 불가능→setInterval 불가피
- 1번과 같은 이유로 페이지가 새로고침 되지 않으면, 현재 시간이 첫 호출 시 저장된 Date로 고정됨→시간 누수 발생
[수정 로직]
- 모든 페이지에서 지속적으로 시간 계산이 이루어져야 하므로, 모든 페이지에 따라다니는 메뉴바에 JS파일 링크(동일)
- JS세션에 입실 시 초를 계산하는 변수 저장
- setInterval 함수 1초마다 호출
- setInterval 함수 내 조건문을 통해 3번 변수가 60이 되면 Ajax 실행
- Ajax를 통해 DB에서 잔여시간을 불러온 후, 중첩 Ajax로 잔여시간-60을 계산한 값 DB에 전송
- 퇴실 시 세션에 저장된 입실시간 제거(동일)
[데이터 흐름도]
- 회원 입실 시 SeatController에서 좌석번호, 회원 입실 상태, 회원번호를 받아 마이페이지로 전달
- forResponsive.js에서 jsp에 전달된 세 변수 찾아서 JS세션에 저장
- checkDeadlineTime.js은 세션에 초 계산 변수가 있으면 setInterval 실행 매초 해당 변수+1한 값을 다시 세션에 저장
- 60초가 되면 조건문에 따라 Ajax 실행해서 SeatController에 해당 회원 번호와 60초 줄어든 잔여시간 전달
[SeatController]
회원이 이용할 수 있는 잔여시간이 있을 때, 입실할 좌석을 선택할 수 있다.
좌석을 선택하면 SeatController에 다음과 같은 매핑으로 데이터가 전달된다.
@RequestMapping("/seat/select.do")
public String selectSeat(@RequestParam int seat_num,HttpServletRequest request,RedirectAttributes attributes, Model model) {
HttpSession session = request.getSession();
MemberVO member = (MemberVO)session.getAttribute("user");
//일부 발췌
세션에 저장되어있던 user 정보를 가져온다.
회원이 좌석 선택을 완료하고 정상적으로 입실 처리가 되었다면, 마이페이지로 redirect된다.
이때 마이페이지에서 입실한 회원의 정보를 받아오기 위해 addFlashAttribute를 사용했다.
이렇게하면 redirect될 때 한 번 해당 컨트롤러로 원하는 데이터를 넘겨줄 수 있다.
attributes.addFlashAttribute("mem_statusForCheckIn", member.getMem_status());
attributes.addFlashAttribute("mem_numForCheckIn", member.getMem_num());
return "redirect:/mypage/myPageMain.do";
}
좌석 번호를 flash로 넘겨주지 않은 이유는 마이페이지에서 좌석번호가 있을 경우 주입받은 서비스에서 호출하여 이용 가능하기 때문이다.
[MypageController]
마이페이지에서 관련 데이터를 받는 부분만 보면 다음과 같다.
@RequestMapping("/mypage/myPageMain.do")
public String form(@RequestParam(value="pageNum", defaultValue="1")int currentPage, HttpSession session, Model model, HttpServletRequest request) {
//좌석 번호 가져오기
SeatVO seat = mypageService.selectCurSeat(user.getMem_num());
//전달 받은 데이터 담을 flashMap
Map<String, ?> flashMap = (Map<String, ?>) RequestContextUtils.getInputFlashMap(request);
//flashMap에 담긴 데이터가 있으면 모델에 담아주는 조건문
if(flashMap!=null) {
model.addAttribute("mem_statusForCheckIn", flashMap.get("mem_statusForCheckIn"));
if(flashMap.get("mem_numForCheckIn") != null || flashMap.get("mem_numForCheckIn") != "") {
model.addAttribute("mem_numForCheckIn", flashMap.get("mem_numForCheckIn"));
}
}
//좌석번호 모델에 담아주기
model.addAttribute("seat", seat);
//마이페이지 호출
return "myPageMain"; //타일스 설정값
}
지금 보니 어차피 DB에서 다 꺼내올 수 있는 데이터들인데 왜 flash로 전달했는지 모르겠다(그 땐 무슨 이유가 있었던 것 같기도..).
아무튼 하고자 했던 것은 마이페이지jsp로 회원 입실 시 입실한 회원 번호, 좌석 번호, 입실 상태를 넘겨주는 것이었다.
[MypageHeader.jsp]
jsp로 넘겨준 데이터를 바로 JS에서 호출해서 쓰는게 가장 좋았지만 잘 안되는 바람에 태그id로 부여해서 JS에서 읽어다 썼다.
<c:if test = "${empty remainTerm}"><!--기간권 회원이 아닌 경우-->
<span id="${mem_statusForCheckIn}" class = "setCheckInStatus"></span>
<span id="${mem_numForCheckIn}" class = "setCheckInMemnum"></span>
<span id="${seat.seat_num}" class = "setCheckSeatNum"></span>
</c:if>
[forResponsive.js]
js에서는 다음과 같이 태그id 값을 저장해두고 각 값들을 js세션에 담아주었다.
$(function(){
let mem_statusForCheckIn = $('.setCheckInStatus').attr('id'); //입실상태
let mem_numForCheckIn = $('.setCheckInMemnum').attr('id'); //회원번호
let seat_numForCheckIn = $('.setCheckSeatNum').attr('id'); //좌석번호
if(mem_statusForCheckIn != ''){
sessionStorage.setItem("isSelect", mem_statusForCheckIn);//세션에 입실상태 저장
}
if(mem_numForCheckIn != '') {
sessionStorage.setItem("isSelectMemnum", mem_numForCheckIn);//세션에 회원번호 저장
}
if(seat_numForCheckIn != '') {
sessionStorage.setItem("isSelectSeatnum", seat_numForCheckIn);//세션에 좌석번호 저장
}
}
[checkDeadlineTime.js-상위조건문]
일단 큰 그림을 먼저 그려보면 다음과 같다.
If(sessionStorage.getItem(‘입실상태’) == ‘입실(1)’){ //회원이 입실해서 세션에 입실상태 변수가 1이면
sessionStorage.setItem(‘Sec변수’, ‘0’); //세션에 초단위 계산 변수를 0으로 초기화 생성
setInterval(function(){ //setInterval 실행
var seconds = Sec변수 + 1; //세션에 저장되어있는 Sec변수 불러와서 1더하기
sessionStorage.setItem(‘Sec변수’, seconds); //다시 세션에 1 더해진 Sec변수 저장
if(seconds >= 60) { //세션에 저장된 변수가 60, 즉 60초가 지나면 조건문 진입
중첩 ajax 실행 // Ajax 실행해서 원하는 데이터 처리 진행
}
}, 1000); //1초마다 setInterval 실행
}else if(sessionStorage.getItem(‘입실상태’) == ‘퇴실(0)’{ //퇴실해서 입실상태 변수가 0이 되면
sessionStorage.removeItem(‘입실상태’);
sessionStorage.removeItem(‘Sec변수’); //세션에 있는 변수들 삭제
}
- 회원이 입실 시 세션에 입실 상태 변수가 1이되고 조건문으로 진입
- 초를 담아둘 변수를 0으로 초기화(이건 세션에 Sec변수가 null일 경우에만)
- setInterval 함수 1초마다 실행
- 매초 Sec변수에 1 더한 값을 세션에 다시 저장
- 60초가 되면 원하는 Ajax 실행
- 퇴실 시 세션에 저장된 데이터 삭제
아래는 해당부분 작성한 코드다.
if (sessionStorage.getItem('isSelect') == '1') {
if (sessionStorage.getItem('plusSec') == null) {
sessionStorage.setItem('plusSec', '0');
}
setInterval(function() {
var updateSec = sessionStorage.getItem('plusSec');
updateSec = parseInt(updateSec) + 1;
sessionStorage.setItem('plusSec', updateSec);
if (updateSec >= 60) {
sessionStorage.setItem('plusSec', '0');
let mem_num = sessionStorage.getItem('isSelectMemnum');
let seat_num = sessionStorage.getItem('isSelectSeatnum');
let newRemain = 0; //시간 처리 후 계산된 값을 담을 변수
let remainTime; //DB에 저장된 잔여시간을 담을 변수
}
}else if (sessionStorage.getItem('isSelect') == '0') {
sessionStorage.removeItem("isSelect");
sessionStorage.removeItem("plusSec");
}
[checkDeadlineTime.js-상위Ajax]
잔여시간을 계산할 때 몇 가지 상황에 따라 데이터 처리를 달리해야해서 Ajax 하위에 Ajax를 두는 식으로 작성했다.
- 기본 시간 차감 - 1분마다 DB에 잔여시간 업데이트
- 잔여 시간 5분 이하 - 잔여시간이 5분 이하가 됐을 때 알림 + 충전 여부 확인
- 잔여시간 소진 - 잔여시간 모두 소진 시 알림 + 자동 퇴실
가장 상위 Ajax 구조는 다음과 같다.
$.ajax({
url : ‘../seat/deadlineCheck.do’,
data : {mem_num : mem_num},
type : ‘post’,
datatype : ‘json’,
success : function(param){
if(param.result == ‘success’){ 1 }
else if(param.result == ‘lessThanFive’){ 2 }
else if(param.result == ‘setLogout’){ 3 }
else {alert(‘잔여시간 불러오기 오류 발생‘)}
},error : function(){
alert(‘네트워크 오류발생(시간가져오기)’);
}
});
SeatController의 deadlineCheck.do에 회원 번호를 넘겨주고 해당 회원의 잔여시간을 받아온다.
잔여시간이 없거나 5분 이하일 경우 2번과 3번 조건문에 들어가게 되고 아닌 경우 1번 조건문에 들어가게 된다.
[checkDeadlineTime.js-1번 조건]
1번 조건에 들어갈 경우 Ajax 처리는 다음과 같다.
if(param.result == 'success') {
remainTime = parseFloat(param.time); //컨트롤러에서 time변수에 담아준 잔여시간 할당
newRemain = parseFloat(remainTime) - parseFloat(60.0);//잔여시간에서 60초 뺀 시간 담아두기
$.ajax({
url : '../seat/updateDeadline.do', //시간 업데이트 할 매핑 호출
data : {newRemain : newRemain}, //1분 차감된 잔여시간 전달
type : 'post',
dataType : 'json',
success : function(param){
if(param.result == 'success') {
if(reloadDiv != ''){
//마이페이지에서 잔여시간UI 부분만 새로고침
$('#remainTimeZone').load('../mypage/myPageMain.do #remainTimeZone');
}
}else {
alert('잔여시간 업데이트 오류 발생');
}
},
error : function(){
alert('NETWORK ERROR(updateTime)');
}
});
}
잔여시간이 5분 초과로 남아있을 경우 1번 조건문이 실행된다.
컨트롤러에 회원번호를 넘겨주고 잔여시간은 time이라는 변수에 넣어줬다.
param.time을 통해 변수에 담겨있던 잔여시간을 꺼내 remainTime에 할당한 후 60을 뺀 값을 newRemain에 저장했다.
SeatController의 updateDeadline.do를 호출하면 전달받은 새 잔여시간을 DB에 업데이트 하도록 했다.
성공적으로 잔여시간이 업데이트 되었다면 마이페이지에 잔여시간을 표시하는 UI 부분 태그만 새로고침 되도록 했다.
[checkDeadlineTime.js-2번 조건]
2번 조건에 들어갈 경우 Ajax 처리는 다음과 같다.
else if (param.result == 'lessThanFive') {
let check = confirm('잔여시간이 5분 남았습니다. 이용권을 추가로 결제할까요?');
if (check) {
remainTime = parseFloat(param.time);
newRemain = parseFloat(remainTime) - parseFloat(60.0);
$.ajax({
url: '../seat/updateDeadline.do',
data: { newRemain: newRemain },
type: 'post',
dataType: 'json',
success: function(param) {
if (param.result == 'success') {
if (reloadDiv != '') {
sessionStorage.setItem('buyTicket', 'buy');
$('#remainTimeZone').load('../mypage/myPageMain.do #remainTimeZone');
location.href = "../seat/out.do?seat_num=" + seat_num;
location.href = '../ticket/study_ticketList.do?seat_num=' + seat_num;
}
} else {
alert('잔여시간 업데이트 오류 발생(5분이하일때)');
}
},
error: function() {
alert('NETWORK ERROR(updateTime)');
}
});
} else {
remainTime = parseFloat(param.time);
newRemain = parseFloat(remainTime) - parseFloat(60.0);
$.ajax({
url: '../seat/updateDeadline.do',
data: { newRemain: newRemain },
type: 'post',
dataType: 'json',
success: function(param) {
if (param.result == 'success') {
if (reloadDiv != '') {
$('#remainTimeZone').load('../mypage/myPageMain.do #remainTimeZone');
}
} else {
alert('잔여시간 업데이트 오류 발생(5분이하인데 연장안할때)');
}
},
error: function() {
alert('NETWORK ERROR(updateTime)');
}
});
alert('5분 후 자동 퇴실됩니다.');
}
}
복잡해보이지만 1번과 비슷하다.
5분 이하일 경우 알림창을 띄우고 시간을 충전할 것인지 묻는 조건문을 추가해줬다.
추가하겠다고 한 경우 우선 퇴실 처리 후 이용권 구매 페이지로 연결해줬고, 추가하지 않겠다고 한 경우 남은 시간은 계속 줄어들어야 하므로 1번과 동일한 처리를 해줬다.
이것 저것 테스트 해보니 오류가 좀 있었다.. 수정한다고 했지만 미완 상태다..나중에 필요하면 수정해서 쓰자..
[checkDeadlineTime.js-3번 조건]
3번 조건에 들어갈 경우 Ajax 처리는 다음과 같다.
lse if (param.result == 'setLogout') {
$.ajax({
url: '../seat/updateDeadline.do',
data: { newRemain: 0.0 },
type: 'post',
dataType: 'json',
success: function(param) {
if (param.result == 'success') {
if (reloadDiv != '') {
}
} else if (param.result == 'end') {
location.href = "../seat/out.do?seat_num=" + seat_num;
alert('잔여시간이 모두 소진되었습니다.');
sessionStorage.setItem("isSelect", '0');
location.href = "../seat/selectForm.do";
}
else {
alert('종료 후 잔여시간 업데이트 오류 발생');
}
},
error: function() {
alert('NETWORK ERROR(updateTimeWhenLogout)');
}
});
}
잔여시간이 모두 소진된 경우 3번 조건에 들어오게 된다.
잔여시간을 0으로 초기화해주고, 퇴실처리 이후 세션에서 입실 상태를 0으로 바꿔주었다.
마지막엔 좌석 선택 페이지로 이동하도록 했다.
[논의 및 한계]
- JS 세션 및 중첩 Ajax 사용으로 데이터 처리하는 방법을 알아봤다.
- JS에서 저장하는 세션과 Java에서 저장하는 세션이 다르다는 것을 알았다.
- 지속적으로 정밀한 계산을 요구하는 작업에 setInterval 함수가 적절하지 않음을 알았다(정확히 1초 단위로 진행안됨).
- 시간에 쫓겨 세세한 오류를 다 잡지 못했다(5분 이하 조건 처리 시 페이지 이동, 입퇴실 처리, 강제 웹페이지 종료 시).
- 코드랑 변수명 등을 더 체계적으로 작성했어야 했다.
- 중첩으로 Ajax를 사용하는 것이 효율적인 프로그래밍인지 모르겠다. 다른 처리 방식도 알아볼 필요성..