๐ ๊ณต๋ถํ๋ ์ง์ง์ํ์นด๋ ์ฒ์์ด์ง?
[E-Commerce App with REST API] (20) Product์์ cloudinary ๋ก ์ด๋ฏธ์ง ์ ๋ก๋ํ๊ณ ๊ด๋ฆฌํ๊ธฐ ๋ณธ๋ฌธ
[E-Commerce App with REST API] (20) Product์์ cloudinary ๋ก ์ด๋ฏธ์ง ์ ๋ก๋ํ๊ณ ๊ด๋ฆฌํ๊ธฐ
์ง์ง์ํ์นด 2023. 4. 11. 01:14<๋ณธ ๋ธ๋ก๊ทธ๋ Developers Corner ์ ์ ํ๋ธ๋ฅผ ์ฐธ๊ณ ํด์ ๊ณต๋ถํ๋ฉฐ ์์ฑํ์์ต๋๋ค :-)>
=> Node.js E-Commerce App with REST API: Let's Build a Real-Life Example!
๐ท Cloudinary
: ์น์ฌ์ดํธ ๋ฐ ๋ชจ๋ฐ์ผ ์ ํ๋ฆฌ์ผ์ด์ ์ฉ ์ด๋ฏธ์ง์ ๋์์์ ๊ด๋ฆฌ, ์ต์ ํ, ์ ์กํ ์ ์๋ ํ๋ซํผ์ ์ ๊ณตํ๋ ํด๋ผ์ฐ๋ ๊ธฐ๋ฐ ์๋น์ค
: ์๋ฒ์ ์ด๋ฏธ์ง๋ฅผ ์ ์ฅํ์ง ์๊ณ storage์ ๋ฐ๋ก ์ด๋ฏธ์ง ํ์ผ์ ์ ์ฅํ๊ณ ์ ํจ
npm i multer sharp cloudinary
โ .env ํ์ผ์ ๋ฐ๋ก ์ ์ฅํ๊ธฐ
// Configuration
cloudinary.config({
cloud_name: "~~~",
api_key: "~~",
api_secret: "~~~"
});
โ Multer
: ํ์ผ์ ์ ๋ก๋ ํ๊ธฐ ์ํ node.js ๋ฏธ๋ค์จ์ด
โ Sharp
: node.js์์ ์ด๋ฏธ์ง๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ข์ ํจํค์ง
: ์ด๋ฏธ์ง์ ์ฌ์ด์ฆ๋ฅผ ๋ณ๊ฒฝ ๊ฐ๋ฅ
โ Path
: ํ์ผ/ํด๋/๋๋ ํฐ๋ฆฌ ๋ฑ์ ๊ฒฝ๋ก๋ฅผ ํธ๋ฆฌํ๊ฒ ์ค์ ํ ์ ์๋ ๊ธฐ๋ฅ
๐ท Product์ ์ด๋ฏธ์ง ์ ๋ก๋ํ๊ธฐ
๐ท ์ฝ๋
โ utils/cloudinary.js
const cloudinary = require("cloudinary");
// Cloudinary API๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด ํ์ํ ์ ๋ณด๋ค
cloudinary.config({
cloud_name: process.env.CLOUD_NAME,
api_key: process.env.API_KEY,
api_secret: process.env.SECRET_KEY,
});
const cloudinaryUploadImg = async (fileToUploads) => {
return new Promise((resolve) => {
// cloudinary์ ์์ ์ ์ฅ๋ ์ด๋ฏธ์ง๋ฅผ ์
๋ก๋
cloudinary.uploader.upload(fileToUploads, (result) => {
resolve(
{
url: result.secure_url,
},
{
resource_type: "auto",
}
);
});
});
};
module.exports = cloudinaryUploadImg;
โ middleweares/uploadimages.js
// ํ์ผ์ ์
๋ก๋ ํ๊ธฐ ์ํ node.js ๋ฏธ๋ค์จ์ด
const multer = require("multer");
// ์ด๋ฏธ์ง๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ข์ ํจํค์ง
const sharp = require("sharp");
// ํ์ผ/ํด๋/๋๋ ํฐ๋ฆฌ ๋ฑ์ ๊ฒฝ๋ก๋ฅผ ํธ๋ฆฌํ๊ฒ ์ค์ ํ ์ ์๋ ๊ธฐ๋ฅ
const path = require("path");
// ์ด๋ฏธ์ง ์ ์ฅ์
const multerStorage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, path.join(__dirname, "../public/images"));
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + "-" + Math.fround(Math.random() * 1e9);
cb(null, file.fieldname + "-" + uniqueSuffix + ".jpeg");
},
});
const multerFilter = (req, file, cb) => {
// mime type : ํ์ผ์ด ์ด๋ ํ ์ข
๋ฅ์ ํ์ผ์ธ์ง์ ๋ํ ์ ๋ณด๊ฐ ๋ด๊ธด ๋ผ๋ฒจ
if (file.mimetype.startsWith("image")) {
// ๋ฐํ๋ฐ์ Mime Type์ ๋ํ ๋ฐ์ดํฐ๊ฐ image ์ธ์ง ์ฒดํฌ
cb(null, true)
}
else {
cb(
{
message: "Unsupported file format",
},
false
);
}
};
const uploadPhoto = multer({
storage: multerStorage,
fileFilter: multerFilter,
limits: { fieldSize: 2000000 },
});
// ์ด๋ฏธ์ง ์ฌ์ด์ฆ ์กฐ์
const productImgResize = async (req, res, next) => {
if (!req.files) return next();
await Promise.all(
req.files.map(async (file) => {
await sharp(file.path)
.resize(300, 300)
.toFormat("jpeg")
.jpeg({ quality: 90 })
.toFile(`public/images/products/${file.filename}`);
})
);
next();
};
const blogImgResize = async (req, res, next) => {
if (!req.files) return next();
await Promise.all(
req.files.map(async (file) => {
await sharp(file.path)
.resize(300, 300)
.toFormat("jpeg")
.jpeg({ quality: 90 })
.toFile(`public/images/blogs/${file.filename}`);
})
);
next();
};
module.exports = {
uploadPhoto,
productImgResize,
blogImgResize
};
โ controllers/productCtrl.js
const Product = require("../models/Product");
const User = require("../models/User");
const asyncHandler = require("express-async-handler");
const slugify = require("slugify");
const { validateMongodbID } = require("../utils/validateMongodbID");
const cloudinaryUploadImg = require("../utils/cloudinary");
// ์ํ ๋ฑ๋ก
const createProduct = asyncHandler(async (req, res) => {
try {
if (req.body.title) {
// slugify : ํ
์คํธ๋ฅผ url ์ฃผ์๋ก ๋ณํํด์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
// slug : ์ด๋ฏธ ์ป์ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ์ฌ ์ ํจํ URL์ ์์ฑ (URL๊ณผ ์๋ฏธ์๋ ์ด๋ฆ์ ์ฌ์ฉ)
req.body.slug = slugify(req.body.title);
}
const newProduct = await Product.create(req.body);
res.json(newProduct);
} catch (error) {
throw new Error(error);
}
});
// ์ํ ์์
const updateProduct = asyncHandler(async (req, res) => {
const { id } = req.params;
validateMongodbID(id);
try {
if (req.body.title) {
// slugify : ํ
์คํธ๋ฅผ url ์ฃผ์๋ก ๋ณํํด์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
// slug : ์ด๋ฏธ ์ป์ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ์ฌ ์ ํจํ URL์ ์์ฑ (URL๊ณผ ์๋ฏธ์๋ ์ด๋ฆ์ ์ฌ์ฉ)
req.body.slug = slugify(req.body.title);
}
const updateProduct = await Product.findOneAndUpdate(id,
req.body, {
new: true,
});
res.json(updateProduct);
} catch (error) {
throw new Error(error);
}
});
// ์ํ ์ญ์
const deleteProduct = asyncHandler(async (req, res) => {
const { id } = req.params;
validateMongodbID(id);
try {
if (req.body.title) {
// slugify : ํ
์คํธ๋ฅผ url ์ฃผ์๋ก ๋ณํํด์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
// slug : ์ด๋ฏธ ์ป์ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ์ฌ ์ ํจํ URL์ ์์ฑ (URL๊ณผ ์๋ฏธ์๋ ์ด๋ฆ์ ์ฌ์ฉ)
req.body.slug = slugify(req.body.title);
}
const deleteProduct = await Product.findOneAndDelete(id);
res.json(deleteProduct);
} catch (error) {
throw new Error(error);
}
});
// ์ํ id ์กฐํ
const getAProduct = asyncHandler(async (req, res) => {
const { id } = req.params;
validateMongodbID(id);
try {
const findProduct = await Product.findById(id);
res.json(findProduct);
} catch (error) {
throw new Error(error);
}
});
// ๋ชจ๋ ์ํ ์กฐํ
const getAllProduct = asyncHandler(async (req, res) => {
try {
// 1) Filtering => t?price[lt]=50000
const queryObj = { ...req.query };
const excludeFields = ["page", "sort", "limit", "fields"];
excludeFields.forEach((el) => delete queryObj[el])
console.log(queryObj);
// JSON.stringify : JavaScript ๊ฐ์ด๋ ๊ฐ์ฒด๋ฅผ JSON ๋ฌธ์์ด๋ก ๋ณํ
let queryStr = JSON.stringify(queryObj);
// Create operators ($gt, $gte, etc)
queryStr = queryStr.replace(/\b(gt|gte|lt|lte)\b/g, (match) => `$${match}`);
// JSON.parse : JSON ๋ฌธ์์ด์ ์ธ์๋ก ๋ฐ๊ณ ๊ฒฐ๊ณผ๊ฐ์ผ๋ก JavaScript ๊ฐ์ฒด๋ฅผ ๋ฐํ
let query = Product.find(JSON.parse(queryStr));
// 2)Sorting => ?sort=-category,-brand
if (req.query.sort) {
const sortBy = req.query.sort.split(",").join(" ");
query = query.sort(sortBy);
} else {
query = query.sort("-createdAt");
}
// 3) Limiting the fields => ?fields=-title,-price,-category
if (req.query.fields) {
const fields = req.query.fields.split(",").join(" ");
query = query.select(fields);
} else {
query = query.select("-__v");
}
// 4) pagination => ?page=4&limit=5
// ํ์ด์ง ๋๋๊ธฐ, ์ฟผ๋ฆฌ์ ๊ฒฐ๊ณผ๊ฐ์ผ๋ก ๋ฆฌํด๋ ๋ฆฌ์์ค๋ฅผ ๋ถํ ํ์ฌ ์ ๋ฌ
const page = req.query.page;
const limit = req.query.limit;
const skip = (page - 1) * limit;
query = query.skip(skip).limit(limit);
if (req.query.page) {
const productCount = await Product.countDocuments();
if (skip >= productCount) {
throw new Error("This page does not exists");
}
}
// console.log(page, limit, skip);
const product = await query;
res.json(product);
} catch (error) {
throw new Error(error);
}
});
// ์์๋ฆฌ์คํธ์ ์ํ ๋ฃ๊ธฐ
const addTowishList = asyncHandler(async (req, res) => {
const { _id } = req.user;
const { prodId } = req.body;
try {
const user = await User.findById(_id);
const alreadyadded = user.wishList.find((id) => id.toString() === prodId);
if (alreadyadded) {
let user = await User.findByIdAndUpdate(_id, {
$pull: { wishList: prodId },
}, {
new: true,
});
res.json(user);
} else {
let user = await User.findByIdAndUpdate(_id, {
$push: { wishList: prodId },
}, {
new: true,
});
res.json(user);
}
} catch (error) {
throw new Error(error);
}
});
// ์ํ ๋ณ์ ๋งค๊ธฐ๊ธฐ
const rating = asyncHandler(async (req, res) => {
const { _id } = req.user;
const { star, comment, prodId } = req.body;
try {
const product = await Product.findById(prodId);
let alreadyRated = product.ratings.find(
(userId) => userId.toString() === _id.toString());
if (alreadyRated) {
const updateRating = await Product.updateOne(
{
ratings: { $elemMatch: alreadyRated },
}, {
$set: { "ratings.$.star": star, "ratings.$.comment": comment },
}, {
new: true
});
// res.json(updateRating);
} else {
const rateProduct = await Product.findByIdAndUpdate(
prodId,
{
$push: {
ratings: {
star: star,
comment: comment,
postedby: _id,
},
},
});
// res.json(rateProduct);
}
// ์ด ๋ณ์ ํ๊ท ๋ด๊ธฐ
const getallratings = await Product.findById(prodId);
let totalRating = getallratings.ratings.length; // ๊ฐ์
let ratingsum = getallratings.ratings
.map((item) => item.star)
.reduce((prev, curr) => prev + curr, 0);
let actualRating = Math.round(ratingsum / totalRating);
let finalproduct = await Product.findByIdAndUpdate(
prodId, {
totalrating: actualRating,
}, {
new: true
});
res.json(finalproduct);
} catch (error) {
throw new Error(error);
}
});
const uploadImages = asyncHandler(async (req, res) => {
const { id } = req.params;
validateMongodbID(id);
try {
const uploader = (path) => cloudinaryUploadImg(path, "images");
const urls = [];
const files = req.files;
for (const file of files) {
const { path } = file;
const newpath = await uploader(path);
urls.push(newpath);
}
const findProduct = await Product.findByIdAndUpdate(
id,
{
images: urls.map((file) => {
return file;
}),
},
{
new: true,
}
);
res.json(findProduct);
} catch (error) {
throw new Error(error);
}
});
module.exports = {
createProduct,
updateProduct,
deleteProduct,
getAProduct,
getAllProduct,
addTowishList,
rating,
uploadImages
};
โ routes/productRoute.js
const express = require("express");
const { createProduct, getAProduct, getAllProduct, updateProduct, deleteProduct, addTowishList, rating, uploadImages } = require("../controllers/productCtrl");
const { isAdmin, authMiddleware } = require("../middlewares/authMiddleware");
const { uploadPhoto, productImgResize } = require("../middlewares/uploadimages");
const router = express.Router();
router.post("/", authMiddleware, isAdmin, createProduct);
router.put("/upload/:id", authMiddleware, isAdmin, uploadPhoto.array("images", 10), productImgResize, uploadImages);
router.get("/:id", getAProduct);
router.put("/wishlist", authMiddleware, addTowishList);
router.put("/rating", authMiddleware, rating);
router.put("/:id", authMiddleware, isAdmin, updateProduct);
router.delete("/:id", authMiddleware, isAdmin, deleteProduct);
router.get("/", getAllProduct);
module.exports = router;