본문 바로가기

블록체인_9기/과제

35강_230522_Node.js(회원가입, 로그인 기능이 있는 게시판 페이지 구현)

728x90

 

 

 


제작내용

회원가입과 로그인 페이지가 있는 게시판 페이지를 구현하도록 한다.

데이터는 데이터베이스 공간에 저장되며, DB공간에 유저의 정보를 담는 User 테이블, 게시글 내용을 담는 Post 테이블을 제작한다. Post테이블은 User테이블의 고유 값인 ' id '컬럼을 참조하며 그 내용을 user_id 컬럼에 담는다.

유저 정보는 회원가입과 로그인 기능이 있으며, 회원가입 시 유저의 정보가 User 테이블의 데이터베이스에 담긴다. 로그인 시 DB에 해당 계정이 있는지 조회 후, 있으면 로그인을 성공시키며 로그인을 유지시킬 Access Token을 발행한다.

회원가입, 로그인 페이지를 제외한 모든 페이지는 로그인을 성공하여 Access Token이 유효할 경우에만 페이지에 접속할 수 있으며 해당 조건을 검사해줄 별도의 미들웨어를 제작한다. 

페이지는 회원가입 페이지, 로그인 페이지, 유저의 마이페이지, 게시글 페이지가 있다.  유저의 마이페이지에는 해당 유저의 이름과 게시글 페이지로 넘어갈 수 있는 버튼, 게시글을 등록할 수 있도록 제작된다. 게시글 페이지는 해당 유저가 작성했던 모든 게시글을 불러와 조회할 수 있도록 한다.

 

 

 


제작과정

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

1. app.js_웹 서버를 열기 위한 기본 세팅 설정

  • 사용할 모듈을 저장 후 불러온다.
    • npm i express express-session mysql2 ejs dotenv sequelize
const express = require("express");
const session = require("express-sviewession");
const dot = require("dotenv").config();
const path = require("path");
  • 서버 인스턴스를 생성한다.
const app = express();
  • view엔진 경로설정,  ejs 사용을 설정한다.
app.set("views", path.join(__dirname,"page"));
app.set("view engine", "ejs");
  • body 객체를 가져온다.
app.use(express.urlencoded({extended:false}));
  • session을 사용하기 위한 설정을 한다.
app.use(session({
    secret : process.env.SESSION_KEY,                // 세션 키 넣을 것
    resave : false,                                  // 다시 저장할 지 여부
    saveUninitialized : false                        //초기화 할지 여부
}))
  • sequelize 연결 매핑을 설정한다.
const { sequelize } = require("./models");

// force: 초기화 여부
sequelize.sync({force : false})
.then((e)=>{
    console.log("연결성공")
}).catch((err)=>{
    console.log(err);
})
  • 서버를 대기시킨다.
app.listen(8000,()=>{
    console.log("서버열림")
})

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

SESSION_KEY = mysessionkey
DATABASE_USERNAME = root
DATABASE_PASSWORD = 000000			// 자신의 데이터베이스 비밀번호를 기입한다.
DATABASE_NAME = test10
DATABASE_HOST = 127.0.0.1

ACCESS_TOKEN_KEY = myAccessToken

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

const config = {
    dev : {
        username : process.env.DATABASE_USERNAME,
        passwoed : process.env.DATABASE_PASSWORD,
        database : process.env.DATABASE_NAME,
        host : process.env.DATABASE_HOST,          // 나중에 배포를 하게 된다면 데이터베이스 주소를 입력해 줄 예정이다.
        dialect : "mysql"
    }
}

module.exports = config;

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

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

class Post extends Sequelize.Model{
    static init(sequelize){
        return super.init({
            msg : {
                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"});
    }
}

module.exports = Post;

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

  • hasMany 메서드를 이용하여 Post 테이블의 참조키 user_id에 User테이블의 id 칼럼을 참조시킨다.
const Sequelize = require("sequelize");
class User extends Sequelize.Model {
    static init(sequelize){
        return super.init({
            // 컬럼의 내용
            name : {
                type : Sequelize.STRING(20),
                allowNull : false,
            },
            age : {
                type : Sequelize.INTEGER,
                allowNull : false
            },
            user_id : {
                type : Sequelize.STRING(20)
            },
            user_pw : {
                type : Sequelize.STRING(64)
            }
        },{
            // 테이블의 내용
            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"})
    }
}

module.exports = User;

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

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

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

const db = {};

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

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

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

module.exports = db;

6. signUpControllers.js_회원가입시 동작할 함수를 설정한다.

  • 유저의 아이디 값은 고유값 이므로, 중복할 수 없다.
    • User 테이블을 불러와, 회원가입 시 사용자가 입력한 user_id 값과 이미 User테이블에 있는 user_id의 값을 비교해서 동일한 값이 있다면 err메시지가 뜨도록한다.
  • 비밀번호는 사용자가 입력한 pw를 그대로 저장할 수 없으므로 bcrypt 모듈을 사용하여 hash를 생성한다.
  • 중복검사와 입력한 비밀번호로 hash생성을 마치면 User 테이블에 사용자가 입력한 값들을 넣어 회원가입을 완료한다.
const { User } = require("../models")
const bcrypt = require("bcrypt");
exports.signUp = async (req,res)=>{
    try {
        const { name, age, user_id, user_pw } = req.body
        // 
        const user = await User.findOne({where : {user_id}});
        if(user != null){
            return res.send("err")
        }

        // 비밀번호 암호화를 위해 설치
        // 회원가입
        // hashSync : 동기적으로 실행할 수 있는 메소드
        const hash = bcrypt.hashSync(user_pw, 10);
        console.log(hash, "sdfsdfsdfs")
        // user 테이블에 회운 추가
        User.create({
            name,
            age,
            user_id,
            user_pw : hash,
        });
        res.redirect('/login');
    } catch (error) {
        console.log(error)
    }
}

7. loginController.js_로그인 시 동작할 함수를 설정한다.

  • 사용자가 입력한 user_id와 User테이블에 저장되어 있는 user_id가 동일한 값이 있는지 확인한다.
    • 동일한 값이 없다면 " 회원가입한 유저가 아님 "을 페이지에 띄운다.
    • User테이블에서 동일한 값이 확인된다면 const user에 해당 계정값을 넣고 아래의 내용을 진행한다.
  • bcrypt모듈을 이용하여, 사용자가 입력한 user_pw와 user_id확인 시 반환받은 const user의 user_pw를 ' bcrypt.compareSync '로 검증한다.
    • 값이 같아 true값이 나오면 session에 넣을 access_Token을 jwt를 이용하여 생성하고 로그인을 완료 시킨다.
    • 만약 동일한 비밀번호가 아니라면 " 비밀번호를 확인하세요! "라는 에러메시지를 띄운다.
const { User } = require("../models");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
// npm i 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("회원가입한 유저가 아님");
        }

        const same = bcrypt.compareSync(user_pw, user.user_pw);
        if(same){       // true면
            let token = jwt.sign({
                id: user.id,
                name: user.name,
                age: user.age
            },process.env.ACCESS_TOKEN_KEY,{
                expiresIn: "5m"
            });
            req.session.access_token = token;
            res.redirect("/border");
        }
        else{
            res.send("비밀번호 확인하세요!")
        }
    } catch (error) {
        console.log(error);
    }
}

8. middleware/login.js_login이 정상적으로 이루어지고 있는지 확인하는 isLogin 미들웨어를 설정한다.

  • 로그인이 정상적으로 이루어지면 담겨있을 req.session에서 access_token을 구조분해할당으로 받는다.
  • jwt.verify로 현재 로그인으로 발행된 access_token과 env의 ACCESS_TOKEN의 값을 검증하고, 만약 이 두 값이 같지 않다면 err를, 같다면 acc_decoded가 실행되도록 한다.
    • err가 실행된다면 페이지에 " 로그인을 다시 하세요 "라는 메시지를 띄운다.
    • acc_decoded가 실행된다면 req에 acc_decoded를 생성하여 acc_decoded의 값을 전달한다.
  • 이 모든 과정이 끝나면 next()로 next매개변수가 실행되며, 다음 미들웨어가 실행될 수 있도록 한다.
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{
            // acc_decoded 키를 추가해서 값을 전달
            req.acc_decoded = acc_decoded;
            // 토큰이 유효한 동안 로그인이 되어 있는 것이고
            // 유저의 필요한 정보도 payload값에 있기 때문에 복호화해서 사용 가능하다.
            // 다음 미들웨어 실행
            next();
        }
    })
}

9. routers/signUp.js_경로 ~ /signUp 으로 접속 시, 실행될 ejs를 설정하고, 해당 ejs에서 post로 요청된 내용을 컨트롤러단에서 제작한 signUp함수를 실행하여 회원가입을 진행한다.

  • app.js에서 설정될 ' ~ /signUp ' 의 경로는 signup.js에서 라우터 될 수 있도록 설정되어 있으므로, ' / '로 접속이 들어오면 ' signUp.ejs ' 가 실행될 수 있도록 한다.
const router = require("express").Router();
const {signUp} = require("../controllers/signUpController");


router.get('/',(req,res)=>{
    res.render("signUp");
})
  • ' signUp.ejs '에서 들어온 post 요청을 signUp함수로 실행시킨다.
router.post('/',signUp);
  • routers / login.js와 page / login.ejs도 동일하게 진행된다.

10. borderController.js_post에 관련된 동작을 하는 함수들을 설정한다. 

const { User, Post } = require("../models");

BorderMain: 해당 유저의 마이페이지에 노출될 유저 이름을 불러오는 함수

  • 현재 로그인한 유저가 가지고 있는 access_token의 값을 acc_decoded로 불러와, User테이블의 name과 acc_decoded.name과 동일한 값을 가진 계정이 있는지 확인하여 user로 반환하고, main.ejs로 렌더하며 반환된 값을 data에 넣어 보내준다.
exports.BorderMain = async (req,res)=>{
    // 해당 유저의 마이 페이지
    const { acc_decoded } = req;
    console.log("acc_decoded",acc_decoded);
    const user = await User.findOne({where: {name: acc_decoded.name}});
    console.log("user",user)
    res.render("main", {data : user});
}

createBorder: 글을 등록하는 함수

  • acc_decoded와 페이지에서 입력한 값을 user_post로 불러와 Post.create 메소드로 해당 내용의 값을 Post 테이블에 추가한다.
exports.createBorder = async (req,res)=>{
    // 글을 등록하는 함수
    // console.log("req확인가자",req)
    const { acc_decoded } = req;
    const { user_post } = req.body;
    console.log("acc_decoded",acc_decoded)
    console.log("user_post",user_post)
    // Post 테이블에 글 추가
    await Post.create({
        msg : user_post,
        user_id : acc_decoded.id
    });
    // 해당 유저가 작성한 글들을 볼 수 있는 페이지로 이동
    res.redirect(`/border/view/${acc_decoded.id}`)
}

borderView: 해당 유저가 작성한 글 목록을 보여주는 함수

  • User테이블의 params.id의 갑과 동일한 user의 정보를 선택하고 해당 유저를 참조하는 Post 테이블의 값을 불러온다.
  • findOne에서 불러온 데이터에는 유저의 정보 값들과 ' Posts: [ [Post]... ] ' 가 들어있는데 [Post]가 해당 계정으로 작성한 게시글 하나에 대한 내용을 담고 있다.
    • 따라서, e.dataValues.Posts는 해당 계정이 작성한 게시글의 정보를 담고 있는 배열을 의미하며, 해당 배열에는 게시글의 id, msg등의 정보를 담고 있다. 해당 정보를 map함수를 이용하여 새로운 골라낸 새로운 정보를 담고 있는 새로운 배열을 생성하고, border페이지를 렌더하며, data에 해당 정보를 담아 클리이언트 측으로 보낸다.

 

---------------------------- ~ .then(e)의 e의 값에 대한 예시 ------------------------------------
User {
   dataValues: {
      id: 4,
      name: '11',
      age: 11,
      user_id: '11',
      user_pw: '$2b$10$DWAvvPFWL5Ytte.jxlgMH.6oAzgZ771zR7.vmwdhzN2wmVGhnVW7C',
      createdAt: 2023-05-29T05:04:47.000Z,
      updatedAt: 2023-05-29T05:04:47.000Z,
      Posts: [ [Post] ] 
   },
   _previousDataValues: {
.........


-------------------- e.dataValues.Posts 에 대한 예시 --------------------------------------
Post {
   dataValues: {
      id: 8,
      msg: 'ewr',
      createdAt: 2023-05-29T05:04:57.000Z,
      updatedAt: 2023-05-29T05:05:04.000Z,
      user_id: 4
   },
   _previousDataValues: {
..........
exports.borderView = (req,res)=>{
    // 글 목록을 보여주는 함수
    User.findOne(
        {
            where : {id: req.params.id}, 
            include : [
                {model : Post}
            ]
        }
    ).then((e)=>{
        // console.log("-------------------",e)
        console.log("-------------------", e.dataValues.Posts)
        // (i)=> i.dataValues === (i)=> { retern i.dataValues}   화살표 함수는 {}가 빠지면 바로 반환시킨다. retern문 생략 가능.
        e.dataValues.Posts = e.dataValues.Posts.map((i)=> i.dataValues);
        const Posts = e.dataValues;
        res.render("border",{data:Posts});
    })

}

updataBorder: 글을 수정하는 함수

  • Post.update 메소드를 사용하여 값을 수정한다.
    • 첫 번째 매개변수: 객체로 전달하며, 수정할 값을 작성한다.
    • 두 번째 매개변수: 객체로 전달하며, 수정할 내용을 찾을 조건을 작성한다.
  • body객체에서 수정할 내용인 msg의 내용을 받아와 update의 첫 번째 매개변수로 전달하고, 수정하기 위한 조건을 작성하기 위해, ejs에서 post.id로 받은 params.id의 값을 받아와 두 번째 매개변수인 where의 조건으로 작성한다.
  • 함수를 실행한 후, 현재 접속 중인 유저가 작성한 글 목록 페이지로 돌아가기 위해 acc_decoded를 불러와 경로로 이용한다.
exports.updataBorder = async(req,res)=>{
    // 글을 수정하는 함수
    const { acc_decoded } = req;
    const { msg } = req.body;
    const { id } = req.params;
    // update() 수정 메소드 사용.
    await Post.update({msg},{where : {id}});
    res.redirect(`/border/view/${acc_decoded.id}`);
}

borderDel: 글을 삭제하는 함수

  • Post.destroy : 삭제 메소드를 이용하여, 해당 조건의 부합하는 값을 삭제한다.
exports.borderDel = async(req,res)=>{
    // 글을 삭제하는 함수
    // 삭제 메소드 사용
    await Post.destroy({
        where : {id : req.params.id}
    });
    res.redirect('/border');
}

10. routers/border.js_post에 관련한 함수를 동작하는 get, post의 처리방식을 설정한다.

  • login페이지를 넘어가면 isLogin 미들웨어로 로그인 유무를 확인한 후 함수를 동작한다.
const router = require("express").Router();
const { isLogin } = require("../middleware/login");
const { BorderMain, borderView, borderDel, createBorder, updataBorder } = require("../controllers/borderController");
const { post } = require("./signUp");


// login 페이지를 넘어가면 isLogin으로 로그인 유무 확인, BorderMain을 실행

// 로그인이 완료되면 루트경로가 호출되고, 
// 해당 루트경로에 따라 로그인된 유저의 정보를 보여주는 BorderMain이 실행되며
// main.ejs가 노출된다. 
router.get('/',isLogin,BorderMain);

// main.ejs에서 '등록 글 보러가기'를 클릭하면 /view/:id로 이동되고 
// 해당 유저가 작성한 글을 보여주는 함수인 borderView가 실행된다.
router.get('/view/:id', isLogin, borderView);

// main.ejs에서 input에 작성됭 글 내용으로 post 요청을 받으면
// createBorder가 실행되며 Post 테이블에 글이 추가된다.
router.post('/create_border', isLogin, createBorder);

// border.ejs에서 각 글마다 있는 수정 input에 수정할 값을 입력하고,
// '수정'버튼을 클릭하면 해당 updataBorder 함수를 이용하여 게시글이 수정된다.
router.post('/view_update/:id', isLogin, updataBorder);

// border.ejs에서 각 글마다 있는 '삭제'버튼을 클릭하면
// borderDel함수를 이용하여 해당 게시글이 삭제된다.
router.get('/del/:id', isLogin, borderDel);

module.exports = router;

11. app.js_각 회원가입, 로그인, 게시글에 대한 라우터를 불러오고 각 라우터마다의 루트경로를 설정하여 연결한다.

const SignUpRouters = require('./routers/signUp')
const LoginRouters = require('./routers/login')
const BorderRouter = require('./routers/border')

app.use("/signUp",SignUpRouters);
app.use("/login",LoginRouters);
app.use("/border",BorderRouter);

 

 

728x90