미니 프로젝트1에서 만든 기본 구조를 가져온다.
npm install connect-flash method-override multer
connect-flash
플래시 메세지를 위한 미들웨어 모듈
method-override
HTML Form 태그에서 원래 POST, GET 메소드만 지원하는데 DELETE, PUT을 사용할 수 있게 지원해 주는 모듈
multer
파일 업로드를 위한 모듈
모든 router.js파일에는 아래 코드를 추가해 준다.
const express = require("express");
const router = express.Router();
module.exports = router
스타일링엔 부트스트랩을 사용한다
1. login.ejs파일
<%- include('../partials/header') %>
<div class="auth-wrapper">
<form method="POST" action="/auth/login">
<div class="text-center">
<h3 class="mb-3 font-weight-normal">로그인</h3>
</div>
<div class="mb-3">
<label for="email">Email address</label>
<input type="email" class="form-control" id="email" name="email">
</div>
<div class="mb-3">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password">
</div>
<div class="d-flex justify-content-between align-items-center">
<div>
<button type="submit" class="btn btn-primary">로그인</button>
</div>
<div>
<a href="/auth/google" class="btn btn-danger" role="button">
구글
</a>
<a href="/auth/kakao" class="btn btn-warning" role="button">
카카오
</a>
</div>
</div>
<div class="text-end mt-2">
<a href="/signup">회원 가입 페이지로...</a>
</div>
</form>
</div>
<%- include('../partials/footer') %>
<%- include('../partials/header') %>
<div class="auth-wrapper">
<form method="POST" action="/auth/signup">
<div class="text-center">
<h3 class="mb-3 font-weight-normal">회원가입</h3>
</div>
<div class="mb-3">
<label for="email">Email address</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">가입하기</button>
<div class="text-end mt-2">
<a href="/login">로그인 페이지로...</a>
</div>
</form>
</div>
<%- include('../partials/footer') %>
<!-- Header Include -->
<%- include('../partials/header') %>
<!-- Form Wrapper -->
<div class="auth-wrapper">
<!-- Form Element -->
<form method="POST" action="/auth/login">
<!-- Email Input -->
<div class="mb-3">
<label for="email">Email address</label>
<input type="email" class="form-control" id="email" name="email">
</div>
<!-- Password Input -->
<div class="mb-3">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password">
</div>
<!-- Login & Social Login Buttons -->
<div class="d-flex justify-content-between align-items-center">
<div>
<button type="submit" class="btn btn-primary">로그인</button>
</div>
<div>
<a href="/auth/google" class="btn btn-danger" role="button">
구글
</a>
<a href="/auth/kakao" class="btn btn-warning" role="button">
카카오
</a>
</div>
</div>
<!-- Signup Link -->
<div class="text-end mt-2">
<a href="/signup">회원 가입 페이지로...</a>
</div>
<!-- Footer Include -->
<%- include('../partials/footer') %>
작동 원리
이 코드는 EJS (Embedded JavaScript) 템플릿 언어로 작성된 HTML 코드 조각이다. 주요로 회원 가입 폼을 포함하며, include 디렉티브를 사용하여 헤더와 푸터를 포함한다. 각 부분의 상세 분석은 다음과 같다.
<!-- Header Include -->
<%- include('../partials/header') %>
<!-- Form Wrapper -->
<div class="auth-wrapper">
<!-- Form Element -->
<form method="POST" action="/auth/signup">
<!-- Email Input -->
<div class="mb-3">
<label for="email">Email address</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<!-- Username Input -->
<div class="mb-3">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<!-- Password Input -->
<div class="mb-3">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<!-- Submit Button -->
<button type="submit" class="btn btn-primary">가입하기</button>
<!-- Footer Include -->
<%- include('../partials/footer') %>
작동 원리
위 확장팩을 설치해 주고, EJS Beautify의 Indentation size를 4로 바꿔준다.
Header와 Footer의 일반적인 구조는 다음과 같다.
이 header와 footer는 다음과 같이 사용이 가능하다.
<%- include('../partials/header') %>
<!-- 파일의 중간 내용 -->
<%- include('../partials/footer') %>
CDN링크를 넣어주자.
<!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>SNS</title>
<link rel="stylesheet" href="/styles/main.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css" integrity="sha512-SzlrxWUlpfuzQ+pcUCosxcglQRNAq/DZjVsC0lE40xsADsfeQoEypE+enwcOiGjk/bSuGGKHEyjSoQ1zVisanQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body>
<nav class="navbar navbar-dark bg-dark shadow-sm fixed-top navbar-expand-lg">
<div class="container">
<a class="navbar-brand">SNS</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarToggle">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarToggle">
<% if (!currentUser) { %>
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/signup">Signup</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/login">Login</a>
</li>
</ul>
<% } else { %>
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/posts">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/friends">Friends</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/profile/<%= currentUser._id %>">
<%= currentUser.username %>
</a>
</li>
<li class="nav-item">
<form action="/auth/logout" method="POST">
<button type="submit" class="nav-link no-outline">
Logout
</button>
</form>
</li>
</ul>
<% } %>
</div>
</div>
</nav>
<br>
<br>
<br>
<div class="container">
<% if (error && error.length > 0) { %>
<div class="alert alert-danger" role="alert">
<%= error %>
</div>
<% } %>
<% if (success && success.length > 0) { %>
<div class="alert alert-success" role="alert">
<%= success %>
</div>
<% } %>
</div>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js" integrity="sha384-oBqDVmMz9ATKxIep9tiCxS/Z9fNfEXiDAYTujMAeBAsjFuCZSmKbSSUnQlmh/jp3" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.min.js" integrity="sha384-mQ93GR66B00ZXjt0YO5KlohRA5SY2XofN4zfuZxLkoj1gXtW8ANNCe9d5Y3eG5eD" crossorigin="anonymous"></script>
</body>
</html>
이 코드는 HTML5 문서로, EJS (Embedded JavaScript) 템플릿 언어를 사용하여 동적 내용을 렌더링한다. 코드는 크게 head 섹션, 네비게이션 바 (navbar), 및 알림 (error 및 success 메시지) 섹션으로 구성된다.
사용되는 변수
<%- include('../partials/header') %>
<div class="form-signin">
<form method="POST" action="/auth/login">
<!-- ... -->
</form>
</div>
<%- include('../partials/footer') %>
위 처럼 include를 사용하여 헤더와 푸터를 사용한다.
.auth-wrapper {
width: 100%;
max-width: 330px;
padding: 15px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border: 1px solid lightgray;
border-radius: 4px;
background-color: #f5f5f5;
}
헤더에서 rel을 해줬다.
이제 로그인페이지와 회원가입 페이지도 스타일링이 잘 되었을 것이다.
현재 로그인을 할 때는 이메일을 이용해서 하며, 가입할 때는 이메일과 유저네임 둘 다 받는다. 하지만 OAuth 로그인을 할 때는 유저네임을 DB에 안 넣고 있었기에 그 부분을 임의로 넣어주겠다.
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
email: {
type: String,
unique: true
},
password: {
type: String,
minLength: 5
},
googleId: {
type: String,
unique: true,
sparse: true,
},
kakaoId: {
type: String,
unique: true,
sparse: true,
},
username: {
type: String,
required: true,
trim: true,
},
firstName: {
type: String,
default: 'First Name'
},
lastName: {
type: String,
default: 'Last Name'
},
bio: {
type: String,
default: '데이터 없음'
},
hometown: {
type: String,
default: '데이터 없음'
},
workspace: {
type: String,
default: '데이터 없음'
},
education: {
type: String,
default: '데이터 없음'
},
contact: {
type: String,
default: '데이터 없음'
},
friends: [{ type: String }], // 배열 안의 문자 타입 ['RTPC', 'qwerty', 'pikachu1']
friendsRequests: [{ type: String }] // 배열 안의 문자 타입
}, { timestamps: true })
const saltRounds = 10;
userSchema.pre('save', function (next) {
let user = this;
// 비밀번호가 변경될 때만
if (user.isModified('password')) {
// salt를 생성합니다.
bcrypt.genSalt(saltRounds, function (err, salt) {
if (err) return next(err);
bcrypt.hash(user.password, salt, function (err, hash) {
if (err) return next(err);
user.password = hash;
next();
})
})
} else {
next();
}
})
userSchema.methods.comparePassword = function (plainPassword, cb) {
// bcrypt compare 비교
// plain password => client , this.password => 데이터베이스에 있는 비밀번호
bcrypt.compare(plainPassword, this.password, function (err, isMatch) {
if (err) return cb(err);
cb(null, isMatch);
})
}
const User = mongoose.model('User', userSchema);
module.exports = User;
해당 코드는 Mongoose를 사용하여 MongoDB에 저장되는 사용자의 비밀번호를 안전하게 관리하기 위한 로직을 구현한 것입니다. 코드는 크게 두 부분으로 나뉜다.
코드에 대한 자세한 내용은 다음과 같다.
userSchema.pre('save', function (next) { ... } )
: Mongoose 스키마에 정의된 pre 메서드를 사용하여 'save' 이벤트 전에 실행될 콜백 함수를 정의한다. 이 콜백은 주로 데이터를 데이터베이스에 저장하기 전에 미리 처리해야 할 작업을 수행하는 데 사용된다.if (user.isModified('password')) { ... }
: isModified 메서드를 사용하여 'password' 필드가 수정되었는지 확인한다. 즉, 비밀번호가 변경될 때만 이 로직을 실행하도록 한다.bcrypt.genSalt(saltRounds, function (err, salt) { ... } )
: bcrypt 라이브러리의 genSalt 메서드를 사용하여 salt를 생성한다. 이 salt는 비밀번호를 해시하는 데 사용된다.bcrypt.hash(user.password, salt, function (err, hash) { ... } )
: bcrypt의 hash 메서드를 사용하여 실제 비밀번호를 해시한다. 해싱이 성공하면 원래의 비밀번호를 해시 값으로 대체한다.userSchema.methods.comparePassword = function (plainPassword, cb) { ... }
: 사용자 스키마에 comparePassword 메서드를 추가한다. 이 메서드는 로그인 시 사용자에게 입력받은 비밀번호 (plainPassword)와 데이터베이스에 저장된 해시된 비밀번호를 비교하는 데 사용된다.bcrypt.compare(plainPassword, this.password, function (err, isMatch) { ... } )
: bcrypt 라이브러리의 compare 메서드를 사용하여 입력받은 비밀번호와 해시된 비밀번호를 비교한다. 비교 결과는 콜백 함수의 두 번째 인수인 isMatch를 통해 반환된다. isMatch 값이 true면 비밀번호가 일치하며, false면 일치하지 않는다.const { default: mongoose } = require("mongoose");
const postSchema = new mongoose.Schema({
description: {
type: String,
required: true
},
comments: [{
type: mongoose.Schema.Types.ObjectId,
ref: "Comment",
}],
author: {
id: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
username: String
},
image: {
type: String
},
likes: [{ type: String }]
}, {
timestamps: true,
})
module.exports = mongoose.model("Post", postSchema);
mongoose.Schema.Types.ObjectId는 Mongoose에서 MongoDB의 ObjectId 데이터 유형을 나타낸다. MongoDB는 각 도큐먼트에 대해 고유한 ID를 자동으로 생성하며, 이 ID의 데이터 유형은 ObjectId이다.
코드에서 mongoose.Schema.Types.ObjectId를 사용하는 부분은 주로 다른 도큐먼트와의 관계를 나타내기 위해 사용된다. 다른 모델의 도큐먼트와 연결할 때 해당 도큐먼트의 ObjectId를 참조하여 관계를 설정한다.
type: mongoose.Schema.Types.ObjectId
, ref: "Comment"
를 사용하면 comments 필드는 "Comment" 모델의 ObjectId들로 구성된 배열이다. 즉, 각 Post 도큐먼트는 여러개의 Comment 도큐먼트들을 참조할 수 있다.type: mongoose.Schema.Types.ObjectId
, ref: "User"
를 사용하면 author.id 필드는 "User" 모델의 특정 도큐먼트를 참조한다. 즉, 각 Post 도큐먼트는 하나의 User 도큐먼트에 연결되어 있으며, 이는 포스트의 작성자를 나타낸다.const { default: mongoose } = require("mongoose");
const commentSchema = new mongoose.Schema({
text: String,
author: {
id: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
username: String
}
}, {
timestamps: true,
})
module.exports = mongoose.model("Comment", commentSchema);
사용 방법
이런 팝업창은 부트스트랩의 Modal을 이용하는 거다.
<%- include('../partials/header') %>
<div class="container">
<div style="max-width: 600px; margin: 1rem auto;">
<%- include('../partials/create-post') %>
</div>
<% posts.forEach((post) => { %>
<!-- 나의 포스트인지 -->
<% if (post.author.id.equals(currentUser._id) ||
// 나의 친구의 포스트인지
currentUser.friends.find(friend => friend === post.author.id.toString())) { %>
<div style="max-width: 600px; margin: 1rem auto;">
<%- include('../partials/post-item', { post: post }) %>
</div>
<% } %>
<% }) %>
</div>
<%- include('../partials/post-modal') %>
<%- include('../partials/footer') %>
<div class="card">
<h5 class="card-header text-start"> 소식 알리기</h5>
<div class="card-body">
<div class="card-text text-muted create-post"
data-bs-toggle="modal" data-bs-target="#newpost"
>
<%= currentUser.username %>의 소식을 공유해주세요!
</div>
</div>
</div>
<div class="modal fade" id="newpost">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5>소식 알리기</h5>
<button type="button" class="btn-close close" data-bs-dismiss="modal"></button>
</div>
<form method="POST" action="/posts" enctype="multipart/form-data">
<div class="modal-body">
<div class="form-group">
<textarea name="desc" id="desc" class="form-control" row="6" cols="50" placeholder="<%= currentUser.username %>소식을 알려주세요!" required></textarea>
</div>
<div class="form-group mt-1">
<input class="form-control" type="file" name="image">
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">소식공유</button>
</div>
</form>
</div>
</div>
</div>
router.get('/', checkAuthenticated, (req, res) => {
Post.find()
.populate('comments')
.sort({ createdAt: -1 })
.exec((err, posts) => {
if (err) {
console.log(err);
} else {
res.render('posts', {
posts: posts,
});
}
})
})
코드는 Express.js 라우터의 한 부분으로, HTTP GET 요청을 / 경로로 처리하는 라우터 핸들러를 정의하고 있다. 요청이 이 경로로 들어오면, 다음의 동작을 수행한다.
이 코드는 사용자가 인증된 경우에만 웹 페이지에서 모든 포스트를 가져와서 표시하는 기능을 제공한다.
router.get("/", checkAuthenticated, (req, res) => {
Post.find()
.populate("comments")
.sort({ createdAt: -1 })
const storageEngine = multer.diskStorage({
destination: (req, file, callback) => {
callback(null, path.join(__dirname, '../public/assets/images')); // 오류가 없다먼 'null'을 전달.
},
filename: (req, file, callback) => {
callback(null, file.originalname); // 오류가 없다먼 'null'을 전달.
}
})
// 이 callback을 사용함으로써, multer는 해당 파일을 지정된 위치와 이름으로 저장할 수 있게 된다.
const upload = multer({ storage: storageEngine }).single('image');
router.post('/', checkAuthenticated, upload, (req, res, next) => {
let desc = req.body.desc; // description. post-modal.ejs로 가보자.
let image = req.file ? req.file.filename : ""; // 파일이 있는 경우
Post.create({
image: image,
description: desc,
author: {
id: req.user._id,
username: req.user.username
},
}, (err, _) => { // post는 사용을 안하니 언더바(_)를 사용했다.
if (err) {
req.flash('error', '포스트 생성 실패');
res.redirect("back"); // 원래 페이지로 되돌아간다.
// next(err);
} else {
req.flash('success', '포스트 생성 성공');
res.redirect("back"); // 원래 페이지로 되돌아간다.
}
})
})
이 코드는 Express.js를 사용한 라우팅 및 파일 업로드를 관리하는 부분이다.
.single('image')
는 요청에서 'image'라는 이름의 단일 파일을 처리할 것임을 나타낸다.let desc = req.body.desc;
: 클라이언트에서 전송된 설명(description)을 가져온다.let image = req.file ? req.file.filename
: "";: req.file이 존재하면(즉, 파일이 첨부되었으면) 업로드된 파일의 이름을 image에 저장하고, 그렇지 않으면 빈 문자열을 저장한다.{ image: image, description: desc, author: { ... } }
객체에 담겨 있다.이 코드는 인증된 사용자가 이미지와 설명(description)을 포함하여 새로운 포스트를 생성할 수 있게 해준다. 사용자가 이미지를 첨부하여 업로드하면 해당 이미지는 서버에 저장되며, 포스트 정보는 MongoDB 데이터베이스에 저장된다.
app.use((err, req, res, next) => {
res.status(err.status || 500);
res.send(err.message || "Error Occurred");
})
이 코드는 Express.js에서 전역 오류 핸들러 미들웨어를 정의하는 부분이다. 오류 핸들러 미들웨어는 다른 미들웨어나 라우트 핸들러에서 발생한 오류를 처리하는 데 사용된다.
app.use((err, req, res, next) => { ... }
res.status(err.status || 500);
:res.send(err.message || "Error Occurred");
:전체적으로, 이 오류 핸들러는 애플리케이션 내의 어디에서든 발생할 수 있는 예상치 못한 오류를 처리하는 역할을 한다. 오류 정보가 제공되면 해당 정보를 사용하여 응답을 반환하고, 제공되지 않으면 기본값을 사용하여 응답한다.
데이터베이스에 저장한 포스트를 가져와서 Posts페이지에서 보여주겠다.
router.get('/', checkAuthenticated, (req, res) => {
Post.find()
.populate('comments')
.sort({ createdAt: -1 })
.exec((err, posts) => {
if (err) {
console.log(err);
} else {
res.render('posts', {
posts: posts,
});
}
})
})
에러 없애기 위해 임시로 가져오기
나의 포스트와 나의 친구들의 포스트만 나열해주기!
<% posts.forEach((post) => { %>
<!-- 나의 포스트인지 -->
<% if (post.author.id.equals(currentUser._id) ||
// 나의 친구의 포스트인지
currentUser.friends.find(friend => friend === post.author.id.toString())) { %>
<div style="max-width: 600px; margin: 1rem auto;">
<%- include('../partials/post-item', { post: post }) %>
</div>
<% } %>
<% }) %>
이 코드는 EJS 템플릿 엔진을 사용하여 데이터를 웹 페이지에 동적으로 표현하는 코드이다.
<% posts.forEach((post) => { %>
<% if (post.author.id.equals(currentUser._id) || ... %>
<div style="max-width: 600px; margin: 1rem auto;">
<div>
요소를 생성한다.<%- include('../partials/post-item', { post: post }) %>
요약하면, 이 코드는 posts 배열을 순회하면서, 현재 로그인한 사용자 또는 사용자의 친구가 작성한 포스트만 웹 페이지에 표시한다.
<div class="card mb-2">
<div class="card-body text-start">
<div class="d-flex align-items-center">
<a href="/profile/<%= post.author.id %>">
<h5>
<%= post.author.username %>
</h5>
</a>
<small class="text-muted ms-auto">
<%= post.createdAt.toDateString() %>
</small>
<% if (post.author.id.equals(currentUser._id)) { %>
<div class="dropdown">
<button class="no-outline" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<img src="/assets/images/ellipsis.png" height="20px">
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item text-center" href="/posts/<%= post._id %>/edit">Edit</a></li>
<li>
<form class="dropdown-item text-center" action="/posts/<%= post._id %>?_method=DELETE" method="POST">
<button type="submit" class="no-outline">Delete</button>
</form>
</li>
</ul>
</div>
<% } %>
</div>
<p class="card-text mt-2">
<%= post.description %>
</p>
<% if (post.image) { %>
<img class="w-100" src="/assets/images/<%= post.image %>" />
<% } %>
<hr class="mt-1" >
<div class="d-flex justify-content-between">
<div class="row">
<form action="/posts/<%= post._id %>/like?_method=PUT" method="POST">
<!-- 이미 좋아요를 눌렀는지 -->
<% if (post.likes.find(like => like === currentUser._id.toString())) { %>
<button type="submit" class="no-outline">
<img src="/assets/images/like-1.png" height="20px" >
<span class="ms-1"> <%= post.likes.length %></span>
</button>
<% } else { %>
<button type="submit" class="no-outline">
<img src="/assets/images/like.png" height="20px" >
<span class="ms-1"> <%= post.likes.length %></span>
</button>
<% } %>
</form>
</div>
<a class="ms-auto pe-2" data-bs-toggle="collapse" href="#post<%= post._id %>">
댓글 <%= post.comments.length %>
</a>
</div>
<hr class="mt-1" >
<div class="collapse show" id="post<%= post._id %>">
<% if (post.comments.length > 0) { %>
<div class="card-body comment-section">
<% post.comments.forEach((comment) => { %>
<div class="d-flex justify-content-between">
<div class="font-weight-bold">
<%= comment.author.username %>
</div>
<small>
<%= comment.createdAt.toDateString() %>
</small>
</div>
<div class="d-flex justify-content-between mt-2">
<p>
<%= comment.text %>
</p>
<% if (comment.author.id.equals(currentUser._id)) { %>
<div class="dropdown">
<button class="no-outline" type="button" data-bs-toggle="dropdown">
<img src="/assets/images/ellipsis.png" height="20px" >
</button>
<div class="dropdown-menu">
<a
class="dropdown-item text-center"
href="/posts/<%= post._id %>/comments/<%= comment._id %>/edit">Edit</a>
<form class="dropdown-item text-center"
action="/posts/<%= post._id %>/comments/<%= comment._id %>?_method=DELETE"
method="POST"
>
<button class="no-outline" type="submit">Delete</button>
</form>
</div>
</div>
<% } %>
</div>
<% }) %>
</div>
<% } %>
</div>
<div>
<form method="POST" action="/posts/<%= post._id %>/comments">
<div class="form-group">
<input name="text" id="desc" class="comment-section" placeholder="댓글을 작성해주세요." required>
<p class="small ms-2"> 엔터를 눌러주세요.</p>
</div>
</form>
</div>
</div>
</div>
포스트 생성을 성공했을 때나 실패했을 때 다른 페이지로 이동해 주는데 그때 다른 페이지에서 성공이나 실패에 대한 메시지를 화면에서 보여주고 싶다.
그때 connect-flash라는 모듈을 이용해서 간단하게 보여줄 수 있다.
connect-flash를 사용할 때는 정보를 유지해줘야 하기 때문에 세션을 사용한다.
npm install connect-flash
connect-flash 미들웨어 등록
const flash = require("connect-flash");
app.use(flash());
페이지를 이동하면서, 이동한 페이지에서 메시지를 보여준다.
app.get('/send', (req, res) => {
res.send('message send page');
})
app.get('/receive', (req, res) => {
res.send('message receive page');
})
app.get('/send', (req, res) => {
res.flash('post success', '포스트가 생성되었습니다.');
res.send('message send page');
})
app.get('/receive', (req, res) => {
res.send(req.flash('post success')[0]);
})
// 페이지를 새로고침 하면 세션에서 메시지가 사라지기에(flash) 화면에도 아무것도 안 보이게 된다.
// connect-flash는 휘발성으로 한번 실행되면 세션에서 저장값이 사라진다.
이 코드는 두 개의 Express 라우터 핸들러를 정의하고 있다. 라우터 핸들러는 특정 URL 경로에 대한 HTTP GET 요청을 처리한다. 여기서는 req 객체를 통해 요청을 받고, res 객체를 통해 응답을 보낸다.
app.get('/send', (req, res) => {...}
:app.get('/receive', (req, res) => {...}
:req.flash('post success')[0]
를 통해 앞서 설정한 flash 메시지의 값을 가져온다. req.flash('post success')는 배열 형태로 값을 반환하므로, [0]
을 통해 첫 번째 값을 가져온다.res.send(req.flash('post success')[0])
; 구문은 해당 flash 메시지의 값을 응답으로 보낸다.app.get('/receive', (req, res) => {...}
req.flash('post success')[0]
를 통해 앞서 설정한 flash 메시지의 값을 가져온다. req.flash('post success')
는 배열 형태로 값을 반환하므로, [0]
을 통해 첫 번째 값을 가져온다.res.send(req.flash('post success')[0])
; 구문은 해당 flash 메시지의 값을 응답으로 보낸다.포스트 생성을 잘했거나 실패했으면 Header.ejs에서 메시지를 보여주겠다.
<div class="container">
<% if (error && error.length > 0) { %>
<div class="alert alert-danger" role="alert">
<%= error %>
</div>
<% } %>
<% if (success && success.length > 0) { %>
<div class="alert alert-success" role="alert">
<%= success %>
</div>
<% } %>
</div>
router.post('/', checkAuthenticated, upload, (req, res, next) => {
let desc = req.body.desc;
let image = req.file ? req.file.filename : "";
Post.create({
image: image,
description: desc,
author: {
id: req.user._id,
username: req.user.username
},
}, (err, _) => {
if (err) {
req.flash('error', '포스트 생성 실패');
res.redirect("back");
// next(err);
} else {
req.flash('success', '포스트 생성 성공');
res.redirect("back");
}
})
})
해당 코드는 Express 라우터 핸들러를 정의하고 있으며, 클라이언트로부터 받은 POST 요청을 처리한다. 구체적인 설명을 아래에서 확인할 수 있다.
Post.create({..}, (err, _) => {...})
: MongoDB 데이터베이스에 새로운 Post를 생성하는 명령이다._id
와 username
으로 구성된다.이 코드는 사용자가 웹 어플리케이션에서 새로운 포스트를 생성할 때 사용되는 로직을 포함하고 있다. 사용자가 이미지와 설명을 업로드하면 해당 정보를 데이터베이스에 저장하고, 성공 또는 실패 메시지를 플래시에 저장하여 다음 페이지에서 사용자에게 보여줄 수 있다.
router.get('/', checkAuthenticated, (req, res) => {
Post.find()
.populate('comments')
.sort({ createdAt: -1 })
.exec((err, posts) => {
if (err) {
console.log('Error occured', err);
} else {
res.render('posts/index', {
posts: posts,
currentUser: req.user,
error: req.flash('error'),
success: req.flash('success')
});
}
})
})
mainRouter.get('/', checkNotAuthenticated, function(req, res, next) {
res.render('auth/login', {
error: req.flash('error'),
success: req.flash('success')
});
});
mainRouter.get('/login', checkNotAuthenticated, function(req, res, next) {
res.render('auth/login', {
error: req.flash('error'),
success: req.flash('success')
});
});
mainRouter.get('/signup', checkNotAuthenticated, function(req, res, next) {
res.render('auth/signup', {
error: req.flash('error'),
success: req.flash('success')
});
});
해당 코드는 Express에서 사용되는 라우터 핸들러를 정의하고 있으며, 클라이언트로부터 받은 특정 GET 요청을 처리한다.
router.get('/', checkAuthenticated, (req, res) => {...})
:Post.find()
: MongoDB에서 Post 모델에 해당하는 모든 문서를 검색하는 쿼리를 시작한다..populate('comments')
: 각 Post 문서의 comments 필드에 연결된 Comment 모델의 데이터를 가져와 함께 채워넣는다. 이렇게 하면 데이터베이스에서 별도의 쿼리를 실행하지 않고도 관련된 코멘트 데이터에 접근할 수 있다..sort({ createdAt: -1 })
: Post 문서를 createdAt 필드 기준으로 내림차순 정렬한다. 이는 최신의 포스트부터 반환되도록 한다..exec((err, posts) => {...})
: 쿼리를 실행하고, 결과나 에러를 콜백 함수로 받는다.console.log('Error occured', err);
: 에러 메시지를 콘솔에 출력한다.res.render('posts/index', {...})
: 'posts/index' 뷰 템플릿을 사용하여 HTML 페이지를 렌더링한다.코드는 인증된 사용자가 요청한 모든 포스트와 관련된 코멘트를 데이터베이스에서 가져와서 웹 페이지에 렌더링하는 역할을 한다.
이 코드는 Express 웹 서버에서 주요 라우팅 로직을 담고 있다.
mainRouter.get('/', checkNotAuthenticated, function(req, res, next) {...}
(예: http://example.com/)
에 대한 GET 요청을 처리한다.mainRouter.get('/login', checkNotAuthenticated, function(req, res, next) {...}
:mainRouter.get('/signup', checkNotAuthenticated, function(req, res, next) {...}
:이 코드는 사용자 인증과 관련된 주요 페이지들에 대한 라우팅을 설정하고 있으며, 각 페이지를 요청할 때마다 인증되지 않은 사용자만 접근할 수 있도록 하는 미들웨어를 사용하고 있다.
// res.locals 객체 안에 있는 프로퍼티들은
// 아래에 있는 routes 들에서 변수로 사용가능하게 된다.
// views에서도 변수를 사용할 수 있게 된다.
app.use((req, res, next) => {
res.locals.error = req.flash('error');
res.locals.success = req.flash('success');
res.locals.currentUser = req.user;
next();
})
app.use('/', mainRouter);
app.use('/auth', usersRouter);
app.use('/posts', postsRouter);
app.use('/posts/:id/comments', commentsRouter);
app.use('/profile/:id', profileRouter);
app.use('/friends', friendsRouter);
app.use(likeRouter);
// 또한 res.locals의 프로퍼티들은 요청이 끝날때 값이 사라지게 된다.
router.get("/:id/edit", checkPostOwnership, (req, res) => {
Post.findById(req.params.id (err, post) => {
if (err) {
res.redirect("/posts")
} else {
res.render("posts/edit", {
post: post
})
}
})
})
function checkPostOwnerShip(req, res, next) {
if (req.isAuthenticated()) {
// id에 맞는 포스트가 있는 포스트인지
Post.findById(req.params.id, (err, post) => {
if (err || !post) {
req.flash('error', '포스트가 없거나 에러가 발생했습니다.');
res.redirect('back');
} else {
// 포스트가 있는데 나의 포스트인지 확인
if (post.author.id.equals(req.user._id)) {
req.post = post;
next();
} else {
req.flash('error', '권한이 없습니다.');
res.redirect('back');
}
}
})
} else {
req.flash('error', '로그인을 먼저 해주세요.');
res.redirect('/login');
}
}