나름(?) 제대로된 처음 해본 프로젝트라고 욕심을 많이 냈던것들,,,,
2주간안에 MVP 모델을 만들어야 하는 프로젝트였다,,,
const productService = require("../services/productService");
const reviewService = require("../services/reviewService");
const getProducts = async (req, res) => {
try {
let { category, sortBy, search } = req.query;
const result = await productService.getProductsList(
category,
sortBy,
search
);
return res.status(200).json({ message: result });
} catch (error) {
return res.status(500).json({ message: error.message });
}
};
const getProductList = async (req, res) => {
try {
const result = await productService.getAllProducts();
res.status(200).json(result);
} catch (error) {
console.log(error);
res.status(error.statusCode || 500).json({ message: error.message });
}
};
const getProductDetail = async (req, res) => {
try {
const productId = req.params.productId;
const [product] = await productService.getProduct(productId);
if (product === undefined)
return res.status(404).json({ message: "PRODUCT_NOT_FOUND" });
const review = await reviewService.findReviewByProductId(productId);
if (review[0].id === null) return res.status(200).json({ product });
res.status(200).json({ product, review });
} catch (err) {
console.error(err);
res.status(err.statusCode || 500).json({ message: err.message });
}
};
module.exports = {
getProducts,
getProductDetail,
getProductList,
};
const cartService = require("../services/cartService");
const auth = require("../utils/auth");
const secretKey = process.env.SECRET_KEY;
const addCartItem = async (req, res) => {
try {
const acccesToken = req.headers.authorization;
const decoded = auth.decoded(acccesToken, secretKey);
const userId = decoded.userId;
const productId = req.params.productId;
const quantity = parseInt(req.body.quantity);
if (!productId || !quantity) {
const error = new Error("KEY_ERROR");
error.statusCode = 400;
throw error;
}
const result = await cartService.addCartItem(productId, quantity, userId);
return res.status(200).json({ message: "add success", data: result });
} catch (error) {
return res.status(error.statusCode || 500).json({ message: error.message });
}
};
const getCartItems = async (req, res) => {
try {
const acccesToken = req.headers.authorization;
const decoded = auth.decoded(acccesToken, secretKey);
const userId = decoded.userId;
console.log("토큰 확인용:", acccesToken);
const result = await cartService.getCartItems(userId);
return res.status(200).json({ message: "read success", data: result });
} catch (error) {
return res.status(error.statusCode || 500).json({ message: error.message });
}
};
const updateCartItemQuantity = async (req, res, operation) => {
try {
const acccesToken = req.headers.authorization;
const decoded = auth.decoded(acccesToken, secretKey);
const userId = decoded.userId;
const productId = req.params.productId;
const quantityDifference = req.body.quantityDifference;
if (!productId || !quantityDifference) {
const error = new Error("KEY ERROR");
error.statusCode = 400;
throw error;
}
const result = await cartService.updateCartItemQuantity(
productId,
quantityDifference,
userId
);
return res
.status(200)
.json({ message: `${operation} successful`, data: result });
} catch (error) {
return res.status(error.statusCode || 500).json({ message: error.message });
}
};
const increaseCartItemQuantity = async (req, res) => {
return updateCartItemQuantity(req, res, "Increase");
};
const decreaseCartItemQuantity = async (req, res) => {
return updateCartItemQuantity(req, res, "Decrease");
};
const deleteCartItem = async (req, res) => {
try {
const acccesToken = req.headers.authorization;
const decoded = auth.decoded(acccesToken, secretKey);
const userId = decoded.userId;
const cartId = req.params.cartId;
if (!cartId) {
const error = new Error("KEY_ERROR");
error.statusCode = 400;
throw error;
}
const result = await cartService.deleteCartItem(cartId, userId);
return res.status(200).json({ message: "delete success", data: result });
} catch (error) {
return res.status(error.statusCode || 500).json({ message: error });
}
};
module.exports = {
addCartItem,
getCartItems,
increaseCartItemQuantity,
decreaseCartItemQuantity,
deleteCartItem,
};
검색 기능을 추가하는 과정에서, 사용자가 입력한 검색어를 어떻게 데이터베이스 쿼리에 적용할지에 대한 고민이 있었고
특히, 검색어를 포함하는 제품 이름 또는 설명을 찾기 위해 SQL LIKE 절을 사용해야 했다.
작성하고보니 인젝션 공격에 취약하다는 팀원들의 피드백이 있었고 구글로 찾아보니 인젝션 공격을 예방하기 위해서 검색어를 쿼리에 삽입하기 전에 쿼리 매개변수를 사용하는 것이었다.
제품을 정렬하는 데에도 어려움이 있었다. 사용자가 선택한 정렬 방식에 따라 SQL 쿼리를 동적으로 조정해야 했는데,
그렇게 되면 코드 유지관리와 가독성이 떨어질거 같다는 피드백을 받아 정렬 옵션을 정의하고 이것을 사용하여 동적으로 SQL 정렬 절을 생성하는 함수를 작성했다.
코드 가독성과 유지보수가 용이해졌다고 생각함 또한, 데이터베이스 쿼리의 실행 계획을 최적화하여 성능을 향상시켰다고 생각함.
const getProductsList = async (category, sortBy, search) => {
const ordering = async (sortBy) => {
const sortOptions = {
priceAsc: "ORDER BY p.price ASC , p.id ASC",
priceDesc: "ORDER BY p.price DESC , p.id ASC",
nameAsc: "ORDER BY p.name ASC , p.id ASC",
newest: "ORDER BY p.created_at DESC , p.id ASC",
default: "ORDER BY p.id",
};
return sortOptions[sortBy] || sortOptions.default;
};
const orderingQuery = await ordering(sortBy);
const searchQuery = search
? `AND (p.name like '%${search}%' OR p.content like '%${search}%')`
: "";
const categoryQuery = category ? `AND p.category_id = ${category}` : "";
const result = await productDao.getProducts(
categoryQuery,
searchQuery,
orderingQuery
);
return result;
};
const getAllProducts = async () => {
const newProducts = await productDao.getNewProducts();
const bestProducts = await productDao.getBestProducts();
const mdRecommendation = await productDao.getMDRecommendations();
return { newProducts, bestProducts, mdRecommendation };
};
const getProduct = async (productId) => {
const product = await productDao.findProduct(productId);
return product;
};
const updateProductSales = async (userId) => {
return await productDao.updateSales(userId);
}
module.exports = {
getProductsList,
getAllProducts,
getProduct,
updateProductSales
};
const appDataSource = require("./dataSource");
const getProducts = async (categoryQuery, searchQuery, orderingQuery) => {
const result = await appDataSource.query(`
SELECT
p.id,
p.image,
p.name,
p.price,
AVG(c.score) AS score,
COUNT(c.id) AS commentCount
FROM products p
JOIN comments c
ON p.id = c.products_id
WHERE 1
${categoryQuery}
${searchQuery}
GROUP BY p.id , p.image , p.name , p.price
${orderingQuery}`);
return result;
};
const findProduct = async (productId) => {
const product = await appDataSource.query(`
SELECT
p.id,
p.name,
p.price,
p.image,
p.content,
IFNULL(ROUND(AVG(c.score), 1),0) as average_score
FROM products p
LEFT JOIN comments c ON p.id = c.products_id
WHERE p.id = ${productId}
GROUP BY p.id
`);
return product;
};
const commonQuery = `
SELECT
p.id, p.name, p.price, p.image, p.content,
COALESCE(AVG(c.score), 0) AS score,
COALESCE(COUNT(c.id), 0) AS commentCount
FROM products p
LEFT JOIN comments c ON p.id = c.products_id
`;
const getNewProducts = async () => {
const query = `
${commonQuery}
GROUP BY p.id
ORDER BY p.created_at DESC
LIMIT 10
`;
return await appDataSource.query(query);
};
const getBestProducts = async () => {
const query = `
${commonQuery}
GROUP BY p.id
ORDER BY p.sales DESC
LIMIT 10
`;
return await appDataSource.query(query);
};
const getMDRecommendations = async () => {
const query = `
${commonQuery}
WHERE p.id IN (9, 10, 14, 15, 17, 18, 19)
GROUP BY p.id
`;
return await appDataSource.query(query);
};
const updateSales = async (userId) => {
try {
return await appDataSource.query(
`
UPDATE
products
LEFT JOIN
cart
ON products.id = cart.products_id
SET products.sales = products.sales + cart.quantity
WHERE
cart.users_id = '${userId}'
AND
cart.status = "DONE"
`
)
} catch(e) {
console.error(e);
}
}
module.exports = {
getNewProducts,
getBestProducts,
getMDRecommendations,
findProduct,
getProducts,
updateSales
};
거의 모든 기능들에서 어려움과 고민들이 있었지만 특히 어려웠던 부분은 장바구니 기능에서 DB 수량을 수정하는 부분에서 문제가 생겼다. 코드 개선 전에는 수량에 숫자가 1 이하로 떨어지는 문제가 발생했고 프론트와 협의해 최소수량 1 밑으로 떨어지지 않게 막아버리고 + , - 버튼을 누를때 "+" , "-"를 매개변수로 받아 쿼리에 수량을 수정할 수 있도록 했다.
const updateCartItemQuantity = async (
productId,
quantityDifference,
userId
) => {
if (quantityDifference !== "+" && quantityDifference !== "-") {
throw new Error("Invalid quantityDifference");
}
const currentQuantity = await cartDao.getCartQuantity(productId, userId);
if (quantityDifference === "-" && currentQuantity === 1) {
throw new Error("Quantity cannot be decreased further.");
}
const updateCartItem = await cartDao.updateCart(
productId,
quantityDifference,
userId
);
return updateCartItem;
};
const updateCart = async (productId, quantityDifference, userId) => {
const result = await appDataSource.query(
`UPDATE cart
SET quantity = quantity ${quantityDifference} 1
WHERE products_id = ?
AND users_id = ?`,
[productId, userId]
);
return result;
};
const getCartQuantity = async (productId, userId) => {
const result = await appDataSource.query(
`
SELECT quantity
FROM cart
WHERE products_id = ?
AND users_id = ?
`,
[productId, userId]
);
return result;
};