내일배움캠프 스파르타 코딩클럽
일단 이번 팀과제 내용 분량이 상당하기 때문에 상세 내용은 나중에 회고해 보도록 하겠다.
< prisma. schema.prisma >
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
previewFeatures = ["omitApi"]
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model User {
userId Int @id @default(autoincrement()) @map("user_id")
email String @unique @map("email")
password String @map("password")
nickName String @unique @map("nick_name")
role UserRole @default(COMMON) @map("role")
refreshToken String? @map("refresh_token")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
profile Profile?
feed Feed[]
comment Comment[]
@@map("users")
}
model Profile {
profileId Int @id @default(autoincrement()) @map("profile_id")
userId Int @unique @map("user_id")
showLog Boolean @default(true) @map("show_log")
nickName String @map("nick_name")
introduce String? @map("introduce")
profile_img_url String? @map("profile_img_url")
maxweight Int? @map("maxweight")
weight Float? @map("weight")
height Float? @map("height")
muscleweight Float? @map("muscleweight")
fat Float? @map("fat")
metabolic Int? @map("metabolic")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [userId])
log Log[]
@@map("profile")
}
model Feed {
feedId Int @id @default(autoincrement()) @map("feed_id")
userId Int @map("user_id")
title String @map("title")
feed_img_url String? @map("feed_img_url")
nickName String @map("nick_name")
content String @map("content") @db.Text
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [userId])
comment Comment[]
@@map("feeds")
}
model Comment {
commentId Int @id @default(autoincrement()) @map("comment_id")
feedId Int @map("feed_id")
userId Int @map("user_id")
comment String @map("comment") @db.Text
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [userId])
feed Feed @relation(fields: [feedId], references: [feedId])
@@map("comments")
}
model Log {
logId Int @id @default(autoincrement()) @map("log_id")
profileId Int @map("profile_id")
changeField String @map("change_field")
oldValue String? @map("old_value")
newValue String @map("new_value")
updatedAt DateTime @updatedAt @map("updated_at")
profile Profile @relation(fields: [profileId], references: [profileId])
@@map("logs")
}
enum UserRole {
COMMON
ADMIN
}
< src. app.js >
import express from 'express';
import { SERVER_PORT } from './constants/env.constant.js';
import { errorHandler } from './middlewares/error-handler.middleware.js';
import { apiRouter } from './routers/index.js';
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get('/health-check', (req, res) => {
return res.status(200).send(`I'm healthy.`);
});
app.use('/api', apiRouter);
app.use(errorHandler);
app.listen(SERVER_PORT, () => {
console.log(SERVER_PORT, '포트로 서버가 열렸어요!');
});
< src.utils. prisma.util.js >
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error'],
errorFormat: 'pretty',
});
try {
await prisma.$connect();
console.log('DB 연결에 성공했습니다.');
} catch (error) {
console.error('DB 연결에 실패했습니다.', error);
}
< src.constants. auth.constant.js >
export const HASH_SALT_ROUNDS = 10;
export const ACCESS_TOKEN_EXPIRES = '1h';
export const REFRESH_TOKEN_EXPIRES = '7d';
< src.constants. comment.constant.js >
export const MIN_COMMENT_LENGTH = 1;
< src.constants. env.constant.js >
import 'dotenv/config';
export const SERVER_PORT = process.env.SERVER_PORT;
export const ACCESS_SECRET_KEY = process.env.ACCESS_SECRET_KEY;
export const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;
export const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET;
< src.constants. http-status.constant.js >
export const HTTP_STATUS = {
OK: 200, // 호출 성공
CREATED: 201, // 생성 성공
BAD_REQUEST: 400, // 사용자의 잘못으로 인한 실패
UNAUTHORIZED: 401, // 인증 실패 (입력 정보가 틀렸을 때)
FORBIDDEN: 403, // 인가 실패 (접근 권한이 없을 때)
NOT_FOUND: 404, // 해당 데이터가 존재하지 않음
CONFLICT: 409, // 충돌 발생
INTERNAL_SERVER_ERROR: 500, // 예상치 못한 에러
};
< src.constants. message.constant.js >
import { MIN_COMMENT_LENGTH } from './comment.constant.js';
export const MESSAGES = {
AUTH: {
COMMON: {
EMAIL: {
REQUIRED: '이메일을 입력해 주세요.',
INVALID_FORMAT: '이메일 형태가 올바르지 않습니다.',
DUPLICATED: '이미 가입된 사용자입니다.',
},
PASSWORD: {
REQUIRED: '비밀번호를 입력해 주세요.',
LENGTH: '비밀번호를 6자리 이상, 12자리 이하로 설정해주세요.',
NO_STRING: '비밀번호는 문자열로 입력해야합니다.',
},
REPEAT_PASSWORD: {
REQUIRED: '비밀번호 확인을 입력해 주세요.',
NOT_MATCHED: '비밀번호가 일치하지 않습니다.',
},
NICKNAME: {
REQUIRED: '별명을 입력해주세요.',
NO_STRING: '별명은 문자열로 입력해야합니다.',
},
INTRODUCE: {
NO_STRING: '자기소개는 문자열로 입력해야합니다.',
},
MAX_WEIGHT: {
NO_NUMBER: '3대 중량을 정수로 입력해주세요.',
},
WEIGHT: {
NO_NUMBER: '본인의 몸무게를 숫자로 입력해주세요.',
PRECISION: '몸무게는 소수점 첫번째 자리까지만 입력해주세요.',
},
HEIGHT: {
NO_NUMBER: '본인의 키를 숫자로 입력해주세요.',
PRECISION: '키는 소수점 첫번째 자리까지만 입력해주세요.',
},
FAT: {
NO_NUMBER: '본인의 체지방률을 숫자로 입력해주세요.',
PRECISION: '체지방률은 소수점 첫번째 자리까지만 입력해주세요.',
},
METABOLIC: {
NO_NUMBER: '기초대사량을 정수로 입력해주세요.',
},
MUSCLEWEIGHT: {
NO_NUMBER: '골격근량을 숫자로 입력해주세요.',
PRECISION: '골격근량은 소수점 첫번째 자리까지만 입력해주세요.',
},
PROFILE_IMG_URL: {
NO_STRING: '프로필 URL을 문자열로 입력해주세요.',
},
SHOWLOG: {
NO_BOOLEAN: '프로필 공개여부를 true, false로 작성해주세요.',
},
},
JWT: {
NO_TOKEN: '인증 정보가 없습니다.',
NOT_SUPPORTED_TYPE: '지원하지 않는 인증 방식입니다.',
EXPIRED: '인증 정보가 만료되었습니다.',
NO_USER: '인증 정보와 일치하는 사용자가 없습니다.',
INVALID: '인증 정보가 유효하지 않습니다.',
},
SIGN_UP: {
SUCCEED: '회원가입에 성공했습니다.',
},
SIGN_IN: {
SUCCEED: '로그인에 성공했습니다.',
UNAUTHORIZED: '인증에 실패했습니다',
TOKEN: '토근 재발급에 성공했습니다.',
},
SIGN_OUT: {
SUCCEED: '로그아웃에 성공했습니다.',
},
},
USRES: {
READ: {
SUCCEED: '프로필 정보 조회에 성공하였습니다.',
},
UPDATE: {
SUCCEED: '프로필 정보 수정에 성공하였습니다.',
},
LOG: {
READ: '프로필 수정 로그 조회에 성공하였습니다.',
},
PASSWORD: {
UPDATE: {
REQUIRED: '변경할 비밀번호를 입력해 주세요.',
SUCCEED: '패스워드 변경에 성공하였습니다.',
},
},
},
COMMENTS: {
COMMON: {
COMMENT: {
REQUIRED: '내용을 입력해 주세요.',
MIN_LENGTH: `내용은 ${MIN_COMMENT_LENGTH}자 이상 작성해야 합니다.`,
},
NOT_FOUND: '댓글이 존재하지 않습니다.',
},
CREATE: {
SUCCEED: '댓글 작성에 성공했습니다.',
},
READ_LIST: {
SUCCEED: '댓글 조회에 성공했습니다.',
},
UPDATE: {
SUCCEED: '댓글 수정에 성공했습니다.',
NO_BODY_DATA: '수정할 정보를 입력해 주세요.',
NO_AUTH: '이 댓글에 대한 수정 권한이 없습니다',
},
DELETE: {
SUCCEED: '댓글 삭제에 성공했습니다.',
NO_AUTH: '이 댓글에 대한 삭제 권한이 없습니다.',
},
},
FEED: {
COMMON: {
REQUIRED: {
TITLE: '제목을 입력해주세요.',
CONTENT: '내용을 입력해주세요.',
UPDATE: '수정할 정보를 입력해주세요.',
},
NO: {
FEED: '게시글이 존재하지 않습니다.',
},
NOT: {
IMG_URL: '이미지 형식이 알맞지 않습니다.',
},
MIN: {
CONTENT: '내용은 10자 이상 작성해야 합니다.',
},
SUCCEED: {
CREATED: '게시글 생성에 성공했습니다.',
UPDATED: '게시글 수정에 성공했습니다.',
GET: '게시글 조회에 성공했습니다.',
GET_ALL: '게시글 목록 조회에 성공했습니다.',
DELETED: '게시글 삭제에 성공했습니다.',
},
},
},
};
< src.middlewares. error-handler.middleware.js >
import { HTTP_STATUS } from '../constants/http-status.constant.js';
export const errorHandler = (err, req, res, next) => {
console.error(err);
// joi에서 발생한 에러 처리
if (err.name === 'ValidationError') {
return res.status(HTTP_STATUS.BAD_REQUEST).json({
status: HTTP_STATUS.BAD_REQUEST,
message: err.message,
});
}
// 그 밖의 예상치 못한 에러 처리
return res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
message: '예상치 못한 에러가 발생했습니다. 관리자에게 문의해 주세요.',
});
};
< src.middlewares. require-access-token.middleware.js >
import { ACCESS_TOKEN_SECRET } from '../constants/env.constant.js';
import { HTTP_STATUS } from '../constants/http-status.constant.js';
import { MESSAGES } from '../constants/message.constant.js';
import jwt from 'jsonwebtoken';
import { prisma } from '../utils/prisma.util.js';
export const requireAccessToken = async (req, res, next) => {
try {
const authorization = req.headers.authorization;
if (!authorization) {
return res.status(HTTP_STATUS.UNAUTHORIZED).json({
message: MESSAGES.AUTH.JWT.NO_TOKEN,
});
}
const [type, accessToken] = authorization.split(' ');
if (type !== 'Bearer') {
return res.status(HTTP_STATUS.UNAUTHORIZED).json({
message: MESSAGES.AUTH.JWT.NOT_SUPPORTED_TYPE,
});
}
if (!accessToken) {
return res.status(HTTP_STATUS.UNAUTHORIZED).json({
message: MESSAGES.AUTH.JWT.NO_TOKEN,
});
}
let payload;
try {
payload = jwt.verify(accessToken, ACCESS_TOKEN_SECRET);
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(HTTP_STATUS.UNAUTHORIZED).json({
message: MESSAGES.AUTH.JWT.EXPIRED,
});
} else {
return res.status(HTTP_STATUS.UNAUTHORIZED).json({
message: MESSAGES.AUTH.JWT.INVALID,
});
}
}
const { id } = payload;
const user = await prisma.user.findUnique({
where: { userId: id },
omit: { password: true },
});
if (!user) {
return res.status(HTTP_STATUS.UNAUTHORIZED).json({
message: MESSAGES.AUTH.JWT.NO_USER,
});
}
if (!user.refreshToken) {
return res.status(HTTP_STATUS.UNAUTHORIZED).json({
message: MESSAGES.AUTH.JWT.EXPIRED,
});
}
req.user = user;
next();
} catch (error) {
next(error);
}
};
< src.middlewares. require-refresh-token.middleware.js >
import { REFRESH_TOKEN_SECRET } from '../constants/env.constant.js';
import { HTTP_STATUS } from '../constants/http-status.constant.js';
import { MESSAGES } from '../constants/message.constant.js';
import jwt from 'jsonwebtoken';
import { prisma } from '../utils/prisma.util.js';
import bcrypt from 'bcrypt';
export const requireRefreshToken = async (req, res, next) => {
try {
const authorization = req.headers.authorization;
if (!authorization) {
return res.status(HTTP_STATUS.UNAUTHORIZED).json({
message: MESSAGES.AUTH.JWT.NO_TOKEN,
});
}
const [type, refreshToken] = authorization.split(' ');
if (type !== 'Bearer') {
return res.status(HTTP_STATUS.UNAUTHORIZED).json({
message: MESSAGES.AUTH.JWT.NOT_SUPPORTED_TYPE,
});
}
if (!refreshToken) {
return res.status(HTTP_STATUS.UNAUTHORIZED).json({
message: MESSAGES.AUTH.JWT.NO_TOKEN,
});
}
let payload;
try {
payload = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET);
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(HTTP_STATUS.UNAUTHORIZED).json({
message: MESSAGES.AUTH.JWT.EXPIRED,
});
} else {
return res.status(HTTP_STATUS.UNAUTHORIZED).json({
message: MESSAGES.AUTH.JWT.INVALID,
});
}
}
const { id } = payload;
const existedRefreshToken = await prisma.user.findUnique({
where: {
userId: id,
},
});
const isValidRefreshToken =
existedRefreshToken?.refreshToken &&
bcrypt.compareSync(refreshToken, existedRefreshToken.refreshToken);
if (!isValidRefreshToken) {
return res.status(HTTP_STATUS.UNAUTHORIZED).json({
message: MESSAGES.AUTH.JWT.NO_USER,
});
}
const user = await prisma.user.findUnique({
where: { userId: id },
omit: { password: true },
});
if (!user) {
return res.status(HTTP_STATUS.UNAUTHORIZED).json({
message: MESSAGES.AUTH.JWT.NO_USER,
});
}
req.user = user;
next();
} catch (error) {
next(error);
}
};
< src.middlewares.validators. sign-up-validator.middleware.js >
import Joi from 'joi';
import { MESSAGES } from '../../constants/message.constant.js';
export const signupValidator = async (req, res, next) => {
try {
const joiSchema = Joi.object({
email: Joi.string()
.email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } })
.required()
.messages({
'string.email': MESSAGES.AUTH.COMMON.EMAIL.INVALID_FORMAT,
'any.required': MESSAGES.AUTH.COMMON.EMAIL.REQUIRED,
}),
nickName: Joi.string().required().messages({
'string.base': MESSAGES.AUTH.COMMON.NICKNAME.NO_STRING,
'any.required': MESSAGES.AUTH.COMMON.NICKNAME.REQUIRED,
}),
password: Joi.string().required().min(6).max(12).messages({
'any.required': MESSAGES.AUTH.COMMON.PASSWORD.REQUIRED,
'string.base': MESSAGES.AUTH.COMMON.PASSWORD.NO_STRING,
'string.min': MESSAGES.AUTH.COMMON.PASSWORD.LENGTH,
'string.max': MESSAGES.AUTH.COMMON.PASSWORD.LENGTH,
}),
repeat_password: Joi.string()
.required()
.valid(Joi.ref('password'))
.messages({
'any.required': MESSAGES.AUTH.COMMON.REPEAT_PASSWORD.REQUIRED,
'any.only': MESSAGES.AUTH.COMMON.REPEAT_PASSWORD.NOT_MATCHED,
}),
introduce: Joi.string().messages({
'string.base': MESSAGES.AUTH.COMMON.INTRODUCE.NO_STRING,
}),
maxweight: Joi.number().integer().messages({
'number.base': MESSAGES.AUTH.COMMON.MAX_WEIGHT.NO_NUMBER,
'number.integer': MESSAGES.AUTH.COMMON.MAX_WEIGHT.NO_NUMBER,
}),
weight: Joi.number().precision(1).messages({
'number.base': MESSAGES.AUTH.COMMON.WEIGHT.NO_NUMBER,
'number.precision': MESSAGES.AUTH.COMMON.WEIGHT.PRECISION,
}),
height: Joi.number().precision(1).messages({
'number.base': MESSAGES.AUTH.COMMON.HEIGHT.NO_NUMBER,
'number.precision': MESSAGES.AUTH.COMMON.HEIGHT.PRECISION,
}),
fat: Joi.number().precision(1).messages({
'number.base': MESSAGES.AUTH.COMMON.FAT.NO_NUMBER,
'number.precision': MESSAGES.AUTH.COMMON.FAT.PRECISION,
}),
metabolic: Joi.number().integer().messages({
'number.base': MESSAGES.AUTH.COMMON.METABOLIC.NO_NUMBER,
'number.integer': MESSAGES.AUTH.COMMON.METABOLIC.NO_NUMBER,
}),
muscleweight: Joi.number().precision(1).messages({
'number.base': MESSAGES.AUTH.COMMON.MUSCLEWEIGHT.NO_NUMBER,
'number.precision': MESSAGES.AUTH.COMMON.MUSCLEWEIGHT.PRECISION,
}),
profile_img_url: Joi.number().precision(1).messages({
'string.base': MESSAGES.AUTH.COMMON.PROFILE_IMG_URL,
}),
showLog: Joi.boolean().messages({
'boolean.base': MESSAGES.AUTH.COMMON.SHOWLOG.NO_BOOLEAN,
}),
});
await joiSchema.validateAsync(req.body);
next();
} catch (error) {
next(error);
}
};
< src.middlewares.validators. sign-in-validator.middleware.js >
import Joi from 'joi';
import { MESSAGES } from '../../constants/message.constant.js';
const schema = Joi.object({
email: Joi.string().email().required().messages({
'any.required': MESSAGES.AUTH.COMMON.EMAIL.REQUIRED,
'string.email': MESSAGES.AUTH.COMMON.EMAIL.INVALID_FORMAT,
}),
password: Joi.string().required().messages({
'any.required': MESSAGES.AUTH.COMMON.PASSWORD.REQUIRED,
}),
});
export const signInValidator = async (req, res, next) => {
try {
await schema.validateAsync(req.body);
next();
} catch (error) {
next(error);
}
};
< src.middlewares.validators. user-profile-validator.middleware.js >
import Joi from 'joi';
import { MESSAGES } from '../../constants/message.constant.js';
export const profileUpdateValidator = async (req, res, next) => {
try {
const joiSchema = Joi.object({
introduce: Joi.string().messages({
'string.base': MESSAGES.AUTH.COMMON.INTRODUCE.NO_STRING,
}),
maxweight: Joi.number().integer().messages({
'number.base': MESSAGES.AUTH.COMMON.MAX_WEIGHT.NO_NUMBER,
'number.integer': MESSAGES.AUTH.COMMON.MAX_WEIGHT.NO_NUMBER,
}),
weight: Joi.number().precision(1).messages({
'number.base': MESSAGES.AUTH.COMMON.WEIGHT.NO_NUMBER,
'number.precision': MESSAGES.AUTH.COMMON.WEIGHT.PRECISION,
}),
height: Joi.number().precision(1).messages({
'number.base': MESSAGES.AUTH.COMMON.HEIGHT.NO_NUMBER,
'number.precision': MESSAGES.AUTH.COMMON.HEIGHT.PRECISION,
}),
fat: Joi.number().precision(1).messages({
'number.base': MESSAGES.AUTH.COMMON.FAT.NO_NUMBER,
'number.precision': MESSAGES.AUTH.COMMON.FAT.PRECISION,
}),
metabolic: Joi.number().integer().messages({
'number.base': MESSAGES.AUTH.COMMON.METABOLIC.NO_NUMBER,
'number.integer': MESSAGES.AUTH.COMMON.METABOLIC.NO_NUMBER,
}),
muscleweight: Joi.number().precision(1).messages({
'number.base': MESSAGES.AUTH.COMMON.MUSCLEWEIGHT.NO_NUMBER,
'number.precision': MESSAGES.AUTH.COMMON.MUSCLEWEIGHT.PRECISION,
}),
profile_img_url: Joi.number().precision(1).messages({
'string.base': MESSAGES.AUTH.COMMON.PROFILE_IMG_URL,
}),
showLog: Joi.boolean().messages({
'boolean.base': MESSAGES.AUTH.COMMON.SHOWLOG.NO_BOOLEAN,
}),
});
await joiSchema.validateAsync(req.body);
next();
} catch (error) {
next(error);
}
};
export const passwordUpdateValidator = async (req, res, next) => {
try {
const joiSchema = Joi.object({
password: Joi.string().required().messages({
'any.required': MESSAGES.AUTH.COMMON.PASSWORD.REQUIRED,
'string.base': MESSAGES.AUTH.COMMON.PASSWORD.NO_STRING,
}),
repeat_password: Joi.string()
.required()
.valid(Joi.ref('updatePassword'))
.messages({
'any.required': MESSAGES.AUTH.COMMON.REPEAT_PASSWORD.REQUIRED,
'any.only': MESSAGES.AUTH.COMMON.REPEAT_PASSWORD.NOT_MATCHED,
}),
updatePassword: Joi.string().required().min(6).max(12).messages({
'any.required': MESSAGES.USRES.PASSWORD.UPDATE.REQUIRED,
'string.base': MESSAGES.AUTH.COMMON.PASSWORD.NO_STRING,
'string.min': MESSAGES.AUTH.COMMON.PASSWORD.LENGTH,
'string.max': MESSAGES.AUTH.COMMON.PASSWORD.LENGTH,
}),
});
await joiSchema.validateAsync(req.body);
next();
} catch (error) {
next(error);
}
};
< src.middlewares.validators. feed-create-validator.middleware.js >
import Joi from 'joi';
import { MESSAGES } from '../../constants/message.constant.js';
// joi 게시물 작성 유효성 검사
const schema = Joi.object({
title: Joi.string().required().messages({
'any.required': MESSAGES.FEED.COMMON.REQUIRED.TITLE,
}),
content: Joi.string().min(10).required().messages({
'any.required': MESSAGES.FEED.COMMON.REQUIRED.CONTENT,
'string.min': MESSAGES.FEED.COMMON.MIN.CONTENT,
}),
feed_img_url: Joi.string().uri().messages({
'string.uri': MESSAGES.FEED.COMMON.NOT.IMG_URL,
}),
});
export const feedCreateValidator = async (req, res, next) => {
try {
await schema.validateAsync(req.body);
next();
} catch (error) {
next(error);
}
};
< src.middlewares.validators. feed-update-validator.middleware.js >
import Joi from 'joi';
import { MESSAGES } from '../../constants/message.constant.js';
// joi 게시물 수정 유효성 검사
const schema = Joi.object({
title: Joi.string(),
content: Joi.string().min(10).messages({
'string.min': MESSAGES.FEED.COMMON.MIN.CONTENT,
}),
feed_img_url: Joi.string().uri().messages({
'string.uri': MESSAGES.FEED.COMMON.NOT.IMG_URL,
}),
});
export const feedUpdateValidator = async (req, res, next) => {
try {
await schema.validateAsync(req.body);
next();
} catch (error) {
next(error);
}
};
< src.middlewares.validators. create-comment-validator.middleware.js >
import Joi from 'joi';
import { MESSAGES } from '../../constants/message.constant.js';
import { MIN_COMMENT_LENGTH } from '../../constants/comment.constant.js';
const schema = Joi.object({
comment: Joi.string().required().min(MIN_COMMENT_LENGTH).messages({
'any.required': MESSAGES.COMMENTS.COMMON.COMMENT.REQUIRED,
'string.min': MESSAGES.COMMENTS.COMMON.COMMENT.MIN_LENGTH,
}),
});
export const createCommentValidator = async (req, res, next) => {
try {
await schema.validateAsync(req.body);
next();
} catch (error) {
next(error);
}
};
< src.middlewares.validators. update-comment-validator.middleware.js >
import Joi from 'joi';
import { MESSAGES } from '../../constants/message.constant.js';
import { MIN_COMMENT_LENGTH } from '../../constants/comment.constant.js';
const schema = Joi.object({
comment: Joi.string().required().min(MIN_COMMENT_LENGTH).messages({
'any.required': MESSAGES.COMMENTS.COMMON.COMMENT.REQUIRED,
'string.min': MESSAGES.COMMENTS.COMMON.COMMENT.MIN_LENGTH,
}),
}).messages({
'object.min': MESSAGES.COMMENTS.UPDATE.NO_BODY_DATA,
});
export const updateCommentValidator = async (req, res, next) => {
try {
await schema.validateAsync(req.body);
next();
} catch (error) {
next(error);
}
};
다음 TIL은 routers 작성을 해보도록 하겠다.
이때 알고가야 할 부분이 있다.
prisma를 사용하면서 다음과 같은 메서드를 사용할 때 차이점을 알아야 할 필요가 있다.
findFirst 와 findUnique의 차이
findFirst는 모든 필드를 이용하여 필터링이 가능하고
findUnique는 unique필드만(@unique)을 이용하여 필터링이 가능하다.
nodemailer를 사용하여 이메일을 보내기 위해 Gmail 계정의 주소와 비밀번호가 필요한 이유는 이메일 전송에 사용되는 SMTP(Simple Mail Transfer Protocol) 서버에 인증을 해야 하기 때문입니다. 이메일 서버는 보안을 위해 이메일을 보내는 사용자가 실제로 해당 이메일 계정의 소유자인지 확인할 필요가 있습니다.
여기서 간단히 nodemailer의 역할과 Gmail 계정의 필요성을 설명하겠습니다:
- SMTP 서버와 nodemailer:
- nodemailer는 Node.js 애플리케이션에서 이메일을 전송하기 위해 SMTP 프로토콜을 사용하는 라이브러리입니다.
- 이메일을 보내기 위해서는 이메일 서비스 제공자의 SMTP 서버에 연결하고 인증을 받아야 합니다. 이 과정에서 이메일 주소와 비밀번호가 필요합니다.
- Gmail 계정의 사용:
- Gmail의 SMTP 서버를 이용하기 위해서는 Gmail 계정의 인증 정보(이메일 주소와 비밀번호)가 필요합니다.
- Gmail의 SMTP 서버 설정:
- SMTP 서버: smtp.gmail.com
- 포트: 587 (TLS) 또는 465 (SSL)
- 인증 방식: 이메일 주소와 비밀번호를 사용한 로그인
- 보안:
- 이메일 서버는 스팸과 불법적인 사용을 방지하기 위해 인증을 요구합니다. 이는 이메일 서버가 사용자의 신원을 확인하고 권한이 있는 사용자만 이메일을 전송할 수 있게 하기 위함입니다.
예제 코드 (보안 정보 환경 변수 사용)
환경 변수 파일(.env)을 사용하여 민감한 정보를 보호할 수 있습니다.
- 환경 변수 파일 설정:
- 프로젝트 루트 디렉토리에 .env 파일을 생성합니다.
NODEMAILER_USER=your-email@gmail.com NODEMAILER_PASS=your-email-password
- 환경 변수 로드 및 nodemailer 설정:
- dotenv 패키지를 설치하고 환경 변수를 로드합니다.
// index.js require('dotenv').config(); const nodemailer = require('nodemailer'); const transporter = nodemailer.createTransport({ service: 'gmail', host: 'smtp.gmail.com', port: 587, secure: false, auth: { user: process.env.NODEMAILER_USER, pass: process.env.NODEMAILER_PASS, }, }); // 이메일 전송 예제 const mailOptions = { from: process.env.NODEMAILER_USER, to: 'recipient@example.com', subject: 'Test Email', text: 'This is a test email sent using nodemailer', }; transporter.sendMail(mailOptions, (error, info) => { if (error) { console.error('Error sending email:', error); } else { console.log('Email sent:', info.response); } });
주의 사항
- Gmail 보안 설정: 기본적으로 Gmail은 외부 애플리케이션의 접근을 차단합니다. 이를 허용하려면 Gmail 계정에서 "보안 수준이 낮은 앱의 액세스 허용"을 활성화해야 합니다. 그러나 이는 보안에 취약할 수 있으므로 권장하지 않습니다.
- 앱 비밀번호 사용: Gmail 계정에서 2단계 인증을 설정한 경우, 애플리케이션 전용 비밀번호를 생성하여 사용하는 것이 더 안전합니다.
- 환경 변수 사용: 민감한 정보는 코드에 직접 포함하지 말고 환경 변수를 사용하여 관리하십시오.
이 방법을 통해 이메일 전송에 필요한 인증 정보를 안전하게 관리하고 Gmail을 통해 이메일을 전송할 수 있습니다.