
다음 내용은 인프런에서 공부한 내용을 복습하는 차원에서 기록한 것입니다.
출처 : https://www.inflearn.com/course/%EB%85%B8%EB%93%9C-js-%EA%B5%90%EA%B3%BC%EC%84%9C
...
{% for twit in twits %}
<div class="twit">
<input type="hidden" value="{{twit.User.id}}" class="twit-user-id">
<input type="hidden" value="{{twit.id}}" class="twit-id">
<div class="twit-author">{{twit.User.nick}}</div>
{% if not followingIdList.includes(twit.User.id) and twit.User.id !== user.id %}
<button class="twit-follow">팔로우하기</button>
{% elif twit.User.id !== user.id %}
<button class="twit-unfollow">팔로우 취소하기</button>
{% endif %}
<div class="twit-content">{{twit.content}}</div>
{% if twit.img %}
<div class="twit-img"><img src="{{twit.img}}" alt="섬네일"></div>
{% endif %}
</div>
{% endfor %}
...
...
exports.unfollow = async (req, res, next) => {
try {
// 현재 로그인한 사용자의 id를 통해 User 모델에서 user 객체를 가져옴
const user = await User.findOne({ where: { id: req.user.id } });
if (user) {
// req.params.id(게시글 작성자 id) 조건에 맞는 데이터를 remove 함
// req.params.id는 기본적으로 문자열을 반환함
await user.removeFollowing(parseInt(req.params.id, 10));
res.redirect('/');
} else {
res.status(404).send('no user');
}
} catch (error) {
console.error(error);
next(error);
}
};
...
<div class="user-name">{{'안녕하세요! ' + user.nick + '님'}}</div>
<div class="half">
<div>팔로잉</div>
<div class="count following-count">{{followingCount}}</div>
</div>
<div class="half">
<div>팔로워</div>
<div class="count follower-count">{{followerCount}}</div>
</div>
<input id="my-id" type="hidden" value="{{user.id}}">
<a id="my-profile" href="/profile" class="btn">내 프로필</a>
<a id="my-profile-update" href="/profile/update" class="btn">프로필 수정</a>
<a id="logout" href="/auth/logout" class="btn">로그아웃</a>
...
{% extends 'layout.html' %}
{% block content %}
<div class="timeline">
<form id="join-form" action="/profile/update_process" method="post">
<div class="input-group">
<label for="join-email">이메일</label>
<input id="join-email" type="email" name="email" value="{{user.email}}"></div>
<div class="input-group">
<label for="join-nick">닉네임</label>
<input id="join-nick" type="text" name="nick" value="{{user.nick}}"></div>
<div class="input-group">
<label for="join-password">비밀번호</label>
<input id="join-password" type="password" name="password">
</div>
<div class="input-group">
<label for="join-password_check">비밀번호 확인</label>
<input id="join-password_check" type="password" name="password_check">
</div>
<button id="join-btn" type="submit" class="btn">수정하기</button>
</form>
</div>
{% endblock %}
{% block script %}
<script>
window.onload = () => {
if (new URL(location.href).searchParams.get('error')) {
alert('이미 존재하는 이메일입니다.');
}
};
</script>
{% endblock %}
const express = require('express');
const router = express.Router();
const { renderJoin, renderMain, renderProfile, renderHashtag, renderProfileUpdate, updateProfile } = require('../controllers/page');
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
// 공통적으로 사용하기 원하는 변수들을 res.locals로 설정
// 로그인 과정이 다 끝나면 여기서 라우터들이 동작함
router.use((req, res, next) => {
res.locals.user = req.user; // 로그인되어 있지 않으면 req.user는 null값을 가짐
res.locals.followerCount = req.user?.Followers?.length || 0;
res.locals.followingCount = req.user?.Followings?.length || 0;
res.locals.followingIdList = req.user?.Followings?.map(f => f.id) || [];
next();
});
router.get('/profile', isLoggedIn, renderProfile);
// 프로필 수정 페이지 라우터 설정
router.get('/profile/update', isLoggedIn, renderProfileUpdate);
// 프로필 수정하는 컨트롤러의 메서드와 연결하는 라우터 설정
router.post('/profile/update_process', isLoggedIn, updateProfile);
router.get('/join', isNotLoggedIn, renderJoin);
router.get('/', renderMain);
router.get('/hashtag', renderHashtag); // hashtag?hashtag=node
module.exports = router;
// 라우터 -> 컨트롤러(요청, 응답을 알고 있음, req랑 res) -> 서비스(요청, 응답을 모름)
const Post = require('../models/post');
const User = require('../models/user');
const Hashtag = require('../models/hashtag');
const bcrypt = require('bcrypt');
...
exports.renderProfileUpdate = (req, res, next) => {
res.render('profile_update', { title: '프로필 수정 - NodeBird' })
};
exports.updateProfile = async (req, res, next) => {
const { nick, email, password } = req.body; // 구조분해 할당에 의해 req.body에서 자동으로 할당됨
try {
// 사용자가 email도 변경했을 경우 DB에서 중복체크
if (req.user.email != email) {
const exUser = await User.findOne({ where: { email } });
if (exUser) {
return res.redirect('/profile/update?error=exist');
}
}
const hash = await bcrypt.hash(password, 12); // 비밀번호 암호화
await User.update({
email: email,
nick: nick,
password: hash,
}, {
where: { id: req.user.id }
});
return res.redirect('/');
} catch(err) {
console.log(err);
next(err);
}
};
...
...
{% for twit in twits %}
<div class="twit">
<input type="hidden" value="{{twit.User.id}}" class="twit-user-id">
<input type="hidden" value="{{twit.id}}" class="twit-id">
<div class="twit-author">{{twit.User.nick}}</div>
{% if user and twit.User.id == user.id %}
<button style="float:right;" class="twit-delete">삭제</button>
<button style="float:right; margin-right: 10px;" class="twit-update">수정</button>
{% endif %}
{% if not followingIdList.includes(twit.User.id) and twit.User.id !== user.id %}
<button class="twit-follow">팔로우하기</button>
{% elif twit.User.id !== user.id %}
<button class="twit-unfollow">팔로우 취소하기</button>
{% endif %}
<div class="twit-content">{{twit.content}}</div>
{% if twit.img %}
<div class="twit-img"><img src="{{twit.img}}" alt="섬네일"></div>
{% endif %}
</div>
{% endfor %}
...
<script>
...
document.querySelectorAll('.twit-update').forEach(function(tag) {
tag.addEventListener('click', function(event) {
const myId = document.querySelector('#my-id'); // 로그인된 id
const twitContent = tag.parentNode.querySelector('.twit-content').textContent;
const twitImg = tag.parentNode.querySelector('.twit-img img');
const twitImgSrc= twitImg ? twitImg.src : '';
if (myId) {
const userId = tag.parentNode.querySelector('.twit-user-id').value; // 작성자 id
if (userId === myId.value) {
document.getElementById('twit').value = twitContent;
document.getElementById('img-preview').src = twitImgSrc;
document.getElementById('img-url').value = twitImgSrc;
if (twitImgSrc) {
document.getElementById('img-preview').style.display = 'inline';
}
}
}
isEditMode = true; // 수정 모드로 변경
editingTwitId = tag.parentNode.querySelector('.twit-id').value;
});
});
document.getElementById('twit-btn').addEventListener('click', function(event) {
event.preventDefault();
const twitContent = document.getElementById('twit').value;
const twitImgSrc = document.getElementById('img-url').value;
if (isEditMode) {
// 수정 API 호출
axios.put(`/post/${editingTwitId}/update`, { content: twitContent, url: twitImgSrc, isEdit: isEditMode, postId: editingTwitId })
.then(() => {
location.reload();
})
.catch((error) => {
console.error(error);
});
} else {
// 새 트윗 생성 API 호출
axios.post('/post', { content: twitContent, url: twitImgSrc })
.then(() => {
location.reload();
})
.catch((err) => {
console.error(err);
});
}
});
document.querySelectorAll('.twit-delete').forEach(function(tag) {
tag.addEventListener('click', function(event) {
const myId = document.querySelector('#my-id'); // 로그인된 id
const twitId = tag.parentNode.querySelector('.twit-id').value;
if (myId) {
const userId = tag.parentNode.querySelector('.twit-user-id').value; // 작성자 id
if (userId === myId.value) {
if (confirm('정말로 게시물을 삭제하겠습니까?')) {
axios.delete(`/post/${twitId}/delete`)
.then(() => {
location.reload();
})
.catch((err) => {
console.error(err);
});
}
}
}
});
});
</script>
const express = require('express');
const router = express.Router();
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
const fs = require('fs');
const multer = require('multer');
const path = require('path');
const { afterUploadImage, uploadPost, deletePost } = require('../controllers/post');
try {
fs.readdirSync('uploads');
} catch (error) {
fs.mkdirSync('uploads');
}
// 이미지를 올리기 위한 multer
const upload = multer({
storage: multer.diskStorage({
destination(req, file, cb) {
console.log(file);
cb(null, 'uploads/');
},
filename(req, file, cb) {
console.log(file);
const ext = path.extname(file.originalname); // 확장자를 추출
cb(null, path.basename(file.originalname, ext) + Date.now() + ext); // 이미지.png -> 이미지12039123.png와 같이 변경(중복 제거를 위해)
}
}),
limits: { fileSize: 20 * 1024 * 1024 },
});
...
router.put('/:id/update/img', isLoggedIn, upload.single('img'), afterUploadImage);
router.put('/:id/update', isLoggedIn, upload2.none(), uploadPost);
router.delete('/:id/delete', isLoggedIn, deletePost);
module.exports = router;
...
exports.uploadPost = async (req, res, next) => {
console.log('uploadPost', req.body); // req.body.content와 req.body.url를 가져올 수 있음
try {
let post = null;
if (req.body.isEdit) {
await Post.update({
content: req.body.content,
img: req.body.url,
UserId: req.user.id,
}, {
where: {
id: req.body.postId
}
});
post = await Post.findOne({ where: { id: req.body.postId } });
} else {
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) {
// Post와 Hashtag 사이의 다대다 관계 생성
const result = await Promise.all(hashtags.map((tag) => {
return Hashtag.findOrCreate({
where: { title: tag.slice(1).toLowerCase() }
});
}));
// Hashtag.findOrCreate()의 반환값으로 [instance, created] 형태의 배열을 반환
// instance에는 값이, created에는 새로운 해시태그가 생성되었는지 여부가 들어 있음
await post.addHashtags(result.map(r => r[0]));
}
res.send('success'); // 원래는 res.redirect('/');
} catch (error) {
console.error(error);
next(error);
}
};
document.getElementById('twit-btn').addEventListener('click', function(event) {
event.preventDefault();
const twitContent = document.getElementById('twit').value;
const twitImgSrc = document.getElementById('img-url').value;
if (isEditMode) {
// 수정 API 호출
axios.put(`/post/${editingTwitId}/update`, { content: twitContent, url: twitImgSrc, isEdit: isEditMode, postId: editingTwitId })
.then(() => {
location.reload();
})
.catch((error) => {
console.error(error);
});
} else {
// 새 트윗 생성 API 호출
axios.post('/post', { content: twitContent, url: twitImgSrc })
.then(() => {
location.reload();
})
.catch((err) => {
console.error(err);
});
}
});
exports.deletePost = async (req, res, next) => {
try {
const post = await Post.findOne({ where: { id: req.params.id } });
if (post) {
await post.destroy();
}
} catch (error) {
console.error(error);
next(error);
}
};
/post/${twitId}/like 로 Post 요청/post/${twitId}/unlike 로 Post 요청
...
{% if twit.img %}
<div class="twit-img"><img src="{{twit.img}}" alt="섬네일"></div>
{% endif %}
{% if user %}
{% if not likes[twits.indexOf(twit)].includes(user.id) %}
<button class="twit-like">좋아요</button>
{% else %}
<button class="twit-unlike">좋아요 취소</button>
{% endif %}
{% endif %}
...
<script>
...
document.querySelectorAll('.twit-like').forEach(function(tag) {
tag.addEventListener('click', function() {
const myId = document.querySelector('#my-id');
if (myId) {
const userId = tag.parentNode.querySelector('.twit-user-id').value;
const twitId = tag.parentNode.querySelector('.twit-id').value;
if (userId !== myId.value) {
axios.post(`/post/${twitId}/like`)
.then(() => {
location.reload();
})
.catch((err) => {
console.error(err);
});
}
}
});
});
document.querySelectorAll('.twit-unlike').forEach(function(tag) {
tag.addEventListener('click', function() {
const myId = document.querySelector('#my-id');
if (myId) {
const userId = tag.parentNode.querySelector('.twit-user-id').value;
const twitId = tag.parentNode.querySelector('.twit-id').value;
if (userId !== myId.value) {
axios.post(`/post/${twitId}/unlike`)
.then(() => {
location.reload();
})
.catch((err) => {
console.error(err);
});
}
}
});
});
...
</script>
...
exports.renderMain = async (req, res, next) => {
try {
const posts = await Post.findAll({
include: [
{
model: User,
attributes: ['id', 'nick'],
},
{
model: User,
attributes: ['id', 'nick'],
as: 'Likers',
}],
order: [['createdAt', 'DESC']]
});
res.render('main', {
title: 'NodeBird',
twits: posts,
likes: posts.map((v) => v.Likers.map((v) => v.id)),
});
} catch (error) {
console.error(error);
next(error);
}
};
...
...
// 테이블 관계
static associate(db) {
db.Post.belongsTo(db.User);
db.Post.belongsToMany(db.Hashtag, { through: 'PostHashtag' });
db.Post.belongsToMany(db.User, {
through: 'Like',
as: 'Likers',
});
}
...
...
// 테이블 관계
static associate(db) {
db.User.hasMany(db.Post);
// 연예인의 팔로워를 찾으려면 연예인의 id로 찾아야 함
db.User.belongsToMany(db.User, { // 팔로워(유명 연예인의 팬)
foreignKey: 'followingId',
as: 'Followers',
through: 'Follow'
});
// 내가 팔로잉을 하는 사람을 찾으려면 나의 id로 찾아야 함
db.User.belongsToMany(db.User, { // 팔로잉 (유명 연예인)
foreignKey: 'followerId',
as: 'Followings',
through: 'Follow'
});
db.User.belongsToMany(db.Post, {
through: 'Like',
as: 'LikedPosts',
});
}
...
const fs = require('fs');
const multer = require('multer');
const path = require('path');
const { afterUploadImage, uploadPost, deletePost, likePost, unlikePost } = require('../controllers/post');
...
router.delete('/:id/delete', isLoggedIn, deletePost);
router.post('/:id/like', isLoggedIn, likePost);
router.post('/:id/unlike', isLoggedIn, unlikePost);
module.exports = router;
...
const Post = require('../models/post');
const User = require('../models/user');
const Hashtag = require('../models/hashtag');
...
exports.likePost = async (req, res, next) => {
try {
const post = await Post.findOne({ where: { id: req.params.id } });
if (post) {
await post.addLiker(parseInt(req.user.id, 10));
res.send('success');
} else {
res.status(404).send('no user');
}
} catch (error) {
console.error(error);
next(error);
}
};
exports.unlikePost = async (req, res, next) => {
try {
const post = await Post.findOne({ where: { id: req.params.id } });
if (post) {
await post.removeLiker(parseInt(req.user.id, 10));
res.send('success');
}
} catch (error) {
console.error(error);
next(error);
}
};
{% extends 'layout.html' %}
{% block content %}
<div class="timeline">
<div class="followings 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 class="like-list">
<h2>좋아요 목록</h2>
{% if user.LikedPosts %}
{% for content in LikedPostContentList %}
<div>{{content}}</div>
{% endfor %}
{% endif %}
</div>
</div>
{% endblock %}
const passport = require('passport');
const local = require('./localStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');
const Post = require('../models/post');
module.exports = () => {
...
// 결과적으로 req.user를 만드는 곳임
passport.deserializeUser((id, done) => {
User.findOne({
where: { id },
// as: 'Followers'와 as: 'Followings'는 User 모델이 Follow 테이블을 통해
// 자기 자신과 맺고 있는 팔로우 관계를 나타내며, 이 관계를 통해 팔로워와 팔로잉 사용자의 정보를 함께 조회할 수 있음
include: [
{
model: User,
attributes: ['id', 'nick'],
as: 'Followers',
},
{
model: User,
attributes: ['id', 'nick'],
as: 'Followings',
},
{
model: Post,
attributes: ['id', 'content'],
as: 'LikedPosts',
},
]
})
.then((user) => done(null, user)) // 그 복원된(조회된) 정보가 req.user가 됨
.catch(err => done(err));
});
local();
kakao();
};
const express = require('express');
const router = express.Router();
const { renderJoin, renderMain, renderProfile, renderHashtag, renderProfileUpdate, updateProfile } = require('../controllers/page');
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
// 공통적으로 사용하기 원하는 변수들을 res.locals로 설정
// 로그인 과정이 다 끝나면 여기서 라우터들이 동작함
router.use((req, res, next) => {
res.locals.user = req.user; // 로그인되어 있지 않으면 req.user는 null값을 가짐
res.locals.followerCount = req.user?.Followers?.length || 0;
res.locals.followingCount = req.user?.Followings?.length || 0;
res.locals.followingIdList = req.user?.Followings?.map(f => f.id) || [];
res.locals.LikedPostContentList = req.user?.LikedPosts?.map(f => f.content) || [];
next();
});
...
<a> 태그를 이용해 클릭 시 주소 이동하게끔 변경...
<input type="hidden" value="{{twit.User.id}}" class="twit-user-id">
<input type="hidden" value="{{twit.id}}" class="twit-id">
<a href="/user/{{twit.User.id}}"><button class="twit-author">{{twit.User.nick}}</button></a>
{% if user and twit.User.id == user.id %}
<button style="float:right;" class="twit-delete">삭제</button>
<button style="float:right; margin-right: 10px;" class="twit-update">수정</button>
{% endif %}
...
...
.twit-author {
font-weight: bold;
font-size: 16px;
margin-right: 10px;
border: none;
background: none;
padding: 0;
cursor: pointer;
}
.twit-author:hover {
opacity: 0.5;
}
.twit-author:active {
opacity: 0.5;
}
...
const express = require('express');
const router = express.Router();
const { renderJoin, renderMain, renderProfile, renderHashtag, renderProfileUpdate, updateProfile, searchUserPost } = require('../controllers/page');
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
...
router.get('/join', isNotLoggedIn, renderJoin);
router.get('/', renderMain);
router.get('/hashtag', renderHashtag); // hashtag?hashtag=node
router.get('/user/:id', searchUserPost);
module.exports = router;
...
exports.searchUserPost = async (req, res, next) => {
try {
console.log(req.params.id);
const user = await User.findOne({ where: { id: req.params.id } });
const posts = await Post.findAll({
where: { UserId: req.params.id },
include: [
{
model: User,
attributes: ['id', 'nick'],
},
{
model: User,
attributes: ['id', 'nick'],
as: 'Likers',
}],
order: [['createdAt', 'DESC']] });
res.render('main', {
title: `${user.nick} | NodeBird`,
twits: posts,
likes: posts.map((v) => v.Likers.map((v) => v.id)) || [],
});
} catch (error) {
console.error(error);
next(error);
}
};
const User = require('../models/user');
const Post = require('../models/post');
...
let userCache = {};
let cacheTTL = 0;
exports.createUserCache = (req, res, next) => {
return {
getUserCache: () => userCache,
getCacheTTL: () => cacheTTL,
setUserCache: async (id) => {
if (id === -1) {
userCache = {};
} else {
userCache = await User.findOne({
where: { id },
include: [
{
model: User,
attributes: ['id', 'nick'],
as: 'Followers',
},
{
model: User,
attributes: ['id', 'nick'],
as: 'Followings',
},
{
model: Post,
attributes: ['id', 'content'],
as: 'LikedPosts',
},
]
})
}
},
setCacheTTL: (newCacheTTL) => {
if (newCacheTTL === 0) {
cacheTTL = 0;
} else {
const TTL = 30 * 1000;
cacheTTL = newCacheTTL + TTL;
}
}
}
};
const kakao = require('./kakaoStrategy');
const User = require('../models/user');
const Post = require('../models/post');
const { createUserCache } = require('../middlewares/index');
const userCache = createUserCache();
module.exports = () => {
...
// 결과적으로 req.user를 만드는 곳임
passport.deserializeUser((id, done) => { // 세션쿠키 값을 통해 얻은 유저 아이디를 가지고 User 정보를 복원시킴
if (Object.keys(userCache.getUserCache()).length !== 0 && userCache.getCacheTTL() > Date.now()){
console.log('Cache: ', userCache.getCacheTTL, Date.now());
return done(null, userCache.getUserCache());
}
User.findOne({
where: { id },
// as: 'Followers'와 as: 'Followings'는 User 모델이 Follow 테이블을 통해
@@ -40,8 +47,12 @@ module.exports = () => {
as: 'LikedPosts',
},
]
})
.then((user) => done(null, user)) // 그 복원된(조회된) 정보가 req.user가 됨
}) // 그 복원된(조회된) 정보가 req.user가 됨
.then((user) => {
done(null, user);
userCache.setUserCache(user.id);
userCache.setCacheTTL(Date.now());
})
.catch(err => done(err));
});
local();
kakao();
};