[TIL] 211205
📝 오늘 한 것
-
비디오 파일 업로드 - multer middleware
-
사용자 프로필 - populate() ( video owner / user videos )
📚 배운 것
user profile
1. video 업로드
1) input(type="file")
//- upload.pug
extends base
block content
if errorMessage
span=errorMessage
form(method="POST", enctype="multipart/form-data")
label(for="video") Video File
input(name="video", type="file", accept="videos/*", id="video")
//- 이하 생략
2) multer middleware
비디오 파일 업로드 - multer middleware
사용자 프로필 - populate() ( video owner / user videos )
user profile
1. video 업로드
1) input(type="file")
//- upload.pug
extends base
block content
if errorMessage
span=errorMessage
form(method="POST", enctype="multipart/form-data")
label(for="video") Video File
input(name="video", type="file", accept="videos/*", id="video")
//- 이하 생략
2) multer middleware
1) input(type="file")
//- upload.pug
extends base
block content
if errorMessage
span=errorMessage
form(method="POST", enctype="multipart/form-data")
label(for="video") Video File
input(name="video", type="file", accept="videos/*", id="video")
//- 이하 생략
2) multer middleware
전에 만들었던 uploadFiels middleware를 각각 파일 크기 제한 옵션을 추가하여 uploadAvatar와 uploadVideo middleware로 나누어 만들었다.
// middlewares.js
export const uploadAvatar = multer({ dest: "uploads/avatars/"}, { limits: { filesize: 5000000 }}); // 5MB 제한
export const uploadVideo = multer({ dest: "uploads/videos/"}, { limits: { filesize: 10000000 }}); // 10MB 제한
3) videoRouter
// videoRouter.js
import { uploadVideo } from "../middlewares";
videoRouter.route("/upload").all(protectorMiddleware).get(getUpload).post(uploadVideo.single("video"), postUpload);
4) videoSchema, postUpload 컨트롤러
Video.js 파일에서 videoSchema에 fileUrl을 추가했다.
// Video.js
const videoSchema = new mongoose.Schema({
title: { type: String, required: true, trim: true, maxLength: 80 },
fileUrl: { type: String, required: true },
description: { type: String, required: true, trim: true, minLength: 20 },
createdAt: { type: Date, required: true, default: Date.now },
hashtags: [{ type: String, trim: true }],
mata: {
views: { type: Number, required: true, default: 0 },
rating: { type: Number, required: true, default: 0 },
},
});
videoController.js 파일에서 postUpload 컨트롤러를 수정했다.
// videoController.js
export const postUpload = async (req, res) => {
const {
body: { title, description, hashtags },
file, // 추가 ❗
} = req;
try {
await Video.create({
title,
fileUrl: file.path, // 추가 ❗
description,
hashtags,
});
} catch(error) {
return res.render("upload", { pageTitle: "Upload Video", errorMessage: error._message });
}
return res.redirect("/");
};
이는 ES6 문법을 이용해 다음과 같이 바꿔볼 수도 있다.
export const postUpload = async (req, res) => {
const { path: fileUrl } = req.file; // 수정 ❗
const { title, description, hashtags } = req.body; // 수정 ❗
try {
await Video.create({
title,
fileUrl, // 수정 ❗
description,
hashtags,
});
} catch(error) {
return res.render("upload", { pageTitle: "Upload Video", errorMessage: error._message });
}
return res.redirect("/");
};
5) video mixins, watch.pug 수정
video.pug와 watch.pug 파일에 video 태그를 추가했다.
//- video.pug
mixin video(video)
div
h4
a(href=`/videos/${video.id}`)=video.title
video(src="/" + video.fileUrl, width="800", controls)
p=video.description
ul
each hashtag in video.hashtags
li=hashtag
small=video.createdAt
hr
//- watch.pug
extends base
block content
video(src="/" + video.fileUrl, width="800", controls)
div
p=video.description
small=video.createdAt
a(href=`${video.id}/edit`) Edit Video →
br
a(href=`${video.id}/delete`) Delete Video →
이제 video 파일을 업로드 하면 video가 뜨는 것을 확인할 수 있다.
2. 사용자 프로필
사용자가 다른 사용자의 프로필을 클릭하면 해당 사용자의 이름, 아바타, 업로드한 영상 등을 볼 수 있도록 만들 것이다.
영상 밑에 영상을 올린 사용자의 이름을 표시하고, 영상을 올린 사용자가 아니라면 그 밑의 edit / delete 링크는 볼 수 없도록 할 것이다.
userSchema에 videoList를 추가하고, videoScheam에 owner를 추가해야 한다.
1) my profile 링크 추가
base.pug 파일에 my profile 메뉴를 추가했다.
이 url은 로그인한 user의 정보를 기반으로 그 _id를 가져와 만들었지만, 누구나 접근할 수 있도록 해당 페이지는 모두에게 공개할 것이다.
//- base.pug
li
a(href=`/users/${loggedInUser._id}`) My Profile
2) userRouter
// userRouter.js
userRouter.get("/:id([0-9a-f]{24})", see);
3) see 컨트롤러
누구나 다른 user의 profile을 볼 수 있도록 하기 위해 req.session.user에서 _id를 가지고 오는 게 아니라 req.params에서 id를 가지고 와야 한다.
// userController.js
export const see = async (req, res) => {
const { id } = req.params;
const user = await User.findById(id);
if (!user) {
return res.render("404", { pageTitle: "User not found" });
}
reutrn res.render("profile", { pageTitle: user.name, user });
};
4) profile.pug 생성
//- profile.pug
extends base
5) video와 user를 연결
video owner / videoList
user에는 해당 user가 업로드한 모든 video의 id를 저장한다.
video에는 해당 video을 업로드한 user의 id를 저장한다.
(1) videoSchema에 owner를 추가
video에 해당 video를 업로드한 user의 id를 저장하려고 한다.
이를 위해 Video.js 파일에서 videoSchema에 owner를 추가했다.
type은 ObjectId가 아니라 mongoose.Schema.Types.ObjectId라고 써야 한다.
ref는 mongoose에게 owner에 User model의 id를 저장하겠다고 알려주기 위해 추가해야 한다.
// Video.js
const videoSchema = new mongoose.Schema({
// 생략
owner: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
});
(2) videoController.js에서 postUpload 컨트롤러 수정
video.owner에 현재 로그인 한 user의 _id를 넣어준다 (video와 user 연결)
// videoController.js
export const postUpload = (req, res) = {
const {
session: {
user: { _id }, // 추가 ❗
},
body: { title, description, hashtags },
file,
} = req;
try {
await Video.create({
fileUrl: file.path,
title,
description,
hashtags: Video.formatHashtags(hashtags),
owner: _id, // 추가 ❗
});
} catch (error) {
return res.status(400).render("upload", {
pageTitle: "Upload Video",
errorMessage: error._message,
});
}
return res.redirect("/");
};
이제 video를 upload 한 후 DB를 확인해보면 owner가 추가된 것을 확인할 수 있다.
그 값은 video를 upload 한 user의 _id 값이다.
video와 user가 연결되었다!
> db.users.find()
{
"_id" : ObjectId("61ac2ca850178cd5ec48b727"),
"email" : "[email protected]",
"avatarUrl" : "https://avatars.githubusercontent.com/u/89576038?v=4",
"username" : "LeeSyong",
"password" : "$2b$05$Ph6WZ/R1cdrM4wcZ0NUBAucc7BNXEEDornqN6sGz703aUvKUtctrO",
"socialOnly" : true,
"name" : "LeeSyong",
"location" : null,
"__v" : 0
}
> db.videos.find()
{
"_id" : ObjectId("61ac2e2365004e56d46c9966"),
"title" : "겨울",
"fileUrl" : "uploads/vidoes/375d678307db8686e3eaa0e92b227416",
"description" : "크리스마스 분위기의 따뜻한 영상입니다",
"hashtags" : [ "#winter", "#video", "#christmas" ],
"mata" : { "views" : 0, "rating" : 0 },
"owner" : ObjectId("61ac2ca850178cd5ec48b727"),
"createdAt" : ISODate("2021-12-05T03:12:35.713Z"),
"__v" : 0
}
(3) edit / delete 링크 숨기기
String(video.owner) === loggedInUser._id
라면, 해당 video를 upload 한 user와 현재 로그인한 user가 같은 사람이란 뜻이므로 edit / delete 링크를 볼 수 있도록 한다.
이때 video.owner은 ObjectId인 데 반해, loggedInUser._id는 String이므로 video.owner에 String()을 씌워야 한다.
//- watch.pug
if String(video.owner) === loggedInUser._id
a(href=`${video.id}/edit`) Edit Video →
br
a(href=`${video.id}/delete`) Delete Video →
(4) video owner 표시하기
다음으로 video를 보러 들어갔을 때 video을 upload 한 user의 이름을 표시하기 위해 watch 컨트롤러와 watch.pug 파일을 다음과 같이 수정하였다.
// videoController.js
export const watch = (req, res) => {
const { id } = req.params;
const video = await Video.findById(id);
if (!video) {
return res.render("404", { pageTitle: "Video not found" });
}
const owner = await User.findById(video.owner);
return res.render("watch", { pageTitle: video.title, video, owner });
};
//- watch
extends base
block content
video(src="/" + video.fileUrl, width="800", controls)
div
p=video.description
small=video.createdAt
div
small Uploaded by
a(href=`/users/${owner._id}`) #{owner.name}
if String(video.owner) === loggedInUser._id
a(href=`${video.id}/edit`) Edit Video →
br
a(href=`${video.id}/delete`) Delete Video →
그러나, videoSchema의 ref: "User"에 의해 video.owner가 곧 해당 video를 올린 user라는 것을 아는데, User DB에서 다시 검색을 해주는 것은 너무 비효율적이다.
이 점을 개선하기 위해 두 번째 DB 검색 결과는 지우고, 대신 populate("owner")
를 사용할 수 있다.
// videoController.js
export const watch = (req, res) => {
const { id } = req.params;
const video = await Video.findById(id).populate("owner"); // 수정 ❗
if (!video) {
return res.render("404", { pageTitle: "Video not found" });
}
return res.render("watch", { pageTitle: video.title, video });
};
populate()를 사용하면, video.owner에 video를 올린 user의 _id가 아니라 해당 user object가 통째로 들어오게 된다.
mongoose는 video.owner에 들어와야 할 값이 ObjectId인데 그 값이 User로부터 온다는 것을 알고 있으므로 video.owner를 해당 user object로 바꿔주는 것이다.
console.log(video)를 통해 살펴보면 다음과 같다
{
mata: { views: 0, rating: 0 },
_id: new ObjectId("61ac2e2365004e56d46c9966"),
title: '겨울',
fileUrl: 'uploads/vidoes/375d678307db8686e3eaa0e92b227416',
description: '크리스마스 분위기의 따뜻한 영상입니다',
hashtags: [ '#winter', '#video', '#christmas' ],
owner: {
_id: new ObjectId("61ac2ca850178cd5ec48b727"),
email: '[email protected]',
avatarUrl: 'https://avatars.githubusercontent.com/u/89576038?v=4',
username: 'LeeSyong',
password: '$2b$05$Ph6WZ/R1cdrM4wcZ0NUBAucc7BNXEEDornqN6sGz703aUvKUtctrO',
socialOnly: true,
name: 'LeeSyong',
location: null,
__v: 0
},
createdAt: 2021-12-05T03:12:35.713Z,
__v: 0
}
(5) user videos 보여주기
userController.js 파일에서 see 컨트롤러(profile 페이지를 보여줌)를 수정한다.
req.params로부터 id를 얻어온 후 이를 통해 DB에서 해당 profile의 주인인 user를 찾는다.
DB에서 video.owner가 id(req.params) 혹은 user._id와 같은 video들을 찾는다.
찾은 videos를 profile 템플릿에 넘겨준 후 profile 템플릿에서 mixins/video를 include 하면 profile 페이지에서 해당 user가 업로드한 video들을 볼 수 있다.
export const see = async (req, res) => {
const { id } = req.params;
const user = await User.findById(id);
const videos = await Video.find({ owner: user._id }); // 혹은 { owner: id } 라고 써도 됨
if (!user) {
return res.status(404).render("404", { pageTitle: "User not found" });
}
return res.render("profile", { pageTitle: user.name, user, videos });
};
//- profile.pug
extends base
include mixins/video
block content
each video in videos
+video(video)
else
li No Videos
한편, 여기서도 위와 마찬가지로 populate("videos")
를 사용할 수 있다.
user에 해당 user가 업로드한 video를 업로드할 때마다 그 video의 id를 저장하려고 한다.
이를 위해 User.js 파일에서 userSchema에 videos를 추가했다.
videos는 ObjectId를 값으로 가지는 배열이다.
// User.js
const userSchema = new mongoose.Schema({
// 생략
videos: [{ type: mongoose.Schema.Types.ObjectId, ref: "Video" }],
});
video를 업로드 할 때 해당 video를 업로드 한 user의 videos에 그 video의 _id가 추가되도록, videoController.js 파일에서 postUpload 컨트롤러를 수정한다.
// videoController.js
export const postUpload = async (req, res) => {
const {
session: {
user: { _id },
},
body: { title, description, hashtags },
file,
} = req;
try {
const newVideo = await Video.create({
file: file.path,
title,
description,
hashtags: Video.formatHashtags(hashtags),
owner: _id,
});
const user = await User.findById(_id);
user.videos.push(newVideo._id);
user.save();
} catch(error) {
return res.status(400).render("upload", { pageTitle: "Upload Video", errorMessage: error._message });
}
return res.redirect("/");
};
populate()를 이용하면 user를 가져올 때 user.videos의 _id 값들을 그 _id 값을 가진 video object로 바꿔준다.
// userController.js
export const see = async (req, res) => {
const { id } = req.params;
const user = await User.findById(id).populate("videos");
// console.log(user);
if (!user) {
return res.status(404).render("404", { pageTitle: "User not found" });
}
return res.render("profile", { pageTitle: user.name, user });
};
console.log(user)를 통해 살펴보면 다음과 같다.
{
_id: new ObjectId("61ac7c5f048c24da7e9df72b"),
email: '[email protected]',
avatarUrl: 'https://avatars.githubusercontent.com/u/89576038?v=4',
username: 'LeeSyong',
password: '$2b$05$DINZAMOVle5hwNO87rnVZekEJmF/vIMEvap8G7qBzi7va/Dulw86y',
socialOnly: true,
name: 'LeeSyong',
location: null,
videos: [
{
mata: [Object],
_id: new ObjectId("61ac7c81048c24da7e9df72e"),
title: '겨울',
fileUrl: 'uploads/vidoes/49acd210e1f4c16ae6891addf62409f0',
description: '겨울겨울겨울겨울겨울겨울겨울겨울겨울겨울',
hashtags: [Array],
owner: new ObjectId("61ac7c5f048c24da7e9df72b"),
createdAt: 2021-12-05T08:46:57.617Z,
__v: 0
},
{
mata: [Object],
_id: new ObjectId("61ac8160bd22b2db227bbc44"),
title: '쓰담쓰담',
fileUrl: 'uploads/vidoes/33aa31574d8b086666e5fafc519fdb91',
description: '쓰담쓰담쓰담쓰담쓰담쓰담쓰담쓰담쓰담쓰담',
hashtags: [Array],
owner: new ObjectId("61ac7c5f048c24da7e9df72b"),
createdAt: 2021-12-05T09:07:44.008Z,
__v: 0
}
],
__v: 3
}
6) 버그 수정
(1) 영상을 올릴 때마다 패스워드가 해싱되는 문제 해결
video를 upload 할 때 해당 video를 upload 하는 user 데이터의 videos에 그 video의 _id를 전해주면서 user.save()를 하는데 이때 User.js 파일의 pre save middleware
에 의해 password가 hashing 된다.
그런데 이렇게 하면 계정을 생성할 때 만들었던 password의 해싱 값이 또 한번 해싱됨으로써 해당 user는 로그아웃을 하는 순간부터는 이전과 같은 password로 로그인할 수 없게 된다.
문제를 해결하기 위해 isModified()를 이용해 password가 수정될 때만 hashing 과정을 거치도록 해야 한다.
// User.js
userSchema.pre("save", async function() {
// 저장하려는 user의 password가 수정된 경우에만 저장 전에 hashing을 거치도록 함 ❗
if (this.isModified("password")) {
this.password = await bcrypt.hash(this.password, 5);
}
});
this란 저장하려는 user document를 말한다.
this.isModified("password")
는 user object의 password property가 수정되었다면 true를 return 한다.
(2) owner가 아닌 로그인 user도 video 수정/삭제가 가능한 문제 해결
현재는 로그인만 되어 있으면 해당 video의 owner가 아니어도 주소 창에 url을 직접 입력하여 edit video와 delete video 페이지에 접근이 가능하다.
로그인한 user라 하더라도 video의 owner가 아니라면 해당 페이지에 접근할 수 없도록 해야 한다.
먼저, videoController.js 파일에서 getEdit와 postEdit 컨트롤러를 모두
수정한다. (postEdit 컨트롤러 또한 백엔드를 보호하기 위해 똑같이 수정해줘야 한다. 수정한 postEdit 컨트롤러는 적지 않았다.)
※ 이때 ObjectId(객체)와 req.session.user._id(문자열)는 데이터 타입이 다르다
는 것에 주의하여 String()을 이용해야 한다.
// videoController.js
export const getEdit = async (req, res) => {
const {
session: {
user: { _id }, // 추가 ❗
},
} = req;
const { id } = req.params;
const video = await Video.findById(id);
// video가 있는지 확인
if (!video) {
return res.status(404).render("404", { pageTitle: "Video not found" });
}
// video의 owner인지 확인 ❗
if (String(video.owner) !== _id) {
return.status(403).redirect("/");
} // status(403)은 forbidden을 뜻함
return res.render("edit", { pageTitle: `Edit ${video.title}`, video });
};
마찬가지로, videoController.js 파일에서 deleteVideo 컨트롤러
를 수정한다.
// videoController.js
export const deleteVideo = async (req, res) => {
const {
session: {
user: { _id }, // 추가 ❗
},
},
const { id } = req.params;
const video = await Video.findById(id);
// video가 있는지 확인
if (!video) {
return res.status(404).render("404", { pageTitle: "Video not found" });
}
// video의 owner인지 확인 ❗
if (video.owner !== _id) {
return res.status(403).redirect("/");
}
// video 삭제
await Video.findByIdAndDelete(id);
return res.redirect("/");
};
정리하면,
- 해당 video가 존재하는지 확인하고
- 작업을 진행하려는 user가 video owner인지 확인한 후에
- edit video 혹은 delete video 해야 한다.
✨ 내일 할 것
-
user profile 처음부터 다시 구현해보기
-
강의 계속 듣기
Author And Source
이 문제에 관하여([TIL] 211205), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다
https://velog.io/@leesyong/TIL-211205
저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)
user profile 처음부터 다시 구현해보기
강의 계속 듣기
Author And Source
이 문제에 관하여([TIL] 211205), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@leesyong/TIL-211205저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)