프로젝트/기능 정리

[Java, JS] SpringBoot 프로젝트 : 잔여시간 계산 및 차감 Ajax 처리

민뇌 2023. 6. 6. 19:14

목표

  • 시간권 구매 회원이 좌석 입실 시 실시간 잔여시간 차감 기능 구현
  • setInterval 사용하지 않아야 함.
  • 페이지 이동 간 시간 계산이 지속적으로 이루어져야 함.
  • 페이지 이동, 새로고침 등에 의해 시간 누수가 발생하지 않아야 함.

 

[기본 로직]

  1. 모든 페이지에서 지속적으로 시간 계산이 이루어져야 하므로, 모든 페이지에 따라다니는 메뉴바에 JS파일 링크
  2. JS세션에 입실했을 때 시간을 저장
  3. JS의 Date 객체를 통해 현재 시간을 구함.
  4. 입실시간 - 현재시간 결과가 60이 되면, 즉 60초가 지나면 DB와 화면을 업데이트하는 function을 만들어 실행
  5. 퇴실 시 세션에 저장된 입실시간 제거

[문제점]

  1. JS는 유니티 update함수처럼 프레임마다 실행시켜주지 않아서 실시간 처리가 불가능→setInterval 불가피
  2. 1번과 같은 이유로 페이지가 새로고침 되지 않으면, 현재 시간이 첫 호출 시 저장된 Date로 고정됨→시간 누수 발생

[수정 로직]

  1. 모든 페이지에서 지속적으로 시간 계산이 이루어져야 하므로, 모든 페이지에 따라다니는 메뉴바에 JS파일 링크(동일)
  2. JS세션에 입실 시 초를 계산하는 변수 저장
  3. setInterval 함수 1초마다 호출
  4. setInterval 함수 내 조건문을 통해 3번 변수가 60이 되면 Ajax 실행
  5. Ajax를 통해 DB에서 잔여시간을 불러온 후, 중첩 Ajax로 잔여시간-60을 계산한 값 DB에 전송
  6. 퇴실 시 세션에 저장된 입실시간 제거(동일)

 

 

[데이터 흐름도]

데이터 흐름

  1. 회원 입실 시 SeatController에서 좌석번호, 회원 입실 상태, 회원번호를 받아 마이페이지로 전달
  2. forResponsive.js에서 jsp에 전달된 세 변수 찾아서 JS세션에 저장
  3. checkDeadlineTime.js은 세션에 초 계산 변수가 있으면 setInterval 실행 매초 해당 변수+1한 값을 다시 세션에 저장
  4. 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. 회원이 입실 시 세션에 입실 상태 변수가 1이되고 조건문으로 진입
  2. 초를 담아둘 변수를 0으로 초기화(이건 세션에 Sec변수가 null일 경우에만)
  3. setInterval 함수 1초마다 실행
  4. 매초 Sec변수에 1 더한 값을 세션에 다시 저장
  5. 60초가 되면 원하는 Ajax 실행
  6. 퇴실 시 세션에 저장된 데이터 삭제

아래는 해당부분 작성한 코드다.

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를 사용하는 것이 효율적인 프로그래밍인지 모르겠다. 다른 처리 방식도 알아볼 필요성..