Node.JS)04.26( ExpressServer - ORM( sequelize ), 게시판 )
Programming/JS

Node.JS)04.26( ExpressServer - ORM( sequelize ), 게시판 )

728x90

 

목차

     


     

    2022.04.25 - [Programming/BACKEND] - Node.JS)04.25( ExpressServer - 라우터 분리, 쿠키/세션 암호화, nunjucks(넌적스)템플릿, ORM( sequelize ) )

     

    1. ORM ( Object-Relational Mapping )

    • ORM(Object-Relational Mapping)은 객체지향 패러다임을 활용하여 관계형 데이터베이스(RDB)의 데이터를 조작하게 하는 기술
    • 이를 활용하면 쿼리를 작성하지 않고도 객체의 메서드를 활용하는 것처럼 쿼리 로직을 작성할 수 있다.
    • Sequelizes는 Node.js의 대표적인 ORM
    • Sequelize는 MySQL, PostgreSQL, MariaDB 등 많은 RDBMS를 지원하고 'Promise 기반으로 구현'되었기 때문에 비동기 로직을 편리하게 작성할 수 있다.

     

    1.1. 사전 설정

    1.1.1. mysql 스키마 생성

    mysql workbench에서 nodejs 스키마를 생성한다.
    ( Charset/Collation : utf8mb4 / utf8mb4_general_ci )

     

    1.1.2. 모듈 다운로드

    express, nunjucks, sequelize, sequelize-cli, mysql2를 받는다.

    그리고 npx sequelize init을 하면 초기설정이 실행된다.

    그럼 다음과 같이 몇 가지 폴더와 파일들이 생성된다.

     

    1.1.3. config/config.json

    "development":{ } 가 개발할 때 필요한 부분이므로, 이 부분만 다음과 같이 수정하면 된다.

    {
      "development": {
        "username": "root",
        "password": "adminuser",
        "database": "nodejs",
        "host": "127.0.0.1",
        "dialect": "mysql"
      },
      "test": {
        
        ...
        
      },
      "production": {
      
        ...
        
      }
    }

     

    1.1.4. public, routers, views 폴더 생성

    다음과 같이 3개의 폴더를 추가한다. 

    • views : html 파일에 해당하는 view가 담길 디렉토리
    • routers : 분리한 라우터가 담길 디렉토리
    • public : view에서 script 태그로 호출되는 sequelize.js가 담기는 디렉토리 

     


     

    1.2. models 파일 구성

    1.2.1. index.js

    • index.js 파일에서 반복문을 돌면서 models 폴더 내에 있는 파일들을 읽고, 그것들을 모델로 정의한다.
    • models에 js 파일로 모델을 정의 하면 원하는 테이블이 만들 수 있다.
    const Sequelize = require('sequelize');
    const env = process.env.NODE_ENV || 'development';
    const config = require(__dirname + '/../config/config.json')[env];
    const db = {};
    
    const User = require('./user');
    const Comment = require('./comment');
    
    let sequelize = new Sequelize(config.database, config.username, config.password, config);
    
    db.sequelize = sequelize;   // db에 연결하기 위한 연결객체를 db객체에 담는다.
    db.Sequelize = Sequelize;   // 현재 파일에 require한 Sequelize를 db객체에 담는다.
    // db = {sequelize:sequelize, Sequelize:Sequelize } 한다는 것
    
    // require한 user모델과 comment 모델도 db에 담는다.
    db.User = User;
    db.Comment = Comment;
    
    // '모델객체 초기화 함수'와 '관계 형성 함수'를 실행한다.
    User.init(sequelize);
    Comment.init(sequelize);
    
    User.associate(db);
    Comment.associate(db);
    
    // 여기까지의 코드가 테이블 생성 내용을 구성하는 코드
    // 이제 db가 export가 되고, app.js에 require하면 require된 db에서 sequelize를 꺼내어 sync함수를 실행하게 되고, 이때 테이블이 생성된다. 
    
    module.exports = db;

     

    1.2.2. user.js

    • users 테이블을 생성/조작하는 테이블모델
    • sequelize를 이용해서 mysql에 테이블을 생성하거나 조작할 수 있는 "테이블모델"을 만든다.
    module.exports = class User extends Sequelize.Model {
        // 테이블을 생성하고 초기화하는 함수
        static init ( sequelize ){
            return super.init({
                // 스키마 정의
                ...
            },
            {
                // 테이블 옵션 설정
                ...
            });
        }
        
        // 테이블간 관계 설정 함수
        static associate(db){
        	...
        }
    };

     

    const Sequelize = require('sequelize');
    
    // 외부에서 User를 requier하고,  User.init( Sequelize ); 로 같이 호출될 예정
    module.exports = class User extends Sequelize.Model {
        // '테이블을 생성'하고 초기화하는 함수
        static init ( sequelize ){
            return super.init({
                // init함수에 각 필드의 이름(키)과 객체속성들(값)이 매칭된 객체가 전달된다.
                // 각 필드를 객체 멤버 형식으로 나열한다.
                // 각 멤버들의 값들도 객체로 구성된다.
                name : {
                    type:Sequelize.STRING(20),
                    allowNull:false,    // null을 허용하겠는가 ( false = NOTNULL )
                    unique:false,   // unique 여부
                },
                age : {
                    type:Sequelize.INTEGER.UNSIGNED,
                    allowNull:false,
                },
                married : {
                    type:Sequelize.BOOLEAN,
                    allowNull:true,
                },
                comment : {
                    type:Sequelize.TEXT,
                    allowNull:true,
                },
                created_at : {  // 레코드의 insert 시점( 날짜 시간 )
                    type:Sequelize.DATE,
                    allowNull:true,
                    defaultValue:Sequelize.NOW,
                },
                // 첫번째 필드 (id)는 따로 기술하지 않아도 자동증가 필드로 추가된다.
            },
            
            {   // 테이블의 옵션들이 멤버형식으로 정의된다.
                sequelize,
                timestamps:false,   // 이 속성이 true면, createAt(생성시간), updateAt(수정시간) 필드가 자동생성된다.
                underscored:false,  // 이 속성이 true면, createAt(생성시간), updateAt(수정시간) 필드의 이름이 create_at, update_at으로 바뀐다.
                modelName:'User',   // sequelize가 사용할 모델(테이블) 이름
                tableName:'users',  // 데이터베이스의 자체 테이블의 이름
                paranoid:false,     // 이 멤버가 true면, deleteAt(삭제시간) 필드가 생성된다
                charset:'utf8mb4',
                collate:'utf8mb4_general_ci',
    
                // createAt(생성시간) : 레코드 insert된 시간
                // updateAt(수정시간) : 레코드update된 시간
                // deleteAt(삭제시간) : 레코드가 삭제된 시간 (실제 데이터는 삭제되지 않고 시간만 기록)
            });
        }
    
        // 테이블간 관계 설정 함수
        static associate(db){
            // User모델의 필드값이 Comment모델에 같은 필드값으로 여러번 나오도록 설정(1:N 관계) 
            db.User.hasMany(db.Comment, {foreignKey:'commenter', sourceKey:'id'} );
            // User 모델의 id필드를 Comment 모델에 commenter 필드로 복사하고, 관계설정을 한다.
        }
    };

     

    1.2.3. comment.js

    // comment.js
    const Sequelize = require('sequelize');
    
    module.exports = class Comment extends Sequelize.Model {
        static init(sequelize){
            return super.init({
                // id필드는 현재 테이블에 기본키로 자동생성(값이 자동증가)
                // 누가 댓글을 썼는지에 대한 필드 : commenter정도의 이름으로 생성될 필드
                // 외래키 설정시 역시 자동생성된다. -> users테이블의 id가 복사되어, 현재 테이블의 필드로 삽입생성된다.
                // 외래키로 설정될 필드는 따로 기술하지 않고, 외래키에서 설정함과 동시에 자동생성되도록 한다. (associate에서 설정)
                comment:{
                    type:Sequelize.STRING(100),
                    allowNull:false,
                },
                create_at:{
                    type:Sequelize.DATE,
                    allowNull:true,
                    defaultValue:Sequelize.NOW,
                },
            },
            {
                sequelize,
                timestamps:false,  
                underscored:false,  
                modelName:'Comment',   
                tableName:'comments',  
                paranoid:false, 
                charset:'utf8mb4',
                collate:'utf8mb4_general_ci',
            });
        }
        static associate(db){
            // Comment모델의 commenter필드가 User모델의 id필드를 참조하면서 복사 & 생성된다.
            db.Comment.belongsTo(db.User, {foreignKey:'commenter', sourceKey:'id'} );
        }
    };

     

    1.2.4. associate( ) ( 테이블 간 관계 설정 )

    • 한 명의 유저(user)는 여러 개의 댓글(comment)을 작성할 수 있다.
    • 따라서 user테이블과 comment테이블은 '1 : N' 관계가 된다.

    따라서 user테이블의 id를  <-  comment테이블의 id가 참조한다.

    // user table 모델
    module.exports = class User extends Sequelize.Model {
        static init ( sequelize ){
            return super.init(
            {
            	// 멤버필드 설정
            },
            {  
            	// 테이블 옵션 설정
            });
        }
    
        // 테이블간 관계 설정 함수
        static associate(db){
            // User모델의 필드값이 Comment모델에 같은 필드값으로 여러번 나오도록 설정(1:N 관계) 
            db.User.hasMany(db.Comment, {foreignKey:'commenter', sourceKey:'id'} );
            // User 모델의 id필드를 Comment 모델에 commenter 필드로 복사하고, 관계설정을 한다.
        }
    };
    
    
    // comment table 모델
    module.exports = class Comment extends Sequelize.Model {
        static init(sequelize){
            return super.init(
            {
            	// 멤버필드 설정
            },
            {  
            	// 테이블 옵션 설정
            });
        }
        // 테이블간 관계 설정 함수
        static associate(db){
            // Comment모델의 commenter필드가 User모델의 id필드를 참조하면서 복사 & 생성된다.
            db.Comment.belongsTo(db.User, {foreignKey:'commenter', sourceKey:'id'} );
        }
    };

     


     

    1.3. app.js 설정

    1.3.1. 핵심내용

    • 여기서는 'static 디렉토리 설정'과,
    • '분리된 라우터를 수집/활용'하는 방법,
    • 그리고, 'sequelize모듈로 db와 연결하는 것'이 포인트가 된다.

     

    const express = require('express');
    
    ...
    
    // static 폴더 설정 ( 여기에는 뷰에서 사용되는 sequelize.js가 위치한다 )
    app.use(express.static(path.join(__dirname, 'public')));
    
    // config.json의 내부정보로 전달하기 위한 db객체를 require한다.
    const{ sequelize } = require('./models');
    
    // 데이터베이스 연결
    // 모델 제작 후 데이터베이스 연결시 해당 모델에 매핑되는 테이블이 없으면 새로 테이블을 만들라는 옵션. (force)
    // force값이 true면 기존 테이블도 지우고 강제로 만들게 되니 주의한다.
    sequelize.sync({ force:false })
    .then(()=>{console.log('데이터베이스 연결 성공');})
    .catch((err)=>{console.error(err);});
    
    
    // 라우터들을 수집한다
    const indexRouter = require('./routers');
    const usersRouter = require('./routers/users');
    const commentsRouter = require('./routers/comments');
    
    app.use('/', indexRouter);
    app.use('/users', usersRouter);
    app.use('/comments', commentsRouter);
    
    ...

     

    1.3.2. 전체내용

    const express = require('express');
    
    const path = require('path');
    const nunjucks = require('nunjucks');
    
    const app = express(); 
    
    app.set('port', process.env.PORT || 3000);
    app.use(express.json());
    app.use(express.urlencoded({extended:false}));
    
    // nunjucks 사용환경 구성
    app.set('view engine', 'html');
    nunjucks.configure('views',{express:app, watch:true,});
    
    // static 폴더 설정 ( 여기에는 뷰에서 사용되는 sequelize.js가 위치한다 )
    app.use(express.static(path.join(__dirname, 'public')));
    
    
    // config.json의 내부정보로 전달하기 위한 db객체를 require한다.
    const{ sequelize } = require('./models');
    
    // 데이터베이스 연결
    // 모델 제작 후 데이터베이스 연결시 해당 모델에 매핑되는 테이블이 없으면 새로 테이블을 만들라는 옵션. (force)
    // force값이 true면 기존 테이블도 지우고 강제로 만들게 되니 주의한다.
    sequelize.sync({ force:false })
    .then(()=>{console.log('데이터베이스 연결 성공');})
    .catch((err)=>{console.error(err);});
    
    
    // 라우터들을 수집한다
    const indexRouter = require('./routers');
    const usersRouter = require('./routers/users');
    const commentsRouter = require('./routers/comments');
    
    app.use('/', indexRouter);
    app.use('/users', usersRouter);
    app.use('/comments', commentsRouter);
    
    /*
    app.get('/', (req,res)=>{
        res.send('<h1>안녕하세요</h1>');
    })
    */
    
    app.use((req,res,next)=>{
        const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
        error.status = 404;
        next(error);
    });
    
    app.use((err, req, res,next)=>{
        res.locals.message = res.message;
        res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
        res.status(err.status || 500);
        console.error(err);
        res.render('error');
    });
    
    app.listen(app.get('port'), ()=>{
        console.log(app.get('port'),'번 포트에서 대기중');
    });

     


     

    1.4. index.html 작성

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>index.html</title>
    </head>
    <body>
        <!-- 사용자 -->
        <div>
            <form id="user-form">
                <fieldset>
                    <legend>사용자 등록</legend>
                    <div><input name="username" type="text" placeholder="이름"> </div>
                    <div><input name="age" type="number" placeholder="나이"> </div>
                    <div><input name="married" type="checkbox" ><label for="married">결혼 여부</label> </div>
                    <button type="submit">등록</button>
                </fieldset>
            </form>
        </div>
        <table id="user-list" border="1">
            <thead><tr><th>아이디</th><th>이름</th><th>나이</th><th>결혼여부</th></tr></thead>
            <tbody>
                {% for user in users %}
                <tr><td>{{user.id}}</td><td>{{user.name}}</td><td>{{user.age}}</td>
                    <td>{{'기혼' if user.married else '미혼'}}</td></tr>
                {% endfor%}
            </tbody>
        </table><br>
    
        <!-- 댓글 -->
        <div>
            <form id="comment-form">
                <fieldset>
                    <legend>댓글 등록</legend>
                    <div><input name="userid" type="text" placeholder="사용자 아이디" </div>
                    <div><input name="comment" type="text" placeholder="댓글" </div>
                    <button type="submit">등록</button>
                </fieldset>
            </form>
        </div><br>
        <table id="comment-list" border="1">
            <thead><tr><th>아이디</th><th>작성자</th><th>댓글</th><th>수정</th><th>삭제</th></tr></thead>
            <tbody></tbody>
        </table>
        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
        <script src="/sequelize.js"></script>
    </body>
    </html>

     


     

    1.5. 라우터 분리

    // 전달되는 값들로 레코드가 추가된다.
    // 1. 레코드 삽입
    // 모델명.create({
    //    필드명1 : 입력값,
    //    필드명2 : 입력값,
    //    필드명3 : 입력값,
    // });
    
    User.create({
       name : 'hong',
       age : 24,
       married : false,
       comment : '일반회원',
    });
    
    
    // 2. 일반조회(모든필드, 모든레코드)
    // 모델명.findAll({});
    
    User.findAll({});
    
    
    // 3. 일부 필드만 조회 (select name, married from users)
    User.findAll({
       attributes:['name', 'married'],
    });
    
    
    // 4. 일부필드 & 일부 레코드(where조건) 조회 
    // ( select name, age from users where married=1 and age>30 )
    User. findAll({
       attributes:['name', 'age'],
       where:{
           married:1,
           age:{[Op.gt] : 30},
       },
    });
    // where절에 두 개의 조건이 별도의 언급없이 ','로 이어졋다면 그 둘을 and로 묶여 있는 것이다.
    
    // or를 쓰려면
    // ( select id, name from users where marreied=0 or age<30 )
    User.findAll({
       attributes:['id', 'name'],
       where:{
           [Op.or] : [{ married:1 }, { age:{ [Op.lt]:30 } }],
       },
    });
    
    
    // 5. 
    // Select id, name from users order by age desc;
    User.findAll({
         attributes:['id', 'name';],
        order:[['age', 'desc']],
    });
    
    // select id, name from users order by age desc, id asc;
    User.findAll({
        attributes:['id', 'name';],
        order:[['age', 'desc'],['id','asc']],
    });
    
    
    // 6. 수정 update users set comment = '바꿀내용' where id=2;
     User.Update({
        comment : '바꿀내용',
    },
    {
        where : {id:2},
    });
    
    
    // 7. 삭제 delete from users where id=2
    User.destroy({
        where : {id:2},
    });

     

    1.5.1. index.js

    const express = require('express');
    
    // 데이터베이스 조작(insert, update, delete, select)를 위해 require한다.
    const User = require('../models/user');     
    const Comment = require('../models/comment');
    
    const router = express.Router();
    
    router.get('/', (req,res)=>{
        res.render('index', { });    //최초 서버 실행시 첫페이지 (sequelize.html로 응답)
    });
    module.exports = router;

     

    1.5.2. users.js

    const express = require('express');
    
    const User = require('../models/user');     
    const Comment = require('../models/comment');
    
    const router = express.Router();
    
    // 사용자 등록 동작을 하는 미들웨어
    router.post('/', async (req, res, next)=>{
        try{
            const user = await User.create({
                name:req.body.name,
                age:req.body.age,
                married:req.body.married,
            });
            // console.log(user);
            res.json(user);
        }catch(err){
            console.error(err);
            next(err);  // 에러 루틴이 있는 라우터로 이동
        }
    });
    
    // 등록된 사용자를 조회하는 미들웨어
    router.get('/', async (req, res, next)=>{
        try{
            // User 객체를 통해 users 테이블의 모든 데이터 조회
            const users = await User.findAll({
    
            });
            // 결과를 json형식으로 리턴
            res.json(users);
        }catch(err){
            console.error(err);
            next(err);  //에러 루틴이 있는 라우터로 이동
        }
    });
    
    module.exports = router;

     

    1.5.3. comments.js

    const express = require('express');
    
    const User = require('../models/user');     
    const Comment = require('../models/comment');
    
    const router = express.Router();
    
    // 댓글 등록 동작하는 미들웨어
    router.post('/', async (req, res, next)=>{
        try{
            const comment = await Comment.create({
                userid:req.body.userid,
                comment:req.body.comment,
    
            });
            res.json(comment);
        }catch(err){
            console.error(err);
            next(err);
        }
    });
    
    // 등록된 댓글을 조회하는 미들웨어
    router.get('/', async (req,res,next)=>{
        try{
            const comments = await Comment.findAll({
                // join을 위해 주인공테이블과 외래키관계(1:N)관계 테이블의 모델을 include한다.
                // 이렇게 join 효과를 볼 수 있다.
                include :{
                    model:User,
                },
            });
            res.json(comments);
        }catch(err){
            console.error(err);
            next(err);
        }
    });
    
    // user테이블과 comments테이블의 조인
    // User.findAll({
    //    include:{
    //        model:comment
    //    },
    // });
    
    // 댓글 수정 라우터
    router.patch('/update/:id', async (req,res)=>{
        try{
            const result = await Comment.update({
                comment:req.body.comment,
            },
            {
                where:{ id:req.params.id },
            });
            res.json(result);
        }catch(err){
            console.error(err);
            next(err);
        }
    });
    
    // 댓글 삭제 라우터
    router.delete('/delete/:id', async(req, res)=>{
        try{
            const result = await Comment.destroy({
                where:{ id : req.params.id }
            });
            res.json(result);
        }catch(err){
            console.error(err);
            next(err);
        }
    });
    
    
    // 조건부 조회 라우터
    router.get('/search/:id', async (req, res,next)=>{
        try{
            const comments = await Comment.findAll({
                // join을 위해 주인공테이블과 외래키관계(1:N)관계 테이블의 모델을 include한다.
                // 이렇게 join 효과를 볼 수 있다.
                include :{
                    model:User,
                },
                where : {commenter : req.params.id}
            });
            res.json(comments);
    
        }catch(err){
            console.error(err);
            next(err);
        }
    } );
    module.exports = router;

     


     

    1.6. public/sequelize.js

    // index.html에 submit했을 때 반응
    getUsers();
    getComments()
    
    //회원추가 : 사용자 등록 - user-form이 submit이벤트를 일으키면 실행.
    document.getElementById('user-form').addEventListener('submit', async (e)=>{
        e.preventDefault();
    
        // 이름, 나이, 결혼여부를 변수에 저장
        const name = e.target.username.value;
        const age = e.target.age.value;
        const married = e.target.married.checked;
    
        if(!name){ return alert('이름을 입력하세요');}
        if(!age){ return alert('나이을 입력하세요');}
    
        try{
            await axios.post('/users',{name, age, married});
    
            // 레코드를 추가하고 되돌아오면, user를 모두 조회하여 user-list테이블에 모두 표시
            // user들을 조회하여 user-list에 행단위로 추가하는 함수 호출
            getUsers();
        }catch(err){
            console.error(err);
        }
        e.target.username.value = '';
        e.target.age.value = '';
        e.target.married.checked = false;
    });
    
    
    async function getUsers(){
        // 모든 user를 조회해서 user-list 테이블을 표시한다.
        try{
            // '/users'의 get 방식으로 모든 사용자 정보를 조회하고 리턴된 데이터를 res에 담는다.
            const res = await axios.get('/users');
            // 결과를 사용하기 위해 변수에 담고 데이터를 추출
            const users = res.data;
    
            const tbody = document.querySelector('#user-list tbody');
            tbody.innerHTML='';
    
            // users에 있는 user들을 하나씩 user변수(함수의 매개변수)에 넣으면서 인원 수 만큼 반복실행한다.
            users.map(function(user){
                const row = document.createElement('tr');   // tr 태그 생성
                let td = document.createElement('td');      // td 태그 생성
                td.textContent = user.id;                   // 생성된 태그에 user의 id 삽입
                row.appendChild(td);                        // tr안에 td 삽입
    
                td = document.createElement('td');
                td.textContent = user.name;
                row.appendChild(td);
                
                td = document.createElement('td');
                td.textContent = user.age;
                row.appendChild(td);
                
                td = document.createElement('td');
                td.textContent = user.married ? '기혼' : '미혼';
                row.appendChild(td);
    
    
                // row를 클릭하면 이벤트가 발생하도록 설정
                row.addEventListener('click', ()=>{
                    getCommentOne(user.id);
                });
    
    
                tbody.appendChild(row);     // 완성된 tr을 tbody에 추가
    
            });
    
        }catch(err){
    
        }
    }
    
    // 작성자를 클릭하면 해당 댓글만 표시하는 기능
    async function getCommentOne(id){
        try{
            const res = await axios.get(`/comments/search/${id}`); 
            const comments = res.data;
            const tbody = document.querySelector('#comment-list tbody');
            tbody.innerHTML = '';
            comments.map(function(comment){
                const row = document.createElement('tr');
                let td = document.createElement('td');
                td.textContent = comment.id;
                row.appendChild(td);
    
                td = document.createElement('td');
                td.textContent = comment.User.name; //comment 포함된 user모델의 필드를 표시
                row.appendChild(td),
    
                td = document.createElement('td');
                td.textContent = comment.comment;
                row.appendChild(td);
    
                // 수정버튼
                const edit = document.createElement('button');
                edit.textContent='수정';
                // 수정버튼 생성
                td = document.createElement('td');  //td버튼 생성
                td.appendChild(edit);               // 버튼을 td에 추가
                row.appendChild(td);                // 버튼이 든 td를 tr에 추가
                
                //삭제버튼
                const remove = document.createElement('button');
                remove.textContent = '삭제';
                //삭제버튼 생성
                td = document.createElement('td');
                td.appendChild(remove);
                row.appendChild(td);
    
                tbody.appendChild(row);
            });
        }catch(err){
            console.error(err);
        }
    }
    
    
    //댓글 등록 : comment-form이 submit이벤트를 일으키면 실행.
    document.getElementById('comment-form').addEventListener('submit', async (e)=>{
        e.preventDefault();
    
        const userid = e.target.userid.value;
        const comment = e.target.comment.value;
    
        if(!userid){ return alert('아이디를 입력하세요');}
        if(!comment){ return alert('댓글을 입력하세요');}
    
        try{
            await axios.post('/comments',{userid, comment});
            getComments();
        }catch(err){
            console.error(err);
        }
        e.target.userid.value = '';
        e.target.comment.value = '';
    });
    
    
    // 저장된 댓글 조회
    async function getComments(){
        try{
            const res = await axios.get('/comments');
            const comments = res.data;
            const tbody = document.querySelector('#comment-list tbody');
            tbody.innerHTML='';
    
            comments.map(function(comment){
                const row = document.createElement('tr');
                let td = document.createElement('td');
                td.textContent = comment.id;
                row.appendChild(td);
    
                td = document.createElement('td');
                td.textContent = comment.User.name; //comment 포함된 user모델의 필드를 표시
                row.appendChild(td),
    
                td = document.createElement('td');
                td.textContent = comment.comment;
                row.appendChild(td);
                
                // 수정버튼
                const edit = document.createElement('button');
                edit.textContent='수정';
                // 수정버튼 생성
                td = document.createElement('td');  //td버튼 생성
                td.appendChild(edit);               // 버튼을 td에 추가
                row.appendChild(td);                // 버튼이 든 td를 tr에 추가
                
                //삭제버튼
                const remove = document.createElement('button');
                remove.textContent = '삭제';
                //삭제버튼 생성
                td = document.createElement('td');
                td.appendChild(remove);
                row.appendChild(td);
    
                tbody.appendChild(row);
    
    
    
                // 수정버튼에 이벤트리스너 설정
                edit.addEventListener('click', async ()=>{
                    // 댓글 id와 입력받은 내용으로 comment를 수정하시고, 다시 댓글들을 검색하여 댓글을 표시해주세요.
                    const newComment = prompt('바꿀 내용을 입력하세요.');
                    if(!newComment) { return alert('내용을 반드시 입력해야 합니다.');}
    
                    try{
                        // http://localhost:3000/comments/3
                        await axios.patch(`/comments/update/${comment.id}`, {comment:newComment});
                        getComments();
    
                    }catch(err){
                        console.error(err);
                    }
                });
                
    
                // 삭제버튼 이벤트 리스너 설정
                remove.addEventListener('click', async ()=>{
                    try{
                        await axios.delete(`/comments/delete/${comment.id}`);
                        getComments();
                    }catch(err){
                        console.error(err);
                    }
                });
            });
        }catch(err){
            console.error(err);
        }
    };

     

     


     

    2. 게시판 만들기

    2.1. 사전설정

    • 해당 프로젝트로 이동 후 npm을 환경을 구성한다.

    2.1.1. npm환경 구성 및 모듈 내려받기 + sequelize 환경설정

    • npm 환경 구성 
      • npm init
      • package.json에서 scripts 속성 수정 : start": "nodemon app"
    • 모듈 내려받기
      • npm i express nunjucks nunjucks-date-filter sequelize sequelize-cli cookie-parser express-session multer
    • nodemon 설치
      • npm i -D nodemon
    • sequelize 환경 구성
      • npx sequelize init

     

    2.1.2. config/config.json 수정

    • development에서 db설정을 수정한다.

     

    2.1.3. models 구성

     

    2.1.4. routers 구성

     


     

    2.2. models 작성하기

    2.2.1. index.js

    const Sequelize = require('sequelize');
    const env = process.env.NODE_ENV || 'development';
    const config = require(__dirname + '/../config/config.json')[env];
    const db = {};
    
    let sequelize = new Sequelize(config.database, config.username, config.password, config);
    
    db.sequelize = sequelize;
    db.Sequelize = Sequelize;
    
    
    // 각 테이블에 대한 모델을 만들고, require
    const Member = require('./member');
    const Board = require('./Board');
    const Reply = require('./Reply');
    
    db.Member = Member;
    db.Board = Board;
    db.Reply = Reply;
    
    Member.init(sequelize);
    Board.init(sequelize);
    Reply.init(sequelize);
    
    Member.associate(db);
    Board.associate(db);
    Reply.associate(db);
    
    module.exports = db;

     

    2.2.2. Member.js

    // userid, pwd, name, phone, email, create_at들을 필드로 하는 모델을 만들어주세요.
    // board의 write와 member의 userid와 N:1 관계를 설정해주세요
    // 기타 설정은 board와 같은 설정이거나 맞춰 설정한다.
    
    
    // userid에 기본키설정이 되어있기 떄문에 자동으로 생성되는 id(일련번호)필드는 생성되지 않는다.
    const Sequelize = require('sequelize');
    
    module.exports = class Member extends Sequelize.Model{
        static init( sequelize ){
            return super.init({
                userid:{
                    type:Sequelize.STRING(30),
                    allowNull:false,
                    primaryKey:true,
                    unique:true,
                },
                pwd:{
                    type:Sequelize.STRING(30),
                    allowNull:false,
                }, 
                name:{
                    type:Sequelize.STRING(100),
                    allowNull:false,
                }, 
                phone:{
                    type:Sequelize.STRING(30),
                    allowNull : false,
                }, 
                email:{
                    type:Sequelize.STRING(100),
                    allowNull : true,
                }, 
                created_at:{
                    type:Sequelize.DATE,
                    allowNull : true,
                    defaultValue:Sequelize.NOW,
                },
            },
            {
                sequelize,
                timestamps:false,   // 이 속성이 true면, createAt(생성시간), updateAt(수정시간) 필드가 자동생성된다.
                modelName:'Member',   // sequelize가 사용할 모델(테이블) 이름
                tableName:'members',  // 데이터베이스의 자체 테이블의 이름
                paranoid:false,     // 이 멤버가 true면, deleteAt(삭제시간) 필드가 생성된다
                charset:'utf8mb4',
                collate:'utf8mb4_general_ci',
            });
        }
        static associate(db){
            db.Member.hasMany(db.Board, {foreignKey:'writer', sourceKey:'userid', onDelete:'cascade'})
        }
    };

     

    2.2.3. Board.js

    const Sequelize = require('sequelize');
    
    module.exports = class Board extends Sequelize.Model{
        static init( sequelize ){
            return super.init({
                subject:{
                    type:Sequelize.STRING(100),
                    allowNull:false,
                }, 
                content:{
                    type:Sequelize.STRING(1000),
                    allowNull:false,
                }, 
                readCount:{
                    type:Sequelize.INTEGER.UNSIGNED,
                    allowNull:false,
                    defaultValue:0,
                }, 
                created_at:{
                    type:Sequelize.DATE,
                    defaultValue:Sequelize.NOW,
                    allowNull:true,
                }, 
                filename:{
                    type:Sequelize.STRING(100),
                    allowNull:true,
                }, 
                realfilename:{
                    type:Sequelize.STRING(100),
                    allowNull:true,
                },
            },
            {
                sequelize,
                timestamps:false,   // 이 속성이 true면, createAt(생성시간), updateAt(수정시간) 필드가 자동생성된다.
                modelName:'Board',   // sequelize가 사용할 모델(테이블) 이름
                tableName:'boards',  // 데이터베이스의 자체 테이블의 이름
                paranoid:false,     // 이 멤버가 true면, deleteAt(삭제시간) 필드가 생성된다
                charset:'utf8mb4',
                collate:'utf8mb4_general_ci',
            });
        }
        static associate(db){
            db.Board.belongsTo(db.Member, {foreignKey:'writer', targetKey:'userid'})
            // 해당 게시글이 지워지면 해당 boardnum인 댓글이 같이 지워짐 (onDelete:'cascade')
            db.Board.hasMany(db.Reply, {foreignKey:'boardnum', sourceKey:'id', onDelete:'cascade' })
        }
    };

     

    2.2.4. Reply.js

    const Sequelize = require('sequelize');
    
    module.exports = class Reply extends Sequelize.Model{
        static init( sequelize ){
            return super.init({
                writer:{
                    type:Sequelize.STRING(30),
                    allowNull:false,
                },
                content:{
                    type:Sequelize.STRING(200),
                    allowNull:false,
                },
                created_at:{
                    type:Sequelize.DATE,
                    defaultValue:Sequelize.NOW,                
                    allowNull:false,
                }
            },
            {
                sequelize,
                timestamps:false,   // 이 속성이 true면, createAt(생성시간), updateAt(수정시간) 필드가 자동생성된다.
                modelName:'Reply',   // sequelize가 사용할 모델(테이블) 이름
                tableName:'replys',  // 데이터베이스의 자체 테이블의 이름
                paranoid:false,     // 이 멤버가 true면, deleteAt(삭제시간) 필드가 생성된다
                charset:'utf8mb4',
                collate:'utf8mb4_general_ci',
            });
        }
        static associate(db){
            db.Reply.belongsTo(db.Board, {foreignKey:'boardnum', targetKey:'id'});
        }
    };

     


     

    2.3. app.js 설정

    const express = require('express');
    
    // 추가모듈
    const nunjucks = require('nunjucks');
    const path = require('path');
    const cookieParser = require('cookie-parser');
    const session = require('express-session');
    // import * as deteFilter from 'nunjucks-date-filter';
    const dateFilter = require('nunjucks-date-filter'); //넌적스에서 사용할 날짜 양식 필터 사용을 위한 모듈
    
    
    const app = express();
    app.set('port', process.env.PORT || 3000);
    app.use(express.json());
    app.use(express.urlencoded({extended:false}));
    
    // static
    app.use(express.static(path.join(__dirname, 'public')));
    
    // nunjucks 템플릿 설정
    app.set('view engine', 'html');
    let env = nunjucks.configure('views', { express:app, watch:true,});
    env.addFilter('date', dateFilter);
    
    // 쿠키, 세션 설정
    app.use(cookieParser());
    app.use(session({
        resave:false,
        saveUninitialized:false,
        secret:'dlrjsgml',
    }));
    
    // sequelize 설정
    const{ sequelize } = require('./models');   // config전달을 위한 db객체 require
    sequelize.sync({force:false})
    .then(()=>{console.log('데이터베이스 연결 성공');})
    .catch((err)=>{
        console.error(err);
    });
    
    // 분리된 라우터 수집
    const indexRouter = require('./routers');
    const memberRouter = require('./routers/members');
    const boardRouter = require('./routers/boards');
    
    // 라우팅
    app.use('/', indexRouter);
    app.use('/members', memberRouter);
    app.use('/boards', boardRouter);
    
    
    // 에러처리 미들웨어
    app.use((req, res, next)=>{
        const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
        error.status = 404;
        next(error);
    });
    
    app.use((err, req, res,next)=>{
        res.locals.message = res.message;
        res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
        res.status(err.status || 500);
        res.render('error');
    });
    
    
    app.listen(app.get('port'), ()=>{
        console.log(app.get('port'),'번 포트에서 대기중');
    });

     


    2.4. views & public

    2.4.1. views

    2.4.1.1. views/login.html

    axios를 내려받고, static인 public디렉토리의 login.js가 호출된다.

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>login.html</title>
    </head>
    <body>
        <div>
            <form id="login-form">
                <fieldset>
                    <legend>Login</legend>
                    <div><input id="userid" type="text" placeholder="userid"></div>
                    <div><input id="pwd" type="password" placeholder="password"></div>
                    <button type="submit">로그인</button>
                    <input type="button" onclick="location.href='/members/joinform'" value="회원가입">
                    <span id="msg"></span>
                </fieldset>
            </form>
        </div>
        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
        <script src="/login.js"></script>
    </body>
    </html>

     

    2.4.1.2. views/main.html

    axios를 내려받고, static인 public디렉토리의 main.js가 호출된다.

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>main.html</title>
        <!-- public디렉토리에 css 추가함 -->
        <link rel="stylesheet" type="text/css" href="/board.css">
        <style>
            #boardnum{text-align: center;}
            #readCount{text-align: center;}
            #writer{text-align: center;}
        </style>
    </head>
    <body>
        {{lUser.userid}}({{lUser.name}})님 어서오세요
        <input type="button" value="회원수정" onclick="location.href='/members/updateForm/{{lUser.userid}}'">
        <input type="button" value="로그아웃" onclick="location.href='/members/logout/logout'">
        <input type="button" value="글쓰기" onclick="location.href='/members/boards/writeForm'"><br><br>
        <table id="board-list" align="left">
            <thead>
                <tr><th width="100">번호</th><th width="400">제목</th><th width="100">작성자</th><th width="100">조회수</th></tr>
            </thead>
            <tbody><tr><td></td><td></td><td></td></tr></tbody>
        </table>
        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
        <script src="/main.js"></script>
    </body>
    </html>

     

    2.4.2. public

    2.4.2.1. login.js

    document.getElementById('login-form').addEventListener('submit', async (e)=>{
        e.preventDefault();
        const userid = e.target.userid.value;
        const pwd = e.target.pwd.value;
        if(!userid){ return alert("아이디를 입력하셈");}
        if(!pwd){ return alert("비번을 입력하셈");} // 여기까지는 작동함
        
        try{
            const res = await axios.post('/members/login', {userid,pwd});
            const mem = res.data;
    
            let m = document.getElementById('msg');
            if(mem == null){
                m.innerHTML = '아이디가 없습니다';
            }else if(mem.pwd != pwd){
                m.innerHTML = '비밀번호를 확인하세요';
            }else if(mem.pwd == pwd){
                location.href = '/boards';
            }else{
                m.innerHTML = '알 수 없는 오류입니다.';
            }
        }catch(err){
    
        }
    })

     

    2.4.2.2. main.js

    main으로 이동시 모든 게시물을 조회하는 동작.

    getBoard_list();
    
    // 데이터베이스에서 게시물들을 읽어와 table의 tbody와 tr과 td로 삽입하는 함수
    async function getBoard_list(){
        try{
            const res = await axios.get('/boards/boardLilst');
            const boards = res.data;
    
            //테이블의 tbody 안을 비운다.
            const tbody = document.querySelector('#board-list tbody');
            tbody.innerHTML='';
    
            boards.map( async function(board){
                const row = document.createElement('tr');
    
                let td = document.createElement('td');
                td.textContent = board.id;
                td.id = 'boardnum';
                row.append(td);
                
                td = document.createElement('td');
                let tContent = board.subject;
                td.innerHTML = tContent;
                row.appendChild(td);
    
                td = document.createElement('td');
                td.textContent = board.writer;
                td.id = 'writer';
                row.appendChild(td);
    
                td = document.createElement('td');
                td.textContent = board.readCount;
                td.id = 'readCount';
                row.appendChild(td);
    
                tbody.appendChild(row);
            })
        }catch(err){
    
        }
    }

     


    2.5. routers

    2.5.1. index.js

    const express = require('express');
    const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
    const Board = require('../models/board');
    const Reply = require('../models/reply');
    const router = express.Router();
    
    // login.html을 표시하는 동작
    router.get('/', (req, res, next)=>{
        try{
            res.render('login',{});
        }catch(err){
            console.error(err);
            next(err);
        }
    });
    
    
    module.exports = router;

     

    2.5.2. members.js

    const express = require('express');
    const Member = require('../models/member');
    const Board = require('../models/board');
    const Reply = require('../models/reply');
    const router = express.Router();
    
    // 로그인 동작
    router.post('/login', async (req, res, next)=>{
        try{
            const luser = await Member.findOne({
                // 전달된 아이디와 같은 레코드 검색 후 luser변수에 저장
                where:{userid : req.body.userid },
            });
            if( (luser != null) && (luser.pwd == req.body.pwd) ){
                req.session.loginUser = luser;
            }
            res.json(luser);
        }catch(err){
            console.error(err);
            next(err);
        }
    });
    
    module.exports = router;

     

    2.5.3. boards.js

    const express = require('express');
    const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
    const Board = require('../models/board');
    const Reply = require('../models/reply');
    const router = express.Router();
    
    // 로그인 후 보여질 main화면 (main.html)
    router.get('/', (req,res)=>{
        const loginUser = req.session.loginUser;
        res.render('main', {lUser:loginUser});
    });
    
    router.get('/boardList',(req,res)=>{
        
    })
    
    module.exports = router;

     


     

    3. 기능 추가

    3.1. 회원가입 폼으로 이동

    3.1.1. views/login.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>login.html</title>
    </head>
    <body>
        <div>
            <form id="login-form">
                <fieldset>
                    <legend>Login</legend>
                    <div><input id="userid" type="text" placeholder="userid"></div>
                    <div><input id="pwd" type="password" placeholder="password"></div>
                    <button type="submit">로그인</button>
                    <input type="button" onclick="location.href='/members/joinform'" value="회원가입">
                    <span id="msg"></span>
                </fieldset>
            </form>
        </div>
        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
        <script src="/login.js"></script>
    </body>
    </html>

     

    3.1.2. routers/members.js - router.get('/joinform')

    const express = require('express');
    const Member = require('../models/member');
    const Board = require('../models/board');
    const Reply = require('../models/reply');
    const router = express.Router();
    
    ...
    
    router.get('/joinform', (req, res, next)=>{
        res.render('memberInsert',{});
    });
    
    module.exports = router;

     

    3.1.3. views/memberInsert.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" type="text/css" href="/board.css">
        <title>memberInsert.html</title>
    </head>
    <body>
        <div id="wrap">
            <form id="join-form">
                <fieldset>
                    <legend>회원가입 - 사용자 등록</legend>
                    <div><input id="userid" type="text" placeholder="userid"> </div>
                    <div><input id="pwd" type="password" placeholder="password"> </div>
                    <div><input id="name" type="text" placeholder="name"> </div>
                    <div><input id="phone" type="text" placeholder="phone"> </div>
                    <div><input id="email" type="text" placeholder="email"> </div>
                    <button type="submit">등록완료</button>
                </fieldset>
            </form>
        </div>
        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
        <script src="/insertmember.js"></script>
    </body>
    
    </html>

     


     

    3.2. 회원가입 동작

    3.2.1. views/memberInsert.html

    <!DOCTYPE html>
    
    ...
    
        <link rel="stylesheet" type="text/css" href="/board.css">
        <title>memberInsert.html</title>
    </head>
    <body>
        <div id="wrap">
            <form id="join-form">
                <fieldset>
                    <legend>회원가입 - 사용자 등록</legend>
                    <div><input id="userid" type="text" placeholder="userid"> </div>
                    <div><input id="pwd" type="password" placeholder="password"> </div>
                    <div><input id="name" type="text" placeholder="name"> </div>
                    <div><input id="phone" type="text" placeholder="phone"> </div>
                    <div><input id="email" type="text" placeholder="email"> </div>
                    <button type="submit">등록완료</button>
                </fieldset>
            </form>
        </div>
        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
        <script src="/insertmember.js"></script>
    </body>
    
    </html>

     

    3.2.2. public/insertmember.js

    document.getElementById('join-form').addEventListener('submit', async (e)=>{
        e.preventDefault();
    
        // 폼에서 입력된 값을 변수에 담기
        const userid = e.target.userid.value;
        const pwd = e.target.pwd.value;
        const name = e.target.name.value;
        const phone = e.target.phone.value;
        const email = e.target.email.value;
    
        // 유효성검사
        if(!userid) { return alert('아이디를 입력하세요');}
        if(!pwd) { return alert('비밀번호를 입력하세요');}
        if(!name) { return alert('이름을 입력하세요');}
        if(!phone) { return alert('전화번호를 입력하세요');}
        if(!email) { return alert('이메일을 입력하세요');}
    
        // axios를 활용해 라우터로 값 전달 (insert라서 리턴받는 변수 X)
        try{
            await axios.post('/members/insertMember',{userid,pwd,name,phone,email});
            location.href='/';
        }catch(err){
    
        }
    });

     

    3.2.3. routers/members.js - router.post('/insertMember')

    const express = require('express');
    const Member = require('../models/member');
    const Board = require('../models/board');
    const Reply = require('../models/reply');
    const router = express.Router();
    
    ...
    
    router.post('/insertMember', async (req,res,next)=>{
        try{
            await Member.create({
                userid:req.body.userid,
                pwd:req.body.pwd,
                name:req.body.name,
                phone:req.body.phone,
                email:req.body.email,
            });
            res.end();
        }catch(err){
            console.error(err);
            next(err);
        }
    });
    
    module.exports = router;

     


     

    3.3. 회원수정 폼으로 이동

    3.3.1. views/main.html

    <!DOCTYPE html>
    
    ...
    
        <title>main.html</title>
        <style>
            #boardnum{text-align: center;}
            #readCount{text-align: center;}
            #writer{text-align: center;}
        </style>
    </head>
    <body>
        <div id="wrap">
            {{lUser.userid}}({{lUser.name}})님 어서오세요 &nbsp;
            <input type="button" value="회원수정" onClick="location.href='/members/updateForm/{{lUser.userid}}'">
            <input type="button" value="로그아웃" onClick="location.href='/members/logout'">
            <input type="button" value="글쓰기" onClick="location.href='/boards/writeForm/'"><br><br>
            
            ...
            
            <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
            <script src="/main.js"></script>
        </div>
    </body>
    </html>

     

    3.3.2. routers/members.js - router.get('/updateForm/:userid')

    const express = require('express');
    const Member = require('../models/member');
    const Board = require('../models/board');
    const Reply = require('../models/reply');
    const router = express.Router();
    
    ...
    
    router.get('/updateForm/:userid', async (req,res,next)=>{
        // userid로 검색해서 검색 결과를 member라는 이름으로 같이 memberUpdateForm.html로 전송 이동.
        try{
            const member = await Member.findOne({
                where : { userid : req.params.userid, }
            });
            res.render('memberUpdateForm', {member});
        }catch(err){
            console.error(err);
            next(err);
        }
    });
    
    module.exports = router;

     

    3.3.3. views/memberUpdateForm.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>memberUpdateForm.html</title>
        <link rel="stylesheet" type="text/css" href="/board.css">
    </head>
    <body>
        <div id="wrap">
            <form id="memberUpdate">
                <fieldset><legend>회원정보 수정</legend>
                    <div>사용자 아이디 : {{member.userid}}
                        <input id="userid" type="hidden" value="{{member.userid}}"></div>
                    <div>비밀번호 : <input id="pwd" type="password" placeholder="password"></div>
                    <div>이름 : <input id="name" type="text" value="{{member.name}}"></div>
                    <div>전화번호 : <input id="phone" type="text" value="{{member.phone}}"></div>
                    <div>이메일 : <input id="email" type="text" value="{{member.email}}"></div>
                    <input type="submit" value="수정완료">
                    <input type="button" value="취소" onclick="location.href='/'">
                </fieldset>
            </form>
            <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
            <script src="/updatemember.js"></script>
        </div>
    </body>
    </html>

     


     

    3.4. 회원수정 동작

    3.4.1. views/memberUpdateForm.html

    <!DOCTYPE html>
    
    ...
    
        <title>memberUpdateForm.html</title>
        <link rel="stylesheet" type="text/css" href="/board.css">
    </head>
    <body>
        <div id="wrap">
            <form id="memberUpdate">
                <fieldset><legend>회원정보 수정</legend>
                    <div>사용자 아이디 : {{member.userid}}
                        <input id="userid" type="hidden" value="{{member.userid}}"></div>
                    <div>비밀번호 : <input id="pwd" type="password" placeholder="password"></div>
                    <div>이름 : <input id="name" type="text" value="{{member.name}}"></div>
                    <div>전화번호 : <input id="phone" type="text" value="{{member.phone}}"></div>
                    <div>이메일 : <input id="email" type="text" value="{{member.email}}"></div>
                    <input type="submit" value="수정완료">
                    <input type="button" value="취소" onclick="location.href='/'">
                </fieldset>
            </form>
            <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
            <script src="/updatemember.js"></script>
        </div>
    </body>
    </html>

     

    3.4.2. public/updatemember.js

    document.getElementById('memberUpdate').addEventListener('submit', async (e)=>{
        e.preventDefault();
    
        // 폼에서 입력된 값을 변수에 담기
        const userid = e.target.userid.value;
        const pwd = e.target.pwd.value;
        const name = e.target.name.value;
        const phone = e.target.phone.value;
        const email = e.target.email.value;
    
        // 유효성검사
        if(!pwd) { return alert('비밀번호를 입력하세요');}
        if(!name) { return alert('이름을 입력하세요');}
        if(!phone) { return alert('전화번호를 입력하세요');}
        if(!email) { return alert('이메일을 입력하세요');}
    
        // axios를 활용해 라우터로 값 전달 후 리턴값 변수에 담기
        try{
            await axios.post('/members/update', {userid,pwd,name,phone,email});
            location.href='/boards';
        }catch(err){
    
        }
    });

     

    3.4.3. routers/members.js - router.post('/update')

    const express = require('express');
    const Member = require('../models/member');
    const Board = require('../models/board');
    const Reply = require('../models/reply');
    const router = express.Router();
    
    ...
    
    router.post('/update', async(req, res, next)=>{
        try{
            // 회원정보 수정
            const result = await Member.update({    // result에는 변경된 건수가 담김
                pwd:req.body.pwd,
                name:req.body.name,
                phone:req.body.phone,
                email:req.body.email,
            },
            {
                where : { userid : req.body.userid, }
            });
    
            // 회원정보 조회하여 세션 저장
            const member = await Member.findOne({
                where : { userid : req.body.userid, }
            });
            req.session.loginUser = member;
            res.json(member);   // 종료의 의미 ( 보내는 것에는 크게 의미없음. )
    
        }catch(err){
            console.error(err);
            next(err);
        }
    });
    
    module.exports = router;

     


     

    3.5. 게시글 작성 폼으로 이동

    3.5.1. views/main.html

    <!DOCTYPE html>
    
    ...
    
    </head>
    <body>
        <div id="wrap">
            {{lUser.userid}}({{lUser.name}})님 어서오세요 &nbsp;
            <input type="button" value="회원수정" onClick="location.href='/members/updateForm/{{lUser.userid}}'">
            <input type="button" value="로그아웃" onClick="location.href='/members/logout'">
            <input type="button" value="글쓰기" onClick="location.href='/boards/writeForm/'"><br><br>
            
            ...
            
            <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
            <script src="/main.js"></script>
        </div>
    </body>
    </html>

     

    3.5.2. routers/boards.js - router.get('/writeForm')

    multer, path, cookie, session 등 모듈을 import/setting한다.

    const express = require('express');
    const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
    const Board = require('../models/board');
    const Reply = require('../models/reply');
    
    // 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
    const multer = require('multer');
    const fs = require('fs');
    const path = require('path');
    
    const router = express.Router();
    
    // 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
    // upload폴더를 public에 넣는 이유는, public이 static이기 때문
    try{
        fs.readdirSync('public/upload');
    }catch(error){
        console.error('upload폴더가 없어 upload폴더를 생성');
        fs.mkdirSync('public/upload');
    }
    
    // multer객체 설정
    const upload = multer({
        storage: multer.diskStorage({
            destination(req,file,done){
                done(null, 'public/upload/');
            },
            filename(req,file,done){
                const ext = path.extname(file.originalname);
                done(null, path.basename(file.originalname,ext)+Date.now() + ext);
            },
        }),
        limits:{fileSize:5*1024*1024},
    });
    
    ...
    
    // 게시글 등록 폼으로 이동
    router.get('/writeForm', (req, res, next)=>{
        const luser = req.session.loginUser;
        res.render('writeForm', {luser});
    });
    
    module.exports = router;

     

    3.5.3. views/writeForm.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>writeForm.html</title>
        <link rel="stylesheet" type="text/css" href="/board.css">
    </head>
    <body>
        <div id="wrap">
            <form id="write-form">
                <h2 style="text-align:center;">게시글 등록</h2>
                <table id="board-list">
                    <tr><th width="100">제목</th>
                        <td width="600">&nbsp;<input type="text" id="subject" size="95"></td>
                    </tr>
                    <tr><th width="100">작성자</th>
                        <td width="600">&nbsp;{{luser.userid}}
                            <input type="hidden" id="userid" value="{{luser.userid}}"></td>
                    </tr>
                    <tr><th width="100">내용</th>
                        <td width="600">&nbsp;<textarea id="text" rows="10" cols="95"></textarea></td>
                    </tr>
                    <tr><th width="100">이미지</th>
                        <td width="500">&nbsp;<input type='file' name="image" id="image" /></td>
                    </tr>                
                    <tr height="80"><td align="center" colspan="2">
                        <button type="submit">글쓰기</button>
                        <input type="button" value="돌아가기" onClick="location.href='/boards'"/></td>
                    </tr>
                </table><br>
            </form>
        </div>
        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
        <script src="/boardWrite.js"></script>
    </body>
    </html>


     

    3.6. 게시글 등록

    3.6.1. views/writeForm.html

    <!DOCTYPE html>
    
    ...
    
    </head>
    <body>
        <div id="wrap">
            <form id="write-form">
                <h2 style="text-align:center;">게시글 등록</h2>
                <table id="board-list">
                    <tr><th width="100">제목</th>
                        <td width="600">&nbsp;<input type="text" id="subject" size="95"></td>
                    </tr>
                    <tr><th width="100">작성자</th>
                        <td width="600">&nbsp;{{luser.userid}}
                            <input type="hidden" id="userid" value="{{luser.userid}}"></td>
                    </tr>
                    <tr><th width="100">내용</th>
                        <td width="600">&nbsp;<textarea id="text" rows="10" cols="95"></textarea></td>
                    </tr>
                    <tr><th width="100">이미지</th>
                        <td width="500">&nbsp;<input type='file' name="image" id="image" /></td>
                    </tr>                
                    <tr height="80"><td align="center" colspan="2">
                        <button type="submit">글쓰기</button>
                        <input type="button" value="돌아가기" onClick="location.href='/boards'"/></td>
                    </tr>
                </table><br>
            </form>
        </div>
        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
        <script src="/boardWrite.js"></script>
    </body>
    </html>

     

    3.6.2. public/boardWrite.js

    document.getElementById('write-form').addEventListener('submit', async (e)=>{
        e.preventDefault();
        const subject = e.target.subject.value;
        const writer = e.target.userid.value;
        const text = e.target.text.value;
        if(!subject){return alert('제목을 입력히셈');}
        if(!text){return alert('내용을 입력히셈');}
    
        const formData = new FormData();
        formData.append('image', e.target.image.files[0]);
        formData.append('subject', subject);
        formData.append('writer', writer);
        formData.append('text', text);
    
        try {
            await axios.post('/boards/writeBoard', formData)
            location.href = '/boards';
        } catch (err) {
            console.error(err);
        }
    });

     

    3.6.3. routers/boards.js - router.post('/writeBoard')

    const express = require('express');
    const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
    const Board = require('../models/board');
    const Reply = require('../models/reply');
    
    // 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
    const multer = require('multer');
    const fs = require('fs');
    const path = require('path');
    
    const router = express.Router();
    
    // 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
    // upload폴더를 public에 넣는 이유는, public이 static이기 때문
    try{
        fs.readdirSync('public/upload');
    }catch(error){
        console.error('upload폴더가 없어 upload폴더를 생성');
        fs.mkdirSync('public/upload');
    }
    
    // multer객체 설정
    const upload = multer({
        storage: multer.diskStorage({
            destination(req,file,done){
                done(null, 'public/upload/');
            },
            filename(req,file,done){
                const ext = path.extname(file.originalname);
                done(null, path.basename(file.originalname,ext)+Date.now() + ext);
            },
        }),
        limits:{fileSize:5*1024*1024},
    });
    
    ...
    
    // 게시글 등록 동작
    router.post('/writeBoard', upload.single('image'), async (req,res,next)=>{
        // 파일 업로드와 게시글 insert를 완성. 
        // filename필드명에는 서버에 저장되는 파일명(현재날짜와 시간이 밀리초로 변환된 값 + 파일명
        // realfilename 필드명에는 원래 파일명
        try {
            const board = await Board.create({
                subject : req.body.subject,
                writer : req.body.writer,
                content : req.body.text,
                filename : req.file.originalname,
                realfilename : req.file.filename,
            });
            res.json(board);
        } catch (err) {
            console.error(err);
            next(err);
        }
    });
    
    module.exports = router;

     


     

    3.7. 게시글 보기

    3.7.1. views/main.html

    <!DOCTYPE html>
    
    ...
    
    </head>
    <body>
        <div id="wrap">
            
            ...
            
            <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
            <script src="/main.js"></script>
        </div>
    </body>
    </html>

     

    3.7.2. public/main.js 

    게시글 행 내부가 시작되는 부분에 클릭이벤트를 추가한다.

    getBoard_list();
    
    // 데이터베이스에서 게시물들을 읽어와 table의 tbody와 tr과 td로 삽입하는 함수
    async function getBoard_list(){
        try{
            const res = await axios.get('/boards/boardList');
            const boards = res.data;
    
            //테이블의 tbody 안을 비운다.
            const tbody = document.querySelector('#board-list tbody');
            tbody.innerHTML='';
    
            boards.map( async function(board){
                const row = document.createElement('tr');
    
                // 게시글 클릭시 boardView로 이동
                row.addEventListener('click',()=>{
                    location.href="/boards/boardView/"+board.id;
                });
    
                let td = document.createElement('td');
                td.textContent = board.id;
                td.id = 'boardnum';
                row.append(td);
                
                td = document.createElement('td');
                let tContent = board.subject;
                td.innerHTML = tContent;
                row.appendChild(td);
    
                ...
    
                tbody.appendChild(row);
            });
        }catch(err){
    
        }
    }

     

    3.7.3. routers/boards.js - router.get('/boardView/:id')

    const express = require('express');
    const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
    const Board = require('../models/board');
    const Reply = require('../models/reply');
    
    // 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
    const multer = require('multer');
    const fs = require('fs');
    const path = require('path');
    
    const router = express.Router();
    
    // 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
    // upload폴더를 public에 넣는 이유는, public이 static이기 때문
    try{
        fs.readdirSync('public/upload');
    }catch(error){
        console.error('upload폴더가 없어 upload폴더를 생성');
        fs.mkdirSync('public/upload');
    }
    
    // multer객체 설정
    const upload = multer({
        storage: multer.diskStorage({
            destination(req,file,done){
                done(null, 'public/upload/');
            },
            filename(req,file,done){
                const ext = path.extname(file.originalname);
                done(null, path.basename(file.originalname,ext)+Date.now() + ext);
            },
        }),
        limits:{fileSize:5*1024*1024},
    });
    
    ...
    
    // 게시물 보기
    router.get('/boardView/:id', async (req, res,next)=>{
        try {
            // 게시물 검색
            const result = await Board.findOne({
                attributes:['readCount'],
                where:{id:req.params.id},
            });
            
            // 검색한 게시물의 조회수를 추출하여 +1 연산
            const cnt = result.readCount + 1;
            
            // 조회수 연산결과를 게시물에 update
            await Board.update({
                readCount:cnt,
            },
            {
                where:{id:req.params.id},
            });
            
            // 게시물을 재검색
            const board = await Board.findOne({
                where:{id:req.params.id},
            });
    
            // render로 전송
            const luser = req.session.loginUser;
            const dt = new Date();
            res.render('boardView', {board, luser, dt});
    
        } catch (err) {
            console.error(err);
            next(err);
        }
    });
    
    module.exports = router;

     

    3.7.4. views/boardView.html

    댓글을 조회하는 스크립트가 내부에 있다.

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>boardView.html</title>
        <link rel="stylesheet" type="text/css" href="/board.css">
        <style>
            #time{text-align: center;}
            #writer{text-align: center;}
            #remove{text-align: center;}
        </style>
    </head>
    <body>
        <div id="wrap">
            <h1 style="text-align: center;">게시글 내용 </h1>
            <table>
                <tr>
                    <th width="150" align="cneter">번호</th>
                    <td width="200" align="cneter">{{board.id}}
                        <input type="hidden" id="boardnum" value="{{board.id}}">
                    </td>
                    <th width="150" align="cneter">작성자</th>
                    <td width="200" align="cneter">{{board.writer}}</td>
                </tr>
                <tr>
                    <th align="center">작성일</th>
                    <td align="center">{{board.create_at | date("YYYY/MM/DD")}}</td>
                    <th align="center">조회수</th>
                    <td align="center">{{board.readCount}}</td>
                <tr>
                    <th align="center" width="150">제목</th>
                    <td colspan="3">&nbsp;{{board.subject}}</td>
                </tr>
                </tr>
                <tr height="300"><th align="center" width="150">내용</th>
                    <td colspan="2" height="300" width="300"><pre>{{board.content}}</pre><br></td>
                    <td width="150" align="center">
                        {% if board.filename %}
                            <img src="/upload/{{board.realfilename}}" width="150"/>
                        {% endif %}
                    </td>
                </tr>
                <tr height="50"><td colspan="4" align="center">
                    <input type="button" value="수정" onclick="location.href='/boards/UpdateForm/{{board.id}}'"/>
                    <input type="button" value="삭제" onclick="location.href='/boards/deleteBoard/{{board.id}}'"/>
                    <input type="button" value="돌아가기" onclick="location.href='/boards/'"/>
                </td></tr>
            </table><br><br>
    
            <form id="reply-list">
                <table>
                    <thead>
                        <tr>
                            <th width="140" align="center">작성일시</th>
                            <th width="90" align="center">작성자</th>
                            <th width="400" align="center">내용</th>
                            <th width="70" align="center">&nbsp;</th>
                        </tr>
                        <tr>
                            <td align="center">{{dt | date("MM/DD HH:mm")}}</td>
                            <td align="center">{{luser.userid}}
                                <input type="hidden" id="writer" value="{{luser.userid}}"></td>
                            <input type="hidden" id="boardnum" value="{{board.id}}">
                            <td>&nbsp;<input type="text" size="50" id="reply"></td>
                            <td align="center"><input type="submit" value="작성"></td>
                        </tr>
                    </thead>
                    <tbody><tr><td></td><td></td><td></td><td></td></tr></tbody>
                </table>
            </form>
            <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
            <script type="text/javascript">
                getReplys('{{board.id}}','{{luser.userid}}');
    
                async function getReplys(boardnum, loginUser){
                    try {
                        const result = await axios.get( `/boards/replyList/${boardnum}`);
                        const replys = result.data;
                        const tbody = document.querySelector('#reply-list tbody');
                        tbody.innerHTML='';
                        
                        replys.map(function(reply){
                            const row = document.createElement('tr');
    
                            let td = document.createElement('td');
                            td.textContent = reply.created_at.substring(5,7) + "/" 
                                + reply.created_at.substring(8,10) + " " 
                                + reply.created_at.substring(11,13) + ":" 
                                + reply.created_at.substring(14,16);
                            td.id = 'time';
                            row.append(td);
    
                            td=document.createElement('td')
                            td.textContent = reply.writer;
                            td.id = 'writer';
                            row.appendChild(td);
                            
                            td = document.createElement('td');
                            td.textContent = reply.content;
                            td.id = 'content';
                            row.appendChild(td);
    
                            const remove1 = document.createElement('input');
                            remove1.setAttribute('type', 'button'); 
                            remove1.value = '삭제';
                            
                            td = document.createElement('td');
                            td.id = 'remove';
                            if(reply.writer == loginUser){ 
                                td.appendChild(remove1);
                            } else {
                                td.innerHTML = '&nbsp;';
                            }
                            row.appendChild(td);
    
                            tbody.appendChild(row);
                        });
                    } catch (err) {
                        console.error(err);
                    }
                }
    
                
            </script>
        </div>
    </body>
    </html>

     

    3.7.5. routers/boards.js - router.get('replyList/:boardnum')

    const express = require('express');
    const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
    const Board = require('../models/board');
    const Reply = require('../models/reply');
    
    // 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
    const multer = require('multer');
    const fs = require('fs');
    const path = require('path');
    
    const router = express.Router();
    
    // 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
    // upload폴더를 public에 넣는 이유는, public이 static이기 때문
    try{
        fs.readdirSync('public/upload');
    }catch(error){
        console.error('upload폴더가 없어 upload폴더를 생성');
        fs.mkdirSync('public/upload');
    }
    
    // multer객체 설정
    const upload = multer({
        storage: multer.diskStorage({
            destination(req,file,done){
                done(null, 'public/upload/');
            },
            filename(req,file,done){
                const ext = path.extname(file.originalname);
                done(null, path.basename(file.originalname,ext)+Date.now() + ext);
            },
        }),
        limits:{fileSize:5*1024*1024},
    });
    
    ...
    
    // 게시물의 댓글들 조회
    router.get('/replyList/:boardnum', async (req, res, next)=>{
        try {
            const replys = await Reply.findAll({
                where:{boardnum:req.params.boardnum},
                order:[['id','DESC']],
            });
            res.json(replys);
        } catch (err) {
            console.error(err);
            next(err);
        }
    });
    
    module.exports = router;

     


     

    3.8. 댓글 작성

    3.8.1. views/boardView.html

    <!DOCTYPE html>
    
    ...
    
    </head>
    <body>
        <div id="wrap">
            <h1 style="text-align: center;">게시글 내용 </h1>
            <table>
                
                ...
                
            </table><br><br>
    
            <form id="reply-list">
                <table>
                    <thead>
                        <tr>
                            <th width="140" align="center">작성일시</th>
                            <th width="90" align="center">작성자</th>
                            <th width="400" align="center">내용</th>
                            <th width="70" align="center">&nbsp;</th>
                        </tr>
                        <tr>
                            <td align="center">{{dt | date("MM/DD HH:mm")}}</td>
                            <td align="center">{{luser.userid}}
                                <input type="hidden" id="writer" value="{{luser.userid}}"></td>
                            <input type="hidden" id="boardnum" value="{{board.id}}">
                            <td>&nbsp;<input type="text" size="50" id="reply"></td>
                            <td align="center"><input type="submit" value="작성"></td>
                        </tr>
                    </thead>
                    <tbody><tr><td></td><td></td><td></td><td></td></tr></tbody>
                </table>
            </form>
            <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
            <script type="text/javascript">
                
                    ...
                    
                document.getElementById('reply-list').addEventListener('submit', async (e)=>{
                    e.preventDefault();
                    const writer = e.target.writer.value;
                    const boardnum = e.target.boardnum.value;
                    const reply = e.target.reply.value;
    
                    if(!reply){return alert('댓글내용을 입력하세요');}
    
                    try{
                        await axios.post('/boards/addReply', {writer, boardnum, reply});
                        getReplys(boardnum, writer);
                    }catch(err){
                        console.error(err);
                    }
                    e.target.reply.value='';
                });
            </script>
        </div>
    </body>
    </html>

     

    3.8.2. routers/boards.js - router.post('/addReply')

    const express = require('express');
    const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
    const Board = require('../models/board');
    const Reply = require('../models/reply');
    
    // 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
    const multer = require('multer');
    const fs = require('fs');
    const path = require('path');
    
    const router = express.Router();
    
    // 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
    // upload폴더를 public에 넣는 이유는, public이 static이기 때문
    try{
        fs.readdirSync('public/upload');
    }catch(error){
        console.error('upload폴더가 없어 upload폴더를 생성');
        fs.mkdirSync('public/upload');
    }
    
    // multer객체 설정
    const upload = multer({
        storage: multer.diskStorage({
            destination(req,file,done){
                done(null, 'public/upload/');
            },
            filename(req,file,done){
                const ext = path.extname(file.originalname);
                done(null, path.basename(file.originalname,ext)+Date.now() + ext);
            },
        }),
        limits:{fileSize:5*1024*1024},
    });
    
    ...
    
    // 댓글 작성
    router.post('/addReply', async (req, res, next)=>{
        try {
            await Reply.create({
                writer:req.body.writer,
                content:req.body.reply,
                boardnum:req.body.boardnum,
            });
            res.end();
        } catch (err) {
            console.error(err);
            next(err);
        }
    });
    
    module.exports = router;

     


     

    3.9. 댓글 삭제

    3.9.1. views/boardView.html

    <!DOCTYPE html>
    
    ...
    
    </head>
    <body>
        <div id="wrap">
            <h1 style="text-align: center;">게시글 내용 </h1>
            <table>
                
                ...
                
            </table><br><br>
    
            <form id="reply-list">
                <table>
                    <thead>
                        <tr>
                            <th width="140" align="center">작성일시</th>
                            <th width="90" align="center">작성자</th>
                            <th width="400" align="center">내용</th>
                            <th width="70" align="center">&nbsp;</th>
                        </tr>
                        <tr>
                            <td align="center">{{dt | date("MM/DD HH:mm")}}</td>
                            <td align="center">{{luser.userid}}
                                <input type="hidden" id="writer" value="{{luser.userid}}"></td>
                            <input type="hidden" id="boardnum" value="{{board.id}}">
                            <td>&nbsp;<input type="text" size="50" id="reply"></td>
                            <td align="center"><input type="submit" value="작성"></td>
                        </tr>
                    </thead>
                    <tbody><tr><td></td><td></td><td></td><td></td></tr></tbody>
                </table>
            </form>
            <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
            <script type="text/javascript">
                getReplys('{{board.id}}','{{luser.userid}}');
    
                async function getReplys(boardnum, loginUser){
                    try {
                        const result = await axios.get( `/boards/replyList/${boardnum}`);
                        const replys = result.data;
                        const tbody = document.querySelector('#reply-list tbody');
                        tbody.innerHTML='';
                        
                        replys.map(function(reply){
                            const row = document.createElement('tr');
    
                            let td = document.createElement('td');
                            td.textContent = reply.created_at.substring(5,7) + "/" 
                                + reply.created_at.substring(8,10) + " " 
                                + reply.created_at.substring(11,13) + ":" 
                                + reply.created_at.substring(14,16);
                            td.id = 'time';
                            row.append(td);
    
                            td=document.createElement('td')
                            td.textContent = reply.writer;
                            td.id = 'writer';
                            row.appendChild(td);
                            
                            td = document.createElement('td');
                            td.textContent = reply.content;
                            td.id = 'content';
                            row.appendChild(td);
    
                            const remove1 = document.createElement('input');
                            remove1.setAttribute('type', 'button'); 
                            remove1.value = '삭제';
                            
                            // 삭제 버튼 클릭시 이벤트
                            remove1.addEventListener('click', async ()=>{
                                try {
                                    await axios.delete(`/boards/deleteReply/${reply.id}`);
                                    getReplys(boardnum, loginUser);
                                } catch (err) {
                                    console.error(err);
                                }
                            });
    
                            
                            td = document.createElement('td');
                            td.id = 'remove';
                            if(reply.writer == loginUser){ 
                                td.appendChild(remove1);
                            } else {
                                td.innerHTML = '&nbsp;';
                            }
                            row.appendChild(td);
    
                            tbody.appendChild(row);
                        });
                    } catch (err) {
                        console.error(err);
                    }
                }
                
                    ...
                    
            </script>
        </div>
    </body>
    </html>

     

    3.9.2. routers/boards.js - router.delete('/deleteReply/:id')

    const express = require('express');
    const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
    const Board = require('../models/board');
    const Reply = require('../models/reply');
    
    // 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
    const multer = require('multer');
    const fs = require('fs');
    const path = require('path');
    
    const router = express.Router();
    
    // 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
    // upload폴더를 public에 넣는 이유는, public이 static이기 때문
    try{
        fs.readdirSync('public/upload');
    }catch(error){
        console.error('upload폴더가 없어 upload폴더를 생성');
        fs.mkdirSync('public/upload');
    }
    
    // multer객체 설정
    const upload = multer({
        storage: multer.diskStorage({
            destination(req,file,done){
                done(null, 'public/upload/');
            },
            filename(req,file,done){
                const ext = path.extname(file.originalname);
                done(null, path.basename(file.originalname,ext)+Date.now() + ext);
            },
        }),
        limits:{fileSize:5*1024*1024},
    });
    
    ...
    
    // 댓글 삭제
    router.delete('/deleteReply/:id', async (req, res, next)=>{
        try {
            const result = await Reply.destroy({
                where:{id:req.params.id}
            });
            res.json(result);
        } catch (err) {
            console.error(err);
            next(err);
        }
    });
    
    module.exports = router;

    삭제한거임;

     


     

    3.10. 게시글 제목에 댓글count 표시하기

    3.10.1. views/main.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        
        ...
        
    </head>
    <body>
        <div id="wrap">
            {{lUser.userid}}({{lUser.name}})님 어서오세요 &nbsp;
            <input type="button" value="회원수정" onClick="location.href='/members/updateForm/{{lUser.userid}}'">
            <input type="button" value="로그아웃" onClick="location.href='/members/logout'">
            <input type="button" value="글쓰기" onClick="location.href='/boards/writeForm/'"><br><br>
            <table id="board-list" align="left">
                <thead>
                    <tr><th width="100">번호</th><th width="400">제목</th><th width="100">작성자</th><th width="100">조회수</th></tr>
                </thead>
                <tbody><tr><td></td><td></td><td></td></tr></tbody>
            </table>
            <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
            <script src="/main.js"></script>
        </div>
    </body>
    </html>

     

    3.10.2. public/main.js 

    getBoard_list();
    
    // 데이터베이스에서 게시물들을 읽어와 table의 tbody와 tr과 td로 삽입하는 함수
    async function getBoard_list(){
        try{
            const res = await axios.get('/boards/boardList');
            const boards = res.data;
    
            //테이블의 tbody 안을 비운다.
            const tbody = document.querySelector('#board-list tbody');
            tbody.innerHTML='';
    
            boards.map( async function(board){
                const row = document.createElement('tr');
    
                // 게시글 클릭시 boardView로 이동
                row.addEventListener('click',()=>{
                    location.href="/boards/boardView/"+board.id;
                });
    
                let td = document.createElement('td');
                td.textContent = board.id;
                td.id = 'boardnum';
                row.append(td);
                
                td = document.createElement('td');
    
                // 현재 게시물의 댓글 개수를 조회해 제목 옆에 표시한다.
                // 갯수 : 조회된 객체.length
                let tContent = board.subject;
                
                try {
                    const result = await axios.get(`/boards/replycnt/${board.id}`);
                    const data = result.data;
                    //let cnt = data.length;
                    let cnt = data.cnt;
                    if(cnt!=0){
                        tContent = tContent + ' <span style="color:red; font-weight:bold">['+ cnt +']</span>';
                    }
                } catch (err) {
                    console.error(err);
                }
    
                td.innerHTML = tContent;
                row.appendChild(td);
    
                td = document.createElement('td');
                td.textContent = board.writer;
                td.id = 'writer';
                row.appendChild(td);
    
                td = document.createElement('td');
                td.textContent = board.readCount;
                td.id = 'readCount';
                row.appendChild(td);
    
                tbody.appendChild(row);
            });
        }catch(err){
    
        }
    }

     

    3.10.3. routers/boards.js - router.get('/replycnt/:id')

    const express = require('express');
    const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
    const Board = require('../models/board');
    const Reply = require('../models/reply');
    
    // 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
    const multer = require('multer');
    const fs = require('fs');
    const path = require('path');
    
    const router = express.Router();
    
    // 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
    // upload폴더를 public에 넣는 이유는, public이 static이기 때문
    try{
        fs.readdirSync('public/upload');
    }catch(error){
        console.error('upload폴더가 없어 upload폴더를 생성');
        fs.mkdirSync('public/upload');
    }
    
    // multer객체 설정
    const upload = multer({
        storage: multer.diskStorage({
            destination(req,file,done){
                done(null, 'public/upload/');
            },
            filename(req,file,done){
                const ext = path.extname(file.originalname);
                done(null, path.basename(file.originalname,ext)+Date.now() + ext);
            },
        }),
        limits:{fileSize:5*1024*1024},
    });
    
    ...
    
    // 댓글개수 조회
    router.get('/replycnt/:id', async (req, res, next)=>{
        try {
            const result = await Reply.findAll({
                where:{boardnum:req.params.id},
            });
            //res.json(result);
            res.json({cnt:result.length})
        } catch (err) {
            console.error(err);
            next(err);
        }
    });
    
    module.exports = router;


     

    3.10. 게시물 수정 폼으로 이동

    3.10.1. boardView.html / html부분

    <!DOCTYPE html>
    
    ...
    
    </head>
    <body>
        <div id="wrap">
            <h1 style="text-align: center;">게시글 내용 </h1>
            <table>
                <tr>
                    <th width="150" align="cneter">번호</th>
                    <td width="200" align="cneter">{{board.id}}
                        <input type="hidden" id="boardnum" value="{{board.id}}">
                    </td>
                    <th width="150" align="cneter">작성자</th>
                    <td width="200" align="cneter">{{board.writer}}</td>
                </tr>
                <tr>
                    <th align="center">작성일</th>
                    <td align="center">{{board.create_at | date("YYYY/MM/DD")}}</td>
                    <th align="center">조회수</th>
                    <td align="center">{{board.readCount}}</td>
                <tr>
                    <th align="center" width="150">제목</th>
                    <td colspan="3">&nbsp;{{board.subject}}</td>
                </tr>
                </tr>
                <tr height="300"><th align="center" width="150">내용</th>
                    <td colspan="2" height="300" width="300"><pre>{{board.content}}</pre><br></td>
                    <td width="150" align="center">
                        {% if board.filename %}
                            <img src="/upload/{{board.realfilename}}" width="150"/>
                        {% endif %}
                    </td>
                </tr>
                <tr height="50"><td colspan="4" align="center">
                    <input type="button" value="수정" onclick="location.href='/boards/updateForm/{{board.id}}'"/>
                    <input type="button" value="삭제" onclick="location.href='/boards/deleteBoard/{{board.id}}'"/>
                    <input type="button" value="돌아가기" onclick="location.href='/boards/'"/>
                </td></tr>
            </table><br><br>
    
            <form id="reply-list">
            
                ...
                
            </form>
            <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
            <script type="text/javascript">
                
                ...
                
            </script>
        </div>
    </body>
    </html>

     

    3.10.2. boards.js - router.get( '/updateForm/:id' )

    const express = require('express');
    const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
    const Board = require('../models/board');
    const Reply = require('../models/reply');
    
    // 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
    const multer = require('multer');
    const fs = require('fs');
    const path = require('path');
    
    const router = express.Router();
    
    // 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
    // upload폴더를 public에 넣는 이유는, public이 static이기 때문
    try{
        fs.readdirSync('public/upload');
    }catch(error){
        console.error('upload폴더가 없어 upload폴더를 생성');
        fs.mkdirSync('public/upload');
    }
    
    // multer객체 설정
    const upload = multer({
        storage: multer.diskStorage({
            destination(req,file,done){
                done(null, 'public/upload/');
            },
            filename(req,file,done){
                const ext = path.extname(file.originalname);
                done(null, path.basename(file.originalname,ext)+Date.now() + ext);
            },
        }),
        limits:{fileSize:5*1024*1024},
    });
    
    ...
    
    // 게시물 수정폼으로 이동
    router.get('/updateForm/:id', async (req,res,next)=>{
        // 전달된 아이디로 게시물을 조회한 후 updateForm.html로 렌더링, 
        // 세션에 있는 '유저아이디'와 조회한 게시물이 같이 이동한다.
        try{
            const board = await Board.findOne({
                where:{id:req.params.id},
            });
            const luser = req.session.loginUser;    // loginUser 세션을 가져와 저장
            res.render('updateForm',{board, luser});
        }catch(err){
            console.error(err);
            next(err);
        }
    });
    
    module.exports = router;

     

    3.10.3. updateForm.html / html부분

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <link rel="stylesheet" type="text/css" href="/board.css">
    </head>
    <body>
        <form id ="update-form">
            <table>
                <tr><th width="150" align="center">번호</th>
                    <td width="150" align="center">{{board.id}} <input type="hidden" name="id" value="{{board.id}}"/> </td><th width="150" align="center">작성자</th>
                    <td width="150" align="center">{{board.writer}} <input type="hidden" name="writer" value="{{board.writer}}"/> </td></tr>
                <tr><th width="150" align="center">작성일</th>
                    <td width="150" align="center">{{board.created_at | date("YYYY/MM/DD")}}</td>
                    <th align="center">조회수</th> <td align="center">{{board.readCount}}</td></tr>
                <tr><th align="center" width="150">제목</th><td width="150" colspan="3">&nbsp;<input type="text" name="subject" size="50" value="{{board.subject}}"> </td>
                <tr><th align="center" width="150">내용</th><td width="150" colspan="3">&nbsp;<textarea name="text" rows="15" cols="60">{{board.content}}</textarea> </td></tr>
                <tr><th align="center" width="150">이미지</th><td colspan="3">&nbsp;<img src="/upload/{{board.realfilename}}" width="50"/><br><input type="file" name="image" id="image"/><br>이미지를 수정하고자 할 때만 선택하세요 </td></tr>
                <tr height="80"><td colspan="4" align="center">
                    <input type="submit" value="수정">
                    <input type="button" value="돌아가기" onclick="location.href='/boards/boardView2/{{board.id}}'"></td></tr>
                </tr>
            </table>
        </form>
        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
        <script type="text/javascript">
            
            ...
            
        </script>
    </body>
    </html>

     


     

    3.11. 게시물 수정 동작

    3.11.1. updateForm.html / html 부분

    <!DOCTYPE html>
    
    ...
    
    </head>
    <body>
        <form id ="update-form">
            <table>
                <tr><th width="150" align="center">번호</th>
                    <td width="150" align="center">{{board.id}} <input type="hidden" name="id" value="{{board.id}}"/> </td><th width="150" align="center">작성자</th>
                    <td width="150" align="center">{{board.writer}} <input type="hidden" name="writer" value="{{board.writer}}"/> </td></tr>
                <tr><th width="150" align="center">작성일</th>
                    <td width="150" align="center">{{board.created_at | date("YYYY/MM/DD")}}</td>
                    <th align="center">조회수</th> <td align="center">{{board.readCount}}</td></tr>
                <tr><th align="center" width="150">제목</th><td width="150" colspan="3">&nbsp;<input type="text" name="subject" size="50" value="{{board.subject}}"> </td>
                <tr><th align="center" width="150">내용</th><td width="150" colspan="3">&nbsp;<textarea name="text" rows="15" cols="60">{{board.content}}</textarea> </td></tr>
                <tr><th align="center" width="150">이미지</th><td colspan="3">&nbsp;<img src="/upload/{{board.realfilename}}" width="50"/><br><input type="file" name="image" id="image"/><br>이미지를 수정하고자 할 때만 선택하세요 </td></tr>
                <tr height="80"><td colspan="4" align="center">
                    <input type="submit" value="수정">
                    <input type="button" value="돌아가기" onclick="location.href='/boards/boardView2/{{board.id}}'"></td></tr>
                </tr>
            </table>
        </form>
        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
        <script type="text/javascript">
            
            ...
            
        </script>
    </body>
    </html>

     

    3.11.2. updateForm.html / script 태그 부분

    <!DOCTYPE html>
    
    ...
    
    </head>
    <body>
        <form id ="update-form">
            
            ...
            
        </form>
        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
        <script type="text/javascript">
            document.getElementById('update-form').addEventListener('submit', async (e)=>{
                e.preventDefault();
                const id = e.target.id.value;
                const subject = e.target.subject.value;
                const text = e.target.text.value;
    
                if(!subject){return alert('제목을 입력하세요');}
                if(!text){return alert('내용을 입력하세요');}
    
                const formData = new FormData();
                formData.append('id', id);  //게시글 아이디(번호)
                formData.append('subject', subject);
                formData.append('text', text);
                formData.append('image', e.target.image.files[0]);
                try{
                    await axios.post('/boards/update', formData);
                    // boardView2는 조회수 카운트를 안하는 라우터
                    location.href='/boards/boardView2/{{board.id}}';
                }catch(err){
                    
                }
            });
        </script>
    </body>
    </html>

     

    3.11.3. boards.js - router.post( '/update' )

    조회수를 올리지 않고, 게시물을 조회하는 라우터, 이 라우터가 수행을 완료하면 다시 await했던 스크립트로 돌아가고, 다음 줄의 코드를 수행한다. 

    const express = require('express');
    const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
    const Board = require('../models/board');
    const Reply = require('../models/reply');
    
    // 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
    const multer = require('multer');
    const fs = require('fs');
    const path = require('path');
    
    const router = express.Router();
    
    // 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
    // upload폴더를 public에 넣는 이유는, public이 static이기 때문
    try{
        fs.readdirSync('public/upload');
    }catch(error){
        console.error('upload폴더가 없어 upload폴더를 생성');
        fs.mkdirSync('public/upload');
    }
    
    // multer객체 설정
    const upload = multer({
        storage: multer.diskStorage({
            destination(req,file,done){
                done(null, 'public/upload/');
            },
            filename(req,file,done){
                const ext = path.extname(file.originalname);
                done(null, path.basename(file.originalname,ext)+Date.now() + ext);
            },
        }),
        limits:{fileSize:5*1024*1024},
    });
    
    ...
    
    // 게시물 수정
    router.post('/update', upload.single('image'), async (req,res,next)=>{
        try {
            if(req.file != undefined){
                // 업로드된 파일이 있다면 (undefined가 아니라면)
                await Board.update({
                    subject:req.body.subject,
                    content:req.body.text,
                    filename:req.file.originalname,
                    realfilename:req.file.filename,
                },{
                    where : {id:req.body.id},
                });
            }else{
                // 업로드된 파일이 없다면 (undefined라면)
                await Board.update({
                    subject:req.body.subject,
                    content:req.body.text,
                },{
                    where : {id:req.body.id},
                });
            }
            res.end();
            // axios를 사용했기 때문에 redirect코드는 의미 없음
            // res.redirect('/boards/boardView2/'+ req.body.id);
            
        } catch (err) {
            console.error(err);
            next(err);
        }
    });
    
    module.exports = router;

     

    3.11.4. boards.js - router.get( '/boardView2/:id' )

    • update 라우터를 완료하고 다음 줄 코드인 location.href='/boards/boardView2/{{board.id}}';로 이 라우터가 동작한다.
    • 게시물을 조회하지만 조회수를 올리지 않는 라우터이다.
    const express = require('express');
    const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
    const Board = require('../models/board');
    const Reply = require('../models/reply');
    
    // 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
    const multer = require('multer');
    const fs = require('fs');
    const path = require('path');
    
    const router = express.Router();
    
    // 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
    // upload폴더를 public에 넣는 이유는, public이 static이기 때문
    try{
        fs.readdirSync('public/upload');
    }catch(error){
        console.error('upload폴더가 없어 upload폴더를 생성');
        fs.mkdirSync('public/upload');
    }
    
    // multer객체 설정
    const upload = multer({
        storage: multer.diskStorage({
            destination(req,file,done){
                done(null, 'public/upload/');
            },
            filename(req,file,done){
                const ext = path.extname(file.originalname);
                done(null, path.basename(file.originalname,ext)+Date.now() + ext);
            },
        }),
        limits:{fileSize:5*1024*1024},
    });
    
    ...
    
    // 게시물 보기 ( 조회수 카운트X )
    router.get('/boardView2/:id', async (req, res,next)=>{
        try {
            // 게시물을 검색
            const board = await Board.findOne({
                where:{id:req.params.id},
            });
    
            // render로 전송
            const luser = req.session.loginUser;
            const dt = new Date();
            res.render('boardView', {board, luser, dt});
    
        } catch(err) {
            console.error(err);
            next(err);
        }
    });
    
    module.exports = router;

     

    3.12. 게시물 Write 동작 수정

    게시물 작성 동작 시 이미지 업로드를 하지 않을 때 처리부분 추가

    3.12.1. boards.js - router.post('/writeBoard')

    • request에서 넘겨받은 file이 'undefined(정의되지 않음)'가 아니라면 ( = 업로드한 파일이 있다면 )
      • filename, realfilename을 포함하여 데이터를 db에 담는다.
    • request에서 넘겨받은 file이 'undefined(정의되지 않음)'라면 ( = 업로드한 파일이 없다면 ) 
      • filename, realfilename을 제외하고 db에 담는다.
    // 게시글 등록 동작
    router.post('/writeBoard', upload.single('image'), async (req,res,next)=>{
        // 파일 업로드와 게시글 insert를 완성. 
        // filename필드명에는 서버에 저장되는 파일명(현재날짜와 시간이 밀리초로 변환된 값 + 파일명
        // realfilename 필드명에는 원래 파일명
        try {
            if(req.file != undefined){
                const board = await Board.create({
                    subject : req.body.subject,
                    writer : req.body.writer,
                    content : req.body.text,
                    filename : req.file.originalname,
                    realfilename : req.file.filename,
                });
                res.json(board);
    
            }else{
                const board = await Board.create({
                    subject : req.body.subject,
                    writer : req.body.writer,
                    content : req.body.text,
                });
                res.json(board);
            }
        } catch (err) {
            console.error(err);
            next(err);
        }
    });

     


     

    3.13. 게시물 삭제 동작

    3.13.1. boardView.html / html 부분

    <!DOCTYPE html>
    
    ...
    
    </head>
    <body>
        <div id="wrap">
            <h1 style="text-align: center;">게시글 내용 </h1>
            <table>
                <tr>
                    <th width="150" align="cneter">번호</th>
                    <td width="200" align="cneter">{{board.id}}
                        <input type="hidden" id="boardnum" value="{{board.id}}">
                    </td>
                    <th width="150" align="cneter">작성자</th>
                    <td width="200" align="cneter">{{board.writer}}</td>
                </tr>
                <tr>
                    <th align="center">작성일</th>
                    <td align="center">{{board.create_at | date("YYYY/MM/DD")}}</td>
                    <th align="center">조회수</th>
                    <td align="center">{{board.readCount}}</td>
                <tr>
                    <th align="center" width="150">제목</th>
                    <td colspan="3">&nbsp;{{board.subject}}</td>
                </tr>
                </tr>
                <tr height="300"><th align="center" width="150">내용</th>
                    <td colspan="2" height="300" width="300"><pre>{{board.content}}</pre><br></td>
                    <td width="150" align="center">
                        {% if board.filename %}
                            <img src="/upload/{{board.realfilename}}" width="150"/>
                        {% endif %}
                    </td>
                </tr>
                <tr height="50"><td colspan="4" align="center">
                    <input type="button" value="수정" onclick="location.href='/boards/updateForm/{{board.id}}'"/>
                    <input type="button" value="삭제" onclick="location.href='/boards/deleteBoard/{{board.id}}'"/>
                    <input type="button" value="돌아가기" onclick="location.href='/boards/'"/>
                </td></tr>
            </table><br><br>
    
            <form id="reply-list">
            
                ...
                
            </form>
            <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
            <script type="text/javascript">
                
                ...
                
            </script>
        </div>
    </body>
    </html>

     

    3.13.2. boards.js - router.get( '/deleteBoard/:id' )

    const express = require('express');
    const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
    const Board = require('../models/board');
    const Reply = require('../models/reply');
    
    // 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
    const multer = require('multer');
    const fs = require('fs');
    const path = require('path');
    
    const router = express.Router();
    
    // 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
    // upload폴더를 public에 넣는 이유는, public이 static이기 때문
    try{
        fs.readdirSync('public/upload');
    }catch(error){
        console.error('upload폴더가 없어 upload폴더를 생성');
        fs.mkdirSync('public/upload');
    }
    
    // multer객체 설정
    const upload = multer({
        storage: multer.diskStorage({
            destination(req,file,done){
                done(null, 'public/upload/');
            },
            filename(req,file,done){
                const ext = path.extname(file.originalname);
                done(null, path.basename(file.originalname,ext)+Date.now() + ext);
            },
        }),
        limits:{fileSize:5*1024*1024},
    });
    
    ...
    
    // 게시물 삭제
    router.get('/deleteBoard/:id', async (req,res,next)=>{
        try {
            await Board.destroy({
                where:{id:req.params.id},
            });
            res.redirect('/boards');
        } catch (err) {
            console.error(err);
            next(err);
        }
    });
    
    module.exports = router;

     

    300x250