๊ธฐ๋ฅ: ๋ก๊ทธ์ธ, ์ด๋ฏธ์ง ์ ๋ก๋, ๊ฒ์๊ธ ์์ฑ, ํด์ํ๊ทธ ๊ฒ์, ํ๋ก์
nodebird ํด๋๋ฅผ ๋ง๋ค๊ณ package.json ํ์ผ ์์ฑ
๐ปpackage.json
{
"name": "nodebird",
"version": "0.0.1",
"description": "์ต์คํ๋ ์ค๋ก ๋ง๋๋ SNS ์๋น์ค",
"main": "app.js",
"scripts": {
"start": "nodemon app"
},
"author": "seokahi",
"license": "MIT",
"dependencies": {
"cookie-parser": "^1.4.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-session": "^1.17.1",
"morgan": "^1.10.0",
"mysql2": "^2.1.0",
"nunjucks": "^3.2.1",
"sequelize": "^5.21.6",
"sequelize-cli": "^5.5.1"
},
"devDependencies": {
"nodemon": "^2.0.3"
}
}
์ํ๋ผ์ด์ฆ ํด๋ ๊ตฌ์กฐ ์์ฑ
npm i sequelize mysql2 sequelize-cli
npx sequelize init
views(ํ ํ๋ฆฟ ์์ง), routes(๋ผ์ฐํฐ), public(์ ์ ํ์ผ), passport(ํจ์คํฌํธ) ํด๋ ์์ฑ
npm ํจํค์ง ์ค์น ํ nodemon๋ ์ค์น
npm i express cookie-parser express-session morgan multer dotenv nunjucks
npm i -D nodemeon
๋ ธ๋ ์๋ฒ์ ํต์ฌ์ธ app.js ํ์ผ ์์ฑ
๐ป.env
COOKIE_SECRET=nodebirdsecret
๐ป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.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
}));
app.use('/', pageRouter);
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);
res.render('error');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '๋ฒ ํฌํธ์์ ๋๊ธฐ์ค');
});
- routes/page.js: ํ ํ๋ฆฟ ์์ง์ ๋ ๋๋งํ๋ ๋ผ์ฐํฐ
- views/layout.html: ํ๋ก ํธ ์๋ ํ๋ฉด ๋ ์ด์์(๋ก๊ทธ์ธ/์ ์ ์ ๋ณด ํ๋ฉด)
- views/main.html: ๋ฉ์ธ ํ๋ฉด(๊ฒ์๊ธ๋ค์ด ๋ณด์)
- views/profile.html: ํ๋กํ ํ๋ฉด(ํ๋ก์ ๊ด๊ณ๊ฐ ๋ณด์)
- views/error.html: ์๋ฌ ๋ฐ์ ์ ์๋ฌ๊ฐ ํ์๋ ํ๋ฉด
- public/main.css: ํ๋ฉด CSS
npm start๋ก ์๋ฒ ์คํ ํ http://localhost:8001 ์ ์
models/index.js
์ํ๋ผ์ด์ฆ๊ฐ ์๋์ผ๋ก ์์ฑํด์ฃผ๋ ์ฝ๋ ๋์ ๋ค์๊ณผ ๊ฐ์ด ๋ณ๊ฒฝ
๐ปmodels/index.js
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
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',
});
}
};
๋ชจ๋ธ๊ฐ์ ๊ด๊ณ๋ค associate์ ์์ฑ
๐ปmodels/hastag.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/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) {
db.Post.belongsTo(db.User);
db.Post.belongsToMany(db.Hashtag, { through: 'PostHashtag' });
}
};
๐ป models/user.js
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',
});
}
};
User(๋ค):User(๋ค)
์ํ๋ผ์ด์ฆ ์ค์ ์ config/config.json์์
๐ปconfig/config.json
{
"development": {
"username": "root",
"password": "nodejsbook",
"database": "nodebird",
"host": "127.0.0.1",
"dialect": "mysql",
"operatorAliases": false
},
"test": {
"username": "root",
"password": null,
"database": "database_test",
"host": "127.0.0.1",
"dialect": "mysql"
},
"production": {
"username": "root",
"password": null,
"database": "database_production",
"host": "127.0.0.1",
"dialect": "mysql"
}
}
์ค์ ํ์ผ ์์ฑ ํ nodebird ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ฑ
sequelize.sync()๊ฐ ํ ์ด๋ธ ์์ฑ
๐ป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'));
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('/', pageRouter);
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);
res.render('error');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '๋ฒ ํฌํธ์์ ๋๊ธฐ์ค');
});
npm start๋ก ์๋ฒ ์คํ ์ ์ฝ์์ SQL๋ฌธ์ด ํ์๋จ
๋ก๊ทธ์ธ ๊ณผ์ ์ ์ฝ๊ฒ ์ฒ๋ฆฌํ ์ ์๊ฒ ๋์์ฃผ๋ 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());
app.use('/', pageRouter);
app.use('/auth', authRouter);
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);
res.render('error');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '๋ฒ ํฌํธ์์ ๋๊ธฐ์ค');
});
passport/index.js ์์ฑ
๐ป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) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
User.findOne({ where: { id } })
.then(user => done(null, user))
.catch(err => done(err));
});
local();
kakao();
};
๋ก๊ทธ์ธ ๊ณผ์
๋ก๊ทธ์ธ ์ดํ ๊ณผ์
passport-local ํจํค์ง ํ์
๐ปroutes/middlewares.js
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}`);
}
};
๐ปroutes/page.js
const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const router = express.Router();
router.use((req, res, next) => {
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;
routes/auth.js ์์ฑ
routes/auth.js ์์ฑ
๐ปroutes/auth.js
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();
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);
}
});
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)๋ฅผ ๋ถ์
๋๋ค.
});
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;
passport/localStrategy.js ์์ฑ
๐ปpassport/localStrategy.js
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
const User = require('../models/user');
module.exports = () => {
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'password',
}, 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);
}
}));
};
passport/kakaoStrategy.js ์์ฑ
๐ปpassport/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);
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);
}
}));
};
ํ์๊ฐ์ ๊ณผ ๋ก๊ทธ์ธ์ด ์ ๋ต์์ ๋์์ ์ํ๋จ
๐ปroutes/auth.js
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();
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);
}
});
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)๋ฅผ ๋ถ์
๋๋ค.
});
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;
๐ป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());
app.use('/', pageRouter);
app.use('/auth', authRouter);
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);
res.render('error');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '๋ฒ ํฌํธ์์ ๋๊ธฐ์ค');
});
https://developers.kakao.com์ ์ ์ํ์ฌ ํ์๊ฐ์
REST API ํค๋ฅผ ์ ์ฅํด์ .env์ ์ ์ฅ
๐ป.env
COOKIE_SECRET=nodebirdsecret
KAKAO_ID=5d4daf57becfd72fd9c919882552c4a6
์น ํ๋ซํผ์ ์ถ๊ฐํด์ผ callbackURL ๋ฑ๋กํ ์ ์์
์ด๋ฉ์ผ, ์์ผ ๋ฑ์ ์ ๋ณด๋ฅผ ์ป๊ธฐ ์ํด ๋์ํญ๋ชฉ ์ค์
์นด์นด์คํก ๋ก๊ทธ์ธ ๋ฒํผ์ ๋๋ฅด๋ฉด ์นด์นด์ค ๋ก๊ทธ์ธ ์ฐฝ์ผ๋ก ์ ํ
form ํ๊ทธ์ enctype์ด multipart/form-data
npm i multer
fs.readdir, fs.mkdirSync๋ก upload ํด๋๊ฐ ์์ผ๋ฉด ์์ฑ
multer() ํจ์๋ก ์
๋ก๋ ๋ฏธ๋ค์จ์ด ์์ฑ
upload2.none()์ multipart/formdata ํ์ ์ ์์ฒญ์ด์ง๋ง ์ด๋ฏธ์ง๋ ์์ ๋ ์ฌ์ฉ
๐ปroutes/post.js
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;
๋ฉ์ธ ํ์ด์ง(/) ์์ฒญ ์ ๊ฒ์๊ธ์ ๋จผ์ ์กฐํํ ํ ํ ํ๋ฆฟ ์์ง ๋ ๋๋ง
๐ปroutes/page.js
const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const { Post, User } = require('../models');
const router = express.Router();
router.use((req, res, next) => {
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('/', 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;
POST /:id/follow ๋ผ์ฐํฐ ์ถ๊ฐ
๐ปroutes/user.js
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;
deserializeUser ์์
๐ป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) => {
done(null, user.id);
});
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();
};
๐ปroutes/page.js
const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const { Post, User, Hashtag } = require('../models');
const router = express.Router();
router.use((req, res, next) => {
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;
GET /hashtag ๋ผ์ฐํฐ ์ถ๊ฐ
๐ปroutes/page.js
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;
express.static ๋ฏธ๋ค์จ์ด๋ก uploads ํด๋์ ์ ์ฅ๋ ์ด๋ฏธ์ง ์ ๊ณต
๐ป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 postRouter = require('./routes/post');
const userRouter = require('./routes/user');
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('/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,
},
}));
app.use(passport.initialize());
app.use(passport.session());
app.use('/', pageRouter);
app.use('/auth', authRouter);
app.use('/post', postRouter);
app.use('/user', userRouter);
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);
res.render('error');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '๋ฒ ํฌํธ์์ ๋๊ธฐ์ค');
});
์๋ฒ๋ฅผ ์คํํ๊ณ http://localhost:8001 ์ ์
๐์ถ์ฒ๐
Node.js ๊ต๊ณผ์ - ๊ธฐ๋ณธ๋ถํฐ ํ๋ก์ ํธ ์ค์ต๊น์ง
https://www.inflearn.com/course/%EB%85%B8%EB%93%9C-%EA%B5%90%EA%B3%BC%EC%84%9C/dashboard