diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5dc33b2..a2cc254 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,7 @@ on: types: - opened - reopened + - synchronize paths-ignore: - 'README.md' diff --git a/src/domains/auth/signup/signupController.js b/src/domains/auth/signup/signupController.js index b8dd0bb..1028033 100644 --- a/src/domains/auth/signup/signupController.js +++ b/src/domains/auth/signup/signupController.js @@ -2,6 +2,7 @@ const signupService = require('./signupService'); const { signupValidation } = require('./signupValidation'); const logger = require('../../../config/logger'); const { validationResult } = require('express-validator'); +const SignupDto = require('./signupDto'); const signupController = [ ...signupValidation, // 유효성 검사 미들웨어 추가 @@ -14,30 +15,28 @@ const signupController = [ const errors = validationResult(req); if (!errors.isEmpty()) { const statusCode = 400; - logger.warn(`(${statusCode}) Validation failed: ${errors.array().map(err => err.msg).join(', ')}`); // 검증 실패 로깅 - return res.status(400).json({ message: errors.array().map(err => err.msg).join(', ') }); + logger.warn(`(${statusCode}) user validation failed: ${errors.array().map(err => err.msg).join(', ')}`); // 검증 실패 로깅 + return res.status(400).json({ + "Invalid values": errors.array().map(err => ({ message: err.msg })) + }); } - // gender 값 변환 - let gender = req.body.gender; - if (gender === '남성') gender = 'm'; - if (gender === '여성') gender = 'f'; - if (gender === '비공개') gender = 'n'; - - const signupData = { - ...req.body, - gender, - }; + const userData = new SignupDto(req.body.username, req.body.password, req.body.name, req.body.gender, req.body.birthDate); // 회원가입 처리 - const user = await signupService.signupUser(signupData); + const user = await signupService.createUser(userData); logger.info(`User created successfully: ID=${user.id}, username=${user.username}`); // 성공 로깅 - const { password, ...userWithoutPassword } = user; - return res.status(201).json(userWithoutPassword); + const responseUser = { + ...user, + gender: SignupDto.convertGenderToKorean(user.gender), + password: undefined + }; + + return res.status(201).json(responseUser); } catch (error) { const statusCode = 500; - logger.error(`(${statusCode}) Signup failed: ${error.message}`); // 오류 로깅 + logger.error(`(${statusCode}) Signup failed: ${error.message}\nStack trace: ${error.stack}`); return res.status(500).json({ message: '서버 오류', error: error.message }); } } diff --git a/src/domains/auth/signup/signupDto.js b/src/domains/auth/signup/signupDto.js index b9bd992..5bceb1e 100644 --- a/src/domains/auth/signup/signupDto.js +++ b/src/domains/auth/signup/signupDto.js @@ -1,4 +1,4 @@ -class SignupDto { +class signupDto { constructor(username, password, name, gender, birthDate) { this.username = username; this.password = password; @@ -6,6 +6,22 @@ class SignupDto { this.gender = gender; this.birthDate = birthDate; } + + // DB에 저장할 때 + static convertGenderToEnglish(gender) { + if (gender === '남성') return 'm'; + if (gender === '여성') return 'f'; + if (gender === '비공개') return 'n'; + return gender; // 유효하지 않은 값은 그대로 반환 + } + + // DB에서 값을 가져올 때 + static convertGenderToKorean(gender) { + if (gender === 'm') return '남성'; + if (gender === 'f') return '여성'; + if (gender === 'n') return '비공개'; + return gender; // 유효하지 않은 값은 그대로 반환 + } } -module.exports = SignupDto; \ No newline at end of file +module.exports = signupDto; \ No newline at end of file diff --git a/src/domains/auth/signup/signupRepository.js b/src/domains/auth/signup/signupRepository.js index fb8e291..0d81c92 100644 --- a/src/domains/auth/signup/signupRepository.js +++ b/src/domains/auth/signup/signupRepository.js @@ -20,7 +20,6 @@ const createUser = async (SignupDto) => { }); return user; } catch (error) { - console.error('Error stack trace:', error.stack); // 에러 스택을 콘솔에 출력 logger.error(`createUser failed: ${error.message}`); throw new Error('createUser 실패'); } diff --git a/src/domains/auth/signup/signupService.js b/src/domains/auth/signup/signupService.js index 3833c1f..50fb536 100644 --- a/src/domains/auth/signup/signupService.js +++ b/src/domains/auth/signup/signupService.js @@ -2,22 +2,23 @@ const bcrypt = require('bcryptjs'); const signupRepository = require('./signupRepository'); const SignupDto = require('./signupDto'); -const signupUser = async (signupData) => { +const createUser = async (userData) => { // 비밀번호 해싱 - const hashedPassword = await bcrypt.hash(signupData.password, 10); + const hashedPassword = await bcrypt.hash(userData.password, 10); - const signupDto = new SignupDto( - signupData.username, - hashedPassword, - signupData.name, - signupData.gender, - signupData.birthDate - ); + const gender = SignupDto.convertGenderToEnglish(userData.gender); - // 유저 생성 - const newUser = await signupRepository.createUser(signupDto); + // DB에 저장할 객체 생성 + const newUser = { + username: userData.username, + password: hashedPassword, + name: userData.name, + gender: gender, + birthDate: userData.birthDate, + }; - return newUser; + // 유저 생성 + return await signupRepository.createUser(newUser); }; -module.exports = { signupUser }; \ No newline at end of file +module.exports = { createUser }; \ No newline at end of file diff --git a/src/domains/auth/signup/signupValidation.js b/src/domains/auth/signup/signupValidation.js index 5f91841..5d1234a 100644 --- a/src/domains/auth/signup/signupValidation.js +++ b/src/domains/auth/signup/signupValidation.js @@ -3,43 +3,60 @@ const { prisma } = require('../../../config/db'); const signupValidation = [ body('username') - .isString() - .notEmpty() - .withMessage('Username is required.') - .isLength({ min: 5, max: 20 }) - .withMessage('Username must be between 5 and 20 characters.') - .custom(async (value) => { - // Prisma를 사용해 username 중복 확인 - const existingUser = await prisma.user.findUnique({ - where: { username: value }, - }); - - if (existingUser) { - throw new Error('Username already exists. Please choose another.'); - } - }), + .notEmpty().withMessage('Username is required.') + .isString().withMessage('Username must be a string.') + .isLength({ min: 5, max: 20 }).withMessage('Username must be between 5 and 20 characters.') + .custom(async (value) => { + const existingUser = await prisma.user.findUnique({ + where: { username: value }, + }); + if (existingUser) { + throw new Error('Username already exists. Please choose another.'); + } + }), body('password') - .isString() - .notEmpty() - .withMessage('Password is required.') - .isLength({ min: 8 }) - .withMessage('Password must be at least 8 characters long.'), - - body('gender') - .isIn(['남성', '여성', '비공개']) - .withMessage('Gender must be either 남성, 여성, or 비공개.'), - - body('birthDate') - .matches(/^\d{4}-\d{2}-\d{2}$/) // YYYY-MM-DD 형식을 검증하는 정규 표현식 - .withMessage('Birthdate must be a valid date in YYYY-MM-DD format.'), + .notEmpty().withMessage('Password is required.') + .custom((value) => { + if (value && value.length < 8) { + throw new Error('Password must be at least 8 characters long.'); + } + return true; + }), body('name') - .isString() - .notEmpty() - .withMessage('Name is required.') - .isLength({ max: 50 }) - .withMessage('Name must not exceed 50 characters.') + .custom((value) => { + if (!value) { + throw new Error('Name is required.'); + } + if (typeof value !== 'string') { + throw new Error('Name must be a string.'); + } + if (value.length > 50) { + throw new Error('Name must not exceed 50 characters.'); + } + return true; + }), + + body('gender') + .notEmpty().withMessage('Gender is required.') + .custom((value) => { + if (value && !['남성', '여성', '비공개'].includes(value)) { + throw new Error('Gender must be either 남성, 여성, or 비공개.'); + } + return true; + }), + + body('birthDate') + .custom((value) => { + if (!value) { + throw new Error('Birthdate is required.'); + } + if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { + throw new Error('Birthdate must be a valid date in YYYY-MM-DD format.'); + } + return true; + }), ]; module.exports = { signupValidation }; diff --git a/src/domains/timeline/controller/timelineController.js b/src/domains/timeline/controller/timelineController.js index 587e9f8..394945e 100644 --- a/src/domains/timeline/controller/timelineController.js +++ b/src/domains/timeline/controller/timelineController.js @@ -5,7 +5,7 @@ const timelineService = require('../service/timelineService'); const createTimeline = async (req, res) => { const userId = req.user.id; // JWT 토큰에서 사용자 정보 가져오기 const { name, items } = req.body; // 요청 본문에서 타임라인 이름과 아이템들 받기 - logger.info(`Received request to create timeline: userId = ${userId}, name = ${name}`); + logger.info(`Received request to create timeline: ${req.method} ${req.originalUrl}, userId=${userId}, name=${name}`); if (!name) { return res.status(400).json({ success: false, message: 'Timeline name is required' }); @@ -26,7 +26,7 @@ const createTimeline = async (req, res) => { const deleteTimeline = async (req, res) => { const userId = req.user.id; const { timelineId } = req.params; - logger.info(`Received request to delete timeline: userId = ${userId}, timelineId = ${timelineId}`); + logger.info(`Received request to delete timeline: ${req.method} ${req.originalUrl}, userId=${userId}, timelineId=${timelineId}`); try { const isDeleted = await timelineService.deleteTimeline(userId, timelineId); @@ -47,7 +47,7 @@ const deleteTimeline = async (req, res) => { // 타임라인 목록 조회 const getTimelines = async (req, res) => { const userId = req.user.id; - logger.info(`Received request to fetch timelines for user: ${userId}`); + logger.info(`Received request to fetch timelines for user: ${req.method} ${req.originalUrl}, userId=${userId}`); try { const timelines = await timelineService.getTimelines(userId); @@ -63,7 +63,7 @@ const getTimelines = async (req, res) => { const getTimelineById = async (req, res) => { const { timelineId } = req.params; const userId = req.user.id; - logger.info(`Received request to fetch a timeline: userId = ${userId}, timelineId = ${timelineId}`); + logger.info(`Received request to fetch a timeline: ${req.method} ${req.originalUrl}, userId=${userId}, timelineId=${timelineId}`); try { const timeline = await timelineService.getTimelineById(timelineId, userId); @@ -86,7 +86,7 @@ const updateTimeline = async (req, res) => { const { timelineId } = req.params; const userId = req.user.id; const { name, items } = req.body; - logger.info(`Received request to update a timeline: userId = ${userId}, timelineId = ${timelineId}`); + logger.info(`Received request to update a timeline: ${req.method} ${req.originalUrl}, userId=${userId}, timelineId=${timelineId}`); try { const updatedTimeline = await timelineService.updateTimeline(userId, timelineId, name, items); @@ -101,6 +101,7 @@ const updateTimeline = async (req, res) => { } catch (error) { // 순서 이상 오류 처리 if (error.code === 'INVALID_POSITIONS') { + logger.warn(`Invalid positions error: ${error.message}`); return res.status(400).json({ success: false, message: 'Positions must be consecutive starting from 1 and cannot have duplicates.', @@ -109,6 +110,7 @@ const updateTimeline = async (req, res) => { // 중복된 scrapId 오류 처리 if (error.code === 'DUPLICATE_SCRAPS') { + logger.warn(`Duplicate scrapId error: ${error.message}`); return res.status(400).json({ success: false, message: 'Duplicate scrapId found within the same timeline.', diff --git a/src/domains/timeline/service/timelineService.js b/src/domains/timeline/service/timelineService.js index e79fae7..18fe0c8 100644 --- a/src/domains/timeline/service/timelineService.js +++ b/src/domains/timeline/service/timelineService.js @@ -117,7 +117,7 @@ const deleteTimeline = async (userId, timelineId) => { logger.info(`Successfully deleted timeline ${timelineId} for user ${userId}`); return true; } catch (error) { - logger.error(`Error deleting timeline ${timelineId}: ${error.message}`); + logger.error(`Error deleting timeline ${timelineId}: ${error.message}\nStack trace: ${error.stack}`); throw error; } }; @@ -137,7 +137,7 @@ const getTimelines = async (userId) => { return timelines; } catch (error) { - logger.error(`Error retrieving timelines for user: ${userId}, Error: ${error.message}`); + logger.error(`Error retrieving timelines for user: ${userId}, Error: ${error.message}\nStack trace: ${error.stack}`); throw error; } }; @@ -163,7 +163,7 @@ const getTimelineById = async (timelineId, userId) => { logger.info(`Successfully retrieved timeline with ID: ${timelineId} for user: ${userId}`); return { ...timeline, items: itemsWithArticles }; } catch (error) { - logger.error(`Error retrieving timeline with ID: ${timelineId} for user: ${userId}, Error: ${error.message}`); + logger.error(`Error retrieving timeline with ID: ${timelineId} for user: ${userId}, Error: ${error.message}\nStack trace: ${error.stack}`); throw error; } }; @@ -233,7 +233,7 @@ const updateTimeline = async (userId, timelineId, name, items) => { return updatedTimeline; }); } catch (error) { - logger.error(`Error updating timeline: ${error.message}, userId = ${userId}, timelineId = ${timelineId}`); + logger.error(`Error updating timeline: ${error.message}, userId = ${userId}, timelineId = ${timelineId}\nStack trace: ${error.stack}`); throw error; } };