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
'블록체인_9기 > 과제' 카테고리의 다른 글
46강_220704_React(캘린더 제작) (0) | 2023.07.04 |
---|---|
44강_220629_React(로또 추첨기 제작) (1) | 2023.06.30 |
35강_230522_Node.js(회원가입, 로그인 기능이 있는 게시판 페이지 구현) (0) | 2023.05.29 |
19강_230328_과제_묵찌빠 배팅게임_renewal (0) | 2023.03.30 |
16강_230323_과제_todo list (0) | 2023.03.29 |