λΉλμ€ νμΌ μ λ‘λ - multer middleware
μ¬μ©μ νλ‘ν - populate() ( video owner / user videos )
//- 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")
//- μ΄ν μλ΅
μ μ λ§λ€μλ 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 μ ν
// videoRouter.js
import { uploadVideo } from "../middlewares";
videoRouter.route("/upload").all(protectorMiddleware).get(getUpload).post(uploadVideo.single("video"), 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("/");
};
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κ° λ¨λ κ²μ νμΈν μ μλ€.
μ¬μ©μκ° λ€λ₯Έ μ¬μ©μμ νλ‘νμ ν΄λ¦νλ©΄ ν΄λΉ μ¬μ©μμ μ΄λ¦, μλ°ν, μ λ‘λν μμ λ±μ λ³Ό μ μλλ‘ λ§λ€ κ²μ΄λ€.
μμ λ°μ μμμ μ¬λ¦° μ¬μ©μμ μ΄λ¦μ νμνκ³ , μμμ μ¬λ¦° μ¬μ©μκ° μλλΌλ©΄ κ·Έ λ°μ edit / delete λ§ν¬λ λ³Ό μ μλλ‘ ν κ²μ΄λ€.
userSchemaμ videoListλ₯Ό μΆκ°νκ³ , videoScheamμ ownerλ₯Ό μΆκ°ν΄μΌ νλ€.
base.pug νμΌμ my profile λ©λ΄λ₯Ό μΆκ°νλ€.
μ΄ urlμ λ‘κ·ΈμΈν userμ μ 보λ₯Ό κΈ°λ°μΌλ‘ κ·Έ _idλ₯Ό κ°μ Έμ λ§λ€μμ§λ§, λꡬλ μ κ·Όν μ μλλ‘ ν΄λΉ νμ΄μ§λ λͺ¨λμκ² κ³΅κ°ν κ²μ΄λ€.
//- base.pug
li
a(href=`/users/${loggedInUser._id}`) My Profile
// userRouter.js
userRouter.get("/:id([0-9a-f]{24})", 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 });
};
//- profile.pug
extends base
video owner / videoList
userμλ ν΄λΉ userκ° μ
λ‘λν λͺ¨λ videoμ idλ₯Ό μ μ₯νλ€.
videoμλ ν΄λΉ videoμ μ
λ‘λν userμ idλ₯Ό μ μ₯νλ€.
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 },
});
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" : "syong@naver.com",
"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
}
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 →
λ€μμΌλ‘ 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: 'syong@naver.com',
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
}
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: 'syong@naver.com',
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
}
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 νλ€.
νμ¬λ λ‘κ·ΈμΈλ§ λμ΄ μμΌλ©΄ ν΄λΉ 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("/");
};
μ 리νλ©΄,
user profile μ²μλΆν° λ€μ ꡬνν΄λ³΄κΈ°
κ°μ κ³μ λ£κΈ°