인증은 끝났으니 이제 진짜 경매 사이트답게, 상품부터 등록할 수 있어야 한다.
Good.init({
name: { type: Sequelize.STRING(40) },
img: { type: Sequelize.STRING(200) },
price: { type: Sequelize.INTEGER, defaultValue: 0 },
});
associate에서 재밌는 부분이 하나 있다.
db.Good.belongsTo(db.User, { as: "Owner" });
db.Good.belongsTo(db.User, { as: "Sold" });
한 상품이 User랑 두 번 연결된다. 등록한 사람(Owner), 낙찰받은 사람(Sold). 관계가 두 개니까 as로 이름을 안 나눠주면 Sequelize가 어느 쪽인지 구분을 못 한다. 나중에 조회할 때 include: { model: User, as: "Owner" } 이런 식으로 꼭 as를 붙여줘야 한다는 걸 몸으로 배웠다.
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) + Date.now() + ext);
},
}),
limits: { fileSize: 5 * 1024 * 1024 },
});
router.post("/good", isLoggedIn, upload.single("img"), createGood);
파일명 뒤에 Date.now()를 붙이는 건 국룰이다. 같은 이름 파일 두 번 올리면 덮어써버리니까. uploads 폴더가 없으면 서버 켤 때 자동으로 만들도록 fs.readdirSync를 try-catch로 감싸뒀다. 로컬에서 처음 클론받은 사람이 폴더 없어서 에러 보는 일은 없어야 하니까.
exports.createGood = async (req, res, next) => {
const { name, price } = req.body;
await Good.create({
name,
img: req.file.filename,
price,
OwnerId: req.user.id,
});
res.redirect("/");
};
req.file은 multer가 채워주고, req.user는 passport가 채워준다. 미들웨어 두 개가 각자 할 일 하고 컨트롤러는 그냥 조립만 하면 된다.
exports.renderMain = async (req, res, next) => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const goods = await Good.findAll({
where: { SoldId: null, createdAt: { [Op.gte]: yesterday } },
});
res.render("main", { title: "NodeAuction", goods });
};
경매 기간을 24시간으로 잡았다. SoldId: null이면 아직 낙찰 안 된 거고, createdAt이 어제 이후면 아직 유효기간 안이라는 뜻. 두 조건을 AND로 걸어야 "지금 입찰 가능한 상품"만 뽑힌다.
여기서 Op.gte(greater than or equal) 하나로 "어제부터 지금까지"를 표현하는데, 처음엔 종료 시점도 같이 걸어야 하나 헷갈렸다. 근데 생각해보면 미래 시점 상품이 있을 리가 없으니 시작 경계만 걸면 충분하다. 종료된 상품을 걸러내는 건 SoldId가 대신 해준다 — 24시간 지나면 스케줄러가 낙찰 처리를 하면서 SoldId를 채워넣을 거니까. (이 스케줄러는 뒤에서 다룬다.)
다음은 이 프로젝트의 핵심, 실제로 입찰이 실시간으로 오가는 부분이다.