(중요한 것만 넣어야겠다.)
express-fileupload: 파일 업로드를 multer를 이용해서 했는데, 얘도 똑같다.
express-session: 세션 데이터를 서버에 저장하고 세션 식별자를 클라이언트에 저장한다. (cookie-session 차이점)
fs-extra: 파일시스템 작업을 수행하는데 파일 및 디렉토리를 생성, 복사, 이동, 삭제하는 등의 작업을 더 쉽게 수행할 수 있습니다. 또한 파일 및 디렉토리의 존재 여부를 확인하고, 파일 내용을 읽고 쓰는 작업도 할 수 있습니다.
mkdirp: 폴더 생성
resize-img: 썸네일 만들어 올리는 것(원래 크기보다 작게)

admin: 관리자 페이지
checkout: 계산하는 페이지

// admin-categories.router.js
router.post('/add-category', checkAdmin, async (req, res) => {
try {
const title = req.body.title;
const slug = title.replace(/\s+/g, '-').toLowerCase();
// 제목 입니다. 제목-입니다. 로 만들어준다.
const category = await Category.findOne({ slug: slug });
// slug에 같은 title + slug가 있는지 찾는다.
if (category) {
req.flash('error', '이미 존재하는 카테고리입니다.');
res.redirect('back');
}
const newCategory = new Category({
title,
slug
})
await newCategory.save();
req.flash('success', '카테고리가 생성되었습니다.');
res.redirect('/admin/categories')
} catch {
console.error(error);
next(error);
}
})
router.get('/', checkAdmin, async (req, res) => {
try {
const categories = await Category.find();
res.render('admin/categories', { categories: categories });
} catch {
console.error(error);
next(error);
}
})
// UI add-product.ejs
<%- include('../partials/header') %>
<div class="d-flex justify-content-between align-items-center">
<h2>상품 생성하기</h2>
<a href="/admin/products" class="btn btn-primary">뒤로</a>
</div>
<br>
<!-- 이미지 파일을 보내기 위해서는 enctype 필수 -->
<form method="POST" action="/admin/products" enctype="multipart/form-data">
<div class="row mb-3">
<label for="title" class="form-label">Title</label>
<input type="text" id="title" class="form-control" name="title" placeholder="Title" required>
</div>
<div class="row mb-3">
<label for="description" class="form-label">Description</label>
<textarea name="desc" id="description" class="form-control" cols="30" rows="10" placeholder="Description" required></textarea>
</div>
<div class="row mb-3">
<label for="category" class="form-label">Category</label>
<select name="category" id="category" class="form-control">
<% categories.forEach((category) => { %>
<option value="<%= category.slug %>"><%= category.title %></option>
<% }) %>
</select>
</div>
<div class="row mb-3">
<label for="price" class="form-label">Price</label>
<input type="number" class="form-control" name="price" id="price" placeholder="Price" required>
</div>
<div class="row mb-3">
<label for="img" class="form-label">Image</label>
<!-- multiple 해주면 여러개 올릴 수 있다. -->
<input multiple type="file" class="form-control" name="image" id="img" required>
<img src="#" id="imgPreview" alt="" class="mt-3" style="width: 100px;">
</div>
<button class="btn btn-primary mb-3">생성하기</button>
</form>
<!-- img src 태그에 내가 올린 이미지 파일이 올라갈 수 있도록 -->
<script>
function readFile(inputEl) {
if (inputEl.files && inputEl.files[0]) {
let reader = new FileReader();
reader.readAsDataURL(inputEl.files[0]);
reader.onload = function(e) {
document.querySelector('#imgPreview').setAttribute('src', e.target.result);
}
}
}
document.querySelector('#img').addEventListener('change', function() {
readFile(this);
})
</script>
<%- include('../partials/footer') %>
router.post('/', async (req, res, next) => {
try {
const {title, desc, price, category} = req.body
const slug = title.replace(/\s+/g, '-').toLowerCase();
const imageFile = req.files.image.name; // filereader api를 이용한다.
const product = await Product.findOne({ slug })
if (product) {
req.flash('error', '이미 존재하는 상품입니다.');
res.redirect('/admin/products');
}
const newProduct = new Product({
title,
slug,
desc,
category,
price,
image: imageFile,
})
await newProduct.save();
// 이미지를 담을 폴더를 생성한다.
await fs.mkdirp('src/public/products-images/' + newProduct._id); // 1경로에 이런 파일이 생긴다?
await fs.mkdirp('src/public/products-images/' + newProduct._id + '/gallery');
await fs.mkdirp('src/public/products-images/' + newProduct._id + '/gallery/thumbs');
// 이미지 파일을 폴더에 넣어주기
const productImage = req.files.image;
const path = 'src/public/products-images/' + newProduct._id + '/' + imageFile;
// 이미지를 path 경로로 옮긴다.
await productImage.mv(path);
req.flash('success', ' 상품이 생성되었습니다.');
res.redirect('/admin/products');
} catch (err) {
console.error(err);
next(err)
}
})
// admin-products.router.js
// 상품 수정 render
router.get('/:id/edit', checkAdmin, async (req, res, next) => {
try {
const categories = await Category.find();
const { _id, title, desc, category, price, image } = await Product.findById(req.params.id);
const galleryDir = 'src/public/products-images/' + _id + '/gallery';
const galleryImages = await fs.readdir(galleryDir);
res.render('admin/edit-product',{galleryImages, title, desc, categories, category: category.replace(/\s+/g, '-').toLowerCase(), price, image, id: _id})
} catch (err) {
console.error(err);
next(err);
}
})
// 상품 이미지 수정
router.post('/product-gallery/:id', checkAdmin, async (req, res, next) => {
const { id } = req.params;
const productImage = req.files.file; // req.files 사용가능한데 우리는 img tag name이 file이라 이렇게 해야된다.
const path = 'src/public/products-images/' + id + '/gallery/' + req.files.file.name; // 원본 이미지 경로
const thumbsPath = 'src/public/products-images/' + id + '/gallery/thumbs/' + req.files.file.name; // 섬네일 이미지 경로
try {
// 원본 이미지를 gallery 폴더에 넣어주기
await productImage.mv(path);
// 이미지를 리사이즈.
const buf = await ResizeImg(fs.readFileSync(path), { width: 100, height: 100 });
fs.writeFileSync(thumbsPath, buf);
res.sendStatus(200);
} catch (err) {
console.error(err);
next(err);
}
})
<%- include('../partials/header') %>
<div class="d-flex justify-content-between align-items-center">
<h2> 상품 수정하기</h2>
<a href="/admin/products" class="btn btn-primary">뒤로</a>
</div>
<p>갤러리(세부 이미지들)</p>
<div class="gallery row">
<% galleryImages.forEach(image => { %>
<% if (image !== 'thumbs') { %>
<div class="col">
<form action="/admin/products/<%= id %>/image/<%= image %>?_method=DELETE" method="POST">
<button type="submit">
<img src="/products-images/<%= id %>/gallery/thumbs/<%= image %>" alt="">
</button>
</form>
</div>
<% } %>
<% }) %>
</div>
<br>
<!-- 이미지를 드래그 해서 올릴 수 있다. -->
<form enctype="multipart/form-data" method="post" action="/admin/products/product-gallery/<%= id %>" class="dropzone" id="dropzoneForm">
<div class="fallback">
<input type="file" name="file" multiple>
<input type="submit" value="upload">
</div>
</form>
<script src="https://unpkg.com/dropzone@5/dist/min/dropzone.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/dropzone@5/dist/min/dropzone.min.css" type="text/css" />
<!-- 이미지를 올리면 바로 올라간다. -->
<script>
Dropzone.options.dropzoneForm = {
acceptedFiles: "image/*", // 이미지만 올릴 수 있게.
init: function() { // 1초 뒤에 새로고침된다.
this.on("queuecomplete", file => {
setTimeout(() => {
location.reload();
}, 1000);
});
}
}
</script>
<%- include('../partials/footer') %>
// 상품 이미지 삭제
router.delete('/:id/image/:imageId', checkAdmin, async (req, res, next) => {
const originalImage = 'src/public/products-images/' + req.params.id + '/gallery/' + req.params.imageId;
const thumbImage = 'src/public/products-images/' + req.params.id + '/gallery/thumbs/' + req.params.imageId;
try {
console.log(originalImage)
await fs.remove(originalImage); // 이미지 삭제
await fs.remove(thumbImage);
req.flash('success', '이미지 삭제')
res.redirect('/admin/products/' + req.params.id + '/edit')
} catch (err) {
console.error(err);
next(err);
}
})
// 장바구니에 상품 추가
router.post('/:product', async (req, res, next) => {
const productSlug = req.params.product;
const product = await Product.findOne({ slug: productSlug});
try {
// 세션에 카트를 담아두고 장바구니에 새로운 상품이 담길 때
if (req.session.cart.length === 0) {
req.session.cart = [];
req.session.cart.push({
title: productSlug,
qty: 1,
price: product.price,
image: '/products-images/' + product._id + '/' + product.image,
});
// 세션의 카트에 같은 상품이 있을때
} else {
let cart = req.session.cart;
let newItem = true;
// 이미 카트에 있는 상품이면 한 개 추가하고 loop break
for (let i = 0; i < cart.length; i++){
if (cart[i].title === productSlug) {
cart[i].qty += 1;
newItem = false;
break;
}
}
// 새로운 상품이 담길 때
if (newItem) {
cart = [];
cart.push({
title: productSlug,
qty: 1,
price: product.price,
image: '/products-images/' + product._id + '/' + product.image,
});
}
}
req.flash('success', '장바구니에 상품이 담겼습니다.');
res.redirect('back');
} catch (err) {
console.error(err);
next(err);
}
})
// ejs
<td>
<!-- 일반적으로 a tag는 get 요청이다. -->
<a class="btn btn-primary" href="/cart/update/<%= product.title %>?action=add">+</a>
<a class="btn btn-danger" href="/cart/update/<%= product.title %>?action=remove">-</a>
<a class="btn btn-dark" href="/cart/update/<%= product.title %>?action=clear">clear</a>
</td>
// router
router.get('/update/:product', async (req, res, next) => {
try {
const slug = req.params.product;
let cart = req.session.cart;
const operator = req.query.action;
for (let i = 0; i < cart.length; i++){
if (cart[i].title === slug) {
if (operator === 'add') {
cart[i].qty += 1;
req.flash('success', '상품 개수가 추가되었습니다.');
} else if (operator === 'remove') {
cart[i].qty -= 1;
if (cart[i].qty <= 0) {
cart.splice(i, 1);
};
req.flash('success', '상품 개수가 제거되었습니다.');
} else if (operator === 'clear') {
cart.splice(i, 1);
req.flash('success', '상품이 제거되었습니다.');
}
}
break;
}
res.redirect('/cart/checkout'); // 변경된 카트를 다시 표시하기 위해 리디렉션합니다.
} catch (err) {
console.error(err);
next(err);
}
})
router.delete('/', async (req, res, next) => {
let cart = req.session.cart;
try {
if (cart.length > 0) {
delete req.session.cart;
// req.session.cart = [];
req.flash('success','장바구니가 비어졌습니다.')
} else {
req.flash('error','장바구니가 비어있습니다.')
}
res.redirect('/cart/checkout')
} catch (err) {
console.error(err);
next(err);
}
})
HEADER.ejs
<script src="https://cdn.iamport.kr/v1/iamport.js"></script>
결제할 창 ejs
<script>
IMP.init("가맹점 ID"); // 예: imp00000000a
function requestPay() {
IMP.request_pay({
pg: "PG PROVIDER.PGID", // 관리자 콘솔 결제 연동에 있음
pay_method: "card",
merchant_uid: "ORD2018012321-00003011", // 주문번호
name: "노르웨이 회전 의자",
amount: 100, // 숫자 타입
buyer_email: "gildong@gmail.com",
buyer_name: "홍길동",
buyer_tel: "010-4242-4242",
buyer_addr: "서울특별시 강남구 신사동",
buyer_postcode: "01181"
}, function(rsp) { // callback
if (rsp.success) {
// 결제 성공 시 로직
fetch('/cart/complete-order')
.then(res => {
location.reload();
})
.catch(err => {
console.error(err);
})
} else {
// 결제 실패 시 로직
console.log('실패', rsp);
}
});
}
</script>