ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Javascript - AJAX] Node.js + AJAX - 기본 페이지
    생활코딩/WEBn 2021. 2. 9. 14:10

    생활코딩 AJAX 강의를 완료하고, 기존에 Node.js로 백엔드를 구현해놓은 코드에 AJAX를 적용해보았다.

     

    가장 큰 차이는 기존에는 MySQL 쿼리문으로 직접 받아왔던 데이터를, fetch API에서는 URL로 받아온다는 것.

    Node.js의 app 라우팅 부분에 데이터 리턴을 추가해서 프론트의 fetch API와 URL로 소통할 수 있도록 했다.

    이쪽을 참조했다: node-js-mysql-how-to-do-the-ajax-call-and-access-the-data-on-server-js

     

    작업하다보니 내용이 길어져서 글을 나누었다.

    기본 페이지(Rread): 현재 포스팅

    편집 페이지(AuthorPage): [Javascript - AJAX] Node.js + AJAX - 편집 페이지(Author)

    편집 페이지(Create/Update/Delete): [Javascript - AJAX] Node.js + AJAX - 편집 페이지(Create/Update/Delete)

    작업하면서 배운 것: [Javascript - AJAX] Node.js + AJAX - 배운 것


    현재 상태는 아래처럼 상단 타이틀은 계속 유지되고, 좌측에서 클릭하는 메뉴에 따라서 오른쪽의 컨텐츠가 변경되는 구성이다.

    변경되는 건 일부분이지만(오른쪽 컨텐츠나 왼쪽 토픽목록 등) 매번 전체 페이지를 리로드 해야하는 불편함이 있었다.

     

    우선 전체 페이지를 박스로 나눠서 구성해보기로 한다.

     

    전체 레이아웃

    먼저 기본 HTML 구조를 만들고, 고정된 부분인 상단 타이틀을 넣어줬다.

    Update/Delete, 검색까지 상단 타이틀에 합쳐서 배너처럼 만들려고 nav 태그로 묶었다.

    <!DOCTYPE html>
      <html lang="en" dir="ltr">
      <head>
        <meta charset="utf-8">
        <title>Lorem ipsum dolor sit amet</title>
      </head>
      <body>
        <div class="banner">
          <h1>Lorem ipsum dolor sit amet</h1>
          <nav class="edit">
            <div class="edit-menu">edit-menu</div>
            <div class="search-form">search-form</div>
          </nav>
        </div>
        <div class="main">
          <div class="left">
            <div class="sort-bar">sort-bar</div>
            <div class="topic-list">topic-list</div>
            <div class="page-bar">page-bar</div>
        </div>
          <div class="right">right</div>
        </div>
      </body>
    </html>

    그리고 스타일을 줘서 기본 레이아웃을 만들었다.

    색상 선택이 고민이었는데 www.htmlcsscolor.com/ 이곳이 아주 유용했다.

    CSS 정렬할 때 유용한 사이트: www.lonniebest.com/FormatCSS/

    더보기

     

    .banner {
    	background: whitesmoke;
    	height: 140px;
    	margin: 0;
    	padding: 0;
    	position: absolute;
    	width: 100%;
    }
    .edit div {
    	color: white;
    }
    .left {
    	background-color: #D1CCD9;
    	display: grid;
    	margin-top: 140px;
    	padding: 20px;
    }
    .main {
    	display: grid;
    	grid-template-columns: 1fr 3fr;
    	height: 100%;
    	margin: 0;
    	padding: 0;
    }
    .right {
    	background-color: gray;
    	margin-top: 140px;
    	padding: 20px;
    }
    h1 {
    	font-size: 40px;
    	text-align: center;
    }
    html, body {
    	height: 100%;
    	margin: 0;
    	padding: 0;
    	width: 100%;
    }
    nav {
    	background-color: #333333;
    	display: grid;
    	grid-template-columns: 1.5fr 1fr;
    	margin: 0;
    	padding: 10px;
    }

     

     

    메인 컨텐츠(right)

    먼저 제일 단순한 오른쪽 컨텐츠 조회 화면을 만든다.

    원래는 Big Tech Companies가 주제여서 로고도 넣고 매출도 정리해넣고 했었는데, 기능에 집중하기 위해 부수적인 내용을 다 뺐다.

    db에도 Title, Description, created, author_id만 남기고, 조회 시에도 Title, Description 2가지만 출력하기로 한다.

     

    오른쪽 화면에서는 <h2> 태그에 Title, <p> 태그에 Description 데이터가 들어간다.

    해당 데이터는 db에서 아래 쿼리로 받아왔었다.

    SELECT title, description FROM test WHERE id = 아이디

    fetch()에서 조회할 URL은 url/content/id로 하고, Node.js에서 해당 요청이 들어오면 데이터를 반환하도록 설정한다.

     

     

    MySQL 데이터를 JSON 포맷으로 넘기는 게 편할 것 같아서 방법을 찾아보았다.

    [참조] how-to-convert-result-table-to-json-array-in-mysql

    SELECT JSON_ARRAYAGG(JSON_OBJECT('title', title, 'description', description)) from test;

    조회하면 이렇게 나온다.

    mysql> SELECT JSON_ARRAYAGG(JSON_OBJECT('title', title)) FROM test WHERE id = 1;
    +--------------------------------------------+
    | JSON_ARRAYAGG(JSON_OBJECT('title', title)) |
    +--------------------------------------------+
    | [{"title": "AAAA"}]                        |
    +--------------------------------------------+
    1 row in set (0.00 sec)

    이제 이걸 node쪽 main.js에 붙여서 JSON으로 리턴하게 한다.

    var http = require('http');
    var url = require('url');
    var path = require('path');
    var db = require('./db');
    
    var app = http.createServer(function(request, response){
      var _url = request.url;
      var pathname = url.parse(_url, true).pathname;
      url_parsed = pathname.split('/');
    
      if (url_parsed[1] === 'content') {
        var id = Number(url_parsed[2]);
        if (Number.isInteger(id)) {
          var result = '';
            db.query(`SELECT JSON_ARRAYAGG(JSON_OBJECT('title', title, 'description', description)) as '?' FROM test WHERE id = ?`, [id, id] , function(err, data){
            if (err) { throw err;};
            if (data.length === 0){         // 값이 나오지 않으면, 없는 id이면
              response.writeHead(404);
              response.end('Data Not Found');
            } else {
              result = data[0][id];
              response.setHeader('Content-Type', 'application/json');
              response.end(result);
            }
          });
        };
      } else {
    	response.writeHead(200);
    	response.end(fs.readFileSync(__dirname + '/index.html'));   // 기본적으로는 여기
      }
    });
    app.listen(3000);

    그러고나서 페이지에서 url/content/3을 입력해서 3번 데이터가 잘 출력되나 확인해본다.

     

    무사히 가져와졌으니, fetch의 해당 부분에 연결한다. 일단 기본값을 id가 3번인 데이터로 줬다.

    <script>
      window.addEventListener("load", function(){
        fetch('/content/3').then(function(response){
          response.json().then(function(data){
          var content = `<h2>${data[0]['title']}</h2>
               		<p>${data[0]['description']}</p>`;
          document.querySelector('.right').innerHTML = content;
          });
        });
      });
    </script>

    원래는 서버를 다르게 하려고 했는데, 보안상의 이유로 교차 출처 리소스 공유(CORS)를 허용해야 한다는 에러가 떴다.

    현재 수준에서 CORS까지 시도하는 건 무리일 것 같아서 이번에는 같은 URL에서 처리하기로 한다.

    실행해보면 right가 있던 부분에 잘 들어가있는 걸 확인할 수 있다.

     

    의도대로 나오니 함수로 정리해두고,

    var viewContent = function(id){
      fetch('/content/' + String(id)).then(function(response){
        response.json().then(function(data){
          var content = `<h2>${data[0]['title']}</h2>
          		<p>${data[0]['description']}</p>`;
          document.querySelector('.right').innerHTML = content;
        });
      });
    }

    이제 URL 해시값으로 타이틀을 받으면 (ex url/#AACC) 해당 페이지를 보여줄 수 있도록 수정한다.

    title로는 데이터를 볼 수 없으니 id값으로 변환이 필요하다. 그러려면 데이터베이스 조회가 필요하니 먼저 API를 만든다.

    API URL은 /url/get-id/title로 했다.

    // main.js 라우팅 추가부분
    
    } else if (url_parsed[1] === 'get-id') {
    		var hash = url_parsed[2];
    			db.query(`SELECT JSON_ARRAYAGG(JSON_OBJECT('id', id, 'title', title)) as title FROM test WHERE title = ?`, [hash] , function(err, data){
    		      if (err) { throw err;};
    		      if (data.length === 0){         // 값이 나오지 않으면, 없는 id이면
    			    response.writeHead(404);
    			    response.end();
    		      } else {
    				result = data[0]['title'];
    				response.setHeader('Content-Type', 'application/json');
    				response.end(result);
    			  }
    			});

    API가 잘 작동하나 확인해본다.

    무사히 출력된다. 이제 id값이 확인되었으니 viewContent 함수로 연결해서 해당 페이지를 보여주면 된다.

    해시값은 location.hash로 확인할 수 있다.

    <script>
      var hash_title = location.hash.replace("!", "").replace("#", "");
        if(hash_title){
          fetch('/get-id/' + hash_title).then(function(response){
          response.json().then(function(data){
            viewContent(data[0]['id']);
          })
        })
      }
    </script>

    해시값을 주고 접속을 시도해본다. 의도대로 출력되었다.

     

     

     

    좌측 상단 편집메뉴(edit-menu)

    *포스팅이 길어져서 여기서는 레이아웃만 잡아두고, 편집 버튼에 연결될 편집 페이지는 다음 포스팅에서 연결하기로 한다.

     

    편집 메뉴에는 Home/Create/Update/Delete/AuthorPage이 있는데, 여기서 Home은 뺐다.

    별도의 버튼을 만드는 것보다 상단 타이틀을 클릭하면 홈으로 연결되게 하는 게 낫겠다는 생각이 들었다.

     

    나머지 중 Update/Delete는 오른쪽의 컨텐츠가 있으면 필요하고, 없으면 필요없는 친구들이다.

    기본값으로 비활성화 했다가 위에서 viewContent 함수를 실행할 때 활성화 해주기로 한다.

     

    일단 해당 부분에 class 값을 주고, display 기본값을 none으로 해둔 다음,

    <style>
      .content-edit {
        display: none;
      }
    </style>
    
    
    <ul>
      <li>AuthorTable</li>
      <li>Create</li>
      <li class="content-edit">Update</li>
      <li class="content-edit">Delete</li>
    </ul>

    오른쪽 컨텐츠를 로드할 때 style.display를 'inline'으로 수정하도록 함수를 추가한다. onEdit()은 위의 viewContent()가 실행되면 실행하도록 한다.

    <script>
      var onEdit = function(){
        var content_edit = document.querySelectorAll('.content-edit');
        for (var i=0;i<content_edit.length;i++){
          content_edit[i].style.display = 'inline';
        };
      };
    </script>

    이제 오른쪽 페이지를 조회하면 Update/Delete가 생긴다.

     

    컨텐츠가 없으면 다시 저걸 none으로 돌려야 하니, 파라미터를 받아서 true면 none으로 바꾸도록 수정한다.

    <script>
    var onEdit = function(bool){
      var content_edit = document.querySelectorAll('.content-edit');
      var change = 'inline';
      if(bool){
        change = 'none';
      }
      for (var i=0;i<content_edit.length;i++){
        content_edit[i].style.display = change;
      };
    };
    </script>

    이제 메뉴가 한 줄에 표시되도록 정렬을 horizontal로 바꿔주고 padding을 주었다.

    ul을 horizontal로 표시하는 방법은 이쪽을 참고했다: www.w3schools.com/html/tryit.asp?filename=tryhtml_lists_menu

    .edit-menu ul {
      list-style-type: none;
      margin: 0;
      padding: 0;
      overflow: hidden;
    }
    .edit-menu li {
      float: left;
      padding: 3px 15px 3px 10px;
    }

    의도대로 가로로 잘 표시된다. edit-menu 텍스트는 이제 필요없으니 지웠다.

     

     

    검색창(search-form)

    *포스팅이 길어져서 여기서는 레이아웃만 잡아두고, 검색창에 연결될 검색 페이지는 다음 포스팅에서 연결하기로 한다.

    먼저 html로 form을 만들고

    <div class="search-form">
      <form action="search" method="get">
        <input type="text" class="search search-bar" name="query" placeholder="검색어를 입력하세요." value="">
        <input type="submit" class="search search-button" value="Search">
      </form>	
    </div>

    CSS를 추가했다.

    .search-form {
      padding-right: 20px;
      text-align: right;
    }
    .search-bar {
      font-size: 14px;
      margin: 5px;
      padding: 3px;
    }
    .search-button {
      background: #8E82A2;
      border: none;
      border-radius: 3px;
      color: whitesmoke;
      cursor: pointer;	
      font-size: 16px;
      padding: 4px 6px 6px 6px;
    }
    .search-button:hover {
      background-color: #A59BB5;
    }

    여기서 헤맸던 부분 중 하나가, 어떻게 텍스트 입력 부분과 버튼을 둘다 오른쪽으로 정렬할까 였다.

    처음에는 <input type="text">인 search-bar에 text-align:right; 을 줬었는데, 그러면 이렇게 된다.

    버튼에 float: right;을 주면 버튼만 간다.

    해답은 입력 부분과 버튼을 둘다 자식으로 가지고 있는 div 태그를 오른쪽으로 정렬하는 것(text-align: right;)

    혹은 form에 클래스를 주고 text-align: right;나 float: right;를 줘도 된다.

     

    div에 float: right;를 주는 건 소용이 없었는데, 이유가 궁금해져서 text-align과 float의 차이를 찾아보았다.

    /difference-between-float-and-align-property-in-css

    text-alignbox의 컨텐츠(내용물, 자식태그?)에 적용되고, float은 box 자체에 적용된다고 한다.

    내 경우 div는 그야말로 껍데기여서 그 자체를 정렬하는 건 의미가 없는 반면,

    form은 input을 자식으로 둔 컨테이너이면서, 동시에 저 태그들을 갖고 있는 box 자체이기 때문에 양쪽 모두에 적용되었던 것 같다.

     

    나는 확인하기 쉽도록 div class로 적용하기로 했다.

     

     

     

    좌측 하단 토픽목록

    여기에 들어가야 하는 것은 3가지다. 정렬 버튼(sort-bar)과 그 아래로 들어갈 토픽 메뉴(topic-list), 그리고 페이지 번호 바(page-bar).

     

    각각의 토픽 메뉴를 누르면 오른쪽에 컨텐츠를 불러온다. 토픽들은 정렬 옵션에 따라 정렬된다.

    한 페이지에는 토픽 15개씩이 들어가고, 아래에 있는 페이지 버튼으로 다음 페이지로 넘어갈 수 있다. 다음 페이지 이동시에도 정렬 상태가 남아있어야 한다.

     

    먼저 정렬 버튼을 누른 후, 페이지에 정렬 값이 반영되도록 설정한다.

     

    좌측 하단 토픽목록 1. 정렬 버튼(sort-bar)

    -정렬 옵션은 총 4개, title-desc, title-asc, created-asc, created-desc.

    -정렬했을 때 페이지에 선택한 옵션이 반영되어 있어야(selected) 하는데, SPA 구성이라서 url이 바뀌지 않으니 이건 따로 적용하지 않아도 될 것 같다.

     

    먼저 html <select> 태그를 이용해 기본 콤보박스를 만들고, CSS를 준다.

    브라우저마다 기본 디자인이 다른데, 이걸 없애려면 appearance: none을 사용해야 한다고 한다.

    select 콤보박스 스타일 관련해서 이 블로그를 참조했다: www.daleseo.com/css-selects/

    <style>
      .sort-bar {
        text-align: left;
        margin: 1.3em 0 0 0.2em;
      }
      .sort-bar select {
        font-size: 13px;
        padding: .6em;
        margin: 0;
        border: 1px solid #aaa;
        border-radius: .5em;
        -moz-appearance: none;
        -webkit-appearance: none;
        appearance: none;
        background-color: #fff;
        background-repeat: no-repeat, repeat;
      }
      .sort-button:hover {
        background-color: #A59BB5;
      }
    </style>
    
    <div class="sort-bar">
      <select name="sortBy">
        <option value="title-asc">타이틀 오름차순</option>
        <option value="title-desc">타이틀 내림차순</option>
        <option value="created-asc">일자 오름차순</option>
        <option value="created-desc">일자 내림차순</option>
        <input type="submit" value="Sort" class="sort-button">
      </select>
    </div>

    이제 선택한 값을 처리해야 한다. onchange를 이용하면 select 옵션이 변경되었을 때 함수를 호출할 수 있다.

    선택한 값을 넘기려면 this.value를 주면 된다.

    혹시 다른 함수에서 선택한 값을 가져오려면 document.getElementById("sort").value를 쓰면 된다.

    <div class="sort-bar">
      <select id="sort" onchange="changeSort(this.value);">
        <option value="title-asc">타이틀 오름차순</option>
        <option value="title-desc">타이틀 내림차순</option>
        <option value="created-asc">일자 오름차순</option>
        <option value="created-desc">일자 내림차순</option>
        <input type="submit" value="Sort" class="sort-button">
      </select>
    </div>

    이제 changeSort() 함수에서 토픽메뉴를 정렬하면 된다. 우선 토픽메뉴를 만든다.

     

    좌측 하단 토픽목록 2. 토픽 메뉴(topic-list)

    -토픽 메뉴에는 위에서 설정한 정렬기준에 따라(혹은 디폴트 기준에 따라) 토픽 목록이 출력되어야 하고,

    -각각의 토픽을 클릭하면 위에서 작성한 viewContent() 함수를 호출해서 오른쪽 페이지를 갱신해야 한다.

    -그리고 오른쪽 페이지가 출력되는 경우, 토픽 목록에서 해당 값을 active 처리(빨간색?)해야 한다.

     

    일단 db에서 인덱스 0-15 범위의 데이터를 가져와본다.

    그러려면 API를 만들어주어야 해서, topic-temp/title-asc/0 으로 구성했다.

    var http = require('http');
    var url = require('url');
    var path = require('path');
    var db = require('./db');
    
    var app = http.createServer(function(request, response){
      var _url = request.url;
      var pathname = url.parse(_url, true).pathname;
      url_parsed = pathname.split('/');
    
      if (url_parsed[1] === 'topic-temp'){
        var sortBy = url_parsed[2];
        var start = Number(url_parsed[3]);
        if (['title-asc', 'title-desc', 'created-asc', 'created-desc'].includes(sortBy)){
          var sortBy = 'ORDER BY ' + sortBy.replace("-", " ");
          var query = `SELECT JSON_ARRAYAGG(JSON_OBJECT('id', id, 'title', title)) as topic FROM (SELECT id, title FROM test ${sortBy} LIMIT ? OFFSET ?)sub`;
          if (Number.isInteger(start) && start >= 0){
            db.query(query, [15, start], function(err, data){
              if (err) {throw err};
              response.setHeader('Content-Type', 'application/json');
              response.end(data[0]['topic']);
            });
          }
        } else {
          response.writeHead(404);
          response.end('Data Not Found');
        }
      } else {
    	response.writeHead(200);
    	response.end(fs.readFileSync(__dirname + '/index.html'));   // 기본적으로는 여기
      }
    });
    app.listen(3000);
    
    
    

    API가 잘 작동하는지 확인해본다. id가 1부터 시작해서 50번째 데이터의 id는 51이다.

     

     

    이제 html에 해당 API를 연결한다.

    document.getElementById("sort").value 값을 받아와서 fetch()에 넘긴 다음,

    위 json 데이터를 받아와서 리스트 처리하고, <li>마다 onclick="viewContent(id)"를 추가해서 각 항목을 누르면 오른쪽에 컨텐츠가 로드되도록 한다.

    var viewTopic = function(){
      var start = 0;
      try {
        var sortBy = document.getElementById("sort").value;
      } catch {  // 아직 정렬을 선택하지 않은 경우, 디폴트
        sortBy = 'title-asc';
      }
      fetch('/topic-temp/' + sortBy + '/' + String(start)).then(function(response){
        response.json().then(function(data){
          var content = `<ul>`;
          for (var j in data){
            var topic = data[j];
            content += `<li><a onclick="viewContent(${topic['id']})">${topic['title']}</a></li>`;
          }
          content += `</ul>`;
          document.querySelector('.topic-list').innerHTML = content;
        });
      });
    }
    window.addEventListener("load", viewTopic());   // html이 로드된 이후에 받아와서 getElementById 작업에 이슈가 없도록

     

    그리고 위에서 작성했던 정렬바의 onchange="" 부분에 이번에 작성한 viewTopic()을 넣어준다.

    의도대로 작동되고 있다!

    이것만으로는 현재 페이지 위치를 알기 어려우니까 일단 URL에 #!을 넣어준다.

    위의 for문에서 content += 부분에 href="#!타이틀"을 추가했다.

    <a href="#!${topic['title']}" onclick="viewContent(${topic['id']});">${topic['title']}</a>

     

    그리고 선택한 페이지가 무엇인지 알 수 있도록 active처리를 하려는데, 어렵다.

    기존에는 페이지를 새로 로딩해서 active가 문제되지 않았는데, 클릭하면 그걸 기준으로 오른쪽만 바뀌는 상황에서, 기준 자신인 topic-list도 변경하려니까 감이 안 잡혔다. 인터넷(howto_js_active_element)을 찾아보니 class를 for문으로 찾아서 active를 바꾸면 된다고 한다.

     

    일단 기존 토픽들에 클래스를 준다. 위의 for문에서 content += 부분에 class="topic-item"을 추가했다.

    <a class="topic-item" href="#!${topic['title']}" onclick="viewContent(${topic['id']});">${topic['title']}</a>

     

    위 페이지에서는 getElementById로 가져왔는데, 나는 아이디를 안 줬기 때문에 querySelectorAll로 가져왔다.

    + 맨처음에 active값이 없을 때 current[0]을 하면 undefined에러가 떠서 length를 검사하는 if문을 추가했다.

    + 추가하는 김에 active인 경우는 현재 페이지니까 연결되는 href도 뺐다.

    처음에는 href를 삭제해야하나 했는데, disabled-href-tag라는 글에서 pointer-events: none이라는 훌륭한 기능이 있다는 걸 배웠다.

    /* CSS */
    .active {
      color: red;
      pointer-events: none;
      cursor: default;
      text-decoration: none;
    }
    var activeRed = function(){
      var topics = document.querySelectorAll('.topic-item');
      for (var i = 0; i < topics.length; i++) {
        topics[i].addEventListener("click", function() {
          var current = document.getElementsByClassName("active");
          if (current.length !== 0) {
            current[0].href = `#!${current[0].value}`;
            current[0].className = current[0].className.replace(" active", "");
          }
          this.className += " active";
          this.removeAttribute("href");
        });
      } 
    };

    이제 이 함수를 위의 viewTopic() 끝에 추가하고

    var viewTopic = function(){
       // bla bla
          document.querySelector('.topic-list').innerHTML = content;
          activeRed();
    }

    테스트를 해보니 무사히 작동한다.

     

     

    좌측 하단 토픽목록 3. 페이지 번호 바(page-bar)

    이제 토픽들에 페이지번호를 붙여줄 차례다. 한 페이지에는 토픽이 15개씩 들어가고, 페이지 간의 이동이 가능해야 한다.

     

    위에서 토픽목록을 가져올 때, 0번 데이터부터 15개를 가져왔다. API url은 topic-temp/title-asc/0을 썼다. 여기가 1번 페이지 범위다.

    그러면 2번 페이지를 만들기 위해서는 topic-temp/title-asc/15(인덱스가 1부터 시작하기 때문)를 쓰면 되고,

    마찬가지로 3번 페이지를 만들기 위해서는 topic-temp/title-asc/30 ...

    해서 마지막 페이지는, 전체 데이터가 676고, 15로 나누면 몫이 45이고 나머지가 1로 0보다 크니까 45*15인 675번 데이터부터 조회하면 된다.

     

    이때 15에 곱하는 몫이 페이지번호다. 매번 데이터번호를 입력하는 건 번거로우니까, 데이터번호의 묶음인 페이지번호로 해당 데이터를 가져올 수 있도록 API를 수정한다.

     

    url/page/sortBy(str)/pageNum(int)

    -pageNumber 정보와 sortBy 정보를 받아서 해당 페이지에 속한 토픽목록, 해당 페이지가 속한 블럭 정보를 리턴하는 API

     

    페이지/블럭에 관한 정보는 지난번 도전과제의 로직을 그대로 가져왔다.

    현재 페이지의 정보를 인자로 받아서, 전체 db 개수와 연산해서 전체 블럭 수(totalBlock), 현재 블럭 번호(curBlock), 현재 블럭의 첫번째 페이지(blockFirst), 현재 블럭의 마지막 페이지(blockLast), 전체 데이터의 마지막 페이지(lastPage)를 리턴한다.

    var db = require('./db');
    var pagePerBlock = 10;
    var topicPerPage = 15;
    
    function getPageInfo(pageNum, callback){
      var query = 'SELECT COUNT(*) as CNT FROM test';
      db.query(query, function(err, data){
        if(err) {throw err};
        var count = data[0]['CNT'];
        var totalBlock = Math.ceil(Math.ceil(count/topicPerPage)/pagePerBlock);   // 전체 블럭
        var curBlock = Math.ceil(pageNum/pagePerBlock);                 // 현재 블럭
        if (curBlock > totalBlock){   // 현재 블록이 전체 블록수보다 많으면
          curBlock = totalBlock;
        } else if (curBlock < 1){
          curBlock = 1;
        };
        var lastPage = Math.ceil(count/topicPerPage);  // 전체 중 마지막 페이지
        var blockFirst = (curBlock-1)*pagePerBlock+1;  // 블록의 첫번째 페이지
        var blockLast = curBlock*pagePerBlock;         // 블록의 마지막 페이지
        if (blockLast > lastPage){    // 현재 블록 마지막 페이지가 전체 마지막 페이지보다 크면
          blockLast = lastPage;
        };
        var pageInfo = {
          'totalBlock': totalBlock,
          'curBlock': curBlock,
          'curPage': pageNum,
          'blockFirst': blockFirst,
          'blockLast': blockLast,
          'lastPage': lastPage
        };
    	callback(pageInfo);
      });
    }

     

    토픽목록 정보는 db.query에서 json 타입 데이터로 가져왔다.

    var db = require('./db');
    var pagePerBlock = 10;
    var topicPerPage = 15;
    
    function getTopicList(sortBy, offset, callback){
    	var query = `SELECT JSON_ARRAYAGG(JSON_OBJECT('id', id, 'title', title)) as topicList FROM (SELECT id, title FROM test ${sortBy} LIMIT ? OFFSET ?)sub`  // 토픽목록 구하는 쿼리
    	db.query(query, [topicPerPage, offSet*topicPerPage], function(err, data){
    		callback(JSON.parse(data[0]['topicList']));
    	});
    }

     

    topic-temp를 처리했던 함수에 위에서 작성한 getPageInfo()와 getTopicList() 함수를 붙여서 page 처리 함수를 만들었다.

    var http = require('http');
    var url = require('url');
    var path = require('path');
    var db = require('./db');
    
    var app = http.createServer(function(request, response){
      var _url = request.url;
      var pathname = url.parse(_url, true).pathname;
      url_parsed = pathname.split('/');
    
      if (url_parsed[1] === 'page'){
        var sortBy = url_parsed[2];
        var pageNum = Number(url_parsed[3]);
        var offSet = pageNum - 1;  // MySQL은 인덱스가 0부터 시작이라서
        if (!Number.isInteger(offSet) || offSet < 0) {   // 페이지 번호가 자연수가 아니면
          response.writeHead(404);
          response.end('Data Not Found');
        } else {
          if (['title-asc', 'title-desc', 'created-asc', 'created-desc'].includes(sortBy)){
          var sortBy = ' ORDER BY ' + sortBy.replace("-", " ");
          getTopicList(sortBy, offSet, function(topicList){
            getPageInfo(pageNum, function(pageInfo){
              var result = {
              'topicList': topicList,
              'pageInfo': pageInfo
              }
              response.setHeader('Content-Type', 'application/json');
              response.end(JSON.stringify(result));
            });
          });
        } else {
          response.writeHead(404);
          response.end('Data Not Found');
        }
      }
    
      } else {
        response.writeHead(200);
        response.end(fs.readFileSync(__dirname + '/index.html'));   // 기본적으로는 여기
      }
    });
    app.listen(3000);

    result라는 제이슨 데이터에 topicList, pageInfo를 담아서 리턴한다.

    url/page/title-asc/23와 url/page/title-desc/1을 각각 테스트해보았다.

     

    url/page/title-asc/23
    /url/page/title-desc/1

     

    모두 의도대로 잘 나온다. 이제 데이터가 확보되었으니 viewTopic() 함수를 수정한다. fetch API에서 받는 리턴값이 변경되었다.

    var viewTopic = function(pageNum){
      if (pageNum === undefined){   // 현재 페이지 기본값 설정
        pageNum = 1;
      }
      try {  // 정렬 기본값 설정
        var sortBy = document.getElementById("sort").value;
      } catch {
        var sortBy = 'title-asc';
      }
      fetch('/page/' + sortBy + '/' + String(pageNum)).then(function(response){
        response.json().then(function(data){
          var topicList = data['topicList'];   // 데이터 구조가 topic-temp와 달라졌다
          var content = `<ul>`;
          for (var j in topicList){
            var topic = topicList[j];
            content += `<li><a class="topic-item" href="#!${topic['title']}" onclick="viewContent(${topic['id']});">${topic['title']}</a></li>`;
          }
          content += `</ul>`;
          document.querySelector('.topic-list').innerHTML = content;
          activeRed();
        });
      });
    }
    window.addEventListener("load", viewTopic());

    아까 topic-temp에서 작동했던 기능이 정상 작동하는지 확인하고

    정상 동작하고 있으니 이제 아래에 페이지번호 바를 추가한다.

    페이지 번호 바의 각 페이지 번호들은 넘어갈 페이지와 링크되어야 한다. 그걸 클릭해서 이동하면 다시 그 페이지에서 이동할 수 있는 페이지 번호들을 보여줘야 한다. 이런 식으로 반복되기 때문에 여기는 재귀로 처리했다.

    페이지가 바뀌면 거기서 보여줘야하는 토픽목록도 바뀌기 때문에, 작업 완료 후 viewTopic()을 다시 호출했다.

    var viewPageBar = function(pageNum){
      if (pageNum === undefined){
        pageNum = 1;
      }
      try {
        var sortBy = document.getElementById("sort").value;
      } catch {
        var sortBy = 'title-asc';
      };
      fetch('/page/' + sortBy + '/' + String(pageNum)).then(function(response){
        response.json().then(function(data){
          var pageBar = '<div class="page-bar">';
          var pageCount = data['pageInfo']['blockFirst'];
          if (data['pageInfo']['curBlock'] > 1){  // 첫 페이지가 아니면
            pageBar += `<button class="button-page" onclick="viewPageBar(${pageCount-1})"><</button>`;
          }
          while (pageCount < data['pageInfo']['blockLast']+1){
            if (pageCount === data['pageInfo']['curPage']){  // 현재 페이지인 경우
              pageBar += `<button class="button-page" onclick="viewPageBar(${pageCount})" id="current-page">${pageCount}</button>`;
            } else {
              pageBar += `<button class="button-page" onclick="viewPageBar(${pageCount})">${pageCount}</button>`;
            }
            pageCount++;
          }
          if (data['pageInfo']['curBlock'] < data['pageInfo']['totalBlock']){  // 마지막 페이지가 아니면
            pageBar += `<button class="button-page" onclick="viewPageBar(${pageCount+1})">></button>`;
          }
          pageBar += '</div>'
          document.querySelector('.page-bar').innerHTML = pageBar;
          viewTopic(pageNum);   // 페이지가 갱신되면 토픽목록도 갱신해줘야 한다
        });
      });
    }

    정상 작동하는지 확인한다.

    페이지바가 생겼다! 넘김도 정상 작동하고, 해당 페이지의 토픽목록도 의도대로 출력된다.

     

    다음으로 넘어가기 전에 코드 정리를 좀 해야할 것 같다. 위에서 페이지, 토픽 데이터를 받을 때 같은 API에 각각 별도로 요청하고 있다. 그외에도 pageNum 설정 등 겹치는 부분이 많다. 둘을 묶어줄 함수를 하나 만들고, 그 안에서 데이터를 받아서 각각 실행하도록 한다.

    // 둘을 묶어줄 함수
    var viewTopicNBar = function(pageNum){
      if (pageNum === undefined){
        pageNum = 1;
      };
      try {
        var sortBy = document.getElementById("sort").value;
      } catch {
        var sortBy = 'title-asc';
      };
      fetch('/page/' + sortBy + '/' + String(pageNum)).then(function(response){
        response.json().then(function(data){
          var startTopic = (data['pageInfo']['curPage']-1)*15+1;
          var topicList = viewTopic(data['topicList'], startTopic);
          var pageInfo = viewPageBar(data['pageInfo']);
          document.querySelector('.topic-list').innerHTML = topicList;
          document.querySelector('.page-bar').innerHTML = pageInfo;
          activeRed();
        });
      });
    }

     

    viewPageBar()과 viewTopic()은 이제 페이지 정보 대신에 위에서 전달해준 data[각자 필요한 데이터]를 받으면 된다.

    pageNum을 받아서 처리하는 함수가 viewTopicNBar()이 됐으니, viewPageBar()에서 호출했던 재귀함수도 viewTopicNBar()가 돼야한다.

    var viewPageBar = function(pageData){
      var pageBar = '<div class="page-bar">';
      var pageCount = pageData['blockFirst'];
      if (pageData['curBlock'] > 1){  // 첫 페이지가 아니면
        pageBar += `<button class="button-page" onclick="viewTopicNBar(${pageCount-1})"><</button>`;
      }
      while (pageCount < pageData['blockLast']+1){
        if (pageCount === pageData['curPage']){  // 현재 페이지인 경우
          pageBar += `<button class="button-page" onclick="viewTopicNBar(${pageCount})" id="current-page">${pageCount}</button>`;
        } else {
          pageBar += `<button class="button-page" onclick="viewTopicNBar(${pageCount})">${pageCount}</button>`;
        }
        pageCount++;
      }
      if (pageData['curBlock'] < pageData['totalBlock']){  // 마지막 페이지가 아니면
        pageBar += `<button class="button-page" onclick="viewTopicNBar(${pageCount+1})">></button>`;
      }
      pageBar += '</div>'
      return pageBar;
    };
    

    viewTopic()은 토픽목록을 리스트로 받으니 pageNum을 받을 필요가 없다. 페이지 구분을 명확히 하려면 목록이 <ul> 이 아니라 <ol>인 편이 좋을 듯해서, 시작번호(startTopic)를 받기로 했다. 인덱스가 1부터여야 해서 현재 페이지 기준으로, 이전 페이지*15+1로 구했다.

    var viewTopic = function(topicData, startTopic){
      var content = `<ol start=${startTopic}>`;   // 토픽목록의 번호 출력을 위해서 바꿨다.
      for (var j in topicData){
        var topic = topicData[j];
        content += `<li><a class="topic-item" href="#!${topic['title']}" onclick="viewContent(${topic['id']});">${topic['title']}</a></li>`;
      }
      content += `</ol>`;
      return content;
    }

    마찬가지로 처음 로드할 함수와 sort 후 onchange에서 실행하는 함수도 viewTopicNBar()로 수정해야 한다.

    <script>
    	window.addEventListener("load", viewTopicNBar());
    </script>
    
    
    <html>
     ...
    		  <select id="sort" onchange="viewTopicNBar();">
    			  <option value="title-asc">타이틀 오름차순</option>
    			  <option value="title-desc">타이틀 내림차순</option>
    			  <option value="created-asc">일자 오름차순</option>
    			  <option value="created-desc">일자 내림차순</option>
    			  <input type="submit" value="Sort" class="sort-button">
    		  </select>
    </html>

    다시 정상 작동하는지 확인하고, 이제 CSS를 얹어준다.

    버튼을 담당하는 .page-bar 부분은 지난번에 썼던 디자인에서 배경색만 밝은 톤으로 바꾸었다.

    .topic-list ol {
      width: 90%;
      padding: 0 0 0 40px;
    }
    
    .topic-list li {
      color: inherit;
      padding: 3px;
    }
    
    .topic-list a:hover {
      background-color: inherit;
      color: red;
    }
    
    #current-page {
      background: #F6F6F6;
      cursor: default;
    }
    
    .page-bar {
      padding: 7px;
      min-height: 150px;
    }
    
    .button-page {
      background: transparent;
      border: none;
      border-radius: 10px;
      cursor: pointer;	
      float: left;
      font-size: 14px;
      margin: 0 0 30px 0;
      padding: 5px;
      width: 32px;
    }
    
    .button-page:hover{
      background: #e8e6e4;
    }

    <li> 태그 처리에서 좀 헤맸다.

    처음에 편집메뉴 설정할 때 .edit <li> 가 아니라 <li> 태그 전체에 horizontal 적용을 했었는데, 한참 후에 .topic-list의 <li> 태그를 작업하다보니까 그 사실을 까맣게 잊어버린 것이다. 대체 왜 자꾸 가로로 정렬이 되는지 몰라서 한참을 헤맸다. 상속 문제를 해결하니 정상적으로 동작한다.

    li 같은 태그는 여기 저기서 등장할 수 있으니, 지엽적인 스타일은 꼭 부모태그를 같이 붙여주는 게 좋겠다.

     

    현재까지 했던 작업은 모두 반영되었다. 이제 페이지바에 2가지 문제가 남았다.

    1. 페이지가 넘어가도 오른쪽 화면이 바뀌지 않는다. for문으로 토픽목록을 만들 때, 첫 번째 토픽을 선택하도록 수정해야 한다.

    2. sort를 변경하면 1페이지 1번 토픽으로 돌아간다. 현재 선택한 토픽 값을 유지하면서 정렬 정보만 갱신할 수 있도록 수정이 필요하다.

     

     

    먼저 1. for문으로 토픽목록을 만들 때, 첫 번째 토픽을 선택하도록 수정한다.

    리스트의 첫번째 데이터면 class에 active를 넣어주고, viewContent()를 실행하고, URL에 #!을 추가한다.

    var viewTopic = function(topicData, startTopic){
      var content = `<ol start=${startTopic}>`;
      for (var j in topicData){
        var topic = topicData[j];
        if (Number(j) === 0){
          content += `<li><a class="topic-item active" href="#!${topic['title']}" onclick="viewContent(${topic['id']});">${topic['title']}</a></li>`;
          viewContent(topic.id);
          window.location.hash = "!" + topic.title;
        } else {
          content += `<li><a class="topic-item" href="#!${topic['title']}" onclick="viewContent(${topic['id']});">${topic['title']}</a></li>`;
        }
      }
      content += `</ol>`;
      return content;
    };

    모두 잘 적용되었다.

     

     

    이제 2. 현재 선택한 토픽 값을 유지하면서 정렬 정보만 갱신할 수 있도록 수정한다.

    그러려면 먼저 새 정렬에서 해당 id가 몇 번째 페이지에 위치하는지 알아내야 한다. ORDER BY 상태에서 특정 id가 몇 번째 데이터가 되는지를 확인하면 된다.

     

    [MySQL] ORDER BY한 값에서 특정 id의 ROW Number 확인하기

    mysql의 변수 기능을 이용, rank 변수를 설정해서 ROW NUMBER를 구할 수 있다. [참조] ranking-mysql-results

    SET @rank=0;
    SELECT @rank:=@rank+1 AS rank, id, title FROM test ORDER BY title DESC;
    
    
    +------+-----+--------------------+
    | rank | id  | title              |
    +------+-----+--------------------+
    |    1 | 675 | ㄹㅇㅎㄴㄹㅇ          |
    |    2 | 674 | ZZZZ               |
    |    3 | 673 | ZZYY               |
    |    4 | 672 | ZZXX               |
    |    5 | 671 | ZZWW               |
    |    6 | 670 | ZZVV               |
    |    7 | 669 | ZZUU               |
    |    8 | 668 | ZZTT               |
    |    9 | 667 | ZZSS               |
    |   10 | 666 | ZZRR               |
    |   11 | 665 | ZZQQ               |
    ...

    rank값이 설정되면 서브쿼리를 이용해서 WHERE id = ?를 찾아주면 된다.

    *처음에 ()sub 없이 그냥 넣었더니 ERROR 1248 (42000): Every derived table must have its own alias 에러가 떴다.

    찾아보니 서브쿼리에 이름을 지어주라는 뜻이라고 한다.

    SELECT rank, id FROM (SELECT @rank:=@rank+1 AS rank, id, title FROM test ORDER BY title DESC)sub WHERE id = 674;
    +------+-----+
    | rank | id  |
    +------+-----+
    |    2 | 674 |
    +------+-----+
    1 row in set (0.01 sec)

    이제 rank값을 topicPerPage로 나눠서 올림하면 현재 페이지값을, 페이지를 다시 pagePerBlock으로 나눠서 올림하면 블록값을 얻을 수 있다.

    정렬이 갱신되면 먼저 해당 id의 rank 정보를 API로 요청 -> rank에서 페이지번호를 구해서 -> page API 요청 순으로 처리하면 된다.

    일단 필요한 데이터는 확보했는데, 데이터가 추가되면 매번 다시 rank를 설정해줘야 하는 문제가 있다.

    이걸 어떻게 효율적으로 할 것인가는 이후 정렬 알고리즘을 공부하고 나서 다시 고민해보기로 하고, 다음 단계로 가본다.

     

    먼저 rank API를 만든다.

    url/rank/id(int)/sortBy(str)

    -sortBy 정보와 id를 받아서 해당 id의 rank값을 리턴하는 API

    위에서 작성했던 rank 쿼리를 mysql로 보내서 처리했다. 복수쿼리 실행 여부가 false이기 때문에 쿼리를 2번에 나눠서 작성했다.

    var http = require('http');
    var url = require('url');
    var path = require('path');
    var db = require('./db');
    
    var app = http.createServer(function(request, response){
      var _url = request.url;
      var pathname = url.parse(_url, true).pathname;
      url_parsed = pathname.split('/');
    
      if (url_parsed[1] === 'rank') {
        var sortBy = url_parsed[2];
        var id = Number(url_parsed[3]);
        var sortByBool = ['title-asc', 'title-desc', 'created-asc', 'created-desc'].includes(sortBy);
        if ((Number.isInteger(id)) && sortByBool) {
          var sortBy = ' ORDER BY ' + sortBy.replace("-", " ");
          var query1 = `SET @rank=0`;
          var query2 = `SELECT rank, id FROM (SELECT @rank:=@rank+1 AS rank, id, title FROM test ${sortBy})sub WHERE id = ?;`
          db.query(query1, function(err1, result1){
            if (err1) { throw err1 };
            db.query(query2, id, function(err2, result2){
              if (err2) { throw err2 };
              if (result2.length === 0) {         // 값이 나오지 않으면, 없는 id이면
                response.writeHead(404);
                response.end();
              } else {
                result = result2[0];
                result['curPage'] = Math.ceil(result['rank']/topicPerPage);  // 페이지정보
                response.writeHead(200, { 'Content-Type': 'application/json' });
                response.end(JSON.stringify(result));
              }
            });
          });
        } else {
          response.writeHead(404);
          response.end('Data Not Found');
        };
      } else {
        response.writeHead(200);
        response.end(fs.readFileSync(__dirname + '/index.html'));   // 기본적으로는 여기
      }
    });
    app.listen(3000);

    id 665로 테스트해보았다.

    의도대로 잘 나온다. 이제 정렬이 변경되면, rank API를 요청해서 curPage 값을 받고, 그 값으로 page API 요청을 하면 된다.

    element를 가져와야하는데, active가 class여서 가져오기가 좀 힘들어졌다. 그래서 active는 id로 바꾸고, activeRed() 함수도 수정했다.

    viewTopic에서 첫번째 토픽일 때 주는 값도 class="active"에서 id="active"로 바꿨다.

    var activeRed = function(){
      var topics = document.querySelectorAll('.topic-item');
      for (var i = 0; i < topics.length; i++) {
        topics[i].addEventListener("click", function() {
          var current = document.querySelector('#active');
          if (current !== null) {
            current.removeAttribute('id');
          }
          this.id = "active";
        });
      } 
    };

    이제 rank API를 요청해서 curPage값을 받아서 page API 요청을 할 함수를 만들고, 정렬의 onchange에 들어갈 함수도 이걸로 바꿔준다.

    var findDataInNewSort = function(){
      var current = document.getElementById("active");   // 현재 active 아이디값 가져오기
      if (current === null){
        current = 'AAAA';                                // 없으면 기본값
      } else {
        current = current.innerHTML;                     // 있으면 타이틀
      }
      try {
        var sortBy = document.getElementById("sort").value;
      } catch {
        var sortBy = 'title-asc';
      };
      fetch('/get-id/' + current).then(function(response1){   // 아이디 획득
        response1.json().then(function(data1){
          var id = data1[0]['id'];
          fetch('/rank/' + sortBy + '/' + String(id)).then(function(response2){   // 현재 페이지 획득
            response2.json().then(function(data2){
              var curPage = data2['curPage'];                
              viewTopicNBar(curPage, id);   // Topic/PageBar 조회
            });
          });
        });
      })
    }

    viewTopicNBar에 현재 페이지만 넣었더니, 위에서 만들었던 함수와 충돌(?)해서 자꾸 해당 토픽목록 1번으로 보내서, id를 받도록 수정했다.

    var viewTopicNBar = function(pageNum, id){
      if (id === undefined){   // 아이디가 안 들어오면 0번으로 지정
        id = 0;
      }
      if (pageNum === undefined){
        pageNum = 1;
      };
      try {
        var sortBy = document.getElementById("sort").value;
      } catch {
        var sortBy = 'title-asc';
      };
      fetch('/page/' + sortBy + '/' + String(pageNum)).then(function(response){
        response.json().then(function(data){
          var startTopic = (data['pageInfo']['curPage']-1)*15+1;
          var topicList = viewTopic(data['topicList'], startTopic, id);
          var pageInfo = viewPageBar(data['pageInfo']);
          document.querySelector('.topic-list').innerHTML = topicList;
          document.querySelector('.page-bar').innerHTML = pageInfo;
          activeRed();
        });
      });
    }

    마찬가지로 viewTopic에도 id를 넘겨야 한다. id값을 받은 경우는 토픽목록만 바꾸면 되고, id값이 없는 경우는 토픽목록도 바꾸고 그 중 첫번째 페이지에 active값을 주면 된다.

    var viewTopic = function(topicData, startTopic, id){
      var content = `<ol start=${startTopic}>`;
      for (var j in topicData){
        var topic = topicData[j];
        if ((id === 0 && Number(j) === 0) || (topic['id'] === id)){
        // id가 없고 첫번째 페이지거나 & id가 topic의 id와 같은 경우 - 현재 페이지
          content += `<li><a id="active" class="topic-item" href="#!${topic['title']}" onclick="viewContent(${topic['id']});">${topic['title']}</a></li>`;
          if (id === 0){  // 첫번째 페이지일때만 갱신. id가 topic의 id와 같은 경우는 굳이 viewContent 갱신할 필요가 없다.
            viewContent(topic.id);
            window.location.hash = "!" + topic.title;
          }
        } else {
          content += `<li><a class="topic-item" href="#!${topic['title']}" onclick="viewContent(${topic['id']});">${topic['title']}</a></li>`;
        }
      }
      content += `</ol>`;
      return content;
    };

    그리고 테스트를 해본다. 특정 정렬에서 페이지를 넘기고 토픽을 선택한 후, 정렬만 변경했을 때 페이지/토픽만 변하고 오른쪽의 데이터는 변하지 않으면 성공이다.

    모두 무사히 작동한다!

     

     

    나머지 수정거리들

    상단 배너를 클릭하면 도착할 홈페이지

    먼저 홈페이지를 만들어야 한다. Hello World!를 출력하기로 한다.

    배너를 클릭하면 연결될 onclick을 만들어주고,

    <style>
    	.home {
    		font-family: cursive;
    		font-size: 100px;
    		text-align: center;
    	}
    </style>
    
    <h1><span onclick="home();">Lorem ipsum dolor sit amet</span></h1>

    누르면 실행할 함수를 만들어준다.

    var home = function(){
      var content = `<p class="home">Hello world!</p>`;
      document.querySelector('.right').innerHTML = content;
      window.location.hash = '';  // 해시 없애기
      onEdit('none');  // 에딧 메뉴 없애기
      var current = document.getElementById("active");
      if (current !== null){
        current.removeAttribute('id');
      };
    };
    
    
    var onEdit = function(none){
      var content_edit = document.querySelectorAll('.content-edit');
      var change = 'inline';
      if(none === 'none'){
        change = 'none';
      }
      for (var i=0;i<content_edit.length;i++){
        content_edit[i].style.display = change;
      };
    };

    위의 Lorem ipsum ~ 배너를 누르면 오른쪽에 Hello world! 메시지가 출력된다. 왼쪽의 토픽에서는 id만 빠지고, 정렬 등 기타 설정은 그대로 유지된다.

     

     

    sort버튼의 무쓸모

    현재 select 콤보박스의 상태는 이렇다.

    <select id="sort" onchange="findDataInNewSort();">
      <option value="title-asc">타이틀 오름차순</option>
      <option value="title-desc">타이틀 내림차순</option>
      <option value="created-asc">일자 오름차순</option>
      <option value="created-desc">일자 내림차순</option>
      <input type="submit" value="Sort" class="sort-button">
    </select>

    onchange에 함수를 넣었더니, submit을 누르지 않아도 콤보박스에서 선택만 변경하면 바로 함수가 실행된다.

    정렬 옵션을 url 쿼리로 연결해서 새로 로딩을 하게 되면 전송이 필요하지만, 그 상태에서 바로 변경하면 버튼이 필요가 없는 것이다.

    쓸모가 없으니 submit 버튼과 해당 CSS를 지우고, 콤보박스 사이즈를 늘렸다.

     

     

     

     

     

    댓글

Designed by Tistory.