From b24b27973ccfb320f4545bd76e925a0303de178d Mon Sep 17 00:00:00 2001 From: DongLee99 <55686088+DongLee99@users.noreply.github.com> Date: Wed, 28 Jun 2023 23:31:18 +0900 Subject: [PATCH 01/27] =?UTF-8?q?[FEAT]=20=EC=B7=A8=ED=96=A5=EB=B3=84=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [STYLE] 띄어쓰기 수정 * [FEAT] Card Api 라우터 추가 * [FEAT] 최근 업데이트, 북마크 된 카트 및 성별 별 베스트 피클 API * [CHORE] package.json * [CHORE] 출력문 제거 --------- Co-authored-by: kyY00n --- package-lock.json | 14 +++ package.json | 1 + src/controllers/cardController.ts | 101 ++++++++++++++++- .../CardRecentlyDateAndInfoResponseDto | 7 ++ src/models/bookmark.ts | 1 + src/modules/responseMessage.ts | 6 +- src/routes/cardRouter.ts | 17 ++- src/routes/index.ts | 6 +- src/services/cardService.ts | 104 +++++++++++++++++- 9 files changed, 251 insertions(+), 6 deletions(-) create mode 100644 src/intefaces/CardRecentlyDateAndInfoResponseDto diff --git a/package-lock.json b/package-lock.json index 21d3be6..6c242e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "express-validator": "^6.14.0", "firebase": "^9.9.3", "helmet": "^5.1.0", + "install": "^0.13.0", "jsonwebtoken": "^8.5.1", "mongoose": "^6.3.1", "multer": "^1.4.4", @@ -4676,6 +4677,14 @@ "node": ">=10" } }, + "node_modules/install": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz", + "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/internal-slot": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", @@ -11768,6 +11777,11 @@ "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", "dev": true }, + "install": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz", + "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==" + }, "internal-slot": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", diff --git a/package.json b/package.json index 7617299..6b66dde 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "express-validator": "^6.14.0", "firebase": "^9.9.3", "helmet": "^5.1.0", + "install": "^0.13.0", "jsonwebtoken": "^8.5.1", "mongoose": "^6.3.1", "multer": "^1.4.4", diff --git a/src/controllers/cardController.ts b/src/controllers/cardController.ts index c0505e5..8101cf1 100644 --- a/src/controllers/cardController.ts +++ b/src/controllers/cardController.ts @@ -30,4 +30,103 @@ const getBestCardList = async ( } }; -export { getBestCardList }; +/** + * @route /cards/:cardId + * @desc 아이디에 해당하는 카드 한 장을 조회합니다. + * @access Public + */ +const getCards = async (req: Request, res: Response, next: NextFunction) => { + try { + const cardId = new Types.ObjectId(req.params.cardId); + const card = await CardService.findCards(cardId, BEST_PIICKLE_SIZE); + return res + .status(statusCode.OK) + .send(util.success(statusCode.OK, message.READ_CARD_SUCCESS, card)); + } catch (err) { + next(err); + } +}; + +/** + * @route /cards/recentlyBookmarkedCard + * @desc 최근 북마크 된 카드를 조회합니다. + * @access Public + */ +const getRecentlyBookmarkedCard = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + console.log('정신차려'); + const card = await CardService.findRecentlyBookmarkedCard(); + return res + .status(statusCode.OK) + .send( + util.success( + statusCode.OK, + message.READ_RECENTLY_BOOKMARKED_CARD_SUCCESS, + card + ) + ); + } catch (err) { + next(err); + } +}; + +/** + * @route /cards/recentlyUpdatedCard + * @desc 최근 업데이트 된 카드를 조회합니다. + * @access Public + */ +const getRecentlyUpdatedCard = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const card = await CardService.findRecentlyUpdatedCard(); + return res + .status(statusCode.OK) + .send( + util.success( + statusCode.OK, + message.READ_RECENTLY_UPDATE_CARD_SUCCESS, + card + ) + ); + } catch (err) { + next(err); + } +}; + +/** + * @route /cards/cardByBookmarkedGender/:gender + * @desc 아이디에 해당하는 카드 한 장을 조회합니다. + * @access Public + */ +const getCardByBookmarkedGender = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const gender = req.params.gender; + const card = await CardService.findCardByBookmarkedGender(gender); + return res + .status(statusCode.OK) + .send( + util.success(statusCode.OK, message.READ_GENDER_BEST_CARD_SUCCESS, card) + ); + } catch (err) { + next(err); + } +}; + +export { + getBestCardList, + getCards, + getRecentlyBookmarkedCard, + getRecentlyUpdatedCard, + getCardByBookmarkedGender +}; diff --git a/src/intefaces/CardRecentlyDateAndInfoResponseDto b/src/intefaces/CardRecentlyDateAndInfoResponseDto new file mode 100644 index 0000000..169e913 --- /dev/null +++ b/src/intefaces/CardRecentlyDateAndInfoResponseDto @@ -0,0 +1,7 @@ +import { Types } from 'mongoose'; +import { CardResponseDto } from './CardResponseDto'; + +export interface CardRecentlyDateAndInfoResponseDto { + recentlyDate: Date; + cardResponseDtos: CardResponseDto[]; +} diff --git a/src/models/bookmark.ts b/src/models/bookmark.ts index ff2458e..f571424 100644 --- a/src/models/bookmark.ts +++ b/src/models/bookmark.ts @@ -23,3 +23,4 @@ const bookmarkSchema = new Schema( const Bookmark = model('Bookmark', bookmarkSchema); export default Bookmark; +export { BookmarkDocument }; diff --git a/src/modules/responseMessage.ts b/src/modules/responseMessage.ts index ce16214..97daca4 100644 --- a/src/modules/responseMessage.ts +++ b/src/modules/responseMessage.ts @@ -23,8 +23,12 @@ const message = { //카드 SEARCH_CARDS_SUCCESS: '필터에 해당하는 카드들 읽어오기 성공', - READ_CARD_SUCCESS: '카테고리에 해당 카드 읽어오기 성공', + READ_CARD_SUCCESS: '공유 받은 해당 카드 읽어오기 성공', + READ_RECENTLY_UPDATE_CARD_SUCCESS: '최근 업데이트 된 카드 읽어오기 성공', + READ_RECENTLY_BOOKMARKED_CARD_SUCCESS: '최근 북마크 된 카드 읽어오기 성공', + READ_GENDER_BEST_CARD_SUCCESS: '성별 별 베스트 카드 읽어오기 성공', READ_BEST_CARDS_SUCCESS: '베스트 카드 읽어오기 성공', + READ_ONE_CARD_SUCCESS: '카드 한 장 읽어오기 성공', //카드 메들리 READ_CARD_MEDLEY_DETAIL_SUCCESS: '카드 메들리 상세 조회 성공', diff --git a/src/routes/cardRouter.ts b/src/routes/cardRouter.ts index ce92de4..8a013fe 100644 --- a/src/routes/cardRouter.ts +++ b/src/routes/cardRouter.ts @@ -3,7 +3,22 @@ import { CardController } from '../controllers'; import flexibleAuth from '../middlewares/flexibleAuth'; const router = Router(); - +router.get( + '/cardByBookmarkedGender/:gender', + flexibleAuth, + CardController.getCardByBookmarkedGender +); +router.get( + '/recentlyBookmarkedCard', + flexibleAuth, + CardController.getRecentlyBookmarkedCard +); +router.get( + '/recentlyUpdatedCard', + flexibleAuth, + CardController.getRecentlyUpdatedCard +); router.get('/best', flexibleAuth, CardController.getBestCardList); +router.get('/:cardId', flexibleAuth, CardController.getCards); export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts index 3bc1dc9..a450d6b 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,5 +1,5 @@ //router index file -import { Router } from 'express'; +import {Request, Response, Router} from 'express'; import UserRouter from './userRouter'; import CategoryRouter from './CategoryRouter'; import BallotRouter from './ballotRouter'; @@ -18,4 +18,8 @@ router.use('/cards', CardRouter); router.use('/test', TestRouter); router.use('/medley', CardMedleyRouter); +router.use('', (req: Request, res: Response) => { + res.status(200).send(); +}); + export default router; diff --git a/src/services/cardService.ts b/src/services/cardService.ts index 1ee470a..eea1351 100644 --- a/src/services/cardService.ts +++ b/src/services/cardService.ts @@ -1,4 +1,4 @@ -import Bookmark from '../models/bookmark'; +import Bookmark, { BookmarkDocument } from '../models/bookmark'; import Card, { CardDocument } from '../models/card'; import util from '../modules/util'; import { Types } from 'mongoose'; @@ -31,6 +31,7 @@ const createCardResponse = async ( isBookmark: await findBookmark(card._id, userId) }; }; + const findBestCardsLimit = async (size: number) => { const cardsWithBookmarkCount = await Bookmark.aggregate() .group({ _id: '$card', count: { $sum: 1 } }) @@ -67,4 +68,103 @@ const findBestCards = async (size: number, userId?: Types.ObjectId) => { return totalCards; }; -export { findBestCards }; +const findCards = async (cardId: Types.ObjectId, size: number) => { + const cards: CardDocument[] = await Card.find({ + _id: cardId + }); + const extraCards = + cards.length < size ? await findExtraCardsExceptFor(cards, size) : []; + const totalCards: CardResponseDto[] = []; + for (const card of [...cards, ...extraCards]) { + totalCards.push({ + _id: card._id, + content: card.content, + tags: card.tags, + category: card.category, + filter: card.filter, + isBookmark: false + }); + } + return totalCards; +}; + +const findRecentlyUpdatedCard = async () => { + const cards: CardDocument[] = await Card.aggregate() + .sort({ createdAt: -1 }) + .limit(20); + const totalCards: CardResponseDto[] = []; + for (const card of cards) { + totalCards.push({ + _id: card._id, + content: card.content, + tags: card.tags, + category: card.category, + filter: card.filter, + isBookmark: false + }); + } + return { + recentlyDate: cards[0].createdAt, + cardResponseDtos: totalCards + }; +}; + +const findRecentlyBookmarkedCard = async () => { + const bookmarks: BookmarkDocument[] = await Bookmark.aggregate() + .sort({ createdAt: -1 }) + .limit(20); + const cards: CardDocument[] = ( + await Promise.all(bookmarks.map(bookmark => Card.findById(bookmark.card))) + ).filter(util.isNotEmpty); + const totalCards: CardResponseDto[] = []; + for (const card of cards) { + totalCards.push({ + _id: card._id, + content: card.content, + tags: card.tags, + category: card.category, + filter: card.filter, + isBookmark: false + }); + } + return { + recentlyDate: bookmarks[0].createdAt, + cardResponseDtos: totalCards + }; +}; + +const findCardByBookmarkedGender = async (gender: string) => { + const bookmarks = await Bookmark.aggregate() + .lookup({ + from: 'users', + localField: 'user', + foreignField: '_id', + as: 'userInfo' + }) + .match({ 'userInfo.gender': gender }) + .group({ _id: '$card', count: { $sum: 1 } }) + .sort('-count _id'); + const cards: CardDocument[] = ( + await Promise.all(bookmarks.map(bookmark => Card.findById(bookmark._id))) + ).filter(util.isNotEmpty); + const totalCards: CardResponseDto[] = []; + for (const card of cards) { + totalCards.push({ + _id: card._id, + content: card.content, + tags: card.tags, + category: card.category, + filter: card.filter, + isBookmark: false + }); + } + return totalCards; +}; + +export { + findBestCards, + findCards, + findRecentlyBookmarkedCard, + findCardByBookmarkedGender, + findRecentlyUpdatedCard +}; From e53a7080db753119768ebdbf9aee43ee60211e0a Mon Sep 17 00:00:00 2001 From: kyY00n Date: Wed, 28 Jun 2023 23:31:54 +0900 Subject: [PATCH 02/27] =?UTF-8?q?[FEAT]=20=EC=86=8C=EC=85=9C=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20(#170)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FEAT] 벤더사 별 요청 url 정의 * [FEAT] 소셜로그인 api 구현 * [FEAT] 네이버 로그인 토큰 발급 * [FEAT] 닉네임 자동 생성 --- .eslintrc.json | 2 +- src/config/index.ts | 4 +- src/controllers/userController.ts | 128 ++++++++++++++----- src/intefaces/user/LoginResponseDto.ts | 6 + src/intefaces/user/SocialLoginDto.ts | 6 + src/intefaces/user/UserBookmarkDto.ts | 14 +- src/intefaces/user/UserProfileResponseDto.ts | 6 +- src/loaders/db.ts | 2 +- src/models/interface/IUser.ts | 8 +- src/models/socialAccount.ts | 6 + src/models/socialVendor.ts | 65 ++++++++++ src/models/user.ts | 49 ------- src/models/user/ageGroup.ts | 12 ++ src/models/user/gender.ts | 7 + src/models/user/user.ts | 69 ++++++++++ src/routes/testRouter.ts | 2 +- src/routes/userRouter.ts | 16 ++- src/services/authService.ts | 6 +- src/services/dto/INaverResponse.ts | 79 ++++++++++++ src/services/dto/KakaoResponse.ts | 90 +++++++++++++ src/services/dto/socialLoginResponse.ts | 5 + src/services/index.ts | 6 +- src/services/naverLoginService.ts | 23 ++++ src/services/preUserService.ts | 2 +- src/services/socialAuthService.ts | 101 +++++++++++++++ src/services/userService.ts | 64 ++++++---- src/types/TypedResponse.ts | 3 + test/cardMedleyService.spec.ts | 12 +- test/user.spec.ts | 21 ++- tsconfig.json | 4 +- 30 files changed, 661 insertions(+), 157 deletions(-) create mode 100644 src/intefaces/user/LoginResponseDto.ts create mode 100644 src/intefaces/user/SocialLoginDto.ts create mode 100644 src/models/socialAccount.ts create mode 100644 src/models/socialVendor.ts delete mode 100644 src/models/user.ts create mode 100644 src/models/user/ageGroup.ts create mode 100644 src/models/user/gender.ts create mode 100644 src/models/user/user.ts create mode 100644 src/services/dto/INaverResponse.ts create mode 100644 src/services/dto/KakaoResponse.ts create mode 100644 src/services/dto/socialLoginResponse.ts create mode 100644 src/services/naverLoginService.ts create mode 100644 src/services/socialAuthService.ts create mode 100644 src/types/TypedResponse.ts diff --git a/.eslintrc.json b/.eslintrc.json index a7585b5..5612b29 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -19,7 +19,7 @@ "plugins": ["@typescript-eslint", "prettier"], "overrides": [ { - "files": ["*.ts", "*.tsx"], // Your TypeScript files extension + "files": ["*.ts", "*.tsx", "*.spec.ts"], // Your TypeScript files extension // As mentioned in the comments, you should extend TypeScript plugins here, // instead of extending them outside the `overrides`. diff --git a/src/config/index.ts b/src/config/index.ts index 91eb686..bf45add 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -55,5 +55,7 @@ export default { prod: process.env.SIGN_UP_REDIRECTION_PROD_URL as string, dev: process.env.SIGN_UP_REDIRECTION_DEV_URL as string }, - clientKey: process.env.CLIENT_KEY as string + clientKey: process.env.CLIENT_KEY as string, + naverClientId: process.env.NAVER_CLIENT_ID as string, + naverClientSecret: process.env.NAVER_CLIENT_SECRET as string }; diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index d09ca47..ce6a895 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -2,7 +2,8 @@ import { NextFunction, Request, Response } from 'express'; import { validationResult } from 'express-validator'; import { EmptyMailCodeException, - IllegalArgumentException + IllegalArgumentException, + IllegalStateException } from '../intefaces/exception'; import { CreateUserCommand, @@ -14,7 +15,13 @@ import getToken from '../modules/jwtHandler'; import message from '../modules/responseMessage'; import statusCode from '../modules/statusCode'; import util from '../modules/util'; -import { UserService, PreUserService, AuthService } from '../services'; +import { + UserService, + PreUserService, + AuthService, + SocialAuthService, + NaverLoginService +} from '../services'; import { UserProfileResponseDto } from '../intefaces/user/UserProfileResponseDto'; import { UserUpdateNicknameDto } from '../intefaces/user/UserUpdateNicknameDto'; import { UserBookmarkDto } from '../intefaces/user/UserBookmarkDto'; @@ -22,8 +29,11 @@ import { UserBookmarkInfo } from '../intefaces/user/UserBookmarkInfo'; import { Types } from 'mongoose'; import { TypedRequest } from '../types/TypedRequest'; import EmailVerificationReqDto from '../intefaces/user/EmailVerificationReqDto'; -import { UpdateUserDto } from '../intefaces/user/UpdateUserDto'; import config from '../config'; +import SocialLoginDto from '../intefaces/user/SocialLoginDto'; +import LoginResponseDto from '../intefaces/user/LoginResponseDto'; +import { UserDocument } from '../models/user/user'; +import { SocialVendor } from '../models/socialVendor'; /** * @route GET /email @@ -93,7 +103,7 @@ const nicknameDuplicationCheck = async ( if (!nickname) { throw new IllegalArgumentException('필요한 값이 없습니다.'); } - const result = await UserService.nicknameDuplicationCheck(nickname); + const result = await UserService.nicknameAlreadyExists(nickname); res .status(statusCode.OK) .send(util.success(statusCode.OK, '닉네임 중복 체크 성공', result)); @@ -196,35 +206,35 @@ const postUser = async ( * @route /users * @access Public */ -const patchUser = async (req: Request, res: Response, next: NextFunction) => { - try { - const userId = req.user?.id; - if (!userId) { - throw new IllegalArgumentException('로그인이 필요합니다.'); - } - const reqErr = validationResult(req); - - if (!reqErr.isEmpty()) { - throw new IllegalArgumentException('필요한 값이 없습니다.'); - } - - const input: UpdateUserDto = { - id: userId, - nickname: req.body.nickname, - birthday: req.body.birthday, - gender: req.body.gender, - profileImgUrl: (req?.file as Express.MulterS3.File)?.location - }; - - await UserService.patchUser(input); - - res - .status(statusCode.OK) - .send(util.success(statusCode.OK, message.USER_UPDATE_SUCCESS)); - } catch (err) { - next(err); - } -}; +// const patchUser = async (req: Request, res: Response, next: NextFunction) => { +// try { +// const userId = req.user?.id; +// if (!userId) { +// throw new IllegalArgumentException('로그인이 필요합니다.'); +// } +// const reqErr = validationResult(req); +// +// if (!reqErr.isEmpty()) { +// throw new IllegalArgumentException('필요한 값이 없습니다.'); +// } +// +// const input: UpdateUserDto = { +// id: userId, +// nickname: req.body.nickname, +// birthday: req.body.birthday, +// gender: req.body.gender, +// profileImgUrl: (req?.file as Express.MulterS3.File)?.location +// }; +// +// await UserService.patchUser(input); +// +// res +// .status(statusCode.OK) +// .send(util.success(statusCode.OK, message.USER_UPDATE_SUCCESS)); +// } catch (err) { +// next(err); +// } +// }; /** * @route /users/login @@ -246,7 +256,7 @@ const postUserLogin = async ( userLoginDto ); const accessToken = getToken(result._id); - const data = { + const data: LoginResponseDto = { _id: result._id, accessToken }; @@ -258,6 +268,53 @@ const postUserLogin = async ( } }; +const getAccessToken = async ( + req: TypedRequest +): Promise => { + const { vendor } = req.body; + let accessToken = req.body.accessToken; + + if (vendor == SocialVendor.NAVER) { + const { code, state } = req.body; + if (!code || !state) { + throw new IllegalArgumentException( + `${SocialVendor.NAVER}는 code와 state가 필요합니다.` + ); + } + accessToken = await NaverLoginService.getToken(code, state); + } + if (!accessToken) { + throw new IllegalStateException('accessToken이 없습니다.'); + } + return accessToken; +}; + +const socialLogin = async ( + req: TypedRequest, + res: Response, + next: NextFunction +) => { + try { + const { vendor } = req.body; + const accessToken = await getAccessToken(req); + const socialUser: UserDocument = + await SocialAuthService.findOrCreateUserBySocialToken( + vendor, + accessToken + ); + + const piickleJwt = getToken(socialUser._id); + return res.status(statusCode.OK).send( + util.success(statusCode.OK, message.USER_LOGIN_SUCCESS, { + _id: socialUser._id, + accessToken: piickleJwt + }) + ); + } catch (err) { + next(err); + } +}; + /** * @route get /users * @desc 유저 프로필 조회 @@ -445,8 +502,9 @@ const deleteUser = async ( export { readEmailIsExisting, + socialLogin, postUser, - patchUser, + // patchUser, postUserLogin, getUserProfile, updateUserNickname, diff --git a/src/intefaces/user/LoginResponseDto.ts b/src/intefaces/user/LoginResponseDto.ts new file mode 100644 index 0000000..b6bdc9e --- /dev/null +++ b/src/intefaces/user/LoginResponseDto.ts @@ -0,0 +1,6 @@ +import { Types } from 'mongoose'; + +export default interface LoginResponseDto { + _id: Types.ObjectId; + accessToken: string; +} diff --git a/src/intefaces/user/SocialLoginDto.ts b/src/intefaces/user/SocialLoginDto.ts new file mode 100644 index 0000000..37b7c5a --- /dev/null +++ b/src/intefaces/user/SocialLoginDto.ts @@ -0,0 +1,6 @@ +export default interface SocialLoginDto { + vendor: string; + accessToken?: string; + code?: string; + state?: string; +} diff --git a/src/intefaces/user/UserBookmarkDto.ts b/src/intefaces/user/UserBookmarkDto.ts index d86c897..a17387a 100644 --- a/src/intefaces/user/UserBookmarkDto.ts +++ b/src/intefaces/user/UserBookmarkDto.ts @@ -1,11 +1,9 @@ -import Types from 'mongoose'; +import { Types } from 'mongoose'; export interface UserBookmarkDto { cardId: Types.ObjectId; - content: { - type: string; - required: true; - }; - tags: [String]; - category: [Types.ObjectId]; - filter: [String]; + content: string; + tags: string[]; + category: Types.ObjectId[]; + filter: string[]; + isBookmark: boolean; } diff --git a/src/intefaces/user/UserProfileResponseDto.ts b/src/intefaces/user/UserProfileResponseDto.ts index c83c51d..f2ccde5 100644 --- a/src/intefaces/user/UserProfileResponseDto.ts +++ b/src/intefaces/user/UserProfileResponseDto.ts @@ -1,9 +1,5 @@ -/** - * deprecated: 이제 안쓸ㄹ거임 - */ export interface UserProfileResponseDto { - name: string; nickname: string; - email: string; + email?: string; profileImageUrl: string; } diff --git a/src/loaders/db.ts b/src/loaders/db.ts index 636b89d..6377d18 100644 --- a/src/loaders/db.ts +++ b/src/loaders/db.ts @@ -3,7 +3,7 @@ import config from '../config'; import Bookmark from '../models/bookmark'; import Card from '../models/card'; import { Category } from '../models/category'; -import User from '../models/user'; +import User from '../models/user/user'; import EmailAuth from '../models/emailAuth'; import { BallotTopic } from '../models/ballotTopic'; import BallotItem from '../models/ballotItem'; diff --git a/src/models/interface/IUser.ts b/src/models/interface/IUser.ts index 0b11679..774263f 100644 --- a/src/models/interface/IUser.ts +++ b/src/models/interface/IUser.ts @@ -1,10 +1,12 @@ import { Types } from 'mongoose'; interface IUser { - email: string; + email?: string; + socialId?: string; + socialVendor?: string; nickname: string; - hashedPassword: string; - birthday: Date; + hashedPassword?: string; + ageGroup?: string; gender: string; profileImageUrl: string; cardIdList: Types.ObjectId[]; diff --git a/src/models/socialAccount.ts b/src/models/socialAccount.ts new file mode 100644 index 0000000..cdc2378 --- /dev/null +++ b/src/models/socialAccount.ts @@ -0,0 +1,6 @@ +import { SocialVendor } from './socialVendor'; + +export default interface SocialAccount { + vendor: SocialVendor; + id: string; +} diff --git a/src/models/socialVendor.ts b/src/models/socialVendor.ts new file mode 100644 index 0000000..17a7211 --- /dev/null +++ b/src/models/socialVendor.ts @@ -0,0 +1,65 @@ +import { IllegalArgumentException } from '../intefaces/exception'; +import KakaoResponse, { + kakaoToPiickleUser +} from '../services/dto/KakaoResponse'; +import NaverResponse, { + naverToPiickleUser +} from '../services/dto/INaverResponse'; +import { SocialLoginResponse } from '../services/dto/socialLoginResponse'; +import IUser from './interface/IUser'; + +enum SocialVendor { + NAVER = 'naver', + KAKAO = 'kakao' +} + +class SocialVendorExtension { + private static nameToVendor: Map = new Map([ + ['naver', SocialVendor.NAVER], + ['kakao', SocialVendor.KAKAO] + ]); + + static vendorToUrl: Map = new Map([ + [SocialVendor.NAVER, 'https://openapi.naver.com/v1/nid/me'], + [SocialVendor.KAKAO, 'https://kapi.kakao.com/v2/user/me'] + ]); + + static getVendorByValue(value: string): SocialVendor { + const socialVendor = this.nameToVendor.get(value); + if (!socialVendor) { + throw new IllegalArgumentException('해당 이름의 소셜 서비스가 없습니다.'); + } + return socialVendor; + } + + static getUrlByVendor(value: string): string { + const socialVendor = SocialVendorExtension.nameToVendor.get(value); + if (!socialVendor) { + throw new IllegalArgumentException('소셜 서비스가 올바르지 않습니다.'); + } + const url = SocialVendorExtension.vendorToUrl.get(socialVendor)!; + return url; + } + + static socialUserConverter = new Map< + SocialVendor, + (response: SocialLoginResponse) => IUser + >([ + [ + SocialVendor.NAVER, + (response: SocialLoginResponse) => { + const data = response.data as unknown as NaverResponse; + return naverToPiickleUser(data); + } + ], + [ + SocialVendor.KAKAO, + (response: SocialLoginResponse) => { + const data: KakaoResponse = response.data as unknown as KakaoResponse; + return kakaoToPiickleUser(data); + } + ] + ]); +} + +export { SocialVendor, SocialVendorExtension }; diff --git a/src/models/user.ts b/src/models/user.ts deleted file mode 100644 index 40ff419..0000000 --- a/src/models/user.ts +++ /dev/null @@ -1,49 +0,0 @@ -import IDocument from './interface/Document'; -import IUser from './interface/IUser'; -import { model, Schema } from 'mongoose'; -import config from '../config'; - -type UserDocument = IUser & IDocument; - -const userSchema = new Schema( - { - email: { - type: String, - required: true, - unique: true - }, - hashedPassword: { - type: String, - required: true - }, - nickname: { - type: String, - required: true - }, - birthday: { - type: Date - }, - gender: { - type: String, - default: '기타' - }, - profileImageUrl: { - type: String, - required: false, - default: config.defaultProfileImgUrl - }, - cardIdList: [ - { - type: Schema.Types.ObjectId, - ref: 'Card' - } - ] - }, - { - timestamps: true - } -); - -const User = model('User', userSchema); - -export default User; diff --git a/src/models/user/ageGroup.ts b/src/models/user/ageGroup.ts new file mode 100644 index 0000000..d86b873 --- /dev/null +++ b/src/models/user/ageGroup.ts @@ -0,0 +1,12 @@ +enum AgeGroup { + CHILDREN = '0-13', // 14세 미만(0-13) + ADOLECENT = '14-19', // 14세 이상(14-19) + TWENTIES = '20-29', // 20대(20-29) + THIRTIES = '30-39', // 30대(30-39) + FOURTIES = '40-49', // 40대(40-49) + FIFTIES = '50-59', // 50대(50-59) + OVER_SIXTIES = '60-', // 60대 이상(60 - ) + UNDEFINED = '정의 안 함' +} + +export { AgeGroup }; diff --git a/src/models/user/gender.ts b/src/models/user/gender.ts new file mode 100644 index 0000000..9720d22 --- /dev/null +++ b/src/models/user/gender.ts @@ -0,0 +1,7 @@ +enum Gender { + MALE = '남', + FEMALE = '여', + ETC = '기타' +} + +export { Gender }; diff --git a/src/models/user/user.ts b/src/models/user/user.ts new file mode 100644 index 0000000..0237d6d --- /dev/null +++ b/src/models/user/user.ts @@ -0,0 +1,69 @@ +import IDocument from '../interface/Document'; +import IUser from '../interface/IUser'; +import { model, Schema } from 'mongoose'; +import config from '../../config'; +import { SocialVendor } from '../socialVendor'; +import { AgeGroup } from './ageGroup'; +import { Gender } from './gender'; + +type UserDocument = IUser & IDocument; + +const userSchema = new Schema( + { + email: { + type: String, + unique: true + }, + socialId: { + type: String, + unique: false + }, + socialVendor: { + type: String, + enum: SocialVendor, + unique: false + }, + hashedPassword: { + type: String, + required: false + }, + nickname: { + type: String, + required: true, + unique: true + }, + ageGroup: { + type: String, + enum: AgeGroup, + default: AgeGroup.UNDEFINED, + required: false + }, + gender: { + type: String, + enum: Gender, + default: Gender.ETC + }, + profileImageUrl: { + type: String, + required: false, + default: config.defaultProfileImgUrl + }, + cardIdList: [ + { + type: Schema.Types.ObjectId, + ref: 'Card' + } + ] + }, + { + timestamps: true + } +); + +userSchema.index({ socialVendor: 1, socialId: 1 }, { unique: true }); + +const User = model('User', userSchema); + +export default User; + +export { UserDocument }; diff --git a/src/routes/testRouter.ts b/src/routes/testRouter.ts index c979924..c70cb4d 100644 --- a/src/routes/testRouter.ts +++ b/src/routes/testRouter.ts @@ -1,5 +1,5 @@ import { Router } from 'express'; -import User from '../models/user'; +import User from '../models/user/user'; import util from '../modules/util'; import { TypedRequest } from '../types/TypedRequest'; import config from '../config'; diff --git a/src/routes/userRouter.ts b/src/routes/userRouter.ts index 607b93f..a2cc46d 100644 --- a/src/routes/userRouter.ts +++ b/src/routes/userRouter.ts @@ -27,13 +27,15 @@ router.post( UserController.postUser ); -router.patch( - '', - auth, - upload.single('imgFile'), - [body('nickname').notEmpty(), body('birthday').notEmpty()], - UserController.patchUser -); +// router.patch( +// '', +// auth, +// upload.single('imgFile'), +// [body('nickname').notEmpty(), body('birthday').notEmpty()], +// UserController.patchUser +// ); + +router.post('/social', UserController.socialLogin); router.post( '/login', diff --git a/src/services/authService.ts b/src/services/authService.ts index 7e75e35..c35c52f 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -1,8 +1,8 @@ import { - getAuth, createUserWithEmailAndPassword, - signInWithEmailAndPassword, - sendEmailVerification + getAuth, + sendEmailVerification, + signInWithEmailAndPassword } from 'firebase/auth'; import { InternalServerError } from '../intefaces/exception'; import firebase from '../loaders/firebase'; diff --git a/src/services/dto/INaverResponse.ts b/src/services/dto/INaverResponse.ts new file mode 100644 index 0000000..319518e --- /dev/null +++ b/src/services/dto/INaverResponse.ts @@ -0,0 +1,79 @@ +import IUser from '../../models/interface/IUser'; +import { SocialVendor } from '../../models/socialVendor'; +import config from '../../config'; +import { AgeGroup } from '../../models/user/ageGroup'; +import { Gender } from '../../models/user/gender'; + +export default interface NaverResponse { + message: string; + response: { + email: string; + /** + * nickname + * (별명이 설정되어 있지 않으면 id*** 형태로 리턴됩니다.) + */ + nickname: string; + profile_image?: string; + age?: string; + /** + * gender + * - F: 여성 + * - M: 남성 + * - U: 확인불가 + */ + gender?: string; + id: string; + name?: string; + birthday?: string; + birthyear?: string; + mobile?: string; + }; +} + +const toPiickleGender = (naverGender?: string): Gender => { + switch (naverGender) { + case 'F': + return Gender.FEMALE; + case 'M': + return Gender.MALE; + default: + return Gender.ETC; + } +}; + +const toPiickleAgeGroup = (age?: string): AgeGroup => { + switch (age) { + case '0-9': + case '10-19': + return AgeGroup.ADOLECENT; + case '20-29': + return AgeGroup.TWENTIES; + case '30-39': + return AgeGroup.THIRTIES; + case '40-49': + return AgeGroup.FOURTIES; + case '50-59': + return AgeGroup.FIFTIES; + case '60-': + return AgeGroup.OVER_SIXTIES; + default: + return AgeGroup.UNDEFINED; + } +}; + +const naverToPiickleUser = (naverUser: NaverResponse): IUser => { + const profileImageUrl = + naverUser.response.profile_image || config.defaultProfileImgUrl; + return { + socialId: naverUser.response.id, + socialVendor: SocialVendor.NAVER, + email: naverUser.response.email, + nickname: naverUser.response.nickname, + ageGroup: toPiickleAgeGroup(naverUser.response.age), + gender: toPiickleGender(naverUser.response.gender), + profileImageUrl, + cardIdList: [] + }; +}; + +export { naverToPiickleUser }; diff --git a/src/services/dto/KakaoResponse.ts b/src/services/dto/KakaoResponse.ts new file mode 100644 index 0000000..3b71d24 --- /dev/null +++ b/src/services/dto/KakaoResponse.ts @@ -0,0 +1,90 @@ +import IUser from '../../models/interface/IUser'; +import User, { UserDocument } from '../../models/user/user'; +import { SocialVendor } from '../../models/socialVendor'; +import config from '../../config'; +import { Gender } from '../../models/user/gender'; +import { AgeGroup } from '../../models/user/ageGroup'; + +export default interface KakaoResponse { + id: string; + kakao_account: { + profile_nickname_needs_agreement: boolean; + profile_image_needs_agreement: boolean; + profile: { + nickname?: string; + thumbnail_image_url?: string; + profile_image_url?: string; + is_default_image?: boolean; + }; + has_email: boolean; + email_needs_agreement: boolean; + is_email_valid: boolean; + is_email_verified: boolean; + email?: string; + has_age_range: boolean; + age_range_needs_agreement: boolean; + age_range: string; //"20~29" + has_birthday: boolean; + birthday_needs_agreement: boolean; + birthday: string; //"1103" + birthday_type: string; //"SOLAR", + has_gender: boolean; + gender_needs_agreement: boolean; + gender: string; // "female" + }; +} + +const toPiickleGender = (gender: string): Gender => { + if (gender == 'female') { + return Gender.FEMALE; + } + if (gender == 'male') { + return Gender.MALE; + } + return Gender.ETC; +}; + +const toPiickleAgeGroup = (ageGroup: string): AgeGroup => { + switch (ageGroup) { + case '1~9': + case '10~14': + return AgeGroup.CHILDREN; + case '15~19': + return AgeGroup.ADOLECENT; + case '20~29': + return AgeGroup.TWENTIES; + case '30~39': + return AgeGroup.THIRTIES; + case '40~49': + return AgeGroup.FOURTIES; + case '50~59': + return AgeGroup.FIFTIES; + case '60~69': + case '70~79': + case '80~89': + return AgeGroup.OVER_SIXTIES; + default: + return AgeGroup.UNDEFINED; + } +}; + +const kakaoToPiickleUser = (kakaoUser: KakaoResponse): IUser => { + const nickname = + kakaoUser.kakao_account.profile.nickname || kakaoUser.id + 'kakao'; + const profileImageUrl = + kakaoUser.kakao_account.profile.profile_image_url || + config.defaultProfileImgUrl; + + return { + socialId: kakaoUser.id, + socialVendor: SocialVendor.KAKAO, + email: kakaoUser.kakao_account.email, + nickname, + gender: toPiickleGender(kakaoUser.kakao_account.gender), + ageGroup: toPiickleAgeGroup(kakaoUser.kakao_account.age_range), + profileImageUrl, + cardIdList: [] + }; +}; + +export { kakaoToPiickleUser }; diff --git a/src/services/dto/socialLoginResponse.ts b/src/services/dto/socialLoginResponse.ts new file mode 100644 index 0000000..567694f --- /dev/null +++ b/src/services/dto/socialLoginResponse.ts @@ -0,0 +1,5 @@ +import { AxiosResponse } from 'axios'; + +type SocialLoginResponse = AxiosResponse<{ data: any }>; + +export { SocialLoginResponse }; diff --git a/src/services/index.ts b/src/services/index.ts index ba7f844..5b078a1 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -5,6 +5,8 @@ import * as CardService from './cardService'; import * as PreUserService from './preUserService'; import * as AuthService from './authService'; import * as CardMedleyService from './cardMedleyService'; +import * as SocialAuthService from './socialAuthService'; +import * as NaverLoginService from './naverLoginService'; export { AuthService, @@ -13,5 +15,7 @@ export { BallotService, CardService, PreUserService, - CardMedleyService + CardMedleyService, + SocialAuthService, + NaverLoginService }; diff --git a/src/services/naverLoginService.ts b/src/services/naverLoginService.ts new file mode 100644 index 0000000..ebf85cd --- /dev/null +++ b/src/services/naverLoginService.ts @@ -0,0 +1,23 @@ +import axios from 'axios'; +import config from '../config'; + +interface NaverTokenResponse { + access_token: string; + refresh_token: string; + token_type: string; + expires_in: string; +} +const getToken = async (code: string, state: string): Promise => { + const response = await axios.get( + 'https://nid.naver.com/oauth2.0/token?' + + 'grant_type=authorization_code' + + `&client_id=${config.naverClientId}` + + `&client_secret=${config.naverClientSecret}` + + `&code=${code}` + + `&state=${state}`, + {} + ); + return response.data.access_token; +}; + +export { getToken }; diff --git a/src/services/preUserService.ts b/src/services/preUserService.ts index eb05698..1c439bb 100644 --- a/src/services/preUserService.ts +++ b/src/services/preUserService.ts @@ -1,4 +1,4 @@ -import User from '../models/user'; +import User from '../models/user/user'; import PreUser from '../models/preUser'; import { IllegalStateException } from '../intefaces/exception'; diff --git a/src/services/socialAuthService.ts b/src/services/socialAuthService.ts new file mode 100644 index 0000000..cb744eb --- /dev/null +++ b/src/services/socialAuthService.ts @@ -0,0 +1,101 @@ +import axios, { AxiosResponse } from 'axios'; +import { SocialVendor, SocialVendorExtension } from '../models/socialVendor'; +import { SocialLoginResponse } from './dto/socialLoginResponse'; +import IUser from '../models/interface/IUser'; +import User, { UserDocument } from '../models/user/user'; +import { Nullable } from '../types/types'; +import { + IllegalArgumentException, + IllegalStateException +} from '../intefaces/exception'; +import { UserService } from './index'; + +const getUserFromSocialVendor = async (token: string, vendor: string) => { + const response: AxiosResponse = await axios({ + method: 'get', + url: SocialVendorExtension.getUrlByVendor(vendor), + headers: { + Authorization: `Bearer ${token}` + } + }); + return response as SocialLoginResponse; +}; + +const convertSocialToPiickle = ( + socialVendor: SocialVendor, + response: AxiosResponse<{ data: any }> +) => { + const piickleUserMaker: (response: SocialLoginResponse) => IUser = + SocialVendorExtension.socialUserConverter.get(socialVendor)!; + return piickleUserMaker(response); +}; + +const isAlreadySocialMember = async (socialMember: IUser): Promise => { + const isExisting = await User.exists({ + socialVendor: socialMember.socialVendor!, + socialId: socialMember.socialId! + }); + if (isExisting) { + return true; + } + return false; +}; + +const findUserBySocial = async ( + vendor: SocialVendor, + socialId: string +): Promise => { + const user: Nullable = await User.findOne({ + socialId: socialId + }); + if (!user) { + throw new IllegalArgumentException( + `User not found. vendor: ${vendor}, socialId: ${socialId}` + ); + } + if (user.socialVendor !== vendor) { + throw new IllegalStateException( + `Invalid Vendor: ${vendor}. Use '${ + user?.socialVendor || 'local login' + }' instead.` + ); + } + return user; +}; + +const join = async (socialMember: IUser): Promise => { + if (socialMember.email) { + const alreadyEmail = await UserService.isEmailExisting(socialMember.email); + if (alreadyEmail) { + throw new IllegalArgumentException( + `같은 이메일로 가입된 유저가 있습니다. email: ${socialMember.email}` + ); + } + } + socialMember.nickname = await UserService.autoGenerateNicknameFrom( + socialMember.nickname + ); + const newMember = new User(socialMember); + await newMember.save(); + return newMember; +}; + +const findOrCreateUserBySocialToken = async (vendor: string, token: string) => { + const response: SocialLoginResponse = await getUserFromSocialVendor( + token, + vendor + ); + const socialVendor = SocialVendorExtension.getVendorByValue(vendor); + const socialMember: IUser = convertSocialToPiickle(socialVendor, response); + const isAlreadyMember = await isAlreadySocialMember(socialMember); + if (isAlreadyMember) { + const alreadyMember = await findUserBySocial( + socialVendor, + socialMember.socialId! + ); + return alreadyMember; + } + return join(socialMember); +}; + +export { findOrCreateUserBySocialToken, findUserBySocial }; diff --git a/src/services/userService.ts b/src/services/userService.ts index 2266e34..7dc586e 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -1,6 +1,6 @@ import { compare, hashSync } from 'bcryptjs'; import { CreateUserCommand } from '../intefaces/createUserCommand'; -import User from '../models/user'; +import User from '../models/user/user'; import { BadCredentialException, DuplicateException, @@ -17,8 +17,8 @@ import { UserBookmarkDto } from '../intefaces/user/UserBookmarkDto'; import { UserBookmarkInfo } from '../intefaces/user/UserBookmarkInfo'; import Bookmark from '../models/bookmark'; import PreUser from '../models/preUser'; -import Card from '../models/card'; -import { UpdateUserDto } from '../intefaces/user/UpdateUserDto'; +import Card, { CardDocument } from '../models/card'; +// import { UpdateUserDto } from '../intefaces/user/UpdateUserDto'; import util from '../modules/util'; import QuitLog from '../models/quitLog'; @@ -64,18 +64,18 @@ const createUser = async (command: CreateUserCommand) => { return user; }; -const patchUser = async (updateUserDto: UpdateUserDto) => { - const user = await User.findById(updateUserDto.id); - if (!user) { - throw new IllegalArgumentException('해당 id의 유저가 존재하지 않습니다.'); - } - const { nickname, profileImgUrl, birthday, gender } = updateUserDto; - user.nickname = nickname; - user.birthday = util.stringToDate(birthday); - user.profileImageUrl = profileImgUrl ? profileImgUrl : user.profileImageUrl; - user.gender = gender ? gender : '기타'; - await user.save(); -}; +// const patchUser = async (updateUserDto: UpdateUserDto) => { +// const user = await User.findById(updateUserDto.id); +// if (!user) { +// throw new IllegalArgumentException('해당 id의 유저가 존재하지 않습니다.'); +// } +// const { nickname, profileImgUrl, birthday, gender } = updateUserDto; +// user.nickname = nickname; +// user.birthday = util.stringToDate(birthday); +// user.profileImageUrl = profileImgUrl ? profileImgUrl : user.profileImageUrl; +// user.gender = gender ? gender : '기타'; +// await user.save(); +// }; const loginUser = async ( userLoginDto: UserLoginDto @@ -89,7 +89,10 @@ const loginUser = async ( '존재하지 않는 email 입니다.' ); } - const isMatch = await compare(userLoginDto.password, user.hashedPassword); + if (user.socialId) { + throw new IllegalStateException('소셜로그인을 해주세요.'); + } + const isMatch = await compare(userLoginDto.password, user.hashedPassword!); if (!isMatch) { throw new BadCredentialException('비밀번호가 일치하지 않습니다.'); } @@ -107,7 +110,6 @@ const findUserById = async ( throw new IllegalArgumentException('존재하지 않는 유저 입니다.'); } return { - name: '김피클', nickname: user.nickname, email: user.email, profileImageUrl: user.profileImageUrl @@ -156,16 +158,17 @@ const getBookmarks = async ( return Promise.all( user.cardIdList.map(async (item: any) => { + const card = item; const isBookmark = - (await Bookmark.find({ user: userId, card: item._id }).count()) > 0 + (await Bookmark.find({ user: userId, card: card._id }).count()) > 0 ? true : false; return { - cardId: item._id, - content: item.content, - tags: item.tags, - category: item.category, - filter: item.filter, + cardId: card._id, + content: card.content, + tags: card.tags, + category: card.category, + filter: card.filter, isBookmark: isBookmark }; }) @@ -205,7 +208,7 @@ const createOrDeleteBookmark = async ( } }; -const nicknameDuplicationCheck = async (nickname: string) => { +const nicknameAlreadyExists = async (nickname: string) => { const user = await User.findOne({ nickname: nickname }); @@ -215,6 +218,14 @@ const nicknameDuplicationCheck = async (nickname: string) => { return false; }; +const autoGenerateNicknameFrom = async (nickname: string): Promise => { + let nicknameWithIncrement = nickname; + + for (let i = 1; await nicknameAlreadyExists(nicknameWithIncrement); i++) { + nicknameWithIncrement = nickname + i.toString(); + } + return nicknameWithIncrement; +}; const deleteUser = async (userId: Types.ObjectId, reasons: string[]) => { const user = await User.findById(userId); if (!user) { @@ -237,13 +248,14 @@ const deleteUser = async (userId: Types.ObjectId, reasons: string[]) => { export { isEmailExisting, createUser, - patchUser, + // patchUser, loginUser, findUserById, updateNickname, updateUserProfileImage, getBookmarks, createOrDeleteBookmark, - nicknameDuplicationCheck, + nicknameAlreadyExists, + autoGenerateNicknameFrom, deleteUser }; diff --git a/src/types/TypedResponse.ts b/src/types/TypedResponse.ts new file mode 100644 index 0000000..783aeae --- /dev/null +++ b/src/types/TypedResponse.ts @@ -0,0 +1,3 @@ +import { Response } from 'express'; + +export type TypedResponse = Response; diff --git a/test/cardMedleyService.spec.ts b/test/cardMedleyService.spec.ts index f39d335..82c00b8 100644 --- a/test/cardMedleyService.spec.ts +++ b/test/cardMedleyService.spec.ts @@ -3,11 +3,9 @@ import { getPreviewById } from '../src/services/cardMedleyService'; import connectDB from '../src/loaders/db'; -import chai from 'chai'; -import { assert } from 'chai'; +import chai, { assert } from 'chai'; import chaiThings from 'chai-things'; import CardMedley, { CardMedleyDocument } from '../src/models/cardMedley'; -import { Nullable } from '../src/types/types'; import CardMedleyPreviewDto from '../src/intefaces/CardMedleyPreviewDto'; chai.should(); @@ -38,7 +36,7 @@ describe('카드 메들리를 가져오는 서비스', () => { }); }); it('카드 메들리의 모든 정보가 포함돼있다.', async () => { - const result = await getCardsById(medleyDocument._id.toString(), null); + const result = await getCardsById(medleyDocument._id.toString(), undefined); assert.containsAllKeys(result, [ '_id', 'title', @@ -49,15 +47,15 @@ describe('카드 메들리를 가져오는 서비스', () => { ]); }); it('미리보기 카드가 3장 존재한다.', async () => { - const result = await getCardsById(medleyDocument._id.toString(), null); + const result = await getCardsById(medleyDocument._id.toString(), undefined); assert(result.previewCards.length == 3); }); it('카드가 비어있지 않다.', async () => { - const result = await getCardsById(medleyDocument._id.toString(), null); + const result = await getCardsById(medleyDocument._id.toString(), undefined); assert(result.cards.length != 0); }); it('카드 정보에는 북마크 여부가 포함된다.', async () => { - const result = await getCardsById(medleyDocument._id.toString(), null); + const result = await getCardsById(medleyDocument._id.toString(), undefined); result.cards.should.all.have.property('isBookmark'); }); }); diff --git a/test/user.spec.ts b/test/user.spec.ts index 09c64b6..9a84f02 100644 --- a/test/user.spec.ts +++ b/test/user.spec.ts @@ -1,15 +1,23 @@ -import User from '../src/models/user'; +import User from '../src/models/user/user'; import { before, after } from 'mocha'; import request from 'supertest'; import app from '../src/index'; import mongoose from 'mongoose'; -import path from 'path'; -import config from '../src/config'; +import dotenv from 'dotenv'; +import { IllegalStateException } from '../src/intefaces/exception'; +import LoginResponseDto from '../src/intefaces/user/LoginResponseDto'; + +dotenv.config({ path: `.env.test` }); +const mongoTestUri = process.env.MONGODB_URI; + +if (!mongoTestUri) { + throw new IllegalStateException('cannot read test database uri'); +} let jwtToken = 'Bearar '; before(async () => { try { - await mongoose.createConnection(config.mongoTestURI); + await mongoose.connect(mongoTestUri); } catch (err) { console.error(err); process.exit(1); @@ -39,6 +47,7 @@ describe('POST /users 유저 생성 API', () => { }); }); +type LoginResponse = { body: { data: LoginResponseDto } }; describe('POST /users/login 유저 로그인 API', () => { it('유저 로그인 성공', async () => { await request(app) @@ -48,8 +57,8 @@ describe('POST /users/login 유저 로그인 API', () => { email: 'test@gmail.com', password: 'test' }) - .expect(res => { - jwtToken = jwtToken + res.body.data.accessToken; + .expect((res: LoginResponse) => { + jwtToken = `Bearer ${res.body.data.accessToken}`; }) .expect(200) // 예측 상태 코드 .expect('Content-Type', /json/); // 예측 content-type diff --git a/tsconfig.json b/tsconfig.json index 1f573f1..5baf8e3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,10 +20,10 @@ ] }, "include": [ - "./src/**/*" // build 시 포함 + "./src/**/*", + "./test/**/*" // build 시 포함 ], "exclude": [ "node_modules", // build 시 제외 - "test" ] } From 2278cb6ba251dc645842ab35aa52d54c03728109 Mon Sep 17 00:00:00 2001 From: kyY00n Date: Thu, 29 Jun 2023 23:04:57 +0900 Subject: [PATCH 03/27] =?UTF-8?q?[CI]=20development=20script=EC=97=90=20sp?= =?UTF-8?q?rint=20=EB=B8=8C=EB=9E=9C=EC=B9=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-development.yml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy-development.yml b/.github/workflows/deploy-development.yml index e0fa593..c51dde2 100644 --- a/.github/workflows/deploy-development.yml +++ b/.github/workflows/deploy-development.yml @@ -2,12 +2,17 @@ name: build and push code on: push: branches: - - develop + - development + - sprint/** jobs: git-push: name: build and push runs-on: ubuntu-latest steps: + - name: Extract branch name + shell: bash + run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT + id: extract_branch - uses: actions/checkout@v2 - uses: borales/actions-yarn@v3.0.0 with: @@ -15,6 +20,9 @@ jobs: - uses: borales/actions-yarn@v3.0.0 with: cmd: build # will run `yarn build` command + - name: ls /dist + shell: bash + run: ls dist - uses: cpina/github-action-push-to-another-repository@main env: API_TOKEN_GITHUB: ${{ secrets.API_TOKEN_GITHUB }} @@ -23,11 +31,5 @@ jobs: destination-github-username: 'TeamPiickle' destination-repository-name: 'express-build' user-email: dev.kayoung@gmail.com - target-branch: develop -# deploy: -# name: deploy pm2 -# needs: git-push -# runs-on: [self-hosted, development] -# steps: -# - name: run shell script -# run: /home/ubuntu/deploy-piickle-server.sh + target-branch: ${{ steps.extract_branch.outputs.branch }} + create-target-branch-if-needed: true From a0b8ce0f80c185535b6a68bae0ce0cdf12b475b9 Mon Sep 17 00:00:00 2001 From: DongLee99 <55686088+DongLee99@users.noreply.github.com> Date: Tue, 4 Jul 2023 00:32:38 +0900 Subject: [PATCH 04/27] =?UTF-8?q?[HOTFIX]=20=EC=B9=B4=EB=93=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EC=8B=9C=20=EB=B6=81=EB=A7=88=ED=81=AC=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=EC=B6=94=EA=B0=80=20(#175)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [HOTFIX] 카드 조회시 북마크 여부 추가 --------- Co-authored-by: kyY00n --- src/controllers/cardController.ts | 13 +++++--- src/services/cardService.ts | 54 ++++++++++--------------------- 2 files changed, 25 insertions(+), 42 deletions(-) diff --git a/src/controllers/cardController.ts b/src/controllers/cardController.ts index 8101cf1..f300cea 100644 --- a/src/controllers/cardController.ts +++ b/src/controllers/cardController.ts @@ -37,8 +37,9 @@ const getBestCardList = async ( */ const getCards = async (req: Request, res: Response, next: NextFunction) => { try { + const userId: Types.ObjectId | undefined = req.user?.id; const cardId = new Types.ObjectId(req.params.cardId); - const card = await CardService.findCards(cardId, BEST_PIICKLE_SIZE); + const card = await CardService.findCards(cardId, BEST_PIICKLE_SIZE, userId); return res .status(statusCode.OK) .send(util.success(statusCode.OK, message.READ_CARD_SUCCESS, card)); @@ -58,8 +59,8 @@ const getRecentlyBookmarkedCard = async ( next: NextFunction ) => { try { - console.log('정신차려'); - const card = await CardService.findRecentlyBookmarkedCard(); + const userId: Types.ObjectId | undefined = req.user?.id; + const card = await CardService.findRecentlyBookmarkedCard(userId); return res .status(statusCode.OK) .send( @@ -85,7 +86,8 @@ const getRecentlyUpdatedCard = async ( next: NextFunction ) => { try { - const card = await CardService.findRecentlyUpdatedCard(); + const userId: Types.ObjectId | undefined = req.user?.id; + const card = await CardService.findRecentlyUpdatedCard(userId); return res .status(statusCode.OK) .send( @@ -112,7 +114,8 @@ const getCardByBookmarkedGender = async ( ) => { try { const gender = req.params.gender; - const card = await CardService.findCardByBookmarkedGender(gender); + const userId: Types.ObjectId | undefined = req.user?.id; + const card = await CardService.findCardByBookmarkedGender(gender, userId); return res .status(statusCode.OK) .send( diff --git a/src/services/cardService.ts b/src/services/cardService.ts index eea1351..6931120 100644 --- a/src/services/cardService.ts +++ b/src/services/cardService.ts @@ -1,8 +1,9 @@ -import Bookmark, { BookmarkDocument } from '../models/bookmark'; import Card, { CardDocument } from '../models/card'; +import Bookmark, { BookmarkDocument } from '../models/bookmark'; import util from '../modules/util'; import { Types } from 'mongoose'; import { CardResponseDto } from '../intefaces/CardResponseDto'; + interface CardIdAndCnt { _id: Types.ObjectId; count: number; @@ -68,7 +69,11 @@ const findBestCards = async (size: number, userId?: Types.ObjectId) => { return totalCards; }; -const findCards = async (cardId: Types.ObjectId, size: number) => { +const findCards = async ( + cardId: Types.ObjectId, + size: number, + userId?: Types.ObjectId +) => { const cards: CardDocument[] = await Card.find({ _id: cardId }); @@ -76,32 +81,18 @@ const findCards = async (cardId: Types.ObjectId, size: number) => { cards.length < size ? await findExtraCardsExceptFor(cards, size) : []; const totalCards: CardResponseDto[] = []; for (const card of [...cards, ...extraCards]) { - totalCards.push({ - _id: card._id, - content: card.content, - tags: card.tags, - category: card.category, - filter: card.filter, - isBookmark: false - }); + totalCards.push(await createCardResponse(card, userId)); } return totalCards; }; -const findRecentlyUpdatedCard = async () => { +const findRecentlyUpdatedCard = async (userId?: Types.ObjectId) => { const cards: CardDocument[] = await Card.aggregate() .sort({ createdAt: -1 }) .limit(20); const totalCards: CardResponseDto[] = []; for (const card of cards) { - totalCards.push({ - _id: card._id, - content: card.content, - tags: card.tags, - category: card.category, - filter: card.filter, - isBookmark: false - }); + totalCards.push(await createCardResponse(card, userId)); } return { recentlyDate: cards[0].createdAt, @@ -109,7 +100,7 @@ const findRecentlyUpdatedCard = async () => { }; }; -const findRecentlyBookmarkedCard = async () => { +const findRecentlyBookmarkedCard = async (userId?: Types.ObjectId) => { const bookmarks: BookmarkDocument[] = await Bookmark.aggregate() .sort({ createdAt: -1 }) .limit(20); @@ -118,14 +109,7 @@ const findRecentlyBookmarkedCard = async () => { ).filter(util.isNotEmpty); const totalCards: CardResponseDto[] = []; for (const card of cards) { - totalCards.push({ - _id: card._id, - content: card.content, - tags: card.tags, - category: card.category, - filter: card.filter, - isBookmark: false - }); + totalCards.push(await createCardResponse(card, userId)); } return { recentlyDate: bookmarks[0].createdAt, @@ -133,7 +117,10 @@ const findRecentlyBookmarkedCard = async () => { }; }; -const findCardByBookmarkedGender = async (gender: string) => { +const findCardByBookmarkedGender = async ( + gender: string, + userId?: Types.ObjectId +) => { const bookmarks = await Bookmark.aggregate() .lookup({ from: 'users', @@ -149,14 +136,7 @@ const findCardByBookmarkedGender = async (gender: string) => { ).filter(util.isNotEmpty); const totalCards: CardResponseDto[] = []; for (const card of cards) { - totalCards.push({ - _id: card._id, - content: card.content, - tags: card.tags, - category: card.category, - filter: card.filter, - isBookmark: false - }); + totalCards.push(await createCardResponse(card, userId)); } return totalCards; }; From 187911d6423cc08366c886aef95249fc5c6ccac8 Mon Sep 17 00:00:00 2001 From: kyY00n Date: Wed, 5 Jul 2023 13:59:27 +0900 Subject: [PATCH 05/27] =?UTF-8?q?[FEAT]=20=EC=B9=B4=EB=93=9C=20=EB=B8=94?= =?UTF-8?q?=EB=9E=99=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80/?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20(#177)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FEAT] 블랙리스트 몽고 스키마 추가 * [FEAT] 블랙리스트 추가/삭제 서비스 * [FEAT] api 라우팅 --- src/controllers/userController.ts | 62 ++++++++++++++++++- src/loaders/db.ts | 4 +- src/models/blockedCard.ts | 27 ++++++++ src/models/interface/IBlockedCard.ts | 6 ++ src/modules/responseMessage.ts | 3 +- .../{userRouter.ts => userRouter/index.ts} | 9 ++- src/routes/userRouter/userCardRouter.ts | 9 +++ src/services/blockCardService.ts | 62 +++++++++++++++++++ src/services/index.ts | 2 + 9 files changed, 177 insertions(+), 7 deletions(-) create mode 100644 src/models/blockedCard.ts create mode 100644 src/models/interface/IBlockedCard.ts rename src/routes/{userRouter.ts => userRouter/index.ts} (86%) create mode 100644 src/routes/userRouter/userCardRouter.ts create mode 100644 src/services/blockCardService.ts diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index ce6a895..a4187f0 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -20,7 +20,8 @@ import { PreUserService, AuthService, SocialAuthService, - NaverLoginService + NaverLoginService, + BlockCardService } from '../services'; import { UserProfileResponseDto } from '../intefaces/user/UserProfileResponseDto'; import { UserUpdateNicknameDto } from '../intefaces/user/UserUpdateNicknameDto'; @@ -34,6 +35,7 @@ import SocialLoginDto from '../intefaces/user/SocialLoginDto'; import LoginResponseDto from '../intefaces/user/LoginResponseDto'; import { UserDocument } from '../models/user/user'; import { SocialVendor } from '../models/socialVendor'; +import card from '../models/card'; /** * @route GET /email @@ -500,6 +502,60 @@ const deleteUser = async ( } }; +/** + * @route POST /users/cards/blacklist + * @desc 카드 블랙리스트 추가 + * @access public + */ +const blockCard = async ( + req: TypedRequest<{ cardId: Types.ObjectId }>, + res: Response, + next: NextFunction +) => { + try { + const userId = req.user?.id; + if (!userId) { + throw new IllegalArgumentException('유저 아이디가 없습니다.'); + } + const { cardId } = req.body; + await BlockCardService.blockCard(userId, cardId); + return res + .status(statusCode.OK) + .send(util.success(statusCode.OK, message.BLOCK_CARD_SUCCESS)); + } catch (e) { + console.error(e); + next(e); + } +}; + +/** + * @route DELETE /users/cards/blacklist/:cardId + * @desc 카드 블랙리스트 추가 + * @access public + */ +const cancelToBlock = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const userId = req.user?.id; + if (!userId) { + throw new IllegalArgumentException('유저 아이디가 없습니다.'); + } + const cardId = new Types.ObjectId(req.params.cardId); + if (!cardId) { + throw new IllegalArgumentException('카드 아이디가 없습니다.'); + } + await BlockCardService.cancelToBlockCard(userId, cardId); + return res + .status(statusCode.OK) + .send(util.success(statusCode.OK, message.CANCEL_BLOCK_CARD_SUCCESS)); + } catch (e) { + next(e); + } +}; + export { readEmailIsExisting, socialLogin, @@ -515,5 +571,7 @@ export { verifyEmail, verifyEmailTest, nicknameDuplicationCheck, - deleteUser + deleteUser, + blockCard, + cancelToBlock }; diff --git a/src/loaders/db.ts b/src/loaders/db.ts index 6377d18..04d55d2 100644 --- a/src/loaders/db.ts +++ b/src/loaders/db.ts @@ -12,6 +12,7 @@ import PreUser from '../models/preUser'; import QuitLog from '../models/quitLog'; import BestCard from '../models/bestCard'; import CardMedley from '../models/cardMedley'; +import { BlockedCard } from '../models/blockedCard'; const createCollection = (model: Model) => { model.createCollection().catch(err => { @@ -35,7 +36,8 @@ const connectDB = async () => { QuitLog, EmailAuth, BestCard, - CardMedley + CardMedley, + BlockedCard ]; models.forEach(model => { diff --git a/src/models/blockedCard.ts b/src/models/blockedCard.ts new file mode 100644 index 0000000..1bed0c0 --- /dev/null +++ b/src/models/blockedCard.ts @@ -0,0 +1,27 @@ +import IBlockedCard from './interface/IBlockedCard'; +import Document from './interface/Document'; +import { model, Schema } from 'mongoose'; + +type BlockedCardDocument = IBlockedCard & Document; + +const blockedCardSchema = new Schema({ + user: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true + }, + card: { + type: Schema.Types.ObjectId, + ref: 'Card', + required: true + } +}); + +blockedCardSchema.index({ user: 1, card: 1 }, { unique: true }); + +const BlockedCard = model( + 'BlockedCard', + blockedCardSchema +); + +export { BlockedCardDocument, BlockedCard }; diff --git a/src/models/interface/IBlockedCard.ts b/src/models/interface/IBlockedCard.ts new file mode 100644 index 0000000..ea577f9 --- /dev/null +++ b/src/models/interface/IBlockedCard.ts @@ -0,0 +1,6 @@ +import { Types } from 'mongoose'; + +export default interface IBlockedCard { + user: Types.ObjectId; + card: Types.ObjectId; +} diff --git a/src/modules/responseMessage.ts b/src/modules/responseMessage.ts index 97daca4..373e47d 100644 --- a/src/modules/responseMessage.ts +++ b/src/modules/responseMessage.ts @@ -28,7 +28,8 @@ const message = { READ_RECENTLY_BOOKMARKED_CARD_SUCCESS: '최근 북마크 된 카드 읽어오기 성공', READ_GENDER_BEST_CARD_SUCCESS: '성별 별 베스트 카드 읽어오기 성공', READ_BEST_CARDS_SUCCESS: '베스트 카드 읽어오기 성공', - READ_ONE_CARD_SUCCESS: '카드 한 장 읽어오기 성공', + BLOCK_CARD_SUCCESS: '카드 블랙리스트 추가 성공', + CANCEL_BLOCK_CARD_SUCCESS: '카드 블랙리스트 제외 성공', //카드 메들리 READ_CARD_MEDLEY_DETAIL_SUCCESS: '카드 메들리 상세 조회 성공', diff --git a/src/routes/userRouter.ts b/src/routes/userRouter/index.ts similarity index 86% rename from src/routes/userRouter.ts rename to src/routes/userRouter/index.ts index a2cc46d..3bc9454 100644 --- a/src/routes/userRouter.ts +++ b/src/routes/userRouter/index.ts @@ -1,11 +1,14 @@ import { Router } from 'express'; import { body } from 'express-validator'; -import { UserController } from '../controllers'; -import auth from '../middlewares/auth'; -import upload from '../config/multer'; +import { UserController } from '../../controllers'; +import auth from '../../middlewares/auth'; +import upload from '../../config/multer'; +import userCardRouter from './userCardRouter'; const router = Router(); +router.use('/cards', userCardRouter); + router.get('/existing', UserController.readEmailIsExisting); router.post( diff --git a/src/routes/userRouter/userCardRouter.ts b/src/routes/userRouter/userCardRouter.ts new file mode 100644 index 0000000..d734dc7 --- /dev/null +++ b/src/routes/userRouter/userCardRouter.ts @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import { UserController } from '../../controllers'; +import auth from '../../middlewares/auth'; + +const router = Router(); + +router.post('/blacklist', auth, UserController.blockCard); +router.delete('/blacklist/:cardId', auth, UserController.cancelToBlock); +export default router; diff --git a/src/services/blockCardService.ts b/src/services/blockCardService.ts new file mode 100644 index 0000000..542b377 --- /dev/null +++ b/src/services/blockCardService.ts @@ -0,0 +1,62 @@ +import { Types, MongooseError } from 'mongoose'; +import User from '../models/user/user'; +import { IllegalArgumentException } from '../intefaces/exception'; +import Card from '../models/card'; +import { BlockedCard } from '../models/blockedCard'; + +const validateUserId = async (userId: Types.ObjectId) => { + const user = await User.findById(userId); + if (!user) { + throw new IllegalArgumentException( + `userId: ${userId.toString()} 해당 아이디의 유저를 찾을 수 없습니다.` + ); + } +}; + +const validateCardId = async (cardId: Types.ObjectId) => { + const card = await Card.findById(cardId); + if (!card) { + throw new IllegalArgumentException( + `cardId: ${cardId.toString()} 해당 아이디의 카드를 찾을 수 없습니다.` + ); + } +}; + +const handleErrorCausedByDuplicatedKey = (error: any) => { + const mongooseError = error as { code: number }; + if (mongooseError.code == 11000) { + throw new IllegalArgumentException('이미 블랙리스트에 있는 카드입니다.'); + } +} + +const blockCard = async (userId: Types.ObjectId, cardId: Types.ObjectId) => { + await validateUserId(userId); + await validateCardId(cardId); + const newBlockedCard = new BlockedCard({ + user: userId, + card: cardId + }); + try { + await newBlockedCard.save(); + } catch (e) { + handleErrorCausedByDuplicatedKey(e); + throw e; + } +}; + +const cancelToBlockCard = async ( + userId: Types.ObjectId, + cardId: Types.ObjectId +) => { + await validateUserId(userId); + await validateCardId(cardId); + const deleted = await BlockedCard.findOneAndDelete({ + user: userId, + card: cardId + }); + if (!deleted) { + throw new IllegalArgumentException('블랙리스트에 없는 카드입니다.'); + } +}; + +export { blockCard, cancelToBlockCard }; diff --git a/src/services/index.ts b/src/services/index.ts index 5b078a1..7a98841 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -7,6 +7,7 @@ import * as AuthService from './authService'; import * as CardMedleyService from './cardMedleyService'; import * as SocialAuthService from './socialAuthService'; import * as NaverLoginService from './naverLoginService'; +import * as BlockCardService from './blockCardService'; export { AuthService, @@ -14,6 +15,7 @@ export { CategoryService, BallotService, CardService, + BlockCardService, PreUserService, CardMedleyService, SocialAuthService, From 3259f09ba41ce1653dabe1db0f10c7fb3971f6af Mon Sep 17 00:00:00 2001 From: kyY00n Date: Fri, 7 Jul 2023 00:25:13 +0900 Subject: [PATCH 06/27] =?UTF-8?q?[FEAT]=20=EC=86=8C=EC=85=9C=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=9D=91=EB=8B=B5=EC=97=90=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=97=AC=EB=B6=80=20=EC=B6=94=EA=B0=80=20(#182)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/userController.ts | 73 +++++++++++-------------------- src/services/socialAuthService.ts | 27 +++++++++++- 2 files changed, 52 insertions(+), 48 deletions(-) diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index a4187f0..3b92efb 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -33,9 +33,8 @@ import EmailVerificationReqDto from '../intefaces/user/EmailVerificationReqDto'; import config from '../config'; import SocialLoginDto from '../intefaces/user/SocialLoginDto'; import LoginResponseDto from '../intefaces/user/LoginResponseDto'; -import { UserDocument } from '../models/user/user'; import { SocialVendor } from '../models/socialVendor'; -import card from '../models/card'; +import IUser from '../models/interface/IUser'; /** * @route GET /email @@ -203,41 +202,6 @@ const postUser = async ( } }; -/** - * @method PATCH - * @route /users - * @access Public - */ -// const patchUser = async (req: Request, res: Response, next: NextFunction) => { -// try { -// const userId = req.user?.id; -// if (!userId) { -// throw new IllegalArgumentException('로그인이 필요합니다.'); -// } -// const reqErr = validationResult(req); -// -// if (!reqErr.isEmpty()) { -// throw new IllegalArgumentException('필요한 값이 없습니다.'); -// } -// -// const input: UpdateUserDto = { -// id: userId, -// nickname: req.body.nickname, -// birthday: req.body.birthday, -// gender: req.body.gender, -// profileImgUrl: (req?.file as Express.MulterS3.File)?.location -// }; -// -// await UserService.patchUser(input); -// -// res -// .status(statusCode.OK) -// .send(util.success(statusCode.OK, message.USER_UPDATE_SUCCESS)); -// } catch (err) { -// next(err); -// } -// }; - /** * @route /users/login * @desc 로그인 api @@ -291,6 +255,18 @@ const getAccessToken = async ( return accessToken; }; +const findOrCreateSocialUser = async ( + alreadyMember: boolean, + buildUser: IUser +) => { + if (alreadyMember) { + const foundUser = await SocialAuthService.findSocialUser(buildUser); + return foundUser; + } + const newUser = await SocialAuthService.join(buildUser); + return newUser; +}; + const socialLogin = async ( req: TypedRequest, res: Response, @@ -298,18 +274,21 @@ const socialLogin = async ( ) => { try { const { vendor } = req.body; - const accessToken = await getAccessToken(req); - const socialUser: UserDocument = - await SocialAuthService.findOrCreateUserBySocialToken( - vendor, - accessToken - ); + const socialAccessToken = await getAccessToken(req); + const buildUser: IUser = await SocialAuthService.getPiickleUser( + vendor, + socialAccessToken + ); + const alreadyUser: boolean = await SocialAuthService.isAlreadySocialMember( + buildUser + ); + const socialUser = await findOrCreateSocialUser(alreadyUser, buildUser); - const piickleJwt = getToken(socialUser._id); return res.status(statusCode.OK).send( util.success(statusCode.OK, message.USER_LOGIN_SUCCESS, { _id: socialUser._id, - accessToken: piickleJwt + accessToken: getToken(socialUser._id), + newMember: !alreadyUser }) ); } catch (err) { @@ -549,8 +528,8 @@ const cancelToBlock = async ( } await BlockCardService.cancelToBlockCard(userId, cardId); return res - .status(statusCode.OK) - .send(util.success(statusCode.OK, message.CANCEL_BLOCK_CARD_SUCCESS)); + .status(statusCode.OK) + .send(util.success(statusCode.OK, message.CANCEL_BLOCK_CARD_SUCCESS)); } catch (e) { next(e); } diff --git a/src/services/socialAuthService.ts b/src/services/socialAuthService.ts index cb744eb..8e53191 100644 --- a/src/services/socialAuthService.ts +++ b/src/services/socialAuthService.ts @@ -80,6 +80,24 @@ const join = async (socialMember: IUser): Promise => { return newMember; }; +const getPiickleUser = async (vendor: string, token: string) => { + const response: SocialLoginResponse = await getUserFromSocialVendor( + token, + vendor + ); + const socialVendor = SocialVendorExtension.getVendorByValue(vendor); + const socialMember: IUser = convertSocialToPiickle(socialVendor, response); + return socialMember; +}; + +const findSocialUser = async (socialMember: IUser) => { + const alreadyMember = await findUserBySocial( + SocialVendorExtension.getVendorByValue(socialMember.socialVendor!), + socialMember.socialId! + ); + return alreadyMember; +}; + const findOrCreateUserBySocialToken = async (vendor: string, token: string) => { const response: SocialLoginResponse = await getUserFromSocialVendor( token, @@ -98,4 +116,11 @@ const findOrCreateUserBySocialToken = async (vendor: string, token: string) => { return join(socialMember); }; -export { findOrCreateUserBySocialToken, findUserBySocial }; +export { + findOrCreateUserBySocialToken, + findUserBySocial, + getPiickleUser, + isAlreadySocialMember, + join, + findSocialUser +}; From 40ce6cd0d9120e79c6201c404f87982fc2874784 Mon Sep 17 00:00:00 2001 From: donglee99 Date: Fri, 7 Jul 2023 14:23:08 +0900 Subject: [PATCH 07/27] =?UTF-8?q?[FEAT]=20=EB=B8=94=EB=9E=99=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=A0=9C=EC=99=B8=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(ctm=20=EB=B2=84=ED=8A=BC,=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=EB=B3=84=20=EC=B9=B4=EB=93=9C=20=EC=A1=B0=ED=9A=8C,?= =?UTF-8?q?=20=ED=95=84=ED=84=B0=20=EB=B3=84=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C,=20=EA=B3=B5=EC=9C=A0=20=EB=B0=9B=EC=9D=80?= =?UTF-8?q?=20=EC=B9=B4=EB=93=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/CategoryService.ts | 53 ++++++++++++++++++++++++++------- src/services/cardService.ts | 28 ++++++++++++++--- 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/src/services/CategoryService.ts b/src/services/CategoryService.ts index beb0395..61c51ae 100644 --- a/src/services/CategoryService.ts +++ b/src/services/CategoryService.ts @@ -6,6 +6,7 @@ import { CardResponseDto } from '../intefaces/CardResponseDto'; import Bookmark from '../models/bookmark'; import { IllegalArgumentException } from '../intefaces/exception'; import util from '../modules/util'; +import { BlockedCard, BlockedCardDocument } from '../models/blockedCard'; const CARD_SIZE_PER_REQUEST = 30; @@ -44,7 +45,19 @@ const getCardsWithIsBookmark = async ( if (!category) { throw new IllegalArgumentException('해당 id의 카테고리가 없습니다.'); } - const allCards = await Card.find({ category: categoryId }); + const blockedCards: BlockedCardDocument[] = await BlockedCard.find({ + user: userId + }); + const ninCards: Types.ObjectId[] = []; + if (blockedCards) { + for (const blockedCard of blockedCards) { + ninCards.push(blockedCard.card); + } + } + const allCards = await Card.find({ + category: categoryId, + _id: { $nin: ninCards } + }); const randomCards = getRandomUniqueNumbersInRange( allCards.length, @@ -85,8 +98,11 @@ const makeQueryOption = (search: string[]) => { return { filter: { $all: search } }; }; -const getRandomizedPrimaryCards = async () => { - const primaryCards = await Card.find({ filter: FILTER_R_RATED }); +const getRandomizedPrimaryCards = async (ninCards: Types.ObjectId[]) => { + const primaryCards = await Card.find({ + filter: FILTER_R_RATED, + _id: { $nin: ninCards } + }); const randomizedPrimaryCards = getRandomUniqueNumbersInRange( primaryCards.length, 4 @@ -96,9 +112,13 @@ const getRandomizedPrimaryCards = async () => { async function getFilteredCardsWithSize( search: string[], - primaryCardsSize: number + primaryCardsSize: number, + ninCards: Types.ObjectId[] ) { - const allCards = await Card.find({ filter: { $all: search } }); + const allCards = await Card.find({ + filter: { $all: search }, + _id: { $nin: ninCards } + }); const sizedCards = getRandomUniqueNumbersInRange( allCards.length, CARD_SIZE_PER_REQUEST - primaryCardsSize @@ -106,11 +126,19 @@ async function getFilteredCardsWithSize( return sizedCards; } -const getCard = async (filterKeywords: string[]): Promise => { +const getCard = async ( + filterKeywords: string[], + blockedCards?: BlockedCardDocument[] +): Promise => { const primaryCards = []; - + const ninCards: Types.ObjectId[] = []; + if (blockedCards) { + for (const blockedCard of blockedCards) { + ninCards.push(blockedCard.card); + } + } if (filterKeywords.includes(FILTER_R_RATED)) { - const randomizedPrimaryCards = await getRandomizedPrimaryCards(); + const randomizedPrimaryCards = await getRandomizedPrimaryCards(ninCards); primaryCards.push(...randomizedPrimaryCards); filterKeywords.splice(filterKeywords.indexOf(FILTER_R_RATED), 1); @@ -118,7 +146,8 @@ const getCard = async (filterKeywords: string[]): Promise => { const sizedCards = await getFilteredCardsWithSize( filterKeywords, - primaryCards.length + primaryCards.length, + ninCards ); return util.shuffle([...primaryCards, ...sizedCards]); }; @@ -128,8 +157,10 @@ const getFilteredCards = async ( userId?: Types.ObjectId ): Promise => { try { - const cardDocuments = await getCard(filterKeywords); - + const blockedCards: BlockedCardDocument[] = await BlockedCard.find({ + user: userId + }); + const cardDocuments = await getCard(filterKeywords, blockedCards); const cardIds = cardDocuments.map(e => e._id); const bookmarks = await Bookmark.find({ user: userId, diff --git a/src/services/cardService.ts b/src/services/cardService.ts index 6931120..912ec2d 100644 --- a/src/services/cardService.ts +++ b/src/services/cardService.ts @@ -3,6 +3,7 @@ import Bookmark, { BookmarkDocument } from '../models/bookmark'; import util from '../modules/util'; import { Types } from 'mongoose'; import { CardResponseDto } from '../intefaces/CardResponseDto'; +import { BlockedCard, BlockedCardDocument } from '../models/blockedCard'; interface CardIdAndCnt { _id: Types.ObjectId; @@ -47,9 +48,24 @@ const findBestCardsLimit = async (size: number) => { ).filter(util.isNotEmpty); }; -const findExtraCardsExceptFor = async (cards: CardDocument[], size: number) => { +const findExtraCardsExceptFor = async ( + cards: CardDocument[], + size: number, + blockedCards?: BlockedCardDocument[] +) => { + const ninCards: Types.ObjectId[] = []; + for (const card of cards) { + ninCards.push(card._id); + } + if (blockedCards) { + for (const blockedCard of blockedCards) { + ninCards.push(blockedCard.card); + } + } const extraCards: CardDocument[] = await Card.find({ - _id: { $nin: cards.map(c => c._id) } + _id: { + $nin: ninCards + } }) .sort('_id') .limit(size - cards.length); @@ -65,7 +81,6 @@ const findBestCards = async (size: number, userId?: Types.ObjectId) => { for (const card of [...cards, ...extraCards]) { totalCards.push(await createCardResponse(card, userId)); } - return totalCards; }; @@ -77,8 +92,13 @@ const findCards = async ( const cards: CardDocument[] = await Card.find({ _id: cardId }); + const blockedCards: BlockedCardDocument[] = await BlockedCard.find({ + user: userId + }); const extraCards = - cards.length < size ? await findExtraCardsExceptFor(cards, size) : []; + cards.length < size + ? await findExtraCardsExceptFor(cards, size, blockedCards) + : []; const totalCards: CardResponseDto[] = []; for (const card of [...cards, ...extraCards]) { totalCards.push(await createCardResponse(card, userId)); From a03d53b712ad80015681e7f3cec042701b859926 Mon Sep 17 00:00:00 2001 From: kyY00n Date: Sat, 8 Jul 2023 15:02:49 +0900 Subject: [PATCH 08/27] =?UTF-8?q?[HOTFIX]=20=EB=8B=89=EB=84=A4=EC=9E=84?= =?UTF-8?q?=EC=9D=B4=20=EC=97=86=EC=9D=84=20=EA=B2=BD=EC=9A=B0=20=EC=9E=84?= =?UTF-8?q?=EC=9D=98=EB=A1=9C=20=EC=A7=80=EC=A0=95=20(#185)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/models/socialVendor.ts | 2 +- src/services/dto/KakaoResponse.ts | 2 +- src/services/dto/{INaverResponse.ts => NaverResponse.ts} | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) rename src/services/dto/{INaverResponse.ts => NaverResponse.ts} (94%) diff --git a/src/models/socialVendor.ts b/src/models/socialVendor.ts index 17a7211..42951fe 100644 --- a/src/models/socialVendor.ts +++ b/src/models/socialVendor.ts @@ -4,7 +4,7 @@ import KakaoResponse, { } from '../services/dto/KakaoResponse'; import NaverResponse, { naverToPiickleUser -} from '../services/dto/INaverResponse'; +} from '../services/dto/NaverResponse'; import { SocialLoginResponse } from '../services/dto/socialLoginResponse'; import IUser from './interface/IUser'; diff --git a/src/services/dto/KakaoResponse.ts b/src/services/dto/KakaoResponse.ts index 3b71d24..758f1cd 100644 --- a/src/services/dto/KakaoResponse.ts +++ b/src/services/dto/KakaoResponse.ts @@ -70,7 +70,7 @@ const toPiickleAgeGroup = (ageGroup: string): AgeGroup => { const kakaoToPiickleUser = (kakaoUser: KakaoResponse): IUser => { const nickname = - kakaoUser.kakao_account.profile.nickname || kakaoUser.id + 'kakao'; + kakaoUser.kakao_account.profile.nickname || `kakao ${kakaoUser.id}`; const profileImageUrl = kakaoUser.kakao_account.profile.profile_image_url || config.defaultProfileImgUrl; diff --git a/src/services/dto/INaverResponse.ts b/src/services/dto/NaverResponse.ts similarity index 94% rename from src/services/dto/INaverResponse.ts rename to src/services/dto/NaverResponse.ts index 319518e..5b63071 100644 --- a/src/services/dto/INaverResponse.ts +++ b/src/services/dto/NaverResponse.ts @@ -62,13 +62,15 @@ const toPiickleAgeGroup = (age?: string): AgeGroup => { }; const naverToPiickleUser = (naverUser: NaverResponse): IUser => { + const nickname = + naverUser.response.nickname || `naver ${naverUser.response.id}`; const profileImageUrl = naverUser.response.profile_image || config.defaultProfileImgUrl; return { socialId: naverUser.response.id, socialVendor: SocialVendor.NAVER, email: naverUser.response.email, - nickname: naverUser.response.nickname, + nickname, ageGroup: toPiickleAgeGroup(naverUser.response.age), gender: toPiickleGender(naverUser.response.gender), profileImageUrl, From c63caba4b92381609107e99ddca579c62a8f3b56 Mon Sep 17 00:00:00 2001 From: kyY00n Date: Mon, 10 Jul 2023 12:29:02 +0900 Subject: [PATCH 09/27] =?UTF-8?q?[FEAT]=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EA=B8=B0=EB=8A=A5=20=EC=97=B0=EB=A0=B9=EB=8C=80=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20(#188)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/userController.ts | 3 ++- src/intefaces/createUserCommand.ts | 6 ++++-- src/models/user/ageGroup.ts | 18 ++++++++++-------- src/routes/userRouter/index.ts | 8 -------- src/services/dto/KakaoResponse.ts | 4 ++-- src/services/dto/NaverResponse.ts | 4 ++-- src/services/userService.ts | 20 ++------------------ 7 files changed, 22 insertions(+), 41 deletions(-) diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index 3b92efb..e19b368 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -35,6 +35,7 @@ import SocialLoginDto from '../intefaces/user/SocialLoginDto'; import LoginResponseDto from '../intefaces/user/LoginResponseDto'; import { SocialVendor } from '../models/socialVendor'; import IUser from '../models/interface/IUser'; +import {AgeGroup, AgeGroupKey} from '../models/user/ageGroup'; /** * @route GET /email @@ -185,7 +186,7 @@ const postUser = async ( email: req.body.email, password: req.body.password, nickname: req.body.nickname, - birthday: req.body.birthday, + ageGroup: AgeGroup[req.body.ageGroup], gender: req.body.gender, profileImgUrl: (req?.file as Express.MulterS3.File)?.location }; diff --git a/src/intefaces/createUserCommand.ts b/src/intefaces/createUserCommand.ts index 6be7f92..d4b2eff 100644 --- a/src/intefaces/createUserCommand.ts +++ b/src/intefaces/createUserCommand.ts @@ -1,8 +1,10 @@ +import {AgeGroup, AgeGroupKey} from '../models/user/ageGroup'; + interface CreateUserCommand { email: string; password: string; nickname: string; - birthday: string; + ageGroup: AgeGroup; gender: string; profileImgUrl: string; } @@ -11,7 +13,7 @@ interface CreateUserReq { email: string; password: string; nickname: string; - birthday: string; + ageGroup: AgeGroupKey; gender: string; } diff --git a/src/models/user/ageGroup.ts b/src/models/user/ageGroup.ts index d86b873..8996023 100644 --- a/src/models/user/ageGroup.ts +++ b/src/models/user/ageGroup.ts @@ -1,12 +1,14 @@ enum AgeGroup { - CHILDREN = '0-13', // 14세 미만(0-13) - ADOLECENT = '14-19', // 14세 이상(14-19) - TWENTIES = '20-29', // 20대(20-29) - THIRTIES = '30-39', // 30대(30-39) - FOURTIES = '40-49', // 40대(40-49) - FIFTIES = '50-59', // 50대(50-59) - OVER_SIXTIES = '60-', // 60대 이상(60 - ) + CHILDREN = '0-13', + ADOLESCENT = '14-19', + TWENTIES = '20-29', + THIRTIES = '30-39', + FORTIES = '40-49', + FIFTIES = '50-59', + OVER_SIXTIES = '60-', UNDEFINED = '정의 안 함' } -export { AgeGroup }; +type AgeGroupKey = keyof typeof AgeGroup; + +export { AgeGroup, AgeGroupKey }; diff --git a/src/routes/userRouter/index.ts b/src/routes/userRouter/index.ts index 3bc9454..33e5546 100644 --- a/src/routes/userRouter/index.ts +++ b/src/routes/userRouter/index.ts @@ -30,14 +30,6 @@ router.post( UserController.postUser ); -// router.patch( -// '', -// auth, -// upload.single('imgFile'), -// [body('nickname').notEmpty(), body('birthday').notEmpty()], -// UserController.patchUser -// ); - router.post('/social', UserController.socialLogin); router.post( diff --git a/src/services/dto/KakaoResponse.ts b/src/services/dto/KakaoResponse.ts index 758f1cd..aceeeaf 100644 --- a/src/services/dto/KakaoResponse.ts +++ b/src/services/dto/KakaoResponse.ts @@ -50,13 +50,13 @@ const toPiickleAgeGroup = (ageGroup: string): AgeGroup => { case '10~14': return AgeGroup.CHILDREN; case '15~19': - return AgeGroup.ADOLECENT; + return AgeGroup.ADOLESCENT; case '20~29': return AgeGroup.TWENTIES; case '30~39': return AgeGroup.THIRTIES; case '40~49': - return AgeGroup.FOURTIES; + return AgeGroup.FORTIES; case '50~59': return AgeGroup.FIFTIES; case '60~69': diff --git a/src/services/dto/NaverResponse.ts b/src/services/dto/NaverResponse.ts index 5b63071..46f4fc4 100644 --- a/src/services/dto/NaverResponse.ts +++ b/src/services/dto/NaverResponse.ts @@ -45,13 +45,13 @@ const toPiickleAgeGroup = (age?: string): AgeGroup => { switch (age) { case '0-9': case '10-19': - return AgeGroup.ADOLECENT; + return AgeGroup.ADOLESCENT; case '20-29': return AgeGroup.TWENTIES; case '30-39': return AgeGroup.THIRTIES; case '40-49': - return AgeGroup.FOURTIES; + return AgeGroup.FORTIES; case '50-59': return AgeGroup.FIFTIES; case '60-': diff --git a/src/services/userService.ts b/src/services/userService.ts index 7dc586e..081af64 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -18,8 +18,6 @@ import { UserBookmarkInfo } from '../intefaces/user/UserBookmarkInfo'; import Bookmark from '../models/bookmark'; import PreUser from '../models/preUser'; import Card, { CardDocument } from '../models/card'; -// import { UpdateUserDto } from '../intefaces/user/UpdateUserDto'; -import util from '../modules/util'; import QuitLog from '../models/quitLog'; const isEmailExisting = async (email: string): Promise => { @@ -46,7 +44,7 @@ const createUser = async (command: CreateUserCommand) => { email, password, nickname, - birthday: birthStr, + ageGroup, gender, profileImgUrl: profileImageUrl } = command; @@ -56,7 +54,7 @@ const createUser = async (command: CreateUserCommand) => { email, hashedPassword: hashSync(password, 10), nickname, - birthday: util.stringToDate(birthStr), + ageGroup, gender, profileImageUrl }); @@ -64,19 +62,6 @@ const createUser = async (command: CreateUserCommand) => { return user; }; -// const patchUser = async (updateUserDto: UpdateUserDto) => { -// const user = await User.findById(updateUserDto.id); -// if (!user) { -// throw new IllegalArgumentException('해당 id의 유저가 존재하지 않습니다.'); -// } -// const { nickname, profileImgUrl, birthday, gender } = updateUserDto; -// user.nickname = nickname; -// user.birthday = util.stringToDate(birthday); -// user.profileImageUrl = profileImgUrl ? profileImgUrl : user.profileImageUrl; -// user.gender = gender ? gender : '기타'; -// await user.save(); -// }; - const loginUser = async ( userLoginDto: UserLoginDto ): Promise => { @@ -248,7 +233,6 @@ const deleteUser = async (userId: Types.ObjectId, reasons: string[]) => { export { isEmailExisting, createUser, - // patchUser, loginUser, findUserById, updateNickname, From cd6f925d1473801da5c6bf3ba3162469cd3cc54d Mon Sep 17 00:00:00 2001 From: kyY00n Date: Sat, 22 Jul 2023 21:06:28 +0900 Subject: [PATCH 10/27] =?UTF-8?q?[CHORE]=20=EC=84=BC=ED=8A=B8=EB=A6=AC=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20(#189)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 147 ++++++++++++++++++++++++++++- package.json | 1 + src/config/index.ts | 4 +- src/index.ts | 22 +---- src/loaders/sentryConfiguration.ts | 29 ++++++ src/loaders/session.ts | 16 ++++ src/middlewares/errorHandler.ts | 9 +- src/modules/slackApi.ts | 10 -- src/routes/index.ts | 7 +- 9 files changed, 199 insertions(+), 46 deletions(-) create mode 100644 src/loaders/sentryConfiguration.ts create mode 100644 src/loaders/session.ts delete mode 100644 src/modules/slackApi.ts diff --git a/package-lock.json b/package-lock.json index 6c242e7..5aa82c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@sentry/node": "^7.57.0", "@slack/client": "^5.0.2", "@types/bcrypt": "^5.0.0", "@types/bcryptjs": "^2.4.2", @@ -1023,6 +1024,79 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@sentry-internal/tracing": { + "version": "7.57.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.57.0.tgz", + "integrity": "sha512-tpViyDd8AhQGYYhI94xi2aaDopXOPfL2Apwrtb3qirWkomIQ2K86W1mPmkce+B0cFOnW2Dxv/ZTFKz6ghjK75A==", + "dependencies": { + "@sentry/core": "7.57.0", + "@sentry/types": "7.57.0", + "@sentry/utils": "7.57.0", + "tslib": "^2.4.1 || ^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/core": { + "version": "7.57.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.57.0.tgz", + "integrity": "sha512-l014NudPH0vQlzybtXajPxYFfs9w762NoarjObC3gu76D1jzBBFzhdRelkGpDbSLNTIsKhEDDRpgAjBWJ9icfw==", + "dependencies": { + "@sentry/types": "7.57.0", + "@sentry/utils": "7.57.0", + "tslib": "^2.4.1 || ^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/node": { + "version": "7.57.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.57.0.tgz", + "integrity": "sha512-63mjyUVM6sfJFVQ5TGVRVGUsoEfESl5ABzIW1W0s9gUiQPaG8SOdaQJglb2VNrkMYxnRHgD8Q9LUh/qcmUyPGw==", + "dependencies": { + "@sentry-internal/tracing": "7.57.0", + "@sentry/core": "7.57.0", + "@sentry/types": "7.57.0", + "@sentry/utils": "7.57.0", + "cookie": "^0.4.1", + "https-proxy-agent": "^5.0.0", + "lru_map": "^0.3.3", + "tslib": "^2.4.1 || ^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/node/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sentry/types": { + "version": "7.57.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.57.0.tgz", + "integrity": "sha512-D7ifoUfxuVCUyktIr5Gc+jXUbtcUMmfHdTtTbf1XCZHua5mJceK9wtl3YCg3eq/HK2Ppd52BKnTzEcS5ZKQM+w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/utils": { + "version": "7.57.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.57.0.tgz", + "integrity": "sha512-YXrkMCiNklqkXctn4mKYkrzNCf/dfVcRUQrkXjeBC+PHXbcpPyaJgInNvztR7Skl8lE3JPGPN4v5XhLxK1bUUg==", + "dependencies": { + "@sentry/types": "7.57.0", + "tslib": "^2.4.1 || ^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -5394,6 +5468,11 @@ "node": ">=0.10.0" } }, + "node_modules/lru_map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", + "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -7613,8 +7692,7 @@ "node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -9102,6 +9180,63 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "@sentry-internal/tracing": { + "version": "7.57.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.57.0.tgz", + "integrity": "sha512-tpViyDd8AhQGYYhI94xi2aaDopXOPfL2Apwrtb3qirWkomIQ2K86W1mPmkce+B0cFOnW2Dxv/ZTFKz6ghjK75A==", + "requires": { + "@sentry/core": "7.57.0", + "@sentry/types": "7.57.0", + "@sentry/utils": "7.57.0", + "tslib": "^2.4.1 || ^1.9.3" + } + }, + "@sentry/core": { + "version": "7.57.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.57.0.tgz", + "integrity": "sha512-l014NudPH0vQlzybtXajPxYFfs9w762NoarjObC3gu76D1jzBBFzhdRelkGpDbSLNTIsKhEDDRpgAjBWJ9icfw==", + "requires": { + "@sentry/types": "7.57.0", + "@sentry/utils": "7.57.0", + "tslib": "^2.4.1 || ^1.9.3" + } + }, + "@sentry/node": { + "version": "7.57.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.57.0.tgz", + "integrity": "sha512-63mjyUVM6sfJFVQ5TGVRVGUsoEfESl5ABzIW1W0s9gUiQPaG8SOdaQJglb2VNrkMYxnRHgD8Q9LUh/qcmUyPGw==", + "requires": { + "@sentry-internal/tracing": "7.57.0", + "@sentry/core": "7.57.0", + "@sentry/types": "7.57.0", + "@sentry/utils": "7.57.0", + "cookie": "^0.4.1", + "https-proxy-agent": "^5.0.0", + "lru_map": "^0.3.3", + "tslib": "^2.4.1 || ^1.9.3" + }, + "dependencies": { + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + } + } + }, + "@sentry/types": { + "version": "7.57.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.57.0.tgz", + "integrity": "sha512-D7ifoUfxuVCUyktIr5Gc+jXUbtcUMmfHdTtTbf1XCZHua5mJceK9wtl3YCg3eq/HK2Ppd52BKnTzEcS5ZKQM+w==" + }, + "@sentry/utils": { + "version": "7.57.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.57.0.tgz", + "integrity": "sha512-YXrkMCiNklqkXctn4mKYkrzNCf/dfVcRUQrkXjeBC+PHXbcpPyaJgInNvztR7Skl8lE3JPGPN4v5XhLxK1bUUg==", + "requires": { + "@sentry/types": "7.57.0", + "tslib": "^2.4.1 || ^1.9.3" + } + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -12281,6 +12416,11 @@ "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", "dev": true }, + "lru_map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", + "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==" + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -13805,8 +13945,7 @@ "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "tsutils": { "version": "3.21.0", diff --git a/package.json b/package.json index 6b66dde..7876227 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "typescript": "^4.7.4" }, "dependencies": { + "@sentry/node": "^7.57.0", "@slack/client": "^5.0.2", "@types/bcrypt": "^5.0.0", "@types/bcryptjs": "^2.4.2", diff --git a/src/config/index.ts b/src/config/index.ts index bf45add..f5a3719 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -13,6 +13,7 @@ const MONGO_URI = : process.env.MONGODB_DEVELOPMENT_URI; export default { + nodeEnv: NODE_ENV, port: parseInt(process.env.PORT as string, 10), /** @@ -57,5 +58,6 @@ export default { }, clientKey: process.env.CLIENT_KEY as string, naverClientId: process.env.NAVER_CLIENT_ID as string, - naverClientSecret: process.env.NAVER_CLIENT_SECRET as string + naverClientSecret: process.env.NAVER_CLIENT_SECRET as string, + sentryDsn: process.env.SENTRY_DSN as string }; diff --git a/src/index.ts b/src/index.ts index 50df830..832af56 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,35 +1,23 @@ -import express, { Request, Response } from 'express'; +import express from 'express'; import connectDB from './loaders/db'; import routes from './routes'; import helmet from 'helmet'; import config from './config'; import errorHandler from './middlewares/errorHandler'; import cors from 'cors'; -import session from 'express-session'; -import MongoStore from 'connect-mongo'; +import expressSession from './loaders/session'; +import * as SentryConfig from './loaders/sentryConfiguration'; const app = express(); app.use(cors()); app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.use(helmet()); - -const expressSession: express.RequestHandler = session({ - secret: config.sessionKey, - store: MongoStore.create({ - mongoUrl: config.mongoURI - }), - cookie: { maxAge: 3.6e6 * 24 * 180 }, - resave: false, - saveUninitialized: false -}); - app.use(expressSession); +SentryConfig.initializeSentry(app); app.use(routes); -app.use('', (req: Request, res: Response) => { - res.status(200).send(); -}); +SentryConfig.attachSentryErrorHandler(app); app.use(errorHandler); connectDB() diff --git a/src/loaders/sentryConfiguration.ts b/src/loaders/sentryConfiguration.ts new file mode 100644 index 0000000..9d793da --- /dev/null +++ b/src/loaders/sentryConfiguration.ts @@ -0,0 +1,29 @@ +import { Express } from 'express'; +import { + init, + Handlers, + Integrations, + autoDiscoverNodePerformanceMonitoringIntegrations +} from '@sentry/node'; +import config from '../config'; + +const initializeSentry = (app: Express) => { + init({ + environment: config.nodeEnv, + dsn: config.sentryDsn, + integrations: [ + new Integrations.Http({ tracing: true }), + new Integrations.Express({ app }), + ...autoDiscoverNodePerformanceMonitoringIntegrations() + ], + tracesSampleRate: 1.0 + }); + app.use(Handlers.requestHandler()); + app.use(Handlers.tracingHandler()); +}; + +const attachSentryErrorHandler = (app: Express) => { + app.use(Handlers.errorHandler()); +}; + +export { initializeSentry, attachSentryErrorHandler }; diff --git a/src/loaders/session.ts b/src/loaders/session.ts new file mode 100644 index 0000000..0ce3031 --- /dev/null +++ b/src/loaders/session.ts @@ -0,0 +1,16 @@ +import express from 'express'; +import session from 'express-session'; +import config from '../config'; +import MongoStore from 'connect-mongo'; + +const expressSession: express.RequestHandler = session({ + secret: config.sessionKey, + store: MongoStore.create({ + mongoUrl: config.mongoURI + }), + cookie: { maxAge: 3.6e6 * 24 * 180 }, + resave: false, + saveUninitialized: false +}); + +export default expressSession; diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index fdb85f1..51e7f21 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -2,19 +2,12 @@ import { NextFunction, Request, Response } from 'express'; import { PiickleException } from '../intefaces/exception'; import statusCode from '../modules/statusCode'; import util from '../modules/util'; -import { generateSlackMessage } from '../modules/returnToSlack'; -import { sendMessagesToSlack } from '../modules/slackApi'; - -const errHandler = async ( +const errHandler = ( err: any, req: Request, res: Response, next: NextFunction ) => { - const errorMessage: string = generateSlackMessage(req, err); - - await sendMessagesToSlack(errorMessage); - if (err instanceof PiickleException) { return res .status(err.statusCode) diff --git a/src/modules/slackApi.ts b/src/modules/slackApi.ts deleted file mode 100644 index eaf3aff..0000000 --- a/src/modules/slackApi.ts +++ /dev/null @@ -1,10 +0,0 @@ -import axios from 'axios'; -import config from '../config'; - -export const sendMessagesToSlack = async (message: string): Promise => { - try { - await axios.post(config.slackWebHookUrl, { text: message }); - } catch (error) { - console.log(error); - } -}; diff --git a/src/routes/index.ts b/src/routes/index.ts index a450d6b..c62b426 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,5 +1,5 @@ //router index file -import {Request, Response, Router} from 'express'; +import { Router } from 'express'; import UserRouter from './userRouter'; import CategoryRouter from './CategoryRouter'; import BallotRouter from './ballotRouter'; @@ -17,9 +17,4 @@ router.use('/ballots', flexibleAuth, guestHandler, BallotRouter); router.use('/cards', CardRouter); router.use('/test', TestRouter); router.use('/medley', CardMedleyRouter); - -router.use('', (req: Request, res: Response) => { - res.status(200).send(); -}); - export default router; From 1846b2962c60814d4e132b5fcf7f43a513648d87 Mon Sep 17 00:00:00 2001 From: kyY00n Date: Sun, 23 Jul 2023 19:04:03 +0900 Subject: [PATCH 11/27] =?UTF-8?q?[HOTFIX]=20=EC=9A=94=EC=B2=AD=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=84=B1=EB=B3=84=EC=9D=84=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(#191)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/userController.ts | 17 ++++++++++------- src/intefaces/createUserCommand.ts | 9 +++++---- src/models/user/gender.ts | 4 +++- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index e19b368..f82fada 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -16,12 +16,12 @@ import message from '../modules/responseMessage'; import statusCode from '../modules/statusCode'; import util from '../modules/util'; import { - UserService, - PreUserService, AuthService, - SocialAuthService, + BlockCardService, NaverLoginService, - BlockCardService + PreUserService, + SocialAuthService, + UserService } from '../services'; import { UserProfileResponseDto } from '../intefaces/user/UserProfileResponseDto'; import { UserUpdateNicknameDto } from '../intefaces/user/UserUpdateNicknameDto'; @@ -35,7 +35,8 @@ import SocialLoginDto from '../intefaces/user/SocialLoginDto'; import LoginResponseDto from '../intefaces/user/LoginResponseDto'; import { SocialVendor } from '../models/socialVendor'; import IUser from '../models/interface/IUser'; -import {AgeGroup, AgeGroupKey} from '../models/user/ageGroup'; +import { AgeGroup } from '../models/user/ageGroup'; +import { Gender } from '../models/user/gender'; /** * @route GET /email @@ -186,8 +187,10 @@ const postUser = async ( email: req.body.email, password: req.body.password, nickname: req.body.nickname, - ageGroup: AgeGroup[req.body.ageGroup], - gender: req.body.gender, + ageGroup: req.body.ageGroup + ? AgeGroup[req.body.ageGroup] + : AgeGroup.UNDEFINED, + gender: req.body.gender ? Gender[req.body.gender] : Gender.ETC, profileImgUrl: (req?.file as Express.MulterS3.File)?.location }; diff --git a/src/intefaces/createUserCommand.ts b/src/intefaces/createUserCommand.ts index d4b2eff..c8f5a2c 100644 --- a/src/intefaces/createUserCommand.ts +++ b/src/intefaces/createUserCommand.ts @@ -1,11 +1,12 @@ -import {AgeGroup, AgeGroupKey} from '../models/user/ageGroup'; +import { AgeGroup, AgeGroupKey } from '../models/user/ageGroup'; +import { Gender, GenderKey } from '../models/user/gender'; interface CreateUserCommand { email: string; password: string; nickname: string; ageGroup: AgeGroup; - gender: string; + gender: Gender; profileImgUrl: string; } @@ -13,8 +14,8 @@ interface CreateUserReq { email: string; password: string; nickname: string; - ageGroup: AgeGroupKey; - gender: string; + ageGroup?: AgeGroupKey; + gender?: GenderKey; } export { CreateUserCommand, CreateUserReq }; diff --git a/src/models/user/gender.ts b/src/models/user/gender.ts index 9720d22..41a95a8 100644 --- a/src/models/user/gender.ts +++ b/src/models/user/gender.ts @@ -4,4 +4,6 @@ enum Gender { ETC = '기타' } -export { Gender }; +type GenderKey = keyof typeof Gender; + +export { Gender, GenderKey }; From 4f6697e0714d23fd092f55954cc063bcd1ac58a1 Mon Sep 17 00:00:00 2001 From: donglee99 Date: Sun, 30 Jul 2023 16:56:39 +0900 Subject: [PATCH 12/27] =?UTF-8?q?=EC=B5=9C=EA=B7=BC=20=EB=B6=81=EB=A7=88?= =?UTF-8?q?=ED=81=AC=20=EB=90=9C=20=EC=B9=B4=EB=93=9C=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EC=8B=9C=20=EC=A4=91=EB=B3=B5=20=EB=90=9C=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/cardService.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/services/cardService.ts b/src/services/cardService.ts index 912ec2d..d644586 100644 --- a/src/services/cardService.ts +++ b/src/services/cardService.ts @@ -122,6 +122,11 @@ const findRecentlyUpdatedCard = async (userId?: Types.ObjectId) => { const findRecentlyBookmarkedCard = async (userId?: Types.ObjectId) => { const bookmarks: BookmarkDocument[] = await Bookmark.aggregate() + .group({ + _id: '$card', + card: { $first: '$card' }, + createdAt: { $first: '$createdAt' } + }) .sort({ createdAt: -1 }) .limit(20); const cards: CardDocument[] = ( From 17caf1ed41927db4a78d2ac6f23a683b49a5ada1 Mon Sep 17 00:00:00 2001 From: DongLee99 <55686088+DongLee99@users.noreply.github.com> Date: Thu, 10 Aug 2023 21:08:12 +0900 Subject: [PATCH 13/27] =?UTF-8?q?[HOTFIX]=20=EC=86=8C=EC=85=9C=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=20=EC=8B=9C=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EC=9D=98=20=EA=B0=80=EC=9E=85=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=EB=A5=BC=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#196)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 소셜 회원가입 시 기존 회원의 가입 여부를 검증 로직 변경 * fix: user 스키마 변경 * fix: 소셜계정으로 가입된 메일로 로컬 회원가입 가능하게 변경 --------- Co-authored-by: Rosie --- src/controllers/userController.ts | 21 +++++---------- src/intefaces/createUserCommand.ts | 17 +++++++++++- src/models/user/user.ts | 5 ++-- src/services/socialAuthService.ts | 42 +++++++++++------------------- src/services/userService.ts | 9 ++++--- 5 files changed, 45 insertions(+), 49 deletions(-) diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index f82fada..a635cb9 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -7,7 +7,8 @@ import { } from '../intefaces/exception'; import { CreateUserCommand, - CreateUserReq + CreateUserReq, + toCommand } from '../intefaces/createUserCommand'; import { PostBaseResponseDto } from '../intefaces/PostBaseResponseDto'; import { UserLoginDto } from '../intefaces/user/UserLoginDto'; @@ -37,6 +38,7 @@ import { SocialVendor } from '../models/socialVendor'; import IUser from '../models/interface/IUser'; import { AgeGroup } from '../models/user/ageGroup'; import { Gender } from '../models/user/gender'; +import { createUser, validateNewLocalEmail } from '../services/userService'; /** * @route GET /email @@ -168,7 +170,7 @@ const verifyEmailTest = async ( }; /** - * @route /users + * @route POST /users * @desc 회원가입 api * @access Public */ @@ -183,18 +185,9 @@ const postUser = async ( throw new IllegalArgumentException('필요한 값이 없습니다.'); } - const createUserCommand: CreateUserCommand = { - email: req.body.email, - password: req.body.password, - nickname: req.body.nickname, - ageGroup: req.body.ageGroup - ? AgeGroup[req.body.ageGroup] - : AgeGroup.UNDEFINED, - gender: req.body.gender ? Gender[req.body.gender] : Gender.ETC, - profileImgUrl: (req?.file as Express.MulterS3.File)?.location - }; - - const createdUser = await UserService.createUser(createUserCommand); + const createUserCommand = toCommand(req); + await validateNewLocalEmail(createUserCommand.email); + const createdUser = await createUser(createUserCommand); const jwt = getToken(createdUser._id); diff --git a/src/intefaces/createUserCommand.ts b/src/intefaces/createUserCommand.ts index c8f5a2c..441bcee 100644 --- a/src/intefaces/createUserCommand.ts +++ b/src/intefaces/createUserCommand.ts @@ -1,5 +1,6 @@ import { AgeGroup, AgeGroupKey } from '../models/user/ageGroup'; import { Gender, GenderKey } from '../models/user/gender'; +import { TypedRequest } from '../types/TypedRequest'; interface CreateUserCommand { email: string; @@ -18,4 +19,18 @@ interface CreateUserReq { gender?: GenderKey; } -export { CreateUserCommand, CreateUserReq }; +const toCommand = (req: TypedRequest) => { + const createUserCommand: CreateUserCommand = { + email: req.body.email, + password: req.body.password, + nickname: req.body.nickname, + ageGroup: req.body.ageGroup + ? AgeGroup[req.body.ageGroup] + : AgeGroup.UNDEFINED, + gender: req.body.gender ? Gender[req.body.gender] : Gender.ETC, + profileImgUrl: (req?.file as Express.MulterS3.File)?.location + }; + return createUserCommand; +}; + +export { CreateUserCommand, CreateUserReq, toCommand }; diff --git a/src/models/user/user.ts b/src/models/user/user.ts index 0237d6d..ddacd60 100644 --- a/src/models/user/user.ts +++ b/src/models/user/user.ts @@ -11,8 +11,7 @@ type UserDocument = IUser & IDocument; const userSchema = new Schema( { email: { - type: String, - unique: true + type: String }, socialId: { type: String, @@ -60,7 +59,7 @@ const userSchema = new Schema( } ); -userSchema.index({ socialVendor: 1, socialId: 1 }, { unique: true }); +userSchema.index({ socialVendor: 1, email: 1 }, { unique: true }); const User = model('User', userSchema); diff --git a/src/services/socialAuthService.ts b/src/services/socialAuthService.ts index 8e53191..784fd78 100644 --- a/src/services/socialAuthService.ts +++ b/src/services/socialAuthService.ts @@ -63,15 +63,22 @@ const findUserBySocial = async ( return user; }; -const join = async (socialMember: IUser): Promise => { - if (socialMember.email) { - const alreadyEmail = await UserService.isEmailExisting(socialMember.email); - if (alreadyEmail) { - throw new IllegalArgumentException( - `같은 이메일로 가입된 유저가 있습니다. email: ${socialMember.email}` - ); - } +const validateMemberState = async (preMember: IUser) => { + const alreadyUser = await User.findOne({ + socialVendor: '$preMember.socialVendor', + socialId: '$preMember.socialId' + }); + if (alreadyUser) { + throw new IllegalArgumentException( + `해당 소셜에 같은 이메일로 가입된 유저가 있습니다. email: ${ + preMember.email || '이메일 알 수 없음' + }` + ); } +}; + +const join = async (socialMember: IUser): Promise => { + await validateMemberState(socialMember); socialMember.nickname = await UserService.autoGenerateNicknameFrom( socialMember.nickname ); @@ -98,26 +105,7 @@ const findSocialUser = async (socialMember: IUser) => { return alreadyMember; }; -const findOrCreateUserBySocialToken = async (vendor: string, token: string) => { - const response: SocialLoginResponse = await getUserFromSocialVendor( - token, - vendor - ); - const socialVendor = SocialVendorExtension.getVendorByValue(vendor); - const socialMember: IUser = convertSocialToPiickle(socialVendor, response); - const isAlreadyMember = await isAlreadySocialMember(socialMember); - if (isAlreadyMember) { - const alreadyMember = await findUserBySocial( - socialVendor, - socialMember.socialId! - ); - return alreadyMember; - } - return join(socialMember); -}; - export { - findOrCreateUserBySocialToken, findUserBySocial, getPiickleUser, isAlreadySocialMember, diff --git a/src/services/userService.ts b/src/services/userService.ts index 081af64..8f3f2ec 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -30,9 +30,10 @@ const isEmailExisting = async (email: string): Promise => { return false; }; -const validateEmail = async (email: string) => { +const validateNewLocalEmail = async (email: string) => { const alreadyUser = await User.findOne({ - email + email, + socialVendor: null }); if (alreadyUser) { throw new IllegalArgumentException('이미 존재하는 이메일입니다.'); @@ -48,7 +49,6 @@ const createUser = async (command: CreateUserCommand) => { gender, profileImgUrl: profileImageUrl } = command; - await validateEmail(email); const user = new User({ email, @@ -241,5 +241,6 @@ export { createOrDeleteBookmark, nicknameAlreadyExists, autoGenerateNicknameFrom, - deleteUser + deleteUser, + validateNewLocalEmail }; From 57d13fe32198540e6dcba9915b92533ffb0a6ad9 Mon Sep 17 00:00:00 2001 From: donglee99 Date: Sat, 12 Aug 2023 15:28:31 +0900 Subject: [PATCH 14/27] =?UTF-8?q?Extra=20=EC=B9=B4=EB=93=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EC=8B=9C=20find=20->=20aggregate=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=B4=20=EB=9E=9C=EB=8D=A4=20=EC=B9=B4=EB=93=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/cardService.ts | 41 ++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/src/services/cardService.ts b/src/services/cardService.ts index d644586..358f978 100644 --- a/src/services/cardService.ts +++ b/src/services/cardService.ts @@ -62,13 +62,14 @@ const findExtraCardsExceptFor = async ( ninCards.push(blockedCard.card); } } - const extraCards: CardDocument[] = await Card.find({ - _id: { - $nin: ninCards - } - }) - .sort('_id') - .limit(size - cards.length); + const extraCards: CardDocument[] = await Card.aggregate() + .match({ + _id: { + $nin: ninCards + } + }) + .sample(size - cards.length) + .sort({ _id: -1 }); return extraCards; }; @@ -166,6 +167,32 @@ const findCardByBookmarkedGender = async ( return totalCards; }; +// const findCardByCardList = async ( +// cardIdList: CardListDto, +// blockedCardId?: Types.ObjectId, +// userId?: Types.ObjectId +// ) => { +// const blockedCard = await BlockedCard.findOne({ +// user: userId, +// card: blockedCardId +// }); +// const ninCards: Types.ObjectId[] = []; +// if (blockedCard) { +// ninCards.push(blockedCard.card); +// } +// const cards: CardDocument[] = await Card.find({ +// _id: { +// $nin: ninCards, +// $in: cardIdList._ids +// } +// }); +// const totalCards: CardResponseDto[] = []; +// for (const card of cards) { +// totalCards.push(await createCardResponse(card, userId)); +// } +// return; +// }; + export { findBestCards, findCards, From 0be21cb3a7f7421778b02c3a3f48574a3915fcef Mon Sep 17 00:00:00 2001 From: donglee99 Date: Sat, 12 Aug 2023 18:36:02 +0900 Subject: [PATCH 15/27] =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/cardService.ts | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/src/services/cardService.ts b/src/services/cardService.ts index 358f978..e1d2cb5 100644 --- a/src/services/cardService.ts +++ b/src/services/cardService.ts @@ -167,32 +167,6 @@ const findCardByBookmarkedGender = async ( return totalCards; }; -// const findCardByCardList = async ( -// cardIdList: CardListDto, -// blockedCardId?: Types.ObjectId, -// userId?: Types.ObjectId -// ) => { -// const blockedCard = await BlockedCard.findOne({ -// user: userId, -// card: blockedCardId -// }); -// const ninCards: Types.ObjectId[] = []; -// if (blockedCard) { -// ninCards.push(blockedCard.card); -// } -// const cards: CardDocument[] = await Card.find({ -// _id: { -// $nin: ninCards, -// $in: cardIdList._ids -// } -// }); -// const totalCards: CardResponseDto[] = []; -// for (const card of cards) { -// totalCards.push(await createCardResponse(card, userId)); -// } -// return; -// }; - export { findBestCards, findCards, From 9978b6ad17f43a050664f5a93f5cfd11b98f7ee9 Mon Sep 17 00:00:00 2001 From: kyY00n Date: Sat, 19 Aug 2023 16:17:32 +0900 Subject: [PATCH 16/27] =?UTF-8?q?feat:=20=EC=8A=A4=ED=82=A4=EB=A7=88=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1=20(#203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 스키마 추가 * feat: 데이터베이스 연결 --- src/loaders/db.ts | 9 ++++++++- src/models/mind23/comment.ts | 28 ++++++++++++++++++++++++++++ src/models/mind23/prizeEntry.ts | 26 ++++++++++++++++++++++++++ src/models/mind23/question.ts | 18 ++++++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/models/mind23/comment.ts create mode 100644 src/models/mind23/prizeEntry.ts create mode 100644 src/models/mind23/question.ts diff --git a/src/loaders/db.ts b/src/loaders/db.ts index 04d55d2..b30ff48 100644 --- a/src/loaders/db.ts +++ b/src/loaders/db.ts @@ -13,6 +13,9 @@ import QuitLog from '../models/quitLog'; import BestCard from '../models/bestCard'; import CardMedley from '../models/cardMedley'; import { BlockedCard } from '../models/blockedCard'; +import Question from '../models/mind23/question'; +import PrizeEntry from '../models/mind23/prizeEntry'; +import Comment from '../models/mind23/comment'; const createCollection = (model: Model) => { model.createCollection().catch(err => { @@ -37,7 +40,11 @@ const connectDB = async () => { EmailAuth, BestCard, CardMedley, - BlockedCard + BlockedCard, + CardMedley, + Question, + Comment, + PrizeEntry ]; models.forEach(model => { diff --git a/src/models/mind23/comment.ts b/src/models/mind23/comment.ts new file mode 100644 index 0000000..c7d308e --- /dev/null +++ b/src/models/mind23/comment.ts @@ -0,0 +1,28 @@ +import { model, Schema, Types } from 'mongoose'; +import IDocument from '../interface/Document'; + +interface IComment { + author: Types.ObjectId; + content: string; +} + +type CommentDocument = IComment & IDocument; + +const commentSchema = new Schema( + { + content: String, + author: { + type: Schema.Types.ObjectId, + ref: 'User' + } + }, + { + timestamps: true + } +); + +const Comment = model('Comment', commentSchema); + +export default Comment; + +export { CommentDocument }; diff --git a/src/models/mind23/prizeEntry.ts b/src/models/mind23/prizeEntry.ts new file mode 100644 index 0000000..b6900e4 --- /dev/null +++ b/src/models/mind23/prizeEntry.ts @@ -0,0 +1,26 @@ +import { model, Schema } from 'mongoose'; +import IDocument from '../interface/Document'; + +interface IPrizeEntry { + user: Schema.Types.ObjectId; +} + +type PrizeEntryDocument = IPrizeEntry & IDocument; + +const prizeEntrySchema = new Schema( + { + user: { + type: Schema.Types.ObjectId, + ref: 'User' + } + }, + { + timestamps: true + } +); + +const PrizeEntry = model('PrizeEntry', prizeEntrySchema); + +export default PrizeEntry; + +export { PrizeEntryDocument }; diff --git a/src/models/mind23/question.ts b/src/models/mind23/question.ts new file mode 100644 index 0000000..8a07a42 --- /dev/null +++ b/src/models/mind23/question.ts @@ -0,0 +1,18 @@ +import { model, Schema } from 'mongoose'; +import IDocument from '../interface/Document'; + +interface IQuestion { + content: string; +} + +type QuestionDocument = IQuestion & IDocument; + +const questionSchema = new Schema({ + content: String +}); + +const Question = model('Question', questionSchema); + +export default Question; + +export { QuestionDocument }; From f1d3397fb271296e508f0b8d23cc62bccbfc2707 Mon Sep 17 00:00:00 2001 From: donglee99 Date: Sun, 20 Aug 2023 15:18:51 +0900 Subject: [PATCH 17/27] =?UTF-8?q?=EB=A7=88=EC=9D=B8=ED=8A=B8=2023=C2=A0?= =?UTF-8?q?=EC=9A=A9=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/index.ts | 4 +- src/controllers/mind23Controller.ts | 108 ++++++++++++++++++++ src/intefaces/mind23/CommentDto.ts | 7 ++ src/intefaces/mind23/QuestionDto.ts | 5 + src/intefaces/mind23/QuestionResponseDto.ts | 7 ++ src/models/mind23/comment.ts | 5 + src/modules/responseMessage.ts | 8 +- src/routes/index.ts | 2 + src/routes/mind23Router.ts | 20 ++++ src/services/index.ts | 4 +- src/services/mind23Service.ts | 99 ++++++++++++++++++ 11 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 src/controllers/mind23Controller.ts create mode 100644 src/intefaces/mind23/CommentDto.ts create mode 100644 src/intefaces/mind23/QuestionDto.ts create mode 100644 src/intefaces/mind23/QuestionResponseDto.ts create mode 100644 src/routes/mind23Router.ts create mode 100644 src/services/mind23Service.ts diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 481fb29..eba4e82 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -3,11 +3,13 @@ import * as CategoryController from './CategoryController'; import * as BallotController from './ballotController'; import * as CardController from './cardController'; import * as CardMedleyController from './cardMedleyController'; +import * as Mind23Controller from './mind23Controller'; export { UserController, CategoryController, BallotController, CardController, - CardMedleyController + CardMedleyController, + Mind23Controller }; diff --git a/src/controllers/mind23Controller.ts b/src/controllers/mind23Controller.ts new file mode 100644 index 0000000..daacd4e --- /dev/null +++ b/src/controllers/mind23Controller.ts @@ -0,0 +1,108 @@ +import { NextFunction, Request, Response } from 'express'; +import message from '../modules/responseMessage'; +import statusCode from '../modules/statusCode'; +import util from '../modules/util'; +import { Mind23Service } from '../services'; +import { Types } from 'mongoose'; +import { QuestionDto } from '../intefaces/mind23/QuestionDto'; +import { QuestionResponseDto } from '../intefaces/mind23/QuestionResponseDto'; +import { TypedRequest } from '../types/TypedRequest'; + +/** + * @route /mind23/api/questions + * @desc mind23 용 질문을 조회합니다. + * @access Public + */ +const getQuestionList = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const data: QuestionResponseDto = await Mind23Service.findQuestionList(); + return res + .status(statusCode.OK) + .send( + util.success(statusCode.OK, message.READ_MIND23_QUESTIONS_SUCCESS, data) + ); + } catch (err) { + next(err); + } +}; + +/** + * @route /mind23/api/comments + * @desc mind23 용 댓글을 조회합니다. + * @access Public + */ +const getCommentList = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const questionId = new Types.ObjectId(req.params.questionId); + const comments = await Mind23Service.findCommentsList(questionId); + return res + .status(statusCode.OK) + .send( + util.success( + statusCode.OK, + message.READ_MIND23_COMMENTS_SUCCESS, + comments + ) + ); + } catch (err) { + next(err); + } +}; + +/** + * @route /mind23/api/comments?questionId= + * @desc mind23 용 댓글을 등록합니다. + * @access Public + */ +const createComment = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const userId: Types.ObjectId | undefined = req.user?.id; + const questionId: Types.ObjectId | undefined = new Types.ObjectId( + req.params.questionId + ); + const comment: String = req.body.content; + await Mind23Service.createComment(userId, questionId, comment); + return res + .status(statusCode.OK) + .send(util.success(statusCode.OK, '마인드 23 댓글 등록 성공')); + } catch (err) { + next(err); + } +}; + +/** + * @route /mind23/api/prize-entry + * @desc mind23 용 경품 응모를 등록합니다. + * @access Public + */ +const createPrizeEntry = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const userId: Types.ObjectId | undefined = req.user?.id; + await Mind23Service.createPrizeEntry(userId); + return res + .status(statusCode.OK) + .send( + util.success(statusCode.OK, message.CREATE_MIND23_PRIZE_ENTRY_SUCCESS) + ); + } catch (err) { + next(err); + } +}; + +export { getQuestionList, getCommentList, createComment, createPrizeEntry }; diff --git a/src/intefaces/mind23/CommentDto.ts b/src/intefaces/mind23/CommentDto.ts new file mode 100644 index 0000000..55e2bdd --- /dev/null +++ b/src/intefaces/mind23/CommentDto.ts @@ -0,0 +1,7 @@ +import { Types } from 'mongoose'; + +export interface CommentDto { + _id: Types.ObjectId; + content: String; + profileImageUrl: string; +} diff --git a/src/intefaces/mind23/QuestionDto.ts b/src/intefaces/mind23/QuestionDto.ts new file mode 100644 index 0000000..45dbce4 --- /dev/null +++ b/src/intefaces/mind23/QuestionDto.ts @@ -0,0 +1,5 @@ +import { Types } from 'mongoose'; + +export interface QuestionDto { + content: String; +} diff --git a/src/intefaces/mind23/QuestionResponseDto.ts b/src/intefaces/mind23/QuestionResponseDto.ts new file mode 100644 index 0000000..c8d7ad1 --- /dev/null +++ b/src/intefaces/mind23/QuestionResponseDto.ts @@ -0,0 +1,7 @@ +import { Types } from 'mongoose'; +import { CardResponseDto } from '../CardResponseDto'; + +export interface QuestionResponseDto { + totalCount: Number; + cards: CardResponseDto[]; +} diff --git a/src/models/mind23/comment.ts b/src/models/mind23/comment.ts index c7d308e..480b64b 100644 --- a/src/models/mind23/comment.ts +++ b/src/models/mind23/comment.ts @@ -2,6 +2,7 @@ import { model, Schema, Types } from 'mongoose'; import IDocument from '../interface/Document'; interface IComment { + question: Types.ObjectId; author: Types.ObjectId; content: string; } @@ -10,6 +11,10 @@ type CommentDocument = IComment & IDocument; const commentSchema = new Schema( { + question: { + type: Schema.Types.ObjectId, + ref: 'Question' + }, content: String, author: { type: Schema.Types.ObjectId, diff --git a/src/modules/responseMessage.ts b/src/modules/responseMessage.ts index 373e47d..6800393 100644 --- a/src/modules/responseMessage.ts +++ b/src/modules/responseMessage.ts @@ -38,7 +38,13 @@ const message = { // 투표 BALLOT_RESULT_CREATED: '투표 성공', BALLOT_STATUS_VIEW_SUCCESS: '투표 현황 조회 성공', - READ_MAIN_BALLOTS_SUCCESS: '메인 투표 주제 조회 성공' + READ_MAIN_BALLOTS_SUCCESS: '메인 투표 주제 조회 성공', + + //마인드 23 + READ_MIND23_QUESTIONS_SUCCESS: '마인드 23 질문 리스트 조회 성공', + CREATE_MIND23_COMMENT_SUCCESS: '마인드 23 댓글 등록 성공', + READ_MIND23_COMMENTS_SUCCESS: '마인드 23 댓글 리스트 조회 성공', + CREATE_MIND23_PRIZE_ENTRY_SUCCESS: '경품 응모 성공' }; export default message; diff --git a/src/routes/index.ts b/src/routes/index.ts index c62b426..fa4fb0a 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -5,6 +5,7 @@ import CategoryRouter from './CategoryRouter'; import BallotRouter from './ballotRouter'; import CardRouter from './cardRouter'; import TestRouter from './testRouter'; +import Mind23Router from './mind23Router'; import CardMedleyRouter from './cardMedleyRouter'; import flexibleAuth from '../middlewares/flexibleAuth'; import guestHandler from '../middlewares/guestHandler'; @@ -17,4 +18,5 @@ router.use('/ballots', flexibleAuth, guestHandler, BallotRouter); router.use('/cards', CardRouter); router.use('/test', TestRouter); router.use('/medley', CardMedleyRouter); +router.use('/mind23/api', Mind23Router); export default router; diff --git a/src/routes/mind23Router.ts b/src/routes/mind23Router.ts new file mode 100644 index 0000000..87f9cb2 --- /dev/null +++ b/src/routes/mind23Router.ts @@ -0,0 +1,20 @@ +import { Router } from 'express'; +import { Mind23Controller } from '../controllers'; +import flexibleAuth from '../middlewares/flexibleAuth'; + +const router: Router = Router(); + +router.get('/questions', Mind23Controller.getQuestionList); +router.post( + '/comments/:questionId', + flexibleAuth, + Mind23Controller.createComment +); +router.get( + '/comments/:questionId', + flexibleAuth, + Mind23Controller.getCommentList +); +router.post('/prize-entry', flexibleAuth, Mind23Controller.createPrizeEntry); + +export default router; diff --git a/src/services/index.ts b/src/services/index.ts index 7a98841..6283cc6 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -8,6 +8,7 @@ import * as CardMedleyService from './cardMedleyService'; import * as SocialAuthService from './socialAuthService'; import * as NaverLoginService from './naverLoginService'; import * as BlockCardService from './blockCardService'; +import * as Mind23Service from './mind23Service'; export { AuthService, @@ -19,5 +20,6 @@ export { PreUserService, CardMedleyService, SocialAuthService, - NaverLoginService + NaverLoginService, + Mind23Service }; diff --git a/src/services/mind23Service.ts b/src/services/mind23Service.ts new file mode 100644 index 0000000..cde1534 --- /dev/null +++ b/src/services/mind23Service.ts @@ -0,0 +1,99 @@ +import util from '../modules/util'; +import Question, { QuestionDocument } from '../models/mind23/question'; +import Comment, { CommentDocument } from '../models/mind23/comment'; +import { IllegalArgumentException } from '../intefaces/exception'; +import PrizeEntry, { PrizeEntryDocument } from '../models/mind23/prizeEntry'; +import { CardResponseDto } from '../intefaces/CardResponseDto'; +import { QuestionResponseDto } from '../intefaces/mind23/QuestionResponseDto'; +import { CommentDto } from '../intefaces/mind23/CommentDto'; +import Card, { CardDocument } from '../models/card'; +import { Types } from 'mongoose'; +import config from '../config'; +import User from '../models/user/user'; +import { QuestionDto } from '../intefaces/mind23/QuestionDto'; + +const findProfileImageUrl = async (userId?: Types.ObjectId) => { + const user = await User.findOne({ _id: userId }); + if (!user) { + throw new IllegalArgumentException('해당하는 아이디의 유저가 없습니다.'); + } + return user.profileImageUrl; +}; + +const createCardResponse = async ( + question: QuestionDocument +): Promise => { + return { + _id: question._id, + content: question.content, + tags: [], + category: [], + filter: [], + isBookmark: false + }; +}; + +const createCommentResponse = async ( + comment: CommentDocument +): Promise => { + return { + _id: comment.author, + content: comment.content, + profileImageUrl: await findProfileImageUrl(comment.author) + }; +}; + +const findQuestionList = async (): Promise => { + const questions = await Question.find({}); + const totalCards: CardResponseDto[] = []; + for (const question of questions) { + totalCards.push(await createCardResponse(question)); + } + const count = await PrizeEntry.find({}).count(); + return { + totalCount: count, + cards: totalCards + }; +}; + +const findCommentsList = async (questionId?: Types.ObjectId) => { + const comments = await Comment.aggregate() + .match({ + question: questionId + }) + .sort({ createdAt: -1 }); + console.log(comments.length); + const totalComments: CommentDto[] = []; + for (const comment of comments) { + totalComments.push(await createCommentResponse(comment)); + } + return totalComments; +}; + +const createComment = async ( + userId?: Types.ObjectId, + questionId?: Types.ObjectId, + content?: String +) => { + if (!userId) { + throw new IllegalArgumentException('해당하는 아이디의 유저가 없습니다.'); + } + const comment = new Comment({ + question: questionId, + author: userId, + content: content + }); + await comment.save(); +}; + +const createPrizeEntry = async (userId?: Types.ObjectId) => { + if (!userId) { + throw new IllegalArgumentException('해당하는 아이디의 유저가 없습니다.'); + } + const prizeEntry = new PrizeEntry({ + user: userId + }); + await prizeEntry.save(); +}; + +export { findQuestionList, findCommentsList, createComment, createPrizeEntry }; From 3a25f72597141954ec2ca09223cf1764f1633086 Mon Sep 17 00:00:00 2001 From: donglee99 Date: Sun, 20 Aug 2023 20:57:28 +0900 Subject: [PATCH 18/27] =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20API=20nickname=20=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/intefaces/mind23/CommentDto.ts | 3 ++- src/services/mind23Service.ts | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/intefaces/mind23/CommentDto.ts b/src/intefaces/mind23/CommentDto.ts index 55e2bdd..0051782 100644 --- a/src/intefaces/mind23/CommentDto.ts +++ b/src/intefaces/mind23/CommentDto.ts @@ -3,5 +3,6 @@ import { Types } from 'mongoose'; export interface CommentDto { _id: Types.ObjectId; content: String; - profileImageUrl: string; + profileImageUrl: String; + nickname: String: } diff --git a/src/services/mind23Service.ts b/src/services/mind23Service.ts index cde1534..a37bc6a 100644 --- a/src/services/mind23Service.ts +++ b/src/services/mind23Service.ts @@ -6,18 +6,16 @@ import PrizeEntry, { PrizeEntryDocument } from '../models/mind23/prizeEntry'; import { CardResponseDto } from '../intefaces/CardResponseDto'; import { QuestionResponseDto } from '../intefaces/mind23/QuestionResponseDto'; import { CommentDto } from '../intefaces/mind23/CommentDto'; -import Card, { CardDocument } from '../models/card'; import { Types } from 'mongoose'; -import config from '../config'; import User from '../models/user/user'; import { QuestionDto } from '../intefaces/mind23/QuestionDto'; -const findProfileImageUrl = async (userId?: Types.ObjectId) => { +const findUserInfo = async (userId?: Types.ObjectId) => { const user = await User.findOne({ _id: userId }); if (!user) { throw new IllegalArgumentException('해당하는 아이디의 유저가 없습니다.'); } - return user.profileImageUrl; + return user; }; const createCardResponse = async ( @@ -36,10 +34,12 @@ const createCardResponse = async ( const createCommentResponse = async ( comment: CommentDocument ): Promise => { + const user = await findUserInfo(comment.author); return { _id: comment.author, content: comment.content, - profileImageUrl: await findProfileImageUrl(comment.author) + profileImageUrl: user.profileImageUrl, + nickname: user.nickname }; }; From ecee42464f3290ab244edc65cfae82edf1e2d8df Mon Sep 17 00:00:00 2001 From: donglee99 Date: Mon, 21 Aug 2023 12:08:25 +0900 Subject: [PATCH 19/27] =?UTF-8?q?=ED=95=84=EC=88=98=20=EC=A7=88=EB=AC=B8?= =?UTF-8?q?=20=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/intefaces/mind23/CommentDto.ts | 2 +- src/intefaces/mind23/Mind23CardResponseDto.ts | 11 +++++++++++ src/intefaces/mind23/QuestionResponseDto.ts | 4 ++-- src/models/mind23/question.ts | 4 +++- src/services/mind23Service.ts | 7 ++++--- 5 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 src/intefaces/mind23/Mind23CardResponseDto.ts diff --git a/src/intefaces/mind23/CommentDto.ts b/src/intefaces/mind23/CommentDto.ts index 0051782..395b1dc 100644 --- a/src/intefaces/mind23/CommentDto.ts +++ b/src/intefaces/mind23/CommentDto.ts @@ -4,5 +4,5 @@ export interface CommentDto { _id: Types.ObjectId; content: String; profileImageUrl: String; - nickname: String: + nickname: String; } diff --git a/src/intefaces/mind23/Mind23CardResponseDto.ts b/src/intefaces/mind23/Mind23CardResponseDto.ts new file mode 100644 index 0000000..4539bcf --- /dev/null +++ b/src/intefaces/mind23/Mind23CardResponseDto.ts @@ -0,0 +1,11 @@ +import { Types } from 'mongoose'; + +export interface Mind23CardResponseDto { + _id: Types.ObjectId; + content: string; + tags: string[]; + category: Types.ObjectId[]; + filter: string[]; + isBookmark?: boolean; + essential: Boolean; +} diff --git a/src/intefaces/mind23/QuestionResponseDto.ts b/src/intefaces/mind23/QuestionResponseDto.ts index c8d7ad1..ee0e078 100644 --- a/src/intefaces/mind23/QuestionResponseDto.ts +++ b/src/intefaces/mind23/QuestionResponseDto.ts @@ -1,7 +1,7 @@ import { Types } from 'mongoose'; -import { CardResponseDto } from '../CardResponseDto'; +import { Mind23CardResponseDto } from '../mind23/Mind23CardResponseDto'; export interface QuestionResponseDto { totalCount: Number; - cards: CardResponseDto[]; + cards: Mind23CardResponseDto[]; } diff --git a/src/models/mind23/question.ts b/src/models/mind23/question.ts index 8a07a42..31b4293 100644 --- a/src/models/mind23/question.ts +++ b/src/models/mind23/question.ts @@ -3,12 +3,14 @@ import IDocument from '../interface/Document'; interface IQuestion { content: string; + essential: Boolean; } type QuestionDocument = IQuestion & IDocument; const questionSchema = new Schema({ - content: String + content: String, + essential: Boolean }); const Question = model('Question', questionSchema); diff --git a/src/services/mind23Service.ts b/src/services/mind23Service.ts index a37bc6a..4e764d4 100644 --- a/src/services/mind23Service.ts +++ b/src/services/mind23Service.ts @@ -3,7 +3,7 @@ import Question, { QuestionDocument } from '../models/mind23/question'; import Comment, { CommentDocument } from '../models/mind23/comment'; import { IllegalArgumentException } from '../intefaces/exception'; import PrizeEntry, { PrizeEntryDocument } from '../models/mind23/prizeEntry'; -import { CardResponseDto } from '../intefaces/CardResponseDto'; +import { Mind23CardResponseDto } from '../intefaces/mind23/Mind23CardResponseDto'; import { QuestionResponseDto } from '../intefaces/mind23/QuestionResponseDto'; import { CommentDto } from '../intefaces/mind23/CommentDto'; import { Types } from 'mongoose'; @@ -20,14 +20,15 @@ const findUserInfo = async (userId?: Types.ObjectId) => { const createCardResponse = async ( question: QuestionDocument -): Promise => { +): Promise => { return { _id: question._id, content: question.content, tags: [], category: [], filter: [], - isBookmark: false + isBookmark: false, + essential: question.essential }; }; From 4cfd8ee4d02ac448a7a75b290e5f6b77148ab815 Mon Sep 17 00:00:00 2001 From: donglee99 Date: Mon, 21 Aug 2023 12:20:42 +0900 Subject: [PATCH 20/27] =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=20=EA=B0=92=208=EC=9E=90=EB=A1=9C=20=EC=A0=9C?= =?UTF-8?q?=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/userService.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/userService.ts b/src/services/userService.ts index 8f3f2ec..8357eb3 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -20,6 +20,8 @@ import PreUser from '../models/preUser'; import Card, { CardDocument } from '../models/card'; import QuitLog from '../models/quitLog'; +const NICKNAME_LIMIT = 8; + const isEmailExisting = async (email: string): Promise => { const alreadyUser = await User.findOne({ email @@ -94,8 +96,9 @@ const findUserById = async ( if (!user) { throw new IllegalArgumentException('존재하지 않는 유저 입니다.'); } + const splitedNickname = user.nickname.slice(0, NICKNAME_LIMIT); return { - nickname: user.nickname, + nickname: splitedNickname, email: user.email, profileImageUrl: user.profileImageUrl }; From a18316ee56b639dd519c0433c365055120209a52 Mon Sep 17 00:00:00 2001 From: donglee99 Date: Mon, 21 Aug 2023 12:24:41 +0900 Subject: [PATCH 21/27] =?UTF-8?q?type=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/mind23Service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/services/mind23Service.ts b/src/services/mind23Service.ts index 4e764d4..f49b563 100644 --- a/src/services/mind23Service.ts +++ b/src/services/mind23Service.ts @@ -46,7 +46,7 @@ const createCommentResponse = async ( const findQuestionList = async (): Promise => { const questions = await Question.find({}); - const totalCards: CardResponseDto[] = []; + const totalCards: Mind23CardResponseDto[] = []; for (const question of questions) { totalCards.push(await createCardResponse(question)); } @@ -63,7 +63,6 @@ const findCommentsList = async (questionId?: Types.ObjectId) => { question: questionId }) .sort({ createdAt: -1 }); - console.log(comments.length); const totalComments: CommentDto[] = []; for (const comment of comments) { totalComments.push(await createCommentResponse(comment)); From db1083ddc232822258166bfd4d06797b8e066848 Mon Sep 17 00:00:00 2001 From: kyY00n Date: Tue, 22 Aug 2023 17:38:28 +0900 Subject: [PATCH 22/27] =?UTF-8?q?[FEAT]=20=EB=B9=84=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=ED=94=BC=ED=81=B4=EB=AF=B8=20(#205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FIX] 카드 메들리 조회 시 카드 순서를 랜덤화 * [CHORE] 주석 삭제 * [FEAT] express-session, mongo-connect 설정 * [CHORE] express request 에 guestId 추가 * [CHORE] request 의 user 타입 변경 * [FEAT] 투표 관련 API flexibleAuth 와 guestHandler 일괄 추가 * [FEAT] 비회원 투표 조회 및 생성 기능 * [FIX] session credential 설정 (#157) * [FIX] 빌드 * [FIX] 투표 주제 조회 제외 * [FIX] 투표 리스트 조회 포함 * [CHORE] option 추가 * [FIX] cors 분기처리 * [FIX] session 설정 app에서 함 * [FIX] cookie option proxy true --- src/controllers/mind23Controller.ts | 4 +--- src/controllers/userController.ts | 1 - src/index.ts | 11 +++++----- src/loaders/session.ts | 16 -------------- src/middlewares/cors.ts | 16 ++++++++++++++ ...lexibleAuth.ts => optionalUserResolver.ts} | 0 src/middlewares/session/guestIdResolver.ts | 14 ++++++++++++ .../session/sessionConfiguration.ts | 22 +++++++++++++++++++ src/middlewares/{auth.ts => userResolver.ts} | 0 src/routes/CategoryRouter.ts | 6 ++--- src/routes/ballotRouter.ts | 1 + src/routes/cardMedleyRouter.ts | 8 +++++-- src/routes/cardRouter.ts | 12 +++++----- src/routes/index.ts | 6 ++--- src/routes/mind23Router.ts | 12 ++++++---- src/routes/userRouter/index.ts | 14 ++++++------ src/routes/userRouter/userCardRouter.ts | 6 ++--- src/services/mind23Service.ts | 21 ++++++++---------- src/types/express.d.ts | 1 + 19 files changed, 105 insertions(+), 66 deletions(-) delete mode 100644 src/loaders/session.ts create mode 100644 src/middlewares/cors.ts rename src/middlewares/{flexibleAuth.ts => optionalUserResolver.ts} (100%) create mode 100644 src/middlewares/session/guestIdResolver.ts create mode 100644 src/middlewares/session/sessionConfiguration.ts rename src/middlewares/{auth.ts => userResolver.ts} (100%) diff --git a/src/controllers/mind23Controller.ts b/src/controllers/mind23Controller.ts index daacd4e..04cbbac 100644 --- a/src/controllers/mind23Controller.ts +++ b/src/controllers/mind23Controller.ts @@ -4,9 +4,7 @@ import statusCode from '../modules/statusCode'; import util from '../modules/util'; import { Mind23Service } from '../services'; import { Types } from 'mongoose'; -import { QuestionDto } from '../intefaces/mind23/QuestionDto'; import { QuestionResponseDto } from '../intefaces/mind23/QuestionResponseDto'; -import { TypedRequest } from '../types/TypedRequest'; /** * @route /mind23/api/questions @@ -72,7 +70,7 @@ const createComment = async ( const questionId: Types.ObjectId | undefined = new Types.ObjectId( req.params.questionId ); - const comment: String = req.body.content; + const comment: string = req.body.content; await Mind23Service.createComment(userId, questionId, comment); return res .status(statusCode.OK) diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index a635cb9..c159a35 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -536,7 +536,6 @@ export { readEmailIsExisting, socialLogin, postUser, - // patchUser, postUserLogin, getUserProfile, updateUserNickname, diff --git a/src/index.ts b/src/index.ts index 832af56..3a0a025 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,20 +2,19 @@ import express from 'express'; import connectDB from './loaders/db'; import routes from './routes'; import helmet from 'helmet'; -import config from './config'; import errorHandler from './middlewares/errorHandler'; -import cors from 'cors'; -import expressSession from './loaders/session'; import * as SentryConfig from './loaders/sentryConfiguration'; +import corsMiddleware from './middlewares/cors'; +import config from './config'; +import sessionConfiguration from './middlewares/session/sessionConfiguration'; const app = express(); -app.use(cors()); app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.use(helmet()); -app.use(expressSession); +app.use(corsMiddleware); +app.use(sessionConfiguration); SentryConfig.initializeSentry(app); - app.use(routes); SentryConfig.attachSentryErrorHandler(app); app.use(errorHandler); diff --git a/src/loaders/session.ts b/src/loaders/session.ts deleted file mode 100644 index 0ce3031..0000000 --- a/src/loaders/session.ts +++ /dev/null @@ -1,16 +0,0 @@ -import express from 'express'; -import session from 'express-session'; -import config from '../config'; -import MongoStore from 'connect-mongo'; - -const expressSession: express.RequestHandler = session({ - secret: config.sessionKey, - store: MongoStore.create({ - mongoUrl: config.mongoURI - }), - cookie: { maxAge: 3.6e6 * 24 * 180 }, - resave: false, - saveUninitialized: false -}); - -export default expressSession; diff --git a/src/middlewares/cors.ts b/src/middlewares/cors.ts new file mode 100644 index 0000000..0c8f314 --- /dev/null +++ b/src/middlewares/cors.ts @@ -0,0 +1,16 @@ +import { Request, Response, NextFunction } from 'express'; +import cors from 'cors'; + +const cookieCors = cors({ + origin: 'https://client-wheat-three.vercel.app', + credentials: true +}); + +const corsMiddleware = (req: Request, res: Response, next: NextFunction) => { + if (req.path.match(/^\/ballots/)) { + return cookieCors(req, res, next); + } + return cors()(req, res, next); +}; + +export default corsMiddleware; diff --git a/src/middlewares/flexibleAuth.ts b/src/middlewares/optionalUserResolver.ts similarity index 100% rename from src/middlewares/flexibleAuth.ts rename to src/middlewares/optionalUserResolver.ts diff --git a/src/middlewares/session/guestIdResolver.ts b/src/middlewares/session/guestIdResolver.ts new file mode 100644 index 0000000..932ef71 --- /dev/null +++ b/src/middlewares/session/guestIdResolver.ts @@ -0,0 +1,14 @@ +import { NextFunction, Request, Response } from 'express'; + +const guestIdResolver = (req: Request, res: Response, next: NextFunction) => { + if (!req.user) { + if (!req.session.uid) { + req.session.uid = req.sessionID; + req.session.save(); + } + req.guestId = req.session.uid; + } + next(); +}; + +export default guestIdResolver; diff --git a/src/middlewares/session/sessionConfiguration.ts b/src/middlewares/session/sessionConfiguration.ts new file mode 100644 index 0000000..a2ab2e2 --- /dev/null +++ b/src/middlewares/session/sessionConfiguration.ts @@ -0,0 +1,22 @@ +import session from 'express-session'; +import MongoStore from 'connect-mongo'; +import config from '../../config'; +import { RequestHandler } from 'express'; + +const sessionConfiguration: RequestHandler = session({ + secret: config.sessionKey, + store: MongoStore.create({ + mongoUrl: config.mongoURI + }), + cookie: { + sameSite: 'none', + httpOnly: true, + secure: true, + maxAge: 3.6e6 * 24 * 180 + }, + resave: false, + proxy: true, + saveUninitialized: false +}); + +export default sessionConfiguration; diff --git a/src/middlewares/auth.ts b/src/middlewares/userResolver.ts similarity index 100% rename from src/middlewares/auth.ts rename to src/middlewares/userResolver.ts diff --git a/src/routes/CategoryRouter.ts b/src/routes/CategoryRouter.ts index 2235cc0..fef3f2c 100644 --- a/src/routes/CategoryRouter.ts +++ b/src/routes/CategoryRouter.ts @@ -1,11 +1,11 @@ import { Router } from 'express'; import { CategoryController } from '../controllers'; -import flexibleAuth from '../middlewares/flexibleAuth'; +import optionalUserResolver from '../middlewares/optionalUserResolver'; const router: Router = Router(); router.get('/', CategoryController.getCategory); -router.get('/cards', flexibleAuth, CategoryController.getCardsBySearch); -router.get('/:categoryId', flexibleAuth, CategoryController.getCards); +router.get('/cards', optionalUserResolver, CategoryController.getCardsBySearch); +router.get('/:categoryId', optionalUserResolver, CategoryController.getCards); export default router; diff --git a/src/routes/ballotRouter.ts b/src/routes/ballotRouter.ts index ae141f1..8067514 100644 --- a/src/routes/ballotRouter.ts +++ b/src/routes/ballotRouter.ts @@ -11,6 +11,7 @@ router.post( ); router.get('/:ballotTopicId', BallotController.getBallotStatus); + router.get('', BallotController.getMainBallotList); export default router; diff --git a/src/routes/cardMedleyRouter.ts b/src/routes/cardMedleyRouter.ts index 5bf1c9e..bae7f13 100644 --- a/src/routes/cardMedleyRouter.ts +++ b/src/routes/cardMedleyRouter.ts @@ -1,10 +1,14 @@ import { Router } from 'express'; import { CardMedleyController } from '../controllers'; -import flexibleAuth from '../middlewares/flexibleAuth'; +import optionalUserResolver from '../middlewares/optionalUserResolver'; const router: Router = Router(); router.get('/', CardMedleyController.getAllMedleyPreview); -router.get('/:medleyId', flexibleAuth, CardMedleyController.getCardMedleyById); +router.get( + '/:medleyId', + optionalUserResolver, + CardMedleyController.getCardMedleyById +); export default router; diff --git a/src/routes/cardRouter.ts b/src/routes/cardRouter.ts index 8a013fe..c0e89a7 100644 --- a/src/routes/cardRouter.ts +++ b/src/routes/cardRouter.ts @@ -1,24 +1,24 @@ import { Router } from 'express'; import { CardController } from '../controllers'; -import flexibleAuth from '../middlewares/flexibleAuth'; +import optionalUserResolver from '../middlewares/optionalUserResolver'; const router = Router(); router.get( '/cardByBookmarkedGender/:gender', - flexibleAuth, + optionalUserResolver, CardController.getCardByBookmarkedGender ); router.get( '/recentlyBookmarkedCard', - flexibleAuth, + optionalUserResolver, CardController.getRecentlyBookmarkedCard ); router.get( '/recentlyUpdatedCard', - flexibleAuth, + optionalUserResolver, CardController.getRecentlyUpdatedCard ); -router.get('/best', flexibleAuth, CardController.getBestCardList); -router.get('/:cardId', flexibleAuth, CardController.getCards); +router.get('/best', optionalUserResolver, CardController.getBestCardList); +router.get('/:cardId', optionalUserResolver, CardController.getCards); export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts index fa4fb0a..7fd64e5 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -7,14 +7,14 @@ import CardRouter from './cardRouter'; import TestRouter from './testRouter'; import Mind23Router from './mind23Router'; import CardMedleyRouter from './cardMedleyRouter'; -import flexibleAuth from '../middlewares/flexibleAuth'; -import guestHandler from '../middlewares/guestHandler'; +import optionalUserResolver from '../middlewares/optionalUserResolver'; +import guestIdResolver from '../middlewares/session/guestIdResolver'; const router = Router(); router.use('/users', UserRouter); router.use('/categories', CategoryRouter); -router.use('/ballots', flexibleAuth, guestHandler, BallotRouter); +router.use('/ballots', optionalUserResolver, guestIdResolver, BallotRouter); router.use('/cards', CardRouter); router.use('/test', TestRouter); router.use('/medley', CardMedleyRouter); diff --git a/src/routes/mind23Router.ts b/src/routes/mind23Router.ts index 87f9cb2..82b50f1 100644 --- a/src/routes/mind23Router.ts +++ b/src/routes/mind23Router.ts @@ -1,20 +1,24 @@ import { Router } from 'express'; import { Mind23Controller } from '../controllers'; -import flexibleAuth from '../middlewares/flexibleAuth'; +import optionalUserResolver from '../middlewares/optionalUserResolver'; const router: Router = Router(); router.get('/questions', Mind23Controller.getQuestionList); router.post( '/comments/:questionId', - flexibleAuth, + optionalUserResolver, Mind23Controller.createComment ); router.get( '/comments/:questionId', - flexibleAuth, + optionalUserResolver, Mind23Controller.getCommentList ); -router.post('/prize-entry', flexibleAuth, Mind23Controller.createPrizeEntry); +router.post( + '/prize-entry', + optionalUserResolver, + Mind23Controller.createPrizeEntry +); export default router; diff --git a/src/routes/userRouter/index.ts b/src/routes/userRouter/index.ts index 33e5546..026ae26 100644 --- a/src/routes/userRouter/index.ts +++ b/src/routes/userRouter/index.ts @@ -1,7 +1,7 @@ import { Router } from 'express'; import { body } from 'express-validator'; import { UserController } from '../../controllers'; -import auth from '../../middlewares/auth'; +import userResolver from '../../middlewares/userResolver'; import upload from '../../config/multer'; import userCardRouter from './userCardRouter'; @@ -38,30 +38,30 @@ router.post( UserController.postUserLogin ); -router.get('', auth, UserController.getUserProfile); -router.get('/bookmarks', auth, UserController.getBookmarks); +router.get('', userResolver, UserController.getUserProfile); +router.get('/bookmarks', userResolver, UserController.getBookmarks); router.put( '/bookmarks', - auth, + userResolver, [body('cardId').notEmpty()], UserController.createdeleteBookmark ); router.patch( '/profile-image', - auth, + userResolver, upload.single('file'), UserController.updateUserProfileImage ); router.patch( '/nickname', - auth, + userResolver, [body('nickname').notEmpty()], UserController.updateUserNickname ); -router.put('/me', auth, UserController.deleteUser); +router.put('/me', userResolver, UserController.deleteUser); export default router; diff --git a/src/routes/userRouter/userCardRouter.ts b/src/routes/userRouter/userCardRouter.ts index d734dc7..08fe88c 100644 --- a/src/routes/userRouter/userCardRouter.ts +++ b/src/routes/userRouter/userCardRouter.ts @@ -1,9 +1,9 @@ import { Router } from 'express'; import { UserController } from '../../controllers'; -import auth from '../../middlewares/auth'; +import userResolver from '../../middlewares/userResolver'; const router = Router(); -router.post('/blacklist', auth, UserController.blockCard); -router.delete('/blacklist/:cardId', auth, UserController.cancelToBlock); +router.post('/blacklist', userResolver, UserController.blockCard); +router.delete('/blacklist/:cardId', userResolver, UserController.cancelToBlock); export default router; diff --git a/src/services/mind23Service.ts b/src/services/mind23Service.ts index f49b563..b0b888e 100644 --- a/src/services/mind23Service.ts +++ b/src/services/mind23Service.ts @@ -1,14 +1,12 @@ -import util from '../modules/util'; import Question, { QuestionDocument } from '../models/mind23/question'; import Comment, { CommentDocument } from '../models/mind23/comment'; import { IllegalArgumentException } from '../intefaces/exception'; -import PrizeEntry, { PrizeEntryDocument } from '../models/mind23/prizeEntry'; +import PrizeEntry from '../models/mind23/prizeEntry'; import { Mind23CardResponseDto } from '../intefaces/mind23/Mind23CardResponseDto'; import { QuestionResponseDto } from '../intefaces/mind23/QuestionResponseDto'; import { CommentDto } from '../intefaces/mind23/CommentDto'; import { Types } from 'mongoose'; import User from '../models/user/user'; -import { QuestionDto } from '../intefaces/mind23/QuestionDto'; const findUserInfo = async (userId?: Types.ObjectId) => { const user = await User.findOne({ _id: userId }); @@ -18,9 +16,9 @@ const findUserInfo = async (userId?: Types.ObjectId) => { return user; }; -const createCardResponse = async ( +const createCardResponse = ( question: QuestionDocument -): Promise => { +): Mind23CardResponseDto => { return { _id: question._id, content: question.content, @@ -46,10 +44,9 @@ const createCommentResponse = async ( const findQuestionList = async (): Promise => { const questions = await Question.find({}); - const totalCards: Mind23CardResponseDto[] = []; - for (const question of questions) { - totalCards.push(await createCardResponse(question)); - } + const totalCards: Mind23CardResponseDto[] = questions.map(question => + createCardResponse(question) + ); const count = await PrizeEntry.find({}).count(); return { totalCount: count, @@ -58,11 +55,11 @@ const findQuestionList = async (): Promise => { }; const findCommentsList = async (questionId?: Types.ObjectId) => { - const comments = await Comment.aggregate() + const comments = (await Comment.aggregate() .match({ question: questionId }) - .sort({ createdAt: -1 }); + .sort({ createdAt: -1 })) as CommentDocument[]; const totalComments: CommentDto[] = []; for (const comment of comments) { totalComments.push(await createCommentResponse(comment)); @@ -73,7 +70,7 @@ const findCommentsList = async (questionId?: Types.ObjectId) => { const createComment = async ( userId?: Types.ObjectId, questionId?: Types.ObjectId, - content?: String + content?: string ) => { if (!userId) { throw new IllegalArgumentException('해당하는 아이디의 유저가 없습니다.'); diff --git a/src/types/express.d.ts b/src/types/express.d.ts index 68d0e11..8bd4cd5 100644 --- a/src/types/express.d.ts +++ b/src/types/express.d.ts @@ -15,6 +15,7 @@ declare global { declare module 'express-session' { interface Session { + uid: string; isGuest: boolean; } } From 503c274a1965ddea30d293737f5038baaeb74e13 Mon Sep 17 00:00:00 2001 From: kyY00n Date: Tue, 22 Aug 2023 21:49:46 +0900 Subject: [PATCH 23/27] =?UTF-8?q?=ED=97=AC=EC=8A=A4=EC=B2=B4=ED=81=AC=20?= =?UTF-8?q?=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/routes/index.ts b/src/routes/index.ts index 7fd64e5..2751cc1 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -19,4 +19,7 @@ router.use('/cards', CardRouter); router.use('/test', TestRouter); router.use('/medley', CardMedleyRouter); router.use('/mind23/api', Mind23Router); +router.use('/health', (req, res) => { + res.status(200).send('ok'); +}); export default router; From 6e2a301a7cf7e61231bb446eef58a8580a07671e Mon Sep 17 00:00:00 2001 From: kyY00n Date: Wed, 23 Aug 2023 17:44:38 +0900 Subject: [PATCH 24/27] =?UTF-8?q?fix:=20origin=20=EC=88=98=EC=A0=95=20(#20?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/index.ts | 11 ++++++++++- src/middlewares/cors.ts | 3 ++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/config/index.ts b/src/config/index.ts index f5a3719..c0b7a98 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -12,6 +12,11 @@ const MONGO_URI = ? process.env.MONGODB_URI : process.env.MONGODB_DEVELOPMENT_URI; +const WEB_APP_URL = + NODE_ENV == 'production' + ? process.env.WEB_APP_URL + : process.env.WEB_APP_DEV_URL; + export default { nodeEnv: NODE_ENV, port: parseInt(process.env.PORT as string, 10), @@ -59,5 +64,9 @@ export default { clientKey: process.env.CLIENT_KEY as string, naverClientId: process.env.NAVER_CLIENT_ID as string, naverClientSecret: process.env.NAVER_CLIENT_SECRET as string, - sentryDsn: process.env.SENTRY_DSN as string + sentryDsn: process.env.SENTRY_DSN as string, + /** + * piickle service application url + */ + webAppUrl: WEB_APP_URL as string }; diff --git a/src/middlewares/cors.ts b/src/middlewares/cors.ts index 0c8f314..92b04c2 100644 --- a/src/middlewares/cors.ts +++ b/src/middlewares/cors.ts @@ -1,8 +1,9 @@ import { Request, Response, NextFunction } from 'express'; import cors from 'cors'; +import config from '../config'; const cookieCors = cors({ - origin: 'https://client-wheat-three.vercel.app', + origin: config.webAppUrl, credentials: true }); From 1b0c263f76474c259235e0fe81102ab5c31a0ca9 Mon Sep 17 00:00:00 2001 From: donglee99 Date: Fri, 25 Aug 2023 22:48:31 +0900 Subject: [PATCH 25/27] =?UTF-8?q?=EB=8B=B9=EC=9D=BC=EC=9A=A9=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EB=8D=95=ED=8A=B8=20=EB=8C=93=EA=B8=80=20=EB=B3=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=97=AC=EB=B6=80,=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EB=94=94=20=EC=A4=91=EB=B3=B5,=20=EA=B2=BD?= =?UTF-8?q?=ED=92=88=20=EC=9D=91=EB=AA=A8=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/mind23Controller.ts | 12 ++++++---- src/intefaces/mind23/CommentDto.ts | 2 ++ src/models/mind23/prizeEntry.ts | 4 +++- src/services/mind23Service.ts | 36 +++++++++++++++++++++++------ 4 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/controllers/mind23Controller.ts b/src/controllers/mind23Controller.ts index 04cbbac..124a10e 100644 --- a/src/controllers/mind23Controller.ts +++ b/src/controllers/mind23Controller.ts @@ -4,7 +4,9 @@ import statusCode from '../modules/statusCode'; import util from '../modules/util'; import { Mind23Service } from '../services'; import { Types } from 'mongoose'; +import { QuestionDto } from '../intefaces/mind23/QuestionDto'; import { QuestionResponseDto } from '../intefaces/mind23/QuestionResponseDto'; +import { TypedRequest } from '../types/TypedRequest'; /** * @route /mind23/api/questions @@ -29,7 +31,7 @@ const getQuestionList = async ( }; /** - * @route /mind23/api/comments + * @route /mind23/api/comments/:questionId * @desc mind23 용 댓글을 조회합니다. * @access Public */ @@ -39,8 +41,9 @@ const getCommentList = async ( next: NextFunction ) => { try { + const userId: Types.ObjectId | undefined = req.user?.id; const questionId = new Types.ObjectId(req.params.questionId); - const comments = await Mind23Service.findCommentsList(questionId); + const comments = await Mind23Service.findCommentsList(userId, questionId); return res .status(statusCode.OK) .send( @@ -56,7 +59,7 @@ const getCommentList = async ( }; /** - * @route /mind23/api/comments?questionId= + * @route /mind23/api/comments/:questionId * @desc mind23 용 댓글을 등록합니다. * @access Public */ @@ -92,7 +95,8 @@ const createPrizeEntry = async ( ) => { try { const userId: Types.ObjectId | undefined = req.user?.id; - await Mind23Service.createPrizeEntry(userId); + const prizeEntryStatus: Boolean = req.body.prizeEntryStatus; + await Mind23Service.createPrizeEntry(userId, prizeEntryStatus); return res .status(statusCode.OK) .send( diff --git a/src/intefaces/mind23/CommentDto.ts b/src/intefaces/mind23/CommentDto.ts index 395b1dc..e216b47 100644 --- a/src/intefaces/mind23/CommentDto.ts +++ b/src/intefaces/mind23/CommentDto.ts @@ -2,7 +2,9 @@ import { Types } from 'mongoose'; export interface CommentDto { _id: Types.ObjectId; + authorId: Types.ObjectId; content: String; profileImageUrl: String; nickname: String; + commentStatus: Boolean; } diff --git a/src/models/mind23/prizeEntry.ts b/src/models/mind23/prizeEntry.ts index b6900e4..a52c61a 100644 --- a/src/models/mind23/prizeEntry.ts +++ b/src/models/mind23/prizeEntry.ts @@ -3,6 +3,7 @@ import IDocument from '../interface/Document'; interface IPrizeEntry { user: Schema.Types.ObjectId; + prizeEntryStatus: Boolean; } type PrizeEntryDocument = IPrizeEntry & IDocument; @@ -12,7 +13,8 @@ const prizeEntrySchema = new Schema( user: { type: Schema.Types.ObjectId, ref: 'User' - } + }, + prizeEntryStatus: Boolean }, { timestamps: true diff --git a/src/services/mind23Service.ts b/src/services/mind23Service.ts index b0b888e..8d122e6 100644 --- a/src/services/mind23Service.ts +++ b/src/services/mind23Service.ts @@ -8,6 +8,7 @@ import { CommentDto } from '../intefaces/mind23/CommentDto'; import { Types } from 'mongoose'; import User from '../models/user/user'; +const NICKNAME_LIMIT = 8; const findUserInfo = async (userId?: Types.ObjectId) => { const user = await User.findOne({ _id: userId }); if (!user) { @@ -30,15 +31,28 @@ const createCardResponse = ( }; }; +const checkCommentStatus = ( + authorId: Types.ObjectId, + userId?: Types.ObjectId +): Boolean => { + if (!userId) { + return false; + } + return userId.toString() === authorId.toString(); +}; + const createCommentResponse = async ( - comment: CommentDocument + comment: CommentDocument, + userId?: Types.ObjectId ): Promise => { const user = await findUserInfo(comment.author); return { - _id: comment.author, + _id: comment._id, + authorId: comment.author, content: comment.content, profileImageUrl: user.profileImageUrl, - nickname: user.nickname + nickname: user.nickname.slice(0, NICKNAME_LIMIT), + commentStatus: checkCommentStatus(comment.author, userId) }; }; @@ -54,7 +68,10 @@ const findQuestionList = async (): Promise => { }; }; -const findCommentsList = async (questionId?: Types.ObjectId) => { +const findCommentsList = async ( + userId?: Types.ObjectId, + questionId?: Types.ObjectId +) => { const comments = (await Comment.aggregate() .match({ question: questionId @@ -62,7 +79,7 @@ const findCommentsList = async (questionId?: Types.ObjectId) => { .sort({ createdAt: -1 })) as CommentDocument[]; const totalComments: CommentDto[] = []; for (const comment of comments) { - totalComments.push(await createCommentResponse(comment)); + totalComments.push(await createCommentResponse(comment, userId)); } return totalComments; }; @@ -83,12 +100,17 @@ const createComment = async ( await comment.save(); }; -const createPrizeEntry = async (userId?: Types.ObjectId) => { +const createPrizeEntry = async ( + userId?: Types.ObjectId, + prizeEntryStatus?: Boolean +) => { if (!userId) { throw new IllegalArgumentException('해당하는 아이디의 유저가 없습니다.'); } + console.log(prizeEntryStatus); const prizeEntry = new PrizeEntry({ - user: userId + user: userId, + prizeEntryStatus: prizeEntryStatus }); await prizeEntry.save(); }; From 1931abdf37fc653ef5feb8f6a198fc44f66df80d Mon Sep 17 00:00:00 2001 From: donglee99 Date: Fri, 25 Aug 2023 23:06:32 +0900 Subject: [PATCH 26/27] =?UTF-8?q?=EC=A7=88=EB=AC=B8=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=8B=9C=20Essentional=20=EA=B0=92=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/intefaces/mind23/Mind23CardResponseDto.ts | 1 - src/services/mind23Service.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/intefaces/mind23/Mind23CardResponseDto.ts b/src/intefaces/mind23/Mind23CardResponseDto.ts index 4539bcf..31bb5b4 100644 --- a/src/intefaces/mind23/Mind23CardResponseDto.ts +++ b/src/intefaces/mind23/Mind23CardResponseDto.ts @@ -7,5 +7,4 @@ export interface Mind23CardResponseDto { category: Types.ObjectId[]; filter: string[]; isBookmark?: boolean; - essential: Boolean; } diff --git a/src/services/mind23Service.ts b/src/services/mind23Service.ts index b0b888e..439a40c 100644 --- a/src/services/mind23Service.ts +++ b/src/services/mind23Service.ts @@ -25,8 +25,7 @@ const createCardResponse = ( tags: [], category: [], filter: [], - isBookmark: false, - essential: question.essential + isBookmark: false }; }; From 32cacac5e4864ccd9b1ea4e4ceb4de1973b5a9f2 Mon Sep 17 00:00:00 2001 From: kyY00n Date: Sat, 26 Aug 2023 22:08:33 +0900 Subject: [PATCH 27/27] =?UTF-8?q?[FEAT]=20=EC=A7=88=EB=AC=B8=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=EC=97=90=20=ED=83=9C=EA=B7=B8=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#215)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 질문 모델에 태그 추가 * feat: dto 변환 로직에 추가 * fix: question 모델에서 필수여부필드 제거 --- src/controllers/mind23Controller.ts | 7 +++---- src/models/mind23/question.ts | 4 ++-- src/services/mind23Service.ts | 7 +++---- src/types/TypedRequest.ts | 4 ++-- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/controllers/mind23Controller.ts b/src/controllers/mind23Controller.ts index 124a10e..47cb5ac 100644 --- a/src/controllers/mind23Controller.ts +++ b/src/controllers/mind23Controller.ts @@ -4,7 +4,6 @@ import statusCode from '../modules/statusCode'; import util from '../modules/util'; import { Mind23Service } from '../services'; import { Types } from 'mongoose'; -import { QuestionDto } from '../intefaces/mind23/QuestionDto'; import { QuestionResponseDto } from '../intefaces/mind23/QuestionResponseDto'; import { TypedRequest } from '../types/TypedRequest'; @@ -64,7 +63,7 @@ const getCommentList = async ( * @access Public */ const createComment = async ( - req: Request, + req: TypedRequest<{ content: string }, any, { questionId: string }>, res: Response, next: NextFunction ) => { @@ -89,13 +88,13 @@ const createComment = async ( * @access Public */ const createPrizeEntry = async ( - req: Request, + req: TypedRequest<{ prizeEntryStatus: boolean }>, res: Response, next: NextFunction ) => { try { const userId: Types.ObjectId | undefined = req.user?.id; - const prizeEntryStatus: Boolean = req.body.prizeEntryStatus; + const prizeEntryStatus: boolean = req.body.prizeEntryStatus; await Mind23Service.createPrizeEntry(userId, prizeEntryStatus); return res .status(statusCode.OK) diff --git a/src/models/mind23/question.ts b/src/models/mind23/question.ts index 31b4293..dcc7568 100644 --- a/src/models/mind23/question.ts +++ b/src/models/mind23/question.ts @@ -3,14 +3,14 @@ import IDocument from '../interface/Document'; interface IQuestion { content: string; - essential: Boolean; + tags: string[]; } type QuestionDocument = IQuestion & IDocument; const questionSchema = new Schema({ content: String, - essential: Boolean + tags: [String] }); const Question = model('Question', questionSchema); diff --git a/src/services/mind23Service.ts b/src/services/mind23Service.ts index d885159..c3f4901 100644 --- a/src/services/mind23Service.ts +++ b/src/services/mind23Service.ts @@ -23,7 +23,7 @@ const createCardResponse = ( return { _id: question._id, content: question.content, - tags: [], + tags: question.tags, category: [], filter: [], isBookmark: false @@ -33,7 +33,7 @@ const createCardResponse = ( const checkCommentStatus = ( authorId: Types.ObjectId, userId?: Types.ObjectId -): Boolean => { +): boolean => { if (!userId) { return false; } @@ -101,12 +101,11 @@ const createComment = async ( const createPrizeEntry = async ( userId?: Types.ObjectId, - prizeEntryStatus?: Boolean + prizeEntryStatus?: boolean ) => { if (!userId) { throw new IllegalArgumentException('해당하는 아이디의 유저가 없습니다.'); } - console.log(prizeEntryStatus); const prizeEntry = new PrizeEntry({ user: userId, prizeEntryStatus: prizeEntryStatus diff --git a/src/types/TypedRequest.ts b/src/types/TypedRequest.ts index 35981c3..f350d60 100644 --- a/src/types/TypedRequest.ts +++ b/src/types/TypedRequest.ts @@ -1,7 +1,7 @@ import { Request } from 'express'; -export type TypedRequest = Request< - any, +export type TypedRequest = Request< + P, any, ReqB, ReqQ