경매 사이트인데 새로고침해야 남의 입찰이 보이면 그게 경매냐. Socket.IO를 붙였다.
db.Good.hasMany(db.Auction);
db.User.hasMany(db.Auction);
상품 하나에 입찰이 여러 개, 유저 하나가 입찰을 여러 번. 둘 다 일대다라서 Auction 테이블에 GoodId, UserId가 자동으로 생긴다.
exports.bid = async (req, res, next) => {
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) return res.status(404).send("해당 상품은 존재하지 않습니다.");
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]?.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");
};
체크리스트가 네 개다. 상품이 존재하는지, 시작가보다 높은지, 24시간이 안 지났는지, 직전 입찰가보다 높은지. 순서가 중요한 게 아니라 — 어차피 하나라도 걸리면 그 자리에서 리턴이니까 — 그냥 하나씩 다 확인하는 게 맞다.
재밌는 부분은 good.Auctions[0]?.bid다. include: { model: Auction }으로 가져온 입찰 목록을 bid DESC로 정렬해뒀기 때문에 배열의 0번째가 항상 최고가다. 별도로 MAX() 쿼리를 안 날려도 정렬만 잘 해두면 배열 첫 번째가 곧 최댓값이 된다.
io.on("connection", (socket) => {
const req = socket.request;
const roomId = new URL(req.headers.referer).pathname.split("/").at(-1);
socket.join(roomId);
socket.on("disconnect", () => socket.leave(roomId));
});
소켓 연결 자체는 전역이지만, referer 헤더에서 URL 경로를 파싱해서 상품 ID를 뽑아내고 그 ID를 방 이름으로 쓴다. 그러니까 /good/3 페이지를 보고 있는 사람들끼리만 같은 방에 묶인다. 입찰이 들어오면 io.to(req.params.id).emit("bid", ...)로 그 방에만 쏘니까, 다른 상품 페이지 보고 있는 사람한테는 안 간다.
referer로 방 ID를 알아내는 게 좀 트릭 같긴 한데, 클라이언트에서 명시적으로 join 이벤트를 보내는 방식보다 코드가 짧아서 이렇게 갔다.
입찰까지 실시간으로 도니까 이제 남은 건 낙찰 처리 — 24시간 지난 상품은 누가 가져가는지 정하는 부분이다.