데이터융합 JAVA응용 SW개발자 기업 채용연계 연수과정 63일차 강의 정리

misung·2022년 6월 28일
0

Spring

jQuery

실습. jQuery02

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <script src="lib/jquery-3.6.0.min.js"></script>
</head>
<body>

    <input type="text" id="test1" readonly>
    <div id="test2">테스트2</div>
    <div id="test3">테스트3</div>

    <img src="#" alt="test" class="test4">

    <button id="btn" class="btn btn-default">클래스 조작</button>
    
    <script>

        const a1 = document.getElementById('test1').getAttribute('id');
        const a2 = $('#test1').attr('id'); //attr('속성이름')은 요소의 속성을 확인.
        console.log(a1);
        console.log(a2);

        console.log('---------------------------------------------');

        // document.getElementById('test1').setAttribute('id', 'melon');
        $('#test1').attr('id', 'melon'); //제이쿼리의 속성 변경

        //여러가지 속성들을 추가, 변경.
        $('.test4').attr({
            src: 'profile.png',
            width: 100,
            height: 100,
            alt: 'hello world!'
        });

        console.log('---------------------------------------');

        document.getElementById('test2').innerHTML = '<a href="#">kkk</a>';
        $('#test2').html('안녕하세요~'); //제이쿼리의 html 삽입 함수.

        console.log('---------------------------------------');

        document.getElementById('test3').style.background = 'red';
        $('#test3').css('background', 'blue'); //제이쿼리의 css 추가, 변경

        console.log('---------------------------------------');

        //클래스 추가
        document.getElementById('btn').classList.add('myBtn');
        $('.btn').addClass('myBtn2');

        //클래스 삭제
        document.getElementById('btn').classList.remove('myBtn');
        $('.btn').removeClass('myBtn2');

        //토글(존재한다면 삭제, 존재하지 않는다면 추가)
        $('.btn').toggleClass('btn');



    </script>

</body>
</html>

차례로 설명.

id 얻어오기

const a1 = document.getElementById('test1').getAttribute('id');
const a2 = $('#test1').attr('id'); //attr('속성이름')은 요소의 속성을 확인.
        console.log(a1);
        console.log(a2);

a1 의 경우, 바닐라 자바스크립트 기준으로 id 를 얻어오려고 할 때 사용하는 방식이고, a2 의 경우에는 JQuery를 사용한 방식이다.

a1 에서 id 를 얻어올 때를 보면, 이미 test1 이라는 id 를 알고있음에도 불구하고, getAttribute()id 를 한번 더 얻어오고 있음을 알 수 있다.

a2 의 경우 JQuery의 attr() 메서드를 사용하여 id를 가져오고 있다.

id 변경하기

// document.getElementById('test1').setAttribute('id', 'melon');
$('#test1').attr('id', 'melon'); //제이쿼리의 속성 변경

//여러가지 속성들을 추가, 변경.
$('.test4').attr({
        src: 'profile.png',
        width: 100,
        height: 100,
        alt: 'hello world!'
});

속성값을 변경할때도 attr() 메서드를 사용하는데, 한 개의 속성 말고도 여러 개의 속성값들을 바꿀 수도 있다.

html 직접 대입

document.getElementById('test2').innerHTML = '<a href="#">kkk</a>';
$('#test2').html('안녕하세요~'); //제이쿼리의 html 삽입 함수.

바닐라 자바스크립트로 test2 id를 가진 태그의 내부 html에 대해서 <a> 태그로 된 kkk 라고 써진 링크를 추가한 다음, jQuery로 다시 test2 태그를 지정하고 '안녕하세요~' 라는 멘트로 치환했다.

css 추가 및 변경

document.getElementById('test3').style.background = 'red';
$('#test3').css('background', 'blue'); //제이쿼리의 css 추가, 변경

바닐라 js의 경우 .style .background 로 접근해야 하지만,
jQuery의 경우에는 .css() 메서드로 한번에 접근한 다음, 속성과 속성값을 각각 지정해 주면 된다.

클래스 추가 및 삭제

//클래스 추가
document.getElementById('btn').classList.add('myBtn');
$('.btn').addClass('myBtn2');

//클래스 삭제
document.getElementById('btn').classList.remove('myBtn');
$('.btn').removeClass('myBtn2');

//토글(존재한다면 삭제, 존재하지 않는다면 추가)
$('.btn').toggleClass('btn');

라이브러리 사용법에 대해 배우는거니까 자잘한 설명보단 깡으로 외우는 방법밖에는 없을 듯 싶다.

실습. jQuery03

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <script src="lib/jquery-3.6.0.min.js"></script>
</head>
<body>

    <script>

        //페이지가 모두 로딩된 후에 실행하는 함수
        //페이지 내에서 1번만 사용이 가능합니다.
        /*
        window.onload = function() {
            const $btn = document.getElementById('btn');
            $btn.onclick = function() {
                alert('버튼 클릭됨!');
            }
        }
        */

        //JS의 윈도우 load 이벤트 구문을 대체하는 방법.
        /*
        $(window).on('load', function() {
            const $btn = document.getElementById('btn');
            $btn.onclick = function() {
                alert('버튼 클릭됨!');
            }
        });
        */

        //JS의 윈도우 load 이벤트를 대체하는 문법2
        //페이지에서 제이쿼리 문법을 시작할 때 주로 많이 사용합니다.
        //여러 번 사용도 가능합니다.
        $(document).ready(function() {
            $('#btn').click(function() {
                alert('제이쿼리의 load!');
            });
        });

        //제이쿼리를 시작할 때 사용하는 또다른 문법 (제일 간단함.)
        $(function() {
            //제이쿼리 문법을 블라블라~~~
        });

    </script>

    <button type="button" id="btn">등록</button>

    
</body>
</html>

.onload 는 문서당 단 한 번만 작성 가능하다고 한다.
.onload 는 페이지 로딩이 다 끝난 다음 실행이 되는 함수인데, 스크립트가 html 페이지 코드보다 앞에 있는 경우 먼저 실행이 될 여지가 있는데 (물론 좋은 배치는 아님) 그것을 방지하기 위해 사용할 때가 있다고 한다.

jQuery로 바닐라 JS의 문법을 대체하는 첫 번째 방법의 경우에는 저렇게는 안 쓰고, 두 번째 방법을 자주 쓴다고 한다.
그리고 여러 번 사용도 가능하다고 한다.

실습. jQuery04

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <script src="lib/jquery-3.6.0.min.js"></script>
</head>
<body>

    <button id="btn">이벤트 등록</button>
    <input type="text" id="tag">
    
    <script>

        //스크립트 이벤트 등록
        /*
        window.onload = function() {
            const $btn = document.getElementById('btn');
            $btn.onclick = function() {
                alert('이벤트 등록!');
            }

            const $tag = document.getElementById('tag');
            $tag.onchange = function() {
                alert('변경 이벤트 등록!');
            }
        }
        */

        //제이쿼리 이벤트 등록
        //선택자 뒤에 on이 빠진 트리거를 작성하고
        //실행할 함수를 매개변수로 전달.
        $(document).ready(function() {
            $('#btn').click(function() {
                alert('이벤트 등록!');
            });

            $('#tag').keyup(function() {
                alert('키 입력 이벤트 등록!');
            });
        });

    </script>


</body>
</html>

이것도 그냥 외워야 해서 그다지 설명할 것이 없다.

실습. jQuery05

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <script src="lib/jquery-3.6.0.min.js"></script>
</head>
<body>
    
    <ul id="test">
        <li id="tag">태그 1</li>
        <li id="tag">태그 2</li>
        <li id="tag">태그 3</li>
    </ul>

    <script>

        //li태그에 모두 동일한 이벤트를 등록하자.
        //부모태그에 이벤트를 등록하고 onclick시 전달되는
        //이벤트 객체를 이용해서 확인하는 방식.
        /*
        const $test = document.getElementById('test');
        $test.addEventListener('click', function(e) {
            if(!e.target.matches('#test > li')) {
                return;
            }
            // if(!e.target.tagName === 'li') {
            //     return;
            // }
            console.log(e.target.textContent);
        });
        */

        //on(이벤트종류, 위임할 요소, 기능)
        $('#test').on('click', 'li', function() {
            //$(this)는 이벤트가 발생한 주체(태그, 요소)를 의미.
            console.log($(this).html());
        });

    </script>

    <div id="box">

    </div>

    <script>

        $(function() {

            //on함수는 실행 순서에 영향을 받지 않고
            //이벤트가 발생하는 순간 on함수에 지정한 이벤트를 발동시킵니다.
            //이는 실행 순서와 상관없는 비동기 통신을 진행할 때 유리합니다.

            $('#box').on('click', 'a', function(e) {
                e.preventDefault();
                alert('실행되나요?');
            });

            let str = '';
            str += '<a href="#">태그1</a> <br>';
            str += '<a href="#">태그2</a> <br>';
            str += '<a href="#">태그3</a> <br>';
            $('#box').html(str);
        });

       

    </script>

</body>
</html>
//on(이벤트종류, 위임할 요소, 기능)
$('#test').on('click', 'li', function() {
//$(this)는 이벤트가 발생한 주체(태그, 요소)를 의미.
console.log($(this).html());

바닐라 js의 경우 이벤트가 발생된 주체가 원하는 것이 맞나 체크를 해야 했지만, jQuery의 경우 지정이 가능하므로 if문으로 체크를 할 필요가 없다.

현재 이 코드 상으로는, 리스트 항목을 누르는 경우 브라우저 콘솔 창에 방금 누른 리스트 요소가 전달된다.

$(function() {

            //on함수는 실행 순서에 영향을 받지 않고
            //이벤트가 발생하는 순간 on함수에 지정한 이벤트를 발동시킵니다.
            //이는 실행 순서와 상관없는 비동기 통신을 진행할 때 유리합니다.

            $('#box').on('click', 'a', function(e) {
                e.preventDefault();
                alert('실행되나요?');
            });

            let str = '';
            str += '<a href="#">태그1</a> <br>';
            str += '<a href="#">태그2</a> <br>';
            str += '<a href="#">태그3</a> <br>';
            $('#box').html(str);
        });

str에 html 코드를 넣고, .html() 함수를 통해 html 코드를 추가하고 있다. 해당 태그 링크 실행 시 .on() 함수에 의해서 onClick (jQuery에서는 on을 뺀 click) 이벤트가 감지되어 alert() 함수의 내용이 실행될 것이다.

실습. jQuery06 (연습문제)

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <style>

        .center {
            text-align: center;
        }
        .content {
            border: 1px solid #777;
        }

        .content,
        .content .left,
        .content .right {
            width: 50%;
            margin: 0 auto;
            box-sizing: border-box;
            padding: 5px;
        }

        .content .left,
        .content .right {
            float: left;
        }

        .left .inner,
        .right .inner {
            border: 1px solid #777;
            height: 300px;
        }

        img {
            width: 100%;
            height: 100%;
        }

        .clearfix::after {
            content: '';
            display: block;
            clear: both;
        }

        .memo {
            width: 100%;
            height: 250px;
            box-sizing: border-box;
            resize: none;
        }

    </style>

    <script src="lib/jquery-3.6.0.min.js"></script>

</head>
<body>

    <header>
        <div class="center">
            <h2>토글 형태 활용하기</h2>
            <button id="btn1">어둡게 보기</button>
            <button id="btn2" class="name">이름 보기</button>
            <button id="btn3">메모장 모드</button>
        </div>
    </header>

    <section>
        <div class="content clearfix">
            <div class="left">
                <div class="inner"><img src="./img/1.jpg" alt="pic"></div>
            </div>
            <div class="right">
                <div class="inner">
                    <p>자바스크립트는 객체 기반의 스크립트 프로그래밍 언어이다. 이 언어는 웹 브라우저 내에서 주로 사용하며, 다른 응용 프로그램의 내장 객체에도 접근할 수 있는 기능을 가지고 있다. 또한 Node.js와 같은 런타임 환경과 같이 서버 사이드 네트워크 프로그래밍에도 사용되고 있다.</p>
                    <a href="http://www.naver.com">네이버로 이동</a>
                </div>
            </div>
        </div>
    </section>

    <script>

        //jQuery를 활용하여 어둡게보기, 이름보기 기능을
        //구현해 주세요.
        //스타일: css(), 텍스트 -> html()

        $(document).ready(function() {
            $('#btn1').click(function() {
                $('#btn1').toggleClass('dark');
                //if($('#btn').hasClass('dark'))
                if($('#btn1').attr('class') === 'dark') {
                    //다크 모드일 경우
                    $('body').css('background', 'black');

                    // $('p, h2, a').css('color', 'white');
                    $('p, h2, a').each(function(index, item) {
                        //each는 일반적인 반복 함수입니다.
                        //앞의 선택자로 지목한 요소를 배열 형태로 받아와서
                        //index와 item으로 나누어서 함수 내부에서 사용할 수 있게 합니다.
                        console.log(item);
                        $(item).css('color', 'white');
                    });


                    $(this).html('밝게 보기');
                } else {
                    //다크 모드가 아닌 경우
                    $('body').css('background', 'white');
                    $('p, h2, a').css('color', 'black');
                    $(this).html('어둡게 보기');
                }
            });

            $('#btn2').click(function() {
                if($('#btn2').hasClass('name')) {
                    $('.inner > p').html('홍길동<br>20세<br>능력단위<br>Java, Oracle, Jsp, CSS, JavaScript');
                    $('.inner > a').css('display', 'none');
                    $(this).html('내용보기');
                    $(this).attr('class', 'cont');
                } else {
                    $('.inner > p').html('자바스크립트는 객체 기반의 스크립트 프로그래밍 언어이다. 이 언어는 웹 브라우저 내에서 주로 사용하며, 다른 응용 프로그램의 내장 객체에도 접근할 수 있는 기능을 가지고 있다. 또한 Node.js와 같은 런타임 환경과 같이 서버 사이드 네트워크 프로그래밍에도 사용되고 있다.');
                    $('.inner > a').css('display', 'block');
                    $(this).html('이름보기');
                    $(this).attr('class', 'name');
                }
            });


        });

    </script>

    
</body>
</html>

솔직히 이 문제는 작성하라고 했을 때 손을 하나도 못 댔다.
보고 나니까 대강 어떻게 돌아가는지는 알겠는데.. JS 지식이 너무 부족했던건지 jQuery를 배우자마자 이렇게 써먹으려니까 힘든건지..

SpringWebMvc 프로젝트 jQuery 추가

content.jsp

...
<form id="formObj" role="form" action="<c:url value='/board/delete' />" method="post">
         
         	<!-- 수정, 삭제의 경우에는 글 번호를 알려줄 필요가 있기 때문에 hidden으로 몰래 추가. -->
         	<!-- post 전송 방식은 url에 파라미터를 묻힐 수가 없기 때문도 있습니다. -->
         	<input type="hidden" name="boardNo" value="${article.boardNo}">
         
	          <input id="list-btn" class="btn" type="button" value="목록"
			style="background-color: #643691; margin-top: 0; height: 40px; color: white; border: 0px solid #388E3C; opacity: 0.8">&nbsp;&nbsp;
	          
	          <input id="mod-btn" class="btn" type="button" value="수정"
			style="background-color: orange; margin-top: 0; height: 40px; color: white; border: 0px solid #388E3C; opacity: 0.8">&nbsp;&nbsp;
	          
	          <input class="btn" type="submit" value="삭제" onclick="return confirm('정말로 삭제하시겠습니까?')"
			style="background-color: red; margin-top: 0; height: 40px; color: white; border: 0px solid #388E3C; opacity: 0.8">&nbsp;&nbsp;
       
       </form>
...

<form> 과 각 <input> 태그들에 id 를 부여해 주었음.
그리고 hidden 타입으로 boardNo 를 추가해 두었는데,
써 있는 대로 post 방식에서는 url에 파라미터를 묻힐 수 없기 때문에 글 번호를 저런 식으로 추가해 둔 것이다.

그리고 저 폼에 대한 처리를 하는 걸 바닐라 JS로 한다고 하면,

//목록 버튼 클릭 이벤트 처리.
	const $listBtn = document.getElementById('list-btn');
	$listBtn.onclick = function() {
		console.log('목록 버튼이 클릭됨!');
		location.href='/board/list';
	}
	
	//수정 버튼 클릭 이벤트 처리
	const $modBtn = document.getElementById('mod-btn');
	const $formElement = document.getElementById('formObj');
	
	$modBtn.onclick = function() {
		$formElement.setAttribute('action', '/board/modify');
		$formElement.setAttribute('method', 'get');
		$formElement.submit();
	}

이런 식이 될 것이다.
$기호가 왜 붙나 긴가민가했는데, 찾아보니 document.getElementById() 를 줄여서 사용했다는 의미라고 보면 될 것 같다.

매번 저 함수를 부를 수 없으니, 매크로로 지정하는 것과 비슷하게 했다고 보면 될 것 같고, 아래의 폼 엘리먼트 컨트롤 중 .submit() 함수는 무엇인가 했더니, 전에 버튼 만들고 submit 속성을 줘서 그걸로 폼 내용을 전송했었는데,

지금은 그 submit 버튼이 없으므로 .submit() 함수를 호출해서 $modBtn 즉, 수정 버튼이 클릭되면 아래의 JS 코드를 타고 actionmethod 가 어트리뷰트로 전달되도록 submit 을 해 주고 있는 것이었다.

하지만 우리는 jQuery를 사용한 방법을 배우고 있으므로, 아래와 같은 방법이 될 것이다.

//제이쿼리 시작
	$(document).ready(function() {
		//목록 버튼 클릭 이벤트 처리.
		$('#list-btn').click(function() {
			console.log('목록 버튼이 클릭됨!');
			location.href='/board/list?page=${p.page}&cpp=${p.cpp}';
		});
		
		//수정 버튼 클릭 이벤트 처리
		$('#mod-btn').click(function() {
			$('#formObj').attr({
				'action': '/board/modify',
				'method': 'get'
			});
			$('#formObj').submit();
		});
		
	});
  1. .ready() 문서가 로드되면
  2. #list_btn 해당 id를 가진 태그에 .click() 이벤트가 발생하는 경우
  3. '목록 버튼이 클릭됨' 이라는 멘트를 출력하고 정해둔 페이지로 보낸다.

[BoardController] 게시글 수정 화면 요청

방금 GET으로 매핑된 글 수정 요청 페이지로의 이동을 만들었으니, 이걸 처리해 줄 부분을 컨트롤러단에서 만들어야 한다.

...
//게시글 수정 화면 요청
	@GetMapping("/modify")
	public void modify(int boardNo, Model model) {
		model.addAttribute("article", service.getArticle(boardNo));
	}
...

게시글 수정 화면 요청 처리 부분에서는 model 객체에 게시글을 하나 얻어와서 "article" 이라는 이름으로 어트리뷰트를 붙여주었다.

[modify.jsp] 게시글 수정 페이지 수정

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<jsp:include page="../include/header.jsp" />
<style>
header.masthead {
	
	display: none;
}	
</style>
<br/><br/>
<div class="container">

<div class="row">
  <div class="col-lg-12">
    <div class="card">
      <div class="card-header text-white" style="background-color: #643691;">${article.boardNo}번 게시물 수정</div>
      <div class="card-body">

        <form role="form" action="#" method="post">
          <div class="form-group">
            <label>작성자</label>
            <input type="text" class="form-control" name='writer' value="${article.writer}">
          </div>
          
          <div class="form-group">
            <label>제목</label>
            <input type="text" class="form-control" name='title' value="${article.title}">
          </div>

          <div class="form-group">
            <label>내용</label>
            <textarea class="form-control" rows="5" name='content'>${article.content}</textarea>
          </div>

          
          <input class="btn" type="submit" value="수정" style="background-color: orange; margin-top: 0; height: 40px; color: white; border: 0px solid #388E3C; opacity: 0.8"/>
          <a class="btn" href="#"
		style="background-color: #643691; margin-top: 0; height: 40px; color: white; border: 0px solid #388E3C; opacity: 0.8">목록</a>&nbsp;&nbsp;
          
        </form>



      </div>
    </div>
  </div>
</div>
</div>
<jsp:include page="../include/footer.jsp" />

value 부분들이 전부 value=#### 로 되어 있었지만, ${article.가져올 멤버변수} 로 전부 수정해 주었다.

[BoardController] 게시글 수정 처리 요청 (POST) 메서드 수정

...
//게시글 수정 처리 요청
	@PostMapping("/modify")
	public String modify(BoardVO article) {
		System.out.println("/board/modify: POST");
		service.update(article);
		
		return "redirect:/board/content/" + article.getBoardNo();
	}
...

원래 return 부분이 boardNo?= 하고 article.getBoardNo(); 로 되어있었지만, 위와 같은 "redirect:/board/content/" 로 수정하였다.

분명 저번에 boardNo?= 로 안 해도 되는 이유가 있었고 그렇게 하도록 만들었었는데, 왜 그렇게 해도 되는지 까먹었어서 62일차 강의 내용을 참고했다.

//게시글 상세보기 요청
	@GetMapping("/content/{boardNo}")
	//@PathVariable은 URL 경로에 변수를 포함시켜 주는 방식
	//null이나 공백이 들어갈 수 있는 파라미터라면 적용하지 않는 것을 추천.
	//파라미터 값에 .이 포함되어 있다면 .뒤의 값은 잘린다는 걸 알아두세요.
	//{}안에 변수명을 지어주시고, @PathVariable 괄호 안에 영역을 지목해서
	//값을 받아옵니다.
	public String content(@PathVariable int boardNo, Model model, 
							@ModelAttribute("p") PageVO paging) {
		System.out.println("/board/content: GET");
		System.out.println("요청된 글 번호: " + boardNo);
		model.addAttribute("article", service.getArticle(boardNo));
		return "board/content";
	}

바로 얘 덕분이었다. 이렇게 하면 boardNo?= 를 작성할 필요도 없고 URL에 노출되지 않는다.

[list.jsp] jQuery 추가

...
<script>

	const msg = '${msg}';
	if(msg === 'delSuccess') {
		alert('삭제가 완료되었습니다.');
	} else if(msg === 'regSuccess') {
		alert('등록이 완료되었습니다.');
	}
	
	//start jQuery
	$(function() {
		
		//한 페이지당 보여줄 게시물 개수가 변동하는 이벤트 처리
		$('#count-per-page .btn-cpp').click(function() {
			const count = $(this).val();
			location.href='/board/list?page=1&cpp=' + count;
		});
		
	}); // end jQuery
	
</script>

'${msg}' 의 경우에는 내가 JS때 제대로 강의를 수강하지 못 해서인지는 모르겠지만, 아마 페이지에 넘어온 어트리뷰트 중 msg 라는 이름의 녀석이 있으면 끌어다 쓰는 것 같다.

[BoardController] 게시글 DB 등록 요청 메서드 수정

...
//게시글 DB 등록 요청
	@PostMapping("/write")
	public String write(BoardVO article, RedirectAttributes ra) {
		System.out.println("/board/write: POST");
		service.insert(article);
		ra.addFlashAttribute("msg", "regSuccess");
		
		return "redirect:/board/list";
	}
...

RedirectAttributes ra 를 매개변수로 받아 addFlashAttribute() 메서드를 사용하여 메시지를 보냈다.

이렇게 메시지를 보냄으로써 방금 위에서 만든 jQuery로 정의한 메서드의 알림창이 호출될 것이다.

지금 화면이 구동이 안 돼서 (강사님 해당 강의일자의 파일을 받아왔는데, 페이징 처리가 다 안 끝난 채로 파일을 받아버려서..) 보여줄 수는 없지만, 페이징 처리가 안 되어 있어서 한 화면에 게시글이 무한 스크롤로 쭉 300개 넘게 노출되고 있다.

그래서 이제부터 페이징 처리를 해야 한다.

[PageAlgorithmTest] 테스트 클래스 작성 및 [BoardMapper.xml] 에 게시물 수 조회하는 메서드 선언하기

테스트용 클래스를 작성한다. 디렉토리는 src/test/java 안에 만들면 된다.

페이징 알고리즘을 만들기 전에, BoardMapper.xml에 다음과 같은 코드를 추가해 둔다.

...
<select id="countArticles" resultType="int">
		SELECT COUNT(*)
		FROM mvc_board
</select>
...

countArticles() 라는 메서드를 만들고, 결과값은 int로 반환하도록 한다. (resultType)

package com.spring.mvc.board;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.spring.mvc.board.commons.PageVO;
import com.spring.mvc.board.repository.IBoardMapper;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/mvc-config.xml"})
public class PageAlgorithmTest {
	
	/*
	 *** 페이징 알고리즘 만들기 ***
	 
	 # 1. 총 게시물의 수를 조회해야 합니다.
	 - 총 게시물 수는 DB로부터 수를 조회하는 SQL문 작성.
	 
	 # 2. 사용자가 현재 위치한 페이지를 기준으로
	  끝 페이지 번호를 계산하는 로직 작성.
	 
	 - 만약 현재 사용자가 보고 있는 페이지가 3페이지고,
	  한 화면에 보여줄 페이지 버튼이 10개씩이면? -> 10페이지.
	  공식: Math.ceil(현재 위치한 페이지 번호 / 페이지 버튼 개수) * 페이지 버튼 개수
	 
	 # 3. 시작페이지 번호 구하기
	 공식: (끝 페이지 번호 - 페이지 버튼 개수) + 1
	 
	 # 4. 이전 버튼 활성 여부
	 공식: 시작페이지가 1이면 비활성, 나머지는 모두 활성.
	 
	 # 5. 다음 버튼 활성 여부
	 공식: 보정 전 끝 페이지 번호 * 한 페이지에 들어갈 게시물의 수 >= 총 게시물 수
	     -> 비활성.
	     
	 # 6. 끝 페이지 값 보정
	 - 다음 버튼이 비활성화 되었을 때 사용. (필요없는 버튼을 제거하는 용도로)
	 - 공식: Math.ceil(총 게시물 수 / 한 페이지에 보여줄 게시물 수)
	 */
	
	@Autowired
	private IBoardMapper mapper;
	
	@Test
	public void pagingAlgorithmTest() {
		System.out.println("# 총 게시물 수: " + mapper.countArticles());
		System.out.println("-------------------------------------");
		
		//끝 페이지 번호 계산 테스트
		PageVO vo = new PageVO();
		vo.setPage(31);
		int buttonNum = 10; //한 화면에 보여질 버튼 개수
		
		int endPage = (int) (Math.ceil(vo.getPage() / (double) buttonNum) * buttonNum);
		System.out.println("끝 페이지 번호: " + endPage + "번");
		
		//시작 페이지 번호
		int beginPage = (endPage - buttonNum) + 1;
		System.out.println("시작 페이지 번호: " + beginPage + "번");
		
		//이전버튼 활성, 비활성 여부
		boolean isPrev = (beginPage == 1) ? false : true;
		
		//다음 버튼 활성, 비활성 여부
		boolean isNext = (endPage * vo.getCpp()) >= mapper.countArticles() ? false : true;
		
		System.out.println("이전 버튼 활성화 여부: " + isPrev);
		System.out.println("다음 버튼 활성화 여부: " + isNext);
		
		//끝 페이지 보정
		if(!isNext) {
			endPage = (int) Math.ceil(mapper.countArticles() / (double) vo.getCpp());
		}
		
		System.out.println("보정 후 끝 페이지 번호: " + endPage + "번");
		
		
	}
	

}

전에 분명 페이징 알고리즘을 백준같은 곳에서 풀었던 기억이 있는데, 지금은 거의 기억이 다 날아갔다..

절차를 다시 한번 차근차근 분석하자면..

  1. 게시글 수를 조회한다 (아마 현재는 323개)
  2. 사용자가 현재 위치한 페이지를 설정 (31페이지)
  3. 끝 페이지 = 올림처리(현재 페이지 / 버튼갯수) * 버튼갯수

이 부분의 경우에는..
올림 (31 / (double) 10) * 10; 이 되겠는데,
3.1에다가 올림처리해서 4, 거기에 10을 곱하니 40이 끝 페이지 번호가 된다.

  1. 시작 페이지 = (끝 페이지 - 버튼 개수) + 1 이니까,
    ? = (40 - 10) + 1 이 된다.
    그러면 31페이지가 나온다.

  2. 이전 버튼 활성 여부의 경우에는 현재 시작 페이지가 1인 경우 이전 버튼을 활성화해도 의미가 없으니, 시작 페이지가 1인 경우만 아니면 항시 표시될 것이다.

  3. 다음 버튼의 경우에는 이렇게 결정된다.
    다음 페이지 버튼 여부 = (끝 페이지 * 한 화면당 보여질 게시물 개수()) >= 총 게시물 개수

끝 페이지는 아까 계산한 40, 한 화면당 보여질 게시물 개수는 vo.getCpp() 메서드를 사용중이다.

PageVo의 정의는 다음과 같다.

package com.spring.mvc.board.commons;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class PageVO {
	
	private int page; //사용자가 선택한 페이지 번호
	private int cpp; //사용자가 선택한 한 화면에 보여질 게시물 개수.
	
	public PageVO() {
		this.page = 1;
		this.cpp = 10;
	}

}

한 페이지에 보일 게시물의 개수를 10개로 정해두고 있다.

그러면 ? = (40 * 10) >= 총 게시물(323) 가 되는데, 지금 총 게시물보다 큰 수를 가지므로 다음 버튼의 활성화는 필요가 없어진다. (버튼이 페이지나 게시물보다 많아져봤자, 없는 게시물들을 표시할 수는 없으니)

하지만 지금 끝 페이지가 40으로 정해져 있을 텐데, 실제 게시물은 323개이므로 페이지 보정을 해야 한다.

  1. 끝 페이지 보정하기
//끝 페이지 보정
		if(!isNext) {
			endPage = (int) Math.ceil(mapper.countArticles() / (double) vo.getCpp());
		}

아까 위에서 isNext는 400>=323?false:true 였으므로 조건 자체는 true이고, 삼항 연산자에서 true는 앞선 결과를 반환하므로 결과적으로는 isNext = false 가 될 것이다.

그러면 끝 페이지 보정 조건을 탈 수 있고, 조건대로라면

끝 페이지 = (int)올림처리(323 / (double)10); 이 된다.

32.3을 올림처리하면 33이 되고, 결국 33페이지가 마지막 페이지 번호가 된다. (323개의 게시물이니 33페이지에서 3개의 게시물이 최종적으로 표시될 것이다.)

휴~ 차례차례 분석하니 어렵지는 않았지만 직접 만들라고 하면 아직은 한 번에 만들기는 어려울 것 같다.

[PageCreator] 생성

com.spring.mvc.board.commons 패키지 내에 생성

package com.spring.mvc.board.commons;

import lombok.Getter;
import lombok.ToString;
import lombok.Setter;

//페이지 알고리즘에 의해 이전, 다음 버튼 및 페이지 버튼 개수 및 번호를 관장할 객체.
@Getter
@Setter
@ToString
public class PageCreator {

	private PageVO paging; //사용자가 선택한 페이지 정보를 갖고있는 객체.
	private int articleTotalCount; //총 게시물의 수
	private int beginPage; //시작 페이지 번호
	private int endPage; //끝 페이지 번호
	private boolean prev; //이전 버튼
	private boolean next; //다음 버튼
	
	//한 화면에 보여질 페이지 버튼 개수
	private final int displayPageNum = 10;
	
	//페이징 알고리즘을 수행할 메서드 선언
	private void calcDataOfPage() {
		endPage = (int) Math.ceil(paging.getPage() / (double) displayPageNum) * displayPageNum;
		
		beginPage = (endPage - displayPageNum) + 1;
		
		prev = (beginPage == 1) ? false : true;
		
		next = (endPage * paging.getCpp()) >= articleTotalCount ? false : true;
		
		if(!next) {
			endPage = (int) Math.ceil(articleTotalCount / (double) paging.getCpp()); 
		}
	}
	
	public void setArticleTotalCount(int articleTotalCount) {
		this.articleTotalCount = articleTotalCount;
		calcDataOfPage();
	}
	
	
	
}

처음엔 무엇인고 했더니, 아까 테스트 디렉토리에다 만들었던 녀석을 정식으로 만들어 준 것이다.

[BoardMapper.xml] getArticleList() 메서드 수정

...
<!-- 쿼리문을 작성할 때 '<', '>', '&'등의 기호를 사용해야 하는 경우가 생기는데, 
		xml 파일에서 이를 그냥 사용할 경우, 태그로 인식되는 경우가 종종 있습니다.
		이럴 경우에는 해당 기호가 태그 문법이 아닌 실제 쿼리에 필요한 문자라고 인식시켜야
		합니다. 이 때 사용하는 문법이 <![CDATA[... 쿼리 ...]]> 입니다. 
		CDATA 안에 쿼리를 작성하면 쿼리 내용의 괄호나 특수문자를 
		마크업 언어로 인식하지 않고 문자열로 인식하게 됩니다.
		< (&lt;)   > (&gt;) -->	
	<select id="getArticleList" resultMap="BoardMap">
		SELECT * FROM
			(
			SELECT ROWNUM AS rn, tbl.* FROM	
				(
				SELECT * FROM mvc_board
				ORDER BY board_no DESC
				) tbl
			)
		<![CDATA[
		WHERE rn > (#{page}-1) * #{cpp}
		AND rn <= #{page} * #{cpp}
		]]>
	</select>
...

일단 완성된 코드는 다음과 같이 표시되어있다.

서브쿼리문을 사용했고.. 서브쿼리는

  1. 게시물 전체를 내림차순으로 얻어옴

  2. 각 레코드에 대해 rownum 붙임

  3. 그리고 WHERE 절로 조건을 붙여서 필요한 부분을 떼 와야 하는데, 조건식에 <, > 등등의 기호가 들어가므로 <![CDATA[ 쿼리... ]]> 를 붙여줘야 한다.

  4. WHERE절의 내용은, 행번호 > (페이지-1) * 한 페이지당 보일 게시물수 AND 행번호 <= 페이지 * 한 페이지당 보일 게시물수 이다.

#{page} 는 대체 어디 있나 찾아보니 PageVO에 멤버 변수로서 등록되어 있었고, 사용자가 선택한 현재 페이지를 의미한다고 한다.

현재 페이지가 1로 설정되어 있었으니

행번호 > 0 AND 행번호 <= 10 이 될 것이다.

현재 1페이지를 골랐으면, 1~10번의 게시물을 땡겨오게 될 것이다. 유후!

그리고 이렇게만 만들어 두면 안 되고, 인터페이스에도 선언을 해 두어야 한다.

[IBoardMapper] getArticleList 수정하기

...
//게시글 전체 조회 기능(페이징 전)
	//List<BoardVO> getArticleList();
	
	//페이징 처리를 포함한 게시글 목록 조회 기능
	List<BoardVO> getArticleList(PageVO paging);
...

이제 페이징 처리를 하고 있으니, getArticleList 메서드에 매개 변수로 PageVO 를 받도록 한다.

그리고 이제 Service 클래스도 수정해야 한다.

[IBoardService], [BoardService] getArticleList 메서드 수정하기

IBoardService.java

...
//게시글 전체 조회 기능(페이징 전)
//	List<BoardVO> getArticleList();
	
	//페이징 처리를 포함한 게시글 목록 조회 기능
	List<BoardVO> getArticleList(PageVO paging);
...

BoardService.java

...
@Override
	public List<BoardVO> getArticleList(PageVO paging) {
		return mapper.getArticleList(paging);
	}
...

mapper를 수정했으니 IBoardService와 BoardService 둘 다를 수정해 준다.

[BoardController] 게시글 요청 메서드 수정

이제 이전처럼 게시글을 1번부터 최종 번호의 게시물까지 한번에 부르지 않고, 페이징 처리를 하여 한 페이지에 보여줄 게시물만 불러오도록 처리를 해야 한다.

...
//페이징 처리 이후 게시글 목록 불러오기 요청
	@GetMapping("/list")
	public void list(PageVO paging, Model model) {
		System.out.println("/board/list: GET");
		System.out.println("페이지 번호: " + paging.getPage());
		
		List<BoardVO> list = service.getArticleList(paging);
		System.out.println("페이징 처리 후 게시물의 수: " + list.size());
		
		PageCreator pc = new PageCreator();
		pc.setPaging(paging);
		pc.setArticleTotalCount(service.countArticles());
		
		System.out.println(pc);
		
		model.addAttribute("articles", list);
		model.addAttribute("pc", pc);
		
	}
...
  1. 보여줄 게시글 리스트 = 서비스 클래스에 .getArticleList(페이징 객체) 로 요청한다.

서비스 클래스의 해당 메서드는

return mapper.getArticleList(paging);

이렇게 리턴을 하고, 매퍼는 아까 봤듯이

<select id="getArticleList" resultMap="BoardMap">
		SELECT * FROM
			(
			SELECT ROWNUM AS rn, tbl.* FROM	
				(
				SELECT * FROM mvc_board
				ORDER BY board_no DESC
				) tbl
			)
		<![CDATA[
		WHERE rn > (#{page}-1) * #{cpp}
		AND rn <= #{page} * #{cpp}
		]]>
	</select>

전체 게시글을 내림차순으로 정렬 후 번호를 붙여둔 다음, 현재 페이지에서 보여줄 부분의 게시글만을 떼오게 한다.

  1. 페이지 크리에이터 객체를 하나 선언하고,
    setPaging(페이징 객체) 를 호출한다.

잠깐 무슨 뜻인가 했는데, lombok에 의해 자동으로 만들어진 setter 메서드로, Paging 객체를 PageCreator 메서드에 할당하고 있는 거였다.

  1. 여튼, 그 다음에 setArticleTotalCount() 메서드를 호출하는데, 매개 변수로 service.countArticles() 를 주고 있다.

서비스쪽 메서드는

@Override
	public int countArticles() {
		return mapper.countArticles();
	}

매퍼의 countArticles() 를 호출하고,

<select id="countArticles" resultType="int">
		SELECT COUNT(*)
		FROM mvc_board
</select>

매퍼측 메서드는 단순히 게시글의 수를 반환해준다.

그러면 게시글 수를 받아온 후 PageCreator의 setArticleTotalCount() 를 호출하게 된다.

  1. PageCreator 쪽을 보면,
...
//페이징 알고리즘을 수행할 메서드 선언
	private void calcDataOfPage() {
		endPage = (int) Math.ceil(paging.getPage() / (double) displayPageNum) * displayPageNum;
		
		beginPage = (endPage - displayPageNum) + 1;
		
		prev = (beginPage == 1) ? false : true;
		
		next = (endPage * paging.getCpp()) >= articleTotalCount ? false : true;
		
		if(!next) {
			endPage = (int) Math.ceil(articleTotalCount / (double) paging.getCpp()); 
		}
	}
	
	public void setArticleTotalCount(int articleTotalCount) {
		this.articleTotalCount = articleTotalCount;
		calcDataOfPage();
	}
...

아까 테스트 클래스에서 작성했었던 페이징 알고리즘이 적용된 calcDataOfPage() 메서드를 setArticleTotalCount() 메서드에서 호출하고 있음을 알 수 있다.

PageCreator 내의 시작/끝 페이지 계산용 멤버변수와, 이전/다음 페이지로의 버튼 활성화 여부를 계산하여 멤버 변수의 값을 갱신시키게 된다.

model.addAttribute("articles", list);
model.addAttribute("pc", pc);

마지막으로는 모델에 어트리뷰트를 추가하는데, 아까 얻어온 현재 페이지에 보여줄 게시물 리스트를 넘기고, 나머지 하나는 페이지 크리에이터 객체를 넘기게 된다.

자, list.jsp 로의 매핑 처리를 한 메서드에 대해 살펴봤으니 list.jsp를 살펴 볼 차례다.

[list.jsp] 수정

...
<!-- 페이징 처리 부분  -->
					<ul class="pagination justify-content-center">
						<!-- 이전 버튼 -->
						<c:if test="${pc.prev}">
	                       	<li class="page-item">
								<a class="page-link" href="<c:url value='/board/list?page=${pc.beginPage-1}&cpp=${pc.paging.cpp}' />" 
								style="background-color: #643691; margin-top: 0; height: 40px; color: white; border: 0px solid #f78f24; opacity: 0.8">이전</a>
							</li>
						</c:if>
						
						<!-- 페이지 버튼 -->
						<c:forEach var="pageNum" begin="${pc.beginPage}" end="${pc.endPage}">
							<li class="page-item">
							   <a href="<c:url value='/board/list?page=${pageNum}&cpp=${pc.paging.cpp}' />" class="page-link ${pc.paging.page == pageNum ? 'page-active' : ''}" style="margin-top: 0; height: 40px; color: pink; border: 1px solid #643691;">${pageNum}</a>
							</li>
						</c:forEach>
					   
					    <!-- 다음 버튼 -->
					    <c:if test="${pc.next}">
						    <li class="page-item">
						      <a class="page-link" href="<c:url value='/board/list?page=${pc.endPage+1}&cpp=${pc.paging.cpp}' />" 
						      style="background-color: #643691; margin-top: 0; height: 40px; color: white; border: 0px solid #f78f24; opacity: 0.8">다음</a>
						    </li>
					    </c:if>
				    </ul>
					<!-- 페이징 처리 끝 -->
...

페이징 처리가 안 되어 있던 부분에 대해서 페이징 처리를 해 준다.

이전이나 다음 버튼의 경우 jstl을 활용하여 <c:if test="${pc.prev 혹은 pc.next}"> 로 판단하여 보일지 말지 여부를 결정하면 되고, 보여야 하는 경우

prev의 경우엔 page=${pc.beginPage-1}&cpp=${pc.paging.cpp}

next의 경우엔 page=${pc.endPage+1}&cpp=${pc.paging.cpp} 이렇게 계산한다.

cpp야 PageCreator 객체의 멤버변수인 CountPerPage(cpp)를 주면 되고, prev는 시작 페이지를 1페이지 줄이고 next는 끝 페이지를 하나 늘린다.
(그런데 보통 '다음' 버튼은 한번에 10페이지씩 건너가는거라 이게 그건지 아닌건지 모르겠다.)

가운데의 페이지 버튼은

<!-- 페이지 버튼 -->
<c:forEach var="pageNum" begin="${pc.beginPage}" end="${pc.endPage}">
	<li class="page-item">
		<a href="<c:url value='/board/list?page=${pageNum}&cpp=${pc.paging.cpp}' />" class="page-link ${pc.paging.page == pageNum ? 'page-active' : ''}" style="margin-top: 0; height: 40px; color: pink; border: 1px solid #643691;">${pageNum}</a>
	</li>
</c:forEach>

PageCreator 객체로부터 시작 페이지와 끝 페이지에 대한 정보를 얻어와서 pageNum 변수로 순환하고,
<li> 들은 <a> 태그를 가지고 있으며, 각각 list의 page로 연결될 수 있게 되어 있다.

그 중, 현재 사용자가 위치한 페이지와 일치하는 페이지가 있으면 그 요소의 스타일을 다르게 주고 있다.

.page-active {
	background: #643691;
}

삼항 연산자에 의해 결정되어서 나오는 스타일은 이러하다.

그리고 코드 중 다른 부분을 보면

...
<span id="count-per-page" style="float: right;">
	<input class="btn btn-cpp" type="button" value="10">  
	<input class="btn btn-cpp" type="button" value="20">   
	<input class="btn btn-cpp" type="button" value="30">
</span>
...

이런 부분이 있다.
한 페이지당 보고싶은 게시물의 수를 저 버튼을 눌러서 정할 수 있는데, 저것에 대한 처리를 해 줘야 한다.

//start jQuery
	$(function() {
		
		//한 페이지당 보여줄 게시물 개수가 변동하는 이벤트 처리
		$('#count-per-page .btn-cpp').click(function() {
			const count = $(this).val();
			location.href='/board/list?page=1&cpp=' + count;
		});
		
	}); // end jQuery

jQuery로 처리된 해당 버튼에 대한 로직이다.

#count-per-page 라는 id 내의 .btn-cpp 라는 클래스에 대해서 (on)click 메서드가 호출(클릭) 된 경우, 그것의 값 ($(this).val()) 을 가져와서, location.href= 에 넘기는 값 중 cpp 값을 방금 얻어온 value인 count 로 넘기고 있다.

예로, 10페이지씩 보기를 원해서 [10] 버튼을 눌렀으면 count는 10이 될 것이고, 20을 누르면 count는 20이 된다.

게시물 보다가 목록 보기 누를 시 1페이지로 이동되는 문제 수정

지금까지의 상태에서는 게시물을 보다가 '목록으로' 버튼을 누르는 경우 아까 보고 있던 페이지가 몇 페이지냐와는 상관없이 1페이지로 이동되게 된다.

따라서 이 문제를 일련의 흐름에 따라 수정해야 한다.

<!-- 게시물이 들어갈 공간 -->
<c:forEach var="b" items="${articles}">
	<tr style="color: #643691;">
		<td>${b.boardNo}</td>
		<td>${b.writer}</td>
		<td>
		<a style="margin-top: 0; height: 40px; color: orange;" href="<c:url value='/board/content/${b.boardNo}?page=${pc.paging.page}&cpp=${pc.paging.cpp}' />">
										${b.title}
		</a>
		</td>

		<td>${b.regDate}</td>
		<td>${b.viewCnt}</td>
	</tr>
</c:forEach>

다른건 그렇다 치는데 여기가 중요하다.

href="<c:url value='/board/content/${b.boardNo}?page=${pc.paging.page}&cpp=${pc.paging.cpp}

원래는 어떻게 되어있었냐면, 아래와 같다.

<c:url value='/board/content/${b.boardNo}' />

이렇게 되어있으면 발생하는 문제가, URL에 게시물 번호가 묻어서 들어가긴 하지만, 지금 내가 몇 페이지를 보고 있었는지와 한 페이지에 게시물을 몇개씩 보고있었는지 전달되지가 않는다.

따라서 위에 개선한 방식으로 적용하게 된 것이고, 이제 이것을 처리할 수 있게 일련의 메서드들도 수정해 주어야 한다.

[BoardController] 수정
...
public String content(@PathVariable int boardNo, Model model, 
							@ModelAttribute("p") PageVO paging) {
		System.out.println("/board/content: GET");
		System.out.println("요청된 글 번호: " + boardNo);
		model.addAttribute("article", service.getArticle(boardNo));
		return "board/content";
	}
...

@ModelAttribute("p") PageVO paging 매개변수로 받는 이 부분을 추가했다.

페이징 객체를 "p" 로 포장해두었고, 이제 이걸 content.jsp에서 다시 처리해 주어야 한다.

[content.jsp] 수정
//제이쿼리 시작
	$(document).ready(function() {
		//목록 버튼 클릭 이벤트 처리.
		$('#list-btn').click(function() {
			console.log('목록 버튼이 클릭됨!');
			location.href='/board/list?page=${p.page}&cpp=${p.cpp}';
		});
		
		//수정 버튼 클릭 이벤트 처리
		$('#mod-btn').click(function() {
			$('#formObj').attr({
				'action': '/board/modify',
				'method': 'get'
			});
			$('#formObj').submit();
		});
		
	});

여기서 추가된 부분이, location.href='/board/list?page=${p.page}&cpp=${p.cpp}'; 이 부분이었는데,

바로 위에서 우리는 컨트롤러에서 페이징 객체를 모델에 담아두었고, 이는 content.jsp로 이동되면서 아직 페이징 객체가 남아있게 된다.

p 라는 이름으로 우리는 객체를 담아두었었으므로, p.page, p.cpp 로 보던 페이지와 한 페이지당 표시할 게시물 정보를 URL에 담아주면 된다.

이것으로 오늘 강의는 종료되었다.

0개의 댓글