* 위 내용을 정리하였음
1. multer 패키지로 이미지 업로드 구현하기
- 멀티파트 형식의 이미지를 업로드 해보자.
npm i multer
- 이미지는 서버 디스크에 저장되고 그 경로만 데이터 베이스에 저장된다.
- post라우터를 작성해보자.
// 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;
- app.use('/post')를 할것이기 때문에 POST /post/img 와 POST /post 라우터를 만든다.
- POST /post/img 라우터는 로그인 확인 -> 이미지 업로드 -> 이미지의 저장 경로 클라이언트로 응답
- static 미들웨어가 /img 경로의 정적 파일들을 제공하므로 클라이언트가 이미지에 접근할 수 있다.
- POST /post 라우터는 게시글 업로드를 처리한다.
- 이전 라우터에서 이미지를 업로드하고 req.body.url에 이미지 주소가 전송되었다.
- 게시물은 이미지가 들어 있지 않으므로 none을 사용
- 정규표현식으로 해시태그 부분을 찾고나서 이미 있다면 가져오고 없으면 생성한 후 가져온다. 그리고 모델들을 추출하여 게시물과 해시테그 모델들을 연결한다.
- 이렇게 정적 파일 제공을 원한다면 multer-s3, multer-google-storage같은 패키지를 활용하여 클라우드 스토리지를 사용하는게 좋다.
// 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;
- page.js를 손을 좀 봐서 메인페이지 로딩시 게시물들이 보이도록 한다.
- 게시물 조회시 사용자 모델에서 id, nick 속성을 join해서 게시물 들을 찾는다.
- twits에 게시물 배열을 넣어 랜더링 한다.
2. 팔로잉, 해시태그 기능 구현
// 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;
- :id가 req.params.id가 된다. (현재 로그인한 사용자)
- 로그인 했다면 디비에서 팔로우할 사용자를 조회한 후
- 그 사용자의 팔로워를 추가해준다.
- 팔로우 팔로잉 관계가 생성 되었으므로 req.user에도 팔로우 팔로잉 관계를 저장해준다.
- 사용자 정보를 불러올때 팔로우 팔로잉 정보도 함께 불러오기 때문에 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();
};
- 라우터 들이 실행되기 전에 deserializeUser가 항상 먼저 실행된다. 따라서 모든 요청마다 사용자 정보를 조회한다.
- 따라서 캐싱을 활용할 필요가 있고 실제 서비스에서는 레디스 같은 데이터베이스에 사용자 정보를 캐싱한다.
- follow, follwer를 표시해야 하므로 page.js도 수정한다.
// 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;
- 로그인시에는 req.user가 존재하기 때문에 이것으로 로그인 여부를 확인한다.
- 팔로워 아이디들을 리스트로 저장하는 이유는 팔로워 리스트에 게시글 작성자의 아이디가 없으면 팔로우 버튼을 보여주기 위해서 이다.
- 이제 해시태그 검색 라우터를 만들어보자.
- 쿼리스트링으로 해시태그 이름을 받는다.
- 해시태그가 없으면 메인페이지로 가고 있다면 데이터베이스에서 작성자 정보를 Join해서 post들을 다 가져오자.
3. 최종 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'), '번 포트에서 대기중');
});
- 업로드한 이미지를 제공할 라우터(/img)도 express.static 미들웨어로 uploads 폴더와 연결한다.
- 이렇게 하면 uploads 폴더 내 사진들이 /img 주소로 제공된다.
app.use('/img', express.static(path.join(__dirname, 'uploads')));
'ComputerScience > NodeJs' 카테고리의 다른 글
node - 10.2 웹 API 서버 만들기 (api 호출, 사용량 제한) (0) | 2022.02.09 |
---|---|
node - 10.1 웹 API 서버 만들기 (프로젝트 구조, jwt토큰) (0) | 2022.02.08 |
node - 9.3 익스프레스로 SNS 서비스 만들기(로그인 구현) (0) | 2022.02.06 |
node - 9.2 익스프레스로 SNS 서비스 만들기(SQL DB) (0) | 2022.02.05 |
node - 9.1 익스프레스로 SNS 서비스 만들기(프로젝트 구조) (0) | 2022.02.05 |