[실습]NodeJS + EXPRESS + MySQL 을 이용한 게시판 만들기 3(MVC)

리스트 및 뷰 페이지

추가 모듈 설치

  • markdown-it (마크다운 파서)

    npm install --save markdown-it

컨트롤러 및 모델 등록

  • app/routes/posts.js

    실습2 이후 나머지 부분들 까지 라우터 모두 작성함

      const express = require('express');
      const router = express.Router();
      const postsController = require('../controllers/postsController');
    
      // list
      router.get('/', postsController.getList);
    
      // New Post Form
      router.get('/new', postsController.getPostForm);
    
      // New Post Process
      router.post('/new', postsController.insertProcess);
    
      // View Post
      router.get('/:id', postsController.getView);
    
      // Edit Post Form - 글쓰기 폼을 공유해서 사용함
      router.get('/:id/edit', postsController.getEditForm);
    
      // Edit Post Process
      router.put('/:id', postsController.updateProcess);
    
      // Delete Post Process
      router.delete('/:id', postsController.deleteProcess);
    
      module.exports = router;
  • app/controllers/postsController.js 현재까지의 소스

      const postsModel = require('../models/postsModel');
      const MarkdownIt = require('markdown-it')({
        html: true,
        linkify: true,
        typographer: true,
      }); // 레퍼런스를 보고 마크다운에 HTML, link, 이미지등을 모두 사용한다로 설정
    
      /**
       * 리스트
       * @param req
       * @param res
       */
      exports.getList = (req, res) => {
        postsModel.getList((result) => {
          if (result) {
            // console.log(result);
    
            res.render('posts/list', {
              title: '게시판 리스트',
              posts: result
            });
          } else {
            res.redirect('/');
          }
        });
      };
    
      /**
       * 글 작성 - 폼
       *
       * @param req
       * @param res
       */
      exports.getPostForm = (req, res) => {
        res.render('posts/writeForm', {
          'title': '글 작성하기'
        });
      };
    
      /**
       * 글 입력 - 프로세스
       * ip 찾기 - https://wedul.site/520
       *
       * @param req
       * @param res
       */
      exports.insertProcess = (req, res) => {
        let item = {
          'name': req.body.name,
          'email': req.body.email,
          'password': req.body.password,
          'subject': req.body.subject,
          'content': req.body.content,
          'ip': req.headers['x-forwarded-for'] || req.connection.remoteAddress,
          'tags': req.body.tags
        };
    
        postsModel.insertData(item, (result) => {
          if (result) {
            // console.log(result);
            if (result.affectedRows === 1) {
              res.redirect('/posts');
            } else {
              res.redirect('/posts/new');
            }
          }
        });
      };
    
      /**
       * 글 읽기
       *
       * @param req
       * @param res
       */
      exports.getView = (req, res) => {
        let id = req.params.id;
    
        postsModel.getView(id, (result) => {
          if (result) {
            // let md = new MarkdownIt();
            result.content = MarkdownIt.render(result.content);
    
            res.render('posts/view', {
              title: result.subject,
              post: result
            });
          }
        });
      };
  • app/models/postsModel.js 현재까지의 소스

      // mysql 연결
      const mysqlConnObj = require('../config/mysql');
      const mysqlConn = mysqlConnObj.init();
      // mysqlConnObj.open(mysqlConn); // 정상적으로 연결되었는지 확인
    
      const bcrypt = require('bcrypt');
      const saltRound = 10;
    
      /**
       * 게시글 리스트
       *
       * * cb : Callback function. After completing select, it returns to the controller
       *
       * @param cb
       */
      exports.getList = (cb) => {
        /**
         * todo : 페이징 처리
         * @type {string}
         */
        let sql = 'SELECT * FROM posts ORDER BY id DESC LIMIT 10';
        mysqlConn.query(sql, (err, results, fields) => {
          if (err) {
            console.error('Error code : ' + err.code);
            console.error('Error Message : ' + err.message);
    
            throw new Error(err);
          } else {
            cb(JSON.parse(JSON.stringify(results)));
          }
        });
      };
    
      /**
       * 글 보기
       *
       * 하나의 결과값만 리턴 할 경우 자체가 JSON 형식이라 따로 JSON.parse 안해줘도 됨
       *
       * id : 게시물 번호
       * cd : 콜백 함수
       *
       * @param id
       * @param cb
       */
      exports.getView = (id, cb) => {
        let sql = 'SELECT `id`, `name`, `email`, `subject`, `content`, `like`, `hate`, `hit`, `comment_cnt`, inet_ntoa(`ip`) AS `ip`, `created_at`, `updated_at`  FROM posts WHERE id=? LIMIT 1';
        mysqlConn.query(sql, [id], (err, results, fields) => {
          if (err) {
            console.error('Error code : ' + err.code);
            console.error('Error Message : ' + err.message);
    
            throw new Error(err);
          } else {
            cb(results[0]);
          }
        });
      };
    
      /**
       * 새로운 글을 작성하면 데이터베이스에 입력한다.
       *
       * data : Input data received from the controller
       * cb : Callback function. After completing input, it returns to the controller
       *
       * @param data
       * @param cb
       */
      exports.insertData = (data, cb) => {
        bcrypt.genSalt(saltRound, (err, salt) => {
          if (err) throw new Error(err);
    
          bcrypt.hash(data.password, salt, (err, hash) => {
            if (err) throw new Error(err);
    
            // 입력 구문
            let now = new Date();
            let sql = 'INSERT INTO posts (name, email, password, subject, content, ip, created_at) VALUES (?, ?, ?, ?, ?, inet_aton(?), ?)';
            let bindParam = [
              data.name,
              data.email,
              hash, // 해싱된 비밀번호
              data.subject,
              data.content,
              data.ip,
              now
            ];
    
            // 참고사이트
            // https://www.npmjs.com/package/mysql
            // https://github.com/gnipbao/express-mvc-framework/blob/master/controllers/task.js
            // https://github.com/gnipbao/express-mvc-framework/blob/master/services/task.js
            mysqlConn.query(sql, bindParam, (err, results, fields) => {
              if (err) {
                console.error('Error code : ' + err.code);
                console.error('Error Message : ' + err.message);
    
                throw new Error(err);
              } else {
                cb(JSON.parse(JSON.stringify(results)));
              }
            });
    
            /**
             * 위 코드에서 result 의 값으로 넘어오는 것들
             *
             *  fieldCount: 0,
             *  affectedRows: 1, // 성공한 개수
             *  insertId: 2,
             *  serverStatus: 2,
             *  warningCount: 0,
             *  message: '',
             *  protocol41: true,
             *  changedRows: 0
             */
          });
        });
      };

디자인 페이지 - 현재까지의 소스(CSS 제외)

개인적으로 디자인의 디자도 모름...😂 그냥 다른 오픈소스 디자인 사이트들을 참고하고 웹서핑하다 괜찮은 사이트의 디자인을 보고 따라 끄적여 봤음😖
CSS는 공부가 끝나면 깃헙에 PUSH 하면서 같이 올릴 예정임🎃

  • app/views/layouts/main_layout.pug

      doctype html
      html(lang='ko')
        head
          meta(charset='utf-8')
          meta(name='viewport' content='width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=3.0')
    
          title= title
          link(rel='shortcut icon', href='favicon.ico', type='image/x-icon')
    
          // 폰트
          link(rel='stylesheet', href='//fonts.googleapis.com/css?family=Noto+Sans+KR:500&display=swap&subset=korean')
          link(rel='stylesheet', href='//fonts.googleapis.com/css?family=East+Sea+Dokdo&display=swap')
          link(rel='stylesheet', href='//cdn.jsdelivr.net/npm/hack-font@3.3.0/build/web/hack.css')
    
          // 기본 CSS
          link(rel='stylesheet', href='/stylesheets/style.css')
          link(rel='stylesheet', href='/stylesheets/button.css')
          link(rel='stylesheet', href='/stylesheets/form.css')
          link(rel='stylesheet', href='/stylesheets/list.css')
    
          script(src='//kit.fontawesome.com/c14d6e5016.js', crossorigin='anonymous')
    
        body#wrapper.theme-light
          block header
            include ../partials/header
          block content
  • app/views/index.pug

      extends layouts/main_layout
    
      block content
        h1= title
        p.code Welcome to #{title}
  • app/views/partials/header.pug

      header#header
        nav
          ul
            li
              a(href='/') Home
            li
              a(href='/posts') Post
  • app/posts/list.pug

      extends ../layouts/main_layout
    
      block content
        div#container
          div.board
            //div#searchDiv
            //  form(action='/posts' method='get')
            //    input(type='hidden' name='page')
            //    input.search-input(type='text' placeholder='Search..' name='search')
            div.rgt
              button.btn.green.pointer(type='button' onclick='location.href="/posts/new"') 글쓰기
            ul
              li 게시판 제목
              li
                ul.list
                  for post, index in posts
                    li
                      ul
                        li.ranking.lft
                          div.like 추천 #{post.like}
                          div.hate 반대 #{post.hate}
                          div.comment 댓글 #{post.comment_cnt}
                          div.hit 조회 #{post.hit}
                        li.content.lft
                          - let id = post.id;
                          div.title(onclick="location.href='/posts/"+id+"'") #{post.subject}
                          div.summary.ellipsis(onclick="location.href='/posts/"+id+"'") #{post.content}
                          div.tag #태그 #태그
                        li.author
                          div 글쓴이
                          div #{post.name}
                        li.date
                          div 작성일
                          div #{post.created_at}
          div.ce
            div#pagingDiv
              ul
                li
                  a(href='#') «
                  a(href='#') 1
                  a(href='#') 2
                  a.active(href='#') 3
                  a(href='#') 4
                  a(href='#') 5
                  a(href='#') »
          div.rgt
            button.btn.green.pointer(type='button' onclick='location.href="/posts/new"') 글쓰기
  • app/posts/view.pug

      extends ../layouts/main_layout
    
      block content
    
        link(rel='stylesheet', href='//cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.18.1/build/styles/an-old-hope.min.css')
        script(src='//cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.18.1/build/highlight.min.js')
        script hljs.initHighlightingOnLoad();
    
        link(rel='stylesheet', href='/stylesheets/md/modest.css')
    
        div#container
          div.row
            div.side-f
            div.main
              div
                h1 #{post.subject}
              div#markdown !{post.content}
              div
                button.btn.blue.pointer(type='button', onclick='location.href="/posts/' + post.id + '/edit"') 수정
              div 댓글 창 나올 곳
            div.side-r
  • app/views/writeForm.pug

      extends ../layouts/main_layout
    
      block content
    
        if mode === 'edit'
          - var tmp_title = '글 수정하기'
          - var tmp_action = '/posts/' + post.id + '?_method=PUT' // 필수
        else
          - var tmp_title = '글 작성하기'
          - var tmp_action = '/posts/new'
    
        h1= tmp_title
        div.ce
          form(action=tmp_action method='post')
            if mode === 'edit'
              //input(type='hidden' name='_method' value='PUT') // 있어도 되고 없어도 되고..
              input(type='hidden' name='id' value='' + post.id + '')
            div.line40
            div.post_title
              label
                textarea.textarea_title(name='subject' id='form_subject' placeholder='제목을 입력하세요.' tabindex='1') #{post.subject}
            div
              label
                textarea.textarea(name='content' id='form_content' tabindex='2') #{post.content}
            div
              label
                input.input.strong(type='text' name='tags' id='form_tags' value='' placeholder='태그1,태그2,태그3' tabindex='3')
            div
              label
                input.input(type='text' name='name' id='form_name' value='' + post.name + '' placeholder='작성자 이름' tabindex='4')
            div
              label
                input.input(type='email' name='email' id='form_email' value='' + post.email + '' placeholder='작성자 메일주소' tabindex='5')
            div
              label
                input.input(type='password' name='password' id='form_passworrd' placeholder='비밀번호 입력' tabindex='6')
            div.line20
            div.ce
              span.m5
                button.btn.green.pointer(type='submit' tabindex='7') 저 장
              span.m5
                button.btn.gray.pointer(type='button' tabindex='8') 취 소

+ Recent posts