๐Ÿ˜Ž ๊ณต๋ถ€ํ•˜๋Š” ์ง•์ง•์•ŒํŒŒ์นด๋Š” ์ฒ˜์Œ์ด์ง€?

[E-Commerce App with REST API] (20) Product์—์„œ cloudinary ๋กœ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ ๋ณธ๋ฌธ

๐Ÿ‘ฉ‍๐Ÿ’ป ๋ฐฑ์—”๋“œ(Back-End)/Node js

[E-Commerce App with REST API] (20) Product์—์„œ cloudinary ๋กœ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ

์ง•์ง•์•ŒํŒŒ์นด 2023. 4. 11. 01:14
728x90
๋ฐ˜์‘ํ˜•

<๋ณธ ๋ธ”๋กœ๊ทธ๋Š” 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;

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90
๋ฐ˜์‘ํ˜•
Comments