From 06e3e19f877c5cd8585cbd73e4865f6da792ad02 Mon Sep 17 00:00:00 2001 From: Hyein Jeong Date: Wed, 13 Nov 2024 23:17:23 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[BE][Feat]=20#159=20:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 회원가입 api에 대한 swagger 문서 추가 - 회원가입 api 구현 - auth에 대한 jsdoc 추가 - 회원가입시 비밀번호 암호화해서 db에 저장 --- backend/src/controllers/authController.js | 28 +++++++++++++-- backend/src/repositories/userRepository.js | 40 +++++++++++++++++++++ backend/src/routes/authRouter.js | 42 ++++++++++++++++++++-- backend/src/services/authService.js | 28 ++++++++++++++- backend/swaggerConfig.js | 25 +++++++++++++ 5 files changed, 158 insertions(+), 5 deletions(-) diff --git a/backend/src/controllers/authController.js b/backend/src/controllers/authController.js index 4cb92083..a2649e12 100644 --- a/backend/src/controllers/authController.js +++ b/backend/src/controllers/authController.js @@ -1,6 +1,9 @@ -import { loginUser } from '../services/authService.js'; +import { loginUser, registerUser } from '../services/authService.js'; -export const login = async (req, res) => { +/** + * @description 로그인 컨트롤러 + */ +export const loginController = async (req, res) => { const { id, password } = req.body; try { @@ -18,3 +21,24 @@ export const login = async (req, res) => { return res.status(500).json({ success: false, message: 'Server error occurred' }); } }; + +/** + * @description 회원가입 컨트롤러 + */ +export const registerUserController = async (req, res) => { + try { + const { id, name, password, email } = req.body; + const newUser = await registerUser(id, name, password, email); + return res.status(201).json({ + success: true, + message: 'Login successfully', + data: newUser, + }); + } catch (error) { + if (error.message === 'User ID already exists') { + return res.status(409).json({ error: 'User ID already exists' }); + } + console.error('User registration error:', error); + res.status(500).json({ error: 'Server error' }); + } +}; diff --git a/backend/src/repositories/userRepository.js b/backend/src/repositories/userRepository.js index 56e133e5..31ea683b 100644 --- a/backend/src/repositories/userRepository.js +++ b/backend/src/repositories/userRepository.js @@ -1,6 +1,46 @@ import { pool } from '../db/db.js'; +/** + * @description 데이터베이스에 새로운 사용자 생성 + * @param {string} id - 사용자 id + * @returns {object} id로 찾은 사용자 정보 + */ export const findUserById = async id => { const result = await pool.query('SELECT * FROM "main"."user" WHERE id = $1', [id]); return result.rows[0]; }; + +/** + * @description 사용자 ID 중복 여부 확인 + * @param {string} id - 사용자 ID + * @returns {boolean} 중복 여부 + */ +export const isUserIdDuplicate = async id => { + const query = ` + SELECT 1 FROM "main"."user" + WHERE id = $1; + `; + const result = await pool.query(query, [id]); + + return result.rows.length > 0; +}; + +/** + * @description 데이터베이스에 새로운 사용자 생성 + * @param {string} id - 사용자 ID + * @param {string} name - 사용자 이름 + * @param {string} password - 사용자 비밀번호 + * @param {string} email - 사용자 이메일 + * @returns {object} 새로 생성된 사용자 정보 + */ +export const createUserInDB = async (id, name, password, email) => { + const query = ` + INSERT INTO "main"."user" (id, name, password, email) + VALUES ($1, $2, $3, $4) + RETURNING id, name, email; + `; + const values = [id, name, password, email]; + const result = await pool.query(query, values); + + return result.rows[0]; +}; diff --git a/backend/src/routes/authRouter.js b/backend/src/routes/authRouter.js index 582c0254..5c81901c 100644 --- a/backend/src/routes/authRouter.js +++ b/backend/src/routes/authRouter.js @@ -1,6 +1,6 @@ import express from 'express'; import { body } from 'express-validator'; -import { login } from '../controllers/authController.js'; +import { loginController, registerUserController } from '../controllers/authController.js'; import { validationMiddleware } from '../middleware/validationMiddleware.js'; export const authRouter = express.Router(); @@ -11,6 +11,7 @@ export const authRouter = express.Router(); * post: * summary: 사용자 로그인 API * description: 사용자가 로그인할 수 있도록 ID와 비밀번호를 통해 인증 후 토큰을 반환합니다. + * tags: [Auth] * requestBody: * required: true * description: 로그인을 위한 ID와 비밀번호를 포함한 요청 body @@ -41,5 +42,42 @@ authRouter.post( .withMessage('Password must be at least 6 characters long'), ], validationMiddleware, - login, + loginController, +); + +/** + * @swagger + * /user/register: + * post: + * summary: "회원가입 API" + * description: "사용자가 회원가입을 통해 계정을 생성합니다." + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/RegisterRequest' + * responses: + * "201": + * description: "회원가입 성공" + * "400": + * description: "유효성 검사 실패" + * "409": + * description: "중복된 사용자 ID" + * "500": + * description: "서버 오류" + */ +authRouter.post( + '/register', + [ + body('id').isLength({ min: 4 }).withMessage('User ID must be at least 4 characters long'), + body('name').notEmpty().withMessage('Name is required'), + body('password') + .isLength({ min: 6 }) + .withMessage('Password must be at least 6 characters long'), + body('email').isEmail().withMessage('Valid email is required'), + ], + validationMiddleware, + registerUserController, ); diff --git a/backend/src/services/authService.js b/backend/src/services/authService.js index e8f7c36c..40c6bce6 100644 --- a/backend/src/services/authService.js +++ b/backend/src/services/authService.js @@ -1,7 +1,13 @@ import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; -import { findUserById } from '../repositories/userRepository.js'; +import { createUserInDB, findUserById, isUserIdDuplicate } from '../repositories/userRepository.js'; +/** + * @description 로그인 서비스 + * @param {string} name - 사용자 이름 + * @param {string} password - 사용자 비밀번호 + * @returns {object} 로그인된 사용자의 토큰과 id + */ export const loginUser = async (id, password) => { const user = await findUserById(id); if (!user) { @@ -17,3 +23,23 @@ export const loginUser = async (id, password) => { const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' }); return { token, userId: user.id }; }; + +/** + * @description 회원가입 서비스 + * @param {string} id - 사용자 ID + * @param {string} name - 사용자 이름 + * @param {string} password - 사용자 비밀번호 + * @param {string} email - 사용자 이메일 + * @returns {object} 새로 생성된 사용자 정보 + */ +export const registerUser = async (id, name, password, email) => { + const isDuplicate = await isUserIdDuplicate(id); + if (isDuplicate) { + throw new Error('User ID already exists'); + } + + const hashedPassword = await bcrypt.hash(password, 10); + const newUser = await createUserInDB(id, name, hashedPassword, email); + + return newUser; +}; diff --git a/backend/swaggerConfig.js b/backend/swaggerConfig.js index 72896848..ec6ed9ad 100644 --- a/backend/swaggerConfig.js +++ b/backend/swaggerConfig.js @@ -43,6 +43,31 @@ const swaggerDefinition = { }, }, + // 회원가입 요청 스키마 + RegisterRequest: { + type: 'object', + properties: { + id: { + type: 'string', + description: '사용자 고유 ID (중복 불가)', + }, + name: { + type: 'string', + description: '사용자 이름', + }, + password: { + type: 'string', + description: '사용자 비밀번호 (최소 6자 이상)', + }, + email: { + type: 'string', + format: 'email', + description: '사용자 이메일 주소', + }, + }, + required: ['id', 'name', 'password', 'email'], + }, + // 채널 생성 요청 스키마 CreateChannelRequest: { type: 'object', From 1d6c50a9d7b4b77c9c55657620e01a3d033007b8 Mon Sep 17 00:00:00 2001 From: Hyein Jeong Date: Wed, 13 Nov 2024 23:25:54 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[BE][Refactor]=20swagger=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20tag=20=EC=B6=94=EA=B0=80,=20jsdoc=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - channel api에 대한 swagger 문서 tag 추가 - controller, service, repository에 대한 jsdoc 추가 - index.js 불필요한 코드 제거 --- backend/src/controllers/channelController.js | 15 +++++++++ backend/src/index.js | 17 ----------- backend/src/routes/channelRouter.js | 5 +++ backend/src/services/channelService.js | 32 ++++++++++++++++++++ 4 files changed, 52 insertions(+), 17 deletions(-) diff --git a/backend/src/controllers/channelController.js b/backend/src/controllers/channelController.js index 664d21b6..7dbf16ff 100644 --- a/backend/src/controllers/channelController.js +++ b/backend/src/controllers/channelController.js @@ -6,6 +6,9 @@ import { getUserChannels, } from '../services/channelService.js'; +/** + * @description 채널 생성 컨트롤러 + */ export const createChannelController = async (req, res) => { try { const { name, host_id, guests } = req.body; @@ -26,6 +29,9 @@ export const createChannelController = async (req, res) => { } }; +/** + * @description 채널에 게스트 추가 컨트롤러 + */ export const addGuestController = async (req, res) => { try { const { channelId } = req.params; @@ -53,6 +59,9 @@ export const addGuestController = async (req, res) => { } }; +/** + * @description 채널 정보 조회 컨트롤러 + */ export const getChannelInfoController = async (req, res) => { const { id } = req.params; @@ -72,6 +81,9 @@ export const getChannelInfoController = async (req, res) => { } }; +/** + * @description 채널에 특정 게스트 정보 조회 컨트롤러 + */ export const getChannelGuestInfoController = async (req, res) => { const { channelId, guestId } = req.params; try { @@ -91,6 +103,9 @@ export const getChannelGuestInfoController = async (req, res) => { } }; +/** + * @description 사용자의 채널 리스트 조회 컨트롤러 + */ export const getUserChannelsController = async (req, res) => { const { userId } = req.params; diff --git a/backend/src/index.js b/backend/src/index.js index 6e822161..355e0673 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -2,7 +2,6 @@ import express from 'express'; import swaggerUi from 'swagger-ui-express'; import http from 'http'; import { specs } from '../swaggerConfig.js'; -import { pool } from './db/db.js'; import { PORT } from './constants/constants.js'; import { initializeWebSocketServer } from './websocketServer.js'; import { authRouter } from './routes/authRouter.js'; @@ -16,22 +15,6 @@ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs)); app.use('/api/auth', authRouter); app.use('/api/channel', channelRouter); -// TODO: 데이터베이스에서 데이터 가져오기 예시 -app.get('/guests', async (req, res) => { - try { - const result = await pool.query('SELECT * FROM guest'); - res.json(result.rows); - } catch (err) { - console.error(err); - res.status(500).send('서버 오류'); - } -}); - -// TODO: 예제 라우터 (추가 예정인 라우터의 주석을 Swagger 주석 형식으로 문서화) -app.get('/example', (req, res) => { - res.send('Hello World'); -}); - // HTTP 서버 생성 const server = http.createServer(app); diff --git a/backend/src/routes/channelRouter.js b/backend/src/routes/channelRouter.js index 069aeb81..67b1dcf1 100644 --- a/backend/src/routes/channelRouter.js +++ b/backend/src/routes/channelRouter.js @@ -19,6 +19,7 @@ export const channelRouter = express.Router(); * post: * summary: '새로운 채널 생성 API' * description: '채널 이름, 주인, 게스트 정보를 포함하여 채널을 생성합니다.' + * tags: [Channel] * requestBody: * required: true * content: @@ -51,6 +52,7 @@ channelRouter.post( * post: * summary: '게스트 추가 API' * description: '특정 채널에 게스트를 추가합니다.' + * tags: [Channel] * parameters: * - name: 'channelId' * in: 'path' @@ -87,6 +89,7 @@ channelRouter.post( * get: * summary: '채널 정보 조회 API' * description: '특정 채널의 정보를 조회합니다.' + * tags: [Channel] * parameters: * - name: 'id' * in: 'path' @@ -117,6 +120,7 @@ channelRouter.get( * get: * summary: '게스트 정보 조회 API' * description: '특정 채널 내의 게스트 정보를 조회합니다.' + * tags: [Channel] * parameters: * - name: 'channelId' * in: 'path' @@ -155,6 +159,7 @@ channelRouter.get( * get: * summary: '사용자가 host인 채널 목록 반환 API' * description: 'userId를 기준으로 해당 사용자가 host인 채널 목록을 반환합니다.' + * tags: [Channel] * parameters: * - in: path * name: userId diff --git a/backend/src/services/channelService.js b/backend/src/services/channelService.js index 7f48ea49..ee333e96 100644 --- a/backend/src/services/channelService.js +++ b/backend/src/services/channelService.js @@ -7,6 +7,13 @@ import { } from '../repositories/channelRepository.js'; import { addGuestToChannel } from '../repositories/guestRepository.js'; +/** + * @description 새로운 채널을 생성하고 게스트를 추가 + * @param {string} name - 채널 이름 + * @param {string} host_id - 채널 호스트 ID + * @param {Array} guests - 추가할 게스트 목록 + * @returns {object} 생성된 채널 정보 + */ export const createChannelService = async (name, host_id, guests) => { const channel = await createChannelInDB(name, host_id); @@ -28,6 +35,12 @@ export const createChannelService = async (name, host_id, guests) => { return channel; }; +/** + * @description 특정 채널에 게스트 추가 + * @param {string} channelId - 채널 ID + * @param {Array} guests - 추가할 게스트 목록 + * @returns {object|null} 채널 정보 (채널이 없을 경우 null 반환) + */ export const addGuestService = async (channelId, guests) => { const channel = await getChannelInfoByIdInDB(channelId); if (!channel) return null; @@ -50,6 +63,12 @@ export const addGuestService = async (channelId, guests) => { return channel; }; +/** + * @description 채널 ID로 채널과 게스트 정보를 조회 + * @param {string} id - 채널 ID + * @returns {object} 채널과 게스트 정보 + * @throws {Error} 채널 조회 실패 시 오류 발생 + */ export const getChannelByIdService = async id => { try { return await getChannelWithGuestsByIdFromDB(id); @@ -59,6 +78,13 @@ export const getChannelByIdService = async id => { } }; +/** + * @description 채널 ID와 게스트 ID로 특정 게스트 정보를 조회 + * @param {string} channelId - 채널 ID + * @param {string} guestId - 게스트 ID + * @returns {object|null} 채널과 해당 게스트 정보 (채널 또는 게스트가 없을 경우 null 반환) + * @throws {Error} 조회 실패 시 오류 발생 + */ export const getChannelGuestInfoService = async (channelId, guestId) => { try { return await getGuestByChannelAndGuestIdFromDB(channelId, guestId); @@ -68,6 +94,12 @@ export const getChannelGuestInfoService = async (channelId, guestId) => { } }; +/** + * @description 사용자 ID로 해당 사용자가 호스트인 채널 목록을 조회 + * @param {string} userId - 사용자 ID + * @returns {Array} 사용자가 호스트인 채널 목록 + * @throws {Error} 채널 조회 실패 시 오류 발생 + */ export const getUserChannels = async userId => { try { return await getChannelsByUserIdFromDB(userId);