기능 : 로그인, 이미지 업로드, 게시글 작성, 해시태그 검색, 팔로잉 등
{
"name": "nodebird",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "nodemon app"
},
"author": "LEETAEHEE",
"license": "MIT",
"devDependencies": {
"nodemon": "^2.0.7"
},
"dependencies": {
"bcrypt": "^5.0.1",
"cookie-parser": "^1.4.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-session": "^1.17.1",
"morgan": "^1.10.0",
"multer": "^1.4.2",
"mysql2": "^2.2.5",
"nunjucks": "^3.2.3",
"passport": "^0.4.1",
"passport-kakao": "^1.0.1",
"passport-local": "^1.0.0",
"sequelize": "^6.6.2",
"sequelize-cli": "^6.2.0"
}
}
npm i -D nodemon
npm i express express-session nunjucks morgan cookie-parser sequelize mysql2 sequelize-cli dotenv multer
npx sequelize init
npx sequelize init(전역설치처럼 이용하기 - npx)
를 사용하면 config, migrations, models, seeders 폴더가 생성됩니다.
자체적으로 view, routes, public, passport 폴더도 생성합니다.
app.js와 .env 파일도 NordBird 폴더안에 만들어줍니다.
.env에는 COOKIE_SECRET=nodebirdsecret을 추가합니다.
기본적인 라우터와 템플린 엔진도 만듭니다.
routes 폴더안에 page.js
views 폴더안에 layout.html, main.thml, profile.html, join.html, error.html을 생성
약간의 디자인을 위해 public 폴더안에 main.css
//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(); //require한 다음 최대한 위에 적어주는게 좋다.
//dotenv를 하는 순간 process env의 설정 값들이 들어가는데 선언한 이후로 들어간다.
//만약 이 위에 process.env.COOKIE_SECRET 이런게 있다면 dotenv 적용이 안된다.
const pageRouter = require('./routes/page');
const app = express();
app.set('port', process.env.PORT || 8001); //8001번 포트를 쓰겠다. 개발할 때, 배포할 때 포트를 달리 사용하기 위해 process.env.PORT한거
app.set('view engine', 'html');
nunjucks.configure('views', { //템플릿 엔진을 위한 설정
express: app,
watch: true,//watch 옵션을 true로 설정하면 HTML 파일이 변경될 때 템플릿 엔진을 다시 렌더링하도록 한다.
});
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) => { //모든 라우터들 뒤에 나오니깐 404처리 미들웨어
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.~ 는 템플릿 엔진의 변수임.
res.locals.error = process.env.NODE_ENV !== 'production' ? err : {}; //개발 모드에서는 에러 상세 내역을 보여주게 하고, 개발 이외의 모드라면 안보여주게 처리하는거임.
res.status(err.status || 500);
res.render('error');
//메서드 체이닝 방식 : res.status(~).render('error');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기중');
});
//routes/page.js -> 템플릿 엔진을 렌더링하는 라우터
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();
}); //res.locals로 묶은 이유는 모든 템플릿 엔진에서 공통으로 사용하기 때문.
router.get('/profile', (req, res) => { //profile 페이지 보여줌
res.render('profile', { title: '내 정보 - NodeBird' });
});
router.get('/join', (req, res) => { //join 페이지 보여줌
res.render('join', { title: '회원가입 - NodeBird' });
});
router.get('/', (req, res, next) => { //메인 페이지 보여줌
const twits = []; //SNS 들어가자마자 보이는 대표적인? 게시물들을 넣어주는 공간. 일단은 게시물 없으니 빈 배열로 만듦.
res.render('main', {
title: 'NodeBird',
twits,
});
});
module.exports
콘솔에
npx sequelize db:create
하면 디폴트 설정인 development DB 설정대로 스키마를 만들어준다.
//config/config.js (시퀄라이즈 설정)
{
"development": { //개발용 DB
"username": "root",
"password": "MySQL_비밀번호",
"database": "nodebird",
"host": "127.0.0.1",
"dialect": "mysql"
//operatorAliases 속성은 삭제해준다
},
"test": { //테스트용 DB
"username": "root",
"password": null,
"database": "nodebird_test",
"host": "127.0.0.1",
"dialect": "mysql"
},
"production": { //배포용 DB
"username": "root",
"password": null,
"database": "nodebird_production",
"host": "127.0.0.1",
"dialect": "mysql"
}
}
models/index.js 는 기본적으로 생성되는것에서 아래 처럼 수정
//models/index.js
//기본적으로 생성되는 형식이 있는데 깔끔하게 바꾼거임!
const Sequelize = require('sequelize');
const env = process.env.NODE_ENV || 'development';
const config = require('../config/config')[env]; //config에 config.js의 development 객체를 담아두는거임
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; //사용자와 게시글은 1:N 관계
db.Post = Post; //게시글과 해시태그는 N:M 관계
db.Hashtag = Hashtag;
User.init(sequelize);
Post.init(sequelize);
Hashtag.init(sequelize);
User.associate(db);
Post.associate(db);
Hashtag.associate(db);
module.exports = db;
sns구현을 위해선 사용자 테이블, 게시물 테이블, 해시태그 테이블이 필요합니다.
//models/user.js
const Sequelize = require('sequelize');
module.exports = class User extends Sequelize.Model { //공식문서를 따르는 형식
static init(sequelize) {
return super.init({
email: { //시퀄라이즈는 id 생략한다.
type: Sequelize.STRING(40),
allowNull: true,
unique: true, //빈 값이 있어도 unique하게 구분된다.
},
nick: {
type: Sequelize.STRING(15),
allowNull: false,
},
password: {
type: Sequelize.STRING(100), //해시화하면 길어지기 때문에 100글자로 지정해놓은거
allowNull: true, //sns 로그인하는 경우에는 비밀번호가 없을 수 있다.
},
provider: {
type: Sequelize.STRING(10),
allowNull: false,
defaultValue: 'local', //local을 통해 로그인한거 / kakao, facebook 등등 가능
},
snsId: { //카카오, 네이버 등등으로 로그인하면 snsId라는걸 주는데 그걸 저장하고 있어야만 나중에
//로그인할 때 id처럼 활용할 수 있다.
type: Sequelize.STRING(30),
allowNull: true,
},
}, {
sequelize,
timestamps: true, //true - createdAt, updatedAt이 자동으로 기록된다.
underscored: false,
modelName: 'User',
tableName: 'users',
paranoid: true, //paranoid - deletedAt이 자동으로 기록된다.
charset: 'utf8',
collate: 'utf8_general_ci',
});
}
// 관계 설정하는 곳
//user와 post는 1:N관계
//user와 user은 N:M관계
static associate(db) {
db.User.hasMany(db.Post);
db.User.belongsToMany(db.User, { //사용자 테이블 간의 관계를 표현한거임.
foreignKey: 'followingId',//왜 여기선 foreignKey를 넣어줬냐면
//안넣어주면 userId와 userId가 돼서 헷갈리게 된다.
//그래서 일부러 구분 짓기 위해 foreignKey 넣어준거.
as: 'Followers',//as에는 foreignKey와 반대되는 것을 넣어줘야됨. -> 그래야 나중에 followers들을 가져올 때 followingId를 보고 가져올 수 있다.
through: 'Follow', //Follow라는 중간 테이블을 만들어줌.
});
db.User.belongsToMany(db.User, {
foreignKey: 'followerId',
as: 'Followings',
through: 'Follow',
});
}
};
//시퀄라이즈는 as 이름을 바탕으로 자동으로
//addFollower, getFollwers, addFollowing, getFollowings 메서드를 생성해준다.
//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' }); //N:M 관계에서 중간 테이블 이름은 through로 지어준다.
}//foreignKey 안넣어주면 기본적으로 postId랑 hashtagId가 된다.
//as를 안넣어주면 기본적으로 post.getHashtags, post.addHashtags, hashtags.getPosts 같은 기본이름으로 관계 메서드가 생성됩니다.
};
//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' });
}
};
생성된 모델은
db.sequelize.models.PostHashtag
db.sequelize.models.Follow
와같이 접근가능하다.
위 app.js 를 수정하여 서버와 모델을 연경한다.
//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 }) //sequelize.sync()가 테이블 생성해준다.
//테이블 정의한거 수정했다고(ex - hashtag.js 수정했다고 db의 테이블이 바로 바뀌지 않음) 테이블이 자동으로 수정되는게 아님!
//두 가지 방법 있음 - 첫번째 force: true - 테이블이 지워졌다가 다시 생성됨(대신 데이터가 지워지는거니까 조심해야한다.)
//alter : true - 데이터는 유지하고 테이블 컬럼 바뀐걸 반영하고 싶을 때 사용(컬럼이랑 기존 데이터들이랑 안맞아서 에러 나는 경우가 많다.
//예를 들어 allowNull이 false인 컬럼을 추가했을 때 기존 데이터들은 그 컬럼에 해당하는 데이터가 없어서 에러 발생함)
//일단 force: false로 해놓고 수정사항 있으면 true로 변경사항 반영 / 실무에서는 force : true 절대 쓰면 안된다. only 개발용
.then(() => {//promise기 때문에 .then(), .catch() 붙여주면 좋음.
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 i passport passport-local, passport-kakao bcrypt
//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(); // passport/index.js 실행한거
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());//요청 객체에 passport 설정을 심음.
app.use(passport.session());//req.session 객체에 passport 정보를 저장.
//이 두 개는 session보다 아래에 있어야 된다.
//얘네가 있음으로써 로그인 이후 요청부터 passport.session()이 실행될 때 index.js의 deserializeUser()가 실행된다.
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
const passport = require('passport');
const local = require('./localStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');
module.exports = () => {
passport.serializeUser((user, done) => { //auth.js에서 여기로 넘어온다.
done(null, user.id); //그러면 user.id만 뽑아서 done을 해준다. -> req.session 객체에 user의 id만 저장하는거
//user만 쓰면 유저를 통째로 저장할 수 있긴 하지만 서버 메모리를 너무 많이 잡아먹기 때문에 id만 저장함
//세션에 아이디를 저장해야 되는 이유 : 메모리에 { id: 3, 'connect.sid': s%1242535325356 } 이런식으로 저장되는데
//connect.sid는 세션 쿠키임. 세션 쿠키는 브라우저로 간다. 브라우저에서 요청을 보낼 때마다 쿠키를 같이 넣어서 보내줌.
//서버가 이 쿠키를 보고 3번 사용자의 쿠키구나 라는걸 인식함
//그리고 3번 사용자를 deserializeUser해서 복구를 해줌
//done 되는 순간 auth.js의 나머지 부분을 실행하러 돌아감
});
//req.session에 저장된 사용자 아이디를 바탕으로 DB 조회를 하여 사용자 정보를 얻어낸 후 유저 정보 전체를 복구해서 req.user에 저장해줌.
passport.deserializeUser((id, done) => {
User.findOne({ where: { id } })
.then(user => done(null, user))
.catch(err => done(err));
});
// 로그인 되어 있을 때 req.user을 하면 로그인한 사용자의 정보가 나옴, req.isAuthenticated()은 로그인 했으면 true 아니면 false 반환.
local();
kakao();
};
//routes/middlewares.js
//-> 로그인했는지 안했는지 여부를 체크해주는 라우터
exports.isLoggedIn = (req, res, next) => {
if (req.isAuthenticated()) {//req.isAuthenticated()가 true면 로그인 되어 있는거
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 { Post, User } = require('../models');
const router = express.Router();
router.use((req, res, next) => { //use를 하면 모든 라우터에 공통 적용되는 특성 적용한거
res.locals.user = req.user; //req.user는 passport의 deserializeUser에서 나온거
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', { //찾은 게시물들을 twits에 넣어준다.
title: 'NodeBird',
twits: posts,
});
} catch (err) {
console.error(err);
next(err);
}
});
module.exports = router;
//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) => {//회원가입 라우터
//async로 가려면 inNotLoggedIn을 통과해야 하므로, 로그인 안한 사람들만 접근할 수 있게 해줌
const { email, nick, password } = req.body; //이메일, 이름, 비밀번호를 받아와서
try {
const exUser = await User.findOne({ where: { email } }); //가입했던 이메일인지 확인해준다.
if (exUser) {
return res.redirect('/join?error=exist');
//있으면 ?error=exist라는
//쿼리 스트링을 붙여서 redirect 해줌.
//그러면 프론트엔트 개발자는 이 쿼리 스트링을 보고
//이미 이메일이 존재하는 이메일이구나를 알아차림
}
const hash = await bcrypt.hash(password, 12); //존재하지 않는 이메일이면 회원가입 시킨다.
//회원가입 할 때 hash화를 해서 회원가입시킴.
//(두번째 인자는 얼마나 복잡하게 할 것인지임,
//숫자가 클수록 해킹 위험은 적지만 오래 걸림.)
await User.create({
email,
nick,
password: hash, //비밀번호만 hash화 해서 유저 생성
});
return res.redirect('/'); //DB에 생성한 후 redirect로 메인 페이지로 돌아가기
} catch (error) {
console.error(error);
return next(error);
}
});
//로그인은 세션 문제도 있고, SNS로 로그인 할 때
//local로 로그인 할 때가 달라서 로직이 복잡해지기 때문에
//passport 라이브러리를 사용한다.
//프론트에서 서버로 로그인 요청을 보낼 때 아래의 라우터가
//실행되는데 그 때 passport.authenticate('local')까지 실행되고
//이게 실행되면 localStrategy.js을 찾아가서 그 파일을 실행한다!!
//(index.js에서 local()을 했기 때문인가? 어떻게 찾아간다는거지)
router.post('/login', (req, res, next) => {
//req.user -> 로그인 하기 전이니까 안들어 있음.
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) => {//로그인이 성공한 경우 req.login을 사용하며 사용자 객체(user)를 넣어준다.
//req.login을 하는 순간 req.login(user, )
//까지만 실행되고 passport/index.js으로
//가서 passport.serializeUser((user, done) 이걸 실행한다.
if (loginError) {
console.error(loginError);
return next(loginError); //에러가 있었으면 에러 처리하러 가고
}
//여기서 세션 쿠키를 브라우져로 보내준다.
//그 다음 요청 부터는 세션 쿠키가 보내져서
//서버가 누가 로그인 했는지 알게 되는거
return res.redirect('/'); //없었으면 메인 페이지로 돌아가고 로그인 성공!
});
})(req, res, next); // 미들웨어 내의 미들웨어에는
//(req, res, next)를 붙인다.
//(이게 미들웨어 확장법)
});
router.get('/logout', isLoggedIn, (req, res) => { //로그인 한 사람만 로그아웃 할 수 있게 isLoggedIn 추가함.
//req.user -> 로그인 되어 있는 상태니까 들어있음
req.logout(); //이거 실행하면 서버에서 세션 쿠키를 삭제해버림
//-> 로그인이 됐는지 안됐는지는 세션에 세션 쿠키가
//들어 있나 없나를 확인하는 것이기 때문에 없으면
//로그아웃됨을 알 수 있다.
req.session.destroy();
res.redirect('/');
});
// 이 부분은 카카오 로그인 전력
router.get('/kakao', passport.authenticate('kakao')); //카카오 로그인하기를 누르면
//passport.authenticate('kakao')가 실행된다.
//-> 이게 실행되면 kakaoStrategy.js으로 간다.
//그리고 카카오 홈페이지 갔다옴
router.get('/kakao/callback', passport.authenticate('kakao', {
failureRedirect: '/', //카카오 로그인 실패시 여기로 옴
}), (req, res) => { //kakaoStrategy에서 로그인에 성공했으면 여기로 온다.
res.redirect('/');
});
//근데 /kakao/callback 요청을 한적이 없는데??
//-> 카카오에서 이쪽으로 요청을 쏴줌
module.exports = router;
//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({//프론트에서 요청의 body로 email과 password를 보내줘야됨
usernameField: 'email', //얘가 req.body.email이 되야됨
passwordField: 'password',//req.body.password
}, async (email, password, done) => { //위의 'email' == email, 'password' == password
try {
const exUser = await User.findOne({ where: { email } });
if (exUser) { //이메일 가진 사람이 있으면
const result = await bcrypt.compare(password, exUser.password); //프론트에서 받은 비밀번호와 DB의 비밀번호를
//비교해서 true, false를 리턴
if (result) { //비번 일치
done(null, exUser); //첫번째 인수 기본적으로 null,
//두번째는 성공했을 경우에는 유저 객체를 넣어줌.
//그리고 done이 실행되면 아까 auth.js에서 실행하다
//여기로 넘어왔는데 나머지를 실행하러 다시 돌아간다.
} else { //비번 불일치
done(null, false, { message: '비밀번호가 일치하지 않습니다.' }); //실패했을 때는 두번째 인자에 false,
//세번째는 실패 이유
}
} else { //이메일 가진 사람이 없을 때
done(null, false, { message: '가입되지 않은 회원입니다.' });
}
} catch (error) {
console.error(error);
done(error); //서버 에러 났을 때는 이렇게 처리
}
}));
};
//passport/kakaoStrategy.js
const passport = require('passport');
const KakaoStrategy = require('passport-kakao').Strategy;
const User = require('../models/user');
module.exports = () => {
passport.use(new KakaoStrategy({
//localStrategy와는 다르게 clientID, callbackURL이 담긴다.
clientID: process.env.KAKAO_ID,
callbackURL: '/auth/kakao/callback',
}, async (accessToken, refreshToken, profile, done) => { //accessToken, refreshToken은 OAUTH2를 공부해보자 / 지금은 profile만 받아온다.
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, //이메일은 profile._json.kakao_~에 있음
nick: profile.displayName,
snsId: profile.id,
provider: 'kakao',
});
done(null, newUser); //회원가입 후 로그인
//회원가입과 로그인이 동시에 일어난다.
}
} catch (error) {
console.error(error);
done(error);
}
}));
};
만약 form 태그의 enctype이 multipart/form-data일 때는 body-parser로는 요청 본문을 해석할 수 없으므로 multer가 필요하다.
npm i multer로 패키지 설치해준다.
//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 { //uploads 폴더에 파일들을 업로드 하는데
//없으면 안되니깐 없다면 생성하게 하는 코드임.
fs.readdirSync('uploads');
} catch (error) {
console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.');
fs.mkdirSync('uploads');
}
const upload = multer({ //업로드 미들웨어
storage: multer.diskStorage({
//diskStorage는 이미지를 서버 디스크에 저장한다는거
destination(req, file, cb) { //저장 경로
cb(null, 'uploads/');
//uploads 폴더에 img를 업로드 하겠다.
},
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 }, //파일 용량 제한: 5MB
});
//이미지 업로드하는 라우터
router.post('/img', isLoggedIn,
upload.single('img'), (req, res) => {
//요청 본문의 img에 담긴 이미지 하나를
//읽어 설정대로 저장하는 미들웨어
//로그인한 사람이 post /img 요청을 보내면서
//form에서 img라는 키로 이미지를 업로드해야됨.
//key가 일치해야된다.
console.log(req.file);
res.json({ url: `/img/${req.file.filename}` });
//업로드 완료하면 이 파일을 요청할 수 있는
//url을 프론트로 돌려보내줌 / 실제 파일은
//uploads에 있는데 요청 주소는 img/임
//-> express static이 이 역할 수행
});
module.exports = router;
//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}` });
});
//왜 이미지 업로드하는 라우터,
//게시글 업로드 하는 라우터 따로 분리해 놓을까?
//이미지, 게시글 동시에 업로드하게 라우터를 묶어
//놓으면 이미지 압축하는데 시간이 오래 걸리기
//때문에 업로드하는데 많은 시간이 소요된다.
//하지만 분리해 놓는다면 특히 이미지를 먼저 받고
//게시글을 그 후에 받는다면
//게시글을 작성하는 동안 이미지를 압축하므로
//시간을 단축할 수 있다.
//다만 url은 프론트로 보내줘서 게시글이랑 url
//(먼저 업로드한 이미지, 동영상 파일이 어디에
//있는지에 대한 주소)이랑은 묶여있게끔 해준다.
//게시글 업로드하는 라우터
const upload2 = multer();
router.post('/', isLoggedIn, upload2.none(), async (req, res, next) => {
//위에서 이미지 같은건 이미 업로드 했기 때문에
//body들만 업로드하면 돼서 upload.none()한거
try {
console.log(req.user);
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); //정규 표현식 / / 안에 사용한다.
//알아두면 좋음!!
//[]는 여러개중 하나, ^는 부정, \s는 띄어쓰기, #은 샵, *은 모두, g는 골라라
//-> 띄어쓰기랑 샵이 아닌 애들 모두를 골라라라는 의미임
//set() 사용하면 중복 되는 것도 막아줘서 더 좋음
//-> 사용하는 방법 알아두면 좋을듯
if (hashtags) { //해쉬태그가 있다면
const result = await Promise.all(
//시퀄라이즈 메서드들은 모두 promise니깐
//한번에 처리해주기 위해 promise.all함
hashtags.map(tag => {
//findOrCreate와 비슷한거: upsert
//-> update+insert 존재하지 않는다면
//추가하고 존재한다면 update한다.
//공식문서 참고!
return Hashtag.findOrCreate({
//findOrCreate -> 중복 저장되지 않도록 해준다.
//검사를 해서 이미 있다면 넘어가고 없다면
//생성해주는 시퀄라이즈 메서드임.
where: { title: tag.slice(1).toLowerCase() }, // tag.slice.toLowerCase가 [#노드, #익스프레스]를 [노드, 익스프레스] 로 만들어준다.
//이걸 findOrCreate하니깐 findOrCreate(노드), findOrCreate(익스프레스) 이런식으로 됨
})
}),
);
//console.log(result);
//-> [[해시태그 객체, true], [해시태그 객체, false]]
//이런식으로 이차원 배열로 결과값이 나온다.
//true는 생성되었다는거고 false는 이미 존재한다 라는 뜻.
await post.addHashtags(result.map(r => r[0]));
//게시글에서 해시태그를 찾아서 게시글과 연결해준다.
//addFollowings([1, 2, 3])처럼 id를 넣어도 되고, 시퀄라이즈 객체 자체를 넣어도 된다.
}
res.redirect('/');
} catch (error) {
console.error(error);
next(error);
}
});
module.exports = router;
const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const { Post, User } = require('../models');
const router = express.Router();
router.use((req, res, next) => { //use를 하면 모든 라우터에 공통 적용되는 특성 적용한거
res.locals.user = req.user; //req.user는 passport의 deserializeUser에서 나온거임
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', { //찾은 게시물들을 twits에 넣어준다.
title: 'NodeBird',
twits: posts,
});
} catch (err) {
console.error(err);
next(err);
}
});
module.exports = router;
//routes/user.js
const express = require('express');
const { isLoggedIn } = require('./middlewares');
const User = require('../models/user');
const router = express.Router();
//REST API는 동사를 사용하면 안되지만 어쩔 수 없이 타협해서 follow 사용함. -> HTTP API정도로 말할 수 있을듯
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));
//parseInt는 req.params.id를 10진수로 바꾸는거다
//(1:N 관계이기 때문에 복수 사용 가능) /
//setFollowings를 하면 팔로잉하는 목록 수정 가능
//-> 기존 목록 다 제거하고 통째로 대체하는거임 /
//removeFollowings하면 제거 /
//getFollwings는 팔로잉 목록 가져오기
//-> 관계 쿼리임
res.send('success');
} else {
res.status(404).send('no user');
}
} catch (error) {
console.error(error);
next(error);
}
});
module.exports = router;
//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: [{
//req.user안에다가 followings, followers
//들을 넣어주려면 include를 넣어줘야 한다.
//req.user.Followers, req.user.Followings로
//팔로워, 팔로잉 접근 가능
model: User,
//둘 다 model: User이기 때문에 구별이
//필요한 것들은 as를 통해 구별해준다.
attributes: ['id', 'nick'],
//보안상 위험하기 때문에 프론트에서 꼭
//필요한 것들만 프론트로 보내준다.
as: 'Followers',
}, {
model: User,
attributes: ['id', 'nick'],
as: 'Followings',
}],
})
.then(user => done(null, user))
//디시리얼라이즈로부터 req.user, req.isAuthenticated()이 생성된다.
.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; //req.user는 deserializeUser로부터 생성된다.(중요)
res.locals.followerCount = req.user ? req.user.Followers.length : 0; //req.user가 있다는건 로그인한 경우라는 것이고
//그렇다면 팔로워 수를 알려주고, 그게 아니라면 0
res.locals.followingCount = req.user ? req.user.Followings.length : 0;
res.locals.followerIdList = req.user ? req.user.Followings.map(f => f.id) : [];
//팔로잉하고 있는 사람들의 아이디들 알려줌
//-> 이미 팔로우 하고 있는 사람들은 [팔로우하기]
//버튼대신 [언팔로우] 버튼을 보여줘야 하기
//때문에 FollowingIdList를 받아온거(변수명 잘못 지으셨대..)
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);
}
});
// GET /hashtag?hashtag=노드 -> 이런식으로 요청이 들어온다.
//한글 검색할 때는 문제가 생길 수도 있기 때문에 데이터를 보낼 때
//주소에 한글이 있다면 encodeURIComponent를
//사용해주고 서버쪽에서는 decodeURIComponent로
//받아줘야 한다.
router.get('/hashtag', async (req, res, next) => { //해시태그 검색 페이지라서 page.js에 추가함
const query = decodeURIComponent(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, attributes: ['id', 'nick'] }] }); //hashtag.getPosts해서 해당하는 해시태그에 딸려있는 게시글들을 가져와준다.
//belongsToMany이기 때문에 getPosts로 복수형을 사용함
//include: [{ model: User }]로 게시글의 작성자까지 다 가져온다.
//만약 include에 model: User만 하면 id, password, provider ...
//프론트로 모두 다 가져오는데 이러면 보안상 위협되므로
//attributes 설정해서 꼭 필요한 것만 보내주는게 좋다.
}
return res.render('main', {
title: `#${query} 검색 결과 | NodeBird`,
twits: posts,
});
} catch (error) {
console.error(error);
return next(error);
}
});
module.exports = router;