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