프로그래밍 언어/NODE JS

프로젝트 마무리하기

· 코딩마이데이

마지막으로 GIF 이미지를 전송하는 것을 구현합니다. 프런트 화면에서 이미지를 선택해 업로드하는 이벤트 리스너를 추가합니다.

 

views/chat.html

{% extends 'layout.html' %} {% block content %}
<h1>{{title}}</h1>
<a href="/" id="exit-btn">방 나가기</a>
<fieldset>
  <legend>채팅 내용</legend>
  <div id="chat-list">
    {% for chat in chats %} {% if chat.user === user %}
    <div class="mine" style="color: {{chat.user}}">
      <div>{{chat.user}}</div>
      {% if chat.gif %}
      <img src="/gif/{{chat.gif}}" />
      {% else %}
      <div>{{chat.chat}}</div>
      {% endif %}
    </div>
    {% elif chat.user === 'system' %}
    <div class="system">
      <div>{{chat.chat}}</div>
    </div>
    {% else %}
    <div class="other" style="colo: {{chat.user}}">
      <div>{{chat.user}}</div>
      {% if chat.gif %}
      <img src="/gif/{{chat.gif}}" />
      {% else %}
      <div>{{chat.chat}}</div>
      {% endif %}
    </div>
    {% endif %} {% endfor %}
  </div>
</fieldset>
<form action="/chat" id="chat-form" method="post" enctype="multipart/form-data">
  <label for="gif">GIF 올리기</label>
  <input type="file" id="gif" name="gif" accept="image/gif" />
  <input type="text" id="chat" name="chat" />
  <button type="submit">전송</button>
</form>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
  const socket = io.connect("http://localhost:8005/chat", {
    path: "/socket.io",
  });
  socket.on("join", function (data) {
    const div = document.createElement("div");
    div.classList.add("system");
    const chat = document.createElement("div");
    div.textContent = data.chat;
    div.appendChild(chat);
    document.querySelector("#chat-list").appendChild(div);
  });
  socket.on("exit", function (data) {
    const div = document.createElement("div");
    div.classList.add("system");
    const chat = document.createElement("div");
    div.textContent = data.chat;
    div.appendChild(chat);
    document.querySelector("#chat-list").appendChild(div);
  });
  socket.on("chat", function (data) {
    const div = document.createElement("div");
    if (data.user === "{{user}}") {
      div.classList.add("mine");
    } else {
      div.classList.add("other");
    }
    const name = document.createElement("div");
    name.textContent = data.user;
    div.appendChild(name);
    if (data.chat) {
      const chat = document.createElement("div");
      chat.textContent = data.chat;
      div.appendChild(chat);
    } else {
      const gif = document.createElement("img");
      gif.src = "/gif/" + data.gif;
      div.appendChild(gif);
    }
    div.style.color = data.user;
    document.querySelector("#chat-list").appendChild(div);
  });

  document.querySelector("#chat-form").addEventListener("submit", function (e) {
    e.preventDefault();
    if (e.target.chat.value) {
      axios
        .post("/room/{{room._id}}/chat", {
          chat: this.chat.value,
        })
        .then(() => {
          e.target.chat.value = "";
        })
        .catch((err) => {
          console.error(err);
        });
    }
  });

  document.querySelector("#gif").addEventListener("change", function (e) {
    console.log(e.target.files);
    const formData = new FormData();
    formData.append("gif", e.target.files[0]);
    axios.post(
      "/room/{{room._id}}/gif",
      formData
        .then(() => {
          e.target.file = null;
        })
        .catch((err) => {
          console.error(err);
        }),
    );
  });
</script>
{% endblock %}

 

GET /room/{{room._id}}/gif 주소에 상응하는 라우터를 작성합니다.

 

routes/index.js

const express = require("express");

const Room = require("../schemas/room");
const Chat = require("../schemas/chat");

const router = express.Router();

router.get("/", async (req, res, next) => {
  try {
    const rooms = await Room.find({});
    res.render("main", { rooms, title: "GIF 채팅방" });
  } catch (error) {
    console.error(error);
    next(error);
  }
});

router.get("/room", (req, res) => {
  res.render("room", { title: "GIF 채팅방 생성" });
});

router.post("/room", async (req, res, next) => {
  // ➊
  try {
    const newRoom = await Room.create({
      title: req.body.title,
      max: req.body.max,
      owner: req.session.color,
      password: req.body.password,
    });
    const io = req.app.get("io");
    io.of("/room").emit("newRoom", newRoom);
    res.redirect(`/room/${newRoom._id}?password=${req.body.password}`);
  } catch (error) {
    console.error(error);
    next(error);
  }
});

router.get("/room/:id", async (req, res, next) => {
  // ➋
  try {
    const room = await Room.findOne({ _id: req.params.id });
    const io = req.app.get("io");
    if (!room) {
      return res.redirect("/?error=존재하지 않는 방입니다.");
    }
    if (room.password && room.password !== req.query.password) {
      return res.redirect("/?error=비밀번호가 틀렸습니다.");
    }
    const { rooms } = io.of("/chat").adapter;
    if (
      rooms &&
      rooms[req.params.id] &&
      room.max <= rooms[req.params.id].length
    ) {
      return res.redirect("/?error=인원이 가득 찬 방입니다.");
    }
    const chats = await Chat.find({ room: room._id }).sort("createdAt");
    return res.render("chat", {
      room,
      title: room.title,
      chats,
      user: req.session.color,
    });
  } catch (error) {
    console.error(error);
    next(error);
  }
});

router.delete("/room/:id", async (req, res, next) => {
  // ➌
  try {
    await Room.remove({ _id: req.params.id });
    await Chat.remove({ room: req.params.id });
    res.send("ok");
    setTimeout(() => {
      req.app;
    }, 2000);
  } catch (error) {
    console.error(error);
    next(error);
  }
});

router.post("/room/:id/chat", async (req, res, next) => {
  try {
    const chat = await Chat.create({
      room: req.params.id,
      user: req.session.color,
      chat: req.body.chat,
    });
    req.app.get("io").of("/chat").to(req.params.id).emit("chat", chat);
    res.send("ok");
  } catch (error) {
    console.error(error);
    next(error);
  }
});

try {
  fs.readFileSync("uploads");
} catch (err) {
  console.error("uploads 폴더가 있어 uploads 폴더를 생성합니다.");
  fs.mkdirSync("uploads");
}
const upload = multer({
  storage: multer.diskStorage({
    destination(req, file, done) {
      done(null, "uploads/");
    },
    filename(req, file, done) {
      const ext = path.extname(file.originalname);
      done(null, path.basename(file.originalname, ext) + Date.now() + ext);
    },
  }),
  limits: { fileSize: 5 * 1024 * 1024 },
});
router.post("/room/:id/gif", upload.single("gif"), async (req, res, next) => {
  try {
    const chat = await Chat.create({
      room: req.params.id,
      user: req.session.color,
      gif: req.file.filename,
    });
    req.app.get("io").of("/chat").to(req.params.id).emit("chat", chat);
    res.send("ok");
  } catch (error) {
    console.error(error);
    next(error);
  }
});

module.exports = router;

 

9장의 이미지 업로드의 방식이 같습니다. uploads 폴더에 사진을 저장하고, 파일명에 타임스템프(Date.now())를 붙이고, 5MB로 용량을 제한했습니다. 파일이 업로드된 후에는 내용을 데이터베이스에 저장하고, 방 안에 있는 모든 소켓에 채팅 데이터를 보냅니다.

이때 이미지를 제공할 uploads 폴더를 express.static 미들웨어로 연결해봅시다.

 

app.js

const express = require("express");
const path = require("path");
const cookieParser = require("cookie-parser");
const session = require("express-session");
const nunjucks = require("nunjucks");
const dotenv = require("dotenv");
const colorHash = require("color-hash");

dotenv.config();
const webSocket = require("./socket");
const indexRouter = require("./routes");
const morgan = require("morgan");
const connect = require("./schemas");

const app = express();
app.set("port", process.env.PORT || 8005);
app.set("view engine", "html");

nunjucks.configure("views", {
  express: app,
  watch: true,
});

connect();

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("/gif", 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(
  session({
    resave: false,
    saveUninitialized: false,
    secret: process.env.COOKIE_SECRET,
    cookie: {
      httpOnly: true,
      secure: false,
    },
  }),
);

app.use((req, res, next) => {
  if (!req.session.color) {
    const colorHash = new colorHash();
    req.session.color = colorHash.hex(req.sessionID);
  }
  next();
});

app.use("/", indexRouter);

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");
});

const server = app.listen(app.get("port"), () => {
  console.log(app.get("port"), "번 포트에서 대기 중");
});

webSocket(server, app, sessionMiddleware);

 

이제 GIF 파일을 올릴 수 있습니다.

GIF 파일 업로드 화면