티스토리 뷰
728x90
SMALL
서버센트 이벤트 사용하기
npm i sse socket.io
sse.js
const SSE = require('sse');
module.exports = (server) => {
const sse = new SSE(server);
sse.on('connection', (client) => {
// 서버센트 이벤트 연결
setInterval(() => {
client.send(Date.now().toString());
}, 1000);
});
};
app.js
const sse = require('./sse');
const webSocket = require('./socket');
webSocket(server, app);
sse(server);
main.html
<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.start); // 경매 시작 시간
const server = new Date(parseInt(e.data, 10));
end.setDate(end.getDate() + 1); // 경매 종료 시간
if (server >= end) {
// 경매가 종료되었으면
return (td.textContent = '00:00:00');
} else {
const t = end - server; // 경매 종료까지 남은 시간
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 = hours + ':' + minutes + ':' + seconds);
}
});
};
</script>
네트워크 탭
auction.html
{% extends 'layout.html' %} {% block good %}
<h2>{{good.name}}</h2>
<div>등록자: {{good.Owner.nick}}</div>
<div>시작가: {{good.price}}원</div>
<strong id="time" data-start="{{good.createdAt}}"></strong>
<img id="good-img" src="/img/{{good.img}}" />
{% endblock %} {% block content %}
<div class="timeline">
<div id="bid">
{% for bid in auction %}
<div>
<span>{{bid.User.nick}}님: </span>
<strong>{{bid.bid}}원에 입찰하셨습니다.</strong>
{% if bid.msg %}
<span>({{bid.msg}})</span>
{% endif %}
</div>
{% endfor %}
</div>
<form id="bid-form">
<input
type="number"
name="bid"
placeholder="입찰가"
required
min="{{good.price}}"
/>
<input
type="msg"
name="msg"
placeholder="메시지(선택사항)"
maxlength="100"
/>
<button class="btn" type="submit">입찰</button>
</form>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/event-source-polyfill/src/eventsource.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script>
document.querySelector('#bid-form').addEventListener('submit', (e) => {
e.preventDefault();
const errorMessage = document.querySelector('.error-message');
axios
.post('/good/{{good.id}}/bid', {
// 입찰 진행
bid: e.target.bid.value,
msg: e.target.msg.value,
})
.catch((err) => {
console.error(err);
alert(err.response.data);
})
.finally(() => {
e.target.bid.value = '';
e.target.msg.value = '';
errorMessage.textContent = '';
});
});
const es = new EventSource('/sse');
const time = document.querySelector('#time');
es.onmessage = (e) => {
const end = new Date(time.dataset.start); // 경매 시작 시간
const server = new Date(parseInt(e.data, 10));
end.setDate(end.getDate() + 1); // 경매 종료 시간
if (server >= end) {
// 경매가 종료되었으면
return (time.textContent = '00:00:00');
} else {
const t = end - server;
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 (time.textContent = hours + ':' + minutes + ':' + seconds);
}
};
const socket = io.connect('http://localhost:8010', {
path: '/socket.io',
});
socket.on('bid', (data) => {
// 누군가가 입찰했을 때
const div = document.createElement('div');
let span = document.createElement('span');
span.textContent = data.nick + '님: ';
const strong = document.createElement('strong');
strong.textContent = data.bid + '원에 입찰하셨습니다.';
div.appendChild(span);
div.appendChild(strong);
if (data.msg) {
span = document.createElement('span');
span.textContent = `(${data.msg})`;
div.appendChild(span);
}
document.querySelector('#bid').appendChild(div);
});
</script>
<script>
window.onload = () => {
if (new URL(location.href).searchParams.get('auctionError')) {
alert(new URL(location.href).searchParams.get('auctionError'));
}
};
</script>
{% endblock %}
routes/index.js
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { Good, Auction, User } = require('../models');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const router = express.Router();
router.use((req, res, next) => {
res.locals.user = req.user;
next();
});
router.get('/', async (req, res, next) => {
try {
const goods = await Good.findAll({ where: { SoldId: null } });
res.render('main', {
title: 'NodeAuction',
goods,
});
} catch (error) {
console.error(error);
next(error);
}
});
router.get('/join', isNotLoggedIn, (req, res) => {
res.render('join', {
title: '회원가입 - NodeAuction',
});
});
router.get('/good', isLoggedIn, (req, res) => {
res.render('good', { title: '상품 등록 - NodeAuction' });
});
try {
fs.readdirSync('uploads');
} catch (error) {
console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.');
fs.mkdirSync('uploads');
}
const upload = multer({
storage: multer.diskStorage({
destination(req, file, cb) {
cb(null, 'uploads/');
},
filename(req, file, cb) {
const ext = path.extname(file.originalname);
cb(
null,
path.basename(file.originalname, ext) + new Date().valueOf() + ext
);
},
}),
limits: { fileSize: 5 * 1024 * 1024 },
});
router.post(
'/good',
isLoggedIn,
upload.single('img'),
async (req, res, next) => {
try {
const { name, price } = req.body;
await Good.create({
OwnerId: req.user.id,
name,
img: req.file.filename,
price,
});
res.redirect('/');
} catch (error) {
console.error(error);
next(error);
}
}
);
router.get('/good/:id', isLoggedIn, async (req, res, next) => {
try {
const [good, auction] = await Promise.all([
Good.findOne({
where: { id: req.params.id },
include: {
model: User,
as: 'Owner',
},
}),
Auction.findOne({
where: { GoodId: req.params.id },
include: { model: User },
order: [['bid', 'ASC']],
}),
]);
res.render('auction', {
title: `${good.name} - NodeAuction`,
good,
auction,
});
} catch (error) {
console.error(error);
next(error);
}
});
router.post('/good/:id/bid', isLoggedIn, async (req, res, next) => {
try {
const { bid, msg } = req.body;
const good = await Good.findOne({
where: { id: req.params.id },
include: { model: Auction },
order: [[{ model: Auction }, 'bid', 'DESC']],
});
if (good.price >= bid) {
return res.status(403).send('시작 가격보다 높게 입찰해야 합니다.');
}
if (new Date(good.createdAt).valueOf() + 24 * 60 * 60 * 1000 < new Date()) {
return res.status(403).send('경매가 이미 종료되었습니다.');
}
if (good.Auctions[0] && good.Auctions[0].bid >= bid) {
return res.status(403).send('이전 입찰가보다 높아야 합니다.');
}
const result = await Auction.create({
bid,
msg,
UserId: req.user.id,
GoodId: req.params.id,
});
// 실시간으로 입찰 내역 전송
req.app.get('io').to(req.params.id).emit('bid', {
bid: result.bid,
msg: result.msg,
nick: req.user.nick,
});
return res.send('ok');
} catch (error) {
console.error(error);
return next(error);
}
});
module.exports = router;
스케줄링 구현하기
카운트다운이 끝나면 더 이상 경매를 진행할 수는 없지만, 아직 낙찰자가 정해지지 않았다. 경매 종료를 24시간 후로 정했으므로 경매가 생성되고 24시간이 지난 후에 낙찰자를 정하는 시스템을 구현해야 한다.
npm i node-schedule
routes/index.js
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const schedule = require('node-schedule');
const { Good, Auction, User } = require('../models');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
...
router.post(
'/good',
isLoggedIn,
upload.single('img'),
async (req, res, next) => {
try {
const { name, price } = req.body;
const good = await Good.create({
OwnerId: req.user.id,
name,
img: req.file.filename,
price,
});
const end = new Date();
end.setDate(end.getDate() + 1); // 하루 뒤
schedule.scheduleJob(end, async () => {
const success = await Auction.findOne({
where: { GoodId: good.id },
order: [['bid', 'DESC']],
});
await Good.update(
{ SoldId: success.UserId },
{ where: { id: good.id } }
);
await User.update(
{
money: sequelize.literal(`money - ${success.bid}`),
},
{
where: { id: success.UserId },
}
);
});
res.redirect('/');
} catch (error) {
console.error(error);
next(error);
}
}
);
module.exports = router;
schedule 객체의 scheduleJob 메서드로 일정을 예약할 수 있다. 첫번째 인수로 실행될 시각, 두번째 인수로 해당 시각이 되었을 때 수행할 콜백 함수를 넣는다. 경매 모델에서 가장 높은 가격으로 입찰한 사람을 찾아 상품 모델의 낙찰자 아이디에 넣어주도록 정의했다. 또한, 낙찰자의 보유 자산을 낙찰 금액만큼 뺀다. { 컬럼: sequelize.literal(컬럼 - 숫자) }가 시퀄라이즈에서 해당 컬럼의 숫자를 줄이는 방법이다.
서버가 꺼지거나 오류가 발생했을 때 대비
checkAuction.js
const { Op } = require('Sequelize');
const { Good, Auction, User, sequelize } = require('./models');
module.exports = async () => {
try {
const yesterday = new Date();
yesterday.getDate.setDate(yesterDay.getDate() - 1);
const targets = await Good.findAll({
where: {
SoldId: null,
createdAt: { [Op.lte]: yesterday },
},
});
targets.forEach(async (target) => {
const success = await Auction.findOne({
where: { GoodId: target.id },
order: [['bid', 'DESC']],
});
await Good.update(
{ SoldId: success.UserId },
{ where: { id: target.id } }
);
await User.update(
{
money: sequelize.literal(`money-${success.bid}`),
},
{
where: { id: success.UserId },
}
);
});
} catch (error) {
console.error(error);
}
};
스스로 해보기
상품 등록자는 참여할 수 업섹 만들기(라우터에서 검사)
경매 시간을 자유롭게 조정할 수 있도록 만드기(상품 등록 시 생성할 수 있게 화면과 DB 수정)
노드 서버가 꺼졌다 다시 켜졌을 때 스케줄러 다시 생성하기(checkAuction에서 DB 조회 후 스케줄러 설정)
아무도 입찰하지 않ㄴ아 낙찰자가 없을 때를 대비한 처리 로직 구현하기(checkAuction과 스케줄러 수정)
728x90
LIST
'책 > nodejs 교과서' 카테고리의 다른 글
15. AWS와 GCP로 배포하기 (0) | 2021.03.14 |
---|---|
14. CLI 프로그램 만들기 (0) | 2021.03.08 |
12. 웹 소켓으로 실시간 데이터 전송하기 (0) | 2021.03.04 |
11. 노드 서비스 테스트하기 (0) | 2021.03.02 |
10. API 서버 이해하기 (0) | 2021.02.27 |
댓글
공지사항