diff --git a/apps/server/package.json b/apps/server/package.json index 80ce6a64..1b4f5812 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -22,6 +22,7 @@ "dotenv": "^16.4.5", "express": "^4.21.2", "express-asyncify": "^2.1.2", + "express-cargo": "^0.4.0", "express-winston": "^4.2.0", "firebase-admin": "^13.4.0", "helmet": "^8.0.0", @@ -33,6 +34,7 @@ "mongoose-paginate-v2": "^1.8.5", "node-fetch": "^2.7.0", "node-schedule": "^2.1.1", + "reflect-metadata": "^0.2.2", "winston": "^3.16.0", "winston-daily-rotate-file": "^5.0.0" }, diff --git a/apps/server/src/controllers/v1/enquiries.ts b/apps/server/src/controllers/v1/enquiries.ts index ac5cebac..76b16c40 100644 --- a/apps/server/src/controllers/v1/enquiries.ts +++ b/apps/server/src/controllers/v1/enquiries.ts @@ -1,16 +1,20 @@ import express, { Request, Response, Router } from 'express' import asyncify from 'express-asyncify' +import { bindingCargo, getCargo } from 'express-cargo' import { EnquiryModel } from '@/models' import { verifyToken } from '@/middlewares/auth' +import { PostEnquiryPayload } from '@/types/payload/enquiry' const router: Router = asyncify(express.Router()) -router.post('/', verifyToken, async (req: Request, res: Response) => { +router.post('/', verifyToken, bindingCargo(PostEnquiryPayload), async (req: Request, res: Response) => { + const { title, body } = getCargo(req) + const enquiry = await EnquiryModel.create({ userId: req.user._id, - title: req.body.title, - body: req.body.body, + title, + body, }) res.status(200).json({ enquiryId: enquiry._id, diff --git a/apps/server/src/controllers/v1/events.ts b/apps/server/src/controllers/v1/events.ts index 49f3aa8e..f93fb8c4 100644 --- a/apps/server/src/controllers/v1/events.ts +++ b/apps/server/src/controllers/v1/events.ts @@ -1,13 +1,30 @@ import express, { Request, Response, Router } from 'express' import asyncify from 'express-asyncify' +import { bindingCargo, getCargo } from 'express-cargo' import mongoose from 'mongoose' +import { CategoryCode, NotificationType } from '@fienmee/types' import { CategoryModel, Events, EventsModel, ReviewsModel, CommentsModel, NotificationModel } from '@/models' import { verifyToken } from '@/middlewares/auth' -import { CategoryCode, NotificationType } from '@fienmee/types' import { verifyCommentAuthor, verifyEventAuthor } from '@/middlewares/events' import { TransactionError } from '@/types/errors/database' import { CommentNotFound, EventNotFound, InvaildDate, KeywordIsEmptyToSearch } from '@/types/errors/events' +import { + EventCommentIdPayload, + EventIdPayload, + GetEventCategoryPayload, + GetEventCommentPayload, + GetEventDateCategoryPayload, + GetEventHotCategoryPayload, + GetEventInterestCategoryPayload, + GetEventReviewPayload, + GetEventSearchPayload, + PostEventCommentPayload, + PostEventPayload, + PostEventReviewPayload, + PutEventCommentPayload, + PutEventPayload, +} from '@/types/payload' const router: Router = asyncify(express.Router()) @@ -27,34 +44,18 @@ router.get('/categories', verifyToken, async (req: Request, res: Response) => { }) }) -router.post('/', verifyToken, async (req: Request, res: Response) => { +router.post('/', verifyToken, bindingCargo(PostEventPayload), async (req: Request, res: Response) => { + const cargo = getCargo(req) + await EventsModel.create({ - name: req.body.name, + ...cargo, authorId: req.user._id, - address: req.body.address, - location: req.body.location, - startDate: req.body.startDate, - endDate: req.body.endDate, - photo: req.body.photo, - cost: req.body.cost, - description: req.body.description, - category: req.body.category, - targetAudience: req.body.targetAudience, - isAllDay: req.body.isAllDay, }) res.sendStatus(204) }) -router.get('/search', async (req: Request, res: Response) => { - const { q, category } = req.query - const target = (req.query.target as string) || 'default' - const sort = req.query.sort || 'default' - const startDate = req.query.startDate ? new Date(req.query.startDate as string) : new Date() - const endDate = req.query.endDate ? new Date(req.query.endDate as string) : undefined - const isAllDay = req.query.isAllDay === 'true' - const page = Number(req.query.page) || 1 - const limit = Number(req.query.limit) || 10 - const skip = (page - 1) * limit +router.get('/search', bindingCargo(GetEventSearchPayload), async (req: Request, res: Response) => { + const { q, category, target, sort, startDate, endDate, isAllDay, limit, skip } = getCargo(req) const pathMap: Record = { default: ['name', 'description'], @@ -119,22 +120,11 @@ router.get('/search', async (req: Request, res: Response) => { res.status(200).json(result) }) -router.put('/:id', verifyToken, verifyEventAuthor, async (req: Request, res: Response) => { +router.put('/:id', verifyToken, bindingCargo(PutEventPayload), verifyEventAuthor, async (req: Request, res: Response) => { + const { id, ...updateData } = getCargo(req) const event = await EventsModel.findOneAndUpdate( - { _id: req.params.id }, - { - name: req.body.name, - address: req.body.address, - location: req.body.location, - startDate: req.body.date, - endDate: req.body.endDate, - photo: req.body.photo, - cost: req.body.cost, - description: req.body.description, - category: req.body.category, - targetAudience: req.body.targetAudience, - isAllDay: req.body.isAllDay, - }, + { _id: id }, + updateData, { returnDocument: 'after' }, ) res.status(200).json({ @@ -144,12 +134,13 @@ router.put('/:id', verifyToken, verifyEventAuthor, async (req: Request, res: Res }) }) -router.delete('/:id', verifyToken, verifyEventAuthor, async (req: Request, res: Response) => { +router.delete('/:id', verifyToken, bindingCargo(EventIdPayload), verifyEventAuthor, async (req: Request, res: Response) => { + const { id } = getCargo(req) const session = await mongoose.startSession() try { session.startTransaction() - await EventsModel.deleteOne({ _id: req.params.id }, { session }) - await CommentsModel.deleteMany({ eventId: req.params.id }, { session }) + await EventsModel.deleteOne({ _id: id }, { session }) + await CommentsModel.deleteMany({ eventId: id }, { session }) await session.commitTransaction() res.sendStatus(204) } catch (error) { @@ -160,8 +151,9 @@ router.delete('/:id', verifyToken, verifyEventAuthor, async (req: Request, res: } }) -router.get('/:id', verifyToken, async (req: Request, res: Response) => { - const event = await EventsModel.findById(req.params.id) +router.get('/:id', verifyToken, bindingCargo(EventIdPayload), async (req: Request, res: Response) => { + const { id } = getCargo(req) + const event = await EventsModel.findById(id) res.status(200).json({ ...event.toJSON(), @@ -170,14 +162,15 @@ router.get('/:id', verifyToken, async (req: Request, res: Response) => { }) }) -router.post('/:id/comments', verifyToken, async (req: Request, res: Response) => { +router.post('/:id/comments', verifyToken, bindingCargo(PostEventCommentPayload), async (req: Request, res: Response) => { + const cargo = getCargo(req) const comment = await CommentsModel.create({ userId: req.user._id, - eventId: req.params.id, - comment: req.body.comment, + eventId: cargo.id, + comment: cargo.comment, }) - const event = await EventsModel.findByIdAndUpdate(req.params.id, { $push: { comments: comment._id } }) + const event = await EventsModel.findByIdAndUpdate(cargo.id, { $push: { comments: comment._id } }) if (event.authorId && !event.authorId.equals(req.user._id)) { await NotificationModel.createAndSendNotification( NotificationType.COMMENT, @@ -191,12 +184,9 @@ router.post('/:id/comments', verifyToken, async (req: Request, res: Response) => res.sendStatus(204) }) -router.get('/:id/comments', verifyToken, async (req: Request, res: Response) => { - const options = { - page: Number(req.query.page) || 1, - limit: Number(req.query.limit) || 10, - } - const result = await CommentsModel.findByEventId(req.params.id, options) +router.get('/:id/comments', verifyToken, bindingCargo(GetEventCommentPayload), async (req: Request, res: Response) => { + const { id, page, limit } = getCargo(req) + const result = await CommentsModel.findByEventId(id, { page, limit }) const modifiedDocs = result.docs.map(comment => ({ ...comment.toObject(), isAuthor: comment.get('userId')?.equals(req.user._id), @@ -216,36 +206,44 @@ router.get('/:id/comments', verifyToken, async (req: Request, res: Response) => }) }) -router.put('/:id/comments/:commentId', verifyToken, verifyCommentAuthor, async (req: Request, res: Response) => { - await CommentsModel.updateOne( - { - _id: req.params.commentId, - }, - { - comment: req.body.comment, - }, - ) - res.sendStatus(204) -}) - -router.delete('/:id/comments/:commentId', verifyToken, verifyCommentAuthor, async (req: Request, res: Response) => { - const session = await mongoose.startSession() - try { - session.startTransaction() - await CommentsModel.deleteOne({ _id: req.params.commentId }, { session }) - await EventsModel.updateOne({ _id: req.params.id }, { $pull: { comments: new mongoose.Types.ObjectId(req.params.commentId) } }, { session }) - await session.commitTransaction() +router.put( + '/:id/comments/:commentId', + verifyToken, + bindingCargo(PutEventCommentPayload), + verifyCommentAuthor, + async (req: Request, res: Response) => { + const { commentId, comment } = getCargo(req) + await CommentsModel.updateOne({ _id: commentId }, { comment }) res.sendStatus(204) - } catch (error) { - await session.abortTransaction() - throw new TransactionError(error) - } finally { - await session.endSession() - } -}) + }, +) + +router.delete( + '/:id/comments/:commentId', + verifyToken, + bindingCargo(EventCommentIdPayload), + verifyCommentAuthor, + async (req: Request, res: Response) => { + const { id, commentId } = getCargo(req) + const session = await mongoose.startSession() + try { + session.startTransaction() + await CommentsModel.deleteOne({ _id: commentId }, { session }) + await EventsModel.updateOne({ _id: id }, { $pull: { comments: new mongoose.Types.ObjectId(commentId) } }, { session }) + await session.commitTransaction() + res.sendStatus(204) + } catch (error) { + await session.abortTransaction() + throw new TransactionError(error) + } finally { + await session.endSession() + } + }, +) -router.post('/:id/comments/:commentId/likes', verifyToken, async (req: Request, res: Response) => { - const comment = await CommentsModel.findById(req.params.commentId).populate<{ eventId: { name: string } }>({ path: 'eventId', select: 'name' }) +router.post('/:id/comments/:commentId/likes', verifyToken, bindingCargo(EventCommentIdPayload), async (req: Request, res: Response) => { + const { id, commentId } = getCargo(req) + const comment = await CommentsModel.findById(commentId).populate<{ eventId: { name: string } }>({ path: 'eventId', select: 'name' }) if (!comment) { throw new CommentNotFound() @@ -253,28 +251,30 @@ router.post('/:id/comments/:commentId/likes', verifyToken, async (req: Request, const prevLiked = comment.likes.includes(req.user._id) const updateLiked = prevLiked ? { $pull: { likes: req.user._id } } : { $push: { likes: req.user._id } } - await CommentsModel.updateOne({ _id: req.params.commentId }, updateLiked) + await CommentsModel.updateOne({ _id: commentId }, updateLiked) if (comment.userId && !prevLiked && !comment.userId.equals(req.user._id)) { await NotificationModel.createAndSendNotification( NotificationType.LIKE, comment.userId, '누군가가 내가 등록한 댓글에 좋아요를 눌렀어요!', `${comment.eventId.name} 행사 댓글에 좋아요가 눌렸어요!`, - `events:detail:${req.params.id}`, + `events:detail:${id}`, ) } res.sendStatus(204) }) -router.post('/:id/likes', verifyToken, async (req: Request, res: Response) => { - const event = await EventsModel.findById(req.params.id) +router.post('/:id/likes', verifyToken, bindingCargo(EventIdPayload), async (req: Request, res: Response) => { + const { id } = getCargo(req) + const event = await EventsModel.findById(id) + const userId = req.user._id - const prevLiked = event.likes.includes(req.user._id) - const update = prevLiked ? { $pull: { likes: req.user._id } } : { $push: { likes: req.user._id } } + const prevLiked = event.likes.includes(userId) + const update = prevLiked ? { $pull: { likes: userId } } : { $push: { likes: userId } } - await EventsModel.updateOne({ _id: req.params.id }, update) - if (event.authorId && !prevLiked && !event.authorId.equals(req.user._id)) { + await EventsModel.updateOne({ _id: id }, update) + if (event.authorId && !prevLiked && !event.authorId.equals(userId)) { await NotificationModel.createAndSendNotification( NotificationType.LIKE, event.authorId, @@ -287,14 +287,15 @@ router.post('/:id/likes', verifyToken, async (req: Request, res: Response) => { res.sendStatus(204) }) -router.post('/:id/reviews', verifyToken, async (req: Request, res: Response) => { - const event = await EventsModel.findById(req.params.id) +router.post('/:id/reviews', verifyToken, bindingCargo(PostEventReviewPayload), async (req: Request, res: Response) => { + const { id, rating, photo, body } = getCargo(req) + const event = await EventsModel.findById(id) const review = await ReviewsModel.create({ eventId: event._id, userId: req.user._id, - rating: req.body.rating, - photo: req.body.photo, - body: req.body.body, + rating, + photo, + body, }) if (event.authorId && !event.authorId.equals(req.user._id)) { @@ -311,12 +312,14 @@ router.post('/:id/reviews', verifyToken, async (req: Request, res: Response) => }) }) -router.get('/:id/reviews', verifyToken, async (req: Request, res: Response) => { - const event = await EventsModel.findById(req.params.id) +router.get('/:id/reviews', verifyToken, bindingCargo(GetEventReviewPayload), async (req: Request, res: Response) => { + const { id, page, limit } = getCargo(req) + + const event = await EventsModel.findById(id) if (!event) { throw new EventNotFound() } - const options = { sort: { createdAt: -1 }, page: Number(req.query.page) || 1, limit: Number(req.query.limit) || 10 } + const options = { sort: { createdAt: -1 }, page: page, limit: limit } const result = await ReviewsModel.findByEventId(event._id, options) res.status(200).json({ reviews: result.docs, @@ -331,15 +334,13 @@ router.get('/:id/reviews', verifyToken, async (req: Request, res: Response) => { }) }) -router.get('/category/dates', verifyToken, async (req: Request, res: Response) => { +router.get('/category/dates', verifyToken, bindingCargo(GetEventDateCategoryPayload), async (req: Request, res: Response) => { + const { from: fromString, to: toString, limit, page } = getCargo(req) const today = new Date() - const from = req.query.from ? new Date(req.query.from as string) : new Date(today.getFullYear(), today.getMonth(), today.getDate()) - const to = req.query.to ? new Date(req.query.to as string) : new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1) - const limit = req.query.limit ? Number(req.query.limit) : 0 - const page = req.query.page ? Number(req.query.page) : 1 + const from = fromString ?? new Date(today.getFullYear(), today.getMonth(), today.getDate()) + const to = toString ?? new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1) const result = await EventsModel.findByDates(from, to, limit, page) - const events = result.docs.map(event => ({ ...event.toJSON(), isAuthor: event.get('authorId')?.equals(req.user._id), @@ -349,7 +350,8 @@ router.get('/category/dates', verifyToken, async (req: Request, res: Response) = res.status(200).json({ events: events }) }) -router.get('/category/interest', verifyToken, async (req: Request, res: Response) => { +router.get('/category/interest', verifyToken, bindingCargo(GetEventInterestCategoryPayload), async (req: Request, res: Response) => { + const { limit, page } = getCargo(req) const interests = req.user.interests as unknown as string[] if (interests.length === 0) { @@ -357,7 +359,7 @@ router.get('/category/interest', verifyToken, async (req: Request, res: Response return } - const options = { sort: { startDate: 1, endDate: 1, createdAt: -1 }, page: Number(req.query.page) || 1, limit: Number(req.query.limit) || 10 } + const options = { sort: { startDate: 1, endDate: 1, createdAt: -1 }, page, limit } const result = await EventsModel.findByCategory(interests, options) const events = result.docs.map(event => ({ @@ -379,15 +381,13 @@ router.get('/category/interest', verifyToken, async (req: Request, res: Response }) }) -router.get(`/category/${CategoryCode.HOTEVENT}`, verifyToken, async (req: Request, res: Response) => { +router.get(`/category/${CategoryCode.HOTEVENT}`, verifyToken, bindingCargo(GetEventHotCategoryPayload), async (req: Request, res: Response) => { + const { from: fromString, to: toString, limit, page } = getCargo(req) const today = new Date() - const from = req.query.from ? new Date(req.query.from as string) : new Date(today.getFullYear(), today.getMonth(), today.getDate()) - const to = req.query.to ? new Date(req.query.to as string) : new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1) - const limit = req.query.limit ? Number(req.query.limit) : 3 - const page = req.query.page ? Number(req.query.page) : 1 + const from = fromString ?? new Date(today.getFullYear(), today.getMonth(), today.getDate()) + const to = toString ?? new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1) const result = await EventsModel.findHot(from, to, limit, page) - const events = result.docs.map(event => ({ ...event, isAuthor: event['authorId']?.equals(req.user._id), @@ -407,14 +407,15 @@ router.get(`/category/${CategoryCode.HOTEVENT}`, verifyToken, async (req: Reques }) }) -router.get('/category/:category', verifyToken, async (req: Request, res: Response) => { - const options = { sort: { startDate: 1, endDate: 1, createdAt: -1 }, page: Number(req.query.page) || 1, limit: Number(req.query.limit) || 10 } +router.get('/category/:category', verifyToken, bindingCargo(GetEventCategoryPayload), async (req: Request, res: Response) => { + const { category, limit, page } = getCargo(req) + const options = { sort: { startDate: 1, endDate: 1, createdAt: -1 }, page, limit } let result: mongoose.PaginateResult> - if (req.params.category === CategoryCode.MYEVENT) { + if (category === CategoryCode.MYEVENT) { result = await EventsModel.findByAuthor(req.user._id, options) } else { - result = await EventsModel.findByCategory([req.params.category], options) + result = await EventsModel.findByCategory([category], options) } const events = result.docs.map(event => ({ ...event.toJSON(), diff --git a/apps/server/src/controllers/v1/notifications.ts b/apps/server/src/controllers/v1/notifications.ts index c3ab1308..84a7f281 100644 --- a/apps/server/src/controllers/v1/notifications.ts +++ b/apps/server/src/controllers/v1/notifications.ts @@ -1,13 +1,16 @@ import express, { Router } from 'express' import asyncify from 'express-asyncify' +import { bindingCargo, getCargo } from 'express-cargo' import { NotificationModel, NotificationTokenModel } from '@/models' import { verifyToken } from '@/middlewares/auth' +import { GetNotificationPayload, PostNotificationReadPayload, PutNotificationTokenPayload } from '@/types/payload' const router: Router = asyncify(express.Router()) -router.get('/', verifyToken, async (req, res) => { - const options = { sort: { createdAt: -1 }, page: Number(req.query.page) || 1, limit: Number(req.query.limit) || 10 } +router.get('/', verifyToken, bindingCargo(GetNotificationPayload), async (req, res) => { + const { page, limit } = getCargo(req) + const options = { sort: { createdAt: -1 }, page, limit } const result = await NotificationModel.findByUserId(req.user._id, options) res.status(200).json({ notifications: result.docs, @@ -22,12 +25,13 @@ router.get('/', verifyToken, async (req, res) => { }) }) -router.put('/token', verifyToken, async (req, res) => { +router.put('/token', verifyToken, bindingCargo(PutNotificationTokenPayload), async (req, res) => { + const { deviceId, token, platform } = getCargo(req) await NotificationTokenModel.findOneAndUpdate( - { deviceId: req.body.deviceId }, + { deviceId: deviceId }, { - token: req.body.token, - platform: req.body.platform, + token: token, + platform: platform, userId: req.user._id, }, { @@ -37,8 +41,9 @@ router.put('/token', verifyToken, async (req, res) => { res.sendStatus(204) }) -router.post('/:id/read', verifyToken, async (req, res) => { - await NotificationModel.updateOne({ _id: req.params.id }, { isRead: true }) +router.post('/:id/read', verifyToken, bindingCargo(PostNotificationReadPayload), async (req, res) => { + const { id } = getCargo(req) + await NotificationModel.updateOne({ _id: id }, { isRead: true }) res.sendStatus(204) }) diff --git a/apps/server/src/controllers/v1/s3.ts b/apps/server/src/controllers/v1/s3.ts index 29629f98..066d2330 100644 --- a/apps/server/src/controllers/v1/s3.ts +++ b/apps/server/src/controllers/v1/s3.ts @@ -1,12 +1,15 @@ import express, { Router } from 'express' import asyncify from 'express-asyncify' +import { bindingCargo, getCargo } from 'express-cargo' + import { s3 } from '@/services/aws' import { AWS_S3_BUCKET } from '@/config' +import { GetS3UploadUrlPayload, GetS3ViewUrlPayload } from '@/types/payload' const router: Router = asyncify(express.Router()) -router.get('/upload-url', async (req, res) => { - const fileName = String(req.query.fileName) +router.get('/upload-url', bindingCargo(GetS3UploadUrlPayload), async (req, res) => { + const { fileName } = getCargo(req) const params = { Bucket: AWS_S3_BUCKET!, @@ -18,8 +21,8 @@ router.get('/upload-url', async (req, res) => { res.json({ presignedUrl }) }) -router.get('/view-url', async (req, res) => { - const key = req.query.key as string +router.get('/view-url', bindingCargo(GetS3ViewUrlPayload), async (req, res) => { + const { key } = getCargo(req) const params = { Bucket: AWS_S3_BUCKET!, diff --git a/apps/server/src/controllers/v1/schedules.ts b/apps/server/src/controllers/v1/schedules.ts index c9e93db5..ad7265d9 100644 --- a/apps/server/src/controllers/v1/schedules.ts +++ b/apps/server/src/controllers/v1/schedules.ts @@ -1,15 +1,17 @@ import asyncify from 'express-asyncify' import express, { Request, Response, Router } from 'express' +import { bindingCargo, getCargo } from 'express-cargo' import { ScheduleModel } from '@/models' import middlewares from '@/middlewares' import { getUserDailyScheduleCount } from '@/services/schedule' +import { GetScheduleDailyCountPayload, GetSchedulePayload, IdPayload, PostSchedulePayload, PutSchedulePayload } from '@/types/payload' const router: Router = asyncify(express.Router()) router.use(middlewares.auth.verifyToken) -router.post('/', middlewares.schedules.addScheduleMiddleware, async (req: Request, res: Response) => { - const { name, eventId, startDate, endDate, address, location, description, isAllDay } = req.body +router.post('/', bindingCargo(PostSchedulePayload), middlewares.schedules.addScheduleMiddleware, async (req: Request, res: Response) => { + const { name, eventId, startDate, endDate, address, location, description, isAllDay } = getCargo(req) const schedule = await ScheduleModel.create({ name: name, eventId: eventId, @@ -26,55 +28,37 @@ router.post('/', middlewares.schedules.addScheduleMiddleware, async (req: Reques }) }) -router.get('/', async (req: Request, res: Response) => { - const options = { - page: Number(req.query.page) || 0, - limit: Number(req.query.limit) || 100, - } - - const filterOption = { - from: new Date(req.query.from as string) || new Date(0), - to: new Date(req.query.to as string) || new Date(), - } - +router.get('/', bindingCargo(GetSchedulePayload), async (req: Request, res: Response) => { + const { from, to, page, limit } = getCargo(req) const userId = req.user?._id.toString() - const schedules = await ScheduleModel.findByUserId(userId, filterOption, options) + const schedules = await ScheduleModel.findByUserId(userId, { from, to }, { page, limit }) res.status(200).json(schedules) }) -router.get('/dailyCount', async (req: Request, res: Response) => { - const from = new Date(req.query.from as string) || new Date(0) - const to = new Date(req.query.to as string) || new Date() - const timezone = (req.query.timezone as string) || 'Asia/Seoul' - +router.get('/dailyCount', bindingCargo(GetScheduleDailyCountPayload), async (req: Request, res: Response) => { + const { from, to, timezone } = getCargo(req) const dailyScheduleCount = await getUserDailyScheduleCount(req.user, from, to, timezone) res.status(200).json(dailyScheduleCount) }) -router.get('/:id', middlewares.schedules.verifyAuthorMiddleware, async (req: Request, res: Response) => { - const schedule = await ScheduleModel.findOne({ _id: req.params.id }) +router.get('/:id', bindingCargo(IdPayload), middlewares.schedules.verifyAuthorMiddleware, async (req: Request, res: Response) => { + const { id } = getCargo(req) + const schedule = await ScheduleModel.findOne({ _id: id }) res.status(200).json(schedule) }) -router.delete('/:id', middlewares.schedules.verifyAuthorMiddleware, async (req: Request, res: Response) => { - await ScheduleModel.deleteOne({ _id: req.params.id }) +router.delete('/:id', bindingCargo(IdPayload), middlewares.schedules.verifyAuthorMiddleware, async (req: Request, res: Response) => { + const { id } = getCargo(req) + await ScheduleModel.deleteOne({ _id: id }) res.sendStatus(204) }) -router.put('/:id', middlewares.schedules.verifyAuthorMiddleware, async (req: Request, res: Response) => { - const { id } = req.params - const { name, startDate, endDate, description, location, address } = req.body +router.put('/:id', bindingCargo(PutSchedulePayload), middlewares.schedules.verifyAuthorMiddleware, async (req: Request, res: Response) => { + const { id, name, startDate, endDate, isAllDay, description, location, address } = getCargo(req) const updated = await ScheduleModel.findOneAndUpdate( { _id: id }, - { - name: name, - startDate: startDate, - endDate: endDate, - description: description, - address: address, - location: location, - }, + { name, startDate, endDate, isAllDay, description, address, location }, { new: true }, ) res.status(200).json(updated) diff --git a/apps/server/src/controllers/v1/users.ts b/apps/server/src/controllers/v1/users.ts index 3cbd0085..32520adf 100644 --- a/apps/server/src/controllers/v1/users.ts +++ b/apps/server/src/controllers/v1/users.ts @@ -1,7 +1,7 @@ -import asyncify from 'express-asyncify' import express, { Request, Response, Router } from 'express' - -import { loginRequest, loginResponse, refreshResponse, registerRequest, registerResponse } from '@fienmee/types' +import asyncify from 'express-asyncify' +import { bindingCargo, getCargo } from 'express-cargo' +import { loginResponse, refreshResponse, registerResponse } from '@fienmee/types' import { DeletedUserError, UnknownUserError } from '@/types/errors/oauth' import { InvalidTokenTypeError } from '@/types/errors' @@ -9,11 +9,12 @@ import { UserModel } from '@/models' import { issueAccessToken, getOAuthUser, expireJwt, issueRefreshToken, unlinkUser } from '@/services/oauth' import { verifyToken } from '@/middlewares/auth' import { registerUser } from '@/services/user' +import { PostRegisterPayload, PostUserGoogleTokenPayload, PostUserInterestPayload, PostUserLoginPayload } from '@/types/payload' const router: Router = asyncify(express.Router()) -router.post('/login', async (req: Request, res: Response) => { - const request: loginRequest = req.body +router.post('/login', bindingCargo(PostUserLoginPayload), async (req: Request, res: Response) => { + const request = getCargo(req) const oauthUser = await getOAuthUser(request) const user = await UserModel.findByProviderId(oauthUser.providerId) @@ -41,11 +42,9 @@ router.delete('/logout', async (req: Request, res: Response) => { res.sendStatus(204) }) -router.post('/register', async (req: Request, res: Response) => { - const request: registerRequest = req.body - +router.post('/register', bindingCargo(PostRegisterPayload), async (req: Request, res: Response) => { + const request = getCargo(req) const oauthUser = await getOAuthUser(request) - const user = await registerUser(oauthUser) const accessToken = issueAccessToken(user) @@ -92,8 +91,8 @@ router.delete('/', verifyToken, async (req: Request, res: Response) => { res.sendStatus(204) }) -router.post('/google-token', async (req: Request, res: Response) => { - const serverAuthCode = req.body.code as string +router.post('/google-token', bindingCargo(PostUserGoogleTokenPayload), async (req: Request, res: Response) => { + const { code: serverAuthCode } = getCargo(req) const params = new URLSearchParams() params.append('code', serverAuthCode) @@ -114,13 +113,14 @@ router.post('/google-token', async (req: Request, res: Response) => { res.json(data.refresh_token) }) -router.put('/interest', verifyToken, async (req: Request, res: Response) => { - const isInterested = req.user.interests.some(category => category._id === req.body.interest) +router.put('/interest', verifyToken, bindingCargo(PostUserInterestPayload), async (req: Request, res: Response) => { + const { interest } = getCargo(req) + const isInterested = req.user.interests.some(category => category._id === interest) if (isInterested) { - await UserModel.removeInterest(req.user._id, req.body.interest) + await UserModel.removeInterest(req.user._id, interest) } else { - await UserModel.addInterest(req.user._id, req.body.interest) + await UserModel.addInterest(req.user._id, interest) } res.sendStatus(204) diff --git a/apps/server/src/types/payload/common.ts b/apps/server/src/types/payload/common.ts new file mode 100644 index 00000000..789b080e --- /dev/null +++ b/apps/server/src/types/payload/common.ts @@ -0,0 +1,27 @@ +import { array, body, defaultValue, equal, params, query, validate } from 'express-cargo' + +export class LocationType { + @body() + @equal('Point') + type: string + + @body() + @array(Number) + @validate(value => Array.isArray(value) && value.length === 2) + coordinates: number[] +} + +export class PaginationPayload { + @query() + @defaultValue(1) + page!: number + + @query() + @defaultValue(10) + limit!: number +} + +export class IdPayload { + @params() + id: string +} diff --git a/apps/server/src/types/payload/enquiry.ts b/apps/server/src/types/payload/enquiry.ts new file mode 100644 index 00000000..7469db9e --- /dev/null +++ b/apps/server/src/types/payload/enquiry.ts @@ -0,0 +1,10 @@ +import { body } from 'express-cargo' +import { IEnquiryFormInputs } from '@fienmee/types' + +export class PostEnquiryPayload implements IEnquiryFormInputs { + @body() + title: string + + @body() + body: string +} diff --git a/apps/server/src/types/payload/event.ts b/apps/server/src/types/payload/event.ts new file mode 100644 index 00000000..0be72fa0 --- /dev/null +++ b/apps/server/src/types/payload/event.ts @@ -0,0 +1,173 @@ +import { array, body, defaultValue, oneOf, optional, params, query, range, virtual } from 'express-cargo' +import { IPostEventRequestBody } from '@fienmee/types' +import { IdPayload, LocationType, PaginationPayload } from './common' + +export class EventBasePayload implements IPostEventRequestBody { + @body() + name: string + + @body() + address!: string + + @body() + location!: LocationType + + @body() + startDate!: Date + + @body() + endDate!: Date + + @body() + @array(String) + photo!: string[] + + @body() + cost!: string + + @body() + description!: string + + @body() + @array(String) + category!: string[] + + @body() + @array(String) + targetAudience!: string[] + + @body() + isAllDay!: boolean +} + +export class EventIdPayload extends IdPayload {} + +export class EventCommentIdPayload extends EventIdPayload { + @params() + commentId!: string +} + +export class PostEventPayload extends EventBasePayload {} + +export class GetEventSearchPayload extends PaginationPayload { + @query() + q!: string + + @query() + @oneOf(['default', 'name', 'address']) + @defaultValue('default') + target!: string + + @query() + @oneOf(['default', 'hot', 'startDate', 'endDate']) + @defaultValue('default') + sort!: string + + @query() + @optional() + category?: string + + @query() + @defaultValue(new Date()) + startDate!: Date + + @query() + @optional() + endDate?: Date + + @query() + @defaultValue(false) + isAllDay!: boolean + + @virtual((obj: GetEventSearchPayload) => (obj.page - 1) * obj.limit) + skip!: number +} + +export class PutEventPayload extends EventBasePayload { + @params() + id!: string +} + +export class PostEventReviewPayload extends EventIdPayload { + @body() + @range(1, 5) + rating!: number + + @body() + @array(String) + photo!: string[] + + @body() + body!: string +} + +export class GetEventReviewPayload extends PaginationPayload { + @params() + id!: string +} + +export class PostEventCommentPayload extends EventIdPayload { + @body() + comment!: string +} + +export class GetEventCommentPayload extends PaginationPayload { + @params() + id!: string +} + +export class PutEventCommentPayload extends EventCommentIdPayload { + @body() + comment!: string +} + +export class GetEventDateCategoryPayload { + @query() + @optional() + from: Date + + @query() + @optional() + to: Date + + @query() + @defaultValue(1) + page!: number + + @query() + @defaultValue(0) + limit!: number +} + +export class GetEventHotCategoryPayload { + @query() + @optional() + from: Date + + @query() + @optional() + to: Date + + @query() + @defaultValue(1) + page!: number + + @query() + @defaultValue(3) + limit!: number +} + +export class GetEventInterestCategoryPayload { + @query() + @defaultValue(1) + page!: number + + @query() + @defaultValue(3) + limit!: number +} + +export class GetEventCategoryPayload extends PaginationPayload { + @params() + category!: string +} diff --git a/apps/server/src/types/payload/index.ts b/apps/server/src/types/payload/index.ts new file mode 100644 index 00000000..efeb657c --- /dev/null +++ b/apps/server/src/types/payload/index.ts @@ -0,0 +1,7 @@ +export * from './common' +export * from './enquiry' +export * from './event' +export * from './notification' +export * from './s3' +export * from './schedule' +export * from './user' diff --git a/apps/server/src/types/payload/notification.ts b/apps/server/src/types/payload/notification.ts new file mode 100644 index 00000000..88eb270d --- /dev/null +++ b/apps/server/src/types/payload/notification.ts @@ -0,0 +1,21 @@ +import { IdPayload, PaginationPayload } from '@/types/payload/common' +import { body, oneOf } from 'express-cargo' +import { INotificationToken, PlatformType } from '@fienmee/types' + +const platforms = Object.values(PlatformType).filter(value => !isNaN(Number(value))) + +export class GetNotificationPayload extends PaginationPayload {} + +export class PutNotificationTokenPayload implements INotificationToken { + @body() + deviceId!: string + + @body() + token!: string + + @body() + @oneOf(platforms) + platform!: number +} + +export class PostNotificationReadPayload extends IdPayload {} diff --git a/apps/server/src/types/payload/s3.ts b/apps/server/src/types/payload/s3.ts new file mode 100644 index 00000000..5a0f96ce --- /dev/null +++ b/apps/server/src/types/payload/s3.ts @@ -0,0 +1,11 @@ +import { query } from 'express-cargo' + +export class GetS3UploadUrlPayload { + @query() + fileName!: string +} + +export class GetS3ViewUrlPayload { + @query() + key!: string +} diff --git a/apps/server/src/types/payload/schedule.ts b/apps/server/src/types/payload/schedule.ts new file mode 100644 index 00000000..60352f5a --- /dev/null +++ b/apps/server/src/types/payload/schedule.ts @@ -0,0 +1,79 @@ +import { body, defaultValue, optional, query } from 'express-cargo' +import { IMakeNewScheduleRequest } from '@fienmee/types' +import { IdPayload, LocationType } from './common' + +class GetSchedulePayloadBase { + @query() + @defaultValue(new Date(0)) + from!: Date + + @query() + @defaultValue(new Date()) + to!: Date +} + +export class PostSchedulePayload implements IMakeNewScheduleRequest { + @body() + name!: string + + @body() + @optional() + eventId?: string + + @body() + isAllDay!: boolean + + @body() + startDate!: Date + + @body() + endDate!: Date + + @body() + address!: string + + @body() + location!: LocationType + + @body() + description!: string +} + +export class GetSchedulePayload extends GetSchedulePayloadBase { + @query() + @defaultValue(1) + page!: number + + @query() + @defaultValue(100) + limit!: number +} + +export class GetScheduleDailyCountPayload extends GetSchedulePayloadBase { + @query() + @defaultValue('Asia/Seoul') + timezone!: string +} + +export class PutSchedulePayload extends IdPayload { + @body() + name!: string + + @body() + startDate!: Date + + @body() + endDate!: Date + + @body() + isAllDay!: boolean + + @body() + address!: string + + @body() + location!: LocationType + + @body() + description!: string +} diff --git a/apps/server/src/types/payload/user.ts b/apps/server/src/types/payload/user.ts new file mode 100644 index 00000000..eb22d4e8 --- /dev/null +++ b/apps/server/src/types/payload/user.ts @@ -0,0 +1,25 @@ +import { body } from 'express-cargo' +import { loginRequest, registerRequest } from '@fienmee/types' + +export class PostUserLoginPayload implements loginRequest { + @body() + provider!: string + + @body() + accessToken!: string + + @body() + refreshToken!: string +} + +export class PostRegisterPayload extends PostUserLoginPayload implements registerRequest {} + +export class PostUserGoogleTokenPayload { + @body() + code!: string +} + +export class PostUserInterestPayload { + @body() + interest!: string +} diff --git a/packages/types/api/events.ts b/packages/types/api/events.ts index 86709b9d..b06bdade 100644 --- a/packages/types/api/events.ts +++ b/packages/types/api/events.ts @@ -1,7 +1,6 @@ import { ICategory } from './category' -export interface IEvent { - _id: string +export interface IEventBase { name: string address: string location: { @@ -13,13 +12,17 @@ export interface IEvent { description: string photo: string[] cost: string + targetAudience: string[] + isAllDay: boolean +} + +export interface IEvent extends IEventBase { + _id: string likeCount: number commentCount: number category: ICategory[] - targetAudience: string[] createdAt: Date isAuthor: boolean - isAllDay: boolean isLiked: boolean } @@ -35,26 +38,12 @@ export interface IGetEventsByCategoryResponse { events: IEvent[] } +export interface IPostEventRequestBody extends IEventBase { + category: string[] +} + export interface IPostEventRequest { - body: { - name: string - address: string - location: { - type: string - coordinates: number[] - } - startDate: Date - endDate: Date - description: string - photo: string[] - cost: string - likeCount: number - commentCount: number - category: string[] - targetAudience: string[] - createdAt: Date - isAllDay: boolean - } + body: IPostEventRequestBody } export interface IPutEventRequest extends IPostEventRequest { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8534d23d..f6f2b30e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -223,6 +223,9 @@ importers: express-asyncify: specifier: ^2.1.2 version: 2.1.3(express@4.21.2) + express-cargo: + specifier: ^0.4.0 + version: 0.4.0(@types/express@5.0.0)(express@4.21.2)(reflect-metadata@0.2.2) express-winston: specifier: ^4.2.0 version: 4.2.0(winston@3.16.0) @@ -256,6 +259,9 @@ importers: node-schedule: specifier: ^2.1.1 version: 2.1.1 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 winston: specifier: ^3.16.0 version: 3.16.0 @@ -4029,6 +4035,13 @@ packages: peerDependencies: express: ^4.19.2 + express-cargo@0.4.0: + resolution: {integrity: sha512-U6dRSQMes8iz9s69a8EgZvyAgDEND1PNgy0kpu34yGQrwA9UUHe45lhLYCuLiUDgrFUpr7Dcm79ExokcaJMo/w==} + peerDependencies: + '@types/express': '>=4.17.0 <6' + express: '>=4.17.0 <6' + reflect-metadata: ^0.2.2 + express-winston@4.2.0: resolution: {integrity: sha512-EMD74g63nVHi7pFleQw7KHCxiA1pjF5uCwbCfzGqmFxs9KvlDPIVS3cMGpULm6MshExMT9TjC3SqmRGB9kb7yw==} engines: {node: '>= 6'} @@ -7525,7 +7538,7 @@ snapshots: '@babel/helper-member-expression-to-functions@7.25.9': dependencies: '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -7547,7 +7560,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.25.9': dependencies: - '@babel/types': 7.27.0 + '@babel/types': 7.28.2 '@babel/helper-plugin-utils@7.26.5': {} @@ -7588,9 +7601,9 @@ snapshots: '@babel/helper-wrap-function@7.25.9': dependencies: - '@babel/template': 7.27.0 + '@babel/template': 7.27.2 '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -8961,7 +8974,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.29 '@types/node': 20.17.6 chalk: 4.1.2 collect-v8-coverage: 1.0.2 @@ -8989,7 +9002,7 @@ snapshots: '@jest/source-map@29.6.3': dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.29 callsites: 3.1.0 graceful-fs: 4.2.11 @@ -9716,7 +9729,7 @@ snapshots: '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.26.10) '@babel/plugin-transform-typescript': 7.25.9(@babel/core@7.26.10) '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.10) - '@babel/template': 7.27.0 + '@babel/template': 7.27.2 '@react-native/babel-plugin-codegen': 0.76.3(@babel/preset-env@7.26.9(@babel/core@7.26.10)) babel-plugin-syntax-hermes-parser: 0.25.1 babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.26.10) @@ -10407,7 +10420,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.0.4) '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.0.4) - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.1 eslint: 8.57.1 ts-api-utils: 1.4.0(typescript@5.0.4) optionalDependencies: @@ -10461,7 +10474,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.1 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -10638,7 +10651,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.0(supports-color@5.5.0) + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -12067,6 +12080,12 @@ snapshots: dependencies: express: 4.21.2 + express-cargo@0.4.0(@types/express@5.0.0)(express@4.21.2)(reflect-metadata@0.2.2): + dependencies: + '@types/express': 5.0.0 + express: 4.21.2 + reflect-metadata: 0.2.2 + express-winston@4.2.0(winston@3.16.0): dependencies: chalk: 2.4.2 @@ -12967,7 +12986,7 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: '@babel/core': 7.26.10 - '@babel/parser': 7.27.0 + '@babel/parser': 7.28.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 7.7.2 @@ -13923,8 +13942,8 @@ snapshots: metro-transform-plugins@0.81.0: dependencies: '@babel/core': 7.26.10 - '@babel/generator': 7.27.0 - '@babel/template': 7.27.0 + '@babel/generator': 7.28.0 + '@babel/template': 7.27.2 '@babel/traverse': 7.27.0 flow-enums-runtime: 0.0.6 nullthrows: 1.1.1 @@ -13934,9 +13953,9 @@ snapshots: metro-transform-worker@0.81.0: dependencies: '@babel/core': 7.26.10 - '@babel/generator': 7.27.0 - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 + '@babel/generator': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.2 flow-enums-runtime: 0.0.6 metro: 0.81.0 metro-babel-transformer: 0.81.0 @@ -14442,7 +14461,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.26.2 + '@babel/code-frame': 7.27.1 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -15909,7 +15928,7 @@ snapshots: v8-to-istanbul@9.3.0: dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.29 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0