본문 바로가기

블록체인_9기/과제

36강_220523_Node.js(게시판 페이지 만들기 총 복습)

728x90

 

 

 

 


미션내용

미션 내용은 다음과 같다.

 

 


기획과정

기획은 간단하게 구성하였다.

데이터베이스 테이블 생성, 페이지 생성 두 과정을 정리한 후 제작에 들어갔다.

데이터베이스 테이블 생성

Post 테이블

  • 게시글이 담길 테이블이다.
  • 구성
    • id : 테이블 생성 시 자동으로 생성될 고유키가 담길 컬럼
    • title: 게시글의 제목이 담길 컬럼
    • content: 게시글의 내용이 담길 컬럼
    • writer: 게시글의 작성자가 담길 칼럼
      • User 테이블의 id값을 참조할 컬럼이다.

User 테이블

  • 유저의 정보가 담길 테이블이다.
  • 구성
    • id : 테이블 생성 시 자동으로 생성될 고유키가 담길 컬럼
    • user_id : 유저의 id값이 담길 칼럼
    • user_pw : 유저의 pw값이 담길 칼럼
    • user_name : 유저의 닉네임이 들어갈 칼럼 
    • user_level : 유저 레벨이 들어갈 칼럼
      • 0: 로그인 불가
      • 1: 로그인 가능, 글 작성 권한
      • 2: 로그인 가능, 글, 댓글 작성 권한
      • 3: 어드민 계정. 관리자 페이지로 이동하여 회원정보 관리

Cmt 테이블

  • 댓글이 담길 테이블
  • 구성
    • id : 테이블 생성 시 자동으로 생성될 고유키가 담길 컬럼
    • text : 작성된 댓글의 내용이 담길 칼럼
    • cmtwriter : 댓글의 작성자가 담길 칼럼
      • User 테이블의 id값을 참조할 컬럼이다.

 

 

페이지 생성

1. 로그인 페이지

  • 로그인 기능

1-1. 회원가입 페이지

  • a태그로 이동
  • 회원가입 기능. 성공 시 로그인 페이지로 이동

❗ 로그인, 회원가입 페이지를 제외한 모든 페이지는 로그인 상태여야 렌더가 가능하다.


2-1. 게시글 목록 페이지

  • 로그인 성공 시 이동
  • 전체 게시글 조회 기능
    • 각 게시글의 제목, 작성자 글번호 조회가능
  • 새로운 게시글 추가 가능

2-1-1. 게시글 상세 페이지

  • 게시글 한 개의 제목, 작성자, 내용, 댓글을 조회할 수 있다.
  • 자신의 게시글을 들어가면 수정, 삭제가 활성화 된다.

2-1-2. 마이 페이지

  • 수정: user_name을 수정할 수 있다.
  • 해당 유저가 작성한 글을 확인할 수 있다.
  • 해당 유저가 작성하 댓글을 확인할 수 있다.


2-2. 관리자 페이지

  • 전체 유저 목록을 확인할 수 있다.
  • 유저를 삭제할 수 있다.
  • 유저의 등급을 설정할 수 있다.

 

 

 


제작과정

제작 순서 보다는 파일 단위로 과정을 설명하고자 한다.

#. 폴더의 경로는 다음과 같다.

 

1. app.js_ session을 사용하기 위한 설정, sequelize 연결 매핑 설정, 라우터 연결 등을 수행한다.

  • 사용한 모듈
    • express, express-session, bcrypt, ejs, dotenv, jsonwebtoken, sequelize
  • session을 사용하기 위한 설정
const session = require("express-session");

app.use(session({
    secret : process.env.ACCESS_TOKEN_KEY,
    resave : false,
    saveUninitialized : false
}))
  • sequelize 연결 매핑 설정
const { sequelize } = require("./models");

sequelize.sync({force: false})
.then((e)=>{
    console.log("sequelize 연결설공");
}).catch((err)=>{
    console.log(err);
})
  • 라우터 연결
    • 경로설정이 익숙치 않아 코드가 길어졌다... 다음에는 간결하게 하도록하자!
const signUpRouter = require("./routers/signUp");
const loginRouter = require("./routers/login");
const PostListRouter = require("./routers/postList");
const adminRouter = require("./routers/admin");
const insertRouter = require("./routers/insert");
const viewRouter = require("./routers/view");
const deleteRouter = require("./routers/delete");
const updateRouter = require("./routers/update");
const userUpdateRouter = require("./routers/userUpdate");
const userDeleteRouter = require("./routers/userDelete");



app.use('/signUp',signUpRouter);
app.use('/login',loginRouter);
app.use('/postList',PostListRouter);
app.use('/admin',adminRouter);
app.use('/insert',insertRouter);
app.use('/view',viewRouter);
app.use('/delete',deleteRouter);
app.use('/update',updateRouter);
app.use('/userUpdate',userUpdateRouter);
app.use('/userDelete',userDeleteRouter);

2. .env_DATABASE의 정보키를 작성하고, SESSION_KEY와 ACCESS_TOKEN_KEY를 작성한다.

ACCESS_TOKEN_KEY = myaccesstoken

DATABASE_USERNAEM = root
DATABASE_PASSWORD = 000000
DATABASE_NAME = toy_postpage2
DATABASE_HOST = 127.0.0.1

3. config/index.js_sequelize 객체 생성 시 들어갈 데이터를 설정한다.

 

const config = {
    dev: {
        username : process.env.DATABASE_USERNAEM,
        password : process.env.DATABASE_PASSWORD,
        database : process.env.DATABASE_NAME,
        host : process.env.DATABASE_HOST,
        dialect : "mysql" 
    }
}

module.exports = config;

4. models / users.js_sequelize의 User의 모델을 설정한다.

  • hasMany 메서드를 이용하여 Post 테이블의 참조키 user_id와 Cmt 테이블의 참조키 user_num에 User테이블의 id 칼럼을 참조시킨다.
const Sequelize = require("sequelize");

class User extends Sequelize.Model {
    static init(sequelize){
        return super.init({
            // 컬럼의 내용
            user_id : {
                type : Sequelize.STRING(20),
                allowNull : false,
                unique : true
            },
            user_pw : {
                type: Sequelize.STRING(64),
                allowNull: false
            },
            user_name : {
                type: Sequelize.STRING(20),
                allowNull: false
            },
            user_level : {
                type: Sequelize.STRING(10)
            }
        }, {
            // 테이블의 내용
            sequelize,
            timestamps : true,
            underscored : false,
            modelName: "User",
            tableName: "users",
            paranoid : false,
            charset : "utf8",
            collate: "utf8_general_ci"

        });
    }

    static associate (db){
        db.User.hasMany(db.Post, { foreignKey : "user_id", sourceKey : "id" });
        db.User.hasMany(db.Cmt, { foreignKey : "user_num", sourceKey : "id" });
    }
}

module.exports = User;

5. models / posts.js_sequelize의 Post의 모델을 설정한다.

  • belongsTo 메서드를 이용하여 참조키 user_id에 User테이블의 id 칼럼을 참조한다.
  • hasMany 메서드를 이용하여 Cmt 테이블의 참조키 post_num에 Post테이블의 id 칼럼을 참조시킨다.
const Sequelize = require("sequelize");

class Post extends Sequelize.Model {
    static init(sequelize) {
        return super.init({
            title: {
                type: Sequelize.STRING(20),
                allowNull: false
            },
            content: {
                type: Sequelize.STRING(300),
                allowNull: false
            },
            writer: {
                type: Sequelize.STRING(20),
                allowNull: false
            }
        }, {
            sequelize,
            timestamps: true,
            modelName: "Post",
            tableName: "posts",
            paranoid: false,
            charset: "utf8",
            collate: "utf8_general_ci"
        })
    }
    static associate(db){
        db.Post.belongsTo(db.User, {foreignKey : "user_id", targetKey: "id"});
        db.Post.hasMany(db.Cmt, { foreignKey : "post_num", sourceKey : "id" });
    }
}

module.exports = Post;

6. models / comment.js_sequelize의 Cmt의 모델을 설정한다.

  • belongsTo 메서드를 이용하여 참조키 user_num에 User테이블의 id 칼럼을 참조하고 post_num에 Post테이블의 id 칼럼을 참조한다.
const Sequelize = require("sequelize");

class Cmt extends Sequelize.Model{
    static init(sequelize){
        return super.init({
            text : {
                type: Sequelize.STRING(100),
                allowNull: false
            }
        },{
            sequelize,
            timestamps: true,
            modelName: "Cmt",
            tableName: "cmts",
            paranoid: false,
            charset: "utf8",
            collate: "utf8_general_ci"
        })
    }

    static associate(db){
        db.Cmt.belongsTo(db.User, {foreignKey : "user_num", targetKey: "id"});
        db.Cmt.belongsTo(db.Post, {foreignKey : "post_num", targetKey: "id"});
    }
}

module.exports = Cmt;

 

7. models / index.js_Sequelize 객체를 생성하고, Post와 User, Cmt의 테이블을 담아 내보낸다.

const Sequelize = require("sequelize");
const config = require("../config");
const User = require("./users");
const Post = require("./posts");
const Cmt = require("./comment");

const sequelize = new Sequelize(
    config.dev.database,
    config.dev.username,
    config.dev.password,
    config.dev
) 

const db = {};

db.sequelize = sequelize;
db.User = User;
db.Post = Post;
db.Cmt = Cmt;

User.init(sequelize);
Post.init(sequelize);
Cmt.init(sequelize);

User.associate(db);
Post.associate(db);
Cmt.associate(db);

module.exports = db;

8. middleware / login.js_로그인이 정상적으로 이루어지고 있는지 확인하는 미들웨어 함수를 생성한다.

const jwt = require("jsonwebtoken");

exports.isLogin = (req,res,next)=>{
    const { access_token } = req.session;
    jwt.verify(access_token, process.env.ACCESS_TOKEN_KEY, (err, acc_decoded)=>{
        if(err){
            console.log(err)
            res.send("로그인 다시 하세요!")
        }
        else{
            req.acc_decoded = acc_decoded;
            next();
        }
    })

}

9. signUpController.js_회원가입 시 실행할 signUp함수 생성

  • 유저가 회원가입 시 User테이블에 유저가 입력한 아이디와 동일한 값이 없으면 회원가입을 진행한다.
    • 동일한 아이디가 있을 시, "이미 가입된 아이디 입니다."라는 멘트가 뜨며 에러페이지가 뜬다.
    • 동일한 아이디가 없을 시, 입력한 패스워드로 hash값을 만들어, User테이블에 값을 추가할 때 비밀번호 값으로 넘긴다.
      • 처음으로 회원가입 되는 계정의 레벨은 무조건 0이다.
  • 유저가 회원가입을 진행했을 때 어드민 계정이 없으면 User 테이블에 admin 계정을 생성한다.
    • 관리자 계정의 레벨은 3으로 설정한다.
const { User } = require("../models");
const bcrypt = require("bcrypt");

exports.signUp = async(req,res)=>{
    try {
        const { user_id, user_pw, user_name } = req.body;
        const user = await User.findOne({where: {user_id}});
        if(user != null){
            return res.send("이미 가입된 아이디 입니다.")
        }
        const hash = bcrypt.hashSync(user_pw, 10);
        User.create({
            user_id,
            user_pw : hash,
            user_name,
            user_level: 0
        });

        const admin = await User.findOne({where: {user_level : 3}})
        const adminHash = bcrypt.hashSync("admin123", 10);
        console.log("어드민", admin);
        if(admin == null){
            User.create({
                user_id : "admin",
                user_pw : adminHash,
                user_name : "관리자",
                user_level : 3
            })
        }
        res.redirect("/login");
    } catch (error) {
        console.log(error);
    }
}

10. loginController.js_로그인 시 실행할 Login 함수 생성

  • 로그인 시, 유저가 입력한 아이디 값과 User테이블의 user_id값을 비교해서 아이디값이 동일한 값이 있을 경우 비밀번호 검증으로 넘어간다.
    • 동일한 아이디 값이 없다면, "가입한 계정이 아닙니다. 다시 확인해 주세요!"라는 멘트와 함께 에러페이지가 뜬다.
  • bcrypt.compareSync메소드를 이용하여 유저가 입력한 비밀번호 값과, User 테이블의 pw값이 동일한지 확인한다.
    • 비밀번호가 동일하지 않다면 "비밀번호를 확인하세요!"라는 메세지와 함께 에러페이지가 뜬다.
  • 비밀번호까지 확인이 완료되면, 해당 유저의 레벨이 0이 아닌지 확인한다. 0이라면 "관리자의 승인이 필요합니다."라는 메세지와 함께 에러페이지가 뜬다.
    • 유저 레벨이 0이 아니라면, 엑세스토큰을 발행하여 로그인을 유지할 수 있도록 한다.
    • 만약 해당 유저 레벨이 3이라면 해당 유저는 관리자 계정으로 로그인이 성공하면 관리자페이지로 이동할 수 있도록 한다.
  • 로그인 성공 시, 게시글 목록 페이지로 이동한다.
const { User } = require('../models');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');

exports.Login = async(req,res)=>{
    try {
        const { user_id, user_pw } = req.body;
        const user = await User.findOne({where: {user_id}});
        if(user == null){
            res.send("가입한 계정이 아닙니다. 다시 확인 해주세요!");
        }

        // req.body의 user_pw와 user_id의 조건으로 검색한 계정의 user_pw의 값이 같으면 true를 반환한다.
        const same = bcrypt.compareSync(user_pw, user.user_pw);
        console.log("유저 레벨",user.user_level)
        if(user.user_level == 0){
            res.send("관리자의 승인이 필요합니다!")
        }

        if(same){
            let token = jwt.sign({
                id: user.id,
                name: user.user_id,
                nick: user.user_name,
                level: user.user_level
            }, process.env.ACCESS_TOKEN_KEY,{
                expiresIn: "3m"
            });
            req.session.access_token = token;
            if(user.user_level == 3){
                res.redirect("/admin");
            }
            res.redirect("/postList");
        }
        else{
            res.send("비밀번호를 확인하세요!");
        }

    } catch (error) {
        console.log(error);
    }
}

11. borderController.js_게시글에 관련한 함수를 생성한다.

PostAll : 전체 글 목록을 보여주는 함수

  • Post.findAll 메소드를 이용하여 해당 테이블의 전체 값을 불러와 리턴한다.
exports.PostAll = async (req,res)=>{
    const result = await Post.findAll({})
    console.log("글 목록은??", result);

    return result
}

postCreate : 게시글을 작성하는 함수

  • 현재 로그인 중인 유저의 레벨이 1 이상일 경우에 게시글을 작성할 수 있도록 게시글을 만드는 함수를 if 조건문으로 감싸준다.
exports.postCreate = async(req,res)=>{
    const { acc_decoded } = req;
    const { title, content } = req.body;

    if(acc_decoded.level >= 1){
        await Post.create({
            title: title,
            content : content,
            user_id: acc_decoded.id,
            writer: acc_decoded.nick
        });
        res.redirect('/postList')
    }
    else{
        res.send("게시글 작성이 불가한 등급입니다.")
    }
}

postView : 해당 게시글의 본문을 보여주는 함수

  • 선택한 게시글의 번호를  params로 불러와 result에 담아 리턴한다.
exports.postView = async(req,res)=>{
    const result = await Post.findOne({
        where : {
            id : req.params.id
        }
    })
    return result;
}

postDel : 해당 게시글을 삭제하는 함수

  • 선택한 글을 삭제하기 위해 ' Post.findOne '메소드를 이용하여 Post테이블에서 선택한다.
  • 게시글은 작성자만 삭제할 수 있기 때문에, 글 작성자를 추리기 위해서 User테이블의 id 컬럼을 Post테이블의 user_id의 값으로 찾는다.
  • 로그인한 유저의 값을 골라내기 위해 req에서 acc_decoded 값을 불러와 User테이블에서 현재 접속중인 유저의 정보를 조회한다.
  • if문을 이용하여 해당 게시글의 작성자 값과, 로그인한 유저의 값이 동일하면 삭제가 진행되도록 ' Post.destroy '를 진행한다.
    • 사실, UserResult는 구하지 않아도 되는 값이다. if문의 매개변수로 " if(postWriter.id == acc_decoded.id) "로 작성해도 동일한 의미로 동작할 수 있을것이니 말이다.
exports.postDel = async(req,res)=>{
     // 선택한 글의 고유 값을 부르기 위한 함수
     const result = await Post.findOne({
        where : {
            id : req.params.id
        }
    })
    
    // 글 작성자에 대한 값
    const postWriter = await User.findOne({
        where: {
            id : result.user_id
        }
    })
    const { acc_decoded } = req;
    //현재 로그인한 유저의 값
    const UserResult = await User.findOne({
        where : {
            id : acc_decoded.id
        }
    })
    

    if(postWriter.id == UserResult.id){
        await Post.destroy({
            where : {id: req.params.id}
        });
        res.redirect('/postList')
    }
    else{
        res.send("작성자만 삭제 가능합니다!")
    }
}

postUpdateView: 해당 게시글의 수정 페이지를 보여주는 함수

  • 바로 위에서 설명한 postDel과 동일한 동작을 수행한다. 
  • 선택한 게시글 조회, 게시글의 작성자 조회, 현재 접속중인 계정을 조회하여 작성자와 접속자의 계정이 동일할 경우 게시글을 수정할 수 있도록 수정페이지로 이동한다.
exports.postUpdateView = async(req,res)=>{    
    // 선택한 글의 고유 값을 부르기 위한 함수
    const result = await Post.findOne({
        where : {
            id : req.params.id
        }
    })
    
    // 글 작성자에 대한 값
    const postWriter = await User.findOne({
        where: {
            id : result.user_id
        }
    })
    const { acc_decoded } = req;
    //현재 로그인한 유저의 값
    const UserResult = await User.findOne({
        where : {
            id : acc_decoded.id
        }
    })
    

    if(postWriter.id == UserResult.id){
        res.render("update",{data:result});
    }
    else{
        res.send("작성자만 수정이 가능합니다!")
    }
}

postUpdate : 해당 게시글을 수정하는 함수

  • " Post.update " 메소드를 사용하여 title과 content의 내용을 담아와 해당 컬럼의 내용을 수정한다.
exports.postUpdate = async(req,res)=>{
    const {title, content} = req.body;
    const {id} = req.params;
    
    await Post.update({title, content}, {where : {id}});
    res.redirect(`/view/${id}`);
}

 

12. adminController.js_관리자 페이지에 관련한 함수를 생성한다.

💡 Sequelize.Op 💡

  • 시퀄라이즈는 자바스크립트 객체를 사용하여 쿼리를 생성하기 때문에 특수한 연산자들이 사용된다.

자주 사용하는 Op객체

Op.gt 초과
Op.gte 이상
Op.lt 미만
Op.lte 이하
Op.ne 같지 않음
Op.or 또는
Op.in 배열 요소 중 하나
Op.notIn 배열 요소와 모두 다름
const Op = Sequelize.Op

[Op.and]: [{a: 5}, {b: 6}] // (a = 5) AND (b = 6)
[Op.or]: [{a: 5}, {a: 6}]  // (a = 5 OR a = 6)

[Op.gt]: 6,                // > 6
[Op.gte]: 6,               // >= 6

[Op.lt]: 10,               // < 10
[Op.lte]: 10,              // <= 10

[Op.ne]: 20,               // != 20
[Op.eq]: 3,                // = 3

[Op.is]: null              // IS NULL
[Op.not]: true,            // IS NOT TRUE

[Op.between]: [6, 10],     // BETWEEN 6 AND 10
[Op.notBetween]: [11, 15], // NOT BETWEEN 11 AND 15

[Op.in]: [1, 2],           // IN [1, 2]
[Op.notIn]: [1, 2],        // NOT IN [1, 2]

[Op.like]: '%hat',         // LIKE '%hat'
[Op.notLike]: '%hat'       // NOT LIKE '%hat'
[Op.startsWith]: 'hat'     // LIKE 'hat%'
[Op.endsWith]: 'hat'       // LIKE '%hat'
[Op.substring]: 'hat'      // LIKE '%hat%'

[Op.regexp]: '^[h|a|t]'    // REGEXP/~ '^[h|a|t]' (MySQL/PG only)
[Op.notRegexp]: '^[h|a|t]' // NOT REGEXP/!~ '^[h|a|t]' (MySQL/PG only)

[Op.like]: { // LIKE ANY ARRAY['cat', 'hat'] - also works for iLike and notLike
	[Op.any]: ['cat', 'hat']
} 

[Op.gt]: { // > ALL (SELECT 1)
	[Op.all]: literal('SELECT 1') 
}
출처: https://inpa.tistory.com/entry/ORM-%F0%9F%93%9A-%EC%8B%9C%ED%80%84%EB%9D%BC%EC%9D%B4%EC%A6%88-%EC%BF%BC%EB%A6%AC-%EB%AC%B8%EB%B2%95

 

 

UserAll:  전체 유저 목록을 보여주는 함수

  • 관리자 계정을 제외한 회원들을 불러오고자 했으므로, user_level이 3인 유저를 제외하고 표시하고자 하였다.
    • 따라서, Op객체를 사용하여 ' [Op.ne : 3] '으로 3이 아닌 값들을 불러오게 하였다.
  • 회원 정보에서 비밀번호는 노출이되면 안되므로 User테이블에서 user_pw를 제외한 컬럼들이 노출되기를 희망하여 attributes 속성으로 원하는 컬럼만 노출될 수 있도록 설정하였다.
exports.UserAll = async (req,res)=>{
    const result = await User.findAll({
        attributes : ['id','user_id','user_name','user_level'],
        where : {user_level: { [Op.ne]:3 }}
    });
    return result
}

userUpdate:  레벨 0인 유저를 레벨 1로 등급 업 시켜주는 함수

  • 선택한 계정을 User테이블에서 골라내어, User.update 메소드를 이용해 user_level의 값을 1로 수정하였다.
exports.userUpdate = async(req,res)=>{
    const { id } = req.params;
    await User.update({
        user_level : 1
    },{
        where : {id}
    })
    res.redirect("/admin")
}

userDel:  선택한 유저를 목록에서 지우는 함수

  • 선택한 계정을 지우기 위해 User.destroy 메소드를 이용하여 해당 유저의 내용을 삭제하였다.
exports.userDel = async(req,res)=>{
    const {id} = req.params;
    await User.destroy({
        where : {id}
    });
    res.redirect("/admin")
}

 

13. routers/signUp.js_' /signUp '에 접속하면 실행되는 ejs와 함수를 설정한다.

const router = require("express").Router();
const { signUp } = require("../controllers/signUpController");


router.get('/',(req,res)=>{
    res.render("signUp");    
})

router.post('/',signUp);

module.exports = router;

 

14. routers/login.js_' /login '에 접속하면 실행되는 ejs와 함수를 설정한다.

const router = require("express").Router();
const { Login } = require("../controllers/loginController");

router.get('/',(req,res)=>{
    res.render("login")
})

router.post('/',Login);

module.exports = router;

 

 

15. routers/postList.js_' /postList'에 접속하면 실행되는 ejs와 함수를 설정한다.

  • 로그인이 정상적으로 이루어지고 있는지 확인하기 위해 " isLogin " 미들웨어를 실행한 후, 로그인 동작을 진행하는 함수를 실행한다.
const router = require("express").Router();
const { PostAll } = require("../controllers/borderController");
const { isLogin } = require("../middleware/login")

router.get('/',isLogin,async (req,res)=>{
    const result = await PostAll(req,res);
    res.render("postList", {data:result})
})

module.exports = router;

 

~ 그 외 다른 router도 동일한 패턴으로 동작을 진행한다. ~ 

댓글기능과, 게시글 조건이 충족되면 등급업이 되는 기능은 시간이 부족하여 마치지 못했다.....


결과물

회원가입, 로그인 성공 / 관리자 승인요청

어드민 계정 로그인, 관리자 페이지 접속 / 승인대기 유저 - 승인, 거절 / 승인완료 유저 - 삭제

승인계정 로그인 / 작성자만 게시글 수정 및 삭제 가능

게시글 등록, 수정, 삭제

 

 

 

 

 

728x90