프로그래밍 언어/NODE JS

실시간 경매 시스템 만들기 - 프로젝트 구조 갖추기(3)

· 코딩마이데이

경매 시스템은 회원가입, 로그인, 경매 상품 등록, 방 참여, 경매 진행으로 이루어져 있습니다. 회원가입, 로그인, 경매 상품 등록 페이지와 라우터를 만들어보겠습니다.

 

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&nbsp;<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>

 

그리고 메인 화면을 담당하는 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 %}

 

회원가입 화면을 담당하는 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 %}

 

상품을 업로드하는 페이지인 good.html 파일을 작성합니다. form에서 이미지 업로드(#good-photo)도 해야 하므로 form 태그의 enctype을 multipart/form-data로 두어 폼 데이터를 사용하도록 설정했습니다.

{% 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 %}

 

public/main.css

* {
  box-sizing: border-box;
}
html,
body {
  margin: 0;
  padding: 0;
  height: 100%;
}
.btn {
  display: inline-block;
  padding: 0 5px;
  text-decoration: none;
  cursor: pointer;
  border-radius: 4px;
  background: white;
  border: 1px solid silver;
  color: crimson;
  height: 37px;
  line-height: 37px;
  vertical-align: top;
  font-size: 12px;
}
input,
textarea {
  border-radius: 4px;
  height: 37px;
  padding: 10px;
  border: 1px solid silver;
}
.container {
  width: 100%;
  height: 100%;
}
@media screen and (min-width: 800px) {
  .container {
    width: 800px;
    margin: 0 auto;
  }
}
.input-group {
  margin-bottom: 15px;
}
.input-group label {
  width: 25%;
  display: inline-block;
}
.input-group input {
  width: 70%;
}
#join {
  float: right;
}
.profile-wrap {
  width: 100%;
  display: inline-block;
  vertical-align: top;
  margin: 10px 0;
}
@media screen and (min-width: 800px) {
  .profile-wrap {
    width: 290px;
    margin-bottom: 0;
  }
}
.profile {
  text-align: left;
  padding: 10px;
  margin-right: 10px;
  border-radius: 4px;
  border: 1px solid silver;
  background: yellow;
}
.user-name,
.user-money {
  font-weight: bold;
  font-size: 18px;
  margin-bottom: 10px;
}
.timeline {
  margin-top: 10px;
  width: 100%;
  display: inline-block;
  border-radius: 4px;
  vertical-align: top;
}
@media screen and (min-width: 800px) {
  .timeline {
    width: 500px;
  }
}
#good-list,
#good-list th,
#good-list td {
  border: 1px solid black;
  border-collapse: collapse;
}
#good-list img {
  max-height: 100px;
  vertical-align: top;
}
#good-img {
  width: 280px;
  display: block;
}
.error-message {
  color: red;
  font-weight: bold;
}
#join-form,
#good-form {
  padding: 10px;
  text-align: center;
}
footer {
  text-align: center;
}

 

마지막으로 라우터를 만듭니다.

 

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,
      });
      res.redirect("/");
    } catch (error) {
      console.error(error);
      next(error);
    }
  },
);

module.exports = router;

 

router.use에서 res.locals.user = req.user;로 모든 png 템플릿에 사용자 정보를 변수로 집어 넣었습니다. 이렇게 하면 res.render 메서드에 user: req.user를 하지 않아도 되므로 중복을 제거할 수 있습니다.

라우터는 GET /, GET /join, GET /good, POST /good으로 이루어져 있습니다. GET /는 메인 화면을 렌더링합니다. 렌더링할 때 경매가 진행 중인 상품 목록도 같이 불러옵니다. SoldId가 낙찰자의 아이디어이므로 낙찰자가 null이면 경매가 진행 중인 것입니다.

GET /join과 GET /good은 각각 회원가입 화면과 상품 등록 화면을 렌더링합니다. POST /good 라우터는 업로드한 상품을 처리합니다. 상품 이미지 업로드 기능이 있어 multer 미들웨어가 붙었습니다.

이제 npm start 명령어로 서버를 실행한 후 http://localhost:8010에 접속하면 됩니다. 회원가입 후 로그인한 후 상품을 등록해봅시다.

localhost:8010 접속 후 상품 등록 후 화면