본문 바로가기

페이팔기반, e커머스 플랫폼 제작하기 [2편. 페이지 CRUD 구현]

by Recstasy 2021. 10. 30.

 

관리자 페이지에서 CRUD기능을 구현하기 위해서는 몇가지 npm모듈이 필요하다. 이를 위해 지난 포스팅[1]에서 bodyParser, express-session, express-validator, connect-flash, express-message 모듈을 설치했으며, index.js에 선언해준다.

 

 


1 index.js

express모듈 아래, bodyParser, session, {check, validationResult} 변수를 추가한다. bodyParser, session 모듈은 꽤 익숙한 분들이 많을 것이다. 반면, express-validator 모듈은 대중적이지 않다. 결론적으로 express-validator는 사용자가 입력한 폼의 유효성을 검사하며, '빈칸', '이메일', '전화번호', '주소'와 같은 형식을 체크하여 그 결과값을 반환해준다. 이 과정에서 함께 사용되는 모듈이 express-message이다. express-message모듈은 서버에서 지정해놓은 특정 메시지를 '프론트-서버'와의 응답과정에서 프론트 쪽으로 전달할 수 있는 기능을 제공한다. 

 


    const express = require('express');
    const path = require('path');
    const mongoose = require('mongoose');
    const config = require('./config/database');
    const app = express();
    const bodyParser = require('body-parser');
    const session = require('express-session');
    const {check, validationResult} = require('express-validator');

  //db 연결
    mongoose.connect(config.database);
    const db = mongoose.connection;
    db.on('error', console.error.bind(console, 'connection error'));
    db.once('open', ()=>{
        console.log(' Connected to MongoDB ');
    })


  //템플릿 설정
    app.set('view engine', 'ejs');
    app.set('views', path.join(__dirname, 'views'));

    app.use(express.static(path.join(__dirname, 'public')));

  //set global errors variable
    app.locals.errors = null;

  //bodyParser
    app.use(bodyParser.urlencoded({ extended : false }));
    app.use(bodyParser.json());

  //Express session middleware
    app.use(session({
        secret: 'webdoli test session',
        resave: true,
        saveUninitialized: true
       // cookie: { secure: true }
    }));

  //express-messages
    app.use(require('connect-flash')());
    app.use(function (req, res, next) {
        res.locals.messages = require('express-messages')(req, res);
        next();
    });

  //라우팅
    let pages = require('./routes/pages.js');
    let adminPages = require('./routes/admin_pages.js');

    app.use('/', pages);
    app.use('/admin/pages', adminPages);

  //https 서버연결
    app.listen(3005);


[ index.js ]

 

express-message모듈은 connect-flash모듈에 의존하는데(npm으로 설치) 이와 관련된 설명은 express-message github홈에서 확인할 수 있다. 의존성 관계를 갖는 connect-flash 설치와 함께 라우팅을 받을 때 req.flash('메서드명', '메시지 내용')를 지정하며, ejs에서 해당 메서드를 사용하는 방법까지 확인할 수 있다.

 

github.com/visionmedia/express-messages

 

express-validator, req.flash( )기능은 add_page.ejs에서 직접 확인하도록 하자.

 

 

 

 

 

 

 


2 admin_pages.js

/routes 폴더에 있는 admin_page.js는 관리자 페이지의 핵심기능을 담고 있다. 'localhost:3005/admin/pages'경로에 요청된  모든 조건을 받아서 약속된 view파일을 연결하기 때문이다. 지금부터 관리자 CRUD(restful API)기능을 하나씩 구현해보자. 

 

 

 2-1 GET요청 ::  /admin/pages

/admin/pages 경로는 관리자 페이지의 '현관문'이다. 게시판 형태로써 DB에 저장된 데이터를 관리자가 모두 볼 수 있어야 한다. 이를 위해, MongoDB 쿼리문으로 pageSchema 데이터를 불러온 뒤, views/admin폴더에 있는 pages.ejs파일에 전달한다.

 


    let
express = require('express');

    let router = express.Router();
    const {check, validationResult} = require('express-validator');

  //Get Page model
    let Page = require('../models/pageSchema');

    router.get('/', (req, res)=>{
        Page.find({}).sort({sorting:1}).exec((err,pages)=>{
            res.render('admin/pages', {
                pages : pages
            })
        })
    });

//Exports
module.exports = router;

[ admin_pages.js ]

 

 

 

 2-2 GET요청 ::  /admin/pages/add-page

/admin/pages/add-page 경로는 관리자가 서버에 데이터를 추가할 때(포스팅) 필요한 Form을 불러온다. 폼입력은 get방식이며, postSchema 프로퍼티에 저장될 값은 변수 형태로 전달한다.(title, slug, content)

 


 // Get add page
    router.get('/add-page', (req, res)=>{
        let title = "";
        let slug = "";
        let content = "";

        res.render('admin/add_page', {
            title:title,
            slug:slug,
            content:content
        });
    });


[ admin_pages.js ]

 

 

 

 2-3 Post요청 ::  /admin/pages/add-page

/admin/pages/add-page 경로는 입력폼에서 사용자가 '제출'을 클릭했을 때, 입력데이터가 전송되는 경로이다. 아래 코드에서는 express-validator를 이용하고 있으며, async - await 프로미스를 사용함으로써 DB에 데이터가 전송되기 전에 유효성 검사를 실행하고 있다. 유효성 체크와 관련된 메서드는 express-validator웹사이트에서 확인할 수 있다.

 

https://express-validator.github.io/docs/custom-error-messages.html

 

 

  //Post add page
    router.post('/add-page', async(req, res)=>{

        await check('title', 'title must have a value.').notEmpty().run(req);
        await check('content', 'Content must have a value.').notEmpty().run(req);

        let title = req.body.title;
        let slug = req.body.slug.replace(/\s+/g, '-').toLowerCase();
        if(slug == "") slug = title.replace(/\s+/g, '-').toLowerCase();
        let content = req.body.content;

  //validator
        let validatorResults = await validationResult(req);

        if(validatorResults.errors[0] !== undefined){   //validatorResults "if구문" 시작
            res.render('admin/add_page', {
                 errors : validatorResults.errors,
                 title:title,
                 slug:slug,
                 content:content
            });
        }else{ //validatorResults "else구문" 시작
            Page.findOne({slug : slug}, (err, page)=>//PageDB 쿼리문 시작

                if(page){  //DB 반환파일 조건문 시작
                    req.flash('danger', 'Page slug exists, choose another.');
                    res.render('admin/add_page', {
                        title:title,
                        slug:slug,
                        content:content
                    });
                }else{
                    let page = new Page({
                        title : title,
                        slug : slug,
                        content : content,
                        sorting : 100
                    });
                    page.save((err)=>{
                        if(err){
                            return console.log(err);
                        }
                        req.flash('sucess', 'Page added!');
                        res.redirect('/admin/pages');
                    });
                } //DB 반환파일 조건문 끝

            }); //PageDB 쿼리문 끝
        } //validatorResults "else구문" 끝
    }); 


[ admin_pages.js ]

 

title, slug 데이터는 공백을 제거하고, 소문자로 변경한 뒤(위의 정규표현식 부분) 각각 해당 변수에 저장한다. 여기서 validatorResults()의 매개변수에 유효성 검사를 마친 요청객체 'req'를 넣었을 때, errors[0]배열값이 존재한다면(!==undefined) 에러가 발생한 상황이다. 에러가 없다면(else구문) PageDB를 쿼리한 뒤, 똑같은 slug가 없을 경우에만 저장한다. 

 

 

 

 2-4 Post요청 ::  /admin/pages/reorder-pages

/admin/pages/reorder-pages 경로는 데이터 순서가 변경되었을 때, 바뀐 순서가 저장되는 기능을 구현한다. 가령, 관리자가 게시판의 글목록 순서를 변경하는 경우가 대표적인 사례로 볼 수 있다. 

 


  //Post reorder pages
      router.post('/reorder-pages', (req, res)=>{
          let ids = req.body['id[]'];
          let count = 0;

          for(let i=0; i<ids.length; i++){
              let id = ids[i];
              count++;

              ((count)=>{
                  Page.findById(id, (err,page)=>{
                      page.sorting = count;
                      page.save((err)=>{
                          if(err) return console.log(err);
                      });
                  })
              })(count);
          }
    });

[ admin_pages.js ]

 

위의 코드에서 req.body['id[]']는 특정 페이지의 게시판 글목록 id를 배열 형태로 가져온다. 반복문으로 해당 id를 순회하며 새롭게 번호를 지정한다. (( count )=>{ })( count ) 부분은 바로 실행되는 함수 구문이며, let count = (function( ){ })(count)구문과 같다. 

 

 

 

 2-5 Post요청 ::  /admin/pages/edit-page/:slug

/admin/pages/edit-page/:id 경로는 관리자가 특정 id의 글을 수정할 때 요청하는 경로이다. 위의 add-page와 거의 코드가 유사하며, findById( )쿼리문을 통해 url에 저장된 id값의 DB값을 찾아서 수정하는 부분이 다르다.

 


  //Post edit page

    router.post('/edit-page/:id', async(req, res)=>{  
        await check('title', 'title must have a value.').notEmpty().run(req);
        await check('content', 'Content must have a value.').notEmpty().run(req);

        let title = req.body.title;
        let slug = req.body.slug.replace(/\s+/g, '-').toLowerCase();
        if(slug == "") slug = title.replace(/\s+/g, '-').toLowerCase();
        let content = req.body.content;
        let id = req.params.id;

  //validator
        let validatorResults = await validationResult(req);
        if(validatorResults.errors[0] !== undefined){    //유효성 검사 조건문 시작
            res.render('admin/edit_page', {
                 errors : validatorResults.errors,
                 title:title,
                 slug:slug,
                 content:content,
                 id : id
             });
         }else//유효성 검사 else문 시작

             Page.findOne({slug : slug, _id:{'$ne':id}}, (err, page)=>//DB findOne 쿼리문 시작

                 if(page){
                     req.flash('danger', 'Page slug exists, choose another.');
                     res.render('admin/edit_page', {
                         title:title,
                         slug:slug,
                         content:content,
                         id : id
                     });
                 }else{ //DB 반환값 else문 시작
                     Page.findById(id, (err,page)=>
                         if(err) return console.log(err);
                     
                         page
.title = title;

                         page.slug = slug;
                         page.content = content;

                         page.save((err)=>{
                             if(err){
                                 return console.log(err);
                             }
                             req.flash('sucess', 'Page added!');
                             res.redirect('/admin/pages/edit-page/'+ id );
                         });
                     }); 
                 } //DB 반환값 else문 끝

             });  //DB findOne 쿼리문 끝

         }  //유효성 검사 else문 끝
     });

[ admin_pages.js ]

 

 

 

 2-6 Get요청 ::  /admin/pages/edit-page/:slug

get방식의 /admin/pages/edit-page/:id 경로는 수정폼을 랜더링하는 기능을 구현한다. findOne()쿼리문으로 게시글의 고유 데이터(slug)와 비교하여 해당 데이터를 'admin/edit_page'뷰 페이지로 넘겨준다.

 

  // Get edit page
    router.get('/edit-page/:id', (req, res)=>{
        Page.findById( req.params.id , (err,page)=>{
            if(err) return console.log(err);

            res.render('admin/edit_page', {
                title : page.title,
                slug : page.slug,
                content : page.content,
                id:page._id
            });
        })
    });

[ admin_pages.js ]

 

 

 

 2-7 Get요청 ::  /admin/pages/delete-page/:slug

get방식의 /admin/pages/delete-page/:id 경로는 요청 id에 해당하는 데이터를 삭제하는 기능을 구현한다. 삭제할 게시글 id를 찾아서 삭제하는 findByIdAndRemove()메서드를 이용해서 데이터를 삭제하고, req.flash()를 통해 프론트에 메시지를 전달한다.

 


  //Get delete page
    router.get('/delete-page/:id', (req, res)=>{
        Page.findByIdAndRemove(req.params.id, (err)=>{
            if(err) return console.log(err);
            req.flash('success', 'Page Deleted');
            res.redirect('/admin/pages/')
        })
    });

[ admin_pages.js ]

 

 

 

 

 

 

 


3 view

뷰 템플릿은 라우트 파일을 통해 전달된 데이터를 처리하는 역할을 담당한다. 뷰 템플릿의 경우, 지난 포스팅에서 작성했던 adminfooter.ejs, footer.ejs 는 변경된 내용이 없다. 그리고 header파일(header.ejs, adminheader.ejs)은 메시지를 전달받는 부분을 추가하면 된다.

 

 

 

 3-1 /layouts :: adminheader.ejs  |  header.ejs

ejs문법은 대부분 <% %>구조로 되어 있다. index.js에서 express.messages를 사용해서 messages를 지정해뒀다면, 아래와 같이 messages 메서드를 호출할 수 있다. 

 


  <!-- 중략...(기존과 코드같음) -->
    <div class="container">
        <%- messages('messages', locals) %>
            <% if(errors) { %>
                <div class="alert alert-danger">
                    <%= error.msg %>
                </div>
            <% } %>

[ adminheader.ejs ]

 

header.ejs의 끝부분에도 메시지를 받는 코드를 추가한다.

 


  <!-- 중략...(기존과 코드같음) -->

    <div class="container">
    <%- messages('messages', locals) %>

[ header.ejs ]

 

 

 

 

 3-2 /admin :: page.ejs 

page.ejs파일은 관리자의 메인 화면이다. 게시글을 수정, 삭제, 게재, 이동할 수 있는 권한이 있으며, 관련 기능을 GUI단에서 실행할 수 있어야 한다. 관리자GUI 기능을 간단하게 구현하기 위해서 아래 코드에서는 GUI는 제이쿼리 + 제이쿼리UI를 사용했으며, 제이쿼리의 table을 활용했다.

 

 <%- include('../layouts/adminheader') %>

      <h2 class="page-title"> Pages </h2>
      <a href="/admin/pages/add-page" class="btn btn-primary">Add a new page</a>
      <br><br>

      <table class="table table-striped sorting">
          <thead>
              <tr class="home">
                  <th>Title</th>
                  <th>Edit</th>
                  <th>Delete</th>
              </tr>
          </thead>
          <tbody>
              <% pages.forEach((page)=>{ %>
              <tr id="id_<%= page._id %>" class="<%=page._slug %>">
                  <td><%= page.title %></td>
                  <td><a href="/admin/pages/edit-page/<%=page.id %>">Edit</a></td>
                  <% if(page.slug === "home"){ %>
                      <td></td>
                 <% } else { %>
                    <td><a class="confirmDeletion" href="/admin/pages/delete-page/<%=page._id %>">Delete</a></td>
                 <% } %>
             </tr>
           <% }) %>
         </tbody>
      </table>

      <script src="//code.jquery.com/ui/1.11.4/jquery-ui.min.js"></script>
      <script>
          $('tbody').sortable({
              items : "tr:not('.home')",
              placeholder : "ui-state-highlight",
              update : function(){
                  let ids = $('tbody').sortable("serialize");
                  let url = "/admin/pages/reorder-pages";
                  $.post(url, ids);
              }
          });
      </script>

<%-include('../layouts/adminfooter') %>

[ page.ejs ]

 

 

 3-3 /admin :: add_page.ejs

add.ejs 파일은 관리자가 get방식으로 요청하는 Form이다. 

 


    <%-
include('../layouts/adminheader') %>


    <h2 class="page-title"> Add a Page </h2>
    <a href="/admin/pages" class="btn btn-primary">Back to all pages</a>
    <br><br>

    <form method="post" action="/admin/pages/add-page">

        <div class="form-group">
            <label for="">Title</label>
            <input type="text" class="form-control" name="title" value="<%= title %>" placeholder="Title">
        </div>

        <div class="form-group">
            <label for="">Slug</label>
            <input type="text" class="form-control" name="slug" value="<%= slug %>" placeholder="Slug">
        </div>

        <div class="form-group">
            <label for="">Content</label>
            <textarea style="height:500px;" name="content" class="form-control" id="editor" cols="30" rows="20" placeholder="content"><%= content %></textarea>
        </div>

        <button class="btn btn-default">Submit</button>
    </form>

    <%-include('../layouts/adminfooter') %>
    <script src="/js/editor.js"></script>

 

관리자 페이지는 공개된 부분이 아니다. 따라서 게시판은 ck에디터와 같은 간단한 라이브러리를 사용하는 편이 좋다. 위의 코드에서는 textarea태그 부분에 id='editor'를 지정하는 방식의 ckEditor 5.0을 사용했다. 그리고 ckEditor실행을 위해서 js파일을 public폴더의 js경로에 지정해준다.

 

 
  ClassicEditor

        .create( document.querySelector( '#editor' ) )
        .catch( error => {
            console.error( error );
        });

[ main.js ]                                   

 
 
//event
    $('a.confirmDeletion').on('click',(e)=>{
        if(!confirm('Confirm deletion')) return false;
    });

                                [ editor.js ]

 

 

ckEditor사용법은 아래 웹사이트에서 확인해보자. 

 

https://ckeditor.com/docs/ckeditor5/latest/builds/guides/predefined-builds/quick-start.html 

 

Quick start - CKEditor 5 Documentation

Learn how to install, integrate and configure CKEditor 5 Builds and how to work with CKEditor 5 Framework, customize it, create your own plugins and custom editors, change the UI or even bring your own UI to the editor. API reference and examples included.

ckeditor.com

 

 

 

 

 3-4 /admin :: edit_page.ejs

edit_page.ejs 파일은 add_page.ejs와 흡사하다. 차이점이라면, 라우트에서 slug속성을 검색한 뒤 관련 데이터를 보내는 부분이다. 따라서 서버에서 받는 데이터가 있다.

 


    <%-
include('../layouts/adminheader') %>


    <h2 class="page-title"> Add a Page </h2>
    <a href="/admin/pages" class="btn btn-primary">Back to all pages</a>
    <br><br>

    <form method="post" action="/admin/pages/edit-page/<%= id %>">

        <div class="form-group">
            <label for="">Title</label>
            <input type="text" class="form-control" name="title" value="<%= title %>" placeholder="Title">
        </div>

        <div class="form-group">
            <label for="">Slug</label>
            <input type="text" class="form-control" name="slug" value="<%= slug %>" placeholder="Slug">
        </div>

        <div class="form-group">
            <label for="">Content</label>
            <textarea name="content" class="form-control" id="editor" cols="30" rows="10" placeholder="content"><%= content %></textarea>
        </div>

        <button class="btn btn-default">Submit</button>
    </form>

    <%-include('../layouts/adminfooter') %>
    <script src="/js/editor.js"></script>

 

 

 

 

 

 

 


4 정리

기타 css파일의 경우, 부트스트랩을 사용하고 있으므로 크게 손댈 부분이 없다.

 

 
.sorting
tr:not(.home){

      cursor:pointer;
  }

  .ui-state-highlight{
      border: 2px dashed tomato;
  }

  .ck-content{
      height:500px;
  }

 

회원 및 게스트 이용자를 위한 style.css파일은 마지막에 구현할 계획이며, css프레임워크를 사용하는 프로젝트에는 개별 css를 될수 있는한 지양하는 편이 좋다. !important를 사용해서 억지로 권한을 부여하며 클래스를 조합하다보면 스탭이 꼬여버리는 경우가 많기 때문이다. public폴더의 css부분은 가벼울수록 좋다.

 

댓글

최신글 전체

이미지
제목
글쓴이
등록일