ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Node.js & MySQL] 도전과제: 검색/페이징/정렬
    생활코딩/WEBn 2021. 1. 24. 11:33

    생활코딩 Node.js - MySQL 마무리에서 제시된 도전과제를 구현하였다.

     

    1. 검색: form get 방식으로 요청을 받고, db단에서 SQL구문 SELECT * FROM 테이블 WHERE 컬럼 LIKE "%키워드%"로 데이터를 찾는다. 찾은 데이터를 html 데이터로 구성해서 웹페이지로 띄운다.

     

    2. 페이징: 한번에 보여줄 페이지를 설정하고, 페이지목록을 만들어서 넘길 수 있도록 한다. 각 페이지는 SELECT * FROM topic LIMIT 0 OFFSET 20; 구문을 응용해서 구성하고, 페이지 목록은 SELECT COUNT(*) FROM topic; 구문으로 전체 글 개수를 확인한 다음 나눠서 구한다.

     

    3. 정렬: 타이틀-오름차순/타이틀-내림차순/작성일순/작성일역순 4가지 옵션으로 검색이 가능하도록 구현한다.

    SELECT * FROM topic ORDER BY id DESC처럼 ORDER 구문을 사용하면 된다.


    1. 검색

    Form 만들기

    일단 검색어를 입력할 폼을 만들어서 메뉴바 중간에 추가한다. 만드는 김에 스타일도 넣어보았다.

    <style>
    .search {
      display: grid;
      padding-bottom: 0px;
    }
    
    .search-bar {
      border: 1px solid #009900;
      float: left;
      font-size: 13px;
      margin: 10px 0 0 25px;
      padding: 5px;
      width: 140px;
    }
    
    .search-button {
      background: #009900;
      border: 1px solid #009900;
      border-left: none; /* Prevent double borders */
      color: white;
      cursor: pointer;	
      float: left;
      font-size: 13px;
      margin: 10px 0 25px 0;
      width: 60px;
      padding: 5px;
    }
    
    .search-button:hover {
    	  background: #006600;
    }
    
    .search-form {
      height: 45px;
    }
    </style>
    
    <form action="search" method="get" class="search-form">
    	<input type="text" class="search-bar" name="query" placeholder="검색어를 입력하세요.">
    	<input type="submit" class="search-button" value="Search" >
    </form>

    이제 search 페이지로 가서 쿼리가 잘 들어오는지 확인해본다.

    get 방식이기 때문에 URL에서 확인할 수 있다. 홈/search?query=검색어 형태로 들어오는 것을 확인했다.

    쿼리값을 가져올 때는 request 변수의 url을 파싱하면 된다.

    var query = sanitize(url.parse(request.url, true).query.query);

     

    이제 데이터베이스에서 해당 검색어를 가져오자. 검색어를 포함하고 있는 타이틀을 가져오려면 와일드카드를 쓰면 된다.

    SELECT title, description FROM TOPIC WHERE title LIKE '%검색어%';

    노드에서 이스케이프 처리를 하려면 뒤의 인자 부분에 '%' + query + '%'처럼 써주면 된다.

    '%?%'를 시도해봤지만 되지 않았다. 아마도 텍스트로 이스케이핑되어서 '%'검색어'%' 처럼 처리되기 때문인 듯하다.

    db.query(`SELECT id, title, description FROM topic WHERE title LIKE ?`, '%' + query + '%', function(err, result){
        console.log(result);
    };
    
    
    /*
    [
      RowDataPacket {
        id: 8,
        title: 'Amazon',
        description: 'Amazon.com, Inc. (/mzn/), is an American multinational technology company b
    ased in Seattle, Washington. Amazon focuses on e-commerce, cloud computing, digital streaming, and artificial intelligence.'
      }
    ]
    */

    데이터를 확보했으니 보여줄 페이지를 만든다.

    타이틀, description은 앞의 100글자까지만 보여주고, 타이틀에는 링크를 연결한다.

    description은 바로 아래에서 100글자까지만 인덱싱한다. 다만 데이터가 100글자보다 짧은 경우가 있을 수 있으니, 길이를 비교해서 ... 를 선택적으로 추가해주었다.

    module.exports의 메소드로 구성했다.

    	searchResult: function(result, callback){
    		var view = ``
    		for(var i in result){
    			if (result[i].description.length === 100){
    				var description = result[i].description + ' ...';
    			} else {
    				var description = result[i].description;
    			};
    			view += `
    			<div class="search-result">
    				<h3 class="search-title"><a href="/?id=${result[i].id}">${result[i].title}</a></h3>
    				<p class="search-desc"></p>${description}</p>
    			</div>`;
    		}
    		callback(view);
    	}

     

     

    description을 인덱싱한다. MySQL에서 데이터의 특정 부분만을 가져오고 싶다면 SUBSTRING 함수를 이용하면 된다.

    SUBSTRING(컬럼명, 시작 인덱스, 끝 인덱스)

    *스트링 인덱스는 0이 아니라 1부터 시작한다. 0을 입력하면 아무것도 안 나온다.

    mysql> SELECT title, SUBSTRING(description, 0, 30) FROM topic WHERE title LIKE '%Amazon%';
    +--------+-------------------------------+
    | title  | SUBSTRING(description, 0, 30) |
    +--------+-------------------------------+
    | Amazon |                               |
    +--------+-------------------------------+
    1 row in set (0.00 sec)
    
    mysql> SELECT title, SUBSTRING(description, 1, 30) FROM topic WHERE title LIKE '%Amazon%';
    +--------+--------------------------------+
    | title  | SUBSTRING(description, 1, 30)  |
    +--------+--------------------------------+
    | Amazon | Amazon.com, Inc. (/mzn/), is a |
    +--------+--------------------------------+
    1 row in set (0.00 sec)

     

    이제 이 쿼리문을 /search 페이지에서 처리하도록 한다. URL 라우팅하는 객체의 프로퍼티로 구성했다.

    	'/search': function(request, response){
    		var option = {
    			home: false,
      			authorPage: false,
    			searchQuery: query
    		};
    		option.page = parseInt(url.parse(request.url, true).query.page);
    		if (Number(option.page) === 0 || Number.isNaN(option.page)){
    			option.page = 1;
    		};
    		var query = sanitize(url.parse(request.url, true).query.query);
    		db.query(`SELECT id, title, SUBSTRING(description, 1, 100) as description FROM topic WHERE title LIKE ?`, '%' + query + '%', function(err, result){
    			if (err) {throw err};
    		    template.menu.Topic(option, 0, function(topic){   // 토픽목록 링크들 구성
    			    template.menu.Edit(option, 0, function(menu){ // Create, Update 등 메뉴 구성
    				    template.view.searchResult(result, function(searchResult){  // 검색결과 출력
    						var html = template.getHTML('create', menu, topic, searchResult);
    						response.writeHead(200);
    						response.write(html);
    						response.end();
    					});
    				});
    			});
    		});
    	}

     

    검색창이 구성되었다!

    각 항목 간에 구분이 좀 있었으면 좋겠다. 각각을 박스로 묶고, 하이퍼링크를 박스 자체에 걸도록 수정한다.

    박스를 묶어주는 div 태그에 onclick="location.href=URL';"을 추가해주었다.

    그리고 폰트와 패딩, 마진값도 보기 좋게 바꿨다.

    .search-desc {
      margin: 0 0 10px 0;
      padding: 0;
    }
    
    .search-form {
      height: 45px;
    }
    
    .search-title {
      font-family: Trebuchet MS;
      font-weight: normal;
      margin: 15px 0 0 0;
      padding: 0;
    }
    
    .search-result {
      border: solid 1px lightgray;
      cursor: pointer;
      margin: 0;
      padding: 0 15px 0 15px;
    }

    완성되었다! search form의 .query 태그에 검색요청 정보를 value로 추가해서 검색결과도 search bar에 남아있도록 했다.

     

     

     


    2. 페이징

    제일 감이 안 잡혔던 과제다. 먼저 검색을 좀 해서 게시판 페이징 구현하기 라는 글을 찾았다.

    글 작성자분은 php를 사용하셔서 코드를 참조할 순 없었지만 원리를 이해하는 데에 큰 도움이 됐다.

     

    페이징을 구현하려면 제일 먼저 몇 개씩? 을 정해야 한다.

    한 페이지에는 글을 몇 개나 보여줄 것인지, 페이지를 넘길 수 있는 목록은 몇 개씩 보여줄 것인지.

    처음 하다보면 두 개가 굉장히 헷갈린다. 처음에는 똑같이 10개 10개 하려다가, 헷갈려서 15개 10개로 바꾸었다.

     

    지금은 데이터가 7건밖에 없어서 일단 데이터를 생성해야한다. 페이지가 넘어가야 하니까 한 200개 이상의 데이터가 필요하다.

    [Node.js & MySQL] 도전과제: 검색 - 색인기능 살펴보기에서 사용했던 방식 그대로 했다.

    var letter = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
    
    var _promise = function (){
    	return new Promise(function (resolve, reject) {
    
    	var text = '';
    	for (var i in letter){
    		for (var j in letter){
    			text += `('${letter[i]}${letter[i]}${letter[j]}${letter[j]}', 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'), `;
    		};
    	};
    	resolve(text);
    	});
    };
    
    
    _promise()
    	.then(function(text){
    		var query = 'INSERT INTO topic (title, description) VALUES' + text.slice(0, -2);
    		db.query(query, function(err, result){
    			if (err) throw err;
    		});
    	});

     

    데이터 추가가 끝나니까 목록바가 어마어마하게 길어졌다.

     

     

    페이지 내부: SELECT * FROM topic LIMIT 15 OFFSET 0;

    먼저 페이지 하나를 만들어보자. 그러려면 pageNum(페이지 번호), topicPerPage(페이지당 보여줄 목록 개수) 변수가 필요하다.

    topicPerPage를 15로 하기로 했으니, 1페이지라면 글 1번부터 15번까지의 토픽이 들어갈 것이다.

    여기서 OFFSET은 글1의 인덱스인 0이 되고 (MySQL 인덱스가 0부터 시작한다), LIMIT은 1~15의 개수인 15가 된다.

     

    db에서 가져오면 이렇다.

    마지막 페이지에 글이 딱 15개 있는 경우는 별로 없겠지만 상관 없다. LIMIT는 최대 개수를 제한해주는 것이라서 있는 만큼만 나온다.

    mysql> SELECT id, title FROM topic LIMIT 15 OFFSET 0;
    +----+-------------+
    | id | title       |
    +----+-------------+
    |  1 | Amazon      |
    |  2 | Apple       |
    | 17 | Lorem       |
    | 23 | consectetur |
    | 24 | adipisicing |
    | 25 | Amazon2     |
    | 26 | Amazon3     |
    | 27 | AAAA        |
    | 28 | AABB        |
    | 29 | AACC        |
    | 30 | AADD        |
    | 31 | AAEE        |
    | 32 | AAFF        |
    | 33 | AAGG        |
    | 34 | AAHH        |
    +----+-------------+
    15 rows in set (0.00 sec)

    이제 기존에 메뉴 부분을 구성하는 코드 부분의 SQL 쿼리문을 이걸로 변경하고, OFFSET과 LIMIT 변수 추가 및 에러처리를 해준다.

    module.exports = {
      Topic: function(option, id, callback){
      // option 객체의 프로퍼티로 필요한 값들을 담고, 해당 값이 integer가 아니면 기본값으로 설정한다.
        if(!Number.isInteger(Number(option.topicLimit))){
          option.topicLimit = 15;
        };
        if(!Number.isInteger(Number(option.topicOffset))){
          option.topicOffset = 0;
        };
        var list = `
          <div class="list">
            <ol>\n`;
        // 쿼리에 LIMIT, OFFSET 값을 준다.
        db.query(`SELECT id, title FROM topic LIMIT ? OFFSET ?`, [option.topicLimit, option.topicOffset], function(err, data){
          for(var i=0; i<data.length; i++){
            list += `			      <li><a href="/?id=${data[i].id}"`;
            if (data[i].id === id){
              list += ` id="active"`;   // 현재 페이지면 CSS처리를 위해 active를 추가
            };
            list += `>${capitalize(data[i].title)}</a></li>\n`;
          };
          list += `\n			  </ol>
          </div>`;
          callback(`${list}`);
        });
      }
    };

    잘 나오나 확인해본다. 의도대로 15개까지만 출력되었다.

    이제 두 번째 페이지를 만들어보자.

    두 번째 페이지는 16 ~ 30번대 목록이고, 페이지번호가 2번이 된다. 3번 페이지는 31번~45번, 4번 페이지는 46번~60번이 된다.

    그러면 OFFSET은 topicPerPage(15로 지정)*(페이지번호(ex 2)-1)+1이 된다.

    전 페이지의 마지막 번호가 해당 페이지번호*topicPerPage이기 때문에, 현재 페이지는 거기에 1을 더하면 알 수 있다.

    2페이지라면, 15*1+1인 16, 3페이지라면 15*2+1인 31가 된다. 1페이지는 0*topicPerPage + 1이니 1이 된다.

     

    OFFSET 값을 15로 변경하고 (2페이지는 16번에서 시작해야 하고, MySQL 인덱스는 0부터 시작하기 때문에) 테스트를 해본다.

    데이터는 잘 나오는데, 번호가 1부터 나온다. HTML의 <ol> 태그에 start값을 줘서 수정한다.

    HTML의 <ol> 태그는 인덱스 1부터 시작하기 때문에 MySQL OFFSET값에 1을 더해줘야 한다.

    <ol start="${option.topicOffset+1}">\n`;

     

    이제 페이지 번호가 주어지면 해당 주제들을 보여줄 수 있게 됐다. 현재 페이지 번호를 처리해보자.

     

    URL 쿼리스트링에 현재 페이지 번호 추가하기

    페이지 쿼리를 어떻게 처리하는지 이해가 필요해서 사이트들을 둘러보다가,

    티스토리 기본 형식에 페이징 방식이 적용되어 있는 걸 확인했다.

    (네이버나 구글 등도 살펴보았는데 URL에 ?page= 로 처리하지 않아서 패스했다.)

     

    div태그의 자식 a태그로 아래처럼 구성되어 있다.

    파랗게 표시된 1에는 링크가 없다. 2번부터는 아래와 같은 a태그가 달려있다.

    <a class="link_page" href="?page=2">
      <span>2</span>
    </a>

    Get Form을 만들까 했는데 이렇게 처리해도 될 것 같다. 나는 버튼이 좋아서 버튼형식으로 하기로 했다.

    button의 onclick='location.href=URL'에 /?page=3을 입력하면 3번 페이지로 연결되고, 31~45번을 보여주는 식이다.

    라우팅 시에 page 번호를 토대로 OFFSET을 구해서 넘기게 해본다.

     

    // URL을 받아서 처리 페이지로 라우팅하는 함수. 구구절절 길어져서 일부분만 가져왔다.
    function(request, response){
      var option = {
        topicPerPage: 15
      };
      option.page = parseInt(url.parse(request.url, true).query.page);
      // 페이지 쿼리스트링을 파싱해서 넣고, 혹시 숫자가 아닌 값이 들어오면 1로 지정한다
      if (Number(option.page) === 0 || Number.isNaN(option.page)){
        option.page = 1;
      };
      // OFFSET은 topicPerPage(15로 지정)*(페이지번호(ex 2)-1)+1 인데,
      // MySQL의 시작 인덱스가 0이기 때문에 -1을 해줘야하므로 +1을 하지 않았다.
      option.topicOffset = option.topicPerPage*(option.page);
      menu.Topic(option, response, function(menu){  // 나머지는 여기서 처리
        response.writeHead(200);
        response.write(menu);
        response.end();
    };

     

    ?page=3을 입력하니 의도대로 31~45번으로 잘 이동한다. 이동시 오른쪽 상세 페이지는 홈을 보여주게 해두었다.

    이제 페이지 이동 버튼를 만들어줄 차례다.

     

    페이지 이동 버튼 만들기

    페이지 이동 버튼을 만들려면 전체 페이지 수가 있어야 한다.

    전체 페이지 수는 전체 데이터 개수를 topicPerPage 변수, 내 경우는 15로 나눠서 구할 수 있다.

    mysql> SELECT COUNT(*) FROM topic;
    +----------+
    | COUNT(*) |
    +----------+
    |      683 |
    +----------+
    1 row in set (0.00 sec)

     

    내 경우 현재 글이 683개 있으니 한번에 15개씩 보여준다고 가정하면 총 페이지는 46페이지가 되고,

    그 페이지들의 리스트 한 번에 10개씩 보여줄 것이므로

     

    1 2 3 4 5 6 7 8 9 10 다음

    이전 11 12 13 14 15 16 17 18 19 20 다음

    ...

    이전 41 42 43 44 45 46

     

    이런 식으로 구성될 것이다.

    내가 현재 3페이지에 머물러 있다고 하면, 1~10까지를 보여주어야 하고,

    41페이지에 머물러 있다고 하면, 41~46까지를 보여주어야 한다.

    내가 현재 머물러 있는 범위를 알고 나면 그 범위 기준으로 이동 버튼을 작성하면 된다.

     

    1~10 범위를 1번 블럭, 11~20 범위를 2번 블럭이라고 하자. 그리고 한 블럭의 페이지 수인 10을 pagePerBlock 변수에 저장한다.

    현재 블럭은 현재 페이지번호를 pagePerBlock으로 나눠서 올림하고,

    (2페이지라면 2/10, 0.2를 올림하면 1. 44페이지라면 44/10, 4.4를 올림하면 5)

    전체 블럭 수는 총 페이지 수인 46을 pagePerBlock으로 나눈 다음 올림해서 구한다.

    (전체가 46페이지라면, 한 블록에 10페이지씩 들어갈 경우 5블록(46/10, 4.6 올림하면 5)이 있어야 한다.

    20페이지씩 들어간다면 3블록(46/20, 2.3 올림하면 3이면 되겠다.)

    먼저 현재 페이지를 기준으로 블럭을 찾아본다.

    var getBlock = function(page, pagePerBlock, topicPerPage){
      db.query(`SELECT COUNT(*) as count FROM topic`, function(err, result){
        var count = result[0].count;  // 총 글 개수
        var totalBlock = Math.ceil(Math.ceil(count/topicPerPage)/pagePerBlock);  // 전체 블럭
        var curBlock = Math.ceil(page/pagePerBlock);   // 현재 블럭
        if (curBlock > totalBlock){   // 현재 블록이 전체 블록수보다 많으면
          curBlock = totalBlock;
        } else if (curBlock < 1){
          curBlock = 1;
        };
        console.log(curBlock, totalBlock);
      });
    };
    
    
    getBlock(page=0, pagePerBlock=10, topicPerPage=15);    // 1 5
    getBlock(page=2, pagePerBlock=10, topicPerPage=15);    // 1 5
    getBlock(page=10, pagePerBlock=10, topicPerPage=15);   // 1 5
    getBlock(page=21, pagePerBlock=10, topicPerPage=15);   // 3 5
    getBlock(page=34, pagePerBlock=10, topicPerPage=15);   // 4 5
    getBlock(page=46, pagePerBlock=10, topicPerPage=15);   // 5 5
    getBlock(page=52, pagePerBlock=10, topicPerPage=15);   // 5 5

    이제 현재 블럭을 알았으니 이동버튼을 만들 수 있다.

    내가 2페이지에 있으면, 1번 블럭에 속하는 1~10페이지가 필요하고, 그 중 2에 active 처리가 되어야 한다.

    21페이지에 있다면 3번 블럭인 21~30이 필요하고, 21에 active 처리가 필요하다.

    45페이지에 있다면, 5번 블럭인 41~46이 필요하고, 45에 active 처리가 필요하다.

     

    현재 블럭의 시작페이지는 현재 페이지를 블럭으로 환산한 다음, 직전 블럭 번호*pagePerBlock+1로 구할 수 있다.

    전 블럭의 마지막 페이지가 그 블럭번호*pagePerBlock이기 때문에, 현재 블럭의 첫 페이지는 거기에 1을 더한 값이 된다.

    OFFSET을 구했던 topicPerPage(15로 지정)*(페이지번호(ex 2)-1)+1과 같다.

     

    현재 블럭의 마지막 페이지현재 블럭*pagePerBlock을 구하면 된다.

    다만 마지막 페이지라면 블럭의 페이지 수가 pagePerBlock보다 작을 수 있으니, 전체 페이지수와 비교해서 작은 값을 고르면 된다.

     

    그리고 이전, 다음 블록으로 이동 기능을 만들어야 한다. 보통 페이지들이 <, >로 표시하는 것 같아 나도 그렇게 했다.

    현재 블록이 1이 아니면 이전 블록으로 이동하는 버튼을 만들고,

    현재 블록이 마지막 블록이 아니면 다음 블록으로 이동하는 버튼을 만든다.

    var getButton = function(pageNum, curBlock, totalBlock, pagePerBlock, lastPage){
      var buttons = `<div class="page">
      `;
      var blockFirst = (curBlock-1)*pagePerBlock+1;
      var blockLast = curBlock*pagePerBlock;
      if (blockLast > lastPage){
        blockLast = lastPage;
      };
      if (curBlock > 1  //  < 버튼
        buttons += `        <button class="button-page" onclick="location.href='/?page=${blockFirst-1}'"><</button>`;
      }
      for (var i = blockFirst; i < blockLast+1;i++){
        if ( i === pageNum){   // active 설정용
          buttons += `<button class="button-page" id="page-active">${i}</button>`;
        } else {
          buttons += `<button class="button-page" onclick="location.href='/?page=${i}'">${i}</button>`;
        };
      };
      if (curBlock < totalBlock){   // > 버튼
        buttons += `
      <button class="button-page" onclick="location.href='/?page=${blockLast+1}'">></button>`;
      }
      buttons += `</div>`
      return buttons;
    }
    
    
    var getBlock = function(pageNum, pagePerBlock, topicPerPage){
      db.query(`SELECT COUNT(*) as count FROM topic`, function(err, result){
        var count = result[0].count;
        var lastPage = Math.ceil(count/topicPerPage);   // 위 코드에서 lastPage를 추가했다
        var totalBlock = Math.ceil(lastPage/pagePerBlock);
        var curBlock = Math.ceil(pageNum/pagePerBlock);
        if (curBlock > totalBlock){
          curBlock = totalBlock;
        } else if (curBlock < 1){
          curBlock = 1;
        };
        var button = module.exports.getButton(pageNum, curBlock, totalBlock, pagePerBlock, lastPage);
        console.log(button);
        });
    }

    버튼은 이렇게 버튼 HTML로 작성된다.

    getBlock(page=34, pagePerBlock=10, topicPerPage=15);
    /*
    <div class="page">
            <button class="button-page" onclick="location.href='/?page=30'"><</button>
            <button class="button-page" onclick="location.href='/?page=31'">31</button>
            <button class="button-page" onclick="location.href='/?page=32'">32</button>
            <button class="button-page" onclick="location.href='/?page=33'">33</button>
            <button class="button-page" id="page-active">34</button>
            <button class="button-page" onclick="location.href='/?page=35'">35</button>
            <button class="button-page" onclick="location.href='/?page=36'">36</button>
            <button class="button-page" onclick="location.href='/?page=37'">37</button>
            <button class="button-page" onclick="location.href='/?page=38'">38</button>
            <button class="button-page" onclick="location.href='/?page=39'">39</button>
            <button class="button-page" onclick="location.href='/?page=40'">40</button>
            <button class="button-page" onclick="location.href='/?page=41'">></button>
    </div>
    */ 

     

    이제 여기에 CSS를 넣으면 버튼은 끝난다.

    #page-active {
      background: #dcdad7;
      cursor: default;
    }
    
    .button-page {
      background: transparent;
      border: none;
      border-radius: 10px;
      cursor: pointer;	
      float: left;
      font-size: 12px;
      margin: 0 0 25px 0;
      padding: 5px;
      width: 30px;
    }
    
    .button-page:hover{
      background: #e8e6e4;
    }

     

    완료된 버튼을 메뉴 아래에 얹고, 의도대로 출력되는지 확인한다.

    무사히 구성되었다!

     

    토픽과 페이지가 연동되지 않는 문제가 남았다.

    페이지바로 페이지를 이동한 상태에서 특정 토픽을 선택하면, 토픽 데이터는 정상적으로 표시되지만 몇 번째 블럭에서 접근했는지와 관계없이 1번 블럭으로 이동해버린다. 토픽을 누르는 순간 페이지값이 상실되는 것이다.

    페이지 버튼에는 ?page=16 쿼리스트링이 연결되어 있지만, 토픽에는 토픽 id 값만 있을뿐 page 정보가 누락되어 생기는 이슈다.

    각각의 토픽의 href에도 페이지 정보를 넣어주어야 한다.

    if(option.page !== 1){
    	var pageNum = `&page=${option.page}`;
    } else {
    	var pageNum = '';
    }
    
    // 데이터 불러와서 <a> 리스트 작성
    db.query(`SELECT id, title FROM topic LIMIT ? OFFSET ?`, [option.topicPerPage, option.topicOffset+1],function(err, data){
    	if (err) { throw err };
    	for(var i=0; i<data.length; i++){
    		list += `			      <li><a href="/?id=${data[i].id}${pageNum}"`;
    		if (data[i].id === id){
    			list += active;
    		};
    			list += `>${capitalize(data[i].title)}</a></li>\n`;
    	}
    	list += `\n			  </ol>
    	</div>`;
        // 나머지 처리
    });

    현재 페이지의 토픽 목록을 만드는 코드에 페이지값 처리를 추가했다.

    option 변수에서 페이지값을 받아와서, 1페이지가 아니면 페이지값을 href 정보에 추가한다.

     

     

    이제 현재 페이지의 다른 토픽으로 이동시에도 페이지값이 상실되지 않는다.

     

     

    이후 할 수 있는 작업들은

    1. 페이지 이동시, 해당 페이지 첫번째 글(위 그림대로면 226. IIKK)을 선택하게 하기

    2. 검색 결과에도 페이징 적용하기

    3. 검색창에서도 페이지 이동 가능하게 하기

    정도가 있을 것 같다. 차차 해봐야겠다.

     

     


    3. 정렬

    정렬 옵션은 오름차순/내림차순/작성일순/작성일역순 4가지 옵션으로 구성한다.

    일단은 정렬 폼을 만들고, 콤보박스를 추가해서 option 값을 받아야한다. 정렬 기준도 검색처럼 get 방식 form으로 받아온다.

    var sortBar = function(){
      var sortForm = ``;
      sortForm += `
        <form action="" method="get" class="sort-by">
          <select name="sortBy">
            <option value="title-asc">Title 오름차순</option>
            <option value="title-diesc">Title 내림차순</option>
            <option value="created-asc">작성일 오름차순</option>
            <option value="created-desc">작성일 내림차순</option>
          <select>
          <input type="submit" value="확인" class="sort-button">
        </form>`;
      return sortForm;
    }

    해당 폼을 메뉴에 추가해준다음

     

    검색창을 이용했을 때 sortBy 값이 잘 들어오나 확인해본다. 작성일 오름차순을 선택하면

    /?sortBy=data-asc

    처럼 URL로 데이터가 들어온다. 이제 라우팅 시에 sortBy 쿼리스트링을 값을 얻도록 수정한다.

    이상한 값이 들어올 수 있으니 유효한 값만 배열에 담아서 includes()로 검증했다.

    현재 필요한 데이터는 option 객체의 프로퍼티로 담아서 보내고 있으니, 이번에도 그렇게 한다.

    var sortBy = url.parse(request.url, true).query.sortBy;
    if (['title-asc', 'title-desc', 'created-asc', 'created-desc'].includes(sortBy)){
      option.sortBy = sortBy;
    } else {
      option.sortBy = '';
    };

    이후에 가능하다면 저기 목록에 들어가는 유효한 값들은 다른 스크립트에 모아두고 임포트해서 쓰는 것이 좋겠다.

     

    sort 값을 받아왔으니 처리할 sort 페이지를 만든다.

    추가되는 부분은 정렬 뿐이니, 위의 페이징에서 Topic 메뉴바를 만들었던 코드의 db 쿼리문에 ORDER 옵션만 추가하면 된다.

     

    현재 메뉴의 db 쿼리는 이렇게 생겼다. option 객체에 조건을 담아서 진행했으니, sort 옵션도 option에 추가해주면 되겠다.

    var option = {
      topicPerPage: 15,  // 조회 개수
      topicOffset: 0     // 조회 시작점
    }
    
    db.query(`SELECT id, title FROM topic LIMIT ? OFFSET ?`, [option.topicPerPage, option.topicOffset+1], function(err, data){
    // code
    }
    

     

    다만 db.query에 넣을 때 ? 이스케이프해서 넣으면 좋겠는데 숫자가 아니면 문자열 취급되면서 따옴표가 들어가서

    데이터가 아니라 컬럼명이나 명령어인 경우에는 ? 이스케이프를 쓸 수가 없다. 그래서 아래처럼 쓰는 건 불가능하다.

    var option = {
      sortBy: 'ORDER BY id ASC',
      topicPerPage: 15,
      topicOffset: 0
    }
    db.query(`SELECT id, title FROM topic ? LIMIT ? OFFSET ?`, [option.sortBy, option.topicPerPage, option.topicOffset+1], function(err, data){
      console.log(db.sql);
      // SELECT id, title FROM topic 'ORDER BY id ASC' LIMIT 15 OFFSET 1
      if (err) { throw err };
      /* 위의 구문은 실행불가하므로 SQL 에러가 뜨면서 err를 보여준다.
      sqlMessage: "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax
     to use near ''ORDER BY id ASC' LIMIT 15 OFFSET 1' at line 1" */
    };
    

    위에서 쿼리스트링 파싱할 때 유효한 값만 필터링을 했기 때문에 현재 option.sortBy는 'title-asc'처럼 유효한 조건이거나, 빈 값이다.

    유효한 조건인 경우 쿼리에 넣어야 하니 ORDER BY를 붙여주고 하이픈을 띄어쓰기로 바꿔준 다음 SQL 쿼리문에 합친다.

    if(['title-asc', 'title-desc', 'date-asc', 'date-desc'].hasOwnProperty(option.sortBy)){
      var sortBy = ' ORDER BY ' + sortBy.replace("-", " ");
    } else {
      var sortBy = ''; 
    }
    
    
    var query = `SELECT id, title FROM topic` + sortBy + ` LIMIT ? OFFSET ?`;
    db.query(query, [option.topicPerPage, option.topicOffset+1],function(err, data){
      if (err) { throw err };
      // code
    };

     

    정렬 조회 폼 입력 -> URL 파싱 -> sort 정보를 db.query에 전달까지 완료했으니, 조회가 정상적으로 되어야 한다. 확인해본다.

    현재 화면은 'Title 내림차순'을 선택하고 확인을 누른 후의 화면이다.

    토픽목록은 의도대로 출력되었는데, select의 option 태그에 selected 설정이 안 되어있어서 혼란이 생긴다.

    sortForm의 해당 부분에 selected 설정을 해줘야한다. sortBar를 호출할 때 option을 받아오도록 해서 if 절을 추가한다.

    var sortBar = function(option){
      valid = ['title-asc', 'title-desc', 'created-asc', 'created-desc'];
      validValues = ['Title 오름차순', 'Title 내림차순', '작성일 오름차순', '작성일 내림차순'];
      var sortForm = `
                  <form action="sort" method="get" class="sort-by">
                  	<select name="sortBy">`;
      for(var i in valid){
        if (valid[i] === option.sortBy){
      		sortForm += `
      			<option value="${valid[i]}" selected>${validValues[i]}</option>`
      	} else {
      		sortForm += `
      			<option value="${valid[i]}">${validValues[i]}</option>`
      	};
      };
      sortForm += `						 <select>
          <input type="submit" value="확인" class="sort-button">
          </form>`;
      return sortForm;
    }

    제대로 동작하는지 확인해본다. 목록바도 있고 previous/next 버튼도 있으니 혼란스러워져서 상대적으로 덜 중요한 previous를 없앴다.

    정렬을 실행해도 선택값이 그대로 남아있다. 의도대로 수정되었다.

     

    문제가 하나 남았다. 정렬이 된 상태에서 페이지를 2페이지로 넘기면 다시 기본 정렬의 2페이지로 돌아가고 만다.

    아래의 페이지 버튼을 만들 때 href에 "/?page=2"만 넣어줬기 때문이다.

     

    정렬 상태를 유지하면서 페이지를 넘기기 위해서는 ?page=2&sortBy=title-desc처럼 sortBy 값도 주도록 추가해줘야 한다.

    위에서 페이지를 만들면서 썼던 코드에 option.sortBy 처리를 추가한다. active면 href가 없기 때문에 else에만 추가했다.

    이쪽이 원래 코드. /?page= 로만 링크되어 있다.

    for (var i = blockFirst; i < blockLast+1;i++){
    	if ( i === pageNum){   // active 설정용
    		buttons += `<button class="button-page" id="page-active">${i}</button>`;
    	} else {
    		buttons += `<button class="button-page" onclick="location.href='/?page=${i}'">${i}</button>`;
    	}
    }

    수정된 코드. <, > 버튼을 처리하는 부분에도 링크를 추가했다. 값이 없으면 ''으로 처리해서 해당 부분에 연결했다.

    if (option.sortBy){
    	var sortBy = `&sortBy=${option.sortBy}`;
    } else {
    	var sortBy = '';
    }
    if (curBlock > 1){
    	buttons += `        <button class="button-page" onclick="location.href='/?page=${blockFirst-1}${sortBy}'"><</button>`;
    }
    for (var i = blockFirst; i < blockLast+1;i++){
    	if ( i === pageNum){   // active 설정용
    		buttons += `
    			<button class="button-page" id="page-active">${i}</button>`;
    	} else {
    		buttons += `
    			<button class="button-page" onclick="location.href='/?page=${i}${sortBy}'">${i}</button>`;
    	};
    };
    if (curBlock < totalBlock){
    	buttons += `
    		<button class="button-page" onclick="location.href='/?page=${blockLast+1}${sortBy}'">></button>`;
    }
    buttons += `\n</div>`
    

    그리고 실행해보니

    ?page=2&sortBy=title-desc 처럼 쿼리스트링이 정상 전달되는 것을 확인했다. 정렬 조건을 유지한 상태로 다음 페이지로 넘어가진다.

     

    댓글

Designed by Tistory.