(1) package.json 생성
- 프로젝트를 생성할 폴더로 이동 후 npm init(5장) 명령어 실행
(2) 관계형 데이터베이스 MySQL 사용을 위해 시퀄라이즈 설치
npm i sequelize mysql2 sequelize-cli
npx sequelize init
(3) 추가로 필요한 views / routes / public / passport 폴더 생성 + app.js / .env 파일 생성
- npm 패키지 설치
npm i express cookie-parser express-session morgan multer dotenv nunjucks
npm i -D nodemon
- app.js
const express = require('express');
const cookieParser = require('cookie-parser');
const morgan = require('morgan');
const path = require('path');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
dotenv.config();
const pageRouter = require('./routes/page');
const app = express();
app.set('port', process.env.PORT || 8001);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
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.COOKE_SECRET,
cookie: {
httpOnly: true,
secure: false,
}
}));
app.use('/', pageRouter);
app.use((req, res, next) => {
const error = new Error(`${req.mothod} ${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);
res.render('error');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기 중');
});
- .env
COOKE_SECRET=cookiesecret
(4) routes/page.js , html 파일과 css 파일 생성
const express = require('express');
const router = express.Router();
router.use((req, res, next) => {
res.locals.user = null;
res.locals.followerCount = 0;
res.locals.followingCount = 0;
res.locals.followerIdList = [];
next();
});
router.get('/profile', (req, res) => {
res.render('profile', { title: '내 정보 - NodeBird'});
});
router.get('/join', (req, res) => {
res.render('join', { title: '회원가입 - NodeBird'});
});
router.get('/', (req, res, next) => {
const twits = [];
res.render('main', {
title: 'NodeBird',
twits,
});
});
module.exports = router;
- views 폴더에 layout.html / main.html / profile.html / join.html / error.html 생성
https://github.com/ZeroCho/nodejs-book/tree/master/ch9/9.1/nodebird/views
- public/main.css 생성
https://github.com/ZeroCho/nodejs-book/blob/master/ch9/9.1/nodebird/public/main.css
(1) models 폴더 안에 user.js / post.js / hashtag.js 생성
- models/user.js
: 사용자 정보 저장 모델, {이메일, 닉네임, 비밀번호} 저장
: SNS 로그인 시 {provider, snsId} 추가 저장(provider=local이면 로컬 / provider=kakao이면 카카오 로그인)
: timestamp / paranoid -> createAt, updateAt, deleteAt 컬럼 생성됨
const Sequelize = require('sequelize');
module.exports = class User extends Sequelize.Model {
static init(sequelize) {
return super.init({
email: {
type: Sequelize.STRING(40),
allowNull: true,
unique: true
},
nick: {
type: Sequelize.STRING(15),
allowNull: false,
},
password: {
type: Sequelize.STRING(100),
allowNull: true,
},
provider: {
type: Sequelize.STRING(10),
allowNull: false,
defaultValue: 'local', // 기본 local
},
snsId: {
type: Sequelize.STRING(30),
allowNull: true,
},
}, {
sequelize,
timestamps: true,
underscored: false,
modelName: 'User',
tableName: 'users',
paranoid: true,
charset: 'utf8',
collate: 'utf8_general_ci',
})
}
static associate(db) {}
}
- models/post.js
: 게시글 모델, 게시글 내용과 이미지 경로 저장
const Sequelize = require('sequelize');
module.exports = class Post extends Sequelize.Model {
static init(sequelize) {
return super.init({
content: {
type: Sequelize.STRING(140),
allowNull: false,
},
img: {
type: Sequelize.STRING(200),
allowNull: true,
},
}, {
sequelize,
timestamps: true,
underscored: false,
modelName: 'Post',
tableName: 'posts',
paranoid: false,
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
});
}
static associate(db) {}
};
- models/hashtag.js
: 태그 이름 저장, 태그 검색 기능을 위해 정의
const Sequelize = require('sequelize');
module.exports = class Hashtag extends Sequelize.Model {
static init(sequelize) {
return super.init({
title: {
type: Sequelize.STRING(15),
allowNull: false,
unique: true,
},
}, {
sequelize,
timestamps: true,
underscored: false,
modelName: 'Hashtag',
tableName: 'hashtags',
paranoid: false,
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
});
}
static associate(db) {
db.Hashtag.belongsToMany(db.Post, { through: 'PostHashtag' });
}
};
- models/index.js
: 7장에서 학습한 시퀄라이즈 생성하는 코드. 자동으로 생성된 코드를 아래와 같이 변경.
const Sequelize = require('sequelize');
const env = process.env.NODE_ENV || 'development';
const config = require('../config/config')[env];
const User = require('./user');
const Post = require('./post');
const Hashtag = require('./hashtag');
const db = {};
const sequelize = new Sequelize(
config.database, config.username, config.password, config,
);
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;
- models/user.js
: static associate(db) {} 안에 각 모델 간의 관계 정의
const Sequelize = require('sequelize');
module.exports = class User extends Sequelize.Model {
static init(sequelize) {
return super.init({
email: {
type: Sequelize.STRING(40),
allowNull: true,
unique: true,
},
nick: {
type: Sequelize.STRING(15),
allowNull: false,
},
password: {
type: Sequelize.STRING(100),
allowNull: true,
},
provider: {
type: Sequelize.STRING(10),
allowNull: false,
defaultValue: 'local',
},
snsId: {
type: Sequelize.STRING(30),
allowNull: true,
},
}, {
sequelize,
timestamps: true,
underscored: false,
modelName: 'User',
tableName: 'users',
paranoid: true,
charset: 'utf8',
collate: 'utf8_general_ci',
});
}
static associate(db) {
db.User.hasMany(db.Post);
db.User.belongsToMany(db.User, {
foreignKey: 'followingId',
as: 'Followers',
through: 'Follow',
});
db.User.belongsToMany(db.User, {
foreignKey: 'followerId',
as: 'Followings',
through: 'Follow',
});
}
};
(2) models/post.js
: User와 Post 모델은 1:N관계 - belongsTo로 연결
: Post와 Hashtag 모델은 N:M 관계 - belongsToMany, 모델명 PostHashtag
const Sequelize = require('sequelize');
module.exports = class Post extends Sequelize.Model {
static init(sequelize) {
return super.init({
content: {
type: Sequelize.STRING(140),
allowNull: false,
},
img: {
type: Sequelize.STRING(200),
allowNull: true,
},
}, {
sequelize,
timestamps: true,
underscored: false,
modelName: 'Post',
tableName: 'posts',
paranoid: false,
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
});
}
static associate(db) {
db.Post.belongsTo(db.User);
db.Post.belongsToMany(db.Hashtag, { through: 'PostHashtag' });
}
};
(3) 데이터베이스-서버와 연결하기 위해 데이터베이스 생성
- config/config.json
{
"development": {
"username": "root",
"password": "nodejsbook",
"database": "nodebird",
"host": "127.0.0.1",
"dialect": "mysql",
"operatorAliases": false
},
}
- DB 생성
npx sequelize db:create
- app.js : 모델을 서버와 연결
const express = require('express');
const cookieParser = require('cookie-parser');
const morgan = require('morgan');
const path = require('path');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
dotenv.config();
const pageRouter = require('./routes/page');
const { sequelize } = require('./models');
const app = express();
app.set('port', process.env.PORT || 8001);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
sequelize.sync({ force: false })
.then(() => {
console.log('데이터베이스 연결 성공');
})
.catch((err) => {
console.error(err);
});
app.use(morgan('dev'));
.
.
.
});
+ 데이터베이스 연결 성공 이라고 뜸
+ 테이블이 없을 때 테이블을 자동으로 생성함
3. Passport 모듈로 로그인 구현하기
세션과 쿠키 처리 등 복잡한 작업 없이 검증된 passport모듈을 이용해
- app.js
const express = require('express');
const cookieParser = require('cookie-parser');
const morgan = require('morgan');
const path = require('path');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
const passport = require('passport');
dotenv.config();
const pageRouter = require('./routes/page');
const authRouter = require('./routes/auth');
const { sequelize } = require('./models');
const passportConfig = require('./passport');
const app = express();
passportConfig(); // 패스포트 설정
app.set('port', process.env.PORT || 8001);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
sequelize.sync({ force: false })
.then(() => {
console.log('데이터베이스 연결 성공');
})
.catch((err) => {
console.error(err);
});
app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
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,
},
}));
app.use(passport.initialize());
app.use(passport.session());
.
.
.
});
- passport/index.js
: Passport 관련 코드
const passport = require('passport');
const local = require('./localStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');
module.exports = () => {
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
User.findOne({ where: { id } })
.then(user => done(null, user))
.catch(err => done(err));
});
local();
kakao();
};
(1) 로컬 로그인 구현하기
- 회원가입 구현하기
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}`);
}
};
const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const router = express.Router();
router.use((req, res, next) => {
// locals로 선언하여 모든 템플릿 엔진에서 공통 사용
res.locals.user = req.user;
res.locals.followerCount = 0;
res.locals.followingCount = 0;
res.locals.followerIdList = [];
next();
});
router.get('/profile', isLoggedIn, (req, res) => {
res.render('profile', { title: '내 정보 - NodeBird' });
});
router.get('/join', isNotLoggedIn, (req, res) => {
res.render('join', { title: '회원가입 - NodeBird' });
});
router.get('/', (req, res, next) => {
const twits = [];
res.render('main', {
title: 'NodeBird',
twits,
});
});
module.exports = router;
const express = require('express');
const passport = require('passport');
const bcrypt = require('bcrypt');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const User = require('../models/user');
const router = express.Router();
// 1. 회원가입 라우터
router.post('/join', isNotLoggedIn, async (req, res, next) => {
const { email, nick, password } = req.body;
try {
const exUser = await User.findOne({ where: { email } });
if (exUser) {
return res.redirect('/join?error=exist');
}
const hash = await bcrypt.hash(password, 12);
await User.create({
email,
nick,
password: hash,
});
return res.redirect('/');
} catch (error) {
console.error(error);
return next(error);
}
});
// 2. 로그인 라우터
router.post('/login', isNotLoggedIn, (req, res, next) => {
passport.authenticate('local', (authError, user, info) => {
if (authError) {
console.error(authError);
return next(authError);
}
if (!user) {
return res.redirect(`/?loginError=${info.message}`);
}
return req.login(user, (loginError) => {
if (loginError) {
console.error(loginError);
return next(loginError);
}
return res.redirect('/');
});
})(req, res, next); // 미들웨어 내의 미들웨어에는 (req, res, next)를 붙입니다.
});
// 3. 로그아웃 라우터
router.get('/logout', isLoggedIn, (req, res) => {
req.logout();
req.session.destroy();
res.redirect('/');
});
router.get('/kakao', passport.authenticate('kakao'));
router.get('/kakao/callback', passport.authenticate('kakao', {
failureRedirect: '/',
}), (req, res) => {
res.redirect('/');
});
module.exports = router;
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
const User = require('../models/user');
module.exports = () => {
// LocalStrategy 생성자
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'password',
// 실제 Strategy 수행
}, async (email, password, done) => {
try {
const exUser = await User.findOne({ where: { email } });
if (exUser) {
const result = await bcrypt.compare(password, exUser.password);
if (result) {
done(null, exUser);
} else {
done(null, false, { message: '비밀번호가 일치하지 않습니다.' });
}
} else {
done(null, false, { message: '가입되지 않은 회원입니다.' });
}
} catch (error) {
console.error(error);
done(error);
}
}));
};
(2) 카카오 로그인 구현하기
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,//카카오에서 발급하는 ID, process.env를 통해 노출 방지
callbackURL: '/auth/kakao/callback', // 인증결과 받을 라우터 주소
}, async (accessToken, refreshToken, profile, done) => {
console.log('kakao profile', profile);
try {
const exUser = await User.findOne({
where: { snsId: profile.id, provider: 'kakao' },
});
if (exUser) {
done(null, exUser);
// 카카오로 회원가입한 적 없는 사람 -> 회원가입
} else {
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);
}
}));
};
.
.
.
router.get('/logout', isLoggedIn, (req, res) => {
req.logout();
req.session.destroy();
res.redirect('/');
});
// 로그인
router.get('/kakao', passport.authenticate('kakao'));
// 위에서 성공/실패 여부를 받음
router.get('/kakao/callback', passport.authenticate('kakao', {
failureRedirect: '/',
}), (req, res) => {
res.redirect('/');
});
module.exports = router;
위에서 이미 작업함 - auth라우터 추가
KAKAO_ID = RESTAPI키
npm i multer
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { Post, Hashtag } = require('../models');
const { isLoggedIn } = require('./middlewares');
const router = express.Router();
try {
fs.readdirSync('uploads');
} catch (error) {
console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.');
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: { fileSize: 5 * 1024 * 1024 },
});
router.post('/img', isLoggedIn, upload.single('img'), (req, res) => {
console.log(req.file);
res.json({ url: `/img/${req.file.filename}` });
});
const upload2 = multer();
router.post('/', isLoggedIn, upload2.none(), async (req, res, next) => {
try {
const post = await Post.create({
content: req.body.content,
img: req.body.url,
UserId: req.user.id,
});
// 해시태그 추출
const hashtags = req.body.content.match(/#[^\s#]*/g);
if (hashtags) {
const result = await Promise.all(
hashtags.map(tag => {
return Hashtag.findOrCreate({
where: { title: tag.slice(1).toLowerCase() },
})
}),
);
await post.addHashtags(result.map(r => r[0]));
}
res.redirect('/');
} catch (error) {
console.error(error);
next(error);
}
});
module.exports = router;
. ..
const { Post, User } = 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: 'NodeBird',
twits: posts,
});
} catch (err) {
console.error(err);
next(err);
}
});
module.exports = router;
const express = require('express');
const { isLoggedIn } = require('./middlewares');
const User = require('../models/user');
const router = express.Router();
router.post('/:id/follow', isLoggedIn, async (req, res, next) => {
try {
const user = await User.findOne({ where: { id: req.user.id } });
if (user) {
await user.addFollowing(parseInt(req.params.id, 10));
res.send('success');
} else {
res.status(404).send('no user');
}
} catch (error) {
console.error(error);
next(error);
}
});
module.exports = router;
.
.
.
passport.deserializeUser((id, done) => {
User.findOne({
where: { id },
include: [{
model: User,
attributes: ['id', 'nick'],
as: 'Followers',
}, {
model: User,
attributes: ['id', 'nick'],
as: 'Followings',
}],
})
.then(user => done(null, user))
.catch(err => done(err));
});
local();
kakao();
};
const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const { Post, User, Hashtag } = require('../models');
const router = express.Router();
router.use((req, res, next) => {
// user 정보 가져와서 팔로잉 / 팔로워 수 저장
res.locals.user = req.user;
res.locals.followerCount = req.user ? req.user.Followers.length : 0;
res.locals.followingCount = req.user ? req.user.Followings.length : 0;
res.locals.followerIdList = req.user ? req.user.Followings.map(f => f.id) : [];
next();
});
router.get('/profile', isLoggedIn, (req, res) => {
res.render('profile', { title: '내 정보 - NodeBird' });
});
router.get('/join', isNotLoggedIn, (req, res) => {
res.render('join', { title: '회원가입 - NodeBird' });
});
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: 'NodeBird',
twits: posts,
});
} catch (err) {
console.error(err);
next(err);
}
});
router.get('/hashtag', async (req, res, next) => {
const query = req.query.hashtag;
if (!query) {
return res.redirect('/');
}
try {
const hashtag = await Hashtag.findOne({ where: { title: query } });
let posts = [];
if (hashtag) {
posts = await hashtag.getPosts({ include: [{ model: User }] });
}
return res.render('main', {
title: `${query} | NodeBird`,
twits: posts,
});
} catch (error) {
console.error(error);
return next(error);
}
});
module.exports = router;
const express = require('express');
const cookieParser = require('cookie-parser');
const morgan = require('morgan');
const path = require('path');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
const passport = require('passport');
.
.
.
app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/img', express.static(path.join(__dirname, 'uploads')));
app.use(express.json());
.
.
.
app.use('/', pageRouter);
app.use('/auth', authRouter);
app.use('/post', postRouter);
app.use('/user', userRouter);
.
.
.
[Node.js] 11장 노드 서비스 테스트하기 (0) | 2022.01.06 |
---|---|
[Node.js] 10장 웹 API 서버 만들기 (0) | 2021.12.27 |
[Node.js] 8장 몽고디비 (0) | 2021.12.01 |
[Node.js] 7장 MySQL (0) | 2021.11.29 |
[Node.js] 6장 익스프레스 웹 서버 만들기 (0) | 2021.11.15 |