본문 바로가기

Node.js

[Node.js] 계정관리 서버 프로젝트 1

개발 일정

일자 to do
23.11.14(화) API 만들기
23.11.15(수) - 공통 응답 모듈 만들기
- 파일 업/다운로드 로그인한 사용자 검사 추가
- 세션관리 모듈 만들기(클라이언트가 요청하는 모든 API(로그인 제외)헤더에 토큰 있는지 검사하는 모듈 만들기)
23.11.16(목) - 로그관리: 유저가 어떤 API를 언제 호출했는지 /  
- 코드 보완: 예외처리, 중복코드 수정
23.11.17(금) - 이메일 중복검사 같은 세세한 부분 체크
- 구글 클라우드 플랫폼 VM 서버 올리기(포트 새로 열기)
- 서버올리고 포스트맨 테스트

 

API

기능 세부기능 method url
계정 추가 항목: 이메일, 비밀번호, 이름,  전화번호  post /api/users
계정 삭제 계정 삭제 delete /api/users
비밀번호 변경 비밀번호 변경 put /api/users
계정인증 이메일, 비밀번호를 입력 / 세션 토큰 리턴 post /api/account
토큰인증 이메일, 토큰으로 유효성 확인 및 만료시간 갱신 post /api/token
로그인 계정 인증, 클라이언트에 필요한 정보 리턴 post /api/users/login
파일 업로드 파일 업로드 / 로그인한 사용자만 post /api/files
파일 다운로드 파일 다운로드 / 로그인한 사용자만 get /api/files

 

 

익스프레스로 노드 프로젝트 생성

 

필요한 모듈 설치하기

 

 

realm

데이터를 저장하기 위해 realm 사용

 

multer

파일업다운로드를 위해 필요한 모듈

 

uuid

토큰을 랜덤값으로 주기 위해서 uuid 사용

 

코드 작성

 

1) 데이터베이스 realm 을 사용하여 UserSchema 스키마 정의

const Realm = require("realm");

// User 스키마(모델) 정의
let UserSchema = {
  name: "User",
  properties: {
    email: "string",
    password: "string",
    name: "string",
    tel: "string",
    token: "string?", // 속성을 선택사항으로 변경하려면 '?'를 붙임
    date: "date",
    tokenExp: "date",
  },
  primaryKey: "email",
};

// Realm 객체 생성
let UserRealm = new Realm({
  path: "user.realm",
  schema: [UserSchema],
  //   schemaVersion: 1,
});

// Realm 객체 내보내기
module.exports = UserRealm;

 

2) user CRUD 만들기

Create

var express = require("express");
var router = express.Router();
const uuid = require("uuid"); // uuid 모듈 추가

var UserRealm = require("../db/realm");

/* POST 계정 추가. */
// todo: 이메일 중복체크
router.post("/", function (req, res, next) {
  console.log(req.body);
  console.log(req.body.email);

  var email = req.body.email;
  var password = req.body.password;
  var name = req.body.name;
  var tel = req.body.tel;

  console.log(`계정 데이터: ${email}`);

  try {
    UserRealm.write(() => {
      UserRealm.create("User", {
        email: email,
        password: password,
        name: name,
        tel: tel,
        toeken: "default",
        date: new Date(),
        tokenExp: new Date(),
      });
    });

    res.status(201).send({
      success: true,
      message: "계정 추가 성공",
    });
  } catch (error) {
    console.log(error);
    res.status(500).send({
      success: false,
      message: "계정 추가 실패",
    });
  }
});

 

Read

/* GET 계정 전체 조회 */
router.get("/", function (req, res, next) {
  const users = UserRealm.objects("User").sorted("date", true);

  res.send({
    success: true,
    data: users,
  });
});

 

 

Update

/* PUT 비밀번호 변경. */
router.put("/:email", function (req, res, next) {
  const user = findUserByEmail(req.params.email);
  const password = findPassword(user.password, req.body.inputPassword);

  try {
    if (user && password) {
      UserRealm.write(() => {
        user.password = req.body.newPassword;
      });

      res.status(200).send({
        success: true,
        message: "비밀번호 변경 성공",
      });
    }
  } catch (error) {
    console.log(error);
    res.status(500).send({
      success: false,
      message: "비밀번호 변경 실패",
    });
  }
});

//------------------------------ 공통 메서드------------------------------//

// 이메일 기반 계정 조회
function findUserByEmail(email) {
  return UserRealm.objects("User").filtered(`email = "${email}"`)[0];
}

// 이메일 기반 비밀번호 조회
function findPassword(password, inputPassword) {
  return password === inputPassword;
}

- 기존 비밀번호랑 같을 새로운 비밀번호 입력하라는 메시지 줄지 고민해보기

 

Delete

/* DELETE 계정 삭제 */
router.delete("/:email", function (req, res, next) {
  const user = findUserByEmail(req.params.email);

  try {
    if (user) {
      UserRealm.write(() => {
        UserRealm.delete(user);
      });

      res.send({
        success: true,
        message: "계정 삭제 성공",
      });
    }
  } catch (error) {
    console.log(error);
    res.status(500).send({
      success: false,
      message: "계정 삭제 실패",
    });
  }
});

 

로그인

/* POST 로그인 */
router.post("/login", function (req, res, next) {
  const user = findUserByEmail(req.body.email);
  console.log(JSON.stringify(user));
  const password = findPassword(user.password, req.body.inputPassword);
  console.log(`user확인: ${user}`);
  console.log(`password확인: ${password}`);

  try {
    if (user && password) {
      UserRealm.write(() => {
        const sessionToken = uuid.v4(); // uuid를 사용해서 세션 토큰 생성
        user.token = sessionToken; // 세션 토큰 저장
      });

      res.status(200).send({
        success: true,
        message: "로그인 성공",
        data: user,
      });
    }
  } catch (error) {
    console.log(error);
    res.status(500).send({
      success: false,
      message: "로그인 실패",
    });
  }
});

 

토큰 인증api

var express = require("express");
var router = express.Router();

var UserRealm = require("../db/realm");

/* GET 토큰 인증 */
router.get("/", function (req, res, next) {
  // 헤더에서 이메일과 토큰 추출
  const email = req.headers.email;
  const token = req.headers.token;

  if (!email || !token) {
    return res.status(401).send({ message: "이메일과 토큰을 넣어주세요." });
  }

  try {
    const user = UserRealm.objects("User").filtered(
      `email = "${email}" AND token = "${token}"`
    );

    if (user.length === 1) {
      // 사용자가 존재하고 토큰이 일치하면 인증 성공
      // 토큰 만료시간 갱신
      UserRealm.write(() => {
        user[0].tokenExp = new Date();
      });

      return res.status(200).send({
        success: true,
        message: "토큰 인증 성공",
      });
    } else {
      // 사용자가 없거나 토큰이 일치하지 않으면 인증 실패
      return res.status(401).send({
        // 401: Unauthorized (인증 실패)
        success: false,
        message: "토큰 인증 실패",
      });
    }
  } catch (error) {
    console.log(error);
    res.status(403).send({
      // 403: Forbidden (권한 없음)
      success: false,
      message: "토큰 인증 실패",
    });
  }
});

module.exports = router;

 

파일 업로드

const express = require("express");
const router = express.Router();
const multer = require("multer"); // multer 모듈 추가
const fs = require("fs"); // fs 모듈 추가
const path = require("path"); // path 모듈 추가

// 파일을 업로드할 uploads 폴더 생성
fs.readdir("uploads", (error) => {
  if (error) {
    console.log("uploads 폴더를 생성합니다.");
    fs.mkdirSync("uploads");
  }
});
// 파일 업로드 설정
const upload = multer({
  storage: multer.diskStorage({
    // 파일 저장 경로 설정
    destination(req, file, cb) {
      // cb 콜백 함수를 통해 전송된 파일 저장 디렉토리 설정, 'uploads/' 디렉토리로 지정
      cb(null, "uploads/");
    },
    // 파일 저장명 설정
    filename(req, file, cb) {
      const ext = path.extname(file.originalname);
      // cb(null, path.basename(file.originalname, ext) + new Date().valueOf() + ext);
      // cb 콜백 함수를 통해 전송된 파일 이름 설정
      // cb(null, path.basename(file.originalname, ext));
      cb(null, file.originalname);
      console.log(file.originalname);
    },
  }),
  // 파일 최대 용량
  limits: { fileSize: 5 * 1024 * 1024 },
});

// api 요청 섹션 시작

/* GET 파일 업로드 */

// 단일 파일 업로드 multer.single(fileName)
router.post("/upload", upload.single("file"), function (req, res, next) {
  console.log("단일 파일 업로드 요청");
  console.log(req.file);
  const imagePath = req.file.path;
  if (imagePath === undefined) {
    // return res.status(400).send(response.fail(400, "파일이 없습니다."));
    return res
      .status(400)
      .send({ success: false, message: "파일이 없습니다." });
  }
  res
    .status(200)
    // .send(response.success(200, "파일 업로드 성공", `저장경로: /${imagePath}`));
    .send({
      success: true,
      message: "파일 업로드 성공",
      path: `/${imagePath}`,
    });
});

// n개 파일 업로드 multer.array(, 개수제한)
/* POST users listing. */
router.post("/", upload.array("files", 10), function (req, res, next) {
  console.log(req.files);
  // res.json(`~~post 요청 응답~~`);
  res.status(200).send(
    // response.success(200, "파일 업로드 성공", `파일개수: ${req.files.length}`)
    { success: true, message: "파일 업로드 성공", count: req.files.length }
  );
});

/* GET 파일 다운로드 */
router.get("/upload", function (req, res, next) {
  res.send("GET 파일 다운로드");
});

module.exports = router;

 

파일 다운로드

var express = require("express");
const multer = require("multer");
const path = require("path");
const fs = require("fs");
const createHttpError = require("http-errors");
// const response = require("../util/response");

var router = express.Router(); // router 객체는 express.Router()로 만듦

var storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, "uploads/");
  },
  filename: function (req, file, cb) {
    cb(null, file.originalname);
  },
});

/* GET users listing. */
router.get("/download/:fileName", function (req, res, next) {
  const fileName = req.params.fileName;
  //   const filePath = path.join(__dirname, "uploads", fileName);
  const filePath = `uploads/${fileName}`;

  console.log(`fileName: ${fileName}`);
  console.log(`filePath: ${filePath}`);
  const ch = fs.existsSync(filePath);
  console.log(`파일 존재여부: ${ch}`);

  // 다운로드 요청한 파일이 존재하는지 확인
  if (fs.existsSync(filePath)) {
    // 파일의 MIME 유형 설정(이미지, pdf, txt 등)
    res.setHeader("Content-Type", getMimeType(fileName));

    // 파일의 Content-Disposition 헤더를 "inline"으로 설정하여 파일을 브라우저에서 바로 보기
    res.setHeader("Content-Disposition", "inline; filename=" + fileName);

    // res.status(200).send(response.success(200), "파일 다운로드 성공");

    // 파일 스트림을 응답에 연결
    const fileStream = fs.createReadStream(filePath);
    fileStream.pipe(res);
  } else {
    // res.status(404).send(response.fail(404, "파일이 존재하지 않습니다."));
    res
      .status(404)
      .send({ success: false, message: "파일이 존재하지 않습니다." });
  }
});

function getMimeType(fileName) {
  const extname = path.extname(fileName);
  switch (extname.toLowerCase()) {
    case ".png":
      return "image/png";
    case ".jpg":
    case ".jpeg":
      return "image/jpeg";
    case ".pdf":
      return "application/pdf";
    case ".txt":
      return "text/plain";
    default:
      return "application/octet-stream";
  }
}

module.exports = router; // 라운터를 모듈로 만듦

 

 

파일 업다운로드는 지난번에 만든 업다운로드 모듈 코드를 사용함

업/다운로드 실습때 공통응답 형식 모듈을 만들었었는데 다른 코드에도 적용해서 수정할 예정

 

 

 

트러블 슈팅

AssertionError: Cannot modify managed objects outside of a write transaction 에러는 Realm에서 관리되는 객체를 트랜잭션 외부에서 수정하려고 할 때 발생합니다. Realm에서는 데이터 변경이 트랜잭션 내에서 이루어져야 하며, write 메서드 내에서 수행되어야 합니다.