// node-auction/package.json
{
"name": "node-auction",
"version": "0.0.1",
"description": "노드 경매 시스템",
"main": "app.js",
"scripts": {
"start": "nodemon app"
},
"author": "Zero Cho",
"license": "ISC",
"dependencies": {
"bcrypt": "^3.0.7",
"cookie-parser": "^1.4.4",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-session": "^1.17.0",
"morgan": "^1.9.1",
"multer": "^1.4.2",
"mysql2": "^2.1.0",
"nunjucks": "^3.2.0",
"passport": "^0.4.1",
"passport-local": "^1.0.0",
"sequelize": "^5.21.3",
"sequelize-cli": "^5.5.1"
},
"devDependencies": {
"nodemon": "^2.0.2"
}
}
패키지 설치
$ npm i
$ npm i sequelize sequelize-cli mysql2
$ npx sequelize init
// models/user.js
const Sequelize = require('sequelize');
module.exports = class User extends Sequelize.Model {
static init(sequelize) {
return super.init({
email: {
type: Sequelize.STRING(40),
allowNull: false,
unique: true,
},
nick: {
type: Sequelize.STRING(15),
allowNull: false,
},
password: {
type: Sequelize.STRING(100),
allowNull: true,
},
money: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
},
}, {
sequelize,
timestamps: true,
paranoid: true,
modelName: 'User',
tableName: 'users',
charset: 'utf8',
collate: 'utf8_general_ci',
});
}
static associate(db) {
db.User.hasMany(db.Auction);
}
};
// 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,
},
}, {
sequelize,
timestamps: true,
paranoid: true,
modelName: 'Good',
tableName: 'goods',
charset: 'utf8',
collate: 'utf8_general_ci',
});
}
static associate(db) {
db.Good.belongsTo(db.User, { as: 'Owner' });
db.Good.belongsTo(db.User, { as: 'Sold' });
db.Good.hasMany(db.Auction);
}
};
// models/auction.js
const Sequelize = require('sequelize');
module.exports = class Auction extends Sequelize.Model {
static init(sequelize) {
return super.init({
bid: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
},
msg: {
type: Sequelize.STRING(100),
allowNull: true,
},
}, {
sequelize,
timestamps: true,
paranoid: true,
modelName: 'Auction',
tableName: 'auctions',
charset: 'utf8',
collate: 'utf8_general_ci',
});
}
static associate(db) {
db.Auction.belongsTo(db.User);
db.Auction.belongsTo(db.Good);
}
};
// config/config.json
{
"development": {
"username": "root",
"password": "nodejsbook", // 각자의 mySQL 비밀번호
"database": "nodeauction",
"host": "127.0.0.1",
"dialect": "mysql"
},
"test": {
"username": "root",
"password": null,
"database": "database_test",
"host": "127.0.0.1",
"dialect": "mysql"
},
"production": {
"username": "root",
"password": null,
"database": "database_production",
"host": "127.0.0.1",
"dialect": "mysql"
}
}
$ npx sequelize db:create
// models/index.js
const Sequelize = require('sequelize');
const User = require('./user');
const Good = require('./good');
const Auction = require('./auction');
const env = process.env.NODE_ENV || 'development';
const config = require('../config/config')[env];
const db = {};
const sequelize = new Sequelize(
config.database, config.username, config.password, config,
);
db.sequelize = sequelize;
db.User = User;
db.Good = Good;
db.Auction = Auction;
User.init(sequelize);
Good.init(sequelize);
Auction.init(sequelize);
User.associate(db);
Good.associate(db);
Auction.associate(db);
module.exports = db;
$ npm i passport passport-local bcrypt
// passport/localStrategy.js
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
const User = require('../models/user');
module.exports = () => {
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'password',
}, async (email, password, done) => {
try {
const exUser = await User.findOne({ where: { email } });
if (exUser) {
const result = await bcrypt.compare(password, exUser.password);
if (result) {
done(null, exUser);
} else {
done(null, false, { message: '비밀번호가 일치하지 않습니다.' });
}
} else {
done(null, false, { message: '가입되지 않은 회원입니다.' });
}
} catch (error) {
console.error(error);
done(error);
}
}));
};
// passport/index.js
const passport = require('passport');
const local = require('./localStrategy');
const User = require('../models/user');
module.exports = () => {
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
User.findOne({ where: { id } })
.then(user => done(null, user))
.catch(err => done(err));
});
local();
};
// routes/auth.js
const express = require('express');
const passport = require('passport');
const bcrypt = require('bcrypt');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const User = require('../models/user');
const router = express.Router();
// 회원가입 라우터
router.post('/join', isNotLoggedIn, async (req, res, next) => {
const { email, nick, password, money } = req.body;
try {
const exUser = await User.findOne({ where: { email } });
if (exUser) {
return res.redirect('/join?joinError=이미 가입된 이메일입니다.');
}
const hash = await bcrypt.hash(password, 12);
await User.create({
email,
nick,
password: hash,
money,
});
return res.redirect('/');
} catch (error) {
console.error(error);
return next(error);
}
});
// 로그인 라우터
router.post('/login', isNotLoggedIn, (req, res, next) => {
passport.authenticate('local', (authError, user, info) => {
if (authError) {
console.error(authError);
return next(authError);
}
if (!user) {
return res.redirect(`/?loginError=${info.message}`);
}
return req.login(user, (loginError) => {
if (loginError) {
console.error(loginError);
return next(loginError);
}
return res.redirect('/');
});
})(req, res, next);
});
// 로그아웃 라우터
router.get('/logout', isLoggedIn, (req, res) => {
req.logout();
req.session.destroy();
res.redirect('/');
});
module.exports = router;
// routes/middlewares.js
exports.isLoggedIn = (req, res, next) => {
if (req.isAuthenticated()) {
next();
} else {
res.redirect('/?loginError=로그인이 필요합니다.');
}
};
exports.isNotLoggedIn = (req, res, next) => {
if (!req.isAuthenticated()) {
next();
} else {
res.redirect('/');
}
};
//.env
COOKIE_SECRET=auction
//app.js
const express = require('express');
const path = require('path');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const passport = require('passport');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
dotenv.config();
const indexRouter = require('./routes/index');
const authRouter = require('./routes/auth');
const { sequelize } = require('./models');
const passportConfig = require('./passport');
const app = express();
passportConfig();
app.set('port', process.env.PORT || 8010);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
sequelize.sync({ force: false })
.then(() => {
console.log('데이터베이스 연결 성공');
})
.catch((err) => {
console.error(err);
});
const sessionMiddleware = session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
});
app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/img', express.static(path.join(__dirname, 'uploads')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(sessionMiddleware);
app.use(passport.initialize());
app.use(passport.session());
app.use('/', indexRouter);
app.use('/auth', authRouter);
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
error.status = 404;
next(error);
});
app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
res.status(err.status || 500);
res.render('error');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기중');
});
<!-- views/error.html-->
{% extends 'layout.html' %}
{% block content %}
<h1>{{message}}</h1>
<h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre>
{% endblock %}
<!-- views/layout.html-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" href="/main.css">
</head>
<body>
<div class="container">
<div class="profile-wrap">
<div class="profile">
{% if user and user.id %}
<div class="user-name">안녕하세요 {{user.nick}}님</div>
<div class="user-money">보유 자산: {{user.money}}원</div>
<input type="hidden" id="my-id" value="user.id">
<a href="/auth/logout" id="logout" class="btn">로그아웃</a>
<a href="/good" id="register" class="btn">상품 등록</a>
{% else %}
<form action="/auth/login" id="login-form" method="post">
<div class="input-group">
<label for="email">이메일</label>
<input type="email" id="email" name="email" required autofocus>
</div>
<div class="input-group">
<label for="password">비밀번호</label>
<input type="password" id="password" name="password" required>
</div>
<a href="/join" id="join" class="btn">회원가입</a>
<button id="login" class="btn" type="submit">로그인</button>
</form>
{% endif %}
</div>
<footer>
Made by <a href="https://www.zerocho.com" target="_blank">ZeroCho</a>
</footer>
{% block good %}
{% endblock %}
</div>
{% block content %}
{% endblock %}
</div>
<script>
window.onload = () => {
if (new URL(location.href).searchParams.get('loginError')) {
alert(new URL(location.href).searchParams.get('loginError'));
}
};
</script>
</body>
</html>
<!-- views/main.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="/img/{{good.img}}">
</td>
<td>{{good.price}}</td>
<td class="time" data-start="{{good.createdAt}}">00:00:00</td>
<td>
<a href="/good/{{good.id}}" class="enter btn">입장</a>
</td>
</tr>
{% endfor %}
</table>
</div>
{% endblock %}
<!-- views/join.html-->
{% extends 'layout.html' %}
{% block content %}
<div class="timeline">
<form action="/auth/join" id="join-form" method="post">
<div class="input-group">
<label for="join-email">이메일</label>
<input type="email" id="join-email" name="email">
</div>
<div class="input-group">
<label for="join-nick">닉네임</label>
<input type="text" id="join-nick" name="nick">
</div>
<div class="input-group">
<label for="join-password">비밀번호</label>
<input type="password" id="join-password" name="password">
</div>
<div class="input-group">
<label for="join-money">보유자산</label>
<input type="number" id="join-money" name="money">
</div>
<button id="join-btn" class="btn" type="submit">회원가입</button>
</form>
</div>
<script>
window.onload = () => {
if (new URL(location.href).searchParams.get('joinError')) {
alert(new URL(location.href).searchParams.get('joinError'));
}
};
</script>
{% endblock %}
<!-- views/good.html-->
{% extends 'layout.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>
<button id="join-btn" class="btn" type="submit">상품 등록</button>
</form>
</div>
{% endblock %}
css 파일은 저자의 github에서 찾을 수 있습니다.
https://github.com/ZeroCho/nodejs-book
// 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; // 모든 pug 템플릿에 사용자 정보를 집어넣음(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);
}
});
module.exports = router;
$ npm start
$ npm i sse socket.io@2
// app.js
...
const passportConfig = require('./passport');
const sse = require('./sse');
const webSocket = require('./socket');
const app = express();
...
const server = app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기중');
});
webSocket(server, app);
sse(server);
// 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);
});
};
// socket.js
const SocketIO = require('socket.io');
module.exports = (server, app) => {
const io = SocketIO(server, { path: '/socket.io' });
app.set('io', io);
io.on('connection', (socket) => { // 웹 소켓 연결 시
const req = socket.request;
const { headers: { referer } } = req;
const roomId = referer.split('/')[referer.split('/').length - 1];
socket.join(roomId);
socket.on('disconnect', () => {
socket.leave(roomId);
});
});
};
<!-- views/main.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="/img/{{good.img}}">
</td>
<td>{{good.price}}</td>
<td class="time" data-start="{{good.createdAt}}">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.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>
{% endblock %}
<!-- view/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
...
// 해당 상품과 기존 입찰정보들을 불러온 뒤 렌더링
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.findAll({
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시간 후 낙찰자를 정하는 시스템 구현: node-schedule 패키지 사용
$ 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');
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;
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 () => { // scheduleJob: 일정 예약
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);
}
});
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.findAll({
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;
// checkAuction.js
const { Op } = require('Sequelize');
const { Good, Auction, User, sequelize } = require('./models');
module.exports = async () => {
console.log('checkAuction');
try {
const yesterday = new Date();
yesterday.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);
}
};
// app.js
...
dotenv.config();
const indexRouter = require('./routes/index');
const authRouter = require('./routes/auth');
const { sequelize } = require('./models');
const passportConfig = require('./passport');
const sse = require('./sse');
const webSocket = require('./socket');
const checkAuction = require('./checkAuction');
const app = express();
passportConfig();
checkAuction();
app.set('port', process.env.PORT || 8010);
...
// routes/index.js
...
router.get('/list', isLoggedIn, async (req, res, next) => {
try {
const goods = await Good.findAll({
where: { SoldId: req.user.id },
include: { model: Auction },
order: [[{ model: Auction }, 'bid', 'DESC']],
});
res.render('list', { title: '낙찰 목록 - NodeAuction', goods });
} catch (error) {
console.error(error);
next(error);
}
});
module.exports = router;
<!-- views/list.html-->
{% extends 'layout.html' %}
{% block content %}
<div class="timeline">
<h2>경매 낙찰 목록</h2>
<table id="good-list">
<tr>
<th>상품명</th>
<th>사진</th>
<th>낙찰가</th>
</tr>
{% for good in goods %}
<tr>
<td>{{good.name}}</td>
<td>
<img src="/img/{{good.img}}">
</td>
<td>{{good.Auctions[0].bid}}</td>
</tr>
{% endfor %}
</table>
</div>
{% endblock %}
<!-- views/layout.html-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" href="/main.css">
</head>
<body>
<div class="container">
<div class="profile-wrap">
<div class="profile">
{% if user and user.id %}
<div class="user-name">안녕하세요 {{user.nick}}님</div>
<div class="user-money">보유 자산: {{user.money}}원</div>
<input type="hidden" id="my-id" value="user.id">
<a href="/auth/logout" id="logout" class="btn">로그아웃</a>
<a href="/good" id="register" class="btn">상품 등록</a>
<a href="/list" id="list" class="btn">낙찰 내역</a>
{% else %}
<form action="/auth/login" id="login-form" method="post">
<div class="input-group">
<label for="email">이메일</label>
<input type="email" id="email" name="email" required autofocus>
</div>
<div class="input-group">
<label for="password">비밀번호</label>
<input type="password" id="password" name="password" required>
</div>
<a href="/join" id="join" class="btn">회원가입</a>
<button id="login" class="btn" type="submit">로그인</button>
</form>
{% endif %}
</div>
<footer>
Made by <a href="https://www.zerocho.com" target="_blank">ZeroCho</a>
</footer>
{% block good %}
{% endblock %}
</div>
{% block content %}
{% endblock %}
</div>
<script>
window.onload = () => {
if (new URL(location.href).searchParams.get('loginError')) {
alert(new URL(location.href).searchParams.get('loginError'));
}
};
</script>
</body>
</html>
[Node.js] 15장 AWS와 GCP로 배포하기 (0) | 2022.01.24 |
---|---|
[Node.js] 14장 CLI 프로그램 만들기 (0) | 2022.01.24 |
[Node.js] 12장(2) 미들웨어와 소켓 연결하기 (0) | 2022.01.10 |
[Node.js] 12장 웹소켓으로 실시간 데이터 전송하기(1) (0) | 2022.01.06 |
[Node.js] 11장 노드 서비스 테스트하기 (0) | 2022.01.06 |