Node.js 교과서 2022년 개정판으로 코드를 살짝 변경했습니다.
1.0 들어가기 전
1.1 변경 사항 (Node.js 교과서 2022 개정판 참고 by.zeroCho)
1. Sequelize Model 정의 부분
기존 : define으로 정의
변경 : Sequelize.Model 상속받아 구현
2. 로직 변경
기존 : router안에 controller 구현 : routes(controller 포함) + services
변경 : router와 controller 분리 : routes + controllers + services
자세한 변경사항을 코드로 확인해 보실 분은 포스팅 맨 아래 깃허브 주소를 기재해 놓았습니다.
1.2 스스로 해보기 문제 총 4문제 풀이
1. 상품 등록자는 참여할 수 없도록 만들기
2. 경매 마감 시간을 자유롭게 조정할 수 있도록 만들기
3. 노드 서버가 다시 켜졌을 때 스케줄러 재 생성
4. 아무도 입찰하지 않아 낙찰자가 없을 때 처리 로직 구현
어렵지 않은 코드이니 천천히 고쳐보겠습니다.
2.0 상품 등록자는 참여할 수 없도록 만들기
상품 등록자를 참여할 수 없게 하려면 입찰을 넣는 사람과 해당 상품의 주인의 id값만 비교해 같으면 입찰을 넣지 못하도록 제한해 두면 됩니다.
NodeAuction은 사용자 인증을 passport로 인증하기에 req.user.id 에서 입찰 넣는 사람의 id를 알아올 수 있습니다. 그리고 상품의 주인 id는 mysql good table에 보시면 ownerId라는 칼럼이 있을 겁니다. 해당 값이 상품의 주인 id입니다.
*./models/good.js에 ownerId 가 없는데 어떻게 생성이 된 걸 가요?
./models/index.js 에 보시면 아래와 같은 코드가 있습니다.
const Sequelize = require('sequelize')
module.exports = class Good extends Sequelize.Model {
... 생략 ...
static associate(db) {
db.Good.belongsTo(db.User, { as: 'Owner' });
db.Good.belongsTo(db.User, { as: 'Sold' });
db.Good.hasMany(db.Auction);
}
}
good 테이블에서 user를 가리키는 외래키 ownerId와 soldId를 만들어 주었기 때문에 칼럼명이 생성이 됩니다.
소스코드
경로 : ./routes/index.js +./services/auction.js
// ./routes/index.js
router.post('/good/:id/bid', isLoggedIn, async (req, res, next) => {
... 생략 ...
const bidDTO = {
userId : req.user.id,
goodId : req.params.id,
bid : req.body.bid,
msg : req.body.msg
}
AuctionService.bid(bidDTO, (err, success, info) => {
if(err) return next(err)
if(!success)
return res.status(403).send(info.message)
req.app.get('io').to(req.params.id).emit('bid', {
bid: info.bid,
msg: info.msg,
nick: req.user.nick,
});
return res.status(200).send("ok")
})
... 생략 ...
})
// ./services/auction.js
module.exports = {
... 생략 ...
bid : async function(bidDTO, done) {
try{
const good = await Good.findOne({
where : { id : bidDTO.goodId },
include : { model : Auction },
order : [[ {model : Auction }, 'bid', 'DESC' ]]
})
// 상품 등록자와 입찰자 id가 같다면
if(good.OwnerId === bidDTO.userId)
return done(null, false, { message : "상품 등록자는 입찰이 불가합니다." })
... 생략 ...
}catch(err){
done(err, false)
}
}
... 생략 ...
}
3.0 경매 마감 시간 자유롭게 조정하기
경매 마감 시간을 자유롭게 조정하기 위해서는 사용자로부터 마감 시간을 입력받고 그 마감 시간을 데이터베이스에 저장해 두어야 합니다. 그리고 그 마감시간만큼 스케줄러에 등록해 자동으로 마감하도록 구현해야 합니다.
3.1 데이터 베이스 수정 (Good Model)
경로 :./models/good.js
const Sequelize = require('sequelize');
module.exports = class Good extends Sequelize.Model {
static init(sequelize) {
return super.init({
name: {
type: Sequelize.STRING(40),
allowNull: false,
},
img: {
type: Sequelize.STRING(200),
allowNull: true,
},
price: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
},
// 경매 마감 시간
endTime : {
type : Sequelize.DATE,
allowNull : false,
}
},{
sequelize,
timestamps: true,
paranoid: true,
modelName: 'Good',
tableName: 'goods',
charset: 'utf8',
collate: 'utf8_general_ci',
})
}
... 생략 ...
}
3.2 HTML 수정
총 두 가지를 수정해주셔야 합니다.
1. 상품을 등록할 때 input 추가
2. sse로 불러오는 시간 계산에 일수도 추가
시간이 3 자릿수가 넘어가면 사용자 입장에서 불편하기 때문에 일 수로 표시해 줍니다.
경로 :../views/good.html
{% block content %}
<div class="timeline">
<form action="/good" id="good-form" method="post" enctype="multipart/form-data">
<div class="input-group">
<label for="good-name">상품명</label>
<input type="text" id="good-name" name="name" required autofocus>
</div>
<div class="input-group">
<label for="good-photo">상품 사진</label>
<input type="file" id="good-photo" name="img" required>
</div>
<div class="input-group">
<label for="good-price">시작 가격</label>
<input type="number" id="good-price" name="price" required>
</div>
<div class="input-group">
<label for="good-endTime">경매 마감 시간</label>
<input type="datetime-local" name="endTime" required>
</div>
<button id="join-btn" class="btn" type="submit">상품 등록</button>
</form>
</div>
{% endblock %}
경로 :./views/index.html
{% extends 'layout.html' %}
{% block content %}
<div class="timeline">
<h2>경매 진행 목록</h2>
<table id="good-list">
<tr>
<th>상품명</th>
<th>이미지</th>
<th>시작 가격</th>
<th>종료 시간</th>
<th>입장</th>
</tr>
{% for good in goods %}
<tr>
<td>{{good.name}}</td>
<td>
<img src="/images/{{good.img}}">
</td>
<td>{{good.price}}</td>
<-- data-endtime 변경 -->
<td class="time" data-endtime="{{good.endTime}}">00:00:00</td>
<td>
<a href="/good/{{good.id}}" class="enter btn">입장</a>
</td>
</tr>
{% endfor %}
</table>
</div>
<script src="https://unpkg.com/event-source-polyfill/src/eventsource.min.js"></script>
<script>
const es = new EventSource('/sse');
es.onmessage = function (e) {
document.querySelectorAll('.time').forEach((td) => {
const end = new Date(td.dataset.endtime); // 경매 시작 시간
const server = new Date(parseInt(e.data, 10));
if (server >= end) { // 경매가 종료되었으면
return td.textContent = '00:00:00';
} else {
const t = end - server;
// 일수 계산
const days = Math.floor(t / (1000 * 60 * 60 * 24));
const seconds = ('0' + Math.floor((t / 1000) % 60)).slice(-2);
const minutes = ('0' + Math.floor((t / 1000 / 60) % 60)).slice(-2);
const hours = ('0' + Math.floor((t / (1000 * 60 * 60)) % 24)).slice(-2);
return td.textContent = days + '일 ' + hours + ':' + minutes + ':' + seconds;
}
});
};
</script>
{% endblock %}
추가로 views/auction.html 에도 시간을 불러오는 부분이 있으니 위와 같이 고쳐주면 됩니다.
4.0 재시작했을 때 스케줄러 재등록 + 아무도 입찰하지 않았을 경우 마감 처리
4.1 낙찰 처리 서비스 만들기
낙찰 처리 서비스에 마감처리도 추가로 넣어줍니다.
경로 :./services/auction.js
const { User, Good, Auction, sequelize } = require('../models')
module.exports = {
... 생략 ...
successfulBid : asycn function(goodId) {
try{
// 입찰한 사람중 금액이 가장 높은 사람 조회
const success = await Auction.findOne({
where : { goodId },
order : [[ 'bid', 'DESC' ]]
})
// 입찰한 사람이 없다면 해당 경매 종료(삭제)
if(!success) {
await Good.destroy({
where : { id : goodId }
})
return
}
await Promise.all([
Good.update(
{ SoldId : success.UserId },
{ where : { id : goodId }
),
User.upadte(
{ money : sequelize.literal(`money - ${success.bid}`) },
{ where : { id : success.UserId } }
)
])
}catch(err){
throw err;
}
}
}
4.2 서버 시작할 때 Auction을 check 하는 함수 수정
경로 :./modules/checkAuction.module.js
const { Good } = require('../models')
const { successfulBid, auctionSchedule } = require('../services/auction')
/**
*
* 서버 시잔 전 (재시작 포함)
*
* - 경매 종료 시간이 지난 상품 낙찰 처리
* - 재시작 된 경우 기존 실행되었던 스케줄러가 종료되었으므로 재실행
*/
module.exports = async function checkAuction() {
try{
const now = new Date();
// 입찰자가 결정되지 않은 목록 불러옴
const targets = await Good.findAll({
where : {
SoldId : null,
}
})
targets.forEach(async (target) => {
// 불러온 목록 중 현재보다 지난 시간이면 낙찰처리
// 그게 아니라면 스케줄러 재등록
if(target.endTime.valueOf() <= now)
await successfulBid(target.id)
else
auctionSchedule(target.endTime, target.id)
})
}catch(err){
console.error(err)
throw err;
}
}
경로 :./services/auction.js
const { User, Good, Auction, sequelize } = require('../models')
const schedule = require('node-schedule')
module.exports = {
... 생략 ...
/**
*
* 입력받은 경매 마감 시간이 지나면 경매 마감 합니다.
*
* - 경매 물품 판매 처리
* - 구매자 등록 처리
* - 구매자 결산 처리
*
* @param {Date} endTime 경매 마감 시간
* @param {String} id good id
*/
auctionSchedule : function(endTime, id) {
try{
schedule.scheduleJob(endTime, async () => {
await this.sucessfulBid(id)
})
}catch(err){
throw err;
}
}
}
5.0 마치면서 (앞으로 개선 사항, github 주소)
스스로 해보기까지 모두 풀어보았습니다. 다음 포스팅부터는 추가로 NodeAuction을 아래와 같은 내용들을 추가해보고자 합니다.
1. 경매가 진행 중인 상품들 스케줄러 등록(낙찰처리)
현재 구현된 NodeAuction의 문제점은 경매 애플리케이션에 사용자가 많아지면 스케줄러에 등록되는 데이터가 많아질 겁니다.
2. Mysql Connection
만약 서버 가동 중 mysql 서버가 종료된다면 서버도 같이 종료될 것입니다.
3. 캐시 서버 부재
사용자가 많아진다는 가정하에 캐시서버의 도입은 필수입니다.
4. 로드밸런싱 부재
사용자가 많아진다는 가정하에 하나의 트래픽을 몰릴 것을 대비해야 합니다.
5. 출력값 검증 부재 (보안)
현재 입력값 검증에 대한 검증은 만들어두었지만 입력값 검증에서 무조건 필터가 가능한 것은 아니기 때문에 출력값 검증이 필요합니다.
6. 에러 노출
사용자가 잘못된 값을 입력하면 서버의 에러가 html에 그대로 노출이 됩니다. 이는 보안에 취약합니다.
GitHub - dotredbee/NodeAuction_update: Node.js교과서 ch12 코드에서 추가로 업데이트한 서버입니다.
Node.js교과서 ch12 코드에서 추가로 업데이트한 서버입니다. Contribute to dotredbee/NodeAuction_update development by creating an account on GitHub.
github.com
공감과 댓글(좋은 지적 포함) 감사합니다.
'Backend > Node.js' 카테고리의 다른 글
[Node.js 교과서] ch12. NodeAuction 개선하기 4 : NginX, Redis (0) | 2023.05.02 |
---|---|
[Node.js 교과서] ch12. NodeAuction 개선하기 2 : 서비스 분할 (0) | 2023.04.05 |
[Node.js 교과서] ch12. NodeAuction 개선하기 1 : 입력값 검증, CSRF 방어 (0) | 2023.04.03 |
[Node.js] web token(jwt) 인증 + Redis(In memory) + 최소한의 보안 (0) | 2023.04.01 |
[Node.js] 캐시, LRU 캐시 (0) | 2023.03.05 |