diff --git a/backend/package.json b/backend/package.json index 00196de4..7de15a91 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,34 +1,34 @@ { - "name": "ddara-backend", - "private": true, - "workspaces": [ - "frontend", - "backend" - ], - "version": "0.0.0", - "type": "module", - "description": "따라따라의 선따라길따라 BackEnd 코드", - "main": "index.js", - "scripts": { - "dev": "node src/index.js", - "test": "vitest", - "test:watch": "vitest --watch", - "test:coverage": "vitest run --coverage", - "lint": "pnpm lint-staged" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "bcrypt": "^5.1.1", - "dotenv": "^16.4.5", - "express": "^4.21.1", - "express-validator": "^7.2.0", - "jsonwebtoken": "^9.0.2", - "pg": "^8.13.1", - "swagger-jsdoc": "^6.2.8", - "swagger-ui-express": "^5.0.1", - "uuid": "^11.0.3", - "ws": "^8.11.0" - } - } \ No newline at end of file + "name": "ddara-backend", + "private": true, + "workspaces": [ + "frontend", + "backend" + ], + "version": "0.0.0", + "type": "module", + "description": "따라따라의 선따라길따라 BackEnd 코드", + "main": "index.js", + "scripts": { + "dev": "node src/index.js", + "test": "vitest", + "test:watch": "vitest --watch", + "test:coverage": "vitest run --coverage", + "lint": "pnpm lint-staged" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "bcrypt": "^5.1.1", + "dotenv": "^16.4.5", + "express": "^4.21.1", + "express-validator": "^7.2.0", + "jsonwebtoken": "^9.0.2", + "pg": "^8.13.1", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "uuid": "^11.0.3", + "ws": "^8.11.0" + } +} diff --git a/backend/src/controllers/authController.js b/backend/src/controllers/authController.js index 6739eee0..a2649e12 100644 --- a/backend/src/controllers/authController.js +++ b/backend/src/controllers/authController.js @@ -1,16 +1,44 @@ -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 { const token = await loginUser(id, password); if (!token) { - return res.status(401).json({ message: 'Invalid ID or password' }); + return res.status(401).json({ success: false, message: 'Invalid ID or password' }); } - return res.status(200).json({ token }); + return res.status(201).json({ + success: true, + message: 'Login successfully', + data: token, + }); } catch (error) { console.error('Login error:', error); - return res.status(500).json({ message: 'Server error occurred' }); + 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/controllers/channelController.js b/backend/src/controllers/channelController.js new file mode 100644 index 00000000..7dbf16ff --- /dev/null +++ b/backend/src/controllers/channelController.js @@ -0,0 +1,126 @@ +import { + addGuestService, + createChannelService, + getChannelByIdService, + getChannelGuestInfoService, + getUserChannels, +} from '../services/channelService.js'; + +/** + * @description 채널 생성 컨트롤러 + */ +export const createChannelController = async (req, res) => { + try { + const { name, host_id, guests } = req.body; + + const channel = await createChannelService(name, host_id, guests); + + return res.status(201).json({ + success: true, + message: 'Channel created successfully', + data: channel, + }); + } catch (err) { + console.error(err); + return res.status(500).json({ + success: false, + message: 'Server error', + }); + } +}; + +/** + * @description 채널에 게스트 추가 컨트롤러 + */ +export const addGuestController = async (req, res) => { + try { + const { channelId } = req.params; + const { guests } = req.body; + + const updatedChannel = await addGuestService(channelId, guests); + + if (!updatedChannel) { + return res.status(404).json({ + success: false, + message: 'Channel not found', + }); + } + + return res.status(200).json({ + success: true, + message: 'Guests added successfully', + }); + } catch (err) { + console.error(err); + return res.status(500).json({ + success: false, + message: 'Server error', + }); + } +}; + +/** + * @description 채널 정보 조회 컨트롤러 + */ +export const getChannelInfoController = async (req, res) => { + const { id } = req.params; + + try { + const channel = await getChannelByIdService(id); + if (!channel) { + return res.status(404).json({ success: false, message: 'Channel not found' }); + } + return res.status(200).json({ + success: true, + message: 'Get channel successfully', + data: channel, + }); + } catch (err) { + console.error(err); + return res.status(500).json({ success: false, message: 'Server error' }); + } +}; + +/** + * @description 채널에 특정 게스트 정보 조회 컨트롤러 + */ +export const getChannelGuestInfoController = async (req, res) => { + const { channelId, guestId } = req.params; + try { + const result = await getChannelGuestInfoService(channelId, guestId); + if (result) { + res.status(200).json({ + success: true, + message: 'Get guest data successfully', + data: result, + }); + } else { + res.status(404).json({ success: false, message: 'Channel or guest not found' }); + } + } catch (error) { + console.error(error); + res.status(500).json({ success: false, message: 'Server error' }); + } +}; + +/** + * @description 사용자의 채널 리스트 조회 컨트롤러 + */ +export const getUserChannelsController = async (req, res) => { + const { userId } = req.params; + + try { + const channels = await getUserChannels(userId); + if (!channels.length) { + return res.status(404).json({ success: false, message: 'No channels found for this user.' }); + } + res.status(200).json({ + success: true, + message: 'Get channels successfully', + data: channels, + }); + } catch (error) { + console.error(error); + return res.status(500).json({ success: false, message: 'Server error' }); + } +}; diff --git a/backend/src/index.js b/backend/src/index.js index 0f772826..355e0673 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -2,10 +2,10 @@ 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'; +import { channelRouter } from './routes/channelRouter.js'; const app = express(); app.use(express.json()); @@ -13,22 +13,7 @@ app.use(express.json()); app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs)); app.use('/api/auth', authRouter); - -// 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'); -}); +app.use('/api/channel', channelRouter); // HTTP 서버 생성 const server = http.createServer(app); diff --git a/backend/src/repositories/channelRepository.js b/backend/src/repositories/channelRepository.js new file mode 100644 index 00000000..e24a272f --- /dev/null +++ b/backend/src/repositories/channelRepository.js @@ -0,0 +1,148 @@ +import { v4 as uuidv4 } from 'uuid'; +import { pool } from '../db/db.js'; + +export const createChannelInDB = async (name, host_id) => { + const id = uuidv4(); + const query = ` + INSERT INTO "main"."channel" (id, name, host_id) + VALUES ($1, $2, $3) + RETURNING *; + `; + const values = [id, name, host_id]; + const result = await pool.query(query, values); + + return result.rows[0]; +}; + +export const getChannelInfoByIdInDB = async channelId => { + const query = ` + SELECT * + FROM "main"."channel" + WHERE id = $1; + `; + const values = [channelId]; + const result = await pool.query(query, values); + + if (result.rows.length === 0) { + return null; + } + + return result.rows[0]; +}; + +export const getChannelWithGuestsByIdFromDB = async id => { + try { + const channelQuery = 'SELECT * FROM "main"."channel" WHERE id = $1'; + const guestQuery = 'SELECT * FROM "main"."guest" WHERE channel_id = $1'; + + const channelResult = await pool.query(channelQuery, [id]); + if (channelResult.rows.length === 0) { + return null; + } + + const channel = channelResult.rows[0]; + + const guestResult = await pool.query(guestQuery, [id]); + + const guests = guestResult.rows; + + return { + id: channel.id, + name: channel.name, + host_id: channel.host_id, + guests: guests.map(guest => ({ + id: guest.id, + name: guest.name, + start_location: { + lat: guest.start_location.lat, + lng: guest.start_location.lng, + }, + end_location: { + lat: guest.end_location.lat, + lng: guest.end_location.lng, + }, + path: guest.path, + marker_style: guest.marker_style, + })), + }; + } catch (error) { + console.error('Database error:', error); + throw error; + } +}; + +export const getGuestByChannelAndGuestIdFromDB = async (channelId, guestId) => { + try { + const channelQuery = ` + SELECT * + FROM "main"."channel" + WHERE id = $1; + `; + const channelResult = await pool.query(channelQuery, [channelId]); + if (channelResult.rows.length === 0) { + return null; + } + + const guestQuery = ` + SELECT * + FROM "main"."guest" + WHERE channel_id = $1 + AND id = $2; + `; + const guestResult = await pool.query(guestQuery, [channelId, guestId]); + if (guestResult.rows.length === 0) { + return null; + } + + const channel = channelResult.rows[0]; + const guest = guestResult.rows[0]; + + return { + id: channel.id, + name: channel.name, + host_id: channel.host_id, + guest: { + id: guest.id, + name: guest.name, + start_location: { + lat: guest.start_location.lat, + lng: guest.start_location.lng, + }, + end_location: { + lat: guest.end_location.lat, + lng: guest.end_location.lng, + }, + path: guest.path, + marker_style: guest.marker_style, + }, + }; + } catch (error) { + console.error('Database error:', error); + throw error; + } +}; + +export const getChannelsByUserIdFromDB = async userId => { + try { + const query = ` + SELECT id, name, generated_at + FROM "main"."channel" + WHERE host_id = $1; + `; + const values = [userId]; + const result = await pool.query(query, values); + + if (result.rows.length === 0) { + return []; + } + + return result.rows.map(channel => ({ + id: channel.id, + name: channel.name, + generated_at: channel.generated_at, + })); + } catch (error) { + console.error('Database error:', error); + throw error; + } +}; diff --git a/backend/src/repositories/guestRepository.js b/backend/src/repositories/guestRepository.js new file mode 100644 index 00000000..35e8c7d5 --- /dev/null +++ b/backend/src/repositories/guestRepository.js @@ -0,0 +1,32 @@ +import { v4 as uuidv4 } from 'uuid'; +import { pool } from '../db/db.js'; + +export const addGuestToChannel = async ( + channel_id, + name, + start_location, + end_location, + path, + marker_style, + host_id, +) => { + const guest_id = uuidv4(); + const query = ` + INSERT INTO "main"."guest" (id, channel_id, name, start_location, end_location, path, marker_style, host_id) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING *; + `; + const values = [ + guest_id, + channel_id, + name, + start_location, + end_location, + path, + marker_style, + host_id, + ]; + const result = await pool.query(query, values); + + return result.rows[0]; +}; 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 caf0b954..5c81901c 100644 --- a/backend/src/routes/authRouter.js +++ b/backend/src/routes/authRouter.js @@ -1,10 +1,38 @@ 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(); +/** + * @swagger + * /auth/login: + * post: + * summary: 사용자 로그인 API + * description: 사용자가 로그인할 수 있도록 ID와 비밀번호를 통해 인증 후 토큰을 반환합니다. + * tags: [Auth] + * requestBody: + * required: true + * description: 로그인을 위한 ID와 비밀번호를 포함한 요청 body + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/LoginRequest' + * responses: + * 200: + * description: 로그인 성공, 토큰 및 사용자 정보 반환 + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/LoginResponse' + * 400: + * description: 잘못된 요청, 필수 정보 누락 또는 형식 오류 + * 401: + * description: 잘못된 ID나 비밀번호 + * 500: + * description: 서버 에러 + */ authRouter.post( '/login', [ @@ -14,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/routes/channelRouter.js b/backend/src/routes/channelRouter.js new file mode 100644 index 00000000..67b1dcf1 --- /dev/null +++ b/backend/src/routes/channelRouter.js @@ -0,0 +1,187 @@ +import express from 'express'; +import { body, param } from 'express-validator'; +import { + addGuestController, + createChannelController, + getChannelGuestInfoController, + getChannelInfoController, + getUserChannelsController, +} from '../controllers/channelController.js'; +import { validationMiddleware } from '../middleware/validationMiddleware.js'; + +export const channelRouter = express.Router(); + +// 채널 생성 API 경로 +/** + * @swagger + * paths: + * /channel: + * post: + * summary: '새로운 채널 생성 API' + * description: '채널 이름, 주인, 게스트 정보를 포함하여 채널을 생성합니다.' + * tags: [Channel] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CreateChannelRequest' + * responses: + * 201: + * description: '채널 생성 성공' + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CreateChannelResponse' + */ +channelRouter.post( + '/', + [ + body('name').notEmpty().withMessage('Name is required'), + body('host_id').notEmpty().withMessage('Host ID is required'), + ], + validationMiddleware, + createChannelController, +); + +// 게스트 추가 API 경로 +/** + * @swagger + * paths: + * /channel/{channelId}/guests: + * post: + * summary: '게스트 추가 API' + * description: '특정 채널에 게스트를 추가합니다.' + * tags: [Channel] + * parameters: + * - name: 'channelId' + * in: 'path' + * required: true + * schema: + * type: 'string' + * description: '채널의 고유 ID' + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AddGuestRequest' + * responses: + * 200: + * description: '게스트 추가 성공' + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AddGuestResponse' + */ +channelRouter.post( + '/:channelId/guests', + [body('guests').isArray().withMessage('Guests must be an array')], + validationMiddleware, + addGuestController, +); + +// 채널 정보 조회 API 경로 +/** + * @swagger + * paths: + * /channel/{id}: + * get: + * summary: '채널 정보 조회 API' + * description: '특정 채널의 정보를 조회합니다.' + * tags: [Channel] + * parameters: + * - name: 'id' + * in: 'path' + * required: true + * schema: + * type: 'string' + * description: '채널의 고유 ID' + * responses: + * 200: + * description: '채널 정보 조회 성공' + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ChannelResponse' + */ +channelRouter.get( + '/:id', + [param('id').notEmpty().withMessage('Channel ID is required')], + validationMiddleware, + getChannelInfoController, +); + +// 게스트 정보 조회 API 경로 +/** + * @swagger + * paths: + * /channel/{channelId}/guest/{guestId}: + * get: + * summary: '게스트 정보 조회 API' + * description: '특정 채널 내의 게스트 정보를 조회합니다.' + * tags: [Channel] + * parameters: + * - name: 'channelId' + * in: 'path' + * required: true + * schema: + * type: 'string' + * description: '채널의 고유 ID' + * - name: 'guestId' + * in: 'path' + * required: true + * schema: + * type: 'string' + * description: '게스트의 고유 ID' + * responses: + * 200: + * description: '게스트 정보 조회 성공' + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/GuestResponse' + */ +channelRouter.get( + '/:channelId/guest/:guestId', + [ + param('channelId').notEmpty().withMessage('Channel ID is required'), + param('guestId').notEmpty().withMessage('Guest ID is required'), + ], + validationMiddleware, + getChannelGuestInfoController, +); + +/** + * @swagger + * paths: + * /channel/user/{userId}: + * get: + * summary: '사용자가 host인 채널 목록 반환 API' + * description: 'userId를 기준으로 해당 사용자가 host인 채널 목록을 반환합니다.' + * tags: [Channel] + * parameters: + * - in: path + * name: userId + * required: true + * description: '사용자의 ID' + * schema: + * type: string + * responses: + * 200: + * description: '채널 목록 반환 성공' + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/GetUserChannelsResponse' + * 404: + * description: '해당 사용자가 host인 채널이 없음' + * 500: + * description: '서버 오류' + */ +channelRouter.get( + '/user/:userId', + [param('userId').notEmpty().withMessage('User ID is required')], + validationMiddleware, + getUserChannelsController, +); diff --git a/backend/src/routes/example.js b/backend/src/routes/example.js deleted file mode 100644 index e9168225..00000000 --- a/backend/src/routes/example.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * @swagger - * /example: - * get: - * summary: Example endpoint - * description: Returns a simple "Hello World" message - * responses: - * 200: - * description: Successful response - */ -app.get('/example', (req, res) => { - res.send('Hello World'); -}); 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/src/services/channelService.js b/backend/src/services/channelService.js new file mode 100644 index 00000000..ee333e96 --- /dev/null +++ b/backend/src/services/channelService.js @@ -0,0 +1,109 @@ +import { + createChannelInDB, + getChannelInfoByIdInDB, + getChannelsByUserIdFromDB, + getChannelWithGuestsByIdFromDB, + getGuestByChannelAndGuestIdFromDB, +} 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); + + const guestPromises = guests.map(guest => { + const { name, start_location, end_location, path, marker_style } = guest; + return addGuestToChannel( + channel.id, + name, + start_location, + end_location, + path, + marker_style, + host_id, + ); + }); + + await Promise.all(guestPromises); + + 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; + + const guestPromises = guests.map(guest => { + const { name, start_location, end_location, path, marker_style } = guest; + return addGuestToChannel( + channelId, + name, + start_location, + end_location, + path, + marker_style, + channel.host_id, + ); + }); + + await Promise.all(guestPromises); + + return channel; +}; + +/** + * @description 채널 ID로 채널과 게스트 정보를 조회 + * @param {string} id - 채널 ID + * @returns {object} 채널과 게스트 정보 + * @throws {Error} 채널 조회 실패 시 오류 발생 + */ +export const getChannelByIdService = async id => { + try { + return await getChannelWithGuestsByIdFromDB(id); + } catch (error) { + console.error('Error fetching channel:', error); + throw error; + } +}; + +/** + * @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); + } catch (error) { + console.error('Error fetching channel:', error); + throw error; + } +}; + +/** + * @description 사용자 ID로 해당 사용자가 호스트인 채널 목록을 조회 + * @param {string} userId - 사용자 ID + * @returns {Array} 사용자가 호스트인 채널 목록 + * @throws {Error} 채널 조회 실패 시 오류 발생 + */ +export const getUserChannels = async userId => { + try { + return await getChannelsByUserIdFromDB(userId); + } catch (error) { + throw new Error('Failed to fetch channels', error); + } +}; diff --git a/backend/swaggerConfig.js b/backend/swaggerConfig.js index 09ae4746..ec6ed9ad 100644 --- a/backend/swaggerConfig.js +++ b/backend/swaggerConfig.js @@ -3,20 +3,458 @@ import swaggerJSDoc from 'swagger-jsdoc'; const swaggerDefinition = { openapi: '3.0.0', info: { - title: 'Your API Name', + title: 'DDara API', version: '1.0.0', - description: 'API documentation for Your Project', + description: 'API documentation for DDara Project', }, servers: [ { - url: 'http://localhost:3001', + url: 'http://localhost:3001/api', }, ], + components: { + schemas: { + // 로그인 요청 스키마 + LoginRequest: { + type: 'object', + properties: { + id: { + type: 'string', + description: '사용자 ID', + }, + password: { + type: 'string', + description: '사용자 비밀번호', + }, + }, + }, + // 로그인 응답 스키마 + LoginResponse: { + type: 'object', + properties: { + token: { + type: 'string', + description: '인증 토큰', + }, + userId: { + type: 'string', + description: '사용자 ID', + }, + }, + }, + + // 회원가입 요청 스키마 + 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', + properties: { + name: { + type: 'string', + description: '채널 이름', + }, + host_id: { + type: 'string', + description: '채널의 호스트 ID (user 테이블의 ID)', + }, + guests: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + description: '게스트의 이름', + }, + start_location: { + type: 'object', + properties: { + lat: { + type: 'number', + description: '출발지 마커의 위도', + }, + lng: { + type: 'number', + description: '출발지 마커의 경도', + }, + }, + }, + end_location: { + type: 'object', + properties: { + lat: { + type: 'number', + description: '도착지 마커의 위도', + }, + lng: { + type: 'number', + description: '도착지 마커의 경도', + }, + }, + }, + path: { + type: 'array', + items: { + type: 'object', + properties: { + lat: { + type: 'number', + description: '경로 n번째 지점의 위도', + }, + lng: { + type: 'number', + description: '경로 n번째 지점의 경도', + }, + }, + }, + description: '해당 사용자의 경로 (위도, 경도)를 담은 배열', + }, + marker_style: { + type: 'object', + properties: { + color: { + type: 'string', + description: '사용자 구분 색상 스타일', + }, + }, + }, + }, + }, + description: '채널의 사용자 object를 담은 배열', + }, + }, + }, + + // 채널 생성 응답 스키마 + CreateChannelResponse: { + type: 'object', + properties: { + id: { + type: 'string', + description: '생성된 채널의 고유 ID (UUID 형태)', + }, + name: { + type: 'string', + description: '생성된 채널의 이름', + }, + host_id: { + type: 'string', + description: '생성된 채널의 host ID (user 테이블의 ID)', + }, + guests: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + description: '생성된 채널의 guest ID (UUID 타입)', + }, + name: { + type: 'string', + description: '생성된 채널의 guest 이름', + }, + }, + }, + description: '생성된 채널의 guest 배열', + }, + created_at: { + type: 'string', + format: 'date-time', + description: '채널 생성 시각', + }, + }, + }, + // 게스트 추가 요청 스키마 + AddGuestRequest: { + type: 'object', + properties: { + channel_id: { + type: 'string', + description: '채널 ID (UUID 형태)', + }, + guest: { + type: 'object', + properties: { + name: { + type: 'string', + description: '게스트의 이름', + }, + start_location: { + type: 'object', + properties: { + lat: { + type: 'number', + description: '출발지 마커의 위도', + }, + lng: { + type: 'number', + description: '출발지 마커의 경도', + }, + }, + }, + end_location: { + type: 'object', + properties: { + lat: { + type: 'number', + description: '도착지 마커의 위도', + }, + lng: { + type: 'number', + description: '도착지 마커의 경도', + }, + }, + }, + path: { + type: 'array', + items: { + type: 'object', + properties: { + lat: { + type: 'number', + description: '경로 n번째 지점의 위도', + }, + lng: { + type: 'number', + description: '경로 n번째 지점의 경도', + }, + }, + }, + description: '게스트의 경로 (위도, 경도)를 담은 배열', + }, + marker_style: { + type: 'object', + properties: { + color: { + type: 'string', + description: '게스트를 구분하는 색상 스타일', + }, + }, + }, + }, + }, + }, + }, + + // 게스트 추가 응답 스키마 + AddGuestResponse: { + type: 'object', + properties: { + success: { + type: 'boolean', + description: '요청이 성공적으로 처리되었는지 여부', + }, + message: { + type: 'string', + description: '응답 메시지', + }, + }, + }, + + // 채널 정보 가져오기 응답 스키마 + ChannelResponse: { + type: 'object', + properties: { + id: { + type: 'string', + description: '채널 ID (UUID 형태)', + }, + name: { + type: 'string', + description: '채널 이름', + }, + host_id: { + type: 'string', + description: '채널의 호스트 ID', + }, + guests: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + description: '게스트 ID (UUID 형태)', + }, + name: { + type: 'string', + description: '게스트 이름', + }, + start_location: { + type: 'object', + properties: { + lat: { + type: 'number', + description: '출발지 마커의 위도', + }, + lng: { + type: 'number', + description: '출발지 마커의 경도', + }, + }, + }, + end_location: { + type: 'object', + properties: { + lat: { + type: 'number', + description: '도착지 마커의 위도', + }, + lng: { + type: 'number', + description: '도착지 마커의 경도', + }, + }, + }, + path: { + type: 'array', + items: { + type: 'object', + properties: { + lat: { + type: 'number', + description: '경로 n번째 지점의 위도', + }, + lng: { + type: 'number', + description: '경로 n번째 지점의 경도', + }, + }, + }, + description: '게스트의 경로를 나타내는 배열', + }, + marker_style: { + type: 'object', + properties: { + color: { + type: 'string', + description: '게스트를 구분하는 색상 스타일', + }, + }, + description: '게스트 마커의 스타일 정보', + }, + }, + }, + description: '해당 채널의 게스트 목록', + }, + }, + }, + + // 특정 게스트의 정보 가져오기 응답 스키마 + GuestResponse: { + type: 'object', + properties: { + id: { + type: 'string', + description: '게스트의 ID (UUID 형식)', + }, + name: { + type: 'string', + description: '게스트 이름', + }, + start_location: { + type: 'object', + properties: { + lat: { + type: 'number', + description: '출발지 마커의 위도', + }, + lng: { + type: 'number', + description: '출발지 마커의 경도', + }, + }, + description: '출발 위치 정보', + }, + end_location: { + type: 'object', + properties: { + lat: { + type: 'number', + description: '도착지 마커의 위도', + }, + lng: { + type: 'number', + description: '도착지 마커의 경도', + }, + }, + description: '도착 위치 정보', + }, + path: { + type: 'array', + items: { + type: 'object', + properties: { + lat: { + type: 'number', + description: '경로 지점의 위도', + }, + lng: { + type: 'number', + description: '경로 지점의 경도', + }, + }, + }, + description: '게스트의 경로 (위도, 경도)를 담은 배열', + }, + marker_style: { + type: 'object', + properties: { + color: { + type: 'string', + description: '게스트를 구분하는 색상 스타일', + }, + }, + description: '마커 스타일 정보', + }, + }, + description: '특정 게스트의 정보', + }, + + // 사용자별 채널 가져오기 응답 스키마 + GetUserChannelsResponse: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'string' }, + name: { type: 'string' }, + generated_at: { type: 'string', format: 'date-time' }, + }, + required: ['id', 'name', 'generated_at'], + }, + }, + }, + }, }; const options = { swaggerDefinition, - apis: ['./routes/*.js'], + apis: ['./src/routes/**/*.js'], }; export const specs = swaggerJSDoc(options); diff --git a/eslint.config.mjs b/eslint.config.mjs index 7caf1e11..0e0dbab0 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -113,6 +113,8 @@ export default [ 'no-console': 'off', 'consistent-return': 'off', 'import/extensions': 'off', + camelcase: 'off', + 'no-shadow': 'off', }, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5414d6c8..83dce576 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -132,6 +132,9 @@ importers: swagger-ui-express: specifier: ^5.0.1 version: 5.0.1(express@4.21.1) + uuid: + specifier: ^11.0.3 + version: 11.0.3 ws: specifier: ^8.11.0 version: 8.11.0 @@ -7404,6 +7407,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@11.0.3: + resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -16718,6 +16725,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@11.0.3: {} + uuid@8.3.2: {} uuid@9.0.1: {}