<최종 목표>
<오늘 목표>
프로젝트를 열고 터미널에 yarn init -y.
package.json 에서 "type" = "module" 추가.
yarn add @prisma/client bcrypt dotenv express jsonwebtoken prisma winston 추가.
.prettiercc 파일 생성하고 아래와 같이 추가.
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80,
"bracketSpacing": true,
"arrowParens": "always"
}
src/
│
├── app.js # 애플리케이션 진입점
├── controllers/ # 컨트롤러 파일
│ ├── authController.js # 인증 관련 컨트롤러
│ └── characterController.js # 캐릭터 관련 컨트롤러
│
├── middlewares/ # 미들웨어 파일
│ ├── authenticateJWT.js # JWT 인증 미들웨어
│ ├── errorHandler.js # 에러 핸들링 미들웨어
│ └── logger.js # 로깅 미들웨어
│
├── routes/ # 라우트 정의 파일
│ ├── authRoutes.js # 인증 관련 라우트
│ └── characterRoutes.js # 캐릭터 관련 라우트
│
└── utils/ # 유틸리티 파일 및 폴더
└── prisma/ # Prisma ORM 관련 파일
└── index.js # Prisma 인스턴스 초기화 및 설정
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient({
// Prisma를 이용해 데이터베이스를 접근할 때, SQL을 출력해줍니다.
log: ['query', 'info', 'warn', 'error'],
// 에러 메시지를 평문이 아닌, 개발자가 읽기 쉬운 형태로 출력해줍니다.
errorFormat: 'pretty',
});
import express from 'express';
import dotenv from 'dotenv';
import { authenticateJWT } from './middlewares/authenticateJWT.js';
import { errorHandler } from './middlewares/errorHandler.js';
import { logger } from './middlewares/logger.js';
import authRoutes from './routes/authRoutes.js'; // 인증 라우트
import characterRoutes from './routes/characterRoutes.js'; // 캐릭터 라우트
dotenv.config(); // 환경 변수 로드
const app = express();
app.use(express.json());
// 로그 미들웨어 적용
app.use(logger);
// 인증 관련 라우트
app.use('/auth', authRoutes);
// 캐릭터 생성 라우트 (JWT 인증 필요)
app.use('/character', characterRoutes);
// JWT 인증이 필요한 보호된 경로
app.get('/protected', authenticateJWT, (req, res) => {
res.json({ message: 'This is a protected route', user: req.locals.user });
});
// 에러 처리 미들웨어
app.use(errorHandler);
// 서버 시작
app.listen(3000, () => {
console.log('포트 3000 서버가 시작');
});
/authRoutes.js
import express from 'express';
import { register, login } from '../controllers/authController.js';
const router = express.Router();
// 회원가입 라우트
router.post('/register', register);
// 로그인 라우트
router.post('/login', login);
export default router;
/characterRoutes.js
import express from 'express';
import { createCharacter } from '../controllers/characterController.js';
import { authenticateJWT } from '../middlewares/authenticateJWT.js';
const router = express.Router();
// 캐릭터 생성 라우트 (JWT 인증 필요)
router.post('/create', authenticateJWT, createCharacter);
export default router;
authController.js
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { prisma } from '../utils/prisma/index.js';
// 정규식으로 영어 소문자와 숫자만 허용하는 패턴
const usernameRegex = /^[a-z0-9]+$/;
// 회원가입 처리
export const register = async (req, res, next) => {
const { userId, password, confirmPassword } = req.body;
try {
// 아이디 중복 확인
const existingUsername = await prisma.account.findUnique({
where: { userId },
});
if (existingUsername) {
return res.status(400).json({ error: '해당 아이디가 이미 존재합니다' });
}
// 아이디 형식 검증 (영어 소문자 + 숫자 조합인지 확인)
if (!usernameRegex.test(userId)) {
return res.status(400).json({ error: '아이디는 소문자와 숫자 조합으로만 만들 수 있습니다' });
}
// 비밀번호 길이 확인 (최소 6자 이상)
if (password.length < 6) {
return res.status(400).json({ error: '비밀번호는 최소 6자리 이상으로 설정해주세요' });
}
// 비밀번호와 비밀번호 확인이 일치하는지 확인
if (password !== confirmPassword) {
return res.status(400).json({ error: '비밀번호가 서로 일치하지 않습니다' });
}
// 비밀번호 암호화
const hashedPassword = await bcrypt.hash(password, 10);
// 새로운 사용자 생성
const newUser = await prisma.account.create({
data: {
userId,
password: hashedPassword,
},
});
res.status(201).json({ message: '회원 가입에 성공하였습니다!', user: { id: newUser.id, userId: newUser.userId } });
} catch (error) {
next(error);
}
};
export const login = async (req, res, next) => {
const { userId, password } = req.body;
try {
// 아이디로 사용자 찾기
const user = await prisma.account.findUnique({
where: { userId },
});
if (!user) {
return res.status(401).json({ error: '아이디가 존재하지 않거나 틀렸습니다' });
}
// 비밀번호 비교
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({ error: '비밀번호가 틀렸습니다' });
}
// JWT 발급
const token = jwt.sign({ id: user.accountId, userId: user.userId }, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({ message: '로그인 성공!!', token });
} catch (error) {
next(error);
}
};
characterController.js
import { prisma } from '../utils/prisma/index.js';
// 캐릭터 생성 API
export const createCharacter = async (req, res, next) => {
const { name } = req.body;
const userId = req.locals.user?.userId; // JWT에서 추출한 userId 확인
// userId가 없으면 오류 반환
if (!userId) {
return res.status(400).json({ error: '사용자 정보가 존재하지 않습니다' });
}
try {
// userId로 계정 조회
const userAccount = await prisma.account.findUnique({
where: { userId }, // userId가 Prisma 모델에서 고유해야 합니다.
});
if (!userAccount) {
return res.status(404).json({ error: '계정이 존재하지 않습니다' });
}
// 이미 존재하는 캐릭터명인지 확인
const existingCharacter = await prisma.character.findUnique({
where: { name },
});
if (existingCharacter) {
return res.status(400).json({ error: '캐릭터 이름이 이미 존재합니다' });
}
// 캐릭터 생성
const newCharacter = await prisma.character.create({
data: {
name,
accountId: userAccount.accountId, // 계정과 연결된 캐릭터 생성
},
});
res.status(201).json({
message: '캐릭터 생성 완료',
characterId: newCharacter.characterId,
});
} catch (error) {
next(error);
}
};
authenticateJWT.js
import jwt from 'jsonwebtoken';
/**
* JWT 인증 미들웨어
*/
export const authenticateJWT = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
const error = new Error('Authorization header is missing');
error.statusCode = 401;
return next(error); // next()를 통해 에러 미들웨어로 전달
}
const tokenParts = authHeader.split(' ');
if (tokenParts[0] !== 'Bearer' || tokenParts.length !== 2) {
const error = new Error('Authorization header must be in the format: Bearer <token>');
error.statusCode = 400;
return next(error);
}
const token = tokenParts[1];
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
if (err.name === 'TokenExpiredError') {
const error = new Error('JWT has expired');
error.statusCode = 401;
return next(error);
} else {
const error = new Error('Invalid JWT token');
error.statusCode = 403;
return next(error);
}
}
req.locals = req.locals || {};
req.locals.user = user;
next();
});
};
errorHandler.js
export const errorHandler = (err, req, res, next) => {
console.error(err); // 서버에서 에러 로깅
const statusCode = err.statusCode || 500; // 기본값은 500 (서버 오류)
const message = err.message || 'Internal Server Error';
res.status(statusCode).json({
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }), // 개발 환경에서는 스택 트레이스를 함께 반환
});
};
logger.js
import winston from 'winston';
// Winston 로그 설정
const loggerInstance = winston.createLogger({
level: 'info', // 로그 레벨 설정 (info, error 등)
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf(({ timestamp, level, message }) => {
return `[${timestamp}] ${level.toUpperCase()}: ${message}`;
})
),
transports: [
new winston.transports.File({ filename: 'logs/combined.log' }), // 로그 파일에 저장
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), // 에러 로그를 별도 파일에 저장
new winston.transports.Console() // 콘솔에도 로그 출력
],
});
// 로그 처리 미들웨어
export const logger = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
const logMessage = `${req.method} ${req.originalUrl} ${res.statusCode} - ${duration}ms`;
// 요청 성공 로그는 info 레벨로 기록
if (res.statusCode >= 400) {
// 에러 상태코드인 경우는 error 레벨로 기록
loggerInstance.error(logMessage);
} else {
loggerInstance.info(logMessage);
}
});
next();
};
오늘 소스코드 구현은 여기까지입니다!