목차
2022.04.29 - [Programming/BACKEND] - Node.JS)04.26( ExpressServer - ORM( sequelize ), 게시판 )
1. 사전 설정
1.1. npm 모듈 설치 및 환경 설정
- npm init (npm 환경 구성)
- npm i express (express 설치)
- npm i -D nodemon (nodemon 설치)
- package.json
- "start": "nodemon app"로 수정
- npm i sequelize sequelize-cli mysql2
- sequelize, mysql2 모듈 설치
- npx sequelize init (sequelize 환경 구성)
- /config/config.json 파일 수정 (pw와 db 수정)
- "password":"adminuser", "database":"nodegram" 으로 변경
- npm i cookie-parser express-session nunjucks dotenv
- npm i passport passport-local passport-kakao bcryp
- passport, public, routers, uploads, views 폴더 생성
- .env 환경변수파일 생성하여 암호화 코드를 작성한다.
COOKIE_SECRET=nodejssns
1.2. 모델 생성
1.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 = {};
const User = require('./user');
const Post = require('./post');
const Hashtag = require('./hashtag');
let sequelize = new Sequelize( config.database, config.username, config.password, config );
db.sequelize = sequelize;
db.Sequelize = Sequelize;
db.User = User;
db.Post = Post;
db.Hashtag = Hashtag;
User.init(sequelize);
Post.init(sequelize);
Hashtag.init(sequelize);
User.associate(db);
Post.associate(db);
Hashtag.associate(db);
module.exports = db;
1.2.2. user.js
user모델로 users 테이블과 follow테이블이 생성되는데,
follow테이블이 생성되는 부분이 복잡할 수 있으니 유심히 봐야한다.
const Sequelize = require('sequelize');
// id는 자동생성
// email(문자 50)null 고유값, nick(문자30)notnull, password(문자100)notnull, provider(문자20)notnull default='local', snsid(문자30)null
// 모델명:User, 테이블명 users, 나머지 associate 설정은 posts테이블과 같다
module.exports = class User extends Sequelize.Model{
static init( sequelize ){
return super.init({
email:{
type:Sequelize.STRING(50),
allowNull:true,
unique:true, // null값은 고유값 적용이 되지 않는다. (null이 여러개 있어도 괜춘)
},
nick:{
type:Sequelize.STRING(30),
allowNull:false,
},
password:{
type:Sequelize.STRING(100),
allowNull:true, // kakao로 로그인 시, 비번이 저장되지 않기 때문
},
provider:{
type:Sequelize.STRING(20),
allowNull:false,
defaultValue:'local',
},
snsid:{
type:Sequelize.STRING(50),
allowNull:true,
},
},{
sequelize,
timestamps:true,// 이 속성이 true면, createAt(생성시간), updateAt(수정시간) 필드가 자동생성된다.
underscored:false,
paranoid:false,
modelName:'User',
tableName:'users',
charset:'utf8mb4',
collate:'utf8mb4_general_ci',
})
}
static associate(db){
//db.User.hasMany(db.Post, {foriegnKey:'', sourceKey:''});
db.User.hasMany(db.Post);
//follower를 위한 것
db.User.belongsToMany(db.User, {foreignKey:'followingId', as:'Followers',through:'Follow'});
//following를 위한 것
db.User.belongsToMany(db.User, {foreignKey:'followerId', as:'Followings', through:'Follow'});
}
}
// 유저1이 유저2를 팔로잉한다 (유저1 -> 유저2)
// 유저1(followers), 유저2(followings)로 레코드 생성
// 반대로 맞팔 하면 (유저1 <- 유저2)
// 유저2(followers), 유저1(followings)로 레코드 생성
// 유저1(followers), 유저3(followings)
// 유저3(followers), 유저1(followings)
// 유저3(followers), 유저2(followings)
// 유저2(followers), 유저3(followings)
// 유저1의 팔로워(followers)를 조회하려면?
// -> following에서 유저1을 조회
// 유저1이 팔로잉(following)하는 유저를 조회하려면?
// -> follower에서 유저1을 조회
1.2.3. post.js
users테이블과 posts테이블이 1:N 관계가 성립되어,
users의 id가 posts 테이블에 외래키로 userId컬럼이 생성된다.
// 모델명 : Post, 테이블명:posts
// 필드 : content(140)notnull, img(200)null, user와 1:n 관계표시 - user모델 생성후 설정
// timestamp true, underscored false, paranoid false 나머지 기존설정
const Sequelize = require('sequelize');
module.exports = class Post extends Sequelize.Model{
static init( sequelize ){
return super.init({
content:{
type:Sequelize.STRING(200),
allowNull:false,
},
img:{
type:Sequelize.STRING(200),
allowNull:true,
},
},{
sequelize,
timestamp:true, // 이 속성이 true면, createAt(생성시간), updateAt(수정시간) 필드가 자동생성된다.
underscored:false,
paranoid:false, // 이 멤버가 true면, deleteAt(삭제시간) 필드가 생성된다
modelName:'Post', // sequelize가 사용할 모델(테이블) 이름
tableName:'posts', // 데이터베이스의 자체 테이블의 이름
charset:'utf8mb4',
collate:'utf8mb4_general_ci',
});
}
static associate(db){
//db.Post.belongTo(db.User, {foreignKey:'', targetKey:'' })
db.Post.belongsTo(db.User);
db.Post.belongsToMany(db.Hashtag,{through:'PostHashtag'});
}
};
1.2.4. hashtag.js
- post의 id는 posthashtag 테이블의 외래키로 postId가 되고,
- hashtags의 id는 posthashtag테이블의 외래키로 hashtagId컬럼이 된다.
// hashtag.js
const Sequelize = require('sequelize');
module.exports = class Hashtag extends Sequelize.Model{
static init(sequelize){
return super.init({
title:{
type:Sequelize.STRING(20),
allowNull:false,
unique:true,
},
},{
sequelize,
timestamps:false,// 이 속성이 true면, createAt(생성시간), updateAt(수정시간) 필드가 자동생성된다.
underscored:false,
paranoid:false,
modelName:'Hashtag',
tableName:'hashtags',
charset:'utf8mb4',
collate:'utf8mb4_general_ci',
});
}
static associate(db){
db.Hashtag.belongsToMany(db.Post, {through:'PostHashtag'});
}
};
// hashtags 테이블의 필드는 id와 title 둘뿐이다.
// posts 테이블과 M:N관계가 성립
// posts 테이블
// 1번 게시물 : #사과 #배
// 2번 게시물 : #배 #오렌지
// 3번 게시물 : #오렌지 #사과
// 중간에 다리역할을 하는 테이블이 필요하다.
// 위 코드로 자동 생성할 수 있다.
// 1번 게시물 - 1번 해시태그
// 1번 게시물 - 2번 해시태그
// 2번 게시물 - 2번 해시태그
// 2번 게시물 - 3번 해시태그
// 3번 게시물 - 1번 해시태그
// 3번 게시물 - 3번 해시태그
// hashtags 테이블
// 1번 해시태그 - 사과
// 1번 해시태그 - 배
// 1번 해시태그 - 오렌지
1.3. routers / 라우터 작성
1.3.1. auth.js
주로 로그인에 관련한 라우터
// auth.js 주로 로그인에 관련한 내용
const express = require('express');
const User = require('../models/user');
const bcrypt = require('bcrypt');
const passport = require('passport');
const router = express.Router();
module.exports = router;
1.3.2. page.js
const express = require('express');
const {Post, User, Hashtag } = require('../models');
const router = express.Router();
module.exports = router;
1.3.3. user.js
const express = require('express');
const User = require('../models/user');
const router = express.Router();
module.exports = router;
1.3.4. post.js
해당 스크립트에서 multer가 사용되므로, npm i multer 명령어로 multer 모듈을 설치한다.
const express = require('express');
const multer = requires('multer');
const path = require('path');
const fs = require('fs');
const { Post, User, Hashtag} = require('../models');
const router = express.Router();
module.exports = router;
1.4. app.js
const express = require('express');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const path = require('path');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
const passport = require('passport');
const app = express();
app.set('port', process.env.PORT || 3000);
// dotenv 설정은 가장 위에 쓰는것이 좋다.
dotenv.config();
app.set('view engine', 'html');
nunjucks.configure('views', {express:app, watch:true,});
app.use(express.static(path.join(__dirname,'public')));
app.use('/img', express.static(path.join(__dirname, 'uploads'))); //이미지용 스태틱폴더 별도 생성
app.use(express.json());
app.use(express.urlencoded({extended:false}));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
resave:false,
saveUninitialized:false,
secret:process.env.COOKIE_SECRET,
cookie:{
httpOnly:true,
secure:false,
},
}));
const {sequelize} = require('./models');
const { prependListener } = require('process');
sequelize.sync({force:false})
.then(()=>{
console.log('db연결 성공');
})
.catch((err)=>{
console.error(err);
});
// 라우터 require
const pageRouter = require('./routers/page');
const postRouter = require('./routers/post');
const authRouter = require('./routers/auth');
const userRouter = require('./routers/user');
app.use('/', pageRouter);
app.use('/post', postRouter);
app.use('/auth', authRouter);
app.use('/user', userRouter);
// app.get('/',(req,res)=>{
// res.send('<h1>nodegram</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 = err.message;
res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
res.status(err.status || 500);
console.log(err);
res.render('error');
});
app.listen(app.get('port'), ()=>{
console.log(app.get('port'),'번 포트에서 대기중');
});
2. 기능 구현
2.1. 로그인 페이지로 이동
2.1.1. views/layout.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>layout.html</title> -->
<title>{{title}}</title>
<link rel="stylesheet" href="/main.css">
</head>
<body>
<div class="container">
<div class="profile-wrap">
<div class="profile">
{% if user %}
<!-- 로그인 유저가 null이 아니라면 (로그인 한 사람이 있다면 )
로그인 한 사람의 정보가 세션에 저장이 되고
그 세션값이 현재 파일에 render에 의해 담겨져 왔다는 뜻-->
<div class="user-name">
{{'안녕하세요' + user.nick + '님'}}
</div>
<div class="harf"><div>팔로워</div>
<div class="count follower-count">{{followerCount}}</div>
</div>
<input type="hidden" id="my-id" value="{{user.id}}">
<a id="my-profile" href="/profile" class="btn">내 프로필</a>
<a id="logout" href="/auth/logout" class="btn">로그아웃</a>
{% else %}
<form id="login-form" action="/auth/login" method="post">
<div class="input-group">
<label id="email">이메일</label>
<input type="text" id="email" name="email">
</div>
<div class="input-group">
<label id="password">비밀번호</label>
<input type="password" id="password" name="password">
</div>
<a id="join" href="/join" class="btn">회원가입</a>
<button id="login" type="submit" class="btn">로그인</button>
<a id="kakao" href="/auth/kakao" class="btn">카카오톡</a>
</form>
{% endif %}
</div>
</div>
{% block content %}
{% endblock %}
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
{% block script %}
{% endblock %}
</body>
</html>
2.1.2. page.js - router.get( '/' )
const express = require('express');
const { Post, User, Hashtag } = require('../models');
const router = express.Router();
...
// 로그인 페이지로 이동 ('/')
router.get('/', async (req, res, next)=>{
try{
//포스트 검색
const posts = await Post.findAll({
include:{
model:User,
attributes:['id', 'nick'],
},
order:[[ 'createdAt', 'DESC' ]],
});
res.render('main',
{
title:'Nodegram', // 타이틀
user:req.user, // 로그인유저 객체
followerCount:0, // 로그인 유저의 팔로워 수
followingCount:0, // 로그인 유저의 팔로잉 수
followerIdList:[], // 팔로워 리스트 (배열)
posts, // 전체 포스팅 객체
}
);
}catch(err){
console.error(err);
next(err);
}
});
...
module.exports = router;
2.1.3. main.html
{% extends 'layout.html' %}
<!--'layout.html의 내용을 확장해서 이곳에 내용을 더 쓰겠다'라는 의미-->
<!--layout.html파일을 이 위치에 호출하여 block content에 들어갈 부분을 아래에 쓰겠다.-->
<!-- 블록 컨텐츠 작성-->
{% block content %}
<div class="timeline">
{% if user %}
<div>
<form id="post-form" action="/post" method="post" enctype="multipart/form-data">
<div class="input-group">
<textarea id="twit" name="content" maxlength="140"></textarea>
</div>
<div class="img-preview">
<img id="img-preview" src="" style="display:none;" width="250" alt="미리보기">
<input id="img-url" type="hidden" name="url">
</div>
<div>
<label id="img-label" for="img">사진 업로드</label>
<input id="img" type="file" accept="image/*">
<button id="post-btn" type="submit" class="btn">포스팅</button>
</div>
</form>
</div>
{% endif %}
<div class="twits">
<form id="hashtag-form" action="/hashtag">
<input type="text" name="hashtag" placeholder="태그검색">
<button class="btn">검색</button>
</form>
<br>
{% for post in posts %}
<div class="twit">
<!-- 아이디 --><!-- 닉네임 -->
<input type="hidden" value="{{post.id}}" class="twit-id">
<input type="hidden" value="{{post.UserId}}" class="twit-user-id">
<div class="twit-author" style="font-weight:bold; font-family:Verdana;">
{{post.id}} - {{post.User.nick}}
</div>
<!-- 이미지 -->
{% if post.img %}
<!-- 현재 게시물의 이미지가 있다면 이미지태그 표시-->
<div class="twit-img"><img src="{{post.img}}"></div><br>
{% endif %}
<!-- content -->
<div class="twit-content" style="font-weight:bold; font-family:Verdana;">
<pre>{{post.content}}</pre>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
<!-- 블록 스크립트 작성 -->
{% block script %}
<script type="text/javascript">
document.getElementById('img').addEventListener('change', (e)=>{
const formData = new FormData();
formData.append('img', e.target.files[0]);
axios.post('/post/img', formData)
.then((res)=>{
docu
document.getElementById('img-url').value = res.data.url;
document.getElementById('img-preview').src = res.data.url;
document.getElementById('img-preview').style.display = 'inline';
})
.catch((err)=>{ console.error(err); });
});
</script>
{% endblock %}
2.2. 회원가입 폼 블럭으로 이동
2.2.1. layout.html
<!DOCTYPE html>
...
<title>{{title}}</title>
<link rel="stylesheet" href="/main.css">
</head>
<body>
<div class="container">
<div class="profile-wrap">
<div class="profile">
{% if user %}
...
{% else %}
<form id="login-form" action="/auth/login" method="post">
...
<a id="join" href="/join" class="btn">회원가입</a>
<button id="login" type="submit" class="btn">로그인</button>
<a id="kakao" href="/auth/kakao" class="btn">카카오톡</a>
</form>
{% endif %}
</div>
</div>
{% block content %}
{% endblock %}
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
{% block script %}
{% endblock %}
</body>
</html>
2.2.2. page.js - router.get( '/join' )
const express = require('express');
const { Post, User, Hashtag } = require('../models');
const router = express.Router();
...
// 회원가입 폼 블럭으로 이동
router.get('/join', (req,res,next)=>{
res.render('join',
{
title:'회원가입-Nodegram',
}
);
});
...
module.exports = router;
2.2.3. join.html
{% extends 'layout.html' %}
{% block content %}
<div class="timeline">
<form id="join-form" action="/auth/join" method="post">
<div class="input-group">
<label for="join-email">이메일</label>
<input id="join-email" type="email" name="email">
</div>
<div class="input-group">
<label for="join-nick">닉네임</label>
<input id="join-nick" type="nick" name="nick">
</div>
<div class="input-group">
<label for="join-password">비밀번호</label>
<input id="join-password" type="password" name="password">
</div>
<button id="join-btn" type="submit" class="btn">회원가입</button>
</form>
</div>
{% endblock %}
<!-- 블록 스크립트 작성 -->
{% block script %}
<script type="text/javascript">
window.onload=()=>{
// localhost:3000/join.html?error=exists
// 폼 실행 후 되돌아와서 페이지 전환을 하고자하는 주소에 error라는 파라미터가 있다면
if(new URL(location.href).searchParams.get('error')){
alert('이미 존재하는 이메일입니다.');
location.href='/';
}
};
</script>
{% endblock %}
2.3. 회원가입 동작
2.3.1. join.html
{% extends 'layout.html' %}
{% block content %}
<div class="timeline">
<form id="join-form" action="/auth/join" method="post">
<div class="input-group">
<label for="join-email">이메일</label>
<input id="join-email" type="email" name="email">
</div>
<div class="input-group">
<label for="join-nick">닉네임</label>
<input id="join-nick" type="nick" name="nick">
</div>
<div class="input-group">
<label for="join-password">비밀번호</label>
<input id="join-password" type="password" name="password">
</div>
<button id="join-btn" type="submit" class="btn">회원가입</button>
</form>
</div>
{% endblock %}
<!-- 블록 스크립트 작성 -->
{% block script %}
<script type="text/javascript">
window.onload=()=>{
// localhost:3000/join.html?error=exists
// 폼 실행 후 되돌아와서 페이지 전환을 하고자하는 주소에 error라는 파라미터가 있다면
if(new URL(location.href).searchParams.get('error')){
alert('이미 존재하는 이메일입니다.');
location.href='/';
}
};
</script>
{% endblock %}
2.3.2. auth.js - router.post( '/join' )
회원가입에 실패하면
join.html의 스크립트 블럭의 searchPrams가 error 파라미터를 찾아내어 가 동작 alert()을 띄운다.
// auth.js 주로 로그인에 관련한 내용
const express = require('express');
const User = require('../models/user');
const bcrypt = require('bcrypt');
const passport = require('passport');
const router = express.Router();
...
//일반 회원가입 동작
router.post('/join',async(req,res,next)=>{
// const email = req.body.email;
// const nick = req.body.nick;
// const password = req.body.password;
// req.body객체 -> {email:'abc@abc.com', nick:'hong' password:'1234'}
const { email, nick, password } = req.body;
try{
const exUser = await User.findOne({
where:{email}
}); // 전송된 email이 이미 가입된 이메일인지 조회
if(exUser){
//exUser가 null이 아니라면 (이미 회원 가입이 되어있다면)
return res.redirect('/join?error=exist');
}
const hash = await bcrypt.hash(password, 12);
// bcrypt로 비밀번호를 암호화한다.
// 해시연산의 뜻 : 암호화와 비슷한 연산의 결과로 같은 원본 데이터라도 연산 결과가 절대 같은 결과가 나오지 않게 하는 연산.
// 12 : 해시화를 하기 위한 복잡도 인수. 숫자가 클수록 해시화 암호화가 복잡해지고, 복구 연산도 오래걸린다. 12는 약 1초 정도의 시간이 걸린다.
await User.create({
email,
nick,
password:hash,
}); //이메일, 닉네임, 패스워드로 회원 추가
return res.redirect('/'); // main페이지로 이동
}catch(err){
console.error(err);
next(err);
}
});
...
module.exports = router;
2.4. 로그인 동작
참고 자료
https://www.zerocho.com/category/NodeJS/post/57b7101ecfbef617003bf457
2.4.1. layout.html
로그인 버튼을 클릭하면 form태그의 action속성의 url로 post전송된다.
<!DOCTYPE html>
...
<title>{{title}}</title>
<link rel="stylesheet" href="/main.css">
</head>
<body>
<div class="container">
<div class="profile-wrap">
<div class="profile">
{% if user %}
...
{% else %}
<form id="login-form" action="/auth/login" method="post">
...
<a id="join" href="/join" class="btn">회원가입</a>
<button id="login" type="submit" class="btn">로그인</button>
<a id="kakao" href="/auth/kakao" class="btn">카카오톡</a>
</form>
{% endif %}
</div>
</div>
{% block content %}
{% endblock %}
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
{% block script %}
{% endblock %}
</body>
</html>
2.4.2. auth.js - router.post( '/login' )
- auth.js에는 bcrypt모듈과 passport모듈이 import되어있다.
- 동작하는 라우터는 '미들웨어 속에 미들웨어가 있는 구조'.
로그인 동작은 passport모듈로 구현되는데,
passport.authenticate( )가 동작됨과 동시에 passport/localStrategy.js에서 로그인을 처리하기 시작하고 응답을 기다린다.
// auth.js 주로 로그인에 관련한 내용
const express = require('express');
const User = require('../models/user');
const bcrypt = require('bcrypt');
const passport = require('passport');
const router = express.Router();
...
// 로그인 동작
router.post('/login', (req,res,next)=>{
// passport 모듈로 로그인을 구현한다.
console.log('/login 라우터 동작');
passport.authenticate('local', (authError, user, info)=>{
// 로그인을 위해 현재 미들웨어가 실행되면,
// 'local'까지만 인식되어지고, passport/localStrategy라는 곳으로 이동하여 로그인을 처리한다.
// done()에 의해 되돌아온 전달값으로 (authError, user, info)=>{}가 실행된다.
if(authError){ // 서버에러가 있다면 서버에러 처리
console.error(authError);
return next(authError);
}
if(!user){ // user가 false라면 (로그인에 실패했다면)
console.log('user가 false여서 로그인 실패');
return res.redirect(`/?loginError=${info.message}`);
}
//여기서부터 정상 로그인
return req.login(user, (loginError)=>{
console.log('정상적으로 로그인에 성공');
//req.login을 하는 순간 index.js로 이동한다. (로그인루틴 정상실행 - 실행 후 복귀)
if(loginError){ //index.js에서 보낸 에러가 있다면 에러처리
console.error(loginError);
return next(loginError);
}
// 세션위치에서 세션쿠키가 브라우저로 보내어진다.
console.log('세션 위치에서 세션쿠키가 브라우저로 보내짐');
return res.redirect('/');
});
})(req,res,next) // 미들웨어 속 미들웨어에는 (req,res,next)를 뒤에 붙인다.
});
...
module.exports = router;
2.4.3. passport/localStrategy.js
- 로그인 처리 절차를 정의해놓은 strategy
- usernameField와 passwordField는 어떤 폼 필드로부터 아이디와 비밀번호를 전달받을지 설정하는 옵션이다.
- 여기서는 req.body.email과 req.body.password의 폼필드와 일치시키면 되므로 email, password로 설정한다.
- done( 서버에러 인자, 로그인 성공시 return값, error메시지 )로 세개 까지 인자로 넣을 수 있다.
- done( error ) - 서버에러 처리시
- done( null, exUser ) - 로그인 성공시 return ( 에러가 없어야하므로 첫 번째 인자는 null )
- done( null, false, { message : '비밀번호 틀림' } ) - 비번이 일치하지 않는 등의 경우에 에러메시지를 보낸다.
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
const User = require('../models/user');
// 일반 사용자의 로그인 절차를 정의한 strategy
module.exports = ()=>{
passport.use(new LocalStrategy({
usernameField:'email', // 보내온 req.body.email의 '필드이름과 일치하게 작성', 'email'
passwordField:'password', // 보내온 req.body.password의 '필드이름과 일치하게 작성',
}, async (email,password,done)=>{
try {
console.log('localStrategy 시작');
console.log('이메일 조회 시작');
const exUser = await User.findOne({
where:{email},
}); // 전달된 email이 user테이블에 존재하는지 조회
if(exUser){
console.log('회원이 존재하여 비밀번호 비교');
// 회원이 존재한다면?
// 암호-해시화 된 비번을 비교
const result = await bcrypt.compare(password, exUser.password);
if(result){
console.log('이메일 - 비번 일치');
// 비밀번호까지 같다면
// localStrategy.js가 호출된 위치의 익명함수로 전달된다.
done(null, exUser);
// localStrategy.js가 호출된 위치의 익명함수로 이동(에러없음 null과, 로그인한 유저를 전달)
}else{
// 비밀번호가 틀리다면
console.log('이메일은 일치하나 비번이 불일치');
done(null, false, {message:'비밀번호가 일치하지 않아요!'});
// localStratege.js가 호출된 위치의 익명함수로 이동(에러없음 null과, false(로그인 실패), 그리고 info 메시지 내용)
}
}else{
// 회원이 존재하지 않다면?
console.log('회원이 일치하지 않음');
done(null, false, {message:'가입되지 않은 회원입니다.'});
}
} catch (err) {
}
}));
};
2.4.4. passport/index.js
- serializeUser : 사용자 정보 객체를 세션에 아이디로 저장한다.
- deserializeUser : 세션에 저장한 아이디로 사용자 정보 객체를 불러온다.
const passport = require('passport');
const local = require('./localStrategy');
const User = require('../models/user');
module.exports = ()=>{
passport.serializeUser((user, done)=>{ //정상적으로 로그인 되었을 때 실행
console.log('정상적으로 로그인되어 serializeUser 시작');
done(null,user.id); // 세션에 아이디만 저장하는 동작.
//이동 직후 '세션에 아이디가 저장된다'라는 것은 세션쿠키에도 암호화된 키로 쿠키가 저장된다는 뜻이다.
// {id:3, 'condect.sid:12424123 } 세션쿠키와 같은 세션쿠키가 생성되면서
// 브라우저에서 connect.sid값의 쿠키가 관리되고 이후로는 아래 디시리얼라이즈유저로 아이디가 사용(세션값으로 복구 및 사용)된다.
});
passport.deserializeUser((id, done)=>{
console.log('deserializeUser 시작');
// 세션쿠키를 사용할 때, 로그인 후 부터 사용한다.
// 세션쿠키로 로그인된 사람이 req.user에 저장되는데, 차후에 추가로 그의 정보와 팔로워 팔로잉도 조인된 결과로 저장된다.
User.findOne({
where:{id},
})
// 세션에 저장된 아이디와 쿠키로 user를 복구, req.user로 사용
.then(user=> done(null,user))
// req.isAuthenticated()함수 결과 : 로그인되어있는 동안 트루값을 갖게된다.
.catch(err => done(err));
});
local();
};
2.5. 로그아웃 동작
2.5.1. layout.html
<!DOCTYPE html>
...
</head>
<body>
<div class="container">
<div class="profile-wrap">
<div class="profile">
{% if user %}
<!-- 로그인 유저가 null이 아니라면 (로그인 한 사람이 있다면 )
로그인 한 사람의 정보가 세션에 저장이 되고
그 세션값이 현재 파일에 render에 의해 담겨져 왔다는 뜻-->
<div class="user-name">
{{'안녕하세요 ' + user.nick + '님😊'}}
</div>
<div class="harf"><div>팔로워</div>
<div class="count follower-count">{{followerCount}}</div>
</div>
<div class="half">
<div>팔로잉</div>
<div class="count following-count">{{followingCount}}</div>
</div>
<input type="hidden" id="my-id" value="{{user.id}}">
<a id="my-profile" href="/profile" class="btn">내 프로필</a>
<a id="logout" href="/auth/logout" class="btn">로그아웃</a>
{% else %}
<form id="login-form" action="/auth/login" method="post">
...
</form>
{% endif %}
</div>
</div>
{% block content %}
{% endblock %}
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
{% block script %}
{% endblock %}
</body>
</html>
2.5.2. auth.js - router.get( '/logout' )
// auth.js 주로 로그인에 관련한 내용
const express = require('express');
const User = require('../models/user');
const bcrypt = require('bcrypt');
const passport = require('passport');
const router = express.Router();
...
router.get('/logout', (req,res)=>{
req.logout(); // 세션 쿠키 삭제
req.session.destroy();
res.redirect('/');
});
...
module.exports = router;
2.6. 내 프로필로 이동
2.6.1. layout.html
<!DOCTYPE html>
...
</head>
<body>
<div class="container">
<div class="profile-wrap">
<div class="profile">
{% if user %}
<!-- 로그인 유저가 null이 아니라면 (로그인 한 사람이 있다면 )
로그인 한 사람의 정보가 세션에 저장이 되고
그 세션값이 현재 파일에 render에 의해 담겨져 왔다는 뜻-->
<div class="user-name">
{{'안녕하세요 ' + user.nick + '님😊'}}
</div>
<div class="harf"><div>팔로워</div>
<div class="count follower-count">{{followerCount}}</div>
</div>
<div class="half">
<div>팔로잉</div>
<div class="count following-count">{{followingCount}}</div>
</div>
<input type="hidden" id="my-id" value="{{user.id}}">
<a id="my-profile" href="/profile" class="btn">내 프로필</a>
<a id="logout" href="/auth/logout" class="btn">로그아웃</a>
{% else %}
<form id="login-form" action="/auth/login" method="post">
...
</form>
{% endif %}
</div>
</div>
{% block content %}
{% endblock %}
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
{% block script %}
{% endblock %}
</body>
</html>
2.6.2. page.js
const express = require('express');
const { Post, User, Hashtag } = require('../models');
const router = express.Router();
...
// 내 프로필로 이동
router.get('/profile',(req,res)=>{
res.render('profile',{
title:'내 프로필 - Nodegram',
user:req.user,
followerCount : 0,
followingCount:0,
followerIdList:[],
});
});
...
module.exports = router;
2.6.3. profile.html
{% extends 'layout.html' %}
{% block content %}
<div class="timeline">
<div class="following half">
<h2>팔로잉 목록</h2>
{% if user.Followings %}
{% for following in user.Followings %}
<div>{{following.nick}}</div>
{% endfor %}
{% endif %}
</div>
<div class="followers half">
<h2>팔로워 목록</h2>
{% if user.Followers %}
{% for follower in user.Followers %}
<div>{{follower.nick}}</div>
{% endfor %}
{% endif %}
</div>
</div>
{% endblock %}
2.7. 포스팅 동작 - 이미지 업로드 부분
2.7.1. main.html - html부분
{% extends 'layout.html' %}
<!--'layout.html의 내용을 확장해서 이곳에 내용을 더 쓰겠다'라는 의미-->
<!--layout.html파일을 이 위치에 호출하여 block content에 들어갈 부분을 아래에 쓰겠다.-->
<!-- 블록 컨텐츠 작성-->
{% block content %}
<div class="timeline">
{% if user %}
<div>
<form id="post-form" action="/post" method="post" enctype="multipart/form-data">
<div class="input-group">
<textarea id="twit" name="content" maxlength="140"></textarea>
</div>
<div class="img-preview">
<img id="img-preview" src="" style="display:none;" width="250" alt="미리보기">
<input id="img-url" type="hidden" name="url">
</div>
<div>
<label id="img-label" for="img">사진 업로드</label>
<input id="img" type="file" accept="image/*">
<button id="post-btn" type="submit" class="btn">포스팅</button>
</div>
</form>
</div>
{% endif %}
<div class="twits">
...
</div>
</div>
{% endblock %}
<!-- 블록 스크립트 작성 -->
{% block script %}
<script type="text/javascript">
...
</script>
{% endblock %}
2.7.2. main.html - script 태그 부분
{% extends 'layout.html' %}
<!--'layout.html의 내용을 확장해서 이곳에 내용을 더 쓰겠다'라는 의미-->
<!--layout.html파일을 이 위치에 호출하여 block content에 들어갈 부분을 아래에 쓰겠다.-->
<!-- 블록 컨텐츠 작성-->
{% block content %}
<div class="timeline">
...
</div>
{% endblock %}
<!-- 블록 스크립트 작성 -->
{% block script %}
<script type="text/javascript">
document.getElementById('img').addEventListener('change', (e)=>{
const formData = new FormData();
formData.append('img', e.target.files[0]);
axios.post('/post/img', formData)
.then((res)=>{
document.getElementById('img-url').value = res.data.url;
document.getElementById('img-preview').src = res.data.url;
document.getElementById('img-preview').style.display = 'inline';
})
.catch((err)=>{ console.error(err); });
});
</script>
{% endblock %}
2.7.3. post.js - router.post( '/img' )
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { Post, User, Hashtag } = require('../models');
const router = express.Router();
try{
fs.readdirSync('uploads');
}catch(error){
console.error('upload폴더가 없으므로 생성합니다.');
fs.mkdirSync('uploads');
}
const upload = multer({
storage:multer.diskStorage({
destination(req,file,cb){
cb(null,'uploads/');
},
filename(req,file,cb){
const ext = path.extname(file.originalname);
cb(null, path.basename(file.originalname,ext)+Date.now()+ext);
},
}),
limits:{fieldSize:5*1024*1024},
});
// 이미지파일을 서버에 업로드하는 동작
router.post('/img', upload.single('img'), (req,res,next)=>{
console.log( `/img/${req.file.filename}` );
res.json({ url:`/img/${req.file.filename}` });
}); // 이미지만 업로드하고, 저장된 경로를 json형식으로 되돌려준다.
...
module.exports = router;
2.8. 포스팅 동작 - 포스팅
2.8.1. main.html
{% extends 'layout.html' %}
<!--'layout.html의 내용을 확장해서 이곳에 내용을 더 쓰겠다'라는 의미-->
<!--layout.html파일을 이 위치에 호출하여 block content에 들어갈 부분을 아래에 쓰겠다.-->
<!-- 블록 컨텐츠 작성-->
{% block content %}
<div class="timeline">
{% if user %}
<div>
<form id="post-form" action="/post" method="post" enctype="multipart/form-data">
<div class="input-group">
<textarea id="twit" name="content" maxlength="140"></textarea>
</div>
<div class="img-preview">
<img id="img-preview" src="" style="display:none;" width="250" alt="미리보기">
<input id="img-url" type="hidden" name="url">
</div>
<div>
<label id="img-label" for="img">사진 업로드</label>
<input id="img" type="file" accept="image/*">
<button id="post-btn" type="submit" class="btn">포스팅</button>
</div>
</form>
</div>
{% endif %}
<div class="twits">
...
</div>
</div>
{% endblock %}
<!-- 블록 스크립트 작성 -->
{% block script %}
<script type="text/javascript">
...
</script>
{% endblock %}
2.8.2. post.js - router.post( '/' )
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { Post, User, Hashtag } = require('../models');
const router = express.Router();
try{
fs.readdirSync('uploads');
}catch(error){
console.error('upload폴더가 없으므로 생성합니다.');
fs.mkdirSync('uploads');
}
const upload = multer({
storage:multer.diskStorage({
destination(req,file,cb){
cb(null,'uploads/');
},
filename(req,file,cb){
const ext = path.extname(file.originalname);
cb(null, path.basename(file.originalname,ext)+Date.now()+ext);
},
}),
limits:{fieldSize:5*1024*1024},
});
...
// 포스팅 동작
const upload2 = multer();
// 폼 내부에 <input type="file"이 있기 때문에 submit할 경우 파일을 한번 더 업로드하려고 동작한다.
// 따라서 file업로드 동작을 생략하기 위해 비어있는 multer객체를 생성하고, upload2.none()를 한다.
router.post('/', upload2.none(), async(req,res)=>{
try {
const currentPost = await Post.create({
content: req.body.content,
img:req.body.url,
UserId:req.user.id,
});
// 게시물을 포스팅할 때 같이 입력한 해시태그(#)를 골라내어, 단어별로 처음 나온 단어를 해시태그 테이블에 insert하고,
// 현재 게시물이 어떤 해시태그를 갖고 있는지의 여부를 posthashtags테이블에 insert한다.
// ** '정규표현식'을 사용한다! **
// ↓ '#'으로 시작해서 빈칸과 '#'이 아닌 곳까지를 단어로 하여 모두 검색한다.
const hashtags = req.body.content.match(/#[^\s#]*/g);
if(hashtags){
// 추출한 해시태그가 있다면~
const result = await Promise.all(
hashtags.map((tag)=>{
return Hashtag.findOrCreate({
//slice(1) -> title 필드값이 해시태그 중 하나(tag)의 내용 중 #을 제외한 나머지 글자와 같은 조건으로 검색한다.
// 같은 title값이 있으면, 지나가고, 없으면 Hashtag테이블에 현재 해시태그로 레코드를 추가하고 그 값으로 리턴한다.
where:{title:tag.slice(1).toLowerCase()},
});
}),
);
await currentPost.addHashtags(result.map( (r) => r[0] ) );
// 지금 추가한 post게시물에 대한 해시태그로 해시태그들을 posthashtags테이블에 추가
// addHashtags : Post모델과, Hashtag모델의 관계에서 Post모델에 자동 생성된 메서드 - Hashtag테이블에 데이터를 추가하는 메서드
}
res.redirect('/');
} catch (err) {
console.error(err);
next(err);
}
});
module.exports = router;
2.9. 포스팅 동작에 해시태그 추출하기
- 게시물을 포스팅할 때 같이 입력한 '해시태그(#)를 골라내어', 단어별로 처음 나온 단어를 해시태그 테이블에 insert하고, 현재 게시물이 어떤 해시태그를 갖고 있는지의 여부를 posthashtags테이블에 insert한다.
- '정규 표현식' 문법을 사용한다.
'정규 표현식'이란?
2.9.1. post.js - router.post( '/' )
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { Post, User, Hashtag } = require('../models');
const router = express.Router();
try{
fs.readdirSync('uploads');
}catch(error){
console.error('upload폴더가 없으므로 생성합니다.');
fs.mkdirSync('uploads');
}
const upload = multer({
storage:multer.diskStorage({
destination(req,file,cb){
cb(null,'uploads/');
},
filename(req,file,cb){
const ext = path.extname(file.originalname);
cb(null, path.basename(file.originalname,ext)+Date.now()+ext);
},
}),
limits:{fieldSize:5*1024*1024},
});
...
// 포스팅 동작
const upload2 = multer();
// 폼 내부에 <input type="file"이 있기 때문에 submit할 경우 파일을 한번 더 업로드하려고 동작한다.
// 따라서 file업로드 동작을 생략하기 위해 비어있는 multer객체를 생성하고, upload2.none()를 한다.
router.post('/', upload2.none(), async(req,res)=>{
try {
const currentPost = await Post.create({
content: req.body.content,
img:req.body.url,
UserId:req.user.id,
});
// 게시물을 포스팅할 때 같이 입력한 해시태그(#)를 골라내어, 단어별로 처음 나온 단어를 해시태그 테이블에 insert하고,
// 현재 게시물이 어떤 해시태그를 갖고 있는지의 여부를 posthashtags테이블에 insert한다.
// ** '정규표현식'을 사용한다! **
// ↓ '#'으로 시작해서 빈칸과 '#'이 아닌 곳까지를 단어로 하여 모두 검색한다.
const hashtags = req.body.content.match(/#[^\s#]*/g);
if(hashtags){
// 추출한 해시태그가 있다면~
const result = await Promise.all(
hashtags.map((tag)=>{
return Hashtag.findOrCreate({
//slice(1) -> title 필드값이 해시태그 중 하나(tag)의 내용 중 #을 제외한 나머지 글자와 같은 조건으로 검색한다.
// 같은 title값이 있으면, 지나가고, 없으면 Hashtag테이블에 현재 해시태그로 레코드를 추가하고 그 값으로 리턴한다.
where:{title:tag.slice(1).toLowerCase()},
});
}),
);
await currentPost.addHashtags(result.map( (r)=>r[0] ) );
// 지금 추가한 post게시물에 대한 해시태그로 해시태그들을 posthashtags테이블에 추가
// addHashtags : Post모델과, Hashtag모델의 관계에서 Post모델에 자동 생성된 메서드
//- Hashtag테이블에 데이터를 추가하는 메서드
}
res.redirect('/');
} catch (err) {
console.error(err);
next(err);
}
});
module.exports = router;
2.9. 해시태그 검색
2.9.1. main.html
{% extends 'layout.html' %}
<!--'layout.html의 내용을 확장해서 이곳에 내용을 더 쓰겠다'라는 의미-->
<!--layout.html파일을 이 위치에 호출하여 block content에 들어갈 부분을 아래에 쓰겠다.-->
<!-- 블록 컨텐츠 작성-->
{% block content %}
<div class="timeline">
{% if user %}
<div>
<form id="post-form" action="/post" method="post" enctype="multipart/form-data">
...
</form>
</div>
{% endif %}
<div class="twits">
<form id="hashtag-form" action="/hashtag">
<input type="text" name="hashtag" placeholder="태그검색">
<button class="btn">검색</button>
</form>
<br>
{% for post in posts %}
<div class="twit">
...
</div>
{% endfor %}
</div>
</div>
{% endblock %}
<!-- 블록 스크립트 작성 -->
{% block script %}
<script type="text/javascript">
...
</script>
{% endblock %}
2.9.2. page.js - router.get( '/hashtag' )
const express = require('express');
const { Post, User, Hashtag } = require('../models');
const router = express.Router();
...
// 해시태그 검색
router.get('/hashtag', async (req,res,next)=>{
const query = req.query.hashtag;
if(!query){
return res.redirect('/'); // 도착한 검색어가 없으면 메인으로 redirect
}
try {
// 해시태그 단어 검색
const hashtag = await Hashtag.findOne({ where:{ title:query, } });
let posts = [];
if(hashtag){
// 해당 해시태그로 Post테이블의 게시물들을 조회 (외래키인 User테이블을 join)
posts = await hashtag.getPosts({ include: [{ model:User }] });
}
return res.render('main', {
title:`${query} | NodeGram`,
posts,
user:req.user,
followerCount:0,
followingCount:0,
followerIdList:[],
});
} catch (err) {
console.error(err);
return next(err);
}
});
module.exports = router;
2.10. 카카오톡으로 로그인 ( passport-kakao )
passport-kakao모듈을 활용해 카카오톡으로 로그인하는 기능을 추가해본다.
2.10.1. layout.html
카카오로 로그인 버튼을 클릭하면 /auth/kakao 라우터가 동작한다.
<!DOCTYPE html>
...
</head>
<body>
<div class="container">
<div class="profile-wrap">
<div class="profile">
{% if user %}
...
{% else %}
<form id="login-form" action="/auth/login" method="post">
<div class="input-group">
<label id="email">이메일</label>
<input type="text" id="email" name="email">
</div>
<div class="input-group">
<label id="password">비밀번호</label>
<input type="password" id="password" name="password">
</div>
<a id="join" href="/join" class="btn">회원가입</a>
<button id="login" type="submit" class="btn">로그인</button>
<a id="kakao" href="/auth/kakao" class="btn">카카오톡</a>
</form>
{% endif %}
</div>
</div>
{% block content %}
{% endblock %}
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
{% block script %}
{% endblock %}
</body>
</html>
출처: https://inpa.tistory.com/entry/NODE-📚-카카오-로그인-Passport-구현 [👨💻 Dev Scroll]
2.10.2. auth.js - router.get( '/kakao' )
// auth.js 주로 로그인에 관련한 내용
const express = require('express');
const User = require('../models/user');
const bcrypt = require('bcrypt');
const passport = require('passport');
const {isLoggedIn, isNotLoggedIn } = require('./middleware');
const router = express.Router();
...
// 카카오톡으로 로그인
router.get('/kakao', passport.authenticate('kakao'));
// 스트레티지를 통해 카카오에 한번 갔다가 콜백을 받아 돌아온 뒤 콜백을 실행
...
module.exports = router;
2.10.3. auth.js - router.get( '/kakao/callback' )
// auth.js 주로 로그인에 관련한 내용
const express = require('express');
const User = require('../models/user');
const bcrypt = require('bcrypt');
const passport = require('passport');
const {isLoggedIn, isNotLoggedIn } = require('./middleware');
const router = express.Router();
...
// 카카오 로그인페이지를 통해 카카오에서 담겨온 데이터와 함께 콜백을 실행
// 카카오 콜백
router.get('/kakao/callback', passport.authenticate('kakao',{
failureRedirect:'/',
}), (req,res)=>{
res.redirect('/');
});
module.exports = router;
2.10.4. kakaoStrategy.js
const passport = require('passport');
const KakaoStrategy = require('passport-kakao').Strategy;
const User = require('../models/user')
module.exports = () =>{
passport.use(new KakaoStrategy({
clientID:process.env.KAKAO_ID,
callbackURL: '/auth/kakao/callback',
}, async(accessToken, refreshToken, profile, done)=>{
console.log( 'kakao profile', profile );
// profile:계정이 동의한 항목들이 들어있는 카카오가 보내준 객체(카카오 이메일주소, 카카오 닉네임, 나이, 성명, 성별 등)
try{
const exUser = await User.findOne({
where : { snsid:profile.id, provider:'kakao' }, //카카오 아이디 검색 (이미 가입된 계정이 있는지 확인)
});
if(exUser){
done(null, exUser); // 아이디가 존재하면 검색결과 회원정보(exUser)를 갖고 바로 done(null, exUser)로 돌아가 로그인절차(세션쿠키저장 등)을 실행한다.
}else{
// 아이디가 없으면 아래와 같이 회원을 Users테이블에 추가하고 로그인절차를 진행
const newUser = await User.create({
email: profile._json && profile._json.kakao_account.email,
nick: profile.displayName,
snsid: profile.id,
provider: 'kakao',
}); // 회원가입 후, 로그인 절차가 진행
done(null, newUser);
}
}catch(error){
console.error(error);
done(error);
}
}));
};
2.10.5. Kakao Developer에 플랫폼 등록
- 회원가입 / 로그인 후 내 애플리케이션을 클릭하여
- 애플리케이션을 추가한다.
- 생성된 어플리케이션을 클릭하여 설정에 들어간다.
- 왼쪽의 플랫폼을 클릭하여 플랫폼 설정에 들어간다.
- Web 플랫폼 등록을 선택
- 인덱스 주소를 입력하고 저장한다.
- 그 다음 좌측에 '카카오 로그인' 탭을 클릭
- '카카오 로그인'을 활성화 시킨다.
- 그리고 하단을 보면 Redirect URI가 있는데 등록버튼을 클릭하여 '/auth/kakao/callback 라우터를 추가'한다.
- 좌측의 '카카오 로그인 - 동의항목' 탭으로 이동
- 닉네임과 카카오 계정(이메일)을 동의설정한다.
- 여기서는 닉네임은 필수동의, 카카오계정은 선택동의로 한다.
- 좌측의 앱키 탭으로 들어가 REST API키를 복사한다.
- 복사한 REST API키를 .env 환경변수파일에 kakao_ID로 붙여넣기한다.
1.10.6. passport/index.js
const passport = require('passport');
const local = require('./localStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');
module.exports = ()=>{
passport.serializeUser((user, done)=>{ //정상적으로 로그인 되었을 때 실행
console.log('정상적으로 로그인되어 serializeUser 시작');
done(null,user.id); // 세션에 아이디만 저장하는 동작.
//이동 직후 '세션에 아이디가 저장된다'라는 것은 세션쿠키에도 암호화된 키로 쿠키가 저장된다는 뜻이다.
// {id:3, 'condect.sid:12424123 } 세션쿠키와 같은 세션쿠키가 생성되면서
// 브라우저에서 connect.sid값의 쿠키가 관리되고 이후로는 아래 디시리얼라이즈유저로 아이디가 사용(세션값으로 복구 및 사용)된다.
});
passport.deserializeUser((id, done)=>{
console.log('deserializeUser 시작');
// 세션쿠키를 사용할 때, 로그인 후 부터 사용한다.
// 세션쿠키로 로그인된 사람이 req.user에 저장되는데, 차후에 추가로 그의 정보와 팔로워 팔로잉도 조인된 결과로 저장된다.
User.findOne({
where:{id},
include: [{
model:User,
attributes:['id', 'nick'],
as: 'Followers',
}, {
model:User,
attributes:['id', 'nick'],
as:'Followings',
}],
})
// 세션에 저장된 아이디와 쿠키로 user를 복구, req.user로 사용
.then(user=> done(null,user))
// req의 내장함수 : req.isAuthenticated()함수 결과 : 로그인되어있는 동안 트루값을 갖게된다.
.catch(err => done(err));
});
local();
kakao();
};
1.10.7. models/user.js의 password필드 확인
카카오로 로그인 시 패스워드는 테이블에 저장되지 않기 때문에 'null 허용'이어야 한다.
1.11. 로그인 상태를 검사하는 미들웨어 추가
가끔씩 로그인이 되어있지 않지만, 로그인이 되어있는 것 처럼 동작할 때가 있다.
이것을 방지하기 위해 미들웨어가 동작할 때 로그인 인증을 하는 미들웨어가 동작하도록 추가한다.
1.11.1. routers/middleware.js
isAuthenticated() : 이 함수를 통하여 현재 로그인이 되어있는지 아닌지를 true, false로 return 한다.
// middleware.js - 도구를 만든다고 생각하면 됨.
const { renderString } = require("nunjucks");
exports.isLoggedIn = (req,res,next)=>{
if(req.isAuthenticated() ){
next();
} else {
res.status(403).send('로그인이 필요합니다!');
}
};
exports.isNotLoggedIn = (req,res,next)=>{
if(!req.isAuthenticated() ){
next();
} else {
const message = encodeURIComponent('이미 로그인이 되어있습니다!');
res.redirect(`/?error=${message}`);
}
};
1.11.2. 로그인 인증이 필요한 미들웨어에 추가
routers/page.js를 예를 들면
middleware.js의 isLoggedIn, isNotLoggedIn를 import하고
로그인이 필요한 동작은 isLoggedIn,
로그인이 필요하지 않은 동작에는 isNotLoggedIn 미들웨어를 추가한다.
const express = require('express');
const { Post, User, Hashtag } = require('../models');
const router = express.Router();
const {isLoggedIn, isNotLoggedIn } = require('./middleware');
...
// 회원가입 폼 블럭으로 이동
router.get('/join', isNotLoggedIn, (req,res,next)=>{
res.render('join',
{
title:'회원가입-Nodegram',
}
);
});
// 내 프로필로 이동
router.get('/profile', isLoggedIn,(req,res)=>{
res.render('profile',{
title:'내 프로필 - Nodegram',
user:req.user,
followerCount : req.user ? req.user. Followers.length: 0, // 로그인 유저의 팔로워 수
followingCount : req.user ? req.user. Followings.length: 0, // 로그인 유저의 팔로잉 수
followerIdList : req.user ? req.user. Followings.map(f=>f.id): [], // 팔로워 리스트 (배열)
});
});
...
module.exports = router;
1.12. follow기능 추가
1.12.1. main.html
main.html의 block content 에 아래와 같이 추가가 되고,
block script 에는 아래와 같이 추가된다.
{% extends 'layout.html' %}
<!--'layout.html의 내용을 확장해서 이곳에 내용을 더 쓰겠다'라는 의미-->
<!--layout.html파일을 이 위치에 호출하여 block content에 들어갈 부분을 아래에 쓰겠다.-->
<!-- 블록 컨텐츠 작성-->
{% block content %}
<div class="timeline">
{% if user %}
<div>
<form id="post-form" action="/post" method="post" enctype="multipart/form-data">
<div class="input-group">
<textarea id="twit" name="content" maxlength="140"></textarea>
</div>
<div class="img-preview">
<img id="img-preview" src="" style="display:none;" width="250" alt="미리보기">
<input id="img-url" type="hidden" name="url">
</div>
<div>
<label id="img-label" for="img">사진 업로드</label>
<input id="img" type="file" accept="image/*">
<button id="post-btn" type="submit" class="btn">포스팅</button>
</div>
</form>
</div>
{% endif %}
<div class="twits">
<form id="hashtag-form" action="/hashtag">
<input type="text" name="hashtag" placeholder="태그검색">
<button class="btn">검색</button>
</form>
<br>
{% for post in posts %}
<div class="twit">
<!-- 아이디 --><!-- 닉네임 -->
<input type="hidden" value="{{post.id}}" class="twit-id">
<input type="hidden" value="{{post.UserId}}" class="twit-user-id">
<div class="twit-author" style="font-weight:bold; font-family:Verdana;">
{{post.id}} - {{post.User.nick}}
</div>
<!-- 팔로우 버튼 -->
{% if not followerIdList.includes(post.User.id) and post.User.id !== user.id %}
<!-- 전달된 팔로워 리스트에 현재 게시물 작성자가 없고, 나의 게시물이 아니라면 버튼을 표시한다-->
<button class="twit-follow">팔로우하기</button><br>
{% endif %}
<!-- 이미지 -->
{% if post.img %}
<!-- 현재 게시물의 이미지가 있다면 이미지태그 표시-->
<div class="twit-img"><img src="{{post.img}}"></div><br>
{% endif %}
<!-- content -->
<div class="twit-content" style="font-weight:bold; font-family:Verdana;">
<pre>{{post.content}}</pre>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
<!-- 블록 스크립트 작성 -->
{% block script %}
<script type="text/javascript">
document.getElementById('img').addEventListener('change', (e)=>{
const formData = new FormData();
formData.append('img', e.target.files[0]);
axios.post('/post/img', formData)
.then((res)=>{
document.getElementById('img-url').value = res.data.url;
document.getElementById('img-preview').src = res.data.url;
document.getElementById('img-preview').style.display = 'inline';
})
.catch((err)=>{ console.error(err); });
});
// class가 twit-follow인 셀렉터를 모두 선택한 후
// -> 그 태그들을 하나씩 tag에 전달하는 익명함수를 실행
document.querySelectorAll('.twit-follow').forEach( function (tag) {
// 전달된 tag를 통해 각 버튼에 모두 이벤트 리스너(click)를 붙여 사용한다.
tag.addEventListener('click', function(){
const myid = document.querySelector('#my-id'); // 로그인 한 아이디
if( myid ){ // 로그인한 상태로 myid가 존재할 때만 실행한다.
const userId = tag.parentNode.querySelector('.twit-user-id').value; // 게시물의 작성자
if( userId !== myid.value ){ // 로그인 유저와 작성자가 같지 않다면 실행
const answer = confirm('팔로우 하시겠습니까?');
if(answer){
// 내가(로그인유저) 현재 게시물의 작성자를 팔로우하겠다. 라고 axios.post를 호출
axios.post(`/user/follow/${userId}`)
.then(()=>{
location.reload();
})
.catch((err)=>{
console.error(err);
});
}
}
}
});
});
</script>
{% endblock %}
1.12.2. routers/user.js - router.post( '/follow/:id' )
const express = require('express');
const User = require('../models/user');
const { isLoggedIn, isNotLoggedIn } = require('./middleware');
const router = express.Router();
// 로그인유저(나)가 전달된 :id 상대를 팔로우한다.
router.post('/follow/:id', isLoggedIn, async (req,res,next)=>{
const loginuser = await User.findOne({
where:{id:req.user.id},
}); //로그인 유저의 user정보 조회
if(loginuser){
await loginuser.addFollowings( parseInt(req.params.id, 10) );
// as : 'Followings'에 따른 메서드가 만들어짐. 복수,단수 모두 가능하다 setFollowing 수정메서드
// getFollowings, removeFollowings 복수면 []를 사용한다.
res.send('success');
}else{
res.status(404).send('no user');
}
});
module.exports = router;